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