initial checkin of MerlinSkinThemes
[enigma2-plugins.git] / podcast / src / plugin.py
1 ##
2 ## Podcast
3 ## by AliAbdul
4 ##
5 from Components.ActionMap import ActionMap
6 from Components.config import config, ConfigSelection, ConfigSubsection, ConfigText, ConfigYesNo, getConfigListEntry
7 from Components.ConfigList import ConfigListScreen
8 from Components.FileList import FileList
9 from Components.Label import Label
10 from Components.Language import language
11 from Components.MenuList import MenuList
12 from Components.PluginComponent import plugins
13 from Components.ProgressBar import ProgressBar
14 from enigma import eServiceReference, eTimer, eEnv
15 from os import environ, system
16 from Plugins.Plugin import PluginDescriptor
17 from Screens.InfoBar import MoviePlayer
18 from Screens.MessageBox import MessageBox
19 from Screens.TextBox import TextBox
20 from Screens.Screen import Screen
21 from Tools.BoundFunction import boundFunction
22 from Tools.Directories import fileExists, resolveFilename, SCOPE_PLUGINS
23 from Tools.Downloader import downloadWithProgress
24 from twisted.web.client import getPage
25 from xml.etree.cElementTree import parse
26 from xml.dom.minidom import parseString as xmlparseString, parse as xmlparse
27 import re, urllib2
28
29 ###################################################
30
31 configDir = eEnv.resolve("${sysconfdir}") + "/podcast/"
32
33 ###################################################
34
35 def remove(file):
36         system('rm "' + file + '"')
37
38 ###################################################
39
40 class ChangedMoviePlayer(MoviePlayer):
41         def __init__(self, session, service):
42                 MoviePlayer.__init__(self, session, service)
43                 self.skinName = "MoviePlayer"
44
45         def leavePlayer(self):
46                 self.session.openWithCallback(self.leavePlayerConfirmed, MessageBox, _("Stop playing this movie?"))
47
48         def leavePlayerConfirmed(self, answer):
49                 if answer:
50                         self.close()
51
52         def doEofInternal(self, playing):
53                 pass
54
55         def getPluginList(self):
56                 list = []
57                 for p in plugins.getPlugins(where=PluginDescriptor.WHERE_EXTENSIONSMENU):
58                         if p.name != _("Podcast"):
59                                 list.append(((boundFunction(self.getPluginName, p.name), boundFunction(self.runPlugin, p), lambda: True), None))
60                 return list
61
62         def showMovies(self):
63                 pass
64
65 ###################################################
66
67 config.plugins.Podcast = ConfigSubsection()
68 config.plugins.Podcast.buffer = ConfigYesNo(default=True)
69 config.plugins.Podcast.bufferDevice = ConfigText(default="/media/hdd/", fixed_size=False)
70 config.plugins.Podcast.keepStored = ConfigSelection(choices={"delete": _("delete"), "keep": _("keep on device"), "ask": _("ask me")}, default="delete")
71
72 ###################################################
73
74 def encodeUrl(url):
75         url = url.replace("&", "&")
76         url = url.replace("&lt;", "<")
77         url = url.replace("&gt;", ">")
78         url = url.replace("&#39;", "'")
79         url = url.replace("&quot;", '"')
80         url = url.replace("&#42;", "*")
81         url = url.replace("&#124;", "|")
82         url = url.replace("&#039;", "'")
83         url = url.replace("&#187;", ">>")
84         return url
85
86 def getText(nodelist):
87         rc = []
88         for node in nodelist:
89                 if node.nodeType == node.TEXT_NODE:
90                         rc.append(node.data)
91         return ''.join(rc)
92                                 
93 ###################################################
94
95 class BufferThread():
96         def __init__(self):
97                 self.progress = 0
98                 self.downloading = False
99                 self.error = ""
100                 self.download = None
101
102         def startDownloading(self, url, file):
103                 self.progress = 0
104                 self.downloading = True
105                 self.error = ""
106                 self.download = downloadWithProgress(url, file)
107                 self.download.addProgress(self.httpProgress)
108                 self.download.start().addCallback(self.httpFinished).addErrback(self.httpFailed)
109
110         def httpProgress(self, recvbytes, totalbytes):
111                 self.progress = int(100 * recvbytes / float(totalbytes))
112
113         def httpFinished(self, string=""):
114                 self.downloading = False
115                 if string is not None:
116                         self.error = str(string)
117                 else:
118                         self.error = ""
119
120         def httpFailed(self, failure_instance=None, error_message=""):
121                 self.downloading = False
122                 if error_message == "" and failure_instance is not None:
123                         error_message = failure_instance.getErrorMessage()
124                         self.error = str(error_message)
125
126         def stop(self):
127                 self.progress = 0
128                 self.downloading = False
129                 self.error = ""
130                 self.download.stop()
131
132 bufferThread = BufferThread()
133
134 ###################################################
135
136 class PodcastBuffer(Screen):
137         skin = """
138                 <screen position="center,center" size="520,180" title="%s" >
139                         <widget name="info" position="5,5" size="510,140" font="Regular;18" halign="center" valign="center" />
140                         <widget name="progress" position="100,150" size="320,14" pixmap="skin_default/progress_big.png" borderWidth="2" borderColor="#cccccc" />
141                 </screen>""" % _("Podcast")
142
143         def __init__(self, session, url, file):
144                 self.session = session
145                 Screen.__init__(self, session)
146                 
147                 self.url = url
148                 self.file = file
149                 
150                 self.infoTimer = eTimer()
151                 self.infoTimer_conn = self.infoTimer.timeout.connect(self.updateInfo)
152                 
153                 self["info"] = Label(_("Downloading movie: %s") % self.file)
154                 self["progress"] = ProgressBar()
155                 
156                 self["actions"] = ActionMap(["OkCancelActions"], {"ok": self.okClicked, "cancel": self.exit}, -1)
157                 
158                 self.onLayoutFinish.append(self.downloadMovie)
159
160         def downloadMovie(self):
161                 bufferThread.startDownloading(self.url, self.file)
162                 self.infoTimer.start(300, False)
163
164         def updateInfo(self):
165                 if bufferThread.error != "":
166                         self["info"].setText(bufferThread.error)
167                         self.infoTimer.stop()
168                 else:
169                         progress = int(bufferThread.progress)
170                         self["progress"].setValue(progress)
171                         if progress == 100:
172                                 self.infoTimer.stop()
173                                 self.close(True)
174
175         def okClicked(self):
176                 if int(bufferThread.progress) > 0:
177                         self.infoTimer.stop()
178                         self.close(True)
179
180         def exit(self):
181                 bufferThread.download.stop()
182                 self.close(None)
183
184 ###################################################
185
186 class PodcastMovies(Screen):
187         skin = """
188                 <screen position="center,center" size="600,460" title="%s" >
189                         <widget name="list" position="5,5" size="590,250" scrollbarMode="showOnDemand" />
190                         <eLabel position="5,260" size="600,2" backgroundColor="#ffffff" />
191                         <widget name="info" position="5,265" size="600,190" font="Regular;18" />
192                 </screen>""" % _("Podcast")
193
194         def __init__(self, session, url):
195                 self.session = session
196                 Screen.__init__(self, session)
197                 
198                 self.url = url
199                 self.list = []
200                 self.movies = []
201                 self.working = True
202                 
203                 self["list"] = MenuList([])
204                 self["list"].onSelectionChanged.append(self.showInfo)
205                 self["info"] = Label()
206                 
207                 self["actions"] = ActionMap(["OkCancelActions"], {"ok": self.ok, "cancel": self.exit}, -1)
208                 
209                 self.onLayoutFinish.append(self.downloadMovies)
210
211         def ok(self):
212                 if self.working == False:
213                         if len(self.list) > 0:
214                                 idx = self["list"].getSelectionIndex()
215                                 (url, length, type) = self.movies[idx][1]
216                                 if config.plugins.Podcast.buffer.value:
217                                         file = url
218                                         while file.__contains__("/"):
219                                                 idx = file.index("/")
220                                                 file = file[idx+1:]
221                                         self.file = "%s%s" % (config.plugins.Podcast.bufferDevice.value, file)
222                                         self.session.openWithCallback(self.bufferCallback, PodcastBuffer, url, self.file)
223                                 else:
224                                         ref = eServiceReference(4097, 0, url)
225                                         self.session.open(ChangedMoviePlayer, ref)
226
227         def bufferCallback(self, callback):
228                 if callback is not None:
229                         ref = eServiceReference(4097, 0, self.file)
230                         self.session.openWithCallback(self.delete, ChangedMoviePlayer, ref)
231
232         def delete(self, callback=None):
233                 if bufferThread.downloading: #still downloading?
234                         bufferThread.stop()
235                 if config.plugins.Podcast.keepStored.value == "delete":
236                         remove(self.file)
237                 elif config.plugins.Podcast.keepStored.value == "ask":
238                         self.session.openWithCallback(self.deleteCallback, MessageBox, _("Delete this movie?"))
239
240         def deleteCallback(self, callback):
241                 if callback:
242                         remove(self.file)
243
244         def exit(self):
245                 if self.working == False:
246                         self.close()
247
248         def downloadMovies(self):
249                 getPage(self.url).addCallback(self.showMovies).addErrback(self.error)
250
251         def showMovies(self, page):
252                 if '<item>' in page:
253                         dom = xmlparseString(page)
254                         items = dom.getElementsByTagName("item")
255                 else:
256                         item = xmlparseString(page)
257                         items = [item]  
258                 
259                 for item in items:
260                         title = getText(item.getElementsByTagName("title")[0].childNodes).encode('utf8')
261                         description = getText(item.getElementsByTagName("description")[0].childNodes).encode('utf8')
262                         url = item.getElementsByTagName("enclosure")[0].getAttribute("url").encode('utf8')
263                         if url == "":
264                                 url = "N/A"
265                         length = item.getElementsByTagName("enclosure")[0].getAttribute("length").encode('utf8')
266                         if length == "":
267                                 length = "N/A" 
268                         type = item.getElementsByTagName("enclosure")[0].getAttribute("type").encode('utf8')
269                         if type == "":
270                                 type = "N/A"
271                         self.list.append(encodeUrl(title))
272                         self.movies.append([description, (url, length, type)])
273                         
274                 self["list"].setList(self.list)
275                 self.showInfo()
276                 self.working = False
277
278         def error(self, error=""):
279                 print "[Podcast] Error:", error
280                 self.instance.setTitle(_("Error getting movies"))
281                 self.working = False
282
283         def showInfo(self):
284                 if len(self.list) > 0:
285                         idx = self["list"].getSelectionIndex()
286                         description = self.movies[idx][0]
287                         (url, length, type) = self.movies[idx][1]
288                         self["info"].setText("%s: %s   %s: %s\n%s" % (_("Length"), length, _("Type"), type, encodeUrl(description)))
289
290 ###################################################
291
292 class PodcastPodcasts(Screen):
293         skin = """
294                 <screen position="center,center" size="420,360" title="%s" >
295                         <widget name="list" position="0,0" size="420,350" scrollbarMode="showOnDemand" />
296                 </screen>""" % _("Podcast")
297
298         def __init__(self, session, provider):
299                 self.session = session
300                 Screen.__init__(self, session)
301                 
302                 self["actions"] = ActionMap(["OkCancelActions"], {"ok": self.ok, "cancel": self.close}, -1)
303                 
304                 self.urls = []
305                 list = []
306                 for podcast in provider.findall("podcast"):
307                         name = podcast.get("name") or None
308                         name = name.encode("UTF-8") or name
309                         url = podcast.get("url") or None
310                         if name and url:
311                                 list.append(name)
312                                 self.urls.append(url)
313                 self["list"] = MenuList(list)
314
315         def ok(self):
316                 if len(self.urls) > 0:
317                         cur = self.urls[self["list"].getSelectedIndex()]
318                         self.session.open(PodcastMovies, cur)
319
320 ###################################################
321
322 class PodcastProvider(Screen):
323         skin = """
324                 <screen position="center,center" size="420,360" title="%s" >
325                         <widget name="list" position="0,0" size="420,350" scrollbarMode="showOnDemand" />
326                 </screen>""" % _("Podcast")
327
328         def __init__(self, session, language):
329                 self.session = session
330                 Screen.__init__(self, session)
331                 
332                 self["actions"] = ActionMap(["OkCancelActions"], {"ok": self.ok, "cancel": self.close}, -1)
333                 
334                 self.providers = []
335                 list = []
336                 for provider in language.findall("provider"):
337                         name = provider.get("name") or None
338                         name = name.encode("UTF-8") or name
339                         if name:
340                                 list.append(name)
341                                 self.providers.append(provider)
342                 self["list"] = MenuList(list)
343
344         def ok(self):
345                 if len(self.providers) > 0:
346                         cur = self.providers[self["list"].getSelectedIndex()]
347                         self.session.open(PodcastPodcasts, cur)
348
349 ###################################################
350
351 class PodcastXML(Screen):
352         skin = """
353                 <screen position="center,center" size="420,360" title="%s" >
354                         <widget name="list" position="0,0" size="420,350" scrollbarMode="showOnDemand" />
355                 </screen>""" % _("Podcast")
356
357         def __init__(self, session):
358                 self.session = session
359                 Screen.__init__(self, session)
360                 
361                 self["actions"] = ActionMap(["OkCancelActions"], {"ok": self.ok, "cancel": self.close}, -1)
362                 
363                 self.languages = []
364                 list = []
365                 file = None
366                 source = None
367
368                 # try user defined list, else fall back to default
369                 if fileExists(configDir + "podcasts_local.xml"):
370                         fileName = configDir + "podcasts_local.xml"
371                 elif fileExists(configDir + "podcasts.xml"):
372                         fileName = configDir + "podcasts.xml"
373                 else:
374                         fileName = resolveFilename(SCOPE_PLUGINS, "Extensions/Podcast/podcasts.xml")
375                                 
376                 try:
377                         file = open(fileName)
378                 except:
379                         pass
380                 
381                 if file:
382                         # check if file is just a proxy to an external XML
383                         head = file.readline()
384                         print head
385                         if head.startswith("http"):
386                                 print "open url"
387                                 file.close
388                                 try:
389                                         source = urllib2.urlopen(head)
390                                 except:
391                                         pass
392                         else:
393                                 file.close
394                                 source = open(fileName)
395                         
396                         if source:
397                                 try:
398                                         xml = parse(source).getroot()
399                                         for language in xml.findall("language"):
400                                                 name = language.get("name") or None
401                                                 name = name.encode("UTF-8") or name
402                                                 if name:
403                                                         list.append(name)
404                                                         self.languages.append(language)
405                                 except:
406                                         pass
407                                 source.close()
408                         
409                 self["list"] = MenuList(list)
410
411         def ok(self):
412                 if len(self.languages) > 0:
413                         cur = self.languages[self["list"].getSelectedIndex()]
414                         self.session.open(PodcastProvider, cur)
415
416 ###################################################
417
418 # Sadly Feedly OPML URL is not stable, seems to change after a while :(
419 # Deactivated in selection
420 class PodcastFeedly(Screen):
421         skin = """
422                 <screen position="center,center" size="420,360" title="%s" >
423                         <widget name="list" position="0,0" size="420,350" scrollbarMode="showOnDemand" />
424                 </screen>""" % _("Podcast")
425                 
426         def __init__(self, session):
427                 self.session = session
428                 Screen.__init__(self, session)
429                 
430                 self["actions"] = ActionMap(["OkCancelActions"], {"ok": self.ok, "cancel": self.close}, -1)
431                 self.urls = []
432                 list = []
433                 file = None
434                 
435                 fileName = configDir + "feedly.opml"
436
437                 try:
438                         if fileExists(fileName):
439                                 file = open(fileName)
440                         else:
441                                 list.append(_("No Feedly configuration"))
442                 except:
443                         pass
444
445                 if file:
446                         # check if file is just a proxy to an external XML
447                         head = file.readline()
448
449                         try:
450                                 if head.startswith("http"):
451                                         file.close
452                                         source = urllib2.urlopen(head)
453                                 else:
454                                         file.close
455                                         source = open(fileName)
456                         except:
457                                 pass
458                         
459                         if source:      
460                                 dom = xmlparse(source)
461                                 for item in dom.getElementsByTagName("outline"):
462                                         if str(item.getAttribute("title")) == "PodcastPlugin":
463                                                 for podcast in item.getElementsByTagName("outline"):
464                                                         list.append(str(podcast.getAttribute("title")))
465                                                         self.urls.append(str(podcast.getAttribute("xmlUrl")))
466
467                 self["list"] = MenuList(list)
468
469         def ok(self):
470                 if len(self.urls) > 0:
471                         cur = self.urls[self["list"].getSelectedIndex()]
472                         self.session.open(PodcastMovies, cur)
473                                                                                                 
474 ###################################################
475
476 class LocationSelection(Screen):
477         skin = """
478         <screen position="center,center" size="560,300" title="%s">
479                 <ePixmap pixmap="skin_default/buttons/red.png" position="0,0" size="140,40" transparent="1" alphatest="on" />
480                 <ePixmap pixmap="skin_default/buttons/green.png" position="140,0" size="140,40" transparent="1" alphatest="on" />
481                 <ePixmap pixmap="skin_default/buttons/yellow.png" position="280,0" size="140,40" transparent="1" alphatest="on" />
482                 <ePixmap pixmap="skin_default/buttons/blue.png" position="420,0" size="140,40" transparent="1" alphatest="on" />
483                 <widget name="key_green" position="140,0" zPosition="1" size="140,40" font="Regular;20" valign="center" halign="center" backgroundColor="#1f771f" transparent="1" />
484                 <widget name="filelist" position="10,45" size="550,255" scrollbarMode="showOnDemand" />
485         </screen>""" % _("Podcast")
486
487         def __init__(self, session, dir="/"):
488                 Screen.__init__(self, session)
489                 
490                 self["key_green"] = Label(_("Select"))
491                 
492                 try: self["filelist"] = FileList(dir, showDirectories=True, showFiles=False)
493                 except: self["filelist"] = FileList("/", showDirectories, showFiles)
494                 
495                 self["actions"] = ActionMap(["ColorActions", "OkCancelActions"],
496                         {
497                                 "ok": self.okClicked,
498                                 "cancel": self.exit,
499                                 "green": self.select
500                         }, -1)
501                 
502                 self.onLayoutFinish.append(self.updateDirectoryName)
503                 
504         def okClicked(self):
505                 if self["filelist"].canDescent():
506                         self["filelist"].descent()
507                         self["filelist"].instance.moveSelectionTo(0)
508                         self.updateDirectoryName()
509
510         def exit(self):
511                 self.close(None)
512
513         def select(self):
514                 dir = self["filelist"].getCurrentDirectory()
515                 if dir is not None:
516                         self.close(dir)
517                 else:
518                         self.close(None)
519
520         def updateDirectoryName(self):
521                 try:
522                         dir = self["filelist"].getCurrentDirectory()
523                         self.instance.setTitle(dir)
524                 except:
525                         self.instance.setTitle("?")
526
527 ###################################################
528
529 class PodcastConfig(ConfigListScreen, Screen):
530         skin = """
531         <screen position="center,center" size="560,180" title="%s">
532                 <ePixmap pixmap="skin_default/buttons/red.png" position="0,0" size="140,40" transparent="1" alphatest="on" />
533                 <ePixmap pixmap="skin_default/buttons/green.png" position="140,0" size="140,40" transparent="1" alphatest="on" />
534                 <ePixmap pixmap="skin_default/buttons/yellow.png" position="280,0" size="140,40" transparent="1" alphatest="on" />
535                 <ePixmap pixmap="skin_default/buttons/blue.png" position="420,0" size="140,40" transparent="1" alphatest="on" />
536                 <widget name="key_green" position="140,0" zPosition="1" size="140,40" font="Regular;20" valign="center" halign="center" backgroundColor="#1f771f" transparent="1" />
537                 <widget name="config" position="10,45" size="550,125" scrollbarMode="showOnDemand" />
538         </screen>""" % _("Podcast")
539
540         def __init__(self, session):
541                 Screen.__init__(self, session)
542                 
543                 self["key_green"] = Label(_("Save"))
544                 
545                 ConfigListScreen.__init__(self, [])
546                         
547                 
548                 self["actions"] = ActionMap(["OkCancelActions", "ColorActions"], {"green": self.save, "cancel": self.exit}, -1)
549                 
550                 self.onLayoutFinish.append(self.createConfig)
551
552         def createConfig(self):
553                 self.deviceEntry = ConfigSelection(choices=[config.plugins.Podcast.bufferDevice.value], default=config.plugins.Podcast.bufferDevice.value)
554                 self["config"].list = [
555                         getConfigListEntry(_("Buffer:"), config.plugins.Podcast.buffer),
556                         getConfigListEntry(_("Buffer device:"), self.deviceEntry),
557                         getConfigListEntry(_("Buffer file handling:"), config.plugins.Podcast.keepStored)]
558
559         def keyLeft(self):
560                 ConfigListScreen.keyLeft(self)
561                 self.handleKeysLeftAndRight()
562
563         def keyRight(self):
564                 ConfigListScreen.keyRight(self)
565                 self.handleKeysLeftAndRight()
566
567         def handleKeysLeftAndRight(self):
568                 sel = self["config"].getCurrent()[1]
569                 if sel == self.deviceEntry:
570                         self.session.openWithCallback(self.locationSelected, LocationSelection, config.plugins.Podcast.bufferDevice.value)
571
572         def locationSelected(self, dir):
573                 if dir is not None and dir != "?":
574                         config.plugins.Podcast.bufferDevice.value = dir
575                         config.plugins.Podcast.bufferDevice.save()
576                         self.createConfig()
577
578         def save(self):
579                 for x in self["config"].list:
580                         x[1].save()
581                 self.close()
582
583         def exit(self):
584                 for x in self["config"].list:
585                         x[1].cancel()
586                 self.close()
587
588 ###################################################
589
590 class Podcast(Screen):
591         skin = """
592                 <screen position="center,center" size="560,360" title="%s" >
593                         <ePixmap pixmap="skin_default/buttons/red.png" position="0,0" size="140,40" transparent="1" alphatest="on" />
594                         <ePixmap pixmap="skin_default/buttons/green.png" position="140,0" size="140,40" transparent="1" alphatest="on" />
595                         <ePixmap pixmap="skin_default/buttons/yellow.png" position="280,0" size="140,40" transparent="1" alphatest="on" />
596                         <ePixmap pixmap="skin_default/buttons/blue.png" position="420,0" size="140,40" transparent="1" alphatest="on" />
597                         <widget name="key_blue" position="420,0" zPosition="1" size="140,40" font="Regular;20" valign="center" halign="center" backgroundColor="#1f771f" transparent="1" />
598                         <widget name="list" position="0,45" size="560,305" scrollbarMode="showOnDemand" />
599                 </screen>""" % _("Podcast")
600
601         def __init__(self, session):
602                 self.session = session
603                 Screen.__init__(self, session)
604                 
605                 self["key_blue"] = Label(_("Help"))
606                 
607                 self["actions"] = ActionMap(["ColorActions", "OkCancelActions"], {"ok": self.ok, "cancel": self.close, "blue": self.help}, -1)
608                 
609
610                 # Feedly removed until found a way to get a stable source URL
611                 self["list"] = MenuList([
612                         _("from xml"),
613                         _("configuration")])
614
615         def ok(self):
616                 cur = self["list"].getCurrent()
617                 if cur == _("from xml"):
618                         self.session.open(PodcastXML)
619                 elif cur == _("Feedly OPML"):
620                         self.session.open(PodcastFeedly)
621                 else:
622                         self.session.open(PodcastConfig)
623
624         def help(self):
625                 localehelpfile = "%s/Extensions/Podcast/help/help_%s" % (resolveFilename(SCOPE_PLUGINS), language.getLanguage()[:2])
626                 fallbackhelpfile = "%s/Extensions/Podcast/help/help_en" % resolveFilename(SCOPE_PLUGINS)
627                 if fileExists(localehelpfile):
628                         helpfile = localehelpfile
629                 else:
630                         helpfile = fallbackhelpfile
631                 h = open(helpfile)
632                 helptext = h.read()
633                 h.close
634                 self.session.open(TextBox, helptext)
635
636 ###################################################
637
638 def main(session, **kwargs):
639         session.open(Podcast)
640
641 def Plugins(**kwargs):
642         return PluginDescriptor(name=_("Podcast"), where=PluginDescriptor.WHERE_EXTENSIONSMENU, fnc=main)