2007-04-01 19:53:04 +02:00
|
|
|
# -*- coding: utf-8 -*-
|
2005-11-21 19:21:25 +01:00
|
|
|
#
|
2007-08-29 20:30:26 +02:00
|
|
|
# gPodder - A media aggregator and podcast client
|
2011-04-01 18:59:42 +02:00
|
|
|
# Copyright (c) 2005-2011 Thomas Perl and the gPodder Team
|
2006-04-07 22:22:30 +02:00
|
|
|
#
|
2007-08-29 20:30:26 +02:00
|
|
|
# gPodder is free software; you can redistribute it and/or modify
|
|
|
|
# it under the terms of the GNU General Public License as published by
|
|
|
|
# the Free Software Foundation; either version 3 of the License, or
|
|
|
|
# (at your option) any later version.
|
2006-04-07 22:22:30 +02:00
|
|
|
#
|
2007-08-29 20:30:26 +02:00
|
|
|
# gPodder is distributed in the hope that it will be useful,
|
2006-04-07 22:22:30 +02:00
|
|
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
|
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
|
|
# GNU General Public License for more details.
|
|
|
|
#
|
|
|
|
# You should have received a copy of the GNU General Public License
|
2007-08-29 20:30:26 +02:00
|
|
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
2005-11-21 19:21:25 +01:00
|
|
|
#
|
|
|
|
|
|
|
|
import os
|
2010-03-21 14:54:16 +01:00
|
|
|
import platform
|
2005-11-21 19:21:25 +01:00
|
|
|
import gtk
|
2007-01-28 10:21:39 +01:00
|
|
|
import gtk.gdk
|
2005-11-21 19:21:25 +01:00
|
|
|
import gobject
|
2006-11-20 12:51:20 +01:00
|
|
|
import pango
|
2010-10-05 12:41:03 +02:00
|
|
|
import random
|
2005-11-21 19:21:25 +01:00
|
|
|
import sys
|
2007-04-23 17:18:31 +02:00
|
|
|
import shutil
|
2008-01-21 10:52:09 +01:00
|
|
|
import subprocess
|
2008-03-03 23:09:34 +01:00
|
|
|
import glob
|
2008-03-20 11:24:26 +01:00
|
|
|
import time
|
2009-08-10 18:32:08 +02:00
|
|
|
import tempfile
|
2009-08-24 18:11:58 +02:00
|
|
|
import collections
|
2009-08-24 20:59:21 +02:00
|
|
|
import threading
|
2010-11-23 16:00:26 +01:00
|
|
|
import urllib
|
2011-02-25 21:05:26 +01:00
|
|
|
import cgi
|
2006-03-04 21:45:01 +01:00
|
|
|
|
2007-07-05 23:07:16 +02:00
|
|
|
|
2008-04-06 02:19:03 +02:00
|
|
|
import gpodder
|
2009-05-09 15:21:04 +02:00
|
|
|
|
2011-07-15 17:55:38 +02:00
|
|
|
import dbus
|
|
|
|
import dbus.service
|
|
|
|
import dbus.mainloop
|
|
|
|
import dbus.glib
|
2009-05-09 15:21:04 +02:00
|
|
|
|
2011-02-06 19:05:52 +01:00
|
|
|
from gpodder import core
|
2009-06-12 00:51:13 +02:00
|
|
|
from gpodder import feedcore
|
2007-08-07 20:11:31 +02:00
|
|
|
from gpodder import util
|
2007-08-19 16:28:24 +02:00
|
|
|
from gpodder import opml
|
2007-09-18 20:25:25 +02:00
|
|
|
from gpodder import download
|
2008-12-08 17:10:53 +01:00
|
|
|
from gpodder import my
|
2010-04-03 00:39:43 +02:00
|
|
|
from gpodder import youtube
|
2010-04-26 11:38:55 +02:00
|
|
|
from gpodder import player
|
2011-07-15 16:32:06 +02:00
|
|
|
|
|
|
|
import logging
|
|
|
|
logger = logging.getLogger(__name__)
|
2008-03-11 18:45:52 +01:00
|
|
|
|
2009-05-07 16:26:07 +02:00
|
|
|
_ = gpodder.gettext
|
2009-12-21 23:18:00 +01:00
|
|
|
N_ = gpodder.ngettext
|
2009-05-07 16:26:07 +02:00
|
|
|
|
2011-02-25 21:14:54 +01:00
|
|
|
from gpodder.gtkui.model import Model
|
2009-08-13 23:36:18 +02:00
|
|
|
from gpodder.gtkui.model import PodcastListModel
|
|
|
|
from gpodder.gtkui.model import EpisodeListModel
|
2009-08-24 18:11:58 +02:00
|
|
|
from gpodder.gtkui.config import UIConfig
|
2009-08-24 16:47:59 +02:00
|
|
|
from gpodder.gtkui.services import CoverDownloader
|
2009-08-24 16:50:06 +02:00
|
|
|
from gpodder.gtkui.widgets import SimpleMessageArea
|
2009-08-24 16:52:37 +02:00
|
|
|
from gpodder.gtkui.desktopfile import UserAppsReader
|
2009-08-24 19:04:28 +02:00
|
|
|
|
2009-09-01 23:38:21 +02:00
|
|
|
from gpodder.gtkui.draw import draw_text_box_centered
|
|
|
|
|
2009-08-24 18:54:45 +02:00
|
|
|
from gpodder.gtkui.interface.common import BuilderWidget
|
2009-09-05 00:12:53 +02:00
|
|
|
from gpodder.gtkui.interface.common import TreeViewHelper
|
2009-08-24 19:10:52 +02:00
|
|
|
from gpodder.gtkui.interface.addpodcast import gPodderAddPodcast
|
2005-11-21 19:21:25 +01:00
|
|
|
|
2011-07-16 18:35:14 +02:00
|
|
|
from gpodder.gtkui.download import DownloadStatusModel
|
|
|
|
|
|
|
|
from gpodder.gtkui.desktop.welcome import gPodderWelcome
|
|
|
|
from gpodder.gtkui.desktop.channel import gPodderChannel
|
|
|
|
from gpodder.gtkui.desktop.preferences import gPodderPreferences
|
|
|
|
from gpodder.gtkui.desktop.shownotes import gPodderShownotes
|
|
|
|
from gpodder.gtkui.desktop.episodeselector import gPodderEpisodeSelector
|
|
|
|
from gpodder.gtkui.desktop.podcastdirectory import gPodderPodcastDirectory
|
|
|
|
from gpodder.gtkui.interface.progress import ProgressIndicator
|
2008-04-06 02:19:03 +02:00
|
|
|
|
2010-01-08 01:58:15 +01:00
|
|
|
from gpodder.dbusproxy import DBusPodcastsProxy
|
2010-07-05 16:08:22 +02:00
|
|
|
from gpodder import hooks
|
2010-01-08 01:58:15 +01:00
|
|
|
|
2009-05-08 14:28:53 +02:00
|
|
|
class gPodder(BuilderWidget, dbus.service.Object):
|
2010-11-22 14:44:12 +01:00
|
|
|
# Delay until live search is started after typing stop
|
2010-11-29 12:51:51 +01:00
|
|
|
LIVE_SEARCH_DELAY = 500
|
2010-11-22 14:44:12 +01:00
|
|
|
|
2011-09-19 10:47:20 +02:00
|
|
|
# Width (in pixels) of episode list icon
|
|
|
|
EPISODE_LIST_ICON_WIDTH = 40
|
|
|
|
|
2011-02-06 19:05:52 +01:00
|
|
|
def __init__(self, bus_name, gpodder_core):
|
2009-02-25 14:57:45 +01:00
|
|
|
dbus.service.Object.__init__(self, object_path=gpodder.dbus_gui_object_path, bus_name=bus_name)
|
2010-01-08 01:58:15 +01:00
|
|
|
self.podcasts_proxy = DBusPodcastsProxy(lambda: self.channels, \
|
|
|
|
self.on_itemUpdate_activate, \
|
|
|
|
self.playback_episodes, \
|
|
|
|
self.download_episode_list, \
|
2010-06-12 17:51:54 +02:00
|
|
|
self.episode_object_by_uri, \
|
2010-01-08 01:58:15 +01:00
|
|
|
bus_name)
|
2011-02-06 19:05:52 +01:00
|
|
|
self.core = gpodder_core
|
|
|
|
self.config = self.core.config
|
|
|
|
self.db = self.core.db
|
2011-10-12 19:59:09 +02:00
|
|
|
self.model = self.core.model
|
2009-08-24 18:37:04 +02:00
|
|
|
BuilderWidget.__init__(self, None)
|
2008-04-24 18:28:39 +02:00
|
|
|
|
2005-11-21 19:21:25 +01:00
|
|
|
def new(self):
|
2011-07-16 18:35:14 +02:00
|
|
|
self.toolbar.set_property('visible', self.config.show_toolbar)
|
2008-05-02 17:36:43 +02:00
|
|
|
|
2010-07-05 20:45:51 +02:00
|
|
|
self.bluetooth_available = util.bluetooth_available()
|
|
|
|
|
2011-07-16 18:35:14 +02:00
|
|
|
self.config.connect_gtk_window(self.gPodder, '_main_window')
|
2011-04-30 18:00:28 +02:00
|
|
|
|
2011-07-16 18:35:14 +02:00
|
|
|
# Default/last paned position for sidebar toggling
|
|
|
|
self._last_paned_position = 200
|
|
|
|
self._last_paned_position_toggling = False
|
|
|
|
self.item_sidebar.set_active(self.config._paned_position > 0)
|
2011-04-30 18:00:28 +02:00
|
|
|
|
2011-07-16 18:35:14 +02:00
|
|
|
self.config.connect_gtk_paned('_paned_position', self.channelPaned)
|
2011-04-11 11:12:11 +02:00
|
|
|
|
2009-09-05 15:42:55 +02:00
|
|
|
self.main_window.show()
|
2009-09-02 18:40:52 +02:00
|
|
|
|
2010-04-26 11:38:55 +02:00
|
|
|
self.player_receiver = player.MediaPlayerDBusReceiver(self.on_played)
|
|
|
|
|
2008-09-27 14:20:43 +02:00
|
|
|
self.gPodder.connect('key-press-event', self.on_key_press)
|
2008-05-02 17:36:43 +02:00
|
|
|
|
2011-04-11 13:09:50 +02:00
|
|
|
self.episode_columns_menu = None
|
2009-08-24 18:11:58 +02:00
|
|
|
self.config.add_observer(self.on_config_changed)
|
2009-08-25 16:19:14 +02:00
|
|
|
|
2009-08-24 19:43:49 +02:00
|
|
|
self.episode_shownotes_window = None
|
2009-09-28 14:23:21 +02:00
|
|
|
self.new_episodes_window = None
|
2009-04-01 13:34:19 +02:00
|
|
|
|
2011-07-16 18:38:19 +02:00
|
|
|
# Mac OS X-specific UI tweaks: Native main menu integration
|
|
|
|
# http://sourceforge.net/apps/trac/gtk-osx/wiki/Integrate
|
|
|
|
if getattr(gtk.gdk, 'WINDOWING', 'x11') == 'quartz':
|
|
|
|
try:
|
|
|
|
import igemacintegration as igemi
|
|
|
|
|
|
|
|
# Move the menu bar from the window to the Mac menu bar
|
|
|
|
self.mainMenu.hide()
|
|
|
|
igemi.ige_mac_menu_set_menu_bar(self.mainMenu)
|
|
|
|
|
|
|
|
# Reparent some items to the "Application" menu
|
|
|
|
for widget in ('/mainMenu/menuHelp/itemAbout', \
|
|
|
|
'/mainMenu/menuPodcasts/itemPreferences'):
|
|
|
|
item = self.uimanager1.get_widget(widget)
|
|
|
|
group = igemi.ige_mac_menu_add_app_menu_group()
|
|
|
|
igemi.ige_mac_menu_add_app_menu_item(group, item, None)
|
|
|
|
|
|
|
|
quit_widget = '/mainMenu/menuPodcasts/itemQuit'
|
|
|
|
quit_item = self.uimanager1.get_widget(quit_widget)
|
|
|
|
igemi.ige_mac_menu_set_quit_menu_item(quit_item)
|
|
|
|
except ImportError:
|
|
|
|
print >>sys.stderr, """
|
|
|
|
Warning: ige-mac-integration not found - no native menus.
|
|
|
|
"""
|
2010-03-15 21:19:35 +01:00
|
|
|
|
2009-08-24 16:17:32 +02:00
|
|
|
self.download_status_model = DownloadStatusModel()
|
2009-08-24 18:11:58 +02:00
|
|
|
self.download_queue_manager = download.DownloadQueueManager(self.config)
|
2007-08-28 00:18:01 +02:00
|
|
|
|
2011-07-16 18:38:19 +02:00
|
|
|
self.itemShowAllEpisodes.set_active(self.config.podcast_list_view_all)
|
|
|
|
self.itemShowToolbar.set_active(self.config.show_toolbar)
|
|
|
|
self.itemShowDescription.set_active(self.config.episode_list_descriptions)
|
2008-02-06 10:29:56 +01:00
|
|
|
|
2011-07-16 18:35:14 +02:00
|
|
|
self.config.connect_gtk_spinbutton('max_downloads', self.spinMaxDownloads)
|
|
|
|
self.config.connect_gtk_togglebutton('max_downloads_enabled', self.cbMaxDownloads)
|
|
|
|
self.config.connect_gtk_spinbutton('limit_rate_value', self.spinLimitDownloads)
|
|
|
|
self.config.connect_gtk_togglebutton('limit_rate', self.cbLimitDownloads)
|
2008-04-17 17:45:29 +02:00
|
|
|
|
2011-07-27 14:19:02 +02:00
|
|
|
self.config.connect_gtk_togglebutton('podcast_list_sections', self.item_podcast_sections)
|
|
|
|
|
2011-07-16 18:35:14 +02:00
|
|
|
# When the amount of maximum downloads changes, notify the queue manager
|
|
|
|
changed_cb = lambda spinbutton: self.download_queue_manager.spawn_threads()
|
|
|
|
self.spinMaxDownloads.connect('value-changed', changed_cb)
|
2008-04-17 17:45:29 +02:00
|
|
|
|
2011-07-16 18:49:19 +02:00
|
|
|
self.default_title = None
|
|
|
|
self.set_title(_('gPodder'))
|
2007-09-02 15:02:28 +02:00
|
|
|
|
2009-09-05 00:12:53 +02:00
|
|
|
self.cover_downloader = CoverDownloader()
|
|
|
|
|
|
|
|
# Generate list models for podcasts and their episodes
|
2010-02-24 01:50:21 +01:00
|
|
|
self.podcast_list_model = PodcastListModel(self.cover_downloader)
|
2009-09-05 00:12:53 +02:00
|
|
|
|
2009-09-05 00:53:46 +02:00
|
|
|
self.cover_downloader.register('cover-available', self.cover_download_finished)
|
|
|
|
self.cover_downloader.register('cover-removed', self.cover_file_removed)
|
|
|
|
|
2010-11-22 14:44:12 +01:00
|
|
|
# Source IDs for timeouts for search-as-you-type
|
|
|
|
self._podcast_list_search_timeout = None
|
|
|
|
self._episode_list_search_timeout = None
|
|
|
|
|
2009-09-05 00:12:53 +02:00
|
|
|
# Init the treeviews that we use
|
|
|
|
self.init_podcast_list_treeview()
|
|
|
|
self.init_episode_list_treeview()
|
|
|
|
self.init_download_list_treeview()
|
|
|
|
|
2009-09-09 16:44:48 +02:00
|
|
|
if self.config.podcast_list_hide_boring:
|
|
|
|
self.item_view_hide_boring_podcasts.set_active(True)
|
|
|
|
|
2009-09-05 00:12:53 +02:00
|
|
|
self.currently_updating = False
|
|
|
|
|
|
|
|
self.download_tasks_seen = set()
|
|
|
|
self.download_list_update_enabled = False
|
2009-12-22 17:01:47 +01:00
|
|
|
self.download_task_monitors = set()
|
2009-09-05 00:12:53 +02:00
|
|
|
|
|
|
|
# Subscribed channels
|
|
|
|
self.active_channel = None
|
2011-10-12 19:59:09 +02:00
|
|
|
self.channels = self.model.get_podcasts()
|
2011-02-25 00:45:49 +01:00
|
|
|
|
2011-10-19 12:37:55 +02:00
|
|
|
gpodder.user_hooks.on_ui_initialized(self.model,
|
|
|
|
self.hooks_podcast_update_cb,
|
|
|
|
self.hooks_episode_download_cb)
|
2011-08-02 15:45:49 +02:00
|
|
|
|
2011-02-25 00:45:49 +01:00
|
|
|
# Check if the user has downloaded any podcast with an external program
|
|
|
|
# and mark episodes as downloaded / move them away (bug 902)
|
|
|
|
for podcast in self.channels:
|
|
|
|
podcast.import_external_files()
|
|
|
|
|
2009-09-05 00:12:53 +02:00
|
|
|
# load list of user applications for audio playback
|
|
|
|
self.user_apps_reader = UserAppsReader(['audio', 'video'])
|
2010-01-28 17:39:10 +01:00
|
|
|
threading.Thread(target=self.user_apps_reader.read).start()
|
2009-09-05 00:12:53 +02:00
|
|
|
|
2010-01-28 19:31:58 +01:00
|
|
|
# Set up the first instance of MygPoClient
|
|
|
|
self.mygpo_client = my.MygPoClient(self.config)
|
|
|
|
|
2009-09-05 00:12:53 +02:00
|
|
|
# Now, update the feed cache, when everything's in place
|
2011-07-16 18:35:14 +02:00
|
|
|
self.btnUpdateFeeds.show()
|
2009-09-05 00:12:53 +02:00
|
|
|
self.feed_cache_update_cancelled = False
|
2011-07-16 20:51:26 +02:00
|
|
|
self.update_podcast_list_model()
|
2009-09-05 00:12:53 +02:00
|
|
|
|
|
|
|
self.message_area = None
|
|
|
|
|
2010-03-07 21:35:52 +01:00
|
|
|
def find_partial_downloads():
|
|
|
|
# Look for partial file downloads
|
2010-12-20 03:09:35 +01:00
|
|
|
partial_files = glob.glob(os.path.join(gpodder.downloads, '*', '*.partial'))
|
2010-03-07 21:35:52 +01:00
|
|
|
count = len(partial_files)
|
|
|
|
resumable_episodes = []
|
|
|
|
if count:
|
2011-07-16 18:35:14 +02:00
|
|
|
util.idle_add(self.wNotebook.set_current_page, 1)
|
2010-03-07 21:35:52 +01:00
|
|
|
indicator = ProgressIndicator(_('Loading incomplete downloads'), \
|
|
|
|
_('Some episodes have not finished downloading in a previous session.'), \
|
2010-05-10 13:33:50 +02:00
|
|
|
False, self.get_dialog_parent())
|
2010-11-22 21:52:58 +01:00
|
|
|
indicator.on_message(N_('%(count)d partial file', '%(count)d partial files', count) % {'count':count})
|
2010-03-07 21:43:34 +01:00
|
|
|
|
|
|
|
candidates = [f[:-len('.partial')] for f in partial_files]
|
|
|
|
found = 0
|
|
|
|
|
|
|
|
for c in self.channels:
|
|
|
|
for e in c.get_all_episodes():
|
|
|
|
filename = e.local_filename(create=False, check_only=True)
|
|
|
|
if filename in candidates:
|
|
|
|
found += 1
|
|
|
|
indicator.on_message(e.title)
|
|
|
|
indicator.on_progress(float(found)/count)
|
|
|
|
candidates.remove(filename)
|
|
|
|
partial_files.remove(filename+'.partial')
|
2011-03-08 15:39:06 +01:00
|
|
|
|
|
|
|
if os.path.exists(filename):
|
|
|
|
# The file has already been downloaded;
|
|
|
|
# remove the leftover partial file
|
|
|
|
util.delete_file(filename+'.partial')
|
|
|
|
else:
|
|
|
|
resumable_episodes.append(e)
|
2010-03-07 21:43:34 +01:00
|
|
|
|
|
|
|
if not candidates:
|
2009-09-05 00:12:53 +02:00
|
|
|
break
|
2010-03-07 21:43:34 +01:00
|
|
|
|
|
|
|
if not candidates:
|
|
|
|
break
|
|
|
|
|
|
|
|
for f in partial_files:
|
2011-07-15 16:32:06 +02:00
|
|
|
logger.warn('Partial file without episode: %s', f)
|
2010-03-07 21:43:34 +01:00
|
|
|
util.delete_file(f)
|
2010-03-07 21:35:52 +01:00
|
|
|
|
|
|
|
util.idle_add(indicator.on_finished)
|
|
|
|
|
|
|
|
if len(resumable_episodes):
|
|
|
|
def offer_resuming():
|
|
|
|
self.download_episode_list_paused(resumable_episodes)
|
2011-07-16 18:35:14 +02:00
|
|
|
resume_all = gtk.Button(_('Resume all'))
|
|
|
|
def on_resume_all(button):
|
|
|
|
selection = self.treeDownloads.get_selection()
|
|
|
|
selection.select_all()
|
|
|
|
selected_tasks, can_queue, can_cancel, can_pause, can_remove, can_force = self.downloads_list_get_selection()
|
|
|
|
selection.unselect_all()
|
|
|
|
self._for_each_task_set_status(selected_tasks, download.DownloadTask.QUEUED)
|
|
|
|
self.message_area.hide()
|
|
|
|
resume_all.connect('clicked', on_resume_all)
|
|
|
|
|
|
|
|
self.message_area = SimpleMessageArea(_('Incomplete downloads from a previous session were found.'), (resume_all,))
|
|
|
|
self.vboxDownloadStatusWidgets.pack_start(self.message_area, expand=False)
|
|
|
|
self.vboxDownloadStatusWidgets.reorder_child(self.message_area, 0)
|
|
|
|
self.message_area.show_all()
|
2010-03-07 21:35:52 +01:00
|
|
|
self.clean_up_downloads(delete_partial=False)
|
|
|
|
util.idle_add(offer_resuming)
|
2011-07-16 18:35:14 +02:00
|
|
|
else:
|
2010-04-10 21:58:42 +02:00
|
|
|
util.idle_add(self.wNotebook.set_current_page, 0)
|
2010-03-07 21:35:52 +01:00
|
|
|
else:
|
|
|
|
util.idle_add(self.clean_up_downloads, True)
|
|
|
|
threading.Thread(target=find_partial_downloads).start()
|
2009-09-05 00:12:53 +02:00
|
|
|
|
|
|
|
# Start the auto-update procedure
|
2009-10-13 22:48:12 +02:00
|
|
|
self._auto_update_timer_source_id = None
|
|
|
|
if self.config.auto_update_feeds:
|
|
|
|
self.restart_auto_update_timer()
|
2009-09-05 00:12:53 +02:00
|
|
|
|
|
|
|
# Delete old episodes if the user wishes to
|
2010-03-02 01:36:23 +01:00
|
|
|
if self.config.auto_remove_played_episodes and \
|
|
|
|
self.config.episode_old_age > 0:
|
2010-03-01 21:29:46 +01:00
|
|
|
old_episodes = list(self.get_expired_episodes())
|
2009-09-05 00:12:53 +02:00
|
|
|
if len(old_episodes) > 0:
|
|
|
|
self.delete_episode_list(old_episodes, confirm=False)
|
|
|
|
self.update_podcast_list_model(set(e.channel.url for e in old_episodes))
|
|
|
|
|
2010-01-28 17:39:10 +01:00
|
|
|
# Do the initial sync with the web service
|
|
|
|
util.idle_add(self.mygpo_client.flush, True)
|
2010-01-19 23:43:59 +01:00
|
|
|
|
2009-09-05 00:12:53 +02:00
|
|
|
# First-time users should be asked if they want to see the OPML
|
2011-07-16 18:35:14 +02:00
|
|
|
if not self.channels:
|
2011-10-19 14:47:16 +02:00
|
|
|
self.on_itemUpdate_activate()
|
2009-09-05 00:12:53 +02:00
|
|
|
|
2011-04-30 18:00:28 +02:00
|
|
|
def on_view_sidebar_toggled(self, menu_item):
|
|
|
|
self.channelPaned.child_set_property(self.vboxChannelNavigator, \
|
|
|
|
'shrink', not menu_item.get_active())
|
|
|
|
|
|
|
|
if self._last_paned_position_toggling:
|
|
|
|
return
|
|
|
|
|
|
|
|
active = menu_item.get_active()
|
|
|
|
if active:
|
|
|
|
if self._last_paned_position == 0:
|
|
|
|
self._last_paned_position = 200
|
|
|
|
self.channelPaned.set_position(self._last_paned_position)
|
|
|
|
else:
|
|
|
|
current_position = self.channelPaned.get_position()
|
|
|
|
if current_position > 0:
|
|
|
|
self._last_paned_position = current_position
|
|
|
|
self.channelPaned.set_position(0)
|
|
|
|
|
2010-06-12 17:51:54 +02:00
|
|
|
def episode_object_by_uri(self, uri):
|
|
|
|
"""Get an episode object given a local or remote URI
|
|
|
|
|
|
|
|
This can be used to quickly access an episode object
|
|
|
|
when all we have is its download filename or episode
|
|
|
|
URL (e.g. from external D-Bus calls / signals, etc..)
|
|
|
|
"""
|
|
|
|
if uri.startswith('/'):
|
2011-02-25 16:33:01 +01:00
|
|
|
uri = 'file://' + urllib.quote(uri)
|
2010-06-12 17:51:54 +02:00
|
|
|
|
2011-02-25 16:33:01 +01:00
|
|
|
prefix = 'file://' + urllib.quote(gpodder.downloads)
|
2010-06-12 17:51:54 +02:00
|
|
|
|
2011-07-26 12:17:17 +02:00
|
|
|
# By default, assume we can't pre-select any channel
|
|
|
|
# but can match episodes simply via the download URL
|
|
|
|
is_channel = lambda c: True
|
|
|
|
is_episode = lambda e: e.url == uri
|
|
|
|
|
2010-06-12 17:51:54 +02:00
|
|
|
if uri.startswith(prefix):
|
|
|
|
# File is on the local filesystem in the download folder
|
2011-07-26 12:17:17 +02:00
|
|
|
# Try to reduce search space by pre-selecting the channel
|
|
|
|
# based on the folder name of the local file
|
|
|
|
|
2010-11-23 16:00:26 +01:00
|
|
|
filename = urllib.unquote(uri[len(prefix):])
|
2011-07-26 12:17:17 +02:00
|
|
|
file_parts = filter(None, filename.split(os.sep))
|
2010-06-12 17:51:54 +02:00
|
|
|
|
2011-07-26 12:17:17 +02:00
|
|
|
if len(file_parts) != 2:
|
|
|
|
return None
|
|
|
|
|
|
|
|
foldername, filename = file_parts
|
|
|
|
|
|
|
|
is_channel = lambda c: c.download_folder == foldername
|
|
|
|
is_episode = lambda e: e.download_filename == filename
|
2010-06-12 17:51:54 +02:00
|
|
|
|
2011-07-26 12:17:17 +02:00
|
|
|
# Deep search through channels and episodes for a match
|
|
|
|
for channel in filter(is_channel, self.channels):
|
|
|
|
for episode in filter(is_episode, channel.get_all_episodes()):
|
|
|
|
return episode
|
2010-06-12 17:51:54 +02:00
|
|
|
|
|
|
|
return None
|
|
|
|
|
2010-04-26 11:38:55 +02:00
|
|
|
def on_played(self, start, end, total, file_uri):
|
|
|
|
"""Handle the "played" signal from a media player"""
|
2010-06-13 02:39:19 +02:00
|
|
|
if start == 0 and end == 0 and total == 0:
|
|
|
|
# Ignore bogus play event
|
|
|
|
return
|
2010-08-17 03:20:41 +02:00
|
|
|
elif end < start + 5:
|
|
|
|
# Ignore "less than five seconds" segments,
|
|
|
|
# as they can happen with seeking, etc...
|
|
|
|
return
|
2010-06-13 02:39:19 +02:00
|
|
|
|
2011-07-15 16:32:06 +02:00
|
|
|
logger.debug('Received play action: %s (%d, %d, %d)', file_uri, start, end, total)
|
2010-06-12 17:51:54 +02:00
|
|
|
episode = self.episode_object_by_uri(file_uri)
|
|
|
|
|
|
|
|
if episode is not None:
|
|
|
|
file_type = episode.file_type()
|
|
|
|
|
|
|
|
now = time.time()
|
|
|
|
if total > 0:
|
|
|
|
episode.total_time = total
|
2010-08-17 03:07:05 +02:00
|
|
|
elif total == 0:
|
|
|
|
# Assume the episode's total time for the action
|
|
|
|
total = episode.total_time
|
2011-08-01 10:34:09 +02:00
|
|
|
|
|
|
|
assert (episode.current_position_updated is None or
|
|
|
|
now >= episode.current_position_updated)
|
|
|
|
|
|
|
|
episode.current_position = end
|
|
|
|
episode.current_position_updated = now
|
2010-06-12 17:51:54 +02:00
|
|
|
episode.mark(is_played=True)
|
|
|
|
episode.save()
|
|
|
|
self.db.commit()
|
|
|
|
self.update_episode_list_icons([episode.url])
|
|
|
|
self.update_podcast_list_model([episode.channel.url])
|
|
|
|
|
|
|
|
# Submit this action to the webservice
|
|
|
|
self.mygpo_client.on_playback_full(episode, \
|
|
|
|
start, end, total)
|
2010-04-26 11:38:55 +02:00
|
|
|
|
2010-01-28 17:39:10 +01:00
|
|
|
def on_add_remove_podcasts_mygpo(self):
|
|
|
|
actions = self.mygpo_client.get_received_actions()
|
|
|
|
if not actions:
|
|
|
|
return False
|
|
|
|
|
2010-01-19 23:43:59 +01:00
|
|
|
existing_urls = [c.url for c in self.channels]
|
|
|
|
|
|
|
|
# Columns for the episode selector window - just one...
|
|
|
|
columns = (
|
|
|
|
('description', None, None, _('Action')),
|
|
|
|
)
|
|
|
|
|
|
|
|
# A list of actions that have to be chosen from
|
2010-01-28 17:39:10 +01:00
|
|
|
changes = []
|
2010-01-19 23:43:59 +01:00
|
|
|
|
2010-01-28 17:39:10 +01:00
|
|
|
# Actions that are ignored (already carried out)
|
|
|
|
ignored = []
|
2010-01-19 23:43:59 +01:00
|
|
|
|
2010-01-28 17:39:10 +01:00
|
|
|
for action in actions:
|
|
|
|
if action.is_add and action.url not in existing_urls:
|
|
|
|
changes.append(my.Change(action))
|
|
|
|
elif action.is_remove and action.url in existing_urls:
|
2010-01-19 23:43:59 +01:00
|
|
|
podcast_object = None
|
|
|
|
for podcast in self.channels:
|
2010-01-28 17:39:10 +01:00
|
|
|
if podcast.url == action.url:
|
2010-01-19 23:43:59 +01:00
|
|
|
podcast_object = podcast
|
|
|
|
break
|
2010-01-28 17:39:10 +01:00
|
|
|
changes.append(my.Change(action, podcast_object))
|
|
|
|
else:
|
|
|
|
ignored.append(action)
|
|
|
|
|
|
|
|
# Confirm all ignored changes
|
|
|
|
self.mygpo_client.confirm_received_actions(ignored)
|
2010-01-19 23:43:59 +01:00
|
|
|
|
|
|
|
def execute_podcast_actions(selected):
|
2010-01-28 17:39:10 +01:00
|
|
|
add_list = [c.action.url for c in selected if c.action.is_add]
|
|
|
|
remove_list = [c.podcast for c in selected if c.action.is_remove]
|
|
|
|
|
2010-01-19 23:43:59 +01:00
|
|
|
# Apply the accepted changes locally
|
2010-01-28 17:39:10 +01:00
|
|
|
self.add_podcast_list(add_list)
|
2010-01-19 23:43:59 +01:00
|
|
|
self.remove_podcast_list(remove_list, confirm=False)
|
|
|
|
|
2010-01-28 17:39:10 +01:00
|
|
|
# All selected items are now confirmed
|
|
|
|
self.mygpo_client.confirm_received_actions(c.action for c in selected)
|
|
|
|
|
|
|
|
# Revert the changes on the server
|
|
|
|
rejected = [c.action for c in changes if c not in selected]
|
|
|
|
self.mygpo_client.reject_received_actions(rejected)
|
2010-01-19 23:43:59 +01:00
|
|
|
|
|
|
|
def ask():
|
|
|
|
# We're abusing the Episode Selector again ;) -- thp
|
|
|
|
gPodderEpisodeSelector(self.main_window, \
|
2010-04-02 23:19:04 +02:00
|
|
|
title=_('Confirm changes from gpodder.net'), \
|
2010-01-19 23:43:59 +01:00
|
|
|
instructions=_('Select the actions you want to carry out.'), \
|
2010-01-28 17:39:10 +01:00
|
|
|
episodes=changes, \
|
2010-01-19 23:43:59 +01:00
|
|
|
columns=columns, \
|
|
|
|
size_attribute=None, \
|
|
|
|
stock_ok_button=gtk.STOCK_APPLY, \
|
|
|
|
callback=execute_podcast_actions, \
|
|
|
|
_config=self.config)
|
|
|
|
|
2010-01-28 17:39:10 +01:00
|
|
|
# There are some actions that need the user's attention
|
|
|
|
if changes:
|
2010-01-19 23:43:59 +01:00
|
|
|
util.idle_add(ask)
|
2010-01-28 17:39:10 +01:00
|
|
|
return True
|
2010-01-19 23:43:59 +01:00
|
|
|
|
2010-01-28 17:39:10 +01:00
|
|
|
# We have no remaining actions - no selection happens
|
|
|
|
return False
|
2010-01-19 23:43:59 +01:00
|
|
|
|
2010-01-28 17:39:10 +01:00
|
|
|
def rewrite_urls_mygpo(self):
|
|
|
|
# Check if we have to rewrite URLs since the last add
|
|
|
|
rewritten_urls = self.mygpo_client.get_rewritten_urls()
|
|
|
|
|
|
|
|
for rewritten_url in rewritten_urls:
|
|
|
|
if not rewritten_url.new_url:
|
|
|
|
continue
|
|
|
|
|
|
|
|
for channel in self.channels:
|
|
|
|
if channel.url == rewritten_url.old_url:
|
2011-07-15 16:32:06 +02:00
|
|
|
logger.info('Updating URL of %s to %s', channel,
|
|
|
|
rewritten_url.new_url)
|
2010-01-28 17:39:10 +01:00
|
|
|
channel.url = rewritten_url.new_url
|
|
|
|
channel.save()
|
|
|
|
util.idle_add(self.update_episode_list_model)
|
|
|
|
break
|
2010-01-19 23:43:59 +01:00
|
|
|
|
|
|
|
def on_send_full_subscriptions(self):
|
2010-04-02 23:19:04 +02:00
|
|
|
# Send the full subscription list to the gpodder.net client
|
2010-01-25 00:53:25 +01:00
|
|
|
# (this will overwrite the subscription list on the server)
|
|
|
|
indicator = ProgressIndicator(_('Uploading subscriptions'), \
|
|
|
|
_('Your subscriptions are being uploaded to the server.'), \
|
2010-05-10 13:33:50 +02:00
|
|
|
False, self.get_dialog_parent())
|
2010-01-25 00:53:25 +01:00
|
|
|
|
|
|
|
try:
|
|
|
|
self.mygpo_client.set_subscriptions([c.url for c in self.channels])
|
|
|
|
util.idle_add(self.show_message, _('List uploaded successfully.'))
|
|
|
|
except Exception, e:
|
2010-01-28 17:39:10 +01:00
|
|
|
def show_error(e):
|
|
|
|
message = str(e)
|
|
|
|
if not message:
|
|
|
|
message = e.__class__.__name__
|
|
|
|
self.show_message(message, \
|
|
|
|
_('Error while uploading'), \
|
|
|
|
important=True)
|
|
|
|
util.idle_add(show_error, e)
|
2010-01-25 00:53:25 +01:00
|
|
|
|
|
|
|
util.idle_add(indicator.on_finished)
|
2010-01-19 23:43:59 +01:00
|
|
|
|
2009-12-17 17:17:47 +01:00
|
|
|
def on_podcast_selected(self, treeview, path, column):
|
|
|
|
# for Maemo 5's UI
|
|
|
|
model = treeview.get_model()
|
|
|
|
channel = model.get_value(model.get_iter(path), \
|
|
|
|
PodcastListModel.C_CHANNEL)
|
|
|
|
self.active_channel = channel
|
|
|
|
self.update_episode_list_model()
|
|
|
|
self.episodes_window.channel = self.active_channel
|
|
|
|
self.episodes_window.show()
|
|
|
|
|
2009-09-16 23:51:18 +02:00
|
|
|
def on_button_subscribe_clicked(self, button):
|
|
|
|
self.on_itemImportChannels_activate(button)
|
|
|
|
|
|
|
|
def on_button_downloads_clicked(self, widget):
|
|
|
|
self.downloads_window.show()
|
|
|
|
|
2009-12-22 17:01:47 +01:00
|
|
|
def show_episode_in_download_manager(self, episode):
|
|
|
|
self.downloads_window.show()
|
|
|
|
model = self.treeDownloads.get_model()
|
|
|
|
selection = self.treeDownloads.get_selection()
|
|
|
|
selection.unselect_all()
|
|
|
|
it = model.get_iter_first()
|
|
|
|
while it is not None:
|
|
|
|
task = model.get_value(it, DownloadStatusModel.C_TASK)
|
|
|
|
if task.episode.url == episode.url:
|
|
|
|
selection.select_iter(it)
|
|
|
|
# FIXME: Scroll to selection in pannable area
|
|
|
|
break
|
|
|
|
it = model.iter_next(it)
|
|
|
|
|
|
|
|
def for_each_episode_set_task_status(self, episodes, status):
|
|
|
|
episode_urls = set(episode.url for episode in episodes)
|
|
|
|
model = self.treeDownloads.get_model()
|
|
|
|
selected_tasks = [(gtk.TreeRowReference(model, row.path), \
|
|
|
|
model.get_value(row.iter, \
|
|
|
|
DownloadStatusModel.C_TASK)) for row in model \
|
|
|
|
if model.get_value(row.iter, DownloadStatusModel.C_TASK).url \
|
|
|
|
in episode_urls]
|
|
|
|
self._for_each_task_set_status(selected_tasks, status)
|
|
|
|
|
2009-09-05 00:12:53 +02:00
|
|
|
def on_treeview_button_pressed(self, treeview, event):
|
2009-09-28 14:31:47 +02:00
|
|
|
if event.window != treeview.get_bin_window():
|
|
|
|
return False
|
|
|
|
|
2011-09-19 10:47:20 +02:00
|
|
|
role = getattr(treeview, TreeViewHelper.ROLE)
|
|
|
|
if role == TreeViewHelper.ROLE_PODCASTS:
|
2009-09-05 00:12:53 +02:00
|
|
|
return self.currently_updating
|
2011-09-19 10:47:20 +02:00
|
|
|
elif (role == TreeViewHelper.ROLE_EPISODES and event.button == 1):
|
|
|
|
# Toggle episode "new" status by clicking the icon (bug 1432)
|
|
|
|
result = treeview.get_path_at_pos(int(event.x), int(event.y))
|
|
|
|
if result is not None:
|
|
|
|
path, column, x, y = result
|
|
|
|
# The user clicked the icon if she clicked in the first column
|
|
|
|
# and the x position is in the area where the icon resides
|
|
|
|
if (x < self.EPISODE_LIST_ICON_WIDTH and
|
|
|
|
column == treeview.get_columns()[0]):
|
|
|
|
model = treeview.get_model()
|
|
|
|
cursor_episode = model.get_value(model.get_iter(path),
|
|
|
|
EpisodeListModel.C_EPISODE)
|
|
|
|
|
|
|
|
new_value = cursor_episode.is_new
|
|
|
|
selected_episodes = self.get_selected_episodes()
|
|
|
|
|
|
|
|
# Avoid changing anything if the clicked episode is not
|
|
|
|
# selected already - otherwise update all selected
|
|
|
|
if cursor_episode in selected_episodes:
|
|
|
|
for episode in selected_episodes:
|
|
|
|
episode.mark(is_played=new_value)
|
|
|
|
|
|
|
|
self.update_episode_list_icons(selected=True)
|
|
|
|
self.update_podcast_list_model(selected=True)
|
|
|
|
return True
|
2009-09-05 00:12:53 +02:00
|
|
|
|
2011-09-19 10:20:04 +02:00
|
|
|
return event.button == 3
|
2009-09-05 00:12:53 +02:00
|
|
|
|
|
|
|
def on_treeview_podcasts_button_released(self, treeview, event):
|
2009-09-28 14:31:47 +02:00
|
|
|
if event.window != treeview.get_bin_window():
|
|
|
|
return False
|
|
|
|
|
2009-09-05 00:12:53 +02:00
|
|
|
return self.treeview_channels_show_context_menu(treeview, event)
|
|
|
|
|
|
|
|
def on_treeview_episodes_button_released(self, treeview, event):
|
2009-09-28 14:31:47 +02:00
|
|
|
if event.window != treeview.get_bin_window():
|
|
|
|
return False
|
|
|
|
|
2009-09-05 00:12:53 +02:00
|
|
|
return self.treeview_available_show_context_menu(treeview, event)
|
|
|
|
|
|
|
|
def on_treeview_downloads_button_released(self, treeview, event):
|
2009-09-28 14:31:47 +02:00
|
|
|
if event.window != treeview.get_bin_window():
|
|
|
|
return False
|
|
|
|
|
2009-09-05 00:12:53 +02:00
|
|
|
return self.treeview_downloads_show_context_menu(treeview, event)
|
|
|
|
|
2009-11-12 03:22:14 +01:00
|
|
|
def on_entry_search_podcasts_changed(self, editable):
|
|
|
|
if self.hbox_search_podcasts.get_property('visible'):
|
2010-11-22 14:44:12 +01:00
|
|
|
def set_search_term(self, text):
|
|
|
|
self.podcast_list_model.set_search_term(text)
|
|
|
|
self._podcast_list_search_timeout = None
|
|
|
|
return False
|
|
|
|
|
|
|
|
if self._podcast_list_search_timeout is not None:
|
|
|
|
gobject.source_remove(self._podcast_list_search_timeout)
|
|
|
|
self._podcast_list_search_timeout = gobject.timeout_add(\
|
|
|
|
self.LIVE_SEARCH_DELAY, \
|
|
|
|
set_search_term, self, editable.get_chars(0, -1))
|
2009-11-12 03:22:14 +01:00
|
|
|
|
|
|
|
def on_entry_search_podcasts_key_press(self, editable, event):
|
|
|
|
if event.keyval == gtk.keysyms.Escape:
|
|
|
|
self.hide_podcast_search()
|
|
|
|
return True
|
|
|
|
|
|
|
|
def hide_podcast_search(self, *args):
|
2010-11-22 14:44:12 +01:00
|
|
|
if self._podcast_list_search_timeout is not None:
|
|
|
|
gobject.source_remove(self._podcast_list_search_timeout)
|
|
|
|
self._podcast_list_search_timeout = None
|
2009-11-12 03:22:14 +01:00
|
|
|
self.hbox_search_podcasts.hide()
|
|
|
|
self.entry_search_podcasts.set_text('')
|
|
|
|
self.podcast_list_model.set_search_term(None)
|
|
|
|
self.treeChannels.grab_focus()
|
|
|
|
|
|
|
|
def show_podcast_search(self, input_char):
|
|
|
|
self.hbox_search_podcasts.show()
|
|
|
|
self.entry_search_podcasts.insert_text(input_char, -1)
|
|
|
|
self.entry_search_podcasts.grab_focus()
|
|
|
|
self.entry_search_podcasts.set_position(-1)
|
|
|
|
|
2009-09-05 00:12:53 +02:00
|
|
|
def init_podcast_list_treeview(self):
|
2009-08-13 22:17:41 +02:00
|
|
|
# Set up podcast channel tree view widget
|
2011-07-27 16:26:20 +02:00
|
|
|
column = gtk.TreeViewColumn('')
|
2007-07-05 23:07:16 +02:00
|
|
|
iconcell = gtk.CellRendererPixbuf()
|
2011-07-27 16:26:20 +02:00
|
|
|
iconcell.set_property('width', 45)
|
|
|
|
column.pack_start(iconcell, False)
|
|
|
|
column.add_attribute(iconcell, 'pixbuf', PodcastListModel.C_COVER)
|
|
|
|
column.add_attribute(iconcell, 'visible', PodcastListModel.C_COVER_VISIBLE)
|
2007-07-05 23:07:16 +02:00
|
|
|
|
|
|
|
namecell = gtk.CellRendererText()
|
|
|
|
namecell.set_property('ellipsize', pango.ELLIPSIZE_END)
|
2011-07-27 16:26:20 +02:00
|
|
|
column.pack_start(namecell, True)
|
|
|
|
column.add_attribute(namecell, 'markup', PodcastListModel.C_DESCRIPTION)
|
2007-07-05 23:07:16 +02:00
|
|
|
|
2011-07-16 18:35:14 +02:00
|
|
|
iconcell = gtk.CellRendererPixbuf()
|
|
|
|
iconcell.set_property('xalign', 1.0)
|
2011-07-27 16:26:20 +02:00
|
|
|
column.pack_start(iconcell, False)
|
|
|
|
column.add_attribute(iconcell, 'pixbuf', PodcastListModel.C_PILL)
|
|
|
|
column.add_attribute(iconcell, 'visible', PodcastListModel.C_PILL_VISIBLE)
|
2010-09-27 02:08:34 +02:00
|
|
|
|
2011-07-27 16:26:20 +02:00
|
|
|
self.treeChannels.append_column(column)
|
2009-08-13 22:17:41 +02:00
|
|
|
|
2009-09-01 18:56:30 +02:00
|
|
|
self.treeChannels.set_model(self.podcast_list_model.get_filtered_model())
|
|
|
|
|
2009-09-05 00:12:53 +02:00
|
|
|
# When no podcast is selected, clear the episode list model
|
2009-09-01 18:56:30 +02:00
|
|
|
selection = self.treeChannels.get_selection()
|
2011-07-27 14:19:02 +02:00
|
|
|
def select_function(selection, model, path, path_currently_selected):
|
|
|
|
url = model.get_value(model.get_iter(path), PodcastListModel.C_URL)
|
|
|
|
return (url != '-')
|
|
|
|
selection.set_select_function(select_function, full=True)
|
2005-11-21 19:21:25 +01:00
|
|
|
|
2009-11-12 03:22:14 +01:00
|
|
|
# Set up type-ahead find for the podcast list
|
|
|
|
def on_key_press(treeview, event):
|
2011-07-16 18:38:19 +02:00
|
|
|
if event.keyval == gtk.keysyms.Right:
|
2011-04-11 14:22:31 +02:00
|
|
|
self.treeAvailable.grab_focus()
|
2011-07-27 16:26:20 +02:00
|
|
|
elif event.keyval in (gtk.keysyms.Up, gtk.keysyms.Down):
|
|
|
|
# If section markers exist in the treeview, we want to
|
|
|
|
# "jump over" them when moving the cursor up and down
|
|
|
|
selection = self.treeChannels.get_selection()
|
|
|
|
model, it = selection.get_selected()
|
|
|
|
|
|
|
|
if event.keyval == gtk.keysyms.Up:
|
|
|
|
step = -1
|
|
|
|
else:
|
|
|
|
step = 1
|
|
|
|
|
|
|
|
path = model.get_path(it)
|
|
|
|
while True:
|
|
|
|
path = (path[0]+step,)
|
|
|
|
|
|
|
|
if path[0] < 0:
|
|
|
|
# Valid paths must have a value >= 0
|
|
|
|
return True
|
|
|
|
|
|
|
|
try:
|
|
|
|
it = model.get_iter(path)
|
|
|
|
except ValueError:
|
|
|
|
# Already at the end of the list
|
|
|
|
return True
|
|
|
|
|
|
|
|
if model.get_value(it, PodcastListModel.C_URL) != '-':
|
|
|
|
break
|
|
|
|
|
|
|
|
self.treeChannels.set_cursor(path)
|
2011-04-11 14:22:31 +02:00
|
|
|
elif event.keyval == gtk.keysyms.Escape:
|
2009-11-12 03:22:14 +01:00
|
|
|
self.hide_podcast_search()
|
2009-11-25 10:14:57 +01:00
|
|
|
elif event.state & gtk.gdk.CONTROL_MASK:
|
|
|
|
# Don't handle type-ahead when control is pressed (so shortcuts
|
|
|
|
# with the Ctrl key still work, e.g. Ctrl+A, ...)
|
|
|
|
return True
|
2009-11-12 03:22:14 +01:00
|
|
|
else:
|
|
|
|
unicode_char_id = gtk.gdk.keyval_to_unicode(event.keyval)
|
|
|
|
if unicode_char_id == 0:
|
|
|
|
return False
|
|
|
|
input_char = unichr(unicode_char_id)
|
|
|
|
self.show_podcast_search(input_char)
|
|
|
|
return True
|
|
|
|
self.treeChannels.connect('key-press-event', on_key_press)
|
|
|
|
|
2011-04-11 14:16:07 +02:00
|
|
|
self.treeChannels.connect('popup-menu', self.treeview_channels_show_context_menu)
|
|
|
|
|
2009-12-16 14:55:55 +01:00
|
|
|
# Enable separators to the podcast list to separate special podcasts
|
|
|
|
# from others (this is used for the "all episodes" view)
|
|
|
|
self.treeChannels.set_row_separator_func(PodcastListModel.row_separator_func)
|
|
|
|
|
2009-09-05 00:12:53 +02:00
|
|
|
TreeViewHelper.set(self.treeChannels, TreeViewHelper.ROLE_PODCASTS)
|
|
|
|
|
2009-11-05 11:00:15 +01:00
|
|
|
def on_entry_search_episodes_changed(self, editable):
|
|
|
|
if self.hbox_search_episodes.get_property('visible'):
|
2010-11-22 14:44:12 +01:00
|
|
|
def set_search_term(self, text):
|
|
|
|
self.episode_list_model.set_search_term(text)
|
|
|
|
self._episode_list_search_timeout = None
|
|
|
|
return False
|
|
|
|
|
|
|
|
if self._episode_list_search_timeout is not None:
|
|
|
|
gobject.source_remove(self._episode_list_search_timeout)
|
|
|
|
self._episode_list_search_timeout = gobject.timeout_add(\
|
|
|
|
self.LIVE_SEARCH_DELAY, \
|
|
|
|
set_search_term, self, editable.get_chars(0, -1))
|
2009-11-05 11:00:15 +01:00
|
|
|
|
|
|
|
def on_entry_search_episodes_key_press(self, editable, event):
|
|
|
|
if event.keyval == gtk.keysyms.Escape:
|
|
|
|
self.hide_episode_search()
|
|
|
|
return True
|
|
|
|
|
|
|
|
def hide_episode_search(self, *args):
|
2010-11-22 14:44:12 +01:00
|
|
|
if self._episode_list_search_timeout is not None:
|
|
|
|
gobject.source_remove(self._episode_list_search_timeout)
|
|
|
|
self._episode_list_search_timeout = None
|
2009-11-05 11:00:15 +01:00
|
|
|
self.hbox_search_episodes.hide()
|
|
|
|
self.entry_search_episodes.set_text('')
|
|
|
|
self.episode_list_model.set_search_term(None)
|
|
|
|
self.treeAvailable.grab_focus()
|
|
|
|
|
|
|
|
def show_episode_search(self, input_char):
|
|
|
|
self.hbox_search_episodes.show()
|
2009-11-12 03:22:14 +01:00
|
|
|
self.entry_search_episodes.insert_text(input_char, -1)
|
2009-11-05 11:00:15 +01:00
|
|
|
self.entry_search_episodes.grab_focus()
|
|
|
|
self.entry_search_episodes.set_position(-1)
|
|
|
|
|
2011-04-11 13:09:50 +02:00
|
|
|
def set_episode_list_column(self, index, new_value):
|
|
|
|
mask = (1 << index)
|
|
|
|
if new_value:
|
|
|
|
self.config.episode_list_columns |= mask
|
|
|
|
else:
|
|
|
|
self.config.episode_list_columns &= ~mask
|
|
|
|
|
|
|
|
def update_episode_list_columns_visibility(self):
|
|
|
|
columns = TreeViewHelper.get_columns(self.treeAvailable)
|
|
|
|
for index, column in enumerate(columns):
|
|
|
|
visible = bool(self.config.episode_list_columns & (1 << index))
|
|
|
|
column.set_visible(visible)
|
|
|
|
self.treeAvailable.columns_autosize()
|
|
|
|
|
|
|
|
if self.episode_columns_menu is not None:
|
|
|
|
children = self.episode_columns_menu.get_children()
|
|
|
|
for index, child in enumerate(children):
|
|
|
|
active = bool(self.config.episode_list_columns & (1 << index))
|
|
|
|
child.set_active(active)
|
|
|
|
|
|
|
|
def on_episode_list_header_clicked(self, button, event):
|
2011-04-11 13:11:58 +02:00
|
|
|
if event.button != 3:
|
|
|
|
return False
|
|
|
|
|
2011-04-11 13:09:50 +02:00
|
|
|
if self.episode_columns_menu is not None:
|
|
|
|
self.episode_columns_menu.popup(None, None, None, event.button, \
|
|
|
|
event.time, None)
|
|
|
|
|
2011-04-11 13:11:58 +02:00
|
|
|
return False
|
|
|
|
|
2009-09-05 00:12:53 +02:00
|
|
|
def init_episode_list_treeview(self):
|
2010-03-01 17:48:56 +01:00
|
|
|
# For loading the list model
|
2010-11-22 14:28:27 +01:00
|
|
|
self.episode_list_model = EpisodeListModel(self.on_episode_list_filter_changed)
|
2009-09-01 17:22:51 +02:00
|
|
|
|
|
|
|
if self.config.episode_list_view_mode == EpisodeListModel.VIEW_UNDELETED:
|
|
|
|
self.item_view_episodes_undeleted.set_active(True)
|
|
|
|
elif self.config.episode_list_view_mode == EpisodeListModel.VIEW_DOWNLOADED:
|
|
|
|
self.item_view_episodes_downloaded.set_active(True)
|
2009-09-09 16:44:48 +02:00
|
|
|
elif self.config.episode_list_view_mode == EpisodeListModel.VIEW_UNPLAYED:
|
|
|
|
self.item_view_episodes_unplayed.set_active(True)
|
2009-09-01 17:22:51 +02:00
|
|
|
else:
|
|
|
|
self.item_view_episodes_all.set_active(True)
|
|
|
|
|
2009-09-09 16:44:48 +02:00
|
|
|
self.episode_list_model.set_view_mode(self.config.episode_list_view_mode)
|
|
|
|
|
2009-09-01 17:22:51 +02:00
|
|
|
self.treeAvailable.set_model(self.episode_list_model.get_filtered_model())
|
2009-08-13 23:19:12 +02:00
|
|
|
|
2009-09-05 00:12:53 +02:00
|
|
|
TreeViewHelper.set(self.treeAvailable, TreeViewHelper.ROLE_EPISODES)
|
2009-04-01 01:12:17 +02:00
|
|
|
|
2007-04-03 08:27:46 +02:00
|
|
|
iconcell = gtk.CellRendererPixbuf()
|
2010-11-19 15:36:39 +01:00
|
|
|
iconcell.set_property('stock-size', gtk.ICON_SIZE_BUTTON)
|
2011-09-19 10:47:20 +02:00
|
|
|
iconcell.set_fixed_size(self.EPISODE_LIST_ICON_WIDTH, -1)
|
2007-04-03 13:21:12 +02:00
|
|
|
|
2005-11-22 14:30:28 +01:00
|
|
|
namecell = gtk.CellRendererText()
|
2008-02-06 10:29:56 +01:00
|
|
|
namecell.set_property('ellipsize', pango.ELLIPSIZE_END)
|
2010-09-27 02:35:24 +02:00
|
|
|
namecolumn = gtk.TreeViewColumn(_('Episode'))
|
2010-11-19 15:36:39 +01:00
|
|
|
namecolumn.pack_start(iconcell, False)
|
|
|
|
namecolumn.add_attribute(iconcell, 'icon-name', EpisodeListModel.C_STATUS_ICON)
|
2010-09-27 02:35:24 +02:00
|
|
|
namecolumn.pack_start(namecell, True)
|
|
|
|
namecolumn.add_attribute(namecell, 'markup', EpisodeListModel.C_DESCRIPTION)
|
2011-07-16 18:35:14 +02:00
|
|
|
namecolumn.set_sort_column_id(EpisodeListModel.C_DESCRIPTION)
|
|
|
|
namecolumn.set_sizing(gtk.TREE_VIEW_COLUMN_AUTOSIZE)
|
|
|
|
namecolumn.set_resizable(True)
|
2008-02-06 10:29:56 +01:00
|
|
|
namecolumn.set_expand(True)
|
2005-11-22 14:30:28 +01:00
|
|
|
|
2011-07-16 18:35:14 +02:00
|
|
|
lockcell = gtk.CellRendererPixbuf()
|
|
|
|
lockcell.set_fixed_size(40, -1)
|
|
|
|
lockcell.set_property('stock-size', gtk.ICON_SIZE_MENU)
|
|
|
|
lockcell.set_property('icon-name', 'emblem-readonly')
|
|
|
|
namecolumn.pack_start(lockcell, False)
|
|
|
|
namecolumn.add_attribute(lockcell, 'visible', EpisodeListModel.C_LOCKED)
|
2010-11-19 15:36:39 +01:00
|
|
|
|
2005-11-22 14:30:28 +01:00
|
|
|
sizecell = gtk.CellRendererText()
|
2010-11-19 15:36:39 +01:00
|
|
|
sizecell.set_property('xalign', 1)
|
2009-08-13 23:19:12 +02:00
|
|
|
sizecolumn = gtk.TreeViewColumn(_('Size'), sizecell, text=EpisodeListModel.C_FILESIZE_TEXT)
|
2010-09-27 01:22:41 +02:00
|
|
|
sizecolumn.set_sort_column_id(EpisodeListModel.C_FILESIZE)
|
2006-04-10 18:46:50 +02:00
|
|
|
|
2011-04-11 13:09:50 +02:00
|
|
|
timecell = gtk.CellRendererText()
|
|
|
|
timecell.set_property('xalign', 1)
|
|
|
|
timecolumn = gtk.TreeViewColumn(_('Duration'), timecell, text=EpisodeListModel.C_TIME)
|
|
|
|
timecolumn.set_sort_column_id(EpisodeListModel.C_TOTAL_TIME)
|
|
|
|
|
2006-04-10 18:46:50 +02:00
|
|
|
releasecell = gtk.CellRendererText()
|
2009-08-13 23:19:12 +02:00
|
|
|
releasecolumn = gtk.TreeViewColumn(_('Released'), releasecell, text=EpisodeListModel.C_PUBLISHED_TEXT)
|
2010-09-27 01:22:41 +02:00
|
|
|
releasecolumn.set_sort_column_id(EpisodeListModel.C_PUBLISHED)
|
2009-09-05 00:12:53 +02:00
|
|
|
|
2010-11-20 23:15:30 +01:00
|
|
|
namecolumn.set_reorderable(True)
|
|
|
|
self.treeAvailable.append_column(namecolumn)
|
2006-06-22 23:41:32 +02:00
|
|
|
|
2011-07-16 18:38:19 +02:00
|
|
|
for itemcolumn in (sizecolumn, timecolumn, releasecolumn):
|
|
|
|
itemcolumn.set_reorderable(True)
|
|
|
|
self.treeAvailable.append_column(itemcolumn)
|
|
|
|
TreeViewHelper.register_column(self.treeAvailable, itemcolumn)
|
|
|
|
|
|
|
|
# Add context menu to all tree view column headers
|
|
|
|
for column in self.treeAvailable.get_columns():
|
|
|
|
label = gtk.Label(column.get_title())
|
|
|
|
label.show_all()
|
|
|
|
column.set_widget(label)
|
|
|
|
|
|
|
|
w = column.get_widget()
|
|
|
|
while w is not None and not isinstance(w, gtk.Button):
|
|
|
|
w = w.get_parent()
|
|
|
|
|
|
|
|
w.connect('button-release-event', self.on_episode_list_header_clicked)
|
|
|
|
|
|
|
|
# Create a new menu for the visible episode list columns
|
|
|
|
for child in self.mainMenu.get_children():
|
|
|
|
if child.get_name() == 'menuView':
|
|
|
|
submenu = child.get_submenu()
|
|
|
|
item = gtk.MenuItem(_('Visible columns'))
|
|
|
|
submenu.append(gtk.SeparatorMenuItem())
|
|
|
|
submenu.append(item)
|
|
|
|
submenu.show_all()
|
|
|
|
|
|
|
|
self.episode_columns_menu = gtk.Menu()
|
|
|
|
item.set_submenu(self.episode_columns_menu)
|
|
|
|
break
|
2011-04-11 13:09:50 +02:00
|
|
|
|
2011-07-16 18:38:19 +02:00
|
|
|
# For each column that can be shown/hidden, add a menu item
|
|
|
|
columns = TreeViewHelper.get_columns(self.treeAvailable)
|
|
|
|
for index, column in enumerate(columns):
|
|
|
|
item = gtk.CheckMenuItem(column.get_title())
|
|
|
|
self.episode_columns_menu.append(item)
|
|
|
|
def on_item_toggled(item, index):
|
|
|
|
self.set_episode_list_column(index, item.get_active())
|
|
|
|
item.connect('toggled', on_item_toggled, index)
|
|
|
|
self.episode_columns_menu.show_all()
|
|
|
|
|
|
|
|
# Update the visibility of the columns and the check menu items
|
|
|
|
self.update_episode_list_columns_visibility()
|
2008-04-06 02:19:03 +02:00
|
|
|
|
2009-11-12 02:53:59 +01:00
|
|
|
# Set up type-ahead find for the episode list
|
|
|
|
def on_key_press(treeview, event):
|
2011-07-16 18:38:19 +02:00
|
|
|
if event.keyval == gtk.keysyms.Left:
|
2011-04-11 14:22:31 +02:00
|
|
|
self.treeChannels.grab_focus()
|
|
|
|
elif event.keyval == gtk.keysyms.Escape:
|
2009-11-12 02:53:59 +01:00
|
|
|
self.hide_episode_search()
|
2009-11-25 10:14:57 +01:00
|
|
|
elif event.state & gtk.gdk.CONTROL_MASK:
|
|
|
|
# Don't handle type-ahead when control is pressed (so shortcuts
|
|
|
|
# with the Ctrl key still work, e.g. Ctrl+A, ...)
|
|
|
|
return False
|
2009-11-12 02:53:59 +01:00
|
|
|
else:
|
|
|
|
unicode_char_id = gtk.gdk.keyval_to_unicode(event.keyval)
|
|
|
|
if unicode_char_id == 0:
|
|
|
|
return False
|
|
|
|
input_char = unichr(unicode_char_id)
|
|
|
|
self.show_episode_search(input_char)
|
|
|
|
return True
|
|
|
|
self.treeAvailable.connect('key-press-event', on_key_press)
|
2007-04-03 08:27:46 +02:00
|
|
|
|
2011-04-11 14:16:07 +02:00
|
|
|
self.treeAvailable.connect('popup-menu', self.treeview_available_show_context_menu)
|
|
|
|
|
2011-07-16 18:38:19 +02:00
|
|
|
self.treeAvailable.enable_model_drag_source(gtk.gdk.BUTTON1_MASK, \
|
|
|
|
(('text/uri-list', 0, 0),), gtk.gdk.ACTION_COPY)
|
|
|
|
def drag_data_get(tree, context, selection_data, info, timestamp):
|
|
|
|
uris = ['file://'+e.local_filename(create=False) \
|
|
|
|
for e in self.get_selected_episodes() \
|
|
|
|
if e.was_downloaded(and_exists=True)]
|
|
|
|
uris.append('') # for the trailing '\r\n'
|
|
|
|
selection_data.set(selection_data.target, 8, '\r\n'.join(uris))
|
|
|
|
self.treeAvailable.connect('drag-data-get', drag_data_get)
|
2009-11-13 17:11:27 +01:00
|
|
|
|
2009-09-05 00:12:53 +02:00
|
|
|
selection = self.treeAvailable.get_selection()
|
2011-07-16 18:35:14 +02:00
|
|
|
selection.set_mode(gtk.SELECTION_MULTIPLE)
|
|
|
|
# Update the sensitivity of the toolbar buttons on the Desktop
|
|
|
|
selection.connect('changed', lambda s: self.play_or_download())
|
2009-05-30 13:28:16 +02:00
|
|
|
|
2009-09-05 00:12:53 +02:00
|
|
|
def init_download_list_treeview(self):
|
2006-06-22 23:41:32 +02:00
|
|
|
# enable multiple selection support
|
2009-04-01 01:12:17 +02:00
|
|
|
self.treeDownloads.get_selection().set_mode(gtk.SELECTION_MULTIPLE)
|
2009-09-05 00:12:53 +02:00
|
|
|
self.treeDownloads.set_search_equal_func(TreeViewHelper.make_search_equal_func(DownloadStatusModel))
|
2009-04-01 01:12:17 +02:00
|
|
|
|
2005-11-22 23:26:51 +01:00
|
|
|
# columns and renderers for "download progress" tab
|
2009-04-01 01:12:17 +02:00
|
|
|
# First column: [ICON] Episodename
|
|
|
|
column = gtk.TreeViewColumn(_('Episode'))
|
|
|
|
|
|
|
|
cell = gtk.CellRendererPixbuf()
|
2010-11-19 15:36:39 +01:00
|
|
|
cell.set_property('stock-size', gtk.ICON_SIZE_BUTTON)
|
2009-04-01 01:12:17 +02:00
|
|
|
column.pack_start(cell, expand=False)
|
2010-11-19 15:36:39 +01:00
|
|
|
column.add_attribute(cell, 'icon-name', \
|
2009-08-24 16:17:32 +02:00
|
|
|
DownloadStatusModel.C_ICON_NAME)
|
2009-04-01 01:12:17 +02:00
|
|
|
|
|
|
|
cell = gtk.CellRendererText()
|
|
|
|
cell.set_property('ellipsize', pango.ELLIPSIZE_END)
|
|
|
|
column.pack_start(cell, expand=True)
|
2009-09-22 18:53:14 +02:00
|
|
|
column.add_attribute(cell, 'markup', DownloadStatusModel.C_NAME)
|
2009-04-01 01:12:17 +02:00
|
|
|
column.set_sizing(gtk.TREE_VIEW_COLUMN_AUTOSIZE)
|
|
|
|
column.set_expand(True)
|
|
|
|
self.treeDownloads.append_column(column)
|
|
|
|
|
|
|
|
# Second column: Progress
|
2009-09-22 18:53:14 +02:00
|
|
|
cell = gtk.CellRendererProgress()
|
|
|
|
cell.set_property('yalign', .5)
|
|
|
|
cell.set_property('ypad', 6)
|
|
|
|
column = gtk.TreeViewColumn(_('Progress'), cell,
|
2009-08-24 16:17:32 +02:00
|
|
|
value=DownloadStatusModel.C_PROGRESS, \
|
|
|
|
text=DownloadStatusModel.C_PROGRESS_TEXT)
|
2009-09-22 18:53:14 +02:00
|
|
|
column.set_sizing(gtk.TREE_VIEW_COLUMN_AUTOSIZE)
|
|
|
|
column.set_expand(False)
|
2009-12-17 17:45:47 +01:00
|
|
|
self.treeDownloads.append_column(column)
|
2011-07-16 18:35:14 +02:00
|
|
|
column.set_property('min-width', 150)
|
|
|
|
column.set_property('max-width', 150)
|
2009-04-01 01:12:17 +02:00
|
|
|
|
2009-08-24 16:17:32 +02:00
|
|
|
self.treeDownloads.set_model(self.download_status_model)
|
2009-09-05 00:12:53 +02:00
|
|
|
TreeViewHelper.set(self.treeDownloads, TreeViewHelper.ROLE_DOWNLOADS)
|
2009-02-09 23:26:47 +01:00
|
|
|
|
2011-04-11 14:16:07 +02:00
|
|
|
self.treeDownloads.connect('popup-menu', self.treeview_downloads_show_context_menu)
|
|
|
|
|
2009-09-05 00:12:53 +02:00
|
|
|
def on_treeview_expose_event(self, treeview, event):
|
|
|
|
if event.window == treeview.get_bin_window():
|
|
|
|
model = treeview.get_model()
|
2009-09-02 19:05:32 +02:00
|
|
|
if (model is not None and model.get_iter_first() is not None):
|
2009-09-01 23:38:21 +02:00
|
|
|
return False
|
|
|
|
|
2010-08-20 15:10:32 +02:00
|
|
|
role = getattr(treeview, TreeViewHelper.ROLE, None)
|
|
|
|
if role is None:
|
|
|
|
return False
|
|
|
|
|
2009-09-01 23:38:21 +02:00
|
|
|
ctx = event.window.cairo_create()
|
|
|
|
ctx.rectangle(event.area.x, event.area.y,
|
|
|
|
event.area.width, event.area.height)
|
|
|
|
ctx.clip()
|
|
|
|
|
|
|
|
x, y, width, height, depth = event.window.get_geometry()
|
2010-03-01 17:48:56 +01:00
|
|
|
progress = None
|
2009-09-05 00:12:53 +02:00
|
|
|
|
|
|
|
if role == TreeViewHelper.ROLE_EPISODES:
|
2009-09-02 19:05:32 +02:00
|
|
|
if self.currently_updating:
|
2010-03-07 15:03:46 +01:00
|
|
|
text = _('Loading episodes')
|
2009-09-02 19:05:32 +02:00
|
|
|
elif self.config.episode_list_view_mode != \
|
2009-09-01 23:38:21 +02:00
|
|
|
EpisodeListModel.VIEW_ALL:
|
2009-09-21 16:03:52 +02:00
|
|
|
text = _('No episodes in current view')
|
2009-09-01 23:38:21 +02:00
|
|
|
else:
|
|
|
|
text = _('No episodes available')
|
2009-09-05 00:12:53 +02:00
|
|
|
elif role == TreeViewHelper.ROLE_PODCASTS:
|
2009-09-01 23:38:21 +02:00
|
|
|
if self.config.episode_list_view_mode != \
|
|
|
|
EpisodeListModel.VIEW_ALL and \
|
|
|
|
self.config.podcast_list_hide_boring and \
|
|
|
|
len(self.channels) > 0:
|
|
|
|
text = _('No podcasts in this view')
|
|
|
|
else:
|
|
|
|
text = _('No subscriptions')
|
2009-09-05 00:12:53 +02:00
|
|
|
elif role == TreeViewHelper.ROLE_DOWNLOADS:
|
2009-09-21 16:03:52 +02:00
|
|
|
text = _('No active downloads')
|
2009-09-01 23:38:21 +02:00
|
|
|
else:
|
2009-09-05 00:12:53 +02:00
|
|
|
raise Exception('on_treeview_expose_event: unknown role')
|
2009-09-01 23:38:21 +02:00
|
|
|
|
2011-07-16 18:35:14 +02:00
|
|
|
font_desc = None
|
2010-03-01 17:48:56 +01:00
|
|
|
draw_text_box_centered(ctx, treeview, width, height, text, font_desc, progress)
|
2009-09-01 23:38:21 +02:00
|
|
|
|
|
|
|
return False
|
|
|
|
|
2009-08-17 21:46:17 +02:00
|
|
|
def enable_download_list_update(self):
|
|
|
|
if not self.download_list_update_enabled:
|
2010-06-05 01:42:32 +02:00
|
|
|
self.update_downloads_list()
|
2009-08-17 21:46:17 +02:00
|
|
|
gobject.timeout_add(1500, self.update_downloads_list)
|
|
|
|
self.download_list_update_enabled = True
|
|
|
|
|
2010-06-05 01:42:32 +02:00
|
|
|
def cleanup_downloads(self):
|
2009-08-24 16:17:32 +02:00
|
|
|
model = self.download_status_model
|
2009-05-26 13:47:47 +02:00
|
|
|
|
|
|
|
all_tasks = [(gtk.TreeRowReference(model, row.path), row[0]) for row in model]
|
2010-03-23 13:51:08 +01:00
|
|
|
changed_episode_urls = set()
|
2009-05-26 13:47:47 +02:00
|
|
|
for row_reference, task in all_tasks:
|
2010-03-23 13:51:08 +01:00
|
|
|
if task.status in (task.DONE, task.CANCELLED):
|
2009-05-26 13:47:47 +02:00
|
|
|
model.remove(model.get_iter(row_reference.get_path()))
|
|
|
|
try:
|
|
|
|
# We don't "see" this task anymore - remove it;
|
|
|
|
# this is needed, so update_episode_list_icons()
|
|
|
|
# below gets the correct list of "seen" tasks
|
|
|
|
self.download_tasks_seen.remove(task)
|
|
|
|
except KeyError, key_error:
|
2011-07-15 16:32:06 +02:00
|
|
|
pass
|
2010-03-23 13:51:08 +01:00
|
|
|
changed_episode_urls.add(task.url)
|
2009-05-26 13:47:47 +02:00
|
|
|
# Tell the task that it has been removed (so it can clean up)
|
|
|
|
task.removed_from_list()
|
|
|
|
|
|
|
|
# Tell the podcasts tab to update icons for our removed podcasts
|
|
|
|
self.update_episode_list_icons(changed_episode_urls)
|
|
|
|
|
2009-09-02 15:57:09 +02:00
|
|
|
# Tell the shownotes window that we have removed the episode
|
|
|
|
if self.episode_shownotes_window is not None and \
|
|
|
|
self.episode_shownotes_window.episode is not None and \
|
|
|
|
self.episode_shownotes_window.episode.url in changed_episode_urls:
|
|
|
|
self.episode_shownotes_window._download_status_changed(None)
|
|
|
|
|
2010-06-05 01:42:32 +02:00
|
|
|
# Update the downloads list one more time
|
|
|
|
self.update_downloads_list(can_call_cleanup=False)
|
2009-05-26 13:47:47 +02:00
|
|
|
|
2009-05-11 23:10:56 +02:00
|
|
|
def on_tool_downloads_toggled(self, toolbutton):
|
|
|
|
if toolbutton.get_active():
|
|
|
|
self.wNotebook.set_current_page(1)
|
|
|
|
else:
|
|
|
|
self.wNotebook.set_current_page(0)
|
|
|
|
|
2009-12-22 17:01:47 +01:00
|
|
|
def add_download_task_monitor(self, monitor):
|
|
|
|
self.download_task_monitors.add(monitor)
|
|
|
|
model = self.download_status_model
|
|
|
|
if model is None:
|
|
|
|
model = ()
|
|
|
|
for row in model:
|
|
|
|
task = row[self.download_status_model.C_TASK]
|
|
|
|
monitor.task_updated(task)
|
|
|
|
|
|
|
|
def remove_download_task_monitor(self, monitor):
|
|
|
|
self.download_task_monitors.remove(monitor)
|
|
|
|
|
2010-06-05 01:42:32 +02:00
|
|
|
def update_downloads_list(self, can_call_cleanup=True):
|
2009-06-05 11:46:20 +02:00
|
|
|
try:
|
2009-08-24 16:17:32 +02:00
|
|
|
model = self.download_status_model
|
2009-04-01 01:12:17 +02:00
|
|
|
|
2009-09-16 23:51:18 +02:00
|
|
|
downloading, failed, finished, queued, paused, others = 0, 0, 0, 0, 0, 0
|
2009-06-05 11:46:20 +02:00
|
|
|
total_speed, total_size, done_size = 0, 0, 0
|
2009-04-01 01:12:17 +02:00
|
|
|
|
2009-06-05 11:46:20 +02:00
|
|
|
# Keep a list of all download tasks that we've seen
|
|
|
|
download_tasks_seen = set()
|
2009-04-01 01:12:17 +02:00
|
|
|
|
2009-09-02 15:57:09 +02:00
|
|
|
# Remember the DownloadTask object for the episode that
|
2009-06-05 11:46:20 +02:00
|
|
|
# has been opened in the episode shownotes dialog (if any)
|
2009-08-24 19:43:49 +02:00
|
|
|
if self.episode_shownotes_window is not None:
|
2009-09-02 15:57:09 +02:00
|
|
|
shownotes_episode = self.episode_shownotes_window.episode
|
|
|
|
shownotes_task = None
|
2009-06-05 11:46:20 +02:00
|
|
|
else:
|
2009-09-02 15:57:09 +02:00
|
|
|
shownotes_episode = None
|
|
|
|
shownotes_task = None
|
2009-04-01 18:46:19 +02:00
|
|
|
|
2009-06-05 11:46:20 +02:00
|
|
|
# Do not go through the list of the model is not (yet) available
|
|
|
|
if model is None:
|
|
|
|
model = ()
|
2009-05-30 12:34:36 +02:00
|
|
|
|
2009-06-05 11:46:20 +02:00
|
|
|
for row in model:
|
2009-08-24 16:17:32 +02:00
|
|
|
self.download_status_model.request_update(row.iter)
|
2009-04-01 01:12:17 +02:00
|
|
|
|
2009-08-24 16:17:32 +02:00
|
|
|
task = row[self.download_status_model.C_TASK]
|
2009-06-05 11:46:20 +02:00
|
|
|
speed, size, status, progress = task.speed, task.total_size, task.status, task.progress
|
2009-04-01 01:12:17 +02:00
|
|
|
|
2009-12-22 17:01:47 +01:00
|
|
|
# Let the download task monitors know of changes
|
|
|
|
for monitor in self.download_task_monitors:
|
|
|
|
monitor.task_updated(task)
|
|
|
|
|
2009-06-05 11:46:20 +02:00
|
|
|
total_size += size
|
|
|
|
done_size += size*progress
|
2009-04-01 01:12:17 +02:00
|
|
|
|
2009-09-02 15:57:09 +02:00
|
|
|
if shownotes_episode is not None and \
|
|
|
|
shownotes_episode.url == task.episode.url:
|
|
|
|
shownotes_task = task
|
2009-04-01 18:46:19 +02:00
|
|
|
|
2009-06-05 11:46:20 +02:00
|
|
|
download_tasks_seen.add(task)
|
2009-04-01 01:12:17 +02:00
|
|
|
|
2009-06-05 11:46:20 +02:00
|
|
|
if status == download.DownloadTask.DOWNLOADING:
|
|
|
|
downloading += 1
|
|
|
|
total_speed += speed
|
|
|
|
elif status == download.DownloadTask.FAILED:
|
|
|
|
failed += 1
|
|
|
|
elif status == download.DownloadTask.DONE:
|
|
|
|
finished += 1
|
|
|
|
elif status == download.DownloadTask.QUEUED:
|
|
|
|
queued += 1
|
2009-09-16 23:51:18 +02:00
|
|
|
elif status == download.DownloadTask.PAUSED:
|
|
|
|
paused += 1
|
2009-06-05 11:46:20 +02:00
|
|
|
else:
|
|
|
|
others += 1
|
|
|
|
|
|
|
|
# Remember which tasks we have seen after this run
|
|
|
|
self.download_tasks_seen = download_tasks_seen
|
|
|
|
|
2011-07-16 18:38:19 +02:00
|
|
|
text = [_('Downloads')]
|
|
|
|
if downloading + failed + queued > 0:
|
|
|
|
s = []
|
|
|
|
if downloading > 0:
|
|
|
|
s.append(N_('%(count)d active', '%(count)d active', downloading) % {'count':downloading})
|
|
|
|
if failed > 0:
|
|
|
|
s.append(N_('%(count)d failed', '%(count)d failed', failed) % {'count':failed})
|
|
|
|
if queued > 0:
|
|
|
|
s.append(N_('%(count)d queued', '%(count)d queued', queued) % {'count':queued})
|
|
|
|
text.append(' (' + ', '.join(s)+')')
|
|
|
|
self.labelDownloads.set_text(''.join(text))
|
2009-06-05 11:46:20 +02:00
|
|
|
|
|
|
|
title = [self.default_title]
|
|
|
|
|
|
|
|
# We have to update all episodes/channels for which the status has
|
|
|
|
# changed. Accessing task.status_changed has the side effect of
|
|
|
|
# re-setting the changed flag, so we need to get the "changed" list
|
|
|
|
# of tuples first and split it into two lists afterwards
|
|
|
|
changed = [(task.url, task.podcast_url) for task in \
|
|
|
|
self.download_tasks_seen if task.status_changed]
|
|
|
|
episode_urls = [episode_url for episode_url, channel_url in changed]
|
|
|
|
channel_urls = [channel_url for episode_url, channel_url in changed]
|
|
|
|
|
|
|
|
count = downloading + queued
|
|
|
|
if count > 0:
|
2010-11-22 21:52:58 +01:00
|
|
|
title.append(N_('downloading %(count)d file', 'downloading %(count)d files', count) % {'count':count})
|
2009-06-05 11:46:20 +02:00
|
|
|
|
|
|
|
if total_size > 0:
|
|
|
|
percentage = 100.0*done_size/total_size
|
|
|
|
else:
|
|
|
|
percentage = 0.0
|
2009-07-06 16:05:59 +02:00
|
|
|
total_speed = util.format_filesize(total_speed)
|
2009-06-05 11:46:20 +02:00
|
|
|
title[1] += ' (%d%%, %s/s)' % (percentage, total_speed)
|
2010-06-05 01:42:32 +02:00
|
|
|
else:
|
2011-07-16 18:38:19 +02:00
|
|
|
self.downloads_finished(self.download_tasks_seen)
|
2011-07-15 16:32:06 +02:00
|
|
|
logger.info('All downloads have finished.')
|
2009-12-17 14:50:39 +01:00
|
|
|
|
2010-06-05 01:42:32 +02:00
|
|
|
# Remove finished episodes
|
|
|
|
if self.config.auto_cleanup_downloads and can_call_cleanup:
|
|
|
|
self.cleanup_downloads()
|
|
|
|
|
|
|
|
# Stop updating the download list here
|
|
|
|
self.download_list_update_enabled = False
|
2009-06-05 11:46:20 +02:00
|
|
|
|
2011-07-16 18:35:14 +02:00
|
|
|
self.gPodder.set_title(' - '.join(title))
|
2009-06-05 11:46:20 +02:00
|
|
|
|
|
|
|
self.update_episode_list_icons(episode_urls)
|
2009-09-02 15:57:09 +02:00
|
|
|
if self.episode_shownotes_window is not None:
|
|
|
|
if (shownotes_task and shownotes_task.url in episode_urls) or \
|
|
|
|
shownotes_task != self.episode_shownotes_window.task:
|
|
|
|
self.episode_shownotes_window._download_status_changed(shownotes_task)
|
|
|
|
self.episode_shownotes_window._download_status_progress()
|
2009-06-05 11:46:20 +02:00
|
|
|
self.play_or_download()
|
|
|
|
if channel_urls:
|
2009-09-04 03:11:01 +02:00
|
|
|
self.update_podcast_list_model(channel_urls)
|
2009-08-17 21:46:17 +02:00
|
|
|
|
2009-08-24 00:43:55 +02:00
|
|
|
return self.download_list_update_enabled
|
2009-06-05 11:46:20 +02:00
|
|
|
except Exception, e:
|
2011-07-15 16:32:06 +02:00
|
|
|
logger.error('Exception happened while updating download list.', exc_info=True)
|
2009-08-24 23:00:25 +02:00
|
|
|
self.show_message('%s\n\n%s' % (_('Please report this problem and restart gPodder:'), str(e)), _('Unhandled exception'), important=True)
|
2009-06-05 11:46:20 +02:00
|
|
|
# We return False here, so the update loop won't be called again,
|
|
|
|
# that's why we require the restart of gPodder in the message.
|
|
|
|
return False
|
2009-04-01 01:12:17 +02:00
|
|
|
|
2010-02-24 02:29:08 +01:00
|
|
|
def on_config_changed(self, *args):
|
|
|
|
util.idle_add(self._on_config_changed, *args)
|
|
|
|
|
|
|
|
def _on_config_changed(self, name, old_value, new_value):
|
2011-07-16 18:38:19 +02:00
|
|
|
if name == 'show_toolbar':
|
2009-09-05 15:42:55 +02:00
|
|
|
self.toolbar.set_property('visible', new_value)
|
2009-09-02 18:40:52 +02:00
|
|
|
elif name == 'episode_list_descriptions':
|
2009-09-04 03:11:01 +02:00
|
|
|
self.update_episode_list_model()
|
2009-10-13 22:48:12 +02:00
|
|
|
elif name in ('auto_update_feeds', 'auto_update_frequency'):
|
|
|
|
self.restart_auto_update_timer()
|
2011-07-27 14:19:02 +02:00
|
|
|
elif name in ('podcast_list_view_all', 'podcast_list_sections'):
|
2009-12-16 14:55:55 +01:00
|
|
|
# Force a update of the podcast list model
|
|
|
|
self.update_podcast_list_model()
|
2011-04-11 13:09:50 +02:00
|
|
|
elif name == 'episode_list_columns':
|
|
|
|
self.update_episode_list_columns_visibility()
|
2011-04-30 18:00:28 +02:00
|
|
|
elif name == '_paned_position':
|
|
|
|
self._last_paned_position_toggling = True
|
|
|
|
self.item_sidebar.set_active(new_value > 0)
|
|
|
|
self._last_paned_position_toggling = False
|
2008-04-24 19:42:57 +02:00
|
|
|
|
2009-09-05 00:12:53 +02:00
|
|
|
def on_treeview_query_tooltip(self, treeview, x, y, keyboard_tooltip, tooltip):
|
2008-06-15 14:28:24 +02:00
|
|
|
# With get_bin_window, we get the window that contains the rows without
|
|
|
|
# the header. The Y coordinate of this window will be the height of the
|
|
|
|
# treeview header. This is the amount we have to subtract from the
|
|
|
|
# event's Y coordinate to get the coordinate to pass to get_path_at_pos
|
|
|
|
(x_bin, y_bin) = treeview.get_bin_window().get_position()
|
|
|
|
y -= x_bin
|
|
|
|
y -= y_bin
|
|
|
|
(path, column, rx, ry) = treeview.get_path_at_pos( x, y) or (None,)*4
|
|
|
|
|
2010-11-20 23:45:07 +01:00
|
|
|
if not getattr(treeview, TreeViewHelper.CAN_TOOLTIP) or x > 50 or (column is not None and column != treeview.get_columns()[0]):
|
2009-09-05 00:12:53 +02:00
|
|
|
setattr(treeview, TreeViewHelper.LAST_TOOLTIP, None)
|
2008-06-15 14:28:24 +02:00
|
|
|
return False
|
|
|
|
|
|
|
|
if path is not None:
|
|
|
|
model = treeview.get_model()
|
|
|
|
iter = model.get_iter(path)
|
2009-09-05 00:12:53 +02:00
|
|
|
role = getattr(treeview, TreeViewHelper.ROLE)
|
2008-06-15 14:28:24 +02:00
|
|
|
|
2009-09-05 00:12:53 +02:00
|
|
|
if role == TreeViewHelper.ROLE_EPISODES:
|
|
|
|
id = model.get_value(iter, EpisodeListModel.C_URL)
|
|
|
|
elif role == TreeViewHelper.ROLE_PODCASTS:
|
|
|
|
id = model.get_value(iter, PodcastListModel.C_URL)
|
2011-07-28 14:58:17 +02:00
|
|
|
if id == '-':
|
|
|
|
# Section header - no tooltip here (for now at least)
|
|
|
|
return False
|
2008-11-19 19:29:55 +01:00
|
|
|
|
2009-09-05 00:12:53 +02:00
|
|
|
last_tooltip = getattr(treeview, TreeViewHelper.LAST_TOOLTIP)
|
|
|
|
if last_tooltip is not None and last_tooltip != id:
|
|
|
|
setattr(treeview, TreeViewHelper.LAST_TOOLTIP, None)
|
2009-08-13 23:19:12 +02:00
|
|
|
return False
|
2009-09-05 00:12:53 +02:00
|
|
|
setattr(treeview, TreeViewHelper.LAST_TOOLTIP, id)
|
|
|
|
|
|
|
|
if role == TreeViewHelper.ROLE_EPISODES:
|
2009-12-27 14:27:44 +01:00
|
|
|
description = model.get_value(iter, EpisodeListModel.C_TOOLTIP)
|
2010-03-01 15:39:33 +01:00
|
|
|
if description:
|
|
|
|
tooltip.set_text(description)
|
|
|
|
else:
|
|
|
|
return False
|
2009-09-05 00:12:53 +02:00
|
|
|
elif role == TreeViewHelper.ROLE_PODCASTS:
|
|
|
|
channel = model.get_value(iter, PodcastListModel.C_CHANNEL)
|
2009-12-16 14:55:55 +01:00
|
|
|
if channel is None:
|
|
|
|
return False
|
2009-09-05 00:12:53 +02:00
|
|
|
error_str = model.get_value(iter, PodcastListModel.C_ERROR)
|
|
|
|
if error_str:
|
2011-02-25 21:05:26 +01:00
|
|
|
error_str = _('Feedparser error: %s') % cgi.escape(error_str.strip())
|
2009-09-05 00:12:53 +02:00
|
|
|
error_str = '<span foreground="#ff0000">%s</span>' % error_str
|
|
|
|
table = gtk.Table(rows=3, columns=3)
|
|
|
|
table.set_row_spacings(5)
|
|
|
|
table.set_col_spacings(5)
|
|
|
|
table.set_border_width(5)
|
|
|
|
|
|
|
|
heading = gtk.Label()
|
|
|
|
heading.set_alignment(0, 1)
|
2011-02-25 21:05:26 +01:00
|
|
|
heading.set_markup('<b><big>%s</big></b>\n<small>%s</small>' % (cgi.escape(channel.title), cgi.escape(channel.url)))
|
2009-09-05 00:12:53 +02:00
|
|
|
table.attach(heading, 0, 1, 0, 1)
|
|
|
|
|
|
|
|
table.attach(gtk.HSeparator(), 0, 3, 1, 2)
|
|
|
|
|
|
|
|
if len(channel.description) < 500:
|
|
|
|
description = channel.description
|
2009-08-13 23:19:12 +02:00
|
|
|
else:
|
2009-09-05 00:12:53 +02:00
|
|
|
pos = channel.description.find('\n\n')
|
|
|
|
if pos == -1 or pos > 500:
|
|
|
|
description = channel.description[:498]+'[...]'
|
|
|
|
else:
|
|
|
|
description = channel.description[:pos]
|
2008-06-05 18:17:09 +02:00
|
|
|
|
2009-09-05 00:12:53 +02:00
|
|
|
description = gtk.Label(description)
|
|
|
|
if error_str:
|
|
|
|
description.set_markup(error_str)
|
|
|
|
description.set_alignment(0, 0)
|
|
|
|
description.set_line_wrap(True)
|
|
|
|
table.attach(description, 0, 3, 2, 3)
|
2008-05-14 15:38:06 +02:00
|
|
|
|
2009-09-05 00:12:53 +02:00
|
|
|
table.show_all()
|
|
|
|
tooltip.set_custom(table)
|
2009-08-13 23:19:12 +02:00
|
|
|
|
|
|
|
return True
|
2007-11-14 21:57:31 +01:00
|
|
|
|
2009-09-05 00:12:53 +02:00
|
|
|
setattr(treeview, TreeViewHelper.LAST_TOOLTIP, None)
|
2007-11-14 21:57:31 +01:00
|
|
|
return False
|
|
|
|
|
2009-09-05 00:12:53 +02:00
|
|
|
def treeview_allow_tooltips(self, treeview, allow):
|
|
|
|
setattr(treeview, TreeViewHelper.CAN_TOOLTIP, allow)
|
|
|
|
|
|
|
|
def treeview_handle_context_menu_click(self, treeview, event):
|
2011-04-11 14:16:07 +02:00
|
|
|
if event is None:
|
|
|
|
selection = treeview.get_selection()
|
|
|
|
return selection.get_selected_rows()
|
|
|
|
|
2009-09-05 00:12:53 +02:00
|
|
|
x, y = int(event.x), int(event.y)
|
|
|
|
path, column, rx, ry = treeview.get_path_at_pos(x, y) or (None,)*4
|
2009-04-01 01:12:17 +02:00
|
|
|
|
2009-09-05 00:12:53 +02:00
|
|
|
selection = treeview.get_selection()
|
|
|
|
model, paths = selection.get_selected_rows()
|
2009-04-01 01:12:17 +02:00
|
|
|
|
2009-09-05 00:12:53 +02:00
|
|
|
if path is None or (path not in paths and \
|
2011-07-16 18:49:19 +02:00
|
|
|
event.button == 3):
|
2009-09-05 00:12:53 +02:00
|
|
|
# We have right-clicked, but not into the selection,
|
|
|
|
# assume we don't want to operate on the selection
|
2009-04-01 01:12:17 +02:00
|
|
|
paths = []
|
2009-09-05 00:12:53 +02:00
|
|
|
|
|
|
|
if path is not None and not paths and \
|
2011-07-16 18:49:19 +02:00
|
|
|
event.button == 3:
|
2009-09-05 00:12:53 +02:00
|
|
|
# No selection or clicked outside selection;
|
|
|
|
# select the single item where we clicked
|
|
|
|
treeview.grab_focus()
|
|
|
|
treeview.set_cursor(path, column, 0)
|
|
|
|
paths = [path]
|
|
|
|
|
|
|
|
if not paths:
|
|
|
|
# Unselect any remaining items (clicked elsewhere)
|
|
|
|
if hasattr(treeview, 'is_rubber_banding_active'):
|
|
|
|
if not treeview.is_rubber_banding_active():
|
|
|
|
selection.unselect_all()
|
|
|
|
else:
|
|
|
|
selection.unselect_all()
|
|
|
|
|
|
|
|
return model, paths
|
|
|
|
|
2009-09-22 19:38:27 +02:00
|
|
|
def downloads_list_get_selection(self, model=None, paths=None):
|
|
|
|
if model is None and paths is None:
|
|
|
|
selection = self.treeDownloads.get_selection()
|
|
|
|
model, paths = selection.get_selected_rows()
|
|
|
|
|
2010-03-07 20:15:36 +01:00
|
|
|
can_queue, can_cancel, can_pause, can_remove, can_force = (True,)*5
|
2009-09-22 19:38:27 +02:00
|
|
|
selected_tasks = [(gtk.TreeRowReference(model, path), \
|
|
|
|
model.get_value(model.get_iter(path), \
|
|
|
|
DownloadStatusModel.C_TASK)) for path in paths]
|
|
|
|
|
|
|
|
for row_reference, task in selected_tasks:
|
2010-03-07 20:15:36 +01:00
|
|
|
if task.status != download.DownloadTask.QUEUED:
|
|
|
|
can_force = False
|
2009-09-22 19:38:27 +02:00
|
|
|
if task.status not in (download.DownloadTask.PAUSED, \
|
|
|
|
download.DownloadTask.FAILED, \
|
|
|
|
download.DownloadTask.CANCELLED):
|
|
|
|
can_queue = False
|
|
|
|
if task.status not in (download.DownloadTask.PAUSED, \
|
|
|
|
download.DownloadTask.QUEUED, \
|
2010-09-29 20:11:37 +02:00
|
|
|
download.DownloadTask.DOWNLOADING, \
|
|
|
|
download.DownloadTask.FAILED):
|
2009-09-22 19:38:27 +02:00
|
|
|
can_cancel = False
|
|
|
|
if task.status not in (download.DownloadTask.QUEUED, \
|
|
|
|
download.DownloadTask.DOWNLOADING):
|
|
|
|
can_pause = False
|
|
|
|
if task.status not in (download.DownloadTask.CANCELLED, \
|
|
|
|
download.DownloadTask.FAILED, \
|
|
|
|
download.DownloadTask.DONE):
|
|
|
|
can_remove = False
|
|
|
|
|
2010-03-07 20:15:36 +01:00
|
|
|
return selected_tasks, can_queue, can_cancel, can_pause, can_remove, can_force
|
2009-09-22 19:38:27 +02:00
|
|
|
|
2009-12-28 20:37:29 +01:00
|
|
|
def downloads_finished(self, download_tasks_seen):
|
2010-12-18 14:50:43 +01:00
|
|
|
finished_downloads = [str(task) for task in download_tasks_seen if task.notify_as_finished()]
|
|
|
|
failed_downloads = [str(task)+' ('+task.error_message+')' for task in download_tasks_seen if task.notify_as_failed()]
|
2009-12-28 20:37:29 +01:00
|
|
|
|
|
|
|
if finished_downloads and failed_downloads:
|
|
|
|
message = self.format_episode_list(finished_downloads, 5)
|
|
|
|
message += '\n\n<i>%s</i>\n' % _('These downloads failed:')
|
|
|
|
message += self.format_episode_list(failed_downloads, 5)
|
2010-01-25 00:55:41 +01:00
|
|
|
self.show_message(message, _('Downloads finished'), True, widget=self.labelDownloads)
|
2009-12-28 20:37:29 +01:00
|
|
|
elif finished_downloads:
|
|
|
|
message = self.format_episode_list(finished_downloads)
|
2010-01-25 00:55:41 +01:00
|
|
|
self.show_message(message, _('Downloads finished'), widget=self.labelDownloads)
|
2009-12-28 20:37:29 +01:00
|
|
|
elif failed_downloads:
|
|
|
|
message = self.format_episode_list(failed_downloads)
|
2010-01-25 00:55:41 +01:00
|
|
|
self.show_message(message, _('Downloads failed'), True, widget=self.labelDownloads)
|
2009-12-28 20:37:29 +01:00
|
|
|
|
2010-06-05 01:45:01 +02:00
|
|
|
|
2009-12-28 20:37:29 +01:00
|
|
|
def format_episode_list(self, episode_list, max_episodes=10):
|
|
|
|
"""
|
|
|
|
Format a list of episode names for notifications
|
|
|
|
|
|
|
|
Will truncate long episode names and limit the amount of
|
|
|
|
episodes displayed (max_episodes=10).
|
|
|
|
|
|
|
|
The episode_list parameter should be a list of strings.
|
|
|
|
"""
|
|
|
|
MAX_TITLE_LENGTH = 100
|
|
|
|
|
|
|
|
result = []
|
|
|
|
for title in episode_list[:min(len(episode_list), max_episodes)]:
|
|
|
|
if len(title) > MAX_TITLE_LENGTH:
|
|
|
|
middle = (MAX_TITLE_LENGTH/2)-2
|
|
|
|
title = '%s...%s' % (title[0:middle], title[-middle:])
|
2011-02-25 21:05:26 +01:00
|
|
|
result.append(cgi.escape(title))
|
2009-12-28 20:37:29 +01:00
|
|
|
result.append('\n')
|
|
|
|
|
|
|
|
more_episodes = len(episode_list) - max_episodes
|
|
|
|
if more_episodes > 0:
|
|
|
|
result.append('(...')
|
2010-11-22 21:52:58 +01:00
|
|
|
result.append(N_('%(count)d more episode', '%(count)d more episodes', more_episodes) % {'count':more_episodes})
|
2009-12-28 20:37:29 +01:00
|
|
|
result.append('...)')
|
|
|
|
|
|
|
|
return (''.join(result)).strip()
|
|
|
|
|
2010-03-07 20:15:36 +01:00
|
|
|
def _for_each_task_set_status(self, tasks, status, force_start=False):
|
2009-09-22 19:38:27 +02:00
|
|
|
episode_urls = set()
|
|
|
|
model = self.treeDownloads.get_model()
|
|
|
|
for row_reference, task in tasks:
|
|
|
|
if status == download.DownloadTask.QUEUED:
|
2010-03-07 20:15:36 +01:00
|
|
|
# Only queue task when its paused/failed/cancelled (or forced)
|
|
|
|
if task.status in (task.PAUSED, task.FAILED, task.CANCELLED) or force_start:
|
|
|
|
self.download_queue_manager.add_task(task, force_start)
|
2009-09-22 19:38:27 +02:00
|
|
|
self.enable_download_list_update()
|
|
|
|
elif status == download.DownloadTask.CANCELLED:
|
|
|
|
# Cancelling a download allowed when downloading/queued
|
|
|
|
if task.status in (task.QUEUED, task.DOWNLOADING):
|
|
|
|
task.status = status
|
2010-09-29 20:11:37 +02:00
|
|
|
# Cancelling paused/failed downloads requires a call to .run()
|
|
|
|
elif task.status in (task.PAUSED, task.FAILED):
|
2009-09-22 19:38:27 +02:00
|
|
|
task.status = status
|
|
|
|
# Call run, so the partial file gets deleted
|
|
|
|
task.run()
|
|
|
|
elif status == download.DownloadTask.PAUSED:
|
|
|
|
# Pausing a download only when queued/downloading
|
|
|
|
if task.status in (task.DOWNLOADING, task.QUEUED):
|
|
|
|
task.status = status
|
|
|
|
elif status is None:
|
|
|
|
# Remove the selected task - cancel downloading/queued tasks
|
|
|
|
if task.status in (task.QUEUED, task.DOWNLOADING):
|
|
|
|
task.status = task.CANCELLED
|
|
|
|
model.remove(model.get_iter(row_reference.get_path()))
|
|
|
|
# Remember the URL, so we can tell the UI to update
|
|
|
|
try:
|
|
|
|
# We don't "see" this task anymore - remove it;
|
|
|
|
# this is needed, so update_episode_list_icons()
|
|
|
|
# below gets the correct list of "seen" tasks
|
|
|
|
self.download_tasks_seen.remove(task)
|
|
|
|
except KeyError, key_error:
|
2011-07-15 16:32:06 +02:00
|
|
|
pass
|
2009-09-22 19:38:27 +02:00
|
|
|
episode_urls.add(task.url)
|
|
|
|
# Tell the task that it has been removed (so it can clean up)
|
|
|
|
task.removed_from_list()
|
|
|
|
else:
|
|
|
|
# We can (hopefully) simply set the task status here
|
|
|
|
task.status = status
|
|
|
|
# Tell the podcasts tab to update icons for our removed podcasts
|
|
|
|
self.update_episode_list_icons(episode_urls)
|
|
|
|
# Update the tab title and downloads list
|
|
|
|
self.update_downloads_list()
|
|
|
|
|
2011-04-11 14:16:07 +02:00
|
|
|
def treeview_downloads_show_context_menu(self, treeview, event=None):
|
2009-09-05 00:12:53 +02:00
|
|
|
model, paths = self.treeview_handle_context_menu_click(treeview, event)
|
|
|
|
if not paths:
|
|
|
|
if not hasattr(treeview, 'is_rubber_banding_active'):
|
2009-04-01 01:12:17 +02:00
|
|
|
return True
|
2009-09-05 00:12:53 +02:00
|
|
|
else:
|
|
|
|
return not treeview.is_rubber_banding_active()
|
|
|
|
|
2011-07-16 18:49:19 +02:00
|
|
|
if event is None or event.button == 3:
|
2010-03-07 20:15:36 +01:00
|
|
|
selected_tasks, can_queue, can_cancel, can_pause, can_remove, can_force = \
|
2009-09-22 19:38:27 +02:00
|
|
|
self.downloads_list_get_selection(model, paths)
|
2009-04-01 01:12:17 +02:00
|
|
|
|
2010-03-07 20:15:36 +01:00
|
|
|
def make_menu_item(label, stock_id, tasks, status, sensitive, force_start=False):
|
2009-04-01 01:12:17 +02:00
|
|
|
# This creates a menu item for selection-wide actions
|
|
|
|
item = gtk.ImageMenuItem(label)
|
|
|
|
item.set_image(gtk.image_new_from_stock(stock_id, gtk.ICON_SIZE_MENU))
|
2010-03-07 20:15:36 +01:00
|
|
|
item.connect('activate', lambda item: self._for_each_task_set_status(tasks, status, force_start))
|
2009-09-22 19:38:27 +02:00
|
|
|
item.set_sensitive(sensitive)
|
2011-02-01 18:06:24 +01:00
|
|
|
return item
|
2009-04-01 01:12:17 +02:00
|
|
|
|
|
|
|
menu = gtk.Menu()
|
|
|
|
|
2009-05-12 10:36:01 +02:00
|
|
|
item = gtk.ImageMenuItem(_('Episode details'))
|
|
|
|
item.set_image(gtk.image_new_from_stock(gtk.STOCK_INFO, gtk.ICON_SIZE_MENU))
|
|
|
|
if len(selected_tasks) == 1:
|
|
|
|
row_reference, task = selected_tasks[0]
|
|
|
|
episode = task.episode
|
|
|
|
item.connect('activate', lambda item: self.show_episode_shownotes(episode))
|
|
|
|
else:
|
|
|
|
item.set_sensitive(False)
|
2011-02-01 18:06:24 +01:00
|
|
|
menu.append(item)
|
2009-04-01 01:12:17 +02:00
|
|
|
menu.append(gtk.SeparatorMenuItem())
|
2010-03-07 20:15:36 +01:00
|
|
|
if can_force:
|
|
|
|
menu.append(make_menu_item(_('Start download now'), gtk.STOCK_GO_DOWN, selected_tasks, download.DownloadTask.QUEUED, True, True))
|
|
|
|
else:
|
|
|
|
menu.append(make_menu_item(_('Download'), gtk.STOCK_GO_DOWN, selected_tasks, download.DownloadTask.QUEUED, can_queue, False))
|
2009-09-22 19:38:27 +02:00
|
|
|
menu.append(make_menu_item(_('Cancel'), gtk.STOCK_CANCEL, selected_tasks, download.DownloadTask.CANCELLED, can_cancel))
|
|
|
|
menu.append(make_menu_item(_('Pause'), gtk.STOCK_MEDIA_PAUSE, selected_tasks, download.DownloadTask.PAUSED, can_pause))
|
2009-04-01 01:12:17 +02:00
|
|
|
menu.append(gtk.SeparatorMenuItem())
|
2009-09-22 19:38:27 +02:00
|
|
|
menu.append(make_menu_item(_('Remove from list'), gtk.STOCK_REMOVE, selected_tasks, None, can_remove))
|
2009-04-01 01:12:17 +02:00
|
|
|
|
|
|
|
menu.show_all()
|
2011-04-11 14:16:07 +02:00
|
|
|
|
|
|
|
if event is None:
|
|
|
|
func = TreeViewHelper.make_popup_position_func(treeview)
|
2011-07-16 18:49:19 +02:00
|
|
|
menu.popup(None, None, func, 3, 0)
|
2011-04-11 14:16:07 +02:00
|
|
|
else:
|
|
|
|
menu.popup(None, None, None, event.button, event.time)
|
2009-04-01 01:12:17 +02:00
|
|
|
return True
|
|
|
|
|
2011-04-11 14:16:07 +02:00
|
|
|
def treeview_channels_show_context_menu(self, treeview, event=None):
|
2009-09-05 00:12:53 +02:00
|
|
|
model, paths = self.treeview_handle_context_menu_click(treeview, event)
|
|
|
|
if not paths:
|
|
|
|
return True
|
2007-09-15 16:29:37 +02:00
|
|
|
|
2009-12-16 14:55:55 +01:00
|
|
|
# Check for valid channel id, if there's no id then
|
|
|
|
# assume that it is a proxy channel or equivalent
|
|
|
|
# and cannot be operated with right click
|
|
|
|
if self.active_channel.id is None:
|
|
|
|
return True
|
|
|
|
|
2011-07-16 18:49:19 +02:00
|
|
|
if event is None or event.button == 3:
|
2007-09-15 16:29:37 +02:00
|
|
|
menu = gtk.Menu()
|
|
|
|
|
2010-08-07 15:59:46 +02:00
|
|
|
item = gtk.ImageMenuItem( _('Update podcast'))
|
2009-07-06 16:14:36 +02:00
|
|
|
item.set_image(gtk.image_new_from_stock(gtk.STOCK_REFRESH, gtk.ICON_SIZE_MENU))
|
2010-08-07 15:59:46 +02:00
|
|
|
item.connect('activate', self.on_itemUpdateChannel_activate)
|
2009-07-06 16:14:36 +02:00
|
|
|
menu.append(item)
|
2008-03-10 16:50:12 +01:00
|
|
|
|
2010-08-07 15:59:46 +02:00
|
|
|
menu.append(gtk.SeparatorMenuItem())
|
2008-11-19 17:05:19 +01:00
|
|
|
|
2010-12-20 00:18:22 +01:00
|
|
|
item = gtk.CheckMenuItem(_('Archive'))
|
2010-12-20 14:35:46 +01:00
|
|
|
item.set_active(self.active_channel.auto_archive_episodes)
|
2010-08-07 15:59:46 +02:00
|
|
|
item.connect('activate', self.on_channel_toggle_lock_activate)
|
2011-02-01 18:06:24 +01:00
|
|
|
menu.append(item)
|
2008-11-19 17:05:19 +01:00
|
|
|
|
2010-08-07 15:59:46 +02:00
|
|
|
item = gtk.ImageMenuItem(_('Remove podcast'))
|
|
|
|
item.set_image(gtk.image_new_from_stock(gtk.STOCK_DELETE, gtk.ICON_SIZE_MENU))
|
|
|
|
item.connect( 'activate', self.on_itemRemoveChannel_activate)
|
|
|
|
menu.append( item)
|
2007-11-02 07:53:30 +01:00
|
|
|
|
2010-08-07 15:59:46 +02:00
|
|
|
menu.append( gtk.SeparatorMenuItem())
|
2007-09-15 16:29:37 +02:00
|
|
|
|
2010-08-07 15:59:46 +02:00
|
|
|
item = gtk.ImageMenuItem(_('Podcast details'))
|
|
|
|
item.set_image(gtk.image_new_from_stock(gtk.STOCK_INFO, gtk.ICON_SIZE_MENU))
|
|
|
|
item.connect('activate', self.on_itemEditChannel_activate)
|
|
|
|
menu.append(item)
|
2007-09-15 16:29:37 +02:00
|
|
|
|
|
|
|
menu.show_all()
|
2008-06-15 14:28:24 +02:00
|
|
|
# Disable tooltips while we are showing the menu, so
|
|
|
|
# the tooltip will not appear over the menu
|
2009-09-05 00:12:53 +02:00
|
|
|
self.treeview_allow_tooltips(self.treeChannels, False)
|
|
|
|
menu.connect('deactivate', lambda menushell: self.treeview_allow_tooltips(self.treeChannels, True))
|
2011-04-11 14:16:07 +02:00
|
|
|
|
|
|
|
if event is None:
|
|
|
|
func = TreeViewHelper.make_popup_position_func(treeview)
|
2011-07-16 18:49:19 +02:00
|
|
|
menu.popup(None, None, func, 3, 0)
|
2011-04-11 14:16:07 +02:00
|
|
|
else:
|
|
|
|
menu.popup(None, None, None, event.button, event.time)
|
2007-09-15 16:29:37 +02:00
|
|
|
|
|
|
|
return True
|
|
|
|
|
2008-06-14 13:43:53 +02:00
|
|
|
def cover_file_removed(self, channel_url):
|
|
|
|
"""
|
|
|
|
The Cover Downloader calls this when a previously-
|
|
|
|
available cover has been removed from the disk. We
|
2009-08-13 22:08:47 +02:00
|
|
|
have to update our model to reflect this change.
|
2008-06-14 13:43:53 +02:00
|
|
|
"""
|
2009-08-13 22:08:47 +02:00
|
|
|
self.podcast_list_model.delete_cover_by_url(channel_url)
|
2008-06-14 13:43:53 +02:00
|
|
|
|
2010-09-27 00:08:30 +02:00
|
|
|
def cover_download_finished(self, channel, pixbuf):
|
2008-06-14 13:43:53 +02:00
|
|
|
"""
|
|
|
|
The Cover Downloader calls this when it has finished
|
|
|
|
downloading (or registering, if already downloaded)
|
|
|
|
a new channel cover, which is ready for displaying.
|
|
|
|
"""
|
2010-09-27 00:08:30 +02:00
|
|
|
self.podcast_list_model.add_cover_by_channel(channel, pixbuf)
|
2008-06-14 13:43:53 +02:00
|
|
|
|
2010-05-03 19:00:29 +02:00
|
|
|
def save_episodes_as_file(self, episodes):
|
|
|
|
for episode in episodes:
|
|
|
|
self.save_episode_as_file(episode)
|
|
|
|
|
2009-08-13 23:19:12 +02:00
|
|
|
def save_episode_as_file(self, episode):
|
2009-09-05 00:53:46 +02:00
|
|
|
PRIVATE_FOLDER_ATTRIBUTE = '_save_episodes_as_file_folder'
|
2009-02-09 23:26:47 +01:00
|
|
|
if episode.was_downloaded(and_exists=True):
|
2009-09-05 00:53:46 +02:00
|
|
|
folder = getattr(self, PRIVATE_FOLDER_ATTRIBUTE, None)
|
2009-02-09 23:26:47 +01:00
|
|
|
copy_from = episode.local_filename(create=False)
|
|
|
|
assert copy_from is not None
|
2011-04-06 12:46:27 +02:00
|
|
|
copy_to = util.sanitize_filename(episode.sync_filename())
|
2009-08-13 20:39:00 +02:00
|
|
|
(result, folder) = self.show_copy_dialog(src_filename=copy_from, dst_filename=copy_to, dst_directory=folder)
|
2009-09-05 00:53:46 +02:00
|
|
|
setattr(self, PRIVATE_FOLDER_ATTRIBUTE, folder)
|
2007-10-28 15:30:11 +01:00
|
|
|
|
2009-08-10 23:14:35 +02:00
|
|
|
def copy_episodes_bluetooth(self, episodes):
|
|
|
|
episodes_to_copy = [e for e in episodes if e.was_downloaded(and_exists=True)]
|
2008-01-21 10:52:09 +01:00
|
|
|
|
2009-08-24 23:00:25 +02:00
|
|
|
def convert_and_send_thread(episode):
|
2009-08-10 23:14:35 +02:00
|
|
|
for episode in episodes:
|
|
|
|
filename = episode.local_filename(create=False)
|
|
|
|
assert filename is not None
|
|
|
|
destfile = os.path.join(tempfile.gettempdir(), \
|
2010-12-20 10:29:41 +01:00
|
|
|
util.sanitize_filename(episode.sync_filename()))
|
2009-08-10 23:14:35 +02:00
|
|
|
(base, ext) = os.path.splitext(filename)
|
|
|
|
if not destfile.endswith(ext):
|
|
|
|
destfile += ext
|
2008-01-21 10:52:09 +01:00
|
|
|
|
2009-08-10 23:14:35 +02:00
|
|
|
try:
|
|
|
|
shutil.copyfile(filename, destfile)
|
|
|
|
util.bluetooth_send_file(destfile)
|
|
|
|
except:
|
2011-07-15 16:32:06 +02:00
|
|
|
logger.error('Cannot copy "%s" to "%s".', filename, destfile)
|
2009-08-24 23:00:25 +02:00
|
|
|
self.notification(_('Error converting file.'), _('Bluetooth file transfer'), important=True)
|
2009-07-06 15:21:36 +02:00
|
|
|
|
2009-08-10 23:14:35 +02:00
|
|
|
util.delete_file(destfile)
|
2008-02-27 09:42:38 +01:00
|
|
|
|
2009-08-24 23:00:25 +02:00
|
|
|
threading.Thread(target=convert_and_send_thread, args=[episodes_to_copy]).start()
|
2008-01-21 10:52:09 +01:00
|
|
|
|
2011-04-11 14:16:07 +02:00
|
|
|
def treeview_available_show_context_menu(self, treeview, event=None):
|
2009-09-05 00:12:53 +02:00
|
|
|
model, paths = self.treeview_handle_context_menu_click(treeview, event)
|
|
|
|
if not paths:
|
|
|
|
if not hasattr(treeview, 'is_rubber_banding_active'):
|
2007-08-26 17:20:46 +02:00
|
|
|
return True
|
2009-09-05 00:12:53 +02:00
|
|
|
else:
|
|
|
|
return not treeview.is_rubber_banding_active()
|
2007-08-26 17:20:46 +02:00
|
|
|
|
2011-07-16 18:49:19 +02:00
|
|
|
if event is None or event.button == 3:
|
2009-08-10 23:14:35 +02:00
|
|
|
episodes = self.get_selected_episodes()
|
2011-02-26 16:48:48 +01:00
|
|
|
any_locked = any(e.archive for e in episodes)
|
2011-07-05 18:32:48 +02:00
|
|
|
any_new = any(e.is_new for e in episodes)
|
2010-05-03 19:00:29 +02:00
|
|
|
downloaded = all(e.was_downloaded(and_exists=True) for e in episodes)
|
2011-07-16 18:04:07 +02:00
|
|
|
downloading = any(e.downloading for e in episodes)
|
2007-08-30 20:54:18 +02:00
|
|
|
|
2007-08-26 17:20:46 +02:00
|
|
|
menu = gtk.Menu()
|
|
|
|
|
2010-12-20 10:29:41 +01:00
|
|
|
(can_play, can_download, can_cancel, can_delete, open_instead_of_play) = self.play_or_download()
|
2007-08-26 17:20:46 +02:00
|
|
|
|
2009-04-01 21:54:17 +02:00
|
|
|
if open_instead_of_play:
|
|
|
|
item = gtk.ImageMenuItem(gtk.STOCK_OPEN)
|
2010-05-03 19:00:29 +02:00
|
|
|
elif downloaded:
|
2009-04-01 21:54:17 +02:00
|
|
|
item = gtk.ImageMenuItem(gtk.STOCK_MEDIA_PLAY)
|
2010-05-03 19:00:29 +02:00
|
|
|
else:
|
2011-07-27 13:37:58 +02:00
|
|
|
if downloading:
|
|
|
|
item = gtk.ImageMenuItem(_('Preview'))
|
|
|
|
else:
|
|
|
|
item = gtk.ImageMenuItem(_('Stream'))
|
2010-05-03 19:00:29 +02:00
|
|
|
item.set_image(gtk.image_new_from_stock(gtk.STOCK_MEDIA_PLAY, gtk.ICON_SIZE_MENU))
|
2009-04-01 21:54:17 +02:00
|
|
|
|
2011-07-27 13:37:58 +02:00
|
|
|
item.set_sensitive(can_play)
|
2009-07-13 14:09:43 +02:00
|
|
|
item.connect('activate', self.on_playback_selected_episodes)
|
2011-02-01 18:06:24 +01:00
|
|
|
menu.append(item)
|
2008-06-30 03:10:18 +02:00
|
|
|
|
2009-04-02 02:22:09 +02:00
|
|
|
if not can_cancel:
|
|
|
|
item = gtk.ImageMenuItem(_('Download'))
|
|
|
|
item.set_image(gtk.image_new_from_stock(gtk.STOCK_GO_DOWN, gtk.ICON_SIZE_MENU))
|
|
|
|
item.set_sensitive(can_download)
|
2009-07-13 14:09:43 +02:00
|
|
|
item.connect('activate', self.on_download_selected_episodes)
|
2011-02-01 18:06:24 +01:00
|
|
|
menu.append(item)
|
2009-04-02 02:22:09 +02:00
|
|
|
else:
|
|
|
|
item = gtk.ImageMenuItem(gtk.STOCK_CANCEL)
|
2009-09-02 15:57:09 +02:00
|
|
|
item.connect('activate', self.on_item_cancel_download_activate)
|
2011-02-01 18:06:24 +01:00
|
|
|
menu.append(item)
|
2007-08-26 17:20:46 +02:00
|
|
|
|
2009-04-01 21:54:17 +02:00
|
|
|
item = gtk.ImageMenuItem(gtk.STOCK_DELETE)
|
2009-08-10 23:14:35 +02:00
|
|
|
item.set_sensitive(can_delete)
|
2009-04-01 21:54:17 +02:00
|
|
|
item.connect('activate', self.on_btnDownloadedDelete_clicked)
|
2011-02-01 18:06:24 +01:00
|
|
|
menu.append(item)
|
2009-04-01 21:54:17 +02:00
|
|
|
|
2011-07-08 22:57:20 +02:00
|
|
|
result = gpodder.user_hooks.on_episodes_context_menu(episodes)
|
|
|
|
if result:
|
|
|
|
menu.append(gtk.SeparatorMenuItem())
|
|
|
|
for label, callback in result:
|
|
|
|
item = gtk.MenuItem(label)
|
|
|
|
item.connect('activate', lambda item, callback:
|
|
|
|
callback(episodes), callback)
|
|
|
|
menu.append(item)
|
2011-06-08 11:01:36 +02:00
|
|
|
|
2009-04-01 21:54:17 +02:00
|
|
|
# Ok, this probably makes sense to only display for downloaded files
|
2010-05-03 19:00:29 +02:00
|
|
|
if downloaded:
|
|
|
|
menu.append(gtk.SeparatorMenuItem())
|
|
|
|
share_item = gtk.MenuItem(_('Send to'))
|
2011-02-01 18:06:24 +01:00
|
|
|
menu.append(share_item)
|
2010-05-03 19:00:29 +02:00
|
|
|
share_menu = gtk.Menu()
|
|
|
|
|
|
|
|
item = gtk.ImageMenuItem(_('Local folder'))
|
|
|
|
item.set_image(gtk.image_new_from_stock(gtk.STOCK_DIRECTORY, gtk.ICON_SIZE_MENU))
|
2010-05-11 19:49:34 +02:00
|
|
|
item.connect('button-press-event', lambda w, ee: self.save_episodes_as_file(episodes))
|
2011-02-01 18:06:24 +01:00
|
|
|
share_menu.append(item)
|
2009-08-11 00:36:38 +02:00
|
|
|
if self.bluetooth_available:
|
2010-05-03 19:00:29 +02:00
|
|
|
item = gtk.ImageMenuItem(_('Bluetooth device'))
|
2011-07-16 18:49:19 +02:00
|
|
|
item.set_image(gtk.image_new_from_icon_name('bluetooth', gtk.ICON_SIZE_MENU))
|
2010-05-11 19:49:34 +02:00
|
|
|
item.connect('button-press-event', lambda w, ee: self.copy_episodes_bluetooth(episodes))
|
2011-02-01 18:06:24 +01:00
|
|
|
share_menu.append(item)
|
2010-05-03 19:00:29 +02:00
|
|
|
|
|
|
|
share_item.set_submenu(share_menu)
|
2007-08-26 17:20:46 +02:00
|
|
|
|
2011-07-05 18:32:48 +02:00
|
|
|
menu.append(gtk.SeparatorMenuItem())
|
|
|
|
|
|
|
|
item = gtk.CheckMenuItem(_('New'))
|
|
|
|
item.set_active(any_new)
|
|
|
|
if any_new:
|
|
|
|
item.connect('activate', lambda w: self.mark_selected_episodes_old())
|
|
|
|
else:
|
|
|
|
item.connect('activate', lambda w: self.mark_selected_episodes_new())
|
|
|
|
menu.append(item)
|
|
|
|
|
|
|
|
if downloaded:
|
|
|
|
item = gtk.CheckMenuItem(_('Archive'))
|
|
|
|
item.set_active(any_locked)
|
|
|
|
item.connect('activate', lambda w: self.on_item_toggle_lock_activate( w, False, not any_locked))
|
|
|
|
menu.append(item)
|
2008-03-19 18:07:59 +01:00
|
|
|
|
2009-04-01 21:54:17 +02:00
|
|
|
menu.append(gtk.SeparatorMenuItem())
|
|
|
|
# Single item, add episode information menu item
|
|
|
|
item = gtk.ImageMenuItem(_('Episode details'))
|
|
|
|
item.set_image(gtk.image_new_from_stock( gtk.STOCK_INFO, gtk.ICON_SIZE_MENU))
|
2009-08-10 23:14:35 +02:00
|
|
|
item.connect('activate', lambda w: self.show_episode_shownotes(episodes[0]))
|
2011-02-01 18:06:24 +01:00
|
|
|
menu.append(item)
|
2009-04-01 21:54:17 +02:00
|
|
|
|
2007-08-26 17:20:46 +02:00
|
|
|
menu.show_all()
|
2011-02-01 18:23:37 +01:00
|
|
|
# Disable tooltips while we are showing the menu, so
|
2008-06-15 14:28:24 +02:00
|
|
|
# the tooltip will not appear over the menu
|
2009-09-05 00:12:53 +02:00
|
|
|
self.treeview_allow_tooltips(self.treeAvailable, False)
|
|
|
|
menu.connect('deactivate', lambda menushell: self.treeview_allow_tooltips(self.treeAvailable, True))
|
2011-04-11 14:16:07 +02:00
|
|
|
if event is None:
|
|
|
|
func = TreeViewHelper.make_popup_position_func(treeview)
|
2011-07-16 18:49:19 +02:00
|
|
|
menu.popup(None, None, func, 3, 0)
|
2011-04-11 14:16:07 +02:00
|
|
|
else:
|
|
|
|
menu.popup(None, None, None, event.button, event.time)
|
2007-08-26 17:20:46 +02:00
|
|
|
|
|
|
|
return True
|
|
|
|
|
2008-04-06 02:19:03 +02:00
|
|
|
def set_title(self, new_title):
|
2011-07-16 18:35:14 +02:00
|
|
|
self.default_title = new_title
|
|
|
|
self.gPodder.set_title(new_title)
|
2008-04-06 02:19:03 +02:00
|
|
|
|
2009-09-04 03:11:01 +02:00
|
|
|
def update_episode_list_icons(self, urls=None, selected=False, all=False):
|
2008-12-13 13:29:45 +01:00
|
|
|
"""
|
2009-09-04 03:11:01 +02:00
|
|
|
Updates the status icons in the episode list.
|
|
|
|
|
|
|
|
If urls is given, it should be a list of URLs
|
|
|
|
of episodes that should be updated.
|
|
|
|
|
|
|
|
If urls is None, set ONE OF selected, all to
|
|
|
|
True (the former updates just the selected
|
|
|
|
episodes and the latter updates all episodes).
|
2008-12-13 13:29:45 +01:00
|
|
|
"""
|
2011-09-19 10:20:04 +02:00
|
|
|
descriptions = self.config.episode_list_descriptions
|
2010-01-07 22:30:58 +01:00
|
|
|
|
2009-09-04 03:11:01 +02:00
|
|
|
if urls is not None:
|
|
|
|
# We have a list of URLs to walk through
|
2011-07-16 18:04:07 +02:00
|
|
|
self.episode_list_model.update_by_urls(urls, descriptions)
|
2009-09-04 03:11:01 +02:00
|
|
|
elif selected and not all:
|
|
|
|
# We should update all selected episodes
|
|
|
|
selection = self.treeAvailable.get_selection()
|
|
|
|
model, paths = selection.get_selected_rows()
|
|
|
|
for path in reversed(paths):
|
|
|
|
iter = model.get_iter(path)
|
2011-07-16 18:04:07 +02:00
|
|
|
self.episode_list_model.update_by_filter_iter(iter, descriptions)
|
2009-09-04 03:11:01 +02:00
|
|
|
elif all and not selected:
|
|
|
|
# We update all (even the filter-hidden) episodes
|
2011-07-16 18:04:07 +02:00
|
|
|
self.episode_list_model.update_all(descriptions)
|
2009-09-04 03:11:01 +02:00
|
|
|
else:
|
|
|
|
# Wrong/invalid call - have to specify at least one parameter
|
|
|
|
raise ValueError('Invalid call to update_episode_list_icons')
|
2008-12-13 14:41:32 +01:00
|
|
|
|
2009-09-02 15:57:09 +02:00
|
|
|
def episode_list_status_changed(self, episodes):
|
2009-09-22 00:42:36 +02:00
|
|
|
self.update_episode_list_icons(set(e.url for e in episodes))
|
|
|
|
self.update_podcast_list_model(set(e.channel.url for e in episodes))
|
|
|
|
self.db.commit()
|
2009-09-02 15:57:09 +02:00
|
|
|
|
2009-08-24 18:11:58 +02:00
|
|
|
def clean_up_downloads(self, delete_partial=False):
|
|
|
|
# Clean up temporary files left behind by old gPodder versions
|
2010-12-20 03:09:35 +01:00
|
|
|
temporary_files = glob.glob('%s/*/.tmp-*' % gpodder.downloads)
|
2009-08-24 18:11:58 +02:00
|
|
|
|
|
|
|
if delete_partial:
|
2010-12-20 03:09:35 +01:00
|
|
|
temporary_files += glob.glob('%s/*/*.partial' % gpodder.downloads)
|
2009-08-24 18:11:58 +02:00
|
|
|
|
|
|
|
for tempfile in temporary_files:
|
|
|
|
util.delete_file(tempfile)
|
|
|
|
|
|
|
|
|
|
|
|
def streaming_possible(self):
|
2011-07-16 18:35:14 +02:00
|
|
|
# User has to have a media player set on the Desktop, or else we
|
|
|
|
# would probably open the browser when giving a URL to xdg-open..
|
|
|
|
return (self.config.player and self.config.player != 'default')
|
2009-08-24 18:11:58 +02:00
|
|
|
|
|
|
|
def playback_episodes_for_real(self, episodes):
|
|
|
|
groups = collections.defaultdict(list)
|
|
|
|
for episode in episodes:
|
|
|
|
file_type = episode.file_type()
|
|
|
|
if file_type == 'video' and self.config.videoplayer and \
|
|
|
|
self.config.videoplayer != 'default':
|
|
|
|
player = self.config.videoplayer
|
|
|
|
elif file_type == 'audio' and self.config.player and \
|
|
|
|
self.config.player != 'default':
|
|
|
|
player = self.config.player
|
|
|
|
else:
|
|
|
|
player = 'default'
|
|
|
|
|
2011-02-01 14:38:37 +01:00
|
|
|
# Mark episode as played in the database
|
2011-02-11 16:25:56 +01:00
|
|
|
episode.playback_mark()
|
2011-02-01 14:38:37 +01:00
|
|
|
self.mygpo_client.on_playback([episode])
|
2009-09-09 14:06:17 +02:00
|
|
|
|
2011-06-08 11:01:36 +02:00
|
|
|
fmt_id = self.config.youtube_preferred_fmt_id
|
2011-07-27 13:37:58 +02:00
|
|
|
allow_partial = (player != 'default')
|
|
|
|
filename = episode.get_playback_url(fmt_id, allow_partial)
|
2010-06-13 02:19:38 +02:00
|
|
|
|
2010-08-20 01:12:22 +02:00
|
|
|
# Determine the playback resume position - if the file
|
|
|
|
# was played 100%, we simply start from the beginning
|
|
|
|
resume_position = episode.current_position
|
|
|
|
if resume_position == episode.total_time:
|
|
|
|
resume_position = 0
|
|
|
|
|
2010-06-13 02:19:38 +02:00
|
|
|
# If Panucci is configured, use D-Bus on Maemo to call it
|
2010-06-13 02:22:25 +02:00
|
|
|
if player == 'panucci':
|
2010-06-13 02:19:38 +02:00
|
|
|
try:
|
|
|
|
PANUCCI_NAME = 'org.panucci.panucciInterface'
|
|
|
|
PANUCCI_PATH = '/panucciInterface'
|
|
|
|
PANUCCI_INTF = 'org.panucci.panucciInterface'
|
2010-08-17 23:52:27 +02:00
|
|
|
o = gpodder.dbus_session_bus.get_object(PANUCCI_NAME, PANUCCI_PATH)
|
2010-06-13 02:19:38 +02:00
|
|
|
i = dbus.Interface(o, PANUCCI_INTF)
|
|
|
|
|
|
|
|
def on_reply(*args):
|
|
|
|
pass
|
|
|
|
|
2010-08-27 14:55:55 +02:00
|
|
|
def error_handler(filename, err):
|
2011-07-15 16:32:06 +02:00
|
|
|
logger.error('Exception in D-Bus call: %s', str(err))
|
2010-06-13 02:19:38 +02:00
|
|
|
|
2010-08-27 14:55:55 +02:00
|
|
|
# Fallback: use the command line client
|
|
|
|
for command in util.format_desktop_command('panucci', \
|
|
|
|
[filename]):
|
2011-07-15 16:32:06 +02:00
|
|
|
logger.info('Executing: %s', repr(command))
|
2010-08-27 14:55:55 +02:00
|
|
|
subprocess.Popen(command)
|
|
|
|
|
|
|
|
on_error = lambda err: error_handler(filename, err)
|
|
|
|
|
2010-06-13 02:19:38 +02:00
|
|
|
# This method only exists in Panucci > 0.9 ('new Panucci')
|
|
|
|
i.playback_from(filename, resume_position, \
|
|
|
|
reply_handler=on_reply, error_handler=on_error)
|
|
|
|
|
|
|
|
continue # This file was handled by the D-Bus call
|
|
|
|
except Exception, e:
|
2011-07-15 16:32:06 +02:00
|
|
|
logger.error('Calling Panucci using D-Bus', exc_info=True)
|
2010-06-13 02:19:38 +02:00
|
|
|
|
2009-08-24 18:11:58 +02:00
|
|
|
groups[player].append(filename)
|
|
|
|
|
|
|
|
# Open episodes with system default player
|
|
|
|
if 'default' in groups:
|
2010-11-23 12:34:24 +01:00
|
|
|
# Special-casing for a single episode when the object is a PDF
|
|
|
|
# file - this is needed on Maemo 5, so we only use gui_open()
|
|
|
|
# for single PDF files, but still use the built-in media player
|
|
|
|
# with an M3U file for single audio/video files. (The Maemo 5
|
|
|
|
# media player behaves differently when opening a single-file
|
|
|
|
# M3U playlist compared to opening the single file directly.)
|
|
|
|
if len(groups['default']) == 1:
|
|
|
|
fn = groups['default'][0]
|
|
|
|
# The list of extensions is taken from gui_open in util.py
|
|
|
|
# where all special-cases of Maemo apps are listed
|
|
|
|
for extension in ('.pdf', '.jpg', '.jpeg', '.png'):
|
|
|
|
if fn.lower().endswith(extension):
|
|
|
|
util.gui_open(fn)
|
|
|
|
groups['default'] = []
|
|
|
|
break
|
|
|
|
|
2011-07-16 18:35:14 +02:00
|
|
|
for filename in groups['default']:
|
|
|
|
logger.debug('Opening with system default: %s', filename)
|
|
|
|
util.gui_open(filename)
|
2009-08-24 18:11:58 +02:00
|
|
|
del groups['default']
|
2009-07-13 15:32:46 +02:00
|
|
|
|
2009-09-22 16:36:07 +02:00
|
|
|
# For each type now, go and create play commands
|
|
|
|
for group in groups:
|
2011-07-05 16:08:25 +02:00
|
|
|
for command in util.format_desktop_command(group, groups[group], resume_position):
|
2011-07-15 16:32:06 +02:00
|
|
|
logger.debug('Executing: %s', repr(command))
|
2009-09-22 16:36:07 +02:00
|
|
|
subprocess.Popen(command)
|
|
|
|
|
2010-04-09 01:01:12 +02:00
|
|
|
# Persist episode status changes to the database
|
|
|
|
self.db.commit()
|
|
|
|
|
2010-01-28 23:58:28 +01:00
|
|
|
# Flush updated episode status
|
|
|
|
self.mygpo_client.flush()
|
|
|
|
|
2009-09-22 16:36:07 +02:00
|
|
|
def playback_episodes(self, episodes):
|
2010-02-05 21:03:34 +01:00
|
|
|
# We need to create a list, because we run through it more than once
|
2010-12-20 15:17:48 +01:00
|
|
|
episodes = list(Model.sort_episodes_by_pubdate(e for e in episodes if \
|
2010-02-05 21:03:34 +01:00
|
|
|
e.was_downloaded(and_exists=True) or self.streaming_possible()))
|
2009-07-13 15:32:46 +02:00
|
|
|
|
|
|
|
try:
|
2009-08-24 18:11:58 +02:00
|
|
|
self.playback_episodes_for_real(episodes)
|
2009-07-13 15:32:46 +02:00
|
|
|
except Exception, e:
|
2011-07-15 16:32:06 +02:00
|
|
|
logger.error('Error in playback!', exc_info=True)
|
2011-07-16 18:38:19 +02:00
|
|
|
self.show_message(_('Please check your media player settings in the preferences dialog.'), \
|
|
|
|
_('Error opening player'), widget=self.toolPreferences)
|
2009-07-13 15:32:46 +02:00
|
|
|
|
2009-09-04 03:11:01 +02:00
|
|
|
channel_urls = set()
|
|
|
|
episode_urls = set()
|
|
|
|
for episode in episodes:
|
|
|
|
channel_urls.add(episode.channel.url)
|
|
|
|
episode_urls.add(episode.url)
|
|
|
|
self.update_episode_list_icons(episode_urls)
|
|
|
|
self.update_podcast_list_model(channel_urls)
|
2007-04-03 13:21:12 +02:00
|
|
|
|
2008-06-30 03:10:18 +02:00
|
|
|
def play_or_download(self):
|
2011-07-16 18:35:14 +02:00
|
|
|
if self.wNotebook.get_current_page() > 0:
|
2011-07-16 18:38:19 +02:00
|
|
|
self.toolCancel.set_sensitive(True)
|
2011-07-16 18:35:14 +02:00
|
|
|
return
|
2007-03-31 04:00:30 +02:00
|
|
|
|
2009-09-15 00:21:12 +02:00
|
|
|
if self.currently_updating:
|
|
|
|
return (False, False, False, False, False, False)
|
|
|
|
|
2010-12-20 10:29:41 +01:00
|
|
|
( can_play, can_download, can_cancel, can_delete ) = (False,)*4
|
2008-06-30 03:10:18 +02:00
|
|
|
( is_played, is_locked ) = (False,)*2
|
2006-12-13 00:11:34 +01:00
|
|
|
|
2008-08-04 23:26:51 +02:00
|
|
|
open_instead_of_play = False
|
|
|
|
|
2007-08-26 17:20:46 +02:00
|
|
|
selection = self.treeAvailable.get_selection()
|
|
|
|
if selection.count_selected_rows() > 0:
|
|
|
|
(model, paths) = selection.get_selected_rows()
|
|
|
|
|
|
|
|
for path in paths:
|
2010-05-28 19:43:32 +02:00
|
|
|
try:
|
|
|
|
episode = model.get_value(model.get_iter(path), EpisodeListModel.C_EPISODE)
|
|
|
|
except TypeError, te:
|
2011-07-15 16:32:06 +02:00
|
|
|
logger.error('Invalid episode at path %s', str(path))
|
2010-05-28 19:43:32 +02:00
|
|
|
continue
|
2008-06-30 03:10:18 +02:00
|
|
|
|
2008-08-04 23:26:51 +02:00
|
|
|
if episode.file_type() not in ('audio', 'video'):
|
|
|
|
open_instead_of_play = True
|
|
|
|
|
2008-10-15 10:06:56 +02:00
|
|
|
if episode.was_downloaded():
|
|
|
|
can_play = episode.was_downloaded(and_exists=True)
|
2011-02-26 16:32:34 +01:00
|
|
|
is_played = not episode.is_new
|
2011-02-26 16:48:48 +01:00
|
|
|
is_locked = episode.archive
|
2008-10-15 10:06:56 +02:00
|
|
|
if not can_play:
|
|
|
|
can_download = True
|
2007-08-26 17:20:46 +02:00
|
|
|
else:
|
2011-07-16 18:04:07 +02:00
|
|
|
if episode.downloading:
|
2007-08-26 17:20:46 +02:00
|
|
|
can_cancel = True
|
|
|
|
else:
|
|
|
|
can_download = True
|
2006-12-13 00:11:34 +01:00
|
|
|
|
2009-09-13 16:48:27 +02:00
|
|
|
can_download = can_download and not can_cancel
|
|
|
|
can_play = self.streaming_possible() or (can_play and not can_cancel and not can_download)
|
2010-03-11 20:01:38 +01:00
|
|
|
can_delete = not can_cancel
|
2007-08-26 17:20:46 +02:00
|
|
|
|
2011-07-16 18:38:19 +02:00
|
|
|
if open_instead_of_play:
|
|
|
|
self.toolPlay.set_stock_id(gtk.STOCK_OPEN)
|
|
|
|
else:
|
|
|
|
self.toolPlay.set_stock_id(gtk.STOCK_MEDIA_PLAY)
|
|
|
|
self.toolPlay.set_sensitive( can_play)
|
|
|
|
self.toolDownload.set_sensitive( can_download)
|
|
|
|
self.toolCancel.set_sensitive( can_cancel)
|
2007-08-26 17:20:46 +02:00
|
|
|
|
2011-07-16 18:35:14 +02:00
|
|
|
self.item_cancel_download.set_sensitive(can_cancel)
|
|
|
|
self.itemDownloadSelected.set_sensitive(can_download)
|
|
|
|
self.itemOpenSelected.set_sensitive(can_play)
|
|
|
|
self.itemPlaySelected.set_sensitive(can_play)
|
|
|
|
self.itemDeleteSelected.set_sensitive(can_delete)
|
|
|
|
self.item_toggle_played.set_sensitive(can_play)
|
|
|
|
self.item_toggle_lock.set_sensitive(can_play)
|
|
|
|
self.itemOpenSelected.set_visible(open_instead_of_play)
|
|
|
|
self.itemPlaySelected.set_visible(not open_instead_of_play)
|
2009-04-01 21:54:17 +02:00
|
|
|
|
2010-12-20 10:29:41 +01:00
|
|
|
return (can_play, can_download, can_cancel, can_delete, open_instead_of_play)
|
2006-12-13 00:11:34 +01:00
|
|
|
|
2008-04-17 17:45:29 +02:00
|
|
|
def on_cbMaxDownloads_toggled(self, widget, *args):
|
|
|
|
self.spinMaxDownloads.set_sensitive(self.cbMaxDownloads.get_active())
|
2008-09-06 22:34:35 +02:00
|
|
|
|
2008-04-17 17:45:29 +02:00
|
|
|
def on_cbLimitDownloads_toggled(self, widget, *args):
|
2008-11-06 17:01:43 +01:00
|
|
|
self.spinLimitDownloads.set_sensitive(self.cbLimitDownloads.get_active())
|
|
|
|
|
2008-12-13 14:41:32 +01:00
|
|
|
def episode_new_status_changed(self, urls):
|
2009-09-04 03:11:01 +02:00
|
|
|
self.update_podcast_list_model()
|
2008-12-13 14:41:32 +01:00
|
|
|
self.update_episode_list_icons(urls)
|
2008-04-17 17:45:29 +02:00
|
|
|
|
2011-07-28 13:50:13 +02:00
|
|
|
def update_podcast_list_model(self, urls=None, selected=False, select_url=None,
|
|
|
|
sections_changed=False):
|
2009-09-04 03:11:01 +02:00
|
|
|
"""Update the podcast list treeview model
|
|
|
|
|
|
|
|
If urls is given, it should list the URLs of each
|
|
|
|
podcast that has to be updated in the list.
|
|
|
|
|
|
|
|
If selected is True, only update the model contents
|
|
|
|
for the currently-selected podcast - nothing more.
|
|
|
|
|
|
|
|
The caller can optionally specify "select_url",
|
|
|
|
which is the URL of the podcast that is to be
|
|
|
|
selected in the list after the update is complete.
|
|
|
|
This only works if the podcast list has to be
|
|
|
|
reloaded; i.e. something has been added or removed
|
|
|
|
since the last update of the podcast list).
|
|
|
|
"""
|
2008-12-13 13:29:45 +01:00
|
|
|
selection = self.treeChannels.get_selection()
|
2009-09-04 03:11:01 +02:00
|
|
|
model, iter = selection.get_selected()
|
2007-09-19 17:04:42 +02:00
|
|
|
|
2011-09-18 22:39:34 +02:00
|
|
|
is_section = lambda r: r[PodcastListModel.C_URL] == '-'
|
|
|
|
is_separator = lambda r: r[PodcastListModel.C_SEPARATOR]
|
|
|
|
sections_active = any(is_section(x) for x in self.podcast_list_model)
|
|
|
|
|
2011-07-16 20:51:26 +02:00
|
|
|
if self.config.podcast_list_view_all:
|
2010-02-22 23:22:58 +01:00
|
|
|
# Update "all episodes" view in any case (if enabled)
|
|
|
|
self.podcast_list_model.update_first_row()
|
2011-09-18 22:39:34 +02:00
|
|
|
# List model length minus 1, because of "All"
|
|
|
|
list_model_length = len(self.podcast_list_model) - 1
|
2011-07-16 20:51:26 +02:00
|
|
|
else:
|
|
|
|
list_model_length = len(self.podcast_list_model)
|
2010-02-22 23:22:58 +01:00
|
|
|
|
2011-07-28 13:50:13 +02:00
|
|
|
force_update = (sections_active != self.config.podcast_list_sections or
|
|
|
|
sections_changed)
|
2011-07-27 14:19:02 +02:00
|
|
|
|
2011-09-18 22:39:34 +02:00
|
|
|
# Filter items in the list model that are not podcasts, so we get the
|
|
|
|
# correct podcast list count (ignore section headers and separators)
|
|
|
|
is_not_podcast = lambda r: is_section(r) or is_separator(r)
|
|
|
|
list_model_length -= len(filter(is_not_podcast, self.podcast_list_model))
|
|
|
|
|
2011-07-28 13:50:13 +02:00
|
|
|
if selected and not force_update:
|
2008-12-13 13:29:45 +01:00
|
|
|
# very cheap! only update selected channel
|
2009-09-04 03:11:01 +02:00
|
|
|
if iter is not None:
|
2010-02-22 23:22:58 +01:00
|
|
|
# If we have selected the "all episodes" view, we have
|
|
|
|
# to update all channels for selected episodes:
|
|
|
|
if self.config.podcast_list_view_all and \
|
|
|
|
self.podcast_list_model.iter_is_first_row(iter):
|
|
|
|
urls = self.get_podcast_urls_from_selected_episodes()
|
|
|
|
self.podcast_list_model.update_by_urls(urls)
|
|
|
|
else:
|
|
|
|
# Otherwise just update the selected row (a podcast)
|
|
|
|
self.podcast_list_model.update_by_filter_iter(iter)
|
2011-07-28 14:58:17 +02:00
|
|
|
|
|
|
|
if self.config.podcast_list_sections:
|
|
|
|
self.podcast_list_model.update_sections()
|
2011-07-27 14:19:02 +02:00
|
|
|
elif list_model_length == len(self.channels) and not force_update:
|
2008-12-13 13:29:45 +01:00
|
|
|
# we can keep the model, but have to update some
|
2009-09-04 03:11:01 +02:00
|
|
|
if urls is None:
|
2008-12-13 13:29:45 +01:00
|
|
|
# still cheaper than reloading the whole list
|
2009-09-14 20:46:42 +02:00
|
|
|
self.podcast_list_model.update_all()
|
2008-12-13 13:29:45 +01:00
|
|
|
else:
|
|
|
|
# ok, we got a bunch of urls to update
|
2009-09-04 03:11:01 +02:00
|
|
|
self.podcast_list_model.update_by_urls(urls)
|
2011-07-28 14:58:17 +02:00
|
|
|
if self.config.podcast_list_sections:
|
|
|
|
self.podcast_list_model.update_sections()
|
2008-09-06 22:34:35 +02:00
|
|
|
else:
|
2009-09-04 03:11:01 +02:00
|
|
|
if model and iter and select_url is None:
|
2008-09-06 22:34:35 +02:00
|
|
|
# Get the URL of the currently-selected podcast
|
2009-09-04 03:11:01 +02:00
|
|
|
select_url = model.get_value(iter, PodcastListModel.C_URL)
|
2008-06-30 03:10:18 +02:00
|
|
|
|
2009-08-13 22:08:47 +02:00
|
|
|
# Update the podcast list model with new channels
|
2009-12-16 14:55:55 +01:00
|
|
|
self.podcast_list_model.set_channels(self.db, self.config, self.channels)
|
2008-05-14 15:38:06 +02:00
|
|
|
|
2008-09-06 22:34:35 +02:00
|
|
|
try:
|
2009-09-04 03:11:01 +02:00
|
|
|
selected_iter = model.get_iter_first()
|
2008-09-06 22:34:35 +02:00
|
|
|
# Find the previously-selected URL in the new
|
|
|
|
# model if we have an URL (else select first)
|
2009-09-04 03:11:01 +02:00
|
|
|
if select_url is not None:
|
2008-09-06 22:34:35 +02:00
|
|
|
pos = model.get_iter_first()
|
|
|
|
while pos is not None:
|
2009-09-04 03:11:01 +02:00
|
|
|
url = model.get_value(pos, PodcastListModel.C_URL)
|
|
|
|
if url == select_url:
|
|
|
|
selected_iter = pos
|
2008-09-06 22:34:35 +02:00
|
|
|
break
|
|
|
|
pos = model.iter_next(pos)
|
|
|
|
|
2011-07-16 18:35:14 +02:00
|
|
|
if selected_iter is not None:
|
|
|
|
selection.select_iter(selected_iter)
|
|
|
|
self.on_treeChannels_cursor_changed(self.treeChannels)
|
2008-09-06 22:34:35 +02:00
|
|
|
except:
|
2011-07-15 16:32:06 +02:00
|
|
|
logger.error('Cannot select podcast in list', exc_info=True)
|
2009-04-01 01:12:17 +02:00
|
|
|
|
2010-11-22 14:28:27 +01:00
|
|
|
def on_episode_list_filter_changed(self, has_episodes):
|
2011-07-16 18:35:14 +02:00
|
|
|
pass # XXX: Remove?
|
2010-11-22 14:28:27 +01:00
|
|
|
|
2009-09-04 03:11:01 +02:00
|
|
|
def update_episode_list_model(self):
|
2008-03-04 12:06:55 +01:00
|
|
|
if self.channels and self.active_channel is not None:
|
2009-01-19 19:00:24 +01:00
|
|
|
self.currently_updating = True
|
2010-11-20 23:45:07 +01:00
|
|
|
self.episode_list_model.clear()
|
2010-11-19 15:36:39 +01:00
|
|
|
|
|
|
|
def update():
|
2011-09-19 10:20:04 +02:00
|
|
|
descriptions = self.config.episode_list_descriptions
|
2011-07-16 18:04:07 +02:00
|
|
|
self.episode_list_model.replace_from_channel(self.active_channel, descriptions)
|
2009-09-04 03:11:01 +02:00
|
|
|
|
2010-11-19 15:36:39 +01:00
|
|
|
self.treeAvailable.get_selection().unselect_all()
|
2010-11-20 23:45:07 +01:00
|
|
|
self.treeAvailable.scroll_to_point(0, 0)
|
|
|
|
|
2010-11-19 15:36:39 +01:00
|
|
|
self.currently_updating = False
|
|
|
|
self.play_or_download()
|
|
|
|
|
|
|
|
util.idle_add(update)
|
2006-12-06 21:25:26 +01:00
|
|
|
else:
|
2009-08-13 23:19:12 +02:00
|
|
|
self.episode_list_model.clear()
|
2010-08-16 22:48:12 +02:00
|
|
|
|
|
|
|
@dbus.service.method(gpodder.dbus_interface)
|
2009-09-09 00:20:24 +02:00
|
|
|
def offer_new_episodes(self, channels=None):
|
|
|
|
new_episodes = self.get_new_episodes(channels)
|
2009-08-25 16:19:14 +02:00
|
|
|
if new_episodes:
|
|
|
|
self.new_episodes_show(new_episodes)
|
|
|
|
return True
|
|
|
|
return False
|
2008-09-23 03:48:59 +02:00
|
|
|
|
2009-09-05 20:40:35 +02:00
|
|
|
def add_podcast_list(self, urls, auth_tokens=None):
|
|
|
|
"""Subscribe to a list of podcast given their URLs
|
2009-08-25 16:19:14 +02:00
|
|
|
|
2009-09-05 20:40:35 +02:00
|
|
|
If auth_tokens is given, it should be a dictionary
|
|
|
|
mapping URLs to (username, password) tuples."""
|
|
|
|
|
|
|
|
if auth_tokens is None:
|
|
|
|
auth_tokens = {}
|
|
|
|
|
|
|
|
# Sort and split the URL list into five buckets
|
|
|
|
queued, failed, existing, worked, authreq = [], [], [], [], []
|
2009-08-25 16:19:14 +02:00
|
|
|
for input_url in urls:
|
|
|
|
url = util.normalize_feed_url(input_url)
|
|
|
|
if url is None:
|
|
|
|
# Fail this one because the URL is not valid
|
|
|
|
failed.append(input_url)
|
2009-09-01 18:56:30 +02:00
|
|
|
elif self.podcast_list_model.get_filter_path_from_url(url) is not None:
|
2009-08-25 16:19:14 +02:00
|
|
|
# A podcast already exists in the list for this URL
|
|
|
|
existing.append(url)
|
|
|
|
else:
|
|
|
|
# This URL has survived the first round - queue for add
|
|
|
|
queued.append(url)
|
2009-09-05 20:40:35 +02:00
|
|
|
if url != input_url and input_url in auth_tokens:
|
|
|
|
auth_tokens[url] = auth_tokens[input_url]
|
|
|
|
|
|
|
|
error_messages = {}
|
|
|
|
redirections = {}
|
2009-08-25 16:19:14 +02:00
|
|
|
|
2009-09-12 15:32:10 +02:00
|
|
|
progress = ProgressIndicator(_('Adding podcasts'), \
|
|
|
|
_('Please wait while episode information is downloaded.'), \
|
2010-05-10 13:33:50 +02:00
|
|
|
parent=self.get_dialog_parent())
|
2009-09-12 15:32:10 +02:00
|
|
|
|
|
|
|
def on_after_update():
|
|
|
|
progress.on_finished()
|
|
|
|
# Report already-existing subscriptions to the user
|
|
|
|
if existing:
|
|
|
|
title = _('Existing subscriptions skipped')
|
|
|
|
message = _('You are already subscribed to these podcasts:') \
|
2011-02-25 21:05:26 +01:00
|
|
|
+ '\n\n' + '\n'.join(cgi.escape(url) for url in existing)
|
2009-09-12 15:32:10 +02:00
|
|
|
self.show_message(message, title, widget=self.treeChannels)
|
|
|
|
|
|
|
|
# Report subscriptions that require authentication
|
|
|
|
if authreq:
|
|
|
|
retry_podcasts = {}
|
|
|
|
for url in authreq:
|
|
|
|
title = _('Podcast requires authentication')
|
2011-02-25 21:05:26 +01:00
|
|
|
message = _('Please login to %s:') % (cgi.escape(url),)
|
2009-09-12 15:32:10 +02:00
|
|
|
success, auth_tokens = self.show_login_dialog(title, message)
|
|
|
|
if success:
|
|
|
|
retry_podcasts[url] = auth_tokens
|
|
|
|
else:
|
|
|
|
# Stop asking the user for more login data
|
|
|
|
retry_podcasts = {}
|
|
|
|
for url in authreq:
|
|
|
|
error_messages[url] = _('Authentication failed')
|
|
|
|
failed.append(url)
|
|
|
|
break
|
2009-09-05 20:40:35 +02:00
|
|
|
|
2009-09-12 15:32:10 +02:00
|
|
|
# If we have authentication data to retry, do so here
|
|
|
|
if retry_podcasts:
|
|
|
|
self.add_podcast_list(retry_podcasts.keys(), retry_podcasts)
|
|
|
|
|
|
|
|
# Report website redirections
|
|
|
|
for url in redirections:
|
|
|
|
title = _('Website redirection detected')
|
2010-01-18 21:20:22 +01:00
|
|
|
message = _('The URL %(url)s redirects to %(target)s.') \
|
2009-09-12 15:32:10 +02:00
|
|
|
+ '\n\n' + _('Do you want to visit the website now?')
|
2010-01-18 21:20:22 +01:00
|
|
|
message = message % {'url': url, 'target': redirections[url]}
|
2009-09-12 15:32:10 +02:00
|
|
|
if self.show_confirmation(message, title):
|
2009-09-22 16:18:45 +02:00
|
|
|
util.open_website(url)
|
2009-09-05 20:40:35 +02:00
|
|
|
else:
|
2009-09-12 15:32:10 +02:00
|
|
|
break
|
|
|
|
|
|
|
|
# Report failed subscriptions to the user
|
|
|
|
if failed:
|
|
|
|
title = _('Could not add some podcasts')
|
|
|
|
message = _('Some podcasts could not be added to your list:') \
|
2011-02-25 21:05:26 +01:00
|
|
|
+ '\n\n' + '\n'.join(cgi.escape('%s: %s' % (url, \
|
2009-09-12 15:32:10 +02:00
|
|
|
error_messages.get(url, _('Unknown')))) for url in failed)
|
|
|
|
self.show_message(message, title, important=True)
|
|
|
|
|
2010-04-02 23:19:04 +02:00
|
|
|
# Upload subscription changes to gpodder.net
|
2010-01-19 23:43:59 +01:00
|
|
|
self.mygpo_client.on_subscribe(worked)
|
|
|
|
|
2011-07-16 20:51:26 +02:00
|
|
|
# Fix URLs if mygpo has rewritten them
|
|
|
|
self.rewrite_urls_mygpo()
|
2010-01-28 17:39:10 +01:00
|
|
|
|
2011-07-16 20:51:26 +02:00
|
|
|
# If only one podcast was added, select it after the update
|
|
|
|
if len(worked) == 1:
|
|
|
|
url = worked[0]
|
|
|
|
else:
|
|
|
|
url = None
|
2009-09-12 15:32:10 +02:00
|
|
|
|
2011-07-16 20:51:26 +02:00
|
|
|
# Update the list of subscribed podcasts
|
|
|
|
self.update_podcast_list_model(select_url=url)
|
2009-09-12 15:32:10 +02:00
|
|
|
|
2011-07-16 20:51:26 +02:00
|
|
|
# Offer to download new episodes
|
|
|
|
episodes = []
|
|
|
|
for podcast in self.channels:
|
|
|
|
if podcast.url in worked:
|
|
|
|
episodes.extend(podcast.get_all_episodes())
|
2010-08-24 00:24:15 +02:00
|
|
|
|
2011-07-16 20:51:26 +02:00
|
|
|
if episodes:
|
|
|
|
episodes = list(Model.sort_episodes_by_pubdate(episodes, \
|
|
|
|
reverse=True))
|
|
|
|
self.new_episodes_show(episodes, \
|
|
|
|
selected=[e.check_is_new() for e in episodes])
|
2010-08-24 00:24:15 +02:00
|
|
|
|
2009-09-12 15:32:10 +02:00
|
|
|
|
|
|
|
def thread_proc():
|
|
|
|
# After the initial sorting and splitting, try all queued podcasts
|
|
|
|
length = len(queued)
|
|
|
|
for index, url in enumerate(queued):
|
|
|
|
progress.on_progress(float(index)/float(length))
|
|
|
|
progress.on_message(url)
|
|
|
|
try:
|
|
|
|
# The URL is valid and does not exist already - subscribe!
|
2011-10-12 19:59:09 +02:00
|
|
|
channel = self.model.load_podcast(url=url, create=True, \
|
2009-09-12 15:32:10 +02:00
|
|
|
authentication_tokens=auth_tokens.get(url, None), \
|
|
|
|
max_episodes=self.config.max_episodes_per_feed, \
|
2010-08-20 23:36:57 +02:00
|
|
|
mimetype_prefs=self.config.mimetype_prefs)
|
2009-09-12 15:32:10 +02:00
|
|
|
|
|
|
|
try:
|
|
|
|
username, password = util.username_password_from_url(url)
|
|
|
|
except ValueError, ve:
|
|
|
|
username, password = (None, None)
|
|
|
|
|
2010-12-20 14:35:46 +01:00
|
|
|
if username is not None and channel.auth_username is None and \
|
|
|
|
password is not None and channel.auth_password is None:
|
|
|
|
channel.auth_username = username
|
|
|
|
channel.auth_password = password
|
2009-09-12 15:32:10 +02:00
|
|
|
channel.save()
|
|
|
|
|
|
|
|
self._update_cover(channel)
|
|
|
|
except feedcore.AuthenticationRequired:
|
|
|
|
if url in auth_tokens:
|
|
|
|
# Fail for wrong authentication data
|
2009-09-05 20:40:35 +02:00
|
|
|
error_messages[url] = _('Authentication failed')
|
|
|
|
failed.append(url)
|
2009-09-12 15:32:10 +02:00
|
|
|
else:
|
|
|
|
# Queue for login dialog later
|
|
|
|
authreq.append(url)
|
|
|
|
continue
|
|
|
|
except feedcore.WifiLogin, error:
|
|
|
|
redirections[url] = error.data
|
|
|
|
failed.append(url)
|
|
|
|
error_messages[url] = _('Redirection detected')
|
|
|
|
continue
|
|
|
|
except Exception, e:
|
2011-07-15 16:32:06 +02:00
|
|
|
logger.error('Subscription error: %s', e, exc_info=True)
|
2009-09-12 15:32:10 +02:00
|
|
|
error_messages[url] = str(e)
|
|
|
|
failed.append(url)
|
|
|
|
continue
|
|
|
|
|
|
|
|
assert channel is not None
|
|
|
|
worked.append(channel.url)
|
2011-07-16 20:51:26 +02:00
|
|
|
|
2009-09-12 15:32:10 +02:00
|
|
|
util.idle_add(on_after_update)
|
|
|
|
threading.Thread(target=thread_proc).start()
|
2008-10-20 06:17:22 +02:00
|
|
|
|
2010-06-04 19:42:34 +02:00
|
|
|
def find_episode(self, podcast_url, episode_url):
|
|
|
|
"""Find an episode given its podcast and episode URL
|
|
|
|
|
|
|
|
The function will return a PodcastEpisode object if
|
|
|
|
the episode is found, or None if it's not found.
|
|
|
|
"""
|
|
|
|
for podcast in self.channels:
|
|
|
|
if podcast_url == podcast.url:
|
|
|
|
for episode in podcast.get_all_episodes():
|
|
|
|
if episode_url == episode.url:
|
|
|
|
return episode
|
|
|
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
def process_received_episode_actions(self, updated_urls):
|
|
|
|
"""Process/merge episode actions from gpodder.net
|
|
|
|
|
|
|
|
This function will merge all changes received from
|
|
|
|
the server to the local database and update the
|
|
|
|
status of the affected episodes as necessary.
|
|
|
|
"""
|
|
|
|
indicator = ProgressIndicator(_('Merging episode actions'), \
|
|
|
|
_('Episode actions from gpodder.net are merged.'), \
|
|
|
|
False, self.get_dialog_parent())
|
|
|
|
|
|
|
|
for idx, action in enumerate(self.mygpo_client.get_episode_actions(updated_urls)):
|
|
|
|
if action.action == 'play':
|
|
|
|
episode = self.find_episode(action.podcast_url, \
|
|
|
|
action.episode_url)
|
|
|
|
|
|
|
|
if episode is not None:
|
2011-07-15 16:32:06 +02:00
|
|
|
logger.debug('Play action for %s', episode.url)
|
2010-06-04 19:42:34 +02:00
|
|
|
episode.mark(is_played=True)
|
|
|
|
|
2010-10-10 11:08:07 +02:00
|
|
|
if action.timestamp > episode.current_position_updated and \
|
|
|
|
action.position is not None:
|
2011-07-15 16:32:06 +02:00
|
|
|
logger.debug('Updating position for %s', episode.url)
|
2010-06-04 19:42:34 +02:00
|
|
|
episode.current_position = action.position
|
|
|
|
episode.current_position_updated = action.timestamp
|
|
|
|
|
|
|
|
if action.total:
|
2011-07-15 16:32:06 +02:00
|
|
|
logger.debug('Updating total time for %s', episode.url)
|
2010-06-04 19:42:34 +02:00
|
|
|
episode.total_time = action.total
|
|
|
|
|
|
|
|
episode.save()
|
|
|
|
elif action.action == 'delete':
|
|
|
|
episode = self.find_episode(action.podcast_url, \
|
|
|
|
action.episode_url)
|
|
|
|
|
|
|
|
if episode is not None:
|
|
|
|
if not episode.was_downloaded(and_exists=True):
|
|
|
|
# Set the episode to a "deleted" state
|
2011-07-15 16:32:06 +02:00
|
|
|
logger.debug('Marking as deleted: %s', episode.url)
|
2010-06-04 19:42:34 +02:00
|
|
|
episode.delete_from_disk()
|
|
|
|
episode.save()
|
|
|
|
|
2010-11-22 21:52:58 +01:00
|
|
|
indicator.on_message(N_('%(count)d action processed', '%(count)d actions processed', idx) % {'count':idx})
|
2010-06-04 19:42:34 +02:00
|
|
|
gtk.main_iteration(False)
|
|
|
|
|
|
|
|
indicator.on_finished()
|
|
|
|
self.db.commit()
|
|
|
|
|
2009-08-24 16:47:59 +02:00
|
|
|
def _update_cover(self, channel):
|
2009-08-24 21:04:50 +02:00
|
|
|
if channel is not None and not os.path.exists(channel.cover_file) and channel.image:
|
2009-08-24 16:47:59 +02:00
|
|
|
self.cover_downloader.request_cover(channel)
|
|
|
|
|
2009-06-11 18:01:01 +02:00
|
|
|
def show_update_feeds_buttons(self):
|
|
|
|
# Make sure that the buttons for updating feeds
|
|
|
|
# appear - this should happen after a feed update
|
2011-02-01 18:23:37 +01:00
|
|
|
self.hboxUpdateFeeds.hide()
|
2009-06-11 18:01:01 +02:00
|
|
|
self.btnUpdateFeeds.show()
|
|
|
|
self.itemUpdate.set_sensitive(True)
|
|
|
|
self.itemUpdateChannel.set_sensitive(True)
|
2007-03-10 16:57:56 +01:00
|
|
|
|
2008-04-22 20:34:41 +02:00
|
|
|
def on_btnCancelFeedUpdate_clicked(self, widget):
|
2009-06-11 18:01:01 +02:00
|
|
|
if not self.feed_cache_update_cancelled:
|
|
|
|
self.pbFeedUpdate.set_text(_('Cancelling...'))
|
2009-01-19 19:00:24 +01:00
|
|
|
self.feed_cache_update_cancelled = True
|
2011-07-16 18:35:14 +02:00
|
|
|
self.btnCancelFeedUpdate.set_sensitive(False)
|
|
|
|
else:
|
2009-06-11 18:01:01 +02:00
|
|
|
self.show_update_feeds_buttons()
|
2008-04-22 20:34:41 +02:00
|
|
|
|
2011-08-04 16:04:36 +02:00
|
|
|
def update_feed_cache(self, channels=None,
|
|
|
|
show_new_episodes_dialog=True):
|
2010-01-28 17:39:10 +01:00
|
|
|
# Fix URLs if mygpo has rewritten them
|
2011-07-16 20:51:26 +02:00
|
|
|
# XXX somewhere else? self.rewrite_urls_mygpo()
|
2008-08-30 19:23:04 +02:00
|
|
|
|
|
|
|
if channels is None:
|
2010-09-27 00:08:30 +02:00
|
|
|
# Only update podcasts for which updates are enabled
|
2010-12-20 14:35:46 +01:00
|
|
|
channels = [c for c in self.channels if not c.pause_subscription]
|
2008-08-30 19:23:04 +02:00
|
|
|
|
2011-07-16 18:35:14 +02:00
|
|
|
self.itemUpdate.set_sensitive(False)
|
|
|
|
self.itemUpdateChannel.set_sensitive(False)
|
2008-01-09 23:46:17 +01:00
|
|
|
|
2011-07-16 18:35:14 +02:00
|
|
|
self.feed_cache_update_cancelled = False
|
|
|
|
self.btnCancelFeedUpdate.show()
|
|
|
|
self.btnCancelFeedUpdate.set_sensitive(True)
|
|
|
|
self.btnCancelFeedUpdate.set_image(gtk.image_new_from_stock(gtk.STOCK_STOP, gtk.ICON_SIZE_BUTTON))
|
|
|
|
self.hboxUpdateFeeds.show_all()
|
|
|
|
self.btnUpdateFeeds.hide()
|
2009-06-11 18:01:01 +02:00
|
|
|
|
2011-07-16 20:51:26 +02:00
|
|
|
count = len(channels)
|
|
|
|
text = N_('Updating %(count)d feed...', 'Updating %(count)d feeds...', count) % {'count':count}
|
|
|
|
|
2010-09-29 23:51:35 +02:00
|
|
|
self.pbFeedUpdate.set_text(text)
|
|
|
|
self.pbFeedUpdate.set_fraction(0)
|
|
|
|
|
2011-07-16 20:51:26 +02:00
|
|
|
def update_feed_cache_proc():
|
|
|
|
updated_channels = []
|
|
|
|
for updated, channel in enumerate(channels):
|
|
|
|
if self.feed_cache_update_cancelled:
|
|
|
|
break
|
|
|
|
|
|
|
|
try:
|
|
|
|
channel.update(max_episodes=self.config.max_episodes_per_feed,
|
|
|
|
mimetype_prefs=self.config.mimetype_prefs)
|
|
|
|
self._update_cover(channel)
|
|
|
|
except Exception, e:
|
|
|
|
d = {'url': cgi.escape(channel.url), 'message': cgi.escape(str(e))}
|
|
|
|
if d['message']:
|
|
|
|
message = _('Error while updating %(url)s: %(message)s')
|
|
|
|
else:
|
|
|
|
message = _('The feed at %(url)s could not be updated.')
|
|
|
|
self.notification(message % d, _('Error while updating feed'), widget=self.treeChannels)
|
|
|
|
logger.error('Error: %s', str(e), exc_info=True)
|
|
|
|
|
|
|
|
updated_channels.append(channel)
|
|
|
|
|
|
|
|
def update_progress(channel):
|
|
|
|
self.update_podcast_list_model([channel.url])
|
|
|
|
|
|
|
|
# If the currently-viewed podcast is updated, reload episodes
|
|
|
|
if self.active_channel is not None and \
|
|
|
|
self.active_channel == channel:
|
|
|
|
logger.debug('Updated channel is active, updating UI')
|
|
|
|
self.update_episode_list_model()
|
|
|
|
|
|
|
|
d = {'podcast': channel.title, 'position': updated+1, 'total': count}
|
|
|
|
progression = _('Updated %(podcast)s (%(position)d/%(total)d)') % d
|
|
|
|
|
|
|
|
self.pbFeedUpdate.set_text(progression)
|
|
|
|
self.pbFeedUpdate.set_fraction(float(updated+1)/float(count))
|
|
|
|
|
|
|
|
util.idle_add(update_progress, channel)
|
|
|
|
|
|
|
|
def update_feed_cache_finish_callback():
|
|
|
|
# Process received episode actions for all updated URLs
|
|
|
|
# XXX somewhere else? self.process_received_episode_actions(updated_urls)
|
|
|
|
|
|
|
|
# If we are currently viewing "All episodes", update its episode list now
|
|
|
|
if self.active_channel is not None and \
|
|
|
|
getattr(self.active_channel, 'ALL_EPISODES_PROXY', False):
|
|
|
|
self.update_episode_list_model()
|
|
|
|
|
|
|
|
if self.feed_cache_update_cancelled:
|
|
|
|
# The user decided to abort the feed update
|
|
|
|
self.show_update_feeds_buttons()
|
|
|
|
|
|
|
|
# Only search for new episodes in podcasts that have been
|
|
|
|
# updated, not in other podcasts (for single-feed updates)
|
|
|
|
episodes = self.get_new_episodes([c for c in updated_channels])
|
|
|
|
|
|
|
|
if not episodes:
|
|
|
|
# Nothing new here - but inform the user
|
|
|
|
self.pbFeedUpdate.set_fraction(1.0)
|
|
|
|
self.pbFeedUpdate.set_text(_('No new episodes'))
|
|
|
|
self.feed_cache_update_cancelled = True
|
|
|
|
self.btnCancelFeedUpdate.show()
|
|
|
|
self.btnCancelFeedUpdate.set_sensitive(True)
|
|
|
|
self.itemUpdate.set_sensitive(True)
|
|
|
|
self.btnCancelFeedUpdate.set_image(gtk.image_new_from_stock(gtk.STOCK_APPLY, gtk.ICON_SIZE_BUTTON))
|
|
|
|
else:
|
|
|
|
count = len(episodes)
|
|
|
|
# New episodes are available
|
|
|
|
self.pbFeedUpdate.set_fraction(1.0)
|
|
|
|
# Are we minimized and should we auto download?
|
|
|
|
if (self.is_iconified() and (self.config.auto_download == 'minimized')) or (self.config.auto_download == 'always'):
|
|
|
|
self.download_episode_list(episodes)
|
|
|
|
title = N_('Downloading %(count)d new episode.', 'Downloading %(count)d new episodes.', count) % {'count':count}
|
|
|
|
self.show_message(title, _('New episodes available'), widget=self.labelDownloads)
|
|
|
|
self.show_update_feeds_buttons()
|
|
|
|
elif self.config.auto_download == 'queue':
|
|
|
|
self.download_episode_list_paused(episodes)
|
|
|
|
title = N_('%(count)d new episode added to download list.', '%(count)d new episodes added to download list.', count) % {'count':count}
|
|
|
|
self.show_message(title, _('New episodes available'), widget=self.labelDownloads)
|
|
|
|
self.show_update_feeds_buttons()
|
|
|
|
else:
|
|
|
|
self.show_update_feeds_buttons()
|
|
|
|
# New episodes are available and we are not minimized
|
2011-08-04 16:04:36 +02:00
|
|
|
if (not self.config.do_not_show_new_episodes_dialog
|
|
|
|
and show_new_episodes_dialog):
|
2011-07-16 20:51:26 +02:00
|
|
|
self.new_episodes_show(episodes, notification=True)
|
|
|
|
else:
|
|
|
|
message = N_('%(count)d new episode available', '%(count)d new episodes available', count) % {'count':count}
|
|
|
|
self.pbFeedUpdate.set_text(message)
|
|
|
|
|
|
|
|
util.idle_add(update_feed_cache_finish_callback)
|
|
|
|
|
|
|
|
threading.Thread(target=update_feed_cache_proc).start()
|
2007-03-10 16:57:56 +01:00
|
|
|
|
2008-01-09 23:46:17 +01:00
|
|
|
def on_gPodder_delete_event(self, widget, *args):
|
|
|
|
"""Called when the GUI wants to close the window
|
|
|
|
Displays a confirmation dialog (and closes/hides gPodder)
|
|
|
|
"""
|
2005-11-21 19:21:25 +01:00
|
|
|
|
2009-08-24 16:17:32 +02:00
|
|
|
downloading = self.download_status_model.are_downloads_in_progress()
|
2008-01-09 23:46:17 +01:00
|
|
|
|
2010-12-20 00:23:10 +01:00
|
|
|
if downloading:
|
2008-01-09 23:46:17 +01:00
|
|
|
dialog = gtk.MessageDialog(self.gPodder, gtk.DIALOG_MODAL, gtk.MESSAGE_QUESTION, gtk.BUTTONS_NONE)
|
|
|
|
dialog.add_button(gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL)
|
2009-08-25 12:52:23 +02:00
|
|
|
quit_button = dialog.add_button(gtk.STOCK_QUIT, gtk.RESPONSE_CLOSE)
|
2008-01-09 23:46:17 +01:00
|
|
|
|
|
|
|
title = _('Quit gPodder')
|
2010-11-22 20:03:11 +01:00
|
|
|
message = _('You are downloading episodes. You can resume downloads the next time you start gPodder. Do you want to quit now?')
|
2008-01-09 23:46:17 +01:00
|
|
|
|
|
|
|
dialog.set_title(title)
|
|
|
|
dialog.set_markup('<span weight="bold" size="larger">%s</span>\n\n%s'%(title, message))
|
|
|
|
|
2009-08-25 12:52:23 +02:00
|
|
|
quit_button.grab_focus()
|
2008-01-09 23:46:17 +01:00
|
|
|
result = dialog.run()
|
|
|
|
dialog.destroy()
|
|
|
|
|
|
|
|
if result == gtk.RESPONSE_CLOSE:
|
|
|
|
self.close_gpodder()
|
|
|
|
else:
|
|
|
|
self.close_gpodder()
|
|
|
|
|
|
|
|
return True
|
|
|
|
|
|
|
|
def close_gpodder(self):
|
|
|
|
""" clean everything and exit properly
|
|
|
|
"""
|
2008-11-19 18:55:59 +01:00
|
|
|
self.gPodder.hide()
|
2009-04-02 00:33:49 +02:00
|
|
|
|
|
|
|
# Notify all tasks to to carry out any clean-up actions
|
2009-08-24 16:17:32 +02:00
|
|
|
self.download_status_model.tell_all_tasks_to_quit()
|
2009-04-02 00:33:49 +02:00
|
|
|
|
2008-11-19 18:55:59 +01:00
|
|
|
while gtk.events_pending():
|
|
|
|
gtk.main_iteration(False)
|
|
|
|
|
2011-02-06 19:05:52 +01:00
|
|
|
self.core.shutdown()
|
2005-11-23 20:53:18 +01:00
|
|
|
|
2009-05-08 14:28:53 +02:00
|
|
|
self.quit()
|
2009-05-11 20:09:21 +02:00
|
|
|
sys.exit(0)
|
2005-11-21 19:21:25 +01:00
|
|
|
|
2010-03-01 21:29:46 +01:00
|
|
|
def get_expired_episodes(self):
|
2007-12-12 20:57:50 +01:00
|
|
|
for channel in self.channels:
|
2008-06-30 03:10:18 +02:00
|
|
|
for episode in channel.get_downloaded_episodes():
|
2011-02-26 16:48:48 +01:00
|
|
|
# Never consider archived episodes as old
|
|
|
|
if episode.archive:
|
2010-03-01 21:29:46 +01:00
|
|
|
continue
|
|
|
|
|
|
|
|
# Never consider fresh episodes as old
|
|
|
|
if episode.age_in_days() < self.config.episode_old_age:
|
|
|
|
continue
|
|
|
|
|
|
|
|
# Do not delete played episodes (except if configured)
|
2011-02-26 16:32:34 +01:00
|
|
|
if not episode.is_new:
|
2010-03-01 21:29:46 +01:00
|
|
|
if not self.config.auto_remove_played_episodes:
|
|
|
|
continue
|
|
|
|
|
2011-07-03 22:47:02 +02:00
|
|
|
# Do not delete unfinished episodes (except if configured)
|
|
|
|
if not episode.is_finished():
|
|
|
|
if not self.config.auto_remove_unfinished_episodes:
|
|
|
|
continue
|
|
|
|
|
2010-03-01 21:29:46 +01:00
|
|
|
# Do not delete unplayed episodes (except if configured)
|
2011-02-26 16:32:34 +01:00
|
|
|
if episode.is_new:
|
2010-03-01 21:29:46 +01:00
|
|
|
if not self.config.auto_remove_unplayed_episodes:
|
|
|
|
continue
|
|
|
|
|
|
|
|
yield episode
|
2007-12-12 20:57:50 +01:00
|
|
|
|
2010-05-01 14:03:05 +02:00
|
|
|
def delete_episode_list(self, episodes, confirm=True, skip_locked=True):
|
2009-09-02 15:57:09 +02:00
|
|
|
if not episodes:
|
2009-10-13 14:36:06 +02:00
|
|
|
return False
|
2008-09-06 22:34:35 +02:00
|
|
|
|
2010-05-01 14:03:05 +02:00
|
|
|
if skip_locked:
|
2011-02-26 16:48:48 +01:00
|
|
|
episodes = [e for e in episodes if not e.archive]
|
2009-09-02 15:57:09 +02:00
|
|
|
|
2010-05-01 14:03:05 +02:00
|
|
|
if not episodes:
|
|
|
|
title = _('Episodes are locked')
|
|
|
|
message = _('The selected episodes are locked. Please unlock the episodes that you want to delete before trying to delete them.')
|
|
|
|
self.notification(message, title, widget=self.treeAvailable)
|
|
|
|
return False
|
2010-04-07 21:01:53 +02:00
|
|
|
|
|
|
|
count = len(episodes)
|
2010-11-22 21:52:58 +01:00
|
|
|
title = N_('Delete %(count)d episode?', 'Delete %(count)d episodes?', count) % {'count':count}
|
2010-04-07 21:01:53 +02:00
|
|
|
message = _('Deleting episodes removes downloaded files.')
|
|
|
|
|
2009-09-02 15:57:09 +02:00
|
|
|
if confirm and not self.show_confirmation(message, title):
|
2009-10-13 14:36:06 +02:00
|
|
|
return False
|
2007-11-08 20:11:57 +01:00
|
|
|
|
2010-04-07 21:01:53 +02:00
|
|
|
progress = ProgressIndicator(_('Deleting episodes'), \
|
2009-12-16 21:57:35 +01:00
|
|
|
_('Please wait while episodes are deleted'), \
|
2010-05-10 13:33:50 +02:00
|
|
|
parent=self.get_dialog_parent())
|
2009-12-16 21:57:35 +01:00
|
|
|
|
|
|
|
def finish_deletion(episode_urls, channel_urls):
|
|
|
|
progress.on_finished()
|
|
|
|
|
|
|
|
# Episodes have been deleted - persist the database
|
|
|
|
self.db.commit()
|
|
|
|
|
|
|
|
self.update_episode_list_icons(episode_urls)
|
|
|
|
self.update_podcast_list_model(channel_urls)
|
|
|
|
self.play_or_download()
|
|
|
|
|
|
|
|
def thread_proc():
|
|
|
|
episode_urls = set()
|
|
|
|
channel_urls = set()
|
|
|
|
|
2010-01-28 23:58:28 +01:00
|
|
|
episodes_status_update = []
|
2009-12-16 21:57:35 +01:00
|
|
|
for idx, episode in enumerate(episodes):
|
|
|
|
progress.on_progress(float(idx)/float(len(episodes)))
|
2011-07-15 16:32:06 +02:00
|
|
|
if not episode.archive or not skip_locked:
|
2010-03-11 19:54:12 +01:00
|
|
|
progress.on_message(episode.title)
|
2009-12-16 21:57:35 +01:00
|
|
|
episode.delete_from_disk()
|
|
|
|
episode_urls.add(episode.url)
|
|
|
|
channel_urls.add(episode.channel.url)
|
2010-01-28 23:58:28 +01:00
|
|
|
episodes_status_update.append(episode)
|
2009-12-16 21:57:35 +01:00
|
|
|
|
|
|
|
# Tell the shownotes window that we have removed the episode
|
|
|
|
if self.episode_shownotes_window is not None and \
|
|
|
|
self.episode_shownotes_window.episode is not None and \
|
|
|
|
self.episode_shownotes_window.episode.url == episode.url:
|
|
|
|
util.idle_add(self.episode_shownotes_window._download_status_changed, None)
|
|
|
|
|
2010-01-28 23:58:28 +01:00
|
|
|
# Notify the web service about the status update + upload
|
|
|
|
self.mygpo_client.on_delete(episodes_status_update)
|
|
|
|
self.mygpo_client.flush()
|
|
|
|
|
2009-12-16 21:57:35 +01:00
|
|
|
util.idle_add(finish_deletion, episode_urls, channel_urls)
|
|
|
|
|
|
|
|
threading.Thread(target=thread_proc).start()
|
2009-04-27 14:54:02 +02:00
|
|
|
|
2009-10-13 14:36:06 +02:00
|
|
|
return True
|
2007-11-08 20:11:57 +01:00
|
|
|
|
2010-11-22 14:58:07 +01:00
|
|
|
def on_itemRemoveOldEpisodes_activate(self, widget):
|
|
|
|
self.show_delete_episodes_window()
|
|
|
|
|
|
|
|
def show_delete_episodes_window(self, channel=None):
|
|
|
|
"""Offer deletion of episodes
|
|
|
|
|
|
|
|
If channel is None, offer deletion of all episodes.
|
|
|
|
Otherwise only offer deletion of episodes in the channel.
|
|
|
|
"""
|
2010-12-20 14:51:20 +01:00
|
|
|
columns = (
|
|
|
|
('markup_delete_episodes', None, None, _('Episode')),
|
|
|
|
)
|
2007-11-08 20:11:57 +01:00
|
|
|
|
2010-11-22 21:52:58 +01:00
|
|
|
msg_older_than = N_('Select older than %(count)d day', 'Select older than %(count)d days', self.config.episode_old_age)
|
2007-11-08 20:11:57 +01:00
|
|
|
selection_buttons = {
|
2011-02-26 16:32:34 +01:00
|
|
|
_('Select played'): lambda episode: not episode.is_new,
|
2010-10-05 11:22:59 +02:00
|
|
|
_('Select finished'): lambda episode: episode.is_finished(),
|
2010-11-22 21:52:58 +01:00
|
|
|
msg_older_than % {'count':self.config.episode_old_age}: lambda episode: episode.age_in_days() > self.config.episode_old_age,
|
2007-11-08 20:11:57 +01:00
|
|
|
}
|
|
|
|
|
2009-09-08 21:35:36 +02:00
|
|
|
instructions = _('Select the episodes you want to delete:')
|
2007-11-08 20:11:57 +01:00
|
|
|
|
2010-11-22 14:58:07 +01:00
|
|
|
if channel is None:
|
|
|
|
channels = self.channels
|
|
|
|
else:
|
|
|
|
channels = [channel]
|
|
|
|
|
2007-11-08 20:11:57 +01:00
|
|
|
episodes = []
|
2010-11-22 14:58:07 +01:00
|
|
|
for channel in channels:
|
2008-06-30 03:10:18 +02:00
|
|
|
for episode in channel.get_downloaded_episodes():
|
2009-12-12 14:11:52 +01:00
|
|
|
# Disallow deletion of locked episodes that still exist
|
2011-02-26 16:48:48 +01:00
|
|
|
if not episode.archive or not episode.file_exists():
|
2008-06-30 03:10:18 +02:00
|
|
|
episodes.append(episode)
|
2010-11-22 14:58:07 +01:00
|
|
|
|
2011-02-26 16:32:34 +01:00
|
|
|
selected = [not e.is_new or not e.file_exists() for e in episodes]
|
2007-11-08 20:11:57 +01:00
|
|
|
|
2010-04-25 21:19:24 +02:00
|
|
|
gPodderEpisodeSelector(self.gPodder, title = _('Delete episodes'), instructions = instructions, \
|
2007-11-08 20:11:57 +01:00
|
|
|
episodes = episodes, selected = selected, columns = columns, \
|
|
|
|
stock_ok_button = gtk.STOCK_DELETE, callback = self.delete_episode_list, \
|
2010-05-03 19:30:28 +02:00
|
|
|
selection_buttons = selection_buttons, _config=self.config, \
|
|
|
|
show_episode_shownotes=self.show_episode_shownotes)
|
2007-11-08 20:11:57 +01:00
|
|
|
|
2009-08-10 23:14:35 +02:00
|
|
|
def on_selected_episodes_status_changed(self):
|
2010-08-31 22:51:07 +02:00
|
|
|
# The order of the updates here is important! When "All episodes" is
|
|
|
|
# selected, the update of the podcast list model depends on the episode
|
|
|
|
# list selection to determine which podcasts are affected. Updating
|
|
|
|
# the episode list could remove the selection if a filter is active.
|
2009-09-04 03:11:01 +02:00
|
|
|
self.update_podcast_list_model(selected=True)
|
2010-08-31 22:51:07 +02:00
|
|
|
self.update_episode_list_icons(selected=True)
|
2009-08-24 18:11:58 +02:00
|
|
|
self.db.commit()
|
2009-08-10 23:14:35 +02:00
|
|
|
|
2008-06-30 03:10:18 +02:00
|
|
|
def mark_selected_episodes_new(self):
|
2009-08-10 23:14:35 +02:00
|
|
|
for episode in self.get_selected_episodes():
|
|
|
|
episode.mark_new()
|
|
|
|
self.on_selected_episodes_status_changed()
|
2007-08-30 20:54:18 +02:00
|
|
|
|
2008-06-30 03:10:18 +02:00
|
|
|
def mark_selected_episodes_old(self):
|
2009-08-10 23:14:35 +02:00
|
|
|
for episode in self.get_selected_episodes():
|
|
|
|
episode.mark_old()
|
|
|
|
self.on_selected_episodes_status_changed()
|
2008-06-13 14:30:42 +02:00
|
|
|
|
2007-08-30 20:54:18 +02:00
|
|
|
def on_item_toggle_played_activate( self, widget, toggle = True, new_value = False):
|
2009-08-10 23:14:35 +02:00
|
|
|
for episode in self.get_selected_episodes():
|
|
|
|
if toggle:
|
2011-02-26 16:32:34 +01:00
|
|
|
episode.mark(is_played=episode.is_new)
|
2009-08-10 23:14:35 +02:00
|
|
|
else:
|
|
|
|
episode.mark(is_played=new_value)
|
|
|
|
self.on_selected_episodes_status_changed()
|
2007-08-30 20:54:18 +02:00
|
|
|
|
2007-12-12 19:50:52 +01:00
|
|
|
def on_item_toggle_lock_activate(self, widget, toggle=True, new_value=False):
|
2009-08-10 23:14:35 +02:00
|
|
|
for episode in self.get_selected_episodes():
|
|
|
|
if toggle:
|
2011-02-26 16:48:48 +01:00
|
|
|
episode.mark(is_locked=not episode.archive)
|
2009-08-10 23:14:35 +02:00
|
|
|
else:
|
|
|
|
episode.mark(is_locked=new_value)
|
|
|
|
self.on_selected_episodes_status_changed()
|
2007-12-12 19:50:52 +01:00
|
|
|
|
2008-11-19 17:05:19 +01:00
|
|
|
def on_channel_toggle_lock_activate(self, widget, toggle=True, new_value=False):
|
2009-09-01 18:56:30 +02:00
|
|
|
if self.active_channel is None:
|
|
|
|
return
|
|
|
|
|
2010-12-20 14:35:46 +01:00
|
|
|
self.active_channel.auto_archive_episodes = not self.active_channel.auto_archive_episodes
|
|
|
|
self.active_channel.save()
|
2008-11-19 17:05:19 +01:00
|
|
|
|
|
|
|
for episode in self.active_channel.get_all_episodes():
|
2010-12-20 14:35:46 +01:00
|
|
|
episode.mark(is_locked=self.active_channel.auto_archive_episodes)
|
2008-11-19 17:05:19 +01:00
|
|
|
|
2009-09-04 03:11:01 +02:00
|
|
|
self.update_podcast_list_model(selected=True)
|
|
|
|
self.update_episode_list_icons(all=True)
|
2008-11-19 17:05:19 +01:00
|
|
|
|
2008-08-30 19:23:04 +02:00
|
|
|
def on_itemUpdateChannel_activate(self, widget=None):
|
2009-09-01 18:56:30 +02:00
|
|
|
if self.active_channel is None:
|
|
|
|
title = _('No podcast selected')
|
|
|
|
message = _('Please select a podcast in the podcasts list to update.')
|
|
|
|
self.show_message( message, title, widget=self.treeChannels)
|
|
|
|
return
|
|
|
|
|
2010-05-28 19:57:21 +02:00
|
|
|
# Dirty hack to check for "All episodes" (see gpodder.gtkui.model)
|
|
|
|
if getattr(self.active_channel, 'ALL_EPISODES_PROXY', False):
|
|
|
|
self.update_feed_cache()
|
|
|
|
else:
|
|
|
|
self.update_feed_cache(channels=[self.active_channel])
|
2008-08-30 19:23:04 +02:00
|
|
|
|
2009-06-11 18:01:01 +02:00
|
|
|
def on_itemUpdate_activate(self, widget=None):
|
2010-01-28 17:39:10 +01:00
|
|
|
# Check if we have outstanding subscribe/unsubscribe actions
|
2011-07-16 20:51:26 +02:00
|
|
|
# FIXME: Implement this somewhere else: self.on_add_remove_podcasts_mygpo()
|
2010-01-28 17:39:10 +01:00
|
|
|
|
2006-12-06 21:25:26 +01:00
|
|
|
if self.channels:
|
2009-06-11 18:01:01 +02:00
|
|
|
self.update_feed_cache()
|
2006-12-06 21:25:26 +01:00
|
|
|
else:
|
2011-10-19 14:47:16 +02:00
|
|
|
welcome_window = gPodderWelcome(self.main_window,
|
|
|
|
center_on_widget=self.main_window,
|
2010-01-19 23:43:59 +01:00
|
|
|
show_example_podcasts_callback=self.on_itemImportChannels_activate,
|
2010-05-12 13:38:31 +02:00
|
|
|
setup_my_gpodder_callback=self.on_download_subscriptions_from_mygpo)
|
2005-11-21 19:21:25 +01:00
|
|
|
|
2011-10-19 14:47:16 +02:00
|
|
|
result = welcome_window.main_window.run()
|
|
|
|
|
|
|
|
welcome_window.main_window.destroy()
|
|
|
|
if result == gPodderWelcome.RESPONSE_OPML:
|
|
|
|
self.on_itemImportChannels_activate(None)
|
|
|
|
elif result == gPodderWelcome.RESPONSE_MYGPO:
|
|
|
|
self.on_download_subscriptions_from_mygpo(None)
|
|
|
|
|
2009-04-01 01:12:17 +02:00
|
|
|
def download_episode_list_paused(self, episodes):
|
|
|
|
self.download_episode_list(episodes, True)
|
|
|
|
|
2010-03-07 20:15:36 +01:00
|
|
|
def download_episode_list(self, episodes, add_paused=False, force_start=False):
|
2010-06-05 01:42:32 +02:00
|
|
|
enable_update = False
|
|
|
|
|
2007-11-08 20:11:57 +01:00
|
|
|
for episode in episodes:
|
2011-07-15 16:32:06 +02:00
|
|
|
logger.debug('Downloading episode: %s', episode.title)
|
2009-04-01 01:12:17 +02:00
|
|
|
if not episode.was_downloaded(and_exists=True):
|
|
|
|
task_exists = False
|
|
|
|
for task in self.download_tasks_seen:
|
|
|
|
if episode.url == task.url and task.status not in (task.DOWNLOADING, task.QUEUED):
|
2010-03-07 20:15:36 +01:00
|
|
|
self.download_queue_manager.add_task(task, force_start)
|
2010-06-05 01:42:32 +02:00
|
|
|
enable_update = True
|
2009-04-01 01:12:17 +02:00
|
|
|
task_exists = True
|
|
|
|
continue
|
|
|
|
|
|
|
|
if task_exists:
|
|
|
|
continue
|
|
|
|
|
2009-06-04 12:35:18 +02:00
|
|
|
try:
|
2009-08-24 18:11:58 +02:00
|
|
|
task = download.DownloadTask(episode, self.config)
|
2009-06-04 12:35:18 +02:00
|
|
|
except Exception, e:
|
2010-01-18 21:20:22 +01:00
|
|
|
d = {'episode': episode.title, 'message': str(e)}
|
|
|
|
message = _('Download error while downloading %(episode)s: %(message)s')
|
|
|
|
self.show_message(message % d, _('Download error'), important=True)
|
2011-07-15 16:32:06 +02:00
|
|
|
logger.error('While downloading %s', episode.title, exc_info=True)
|
2009-06-04 12:35:18 +02:00
|
|
|
continue
|
|
|
|
|
2009-04-01 01:12:17 +02:00
|
|
|
if add_paused:
|
|
|
|
task.status = task.PAUSED
|
|
|
|
else:
|
2010-01-28 23:58:28 +01:00
|
|
|
self.mygpo_client.on_download([task.episode])
|
2010-03-07 20:15:36 +01:00
|
|
|
self.download_queue_manager.add_task(task, force_start)
|
2009-08-24 16:17:32 +02:00
|
|
|
|
|
|
|
self.download_status_model.register_task(task)
|
2010-06-05 01:42:32 +02:00
|
|
|
enable_update = True
|
|
|
|
|
|
|
|
if enable_update:
|
|
|
|
self.enable_download_list_update()
|
2006-12-20 20:44:29 +01:00
|
|
|
|
2010-01-28 23:58:28 +01:00
|
|
|
# Flush updated episode status
|
|
|
|
self.mygpo_client.flush()
|
|
|
|
|
2009-09-02 15:57:09 +02:00
|
|
|
def cancel_task_list(self, tasks):
|
|
|
|
if not tasks:
|
|
|
|
return
|
|
|
|
|
|
|
|
for task in tasks:
|
|
|
|
if task.status in (task.QUEUED, task.DOWNLOADING):
|
|
|
|
task.status = task.CANCELLED
|
|
|
|
elif task.status == task.PAUSED:
|
|
|
|
task.status = task.CANCELLED
|
|
|
|
# Call run, so the partial file gets deleted
|
|
|
|
task.run()
|
|
|
|
|
|
|
|
self.update_episode_list_icons([task.url for task in tasks])
|
|
|
|
self.play_or_download()
|
|
|
|
|
|
|
|
# Update the tab title and downloads list
|
|
|
|
self.update_downloads_list()
|
|
|
|
|
2010-08-24 00:24:15 +02:00
|
|
|
def new_episodes_show(self, episodes, notification=False, selected=None):
|
2010-12-20 14:51:20 +01:00
|
|
|
columns = (
|
|
|
|
('markup_new_episodes', None, None, _('Episode')),
|
|
|
|
)
|
2007-11-08 20:11:57 +01:00
|
|
|
|
2009-09-08 21:35:36 +02:00
|
|
|
instructions = _('Select the episodes you want to download:')
|
2007-03-10 18:42:32 +01:00
|
|
|
|
2009-09-28 14:23:21 +02:00
|
|
|
if self.new_episodes_window is not None:
|
|
|
|
self.new_episodes_window.main_window.destroy()
|
|
|
|
self.new_episodes_window = None
|
|
|
|
|
|
|
|
def download_episodes_callback(episodes):
|
|
|
|
self.new_episodes_window = None
|
|
|
|
self.download_episode_list(episodes)
|
|
|
|
|
2010-08-24 00:24:15 +02:00
|
|
|
if selected is None:
|
|
|
|
# Select all by default
|
|
|
|
selected = [True]*len(episodes)
|
|
|
|
|
2009-09-28 14:23:21 +02:00
|
|
|
self.new_episodes_window = gPodderEpisodeSelector(self.gPodder, \
|
|
|
|
title=_('New episodes available'), \
|
|
|
|
instructions=instructions, \
|
|
|
|
episodes=episodes, \
|
|
|
|
columns=columns, \
|
2010-08-24 00:24:15 +02:00
|
|
|
selected=selected, \
|
2009-09-28 14:23:21 +02:00
|
|
|
stock_ok_button = 'gpodder-download', \
|
|
|
|
callback=download_episodes_callback, \
|
|
|
|
remove_callback=lambda e: e.mark_old(), \
|
|
|
|
remove_action=_('Mark as old'), \
|
|
|
|
remove_finished=self.episode_new_status_changed, \
|
|
|
|
_config=self.config, \
|
2011-07-16 18:35:14 +02:00
|
|
|
show_notification=False, \
|
2010-05-03 19:30:28 +02:00
|
|
|
show_episode_shownotes=self.show_episode_shownotes)
|
2008-01-22 10:10:08 +01:00
|
|
|
|
|
|
|
def on_itemDownloadAllNew_activate(self, widget, *args):
|
2009-08-25 16:19:14 +02:00
|
|
|
if not self.offer_new_episodes():
|
|
|
|
self.show_message(_('Please check for new episodes later.'), \
|
|
|
|
_('No new episodes available'), widget=self.btnUpdateFeeds)
|
2008-08-30 19:23:04 +02:00
|
|
|
|
2009-04-02 15:24:54 +02:00
|
|
|
def get_new_episodes(self, channels=None):
|
2011-07-16 18:04:07 +02:00
|
|
|
return [e for c in channels or self.channels for e in
|
2011-07-16 20:51:26 +02:00
|
|
|
filter(lambda e: e.check_is_new(), c.get_all_episodes())]
|
2006-12-20 20:44:29 +01:00
|
|
|
|
2010-02-25 01:54:44 +01:00
|
|
|
def commit_changes_to_database(self):
|
|
|
|
"""This will be called after the sync process is finished"""
|
2009-08-24 18:11:58 +02:00
|
|
|
self.db.commit()
|
2009-04-27 14:54:02 +02:00
|
|
|
|
2010-02-24 02:29:08 +01:00
|
|
|
def on_itemShowAllEpisodes_activate(self, widget):
|
|
|
|
self.config.podcast_list_view_all = widget.get_active()
|
|
|
|
|
2008-02-06 10:29:56 +01:00
|
|
|
def on_itemShowToolbar_activate(self, widget):
|
2009-08-24 18:11:58 +02:00
|
|
|
self.config.show_toolbar = self.itemShowToolbar.get_active()
|
2008-02-06 10:29:56 +01:00
|
|
|
|
|
|
|
def on_itemShowDescription_activate(self, widget):
|
2009-08-24 18:11:58 +02:00
|
|
|
self.config.episode_list_descriptions = self.itemShowDescription.get_active()
|
2008-02-06 10:29:56 +01:00
|
|
|
|
2009-09-01 18:56:30 +02:00
|
|
|
def on_item_view_hide_boring_podcasts_toggled(self, toggleaction):
|
|
|
|
self.config.podcast_list_hide_boring = toggleaction.get_active()
|
|
|
|
if self.config.podcast_list_hide_boring:
|
|
|
|
self.podcast_list_model.set_view_mode(self.config.episode_list_view_mode)
|
|
|
|
else:
|
2009-09-09 16:44:48 +02:00
|
|
|
self.podcast_list_model.set_view_mode(-1)
|
2009-09-01 18:56:30 +02:00
|
|
|
|
2009-09-01 17:22:51 +02:00
|
|
|
def on_item_view_episodes_changed(self, radioaction, current):
|
|
|
|
if current == self.item_view_episodes_all:
|
2010-11-22 14:28:27 +01:00
|
|
|
self.config.episode_list_view_mode = EpisodeListModel.VIEW_ALL
|
2009-09-01 17:22:51 +02:00
|
|
|
elif current == self.item_view_episodes_undeleted:
|
2010-11-22 14:28:27 +01:00
|
|
|
self.config.episode_list_view_mode = EpisodeListModel.VIEW_UNDELETED
|
2009-09-01 17:22:51 +02:00
|
|
|
elif current == self.item_view_episodes_downloaded:
|
2010-11-22 14:28:27 +01:00
|
|
|
self.config.episode_list_view_mode = EpisodeListModel.VIEW_DOWNLOADED
|
2009-09-09 16:44:48 +02:00
|
|
|
elif current == self.item_view_episodes_unplayed:
|
2010-11-22 14:28:27 +01:00
|
|
|
self.config.episode_list_view_mode = EpisodeListModel.VIEW_UNPLAYED
|
2009-09-01 17:22:51 +02:00
|
|
|
|
2010-11-22 14:28:27 +01:00
|
|
|
self.episode_list_model.set_view_mode(self.config.episode_list_view_mode)
|
2009-09-01 17:22:51 +02:00
|
|
|
|
2011-07-16 18:35:14 +02:00
|
|
|
if self.config.podcast_list_hide_boring:
|
2009-09-01 18:56:30 +02:00
|
|
|
self.podcast_list_model.set_view_mode(self.config.episode_list_view_mode)
|
|
|
|
|
2005-11-21 19:21:25 +01:00
|
|
|
def on_itemPreferences_activate(self, widget, *args):
|
2011-07-16 18:49:19 +02:00
|
|
|
gPodderPreferences(self.main_window, \
|
2010-02-24 02:29:08 +01:00
|
|
|
_config=self.config, \
|
2009-11-22 12:04:54 +01:00
|
|
|
user_apps_reader=self.user_apps_reader, \
|
2010-05-03 21:16:13 +02:00
|
|
|
parent_window=self.main_window, \
|
|
|
|
mygpo_client=self.mygpo_client, \
|
2011-02-25 16:31:46 +01:00
|
|
|
on_send_full_subscriptions=self.on_send_full_subscriptions, \
|
|
|
|
on_itemExportChannels_activate=self.on_itemExportChannels_activate)
|
2010-02-24 23:39:18 +01:00
|
|
|
|
2009-12-27 14:35:42 +01:00
|
|
|
def on_goto_mygpo(self, widget):
|
2010-01-19 23:43:59 +01:00
|
|
|
self.mygpo_client.open_website()
|
2008-12-08 17:10:53 +01:00
|
|
|
|
2010-05-12 13:38:31 +02:00
|
|
|
def on_download_subscriptions_from_mygpo(self, action=None):
|
|
|
|
title = _('Login to gpodder.net')
|
|
|
|
message = _('Please login to download your subscriptions.')
|
|
|
|
success, (username, password) = self.show_login_dialog(title, message, \
|
|
|
|
self.config.mygpo_username, self.config.mygpo_password)
|
|
|
|
if not success:
|
|
|
|
return
|
|
|
|
|
|
|
|
self.config.mygpo_username = username
|
|
|
|
self.config.mygpo_password = password
|
|
|
|
|
|
|
|
dir = gPodderPodcastDirectory(self.gPodder, _config=self.config, \
|
|
|
|
custom_title=_('Subscriptions on gpodder.net'), \
|
|
|
|
add_urls_callback=self.add_podcast_list, \
|
|
|
|
hide_url_entry=True)
|
|
|
|
|
|
|
|
# TODO: Refactor this into "gpodder.my" or mygpoclient, so that
|
|
|
|
# we do not have to hardcode the URL here
|
|
|
|
OPML_URL = 'http://gpodder.net/subscriptions/%s.opml' % self.config.mygpo_username
|
|
|
|
url = util.url_add_authentication(OPML_URL, \
|
|
|
|
self.config.mygpo_username, \
|
|
|
|
self.config.mygpo_password)
|
|
|
|
dir.download_opml_file(url)
|
|
|
|
|
2009-09-24 17:32:32 +02:00
|
|
|
def on_itemAddChannel_activate(self, widget=None):
|
2009-08-25 16:19:14 +02:00
|
|
|
gPodderAddPodcast(self.gPodder, \
|
|
|
|
add_urls_callback=self.add_podcast_list)
|
2005-11-21 19:21:25 +01:00
|
|
|
|
|
|
|
def on_itemEditChannel_activate(self, widget, *args):
|
2008-02-26 16:49:59 +01:00
|
|
|
if self.active_channel is None:
|
2008-05-02 17:36:43 +02:00
|
|
|
title = _('No podcast selected')
|
|
|
|
message = _('Please select a podcast in the podcasts list to edit.')
|
2009-08-24 23:00:25 +02:00
|
|
|
self.show_message( message, title, widget=self.treeChannels)
|
2006-04-06 16:11:03 +02:00
|
|
|
return
|
2007-07-11 20:28:09 +02:00
|
|
|
|
2009-09-07 18:18:23 +02:00
|
|
|
gPodderChannel(self.main_window, \
|
|
|
|
channel=self.active_channel, \
|
2011-07-28 13:50:13 +02:00
|
|
|
update_podcast_list_model=self.update_podcast_list_model, \
|
|
|
|
cover_downloader=self.cover_downloader, \
|
|
|
|
sections=set(c.section for c in self.channels))
|
2008-03-03 23:09:34 +01:00
|
|
|
|
2009-12-16 23:33:12 +01:00
|
|
|
def on_itemMassUnsubscribe_activate(self, item=None):
|
|
|
|
columns = (
|
|
|
|
('title', None, None, _('Podcast')),
|
|
|
|
)
|
|
|
|
|
|
|
|
# We're abusing the Episode Selector for selecting Podcasts here,
|
|
|
|
# but it works and looks good, so why not? -- thp
|
|
|
|
gPodderEpisodeSelector(self.main_window, \
|
|
|
|
title=_('Remove podcasts'), \
|
|
|
|
instructions=_('Select the podcast you want to remove.'), \
|
|
|
|
episodes=self.channels, \
|
|
|
|
columns=columns, \
|
|
|
|
size_attribute=None, \
|
2010-08-06 15:21:13 +02:00
|
|
|
stock_ok_button=_('Remove'), \
|
2009-12-16 23:33:12 +01:00
|
|
|
callback=self.remove_podcast_list, \
|
|
|
|
_config=self.config)
|
|
|
|
|
|
|
|
def remove_podcast_list(self, channels, confirm=True):
|
|
|
|
if not channels:
|
2009-09-01 18:56:30 +02:00
|
|
|
return
|
|
|
|
|
2009-12-16 23:33:12 +01:00
|
|
|
if len(channels) == 1:
|
|
|
|
title = _('Removing podcast')
|
|
|
|
info = _('Please wait while the podcast is removed')
|
|
|
|
message = _('Do you really want to remove this podcast and its episodes?')
|
|
|
|
else:
|
|
|
|
title = _('Removing podcasts')
|
|
|
|
info = _('Please wait while the podcasts are removed')
|
|
|
|
message = _('Do you really want to remove the selected podcasts and their episodes?')
|
2009-12-16 21:57:35 +01:00
|
|
|
|
2009-12-16 23:33:12 +01:00
|
|
|
if confirm and not self.show_confirmation(message, title):
|
|
|
|
return
|
|
|
|
|
2010-05-10 13:33:50 +02:00
|
|
|
progress = ProgressIndicator(title, info, parent=self.get_dialog_parent())
|
2009-12-16 21:57:35 +01:00
|
|
|
|
2009-12-16 23:33:12 +01:00
|
|
|
def finish_deletion(select_url):
|
2010-01-19 23:43:59 +01:00
|
|
|
# Upload subscription list changes to the web service
|
|
|
|
self.mygpo_client.on_unsubscribe([c.url for c in channels])
|
|
|
|
|
2009-12-16 23:33:12 +01:00
|
|
|
# Re-load the channels and select the desired new channel
|
2011-07-16 20:51:26 +02:00
|
|
|
self.update_podcast_list_model(select_url=select_url)
|
2009-12-16 23:33:12 +01:00
|
|
|
progress.on_finished()
|
2009-12-16 21:57:35 +01:00
|
|
|
|
2009-12-16 23:33:12 +01:00
|
|
|
def thread_proc():
|
|
|
|
select_url = None
|
|
|
|
|
|
|
|
for idx, channel in enumerate(channels):
|
|
|
|
# Update the UI for correct status messages
|
|
|
|
progress.on_progress(float(idx)/float(len(channels)))
|
2010-03-11 19:54:12 +01:00
|
|
|
progress.on_message(channel.title)
|
2009-12-16 23:33:12 +01:00
|
|
|
|
|
|
|
# Delete downloaded episodes
|
|
|
|
channel.remove_downloaded()
|
|
|
|
|
|
|
|
# cancel any active downloads from this channel
|
|
|
|
for episode in channel.get_all_episodes():
|
2011-07-16 18:04:07 +02:00
|
|
|
if episode.downloading:
|
|
|
|
episode.download_task.cancel()
|
2009-12-16 23:33:12 +01:00
|
|
|
|
|
|
|
if len(channels) == 1:
|
2009-12-16 21:57:35 +01:00
|
|
|
# get the URL of the podcast we want to select next
|
2010-01-19 23:43:59 +01:00
|
|
|
if channel in self.channels:
|
|
|
|
position = self.channels.index(channel)
|
|
|
|
else:
|
|
|
|
position = -1
|
|
|
|
|
2009-12-16 21:57:35 +01:00
|
|
|
if position == len(self.channels)-1:
|
|
|
|
# this is the last podcast, so select the URL
|
|
|
|
# of the item before this one (i.e. the "new last")
|
|
|
|
select_url = self.channels[position-1].url
|
|
|
|
else:
|
|
|
|
# there is a podcast after the deleted one, so
|
|
|
|
# we simply select the one that comes after it
|
|
|
|
select_url = self.channels[position+1].url
|
2008-05-14 15:38:06 +02:00
|
|
|
|
2009-12-16 23:33:12 +01:00
|
|
|
# Remove the channel and clean the database entries
|
2010-04-24 18:51:19 +02:00
|
|
|
channel.delete()
|
2008-05-14 15:38:06 +02:00
|
|
|
|
2009-12-16 23:33:12 +01:00
|
|
|
# Clean up downloads and download directories
|
|
|
|
self.clean_up_downloads()
|
2009-09-16 23:51:18 +02:00
|
|
|
|
2009-12-16 23:33:12 +01:00
|
|
|
# The remaining stuff is to be done in the GTK main thread
|
|
|
|
util.idle_add(finish_deletion, select_url)
|
|
|
|
|
|
|
|
threading.Thread(target=thread_proc).start()
|
|
|
|
|
|
|
|
def on_itemRemoveChannel_activate(self, widget, *args):
|
|
|
|
if self.active_channel is None:
|
|
|
|
title = _('No podcast selected')
|
|
|
|
message = _('Please select a podcast in the podcasts list to remove.')
|
|
|
|
self.show_message( message, title, widget=self.treeChannels)
|
|
|
|
return
|
|
|
|
|
|
|
|
self.remove_podcast_list([self.active_channel])
|
2005-11-21 19:21:25 +01:00
|
|
|
|
2008-05-02 17:36:43 +02:00
|
|
|
def get_opml_filter(self):
|
|
|
|
filter = gtk.FileFilter()
|
|
|
|
filter.add_pattern('*.opml')
|
|
|
|
filter.add_pattern('*.xml')
|
|
|
|
filter.set_name(_('OPML files')+' (*.opml, *.xml)')
|
|
|
|
return filter
|
|
|
|
|
2008-06-14 15:17:55 +02:00
|
|
|
def on_item_import_from_file_activate(self, widget, filename=None):
|
|
|
|
if filename is None:
|
2011-07-16 18:35:14 +02:00
|
|
|
dlg = gtk.FileChooserDialog(title=_('Import from OPML'), \
|
|
|
|
parent=None, action=gtk.FILE_CHOOSER_ACTION_OPEN)
|
|
|
|
dlg.add_button(gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL)
|
|
|
|
dlg.add_button(gtk.STOCK_OPEN, gtk.RESPONSE_OK)
|
2008-06-14 15:17:55 +02:00
|
|
|
dlg.set_filter(self.get_opml_filter())
|
|
|
|
response = dlg.run()
|
|
|
|
filename = None
|
|
|
|
if response == gtk.RESPONSE_OK:
|
|
|
|
filename = dlg.get_filename()
|
|
|
|
dlg.destroy()
|
|
|
|
|
2008-05-02 17:36:43 +02:00
|
|
|
if filename is not None:
|
2009-08-25 16:19:14 +02:00
|
|
|
dir = gPodderPodcastDirectory(self.gPodder, _config=self.config, \
|
|
|
|
custom_title=_('Import podcasts from OPML file'), \
|
|
|
|
add_urls_callback=self.add_podcast_list, \
|
|
|
|
hide_url_entry=True)
|
|
|
|
dir.download_opml_file(filename)
|
2008-05-02 17:36:43 +02:00
|
|
|
|
2005-11-21 19:21:25 +01:00
|
|
|
def on_itemExportChannels_activate(self, widget, *args):
|
2006-12-09 02:59:53 +01:00
|
|
|
if not self.channels:
|
2007-03-10 18:42:32 +01:00
|
|
|
title = _('Nothing to export')
|
2008-05-02 17:36:43 +02:00
|
|
|
message = _('Your list of podcast subscriptions is empty. Please subscribe to some podcasts first before trying to export your subscription list.')
|
2009-08-24 23:00:25 +02:00
|
|
|
self.show_message(message, title, widget=self.treeChannels)
|
2006-12-09 02:59:53 +01:00
|
|
|
return
|
|
|
|
|
2011-07-16 18:35:14 +02:00
|
|
|
dlg = gtk.FileChooserDialog(title=_('Export to OPML'), parent=self.gPodder, action=gtk.FILE_CHOOSER_ACTION_SAVE)
|
|
|
|
dlg.add_button(gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL)
|
|
|
|
dlg.add_button(gtk.STOCK_SAVE, gtk.RESPONSE_OK)
|
2008-05-02 17:36:43 +02:00
|
|
|
dlg.set_filter(self.get_opml_filter())
|
2005-12-08 20:47:35 +01:00
|
|
|
response = dlg.run()
|
|
|
|
if response == gtk.RESPONSE_OK:
|
2007-08-19 16:28:24 +02:00
|
|
|
filename = dlg.get_filename()
|
2008-09-10 16:02:06 +02:00
|
|
|
dlg.destroy()
|
2007-08-19 16:28:24 +02:00
|
|
|
exporter = opml.Exporter( filename)
|
2011-02-25 16:31:46 +01:00
|
|
|
if filename is not None and exporter.write(self.channels):
|
2009-12-21 23:18:00 +01:00
|
|
|
count = len(self.channels)
|
2010-11-22 21:52:58 +01:00
|
|
|
title = N_('%(count)d subscription exported', '%(count)d subscriptions exported', count) % {'count':count}
|
2009-08-24 23:00:25 +02:00
|
|
|
self.show_message(_('Your podcast list has been successfully exported.'), title, widget=self.treeChannels)
|
2008-09-10 16:02:06 +02:00
|
|
|
else:
|
2009-08-24 23:00:25 +02:00
|
|
|
self.show_message( _('Could not export OPML to file. Please check your permissions.'), _('OPML export failed'), important=True)
|
2008-09-10 16:02:06 +02:00
|
|
|
else:
|
|
|
|
dlg.destroy()
|
2005-11-21 19:21:25 +01:00
|
|
|
|
2006-06-13 23:00:31 +02:00
|
|
|
def on_itemImportChannels_activate(self, widget, *args):
|
2011-07-16 18:35:14 +02:00
|
|
|
dir = gPodderPodcastDirectory(self.main_window, _config=self.config, \
|
|
|
|
add_urls_callback=self.add_podcast_list)
|
|
|
|
util.idle_add(dir.download_opml_file, self.config.example_opml)
|
2006-06-13 23:00:31 +02:00
|
|
|
|
2006-12-04 14:06:42 +01:00
|
|
|
def on_homepage_activate(self, widget, *args):
|
2009-05-07 16:26:07 +02:00
|
|
|
util.open_website(gpodder.__url__)
|
2006-12-04 14:06:42 +01:00
|
|
|
|
2007-12-13 08:05:25 +01:00
|
|
|
def on_wiki_activate(self, widget, *args):
|
2010-02-05 14:38:16 +01:00
|
|
|
util.open_website('http://gpodder.org/wiki/User_Manual')
|
2006-12-04 14:06:42 +01:00
|
|
|
|
2008-01-14 20:23:49 +01:00
|
|
|
def on_bug_tracker_activate(self, widget, *args):
|
2011-07-16 18:35:14 +02:00
|
|
|
util.open_website('https://bugs.gpodder.org/enter_bug.cgi?product=gPodder&component=Application&version=%s' % gpodder.__version__)
|
2009-05-31 11:45:54 +02:00
|
|
|
|
2009-12-12 15:58:01 +01:00
|
|
|
def on_item_support_activate(self, widget):
|
|
|
|
util.open_website('http://gpodder.org/donate')
|
|
|
|
|
2005-11-21 19:21:25 +01:00
|
|
|
def on_itemAbout_activate(self, widget, *args):
|
2011-04-12 01:28:21 +02:00
|
|
|
dlg = gtk.Dialog(_('About gPodder'), self.main_window, \
|
|
|
|
gtk.DIALOG_MODAL)
|
2011-10-13 16:35:43 +02:00
|
|
|
dlg.add_button(gtk.STOCK_CLOSE, gtk.RESPONSE_OK).show()
|
2011-04-12 01:28:21 +02:00
|
|
|
dlg.set_resizable(False)
|
|
|
|
|
|
|
|
bg = gtk.HBox(spacing=10)
|
|
|
|
bg.pack_start(gtk.image_new_from_file(gpodder.icon_file), expand=False)
|
|
|
|
vb = gtk.VBox()
|
|
|
|
label = gtk.Label()
|
|
|
|
label.set_alignment(0, 1)
|
|
|
|
label.set_markup('<b><big>gPodder</big> %s</b>' % gpodder.__version__)
|
|
|
|
vb.pack_start(label)
|
|
|
|
label = gtk.Label()
|
|
|
|
label.set_alignment(0, 0)
|
|
|
|
label.set_markup('<small>%s</small>' % \
|
|
|
|
cgi.escape(_('A podcast client with focus on usability')))
|
|
|
|
vb.pack_start(label)
|
|
|
|
label = gtk.Label()
|
|
|
|
label.set_alignment(0, 0)
|
|
|
|
label.set_markup('<small><a href="%s">%s</a></small>' % \
|
|
|
|
((cgi.escape(gpodder.__url__),)*2))
|
|
|
|
vb.pack_start(label)
|
|
|
|
bg.pack_start(vb)
|
|
|
|
|
|
|
|
out = gtk.VBox(spacing=10)
|
|
|
|
out.set_border_width(12)
|
|
|
|
out.pack_start(bg, expand=False)
|
|
|
|
out.pack_start(gtk.HSeparator())
|
|
|
|
out.pack_start(gtk.Label(gpodder.__copyright__))
|
|
|
|
|
|
|
|
button_box = gtk.HButtonBox()
|
|
|
|
button = gtk.Button(_('Donate / Wishlist'))
|
|
|
|
button.connect('clicked', self.on_item_support_activate)
|
|
|
|
button_box.pack_start(button)
|
|
|
|
button = gtk.Button(_('Report a problem'))
|
|
|
|
button.connect('clicked', self.on_bug_tracker_activate)
|
|
|
|
button_box.pack_start(button)
|
|
|
|
out.pack_start(button_box, expand=False)
|
|
|
|
|
|
|
|
credits = gtk.TextView()
|
|
|
|
credits.set_left_margin(5)
|
|
|
|
credits.set_right_margin(5)
|
|
|
|
credits.set_pixels_above_lines(5)
|
|
|
|
credits.set_pixels_below_lines(5)
|
|
|
|
credits.set_editable(False)
|
|
|
|
credits.set_cursor_visible(False)
|
|
|
|
sw = gtk.ScrolledWindow()
|
|
|
|
sw.set_shadow_type(gtk.SHADOW_IN)
|
|
|
|
sw.set_policy(gtk.POLICY_NEVER, gtk.POLICY_AUTOMATIC)
|
|
|
|
sw.add(credits)
|
|
|
|
credits.set_size_request(-1, 160)
|
|
|
|
out.pack_start(sw, expand=True, fill=True)
|
|
|
|
|
|
|
|
dlg.vbox.pack_start(out, expand=False)
|
|
|
|
dlg.connect('response', lambda dlg, response: dlg.destroy())
|
|
|
|
|
|
|
|
dlg.vbox.show_all()
|
|
|
|
|
|
|
|
if os.path.exists(gpodder.credits_file):
|
|
|
|
credits_txt = open(gpodder.credits_file).read().strip().split('\n')
|
|
|
|
translator_credits = _('translator-credits')
|
|
|
|
if translator_credits != 'translator-credits':
|
|
|
|
app_authors = [_('Translation by:'), translator_credits, '']
|
|
|
|
else:
|
|
|
|
app_authors = []
|
2006-12-04 14:06:42 +01:00
|
|
|
|
2011-04-12 01:28:21 +02:00
|
|
|
app_authors += [_('Thanks to:')]
|
|
|
|
app_authors += credits_txt
|
|
|
|
|
|
|
|
buffer = gtk.TextBuffer()
|
|
|
|
buffer.set_text('\n'.join(app_authors))
|
|
|
|
credits.set_buffer(buffer)
|
|
|
|
else:
|
|
|
|
sw.hide()
|
2010-04-25 20:42:01 +02:00
|
|
|
|
2011-04-12 01:28:21 +02:00
|
|
|
credits.grab_focus()
|
2005-11-21 19:21:25 +01:00
|
|
|
dlg.run()
|
2006-11-29 00:23:16 +01:00
|
|
|
|
2011-04-05 21:33:06 +02:00
|
|
|
def on_wNotebook_switch_page(self, notebook, page, page_num):
|
2007-03-31 04:00:30 +02:00
|
|
|
if page_num == 0:
|
|
|
|
self.play_or_download()
|
2009-04-01 01:12:17 +02:00
|
|
|
# The message area in the downloads tab should be hidden
|
|
|
|
# when the user switches away from the downloads tab
|
|
|
|
if self.message_area is not None:
|
|
|
|
self.message_area.hide()
|
|
|
|
self.message_area = None
|
2011-07-16 18:38:19 +02:00
|
|
|
else:
|
2011-04-05 21:33:06 +02:00
|
|
|
self.toolDownload.set_sensitive(False)
|
|
|
|
self.toolPlay.set_sensitive(False)
|
|
|
|
self.toolCancel.set_sensitive(False)
|
2006-02-04 18:29:17 +01:00
|
|
|
|
2009-03-13 20:08:57 +01:00
|
|
|
def on_treeChannels_row_activated(self, widget, path, *args):
|
|
|
|
# double-click action of the podcast list or enter
|
|
|
|
self.treeChannels.set_cursor(path)
|
2007-07-05 23:07:16 +02:00
|
|
|
|
|
|
|
def on_treeChannels_cursor_changed(self, widget, *args):
|
2007-09-19 17:04:42 +02:00
|
|
|
( model, iter ) = self.treeChannels.get_selection().get_selected()
|
2007-07-11 13:05:02 +02:00
|
|
|
|
2008-12-13 13:29:45 +01:00
|
|
|
if model is not None and iter is not None:
|
|
|
|
old_active_channel = self.active_channel
|
2009-08-13 23:19:12 +02:00
|
|
|
self.active_channel = model.get_value(iter, PodcastListModel.C_CHANNEL)
|
2006-12-09 02:59:53 +01:00
|
|
|
|
2008-12-13 13:29:45 +01:00
|
|
|
if self.active_channel == old_active_channel:
|
|
|
|
return
|
|
|
|
|
2010-05-28 19:57:21 +02:00
|
|
|
# Dirty hack to check for "All episodes" (see gpodder.gtkui.model)
|
|
|
|
if getattr(self.active_channel, 'ALL_EPISODES_PROXY', False):
|
|
|
|
self.itemEditChannel.set_visible(False)
|
|
|
|
self.itemRemoveChannel.set_visible(False)
|
|
|
|
else:
|
|
|
|
self.itemEditChannel.set_visible(True)
|
|
|
|
self.itemRemoveChannel.set_visible(True)
|
2006-12-16 18:44:13 +01:00
|
|
|
else:
|
2007-09-19 17:04:42 +02:00
|
|
|
self.active_channel = None
|
2009-05-08 14:28:53 +02:00
|
|
|
self.itemEditChannel.set_visible(False)
|
|
|
|
self.itemRemoveChannel.set_visible(False)
|
2006-12-16 18:44:13 +01:00
|
|
|
|
2009-09-04 03:11:01 +02:00
|
|
|
self.update_episode_list_model()
|
2005-11-21 19:21:25 +01:00
|
|
|
|
2006-03-04 21:45:01 +01:00
|
|
|
def on_btnEditChannel_clicked(self, widget, *args):
|
|
|
|
self.on_itemEditChannel_activate( widget, args)
|
|
|
|
|
2010-02-22 23:22:58 +01:00
|
|
|
def get_podcast_urls_from_selected_episodes(self):
|
|
|
|
"""Get a set of podcast URLs based on the selected episodes"""
|
|
|
|
return set(episode.channel.url for episode in \
|
|
|
|
self.get_selected_episodes())
|
|
|
|
|
2009-07-13 14:09:43 +02:00
|
|
|
def get_selected_episodes(self):
|
|
|
|
"""Get a list of selected episodes from treeAvailable"""
|
|
|
|
selection = self.treeAvailable.get_selection()
|
|
|
|
model, paths = selection.get_selected_rows()
|
2007-03-18 19:28:17 +01:00
|
|
|
|
2009-08-13 23:19:12 +02:00
|
|
|
episodes = [model.get_value(model.get_iter(path), EpisodeListModel.C_EPISODE) for path in paths]
|
2009-07-13 14:09:43 +02:00
|
|
|
return episodes
|
2008-12-14 17:30:17 +01:00
|
|
|
|
2009-07-13 14:09:43 +02:00
|
|
|
def on_playback_selected_episodes(self, widget):
|
2009-07-13 15:32:46 +02:00
|
|
|
self.playback_episodes(self.get_selected_episodes())
|
2007-03-18 19:28:17 +01:00
|
|
|
|
2009-07-13 14:09:43 +02:00
|
|
|
def on_shownotes_selected_episodes(self, widget):
|
|
|
|
episodes = self.get_selected_episodes()
|
|
|
|
if episodes:
|
|
|
|
episode = episodes.pop(0)
|
|
|
|
self.show_episode_shownotes(episode)
|
|
|
|
else:
|
2009-08-24 23:00:25 +02:00
|
|
|
self.show_message(_('Please select an episode from the episode list to display shownotes.'), _('No episode selected'), widget=self.treeAvailable)
|
2009-07-13 14:09:43 +02:00
|
|
|
|
|
|
|
def on_download_selected_episodes(self, widget):
|
|
|
|
episodes = self.get_selected_episodes()
|
|
|
|
self.download_episode_list(episodes)
|
|
|
|
self.update_episode_list_icons([episode.url for episode in episodes])
|
|
|
|
self.play_or_download()
|
|
|
|
|
|
|
|
def on_treeAvailable_row_activated(self, widget, path, view_column):
|
|
|
|
"""Double-click/enter action handler for treeAvailable"""
|
2011-02-01 16:47:03 +01:00
|
|
|
self.on_shownotes_selected_episodes(widget)
|
2005-11-21 19:21:25 +01:00
|
|
|
|
2009-05-12 10:36:01 +02:00
|
|
|
def show_episode_shownotes(self, episode):
|
2009-08-24 19:43:49 +02:00
|
|
|
if self.episode_shownotes_window is None:
|
|
|
|
self.episode_shownotes_window = gPodderShownotes(self.gPodder, _config=self.config, \
|
2009-09-02 15:57:09 +02:00
|
|
|
_download_episode_list=self.download_episode_list, \
|
|
|
|
_playback_episodes=self.playback_episodes, \
|
|
|
|
_delete_episode_list=self.delete_episode_list, \
|
|
|
|
_episode_list_status_changed=self.episode_list_status_changed, \
|
2009-09-22 00:42:36 +02:00
|
|
|
_cancel_task_list=self.cancel_task_list, \
|
2009-11-18 00:35:08 +01:00
|
|
|
_streaming_possible=self.streaming_possible())
|
2009-09-02 15:57:09 +02:00
|
|
|
self.episode_shownotes_window.show(episode)
|
2011-07-16 18:04:07 +02:00
|
|
|
if episode.downloading:
|
2009-09-02 15:57:09 +02:00
|
|
|
self.update_downloads_list()
|
2009-05-12 10:36:01 +02:00
|
|
|
|
2009-10-13 22:48:12 +02:00
|
|
|
def restart_auto_update_timer(self):
|
|
|
|
if self._auto_update_timer_source_id is not None:
|
2011-07-15 16:32:06 +02:00
|
|
|
logger.debug('Removing existing auto update timer.')
|
2009-10-13 22:48:12 +02:00
|
|
|
gobject.source_remove(self._auto_update_timer_source_id)
|
|
|
|
self._auto_update_timer_source_id = None
|
|
|
|
|
2010-03-02 01:36:23 +01:00
|
|
|
if self.config.auto_update_feeds and \
|
|
|
|
self.config.auto_update_frequency:
|
2009-10-13 22:48:12 +02:00
|
|
|
interval = 60*1000*self.config.auto_update_frequency
|
2011-07-15 16:32:06 +02:00
|
|
|
logger.debug('Setting up auto update timer with interval %d.',
|
|
|
|
self.config.auto_update_frequency)
|
2009-10-13 22:48:12 +02:00
|
|
|
self._auto_update_timer_source_id = gobject.timeout_add(\
|
|
|
|
interval, self._on_auto_update_timer)
|
|
|
|
|
|
|
|
def _on_auto_update_timer(self):
|
2011-07-15 16:32:06 +02:00
|
|
|
logger.debug('Auto update timer fired.')
|
2011-07-16 20:51:26 +02:00
|
|
|
self.update_feed_cache()
|
2010-01-28 17:39:10 +01:00
|
|
|
|
|
|
|
# Ask web service for sub changes (if enabled)
|
|
|
|
self.mygpo_client.flush()
|
|
|
|
|
2009-10-13 22:48:12 +02:00
|
|
|
return True
|
2006-07-20 15:19:19 +02:00
|
|
|
|
2005-11-23 20:53:18 +01:00
|
|
|
def on_treeDownloads_row_activated(self, widget, *args):
|
2009-04-01 01:12:17 +02:00
|
|
|
# Use the standard way of working on the treeview
|
|
|
|
selection = self.treeDownloads.get_selection()
|
|
|
|
(model, paths) = selection.get_selected_rows()
|
|
|
|
selected_tasks = [(gtk.TreeRowReference(model, path), model.get_value(model.get_iter(path), 0)) for path in paths]
|
|
|
|
|
|
|
|
for tree_row_reference, task in selected_tasks:
|
|
|
|
if task.status in (task.DOWNLOADING, task.QUEUED):
|
|
|
|
task.status = task.PAUSED
|
|
|
|
elif task.status in (task.CANCELLED, task.PAUSED, task.FAILED):
|
|
|
|
self.download_queue_manager.add_task(task)
|
2009-08-17 21:46:17 +02:00
|
|
|
self.enable_download_list_update()
|
2009-04-01 01:12:17 +02:00
|
|
|
elif task.status == task.DONE:
|
|
|
|
model.remove(model.get_iter(tree_row_reference.get_path()))
|
|
|
|
|
|
|
|
self.play_or_download()
|
2005-11-23 20:53:18 +01:00
|
|
|
|
2009-08-24 00:43:55 +02:00
|
|
|
# Update the tab title and downloads list
|
|
|
|
self.update_downloads_list()
|
|
|
|
|
2009-09-02 15:57:09 +02:00
|
|
|
def on_item_cancel_download_activate(self, widget):
|
|
|
|
if self.wNotebook.get_current_page() == 0:
|
|
|
|
selection = self.treeAvailable.get_selection()
|
|
|
|
(model, paths) = selection.get_selected_rows()
|
|
|
|
urls = [model.get_value(model.get_iter(path), \
|
|
|
|
self.episode_list_model.C_URL) for path in paths]
|
|
|
|
selected_tasks = [task for task in self.download_tasks_seen \
|
|
|
|
if task.url in urls]
|
|
|
|
else:
|
|
|
|
selection = self.treeDownloads.get_selection()
|
|
|
|
(model, paths) = selection.get_selected_rows()
|
|
|
|
selected_tasks = [model.get_value(model.get_iter(path), \
|
|
|
|
self.download_status_model.C_TASK) for path in paths]
|
|
|
|
self.cancel_task_list(selected_tasks)
|
2005-11-23 20:53:18 +01:00
|
|
|
|
2006-07-20 15:19:19 +02:00
|
|
|
def on_btnCancelAll_clicked(self, widget, *args):
|
2009-09-02 15:57:09 +02:00
|
|
|
self.cancel_task_list(self.download_tasks_seen)
|
2009-08-24 00:43:55 +02:00
|
|
|
|
2006-03-24 20:08:59 +01:00
|
|
|
def on_btnDownloadedDelete_clicked(self, widget, *args):
|
2009-08-10 23:14:35 +02:00
|
|
|
episodes = self.get_selected_episodes()
|
2010-05-01 14:03:05 +02:00
|
|
|
if len(episodes) == 1:
|
|
|
|
self.delete_episode_list(episodes, skip_locked=False)
|
|
|
|
else:
|
|
|
|
self.delete_episode_list(episodes)
|
2006-03-24 20:08:59 +01:00
|
|
|
|
2008-04-06 02:19:03 +02:00
|
|
|
def on_key_press(self, widget, event):
|
2008-09-27 14:20:43 +02:00
|
|
|
# Allow tab switching with Ctrl + PgUp/PgDown
|
|
|
|
if event.state & gtk.gdk.CONTROL_MASK:
|
|
|
|
if event.keyval == gtk.keysyms.Page_Up:
|
|
|
|
self.wNotebook.prev_page()
|
|
|
|
return True
|
|
|
|
elif event.keyval == gtk.keysyms.Page_Down:
|
|
|
|
self.wNotebook.next_page()
|
|
|
|
return True
|
|
|
|
|
2009-01-19 19:00:24 +01:00
|
|
|
return False
|
2008-01-09 23:46:17 +01:00
|
|
|
|
|
|
|
def uniconify_main_window(self):
|
2009-09-05 00:37:12 +02:00
|
|
|
if self.is_iconified():
|
2010-09-26 22:51:50 +02:00
|
|
|
# We need to hide and then show the window in WMs like Metacity
|
|
|
|
# or KWin4 to move the window to the active workspace
|
|
|
|
# (see http://gpodder.org/bug/1125)
|
|
|
|
self.gPodder.hide()
|
|
|
|
self.gPodder.show()
|
2008-01-09 23:46:17 +01:00
|
|
|
self.gPodder.present()
|
|
|
|
|
|
|
|
def iconify_main_window(self):
|
2009-09-05 00:37:12 +02:00
|
|
|
if not self.is_iconified():
|
2008-07-11 19:13:04 +02:00
|
|
|
self.gPodder.iconify()
|
|
|
|
|
2009-02-25 14:57:45 +01:00
|
|
|
@dbus.service.method(gpodder.dbus_interface)
|
|
|
|
def show_gui_window(self):
|
2010-08-16 22:48:12 +02:00
|
|
|
parent = self.get_dialog_parent()
|
|
|
|
parent.present()
|
2009-02-25 14:57:45 +01:00
|
|
|
|
2009-08-26 14:45:54 +02:00
|
|
|
@dbus.service.method(gpodder.dbus_interface)
|
|
|
|
def subscribe_to_url(self, url):
|
|
|
|
gPodderAddPodcast(self.gPodder,
|
|
|
|
add_urls_callback=self.add_podcast_list,
|
|
|
|
preset_url=url)
|
|
|
|
|
2009-09-09 14:06:17 +02:00
|
|
|
@dbus.service.method(gpodder.dbus_interface)
|
|
|
|
def mark_episode_played(self, filename):
|
|
|
|
if filename is None:
|
|
|
|
return False
|
|
|
|
|
|
|
|
for channel in self.channels:
|
|
|
|
for episode in channel.get_all_episodes():
|
|
|
|
fn = episode.local_filename(create=False, check_only=True)
|
|
|
|
if fn == filename:
|
|
|
|
episode.mark(is_played=True)
|
|
|
|
self.db.commit()
|
|
|
|
self.update_episode_list_icons([episode.url])
|
|
|
|
self.update_podcast_list_model([episode.channel.url])
|
|
|
|
return True
|
|
|
|
|
|
|
|
return False
|
|
|
|
|
2011-10-19 12:37:55 +02:00
|
|
|
def hooks_podcast_update_cb(self, podcast):
|
|
|
|
logger.debug('hooks_podcast_update_cb(%s)', podcast)
|
|
|
|
self.update_feed_cache(channels=[podcast],
|
|
|
|
show_new_episodes_dialog=False)
|
2011-08-02 15:45:49 +02:00
|
|
|
|
2011-10-19 12:37:55 +02:00
|
|
|
def hooks_episode_download_cb(self, episode):
|
|
|
|
logger.debug('hooks_episode_download_cb(%s)', episode)
|
2011-08-02 15:45:49 +02:00
|
|
|
self.download_episode_list(episodes=[episode])
|
2005-11-21 19:21:25 +01:00
|
|
|
|
2009-08-26 14:45:54 +02:00
|
|
|
def main(options=None):
|
2006-04-03 22:59:10 +02:00
|
|
|
gobject.threads_init()
|
2009-09-05 15:42:55 +02:00
|
|
|
gobject.set_application_name('gPodder')
|
2009-09-06 23:05:38 +02:00
|
|
|
|
2009-09-05 15:42:55 +02:00
|
|
|
gtk.window_set_default_icon_name('gpodder')
|
2009-09-05 00:53:46 +02:00
|
|
|
gtk.about_dialog_set_url_hook(lambda dlg, link, data: util.open_website(link), None)
|
2005-11-21 19:21:25 +01:00
|
|
|
|
2009-05-30 11:31:02 +02:00
|
|
|
try:
|
2010-08-17 23:52:27 +02:00
|
|
|
dbus_main_loop = dbus.glib.DBusGMainLoop(set_as_default=True)
|
|
|
|
gpodder.dbus_session_bus = dbus.SessionBus(dbus_main_loop)
|
|
|
|
|
|
|
|
bus_name = dbus.service.BusName(gpodder.dbus_bus_name, bus=gpodder.dbus_session_bus)
|
2009-05-30 11:31:02 +02:00
|
|
|
except dbus.exceptions.DBusException, dbe:
|
2011-07-15 16:32:06 +02:00
|
|
|
logger.warn('Cannot get "on the bus".', exc_info=True)
|
2009-05-30 11:31:02 +02:00
|
|
|
dlg = gtk.MessageDialog(None, gtk.DIALOG_MODAL, gtk.MESSAGE_ERROR, \
|
|
|
|
gtk.BUTTONS_CLOSE, _('Cannot start gPodder'))
|
|
|
|
dlg.format_secondary_markup(_('D-Bus error: %s') % (str(dbe),))
|
|
|
|
dlg.set_title('gPodder')
|
|
|
|
dlg.run()
|
|
|
|
dlg.destroy()
|
|
|
|
sys.exit(0)
|
2009-05-11 21:35:33 +02:00
|
|
|
|
2011-10-12 19:59:09 +02:00
|
|
|
gp = gPodder(bus_name, core.Core(UIConfig, model_class=Model))
|
2009-08-26 14:45:54 +02:00
|
|
|
|
|
|
|
# Handle options
|
|
|
|
if options.subscribe:
|
|
|
|
util.idle_add(gp.subscribe_to_url, options.subscribe)
|
|
|
|
|
2010-03-21 14:54:16 +01:00
|
|
|
# mac OS X stuff :
|
|
|
|
# handle "subscribe to podcast" events from firefox
|
|
|
|
if platform.system() == 'Darwin':
|
2011-07-15 17:44:11 +02:00
|
|
|
from gpodder.gtkui import macosx
|
|
|
|
macosx.register_handlers(gp)
|
2010-03-21 14:54:16 +01:00
|
|
|
# end mac OS X stuff
|
|
|
|
|
2009-02-25 14:57:45 +01:00
|
|
|
gp.run()
|
2005-11-21 19:21:25 +01:00
|
|
|
|
2008-02-20 13:46:51 +01:00
|
|
|
|
|
|
|
|