Merge branch 'master' into dev-adaptive

This commit is contained in:
Teemu Ikonen 2022-04-12 16:24:25 +03:00
commit cd15106eac
9 changed files with 1091 additions and 1030 deletions

14
bin/gpo
View File

@ -216,8 +216,8 @@ class gPodderCli(object):
self._extensions_episode_download_cb)
@contextlib.contextmanager
def _action(self, msg, *args):
self._start_action(msg, *args)
def _action(self, msg):
self._start_action(msg)
try:
yield
self._finish_action()
@ -271,8 +271,8 @@ class gPodderCli(object):
self._info(_('Episode download requested by extensions.'))
self._download_episode(episode)
def _start_action(self, msg, *args):
line = util.convert_bytes(msg % args)
def _start_action(self, msg):
line = util.convert_bytes(msg)
if len(line) > self.COLUMNS - 7:
line = line[:self.COLUMNS - 7 - 3] + '...'
else:
@ -543,7 +543,7 @@ class gPodderCli(object):
return True
def _update_podcast(self, podcast):
with self._action(' %s', podcast.title):
with self._action(' %s' % podcast.title):
podcast.update()
def _pending_message(self, count):
@ -618,7 +618,7 @@ class gPodderCli(object):
return True
def _download_episode(self, episode):
with self._action('Downloading %s', episode.title):
with self._action('Downloading %s' % episode.title):
task = download.DownloadTask(episode, self._config)
task.add_progress_callback(self._update_action)
task.status = download.DownloadTask.DOWNLOADING
@ -951,7 +951,7 @@ class gPodderCli(object):
def queue_task(x, task):
def progress_updated(progress):
self._update_action(progress)
with self._action(_('Syncing %s'), ep_repr(task.episode)):
with self._action(_('Syncing %s') % ep_repr(task.episode)):
task.status = sync.SyncTask.DOWNLOADING
task.add_progress_callback(progress_updated)
task.run()

View File

