local: Treat dots/periods as digits in destructuring directories.
[automanga.git] / manga / lib.py
1 class library(object):
2     """Class representing a single source of multiple mangas."""
3     
4     def byname(self, prefix):
5         """Returns an iterable object of all mangas in this library
6         whose names (case-insensitively) begin with the given
7         prefix.
8
9         All libraries should implement this."""
10         raise NotImplementedError()
11
12     def search(self, string):
13         """Returns an iterable object of mangas in this library that
14         matches the search string in a library-dependent manner. While
15         each library is at liberty to define its own matching
16         criteria, it is probably likely to involve something akin to
17         searching for keywords in the titles of the library.
18
19         Searching may return very many results and may be slow to
20         iterate.
21
22         Not all libraries need implement this."""
23         raise NotImplementedError()
24
25     def byid(self, id):
26         """Returns a previously known manga by its string ID, or
27         raises KeyError if no such manga could be found.
28
29         All libraries should implement this."""
30         raise KeyError(id)
31
32     def __iter__(self):
33         """Return an iterator of all known mangas in this library.
34
35         Not all libraries need implement this."""
36         raise NotImplementedError("manga.lib.library iterator")
37
38 class pagetree(object):
39     """Base class for objects in the tree of pages and pagelists.
40
41     All pagetree objects should contain an attribute `stack',
42     containing a list of pairs. The last pair in the list should be
43     the pagetree object which yielded this pagetree object, along with
44     the index which yielded it. Every non-last pair should be the same
45     information for the pair following it. The only objects with empty
46     `stack' lists should be `manga' objects.
47     
48     All non-root pagetree objects should also contain an attribute
49     `id', which should be a string that can be passed to the `byid'
50     function of its parent node to recover the node. Such string ID
51     should be more persistent than the node's numeric index in the
52     parent.
53
54     All pagetree objects should contain an attribute `name',
55     containing some human-readable Unicode representation of the
56     pagelist."""
57     
58     def idlist(self):
59         """Returns a list of the IDs necessary to resolve this node
60         from the root node."""
61         if len(self.stack) == 0:
62             return []
63         return self.stack[-1][0].idlist() + [self.id]
64
65     def byidlist(self, idlist):
66         if len(idlist) == 0:
67             return self
68         return self.byid(idlist[0]).byidlist(idlist[1:])
69
70 class pagelist(pagetree):
71     """Class representing a list of either pages, or nested
72     pagelists. Might be, for instance, a volume or a chapter."""
73
74     def __len__(self):
75         """Return the number of (direct) sub-nodes in this pagelist.
76
77         All pagelists need to implement this."""
78         raise NotImplementedError()
79
80     def __getitem__(self, idx):
81         """Return the direct sub-node of the given index in this
82         pagelist. Sub-node indexes are always zero-based and
83         contiguous, regardless of any gaps in the underlying medium,
84         which should be indicated instead by way of the `name'
85         attribute.
86
87         All pagelists need to implement this."""
88         raise NotImplementedError()
89
90     def byid(self, id):
91         """Return the direct sub-node of this pagelist which has the
92         given string ID. If none is found, a KeyError is raised.
93
94         This default method iterates the children of this node, but
95         may be overridden by some more efficient implementation.
96         """
97         for ch in self:
98             if ch.id == id:
99                 return ch
100         raise KeyError(id)
101
102 class manga(pagelist):
103     """Class reprenting a single manga. Includes the pagelist class,
104     and all constraints valid for it.
105
106     A manga is a root pagetree node, but should also contain an `id'
107     attribute, which can be used to recover the manga from its
108     library's `byid' function."""
109     pass
110
111 class page(pagetree):
112     """Class representing a single page of a manga. Pages make up the
113     leaf nodes of a pagelist tree.
114
115     All pages should contain an attribute `manga', referring back to
116     the containing manga instance."""
117     
118     def open(self):
119         """Open a stream for the image this page represents. The
120         returned object should be an imgstream class.
121
122         All pages need to implement this."""
123         raise NotImplementedError()
124
125 class imgstream(object):
126     """An open image I/O stream for a manga page. Generally, it should
127     be file-like. This base class implements the resource-manager
128     interface for use in `with' statements, calling close() on itself
129     when exiting the with-scope.
130
131     All imgstreams should contain an attribute `ctype', being the
132     Content-Type of the image being read by the stream, and `clen`,
133     being either an int describing the total number of bytes in the
134     stream, or None if the value is not known in advance."""
135
136     def __enter__(self):
137         return self
138
139     def __exit__(self, *exc_info):
140         self.close()
141
142     def fileno(self):
143         """If reading the imgstream may block, fileno() should return
144         a file descriptor that can be polled. If fileno() returns
145         None, that should mean that reading will not block."""
146         return None
147
148     def close(self):
149         """Close this stream."""
150         raise NotImplementedError()
151
152     def read(self, sz=None):
153         """Read SZ bytes from the stream, or the entire rest of the
154         stream of SZ is not given."""
155         raise NotImplementedError()
156
157 class stdimgstream(imgstream):
158     """A standard implementation of imgstream, for libraries which
159     have no particular implementation requirements."""
160
161     def __init__(self, url, referer=None):
162         import urllib.request
163         headers = {"User-Agent": "automanga/1"}
164         if referer:
165             headers["Referer"] = referer
166         req = urllib.request.Request(url, headers=headers)
167         self.bk = urllib.request.urlopen(req)
168         ok = False
169         try:
170             if self.bk.getcode() != 200:
171                 raise IOError("Server error: " + str(self.bk.getcode()))
172             self.ctype = self.bk.info()["Content-Type"]
173             self.clen = int(self.bk.info()["Content-Length"])
174             ok = True
175         finally:
176             if not ok:
177                 self.bk.close()
178
179     def fileno(self):
180         return self.bk.fileno()
181
182     def close(self):
183         self.bk.close()
184
185     def read(self, sz=None):
186         if sz is None:
187             return self.bk.read()
188         else:
189             return self.bk.read(sz)
190
191 class cursor(object):
192     def __init__(self, ob):
193         if isinstance(ob, cursor):
194             self.cur = ob.cur
195         else:
196             self.cur = self.descend(ob)
197
198     def descend(self, ob, last=False):
199         while isinstance(ob, pagelist):
200             ob = ob[len(ob) - 1 if last else 0]
201         if not isinstance(ob, page):
202             raise TypeError("object in page tree was unexpectedly not a pagetree")
203         return ob
204
205     def next(self):
206         for n, i in reversed(self.cur.stack):
207             if i < len(n) - 1:
208                 self.cur = self.descend(n[i + 1])
209                 return self.cur
210         raise StopIteration()
211
212     def prev(self):
213         for n, i in reversed(self.cur.stack):
214             if i > 0:
215                 self.cur = self.descend(n[i - 1], True)
216                 return self.cur
217         raise StopIteration()
218
219     def __iter__(self):
220         def iterator():
221             yield self.cur
222             while True:
223                 try:
224                     yield self.next()
225                 except StopIteration:
226                     break
227         return iterator()
228
229 loaded = {}
230 def findlib(name):
231     def load(name):
232         import importlib
233         mod = importlib.import_module(name)
234         if not hasattr(mod, "library"):
235             raise ImportError("module " + name + " is not a manga library")
236         return mod.library()
237     if name not in loaded:
238         try:
239             loaded[name] = load("manga." + name)
240         except ImportError:
241             loaded[name] = load(name)
242     return loaded[name]