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
|
2018-01-28 19:39:53 +01:00
|
|
|
# Copyright (c) 2005-2018 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
|
|
|
#
|
|
|
|
|
2018-07-24 11:08:10 +02:00
|
|
|
import cgi
|
|
|
|
import collections
|
|
|
|
import glob
|
|
|
|
import logging
|
2005-11-21 19:21:25 +01:00
|
|
|
import os
|
2010-03-21 14:54:16 +01:00
|
|
|
import platform
|
2010-10-05 12:41:03 +02:00
|
|
|
import random
|
2017-04-21 03:24:08 +02:00
|
|
|
import re
|
2007-04-23 17:18:31 +02:00
|
|
|
import shutil
|
2008-01-21 10:52:09 +01:00
|
|
|
import subprocess
|
2018-07-24 11:08:10 +02:00
|
|
|
import sys
|
2009-08-10 18:32:08 +02:00
|
|
|
import tempfile
|
2018-07-24 11:08:10 +02:00
|
|
|
import threading
|
|
|
|
import time
|
2018-05-05 23:50:37 +02:00
|
|
|
import urllib.error
|
|
|
|
import urllib.parse
|
|
|
|
import urllib.request
|
2009-05-09 15:21:04 +02:00
|
|
|
|
2011-07-15 17:55:38 +02:00
|
|
|
import dbus
|
|
|
|
import dbus.glib
|
2018-07-24 11:08:10 +02:00
|
|
|
import dbus.mainloop
|
|
|
|
import dbus.service
|
2009-05-09 15:21:04 +02:00
|
|
|
|
2008-04-06 02:19:03 +02:00
|
|
|
import gpodder
|
2018-07-24 11:08:10 +02:00
|
|
|
from gpodder import (common, core, download, extensions, feedcore, my, opml,
|
|
|
|
player, util, youtube)
|
|
|
|
from gpodder.dbusproxy import DBusPodcastsProxy
|
|
|
|
from gpodder.gtkui import shownotes
|
2009-08-24 18:11:58 +02:00
|
|
|
from gpodder.gtkui.config import UIConfig
|
2011-07-16 18:35:14 +02:00
|
|
|
from gpodder.gtkui.desktop.channel import gPodderChannel
|
|
|
|
from gpodder.gtkui.desktop.episodeselector import gPodderEpisodeSelector
|
2018-10-13 18:18:25 +02:00
|
|
|
from gpodder.gtkui.desktop.exportlocal import gPodderExportToLocalFolder
|
2011-07-16 18:35:14 +02:00
|
|
|
from gpodder.gtkui.desktop.podcastdirectory import gPodderPodcastDirectory
|
2018-07-24 11:08:10 +02:00
|
|
|
from gpodder.gtkui.desktop.preferences import gPodderPreferences
|
|
|
|
from gpodder.gtkui.desktop.welcome import gPodderWelcome
|
|
|
|
from gpodder.gtkui.desktopfile import UserAppsReader
|
|
|
|
from gpodder.gtkui.download import DownloadStatusModel
|
|
|
|
from gpodder.gtkui.draw import (EPISODE_LIST_ICON_SIZE, draw_cake_pixbuf,
|
|
|
|
draw_text_box_centered)
|
|
|
|
from gpodder.gtkui.interface.addpodcast import gPodderAddPodcast
|
|
|
|
from gpodder.gtkui.interface.common import BuilderWidget, TreeViewHelper
|
2011-07-16 18:35:14 +02:00
|
|
|
from gpodder.gtkui.interface.progress import ProgressIndicator
|
2018-07-24 11:08:10 +02:00
|
|
|
from gpodder.gtkui.model import EpisodeListModel, Model, PodcastListModel
|
|
|
|
from gpodder.gtkui.services import CoverDownloader
|
|
|
|
from gpodder.gtkui.widgets import SimpleMessageArea
|
|
|
|
from gpodder.model import PodcastEpisode, check_root_folder_path
|
2018-10-14 16:10:45 +02:00
|
|
|
from gpodder.syncui import gPodderSyncUI
|
2008-04-06 02:19:03 +02:00
|
|
|
|
2018-07-28 17:04:41 +02:00
|
|
|
import gi # isort:skip
|
|
|
|
gi.require_version('Gtk', '3.0') # isort:skip
|
|
|
|
from gi.repository import Gdk, GdkPixbuf, Gio, GLib, GObject, Gtk, Pango # isort:skip
|
2012-07-02 05:53:33 +02:00
|
|
|
|
2018-07-24 11:08:10 +02:00
|
|
|
|
2011-07-15 16:32:06 +02:00
|
|
|
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
|
2010-01-08 01:58:15 +01:00
|
|
|
|
2012-03-02 21:20:01 +01:00
|
|
|
|
2009-05-08 14:28:53 +02:00
|
|
|
class gPodder(BuilderWidget, dbus.service.Object):
|
2011-09-19 10:47:20 +02:00
|
|
|
# Width (in pixels) of episode list icon
|
|
|
|
EPISODE_LIST_ICON_WIDTH = 40
|
|
|
|
|
2016-09-28 12:04:21 +02:00
|
|
|
def __init__(self, app, bus_name, gpodder_core, options):
|
2009-02-25 14:57:45 +01:00
|
|
|
dbus.service.Object.__init__(self, object_path=gpodder.dbus_gui_object_path, bus_name=bus_name)
|
2012-02-08 22:03:51 +01:00
|
|
|
self.podcasts_proxy = DBusPodcastsProxy(lambda: self.channels,
|
|
|
|
self.on_itemUpdate_activate,
|
|
|
|
self.playback_episodes,
|
|
|
|
self.download_episode_list,
|
|
|
|
self.episode_object_by_uri,
|
2010-01-08 01:58:15 +01:00
|
|
|
bus_name)
|
2016-09-28 12:04:21 +02:00
|
|
|
self.application = app
|
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
|
2014-09-27 15:01:06 +02:00
|
|
|
self.options = options
|
2017-07-25 14:57:36 +02:00
|
|
|
self.extensions_menu = None
|
|
|
|
self.extensions_actions = []
|
2016-09-28 12:04:21 +02:00
|
|
|
BuilderWidget.__init__(self, None, _builder_expose={'app': app})
|
2013-02-08 11:08:28 +01:00
|
|
|
|
2005-11-21 19:21:25 +01:00
|
|
|
def new(self):
|
2012-02-24 00:44:16 +01:00
|
|
|
gpodder.user_extensions.on_ui_object_available('gpodder-gtk', 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()
|
|
|
|
|
2012-01-03 23:59:19 +01:00
|
|
|
self.config.connect_gtk_window(self.main_window, 'main_window')
|
2011-04-30 18:00:28 +02:00
|
|
|
|
2013-03-05 11:36:41 +01:00
|
|
|
self.config.connect_gtk_paned('ui.gtk.state.main_window.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
|
|
|
|
2016-09-20 12:10:34 +02:00
|
|
|
self.shownotes_pane = Gtk.Box()
|
2017-01-29 22:38:02 +01:00
|
|
|
self.shownotes_object = shownotes.get_shownotes(self.config.ui.gtk.html_shownotes, self.shownotes_pane)
|
2013-03-05 11:36:41 +01:00
|
|
|
|
|
|
|
# Vertical paned for the episode list and shownotes
|
2016-09-20 12:10:34 +02:00
|
|
|
self.vpaned = Gtk.Paned(orientation=Gtk.Orientation.VERTICAL)
|
2013-03-05 11:36:41 +01:00
|
|
|
paned = self.vbox_episode_list.get_parent()
|
|
|
|
self.vbox_episode_list.reparent(self.vpaned)
|
|
|
|
self.vpaned.child_set_property(self.vbox_episode_list, 'resize', True)
|
|
|
|
self.vpaned.child_set_property(self.vbox_episode_list, 'shrink', False)
|
2014-01-23 22:44:52 +01:00
|
|
|
self.vpaned.pack2(self.shownotes_pane, resize=False, shrink=False)
|
2013-03-05 11:36:41 +01:00
|
|
|
self.vpaned.show()
|
|
|
|
|
|
|
|
# Minimum height for both episode list and shownotes
|
|
|
|
self.vbox_episode_list.set_size_request(-1, 100)
|
2014-01-23 22:44:52 +01:00
|
|
|
self.shownotes_pane.set_size_request(-1, 100)
|
2013-03-05 11:36:41 +01:00
|
|
|
|
|
|
|
self.config.connect_gtk_paned('ui.gtk.state.main_window.episode_list_size',
|
|
|
|
self.vpaned)
|
|
|
|
paned.add2(self.vpaned)
|
2012-11-17 16:24:18 +01:00
|
|
|
|
2009-09-28 14:23:21 +02:00
|
|
|
self.new_episodes_window = None
|
2009-04-01 13:34:19 +02:00
|
|
|
|
2009-08-24 16:17:32 +02:00
|
|
|
self.download_status_model = DownloadStatusModel()
|
2016-11-15 04:54:21 +01:00
|
|
|
self.download_queue_manager = download.DownloadQueueManager(self.config, self.download_status_model)
|
2007-08-28 00:18:01 +02:00
|
|
|
|
2018-04-29 15:59:22 +02:00
|
|
|
self.config.connect_gtk_spinbutton('limit.downloads.concurrent', self.spinMaxDownloads,
|
2018-08-25 09:42:10 +02:00
|
|
|
self.config.limit.downloads.concurrent_max)
|
2011-07-16 18:35:14 +02:00
|
|
|
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-16 18:35:14 +02:00
|
|
|
# When the amount of maximum downloads changes, notify the queue manager
|
2018-05-06 22:26:54 +02:00
|
|
|
def changed_cb(spinbutton):
|
|
|
|
return self.download_queue_manager.update_max_downloads()
|
|
|
|
|
2011-07-16 18:35:14 +02:00
|
|
|
self.spinMaxDownloads.connect('value-changed', changed_cb)
|
2018-04-29 15:15:18 +02:00
|
|
|
self.cbMaxDownloads.connect('toggled', changed_cb)
|
2008-04-17 17:45:29 +02:00
|
|
|
|
2014-09-27 14:51:35 +02:00
|
|
|
# Keep a reference to the last add podcast dialog instance
|
|
|
|
self._add_podcast_dialog = None
|
|
|
|
|
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)
|
2017-03-27 22:58:47 +02:00
|
|
|
self.apply_podcast_list_hide_boring()
|
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)
|
|
|
|
|
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
|
|
|
|
|
2016-09-25 14:31:58 +02:00
|
|
|
# Subscribed channels
|
|
|
|
self.active_channel = None
|
|
|
|
self.channels = self.model.get_podcasts()
|
|
|
|
|
|
|
|
# For loading the list model
|
|
|
|
self.episode_list_model = EpisodeListModel(self.config, self.on_episode_list_filter_changed)
|
|
|
|
|
2016-09-28 15:51:24 +02:00
|
|
|
self.create_actions()
|
|
|
|
|
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()
|
|
|
|
|
|
|
|
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
|
|
|
|
2012-10-13 16:54:27 +02:00
|
|
|
# Set up the first instance of MygPoClient
|
|
|
|
self.mygpo_client = my.MygPoClient(self.config)
|
2011-02-25 00:45:49 +01:00
|
|
|
|
2017-06-25 09:25:35 +02:00
|
|
|
self.inject_extensions_menu()
|
|
|
|
|
2012-02-04 21:43:37 +01:00
|
|
|
gpodder.user_extensions.on_ui_initialized(self.model,
|
|
|
|
self.extensions_podcast_update_cb,
|
|
|
|
self.extensions_episode_download_cb)
|
2011-08-02 15:45:49 +02:00
|
|
|
|
2017-09-13 20:32:23 +02:00
|
|
|
gpodder.user_extensions.on_application_started()
|
|
|
|
|
2009-09-05 00:12:53 +02:00
|
|
|
# load list of user applications for audio playback
|
|
|
|
self.user_apps_reader = UserAppsReader(['audio', 'video'])
|
2012-07-10 13:52:34 +02:00
|
|
|
util.run_in_background(self.user_apps_reader.read)
|
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
|
|
|
|
|
2012-08-16 11:08:35 +02:00
|
|
|
self.partial_downloads_indicator = None
|
|
|
|
util.run_in_background(self.find_partial_downloads)
|
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
|
|
|
|
2012-01-21 10:41:29 +01:00
|
|
|
# Find expired (old) episodes and delete them
|
2012-10-13 16:21:25 +02:00
|
|
|
old_episodes = list(common.get_expired_episodes(self.channels, self.config))
|
2012-01-21 10:41:29 +01:00
|
|
|
if len(old_episodes) > 0:
|
|
|
|
self.delete_episode_list(old_episodes, confirm=False)
|
|
|
|
updated_urls = set(e.channel.url for e in old_episodes)
|
|
|
|
self.update_podcast_list_model(updated_urls)
|
2009-09-05 00:12:53 +02:00
|
|
|
|
2010-01-28 17:39:10 +01:00
|
|
|
# Do the initial sync with the web service
|
2013-10-14 20:55:26 +02:00
|
|
|
if self.mygpo_client.can_access_webservice():
|
|
|
|
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
|
2014-09-27 15:01:06 +02:00
|
|
|
if self.options.subscribe:
|
|
|
|
util.idle_add(self.subscribe_to_url, self.options.subscribe)
|
|
|
|
elif not self.channels:
|
2011-10-19 14:47:16 +02:00
|
|
|
self.on_itemUpdate_activate()
|
2012-03-03 21:09:05 +01:00
|
|
|
elif self.config.software_update.check_on_startup:
|
|
|
|
# Check for software updates from gpodder.org
|
|
|
|
diff = time.time() - self.config.software_update.last_check
|
2018-03-23 20:53:31 +01:00
|
|
|
if diff > (60 * 60 * 24) * self.config.software_update.interval:
|
2012-03-03 21:09:05 +01:00
|
|
|
self.config.software_update.last_check = int(time.time())
|
|
|
|
self.check_for_updates(silent=True)
|
2009-09-05 00:12:53 +02:00
|
|
|
|
2016-09-28 12:04:21 +02:00
|
|
|
def create_actions(self):
|
2016-10-01 21:01:29 +02:00
|
|
|
g = self.gPodder
|
2016-09-28 12:04:21 +02:00
|
|
|
|
|
|
|
action = Gio.SimpleAction.new_stateful(
|
2016-10-02 19:34:24 +02:00
|
|
|
'showEpisodeDescription', None, GLib.Variant.new_boolean(self.config.episode_list_descriptions))
|
|
|
|
action.connect('activate', self.on_itemShowDescription_activate)
|
2016-10-01 21:01:29 +02:00
|
|
|
g.add_action(action)
|
2016-09-28 12:04:21 +02:00
|
|
|
|
|
|
|
action = Gio.SimpleAction.new_stateful(
|
2016-10-02 19:34:24 +02:00
|
|
|
'viewHideBoringPodcasts', None, GLib.Variant.new_boolean(self.config.podcast_list_hide_boring))
|
|
|
|
action.connect('activate', self.on_item_view_hide_boring_podcasts_toggled)
|
2016-10-01 21:01:29 +02:00
|
|
|
g.add_action(action)
|
2016-09-28 12:04:21 +02:00
|
|
|
|
2018-05-27 20:09:40 +02:00
|
|
|
value = EpisodeListModel.VIEWS[
|
|
|
|
self.config.episode_list_view_mode or EpisodeListModel.VIEW_ALL]
|
2016-09-28 12:04:21 +02:00
|
|
|
action = Gio.SimpleAction.new_stateful(
|
2018-05-27 20:09:40 +02:00
|
|
|
'viewEpisodes', GLib.VariantType.new('s'),
|
|
|
|
GLib.Variant.new_string(value))
|
2016-10-02 19:34:24 +02:00
|
|
|
action.connect('activate', self.on_item_view_episodes_changed)
|
2016-10-01 21:01:29 +02:00
|
|
|
g.add_action(action)
|
2016-09-28 12:04:21 +02:00
|
|
|
|
2016-10-02 19:34:24 +02:00
|
|
|
action = Gio.SimpleAction.new('update', None)
|
|
|
|
action.connect('activate', self.on_itemUpdate_activate)
|
2016-10-01 21:01:29 +02:00
|
|
|
g.add_action(action)
|
2016-09-28 15:51:24 +02:00
|
|
|
self.update_action = action
|
|
|
|
|
2016-10-02 19:34:24 +02:00
|
|
|
action = Gio.SimpleAction.new('downloadAllNew', None)
|
|
|
|
action.connect('activate', self.on_itemDownloadAllNew_activate)
|
2016-10-01 21:01:29 +02:00
|
|
|
g.add_action(action)
|
2018-01-30 14:04:28 +01:00
|
|
|
|
2016-10-02 19:34:24 +02:00
|
|
|
action = Gio.SimpleAction.new('removeOldEpisodes', None)
|
|
|
|
action.connect('activate', self.on_itemRemoveOldEpisodes_activate)
|
2016-10-01 21:01:29 +02:00
|
|
|
g.add_action(action)
|
2016-09-28 12:04:21 +02:00
|
|
|
|
2016-10-02 19:34:24 +02:00
|
|
|
action = Gio.SimpleAction.new('discover', None)
|
|
|
|
action.connect('activate', self.on_itemImportChannels_activate)
|
2016-10-01 21:01:29 +02:00
|
|
|
g.add_action(action)
|
2016-09-28 12:04:21 +02:00
|
|
|
|
2016-10-02 19:34:24 +02:00
|
|
|
action = Gio.SimpleAction.new('addChannel', None)
|
|
|
|
action.connect('activate', self.on_itemAddChannel_activate)
|
2016-10-01 21:01:29 +02:00
|
|
|
g.add_action(action)
|
2016-09-28 12:04:21 +02:00
|
|
|
|
2016-10-02 19:34:24 +02:00
|
|
|
action = Gio.SimpleAction.new('massUnsubscribe', None)
|
|
|
|
action.connect('activate', self.on_itemMassUnsubscribe_activate)
|
2016-10-01 21:01:29 +02:00
|
|
|
g.add_action(action)
|
2016-09-28 12:04:21 +02:00
|
|
|
|
2016-10-02 19:34:24 +02:00
|
|
|
action = Gio.SimpleAction.new('updateChannel', None)
|
|
|
|
action.connect('activate', self.on_itemUpdateChannel_activate)
|
2016-10-01 21:01:29 +02:00
|
|
|
g.add_action(action)
|
2016-09-28 15:51:24 +02:00
|
|
|
self.update_channel_action = action
|
|
|
|
|
2016-10-02 19:34:24 +02:00
|
|
|
action = Gio.SimpleAction.new('editChannel', None)
|
|
|
|
action.connect('activate', self.on_itemEditChannel_activate)
|
2016-10-01 21:01:29 +02:00
|
|
|
g.add_action(action)
|
2016-09-28 15:51:24 +02:00
|
|
|
self.edit_channel_action = action
|
|
|
|
|
2016-10-02 19:34:24 +02:00
|
|
|
action = Gio.SimpleAction.new('importFromFile', None)
|
|
|
|
action.connect('activate', self.on_item_import_from_file_activate)
|
2016-10-01 21:01:29 +02:00
|
|
|
g.add_action(action)
|
2016-09-28 12:04:21 +02:00
|
|
|
|
2016-10-02 19:34:24 +02:00
|
|
|
action = Gio.SimpleAction.new('exportChannels', None)
|
|
|
|
action.connect('activate', self.on_itemExportChannels_activate)
|
2016-10-01 21:01:29 +02:00
|
|
|
g.add_action(action)
|
2016-09-28 12:04:21 +02:00
|
|
|
|
2016-10-02 19:34:24 +02:00
|
|
|
action = Gio.SimpleAction.new('play', None)
|
|
|
|
action.connect('activate', self.on_playback_selected_episodes)
|
2016-10-01 21:01:29 +02:00
|
|
|
g.add_action(action)
|
2016-09-28 15:51:24 +02:00
|
|
|
self.play_action = action
|
|
|
|
|
2016-10-02 19:34:24 +02:00
|
|
|
action = Gio.SimpleAction.new('open', None)
|
|
|
|
action.connect('activate', self.on_playback_selected_episodes)
|
2016-10-01 21:01:29 +02:00
|
|
|
g.add_action(action)
|
2016-09-28 15:51:24 +02:00
|
|
|
self.open_action = action
|
|
|
|
|
2016-10-02 19:34:24 +02:00
|
|
|
action = Gio.SimpleAction.new('download', None)
|
|
|
|
action.connect('activate', self.on_download_selected_episodes)
|
2016-10-01 21:01:29 +02:00
|
|
|
g.add_action(action)
|
2016-09-28 15:51:24 +02:00
|
|
|
self.download_action = action
|
|
|
|
|
2016-10-02 19:34:24 +02:00
|
|
|
action = Gio.SimpleAction.new('cancel', None)
|
|
|
|
action.connect('activate', self.on_item_cancel_download_activate)
|
2016-10-01 21:01:29 +02:00
|
|
|
g.add_action(action)
|
2016-09-28 15:51:24 +02:00
|
|
|
self.cancel_action = action
|
|
|
|
|
2016-10-02 19:34:24 +02:00
|
|
|
action = Gio.SimpleAction.new('delete', None)
|
|
|
|
action.connect('activate', self.on_btnDownloadedDelete_clicked)
|
2016-10-01 21:01:29 +02:00
|
|
|
g.add_action(action)
|
2016-09-28 15:51:24 +02:00
|
|
|
self.delete_action = action
|
|
|
|
|
2016-10-02 19:34:24 +02:00
|
|
|
action = Gio.SimpleAction.new('toggleEpisodeNew', None)
|
|
|
|
action.connect('activate', self.on_item_toggle_played_activate)
|
2016-10-01 21:01:29 +02:00
|
|
|
g.add_action(action)
|
2016-09-28 15:51:24 +02:00
|
|
|
self.toggle_episode_new_action = action
|
|
|
|
|
2016-10-02 19:34:24 +02:00
|
|
|
action = Gio.SimpleAction.new('toggleEpisodeLock', None)
|
|
|
|
action.connect('activate', self.on_item_toggle_lock_activate)
|
2016-10-01 21:01:29 +02:00
|
|
|
g.add_action(action)
|
2016-09-28 15:51:24 +02:00
|
|
|
self.toggle_episode_lock_action = action
|
|
|
|
|
2016-10-02 19:34:24 +02:00
|
|
|
action = Gio.SimpleAction.new('toggleShownotes', None)
|
|
|
|
action.connect('activate', self.on_shownotes_selected_episodes)
|
2016-10-01 21:01:29 +02:00
|
|
|
g.add_action(action)
|
2016-09-28 15:51:24 +02:00
|
|
|
|
|
|
|
action = Gio.SimpleAction.new_stateful(
|
2016-10-02 19:34:24 +02:00
|
|
|
'showToolbar', None, GLib.Variant.new_boolean(self.config.show_toolbar))
|
|
|
|
action.connect('activate', self.on_itemShowToolbar_activate)
|
2016-10-01 21:01:29 +02:00
|
|
|
g.add_action(action)
|
2016-09-28 15:51:24 +02:00
|
|
|
|
2016-10-02 19:34:24 +02:00
|
|
|
action = Gio.SimpleAction.new('sync', None)
|
|
|
|
action.connect('activate', self.on_sync_to_device_activate)
|
2016-10-01 21:01:29 +02:00
|
|
|
g.add_action(action)
|
2016-09-28 12:04:21 +02:00
|
|
|
|
2016-10-02 19:34:24 +02:00
|
|
|
action = Gio.SimpleAction.new('updateYoutubeSubscriptions', None)
|
|
|
|
action.connect('activate', self.on_update_youtube_subscriptions_activate)
|
2016-10-01 21:01:29 +02:00
|
|
|
g.add_action(action)
|
2016-09-28 12:04:21 +02:00
|
|
|
|
2017-06-25 09:25:35 +02:00
|
|
|
def inject_extensions_menu(self):
|
2017-07-25 14:57:36 +02:00
|
|
|
"""
|
|
|
|
Update Extras/Extensions menu.
|
|
|
|
Called at startup and when en/dis-abling extenstions.
|
|
|
|
"""
|
2017-06-25 09:25:35 +02:00
|
|
|
def gen_callback(label, callback):
|
|
|
|
return lambda action, param: callback()
|
|
|
|
|
2017-07-25 14:57:36 +02:00
|
|
|
for a in self.extensions_actions:
|
|
|
|
self.gPodder.remove_action(a.get_property('name'))
|
|
|
|
self.extensions_actions = []
|
|
|
|
|
|
|
|
if self.extensions_menu is None:
|
|
|
|
# insert menu section at startup (hides when empty)
|
|
|
|
self.extensions_menu = Gio.Menu.new()
|
2017-06-25 09:25:35 +02:00
|
|
|
menubar = self.application.get_menubar()
|
|
|
|
for i in range(0, menubar.get_n_items()):
|
|
|
|
menu = menubar.do_get_item_link(menubar, i, Gio.MENU_LINK_SUBMENU)
|
|
|
|
menuname = menubar.get_item_attribute_value(i, Gio.MENU_ATTRIBUTE_LABEL, None)
|
2018-07-28 21:04:25 +02:00
|
|
|
if menuname is not None and menuname.get_string() == _('E_xtras'):
|
2017-07-25 14:57:36 +02:00
|
|
|
menu.append_section(_('Extensions'), self.extensions_menu)
|
|
|
|
else:
|
|
|
|
self.extensions_menu.remove_all()
|
2017-06-25 09:25:35 +02:00
|
|
|
|
2017-07-25 14:57:36 +02:00
|
|
|
extension_entries = gpodder.user_extensions.on_create_menu()
|
|
|
|
if extension_entries:
|
|
|
|
# populate menu
|
|
|
|
for i, (label, callback) in enumerate(extension_entries):
|
|
|
|
action_id = 'extensions.action_%d' % i
|
|
|
|
action = Gio.SimpleAction.new(action_id)
|
|
|
|
action.connect('activate', gen_callback(label, callback))
|
|
|
|
self.extensions_actions.append(action)
|
|
|
|
self.gPodder.add_action(action)
|
|
|
|
itm = Gio.MenuItem.new(label, 'win.' + action_id)
|
|
|
|
self.extensions_menu.append_item(itm)
|
2017-06-25 09:25:35 +02:00
|
|
|
|
2012-08-16 11:08:35 +02:00
|
|
|
def find_partial_downloads(self):
|
|
|
|
def start_progress_callback(count):
|
|
|
|
self.partial_downloads_indicator = ProgressIndicator(
|
|
|
|
_('Loading incomplete downloads'),
|
|
|
|
_('Some episodes have not finished downloading in a previous session.'),
|
|
|
|
False, self.get_dialog_parent())
|
2018-05-29 22:54:05 +02:00
|
|
|
self.partial_downloads_indicator.on_message(N_(
|
|
|
|
'%(count)d partial file', '%(count)d partial files',
|
|
|
|
count) % {'count': count})
|
2012-08-16 11:08:35 +02:00
|
|
|
|
|
|
|
util.idle_add(self.wNotebook.set_current_page, 1)
|
|
|
|
|
|
|
|
def progress_callback(title, progress):
|
|
|
|
self.partial_downloads_indicator.on_message(title)
|
|
|
|
self.partial_downloads_indicator.on_progress(progress)
|
|
|
|
|
|
|
|
def finish_progress_callback(resumable_episodes):
|
|
|
|
util.idle_add(self.partial_downloads_indicator.on_finished)
|
|
|
|
self.partial_downloads_indicator = None
|
|
|
|
|
|
|
|
if resumable_episodes:
|
|
|
|
def offer_resuming():
|
|
|
|
self.download_episode_list_paused(resumable_episodes)
|
2016-09-25 14:31:58 +02:00
|
|
|
resume_all = Gtk.Button(_('Resume all'))
|
2018-02-06 18:33:52 +01:00
|
|
|
|
2012-08-16 11:08:35 +02:00
|
|
|
def on_resume_all(button):
|
|
|
|
selection = self.treeDownloads.get_selection()
|
|
|
|
selection.select_all()
|
|
|
|
selected_tasks, _, _, _, _, _ = 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)
|
|
|
|
|
2016-11-10 02:05:43 +01:00
|
|
|
self.message_area = SimpleMessageArea(
|
|
|
|
_('Incomplete downloads from a previous session were found.'),
|
|
|
|
(resume_all,))
|
|
|
|
self.vboxDownloadStatusWidgets.attach(self.message_area, 0, -1, 1, 1)
|
2012-08-16 11:08:35 +02:00
|
|
|
self.message_area.show_all()
|
|
|
|
common.clean_up_downloads(delete_partial=False)
|
|
|
|
util.idle_add(offer_resuming)
|
|
|
|
else:
|
|
|
|
util.idle_add(self.wNotebook.set_current_page, 0)
|
|
|
|
|
|
|
|
common.find_partial_downloads(self.channels,
|
|
|
|
start_progress_callback,
|
|
|
|
progress_callback,
|
|
|
|
finish_progress_callback)
|
|
|
|
|
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('/'):
|
2016-11-21 23:13:46 +01:00
|
|
|
uri = 'file://' + urllib.parse.quote(uri)
|
2010-06-12 17:51:54 +02:00
|
|
|
|
2016-11-21 23:13:46 +01:00
|
|
|
prefix = 'file://' + urllib.parse.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
|
2018-05-06 22:26:54 +02:00
|
|
|
|
|
|
|
def is_channel(c):
|
|
|
|
return True
|
|
|
|
|
|
|
|
def is_episode(e):
|
|
|
|
return e.url == uri
|
2011-07-26 12:17:17 +02:00
|
|
|
|
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
|
|
|
|
|
2016-11-21 23:13:46 +01:00
|
|
|
filename = urllib.parse.unquote(uri[len(prefix):])
|
|
|
|
file_parts = [_f for _f in filename.split(os.sep) if _f]
|
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
|
|
|
|
|
2018-05-06 22:26:54 +02:00
|
|
|
def is_channel(c):
|
|
|
|
return c.download_folder == foldername
|
|
|
|
|
|
|
|
def is_episode(e):
|
|
|
|
return 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()
|
2015-11-17 19:37:09 +01:00
|
|
|
self.episode_list_status_changed([episode])
|
2010-06-12 17:51:54 +02:00
|
|
|
|
|
|
|
# Submit this action to the webservice
|
2012-02-08 22:03:51 +01:00
|
|
|
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):
|
2012-11-17 12:53:00 +01:00
|
|
|
# In the future, we might retrieve the title from gpodder.net here,
|
|
|
|
# but for now, we just use "None" to use the feed-provided title
|
|
|
|
title = None
|
|
|
|
add_list = [(title, c.action.url)
|
|
|
|
for c in selected if c.action.is_add]
|
2010-01-28 17:39:10 +01:00
|
|
|
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
|
2018-05-08 10:43:56 +02:00
|
|
|
gPodderEpisodeSelector(self.main_window,
|
|
|
|
title=_('Confirm changes from gpodder.net'),
|
|
|
|
instructions=_('Select the actions you want to carry out.'),
|
|
|
|
episodes=changes,
|
|
|
|
columns=columns,
|
|
|
|
size_attribute=None,
|
|
|
|
stock_ok_button=Gtk.STOCK_APPLY,
|
|
|
|
callback=execute_podcast_actions,
|
2010-01-19 23:43:59 +01:00
|
|
|
_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()
|
2011-11-01 19:38:07 +01:00
|
|
|
changed = False
|
2010-01-28 17:39:10 +01:00
|
|
|
|
|
|
|
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()
|
2011-11-01 19:38:07 +01:00
|
|
|
changed = True
|
2010-01-28 17:39:10 +01:00
|
|
|
break
|
2010-01-19 23:43:59 +01:00
|
|
|
|
2011-11-01 19:38:07 +01:00
|
|
|
if changed:
|
|
|
|
util.idle_add(self.update_episode_list_model)
|
|
|
|
|
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)
|
2018-05-08 10:43:56 +02:00
|
|
|
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.'))
|
2016-11-21 23:13:46 +01:00
|
|
|
except Exception as e:
|
2010-01-28 17:39:10 +01:00
|
|
|
def show_error(e):
|
|
|
|
message = str(e)
|
|
|
|
if not message:
|
|
|
|
message = e.__class__.__name__
|
2018-05-08 10:43:56 +02:00
|
|
|
self.show_message(message,
|
|
|
|
_('Error while uploading'),
|
2010-01-28 17:39:10 +01:00
|
|
|
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-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-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)
|
2016-11-20 14:38:48 +01:00
|
|
|
if role == TreeViewHelper.ROLE_EPISODES and event.button == 1:
|
2011-09-19 10:47:20 +02:00
|
|
|
# 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:
|
2016-09-25 14:31:58 +02:00
|
|
|
GObject.source_remove(self._podcast_list_search_timeout)
|
|
|
|
self._podcast_list_search_timeout = GObject.timeout_add(
|
2012-02-27 14:53:27 +01:00
|
|
|
self.config.ui.gtk.live_search_delay,
|
2010-11-22 14:44:12 +01:00
|
|
|
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):
|
2016-09-25 14:31:58 +02:00
|
|
|
if event.keyval == Gdk.KEY_Escape:
|
2009-11-12 03:22:14 +01:00
|
|
|
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:
|
2016-09-25 14:31:58 +02:00
|
|
|
GObject.source_remove(self._podcast_list_search_timeout)
|
2010-11-22 14:44:12 +01:00
|
|
|
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
|
2016-09-25 14:31:58 +02:00
|
|
|
column = Gtk.TreeViewColumn('')
|
|
|
|
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
|
|
|
|
2016-09-25 14:31:58 +02:00
|
|
|
namecell = Gtk.CellRendererText()
|
|
|
|
namecell.set_property('ellipsize', Pango.EllipsizeMode.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
|
|
|
|
2016-09-21 12:45:03 +02:00
|
|
|
iconcell = Gtk.CellRendererPixbuf()
|
2011-07-16 18:35:14 +02:00
|
|
|
iconcell.set_property('xalign', 1.0)
|
2011-07-27 16:26:20 +02:00
|
|
|
column.pack_start(iconcell, False)
|
2016-09-21 12:45:03 +02:00
|
|
|
column.add_attribute(iconcell, 'pixbuf', PodcastListModel.C_PILL)
|
2011-07-27 16:26:20 +02:00
|
|
|
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()
|
2018-02-06 18:33:52 +01:00
|
|
|
|
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 != '-')
|
2018-02-17 10:14:48 +01:00
|
|
|
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):
|
2016-09-25 14:31:58 +02:00
|
|
|
if event.keyval == Gdk.KEY_Right:
|
2011-04-11 14:22:31 +02:00
|
|
|
self.treeAvailable.grab_focus()
|
2016-09-25 14:31:58 +02:00
|
|
|
elif event.keyval in (Gdk.KEY_Up, Gdk.KEY_Down):
|
2011-07-27 16:26:20 +02:00
|
|
|
# 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()
|
|
|
|
|
2016-09-25 14:31:58 +02:00
|
|
|
if event.keyval == Gdk.KEY_Up:
|
2011-07-27 16:26:20 +02:00
|
|
|
step = -1
|
|
|
|
else:
|
|
|
|
step = 1
|
|
|
|
|
|
|
|
path = model.get_path(it)
|
|
|
|
while True:
|
2018-03-23 20:53:31 +01:00
|
|
|
path = (path[0] + step,)
|
2011-07-27 16:26:20 +02:00
|
|
|
|
|
|
|
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)
|
2016-09-25 14:31:58 +02:00
|
|
|
elif event.keyval == Gdk.KEY_Escape:
|
2009-11-12 03:22:14 +01:00
|
|
|
self.hide_podcast_search()
|
2016-09-25 14:31:58 +02:00
|
|
|
elif event.get_state() & Gdk.ModifierType.CONTROL_MASK:
|
2009-11-25 10:14:57 +01:00
|
|
|
# Don't handle type-ahead when control is pressed (so shortcuts
|
|
|
|
# with the Ctrl key still work, e.g. Ctrl+A, ...)
|
|
|
|
return True
|
2017-03-26 22:14:10 +02:00
|
|
|
elif event.keyval == Gdk.KEY_Delete:
|
|
|
|
return False
|
2009-11-12 03:22:14 +01:00
|
|
|
else:
|
2016-09-25 14:31:58 +02:00
|
|
|
unicode_char_id = Gdk.keyval_to_unicode(event.keyval)
|
2017-03-26 22:14:10 +02:00
|
|
|
# < 32 to intercept Delete and Tab events
|
|
|
|
if unicode_char_id < 32:
|
2009-11-12 03:22:14 +01:00
|
|
|
return False
|
2016-11-21 23:13:46 +01:00
|
|
|
input_char = chr(unicode_char_id)
|
2009-11-12 03:22:14 +01:00
|
|
|
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:
|
2016-09-25 14:31:58 +02:00
|
|
|
GObject.source_remove(self._episode_list_search_timeout)
|
|
|
|
self._episode_list_search_timeout = GObject.timeout_add(
|
2012-02-27 14:53:27 +01:00
|
|
|
self.config.ui.gtk.live_search_delay,
|
2010-11-22 14:44:12 +01:00
|
|
|
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):
|
2016-09-25 14:31:58 +02:00
|
|
|
if event.keyval == Gdk.KEY_Escape:
|
2009-11-05 11:00:15 +01:00
|
|
|
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:
|
2016-09-25 14:31:58 +02:00
|
|
|
GObject.source_remove(self._episode_list_search_timeout)
|
2010-11-22 14:44:12 +01:00
|
|
|
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)
|
2016-09-30 14:27:50 +02:00
|
|
|
self.view_column_actions[index].set_state(GLib.Variant.new_boolean(visible))
|
2011-04-11 13:09:50 +02:00
|
|
|
self.treeAvailable.columns_autosize()
|
|
|
|
|
|
|
|
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:
|
2016-09-25 14:31:58 +02:00
|
|
|
self.episode_columns_menu.popup(None, None, None, None, event.button, event.time)
|
2011-04-11 13:09:50 +02:00
|
|
|
|
2011-04-11 13:11:58 +02:00
|
|
|
return False
|
|
|
|
|
2009-09-05 00:12:53 +02:00
|
|
|
def init_episode_list_treeview(self):
|
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
|
|
|
|
2016-09-25 14:31:58 +02:00
|
|
|
iconcell = Gtk.CellRendererPixbuf()
|
|
|
|
episode_list_icon_size = Gtk.icon_size_register('episode-list',
|
2012-03-03 20:02:58 +01:00
|
|
|
EPISODE_LIST_ICON_SIZE, EPISODE_LIST_ICON_SIZE)
|
|
|
|
iconcell.set_property('stock-size', episode_list_icon_size)
|
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
|
|
|
|
2016-09-25 14:31:58 +02:00
|
|
|
namecell = Gtk.CellRendererText()
|
|
|
|
namecell.set_property('ellipsize', Pango.EllipsizeMode.END)
|
|
|
|
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)
|
2016-09-25 14:31:58 +02:00
|
|
|
namecolumn.set_sizing(Gtk.TreeViewColumnSizing.AUTOSIZE)
|
2011-07-16 18:35:14 +02:00
|
|
|
namecolumn.set_resizable(True)
|
2008-02-06 10:29:56 +01:00
|
|
|
namecolumn.set_expand(True)
|
2005-11-22 14:30:28 +01:00
|
|
|
|
2016-09-25 14:31:58 +02:00
|
|
|
lockcell = Gtk.CellRendererPixbuf()
|
2011-07-16 18:35:14 +02:00
|
|
|
lockcell.set_fixed_size(40, -1)
|
2016-09-25 14:31:58 +02:00
|
|
|
lockcell.set_property('stock-size', Gtk.IconSize.MENU)
|
2011-07-16 18:35:14 +02:00
|
|
|
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
|
|
|
|
2016-09-25 14:31:58 +02:00
|
|
|
sizecell = Gtk.CellRendererText()
|
2010-11-19 15:36:39 +01:00
|
|
|
sizecell.set_property('xalign', 1)
|
2016-09-25 14:31:58 +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
|
|
|
|
2016-09-25 14:31:58 +02:00
|
|
|
timecell = Gtk.CellRendererText()
|
2011-04-11 13:09:50 +02:00
|
|
|
timecell.set_property('xalign', 1)
|
2016-09-25 14:31:58 +02:00
|
|
|
timecolumn = Gtk.TreeViewColumn(_('Duration'), timecell, text=EpisodeListModel.C_TIME)
|
2011-04-11 13:09:50 +02:00
|
|
|
timecolumn.set_sort_column_id(EpisodeListModel.C_TOTAL_TIME)
|
|
|
|
|
2016-09-25 14:31:58 +02:00
|
|
|
releasecell = Gtk.CellRendererText()
|
|
|
|
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():
|
2016-09-25 14:31:58 +02:00
|
|
|
label = Gtk.Label(label=column.get_title())
|
2011-07-16 18:38:19 +02:00
|
|
|
label.show_all()
|
|
|
|
column.set_widget(label)
|
|
|
|
|
|
|
|
w = column.get_widget()
|
2016-09-25 14:31:58 +02:00
|
|
|
while w is not None and not isinstance(w, Gtk.Button):
|
2011-07-16 18:38:19 +02:00
|
|
|
w = w.get_parent()
|
|
|
|
|
|
|
|
w.connect('button-release-event', self.on_episode_list_header_clicked)
|
|
|
|
|
|
|
|
# For each column that can be shown/hidden, add a menu item
|
2016-09-30 14:27:50 +02:00
|
|
|
self.view_column_actions = []
|
2011-07-16 18:38:19 +02:00
|
|
|
columns = TreeViewHelper.get_columns(self.treeAvailable)
|
|
|
|
|
2016-09-30 14:27:50 +02:00
|
|
|
def on_visible_toggled(action, param, index):
|
|
|
|
state = action.get_state()
|
|
|
|
self.set_episode_list_column(index, not state)
|
|
|
|
action.set_state(GLib.Variant.new_boolean(not state))
|
|
|
|
|
|
|
|
for index, column in enumerate(columns):
|
2016-10-02 19:34:24 +02:00
|
|
|
name = 'showColumn%i' % index
|
2016-09-30 14:27:50 +02:00
|
|
|
action = Gio.SimpleAction.new_stateful(
|
|
|
|
name, None, GLib.Variant.new_boolean(False))
|
2016-10-02 19:34:24 +02:00
|
|
|
action.connect('activate', on_visible_toggled, index)
|
2016-10-02 19:33:34 +02:00
|
|
|
self.main_window.add_action(action)
|
2016-09-30 14:27:50 +02:00
|
|
|
self.view_column_actions.append(action)
|
2016-10-02 19:34:24 +02:00
|
|
|
self.application.menu_view_columns.insert(index, column.get_title(), 'win.' + name)
|
2016-09-30 14:27:50 +02:00
|
|
|
|
|
|
|
self.episode_columns_menu = Gtk.Menu.new_from_model(self.application.menu_view_columns)
|
|
|
|
self.episode_columns_menu.attach_to_widget(self.main_window)
|
2011-07-16 18:38:19 +02:00
|
|
|
# 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):
|
2016-09-25 14:31:58 +02:00
|
|
|
if event.keyval == Gdk.KEY_Left:
|
2011-04-11 14:22:31 +02:00
|
|
|
self.treeChannels.grab_focus()
|
2016-09-25 14:31:58 +02:00
|
|
|
elif event.keyval == Gdk.KEY_Escape:
|
2012-11-17 16:24:18 +01:00
|
|
|
if self.hbox_search_episodes.get_property('visible'):
|
|
|
|
self.hide_episode_search()
|
|
|
|
else:
|
2014-01-23 22:44:52 +01:00
|
|
|
self.shownotes_object.hide_pane()
|
2016-09-25 14:31:58 +02:00
|
|
|
elif event.get_state() & Gdk.ModifierType.CONTROL_MASK:
|
2009-11-25 10:14:57 +01:00
|
|
|
# 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:
|
2016-09-25 14:31:58 +02:00
|
|
|
unicode_char_id = Gdk.keyval_to_unicode(event.keyval)
|
2017-03-26 22:14:10 +02:00
|
|
|
# < 32 to intercept Delete and Tab events
|
|
|
|
if unicode_char_id < 32:
|
2009-11-12 02:53:59 +01:00
|
|
|
return False
|
2016-11-21 23:13:46 +01:00
|
|
|
input_char = chr(unicode_char_id)
|
2009-11-12 02:53:59 +01:00
|
|
|
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)
|
|
|
|
|
2018-05-08 10:43:56 +02:00
|
|
|
self.treeAvailable.enable_model_drag_source(Gdk.ModifierType.BUTTON1_MASK,
|
2016-09-25 14:31:58 +02:00
|
|
|
(('text/uri-list', 0, 0),), Gdk.DragAction.COPY)
|
2018-02-06 18:33:52 +01:00
|
|
|
|
2011-07-16 18:38:19 +02:00
|
|
|
def drag_data_get(tree, context, selection_data, info, timestamp):
|
2018-05-08 10:43:56 +02:00
|
|
|
uris = ['file://' + e.local_filename(create=False)
|
|
|
|
for e in self.get_selected_episodes()
|
2011-07-16 18:38:19 +02:00
|
|
|
if e.was_downloaded(and_exists=True)]
|
2018-05-10 04:31:09 +02:00
|
|
|
selection_data.set_uris(uris)
|
2011-07-16 18:38:19 +02:00
|
|
|
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()
|
2016-09-25 14:31:58 +02:00
|
|
|
selection.set_mode(Gtk.SelectionMode.MULTIPLE)
|
2018-07-21 21:34:24 +02:00
|
|
|
self.selection_handler_id = selection.connect('changed', self.on_episode_list_selection_changed)
|
2012-07-12 21:14:53 +02:00
|
|
|
|
|
|
|
def on_episode_list_selection_changed(self, selection):
|
|
|
|
# Update the toolbar buttons
|
|
|
|
self.play_or_download()
|
2014-01-23 22:44:52 +01:00
|
|
|
# and the shownotes
|
|
|
|
self.shownotes_object.set_episodes(self.get_selected_episodes())
|
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
|
2016-09-25 14:31:58 +02:00
|
|
|
self.treeDownloads.get_selection().set_mode(Gtk.SelectionMode.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
|
2016-09-25 14:31:58 +02:00
|
|
|
column = Gtk.TreeViewColumn(_('Episode'))
|
2009-04-01 01:12:17 +02:00
|
|
|
|
2016-09-25 14:31:58 +02:00
|
|
|
cell = Gtk.CellRendererPixbuf()
|
|
|
|
cell.set_property('stock-size', Gtk.IconSize.BUTTON)
|
|
|
|
column.pack_start(cell, False)
|
2018-05-08 10:43:56 +02: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
|
|
|
|
2016-09-25 14:31:58 +02:00
|
|
|
cell = Gtk.CellRendererText()
|
|
|
|
cell.set_property('ellipsize', Pango.EllipsizeMode.END)
|
|
|
|
column.pack_start(cell, True)
|
2009-09-22 18:53:14 +02:00
|
|
|
column.add_attribute(cell, 'markup', DownloadStatusModel.C_NAME)
|
2016-09-25 14:31:58 +02:00
|
|
|
column.set_sizing(Gtk.TreeViewColumnSizing.AUTOSIZE)
|
2009-04-01 01:12:17 +02:00
|
|
|
column.set_expand(True)
|
|
|
|
self.treeDownloads.append_column(column)
|
|
|
|
|
|
|
|
# Second column: Progress
|
2016-09-25 14:31:58 +02:00
|
|
|
cell = Gtk.CellRendererProgress()
|
2009-09-22 18:53:14 +02:00
|
|
|
cell.set_property('yalign', .5)
|
|
|
|
cell.set_property('ypad', 6)
|
2016-09-25 14:31:58 +02:00
|
|
|
column = Gtk.TreeViewColumn(_('Progress'), cell,
|
2018-05-08 10:43:56 +02:00
|
|
|
value=DownloadStatusModel.C_PROGRESS,
|
2009-08-24 16:17:32 +02:00
|
|
|
text=DownloadStatusModel.C_PROGRESS_TEXT)
|
2016-09-25 14:31:58 +02:00
|
|
|
column.set_sizing(Gtk.TreeViewColumnSizing.AUTOSIZE)
|
2009-09-22 18:53:14 +02:00
|
|
|
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)
|
|
|
|
|
2017-04-23 17:37:13 +02:00
|
|
|
def on_treeview_expose_event(self, treeview, ctx):
|
|
|
|
model = treeview.get_model()
|
|
|
|
if (model is not None and model.get_iter_first() is not None):
|
|
|
|
return False
|
2010-08-20 15:10:32 +02:00
|
|
|
|
2017-04-23 17:37:13 +02:00
|
|
|
role = getattr(treeview, TreeViewHelper.ROLE, None)
|
|
|
|
if role is None:
|
|
|
|
return False
|
2009-09-01 23:38:21 +02:00
|
|
|
|
2017-04-23 17:37:13 +02:00
|
|
|
width = treeview.get_allocated_width()
|
2018-04-17 14:39:05 +02:00
|
|
|
height = treeview.get_allocated_height()
|
2009-09-05 00:12:53 +02:00
|
|
|
|
2017-04-23 17:37:13 +02:00
|
|
|
if role == TreeViewHelper.ROLE_EPISODES:
|
|
|
|
if self.config.episode_list_view_mode != EpisodeListModel.VIEW_ALL:
|
|
|
|
text = _('No episodes in current view')
|
2009-09-01 23:38:21 +02:00
|
|
|
else:
|
2017-04-23 17:37:13 +02:00
|
|
|
text = _('No episodes available')
|
|
|
|
elif role == TreeViewHelper.ROLE_PODCASTS:
|
|
|
|
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')
|
|
|
|
elif role == TreeViewHelper.ROLE_DOWNLOADS:
|
|
|
|
text = _('No active tasks')
|
|
|
|
else:
|
|
|
|
raise Exception('on_treeview_expose_event: unknown role')
|
2009-09-01 23:38:21 +02:00
|
|
|
|
2017-04-23 17:37:13 +02:00
|
|
|
draw_text_box_centered(ctx, treeview, width, height, text, None, None)
|
|
|
|
return True
|
2009-09-01 23:38:21 +02:00
|
|
|
|
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()
|
2016-09-25 14:31:58 +02:00
|
|
|
GObject.timeout_add(1500, self.update_downloads_list)
|
2009-08-17 21:46:17 +02:00
|
|
|
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
|
|
|
|
2017-04-18 16:19:10 +02:00
|
|
|
all_tasks = [(Gtk.TreeRowReference.new(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)
|
2016-11-21 23:13:46 +01:00
|
|
|
except KeyError as 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)
|
|
|
|
|
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)
|
|
|
|
|
2012-02-07 00:32:52 +01:00
|
|
|
def set_download_progress(self, progress):
|
2012-02-24 00:12:00 +01:00
|
|
|
gpodder.user_extensions.on_download_progress(progress)
|
2012-02-07 00:32:52 +01:00
|
|
|
|
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
|
|
|
|
2012-07-02 05:53:33 +02:00
|
|
|
downloading, synchronizing, failed, finished, queued, paused, others = 0, 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-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]
|
2012-07-07 23:41:56 +02:00
|
|
|
speed, size, status, progress, activity = task.speed, task.total_size, task.status, task.progress, task.activity
|
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
|
2018-03-23 20:53:31 +01:00
|
|
|
done_size += size * progress
|
2009-04-01 01:12:17 +02:00
|
|
|
|
2009-06-05 11:46:20 +02:00
|
|
|
download_tasks_seen.add(task)
|
2009-04-01 01:12:17 +02:00
|
|
|
|
2012-07-09 21:08:40 +02:00
|
|
|
if (status == download.DownloadTask.DOWNLOADING and
|
|
|
|
activity == download.DownloadTask.ACTIVITY_DOWNLOAD):
|
2009-06-05 11:46:20 +02:00
|
|
|
downloading += 1
|
|
|
|
total_speed += speed
|
2012-07-09 21:08:40 +02:00
|
|
|
elif (status == download.DownloadTask.DOWNLOADING and
|
|
|
|
activity == download.DownloadTask.ACTIVITY_SYNCHRONIZE):
|
|
|
|
synchronizing += 1
|
2009-06-05 11:46:20 +02:00
|
|
|
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
|
|
|
|
|
2012-07-07 23:41:56 +02:00
|
|
|
text = [_('Progress')]
|
2012-07-02 05:53:33 +02:00
|
|
|
if downloading + failed + queued + synchronizing > 0:
|
2011-07-16 18:38:19 +02:00
|
|
|
s = []
|
|
|
|
if downloading > 0:
|
2018-03-23 20:53:31 +01:00
|
|
|
s.append(N_('%(count)d active', '%(count)d active', downloading) % {'count': downloading})
|
2012-07-02 05:53:33 +02:00
|
|
|
if synchronizing > 0:
|
2018-03-23 20:53:31 +01:00
|
|
|
s.append(N_('%(count)d active', '%(count)d active', synchronizing) % {'count': synchronizing})
|
2011-07-16 18:38:19 +02:00
|
|
|
if failed > 0:
|
2018-03-23 20:53:31 +01:00
|
|
|
s.append(N_('%(count)d failed', '%(count)d failed', failed) % {'count': failed})
|
2011-07-16 18:38:19 +02:00
|
|
|
if queued > 0:
|
2018-03-23 20:53:31 +01:00
|
|
|
s.append(N_('%(count)d queued', '%(count)d queued', queued) % {'count': queued})
|
|
|
|
text.append(' (' + ', '.join(s) + ')')
|
2016-11-21 23:13:46 +01:00
|
|
|
self.labelDownloads.set_text(''.join(text))
|
2009-06-05 11:46:20 +02:00
|
|
|
|
|
|
|
title = [self.default_title]
|
|
|
|
|
2012-02-05 23:42:23 +01:00
|
|
|
# Accessing task.status_changed has the side effect of re-setting
|
|
|
|
# the changed flag, but we only do it once here so that's okay
|
|
|
|
channel_urls = [task.podcast_url for task in
|
2009-06-05 11:46:20 +02:00
|
|
|
self.download_tasks_seen if task.status_changed]
|
2012-02-05 23:42:23 +01:00
|
|
|
episode_urls = [task.url for task in self.download_tasks_seen]
|
2009-06-05 11:46:20 +02:00
|
|
|
|
2012-07-02 05:53:33 +02:00
|
|
|
if downloading > 0:
|
2018-05-29 22:54:05 +02:00
|
|
|
title.append(N_('downloading %(count)d file',
|
|
|
|
'downloading %(count)d files',
|
|
|
|
downloading) % {'count': downloading})
|
2009-06-05 11:46:20 +02:00
|
|
|
|
|
|
|
if total_size > 0:
|
2018-03-23 20:53:31 +01:00
|
|
|
percentage = 100.0 * done_size / total_size
|
2009-06-05 11:46:20 +02:00
|
|
|
else:
|
|
|
|
percentage = 0.0
|
2018-03-23 20:53:31 +01:00
|
|
|
self.set_download_progress(percentage / 100)
|
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)
|
2012-07-02 05:53:33 +02:00
|
|
|
if synchronizing > 0:
|
2018-05-29 22:54:05 +02:00
|
|
|
title.append(N_('synchronizing %(count)d file',
|
|
|
|
'synchronizing %(count)d files',
|
|
|
|
synchronizing) % {'count': synchronizing})
|
2012-07-02 05:53:33 +02:00
|
|
|
if queued > 0:
|
2018-05-29 22:54:05 +02:00
|
|
|
title.append(N_('%(queued)d task queued',
|
|
|
|
'%(queued)d tasks queued',
|
|
|
|
queued) % {'queued': queued})
|
2018-04-17 14:39:05 +02:00
|
|
|
if (downloading + synchronizing + queued) == 0:
|
2012-02-07 00:32:52 +01:00
|
|
|
self.set_download_progress(1.)
|
2011-07-16 18:38:19 +02:00
|
|
|
self.downloads_finished(self.download_tasks_seen)
|
2012-02-04 21:43:37 +01:00
|
|
|
gpodder.user_extensions.on_all_episodes_downloaded()
|
2012-07-02 05:53:33 +02:00
|
|
|
logger.info('All tasks have finished.')
|
2009-12-17 14:50:39 +01:00
|
|
|
|
2010-06-05 01:42:32 +02:00
|
|
|
# Remove finished episodes
|
2018-11-10 16:52:12 +01:00
|
|
|
if self.config.ui.gtk.download_list.remove_finished and can_call_cleanup:
|
2010-06-05 01:42:32 +02:00
|
|
|
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)
|
|
|
|
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
|
2016-11-21 23:13:46 +01:00
|
|
|
except Exception as e:
|
2011-07-15 16:32:06 +02:00
|
|
|
logger.error('Exception happened while updating download list.', exc_info=True)
|
2018-09-07 06:52:32 +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):
|
2012-01-03 23:59:19 +01:00
|
|
|
if name == 'ui.gtk.toolbar':
|
2009-09-05 15:42:55 +02:00
|
|
|
self.toolbar.set_property('visible', new_value)
|
2012-01-03 23:59:19 +01:00
|
|
|
elif name == 'ui.gtk.episode_list.descriptions':
|
2009-09-04 03:11:01 +02:00
|
|
|
self.update_episode_list_model()
|
2012-01-03 23:59:19 +01:00
|
|
|
elif name in ('auto.update.enabled', 'auto.update.frequency'):
|
2009-10-13 22:48:12 +02:00
|
|
|
self.restart_auto_update_timer()
|
2012-01-03 23:59:19 +01:00
|
|
|
elif name in ('ui.gtk.podcast_list.all_episodes',
|
|
|
|
'ui.gtk.podcast_list.sections'):
|
2009-12-16 14:55:55 +01:00
|
|
|
# Force a update of the podcast list model
|
|
|
|
self.update_podcast_list_model()
|
2012-01-03 23:59:19 +01:00
|
|
|
elif name == 'ui.gtk.episode_list.columns':
|
2011-04-11 13:09:50 +02:00
|
|
|
self.update_episode_list_columns_visibility()
|
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
|
2018-05-27 20:09:40 +02:00
|
|
|
(path, column, rx, ry) = treeview.get_path_at_pos(x, y) or (None,) * 4
|
2008-06-15 14:28:24 +02:00
|
|
|
|
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)
|
2011-12-05 16:54:56 +01:00
|
|
|
if channel is None or not hasattr(channel, 'title'):
|
2009-12-16 14:55:55 +01:00
|
|
|
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
|
2016-09-25 14:31:58 +02:00
|
|
|
table = Gtk.Table(rows=3, columns=3)
|
2009-09-05 00:12:53 +02:00
|
|
|
table.set_row_spacings(5)
|
|
|
|
table.set_col_spacings(5)
|
|
|
|
table.set_border_width(5)
|
|
|
|
|
2016-09-25 14:31:58 +02:00
|
|
|
heading = Gtk.Label()
|
2009-09-05 00:12:53 +02:00
|
|
|
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)
|
|
|
|
|
2016-09-25 14:31:58 +02:00
|
|
|
table.attach(Gtk.HSeparator(), 0, 3, 1, 2)
|
2009-09-05 00:12:53 +02:00
|
|
|
|
|
|
|
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:
|
2018-03-23 20:53:31 +01:00
|
|
|
description = channel.description[:498] + '[...]'
|
2009-09-05 00:12:53 +02:00
|
|
|
else:
|
|
|
|
description = channel.description[:pos]
|
2008-06-05 18:17:09 +02:00
|
|
|
|
2016-09-25 14:31:58 +02:00
|
|
|
description = Gtk.Label(label=description)
|
2009-09-05 00:12:53 +02:00
|
|
|
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)
|
2018-03-23 20:53:31 +01:00
|
|
|
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
|
|
|
|
2018-05-08 10:43:56 +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
|
|
|
|
2018-05-08 11:41:52 +02:00
|
|
|
if (path is not None and not paths and
|
|
|
|
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)
|
2018-11-10 16:53:59 +01:00
|
|
|
if not treeview.is_rubber_banding_active():
|
2009-09-05 00:12:53 +02:00
|
|
|
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()
|
|
|
|
|
2018-03-23 20:53:31 +01:00
|
|
|
can_queue, can_cancel, can_pause, can_remove, can_force = (True,) * 5
|
2018-05-08 10:43:56 +02:00
|
|
|
selected_tasks = [(Gtk.TreeRowReference.new(model, path),
|
|
|
|
model.get_value(model.get_iter(path),
|
2009-09-22 19:38:27 +02:00
|
|
|
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
|
2018-05-08 10:43:56 +02:00
|
|
|
if task.status not in (download.DownloadTask.PAUSED,
|
|
|
|
download.DownloadTask.FAILED,
|
2009-09-22 19:38:27 +02:00
|
|
|
download.DownloadTask.CANCELLED):
|
|
|
|
can_queue = False
|
2018-05-08 10:43:56 +02:00
|
|
|
if task.status not in (download.DownloadTask.PAUSED,
|
|
|
|
download.DownloadTask.QUEUED,
|
|
|
|
download.DownloadTask.DOWNLOADING,
|
2010-09-29 20:11:37 +02:00
|
|
|
download.DownloadTask.FAILED):
|
2009-09-22 19:38:27 +02:00
|
|
|
can_cancel = False
|
2018-05-08 10:43:56 +02:00
|
|
|
if task.status not in (download.DownloadTask.QUEUED,
|
2009-09-22 19:38:27 +02:00
|
|
|
download.DownloadTask.DOWNLOADING):
|
|
|
|
can_pause = False
|
2018-05-08 10:43:56 +02:00
|
|
|
if task.status not in (download.DownloadTask.CANCELLED,
|
|
|
|
download.DownloadTask.FAILED,
|
2009-09-22 19:38:27 +02:00
|
|
|
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):
|
2012-07-09 21:08:40 +02:00
|
|
|
# Separate tasks into downloads & syncs
|
|
|
|
# Since calling notify_as_finished or notify_as_failed clears the flag,
|
|
|
|
# need to iterate through downloads & syncs separately, else all sync
|
|
|
|
# tasks will have their flags cleared if we do downloads first
|
2012-07-02 05:53:33 +02:00
|
|
|
|
2012-07-09 21:08:40 +02:00
|
|
|
def filter_by_activity(activity, tasks):
|
2016-11-21 23:13:46 +01:00
|
|
|
return [task for task in tasks if task.activity == activity]
|
2012-07-02 05:53:33 +02:00
|
|
|
|
2012-07-09 21:08:40 +02:00
|
|
|
download_tasks = filter_by_activity(download.DownloadTask.ACTIVITY_DOWNLOAD,
|
|
|
|
download_tasks_seen)
|
2012-07-02 05:53:33 +02:00
|
|
|
|
2016-11-21 23:13:46 +01:00
|
|
|
finished_downloads = [str(task)
|
2012-07-09 21:08:40 +02:00
|
|
|
for task in download_tasks if task.notify_as_finished()]
|
2016-11-15 05:48:34 +01:00
|
|
|
failed_downloads = ['%s (%s)' % (task, task.error_message)
|
2012-07-09 21:08:40 +02:00
|
|
|
for task in download_tasks if task.notify_as_failed()]
|
2012-07-02 05:53:33 +02:00
|
|
|
|
2012-07-09 21:08:40 +02:00
|
|
|
sync_tasks = filter_by_activity(download.DownloadTask.ACTIVITY_SYNCHRONIZE,
|
|
|
|
download_tasks_seen)
|
|
|
|
|
|
|
|
finished_syncs = [task for task in sync_tasks if task.notify_as_finished()]
|
|
|
|
failed_syncs = [task for task in sync_tasks if task.notify_as_failed()]
|
|
|
|
|
|
|
|
# Note that 'finished_ / failed_downloads' is a list of strings
|
|
|
|
# Whereas 'finished_ / failed_syncs' is a list of SyncTask objects
|
2009-12-28 20:37:29 +01:00
|
|
|
|
|
|
|
if finished_downloads and failed_downloads:
|
|
|
|
message = self.format_episode_list(finished_downloads, 5)
|
2012-07-09 21:08:40 +02:00
|
|
|
message += '\n\n<i>%s</i>\n' % _('Could not download some episodes:')
|
2009-12-28 20:37:29 +01:00
|
|
|
message += self.format_episode_list(failed_downloads, 5)
|
2016-09-28 11:38:16 +02:00
|
|
|
self.show_message(message, _('Downloads finished'))
|
2009-12-28 20:37:29 +01:00
|
|
|
elif finished_downloads:
|
|
|
|
message = self.format_episode_list(finished_downloads)
|
2016-09-28 11:38:16 +02:00
|
|
|
self.show_message(message, _('Downloads finished'))
|
2009-12-28 20:37:29 +01:00
|
|
|
elif failed_downloads:
|
|
|
|
message = self.format_episode_list(failed_downloads)
|
2016-09-28 11:38:16 +02:00
|
|
|
self.show_message(message, _('Downloads failed'))
|
2009-12-28 20:37:29 +01:00
|
|
|
|
2012-07-02 05:53:33 +02:00
|
|
|
if finished_syncs and failed_syncs:
|
2018-05-29 22:54:05 +02:00
|
|
|
message = self.format_episode_list(list(map((
|
|
|
|
lambda task: str(task)), finished_syncs)), 5)
|
2012-07-09 21:08:40 +02:00
|
|
|
message += '\n\n<i>%s</i>\n' % _('Could not sync some episodes:')
|
2018-05-29 22:54:05 +02:00
|
|
|
message += self.format_episode_list(list(map((
|
|
|
|
lambda task: str(task)), failed_syncs)), 5)
|
2016-09-28 11:38:16 +02:00
|
|
|
self.show_message(message, _('Device synchronization finished'), True)
|
2012-07-02 05:53:33 +02:00
|
|
|
elif finished_syncs:
|
2018-05-29 22:54:05 +02:00
|
|
|
message = self.format_episode_list(list(map((
|
|
|
|
lambda task: str(task)), finished_syncs)))
|
2016-09-28 11:38:16 +02:00
|
|
|
self.show_message(message, _('Device synchronization finished'))
|
2012-07-02 05:53:33 +02:00
|
|
|
elif failed_syncs:
|
2018-05-29 22:54:05 +02:00
|
|
|
message = self.format_episode_list(list(map((
|
|
|
|
lambda task: str(task)), failed_syncs)))
|
2016-09-28 11:38:16 +02:00
|
|
|
self.show_message(message, _('Device synchronization failed'), True)
|
2012-07-02 05:53:33 +02:00
|
|
|
|
2012-07-09 21:08:40 +02:00
|
|
|
# Do post-sync processing if required
|
2013-12-20 02:01:16 +01:00
|
|
|
for task in finished_syncs:
|
2012-07-02 05:53:33 +02:00
|
|
|
if self.config.device_sync.after_sync.mark_episodes_played:
|
|
|
|
logger.info('Marking as played on transfer: %s', task.episode.url)
|
2012-07-09 21:08:40 +02:00
|
|
|
task.episode.mark(is_played=True)
|
|
|
|
|
2012-07-02 05:53:33 +02:00
|
|
|
if self.config.device_sync.after_sync.delete_episodes:
|
|
|
|
logger.info('Removing episode after transfer: %s', task.episode.url)
|
|
|
|
task.episode.delete_from_disk()
|
|
|
|
|
2012-07-09 21:08:40 +02:00
|
|
|
self.sync_ui.device.close()
|
2012-07-02 05:53:33 +02:00
|
|
|
|
2012-07-09 21:08:40 +02:00
|
|
|
# Update icon list to show changes, if any
|
2012-07-02 05:53:33 +02:00
|
|
|
self.update_episode_list_icons(all=True)
|
2016-09-17 18:27:09 +02:00
|
|
|
self.update_podcast_list_model()
|
2012-07-02 05:53:33 +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)]:
|
2013-08-03 11:56:53 +02:00
|
|
|
# Bug 1834: make sure title is a unicode string,
|
|
|
|
# so it may be cut correctly on UTF-8 char boundaries
|
|
|
|
title = util.convert_bytes(title)
|
2009-12-28 20:37:29 +01:00
|
|
|
if len(title) > MAX_TITLE_LENGTH:
|
2018-03-23 20:53:31 +01:00
|
|
|
middle = (MAX_TITLE_LENGTH // 2) - 2
|
2009-12-28 20:37:29 +01:00
|
|
|
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('(...')
|
2018-05-29 22:54:05 +02: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:
|
2016-11-15 04:54:21 +01:00
|
|
|
if force_start:
|
|
|
|
self.download_queue_manager.force_start_task(task)
|
|
|
|
else:
|
|
|
|
self.download_queue_manager.queue_task(task)
|
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)
|
2016-11-21 23:13:46 +01:00
|
|
|
except KeyError as 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:
|
2018-11-10 16:53:59 +01:00
|
|
|
return not treeview.is_rubber_banding_active()
|
2009-09-05 00:12:53 +02:00
|
|
|
|
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
|
|
|
|
2016-11-15 04:54:21 +01:00
|
|
|
def make_menu_item(label, icon_name, tasks=None, status=None, sensitive=True, force_start=False, action=None):
|
2009-04-01 01:12:17 +02:00
|
|
|
# This creates a menu item for selection-wide actions
|
2016-09-20 15:25:39 +02:00
|
|
|
item = Gtk.ImageMenuItem.new_with_mnemonic(label)
|
|
|
|
if icon_name is not None:
|
|
|
|
item.set_image(Gtk.Image.new_from_icon_name(icon_name, Gtk.IconSize.MENU))
|
2016-11-15 04:54:21 +01:00
|
|
|
if action is not None:
|
|
|
|
item.connect('activate', action)
|
|
|
|
else:
|
|
|
|
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
|
|
|
|
2016-11-15 04:54:21 +01:00
|
|
|
def move_selected_items_up(menu_item):
|
|
|
|
selection = self.treeDownloads.get_selection()
|
|
|
|
model, selected_paths = selection.get_selected_rows()
|
|
|
|
for path in selected_paths:
|
2018-03-23 20:53:31 +01:00
|
|
|
index_above = path[0] - 1
|
2016-11-15 04:54:21 +01:00
|
|
|
if index_above < 0:
|
|
|
|
return
|
|
|
|
task = model.get_value(
|
|
|
|
model.get_iter(path),
|
|
|
|
DownloadStatusModel.C_TASK)
|
|
|
|
model.move_before(
|
|
|
|
model.get_iter(path),
|
|
|
|
model.get_iter((index_above,)))
|
|
|
|
|
|
|
|
def move_selected_items_down(menu_item):
|
|
|
|
selection = self.treeDownloads.get_selection()
|
|
|
|
model, selected_paths = selection.get_selected_rows()
|
|
|
|
for path in reversed(selected_paths):
|
2018-03-23 20:53:31 +01:00
|
|
|
index_below = path[0] + 1
|
2016-11-15 04:54:21 +01:00
|
|
|
if index_below >= len(model):
|
|
|
|
return
|
|
|
|
task = model.get_value(
|
|
|
|
model.get_iter(path),
|
|
|
|
DownloadStatusModel.C_TASK)
|
|
|
|
model.move_after(
|
|
|
|
model.get_iter(path),
|
|
|
|
model.get_iter((index_below,)))
|
|
|
|
|
2016-09-25 14:31:58 +02:00
|
|
|
menu = Gtk.Menu()
|
2009-04-01 01:12:17 +02:00
|
|
|
|
2010-03-07 20:15:36 +01:00
|
|
|
if can_force:
|
2018-09-07 06:52:32 +02:00
|
|
|
menu.append(make_menu_item(_('Start download now'), 'document-save',
|
|
|
|
selected_tasks,
|
|
|
|
download.DownloadTask.QUEUED,
|
|
|
|
force_start=True))
|
2010-03-07 20:15:36 +01:00
|
|
|
else:
|
2018-09-07 06:52:32 +02:00
|
|
|
menu.append(make_menu_item(_('Download'), 'document-save',
|
|
|
|
selected_tasks,
|
|
|
|
download.DownloadTask.QUEUED,
|
|
|
|
can_queue))
|
|
|
|
|
|
|
|
menu.append(make_menu_item(_('Cancel'), 'media-playback-stop',
|
|
|
|
selected_tasks,
|
|
|
|
download.DownloadTask.CANCELLED,
|
|
|
|
can_cancel))
|
|
|
|
menu.append(make_menu_item(_('Pause'), 'media-playback-pause',
|
|
|
|
selected_tasks,
|
|
|
|
download.DownloadTask.PAUSED, can_pause))
|
2016-09-25 14:31:58 +02:00
|
|
|
menu.append(Gtk.SeparatorMenuItem())
|
2018-09-07 06:52:32 +02:00
|
|
|
menu.append(make_menu_item(_('Move up'), 'go-up',
|
|
|
|
action=move_selected_items_up))
|
|
|
|
menu.append(make_menu_item(_('Move down'), 'go-down',
|
|
|
|
action=move_selected_items_down))
|
2016-11-15 04:54:21 +01:00
|
|
|
menu.append(Gtk.SeparatorMenuItem())
|
2018-09-07 06:52:32 +02:00
|
|
|
menu.append(make_menu_item(_('Remove from list'), 'list-remove',
|
|
|
|
selected_tasks, sensitive=can_remove))
|
2009-04-01 01:12:17 +02:00
|
|
|
|
2016-11-09 00:11:50 +01:00
|
|
|
menu.attach_to_widget(treeview)
|
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)
|
2016-09-28 11:47:44 +02:00
|
|
|
menu.popup(None, None, func, None, 3, Gtk.get_current_event_time())
|
2011-04-11 14:16:07 +02:00
|
|
|
else:
|
2016-09-25 14:31:58 +02:00
|
|
|
menu.popup(None, None, None, None, event.button, event.time)
|
2009-04-01 01:12:17 +02:00
|
|
|
return True
|
|
|
|
|
2011-12-30 12:09:48 +01:00
|
|
|
def on_mark_episodes_as_old(self, item):
|
|
|
|
assert self.active_channel is not None
|
|
|
|
|
|
|
|
for episode in self.active_channel.get_all_episodes():
|
|
|
|
if not episode.was_downloaded(and_exists=True):
|
|
|
|
episode.mark(is_played=True)
|
|
|
|
|
|
|
|
self.update_podcast_list_model(selected=True)
|
|
|
|
self.update_episode_list_icons(all=True)
|
|
|
|
|
2012-04-01 18:28:28 +02:00
|
|
|
def on_open_download_folder(self, item):
|
|
|
|
assert self.active_channel is not None
|
|
|
|
util.gui_open(self.active_channel.save_dir)
|
|
|
|
|
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:
|
2016-09-25 14:31:58 +02:00
|
|
|
menu = Gtk.Menu()
|
2007-09-15 16:29:37 +02:00
|
|
|
|
2018-05-27 20:09:40 +02:00
|
|
|
item = Gtk.ImageMenuItem(_('Update podcast'))
|
2016-10-02 19:34:24 +02:00
|
|
|
item.set_image(Gtk.Image.new_from_icon_name('view-refresh', Gtk.IconSize.MENU))
|
|
|
|
item.set_action_name('win.updateChannel')
|
2009-07-06 16:14:36 +02:00
|
|
|
menu.append(item)
|
2008-03-10 16:50:12 +01:00
|
|
|
|
2016-09-25 14:31:58 +02:00
|
|
|
menu.append(Gtk.SeparatorMenuItem())
|
2008-11-19 17:05:19 +01:00
|
|
|
|
2016-09-25 14:31:58 +02:00
|
|
|
item = Gtk.MenuItem(_('Open download folder'))
|
2012-04-01 18:28:28 +02:00
|
|
|
item.connect('activate', self.on_open_download_folder)
|
|
|
|
menu.append(item)
|
|
|
|
|
2016-09-25 14:31:58 +02:00
|
|
|
menu.append(Gtk.SeparatorMenuItem())
|
2012-04-01 18:28:28 +02:00
|
|
|
|
2016-09-25 14:31:58 +02:00
|
|
|
item = Gtk.MenuItem(_('Mark episodes as old'))
|
2011-12-30 12:09:48 +01:00
|
|
|
item.connect('activate', self.on_mark_episodes_as_old)
|
|
|
|
menu.append(item)
|
|
|
|
|
2016-09-25 14:31:58 +02: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
|
|
|
|
2017-04-26 01:58:00 +02:00
|
|
|
item = Gtk.ImageMenuItem(_('Delete podcast'))
|
2016-10-02 19:34:24 +02:00
|
|
|
item.set_image(Gtk.Image.new_from_icon_name('edit-delete', Gtk.IconSize.MENU))
|
2018-05-27 20:09:40 +02:00
|
|
|
item.connect('activate', self.on_itemRemoveChannel_activate)
|
|
|
|
menu.append(item)
|
2007-11-02 07:53:30 +01:00
|
|
|
|
2012-06-25 16:07:17 +02:00
|
|
|
result = gpodder.user_extensions.on_channel_context_menu(self.active_channel)
|
|
|
|
if result:
|
2016-09-25 14:31:58 +02:00
|
|
|
menu.append(Gtk.SeparatorMenuItem())
|
2012-06-25 16:07:17 +02:00
|
|
|
for label, callback in result:
|
2016-09-25 14:31:58 +02:00
|
|
|
item = Gtk.MenuItem(label)
|
2012-06-25 16:07:17 +02:00
|
|
|
item.connect('activate', lambda item, callback: callback(self.active_channel), callback)
|
|
|
|
menu.append(item)
|
|
|
|
|
2016-09-25 14:31:58 +02:00
|
|
|
menu.append(Gtk.SeparatorMenuItem())
|
2007-09-15 16:29:37 +02:00
|
|
|
|
2016-09-25 14:31:58 +02:00
|
|
|
item = Gtk.ImageMenuItem(_('Podcast settings'))
|
2016-10-02 19:34:24 +02:00
|
|
|
item.set_image(Gtk.Image.new_from_icon_name('document-properties', Gtk.IconSize.MENU))
|
2016-09-30 14:27:50 +02:00
|
|
|
item.set_action_name('win.editChannel')
|
2010-08-07 15:59:46 +02:00
|
|
|
menu.append(item)
|
2007-09-15 16:29:37 +02:00
|
|
|
|
2016-09-30 14:27:50 +02:00
|
|
|
menu.attach_to_widget(treeview)
|
2007-09-15 16:29:37 +02:00
|
|
|
menu.show_all()
|
2013-02-08 11:08:28 +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.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)
|
2016-09-28 11:47:44 +02:00
|
|
|
menu.popup(None, None, func, None, 3, Gtk.get_current_event_time())
|
2011-04-11 14:16:07 +02:00
|
|
|
else:
|
2016-09-25 14:31:58 +02:00
|
|
|
menu.popup(None, None, None, None, event.button, event.time)
|
2007-09-15 16:29:37 +02:00
|
|
|
|
|
|
|
return True
|
|
|
|
|
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.
|
|
|
|
"""
|
2012-03-05 10:44:19 +01:00
|
|
|
util.idle_add(self.podcast_list_model.add_cover_by_channel,
|
|
|
|
channel, pixbuf)
|
2008-06-14 13:43:53 +02:00
|
|
|
|
2016-12-07 20:51:08 +01:00
|
|
|
@staticmethod
|
|
|
|
def build_filename(filename, extension):
|
2018-07-19 18:44:58 +02:00
|
|
|
filename = util.sanitize_filename(filename, PodcastEpisode.MAX_FILENAME_LENGTH)
|
2016-12-07 20:51:08 +01:00
|
|
|
if not filename.endswith(extension):
|
|
|
|
filename += extension
|
|
|
|
return filename
|
2010-05-03 19:00:29 +02:00
|
|
|
|
2016-12-07 20:51:08 +01:00
|
|
|
def save_episodes_as_file(self, episodes):
|
2018-10-13 18:18:25 +02:00
|
|
|
def do_save_episode(copy_from, copy_to):
|
2018-12-10 17:16:16 +01:00
|
|
|
if os.path.exists(copy_to):
|
2018-12-13 09:59:02 +01:00
|
|
|
logger.warn(copy_from)
|
|
|
|
logger.warn(copy_to)
|
2018-12-10 17:38:32 +01:00
|
|
|
title = _('File already exist')
|
2018-12-15 14:56:51 +01:00
|
|
|
d = {'filename': os.path.basename(copy_to)}
|
|
|
|
message = _('A file named "%(filename)s" already exist. Do you want to replace it?') % d
|
2018-12-10 17:38:32 +01:00
|
|
|
if not self.show_confirmation(message, title):
|
|
|
|
return
|
2018-10-13 18:18:25 +02:00
|
|
|
try:
|
|
|
|
shutil.copyfile(copy_from, copy_to)
|
|
|
|
except (OSError, IOError) as e:
|
|
|
|
logger.warn('Error copying from %s to %s: %r', copy_from, copy_to, e, exc_info=True)
|
|
|
|
folder, filename = os.path.split(copy_to)
|
|
|
|
# Remove characters not supported by VFAT (#282)
|
|
|
|
new_filename = re.sub(r"[\"*/:<>?\\|]", "_", filename)
|
|
|
|
destination = os.path.join(folder, new_filename)
|
|
|
|
if (copy_to != destination):
|
|
|
|
shutil.copyfile(copy_from, destination)
|
|
|
|
else:
|
|
|
|
raise
|
|
|
|
|
2009-09-05 00:53:46 +02:00
|
|
|
PRIVATE_FOLDER_ATTRIBUTE = '_save_episodes_as_file_folder'
|
2016-12-07 20:51:08 +01:00
|
|
|
folder = getattr(self, PRIVATE_FOLDER_ATTRIBUTE, None)
|
2018-10-13 18:18:25 +02:00
|
|
|
allRemainingDefault = False
|
|
|
|
remaining = len(episodes)
|
|
|
|
dialog = gPodderExportToLocalFolder(self.main_window,
|
|
|
|
_config=self.config)
|
|
|
|
for episode in episodes:
|
|
|
|
remaining -= 1
|
|
|
|
if episode.was_downloaded(and_exists=True):
|
|
|
|
copy_from = episode.local_filename(create=False)
|
|
|
|
assert copy_from is not None
|
|
|
|
|
|
|
|
base, extension = os.path.splitext(copy_from)
|
|
|
|
filename = self.build_filename(episode.sync_filename(), extension)
|
2016-12-07 20:51:08 +01:00
|
|
|
|
2018-10-13 18:18:25 +02:00
|
|
|
try:
|
|
|
|
if allRemainingDefault:
|
|
|
|
do_save_episode(copy_from, os.path.join(folder, filename))
|
|
|
|
else:
|
|
|
|
(notCancelled, folder, dest_path, allRemainingDefault) = dialog.save_as(folder, filename, remaining)
|
|
|
|
if notCancelled:
|
|
|
|
do_save_episode(copy_from, dest_path)
|
2017-04-23 17:29:43 +02:00
|
|
|
else:
|
2018-10-13 18:18:25 +02:00
|
|
|
break
|
|
|
|
except (OSError, IOError) as e:
|
|
|
|
if remaining:
|
|
|
|
msg = _('Error saving to local folder: %(error)r.\n'
|
|
|
|
'Would you like to continue?') % dict(error=e)
|
|
|
|
if not self.show_confirmation(msg, _('Error saving to local folder')):
|
|
|
|
logger.warn("Save to Local Folder cancelled following error")
|
|
|
|
break
|
|
|
|
else:
|
|
|
|
self.notification(_('Error saving to local folder: %(error)r') % dict(error=e),
|
|
|
|
_('Error saving to local folder'), important=True)
|
|
|
|
|
|
|
|
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
|
|
|
|
(base, ext) = os.path.splitext(filename)
|
2018-03-25 17:59:26 +02:00
|
|
|
destfile = self.build_filename(episode.sync_filename(), ext)
|
2016-12-07 20:51:08 +01:00
|
|
|
destfile = os.path.join(tempfile.gettempdir(), destfile)
|
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
|
|
|
|
2012-07-10 13:52:34 +02:00
|
|
|
util.run_in_background(lambda: convert_and_send_thread(episodes_to_copy))
|
2008-01-21 10:52:09 +01:00
|
|
|
|
2012-12-22 16:21:19 +01:00
|
|
|
def _add_sub_menu(self, menu, label):
|
2016-09-25 14:31:58 +02:00
|
|
|
root_item = Gtk.MenuItem(label)
|
2012-12-22 16:21:19 +01:00
|
|
|
menu.append(root_item)
|
2016-09-25 14:31:58 +02:00
|
|
|
sub_menu = Gtk.Menu()
|
2012-12-22 16:21:19 +01:00
|
|
|
root_item.set_submenu(sub_menu)
|
|
|
|
return sub_menu
|
|
|
|
|
2013-03-09 11:35:36 +01:00
|
|
|
def _submenu_item_activate_hack(self, item, callback, *args):
|
|
|
|
# See http://stackoverflow.com/questions/5221326/submenu-item-does-not-call-function-with-working-solution
|
|
|
|
# Note that we can't just call the callback on button-press-event, as
|
|
|
|
# it might be blocking (see http://gpodder.org/bug/1778), so we run
|
|
|
|
# this in the GUI thread at a later point in time (util.idle_add).
|
|
|
|
# Also, we also have to connect to the activate signal, as this is the
|
|
|
|
# only signal that is fired when keyboard navigation is used.
|
|
|
|
|
|
|
|
# It can happen that both (button-release-event and activate) signals
|
|
|
|
# are fired, and we must avoid calling the callback twice. We do this
|
|
|
|
# using a semaphore and only acquiring (but never releasing) it, making
|
|
|
|
# sure that the util.idle_add() call below is only ever called once.
|
|
|
|
only_once = threading.Semaphore(1)
|
|
|
|
|
|
|
|
def handle_event(item, event=None):
|
|
|
|
if only_once.acquire(False):
|
|
|
|
util.idle_add(callback, *args)
|
|
|
|
|
|
|
|
item.connect('button-press-event', handle_event)
|
|
|
|
item.connect('activate', handle_event)
|
|
|
|
|
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:
|
2018-11-10 16:53:59 +01:00
|
|
|
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
|
|
|
|
2016-09-25 14:31:58 +02:00
|
|
|
menu = Gtk.Menu()
|
2007-08-26 17:20:46 +02:00
|
|
|
|
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:
|
2016-09-25 14:31:58 +02:00
|
|
|
item = Gtk.ImageMenuItem(Gtk.STOCK_OPEN)
|
2010-05-03 19:00:29 +02:00
|
|
|
elif downloaded:
|
2016-10-02 19:34:24 +02:00
|
|
|
item = Gtk.ImageMenuItem(_('Play'))
|
|
|
|
item.set_image(Gtk.Image.new_from_icon_name('media-playback-start', Gtk.IconSize.MENU))
|
2010-05-03 19:00:29 +02:00
|
|
|
else:
|
2011-07-27 13:37:58 +02:00
|
|
|
if downloading:
|
2016-09-25 14:31:58 +02:00
|
|
|
item = Gtk.ImageMenuItem(_('Preview'))
|
2011-07-27 13:37:58 +02:00
|
|
|
else:
|
2016-09-25 14:31:58 +02:00
|
|
|
item = Gtk.ImageMenuItem(_('Stream'))
|
2016-10-02 19:34:24 +02:00
|
|
|
item.set_image(Gtk.Image.new_from_icon_name('media-playback-start', Gtk.IconSize.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:
|
2016-09-25 14:31:58 +02:00
|
|
|
item = Gtk.ImageMenuItem(_('Download'))
|
2016-11-15 04:54:21 +01:00
|
|
|
item.set_image(Gtk.Image.new_from_icon_name('document-save', Gtk.IconSize.MENU))
|
2016-09-30 14:27:50 +02:00
|
|
|
item.set_action_name('win.download')
|
2011-02-01 18:06:24 +01:00
|
|
|
menu.append(item)
|
2009-04-02 02:22:09 +02:00
|
|
|
else:
|
2016-10-02 19:34:24 +02:00
|
|
|
item = Gtk.ImageMenuItem.new_with_mnemonic(_('_Cancel'))
|
2016-09-30 14:27:50 +02:00
|
|
|
item.set_action_name('win.cancel')
|
2011-02-01 18:06:24 +01:00
|
|
|
menu.append(item)
|
2007-08-26 17:20:46 +02:00
|
|
|
|
2016-10-02 19:34:24 +02:00
|
|
|
item = Gtk.ImageMenuItem.new_with_mnemonic(_('_Delete'))
|
|
|
|
item.set_image(Gtk.Image.new_from_icon_name('edit-delete', Gtk.IconSize.MENU))
|
|
|
|
item.set_action_name('win.delete')
|
2011-02-01 18:06:24 +01:00
|
|
|
menu.append(item)
|
2009-04-01 21:54:17 +02:00
|
|
|
|
2012-02-04 21:43:37 +01:00
|
|
|
result = gpodder.user_extensions.on_episodes_context_menu(episodes)
|
2011-07-08 22:57:20 +02:00
|
|
|
if result:
|
2016-09-25 14:31:58 +02:00
|
|
|
menu.append(Gtk.SeparatorMenuItem())
|
2012-12-22 16:21:19 +01:00
|
|
|
submenus = {}
|
2011-07-08 22:57:20 +02:00
|
|
|
for label, callback in result:
|
2013-02-27 12:15:12 +01:00
|
|
|
key, sep, title = label.rpartition('/')
|
2016-09-25 14:31:58 +02:00
|
|
|
item = Gtk.ImageMenuItem(title)
|
2013-03-09 11:35:36 +01:00
|
|
|
self._submenu_item_activate_hack(item, callback, episodes)
|
2012-12-22 16:21:19 +01:00
|
|
|
if key:
|
|
|
|
if key not in submenus:
|
|
|
|
sub_menu = self._add_sub_menu(menu, key)
|
|
|
|
submenus[key] = sub_menu
|
|
|
|
else:
|
|
|
|
sub_menu = submenus[key]
|
|
|
|
sub_menu.append(item)
|
|
|
|
else:
|
|
|
|
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:
|
2016-09-25 14:31:58 +02:00
|
|
|
menu.append(Gtk.SeparatorMenuItem())
|
2012-12-22 16:21:19 +01:00
|
|
|
share_menu = self._add_sub_menu(menu, _('Send to'))
|
2010-05-03 19:00:29 +02:00
|
|
|
|
2016-09-25 14:31:58 +02:00
|
|
|
item = Gtk.ImageMenuItem(_('Local folder'))
|
2016-10-02 19:34:24 +02:00
|
|
|
item.set_image(Gtk.Image.new_from_icon_name('folder', Gtk.IconSize.MENU))
|
2013-03-09 11:35:36 +01:00
|
|
|
self._submenu_item_activate_hack(item, 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:
|
2016-09-25 14:31:58 +02:00
|
|
|
item = Gtk.ImageMenuItem(_('Bluetooth device'))
|
|
|
|
item.set_image(Gtk.Image.new_from_icon_name('bluetooth', Gtk.IconSize.MENU))
|
2013-03-09 11:35:36 +01:00
|
|
|
self._submenu_item_activate_hack(item, 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
|
|
|
|
2016-09-25 14:31:58 +02:00
|
|
|
menu.append(Gtk.SeparatorMenuItem())
|
2011-07-05 18:32:48 +02:00
|
|
|
|
2016-09-25 14:31:58 +02:00
|
|
|
item = Gtk.CheckMenuItem(_('New'))
|
2011-07-05 18:32:48 +02:00
|
|
|
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:
|
2016-09-25 14:31:58 +02:00
|
|
|
item = Gtk.CheckMenuItem(_('Archive'))
|
2011-07-05 18:32:48 +02:00
|
|
|
item.set_active(any_locked)
|
2018-05-27 20:09:40 +02:00
|
|
|
item.connect('activate',
|
|
|
|
lambda w: self.on_item_toggle_lock_activate(
|
|
|
|
w, False, not any_locked))
|
2011-07-05 18:32:48 +02:00
|
|
|
menu.append(item)
|
2013-02-08 11:08:28 +01:00
|
|
|
|
2016-09-25 14:31:58 +02:00
|
|
|
menu.append(Gtk.SeparatorMenuItem())
|
2009-04-01 21:54:17 +02:00
|
|
|
# Single item, add episode information menu item
|
2016-09-25 14:31:58 +02:00
|
|
|
item = Gtk.ImageMenuItem(_('Episode details'))
|
2018-05-27 20:09:40 +02:00
|
|
|
item.set_image(Gtk.Image.new_from_icon_name('dialog-information',
|
|
|
|
Gtk.IconSize.MENU))
|
2016-10-02 19:34:24 +02:00
|
|
|
item.set_action_name('win.toggleShownotes')
|
2011-02-01 18:06:24 +01:00
|
|
|
menu.append(item)
|
2009-04-01 21:54:17 +02:00
|
|
|
|
2016-09-30 14:27:50 +02:00
|
|
|
menu.attach_to_widget(treeview)
|
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)
|
2016-09-28 11:47:44 +02:00
|
|
|
menu.popup(None, None, func, None, 3, Gtk.get_current_event_time())
|
2011-04-11 14:16:07 +02:00
|
|
|
else:
|
2016-09-25 14:31:58 +02:00
|
|
|
menu.popup(None, 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 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
|
|
|
|
2012-10-01 10:56:26 +02:00
|
|
|
fmt_ids = youtube.get_fmt_ids(self.config.youtube)
|
2014-09-30 13:35:23 +02:00
|
|
|
vimeo_fmt = self.config.vimeo.fileformat
|
2012-09-19 13:43:20 +02:00
|
|
|
|
2011-07-27 13:37:58 +02:00
|
|
|
allow_partial = (player != 'default')
|
2014-09-30 13:35:23 +02:00
|
|
|
filename = episode.get_playback_url(fmt_ids, vimeo_fmt, 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
|
|
|
|
|
2012-09-15 20:22:06 +02:00
|
|
|
# If Panucci is configured, use D-Bus 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
|
2018-05-08 10:43:56 +02:00
|
|
|
for command in util.format_desktop_command('panucci',
|
2010-08-27 14:55:55 +02:00
|
|
|
[filename]):
|
2011-07-15 16:32:06 +02:00
|
|
|
logger.info('Executing: %s', repr(command))
|
2018-05-28 21:13:29 +02:00
|
|
|
util.Popen(command, close_fds=True)
|
2010-08-27 14:55:55 +02:00
|
|
|
|
2018-05-06 22:26:54 +02:00
|
|
|
def on_error(err):
|
|
|
|
return error_handler(filename, err)
|
2010-08-27 14:55:55 +02:00
|
|
|
|
2010-06-13 02:19:38 +02:00
|
|
|
# This method only exists in Panucci > 0.9 ('new Panucci')
|
2018-05-08 10:43:56 +02:00
|
|
|
i.playback_from(filename, resume_position,
|
2010-06-13 02:19:38 +02:00
|
|
|
reply_handler=on_reply, error_handler=on_error)
|
|
|
|
|
2018-05-16 18:17:52 +02:00
|
|
|
continue # This file was handled by the D-Bus call
|
2016-11-21 23:13:46 +01:00
|
|
|
except Exception as 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:
|
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))
|
2018-05-28 21:13:29 +02:00
|
|
|
util.Popen(command, close_fds=True)
|
2009-09-22 16:36:07 +02:00
|
|
|
|
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
|
2013-10-14 20:55:26 +02:00
|
|
|
if self.mygpo_client.can_access_webservice():
|
|
|
|
self.mygpo_client.flush()
|
2010-01-28 23:58:28 +01:00
|
|
|
|
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
|
2018-05-08 10:43:56 +02: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)
|
2016-11-21 23:13:46 +01:00
|
|
|
except Exception as e:
|
2011-07-15 16:32:06 +02:00
|
|
|
logger.error('Error in playback!', exc_info=True)
|
2018-05-08 10:43:56 +02:00
|
|
|
self.show_message(_('Please check your media player settings in the preferences dialog.'),
|
2016-09-28 11:38:16 +02:00
|
|
|
_('Error opening player'))
|
2009-07-13 15:32:46 +02:00
|
|
|
|
2015-11-17 19:37:09 +01:00
|
|
|
self.episode_list_status_changed(episodes)
|
2007-04-03 13:21:12 +02:00
|
|
|
|
2018-11-10 16:55:04 +01:00
|
|
|
def play_or_download(self, current_page=None):
|
|
|
|
if current_page is None:
|
|
|
|
current_page = self.wNotebook.get_current_page()
|
|
|
|
if current_page > 0:
|
|
|
|
print("play_or_download > 0")
|
2011-07-16 18:38:19 +02:00
|
|
|
self.toolCancel.set_sensitive(True)
|
2018-11-10 16:55:04 +01:00
|
|
|
return (False, False, False, False, False)
|
2007-03-31 04:00:30 +02:00
|
|
|
|
2018-05-27 20:09:40 +02:00
|
|
|
(can_play, can_download, can_cancel, can_delete) = (False,) * 4
|
|
|
|
(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()
|
2013-02-08 11:08:28 +01:00
|
|
|
|
2007-08-26 17:20:46 +02:00
|
|
|
for path in paths:
|
2010-05-28 19:43:32 +02:00
|
|
|
try:
|
|
|
|
episode = model.get_value(model.get_iter(path), EpisodeListModel.C_EPISODE)
|
2017-03-27 01:30:26 +02:00
|
|
|
if episode is None:
|
|
|
|
logger.info('Invalid episode at path %s', str(path))
|
|
|
|
continue
|
2016-11-21 23:13:46 +01:00
|
|
|
except TypeError as 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
|
|
|
|
2016-09-28 15:51:24 +02:00
|
|
|
if open_instead_of_play:
|
2018-03-01 22:25:02 +01:00
|
|
|
self.toolPlay.set_stock_id(Gtk.STOCK_OPEN)
|
2016-09-28 15:51:24 +02:00
|
|
|
else:
|
2018-03-01 22:25:02 +01:00
|
|
|
self.toolPlay.set_stock_id(Gtk.STOCK_MEDIA_PLAY)
|
2016-09-28 15:51:24 +02:00
|
|
|
self.toolPlay.set_sensitive(can_play)
|
|
|
|
self.toolDownload.set_sensitive(can_download)
|
|
|
|
self.toolCancel.set_sensitive(can_cancel)
|
|
|
|
|
|
|
|
self.cancel_action.set_enabled(can_cancel)
|
|
|
|
self.download_action.set_enabled(can_download)
|
|
|
|
self.open_action.set_enabled(can_play and open_instead_of_play)
|
|
|
|
self.play_action.set_enabled(can_play and not open_instead_of_play)
|
|
|
|
self.delete_action.set_enabled(can_delete)
|
|
|
|
self.toggle_episode_new_action.set_enabled(can_play)
|
|
|
|
self.toggle_episode_lock_action.set_enabled(can_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
|
|
|
|
2018-05-06 22:26:54 +02:00
|
|
|
def is_section(r):
|
|
|
|
return r[PodcastListModel.C_URL] == '-'
|
|
|
|
|
|
|
|
def is_separator(r):
|
|
|
|
return r[PodcastListModel.C_SEPARATOR]
|
|
|
|
|
2011-09-18 22:39:34 +02:00
|
|
|
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)
|
2018-05-06 22:26:54 +02:00
|
|
|
|
|
|
|
def is_not_podcast(r):
|
|
|
|
return is_section(r) or is_separator(r)
|
|
|
|
|
2016-11-21 23:13:46 +01:00
|
|
|
list_model_length -= len(list(filter(is_not_podcast, self.podcast_list_model)))
|
2011-09-18 22:39:34 +02:00
|
|
|
|
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):
|
2016-11-20 14:38:48 +01:00
|
|
|
self.play_or_download()
|
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:
|
2016-11-20 14:38:48 +01:00
|
|
|
self.treeAvailable.get_selection().unselect_all()
|
|
|
|
self.treeAvailable.scroll_to_point(0, 0)
|
2009-09-04 03:11:01 +02:00
|
|
|
|
2016-11-20 14:38:48 +01:00
|
|
|
descriptions = self.config.episode_list_descriptions
|
2018-07-21 21:34:24 +02:00
|
|
|
with self.treeAvailable.get_selection().handler_block(self.selection_handler_id):
|
|
|
|
# have to block the on_episode_list_selection_changed handler because
|
|
|
|
# when selecting any channel from All Episodes, on_episode_list_selection_changed
|
|
|
|
# is called once per episode (4k time in my case), causing episode shownotes
|
|
|
|
# to be updated as many time, resulting in UI freeze for 10 seconds.
|
|
|
|
self.episode_list_model.replace_from_channel(self.active_channel, descriptions)
|
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
|
|
|
|
2012-11-17 12:53:00 +01:00
|
|
|
def add_podcast_list(self, podcasts, auth_tokens=None):
|
|
|
|
"""Subscribe to a list of podcast given (title, url) pairs
|
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 = {}
|
|
|
|
|
2011-11-12 15:19:12 +01:00
|
|
|
existing_urls = set(podcast.url for podcast in self.channels)
|
|
|
|
|
2012-11-17 12:53:00 +01:00
|
|
|
# For a given URL, the desired title (or None)
|
|
|
|
title_for_url = {}
|
|
|
|
|
2009-09-05 20:40:35 +02:00
|
|
|
# Sort and split the URL list into five buckets
|
|
|
|
queued, failed, existing, worked, authreq = [], [], [], [], []
|
2012-11-17 12:53:00 +01:00
|
|
|
for input_title, input_url in podcasts:
|
2009-08-25 16:19:14 +02:00
|
|
|
url = util.normalize_feed_url(input_url)
|
2015-05-20 21:10:57 +02:00
|
|
|
|
2018-04-10 10:00:28 +02:00
|
|
|
# Check if it's a YouTube channel, user, or playlist and resolves it to its feed if that's the case
|
2018-04-08 23:08:05 +02:00
|
|
|
url = youtube.parse_youtube_url(url)
|
2015-05-20 21:10:57 +02:00
|
|
|
|
2009-08-25 16:19:14 +02:00
|
|
|
if url is None:
|
|
|
|
# Fail this one because the URL is not valid
|
|
|
|
failed.append(input_url)
|
2011-11-12 15:19:12 +01:00
|
|
|
elif url in existing_urls:
|
2009-08-25 16:19:14 +02:00
|
|
|
# A podcast already exists in the list for this URL
|
|
|
|
existing.append(url)
|
2012-11-17 12:53:00 +01:00
|
|
|
# XXX: Should we try to update the title of the existing
|
|
|
|
# subscription from input_title here if it is different?
|
2009-08-25 16:19:14 +02:00
|
|
|
else:
|
|
|
|
# This URL has survived the first round - queue for add
|
2012-11-17 12:53:00 +01:00
|
|
|
title_for_url[url] = input_title
|
2009-08-25 16:19:14 +02:00
|
|
|
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
|
|
|
|
2018-05-08 10:43:56 +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:') \
|
2018-08-25 09:42:10 +02: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
|
2012-01-09 14:41:03 +01:00
|
|
|
retry_podcasts = {}
|
2009-09-12 15:32:10 +02:00
|
|
|
if authreq:
|
|
|
|
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
|
|
|
# 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.') \
|
2018-08-25 09:42: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:') \
|
2018-08-25 09:42:10 +02:00
|
|
|
+ '\n\n' + '\n'.join(
|
|
|
|
cgi.escape('%s: %s' % (
|
|
|
|
url, error_messages.get(url, _('Unknown')))) for url in failed)
|
2009-09-12 15:32:10 +02:00
|
|
|
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
|
|
|
|
2012-01-09 14:41:03 +01:00
|
|
|
# If we have authentication data to retry, do so here
|
|
|
|
if retry_podcasts:
|
2012-11-17 12:53:00 +01:00
|
|
|
podcasts = [(title_for_url.get(url), url)
|
2016-11-21 23:13:46 +01:00
|
|
|
for url in list(retry_podcasts.keys())]
|
2012-11-17 12:53:00 +01:00
|
|
|
self.add_podcast_list(podcasts, retry_podcasts)
|
2012-01-09 14:41:03 +01:00
|
|
|
# This will NOT show new episodes for podcasts that have
|
|
|
|
# been added ("worked"), but it will prevent problems with
|
|
|
|
# multiple dialogs being open at the same time ;)
|
|
|
|
return
|
|
|
|
|
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:
|
2018-05-08 10:43:56 +02:00
|
|
|
episodes = list(Model.sort_episodes_by_pubdate(episodes,
|
2011-07-16 20:51:26 +02:00
|
|
|
reverse=True))
|
2018-05-08 10:43:56 +02:00
|
|
|
self.new_episodes_show(episodes,
|
2011-07-16 20:51:26 +02:00
|
|
|
selected=[e.check_is_new() for e in episodes])
|
2010-08-24 00:24:15 +02:00
|
|
|
|
2012-07-10 13:52:34 +02:00
|
|
|
@util.run_in_background
|
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):
|
2012-11-17 12:53:00 +01:00
|
|
|
title = title_for_url.get(url)
|
2018-03-23 20:53:31 +01:00
|
|
|
progress.on_progress(float(index) / float(length))
|
2012-11-17 12:53:00 +01:00
|
|
|
progress.on_message(title or url)
|
2009-09-12 15:32:10 +02:00
|
|
|
try:
|
|
|
|
# The URL is valid and does not exist already - subscribe!
|
2018-05-08 10:43:56 +02:00
|
|
|
channel = self.model.load_podcast(url=url, create=True,
|
|
|
|
authentication_tokens=auth_tokens.get(url, None),
|
2012-01-03 23:59:19 +01:00
|
|
|
max_episodes=self.config.max_episodes_per_feed)
|
2009-09-12 15:32:10 +02:00
|
|
|
|
|
|
|
try:
|
|
|
|
username, password = util.username_password_from_url(url)
|
2016-11-21 23:13:46 +01:00
|
|
|
except ValueError as ve:
|
2009-09-12 15:32:10 +02:00
|
|
|
username, password = (None, None)
|
|
|
|
|
2012-11-17 12:53:00 +01:00
|
|
|
if title is not None:
|
|
|
|
# Prefer title from subscription source (bug 1711)
|
|
|
|
channel.title = title
|
|
|
|
|
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
|
2012-11-17 12:53:00 +01:00
|
|
|
|
|
|
|
channel.save()
|
2009-09-12 15:32:10 +02:00
|
|
|
|
|
|
|
self._update_cover(channel)
|
2018-12-15 16:10:00 +01:00
|
|
|
except feedcore.AuthenticationRequired as e:
|
|
|
|
# use e.url because there might have been a redirection (#571)
|
|
|
|
if e.url in auth_tokens:
|
2009-09-12 15:32:10 +02:00
|
|
|
# Fail for wrong authentication data
|
2018-12-15 16:10:00 +01:00
|
|
|
error_messages[e.url] = _('Authentication failed')
|
|
|
|
failed.append(e.url)
|
2009-09-12 15:32:10 +02:00
|
|
|
else:
|
|
|
|
# Queue for login dialog later
|
2018-12-15 16:10:00 +01:00
|
|
|
authreq.append(e.url)
|
2009-09-12 15:32:10 +02:00
|
|
|
continue
|
2016-11-21 23:13:46 +01:00
|
|
|
except feedcore.WifiLogin as error:
|
2009-09-12 15:32:10 +02:00
|
|
|
redirections[url] = error.data
|
|
|
|
failed.append(url)
|
|
|
|
error_messages[url] = _('Redirection detected')
|
|
|
|
continue
|
2016-11-21 23:13:46 +01:00
|
|
|
except Exception as 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)
|
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
|
|
|
|
|
2011-10-31 21:01:01 +01:00
|
|
|
def process_received_episode_actions(self):
|
2010-06-04 19:42:34 +02:00
|
|
|
"""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.
|
|
|
|
"""
|
2018-05-08 10:43:56 +02:00
|
|
|
indicator = ProgressIndicator(_('Merging episode actions'),
|
|
|
|
_('Episode actions from gpodder.net are merged.'),
|
2010-06-04 19:42:34 +02:00
|
|
|
False, self.get_dialog_parent())
|
|
|
|
|
2017-12-16 13:36:28 +01:00
|
|
|
Gtk.main_iteration()
|
2010-06-04 19:42:34 +02:00
|
|
|
|
2011-11-02 10:41:38 +01:00
|
|
|
self.mygpo_client.process_episode_actions(self.find_episode)
|
|
|
|
|
2010-06-04 19:42:34 +02:00
|
|
|
indicator.on_finished()
|
|
|
|
self.db.commit()
|
|
|
|
|
2009-08-24 16:47:59 +02:00
|
|
|
def _update_cover(self, channel):
|
2012-03-05 10:44:19 +01:00
|
|
|
if channel is not None:
|
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()
|
2016-09-28 15:51:24 +02:00
|
|
|
self.update_action.set_enabled(True)
|
|
|
|
self.update_channel_action.set_enabled(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):
|
2012-10-13 13:52:14 +02:00
|
|
|
if not util.connection_available():
|
|
|
|
self.show_message(_('Please connect to a network, then try again.'),
|
|
|
|
_('No network connection'), important=True)
|
|
|
|
return
|
|
|
|
|
2010-01-28 17:39:10 +01:00
|
|
|
# Fix URLs if mygpo has rewritten them
|
2011-10-31 21:01:01 +01:00
|
|
|
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
|
|
|
|
2016-09-28 15:51:24 +02:00
|
|
|
self.update_action.set_enabled(False)
|
|
|
|
self.update_channel_action.set_enabled(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)
|
2016-10-02 19:34:24 +02:00
|
|
|
self.btnCancelFeedUpdate.set_image(Gtk.Image.new_from_icon_name('process-stop', Gtk.IconSize.BUTTON))
|
2011-07-16 18:35:14 +02:00
|
|
|
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)
|
2018-05-29 22:54:05 +02:00
|
|
|
text = N_('Updating %(count)d feed...', 'Updating %(count)d feeds...',
|
|
|
|
count) % {'count': count}
|
2011-07-16 20:51:26 +02:00
|
|
|
|
2010-09-29 23:51:35 +02:00
|
|
|
self.pbFeedUpdate.set_text(text)
|
|
|
|
self.pbFeedUpdate.set_fraction(0)
|
|
|
|
|
2012-07-10 13:52:34 +02:00
|
|
|
@util.run_in_background
|
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
|
|
|
|
|
2017-04-21 14:37:49 +02:00
|
|
|
def indicate_updating_podcast(channel):
|
2018-03-23 20:53:31 +01:00
|
|
|
d = {'podcast': channel.title, 'position': updated + 1, 'total': count}
|
2017-04-21 14:37:49 +02:00
|
|
|
progression = _('Updating %(podcast)s (%(position)d/%(total)d)') % d
|
2018-08-15 15:56:22 +02:00
|
|
|
logger.info(progression)
|
2017-04-21 14:37:49 +02:00
|
|
|
self.pbFeedUpdate.set_text(progression)
|
|
|
|
|
2011-07-16 20:51:26 +02:00
|
|
|
try:
|
2017-04-21 14:37:49 +02:00
|
|
|
util.idle_add(indicate_updating_podcast, channel)
|
2012-01-03 23:59:19 +01:00
|
|
|
channel.update(max_episodes=self.config.max_episodes_per_feed)
|
2011-07-16 20:51:26 +02:00
|
|
|
self._update_cover(channel)
|
2016-11-21 23:13:46 +01:00
|
|
|
except Exception as e:
|
2011-07-16 20:51:26 +02:00
|
|
|
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()
|
|
|
|
|
2018-03-23 20:53:31 +01:00
|
|
|
self.pbFeedUpdate.set_fraction(float(updated + 1) / float(count))
|
2011-07-16 20:51:26 +02:00
|
|
|
|
|
|
|
util.idle_add(update_progress, channel)
|
|
|
|
|
|
|
|
def update_feed_cache_finish_callback():
|
|
|
|
# Process received episode actions for all updated URLs
|
2011-10-31 21:01:01 +01:00
|
|
|
self.process_received_episode_actions()
|
2011-07-16 20:51:26 +02:00
|
|
|
|
|
|
|
# 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])
|
2014-03-10 01:11:35 +01:00
|
|
|
|
2014-05-17 11:46:09 +02:00
|
|
|
if self.config.downloads.chronological_order:
|
|
|
|
# download older episodes first
|
|
|
|
episodes = list(Model.sort_episodes_by_pubdate(episodes))
|
2011-07-16 20:51:26 +02:00
|
|
|
|
|
|
|
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)
|
2016-09-28 15:51:24 +02:00
|
|
|
self.update_action.set_enabled(True)
|
2016-10-02 19:34:24 +02:00
|
|
|
self.btnCancelFeedUpdate.set_image(Gtk.Image.new_from_icon_name('edit-clear', Gtk.IconSize.BUTTON))
|
2011-07-16 20:51:26 +02:00
|
|
|
else:
|
|
|
|
count = len(episodes)
|
|
|
|
# New episodes are available
|
|
|
|
self.pbFeedUpdate.set_fraction(1.0)
|
2012-01-03 23:59:19 +01:00
|
|
|
|
|
|
|
if self.config.auto_download == 'download':
|
2011-07-16 20:51:26 +02:00
|
|
|
self.download_episode_list(episodes)
|
2018-05-29 22:54:05 +02:00
|
|
|
title = N_('Downloading %(count)d new episode.',
|
|
|
|
'Downloading %(count)d new episodes.',
|
|
|
|
count) % {'count': count}
|
2016-09-28 11:38:16 +02:00
|
|
|
self.show_message(title, _('New episodes available'))
|
2011-07-16 20:51:26 +02:00
|
|
|
elif self.config.auto_download == 'queue':
|
|
|
|
self.download_episode_list_paused(episodes)
|
2018-05-29 22:54:05 +02:00
|
|
|
title = N_(
|
|
|
|
'%(count)d new episode added to download list.',
|
|
|
|
'%(count)d new episodes added to download list.',
|
|
|
|
count) % {'count': count}
|
2016-09-28 11:38:16 +02:00
|
|
|
self.show_message(title, _('New episodes available'))
|
2011-07-16 20:51:26 +02:00
|
|
|
else:
|
2012-01-03 23:59:19 +01:00
|
|
|
if (show_new_episodes_dialog and
|
|
|
|
self.config.auto_download == 'show'):
|
2011-07-16 20:51:26 +02:00
|
|
|
self.new_episodes_show(episodes, notification=True)
|
2018-05-16 18:17:52 +02:00
|
|
|
else: # !show_new_episodes_dialog or auto_download == 'ignore'
|
2018-05-29 22:54:05 +02:00
|
|
|
message = N_('%(count)d new episode available',
|
|
|
|
'%(count)d new episodes available',
|
|
|
|
count) % {'count': count}
|
2011-07-16 20:51:26 +02:00
|
|
|
self.pbFeedUpdate.set_text(message)
|
|
|
|
|
2012-01-03 23:59:19 +01:00
|
|
|
self.show_update_feeds_buttons()
|
|
|
|
|
2011-07-16 20:51:26 +02:00
|
|
|
util.idle_add(update_feed_cache_finish_callback)
|
|
|
|
|
2016-10-01 13:01:46 +02:00
|
|
|
def on_gPodder_delete_event(self, *args):
|
2008-01-09 23:46:17 +01:00
|
|
|
"""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
|
|
|
|
2016-10-01 13:01:46 +02:00
|
|
|
if self.confirm_quit():
|
|
|
|
self.close_gpodder()
|
|
|
|
|
|
|
|
return True
|
|
|
|
|
|
|
|
def confirm_quit(self):
|
|
|
|
"""Called when the GUI wants to close the window
|
|
|
|
Displays a confirmation dialog
|
|
|
|
"""
|
|
|
|
|
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:
|
2016-09-25 14:31:58 +02:00
|
|
|
dialog = Gtk.MessageDialog(self.gPodder, Gtk.DialogFlags.MODAL, Gtk.MessageType.QUESTION, Gtk.ButtonsType.NONE)
|
|
|
|
dialog.add_button(Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL)
|
|
|
|
quit_button = dialog.add_button(Gtk.STOCK_QUIT, Gtk.ResponseType.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)
|
2018-03-18 01:00:02 +01:00
|
|
|
dialog.set_markup('<span weight="bold" size="larger">%s</span>\n\n%s' % (title, message))
|
2008-01-09 23:46:17 +01:00
|
|
|
|
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()
|
|
|
|
|
2016-10-01 13:01:46 +02:00
|
|
|
return result == Gtk.ResponseType.CLOSE
|
2008-01-09 23:46:17 +01:00
|
|
|
else:
|
2016-10-01 13:01:46 +02:00
|
|
|
return True
|
2008-01-09 23:46:17 +01:00
|
|
|
|
|
|
|
def close_gpodder(self):
|
|
|
|
""" clean everything and exit properly
|
|
|
|
"""
|
2016-11-20 14:38:48 +01:00
|
|
|
# Cancel any running background updates of the episode list model
|
|
|
|
self.episode_list_model.background_update = None
|
|
|
|
|
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
|
|
|
|
2016-09-25 14:31:58 +02:00
|
|
|
while Gtk.events_pending():
|
|
|
|
Gtk.main_iteration()
|
2008-11-19 18:55:59 +01:00
|
|
|
|
2011-02-06 19:05:52 +01:00
|
|
|
self.core.shutdown()
|
2018-01-30 14:04:28 +01:00
|
|
|
|
2016-09-28 12:04:21 +02:00
|
|
|
self.application.remove_window(self.gPodder)
|
2005-11-23 20:53:18 +01:00
|
|
|
|
2013-02-08 11:08:28 +01:00
|
|
|
def delete_episode_list(self, episodes, confirm=True, skip_locked=True,
|
|
|
|
callback=None):
|
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')
|
2018-09-15 20:11:18 +02:00
|
|
|
message = _(
|
|
|
|
'The selected episodes are locked. Please unlock the '
|
|
|
|
'episodes that you want to delete before trying '
|
|
|
|
'to delete them.')
|
2010-05-01 14:03:05 +02:00
|
|
|
self.notification(message, title, widget=self.treeAvailable)
|
|
|
|
return False
|
2010-04-07 21:01:53 +02:00
|
|
|
|
|
|
|
count = len(episodes)
|
2018-05-29 22:54:05 +02: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
|
|
|
|
2018-05-08 10:43:56 +02:00
|
|
|
progress = ProgressIndicator(_('Deleting episodes'),
|
|
|
|
_('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()
|
|
|
|
|
2012-07-10 13:52:34 +02:00
|
|
|
@util.run_in_background
|
2009-12-16 21:57:35 +01:00
|
|
|
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):
|
2018-03-23 20:53:31 +01:00
|
|
|
progress.on_progress(idx / 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
|
|
|
|
2010-01-28 23:58:28 +01:00
|
|
|
# Notify the web service about the status update + upload
|
2013-10-14 20:55:26 +02:00
|
|
|
if self.mygpo_client.can_access_webservice():
|
|
|
|
self.mygpo_client.on_delete(episodes_status_update)
|
|
|
|
self.mygpo_client.flush()
|
2010-01-28 23:58:28 +01:00
|
|
|
|
2013-02-08 11:08:28 +01:00
|
|
|
if callback is None:
|
2013-02-01 04:36:21 +01:00
|
|
|
util.idle_add(finish_deletion, episode_urls, channel_urls)
|
|
|
|
else:
|
2013-02-08 11:08:28 +01:00
|
|
|
util.idle_add(callback, episode_urls, channel_urls, progress)
|
2009-12-16 21:57:35 +01:00
|
|
|
|
2009-10-13 14:36:06 +02:00
|
|
|
return True
|
2007-11-08 20:11:57 +01:00
|
|
|
|
2016-09-28 12:04:21 +02:00
|
|
|
def on_itemRemoveOldEpisodes_activate(self, action, param):
|
2010-11-22 14:58:07 +01:00
|
|
|
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(),
|
2018-05-29 22:54:05 +02: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:
|
2012-12-28 18:31:39 +01:00
|
|
|
for episode in channel.get_episodes(gpodder.STATE_DOWNLOADED):
|
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
|
|
|
|
2018-05-21 19:26:01 +02:00
|
|
|
gPodderEpisodeSelector(
|
|
|
|
self.main_window, title=_('Delete episodes'),
|
|
|
|
instructions=instructions,
|
|
|
|
episodes=episodes, selected=selected, columns=columns,
|
|
|
|
stock_ok_button='edit-delete', callback=self.delete_episode_list,
|
|
|
|
selection_buttons=selection_buttons, _config=self.config)
|
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()
|
2013-02-08 11:08:28 +01:00
|
|
|
|
2016-09-28 15:51:24 +02:00
|
|
|
def on_item_toggle_played_activate(self, action, param):
|
2009-08-10 23:14:35 +02:00
|
|
|
for episode in self.get_selected_episodes():
|
2011-02-26 16:32:34 +01:00
|
|
|
episode.mark(is_played=episode.is_new)
|
2009-08-10 23:14:35 +02:00
|
|
|
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
|
|
|
|
2016-09-28 15:51:24 +02:00
|
|
|
def on_itemUpdateChannel_activate(self, *params):
|
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.')
|
2018-05-27 20:09:40 +02:00
|
|
|
self.show_message(message, title, widget=self.treeChannels)
|
2009-09-01 18:56:30 +02:00
|
|
|
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
|
|
|
|
2016-09-28 15:51:24 +02:00
|
|
|
def on_itemUpdate_activate(self, action=None, param=None):
|
2010-01-28 17:39:10 +01:00
|
|
|
# Check if we have outstanding subscribe/unsubscribe actions
|
2011-10-31 21:01:01 +01:00
|
|
|
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:
|
2012-03-08 10:52:21 +01:00
|
|
|
def show_welcome_window():
|
|
|
|
def on_show_example_podcasts(widget):
|
2016-09-25 14:31:58 +02:00
|
|
|
welcome_window.main_window.response(Gtk.ResponseType.CANCEL)
|
2012-03-08 10:52:21 +01:00
|
|
|
self.on_itemImportChannels_activate(None)
|
2005-11-21 19:21:25 +01:00
|
|
|
|
2012-03-08 10:52:21 +01:00
|
|
|
def on_add_podcast_via_url(widget):
|
2016-09-25 14:31:58 +02:00
|
|
|
welcome_window.main_window.response(Gtk.ResponseType.CANCEL)
|
2012-03-08 10:52:21 +01:00
|
|
|
self.on_itemAddChannel_activate(None)
|
2011-10-19 14:47:16 +02:00
|
|
|
|
2012-03-08 10:52:21 +01:00
|
|
|
def on_setup_my_gpodder(widget):
|
2016-09-25 14:31:58 +02:00
|
|
|
welcome_window.main_window.response(Gtk.ResponseType.CANCEL)
|
2012-03-08 10:52:21 +01:00
|
|
|
self.on_download_subscriptions_from_mygpo(None)
|
|
|
|
|
|
|
|
welcome_window = gPodderWelcome(self.main_window,
|
|
|
|
center_on_widget=self.main_window,
|
|
|
|
on_show_example_podcasts=on_show_example_podcasts,
|
|
|
|
on_add_podcast_via_url=on_add_podcast_via_url,
|
|
|
|
on_setup_my_gpodder=on_setup_my_gpodder)
|
|
|
|
|
|
|
|
welcome_window.main_window.run()
|
|
|
|
welcome_window.main_window.destroy()
|
|
|
|
|
|
|
|
util.idle_add(show_welcome_window)
|
2011-10-19 14:47:16 +02:00
|
|
|
|
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
|
|
|
|
|
2014-05-17 11:46:09 +02:00
|
|
|
if self.config.downloads.chronological_order:
|
|
|
|
# Download episodes in chronological order (older episodes first)
|
|
|
|
episodes = list(Model.sort_episodes_by_pubdate(episodes))
|
|
|
|
|
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:
|
2013-10-07 01:49:06 +02:00
|
|
|
if episode.url == task.url:
|
2009-04-01 01:12:17 +02:00
|
|
|
task_exists = True
|
2013-10-07 01:49:06 +02:00
|
|
|
if task.status not in (task.DOWNLOADING, task.QUEUED):
|
2016-11-15 04:54:21 +01:00
|
|
|
if force_start:
|
|
|
|
self.download_queue_manager.force_start_task(task)
|
|
|
|
else:
|
|
|
|
self.download_queue_manager.queue_task(task)
|
2013-10-07 01:49:06 +02:00
|
|
|
enable_update = True
|
|
|
|
continue
|
2009-04-01 01:12:17 +02:00
|
|
|
|
|
|
|
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)
|
2016-11-21 23:13:46 +01:00
|
|
|
except Exception as 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
|
|
|
|
|
2016-11-15 04:54:21 +01:00
|
|
|
# New Task, we must wait on the GTK Loop
|
2009-08-24 16:17:32 +02:00
|
|
|
self.download_status_model.register_task(task)
|
2018-02-06 18:33:52 +01:00
|
|
|
|
2016-11-15 04:54:21 +01:00
|
|
|
def queue_task(task):
|
|
|
|
if add_paused:
|
|
|
|
task.status = task.PAUSED
|
|
|
|
else:
|
|
|
|
self.mygpo_client.on_download([task.episode])
|
|
|
|
if force_start:
|
|
|
|
self.download_queue_manager.force_start_task(task)
|
|
|
|
else:
|
|
|
|
self.download_queue_manager.queue_task(task)
|
|
|
|
# Executes after task has been registered
|
|
|
|
util.idle_add(queue_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
|
2013-10-14 20:55:26 +02:00
|
|
|
if self.mygpo_client.can_access_webservice():
|
|
|
|
self.mygpo_client.flush()
|
2010-01-28 23:58:28 +01:00
|
|
|
|
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
|
2018-03-23 20:53:31 +01:00
|
|
|
selected = [True] * len(episodes)
|
2010-08-24 00:24:15 +02:00
|
|
|
|
2018-05-08 10:43:56 +02:00
|
|
|
self.new_episodes_window = gPodderEpisodeSelector(self.main_window,
|
|
|
|
title=_('New episodes available'),
|
|
|
|
instructions=instructions,
|
|
|
|
episodes=episodes,
|
|
|
|
columns=columns,
|
|
|
|
selected=selected,
|
2018-05-21 19:26:01 +02:00
|
|
|
stock_ok_button='gpodder-download',
|
2018-05-08 10:43:56 +02:00
|
|
|
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,
|
2014-01-23 22:44:52 +01:00
|
|
|
show_notification=False)
|
2008-01-22 10:10:08 +01:00
|
|
|
|
2016-09-28 12:04:21 +02:00
|
|
|
def on_itemDownloadAllNew_activate(self, action, param):
|
2009-08-25 16:19:14 +02:00
|
|
|
if not self.offer_new_episodes():
|
2018-05-08 10:43:56 +02:00
|
|
|
self.show_message(_('Please check for new episodes later.'),
|
2016-09-28 11:38:16 +02:00
|
|
|
_('No new episodes available'))
|
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
|
2016-11-21 23:13:46 +01:00
|
|
|
[e for e in c.get_all_episodes() if e.check_is_new()]]
|
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
|
|
|
|
2016-09-28 15:51:24 +02:00
|
|
|
def on_itemShowToolbar_activate(self, action, param):
|
|
|
|
state = action.get_state()
|
|
|
|
self.config.show_toolbar = not state
|
|
|
|
action.set_state(GLib.Variant.new_boolean(not state))
|
2008-02-06 10:29:56 +01:00
|
|
|
|
2016-09-28 12:04:21 +02:00
|
|
|
def on_itemShowDescription_activate(self, action, param):
|
|
|
|
state = action.get_state()
|
|
|
|
self.config.episode_list_descriptions = not state
|
|
|
|
action.set_state(GLib.Variant.new_boolean(not state))
|
2008-02-06 10:29:56 +01:00
|
|
|
|
2016-09-28 12:04:21 +02:00
|
|
|
def on_item_view_hide_boring_podcasts_toggled(self, action, param):
|
|
|
|
state = action.get_state()
|
|
|
|
self.config.podcast_list_hide_boring = not state
|
|
|
|
action.set_state(GLib.Variant.new_boolean(not state))
|
2017-03-27 22:58:47 +02:00
|
|
|
self.apply_podcast_list_hide_boring()
|
2009-09-01 18:56:30 +02:00
|
|
|
|
2016-09-28 12:04:21 +02:00
|
|
|
def on_item_view_episodes_changed(self, action, param):
|
|
|
|
self.config.episode_list_view_mode = getattr(EpisodeListModel, param.get_string()) or EpisodeListModel.VIEW_ALL
|
|
|
|
action.set_state(param)
|
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)
|
2017-03-27 22:58:47 +02:00
|
|
|
self.apply_podcast_list_hide_boring()
|
2009-09-01 17:22:51 +02:00
|
|
|
|
2017-03-27 22:58:47 +02:00
|
|
|
def apply_podcast_list_hide_boring(self):
|
2011-07-16 18:35:14 +02:00
|
|
|
if self.config.podcast_list_hide_boring:
|
2018-03-01 22:25:02 +01:00
|
|
|
self.podcast_list_model.set_view_mode(self.config.episode_list_view_mode)
|
2017-03-27 22:58:47 +02:00
|
|
|
else:
|
|
|
|
self.podcast_list_model.set_view_mode(-1)
|
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):
|
2017-04-08 16:38:36 +02:00
|
|
|
def after_login():
|
2017-04-09 18:37:15 +02:00
|
|
|
title = _('Subscriptions on %(server)s') \
|
|
|
|
% {'server': self.config.mygpo.server}
|
2017-04-08 16:38:36 +02:00
|
|
|
dir = gPodderPodcastDirectory(self.gPodder,
|
2018-08-25 09:42:10 +02:00
|
|
|
_config=self.config,
|
|
|
|
custom_title=title,
|
|
|
|
add_podcast_list=self.add_podcast_list,
|
|
|
|
hide_url_entry=True)
|
2017-04-08 16:38:36 +02:00
|
|
|
|
|
|
|
url = self.mygpo_client.get_download_user_subscriptions_url()
|
|
|
|
dir.download_opml_file(url)
|
|
|
|
|
2010-05-12 13:38:31 +02:00
|
|
|
title = _('Login to gpodder.net')
|
|
|
|
message = _('Please login to download your subscriptions.')
|
2012-03-08 10:52:21 +01:00
|
|
|
|
|
|
|
def on_register_button_clicked():
|
|
|
|
util.open_website('http://gpodder.net/register/')
|
|
|
|
|
2017-04-08 16:38:36 +02:00
|
|
|
success, (root_url, username, password) = self.show_login_dialog(title, message,
|
|
|
|
self.config.mygpo.server,
|
2012-03-08 10:52:21 +01:00
|
|
|
self.config.mygpo.username, self.config.mygpo.password,
|
2017-04-09 18:21:47 +02:00
|
|
|
register_callback=on_register_button_clicked,
|
|
|
|
ask_server=True)
|
2010-05-12 13:38:31 +02:00
|
|
|
if not success:
|
|
|
|
return
|
|
|
|
|
2017-04-08 16:38:36 +02:00
|
|
|
self.config.mygpo.server = root_url
|
2012-01-03 23:59:19 +01:00
|
|
|
self.config.mygpo.username = username
|
|
|
|
self.config.mygpo.password = password
|
2010-05-12 13:38:31 +02:00
|
|
|
|
2017-04-08 16:38:36 +02:00
|
|
|
util.idle_add(after_login)
|
2010-05-12 13:38:31 +02:00
|
|
|
|
2016-09-28 12:04:21 +02:00
|
|
|
def on_itemAddChannel_activate(self, action=None, param=None):
|
2018-05-08 10:43:56 +02:00
|
|
|
self._add_podcast_dialog = gPodderAddPodcast(self.gPodder,
|
2012-11-17 12:53:00 +01:00
|
|
|
add_podcast_list=self.add_podcast_list)
|
2005-11-21 19:21:25 +01:00
|
|
|
|
2016-09-28 15:51:24 +02:00
|
|
|
def on_itemEditChannel_activate(self, action, param=None):
|
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.')
|
2018-05-27 20:09:40 +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
|
|
|
|
2012-03-05 10:44:19 +01:00
|
|
|
gPodderChannel(self.main_window,
|
|
|
|
channel=self.active_channel,
|
|
|
|
update_podcast_list_model=self.update_podcast_list_model,
|
|
|
|
cover_downloader=self.cover_downloader,
|
|
|
|
sections=set(c.section for c in self.channels),
|
2012-05-24 23:17:55 +02:00
|
|
|
clear_cover_cache=self.podcast_list_model.clear_cover_cache,
|
2016-02-03 20:15:41 +01:00
|
|
|
_config=self.config)
|
2008-03-03 23:09:34 +01:00
|
|
|
|
2016-09-28 12:04:21 +02:00
|
|
|
def on_itemMassUnsubscribe_activate(self, action, param):
|
2009-12-16 23:33:12 +01:00
|
|
|
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
|
2018-05-08 10:43:56 +02:00
|
|
|
gPodderEpisodeSelector(self.main_window,
|
|
|
|
title=_('Delete podcasts'),
|
|
|
|
instructions=_('Select the podcast you want to delete.'),
|
|
|
|
episodes=self.channels,
|
|
|
|
columns=columns,
|
|
|
|
size_attribute=None,
|
|
|
|
stock_ok_button=_('Delete'),
|
|
|
|
callback=self.remove_podcast_list,
|
2009-12-16 23:33:12 +01:00
|
|
|
_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:
|
2017-04-26 01:58:00 +02:00
|
|
|
title = _('Deleting podcast')
|
|
|
|
info = _('Please wait while the podcast is deleted')
|
|
|
|
message = _('This podcast and all its episodes will be PERMANENTLY DELETED.\r\nAre you sure you want to continue?')
|
2009-12-16 23:33:12 +01:00
|
|
|
else:
|
2017-04-26 01:58:00 +02:00
|
|
|
title = _('Deleting podcasts')
|
|
|
|
info = _('Please wait while the podcasts are deleted')
|
|
|
|
message = _('These podcasts and all their episodes will be PERMANENTLY DELETED.\r\nAre you sure you want to continue?')
|
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
|
|
|
|
2012-07-10 13:52:34 +02:00
|
|
|
@util.run_in_background
|
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
|
2018-03-23 20:53:31 +01:00
|
|
|
progress.on_progress(idx / 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
|
|
|
|
|
2018-03-23 20:53:31 +01:00
|
|
|
if position == len(self.channels) - 1:
|
2009-12-16 21:57:35 +01:00
|
|
|
# this is the last podcast, so select the URL
|
|
|
|
# of the item before this one (i.e. the "new last")
|
2018-03-23 20:53:31 +01:00
|
|
|
select_url = self.channels[position - 1].url
|
2009-12-16 21:57:35 +01:00
|
|
|
else:
|
|
|
|
# there is a podcast after the deleted one, so
|
|
|
|
# we simply select the one that comes after it
|
2018-03-23 20:53:31 +01:00
|
|
|
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
|
2012-08-16 11:08:35 +02:00
|
|
|
common.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)
|
|
|
|
|
|
|
|
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.')
|
2018-05-27 20:09:40 +02:00
|
|
|
self.show_message(message, title, widget=self.treeChannels)
|
2009-12-16 23:33:12 +01:00
|
|
|
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):
|
2016-09-25 14:31:58 +02:00
|
|
|
filter = Gtk.FileFilter()
|
2008-05-02 17:36:43 +02:00
|
|
|
filter.add_pattern('*.opml')
|
|
|
|
filter.add_pattern('*.xml')
|
2018-03-23 20:53:31 +01:00
|
|
|
filter.set_name(_('OPML files') + ' (*.opml, *.xml)')
|
2008-05-02 17:36:43 +02:00
|
|
|
return filter
|
|
|
|
|
2016-09-28 12:04:21 +02:00
|
|
|
def on_item_import_from_file_activate(self, action, filename=None):
|
2008-06-14 15:17:55 +02:00
|
|
|
if filename is None:
|
2016-09-25 14:31:58 +02:00
|
|
|
dlg = Gtk.FileChooserDialog(title=_('Import from OPML'),
|
2012-01-24 20:15:36 +01:00
|
|
|
parent=self.main_window,
|
2016-09-25 14:31:58 +02:00
|
|
|
action=Gtk.FileChooserAction.OPEN)
|
|
|
|
dlg.add_button(Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL)
|
|
|
|
dlg.add_button(Gtk.STOCK_OPEN, Gtk.ResponseType.OK)
|
2008-06-14 15:17:55 +02:00
|
|
|
dlg.set_filter(self.get_opml_filter())
|
|
|
|
response = dlg.run()
|
|
|
|
filename = None
|
2016-09-25 14:31:58 +02:00
|
|
|
if response == Gtk.ResponseType.OK:
|
2008-06-14 15:17:55 +02:00
|
|
|
filename = dlg.get_filename()
|
|
|
|
dlg.destroy()
|
|
|
|
|
2008-05-02 17:36:43 +02:00
|
|
|
if filename is not None:
|
2018-05-08 10:43:56 +02:00
|
|
|
dir = gPodderPodcastDirectory(self.gPodder, _config=self.config,
|
|
|
|
custom_title=_('Import podcasts from OPML file'),
|
2012-11-17 12:53:00 +01:00
|
|
|
add_podcast_list=self.add_podcast_list,
|
2009-08-25 16:19:14 +02:00
|
|
|
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')
|
2018-05-27 20:09:40 +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
|
|
|
|
|
2018-05-27 20:09:40 +02:00
|
|
|
dlg = Gtk.FileChooserDialog(title=_('Export to OPML'),
|
|
|
|
parent=self.gPodder,
|
|
|
|
action=Gtk.FileChooserAction.SAVE)
|
2016-09-25 14:31:58 +02:00
|
|
|
dlg.add_button(Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL)
|
|
|
|
dlg.add_button(Gtk.STOCK_SAVE, Gtk.ResponseType.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()
|
2016-09-25 14:31:58 +02:00
|
|
|
if response == Gtk.ResponseType.OK:
|
2007-08-19 16:28:24 +02:00
|
|
|
filename = dlg.get_filename()
|
2008-09-10 16:02:06 +02:00
|
|
|
dlg.destroy()
|
2018-05-27 20:09:40 +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)
|
2018-05-27 20:09:40 +02:00
|
|
|
title = N_('%(count)d subscription exported',
|
|
|
|
'%(count)d subscriptions exported',
|
2018-05-29 22:54:05 +02:00
|
|
|
count) % {'count': count}
|
2018-05-27 20:09:40 +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:
|
2018-05-27 20:09:40 +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):
|
2014-10-22 21:23:06 +02:00
|
|
|
self._podcast_directory = gPodderPodcastDirectory(self.main_window,
|
|
|
|
_config=self.config,
|
2012-11-17 12:53:00 +01:00
|
|
|
add_podcast_list=self.add_podcast_list)
|
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
|
|
|
|
2012-03-03 21:09:05 +01:00
|
|
|
def check_for_updates(self, silent):
|
|
|
|
"""Check for updates and (optionally) show a message
|
|
|
|
|
|
|
|
If silent=False, a message will be shown even if no updates are
|
|
|
|
available (set silent=False when the check is manually triggered).
|
|
|
|
"""
|
2016-05-03 07:45:58 +02:00
|
|
|
try:
|
|
|
|
up_to_date, version, released, days = util.get_update_info()
|
|
|
|
except Exception as e:
|
|
|
|
if silent:
|
|
|
|
logger.warn('Could not check for updates.', exc_info=True)
|
|
|
|
else:
|
|
|
|
title = _('Could not check for updates')
|
|
|
|
message = _('Please try again later.')
|
|
|
|
self.show_message(message, title, important=True)
|
|
|
|
return
|
2012-03-03 21:09:05 +01:00
|
|
|
|
|
|
|
if up_to_date and not silent:
|
|
|
|
title = _('No updates available')
|
|
|
|
message = _('You have the latest version of gPodder.')
|
|
|
|
self.show_message(message, title, important=True)
|
|
|
|
|
|
|
|
if not up_to_date:
|
|
|
|
title = _('New version available')
|
|
|
|
message = '\n'.join([
|
|
|
|
_('Installed version: %s') % gpodder.__version__,
|
|
|
|
_('Newest version: %s') % version,
|
|
|
|
_('Release date: %s') % released,
|
|
|
|
'',
|
|
|
|
_('Download the latest version from gpodder.org?'),
|
|
|
|
])
|
|
|
|
|
|
|
|
if self.show_confirmation(message, title):
|
|
|
|
util.open_website('http://gpodder.org/downloads')
|
|
|
|
|
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:
|
2018-11-10 16:55:04 +01:00
|
|
|
self.play_or_download(current_page=page_num)
|
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):
|
2018-05-27 20:09:40 +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)
|
2016-09-28 15:51:24 +02:00
|
|
|
if getattr(self.active_channel, 'ALL_EPISODES_PROXY', False):
|
2018-03-01 22:25:02 +01:00
|
|
|
self.edit_channel_action.set_enabled(False)
|
2016-09-28 15:51:24 +02:00
|
|
|
else:
|
2018-03-01 22:25:02 +01:00
|
|
|
self.edit_channel_action.set_enabled(True)
|
2006-12-16 18:44:13 +01:00
|
|
|
else:
|
2007-09-19 17:04:42 +02:00
|
|
|
self.active_channel = None
|
2016-09-28 15:51:24 +02:00
|
|
|
self.edit_channel_action.set_enabled(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):
|
2018-05-27 20:09:40 +02:00
|
|
|
self.on_itemEditChannel_activate(widget, args)
|
2006-03-04 21:45:01 +01:00
|
|
|
|
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"""
|
2018-05-08 10:43:56 +02:00
|
|
|
return set(episode.channel.url for episode in
|
2010-02-22 23:22:58 +01:00
|
|
|
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]
|
2017-04-25 02:49:26 +02:00
|
|
|
episodes = [e for e in episodes if e is not None]
|
2009-07-13 14:09:43 +02:00
|
|
|
return episodes
|
2008-12-14 17:30:17 +01:00
|
|
|
|
2016-09-28 15:51:24 +02:00
|
|
|
def on_playback_selected_episodes(self, *params):
|
2009-07-13 15:32:46 +02:00
|
|
|
self.playback_episodes(self.get_selected_episodes())
|
2007-03-18 19:28:17 +01:00
|
|
|
|
2016-09-28 15:51:24 +02:00
|
|
|
def on_shownotes_selected_episodes(self, *params):
|
2009-07-13 14:09:43 +02:00
|
|
|
episodes = self.get_selected_episodes()
|
2014-01-23 22:44:52 +01:00
|
|
|
self.shownotes_object.toggle_pane_visibility(episodes)
|
2009-07-13 14:09:43 +02:00
|
|
|
|
2016-09-28 15:51:24 +02:00
|
|
|
def on_download_selected_episodes(self, action_or_widget, param=None):
|
2009-07-13 14:09:43 +02:00
|
|
|
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-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.')
|
2016-09-25 14:31:58 +02:00
|
|
|
GObject.source_remove(self._auto_update_timer_source_id)
|
2009-10-13 22:48:12 +02:00
|
|
|
self._auto_update_timer_source_id = None
|
|
|
|
|
2018-05-08 11:41:52 +02:00
|
|
|
if (self.config.auto_update_feeds and
|
|
|
|
self.config.auto_update_frequency):
|
2018-03-23 20:53:31 +01: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)
|
2018-05-08 10:43:56 +02:00
|
|
|
self._auto_update_timer_source_id = GObject.timeout_add(
|
2009-10-13 22:48:12 +02:00
|
|
|
interval, self._on_auto_update_timer)
|
|
|
|
|
|
|
|
def _on_auto_update_timer(self):
|
2012-10-13 13:52:14 +02:00
|
|
|
if not util.connection_available():
|
|
|
|
logger.debug('Skipping auto update (no connection available)')
|
2012-10-16 10:40:48 +02:00
|
|
|
return True
|
2012-10-13 13:52:14 +02:00
|
|
|
|
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)
|
2013-10-14 20:55:26 +02:00
|
|
|
if self.mygpo_client.can_access_webservice():
|
|
|
|
self.mygpo_client.flush()
|
2010-01-28 17:39:10 +01:00
|
|
|
|
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()
|
2017-04-18 16:19:10 +02:00
|
|
|
selected_tasks = [(Gtk.TreeRowReference.new(model, path), model.get_value(model.get_iter(path), 0)) for path in paths]
|
2009-04-01 01:12:17 +02:00
|
|
|
|
|
|
|
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):
|
2016-11-15 04:54:21 +01:00
|
|
|
self.download_queue_manager.queue_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()))
|
2013-02-08 11:08:28 +01:00
|
|
|
|
2009-04-01 01:12:17 +02:00
|
|
|
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()
|
|
|
|
|
2016-09-28 15:51:24 +02:00
|
|
|
def on_item_cancel_download_activate(self, *params):
|
2009-09-02 15:57:09 +02:00
|
|
|
if self.wNotebook.get_current_page() == 0:
|
|
|
|
selection = self.treeAvailable.get_selection()
|
|
|
|
(model, paths) = selection.get_selected_rows()
|
2018-05-08 10:43:56 +02:00
|
|
|
urls = [model.get_value(model.get_iter(path),
|
2009-09-02 15:57:09 +02:00
|
|
|
self.episode_list_model.C_URL) for path in paths]
|
2018-05-08 10:43:56 +02:00
|
|
|
selected_tasks = [task for task in self.download_tasks_seen
|
2009-09-02 15:57:09 +02:00
|
|
|
if task.url in urls]
|
|
|
|
else:
|
|
|
|
selection = self.treeDownloads.get_selection()
|
|
|
|
(model, paths) = selection.get_selected_rows()
|
2018-05-08 10:43:56 +02:00
|
|
|
selected_tasks = [model.get_value(model.get_iter(path),
|
2009-09-02 15:57:09 +02:00
|
|
|
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):
|
2012-03-22 11:22:46 +01:00
|
|
|
# Allow tab switching with Ctrl + PgUp/PgDown/Tab
|
2016-09-25 14:31:58 +02:00
|
|
|
if event.get_state() & Gdk.ModifierType.CONTROL_MASK:
|
2016-09-20 16:02:47 +02:00
|
|
|
current_page = self.wNotebook.get_current_page()
|
2018-05-27 20:29:39 +02:00
|
|
|
if event.keyval in (Gdk.KEY_Page_Up, Gdk.KEY_ISO_Left_Tab):
|
2016-09-20 16:02:47 +02:00
|
|
|
if current_page == 0:
|
|
|
|
current_page = self.wNotebook.get_n_pages()
|
|
|
|
self.wNotebook.set_current_page(current_page - 1)
|
2008-09-27 14:20:43 +02:00
|
|
|
return True
|
2016-09-20 16:02:47 +02:00
|
|
|
elif event.keyval in (Gdk.KEY_Page_Down, Gdk.KEY_Tab):
|
|
|
|
if current_page == self.wNotebook.get_n_pages() - 1:
|
|
|
|
current_page = -1
|
|
|
|
self.wNotebook.set_current_page(current_page + 1)
|
2012-03-22 11:22:46 +01:00
|
|
|
return True
|
2017-03-26 22:14:10 +02:00
|
|
|
elif event.keyval == Gdk.KEY_Delete:
|
|
|
|
if isinstance(widget.get_focus(), Gtk.Entry):
|
|
|
|
logger.debug("Entry has focus, ignoring Delete")
|
|
|
|
else:
|
|
|
|
self.main_window.activate_action('delete')
|
|
|
|
return True
|
2008-09-27 14:20:43 +02:00
|
|
|
|
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()
|
2013-02-08 11:08:28 +01:00
|
|
|
|
2008-01-09 23:46:17 +01:00
|
|
|
def iconify_main_window(self):
|
2009-09-05 00:37:12 +02:00
|
|
|
if not self.is_iconified():
|
2013-02-08 11:08:28 +01:00
|
|
|
self.gPodder.iconify()
|
2008-07-11 19:13:04 +02:00
|
|
|
|
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):
|
2014-09-27 15:01:06 +02:00
|
|
|
# Strip leading application protocol, so these URLs work:
|
|
|
|
# gpodder://example.com/episodes.rss
|
|
|
|
# gpodder:https://example.org/podcast.xml
|
|
|
|
if url.startswith('gpodder:'):
|
|
|
|
url = url[len('gpodder:'):]
|
|
|
|
while url.startswith('/'):
|
|
|
|
url = url[1:]
|
|
|
|
|
2014-09-27 14:51:35 +02:00
|
|
|
self._add_podcast_dialog = gPodderAddPodcast(self.gPodder,
|
2012-11-17 12:53:00 +01:00
|
|
|
add_podcast_list=self.add_podcast_list,
|
2009-08-26 14:45:54 +02:00
|
|
|
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
|
|
|
|
|
2012-02-04 21:43:37 +01:00
|
|
|
def extensions_podcast_update_cb(self, podcast):
|
|
|
|
logger.debug('extensions_podcast_update_cb(%s)', podcast)
|
2011-10-19 12:37:55 +02:00
|
|
|
self.update_feed_cache(channels=[podcast],
|
|
|
|
show_new_episodes_dialog=False)
|
2011-08-02 15:45:49 +02:00
|
|
|
|
2012-02-04 21:43:37 +01:00
|
|
|
def extensions_episode_download_cb(self, episode):
|
|
|
|
logger.debug('extension_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
|
|
|
|
2012-07-02 05:53:33 +02:00
|
|
|
def on_sync_to_device_activate(self, widget, episodes=None, force_played=True):
|
2012-07-09 21:08:40 +02:00
|
|
|
self.sync_ui = gPodderSyncUI(self.config, self.notification,
|
|
|
|
self.main_window,
|
|
|
|
self.show_confirmation,
|
2016-09-28 12:04:21 +02:00
|
|
|
self.application.on_itemPreferences_activate,
|
2013-02-01 04:36:21 +01:00
|
|
|
self.channels,
|
2012-07-09 21:08:40 +02:00
|
|
|
self.download_status_model,
|
|
|
|
self.download_queue_manager,
|
|
|
|
self.enable_download_list_update,
|
2013-02-01 04:36:21 +01:00
|
|
|
self.commit_changes_to_database,
|
2018-10-14 16:10:45 +02:00
|
|
|
self.delete_episode_list,
|
|
|
|
gPodderEpisodeSelector)
|
2012-07-02 05:53:33 +02:00
|
|
|
|
2018-10-14 16:10:45 +02:00
|
|
|
self.sync_ui.on_synchronize_episodes(self.channels, episodes, force_played,
|
|
|
|
self.enable_download_list_update)
|
2012-07-02 05:53:33 +02:00
|
|
|
|
2016-09-28 12:04:21 +02:00
|
|
|
def on_update_youtube_subscriptions_activate(self, action, param):
|
2015-05-20 21:10:57 +02:00
|
|
|
if not self.config.youtube.api_key_v3:
|
|
|
|
if self.show_confirmation('\n'.join((_('Please register a YouTube API key and set it in the preferences.'),
|
|
|
|
_('Would you like to set up an API key now?'))), _('API key required')):
|
2016-09-28 12:04:21 +02:00
|
|
|
self.application.on_itemPreferences_activate(self, None)
|
2015-05-20 21:10:57 +02:00
|
|
|
return
|
|
|
|
|
|
|
|
failed_urls = []
|
|
|
|
migrated_users = []
|
|
|
|
for podcast in self.channels:
|
|
|
|
url, user = youtube.for_each_feed_pattern(lambda url, channel: (url, channel), podcast.url, (None, None))
|
|
|
|
if url is not None and user is not None:
|
|
|
|
try:
|
|
|
|
logger.info('Getting channels for YouTube user %s (%s)', user, url)
|
|
|
|
new_urls = youtube.get_channels_for_user(user, self.config.youtube.api_key_v3)
|
|
|
|
logger.debug('YouTube channels retrieved: %r', new_urls)
|
|
|
|
|
2015-07-01 22:58:44 +02:00
|
|
|
if len(new_urls) == 0 and youtube.get_youtube_id(url) is not None:
|
|
|
|
logger.info('No need to update %s', url)
|
|
|
|
continue
|
|
|
|
|
2015-05-20 21:10:57 +02:00
|
|
|
if len(new_urls) != 1:
|
2015-07-01 22:58:44 +02:00
|
|
|
failed_urls.append((url, _('No unique URL found')))
|
2015-05-20 21:10:57 +02:00
|
|
|
continue
|
|
|
|
|
|
|
|
new_url = new_urls[0]
|
|
|
|
if new_url in set(x.url for x in self.model.get_podcasts()):
|
|
|
|
failed_urls.append((url, _('Already subscribed')))
|
|
|
|
continue
|
|
|
|
|
|
|
|
logger.info('New feed location: %s => %s', url, new_url)
|
|
|
|
podcast.url = new_url
|
|
|
|
podcast.save()
|
|
|
|
migrated_users.append(user)
|
|
|
|
except Exception as e:
|
|
|
|
logger.error('Exception happened while updating download list.', exc_info=True)
|
|
|
|
self.show_message(_('Make sure the API key is correct. Error: %(message)s') % {'message': str(e)},
|
|
|
|
_('Error getting YouTube channels'), important=True)
|
|
|
|
|
|
|
|
if migrated_users:
|
|
|
|
self.show_message('\n'.join(migrated_users), _('Successfully migrated subscriptions'))
|
|
|
|
elif not failed_urls:
|
|
|
|
self.show_message(_('Subscriptions are up to date'))
|
|
|
|
|
|
|
|
if failed_urls:
|
2015-05-20 21:50:10 +02:00
|
|
|
self.show_message('\n'.join([_('These URLs failed:'), ''] + ['{0}: {1}'.format(url, message)
|
2015-05-20 21:10:57 +02:00
|
|
|
for url, message in failed_urls]),
|
|
|
|
_('Could not migrate some subscriptions'), important=True)
|
|
|
|
|
2017-07-25 14:57:36 +02:00
|
|
|
def on_extension_enabled(self, extension):
|
2017-08-15 22:01:26 +02:00
|
|
|
if getattr(extension, 'on_ui_object_available', None) is not None:
|
|
|
|
extension.on_ui_object_available('gpodder-gtk', self)
|
|
|
|
if getattr(extension, 'on_ui_initialized', None) is not None:
|
|
|
|
extension.on_ui_initialized(self.model,
|
|
|
|
self.extensions_podcast_update_cb,
|
|
|
|
self.extensions_episode_download_cb)
|
2017-07-25 14:57:36 +02:00
|
|
|
self.inject_extensions_menu()
|
|
|
|
|
|
|
|
def on_extension_disabled(self, extension):
|
|
|
|
self.inject_extensions_menu()
|
2015-05-20 21:10:57 +02:00
|
|
|
|
2009-09-06 23:05:38 +02:00
|
|
|
|
2016-09-28 12:04:21 +02:00
|
|
|
class gPodderApplication(Gtk.Application):
|
2012-02-05 23:42:23 +01:00
|
|
|
|
2016-09-28 12:04:21 +02:00
|
|
|
def __init__(self, options):
|
2016-10-02 19:34:24 +02:00
|
|
|
Gtk.Application.__init__(self, application_id='org.gpodder.gpodder',
|
2016-09-28 12:04:21 +02:00
|
|
|
flags=Gio.ApplicationFlags.FLAGS_NONE)
|
|
|
|
self.window = None
|
2018-02-06 15:19:08 +01:00
|
|
|
self.options = options
|
2016-10-02 19:34:24 +02:00
|
|
|
self.connect('window-removed', self.on_window_removed)
|
2016-09-28 12:04:21 +02:00
|
|
|
|
|
|
|
def create_actions(self):
|
2016-10-02 19:34:24 +02:00
|
|
|
action = Gio.SimpleAction.new('about', None)
|
|
|
|
action.connect('activate', self.on_about)
|
2016-09-28 12:04:21 +02:00
|
|
|
self.add_action(action)
|
|
|
|
|
2016-10-02 19:34:24 +02:00
|
|
|
action = Gio.SimpleAction.new('quit', None)
|
|
|
|
action.connect('activate', self.on_quit)
|
2016-09-28 12:04:21 +02:00
|
|
|
self.add_action(action)
|
|
|
|
|
2017-10-20 02:52:25 +02:00
|
|
|
action = Gio.SimpleAction.new('help', None)
|
|
|
|
action.connect('activate', self.on_help_activate)
|
2016-09-28 12:04:21 +02:00
|
|
|
self.add_action(action)
|
|
|
|
|
2016-10-02 19:34:24 +02:00
|
|
|
action = Gio.SimpleAction.new('preferences', None)
|
|
|
|
action.connect('activate', self.on_itemPreferences_activate)
|
2016-09-28 12:04:21 +02:00
|
|
|
self.add_action(action)
|
|
|
|
|
2016-10-02 19:34:24 +02:00
|
|
|
action = Gio.SimpleAction.new('gotoMygpo', None)
|
|
|
|
action.connect('activate', self.on_goto_mygpo)
|
2016-09-28 12:04:21 +02:00
|
|
|
self.add_action(action)
|
|
|
|
|
2016-10-02 19:34:24 +02:00
|
|
|
action = Gio.SimpleAction.new('checkForUpdates', None)
|
|
|
|
action.connect('activate', self.on_check_for_updates_activate)
|
2016-09-28 12:04:21 +02:00
|
|
|
self.add_action(action)
|
|
|
|
|
|
|
|
def do_startup(self):
|
|
|
|
Gtk.Application.do_startup(self)
|
|
|
|
|
|
|
|
self.create_actions()
|
|
|
|
|
|
|
|
builder = Gtk.Builder()
|
|
|
|
builder.set_translation_domain(gpodder.textdomain)
|
|
|
|
|
|
|
|
for ui_folder in gpodder.ui_folders:
|
2016-10-02 19:34:24 +02:00
|
|
|
filename = os.path.join(ui_folder, 'gtk/menus.ui')
|
2016-09-28 12:04:21 +02:00
|
|
|
if os.path.exists(filename):
|
|
|
|
builder.add_from_file(filename)
|
|
|
|
break
|
|
|
|
|
2016-10-02 19:34:24 +02:00
|
|
|
menubar = builder.get_object('menubar')
|
2016-09-30 20:26:27 +02:00
|
|
|
if menubar is None:
|
|
|
|
logger.error('Cannot find gtk/menus.ui in %r, exiting' % gpodder.ui_folders)
|
|
|
|
sys.exit(1)
|
2016-09-28 12:04:21 +02:00
|
|
|
|
2016-10-02 19:34:24 +02:00
|
|
|
self.menu_view_columns = builder.get_object('menuViewColumns')
|
2016-09-30 20:26:27 +02:00
|
|
|
self.set_menubar(menubar)
|
2016-09-28 12:04:21 +02:00
|
|
|
|
2016-10-02 19:34:24 +02:00
|
|
|
self.set_app_menu(builder.get_object('app-menu'))
|
2016-09-30 20:26:27 +02:00
|
|
|
|
2016-09-28 12:04:21 +02:00
|
|
|
for i in range(EpisodeListModel.PROGRESS_STEPS + 1):
|
2018-03-01 22:25:02 +01:00
|
|
|
pixbuf = draw_cake_pixbuf(i /
|
2017-02-14 15:50:07 +01:00
|
|
|
EpisodeListModel.PROGRESS_STEPS)
|
2018-03-01 22:25:02 +01:00
|
|
|
icon_name = 'gpodder-progress-%d' % i
|
|
|
|
Gtk.IconTheme.add_builtin_icon(icon_name, pixbuf.get_width(), pixbuf)
|
2018-01-30 14:04:28 +01:00
|
|
|
|
2016-09-28 12:04:21 +02:00
|
|
|
Gtk.Window.set_default_icon_name('gpodder')
|
2018-05-16 18:17:52 +02:00
|
|
|
# Gtk.AboutDialog.set_url_hook(lambda dlg, link, data: util.open_website(link), None)
|
2018-01-30 14:04:28 +01:00
|
|
|
|
2016-09-28 12:04:21 +02:00
|
|
|
try:
|
2018-03-01 22:25:02 +01:00
|
|
|
dbus_main_loop = dbus.glib.DBusGMainLoop(set_as_default=True)
|
|
|
|
gpodder.dbus_session_bus = dbus.SessionBus(dbus_main_loop)
|
2018-01-30 14:04:28 +01:00
|
|
|
|
2018-03-01 22:25:02 +01:00
|
|
|
self.bus_name = dbus.service.BusName(gpodder.dbus_bus_name, bus=gpodder.dbus_session_bus)
|
2016-11-21 23:13:46 +01:00
|
|
|
except dbus.exceptions.DBusException as dbe:
|
2018-03-01 22:25:02 +01:00
|
|
|
logger.warn('Cannot get "on the bus".', exc_info=True)
|
2018-05-08 10:43:56 +02:00
|
|
|
dlg = Gtk.MessageDialog(None, Gtk.DialogFlags.MODAL, Gtk.MessageType.ERROR,
|
2016-09-28 12:04:21 +02:00
|
|
|
Gtk.ButtonsType.CLOSE, _('Cannot start gPodder'))
|
2018-03-01 22:25:02 +01:00
|
|
|
dlg.format_secondary_markup(_('D-Bus error: %s') % (str(dbe),))
|
|
|
|
dlg.set_title('gPodder')
|
|
|
|
dlg.run()
|
|
|
|
dlg.destroy()
|
|
|
|
sys.exit(0)
|
2018-07-20 09:34:40 +02:00
|
|
|
util.idle_add(self.check_root_folder_path_gui)
|
2016-09-28 12:04:21 +02:00
|
|
|
|
|
|
|
def do_activate(self):
|
|
|
|
# We only allow a single window and raise any existing ones
|
|
|
|
if not self.window:
|
|
|
|
# Windows are associated with the application
|
|
|
|
# when the last one is closed the application shuts down
|
|
|
|
self.window = gPodder(self, self.bus_name, core.Core(UIConfig, model_class=Model), self.options)
|
2018-01-30 14:04:28 +01:00
|
|
|
|
2016-09-28 12:04:21 +02:00
|
|
|
if gpodder.ui.osx:
|
2018-02-06 15:19:08 +01:00
|
|
|
from gpodder.gtkui import macosx
|
2018-01-30 14:04:28 +01:00
|
|
|
|
2018-02-06 15:19:08 +01:00
|
|
|
# Handle "subscribe to podcast" events from firefox
|
|
|
|
macosx.register_handlers(self.window)
|
2018-01-30 14:04:28 +01:00
|
|
|
|
2016-09-28 12:04:21 +02:00
|
|
|
self.window.gPodder.present()
|
|
|
|
|
|
|
|
def on_about(self, action, param):
|
2018-05-08 10:43:56 +02:00
|
|
|
dlg = Gtk.Dialog(_('About gPodder'), self.window.gPodder,
|
2016-09-28 12:04:21 +02:00
|
|
|
Gtk.DialogFlags.MODAL)
|
|
|
|
dlg.add_button(Gtk.STOCK_CLOSE, Gtk.ResponseType.OK).show()
|
|
|
|
dlg.set_resizable(False)
|
|
|
|
|
|
|
|
bg = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=6)
|
|
|
|
pb = GdkPixbuf.Pixbuf.new_from_file_at_size(gpodder.icon_file, 160, 160)
|
|
|
|
bg.pack_start(Gtk.Image.new_from_pixbuf(pb), False, False, 0)
|
|
|
|
vb = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=6)
|
|
|
|
label = Gtk.Label()
|
|
|
|
label.set_alignment(0, 0.5)
|
|
|
|
label.set_markup('\n'.join(x.strip() for x in """
|
|
|
|
<b>gPodder {version} ({date})</b>
|
|
|
|
|
|
|
|
{copyright}
|
|
|
|
License: {license}
|
|
|
|
|
2018-08-02 22:40:17 +02:00
|
|
|
<a href="{url}">{tr_website}</a> · <a href="{bugs_url}">{tr_bugtracker}</a>
|
2016-09-28 12:04:21 +02:00
|
|
|
""".format(version=gpodder.__version__,
|
|
|
|
date=gpodder.__date__,
|
|
|
|
copyright=gpodder.__copyright__,
|
|
|
|
license=gpodder.__license__,
|
2017-04-23 19:37:59 +02:00
|
|
|
bugs_url='https://github.com/gpodder/gpodder/issues',
|
2018-08-02 22:40:17 +02:00
|
|
|
url=cgi.escape(gpodder.__url__),
|
|
|
|
tr_website=_('Website'),
|
|
|
|
tr_bugtracker=_('Bug Tracker')).strip().split('\n')))
|
2016-09-28 12:04:21 +02:00
|
|
|
|
|
|
|
vb.pack_start(label, False, False, 0)
|
|
|
|
bg.pack_start(vb, False, False, 0)
|
|
|
|
bg.pack_start(Gtk.Label(), False, False, 0)
|
|
|
|
|
|
|
|
dlg.vbox.pack_start(bg, False, False, 0)
|
|
|
|
dlg.connect('response', lambda dlg, response: dlg.destroy())
|
|
|
|
|
|
|
|
dlg.vbox.show_all()
|
2005-11-21 19:21:25 +01:00
|
|
|
|
2009-05-30 11:31:02 +02:00
|
|
|
dlg.run()
|
2009-05-11 21:35:33 +02:00
|
|
|
|
2016-09-28 12:04:21 +02:00
|
|
|
def on_quit(self, *args):
|
2016-10-01 13:01:46 +02:00
|
|
|
self.window.on_gPodder_delete_event()
|
|
|
|
|
|
|
|
def on_window_removed(self, *args):
|
2016-09-28 12:04:21 +02:00
|
|
|
self.quit()
|
2009-08-26 14:45:54 +02:00
|
|
|
|
2017-10-20 02:52:25 +02:00
|
|
|
def on_help_activate(self, action, param):
|
|
|
|
util.open_website('https://gpodder.github.io/docs/')
|
2012-03-02 21:20:01 +01:00
|
|
|
|
2016-09-28 12:04:21 +02:00
|
|
|
def on_itemPreferences_activate(self, action, param=None):
|
2018-05-08 10:43:56 +02:00
|
|
|
gPodderPreferences(self.window.gPodder,
|
|
|
|
_config=self.window.config,
|
|
|
|
user_apps_reader=self.window.user_apps_reader,
|
|
|
|
parent_window=self.window.main_window,
|
|
|
|
mygpo_client=self.window.mygpo_client,
|
|
|
|
on_send_full_subscriptions=self.window.on_send_full_subscriptions,
|
|
|
|
on_itemExportChannels_activate=self.window.on_itemExportChannels_activate,
|
2017-07-25 14:57:36 +02:00
|
|
|
on_extension_enabled=self.on_extension_enabled,
|
|
|
|
on_extension_disabled=self.on_extension_disabled)
|
2010-03-21 14:54:16 +01:00
|
|
|
|
2016-09-28 12:04:21 +02:00
|
|
|
def on_goto_mygpo(self, action, param):
|
|
|
|
self.window.mygpo_client.open_website()
|
2012-03-02 21:20:01 +01:00
|
|
|
|
2016-09-28 12:04:21 +02:00
|
|
|
def on_check_for_updates_activate(self, action, param):
|
|
|
|
self.window.check_for_updates(silent=False)
|
|
|
|
|
2017-07-25 14:57:36 +02:00
|
|
|
def on_extension_enabled(self, extension):
|
|
|
|
self.window.on_extension_enabled(extension)
|
|
|
|
|
|
|
|
def on_extension_disabled(self, extension):
|
|
|
|
self.window.on_extension_disabled(extension)
|
|
|
|
|
2018-07-20 09:34:40 +02:00
|
|
|
@staticmethod
|
|
|
|
def check_root_folder_path_gui():
|
|
|
|
msg = check_root_folder_path()
|
|
|
|
if msg:
|
|
|
|
dlg = Gtk.MessageDialog(None, Gtk.DialogFlags.MODAL, Gtk.MessageType.WARNING,
|
|
|
|
Gtk.ButtonsType.CLOSE, msg)
|
|
|
|
dlg.set_title(_('Path to gPodder home is too long'))
|
|
|
|
dlg.run()
|
|
|
|
dlg.destroy()
|
|
|
|
|
2017-07-25 14:57:36 +02:00
|
|
|
|
2016-09-28 12:04:21 +02:00
|
|
|
def main(options=None):
|
2016-09-30 20:26:57 +02:00
|
|
|
GObject.set_application_name('gPodder')
|
|
|
|
|
2016-09-28 12:04:21 +02:00
|
|
|
gp = gPodderApplication(options)
|
2009-02-25 14:57:45 +01:00
|
|
|
gp.run()
|
2016-09-30 20:26:57 +02:00
|
|
|
sys.exit(0)
|