local: Treat dots/periods as digits in destructuring directories.
[automanga.git] / manga / profile.py
1 import os
2 pj = os.path.join
3
4 home = os.getenv("HOME")
5 if home is None or not os.path.isdir(home):
6     raise Exception("Could not find home directory for profile keeping")
7 confdir = pj(home, ".manga")
8 basedir = pj(confdir, "profiles")
9
10 class txfile(object):
11     def __init__(self, name, mode):
12         self.realname = name
13         self.tempname = name + ".new"
14         self.bk = open(self.tempname, mode)
15
16     def close(self, abort=False):
17         self.bk.close()
18         if abort:
19             os.unlink(self.tempname)
20         else:
21             os.rename(self.tempname, self.realname)
22
23     def read(self, sz=-1):
24         return self.bk.read(sz)
25
26     def write(self, data):
27         return self.bk.write(data)
28
29     def __enter__(self):
30         return self
31
32     def __exit__(self, *exc_info):
33         if exc_info[0] is not None:
34             self.close(True)
35         else:
36             self.close(False)
37
38 def openwdir(nm, mode="r"):
39     ft = open
40     if mode == "W":
41         mode = "w"
42         ft = txfile
43     if os.path.exists(nm):
44         return ft(nm, mode)
45     if mode != "r":
46         d = os.path.dirname(nm)
47         if not os.path.isdir(d):
48             os.makedirs(d)
49     return ft(nm, mode)
50
51 def splitline(line):
52     def bsq(c):
53         if c == "\\": return "\\"
54         elif c == '"': return '"'
55         elif c == " ": return " "
56         elif c == "n": return "\n"
57         else: return ""
58     ret = []
59     p = 0
60     buf = ""
61     a = False
62     while p < len(line):
63         c = line[p]
64         if c.isspace():
65             p += 1
66         else:
67             while p < len(line):
68                 c = line[p]
69                 p += 1
70                 if c == '"':
71                     a = True
72                     while p < len(line):
73                         c = line[p]
74                         p += 1
75                         if c == '"':
76                             break
77                         elif c == "\\" and p < len(line):
78                             buf += bsq(line[p])
79                             p += 1
80                         else:
81                             buf += c
82                 elif c.isspace():
83                     ret.append(buf)
84                     buf = ""
85                     a = False
86                     break
87                 elif c == "\\" and p < len(line):
88                     buf += bsq(line[p])
89                     p += 1
90                 else:
91                     buf += c
92     if a or buf != "":
93         ret.append(buf)
94     return ret
95
96 def splitlines(fp):
97     for line in fp:
98         cur = splitline(line)
99         if len(cur) < 1:
100             continue
101         yield cur
102
103 def consline(*words):
104     buf = ""
105     for w in words:
106         if any((c == "\\" or c == '"' or c == "\n" for c in w)):
107             wb = ""
108             for c in w:
109                 if c == "\\": wb += "\\\\"
110                 elif c == '"': wb += '\\"'
111                 elif c == "\n": wb += "\\n"
112                 else: wb += c
113             w = wb
114         if w == "" or any((c.isspace() for c in w)):
115             w = '"' + w + '"'
116         if buf != "":
117             buf += " "
118         buf += w
119     return buf
120
121 class manga(object):
122     def __init__(self, profile, libnm, id):
123         self.profile = profile
124         self.libnm = libnm
125         self.id = id
126         self.props = self.loadprops()
127
128     def open(self):
129         from . import lib
130         return lib.findlib(self.libnm).byid(self.id)
131
132     def save(self):
133         pass
134
135 class memmanga(manga):
136     def __init__(self, profile, libnm, id):
137         super(memmanga, self).__init__(profile, libnm, id)
138
139     def loadprops(self):
140         return {}
141
142 class tagview(object):
143     def __init__(self, manga):
144         self.manga = manga
145         self.profile = manga.profile
146
147     def add(self, *tags):
148         mt = self.getall(self.profile)
149         ctags = mt.setdefault((self.manga.libnm, self.manga.id), set())
150         ctags |= set(tags)
151         self.save(self.profile, mt)
152
153     def remove(self, *tags):
154         mt = self.getall(self.profile)
155         ctags = mt.get((self.manga.libnm, self.manga.id), set())
156         ctags -= set(tags)
157         if len(ctags) < 1:
158             try:
159                 del mt[self.manga.libnm, self.manga.id]
160             except KeyError:
161                 pass
162         self.save(self.profile, mt)
163
164     def __iter__(self):
165         return iter(self.getall(self.profile).get((self.manga.libnm, self.manga.id), set()))
166
167     @staticmethod
168     def getall(profile):
169         ret = {}
170         try:
171             with profile.file("tags") as fp:
172                 for words in splitlines(fp):
173                     libnm, id = words[0:2]
174                     tags = set(words[2:])
175                     ret[libnm, id] = tags
176         except IOError:
177             pass
178         return ret
179
180     @staticmethod
181     def save(profile, m):
182         with profile.file("tags", "W") as fp:
183             for (libnm, id), tags in m.items():
184                 fp.write(consline(libnm, id, *tags) + "\n")
185
186     @staticmethod
187     def bytag(profile, tag):
188         try:
189             with profile.file("tags") as fp:
190                 for words in splitlines(fp):
191                     libnm, id = words[0:2]
192                     tags = words[2:]
193                     if tag in tags:
194                         yield profile.getmanga(libnm, id)
195         except IOError:
196             pass
197
198 class filemanga(manga):
199     def __init__(self, profile, libnm, id, path):
200         self.path = path
201         super(filemanga, self).__init__(profile, libnm, id)
202         self.tags = tagview(self)
203
204     def loadprops(self):
205         ret = {}
206         with openwdir(self.path) as f:
207             for words in splitlines(f):
208                 if words[0] == "set" and len(words) > 2:
209                     ret[words[1]] = words[2]
210                 elif words[0] == "lset" and len(words) > 1:
211                     ret[words[1]] = words[2:]
212         return ret
213
214     def save(self):
215         with openwdir(self.path, "W") as f:
216             for key, val in self.props.items():
217                 if isinstance(val, str):
218                     f.write(consline("set", key, val) + "\n")
219                 else:
220                     f.write(consline("lset", key, *val) + "\n")
221
222 class profile(object):
223     def __init__(self, dir):
224         self.dir = dir
225         self.name = None
226
227     def getmapping(self):
228         seq = 0
229         ret = {}
230         if os.path.exists(pj(self.dir, "map")):
231             with openwdir(pj(self.dir, "map")) as f:
232                 for words in splitlines(f):
233                     if words[0] == "seq" and len(words) > 1:
234                         try:
235                             seq = int(words[1])
236                         except ValueError:
237                             pass
238                     elif words[0] == "manga" and len(words) > 3:
239                         try:
240                             ret[words[1], words[2]] = int(words[3])
241                         except ValueError:
242                             pass
243         return seq, ret
244
245     def savemapping(self, seq, m):
246         with openwdir(pj(self.dir, "map"), "W") as f:
247             f.write(consline("seq", str(seq)) + "\n")
248             for (libnm, id), num in m.items():
249                 f.write(consline("manga", libnm, id, str(num)) + "\n")
250
251     def getmanga(self, libnm, id, creat=False):
252         seq, m = self.getmapping()
253         if (libnm, id) in m:
254             return filemanga(self, libnm, id, pj(self.dir, "%i.manga" % m[(libnm, id)]))
255         if not creat:
256             raise KeyError("no such manga: (%s, %s)" % (libnm, id))
257         while True:
258             try:
259                 fp = openwdir(pj(self.dir, "%i.manga" % seq), "x")
260             except IOError:
261                 seq += 1
262             else:
263                 break
264         fp.close()
265         m[(libnm, id)] = seq
266         self.savemapping(seq, m)
267         return filemanga(self, libnm, id, pj(self.dir, "%i.manga" % seq))
268
269     def setlast(self):
270         if self.name is None:
271             raise ValueError("profile at " + self.dir + " has no name")
272         with openwdir(pj(basedir, "last"), "W") as f:
273             f.write(self.name + "\n")
274
275     def getaliases(self):
276         ret = {}
277         if os.path.exists(pj(self.dir, "alias")):
278             with openwdir(pj(self.dir, "alias")) as f:
279                 for ln in f:
280                     ln = splitline(ln)
281                     if len(ln) < 1: continue
282                     if ln[0] == "alias" and len(ln) > 3:
283                         ret[ln[1]] = ln[2], ln[3]
284         return ret
285
286     def savealiases(self, map):
287         with openwdir(pj(self.dir, "alias"), "W") as f:
288             for nm, (libnm, id) in map.items():
289                 f.write(consline("alias", nm, libnm, id) + "\n")
290
291     def file(self, name, mode="r"):
292         return openwdir(pj(self.dir, name), mode)
293
294     def getalias(self, nm):
295         return self.getaliases()[nm]
296
297     def setalias(self, nm, libnm, id):
298         aliases = self.getaliases()
299         aliases[nm] = libnm, id
300         self.savealiases(aliases)
301
302     def bytag(self, tag):
303         return tagview.bytag(self, tag)
304
305     @classmethod
306     def byname(cls, name):
307         if not name or name == "last" or name[0] == '.':
308             raise KeyError("invalid profile name: " + name)
309         ret = cls(pj(basedir, name))
310         ret.name = name
311         return ret
312
313     @classmethod
314     def last(cls):
315         if not os.path.exists(pj(basedir, "last")):
316             raise KeyError("there is no last used profile")
317         with open(pj(basedir, "last")) as f:
318             return cls.byname(f.readline().strip())