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 # Delay until live search is started after typing stop
165 LIVE_SEARCH_DELAY
= 200
167 def __init__(self
, bus_name
, config
):
168 dbus
.service
.Object
.__init
__(self
, object_path
=gpodder
.dbus_gui_object_path
, bus_name
=bus_name
)
169 self
.podcasts_proxy
= DBusPodcastsProxy(lambda: self
.channels
, \
170 self
.on_itemUpdate_activate
, \
171 self
.playback_episodes
, \
172 self
.download_episode_list
, \
173 self
.episode_object_by_uri
, \
175 self
.db
= Database(gpodder
.database_file
)
177 BuilderWidget
.__init
__(self
, None)
180 if gpodder
.ui
.diablo
:
182 self
.app
= hildon
.Program()
183 self
.app
.add_window(self
.main_window
)
184 self
.main_window
.add_toolbar(self
.toolbar
)
186 for child
in self
.main_menu
.get_children():
188 self
.main_window
.set_menu(self
.set_finger_friendly(menu
))
189 self
._last
_orientation
= Orientation
.LANDSCAPE
190 elif gpodder
.ui
.fremantle
:
192 self
.app
= hildon
.Program()
193 self
.app
.add_window(self
.main_window
)
195 appmenu
= hildon
.AppMenu()
197 for filter in (self
.item_view_podcasts_all
, \
198 self
.item_view_podcasts_downloaded
, \
199 self
.item_view_podcasts_unplayed
):
200 button
= gtk
.ToggleButton()
201 filter.connect_proxy(button
)
202 appmenu
.add_filter(button
)
204 for action
in (self
.itemPreferences
, \
205 self
.item_downloads
, \
206 self
.itemRemoveOldEpisodes
, \
207 self
.item_unsubscribe
, \
209 button
= hildon
.Button(gtk
.HILDON_SIZE_AUTO
,\
210 hildon
.BUTTON_ARRANGEMENT_HORIZONTAL
)
211 action
.connect_proxy(button
)
212 if action
== self
.item_downloads
:
213 button
.set_title(_('Downloads'))
214 button
.set_value(_('Idle'))
215 self
.button_downloads
= button
216 appmenu
.append(button
)
218 def show_hint(button
):
219 self
.show_message(random
.choice(HINT_STRINGS
), important
=True)
221 button
= hildon
.Button(gtk
.HILDON_SIZE_AUTO
,\
222 hildon
.BUTTON_ARRANGEMENT_HORIZONTAL
)
223 button
.set_title(_('Hint of the day'))
224 button
.connect('clicked', show_hint
)
225 appmenu
.append(button
)
228 self
.main_window
.set_app_menu(appmenu
)
230 # Initialize portrait mode / rotation manager
231 self
._fremantle
_rotation
= FremantleRotation('gPodder', \
233 gpodder
.__version
__, \
234 self
.config
.rotation_mode
)
236 if self
.config
.rotation_mode
== FremantleRotation
.ALWAYS
:
237 util
.idle_add(self
.on_window_orientation_changed
, \
238 Orientation
.PORTRAIT
)
239 self
._last
_orientation
= Orientation
.PORTRAIT
241 self
._last
_orientation
= Orientation
.LANDSCAPE
243 # Flag set when a notification is being shown (Maemo bug 11235)
244 self
._fremantle
_notification
_visible
= False
246 self
._last
_orientation
= Orientation
.LANDSCAPE
247 self
.toolbar
.set_property('visible', self
.config
.show_toolbar
)
249 self
.bluetooth_available
= util
.bluetooth_available()
251 self
.config
.connect_gtk_window(self
.gPodder
, 'main_window')
252 if not gpodder
.ui
.fremantle
:
253 self
.config
.connect_gtk_paned('paned_position', self
.channelPaned
)
254 self
.main_window
.show()
256 self
.player_receiver
= player
.MediaPlayerDBusReceiver(self
.on_played
)
258 if gpodder
.ui
.fremantle
:
259 # Create a D-Bus monitoring object that takes care of
260 # tracking MAFW (Nokia Media Player) playback events
261 # and sends episode playback status events via D-Bus
262 self
.mafw_monitor
= MafwPlaybackMonitor(gpodder
.dbus_session_bus
)
264 self
.gPodder
.connect('key-press-event', self
.on_key_press
)
266 self
.preferences_dialog
= None
267 self
.config
.add_observer(self
.on_config_changed
)
269 self
.tray_icon
= None
270 self
.episode_shownotes_window
= None
271 self
.new_episodes_window
= None
273 if gpodder
.ui
.desktop
:
274 # Mac OS X-specific UI tweaks: Native main menu integration
275 # http://sourceforge.net/apps/trac/gtk-osx/wiki/Integrate
276 if getattr(gtk
.gdk
, 'WINDOWING', 'x11') == 'quartz':
278 import igemacintegration
as igemi
280 # Move the menu bar from the window to the Mac menu bar
282 igemi
.ige_mac_menu_set_menu_bar(self
.mainMenu
)
284 # Reparent some items to the "Application" menu
285 for widget
in ('/mainMenu/menuHelp/itemAbout', \
286 '/mainMenu/menuPodcasts/itemPreferences'):
287 item
= self
.uimanager1
.get_widget(widget
)
288 group
= igemi
.ige_mac_menu_add_app_menu_group()
289 igemi
.ige_mac_menu_add_app_menu_item(group
, item
, None)
291 quit_widget
= '/mainMenu/menuPodcasts/itemQuit'
292 quit_item
= self
.uimanager1
.get_widget(quit_widget
)
293 igemi
.ige_mac_menu_set_quit_menu_item(quit_item
)
295 print >>sys
.stderr
, """
296 Warning: ige-mac-integration not found - no native menus.
299 self
.sync_ui
= gPodderSyncUI(self
.config
, self
.notification
, \
300 self
.main_window
, self
.show_confirmation
, \
301 self
.update_episode_list_icons
, \
302 self
.update_podcast_list_model
, self
.toolPreferences
, \
303 gPodderEpisodeSelector
, \
304 self
.commit_changes_to_database
)
308 self
.download_status_model
= DownloadStatusModel()
309 self
.download_queue_manager
= download
.DownloadQueueManager(self
.config
)
311 if gpodder
.ui
.desktop
:
312 self
.show_hide_tray_icon()
313 self
.itemShowAllEpisodes
.set_active(self
.config
.podcast_list_view_all
)
314 self
.itemShowToolbar
.set_active(self
.config
.show_toolbar
)
315 self
.itemShowDescription
.set_active(self
.config
.episode_list_descriptions
)
317 if not gpodder
.ui
.fremantle
:
318 self
.config
.connect_gtk_spinbutton('max_downloads', self
.spinMaxDownloads
)
319 self
.config
.connect_gtk_togglebutton('max_downloads_enabled', self
.cbMaxDownloads
)
320 self
.config
.connect_gtk_spinbutton('limit_rate_value', self
.spinLimitDownloads
)
321 self
.config
.connect_gtk_togglebutton('limit_rate', self
.cbLimitDownloads
)
323 # When the amount of maximum downloads changes, notify the queue manager
324 changed_cb
= lambda spinbutton
: self
.download_queue_manager
.spawn_threads()
325 self
.spinMaxDownloads
.connect('value-changed', changed_cb
)
327 self
.default_title
= 'gPodder'
328 if gpodder
.__version
__.rfind('git') != -1:
329 self
.set_title('gPodder %s' % gpodder
.__version
__)
331 title
= self
.gPodder
.get_title()
332 if title
is not None:
333 self
.set_title(title
)
335 self
.set_title(_('gPodder'))
337 self
.cover_downloader
= CoverDownloader()
339 # Generate list models for podcasts and their episodes
340 self
.podcast_list_model
= PodcastListModel(self
.cover_downloader
)
342 self
.cover_downloader
.register('cover-available', self
.cover_download_finished
)
343 self
.cover_downloader
.register('cover-removed', self
.cover_file_removed
)
345 if gpodder
.ui
.fremantle
:
346 # Work around Maemo bug #4718
347 self
.button_refresh
.set_name('HildonButton-finger')
348 self
.button_subscribe
.set_name('HildonButton-finger')
350 self
.button_refresh
.set_sensitive(False)
351 self
.button_subscribe
.set_sensitive(False)
353 self
.button_subscribe
.set_image(gtk
.image_new_from_icon_name(\
354 self
.ICON_GENERAL_ADD
, gtk
.ICON_SIZE_BUTTON
))
355 self
.button_refresh
.set_image(gtk
.image_new_from_icon_name(\
356 self
.ICON_GENERAL_REFRESH
, gtk
.ICON_SIZE_BUTTON
))
358 # Make the button scroll together with the TreeView contents
359 action_area_box
= self
.treeChannels
.get_action_area_box()
360 for child
in self
.buttonbox
:
361 child
.reparent(action_area_box
)
362 self
.vbox
.remove(self
.buttonbox
)
363 action_area_box
.set_spacing(2)
364 action_area_box
.set_border_width(3)
365 self
.treeChannels
.set_action_area_visible(True)
367 # Set up a very nice progress bar setup
368 self
.fancy_progress_bar
= FancyProgressBar(self
.main_window
, \
369 self
.on_btnCancelFeedUpdate_clicked
)
370 self
.pbFeedUpdate
= self
.fancy_progress_bar
.progress_bar
371 self
.pbFeedUpdate
.set_ellipsize(pango
.ELLIPSIZE_MIDDLE
)
372 self
.vbox
.pack_start(self
.fancy_progress_bar
.event_box
, False)
374 from gpodder
.gtkui
.frmntl
import style
375 sub_font
= style
.get_font_desc('SmallSystemFont')
376 sub_color
= style
.get_color('SecondaryTextColor')
377 sub
= (sub_font
.to_string(), sub_color
.to_string())
378 sub
= '<span font_desc="%s" foreground="%s">%%s</span>' % sub
379 self
.label_footer
.set_markup(sub
% gpodder
.__copyright
__)
381 hildon
.hildon_gtk_window_set_progress_indicator(self
.main_window
, True)
382 while gtk
.events_pending():
383 gtk
.main_iteration(False)
386 # Try to get the real package version from dpkg
387 p
= subprocess
.Popen(['dpkg-query', '-W', '-f=${Version}', 'gpodder'], stdout
=subprocess
.PIPE
)
388 version
, _stderr
= p
.communicate()
392 version
= gpodder
.__version
__
393 self
.label_footer
.set_markup(sub
% ('v %s' % version
))
394 self
.label_footer
.hide()
396 self
.episodes_window
= gPodderEpisodes(self
.main_window
, \
397 on_treeview_expose_event
=self
.on_treeview_expose_event
, \
398 show_episode_shownotes
=self
.show_episode_shownotes
, \
399 update_podcast_list_model
=self
.update_podcast_list_model
, \
400 on_itemRemoveChannel_activate
=self
.on_itemRemoveChannel_activate
, \
401 item_view_episodes_all
=self
.item_view_episodes_all
, \
402 item_view_episodes_unplayed
=self
.item_view_episodes_unplayed
, \
403 item_view_episodes_downloaded
=self
.item_view_episodes_downloaded
, \
404 item_view_episodes_undeleted
=self
.item_view_episodes_undeleted
, \
405 on_entry_search_episodes_changed
=self
.on_entry_search_episodes_changed
, \
406 on_entry_search_episodes_key_press
=self
.on_entry_search_episodes_key_press
, \
407 hide_episode_search
=self
.hide_episode_search
, \
408 on_itemUpdateChannel_activate
=self
.on_itemUpdateChannel_activate
, \
409 playback_episodes
=self
.playback_episodes
, \
410 delete_episode_list
=self
.delete_episode_list
, \
411 episode_list_status_changed
=self
.episode_list_status_changed
, \
412 download_episode_list
=self
.download_episode_list
, \
413 episode_is_downloading
=self
.episode_is_downloading
, \
414 show_episode_in_download_manager
=self
.show_episode_in_download_manager
, \
415 add_download_task_monitor
=self
.add_download_task_monitor
, \
416 remove_download_task_monitor
=self
.remove_download_task_monitor
, \
417 for_each_episode_set_task_status
=self
.for_each_episode_set_task_status
, \
418 on_delete_episodes_button_clicked
=self
.on_itemRemoveOldEpisodes_activate
, \
419 on_itemUpdate_activate
=self
.on_itemUpdate_activate
)
421 # Expose objects for episode list type-ahead find
422 self
.hbox_search_episodes
= self
.episodes_window
.hbox_search_episodes
423 self
.entry_search_episodes
= self
.episodes_window
.entry_search_episodes
424 self
.button_search_episodes_clear
= self
.episodes_window
.button_search_episodes_clear
426 self
.downloads_window
= gPodderDownloads(self
.main_window
, \
427 on_treeview_expose_event
=self
.on_treeview_expose_event
, \
428 cleanup_downloads
=self
.cleanup_downloads
, \
429 _for_each_task_set_status
=self
._for
_each
_task
_set
_status
, \
430 downloads_list_get_selection
=self
.downloads_list_get_selection
, \
433 self
.treeAvailable
= self
.episodes_window
.treeview
434 self
.treeDownloads
= self
.downloads_window
.treeview
436 # Source IDs for timeouts for search-as-you-type
437 self
._podcast
_list
_search
_timeout
= None
438 self
._episode
_list
_search
_timeout
= None
440 # Init the treeviews that we use
441 self
.init_podcast_list_treeview()
442 self
.init_episode_list_treeview()
443 self
.init_download_list_treeview()
445 if self
.config
.podcast_list_hide_boring
:
446 self
.item_view_hide_boring_podcasts
.set_active(True)
448 self
.currently_updating
= False
450 if gpodder
.ui
.maemo
or self
.config
.enable_fingerscroll
:
451 self
.context_menu_mouse_button
= 1
453 self
.context_menu_mouse_button
= 3
455 if self
.config
.start_iconified
:
456 self
.iconify_main_window()
458 self
.download_tasks_seen
= set()
459 self
.download_list_update_enabled
= False
460 self
.download_task_monitors
= set()
462 # Subscribed channels
463 self
.active_channel
= None
464 self
.channels
= PodcastChannel
.load_from_db(self
.db
, self
.config
.download_dir
)
465 self
.channel_list_changed
= True
466 self
.update_podcasts_tab()
468 # load list of user applications for audio playback
469 self
.user_apps_reader
= UserAppsReader(['audio', 'video'])
470 threading
.Thread(target
=self
.user_apps_reader
.read
).start()
472 # Set the "Device" menu item for the first time
473 if gpodder
.ui
.desktop
:
474 self
.update_item_device()
476 # Set up the first instance of MygPoClient
477 self
.mygpo_client
= my
.MygPoClient(self
.config
)
479 # Now, update the feed cache, when everything's in place
480 if not gpodder
.ui
.fremantle
:
481 self
.btnUpdateFeeds
.show()
482 self
.updating_feed_cache
= False
483 self
.feed_cache_update_cancelled
= False
484 self
.update_feed_cache(force_update
=self
.config
.update_on_startup
)
486 self
.message_area
= None
488 def find_partial_downloads():
489 # Look for partial file downloads
490 partial_files
= glob
.glob(os
.path
.join(self
.config
.download_dir
, '*', '*.partial'))
491 count
= len(partial_files
)
492 resumable_episodes
= []
494 if not gpodder
.ui
.fremantle
:
495 util
.idle_add(self
.wNotebook
.set_current_page
, 1)
496 indicator
= ProgressIndicator(_('Loading incomplete downloads'), \
497 _('Some episodes have not finished downloading in a previous session.'), \
498 False, self
.get_dialog_parent())
499 indicator
.on_message(N_('%d partial file', '%d partial files', count
) % count
)
501 candidates
= [f
[:-len('.partial')] for f
in partial_files
]
504 for c
in self
.channels
:
505 for e
in c
.get_all_episodes():
506 filename
= e
.local_filename(create
=False, check_only
=True)
507 if filename
in candidates
:
508 log('Found episode: %s', e
.title
, sender
=self
)
510 indicator
.on_message(e
.title
)
511 indicator
.on_progress(float(found
)/count
)
512 candidates
.remove(filename
)
513 partial_files
.remove(filename
+'.partial')
514 resumable_episodes
.append(e
)
522 for f
in partial_files
:
523 log('Partial file without episode: %s', f
, sender
=self
)
526 util
.idle_add(indicator
.on_finished
)
528 if len(resumable_episodes
):
529 def offer_resuming():
530 self
.download_episode_list_paused(resumable_episodes
)
531 if not gpodder
.ui
.fremantle
:
532 resume_all
= gtk
.Button(_('Resume all'))
533 #resume_all.set_border_width(0)
534 def on_resume_all(button
):
535 selection
= self
.treeDownloads
.get_selection()
536 selection
.select_all()
537 selected_tasks
, can_queue
, can_cancel
, can_pause
, can_remove
, can_force
= self
.downloads_list_get_selection()
538 selection
.unselect_all()
539 self
._for
_each
_task
_set
_status
(selected_tasks
, download
.DownloadTask
.QUEUED
)
540 self
.message_area
.hide()
541 resume_all
.connect('clicked', on_resume_all
)
543 self
.message_area
= SimpleMessageArea(_('Incomplete downloads from a previous session were found.'), (resume_all
,))
544 self
.vboxDownloadStatusWidgets
.pack_start(self
.message_area
, expand
=False)
545 self
.vboxDownloadStatusWidgets
.reorder_child(self
.message_area
, 0)
546 self
.message_area
.show_all()
547 self
.clean_up_downloads(delete_partial
=False)
548 util
.idle_add(offer_resuming
)
549 elif not gpodder
.ui
.fremantle
:
550 util
.idle_add(self
.wNotebook
.set_current_page
, 0)
552 util
.idle_add(self
.clean_up_downloads
, True)
553 threading
.Thread(target
=find_partial_downloads
).start()
555 # Start the auto-update procedure
556 self
._auto
_update
_timer
_source
_id
= None
557 if self
.config
.auto_update_feeds
:
558 self
.restart_auto_update_timer()
560 # Delete old episodes if the user wishes to
561 if self
.config
.auto_remove_played_episodes
and \
562 self
.config
.episode_old_age
> 0:
563 old_episodes
= list(self
.get_expired_episodes())
564 if len(old_episodes
) > 0:
565 self
.delete_episode_list(old_episodes
, confirm
=False)
566 self
.update_podcast_list_model(set(e
.channel
.url
for e
in old_episodes
))
568 if gpodder
.ui
.fremantle
:
569 hildon
.hildon_gtk_window_set_progress_indicator(self
.main_window
, False)
570 self
.button_refresh
.set_sensitive(True)
571 self
.button_subscribe
.set_sensitive(True)
572 self
.main_window
.set_title(_('gPodder'))
573 hildon
.hildon_gtk_window_take_screenshot(self
.main_window
, True)
575 # Do the initial sync with the web service
576 util
.idle_add(self
.mygpo_client
.flush
, True)
578 # First-time users should be asked if they want to see the OPML
579 if not self
.channels
and not gpodder
.ui
.fremantle
:
580 util
.idle_add(self
.on_itemUpdate_activate
)
582 def episode_object_by_uri(self
, uri
):
583 """Get an episode object given a local or remote URI
585 This can be used to quickly access an episode object
586 when all we have is its download filename or episode
587 URL (e.g. from external D-Bus calls / signals, etc..)
589 if uri
.startswith('/'):
590 uri
= 'file://' + uri
592 prefix
= 'file://' + self
.config
.download_dir
594 if uri
.startswith(prefix
):
595 # File is on the local filesystem in the download folder
596 filename
= uri
[len(prefix
):]
597 file_parts
= [x
for x
in filename
.split(os
.sep
) if x
]
599 if len(file_parts
) == 2:
600 dir_name
, filename
= file_parts
601 channels
= [c
for c
in self
.channels
if c
.foldername
== dir_name
]
602 if len(channels
) == 1:
603 channel
= channels
[0]
604 return channel
.get_episode_by_filename(filename
)
606 # Possibly remote file - search the database for a podcast
607 channel_id
= self
.db
.get_channel_id_from_episode_url(uri
)
609 if channel_id
is not None:
610 channels
= [c
for c
in self
.channels
if c
.id == channel_id
]
611 if len(channels
) == 1:
612 channel
= channels
[0]
613 return channel
.get_episode_by_url(uri
)
617 def on_played(self
, start
, end
, total
, file_uri
):
618 """Handle the "played" signal from a media player"""
619 if start
== 0 and end
== 0 and total
== 0:
620 # Ignore bogus play event
622 elif end
< start
+ 5:
623 # Ignore "less than five seconds" segments,
624 # as they can happen with seeking, etc...
627 log('Received play action: %s (%d, %d, %d)', file_uri
, start
, end
, total
, sender
=self
)
628 episode
= self
.episode_object_by_uri(file_uri
)
630 if episode
is not None:
631 file_type
= episode
.file_type()
632 # Automatically enable D-Bus played status mode
633 if file_type
== 'audio':
634 self
.config
.audio_played_dbus
= True
635 elif file_type
== 'video':
636 self
.config
.video_played_dbus
= True
640 episode
.total_time
= total
642 # Assume the episode's total time for the action
643 total
= episode
.total_time
644 if episode
.current_position_updated
is None or \
645 now
> episode
.current_position_updated
:
646 episode
.current_position
= end
647 episode
.current_position_updated
= now
648 episode
.mark(is_played
=True)
651 self
.update_episode_list_icons([episode
.url
])
652 self
.update_podcast_list_model([episode
.channel
.url
])
654 # Submit this action to the webservice
655 self
.mygpo_client
.on_playback_full(episode
, \
658 def on_add_remove_podcasts_mygpo(self
):
659 actions
= self
.mygpo_client
.get_received_actions()
663 existing_urls
= [c
.url
for c
in self
.channels
]
665 # Columns for the episode selector window - just one...
667 ('description', None, None, _('Action')),
670 # A list of actions that have to be chosen from
673 # Actions that are ignored (already carried out)
676 for action
in actions
:
677 if action
.is_add
and action
.url
not in existing_urls
:
678 changes
.append(my
.Change(action
))
679 elif action
.is_remove
and action
.url
in existing_urls
:
680 podcast_object
= None
681 for podcast
in self
.channels
:
682 if podcast
.url
== action
.url
:
683 podcast_object
= podcast
685 changes
.append(my
.Change(action
, podcast_object
))
687 log('Ignoring action: %s', action
, sender
=self
)
688 ignored
.append(action
)
690 # Confirm all ignored changes
691 self
.mygpo_client
.confirm_received_actions(ignored
)
693 def execute_podcast_actions(selected
):
694 add_list
= [c
.action
.url
for c
in selected
if c
.action
.is_add
]
695 remove_list
= [c
.podcast
for c
in selected
if c
.action
.is_remove
]
697 # Apply the accepted changes locally
698 self
.add_podcast_list(add_list
)
699 self
.remove_podcast_list(remove_list
, confirm
=False)
701 # All selected items are now confirmed
702 self
.mygpo_client
.confirm_received_actions(c
.action
for c
in selected
)
704 # Revert the changes on the server
705 rejected
= [c
.action
for c
in changes
if c
not in selected
]
706 self
.mygpo_client
.reject_received_actions(rejected
)
709 # We're abusing the Episode Selector again ;) -- thp
710 gPodderEpisodeSelector(self
.main_window
, \
711 title
=_('Confirm changes from gpodder.net'), \
712 instructions
=_('Select the actions you want to carry out.'), \
715 size_attribute
=None, \
716 stock_ok_button
=gtk
.STOCK_APPLY
, \
717 callback
=execute_podcast_actions
, \
720 # There are some actions that need the user's attention
725 # We have no remaining actions - no selection happens
728 def rewrite_urls_mygpo(self
):
729 # Check if we have to rewrite URLs since the last add
730 rewritten_urls
= self
.mygpo_client
.get_rewritten_urls()
732 for rewritten_url
in rewritten_urls
:
733 if not rewritten_url
.new_url
:
736 for channel
in self
.channels
:
737 if channel
.url
== rewritten_url
.old_url
:
738 log('Updating URL of %s to %s', channel
, \
739 rewritten_url
.new_url
, sender
=self
)
740 channel
.url
= rewritten_url
.new_url
742 self
.channel_list_changed
= True
743 util
.idle_add(self
.update_episode_list_model
)
746 def on_send_full_subscriptions(self
):
747 # Send the full subscription list to the gpodder.net client
748 # (this will overwrite the subscription list on the server)
749 indicator
= ProgressIndicator(_('Uploading subscriptions'), \
750 _('Your subscriptions are being uploaded to the server.'), \
751 False, self
.get_dialog_parent())
754 self
.mygpo_client
.set_subscriptions([c
.url
for c
in self
.channels
])
755 util
.idle_add(self
.show_message
, _('List uploaded successfully.'))
760 message
= e
.__class
__.__name
__
761 self
.show_message(message
, \
762 _('Error while uploading'), \
764 util
.idle_add(show_error
, e
)
766 util
.idle_add(indicator
.on_finished
)
768 def on_podcast_selected(self
, treeview
, path
, column
):
770 model
= treeview
.get_model()
771 channel
= model
.get_value(model
.get_iter(path
), \
772 PodcastListModel
.C_CHANNEL
)
773 self
.active_channel
= channel
774 self
.update_episode_list_model()
775 self
.episodes_window
.channel
= self
.active_channel
776 self
.episodes_window
.show()
778 def on_button_subscribe_clicked(self
, button
):
779 self
.on_itemImportChannels_activate(button
)
781 def on_button_downloads_clicked(self
, widget
):
782 self
.downloads_window
.show()
784 def show_episode_in_download_manager(self
, episode
):
785 self
.downloads_window
.show()
786 model
= self
.treeDownloads
.get_model()
787 selection
= self
.treeDownloads
.get_selection()
788 selection
.unselect_all()
789 it
= model
.get_iter_first()
790 while it
is not None:
791 task
= model
.get_value(it
, DownloadStatusModel
.C_TASK
)
792 if task
.episode
.url
== episode
.url
:
793 selection
.select_iter(it
)
794 # FIXME: Scroll to selection in pannable area
796 it
= model
.iter_next(it
)
798 def for_each_episode_set_task_status(self
, episodes
, status
):
799 episode_urls
= set(episode
.url
for episode
in episodes
)
800 model
= self
.treeDownloads
.get_model()
801 selected_tasks
= [(gtk
.TreeRowReference(model
, row
.path
), \
802 model
.get_value(row
.iter, \
803 DownloadStatusModel
.C_TASK
)) for row
in model \
804 if model
.get_value(row
.iter, DownloadStatusModel
.C_TASK
).url \
806 self
._for
_each
_task
_set
_status
(selected_tasks
, status
)
808 def on_window_orientation_changed(self
, orientation
):
809 self
._last
_orientation
= orientation
810 if self
.preferences_dialog
is not None:
811 self
.preferences_dialog
.on_window_orientation_changed(orientation
)
813 treeview
= self
.treeChannels
814 if orientation
== Orientation
.PORTRAIT
:
815 treeview
.set_action_area_orientation(gtk
.ORIENTATION_VERTICAL
)
816 # Work around Maemo bug #4718
817 self
.button_subscribe
.set_name('HildonButton-thumb')
818 self
.button_refresh
.set_name('HildonButton-thumb')
820 treeview
.set_action_area_orientation(gtk
.ORIENTATION_HORIZONTAL
)
821 # Work around Maemo bug #4718
822 self
.button_subscribe
.set_name('HildonButton-finger')
823 self
.button_refresh
.set_name('HildonButton-finger')
825 if gpodder
.ui
.fremantle
:
826 self
.fancy_progress_bar
.relayout()
828 def on_treeview_podcasts_selection_changed(self
, selection
):
829 model
, iter = selection
.get_selected()
831 self
.active_channel
= None
832 self
.episode_list_model
.clear()
834 def on_treeview_button_pressed(self
, treeview
, event
):
835 if event
.window
!= treeview
.get_bin_window():
838 TreeViewHelper
.save_button_press_event(treeview
, event
)
840 if getattr(treeview
, TreeViewHelper
.ROLE
) == \
841 TreeViewHelper
.ROLE_PODCASTS
:
842 return self
.currently_updating
844 return event
.button
== self
.context_menu_mouse_button
and \
847 def on_treeview_podcasts_button_released(self
, treeview
, event
):
848 if event
.window
!= treeview
.get_bin_window():
852 return self
.treeview_channels_handle_gestures(treeview
, event
)
853 return self
.treeview_channels_show_context_menu(treeview
, event
)
855 def on_treeview_episodes_button_released(self
, treeview
, event
):
856 if event
.window
!= treeview
.get_bin_window():
859 if self
.config
.enable_fingerscroll
or self
.config
.maemo_enable_gestures
:
860 return self
.treeview_available_handle_gestures(treeview
, event
)
862 return self
.treeview_available_show_context_menu(treeview
, event
)
864 def on_treeview_downloads_button_released(self
, treeview
, event
):
865 if event
.window
!= treeview
.get_bin_window():
868 return self
.treeview_downloads_show_context_menu(treeview
, event
)
870 def on_entry_search_podcasts_changed(self
, editable
):
871 if self
.hbox_search_podcasts
.get_property('visible'):
872 def set_search_term(self
, text
):
873 self
.podcast_list_model
.set_search_term(text
)
874 self
._podcast
_list
_search
_timeout
= None
877 if self
._podcast
_list
_search
_timeout
is not None:
878 gobject
.source_remove(self
._podcast
_list
_search
_timeout
)
879 self
._podcast
_list
_search
_timeout
= gobject
.timeout_add(\
880 self
.LIVE_SEARCH_DELAY
, \
881 set_search_term
, self
, editable
.get_chars(0, -1))
883 def on_entry_search_podcasts_key_press(self
, editable
, event
):
884 if event
.keyval
== gtk
.keysyms
.Escape
:
885 self
.hide_podcast_search()
888 def hide_podcast_search(self
, *args
):
889 if self
._podcast
_list
_search
_timeout
is not None:
890 gobject
.source_remove(self
._podcast
_list
_search
_timeout
)
891 self
._podcast
_list
_search
_timeout
= None
892 self
.hbox_search_podcasts
.hide()
893 self
.entry_search_podcasts
.set_text('')
894 self
.podcast_list_model
.set_search_term(None)
895 self
.treeChannels
.grab_focus()
897 def show_podcast_search(self
, input_char
):
898 self
.hbox_search_podcasts
.show()
899 self
.entry_search_podcasts
.insert_text(input_char
, -1)
900 self
.entry_search_podcasts
.grab_focus()
901 self
.entry_search_podcasts
.set_position(-1)
903 def init_podcast_list_treeview(self
):
904 # Set up podcast channel tree view widget
905 if gpodder
.ui
.fremantle
:
906 if self
.config
.podcast_list_view_mode
== EpisodeListModel
.VIEW_DOWNLOADED
:
907 self
.item_view_podcasts_downloaded
.set_active(True)
908 elif self
.config
.podcast_list_view_mode
== EpisodeListModel
.VIEW_UNPLAYED
:
909 self
.item_view_podcasts_unplayed
.set_active(True)
911 self
.item_view_podcasts_all
.set_active(True)
912 self
.podcast_list_model
.set_view_mode(self
.config
.podcast_list_view_mode
)
914 iconcolumn
= gtk
.TreeViewColumn('')
915 iconcell
= gtk
.CellRendererPixbuf()
916 iconcolumn
.pack_start(iconcell
, False)
917 iconcolumn
.add_attribute(iconcell
, 'pixbuf', PodcastListModel
.C_COVER
)
918 self
.treeChannels
.append_column(iconcolumn
)
920 namecolumn
= gtk
.TreeViewColumn('')
921 namecell
= gtk
.CellRendererText()
922 namecell
.set_property('ellipsize', pango
.ELLIPSIZE_END
)
923 namecolumn
.pack_start(namecell
, True)
924 namecolumn
.add_attribute(namecell
, 'markup', PodcastListModel
.C_DESCRIPTION
)
926 if gpodder
.ui
.fremantle
:
927 countcell
= gtk
.CellRendererText()
928 from gpodder
.gtkui
.frmntl
import style
929 countcell
.set_property('font-desc', style
.get_font_desc('EmpSystemFont'))
930 countcell
.set_property('foreground-gdk', style
.get_color('SecondaryTextColor'))
931 countcell
.set_property('alignment', pango
.ALIGN_RIGHT
)
932 countcell
.set_property('xalign', 1.)
933 countcell
.set_property('xpad', 5)
934 namecolumn
.pack_start(countcell
, False)
935 namecolumn
.add_attribute(countcell
, 'text', PodcastListModel
.C_DOWNLOADS
)
936 namecolumn
.add_attribute(countcell
, 'visible', PodcastListModel
.C_DOWNLOADS
)
938 iconcell
= gtk
.CellRendererPixbuf()
939 iconcell
.set_property('xalign', 1.0)
940 namecolumn
.pack_start(iconcell
, False)
941 namecolumn
.add_attribute(iconcell
, 'pixbuf', PodcastListModel
.C_PILL
)
942 namecolumn
.add_attribute(iconcell
, 'visible', PodcastListModel
.C_PILL_VISIBLE
)
944 self
.treeChannels
.append_column(namecolumn
)
946 self
.treeChannels
.set_model(self
.podcast_list_model
.get_filtered_model())
948 # When no podcast is selected, clear the episode list model
949 selection
= self
.treeChannels
.get_selection()
950 selection
.connect('changed', self
.on_treeview_podcasts_selection_changed
)
952 # Set up type-ahead find for the podcast list
953 def on_key_press(treeview
, event
):
954 if event
.keyval
== gtk
.keysyms
.Escape
:
955 self
.hide_podcast_search()
956 elif gpodder
.ui
.fremantle
and event
.keyval
== gtk
.keysyms
.BackSpace
:
957 self
.hide_podcast_search()
958 elif event
.state
& gtk
.gdk
.CONTROL_MASK
:
959 # Don't handle type-ahead when control is pressed (so shortcuts
960 # with the Ctrl key still work, e.g. Ctrl+A, ...)
963 unicode_char_id
= gtk
.gdk
.keyval_to_unicode(event
.keyval
)
964 if unicode_char_id
== 0:
966 input_char
= unichr(unicode_char_id
)
967 self
.show_podcast_search(input_char
)
969 self
.treeChannels
.connect('key-press-event', on_key_press
)
971 # Enable separators to the podcast list to separate special podcasts
972 # from others (this is used for the "all episodes" view)
973 self
.treeChannels
.set_row_separator_func(PodcastListModel
.row_separator_func
)
975 TreeViewHelper
.set(self
.treeChannels
, TreeViewHelper
.ROLE_PODCASTS
)
977 def on_entry_search_episodes_changed(self
, editable
):
978 if self
.hbox_search_episodes
.get_property('visible'):
979 def set_search_term(self
, text
):
980 self
.episode_list_model
.set_search_term(text
)
981 self
._episode
_list
_search
_timeout
= None
984 if self
._episode
_list
_search
_timeout
is not None:
985 gobject
.source_remove(self
._episode
_list
_search
_timeout
)
986 self
._episode
_list
_search
_timeout
= gobject
.timeout_add(\
987 self
.LIVE_SEARCH_DELAY
, \
988 set_search_term
, self
, editable
.get_chars(0, -1))
990 def on_entry_search_episodes_key_press(self
, editable
, event
):
991 if event
.keyval
== gtk
.keysyms
.Escape
:
992 self
.hide_episode_search()
995 def hide_episode_search(self
, *args
):
996 if self
._episode
_list
_search
_timeout
is not None:
997 gobject
.source_remove(self
._episode
_list
_search
_timeout
)
998 self
._episode
_list
_search
_timeout
= None
999 self
.hbox_search_episodes
.hide()
1000 self
.entry_search_episodes
.set_text('')
1001 self
.episode_list_model
.set_search_term(None)
1002 self
.treeAvailable
.grab_focus()
1004 def show_episode_search(self
, input_char
):
1005 self
.hbox_search_episodes
.show()
1006 self
.entry_search_episodes
.insert_text(input_char
, -1)
1007 self
.entry_search_episodes
.grab_focus()
1008 self
.entry_search_episodes
.set_position(-1)
1010 def init_episode_list_treeview(self
):
1011 # For loading the list model
1012 self
.episode_list_model
= EpisodeListModel(self
.on_episode_list_filter_changed
)
1014 if self
.config
.episode_list_view_mode
== EpisodeListModel
.VIEW_UNDELETED
:
1015 self
.item_view_episodes_undeleted
.set_active(True)
1016 elif self
.config
.episode_list_view_mode
== EpisodeListModel
.VIEW_DOWNLOADED
:
1017 self
.item_view_episodes_downloaded
.set_active(True)
1018 elif self
.config
.episode_list_view_mode
== EpisodeListModel
.VIEW_UNPLAYED
:
1019 self
.item_view_episodes_unplayed
.set_active(True)
1021 self
.item_view_episodes_all
.set_active(True)
1023 self
.episode_list_model
.set_view_mode(self
.config
.episode_list_view_mode
)
1025 self
.treeAvailable
.set_model(self
.episode_list_model
.get_filtered_model())
1027 TreeViewHelper
.set(self
.treeAvailable
, TreeViewHelper
.ROLE_EPISODES
)
1029 iconcell
= gtk
.CellRendererPixbuf()
1030 iconcell
.set_property('stock-size', gtk
.ICON_SIZE_BUTTON
)
1031 if gpodder
.ui
.maemo
:
1032 iconcell
.set_fixed_size(50, 50)
1034 iconcell
.set_fixed_size(40, -1)
1036 namecell
= gtk
.CellRendererText()
1037 namecell
.set_property('ellipsize', pango
.ELLIPSIZE_END
)
1038 namecolumn
= gtk
.TreeViewColumn(_('Episode'))
1039 namecolumn
.pack_start(iconcell
, False)
1040 namecolumn
.add_attribute(iconcell
, 'icon-name', EpisodeListModel
.C_STATUS_ICON
)
1041 namecolumn
.pack_start(namecell
, True)
1042 namecolumn
.add_attribute(namecell
, 'markup', EpisodeListModel
.C_DESCRIPTION
)
1043 if gpodder
.ui
.fremantle
:
1044 namecolumn
.set_sizing(gtk
.TREE_VIEW_COLUMN_FIXED
)
1046 namecolumn
.set_sort_column_id(EpisodeListModel
.C_DESCRIPTION
)
1047 namecolumn
.set_sizing(gtk
.TREE_VIEW_COLUMN_AUTOSIZE
)
1048 namecolumn
.set_resizable(True)
1049 namecolumn
.set_expand(True)
1051 if gpodder
.ui
.fremantle
:
1052 from gpodder
.gtkui
.frmntl
import style
1053 timecell
= gtk
.CellRendererText()
1054 timecell
.set_property('font-desc', style
.get_font_desc('SystemFont'))
1055 timecell
.set_property('foreground-gdk', style
.get_color('SecondaryTextColor'))
1056 timecell
.set_property('alignment', pango
.ALIGN_RIGHT
)
1057 timecell
.set_property('xalign', 1.)
1058 timecell
.set_property('xpad', 5)
1059 namecolumn
.pack_start(timecell
, False)
1060 namecolumn
.add_attribute(timecell
, 'text', EpisodeListModel
.C_TIME
)
1061 namecolumn
.add_attribute(timecell
, 'visible', EpisodeListModel
.C_TIME1_VISIBLE
)
1063 # Add another cell renderer to fix a sizing issue (one renderer
1064 # only renders short text and the other one longer text to avoid
1065 # having titles of episodes unnecessarily cut off)
1066 timecell
= gtk
.CellRendererText()
1067 timecell
.set_property('font-desc', style
.get_font_desc('SystemFont'))
1068 timecell
.set_property('foreground-gdk', style
.get_color('SecondaryTextColor'))
1069 timecell
.set_property('alignment', pango
.ALIGN_RIGHT
)
1070 timecell
.set_property('xalign', 1.)
1071 timecell
.set_property('xpad', 5)
1072 namecolumn
.pack_start(timecell
, False)
1073 namecolumn
.add_attribute(timecell
, 'text', EpisodeListModel
.C_TIME
)
1074 namecolumn
.add_attribute(timecell
, 'visible', EpisodeListModel
.C_TIME2_VISIBLE
)
1076 lockcell
= gtk
.CellRendererPixbuf()
1077 lockcell
.set_property('stock-size', gtk
.ICON_SIZE_MENU
)
1078 if gpodder
.ui
.fremantle
:
1079 lockcell
.set_property('icon-name', 'general_locked')
1081 lockcell
.set_property('icon-name', 'emblem-readonly')
1083 namecolumn
.pack_start(lockcell
, False)
1084 namecolumn
.add_attribute(lockcell
, 'visible', EpisodeListModel
.C_LOCKED
)
1086 sizecell
= gtk
.CellRendererText()
1087 sizecell
.set_property('xalign', 1)
1088 sizecolumn
= gtk
.TreeViewColumn(_('Size'), sizecell
, text
=EpisodeListModel
.C_FILESIZE_TEXT
)
1089 sizecolumn
.set_sort_column_id(EpisodeListModel
.C_FILESIZE
)
1091 releasecell
= gtk
.CellRendererText()
1092 releasecolumn
= gtk
.TreeViewColumn(_('Released'), releasecell
, text
=EpisodeListModel
.C_PUBLISHED_TEXT
)
1093 releasecolumn
.set_sort_column_id(EpisodeListModel
.C_PUBLISHED
)
1095 namecolumn
.set_reorderable(True)
1096 self
.treeAvailable
.append_column(namecolumn
)
1098 if not gpodder
.ui
.maemo
:
1099 for itemcolumn
in (sizecolumn
, releasecolumn
):
1100 itemcolumn
.set_reorderable(True)
1101 self
.treeAvailable
.append_column(itemcolumn
)
1103 # Set up type-ahead find for the episode list
1104 def on_key_press(treeview
, event
):
1105 if event
.keyval
== gtk
.keysyms
.Escape
:
1106 self
.hide_episode_search()
1107 elif gpodder
.ui
.fremantle
and event
.keyval
== gtk
.keysyms
.BackSpace
:
1108 self
.hide_episode_search()
1109 elif event
.state
& gtk
.gdk
.CONTROL_MASK
:
1110 # Don't handle type-ahead when control is pressed (so shortcuts
1111 # with the Ctrl key still work, e.g. Ctrl+A, ...)
1114 unicode_char_id
= gtk
.gdk
.keyval_to_unicode(event
.keyval
)
1115 if unicode_char_id
== 0:
1117 input_char
= unichr(unicode_char_id
)
1118 self
.show_episode_search(input_char
)
1120 self
.treeAvailable
.connect('key-press-event', on_key_press
)
1122 if gpodder
.ui
.desktop
and not self
.config
.enable_fingerscroll
:
1123 self
.treeAvailable
.enable_model_drag_source(gtk
.gdk
.BUTTON1_MASK
, \
1124 (('text/uri-list', 0, 0),), gtk
.gdk
.ACTION_COPY
)
1125 def drag_data_get(tree
, context
, selection_data
, info
, timestamp
):
1126 if self
.config
.on_drag_mark_played
:
1127 for episode
in self
.get_selected_episodes():
1128 episode
.mark(is_played
=True)
1129 self
.on_selected_episodes_status_changed()
1130 uris
= ['file://'+e
.local_filename(create
=False) \
1131 for e
in self
.get_selected_episodes() \
1132 if e
.was_downloaded(and_exists
=True)]
1133 uris
.append('') # for the trailing '\r\n'
1134 selection_data
.set(selection_data
.target
, 8, '\r\n'.join(uris
))
1135 self
.treeAvailable
.connect('drag-data-get', drag_data_get
)
1137 selection
= self
.treeAvailable
.get_selection()
1138 if self
.config
.maemo_enable_gestures
or self
.config
.enable_fingerscroll
:
1139 selection
.set_mode(gtk
.SELECTION_SINGLE
)
1140 elif gpodder
.ui
.fremantle
:
1141 selection
.set_mode(gtk
.SELECTION_SINGLE
)
1143 selection
.set_mode(gtk
.SELECTION_MULTIPLE
)
1144 # Update the sensitivity of the toolbar buttons on the Desktop
1145 selection
.connect('changed', lambda s
: self
.play_or_download())
1147 if gpodder
.ui
.diablo
:
1148 # Set up the tap-and-hold context menu for podcasts
1150 menu
.append(self
.itemUpdateChannel
.create_menu_item())
1151 menu
.append(self
.itemEditChannel
.create_menu_item())
1152 menu
.append(gtk
.SeparatorMenuItem())
1153 menu
.append(self
.itemRemoveChannel
.create_menu_item())
1154 menu
.append(gtk
.SeparatorMenuItem())
1155 item
= gtk
.ImageMenuItem(_('Close this menu'))
1156 item
.set_image(gtk
.image_new_from_stock(gtk
.STOCK_CLOSE
, \
1157 gtk
.ICON_SIZE_MENU
))
1160 menu
= self
.set_finger_friendly(menu
)
1161 self
.treeChannels
.tap_and_hold_setup(menu
)
1164 def init_download_list_treeview(self
):
1165 # enable multiple selection support
1166 self
.treeDownloads
.get_selection().set_mode(gtk
.SELECTION_MULTIPLE
)
1167 self
.treeDownloads
.set_search_equal_func(TreeViewHelper
.make_search_equal_func(DownloadStatusModel
))
1169 # columns and renderers for "download progress" tab
1170 # First column: [ICON] Episodename
1171 column
= gtk
.TreeViewColumn(_('Episode'))
1173 cell
= gtk
.CellRendererPixbuf()
1174 if gpodder
.ui
.maemo
:
1175 cell
.set_fixed_size(50, 50)
1176 cell
.set_property('stock-size', gtk
.ICON_SIZE_BUTTON
)
1177 column
.pack_start(cell
, expand
=False)
1178 column
.add_attribute(cell
, 'icon-name', \
1179 DownloadStatusModel
.C_ICON_NAME
)
1181 cell
= gtk
.CellRendererText()
1182 cell
.set_property('ellipsize', pango
.ELLIPSIZE_END
)
1183 column
.pack_start(cell
, expand
=True)
1184 column
.add_attribute(cell
, 'markup', DownloadStatusModel
.C_NAME
)
1185 column
.set_sizing(gtk
.TREE_VIEW_COLUMN_AUTOSIZE
)
1186 column
.set_expand(True)
1187 self
.treeDownloads
.append_column(column
)
1189 # Second column: Progress
1190 cell
= gtk
.CellRendererProgress()
1191 cell
.set_property('yalign', .5)
1192 cell
.set_property('ypad', 6)
1193 column
= gtk
.TreeViewColumn(_('Progress'), cell
,
1194 value
=DownloadStatusModel
.C_PROGRESS
, \
1195 text
=DownloadStatusModel
.C_PROGRESS_TEXT
)
1196 column
.set_sizing(gtk
.TREE_VIEW_COLUMN_AUTOSIZE
)
1197 column
.set_expand(False)
1198 self
.treeDownloads
.append_column(column
)
1199 if gpodder
.ui
.maemo
:
1200 column
.set_property('min-width', 200)
1201 column
.set_property('max-width', 200)
1203 column
.set_property('min-width', 150)
1204 column
.set_property('max-width', 150)
1206 self
.treeDownloads
.set_model(self
.download_status_model
)
1207 TreeViewHelper
.set(self
.treeDownloads
, TreeViewHelper
.ROLE_DOWNLOADS
)
1209 def on_treeview_expose_event(self
, treeview
, event
):
1210 if event
.window
== treeview
.get_bin_window():
1211 model
= treeview
.get_model()
1212 if (model
is not None and model
.get_iter_first() is not None):
1215 role
= getattr(treeview
, TreeViewHelper
.ROLE
, None)
1219 ctx
= event
.window
.cairo_create()
1220 ctx
.rectangle(event
.area
.x
, event
.area
.y
,
1221 event
.area
.width
, event
.area
.height
)
1224 x
, y
, width
, height
, depth
= event
.window
.get_geometry()
1227 if role
== TreeViewHelper
.ROLE_EPISODES
:
1228 if self
.currently_updating
:
1229 text
= _('Loading episodes')
1230 elif self
.config
.episode_list_view_mode
!= \
1231 EpisodeListModel
.VIEW_ALL
:
1232 text
= _('No episodes in current view')
1234 text
= _('No episodes available')
1235 elif role
== TreeViewHelper
.ROLE_PODCASTS
:
1236 if self
.config
.episode_list_view_mode
!= \
1237 EpisodeListModel
.VIEW_ALL
and \
1238 self
.config
.podcast_list_hide_boring
and \
1239 len(self
.channels
) > 0:
1240 text
= _('No podcasts in this view')
1242 text
= _('No subscriptions')
1243 elif role
== TreeViewHelper
.ROLE_DOWNLOADS
:
1244 text
= _('No active downloads')
1246 raise Exception('on_treeview_expose_event: unknown role')
1248 if gpodder
.ui
.fremantle
:
1249 from gpodder
.gtkui
.frmntl
import style
1250 font_desc
= style
.get_font_desc('LargeSystemFont')
1254 draw_text_box_centered(ctx
, treeview
, width
, height
, text
, font_desc
, progress
)
1258 def enable_download_list_update(self
):
1259 if not self
.download_list_update_enabled
:
1260 self
.update_downloads_list()
1261 gobject
.timeout_add(1500, self
.update_downloads_list
)
1262 self
.download_list_update_enabled
= True
1264 def cleanup_downloads(self
):
1265 model
= self
.download_status_model
1267 all_tasks
= [(gtk
.TreeRowReference(model
, row
.path
), row
[0]) for row
in model
]
1268 changed_episode_urls
= set()
1269 for row_reference
, task
in all_tasks
:
1270 if task
.status
in (task
.DONE
, task
.CANCELLED
):
1271 model
.remove(model
.get_iter(row_reference
.get_path()))
1273 # We don't "see" this task anymore - remove it;
1274 # this is needed, so update_episode_list_icons()
1275 # below gets the correct list of "seen" tasks
1276 self
.download_tasks_seen
.remove(task
)
1277 except KeyError, key_error
:
1278 log('Cannot remove task from "seen" list: %s', task
, sender
=self
)
1279 changed_episode_urls
.add(task
.url
)
1280 # Tell the task that it has been removed (so it can clean up)
1281 task
.removed_from_list()
1283 # Tell the podcasts tab to update icons for our removed podcasts
1284 self
.update_episode_list_icons(changed_episode_urls
)
1286 # Tell the shownotes window that we have removed the episode
1287 if self
.episode_shownotes_window
is not None and \
1288 self
.episode_shownotes_window
.episode
is not None and \
1289 self
.episode_shownotes_window
.episode
.url
in changed_episode_urls
:
1290 self
.episode_shownotes_window
._download
_status
_changed
(None)
1292 # Update the downloads list one more time
1293 self
.update_downloads_list(can_call_cleanup
=False)
1295 def on_tool_downloads_toggled(self
, toolbutton
):
1296 if toolbutton
.get_active():
1297 self
.wNotebook
.set_current_page(1)
1299 self
.wNotebook
.set_current_page(0)
1301 def add_download_task_monitor(self
, monitor
):
1302 self
.download_task_monitors
.add(monitor
)
1303 model
= self
.download_status_model
1307 task
= row
[self
.download_status_model
.C_TASK
]
1308 monitor
.task_updated(task
)
1310 def remove_download_task_monitor(self
, monitor
):
1311 self
.download_task_monitors
.remove(monitor
)
1313 def update_downloads_list(self
, can_call_cleanup
=True):
1315 model
= self
.download_status_model
1317 downloading
, failed
, finished
, queued
, paused
, others
= 0, 0, 0, 0, 0, 0
1318 total_speed
, total_size
, done_size
= 0, 0, 0
1320 # Keep a list of all download tasks that we've seen
1321 download_tasks_seen
= set()
1323 # Remember the DownloadTask object for the episode that
1324 # has been opened in the episode shownotes dialog (if any)
1325 if self
.episode_shownotes_window
is not None:
1326 shownotes_episode
= self
.episode_shownotes_window
.episode
1327 shownotes_task
= None
1329 shownotes_episode
= None
1330 shownotes_task
= None
1332 # Do not go through the list of the model is not (yet) available
1336 failed_downloads
= []
1338 self
.download_status_model
.request_update(row
.iter)
1340 task
= row
[self
.download_status_model
.C_TASK
]
1341 speed
, size
, status
, progress
= task
.speed
, task
.total_size
, task
.status
, task
.progress
1343 # Let the download task monitors know of changes
1344 for monitor
in self
.download_task_monitors
:
1345 monitor
.task_updated(task
)
1348 done_size
+= size
*progress
1350 if shownotes_episode
is not None and \
1351 shownotes_episode
.url
== task
.episode
.url
:
1352 shownotes_task
= task
1354 download_tasks_seen
.add(task
)
1356 if status
== download
.DownloadTask
.DOWNLOADING
:
1358 total_speed
+= speed
1359 elif status
== download
.DownloadTask
.FAILED
:
1360 failed_downloads
.append(task
)
1362 elif status
== download
.DownloadTask
.DONE
:
1364 elif status
== download
.DownloadTask
.QUEUED
:
1366 elif status
== download
.DownloadTask
.PAUSED
:
1371 # Remember which tasks we have seen after this run
1372 self
.download_tasks_seen
= download_tasks_seen
1374 if gpodder
.ui
.desktop
:
1375 text
= [_('Downloads')]
1376 if downloading
+ failed
+ queued
> 0:
1379 s
.append(N_('%d active', '%d active', downloading
) % downloading
)
1381 s
.append(N_('%d failed', '%d failed', failed
) % failed
)
1383 s
.append(N_('%d queued', '%d queued', queued
) % queued
)
1384 text
.append(' (' + ', '.join(s
)+')')
1385 self
.labelDownloads
.set_text(''.join(text
))
1386 elif gpodder
.ui
.diablo
:
1387 sum = downloading
+ failed
+ finished
+ queued
+ paused
+ others
1389 self
.tool_downloads
.set_label(_('Downloads (%d)') % sum)
1391 self
.tool_downloads
.set_label(_('Downloads'))
1392 elif gpodder
.ui
.fremantle
:
1393 if downloading
+ queued
> 0:
1394 self
.button_downloads
.set_value(N_('%d active', '%d active', downloading
+queued
) % (downloading
+queued
))
1396 self
.button_downloads
.set_value(N_('%d failed', '%d failed', failed
) % failed
)
1398 self
.button_downloads
.set_value(N_('%d paused', '%d paused', paused
) % paused
)
1400 self
.button_downloads
.set_value(_('Idle'))
1402 title
= [self
.default_title
]
1404 # We have to update all episodes/channels for which the status has
1405 # changed. Accessing task.status_changed has the side effect of
1406 # re-setting the changed flag, so we need to get the "changed" list
1407 # of tuples first and split it into two lists afterwards
1408 changed
= [(task
.url
, task
.podcast_url
) for task
in \
1409 self
.download_tasks_seen
if task
.status_changed
]
1410 episode_urls
= [episode_url
for episode_url
, channel_url
in changed
]
1411 channel_urls
= [channel_url
for episode_url
, channel_url
in changed
]
1413 count
= downloading
+ queued
1415 title
.append(N_('downloading %d file', 'downloading %d files', count
) % count
)
1418 percentage
= 100.0*done_size
/total_size
1421 total_speed
= util
.format_filesize(total_speed
)
1422 title
[1] += ' (%d%%, %s/s)' % (percentage
, total_speed
)
1423 if self
.tray_icon
is not None:
1424 # Update the tray icon status and progress bar
1425 self
.tray_icon
.set_status(self
.tray_icon
.STATUS_DOWNLOAD_IN_PROGRESS
, title
[1])
1426 self
.tray_icon
.draw_progress_bar(percentage
/100.)
1428 if self
.tray_icon
is not None:
1429 # Update the tray icon status
1430 self
.tray_icon
.set_status()
1431 if gpodder
.ui
.desktop
:
1432 self
.downloads_finished(self
.download_tasks_seen
)
1433 if gpodder
.ui
.diablo
:
1434 hildon
.hildon_banner_show_information(self
.gPodder
, '', 'gPodder: %s' % _('All downloads finished'))
1435 log('All downloads have finished.', sender
=self
)
1436 if self
.config
.cmd_all_downloads_complete
:
1437 util
.run_external_command(self
.config
.cmd_all_downloads_complete
)
1439 if gpodder
.ui
.fremantle
and failed
:
1440 message
= '\n'.join(['%s: %s' % (str(task
), \
1441 task
.error_message
) for task
in failed_downloads
])
1442 self
.show_message(message
, _('Downloads failed'), important
=True)
1444 # Remove finished episodes
1445 if self
.config
.auto_cleanup_downloads
and can_call_cleanup
:
1446 self
.cleanup_downloads()
1448 # Stop updating the download list here
1449 self
.download_list_update_enabled
= False
1451 if not gpodder
.ui
.fremantle
:
1452 self
.gPodder
.set_title(' - '.join(title
))
1454 self
.update_episode_list_icons(episode_urls
)
1455 if self
.episode_shownotes_window
is not None:
1456 if (shownotes_task
and shownotes_task
.url
in episode_urls
) or \
1457 shownotes_task
!= self
.episode_shownotes_window
.task
:
1458 self
.episode_shownotes_window
._download
_status
_changed
(shownotes_task
)
1459 self
.episode_shownotes_window
._download
_status
_progress
()
1460 self
.play_or_download()
1462 self
.update_podcast_list_model(channel_urls
)
1464 return self
.download_list_update_enabled
1465 except Exception, e
:
1466 log('Exception happened while updating download list.', sender
=self
, traceback
=True)
1467 self
.show_message('%s\n\n%s' % (_('Please report this problem and restart gPodder:'), str(e
)), _('Unhandled exception'), important
=True)
1468 # We return False here, so the update loop won't be called again,
1469 # that's why we require the restart of gPodder in the message.
1472 def on_config_changed(self
, *args
):
1473 util
.idle_add(self
._on
_config
_changed
, *args
)
1475 def _on_config_changed(self
, name
, old_value
, new_value
):
1476 if name
== 'show_toolbar' and gpodder
.ui
.desktop
:
1477 self
.toolbar
.set_property('visible', new_value
)
1478 elif name
== 'videoplayer':
1479 self
.config
.video_played_dbus
= False
1480 elif name
== 'player':
1481 self
.config
.audio_played_dbus
= False
1482 elif name
== 'episode_list_descriptions':
1483 self
.update_episode_list_model()
1484 elif name
== 'episode_list_thumbnails':
1485 self
.update_episode_list_icons(all
=True)
1486 elif name
== 'rotation_mode':
1487 self
._fremantle
_rotation
.set_mode(new_value
)
1488 elif name
in ('auto_update_feeds', 'auto_update_frequency'):
1489 self
.restart_auto_update_timer()
1490 elif name
== 'podcast_list_view_all':
1491 # Force a update of the podcast list model
1492 self
.channel_list_changed
= True
1493 if gpodder
.ui
.fremantle
:
1494 hildon
.hildon_gtk_window_set_progress_indicator(self
.main_window
, True)
1495 while gtk
.events_pending():
1496 gtk
.main_iteration(False)
1497 self
.update_podcast_list_model()
1498 if gpodder
.ui
.fremantle
:
1499 hildon
.hildon_gtk_window_set_progress_indicator(self
.main_window
, False)
1501 def on_treeview_query_tooltip(self
, treeview
, x
, y
, keyboard_tooltip
, tooltip
):
1502 # With get_bin_window, we get the window that contains the rows without
1503 # the header. The Y coordinate of this window will be the height of the
1504 # treeview header. This is the amount we have to subtract from the
1505 # event's Y coordinate to get the coordinate to pass to get_path_at_pos
1506 (x_bin
, y_bin
) = treeview
.get_bin_window().get_position()
1509 (path
, column
, rx
, ry
) = treeview
.get_path_at_pos( x
, y
) or (None,)*4
1511 if not getattr(treeview
, TreeViewHelper
.CAN_TOOLTIP
) or x
> 50 or (column
is not None and column
!= treeview
.get_columns()[0]):
1512 setattr(treeview
, TreeViewHelper
.LAST_TOOLTIP
, None)
1515 if path
is not None:
1516 model
= treeview
.get_model()
1517 iter = model
.get_iter(path
)
1518 role
= getattr(treeview
, TreeViewHelper
.ROLE
)
1520 if role
== TreeViewHelper
.ROLE_EPISODES
:
1521 id = model
.get_value(iter, EpisodeListModel
.C_URL
)
1522 elif role
== TreeViewHelper
.ROLE_PODCASTS
:
1523 id = model
.get_value(iter, PodcastListModel
.C_URL
)
1525 last_tooltip
= getattr(treeview
, TreeViewHelper
.LAST_TOOLTIP
)
1526 if last_tooltip
is not None and last_tooltip
!= id:
1527 setattr(treeview
, TreeViewHelper
.LAST_TOOLTIP
, None)
1529 setattr(treeview
, TreeViewHelper
.LAST_TOOLTIP
, id)
1531 if role
== TreeViewHelper
.ROLE_EPISODES
:
1532 description
= model
.get_value(iter, EpisodeListModel
.C_TOOLTIP
)
1534 tooltip
.set_text(description
)
1537 elif role
== TreeViewHelper
.ROLE_PODCASTS
:
1538 channel
= model
.get_value(iter, PodcastListModel
.C_CHANNEL
)
1541 channel
.request_save_dir_size()
1542 diskspace_str
= util
.format_filesize(channel
.save_dir_size
, 0)
1543 error_str
= model
.get_value(iter, PodcastListModel
.C_ERROR
)
1545 error_str
= _('Feedparser error: %s') % saxutils
.escape(error_str
.strip())
1546 error_str
= '<span foreground="#ff0000">%s</span>' % error_str
1547 table
= gtk
.Table(rows
=3, columns
=3)
1548 table
.set_row_spacings(5)
1549 table
.set_col_spacings(5)
1550 table
.set_border_width(5)
1552 heading
= gtk
.Label()
1553 heading
.set_alignment(0, 1)
1554 heading
.set_markup('<b><big>%s</big></b>\n<small>%s</small>' % (saxutils
.escape(channel
.title
), saxutils
.escape(channel
.url
)))
1555 table
.attach(heading
, 0, 1, 0, 1)
1556 size_info
= gtk
.Label()
1557 size_info
.set_alignment(1, 1)
1558 size_info
.set_justify(gtk
.JUSTIFY_RIGHT
)
1559 size_info
.set_markup('<b>%s</b>\n<small>%s</small>' % (diskspace_str
, _('disk usage')))
1560 table
.attach(size_info
, 2, 3, 0, 1)
1562 table
.attach(gtk
.HSeparator(), 0, 3, 1, 2)
1564 if len(channel
.description
) < 500:
1565 description
= channel
.description
1567 pos
= channel
.description
.find('\n\n')
1568 if pos
== -1 or pos
> 500:
1569 description
= channel
.description
[:498]+'[...]'
1571 description
= channel
.description
[:pos
]
1573 description
= gtk
.Label(description
)
1575 description
.set_markup(error_str
)
1576 description
.set_alignment(0, 0)
1577 description
.set_line_wrap(True)
1578 table
.attach(description
, 0, 3, 2, 3)
1581 tooltip
.set_custom(table
)
1585 setattr(treeview
, TreeViewHelper
.LAST_TOOLTIP
, None)
1588 def treeview_allow_tooltips(self
, treeview
, allow
):
1589 setattr(treeview
, TreeViewHelper
.CAN_TOOLTIP
, allow
)
1591 def update_m3u_playlist_clicked(self
, widget
):
1592 if self
.active_channel
is not None:
1593 self
.active_channel
.update_m3u_playlist()
1594 self
.show_message(_('Updated M3U playlist in download folder.'), _('Updated playlist'), widget
=self
.treeChannels
)
1596 def treeview_handle_context_menu_click(self
, treeview
, event
):
1597 x
, y
= int(event
.x
), int(event
.y
)
1598 path
, column
, rx
, ry
= treeview
.get_path_at_pos(x
, y
) or (None,)*4
1600 selection
= treeview
.get_selection()
1601 model
, paths
= selection
.get_selected_rows()
1603 if path
is None or (path
not in paths
and \
1604 event
.button
== self
.context_menu_mouse_button
):
1605 # We have right-clicked, but not into the selection,
1606 # assume we don't want to operate on the selection
1609 if path
is not None and not paths
and \
1610 event
.button
== self
.context_menu_mouse_button
:
1611 # No selection or clicked outside selection;
1612 # select the single item where we clicked
1613 treeview
.grab_focus()
1614 treeview
.set_cursor(path
, column
, 0)
1618 # Unselect any remaining items (clicked elsewhere)
1619 if hasattr(treeview
, 'is_rubber_banding_active'):
1620 if not treeview
.is_rubber_banding_active():
1621 selection
.unselect_all()
1623 selection
.unselect_all()
1627 def downloads_list_get_selection(self
, model
=None, paths
=None):
1628 if model
is None and paths
is None:
1629 selection
= self
.treeDownloads
.get_selection()
1630 model
, paths
= selection
.get_selected_rows()
1632 can_queue
, can_cancel
, can_pause
, can_remove
, can_force
= (True,)*5
1633 selected_tasks
= [(gtk
.TreeRowReference(model
, path
), \
1634 model
.get_value(model
.get_iter(path
), \
1635 DownloadStatusModel
.C_TASK
)) for path
in paths
]
1637 for row_reference
, task
in selected_tasks
:
1638 if task
.status
!= download
.DownloadTask
.QUEUED
:
1640 if task
.status
not in (download
.DownloadTask
.PAUSED
, \
1641 download
.DownloadTask
.FAILED
, \
1642 download
.DownloadTask
.CANCELLED
):
1644 if task
.status
not in (download
.DownloadTask
.PAUSED
, \
1645 download
.DownloadTask
.QUEUED
, \
1646 download
.DownloadTask
.DOWNLOADING
, \
1647 download
.DownloadTask
.FAILED
):
1649 if task
.status
not in (download
.DownloadTask
.QUEUED
, \
1650 download
.DownloadTask
.DOWNLOADING
):
1652 if task
.status
not in (download
.DownloadTask
.CANCELLED
, \
1653 download
.DownloadTask
.FAILED
, \
1654 download
.DownloadTask
.DONE
):
1657 return selected_tasks
, can_queue
, can_cancel
, can_pause
, can_remove
, can_force
1659 def downloads_finished(self
, download_tasks_seen
):
1660 # FIXME: Filter all tasks that have already been reported
1661 finished_downloads
= [str(task
) for task
in download_tasks_seen
if task
.status
== task
.DONE
]
1662 failed_downloads
= [str(task
)+' ('+task
.error_message
+')' for task
in download_tasks_seen
if task
.status
== task
.FAILED
]
1664 if finished_downloads
and failed_downloads
:
1665 message
= self
.format_episode_list(finished_downloads
, 5)
1666 message
+= '\n\n<i>%s</i>\n' % _('These downloads failed:')
1667 message
+= self
.format_episode_list(failed_downloads
, 5)
1668 self
.show_message(message
, _('Downloads finished'), True, widget
=self
.labelDownloads
)
1669 elif finished_downloads
:
1670 message
= self
.format_episode_list(finished_downloads
)
1671 self
.show_message(message
, _('Downloads finished'), widget
=self
.labelDownloads
)
1672 elif failed_downloads
:
1673 message
= self
.format_episode_list(failed_downloads
)
1674 self
.show_message(message
, _('Downloads failed'), True, widget
=self
.labelDownloads
)
1676 # Open torrent files right after download (bug 1029)
1677 if self
.config
.open_torrent_after_download
:
1678 for task
in download_tasks_seen
:
1679 if task
.status
!= task
.DONE
:
1682 episode
= task
.episode
1683 if episode
.mimetype
!= 'application/x-bittorrent':
1686 self
.playback_episodes([episode
])
1689 def format_episode_list(self
, episode_list
, max_episodes
=10):
1691 Format a list of episode names for notifications
1693 Will truncate long episode names and limit the amount of
1694 episodes displayed (max_episodes=10).
1696 The episode_list parameter should be a list of strings.
1698 MAX_TITLE_LENGTH
= 100
1701 for title
in episode_list
[:min(len(episode_list
), max_episodes
)]:
1702 if len(title
) > MAX_TITLE_LENGTH
:
1703 middle
= (MAX_TITLE_LENGTH
/2)-2
1704 title
= '%s...%s' % (title
[0:middle
], title
[-middle
:])
1705 result
.append(saxutils
.escape(title
))
1708 more_episodes
= len(episode_list
) - max_episodes
1709 if more_episodes
> 0:
1710 result
.append('(...')
1711 result
.append(N_('%d more episode', '%d more episodes', more_episodes
) % more_episodes
)
1712 result
.append('...)')
1714 return (''.join(result
)).strip()
1716 def _for_each_task_set_status(self
, tasks
, status
, force_start
=False):
1717 episode_urls
= set()
1718 model
= self
.treeDownloads
.get_model()
1719 for row_reference
, task
in tasks
:
1720 if status
== download
.DownloadTask
.QUEUED
:
1721 # Only queue task when its paused/failed/cancelled (or forced)
1722 if task
.status
in (task
.PAUSED
, task
.FAILED
, task
.CANCELLED
) or force_start
:
1723 self
.download_queue_manager
.add_task(task
, force_start
)
1724 self
.enable_download_list_update()
1725 elif status
== download
.DownloadTask
.CANCELLED
:
1726 # Cancelling a download allowed when downloading/queued
1727 if task
.status
in (task
.QUEUED
, task
.DOWNLOADING
):
1728 task
.status
= status
1729 # Cancelling paused/failed downloads requires a call to .run()
1730 elif task
.status
in (task
.PAUSED
, task
.FAILED
):
1731 task
.status
= status
1732 # Call run, so the partial file gets deleted
1734 elif status
== download
.DownloadTask
.PAUSED
:
1735 # Pausing a download only when queued/downloading
1736 if task
.status
in (task
.DOWNLOADING
, task
.QUEUED
):
1737 task
.status
= status
1738 elif status
is None:
1739 # Remove the selected task - cancel downloading/queued tasks
1740 if task
.status
in (task
.QUEUED
, task
.DOWNLOADING
):
1741 task
.status
= task
.CANCELLED
1742 model
.remove(model
.get_iter(row_reference
.get_path()))
1743 # Remember the URL, so we can tell the UI to update
1745 # We don't "see" this task anymore - remove it;
1746 # this is needed, so update_episode_list_icons()
1747 # below gets the correct list of "seen" tasks
1748 self
.download_tasks_seen
.remove(task
)
1749 except KeyError, key_error
:
1750 log('Cannot remove task from "seen" list: %s', task
, sender
=self
)
1751 episode_urls
.add(task
.url
)
1752 # Tell the task that it has been removed (so it can clean up)
1753 task
.removed_from_list()
1755 # We can (hopefully) simply set the task status here
1756 task
.status
= status
1757 # Tell the podcasts tab to update icons for our removed podcasts
1758 self
.update_episode_list_icons(episode_urls
)
1759 # Update the tab title and downloads list
1760 self
.update_downloads_list()
1762 def treeview_downloads_show_context_menu(self
, treeview
, event
):
1763 model
, paths
= self
.treeview_handle_context_menu_click(treeview
, event
)
1765 if not hasattr(treeview
, 'is_rubber_banding_active'):
1768 return not treeview
.is_rubber_banding_active()
1770 if event
.button
== self
.context_menu_mouse_button
:
1771 selected_tasks
, can_queue
, can_cancel
, can_pause
, can_remove
, can_force
= \
1772 self
.downloads_list_get_selection(model
, paths
)
1774 def make_menu_item(label
, stock_id
, tasks
, status
, sensitive
, force_start
=False):
1775 # This creates a menu item for selection-wide actions
1776 item
= gtk
.ImageMenuItem(label
)
1777 item
.set_image(gtk
.image_new_from_stock(stock_id
, gtk
.ICON_SIZE_MENU
))
1778 item
.connect('activate', lambda item
: self
._for
_each
_task
_set
_status
(tasks
, status
, force_start
))
1779 item
.set_sensitive(sensitive
)
1780 return self
.set_finger_friendly(item
)
1784 item
= gtk
.ImageMenuItem(_('Episode details'))
1785 item
.set_image(gtk
.image_new_from_stock(gtk
.STOCK_INFO
, gtk
.ICON_SIZE_MENU
))
1786 if len(selected_tasks
) == 1:
1787 row_reference
, task
= selected_tasks
[0]
1788 episode
= task
.episode
1789 item
.connect('activate', lambda item
: self
.show_episode_shownotes(episode
))
1791 item
.set_sensitive(False)
1792 menu
.append(self
.set_finger_friendly(item
))
1793 menu
.append(gtk
.SeparatorMenuItem())
1795 menu
.append(make_menu_item(_('Start download now'), gtk
.STOCK_GO_DOWN
, selected_tasks
, download
.DownloadTask
.QUEUED
, True, True))
1797 menu
.append(make_menu_item(_('Download'), gtk
.STOCK_GO_DOWN
, selected_tasks
, download
.DownloadTask
.QUEUED
, can_queue
, False))
1798 menu
.append(make_menu_item(_('Cancel'), gtk
.STOCK_CANCEL
, selected_tasks
, download
.DownloadTask
.CANCELLED
, can_cancel
))
1799 menu
.append(make_menu_item(_('Pause'), gtk
.STOCK_MEDIA_PAUSE
, selected_tasks
, download
.DownloadTask
.PAUSED
, can_pause
))
1800 menu
.append(gtk
.SeparatorMenuItem())
1801 menu
.append(make_menu_item(_('Remove from list'), gtk
.STOCK_REMOVE
, selected_tasks
, None, can_remove
))
1803 if gpodder
.ui
.maemo
or self
.config
.enable_fingerscroll
:
1804 # Because we open the popup on left-click for Maemo,
1805 # we also include a non-action to close the menu
1806 menu
.append(gtk
.SeparatorMenuItem())
1807 item
= gtk
.ImageMenuItem(_('Close this menu'))
1808 item
.set_image(gtk
.image_new_from_stock(gtk
.STOCK_CLOSE
, gtk
.ICON_SIZE_MENU
))
1810 menu
.append(self
.set_finger_friendly(item
))
1813 menu
.popup(None, None, None, event
.button
, event
.time
)
1816 def treeview_channels_show_context_menu(self
, treeview
, event
):
1817 model
, paths
= self
.treeview_handle_context_menu_click(treeview
, event
)
1821 # Check for valid channel id, if there's no id then
1822 # assume that it is a proxy channel or equivalent
1823 # and cannot be operated with right click
1824 if self
.active_channel
.id is None:
1827 if event
.button
== 3:
1832 item
= gtk
.ImageMenuItem( _('Update podcast'))
1833 item
.set_image(gtk
.image_new_from_stock(gtk
.STOCK_REFRESH
, gtk
.ICON_SIZE_MENU
))
1834 item
.connect('activate', self
.on_itemUpdateChannel_activate
)
1835 item
.set_sensitive(not self
.updating_feed_cache
)
1838 menu
.append(gtk
.SeparatorMenuItem())
1840 item
= gtk
.CheckMenuItem(_('Keep episodes'))
1841 item
.set_active(self
.active_channel
.channel_is_locked
)
1842 item
.connect('activate', self
.on_channel_toggle_lock_activate
)
1843 menu
.append(self
.set_finger_friendly(item
))
1845 item
= gtk
.ImageMenuItem(_('Remove podcast'))
1846 item
.set_image(gtk
.image_new_from_stock(gtk
.STOCK_DELETE
, gtk
.ICON_SIZE_MENU
))
1847 item
.connect( 'activate', self
.on_itemRemoveChannel_activate
)
1850 if self
.config
.device_type
!= 'none':
1851 item
= gtk
.MenuItem(_('Synchronize to device'))
1852 item
.connect('activate', lambda item
: self
.on_sync_to_ipod_activate(item
, self
.active_channel
.get_downloaded_episodes()))
1855 menu
.append( gtk
.SeparatorMenuItem())
1857 item
= gtk
.ImageMenuItem(_('Podcast details'))
1858 item
.set_image(gtk
.image_new_from_stock(gtk
.STOCK_INFO
, gtk
.ICON_SIZE_MENU
))
1859 item
.connect('activate', self
.on_itemEditChannel_activate
)
1863 # Disable tooltips while we are showing the menu, so
1864 # the tooltip will not appear over the menu
1865 self
.treeview_allow_tooltips(self
.treeChannels
, False)
1866 menu
.connect('deactivate', lambda menushell
: self
.treeview_allow_tooltips(self
.treeChannels
, True))
1867 menu
.popup( None, None, None, event
.button
, event
.time
)
1871 def on_itemClose_activate(self
, widget
):
1872 if self
.tray_icon
is not None:
1873 self
.iconify_main_window()
1875 self
.on_gPodder_delete_event(widget
)
1877 def cover_file_removed(self
, channel_url
):
1879 The Cover Downloader calls this when a previously-
1880 available cover has been removed from the disk. We
1881 have to update our model to reflect this change.
1883 self
.podcast_list_model
.delete_cover_by_url(channel_url
)
1885 def cover_download_finished(self
, channel
, pixbuf
):
1887 The Cover Downloader calls this when it has finished
1888 downloading (or registering, if already downloaded)
1889 a new channel cover, which is ready for displaying.
1891 self
.podcast_list_model
.add_cover_by_channel(channel
, pixbuf
)
1893 def save_episodes_as_file(self
, episodes
):
1894 for episode
in episodes
:
1895 self
.save_episode_as_file(episode
)
1897 def save_episode_as_file(self
, episode
):
1898 PRIVATE_FOLDER_ATTRIBUTE
= '_save_episodes_as_file_folder'
1899 if episode
.was_downloaded(and_exists
=True):
1900 folder
= getattr(self
, PRIVATE_FOLDER_ATTRIBUTE
, None)
1901 copy_from
= episode
.local_filename(create
=False)
1902 assert copy_from
is not None
1903 copy_to
= episode
.sync_filename(self
.config
.custom_sync_name_enabled
, self
.config
.custom_sync_name
)
1904 (result
, folder
) = self
.show_copy_dialog(src_filename
=copy_from
, dst_filename
=copy_to
, dst_directory
=folder
)
1905 setattr(self
, PRIVATE_FOLDER_ATTRIBUTE
, folder
)
1907 def copy_episodes_bluetooth(self
, episodes
):
1908 episodes_to_copy
= [e
for e
in episodes
if e
.was_downloaded(and_exists
=True)]
1910 if gpodder
.ui
.maemo
:
1911 util
.bluetooth_send_files_maemo([e
.local_filename(create
=False) \
1912 for e
in episodes_to_copy
])
1915 def convert_and_send_thread(episode
):
1916 for episode
in episodes
:
1917 filename
= episode
.local_filename(create
=False)
1918 assert filename
is not None
1919 destfile
= os
.path
.join(tempfile
.gettempdir(), \
1920 util
.sanitize_filename(episode
.sync_filename(self
.config
.custom_sync_name_enabled
, self
.config
.custom_sync_name
)))
1921 (base
, ext
) = os
.path
.splitext(filename
)
1922 if not destfile
.endswith(ext
):
1926 shutil
.copyfile(filename
, destfile
)
1927 util
.bluetooth_send_file(destfile
)
1929 log('Cannot copy "%s" to "%s".', filename
, destfile
, sender
=self
)
1930 self
.notification(_('Error converting file.'), _('Bluetooth file transfer'), important
=True)
1932 util
.delete_file(destfile
)
1934 threading
.Thread(target
=convert_and_send_thread
, args
=[episodes_to_copy
]).start()
1936 def get_device_name(self
):
1937 if self
.config
.device_type
== 'ipod':
1939 elif self
.config
.device_type
in ('filesystem', 'mtp'):
1940 return _('MP3 player')
1942 return '(unknown device)'
1944 def _treeview_button_released(self
, treeview
, event
):
1945 xpos
, ypos
= TreeViewHelper
.get_button_press_event(treeview
)
1946 dy
= int(abs(event
.y
-ypos
))
1947 dx
= int(event
.x
-xpos
)
1949 selection
= treeview
.get_selection()
1950 path
= treeview
.get_path_at_pos(int(event
.x
), int(event
.y
))
1951 if path
is None or dy
> 30:
1952 return (False, dx
, dy
)
1954 path
, column
, x
, y
= path
1955 selection
.select_path(path
)
1956 treeview
.set_cursor(path
)
1957 treeview
.grab_focus()
1959 return (True, dx
, dy
)
1961 def treeview_channels_handle_gestures(self
, treeview
, event
):
1962 if self
.currently_updating
:
1965 selected
, dx
, dy
= self
._treeview
_button
_released
(treeview
, event
)
1968 if self
.config
.maemo_enable_gestures
:
1970 self
.on_itemUpdateChannel_activate()
1972 self
.on_itemEditChannel_activate(treeview
)
1976 def treeview_available_handle_gestures(self
, treeview
, event
):
1977 selected
, dx
, dy
= self
._treeview
_button
_released
(treeview
, event
)
1980 if self
.config
.maemo_enable_gestures
:
1982 self
.on_playback_selected_episodes(None)
1985 self
.on_shownotes_selected_episodes(None)
1988 # Pass the event to the context menu handler for treeAvailable
1989 self
.treeview_available_show_context_menu(treeview
, event
)
1993 def treeview_available_show_context_menu(self
, treeview
, event
):
1994 model
, paths
= self
.treeview_handle_context_menu_click(treeview
, event
)
1996 if not hasattr(treeview
, 'is_rubber_banding_active'):
1999 return not treeview
.is_rubber_banding_active()
2001 if event
.button
== self
.context_menu_mouse_button
:
2002 episodes
= self
.get_selected_episodes()
2003 any_locked
= any(e
.is_locked
for e
in episodes
)
2004 any_played
= any(e
.is_played
for e
in episodes
)
2005 one_is_new
= any(e
.state
== gpodder
.STATE_NORMAL
and not e
.is_played
for e
in episodes
)
2006 downloaded
= all(e
.was_downloaded(and_exists
=True) for e
in episodes
)
2007 downloading
= any(self
.episode_is_downloading(e
) for e
in episodes
)
2011 (can_play
, can_download
, can_transfer
, can_cancel
, can_delete
, open_instead_of_play
) = self
.play_or_download()
2013 if open_instead_of_play
:
2014 item
= gtk
.ImageMenuItem(gtk
.STOCK_OPEN
)
2016 item
= gtk
.ImageMenuItem(gtk
.STOCK_MEDIA_PLAY
)
2018 item
= gtk
.ImageMenuItem(_('Stream'))
2019 item
.set_image(gtk
.image_new_from_stock(gtk
.STOCK_MEDIA_PLAY
, gtk
.ICON_SIZE_MENU
))
2021 item
.set_sensitive(can_play
and not downloading
)
2022 item
.connect('activate', self
.on_playback_selected_episodes
)
2023 menu
.append(self
.set_finger_friendly(item
))
2026 item
= gtk
.ImageMenuItem(_('Download'))
2027 item
.set_image(gtk
.image_new_from_stock(gtk
.STOCK_GO_DOWN
, gtk
.ICON_SIZE_MENU
))
2028 item
.set_sensitive(can_download
)
2029 item
.connect('activate', self
.on_download_selected_episodes
)
2030 menu
.append(self
.set_finger_friendly(item
))
2032 item
= gtk
.ImageMenuItem(gtk
.STOCK_CANCEL
)
2033 item
.connect('activate', self
.on_item_cancel_download_activate
)
2034 menu
.append(self
.set_finger_friendly(item
))
2036 item
= gtk
.ImageMenuItem(gtk
.STOCK_DELETE
)
2037 item
.set_sensitive(can_delete
)
2038 item
.connect('activate', self
.on_btnDownloadedDelete_clicked
)
2039 menu
.append(self
.set_finger_friendly(item
))
2043 # Ok, this probably makes sense to only display for downloaded files
2045 menu
.append(gtk
.SeparatorMenuItem())
2046 share_item
= gtk
.MenuItem(_('Send to'))
2047 menu
.append(self
.set_finger_friendly(share_item
))
2048 share_menu
= gtk
.Menu()
2050 item
= gtk
.ImageMenuItem(_('Local folder'))
2051 item
.set_image(gtk
.image_new_from_stock(gtk
.STOCK_DIRECTORY
, gtk
.ICON_SIZE_MENU
))
2052 item
.connect('button-press-event', lambda w
, ee
: self
.save_episodes_as_file(episodes
))
2053 share_menu
.append(self
.set_finger_friendly(item
))
2054 if self
.bluetooth_available
:
2055 item
= gtk
.ImageMenuItem(_('Bluetooth device'))
2056 if gpodder
.ui
.maemo
:
2057 icon_name
= ICON('qgn_list_filesys_bluetooth')
2059 icon_name
= ICON('bluetooth')
2060 item
.set_image(gtk
.image_new_from_icon_name(icon_name
, gtk
.ICON_SIZE_MENU
))
2061 item
.connect('button-press-event', lambda w
, ee
: self
.copy_episodes_bluetooth(episodes
))
2062 share_menu
.append(self
.set_finger_friendly(item
))
2064 item
= gtk
.ImageMenuItem(self
.get_device_name())
2065 item
.set_image(gtk
.image_new_from_icon_name(ICON('multimedia-player'), gtk
.ICON_SIZE_MENU
))
2066 item
.connect('button-press-event', lambda w
, ee
: self
.on_sync_to_ipod_activate(w
, episodes
))
2067 share_menu
.append(self
.set_finger_friendly(item
))
2069 share_item
.set_submenu(share_menu
)
2071 if (downloaded
or one_is_new
or can_download
) and not downloading
:
2072 menu
.append(gtk
.SeparatorMenuItem())
2074 item
= gtk
.CheckMenuItem(_('New'))
2075 item
.set_active(True)
2076 item
.connect('activate', lambda w
: self
.mark_selected_episodes_old())
2077 menu
.append(self
.set_finger_friendly(item
))
2079 item
= gtk
.CheckMenuItem(_('New'))
2080 item
.set_active(False)
2081 item
.connect('activate', lambda w
: self
.mark_selected_episodes_new())
2082 menu
.append(self
.set_finger_friendly(item
))
2085 item
= gtk
.CheckMenuItem(_('Played'))
2086 item
.set_active(any_played
)
2087 item
.connect( 'activate', lambda w
: self
.on_item_toggle_played_activate( w
, False, not any_played
))
2088 menu
.append(self
.set_finger_friendly(item
))
2090 item
= gtk
.CheckMenuItem(_('Keep episode'))
2091 item
.set_active(any_locked
)
2092 item
.connect('activate', lambda w
: self
.on_item_toggle_lock_activate( w
, False, not any_locked
))
2093 menu
.append(self
.set_finger_friendly(item
))
2095 menu
.append(gtk
.SeparatorMenuItem())
2096 # Single item, add episode information menu item
2097 item
= gtk
.ImageMenuItem(_('Episode details'))
2098 item
.set_image(gtk
.image_new_from_stock( gtk
.STOCK_INFO
, gtk
.ICON_SIZE_MENU
))
2099 item
.connect('activate', lambda w
: self
.show_episode_shownotes(episodes
[0]))
2100 menu
.append(self
.set_finger_friendly(item
))
2102 if gpodder
.ui
.maemo
or self
.config
.enable_fingerscroll
:
2103 # Because we open the popup on left-click for Maemo,
2104 # we also include a non-action to close the menu
2105 menu
.append(gtk
.SeparatorMenuItem())
2106 item
= gtk
.ImageMenuItem(_('Close this menu'))
2107 item
.set_image(gtk
.image_new_from_stock(gtk
.STOCK_CLOSE
, gtk
.ICON_SIZE_MENU
))
2108 menu
.append(self
.set_finger_friendly(item
))
2111 # Disable tooltips while we are showing the menu, so
2112 # the tooltip will not appear over the menu
2113 self
.treeview_allow_tooltips(self
.treeAvailable
, False)
2114 menu
.connect('deactivate', lambda menushell
: self
.treeview_allow_tooltips(self
.treeAvailable
, True))
2115 menu
.popup( None, None, None, event
.button
, event
.time
)
2119 def set_title(self
, new_title
):
2120 if not gpodder
.ui
.fremantle
:
2121 self
.default_title
= new_title
2122 self
.gPodder
.set_title(new_title
)
2124 def update_episode_list_icons(self
, urls
=None, selected
=False, all
=False):
2126 Updates the status icons in the episode list.
2128 If urls is given, it should be a list of URLs
2129 of episodes that should be updated.
2131 If urls is None, set ONE OF selected, all to
2132 True (the former updates just the selected
2133 episodes and the latter updates all episodes).
2135 additional_args
= (self
.episode_is_downloading
, \
2136 self
.config
.episode_list_descriptions
and gpodder
.ui
.desktop
, \
2137 self
.config
.episode_list_thumbnails
and gpodder
.ui
.desktop
)
2139 if urls
is not None:
2140 # We have a list of URLs to walk through
2141 self
.episode_list_model
.update_by_urls(urls
, *additional_args
)
2142 elif selected
and not all
:
2143 # We should update all selected episodes
2144 selection
= self
.treeAvailable
.get_selection()
2145 model
, paths
= selection
.get_selected_rows()
2146 for path
in reversed(paths
):
2147 iter = model
.get_iter(path
)
2148 self
.episode_list_model
.update_by_filter_iter(iter, \
2150 elif all
and not selected
:
2151 # We update all (even the filter-hidden) episodes
2152 self
.episode_list_model
.update_all(*additional_args
)
2154 # Wrong/invalid call - have to specify at least one parameter
2155 raise ValueError('Invalid call to update_episode_list_icons')
2157 def episode_list_status_changed(self
, episodes
):
2158 self
.update_episode_list_icons(set(e
.url
for e
in episodes
))
2159 self
.update_podcast_list_model(set(e
.channel
.url
for e
in episodes
))
2162 def clean_up_downloads(self
, delete_partial
=False):
2163 # Clean up temporary files left behind by old gPodder versions
2164 temporary_files
= glob
.glob('%s/*/.tmp-*' % self
.config
.download_dir
)
2167 temporary_files
+= glob
.glob('%s/*/*.partial' % self
.config
.download_dir
)
2169 for tempfile
in temporary_files
:
2170 util
.delete_file(tempfile
)
2172 # Clean up empty download folders and abandoned download folders
2173 download_dirs
= glob
.glob(os
.path
.join(self
.config
.download_dir
, '*'))
2174 for ddir
in download_dirs
:
2175 if os
.path
.isdir(ddir
) and False: # FIXME not db.channel_foldername_exists(os.path.basename(ddir)):
2176 globr
= glob
.glob(os
.path
.join(ddir
, '*'))
2177 if len(globr
) == 0 or (len(globr
) == 1 and globr
[0].endswith('/cover')):
2178 log('Stale download directory found: %s', os
.path
.basename(ddir
), sender
=self
)
2179 shutil
.rmtree(ddir
, ignore_errors
=True)
2181 def streaming_possible(self
):
2182 if gpodder
.ui
.desktop
:
2183 # User has to have a media player set on the Desktop, or else we
2184 # would probably open the browser when giving a URL to xdg-open..
2185 return (self
.config
.player
and self
.config
.player
!= 'default')
2186 elif gpodder
.ui
.maemo
:
2187 # On Maemo, the default is to use the Nokia Media Player, which is
2188 # already able to deal with HTTP URLs the right way, so we
2189 # unconditionally enable streaming always on Maemo
2194 def playback_episodes_for_real(self
, episodes
):
2195 groups
= collections
.defaultdict(list)
2196 for episode
in episodes
:
2197 file_type
= episode
.file_type()
2198 if file_type
== 'video' and self
.config
.videoplayer
and \
2199 self
.config
.videoplayer
!= 'default':
2200 player
= self
.config
.videoplayer
2201 if gpodder
.ui
.diablo
:
2202 # Use the wrapper script if it's installed to crop 3GP YouTube
2203 # videos to fit the screen (looks much nicer than w/ black border)
2204 if player
== 'mplayer' and util
.find_command('gpodder-mplayer'):
2205 player
= 'gpodder-mplayer'
2206 elif gpodder
.ui
.fremantle
and player
== 'mplayer':
2207 player
= 'mplayer -fs %F'
2208 elif file_type
== 'audio' and self
.config
.player
and \
2209 self
.config
.player
!= 'default':
2210 player
= self
.config
.player
2214 if file_type
not in ('audio', 'video') or \
2215 (file_type
== 'audio' and not self
.config
.audio_played_dbus
) or \
2216 (file_type
== 'video' and not self
.config
.video_played_dbus
):
2217 # Mark episode as played in the database
2218 episode
.mark(is_played
=True)
2219 self
.mygpo_client
.on_playback([episode
])
2221 filename
= episode
.local_filename(create
=False)
2222 if filename
is None or not os
.path
.exists(filename
):
2223 filename
= episode
.url
2224 if youtube
.is_video_link(filename
):
2225 fmt_id
= self
.config
.youtube_preferred_fmt_id
2226 if gpodder
.ui
.fremantle
:
2228 filename
= youtube
.get_real_download_url(filename
, fmt_id
)
2230 # Determine the playback resume position - if the file
2231 # was played 100%, we simply start from the beginning
2232 resume_position
= episode
.current_position
2233 if resume_position
== episode
.total_time
:
2236 if gpodder
.ui
.fremantle
:
2237 self
.mafw_monitor
.set_resume_point(filename
, resume_position
)
2239 # If Panucci is configured, use D-Bus on Maemo to call it
2240 if player
== 'panucci':
2242 PANUCCI_NAME
= 'org.panucci.panucciInterface'
2243 PANUCCI_PATH
= '/panucciInterface'
2244 PANUCCI_INTF
= 'org.panucci.panucciInterface'
2245 o
= gpodder
.dbus_session_bus
.get_object(PANUCCI_NAME
, PANUCCI_PATH
)
2246 i
= dbus
.Interface(o
, PANUCCI_INTF
)
2248 def on_reply(*args
):
2251 def error_handler(filename
, err
):
2252 log('Exception in D-Bus call: %s', str(err
), \
2255 # Fallback: use the command line client
2256 for command
in util
.format_desktop_command('panucci', \
2258 log('Executing: %s', repr(command
), sender
=self
)
2259 subprocess
.Popen(command
)
2261 on_error
= lambda err
: error_handler(filename
, err
)
2263 # This method only exists in Panucci > 0.9 ('new Panucci')
2264 i
.playback_from(filename
, resume_position
, \
2265 reply_handler
=on_reply
, error_handler
=on_error
)
2267 continue # This file was handled by the D-Bus call
2268 except Exception, e
:
2269 log('Error calling Panucci using D-Bus', sender
=self
, traceback
=True)
2270 elif player
== 'MediaBox' and gpodder
.ui
.maemo
:
2272 MEDIABOX_NAME
= 'de.pycage.mediabox'
2273 MEDIABOX_PATH
= '/de/pycage/mediabox/control'
2274 MEDIABOX_INTF
= 'de.pycage.mediabox.control'
2275 o
= gpodder
.dbus_session_bus
.get_object(MEDIABOX_NAME
, MEDIABOX_PATH
)
2276 i
= dbus
.Interface(o
, MEDIABOX_INTF
)
2278 def on_reply(*args
):
2282 log('Exception in D-Bus call: %s', str(err
), \
2285 i
.load(filename
, '%s/x-unknown' % file_type
, \
2286 reply_handler
=on_reply
, error_handler
=on_error
)
2288 continue # This file was handled by the D-Bus call
2289 except Exception, e
:
2290 log('Error calling MediaBox using D-Bus', sender
=self
, traceback
=True)
2292 groups
[player
].append(filename
)
2294 # Open episodes with system default player
2295 if 'default' in groups
:
2296 if gpodder
.ui
.maemo
and len(groups
['default']) > 1:
2297 # The Nokia Media Player app does not support receiving multiple
2298 # file names via D-Bus, so we simply place all file names into a
2299 # temporary M3U playlist and open that with the Media Player.
2300 m3u_filename
= os
.path
.join(gpodder
.home
, 'gpodder_open_with.m3u')
2301 util
.write_m3u_playlist(m3u_filename
, groups
['default'], extm3u
=False)
2302 util
.gui_open(m3u_filename
)
2304 for filename
in groups
['default']:
2305 log('Opening with system default: %s', filename
, sender
=self
)
2306 util
.gui_open(filename
)
2307 del groups
['default']
2308 elif gpodder
.ui
.maemo
and groups
:
2309 # When on Maemo and not opening with default, show a notification
2310 # (no startup notification for Panucci / MPlayer yet...)
2311 if len(episodes
) == 1:
2312 text
= _('Opening %s') % episodes
[0].title
2314 count
= len(episodes
)
2315 text
= N_('Opening %d episode', 'Opening %d episodes', count
) % count
2317 banner
= hildon
.hildon_banner_show_animation(self
.gPodder
, '', text
)
2319 def destroy_banner_later(banner
):
2322 gobject
.timeout_add(5000, destroy_banner_later
, banner
)
2324 # For each type now, go and create play commands
2325 for group
in groups
:
2326 for command
in util
.format_desktop_command(group
, groups
[group
]):
2327 log('Executing: %s', repr(command
), sender
=self
)
2328 subprocess
.Popen(command
)
2330 # Persist episode status changes to the database
2333 # Flush updated episode status
2334 self
.mygpo_client
.flush()
2336 def playback_episodes(self
, episodes
):
2337 # We need to create a list, because we run through it more than once
2338 episodes
= list(PodcastEpisode
.sort_by_pubdate(e
for e
in episodes
if \
2339 e
.was_downloaded(and_exists
=True) or self
.streaming_possible()))
2342 self
.playback_episodes_for_real(episodes
)
2343 except Exception, e
:
2344 log('Error in playback!', sender
=self
, traceback
=True)
2345 if gpodder
.ui
.desktop
:
2346 self
.show_message(_('Please check your media player settings in the preferences dialog.'), \
2347 _('Error opening player'), widget
=self
.toolPreferences
)
2349 self
.show_message(_('Please check your media player settings in the preferences dialog.'))
2351 channel_urls
= set()
2352 episode_urls
= set()
2353 for episode
in episodes
:
2354 channel_urls
.add(episode
.channel
.url
)
2355 episode_urls
.add(episode
.url
)
2356 self
.update_episode_list_icons(episode_urls
)
2357 self
.update_podcast_list_model(channel_urls
)
2359 def play_or_download(self
):
2360 if not gpodder
.ui
.fremantle
:
2361 if self
.wNotebook
.get_current_page() > 0:
2362 if gpodder
.ui
.desktop
:
2363 self
.toolCancel
.set_sensitive(True)
2366 if self
.currently_updating
:
2367 return (False, False, False, False, False, False)
2369 ( can_play
, can_download
, can_transfer
, can_cancel
, can_delete
) = (False,)*5
2370 ( is_played
, is_locked
) = (False,)*2
2372 open_instead_of_play
= False
2374 selection
= self
.treeAvailable
.get_selection()
2375 if selection
.count_selected_rows() > 0:
2376 (model
, paths
) = selection
.get_selected_rows()
2380 episode
= model
.get_value(model
.get_iter(path
), EpisodeListModel
.C_EPISODE
)
2381 except TypeError, te
:
2382 log('Invalid episode at path %s', str(path
), sender
=self
)
2385 if episode
.file_type() not in ('audio', 'video'):
2386 open_instead_of_play
= True
2388 if episode
.was_downloaded():
2389 can_play
= episode
.was_downloaded(and_exists
=True)
2390 is_played
= episode
.is_played
2391 is_locked
= episode
.is_locked
2395 if self
.episode_is_downloading(episode
):
2400 can_download
= can_download
and not can_cancel
2401 can_play
= self
.streaming_possible() or (can_play
and not can_cancel
and not can_download
)
2402 can_transfer
= can_play
and self
.config
.device_type
!= 'none' and not can_cancel
and not can_download
and not open_instead_of_play
2403 can_delete
= not can_cancel
2405 if gpodder
.ui
.desktop
:
2406 if open_instead_of_play
:
2407 self
.toolPlay
.set_stock_id(gtk
.STOCK_OPEN
)
2409 self
.toolPlay
.set_stock_id(gtk
.STOCK_MEDIA_PLAY
)
2410 self
.toolPlay
.set_sensitive( can_play
)
2411 self
.toolDownload
.set_sensitive( can_download
)
2412 self
.toolTransfer
.set_sensitive( can_transfer
)
2413 self
.toolCancel
.set_sensitive( can_cancel
)
2415 if not gpodder
.ui
.fremantle
:
2416 self
.item_cancel_download
.set_sensitive(can_cancel
)
2417 self
.itemDownloadSelected
.set_sensitive(can_download
)
2418 self
.itemOpenSelected
.set_sensitive(can_play
)
2419 self
.itemPlaySelected
.set_sensitive(can_play
)
2420 self
.itemDeleteSelected
.set_sensitive(can_delete
)
2421 self
.item_toggle_played
.set_sensitive(can_play
)
2422 self
.item_toggle_lock
.set_sensitive(can_play
)
2423 self
.itemOpenSelected
.set_visible(open_instead_of_play
)
2424 self
.itemPlaySelected
.set_visible(not open_instead_of_play
)
2426 return (can_play
, can_download
, can_transfer
, can_cancel
, can_delete
, open_instead_of_play
)
2428 def on_cbMaxDownloads_toggled(self
, widget
, *args
):
2429 self
.spinMaxDownloads
.set_sensitive(self
.cbMaxDownloads
.get_active())
2431 def on_cbLimitDownloads_toggled(self
, widget
, *args
):
2432 self
.spinLimitDownloads
.set_sensitive(self
.cbLimitDownloads
.get_active())
2434 def episode_new_status_changed(self
, urls
):
2435 self
.update_podcast_list_model()
2436 self
.update_episode_list_icons(urls
)
2438 def update_podcast_list_model(self
, urls
=None, selected
=False, select_url
=None):
2439 """Update the podcast list treeview model
2441 If urls is given, it should list the URLs of each
2442 podcast that has to be updated in the list.
2444 If selected is True, only update the model contents
2445 for the currently-selected podcast - nothing more.
2447 The caller can optionally specify "select_url",
2448 which is the URL of the podcast that is to be
2449 selected in the list after the update is complete.
2450 This only works if the podcast list has to be
2451 reloaded; i.e. something has been added or removed
2452 since the last update of the podcast list).
2454 selection
= self
.treeChannels
.get_selection()
2455 model
, iter = selection
.get_selected()
2457 if self
.config
.podcast_list_view_all
and not self
.channel_list_changed
:
2458 # Update "all episodes" view in any case (if enabled)
2459 self
.podcast_list_model
.update_first_row()
2462 # very cheap! only update selected channel
2463 if iter is not None:
2464 # If we have selected the "all episodes" view, we have
2465 # to update all channels for selected episodes:
2466 if self
.config
.podcast_list_view_all
and \
2467 self
.podcast_list_model
.iter_is_first_row(iter):
2468 urls
= self
.get_podcast_urls_from_selected_episodes()
2469 self
.podcast_list_model
.update_by_urls(urls
)
2471 # Otherwise just update the selected row (a podcast)
2472 self
.podcast_list_model
.update_by_filter_iter(iter)
2473 elif not self
.channel_list_changed
:
2474 # we can keep the model, but have to update some
2476 # still cheaper than reloading the whole list
2477 self
.podcast_list_model
.update_all()
2479 # ok, we got a bunch of urls to update
2480 self
.podcast_list_model
.update_by_urls(urls
)
2482 if model
and iter and select_url
is None:
2483 # Get the URL of the currently-selected podcast
2484 select_url
= model
.get_value(iter, PodcastListModel
.C_URL
)
2486 # Update the podcast list model with new channels
2487 self
.podcast_list_model
.set_channels(self
.db
, self
.config
, self
.channels
)
2490 selected_iter
= model
.get_iter_first()
2491 # Find the previously-selected URL in the new
2492 # model if we have an URL (else select first)
2493 if select_url
is not None:
2494 pos
= model
.get_iter_first()
2495 while pos
is not None:
2496 url
= model
.get_value(pos
, PodcastListModel
.C_URL
)
2497 if url
== select_url
:
2500 pos
= model
.iter_next(pos
)
2502 if not gpodder
.ui
.fremantle
:
2503 if selected_iter
is not None:
2504 selection
.select_iter(selected_iter
)
2505 self
.on_treeChannels_cursor_changed(self
.treeChannels
)
2507 log('Cannot select podcast in list', traceback
=True, sender
=self
)
2508 self
.channel_list_changed
= False
2510 def episode_is_downloading(self
, episode
):
2511 """Returns True if the given episode is being downloaded at the moment"""
2515 return episode
.url
in (task
.url
for task
in self
.download_tasks_seen
if task
.status
in (task
.DOWNLOADING
, task
.QUEUED
, task
.PAUSED
))
2517 def on_episode_list_filter_changed(self
, has_episodes
):
2518 if gpodder
.ui
.fremantle
:
2520 self
.episodes_window
.empty_label
.hide()
2521 self
.episodes_window
.pannablearea
.show()
2523 if self
.config
.episode_list_view_mode
!= \
2524 EpisodeListModel
.VIEW_ALL
:
2525 text
= _('No episodes in current view')
2527 text
= _('No episodes available')
2528 self
.episodes_window
.empty_label
.set_text(text
)
2529 self
.episodes_window
.pannablearea
.hide()
2530 self
.episodes_window
.empty_label
.show()
2532 def update_episode_list_model(self
):
2533 if self
.channels
and self
.active_channel
is not None:
2534 if gpodder
.ui
.fremantle
:
2535 hildon
.hildon_gtk_window_set_progress_indicator(self
.episodes_window
.main_window
, True)
2537 self
.currently_updating
= True
2538 self
.episode_list_model
.clear()
2539 if gpodder
.ui
.fremantle
:
2540 self
.episodes_window
.pannablearea
.hide()
2541 self
.episodes_window
.empty_label
.set_text(_('Loading episodes'))
2542 self
.episodes_window
.empty_label
.show()
2545 additional_args
= (self
.episode_is_downloading
, \
2546 self
.config
.episode_list_descriptions
and gpodder
.ui
.desktop
, \
2547 self
.config
.episode_list_thumbnails
and gpodder
.ui
.desktop
)
2548 self
.episode_list_model
.replace_from_channel(self
.active_channel
, *additional_args
)
2550 self
.treeAvailable
.get_selection().unselect_all()
2551 self
.treeAvailable
.scroll_to_point(0, 0)
2553 self
.currently_updating
= False
2554 self
.play_or_download()
2556 if gpodder
.ui
.fremantle
:
2557 hildon
.hildon_gtk_window_set_progress_indicator(\
2558 self
.episodes_window
.main_window
, False)
2560 util
.idle_add(update
)
2562 self
.episode_list_model
.clear()
2564 @dbus.service
.method(gpodder
.dbus_interface
)
2565 def offer_new_episodes(self
, channels
=None):
2566 if gpodder
.ui
.fremantle
:
2567 # Assume that when this function is called that the
2568 # notification is not shown anymore (Maemo bug 11345)
2569 self
._fremantle
_notification
_visible
= False
2571 new_episodes
= self
.get_new_episodes(channels
)
2573 self
.new_episodes_show(new_episodes
)
2577 def add_podcast_list(self
, urls
, auth_tokens
=None):
2578 """Subscribe to a list of podcast given their URLs
2580 If auth_tokens is given, it should be a dictionary
2581 mapping URLs to (username, password) tuples."""
2583 if auth_tokens
is None:
2586 # Sort and split the URL list into five buckets
2587 queued
, failed
, existing
, worked
, authreq
= [], [], [], [], []
2588 for input_url
in urls
:
2589 url
= util
.normalize_feed_url(input_url
)
2591 # Fail this one because the URL is not valid
2592 failed
.append(input_url
)
2593 elif self
.podcast_list_model
.get_filter_path_from_url(url
) is not None:
2594 # A podcast already exists in the list for this URL
2595 existing
.append(url
)
2597 # This URL has survived the first round - queue for add
2599 if url
!= input_url
and input_url
in auth_tokens
:
2600 auth_tokens
[url
] = auth_tokens
[input_url
]
2605 progress
= ProgressIndicator(_('Adding podcasts'), \
2606 _('Please wait while episode information is downloaded.'), \
2607 parent
=self
.get_dialog_parent())
2609 def on_after_update():
2610 progress
.on_finished()
2611 # Report already-existing subscriptions to the user
2613 title
= _('Existing subscriptions skipped')
2614 message
= _('You are already subscribed to these podcasts:') \
2615 + '\n\n' + '\n'.join(saxutils
.escape(url
) for url
in existing
)
2616 self
.show_message(message
, title
, widget
=self
.treeChannels
)
2618 # Report subscriptions that require authentication
2622 title
= _('Podcast requires authentication')
2623 message
= _('Please login to %s:') % (saxutils
.escape(url
),)
2624 success
, auth_tokens
= self
.show_login_dialog(title
, message
)
2626 retry_podcasts
[url
] = auth_tokens
2628 # Stop asking the user for more login data
2631 error_messages
[url
] = _('Authentication failed')
2635 # If we have authentication data to retry, do so here
2637 self
.add_podcast_list(retry_podcasts
.keys(), retry_podcasts
)
2639 # Report website redirections
2640 for url
in redirections
:
2641 title
= _('Website redirection detected')
2642 message
= _('The URL %(url)s redirects to %(target)s.') \
2643 + '\n\n' + _('Do you want to visit the website now?')
2644 message
= message
% {'url': url
, 'target': redirections
[url
]}
2645 if self
.show_confirmation(message
, title
):
2646 util
.open_website(url
)
2650 # Report failed subscriptions to the user
2652 title
= _('Could not add some podcasts')
2653 message
= _('Some podcasts could not be added to your list:') \
2654 + '\n\n' + '\n'.join(saxutils
.escape('%s: %s' % (url
, \
2655 error_messages
.get(url
, _('Unknown')))) for url
in failed
)
2656 self
.show_message(message
, title
, important
=True)
2658 # Upload subscription changes to gpodder.net
2659 self
.mygpo_client
.on_subscribe(worked
)
2661 # If at least one podcast has been added, save and update all
2662 if self
.channel_list_changed
:
2663 # Fix URLs if mygpo has rewritten them
2664 self
.rewrite_urls_mygpo()
2666 self
.save_channels_opml()
2668 # If only one podcast was added, select it after the update
2669 if len(worked
) == 1:
2674 # Update the list of subscribed podcasts
2675 self
.update_feed_cache(force_update
=False, select_url_afterwards
=url
)
2676 self
.update_podcasts_tab()
2678 # Offer to download new episodes
2680 for podcast
in self
.channels
:
2681 if podcast
.url
in worked
:
2682 episodes
.extend(podcast
.get_all_episodes())
2685 episodes
= list(PodcastEpisode
.sort_by_pubdate(episodes
, \
2687 self
.new_episodes_show(episodes
, \
2688 selected
=[e
.check_is_new() for e
in episodes
])
2692 # After the initial sorting and splitting, try all queued podcasts
2693 length
= len(queued
)
2694 for index
, url
in enumerate(queued
):
2695 progress
.on_progress(float(index
)/float(length
))
2696 progress
.on_message(url
)
2697 log('QUEUE RUNNER: %s', url
, sender
=self
)
2699 # The URL is valid and does not exist already - subscribe!
2700 channel
= PodcastChannel
.load(self
.db
, url
=url
, create
=True, \
2701 authentication_tokens
=auth_tokens
.get(url
, None), \
2702 max_episodes
=self
.config
.max_episodes_per_feed
, \
2703 download_dir
=self
.config
.download_dir
, \
2704 allow_empty_feeds
=self
.config
.allow_empty_feeds
, \
2705 mimetype_prefs
=self
.config
.mimetype_prefs
)
2708 username
, password
= util
.username_password_from_url(url
)
2709 except ValueError, ve
:
2710 username
, password
= (None, None)
2712 if username
is not None and channel
.username
is None and \
2713 password
is not None and channel
.password
is None:
2714 channel
.username
= username
2715 channel
.password
= password
2718 self
._update
_cover
(channel
)
2719 except feedcore
.AuthenticationRequired
:
2720 if url
in auth_tokens
:
2721 # Fail for wrong authentication data
2722 error_messages
[url
] = _('Authentication failed')
2725 # Queue for login dialog later
2728 except feedcore
.WifiLogin
, error
:
2729 redirections
[url
] = error
.data
2731 error_messages
[url
] = _('Redirection detected')
2733 except Exception, e
:
2734 log('Subscription error: %s', e
, traceback
=True, sender
=self
)
2735 error_messages
[url
] = str(e
)
2739 assert channel
is not None
2740 worked
.append(channel
.url
)
2741 self
.channels
.append(channel
)
2742 self
.channel_list_changed
= True
2743 util
.idle_add(on_after_update
)
2744 threading
.Thread(target
=thread_proc
).start()
2746 def save_channels_opml(self
):
2747 exporter
= opml
.Exporter(gpodder
.subscription_file
)
2748 return exporter
.write(self
.channels
)
2750 def find_episode(self
, podcast_url
, episode_url
):
2751 """Find an episode given its podcast and episode URL
2753 The function will return a PodcastEpisode object if
2754 the episode is found, or None if it's not found.
2756 for podcast
in self
.channels
:
2757 if podcast_url
== podcast
.url
:
2758 for episode
in podcast
.get_all_episodes():
2759 if episode_url
== episode
.url
:
2764 def process_received_episode_actions(self
, updated_urls
):
2765 """Process/merge episode actions from gpodder.net
2767 This function will merge all changes received from
2768 the server to the local database and update the
2769 status of the affected episodes as necessary.
2771 indicator
= ProgressIndicator(_('Merging episode actions'), \
2772 _('Episode actions from gpodder.net are merged.'), \
2773 False, self
.get_dialog_parent())
2775 for idx
, action
in enumerate(self
.mygpo_client
.get_episode_actions(updated_urls
)):
2776 if action
.action
== 'play':
2777 episode
= self
.find_episode(action
.podcast_url
, \
2780 if episode
is not None:
2781 log('Play action for %s', episode
.url
, sender
=self
)
2782 episode
.mark(is_played
=True)
2784 if action
.timestamp
> episode
.current_position_updated
and \
2785 action
.position
is not None:
2786 log('Updating position for %s', episode
.url
, sender
=self
)
2787 episode
.current_position
= action
.position
2788 episode
.current_position_updated
= action
.timestamp
2791 log('Updating total time for %s', episode
.url
, sender
=self
)
2792 episode
.total_time
= action
.total
2795 elif action
.action
== 'delete':
2796 episode
= self
.find_episode(action
.podcast_url
, \
2799 if episode
is not None:
2800 if not episode
.was_downloaded(and_exists
=True):
2801 # Set the episode to a "deleted" state
2802 log('Marking as deleted: %s', episode
.url
, sender
=self
)
2803 episode
.delete_from_disk()
2806 indicator
.on_message(N_('%d action processed', '%d actions processed', idx
) % idx
)
2807 gtk
.main_iteration(False)
2809 indicator
.on_finished()
2813 def update_feed_cache_finish_callback(self
, updated_urls
=None, select_url_afterwards
=None):
2815 self
.updating_feed_cache
= False
2817 self
.channels
= PodcastChannel
.load_from_db(self
.db
, self
.config
.download_dir
)
2819 # Process received episode actions for all updated URLs
2820 self
.process_received_episode_actions(updated_urls
)
2822 self
.channel_list_changed
= True
2823 self
.update_podcast_list_model(select_url
=select_url_afterwards
)
2825 # Only search for new episodes in podcasts that have been
2826 # updated, not in other podcasts (for single-feed updates)
2827 episodes
= self
.get_new_episodes([c
for c
in self
.channels
if c
.url
in updated_urls
])
2829 if gpodder
.ui
.fremantle
:
2830 self
.fancy_progress_bar
.hide()
2831 self
.button_subscribe
.set_sensitive(True)
2832 self
.button_refresh
.set_sensitive(True)
2833 hildon
.hildon_gtk_window_set_progress_indicator(self
.main_window
, False)
2834 hildon
.hildon_gtk_window_set_progress_indicator(self
.episodes_window
.main_window
, False)
2835 self
.update_podcasts_tab()
2836 self
.update_episode_list_model()
2837 if self
.feed_cache_update_cancelled
:
2840 def application_in_foreground():
2842 return any(w
.get_property('is-topmost') for w
in hildon
.WindowStack
.get_default().get_windows())
2843 except Exception, e
:
2844 log('Could not determine is-topmost', traceback
=True)
2845 # When in doubt, assume not in foreground
2849 if self
.config
.auto_download
== 'quiet' and not self
.config
.auto_update_feeds
:
2850 # New episodes found, but we should do nothing
2851 self
.show_message(_('New episodes are available.'))
2852 elif self
.config
.auto_download
== 'always':
2853 count
= len(episodes
)
2854 title
= N_('Downloading %d new episode.', 'Downloading %d new episodes.', count
) % count
2855 self
.show_message(title
)
2856 self
.download_episode_list(episodes
)
2857 elif self
.config
.auto_download
== 'queue':
2858 self
.show_message(_('New episodes have been added to the download list.'))
2859 self
.download_episode_list_paused(episodes
)
2860 elif application_in_foreground():
2861 if not self
._fremantle
_notification
_visible
:
2862 self
.new_episodes_show(episodes
)
2863 elif not self
._fremantle
_notification
_visible
:
2866 pynotify
.init('gPodder')
2867 n
= pynotify
.Notification('gPodder', _('New episodes available'), 'gpodder')
2868 n
.set_urgency(pynotify
.URGENCY_CRITICAL
)
2869 n
.set_hint('dbus-callback-default', ' '.join([
2870 gpodder
.dbus_bus_name
,
2871 gpodder
.dbus_gui_object_path
,
2872 gpodder
.dbus_interface
,
2873 'offer_new_episodes',
2875 n
.set_category('gpodder-new-episodes')
2877 self
._fremantle
_notification
_visible
= True
2878 except Exception, e
:
2879 log('Error: %s', str(e
), sender
=self
, traceback
=True)
2880 self
.new_episodes_show(episodes
)
2881 self
._fremantle
_notification
_visible
= False
2882 elif not self
.config
.auto_update_feeds
:
2883 self
.show_message(_('No new episodes. Please check for new episodes later.'))
2887 self
.tray_icon
.set_status()
2889 if self
.feed_cache_update_cancelled
:
2890 # The user decided to abort the feed update
2891 self
.show_update_feeds_buttons()
2893 # Nothing new here - but inform the user
2894 self
.pbFeedUpdate
.set_fraction(1.0)
2895 self
.pbFeedUpdate
.set_text(_('No new episodes'))
2896 self
.feed_cache_update_cancelled
= True
2897 self
.btnCancelFeedUpdate
.show()
2898 self
.btnCancelFeedUpdate
.set_sensitive(True)
2899 self
.itemUpdate
.set_sensitive(True)
2900 if gpodder
.ui
.maemo
:
2901 # btnCancelFeedUpdate is a ToolButton on Maemo
2902 self
.btnCancelFeedUpdate
.set_stock_id(gtk
.STOCK_APPLY
)
2904 # btnCancelFeedUpdate is a normal gtk.Button
2905 self
.btnCancelFeedUpdate
.set_image(gtk
.image_new_from_stock(gtk
.STOCK_APPLY
, gtk
.ICON_SIZE_BUTTON
))
2907 count
= len(episodes
)
2908 # New episodes are available
2909 self
.pbFeedUpdate
.set_fraction(1.0)
2910 # Are we minimized and should we auto download?
2911 if (self
.is_iconified() and (self
.config
.auto_download
== 'minimized')) or (self
.config
.auto_download
== 'always'):
2912 self
.download_episode_list(episodes
)
2913 title
= N_('Downloading %d new episode.', 'Downloading %d new episodes.', count
) % count
2914 self
.show_message(title
, _('New episodes available'), widget
=self
.labelDownloads
)
2915 self
.show_update_feeds_buttons()
2916 elif self
.config
.auto_download
== 'queue':
2917 self
.download_episode_list_paused(episodes
)
2918 title
= N_('%d new episode added to download list.', '%d new episodes added to download list.', count
) % count
2919 self
.show_message(title
, _('New episodes available'), widget
=self
.labelDownloads
)
2920 self
.show_update_feeds_buttons()
2922 self
.show_update_feeds_buttons()
2923 # New episodes are available and we are not minimized
2924 if not self
.config
.do_not_show_new_episodes_dialog
:
2925 self
.new_episodes_show(episodes
, notification
=True)
2927 message
= N_('%d new episode available', '%d new episodes available', count
) % count
2928 self
.pbFeedUpdate
.set_text(message
)
2930 def _update_cover(self
, channel
):
2931 if channel
is not None and not os
.path
.exists(channel
.cover_file
) and channel
.image
:
2932 self
.cover_downloader
.request_cover(channel
)
2934 def update_feed_cache_proc(self
, channels
, select_url_afterwards
):
2935 total
= len(channels
)
2937 for updated
, channel
in enumerate(channels
):
2938 if not self
.feed_cache_update_cancelled
:
2940 channel
.update(max_episodes
=self
.config
.max_episodes_per_feed
, \
2941 mimetype_prefs
=self
.config
.mimetype_prefs
)
2942 self
._update
_cover
(channel
)
2943 except Exception, e
:
2944 d
= {'url': saxutils
.escape(channel
.url
), 'message': saxutils
.escape(str(e
))}
2946 message
= _('Error while updating %(url)s: %(message)s')
2948 message
= _('The feed at %(url)s could not be updated.')
2949 self
.notification(message
% d
, _('Error while updating feed'), widget
=self
.treeChannels
)
2950 log('Error: %s', str(e
), sender
=self
, traceback
=True)
2952 if self
.feed_cache_update_cancelled
:
2955 # By the time we get here the update may have already been cancelled
2956 if not self
.feed_cache_update_cancelled
:
2957 def update_progress():
2958 d
= {'podcast': channel
.title
, 'position': updated
+1, 'total': total
}
2959 progression
= _('Updated %(podcast)s (%(position)d/%(total)d)') % d
2960 self
.pbFeedUpdate
.set_text(progression
)
2962 self
.tray_icon
.set_status(self
.tray_icon
.STATUS_UPDATING_FEED_CACHE
, progression
)
2963 self
.pbFeedUpdate
.set_fraction(float(updated
+1)/float(total
))
2964 util
.idle_add(update_progress
)
2966 updated_urls
= [c
.url
for c
in channels
]
2967 util
.idle_add(self
.update_feed_cache_finish_callback
, updated_urls
, select_url_afterwards
)
2969 def show_update_feeds_buttons(self
):
2970 # Make sure that the buttons for updating feeds
2971 # appear - this should happen after a feed update
2972 if gpodder
.ui
.maemo
:
2973 self
.btnUpdateSelectedFeed
.show()
2974 self
.toolFeedUpdateProgress
.hide()
2975 self
.btnCancelFeedUpdate
.hide()
2976 self
.btnCancelFeedUpdate
.set_is_important(False)
2977 self
.btnCancelFeedUpdate
.set_stock_id(gtk
.STOCK_CLOSE
)
2978 self
.toolbarSpacer
.set_expand(True)
2979 self
.toolbarSpacer
.set_draw(False)
2981 self
.hboxUpdateFeeds
.hide()
2982 self
.btnUpdateFeeds
.show()
2983 self
.itemUpdate
.set_sensitive(True)
2984 self
.itemUpdateChannel
.set_sensitive(True)
2986 def on_btnCancelFeedUpdate_clicked(self
, widget
):
2987 if not self
.feed_cache_update_cancelled
:
2988 self
.pbFeedUpdate
.set_text(_('Cancelling...'))
2989 self
.feed_cache_update_cancelled
= True
2990 if not gpodder
.ui
.fremantle
:
2991 self
.btnCancelFeedUpdate
.set_sensitive(False)
2992 elif not gpodder
.ui
.fremantle
:
2993 self
.show_update_feeds_buttons()
2995 def update_feed_cache(self
, channels
=None, force_update
=True, select_url_afterwards
=None):
2996 if self
.updating_feed_cache
:
2997 if gpodder
.ui
.fremantle
:
2998 self
.feed_cache_update_cancelled
= True
3001 if not force_update
:
3002 self
.channels
= PodcastChannel
.load_from_db(self
.db
, self
.config
.download_dir
)
3003 self
.channel_list_changed
= True
3004 self
.update_podcast_list_model(select_url
=select_url_afterwards
)
3007 # Fix URLs if mygpo has rewritten them
3008 self
.rewrite_urls_mygpo()
3010 self
.updating_feed_cache
= True
3012 if channels
is None:
3013 # Only update podcasts for which updates are enabled
3014 channels
= [c
for c
in self
.channels
if c
.feed_update_enabled
]
3016 if gpodder
.ui
.fremantle
:
3017 hildon
.hildon_gtk_window_set_progress_indicator(self
.main_window
, True)
3018 hildon
.hildon_gtk_window_set_progress_indicator(self
.episodes_window
.main_window
, True)
3019 self
.fancy_progress_bar
.show()
3020 self
.button_subscribe
.set_sensitive(False)
3021 self
.button_refresh
.set_sensitive(False)
3022 self
.feed_cache_update_cancelled
= False
3024 self
.itemUpdate
.set_sensitive(False)
3025 self
.itemUpdateChannel
.set_sensitive(False)
3028 self
.tray_icon
.set_status(self
.tray_icon
.STATUS_UPDATING_FEED_CACHE
)
3030 self
.feed_cache_update_cancelled
= False
3031 self
.btnCancelFeedUpdate
.show()
3032 self
.btnCancelFeedUpdate
.set_sensitive(True)
3033 if gpodder
.ui
.maemo
:
3034 self
.toolbarSpacer
.set_expand(False)
3035 self
.toolbarSpacer
.set_draw(True)
3036 self
.btnUpdateSelectedFeed
.hide()
3037 self
.toolFeedUpdateProgress
.show_all()
3039 self
.btnCancelFeedUpdate
.set_image(gtk
.image_new_from_stock(gtk
.STOCK_STOP
, gtk
.ICON_SIZE_BUTTON
))
3040 self
.hboxUpdateFeeds
.show_all()
3041 self
.btnUpdateFeeds
.hide()
3043 if len(channels
) == 1:
3044 text
= _('Updating "%s"...') % channels
[0].title
3046 count
= len(channels
)
3047 text
= N_('Updating %d feed...', 'Updating %d feeds...', count
) % count
3048 self
.pbFeedUpdate
.set_text(text
)
3049 self
.pbFeedUpdate
.set_fraction(0)
3051 args
= (channels
, select_url_afterwards
)
3052 threading
.Thread(target
=self
.update_feed_cache_proc
, args
=args
).start()
3054 def on_gPodder_delete_event(self
, widget
, *args
):
3055 """Called when the GUI wants to close the window
3056 Displays a confirmation dialog (and closes/hides gPodder)
3059 downloading
= self
.download_status_model
.are_downloads_in_progress()
3061 # Only iconify if we are using the window's "X" button,
3062 # but not when we are using "Quit" in the menu or toolbar
3063 if self
.config
.on_quit_systray
and self
.tray_icon
and widget
.get_name() not in ('toolQuit', 'itemQuit'):
3064 self
.iconify_main_window()
3065 elif self
.config
.on_quit_ask
or downloading
:
3066 if gpodder
.ui
.fremantle
:
3067 self
.close_gpodder()
3068 elif gpodder
.ui
.diablo
:
3069 result
= self
.show_confirmation(_('Do you really want to quit gPodder now?'))
3071 self
.close_gpodder()
3074 dialog
= gtk
.MessageDialog(self
.gPodder
, gtk
.DIALOG_MODAL
, gtk
.MESSAGE_QUESTION
, gtk
.BUTTONS_NONE
)
3075 dialog
.add_button(gtk
.STOCK_CANCEL
, gtk
.RESPONSE_CANCEL
)
3076 quit_button
= dialog
.add_button(gtk
.STOCK_QUIT
, gtk
.RESPONSE_CLOSE
)
3078 title
= _('Quit gPodder')
3080 message
= _('You are downloading episodes. You can resume downloads the next time you start gPodder. Do you want to quit now?')
3082 message
= _('Do you really want to quit gPodder now?')
3084 dialog
.set_title(title
)
3085 dialog
.set_markup('<span weight="bold" size="larger">%s</span>\n\n%s'%(title
, message
))
3087 cb_ask
= gtk
.CheckButton(_("Don't ask me again"))
3088 dialog
.vbox
.pack_start(cb_ask
)
3091 quit_button
.grab_focus()
3092 result
= dialog
.run()
3095 if result
== gtk
.RESPONSE_CLOSE
:
3096 if not downloading
and cb_ask
.get_active() == True:
3097 self
.config
.on_quit_ask
= False
3098 self
.close_gpodder()
3100 self
.close_gpodder()
3104 def close_gpodder(self
):
3105 """ clean everything and exit properly
3108 if self
.save_channels_opml():
3109 pass # FIXME: Add mygpo synchronization here
3111 self
.show_message(_('Please check your permissions and free disk space.'), _('Error saving podcast list'), important
=True)
3115 if self
.tray_icon
is not None:
3116 self
.tray_icon
.set_visible(False)
3118 # Notify all tasks to to carry out any clean-up actions
3119 self
.download_status_model
.tell_all_tasks_to_quit()
3121 while gtk
.events_pending():
3122 gtk
.main_iteration(False)
3129 def get_expired_episodes(self
):
3130 for channel
in self
.channels
:
3131 for episode
in channel
.get_downloaded_episodes():
3132 # Never consider locked episodes as old
3133 if episode
.is_locked
:
3136 # Never consider fresh episodes as old
3137 if episode
.age_in_days() < self
.config
.episode_old_age
:
3140 # Do not delete played episodes (except if configured)
3141 if episode
.is_played
:
3142 if not self
.config
.auto_remove_played_episodes
:
3145 # Do not delete unplayed episodes (except if configured)
3146 if not episode
.is_played
:
3147 if not self
.config
.auto_remove_unplayed_episodes
:
3152 def delete_episode_list(self
, episodes
, confirm
=True, skip_locked
=True):
3157 episodes
= [e
for e
in episodes
if not e
.is_locked
]
3160 title
= _('Episodes are locked')
3161 message
= _('The selected episodes are locked. Please unlock the episodes that you want to delete before trying to delete them.')
3162 self
.notification(message
, title
, widget
=self
.treeAvailable
)
3165 count
= len(episodes
)
3166 title
= N_('Delete %d episode?', 'Delete %d episodes?', count
) % count
3167 message
= _('Deleting episodes removes downloaded files.')
3169 if gpodder
.ui
.fremantle
:
3170 message
= '\n'.join([title
, message
])
3172 if confirm
and not self
.show_confirmation(message
, title
):
3175 progress
= ProgressIndicator(_('Deleting episodes'), \
3176 _('Please wait while episodes are deleted'), \
3177 parent
=self
.get_dialog_parent())
3179 def finish_deletion(episode_urls
, channel_urls
):
3180 progress
.on_finished()
3182 # Episodes have been deleted - persist the database
3185 self
.update_episode_list_icons(episode_urls
)
3186 self
.update_podcast_list_model(channel_urls
)
3187 self
.play_or_download()
3190 episode_urls
= set()
3191 channel_urls
= set()
3193 episodes_status_update
= []
3194 for idx
, episode
in enumerate(episodes
):
3195 progress
.on_progress(float(idx
)/float(len(episodes
)))
3196 if episode
.is_locked
and skip_locked
:
3197 log('Not deleting episode (is locked): %s', episode
.title
)
3199 log('Deleting episode: %s', episode
.title
)
3200 progress
.on_message(episode
.title
)
3201 episode
.delete_from_disk()
3202 episode_urls
.add(episode
.url
)
3203 channel_urls
.add(episode
.channel
.url
)
3204 episodes_status_update
.append(episode
)
3206 # Tell the shownotes window that we have removed the episode
3207 if self
.episode_shownotes_window
is not None and \
3208 self
.episode_shownotes_window
.episode
is not None and \
3209 self
.episode_shownotes_window
.episode
.url
== episode
.url
:
3210 util
.idle_add(self
.episode_shownotes_window
._download
_status
_changed
, None)
3212 # Notify the web service about the status update + upload
3213 self
.mygpo_client
.on_delete(episodes_status_update
)
3214 self
.mygpo_client
.flush()
3216 util
.idle_add(finish_deletion
, episode_urls
, channel_urls
)
3218 threading
.Thread(target
=thread_proc
).start()
3222 def on_itemRemoveOldEpisodes_activate( self
, widget
):
3223 if gpodder
.ui
.maemo
:
3225 ('maemo_remove_markup', None, None, _('Episode')),
3229 ('title_markup', None, None, _('Episode')),
3230 ('filesize_prop', 'length', gobject
.TYPE_INT
, _('Size')),
3231 ('pubdate_prop', 'pubDate', gobject
.TYPE_INT
, _('Released')),
3232 ('played_prop', None, None, _('Status')),
3233 ('age_prop', 'age_int_prop', gobject
.TYPE_INT
, _('Downloaded')),
3236 msg_older_than
= N_('Select older than %d day', 'Select older than %d days', self
.config
.episode_old_age
)
3237 selection_buttons
= {
3238 _('Select played'): lambda episode
: episode
.is_played
,
3239 _('Select finished'): lambda episode
: episode
.is_finished(),
3240 msg_older_than
% self
.config
.episode_old_age
: lambda episode
: episode
.age_in_days() > self
.config
.episode_old_age
,
3243 instructions
= _('Select the episodes you want to delete:')
3247 for channel
in self
.channels
:
3248 for episode
in channel
.get_downloaded_episodes():
3249 # Disallow deletion of locked episodes that still exist
3250 if not episode
.is_locked
or not episode
.file_exists():
3251 episodes
.append(episode
)
3252 # Automatically select played and file-less episodes
3253 selected
.append(episode
.is_played
or \
3254 not episode
.file_exists())
3256 gPodderEpisodeSelector(self
.gPodder
, title
= _('Delete episodes'), instructions
= instructions
, \
3257 episodes
= episodes
, selected
= selected
, columns
= columns
, \
3258 stock_ok_button
= gtk
.STOCK_DELETE
, callback
= self
.delete_episode_list
, \
3259 selection_buttons
= selection_buttons
, _config
=self
.config
, \
3260 show_episode_shownotes
=self
.show_episode_shownotes
)
3262 def on_selected_episodes_status_changed(self
):
3263 # The order of the updates here is important! When "All episodes" is
3264 # selected, the update of the podcast list model depends on the episode
3265 # list selection to determine which podcasts are affected. Updating
3266 # the episode list could remove the selection if a filter is active.
3267 self
.update_podcast_list_model(selected
=True)
3268 self
.update_episode_list_icons(selected
=True)
3271 def mark_selected_episodes_new(self
):
3272 for episode
in self
.get_selected_episodes():
3274 self
.on_selected_episodes_status_changed()
3276 def mark_selected_episodes_old(self
):
3277 for episode
in self
.get_selected_episodes():
3279 self
.on_selected_episodes_status_changed()
3281 def on_item_toggle_played_activate( self
, widget
, toggle
= True, new_value
= False):
3282 for episode
in self
.get_selected_episodes():
3284 episode
.mark(is_played
=not episode
.is_played
)
3286 episode
.mark(is_played
=new_value
)
3287 self
.on_selected_episodes_status_changed()
3289 def on_item_toggle_lock_activate(self
, widget
, toggle
=True, new_value
=False):
3290 for episode
in self
.get_selected_episodes():
3292 episode
.mark(is_locked
=not episode
.is_locked
)
3294 episode
.mark(is_locked
=new_value
)
3295 self
.on_selected_episodes_status_changed()
3297 def on_channel_toggle_lock_activate(self
, widget
, toggle
=True, new_value
=False):
3298 if self
.active_channel
is None:
3301 self
.active_channel
.channel_is_locked
= not self
.active_channel
.channel_is_locked
3302 self
.active_channel
.update_channel_lock()
3304 for episode
in self
.active_channel
.get_all_episodes():
3305 episode
.mark(is_locked
=self
.active_channel
.channel_is_locked
)
3307 self
.update_podcast_list_model(selected
=True)
3308 self
.update_episode_list_icons(all
=True)
3310 def on_itemUpdateChannel_activate(self
, widget
=None):
3311 if self
.active_channel
is None:
3312 title
= _('No podcast selected')
3313 message
= _('Please select a podcast in the podcasts list to update.')
3314 self
.show_message( message
, title
, widget
=self
.treeChannels
)
3317 # Dirty hack to check for "All episodes" (see gpodder.gtkui.model)
3318 if getattr(self
.active_channel
, 'ALL_EPISODES_PROXY', False):
3319 self
.update_feed_cache()
3321 self
.update_feed_cache(channels
=[self
.active_channel
])
3323 def on_itemUpdate_activate(self
, widget
=None):
3324 # Check if we have outstanding subscribe/unsubscribe actions
3325 if self
.on_add_remove_podcasts_mygpo():
3326 log('Update cancelled (received server changes)', sender
=self
)
3330 self
.update_feed_cache()
3332 gPodderWelcome(self
.gPodder
,
3333 center_on_widget
=self
.gPodder
,
3334 show_example_podcasts_callback
=self
.on_itemImportChannels_activate
,
3335 setup_my_gpodder_callback
=self
.on_download_subscriptions_from_mygpo
)
3337 def download_episode_list_paused(self
, episodes
):
3338 self
.download_episode_list(episodes
, True)
3340 def download_episode_list(self
, episodes
, add_paused
=False, force_start
=False):
3341 enable_update
= False
3343 for episode
in episodes
:
3344 log('Downloading episode: %s', episode
.title
, sender
= self
)
3345 if not episode
.was_downloaded(and_exists
=True):
3347 for task
in self
.download_tasks_seen
:
3348 if episode
.url
== task
.url
and task
.status
not in (task
.DOWNLOADING
, task
.QUEUED
):
3349 self
.download_queue_manager
.add_task(task
, force_start
)
3350 enable_update
= True
3358 task
= download
.DownloadTask(episode
, self
.config
)
3359 except Exception, e
:
3360 d
= {'episode': episode
.title
, 'message': str(e
)}
3361 message
= _('Download error while downloading %(episode)s: %(message)s')
3362 self
.show_message(message
% d
, _('Download error'), important
=True)
3363 log('Download error while downloading %s', episode
.title
, sender
=self
, traceback
=True)
3367 task
.status
= task
.PAUSED
3369 self
.mygpo_client
.on_download([task
.episode
])
3370 self
.download_queue_manager
.add_task(task
, force_start
)
3372 self
.download_status_model
.register_task(task
)
3373 enable_update
= True
3376 self
.enable_download_list_update()
3378 # Flush updated episode status
3379 self
.mygpo_client
.flush()
3381 def cancel_task_list(self
, tasks
):
3386 if task
.status
in (task
.QUEUED
, task
.DOWNLOADING
):
3387 task
.status
= task
.CANCELLED
3388 elif task
.status
== task
.PAUSED
:
3389 task
.status
= task
.CANCELLED
3390 # Call run, so the partial file gets deleted
3393 self
.update_episode_list_icons([task
.url
for task
in tasks
])
3394 self
.play_or_download()
3396 # Update the tab title and downloads list
3397 self
.update_downloads_list()
3399 def new_episodes_show(self
, episodes
, notification
=False, selected
=None):
3400 if gpodder
.ui
.maemo
:
3402 ('maemo_markup', None, None, _('Episode')),
3404 show_notification
= notification
3407 ('title_markup', None, None, _('Episode')),
3408 ('filesize_prop', 'length', gobject
.TYPE_INT
, _('Size')),
3409 ('pubdate_prop', 'pubDate', gobject
.TYPE_INT
, _('Released')),
3411 show_notification
= False
3413 instructions
= _('Select the episodes you want to download:')
3415 if self
.new_episodes_window
is not None:
3416 self
.new_episodes_window
.main_window
.destroy()
3417 self
.new_episodes_window
= None
3419 def download_episodes_callback(episodes
):
3420 self
.new_episodes_window
= None
3421 self
.download_episode_list(episodes
)
3423 if selected
is None:
3424 # Select all by default
3425 selected
= [True]*len(episodes
)
3427 self
.new_episodes_window
= gPodderEpisodeSelector(self
.gPodder
, \
3428 title
=_('New episodes available'), \
3429 instructions
=instructions
, \
3430 episodes
=episodes
, \
3432 selected
=selected
, \
3433 stock_ok_button
= 'gpodder-download', \
3434 callback
=download_episodes_callback
, \
3435 remove_callback
=lambda e
: e
.mark_old(), \
3436 remove_action
=_('Mark as old'), \
3437 remove_finished
=self
.episode_new_status_changed
, \
3438 _config
=self
.config
, \
3439 show_notification
=show_notification
, \
3440 show_episode_shownotes
=self
.show_episode_shownotes
)
3442 def on_itemDownloadAllNew_activate(self
, widget
, *args
):
3443 if not self
.offer_new_episodes():
3444 self
.show_message(_('Please check for new episodes later.'), \
3445 _('No new episodes available'), widget
=self
.btnUpdateFeeds
)
3447 def get_new_episodes(self
, channels
=None):
3448 if channels
is None:
3449 channels
= self
.channels
3451 for channel
in channels
:
3452 for episode
in channel
.get_new_episodes(downloading
=self
.episode_is_downloading
):
3453 episodes
.append(episode
)
3457 @dbus.service
.method(gpodder
.dbus_interface
)
3458 def start_device_synchronization(self
):
3459 """Public D-Bus API for starting Device sync (Desktop only)
3461 This method can be called to initiate a synchronization with
3462 a configured protable media player. This only works for the
3463 Desktop version of gPodder and does nothing on Maemo.
3465 if gpodder
.ui
.desktop
:
3466 self
.on_sync_to_ipod_activate(None)
3471 def on_sync_to_ipod_activate(self
, widget
, episodes
=None):
3472 self
.sync_ui
.on_synchronize_episodes(self
.channels
, episodes
)
3474 def commit_changes_to_database(self
):
3475 """This will be called after the sync process is finished"""
3478 def on_cleanup_ipod_activate(self
, widget
, *args
):
3479 self
.sync_ui
.on_cleanup_device()
3481 def on_manage_device_playlist(self
, widget
):
3482 self
.sync_ui
.on_manage_device_playlist()
3484 def show_hide_tray_icon(self
):
3485 if self
.config
.display_tray_icon
and have_trayicon
and self
.tray_icon
is None:
3486 self
.tray_icon
= GPodderStatusIcon(self
, gpodder
.icon_file
, self
.config
)
3487 elif not self
.config
.display_tray_icon
and self
.tray_icon
is not None:
3488 self
.tray_icon
.set_visible(False)
3490 self
.tray_icon
= None
3492 if self
.config
.minimize_to_tray
and self
.tray_icon
:
3493 self
.tray_icon
.set_visible(self
.is_iconified())
3494 elif self
.tray_icon
:
3495 self
.tray_icon
.set_visible(True)
3497 def on_itemShowAllEpisodes_activate(self
, widget
):
3498 self
.config
.podcast_list_view_all
= widget
.get_active()
3500 def on_itemShowToolbar_activate(self
, widget
):
3501 self
.config
.show_toolbar
= self
.itemShowToolbar
.get_active()
3503 def on_itemShowDescription_activate(self
, widget
):
3504 self
.config
.episode_list_descriptions
= self
.itemShowDescription
.get_active()
3506 def on_item_view_hide_boring_podcasts_toggled(self
, toggleaction
):
3507 self
.config
.podcast_list_hide_boring
= toggleaction
.get_active()
3508 if self
.config
.podcast_list_hide_boring
:
3509 self
.podcast_list_model
.set_view_mode(self
.config
.episode_list_view_mode
)
3511 self
.podcast_list_model
.set_view_mode(-1)
3513 def on_item_view_podcasts_changed(self
, radioaction
, current
):
3515 if current
== self
.item_view_podcasts_all
:
3516 self
.podcast_list_model
.set_view_mode(-1)
3517 elif current
== self
.item_view_podcasts_downloaded
:
3518 self
.podcast_list_model
.set_view_mode(EpisodeListModel
.VIEW_DOWNLOADED
)
3519 elif current
== self
.item_view_podcasts_unplayed
:
3520 self
.podcast_list_model
.set_view_mode(EpisodeListModel
.VIEW_UNPLAYED
)
3522 self
.config
.podcast_list_view_mode
= self
.podcast_list_model
.get_view_mode()
3524 def on_item_view_episodes_changed(self
, radioaction
, current
):
3525 if current
== self
.item_view_episodes_all
:
3526 self
.config
.episode_list_view_mode
= EpisodeListModel
.VIEW_ALL
3527 elif current
== self
.item_view_episodes_undeleted
:
3528 self
.config
.episode_list_view_mode
= EpisodeListModel
.VIEW_UNDELETED
3529 elif current
== self
.item_view_episodes_downloaded
:
3530 self
.config
.episode_list_view_mode
= EpisodeListModel
.VIEW_DOWNLOADED
3531 elif current
== self
.item_view_episodes_unplayed
:
3532 self
.config
.episode_list_view_mode
= EpisodeListModel
.VIEW_UNPLAYED
3534 self
.episode_list_model
.set_view_mode(self
.config
.episode_list_view_mode
)
3536 if self
.config
.podcast_list_hide_boring
and not gpodder
.ui
.fremantle
:
3537 self
.podcast_list_model
.set_view_mode(self
.config
.episode_list_view_mode
)
3539 def update_item_device( self
):
3540 if not gpodder
.ui
.fremantle
:
3541 if self
.config
.device_type
!= 'none':
3542 self
.itemDevice
.set_visible(True)
3543 self
.itemDevice
.label
= self
.get_device_name()
3545 self
.itemDevice
.set_visible(False)
3547 def properties_closed( self
):
3548 self
.preferences_dialog
= None
3549 self
.show_hide_tray_icon()
3550 self
.update_item_device()
3551 if gpodder
.ui
.maemo
:
3552 selection
= self
.treeAvailable
.get_selection()
3553 if self
.config
.maemo_enable_gestures
or \
3554 self
.config
.enable_fingerscroll
:
3555 selection
.set_mode(gtk
.SELECTION_SINGLE
)
3557 selection
.set_mode(gtk
.SELECTION_MULTIPLE
)
3559 def on_itemPreferences_activate(self
, widget
, *args
):
3560 self
.preferences_dialog
= gPodderPreferences(self
.main_window
, \
3561 _config
=self
.config
, \
3562 callback_finished
=self
.properties_closed
, \
3563 user_apps_reader
=self
.user_apps_reader
, \
3564 parent_window
=self
.main_window
, \
3565 mygpo_client
=self
.mygpo_client
, \
3566 on_send_full_subscriptions
=self
.on_send_full_subscriptions
)
3568 # Initial message to relayout window (in case it's opened in portrait mode
3569 self
.preferences_dialog
.on_window_orientation_changed(self
._last
_orientation
)
3571 def on_itemDependencies_activate(self
, widget
):
3572 gPodderDependencyManager(self
.gPodder
)
3574 def on_goto_mygpo(self
, widget
):
3575 self
.mygpo_client
.open_website()
3577 def on_download_subscriptions_from_mygpo(self
, action
=None):
3578 title
= _('Login to gpodder.net')
3579 message
= _('Please login to download your subscriptions.')
3580 success
, (username
, password
) = self
.show_login_dialog(title
, message
, \
3581 self
.config
.mygpo_username
, self
.config
.mygpo_password
)
3585 self
.config
.mygpo_username
= username
3586 self
.config
.mygpo_password
= password
3588 dir = gPodderPodcastDirectory(self
.gPodder
, _config
=self
.config
, \
3589 custom_title
=_('Subscriptions on gpodder.net'), \
3590 add_urls_callback
=self
.add_podcast_list
, \
3591 hide_url_entry
=True)
3593 # TODO: Refactor this into "gpodder.my" or mygpoclient, so that
3594 # we do not have to hardcode the URL here
3595 OPML_URL
= 'http://gpodder.net/subscriptions/%s.opml' % self
.config
.mygpo_username
3596 url
= util
.url_add_authentication(OPML_URL
, \
3597 self
.config
.mygpo_username
, \
3598 self
.config
.mygpo_password
)
3599 dir.download_opml_file(url
)
3601 def on_mygpo_settings_activate(self
, action
=None):
3602 # This dialog is only used for Maemo 4
3603 if not gpodder
.ui
.diablo
:
3606 settings
= MygPodderSettings(self
.main_window
, \
3607 config
=self
.config
, \
3608 mygpo_client
=self
.mygpo_client
, \
3609 on_send_full_subscriptions
=self
.on_send_full_subscriptions
)
3611 def on_itemAddChannel_activate(self
, widget
=None):
3612 gPodderAddPodcast(self
.gPodder
, \
3613 add_urls_callback
=self
.add_podcast_list
)
3615 def on_itemEditChannel_activate(self
, widget
, *args
):
3616 if self
.active_channel
is None:
3617 title
= _('No podcast selected')
3618 message
= _('Please select a podcast in the podcasts list to edit.')
3619 self
.show_message( message
, title
, widget
=self
.treeChannels
)
3622 callback_closed
= lambda: self
.update_podcast_list_model(selected
=True)
3623 gPodderChannel(self
.main_window
, \
3624 channel
=self
.active_channel
, \
3625 callback_closed
=callback_closed
, \
3626 cover_downloader
=self
.cover_downloader
)
3628 def on_itemMassUnsubscribe_activate(self
, item
=None):
3630 ('title', None, None, _('Podcast')),
3633 # We're abusing the Episode Selector for selecting Podcasts here,
3634 # but it works and looks good, so why not? -- thp
3635 gPodderEpisodeSelector(self
.main_window
, \
3636 title
=_('Remove podcasts'), \
3637 instructions
=_('Select the podcast you want to remove.'), \
3638 episodes
=self
.channels
, \
3640 size_attribute
=None, \
3641 stock_ok_button
=_('Remove'), \
3642 callback
=self
.remove_podcast_list
, \
3643 _config
=self
.config
)
3645 def remove_podcast_list(self
, channels
, confirm
=True):
3647 log('No podcasts selected for deletion', sender
=self
)
3650 if len(channels
) == 1:
3651 title
= _('Removing podcast')
3652 info
= _('Please wait while the podcast is removed')
3653 message
= _('Do you really want to remove this podcast and its episodes?')
3655 title
= _('Removing podcasts')
3656 info
= _('Please wait while the podcasts are removed')
3657 message
= _('Do you really want to remove the selected podcasts and their episodes?')
3659 if confirm
and not self
.show_confirmation(message
, title
):
3662 progress
= ProgressIndicator(title
, info
, parent
=self
.get_dialog_parent())
3664 def finish_deletion(select_url
):
3665 # Upload subscription list changes to the web service
3666 self
.mygpo_client
.on_unsubscribe([c
.url
for c
in channels
])
3668 # Re-load the channels and select the desired new channel
3669 self
.update_feed_cache(force_update
=False, select_url_afterwards
=select_url
)
3670 progress
.on_finished()
3671 self
.update_podcasts_tab()
3676 for idx
, channel
in enumerate(channels
):
3677 # Update the UI for correct status messages
3678 progress
.on_progress(float(idx
)/float(len(channels
)))
3679 progress
.on_message(channel
.title
)
3681 # Delete downloaded episodes
3682 channel
.remove_downloaded()
3684 # cancel any active downloads from this channel
3685 for episode
in channel
.get_all_episodes():
3686 util
.idle_add(self
.download_status_model
.cancel_by_url
,
3689 if len(channels
) == 1:
3690 # get the URL of the podcast we want to select next
3691 if channel
in self
.channels
:
3692 position
= self
.channels
.index(channel
)
3696 if position
== len(self
.channels
)-1:
3697 # this is the last podcast, so select the URL
3698 # of the item before this one (i.e. the "new last")
3699 select_url
= self
.channels
[position
-1].url
3701 # there is a podcast after the deleted one, so
3702 # we simply select the one that comes after it
3703 select_url
= self
.channels
[position
+1].url
3705 # Remove the channel and clean the database entries
3707 self
.channels
.remove(channel
)
3709 # Clean up downloads and download directories
3710 self
.clean_up_downloads()
3712 self
.channel_list_changed
= True
3713 self
.save_channels_opml()
3715 # The remaining stuff is to be done in the GTK main thread
3716 util
.idle_add(finish_deletion
, select_url
)
3718 threading
.Thread(target
=thread_proc
).start()
3720 def on_itemRemoveChannel_activate(self
, widget
, *args
):
3721 if self
.active_channel
is None:
3722 title
= _('No podcast selected')
3723 message
= _('Please select a podcast in the podcasts list to remove.')
3724 self
.show_message( message
, title
, widget
=self
.treeChannels
)
3727 self
.remove_podcast_list([self
.active_channel
])
3729 def get_opml_filter(self
):
3730 filter = gtk
.FileFilter()
3731 filter.add_pattern('*.opml')
3732 filter.add_pattern('*.xml')
3733 filter.set_name(_('OPML files')+' (*.opml, *.xml)')
3736 def on_item_import_from_file_activate(self
, widget
, filename
=None):
3737 if filename
is None:
3738 if gpodder
.ui
.desktop
or gpodder
.ui
.fremantle
:
3739 # FIXME: Hildonization on Fremantle
3740 dlg
= gtk
.FileChooserDialog(title
=_('Import from OPML'), parent
=None, action
=gtk
.FILE_CHOOSER_ACTION_OPEN
)
3741 dlg
.add_button(gtk
.STOCK_CANCEL
, gtk
.RESPONSE_CANCEL
)
3742 dlg
.add_button(gtk
.STOCK_OPEN
, gtk
.RESPONSE_OK
)
3743 elif gpodder
.ui
.diablo
:
3744 dlg
= hildon
.FileChooserDialog(self
.gPodder
, gtk
.FILE_CHOOSER_ACTION_OPEN
)
3745 dlg
.set_filter(self
.get_opml_filter())
3746 response
= dlg
.run()
3748 if response
== gtk
.RESPONSE_OK
:
3749 filename
= dlg
.get_filename()
3752 if filename
is not None:
3753 dir = gPodderPodcastDirectory(self
.gPodder
, _config
=self
.config
, \
3754 custom_title
=_('Import podcasts from OPML file'), \
3755 add_urls_callback
=self
.add_podcast_list
, \
3756 hide_url_entry
=True)
3757 dir.download_opml_file(filename
)
3759 def on_itemExportChannels_activate(self
, widget
, *args
):
3760 if not self
.channels
:
3761 title
= _('Nothing to export')
3762 message
= _('Your list of podcast subscriptions is empty. Please subscribe to some podcasts first before trying to export your subscription list.')
3763 self
.show_message(message
, title
, widget
=self
.treeChannels
)
3766 if gpodder
.ui
.desktop
or gpodder
.ui
.fremantle
:
3767 # FIXME: Hildonization on Fremantle
3768 dlg
= gtk
.FileChooserDialog(title
=_('Export to OPML'), parent
=self
.gPodder
, action
=gtk
.FILE_CHOOSER_ACTION_SAVE
)
3769 dlg
.add_button(gtk
.STOCK_CANCEL
, gtk
.RESPONSE_CANCEL
)
3770 dlg
.add_button(gtk
.STOCK_SAVE
, gtk
.RESPONSE_OK
)
3771 elif gpodder
.ui
.diablo
:
3772 dlg
= hildon
.FileChooserDialog(self
.gPodder
, gtk
.FILE_CHOOSER_ACTION_SAVE
)
3773 dlg
.set_filter(self
.get_opml_filter())
3774 response
= dlg
.run()
3775 if response
== gtk
.RESPONSE_OK
:
3776 filename
= dlg
.get_filename()
3778 exporter
= opml
.Exporter( filename
)
3779 if exporter
.write(self
.channels
):
3780 count
= len(self
.channels
)
3781 title
= N_('%d subscription exported', '%d subscriptions exported', count
) % count
3782 self
.show_message(_('Your podcast list has been successfully exported.'), title
, widget
=self
.treeChannels
)
3784 self
.show_message( _('Could not export OPML to file. Please check your permissions.'), _('OPML export failed'), important
=True)
3788 def on_itemImportChannels_activate(self
, widget
, *args
):
3789 if gpodder
.ui
.fremantle
:
3790 gPodderPodcastDirectory
.show_add_podcast_picker(self
.main_window
, \
3791 self
.config
.toplist_url
, \
3792 self
.config
.opml_url
, \
3793 self
.add_podcast_list
, \
3794 self
.on_itemAddChannel_activate
, \
3795 self
.on_download_subscriptions_from_mygpo
, \
3796 self
.show_text_edit_dialog
)
3798 dir = gPodderPodcastDirectory(self
.main_window
, _config
=self
.config
, \
3799 add_urls_callback
=self
.add_podcast_list
)
3800 util
.idle_add(dir.download_opml_file
, self
.config
.opml_url
)
3802 def on_homepage_activate(self
, widget
, *args
):
3803 util
.open_website(gpodder
.__url
__)
3805 def on_wiki_activate(self
, widget
, *args
):
3806 util
.open_website('http://gpodder.org/wiki/User_Manual')
3808 def on_bug_tracker_activate(self
, widget
, *args
):
3809 if gpodder
.ui
.maemo
:
3810 util
.open_website('http://bugs.maemo.org/enter_bug.cgi?product=gPodder')
3812 util
.open_website('https://bugs.gpodder.org/enter_bug.cgi?product=gPodder')
3814 def on_item_support_activate(self
, widget
):
3815 util
.open_website('http://gpodder.org/donate')
3817 def on_itemAbout_activate(self
, widget
, *args
):
3818 if gpodder
.ui
.fremantle
:
3819 from gpodder
.gtkui
.frmntl
.about
import HeAboutDialog
3820 HeAboutDialog
.present(self
.main_window
,
3823 gpodder
.__version
__,
3824 _('A podcast client with focus on usability'),
3825 gpodder
.__copyright
__,
3827 'http://bugs.maemo.org/enter_bug.cgi?product=gPodder',
3828 'http://gpodder.org/donate')
3831 dlg
= gtk
.AboutDialog()
3832 dlg
.set_transient_for(self
.main_window
)
3833 dlg
.set_name('gPodder')
3834 dlg
.set_version(gpodder
.__version
__)
3835 dlg
.set_copyright(gpodder
.__copyright
__)
3836 dlg
.set_comments(_('A podcast client with focus on usability'))
3837 dlg
.set_website(gpodder
.__url
__)
3838 dlg
.set_translator_credits( _('translator-credits'))
3839 dlg
.connect( 'response', lambda dlg
, response
: dlg
.destroy())
3841 if gpodder
.ui
.desktop
:
3842 # For the "GUI" version, we add some more
3843 # items to the about dialog (credits and logo)
3846 'Thomas Perl <thpinfo.com>',
3849 if os
.path
.exists(gpodder
.credits_file
):
3850 credits
= open(gpodder
.credits_file
).read().strip().split('\n')
3851 app_authors
+= ['', _('Patches, bug reports and donations by:')]
3852 app_authors
+= credits
3854 dlg
.set_authors(app_authors
)
3856 dlg
.set_logo(gtk
.gdk
.pixbuf_new_from_file(gpodder
.icon_file
))
3858 dlg
.set_logo_icon_name('gpodder')
3862 def on_wNotebook_switch_page(self
, widget
, *args
):
3864 if gpodder
.ui
.maemo
:
3865 self
.tool_downloads
.set_active(page_num
== 1)
3866 page
= self
.wNotebook
.get_nth_page(page_num
)
3867 tab_label
= self
.wNotebook
.get_tab_label(page
).get_text()
3868 if page_num
== 0 and self
.active_channel
is not None:
3869 self
.set_title(self
.active_channel
.title
)
3871 self
.set_title(tab_label
)
3873 self
.play_or_download()
3874 self
.menuChannels
.set_sensitive(True)
3875 self
.menuSubscriptions
.set_sensitive(True)
3876 # The message area in the downloads tab should be hidden
3877 # when the user switches away from the downloads tab
3878 if self
.message_area
is not None:
3879 self
.message_area
.hide()
3880 self
.message_area
= None
3882 self
.menuChannels
.set_sensitive(False)
3883 self
.menuSubscriptions
.set_sensitive(False)
3884 if gpodder
.ui
.desktop
:
3885 self
.toolDownload
.set_sensitive(False)
3886 self
.toolPlay
.set_sensitive(False)
3887 self
.toolTransfer
.set_sensitive(False)
3888 self
.toolCancel
.set_sensitive(False)
3890 def on_treeChannels_row_activated(self
, widget
, path
, *args
):
3891 # double-click action of the podcast list or enter
3892 self
.treeChannels
.set_cursor(path
)
3894 def on_treeChannels_cursor_changed(self
, widget
, *args
):
3895 ( model
, iter ) = self
.treeChannels
.get_selection().get_selected()
3897 if model
is not None and iter is not None:
3898 old_active_channel
= self
.active_channel
3899 self
.active_channel
= model
.get_value(iter, PodcastListModel
.C_CHANNEL
)
3901 if self
.active_channel
== old_active_channel
:
3904 if gpodder
.ui
.maemo
:
3905 self
.set_title(self
.active_channel
.title
)
3907 # Dirty hack to check for "All episodes" (see gpodder.gtkui.model)
3908 if getattr(self
.active_channel
, 'ALL_EPISODES_PROXY', False):
3909 self
.itemEditChannel
.set_visible(False)
3910 self
.itemRemoveChannel
.set_visible(False)
3912 self
.itemEditChannel
.set_visible(True)
3913 self
.itemRemoveChannel
.set_visible(True)
3915 self
.active_channel
= None
3916 self
.itemEditChannel
.set_visible(False)
3917 self
.itemRemoveChannel
.set_visible(False)
3919 self
.update_episode_list_model()
3921 def on_btnEditChannel_clicked(self
, widget
, *args
):
3922 self
.on_itemEditChannel_activate( widget
, args
)
3924 def get_podcast_urls_from_selected_episodes(self
):
3925 """Get a set of podcast URLs based on the selected episodes"""
3926 return set(episode
.channel
.url
for episode
in \
3927 self
.get_selected_episodes())
3929 def get_selected_episodes(self
):
3930 """Get a list of selected episodes from treeAvailable"""
3931 selection
= self
.treeAvailable
.get_selection()
3932 model
, paths
= selection
.get_selected_rows()
3934 episodes
= [model
.get_value(model
.get_iter(path
), EpisodeListModel
.C_EPISODE
) for path
in paths
]
3937 def on_transfer_selected_episodes(self
, widget
):
3938 self
.on_sync_to_ipod_activate(widget
, self
.get_selected_episodes())
3940 def on_playback_selected_episodes(self
, widget
):
3941 self
.playback_episodes(self
.get_selected_episodes())
3943 def on_shownotes_selected_episodes(self
, widget
):
3944 episodes
= self
.get_selected_episodes()
3946 episode
= episodes
.pop(0)
3947 self
.show_episode_shownotes(episode
)
3949 self
.show_message(_('Please select an episode from the episode list to display shownotes.'), _('No episode selected'), widget
=self
.treeAvailable
)
3951 def on_download_selected_episodes(self
, widget
):
3952 episodes
= self
.get_selected_episodes()
3953 self
.download_episode_list(episodes
)
3954 self
.update_episode_list_icons([episode
.url
for episode
in episodes
])
3955 self
.play_or_download()
3957 def on_treeAvailable_row_activated(self
, widget
, path
, view_column
):
3958 """Double-click/enter action handler for treeAvailable"""
3959 # We should only have one one selected as it was double clicked!
3960 e
= self
.get_selected_episodes()[0]
3962 if (self
.config
.double_click_episode_action
== 'download'):
3963 # If the episode has already been downloaded and exists then play it
3964 if e
.was_downloaded(and_exists
=True):
3965 self
.playback_episodes(self
.get_selected_episodes())
3966 # else download it if it is not already downloading
3967 elif not self
.episode_is_downloading(e
):
3968 self
.download_episode_list([e
])
3969 self
.update_episode_list_icons([e
.url
])
3970 self
.play_or_download()
3971 elif (self
.config
.double_click_episode_action
== 'stream'):
3972 # If we happen to have downloaded this episode simple play it
3973 if e
.was_downloaded(and_exists
=True):
3974 self
.playback_episodes(self
.get_selected_episodes())
3975 # else if streaming is possible stream it
3976 elif self
.streaming_possible():
3977 self
.playback_episodes(self
.get_selected_episodes())
3979 log('Unable to stream episode - default media player selected!', sender
=self
, traceback
=True)
3980 self
.show_message(_('Please check your media player settings in the preferences dialog.'), _('Unable to stream episode'), widget
=self
.toolPreferences
)
3982 # default action is to display show notes
3983 self
.on_shownotes_selected_episodes(widget
)
3985 def show_episode_shownotes(self
, episode
):
3986 if self
.episode_shownotes_window
is None:
3987 log('First-time use of episode window --- creating', sender
=self
)
3988 self
.episode_shownotes_window
= gPodderShownotes(self
.gPodder
, _config
=self
.config
, \
3989 _download_episode_list
=self
.download_episode_list
, \
3990 _playback_episodes
=self
.playback_episodes
, \
3991 _delete_episode_list
=self
.delete_episode_list
, \
3992 _episode_list_status_changed
=self
.episode_list_status_changed
, \
3993 _cancel_task_list
=self
.cancel_task_list
, \
3994 _episode_is_downloading
=self
.episode_is_downloading
, \
3995 _streaming_possible
=self
.streaming_possible())
3996 self
.episode_shownotes_window
.show(episode
)
3997 if self
.episode_is_downloading(episode
):
3998 self
.update_downloads_list()
4000 def restart_auto_update_timer(self
):
4001 if self
._auto
_update
_timer
_source
_id
is not None:
4002 log('Removing existing auto update timer.', sender
=self
)
4003 gobject
.source_remove(self
._auto
_update
_timer
_source
_id
)
4004 self
._auto
_update
_timer
_source
_id
= None
4006 if self
.config
.auto_update_feeds
and \
4007 self
.config
.auto_update_frequency
:
4008 interval
= 60*1000*self
.config
.auto_update_frequency
4009 log('Setting up auto update timer with interval %d.', \
4010 self
.config
.auto_update_frequency
, sender
=self
)
4011 self
._auto
_update
_timer
_source
_id
= gobject
.timeout_add(\
4012 interval
, self
._on
_auto
_update
_timer
)
4014 def _on_auto_update_timer(self
):
4015 log('Auto update timer fired.', sender
=self
)
4016 self
.update_feed_cache(force_update
=True)
4018 # Ask web service for sub changes (if enabled)
4019 self
.mygpo_client
.flush()
4023 def on_treeDownloads_row_activated(self
, widget
, *args
):
4024 # Use the standard way of working on the treeview
4025 selection
= self
.treeDownloads
.get_selection()
4026 (model
, paths
) = selection
.get_selected_rows()
4027 selected_tasks
= [(gtk
.TreeRowReference(model
, path
), model
.get_value(model
.get_iter(path
), 0)) for path
in paths
]
4029 for tree_row_reference
, task
in selected_tasks
:
4030 if task
.status
in (task
.DOWNLOADING
, task
.QUEUED
):
4031 task
.status
= task
.PAUSED
4032 elif task
.status
in (task
.CANCELLED
, task
.PAUSED
, task
.FAILED
):
4033 self
.download_queue_manager
.add_task(task
)
4034 self
.enable_download_list_update()
4035 elif task
.status
== task
.DONE
:
4036 model
.remove(model
.get_iter(tree_row_reference
.get_path()))
4038 self
.play_or_download()
4040 # Update the tab title and downloads list
4041 self
.update_downloads_list()
4043 def on_item_cancel_download_activate(self
, widget
):
4044 if self
.wNotebook
.get_current_page() == 0:
4045 selection
= self
.treeAvailable
.get_selection()
4046 (model
, paths
) = selection
.get_selected_rows()
4047 urls
= [model
.get_value(model
.get_iter(path
), \
4048 self
.episode_list_model
.C_URL
) for path
in paths
]
4049 selected_tasks
= [task
for task
in self
.download_tasks_seen \
4050 if task
.url
in urls
]
4052 selection
= self
.treeDownloads
.get_selection()
4053 (model
, paths
) = selection
.get_selected_rows()
4054 selected_tasks
= [model
.get_value(model
.get_iter(path
), \
4055 self
.download_status_model
.C_TASK
) for path
in paths
]
4056 self
.cancel_task_list(selected_tasks
)
4058 def on_btnCancelAll_clicked(self
, widget
, *args
):
4059 self
.cancel_task_list(self
.download_tasks_seen
)
4061 def on_btnDownloadedDelete_clicked(self
, widget
, *args
):
4062 episodes
= self
.get_selected_episodes()
4063 if len(episodes
) == 1:
4064 self
.delete_episode_list(episodes
, skip_locked
=False)
4066 self
.delete_episode_list(episodes
)
4068 def on_key_press(self
, widget
, event
):
4069 # Allow tab switching with Ctrl + PgUp/PgDown
4070 if event
.state
& gtk
.gdk
.CONTROL_MASK
:
4071 if event
.keyval
== gtk
.keysyms
.Page_Up
:
4072 self
.wNotebook
.prev_page()
4074 elif event
.keyval
== gtk
.keysyms
.Page_Down
:
4075 self
.wNotebook
.next_page()
4078 # After this code we only handle Maemo hardware keys,
4079 # so if we are not a Maemo app, we don't do anything
4080 if not gpodder
.ui
.maemo
:
4084 if event
.keyval
== gtk
.keysyms
.F7
: #plus
4086 elif event
.keyval
== gtk
.keysyms
.F8
: #minus
4089 if diff
!= 0 and not self
.currently_updating
:
4090 selection
= self
.treeChannels
.get_selection()
4091 (model
, iter) = selection
.get_selected()
4092 new_path
= ((model
.get_path(iter)[0]+diff
)%len(model
),)
4093 selection
.select_path(new_path
)
4094 self
.treeChannels
.set_cursor(new_path
)
4099 def on_iconify(self
):
4101 self
.gPodder
.set_skip_taskbar_hint(True)
4102 if self
.config
.minimize_to_tray
:
4103 self
.tray_icon
.set_visible(True)
4105 self
.gPodder
.set_skip_taskbar_hint(False)
4107 def on_uniconify(self
):
4109 self
.gPodder
.set_skip_taskbar_hint(False)
4110 if self
.config
.minimize_to_tray
:
4111 self
.tray_icon
.set_visible(False)
4113 self
.gPodder
.set_skip_taskbar_hint(False)
4115 def uniconify_main_window(self
):
4116 if self
.is_iconified():
4117 # We need to hide and then show the window in WMs like Metacity
4118 # or KWin4 to move the window to the active workspace
4119 # (see http://gpodder.org/bug/1125)
4122 self
.gPodder
.present()
4124 def iconify_main_window(self
):
4125 if not self
.is_iconified():
4126 self
.gPodder
.iconify()
4128 def update_podcasts_tab(self
):
4129 if len(self
.channels
):
4130 if gpodder
.ui
.fremantle
:
4131 self
.button_refresh
.set_title(_('Check for new episodes'))
4132 self
.button_refresh
.show()
4134 self
.label2
.set_text(_('Podcasts (%d)') % len(self
.channels
))
4136 if gpodder
.ui
.fremantle
:
4137 self
.button_refresh
.hide()
4139 self
.label2
.set_text(_('Podcasts'))
4141 @dbus.service
.method(gpodder
.dbus_interface
)
4142 def show_gui_window(self
):
4143 parent
= self
.get_dialog_parent()
4146 @dbus.service
.method(gpodder
.dbus_interface
)
4147 def subscribe_to_url(self
, url
):
4148 gPodderAddPodcast(self
.gPodder
,
4149 add_urls_callback
=self
.add_podcast_list
,
4152 @dbus.service
.method(gpodder
.dbus_interface
)
4153 def mark_episode_played(self
, filename
):
4154 if filename
is None:
4157 for channel
in self
.channels
:
4158 for episode
in channel
.get_all_episodes():
4159 fn
= episode
.local_filename(create
=False, check_only
=True)
4161 episode
.mark(is_played
=True)
4163 self
.update_episode_list_icons([episode
.url
])
4164 self
.update_podcast_list_model([episode
.channel
.url
])
4170 def main(options
=None):
4171 gobject
.threads_init()
4172 gobject
.set_application_name('gPodder')
4174 if gpodder
.ui
.maemo
:
4175 # Try to enable the custom icon theme for gPodder on Maemo
4176 settings
= gtk
.settings_get_default()
4177 settings
.set_string_property('gtk-icon-theme-name', \
4178 'gpodder', __file__
)
4179 # Extend the search path for the optified icon theme (Maemo 5)
4180 icon_theme
= gtk
.icon_theme_get_default()
4181 icon_theme
.prepend_search_path('/opt/gpodder-icon-theme/')
4183 gtk
.window_set_default_icon_name('gpodder')
4184 gtk
.about_dialog_set_url_hook(lambda dlg
, link
, data
: util
.open_website(link
), None)
4187 dbus_main_loop
= dbus
.glib
.DBusGMainLoop(set_as_default
=True)
4188 gpodder
.dbus_session_bus
= dbus
.SessionBus(dbus_main_loop
)
4190 bus_name
= dbus
.service
.BusName(gpodder
.dbus_bus_name
, bus
=gpodder
.dbus_session_bus
)
4191 except dbus
.exceptions
.DBusException
, dbe
:
4192 log('Warning: Cannot get "on the bus".', traceback
=True)
4193 dlg
= gtk
.MessageDialog(None, gtk
.DIALOG_MODAL
, gtk
.MESSAGE_ERROR
, \
4194 gtk
.BUTTONS_CLOSE
, _('Cannot start gPodder'))
4195 dlg
.format_secondary_markup(_('D-Bus error: %s') % (str(dbe
),))
4196 dlg
.set_title('gPodder')
4201 util
.make_directory(gpodder
.home
)
4202 gpodder
.load_plugins()
4204 config
= UIConfig(gpodder
.config_file
)
4206 # Load hook modules and install the hook manager globally
4207 # if modules have been found an instantiated by the manager
4208 user_hooks
= hooks
.HookManager()
4209 if user_hooks
.has_modules():
4210 gpodder
.user_hooks
= user_hooks
4212 if gpodder
.ui
.diablo
:
4213 # Detect changing of SD cards between mmc1/mmc2 if a gpodder
4214 # folder exists there (allow moving "gpodder" between SD cards or USB)
4215 # Also allow moving "gpodder" to home folder (e.g. rootfs on SD)
4216 if not os
.path
.exists(config
.download_dir
):
4217 log('Downloads might have been moved. Trying to locate them...')
4218 for basedir
in ['/media/mmc1', '/media/mmc2']+glob
.glob('/media/usb/*')+['/home/user/MyDocs']:
4219 dir = os
.path
.join(basedir
, 'gpodder')
4220 if os
.path
.exists(dir):
4221 log('Downloads found in: %s', dir)
4222 config
.download_dir
= dir
4225 log('Downloads NOT FOUND in %s', dir)
4226 elif gpodder
.ui
.fremantle
:
4227 config
.on_quit_ask
= False
4229 if config
.enable_fingerscroll
:
4230 BuilderWidget
.use_fingerscroll
= True
4232 config
.mygpo_device_type
= util
.detect_device_type()
4234 gp
= gPodder(bus_name
, config
)
4237 if options
.subscribe
:
4238 util
.idle_add(gp
.subscribe_to_url
, options
.subscribe
)
4241 # handle "subscribe to podcast" events from firefox
4242 if platform
.system() == 'Darwin':
4243 from gpodder
import gpodderosx
4244 gpodderosx
.register_handlers(gp
)
4245 # end mac OS X stuff