Overwrite profile files more robustly.
[automanga.git] / manga / profile.py
index 2ff6be5..f304252 100644 (file)
@@ -6,14 +6,40 @@ if home is None or not os.path.isdir(home):
     raise Exception("Could not find home directory for profile keeping")
 basedir = pj(home, ".manga", "profiles")
 
+class txfile(file):
+    def __init__(self, name, mode):
+        self.realname = name
+        self.tempname = name + ".new"
+        super(txfile, self).__init__(self.tempname, mode)
+
+    def close(self, abort=False):
+        super(txfile, self).close()
+        if abort:
+            os.unlink(self.tempname)
+        else:
+            os.rename(self.tempname, self.realname)
+
+    def __enter__(self):
+        return self
+
+    def __exit__(self, *exc_info):
+        if exc_info[0] is not None:
+            self.close(True)
+        else:
+            self.close(False)
+
 def openwdir(nm, mode="r"):
+    ft = file
+    if mode == "W":
+        mode = "w"
+        ft = txfile
     if os.path.exists(nm):
-        return open(nm, mode)
+        return ft(nm, mode)
     if mode != "r":
         d = os.path.dirname(nm)
         if not os.path.isdir(d):
             os.makedirs(d)
-    return open(nm, mode)
+    return ft(nm, mode)
 
 def splitline(line):
     def bsq(c):
@@ -60,6 +86,13 @@ def splitline(line):
         ret.append(buf)
     return ret
 
+def splitlines(fp):
+    for line in fp:
+        cur = splitline(line)
+        if len(cur) < 1:
+            continue
+        yield cur
+
 def consline(*words):
     buf = ""
     for w in words:
@@ -79,53 +112,106 @@ def consline(*words):
     return buf
 
 class manga(object):
-    def __init__(self, profile, libnm, id, path):
+    def __init__(self, profile, libnm, id):
         self.profile = profile
         self.libnm = libnm
         self.id = id
-        self.path = path
         self.props = self.loadprops()
 
+    def open(self):
+        import lib
+        return lib.findlib(self.libnm).byid(self.id)
+
+    def save(self):
+        pass
+
+class memmanga(manga):
+    def __init__(self, profile, libnm, id):
+        super(memmanga, self).__init__(profile, libnm, id)
+
+    def loadprops(self):
+        return {}
+
+class tagview(object):
+    def __init__(self, manga):
+        self.manga = manga
+        self.profile = manga.profile
+
+    def add(self, *tags):
+        mt = self.getall(self.profile)
+        ctags = mt.setdefault((self.manga.libnm, self.manga.id), set())
+        ctags |= set(tags)
+        self.save(self.profile, mt)
+
+    def remove(self, *tags):
+        mt = self.getall(self.profile)
+        ctags = mt.get((self.manga.libnm, self.manga.id), set())
+        ctags -= set(tags)
+        if len(ctags) < 1:
+            try:
+                del mt[self.manga.libnm, self.manga.id]
+            except KeyError:
+                pass
+        self.save(self.profile, mt)
+
+    def __iter__(self):
+        return iter(self.getall(self.profile).get((self.manga.libnm, self.manga.id), set()))
+
+    @staticmethod
+    def getall(profile):
+        ret = {}
+        try:
+            with profile.file("tags") as fp:
+                for words in splitlines(fp):
+                    libnm, id = words[0:2]
+                    tags = set(words[2:])
+                    ret[libnm, id] = tags
+        except IOError:
+            pass
+        return ret
+
+    @staticmethod
+    def save(profile, m):
+        with profile.file("tags", "W") as fp:
+            for (libnm, id), tags in m.iteritems():
+                fp.write(consline(libnm, id, *tags) + "\n")
+
+    @staticmethod
+    def bytag(profile, tag):
+        try:
+            with profile.file("tags") as fp:
+                for words in splitlines(fp):
+                    libnm, id = words[0:2]
+                    tags = words[2:]
+                    if tag in tags:
+                        yield profile.getmanga(libnm, id)
+        except IOError:
+            pass
+
+class filemanga(manga):
+    def __init__(self, profile, libnm, id, path):
+        self.path = path
+        super(filemanga, self).__init__(profile, libnm, id)
+        self.tags = tagview(self)
+
     def loadprops(self):
         ret = {}
         with openwdir(self.path) as f:
