Skip to content

Commit

Permalink
Merged and cleaned-up the device sync code (bug 1579)
Browse files Browse the repository at this point in the history
  • Loading branch information
thp committed Jul 9, 2012
1 parent 5b949ef commit 972c045
Show file tree
Hide file tree
Showing 7 changed files with 220 additions and 197 deletions.
2 changes: 1 addition & 1 deletion share/gpodder/ui/gtk/gpodder.ui
Expand Up @@ -228,7 +228,7 @@
<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>
<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"/>
Expand Down
16 changes: 10 additions & 6 deletions src/gpodder/config.py
Expand Up @@ -155,22 +155,26 @@
},
},

# Synchronization with portable devices (MP3 players, etc..)
'device_sync': {
# Settings for Device sync
'device_type': 'none', # Possible values: 'none', 'filesystem'
'device_folder': '/media',

'one_folder_per_podcast': 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,
},

'after_sync': {
'mark_episodes_played': False,
'delete_episodes': False,
'sync_disks': False,
},
},

'youtube': {
'preferred_fmt_id': 18,
Expand Down
9 changes: 5 additions & 4 deletions src/gpodder/download.py
Expand Up @@ -531,9 +531,10 @@ class DownloadTask(object):
STATUS_MESSAGE = (_('Added'), _('Queued'), _('Downloading'),
_('Finished'), _('Failed'), _('Cancelled'), _('Paused'))
(INIT, QUEUED, DOWNLOADING, DONE, FAILED, CANCELLED, PAUSED) = range(7)

(ACTIVITY_DOWNLOAD, ACTIVITY_SYNCHRONIZE) = range(2)


# Wheter this task represents a file download or a device sync operation
ACTIVITY_DOWNLOAD, ACTIVITY_SYNCHRONIZE = range(2)


def __str__(self):
return self.__episode.title
Expand Down Expand Up @@ -592,7 +593,7 @@ def removed_from_list(self):
def __init__(self, episode, config):
assert episode.download_task is None
self.__status = DownloadTask.INIT
self.__activity=DownloadTask.ACTIVITY_DOWNLOAD
self.__activity = DownloadTask.ACTIVITY_DOWNLOAD
self.__status_changed = True
self.__episode = episode
self._config = config
Expand Down
14 changes: 9 additions & 5 deletions src/gpodder/gtkui/desktop/preferences.py
Expand Up @@ -93,7 +93,7 @@ def get_index(self):
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
row[self.C_ON_SYNC_MARK_PLAYED] and not
self._config.device_sync.after_sync.delete_episodes):
return index
return 0 # Some sane default
Expand Down Expand Up @@ -375,19 +375,23 @@ def on_combobox_device_type_changed(self, widget):
if device_type == 'none':
self.btn_filesystemMountpoint.set_label('')
self.btn_filesystemMountpoint.set_sensitive(False)
elif device_type == 'filesystem': #JOSEPH: add back in ipod & mtp support
elif device_type == 'filesystem':
self.btn_filesystemMountpoint.set_label(self._config.device_sync.device_folder)
self.btn_filesystemMountpoint.set_sensitive(True)
else:
# TODO: Add support for iPod and MTP devices
pass

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 = 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()
Expand Down
54 changes: 29 additions & 25 deletions src/gpodder/gtkui/desktop/sync.py
@@ -1,7 +1,7 @@
# -*- coding: utf-8 -*-
#
# gPodder - A media aggregator and podcast client
# Copyright (c) 2005-2011 Thomas Perl and the gPodder Team
# Copyright (c) 2005-2012 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
Expand All @@ -19,6 +19,7 @@

# 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)
# Ported to gPodder 3 by Joseph Wickremasinghe in June 2012

import gtk
import threading
Expand All @@ -28,33 +29,37 @@

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,
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.device = None

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.download_status_model = download_status_model
self.download_queue_manager = download_queue_manager
self.enable_download_list_update = enable_download_list_update
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
Expand All @@ -70,7 +75,7 @@ def _filter_sync_episodes(self, channels, only_downloaded=False):
continue

