local: Treat dots/periods as digits in destructuring directories.
[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")
40277671
FT
7confdir = pj(home, ".manga")
8basedir = pj(confdir, "profiles")
375a030d 9
3cc7937c 10class txfile(object):
6b0254b2
FT
11 def __init__(self, name, mode):
12 self.realname = name
13 self.tempname = name + ".new"
3cc7937c 14 self.bk = open(self.tempname, mode)
6b0254b2
FT
15
16 def close(self, abort=False):
3cc7937c 17 self.bk.close()
6b0254b2
FT
18 if abort:
19 os.unlink(self.tempname)
20 else:
21 os.rename(self.tempname, self.realname)
22
3cc7937c
FT
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
6b0254b2
FT
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
375a030d 38def openwdir(nm, mode="r"):
3cc7937c 39 ft = open
6b0254b2
FT
40 if mode == "W":
41 mode = "w"
42 ft = txfile
375a030d 43 if os.path.exists(nm):
6b0254b2 44 return ft(nm, mode)
375a030d
FT
45 if mode != "r":
46 d = os.path.dirname(nm)
47 if not os.path.isdir(d):
48 os.makedirs(d)
6b0254b2 49 return ft(nm, mode)
375a030d
FT
50
51def 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 = ""
012c4cae 85 a = False
375a030d
FT
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
f03018e9
FT
96def splitlines(fp):
97 for line in fp:
98 cur = splitline(line)
99 if len(cur) < 1:
100 continue
101 yield cur
102
375a030d
FT
103def 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
121class manga(object):
43423668 122 def __init__(self, profile, libnm, id):
375a030d
FT
123 self.profile = profile
124 self.libnm = libnm
125 self.id = id
375a030d
FT
126 self.props = self.loadprops()
127
43423668 128 def open(self):
3cc7937c 129 from . import lib
43423668
FT
130 return lib.findlib(self.libnm).byid(self.id)
131
5997ac77
FT
132 def save(self):
133 pass
134
43423668
FT
135class 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
f03018e9
FT
142class 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):
6b0254b2 182 with profile.file("tags", "W") as fp:
3cc7937c 183 for (libnm, id), tags in m.items():
f03018e9
FT
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
43423668
FT
198class filemanga(manga):
199 def __init__(self, profile, libnm, id, path):
200 self.path = path
201 super(filemanga, self).__init__(profile, libnm, id)
f03018e9 202 self.tags = tagview(self)
43423668 203
375a030d
FT
204 def loadprops(self):
205 ret = {}
206 with openwdir(self.path) as f:
f03018e9 207 for words in splitlines(f):
375a030d
FT
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
43423668 214 def save(self):
6b0254b2 215 with openwdir(self.path, "W") as f:
3cc7937c 216 for key, val in self.props.items():
375a030d
FT
217 if isinstance(val, str):
218 f.write(consline("set", key, val) + "\n")
219 else:
220 f.write(consline("lset", key, *val) + "\n")
221
61e4bd12
FT
222 def mtime(self):
223 try:
224 return os.stat(self.path).st_mtime
225 except FileNotFoundError:
226 return 0
227
375a030d
FT
228class profile(object):
229 def __init__(self, dir):
230 self.dir = dir
231 self.name = None
232
233 def getmapping(self):
234 seq = 0
235 ret = {}
236 if os.path.exists(pj(self.dir, "map")):
237 with openwdir(pj(self.dir, "map")) as f:
f03018e9 238 for words in splitlines(f):
375a030d
FT
239 if words[0] == "seq" and len(words) > 1:
240 try:
241 seq = int(words[1])
242 except ValueError:
243 pass
244 elif words[0] == "manga" and len(words) > 3:
245 try:
246 ret[words[1], words[2]] = int(words[3])
247 except ValueError:
248 pass
249 return seq, ret
250
251 def savemapping(self, seq, m):
6b0254b2 252 with openwdir(pj(self.dir, "map"), "W") as f:
375a030d 253 f.write(consline("seq", str(seq)) + "\n")
3cc7937c 254 for (libnm, id), num in m.items():
375a030d
FT
255 f.write(consline("manga", libnm, id, str(num)) + "\n")
256
257 def getmanga(self, libnm, id, creat=False):
258 seq, m = self.getmapping()
259 if (libnm, id) in m:
43423668 260 return filemanga(self, libnm, id, pj(self.dir, "%i.manga" % m[(libnm, id)]))
375a030d
FT
261 if not creat:
262 raise KeyError("no such manga: (%s, %s)" % (libnm, id))
263 while True:
264 try:
e02d1bb3 265 fp = openwdir(pj(self.dir, "%i.manga" % seq), "x")
375a030d
FT
266 except IOError:
267 seq += 1
268 else:
269 break
270 fp.close()
271 m[(libnm, id)] = seq
272 self.savemapping(seq, m)
43423668 273 return filemanga(self, libnm, id, pj(self.dir, "%i.manga" % seq))
375a030d
FT
274
275 def setlast(self):
276 if self.name is None:
277 raise ValueError("profile at " + self.dir + " has no name")
6b0254b2 278 with openwdir(pj(basedir, "last"), "W") as f:
375a030d
FT
279 f.write(self.name + "\n")
280
271d68da
FT
281 def getaliases(self):
282 ret = {}
283 if os.path.exists(pj(self.dir, "alias")):
284 with openwdir(pj(self.dir, "alias")) as f:
285 for ln in f:
286 ln = splitline(ln)
287 if len(ln) < 1: continue
288 if ln[0] == "alias" and len(ln) > 3:
289 ret[ln[1]] = ln[2], ln[3]
290 return ret
291
292 def savealiases(self, map):
6b0254b2 293 with openwdir(pj(self.dir, "alias"), "W") as f:
3cc7937c 294 for nm, (libnm, id) in map.items():
271d68da
FT
295 f.write(consline("alias", nm, libnm, id) + "\n")
296
477d3ba0
FT
297 def file(self, name, mode="r"):
298 return openwdir(pj(self.dir, name), mode)
299
271d68da
FT
300 def getalias(self, nm):
301 return self.getaliases()[nm]
302
303 def setalias(self, nm, libnm, id):
304 aliases = self.getaliases()
305 aliases[nm] = libnm, id
306 self.savealiases(aliases)
307
f03018e9
FT
308 def bytag(self, tag):
309 return tagview.bytag(self, tag)
310
375a030d
FT
311 @classmethod
312 def byname(cls, name):
313 if not name or name == "last" or name[0] == '.':
314 raise KeyError("invalid profile name: " + name)
315 ret = cls(pj(basedir, name))
316 ret.name = name
317 return ret
318
319 @classmethod
320 def last(cls):
321 if not os.path.exists(pj(basedir, "last")):
322 raise KeyError("there is no last used profile")
323 with open(pj(basedir, "last")) as f:
324 return cls.byname(f.readline().strip())