-            for line in f:
-                words = splitline(line)
-                if len(words) < 1: continue
+            for words in splitlines(f):
                 if words[0] == "set" and len(words) > 2:
                     ret[words[1]] = words[2]
                 elif words[0] == "lset" and len(words) > 1:
                     ret[words[1]] = words[2:]
         return ret
 
-    def prop(self, key, default=KeyError):
-        if key not in self.props:
-            if default is KeyError:
-                raise KeyError(key)
-            return default
-        return self.props[key]
-
-    def __getitem__(self, key):
-        return self.props[key]
-
-    def __contains__(self, key):
-        return key in self.props
-
-    def setprop(self, key, val):
-        self.props[key] = val
-
-    def saveprops(self):
-        with openwdir(self.path, "w") as f:
+    def save(self):
+        with openwdir(self.path, "W") as f:
             for key, val in self.props.iteritems():
                 if isinstance(val, str):
                     f.write(consline("set", key, val) + "\n")
                 else:
                     f.write(consline("lset", key, *val) + "\n")
 
-    def open(self):
-        import lib
-        return lib.findlib(self.libnm).byid(self.id)
-
 class profile(object):
     def __init__(self, dir):
         self.dir = dir
@@ -136,10 +222,7 @@ class profile(object):
         ret = {}
         if os.path.exists(pj(self.dir, "map")):
             with openwdir(pj(self.dir, "map")) as f:
-                for ln in f:
-                    words = splitline(ln)
-                    if len(words) < 1:
-                        continue
+                for words in splitlines(f):
                     if words[0] == "seq" and len(words) > 1:
                         try:
                             seq = int(words[1])
@@ -153,7 +236,7 @@ class profile(object):
         return seq, ret
 
     def savemapping(self, seq, m):
-        with openwdir(pj(self.dir, "map"), "w") as f:
+        with openwdir(pj(self.dir, "map"), "W") as f:
             f.write(consline("seq", str(seq)) + "\n")
             for (libnm, id), num in m.iteritems():
                 f.write(consline("manga", libnm, id, str(num)) + "\n")
@@ -161,7 +244,7 @@ class profile(object):
     def getmanga(self, libnm, id, creat=False):
         seq, m = self.getmapping()
         if (libnm, id) in m:
-            return manga(self, libnm, id, pj(self.dir, "%i.manga" % m[(libnm, id)]))
+            return filemanga(self, libnm, id, pj(self.dir, "%i.manga" % m[(libnm, id)]))
         if not creat:
             raise KeyError("no such manga: (%s, %s)" % (libnm, id))
         while True:
@@ -174,12 +257,12 @@ class profile(object):
         fp.close()
         m[(libnm, id)] = seq
         self.savemapping(seq, m)
-        return manga(self, libnm, id, pj(self.dir, "%i.manga" % seq))
+        return filemanga(self, libnm, id, pj(self.dir, "%i.manga" % seq))
 
     def setlast(self):
         if self.name is None:
             raise ValueError("profile at " + self.dir + " has no name")
-        with openwdir(pj(basedir, "last"), "w") as f:
+        with openwdir(pj(basedir, "last"), "W") as f:
             f.write(self.name + "\n")
 
     def getaliases(self):
@@ -194,10 +277,13 @@ class profile(object):
         return ret
 
     def savealiases(self, map):
-        with openwdir(pj(self.dir, "alias"), "w") as f:
+        with openwdir(pj(self.dir, "alias"), "W") as f:
             for nm, (libnm, id) in map.iteritems():
                 f.write(consline("alias", nm, libnm, id) + "\n")
 
+    def file(self, name, mode="r"):
+        return openwdir(pj(self.dir, name), mode)
+
     def getalias(self, nm):
         return self.getaliases()[nm]
 
@@ -206,6 +292,9 @@ class profile(object):
         aliases[nm] = libnm, id
         self.savealiases(aliases)
 
+    def bytag(self, tag):
+        return tagview.bytag(self, tag)
+
     @classmethod
     def byname(cls, name):
         if not name or name == "last" or name[0] == '.':