Added simplistic keyword searching to mrnet and local libraries.
[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 cursor(object):
158     def __init__(self, ob):
159         if isinstance(ob, cursor):
160             self.cur = ob.cur
161         else:
162             self.cur = self.descend(ob)
163
164     def descend(self, ob, last=False):
165         while isinstance(ob, pagelist):
166             ob = ob[len(ob) - 1 if last else 0]
167         if not isinstance(ob, page):
168             raise TypeError("object in page tree was unexpectedly not a pagetree")
169         return ob
170
171     def next(self):
172         for n, i in reversed(self.cur.stack):
173             if i < len(n) - 1:
174                 self.cur = self.descend(n[i + 1])
175                 return self.cur
176         raise StopIteration()
177
178     def prev(self):
179         for n, i in reversed(self.cur.stack):
180             if i > 0:
181                 self.cur = self.descend(n[i - 1], True)
182                 return self.cur
183         raise StopIteration()
184
185     def __iter__(self):
186         return self
187
188 loaded = {}
189 def findlib(name):
190     def load(name):
191         mod = __import__(name, fromlist=["dummy"])
192         if not hasattr(mod, "library"):
193             raise ImportError("module " + name + " is not a manga library")
194         return mod.library()
195     if name not in loaded:
196         try:
197             loaded[name] = load("manga." + name)
198         except ImportError:
199             loaded[name] = load(name)
200     return loaded[name]