@ -125,7 +125,7 @@ class CurrentTrackTracker(object):
('status' not in kwargs or kwargs['status'] == 'Playing') and not
subsecond_difference(cur['pos'], kwargs['pos'])):
logger.debug('notify Stopped: playback discontinuity:' +
'calc: %f observed: %f', cur['pos'], kwargs['pos'])
'calc: %r observed: %r', cur['pos'], kwargs['pos'])
self.notify_stop()
if ((kwargs['pos']) == 0 and
@ -159,7 +159,7 @@ class CurrentTrackTracker(object):
if self.status == 'Playing':
self.notify_playing()
else:
logger.debug('notify Stopped: status %s', self.status)
logger.debug('notify Stopped: status %r', self.status)
self.notify_stop()
def getinfo(self):
@ -289,12 +289,20 @@ class MPRISDBusReceiver(object):
def query_position(self, sender):
proxy = self.bus.get_object(sender, self.PATH_MPRIS)
props = dbus.Interface(proxy, self.INTERFACE_PROPS)
return props.Get(self.INTERFACE_MPRIS, 'Position')
try:
pos = props.Get(self.INTERFACE_MPRIS, 'Position')
except:
pos = None
return pos
def query_status(self, sender):
proxy = self.bus.get_object(sender, self.PATH_MPRIS)
props = dbus.Interface(proxy, self.INTERFACE_PROPS)
return props.Get(self.INTERFACE_MPRIS, 'PlaybackStatus')
try:
status = props.Get(self.INTERFACE_MPRIS, 'PlaybackStatus')
except:
status = None
return status
class gPodderNotifier(dbus.service.Object):

View File

@ -14,12 +14,9 @@ try:
import yt_dlp as youtube_dl
except:
import youtube_dl
from youtube_dl.utils import DownloadError, ExtractorError, sanitize_url
import gpodder
from gpodder import download, feedcore, model, registry, youtube
from gpodder.util import (mimetype_from_extension, nice_html_description,
remove_html_tags)
from gpodder import download, feedcore, model, registry, util, youtube
_ = gpodder.gettext
@ -112,7 +109,7 @@ class YoutubeCustomDownload(download.CustomDownload):
os.rename(tempname_with_ext, tempname)
dot_ext = try_ext
break
ext_filetype = mimetype_from_extension(dot_ext)
ext_filetype = util.mimetype_from_extension(dot_ext)
if ext_filetype:
# Youtube weba formats have a webm extension and get a video/webm mime-type
# but audio content has no width or height, so change it to audio/webm for correct icon and player
@ -210,10 +207,10 @@ class YoutubeFeed(model.Feed):
episodes = []
for en in self._ie_result['entries']:
guid = video_guid(en['id'])
description = remove_html_tags(en.get('description') or _('No description available'))
html_description = nice_html_description(en.get('thumbnail'), description)
description = util.remove_html_tags(en.get('description') or _('No description available'))
html_description = util.nice_html_description(en.get('thumbnail'), description)
if en.get('ext'):
mime_type = mimetype_from_extension('.{}'.format(en['ext']))
mime_type = util.mimetype_from_extension('.{}'.format(en['ext']))
else:
mime_type = 'application/octet-stream'
if en.get('filesize'):
@ -326,8 +323,8 @@ class gPodderYoutubeDL(download.CustomDownloader):
with youtube_dl.YoutubeDL(opts) as ydl:
ydl.process_ie_result(tmp, download=False)
new_entries.extend(tmp.get('entries'))
except DownloadError as ex:
if ex.exc_info[0] == ExtractorError:
except youtube_dl.utils.DownloadError as ex:
if ex.exc_info[0] == youtube_dl.utils.ExtractorError:
# for instance "This video contains content from xyz, who has blocked it on copyright grounds"
logger.warning('Skipping %s: %s', e.get('title', ''), ex.exc_info[1])
continue
@ -360,7 +357,7 @@ class gPodderYoutubeDL(download.CustomDownloader):
result_type, has_playlist = extract_type(ie_result)
while not has_playlist:
if result_type in ('url', 'url_transparent'):
ie_result['url'] = sanitize_url(ie_result['url'])
ie_result['url'] = youtube_dl.utils.sanitize_url(ie_result['url'])
if result_type == 'url':
logger.debug("extract_info(%s) to get the video list", ie_result['url'])
# We have to add extra_info to the results because it may be
@ -400,6 +397,8 @@ class gPodderYoutubeDL(download.CustomDownloader):
return None
def is_supported_url(self, url):
if url is None:
return False
if self.regex_cache[0].match(url) is not None:
return True
for r in self.regex_cache[1:]:
@ -469,12 +468,12 @@ class gPodderExtension:
_('Old Youtube-DL'), important=True, widget=ui_object.main_window)
def on_episodes_context_menu(self, episodes):
if not self.container.config.manage_downloads \
and not all(e.was_downloaded(and_exists=True) for e in episodes) \
and not any(e.downloading for e in episodes):
if not self.container.config.manage_downloads and any(e.can_download() for e in episodes):
return [(_("Download with Youtube-DL"), self.download_episodes)]
def download_episodes(self, episodes):
episodes = [e for e in episodes if e.can_download()]
# create a new gPodderYoutubeDL to force using it even if manage_downloads is False
downloader = gPodderYoutubeDL(self.container.manager.core.config, self.container.config, force=True)
self.gpodder.download_episode_list(episodes, downloader=downloader)

File diff suppressed because it is too large Load Diff

View File

@ -612,6 +612,18 @@ class DownloadTask(object):
downloader = property(fget=__get_downloader, fset=__set_downloader)
def can_queue(self):
return self.status in (self.CANCELLED, self.PAUSED, self.FAILED)
def unpause(self):
with self:
# Resume a downloading task that was transitioning to paused
if self.status == self.PAUSING:
self.status = self.DOWNLOADING
def can_pause(self):
return self.status in (self.DOWNLOADING, self.QUEUED)
def pause(self):
with self:
# Pause a queued download
@ -622,11 +634,8 @@ class DownloadTask(object):
self.status = self.PAUSING
# download rate limited tasks sleep and take longer to transition from the PAUSING state to the PAUSED state
def unpause(self):
with self:
# Resume a downloading task that was transitioning to paused
if self.status == self.PAUSING:
self.status = self.DOWNLOADING
def can_cancel(self):
return self.status in (self.DOWNLOADING, self.QUEUED, self.PAUSED, self.FAILED)
def cancel(self):
with self:
@ -639,6 +648,9 @@ class DownloadTask(object):
elif self.status == self.DOWNLOADING:
self.status = self.CANCELLING
def can_remove(self):
return self.status in (self.CANCELLED, self.FAILED, self.DONE)
def delete_partial_files(self):
temporary_files = [self.tempname]
# YoutubeDL creates .partial.* files for adaptive formats

View File

