Properly update existing episodes (bug 211)
[gpodder.git] / src / gpodder / download.py
blobd539cc6c67a3f03fc443194a65173166c15580c5
1 # -*- coding: utf-8 -*-
3 # gPodder - A media aggregator and podcast client
4 # Copyright (c) 2005-2008 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 import gpodder
34 import threading
35 import urllib
36 import shutil
37 import os.path
38 import os
39 import time
41 from xml.sax import saxutils
43 class DownloadCancelledException(Exception): pass
45 class gPodderDownloadHTTPError(Exception):
46 def __init__(self, url, error_code, error_message):
47 self.url = url
48 self.error_code = error_code
49 self.error_message = error_message
51 class DownloadURLOpener(urllib.FancyURLopener):
52 version = gpodder.user_agent
54 def __init__( self, channel):
55 if gl.config.proxy_use_environment:
56 proxies = None
57 else:
58 proxies = {}
59 if gl.config.http_proxy:
60 proxies['http'] = gl.config.http_proxy
61 if gl.config.ftp_proxy:
62 proxies['ftp'] = gl.config.ftp_proxy
64 self.channel = channel
65 urllib.FancyURLopener.__init__( self, proxies)
67 def http_error_default(self, url, fp, errcode, errmsg, headers):
68 """
69 FancyURLopener by default does not raise an exception when
70 there is some unknown HTTP error code. We want to override
71 this and provide a function to log the error and raise an
72 exception, so we don't download the HTTP error page here.
73 """
74 # The following two lines are copied from urllib.URLopener's
75 # implementation of http_error_default
76 void = fp.read()
77 fp.close()
78 raise gPodderDownloadHTTPError(url, errcode, errmsg)
80 def prompt_user_passwd( self, host, realm):
81 if self.channel.username or self.channel.password:
82 log( 'Authenticating as "%s" to "%s" for realm "%s".', self.channel.username, host, realm, sender = self)
83 return ( self.channel.username, self.channel.password )
85 return ( None, None )
88 class DownloadThread(threading.Thread):
89 MAX_UPDATES_PER_SEC = 1
91 def __init__( self, channel, episode, notification = None):
92 threading.Thread.__init__( self)
93 self.setDaemon( True)
95 self.channel = channel
96 self.episode = episode
98 self.notification = notification
100 self.url = self.episode.url
101 self.filename = self.episode.local_filename()
102 self.tempname = os.path.join( os.path.dirname( self.filename), '.tmp-' + os.path.basename( self.filename))
104 # Make an educated guess about the total file size
105 self.total_size = self.episode.length
107 self.cancelled = False
108 self.start_time = 0.0
109 self.speed = _('Queued')
110 self.speed_value = 0
111 self.progress = 0.0
112 self.downloader = DownloadURLOpener( self.channel)
113 self.last_update = 0.0
115 # Keep a copy of these global variables for comparison later
116 self.limit_rate_value = gl.config.limit_rate_value
117 self.limit_rate = gl.config.limit_rate
118 self.start_blocks = 0
120 def cancel( self):
121 self.cancelled = True
123 def status_updated( self, count, blockSize, totalSize):
124 if totalSize:
125 # We see a different "total size" while downloading,
126 # so correct the total size variable in the thread
127 if totalSize != self.total_size and totalSize > 0:
128 log('Correcting file size for %s from %d to %d while downloading.', self.url, self.total_size, totalSize, sender=self)
129 self.total_size = totalSize
130 elif totalSize < 0:
131 # The current download has a negative value, so assume
132 # the total size given from the feed is correct
133 totalSize = self.total_size
134 self.progress = 100.0*float(count*blockSize)/float(totalSize)
135 else:
136 self.progress = 100.0
138 # Sanity checks for "progress" in valid range (0..100)
139 if self.progress < 0.0:
140 log('Warning: Progress is lower than 0 (count=%d, blockSize=%d, totalSize=%d)', count, blockSize, totalSize, sender=self)
141 self.progress = 0.0
142 elif self.progress > 100.0:
143 log('Warning: Progress is more than 100 (count=%d, blockSize=%d, totalSize=%d)', count, blockSize, totalSize, sender=self)
144 self.progress = 100.0
146 self.calculate_speed( count, blockSize)
147 if self.last_update < time.time() - (1.0 / self.MAX_UPDATES_PER_SEC):
148 services.download_status_manager.update_status( self.download_id, speed = self.speed, progress = self.progress)
149 self.last_update = time.time()
151 if self.cancelled:
152 util.delete_file( self.tempname)
153 raise DownloadCancelledException()
155 def calculate_speed( self, count, blockSize):
156 if count % 5 == 0:
157 now = time.time()
158 if self.start_time > 0:
160 # Has rate limiting been enabled or disabled?
161 if self.limit_rate != gl.config.limit_rate:
162 # If it has been enabled then reset base time and block count
163 if gl.config.limit_rate:
164 self.start_time = now
165 self.start_blocks = count
166 self.limit_rate = gl.config.limit_rate
168 # Has the rate been changed and are we currently limiting?
169 if self.limit_rate_value != gl.config.limit_rate_value and self.limit_rate:
170 self.start_time = now
171 self.start_blocks = count
172 self.limit_rate_value = gl.config.limit_rate_value
174 passed = now - self.start_time
175 if passed > 0:
176 speed = ((count-self.start_blocks)*blockSize)/passed
177 else:
178 speed = 0
179 else:
180 self.start_time = now
181 self.start_blocks = count
182 passed = now - self.start_time
183 speed = count*blockSize
185 self.speed = '%s/s' % gl.format_filesize(speed)
186 self.speed_value = speed
188 if gl.config.limit_rate and speed > gl.config.limit_rate_value:
189 # calculate the time that should have passed to reach
190 # the desired download rate and wait if necessary
191 should_have_passed = float((count-self.start_blocks)*blockSize)/(gl.config.limit_rate_value*1024.0)
192 if should_have_passed > passed:
193 # sleep a maximum of 10 seconds to not cause time-outs
194 delay = min( 10.0, float(should_have_passed-passed))
195 time.sleep( delay)
197 def run( self):
198 self.download_id = services.download_status_manager.reserve_download_id()
199 services.download_status_manager.register_download_id( self.download_id, self)
201 # Initial status update
202 services.download_status_manager.update_status( self.download_id, episode = self.episode.title, url = self.episode.url, speed = self.speed, progress = self.progress)
204 acquired = services.download_status_manager.s_acquire()
205 try:
206 try:
207 if self.cancelled:
208 return
210 util.delete_file( self.tempname)
211 self.downloader.retrieve( self.episode.url, self.tempname, reporthook = self.status_updated)
212 shutil.move( self.tempname, self.filename)
213 self.channel.addDownloadedItem( self.episode)
214 services.download_status_manager.download_completed(self.download_id)
215 # Get the _real_ filesize once we actually have the file
216 self.episode.length = os.path.getsize(self.filename)
217 self.episode.save()
219 # If a user command has been defined, execute the command setting some environment variables
220 if len(gl.config.cmd_download_complete) > 0:
221 os.environ["GPODDER_EPISODE_URL"]=self.episode.url or ''
222 os.environ["GPODDER_EPISODE_TITLE"]=self.episode.title or ''
223 os.environ["GPODDER_EPISODE_FILENAME"]=self.filename or ''
224 os.environ["GPODDER_EPISODE_PUBDATE"]=str(int(self.episode.pubDate))
225 os.environ["GPODDER_EPISODE_LINK"]=self.episode.link or ''
226 os.environ["GPODDER_EPISODE_DESC"]=self.episode.description or ''
227 threading.Thread(target=gl.ext_command_thread, args=(self.notification,gl.config.cmd_download_complete)).start()
229 finally:
230 services.download_status_manager.remove_download_id( self.download_id)
231 services.download_status_manager.s_release( acquired)
232 except DownloadCancelledException:
233 log('Download has been cancelled: %s', self.episode.title, traceback=None, sender=self)
234 except IOError, ioe:
235 if self.notification is not None:
236 title = ioe.strerror
237 message = _('An error happened while trying to download <b>%s</b>.') % ( saxutils.escape( self.episode.title), )
238 self.notification( message, title)
239 log( 'Error "%s" while downloading "%s": %s', ioe.strerror, self.episode.title, ioe.filename, sender = self)
240 except gPodderDownloadHTTPError, gdhe:
241 if self.notification is not None:
242 title = gdhe.error_message
243 message = _('An error (HTTP %d) happened while trying to download <b>%s</b>.') % ( gdhe.error_code, saxutils.escape( self.episode.title), )
244 self.notification( message, title)
245 log( 'HTTP error %s while downloading "%s": %s', gdhe.error_code, self.episode.title, gdhe.error_message, sender=self)
246 except:
247 log( 'Error while downloading "%s".', self.episode.title, sender = self, traceback = True)