Overwrite profile files more robustly.
[automanga.git] / manga / profile.py
CommitLineData
375a030d
FT
1import os
2pj = os.path.join
3
4home = os.getenv("HOME")
5if home is None or not os.path.isdir(home):
6 raise Exception("Could not find home directory for profile keeping")
7basedir = pj(home, ".manga", "profiles")
8
6b0254b2
FT
9class 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
375a030d 31def openwdir(nm, mode="r"):
6b0254b2
FT
32 ft = file
33 if mode == "W":
34 mode = "w"
35 ft = txfile
375a030d 36 if os.path.exists(nm):
6b0254b2 37 return ft(nm, mode)
375a030d
FT
38 if mode != "r":
39 d = os.path.dirname(nm)
40 if not os.path.isdir(d):
41 os.makedirs(d)
6b0254b2 42 return ft(nm, mode)
375a030d
FT
43
44def 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 = ""
012c4cae 78 a = False
375a030d
FT
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
f03018e9
FT
89def splitlines(fp):
90 for line in fp:
91 cur = splitline(line)
92 if len(cur) < 1:
93 continue
94 yield cur
95
375a030d
FT
96def 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
114class manga(object):
43423668 115 def __init__(self, profile, libnm, id):
375a030d
FT
116 self.profile = profile
117 self.libnm = libnm
118 self.id = id
375a030d
FT
119 self.props = self.loadprops()
120
43423668
FT
121 def open(self):
122 import lib
123 return lib.findlib(self.libnm).byid(self.id)
124
5997ac77
FT
125 def save(self):
126 pass
127
43423668
FT
128class 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
f03018e9
FT
135class 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):
6b0254b2 175 with profile.file("tags", "W") as fp:
f03018e9
FT
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
43423668
FT
191class filemanga(manga):
192 def __init__(self, profile, libnm, id, path):
193 self.path = path
194 super(filemanga, self).__init__(profile, libnm, id)
f03018e9 195 self.tags = tagview(self)
43423668 196
375a030d
FT
197 def loadprops(self):
198 ret = {}
199 with openwdir(self.path) as f:
f03018e9 200 for words in splitlines(f):
375a030d
FT
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
43423668 207 def save(self):
6b0254b2 208 with openwdir(self.path, "W") as f:
375a030d
FT
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
375a030d
FT
215class 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:
f03018e9 225 for words in splitlines(f):
375a030d
FT
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):
6b0254b2 239 with openwdir(pj(self.dir, "map"), "W") as f:
375a030d
FT
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:
43423668 247 return filemanga(self, libnm, id, pj(self.dir, "%i.manga" % m[(libnm, id)]))
375a030d
FT
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)
43423668 260 return filemanga(self, libnm, id, pj(self.dir, "%i.manga" % seq))
375a030d
FT
261
262 def setlast(self):
263 if self.name is None:
264 raise ValueError("profile at " + self.dir + " has no name")
6b0254b2 265 with openwdir(pj(basedir, "last"), "W") as f:
375a030d
FT
266 f.write(self.name + "\n")
267
271d68da
FT
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):
6b0254b2 280 with openwdir(pj(self.dir, "alias"), "W") as f:
271d68da
FT
281 for nm, (libnm, id) in map.iteritems():
282 f.write(consline("alias", nm, libnm, id) + "\n")
283
477d3ba0
FT
284 def file(self, name, mode="r"):
285 return openwdir(pj(self.dir, name), mode)
286
271d68da
FT
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
f03018e9
FT
295 def bytag(self, tag):
296 return tagview.bytag(self, tag)
297
375a030d
FT
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())