Adding magazine covers
[watersofshiloah] / src / stream_index.py
1 import weakref
2 import logging
3
4 import util.misc as misc_utils
5 from util import go_utils
6 import backend
7
8
9 _moduleLogger = logging.getLogger(__name__)
10
11
12 SOURCE_RADIO = "radio"
13 SOURCE_CONFERENCES = "conferences"
14 SOURCE_MAGAZINES = "magazines"
15 SOURCE_SCRIPTURES = "scriptures"
16
17
18 class Connection(object):
19
20         def __init__(self):
21                 self._backend = backend.Backend()
22                 self._indexing = go_utils.AsyncPool()
23
24         def start(self):
25                 self._indexing.start()
26
27         def stop(self):
28                 self._indexing.stop()
29
30         def download(self, func, on_success, on_error, args = None, kwds = None):
31                 if args is None:
32                         args = ()
33                 if kwds is None:
34                         kwds = {}
35
36                 self._indexing.add_task(
37                         getattr(self._backend, func),
38                         args,
39                         kwds,
40                         on_success,
41                         on_error,
42                 )
43
44
45 class AudioIndex(object):
46
47         def __init__(self):
48                 self._connection = Connection()
49                 self._languages = None
50                 self._languagesRequest = None
51                 self._sources = {}
52
53         def start(self):
54                 self._connection.start()
55
56         def stop(self):
57                 self._connection.stop()
58
59         def get_languages(self, on_success, on_error):
60                 if self._languages is None:
61                         assert self._languagesRequest is None
62                         self._languagesRequest = on_success, on_error
63                         self._connection.download(
64                                 "get_languages",
65                                 self._on_get_languages,
66                                 self._on_languages_error
67                         )
68                 else:
69                         on_success(self._languages)
70
71         def get_source(self, source, langId = None):
72                 key = (source, langId)
73                 if key in self._sources:
74                         node = self._sources[key]
75                 else:
76                         if source == SOURCE_RADIO:
77                                 node = RadioNode(self._connection)
78                         elif source == SOURCE_CONFERENCES:
79                                 assert langId is not None
80                                 node = ConferencesNode(self._connection, langId)
81                         elif source == SOURCE_MAGAZINES:
82                                 assert langId is not None
83                                 node = MagazinesNode(self._connection, langId)
84                         elif source == SOURCE_SCRIPTURES:
85                                 assert langId is not None
86                                 node = ScripturesNode(self._connection, langId)
87                         else:
88                                 raise NotImplementedError(source)
89                         self._sources[key] = node
90
91                 return node
92
93         @misc_utils.log_exception(_moduleLogger)
94         def _on_get_languages(self, languages):
95                 assert self._languages is None
96                 assert self._languagesRequest is not None
97                 r = self._languagesRequest
98                 self._languagesRequest = None
99                 self._languages = languages
100                 r[0](self._languages)
101
102         @misc_utils.log_exception(_moduleLogger)
103         def _on_languages_error(self, e):
104                 assert self._languages is None
105                 assert self._languagesRequest is not None
106                 r = self._languagesRequest
107                 self._languagesRequest = None
108                 r[1](self._languages)
109
110
111 class Node(object):
112
113         def __init__(self, connection, parent, data, id):
114                 self._connection = connection
115                 self._parent = weakref.ref(parent) if parent is not None else None
116                 self._data = data
117                 self._children = None
118                 self._id = id
119
120         def get_children(self, on_success, on_error):
121                 if self._children is None:
122                         self._get_children(on_success, on_error)
123                 else:
124                         on_success(self._children)
125
126         def get_parent(self):
127                 if self._parent is None:
128                         raise RuntimeError("")
129                 parent = self._parent()
130                 return parent
131
132         def get_properties(self):
133                 return self._data
134
135         @property
136         def title(self):
137                 raise NotImplementedError("On %s" % type(self))
138
139         @property
140         def id(self):
141                 return self._id
142
143         def is_leaf(self):
144                 raise NotImplementedError("")
145
146         def _get_children(self, on_success, on_error):
147                 raise NotImplementedError("")
148
149
150 class ParentNode(Node):
151
152         def __init__(self, connection, parent, data, id):
153                 Node.__init__(self, connection, parent, data, id)
154                 self._request = None
155
156         def is_leaf(self):
157                 return False
158
159         def _get_children(self, on_success, on_error):
160                 assert self._request is None
161                 assert self._children is None
162                 self._request = on_success, on_error
163
164                 func, args, kwds = self._get_func()
165
166                 self._connection.download(
167                         func,
168                         self._on_success,
169                         self._on_error,
170                         args,
171                         kwds,
172                 )
173
174         def _get_func(self):
175                 raise NotImplementedError()
176
177         def _create_child(self, data, id):
178                 raise NotImplementedError()
179
180         @misc_utils.log_exception(_moduleLogger)
181         def _on_success(self, data):
182                 r = self._request
183                 self._request = None
184                 try:
185                         self._children = [
186                                 self._create_child(child, i)
187                                 for i, child in enumerate(data)
188                         ]
189                 except Exception, e:
190                         _moduleLogger.exception("Translating error")
191                         self._children = None
192                         r[1](e)
193                 else:
194                         r[0](self._children)
195
196         @misc_utils.log_exception(_moduleLogger)
197         def _on_error(self, error):
198                 r = self._request
199                 self._request = None
200                 r[1](error)
201
202
203 class LeafNode(Node):
204
205         def __init__(self, connection, parent, data, id):
206                 Node.__init__(self, connection, parent, data, id)
207
208         def is_leaf(self):
209                 return True
210
211         @property
212         def can_navigate(self):
213                 raise NotImplementedError("On %s" % type(self))
214
215         @property
216         def subtitle(self):
217                 raise NotImplementedError("On %s" % type(self))
218
219         @property
220         def uri(self):
221                 raise NotImplementedError("On %s" % type(self))
222
223         def _get_children(self, on_success, on_error):
224                 raise RuntimeError("Not is a leaf")
225
226
227 class RadioNode(ParentNode):
228
229         def __init__(self, connection):
230                 ParentNode.__init__(self, connection, None, {}, SOURCE_RADIO)
231
232         @property
233         def title(self):
234                 return "Radio"
235
236         def _get_func(self):
237                 return "get_radio_channels", (), {}
238
239         def _create_child(self, data, id):
240                 return RadioChannelNode(self._connection, self, data, id)
241
242
243 class RadioChannelNode(LeafNode):
244
245         def __init__(self, connection, parent, data, id):
246                 LeafNode.__init__(self, connection, parent, data, id)
247                 self._extendedData = {}
248                 self._request = None
249
250         @property
251         def can_navigate(self):
252                 return False
253
254         @property
255         def title(self):
256                 return "Radio"
257
258         @property
259         def subtitle(self):
260                 return ""
261
262         @property
263         def uri(self):
264                 return self._data["url"]
265
266         def get_programming(self, date, on_success, on_error):
267                 date = date.strftime("%Y-%m-%d")
268                 try:
269                         programming = self._extendedData[date]
270                 except KeyError:
271                         self._get_programming(date, on_success, on_error)
272                 else:
273                         on_success(programming)
274
275         def _get_programming(self, date, on_success, on_error):
276                 assert self._request is None
277                 assert date not in self._extendedData
278                 self._request = on_success, on_error, date
279
280                 self._connection.download(
281                         "get_radio_channel_programming",
282                         self._on_success,
283                         self._on_error,
284                         (self._data["id"], date),
285                         {},
286                 )
287
288         @misc_utils.log_exception(_moduleLogger)
289         def _on_success(self, data):
290                 r = self._request
291                 date = r[2]
292                 self._request = None
293                 try:
294                         self._extendedData[date] = [
295                                 child
296                                 for child in data
297                         ]
298                 except Exception, e:
299                         _moduleLogger.exception("Translating error")
300                         del self._extendedData[date]
301                         r[1](e)
302                 else:
303                         r[0](self._extendedData[date])
304
305         @misc_utils.log_exception(_moduleLogger)
306         def _on_error(self, error):
307                 r = self._request
308                 self._request = None
309                 r[1](error)
310
311
312 class ConferencesNode(ParentNode):
313
314         def __init__(self, connection, langId):
315                 ParentNode.__init__(self, connection, None, {}, SOURCE_CONFERENCES)
316                 self._langId = langId
317
318         @property
319         def title(self):
320                 return "Conferences"
321
322         def _get_func(self):
323                 return "get_conferences", (self._langId, ), {}
324
325         def _create_child(self, data, id):
326                 return ConferenceNode(self._connection, self, data, id)
327
328
329 class ConferenceNode(ParentNode):
330
331         def __init__(self, connection, parent, data, id):
332                 ParentNode.__init__(self, connection, parent, data, id)
333
334         @property
335         def title(self):
336                 return self._data["title"]
337
338         def _get_func(self):
339                 return "get_conference_sessions", (self._data["id"], ), {}
340
341         def _create_child(self, data, id):
342                 return SessionNode(self._connection, self, data, id)
343
344
345 class SessionNode(ParentNode):
346
347         def __init__(self, connection, parent, data, id):
348                 ParentNode.__init__(self, connection, parent, data, id)
349
350         @property
351         def title(self):
352                 return self._data["title"]
353
354         def _get_func(self):
355                 return "get_conference_talks", (self._data["id"], ), {}
356
357         def _create_child(self, data, id):
358                 return TalkNode(self._connection, self, data, id)
359
360
361 class TalkNode(LeafNode):
362
363         def __init__(self, connection, parent, data, id):
364                 LeafNode.__init__(self, connection, parent, data, id)
365
366         @property
367         def can_navigate(self):
368                 return True
369
370         @property
371         def title(self):
372                 return self._data["title"]
373
374         @property
375         def subtitle(self):
376                 speaker = self._data["speaker"]
377                 if speaker is not None:
378                         return speaker
379                 else:
380                         return ""
381
382         @property
383         def uri(self):
384                 return self._data["url"]
385
386
387 class MagazinesNode(ParentNode):
388
389         def __init__(self, connection, langId):
390                 ParentNode.__init__(self, connection, None, {}, SOURCE_MAGAZINES)
391                 self._langId = langId
392
393         @property
394         def title(self):
395                 return "Magazines"
396
397         def _get_func(self):
398                 return "get_magazines", (self._langId, ), {}
399
400         def _create_child(self, data, id):
401                 return MagazineNode(self._connection, self, data, id)
402
403
404 class MagazineNode(ParentNode):
405
406         def __init__(self, connection, parent, data, id):
407                 ParentNode.__init__(self, connection, parent, data, id)
408
409         @property
410         def title(self):
411                 return self._data["title"]
412
413         def _get_func(self):
414                 return "get_magazine_issues", (self._data["id"], ), {}
415
416         def _create_child(self, data, id):
417                 return IssueNode(self._connection, self, data, id)
418
419
420 class IssueNode(ParentNode):
421
422         def __init__(self, connection, parent, data, id):
423                 ParentNode.__init__(self, connection, parent, data, id)
424
425         @property
426         def title(self):
427                 return self._data["title"]
428
429         def _get_func(self):
430                 return "get_magazine_articles", (self._data["id"], ), {}
431
432         def _create_child(self, data, id):
433                 return ArticleNode(self._connection, self, data, id)
434
435
436 class ArticleNode(LeafNode):
437
438         def __init__(self, connection, parent, data, id):
439                 LeafNode.__init__(self, connection, parent, data, id)
440
441         @property
442         def can_navigate(self):
443                 return True
444
445         @property
446         def title(self):
447                 return self._data["title"]
448
449         @property
450         def subtitle(self):
451                 speaker = self._data["author"]
452                 if speaker is not None:
453                         return speaker
454                 else:
455                         return ""
456
457         @property
458         def uri(self):
459                 return self._data["url"]
460
461
462 class ScripturesNode(ParentNode):
463
464         def __init__(self, connection, langId):
465                 ParentNode.__init__(self, connection, None, {}, SOURCE_SCRIPTURES)
466                 self._langId = langId
467
468         @property
469         def title(self):
470                 return "Scriptures"
471
472         def _get_func(self):
473                 return "get_scriptures", (self._langId, ), {}
474
475         def _create_child(self, data, id):
476                 return ScriptureNode(self._connection, self, data, id)
477
478
479 class ScriptureNode(ParentNode):
480
481         def __init__(self, connection, parent, data, id):
482                 ParentNode.__init__(self, connection, parent, data, id)
483
484         @property
485         def title(self):
486                 return self._data["title"]
487
488         def _get_func(self):
489                 return "get_scripture_books", (self._data["id"], ), {}
490
491         def _create_child(self, data, id):
492                 return BookNode(self._connection, self, data, id)
493
494
495 class BookNode(ParentNode):
496
497         def __init__(self, connection, parent, data, id):
498                 ParentNode.__init__(self, connection, parent, data, id)
499
500         @property
501         def title(self):
502                 return self._data["title"]
503
504         def _get_func(self):
505                 return "get_scripture_chapters", (self._data["id"], ), {}
506
507         def _create_child(self, data, id):
508                 return ChapterNode(self._connection, self, data, id)
509
510
511 class ChapterNode(LeafNode):
512
513         def __init__(self, connection, parent, data, id):
514                 LeafNode.__init__(self, connection, parent, data, id)
515
516         @property
517         def can_navigate(self):
518                 return True
519
520         @property
521         def title(self):
522                 return self._data["title"]
523
524         @property
525         def subtitle(self):
526                 return ""
527
528         @property
529         def uri(self):
530                 return self._data["url"]
531
532
533 def walk_ancestors(node):
534         while True:
535                 yield node
536                 try:
537                         node = node.get_parent()
538                 except RuntimeError:
539                         return
540
541
542 def common_paths(targetNode, currentNode):
543         targetNodePath = list(walk_ancestors(targetNode))
544         targetNodePath.reverse()
545         currentNodePath = list(walk_ancestors(currentNode))
546         currentNodePath.reverse()
547
548         ancestors = []
549         descendants = []
550
551         for i, (t, c) in enumerate(zip(targetNodePath, currentNodePath)):
552                 if t is not c:
553                         return ancestors, None, descendants
554                 ancestors.append(t)
555
556         descendants.extend(
557                 child
558                 for child in targetNodePath[i+1:]
559         )
560
561         return ancestors, currentNode, descendants
562
563
564 class AsyncWalker(object):
565
566         def __init__(self, func):
567                 self._func = func
568                 self._run = None
569
570         def start(self, *args, **kwds):
571                 assert self._run is None
572                 self._run = self._func(*args, **kwds)
573                 node = self._run.send(None) # priming the function
574                 node.get_children(self.on_success, self.on_error)
575
576         @misc_utils.log_exception(_moduleLogger)
577         def on_success(self, children):
578                 _moduleLogger.debug("Processing success for: %r", self._func)
579                 try:
580                         node = self._run.send(children)
581                 except StopIteration, e:
582                         pass
583                 else:
584                         node.get_children(self.on_success, self.on_error)
585
586         @misc_utils.log_exception(_moduleLogger)
587         def on_error(self, error):
588                 _moduleLogger.debug("Processing error for: %r", self._func)
589                 try:
590                         node = self._run.throw(error)
591                 except StopIteration, e:
592                         pass
593                 else:
594                         node.get_children(self.on_success, self.on_error)
595
596
597 def get_next(node, on_success, on_error):
598         try:
599                 assert node.is_leaf(), node
600
601                 # Find next branch
602                 childNode = node
603                 while True:
604                         parent = childNode.get_parent()
605                         siblings = yield parent
606                         for i, sibling in enumerate(siblings):
607                                 if sibling is childNode:
608                                         break
609                         i += 1
610                         if i < len(siblings):
611                                 sibling = siblings[i]
612                                 break
613                         else:
614                                 childNode = parent
615
616                 # dig into that branch to find the first leaf
617                 nodes = [sibling]
618                 while nodes:
619                         child = nodes.pop(0)
620                         if child.is_leaf():
621                                 on_success(child)
622                                 return
623                         children = yield child
624                         nodes[0:0] = children
625                 raise RuntimeError("Ran out of nodes when hunting for first leaf of %s" % node)
626         except Exception, e:
627                 on_error(e)
628
629
630 def get_previous(node, on_success, on_error):
631         try:
632                 assert node.is_leaf(), node
633
634                 # Find next branch
635                 childNode = node
636                 while True:
637                         parent = childNode.get_parent()
638                         siblings = yield parent
639                         for i, sibling in enumerate(siblings):
640                                 if sibling is childNode:
641                                         break
642                         i -= 1
643                         if 0 <= i:
644                                 sibling = siblings[i]
645                                 break
646                         else:
647                                 childNode = parent
648
649                 # dig into that branch to find the first leaf
650                 nodes = [sibling]
651                 while nodes:
652                         child = nodes.pop(-1)
653                         if child.is_leaf():
654                                 on_success(child)
655                                 return
656                         children = yield child
657                         nodes[0:0] = children
658                 raise RuntimeError("Ran out of nodes when hunting for first leaf of %s" % node)
659         except Exception, e:
660                 on_error(e)