Initial work on the new download manager code
This is still a work-in-progress, and many things have been broken by introducing it, but the new code is easier to understand and maintain, and should also prove performance-enhancing on Maemo. Last but not least, when it's done, it will fix these bugs: 242, 361 (http://bugs.gpodder.org)
This commit is contained in:
parent
9a3c45f082
commit
e3a8795a3e
|
@ -199,7 +199,7 @@
|
|||
</child>
|
||||
|
||||
<child>
|
||||
<widget class="GtkMenuItem" id="advanced1">
|
||||
<widget class="GtkMenuItem" id="menuSubscriptions">
|
||||
<property name="visible">True</property>
|
||||
<property name="label" translatable="yes">_Subscriptions</property>
|
||||
<property name="use_underline">True</property>
|
||||
|
|
|
@ -99,8 +99,13 @@ def run():
|
|||
for channel in channels:
|
||||
for episode in channel.get_new_episodes():
|
||||
msg( 'downloading', urllib.unquote( episode.url))
|
||||
# Calling run() calls the code in the current thread
|
||||
download.DownloadThread( channel, episode).run()
|
||||
task = download.DownloadTask(episode)
|
||||
task.status = download.DownloadTask.QUEUED
|
||||
task.run()
|
||||
if task.status == task.DONE:
|
||||
msg('done', 'Finished.')
|
||||
elif task.status == task.FAILED:
|
||||
msg('failed', 'Download error: %s' % task.error_message)
|
||||
new_episodes += 1
|
||||
|
||||
if new_episodes == 0:
|
||||
|
|
|
@ -25,6 +25,8 @@
|
|||
# Based on libwget.py (2005-10-29)
|
||||
#
|
||||
|
||||
from __future__ import with_statement
|
||||
|
||||
from gpodder.liblogger import log
|
||||
from gpodder.libgpodder import gl
|
||||
from gpodder.dbsqlite import db
|
||||
|
@ -39,6 +41,7 @@ import shutil
|
|||
import os.path
|
||||
import os
|
||||
import time
|
||||
import collections
|
||||
|
||||
from xml.sax import saxutils
|
||||
|
||||
|
@ -158,212 +161,321 @@ class DownloadURLOpener(urllib.FancyURLopener):
|
|||
return ( None, None )
|
||||
|
||||
|
||||
class DownloadThread(threading.Thread):
|
||||
MAX_UPDATES_PER_SEC = 1
|
||||
class DownloadQueueWorker(threading.Thread):
|
||||
def __init__(self, queue, exit_callback):
|
||||
threading.Thread.__init__(self)
|
||||
self.queue = queue
|
||||
self.exit_callback = exit_callback
|
||||
self.cancelled = False
|
||||
|
||||
def __init__( self, channel, episode, notification = None):
|
||||
threading.Thread.__init__( self)
|
||||
self.setDaemon( True)
|
||||
def stop_accepting_tasks(self):
|
||||
"""
|
||||
When this is called, the worker will not accept new tasks,
|
||||
but quit when the current task has been finished.
|
||||
"""
|
||||
if not self.cancelled:
|
||||
self.cancelled = True
|
||||
log('%s stopped accepting tasks.', self.getName(), sender=self)
|
||||
|
||||
if gpodder.interface == gpodder.MAEMO:
|
||||
# Only update status every 3 seconds on Maemo
|
||||
self.MAX_UPDATES_PER_SEC = 1./3.
|
||||
def run(self):
|
||||
log('Running new thread: %s', self.getName(), sender=self)
|
||||
while not self.cancelled:
|
||||
try:
|
||||
task = self.queue.pop()
|
||||
log('%s is processing: %s', self.getName(), task, sender=self)
|
||||
task.run()
|
||||
except IndexError, e:
|
||||
log('No more tasks for %s to carry out.', self.getName(), sender=self)
|
||||
break
|
||||
self.exit_callback(self)
|
||||
|
||||
self.channel = channel
|
||||
self.episode = episode
|
||||
|
||||
self.notification = notification
|
||||
class DownloadQueueManager(object):
|
||||
def __init__(self, download_status_manager):
|
||||
self.download_status_manager = download_status_manager
|
||||
self.tasks = collections.deque()
|
||||
|
||||
self.url = self.episode.url
|
||||
self.filename = self.episode.local_filename(create=True)
|
||||
# Commit the database, so we won't lose the (possibly created) filename
|
||||
self.worker_threads_access = threading.RLock()
|
||||
self.worker_threads = []
|
||||
|
||||
def __exit_callback(self, worker_thread):
|
||||
with self.worker_threads_access:
|
||||
self.worker_threads.remove(worker_thread)
|
||||
|
||||
def spawn_and_retire_threads(self, request_new_thread=False):
|
||||
with self.worker_threads_access:
|
||||
if len(self.worker_threads) > gl.config.max_downloads and \
|
||||
gl.config.max_downloads_enabled:
|
||||
# Tell the excessive amount of oldest worker threads to quit, but keep at least one
|
||||
count = min(len(self.worker_threads)-1, len(self.worker_threads)-gl.config.max_downloads)
|
||||
for worker in self.worker_threads[:count]:
|
||||
worker.stop_accepting_tasks()
|
||||
|
||||
if request_new_thread and (len(self.worker_threads) == 0 or \
|
||||
len(self.worker_threads) < gl.config.max_downloads or \
|
||||
not gl.config.max_downloads_enabled):
|
||||
# We have to create a new thread here, there's work to do
|
||||
log('I am going to spawn a new worker thread.', sender=self)
|
||||
worker = DownloadQueueWorker(self.tasks, self.__exit_callback)
|
||||
self.worker_threads.append(worker)
|
||||
worker.start()
|
||||
|
||||
def add_resumed_task(self, task):
|
||||
"""Simply add the task without starting the download"""
|
||||
self.download_status_manager.register_task(task)
|
||||
|
||||
def add_task(self, task):
|
||||
if task.status == DownloadTask.INIT:
|
||||
# This task is fresh, so add it to our status manager
|
||||
self.download_status_manager.register_task(task)
|
||||
|
||||
task.status = DownloadTask.QUEUED
|
||||
self.tasks.appendleft(task)
|
||||
self.spawn_and_retire_threads(request_new_thread=True)
|
||||
|
||||
|
||||
class DownloadTask(object):
|
||||
"""An object representing the download task of an episode
|
||||
|
||||
You can create a new download task like this:
|
||||
|
||||
task = DownloadTask(episode)
|
||||
task.status = DownloadTask.QUEUED
|
||||
task.run()
|
||||
|
||||
While the download is in progress, you can access its properties:
|
||||
|
||||
task.total_size # in bytes
|
||||
task.progress # from 0.0 to 1.0
|
||||
task.speed # in bytes per second
|
||||
str(task) # name of the episode
|
||||
task.status # current status
|
||||
|
||||
You can cancel a running download task by setting its status:
|
||||
|
||||
task.status = DownloadTask.CANCELLED
|
||||
|
||||
The task will then abort as soon as possible (due to the nature
|
||||
of downloading data, this can take a while when the Internet is
|
||||
busy).
|
||||
|
||||
While the download is taking place and after the .run() method
|
||||
has finished, you can get the final status to check if the download
|
||||
was successful:
|
||||
|
||||
if task.status == DownloadTask.DONE:
|
||||
# .. everything ok ..
|
||||
elif task.status == DownloadTask.FAILED:
|
||||
# .. an error happened, and the
|
||||
# error_message attribute is set ..
|
||||
print task.error_message
|
||||
elif task.status == DownloadTask.PAUSED:
|
||||
# .. user paused the download ..
|
||||
elif task.status == DownloadTask.CANCELLED:
|
||||
# .. user cancelled the download ..
|
||||
|
||||
The difference between cancelling and pausing a DownloadTask is
|
||||
that the temporary file gets deleted when cancelling, but does
|
||||
not get deleted when pausing.
|
||||
|
||||
Be sure to call .removed_from_list() on this task when removing
|
||||
it from the UI, so that it can carry out any pending clean-up
|
||||
actions (e.g. removing the temporary file when the task has not
|
||||
finished successfully; i.e. task.status != DownloadTask.DONE).
|
||||
"""
|
||||
# Possible states this download task can be in
|
||||
STATUS_MESSAGE = (_('Added'), _('Queued'), _('Downloading'),
|
||||
_('Finished'), _('Failed'), _('Cancelled'), _('Paused'))
|
||||
(INIT, QUEUED, DOWNLOADING, DONE, FAILED, CANCELLED, PAUSED) = range(7)
|
||||
|
||||
def __str__(self):
|
||||
return self.__episode.title
|
||||
|
||||
def __get_status(self):
|
||||
return self.__status
|
||||
|
||||
def __set_status(self, status):
|
||||
self.__status = status
|
||||
|
||||
status = property(fget=__get_status, fset=__set_status)
|
||||
|
||||
def __get_url(self):
|
||||
return self.__episode.url
|
||||
|
||||
url = property(fget=__get_url)
|
||||
|
||||
def removed_from_list(self):
|
||||
if self.status != self.DONE:
|
||||
util.delete_file(self.tempname)
|
||||
|
||||
def __init__(self, episode):
|
||||
self.__status = DownloadTask.INIT
|
||||
self.__episode = episode
|
||||
|
||||
# Create the target filename and save it in the database
|
||||
self.filename = self.__episode.local_filename(create=True)
|
||||
self.tempname = self.filename + '.partial'
|
||||
db.commit()
|
||||
|
||||
self.tempname = self.filename + '.partial'
|
||||
|
||||
# Make an educated guess about the total file size
|
||||
self.total_size = self.episode.length
|
||||
|
||||
self.cancelled = False
|
||||
self.keep_files = False
|
||||
self.start_time = 0.0
|
||||
self.speed = _('Queued')
|
||||
self.speed_value = 0
|
||||
self.total_size = self.__episode.length
|
||||
self.speed = 0.0
|
||||
self.progress = 0.0
|
||||
self.downloader = DownloadURLOpener( self.channel)
|
||||
self.last_update = 0.0
|
||||
self.error_message = None
|
||||
|
||||
# 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
|
||||
|
||||
def cancel(self, keep_files=False):
|
||||
self.cancelled = True
|
||||
self.keep_files = keep_files
|
||||
|
||||
def status_updated( self, count, blockSize, totalSize):
|
||||
if totalSize:
|
||||
# We see a different "total size" while downloading,
|
||||
# so correct the total size variable in the thread
|
||||
if totalSize != self.total_size and totalSize > 0:
|
||||
log('Correcting file size for %s from %d to %d while downloading.', self.url, self.total_size, totalSize, sender=self)
|
||||
self.total_size = totalSize
|
||||
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
|
||||
|
||||
try:
|
||||
self.progress = 100.0*float(count*blockSize)/float(totalSize)
|
||||
except ZeroDivisionError, zde:
|
||||
log('Totalsize unknown, cannot determine progress.', sender=self)
|
||||
self.progress = 100.0
|
||||
else:
|
||||
self.progress = 100.0
|
||||
|
||||
# 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
|
||||
|
||||
self.calculate_speed( count, blockSize)
|
||||
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()
|
||||
|
||||
if self.cancelled:
|
||||
if not self.keep_files:
|
||||
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:
|
||||
|
||||
# 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
|
||||
|
||||
passed = now - self.start_time
|
||||
if passed > 0:
|
||||
speed = ((count-self.start_blocks)*blockSize)/passed
|
||||
else:
|
||||
speed = 0
|
||||
else:
|
||||
self.start_time = now
|
||||
self.start_blocks = count
|
||||
passed = now - self.start_time
|
||||
speed = count*blockSize
|
||||
|
||||
self.speed = '%s/s' % gl.format_filesize(speed)
|
||||
self.speed_value = speed
|
||||
|
||||
if gl.config.limit_rate and speed > gl.config.limit_rate_value:
|
||||
# calculate the time that should have passed to reach
|
||||
# the desired download rate and wait if necessary
|
||||
should_have_passed = float((count-self.start_blocks)*blockSize)/(gl.config.limit_rate_value*1024.0)
|
||||
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)
|
||||
# Variables for speed limit and speed calculation
|
||||
self.__start_time = 0
|
||||
self.__start_blocks = 0
|
||||
self.__limit_rate_value = gl.config.limit_rate_value
|
||||
self.__limit_rate = gl.config.limit_rate
|
||||
|
||||
# If the tempname already exists, set progress accordingly
|
||||
if os.path.exists(self.tempname):
|
||||
try:
|
||||
already_downloaded = os.path.getsize(self.tempname)
|
||||
if self.total_size > 0:
|
||||
self.progress = already_downloaded/self.total_size
|
||||
|
||||
if already_downloaded > 0:
|
||||
self.speed = _('Queued (partial)')
|
||||
except:
|
||||
pass
|
||||
self.progress = max(0.0, min(1.0, float(already_downloaded)/self.total_size))
|
||||
except OSError, os_error:
|
||||
log('Error while getting size for existing file: %s', os_error, sender=self)
|
||||
else:
|
||||
# "touch self.tempname", so we also get partial
|
||||
# files for resuming when the file is queued
|
||||
open(self.tempname, 'w').close()
|
||||
|
||||
# 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)
|
||||
def status_updated(self, count, blockSize, totalSize):
|
||||
# We see a different "total size" while downloading,
|
||||
# so correct the total size variable in the thread
|
||||
if totalSize != self.total_size and totalSize > 0:
|
||||
self.total_size = float(totalSize)
|
||||
|
||||
if self.total_size > 0:
|
||||
self.progress = max(0.0, min(1.0, float(count*blockSize)/self.total_size))
|
||||
|
||||
self.calculate_speed(count, blockSize)
|
||||
|
||||
if self.status == DownloadTask.CANCELLED:
|
||||
raise DownloadCancelledException()
|
||||
|
||||
if self.status == DownloadTask.PAUSED:
|
||||
raise DownloadCancelledException()
|
||||
|
||||
def calculate_speed(self, count, blockSize):
|
||||
if count % 5 == 0:
|
||||
now = time.time()
|
||||
if self.__start_time > 0:
|
||||
# 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
|
||||
|
||||
passed = now - self.__start_time
|
||||
if passed > 0:
|
||||
speed = ((count-self.__start_blocks)*blockSize)/passed
|
||||
else:
|
||||
speed = 0
|
||||
else:
|
||||
self.__start_time = now
|
||||
self.__start_blocks = count
|
||||
passed = now - self.__start_time
|
||||
speed = count*blockSize
|
||||
|
||||
self.speed = float(speed)
|
||||
|
||||
if gl.config.limit_rate and speed > gl.config.limit_rate_value:
|
||||
# calculate the time that should have passed to reach
|
||||
# the desired download rate and wait if necessary
|
||||
should_have_passed = float((count-self.__start_blocks)*blockSize)/(gl.config.limit_rate_value*1024.0)
|
||||
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):
|
||||
# Speed calculation (re-)starts here
|
||||
self.__start_time = 0
|
||||
self.__start_blocks = 0
|
||||
|
||||
# If the download has already been cancelled, skip it
|
||||
if self.status == DownloadTask.CANCELLED:
|
||||
util.delete_file(self.tempname)
|
||||
return False
|
||||
|
||||
# We only start this download if its status is "queued"
|
||||
if self.status != DownloadTask.QUEUED:
|
||||
return False
|
||||
|
||||
# We are downloading this file right now
|
||||
self.status = DownloadTask.DOWNLOADING
|
||||
|
||||
acquired = services.download_status_manager.s_acquire()
|
||||
try:
|
||||
try:
|
||||
if self.cancelled:
|
||||
# Remove the partial file in case we do
|
||||
# not want to keep it (e.g. user cancelled)
|
||||
if not self.keep_files:
|
||||
util.delete_file(self.tempname)
|
||||
return
|
||||
|
||||
(unused, headers) = self.downloader.retrieve_resume(resolver.get_real_download_url(self.url), self.tempname, reporthook=self.status_updated)
|
||||
# Resolve URL and start downloading the episode
|
||||
url = resolver.get_real_download_url(self.__episode.url)
|
||||
downloader = DownloadURLOpener(self.__episode.channel)
|
||||
(unused, headers) = downloader.retrieve_resume(url,
|
||||
self.tempname, reporthook=self.status_updated)
|
||||
|
||||
new_mimetype = headers.get('content-type', self.episode.mimetype)
|
||||
old_mimetype = self.episode.mimetype
|
||||
if new_mimetype != old_mimetype:
|
||||
log('Correcting mime type: %s => %s', old_mimetype, new_mimetype, sender=self)
|
||||
old_extension = self.episode.extension()
|
||||
self.episode.mimetype = new_mimetype
|
||||
new_extension = self.episode.extension()
|
||||
new_mimetype = headers.get('content-type', self.__episode.mimetype)
|
||||
old_mimetype = self.__episode.mimetype
|
||||
if new_mimetype != old_mimetype:
|
||||
log('Correcting mime type: %s => %s', old_mimetype, new_mimetype, sender=self)
|
||||
old_extension = self.__episode.extension()
|
||||
self.__episode.mimetype = new_mimetype
|
||||
new_extension = self.__episode.extension()
|
||||
|
||||
# If the desired filename extension changed due to the new mimetype,
|
||||
# we force an update of the local filename to fix the extension
|
||||
if old_extension != new_extension:
|
||||
self.filename = self.episode.local_filename(create=True, force_update=True)
|
||||
# If the desired filename extension changed due to the new mimetype,
|
||||
# we force an update of the local filename to fix the extension
|
||||
if old_extension != new_extension:
|
||||
self.filename = self.__episode.local_filename(create=True, force_update=True)
|
||||
|
||||
shutil.move( self.tempname, self.filename)
|
||||
# Get the _real_ filesize once we actually have the file
|
||||
self.episode.length = os.path.getsize(self.filename)
|
||||
self.channel.addDownloadedItem( self.episode)
|
||||
services.download_status_manager.download_completed(self.download_id)
|
||||
|
||||
# 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()
|
||||
shutil.move(self.tempname, self.filename)
|
||||
|
||||
finally:
|
||||
services.download_status_manager.remove_download_id( self.download_id)
|
||||
services.download_status_manager.s_release( acquired)
|
||||
# Get the _real_ filesize once we actually have the file
|
||||
self.__episode.length = os.path.getsize(self.filename)
|
||||
self.__episode.channel.addDownloadedItem(self.__episode)
|
||||
|
||||
# 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=(gl.config.cmd_download_complete,)).start()
|
||||
except DownloadCancelledException:
|
||||
log('Download has been cancelled: %s', self.episode.title, traceback=None, sender=self)
|
||||
if not self.keep_files:
|
||||
log('Download has been cancelled/paused: %s', self, sender=self)
|
||||
if self.status == DownloadTask.CANCELLED:
|
||||
util.delete_file(self.tempname)
|
||||
self.progress = 0.0
|
||||
self.speed = 0.0
|
||||
except IOError, ioe:
|
||||
if self.notification is not None:
|
||||
title = ioe.strerror
|
||||
message = _('An error happened while trying to download <b>%s</b>. Please try again later.') % ( 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)
|
||||
log( 'Error "%s" while downloading "%s": %s', ioe.strerror, self.__episode.title, ioe.filename, sender=self)
|
||||
self.status = DownloadTask.FAILED
|
||||
self.error_message = _('I/O Error: %s: %s') % (ioe.strerror, ioe.filename)
|
||||
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>. You can try to resume the download later.') % ( 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)
|
||||
except:
|
||||
log( 'Error while downloading "%s".', self.episode.title, sender = self, traceback = True)
|
||||
log( 'HTTP error %s while downloading "%s": %s', gdhe.error_code, self.__episode.title, gdhe.error_message, sender=self)
|
||||
self.status = DownloadTask.FAILED
|
||||
self.error_message = _('HTTP Error %s: %s') % (gdhe.error_code, gdhe.error_message)
|
||||
except Exception, e:
|
||||
self.status = DownloadTask.FAILED
|
||||
self.error_message = _('Error: %s') % (e.message,)
|
||||
|
||||
if self.status == DownloadTask.DOWNLOADING:
|
||||
# Everything went well - we're done
|
||||
self.status = DownloadTask.DONE
|
||||
self.progress = 1.0
|
||||
return True
|
||||
|
||||
self.speed = 0.0
|
||||
|
||||
# We finished, but not successfully (at least not really)
|
||||
return False
|
||||
|
||||
|
|
|
@ -51,6 +51,7 @@ from gpodder import sync
|
|||
from gpodder import download
|
||||
from gpodder import SimpleGladeApp
|
||||
from gpodder import my
|
||||
from gpodder import widgets
|
||||
from gpodder.liblogger import log
|
||||
from gpodder.dbsqlite import db
|
||||
from gpodder import resolver
|
||||
|
@ -463,6 +464,7 @@ class gPodder(GladeWidget, dbus.service.Object):
|
|||
self.uar = None
|
||||
self.tray_icon = None
|
||||
self.gpodder_episode_window = None
|
||||
self.download_queue_manager = download.DownloadQueueManager(services.download_status_manager)
|
||||
|
||||
self.fullscreen = False
|
||||
self.minimized = False
|
||||
|
@ -483,9 +485,8 @@ class gPodder(GladeWidget, dbus.service.Object):
|
|||
gl.config.connect_gtk_spinbutton('limit_rate_value', self.spinLimitDownloads)
|
||||
gl.config.connect_gtk_togglebutton('limit_rate', self.cbLimitDownloads)
|
||||
|
||||
# Make sure we free/close the download queue when we
|
||||
# update the "max downloads" spin button
|
||||
changed_cb = lambda spinbutton: services.download_status_manager.update_max_downloads()
|
||||
# Then the amount of maximum downloads changes, notify the queue manager
|
||||
changed_cb = lambda spinbutton: self.download_queue_manager.spawn_and_retire_threads()
|
||||
self.spinMaxDownloads.connect('value-changed', changed_cb)
|
||||
|
||||
self.default_title = None
|
||||
|
@ -576,6 +577,8 @@ class gPodder(GladeWidget, dbus.service.Object):
|
|||
self.treeAvailable.connect('button-press-event', self.treeview_button_pressed)
|
||||
self.treeChannels.connect('button-press-event', self.treeview_channels_button_pressed)
|
||||
|
||||
self.treeDownloads.connect('button-press-event', self.treeview_downloads_button_pressed)
|
||||
|
||||
iconcell = gtk.CellRendererPixbuf()
|
||||
if gpodder.interface == gpodder.MAEMO:
|
||||
iconcell.set_fixed_size(-1, 52)
|
||||
|
@ -617,25 +620,53 @@ class gPodder(GladeWidget, dbus.service.Object):
|
|||
self.treeAvailable.get_selection().set_mode(gtk.SELECTION_SINGLE)
|
||||
else:
|
||||
self.treeAvailable.get_selection().set_mode(gtk.SELECTION_MULTIPLE)
|
||||
self.treeDownloads.get_selection().set_mode( gtk.SELECTION_MULTIPLE)
|
||||
self.treeDownloads.get_selection().set_mode(gtk.SELECTION_MULTIPLE)
|
||||
|
||||
if hasattr(self.treeDownloads, 'set_rubber_banding'):
|
||||
# Available in PyGTK 2.10 and above
|
||||
self.treeDownloads.set_rubber_banding(True)
|
||||
|
||||
# columns and renderers for "download progress" tab
|
||||
episodecell = gtk.CellRendererText()
|
||||
episodecell.set_property('ellipsize', pango.ELLIPSIZE_END)
|
||||
episodecolumn = gtk.TreeViewColumn( _("Episode"), episodecell, text=0)
|
||||
episodecolumn.set_sizing(gtk.TREE_VIEW_COLUMN_AUTOSIZE)
|
||||
episodecolumn.set_resizable(True)
|
||||
episodecolumn.set_expand(True)
|
||||
|
||||
speedcell = gtk.CellRendererText()
|
||||
speedcolumn = gtk.TreeViewColumn( _("Speed"), speedcell, text=1)
|
||||
|
||||
progresscell = gtk.CellRendererProgress()
|
||||
progresscolumn = gtk.TreeViewColumn( _("Progress"), progresscell, value=2)
|
||||
progresscolumn.set_expand(True)
|
||||
|
||||
for itemcolumn in ( episodecolumn, speedcolumn, progresscolumn ):
|
||||
self.treeDownloads.append_column( itemcolumn)
|
||||
dsm = services.download_status_manager
|
||||
|
||||
# First column: [ICON] Episodename
|
||||
column = gtk.TreeViewColumn(_('Episode'))
|
||||
|
||||
cell = gtk.CellRendererPixbuf()
|
||||
cell.set_property('stock-size', gtk.ICON_SIZE_MENU)
|
||||
column.pack_start(cell, expand=False)
|
||||
column.add_attribute(cell, 'stock-id', dsm.C_ICON_NAME)
|
||||
|
||||
cell = gtk.CellRendererText()
|
||||
cell.set_property('ellipsize', pango.ELLIPSIZE_END)
|
||||
column.pack_start(cell, expand=True)
|
||||
column.add_attribute(cell, 'text', dsm.C_NAME)
|
||||
|
||||
column.set_sizing(gtk.TREE_VIEW_COLUMN_AUTOSIZE)
|
||||
column.set_resizable(True)
|
||||
column.set_expand(True)
|
||||
self.treeDownloads.append_column(column)
|
||||
|
||||
# Second column: Progress
|
||||
column = gtk.TreeViewColumn(_('Progress'), gtk.CellRendererProgress(),
|
||||
value=dsm.C_PROGRESS, text=dsm.C_PROGRESS_TEXT)
|
||||
column.set_expand(True)
|
||||
self.treeDownloads.append_column(column)
|
||||
|
||||
# Third column: Size
|
||||
column = gtk.TreeViewColumn(_('Size'), gtk.CellRendererText(),
|
||||
text=dsm.C_SIZE_TEXT)
|
||||
self.treeDownloads.append_column(column)
|
||||
|
||||
# Fourth column: Speed
|
||||
column = gtk.TreeViewColumn(_('Speed'), gtk.CellRendererText(),
|
||||
text=dsm.C_SPEED_TEXT)
|
||||
self.treeDownloads.append_column(column)
|
||||
|
||||
# Fifth column: Status
|
||||
column = gtk.TreeViewColumn(_('Status'), gtk.CellRendererText(),
|
||||
text=dsm.C_STATUS_TEXT)
|
||||
self.treeDownloads.append_column(column)
|
||||
|
||||
# After we've set up most of the window, show it :)
|
||||
if not gpodder.interface == gpodder.MAEMO:
|
||||
|
@ -655,13 +686,13 @@ class gPodder(GladeWidget, dbus.service.Object):
|
|||
# treeChannels row numbers to generate tree paths
|
||||
self.channel_url_path_mapping = {}
|
||||
|
||||
services.download_status_manager.register( 'list-changed', self.download_status_updated)
|
||||
services.download_status_manager.register( 'progress-changed', self.download_progress_updated)
|
||||
services.cover_downloader.register('cover-available', self.cover_download_finished)
|
||||
services.cover_downloader.register('cover-removed', self.cover_file_removed)
|
||||
self.cover_cache = {}
|
||||
|
||||
self.treeDownloads.set_model( services.download_status_manager.tree_model)
|
||||
self.treeDownloads.set_model(services.download_status_manager.get_tree_model())
|
||||
gobject.timeout_add(1500, self.update_downloads_list)
|
||||
self.download_tasks_seen = set()
|
||||
|
||||
#Add Drag and Drop Support
|
||||
flags = gtk.DEST_DEFAULT_ALL
|
||||
|
@ -703,6 +734,9 @@ class gPodder(GladeWidget, dbus.service.Object):
|
|||
# Clean up old, orphaned download files
|
||||
partial_files = gl.find_partial_files()
|
||||
|
||||
# Message area
|
||||
self.message_area = None
|
||||
|
||||
resumable_episodes = []
|
||||
if len(partial_files) > 0:
|
||||
for f in partial_files:
|
||||
|
@ -720,28 +754,13 @@ class gPodder(GladeWidget, dbus.service.Object):
|
|||
if found_episode:
|
||||
break
|
||||
|
||||
def remove_partial_file(episode):
|
||||
fn = episode.local_filename(create=False)
|
||||
if fn is not None:
|
||||
util.delete_file(fn+'.partial')
|
||||
|
||||
if len(resumable_episodes):
|
||||
if gl.config.resume_ask_every_episode:
|
||||
gPodderEpisodeSelector(title = _('Resume downloads'), instructions = _('There are unfinished downloads from your last session. Pick the ones you want to resume.'), \
|
||||
episodes = resumable_episodes, \
|
||||
stock_ok_button = 'gpodder-download', callback = self.download_episode_list, remove_callback=remove_partial_file)
|
||||
else:
|
||||
if len(resumable_episodes) == 0:
|
||||
question = _('There is one partially downloaded episode. Do you want to continue downloading it?')
|
||||
else:
|
||||
question = _('There are %d partially downloaded episodes. Do you want to continue downloading them?') % (len(resumable_episodes))
|
||||
|
||||
if self.show_confirmation(question, _('Resume downloads from last session')):
|
||||
self.download_episode_list(resumable_episodes)
|
||||
else:
|
||||
for episode in resumable_episodes:
|
||||
remove_partial_file(episode)
|
||||
|
||||
self.download_episode_list_paused(resumable_episodes)
|
||||
self.message_area = widgets.SimpleMessageArea(_('There are unfinished downloads from your last session.\nPick the ones you want to continue downloading.'))
|
||||
self.vboxDownloadStatusWidgets.pack_start(self.message_area, expand=False)
|
||||
self.vboxDownloadStatusWidgets.reorder_child(self.message_area, 0)
|
||||
self.message_area.show_all()
|
||||
self.wNotebook.set_current_page(1)
|
||||
|
||||
gl.clean_up_downloads(delete_partial=False)
|
||||
else:
|
||||
|
@ -761,6 +780,83 @@ class gPodder(GladeWidget, dbus.service.Object):
|
|||
if len(self.channels) == 0:
|
||||
util.idle_add(self.on_itemUpdate_activate, None)
|
||||
|
||||
def update_downloads_list(self):
|
||||
model = self.treeDownloads.get_model()
|
||||
|
||||
downloading, failed, finished, queued = 0, 0, 0, 0
|
||||
total_speed, total_size, done_size = 0, 0, 0
|
||||
|
||||
# Keep a list of all download tasks that we've seen
|
||||
old_download_tasks_seen = self.download_tasks_seen
|
||||
self.download_tasks_seen = set()
|
||||
|
||||
for row in model:
|
||||
services.download_status_manager.request_update(row.iter)
|
||||
|
||||
task = row[services.download_status_manager.C_TASK]
|
||||
speed, size, status, progress = task.speed, task.total_size, task.status, task.progress
|
||||
|
||||
total_size += size
|
||||
done_size += size*progress
|
||||
|
||||
self.download_tasks_seen.add(task)
|
||||
|
||||
if status == download.DownloadTask.DOWNLOADING:
|
||||
downloading += 1
|
||||
total_speed += speed
|
||||
elif status == download.DownloadTask.FAILED:
|
||||
failed += 1
|
||||
elif status == download.DownloadTask.DONE:
|
||||
finished += 1
|
||||
elif status == download.DownloadTask.QUEUED:
|
||||
queued += 1
|
||||
|
||||
text = [_('Downloads')]
|
||||
if downloading + failed + finished + queued > 0:
|
||||
s = []
|
||||
if downloading > 0:
|
||||
s.append(_('%d downloading') % downloading)
|
||||
if failed > 0:
|
||||
s.append(_('%d failed') % failed)
|
||||
if finished > 0:
|
||||
s.append(_('%d done') % finished)
|
||||
if queued > 0:
|
||||
s.append(_('%d queued') % queued)
|
||||
text.append(' (' + ', '.join(s)+')')
|
||||
self.labelDownloads.set_text(''.join(text))
|
||||
|
||||
title = [self.default_title]
|
||||
|
||||
# Calculate the difference based on what we've seen now and before
|
||||
episode_urls = [task.url for task in self.download_tasks_seen.symmetric_difference(old_download_tasks_seen)]
|
||||
last_download_count = sum(1 for task in old_download_tasks_seen if task.status in (task.DOWNLOADING, task.QUEUED))
|
||||
|
||||
count = downloading + queued
|
||||
if count > 0:
|
||||
if count == 1:
|
||||
title.append( _('downloading one file'))
|
||||
elif count > 1:
|
||||
title.append( _('downloading %d files') % count)
|
||||
|
||||
if total_size > 0:
|
||||
percentage = 100.0*done_size/total_size
|
||||
else:
|
||||
percentage = 0.0
|
||||
total_speed = gl.format_filesize(total_speed)
|
||||
title[1] += ' (%d%%, %s/s)' % (percentage, total_speed)
|
||||
elif last_download_count > 0:
|
||||
log('All downloads have finished.', sender=self)
|
||||
if gl.config.cmd_all_downloads_complete:
|
||||
Thread(target=gl.ext_command_thread, args=( \
|
||||
gl.config.cmd_all_downloads_complete)).start()
|
||||
|
||||
self.gPodder.set_title(' - '.join(title))
|
||||
|
||||
self.update_episode_list_icons(episode_urls)
|
||||
self.play_or_download()
|
||||
#self.updateComboBox(only_these_urls=channel_urls)
|
||||
return True
|
||||
|
||||
def on_tree_channels_resize(self, widget, allocation):
|
||||
if not gl.config.podcast_sidebar_save_space:
|
||||
return
|
||||
|
@ -909,6 +1005,91 @@ class gPodder(GladeWidget, dbus.service.Object):
|
|||
self.active_channel.update_m3u_playlist()
|
||||
self.show_message(_('Updated M3U playlist in download folder.'), _('Updated playlist'))
|
||||
|
||||
def treeview_downloads_button_pressed(self, treeview, event):
|
||||
if event.button == 1:
|
||||
# Catch left mouse button presses, and if we there is no
|
||||
# path at the given position, deselect all items
|
||||
(x, y) = (int(event.x), int(event.y))
|
||||
(path, column, rx, ry) = treeview.get_path_at_pos(x, y) or (None,)*4
|
||||
if path is None:
|
||||
treeview.get_selection().unselect_all()
|
||||
|
||||
if event.button == 3:
|
||||
(x, y) = (int(event.x), int(event.y))
|
||||
(path, column, rx, ry) = treeview.get_path_at_pos(x, y) or (None,)*4
|
||||
|
||||
paths = []
|
||||
# Did the user right-click into a selection?
|
||||
selection = treeview.get_selection()
|
||||
if selection.count_selected_rows() and path:
|
||||
(model, paths) = selection.get_selected_rows()
|
||||
if path not in paths:
|
||||
# We have right-clicked, but not into the
|
||||
# selection, assume we don't want to operate
|
||||
# on the selection
|
||||
paths = []
|
||||
|
||||
# No selection or right click not in selection:
|
||||
# Select the single item where we clicked
|
||||
if not paths and path:
|
||||
treeview.grab_focus()
|
||||
treeview.set_cursor( path, column, 0)
|
||||
(model, paths) = (treeview.get_model(), [path])
|
||||
|
||||
# We did not find a selection, and the user didn't
|
||||
# click on an item to select -- don't show the menu
|
||||
if not paths:
|
||||
return True
|
||||
|
||||
selected_tasks = [(gtk.TreeRowReference(model, path), model.get_value(model.get_iter(path), 0)) for path in paths]
|
||||
|
||||
def make_menu_item(label, stock_id, tasks, status):
|
||||
# This creates a menu item for selection-wide actions
|
||||
def for_each_task_set_status(tasks, status):
|
||||
for row_reference, task in tasks:
|
||||
if status is not None:
|
||||
if status == download.DownloadTask.QUEUED:
|
||||
# Only queue task when its paused/failed/cancelled
|
||||
if task.status in (task.PAUSED, task.FAILED, task.CANCELLED):
|
||||
self.download_queue_manager.add_task(task)
|
||||
elif status == download.DownloadTask.CANCELLED:
|
||||
# Cancelling a download only allows when paused/downloading/queued
|
||||
if task.status in (task.QUEUED, task.DOWNLOADING, task.PAUSED):
|
||||
task.status = status
|
||||
elif status == download.DownloadTask.PAUSED:
|
||||
# Pausing a download only when queued/downloading
|
||||
if task.status in (task.DOWNLOADING, task.QUEUED):
|
||||
task.status = status
|
||||
else:
|
||||
# We (hopefully) can simply set the task status here
|
||||
task.status = status
|
||||
else:
|
||||
# Remove the selected task - cancel downloading/queued tasks
|
||||
if task.status in (task.QUEUED, task.DOWNLOADING):
|
||||
task.status = task.CANCELLED
|
||||
model.remove(model.get_iter(row_reference.get_path()))
|
||||
# Tell the task that it has been removed (so it can clean up)
|
||||
task.removed_from_list()
|
||||
return True
|
||||
item = gtk.ImageMenuItem(label)
|
||||
item.set_image(gtk.image_new_from_stock(stock_id, gtk.ICON_SIZE_MENU))
|
||||
item.connect('activate', lambda item: for_each_task_set_status(tasks, status))
|
||||
return item
|
||||
|
||||
menu = gtk.Menu()
|
||||
|
||||
menu.append(make_menu_item(_('Download'), gtk.STOCK_GO_DOWN, selected_tasks, download.DownloadTask.QUEUED))
|
||||
menu.append(gtk.SeparatorMenuItem())
|
||||
menu.append(make_menu_item(_('Cancel'), gtk.STOCK_CANCEL, selected_tasks, download.DownloadTask.CANCELLED))
|
||||
menu.append(make_menu_item(_('Pause'), gtk.STOCK_MEDIA_PAUSE, selected_tasks, download.DownloadTask.PAUSED))
|
||||
menu.append(gtk.SeparatorMenuItem())
|
||||
menu.append(make_menu_item(_('Remove from list'), gtk.STOCK_REMOVE, selected_tasks, None))
|
||||
|
||||
menu.show_all()
|
||||
menu.popup(None, None, None, event.button, event.time)
|
||||
return True
|
||||
|
||||
|
||||
def treeview_channels_button_pressed( self, treeview, event):
|
||||
global WEB_BROWSER_ICON
|
||||
|
||||
|
@ -1311,27 +1492,6 @@ class gPodder(GladeWidget, dbus.service.Object):
|
|||
self.default_title = new_title
|
||||
self.gPodder.set_title(new_title)
|
||||
|
||||
def download_progress_updated( self, count, percentage):
|
||||
title = [ self.default_title ]
|
||||
|
||||
total_speed = gl.format_filesize(services.download_status_manager.total_speed())
|
||||
|
||||
if count == 1:
|
||||
title.append( _('downloading one file'))
|
||||
elif count > 1:
|
||||
title.append( _('downloading %d files') % count)
|
||||
|
||||
if len(title) == 2:
|
||||
title[1] = ''.join( [ title[1], ' (%d%%, %s/s)' % (percentage, total_speed) ])
|
||||
|
||||
self.gPodder.set_title( ' - '.join( title))
|
||||
|
||||
# Have all the downloads completed?
|
||||
# If so execute user command if defined, else do nothing
|
||||
if count == 0:
|
||||
if len(gl.config.cmd_all_downloads_complete) > 0:
|
||||
Thread(target=gl.ext_command_thread, args=(self.notification,gl.config.cmd_all_downloads_complete)).start()
|
||||
|
||||
def update_selected_episode_list_icons(self):
|
||||
"""
|
||||
Updates the status icons in the episode list
|
||||
|
@ -1340,7 +1500,7 @@ class gPodder(GladeWidget, dbus.service.Object):
|
|||
(model, paths) = selection.get_selected_rows()
|
||||
for path in paths:
|
||||
iter = model.get_iter(path)
|
||||
self.active_channel.iter_set_downloading_columns(model, iter)
|
||||
self.active_channel.iter_set_downloading_columns(model, iter, downloading=self.episode_is_downloading)
|
||||
|
||||
def update_episode_list_icons(self, urls):
|
||||
"""
|
||||
|
@ -1358,7 +1518,7 @@ class gPodder(GladeWidget, dbus.service.Object):
|
|||
for url in urls:
|
||||
if url in self.url_path_mapping:
|
||||
path = (self.url_path_mapping[url],)
|
||||
self.active_channel.iter_set_downloading_columns(model, model.get_iter(path))
|
||||
self.active_channel.iter_set_downloading_columns(model, model.get_iter(path), downloading=self.episode_is_downloading)
|
||||
|
||||
def playback_episode(self, episode, stream=False):
|
||||
if gpodder.interface == gpodder.MAEMO:
|
||||
|
@ -1426,7 +1586,7 @@ class gPodder(GladeWidget, dbus.service.Object):
|
|||
if not can_play:
|
||||
can_download = True
|
||||
else:
|
||||
if services.download_status_manager.is_download_in_progress(url):
|
||||
if self.episode_is_downloading(episode):
|
||||
can_cancel = True
|
||||
else:
|
||||
can_download = True
|
||||
|
@ -1494,16 +1654,6 @@ class gPodder(GladeWidget, dbus.service.Object):
|
|||
|
||||
return (can_play, can_download, can_transfer, can_cancel, can_delete, open_instead_of_play)
|
||||
|
||||
def download_status_updated(self, episode_urls, channel_urls):
|
||||
count = services.download_status_manager.count()
|
||||
if count:
|
||||
self.labelDownloads.set_text( _('Downloads (%d)') % count)
|
||||
else:
|
||||
self.labelDownloads.set_text( _('Downloads'))
|
||||
|
||||
self.update_episode_list_icons(episode_urls)
|
||||
self.updateComboBox(only_these_urls=channel_urls)
|
||||
|
||||
def on_cbMaxDownloads_toggled(self, widget, *args):
|
||||
self.spinMaxDownloads.set_sensitive(self.cbMaxDownloads.get_active())
|
||||
|
||||
|
@ -1582,6 +1732,10 @@ class gPodder(GladeWidget, dbus.service.Object):
|
|||
log( 'Cannot set selection on treeChannels', sender = self)
|
||||
self.on_treeChannels_cursor_changed( self.treeChannels)
|
||||
self.channel_list_changed = False
|
||||
|
||||
def episode_is_downloading(self, episode):
|
||||
"""Returns True if the given episode is being downloaded at the moment"""
|
||||
return episode.url in (task.url for task in self.download_tasks_seen if task.status in (task.DOWNLOADING, task.QUEUED, task.PAUSED))
|
||||
|
||||
def updateTreeView(self):
|
||||
if self.channels and self.active_channel is not None:
|
||||
|
@ -1590,7 +1744,7 @@ class gPodder(GladeWidget, dbus.service.Object):
|
|||
else:
|
||||
banner = None
|
||||
def thread_func(self, banner, active_channel):
|
||||
(model, urls) = self.active_channel.get_tree_model()
|
||||
(model, urls) = self.active_channel.get_tree_model(self.episode_is_downloading)
|
||||
mapping = dict(zip(urls, range(len(urls))))
|
||||
def update_gui_with_new_model(self, channel, model, urls, mapping, banner):
|
||||
if self.active_channel is not None and channel is not None:
|
||||
|
@ -1931,7 +2085,7 @@ class gPodder(GladeWidget, dbus.service.Object):
|
|||
Displays a confirmation dialog (and closes/hides gPodder)
|
||||
"""
|
||||
|
||||
downloading = services.download_status_manager.has_items()
|
||||
downloading = False #services.download_status_manager.has_items()
|
||||
|
||||
# Only iconify if we are using the window's "X" button,
|
||||
# but not when we are using "Quit" in the menu or toolbar
|
||||
|
@ -1984,7 +2138,7 @@ class gPodder(GladeWidget, dbus.service.Object):
|
|||
else:
|
||||
self.show_message(_('Please check your permissions and free disk space.'), _('Error saving podcast list'))
|
||||
|
||||
services.download_status_manager.cancel_all(keep_files=True)
|
||||
services.download_status_manager.pause_all_downloads()
|
||||
self.gPodder.hide()
|
||||
while gtk.events_pending():
|
||||
gtk.main_iteration(False)
|
||||
|
@ -2035,7 +2189,7 @@ class gPodder(GladeWidget, dbus.service.Object):
|
|||
episode_urls.add(episode.url)
|
||||
channel_urls.add(episode.channel.url)
|
||||
|
||||
self.download_status_updated(episode_urls, channel_urls)
|
||||
#self.download_status_updated(episode_urls, channel_urls)
|
||||
|
||||
def on_itemRemoveOldEpisodes_activate( self, widget):
|
||||
columns = (
|
||||
|
@ -2167,13 +2321,29 @@ class gPodder(GladeWidget, dbus.service.Object):
|
|||
else:
|
||||
gPodderWelcome(center_on_widget=self.gPodder, show_example_podcasts_callback=self.on_itemImportChannels_activate, setup_my_gpodder_callback=self.on_download_from_mygpo)
|
||||
|
||||
def download_episode_list(self, episodes):
|
||||
services.download_status_manager.start_batch_mode()
|
||||
def download_episode_list_paused(self, episodes):
|
||||
self.download_episode_list(episodes, True)
|
||||
|
||||
def download_episode_list(self, episodes, add_paused=False):
|
||||
for episode in episodes:
|
||||
log('Downloading episode: %s', episode.title, sender = self)
|
||||
if not episode.was_downloaded(and_exists=True) and not services.download_status_manager.is_download_in_progress( episode.url):
|
||||
download.DownloadThread(episode.channel, episode, self.notification).start()
|
||||
services.download_status_manager.end_batch_mode()
|
||||
if not episode.was_downloaded(and_exists=True):
|
||||
task_exists = False
|
||||
for task in self.download_tasks_seen:
|
||||
if episode.url == task.url and task.status not in (task.DOWNLOADING, task.QUEUED):
|
||||
self.download_queue_manager.add_task(task)
|
||||
task_exists = True
|
||||
continue
|
||||
|
||||
if task_exists:
|
||||
continue
|
||||
|
||||
task = download.DownloadTask(episode)
|
||||
if add_paused:
|
||||
task.status = task.PAUSED
|
||||
self.download_queue_manager.add_resumed_task(task)
|
||||
else:
|
||||
self.download_queue_manager.add_task(task)
|
||||
|
||||
def new_episodes_show(self, episodes):
|
||||
columns = (
|
||||
|
@ -2612,7 +2782,7 @@ class gPodder(GladeWidget, dbus.service.Object):
|
|||
log('Not removing downloaded episodes', sender=self)
|
||||
|
||||
# only delete partial files if we do not have any downloads in progress
|
||||
delete_partial = not services.download_status_manager.has_items()
|
||||
delete_partial = False #not services.download_status_manager.has_items()
|
||||
gl.clean_up_downloads(delete_partial)
|
||||
|
||||
# cancel any active downloads from this channel
|
||||
|
@ -2741,11 +2911,20 @@ class gPodder(GladeWidget, dbus.service.Object):
|
|||
self.set_title(tab_label)
|
||||
if page_num == 0:
|
||||
self.play_or_download()
|
||||
self.menuChannels.set_sensitive(True)
|
||||
self.menuSubscriptions.set_sensitive(True)
|
||||
# The message area in the downloads tab should be hidden
|
||||
# when the user switches away from the downloads tab
|
||||
if self.message_area is not None:
|
||||
self.message_area.hide()
|
||||
self.message_area = None
|
||||
else:
|
||||
self.menuChannels.set_sensitive(False)
|
||||
self.menuSubscriptions.set_sensitive(False)
|
||||
self.toolDownload.set_sensitive( False)
|
||||
self.toolPlay.set_sensitive( False)
|
||||
self.toolTransfer.set_sensitive( False)
|
||||
self.toolCancel.set_sensitive( services.download_status_manager.has_items())
|
||||
self.toolCancel.set_sensitive( False)#services.download_status_manager.has_items())
|
||||
|
||||
def on_treeChannels_row_activated(self, widget, path, *args):
|
||||
# double-click action of the podcast list or enter
|
||||
|
@ -2845,6 +3024,7 @@ class gPodder(GladeWidget, dbus.service.Object):
|
|||
self.gpodder_episode_window.show(episode=episode, download_callback=download_callback, play_callback=play_callback)
|
||||
else:
|
||||
self.download_episode_list(episodes)
|
||||
self.update_selected_episode_list_icons()
|
||||
self.play_or_download()
|
||||
except:
|
||||
log('Error in on_treeAvailable_row_activated', traceback=True, sender=self)
|
||||
|
@ -2861,38 +3041,32 @@ class gPodder(GladeWidget, dbus.service.Object):
|
|||
gobject.timeout_add(next_update, self.auto_update_procedure)
|
||||
|
||||
def on_treeDownloads_row_activated(self, widget, *args):
|
||||
cancel_urls = []
|
||||
|
||||
if self.wNotebook.get_current_page() > 0:
|
||||
# Use the download list treeview + model
|
||||
( tree, column ) = ( self.treeDownloads, 3 )
|
||||
else:
|
||||
if self.wNotebook.get_current_page() == 0:
|
||||
# Use the available podcasts treeview + model
|
||||
( tree, column ) = ( self.treeAvailable, 0 )
|
||||
|
||||
selection = tree.get_selection()
|
||||
(model, paths) = selection.get_selected_rows()
|
||||
for path in paths:
|
||||
url = model.get_value( model.get_iter( path), column)
|
||||
cancel_urls.append( url)
|
||||
|
||||
if len( cancel_urls) == 0:
|
||||
log('Nothing selected.', sender = self)
|
||||
selection = self.treeAvailable.get_selection()
|
||||
(model, paths) = selection.get_selected_rows()
|
||||
urls = [model.get_value(model.get_iter(path), 0) for path in paths]
|
||||
selected_tasks = [task for task in self.download_tasks_seen if task.url in urls]
|
||||
for task in selected_tasks:
|
||||
task.status = task.CANCELLED
|
||||
self.update_selected_episode_list_icons()
|
||||
self.play_or_download()
|
||||
return
|
||||
|
||||
if len( cancel_urls) == 1:
|
||||
title = _('Cancel download?')
|
||||
message = _("Cancelling this download will remove the partially downloaded file and stop the download.")
|
||||
else:
|
||||
title = _('Cancel downloads?')
|
||||
message = _("Cancelling the download will stop the %d selected downloads and remove partially downloaded files.") % selection.count_selected_rows()
|
||||
# Use the standard way of working on the treeview
|
||||
selection = self.treeDownloads.get_selection()
|
||||
(model, paths) = selection.get_selected_rows()
|
||||
selected_tasks = [(gtk.TreeRowReference(model, path), model.get_value(model.get_iter(path), 0)) for path in paths]
|
||||
|
||||
if self.show_confirmation( message, title):
|
||||
services.download_status_manager.start_batch_mode()
|
||||
for url in cancel_urls:
|
||||
services.download_status_manager.cancel_by_url( url)
|
||||
services.download_status_manager.end_batch_mode()
|
||||
self.play_or_download()
|
||||
for tree_row_reference, task in selected_tasks:
|
||||
if task.status in (task.DOWNLOADING, task.QUEUED):
|
||||
task.status = task.PAUSED
|
||||
elif task.status in (task.CANCELLED, task.PAUSED, task.FAILED):
|
||||
self.download_queue_manager.add_task(task)
|
||||
elif task.status == task.DONE:
|
||||
model.remove(model.get_iter(tree_row_reference.get_path()))
|
||||
|
||||
self.play_or_download()
|
||||
|
||||
def on_btnCancelDownloadStatus_clicked(self, widget, *args):
|
||||
self.on_treeDownloads_row_activated( widget, None)
|
||||
|
@ -2959,7 +3133,7 @@ class gPodder(GladeWidget, dbus.service.Object):
|
|||
log( 'Error while deleting (some) downloads.', traceback=True, sender=self)
|
||||
|
||||
# only delete partial files if we do not have any downloads in progress
|
||||
delete_partial = not services.download_status_manager.has_items()
|
||||
delete_partial = False #not services.download_status_manager.has_items()
|
||||
gl.clean_up_downloads(delete_partial)
|
||||
self.update_selected_episode_list_icons()
|
||||
self.play_or_download()
|
||||
|
@ -3569,8 +3743,8 @@ class gPodderEpisode(GladeWidget):
|
|||
setattr(self, 'play_callback', None)
|
||||
self.gPodderEpisode.connect('delete-event', self.on_delete_event)
|
||||
gl.config.connect_gtk_window(self.gPodderEpisode, 'episode_window', True)
|
||||
services.download_status_manager.register('list-changed', self.on_download_status_changed)
|
||||
services.download_status_manager.register('progress-detail', self.on_download_status_progress)
|
||||
#services.download_status_manager.register('list-changed', self.on_download_status_changed)
|
||||
#services.download_status_manager.register('progress-detail', self.on_download_status_progress)
|
||||
self.textview.modify_bg(gtk.STATE_NORMAL, gtk.gdk.color_parse('#ffffff'))
|
||||
if gl.config.enable_html_shownotes:
|
||||
try:
|
||||
|
@ -3668,8 +3842,6 @@ class gPodderEpisode(GladeWidget):
|
|||
b.insert_with_tags_by_name(b.get_end_iter(), '\n'.join(footer), 'footer')
|
||||
b.place_cursor(b.get_start_iter())
|
||||
|
||||
services.download_status_manager.request_progress_detail(self.episode.url)
|
||||
|
||||
def on_cancel(self, widget):
|
||||
services.download_status_manager.cancel_by_url(self.episode.url)
|
||||
|
||||
|
@ -3706,7 +3878,7 @@ class gPodderEpisode(GladeWidget):
|
|||
self.download_progress.set_text('Downloading: %d%% (%s)' % (progress, speed))
|
||||
|
||||
def hide_show_widgets(self):
|
||||
is_downloading = services.download_status_manager.is_download_in_progress(self.episode.url)
|
||||
is_downloading = False #services.download_status_manager.is_download_in_progress(self.episode.url)
|
||||
if is_downloading:
|
||||
self.download_progress.show_all()
|
||||
self.btnCancel.show_all()
|
||||
|
|
|
@ -389,7 +389,7 @@ class gPodderLib(object):
|
|||
return ( False, command_line[0] )
|
||||
return ( True, command_line[0] )
|
||||
|
||||
def ext_command_thread(self, notification, command_line):
|
||||
def ext_command_thread(self, command_line):
|
||||
"""
|
||||
This is the function that will be called in a separate
|
||||
thread that will call an external command (specified by
|
||||
|
@ -404,6 +404,10 @@ class gPodderLib(object):
|
|||
|
||||
p = subprocess.Popen(command_line, shell=True, stdout=sys.stdout, stderr=sys.stderr)
|
||||
result = p.wait()
|
||||
|
||||
# FIXME: NOTIFICATION
|
||||
def notification(message, title):
|
||||
log('Message: %s (%s)', title, message, sender=self)
|
||||
|
||||
if result == 127:
|
||||
title = _('User command not found')
|
||||
|
|
|
@ -356,7 +356,7 @@ class PodcastChannel(PodcastModelObject):
|
|||
return db.load_episodes(self, factory=self.episode_factory, state=db.STATE_DOWNLOADED)
|
||||
|
||||
def get_new_episodes( self):
|
||||
return [episode for episode in db.load_episodes(self, factory=self.episode_factory) if episode.state == db.STATE_NORMAL and not episode.is_played and not services.download_status_manager.is_download_in_progress(episode.url)]
|
||||
return [episode for episode in db.load_episodes(self, factory=self.episode_factory) if episode.state == db.STATE_NORMAL and not episode.is_played] # and not services.download_status_manager.is_download_in_progress(episode.url)]
|
||||
|
||||
def update_m3u_playlist(self):
|
||||
if gl.config.create_m3u_playlists:
|
||||
|
@ -407,7 +407,7 @@ class PodcastChannel(PodcastModelObject):
|
|||
def get_all_episodes(self):
|
||||
return db.load_episodes(self, factory=self.episode_factory)
|
||||
|
||||
def iter_set_downloading_columns( self, model, iter, episode=None):
|
||||
def iter_set_downloading_columns(self, model, iter, episode=None, downloading=None):
|
||||
global ICON_AUDIO_FILE, ICON_VIDEO_FILE
|
||||
global ICON_DOWNLOADING, ICON_DELETED, ICON_NEW
|
||||
|
||||
|
@ -422,7 +422,7 @@ class PodcastChannel(PodcastModelObject):
|
|||
else:
|
||||
icon_size = 16
|
||||
|
||||
if services.download_status_manager.is_download_in_progress(url):
|
||||
if downloading is not None and downloading(episode):
|
||||
status_icon = util.get_tree_icon(ICON_DOWNLOADING, icon_cache=self.icon_cache, icon_size=icon_size)
|
||||
else:
|
||||
if episode.state == db.STATE_NORMAL:
|
||||
|
@ -451,7 +451,7 @@ class PodcastChannel(PodcastModelObject):
|
|||
|
||||
model.set( iter, 4, status_icon)
|
||||
|
||||
def get_tree_model(self):
|
||||
def get_tree_model(self, downloading=None):
|
||||
"""
|
||||
Return a gtk.ListStore containing episodes for this channel
|
||||
"""
|
||||
|
@ -472,7 +472,7 @@ class PodcastChannel(PodcastModelObject):
|
|||
new_iter = new_model.append((item.url, item.title, filelength,
|
||||
True, None, item.cute_pubdate(), description, util.remove_html_tags(item.description),
|
||||
'XXXXXXXXXXXXXUNUSEDXXXXXXXXXXXXXXXXXXX', item.extension()))
|
||||
self.iter_set_downloading_columns( new_model, new_iter, episode=item)
|
||||
self.iter_set_downloading_columns( new_model, new_iter, episode=item, downloading=downloading)
|
||||
urls.append(item.url)
|
||||
|
||||
self.update_save_dir_size()
|
||||
|
|
|
@ -24,6 +24,8 @@
|
|||
#
|
||||
#
|
||||
|
||||
from __future__ import with_statement
|
||||
|
||||
from gpodder.liblogger import log
|
||||
from gpodder.libgpodder import gl
|
||||
|
||||
|
@ -33,6 +35,7 @@ from gpodder import resolver
|
|||
import gtk
|
||||
import gobject
|
||||
|
||||
import collections
|
||||
import threading
|
||||
import time
|
||||
import urllib2
|
||||
|
@ -281,242 +284,76 @@ class CoverDownloader(ObservableService):
|
|||
cover_downloader = CoverDownloader()
|
||||
|
||||
|
||||
class DownloadStatusManager(ObservableService):
|
||||
COLUMN_NAMES = { 0: 'episode', 1: 'speed', 2: 'progress', 3: 'url' }
|
||||
COLUMN_TYPES = ( gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_FLOAT, gobject.TYPE_STRING )
|
||||
PROGRESS_HOLDDOWN_TIMEOUT = 5
|
||||
class DownloadStatusManager(object):
|
||||
# Types of columns, needed for creation of gtk.ListStore
|
||||
COLUMNS = (object, str, str, int, str, str, str, str, str)
|
||||
|
||||
def __init__( self):
|
||||
self.status_list = {}
|
||||
self.next_status_id = 0
|
||||
# Symbolic names for our columns, so we know what we're up to
|
||||
C_TASK, C_NAME, C_URL, C_PROGRESS, C_PROGRESS_TEXT, C_SIZE_TEXT, \
|
||||
C_ICON_NAME, C_SPEED_TEXT, C_STATUS_TEXT = range(len(COLUMNS))
|
||||
|
||||
self.last_progress_status = (0, 0)
|
||||
self.last_progress_update = 0
|
||||
|
||||
# use to correctly calculate percentage done
|
||||
self.downloads_done_bytes = 0
|
||||
|
||||
self.max_downloads = gl.config.max_downloads
|
||||
self.semaphore = threading.Semaphore( self.max_downloads)
|
||||
def __init__(self):
|
||||
self.__model = gtk.ListStore(*DownloadStatusManager.COLUMNS)
|
||||
# FIXME: do not duplicate the names here (from DownloadTask!)
|
||||
(INIT, QUEUED, DOWNLOADING, DONE, FAILED, CANCELLED, PAUSED) = range(7)
|
||||
|
||||
self.tree_model = gtk.ListStore( *self.COLUMN_TYPES)
|
||||
self.tree_model_lock = threading.Lock()
|
||||
self.status_stock_ids = collections.defaultdict(lambda: None)
|
||||
self.status_stock_ids[DOWNLOADING] = gtk.STOCK_GO_DOWN
|
||||
self.status_stock_ids[DONE] = gtk.STOCK_APPLY
|
||||
self.status_stock_ids[FAILED] = gtk.STOCK_STOP
|
||||
self.status_stock_ids[CANCELLED] = gtk.STOCK_CANCEL
|
||||
self.status_stock_ids[PAUSED] = gtk.STOCK_MEDIA_PAUSE
|
||||
|
||||
# batch add in progress?
|
||||
self.batch_mode_enabled = False
|
||||
# remember which episodes and channels changed during batch mode
|
||||
self.batch_mode_changed_episode_urls = set()
|
||||
self.batch_mode_changed_channel_urls = set()
|
||||
def get_tree_model(self):
|
||||
return self.__model
|
||||
|
||||
# Used to notify all threads that they should
|
||||
# re-check if they can acquire the lock
|
||||
self.notification_event = threading.Event()
|
||||
self.notification_event_waiters = 0
|
||||
|
||||
signal_names = ['list-changed', 'progress-changed', 'progress-detail', 'download-complete']
|
||||
ObservableService.__init__(self, signal_names)
|
||||
|
||||
def start_batch_mode(self):
|
||||
"""
|
||||
This is called when we are going to add multiple
|
||||
episodes to our download list, and do not want to
|
||||
notify the GUI for every single episode.
|
||||
|
||||
After all episodes have been added, you MUST call
|
||||
the end_batch_mode() method to trigger a notification.
|
||||
"""
|
||||
self.batch_mode_enabled = True
|
||||
|
||||
def end_batch_mode(self):
|
||||
"""
|
||||
This is called after multiple episodes have been
|
||||
added when start_batch_mode() has been called before.
|
||||
|
||||
This sends out a notification that the list has changed.
|
||||
"""
|
||||
self.batch_mode_enabled = False
|
||||
if len(self.batch_mode_changed_episode_urls) + len(self.batch_mode_changed_channel_urls) > 0:
|
||||
self.notify('list-changed', self.batch_mode_changed_episode_urls, self.batch_mode_changed_channel_urls)
|
||||
self.batch_mode_changed_episode_urls = set()
|
||||
self.batch_mode_changed_channel_urls = set()
|
||||
|
||||
def notify_progress(self, force=False):
|
||||
now = (self.count(), self.average_progress())
|
||||
|
||||
next_progress_update = self.last_progress_update + self.PROGRESS_HOLDDOWN_TIMEOUT
|
||||
|
||||
if force or (now != self.last_progress_status and \
|
||||
time.time() > next_progress_update):
|
||||
self.notify( 'progress-changed', *now)
|
||||
self.last_progress_status = now
|
||||
self.last_progress_update = time.time()
|
||||
|
||||
def s_acquire( self):
|
||||
if not gl.config.max_downloads_enabled:
|
||||
return False
|
||||
|
||||
# Acquire queue slots if user has decreased the slots
|
||||
while self.max_downloads > gl.config.max_downloads:
|
||||
self.semaphore.acquire()
|
||||
self.max_downloads -= 1
|
||||
|
||||
# Make sure we update the maximum number of downloads
|
||||
self.update_max_downloads()
|
||||
|
||||
while self.semaphore.acquire(False) == False:
|
||||
self.notification_event_waiters += 1
|
||||
self.notification_event.wait(2.)
|
||||
self.notification_event_waiters -= 1
|
||||
|
||||
# If we are the last thread that woke up from
|
||||
# the notification_event, clear the flag here
|
||||
if self.notification_event_waiters == 0:
|
||||
self.notification_event.clear()
|
||||
|
||||
# If the user has change the config option since the
|
||||
# last time we checked, return false and start download
|
||||
if not gl.config.max_downloads_enabled:
|
||||
return False
|
||||
|
||||
# If we land here, we've acquired exactly the one we need
|
||||
return True
|
||||
|
||||
def update_max_downloads(self):
|
||||
# Release queue slots if user has enabled more slots
|
||||
while self.max_downloads < gl.config.max_downloads:
|
||||
self.semaphore.release()
|
||||
self.max_downloads += 1
|
||||
|
||||
# Notify all threads that the limit might have been changed
|
||||
self.notification_event.set()
|
||||
|
||||
def s_release( self, acquired = True):
|
||||
if acquired:
|
||||
self.semaphore.release()
|
||||
|
||||
def reserve_download_id( self):
|
||||
id = self.next_status_id
|
||||
self.next_status_id = id + 1
|
||||
return id
|
||||
|
||||
def remove_iter( self, iter):
|
||||
self.tree_model.remove( iter)
|
||||
return False
|
||||
|
||||
def register_download_id( self, id, thread):
|
||||
self.tree_model_lock.acquire()
|
||||
self.status_list[id] = { 'iter': self.tree_model.append(), 'thread': thread, 'progress': 0.0, 'speed': _('Queued'), }
|
||||
if self.batch_mode_enabled:
|
||||
self.batch_mode_changed_episode_urls.add(thread.episode.url)
|
||||
self.batch_mode_changed_channel_urls.add(thread.channel.url)
|
||||
def request_update(self, iter, task=None):
|
||||
if task is None:
|
||||
# Ongoing update request from UI - get task from model
|
||||
task = self.__model.get_value(iter, self.C_TASK)
|
||||
else:
|
||||
self.notify('list-changed', [thread.episode.url], [thread.channel.url])
|
||||
self.tree_model_lock.release()
|
||||
# Initial update request - update non-changing fields
|
||||
self.__model.set(iter,
|
||||
self.C_TASK, task,
|
||||
self.C_NAME, str(task),
|
||||
self.C_URL, task.url)
|
||||
|
||||
def remove_download_id( self, id):
|
||||
if not id in self.status_list:
|
||||
return
|
||||
iter = self.status_list[id]['iter']
|
||||
if iter is not None:
|
||||
self.tree_model_lock.acquire()
|
||||
util.idle_add(self.remove_iter, iter)
|
||||
self.tree_model_lock.release()
|
||||
self.status_list[id]['iter'] = None
|
||||
episode_url = self.status_list[id]['thread'].episode.url
|
||||
channel_url = self.status_list[id]['thread'].channel.url
|
||||
self.status_list[id]['thread'].cancel()
|
||||
del self.status_list[id]
|
||||
if not self.has_items():
|
||||
# Reset the counter now
|
||||
self.downloads_done_bytes = 0
|
||||
if task.status == task.FAILED:
|
||||
status_message = _('Failed: %s') % (task.error_message,)
|
||||
else:
|
||||
episode_url = None
|
||||
channel_url = None
|
||||
status_message = task.STATUS_MESSAGE[task.status]
|
||||
|
||||
if self.batch_mode_enabled:
|
||||
self.batch_mode_changed_episode_urls.add(episode_url)
|
||||
self.batch_mode_changed_channel_urls.add(channel_url)
|
||||
if task.status == task.DOWNLOADING:
|
||||
speed_message = '%s/s' % util.format_filesize(task.speed)
|
||||
else:
|
||||
self.notify('list-changed', [episode_url], [channel_url])
|
||||
self.notify_progress(force=True)
|
||||
speed_message = ''
|
||||
|
||||
def count( self):
|
||||
return len(self.status_list)
|
||||
self.__model.set(iter,
|
||||
self.C_PROGRESS, 100.*task.progress,
|
||||
self.C_PROGRESS_TEXT, '%.0f%%' % (task.progress*100.,),
|
||||
self.C_SIZE_TEXT, util.format_filesize(task.total_size),
|
||||
self.C_ICON_NAME, self.status_stock_ids[task.status],
|
||||
self.C_SPEED_TEXT, speed_message,
|
||||
self.C_STATUS_TEXT, status_message)
|
||||
|
||||
def has_items( self):
|
||||
return self.count() > 0
|
||||
|
||||
def average_progress( self):
|
||||
if not len(self.status_list):
|
||||
return 0
|
||||
def __add_new_task(self, task):
|
||||
iter = self.__model.append()
|
||||
self.request_update(iter, task)
|
||||
|
||||
done = sum(status['progress']/100. * status['thread'].total_size for status in self.status_list.values())
|
||||
total = sum(status['thread'].total_size for status in self.status_list.values())
|
||||
if total + self.downloads_done_bytes == 0:
|
||||
return 0
|
||||
return float(done + self.downloads_done_bytes) / float(total + self.downloads_done_bytes) * 100
|
||||
def register_task(self, task):
|
||||
util.idle_add(self.__add_new_task, task)
|
||||
|
||||
def total_speed(self):
|
||||
if not len(self.status_list):
|
||||
return 0
|
||||
def pause_all_downloads(self):
|
||||
for row in self.__model:
|
||||
task = row[DownloadStatusManager.C_TASK]
|
||||
if task is not None:
|
||||
task.status = task.PAUSED
|
||||
|
||||
return sum(status['thread'].speed_value for status in self.status_list.values())
|
||||
|
||||
def update_status( self, id, **kwargs):
|
||||
if not id in self.status_list:
|
||||
return
|
||||
|
||||
iter = self.status_list[id]['iter']
|
||||
if iter:
|
||||
self.tree_model_lock.acquire()
|
||||
for ( column, key ) in self.COLUMN_NAMES.items():
|
||||
if key in kwargs:
|
||||
util.idle_add(self.tree_model.set, iter, column, kwargs[key])
|
||||
self.status_list[id][key] = kwargs[key]
|
||||
self.tree_model_lock.release()
|
||||
|
||||
if 'progress' in kwargs and 'speed' in kwargs and 'url' in self.status_list[id]:
|
||||
self.notify( 'progress-detail', self.status_list[id]['url'], kwargs['progress'], kwargs['speed'])
|
||||
|
||||
self.notify_progress()
|
||||
|
||||
def download_completed(self, id):
|
||||
if id in self.status_list:
|
||||
self.notify('download-complete', self.status_list[id]['episode'])
|
||||
self.downloads_done_bytes += self.status_list[id]['thread'].total_size
|
||||
|
||||
def request_progress_detail( self, url):
|
||||
for status in self.status_list.values():
|
||||
if 'url' in status and status['url'] == url and 'progress' in status and 'speed' in status:
|
||||
self.notify( 'progress-detail', url, status['progress'], status['speed'])
|
||||
|
||||
def is_download_in_progress( self, url):
|
||||
for element in self.status_list.keys():
|
||||
# We need this, because status_list is modified from other threads
|
||||
if element in self.status_list:
|
||||
try:
|
||||
thread = self.status_list[element]['thread']
|
||||
except:
|
||||
thread = None
|
||||
|
||||
if thread is not None and thread.url == url:
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def cancel_all(self, keep_files=False):
|
||||
for element in self.status_list:
|
||||
self.status_list[element]['iter'] = None
|
||||
self.status_list[element]['thread'].cancel(keep_files)
|
||||
# clear the tree model after cancelling
|
||||
util.idle_add(self.tree_model.clear)
|
||||
self.downloads_done_bytes = 0
|
||||
|
||||
def cancel_by_url( self, url):
|
||||
for element in self.status_list:
|
||||
thread = self.status_list[element]['thread']
|
||||
if thread is not None and thread.url == url:
|
||||
self.remove_download_id( element)
|
||||
def cancel_by_url(self, url):
|
||||
for row in self.__model:
|
||||
task = row[DownloadStatusManager.C_TASK]
|
||||
if task.url == url and task.status == task.DOWNLOADING:
|
||||
task.status = task.CANCELLED
|
||||
return True
|
||||
|
||||
return False
|
||||
|
|
|
@ -116,10 +116,7 @@ class GPodderStatusIcon(gtk.StatusIcon):
|
|||
if not pynotify.init('gPodder'):
|
||||
log('Error: unable to initialise pynotify', sender=self)
|
||||
|
||||
# Register with the download status manager
|
||||
dl_man = services.download_status_manager
|
||||
dl_man.register('progress-changed', self.__on_download_progress_changed)
|
||||
dl_man.register('download-complete', self.__on_download_complete)
|
||||
# Register with the download status manager FIXME FIXME
|
||||
|
||||
def __create_context_menu(self):
|
||||
# build and connect the popup menu
|
||||
|
|
|
@ -0,0 +1,85 @@
|
|||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# gPodder - A media aggregator and podcast client
|
||||
# Copyright (c) 2005-2009 Thomas Perl and the gPodder Team
|
||||
#
|
||||
# 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/>.
|
||||
#
|
||||
|
||||
|
||||
#
|
||||
# widgets.py -- Additional widgets for gPodder
|
||||
# Thomas Perl <thp@gpodder.org> 2009-03-31
|
||||
#
|
||||
|
||||
import gtk
|
||||
|
||||
from xml.sax import saxutils
|
||||
|
||||
class SimpleMessageArea(gtk.HBox):
|
||||
"""A simple, yellow message area. Inspired by gedit.
|
||||
|
||||
Original C source code:
|
||||
http://svn.gnome.org/viewvc/gedit/trunk/gedit/gedit-message-area.c
|
||||
"""
|
||||
def __init__(self, message):
|
||||
gtk.HBox.__init__(self, spacing=6)
|
||||
self.set_border_width(6)
|
||||
self.__in_style_set = False
|
||||
self.connect('style-set', self.__style_set)
|
||||
self.connect('expose-event', self.__expose_event)
|
||||
|
||||
self.__label = gtk.Label()
|
||||
self.__label.set_alignment(0.0, 0.5)
|
||||
self.__label.set_line_wrap(False)
|
||||
self.__label.set_markup('<b>%s</b>' % saxutils.escape(message))
|
||||
self.pack_start(self.__label, expand=True, fill=True)
|
||||
|
||||
self.__image = gtk.image_new_from_stock(gtk.STOCK_CLOSE, gtk.ICON_SIZE_BUTTON)
|
||||
self.__button = gtk.ToolButton(self.__image)
|
||||
self.__button.set_border_width(0)
|
||||
self.__button.connect('clicked', self.__button_clicked)
|
||||
vbox = gtk.VBox()
|
||||
vbox.pack_start(self.__button, expand=True, fill=False)
|
||||
self.pack_start(vbox, expand=False, fill=False)
|
||||
|
||||
def __style_set(self, widget, previous_style):
|
||||
if self.__in_style_set:
|
||||
return
|
||||
|
||||
w = gtk.Window(gtk.WINDOW_POPUP)
|
||||
w.set_name('gtk-tooltip')
|
||||
w.ensure_style()
|
||||
style = w.get_style()
|
||||
|
||||
self.__in_style_set = True
|
||||
self.set_style(style)
|
||||
self.__in_style_set = False
|
||||
|
||||
w.destroy()
|
||||
|
||||
self.queue_draw()
|
||||
|
||||
def __expose_event(self, widget, event):
|
||||
style = widget.get_style()
|
||||
rect = widget.get_allocation()
|
||||
style.paint_flat_box(widget.window, gtk.STATE_NORMAL,
|
||||
gtk.SHADOW_OUT, None, widget, "tooltip",
|
||||
rect.x, rect.y, rect.width, rect.height)
|
||||
return False
|
||||
|
||||
def __button_clicked(self, toolbutton):
|
||||
self.hide_all()
|
||||
|
Loading…
Reference in New Issue