local: Treat dots/periods as digits in destructuring directories.
[automanga.git] / manga / lib.py
... / ...
CommitLineData
1class 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
38class 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
70class 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
102class 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
111class 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
125class 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
157class 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
191class 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
229loaded = {}
230def 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]