intial checkin of mediadownload.
authorMoritz Venn <ritzmo@users.schwerkraft.elitedvb.net>
Tue, 6 Jan 2009 20:17:25 +0000 (20:17 +0000)
committerMoritz Venn <ritzmo@users.schwerkraft.elitedvb.net>
Tue, 6 Jan 2009 20:17:25 +0000 (20:17 +0000)
can be used as a generic download addon also has a filescanner which is true for all files starting with any of http://, https://, ftp://.
currently simplerss can make use of it to download enclosures.

12 files changed:
Makefile.am
configure.ac
mediadownloader/CONTROL/control [new file with mode: 0644]
mediadownloader/Makefile.am [new file with mode: 0755]
mediadownloader/src/FTPProgressDownloader.py [new file with mode: 0644]
mediadownloader/src/HTTPProgressDownloader.py [new file with mode: 0644]
mediadownloader/src/Makefile.am [new file with mode: 0755]
mediadownloader/src/MediaDownloader.py [new file with mode: 0644]
mediadownloader/src/VariableProgressSource.py [new file with mode: 0644]
mediadownloader/src/__init__.py [new file with mode: 0644]
mediadownloader/src/maintainer.info [new file with mode: 0644]
mediadownloader/src/plugin.py [new file with mode: 0644]

index f831a9d..feabe0f 100644 (file)
@@ -1,2 +1,2 @@
 AUTOMAKE_OPTIONS = gnu
-SUBDIRS = antiscrollbar movietagger webinterface wirelesslan netcaster lastfm logomanager vlcplayer simplerss trafficinfo fritzcall webcamviewer emailclient autotimer epgrefresh werbezapper httpproxy startuptostandby imdb ofdb networkwizard movieretitle moviecut tageditor cdinfo unwetterzentrale youtubeplayer zaphistorybrowser passwordchanger autoresolution mosaic googlemaps rsdownloader permanentclock
+SUBDIRS = antiscrollbar movietagger webinterface wirelesslan netcaster lastfm logomanager vlcplayer simplerss trafficinfo fritzcall webcamviewer emailclient autotimer epgrefresh werbezapper httpproxy startuptostandby imdb ofdb networkwizard movieretitle moviecut tageditor cdinfo unwetterzentrale youtubeplayer zaphistorybrowser passwordchanger autoresolution mosaic googlemaps rsdownloader permanentclock mediadownloader
index c9b7c1f..7f02fd6 100644 (file)
@@ -161,4 +161,6 @@ permanentclock/Makefile
 permanentclock/po/Makefile
 permanentclock/src/Makefile
 
+mediadownloader/Makefile
+mediadownloader/src/Makefile
 ])
