1
# -*- coding: utf-8 -*-
3
# gPodder - A media aggregator and podcast client
4
# Copyright (c) 2005-2011 Thomas Perl and the gPodder Team
6
# gPodder is free software; you can redistribute it and/or modify
7
# it under the terms of the GNU General Public License as published by
8
# the Free Software Foundation; either version 3 of the License, or
9
# (at your option) any later version.
11
# gPodder is distributed in the hope that it will be useful,
12
# but WITHOUT ANY WARRANTY; without even the implied warranty of
13
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14
# GNU General Public License for more details.
16
# You should have received a copy of the GNU General Public License
17
# along with this program. If not, see <http://www.gnu.org/licenses/>.
37
from xml.sax import saxutils
47
# Mock the required D-Bus interfaces with no-ops (ugly? maybe.)
50
def __init__(self, *args, **kwargs):
52
def add_signal_receiver(self, *args, **kwargs):
56
def __init__(self, *args, **kwargs):
60
def method(*args, **kwargs):
63
def __init__(self, *args, **kwargs):
66
def __init__(self, *args, **kwargs):
70
from gpodder import feedcore
71
from gpodder import util
72
from gpodder import opml
73
from gpodder import download
74
from gpodder import my
75
from gpodder import youtube
76
from gpodder import player
77
from gpodder.liblogger import log
82
from gpodder.model import PodcastChannel
83
from gpodder.model import PodcastEpisode
84
from gpodder.dbsqlite import Database
86
from gpodder.gtkui.model import PodcastListModel
87
from gpodder.gtkui.model import EpisodeListModel
88
from gpodder.gtkui.config import UIConfig
89
from gpodder.gtkui.services import CoverDownloader
90
from gpodder.gtkui.widgets import SimpleMessageArea
91
from gpodder.gtkui.desktopfile import UserAppsReader
93
from gpodder.gtkui.draw import draw_text_box_centered
95
from gpodder.gtkui.interface.common import BuilderWidget
96
from gpodder.gtkui.interface.common import TreeViewHelper
97
from gpodder.gtkui.interface.addpodcast import gPodderAddPodcast
99
if gpodder.ui.desktop:
100
from gpodder.gtkui.download import DownloadStatusModel
102
from gpodder.gtkui.desktop.sync import gPodderSyncUI
104
from gpodder.gtkui.desktop.channel import gPodderChannel
105
from gpodder.gtkui.desktop.preferences import gPodderPreferences
106
from gpodder.gtkui.desktop.shownotes import gPodderShownotes
107
from gpodder.gtkui.desktop.episodeselector import gPodderEpisodeSelector
108
from gpodder.gtkui.desktop.podcastdirectory import gPodderPodcastDirectory
109
from gpodder.gtkui.desktop.dependencymanager import gPodderDependencyManager
110
from gpodder.gtkui.interface.progress import ProgressIndicator
112
from gpodder.gtkui.desktop.trayicon import GPodderStatusIcon
114
except Exception, exc:
115
log('Warning: Could not import gpodder.trayicon.', traceback=True)
116
log('Warning: This probably means your PyGTK installation is too old!')
117
have_trayicon = False
118
elif gpodder.ui.diablo:
119
from gpodder.gtkui.download import DownloadStatusModel
121
from gpodder.gtkui.maemo.channel import gPodderChannel
122
from gpodder.gtkui.maemo.preferences import gPodderPreferences
123
from gpodder.gtkui.maemo.shownotes import gPodderShownotes
124
from gpodder.gtkui.maemo.episodeselector import gPodderEpisodeSelector
125
from gpodder.gtkui.maemo.podcastdirectory import gPodderPodcastDirectory
126
from gpodder.gtkui.maemo.mygpodder import MygPodderSettings
127
from gpodder.gtkui.interface.progress import ProgressIndicator
128
have_trayicon = False
129
elif gpodder.ui.fremantle:
130
from gpodder.gtkui.frmntl.model import DownloadStatusModel
131
from gpodder.gtkui.frmntl.model import EpisodeListModel
132
from gpodder.gtkui.frmntl.model import PodcastListModel
134
from gpodder.gtkui.maemo.channel import gPodderChannel
135
from gpodder.gtkui.frmntl.preferences import gPodderPreferences
136
from gpodder.gtkui.frmntl.shownotes import gPodderShownotes
137
from gpodder.gtkui.frmntl.episodeselector import gPodderEpisodeSelector
138
from gpodder.gtkui.frmntl.podcastdirectory import gPodderPodcastDirectory
139
from gpodder.gtkui.frmntl.episodes import gPodderEpisodes
140
from gpodder.gtkui.frmntl.downloads import gPodderDownloads
141
from gpodder.gtkui.frmntl.progress import ProgressIndicator
142
from gpodder.gtkui.frmntl.widgets import FancyProgressBar
143
have_trayicon = False
145
from gpodder.gtkui.frmntl.portrait import FremantleRotation
146
from gpodder.gtkui.frmntl.mafw import MafwPlaybackMonitor
147
from gpodder.gtkui.frmntl.hints import HINT_STRINGS
148
from gpodder.gtkui.frmntl.network import NetworkManager
150
from gpodder.gtkui.interface.common import Orientation
152
from gpodder.gtkui.interface.welcome import gPodderWelcome
157
from gpodder.dbusproxy import DBusPodcastsProxy
158
from gpodder import hooks
160
class gPodder(BuilderWidget, dbus.service.Object):
161
finger_friendly_widgets = ['btnCleanUpDownloads', 'button_search_episodes_clear', 'label2', 'labelDownloads', 'btnUpdateFeeds']
163
ICON_GENERAL_ADD = 'general_add'
164
ICON_GENERAL_REFRESH = 'general_refresh'
166
# Delay until live search is started after typing stop
167
LIVE_SEARCH_DELAY = 200
169
def __init__(self, bus_name, config):
170
dbus.service.Object.__init__(self, object_path=gpodder.dbus_gui_object_path, bus_name=bus_name)
171
self.podcasts_proxy = DBusPodcastsProxy(lambda: self.channels, \
172
self.on_itemUpdate_activate, \
173
self.playback_episodes, \
174
self.download_episode_list, \
175
self.episode_object_by_uri, \
177
self.db = Database(gpodder.database_file)
179
BuilderWidget.__init__(self, None)
182
if gpodder.ui.diablo:
184
self.app = hildon.Program()
185
self.app.add_window(self.main_window)
186
self.main_window.add_toolbar(self.toolbar)
188
for child in self.main_menu.get_children():
190
self.main_window.set_menu(self.set_finger_friendly(menu))
191
self._last_orientation = Orientation.LANDSCAPE
192
elif gpodder.ui.fremantle:
194
self.app = hildon.Program()
195
self.app.add_window(self.main_window)
197
appmenu = hildon.AppMenu()
199
for filter in (self.item_view_podcasts_all, \
200
self.item_view_podcasts_downloaded, \
201
self.item_view_podcasts_unplayed):
202
button = gtk.ToggleButton()
203
filter.connect_proxy(button)
204
appmenu.add_filter(button)
206
for action in (self.itemPreferences, \
207
self.item_downloads, \
208
self.itemRemoveOldEpisodes, \
209
self.item_unsubscribe, \
211
button = hildon.Button(gtk.HILDON_SIZE_AUTO,\
212
hildon.BUTTON_ARRANGEMENT_HORIZONTAL)
213
action.connect_proxy(button)
214
if action == self.item_downloads:
215
button.set_title(_('Downloads'))
216
button.set_value(_('Idle'))
217
self.button_downloads = button
218
appmenu.append(button)
220
def show_hint(button):
221
self.show_message(random.choice(HINT_STRINGS), important=True)
223
button = hildon.Button(gtk.HILDON_SIZE_AUTO,\
224
hildon.BUTTON_ARRANGEMENT_HORIZONTAL)
225
button.set_title(_('Hint of the day'))
226
button.connect('clicked', show_hint)
227
appmenu.append(button)
230
self.main_window.set_app_menu(appmenu)
232
# Initialize portrait mode / rotation manager
233
self._fremantle_rotation = FremantleRotation('gPodder', \
235
gpodder.__version__, \
236
self.config.rotation_mode)
238
# Initialize the Fremantle network manager
239
self.network_manager = NetworkManager()
241
if self.config.rotation_mode == FremantleRotation.ALWAYS:
242
util.idle_add(self.on_window_orientation_changed, \
243
Orientation.PORTRAIT)
244
self._last_orientation = Orientation.PORTRAIT
246
self._last_orientation = Orientation.LANDSCAPE
248
# Flag set when a notification is being shown (Maemo bug 11235)
249
self._fremantle_notification_visible = False
251
self._last_orientation = Orientation.LANDSCAPE
252
self.toolbar.set_property('visible', self.config.show_toolbar)
254
self.bluetooth_available = util.bluetooth_available()
256
self.config.connect_gtk_window(self.gPodder, 'main_window')
257
if not gpodder.ui.fremantle:
258
self.config.connect_gtk_paned('paned_position', self.channelPaned)
259
self.main_window.show()
261
self.player_receiver = player.MediaPlayerDBusReceiver(self.on_played)
263
if gpodder.ui.fremantle:
264
# Create a D-Bus monitoring object that takes care of
265
# tracking MAFW (Nokia Media Player) playback events
266
# and sends episode playback status events via D-Bus
267
self.mafw_monitor = MafwPlaybackMonitor(gpodder.dbus_session_bus)
269
self.gPodder.connect('key-press-event', self.on_key_press)
271
self.preferences_dialog = None
272
self.config.add_observer(self.on_config_changed)
274
self.tray_icon = None
275
self.episode_shownotes_window = None
276
self.new_episodes_window = None
278
if gpodder.ui.desktop:
279
# Mac OS X-specific UI tweaks: Native main menu integration
280
# http://sourceforge.net/apps/trac/gtk-osx/wiki/Integrate
281
if getattr(gtk.gdk, 'WINDOWING', 'x11') == 'quartz':
283
import igemacintegration as igemi
285
# Move the menu bar from the window to the Mac menu bar
287
igemi.ige_mac_menu_set_menu_bar(self.mainMenu)
289
# Reparent some items to the "Application" menu
290
for widget in ('/mainMenu/menuHelp/itemAbout', \
291
'/mainMenu/menuPodcasts/itemPreferences'):
292
item = self.uimanager1.get_widget(widget)
293
group = igemi.ige_mac_menu_add_app_menu_group()
294
igemi.ige_mac_menu_add_app_menu_item(group, item, None)
296
quit_widget = '/mainMenu/menuPodcasts/itemQuit'
297
quit_item = self.uimanager1.get_widget(quit_widget)
298
igemi.ige_mac_menu_set_quit_menu_item(quit_item)
300
print >>sys.stderr, """
301
Warning: ige-mac-integration not found - no native menus.
304
self.sync_ui = gPodderSyncUI(self.config, self.notification, \
305
self.main_window, self.show_confirmation, \
306
self.update_episode_list_icons, \
307
self.update_podcast_list_model, self.toolPreferences, \
308
gPodderEpisodeSelector, \
309
self.commit_changes_to_database)
313
self.download_status_model = DownloadStatusModel()
314
self.download_queue_manager = download.DownloadQueueManager(self.config)
316
if gpodder.ui.desktop:
317
self.show_hide_tray_icon()
318
self.itemShowAllEpisodes.set_active(self.config.podcast_list_view_all)
319
self.itemShowToolbar.set_active(self.config.show_toolbar)
320
self.itemShowDescription.set_active(self.config.episode_list_descriptions)
322
if not gpodder.ui.fremantle:
323
self.config.connect_gtk_spinbutton('max_downloads', self.spinMaxDownloads)
324
self.config.connect_gtk_togglebutton('max_downloads_enabled', self.cbMaxDownloads)
325
self.config.connect_gtk_spinbutton('limit_rate_value', self.spinLimitDownloads)
326
self.config.connect_gtk_togglebutton('limit_rate', self.cbLimitDownloads)
328
# When the amount of maximum downloads changes, notify the queue manager
329
changed_cb = lambda spinbutton: self.download_queue_manager.spawn_threads()
330
self.spinMaxDownloads.connect('value-changed', changed_cb)
332
self.default_title = 'gPodder'
333
if gpodder.__version__.rfind('git') != -1:
334
self.set_title('gPodder %s' % gpodder.__version__)
336
title = self.gPodder.get_title()
337
if title is not None:
338
self.set_title(title)
340
self.set_title(_('gPodder'))
342
self.cover_downloader = CoverDownloader()
344
# Generate list models for podcasts and their episodes
345
self.podcast_list_model = PodcastListModel(self.cover_downloader)
347
self.cover_downloader.register('cover-available', self.cover_download_finished)
348
self.cover_downloader.register('cover-removed', self.cover_file_removed)
350
if gpodder.ui.fremantle:
351
# Work around Maemo bug #4718
352
self.button_refresh.set_name('HildonButton-finger')
353
self.button_subscribe.set_name('HildonButton-finger')
355
self.button_refresh.set_sensitive(False)
356
self.button_subscribe.set_sensitive(False)
358
self.button_subscribe.set_image(gtk.image_new_from_icon_name(\
359
self.ICON_GENERAL_ADD, gtk.ICON_SIZE_BUTTON))
360
self.button_refresh.set_image(gtk.image_new_from_icon_name(\
361
self.ICON_GENERAL_REFRESH, gtk.ICON_SIZE_BUTTON))
363
# Make the button scroll together with the TreeView contents
364
action_area_box = self.treeChannels.get_action_area_box()
365
for child in self.buttonbox:
366
child.reparent(action_area_box)
367
self.vbox.remove(self.buttonbox)
368
self.treeChannels.set_action_area_visible(True)
370
# Set up a very nice progress bar setup
371
self.fancy_progress_bar = FancyProgressBar(self.main_window, \
372
self.on_btnCancelFeedUpdate_clicked)
373
self.pbFeedUpdate = self.fancy_progress_bar.progress_bar
374
self.pbFeedUpdate.set_ellipsize(pango.ELLIPSIZE_MIDDLE)
375
self.vbox.pack_start(self.fancy_progress_bar.event_box, False)
377
from gpodder.gtkui.frmntl import style
378
sub_font = style.get_font_desc('SmallSystemFont')
379
sub_color = style.get_color('SecondaryTextColor')
380
sub = (sub_font.to_string(), sub_color.to_string())
381
sub = '<span font_desc="%s" foreground="%s">%%s</span>' % sub
382
self.label_footer.set_markup(sub % gpodder.__copyright__)
384
hildon.hildon_gtk_window_set_progress_indicator(self.main_window, True)
385
while gtk.events_pending():
386
gtk.main_iteration(False)
389
# Try to get the real package version from dpkg
390
p = subprocess.Popen(['dpkg-query', '-W', '-f=${Version}', 'gpodder'], stdout=subprocess.PIPE)
391
version, _stderr = p.communicate()
395
version = gpodder.__version__
396
self.label_footer.set_markup(sub % ('v %s' % version))
397
self.label_footer.hide()
399
self.episodes_window = gPodderEpisodes(self.main_window, \
400
on_treeview_expose_event=self.on_treeview_expose_event, \
401
show_episode_shownotes=self.show_episode_shownotes, \
402
update_podcast_list_model=self.update_podcast_list_model, \
403
on_itemRemoveChannel_activate=self.on_itemRemoveChannel_activate, \
404
item_view_episodes_all=self.item_view_episodes_all, \
405
item_view_episodes_unplayed=self.item_view_episodes_unplayed, \
406
item_view_episodes_downloaded=self.item_view_episodes_downloaded, \
407
item_view_episodes_undeleted=self.item_view_episodes_undeleted, \
408
on_entry_search_episodes_changed=self.on_entry_search_episodes_changed, \
409
on_entry_search_episodes_key_press=self.on_entry_search_episodes_key_press, \
410
hide_episode_search=self.hide_episode_search, \
411
on_itemUpdateChannel_activate=self.on_itemUpdateChannel_activate, \
412
playback_episodes=self.playback_episodes, \
413
delete_episode_list=self.delete_episode_list, \
414
episode_list_status_changed=self.episode_list_status_changed, \
415
download_episode_list=self.download_episode_list, \
416
episode_is_downloading=self.episode_is_downloading, \
417
show_episode_in_download_manager=self.show_episode_in_download_manager, \
418
add_download_task_monitor=self.add_download_task_monitor, \
419
remove_download_task_monitor=self.remove_download_task_monitor, \
420
for_each_episode_set_task_status=self.for_each_episode_set_task_status, \
421
on_itemUpdate_activate=self.on_itemUpdate_activate, \
422
show_delete_episodes_window=self.show_delete_episodes_window, \
423
cover_downloader=self.cover_downloader)
425
# Expose objects for episode list type-ahead find
426
self.hbox_search_episodes = self.episodes_window.hbox_search_episodes
427
self.entry_search_episodes = self.episodes_window.entry_search_episodes
428
self.button_search_episodes_clear = self.episodes_window.button_search_episodes_clear
430
self.downloads_window = gPodderDownloads(self.main_window, \
431
on_treeview_expose_event=self.on_treeview_expose_event, \
432
cleanup_downloads=self.cleanup_downloads, \
433
_for_each_task_set_status=self._for_each_task_set_status, \
434
downloads_list_get_selection=self.downloads_list_get_selection, \
437
self.treeAvailable = self.episodes_window.treeview
438
self.treeDownloads = self.downloads_window.treeview
440
# Source IDs for timeouts for search-as-you-type
441
self._podcast_list_search_timeout = None
442
self._episode_list_search_timeout = None
444
# Init the treeviews that we use
445
self.init_podcast_list_treeview()
446
self.init_episode_list_treeview()
447
self.init_download_list_treeview()
449
if self.config.podcast_list_hide_boring:
450
self.item_view_hide_boring_podcasts.set_active(True)
452
self.currently_updating = False
454
if gpodder.ui.maemo or self.config.enable_fingerscroll:
455
self.context_menu_mouse_button = 1
457
self.context_menu_mouse_button = 3
459
if self.config.start_iconified:
460
self.iconify_main_window()
462
self.download_tasks_seen = set()
463
self.download_list_update_enabled = False
464
self.download_task_monitors = set()
466
# Subscribed channels
467
self.active_channel = None
468
self.channels = PodcastChannel.load_from_db(self.db, self.config.download_dir)
469
self.channel_list_changed = True
470
self.update_podcasts_tab()
472
# load list of user applications for audio playback
473
self.user_apps_reader = UserAppsReader(['audio', 'video'])
474
threading.Thread(target=self.user_apps_reader.read).start()
476
# Set the "Device" menu item for the first time
477
if gpodder.ui.desktop:
478
self.update_item_device()
480
# Set up the first instance of MygPoClient
481
self.mygpo_client = my.MygPoClient(self.config)
483
# Now, update the feed cache, when everything's in place
484
if not gpodder.ui.fremantle:
485
self.btnUpdateFeeds.show()
486
self.updating_feed_cache = False
487
self.feed_cache_update_cancelled = False
488
self.update_feed_cache(force_update=self.config.update_on_startup)
490
self.message_area = None
492
def find_partial_downloads():
493
# Look for partial file downloads
494
partial_files = glob.glob(os.path.join(self.config.download_dir, '*', '*.partial'))
495
count = len(partial_files)
496
resumable_episodes = []
498
if not gpodder.ui.fremantle:
499
util.idle_add(self.wNotebook.set_current_page, 1)
500
indicator = ProgressIndicator(_('Loading incomplete downloads'), \
501
_('Some episodes have not finished downloading in a previous session.'), \
502
False, self.get_dialog_parent())
503
indicator.on_message(N_('%(count)d partial file', '%(count)d partial files', count) % {'count':count})
505
candidates = [f[:-len('.partial')] for f in partial_files]
508
for c in self.channels:
509
for e in c.get_all_episodes():
510
filename = e.local_filename(create=False, check_only=True)
511
if filename in candidates:
512
log('Found episode: %s', e.title, sender=self)
514
indicator.on_message(e.title)
515
indicator.on_progress(float(found)/count)
516
candidates.remove(filename)
517
partial_files.remove(filename+'.partial')
519
if os.path.exists(filename):
520
# The file has already been downloaded;
521
# remove the leftover partial file
522
util.delete_file(filename+'.partial')
524
resumable_episodes.append(e)
532
for f in partial_files:
533
log('Partial file without episode: %s', f, sender=self)
536
util.idle_add(indicator.on_finished)
538
if len(resumable_episodes):
539
def offer_resuming():
540
self.download_episode_list_paused(resumable_episodes)
541
if not gpodder.ui.fremantle:
542
resume_all = gtk.Button(_('Resume all'))
543
#resume_all.set_border_width(0)
544
def on_resume_all(button):
545
selection = self.treeDownloads.get_selection()
546
selection.select_all()
547
selected_tasks, can_queue, can_cancel, can_pause, can_remove, can_force = self.downloads_list_get_selection()
548
selection.unselect_all()
549
self._for_each_task_set_status(selected_tasks, download.DownloadTask.QUEUED)
550
self.message_area.hide()
551
resume_all.connect('clicked', on_resume_all)
553
self.message_area = SimpleMessageArea(_('Incomplete downloads from a previous session were found.'), (resume_all,))
554
self.vboxDownloadStatusWidgets.pack_start(self.message_area, expand=False)
555
self.vboxDownloadStatusWidgets.reorder_child(self.message_area, 0)
556
self.message_area.show_all()
557
self.clean_up_downloads(delete_partial=False)
558
util.idle_add(offer_resuming)
559
elif not gpodder.ui.fremantle:
560
util.idle_add(self.wNotebook.set_current_page, 0)
562
util.idle_add(self.clean_up_downloads, True)
563
threading.Thread(target=find_partial_downloads).start()
565
# Start the auto-update procedure
566
self._auto_update_timer_source_id = None
567
if self.config.auto_update_feeds:
568
self.restart_auto_update_timer()
570
# Delete old episodes if the user wishes to
571
if self.config.auto_remove_played_episodes and \
572
self.config.episode_old_age > 0:
573
old_episodes = list(self.get_expired_episodes())
574
if len(old_episodes) > 0:
575
self.delete_episode_list(old_episodes, confirm=False)
576
self.update_podcast_list_model(set(e.channel.url for e in old_episodes))
578
if gpodder.ui.fremantle:
579
hildon.hildon_gtk_window_set_progress_indicator(self.main_window, False)
580
self.button_refresh.set_sensitive(True)
581
self.button_subscribe.set_sensitive(True)
582
self.main_window.set_title(_('gPodder'))
583
hildon.hildon_gtk_window_take_screenshot(self.main_window, True)
585
# Do the initial sync with the web service
586
util.idle_add(self.mygpo_client.flush, True)
588
# First-time users should be asked if they want to see the OPML
589
if not self.channels and not gpodder.ui.fremantle:
590
util.idle_add(self.on_itemUpdate_activate)
592
def episode_object_by_uri(self, uri):
593
"""Get an episode object given a local or remote URI
595
This can be used to quickly access an episode object
596
when all we have is its download filename or episode
597
URL (e.g. from external D-Bus calls / signals, etc..)
599
if uri.startswith('/'):
600
uri = 'file://' + urllib.quote(uri)
602
prefix = 'file://' + urllib.quote(self.config.download_dir)
604
if uri.startswith(prefix):
605
# File is on the local filesystem in the download folder
606
filename = urllib.unquote(uri[len(prefix):])
607
file_parts = [x for x in filename.split(os.sep) if x]
609
if len(file_parts) == 2:
610
dir_name, filename = file_parts
611
channels = [c for c in self.channels if c.foldername == dir_name]
612
if len(channels) == 1:
613
channel = channels[0]
614
return channel.get_episode_by_filename(filename)
616
# Possibly remote file - search the database for a podcast
617
channel_id = self.db.get_channel_id_from_episode_url(uri)
619
if channel_id is not None:
620
channels = [c for c in self.channels if c.id == channel_id]
621
if len(channels) == 1:
622
channel = channels[0]
623
return channel.get_episode_by_url(uri)
627
def on_played(self, start, end, total, file_uri):
628
"""Handle the "played" signal from a media player"""
629
if start == 0 and end == 0 and total == 0:
630
# Ignore bogus play event
632
elif end < start + 5:
633
# Ignore "less than five seconds" segments,
634
# as they can happen with seeking, etc...
637
log('Received play action: %s (%d, %d, %d)', file_uri, start, end, total, sender=self)
638
episode = self.episode_object_by_uri(file_uri)
640
if episode is not None:
641
file_type = episode.file_type()
645
episode.total_time = total
647
# Assume the episode's total time for the action
648
total = episode.total_time
649
if episode.current_position_updated is None or \
650
now > episode.current_position_updated:
651
episode.current_position = end
652
episode.current_position_updated = now
653
episode.mark(is_played=True)
656
self.update_episode_list_icons([episode.url])
657
self.update_podcast_list_model([episode.channel.url])
659
# Submit this action to the webservice
660
self.mygpo_client.on_playback_full(episode, \
663
def on_add_remove_podcasts_mygpo(self):
664
actions = self.mygpo_client.get_received_actions()
668
existing_urls = [c.url for c in self.channels]
670
# Columns for the episode selector window - just one...
672
('description', None, None, _('Action')),
675
# A list of actions that have to be chosen from
678
# Actions that are ignored (already carried out)
681
for action in actions:
682
if action.is_add and action.url not in existing_urls:
683
changes.append(my.Change(action))
684
elif action.is_remove and action.url in existing_urls:
685
podcast_object = None
686
for podcast in self.channels:
687
if podcast.url == action.url:
688
podcast_object = podcast
690
changes.append(my.Change(action, podcast_object))
692
log('Ignoring action: %s', action, sender=self)
693
ignored.append(action)
695
# Confirm all ignored changes
696
self.mygpo_client.confirm_received_actions(ignored)
698
def execute_podcast_actions(selected):
699
add_list = [c.action.url for c in selected if c.action.is_add]
700
remove_list = [c.podcast for c in selected if c.action.is_remove]
702
# Apply the accepted changes locally
703
self.add_podcast_list(add_list)
704
self.remove_podcast_list(remove_list, confirm=False)
706
# All selected items are now confirmed
707
self.mygpo_client.confirm_received_actions(c.action for c in selected)
709
# Revert the changes on the server
710
rejected = [c.action for c in changes if c not in selected]
711
self.mygpo_client.reject_received_actions(rejected)
714
# We're abusing the Episode Selector again ;) -- thp
715
gPodderEpisodeSelector(self.main_window, \
716
title=_('Confirm changes from gpodder.net'), \
717
instructions=_('Select the actions you want to carry out.'), \
720
size_attribute=None, \
721
stock_ok_button=gtk.STOCK_APPLY, \
722
callback=execute_podcast_actions, \
725
# There are some actions that need the user's attention
730
# We have no remaining actions - no selection happens
733
def rewrite_urls_mygpo(self):
734
# Check if we have to rewrite URLs since the last add
735
rewritten_urls = self.mygpo_client.get_rewritten_urls()
737
for rewritten_url in rewritten_urls:
738
if not rewritten_url.new_url:
741
for channel in self.channels:
742
if channel.url == rewritten_url.old_url:
743
log('Updating URL of %s to %s', channel, \
744
rewritten_url.new_url, sender=self)
745
channel.url = rewritten_url.new_url
747
self.channel_list_changed = True
748
util.idle_add(self.update_episode_list_model)
751
def on_send_full_subscriptions(self):
752
# Send the full subscription list to the gpodder.net client
753
# (this will overwrite the subscription list on the server)
754
indicator = ProgressIndicator(_('Uploading subscriptions'), \
755
_('Your subscriptions are being uploaded to the server.'), \
756
False, self.get_dialog_parent())
759
self.mygpo_client.set_subscriptions([c.url for c in self.channels])
760
util.idle_add(self.show_message, _('List uploaded successfully.'))
765
message = e.__class__.__name__
766
self.show_message(message, \
767
_('Error while uploading'), \
769
util.idle_add(show_error, e)
771
util.idle_add(indicator.on_finished)
773
def on_podcast_selected(self, treeview, path, column):
775
model = treeview.get_model()
776
channel = model.get_value(model.get_iter(path), \
777
PodcastListModel.C_CHANNEL)
778
self.active_channel = channel
779
self.update_episode_list_model()
780
self.episodes_window.channel = self.active_channel
781
self.episodes_window.show()
783
def on_button_subscribe_clicked(self, button):
784
self.on_itemImportChannels_activate(button)
786
def on_button_downloads_clicked(self, widget):
787
self.downloads_window.show()
789
def show_episode_in_download_manager(self, episode):
790
self.downloads_window.show()
791
model = self.treeDownloads.get_model()
792
selection = self.treeDownloads.get_selection()
793
selection.unselect_all()
794
it = model.get_iter_first()
795
while it is not None:
796
task = model.get_value(it, DownloadStatusModel.C_TASK)
797
if task.episode.url == episode.url:
798
selection.select_iter(it)
799
# FIXME: Scroll to selection in pannable area
801
it = model.iter_next(it)
803
def for_each_episode_set_task_status(self, episodes, status):
804
episode_urls = set(episode.url for episode in episodes)
805
model = self.treeDownloads.get_model()
806
selected_tasks = [(gtk.TreeRowReference(model, row.path), \
807
model.get_value(row.iter, \
808
DownloadStatusModel.C_TASK)) for row in model \
809
if model.get_value(row.iter, DownloadStatusModel.C_TASK).url \
811
self._for_each_task_set_status(selected_tasks, status)
813
def on_window_orientation_changed(self, orientation):
814
self._last_orientation = orientation
815
if self.preferences_dialog is not None:
816
self.preferences_dialog.on_window_orientation_changed(orientation)
818
treeview = self.treeChannels
819
if orientation == Orientation.PORTRAIT:
820
treeview.set_action_area_orientation(gtk.ORIENTATION_VERTICAL)
821
# Work around Maemo bug #4718
822
self.button_subscribe.set_name('HildonButton-thumb')
823
self.button_refresh.set_name('HildonButton-thumb')
825
treeview.set_action_area_orientation(gtk.ORIENTATION_HORIZONTAL)
826
# Work around Maemo bug #4718
827
self.button_subscribe.set_name('HildonButton-finger')
828
self.button_refresh.set_name('HildonButton-finger')
830
if gpodder.ui.fremantle:
831
self.fancy_progress_bar.relayout()
833
def on_treeview_podcasts_selection_changed(self, selection):
834
model, iter = selection.get_selected()
836
self.active_channel = None
837
self.episode_list_model.clear()
839
def on_treeview_button_pressed(self, treeview, event):
840
if event.window != treeview.get_bin_window():
843
TreeViewHelper.save_button_press_event(treeview, event)
845
if getattr(treeview, TreeViewHelper.ROLE) == \
846
TreeViewHelper.ROLE_PODCASTS:
847
return self.currently_updating
849
return event.button == self.context_menu_mouse_button and \
852
def on_treeview_podcasts_button_released(self, treeview, event):
853
if event.window != treeview.get_bin_window():
857
return self.treeview_channels_handle_gestures(treeview, event)
858
return self.treeview_channels_show_context_menu(treeview, event)
860
def on_treeview_episodes_button_released(self, treeview, event):
861
if event.window != treeview.get_bin_window():
864
if self.config.enable_fingerscroll or self.config.maemo_enable_gestures:
865
return self.treeview_available_handle_gestures(treeview, event)
867
return self.treeview_available_show_context_menu(treeview, event)
869
def on_treeview_downloads_button_released(self, treeview, event):
870
if event.window != treeview.get_bin_window():
873
return self.treeview_downloads_show_context_menu(treeview, event)
875
def on_entry_search_podcasts_changed(self, editable):
876
if self.hbox_search_podcasts.get_property('visible'):
877
def set_search_term(self, text):
878
self.podcast_list_model.set_search_term(text)
879
self._podcast_list_search_timeout = None
882
if self._podcast_list_search_timeout is not None:
883
gobject.source_remove(self._podcast_list_search_timeout)
884
self._podcast_list_search_timeout = gobject.timeout_add(\
885
self.LIVE_SEARCH_DELAY, \
886
set_search_term, self, editable.get_chars(0, -1))
888
def on_entry_search_podcasts_key_press(self, editable, event):
889
if event.keyval == gtk.keysyms.Escape:
890
self.hide_podcast_search()
893
def hide_podcast_search(self, *args):
894
if self._podcast_list_search_timeout is not None:
895
gobject.source_remove(self._podcast_list_search_timeout)
896
self._podcast_list_search_timeout = None
897
self.hbox_search_podcasts.hide()
898
self.entry_search_podcasts.set_text('')
899
self.podcast_list_model.set_search_term(None)
900
self.treeChannels.grab_focus()
902
def show_podcast_search(self, input_char):
903
self.hbox_search_podcasts.show()
904
self.entry_search_podcasts.insert_text(input_char, -1)
905
self.entry_search_podcasts.grab_focus()
906
self.entry_search_podcasts.set_position(-1)
908
def init_podcast_list_treeview(self):
909
# Set up podcast channel tree view widget
910
if gpodder.ui.fremantle:
911
if self.config.podcast_list_view_mode == EpisodeListModel.VIEW_DOWNLOADED:
912
self.item_view_podcasts_downloaded.set_active(True)
913
elif self.config.podcast_list_view_mode == EpisodeListModel.VIEW_UNPLAYED:
914
self.item_view_podcasts_unplayed.set_active(True)
916
self.item_view_podcasts_all.set_active(True)
917
self.podcast_list_model.set_view_mode(self.config.podcast_list_view_mode)
919
iconcolumn = gtk.TreeViewColumn('')
920
iconcell = gtk.CellRendererPixbuf()
921
iconcolumn.pack_start(iconcell, False)
922
iconcolumn.add_attribute(iconcell, 'pixbuf', PodcastListModel.C_COVER)
923
self.treeChannels.append_column(iconcolumn)
925
namecolumn = gtk.TreeViewColumn('')
926
namecell = gtk.CellRendererText()
927
namecell.set_property('ellipsize', pango.ELLIPSIZE_END)
928
namecolumn.pack_start(namecell, True)
929
namecolumn.add_attribute(namecell, 'markup', PodcastListModel.C_DESCRIPTION)
931
if gpodder.ui.fremantle:
932
countcell = gtk.CellRendererText()
933
from gpodder.gtkui.frmntl import style
934
countcell.set_property('font-desc', style.get_font_desc('EmpSystemFont'))
935
countcell.set_property('foreground-gdk', style.get_color('SecondaryTextColor'))
936
countcell.set_property('alignment', pango.ALIGN_RIGHT)
937
countcell.set_property('xalign', 1.)
938
countcell.set_property('xpad', 5)
939
namecolumn.pack_start(countcell, False)
940
namecolumn.add_attribute(countcell, 'text', PodcastListModel.C_DOWNLOADS)
941
namecolumn.add_attribute(countcell, 'visible', PodcastListModel.C_DOWNLOADS)
943
iconcell = gtk.CellRendererPixbuf()
944
iconcell.set_property('xalign', 1.0)
945
namecolumn.pack_start(iconcell, False)
946
namecolumn.add_attribute(iconcell, 'pixbuf', PodcastListModel.C_PILL)
947
namecolumn.add_attribute(iconcell, 'visible', PodcastListModel.C_PILL_VISIBLE)
949
self.treeChannels.append_column(namecolumn)
951
self.treeChannels.set_model(self.podcast_list_model.get_filtered_model())
953
# When no podcast is selected, clear the episode list model
954
selection = self.treeChannels.get_selection()
955
selection.connect('changed', self.on_treeview_podcasts_selection_changed)
957
# Set up type-ahead find for the podcast list
958
def on_key_press(treeview, event):
959
if event.keyval == gtk.keysyms.Escape:
960
self.hide_podcast_search()
961
elif gpodder.ui.fremantle and event.keyval == gtk.keysyms.BackSpace:
962
self.hide_podcast_search()
963
elif event.state & gtk.gdk.CONTROL_MASK:
964
# Don't handle type-ahead when control is pressed (so shortcuts
965
# with the Ctrl key still work, e.g. Ctrl+A, ...)
968
unicode_char_id = gtk.gdk.keyval_to_unicode(event.keyval)
969
if unicode_char_id == 0:
971
input_char = unichr(unicode_char_id)
972
self.show_podcast_search(input_char)
974
self.treeChannels.connect('key-press-event', on_key_press)
976
# Enable separators to the podcast list to separate special podcasts
977
# from others (this is used for the "all episodes" view)
978
self.treeChannels.set_row_separator_func(PodcastListModel.row_separator_func)
980
TreeViewHelper.set(self.treeChannels, TreeViewHelper.ROLE_PODCASTS)
982
def on_entry_search_episodes_changed(self, editable):
983
if self.hbox_search_episodes.get_property('visible'):
984
def set_search_term(self, text):
985
self.episode_list_model.set_search_term(text)
986
self._episode_list_search_timeout = None
989
if self._episode_list_search_timeout is not None:
990
gobject.source_remove(self._episode_list_search_timeout)
991
self._episode_list_search_timeout = gobject.timeout_add(\
992
self.LIVE_SEARCH_DELAY, \
993
set_search_term, self, editable.get_chars(0, -1))
995
def on_entry_search_episodes_key_press(self, editable, event):
996
if event.keyval == gtk.keysyms.Escape:
997
self.hide_episode_search()
1000
def hide_episode_search(self, *args):
1001
if self._episode_list_search_timeout is not None:
1002
gobject.source_remove(self._episode_list_search_timeout)
1003
self._episode_list_search_timeout = None
1004
self.hbox_search_episodes.hide()
1005
self.entry_search_episodes.set_text('')
1006
self.episode_list_model.set_search_term(None)
1007
self.treeAvailable.grab_focus()
1009
def show_episode_search(self, input_char):
1010
self.hbox_search_episodes.show()
1011
self.entry_search_episodes.insert_text(input_char, -1)
1012
self.entry_search_episodes.grab_focus()
1013
self.entry_search_episodes.set_position(-1)
1015
def init_episode_list_treeview(self):
1016
# For loading the list model
1017
self.episode_list_model = EpisodeListModel(self.on_episode_list_filter_changed)
1019
if self.config.episode_list_view_mode == EpisodeListModel.VIEW_UNDELETED:
1020
self.item_view_episodes_undeleted.set_active(True)
1021
elif self.config.episode_list_view_mode == EpisodeListModel.VIEW_DOWNLOADED:
1022
self.item_view_episodes_downloaded.set_active(True)
1023
elif self.config.episode_list_view_mode == EpisodeListModel.VIEW_UNPLAYED:
1024
self.item_view_episodes_unplayed.set_active(True)
1026
self.item_view_episodes_all.set_active(True)
1028
self.episode_list_model.set_view_mode(self.config.episode_list_view_mode)
1030
self.treeAvailable.set_model(self.episode_list_model.get_filtered_model())
1032
TreeViewHelper.set(self.treeAvailable, TreeViewHelper.ROLE_EPISODES)
1034
iconcell = gtk.CellRendererPixbuf()
1035
iconcell.set_property('stock-size', gtk.ICON_SIZE_BUTTON)
1036
if gpodder.ui.maemo:
1037
iconcell.set_fixed_size(50, 50)
1039
iconcell.set_fixed_size(40, -1)
1041
namecell = gtk.CellRendererText()
1042
namecell.set_property('ellipsize', pango.ELLIPSIZE_END)
1043
namecolumn = gtk.TreeViewColumn(_('Episode'))
1044
namecolumn.pack_start(iconcell, False)
1045
namecolumn.add_attribute(iconcell, 'icon-name', EpisodeListModel.C_STATUS_ICON)
1046
namecolumn.pack_start(namecell, True)
1047
namecolumn.add_attribute(namecell, 'markup', EpisodeListModel.C_DESCRIPTION)
1048
if gpodder.ui.fremantle:
1049
namecolumn.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
1051
namecolumn.set_sort_column_id(EpisodeListModel.C_DESCRIPTION)
1052
namecolumn.set_sizing(gtk.TREE_VIEW_COLUMN_AUTOSIZE)
1053
namecolumn.set_resizable(True)
1054
namecolumn.set_expand(True)
1056
if gpodder.ui.fremantle:
1057
from gpodder.gtkui.frmntl import style
1058
timecell = gtk.CellRendererText()
1059
timecell.set_property('font-desc', style.get_font_desc('SmallSystemFont'))
1060
timecell.set_property('foreground-gdk', style.get_color('SecondaryTextColor'))
1061
timecell.set_property('alignment', pango.ALIGN_RIGHT)
1062
timecell.set_property('xalign', 1.)
1063
timecell.set_property('xpad', 5)
1064
timecell.set_property('yalign', .85)
1065
namecolumn.pack_start(timecell, False)
1066
namecolumn.add_attribute(timecell, 'text', EpisodeListModel.C_TIME)
1067
namecolumn.add_attribute(timecell, 'visible', EpisodeListModel.C_TIME_VISIBLE)
1069
lockcell = gtk.CellRendererPixbuf()
1070
lockcell.set_fixed_size(40, -1)
1071
lockcell.set_property('stock-size', gtk.ICON_SIZE_MENU)
1072
lockcell.set_property('icon-name', 'emblem-readonly')
1073
namecolumn.pack_start(lockcell, False)
1074
namecolumn.add_attribute(lockcell, 'visible', EpisodeListModel.C_LOCKED)
1076
sizecell = gtk.CellRendererText()
1077
sizecell.set_property('xalign', 1)
1078
sizecolumn = gtk.TreeViewColumn(_('Size'), sizecell, text=EpisodeListModel.C_FILESIZE_TEXT)
1079
sizecolumn.set_sort_column_id(EpisodeListModel.C_FILESIZE)
1081
releasecell = gtk.CellRendererText()
1082
releasecolumn = gtk.TreeViewColumn(_('Released'), releasecell, text=EpisodeListModel.C_PUBLISHED_TEXT)
1083
releasecolumn.set_sort_column_id(EpisodeListModel.C_PUBLISHED)
1085
namecolumn.set_reorderable(True)
1086
self.treeAvailable.append_column(namecolumn)
1088
if not gpodder.ui.maemo:
1089
for itemcolumn in (sizecolumn, releasecolumn):
1090
itemcolumn.set_reorderable(True)
1091
self.treeAvailable.append_column(itemcolumn)
1093
# Set up type-ahead find for the episode list
1094
def on_key_press(treeview, event):
1095
if event.keyval == gtk.keysyms.Escape:
1096
self.hide_episode_search()
1097
elif gpodder.ui.fremantle and event.keyval == gtk.keysyms.BackSpace:
1098
self.hide_episode_search()
1099
elif event.state & gtk.gdk.CONTROL_MASK:
1100
# Don't handle type-ahead when control is pressed (so shortcuts
1101
# with the Ctrl key still work, e.g. Ctrl+A, ...)
1104
unicode_char_id = gtk.gdk.keyval_to_unicode(event.keyval)
1105
if unicode_char_id == 0:
1107
input_char = unichr(unicode_char_id)
1108
self.show_episode_search(input_char)
1110
self.treeAvailable.connect('key-press-event', on_key_press)
1112
if gpodder.ui.desktop and not self.config.enable_fingerscroll:
1113
self.treeAvailable.enable_model_drag_source(gtk.gdk.BUTTON1_MASK, \
1114
(('text/uri-list', 0, 0),), gtk.gdk.ACTION_COPY)
1115
def drag_data_get(tree, context, selection_data, info, timestamp):
1116
if self.config.on_drag_mark_played:
1117
for episode in self.get_selected_episodes():
1118
episode.mark(is_played=True)
1119
self.on_selected_episodes_status_changed()
1120
uris = ['file://'+e.local_filename(create=False) \
1121
for e in self.get_selected_episodes() \
1122
if e.was_downloaded(and_exists=True)]
1123
uris.append('') # for the trailing '\r\n'
1124
selection_data.set(selection_data.target, 8, '\r\n'.join(uris))
1125
self.treeAvailable.connect('drag-data-get', drag_data_get)
1127
selection = self.treeAvailable.get_selection()
1128
if self.config.maemo_enable_gestures or self.config.enable_fingerscroll:
1129
selection.set_mode(gtk.SELECTION_SINGLE)
1130
elif gpodder.ui.fremantle:
1131
selection.set_mode(gtk.SELECTION_SINGLE)
1133
selection.set_mode(gtk.SELECTION_MULTIPLE)
1134
# Update the sensitivity of the toolbar buttons on the Desktop
1135
selection.connect('changed', lambda s: self.play_or_download())
1137
if gpodder.ui.diablo:
1138
# Set up the tap-and-hold context menu for podcasts
1140
menu.append(self.itemUpdateChannel.create_menu_item())
1141
menu.append(self.itemEditChannel.create_menu_item())
1142
menu.append(gtk.SeparatorMenuItem())
1143
menu.append(self.itemRemoveChannel.create_menu_item())
1144
menu.append(gtk.SeparatorMenuItem())
1145
item = gtk.ImageMenuItem(_('Close this menu'))
1146
item.set_image(gtk.image_new_from_stock(gtk.STOCK_CLOSE, \
1147
gtk.ICON_SIZE_MENU))
1150
menu = self.set_finger_friendly(menu)
1151
self.treeChannels.tap_and_hold_setup(menu)
1154
def init_download_list_treeview(self):
1155
# enable multiple selection support
1156
self.treeDownloads.get_selection().set_mode(gtk.SELECTION_MULTIPLE)
1157
self.treeDownloads.set_search_equal_func(TreeViewHelper.make_search_equal_func(DownloadStatusModel))
1159
# columns and renderers for "download progress" tab
1160
# First column: [ICON] Episodename
1161
column = gtk.TreeViewColumn(_('Episode'))
1163
cell = gtk.CellRendererPixbuf()
1164
if gpodder.ui.maemo:
1165
cell.set_fixed_size(50, 50)
1166
cell.set_property('stock-size', gtk.ICON_SIZE_BUTTON)
1167
column.pack_start(cell, expand=False)
1168
column.add_attribute(cell, 'icon-name', \
1169
DownloadStatusModel.C_ICON_NAME)
1171
cell = gtk.CellRendererText()
1172
cell.set_property('ellipsize', pango.ELLIPSIZE_END)
1173
column.pack_start(cell, expand=True)
1174
column.add_attribute(cell, 'markup', DownloadStatusModel.C_NAME)
1175
column.set_sizing(gtk.TREE_VIEW_COLUMN_AUTOSIZE)
1176
column.set_expand(True)
1177
self.treeDownloads.append_column(column)
1179
# Second column: Progress
1180
cell = gtk.CellRendererProgress()
1181
cell.set_property('yalign', .5)
1182
cell.set_property('ypad', 6)
1183
column = gtk.TreeViewColumn(_('Progress'), cell,
1184
value=DownloadStatusModel.C_PROGRESS, \
1185
text=DownloadStatusModel.C_PROGRESS_TEXT)
1186
column.set_sizing(gtk.TREE_VIEW_COLUMN_AUTOSIZE)
1187
column.set_expand(False)
1188
self.treeDownloads.append_column(column)
1189
if gpodder.ui.maemo:
1190
column.set_property('min-width', 200)
1191
column.set_property('max-width', 200)
1193
column.set_property('min-width', 150)
1194
column.set_property('max-width', 150)
1196
self.treeDownloads.set_model(self.download_status_model)
1197
TreeViewHelper.set(self.treeDownloads, TreeViewHelper.ROLE_DOWNLOADS)
1199
def on_treeview_expose_event(self, treeview, event):
1200
if event.window == treeview.get_bin_window():
1201
model = treeview.get_model()
1202
if (model is not None and model.get_iter_first() is not None):
1205
role = getattr(treeview, TreeViewHelper.ROLE, None)
1209
ctx = event.window.cairo_create()
1210
ctx.rectangle(event.area.x, event.area.y,
1211
event.area.width, event.area.height)
1214
x, y, width, height, depth = event.window.get_geometry()
1217
if role == TreeViewHelper.ROLE_EPISODES:
1218
if self.currently_updating:
1219
text = _('Loading episodes')
1220
elif self.config.episode_list_view_mode != \
1221
EpisodeListModel.VIEW_ALL:
1222
text = _('No episodes in current view')
1224
text = _('No episodes available')
1225
elif role == TreeViewHelper.ROLE_PODCASTS:
1226
if self.config.episode_list_view_mode != \
1227
EpisodeListModel.VIEW_ALL and \
1228
self.config.podcast_list_hide_boring and \
1229
len(self.channels) > 0:
1230
text = _('No podcasts in this view')
1232
text = _('No subscriptions')
1233
elif role == TreeViewHelper.ROLE_DOWNLOADS:
1234
text = _('No active downloads')
1236
raise Exception('on_treeview_expose_event: unknown role')
1238
if gpodder.ui.fremantle:
1239
from gpodder.gtkui.frmntl import style
1240
font_desc = style.get_font_desc('LargeSystemFont')
1244
draw_text_box_centered(ctx, treeview, width, height, text, font_desc, progress)
1248
def enable_download_list_update(self):
1249
if not self.download_list_update_enabled:
1250
self.update_downloads_list()
1251
gobject.timeout_add(1500, self.update_downloads_list)
1252
self.download_list_update_enabled = True
1254
def cleanup_downloads(self):
1255
model = self.download_status_model
1257
all_tasks = [(gtk.TreeRowReference(model, row.path), row[0]) for row in model]
1258
changed_episode_urls = set()
1259
for row_reference, task in all_tasks:
1260
if task.status in (task.DONE, task.CANCELLED):
1261
model.remove(model.get_iter(row_reference.get_path()))
1263
# We don't "see" this task anymore - remove it;
1264
# this is needed, so update_episode_list_icons()
1265
# below gets the correct list of "seen" tasks
1266
self.download_tasks_seen.remove(task)
1267
except KeyError, key_error:
1268
log('Cannot remove task from "seen" list: %s', task, sender=self)
1269
changed_episode_urls.add(task.url)
1270
# Tell the task that it has been removed (so it can clean up)
1271
task.removed_from_list()
1273
# Tell the podcasts tab to update icons for our removed podcasts
1274
self.update_episode_list_icons(changed_episode_urls)
1276
# Tell the shownotes window that we have removed the episode
1277
if self.episode_shownotes_window is not None and \
1278
self.episode_shownotes_window.episode is not None and \
1279
self.episode_shownotes_window.episode.url in changed_episode_urls:
1280
self.episode_shownotes_window._download_status_changed(None)
1282
# Update the downloads list one more time
1283
self.update_downloads_list(can_call_cleanup=False)
1285
def on_tool_downloads_toggled(self, toolbutton):
1286
if toolbutton.get_active():
1287
self.wNotebook.set_current_page(1)
1289
self.wNotebook.set_current_page(0)
1291
def add_download_task_monitor(self, monitor):
1292
self.download_task_monitors.add(monitor)
1293
model = self.download_status_model
1297
task = row[self.download_status_model.C_TASK]
1298
monitor.task_updated(task)
1300
def remove_download_task_monitor(self, monitor):
1301
self.download_task_monitors.remove(monitor)
1303
def update_downloads_list(self, can_call_cleanup=True):
1305
model = self.download_status_model
1307
downloading, failed, finished, queued, paused, others = 0, 0, 0, 0, 0, 0
1308
total_speed, total_size, done_size = 0, 0, 0
1310
# Keep a list of all download tasks that we've seen
1311
download_tasks_seen = set()
1313
# Remember the DownloadTask object for the episode that
1314
# has been opened in the episode shownotes dialog (if any)
1315
if self.episode_shownotes_window is not None:
1316
shownotes_episode = self.episode_shownotes_window.episode
1317
shownotes_task = None
1319
shownotes_episode = None
1320
shownotes_task = None
1322
# Do not go through the list of the model is not (yet) available
1327
self.download_status_model.request_update(row.iter)
1329
task = row[self.download_status_model.C_TASK]
1330
speed, size, status, progress = task.speed, task.total_size, task.status, task.progress
1332
# Let the download task monitors know of changes
1333
for monitor in self.download_task_monitors:
1334
monitor.task_updated(task)
1337
done_size += size*progress
1339
if shownotes_episode is not None and \
1340
shownotes_episode.url == task.episode.url:
1341
shownotes_task = task
1343
download_tasks_seen.add(task)
1345
if status == download.DownloadTask.DOWNLOADING:
1347
total_speed += speed
1348
elif status == download.DownloadTask.FAILED:
1350
elif status == download.DownloadTask.DONE:
1352
elif status == download.DownloadTask.QUEUED:
1354
elif status == download.DownloadTask.PAUSED:
1359
# Remember which tasks we have seen after this run
1360
self.download_tasks_seen = download_tasks_seen
1362
if gpodder.ui.desktop:
1363
text = [_('Downloads')]
1364
if downloading + failed + queued > 0:
1367
s.append(N_('%(count)d active', '%(count)d active', downloading) % {'count':downloading})
1369
s.append(N_('%(count)d failed', '%(count)d failed', failed) % {'count':failed})
1371
s.append(N_('%(count)d queued', '%(count)d queued', queued) % {'count':queued})
1372
text.append(' (' + ', '.join(s)+')')
1373
self.labelDownloads.set_text(''.join(text))
1374
elif gpodder.ui.diablo:
1375
sum = downloading + failed + finished + queued + paused + others
1377
self.tool_downloads.set_label(_('Downloads (%d)') % sum)
1379
self.tool_downloads.set_label(_('Downloads'))
1380
elif gpodder.ui.fremantle:
1381
if downloading + queued > 0:
1382
self.button_downloads.set_value(N_('%(count)d active', '%(count)d active', downloading+queued) % {'count':(downloading+queued)})
1384
self.button_downloads.set_value(N_('%(count)d failed', '%(count)d failed', failed) % {'count':failed})
1386
self.button_downloads.set_value(N_('%(count)d paused', '%(count)d paused', paused) % {'count':paused})
1388
self.button_downloads.set_value(_('Idle'))
1390
title = [self.default_title]
1392
# We have to update all episodes/channels for which the status has
1393
# changed. Accessing task.status_changed has the side effect of
1394
# re-setting the changed flag, so we need to get the "changed" list
1395
# of tuples first and split it into two lists afterwards
1396
changed = [(task.url, task.podcast_url) for task in \
1397
self.download_tasks_seen if task.status_changed]
1398
episode_urls = [episode_url for episode_url, channel_url in changed]
1399
channel_urls = [channel_url for episode_url, channel_url in changed]
1401
count = downloading + queued
1403
title.append(N_('downloading %(count)d file', 'downloading %(count)d files', count) % {'count':count})
1406
percentage = 100.0*done_size/total_size
1409
total_speed = util.format_filesize(total_speed)
1410
title[1] += ' (%d%%, %s/s)' % (percentage, total_speed)
1411
if self.tray_icon is not None:
1412
# Update the tray icon status and progress bar
1413
self.tray_icon.set_status(self.tray_icon.STATUS_DOWNLOAD_IN_PROGRESS, title[1])
1414
self.tray_icon.draw_progress_bar(percentage/100.)
1416
if self.tray_icon is not None:
1417
# Update the tray icon status
1418
self.tray_icon.set_status()
1419
if gpodder.ui.desktop:
1420
self.downloads_finished(self.download_tasks_seen)
1421
if gpodder.ui.diablo:
1422
hildon.hildon_banner_show_information(self.gPodder, '', 'gPodder: %s' % _('All downloads finished'))
1423
log('All downloads have finished.', sender=self)
1424
if self.config.cmd_all_downloads_complete:
1425
util.run_external_command(self.config.cmd_all_downloads_complete)
1427
if gpodder.ui.fremantle:
1428
message = '\n'.join(['%s: %s' % (str(task), \
1429
task.error_message) for task in self.download_tasks_seen if task.notify_as_failed()])
1431
self.show_message(message, _('Downloads failed'), important=True)
1433
# Remove finished episodes
1434
if self.config.auto_cleanup_downloads and can_call_cleanup:
1435
self.cleanup_downloads()
1437
# Stop updating the download list here
1438
self.download_list_update_enabled = False
1440
if not gpodder.ui.fremantle:
1441
self.gPodder.set_title(' - '.join(title))
1443
self.update_episode_list_icons(episode_urls)
1444
if self.episode_shownotes_window is not None:
1445
if (shownotes_task and shownotes_task.url in episode_urls) or \
1446
shownotes_task != self.episode_shownotes_window.task:
1447
self.episode_shownotes_window._download_status_changed(shownotes_task)
1448
self.episode_shownotes_window._download_status_progress()
1449
self.play_or_download()
1451
self.update_podcast_list_model(channel_urls)
1453
return self.download_list_update_enabled
1454
except Exception, e:
1455
log('Exception happened while updating download list.', sender=self, traceback=True)
1456
self.show_message('%s\n\n%s' % (_('Please report this problem and restart gPodder:'), str(e)), _('Unhandled exception'), important=True)
1457
# We return False here, so the update loop won't be called again,
1458
# that's why we require the restart of gPodder in the message.
1461
def on_config_changed(self, *args):
1462
util.idle_add(self._on_config_changed, *args)
1464
def _on_config_changed(self, name, old_value, new_value):
1465
if name == 'show_toolbar' and gpodder.ui.desktop:
1466
self.toolbar.set_property('visible', new_value)
1467
elif name == 'episode_list_descriptions':
1468
self.update_episode_list_model()
1469
elif name == 'episode_list_thumbnails':
1470
self.update_episode_list_icons(all=True)
1471
elif name == 'rotation_mode':
1472
self._fremantle_rotation.set_mode(new_value)
1473
elif name in ('auto_update_feeds', 'auto_update_frequency'):
1474
self.restart_auto_update_timer()
1475
elif name == 'podcast_list_view_all':
1476
# Force a update of the podcast list model
1477
self.channel_list_changed = True
1478
if gpodder.ui.fremantle:
1479
hildon.hildon_gtk_window_set_progress_indicator(self.main_window, True)
1480
while gtk.events_pending():
1481
gtk.main_iteration(False)
1482
self.update_podcast_list_model()
1483
if gpodder.ui.fremantle:
1484
hildon.hildon_gtk_window_set_progress_indicator(self.main_window, False)
1486
def on_treeview_query_tooltip(self, treeview, x, y, keyboard_tooltip, tooltip):
1487
# With get_bin_window, we get the window that contains the rows without
1488
# the header. The Y coordinate of this window will be the height of the
1489
# treeview header. This is the amount we have to subtract from the
1490
# event's Y coordinate to get the coordinate to pass to get_path_at_pos
1491
(x_bin, y_bin) = treeview.get_bin_window().get_position()
1494
(path, column, rx, ry) = treeview.get_path_at_pos( x, y) or (None,)*4
1496
if not getattr(treeview, TreeViewHelper.CAN_TOOLTIP) or x > 50 or (column is not None and column != treeview.get_columns()[0]):
1497
setattr(treeview, TreeViewHelper.LAST_TOOLTIP, None)
1500
if path is not None:
1501
model = treeview.get_model()
1502
iter = model.get_iter(path)
1503
role = getattr(treeview, TreeViewHelper.ROLE)
1505
if role == TreeViewHelper.ROLE_EPISODES:
1506
id = model.get_value(iter, EpisodeListModel.C_URL)
1507
elif role == TreeViewHelper.ROLE_PODCASTS:
1508
id = model.get_value(iter, PodcastListModel.C_URL)
1510
last_tooltip = getattr(treeview, TreeViewHelper.LAST_TOOLTIP)
1511
if last_tooltip is not None and last_tooltip != id:
1512
setattr(treeview, TreeViewHelper.LAST_TOOLTIP, None)
1514
setattr(treeview, TreeViewHelper.LAST_TOOLTIP, id)
1516
if role == TreeViewHelper.ROLE_EPISODES:
1517
description = model.get_value(iter, EpisodeListModel.C_TOOLTIP)
1519
tooltip.set_text(description)
1522
elif role == TreeViewHelper.ROLE_PODCASTS:
1523
channel = model.get_value(iter, PodcastListModel.C_CHANNEL)
1526
channel.request_save_dir_size()
1527
diskspace_str = util.format_filesize(channel.save_dir_size, 0)
1528
error_str = model.get_value(iter, PodcastListModel.C_ERROR)
1530
error_str = _('Feedparser error: %s') % saxutils.escape(error_str.strip())
1531
error_str = '<span foreground="#ff0000">%s</span>' % error_str
1532
table = gtk.Table(rows=3, columns=3)
1533
table.set_row_spacings(5)
1534
table.set_col_spacings(5)
1535
table.set_border_width(5)
1537
heading = gtk.Label()
1538
heading.set_alignment(0, 1)
1539
heading.set_markup('<b><big>%s</big></b>\n<small>%s</small>' % (saxutils.escape(channel.title), saxutils.escape(channel.url)))
1540
table.attach(heading, 0, 1, 0, 1)
1541
size_info = gtk.Label()
1542
size_info.set_alignment(1, 1)
1543
size_info.set_justify(gtk.JUSTIFY_RIGHT)
1544
size_info.set_markup('<b>%s</b>\n<small>%s</small>' % (diskspace_str, _('disk usage')))
1545
table.attach(size_info, 2, 3, 0, 1)
1547
table.attach(gtk.HSeparator(), 0, 3, 1, 2)
1549
if len(channel.description) < 500:
1550
description = channel.description
1552
pos = channel.description.find('\n\n')
1553
if pos == -1 or pos > 500:
1554
description = channel.description[:498]+'[...]'
1556
description = channel.description[:pos]
1558
description = gtk.Label(description)
1560
description.set_markup(error_str)
1561
description.set_alignment(0, 0)
1562
description.set_line_wrap(True)
1563
table.attach(description, 0, 3, 2, 3)
1566
tooltip.set_custom(table)
1570
setattr(treeview, TreeViewHelper.LAST_TOOLTIP, None)
1573
def treeview_allow_tooltips(self, treeview, allow):
1574
setattr(treeview, TreeViewHelper.CAN_TOOLTIP, allow)
1576
def update_m3u_playlist_clicked(self, widget):
1577
if self.active_channel is not None:
1578
self.active_channel.update_m3u_playlist()
1579
self.show_message(_('Updated M3U playlist in download folder.'), _('Updated playlist'), widget=self.treeChannels)
1581
def treeview_handle_context_menu_click(self, treeview, event):
1582
x, y = int(event.x), int(event.y)
1583
path, column, rx, ry = treeview.get_path_at_pos(x, y) or (None,)*4
1585
selection = treeview.get_selection()
1586
model, paths = selection.get_selected_rows()
1588
if path is None or (path not in paths and \
1589
event.button == self.context_menu_mouse_button):
1590
# We have right-clicked, but not into the selection,
1591
# assume we don't want to operate on the selection
1594
if path is not None and not paths and \
1595
event.button == self.context_menu_mouse_button:
1596
# No selection or clicked outside selection;
1597
# select the single item where we clicked
1598
treeview.grab_focus()
1599
treeview.set_cursor(path, column, 0)
1603
# Unselect any remaining items (clicked elsewhere)
1604
if hasattr(treeview, 'is_rubber_banding_active'):
1605
if not treeview.is_rubber_banding_active():
1606
selection.unselect_all()
1608
selection.unselect_all()
1612
def downloads_list_get_selection(self, model=None, paths=None):
1613
if model is None and paths is None:
1614
selection = self.treeDownloads.get_selection()
1615
model, paths = selection.get_selected_rows()
1617
can_queue, can_cancel, can_pause, can_remove, can_force = (True,)*5
1618
selected_tasks = [(gtk.TreeRowReference(model, path), \
1619
model.get_value(model.get_iter(path), \
1620
DownloadStatusModel.C_TASK)) for path in paths]
1622
for row_reference, task in selected_tasks:
1623
if task.status != download.DownloadTask.QUEUED:
1625
if task.status not in (download.DownloadTask.PAUSED, \
1626
download.DownloadTask.FAILED, \
1627
download.DownloadTask.CANCELLED):
1629
if task.status not in (download.DownloadTask.PAUSED, \
1630
download.DownloadTask.QUEUED, \
1631
download.DownloadTask.DOWNLOADING, \
1632
download.DownloadTask.FAILED):
1634
if task.status not in (download.DownloadTask.QUEUED, \
1635
download.DownloadTask.DOWNLOADING):
1637
if task.status not in (download.DownloadTask.CANCELLED, \
1638
download.DownloadTask.FAILED, \
1639
download.DownloadTask.DONE):
1642
return selected_tasks, can_queue, can_cancel, can_pause, can_remove, can_force
1644
def downloads_finished(self, download_tasks_seen):
1645
finished_downloads = [str(task) for task in download_tasks_seen if task.notify_as_finished()]
1646
failed_downloads = [str(task)+' ('+task.error_message+')' for task in download_tasks_seen if task.notify_as_failed()]
1648
if finished_downloads and failed_downloads:
1649
message = self.format_episode_list(finished_downloads, 5)
1650
message += '\n\n<i>%s</i>\n' % _('These downloads failed:')
1651
message += self.format_episode_list(failed_downloads, 5)
1652
self.show_message(message, _('Downloads finished'), True, widget=self.labelDownloads)
1653
elif finished_downloads:
1654
message = self.format_episode_list(finished_downloads)
1655
self.show_message(message, _('Downloads finished'), widget=self.labelDownloads)
1656
elif failed_downloads:
1657
message = self.format_episode_list(failed_downloads)
1658
self.show_message(message, _('Downloads failed'), True, widget=self.labelDownloads)
1660
# Open torrent files right after download (bug 1029)
1661
if self.config.open_torrent_after_download:
1662
for task in download_tasks_seen:
1663
if task.status != task.DONE:
1666
episode = task.episode
1667
if episode.mimetype != 'application/x-bittorrent':
1670
self.playback_episodes([episode])
1673
def format_episode_list(self, episode_list, max_episodes=10):
1675
Format a list of episode names for notifications
1677
Will truncate long episode names and limit the amount of
1678
episodes displayed (max_episodes=10).
1680
The episode_list parameter should be a list of strings.
1682
MAX_TITLE_LENGTH = 100
1685
for title in episode_list[:min(len(episode_list), max_episodes)]:
1686
if len(title) > MAX_TITLE_LENGTH:
1687
middle = (MAX_TITLE_LENGTH/2)-2
1688
title = '%s...%s' % (title[0:middle], title[-middle:])
1689
result.append(saxutils.escape(title))
1692
more_episodes = len(episode_list) - max_episodes
1693
if more_episodes > 0:
1694
result.append('(...')
1695
result.append(N_('%(count)d more episode', '%(count)d more episodes', more_episodes) % {'count':more_episodes})
1696
result.append('...)')
1698
return (''.join(result)).strip()
1700
def _for_each_task_set_status(self, tasks, status, force_start=False):
1701
episode_urls = set()
1702
model = self.treeDownloads.get_model()
1703
for row_reference, task in tasks:
1704
if status == download.DownloadTask.QUEUED:
1705
# Only queue task when its paused/failed/cancelled (or forced)
1706
if task.status in (task.PAUSED, task.FAILED, task.CANCELLED) or force_start:
1707
self.download_queue_manager.add_task(task, force_start)
1708
self.enable_download_list_update()
1709
elif status == download.DownloadTask.CANCELLED:
1710
# Cancelling a download allowed when downloading/queued
1711
if task.status in (task.QUEUED, task.DOWNLOADING):
1712
task.status = status
1713
# Cancelling paused/failed downloads requires a call to .run()
1714
elif task.status in (task.PAUSED, task.FAILED):
1715
task.status = status
1716
# Call run, so the partial file gets deleted
1718
elif status == download.DownloadTask.PAUSED:
1719
# Pausing a download only when queued/downloading
1720
if task.status in (task.DOWNLOADING, task.QUEUED):
1721
task.status = status
1722
elif status is None:
1723
# Remove the selected task - cancel downloading/queued tasks
1724
if task.status in (task.QUEUED, task.DOWNLOADING):
1725
task.status = task.CANCELLED
1726
model.remove(model.get_iter(row_reference.get_path()))
1727
# Remember the URL, so we can tell the UI to update
1729
# We don't "see" this task anymore - remove it;
1730
# this is needed, so update_episode_list_icons()
1731
# below gets the correct list of "seen" tasks
1732
self.download_tasks_seen.remove(task)
1733
except KeyError, key_error:
1734
log('Cannot remove task from "seen" list: %s', task, sender=self)
1735
episode_urls.add(task.url)
1736
# Tell the task that it has been removed (so it can clean up)
1737
task.removed_from_list()
1739
# We can (hopefully) simply set the task status here
1740
task.status = status
1741
# Tell the podcasts tab to update icons for our removed podcasts
1742
self.update_episode_list_icons(episode_urls)
1743
# Update the tab title and downloads list
1744
self.update_downloads_list()
1746
def treeview_downloads_show_context_menu(self, treeview, event):
1747
model, paths = self.treeview_handle_context_menu_click(treeview, event)
1749
if not hasattr(treeview, 'is_rubber_banding_active'):
1752
return not treeview.is_rubber_banding_active()
1754
if event.button == self.context_menu_mouse_button:
1755
selected_tasks, can_queue, can_cancel, can_pause, can_remove, can_force = \
1756
self.downloads_list_get_selection(model, paths)
1758
def make_menu_item(label, stock_id, tasks, status, sensitive, force_start=False):
1759
# This creates a menu item for selection-wide actions
1760
item = gtk.ImageMenuItem(label)
1761
item.set_image(gtk.image_new_from_stock(stock_id, gtk.ICON_SIZE_MENU))
1762
item.connect('activate', lambda item: self._for_each_task_set_status(tasks, status, force_start))
1763
item.set_sensitive(sensitive)
1764
return self.set_finger_friendly(item)
1768
item = gtk.ImageMenuItem(_('Episode details'))
1769
item.set_image(gtk.image_new_from_stock(gtk.STOCK_INFO, gtk.ICON_SIZE_MENU))
1770
if len(selected_tasks) == 1:
1771
row_reference, task = selected_tasks[0]
1772
episode = task.episode
1773
item.connect('activate', lambda item: self.show_episode_shownotes(episode))
1775
item.set_sensitive(False)
1776
menu.append(self.set_finger_friendly(item))
1777
menu.append(gtk.SeparatorMenuItem())
1779
menu.append(make_menu_item(_('Start download now'), gtk.STOCK_GO_DOWN, selected_tasks, download.DownloadTask.QUEUED, True, True))
1781
menu.append(make_menu_item(_('Download'), gtk.STOCK_GO_DOWN, selected_tasks, download.DownloadTask.QUEUED, can_queue, False))
1782
menu.append(make_menu_item(_('Cancel'), gtk.STOCK_CANCEL, selected_tasks, download.DownloadTask.CANCELLED, can_cancel))
1783
menu.append(make_menu_item(_('Pause'), gtk.STOCK_MEDIA_PAUSE, selected_tasks, download.DownloadTask.PAUSED, can_pause))
1784
menu.append(gtk.SeparatorMenuItem())
1785
menu.append(make_menu_item(_('Remove from list'), gtk.STOCK_REMOVE, selected_tasks, None, can_remove))
1787
if gpodder.ui.maemo or self.config.enable_fingerscroll:
1788
# Because we open the popup on left-click for Maemo,
1789
# we also include a non-action to close the menu
1790
menu.append(gtk.SeparatorMenuItem())
1791
item = gtk.ImageMenuItem(_('Close this menu'))
1792
item.set_image(gtk.image_new_from_stock(gtk.STOCK_CLOSE, gtk.ICON_SIZE_MENU))
1794
menu.append(self.set_finger_friendly(item))
1797
menu.popup(None, None, None, event.button, event.time)
1800
def treeview_channels_show_context_menu(self, treeview, event):
1801
model, paths = self.treeview_handle_context_menu_click(treeview, event)
1805
# Check for valid channel id, if there's no id then
1806
# assume that it is a proxy channel or equivalent
1807
# and cannot be operated with right click
1808
if self.active_channel.id is None:
1811
if event.button == 3:
1816
item = gtk.ImageMenuItem( _('Update podcast'))
1817
item.set_image(gtk.image_new_from_stock(gtk.STOCK_REFRESH, gtk.ICON_SIZE_MENU))
1818
item.connect('activate', self.on_itemUpdateChannel_activate)
1819
item.set_sensitive(not self.updating_feed_cache)
1822
menu.append(gtk.SeparatorMenuItem())
1824
item = gtk.CheckMenuItem(_('Keep episodes'))
1825
item.set_active(self.active_channel.channel_is_locked)
1826
item.connect('activate', self.on_channel_toggle_lock_activate)
1827
menu.append(self.set_finger_friendly(item))
1829
item = gtk.ImageMenuItem(_('Remove podcast'))
1830
item.set_image(gtk.image_new_from_stock(gtk.STOCK_DELETE, gtk.ICON_SIZE_MENU))
1831
item.connect( 'activate', self.on_itemRemoveChannel_activate)
1834
if self.config.device_type != 'none':
1835
item = gtk.MenuItem(_('Synchronize to device'))
1836
item.connect('activate', lambda item: self.on_sync_to_ipod_activate(item, self.active_channel.get_downloaded_episodes(), force_played=False))
1839
menu.append( gtk.SeparatorMenuItem())
1841
item = gtk.ImageMenuItem(_('Podcast details'))
1842
item.set_image(gtk.image_new_from_stock(gtk.STOCK_INFO, gtk.ICON_SIZE_MENU))
1843
item.connect('activate', self.on_itemEditChannel_activate)
1847
# Disable tooltips while we are showing the menu, so
1848
# the tooltip will not appear over the menu
1849
self.treeview_allow_tooltips(self.treeChannels, False)
1850
menu.connect('deactivate', lambda menushell: self.treeview_allow_tooltips(self.treeChannels, True))
1851
menu.popup( None, None, None, event.button, event.time)
1855
def on_itemClose_activate(self, widget):
1856
if self.tray_icon is not None:
1857
self.iconify_main_window()
1859
self.on_gPodder_delete_event(widget)
1861
def cover_file_removed(self, channel_url):
1863
The Cover Downloader calls this when a previously-
1864
available cover has been removed from the disk. We
1865
have to update our model to reflect this change.
1867
self.podcast_list_model.delete_cover_by_url(channel_url)
1869
def cover_download_finished(self, channel, pixbuf):
1871
The Cover Downloader calls this when it has finished
1872
downloading (or registering, if already downloaded)
1873
a new channel cover, which is ready for displaying.
1875
self.podcast_list_model.add_cover_by_channel(channel, pixbuf)
1877
def save_episodes_as_file(self, episodes):
1878
for episode in episodes:
1879
self.save_episode_as_file(episode)
1881
def save_episode_as_file(self, episode):
1882
PRIVATE_FOLDER_ATTRIBUTE = '_save_episodes_as_file_folder'
1883
if episode.was_downloaded(and_exists=True):
1884
folder = getattr(self, PRIVATE_FOLDER_ATTRIBUTE, None)
1885
copy_from = episode.local_filename(create=False)
1886
assert copy_from is not None
1887
copy_to = util.sanitize_filename(episode.sync_filename(\
1888
self.config.custom_sync_name_enabled, \
1889
self.config.custom_sync_name))
1890
(result, folder) = self.show_copy_dialog(src_filename=copy_from, dst_filename=copy_to, dst_directory=folder)
1891
setattr(self, PRIVATE_FOLDER_ATTRIBUTE, folder)
1893
def copy_episodes_bluetooth(self, episodes):
1894
episodes_to_copy = [e for e in episodes if e.was_downloaded(and_exists=True)]
1896
if gpodder.ui.maemo:
1897
util.bluetooth_send_files_maemo([e.local_filename(create=False) \
1898
for e in episodes_to_copy])
1901
def convert_and_send_thread(episode):
1902
for episode in episodes:
1903
filename = episode.local_filename(create=False)
1904
assert filename is not None
1905
destfile = os.path.join(tempfile.gettempdir(), \
1906
util.sanitize_filename(episode.sync_filename(self.config.custom_sync_name_enabled, self.config.custom_sync_name)))
1907
(base, ext) = os.path.splitext(filename)
1908
if not destfile.endswith(ext):
1912
shutil.copyfile(filename, destfile)
1913
util.bluetooth_send_file(destfile)
1915
log('Cannot copy "%s" to "%s".', filename, destfile, sender=self)
1916
self.notification(_('Error converting file.'), _('Bluetooth file transfer'), important=True)
1918
util.delete_file(destfile)
1920
threading.Thread(target=convert_and_send_thread, args=[episodes_to_copy]).start()
1922
def get_device_name(self):
1923
if self.config.device_type == 'ipod':
1925
elif self.config.device_type in ('filesystem', 'mtp'):
1926
return _('MP3 player')
1928
return '(unknown device)'
1930
def _treeview_button_released(self, treeview, event):
1931
xpos, ypos = TreeViewHelper.get_button_press_event(treeview)
1932
dy = int(abs(event.y-ypos))
1933
dx = int(event.x-xpos)
1935
selection = treeview.get_selection()
1936
path = treeview.get_path_at_pos(int(event.x), int(event.y))
1937
if path is None or dy > 30:
1938
return (False, dx, dy)
1940
path, column, x, y = path
1941
selection.select_path(path)
1942
treeview.set_cursor(path)
1943
treeview.grab_focus()
1945
return (True, dx, dy)
1947
def treeview_channels_handle_gestures(self, treeview, event):
1948
if self.currently_updating:
1951
selected, dx, dy = self._treeview_button_released(treeview, event)
1954
if self.config.maemo_enable_gestures:
1956
self.on_itemUpdateChannel_activate()
1958
self.on_itemEditChannel_activate(treeview)
1962
def treeview_available_handle_gestures(self, treeview, event):
1963
selected, dx, dy = self._treeview_button_released(treeview, event)
1966
if self.config.maemo_enable_gestures:
1968
self.on_playback_selected_episodes(None)
1971
self.on_shownotes_selected_episodes(None)
1974
# Pass the event to the context menu handler for treeAvailable
1975
self.treeview_available_show_context_menu(treeview, event)
1979
def treeview_available_show_context_menu(self, treeview, event):
1980
model, paths = self.treeview_handle_context_menu_click(treeview, event)
1982
if not hasattr(treeview, 'is_rubber_banding_active'):
1985
return not treeview.is_rubber_banding_active()
1987
if event.button == self.context_menu_mouse_button:
1988
episodes = self.get_selected_episodes()
1989
any_locked = any(e.is_locked for e in episodes)
1990
any_played = any(e.is_played for e in episodes)
1991
one_is_new = any(e.state == gpodder.STATE_NORMAL and not e.is_played for e in episodes)
1992
downloaded = all(e.was_downloaded(and_exists=True) for e in episodes)
1993
downloading = any(self.episode_is_downloading(e) for e in episodes)
1997
(can_play, can_download, can_transfer, can_cancel, can_delete, open_instead_of_play) = self.play_or_download()
1999
if open_instead_of_play:
2000
item = gtk.ImageMenuItem(gtk.STOCK_OPEN)
2002
item = gtk.ImageMenuItem(gtk.STOCK_MEDIA_PLAY)
2004
item = gtk.ImageMenuItem(_('Stream'))
2005
item.set_image(gtk.image_new_from_stock(gtk.STOCK_MEDIA_PLAY, gtk.ICON_SIZE_MENU))
2007
item.set_sensitive(can_play and not downloading)
2008
item.connect('activate', self.on_playback_selected_episodes)
2009
menu.append(self.set_finger_friendly(item))
2012
item = gtk.ImageMenuItem(_('Download'))
2013
item.set_image(gtk.image_new_from_stock(gtk.STOCK_GO_DOWN, gtk.ICON_SIZE_MENU))
2014
item.set_sensitive(can_download)
2015
item.connect('activate', self.on_download_selected_episodes)
2016
menu.append(self.set_finger_friendly(item))
2018
item = gtk.ImageMenuItem(gtk.STOCK_CANCEL)
2019
item.connect('activate', self.on_item_cancel_download_activate)
2020
menu.append(self.set_finger_friendly(item))
2022
item = gtk.ImageMenuItem(gtk.STOCK_DELETE)
2023
item.set_sensitive(can_delete)
2024
item.connect('activate', self.on_btnDownloadedDelete_clicked)
2025
menu.append(self.set_finger_friendly(item))
2029
# Ok, this probably makes sense to only display for downloaded files
2031
menu.append(gtk.SeparatorMenuItem())
2032
share_item = gtk.MenuItem(_('Send to'))
2033
menu.append(self.set_finger_friendly(share_item))
2034
share_menu = gtk.Menu()
2036
item = gtk.ImageMenuItem(_('Local folder'))
2037
item.set_image(gtk.image_new_from_stock(gtk.STOCK_DIRECTORY, gtk.ICON_SIZE_MENU))
2038
item.connect('button-press-event', lambda w, ee: self.save_episodes_as_file(episodes))
2039
share_menu.append(self.set_finger_friendly(item))
2040
if self.bluetooth_available:
2041
item = gtk.ImageMenuItem(_('Bluetooth device'))
2042
if gpodder.ui.maemo:
2043
icon_name = ICON('qgn_list_filesys_bluetooth')
2045
icon_name = ICON('bluetooth')
2046
item.set_image(gtk.image_new_from_icon_name(icon_name, gtk.ICON_SIZE_MENU))
2047
item.connect('button-press-event', lambda w, ee: self.copy_episodes_bluetooth(episodes))
2048
share_menu.append(self.set_finger_friendly(item))
2050
item = gtk.ImageMenuItem(self.get_device_name())
2051
item.set_image(gtk.image_new_from_icon_name(ICON('multimedia-player'), gtk.ICON_SIZE_MENU))
2052
item.connect('button-press-event', lambda w, ee: self.on_sync_to_ipod_activate(w, episodes))
2053
share_menu.append(self.set_finger_friendly(item))
2055
share_item.set_submenu(share_menu)
2057
if (downloaded or one_is_new or can_download) and not downloading:
2058
menu.append(gtk.SeparatorMenuItem())
2060
item = gtk.CheckMenuItem(_('New'))
2061
item.set_active(True)
2062
item.connect('activate', lambda w: self.mark_selected_episodes_old())
2063
menu.append(self.set_finger_friendly(item))
2065
item = gtk.CheckMenuItem(_('New'))
2066
item.set_active(False)
2067
item.connect('activate', lambda w: self.mark_selected_episodes_new())
2068
menu.append(self.set_finger_friendly(item))
2071
item = gtk.CheckMenuItem(_('Played'))
2072
item.set_active(any_played)
2073
item.connect( 'activate', lambda w: self.on_item_toggle_played_activate( w, False, not any_played))
2074
menu.append(self.set_finger_friendly(item))
2076
item = gtk.CheckMenuItem(_('Keep episode'))
2077
item.set_active(any_locked)
2078
item.connect('activate', lambda w: self.on_item_toggle_lock_activate( w, False, not any_locked))
2079
menu.append(self.set_finger_friendly(item))
2081
menu.append(gtk.SeparatorMenuItem())
2082
# Single item, add episode information menu item
2083
item = gtk.ImageMenuItem(_('Episode details'))
2084
item.set_image(gtk.image_new_from_stock( gtk.STOCK_INFO, gtk.ICON_SIZE_MENU))
2085
item.connect('activate', lambda w: self.show_episode_shownotes(episodes[0]))
2086
menu.append(self.set_finger_friendly(item))
2088
if gpodder.ui.maemo or self.config.enable_fingerscroll:
2089
# Because we open the popup on left-click for Maemo,
2090
# we also include a non-action to close the menu
2091
menu.append(gtk.SeparatorMenuItem())
2092
item = gtk.ImageMenuItem(_('Close this menu'))
2093
item.set_image(gtk.image_new_from_stock(gtk.STOCK_CLOSE, gtk.ICON_SIZE_MENU))
2094
menu.append(self.set_finger_friendly(item))
2097
# Disable tooltips while we are showing the menu, so
2098
# the tooltip will not appear over the menu
2099
self.treeview_allow_tooltips(self.treeAvailable, False)
2100
menu.connect('deactivate', lambda menushell: self.treeview_allow_tooltips(self.treeAvailable, True))
2101
menu.popup( None, None, None, event.button, event.time)
2105
def set_title(self, new_title):
2106
if not gpodder.ui.fremantle:
2107
self.default_title = new_title
2108
self.gPodder.set_title(new_title)
2110
def update_episode_list_icons(self, urls=None, selected=False, all=False):
2112
Updates the status icons in the episode list.
2114
If urls is given, it should be a list of URLs
2115
of episodes that should be updated.
2117
If urls is None, set ONE OF selected, all to
2118
True (the former updates just the selected
2119
episodes and the latter updates all episodes).
2121
additional_args = (self.episode_is_downloading, \
2122
self.config.episode_list_descriptions and gpodder.ui.desktop, \
2123
self.config.episode_list_thumbnails and gpodder.ui.desktop)
2125
if urls is not None:
2126
# We have a list of URLs to walk through
2127
self.episode_list_model.update_by_urls(urls, *additional_args)
2128
elif selected and not all:
2129
# We should update all selected episodes
2130
selection = self.treeAvailable.get_selection()
2131
model, paths = selection.get_selected_rows()
2132
for path in reversed(paths):
2133
iter = model.get_iter(path)
2134
self.episode_list_model.update_by_filter_iter(iter, \
2136
elif all and not selected:
2137
# We update all (even the filter-hidden) episodes
2138
self.episode_list_model.update_all(*additional_args)
2140
# Wrong/invalid call - have to specify at least one parameter
2141
raise ValueError('Invalid call to update_episode_list_icons')
2143
def episode_list_status_changed(self, episodes):
2144
self.update_episode_list_icons(set(e.url for e in episodes))
2145
self.update_podcast_list_model(set(e.channel.url for e in episodes))
2148
def clean_up_downloads(self, delete_partial=False):
2149
# Clean up temporary files left behind by old gPodder versions
2150
temporary_files = glob.glob('%s/*/.tmp-*' % self.config.download_dir)
2153
temporary_files += glob.glob('%s/*/*.partial' % self.config.download_dir)
2155
for tempfile in temporary_files:
2156
util.delete_file(tempfile)
2158
# Clean up empty download folders and abandoned download folders
2159
download_dirs = glob.glob(os.path.join(self.config.download_dir, '*'))
2160
for ddir in download_dirs:
2161
if os.path.isdir(ddir) and False: # FIXME not db.channel_foldername_exists(os.path.basename(ddir)):
2162
globr = glob.glob(os.path.join(ddir, '*'))
2163
if len(globr) == 0 or (len(globr) == 1 and globr[0].endswith('/cover')):
2164
log('Stale download directory found: %s', os.path.basename(ddir), sender=self)
2165
shutil.rmtree(ddir, ignore_errors=True)
2167
def streaming_possible(self):
2168
if gpodder.ui.desktop:
2169
# User has to have a media player set on the Desktop, or else we
2170
# would probably open the browser when giving a URL to xdg-open..
2171
return (self.config.player and self.config.player != 'default')
2172
elif gpodder.ui.maemo:
2173
# On Maemo, the default is to use the Nokia Media Player, which is
2174
# already able to deal with HTTP URLs the right way, so we
2175
# unconditionally enable streaming always on Maemo
2180
def playback_episodes_for_real(self, episodes):
2181
groups = collections.defaultdict(list)
2182
for episode in episodes:
2183
file_type = episode.file_type()
2184
if file_type == 'video' and self.config.videoplayer and \
2185
self.config.videoplayer != 'default':
2186
player = self.config.videoplayer
2187
if gpodder.ui.diablo:
2188
# Use the wrapper script if it's installed to crop 3GP YouTube
2189
# videos to fit the screen (looks much nicer than w/ black border)
2190
if player == 'mplayer' and util.find_command('gpodder-mplayer'):
2191
player = 'gpodder-mplayer'
2192
elif gpodder.ui.fremantle and player == 'mplayer':
2193
player = 'mplayer -fs %F'
2194
elif file_type == 'audio' and self.config.player and \
2195
self.config.player != 'default':
2196
player = self.config.player
2200
# Mark episode as played in the database
2201
episode.mark(is_played=True)
2202
self.mygpo_client.on_playback([episode])
2204
filename = episode.local_filename(create=False)
2205
if filename is None or not os.path.exists(filename):
2206
filename = episode.url
2207
if youtube.is_video_link(filename):
2208
fmt_id = self.config.youtube_preferred_fmt_id
2209
if gpodder.ui.fremantle:
2211
filename = youtube.get_real_download_url(filename, fmt_id)
2213
# Determine the playback resume position - if the file
2214
# was played 100%, we simply start from the beginning
2215
resume_position = episode.current_position
2216
if resume_position == episode.total_time:
2219
# Only on Maemo 5, and only if the episode isn't finished yet
2220
if gpodder.ui.fremantle and not episode.is_finished():
2221
self.mafw_monitor.set_resume_point(filename, resume_position)
2223
# If Panucci is configured, use D-Bus on Maemo to call it
2224
if player == 'panucci':
2226
PANUCCI_NAME = 'org.panucci.panucciInterface'
2227
PANUCCI_PATH = '/panucciInterface'
2228
PANUCCI_INTF = 'org.panucci.panucciInterface'
2229
o = gpodder.dbus_session_bus.get_object(PANUCCI_NAME, PANUCCI_PATH)
2230
i = dbus.Interface(o, PANUCCI_INTF)
2232
def on_reply(*args):
2235
def error_handler(filename, err):
2236
log('Exception in D-Bus call: %s', str(err), \
2239
# Fallback: use the command line client
2240
for command in util.format_desktop_command('panucci', \
2242
log('Executing: %s', repr(command), sender=self)
2243
subprocess.Popen(command)
2245
on_error = lambda err: error_handler(filename, err)
2247
# This method only exists in Panucci > 0.9 ('new Panucci')
2248
i.playback_from(filename, resume_position, \
2249
reply_handler=on_reply, error_handler=on_error)
2251
continue # This file was handled by the D-Bus call
2252
except Exception, e:
2253
log('Error calling Panucci using D-Bus', sender=self, traceback=True)
2254
elif player == 'MediaBox' and gpodder.ui.maemo:
2256
MEDIABOX_NAME = 'de.pycage.mediabox'
2257
MEDIABOX_PATH = '/de/pycage/mediabox/control'
2258
MEDIABOX_INTF = 'de.pycage.mediabox.control'
2259
o = gpodder.dbus_session_bus.get_object(MEDIABOX_NAME, MEDIABOX_PATH)
2260
i = dbus.Interface(o, MEDIABOX_INTF)
2262
def on_reply(*args):
2266
log('Exception in D-Bus call: %s', str(err), \
2269
i.load(filename, '%s/x-unknown' % file_type, \
2270
reply_handler=on_reply, error_handler=on_error)
2272
continue # This file was handled by the D-Bus call
2273
except Exception, e:
2274
log('Error calling MediaBox using D-Bus', sender=self, traceback=True)
2276
groups[player].append(filename)
2278
# Open episodes with system default player
2279
if 'default' in groups:
2280
# Special-casing for a single episode when the object is a PDF
2281
# file - this is needed on Maemo 5, so we only use gui_open()
2282
# for single PDF files, but still use the built-in media player
2283
# with an M3U file for single audio/video files. (The Maemo 5
2284
# media player behaves differently when opening a single-file
2285
# M3U playlist compared to opening the single file directly.)
2286
if len(groups['default']) == 1:
2287
fn = groups['default'][0]
2288
# The list of extensions is taken from gui_open in util.py
2289
# where all special-cases of Maemo apps are listed
2290
for extension in ('.pdf', '.jpg', '.jpeg', '.png'):
2291
if fn.lower().endswith(extension):
2293
groups['default'] = []
2296
if gpodder.ui.maemo and groups['default']:
2297
# The Nokia Media Player app does not support receiving multiple
2298
# file names via D-Bus, so we simply place all file names into a
2299
# temporary M3U playlist and open that with the Media Player.
2300
m3u_filename = os.path.join(gpodder.home, 'gpodder_open_with.m3u')
2303
# Diablo's Player hates file:// URLs (Maemo bug 11647)
2304
if gpodder.ui.diablo:
2308
return 'file://' + urllib.quote(os.path.abspath(x))
2311
util.write_m3u_playlist(m3u_filename, \
2312
map(to_url, groups['default']), \
2314
util.gui_open(m3u_filename)
2316
for filename in groups['default']:
2317
log('Opening with system default: %s', filename, sender=self)
2318
util.gui_open(filename)
2319
del groups['default']
2320
elif gpodder.ui.maemo and groups:
2321
# When on Maemo and not opening with default, show a notification
2322
# (no startup notification for Panucci / MPlayer yet...)
2323
if len(episodes) == 1:
2324
text = _('Opening %s') % episodes[0].title
2326
count = len(episodes)
2327
text = N_('Opening %(count)d episode', 'Opening %(count)d episodes', count) % {'count':count}
2329
banner = hildon.hildon_banner_show_animation(self.gPodder, '', text)
2331
def destroy_banner_later(banner):
2334
gobject.timeout_add(5000, destroy_banner_later, banner)
2336
# For each type now, go and create play commands
2337
for group in groups:
2338
for command in util.format_desktop_command(group, groups[group]):
2339
log('Executing: %s', repr(command), sender=self)
2340
subprocess.Popen(command)
2342
# Persist episode status changes to the database
2345
# Flush updated episode status
2346
self.mygpo_client.flush()
2348
def playback_episodes(self, episodes):
2349
# We need to create a list, because we run through it more than once
2350
episodes = list(PodcastEpisode.sort_by_pubdate(e for e in episodes if \
2351
e.was_downloaded(and_exists=True) or self.streaming_possible()))
2354
self.playback_episodes_for_real(episodes)
2355
except Exception, e:
2356
log('Error in playback!', sender=self, traceback=True)
2357
if gpodder.ui.desktop:
2358
self.show_message(_('Please check your media player settings in the preferences dialog.'), \
2359
_('Error opening player'), widget=self.toolPreferences)
2361
self.show_message(_('Please check your media player settings in the preferences dialog.'))
2363
channel_urls = set()
2364
episode_urls = set()
2365
for episode in episodes:
2366
channel_urls.add(episode.channel.url)
2367
episode_urls.add(episode.url)
2368
self.update_episode_list_icons(episode_urls)
2369
self.update_podcast_list_model(channel_urls)
2371
def play_or_download(self):
2372
if not gpodder.ui.fremantle:
2373
if self.wNotebook.get_current_page() > 0:
2374
if gpodder.ui.desktop:
2375
self.toolCancel.set_sensitive(True)
2378
if self.currently_updating:
2379
return (False, False, False, False, False, False)
2381
( can_play, can_download, can_transfer, can_cancel, can_delete ) = (False,)*5
2382
( is_played, is_locked ) = (False,)*2
2384
open_instead_of_play = False
2386
selection = self.treeAvailable.get_selection()
2387
if selection.count_selected_rows() > 0:
2388
(model, paths) = selection.get_selected_rows()
2392
episode = model.get_value(model.get_iter(path), EpisodeListModel.C_EPISODE)
2393
except TypeError, te:
2394
log('Invalid episode at path %s', str(path), sender=self)
2397
if episode.file_type() not in ('audio', 'video'):
2398
open_instead_of_play = True
2400
if episode.was_downloaded():
2401
can_play = episode.was_downloaded(and_exists=True)
2402
is_played = episode.is_played
2403
is_locked = episode.is_locked
2407
if self.episode_is_downloading(episode):
2412
can_download = can_download and not can_cancel
2413
can_play = self.streaming_possible() or (can_play and not can_cancel and not can_download)
2414
can_transfer = can_play and self.config.device_type != 'none' and not can_cancel and not can_download and not open_instead_of_play
2415
can_delete = not can_cancel
2417
if gpodder.ui.desktop:
2418
if open_instead_of_play:
2419
self.toolPlay.set_stock_id(gtk.STOCK_OPEN)
2421
self.toolPlay.set_stock_id(gtk.STOCK_MEDIA_PLAY)
2422
self.toolPlay.set_sensitive( can_play)
2423
self.toolDownload.set_sensitive( can_download)
2424
self.toolTransfer.set_sensitive( can_transfer)
2425
self.toolCancel.set_sensitive( can_cancel)
2427
if not gpodder.ui.fremantle:
2428
self.item_cancel_download.set_sensitive(can_cancel)
2429
self.itemDownloadSelected.set_sensitive(can_download)
2430
self.itemOpenSelected.set_sensitive(can_play)
2431
self.itemPlaySelected.set_sensitive(can_play)
2432
self.itemDeleteSelected.set_sensitive(can_delete)
2433
self.item_toggle_played.set_sensitive(can_play)
2434
self.item_toggle_lock.set_sensitive(can_play)
2435
self.itemOpenSelected.set_visible(open_instead_of_play)
2436
self.itemPlaySelected.set_visible(not open_instead_of_play)
2438
return (can_play, can_download, can_transfer, can_cancel, can_delete, open_instead_of_play)
2440
def on_cbMaxDownloads_toggled(self, widget, *args):
2441
self.spinMaxDownloads.set_sensitive(self.cbMaxDownloads.get_active())
2443
def on_cbLimitDownloads_toggled(self, widget, *args):
2444
self.spinLimitDownloads.set_sensitive(self.cbLimitDownloads.get_active())
2446
def episode_new_status_changed(self, urls):
2447
self.update_podcast_list_model()
2448
self.update_episode_list_icons(urls)
2450
def update_podcast_list_model(self, urls=None, selected=False, select_url=None):
2451
"""Update the podcast list treeview model
2453
If urls is given, it should list the URLs of each
2454
podcast that has to be updated in the list.
2456
If selected is True, only update the model contents
2457
for the currently-selected podcast - nothing more.
2459
The caller can optionally specify "select_url",
2460
which is the URL of the podcast that is to be
2461
selected in the list after the update is complete.
2462
This only works if the podcast list has to be
2463
reloaded; i.e. something has been added or removed
2464
since the last update of the podcast list).
2466
selection = self.treeChannels.get_selection()
2467
model, iter = selection.get_selected()
2469
if self.config.podcast_list_view_all and not self.channel_list_changed:
2470
# Update "all episodes" view in any case (if enabled)
2471
self.podcast_list_model.update_first_row()
2474
# very cheap! only update selected channel
2475
if iter is not None:
2476
# If we have selected the "all episodes" view, we have
2477
# to update all channels for selected episodes:
2478
if self.config.podcast_list_view_all and \
2479
self.podcast_list_model.iter_is_first_row(iter):
2480
urls = self.get_podcast_urls_from_selected_episodes()
2481
self.podcast_list_model.update_by_urls(urls)
2483
# Otherwise just update the selected row (a podcast)
2484
self.podcast_list_model.update_by_filter_iter(iter)
2485
elif not self.channel_list_changed:
2486
# we can keep the model, but have to update some
2488
# still cheaper than reloading the whole list
2489
self.podcast_list_model.update_all()
2491
# ok, we got a bunch of urls to update
2492
self.podcast_list_model.update_by_urls(urls)
2494
if model and iter and select_url is None:
2495
# Get the URL of the currently-selected podcast
2496
select_url = model.get_value(iter, PodcastListModel.C_URL)
2498
# Update the podcast list model with new channels
2499
self.podcast_list_model.set_channels(self.db, self.config, self.channels)
2502
selected_iter = model.get_iter_first()
2503
# Find the previously-selected URL in the new
2504
# model if we have an URL (else select first)
2505
if select_url is not None:
2506
pos = model.get_iter_first()
2507
while pos is not None:
2508
url = model.get_value(pos, PodcastListModel.C_URL)
2509
if url == select_url:
2512
pos = model.iter_next(pos)
2514
if not gpodder.ui.maemo:
2515
if selected_iter is not None:
2516
selection.select_iter(selected_iter)
2517
self.on_treeChannels_cursor_changed(self.treeChannels)
2519
log('Cannot select podcast in list', traceback=True, sender=self)
2520
self.channel_list_changed = False
2522
def episode_is_downloading(self, episode):
2523
"""Returns True if the given episode is being downloaded at the moment"""
2527
return episode.url in (task.url for task in self.download_tasks_seen if task.status in (task.DOWNLOADING, task.QUEUED, task.PAUSED))
2529
def on_episode_list_filter_changed(self, has_episodes):
2530
if gpodder.ui.fremantle:
2532
self.episodes_window.empty_label.hide()
2533
self.episodes_window.pannablearea.show()
2535
if self.config.episode_list_view_mode != \
2536
EpisodeListModel.VIEW_ALL:
2537
text = _('No episodes in current view')
2539
text = _('No episodes available')
2540
self.episodes_window.empty_label.set_text(text)
2541
self.episodes_window.pannablearea.hide()
2542
self.episodes_window.empty_label.show()
2544
def update_episode_list_model(self):
2545
if self.channels and self.active_channel is not None:
2546
if gpodder.ui.fremantle:
2547
hildon.hildon_gtk_window_set_progress_indicator(self.episodes_window.main_window, True)
2549
self.currently_updating = True
2550
self.episode_list_model.clear()
2551
if gpodder.ui.fremantle:
2552
self.episodes_window.pannablearea.hide()
2553
self.episodes_window.empty_label.set_text(_('Loading episodes'))
2554
self.episodes_window.empty_label.show()
2557
additional_args = (self.episode_is_downloading, \
2558
self.config.episode_list_descriptions and gpodder.ui.desktop, \
2559
self.config.episode_list_thumbnails and gpodder.ui.desktop)
2560
self.episode_list_model.replace_from_channel(self.active_channel, *additional_args)
2562
self.treeAvailable.get_selection().unselect_all()
2563
self.treeAvailable.scroll_to_point(0, 0)
2565
self.currently_updating = False
2566
self.play_or_download()
2568
if gpodder.ui.fremantle:
2569
hildon.hildon_gtk_window_set_progress_indicator(\
2570
self.episodes_window.main_window, False)
2572
util.idle_add(update)
2574
self.episode_list_model.clear()
2576
@dbus.service.method(gpodder.dbus_interface)
2577
def offer_new_episodes(self, channels=None):
2578
if gpodder.ui.fremantle:
2579
# Assume that when this function is called that the
2580
# notification is not shown anymore (Maemo bug 11345)
2581
self._fremantle_notification_visible = False
2583
new_episodes = self.get_new_episodes(channels)
2585
self.new_episodes_show(new_episodes)
2589
def add_podcast_list(self, urls, auth_tokens=None):
2590
"""Subscribe to a list of podcast given their URLs
2592
If auth_tokens is given, it should be a dictionary
2593
mapping URLs to (username, password) tuples."""
2595
if auth_tokens is None:
2598
# Sort and split the URL list into five buckets
2599
queued, failed, existing, worked, authreq = [], [], [], [], []
2600
for input_url in urls:
2601
url = util.normalize_feed_url(input_url)
2603
# Fail this one because the URL is not valid
2604
failed.append(input_url)
2605
elif self.podcast_list_model.get_filter_path_from_url(url) is not None:
2606
# A podcast already exists in the list for this URL
2607
existing.append(url)
2609
# This URL has survived the first round - queue for add
2611
if url != input_url and input_url in auth_tokens:
2612
auth_tokens[url] = auth_tokens[input_url]
2617
progress = ProgressIndicator(_('Adding podcasts'), \
2618
_('Please wait while episode information is downloaded.'), \
2619
parent=self.get_dialog_parent())
2621
def on_after_update():
2622
progress.on_finished()
2623
# Report already-existing subscriptions to the user
2625
title = _('Existing subscriptions skipped')
2626
message = _('You are already subscribed to these podcasts:') \
2627
+ '\n\n' + '\n'.join(saxutils.escape(url) for url in existing)
2628
self.show_message(message, title, widget=self.treeChannels)
2630
# Report subscriptions that require authentication
2634
title = _('Podcast requires authentication')
2635
message = _('Please login to %s:') % (saxutils.escape(url),)
2636
success, auth_tokens = self.show_login_dialog(title, message)
2638
retry_podcasts[url] = auth_tokens
2640
# Stop asking the user for more login data
2643
error_messages[url] = _('Authentication failed')
2647
# If we have authentication data to retry, do so here
2649
self.add_podcast_list(retry_podcasts.keys(), retry_podcasts)
2651
# Report website redirections
2652
for url in redirections:
2653
title = _('Website redirection detected')
2654
message = _('The URL %(url)s redirects to %(target)s.') \
2655
+ '\n\n' + _('Do you want to visit the website now?')
2656
message = message % {'url': url, 'target': redirections[url]}
2657
if self.show_confirmation(message, title):
2658
util.open_website(url)
2662
# Report failed subscriptions to the user
2664
title = _('Could not add some podcasts')
2665
message = _('Some podcasts could not be added to your list:') \
2666
+ '\n\n' + '\n'.join(saxutils.escape('%s: %s' % (url, \
2667
error_messages.get(url, _('Unknown')))) for url in failed)
2668
self.show_message(message, title, important=True)
2670
# Upload subscription changes to gpodder.net
2671
self.mygpo_client.on_subscribe(worked)
2673
# If at least one podcast has been added, save and update all
2674
if self.channel_list_changed:
2675
# Fix URLs if mygpo has rewritten them
2676
self.rewrite_urls_mygpo()
2678
self.save_channels_opml()
2680
# If only one podcast was added, select it after the update
2681
if len(worked) == 1:
2686
# Update the list of subscribed podcasts
2687
self.update_feed_cache(force_update=False, select_url_afterwards=url)
2688
self.update_podcasts_tab()
2690
# Offer to download new episodes
2692
for podcast in self.channels:
2693
if podcast.url in worked:
2694
episodes.extend(podcast.get_all_episodes())
2697
episodes = list(PodcastEpisode.sort_by_pubdate(episodes, \
2699
self.new_episodes_show(episodes, \
2700
selected=[e.check_is_new() for e in episodes])
2704
# After the initial sorting and splitting, try all queued podcasts
2705
length = len(queued)
2706
for index, url in enumerate(queued):
2707
progress.on_progress(float(index)/float(length))
2708
progress.on_message(url)
2709
log('QUEUE RUNNER: %s', url, sender=self)
2711
# The URL is valid and does not exist already - subscribe!
2712
channel = PodcastChannel.load(self.db, url=url, create=True, \
2713
authentication_tokens=auth_tokens.get(url, None), \
2714
max_episodes=self.config.max_episodes_per_feed, \
2715
download_dir=self.config.download_dir, \
2716
allow_empty_feeds=self.config.allow_empty_feeds, \
2717
mimetype_prefs=self.config.mimetype_prefs)
2720
username, password = util.username_password_from_url(url)
2721
except ValueError, ve:
2722
username, password = (None, None)
2724
if username is not None and channel.username is None and \
2725
password is not None and channel.password is None:
2726
channel.username = username
2727
channel.password = password
2730
self._update_cover(channel)
2731
except feedcore.AuthenticationRequired:
2732
if url in auth_tokens:
2733
# Fail for wrong authentication data
2734
error_messages[url] = _('Authentication failed')
2737
# Queue for login dialog later
2740
except feedcore.WifiLogin, error:
2741
redirections[url] = error.data
2743
error_messages[url] = _('Redirection detected')
2745
except Exception, e:
2746
log('Subscription error: %s', e, traceback=True, sender=self)
2747
error_messages[url] = str(e)
2751
assert channel is not None
2752
worked.append(channel.url)
2753
self.channels.append(channel)
2754
self.channel_list_changed = True
2755
util.idle_add(on_after_update)
2756
threading.Thread(target=thread_proc).start()
2758
def save_channels_opml(self):
2759
exporter = opml.Exporter(gpodder.subscription_file)
2760
return exporter.write(self.channels)
2762
def find_episode(self, podcast_url, episode_url):
2763
"""Find an episode given its podcast and episode URL
2765
The function will return a PodcastEpisode object if
2766
the episode is found, or None if it's not found.
2768
for podcast in self.channels:
2769
if podcast_url == podcast.url:
2770
for episode in podcast.get_all_episodes():
2771
if episode_url == episode.url:
2776
def process_received_episode_actions(self, updated_urls):
2777
"""Process/merge episode actions from gpodder.net
2779
This function will merge all changes received from
2780
the server to the local database and update the
2781
status of the affected episodes as necessary.
2783
indicator = ProgressIndicator(_('Merging episode actions'), \
2784
_('Episode actions from gpodder.net are merged.'), \
2785
False, self.get_dialog_parent())
2787
for idx, action in enumerate(self.mygpo_client.get_episode_actions(updated_urls)):
2788
if action.action == 'play':
2789
episode = self.find_episode(action.podcast_url, \
2792
if episode is not None:
2793
log('Play action for %s', episode.url, sender=self)
2794
episode.mark(is_played=True)
2796
if action.timestamp > episode.current_position_updated and \
2797
action.position is not None:
2798
log('Updating position for %s', episode.url, sender=self)
2799
episode.current_position = action.position
2800
episode.current_position_updated = action.timestamp
2803
log('Updating total time for %s', episode.url, sender=self)
2804
episode.total_time = action.total
2807
elif action.action == 'delete':
2808
episode = self.find_episode(action.podcast_url, \
2811
if episode is not None:
2812
if not episode.was_downloaded(and_exists=True):
2813
# Set the episode to a "deleted" state
2814
log('Marking as deleted: %s', episode.url, sender=self)
2815
episode.delete_from_disk()
2818
indicator.on_message(N_('%(count)d action processed', '%(count)d actions processed', idx) % {'count':idx})
2819
gtk.main_iteration(False)
2821
indicator.on_finished()
2825
def update_feed_cache_finish_callback(self, updated_urls=None, select_url_afterwards=None):
2827
self.updating_feed_cache = False
2829
self.channels = PodcastChannel.load_from_db(self.db, self.config.download_dir)
2831
# Process received episode actions for all updated URLs
2832
self.process_received_episode_actions(updated_urls)
2834
self.channel_list_changed = True
2835
self.update_podcast_list_model(select_url=select_url_afterwards)
2837
# Only search for new episodes in podcasts that have been
2838
# updated, not in other podcasts (for single-feed updates)
2839
episodes = self.get_new_episodes([c for c in self.channels if c.url in updated_urls])
2841
if gpodder.ui.fremantle:
2842
self.fancy_progress_bar.hide()
2843
self.button_subscribe.set_sensitive(True)
2844
self.button_refresh.set_sensitive(True)
2845
hildon.hildon_gtk_window_set_progress_indicator(self.main_window, False)
2846
hildon.hildon_gtk_window_set_progress_indicator(self.episodes_window.main_window, False)
2847
self.update_podcasts_tab()
2848
self.update_episode_list_model()
2849
if self.feed_cache_update_cancelled:
2852
def application_in_foreground():
2854
return any(w.get_property('is-topmost') for w in hildon.WindowStack.get_default().get_windows())
2855
except Exception, e:
2856
log('Could not determine is-topmost', traceback=True)
2857
# When in doubt, assume not in foreground
2861
if self.config.auto_download == 'quiet' and not self.config.auto_update_feeds:
2862
# New episodes found, but we should do nothing
2863
self.show_message(_('New episodes are available.'))
2864
elif self.config.auto_download == 'always' or \
2865
(self.config.auto_download == 'wifi' and \
2866
self.network_manager.connection_is_wlan()):
2867
count = len(episodes)
2868
title = N_('Downloading %(count)d new episode.', 'Downloading %(count)d new episodes.', count) % {'count':count}
2869
self.show_message(title)
2870
self.download_episode_list(episodes)
2871
elif self.config.auto_download == 'queue':
2872
self.show_message(_('New episodes have been added to the download list.'))
2873
self.download_episode_list_paused(episodes)
2874
elif application_in_foreground():
2875
if not self._fremantle_notification_visible:
2876
self.new_episodes_show(episodes)
2877
elif not self._fremantle_notification_visible:
2880
pynotify.init('gPodder')
2881
n = pynotify.Notification('gPodder', _('New episodes available'), 'gpodder')
2882
n.set_urgency(pynotify.URGENCY_CRITICAL)
2883
n.set_hint('dbus-callback-default', ' '.join([
2884
gpodder.dbus_bus_name,
2885
gpodder.dbus_gui_object_path,
2886
gpodder.dbus_interface,
2887
'offer_new_episodes',
2889
n.set_category('gpodder-new-episodes')
2891
self._fremantle_notification_visible = True
2892
except Exception, e:
2893
log('Error: %s', str(e), sender=self, traceback=True)
2894
self.new_episodes_show(episodes)
2895
self._fremantle_notification_visible = False
2896
elif not self.config.auto_update_feeds:
2897
self.show_message(_('No new episodes. Please check for new episodes later.'))
2901
self.tray_icon.set_status()
2903
if self.feed_cache_update_cancelled:
2904
# The user decided to abort the feed update
2905
self.show_update_feeds_buttons()
2907
# Nothing new here - but inform the user
2908
self.pbFeedUpdate.set_fraction(1.0)
2909
self.pbFeedUpdate.set_text(_('No new episodes'))
2910
self.feed_cache_update_cancelled = True
2911
self.btnCancelFeedUpdate.show()
2912
self.btnCancelFeedUpdate.set_sensitive(True)
2913
self.itemUpdate.set_sensitive(True)
2914
if gpodder.ui.maemo:
2915
# btnCancelFeedUpdate is a ToolButton on Maemo
2916
self.btnCancelFeedUpdate.set_stock_id(gtk.STOCK_APPLY)
2918
# btnCancelFeedUpdate is a normal gtk.Button
2919
self.btnCancelFeedUpdate.set_image(gtk.image_new_from_stock(gtk.STOCK_APPLY, gtk.ICON_SIZE_BUTTON))
2921
count = len(episodes)
2922
# New episodes are available
2923
self.pbFeedUpdate.set_fraction(1.0)
2924
# Are we minimized and should we auto download?
2925
if (self.is_iconified() and (self.config.auto_download == 'minimized')) or (self.config.auto_download == 'always'):
2926
self.download_episode_list(episodes)
2927
title = N_('Downloading %(count)d new episode.', 'Downloading %(count)d new episodes.', count) % {'count':count}
2928
self.show_message(title, _('New episodes available'), widget=self.labelDownloads)
2929
self.show_update_feeds_buttons()
2930
elif self.config.auto_download == 'queue':
2931
self.download_episode_list_paused(episodes)
2932
title = N_('%(count)d new episode added to download list.', '%(count)d new episodes added to download list.', count) % {'count':count}
2933
self.show_message(title, _('New episodes available'), widget=self.labelDownloads)
2934
self.show_update_feeds_buttons()
2936
self.show_update_feeds_buttons()
2937
# New episodes are available and we are not minimized
2938
if not self.config.do_not_show_new_episodes_dialog:
2939
self.new_episodes_show(episodes, notification=True)
2941
message = N_('%(count)d new episode available', '%(count)d new episodes available', count) % {'count':count}
2942
self.pbFeedUpdate.set_text(message)
2944
def _update_cover(self, channel):
2945
if channel is not None and not os.path.exists(channel.cover_file) and channel.image:
2946
self.cover_downloader.request_cover(channel)
2948
def update_feed_cache_proc(self, channels, select_url_afterwards):
2949
total = len(channels)
2951
for updated, channel in enumerate(channels):
2952
if not self.feed_cache_update_cancelled:
2954
channel.update(max_episodes=self.config.max_episodes_per_feed, \
2955
mimetype_prefs=self.config.mimetype_prefs)
2956
self._update_cover(channel)
2957
except Exception, e:
2958
d = {'url': saxutils.escape(channel.url), 'message': saxutils.escape(str(e))}
2960
message = _('Error while updating %(url)s: %(message)s')
2962
message = _('The feed at %(url)s could not be updated.')
2963
self.notification(message % d, _('Error while updating feed'), widget=self.treeChannels)
2964
log('Error: %s', str(e), sender=self, traceback=True)
2966
if self.feed_cache_update_cancelled:
2969
# By the time we get here the update may have already been cancelled
2970
if not self.feed_cache_update_cancelled:
2971
def update_progress():
2972
d = {'podcast': channel.title, 'position': updated+1, 'total': total}
2973
progression = _('Updated %(podcast)s (%(position)d/%(total)d)') % d
2974
self.pbFeedUpdate.set_text(progression)
2976
self.tray_icon.set_status(self.tray_icon.STATUS_UPDATING_FEED_CACHE, progression)
2977
self.pbFeedUpdate.set_fraction(float(updated+1)/float(total))
2978
util.idle_add(update_progress)
2980
updated_urls = [c.url for c in channels]
2981
util.idle_add(self.update_feed_cache_finish_callback, updated_urls, select_url_afterwards)
2983
def show_update_feeds_buttons(self):
2984
# Make sure that the buttons for updating feeds
2985
# appear - this should happen after a feed update
2986
if gpodder.ui.maemo:
2987
self.btnUpdateSelectedFeed.show()
2988
self.toolFeedUpdateProgress.hide()
2989
self.btnCancelFeedUpdate.hide()
2990
self.btnCancelFeedUpdate.set_is_important(False)
2991
self.btnCancelFeedUpdate.set_stock_id(gtk.STOCK_CLOSE)
2992
self.toolbarSpacer.set_expand(True)
2993
self.toolbarSpacer.set_draw(False)
2995
self.hboxUpdateFeeds.hide()
2996
self.btnUpdateFeeds.show()
2997
self.itemUpdate.set_sensitive(True)
2998
self.itemUpdateChannel.set_sensitive(True)
3000
def on_btnCancelFeedUpdate_clicked(self, widget):
3001
if not self.feed_cache_update_cancelled:
3002
self.pbFeedUpdate.set_text(_('Cancelling...'))
3003
self.feed_cache_update_cancelled = True
3004
if not gpodder.ui.fremantle:
3005
self.btnCancelFeedUpdate.set_sensitive(False)
3006
elif not gpodder.ui.fremantle:
3007
self.show_update_feeds_buttons()
3009
def update_feed_cache(self, channels=None, force_update=True, select_url_afterwards=None):
3010
if self.updating_feed_cache:
3011
if gpodder.ui.fremantle:
3012
self.feed_cache_update_cancelled = True
3015
if not force_update:
3016
self.channels = PodcastChannel.load_from_db(self.db, self.config.download_dir)
3017
self.channel_list_changed = True
3018
self.update_podcast_list_model(select_url=select_url_afterwards)
3021
# Fix URLs if mygpo has rewritten them
3022
self.rewrite_urls_mygpo()
3024
self.updating_feed_cache = True
3026
if channels is None:
3027
# Only update podcasts for which updates are enabled
3028
channels = [c for c in self.channels if c.feed_update_enabled]
3030
if gpodder.ui.fremantle:
3031
hildon.hildon_gtk_window_set_progress_indicator(self.main_window, True)
3032
hildon.hildon_gtk_window_set_progress_indicator(self.episodes_window.main_window, True)
3033
self.fancy_progress_bar.show()
3034
self.button_subscribe.set_sensitive(False)
3035
self.button_refresh.set_sensitive(False)
3036
self.feed_cache_update_cancelled = False
3038
self.itemUpdate.set_sensitive(False)
3039
self.itemUpdateChannel.set_sensitive(False)
3042
self.tray_icon.set_status(self.tray_icon.STATUS_UPDATING_FEED_CACHE)
3044
self.feed_cache_update_cancelled = False
3045
self.btnCancelFeedUpdate.show()
3046
self.btnCancelFeedUpdate.set_sensitive(True)
3047
if gpodder.ui.maemo:
3048
self.toolbarSpacer.set_expand(False)
3049
self.toolbarSpacer.set_draw(True)
3050
self.btnUpdateSelectedFeed.hide()
3051
self.toolFeedUpdateProgress.show_all()
3053
self.btnCancelFeedUpdate.set_image(gtk.image_new_from_stock(gtk.STOCK_STOP, gtk.ICON_SIZE_BUTTON))
3054
self.hboxUpdateFeeds.show_all()
3055
self.btnUpdateFeeds.hide()
3057
if len(channels) == 1:
3058
text = _('Updating "%s"...') % channels[0].title
3060
count = len(channels)
3061
text = N_('Updating %(count)d feed...', 'Updating %(count)d feeds...', count) % {'count':count}
3062
self.pbFeedUpdate.set_text(text)
3063
self.pbFeedUpdate.set_fraction(0)
3065
args = (channels, select_url_afterwards)
3066
threading.Thread(target=self.update_feed_cache_proc, args=args).start()
3068
def on_gPodder_delete_event(self, widget, *args):
3069
"""Called when the GUI wants to close the window
3070
Displays a confirmation dialog (and closes/hides gPodder)
3073
downloading = self.download_status_model.are_downloads_in_progress()
3076
if gpodder.ui.fremantle:
3077
self.close_gpodder()
3078
elif gpodder.ui.diablo:
3079
result = self.show_confirmation(_('Do you really want to quit gPodder now?'))
3081
self.close_gpodder()
3084
dialog = gtk.MessageDialog(self.gPodder, gtk.DIALOG_MODAL, gtk.MESSAGE_QUESTION, gtk.BUTTONS_NONE)
3085
dialog.add_button(gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL)
3086
quit_button = dialog.add_button(gtk.STOCK_QUIT, gtk.RESPONSE_CLOSE)
3088
title = _('Quit gPodder')
3089
message = _('You are downloading episodes. You can resume downloads the next time you start gPodder. Do you want to quit now?')
3091
dialog.set_title(title)
3092
dialog.set_markup('<span weight="bold" size="larger">%s</span>\n\n%s'%(title, message))
3094
quit_button.grab_focus()
3095
result = dialog.run()
3098
if result == gtk.RESPONSE_CLOSE:
3099
self.close_gpodder()
3101
self.close_gpodder()
3105
def close_gpodder(self):
3106
""" clean everything and exit properly
3109
if self.save_channels_opml():
3110
pass # FIXME: Add mygpo synchronization here
3112
self.show_message(_('Please check your permissions and free disk space.'), _('Error saving podcast list'), important=True)
3116
if self.tray_icon is not None:
3117
self.tray_icon.set_visible(False)
3119
# Notify all tasks to to carry out any clean-up actions
3120
self.download_status_model.tell_all_tasks_to_quit()
3122
while gtk.events_pending():
3123
gtk.main_iteration(False)
3130
def get_expired_episodes(self):
3131
for channel in self.channels:
3132
for episode in channel.get_downloaded_episodes():
3133
# Never consider locked episodes as old
3134
if episode.is_locked:
3137
# Never consider fresh episodes as old
3138
if episode.age_in_days() < self.config.episode_old_age:
3141
# Do not delete played episodes (except if configured)
3142
if episode.is_played:
3143
if not self.config.auto_remove_played_episodes:
3146
# Do not delete unplayed episodes (except if configured)
3147
if not episode.is_played:
3148
if not self.config.auto_remove_unplayed_episodes:
3153
def delete_episode_list(self, episodes, confirm=True, skip_locked=True):
3158
episodes = [e for e in episodes if not e.is_locked]
3161
title = _('Episodes are locked')
3162
message = _('The selected episodes are locked. Please unlock the episodes that you want to delete before trying to delete them.')
3163
self.notification(message, title, widget=self.treeAvailable)
3166
count = len(episodes)
3167
title = N_('Delete %(count)d episode?', 'Delete %(count)d episodes?', count) % {'count':count}
3168
message = _('Deleting episodes removes downloaded files.')
3170
if gpodder.ui.fremantle:
3171
message = '\n'.join([title, message])
3173
if confirm and not self.show_confirmation(message, title):
3176
progress = ProgressIndicator(_('Deleting episodes'), \
3177
_('Please wait while episodes are deleted'), \
3178
parent=self.get_dialog_parent())
3180
def finish_deletion(episode_urls, channel_urls):
3181
progress.on_finished()
3183
# Episodes have been deleted - persist the database
3186
self.update_episode_list_icons(episode_urls)
3187
self.update_podcast_list_model(channel_urls)
3188
self.play_or_download()
3191
episode_urls = set()
3192
channel_urls = set()
3194
episodes_status_update = []
3195
for idx, episode in enumerate(episodes):
3196
progress.on_progress(float(idx)/float(len(episodes)))
3197
if episode.is_locked and skip_locked:
3198
log('Not deleting episode (is locked): %s', episode.title)
3200
log('Deleting episode: %s', episode.title)
3201
progress.on_message(episode.title)
3202
episode.delete_from_disk()
3203
episode_urls.add(episode.url)
3204
channel_urls.add(episode.channel.url)
3205
episodes_status_update.append(episode)
3207
# Tell the shownotes window that we have removed the episode
3208
if self.episode_shownotes_window is not None and \
3209
self.episode_shownotes_window.episode is not None and \
3210
self.episode_shownotes_window.episode.url == episode.url:
3211
util.idle_add(self.episode_shownotes_window._download_status_changed, None)
3213
# Notify the web service about the status update + upload
3214
self.mygpo_client.on_delete(episodes_status_update)
3215
self.mygpo_client.flush()
3217
util.idle_add(finish_deletion, episode_urls, channel_urls)
3219
threading.Thread(target=thread_proc).start()
3223
def on_itemRemoveOldEpisodes_activate(self, widget):
3224
self.show_delete_episodes_window()
3226
def show_delete_episodes_window(self, channel=None):
3227
"""Offer deletion of episodes
3229
If channel is None, offer deletion of all episodes.
3230
Otherwise only offer deletion of episodes in the channel.
3232
if gpodder.ui.maemo:
3234
('maemo_remove_markup', None, None, _('Episode')),
3238
('title_markup', None, None, _('Episode')),
3239
('filesize_prop', 'length', gobject.TYPE_INT, _('Size')),
3240
('pubdate_prop', 'pubDate', gobject.TYPE_INT, _('Released')),
3241
('played_prop', None, None, _('Status')),
3242
('age_prop', 'age_int_prop', gobject.TYPE_INT, _('Downloaded')),
3245
msg_older_than = N_('Select older than %(count)d day', 'Select older than %(count)d days', self.config.episode_old_age)
3246
selection_buttons = {
3247
_('Select played'): lambda episode: episode.is_played,
3248
_('Select finished'): lambda episode: episode.is_finished(),
3249
msg_older_than % {'count':self.config.episode_old_age}: lambda episode: episode.age_in_days() > self.config.episode_old_age,
3252
instructions = _('Select the episodes you want to delete:')
3255
channels = self.channels
3257
channels = [channel]
3260
for channel in channels:
3261
for episode in channel.get_downloaded_episodes():
3262
# Disallow deletion of locked episodes that still exist
3263
if not episode.is_locked or not episode.file_exists():
3264
episodes.append(episode)
3266
selected = [e.is_played or not e.file_exists() for e in episodes]
3268
gPodderEpisodeSelector(self.gPodder, title = _('Delete episodes'), instructions = instructions, \
3269
episodes = episodes, selected = selected, columns = columns, \
3270
stock_ok_button = gtk.STOCK_DELETE, callback = self.delete_episode_list, \
3271
selection_buttons = selection_buttons, _config=self.config, \
3272
show_episode_shownotes=self.show_episode_shownotes)
3274
def on_selected_episodes_status_changed(self):
3275
# The order of the updates here is important! When "All episodes" is
3276
# selected, the update of the podcast list model depends on the episode
3277
# list selection to determine which podcasts are affected. Updating
3278
# the episode list could remove the selection if a filter is active.
3279
self.update_podcast_list_model(selected=True)
3280
self.update_episode_list_icons(selected=True)
3283
def mark_selected_episodes_new(self):
3284
for episode in self.get_selected_episodes():
3286
self.on_selected_episodes_status_changed()
3288
def mark_selected_episodes_old(self):
3289
for episode in self.get_selected_episodes():
3291
self.on_selected_episodes_status_changed()
3293
def on_item_toggle_played_activate( self, widget, toggle = True, new_value = False):
3294
for episode in self.get_selected_episodes():
3296
episode.mark(is_played=not episode.is_played)
3298
episode.mark(is_played=new_value)
3299
self.on_selected_episodes_status_changed()
3301
def on_item_toggle_lock_activate(self, widget, toggle=True, new_value=False):
3302
for episode in self.get_selected_episodes():
3304
episode.mark(is_locked=not episode.is_locked)
3306
episode.mark(is_locked=new_value)
3307
self.on_selected_episodes_status_changed()
3309
def on_channel_toggle_lock_activate(self, widget, toggle=True, new_value=False):
3310
if self.active_channel is None:
3313
self.active_channel.channel_is_locked = not self.active_channel.channel_is_locked
3314
self.active_channel.update_channel_lock()
3316
for episode in self.active_channel.get_all_episodes():
3317
episode.mark(is_locked=self.active_channel.channel_is_locked)
3319
self.update_podcast_list_model(selected=True)
3320
self.update_episode_list_icons(all=True)
3322
def on_itemUpdateChannel_activate(self, widget=None):
3323
if self.active_channel is None:
3324
title = _('No podcast selected')
3325
message = _('Please select a podcast in the podcasts list to update.')
3326
self.show_message( message, title, widget=self.treeChannels)
3329
# Dirty hack to check for "All episodes" (see gpodder.gtkui.model)
3330
if getattr(self.active_channel, 'ALL_EPISODES_PROXY', False):
3331
self.update_feed_cache()
3333
self.update_feed_cache(channels=[self.active_channel])
3335
def on_itemUpdate_activate(self, widget=None):
3336
# Check if we have outstanding subscribe/unsubscribe actions
3337
if self.on_add_remove_podcasts_mygpo():
3338
log('Update cancelled (received server changes)', sender=self)
3342
self.update_feed_cache()
3344
gPodderWelcome(self.gPodder,
3345
center_on_widget=self.gPodder,
3346
show_example_podcasts_callback=self.on_itemImportChannels_activate,
3347
setup_my_gpodder_callback=self.on_download_subscriptions_from_mygpo)
3349
def download_episode_list_paused(self, episodes):
3350
self.download_episode_list(episodes, True)
3352
def download_episode_list(self, episodes, add_paused=False, force_start=False):
3353
enable_update = False
3355
for episode in episodes:
3356
log('Downloading episode: %s', episode.title, sender = self)
3357
if not episode.was_downloaded(and_exists=True):
3359
for task in self.download_tasks_seen:
3360
if episode.url == task.url and task.status not in (task.DOWNLOADING, task.QUEUED):
3361
self.download_queue_manager.add_task(task, force_start)
3362
enable_update = True
3370
task = download.DownloadTask(episode, self.config)
3371
except Exception, e:
3372
d = {'episode': episode.title, 'message': str(e)}
3373
message = _('Download error while downloading %(episode)s: %(message)s')
3374
self.show_message(message % d, _('Download error'), important=True)
3375
log('Download error while downloading %s', episode.title, sender=self, traceback=True)
3379
task.status = task.PAUSED
3381
self.mygpo_client.on_download([task.episode])
3382
self.download_queue_manager.add_task(task, force_start)
3384
self.download_status_model.register_task(task)
3385
enable_update = True
3388
self.enable_download_list_update()
3390
# Flush updated episode status
3391
self.mygpo_client.flush()
3393
def cancel_task_list(self, tasks):
3398
if task.status in (task.QUEUED, task.DOWNLOADING):
3399
task.status = task.CANCELLED
3400
elif task.status == task.PAUSED:
3401
task.status = task.CANCELLED
3402
# Call run, so the partial file gets deleted
3405
self.update_episode_list_icons([task.url for task in tasks])
3406
self.play_or_download()
3408
# Update the tab title and downloads list
3409
self.update_downloads_list()
3411
def new_episodes_show(self, episodes, notification=False, selected=None):
3412
if gpodder.ui.maemo:
3414
('maemo_markup', None, None, _('Episode')),
3416
show_notification = notification
3419
('title_markup', None, None, _('Episode')),
3420
('filesize_prop', 'length', gobject.TYPE_INT, _('Size')),
3421
('pubdate_prop', 'pubDate', gobject.TYPE_INT, _('Released')),
3423
show_notification = False
3425
instructions = _('Select the episodes you want to download:')
3427
if self.new_episodes_window is not None:
3428
self.new_episodes_window.main_window.destroy()
3429
self.new_episodes_window = None
3431
def download_episodes_callback(episodes):
3432
self.new_episodes_window = None
3433
self.download_episode_list(episodes)
3435
if selected is None:
3436
# Select all by default
3437
selected = [True]*len(episodes)
3439
self.new_episodes_window = gPodderEpisodeSelector(self.gPodder, \
3440
title=_('New episodes available'), \
3441
instructions=instructions, \
3442
episodes=episodes, \
3444
selected=selected, \
3445
stock_ok_button = 'gpodder-download', \
3446
callback=download_episodes_callback, \
3447
remove_callback=lambda e: e.mark_old(), \
3448
remove_action=_('Mark as old'), \
3449
remove_finished=self.episode_new_status_changed, \
3450
_config=self.config, \
3451
show_notification=show_notification, \
3452
show_episode_shownotes=self.show_episode_shownotes)
3454
def on_itemDownloadAllNew_activate(self, widget, *args):
3455
if not self.offer_new_episodes():
3456
self.show_message(_('Please check for new episodes later.'), \
3457
_('No new episodes available'), widget=self.btnUpdateFeeds)
3459
def get_new_episodes(self, channels=None):
3460
if channels is None:
3461
channels = self.channels
3463
for channel in channels:
3464
for episode in channel.get_new_episodes(downloading=self.episode_is_downloading):
3465
episodes.append(episode)
3469
@dbus.service.method(gpodder.dbus_interface)
3470
def start_device_synchronization(self):
3471
"""Public D-Bus API for starting Device sync (Desktop only)
3473
This method can be called to initiate a synchronization with
3474
a configured protable media player. This only works for the
3475
Desktop version of gPodder and does nothing on Maemo.
3477
if gpodder.ui.desktop:
3478
self.on_sync_to_ipod_activate(None)
3483
def on_sync_to_ipod_activate(self, widget, episodes=None, force_played=True):
3484
self.sync_ui.on_synchronize_episodes(self.channels, episodes, force_played)
3486
def commit_changes_to_database(self):
3487
"""This will be called after the sync process is finished"""
3490
def on_cleanup_ipod_activate(self, widget, *args):
3491
self.sync_ui.on_cleanup_device()
3493
def on_manage_device_playlist(self, widget):
3494
self.sync_ui.on_manage_device_playlist()
3496
def show_hide_tray_icon(self):
3497
if self.config.display_tray_icon and have_trayicon and self.tray_icon is None:
3498
self.tray_icon = GPodderStatusIcon(self, gpodder.icon_file, self.config)
3499
elif not self.config.display_tray_icon and self.tray_icon:
3500
self.tray_icon.set_visible(False)
3502
self.tray_icon = None
3505
self.tray_icon.set_visible(True)
3507
def on_itemShowAllEpisodes_activate(self, widget):
3508
self.config.podcast_list_view_all = widget.get_active()
3510
def on_itemShowToolbar_activate(self, widget):
3511
self.config.show_toolbar = self.itemShowToolbar.get_active()
3513
def on_itemShowDescription_activate(self, widget):
3514
self.config.episode_list_descriptions = self.itemShowDescription.get_active()
3516
def on_item_view_hide_boring_podcasts_toggled(self, toggleaction):
3517
self.config.podcast_list_hide_boring = toggleaction.get_active()
3518
if self.config.podcast_list_hide_boring:
3519
self.podcast_list_model.set_view_mode(self.config.episode_list_view_mode)
3521
self.podcast_list_model.set_view_mode(-1)
3523
def on_item_view_podcasts_changed(self, radioaction, current):
3525
if current == self.item_view_podcasts_all:
3526
self.podcast_list_model.set_view_mode(-1)
3527
elif current == self.item_view_podcasts_downloaded:
3528
self.podcast_list_model.set_view_mode(EpisodeListModel.VIEW_DOWNLOADED)
3529
elif current == self.item_view_podcasts_unplayed:
3530
self.podcast_list_model.set_view_mode(EpisodeListModel.VIEW_UNPLAYED)
3532
self.config.podcast_list_view_mode = self.podcast_list_model.get_view_mode()
3534
def on_item_view_episodes_changed(self, radioaction, current):
3535
if current == self.item_view_episodes_all:
3536
self.config.episode_list_view_mode = EpisodeListModel.VIEW_ALL
3537
elif current == self.item_view_episodes_undeleted:
3538
self.config.episode_list_view_mode = EpisodeListModel.VIEW_UNDELETED
3539
elif current == self.item_view_episodes_downloaded:
3540
self.config.episode_list_view_mode = EpisodeListModel.VIEW_DOWNLOADED
3541
elif current == self.item_view_episodes_unplayed:
3542
self.config.episode_list_view_mode = EpisodeListModel.VIEW_UNPLAYED
3544
self.episode_list_model.set_view_mode(self.config.episode_list_view_mode)
3546
if self.config.podcast_list_hide_boring and not gpodder.ui.fremantle:
3547
self.podcast_list_model.set_view_mode(self.config.episode_list_view_mode)
3549
def update_item_device( self):
3550
if not gpodder.ui.fremantle:
3551
if self.config.device_type != 'none':
3552
self.itemDevice.set_visible(True)
3553
self.itemDevice.label = self.get_device_name()
3555
self.itemDevice.set_visible(False)
3557
def properties_closed( self):
3558
self.preferences_dialog = None
3559
self.show_hide_tray_icon()
3560
self.update_item_device()
3561
if gpodder.ui.maemo:
3562
selection = self.treeAvailable.get_selection()
3563
if self.config.maemo_enable_gestures or \
3564
self.config.enable_fingerscroll:
3565
selection.set_mode(gtk.SELECTION_SINGLE)
3567
selection.set_mode(gtk.SELECTION_MULTIPLE)
3569
def on_itemPreferences_activate(self, widget, *args):
3570
self.preferences_dialog = gPodderPreferences(self.main_window, \
3571
_config=self.config, \
3572
callback_finished=self.properties_closed, \
3573
user_apps_reader=self.user_apps_reader, \
3574
parent_window=self.main_window, \
3575
mygpo_client=self.mygpo_client, \
3576
on_send_full_subscriptions=self.on_send_full_subscriptions, \
3577
on_itemExportChannels_activate=self.on_itemExportChannels_activate)
3579
# Initial message to relayout window (in case it's opened in portrait mode
3580
self.preferences_dialog.on_window_orientation_changed(self._last_orientation)
3582
def on_itemDependencies_activate(self, widget):
3583
gPodderDependencyManager(self.gPodder)
3585
def on_goto_mygpo(self, widget):
3586
self.mygpo_client.open_website()
3588
def on_download_subscriptions_from_mygpo(self, action=None):
3589
title = _('Login to gpodder.net')
3590
message = _('Please login to download your subscriptions.')
3591
success, (username, password) = self.show_login_dialog(title, message, \
3592
self.config.mygpo_username, self.config.mygpo_password)
3596
self.config.mygpo_username = username
3597
self.config.mygpo_password = password
3599
dir = gPodderPodcastDirectory(self.gPodder, _config=self.config, \
3600
custom_title=_('Subscriptions on gpodder.net'), \
3601
add_urls_callback=self.add_podcast_list, \
3602
hide_url_entry=True)
3604
# TODO: Refactor this into "gpodder.my" or mygpoclient, so that
3605
# we do not have to hardcode the URL here
3606
OPML_URL = 'http://gpodder.net/subscriptions/%s.opml' % self.config.mygpo_username
3607
url = util.url_add_authentication(OPML_URL, \
3608
self.config.mygpo_username, \
3609
self.config.mygpo_password)
3610
dir.download_opml_file(url)
3612
def on_mygpo_settings_activate(self, action=None):
3613
# This dialog is only used for Maemo 4
3614
if not gpodder.ui.diablo:
3617
settings = MygPodderSettings(self.main_window, \
3618
config=self.config, \
3619
mygpo_client=self.mygpo_client, \
3620
on_send_full_subscriptions=self.on_send_full_subscriptions)
3622
def on_itemAddChannel_activate(self, widget=None):
3623
gPodderAddPodcast(self.gPodder, \
3624
add_urls_callback=self.add_podcast_list)
3626
def on_itemEditChannel_activate(self, widget, *args):
3627
if self.active_channel is None:
3628
title = _('No podcast selected')
3629
message = _('Please select a podcast in the podcasts list to edit.')
3630
self.show_message( message, title, widget=self.treeChannels)
3633
callback_closed = lambda: self.update_podcast_list_model(selected=True)
3634
gPodderChannel(self.main_window, \
3635
channel=self.active_channel, \
3636
callback_closed=callback_closed, \
3637
cover_downloader=self.cover_downloader)
3639
def on_itemMassUnsubscribe_activate(self, item=None):
3641
('title', None, None, _('Podcast')),
3644
# We're abusing the Episode Selector for selecting Podcasts here,
3645
# but it works and looks good, so why not? -- thp
3646
gPodderEpisodeSelector(self.main_window, \
3647
title=_('Remove podcasts'), \
3648
instructions=_('Select the podcast you want to remove.'), \
3649
episodes=self.channels, \
3651
size_attribute=None, \
3652
stock_ok_button=_('Remove'), \
3653
callback=self.remove_podcast_list, \
3654
_config=self.config)
3656
def remove_podcast_list(self, channels, confirm=True):
3658
log('No podcasts selected for deletion', sender=self)
3661
if len(channels) == 1:
3662
title = _('Removing podcast')
3663
info = _('Please wait while the podcast is removed')
3664
message = _('Do you really want to remove this podcast and its episodes?')
3666
title = _('Removing podcasts')
3667
info = _('Please wait while the podcasts are removed')
3668
message = _('Do you really want to remove the selected podcasts and their episodes?')
3670
if confirm and not self.show_confirmation(message, title):
3673
progress = ProgressIndicator(title, info, parent=self.get_dialog_parent())
3675
def finish_deletion(select_url):
3676
# Upload subscription list changes to the web service
3677
self.mygpo_client.on_unsubscribe([c.url for c in channels])
3679
# Re-load the channels and select the desired new channel
3680
self.update_feed_cache(force_update=False, select_url_afterwards=select_url)
3681
progress.on_finished()
3682
self.update_podcasts_tab()
3687
for idx, channel in enumerate(channels):
3688
# Update the UI for correct status messages
3689
progress.on_progress(float(idx)/float(len(channels)))
3690
progress.on_message(channel.title)
3692
# Delete downloaded episodes
3693
channel.remove_downloaded()
3695
# cancel any active downloads from this channel
3696
for episode in channel.get_all_episodes():
3697
util.idle_add(self.download_status_model.cancel_by_url,
3700
if len(channels) == 1:
3701
# get the URL of the podcast we want to select next
3702
if channel in self.channels:
3703
position = self.channels.index(channel)
3707
if position == len(self.channels)-1:
3708
# this is the last podcast, so select the URL
3709
# of the item before this one (i.e. the "new last")
3710
select_url = self.channels[position-1].url
3712
# there is a podcast after the deleted one, so
3713
# we simply select the one that comes after it
3714
select_url = self.channels[position+1].url
3716
# Remove the channel and clean the database entries
3718
self.channels.remove(channel)
3720
# Clean up downloads and download directories
3721
self.clean_up_downloads()
3723
self.channel_list_changed = True
3724
self.save_channels_opml()
3726
# The remaining stuff is to be done in the GTK main thread
3727
util.idle_add(finish_deletion, select_url)
3729
threading.Thread(target=thread_proc).start()
3731
def on_itemRemoveChannel_activate(self, widget, *args):
3732
if self.active_channel is None:
3733
title = _('No podcast selected')
3734
message = _('Please select a podcast in the podcasts list to remove.')
3735
self.show_message( message, title, widget=self.treeChannels)
3738
self.remove_podcast_list([self.active_channel])
3740
def get_opml_filter(self):
3741
filter = gtk.FileFilter()
3742
filter.add_pattern('*.opml')
3743
filter.add_pattern('*.xml')
3744
filter.set_name(_('OPML files')+' (*.opml, *.xml)')
3747
def on_item_import_from_file_activate(self, widget, filename=None):
3748
if filename is None:
3749
if gpodder.ui.desktop or gpodder.ui.fremantle:
3750
dlg = gtk.FileChooserDialog(title=_('Import from OPML'), \
3751
parent=None, action=gtk.FILE_CHOOSER_ACTION_OPEN)
3752
dlg.add_button(gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL)
3753
dlg.add_button(gtk.STOCK_OPEN, gtk.RESPONSE_OK)
3754
elif gpodder.ui.diablo:
3755
dlg = hildon.FileChooserDialog(self.gPodder, gtk.FILE_CHOOSER_ACTION_OPEN)
3756
dlg.set_filter(self.get_opml_filter())
3757
response = dlg.run()
3759
if response == gtk.RESPONSE_OK:
3760
filename = dlg.get_filename()
3763
if filename is not None:
3764
dir = gPodderPodcastDirectory(self.gPodder, _config=self.config, \
3765
custom_title=_('Import podcasts from OPML file'), \
3766
add_urls_callback=self.add_podcast_list, \
3767
hide_url_entry=True)
3768
dir.download_opml_file(filename)
3770
def on_itemExportChannels_activate(self, widget, *args):
3771
if not self.channels:
3772
title = _('Nothing to export')
3773
message = _('Your list of podcast subscriptions is empty. Please subscribe to some podcasts first before trying to export your subscription list.')
3774
self.show_message(message, title, widget=self.treeChannels)
3777
if gpodder.ui.desktop:
3778
dlg = gtk.FileChooserDialog(title=_('Export to OPML'), parent=self.gPodder, action=gtk.FILE_CHOOSER_ACTION_SAVE)
3779
dlg.add_button(gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL)
3780
dlg.add_button(gtk.STOCK_SAVE, gtk.RESPONSE_OK)
3781
elif gpodder.ui.fremantle:
3782
dlg = gobject.new(hildon.FileChooserDialog, \
3783
action=gtk.FILE_CHOOSER_ACTION_SAVE)
3784
dlg.set_title(_('Export to OPML'))
3785
elif gpodder.ui.diablo:
3786
dlg = hildon.FileChooserDialog(self.gPodder, gtk.FILE_CHOOSER_ACTION_SAVE)
3787
dlg.set_filter(self.get_opml_filter())
3788
response = dlg.run()
3789
if response == gtk.RESPONSE_OK:
3790
filename = dlg.get_filename()
3792
exporter = opml.Exporter( filename)
3793
if filename is not None and exporter.write(self.channels):
3794
count = len(self.channels)
3795
title = N_('%(count)d subscription exported', '%(count)d subscriptions exported', count) % {'count':count}
3796
self.show_message(_('Your podcast list has been successfully exported.'), title, widget=self.treeChannels)
3798
self.show_message( _('Could not export OPML to file. Please check your permissions.'), _('OPML export failed'), important=True)
3802
def on_itemImportChannels_activate(self, widget, *args):
3803
if gpodder.ui.fremantle:
3804
gPodderPodcastDirectory.show_add_podcast_picker(self.main_window, \
3805
self.config.toplist_url, \
3806
self.config.opml_url, \
3807
self.add_podcast_list, \
3808
self.on_itemAddChannel_activate, \
3809
self.on_download_subscriptions_from_mygpo, \
3810
self.show_text_edit_dialog)
3812
dir = gPodderPodcastDirectory(self.main_window, _config=self.config, \
3813
add_urls_callback=self.add_podcast_list)
3814
util.idle_add(dir.download_opml_file, self.config.opml_url)
3816
def on_homepage_activate(self, widget, *args):
3817
util.open_website(gpodder.__url__)
3819
def on_wiki_activate(self, widget, *args):
3820
util.open_website('http://gpodder.org/wiki/User_Manual')
3822
def on_bug_tracker_activate(self, widget, *args):
3823
if gpodder.ui.maemo:
3824
util.open_website('http://bugs.maemo.org/enter_bug.cgi?product=gPodder')
3826
util.open_website('https://bugs.gpodder.org/enter_bug.cgi?product=gPodder')
3828
def on_item_support_activate(self, widget):
3829
util.open_website('http://gpodder.org/donate')
3831
def on_itemAbout_activate(self, widget, *args):
3832
if gpodder.ui.fremantle:
3833
from gpodder.gtkui.frmntl.about import HeAboutDialog
3834
HeAboutDialog.present(self.main_window,
3837
gpodder.__version__,
3838
_('A podcast client with focus on usability'),
3839
gpodder.__copyright__,
3841
'http://bugs.maemo.org/enter_bug.cgi?product=gPodder',
3842
'http://gpodder.org/donate')
3845
dlg = gtk.AboutDialog()
3846
dlg.set_transient_for(self.main_window)
3847
dlg.set_name('gPodder')
3848
dlg.set_version(gpodder.__version__)
3849
dlg.set_copyright(gpodder.__copyright__)
3850
dlg.set_comments(_('A podcast client with focus on usability'))
3851
dlg.set_website(gpodder.__url__)
3852
dlg.set_translator_credits( _('translator-credits'))
3853
dlg.connect( 'response', lambda dlg, response: dlg.destroy())
3855
if gpodder.ui.desktop:
3856
# For the "GUI" version, we add some more
3857
# items to the about dialog (credits and logo)
3860
'Thomas Perl <thp.io>',
3863
if os.path.exists(gpodder.credits_file):
3864
credits = open(gpodder.credits_file).read().strip().split('\n')
3865
app_authors += ['', _('Patches, bug reports and donations by:')]
3866
app_authors += credits
3868
dlg.set_authors(app_authors)
3870
dlg.set_logo(gtk.gdk.pixbuf_new_from_file(gpodder.icon_file))
3872
dlg.set_logo_icon_name('gpodder')
3876
def on_wNotebook_switch_page(self, widget, *args):
3878
if gpodder.ui.maemo:
3879
self.tool_downloads.set_active(page_num == 1)
3880
page = self.wNotebook.get_nth_page(page_num)
3881
tab_label = self.wNotebook.get_tab_label(page).get_text()
3882
if page_num == 0 and self.active_channel is not None:
3883
self.set_title(self.active_channel.title)
3885
self.set_title(tab_label)
3887
self.play_or_download()
3888
self.menuChannels.set_sensitive(True)
3889
self.menuSubscriptions.set_sensitive(True)
3890
# The message area in the downloads tab should be hidden
3891
# when the user switches away from the downloads tab
3892
if self.message_area is not None:
3893
self.message_area.hide()
3894
self.message_area = None
3896
self.menuChannels.set_sensitive(False)
3897
self.menuSubscriptions.set_sensitive(False)
3898
if gpodder.ui.desktop:
3899
self.toolDownload.set_sensitive(False)
3900
self.toolPlay.set_sensitive(False)
3901
self.toolTransfer.set_sensitive(False)
3902
self.toolCancel.set_sensitive(False)
3904
def on_treeChannels_row_activated(self, widget, path, *args):
3905
# double-click action of the podcast list or enter
3906
self.treeChannels.set_cursor(path)
3908
def on_treeChannels_cursor_changed(self, widget, *args):
3909
( model, iter ) = self.treeChannels.get_selection().get_selected()
3911
if model is not None and iter is not None:
3912
old_active_channel = self.active_channel
3913
self.active_channel = model.get_value(iter, PodcastListModel.C_CHANNEL)
3915
if self.active_channel == old_active_channel:
3918
if gpodder.ui.maemo:
3919
self.set_title(self.active_channel.title)
3921
# Dirty hack to check for "All episodes" (see gpodder.gtkui.model)
3922
if getattr(self.active_channel, 'ALL_EPISODES_PROXY', False):
3923
self.itemEditChannel.set_visible(False)
3924
self.itemRemoveChannel.set_visible(False)
3926
self.itemEditChannel.set_visible(True)
3927
self.itemRemoveChannel.set_visible(True)
3929
self.active_channel = None
3930
self.itemEditChannel.set_visible(False)
3931
self.itemRemoveChannel.set_visible(False)
3933
self.update_episode_list_model()
3935
def on_btnEditChannel_clicked(self, widget, *args):
3936
self.on_itemEditChannel_activate( widget, args)
3938
def get_podcast_urls_from_selected_episodes(self):
3939
"""Get a set of podcast URLs based on the selected episodes"""
3940
return set(episode.channel.url for episode in \
3941
self.get_selected_episodes())
3943
def get_selected_episodes(self):
3944
"""Get a list of selected episodes from treeAvailable"""
3945
selection = self.treeAvailable.get_selection()
3946
model, paths = selection.get_selected_rows()
3948
episodes = [model.get_value(model.get_iter(path), EpisodeListModel.C_EPISODE) for path in paths]
3951
def on_transfer_selected_episodes(self, widget):
3952
self.on_sync_to_ipod_activate(widget, self.get_selected_episodes())
3954
def on_playback_selected_episodes(self, widget):
3955
self.playback_episodes(self.get_selected_episodes())
3957
def on_shownotes_selected_episodes(self, widget):
3958
episodes = self.get_selected_episodes()
3960
episode = episodes.pop(0)
3961
self.show_episode_shownotes(episode)
3963
self.show_message(_('Please select an episode from the episode list to display shownotes.'), _('No episode selected'), widget=self.treeAvailable)
3965
def on_download_selected_episodes(self, widget):
3966
episodes = self.get_selected_episodes()
3967
self.download_episode_list(episodes)
3968
self.update_episode_list_icons([episode.url for episode in episodes])
3969
self.play_or_download()
3971
def on_treeAvailable_row_activated(self, widget, path, view_column):
3972
"""Double-click/enter action handler for treeAvailable"""
3973
# We should only have one one selected as it was double clicked!
3974
e = self.get_selected_episodes()[0]
3976
if (self.config.double_click_episode_action == 'download'):
3977
# If the episode has already been downloaded and exists then play it
3978
if e.was_downloaded(and_exists=True):
3979
self.playback_episodes(self.get_selected_episodes())
3980
# else download it if it is not already downloading
3981
elif not self.episode_is_downloading(e):
3982
self.download_episode_list([e])
3983
self.update_episode_list_icons([e.url])
3984
self.play_or_download()
3985
elif (self.config.double_click_episode_action == 'stream'):
3986
# If we happen to have downloaded this episode simple play it
3987
if e.was_downloaded(and_exists=True):
3988
self.playback_episodes(self.get_selected_episodes())
3989
# else if streaming is possible stream it
3990
elif self.streaming_possible():
3991
self.playback_episodes(self.get_selected_episodes())
3993
log('Unable to stream episode - default media player selected!', sender=self, traceback=True)
3994
self.show_message(_('Please check your media player settings in the preferences dialog.'), _('Unable to stream episode'), widget=self.toolPreferences)
3996
# default action is to display show notes
3997
self.on_shownotes_selected_episodes(widget)
3999
def show_episode_shownotes(self, episode):
4000
if self.episode_shownotes_window is None:
4001
log('First-time use of episode window --- creating', sender=self)
4002
self.episode_shownotes_window = gPodderShownotes(self.gPodder, _config=self.config, \
4003
_download_episode_list=self.download_episode_list, \
4004
_playback_episodes=self.playback_episodes, \
4005
_delete_episode_list=self.delete_episode_list, \
4006
_episode_list_status_changed=self.episode_list_status_changed, \
4007
_cancel_task_list=self.cancel_task_list, \
4008
_episode_is_downloading=self.episode_is_downloading, \
4009
_streaming_possible=self.streaming_possible())
4010
self.episode_shownotes_window.show(episode)
4011
if self.episode_is_downloading(episode):
4012
self.update_downloads_list()
4014
def restart_auto_update_timer(self):
4015
if self._auto_update_timer_source_id is not None:
4016
log('Removing existing auto update timer.', sender=self)
4017
gobject.source_remove(self._auto_update_timer_source_id)
4018
self._auto_update_timer_source_id = None
4020
if self.config.auto_update_feeds and \
4021
self.config.auto_update_frequency:
4022
interval = 60*1000*self.config.auto_update_frequency
4023
log('Setting up auto update timer with interval %d.', \
4024
self.config.auto_update_frequency, sender=self)
4025
self._auto_update_timer_source_id = gobject.timeout_add(\
4026
interval, self._on_auto_update_timer)
4028
def _on_auto_update_timer(self):
4029
log('Auto update timer fired.', sender=self)
4030
self.update_feed_cache(force_update=True)
4032
# Ask web service for sub changes (if enabled)
4033
self.mygpo_client.flush()
4037
def on_treeDownloads_row_activated(self, widget, *args):
4038
# Use the standard way of working on the treeview
4039
selection = self.treeDownloads.get_selection()
4040
(model, paths) = selection.get_selected_rows()
4041
selected_tasks = [(gtk.TreeRowReference(model, path), model.get_value(model.get_iter(path), 0)) for path in paths]
4043
for tree_row_reference, task in selected_tasks:
4044
if task.status in (task.DOWNLOADING, task.QUEUED):
4045
task.status = task.PAUSED
4046
elif task.status in (task.CANCELLED, task.PAUSED, task.FAILED):
4047
self.download_queue_manager.add_task(task)
4048
self.enable_download_list_update()
4049
elif task.status == task.DONE:
4050
model.remove(model.get_iter(tree_row_reference.get_path()))
4052
self.play_or_download()
4054
# Update the tab title and downloads list
4055
self.update_downloads_list()
4057
def on_item_cancel_download_activate(self, widget):
4058
if self.wNotebook.get_current_page() == 0:
4059
selection = self.treeAvailable.get_selection()
4060
(model, paths) = selection.get_selected_rows()
4061
urls = [model.get_value(model.get_iter(path), \
4062
self.episode_list_model.C_URL) for path in paths]
4063
selected_tasks = [task for task in self.download_tasks_seen \
4064
if task.url in urls]
4066
selection = self.treeDownloads.get_selection()
4067
(model, paths) = selection.get_selected_rows()
4068
selected_tasks = [model.get_value(model.get_iter(path), \
4069
self.download_status_model.C_TASK) for path in paths]
4070
self.cancel_task_list(selected_tasks)
4072
def on_btnCancelAll_clicked(self, widget, *args):
4073
self.cancel_task_list(self.download_tasks_seen)
4075
def on_btnDownloadedDelete_clicked(self, widget, *args):
4076
episodes = self.get_selected_episodes()
4077
if len(episodes) == 1:
4078
self.delete_episode_list(episodes, skip_locked=False)
4080
self.delete_episode_list(episodes)
4082
def on_key_press(self, widget, event):
4083
# Allow tab switching with Ctrl + PgUp/PgDown
4084
if event.state & gtk.gdk.CONTROL_MASK:
4085
if event.keyval == gtk.keysyms.Page_Up:
4086
self.wNotebook.prev_page()
4088
elif event.keyval == gtk.keysyms.Page_Down:
4089
self.wNotebook.next_page()
4092
# After this code we only handle Maemo hardware keys,
4093
# so if we are not a Maemo app, we don't do anything
4094
if not gpodder.ui.maemo:
4098
if event.keyval == gtk.keysyms.F7: #plus
4100
elif event.keyval == gtk.keysyms.F8: #minus
4103
if diff != 0 and not self.currently_updating:
4104
selection = self.treeChannels.get_selection()
4105
(model, iter) = selection.get_selected()
4106
new_path = ((model.get_path(iter)[0]+diff)%len(model),)
4107
selection.select_path(new_path)
4108
self.treeChannels.set_cursor(new_path)
4113
def on_iconify(self):
4115
self.gPodder.set_skip_taskbar_hint(False)
4117
self.gPodder.set_skip_taskbar_hint(False)
4119
def on_uniconify(self):
4121
self.gPodder.set_skip_taskbar_hint(False)
4123
self.gPodder.set_skip_taskbar_hint(False)
4125
def uniconify_main_window(self):
4126
# We need to hide and then show the window in WMs like Metacity
4127
# or KWin4 to move the window to the active workspace
4128
# (see http://gpodder.org/bug/1125)
4131
self.gPodder.present()
4133
def iconify_main_window(self):
4134
if not self.is_iconified():
4137
def update_podcasts_tab(self):
4138
if gpodder.ui.fremantle:
4141
self.label2.set_text(_('Podcasts'))
4142
count = len(self.channels)
4144
self.label2.set_text(self.label2.get_text() + ' (%d)' % count)
4146
@dbus.service.method(gpodder.dbus_interface)
4147
def show_gui_window(self):
4148
parent = self.get_dialog_parent()
4151
@dbus.service.method(gpodder.dbus_interface)
4152
def subscribe_to_url(self, url):
4153
gPodderAddPodcast(self.gPodder,
4154
add_urls_callback=self.add_podcast_list,
4157
@dbus.service.method(gpodder.dbus_interface)
4158
def mark_episode_played(self, filename):
4159
if filename is None:
4162
for channel in self.channels:
4163
for episode in channel.get_all_episodes():
4164
fn = episode.local_filename(create=False, check_only=True)
4166
episode.mark(is_played=True)
4168
self.update_episode_list_icons([episode.url])
4169
self.update_podcast_list_model([episode.channel.url])
4175
def main(options=None):
4176
gobject.threads_init()
4177
gobject.set_application_name('gPodder')
4179
if gpodder.ui.maemo:
4180
# Try to enable the custom icon theme for gPodder on Maemo
4181
settings = gtk.settings_get_default()
4182
settings.set_string_property('gtk-icon-theme-name', \
4183
'gpodder', __file__)
4184
# Extend the search path for the optified icon theme (Maemo 5)
4185
icon_theme = gtk.icon_theme_get_default()
4186
icon_theme.prepend_search_path('/opt/gpodder-icon-theme/')
4188
# Add custom icons for the new Maemo 5 look :)
4189
for id in ('audio', 'video', 'download', 'audio-locked', 'video-locked'):
4190
filename = os.path.join(gpodder.images_folder, '%s.png' % id)
4191
pixbuf = gtk.gdk.pixbuf_new_from_file(filename)
4192
gtk.icon_theme_add_builtin_icon('gpodder-%s' % id, 40, pixbuf)
4194
gtk.window_set_default_icon_name('gpodder')
4195
gtk.about_dialog_set_url_hook(lambda dlg, link, data: util.open_website(link), None)
4198
dbus_main_loop = dbus.glib.DBusGMainLoop(set_as_default=True)
4199
gpodder.dbus_session_bus = dbus.SessionBus(dbus_main_loop)
4201
bus_name = dbus.service.BusName(gpodder.dbus_bus_name, bus=gpodder.dbus_session_bus)
4202
except dbus.exceptions.DBusException, dbe:
4203
log('Warning: Cannot get "on the bus".', traceback=True)
4204
dlg = gtk.MessageDialog(None, gtk.DIALOG_MODAL, gtk.MESSAGE_ERROR, \
4205
gtk.BUTTONS_CLOSE, _('Cannot start gPodder'))
4206
dlg.format_secondary_markup(_('D-Bus error: %s') % (str(dbe),))
4207
dlg.set_title('gPodder')
4212
util.make_directory(gpodder.home)
4213
gpodder.load_plugins()
4215
config = UIConfig(gpodder.config_file)
4217
# Load hook modules and install the hook manager globally
4218
# if modules have been found an instantiated by the manager
4219
user_hooks = hooks.HookManager()
4220
if user_hooks.has_modules():
4221
gpodder.user_hooks = user_hooks
4223
if gpodder.ui.diablo:
4224
# Detect changing of SD cards between mmc1/mmc2 if a gpodder
4225
# folder exists there (allow moving "gpodder" between SD cards or USB)
4226
# Also allow moving "gpodder" to home folder (e.g. rootfs on SD)
4227
if not os.path.exists(config.download_dir):
4228
log('Downloads might have been moved. Trying to locate them...')
4229
for basedir in ['/media/mmc1', '/media/mmc2']+glob.glob('/media/usb/*')+['/home/user/MyDocs']:
4230
dir = os.path.join(basedir, 'gpodder')
4231
if os.path.exists(dir):
4232
log('Downloads found in: %s', dir)
4233
config.download_dir = dir
4236
log('Downloads NOT FOUND in %s', dir)
4238
if config.enable_fingerscroll:
4239
BuilderWidget.use_fingerscroll = True
4241
config.mygpo_device_type = util.detect_device_type()
4243
gp = gPodder(bus_name, config)
4246
if options.subscribe:
4247
util.idle_add(gp.subscribe_to_url, options.subscribe)
4250
# handle "subscribe to podcast" events from firefox
4251
if platform.system() == 'Darwin':
4252
from gpodder import gpodderosx
4253
gpodderosx.register_handlers(gp)
4254
# end mac OS X stuff