Fixed Batoto search.
[automanga.git] / manga / lib.py
CommitLineData
f3ad0817 1class 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
38class 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
70class 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 102class 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 111class 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
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
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
157class 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
188class 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
219loaded = {}
220def 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]