2007-09-18 20:25:25 +02:00
|
|
|
# -*- coding: utf-8 -*-
|
|
|
|
#
|
|
|
|
# gPodder - A media aggregator and podcast client
|
2009-02-01 21:22:21 +01:00
|
|
|
# Copyright (c) 2005-2009 Thomas Perl and the gPodder Team
|
2007-09-18 20:25:25 +02:00
|
|
|
#
|
|
|
|
# gPodder is free software; you can redistribute it and/or modify
|
|
|
|
# it under the terms of the GNU General Public License as published by
|
|
|
|
# the Free Software Foundation; either version 3 of the License, or
|
|
|
|
# (at your option) any later version.
|
|
|
|
#
|
|
|
|
# gPodder is distributed in the hope that it will be useful,
|
|
|
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
|
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
|
|
# GNU General Public License for more details.
|
|
|
|
#
|
|
|
|
# You should have received a copy of the GNU General Public License
|
|
|
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
|
|
#
|
|
|
|
|
|
|
|
|
|
|
|
#
|
|
|
|
# download.py -- Download client using DownloadStatusManager
|
|
|
|
# Thomas Perl <thp@perli.net> 2007-09-15
|
|
|
|
#
|
|
|
|
# Based on libwget.py (2005-10-29)
|
|
|
|
#
|
|
|
|
|
|
|
|
from gpodder.liblogger import log
|
2008-03-02 14:22:29 +01:00
|
|
|
from gpodder.libgpodder import gl
|
2007-09-18 20:25:25 +02:00
|
|
|
from gpodder import util
|
|
|
|
from gpodder import services
|
2008-10-13 15:28:44 +02:00
|
|
|
from gpodder import resolver
|
2007-09-18 20:25:25 +02:00
|
|
|
import gpodder
|
|
|
|
|
|
|
|
import threading
|
|
|
|
import urllib
|
|
|
|
import shutil
|
|
|
|
import os.path
|
2008-06-15 14:46:34 +02:00
|
|
|
import os
|
2007-09-18 20:25:25 +02:00
|
|
|
import time
|
|
|
|
|
2007-11-05 13:55:36 +01:00
|
|
|
from xml.sax import saxutils
|
|
|
|
|
2007-09-18 20:25:25 +02:00
|
|
|
class DownloadCancelledException(Exception): pass
|
|
|
|
|
2008-08-10 14:38:20 +02:00
|
|
|
class gPodderDownloadHTTPError(Exception):
|
|
|
|
def __init__(self, url, error_code, error_message):
|
|
|
|
self.url = url
|
|
|
|
self.error_code = error_code
|
|
|
|
self.error_message = error_message
|
2007-09-18 20:25:25 +02:00
|
|
|
|
|
|
|
class DownloadURLOpener(urllib.FancyURLopener):
|
|
|
|
version = gpodder.user_agent
|
|
|
|
|
|
|
|
def __init__( self, channel):
|
2007-11-02 17:37:14 +01:00
|
|
|
if gl.config.proxy_use_environment:
|
2007-09-18 20:25:25 +02:00
|
|
|
proxies = None
|
|
|
|
else:
|
|
|
|
proxies = {}
|
2007-11-02 17:37:14 +01:00
|
|
|
if gl.config.http_proxy:
|
|
|
|
proxies['http'] = gl.config.http_proxy
|
|
|
|
if gl.config.ftp_proxy:
|
|
|
|
proxies['ftp'] = gl.config.ftp_proxy
|
2007-09-18 20:25:25 +02:00
|
|
|
|
|
|
|
self.channel = channel
|
|
|
|
urllib.FancyURLopener.__init__( self, proxies)
|
|
|
|
|
2008-08-10 14:38:20 +02:00
|
|
|
def http_error_default(self, url, fp, errcode, errmsg, headers):
|
|
|
|
"""
|
|
|
|
FancyURLopener by default does not raise an exception when
|
|
|
|
there is some unknown HTTP error code. We want to override
|
|
|
|
this and provide a function to log the error and raise an
|
|
|
|
exception, so we don't download the HTTP error page here.
|
|
|
|
"""
|
|
|
|
# The following two lines are copied from urllib.URLopener's
|
|
|
|
# implementation of http_error_default
|
|
|
|
void = fp.read()
|
|
|
|
fp.close()
|
|
|
|
raise gPodderDownloadHTTPError(url, errcode, errmsg)
|
|
|
|
|
2007-09-18 20:25:25 +02:00
|
|
|
def prompt_user_passwd( self, host, realm):
|
|
|
|
if self.channel.username or self.channel.password:
|
|
|
|
log( 'Authenticating as "%s" to "%s" for realm "%s".', self.channel.username, host, realm, sender = self)
|
|
|
|
return ( self.channel.username, self.channel.password )
|
|
|
|
|
|
|
|
return ( None, None )
|
|
|
|
|
|
|
|
|
|
|
|
class DownloadThread(threading.Thread):
|
2007-10-06 12:41:46 +02:00
|
|
|
MAX_UPDATES_PER_SEC = 1
|
|
|
|
|
2007-11-05 13:55:36 +01:00
|
|
|
def __init__( self, channel, episode, notification = None):
|
2007-09-18 20:25:25 +02:00
|
|
|
threading.Thread.__init__( self)
|
|
|
|
self.setDaemon( True)
|
|
|
|
|
2008-12-13 13:29:45 +01:00
|
|
|
if gpodder.interface == gpodder.MAEMO:
|
|
|
|
# Only update status every 3 seconds on Maemo
|
|
|
|
self.MAX_UPDATES_PER_SEC = 1./3.
|
|
|
|
|
2007-09-18 20:25:25 +02:00
|
|
|
self.channel = channel
|
|
|
|
self.episode = episode
|
|
|
|
|
2007-11-05 13:55:36 +01:00
|
|
|
self.notification = notification
|
|
|
|
|
2007-09-18 20:25:25 +02:00
|
|
|
self.url = self.episode.url
|
|
|
|
self.filename = self.episode.local_filename()
|
2009-01-03 17:32:26 +01:00
|
|
|
self.tempname = self.filename + '.partial'
|
2007-09-18 20:25:25 +02:00
|
|
|
|
2008-04-17 17:59:38 +02:00
|
|
|
# Make an educated guess about the total file size
|
|
|
|
self.total_size = self.episode.length
|
|
|
|
|
2007-09-18 20:25:25 +02:00
|
|
|
self.cancelled = False
|
|
|
|
self.start_time = 0.0
|
|
|
|
self.speed = _('Queued')
|
2008-08-04 14:17:01 +02:00
|
|
|
self.speed_value = 0
|
2007-09-18 20:25:25 +02:00
|
|
|
self.progress = 0.0
|
|
|
|
self.downloader = DownloadURLOpener( self.channel)
|
2007-10-06 12:41:46 +02:00
|
|
|
self.last_update = 0.0
|
2007-09-18 20:25:25 +02:00
|
|
|
|
2008-04-17 17:45:29 +02:00
|
|
|
# Keep a copy of these global variables for comparison later
|
|
|
|
self.limit_rate_value = gl.config.limit_rate_value
|
|
|
|
self.limit_rate = gl.config.limit_rate
|
|
|
|
self.start_blocks = 0
|
|
|
|
|
2007-09-18 20:25:25 +02:00
|
|
|
def cancel( self):
|
|
|
|
self.cancelled = True
|
|
|
|
|
|
|
|
def status_updated( self, count, blockSize, totalSize):
|
|
|
|
if totalSize:
|
2008-04-17 17:59:38 +02:00
|
|
|
# We see a different "total size" while downloading,
|
|
|
|
# so correct the total size variable in the thread
|
2008-08-09 17:14:16 +02:00
|
|
|
if totalSize != self.total_size and totalSize > 0:
|
2008-04-17 17:59:38 +02:00
|
|
|
log('Correcting file size for %s from %d to %d while downloading.', self.url, self.total_size, totalSize, sender=self)
|
|
|
|
self.total_size = totalSize
|
2008-08-09 17:14:16 +02:00
|
|
|
elif totalSize < 0:
|
|
|
|
# The current download has a negative value, so assume
|
|
|
|
# the total size given from the feed is correct
|
|
|
|
totalSize = self.total_size
|
|
|
|
self.progress = 100.0*float(count*blockSize)/float(totalSize)
|
2007-09-18 20:25:25 +02:00
|
|
|
else:
|
|
|
|
self.progress = 100.0
|
|
|
|
|
2008-08-09 17:14:16 +02:00
|
|
|
# Sanity checks for "progress" in valid range (0..100)
|
|
|
|
if self.progress < 0.0:
|
|
|
|
log('Warning: Progress is lower than 0 (count=%d, blockSize=%d, totalSize=%d)', count, blockSize, totalSize, sender=self)
|
|
|
|
self.progress = 0.0
|
|
|
|
elif self.progress > 100.0:
|
|
|
|
log('Warning: Progress is more than 100 (count=%d, blockSize=%d, totalSize=%d)', count, blockSize, totalSize, sender=self)
|
|
|
|
self.progress = 100.0
|
|
|
|
|
2007-09-18 20:25:25 +02:00
|
|
|
self.calculate_speed( count, blockSize)
|
2007-10-06 12:41:46 +02:00
|
|
|
if self.last_update < time.time() - (1.0 / self.MAX_UPDATES_PER_SEC):
|
|
|
|
services.download_status_manager.update_status( self.download_id, speed = self.speed, progress = self.progress)
|
|
|
|
self.last_update = time.time()
|
2007-09-18 20:25:25 +02:00
|
|
|
|
|
|
|
if self.cancelled:
|
|
|
|
util.delete_file( self.tempname)
|
|
|
|
raise DownloadCancelledException()
|
|
|
|
|
|
|
|
def calculate_speed( self, count, blockSize):
|
|
|
|
if count % 5 == 0:
|
|
|
|
now = time.time()
|
|
|
|
if self.start_time > 0:
|
2008-04-17 17:45:29 +02:00
|
|
|
|
|
|
|
# Has rate limiting been enabled or disabled?
|
|
|
|
if self.limit_rate != gl.config.limit_rate:
|
|
|
|
# If it has been enabled then reset base time and block count
|
|
|
|
if gl.config.limit_rate:
|
|
|
|
self.start_time = now
|
|
|
|
self.start_blocks = count
|
|
|
|
self.limit_rate = gl.config.limit_rate
|
|
|
|
|
|
|
|
# Has the rate been changed and are we currently limiting?
|
|
|
|
if self.limit_rate_value != gl.config.limit_rate_value and self.limit_rate:
|
|
|
|
self.start_time = now
|
|
|
|
self.start_blocks = count
|
|
|
|
self.limit_rate_value = gl.config.limit_rate_value
|
|
|
|
|
2007-09-18 20:25:25 +02:00
|
|
|
passed = now - self.start_time
|
2007-09-25 22:06:48 +02:00
|
|
|
if passed > 0:
|
2008-04-17 17:45:29 +02:00
|
|
|
speed = ((count-self.start_blocks)*blockSize)/passed
|
2007-09-25 22:06:48 +02:00
|
|
|
else:
|
|
|
|
speed = 0
|
2007-09-18 20:25:25 +02:00
|
|
|
else:
|
|
|
|
self.start_time = now
|
2008-04-17 17:45:29 +02:00
|
|
|
self.start_blocks = count
|
2007-09-18 20:25:25 +02:00
|
|
|
passed = now - self.start_time
|
|
|
|
speed = count*blockSize
|
2008-04-17 17:45:29 +02:00
|
|
|
|
2008-03-02 14:22:29 +01:00
|
|
|
self.speed = '%s/s' % gl.format_filesize(speed)
|
2008-08-04 14:17:01 +02:00
|
|
|
self.speed_value = speed
|
2007-09-18 20:25:25 +02:00
|
|
|
|
2008-04-17 17:45:29 +02:00
|
|
|
if gl.config.limit_rate and speed > gl.config.limit_rate_value:
|
2007-09-18 20:25:25 +02:00
|
|
|
# calculate the time that should have passed to reach
|
|
|
|
# the desired download rate and wait if necessary
|
2008-04-17 17:45:29 +02:00
|
|
|
should_have_passed = float((count-self.start_blocks)*blockSize)/(gl.config.limit_rate_value*1024.0)
|
2007-09-18 20:25:25 +02:00
|
|
|
if should_have_passed > passed:
|
|
|
|
# sleep a maximum of 10 seconds to not cause time-outs
|
|
|
|
delay = min( 10.0, float(should_have_passed-passed))
|
|
|
|
time.sleep( delay)
|
|
|
|
|
|
|
|
def run( self):
|
|
|
|
self.download_id = services.download_status_manager.reserve_download_id()
|
|
|
|
services.download_status_manager.register_download_id( self.download_id, self)
|
|
|
|
|
|
|
|
# Initial status update
|
|
|
|
services.download_status_manager.update_status( self.download_id, episode = self.episode.title, url = self.episode.url, speed = self.speed, progress = self.progress)
|
|
|
|
|
|
|
|
acquired = services.download_status_manager.s_acquire()
|
|
|
|
try:
|
|
|
|
try:
|
|
|
|
if self.cancelled:
|
|
|
|
return
|
|
|
|
|
|
|
|
util.delete_file( self.tempname)
|
2008-10-13 15:28:44 +02:00
|
|
|
(unused, headers) = self.downloader.retrieve( resolver.get_real_download_url(self.url), self.tempname, reporthook = self.status_updated)
|
|
|
|
|
|
|
|
if 'content-type' in headers and headers['content-type'] != self.episode.mimetype:
|
|
|
|
log('Correcting mime type: %s => %s', self.episode.mimetype, headers['content-type'])
|
|
|
|
self.episode.mimetype = headers['content-type']
|
|
|
|
# File names are constructed with regard to the mime type.
|
|
|
|
self.filename = self.episode.local_filename()
|
|
|
|
|
2007-09-18 20:25:25 +02:00
|
|
|
shutil.move( self.tempname, self.filename)
|
2008-08-07 04:30:42 +02:00
|
|
|
# Get the _real_ filesize once we actually have the file
|
|
|
|
self.episode.length = os.path.getsize(self.filename)
|
2008-10-13 15:28:44 +02:00
|
|
|
self.channel.addDownloadedItem( self.episode)
|
|
|
|
services.download_status_manager.download_completed(self.download_id)
|
2008-06-15 14:46:34 +02:00
|
|
|
|
|
|
|
# If a user command has been defined, execute the command setting some environment variables
|
|
|
|
if len(gl.config.cmd_download_complete) > 0:
|
|
|
|
os.environ["GPODDER_EPISODE_URL"]=self.episode.url or ''
|
|
|
|
os.environ["GPODDER_EPISODE_TITLE"]=self.episode.title or ''
|
|
|
|
os.environ["GPODDER_EPISODE_FILENAME"]=self.filename or ''
|
|
|
|
os.environ["GPODDER_EPISODE_PUBDATE"]=str(int(self.episode.pubDate))
|
|
|
|
os.environ["GPODDER_EPISODE_LINK"]=self.episode.link or ''
|
|
|
|
os.environ["GPODDER_EPISODE_DESC"]=self.episode.description or ''
|
|
|
|
threading.Thread(target=gl.ext_command_thread, args=(self.notification,gl.config.cmd_download_complete)).start()
|
|
|
|
|
2007-09-18 20:25:25 +02:00
|
|
|
finally:
|
|
|
|
services.download_status_manager.remove_download_id( self.download_id)
|
|
|
|
services.download_status_manager.s_release( acquired)
|
|
|
|
except DownloadCancelledException:
|
2008-04-22 21:57:02 +02:00
|
|
|
log('Download has been cancelled: %s', self.episode.title, traceback=None, sender=self)
|
2007-11-05 13:55:36 +01:00
|
|
|
except IOError, ioe:
|
2008-04-22 21:16:30 +02:00
|
|
|
if self.notification is not None:
|
2007-11-05 13:55:36 +01:00
|
|
|
title = ioe.strerror
|
|
|
|
message = _('An error happened while trying to download <b>%s</b>.') % ( saxutils.escape( self.episode.title), )
|
|
|
|
self.notification( message, title)
|
|
|
|
log( 'Error "%s" while downloading "%s": %s', ioe.strerror, self.episode.title, ioe.filename, sender = self)
|
2008-08-10 14:38:20 +02:00
|
|
|
except gPodderDownloadHTTPError, gdhe:
|
|
|
|
if self.notification is not None:
|
|
|
|
title = gdhe.error_message
|
|
|
|
message = _('An error (HTTP %d) happened while trying to download <b>%s</b>.') % ( gdhe.error_code, saxutils.escape( self.episode.title), )
|
|
|
|
self.notification( message, title)
|
|
|
|
log( 'HTTP error %s while downloading "%s": %s', gdhe.error_code, self.episode.title, gdhe.error_message, sender=self)
|
2007-09-18 20:25:25 +02:00
|
|
|
except:
|
2008-04-22 22:24:19 +02:00
|
|
|
log( 'Error while downloading "%s".', self.episode.title, sender = self, traceback = True)
|
2007-09-18 20:25:25 +02:00
|
|
|
|