intial checkin of mediadownload.
[vuplus_dvbapp-plugin] / mediadownloader / src / MediaDownloader.py
1 # GUI (Screens)
2 from Screens.Screen import Screen
3 from Screens.MessageBox import MessageBox
4
5 # GUI (Components)
6 from Components.ActionMap import ActionMap
7 from Components.Label import Label
8
9 # Download
10 from VariableProgressSource import VariableProgressSource
11
12 from Components.config import config
13 from urlparse import urlparse, urlunparse
14
15 import time
16
17 def _parse(url, defaultPort = None):
18         url = url.strip()
19         parsed = urlparse(url)
20         scheme = parsed[0]
21         path = urlunparse(('','')+parsed[2:])
22
23         if defaultPort is None:
24                 if scheme == 'https':
25                         defaultPort = 443
26                 elif scheme == 'ftp':
27                         defaultPort = 21
28                 else:
29                         defaultPort = 80
30
31         host, port = parsed[1], defaultPort
32
33         if '@' in host:
34                 username, host = host.split('@')
35                 if ':' in username:
36                         username, password = username.split(':')
37                 else:
38                         password = ""
39         else:
40                 username = ""
41                 password = ""
42
43         if ':' in host:
44                 host, port = host.split(':')
45                 port = int(port)
46
47         if path == "":
48                 path = "/"
49
50         return scheme, host, port, path, username, password
51
52 def download(url, file, writeProgress = None, contextFactory = None, \
53         *args, **kwargs):
54
55         """Download a remote file and provide current-/total-length.
56
57         @param file: path to file on filesystem, or file-like object.
58         @param writeProgress: function or list of functions taking two parameters (pos, length)
59
60         See HTTPDownloader to see what extra args can be passed if remote file
61         is accessible via http or https. Both Backends should offer supportPartial.
62         """
63
64         scheme, host, port, path, username, password = _parse(url)      
65
66         if scheme == 'ftp':
67                 from FTPProgressDownloader import FTPProgressDownloader
68
69                 if not (username and password):
70                         username = 'anonymous'
71                         password = 'my@email.com'
72
73                 client = FTPProgressDownloader(
74                         host,
75                         port,
76                         path,
77                         file,
78                         username,
79                         password,
80                         writeProgress,
81                         *args,
82                         **kwargs
83                 )
84                 return client.deferred
85
86         # We force username and password here as we lack a satisfying input method
87         if username and password:
88                 from base64 import encodestring
89
90                 # twisted will crash if we don't rewrite this ;-)
91                 url = scheme + '://' + host + ':' + str(port) + path
92
93                 basicAuth = encodestring("%s:%s" % (username, password))
94                 authHeader = "Basic " + basicAuth.strip()
95                 AuthHeaders = {"Authorization": authHeader}
96
97                 if kwargs.has_key("headers"):
98                         kwargs["headers"].update(AuthHeaders)
99                 else:
100                         kwargs["headers"] = AuthHeaders
101
102         from HTTPProgressDownloader import HTTPProgressDownloader
103         from twisted.internet import reactor
104
105         factory = HTTPProgressDownloader(url, file, writeProgress, *args, **kwargs)
106         if scheme == 'https':
107                 from twisted.internet import ssl
108                 if contextFactory is None:
109                         contextFactory = ssl.ClientContextFactory()
110                 reactor.connectSSL(host, port, factory, contextFactory)
111         else:
112                 reactor.connectTCP(host, port, factory)
113
114         return factory.deferred
115
116 class MediaDownloader(Screen):
117         """Simple Plugin which downloads a given file. If not targetfile is specified the user will be asked
118         for a location (see LocationBox). If doOpen is True the Plugin will try to open it after downloading."""
119
120         skin = """<screen name="MediaDownloader" position="100,150" size="540,95" >
121                         <widget name="wait" position="2,10" size="500,30" valign="center" font="Regular;23" />
122                         <widget source="progress" render="Progress" position="2,40" size="536,20" />
123                         <widget name="eta" position="2,65" size="200,30" font="Regular;23" />
124                         <widget name="speed" position="338,65" size="200,30" halign="right" font="Regular;23" />
125                 </screen>"""
126
127         def __init__(self, session, file, askOpen = False, downloadTo = None, callback = None):
128                 Screen.__init__(self, session)
129
130                 # Save arguments local
131                 self.file = file
132                 self.askOpen = askOpen
133                 self.filename = downloadTo
134                 self.callback = callback
135
136                 # Init what we need for progress callback
137                 self.lastLength = 0
138                 self.lastTime = 0
139                 self.lastApprox = 0
140
141                 # Inform user about whats currently done
142                 self["wait"] = Label(_("Downloading..."))
143                 self["progress"] = VariableProgressSource()
144                 self["eta"] = Label(_("ETA ??:?? h")) # XXX: we could just leave eta and speed empty
145                 self["speed"] = Label(_("?? kb/s"))
146
147                 # Set Limit if we know it already (Server might not tell it)
148                 if self.file.size:
149                         self["progress"].writeValues(0, self.file.size*1048576)
150
151                 # Call getFilename as soon as we are able to open a new screen
152                 self.onExecBegin.append(self.getFilename)
153
154         def getFilename(self):
155                 self.onExecBegin.remove(self.getFilename)
156
157                 # If we have a filename (downloadTo provided) start fetching
158                 if self.filename is not None:
159                         self.fetchFile()
160                 # Else open LocationBox to determine where to save
161                 else:
162                         # TODO: determine basename without os.path?
163                         from os import path
164                         from Screens.LocationBox import LocationBox
165
166                         self.session.openWithCallback(
167                                 self.gotFilename,
168                                 LocationBox,
169                                 _("Where to save?"),
170                                 path.basename(self.file.path),
171                                 minFree = self.file.size,
172                                 bookmarks = config.plugins.mediadownloader.bookmarks
173                         )
174
175         def gotFilename(self, res):
176                 # If we got a filename try to fetch file
177                 if res is not None:
178                         self.filename = res
179                         self.fetchFile()
180                 # Else close
181                 else:
182                         self.close()
183
184         def fetchFile(self):
185                 # Fetch file
186                 d = download(
187                         self.file.path,
188                         self.filename,
189                         [
190                                 self["progress"].writeValues,
191                                 self.gotProgress
192                         ]
193                 )
194
195                 d.addCallback(self.gotFile).addErrback(self.error)
196
197         def gotProgress(self, pos, max):
198                 newTime = time.time()
199                 # Check if we're called the first time (got total)
200                 lastTime = self.lastTime
201                 if lastTime == 0:
202                         self.lastTime = newTime
203
204                 # 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)
205                 elif int(newTime - lastTime) >= 2:
206                         newLength = pos
207
208                         lastApprox = round(((newLength - self.lastLength) / (newTime - lastTime) / 1024), 2)
209
210                         secLen = int(round(((max-pos) / 1024) / lastApprox))
211                         self["eta"].setText(_("ETA %d:%02d min") % (secLen / 60, secLen % 60))
212                         self["speed"].setText(_("%d kb/s") % (lastApprox))
213
214                         self.lastApprox = lastApprox
215                         self.lastLength = newLength
216                         self.lastTime = newTime
217
218         def openCallback(self, res):
219                 from Components.Scanner import openFile
220
221                 # Try to open file if res was True
222                 if res and not openFile(self.session, None, self.filename):
223                         self.session.open(
224                                 MessageBox,
225                                 _("No suitable Viewer found!"),
226                                 type = MessageBox.TYPE_ERROR,
227                                 timeout = 5
228                         )
229
230                 # Calback with Filename on success
231                 if self.callback is not None:
232                         self.callback(self.filename)
233
234                 self.close()
235
236         def gotFile(self, data = ""):
237                 # Ask if file should be opened unless told not to
238                 if self.askOpen:
239                         self.session.openWithCallback(
240                                 self.openCallback,
241                                 MessageBox,
242                                 _("Do you want to try to open the downloaded file?"),
243                                 type = MessageBox.TYPE_YESNO
244                         )
245                 # Otherwise callback and close
246                 else:
247                         # Calback with Filename on success
248                         if self.callback is not None:
249                                 self.callback(self.filename)
250
251                         self.close()
252
253         def error(self, msg = ""):
254                 if msg != "":
255                         print "[MediaDownloader] Error downloading:", msg
256
257                 self.session.open(
258                         MessageBox,
259                         _("Error while downloading file %s") % (self.file.path),
260                         type = MessageBox.TYPE_ERROR,
261                         timeout = 3
262                 )
263
264                 # Calback with None on failure
265                 if self.callback is not None:
266                         self.callback(None)
267
268                 self.close()