]>
| Commit | Line | Data |
|---|---|---|
| 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] |