@ -321,7 +321,7 @@ class gPodderPreferences(BuilderWidget):
result = gpodder.user_extensions.on_preferences()
if result:
for label, callback in result:
self.notebook.append_page(callback(), Gtk.Label(label))
self.prefs_stack.add_titled(callback(), label, label)
def _extensions_select_function(self, selection, model, path, path_currently_selected):
return model.get_value(model.get_iter(path), self.C_SHOW_TOGGLE)

View File

@ -1264,7 +1264,7 @@ class gPodder(BuilderWidget, dbus.service.Object):
selection = self.treeAvailable.get_selection()
selection.set_mode(Gtk.SelectionMode.MULTIPLE)
self.selection_handler_id = selection.connect('changed', self.on_episode_list_selection_changed)
self.episode_selection_handler_id = selection.connect('changed', self.on_episode_list_selection_changed)
self._search_episodes = SearchTreeBar(self.episodes_search_bar,
self.entry_search_episodes,
@ -1287,11 +1287,13 @@ class gPodder(BuilderWidget, dbus.service.Object):
self.deck.set_can_swipe_forward(False)
self.shownotes_object.set_episodes(eps)
def init_download_list_treeview(self):
# enable multiple selection support
self.treeDownloads.get_selection().set_mode(Gtk.SelectionMode.MULTIPLE)
self.treeDownloads.set_search_equal_func(TreeViewHelper.make_search_equal_func(DownloadStatusModel))
def on_download_list_selection_changed(self, selection):
# if self.wNotebook.get_current_page() > 0:
# # Update the toolbar buttons
# self.play_or_download()
pass
def init_download_list_treeview(self):
# columns and renderers for "download progress" tab
# First column: [ICON] Episodename
column = Gtk.TreeViewColumn(_('Episode'))
@ -1340,6 +1342,12 @@ class gPodder(BuilderWidget, dbus.service.Object):
self.treeDownloads.connect('popup-menu', self.treeview_downloads_show_context_menu)
# enable multiple selection support
selection = self.treeDownloads.get_selection()
selection.set_mode(Gtk.SelectionMode.MULTIPLE)
self.download_selection_handler_id = selection.connect('changed', self.on_download_list_selection_changed)
self.treeDownloads.set_search_equal_func(TreeViewHelper.make_search_equal_func(DownloadStatusModel))
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):
@ -1724,7 +1732,7 @@ class gPodder(BuilderWidget, dbus.service.Object):
selection = self.treeDownloads.get_selection()
model, paths = selection.get_selected_rows()
can_queue, can_cancel, can_pause, can_remove, can_force = (True,) * 5
can_force, can_queue, can_pause, can_cancel, can_remove = (True,) * 5
selected_tasks = [(Gtk.TreeRowReference.new(model, path),
model.get_value(model.get_iter(path),
DownloadStatusModel.C_TASK)) for path in paths]
@ -1732,24 +1740,16 @@ class gPodder(BuilderWidget, dbus.service.Object):
for row_reference, task in selected_tasks:
if task.status != download.DownloadTask.QUEUED:
can_force = False
if task.status not in (download.DownloadTask.PAUSED,
download.DownloadTask.FAILED,
download.DownloadTask.CANCELLED):
if not task.can_queue():
can_queue = False
if task.status not in (download.DownloadTask.PAUSED,
download.DownloadTask.QUEUED,
download.DownloadTask.DOWNLOADING,
download.DownloadTask.FAILED):
can_cancel = False
if task.status not in (download.DownloadTask.QUEUED,
download.DownloadTask.DOWNLOADING):
if not task.can_pause():
can_pause = False
if task.status not in (download.DownloadTask.CANCELLED,
download.DownloadTask.FAILED,
download.DownloadTask.DONE):
if not task.can_cancel():
can_cancel = False
if not task.can_remove():
can_remove = False
return selected_tasks, can_queue, can_cancel, can_pause, can_remove, can_force
return selected_tasks, can_force, can_queue, can_pause, can_cancel, can_remove
def downloads_finished(self, download_tasks_seen):
# Separate tasks into downloads & syncs
@ -1866,10 +1866,7 @@ class gPodder(BuilderWidget, dbus.service.Object):
with task:
if status == download.DownloadTask.QUEUED:
# Only queue task when it's paused/failed/cancelled (or forced)
if task.status in (download.DownloadTask.PAUSED,
download.DownloadTask.FAILED,
download.DownloadTask.CANCELLED) or force_start:
if task.can_queue() or force_start:
# add the task back in if it was already cleaned up
# (to trigger this cancel one downloads in the active list, cancel all
# other downloads, quickly right click on the cancelled on one to get
@ -1887,9 +1884,8 @@ class gPodder(BuilderWidget, dbus.service.Object):
elif status == download.DownloadTask.PAUSING:
task.pause()
elif status is None:
# Remove the selected task - cancel downloading/queued tasks
if task.status in (download.DownloadTask.QUEUED, download.DownloadTask.DOWNLOADING):
task.status = download.DownloadTask.CANCELLED
if task.can_cancel():
task.cancel()
path = row_reference.get_path()
# path isn't set if the item has already been removed from the list
# (to trigger this cancel one downloads in the active list, cancel all
@ -1923,7 +1919,7 @@ class gPodder(BuilderWidget, dbus.service.Object):
return not treeview.is_rubber_banding_active()
if event is None or event.button == 3:
selected_tasks, can_queue, can_cancel, can_pause, can_remove, can_force = \
selected_tasks, can_force, can_queue, can_pause, can_cancel, can_remove = \
self.downloads_list_get_selection(model, paths)
@ -1944,12 +1940,13 @@ class gPodder(BuilderWidget, dbus.service.Object):
selected_tasks, download.DownloadTask.QUEUED, force_start=True)
else:
item = make_menu_item(_('Download'), 'setDownloadQueued',
selected_tasks, download.DownloadTask.QUEUED)
selected_tasks, download.DownloadTask.QUEUED, can_queue)
menu.append_item(item)
menu.append_item(make_menu_item(_('Cancel'), 'setDownloadCancelled',
selected_tasks, download.DownloadTask.CANCELLING))
menu.append_item(make_menu_item(_('Pause'), 'setDownloadPaused',
selected_tasks, download.DownloadTask.PAUSING))
menu.append_item(make_menu_item(_('Pause'), 'media-playback-pause',
selected_tasks,
download.DownloadTask.PAUSING, can_pause))
rmenu = Gio.Menu()
rmenu.append_item(make_menu_item(_('Remove from list'), 'setDownloadRemove',
selected_tasks, sensitive=can_remove))
@ -2434,32 +2431,61 @@ class gPodder(BuilderWidget, dbus.service.Object):
self.episode_list_status_changed(episodes)
def play_or_download(self, current_page=None):
(open_instead_of_play, can_play, can_download, can_pause, can_cancel, can_delete) = (False,) * 6
# if current_page is None:
# current_page = self.wNotebook.get_current_page()
# if current_page == 0:
if True:
(open_instead_of_play, can_play, can_download, can_pause, can_cancel, can_delete) = (False,) * 6
selection = self.treeAvailable.get_selection()
if selection.count_selected_rows() > 0:
(model, paths) = selection.get_selected_rows()
selection = self.treeAvailable.get_selection()
if selection.count_selected_rows() > 0:
(model, paths) = selection.get_selected_rows()
for path in paths:
try:
episode = model.get_value(model.get_iter(path), EpisodeListModel.C_EPISODE)
if episode is None:
logger.info('Invalid episode at path %s', str(path))
for path in paths:
try:
episode = model.get_value(model.get_iter(path), EpisodeListModel.C_EPISODE)
if episode is None:
logger.info('Invalid episode at path %s', str(path))
continue
except TypeError as te:
logger.error('Invalid episode at path %s', str(path))
continue
except TypeError as te:
logger.error('Invalid episode at path %s', str(path))
continue
open_instead_of_play = open_instead_of_play or episode.file_type() not in ('audio', 'video')
can_play = can_play or episode.can_play(self.config)
can_download = can_download or episode.can_download()
can_pause = can_pause or episode.can_pause()
can_cancel = can_cancel or episode.can_cancel()
can_delete = can_delete or episode.can_delete()
open_instead_of_play = open_instead_of_play or episode.file_type() not in ('audio', 'video')
can_play = can_play or episode.can_play(self.config)
can_download = can_download or episode.can_download()
can_pause = can_pause or episode.can_pause()
can_cancel = can_cancel or episode.can_cancel()
can_delete = can_delete or episode.can_delete()
self.set_episode_actions(open_instead_of_play, can_play, can_download, can_pause, can_cancel, can_delete)
self.set_episode_actions(open_instead_of_play, can_play, can_download, can_pause, can_cancel, can_delete)
return (open_instead_of_play, can_play, can_download, can_pause, can_cancel, can_delete)
return (open_instead_of_play, can_play, can_download, can_pause, can_cancel, can_delete)
else:
(can_queue, can_pause, can_cancel, can_remove) = (False,) * 4
selection = self.treeDownloads.get_selection()
if selection.count_selected_rows() > 0:
(model, paths) = selection.get_selected_rows()
for path in paths:
try:
task = model.get_value(model.get_iter(path), 0)
if task is None:
logger.info('Invalid task at path %s', str(path))
continue
except TypeError as te:
logger.error('Invalid task at path %s', str(path))
continue
can_queue = can_queue or task.can_queue()
can_pause = can_pause or task.can_pause()
can_cancel = can_cancel or task.can_cancel()
can_remove = can_remove or task.can_remove()
self.set_episode_actions(False, False, can_queue, can_pause, can_cancel, can_remove)
return (False, False, can_queue, can_pause, can_cancel, can_remove)
def on_cbMaxDownloads_toggled(self, widget, *args):
self.spinMaxDownloads.set_sensitive(self.cbMaxDownloads.get_active())
@ -2614,7 +2640,7 @@ class gPodder(BuilderWidget, dbus.service.Object):
self.treeAvailable.scroll_to_point(0, 0)
descriptions = self.config.episode_list_descriptions
with self.treeAvailable.get_selection().handler_block(self.selection_handler_id):
with self.treeAvailable.get_selection().handler_block(self.episode_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
@ -3086,6 +3112,15 @@ class gPodder(BuilderWidget, dbus.service.Object):
return '\n'.join(titles) + '\n\n' + message
def delete_episode_list(self, episodes, confirm=True, callback=None):
# if self.wNotebook.get_current_page() > 0:
# selection = self.treeDownloads.get_selection()
# (model, paths) = selection.get_selected_rows()
# selected_tasks = [(Gtk.TreeRowReference.new(model, path),
# model.get_value(model.get_iter(path),
# DownloadStatusModel.C_TASK)) for path in paths]
# self._for_each_task_set_status(selected_tasks, status=None, force_start=False)
# return
#
if not episodes:
return False
@ -3910,7 +3945,6 @@ class gPodder(BuilderWidget, dbus.service.Object):
for episode in self.get_selected_episodes():
if episode.can_pause():
episode.download_task.pause()
self.update_downloads_list()
def on_episode_download_clicked(self, button):

View File

@ -485,20 +485,20 @@ class PodcastEpisode(PodcastModelObject):
"""
return not self.was_downloaded(and_exists=True) and (
not self.download_task
or self.download_task.status in (self.download_task.PAUSING, self.download_task.PAUSED, self.download_task.FAILED))
or self.download_task.can_queue()
or self.download_task.status == self.download_task.PAUSING)
def can_pause(self):
"""
gPodder.on_pause_selected_episodes() filters selection with this method.
"""
return self.download_task and self.download_task.status in (self.download_task.QUEUED, self.download_task.DOWNLOADING)
return self.download_task and self.download_task.can_pause()
def can_cancel(self):
"""
DownloadTask.cancel() only cancels the following tasks.
"""
return self.download_task and self.download_task.status in \
(self.download_task.DOWNLOADING, self.download_task.QUEUED, self.download_task.PAUSED, self.download_task.FAILED)
return self.download_task and self.download_task.can_cancel()
def can_delete(self):
"""

View File

@ -779,6 +779,12 @@ class SyncTask(download.DownloadTask):
episode = property(fget=__get_episode)
def can_queue(self):
return self.status in (self.CANCELLED, self.PAUSED, self.FAILED)
def can_pause(self):
return self.status in (self.DOWNLOADING, self.QUEUED)
def pause(self):
with self:
# Pause a queued download
@ -788,6 +794,9 @@ class SyncTask(download.DownloadTask):
elif self.status == self.DOWNLOADING:
self.status = self.PAUSING
def can_cancel(self):
return self.status in (self.DOWNLOADING, self.QUEUED, self.PAUSED, self.FAILED)
def cancel(self):
with self:
# Cancelling directly is allowed if the task isn't currently downloading
@ -801,6 +810,9 @@ class SyncTask(download.DownloadTask):
self.status = self.CANCELLING
self.device.cancel()
def can_remove(self):
return self.status in (self.CANCELLED, self.FAILED, self.DONE)
def removed_from_list(self):
if self.status != self.DONE:
self.device.cleanup_task(self)