Commit | Line | Data |
---|---|---|
f3ad0817 | 1 | class library(object): |
6a1e046b FT |
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.""" | |
ffd12e71 FT |
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.""" | |
6a1e046b FT |
23 | raise NotImplementedError() |
24 | ||
46b3b90e FT |
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 | ||
6a1e046b FT |
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") | |
f3ad0817 | 37 | |
3683ab38 FT |
38 | class pagetree(object): |
39 | """Base class for objects in the tree of pages and pagelists. | |
40 | ||
46b3b90e FT |
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 | |
3683ab38 | 45 | information for the pair following it. The only objects with empty |
46b3b90e FT |
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 | |
699d0c17 FT |
52 | parent. |
53 | ||
54 | All pagetree objects should contain an attribute `name', | |
55 | containing some human-readable Unicode representation of the | |
56 | pagelist.""" | |
46b3b90e FT |
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: | |
bc48738d FT |
62 | return [] |
63 | return self.stack[-1][0].idlist() + [self.id] | |
46b3b90e FT |
64 | |
65 | def byidlist(self, idlist): | |
66 | if len(idlist) == 0: | |
67 | return self | |
68 | return self.byid(idlist[0]).byidlist(idlist[1:]) | |
3683ab38 FT |
69 | |
70 | class pagelist(pagetree): | |
6a1e046b | 71 | """Class representing a list of either pages, or nested |
699d0c17 | 72 | pagelists. Might be, for instance, a volume or a chapter.""" |
6a1e046b FT |
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() | |
f3ad0817 | 89 | |
46b3b90e FT |
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 | ||
f3ad0817 | 102 | class manga(pagelist): |
6a1e046b | 103 | """Class reprenting a single manga. Includes the pagelist class, |
46b3b90e FT |
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.""" | |
f3ad0817 FT |
109 | pass |
110 | ||
3683ab38 | 111 | class page(pagetree): |
6a1e046b FT |
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 | |
9948db89 FT |
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.""" | |
6a1e046b FT |
135 | |
136 | def __enter__(self): | |
137 | return self | |
138 | ||
139 | def __exit__(self, *exc_info): | |
140 | self.close() | |
141 | ||
af730068 FT |
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 | ||
6a1e046b FT |
148 | def close(self): |
149 | """Close this stream.""" | |
150 | raise NotImplementedError() | |
151 | ||
3cc7937c | 152 | def read(self, sz=None): |
6a1e046b FT |
153 | """Read SZ bytes from the stream, or the entire rest of the |
154 | stream of SZ is not given.""" | |
155 | raise NotImplementedError() | |
07be272b | 156 | |
b9e558ac FT |
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): | |
3cc7937c | 162 | import urllib.request |
531e4473 | 163 | req = urllib.request.Request(url, headers={"User-Agent": "automanga/1"}) |
531e4473 | 164 | self.bk = urllib.request.urlopen(req) |
b9e558ac FT |
165 | ok = False |
166 | try: | |
167 | if self.bk.getcode() != 200: | |
168 | raise IOError("Server error: " + str(self.bk.getcode())) | |
169 | self.ctype = self.bk.info()["Content-Type"] | |
170 | self.clen = int(self.bk.info()["Content-Length"]) | |
171 | ok = True | |
172 | finally: | |
173 | if not ok: | |
174 | self.bk.close() | |
175 | ||
176 | def fileno(self): | |
177 | return self.bk.fileno() | |
178 | ||
179 | def close(self): | |
180 | self.bk.close() | |
181 | ||
3cc7937c | 182 | def read(self, sz=None): |
b9e558ac FT |
183 | if sz is None: |
184 | return self.bk.read() | |
185 | else: | |
186 | return self.bk.read(sz) | |
187 | ||
055ad3fd FT |
188 | class cursor(object): |
189 | def __init__(self, ob): | |
d8bea304 FT |
190 | if isinstance(ob, cursor): |
191 | self.cur = ob.cur | |
192 | else: | |
193 | self.cur = self.descend(ob) | |
055ad3fd | 194 | |
39b66c75 | 195 | def descend(self, ob, last=False): |
055ad3fd | 196 | while isinstance(ob, pagelist): |
39b66c75 | 197 | ob = ob[len(ob) - 1 if last else 0] |
055ad3fd FT |
198 | if not isinstance(ob, page): |
199 | raise TypeError("object in page tree was unexpectedly not a pagetree") | |
200 | return ob | |
07be272b FT |
201 | |
202 | def next(self): | |
055ad3fd FT |
203 | for n, i in reversed(self.cur.stack): |
204 | if i < len(n) - 1: | |
205 | self.cur = self.descend(n[i + 1]) | |
206 | return self.cur | |
207 | raise StopIteration() | |
208 | ||
209 | def prev(self): | |
f19ac53a | 210 | for n, i in reversed(self.cur.stack): |
055ad3fd | 211 | if i > 0: |
39b66c75 | 212 | self.cur = self.descend(n[i - 1], True) |
055ad3fd FT |
213 | return self.cur |
214 | raise StopIteration() | |
07be272b FT |
215 | |
216 | def __iter__(self): | |
217 | return self | |
53395a9d | 218 | |
31ea855c FT |
219 | loaded = {} |
220 | def findlib(name): | |
221 | def load(name): | |
3cc7937c FT |
222 | import importlib |
223 | mod = importlib.import_module(name) | |
31ea855c FT |
224 | if not hasattr(mod, "library"): |
225 | raise ImportError("module " + name + " is not a manga library") | |
226 | return mod.library() | |
227 | if name not in loaded: | |
228 | try: | |
229 | loaded[name] = load("manga." + name) | |
230 | except ImportError: | |
231 | loaded[name] = load(name) | |
232 | return loaded[name] |