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
41 from xml
.sax
import saxutils
43 class DownloadCancelledException(Exception): pass
45 class gPodderDownloadHTTPError(Exception):
46 def __init__(self
, url
, error_code
, error_message
):
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
:
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
):
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.
74 # The following two lines are copied from urllib.URLopener's
75 # implementation of http_error_default
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
)
88 class DownloadThread(threading
.Thread
):
89 MAX_UPDATES_PER_SEC
= 1
91 def __init__( self
, channel
, episode
, notification
= None):
92 threading
.Thread
.__init
__( self
)
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')
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
121 self
.cancelled
= True
123 def status_updated( self
, count
, blockSize
, 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
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
)
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
)
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()
152 util
.delete_file( self
.tempname
)
153 raise DownloadCancelledException()
155 def calculate_speed( self
, count
, blockSize
):
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
176 speed
= ((count
-self
.start_blocks
)*blockSize
)/passed
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
))
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()
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
)
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()
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
)
235 if self
.notification
is not None:
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
)
247 log( 'Error while downloading "%s".', self
.episode
.title
, sender
= self
, traceback
= True)