add simplerss
[vuplus_dvbapp-plugin] / simplerss / src / plugin.py
1 # warning, this is work in progress.
2 # plus, the error handling sucks.
3 #
4 # TODO:
5 #  - inline todos
6 #  - all that stuff I forgot...
7 #
8 from Screens.Screen import Screen
9 from Screens.MessageBox import MessageBox
10 from Screens.ChoiceBox import ChoiceBox
11 from Components.ActionMap import ActionMap
12 from Components.Label import Label
13 from Components.ScrollLabel import ScrollLabel
14 from Components.GUIComponent import GUIComponent
15 from Components.MultiContent import MultiContentEntryText
16 from Components.Button import Button
17 from Plugins.Plugin import PluginDescriptor
18 from enigma import eTimer, eListboxPythonMultiContent, eListbox, gFont, RT_HALIGN_LEFT, RT_WRAP
19
20 from httpclient import getPage
21 from urlparse import urlsplit
22 import xml.dom.minidom
23
24 from sets import Set
25
26 from Components.config import config, configfile, ConfigSubsection, ConfigSubList, ConfigEnableDisable, ConfigInteger, ConfigText, getConfigListEntry
27 from Components.ConfigList import ConfigListScreen
28
29 config.plugins.simpleRSS = ConfigSubsection()
30 config.plugins.simpleRSS.show_new = ConfigEnableDisable(default=True)
31 config.plugins.simpleRSS.interval = ConfigInteger(default=10, limits=(5, 300))
32 config.plugins.simpleRSS.feedcount = ConfigInteger(default=0)
33 config.plugins.simpleRSS.feed = ConfigSubList()
34 for i in range(0, config.plugins.simpleRSS.feedcount.value):
35         config.plugins.simpleRSS.feed.append(ConfigSubsection())
36         config.plugins.simpleRSS.feed[i].uri = ConfigText(default="http://", fixed_size = False)
37         config.plugins.simpleRSS.feed[i].autoupdate = ConfigEnableDisable(default=True)
38
39 class SimpleRSSEdit(ConfigListScreen, Screen):
40         skin = """
41                 <screen name="SimpleRSSEdit" position="100,100" size="550,120" title="Simple RSS Reader Setup" >
42                         <widget name="config" position="20,10" size="510,75" scrollbarMode="showOnDemand" />
43                         <ePixmap name="red"    position="0,75"   zPosition="4" size="140,40" pixmap="key_red-fs8.png" transparent="1" alphatest="on" />
44                         <ePixmap name="green"  position="140,75" zPosition="4" size="140,40" pixmap="key_green-fs8.png" transparent="1" alphatest="on" />
45                         <widget name="key_red" position="0,75" zPosition="5" size="140,40" valign="center" halign="center" font="Regular;21" transparent="1" foregroundColor="white" shadowColor="black" shadowOffset="-1,-1" />
46                         <widget name="key_green" position="140,75" zPosition="5" size="140,40" valign="center" halign="center" font="Regular;21" transparent="1" foregroundColor="white" shadowColor="black" shadowOffset="-1,-1" />
47                 </screen>"""
48
49         def __init__(self, session, id):
50                 Screen.__init__(self, session)
51
52                 self.list = [ getConfigListEntry(_("Autoupdate: "), config.plugins.simpleRSS.feed[id].autoupdate), getConfigListEntry(_("Feed URI: "), config.plugins.simpleRSS.feed[id].uri) ]
53
54                 ConfigListScreen.__init__(self, self.list, session)
55
56                 self["key_red"] = Button(_("Cancel"))
57                 self["key_green"] = Button(_("OK"))
58
59                 self["setupActions"] = ActionMap(["SetupActions"],
60                 {
61                         "save": self.save,
62                         "cancel": self.keyCancel
63                 }, -1)
64
65                 self.id = id
66
67         def save(self):
68                 config.plugins.simpleRSS.feed[self.id].save()
69                 config.plugins.simpleRSS.feed.save()
70                 self.close()
71
72 class SimpleRSS(ConfigListScreen, Screen):
73         skin = """
74                 <screen name="SimpleRSS" position="100,100" size="550,400" title="Simple RSS Reader Setup" >
75                         <widget name="config"  position="20,10" size="510,350" scrollbarMode="showOnDemand" />
76                         <ePixmap name="red"    position="0,360"   zPosition="4" size="140,40" pixmap="key_red-fs8.png" transparent="1" alphatest="on" />
77                         <ePixmap name="green"  position="140,360" zPosition="4" size="140,40" pixmap="key_green-fs8.png" transparent="1" alphatest="on" />
78                         <ePixmap name="yellow" position="280,360" zPosition="4" size="140,40" pixmap="key_yellow-fs8.png" transparent="1" alphatest="on" />
79                         <ePixmap name="blue"   position="420,360" zPosition="4" size="140,40" pixmap="key_blue-fs8.png" transparent="1" alphatest="on" />
80                         <widget name="key_red"    position="0,360" zPosition="5" size="140,40" valign="center" halign="center" font="Regular;21" transparent="1" foregroundColor="white" shadowColor="black" shadowOffset="-1,-1" />
81                         <widget name="key_green"  position="140,360" zPosition="5" size="140,40" valign="center" halign="center" font="Regular;21" transparent="1" foregroundColor="white" shadowColor="black" shadowOffset="-1,-1" />
82                         <widget name="key_yellow" position="280,360" zPosition="5" size="140,40" valign="center" halign="center"  font="Regular;21" transparent="1" foregroundColor="white" shadowColor="black" shadowOffset="-1,-1" />
83                         <widget name="key_blue"   position="420,360" zPosition="5" size="140,40" valign="center" halign="center"  font="Regular;21" transparent="1" foregroundColor="white" shadowColor="black" shadowOffset="-1,-1" />
84                 </screen>"""
85
86         def __init__(self, session, args = None):
87                 Screen.__init__(self, session)
88
89                 self.onClose.append(self.abort)
90
91                 # nun erzeugen wir eine liste von elementen fuer die menu liste.
92                 self.list = [ ]
93                 for i in range(0, config.plugins.simpleRSS.feedcount.value):
94                         self.list.append(getConfigListEntry(_("Feed: "), config.plugins.simpleRSS.feed[i].uri))
95
96                 self.list.append(getConfigListEntry(_("Show new Messages: "), config.plugins.simpleRSS.show_new))
97                 self.list.append(getConfigListEntry(_("Update Interval (min): "), config.plugins.simpleRSS.interval))
98
99                 # die liste selbst
100                 ConfigListScreen.__init__(self, self.list, session)
101
102                 self["key_red"] = Button(_("Cancel"))
103                 self["key_green"] = Button(_("OK"))
104                 self["key_yellow"] = Button(_("New"))
105                 self["key_blue"] = Button(_("Delete"))
106
107                 self["setupActions"] = ActionMap(["SetupActions", "ColorActions"],
108                 {
109                         "blue": self.delete,
110                         "yellow": self.new,
111                         "save": self.keySave,
112                         "cancel": self.keyCancel,
113                         "ok": self.ok
114                 }, -1)
115         
116         def delete(self):
117                 self.session.openWithCallback(self.deleteConfirm, MessageBox, "Really delete this entry?\nIt cannot be recovered!")
118
119         def deleteConfirm(self, result):
120                 if result:
121                         id = self["config"].instance.getCurrentIndex()
122                         del config.plugins.simpleRSS.feed[id]
123                         config.plugins.simpleRSS.feedcount.value -= 1
124                         self.list.pop(id)
125                         # redraw list
126                         self["config"].setList(self.list)
127
128         def ok(self):
129                 id = self["config"].instance.getCurrentIndex()
130                 self.session.openWithCallback(self.refresh, SimpleRSSEdit, id)
131
132         def refresh(self):
133                 pass
134
135         def new(self):
136                 id = len(config.plugins.simpleRSS.feed)
137                 config.plugins.simpleRSS.feed.append(ConfigSubsection())
138                 config.plugins.simpleRSS.feed[id].uri = ConfigText(default="http://", fixed_size = False)
139                 config.plugins.simpleRSS.feed[id].autoupdate = ConfigEnableDisable(default=True)
140                 self.session.openWithCallback(self.conditionalNew, SimpleRSSEdit, id)
141
142         def conditionalNew(self):
143                 id = len(config.plugins.simpleRSS.feed)-1
144                 # Check if new feed differs from default
145                 if config.plugins.simpleRSS.feed[id].uri.value == "http://":
146                         del config.plugins.simpleRSS.feed[id]
147                 else:
148                         self.list.insert(id, getConfigListEntry(_("Feed: "), config.plugins.simpleRSS.feed[id].uri))
149                         config.plugins.simpleRSS.feedcount.value = id+1
150
151         def keySave(self):
152                 global rssPoller
153                 rssPoller.triggerReload()
154                 ConfigListScreen.keySave(self)
155
156         def abort(self):
157                 print "[SimpleRSS] Closing Setup Dialog"
158                 # Keep feedcount sane
159                 config.plugins.simpleRSS.feedcount.value = len(config.plugins.simpleRSS.feed)
160                 config.plugins.simpleRSS.feedcount.save()
161
162 class RSSList(GUIComponent):
163         def __init__(self, entries):
164                 GUIComponent.__init__(self)
165                 self.list = entries
166                 self.l = eListboxPythonMultiContent()
167                 self.l.setFont(0, gFont("Regular", 22))
168                 self.l.setFont(1, gFont("Regular", 18))
169                 self.l.setBuildFunc(self.buildListboxEntry)
170                 self.l.setList(entries)
171                 
172                 self.onSelectionChanged = [ ]
173                 
174         def connectSelChanged(self, fnc):
175                 if not fnc in self.onSelectionChanged:
176                         self.onSelectionChanged.append(fnc)
177
178         def disconnectSelChanged(self, fnc):
179                 if fnc in self.onSelectionChanged:
180                         self.onSelectionChanged.remove(fnc)
181
182         def selectionChanged(self):
183                 for x in self.onSelectionChanged:
184                         x()
185
186         GUI_WIDGET = eListbox
187
188         def postWidgetCreate(self, instance):
189                 instance.setContent(self.l)
190                 instance.setItemHeight(100)
191                 instance.selectionChanged.get().append(self.selectionChanged)
192
193         def buildListboxEntry(self, title, link, summary, enclosures):
194                 res = [ None ]
195                 width = self.l.getItemSize().width()
196                 res.append(MultiContentEntryText(pos=(0, 0), size=(width, 75), font=0, flags = RT_HALIGN_LEFT|RT_WRAP, text = title))
197                 res.append(MultiContentEntryText(pos=(0, 75), size=(width, 20), font=1, flags = RT_HALIGN_LEFT, text = link))
198                 return res
199
200         def getCurrentEntry(self):
201                 return self.l.getCurrentSelection()
202
203         def getCurrentIndex(self):
204                 return self.instance.getCurrentIndex()
205
206         def moveToIndex(self, index):
207                 self.instance.moveSelectionTo(index)
208
209         def moveToEntry(self, entry):
210                 if entry is None:
211                         return
212
213                 count = 0
214                 for x in self.list:
215                         if entry[0] == x[0]:
216                                 self.instance.moveSelectionTo(count)
217                                 break
218                         count += 1
219
220         def moveDown(self):
221                 self.instance.moveSelection(self.instance.moveDown)
222
223         def moveUp(self):
224                 self.instance.moveSelection(self.instance.moveUp)
225
226 class RSSView(Screen):
227         skin = """
228                 <screen position="100,100" size="460,400" title="Simple RSS Reader" >
229                         <widget name="content" position="0,0" size="460,400" font="Regular; 22" />
230                 </screen>"""
231
232         def __init__(self, session, data, enclosureCB=None, nextCB=None, previousCB=None):
233                 Screen.__init__(self, session)
234
235                 self.enclosureCB = enclosureCB
236                 self.nextCB = nextCB
237                 self.previousCB = previousCB
238
239                 self.data = data
240                 if data is not None:
241                         self["content"] = ScrollLabel("\n\n".join([data[0], data[2], " ".join([str(len(data[3])), "Enclosures"])]))
242                 else:
243                         self["content"] = ScrollLabel()
244
245                 self["actions"] = ActionMap([ "OkCancelActions", "ColorActions", "DirectionActions" ],
246                 {
247                         "cancel": self.close,
248                         "ok": self.selectEnclosure,
249                         "up": self.up,
250                         "down": self.down,
251                         "right": self.next,
252                         "left": self.previous,
253                 })
254
255         def up(self):
256                 self["content"].pageUp()
257
258         def down(self):
259                 self["content"].pageDown()
260
261         def next(self):
262                 if self.nextCB is not None:
263                         self.data = self.nextCB()
264                         self.setContent()
265
266         def previous(self):
267                 if self.previousCB is not None:
268                         self.data = self.previousCB()
269                         self.setContent()
270
271         def setContent(self):
272                 if self.data is not None:
273                         self["content"].setText("\n\n".join([self.data[0], self.data[2], " ".join([str(len(self.data[3])), "Enclosures"])]))
274                 else:
275                         self["content"].setText("")
276
277         def selectEnclosure(self):
278                 if self.data is not None and self.enclosureCB is not None:
279                         self.enclosureCB(self.data)
280
281 class RSSDisplay(Screen):
282         skin = """
283                 <screen position="100,100" size="460,400" title="Simple RSS Reader" >
284                         <widget name="content" position="0,0" size="460,304" scrollbarMode="showOnDemand" />
285                         <widget name="summary" position="0,305" size="460,95" font="Regular;16" />
286                 </screen>"""
287
288         MENU_UPDATE = 1
289         MENU_CONFIG = 2
290
291         def __init__(self, session, data, interactive = False, poller = None):
292                 Screen.__init__(self, session)
293
294                 if interactive:
295                         self["actions"] = ActionMap([ "OkCancelActions", "ChannelSelectBaseActions", "MenuActions" ], 
296                         {
297                                 "ok": self.showCurrentEntry,
298                                 "cancel": self.conditionalClose,
299                                 "nextBouquet": self.next,
300                                 "prevBouquet": self.previous,
301                                 "menu": self.menu
302                         })
303                         self.onShown.append(self.__show)
304                         self.onClose.append(self.__close)
305
306                 self.rssPoller = poller
307                 self.feedview = False
308                 self.feeds = None
309                 self.feedid = None
310                 if len(data):
311                         if isinstance(data[0], Feed):
312                                 self.feedview = True
313                                 # TODO: find better way to solve this
314                                 self.feeds = ([(feed.title, feed.description, ' '.join([str(len(feed.history)), "Entries"]), feed.history) for feed in data])
315                                 self["content"] = RSSList(self.feeds)
316                                 self["summary"] = Label(self.feeds[0][2])
317                         else:
318                                 self["content"] = RSSList(data)
319                                 self["summary"] = Label(data[0][2])
320                 else:
321                         self["content"] = RSSList(data)
322                         self["summary"] = Label("")
323
324                 self["content"].connectSelChanged(self.updateSummary)
325                 self.onLayoutFinish.append(self.setConditionalTitle)
326
327         def __show(self):
328                 if rssPoller is not None:
329                         self.rssPoller.addCallback(self.pollCallback)
330
331         def __close(self):
332                 if rssPoller is not None:
333                         self.rssPoller.removeCallback(self.pollCallback)
334
335         def pollCallback(self, id = None):
336                 print "[SimpleRSS] RSSDisplay called back"
337                 current_entry = self["content"].getCurrentEntry()
338
339                 if self.feeds:
340                         if id is not None:
341                                 print "[SimpleRSS] pollCallback updating feed", id
342                                 self.feeds[id] = (self.rssPoller.feeds[id].title, self.rssPoller.feeds[id].description, ' '.join([str(len(self.rssPoller.feeds[id].history)), "Entries"]), self.rssPoller.feeds[id].history)
343                         else:
344                                 print "[SimpleRSS] pollCallback updating all feeds"
345                                 self.feeds = ([(feed.title, feed.description, ' '.join([str(len(feed.history)), "Entries"]), feed.history) for feed in self.rssPoller.feeds])
346
347                 if self.feedview:
348                         print "[SimpleRSS] pollCallback updating Feedlist"
349                         self["content"].l.setList(self.feeds)
350                 elif self.feedid:
351                         print "[SimpleRSS] pollCallback updating Itemlist"
352                         self["content"].l.setList(self.feeds[self.feedid][3])
353
354                 self["content"].moveToEntry(current_entry)
355                 self.setConditionalTitle()
356                 self.updateSummary()
357
358         def setConditionalTitle(self):
359                 # Feedview: Overview, has feeds
360                 if self.feedview:
361                         self.setTitle("Simple RSS Reader")
362                 # Feedid: Feed, has feeds
363                 elif self.feedid is not None:
364                         self.setTitle(''.join(["Simple RSS Reader: ", self.feeds[self.feedid][0]]))
365                 # None: new_items
366                 else:
367                         self.setTitle("Simple RSS Reader: New Items")
368
369         def updateSummary(self):
370                 current_entry = self["content"].getCurrentEntry()
371                 if current_entry:
372                         self["summary"].setText(current_entry[2])
373                 else:
374                         self["summary"].setText("")
375
376         def menu(self):
377                 self.session.openWithCallback(self.menuChoice, ChoiceBox, "What to do?", [(_("Update Feed"), self.MENU_UPDATE), (_("Setup"), self.MENU_CONFIG)])
378
379         def menuChoice(self, result):
380                 if result:
381                         if result[1] == self.MENU_UPDATE:
382                                 self.rssPoller.singlePoll(self.feedid or self["content"].getCurrentIndex(), self.pollCallback)
383                                 self.session.open(MessageBox, "Update is being done in Background.\nContents will automatically be updated when it's done.", type = MessageBox.TYPE_INFO, timeout = 5)
384                         elif result[1] == self.MENU_CONFIG:
385                                 self.session.openWithCallback(self.menuClosed, SimpleRSS)
386
387         def menuClosed(self):
388                 if self.feeds:
389                         current_entry = self["content"].getCurrentEntry()
390
391                         self.rssPoller.triggerReload()
392
393                         # TODO: fix this, its still as evil as some lines above
394                         self.feeds = ([(feed.title, feed.description, ' '.join([str(len(feed.history)), " Entries"]), feed.history) for feed in rssPoller.feeds])
395                         if self.feedview:
396                                 self["content"].l.setList(self.feeds)
397
398                         self["content"].moveToEntry(current_entry)
399
400         def nextEntryCB(self):
401                 self["content"].moveDown()
402                 return self["content"].getCurrentEntry()
403
404         def previousEntryCB(self):
405                 self["content"].moveUp()
406                 return self["content"].getCurrentEntry()
407
408         def next(self):
409                 if not self.feedview and self.feeds:
410                         self.feedid += 1
411                         if self.feedid == len(self.feeds):
412                                 self.feedid = 0
413                         self["content"].l.setList(self.feeds[self.feedid][3])
414                         self["content"].moveToIndex(0)
415                         self.updateSummary()
416                         self.setConditionalTitle()
417
418         def previous(self):
419                 if not self.feedview and self.feeds:
420                         if self.feedid == 0:
421                                 self.feedid = len(self.feeds)
422                         self.feedid -= 1
423                         self["content"].l.setList(self.feeds[self.feedid][3])
424                         self["content"].moveToIndex(0)
425                         self.updateSummary()
426                         self.setConditionalTitle()
427
428         def conditionalClose(self):
429                 if not self.feedview and self.feeds:
430                         self["content"].l.setList(self.feeds)
431                         self["content"].moveToIndex(self.feedid)
432                         self.feedview = True
433                         self.feedid = None
434                         self.updateSummary()
435                         self.setConditionalTitle()
436                 else:
437                         self.close()
438
439         def showCurrentEntry(self):
440                 current_entry = self["content"].getCurrentEntry()
441                 if current_entry is None: # empty list
442                         return
443
444                 # If showing feeds right now show items of marked feed
445                 if self.feedview:
446                         self.feedid = self["content"].getCurrentIndex()
447                         self["content"].l.setList(current_entry[3])
448                         self["content"].moveToIndex(0)
449                         self.feedview = False
450                         self.updateSummary()
451                         self.setConditionalTitle()
452                 # Else we're showing items -> show marked item
453                 else:
454                         self.session.open(RSSView, current_entry, enclosureCB=self.selectEnclosure, nextCB=self.nextEntryCB, previousCB=self.previousEntryCB)
455
456         def selectEnclosure(self, current_entry = None):
457                 if current_entry is None: # no entry given
458                         current_entry = self["content"].getCurrentEntry()
459
460                 if current_entry is None: # empty list
461                         return
462
463                 # Select stream in ChoiceBox if more than one present
464                 if len(current_entry[3]) > 1:
465                         # TODO: beautify
466                         self.session.openWithCallback(self.enclosureSelected, ChoiceBox, "Select enclosure to play", [(x[0][x[0].rfind("/")+1:], x) for x in current_entry[3]])
467                 # Play if one present
468                 elif len(current_entry[3]):
469                         self.enclosureSelected((None, current_entry[3][0]))
470                 # Nothing if none present
471
472         def enclosureSelected(self, enclosure):
473                 if enclosure:
474                         (url, type) = enclosure[1]
475
476                         print "[SimpleRSS] Trying to play back enclosure: url=%s, type=%s" % (url, type)
477
478                         # TODO: other types? (showing images wouldn't be hard if the source was local)
479                         if type in ["video/mpeg", "audio/mpeg"]:
480                                 # We should launch a Player here, but the MediaPlayer gets angry about our non-local sources
481                                 from enigma import eServiceReference
482                                 self.session.nav.playService(eServiceReference(4097, 0, url))
483
484 class Feed:
485         MAX_HISTORY_ELEMENTS = 100
486
487         RSS = 1
488         ATOM = 2
489
490         def __init__(self, uri, autoupdate):
491                 self.uri = uri
492                 self.autoupdate = autoupdate
493                 self.type = None
494                 self.title = uri.encode("UTF-8")
495                 self.description = ""
496                 self.last_update = None
497                 self.last_ids = set()
498                 self.history = []
499
500         def gotDom(self, dom):
501                 if self.type is None:
502                         # RSS 2.0
503                         if dom.documentElement.getAttribute("version") in ["2.0", "0.94", "0.93", "0.92", "0.91"]:
504                                 self.type = self.RSS
505                                 try:
506                                         self.title = dom.getElementsByTagName("channel")[0].getElementsByTagName("title")[0].childNodes[0].data.encode("UTF-8")
507                                         self.description = dom.getElementsByTagName("channel")[0].getElementsByTagName("description")[0].childNodes[0].data.encode("UTF-8")
508                                 except:
509                                         pass
510                         # RSS 1.0 (NS: http://www.w3.org/1999/02/22-rdf-syntax-ns#)
511                         elif dom.documentElement.localName == "RDF":
512                                 self.type = self.RSS
513                                 try:
514                                         self.title = dom.getElementsByTagName("channel")[0].getElementsByTagName("title")[0].childNodes[0].data.encode("UTF-8")
515                                         self.description = dom.getElementsByTagName("channel")[0].getElementsByTagName("description")[0].childNodes[0].data.encode("UTF-8")
516                                 except:
517                                         pass
518                         # Atom (NS: http://www.w3.org/2005/Atom)
519                         elif dom.documentElement.localName == "feed":
520                                 self.type = self.ATOM
521                                 try:
522                                         self.title = dom.getElementsByTagName("title")[0].childNodes[0].data.encode("UTF-8")
523                                         self.description = dom.getElementsByTagName("subtitle")[0].childNodes[0].data.encode("UTF-8")
524                                 except:
525                                         pass
526                         else:
527                                 raise NotImplementedError, 'Unsupported Feed: %s' % dom.documentElement.localName
528                 if self.type == self.RSS:
529                         print "[SimpleRSS] type is rss"
530                         return self.gotRSSDom(dom)
531                 elif self.type == self.ATOM:
532                         print "[SimpleRSS] type is atom"
533                         return self.gotAtomDom(dom)
534
535         def gotRSSDom(self, dom):
536                 # Try to read when feed was last updated, if time equals return empty list. else fetch new items
537                 try:
538                         updated = dom.getElementsByTagName("lastBuildDate")[0].childNodes[0].data
539                         if not self.last_update == updated:
540                                 self.last_update = updated
541                                 return self.parseRSS(dom.getElementsByTagName("item"))
542                         else:
543                                 return [ ]
544                 except:
545                         return self.parseRSS(dom.getElementsByTagName("item"))
546
547         def parseRSS(self, items):
548                 new_items = []
549                 for item in items:
550                         enclosure = []
551
552                         # Try to read title, continue if none found
553                         try:
554                                 title = item.getElementsByTagName("title")[0].childNodes[0].data
555                         except:
556                                 continue
557
558                         # Try to read link, empty if none
559                         try:
560                                 link = item.getElementsByTagName("link")[0].childNodes[0].data
561                         except:
562                                 link = ""
563                         
564                         # Try to read guid, link if none (RSS 1.0 or invalid RSS 2.0)
565                         try:
566                                 guid = item.getElementsByTagName("guid")[0].childNodes[0].data
567                         except:
568                                 guid = link
569
570                         # Continue if item is to be excluded
571                         if guid in self.last_ids:
572                                 continue
573
574                         # Try to read summary (description element), empty if none
575                         try:
576                                 summary = item.getElementsByTagName("description")[0].childNodes[0].data
577                         except:
578                                 summary = ""
579
580                         # Read out enclosures
581                         for current in item.getElementsByTagName("enclosure"):
582                                 enclosure.append((current.getAttribute("url").encode("UTF-8"), current.getAttribute("type").encode("UTF-8")))
583
584                         # Update Lists
585                         new_items.append((title.encode("UTF-8").strip(), link.encode("UTF-8").strip(), summary.encode("UTF-8").strip(), enclosure))
586                         self.last_ids.add(guid)
587
588                 # Append known Items to new Items and evenentually cut it
589                 self.history = new_items + self.history
590                 self.history[:self.MAX_HISTORY_ELEMENTS]
591                 
592                 return new_items
593
594         def gotAtomDom(self, dom):
595                 try:
596                         # Try to read when feed was last updated, if time equals return empty list. else fetch new items
597                         updated = dom.getElementsByTagName("updated")[0].childNodes[0].data
598                         if not self.last_update == updated:
599                                 self.last_update = updated
600                                 return self.parseAtom(dom.getElementsByTagName("entry"))
601                         else:
602                                 return [ ]
603                 except:
604                         return self.parseAtom(dom.getElementsByTagName("entry"))
605
606         def parseAtom(self, items):
607                 new_items = []
608                 for item in items:
609                         enclosure = []
610                         link = ""
611                         
612                         # Try to read title, continue if none found
613                         try:
614                                 title = item.getElementsByTagName("title")[0].childNodes[0].data
615                         except:
616                                 continue
617
618                         # Try to read id, continue if none found (invalid feed, should be handled differently) or to be excluded
619                         try:
620                                 id = item.getElementsByTagName("id")[0].childNodes[0].data
621                                 if id in self.last_ids:
622                                         continue
623                         except:
624                                 continue
625
626                         # Read out enclosures and link
627                         for current in item.getElementsByTagName("link"):
628                                 # Enclosure
629                                 if current.getAttribute("rel") == "enclosure":
630                                         enclosure.append((current.getAttribute("href").encode("UTF-8"), current.getAttribute("type").encode("UTF-8")))
631                                 # No Enclosure, assume its a link to the item
632                                 else:
633                                         link = current.getAttribute("href")
634                         
635                         # Try to read summary, empty if none
636                         try:
637                                 summary = item.getElementsByTagName("summary")[0].childNodes[0].data
638                         except:
639                                 summary = ""
640
641                         # Update Lists
642                         new_items.append((title.encode("UTF-8").strip(), link.encode("UTF-8").strip(), summary.encode("UTF-8").strip(), enclosure))
643                         self.last_ids.add(id)
644
645                  # Append known Items to new Items and evenentually cut it
646                 self.history = new_items + self.history
647                 self.history[:self.MAX_HISTORY_ELEMENTS]
648
649                 return new_items
650
651 class RSSPoller:
652         def __init__(self, session):
653                 self.poll_timer = eTimer()
654                 self.poll_timer.timeout.get().append(self.poll)
655                 self.poll_timer.start(0, 1)
656                 self.update_callbacks = [ ]
657                 self.last_links = Set()
658                 self.session = session
659                 self.dialog = None
660                 self.reloading = False
661         
662                 self.feeds = [ ]
663                 for i in range(0, config.plugins.simpleRSS.feedcount.value):
664                         self.feeds.append(Feed(config.plugins.simpleRSS.feed[i].uri.value, config.plugins.simpleRSS.feed[i].autoupdate.value))
665                 self.new_items = [ ]
666                 self.current_feed = 0
667
668         def addCallback(self, callback):
669                 if callback not in self.update_callbacks:
670                         self.update_callbacks.append(callback)
671
672         def removeCallback(self, callback):
673                 if callback in self.update_callbacks:
674                         self.update_callbacks.remove(callback)
675
676         def doCallback(self):
677                 for callback in self.update_callbacks:
678                         try:
679                                 callback()
680                         except:
681                                 pass
682
683         # Single Functions are here to wrap
684         def _gotSinglePage(self, id, callback, errorback, data):
685                 self._gotPage(data, id, callback, errorback)
686
687         def singleError(self, errorback, error):
688                 self.error(error, errorback)
689
690         def error(self, error, errorback = None):
691                 if not self.session:
692                         print "[SimpleRSS] error polling"
693                 elif errorback:
694                         errorback(error)
695                 else:
696                         self.session.open(MessageBox, "Sorry, failed to fetch feed.\n" + error, type = MessageBox.TYPE_INFO, timeout = 5)
697                         # Assume its just a temporary failure and jump over to next feed                          
698                         self.current_feed += 1                     
699                         self.poll_timer.start(1000, 1)
700
701         def _gotPage(self, data, id = None, callback = None, errorback = None):
702                 # workaround: exceptions in gotPage-callback were ignored
703                 try:
704                         self.gotPage(data, id)
705                         if callback is not None:
706                                 callback(id)
707                 except NotImplementedError, errmsg:
708                         # TODO: Annoying with Multifeed?
709                         self.session.open(MessageBox, "Sorry, this type of feed is unsupported.\n"+ str(errmsg), type = MessageBox.TYPE_INFO, timeout = 5)
710                 except:
711                         import traceback, sys
712                         traceback.print_exc(file=sys.stdout)
713                         if errorback is not None:
714                                 errorback()
715                         # Assume its just a temporary failure and jump over to next feed
716                         self.current_feed += 1
717                         self.poll_timer.start(1000, 1)
718         
719         def gotPage(self, data, id = None):
720                 print "[SimpleRSS] parsing.."
721
722                 # sometimes activates spinner :-/
723                 dom = xml.dom.minidom.parseString(data)
724
725                 print "[SimpleRSS] xml parsed.."
726
727                 # For Single-Polling
728                 if id is not None:
729                         self.feeds[id].gotDom(dom)
730                         print "[SimpleRSS] single feed parsed.."
731                         return
732                 else:
733                         new_items = self.feeds[self.current_feed].gotDom(dom)
734
735                 print "[SimpleRSS] feed parsed.."
736
737                 # Append new items to locally bound ones
738                 self.new_items.extend(new_items)
739
740                 # Start Timer so we can either fetch next feed or show new_items
741                 self.current_feed += 1
742                 self.poll_timer.start(1000, 1)
743
744
745         def singlePoll(self, id, callback = None, errorback = None):
746                 from Tools.BoundFunction import boundFunction
747                 remote = urlsplit(self.feeds[id].uri)
748                 print "[SimpleRSS] updating", remote.geturl()
749                 hostname = remote.hostname
750                 port = remote.port or 80
751                 path = '?'.join([remote.path, remote.query])
752                 print "[SimpleRSS] hostname:", hostname, ", port:", port, ", path:", path
753                 getPage(hostname, port, path, callback=boundFunction(self._gotSinglePage, id, callback, errorback), errorback=boundFunction(self.error, errorback))
754
755         def poll(self):
756                 # Reloading, reschedule
757                 if self.reloading:
758                         print "[SimpleRSS] timer triggered while reloading, rescheduling"
759                         self.poll_timer.start(10000, 1)
760                 # Dialog shown, hide
761                 elif self.dialog:
762                         print "[SimpleRSS] hiding"
763                         self.dialog.hide()
764                         self.dialog = None
765                         self.new_items = [ ]
766                         self.current_feed = 0
767                         self.poll_timer.startLongTimer(config.plugins.simpleRSS.interval.value*60)
768                 # End of List
769                 elif len(self.feeds) <= self.current_feed:
770                         # New Items
771                         if len(self.new_items):
772                                 print "[SimpleRSS] got", len(self.new_items), "new items"
773                                 print "[SimpleRSS] calling back"
774                                 self.doCallback()
775                                 # Inform User
776                                 if config.plugins.simpleRSS.show_new.value:
777                                         self.dialog = self.session.instantiateDialog(RSSDisplay, self.new_items, poller = self)
778                                         self.dialog.show()
779                                         self.poll_timer.startLongTimer(5)
780                         # No new Items
781                         else:
782                                 print "[SimpleRSS] no new items"
783                                 self.new_items = [ ]
784                                 self.current_feed = 0
785                                 self.poll_timer.startLongTimer(config.plugins.simpleRSS.interval.value*60)
786                 # Feed is supposed to auto-update
787                 elif self.feeds[self.current_feed].autoupdate:
788                         remote = urlsplit(self.feeds[self.current_feed].uri)
789                         hostname = remote.hostname
790                         port = remote.port or 80
791                         path = '?'.join([remote.path, remote.query])
792                         print "[SimpleRSS] hostname:", hostname, ", port:", port, ", path:", path
793                         self.d = getPage(hostname, port, path, callback=self._gotPage, errorback=self.error)
794                 # Go to next feed in 100ms
795                 else:
796                         print "[SimpleRSS] passing feed"
797                         self.current_feed += 1
798                         self.poll_timer.start(100, 1)
799
800         def shutdown(self):
801                 self.poll_timer.timeout.get().remove(self.poll)
802                 self.poll_timer = None
803
804         def triggerReload(self):
805                 self.reloading = True
806
807                 # TODO: Fix this evil way of updating feeds
808                 newfeeds = []
809                 for i in range(0, config.plugins.simpleRSS.feedcount.value):
810                         newfeeds.append(Feed(config.plugins.simpleRSS.feed[i].uri.value, config.plugins.simpleRSS.feed[i].autoupdate.value))
811
812                 self.feeds = newfeeds
813
814                 self.reloading = False
815
816 def main(session, **kwargs):
817         print "[SimpleRSS] Displaying SimpleRSS-Setup"
818         session.open(SimpleRSS)
819
820 rssPoller = None
821
822 def autostart(reason, **kwargs):
823         global rssPoller
824         
825         # not nice (?), but works
826         if kwargs.has_key("session") and reason == 0:
827                 rssPoller = RSSPoller(kwargs["session"])
828         elif reason == 1:
829                 rssPoller.shutdown()
830                 rssPoller = None
831
832 def showCurrent(session, **kwargs):
833         global rssPoller
834         if rssPoller is None:
835                 return
836         session.open(RSSDisplay, rssPoller.feeds, interactive = True, poller = rssPoller)
837
838 def Plugins(**kwargs):
839         return [ PluginDescriptor(name="RSS Reader", description="A (really) simple RSS reader", where = PluginDescriptor.WHERE_PLUGINMENU, fnc=main),
840                 PluginDescriptor(where = [PluginDescriptor.WHERE_SESSIONSTART, PluginDescriptor.WHERE_AUTOSTART], fnc = autostart),
841                 PluginDescriptor(name="View RSS", description="Let's you view current RSS entries", where = PluginDescriptor.WHERE_EXTENSIONSMENU, fnc=showCurrent) ]