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