for episode in channel.get_all_episodes():
if (episode.was_downloaded(and_exists=True) or
if (episode.was_downloaded(and_exists=True) or
not only_downloaded):
episodes.append(episode)
return episodes
Expand All @@ -86,7 +91,6 @@ def _show_message_cannot_open(self):
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:
Expand All @@ -95,14 +99,13 @@ def on_synchronize_episodes(self, channels, episodes=None, force_played=True):
if not device.open():
return self._show_message_cannot_open()
else:
#only set if device is configured
#and opened successfully
self.device=device
# 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):
Expand All @@ -111,7 +114,7 @@ def will_add(episode):
return False

# Might not be synced if it's played already
if (not force_played and
if (not force_played and
self._config.device_sync.skip_played_episodes):
return False

Expand Down Expand Up @@ -139,9 +142,9 @@ def file_size(episode):
device.close()
return

# Finally start the synchronization process
# Finally start the synchronization process
def sync_thread_func():
self.enable_download_list_update()
self.enable_download_list_update()
device.add_sync_tasks(episodes, force_played=force_played)

threading.Thread(target=sync_thread_func).start()
Expand All @@ -150,9 +153,9 @@ def sync_thread_func():
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
if (self._config.device_sync.delete_played_episodes and
self._config.device_sync.skip_played_episodes):
all_episodes = self._filter_sync_episodes(channels,
all_episodes = self._filter_sync_episodes(channels,
only_downloaded=False)
episodes_on_device = device.get_all_tracks()
for local_episode in all_episodes:
Expand All @@ -173,3 +176,4 @@ def cleanup_episodes():
# 2. Check for free space (in UI thread)
# 3. Sync the device (in UI thread)
threading.Thread(target=cleanup_episodes).start()

83 changes: 44 additions & 39 deletions src/gpodder/gtkui/main.py
Expand Up @@ -1096,11 +1096,13 @@ def update_downloads_list(self, can_call_cleanup=True):

download_tasks_seen.add(task)

if (status == download.DownloadTask.DOWNLOADING and activity==download.DownloadTask.ACTIVITY_DOWNLOAD):
if (status == download.DownloadTask.DOWNLOADING and
activity == download.DownloadTask.ACTIVITY_DOWNLOAD):
downloading += 1
total_speed += speed
elif (status == download.DownloadTask.DOWNLOADING and activity==download.DownloadTask.ACTIVITY_SYNCHRONIZE):
synchronizing+=1
elif (status == download.DownloadTask.DOWNLOADING and
activity == download.DownloadTask.ACTIVITY_SYNCHRONIZE):
synchronizing += 1
elif status == download.DownloadTask.FAILED:
failed += 1
elif status == download.DownloadTask.DONE:
Expand All @@ -1121,7 +1123,7 @@ def update_downloads_list(self, can_call_cleanup=True):
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})
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:
Expand Down Expand Up @@ -1356,31 +1358,34 @@ def downloads_list_get_selection(self, model=None, paths=None):
return selected_tasks, can_queue, can_cancel, can_pause, can_remove, can_force

def downloads_finished(self, download_tasks_seen):
# 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

#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)
def filter_by_activity(activity, tasks):
return filter(lambda task: task.activity == activity, tasks)

sync_tasks=filter((lambda task: type(task).__name__=='SyncTask'),download_tasks_seen)
download_tasks = filter_by_activity(download.DownloadTask.ACTIVITY_DOWNLOAD,
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()]
finished_downloads = [str(task)
for task in download_tasks if task.notify_as_finished()]
failed_downloads = ['%s (%s)' % (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)
sync_tasks = filter_by_activity(download.DownloadTask.ACTIVITY_SYNCHRONIZE,
download_tasks_seen)

finished_syncs = [task for task in sync_tasks if task.notify_as_finished()]
failed_syncs = [task for task in sync_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

if finished_downloads and failed_downloads:
message = self.format_episode_list(finished_downloads, 5)
message += '\n\n<i>%s</i>\n' % _('These downloads failed:')
message += '\n\n<i>%s</i>\n' % _('Could not download some episodes:')
message += self.format_episode_list(failed_downloads, 5)
self.show_message(message, _('Downloads finished'), True, widget=self.labelDownloads)
elif finished_downloads:
Expand All @@ -1392,7 +1397,7 @@ def downloads_finished(self, download_tasks_seen):

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 += '\n\n<i>%s</i>\n' % _('Could not sync some episodes:')
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:
Expand All @@ -1402,21 +1407,19 @@ def downloads_finished(self, download_tasks_seen):
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:
# Do post-sync processing if required
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)
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()

self.sync_ui.device.close()

#update icon list to show changes, if any
# Update icon list to show changes, if any
self.update_episode_list_icons(all=True)


Expand Down Expand Up @@ -3469,15 +3472,17 @@ def extensions_episode_download_cb(self, 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,
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.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)

Expand Down

0 comments on commit 972c045

Please sign in to comment.