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