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