Initial port of core code to Python3.
[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):
162         import urllib.request
163         self.bk = urllib.request.urlopen(url)
164         ok = False
165         try:
166             if self.bk.getcode() != 200:
167                 raise IOError("Server error: " + str(self.bk.getcode()))
168             self.ctype = self.bk.info()["Content-Type"]
169             self.clen = int(self.bk.info()["Content-Length"])
170             ok = True
171         finally:
172             if not ok:
173                 self.bk.close()
174
175     def fileno(self):
176         return self.bk.fileno()
177
178     def close(self):
179         self.bk.close()
180
181     def read(self, sz=None):
182         if sz is None:
183             return self.bk.read()
184         else:
185             return self.bk.read(sz)
186
187 class cursor(object):
188     def __init__(self, ob):
189         if isinstance(ob, cursor):
190             self.cur = ob.cur
191         else:
192             self.cur = self.descend(ob)
193
194     def descend(self, ob, last=False):
195         while isinstance(ob, pagelist):
196             ob = ob[len(ob) - 1 if last else 0]
197         if not isinstance(ob, page):
198             raise TypeError("object in page tree was unexpectedly not a pagetree")
199         return ob
200
201     def next(self):
202         for n, i in reversed(self.cur.stack):
203             if i < len(n) - 1:
204                 self.cur = self.descend(n[i + 1])
205                 return self.cur
206         raise StopIteration()
207
208     def prev(self):
209         for n, i in reversed(self.cur.stack):
210             if i > 0:
211                 self.cur = self.descend(n[i - 1], True)
212                 return self.cur
213         raise StopIteration()
214
215     def __iter__(self):
216         return self
217
218 loaded = {}
219 def findlib(name):
220     def load(name):
221         import importlib
222         mod = importlib.import_module(name)
223         if not hasattr(mod, "library"):
224             raise ImportError("module " + name + " is not a manga library")
225         return mod.library()
226     if name not in loaded:
227         try:
228             loaded[name] = load("manga." + name)
229         except ImportError:
230             loaded[name] = load(name)
231     return loaded[name]