Added device sync using 'Downloads' tab to show progress
This commit is contained in:
parent
f9c6a74fb6
commit
2774142f8f
|
@ -218,6 +218,21 @@
|
|||
<signal handler="on_shownotes_selected_episodes" name="activate"/>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkAction" id="menuExtras">
|
||||
<property name="name">menuExtras</property>
|
||||
<property name="label" translatable="yes">E_xtras</property>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkAction" id="item_sync">
|
||||
<property name="stock_id">gtk-refresh</property>
|
||||
<property name="name">item_sync</property>
|
||||
<property name="label" translatable="yes">Sync to Device</property>
|
||||
<signal handler="on_sync_to_device_activate" name="activate"/>
|
||||
</object>
|
||||
<accelerator key="S" modifiers="GDK_CONTROL_MASK"/>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkAction" id="menuView">
|
||||
<property name="name">menuView</property>
|
||||
|
@ -363,6 +378,9 @@
|
|||
<separator/>
|
||||
<menuitem action="item_episode_details"/>
|
||||
</menu>
|
||||
<menu action="menuExtras">
|
||||
<menuitem action="item_sync"/>
|
||||
</menu>
|
||||
<menu action="menuView">
|
||||
<menuitem action="itemShowAllEpisodes"/>
|
||||
<menuitem action="item_podcast_sections"/>
|
||||
|
@ -928,7 +946,7 @@
|
|||
<child type="tab">
|
||||
<object class="GtkLabel" id="labelDownloads">
|
||||
<property name="visible">True</property>
|
||||
<property name="label" translatable="yes">Downloads</property>
|
||||
<property name="label" translatable="yes">Tasks</property>
|
||||
<property name="use_underline">False</property>
|
||||
<property name="use_markup">False</property>
|
||||
<property name="wrap">False</property>
|
||||
|
|
|
@ -477,6 +477,122 @@
|
|||
<property name="position">3</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkVBox" id="vbox_devices">
|
||||
<property name="border_width">12</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="spacing">6</property>
|
||||
<child>
|
||||
<object class="GtkTable" id="table_devices">
|
||||
<property name="visible">True</property>
|
||||
<property name="n_rows">3</property>
|
||||
<property name="n_columns">2</property>
|
||||
<property name="column_spacing">6</property>
|
||||
<property name="row_spacing">6</property>
|
||||
<child>
|
||||
<object class="GtkLabel" id="label_device_type">
|
||||
<property name="visible">True</property>
|
||||
<property name="xalign">1</property>
|
||||
<property name="label" translatable="yes">Device type:</property>
|
||||
<property name="justify">right</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="x_options">GTK_FILL</property>
|
||||
<property name="y_options"></property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkComboBox" id="combobox_device_type">
|
||||
<property name="visible">True</property>
|
||||
<signal name="changed" handler="on_combobox_device_type_changed"/>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="left_attach">1</property>
|
||||
<property name="right_attach">2</property>
|
||||
<property name="y_options"></property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkLabel" id="label_device_mount">
|
||||
<property name="visible">True</property>
|
||||
<property name="xalign">1</property>
|
||||
<property name="label" translatable="yes">Mountpoint:</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="top_attach">1</property>
|
||||
<property name="bottom_attach">2</property>
|
||||
<property name="x_options">GTK_FILL</property>
|
||||
<property name="y_options"></property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkButton" id="btn_filesystemMountpoint">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="receives_default">False</property>
|
||||
<property name="label" translatable="no"></property>
|
||||
<signal name="clicked" handler="on_btn_device_mountpoint_clicked"/>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="left_attach">1</property>
|
||||
<property name="right_attach">2</property>
|
||||
<property name="top_attach">1</property>
|
||||
<property name="bottom_attach">2</property>
|
||||
<property name="y_options"></property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkLabel" id="label_on_sync">
|
||||
<property name="visible">True</property>
|
||||
<property name="xalign">1</property>
|
||||
<property name="label" translatable="yes">After syncing an episode:</property>
|
||||
<property name="justify">right</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="x_options">GTK_FILL</property>
|
||||
<property name="y_options"></property>
|
||||
<property name="top_attach">2</property>
|
||||
<property name="bottom_attach">3</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkComboBox" id="combobox_on_sync">
|
||||
<property name="visible">True</property>
|
||||
<signal name="changed" handler="on_combobox_on_sync_changed"/>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="left_attach">1</property>
|
||||
<property name="right_attach">2</property>
|
||||
<property name="y_options"></property>
|
||||
<property name="top_attach">2</property>
|
||||
<property name="bottom_attach">3</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkCheckButton" id="checkbutton_skip_played_episodes">
|
||||
<property name="label" translatable="yes">Only sync unplayed episodes</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="receives_default">False</property>
|
||||
<property name="draw_indicator">True</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="position">3</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="tab-label" translatable="yes">Devices</property>
|
||||
<property name="position">4</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="position">2</property>
|
||||
|
|
|
@ -155,6 +155,23 @@ defaults = {
|
|||
},
|
||||
},
|
||||
|
||||
'device_sync': {
|
||||
# Settings for Device sync
|
||||
'device_type': 'None',
|
||||
'device_folder': '/media',
|
||||
'one_folder_per_podcasts': True,
|
||||
'skip_played_episodes': True,
|
||||
'delete_played_episodes': False,
|
||||
'max_filename_length': 999,
|
||||
'custom_sync_name': '{episode.pubdate_prop}_{episode.title}',
|
||||
'custom_sync_name_enabled': False,
|
||||
'after_sync': {
|
||||
'mark_episodes_played': False,
|
||||
'delete_episodes': False,
|
||||
'sync_disks': False,
|
||||
},
|
||||
},
|
||||
|
||||
'youtube': {
|
||||
'preferred_fmt_id': 18,
|
||||
},
|
||||
|
|
|
@ -57,6 +57,52 @@ class NewEpisodeActionList(gtk.ListStore):
|
|||
def set_index(self, index):
|
||||
self._config.auto_download = self[index][self.C_AUTO_DOWNLOAD]
|
||||
|
||||
class DeviceTypeActionList(gtk.ListStore):
|
||||
C_CAPTION, C_DEVICE_TYPE = range(2)
|
||||
|
||||
def __init__(self, config):
|
||||
gtk.ListStore.__init__(self, str, str)
|
||||
self._config = config
|
||||
self.append((_('None'), 'none'))
|
||||
self.append((_('Filesystem-based'), 'filesystem'))
|
||||
|
||||
def get_index(self):
|
||||
for index, row in enumerate(self):
|
||||
if self._config.device_sync.device_type == row[self.C_DEVICE_TYPE]:
|
||||
return index
|
||||
return 0 # Some sane default
|
||||
|
||||
def set_index(self, index):
|
||||
self._config.device_sync.device_type = self[index][self.C_DEVICE_TYPE]
|
||||
|
||||
|
||||
class OnSyncActionList(gtk.ListStore):
|
||||
C_CAPTION, C_ON_SYNC_DELETE, C_ON_SYNC_MARK_PLAYED = range(3)
|
||||
ACTION_NONE, ACTION_ASK, ACTION_MINIMIZED, ACTION_ALWAYS = range(4)
|
||||
|
||||
def __init__(self, config):
|
||||
gtk.ListStore.__init__(self, str, bool, bool)
|
||||
self._config = config
|
||||
self.append((_('Do nothing'), False, False))
|
||||
self.append((_('Mark it as played'), False, True))
|
||||
self.append((_('Delete it from gPodder'), True, False))
|
||||
|
||||
def get_index(self):
|
||||
for index, row in enumerate(self):
|
||||
if (self._config.device_sync.after_sync.delete_episodes and
|
||||
row[self.C_ON_SYNC_DELETE]):
|
||||
return index
|
||||
if (self._config.device_sync.after_sync.mark_episodes_played and
|
||||
row[self.C_ON_SYNC_MARK_PLAYED] and not
|
||||
self._config.device_sync.after_sync.delete_episodes):
|
||||
return index
|
||||
return 0 # Some sane default
|
||||
|
||||
def set_index(self, index):
|
||||
self._config.device_sync.after_sync.delete_episodes = self[index][self.C_ON_SYNC_DELETE]
|
||||
self._config.device_sync.after_sync.mark_episodes_played = self[index][self.C_ON_SYNC_MARK_PLAYED]
|
||||
|
||||
|
||||
|
||||
class gPodderPreferences(BuilderWidget):
|
||||
C_TOGGLE, C_LABEL, C_EXTENSION = range(3)
|
||||
|
@ -119,6 +165,23 @@ class gPodderPreferences(BuilderWidget):
|
|||
self._config.connect_gtk_togglebutton('auto_remove_unplayed_episodes', self.checkbutton_expiration_unplayed)
|
||||
self._config.connect_gtk_togglebutton('auto_remove_unfinished_episodes', self.checkbutton_expiration_unfinished)
|
||||
|
||||
self.device_type_model = DeviceTypeActionList(self._config)
|
||||
self.combobox_device_type.set_model(self.device_type_model)
|
||||
cellrenderer = gtk.CellRendererText()
|
||||
self.combobox_device_type.pack_start(cellrenderer, True)
|
||||
self.combobox_device_type.add_attribute(cellrenderer, 'text', DeviceTypeActionList.C_CAPTION)
|
||||
self.combobox_device_type.set_active(self.device_type_model.get_index())
|
||||
|
||||
self.on_sync_model = OnSyncActionList(self._config)
|
||||
self.combobox_on_sync.set_model(self.on_sync_model)
|
||||
cellrenderer = gtk.CellRendererText()
|
||||
self.combobox_on_sync.pack_start(cellrenderer, True)
|
||||
self.combobox_on_sync.add_attribute(cellrenderer, 'text', OnSyncActionList.C_CAPTION)
|
||||
self.combobox_on_sync.set_active(self.on_sync_model.get_index())
|
||||
|
||||
self._config.connect_gtk_togglebutton('device_sync.skip_played_episodes', self.checkbutton_skip_played_episodes)
|
||||
|
||||
|
||||
# Have to do this before calling set_active on checkbutton_enable
|
||||
self._enable_mygpo = self._config.mygpo.enabled
|
||||
|
||||
|
@ -301,3 +364,37 @@ class gPodderPreferences(BuilderWidget):
|
|||
self._config.mygpo.enabled = False
|
||||
threading.Thread(target=thread_proc).start()
|
||||
|
||||
def on_combobox_on_sync_changed(self, widget):
|
||||
index = self.combobox_on_sync.get_active()
|
||||
self.on_sync_model.set_index(index)
|
||||
|
||||
def on_combobox_device_type_changed(self, widget):
|
||||
index = self.combobox_device_type.get_active()
|
||||
self.device_type_model.set_index(index)
|
||||
if index == 0:
|
||||
self.btn_filesystemMountpoint.set_label('')
|
||||
self.btn_filesystemMountpoint.set_sensitive(False)
|
||||
if index == 1: #JOSEPH: add back in ipod & mtp support
|
||||
self.btn_filesystemMountpoint.set_label(self._config.device_sync.device_folder)
|
||||
self.btn_filesystemMountpoint.set_sensitive(True)
|
||||
|
||||
children = self.btn_filesystemMountpoint.get_children()
|
||||
if children:
|
||||
label = children.pop()
|
||||
label.set_alignment(0., .5)
|
||||
|
||||
def on_btn_device_mountpoint_clicked(self, widget):
|
||||
fs = gtk.FileChooserDialog( title = _('Select folder for mount point'), action = gtk.FILE_CHOOSER_ACTION_SELECT_FOLDER)
|
||||
fs.add_button( gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL)
|
||||
fs.add_button( gtk.STOCK_OPEN, gtk.RESPONSE_OK)
|
||||
fs.set_current_folder(self.btn_filesystemMountpoint.get_label())
|
||||
if fs.run() == gtk.RESPONSE_OK:
|
||||
filename = fs.get_filename()
|
||||
if self._config.device_sync.device_type == 'filesystem':
|
||||
self._config.device_sync.device_folder = filename
|
||||
|
||||
# Request an update of the mountpoint button
|
||||
self.on_combobox_device_type_changed(None)
|
||||
|
||||
fs.destroy()
|
||||
|
||||
|
|
|
@ -0,0 +1,175 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# gPodder - A media aggregator and podcast client
|
||||
# Copyright (c) 2005-2011 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/>.
|
||||
#
|
||||
|
||||
# gpodder.gtkui.desktop.sync - Glue code between GTK+ UI and sync module
|
||||
# Thomas Perl <thp@gpodder.org>; 2009-09-05 (based on code from gui.py)
|
||||
|
||||
import gtk
|
||||
import threading
|
||||
import gpodder
|
||||
|
||||
_ = gpodder.gettext
|
||||
|
||||
from gpodder import util
|
||||
from gpodder import sync
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class gPodderSyncUI(object):
|
||||
def __init__(self, config, notification,
|
||||
parent_window, show_confirmation,
|
||||
update_episode_list_icons,
|
||||
update_podcast_list_model,
|
||||
preferences_widget,
|
||||
episode_selector_class, download_status_model,
|
||||
download_queue_manager,
|
||||
enable_download_list_update,
|
||||
commit_changes_to_database):
|
||||
self._config = config
|
||||
self.notification = notification
|
||||
self.parent_window = parent_window
|
||||
self.show_confirmation = show_confirmation
|
||||
self.update_episode_list_icons = update_episode_list_icons
|
||||
self.update_podcast_list_model = update_podcast_list_model
|
||||
self.preferences_widget = preferences_widget
|
||||
self.episode_selector_class = episode_selector_class
|
||||
self.commit_changes_to_database = commit_changes_to_database
|
||||
self.download_status_model=download_status_model
|
||||
self.download_queue_manager=download_queue_manager
|
||||
self.enable_download_list_update=enable_download_list_update
|
||||
self.device=None
|
||||
|
||||
|
||||
def _filter_sync_episodes(self, channels, only_downloaded=False):
|
||||
"""Return a list of episodes for device synchronization
|
||||
|
||||
If only_downloaded is True, this will skip episodes that
|
||||
have not been downloaded yet and podcasts that are marked
|
||||
as "Do not synchronize to my device".
|
||||
"""
|
||||
episodes = []
|
||||
for channel in channels:
|
||||
if only_downloaded:
|
||||
logger.info('Skipping channel: %s', channel.title)
|
||||
continue
|
||||
|
||||
for episode in channel.get_all_episodes():
|
||||
if (episode.was_downloaded(and_exists=True) or
|
||||
not only_downloaded):
|
||||
episodes.append(episode)
|
||||
return episodes
|
||||
|
||||
def _show_message_unconfigured(self):
|
||||
title = _('No device configured')
|
||||
message = _('Please set up your device in the preferences dialog.')
|
||||
self.notification(message, title, widget=self.preferences_widget)
|
||||
|
||||
def _show_message_cannot_open(self):
|
||||
title = _('Cannot open device')
|
||||
message = _('Please check the settings in the preferences dialog.')
|
||||
self.notification(message, title, widget=self.preferences_widget)
|
||||
|
||||
def on_synchronize_episodes(self, channels, episodes=None, force_played=True):
|
||||
|
||||
device = sync.open_device(self)
|
||||
|
||||
if device is None:
|
||||
return self._show_message_unconfigured()
|
||||
|
||||
if not device.open():
|
||||
return self._show_message_cannot_open()
|
||||
else:
|
||||
#only set if device is configured
|
||||
#and opened successfully
|
||||
self.device=device
|
||||
|
||||
if episodes is None:
|
||||
force_played = False
|
||||
episodes = self._filter_sync_episodes(channels)
|
||||
|
||||
def check_free_space():
|
||||
# "Will we add this episode to the device?"
|
||||
def will_add(episode):
|
||||
# If already on-device, it won't take up any space
|
||||
if device.episode_on_device(episode):
|
||||
return False
|
||||
|
||||
# Might not be synced if it's played already
|
||||
if (not force_played and
|
||||
self._config.device_sync.skip_played_episodes):
|
||||
return False
|
||||
|
||||
# In all other cases, we expect the episode to be
|
||||
# synchronized to the device, so "answer" positive
|
||||
return True
|
||||
|
||||
# "What is the file size of this episode?"
|
||||
def file_size(episode):
|
||||
filename = episode.local_filename(create=False)
|
||||
if filename is None:
|
||||
return 0
|
||||
return util.calculate_size(str(filename))
|
||||
|
||||
# Calculate total size of sync and free space on device
|
||||
total_size = sum(file_size(e) for e in episodes if will_add(e))
|
||||
free_space = max(device.get_free_space(), 0)
|
||||
|
||||
if total_size > free_space:
|
||||
title = _('Not enough space left on device')
|
||||
message = _('You need to free up %s.\nDo you want to continue?') \
|
||||
% (util.format_filesize(total_size-free_space),)
|
||||
if not self.show_confirmation(message, title):
|
||||
device.cancel()
|
||||
device.close()
|
||||
return
|
||||
|
||||
# Finally start the synchronization process
|
||||
def sync_thread_func():
|
||||
self.enable_download_list_update()
|
||||
device.add_sync_tasks(episodes, force_played=force_played)
|
||||
|
||||
threading.Thread(target=sync_thread_func).start()
|
||||
|
||||
# This function is used to remove files from the device
|
||||
def cleanup_episodes():
|
||||
# 'skip_played_episodes' must be used or else all the
|
||||
# played tracks will be copied then immediately deleted
|
||||
if (self._config.device_sync.delete_played_episodes and
|
||||
self._config.device_sync.skip_played_episodes):
|
||||
all_episodes = self._filter_sync_episodes(channels,
|
||||
only_downloaded=False)
|
||||
episodes_on_device = device.get_all_tracks()
|
||||
for local_episode in all_episodes:
|
||||
episode = device.episode_on_device(local_episode)
|
||||
if episode is None:
|
||||
continue
|
||||
|
||||
if local_episode.state == gpodder.STATE_DELETED:
|
||||
logger.info('Removing episode from device: %s',
|
||||
episode.title)
|
||||
device.remove_track(episode)
|
||||
|
||||
# When this is done, start the callback in the UI code
|
||||
util.idle_add(check_free_space)
|
||||
|
||||
# This will run the following chain of actions:
|
||||
# 1. Remove old episodes (in worker thread)
|
||||
# 2. Check for free space (in UI thread)
|
||||
# 3. Sync the device (in UI thread)
|
||||
threading.Thread(target=cleanup_episodes).start()
|
|
@ -83,6 +83,8 @@ from gpodder.gtkui.desktop.episodeselector import gPodderEpisodeSelector
|
|||
from gpodder.gtkui.desktop.podcastdirectory import gPodderPodcastDirectory
|
||||
from gpodder.gtkui.interface.progress import ProgressIndicator
|
||||
|
||||
from gpodder.gtkui.desktop.sync import gPodderSyncUI
|
||||
|
||||
from gpodder.dbusproxy import DBusPodcastsProxy
|
||||
from gpodder import extensions
|
||||
|
||||
|
@ -985,7 +987,7 @@ class gPodder(BuilderWidget, dbus.service.Object):
|
|||
else:
|
||||
text = _('No subscriptions')
|
||||
elif role == TreeViewHelper.ROLE_DOWNLOADS:
|
||||
text = _('No active downloads')
|
||||
text = _('No active tasks')
|
||||
else:
|
||||
raise Exception('on_treeview_expose_event: unknown role')
|
||||
|
||||
|
@ -1056,7 +1058,7 @@ class gPodder(BuilderWidget, dbus.service.Object):
|
|||
try:
|
||||
model = self.download_status_model
|
||||
|
||||
downloading, failed, finished, queued, paused, others = 0, 0, 0, 0, 0, 0
|
||||
downloading, synchronizing, failed, finished, queued, paused, others = 0, 0, 0, 0, 0, 0, 0
|
||||
total_speed, total_size, done_size = 0, 0, 0
|
||||
|
||||
# Keep a list of all download tasks that we've seen
|
||||
|
@ -1079,7 +1081,7 @@ class gPodder(BuilderWidget, dbus.service.Object):
|
|||
self.download_status_model.request_update(row.iter)
|
||||
|
||||
task = row[self.download_status_model.C_TASK]
|
||||
speed, size, status, progress = task.speed, task.total_size, task.status, task.progress
|
||||
speed, size, status, progress, activity = task.speed, task.total_size, task.status, task.progress, type(task).__name__
|
||||
|
||||
# Let the download task monitors know of changes
|
||||
for monitor in self.download_task_monitors:
|
||||
|
@ -1094,9 +1096,11 @@ class gPodder(BuilderWidget, dbus.service.Object):
|
|||
|
||||
download_tasks_seen.add(task)
|
||||
|
||||
if status == download.DownloadTask.DOWNLOADING:
|
||||
if (status == download.DownloadTask.DOWNLOADING and activity=='DownloadTask'):
|
||||
downloading += 1
|
||||
total_speed += speed
|
||||
elif (status == download.DownloadTask.DOWNLOADING and activity=='SyncTask'):
|
||||
synchronizing+=1
|
||||
elif status == download.DownloadTask.FAILED:
|
||||
failed += 1
|
||||
elif status == download.DownloadTask.DONE:
|
||||
|
@ -1111,11 +1115,13 @@ class gPodder(BuilderWidget, dbus.service.Object):
|
|||
# Remember which tasks we have seen after this run
|
||||
self.download_tasks_seen = download_tasks_seen
|
||||
|
||||
text = [_('Downloads')]
|
||||
if downloading + failed + queued > 0:
|
||||
text = [_('Tasks')]
|
||||
if downloading + failed + queued + synchronizing > 0:
|
||||
s = []
|
||||
if downloading > 0:
|
||||
s.append(N_('%(count)d active', '%(count)d active', downloading) % {'count':downloading})
|
||||
if synchronizing > 0:
|
||||
s.append(N_('%(count)d active', '%(count)d active', synchronizing) % {'count':synchronizing})
|
||||
if failed > 0:
|
||||
s.append(N_('%(count)d failed', '%(count)d failed', failed) % {'count':failed})
|
||||
if queued > 0:
|
||||
|
@ -1131,9 +1137,9 @@ class gPodder(BuilderWidget, dbus.service.Object):
|
|||
self.download_tasks_seen if task.status_changed]
|
||||
episode_urls = [task.url for task in self.download_tasks_seen]
|
||||
|
||||
count = downloading + queued
|
||||
if count > 0:
|
||||
title.append(N_('downloading %(count)d file', 'downloading %(count)d files', count) % {'count':count})
|
||||
|
||||
if downloading > 0:
|
||||
title.append(N_('downloading %(count)d file', 'downloading %(count)d files', downloading) % {'count':downloading})
|
||||
|
||||
if total_size > 0:
|
||||
percentage = 100.0*done_size/total_size
|
||||
|
@ -1142,11 +1148,15 @@ class gPodder(BuilderWidget, dbus.service.Object):
|
|||
self.set_download_progress(percentage/100.)
|
||||
total_speed = util.format_filesize(total_speed)
|
||||
title[1] += ' (%d%%, %s/s)' % (percentage, total_speed)
|
||||
else:
|
||||
if synchronizing > 0:
|
||||
title.append(N_('synchronizing %(count)d file', 'synchronizing %(count)d files', synchronizing) % {'count':synchronizing})
|
||||
if queued > 0:
|
||||
title.append(N_('%(queued)d task queued', '%(queued)d tasks queued', queued) % {'queued':queued})
|
||||
if (downloading + synchronizing + queued)==0:
|
||||
self.set_download_progress(1.)
|
||||
self.downloads_finished(self.download_tasks_seen)
|
||||
gpodder.user_extensions.on_all_episodes_downloaded()
|
||||
logger.info('All downloads have finished.')
|
||||
logger.info('All tasks have finished.')
|
||||
|
||||
# Remove finished episodes
|
||||
if self.config.auto_cleanup_downloads and can_call_cleanup:
|
||||
|
@ -1346,8 +1356,27 @@ class gPodder(BuilderWidget, dbus.service.Object):
|
|||
return selected_tasks, can_queue, can_cancel, can_pause, can_remove, can_force
|
||||
|
||||
def downloads_finished(self, download_tasks_seen):
|
||||
finished_downloads = [str(task) for task in download_tasks_seen if task.notify_as_finished()]
|
||||
failed_downloads = [str(task)+' ('+task.error_message+')' for task in download_tasks_seen if task.notify_as_failed()]
|
||||
|
||||
#separate tasks into downloads & syncs
|
||||
#since calling notify_as_finished or notify_as_failed clears the flag,
|
||||
#need to iterate through downloads & syncs separately, else all sync
|
||||
#tasks will have their flags cleared if we do downloads first
|
||||
|
||||
download_tasks=filter((lambda task: type(task).__name__=='DownloadTask'),download_tasks_seen)
|
||||
|
||||
sync_tasks=filter((lambda task: type(task).__name__=='SyncTask'),download_tasks_seen)
|
||||
|
||||
finished_downloads = [str(task) for task in download_tasks if
|
||||
task.notify_as_finished()]
|
||||
failed_downloads = [str(task)+' ('+task.error_message+')'
|
||||
for task in download_tasks if
|
||||
task.notify_as_failed()]
|
||||
|
||||
#Note that 'finished_ / failed_downloads' is a list of strings
|
||||
#Whereas 'finished_ / failed_syncs' is a list of SyncTask objects
|
||||
finished_syncs=filter((lambda task: task.notify_as_finished()),sync_tasks)
|
||||
|
||||
failed_syncs=filter((lambda task: task.notify_as_failed()),sync_tasks)
|
||||
|
||||
if finished_downloads and failed_downloads:
|
||||
message = self.format_episode_list(finished_downloads, 5)
|
||||
|
@ -1361,6 +1390,35 @@ class gPodder(BuilderWidget, dbus.service.Object):
|
|||
message = self.format_episode_list(failed_downloads)
|
||||
self.show_message(message, _('Downloads failed'), True, widget=self.labelDownloads)
|
||||
|
||||
if finished_syncs and failed_syncs:
|
||||
message = self.format_episode_list(map((lambda task: str(task)),finished_syncs), 5)
|
||||
message += '\n\n<i>%s</i>\n' % _('These syncs failed:')
|
||||
message += self.format_episode_list(map((lambda task: str(task)),failed_syncs), 5)
|
||||
self.show_message(message, _('Device synchronization finished'), True, widget=self.labelDownloads)
|
||||
elif finished_syncs:
|
||||
message = self.format_episode_list(map((lambda task: str(task)),finished_syncs))
|
||||
self.show_message(message, _('Device synchronization finished'), widget=self.labelDownloads)
|
||||
elif failed_syncs:
|
||||
message = self.format_episode_list(map((lambda task: str(task)),failed_syncs))
|
||||
self.show_message(message, _('Device synchronization failed'), True, widget=self.labelDownloads)
|
||||
|
||||
|
||||
#do post sync processing if appropriate
|
||||
for task in finished_syncs+failed_syncs:
|
||||
if self.config.device_sync.after_sync.mark_episodes_played:
|
||||
logger.info('Marking as played on transfer: %s', task.episode.url)
|
||||
task.episode.mark(is_played=True)
|
||||
|
||||
if self.config.device_sync.after_sync.delete_episodes:
|
||||
logger.info('Removing episode after transfer: %s', task.episode.url)
|
||||
task.episode.delete_from_disk()
|
||||
|
||||
self.sync_ui.device.close()
|
||||
|
||||
|
||||
#update icon list to show changes, if any
|
||||
self.update_episode_list_icons(all=True)
|
||||
|
||||
|
||||
def format_episode_list(self, episode_list, max_episodes=10):
|
||||
"""
|
||||
|
@ -3410,6 +3468,19 @@ class gPodder(BuilderWidget, dbus.service.Object):
|
|||
logger.debug('extension_episode_download_cb(%s)', episode)
|
||||
self.download_episode_list(episodes=[episode])
|
||||
|
||||
def on_sync_to_device_activate(self, widget, episodes=None, force_played=True):
|
||||
self.sync_ui = gPodderSyncUI(self.config, self.notification,
|
||||
self.main_window, self.show_confirmation,
|
||||
self.update_episode_list_icons,
|
||||
self.update_podcast_list_model, self.toolPreferences,
|
||||
gPodderEpisodeSelector,
|
||||
self.download_status_model,self.download_queue_manager,
|
||||
self.enable_download_list_update,
|
||||
self.commit_changes_to_database
|
||||
)
|
||||
|
||||
self.sync_ui.on_synchronize_episodes(self.channels, episodes, force_played)
|
||||
|
||||
def main(options=None):
|
||||
gobject.threads_init()
|
||||
gobject.set_application_name('gPodder')
|
||||
|
|
|
@ -667,8 +667,12 @@ class PodcastEpisode(PodcastModelObject):
|
|||
return False
|
||||
return True
|
||||
|
||||
def sync_filename(self):
|
||||
return self.title
|
||||
def sync_filename(self, use_custom=False, custom_format=None):
|
||||
if use_custom:
|
||||
return util.object_string_formatter(custom_format,
|
||||
episode=self, podcast=self.channel)
|
||||
else:
|
||||
return self.title
|
||||
|
||||
def file_type(self):
|
||||
# Assume all YouTube/Vimeo links are video files
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -305,6 +305,13 @@ def username_password_from_url(url):
|
|||
|
||||
return (username, password)
|
||||
|
||||
def directory_is_writable(path):
|
||||
"""
|
||||
Returns True if the specified directory exists and is writable
|
||||
by the current user.
|
||||
"""
|
||||
return os.path.isdir(path) and os.access(path, os.W_OK)
|
||||
|
||||
|
||||
def calculate_size( path):
|
||||
"""
|
||||
|
@ -371,6 +378,20 @@ def file_age_in_days(filename):
|
|||
else:
|
||||
return (datetime.datetime.now()-dt).days
|
||||
|
||||
def file_modification_timestamp(filename):
|
||||
"""
|
||||
Returns the modification date of the specified file as a number
|
||||
or -1 if the modification date cannot be determined.
|
||||
"""
|
||||
if filename is None:
|
||||
return -1
|
||||
try:
|
||||
s = os.stat(filename)
|
||||
return s[stat.ST_MTIME]
|
||||
except:
|
||||
logger.warn('Cannot get modification timestamp for %s', filename)
|
||||
return -1
|
||||
|
||||
|
||||
def file_age_to_string(days):
|
||||
"""
|
||||
|
|
Loading…
Reference in New Issue