Update copyright info from 2005-2008 to 2005-2009
[gpodder.git] / src / gpodder / download.py
blob015598c31331cf581749299d6a158719b1e73a9b
1 # -*- coding: utf-8 -*-
3 # gPodder - A media aggregator and podcast client
4 # Copyright (c) 2005-2009 Thomas Perl and the gPodder Team
6 # gPodder is free software; you can redistribute it and/or modify
7 # it under the terms of the GNU General Public License as published by
8 # the Free Software Foundation; either version 3 of the License, or
9 # (at your option) any later version.
11 # gPodder is distributed in the hope that it will be useful,
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 # GNU General Public License for more details.
16 # You should have received a copy of the GNU General Public License
17 # along with this program. If not, see <http://www.gnu.org/licenses/>.
22 # download.py -- Download client using DownloadStatusManager
23 # Thomas Perl <thp@perli.net> 2007-09-15
25 # Based on libwget.py (2005-10-29)
28 from gpodder.liblogger import log
29 from gpodder.libgpodder import gl
30 from gpodder import util
31 from gpodder import services
32 from gpodder import resolver
33 import gpodder
35 import threading
36 import urllib
37 import shutil
38 import os.path
39 import os
40 import time
42 from xml.sax import saxutils
44 class DownloadCancelledException(Exception): pass
46 class gPodderDownloadHTTPError(Exception):
47 def __init__(self, url, error_code, error_message):
48 self.url = url
49 self.error_code = error_code
50 self.error_message = error_message
52 class DownloadURLOpener(urllib.FancyURLopener):
53 version = gpodder.user_agent
55 def __init__( self, channel):
56 if gl.config.proxy_use_environment:
57 proxies = None
58 else:
59 proxies = {}
60 if gl.config.http_proxy:
61 proxies['http'] = gl.config.http_proxy
62 if gl.config.ftp_proxy:
63 proxies['ftp'] = gl.config.ftp_proxy
65 self.channel = channel
66 urllib.FancyURLopener.__init__( self, proxies)
68 def http_error_default(self, url, fp, errcode, errmsg, headers):
69 """
70 FancyURLopener by default does not raise an exception when
71 there is some unknown HTTP error code. We want to override
72 this and provide a function to log the error and raise an
73 exception, so we don't download the HTTP error page here.
74 """
75 # The following two lines are copied from urllib.URLopener's
76 # implementation of http_error_default
77 void = fp.read()
78 fp.close()
79 raise gPodderDownloadHTTPError(url, errcode, errmsg)
81 def prompt_user_passwd( self, host, realm):
82 if self.channel.username or self.channel.password:
83 log( 'Authenticating as "%s" to "%s" for realm "%s".', self.channel.username, host, realm, sender = self)
84 return ( self.channel.username, self.channel.password )
86 return ( None, None )
89 class DownloadThread(threading.Thread):
90 MAX_UPDATES_PER_SEC = 1
92 def __init__( self, channel, episode, notification = None):
93 threading.Thread.__init__( self)
94 self.setDaemon( True)
96 if gpodder.interface == gpodder.MAEMO:
97 # Only update status every 3 seconds on Maemo
98 self.MAX_UPDATES_PER_SEC = 1./3.
100 self.channel = channel
101 self.episode = episode
103 self.notification = notification
105 self.url = self.episode.url
106 self.filename = self.episode.local_filename()
107 self.tempname = self.filename + '.partial'
109 # Make an educated guess about the total file size
110 self.total_size = self.episode.length
112 self.cancelled = False
113 self.start_time = 0.0
114 self.speed = _('Queued')
115 self.speed_value = 0
116 self.progress = 0.0
117 self.downloader = DownloadURLOpener( self.channel)
118 self.last_update = 0.0
120 # Keep a copy of these global variables for comparison later
121 self.limit_rate_value = gl.config.limit_rate_value
122 self.limit_rate = gl.config.limit_rate
123 self.start_blocks = 0
125 def cancel( self):
126 self.cancelled = True
128 def status_updated( self, count, blockSize, totalSize):
129 if totalSize:
130 # We see a different "total size" while downloading,
131 # so correct the total size variable in the thread
132 if totalSize != self.total_size and totalSize > 0:
133 log('Correcting file size for %s from %d to %d while downloading.', self.url, self.total_size, totalSize, sender=self)
134 self.total_size = totalSize
135 elif totalSize < 0:
136 # The current download has a negative value, so assume
137 # the total size given from the feed is correct
138 totalSize = self.total_size
139 self.progress = 100.0*float(count*blockSize)/float(totalSize)
140 else:
141 self.progress = 100.0
143 # Sanity checks for "progress" in valid range (0..100)
144 if self.progress < 0.0:
145 log('Warning: Progress is lower than 0 (count=%d, blockSize=%d, totalSize=%d)', count, blockSize, totalSize, sender=self)
146 self.progress = 0.0
147 elif self.progress > 100.0:
148 log('Warning: Progress is more than 100 (count=%d, blockSize=%d, totalSize=%d)', count, blockSize, totalSize, sender=self)
149 self.progress = 100.0
151 self.calculate_speed( count, blockSize)
152 if self.last_update < time.time() - (1.0 / self.MAX_UPDATES_PER_SEC):
153 services.download_status_manager.update_status( self.download_id, speed = self.speed, progress = self.progress)
154 self.last_update = time.time()
156 if self.cancelled:
157 util.delete_file( self.tempname)
158 raise DownloadCancelledException()
160 def calculate_speed( self, count, blockSize):
161 if count % 5 == 0:
162 now = time.time()
163 if self.start_time > 0:
165 # Has rate limiting been enabled or disabled?
166 if self.limit_rate != gl.config.limit_rate:
167 # If it has been enabled then reset base time and block count
168 if gl.config.limit_rate:
169 self.start_time = now
170 self.start_blocks = count
171 self.limit_rate = gl.config.limit_rate
173 # Has the rate been changed and are we currently limiting?
174 if self.limit_rate_value != gl.config.limit_rate_value and self.limit_rate:
175 self.start_time = now
176 self.start_blocks = count
177 self.limit_rate_value = gl.config.limit_rate_value
179 passed = now - self.start_time
180 if passed > 0:
181 speed = ((count-self.start_blocks)*blockSize)/passed
182 else:
183 speed = 0
184 else:
185 self.start_time = now
186 self.start_blocks = count
187 passed = now - self.start_time
188 speed = count*blockSize
190 self.speed = '%s/s' % gl.format_filesize(speed)
191 self.speed_value = speed
193 if gl.config.limit_rate and speed > gl.config.limit_rate_value:
194 # calculate the time that should have passed to reach
195 # the desired download rate and wait if necessary
196 should_have_passed = float((count-self.start_blocks)*blockSize)/(gl.config.limit_rate_value*1024.0)
197 if should_have_passed > passed:
198 # sleep a maximum of 10 seconds to not cause time-outs
199 delay = min( 10.0, float(should_have_passed-passed))
200 time.sleep( delay)
202 def run( self):
203 self.download_id = services.download_status_manager.reserve_download_id()
204 services.download_status_manager.register_download_id( self.download_id, self)
206 # Initial status update
207 services.download_status_manager.update_status( self.download_id, episode = self.episode.title, url = self.episode.url, speed = self.speed, progress = self.progress)
209 acquired = services.download_status_manager.s_acquire()
210 try:
211 try:
212 if self.cancelled:
213 return
215 util.delete_file( self.tempname)
216 (unused, headers) = self.downloader.retrieve( resolver.get_real_download_url(self.url), self.tempname, reporthook = self.status_updated)
218 if 'content-type' in headers and headers['content-type'] != self.episode.mimetype:
219 log('Correcting mime type: %s => %s', self.episode.mimetype, headers['content-type'])
220 self.episode.mimetype = headers['content-type']
221 # File names are constructed with regard to the mime type.
222 self.filename = self.episode.local_filename()
224 shutil.move( self.tempname, self.filename)
225 # Get the _real_ filesize once we actually have the file
226 self.episode.length = os.path.getsize(self.filename)
227 self.channel.addDownloadedItem( self.episode)
228 services.download_status_manager.download_completed(self.download_id)
230 # If a user command has been defined, execute the command setting some environment variables
231 if len(gl.config.cmd_download_complete) > 0:
232 os.environ["GPODDER_EPISODE_URL"]=self.episode.url or ''
233 os.environ["GPODDER_EPISODE_TITLE"]=self.episode.title or ''
234 os.environ["GPODDER_EPISODE_FILENAME"]=self.filename or ''
235 os.environ["GPODDER_EPISODE_PUBDATE"]=str(int(self.episode.pubDate))
236 os.environ["GPODDER_EPISODE_LINK"]=self.episode.link or ''
237 os.environ["GPODDER_EPISODE_DESC"]=self.episode.description or ''
238 threading.Thread(target=gl.ext_command_thread, args=(self.notification,gl.config.cmd_download_complete)).start()
240 finally:
241 services.download_status_manager.remove_download_id( self.download_id)
242 services.download_status_manager.s_release( acquired)
243 except DownloadCancelledException:
244 log('Download has been cancelled: %s', self.episode.title, traceback=None, sender=self)
245 except IOError, ioe:
246 if self.notification is not None:
247 title = ioe.strerror
248 message = _('An error happened while trying to download <b>%s</b>.') % ( saxutils.escape( self.episode.title), )
249 self.notification( message, title)
250 log( 'Error "%s" while downloading "%s": %s', ioe.strerror, self.episode.title, ioe.filename, sender = self)
251 except gPodderDownloadHTTPError, gdhe:
252 if self.notification is not None:
253 title = gdhe.error_message
254 message = _('An error (HTTP %d) happened while trying to download <b>%s</b>.') % ( gdhe.error_code, saxutils.escape( self.episode.title), )
255 self.notification( message, title)
256 log( 'HTTP error %s while downloading "%s": %s', gdhe.error_code, self.episode.title, gdhe.error_message, sender=self)
257 except:
258 log( 'Error while downloading "%s".', self.episode.title, sender = self, traceback = True)