1 # -*- coding: utf-8 -*-
3 # gPodder - A media aggregator and podcast client
4 # Copyright (c) 2005-2010 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/>.
36 from xml
.sax
import saxutils
46 # Mock the required D-Bus interfaces with no-ops (ugly? maybe.)
49 def __init__(self
, *args
, **kwargs
):
51 def add_signal_receiver(self
, *args
, **kwargs
):
55 def __init__(self
, *args
, **kwargs
):
59 def method(*args
, **kwargs
):
62 def __init__(self
, *args
, **kwargs
):
65 def __init__(self
, *args
, **kwargs
):
69 from gpodder
import feedcore
70 from gpodder
import util
71 from gpodder
import opml
72 from gpodder
import download
73 from gpodder
import my
74 from gpodder
import youtube
75 from gpodder
import player
76 from gpodder
.liblogger
import log
81 from gpodder
.model
import PodcastChannel
82 from gpodder
.model
import PodcastEpisode
83 from gpodder
.dbsqlite
import Database
85 from gpodder
.gtkui
.model
import PodcastListModel
86 from gpodder
.gtkui
.model
import EpisodeListModel
87 from gpodder
.gtkui
.config
import UIConfig
88 from gpodder
.gtkui
.services
import CoverDownloader
89 from gpodder
.gtkui
.widgets
import SimpleMessageArea
90 from gpodder
.gtkui
.desktopfile
import UserAppsReader
92 from gpodder
.gtkui
.draw
import draw_text_box_centered
94 from gpodder
.gtkui
.interface
.common
import BuilderWidget
95 from gpodder
.gtkui
.interface
.common
import TreeViewHelper
96 from gpodder
.gtkui
.interface
.addpodcast
import gPodderAddPodcast
98 if gpodder
.ui
.desktop
:
99 from gpodder
.gtkui
.download
import DownloadStatusModel
101 from gpodder
.gtkui
.desktop
.sync
import gPodderSyncUI
103 from gpodder
.gtkui
.desktop
.channel
import gPodderChannel
104 from gpodder
.gtkui
.desktop
.preferences
import gPodderPreferences
105 from gpodder
.gtkui
.desktop
.shownotes
import gPodderShownotes
106 from gpodder
.gtkui
.desktop
.episodeselector
import gPodderEpisodeSelector
107 from gpodder
.gtkui
.desktop
.podcastdirectory
import gPodderPodcastDirectory
108 from gpodder
.gtkui
.desktop
.dependencymanager
import gPodderDependencyManager
109 from gpodder
.gtkui
.interface
.progress
import ProgressIndicator
111 from gpodder
.gtkui
.desktop
.trayicon
import GPodderStatusIcon
113 except Exception, exc
:
114 log('Warning: Could not import gpodder.trayicon.', traceback
=True)
115 log('Warning: This probably means your PyGTK installation is too old!')
116 have_trayicon
= False
117 elif gpodder
.ui
.diablo
:
118 from gpodder
.gtkui
.download
import DownloadStatusModel
120 from gpodder
.gtkui
.maemo
.channel
import gPodderChannel
121 from gpodder
.gtkui
.maemo
.preferences
import gPodderPreferences
122 from gpodder
.gtkui
.maemo
.shownotes
import gPodderShownotes
123 from gpodder
.gtkui
.maemo
.episodeselector
import gPodderEpisodeSelector
124 from gpodder
.gtkui
.maemo
.podcastdirectory
import gPodderPodcastDirectory
125 from gpodder
.gtkui
.maemo
.mygpodder
import MygPodderSettings
126 from gpodder
.gtkui
.interface
.progress
import ProgressIndicator
127 have_trayicon
= False
128 elif gpodder
.ui
.fremantle
:
129 from gpodder
.gtkui
.frmntl
.model
import DownloadStatusModel
130 from gpodder
.gtkui
.frmntl
.model
import EpisodeListModel
131 from gpodder
.gtkui
.frmntl
.model
import PodcastListModel
133 from gpodder
.gtkui
.maemo
.channel
import gPodderChannel
134 from gpodder
.gtkui
.frmntl
.preferences
import gPodderPreferences
135 from gpodder
.gtkui
.frmntl
.shownotes
import gPodderShownotes
136 from gpodder
.gtkui
.frmntl
.episodeselector
import gPodderEpisodeSelector
137 from gpodder
.gtkui
.frmntl
.podcastdirectory
import gPodderPodcastDirectory
138 from gpodder
.gtkui
.frmntl
.episodes
import gPodderEpisodes
139 from gpodder
.gtkui
.frmntl
.downloads
import gPodderDownloads
140 from gpodder
.gtkui
.frmntl
.progress
import ProgressIndicator
141 from gpodder
.gtkui
.frmntl
.widgets
import FancyProgressBar
142 have_trayicon
= False
144 from gpodder
.gtkui
.frmntl
.portrait
import FremantleRotation
145 from gpodder
.gtkui
.frmntl
.mafw
import MafwPlaybackMonitor
146 from gpodder
.gtkui
.frmntl
.hints
import HINT_STRINGS
148 from gpodder
.gtkui
.interface
.common
import Orientation
150 from gpodder
.gtkui
.interface
.welcome
import gPodderWelcome
155 from gpodder
.dbusproxy
import DBusPodcastsProxy
156 from gpodder
import hooks
158 class gPodder(BuilderWidget
, dbus
.service
.Object
):
159 finger_friendly_widgets
= ['btnCleanUpDownloads', 'button_search_episodes_clear', 'label2', 'labelDownloads', 'btnUpdateFeeds']
161 ICON_GENERAL_ADD
= 'general_add'
162 ICON_GENERAL_REFRESH
= 'general_refresh'
164 def __init__(self
, bus_name
, config
):
165 dbus
.service
.Object
.__init
__(self
, object_path
=gpodder
.dbus_gui_object_path
, bus_name
=bus_name
)
166 self
.podcasts_proxy
= DBusPodcastsProxy(lambda: self
.channels
, \
167 self
.on_itemUpdate_activate
, \
168 self
.playback_episodes
, \
169 self
.download_episode_list
, \
170 self
.episode_object_by_uri
, \
172 self
.db
= Database(gpodder
.database_file
)
174 BuilderWidget
.__init
__(self
, None)
177 if gpodder
.ui
.diablo
:
179 self
.app
= hildon
.Program()
180 self
.app
.add_window(self
.main_window
)
181 self
.main_window
.add_toolbar(self
.toolbar
)
183 for child
in self
.main_menu
.get_children():
185 self
.main_window
.set_menu(self
.set_finger_friendly(menu
))
186 self
._last
_orientation
= Orientation
.LANDSCAPE
187 elif gpodder
.ui
.fremantle
:
189 self
.app
= hildon
.Program()
190 self
.app
.add_window(self
.main_window
)
192 appmenu
= hildon
.AppMenu()
194 for filter in (self
.item_view_podcasts_all
, \
195 self
.item_view_podcasts_downloaded
, \
196 self
.item_view_podcasts_unplayed
):
197 button
= gtk
.ToggleButton()
198 filter.connect_proxy(button
)
199 appmenu
.add_filter(button
)
201 for action
in (self
.itemPreferences
, \
202 self
.item_downloads
, \
203 self
.itemRemoveOldEpisodes
, \
204 self
.item_unsubscribe
, \
206 button
= hildon
.Button(gtk
.HILDON_SIZE_AUTO
,\
207 hildon
.BUTTON_ARRANGEMENT_HORIZONTAL
)
208 action
.connect_proxy(button
)
209 if action
== self
.item_downloads
:
210 button
.set_title(_('Downloads'))
211 button
.set_value(_('Idle'))
212 self
.button_downloads
= button
213 appmenu
.append(button
)
215 def show_hint(button
):
216 self
.show_message(random
.choice(HINT_STRINGS
), important
=True)
218 button
= hildon
.Button(gtk
.HILDON_SIZE_AUTO
,\
219 hildon
.BUTTON_ARRANGEMENT_HORIZONTAL
)
220 button
.set_title(_('Hint of the day'))
221 button
.connect('clicked', show_hint
)
222 appmenu
.append(button
)
225 self
.main_window
.set_app_menu(appmenu
)
227 # Initialize portrait mode / rotation manager
228 self
._fremantle
_rotation
= FremantleRotation('gPodder', \
230 gpodder
.__version
__, \
231 self
.config
.rotation_mode
)
233 if self
.config
.rotation_mode
== FremantleRotation
.ALWAYS
:
234 util
.idle_add(self
.on_window_orientation_changed
, \
235 Orientation
.PORTRAIT
)
236 self
._last
_orientation
= Orientation
.PORTRAIT
238 self
._last
_orientation
= Orientation
.LANDSCAPE
240 # Flag set when a notification is being shown (Maemo bug 11235)
241 self
._fremantle
_notification
_visible
= False
243 self
._last
_orientation
= Orientation
.LANDSCAPE
244 self
.toolbar
.set_property('visible', self
.config
.show_toolbar
)
246 self
.bluetooth_available
= util
.bluetooth_available()
248 self
.config
.connect_gtk_window(self
.gPodder
, 'main_window')
249 if not gpodder
.ui
.fremantle
:
250 self
.config
.connect_gtk_paned('paned_position', self
.channelPaned
)
251 self
.main_window
.show()
253 self
.player_receiver
= player
.MediaPlayerDBusReceiver(self
.on_played
)
255 if gpodder
.ui
.fremantle
:
256 # Create a D-Bus monitoring object that takes care of
257 # tracking MAFW (Nokia Media Player) playback events
258 # and sends episode playback status events via D-Bus
259 self
.mafw_monitor
= MafwPlaybackMonitor(gpodder
.dbus_session_bus
)
261 self
.gPodder
.connect('key-press-event', self
.on_key_press
)
263 self
.preferences_dialog
= None
264 self
.config
.add_observer(self
.on_config_changed
)
266 self
.tray_icon
= None
267 self
.episode_shownotes_window
= None
268 self
.new_episodes_window
= None
270 if gpodder
.ui
.desktop
:
271 # Mac OS X-specific UI tweaks: Native main menu integration
272 # http://sourceforge.net/apps/trac/gtk-osx/wiki/Integrate
273 if getattr(gtk
.gdk
, 'WINDOWING', 'x11') == 'quartz':
275 import igemacintegration
as igemi
277 # Move the menu bar from the window to the Mac menu bar
279 igemi
.ige_mac_menu_set_menu_bar(self
.mainMenu
)
281 # Reparent some items to the "Application" menu
282 for widget
in ('/mainMenu/menuHelp/itemAbout', \
283 '/mainMenu/menuPodcasts/itemPreferences'):
284 item
= self
.uimanager1
.get_widget(widget
)
285 group
= igemi
.ige_mac_menu_add_app_menu_group()
286 igemi
.ige_mac_menu_add_app_menu_item(group
, item
, None)
288 quit_widget
= '/mainMenu/menuPodcasts/itemQuit'
289 quit_item
= self
.uimanager1
.get_widget(quit_widget
)
290 igemi
.ige_mac_menu_set_quit_menu_item(quit_item
)
292 print >>sys
.stderr
, """
293 Warning: ige-mac-integration not found - no native menus.
296 self
.sync_ui
= gPodderSyncUI(self
.config
, self
.notification
, \
297 self
.main_window
, self
.show_confirmation
, \
298 self
.update_episode_list_icons
, \
299 self
.update_podcast_list_model
, self
.toolPreferences
, \
300 gPodderEpisodeSelector
, \
301 self
.commit_changes_to_database
)
305 self
.download_status_model
= DownloadStatusModel()
306 self
.download_queue_manager
= download
.DownloadQueueManager(self
.config
)
308 if gpodder
.ui
.desktop
:
309 self
.show_hide_tray_icon()
310 self
.itemShowAllEpisodes
.set_active(self
.config
.podcast_list_view_all
)
311 self
.itemShowToolbar
.set_active(self
.config
.show_toolbar
)
312 self
.itemShowDescription
.set_active(self
.config
.episode_list_descriptions
)
314 if not gpodder
.ui
.fremantle
:
315 self
.config
.connect_gtk_spinbutton('max_downloads', self
.spinMaxDownloads
)
316 self
.config
.connect_gtk_togglebutton('max_downloads_enabled', self
.cbMaxDownloads
)
317 self
.config
.connect_gtk_spinbutton('limit_rate_value', self
.spinLimitDownloads
)
318 self
.config
.connect_gtk_togglebutton('limit_rate', self
.cbLimitDownloads
)
320 # When the amount of maximum downloads changes, notify the queue manager
321 changed_cb
= lambda spinbutton
: self
.download_queue_manager
.spawn_threads()
322 self
.spinMaxDownloads
.connect('value-changed', changed_cb
)
324 self
.default_title
= 'gPodder'
325 if gpodder
.__version
__.rfind('git') != -1:
326 self
.set_title('gPodder %s' % gpodder
.__version
__)
328 title
= self
.gPodder
.get_title()
329 if title
is not None:
330 self
.set_title(title
)
332 self
.set_title(_('gPodder'))
334 self
.cover_downloader
= CoverDownloader()
336 # Generate list models for podcasts and their episodes
337 self
.podcast_list_model
= PodcastListModel(self
.cover_downloader
)
339 self
.cover_downloader
.register('cover-available', self
.cover_download_finished
)
340 self
.cover_downloader
.register('cover-removed', self
.cover_file_removed
)
342 if gpodder
.ui
.fremantle
:
343 # Work around Maemo bug #4718
344 self
.button_refresh
.set_name('HildonButton-finger')
345 self
.button_subscribe
.set_name('HildonButton-finger')
347 self
.button_refresh
.set_sensitive(False)
348 self
.button_subscribe
.set_sensitive(False)
350 self
.button_subscribe
.set_image(gtk
.image_new_from_icon_name(\
351 self
.ICON_GENERAL_ADD
, gtk
.ICON_SIZE_BUTTON
))
352 self
.button_refresh
.set_image(gtk
.image_new_from_icon_name(\
353 self
.ICON_GENERAL_REFRESH
, gtk
.ICON_SIZE_BUTTON
))
355 # Make the button scroll together with the TreeView contents
356 action_area_box
= self
.treeChannels
.get_action_area_box()
357 for child
in self
.buttonbox
:
358 child
.reparent(action_area_box
)
359 self
.vbox
.remove(self
.buttonbox
)
360 action_area_box
.set_spacing(2)
361 action_area_box
.set_border_width(3)
362 self
.treeChannels
.set_action_area_visible(True)
364 # Set up a very nice progress bar setup
365 self
.fancy_progress_bar
= FancyProgressBar(self
.main_window
, \
366 self
.on_btnCancelFeedUpdate_clicked
)
367 self
.pbFeedUpdate
= self
.fancy_progress_bar
.progress_bar
368 self
.pbFeedUpdate
.set_ellipsize(pango
.ELLIPSIZE_MIDDLE
)
369 self
.vbox
.pack_start(self
.fancy_progress_bar
.event_box
, False)
371 from gpodder
.gtkui
.frmntl
import style
372 sub_font
= style
.get_font_desc('SmallSystemFont')
373 sub_color
= style
.get_color('SecondaryTextColor')
374 sub
= (sub_font
.to_string(), sub_color
.to_string())
375 sub
= '<span font_desc="%s" foreground="%s">%%s</span>' % sub
376 self
.label_footer
.set_markup(sub
% gpodder
.__copyright
__)
378 hildon
.hildon_gtk_window_set_progress_indicator(self
.main_window
, True)
379 while gtk
.events_pending():
380 gtk
.main_iteration(False)
383 # Try to get the real package version from dpkg
384 p
= subprocess
.Popen(['dpkg-query', '-W', '-f=${Version}', 'gpodder'], stdout
=subprocess
.PIPE
)
385 version
, _stderr
= p
.communicate()
389 version
= gpodder
.__version
__
390 self
.label_footer
.set_markup(sub
% ('v %s' % version
))
391 self
.label_footer
.hide()
393 self
.episodes_window
= gPodderEpisodes(self
.main_window
, \
394 on_treeview_expose_event
=self
.on_treeview_expose_event
, \
395 show_episode_shownotes
=self
.show_episode_shownotes
, \
396 update_podcast_list_model
=self
.update_podcast_list_model
, \
397 on_itemRemoveChannel_activate
=self
.on_itemRemoveChannel_activate
, \
398 item_view_episodes_all
=self
.item_view_episodes_all
, \
399 item_view_episodes_unplayed
=self
.item_view_episodes_unplayed
, \
400 item_view_episodes_downloaded
=self
.item_view_episodes_downloaded
, \
401 item_view_episodes_undeleted
=self
.item_view_episodes_undeleted
, \
402 on_entry_search_episodes_changed
=self
.on_entry_search_episodes_changed
, \
403 on_entry_search_episodes_key_press
=self
.on_entry_search_episodes_key_press
, \
404 hide_episode_search
=self
.hide_episode_search
, \
405 on_itemUpdateChannel_activate
=self
.on_itemUpdateChannel_activate
, \
406 playback_episodes
=self
.playback_episodes
, \
407 delete_episode_list
=self
.delete_episode_list
, \
408 episode_list_status_changed
=self
.episode_list_status_changed
, \
409 download_episode_list
=self
.download_episode_list
, \
410 episode_is_downloading
=self
.episode_is_downloading
, \
411 show_episode_in_download_manager
=self
.show_episode_in_download_manager
, \
412 add_download_task_monitor
=self
.add_download_task_monitor
, \
413 remove_download_task_monitor
=self
.remove_download_task_monitor
, \
414 for_each_episode_set_task_status
=self
.for_each_episode_set_task_status
, \
415 on_delete_episodes_button_clicked
=self
.on_itemRemoveOldEpisodes_activate
, \
416 on_itemUpdate_activate
=self
.on_itemUpdate_activate
)
418 # Expose objects for episode list type-ahead find
419 self
.hbox_search_episodes
= self
.episodes_window
.hbox_search_episodes
420 self
.entry_search_episodes
= self
.episodes_window
.entry_search_episodes
421 self
.button_search_episodes_clear
= self
.episodes_window
.button_search_episodes_clear
423 self
.downloads_window
= gPodderDownloads(self
.main_window
, \
424 on_treeview_expose_event
=self
.on_treeview_expose_event
, \
425 cleanup_downloads
=self
.cleanup_downloads
, \
426 _for_each_task_set_status
=self
._for
_each
_task
_set
_status
, \
427 downloads_list_get_selection
=self
.downloads_list_get_selection
, \
430 self
.treeAvailable
= self
.episodes_window
.treeview
431 self
.treeDownloads
= self
.downloads_window
.treeview
433 # Init the treeviews that we use
434 self
.init_podcast_list_treeview()
435 self
.init_episode_list_treeview()
436 self
.init_download_list_treeview()
438 if self
.config
.podcast_list_hide_boring
:
439 self
.item_view_hide_boring_podcasts
.set_active(True)
441 self
.currently_updating
= False
443 if gpodder
.ui
.maemo
or self
.config
.enable_fingerscroll
:
444 self
.context_menu_mouse_button
= 1
446 self
.context_menu_mouse_button
= 3
448 if self
.config
.start_iconified
:
449 self
.iconify_main_window()
451 self
.download_tasks_seen
= set()
452 self
.download_list_update_enabled
= False
453 self
.download_task_monitors
= set()
455 # Subscribed channels
456 self
.active_channel
= None
457 self
.channels
= PodcastChannel
.load_from_db(self
.db
, self
.config
.download_dir
)
458 self
.channel_list_changed
= True
459 self
.update_podcasts_tab()
461 # load list of user applications for audio playback
462 self
.user_apps_reader
= UserAppsReader(['audio', 'video'])
463 threading
.Thread(target
=self
.user_apps_reader
.read
).start()
465 # Set the "Device" menu item for the first time
466 if gpodder
.ui
.desktop
:
467 self
.update_item_device()
469 # Set up the first instance of MygPoClient
470 self
.mygpo_client
= my
.MygPoClient(self
.config
)
472 # Now, update the feed cache, when everything's in place
473 if not gpodder
.ui
.fremantle
:
474 self
.btnUpdateFeeds
.show()
475 self
.updating_feed_cache
= False
476 self
.feed_cache_update_cancelled
= False
477 self
.update_feed_cache(force_update
=self
.config
.update_on_startup
)
479 self
.message_area
= None
481 def find_partial_downloads():
482 # Look for partial file downloads
483 partial_files
= glob
.glob(os
.path
.join(self
.config
.download_dir
, '*', '*.partial'))
484 count
= len(partial_files
)
485 resumable_episodes
= []
487 if not gpodder
.ui
.fremantle
:
488 util
.idle_add(self
.wNotebook
.set_current_page
, 1)
489 indicator
= ProgressIndicator(_('Loading incomplete downloads'), \
490 _('Some episodes have not finished downloading in a previous session.'), \
491 False, self
.get_dialog_parent())
492 indicator
.on_message(N_('%d partial file', '%d partial files', count
) % count
)
494 candidates
= [f
[:-len('.partial')] for f
in partial_files
]
497 for c
in self
.channels
:
498 for e
in c
.get_all_episodes():
499 filename
= e
.local_filename(create
=False, check_only
=True)
500 if filename
in candidates
:
501 log('Found episode: %s', e
.title
, sender
=self
)
503 indicator
.on_message(e
.title
)
504 indicator
.on_progress(float(found
)/count
)
505 candidates
.remove(filename
)
506 partial_files
.remove(filename
+'.partial')
507 resumable_episodes
.append(e
)
515 for f
in partial_files
:
516 log('Partial file without episode: %s', f
, sender
=self
)
519 util
.idle_add(indicator
.on_finished
)
521 if len(resumable_episodes
):
522 def offer_resuming():
523 self
.download_episode_list_paused(resumable_episodes
)
524 if not gpodder
.ui
.fremantle
:
525 resume_all
= gtk
.Button(_('Resume all'))
526 #resume_all.set_border_width(0)
527 def on_resume_all(button
):
528 selection
= self
.treeDownloads
.get_selection()
529 selection
.select_all()
530 selected_tasks
, can_queue
, can_cancel
, can_pause
, can_remove
, can_force
= self
.downloads_list_get_selection()
531 selection
.unselect_all()
532 self
._for
_each
_task
_set
_status
(selected_tasks
, download
.DownloadTask
.QUEUED
)
533 self
.message_area
.hide()
534 resume_all
.connect('clicked', on_resume_all
)
536 self
.message_area
= SimpleMessageArea(_('Incomplete downloads from a previous session were found.'), (resume_all
,))
537 self
.vboxDownloadStatusWidgets
.pack_start(self
.message_area
, expand
=False)
538 self
.vboxDownloadStatusWidgets
.reorder_child(self
.message_area
, 0)
539 self
.message_area
.show_all()
540 self
.clean_up_downloads(delete_partial
=False)
541 util
.idle_add(offer_resuming
)
542 elif not gpodder
.ui
.fremantle
:
543 util
.idle_add(self
.wNotebook
.set_current_page
, 0)
545 util
.idle_add(self
.clean_up_downloads
, True)
546 threading
.Thread(target
=find_partial_downloads
).start()
548 # Start the auto-update procedure
549 self
._auto
_update
_timer
_source
_id
= None
550 if self
.config
.auto_update_feeds
:
551 self
.restart_auto_update_timer()
553 # Delete old episodes if the user wishes to
554 if self
.config
.auto_remove_played_episodes
and \
555 self
.config
.episode_old_age
> 0:
556 old_episodes
= list(self
.get_expired_episodes())
557 if len(old_episodes
) > 0:
558 self
.delete_episode_list(old_episodes
, confirm
=False)
559 self
.update_podcast_list_model(set(e
.channel
.url
for e
in old_episodes
))
561 if gpodder
.ui
.fremantle
:
562 hildon
.hildon_gtk_window_set_progress_indicator(self
.main_window
, False)
563 self
.button_refresh
.set_sensitive(True)
564 self
.button_subscribe
.set_sensitive(True)
565 self
.main_window
.set_title(_('gPodder'))
566 hildon
.hildon_gtk_window_take_screenshot(self
.main_window
, True)
568 # Do the initial sync with the web service
569 util
.idle_add(self
.mygpo_client
.flush
, True)
571 # First-time users should be asked if they want to see the OPML
572 if not self
.channels
and not gpodder
.ui
.fremantle
:
573 util
.idle_add(self
.on_itemUpdate_activate
)
575 def episode_object_by_uri(self
, uri
):
576 """Get an episode object given a local or remote URI
578 This can be used to quickly access an episode object
579 when all we have is its download filename or episode
580 URL (e.g. from external D-Bus calls / signals, etc..)
582 if uri
.startswith('/'):
583 uri
= 'file://' + uri
585 prefix
= 'file://' + self
.config
.download_dir
587 if uri
.startswith(prefix
):
588 # File is on the local filesystem in the download folder
589 filename
= uri
[len(prefix
):]
590 file_parts
= [x
for x
in filename
.split(os
.sep
) if x
]
592 if len(file_parts
) == 2:
593 dir_name
, filename
= file_parts
594 channels
= [c
for c
in self
.channels
if c
.foldername
== dir_name
]
595 if len(channels
) == 1:
596 channel
= channels
[0]
597 return channel
.get_episode_by_filename(filename
)
599 # Possibly remote file - search the database for a podcast
600 channel_id
= self
.db
.get_channel_id_from_episode_url(uri
)
602 if channel_id
is not None:
603 channels
= [c
for c
in self
.channels
if c
.id == channel_id
]
604 if len(channels
) == 1:
605 channel
= channels
[0]
606 return channel
.get_episode_by_url(uri
)
610 def on_played(self
, start
, end
, total
, file_uri
):
611 """Handle the "played" signal from a media player"""
612 if start
== 0 and end
== 0 and total
== 0:
613 # Ignore bogus play event
615 elif end
< start
+ 5:
616 # Ignore "less than five seconds" segments,
617 # as they can happen with seeking, etc...
620 log('Received play action: %s (%d, %d, %d)', file_uri
, start
, end
, total
, sender
=self
)
621 episode
= self
.episode_object_by_uri(file_uri
)
623 if episode
is not None:
624 file_type
= episode
.file_type()
625 # Automatically enable D-Bus played status mode
626 if file_type
== 'audio':
627 self
.config
.audio_played_dbus
= True
628 elif file_type
== 'video':
629 self
.config
.video_played_dbus
= True
633 episode
.total_time
= total
635 # Assume the episode's total time for the action
636 total
= episode
.total_time
637 if episode
.current_position_updated
is None or \
638 now
> episode
.current_position_updated
:
639 episode
.current_position
= end
640 episode
.current_position_updated
= now
641 episode
.mark(is_played
=True)
644 self
.update_episode_list_icons([episode
.url
])
645 self
.update_podcast_list_model([episode
.channel
.url
])
647 # Submit this action to the webservice
648 self
.mygpo_client
.on_playback_full(episode
, \
651 def on_add_remove_podcasts_mygpo(self
):
652 actions
= self
.mygpo_client
.get_received_actions()
656 existing_urls
= [c
.url
for c
in self
.channels
]
658 # Columns for the episode selector window - just one...
660 ('description', None, None, _('Action')),
663 # A list of actions that have to be chosen from
666 # Actions that are ignored (already carried out)
669 for action
in actions
:
670 if action
.is_add
and action
.url
not in existing_urls
:
671 changes
.append(my
.Change(action
))
672 elif action
.is_remove
and action
.url
in existing_urls
:
673 podcast_object
= None
674 for podcast
in self
.channels
:
675 if podcast
.url
== action
.url
:
676 podcast_object
= podcast
678 changes
.append(my
.Change(action
, podcast_object
))
680 log('Ignoring action: %s', action
, sender
=self
)
681 ignored
.append(action
)
683 # Confirm all ignored changes
684 self
.mygpo_client
.confirm_received_actions(ignored
)
686 def execute_podcast_actions(selected
):
687 add_list
= [c
.action
.url
for c
in selected
if c
.action
.is_add
]
688 remove_list
= [c
.podcast
for c
in selected
if c
.action
.is_remove
]
690 # Apply the accepted changes locally
691 self
.add_podcast_list(add_list
)
692 self
.remove_podcast_list(remove_list
, confirm
=False)
694 # All selected items are now confirmed
695 self
.mygpo_client
.confirm_received_actions(c
.action
for c
in selected
)
697 # Revert the changes on the server
698 rejected
= [c
.action
for c
in changes
if c
not in selected
]
699 self
.mygpo_client
.reject_received_actions(rejected
)
702 # We're abusing the Episode Selector again ;) -- thp
703 gPodderEpisodeSelector(self
.main_window
, \
704 title
=_('Confirm changes from gpodder.net'), \
705 instructions
=_('Select the actions you want to carry out.'), \
708 size_attribute
=None, \
709 stock_ok_button
=gtk
.STOCK_APPLY
, \
710 callback
=execute_podcast_actions
, \
713 # There are some actions that need the user's attention
718 # We have no remaining actions - no selection happens
721 def rewrite_urls_mygpo(self
):
722 # Check if we have to rewrite URLs since the last add
723 rewritten_urls
= self
.mygpo_client
.get_rewritten_urls()
725 for rewritten_url
in rewritten_urls
:
726 if not rewritten_url
.new_url
:
729 for channel
in self
.channels
:
730 if channel
.url
== rewritten_url
.old_url
:
731 log('Updating URL of %s to %s', channel
, \
732 rewritten_url
.new_url
, sender
=self
)
733 channel
.url
= rewritten_url
.new_url
735 self
.channel_list_changed
= True
736 util
.idle_add(self
.update_episode_list_model
)
739 def on_send_full_subscriptions(self
):
740 # Send the full subscription list to the gpodder.net client
741 # (this will overwrite the subscription list on the server)
742 indicator
= ProgressIndicator(_('Uploading subscriptions'), \
743 _('Your subscriptions are being uploaded to the server.'), \
744 False, self
.get_dialog_parent())
747 self
.mygpo_client
.set_subscriptions([c
.url
for c
in self
.channels
])
748 util
.idle_add(self
.show_message
, _('List uploaded successfully.'))
753 message
= e
.__class
__.__name
__
754 self
.show_message(message
, \
755 _('Error while uploading'), \
757 util
.idle_add(show_error
, e
)
759 util
.idle_add(indicator
.on_finished
)
761 def on_podcast_selected(self
, treeview
, path
, column
):
763 model
= treeview
.get_model()
764 channel
= model
.get_value(model
.get_iter(path
), \
765 PodcastListModel
.C_CHANNEL
)
766 self
.active_channel
= channel
767 self
.update_episode_list_model()
768 self
.episodes_window
.channel
= self
.active_channel
769 self
.episodes_window
.show()
771 def on_button_subscribe_clicked(self
, button
):
772 self
.on_itemImportChannels_activate(button
)
774 def on_button_downloads_clicked(self
, widget
):
775 self
.downloads_window
.show()
777 def show_episode_in_download_manager(self
, episode
):
778 self
.downloads_window
.show()
779 model
= self
.treeDownloads
.get_model()
780 selection
= self
.treeDownloads
.get_selection()
781 selection
.unselect_all()
782 it
= model
.get_iter_first()
783 while it
is not None:
784 task
= model
.get_value(it
, DownloadStatusModel
.C_TASK
)
785 if task
.episode
.url
== episode
.url
:
786 selection
.select_iter(it
)
787 # FIXME: Scroll to selection in pannable area
789 it
= model
.iter_next(it
)
791 def for_each_episode_set_task_status(self
, episodes
, status
):
792 episode_urls
= set(episode
.url
for episode
in episodes
)
793 model
= self
.treeDownloads
.get_model()
794 selected_tasks
= [(gtk
.TreeRowReference(model
, row
.path
), \
795 model
.get_value(row
.iter, \
796 DownloadStatusModel
.C_TASK
)) for row
in model \
797 if model
.get_value(row
.iter, DownloadStatusModel
.C_TASK
).url \
799 self
._for
_each
_task
_set
_status
(selected_tasks
, status
)
801 def on_window_orientation_changed(self
, orientation
):
802 self
._last
_orientation
= orientation
803 if self
.preferences_dialog
is not None:
804 self
.preferences_dialog
.on_window_orientation_changed(orientation
)
806 treeview
= self
.treeChannels
807 if orientation
== Orientation
.PORTRAIT
:
808 treeview
.set_action_area_orientation(gtk
.ORIENTATION_VERTICAL
)
809 # Work around Maemo bug #4718
810 self
.button_subscribe
.set_name('HildonButton-thumb')
811 self
.button_refresh
.set_name('HildonButton-thumb')
813 treeview
.set_action_area_orientation(gtk
.ORIENTATION_HORIZONTAL
)
814 # Work around Maemo bug #4718
815 self
.button_subscribe
.set_name('HildonButton-finger')
816 self
.button_refresh
.set_name('HildonButton-finger')
818 if gpodder
.ui
.fremantle
:
819 self
.fancy_progress_bar
.relayout()
821 def on_treeview_podcasts_selection_changed(self
, selection
):
822 model
, iter = selection
.get_selected()
824 self
.active_channel
= None
825 self
.episode_list_model
.clear()
827 def on_treeview_button_pressed(self
, treeview
, event
):
828 if event
.window
!= treeview
.get_bin_window():
831 TreeViewHelper
.save_button_press_event(treeview
, event
)
833 if getattr(treeview
, TreeViewHelper
.ROLE
) == \
834 TreeViewHelper
.ROLE_PODCASTS
:
835 return self
.currently_updating
837 return event
.button
== self
.context_menu_mouse_button
and \
840 def on_treeview_podcasts_button_released(self
, treeview
, event
):
841 if event
.window
!= treeview
.get_bin_window():
845 return self
.treeview_channels_handle_gestures(treeview
, event
)
846 return self
.treeview_channels_show_context_menu(treeview
, event
)
848 def on_treeview_episodes_button_released(self
, treeview
, event
):
849 if event
.window
!= treeview
.get_bin_window():
852 if self
.config
.enable_fingerscroll
or self
.config
.maemo_enable_gestures
:
853 return self
.treeview_available_handle_gestures(treeview
, event
)
855 return self
.treeview_available_show_context_menu(treeview
, event
)
857 def on_treeview_downloads_button_released(self
, treeview
, event
):
858 if event
.window
!= treeview
.get_bin_window():
861 return self
.treeview_downloads_show_context_menu(treeview
, event
)
863 def on_entry_search_podcasts_changed(self
, editable
):
864 if self
.hbox_search_podcasts
.get_property('visible'):
865 self
.podcast_list_model
.set_search_term(editable
.get_chars(0, -1))
867 def on_entry_search_podcasts_key_press(self
, editable
, event
):
868 if event
.keyval
== gtk
.keysyms
.Escape
:
869 self
.hide_podcast_search()
872 def hide_podcast_search(self
, *args
):
873 self
.hbox_search_podcasts
.hide()
874 self
.entry_search_podcasts
.set_text('')
875 self
.podcast_list_model
.set_search_term(None)
876 self
.treeChannels
.grab_focus()
878 def show_podcast_search(self
, input_char
):
879 self
.hbox_search_podcasts
.show()
880 self
.entry_search_podcasts
.insert_text(input_char
, -1)
881 self
.entry_search_podcasts
.grab_focus()
882 self
.entry_search_podcasts
.set_position(-1)
884 def init_podcast_list_treeview(self
):
885 # Set up podcast channel tree view widget
886 if gpodder
.ui
.fremantle
:
887 if self
.config
.podcast_list_view_mode
== EpisodeListModel
.VIEW_DOWNLOADED
:
888 self
.item_view_podcasts_downloaded
.set_active(True)
889 elif self
.config
.podcast_list_view_mode
== EpisodeListModel
.VIEW_UNPLAYED
:
890 self
.item_view_podcasts_unplayed
.set_active(True)
892 self
.item_view_podcasts_all
.set_active(True)
893 self
.podcast_list_model
.set_view_mode(self
.config
.podcast_list_view_mode
)
895 iconcolumn
= gtk
.TreeViewColumn('')
896 iconcell
= gtk
.CellRendererPixbuf()
897 iconcolumn
.pack_start(iconcell
, False)
898 iconcolumn
.add_attribute(iconcell
, 'pixbuf', PodcastListModel
.C_COVER
)
899 self
.treeChannels
.append_column(iconcolumn
)
901 namecolumn
= gtk
.TreeViewColumn('')
902 namecell
= gtk
.CellRendererText()
903 namecell
.set_property('ellipsize', pango
.ELLIPSIZE_END
)
904 namecolumn
.pack_start(namecell
, True)
905 namecolumn
.add_attribute(namecell
, 'markup', PodcastListModel
.C_DESCRIPTION
)
907 if gpodder
.ui
.fremantle
:
908 countcell
= gtk
.CellRendererText()
909 from gpodder
.gtkui
.frmntl
import style
910 countcell
.set_property('font-desc', style
.get_font_desc('EmpSystemFont'))
911 countcell
.set_property('foreground-gdk', style
.get_color('SecondaryTextColor'))
912 countcell
.set_property('alignment', pango
.ALIGN_RIGHT
)
913 countcell
.set_property('xalign', 1.)
914 countcell
.set_property('xpad', 5)
915 namecolumn
.pack_start(countcell
, False)
916 namecolumn
.add_attribute(countcell
, 'text', PodcastListModel
.C_DOWNLOADS
)
917 namecolumn
.add_attribute(countcell
, 'visible', PodcastListModel
.C_DOWNLOADS
)
919 iconcell
= gtk
.CellRendererPixbuf()
920 iconcell
.set_property('xalign', 1.0)
921 namecolumn
.pack_start(iconcell
, False)
922 namecolumn
.add_attribute(iconcell
, 'pixbuf', PodcastListModel
.C_PILL
)
923 namecolumn
.add_attribute(iconcell
, 'visible', PodcastListModel
.C_PILL_VISIBLE
)
925 self
.treeChannels
.append_column(namecolumn
)
927 self
.treeChannels
.set_model(self
.podcast_list_model
.get_filtered_model())
929 # When no podcast is selected, clear the episode list model
930 selection
= self
.treeChannels
.get_selection()
931 selection
.connect('changed', self
.on_treeview_podcasts_selection_changed
)
933 # Set up type-ahead find for the podcast list
934 def on_key_press(treeview
, event
):
935 if event
.keyval
== gtk
.keysyms
.Escape
:
936 self
.hide_podcast_search()
937 elif gpodder
.ui
.fremantle
and event
.keyval
== gtk
.keysyms
.BackSpace
:
938 self
.hide_podcast_search()
939 elif event
.state
& gtk
.gdk
.CONTROL_MASK
:
940 # Don't handle type-ahead when control is pressed (so shortcuts
941 # with the Ctrl key still work, e.g. Ctrl+A, ...)
944 unicode_char_id
= gtk
.gdk
.keyval_to_unicode(event
.keyval
)
945 if unicode_char_id
== 0:
947 input_char
= unichr(unicode_char_id
)
948 self
.show_podcast_search(input_char
)
950 self
.treeChannels
.connect('key-press-event', on_key_press
)
952 # Enable separators to the podcast list to separate special podcasts
953 # from others (this is used for the "all episodes" view)
954 self
.treeChannels
.set_row_separator_func(PodcastListModel
.row_separator_func
)
956 TreeViewHelper
.set(self
.treeChannels
, TreeViewHelper
.ROLE_PODCASTS
)
958 def on_entry_search_episodes_changed(self
, editable
):
959 if self
.hbox_search_episodes
.get_property('visible'):
960 self
.episode_list_model
.set_search_term(editable
.get_chars(0, -1))
962 def on_entry_search_episodes_key_press(self
, editable
, event
):
963 if event
.keyval
== gtk
.keysyms
.Escape
:
964 self
.hide_episode_search()
967 def hide_episode_search(self
, *args
):
968 self
.hbox_search_episodes
.hide()
969 self
.entry_search_episodes
.set_text('')
970 self
.episode_list_model
.set_search_term(None)
971 self
.treeAvailable
.grab_focus()
973 def show_episode_search(self
, input_char
):
974 self
.hbox_search_episodes
.show()
975 self
.entry_search_episodes
.insert_text(input_char
, -1)
976 self
.entry_search_episodes
.grab_focus()
977 self
.entry_search_episodes
.set_position(-1)
979 def init_episode_list_treeview(self
):
980 # For loading the list model
981 self
.episode_list_model
= EpisodeListModel()
983 if self
.config
.episode_list_view_mode
== EpisodeListModel
.VIEW_UNDELETED
:
984 self
.item_view_episodes_undeleted
.set_active(True)
985 elif self
.config
.episode_list_view_mode
== EpisodeListModel
.VIEW_DOWNLOADED
:
986 self
.item_view_episodes_downloaded
.set_active(True)
987 elif self
.config
.episode_list_view_mode
== EpisodeListModel
.VIEW_UNPLAYED
:
988 self
.item_view_episodes_unplayed
.set_active(True)
990 self
.item_view_episodes_all
.set_active(True)
992 self
.episode_list_model
.set_view_mode(self
.config
.episode_list_view_mode
)
994 self
.treeAvailable
.set_model(self
.episode_list_model
.get_filtered_model())
996 TreeViewHelper
.set(self
.treeAvailable
, TreeViewHelper
.ROLE_EPISODES
)
998 iconcell
= gtk
.CellRendererPixbuf()
999 iconcell
.set_property('stock-size', gtk
.ICON_SIZE_BUTTON
)
1000 if gpodder
.ui
.maemo
:
1001 iconcell
.set_fixed_size(50, 50)
1003 iconcell
.set_fixed_size(40, -1)
1005 namecell
= gtk
.CellRendererText()
1006 namecell
.set_property('ellipsize', pango
.ELLIPSIZE_END
)
1007 namecolumn
= gtk
.TreeViewColumn(_('Episode'))
1008 namecolumn
.pack_start(iconcell
, False)
1009 namecolumn
.add_attribute(iconcell
, 'icon-name', EpisodeListModel
.C_STATUS_ICON
)
1010 namecolumn
.pack_start(namecell
, True)
1011 namecolumn
.add_attribute(namecell
, 'markup', EpisodeListModel
.C_DESCRIPTION
)
1012 namecolumn
.set_sort_column_id(EpisodeListModel
.C_DESCRIPTION
)
1013 namecolumn
.set_sizing(gtk
.TREE_VIEW_COLUMN_AUTOSIZE
)
1014 namecolumn
.set_resizable(True)
1015 namecolumn
.set_expand(True)
1017 if gpodder
.ui
.fremantle
:
1018 from gpodder
.gtkui
.frmntl
import style
1019 timecell
= gtk
.CellRendererText()
1020 timecell
.set_property('font-desc', style
.get_font_desc('SystemFont'))
1021 timecell
.set_property('foreground-gdk', style
.get_color('SecondaryTextColor'))
1022 timecell
.set_property('alignment', pango
.ALIGN_RIGHT
)
1023 timecell
.set_property('xalign', 1.)
1024 timecell
.set_property('xpad', 5)
1025 namecolumn
.pack_start(timecell
, False)
1026 namecolumn
.add_attribute(timecell
, 'text', EpisodeListModel
.C_TIME
)
1027 namecolumn
.add_attribute(timecell
, 'visible', EpisodeListModel
.C_TIME1_VISIBLE
)
1029 # Add another cell renderer to fix a sizing issue (one renderer
1030 # only renders short text and the other one longer text to avoid
1031 # having titles of episodes unnecessarily cut off)
1032 timecell
= gtk
.CellRendererText()
1033 timecell
.set_property('font-desc', style
.get_font_desc('SystemFont'))
1034 timecell
.set_property('foreground-gdk', style
.get_color('SecondaryTextColor'))
1035 timecell
.set_property('alignment', pango
.ALIGN_RIGHT
)
1036 timecell
.set_property('xalign', 1.)
1037 timecell
.set_property('xpad', 5)
1038 namecolumn
.pack_start(timecell
, False)
1039 namecolumn
.add_attribute(timecell
, 'text', EpisodeListModel
.C_TIME
)
1040 namecolumn
.add_attribute(timecell
, 'visible', EpisodeListModel
.C_TIME2_VISIBLE
)
1042 lockcell
= gtk
.CellRendererPixbuf()
1043 lockcell
.set_property('stock-size', gtk
.ICON_SIZE_MENU
)
1044 if gpodder
.ui
.fremantle
:
1045 lockcell
.set_property('icon-name', 'general_locked')
1047 lockcell
.set_property('icon-name', 'emblem-readonly')
1049 namecolumn
.pack_start(lockcell
, False)
1050 namecolumn
.add_attribute(lockcell
, 'visible', EpisodeListModel
.C_LOCKED
)
1052 sizecell
= gtk
.CellRendererText()
1053 sizecell
.set_property('xalign', 1)
1054 sizecolumn
= gtk
.TreeViewColumn(_('Size'), sizecell
, text
=EpisodeListModel
.C_FILESIZE_TEXT
)
1055 sizecolumn
.set_sort_column_id(EpisodeListModel
.C_FILESIZE
)
1057 releasecell
= gtk
.CellRendererText()
1058 releasecolumn
= gtk
.TreeViewColumn(_('Released'), releasecell
, text
=EpisodeListModel
.C_PUBLISHED_TEXT
)
1059 releasecolumn
.set_sort_column_id(EpisodeListModel
.C_PUBLISHED
)
1061 for itemcolumn
in (namecolumn
, sizecolumn
, releasecolumn
):
1062 itemcolumn
.set_reorderable(True)
1063 self
.treeAvailable
.append_column(itemcolumn
)
1065 if gpodder
.ui
.maemo
:
1066 sizecolumn
.set_visible(False)
1067 releasecolumn
.set_visible(False)
1069 # Set up type-ahead find for the episode list
1070 def on_key_press(treeview
, event
):
1071 if event
.keyval
== gtk
.keysyms
.Escape
:
1072 self
.hide_episode_search()
1073 elif gpodder
.ui
.fremantle
and event
.keyval
== gtk
.keysyms
.BackSpace
:
1074 self
.hide_episode_search()
1075 elif event
.state
& gtk
.gdk
.CONTROL_MASK
:
1076 # Don't handle type-ahead when control is pressed (so shortcuts
1077 # with the Ctrl key still work, e.g. Ctrl+A, ...)
1080 unicode_char_id
= gtk
.gdk
.keyval_to_unicode(event
.keyval
)
1081 if unicode_char_id
== 0:
1083 input_char
= unichr(unicode_char_id
)
1084 self
.show_episode_search(input_char
)
1086 self
.treeAvailable
.connect('key-press-event', on_key_press
)
1088 if gpodder
.ui
.desktop
and not self
.config
.enable_fingerscroll
:
1089 self
.treeAvailable
.enable_model_drag_source(gtk
.gdk
.BUTTON1_MASK
, \
1090 (('text/uri-list', 0, 0),), gtk
.gdk
.ACTION_COPY
)
1091 def drag_data_get(tree
, context
, selection_data
, info
, timestamp
):
1092 if self
.config
.on_drag_mark_played
:
1093 for episode
in self
.get_selected_episodes():
1094 episode
.mark(is_played
=True)
1095 self
.on_selected_episodes_status_changed()
1096 uris
= ['file://'+e
.local_filename(create
=False) \
1097 for e
in self
.get_selected_episodes() \
1098 if e
.was_downloaded(and_exists
=True)]
1099 uris
.append('') # for the trailing '\r\n'
1100 selection_data
.set(selection_data
.target
, 8, '\r\n'.join(uris
))
1101 self
.treeAvailable
.connect('drag-data-get', drag_data_get
)
1103 selection
= self
.treeAvailable
.get_selection()
1104 if self
.config
.maemo_enable_gestures
or self
.config
.enable_fingerscroll
:
1105 selection
.set_mode(gtk
.SELECTION_SINGLE
)
1106 elif gpodder
.ui
.fremantle
:
1107 selection
.set_mode(gtk
.SELECTION_SINGLE
)
1109 selection
.set_mode(gtk
.SELECTION_MULTIPLE
)
1110 # Update the sensitivity of the toolbar buttons on the Desktop
1111 selection
.connect('changed', lambda s
: self
.play_or_download())
1113 if gpodder
.ui
.diablo
:
1114 # Set up the tap-and-hold context menu for podcasts
1116 menu
.append(self
.itemUpdateChannel
.create_menu_item())
1117 menu
.append(self
.itemEditChannel
.create_menu_item())
1118 menu
.append(gtk
.SeparatorMenuItem())
1119 menu
.append(self
.itemRemoveChannel
.create_menu_item())
1120 menu
.append(gtk
.SeparatorMenuItem())
1121 item
= gtk
.ImageMenuItem(_('Close this menu'))
1122 item
.set_image(gtk
.image_new_from_stock(gtk
.STOCK_CLOSE
, \
1123 gtk
.ICON_SIZE_MENU
))
1126 menu
= self
.set_finger_friendly(menu
)
1127 self
.treeChannels
.tap_and_hold_setup(menu
)
1130 def init_download_list_treeview(self
):
1131 # enable multiple selection support
1132 self
.treeDownloads
.get_selection().set_mode(gtk
.SELECTION_MULTIPLE
)
1133 self
.treeDownloads
.set_search_equal_func(TreeViewHelper
.make_search_equal_func(DownloadStatusModel
))
1135 # columns and renderers for "download progress" tab
1136 # First column: [ICON] Episodename
1137 column
= gtk
.TreeViewColumn(_('Episode'))
1139 cell
= gtk
.CellRendererPixbuf()
1140 if gpodder
.ui
.maemo
:
1141 cell
.set_fixed_size(50, 50)
1142 cell
.set_property('stock-size', gtk
.ICON_SIZE_BUTTON
)
1143 column
.pack_start(cell
, expand
=False)
1144 column
.add_attribute(cell
, 'icon-name', \
1145 DownloadStatusModel
.C_ICON_NAME
)
1147 cell
= gtk
.CellRendererText()
1148 cell
.set_property('ellipsize', pango
.ELLIPSIZE_END
)
1149 column
.pack_start(cell
, expand
=True)
1150 column
.add_attribute(cell
, 'markup', DownloadStatusModel
.C_NAME
)
1151 column
.set_sizing(gtk
.TREE_VIEW_COLUMN_AUTOSIZE
)
1152 column
.set_expand(True)
1153 self
.treeDownloads
.append_column(column
)
1155 # Second column: Progress
1156 cell
= gtk
.CellRendererProgress()
1157 cell
.set_property('yalign', .5)
1158 cell
.set_property('ypad', 6)
1159 column
= gtk
.TreeViewColumn(_('Progress'), cell
,
1160 value
=DownloadStatusModel
.C_PROGRESS
, \
1161 text
=DownloadStatusModel
.C_PROGRESS_TEXT
)
1162 column
.set_sizing(gtk
.TREE_VIEW_COLUMN_AUTOSIZE
)
1163 column
.set_expand(False)
1164 self
.treeDownloads
.append_column(column
)
1165 if gpodder
.ui
.maemo
:
1166 column
.set_property('min-width', 200)
1167 column
.set_property('max-width', 200)
1169 column
.set_property('min-width', 150)
1170 column
.set_property('max-width', 150)
1172 self
.treeDownloads
.set_model(self
.download_status_model
)
1173 TreeViewHelper
.set(self
.treeDownloads
, TreeViewHelper
.ROLE_DOWNLOADS
)
1175 def on_treeview_expose_event(self
, treeview
, event
):
1176 if event
.window
== treeview
.get_bin_window():
1177 model
= treeview
.get_model()
1178 if (model
is not None and model
.get_iter_first() is not None):
1181 role
= getattr(treeview
, TreeViewHelper
.ROLE
, None)
1185 ctx
= event
.window
.cairo_create()
1186 ctx
.rectangle(event
.area
.x
, event
.area
.y
,
1187 event
.area
.width
, event
.area
.height
)
1190 x
, y
, width
, height
, depth
= event
.window
.get_geometry()
1193 if role
== TreeViewHelper
.ROLE_EPISODES
:
1194 if self
.currently_updating
:
1195 text
= _('Loading episodes')
1196 elif self
.config
.episode_list_view_mode
!= \
1197 EpisodeListModel
.VIEW_ALL
:
1198 text
= _('No episodes in current view')
1200 text
= _('No episodes available')
1201 elif role
== TreeViewHelper
.ROLE_PODCASTS
:
1202 if self
.config
.episode_list_view_mode
!= \
1203 EpisodeListModel
.VIEW_ALL
and \
1204 self
.config
.podcast_list_hide_boring
and \
1205 len(self
.channels
) > 0:
1206 text
= _('No podcasts in this view')
1208 text
= _('No subscriptions')
1209 elif role
== TreeViewHelper
.ROLE_DOWNLOADS
:
1210 text
= _('No active downloads')
1212 raise Exception('on_treeview_expose_event: unknown role')
1214 if gpodder
.ui
.fremantle
:
1215 from gpodder
.gtkui
.frmntl
import style
1216 font_desc
= style
.get_font_desc('LargeSystemFont')
1220 draw_text_box_centered(ctx
, treeview
, width
, height
, text
, font_desc
, progress
)
1222 if role
== TreeViewHelper
.ROLE_EPISODES
and \
1223 self
.currently_updating
:
1228 def enable_download_list_update(self
):
1229 if not self
.download_list_update_enabled
:
1230 self
.update_downloads_list()
1231 gobject
.timeout_add(1500, self
.update_downloads_list
)
1232 self
.download_list_update_enabled
= True
1234 def cleanup_downloads(self
):
1235 model
= self
.download_status_model
1237 all_tasks
= [(gtk
.TreeRowReference(model
, row
.path
), row
[0]) for row
in model
]
1238 changed_episode_urls
= set()
1239 for row_reference
, task
in all_tasks
:
1240 if task
.status
in (task
.DONE
, task
.CANCELLED
):
1241 model
.remove(model
.get_iter(row_reference
.get_path()))
1243 # We don't "see" this task anymore - remove it;
1244 # this is needed, so update_episode_list_icons()
1245 # below gets the correct list of "seen" tasks
1246 self
.download_tasks_seen
.remove(task
)
1247 except KeyError, key_error
:
1248 log('Cannot remove task from "seen" list: %s', task
, sender
=self
)
1249 changed_episode_urls
.add(task
.url
)
1250 # Tell the task that it has been removed (so it can clean up)
1251 task
.removed_from_list()
1253 # Tell the podcasts tab to update icons for our removed podcasts
1254 self
.update_episode_list_icons(changed_episode_urls
)
1256 # Tell the shownotes window that we have removed the episode
1257 if self
.episode_shownotes_window
is not None and \
1258 self
.episode_shownotes_window
.episode
is not None and \
1259 self
.episode_shownotes_window
.episode
.url
in changed_episode_urls
:
1260 self
.episode_shownotes_window
._download
_status
_changed
(None)
1262 # Update the downloads list one more time
1263 self
.update_downloads_list(can_call_cleanup
=False)
1265 def on_tool_downloads_toggled(self
, toolbutton
):
1266 if toolbutton
.get_active():
1267 self
.wNotebook
.set_current_page(1)
1269 self
.wNotebook
.set_current_page(0)
1271 def add_download_task_monitor(self
, monitor
):
1272 self
.download_task_monitors
.add(monitor
)
1273 model
= self
.download_status_model
1277 task
= row
[self
.download_status_model
.C_TASK
]
1278 monitor
.task_updated(task
)
1280 def remove_download_task_monitor(self
, monitor
):
1281 self
.download_task_monitors
.remove(monitor
)
1283 def update_downloads_list(self
, can_call_cleanup
=True):
1285 model
= self
.download_status_model
1287 downloading
, failed
, finished
, queued
, paused
, others
= 0, 0, 0, 0, 0, 0
1288 total_speed
, total_size
, done_size
= 0, 0, 0
1290 # Keep a list of all download tasks that we've seen
1291 download_tasks_seen
= set()
1293 # Remember the DownloadTask object for the episode that
1294 # has been opened in the episode shownotes dialog (if any)
1295 if self
.episode_shownotes_window
is not None:
1296 shownotes_episode
= self
.episode_shownotes_window
.episode
1297 shownotes_task
= None
1299 shownotes_episode
= None
1300 shownotes_task
= None
1302 # Do not go through the list of the model is not (yet) available
1306 failed_downloads
= []
1308 self
.download_status_model
.request_update(row
.iter)
1310 task
= row
[self
.download_status_model
.C_TASK
]
1311 speed
, size
, status
, progress
= task
.speed
, task
.total_size
, task
.status
, task
.progress
1313 # Let the download task monitors know of changes
1314 for monitor
in self
.download_task_monitors
:
1315 monitor
.task_updated(task
)
1318 done_size
+= size
*progress
1320 if shownotes_episode
is not None and \
1321 shownotes_episode
.url
== task
.episode
.url
:
1322 shownotes_task
= task
1324 download_tasks_seen
.add(task
)
1326 if status
== download
.DownloadTask
.DOWNLOADING
:
1328 total_speed
+= speed
1329 elif status
== download
.DownloadTask
.FAILED
:
1330 failed_downloads
.append(task
)
1332 elif status
== download
.DownloadTask
.DONE
:
1334 elif status
== download
.DownloadTask
.QUEUED
:
1336 elif status
== download
.DownloadTask
.PAUSED
:
1341 # Remember which tasks we have seen after this run
1342 self
.download_tasks_seen
= download_tasks_seen
1344 if gpodder
.ui
.desktop
:
1345 text
= [_('Downloads')]
1346 if downloading
+ failed
+ queued
> 0:
1349 s
.append(N_('%d active', '%d active', downloading
) % downloading
)
1351 s
.append(N_('%d failed', '%d failed', failed
) % failed
)
1353 s
.append(N_('%d queued', '%d queued', queued
) % queued
)
1354 text
.append(' (' + ', '.join(s
)+')')
1355 self
.labelDownloads
.set_text(''.join(text
))
1356 elif gpodder
.ui
.diablo
:
1357 sum = downloading
+ failed
+ finished
+ queued
+ paused
+ others
1359 self
.tool_downloads
.set_label(_('Downloads (%d)') % sum)
1361 self
.tool_downloads
.set_label(_('Downloads'))
1362 elif gpodder
.ui
.fremantle
:
1363 if downloading
+ queued
> 0:
1364 self
.button_downloads
.set_value(N_('%d active', '%d active', downloading
+queued
) % (downloading
+queued
))
1366 self
.button_downloads
.set_value(N_('%d failed', '%d failed', failed
) % failed
)
1368 self
.button_downloads
.set_value(N_('%d paused', '%d paused', paused
) % paused
)
1370 self
.button_downloads
.set_value(_('Idle'))
1372 title
= [self
.default_title
]
1374 # We have to update all episodes/channels for which the status has
1375 # changed. Accessing task.status_changed has the side effect of
1376 # re-setting the changed flag, so we need to get the "changed" list
1377 # of tuples first and split it into two lists afterwards
1378 changed
= [(task
.url
, task
.podcast_url
) for task
in \
1379 self
.download_tasks_seen
if task
.status_changed
]
1380 episode_urls
= [episode_url
for episode_url
, channel_url
in changed
]
1381 channel_urls
= [channel_url
for episode_url
, channel_url
in changed
]
1383 count
= downloading
+ queued
1385 title
.append(N_('downloading %d file', 'downloading %d files', count
) % count
)
1388 percentage
= 100.0*done_size
/total_size
1391 total_speed
= util
.format_filesize(total_speed
)
1392 title
[1] += ' (%d%%, %s/s)' % (percentage
, total_speed
)
1393 if self
.tray_icon
is not None:
1394 # Update the tray icon status and progress bar
1395 self
.tray_icon
.set_status(self
.tray_icon
.STATUS_DOWNLOAD_IN_PROGRESS
, title
[1])
1396 self
.tray_icon
.draw_progress_bar(percentage
/100.)
1398 if self
.tray_icon
is not None:
1399 # Update the tray icon status
1400 self
.tray_icon
.set_status()
1401 if gpodder
.ui
.desktop
:
1402 self
.downloads_finished(self
.download_tasks_seen
)
1403 if gpodder
.ui
.diablo
:
1404 hildon
.hildon_banner_show_information(self
.gPodder
, '', 'gPodder: %s' % _('All downloads finished'))
1405 log('All downloads have finished.', sender
=self
)
1406 if self
.config
.cmd_all_downloads_complete
:
1407 util
.run_external_command(self
.config
.cmd_all_downloads_complete
)
1409 if gpodder
.ui
.fremantle
and failed
:
1410 message
= '\n'.join(['%s: %s' % (str(task
), \
1411 task
.error_message
) for task
in failed_downloads
])
1412 self
.show_message(message
, _('Downloads failed'), important
=True)
1414 # Remove finished episodes
1415 if self
.config
.auto_cleanup_downloads
and can_call_cleanup
:
1416 self
.cleanup_downloads()
1418 # Stop updating the download list here
1419 self
.download_list_update_enabled
= False
1421 if not gpodder
.ui
.fremantle
:
1422 self
.gPodder
.set_title(' - '.join(title
))
1424 self
.update_episode_list_icons(episode_urls
)
1425 if self
.episode_shownotes_window
is not None:
1426 if (shownotes_task
and shownotes_task
.url
in episode_urls
) or \
1427 shownotes_task
!= self
.episode_shownotes_window
.task
:
1428 self
.episode_shownotes_window
._download
_status
_changed
(shownotes_task
)
1429 self
.episode_shownotes_window
._download
_status
_progress
()
1430 self
.play_or_download()
1432 self
.update_podcast_list_model(channel_urls
)
1434 return self
.download_list_update_enabled
1435 except Exception, e
:
1436 log('Exception happened while updating download list.', sender
=self
, traceback
=True)
1437 self
.show_message('%s\n\n%s' % (_('Please report this problem and restart gPodder:'), str(e
)), _('Unhandled exception'), important
=True)
1438 # We return False here, so the update loop won't be called again,
1439 # that's why we require the restart of gPodder in the message.
1442 def on_config_changed(self
, *args
):
1443 util
.idle_add(self
._on
_config
_changed
, *args
)
1445 def _on_config_changed(self
, name
, old_value
, new_value
):
1446 if name
== 'show_toolbar' and gpodder
.ui
.desktop
:
1447 self
.toolbar
.set_property('visible', new_value
)
1448 elif name
== 'videoplayer':
1449 self
.config
.video_played_dbus
= False
1450 elif name
== 'player':
1451 self
.config
.audio_played_dbus
= False
1452 elif name
== 'episode_list_descriptions':
1453 self
.update_episode_list_model()
1454 elif name
== 'episode_list_thumbnails':
1455 self
.update_episode_list_icons(all
=True)
1456 elif name
== 'rotation_mode':
1457 self
._fremantle
_rotation
.set_mode(new_value
)
1458 elif name
in ('auto_update_feeds', 'auto_update_frequency'):
1459 self
.restart_auto_update_timer()
1460 elif name
== 'podcast_list_view_all':
1461 # Force a update of the podcast list model
1462 self
.channel_list_changed
= True
1463 if gpodder
.ui
.fremantle
:
1464 hildon
.hildon_gtk_window_set_progress_indicator(self
.main_window
, True)
1465 while gtk
.events_pending():
1466 gtk
.main_iteration(False)
1467 self
.update_podcast_list_model()
1468 if gpodder
.ui
.fremantle
:
1469 hildon
.hildon_gtk_window_set_progress_indicator(self
.main_window
, False)
1471 def on_treeview_query_tooltip(self
, treeview
, x
, y
, keyboard_tooltip
, tooltip
):
1472 # With get_bin_window, we get the window that contains the rows without
1473 # the header. The Y coordinate of this window will be the height of the
1474 # treeview header. This is the amount we have to subtract from the
1475 # event's Y coordinate to get the coordinate to pass to get_path_at_pos
1476 (x_bin
, y_bin
) = treeview
.get_bin_window().get_position()
1479 (path
, column
, rx
, ry
) = treeview
.get_path_at_pos( x
, y
) or (None,)*4
1481 if not getattr(treeview
, TreeViewHelper
.CAN_TOOLTIP
) or (column
is not None and column
!= treeview
.get_columns()[0]):
1482 setattr(treeview
, TreeViewHelper
.LAST_TOOLTIP
, None)
1485 if path
is not None:
1486 model
= treeview
.get_model()
1487 iter = model
.get_iter(path
)
1488 role
= getattr(treeview
, TreeViewHelper
.ROLE
)
1490 if role
== TreeViewHelper
.ROLE_EPISODES
:
1491 id = model
.get_value(iter, EpisodeListModel
.C_URL
)
1492 elif role
== TreeViewHelper
.ROLE_PODCASTS
:
1493 id = model
.get_value(iter, PodcastListModel
.C_URL
)
1495 last_tooltip
= getattr(treeview
, TreeViewHelper
.LAST_TOOLTIP
)
1496 if last_tooltip
is not None and last_tooltip
!= id:
1497 setattr(treeview
, TreeViewHelper
.LAST_TOOLTIP
, None)
1499 setattr(treeview
, TreeViewHelper
.LAST_TOOLTIP
, id)
1501 if role
== TreeViewHelper
.ROLE_EPISODES
:
1502 description
= model
.get_value(iter, EpisodeListModel
.C_TOOLTIP
)
1504 tooltip
.set_text(description
)
1507 elif role
== TreeViewHelper
.ROLE_PODCASTS
:
1508 channel
= model
.get_value(iter, PodcastListModel
.C_CHANNEL
)
1511 channel
.request_save_dir_size()
1512 diskspace_str
= util
.format_filesize(channel
.save_dir_size
, 0)
1513 error_str
= model
.get_value(iter, PodcastListModel
.C_ERROR
)
1515 error_str
= _('Feedparser error: %s') % saxutils
.escape(error_str
.strip())
1516 error_str
= '<span foreground="#ff0000">%s</span>' % error_str
1517 table
= gtk
.Table(rows
=3, columns
=3)
1518 table
.set_row_spacings(5)
1519 table
.set_col_spacings(5)
1520 table
.set_border_width(5)
1522 heading
= gtk
.Label()
1523 heading
.set_alignment(0, 1)
1524 heading
.set_markup('<b><big>%s</big></b>\n<small>%s</small>' % (saxutils
.escape(channel
.title
), saxutils
.escape(channel
.url
)))
1525 table
.attach(heading
, 0, 1, 0, 1)
1526 size_info
= gtk
.Label()
1527 size_info
.set_alignment(1, 1)
1528 size_info
.set_justify(gtk
.JUSTIFY_RIGHT
)
1529 size_info
.set_markup('<b>%s</b>\n<small>%s</small>' % (diskspace_str
, _('disk usage')))
1530 table
.attach(size_info
, 2, 3, 0, 1)
1532 table
.attach(gtk
.HSeparator(), 0, 3, 1, 2)
1534 if len(channel
.description
) < 500:
1535 description
= channel
.description
1537 pos
= channel
.description
.find('\n\n')
1538 if pos
== -1 or pos
> 500:
1539 description
= channel
.description
[:498]+'[...]'
1541 description
= channel
.description
[:pos
]
1543 description
= gtk
.Label(description
)
1545 description
.set_markup(error_str
)
1546 description
.set_alignment(0, 0)
1547 description
.set_line_wrap(True)
1548 table
.attach(description
, 0, 3, 2, 3)
1551 tooltip
.set_custom(table
)
1555 setattr(treeview
, TreeViewHelper
.LAST_TOOLTIP
, None)
1558 def treeview_allow_tooltips(self
, treeview
, allow
):
1559 setattr(treeview
, TreeViewHelper
.CAN_TOOLTIP
, allow
)
1561 def update_m3u_playlist_clicked(self
, widget
):
1562 if self
.active_channel
is not None:
1563 self
.active_channel
.update_m3u_playlist()
1564 self
.show_message(_('Updated M3U playlist in download folder.'), _('Updated playlist'), widget
=self
.treeChannels
)
1566 def treeview_handle_context_menu_click(self
, treeview
, event
):
1567 x
, y
= int(event
.x
), int(event
.y
)
1568 path
, column
, rx
, ry
= treeview
.get_path_at_pos(x
, y
) or (None,)*4
1570 selection
= treeview
.get_selection()
1571 model
, paths
= selection
.get_selected_rows()
1573 if path
is None or (path
not in paths
and \
1574 event
.button
== self
.context_menu_mouse_button
):
1575 # We have right-clicked, but not into the selection,
1576 # assume we don't want to operate on the selection
1579 if path
is not None and not paths
and \
1580 event
.button
== self
.context_menu_mouse_button
:
1581 # No selection or clicked outside selection;
1582 # select the single item where we clicked
1583 treeview
.grab_focus()
1584 treeview
.set_cursor(path
, column
, 0)
1588 # Unselect any remaining items (clicked elsewhere)
1589 if hasattr(treeview
, 'is_rubber_banding_active'):
1590 if not treeview
.is_rubber_banding_active():
1591 selection
.unselect_all()
1593 selection
.unselect_all()
1597 def downloads_list_get_selection(self
, model
=None, paths
=None):
1598 if model
is None and paths
is None:
1599 selection
= self
.treeDownloads
.get_selection()
1600 model
, paths
= selection
.get_selected_rows()
1602 can_queue
, can_cancel
, can_pause
, can_remove
, can_force
= (True,)*5
1603 selected_tasks
= [(gtk
.TreeRowReference(model
, path
), \
1604 model
.get_value(model
.get_iter(path
), \
1605 DownloadStatusModel
.C_TASK
)) for path
in paths
]
1607 for row_reference
, task
in selected_tasks
:
1608 if task
.status
!= download
.DownloadTask
.QUEUED
:
1610 if task
.status
not in (download
.DownloadTask
.PAUSED
, \
1611 download
.DownloadTask
.FAILED
, \
1612 download
.DownloadTask
.CANCELLED
):
1614 if task
.status
not in (download
.DownloadTask
.PAUSED
, \
1615 download
.DownloadTask
.QUEUED
, \
1616 download
.DownloadTask
.DOWNLOADING
, \
1617 download
.DownloadTask
.FAILED
):
1619 if task
.status
not in (download
.DownloadTask
.QUEUED
, \
1620 download
.DownloadTask
.DOWNLOADING
):
1622 if task
.status
not in (download
.DownloadTask
.CANCELLED
, \
1623 download
.DownloadTask
.FAILED
, \
1624 download
.DownloadTask
.DONE
):
1627 return selected_tasks
, can_queue
, can_cancel
, can_pause
, can_remove
, can_force
1629 def downloads_finished(self
, download_tasks_seen
):
1630 # FIXME: Filter all tasks that have already been reported
1631 finished_downloads
= [str(task
) for task
in download_tasks_seen
if task
.status
== task
.DONE
]
1632 failed_downloads
= [str(task
)+' ('+task
.error_message
+')' for task
in download_tasks_seen
if task
.status
== task
.FAILED
]
1634 if finished_downloads
and failed_downloads
:
1635 message
= self
.format_episode_list(finished_downloads
, 5)
1636 message
+= '\n\n<i>%s</i>\n' % _('These downloads failed:')
1637 message
+= self
.format_episode_list(failed_downloads
, 5)
1638 self
.show_message(message
, _('Downloads finished'), True, widget
=self
.labelDownloads
)
1639 elif finished_downloads
:
1640 message
= self
.format_episode_list(finished_downloads
)
1641 self
.show_message(message
, _('Downloads finished'), widget
=self
.labelDownloads
)
1642 elif failed_downloads
:
1643 message
= self
.format_episode_list(failed_downloads
)
1644 self
.show_message(message
, _('Downloads failed'), True, widget
=self
.labelDownloads
)
1646 # Open torrent files right after download (bug 1029)
1647 if self
.config
.open_torrent_after_download
:
1648 for task
in download_tasks_seen
:
1649 if task
.status
!= task
.DONE
:
1652 episode
= task
.episode
1653 if episode
.mimetype
!= 'application/x-bittorrent':
1656 self
.playback_episodes([episode
])
1659 def format_episode_list(self
, episode_list
, max_episodes
=10):
1661 Format a list of episode names for notifications
1663 Will truncate long episode names and limit the amount of
1664 episodes displayed (max_episodes=10).
1666 The episode_list parameter should be a list of strings.
1668 MAX_TITLE_LENGTH
= 100
1671 for title
in episode_list
[:min(len(episode_list
), max_episodes
)]:
1672 if len(title
) > MAX_TITLE_LENGTH
:
1673 middle
= (MAX_TITLE_LENGTH
/2)-2
1674 title
= '%s...%s' % (title
[0:middle
], title
[-middle
:])
1675 result
.append(saxutils
.escape(title
))
1678 more_episodes
= len(episode_list
) - max_episodes
1679 if more_episodes
> 0:
1680 result
.append('(...')
1681 result
.append(N_('%d more episode', '%d more episodes', more_episodes
) % more_episodes
)
1682 result
.append('...)')
1684 return (''.join(result
)).strip()
1686 def _for_each_task_set_status(self
, tasks
, status
, force_start
=False):
1687 episode_urls
= set()
1688 model
= self
.treeDownloads
.get_model()
1689 for row_reference
, task
in tasks
:
1690 if status
== download
.DownloadTask
.QUEUED
:
1691 # Only queue task when its paused/failed/cancelled (or forced)
1692 if task
.status
in (task
.PAUSED
, task
.FAILED
, task
.CANCELLED
) or force_start
:
1693 self
.download_queue_manager
.add_task(task
, force_start
)
1694 self
.enable_download_list_update()
1695 elif status
== download
.DownloadTask
.CANCELLED
:
1696 # Cancelling a download allowed when downloading/queued
1697 if task
.status
in (task
.QUEUED
, task
.DOWNLOADING
):
1698 task
.status
= status
1699 # Cancelling paused/failed downloads requires a call to .run()
1700 elif task
.status
in (task
.PAUSED
, task
.FAILED
):
1701 task
.status
= status
1702 # Call run, so the partial file gets deleted
1704 elif status
== download
.DownloadTask
.PAUSED
:
1705 # Pausing a download only when queued/downloading
1706 if task
.status
in (task
.DOWNLOADING
, task
.QUEUED
):
1707 task
.status
= status
1708 elif status
is None:
1709 # Remove the selected task - cancel downloading/queued tasks
1710 if task
.status
in (task
.QUEUED
, task
.DOWNLOADING
):
1711 task
.status
= task
.CANCELLED
1712 model
.remove(model
.get_iter(row_reference
.get_path()))
1713 # Remember the URL, so we can tell the UI to update
1715 # We don't "see" this task anymore - remove it;
1716 # this is needed, so update_episode_list_icons()
1717 # below gets the correct list of "seen" tasks
1718 self
.download_tasks_seen
.remove(task
)
1719 except KeyError, key_error
:
1720 log('Cannot remove task from "seen" list: %s', task
, sender
=self
)
1721 episode_urls
.add(task
.url
)
1722 # Tell the task that it has been removed (so it can clean up)
1723 task
.removed_from_list()
1725 # We can (hopefully) simply set the task status here
1726 task
.status
= status
1727 # Tell the podcasts tab to update icons for our removed podcasts
1728 self
.update_episode_list_icons(episode_urls
)
1729 # Update the tab title and downloads list
1730 self
.update_downloads_list()
1732 def treeview_downloads_show_context_menu(self
, treeview
, event
):
1733 model
, paths
= self
.treeview_handle_context_menu_click(treeview
, event
)
1735 if not hasattr(treeview
, 'is_rubber_banding_active'):
1738 return not treeview
.is_rubber_banding_active()
1740 if event
.button
== self
.context_menu_mouse_button
:
1741 selected_tasks
, can_queue
, can_cancel
, can_pause
, can_remove
, can_force
= \
1742 self
.downloads_list_get_selection(model
, paths
)
1744 def make_menu_item(label
, stock_id
, tasks
, status
, sensitive
, force_start
=False):
1745 # This creates a menu item for selection-wide actions
1746 item
= gtk
.ImageMenuItem(label
)
1747 item
.set_image(gtk
.image_new_from_stock(stock_id
, gtk
.ICON_SIZE_MENU
))
1748 item
.connect('activate', lambda item
: self
._for
_each
_task
_set
_status
(tasks
, status
, force_start
))
1749 item
.set_sensitive(sensitive
)
1750 return self
.set_finger_friendly(item
)
1754 item
= gtk
.ImageMenuItem(_('Episode details'))
1755 item
.set_image(gtk
.image_new_from_stock(gtk
.STOCK_INFO
, gtk
.ICON_SIZE_MENU
))
1756 if len(selected_tasks
) == 1:
1757 row_reference
, task
= selected_tasks
[0]
1758 episode
= task
.episode
1759 item
.connect('activate', lambda item
: self
.show_episode_shownotes(episode
))
1761 item
.set_sensitive(False)
1762 menu
.append(self
.set_finger_friendly(item
))
1763 menu
.append(gtk
.SeparatorMenuItem())
1765 menu
.append(make_menu_item(_('Start download now'), gtk
.STOCK_GO_DOWN
, selected_tasks
, download
.DownloadTask
.QUEUED
, True, True))
1767 menu
.append(make_menu_item(_('Download'), gtk
.STOCK_GO_DOWN
, selected_tasks
, download
.DownloadTask
.QUEUED
, can_queue
, False))
1768 menu
.append(make_menu_item(_('Cancel'), gtk
.STOCK_CANCEL
, selected_tasks
, download
.DownloadTask
.CANCELLED
, can_cancel
))
1769 menu
.append(make_menu_item(_('Pause'), gtk
.STOCK_MEDIA_PAUSE
, selected_tasks
, download
.DownloadTask
.PAUSED
, can_pause
))
1770 menu
.append(gtk
.SeparatorMenuItem())
1771 menu
.append(make_menu_item(_('Remove from list'), gtk
.STOCK_REMOVE
, selected_tasks
, None, can_remove
))
1773 if gpodder
.ui
.maemo
or self
.config
.enable_fingerscroll
:
1774 # Because we open the popup on left-click for Maemo,
1775 # we also include a non-action to close the menu
1776 menu
.append(gtk
.SeparatorMenuItem())
1777 item
= gtk
.ImageMenuItem(_('Close this menu'))
1778 item
.set_image(gtk
.image_new_from_stock(gtk
.STOCK_CLOSE
, gtk
.ICON_SIZE_MENU
))
1780 menu
.append(self
.set_finger_friendly(item
))
1783 menu
.popup(None, None, None, event
.button
, event
.time
)
1786 def treeview_channels_show_context_menu(self
, treeview
, event
):
1787 model
, paths
= self
.treeview_handle_context_menu_click(treeview
, event
)
1791 # Check for valid channel id, if there's no id then
1792 # assume that it is a proxy channel or equivalent
1793 # and cannot be operated with right click
1794 if self
.active_channel
.id is None:
1797 if event
.button
== 3:
1802 item
= gtk
.ImageMenuItem( _('Update podcast'))
1803 item
.set_image(gtk
.image_new_from_stock(gtk
.STOCK_REFRESH
, gtk
.ICON_SIZE_MENU
))
1804 item
.connect('activate', self
.on_itemUpdateChannel_activate
)
1805 item
.set_sensitive(not self
.updating_feed_cache
)
1808 menu
.append(gtk
.SeparatorMenuItem())
1810 item
= gtk
.CheckMenuItem(_('Keep episodes'))
1811 item
.set_active(self
.active_channel
.channel_is_locked
)
1812 item
.connect('activate', self
.on_channel_toggle_lock_activate
)
1813 menu
.append(self
.set_finger_friendly(item
))
1815 item
= gtk
.ImageMenuItem(_('Remove podcast'))
1816 item
.set_image(gtk
.image_new_from_stock(gtk
.STOCK_DELETE
, gtk
.ICON_SIZE_MENU
))
1817 item
.connect( 'activate', self
.on_itemRemoveChannel_activate
)
1820 if self
.config
.device_type
!= 'none':
1821 item
= gtk
.MenuItem(_('Synchronize to device'))
1822 item
.connect('activate', lambda item
: self
.on_sync_to_ipod_activate(item
, self
.active_channel
.get_downloaded_episodes()))
1825 menu
.append( gtk
.SeparatorMenuItem())
1827 item
= gtk
.ImageMenuItem(_('Podcast details'))
1828 item
.set_image(gtk
.image_new_from_stock(gtk
.STOCK_INFO
, gtk
.ICON_SIZE_MENU
))
1829 item
.connect('activate', self
.on_itemEditChannel_activate
)
1833 # Disable tooltips while we are showing the menu, so
1834 # the tooltip will not appear over the menu
1835 self
.treeview_allow_tooltips(self
.treeChannels
, False)
1836 menu
.connect('deactivate', lambda menushell
: self
.treeview_allow_tooltips(self
.treeChannels
, True))
1837 menu
.popup( None, None, None, event
.button
, event
.time
)
1841 def on_itemClose_activate(self
, widget
):
1842 if self
.tray_icon
is not None:
1843 self
.iconify_main_window()
1845 self
.on_gPodder_delete_event(widget
)
1847 def cover_file_removed(self
, channel_url
):
1849 The Cover Downloader calls this when a previously-
1850 available cover has been removed from the disk. We
1851 have to update our model to reflect this change.
1853 self
.podcast_list_model
.delete_cover_by_url(channel_url
)
1855 def cover_download_finished(self
, channel
, pixbuf
):
1857 The Cover Downloader calls this when it has finished
1858 downloading (or registering, if already downloaded)
1859 a new channel cover, which is ready for displaying.
1861 self
.podcast_list_model
.add_cover_by_channel(channel
, pixbuf
)
1863 def save_episodes_as_file(self
, episodes
):
1864 for episode
in episodes
:
1865 self
.save_episode_as_file(episode
)
1867 def save_episode_as_file(self
, episode
):
1868 PRIVATE_FOLDER_ATTRIBUTE
= '_save_episodes_as_file_folder'
1869 if episode
.was_downloaded(and_exists
=True):
1870 folder
= getattr(self
, PRIVATE_FOLDER_ATTRIBUTE
, None)
1871 copy_from
= episode
.local_filename(create
=False)
1872 assert copy_from
is not None
1873 copy_to
= episode
.sync_filename(self
.config
.custom_sync_name_enabled
, self
.config
.custom_sync_name
)
1874 (result
, folder
) = self
.show_copy_dialog(src_filename
=copy_from
, dst_filename
=copy_to
, dst_directory
=folder
)
1875 setattr(self
, PRIVATE_FOLDER_ATTRIBUTE
, folder
)
1877 def copy_episodes_bluetooth(self
, episodes
):
1878 episodes_to_copy
= [e
for e
in episodes
if e
.was_downloaded(and_exists
=True)]
1880 if gpodder
.ui
.maemo
:
1881 util
.bluetooth_send_files_maemo([e
.local_filename(create
=False) \
1882 for e
in episodes_to_copy
])
1885 def convert_and_send_thread(episode
):
1886 for episode
in episodes
:
1887 filename
= episode
.local_filename(create
=False)
1888 assert filename
is not None
1889 destfile
= os
.path
.join(tempfile
.gettempdir(), \
1890 util
.sanitize_filename(episode
.sync_filename(self
.config
.custom_sync_name_enabled
, self
.config
.custom_sync_name
)))
1891 (base
, ext
) = os
.path
.splitext(filename
)
1892 if not destfile
.endswith(ext
):
1896 shutil
.copyfile(filename
, destfile
)
1897 util
.bluetooth_send_file(destfile
)
1899 log('Cannot copy "%s" to "%s".', filename
, destfile
, sender
=self
)
1900 self
.notification(_('Error converting file.'), _('Bluetooth file transfer'), important
=True)
1902 util
.delete_file(destfile
)
1904 threading
.Thread(target
=convert_and_send_thread
, args
=[episodes_to_copy
]).start()
1906 def get_device_name(self
):
1907 if self
.config
.device_type
== 'ipod':
1909 elif self
.config
.device_type
in ('filesystem', 'mtp'):
1910 return _('MP3 player')
1912 return '(unknown device)'
1914 def _treeview_button_released(self
, treeview
, event
):
1915 xpos
, ypos
= TreeViewHelper
.get_button_press_event(treeview
)
1916 dy
= int(abs(event
.y
-ypos
))
1917 dx
= int(event
.x
-xpos
)
1919 selection
= treeview
.get_selection()
1920 path
= treeview
.get_path_at_pos(int(event
.x
), int(event
.y
))
1921 if path
is None or dy
> 30:
1922 return (False, dx
, dy
)
1924 path
, column
, x
, y
= path
1925 selection
.select_path(path
)
1926 treeview
.set_cursor(path
)
1927 treeview
.grab_focus()
1929 return (True, dx
, dy
)
1931 def treeview_channels_handle_gestures(self
, treeview
, event
):
1932 if self
.currently_updating
:
1935 selected
, dx
, dy
= self
._treeview
_button
_released
(treeview
, event
)
1938 if self
.config
.maemo_enable_gestures
:
1940 self
.on_itemUpdateChannel_activate()
1942 self
.on_itemEditChannel_activate(treeview
)
1946 def treeview_available_handle_gestures(self
, treeview
, event
):
1947 selected
, dx
, dy
= self
._treeview
_button
_released
(treeview
, event
)
1950 if self
.config
.maemo_enable_gestures
:
1952 self
.on_playback_selected_episodes(None)
1955 self
.on_shownotes_selected_episodes(None)
1958 # Pass the event to the context menu handler for treeAvailable
1959 self
.treeview_available_show_context_menu(treeview
, event
)
1963 def treeview_available_show_context_menu(self
, treeview
, event
):
1964 model
, paths
= self
.treeview_handle_context_menu_click(treeview
, event
)
1966 if not hasattr(treeview
, 'is_rubber_banding_active'):
1969 return not treeview
.is_rubber_banding_active()
1971 if event
.button
== self
.context_menu_mouse_button
:
1972 episodes
= self
.get_selected_episodes()
1973 any_locked
= any(e
.is_locked
for e
in episodes
)
1974 any_played
= any(e
.is_played
for e
in episodes
)
1975 one_is_new
= any(e
.state
== gpodder
.STATE_NORMAL
and not e
.is_played
for e
in episodes
)
1976 downloaded
= all(e
.was_downloaded(and_exists
=True) for e
in episodes
)
1977 downloading
= any(self
.episode_is_downloading(e
) for e
in episodes
)
1981 (can_play
, can_download
, can_transfer
, can_cancel
, can_delete
, open_instead_of_play
) = self
.play_or_download()
1983 if open_instead_of_play
:
1984 item
= gtk
.ImageMenuItem(gtk
.STOCK_OPEN
)
1986 item
= gtk
.ImageMenuItem(gtk
.STOCK_MEDIA_PLAY
)
1988 item
= gtk
.ImageMenuItem(_('Stream'))
1989 item
.set_image(gtk
.image_new_from_stock(gtk
.STOCK_MEDIA_PLAY
, gtk
.ICON_SIZE_MENU
))
1991 item
.set_sensitive(can_play
and not downloading
)
1992 item
.connect('activate', self
.on_playback_selected_episodes
)
1993 menu
.append(self
.set_finger_friendly(item
))
1996 item
= gtk
.ImageMenuItem(_('Download'))
1997 item
.set_image(gtk
.image_new_from_stock(gtk
.STOCK_GO_DOWN
, gtk
.ICON_SIZE_MENU
))
1998 item
.set_sensitive(can_download
)
1999 item
.connect('activate', self
.on_download_selected_episodes
)
2000 menu
.append(self
.set_finger_friendly(item
))
2002 item
= gtk
.ImageMenuItem(gtk
.STOCK_CANCEL
)
2003 item
.connect('activate', self
.on_item_cancel_download_activate
)
2004 menu
.append(self
.set_finger_friendly(item
))
2006 item
= gtk
.ImageMenuItem(gtk
.STOCK_DELETE
)
2007 item
.set_sensitive(can_delete
)
2008 item
.connect('activate', self
.on_btnDownloadedDelete_clicked
)
2009 menu
.append(self
.set_finger_friendly(item
))
2013 # Ok, this probably makes sense to only display for downloaded files
2015 menu
.append(gtk
.SeparatorMenuItem())
2016 share_item
= gtk
.MenuItem(_('Send to'))
2017 menu
.append(self
.set_finger_friendly(share_item
))
2018 share_menu
= gtk
.Menu()
2020 item
= gtk
.ImageMenuItem(_('Local folder'))
2021 item
.set_image(gtk
.image_new_from_stock(gtk
.STOCK_DIRECTORY
, gtk
.ICON_SIZE_MENU
))
2022 item
.connect('button-press-event', lambda w
, ee
: self
.save_episodes_as_file(episodes
))
2023 share_menu
.append(self
.set_finger_friendly(item
))
2024 if self
.bluetooth_available
:
2025 item
= gtk
.ImageMenuItem(_('Bluetooth device'))
2026 if gpodder
.ui
.maemo
:
2027 icon_name
= ICON('qgn_list_filesys_bluetooth')
2029 icon_name
= ICON('bluetooth')
2030 item
.set_image(gtk
.image_new_from_icon_name(icon_name
, gtk
.ICON_SIZE_MENU
))
2031 item
.connect('button-press-event', lambda w
, ee
: self
.copy_episodes_bluetooth(episodes
))
2032 share_menu
.append(self
.set_finger_friendly(item
))
2034 item
= gtk
.ImageMenuItem(self
.get_device_name())
2035 item
.set_image(gtk
.image_new_from_icon_name(ICON('multimedia-player'), gtk
.ICON_SIZE_MENU
))
2036 item
.connect('button-press-event', lambda w
, ee
: self
.on_sync_to_ipod_activate(w
, episodes
))
2037 share_menu
.append(self
.set_finger_friendly(item
))
2039 share_item
.set_submenu(share_menu
)
2041 if (downloaded
or one_is_new
or can_download
) and not downloading
:
2042 menu
.append(gtk
.SeparatorMenuItem())
2044 item
= gtk
.CheckMenuItem(_('New'))
2045 item
.set_active(True)
2046 item
.connect('activate', lambda w
: self
.mark_selected_episodes_old())
2047 menu
.append(self
.set_finger_friendly(item
))
2049 item
= gtk
.CheckMenuItem(_('New'))
2050 item
.set_active(False)
2051 item
.connect('activate', lambda w
: self
.mark_selected_episodes_new())
2052 menu
.append(self
.set_finger_friendly(item
))
2055 item
= gtk
.CheckMenuItem(_('Played'))
2056 item
.set_active(any_played
)
2057 item
.connect( 'activate', lambda w
: self
.on_item_toggle_played_activate( w
, False, not any_played
))
2058 menu
.append(self
.set_finger_friendly(item
))
2060 item
= gtk
.CheckMenuItem(_('Keep episode'))
2061 item
.set_active(any_locked
)
2062 item
.connect('activate', lambda w
: self
.on_item_toggle_lock_activate( w
, False, not any_locked
))
2063 menu
.append(self
.set_finger_friendly(item
))
2065 menu
.append(gtk
.SeparatorMenuItem())
2066 # Single item, add episode information menu item
2067 item
= gtk
.ImageMenuItem(_('Episode details'))
2068 item
.set_image(gtk
.image_new_from_stock( gtk
.STOCK_INFO
, gtk
.ICON_SIZE_MENU
))
2069 item
.connect('activate', lambda w
: self
.show_episode_shownotes(episodes
[0]))
2070 menu
.append(self
.set_finger_friendly(item
))
2072 if gpodder
.ui
.maemo
or self
.config
.enable_fingerscroll
:
2073 # Because we open the popup on left-click for Maemo,
2074 # we also include a non-action to close the menu
2075 menu
.append(gtk
.SeparatorMenuItem())
2076 item
= gtk
.ImageMenuItem(_('Close this menu'))
2077 item
.set_image(gtk
.image_new_from_stock(gtk
.STOCK_CLOSE
, gtk
.ICON_SIZE_MENU
))
2078 menu
.append(self
.set_finger_friendly(item
))
2081 # Disable tooltips while we are showing the menu, so
2082 # the tooltip will not appear over the menu
2083 self
.treeview_allow_tooltips(self
.treeAvailable
, False)
2084 menu
.connect('deactivate', lambda menushell
: self
.treeview_allow_tooltips(self
.treeAvailable
, True))
2085 menu
.popup( None, None, None, event
.button
, event
.time
)
2089 def set_title(self
, new_title
):
2090 if not gpodder
.ui
.fremantle
:
2091 self
.default_title
= new_title
2092 self
.gPodder
.set_title(new_title
)
2094 def update_episode_list_icons(self
, urls
=None, selected
=False, all
=False):
2096 Updates the status icons in the episode list.
2098 If urls is given, it should be a list of URLs
2099 of episodes that should be updated.
2101 If urls is None, set ONE OF selected, all to
2102 True (the former updates just the selected
2103 episodes and the latter updates all episodes).
2105 additional_args
= (self
.episode_is_downloading
, \
2106 self
.config
.episode_list_descriptions
and gpodder
.ui
.desktop
, \
2107 self
.config
.episode_list_thumbnails
and gpodder
.ui
.desktop
)
2109 if urls
is not None:
2110 # We have a list of URLs to walk through
2111 self
.episode_list_model
.update_by_urls(urls
, *additional_args
)
2112 elif selected
and not all
:
2113 # We should update all selected episodes
2114 selection
= self
.treeAvailable
.get_selection()
2115 model
, paths
= selection
.get_selected_rows()
2116 for path
in reversed(paths
):
2117 iter = model
.get_iter(path
)
2118 self
.episode_list_model
.update_by_filter_iter(iter, \
2120 elif all
and not selected
:
2121 # We update all (even the filter-hidden) episodes
2122 self
.episode_list_model
.update_all(*additional_args
)
2124 # Wrong/invalid call - have to specify at least one parameter
2125 raise ValueError('Invalid call to update_episode_list_icons')
2127 def episode_list_status_changed(self
, episodes
):
2128 self
.update_episode_list_icons(set(e
.url
for e
in episodes
))
2129 self
.update_podcast_list_model(set(e
.channel
.url
for e
in episodes
))
2132 def clean_up_downloads(self
, delete_partial
=False):
2133 # Clean up temporary files left behind by old gPodder versions
2134 temporary_files
= glob
.glob('%s/*/.tmp-*' % self
.config
.download_dir
)
2137 temporary_files
+= glob
.glob('%s/*/*.partial' % self
.config
.download_dir
)
2139 for tempfile
in temporary_files
:
2140 util
.delete_file(tempfile
)
2142 # Clean up empty download folders and abandoned download folders
2143 download_dirs
= glob
.glob(os
.path
.join(self
.config
.download_dir
, '*'))
2144 for ddir
in download_dirs
:
2145 if os
.path
.isdir(ddir
) and False: # FIXME not db.channel_foldername_exists(os.path.basename(ddir)):
2146 globr
= glob
.glob(os
.path
.join(ddir
, '*'))
2147 if len(globr
) == 0 or (len(globr
) == 1 and globr
[0].endswith('/cover')):
2148 log('Stale download directory found: %s', os
.path
.basename(ddir
), sender
=self
)
2149 shutil
.rmtree(ddir
, ignore_errors
=True)
2151 def streaming_possible(self
):
2152 if gpodder
.ui
.desktop
:
2153 # User has to have a media player set on the Desktop, or else we
2154 # would probably open the browser when giving a URL to xdg-open..
2155 return (self
.config
.player
and self
.config
.player
!= 'default')
2156 elif gpodder
.ui
.maemo
:
2157 # On Maemo, the default is to use the Nokia Media Player, which is
2158 # already able to deal with HTTP URLs the right way, so we
2159 # unconditionally enable streaming always on Maemo
2164 def playback_episodes_for_real(self
, episodes
):
2165 groups
= collections
.defaultdict(list)
2166 for episode
in episodes
:
2167 file_type
= episode
.file_type()
2168 if file_type
== 'video' and self
.config
.videoplayer
and \
2169 self
.config
.videoplayer
!= 'default':
2170 player
= self
.config
.videoplayer
2171 if gpodder
.ui
.diablo
:
2172 # Use the wrapper script if it's installed to crop 3GP YouTube
2173 # videos to fit the screen (looks much nicer than w/ black border)
2174 if player
== 'mplayer' and util
.find_command('gpodder-mplayer'):
2175 player
= 'gpodder-mplayer'
2176 elif gpodder
.ui
.fremantle
and player
== 'mplayer':
2177 player
= 'mplayer -fs %F'
2178 elif file_type
== 'audio' and self
.config
.player
and \
2179 self
.config
.player
!= 'default':
2180 player
= self
.config
.player
2184 if file_type
not in ('audio', 'video') or \
2185 (file_type
== 'audio' and not self
.config
.audio_played_dbus
) or \
2186 (file_type
== 'video' and not self
.config
.video_played_dbus
):
2187 # Mark episode as played in the database
2188 episode
.mark(is_played
=True)
2189 self
.mygpo_client
.on_playback([episode
])
2191 filename
= episode
.local_filename(create
=False)
2192 if filename
is None or not os
.path
.exists(filename
):
2193 filename
= episode
.url
2194 if youtube
.is_video_link(filename
):
2195 fmt_id
= self
.config
.youtube_preferred_fmt_id
2196 if gpodder
.ui
.fremantle
:
2198 filename
= youtube
.get_real_download_url(filename
, fmt_id
)
2200 # Determine the playback resume position - if the file
2201 # was played 100%, we simply start from the beginning
2202 resume_position
= episode
.current_position
2203 if resume_position
== episode
.total_time
:
2206 if gpodder
.ui
.fremantle
:
2207 self
.mafw_monitor
.set_resume_point(filename
, resume_position
)
2209 # If Panucci is configured, use D-Bus on Maemo to call it
2210 if player
== 'panucci':
2212 PANUCCI_NAME
= 'org.panucci.panucciInterface'
2213 PANUCCI_PATH
= '/panucciInterface'
2214 PANUCCI_INTF
= 'org.panucci.panucciInterface'
2215 o
= gpodder
.dbus_session_bus
.get_object(PANUCCI_NAME
, PANUCCI_PATH
)
2216 i
= dbus
.Interface(o
, PANUCCI_INTF
)
2218 def on_reply(*args
):
2221 def error_handler(filename
, err
):
2222 log('Exception in D-Bus call: %s', str(err
), \
2225 # Fallback: use the command line client
2226 for command
in util
.format_desktop_command('panucci', \
2228 log('Executing: %s', repr(command
), sender
=self
)
2229 subprocess
.Popen(command
)
2231 on_error
= lambda err
: error_handler(filename
, err
)
2233 # This method only exists in Panucci > 0.9 ('new Panucci')
2234 i
.playback_from(filename
, resume_position
, \
2235 reply_handler
=on_reply
, error_handler
=on_error
)
2237 continue # This file was handled by the D-Bus call
2238 except Exception, e
:
2239 log('Error calling Panucci using D-Bus', sender
=self
, traceback
=True)
2240 elif player
== 'MediaBox' and gpodder
.ui
.maemo
:
2242 MEDIABOX_NAME
= 'de.pycage.mediabox'
2243 MEDIABOX_PATH
= '/de/pycage/mediabox/control'
2244 MEDIABOX_INTF
= 'de.pycage.mediabox.control'
2245 o
= gpodder
.dbus_session_bus
.get_object(MEDIABOX_NAME
, MEDIABOX_PATH
)
2246 i
= dbus
.Interface(o
, MEDIABOX_INTF
)
2248 def on_reply(*args
):
2252 log('Exception in D-Bus call: %s', str(err
), \
2255 i
.load(filename
, '%s/x-unknown' % file_type
, \
2256 reply_handler
=on_reply
, error_handler
=on_error
)
2258 continue # This file was handled by the D-Bus call
2259 except Exception, e
:
2260 log('Error calling MediaBox using D-Bus', sender
=self
, traceback
=True)
2262 groups
[player
].append(filename
)
2264 # Open episodes with system default player
2265 if 'default' in groups
:
2266 if gpodder
.ui
.maemo
and len(groups
['default']) > 1:
2267 # The Nokia Media Player app does not support receiving multiple
2268 # file names via D-Bus, so we simply place all file names into a
2269 # temporary M3U playlist and open that with the Media Player.
2270 m3u_filename
= os
.path
.join(gpodder
.home
, 'gpodder_open_with.m3u')
2271 util
.write_m3u_playlist(m3u_filename
, groups
['default'], extm3u
=False)
2272 util
.gui_open(m3u_filename
)
2274 for filename
in groups
['default']:
2275 log('Opening with system default: %s', filename
, sender
=self
)
2276 util
.gui_open(filename
)
2277 del groups
['default']
2278 elif gpodder
.ui
.maemo
and groups
:
2279 # When on Maemo and not opening with default, show a notification
2280 # (no startup notification for Panucci / MPlayer yet...)
2281 if len(episodes
) == 1:
2282 text
= _('Opening %s') % episodes
[0].title
2284 count
= len(episodes
)
2285 text
= N_('Opening %d episode', 'Opening %d episodes', count
) % count
2287 banner
= hildon
.hildon_banner_show_animation(self
.gPodder
, '', text
)
2289 def destroy_banner_later(banner
):
2292 gobject
.timeout_add(5000, destroy_banner_later
, banner
)
2294 # For each type now, go and create play commands
2295 for group
in groups
:
2296 for command
in util
.format_desktop_command(group
, groups
[group
]):
2297 log('Executing: %s', repr(command
), sender
=self
)
2298 subprocess
.Popen(command
)
2300 # Persist episode status changes to the database
2303 # Flush updated episode status
2304 self
.mygpo_client
.flush()
2306 def playback_episodes(self
, episodes
):
2307 # We need to create a list, because we run through it more than once
2308 episodes
= list(PodcastEpisode
.sort_by_pubdate(e
for e
in episodes
if \
2309 e
.was_downloaded(and_exists
=True) or self
.streaming_possible()))
2312 self
.playback_episodes_for_real(episodes
)
2313 except Exception, e
:
2314 log('Error in playback!', sender
=self
, traceback
=True)
2315 if gpodder
.ui
.desktop
:
2316 self
.show_message(_('Please check your media player settings in the preferences dialog.'), \
2317 _('Error opening player'), widget
=self
.toolPreferences
)
2319 self
.show_message(_('Please check your media player settings in the preferences dialog.'))
2321 channel_urls
= set()
2322 episode_urls
= set()
2323 for episode
in episodes
:
2324 channel_urls
.add(episode
.channel
.url
)
2325 episode_urls
.add(episode
.url
)
2326 self
.update_episode_list_icons(episode_urls
)
2327 self
.update_podcast_list_model(channel_urls
)
2329 def play_or_download(self
):
2330 if not gpodder
.ui
.fremantle
:
2331 if self
.wNotebook
.get_current_page() > 0:
2332 if gpodder
.ui
.desktop
:
2333 self
.toolCancel
.set_sensitive(True)
2336 if self
.currently_updating
:
2337 return (False, False, False, False, False, False)
2339 ( can_play
, can_download
, can_transfer
, can_cancel
, can_delete
) = (False,)*5
2340 ( is_played
, is_locked
) = (False,)*2
2342 open_instead_of_play
= False
2344 selection
= self
.treeAvailable
.get_selection()
2345 if selection
.count_selected_rows() > 0:
2346 (model
, paths
) = selection
.get_selected_rows()
2350 episode
= model
.get_value(model
.get_iter(path
), EpisodeListModel
.C_EPISODE
)
2351 except TypeError, te
:
2352 log('Invalid episode at path %s', str(path
), sender
=self
)
2355 if episode
.file_type() not in ('audio', 'video'):
2356 open_instead_of_play
= True
2358 if episode
.was_downloaded():
2359 can_play
= episode
.was_downloaded(and_exists
=True)
2360 is_played
= episode
.is_played
2361 is_locked
= episode
.is_locked
2365 if self
.episode_is_downloading(episode
):
2370 can_download
= can_download
and not can_cancel
2371 can_play
= self
.streaming_possible() or (can_play
and not can_cancel
and not can_download
)
2372 can_transfer
= can_play
and self
.config
.device_type
!= 'none' and not can_cancel
and not can_download
and not open_instead_of_play
2373 can_delete
= not can_cancel
2375 if gpodder
.ui
.desktop
:
2376 if open_instead_of_play
:
2377 self
.toolPlay
.set_stock_id(gtk
.STOCK_OPEN
)
2379 self
.toolPlay
.set_stock_id(gtk
.STOCK_MEDIA_PLAY
)
2380 self
.toolPlay
.set_sensitive( can_play
)
2381 self
.toolDownload
.set_sensitive( can_download
)
2382 self
.toolTransfer
.set_sensitive( can_transfer
)
2383 self
.toolCancel
.set_sensitive( can_cancel
)
2385 if not gpodder
.ui
.fremantle
:
2386 self
.item_cancel_download
.set_sensitive(can_cancel
)
2387 self
.itemDownloadSelected
.set_sensitive(can_download
)
2388 self
.itemOpenSelected
.set_sensitive(can_play
)
2389 self
.itemPlaySelected
.set_sensitive(can_play
)
2390 self
.itemDeleteSelected
.set_sensitive(can_delete
)
2391 self
.item_toggle_played
.set_sensitive(can_play
)
2392 self
.item_toggle_lock
.set_sensitive(can_play
)
2393 self
.itemOpenSelected
.set_visible(open_instead_of_play
)
2394 self
.itemPlaySelected
.set_visible(not open_instead_of_play
)
2396 return (can_play
, can_download
, can_transfer
, can_cancel
, can_delete
, open_instead_of_play
)
2398 def on_cbMaxDownloads_toggled(self
, widget
, *args
):
2399 self
.spinMaxDownloads
.set_sensitive(self
.cbMaxDownloads
.get_active())
2401 def on_cbLimitDownloads_toggled(self
, widget
, *args
):
2402 self
.spinLimitDownloads
.set_sensitive(self
.cbLimitDownloads
.get_active())
2404 def episode_new_status_changed(self
, urls
):
2405 self
.update_podcast_list_model()
2406 self
.update_episode_list_icons(urls
)
2408 def update_podcast_list_model(self
, urls
=None, selected
=False, select_url
=None):
2409 """Update the podcast list treeview model
2411 If urls is given, it should list the URLs of each
2412 podcast that has to be updated in the list.
2414 If selected is True, only update the model contents
2415 for the currently-selected podcast - nothing more.
2417 The caller can optionally specify "select_url",
2418 which is the URL of the podcast that is to be
2419 selected in the list after the update is complete.
2420 This only works if the podcast list has to be
2421 reloaded; i.e. something has been added or removed
2422 since the last update of the podcast list).
2424 selection
= self
.treeChannels
.get_selection()
2425 model
, iter = selection
.get_selected()
2427 if self
.config
.podcast_list_view_all
and not self
.channel_list_changed
:
2428 # Update "all episodes" view in any case (if enabled)
2429 self
.podcast_list_model
.update_first_row()
2432 # very cheap! only update selected channel
2433 if iter is not None:
2434 # If we have selected the "all episodes" view, we have
2435 # to update all channels for selected episodes:
2436 if self
.config
.podcast_list_view_all
and \
2437 self
.podcast_list_model
.iter_is_first_row(iter):
2438 urls
= self
.get_podcast_urls_from_selected_episodes()
2439 self
.podcast_list_model
.update_by_urls(urls
)
2441 # Otherwise just update the selected row (a podcast)
2442 self
.podcast_list_model
.update_by_filter_iter(iter)
2443 elif not self
.channel_list_changed
:
2444 # we can keep the model, but have to update some
2446 # still cheaper than reloading the whole list
2447 self
.podcast_list_model
.update_all()
2449 # ok, we got a bunch of urls to update
2450 self
.podcast_list_model
.update_by_urls(urls
)
2452 if model
and iter and select_url
is None:
2453 # Get the URL of the currently-selected podcast
2454 select_url
= model
.get_value(iter, PodcastListModel
.C_URL
)
2456 # Update the podcast list model with new channels
2457 self
.podcast_list_model
.set_channels(self
.db
, self
.config
, self
.channels
)
2460 selected_iter
= model
.get_iter_first()
2461 # Find the previously-selected URL in the new
2462 # model if we have an URL (else select first)
2463 if select_url
is not None:
2464 pos
= model
.get_iter_first()
2465 while pos
is not None:
2466 url
= model
.get_value(pos
, PodcastListModel
.C_URL
)
2467 if url
== select_url
:
2470 pos
= model
.iter_next(pos
)
2472 if not gpodder
.ui
.fremantle
:
2473 if selected_iter
is not None:
2474 selection
.select_iter(selected_iter
)
2475 self
.on_treeChannels_cursor_changed(self
.treeChannels
)
2477 log('Cannot select podcast in list', traceback
=True, sender
=self
)
2478 self
.channel_list_changed
= False
2480 def episode_is_downloading(self
, episode
):
2481 """Returns True if the given episode is being downloaded at the moment"""
2485 return episode
.url
in (task
.url
for task
in self
.download_tasks_seen
if task
.status
in (task
.DOWNLOADING
, task
.QUEUED
, task
.PAUSED
))
2487 def update_episode_list_model(self
):
2488 if self
.channels
and self
.active_channel
is not None:
2489 if gpodder
.ui
.fremantle
:
2490 hildon
.hildon_gtk_window_set_progress_indicator(self
.episodes_window
.main_window
, True)
2492 self
.currently_updating
= True
2493 self
.treeAvailable
.hide()
2496 additional_args
= (self
.episode_is_downloading
, \
2497 self
.config
.episode_list_descriptions
and gpodder
.ui
.desktop
, \
2498 self
.config
.episode_list_thumbnails
and gpodder
.ui
.desktop
)
2499 self
.episode_list_model
.replace_from_channel(self
.active_channel
, *additional_args
)
2501 self
.treeAvailable
.get_selection().unselect_all()
2502 self
.treeAvailable
.show()
2503 util
.idle_add(self
.treeAvailable
.scroll_to_point
, 0, 0)
2504 self
.currently_updating
= False
2505 self
.play_or_download()
2507 if gpodder
.ui
.fremantle
:
2508 util
.idle_add(hildon
.hildon_gtk_window_set_progress_indicator
,
2509 self
.episodes_window
.main_window
, False)
2511 util
.idle_add(update
)
2513 self
.episode_list_model
.clear()
2515 @dbus.service
.method(gpodder
.dbus_interface
)
2516 def offer_new_episodes(self
, channels
=None):
2517 if gpodder
.ui
.fremantle
:
2518 # Assume that when this function is called that the
2519 # notification is not shown anymore (Maemo bug 11345)
2520 self
._fremantle
_notification
_visible
= False
2522 new_episodes
= self
.get_new_episodes(channels
)
2524 self
.new_episodes_show(new_episodes
)
2528 def add_podcast_list(self
, urls
, auth_tokens
=None):
2529 """Subscribe to a list of podcast given their URLs
2531 If auth_tokens is given, it should be a dictionary
2532 mapping URLs to (username, password) tuples."""
2534 if auth_tokens
is None:
2537 # Sort and split the URL list into five buckets
2538 queued
, failed
, existing
, worked
, authreq
= [], [], [], [], []
2539 for input_url
in urls
:
2540 url
= util
.normalize_feed_url(input_url
)
2542 # Fail this one because the URL is not valid
2543 failed
.append(input_url
)
2544 elif self
.podcast_list_model
.get_filter_path_from_url(url
) is not None:
2545 # A podcast already exists in the list for this URL
2546 existing
.append(url
)
2548 # This URL has survived the first round - queue for add
2550 if url
!= input_url
and input_url
in auth_tokens
:
2551 auth_tokens
[url
] = auth_tokens
[input_url
]
2556 progress
= ProgressIndicator(_('Adding podcasts'), \
2557 _('Please wait while episode information is downloaded.'), \
2558 parent
=self
.get_dialog_parent())
2560 def on_after_update():
2561 progress
.on_finished()
2562 # Report already-existing subscriptions to the user
2564 title
= _('Existing subscriptions skipped')
2565 message
= _('You are already subscribed to these podcasts:') \
2566 + '\n\n' + '\n'.join(saxutils
.escape(url
) for url
in existing
)
2567 self
.show_message(message
, title
, widget
=self
.treeChannels
)
2569 # Report subscriptions that require authentication
2573 title
= _('Podcast requires authentication')
2574 message
= _('Please login to %s:') % (saxutils
.escape(url
),)
2575 success
, auth_tokens
= self
.show_login_dialog(title
, message
)
2577 retry_podcasts
[url
] = auth_tokens
2579 # Stop asking the user for more login data
2582 error_messages
[url
] = _('Authentication failed')
2586 # If we have authentication data to retry, do so here
2588 self
.add_podcast_list(retry_podcasts
.keys(), retry_podcasts
)
2590 # Report website redirections
2591 for url
in redirections
:
2592 title
= _('Website redirection detected')
2593 message
= _('The URL %(url)s redirects to %(target)s.') \
2594 + '\n\n' + _('Do you want to visit the website now?')
2595 message
= message
% {'url': url
, 'target': redirections
[url
]}
2596 if self
.show_confirmation(message
, title
):
2597 util
.open_website(url
)
2601 # Report failed subscriptions to the user
2603 title
= _('Could not add some podcasts')
2604 message
= _('Some podcasts could not be added to your list:') \
2605 + '\n\n' + '\n'.join(saxutils
.escape('%s: %s' % (url
, \
2606 error_messages
.get(url
, _('Unknown')))) for url
in failed
)
2607 self
.show_message(message
, title
, important
=True)
2609 # Upload subscription changes to gpodder.net
2610 self
.mygpo_client
.on_subscribe(worked
)
2612 # If at least one podcast has been added, save and update all
2613 if self
.channel_list_changed
:
2614 # Fix URLs if mygpo has rewritten them
2615 self
.rewrite_urls_mygpo()
2617 self
.save_channels_opml()
2619 # If only one podcast was added, select it after the update
2620 if len(worked
) == 1:
2625 # Update the list of subscribed podcasts
2626 self
.update_feed_cache(force_update
=False, select_url_afterwards
=url
)
2627 self
.update_podcasts_tab()
2629 # Offer to download new episodes
2631 for podcast
in self
.channels
:
2632 if podcast
.url
in worked
:
2633 episodes
.extend(podcast
.get_all_episodes())
2636 episodes
= list(PodcastEpisode
.sort_by_pubdate(episodes
, \
2638 self
.new_episodes_show(episodes
, \
2639 selected
=[e
.check_is_new() for e
in episodes
])
2643 # After the initial sorting and splitting, try all queued podcasts
2644 length
= len(queued
)
2645 for index
, url
in enumerate(queued
):
2646 progress
.on_progress(float(index
)/float(length
))
2647 progress
.on_message(url
)
2648 log('QUEUE RUNNER: %s', url
, sender
=self
)
2650 # The URL is valid and does not exist already - subscribe!
2651 channel
= PodcastChannel
.load(self
.db
, url
=url
, create
=True, \
2652 authentication_tokens
=auth_tokens
.get(url
, None), \
2653 max_episodes
=self
.config
.max_episodes_per_feed
, \
2654 download_dir
=self
.config
.download_dir
, \
2655 allow_empty_feeds
=self
.config
.allow_empty_feeds
, \
2656 mimetype_prefs
=self
.config
.mimetype_prefs
)
2659 username
, password
= util
.username_password_from_url(url
)
2660 except ValueError, ve
:
2661 username
, password
= (None, None)
2663 if username
is not None and channel
.username
is None and \
2664 password
is not None and channel
.password
is None:
2665 channel
.username
= username
2666 channel
.password
= password
2669 self
._update
_cover
(channel
)
2670 except feedcore
.AuthenticationRequired
:
2671 if url
in auth_tokens
:
2672 # Fail for wrong authentication data
2673 error_messages
[url
] = _('Authentication failed')
2676 # Queue for login dialog later
2679 except feedcore
.WifiLogin
, error
:
2680 redirections
[url
] = error
.data
2682 error_messages
[url
] = _('Redirection detected')
2684 except Exception, e
:
2685 log('Subscription error: %s', e
, traceback
=True, sender
=self
)
2686 error_messages
[url
] = str(e
)
2690 assert channel
is not None
2691 worked
.append(channel
.url
)
2692 self
.channels
.append(channel
)
2693 self
.channel_list_changed
= True
2694 util
.idle_add(on_after_update
)
2695 threading
.Thread(target
=thread_proc
).start()
2697 def save_channels_opml(self
):
2698 exporter
= opml
.Exporter(gpodder
.subscription_file
)
2699 return exporter
.write(self
.channels
)
2701 def find_episode(self
, podcast_url
, episode_url
):
2702 """Find an episode given its podcast and episode URL
2704 The function will return a PodcastEpisode object if
2705 the episode is found, or None if it's not found.
2707 for podcast
in self
.channels
:
2708 if podcast_url
== podcast
.url
:
2709 for episode
in podcast
.get_all_episodes():
2710 if episode_url
== episode
.url
:
2715 def process_received_episode_actions(self
, updated_urls
):
2716 """Process/merge episode actions from gpodder.net
2718 This function will merge all changes received from
2719 the server to the local database and update the
2720 status of the affected episodes as necessary.
2722 indicator
= ProgressIndicator(_('Merging episode actions'), \
2723 _('Episode actions from gpodder.net are merged.'), \
2724 False, self
.get_dialog_parent())
2726 for idx
, action
in enumerate(self
.mygpo_client
.get_episode_actions(updated_urls
)):
2727 if action
.action
== 'play':
2728 episode
= self
.find_episode(action
.podcast_url
, \
2731 if episode
is not None:
2732 log('Play action for %s', episode
.url
, sender
=self
)
2733 episode
.mark(is_played
=True)
2735 if action
.timestamp
> episode
.current_position_updated
and \
2736 action
.position
is not None:
2737 log('Updating position for %s', episode
.url
, sender
=self
)
2738 episode
.current_position
= action
.position
2739 episode
.current_position_updated
= action
.timestamp
2742 log('Updating total time for %s', episode
.url
, sender
=self
)
2743 episode
.total_time
= action
.total
2746 elif action
.action
== 'delete':
2747 episode
= self
.find_episode(action
.podcast_url
, \
2750 if episode
is not None:
2751 if not episode
.was_downloaded(and_exists
=True):
2752 # Set the episode to a "deleted" state
2753 log('Marking as deleted: %s', episode
.url
, sender
=self
)
2754 episode
.delete_from_disk()
2757 indicator
.on_message(N_('%d action processed', '%d actions processed', idx
) % idx
)
2758 gtk
.main_iteration(False)
2760 indicator
.on_finished()
2764 def update_feed_cache_finish_callback(self
, updated_urls
=None, select_url_afterwards
=None):
2766 self
.updating_feed_cache
= False
2768 self
.channels
= PodcastChannel
.load_from_db(self
.db
, self
.config
.download_dir
)
2770 # Process received episode actions for all updated URLs
2771 self
.process_received_episode_actions(updated_urls
)
2773 self
.channel_list_changed
= True
2774 self
.update_podcast_list_model(select_url
=select_url_afterwards
)
2776 # Only search for new episodes in podcasts that have been
2777 # updated, not in other podcasts (for single-feed updates)
2778 episodes
= self
.get_new_episodes([c
for c
in self
.channels
if c
.url
in updated_urls
])
2780 if gpodder
.ui
.fremantle
:
2781 self
.fancy_progress_bar
.hide()
2782 self
.button_subscribe
.set_sensitive(True)
2783 self
.button_refresh
.set_sensitive(True)
2784 hildon
.hildon_gtk_window_set_progress_indicator(self
.main_window
, False)
2785 hildon
.hildon_gtk_window_set_progress_indicator(self
.episodes_window
.main_window
, False)
2786 self
.update_podcasts_tab()
2787 self
.update_episode_list_model()
2788 if self
.feed_cache_update_cancelled
:
2791 def application_in_foreground():
2793 return any(w
.get_property('is-topmost') for w
in hildon
.WindowStack
.get_default().get_windows())
2794 except Exception, e
:
2795 log('Could not determine is-topmost', traceback
=True)
2796 # When in doubt, assume not in foreground
2800 if self
.config
.auto_download
== 'quiet' and not self
.config
.auto_update_feeds
:
2801 # New episodes found, but we should do nothing
2802 self
.show_message(_('New episodes are available.'))
2803 elif self
.config
.auto_download
== 'always':
2804 count
= len(episodes
)
2805 title
= N_('Downloading %d new episode.', 'Downloading %d new episodes.', count
) % count
2806 self
.show_message(title
)
2807 self
.download_episode_list(episodes
)
2808 elif self
.config
.auto_download
== 'queue':
2809 self
.show_message(_('New episodes have been added to the download list.'))
2810 self
.download_episode_list_paused(episodes
)
2811 elif application_in_foreground():
2812 if not self
._fremantle
_notification
_visible
:
2813 self
.new_episodes_show(episodes
)
2814 elif not self
._fremantle
_notification
_visible
:
2817 pynotify
.init('gPodder')
2818 n
= pynotify
.Notification('gPodder', _('New episodes available'), 'gpodder')
2819 n
.set_urgency(pynotify
.URGENCY_CRITICAL
)
2820 n
.set_hint('dbus-callback-default', ' '.join([
2821 gpodder
.dbus_bus_name
,
2822 gpodder
.dbus_gui_object_path
,
2823 gpodder
.dbus_interface
,
2824 'offer_new_episodes',
2826 n
.set_category('gpodder-new-episodes')
2828 self
._fremantle
_notification
_visible
= True
2829 except Exception, e
:
2830 log('Error: %s', str(e
), sender
=self
, traceback
=True)
2831 self
.new_episodes_show(episodes
)
2832 self
._fremantle
_notification
_visible
= False
2833 elif not self
.config
.auto_update_feeds
:
2834 self
.show_message(_('No new episodes. Please check for new episodes later.'))
2838 self
.tray_icon
.set_status()
2840 if self
.feed_cache_update_cancelled
:
2841 # The user decided to abort the feed update
2842 self
.show_update_feeds_buttons()
2844 # Nothing new here - but inform the user
2845 self
.pbFeedUpdate
.set_fraction(1.0)
2846 self
.pbFeedUpdate
.set_text(_('No new episodes'))
2847 self
.feed_cache_update_cancelled
= True
2848 self
.btnCancelFeedUpdate
.show()
2849 self
.btnCancelFeedUpdate
.set_sensitive(True)
2850 self
.itemUpdate
.set_sensitive(True)
2851 if gpodder
.ui
.maemo
:
2852 # btnCancelFeedUpdate is a ToolButton on Maemo
2853 self
.btnCancelFeedUpdate
.set_stock_id(gtk
.STOCK_APPLY
)
2855 # btnCancelFeedUpdate is a normal gtk.Button
2856 self
.btnCancelFeedUpdate
.set_image(gtk
.image_new_from_stock(gtk
.STOCK_APPLY
, gtk
.ICON_SIZE_BUTTON
))
2858 count
= len(episodes
)
2859 # New episodes are available
2860 self
.pbFeedUpdate
.set_fraction(1.0)
2861 # Are we minimized and should we auto download?
2862 if (self
.is_iconified() and (self
.config
.auto_download
== 'minimized')) or (self
.config
.auto_download
== 'always'):
2863 self
.download_episode_list(episodes
)
2864 title
= N_('Downloading %d new episode.', 'Downloading %d new episodes.', count
) % count
2865 self
.show_message(title
, _('New episodes available'), widget
=self
.labelDownloads
)
2866 self
.show_update_feeds_buttons()
2867 elif self
.config
.auto_download
== 'queue':
2868 self
.download_episode_list_paused(episodes
)
2869 title
= N_('%d new episode added to download list.', '%d new episodes added to download list.', count
) % count
2870 self
.show_message(title
, _('New episodes available'), widget
=self
.labelDownloads
)
2871 self
.show_update_feeds_buttons()
2873 self
.show_update_feeds_buttons()
2874 # New episodes are available and we are not minimized
2875 if not self
.config
.do_not_show_new_episodes_dialog
:
2876 self
.new_episodes_show(episodes
, notification
=True)
2878 message
= N_('%d new episode available', '%d new episodes available', count
) % count
2879 self
.pbFeedUpdate
.set_text(message
)
2881 def _update_cover(self
, channel
):
2882 if channel
is not None and not os
.path
.exists(channel
.cover_file
) and channel
.image
:
2883 self
.cover_downloader
.request_cover(channel
)
2885 def update_feed_cache_proc(self
, channels
, select_url_afterwards
):
2886 total
= len(channels
)
2888 for updated
, channel
in enumerate(channels
):
2889 if not self
.feed_cache_update_cancelled
:
2891 channel
.update(max_episodes
=self
.config
.max_episodes_per_feed
, \
2892 mimetype_prefs
=self
.config
.mimetype_prefs
)
2893 self
._update
_cover
(channel
)
2894 except Exception, e
:
2895 d
= {'url': saxutils
.escape(channel
.url
), 'message': saxutils
.escape(str(e
))}
2897 message
= _('Error while updating %(url)s: %(message)s')
2899 message
= _('The feed at %(url)s could not be updated.')
2900 self
.notification(message
% d
, _('Error while updating feed'), widget
=self
.treeChannels
)
2901 log('Error: %s', str(e
), sender
=self
, traceback
=True)
2903 if self
.feed_cache_update_cancelled
:
2906 # By the time we get here the update may have already been cancelled
2907 if not self
.feed_cache_update_cancelled
:
2908 def update_progress():
2909 d
= {'podcast': channel
.title
, 'position': updated
+1, 'total': total
}
2910 progression
= _('Updated %(podcast)s (%(position)d/%(total)d)') % d
2911 self
.pbFeedUpdate
.set_text(progression
)
2913 self
.tray_icon
.set_status(self
.tray_icon
.STATUS_UPDATING_FEED_CACHE
, progression
)
2914 self
.pbFeedUpdate
.set_fraction(float(updated
+1)/float(total
))
2915 util
.idle_add(update_progress
)
2917 updated_urls
= [c
.url
for c
in channels
]
2918 util
.idle_add(self
.update_feed_cache_finish_callback
, updated_urls
, select_url_afterwards
)
2920 def show_update_feeds_buttons(self
):
2921 # Make sure that the buttons for updating feeds
2922 # appear - this should happen after a feed update
2923 if gpodder
.ui
.maemo
:
2924 self
.btnUpdateSelectedFeed
.show()
2925 self
.toolFeedUpdateProgress
.hide()
2926 self
.btnCancelFeedUpdate
.hide()
2927 self
.btnCancelFeedUpdate
.set_is_important(False)
2928 self
.btnCancelFeedUpdate
.set_stock_id(gtk
.STOCK_CLOSE
)
2929 self
.toolbarSpacer
.set_expand(True)
2930 self
.toolbarSpacer
.set_draw(False)
2932 self
.hboxUpdateFeeds
.hide()
2933 self
.btnUpdateFeeds
.show()
2934 self
.itemUpdate
.set_sensitive(True)
2935 self
.itemUpdateChannel
.set_sensitive(True)
2937 def on_btnCancelFeedUpdate_clicked(self
, widget
):
2938 if not self
.feed_cache_update_cancelled
:
2939 self
.pbFeedUpdate
.set_text(_('Cancelling...'))
2940 self
.feed_cache_update_cancelled
= True
2941 if not gpodder
.ui
.fremantle
:
2942 self
.btnCancelFeedUpdate
.set_sensitive(False)
2943 elif not gpodder
.ui
.fremantle
:
2944 self
.show_update_feeds_buttons()
2946 def update_feed_cache(self
, channels
=None, force_update
=True, select_url_afterwards
=None):
2947 if self
.updating_feed_cache
:
2948 if gpodder
.ui
.fremantle
:
2949 self
.feed_cache_update_cancelled
= True
2952 if not force_update
:
2953 self
.channels
= PodcastChannel
.load_from_db(self
.db
, self
.config
.download_dir
)
2954 self
.channel_list_changed
= True
2955 self
.update_podcast_list_model(select_url
=select_url_afterwards
)
2958 # Fix URLs if mygpo has rewritten them
2959 self
.rewrite_urls_mygpo()
2961 self
.updating_feed_cache
= True
2963 if channels
is None:
2964 # Only update podcasts for which updates are enabled
2965 channels
= [c
for c
in self
.channels
if c
.feed_update_enabled
]
2967 if gpodder
.ui
.fremantle
:
2968 hildon
.hildon_gtk_window_set_progress_indicator(self
.main_window
, True)
2969 hildon
.hildon_gtk_window_set_progress_indicator(self
.episodes_window
.main_window
, True)
2970 self
.fancy_progress_bar
.show()
2971 self
.button_subscribe
.set_sensitive(False)
2972 self
.button_refresh
.set_sensitive(False)
2973 self
.feed_cache_update_cancelled
= False
2975 self
.itemUpdate
.set_sensitive(False)
2976 self
.itemUpdateChannel
.set_sensitive(False)
2979 self
.tray_icon
.set_status(self
.tray_icon
.STATUS_UPDATING_FEED_CACHE
)
2981 self
.feed_cache_update_cancelled
= False
2982 self
.btnCancelFeedUpdate
.show()
2983 self
.btnCancelFeedUpdate
.set_sensitive(True)
2984 if gpodder
.ui
.maemo
:
2985 self
.toolbarSpacer
.set_expand(False)
2986 self
.toolbarSpacer
.set_draw(True)
2987 self
.btnUpdateSelectedFeed
.hide()
2988 self
.toolFeedUpdateProgress
.show_all()
2990 self
.btnCancelFeedUpdate
.set_image(gtk
.image_new_from_stock(gtk
.STOCK_STOP
, gtk
.ICON_SIZE_BUTTON
))
2991 self
.hboxUpdateFeeds
.show_all()
2992 self
.btnUpdateFeeds
.hide()
2994 if len(channels
) == 1:
2995 text
= _('Updating "%s"...') % channels
[0].title
2997 count
= len(channels
)
2998 text
= N_('Updating %d feed...', 'Updating %d feeds...', count
) % count
2999 self
.pbFeedUpdate
.set_text(text
)
3000 self
.pbFeedUpdate
.set_fraction(0)
3002 args
= (channels
, select_url_afterwards
)
3003 threading
.Thread(target
=self
.update_feed_cache_proc
, args
=args
).start()
3005 def on_gPodder_delete_event(self
, widget
, *args
):
3006 """Called when the GUI wants to close the window
3007 Displays a confirmation dialog (and closes/hides gPodder)
3010 downloading
= self
.download_status_model
.are_downloads_in_progress()
3012 # Only iconify if we are using the window's "X" button,
3013 # but not when we are using "Quit" in the menu or toolbar
3014 if self
.config
.on_quit_systray
and self
.tray_icon
and widget
.get_name() not in ('toolQuit', 'itemQuit'):
3015 self
.iconify_main_window()
3016 elif self
.config
.on_quit_ask
or downloading
:
3017 if gpodder
.ui
.fremantle
:
3018 self
.close_gpodder()
3019 elif gpodder
.ui
.diablo
:
3020 result
= self
.show_confirmation(_('Do you really want to quit gPodder now?'))
3022 self
.close_gpodder()
3025 dialog
= gtk
.MessageDialog(self
.gPodder
, gtk
.DIALOG_MODAL
, gtk
.MESSAGE_QUESTION
, gtk
.BUTTONS_NONE
)
3026 dialog
.add_button(gtk
.STOCK_CANCEL
, gtk
.RESPONSE_CANCEL
)
3027 quit_button
= dialog
.add_button(gtk
.STOCK_QUIT
, gtk
.RESPONSE_CLOSE
)
3029 title
= _('Quit gPodder')
3031 message
= _('You are downloading episodes. You can resume downloads the next time you start gPodder. Do you want to quit now?')
3033 message
= _('Do you really want to quit gPodder now?')
3035 dialog
.set_title(title
)
3036 dialog
.set_markup('<span weight="bold" size="larger">%s</span>\n\n%s'%(title
, message
))
3038 cb_ask
= gtk
.CheckButton(_("Don't ask me again"))
3039 dialog
.vbox
.pack_start(cb_ask
)
3042 quit_button
.grab_focus()
3043 result
= dialog
.run()
3046 if result
== gtk
.RESPONSE_CLOSE
:
3047 if not downloading
and cb_ask
.get_active() == True:
3048 self
.config
.on_quit_ask
= False
3049 self
.close_gpodder()
3051 self
.close_gpodder()
3055 def close_gpodder(self
):
3056 """ clean everything and exit properly
3059 if self
.save_channels_opml():
3060 pass # FIXME: Add mygpo synchronization here
3062 self
.show_message(_('Please check your permissions and free disk space.'), _('Error saving podcast list'), important
=True)
3066 if self
.tray_icon
is not None:
3067 self
.tray_icon
.set_visible(False)
3069 # Notify all tasks to to carry out any clean-up actions
3070 self
.download_status_model
.tell_all_tasks_to_quit()
3072 while gtk
.events_pending():
3073 gtk
.main_iteration(False)
3080 def get_expired_episodes(self
):
3081 for channel
in self
.channels
:
3082 for episode
in channel
.get_downloaded_episodes():
3083 # Never consider locked episodes as old
3084 if episode
.is_locked
:
3087 # Never consider fresh episodes as old
3088 if episode
.age_in_days() < self
.config
.episode_old_age
:
3091 # Do not delete played episodes (except if configured)
3092 if episode
.is_played
:
3093 if not self
.config
.auto_remove_played_episodes
:
3096 # Do not delete unplayed episodes (except if configured)
3097 if not episode
.is_played
:
3098 if not self
.config
.auto_remove_unplayed_episodes
:
3103 def delete_episode_list(self
, episodes
, confirm
=True, skip_locked
=True):
3108 episodes
= [e
for e
in episodes
if not e
.is_locked
]
3111 title
= _('Episodes are locked')
3112 message
= _('The selected episodes are locked. Please unlock the episodes that you want to delete before trying to delete them.')
3113 self
.notification(message
, title
, widget
=self
.treeAvailable
)
3116 count
= len(episodes
)
3117 title
= N_('Delete %d episode?', 'Delete %d episodes?', count
) % count
3118 message
= _('Deleting episodes removes downloaded files.')
3120 if gpodder
.ui
.fremantle
:
3121 message
= '\n'.join([title
, message
])
3123 if confirm
and not self
.show_confirmation(message
, title
):
3126 progress
= ProgressIndicator(_('Deleting episodes'), \
3127 _('Please wait while episodes are deleted'), \
3128 parent
=self
.get_dialog_parent())
3130 def finish_deletion(episode_urls
, channel_urls
):
3131 progress
.on_finished()
3133 # Episodes have been deleted - persist the database
3136 self
.update_episode_list_icons(episode_urls
)
3137 self
.update_podcast_list_model(channel_urls
)
3138 self
.play_or_download()
3141 episode_urls
= set()
3142 channel_urls
= set()
3144 episodes_status_update
= []
3145 for idx
, episode
in enumerate(episodes
):
3146 progress
.on_progress(float(idx
)/float(len(episodes
)))
3147 if episode
.is_locked
and skip_locked
:
3148 log('Not deleting episode (is locked): %s', episode
.title
)
3150 log('Deleting episode: %s', episode
.title
)
3151 progress
.on_message(episode
.title
)
3152 episode
.delete_from_disk()
3153 episode_urls
.add(episode
.url
)
3154 channel_urls
.add(episode
.channel
.url
)
3155 episodes_status_update
.append(episode
)
3157 # Tell the shownotes window that we have removed the episode
3158 if self
.episode_shownotes_window
is not None and \
3159 self
.episode_shownotes_window
.episode
is not None and \
3160 self
.episode_shownotes_window
.episode
.url
== episode
.url
:
3161 util
.idle_add(self
.episode_shownotes_window
._download
_status
_changed
, None)
3163 # Notify the web service about the status update + upload
3164 self
.mygpo_client
.on_delete(episodes_status_update
)
3165 self
.mygpo_client
.flush()
3167 util
.idle_add(finish_deletion
, episode_urls
, channel_urls
)
3169 threading
.Thread(target
=thread_proc
).start()
3173 def on_itemRemoveOldEpisodes_activate( self
, widget
):
3174 if gpodder
.ui
.maemo
:
3176 ('maemo_remove_markup', None, None, _('Episode')),
3180 ('title_markup', None, None, _('Episode')),
3181 ('filesize_prop', 'length', gobject
.TYPE_INT
, _('Size')),
3182 ('pubdate_prop', 'pubDate', gobject
.TYPE_INT
, _('Released')),
3183 ('played_prop', None, None, _('Status')),
3184 ('age_prop', 'age_int_prop', gobject
.TYPE_INT
, _('Downloaded')),
3187 msg_older_than
= N_('Select older than %d day', 'Select older than %d days', self
.config
.episode_old_age
)
3188 selection_buttons
= {
3189 _('Select played'): lambda episode
: episode
.is_played
,
3190 _('Select finished'): lambda episode
: episode
.is_finished(),
3191 msg_older_than
% self
.config
.episode_old_age
: lambda episode
: episode
.age_in_days() > self
.config
.episode_old_age
,
3194 instructions
= _('Select the episodes you want to delete:')
3198 for channel
in self
.channels
:
3199 for episode
in channel
.get_downloaded_episodes():
3200 # Disallow deletion of locked episodes that still exist
3201 if not episode
.is_locked
or not episode
.file_exists():
3202 episodes
.append(episode
)
3203 # Automatically select played and file-less episodes
3204 selected
.append(episode
.is_played
or \
3205 not episode
.file_exists())
3207 gPodderEpisodeSelector(self
.gPodder
, title
= _('Delete episodes'), instructions
= instructions
, \
3208 episodes
= episodes
, selected
= selected
, columns
= columns
, \
3209 stock_ok_button
= gtk
.STOCK_DELETE
, callback
= self
.delete_episode_list
, \
3210 selection_buttons
= selection_buttons
, _config
=self
.config
, \
3211 show_episode_shownotes
=self
.show_episode_shownotes
)
3213 def on_selected_episodes_status_changed(self
):
3214 # The order of the updates here is important! When "All episodes" is
3215 # selected, the update of the podcast list model depends on the episode
3216 # list selection to determine which podcasts are affected. Updating
3217 # the episode list could remove the selection if a filter is active.
3218 self
.update_podcast_list_model(selected
=True)
3219 self
.update_episode_list_icons(selected
=True)
3222 def mark_selected_episodes_new(self
):
3223 for episode
in self
.get_selected_episodes():
3225 self
.on_selected_episodes_status_changed()
3227 def mark_selected_episodes_old(self
):
3228 for episode
in self
.get_selected_episodes():
3230 self
.on_selected_episodes_status_changed()
3232 def on_item_toggle_played_activate( self
, widget
, toggle
= True, new_value
= False):
3233 for episode
in self
.get_selected_episodes():
3235 episode
.mark(is_played
=not episode
.is_played
)
3237 episode
.mark(is_played
=new_value
)
3238 self
.on_selected_episodes_status_changed()
3240 def on_item_toggle_lock_activate(self
, widget
, toggle
=True, new_value
=False):
3241 for episode
in self
.get_selected_episodes():
3243 episode
.mark(is_locked
=not episode
.is_locked
)
3245 episode
.mark(is_locked
=new_value
)
3246 self
.on_selected_episodes_status_changed()
3248 def on_channel_toggle_lock_activate(self
, widget
, toggle
=True, new_value
=False):
3249 if self
.active_channel
is None:
3252 self
.active_channel
.channel_is_locked
= not self
.active_channel
.channel_is_locked
3253 self
.active_channel
.update_channel_lock()
3255 for episode
in self
.active_channel
.get_all_episodes():
3256 episode
.mark(is_locked
=self
.active_channel
.channel_is_locked
)
3258 self
.update_podcast_list_model(selected
=True)
3259 self
.update_episode_list_icons(all
=True)
3261 def on_itemUpdateChannel_activate(self
, widget
=None):
3262 if self
.active_channel
is None:
3263 title
= _('No podcast selected')
3264 message
= _('Please select a podcast in the podcasts list to update.')
3265 self
.show_message( message
, title
, widget
=self
.treeChannels
)
3268 # Dirty hack to check for "All episodes" (see gpodder.gtkui.model)
3269 if getattr(self
.active_channel
, 'ALL_EPISODES_PROXY', False):
3270 self
.update_feed_cache()
3272 self
.update_feed_cache(channels
=[self
.active_channel
])
3274 def on_itemUpdate_activate(self
, widget
=None):
3275 # Check if we have outstanding subscribe/unsubscribe actions
3276 if self
.on_add_remove_podcasts_mygpo():
3277 log('Update cancelled (received server changes)', sender
=self
)
3281 self
.update_feed_cache()
3283 gPodderWelcome(self
.gPodder
,
3284 center_on_widget
=self
.gPodder
,
3285 show_example_podcasts_callback
=self
.on_itemImportChannels_activate
,
3286 setup_my_gpodder_callback
=self
.on_download_subscriptions_from_mygpo
)
3288 def download_episode_list_paused(self
, episodes
):
3289 self
.download_episode_list(episodes
, True)
3291 def download_episode_list(self
, episodes
, add_paused
=False, force_start
=False):
3292 enable_update
= False
3294 for episode
in episodes
:
3295 log('Downloading episode: %s', episode
.title
, sender
= self
)
3296 if not episode
.was_downloaded(and_exists
=True):
3298 for task
in self
.download_tasks_seen
:
3299 if episode
.url
== task
.url
and task
.status
not in (task
.DOWNLOADING
, task
.QUEUED
):
3300 self
.download_queue_manager
.add_task(task
, force_start
)
3301 enable_update
= True
3309 task
= download
.DownloadTask(episode
, self
.config
)
3310 except Exception, e
:
3311 d
= {'episode': episode
.title
, 'message': str(e
)}
3312 message
= _('Download error while downloading %(episode)s: %(message)s')
3313 self
.show_message(message
% d
, _('Download error'), important
=True)
3314 log('Download error while downloading %s', episode
.title
, sender
=self
, traceback
=True)
3318 task
.status
= task
.PAUSED
3320 self
.mygpo_client
.on_download([task
.episode
])
3321 self
.download_queue_manager
.add_task(task
, force_start
)
3323 self
.download_status_model
.register_task(task
)
3324 enable_update
= True
3327 self
.enable_download_list_update()
3329 # Flush updated episode status
3330 self
.mygpo_client
.flush()
3332 def cancel_task_list(self
, tasks
):
3337 if task
.status
in (task
.QUEUED
, task
.DOWNLOADING
):
3338 task
.status
= task
.CANCELLED
3339 elif task
.status
== task
.PAUSED
:
3340 task
.status
= task
.CANCELLED
3341 # Call run, so the partial file gets deleted
3344 self
.update_episode_list_icons([task
.url
for task
in tasks
])
3345 self
.play_or_download()
3347 # Update the tab title and downloads list
3348 self
.update_downloads_list()
3350 def new_episodes_show(self
, episodes
, notification
=False, selected
=None):
3351 if gpodder
.ui
.maemo
:
3353 ('maemo_markup', None, None, _('Episode')),
3355 show_notification
= notification
3358 ('title_markup', None, None, _('Episode')),
3359 ('filesize_prop', 'length', gobject
.TYPE_INT
, _('Size')),
3360 ('pubdate_prop', 'pubDate', gobject
.TYPE_INT
, _('Released')),
3362 show_notification
= False
3364 instructions
= _('Select the episodes you want to download:')
3366 if self
.new_episodes_window
is not None:
3367 self
.new_episodes_window
.main_window
.destroy()
3368 self
.new_episodes_window
= None
3370 def download_episodes_callback(episodes
):
3371 self
.new_episodes_window
= None
3372 self
.download_episode_list(episodes
)
3374 if selected
is None:
3375 # Select all by default
3376 selected
= [True]*len(episodes
)
3378 self
.new_episodes_window
= gPodderEpisodeSelector(self
.gPodder
, \
3379 title
=_('New episodes available'), \
3380 instructions
=instructions
, \
3381 episodes
=episodes
, \
3383 selected
=selected
, \
3384 stock_ok_button
= 'gpodder-download', \
3385 callback
=download_episodes_callback
, \
3386 remove_callback
=lambda e
: e
.mark_old(), \
3387 remove_action
=_('Mark as old'), \
3388 remove_finished
=self
.episode_new_status_changed
, \
3389 _config
=self
.config
, \
3390 show_notification
=show_notification
, \
3391 show_episode_shownotes
=self
.show_episode_shownotes
)
3393 def on_itemDownloadAllNew_activate(self
, widget
, *args
):
3394 if not self
.offer_new_episodes():
3395 self
.show_message(_('Please check for new episodes later.'), \
3396 _('No new episodes available'), widget
=self
.btnUpdateFeeds
)
3398 def get_new_episodes(self
, channels
=None):
3399 if channels
is None:
3400 channels
= self
.channels
3402 for channel
in channels
:
3403 for episode
in channel
.get_new_episodes(downloading
=self
.episode_is_downloading
):
3404 episodes
.append(episode
)
3408 @dbus.service
.method(gpodder
.dbus_interface
)
3409 def start_device_synchronization(self
):
3410 """Public D-Bus API for starting Device sync (Desktop only)
3412 This method can be called to initiate a synchronization with
3413 a configured protable media player. This only works for the
3414 Desktop version of gPodder and does nothing on Maemo.
3416 if gpodder
.ui
.desktop
:
3417 self
.on_sync_to_ipod_activate(None)
3422 def on_sync_to_ipod_activate(self
, widget
, episodes
=None):
3423 self
.sync_ui
.on_synchronize_episodes(self
.channels
, episodes
)
3425 def commit_changes_to_database(self
):
3426 """This will be called after the sync process is finished"""
3429 def on_cleanup_ipod_activate(self
, widget
, *args
):
3430 self
.sync_ui
.on_cleanup_device()
3432 def on_manage_device_playlist(self
, widget
):
3433 self
.sync_ui
.on_manage_device_playlist()
3435 def show_hide_tray_icon(self
):
3436 if self
.config
.display_tray_icon
and have_trayicon
and self
.tray_icon
is None:
3437 self
.tray_icon
= GPodderStatusIcon(self
, gpodder
.icon_file
, self
.config
)
3438 elif not self
.config
.display_tray_icon
and self
.tray_icon
is not None:
3439 self
.tray_icon
.set_visible(False)
3441 self
.tray_icon
= None
3443 if self
.config
.minimize_to_tray
and self
.tray_icon
:
3444 self
.tray_icon
.set_visible(self
.is_iconified())
3445 elif self
.tray_icon
:
3446 self
.tray_icon
.set_visible(True)
3448 def on_itemShowAllEpisodes_activate(self
, widget
):
3449 self
.config
.podcast_list_view_all
= widget
.get_active()
3451 def on_itemShowToolbar_activate(self
, widget
):
3452 self
.config
.show_toolbar
= self
.itemShowToolbar
.get_active()
3454 def on_itemShowDescription_activate(self
, widget
):
3455 self
.config
.episode_list_descriptions
= self
.itemShowDescription
.get_active()
3457 def on_item_view_hide_boring_podcasts_toggled(self
, toggleaction
):
3458 self
.config
.podcast_list_hide_boring
= toggleaction
.get_active()
3459 if self
.config
.podcast_list_hide_boring
:
3460 self
.podcast_list_model
.set_view_mode(self
.config
.episode_list_view_mode
)
3462 self
.podcast_list_model
.set_view_mode(-1)
3464 def on_item_view_podcasts_changed(self
, radioaction
, current
):
3466 if current
== self
.item_view_podcasts_all
:
3467 self
.podcast_list_model
.set_view_mode(-1)
3468 elif current
== self
.item_view_podcasts_downloaded
:
3469 self
.podcast_list_model
.set_view_mode(EpisodeListModel
.VIEW_DOWNLOADED
)
3470 elif current
== self
.item_view_podcasts_unplayed
:
3471 self
.podcast_list_model
.set_view_mode(EpisodeListModel
.VIEW_UNPLAYED
)
3473 self
.config
.podcast_list_view_mode
= self
.podcast_list_model
.get_view_mode()
3475 def on_item_view_episodes_changed(self
, radioaction
, current
):
3476 if current
== self
.item_view_episodes_all
:
3477 self
.episode_list_model
.set_view_mode(EpisodeListModel
.VIEW_ALL
)
3478 elif current
== self
.item_view_episodes_undeleted
:
3479 self
.episode_list_model
.set_view_mode(EpisodeListModel
.VIEW_UNDELETED
)
3480 elif current
== self
.item_view_episodes_downloaded
:
3481 self
.episode_list_model
.set_view_mode(EpisodeListModel
.VIEW_DOWNLOADED
)
3482 elif current
== self
.item_view_episodes_unplayed
:
3483 self
.episode_list_model
.set_view_mode(EpisodeListModel
.VIEW_UNPLAYED
)
3485 self
.config
.episode_list_view_mode
= self
.episode_list_model
.get_view_mode()
3487 if self
.config
.podcast_list_hide_boring
and not gpodder
.ui
.fremantle
:
3488 self
.podcast_list_model
.set_view_mode(self
.config
.episode_list_view_mode
)
3490 def update_item_device( self
):
3491 if not gpodder
.ui
.fremantle
:
3492 if self
.config
.device_type
!= 'none':
3493 self
.itemDevice
.set_visible(True)
3494 self
.itemDevice
.label
= self
.get_device_name()
3496 self
.itemDevice
.set_visible(False)
3498 def properties_closed( self
):
3499 self
.preferences_dialog
= None
3500 self
.show_hide_tray_icon()
3501 self
.update_item_device()
3502 if gpodder
.ui
.maemo
:
3503 selection
= self
.treeAvailable
.get_selection()
3504 if self
.config
.maemo_enable_gestures
or \
3505 self
.config
.enable_fingerscroll
:
3506 selection
.set_mode(gtk
.SELECTION_SINGLE
)
3508 selection
.set_mode(gtk
.SELECTION_MULTIPLE
)
3510 def on_itemPreferences_activate(self
, widget
, *args
):
3511 self
.preferences_dialog
= gPodderPreferences(self
.main_window
, \
3512 _config
=self
.config
, \
3513 callback_finished
=self
.properties_closed
, \
3514 user_apps_reader
=self
.user_apps_reader
, \
3515 parent_window
=self
.main_window
, \
3516 mygpo_client
=self
.mygpo_client
, \
3517 on_send_full_subscriptions
=self
.on_send_full_subscriptions
)
3519 # Initial message to relayout window (in case it's opened in portrait mode
3520 self
.preferences_dialog
.on_window_orientation_changed(self
._last
_orientation
)
3522 def on_itemDependencies_activate(self
, widget
):
3523 gPodderDependencyManager(self
.gPodder
)
3525 def on_goto_mygpo(self
, widget
):
3526 self
.mygpo_client
.open_website()
3528 def on_download_subscriptions_from_mygpo(self
, action
=None):
3529 title
= _('Login to gpodder.net')
3530 message
= _('Please login to download your subscriptions.')
3531 success
, (username
, password
) = self
.show_login_dialog(title
, message
, \
3532 self
.config
.mygpo_username
, self
.config
.mygpo_password
)
3536 self
.config
.mygpo_username
= username
3537 self
.config
.mygpo_password
= password
3539 dir = gPodderPodcastDirectory(self
.gPodder
, _config
=self
.config
, \
3540 custom_title
=_('Subscriptions on gpodder.net'), \
3541 add_urls_callback
=self
.add_podcast_list
, \
3542 hide_url_entry
=True)
3544 # TODO: Refactor this into "gpodder.my" or mygpoclient, so that
3545 # we do not have to hardcode the URL here
3546 OPML_URL
= 'http://gpodder.net/subscriptions/%s.opml' % self
.config
.mygpo_username
3547 url
= util
.url_add_authentication(OPML_URL
, \
3548 self
.config
.mygpo_username
, \
3549 self
.config
.mygpo_password
)
3550 dir.download_opml_file(url
)
3552 def on_mygpo_settings_activate(self
, action
=None):
3553 # This dialog is only used for Maemo 4
3554 if not gpodder
.ui
.diablo
:
3557 settings
= MygPodderSettings(self
.main_window
, \
3558 config
=self
.config
, \
3559 mygpo_client
=self
.mygpo_client
, \
3560 on_send_full_subscriptions
=self
.on_send_full_subscriptions
)
3562 def on_itemAddChannel_activate(self
, widget
=None):
3563 gPodderAddPodcast(self
.gPodder
, \
3564 add_urls_callback
=self
.add_podcast_list
)
3566 def on_itemEditChannel_activate(self
, widget
, *args
):
3567 if self
.active_channel
is None:
3568 title
= _('No podcast selected')
3569 message
= _('Please select a podcast in the podcasts list to edit.')
3570 self
.show_message( message
, title
, widget
=self
.treeChannels
)
3573 callback_closed
= lambda: self
.update_podcast_list_model(selected
=True)
3574 gPodderChannel(self
.main_window
, \
3575 channel
=self
.active_channel
, \
3576 callback_closed
=callback_closed
, \
3577 cover_downloader
=self
.cover_downloader
)
3579 def on_itemMassUnsubscribe_activate(self
, item
=None):
3581 ('title', None, None, _('Podcast')),
3584 # We're abusing the Episode Selector for selecting Podcasts here,
3585 # but it works and looks good, so why not? -- thp
3586 gPodderEpisodeSelector(self
.main_window
, \
3587 title
=_('Remove podcasts'), \
3588 instructions
=_('Select the podcast you want to remove.'), \
3589 episodes
=self
.channels
, \
3591 size_attribute
=None, \
3592 stock_ok_button
=_('Remove'), \
3593 callback
=self
.remove_podcast_list
, \
3594 _config
=self
.config
)
3596 def remove_podcast_list(self
, channels
, confirm
=True):
3598 log('No podcasts selected for deletion', sender
=self
)
3601 if len(channels
) == 1:
3602 title
= _('Removing podcast')
3603 info
= _('Please wait while the podcast is removed')
3604 message
= _('Do you really want to remove this podcast and its episodes?')
3606 title
= _('Removing podcasts')
3607 info
= _('Please wait while the podcasts are removed')
3608 message
= _('Do you really want to remove the selected podcasts and their episodes?')
3610 if confirm
and not self
.show_confirmation(message
, title
):
3613 progress
= ProgressIndicator(title
, info
, parent
=self
.get_dialog_parent())
3615 def finish_deletion(select_url
):
3616 # Upload subscription list changes to the web service
3617 self
.mygpo_client
.on_unsubscribe([c
.url
for c
in channels
])
3619 # Re-load the channels and select the desired new channel
3620 self
.update_feed_cache(force_update
=False, select_url_afterwards
=select_url
)
3621 progress
.on_finished()
3622 self
.update_podcasts_tab()
3627 for idx
, channel
in enumerate(channels
):
3628 # Update the UI for correct status messages
3629 progress
.on_progress(float(idx
)/float(len(channels
)))
3630 progress
.on_message(channel
.title
)
3632 # Delete downloaded episodes
3633 channel
.remove_downloaded()
3635 # cancel any active downloads from this channel
3636 for episode
in channel
.get_all_episodes():
3637 util
.idle_add(self
.download_status_model
.cancel_by_url
,
3640 if len(channels
) == 1:
3641 # get the URL of the podcast we want to select next
3642 if channel
in self
.channels
:
3643 position
= self
.channels
.index(channel
)
3647 if position
== len(self
.channels
)-1:
3648 # this is the last podcast, so select the URL
3649 # of the item before this one (i.e. the "new last")
3650 select_url
= self
.channels
[position
-1].url
3652 # there is a podcast after the deleted one, so
3653 # we simply select the one that comes after it
3654 select_url
= self
.channels
[position
+1].url
3656 # Remove the channel and clean the database entries
3658 self
.channels
.remove(channel
)
3660 # Clean up downloads and download directories
3661 self
.clean_up_downloads()
3663 self
.channel_list_changed
= True
3664 self
.save_channels_opml()
3666 # The remaining stuff is to be done in the GTK main thread
3667 util
.idle_add(finish_deletion
, select_url
)
3669 threading
.Thread(target
=thread_proc
).start()
3671 def on_itemRemoveChannel_activate(self
, widget
, *args
):
3672 if self
.active_channel
is None:
3673 title
= _('No podcast selected')
3674 message
= _('Please select a podcast in the podcasts list to remove.')
3675 self
.show_message( message
, title
, widget
=self
.treeChannels
)
3678 self
.remove_podcast_list([self
.active_channel
])
3680 def get_opml_filter(self
):
3681 filter = gtk
.FileFilter()
3682 filter.add_pattern('*.opml')
3683 filter.add_pattern('*.xml')
3684 filter.set_name(_('OPML files')+' (*.opml, *.xml)')
3687 def on_item_import_from_file_activate(self
, widget
, filename
=None):
3688 if filename
is None:
3689 if gpodder
.ui
.desktop
or gpodder
.ui
.fremantle
:
3690 # FIXME: Hildonization on Fremantle
3691 dlg
= gtk
.FileChooserDialog(title
=_('Import from OPML'), parent
=None, action
=gtk
.FILE_CHOOSER_ACTION_OPEN
)
3692 dlg
.add_button(gtk
.STOCK_CANCEL
, gtk
.RESPONSE_CANCEL
)
3693 dlg
.add_button(gtk
.STOCK_OPEN
, gtk
.RESPONSE_OK
)
3694 elif gpodder
.ui
.diablo
:
3695 dlg
= hildon
.FileChooserDialog(self
.gPodder
, gtk
.FILE_CHOOSER_ACTION_OPEN
)
3696 dlg
.set_filter(self
.get_opml_filter())
3697 response
= dlg
.run()
3699 if response
== gtk
.RESPONSE_OK
:
3700 filename
= dlg
.get_filename()
3703 if filename
is not None:
3704 dir = gPodderPodcastDirectory(self
.gPodder
, _config
=self
.config
, \
3705 custom_title
=_('Import podcasts from OPML file'), \
3706 add_urls_callback
=self
.add_podcast_list
, \
3707 hide_url_entry
=True)
3708 dir.download_opml_file(filename
)
3710 def on_itemExportChannels_activate(self
, widget
, *args
):
3711 if not self
.channels
:
3712 title
= _('Nothing to export')
3713 message
= _('Your list of podcast subscriptions is empty. Please subscribe to some podcasts first before trying to export your subscription list.')
3714 self
.show_message(message
, title
, widget
=self
.treeChannels
)
3717 if gpodder
.ui
.desktop
or gpodder
.ui
.fremantle
:
3718 # FIXME: Hildonization on Fremantle
3719 dlg
= gtk
.FileChooserDialog(title
=_('Export to OPML'), parent
=self
.gPodder
, action
=gtk
.FILE_CHOOSER_ACTION_SAVE
)
3720 dlg
.add_button(gtk
.STOCK_CANCEL
, gtk
.RESPONSE_CANCEL
)
3721 dlg
.add_button(gtk
.STOCK_SAVE
, gtk
.RESPONSE_OK
)
3722 elif gpodder
.ui
.diablo
:
3723 dlg
= hildon
.FileChooserDialog(self
.gPodder
, gtk
.FILE_CHOOSER_ACTION_SAVE
)
3724 dlg
.set_filter(self
.get_opml_filter())
3725 response
= dlg
.run()
3726 if response
== gtk
.RESPONSE_OK
:
3727 filename
= dlg
.get_filename()
3729 exporter
= opml
.Exporter( filename
)
3730 if exporter
.write(self
.channels
):
3731 count
= len(self
.channels
)
3732 title
= N_('%d subscription exported', '%d subscriptions exported', count
) % count
3733 self
.show_message(_('Your podcast list has been successfully exported.'), title
, widget
=self
.treeChannels
)
3735 self
.show_message( _('Could not export OPML to file. Please check your permissions.'), _('OPML export failed'), important
=True)
3739 def on_itemImportChannels_activate(self
, widget
, *args
):
3740 if gpodder
.ui
.fremantle
:
3741 gPodderPodcastDirectory
.show_add_podcast_picker(self
.main_window
, \
3742 self
.config
.toplist_url
, \
3743 self
.config
.opml_url
, \
3744 self
.add_podcast_list
, \
3745 self
.on_itemAddChannel_activate
, \
3746 self
.on_download_subscriptions_from_mygpo
, \
3747 self
.show_text_edit_dialog
)
3749 dir = gPodderPodcastDirectory(self
.main_window
, _config
=self
.config
, \
3750 add_urls_callback
=self
.add_podcast_list
)
3751 util
.idle_add(dir.download_opml_file
, self
.config
.opml_url
)
3753 def on_homepage_activate(self
, widget
, *args
):
3754 util
.open_website(gpodder
.__url
__)
3756 def on_wiki_activate(self
, widget
, *args
):
3757 util
.open_website('http://gpodder.org/wiki/User_Manual')
3759 def on_bug_tracker_activate(self
, widget
, *args
):
3760 if gpodder
.ui
.maemo
:
3761 util
.open_website('http://bugs.maemo.org/enter_bug.cgi?product=gPodder')
3763 util
.open_website('https://bugs.gpodder.org/enter_bug.cgi?product=gPodder')
3765 def on_item_support_activate(self
, widget
):
3766 util
.open_website('http://gpodder.org/donate')
3768 def on_itemAbout_activate(self
, widget
, *args
):
3769 if gpodder
.ui
.fremantle
:
3770 from gpodder
.gtkui
.frmntl
.about
import HeAboutDialog
3771 HeAboutDialog
.present(self
.main_window
,
3774 gpodder
.__version
__,
3775 _('A podcast client with focus on usability'),
3776 gpodder
.__copyright
__,
3778 'http://bugs.maemo.org/enter_bug.cgi?product=gPodder',
3779 'http://gpodder.org/donate')
3782 dlg
= gtk
.AboutDialog()
3783 dlg
.set_transient_for(self
.main_window
)
3784 dlg
.set_name('gPodder')
3785 dlg
.set_version(gpodder
.__version
__)
3786 dlg
.set_copyright(gpodder
.__copyright
__)
3787 dlg
.set_comments(_('A podcast client with focus on usability'))
3788 dlg
.set_website(gpodder
.__url
__)
3789 dlg
.set_translator_credits( _('translator-credits'))
3790 dlg
.connect( 'response', lambda dlg
, response
: dlg
.destroy())
3792 if gpodder
.ui
.desktop
:
3793 # For the "GUI" version, we add some more
3794 # items to the about dialog (credits and logo)
3797 'Thomas Perl <thpinfo.com>',
3800 if os
.path
.exists(gpodder
.credits_file
):
3801 credits
= open(gpodder
.credits_file
).read().strip().split('\n')
3802 app_authors
+= ['', _('Patches, bug reports and donations by:')]
3803 app_authors
+= credits
3805 dlg
.set_authors(app_authors
)
3807 dlg
.set_logo(gtk
.gdk
.pixbuf_new_from_file(gpodder
.icon_file
))
3809 dlg
.set_logo_icon_name('gpodder')
3813 def on_wNotebook_switch_page(self
, widget
, *args
):
3815 if gpodder
.ui
.maemo
:
3816 self
.tool_downloads
.set_active(page_num
== 1)
3817 page
= self
.wNotebook
.get_nth_page(page_num
)
3818 tab_label
= self
.wNotebook
.get_tab_label(page
).get_text()
3819 if page_num
== 0 and self
.active_channel
is not None:
3820 self
.set_title(self
.active_channel
.title
)
3822 self
.set_title(tab_label
)
3824 self
.play_or_download()
3825 self
.menuChannels
.set_sensitive(True)
3826 self
.menuSubscriptions
.set_sensitive(True)
3827 # The message area in the downloads tab should be hidden
3828 # when the user switches away from the downloads tab
3829 if self
.message_area
is not None:
3830 self
.message_area
.hide()
3831 self
.message_area
= None
3833 self
.menuChannels
.set_sensitive(False)
3834 self
.menuSubscriptions
.set_sensitive(False)
3835 if gpodder
.ui
.desktop
:
3836 self
.toolDownload
.set_sensitive(False)
3837 self
.toolPlay
.set_sensitive(False)
3838 self
.toolTransfer
.set_sensitive(False)
3839 self
.toolCancel
.set_sensitive(False)
3841 def on_treeChannels_row_activated(self
, widget
, path
, *args
):
3842 # double-click action of the podcast list or enter
3843 self
.treeChannels
.set_cursor(path
)
3845 def on_treeChannels_cursor_changed(self
, widget
, *args
):
3846 ( model
, iter ) = self
.treeChannels
.get_selection().get_selected()
3848 if model
is not None and iter is not None:
3849 old_active_channel
= self
.active_channel
3850 self
.active_channel
= model
.get_value(iter, PodcastListModel
.C_CHANNEL
)
3852 if self
.active_channel
== old_active_channel
:
3855 if gpodder
.ui
.maemo
:
3856 self
.set_title(self
.active_channel
.title
)
3858 # Dirty hack to check for "All episodes" (see gpodder.gtkui.model)
3859 if getattr(self
.active_channel
, 'ALL_EPISODES_PROXY', False):
3860 self
.itemEditChannel
.set_visible(False)
3861 self
.itemRemoveChannel
.set_visible(False)
3863 self
.itemEditChannel
.set_visible(True)
3864 self
.itemRemoveChannel
.set_visible(True)
3866 self
.active_channel
= None
3867 self
.itemEditChannel
.set_visible(False)
3868 self
.itemRemoveChannel
.set_visible(False)
3870 self
.update_episode_list_model()
3872 def on_btnEditChannel_clicked(self
, widget
, *args
):
3873 self
.on_itemEditChannel_activate( widget
, args
)
3875 def get_podcast_urls_from_selected_episodes(self
):
3876 """Get a set of podcast URLs based on the selected episodes"""
3877 return set(episode
.channel
.url
for episode
in \
3878 self
.get_selected_episodes())
3880 def get_selected_episodes(self
):
3881 """Get a list of selected episodes from treeAvailable"""
3882 selection
= self
.treeAvailable
.get_selection()
3883 model
, paths
= selection
.get_selected_rows()
3885 episodes
= [model
.get_value(model
.get_iter(path
), EpisodeListModel
.C_EPISODE
) for path
in paths
]
3888 def on_transfer_selected_episodes(self
, widget
):
3889 self
.on_sync_to_ipod_activate(widget
, self
.get_selected_episodes())
3891 def on_playback_selected_episodes(self
, widget
):
3892 self
.playback_episodes(self
.get_selected_episodes())
3894 def on_shownotes_selected_episodes(self
, widget
):
3895 episodes
= self
.get_selected_episodes()
3897 episode
= episodes
.pop(0)
3898 self
.show_episode_shownotes(episode
)
3900 self
.show_message(_('Please select an episode from the episode list to display shownotes.'), _('No episode selected'), widget
=self
.treeAvailable
)
3902 def on_download_selected_episodes(self
, widget
):
3903 episodes
= self
.get_selected_episodes()
3904 self
.download_episode_list(episodes
)
3905 self
.update_episode_list_icons([episode
.url
for episode
in episodes
])
3906 self
.play_or_download()
3908 def on_treeAvailable_row_activated(self
, widget
, path
, view_column
):
3909 """Double-click/enter action handler for treeAvailable"""
3910 # We should only have one one selected as it was double clicked!
3911 e
= self
.get_selected_episodes()[0]
3913 if (self
.config
.double_click_episode_action
== 'download'):
3914 # If the episode has already been downloaded and exists then play it
3915 if e
.was_downloaded(and_exists
=True):
3916 self
.playback_episodes(self
.get_selected_episodes())
3917 # else download it if it is not already downloading
3918 elif not self
.episode_is_downloading(e
):
3919 self
.download_episode_list([e
])
3920 self
.update_episode_list_icons([e
.url
])
3921 self
.play_or_download()
3922 elif (self
.config
.double_click_episode_action
== 'stream'):
3923 # If we happen to have downloaded this episode simple play it
3924 if e
.was_downloaded(and_exists
=True):
3925 self
.playback_episodes(self
.get_selected_episodes())
3926 # else if streaming is possible stream it
3927 elif self
.streaming_possible():
3928 self
.playback_episodes(self
.get_selected_episodes())
3930 log('Unable to stream episode - default media player selected!', sender
=self
, traceback
=True)
3931 self
.show_message(_('Please check your media player settings in the preferences dialog.'), _('Unable to stream episode'), widget
=self
.toolPreferences
)
3933 # default action is to display show notes
3934 self
.on_shownotes_selected_episodes(widget
)
3936 def show_episode_shownotes(self
, episode
):
3937 if self
.episode_shownotes_window
is None:
3938 log('First-time use of episode window --- creating', sender
=self
)
3939 self
.episode_shownotes_window
= gPodderShownotes(self
.gPodder
, _config
=self
.config
, \
3940 _download_episode_list
=self
.download_episode_list
, \
3941 _playback_episodes
=self
.playback_episodes
, \
3942 _delete_episode_list
=self
.delete_episode_list
, \
3943 _episode_list_status_changed
=self
.episode_list_status_changed
, \
3944 _cancel_task_list
=self
.cancel_task_list
, \
3945 _episode_is_downloading
=self
.episode_is_downloading
, \
3946 _streaming_possible
=self
.streaming_possible())
3947 self
.episode_shownotes_window
.show(episode
)
3948 if self
.episode_is_downloading(episode
):
3949 self
.update_downloads_list()
3951 def restart_auto_update_timer(self
):
3952 if self
._auto
_update
_timer
_source
_id
is not None:
3953 log('Removing existing auto update timer.', sender
=self
)
3954 gobject
.source_remove(self
._auto
_update
_timer
_source
_id
)
3955 self
._auto
_update
_timer
_source
_id
= None
3957 if self
.config
.auto_update_feeds
and \
3958 self
.config
.auto_update_frequency
:
3959 interval
= 60*1000*self
.config
.auto_update_frequency
3960 log('Setting up auto update timer with interval %d.', \
3961 self
.config
.auto_update_frequency
, sender
=self
)
3962 self
._auto
_update
_timer
_source
_id
= gobject
.timeout_add(\
3963 interval
, self
._on
_auto
_update
_timer
)
3965 def _on_auto_update_timer(self
):
3966 log('Auto update timer fired.', sender
=self
)
3967 self
.update_feed_cache(force_update
=True)
3969 # Ask web service for sub changes (if enabled)
3970 self
.mygpo_client
.flush()
3974 def on_treeDownloads_row_activated(self
, widget
, *args
):
3975 # Use the standard way of working on the treeview
3976 selection
= self
.treeDownloads
.get_selection()
3977 (model
, paths
) = selection
.get_selected_rows()
3978 selected_tasks
= [(gtk
.TreeRowReference(model
, path
), model
.get_value(model
.get_iter(path
), 0)) for path
in paths
]
3980 for tree_row_reference
, task
in selected_tasks
:
3981 if task
.status
in (task
.DOWNLOADING
, task
.QUEUED
):
3982 task
.status
= task
.PAUSED
3983 elif task
.status
in (task
.CANCELLED
, task
.PAUSED
, task
.FAILED
):
3984 self
.download_queue_manager
.add_task(task
)
3985 self
.enable_download_list_update()
3986 elif task
.status
== task
.DONE
:
3987 model
.remove(model
.get_iter(tree_row_reference
.get_path()))
3989 self
.play_or_download()
3991 # Update the tab title and downloads list
3992 self
.update_downloads_list()
3994 def on_item_cancel_download_activate(self
, widget
):
3995 if self
.wNotebook
.get_current_page() == 0:
3996 selection
= self
.treeAvailable
.get_selection()
3997 (model
, paths
) = selection
.get_selected_rows()
3998 urls
= [model
.get_value(model
.get_iter(path
), \
3999 self
.episode_list_model
.C_URL
) for path
in paths
]
4000 selected_tasks
= [task
for task
in self
.download_tasks_seen \
4001 if task
.url
in urls
]
4003 selection
= self
.treeDownloads
.get_selection()
4004 (model
, paths
) = selection
.get_selected_rows()
4005 selected_tasks
= [model
.get_value(model
.get_iter(path
), \
4006 self
.download_status_model
.C_TASK
) for path
in paths
]
4007 self
.cancel_task_list(selected_tasks
)
4009 def on_btnCancelAll_clicked(self
, widget
, *args
):
4010 self
.cancel_task_list(self
.download_tasks_seen
)
4012 def on_btnDownloadedDelete_clicked(self
, widget
, *args
):
4013 episodes
= self
.get_selected_episodes()
4014 if len(episodes
) == 1:
4015 self
.delete_episode_list(episodes
, skip_locked
=False)
4017 self
.delete_episode_list(episodes
)
4019 def on_key_press(self
, widget
, event
):
4020 # Allow tab switching with Ctrl + PgUp/PgDown
4021 if event
.state
& gtk
.gdk
.CONTROL_MASK
:
4022 if event
.keyval
== gtk
.keysyms
.Page_Up
:
4023 self
.wNotebook
.prev_page()
4025 elif event
.keyval
== gtk
.keysyms
.Page_Down
:
4026 self
.wNotebook
.next_page()
4029 # After this code we only handle Maemo hardware keys,
4030 # so if we are not a Maemo app, we don't do anything
4031 if not gpodder
.ui
.maemo
:
4035 if event
.keyval
== gtk
.keysyms
.F7
: #plus
4037 elif event
.keyval
== gtk
.keysyms
.F8
: #minus
4040 if diff
!= 0 and not self
.currently_updating
:
4041 selection
= self
.treeChannels
.get_selection()
4042 (model
, iter) = selection
.get_selected()
4043 new_path
= ((model
.get_path(iter)[0]+diff
)%len(model
),)
4044 selection
.select_path(new_path
)
4045 self
.treeChannels
.set_cursor(new_path
)
4050 def on_iconify(self
):
4052 self
.gPodder
.set_skip_taskbar_hint(True)
4053 if self
.config
.minimize_to_tray
:
4054 self
.tray_icon
.set_visible(True)
4056 self
.gPodder
.set_skip_taskbar_hint(False)
4058 def on_uniconify(self
):
4060 self
.gPodder
.set_skip_taskbar_hint(False)
4061 if self
.config
.minimize_to_tray
:
4062 self
.tray_icon
.set_visible(False)
4064 self
.gPodder
.set_skip_taskbar_hint(False)
4066 def uniconify_main_window(self
):
4067 if self
.is_iconified():
4068 # We need to hide and then show the window in WMs like Metacity
4069 # or KWin4 to move the window to the active workspace
4070 # (see http://gpodder.org/bug/1125)
4073 self
.gPodder
.present()
4075 def iconify_main_window(self
):
4076 if not self
.is_iconified():
4077 self
.gPodder
.iconify()
4079 def update_podcasts_tab(self
):
4080 if len(self
.channels
):
4081 if gpodder
.ui
.fremantle
:
4082 self
.button_refresh
.set_title(_('Check for new episodes'))
4083 self
.button_refresh
.show()
4085 self
.label2
.set_text(_('Podcasts (%d)') % len(self
.channels
))
4087 if gpodder
.ui
.fremantle
:
4088 self
.button_refresh
.hide()
4090 self
.label2
.set_text(_('Podcasts'))
4092 @dbus.service
.method(gpodder
.dbus_interface
)
4093 def show_gui_window(self
):
4094 parent
= self
.get_dialog_parent()
4097 @dbus.service
.method(gpodder
.dbus_interface
)
4098 def subscribe_to_url(self
, url
):
4099 gPodderAddPodcast(self
.gPodder
,
4100 add_urls_callback
=self
.add_podcast_list
,
4103 @dbus.service
.method(gpodder
.dbus_interface
)
4104 def mark_episode_played(self
, filename
):
4105 if filename
is None:
4108 for channel
in self
.channels
:
4109 for episode
in channel
.get_all_episodes():
4110 fn
= episode
.local_filename(create
=False, check_only
=True)
4112 episode
.mark(is_played
=True)
4114 self
.update_episode_list_icons([episode
.url
])
4115 self
.update_podcast_list_model([episode
.channel
.url
])
4121 def main(options
=None):
4122 gobject
.threads_init()
4123 gobject
.set_application_name('gPodder')
4125 if gpodder
.ui
.maemo
:
4126 # Try to enable the custom icon theme for gPodder on Maemo
4127 settings
= gtk
.settings_get_default()
4128 settings
.set_string_property('gtk-icon-theme-name', \
4129 'gpodder', __file__
)
4130 # Extend the search path for the optified icon theme (Maemo 5)
4131 icon_theme
= gtk
.icon_theme_get_default()
4132 icon_theme
.prepend_search_path('/opt/gpodder-icon-theme/')
4134 gtk
.window_set_default_icon_name('gpodder')
4135 gtk
.about_dialog_set_url_hook(lambda dlg
, link
, data
: util
.open_website(link
), None)
4138 dbus_main_loop
= dbus
.glib
.DBusGMainLoop(set_as_default
=True)
4139 gpodder
.dbus_session_bus
= dbus
.SessionBus(dbus_main_loop
)
4141 bus_name
= dbus
.service
.BusName(gpodder
.dbus_bus_name
, bus
=gpodder
.dbus_session_bus
)
4142 except dbus
.exceptions
.DBusException
, dbe
:
4143 log('Warning: Cannot get "on the bus".', traceback
=True)
4144 dlg
= gtk
.MessageDialog(None, gtk
.DIALOG_MODAL
, gtk
.MESSAGE_ERROR
, \
4145 gtk
.BUTTONS_CLOSE
, _('Cannot start gPodder'))
4146 dlg
.format_secondary_markup(_('D-Bus error: %s') % (str(dbe
),))
4147 dlg
.set_title('gPodder')
4152 util
.make_directory(gpodder
.home
)
4153 gpodder
.load_plugins()
4155 config
= UIConfig(gpodder
.config_file
)
4157 # Load hook modules and install the hook manager globally
4158 # if modules have been found an instantiated by the manager
4159 user_hooks
= hooks
.HookManager()
4160 if user_hooks
.has_modules():
4161 gpodder
.user_hooks
= user_hooks
4163 if gpodder
.ui
.diablo
:
4164 # Detect changing of SD cards between mmc1/mmc2 if a gpodder
4165 # folder exists there (allow moving "gpodder" between SD cards or USB)
4166 # Also allow moving "gpodder" to home folder (e.g. rootfs on SD)
4167 if not os
.path
.exists(config
.download_dir
):
4168 log('Downloads might have been moved. Trying to locate them...')
4169 for basedir
in ['/media/mmc1', '/media/mmc2']+glob
.glob('/media/usb/*')+['/home/user/MyDocs']:
4170 dir = os
.path
.join(basedir
, 'gpodder')
4171 if os
.path
.exists(dir):
4172 log('Downloads found in: %s', dir)
4173 config
.download_dir
= dir
4176 log('Downloads NOT FOUND in %s', dir)
4177 elif gpodder
.ui
.fremantle
:
4178 config
.on_quit_ask
= False
4180 if config
.enable_fingerscroll
:
4181 BuilderWidget
.use_fingerscroll
= True
4183 config
.mygpo_device_type
= util
.detect_device_type()
4185 gp
= gPodder(bus_name
, config
)
4188 if options
.subscribe
:
4189 util
.idle_add(gp
.subscribe_to_url
, options
.subscribe
)
4192 # handle "subscribe to podcast" events from firefox
4193 if platform
.system() == 'Darwin':
4194 from gpodder
import gpodderosx
4195 gpodderosx
.register_handlers(gp
)
4196 # end mac OS X stuff