diff --git a/mediadownloader/CONTROL/control b/mediadownloader/CONTROL/control
new file mode 100644 (file)
index 0000000..f09ce20
--- /dev/null
@@ -0,0 +1,10 @@
+Package: enigma2-plugin-extensions-mediadownloader
+Version: 0.2-20070927-r0
+Description: Downloader-Plugin for Enigma2
+Architecture: mipsel
+Section: extra
+Priority: optional
+Maintainer: Moritz Venn <moritz.venn@freaque.net>
+Homepage: http://www.ritzmo.de
+Depends: enigma2(>=2.5cvs20080416), twisted-web, python-shell
+Source: http://www.ritzmo.de
diff --git a/mediadownloader/Makefile.am b/mediadownloader/Makefile.am
new file mode 100755 (executable)
index 0000000..308a09c
--- /dev/null
@@ -0,0 +1 @@
+SUBDIRS = src\r
diff --git a/mediadownloader/src/FTPProgressDownloader.py b/mediadownloader/src/FTPProgressDownloader.py
new file mode 100644 (file)
index 0000000..ff0b749
--- /dev/null
@@ -0,0 +1,141 @@
+from twisted.internet import reactor, defer
+from twisted.internet.protocol import Protocol, ClientCreator
+from twisted.protocols.ftp import FTPClient, FTPFileListProtocol
+
+from os import SEEK_END
+
+# XXX: did I ever actually test supportPartial?
+class FTPProgressDownloader(Protocol):
+       """Download to a file from FTP and keep track of progress."""
+
+       def __init__(self, host, port, path, fileOrName, username = 'anonymous', \
+               password = 'my@email.com', writeProgress = None, passive = True, \
+               supportPartial = False, *args, **kwargs):
+
+               timeout = 30
+
+               # We need this later
+               self.path = path
+               self.resume = supportPartial
+
+               # Initialize
+               self.currentlength = 0
+               self.totallength = None
+               if writeProgress and type(writeProgress) is not list:
+                       writeProgress = [ writeProgress ]
+               self.writeProgress = writeProgress
+
+               # Output
+               if isinstance(fileOrName, str):
+                       self.filename = fileOrName
+                       self.file = None
+               else:
+                       self.file = fileOrName
+
+               creator = ClientCreator(reactor, FTPClient, username, password, passive = passive)
+
+               creator.connectTCP(host, port, timeout).addCallback(self.controlConnectionMade).addErrback(self.connectionFailed)
+
+               self.deferred = defer.Deferred()
+
+       def controlConnectionMade(self, ftpclient):
+               # We need the client locally
+               self.ftpclient = ftpclient
+
+               # Try to fetch filesize
+               self.ftpFetchSize()
+
+       # Handle recieved msg
+       def sizeRcvd(self, msgs):
+               # Split up return
+               code, msg = msgs[0].split()
+               if code == '213':
+                       self.totallength = int(msg)
+                       for cb in self.writeProgress or [ ]:
+                               cb(0, self.totallength)
+
+                       # We know the size, so start fetching
+                       self.ftpFetchFile()
+               else:
+                       # Error while reading size, try to list it
+                       self.ftpFetchList()
+
+       def ftpFetchSize(self):
+               d = self.ftpclient.queueStringCommand('SIZE ' + self.path)
+               d.addCallback(self.sizeRcvd).addErrback(self.ftpFetchList)
+
+       # Handle recieved msg
+       def listRcvd(self, *args):
+               # Quit if file not found
+               if not len(self.filelist.files):
+                       self.connectionFailed()
+                       return
+
+               self.totallength = self.filelist.files[0]['size']
+               for cb in self.writeProgress or [ ]:
+                       cb(0, self.totallength)
+
+               # Invalidate list
+               self.filelist = None
+
+               # We know the size, so start fetching
+               self.ftpFetchFile()
+
+       def ftpFetchList(self, *args, **kwargs):
+               self.filelist = FTPFileListProtocol()
+               d = self.ftpclient.list(self.path, self.filelist)
+               d.addCallback(self.listRcvd).addErrback(self.connectionFailed)
+
+       def openFile(self):
+               if self.resume:
+                       file = open(self.filename, 'ab')
+               else:
+                       file = open(self.filename, 'wb')
+
+               return (file, file.tell())
+
+       def ftpFetchFile(self):
+               offset = 0
+
+               # Finally open file
+               if self.file is None:
+                       try:
+                               self.file, offset = self.openFile()
+                       except IOError, ie:
+                               # TODO: handle exception
+                               raise ie
+
+               offset = self.resume and offset or 0
+
+               d = self.ftpclient.retrieveFile(self.path, self, offset = offset)
+               d.addCallback(self.ftpFinish).addErrback(self.connectionFailed)
+
+       def dataReceived(self, data):
+               if not self.file:
+                       return
+
+               if self.writeProgress:
+                       self.currentlength += len(data)
+                       for cb in self.writeProgress:
+                               cb(self.currentlength, self.totallength)
+               try:
+                       # XXX: why did i always seek? do we really need this?
+                       if self.resume:
+                               self.file.seek(0, SEEK_END)
+
+                       self.file.write(data)
+               except IOError, ie:
+                       # TODO: handle exception
+                       self.file = None
+                       raise ie
+
+       def ftpFinish(self, code = 0, message = None):
+               self.ftpclient.quit()
+               if self.file is not None:
+                       self.file.close()
+               self.deferred.callback(code)
+
+       def connectionFailed(self, reason = None):
+               if self.file is not None:
+                       self.file.close()
+               self.deferred.errback(reason)
diff --git a/mediadownloader/src/HTTPProgressDownloader.py b/mediadownloader/src/HTTPProgressDownloader.py
new file mode 100644 (file)
index 0000000..4d290c8
--- /dev/null
@@ -0,0 +1,35 @@
+from twisted.web.client import HTTPDownloader
+
+class HTTPProgressDownloader(HTTPDownloader): 
+       """Download to a file and keep track of progress."""
+
+       def __init__(self, url, fileOrName, writeProgress = None, *args, **kwargs):
+               HTTPDownloader.__init__(self, url, fileOrName, *args, **kwargs)
+
+               # Save callback(s) locally
+               if writeProgress and type(writeProgress) is not list:
+                       writeProgress = [ writeProgress ]
+               self.writeProgress = writeProgress
+
+               # Initialize
+               self.currentlength = 0
+               self.totallength = None
+
+       def gotHeaders(self, headers):
+               # If we have a callback and 'OK' from Server try to get length
+               if self.writeProgress and self.status == '200':
+                       if headers.has_key('content-length'):
+                               self.totallength = int(headers['content-length'][0])
+                               for cb in self.writeProgress:
+                                       cb(0, self.totallength)
+
+               return HTTPDownloader.gotHeaders(self, headers)
+
+       def pagePart(self, data):
+               # If we have a callback and 'OK' from server increment pos
+               if self.writeProgress and self.status == '200':
+                       self.currentlength += len(data)
+                       for cb in self.writeProgress:
+                               cb(self.currentlength, self.totallength)
+
+               return HTTPDownloader.pagePart(self, data)
diff --git a/mediadownloader/src/Makefile.am b/mediadownloader/src/Makefile.am
new file mode 100755 (executable)
index 0000000..70b6b39
--- /dev/null
@@ -0,0 +1,4 @@
+installdir = /usr/lib/enigma2/python/Plugins/Extensions/MediaDownloader\r
+\r
+install_PYTHON = *.py\r
+install_DATA = maintainer.info\r
diff --git a/mediadownloader/src/MediaDownloader.py b/mediadownloader/src/MediaDownloader.py
new file mode 100644 (file)
index 0000000..642e734
--- /dev/null
@@ -0,0 +1,268 @@
+# GUI (Screens)
+from Screens.Screen import Screen
+from Screens.MessageBox import MessageBox
+
+# GUI (Components)
+from Components.ActionMap import ActionMap
+from Components.Label import Label
+
+# Download
+from VariableProgressSource import VariableProgressSource
+
+from Components.config import config
+from urlparse import urlparse, urlunparse
+
+import time
+
+def _parse(url, defaultPort = None):
+       url = url.strip()
+       parsed = urlparse(url)
+       scheme = parsed[0]
+       path = urlunparse(('','')+parsed[2:])
+
+       if defaultPort is None:
+               if scheme == 'https':
+                       defaultPort = 443
+               elif scheme == 'ftp':
+                       defaultPort = 21
+               else:
+                       defaultPort = 80
+
+       host, port = parsed[1], defaultPort
+
+       if '@' in host:
+               username, host = host.split('@')
+               if ':' in username:
+                       username, password = username.split(':')
+               else:
+                       password = ""
+       else:
+               username = ""
+               password = ""
+
+       if ':' in host:
+               host, port = host.split(':')
+               port = int(port)
+
+       if path == "":
+               path = "/"
+
+       return scheme, host, port, path, username, password
+
+def download(url, file, writeProgress = None, contextFactory = None, \
+       *args, **kwargs):
+
+       """Download a remote file and provide current-/total-length.
+
+       @param file: path to file on filesystem, or file-like object.
+       @param writeProgress: function or list of functions taking two parameters (pos, length)
+
+       See HTTPDownloader to see what extra args can be passed if remote file
+       is accessible via http or https. Both Backends should offer supportPartial.
+       """
+
+       scheme, host, port, path, username, password = _parse(url)      
+
+       if scheme == 'ftp':
+               from FTPProgressDownloader import FTPProgressDownloader
+
+               if not (username and password):
+                       username = 'anonymous'
+                       password = 'my@email.com'
+
+               client = FTPProgressDownloader(
+                       host,
+                       port,
+                       path,
+                       file,
+                       username,
+                       password,
+                       writeProgress,
+                       *args,
+                       **kwargs
+               )
+               return client.deferred
+
+       # We force username and password here as we lack a satisfying input method
+       if username and password:
+               from base64 import encodestring
+
+               # twisted will crash if we don't rewrite this ;-)
+               url = scheme + '://' + host + ':' + str(port) + path
+
+               basicAuth = encodestring("%s:%s" % (username, password))
+               authHeader = "Basic " + basicAuth.strip()
+               AuthHeaders = {"Authorization": authHeader}
+
+               if kwargs.has_key("headers"):
+                       kwargs["headers"].update(AuthHeaders)
+               else:
+                       kwargs["headers"] = AuthHeaders
+
+       from HTTPProgressDownloader import HTTPProgressDownloader
+       from twisted.internet import reactor
+
+       factory = HTTPProgressDownloader(url, file, writeProgress, *args, **kwargs)
+       if scheme == 'https':
+               from twisted.internet import ssl
+               if contextFactory is None:
+                       contextFactory = ssl.ClientContextFactory()
+               reactor.connectSSL(host, port, factory, contextFactory)
+       else:
+               reactor.connectTCP(host, port, factory)
+
+       return factory.deferred
+
+class MediaDownloader(Screen):
+       """Simple Plugin which downloads a given file. If not targetfile is specified the user will be asked
+       for a location (see LocationBox). If doOpen is True the Plugin will try to open it after downloading."""
+
+       skin = """<screen name="MediaDownloader" position="100,150" size="540,95" >
+                       <widget name="wait" position="2,10" size="500,30" valign="center" font="Regular;23" />
+                       <widget source="progress" render="Progress" position="2,40" size="536,20" />
+                       <widget name="eta" position="2,65" size="200,30" font="Regular;23" />
+                       <widget name="speed" position="338,65" size="200,30" halign="right" font="Regular;23" />
+               </screen>"""
+
+       def __init__(self, session, file, askOpen = False, downloadTo = None, callback = None):
+               Screen.__init__(self, session)
+
+               # Save arguments local
+               self.file = file
+               self.askOpen = askOpen
+               self.filename = downloadTo
+               self.callback = callback
+
+               # Init what we need for progress callback
+               self.lastLength = 0
+               self.lastTime = 0
+               self.lastApprox = 0
+
+               # Inform user about whats currently done
+               self["wait"] = Label(_("Downloading..."))
+               self["progress"] = VariableProgressSource()
+               self["eta"] = Label(_("ETA ??:?? h")) # XXX: we could just leave eta and speed empty
+               self["speed"] = Label(_("?? kb/s"))
+
+               # Set Limit if we know it already (Server might not tell it)
+               if self.file.size:
+                       self["progress"].writeValues(0, self.file.size*1048576)
+
+               # Call getFilename as soon as we are able to open a new screen
+               self.onExecBegin.append(self.getFilename)
+
+       def getFilename(self):
+               self.onExecBegin.remove(self.getFilename)
+
+               # If we have a filename (downloadTo provided) start fetching
+               if self.filename is not None:
+                       self.fetchFile()
+               # Else open LocationBox to determine where to save
+               else:
+                       # TODO: determine basename without os.path?
+                       from os import path
+                       from Screens.LocationBox import LocationBox
+
+                       self.session.openWithCallback(
+                               self.gotFilename,
+                               LocationBox,
+                               _("Where to save?"),
+                               path.basename(self.file.path),
+                               minFree = self.file.size,
+                               bookmarks = config.plugins.mediadownloader.bookmarks
+                       )
+
+       def gotFilename(self, res):
+               # If we got a filename try to fetch file
+               if res is not None:
+                       self.filename = res
+                       self.fetchFile()
+               # Else close
+               else:
+                       self.close()
+
+       def fetchFile(self):
+               # Fetch file
+               d = download(
+                       self.file.path,
+                       self.filename,
+                       [
+                               self["progress"].writeValues,
+                               self.gotProgress
+                       ]
+               )
+
+               d.addCallback(self.gotFile).addErrback(self.error)
+
+       def gotProgress(self, pos, max):
+               newTime = time.time()
+               # Check if we're called the first time (got total)
+               lastTime = self.lastTime
+               if lastTime == 0:
+                       self.lastTime = newTime
+
+               # We dont want to update more often than every two sec (could be done by a timer, but this should give a more accurate result though it might lag)
+               elif int(newTime - lastTime) >= 2:
+                       newLength = pos
+
+                       lastApprox = round(((newLength - self.lastLength) / (newTime - lastTime) / 1024), 2)
+
+                       secLen = int(round(((max-pos) / 1024) / lastApprox))
+                       self["eta"].setText(_("ETA %d:%02d min") % (secLen / 60, secLen % 60))
+                       self["speed"].setText(_("%d kb/s") % (lastApprox))
+
+                       self.lastApprox = lastApprox
+                       self.lastLength = newLength
+                       self.lastTime = newTime
+
+       def openCallback(self, res):
+               from Components.Scanner import openFile
+
+               # Try to open file if res was True
+               if res and not openFile(self.session, None, self.filename):
+                       self.session.open(
+                               MessageBox,
+                               _("No suitable Viewer found!"),
+                               type = MessageBox.TYPE_ERROR,
+                               timeout = 5
+                       )
+
+               # Calback with Filename on success
+               if self.callback is not None:
+                       self.callback(self.filename)
+
+               self.close()
+
+       def gotFile(self, data = ""):
+               # Ask if file should be opened unless told not to
+               if self.askOpen:
+                       self.session.openWithCallback(
+                               self.openCallback,
+                               MessageBox,
+                               _("Do you want to try to open the downloaded file?"),
+                               type = MessageBox.TYPE_YESNO
+                       )
+               # Otherwise callback and close
+               else:
+                       # Calback with Filename on success
+                       if self.callback is not None:
+                               self.callback(self.filename)
+
+                       self.close()
+
+       def error(self, msg = ""):
+               if msg != "":
+                       print "[MediaDownloader] Error downloading:", msg
+
+               self.session.open(
+                       MessageBox,
+                       _("Error while downloading file %s") % (self.file.path),
+                       type = MessageBox.TYPE_ERROR,
+                       timeout = 3
+               )
+
+               # Calback with None on failure
+               if self.callback is not None:
+                       self.callback(None)
+
+               self.close()
diff --git a/mediadownloader/src/VariableProgressSource.py b/mediadownloader/src/VariableProgressSource.py
new file mode 100644 (file)
index 0000000..f6aed8c
--- /dev/null
@@ -0,0 +1,32 @@
+from Components.Sources.Source import Source
+
+class VariableProgressSource(Source):
+       """Source to feed Progress Renderer from HTTPProgressDownloader"""
+
+       def __init__(self):
+               # Initialize and invalidate
+               Source.__init__(self)
+               self.invalidate()
+
+       def invalidate(self):
+               # Invalidate
+               self.range = None
+               self.value = 0
+               self.factor = 1
+               self.changed((self.CHANGED_CLEAR, ))
+
+       def writeValues(self, pos, max):
+               # Increase Factor as long as range is too big
+               if self.range > 5000000:
+                       self.factor *= 500
+                       self.range /= 500
+
+               # Only save range if not None
+               if max is not None:
+                       self.range = max / self.factor
+
+               # Save pos
+               self.value = pos / self.factor          
+
+               # Trigger change
+               self.changed((self.CHANGED_ALL, ))
\ No newline at end of file
diff --git a/mediadownloader/src/__init__.py b/mediadownloader/src/__init__.py
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/mediadownloader/src/maintainer.info b/mediadownloader/src/maintainer.info
new file mode 100644 (file)
index 0000000..eab0c95
--- /dev/null
@@ -0,0 +1,2 @@
+moritz.venn@freaque.net
+MediaDownloader
diff --git a/mediadownloader/src/plugin.py b/mediadownloader/src/plugin.py
new file mode 100644 (file)
index 0000000..8efcc02
--- /dev/null
@@ -0,0 +1,94 @@
+#
+# To be used as simple Downloading Application by other Plugins
+#
+
+from Components.config import config, ConfigSubsection, ConfigLocations
+from Tools.Directories import resolveFilename, SCOPE_HDD
+
+# SCOPE_HDD is not really what we want but the best we can get :-)
+config.plugins.mediadownloader = ConfigSubsection()
+config.plugins.mediadownloader.bookmarks = ConfigLocations(default = [resolveFilename(SCOPE_HDD)])
+
+# TODO: support custom bookmark element?
+
+# Download a single File
+def download_file(session, url, to = None, askOpen = False, callback = None, \
+       **kwargs):
+       """Provides a simple downloader Application"""
+
+       from Components.Scanner import ScanFile
+       file = ScanFile(url, autodetect = False)
+
+       from MediaDownloader import MediaDownloader
+       session.open(MediaDownloader, file, askOpen, to, callback)
+
+# Item chosen
+def filescan_chosen(session, item):
+       if item:
+               from MediaDownloader import MediaDownloader
+
+               session.open(MediaDownloader, item[1], askOpen = True)
+
+# Open as FileScanner
+def filescan_open(items, session, **kwargs):
+       """Download a file from a given List"""
+
+       Len = len(items)
+       if Len > 1:
+               from Screens.ChoiceBox import ChoiceBox
+               from Tools.BoundFunction import boundFunction
+
+               # Create human-readable filenames
+               choices = [
+                       (
+                               item.path[item.path.rfind("/")+1:].replace('%20', ' ').\
+                                       replace('%5F', '_').replace('%2D', '-'),
+                               item
+                       )
+                               for item in items
+               ]
+
+               # And let the user choose one
+               session.openWithCallback(
+                       boundFunction(filescan_chosen, session),
+                       ChoiceBox,
+                       _("Which file do you want to download?"),
+                       choices
+               )
+       elif Len:
+               from MediaDownloader import MediaDownloader
+
+               session.open(MediaDownloader, items[0], askOpen = True)
+
+# Return Scanner provided by this Plugin
+def filescan(**kwargs):
+       from Components.Scanner import Scanner, ScanPath
+
+       # Overwrite checkFile to detect remote files
+       class RemoteScanner(Scanner):
+               def checkFile(self, file):
+                       return file.path.startswith(("http://", "https://", "ftp://"))
+
+       return [
+               RemoteScanner(
+                       mimetypes = None,
+                       paths_to_scan = 
+                               [
+                                       ScanPath(path = "", with_subdirs = False),
+                               ],
+                       name = "Download",
+                       description = "Download...",
+                       openfnc = filescan_open,
+               )
+       ]
+
+def Plugins(**kwargs):
+       from Plugins.Plugin import PluginDescriptor
+
+       return [
+               PluginDescriptor(
+                       name="MediaDownloader",
+                       where = PluginDescriptor.WHERE_FILESCAN,
+                       fnc = filescan
+               )
+       ]