1 # -*- coding: utf-8 -*-
3 # gPodder - A media aggregator and podcast client
4 # Copyright (c) 2005-2011 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 queue management
23 # Thomas Perl <thp@perli.net> 2007-09-15
25 # Based on libwget.py (2005-10-29)
28 from __future__
import with_statement
30 from gpodder
.liblogger
import log
31 from gpodder
import util
32 from gpodder
import youtube
48 from xml
.sax
import saxutils
52 def get_header_param(headers
, param
, header_name
):
53 """Extract a HTTP header parameter from a dict
55 Uses the "email" module to retrieve parameters
56 from HTTP headers. This can be used to get the
57 "filename" parameter of the "content-disposition"
58 header for downloads to pick a good filename.
60 Returns None if the filename cannot be retrieved.
63 headers_string
= ['%s:%s'%(k
,v
) for k
,v
in headers
.items()]
64 msg
= email
.message_from_string('\n'.join(headers_string
))
65 if header_name
in msg
:
66 value
= msg
.get_param(param
, header
=header_name
)
69 decoded_list
= email
.Header
.decode_header(value
)
71 for part
, encoding
in decoded_list
:
73 value
.append(part
.decode(encoding
))
75 value
.append(unicode(part
))
76 return u
''.join(value
)
78 log('Error trying to get %s from %s: %s', \
79 param
, header_name
, str(e
), traceback
=True)
83 class ContentRange(object):
85 # http://svn.pythonpaste.org/Paste/WebOb/trunk/webob/byterange.py
87 # Copyright (c) 2007 Ian Bicking and Contributors
89 # Permission is hereby granted, free of charge, to any person obtaining
90 # a copy of this software and associated documentation files (the
91 # "Software"), to deal in the Software without restriction, including
92 # without limitation the rights to use, copy, modify, merge, publish,
93 # distribute, sublicense, and/or sell copies of the Software, and to
94 # permit persons to whom the Software is furnished to do so, subject to
95 # the following conditions:
97 # The above copyright notice and this permission notice shall be
98 # included in all copies or substantial portions of the Software.
100 # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
101 # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
102 # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
103 # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
104 # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
105 # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
106 # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
108 Represents the Content-Range header
110 This header is ``start-stop/length``, where stop and length can be
111 ``*`` (represented as None in the attributes).
114 def __init__(self
, start
, stop
, length
):
115 assert start
>= 0, "Bad start: %r" % start
116 assert stop
is None or (stop
>= 0 and stop
>= start
), (
117 "Bad stop: %r" % stop
)
124 self
.__class
__.__name
__,
128 if self
.stop
is None:
132 if self
.length
is None:
136 return 'bytes %s-%s/%s' % (self
.start
, stop
, length
)
140 Mostly so you can unpack this, like:
142 start, stop, length = res.content_range
144 return iter([self
.start
, self
.stop
, self
.length
])
147 def parse(cls
, value
):
149 Parse the header. May return None if it cannot parse.
153 value
= value
.strip()
154 if not value
.startswith('bytes '):
157 value
= value
[len('bytes '):].strip()
159 # Invalid, no length given
161 range, length
= value
.split('/', 1)
165 start
, end
= range.split('-', 1)
180 return cls(start
, None, length
)
182 return cls(start
, end
-1, length
)
185 class DownloadCancelledException(Exception): pass
186 class AuthenticationError(Exception): pass
188 class gPodderDownloadHTTPError(Exception):
189 def __init__(self
, url
, error_code
, error_message
):
191 self
.error_code
= error_code
192 self
.error_message
= error_message
194 class DownloadURLOpener(urllib
.FancyURLopener
):
195 version
= gpodder
.user_agent
197 # Sometimes URLs are not escaped correctly - try to fix them
198 # (see RFC2396; Section 2.4.3. Excluded US-ASCII Characters)
199 # FYI: The omission of "%" in the list is to avoid double escaping!
200 ESCAPE_CHARS
= dict((ord(c
), u
'%%%x'%ord(c
)) for c
in u
' <>#"{}|\\^[]`')
202 def __init__( self
, channel
):
203 self
.channel
= channel
204 self
._auth
_retry
_counter
= 0
205 urllib
.FancyURLopener
.__init
__(self
, None)
207 def http_error_default(self
, url
, fp
, errcode
, errmsg
, headers
):
209 FancyURLopener by default does not raise an exception when
210 there is some unknown HTTP error code. We want to override
211 this and provide a function to log the error and raise an
212 exception, so we don't download the HTTP error page here.
214 # The following two lines are copied from urllib.URLopener's
215 # implementation of http_error_default
218 raise gPodderDownloadHTTPError(url
, errcode
, errmsg
)
220 def redirect_internal(self
, url
, fp
, errcode
, errmsg
, headers
, data
):
221 """ This is the exact same function that's included with urllib
222 except with "void = fp.read()" commented out. """
224 if 'location' in headers
:
225 newurl
= headers
['location']
226 elif 'uri' in headers
:
227 newurl
= headers
['uri']
231 # This blocks forever(?) with certain servers (see bug #465)
235 # In case the server sent a relative URL, join with original:
236 newurl
= urlparse
.urljoin(self
.type + ":" + url
, newurl
)
237 return self
.open(newurl
)
239 # The following is based on Python's urllib.py "URLopener.retrieve"
240 # Also based on http://mail.python.org/pipermail/python-list/2001-October/110069.html
242 def http_error_206(self
, url
, fp
, errcode
, errmsg
, headers
, data
=None):
243 # The next line is taken from urllib's URLopener.open_http
244 # method, at the end after the line "if errcode == 200:"
245 return urllib
.addinfourl(fp
, headers
, 'http:' + url
)
247 def retrieve_resume(self
, url
, filename
, reporthook
=None, data
=None):
248 """Download files from an URL; return (headers, real_url)
250 Resumes a download if the local filename exists and
251 the server supports download resuming.
256 if os
.path
.exists(filename
):
258 current_size
= os
.path
.getsize(filename
)
259 tfp
= open(filename
, 'ab')
260 #If the file exists, then only download the remainder
262 self
.addheader('Range', 'bytes=%s-' % (current_size
))
264 log('Cannot open file for resuming: %s', filename
, sender
=self
, traceback
=True)
269 tfp
= open(filename
, 'wb')
271 # Fix a problem with bad URLs that are not encoded correctly (bug 549)
272 url
= url
.decode('ascii', 'ignore')
273 url
= url
.translate(self
.ESCAPE_CHARS
)
274 url
= url
.encode('ascii')
276 url
= urllib
.unwrap(urllib
.toBytes(url
))
277 fp
= self
.open(url
, data
)
281 # We told the server to resume - see if she agrees
282 # See RFC2616 (206 Partial Content + Section 14.16)
283 # XXX check status code here, too...
284 range = ContentRange
.parse(headers
.get('content-range', ''))
285 if range is None or range.start
!= current_size
:
286 # Ok, that did not work. Reset the download
287 # TODO: seek and truncate if content-range differs from request
289 tfp
= open(filename
, 'wb')
291 log('Cannot resume. Missing or wrong Content-Range header (RFC2616)', sender
=self
)
293 result
= headers
, fp
.geturl()
297 blocknum
= int(current_size
/bs
)
299 if "content-length" in headers
:
300 size
= int(headers
.getrawheader("Content-Length")) + current_size
301 reporthook(blocknum
, bs
, size
)
302 while read
< size
or size
== -1:
306 block
= fp
.read(min(size
-read
, bs
))
313 reporthook(blocknum
, bs
, size
)
319 # raise exception if actual size does not match content-length header
320 if size
>= 0 and read
< size
:
321 raise urllib
.ContentTooShortError("retrieval incomplete: got only %i out "
322 "of %i bytes" % (read
, size
), result
)
326 # end code based on urllib.py
328 def prompt_user_passwd( self
, host
, realm
):
329 # Keep track of authentication attempts, fail after the third one
330 self
._auth
_retry
_counter
+= 1
331 if self
._auth
_retry
_counter
> 3:
332 raise AuthenticationError(_('Wrong username/password'))
334 if self
.channel
.username
or self
.channel
.password
:
335 log( 'Authenticating as "%s" to "%s" for realm "%s".', self
.channel
.username
, host
, realm
, sender
= self
)
336 return ( self
.channel
.username
, self
.channel
.password
)
341 class DownloadQueueWorker(threading
.Thread
):
342 def __init__(self
, queue
, exit_callback
, continue_check_callback
, minimum_tasks
):
343 threading
.Thread
.__init
__(self
)
345 self
.exit_callback
= exit_callback
346 self
.continue_check_callback
= continue_check_callback
348 # The minimum amount of tasks that should be downloaded by this worker
349 # before using the continue_check_callback to determine if it might
350 # continue accepting tasks. This can be used to forcefully start a
351 # download, even if a download limit is in effect.
352 self
.minimum_tasks
= minimum_tasks
355 log('Running new thread: %s', self
.getName(), sender
=self
)
357 # Check if this thread is allowed to continue accepting tasks
358 # (But only after reducing minimum_tasks to zero - see above)
359 if self
.minimum_tasks
> 0:
360 self
.minimum_tasks
-= 1
361 elif not self
.continue_check_callback(self
):
362 log('%s must not accept new tasks.', self
.getName(), sender
=self
)
366 task
= self
.queue
.pop()
367 log('%s is processing: %s', self
.getName(), task
, sender
=self
)
369 except IndexError, e
:
370 log('No more tasks for %s to carry out.', self
.getName(), sender
=self
)
372 self
.exit_callback(self
)
375 class DownloadQueueManager(object):
376 def __init__(self
, config
):
377 self
._config
= config
378 self
.tasks
= collections
.deque()
380 self
.worker_threads_access
= threading
.RLock()
381 self
.worker_threads
= []
383 def __exit_callback(self
, worker_thread
):
384 with self
.worker_threads_access
:
385 self
.worker_threads
.remove(worker_thread
)
387 def __continue_check_callback(self
, worker_thread
):
388 with self
.worker_threads_access
:
389 if len(self
.worker_threads
) > self
._config
.max_downloads
and \
390 self
._config
.max_downloads_enabled
:
391 self
.worker_threads
.remove(worker_thread
)
396 def spawn_threads(self
, force_start
=False):
397 """Spawn new worker threads if necessary
399 If force_start is True, forcefully spawn a thread and
400 let it process at least one episodes, even if a download
401 limit is in effect at the moment.
403 with self
.worker_threads_access
:
404 if not len(self
.tasks
):
407 if force_start
or len(self
.worker_threads
) == 0 or \
408 len(self
.worker_threads
) < self
._config
.max_downloads
or \
409 not self
._config
.max_downloads_enabled
:
410 # We have to create a new thread here, there's work to do
411 log('I am going to spawn a new worker thread.', sender
=self
)
413 # The new worker should process at least one task (the one
414 # that we want to forcefully start) if force_start is True.
420 worker
= DownloadQueueWorker(self
.tasks
, self
.__exit
_callback
, \
421 self
.__continue
_check
_callback
, minimum_tasks
)
422 self
.worker_threads
.append(worker
)
425 def are_queued_or_active_tasks(self
):
426 with self
.worker_threads_access
:
427 return len(self
.worker_threads
) > 0
429 def add_task(self
, task
, force_start
=False):
430 """Add a new task to the download queue
432 If force_start is True, ignore the download limit
433 and forcefully start the download right away.
435 if task
.status
!= DownloadTask
.INIT
:
436 # This task is old so update episode from db
437 task
.episode
.reload_from_db()
439 # Remove the task from its current position in the
440 # download queue (if any) to avoid race conditions
441 # where two worker threads download the same file
443 self
.tasks
.remove(task
)
444 except ValueError, e
:
446 task
.status
= DownloadTask
.QUEUED
448 # Add the task to be taken on next pop
449 self
.tasks
.append(task
)
451 # Add the task to the end of the queue
452 self
.tasks
.appendleft(task
)
453 self
.spawn_threads(force_start
)
456 class DownloadTask(object):
457 """An object representing the download task of an episode
459 You can create a new download task like this:
461 task = DownloadTask(episode, gpodder.config.Config(CONFIGFILE))
462 task.status = DownloadTask.QUEUED
465 While the download is in progress, you can access its properties:
467 task.total_size # in bytes
468 task.progress # from 0.0 to 1.0
469 task.speed # in bytes per second
470 str(task) # name of the episode
471 task.status # current status
472 task.status_changed # True if the status has been changed (see below)
473 task.url # URL of the episode being downloaded
474 task.podcast_url # URL of the podcast this download belongs to
476 You can cancel a running download task by setting its status:
478 task.status = DownloadTask.CANCELLED
480 The task will then abort as soon as possible (due to the nature
481 of downloading data, this can take a while when the Internet is
484 The "status_changed" attribute gets set to True everytime the
485 "status" attribute changes its value. After you get the value of
486 the "status_changed" attribute, it is always reset to False:
488 if task.status_changed:
489 new_status = task.status
490 # .. update the UI accordingly ..
492 Obviously, this also means that you must have at most *one*
493 place in your UI code where you check for status changes and
494 broadcast the status updates from there.
496 While the download is taking place and after the .run() method
497 has finished, you can get the final status to check if the download
500 if task.status == DownloadTask.DONE:
501 # .. everything ok ..
502 elif task.status == DownloadTask.FAILED:
503 # .. an error happened, and the
504 # error_message attribute is set ..
505 print task.error_message
506 elif task.status == DownloadTask.PAUSED:
507 # .. user paused the download ..
508 elif task.status == DownloadTask.CANCELLED:
509 # .. user cancelled the download ..
511 The difference between cancelling and pausing a DownloadTask is
512 that the temporary file gets deleted when cancelling, but does
513 not get deleted when pausing.
515 Be sure to call .removed_from_list() on this task when removing
516 it from the UI, so that it can carry out any pending clean-up
517 actions (e.g. removing the temporary file when the task has not
518 finished successfully; i.e. task.status != DownloadTask.DONE).
520 The UI can call the method "notify_as_finished()" to determine if
521 this episode still has still to be shown as "finished" download
522 in a notification window. This will return True only the first time
523 it is called when the status is DONE. After returning True once,
524 it will always return False afterwards.
526 The same thing works for failed downloads ("notify_as_failed()").
528 # Possible states this download task can be in
529 STATUS_MESSAGE
= (_('Added'), _('Queued'), _('Downloading'),
530 _('Finished'), _('Failed'), _('Cancelled'), _('Paused'))
531 (INIT
, QUEUED
, DOWNLOADING
, DONE
, FAILED
, CANCELLED
, PAUSED
) = range(7)
534 return self
.__episode
.title
536 def __get_status(self
):
539 def __set_status(self
, status
):
540 if status
!= self
.__status
:
541 self
.__status
_changed
= True
542 self
.__status
= status
544 status
= property(fget
=__get_status
, fset
=__set_status
)
546 def __get_status_changed(self
):
547 if self
.__status
_changed
:
548 self
.__status
_changed
= False
553 status_changed
= property(fget
=__get_status_changed
)
556 return self
.__episode
.url
558 url
= property(fget
=__get_url
)
560 def __get_podcast_url(self
):
561 return self
.__episode
.channel
.url
563 podcast_url
= property(fget
=__get_podcast_url
)
565 def __get_episode(self
):
566 return self
.__episode
568 episode
= property(fget
=__get_episode
)
570 def removed_from_list(self
):
571 if self
.status
!= self
.DONE
:
572 util
.delete_file(self
.tempname
)
574 def __init__(self
, episode
, config
):
575 self
.__status
= DownloadTask
.INIT
576 self
.__status
_changed
= True
577 self
.__episode
= episode
578 self
._config
= config
580 # Set names for the downloads list
581 self
.markup_name
= saxutils
.escape(self
.__episode
.title
)
582 self
.markup_podcast_name
= saxutils
.escape(self
.__episode
.channel
.title
)
584 # Create the target filename and save it in the database
585 self
.filename
= self
.__episode
.local_filename(create
=True)
586 self
.tempname
= self
.filename
+ '.partial'
588 self
.total_size
= self
.__episode
.length
591 self
.error_message
= None
593 # Have we already shown this task in a notification?
594 self
._notification
_shown
= False
596 # Variables for speed limit and speed calculation
597 self
.__start
_time
= 0
598 self
.__start
_blocks
= 0
599 self
.__limit
_rate
_value
= self
._config
.limit_rate_value
600 self
.__limit
_rate
= self
._config
.limit_rate
603 self
._progress
_updated
= lambda x
: None
605 # If the tempname already exists, set progress accordingly
606 if os
.path
.exists(self
.tempname
):
608 already_downloaded
= os
.path
.getsize(self
.tempname
)
609 if self
.total_size
> 0:
610 self
.progress
= max(0.0, min(1.0, float(already_downloaded
)/self
.total_size
))
611 except OSError, os_error
:
612 log('Error while getting size for existing file: %s', os_error
, sender
=self
)
614 # "touch self.tempname", so we also get partial
615 # files for resuming when the file is queued
616 open(self
.tempname
, 'w').close()
618 def notify_as_finished(self
):
619 if self
.status
== DownloadTask
.DONE
:
620 if self
._notification
_shown
:
623 self
._notification
_shown
= True
628 def notify_as_failed(self
):
629 if self
.status
== DownloadTask
.FAILED
:
630 if self
._notification
_shown
:
633 self
._notification
_shown
= True
638 def add_progress_callback(self
, callback
):
639 self
._progress
_updated
= callback
641 def status_updated(self
, count
, blockSize
, totalSize
):
642 # We see a different "total size" while downloading,
643 # so correct the total size variable in the thread
644 if totalSize
!= self
.total_size
and totalSize
> 0:
645 self
.total_size
= float(totalSize
)
647 if self
.total_size
> 0:
648 self
.progress
= max(0.0, min(1.0, float(count
*blockSize
)/self
.total_size
))
649 self
._progress
_updated
(self
.progress
)
651 self
.calculate_speed(count
, blockSize
)
653 if self
.status
== DownloadTask
.CANCELLED
:
654 raise DownloadCancelledException()
656 if self
.status
== DownloadTask
.PAUSED
:
657 raise DownloadCancelledException()
659 def calculate_speed(self
, count
, blockSize
):
662 if self
.__start
_time
> 0:
663 # Has rate limiting been enabled or disabled?
664 if self
.__limit
_rate
!= self
._config
.limit_rate
:
665 # If it has been enabled then reset base time and block count
666 if self
._config
.limit_rate
:
667 self
.__start
_time
= now
668 self
.__start
_blocks
= count
669 self
.__limit
_rate
= self
._config
.limit_rate
671 # Has the rate been changed and are we currently limiting?
672 if self
.__limit
_rate
_value
!= self
._config
.limit_rate_value
and self
.__limit
_rate
:
673 self
.__start
_time
= now
674 self
.__start
_blocks
= count
675 self
.__limit
_rate
_value
= self
._config
.limit_rate_value
677 passed
= now
- self
.__start
_time
679 speed
= ((count
-self
.__start
_blocks
)*blockSize
)/passed
683 self
.__start
_time
= now
684 self
.__start
_blocks
= count
685 passed
= now
- self
.__start
_time
686 speed
= count
*blockSize
688 self
.speed
= float(speed
)
690 if self
._config
.limit_rate
and speed
> self
._config
.limit_rate_value
:
691 # calculate the time that should have passed to reach
692 # the desired download rate and wait if necessary
693 should_have_passed
= float((count
-self
.__start
_blocks
)*blockSize
)/(self
._config
.limit_rate_value
*1024.0)
694 if should_have_passed
> passed
:
695 # sleep a maximum of 10 seconds to not cause time-outs
696 delay
= min(10.0, float(should_have_passed
-passed
))
700 # Speed calculation (re-)starts here
701 self
.__start
_time
= 0
702 self
.__start
_blocks
= 0
704 # If the download has already been cancelled, skip it
705 if self
.status
== DownloadTask
.CANCELLED
:
706 util
.delete_file(self
.tempname
)
711 # We only start this download if its status is "queued"
712 if self
.status
!= DownloadTask
.QUEUED
:
715 # We are downloading this file right now
716 self
.status
= DownloadTask
.DOWNLOADING
717 self
._notification
_shown
= False
720 # Resolve URL and start downloading the episode
721 url
= youtube
.get_real_download_url(self
.__episode
.url
, \
722 self
._config
.youtube_preferred_fmt_id
)
723 downloader
= DownloadURLOpener(self
.__episode
.channel
)
724 headers
, real_url
= downloader
.retrieve_resume(url
, \
725 self
.tempname
, reporthook
=self
.status_updated
)
727 new_mimetype
= headers
.get('content-type', self
.__episode
.mimetype
)
728 old_mimetype
= self
.__episode
.mimetype
729 _basename
, ext
= os
.path
.splitext(self
.filename
)
730 if new_mimetype
!= old_mimetype
or util
.wrong_extension(ext
):
731 log('Correcting mime type: %s => %s', old_mimetype
, new_mimetype
, sender
=self
)
732 old_extension
= self
.__episode
.extension()
733 self
.__episode
.mimetype
= new_mimetype
734 new_extension
= self
.__episode
.extension()
736 # If the desired filename extension changed due to the new
737 # mimetype, we force an update of the local filename to fix the
739 if old_extension
!= new_extension
or util
.wrong_extension(ext
):
740 self
.filename
= self
.__episode
.local_filename(create
=True, force_update
=True)
742 # TODO: Check if "real_url" is different from "url" and if it is,
743 # see if we can get a better episode filename out of it
745 # Look at the Content-disposition header; use if if available
746 disposition_filename
= get_header_param(headers
, \
747 'filename', 'content-disposition')
749 if disposition_filename
is not None:
750 # The server specifies a download filename - try to use it
751 disposition_filename
= os
.path
.basename(disposition_filename
)
752 self
.filename
= self
.__episode
.local_filename(create
=True, \
753 force_update
=True, template
=disposition_filename
)
754 new_mimetype
, encoding
= mimetypes
.guess_type(self
.filename
)
755 if new_mimetype
is not None:
756 log('Using content-disposition mimetype: %s',
757 new_mimetype
, sender
=self
)
758 self
.__episode
.set_mimetype(new_mimetype
, commit
=True)
760 shutil
.move(self
.tempname
, self
.filename
)
762 # Model- and database-related updates after a download has finished
763 self
.__episode
.on_downloaded(self
.filename
)
765 # If a user command has been defined, execute the command setting some environment variables
766 if len(self
._config
.cmd_download_complete
) > 0:
767 os
.environ
["GPODDER_EPISODE_URL"]=self
.__episode
.url
or ''
768 os
.environ
["GPODDER_EPISODE_TITLE"]=self
.__episode
.title
or ''
769 os
.environ
["GPODDER_EPISODE_FILENAME"]=self
.filename
or ''
770 os
.environ
["GPODDER_EPISODE_PUBDATE"]=str(int(self
.__episode
.pubDate
))
771 os
.environ
["GPODDER_EPISODE_LINK"]=self
.__episode
.link
or ''
772 os
.environ
["GPODDER_EPISODE_DESC"]=self
.__episode
.description
or ''
773 os
.environ
["GPODDER_CHANNEL_TITLE"]=self
.__episode
.channel
.title
or ''
774 util
.run_external_command(self
._config
.cmd_download_complete
)
775 except DownloadCancelledException
:
776 log('Download has been cancelled/paused: %s', self
, sender
=self
)
777 if self
.status
== DownloadTask
.CANCELLED
:
778 util
.delete_file(self
.tempname
)
781 except urllib
.ContentTooShortError
, ctse
:
782 self
.status
= DownloadTask
.FAILED
783 self
.error_message
= _('Missing content from server')
785 log( 'Error "%s" while downloading "%s": %s', ioe
.strerror
, self
.__episode
.title
, ioe
.filename
, sender
=self
, traceback
=True)
786 self
.status
= DownloadTask
.FAILED
787 d
= {'error': ioe
.strerror
, 'filename': ioe
.filename
}
788 self
.error_message
= _('I/O Error: %(error)s: %(filename)s') % d
789 except gPodderDownloadHTTPError
, gdhe
:
790 log( 'HTTP error %s while downloading "%s": %s', gdhe
.error_code
, self
.__episode
.title
, gdhe
.error_message
, sender
=self
)
791 self
.status
= DownloadTask
.FAILED
792 d
= {'code': gdhe
.error_code
, 'message': gdhe
.error_message
}
793 self
.error_message
= _('HTTP Error %(code)s: %(message)s') % d
795 self
.status
= DownloadTask
.FAILED
796 log('Download error: %s', str(e
), traceback
=True, sender
=self
)
797 self
.error_message
= _('Error: %s') % (str(e
),)
799 if self
.status
== DownloadTask
.DOWNLOADING
:
800 # Everything went well - we're done
801 self
.status
= DownloadTask
.DONE
802 if self
.total_size
<= 0:
803 self
.total_size
= util
.calculate_size(self
.filename
)
804 log('Total size updated to %d', self
.total_size
, sender
=self
)
806 if gpodder
.user_hooks
is not None:
807 gpodder
.user_hooks
.on_episode_downloaded(self
.__episode
)
812 # We finished, but not successfully (at least not really)