Merge tag '3.11.1' into dev-adaptive
gPodder 3.11.1 release
This commit is contained in:
commit
4539d8c5e4
|
@ -15,9 +15,9 @@ jobs:
|
|||
python-version: ['3.10']
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v3
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
uses: actions/setup-python@v2
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
- name: Install dependencies
|
||||
|
|
2
bin/gpo
2
bin/gpo
|
@ -628,6 +628,7 @@ class gPodderCli(object):
|
|||
show_guid = '--guid' in args
|
||||
|
||||
common.find_partial_downloads(self._model.get_podcasts(),
|
||||
noop,
|
||||
noop,
|
||||
noop,
|
||||
on_finish)
|
||||
|
@ -687,6 +688,7 @@ class gPodderCli(object):
|
|||
self._download_episodes(episodes)
|
||||
|
||||
common.find_partial_downloads(self._model.get_podcasts(),
|
||||
noop,
|
||||
noop,
|
||||
noop,
|
||||
on_finish)
|
||||
|
|
979
po/cs_CZ.po
979
po/cs_CZ.po
File diff suppressed because it is too large
Load Diff
979
po/es_ES.po
979
po/es_ES.po
File diff suppressed because it is too large
Load Diff
979
po/es_MX.po
979
po/es_MX.po
File diff suppressed because it is too large
Load Diff
963
po/fa_IR.po
963
po/fa_IR.po
File diff suppressed because it is too large
Load Diff
961
po/id_ID.po
961
po/id_ID.po
File diff suppressed because it is too large
Load Diff
979
po/ko_KR.po
979
po/ko_KR.po
File diff suppressed because it is too large
Load Diff
961
po/messages.pot
961
po/messages.pot
File diff suppressed because it is too large
Load Diff
979
po/pt_BR.po
979
po/pt_BR.po
File diff suppressed because it is too large
Load Diff
979
po/zh_CN.po
979
po/zh_CN.po
File diff suppressed because it is too large
Load Diff
|
@ -9,7 +9,6 @@ import time
|
|||
|
||||
import gpodder
|
||||
from gpodder import util
|
||||
from gpodder.gtkui.interface.progress import ProgressIndicator
|
||||
from gpodder.model import PodcastEpisode
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
@ -55,32 +54,30 @@ class gPodderExtension:
|
|||
return [(_("Rename all downloaded episodes"), self.rename_all_downloaded_episodes)]
|
||||
|
||||
def rename_all_downloaded_episodes(self):
|
||||
model = self.gpodder.episode_list_model
|
||||
episodes = [row[model.C_EPISODE] for row in model if row[model.C_EPISODE].state == gpodder.STATE_DOWNLOADED]
|
||||
|
||||
episodes = [e for c in self.gpodder.channels for e in [e for e in c.children if e.state == gpodder.STATE_DOWNLOADED]]
|
||||
number_of_episodes = len(episodes)
|
||||
if number_of_episodes == 0:
|
||||
self.gpodder.show_message(_('No downloaded episodes to rename'),
|
||||
_('Rename all downloaded episodes'), important=True)
|
||||
|
||||
from gpodder.gtkui.interface.progress import ProgressIndicator
|
||||
|
||||
progress_indicator = ProgressIndicator(
|
||||
_('Renaming all downloaded episodes'),
|
||||
'', True, self.gpodder.get_dialog_parent())
|
||||
progress_indicator.on_message('0 / %d' % number_of_episodes)
|
||||
'', True, self.gpodder.get_dialog_parent(), number_of_episodes)
|
||||
|
||||
renamed_count = 0
|
||||
for episode in episodes:
|
||||
self.on_episode_downloaded(episode)
|
||||
|
||||
renamed_count += 1
|
||||
progress_indicator.on_message('%d / %d' % (renamed_count, number_of_episodes))
|
||||
progress_indicator.on_progress(renamed_count / number_of_episodes)
|
||||
if time.time() >= progress_indicator.next_update:
|
||||
progress_indicator.update_gui()
|
||||
self.gpodder.force_ui_update()
|
||||
if not progress_indicator.cancellable:
|
||||
break
|
||||
if not progress_indicator.on_tick():
|
||||
break
|
||||
renamed_count = progress_indicator.tick_counter
|
||||
|
||||
progress_indicator.on_finished()
|
||||
|
||||
self.gpodder.show_message(_('Renamed %(count)d downloaded episodes') % {'count': number_of_episodes},
|
||||
_('Renaming finished'), important=True)
|
||||
if renamed_count > 0:
|
||||
self.gpodder.show_message(_('Renamed %(count)d downloaded episodes') % {'count': renamed_count},
|
||||
_('Rename all downloaded episodes'), important=True)
|
||||
|
||||
def make_filename(self, current_filename, title, sortdate, podcast_title):
|
||||
dirname = os.path.dirname(current_filename)
|
||||
|
|
|
@ -13,11 +13,11 @@ import time
|
|||
try:
|
||||
import yt_dlp as youtube_dl
|
||||
program_name = 'yt-dlp'
|
||||
want_ytdl_version = '2021.02.04'
|
||||
want_ytdl_version = '2023.02.17'
|
||||
except:
|
||||
import youtube_dl
|
||||
program_name = 'youtube-dl'
|
||||
want_ytdl_version = '2021.02.04'
|
||||
want_ytdl_version = '2023.02.17' # youtube-dl has been patched, but not yet released
|
||||
|
||||
import gpodder
|
||||
from gpodder import download, feedcore, model, registry, util, youtube
|
||||
|
|
|
@ -512,7 +512,7 @@
|
|||
<child>
|
||||
<object class="GtkFlowBoxChild">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<child>
|
||||
<object class="GtkLabel" id="label_episode_limit">
|
||||
<property name="visible">True</property>
|
||||
|
@ -527,7 +527,7 @@
|
|||
<child>
|
||||
<object class="GtkFlowBoxChild">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<child>
|
||||
<object class="GtkSpinButton" id="spinbutton_episode_limit">
|
||||
<property name="visible">True</property>
|
||||
|
@ -558,6 +558,21 @@
|
|||
<property name="position">3</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkCheckButton" id="checkbutton_only_added_are_new">
|
||||
<property name="label" translatable="yes">Consider only episodes added in the update as new</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">True</property>
|
||||
<property name="receives-default">False</property>
|
||||
<property name="margin-bottom">5</property>
|
||||
<property name="draw-indicator">True</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">4</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkFlowBox">
|
||||
<property name="visible">True</property>
|
||||
|
@ -568,7 +583,7 @@
|
|||
<child>
|
||||
<object class="GtkFlowBoxChild">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<child>
|
||||
<object class="GtkLabel" id="label_auto_download">
|
||||
<property name="visible">True</property>
|
||||
|
@ -583,7 +598,7 @@
|
|||
<child>
|
||||
<object class="GtkFlowBoxChild">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<child>
|
||||
<object class="GtkComboBox" id="combo_auto_download">
|
||||
<property name="visible">True</property>
|
||||
|
@ -597,7 +612,7 @@
|
|||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">4</property>
|
||||
<property name="position">5</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
|
@ -739,7 +754,7 @@
|
|||
<child>
|
||||
<object class="GtkFlowBoxChild">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<child>
|
||||
<object class="GtkLabel" id="label_device_type">
|
||||
<property name="visible">True</property>
|
||||
|
@ -753,7 +768,7 @@
|
|||
<child>
|
||||
<object class="GtkFlowBoxChild">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<child>
|
||||
<object class="GtkComboBox" id="combobox_device_type">
|
||||
<property name="visible">True</property>
|
||||
|
@ -781,7 +796,7 @@
|
|||
<child>
|
||||
<object class="GtkFlowBoxChild">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<child>
|
||||
<object class="GtkLabel" id="label_device_mount">
|
||||
<property name="visible">True</property>
|
||||
|
@ -795,7 +810,7 @@
|
|||
<child>
|
||||
<object class="GtkFlowBoxChild">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<child>
|
||||
<object class="GtkButton" id="btn_filesystemMountpoint">
|
||||
<property name="visible">True</property>
|
||||
|
@ -851,7 +866,7 @@
|
|||
<child>
|
||||
<object class="GtkFlowBoxChild">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<child>
|
||||
<object class="GtkLabel" id="label_device_playlists">
|
||||
<property name="visible">True</property>
|
||||
|
@ -865,7 +880,7 @@
|
|||
<child>
|
||||
<object class="GtkFlowBoxChild">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<child>
|
||||
<object class="GtkButton" id="btn_playlistfolder">
|
||||
<property name="visible">True</property>
|
||||
|
@ -920,7 +935,7 @@
|
|||
<child>
|
||||
<object class="GtkFlowBoxChild">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<child>
|
||||
<object class="GtkLabel" id="label_on_sync">
|
||||
<property name="visible">True</property>
|
||||
|
@ -934,7 +949,7 @@
|
|||
<child>
|
||||
<object class="GtkFlowBoxChild">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<child>
|
||||
<object class="GtkComboBox" id="combobox_on_sync">
|
||||
<property name="visible">True</property>
|
||||
|
|
|
@ -149,6 +149,10 @@
|
|||
<attribute name="action">win.openEpisodeDownloadFolder</attribute>
|
||||
<attribute name="label" translatable="yes">Open download folder</attribute>
|
||||
</item>
|
||||
<item>
|
||||
<attribute name="action">win.selectChannel</attribute>
|
||||
<attribute name="label" translatable="yes">Select channel</attribute>
|
||||
</item>
|
||||
</section>
|
||||
<section>
|
||||
<item>
|
||||
|
@ -228,6 +232,10 @@
|
|||
<attribute name="action">win.viewAlwaysShowNewEpisodes</attribute>
|
||||
<attribute name="label" translatable="yes">Always show New Episodes</attribute>
|
||||
</item>
|
||||
<item>
|
||||
<attribute name="action">win.viewTrimEpisodeTitlePrefix</attribute>
|
||||
<attribute name="label" translatable="yes">Trim episode title prefix</attribute>
|
||||
</item>
|
||||
<item>
|
||||
<attribute name="action">win.viewShowEpisodeDescription</attribute>
|
||||
<attribute name="label" translatable="yes">Episode descriptions</attribute>
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
.TH GPO "1" "July 2022" "gpodder 3.11.0" "User Commands"
|
||||
.TH GPO "1" "February 2023" "gpodder 3.11.1" "User Commands"
|
||||
.SH NAME
|
||||
gpo \- Text mode interface of gPodder
|
||||
.SH SYNOPSIS
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
.\" DO NOT MODIFY THIS FILE! It was generated by help2man 1.48.5.
|
||||
.TH GPODDER "1" "July 2022" "gpodder 3.11.0" "User Commands"
|
||||
.TH GPODDER "1" "February 2023" "gpodder 3.11.1" "User Commands"
|
||||
.SH NAME
|
||||
gpodder \- Media aggregator and podcast client
|
||||
.SH SYNOPSIS
|
||||
|
|
|
@ -20,9 +20,9 @@
|
|||
# This metadata block gets parsed by setup.py - use single quotes only
|
||||
__tagline__ = 'Media aggregator and podcast client'
|
||||
__author__ = 'Thomas Perl <thp@gpodder.org>'
|
||||
__version__ = '3.11.0+1'
|
||||
__date__ = '2022-08-30'
|
||||
__copyright__ = '© 2005-2022 The gPodder Team'
|
||||
__version__ = '3.11.1'
|
||||
__date__ = '2023-02-17'
|
||||
__copyright__ = '© 2005-2023 The gPodder Team'
|
||||
__license__ = 'GNU General Public License, version 3 or later'
|
||||
__url__ = 'http://gpodder.org/'
|
||||
|
||||
|
|
|
@ -47,7 +47,7 @@ def clean_up_downloads(delete_partial=False):
|
|||
util.delete_file(tempfile)
|
||||
|
||||
|
||||
def find_partial_downloads(channels, start_progress_callback, progress_callback, finish_progress_callback):
|
||||
def find_partial_downloads(channels, start_progress_callback, progress_callback, final_progress_callback, finish_progress_callback):
|
||||
"""Find partial downloads and match them with episodes
|
||||
|
||||
channels - A list of all model.PodcastChannel objects
|
||||
|
@ -86,6 +86,8 @@ def find_partial_downloads(channels, start_progress_callback, progress_callback,
|
|||
if not candidates:
|
||||
break
|
||||
|
||||
final_progress_callback()
|
||||
|
||||
for f in partial_files:
|
||||
logger.warning('Partial file without episode: %s', f)
|
||||
util.delete_file(f)
|
||||
|
|
|
@ -154,6 +154,7 @@ defaults = {
|
|||
|
||||
'toolbar': False,
|
||||
'new_episodes': 'show', # ignore, show, queue, download
|
||||
'only_added_are_new': False, # Only just added episodes are considered new after an update
|
||||
'live_search_delay': 200,
|
||||
'search_always_visible': False,
|
||||
'find_as_you_type': True,
|
||||
|
@ -168,6 +169,7 @@ defaults = {
|
|||
'episode_list': {
|
||||
'view_mode': 1,
|
||||
'always_show_new': True,
|
||||
'trim_title_prefix': True,
|
||||
'descriptions': True,
|
||||
'ctrl_click_to_sort': False,
|
||||
'columns': int('110', 2), # bitfield of visible columns
|
||||
|
@ -206,6 +208,7 @@ defaults = {
|
|||
'two_way_sync': False,
|
||||
'use_absolute_path': True,
|
||||
'folder': 'Playlists',
|
||||
'extension': 'm3u',
|
||||
}
|
||||
|
||||
},
|
||||
|
|
|
@ -44,6 +44,7 @@ class CoverDownloader(object):
|
|||
'.jpg': lambda d: d.startswith(b'\xff\xd8'),
|
||||
'.gif': lambda d: d.startswith(b'GIF89a') or d.startswith(b'GIF87a'),
|
||||
'.ico': lambda d: d.startswith(b'\0\0\1\0'),
|
||||
'.svg': lambda d: d.startswith(b'<svg '),
|
||||
}
|
||||
|
||||
EXTENSIONS = list(SUPPORTED_EXTENSIONS.keys())
|
||||
|
|
|
@ -39,7 +39,8 @@ class gPodderDevicePlaylist(object):
|
|||
def __init__(self, config, playlist_name):
|
||||
self._config = config
|
||||
self.linebreak = '\r\n'
|
||||
self.playlist_file = util.sanitize_filename(playlist_name, self._config.device_sync.max_filename_length) + '.m3u'
|
||||
self.playlist_file = (util.sanitize_filename(playlist_name, self._config.device_sync.max_filename_length) +
|
||||
'.' + self._config.device_sync.playlists.extension)
|
||||
device_folder = util.new_gio_file(self._config.device_sync.device_folder)
|
||||
self.playlist_folder = device_folder.resolve_relative_path(self._config.device_sync.playlists.folder)
|
||||
|
||||
|
@ -101,7 +102,7 @@ class gPodderDevicePlaylist(object):
|
|||
foldername = episode_foldername_on_device(self._config, episode)
|
||||
if foldername:
|
||||
filename = os.path.join(foldername, filename)
|
||||
if self._config.device_sync.playlist.absolute_path:
|
||||
if self._config.device_sync.playlists.use_absolute_path:
|
||||
filename = os.path.join(util.relpath(self._config.device_sync.device_folder, self.mountpoint.get_uri()), filename)
|
||||
return filename
|
||||
|
||||
|
|
|
@ -412,7 +412,7 @@ class DownloadQueueWorker(object):
|
|||
if not self.continue_check_callback(self):
|
||||
return
|
||||
|
||||
task = self.queue.get_next()
|
||||
task = self.queue.get_next() if self.queue.enabled else None
|
||||
if not task:
|
||||
logger.info('No more tasks for %s to carry out.', self)
|
||||
break
|
||||
|
@ -445,6 +445,13 @@ class DownloadQueueManager(object):
|
|||
self.worker_threads_access = threading.RLock()
|
||||
self.worker_threads = []
|
||||
|
||||
def disable(self):
|
||||
self.tasks.enabled = False
|
||||
|
||||
def enable(self):
|
||||
self.tasks.enabled = True
|
||||
self.__spawn_threads()
|
||||
|
||||
def __exit_callback(self, worker_thread):
|
||||
with self.worker_threads_access:
|
||||
self.worker_threads.remove(worker_thread)
|
||||
|
@ -461,6 +468,9 @@ class DownloadQueueManager(object):
|
|||
def __spawn_threads(self):
|
||||
"""Spawn new worker threads if necessary
|
||||
"""
|
||||
if not self.tasks.enabled:
|
||||
return
|
||||
|
||||
with self.worker_threads_access:
|
||||
work_count = self.tasks.available_work_count()
|
||||
if self._config.limit.downloads.enabled:
|
||||
|
@ -974,7 +984,7 @@ class DownloadTask(object):
|
|||
except ConnectionError as ce:
|
||||
# special case request exception
|
||||
result = DownloadTask.FAILED
|
||||
logger.error('Download failed: %s', str(ce), exc_info=True)
|
||||
logger.error('Download failed: %s', str(ce))
|
||||
d = {'host': ce.args[0].pool.host, 'port': ce.args[0].pool.port}
|
||||
self.error_message = _("Couldn't connect to server %(host)s:%(port)s" % d)
|
||||
except RequestException as re:
|
||||
|
@ -982,20 +992,19 @@ class DownloadTask(object):
|
|||
if isinstance(re.args[0], MaxRetryError):
|
||||
re = re.args[0]
|
||||
logger.error('%s while downloading "%s"', str(re),
|
||||
self.__episode.title, exc_info=True)
|
||||
self.__episode.title)
|
||||
result = DownloadTask.FAILED
|
||||
d = {'error': str(re)}
|
||||
self.error_message = _('Request Error: %(error)s') % d
|
||||
except IOError as ioe:
|
||||
logger.error('%s while downloading "%s": %s', ioe.strerror,
|
||||
self.__episode.title, ioe.filename, exc_info=True)
|
||||
self.__episode.title, ioe.filename)
|
||||
result = DownloadTask.FAILED
|
||||
d = {'error': ioe.strerror, 'filename': ioe.filename}
|
||||
self.error_message = _('I/O Error: %(error)s: %(filename)s') % d
|
||||
except gPodderDownloadHTTPError as gdhe:
|
||||
logger.error('HTTP %s while downloading "%s": %s',
|
||||
gdhe.error_code, self.__episode.title, gdhe.error_message,
|
||||
exc_info=True)
|
||||
gdhe.error_code, self.__episode.title, gdhe.error_message)
|
||||
result = DownloadTask.FAILED
|
||||
d = {'code': gdhe.error_code, 'message': gdhe.error_message}
|
||||
self.error_message = _('HTTP Error %(code)s: %(message)s') % d
|
||||
|
|
|
@ -257,6 +257,9 @@ class gPodderPreferences(BuilderWidget):
|
|||
|
||||
self._config.connect_gtk_spinbutton('limit.episodes', self.spinbutton_episode_limit)
|
||||
|
||||
self._config.connect_gtk_togglebutton('ui.gtk.only_added_are_new',
|
||||
self.checkbutton_only_added_are_new)
|
||||
|
||||
self.auto_download_model = NewEpisodeActionList(self._config)
|
||||
self.combo_auto_download.set_model(self.auto_download_model)
|
||||
cellrenderer = Gtk.CellRendererText()
|
||||
|
|
|
@ -72,6 +72,8 @@ class DownloadStatusModel(Gtk.ListStore):
|
|||
self._status_ids[download.DownloadTask.PAUSING] = 'media-playback-pause'
|
||||
self._status_ids[download.DownloadTask.PAUSED] = 'media-playback-pause'
|
||||
|
||||
self.enabled = True
|
||||
|
||||
def _format_message(self, episode, message, podcast):
|
||||
episode = html.escape(episode)
|
||||
podcast = html.escape(podcast)
|
||||
|
@ -193,10 +195,7 @@ class DownloadStatusModel(Gtk.ListStore):
|
|||
# as only the main thread is allowed to manipulate the list store.
|
||||
def get_next(self):
|
||||
dqr = DequeueRequest()
|
||||
# this can not be idle_add because update_downloads_list() is called from a higher
|
||||
# priority timeout_add and would spin forever, never calling this.
|
||||
from gi.repository import GLib
|
||||
GLib.timeout_add(0, self.__get_next, dqr)
|
||||
util.idle_add(self.__get_next, dqr)
|
||||
return dqr.dequeue()
|
||||
|
||||
def _work_gen(self):
|
||||
|
|
|
@ -22,6 +22,7 @@ import time
|
|||
from gi.repository import GLib, Gtk, Pango
|
||||
|
||||
import gpodder
|
||||
from gpodder import util
|
||||
from gpodder.gtkui.widgets import SpinningProgressIndicator
|
||||
|
||||
_ = gpodder.gettext
|
||||
|
@ -32,14 +33,15 @@ class ProgressIndicator(object):
|
|||
DELAY = 500
|
||||
|
||||
# Time between GUI updates after window creation
|
||||
INTERVAL = 100
|
||||
INTERVAL = 250
|
||||
|
||||
def __init__(self, title, subtitle=None, cancellable=False, parent=None):
|
||||
def __init__(self, title, subtitle=None, cancellable=False, parent=None, max_ticks=None):
|
||||
self.title = title
|
||||
self.subtitle = subtitle
|
||||
self.cancellable = True if cancellable else False
|
||||
self.cancel_callback = cancellable
|
||||
self.cancel_id = 0
|
||||
self.cancelled = False
|
||||
self.next_update = time.time() + (self.DELAY / 1000)
|
||||
self.parent = parent
|
||||
self.dialog = None
|
||||
|
@ -48,12 +50,22 @@ class ProgressIndicator(object):
|
|||
self._initial_message = None
|
||||
self._initial_progress = None
|
||||
self._progress_set = False
|
||||
# use timeout_add, not util.idle_timeout_add, so it updates before Gtk+ redraws the dialog
|
||||
self.source_id = GLib.timeout_add(self.DELAY, self._create_progress)
|
||||
|
||||
self.set_max_ticks(max_ticks)
|
||||
|
||||
def set_max_ticks(self, max_ticks):
|
||||
self.max_ticks = max_ticks
|
||||
self.tick_counter = 0
|
||||
if max_ticks is not None:
|
||||
self.on_message('0 / %d' % max_ticks)
|
||||
|
||||
def _on_delete_event(self, window, event):
|
||||
if self.cancellable:
|
||||
self.dialog.response(Gtk.ResponseType.CANCEL)
|
||||
self.cancellable = False
|
||||
self.cancelled = True
|
||||
return True
|
||||
|
||||
def _create_progress(self):
|
||||
|
@ -64,6 +76,7 @@ class ProgressIndicator(object):
|
|||
if self.cancellable:
|
||||
def cancel_callback(dialog, response):
|
||||
self.cancellable = False
|
||||
self.cancelled = True
|
||||
self.dialog.set_deletable(False)
|
||||
self.dialog.set_response_sensitive(Gtk.ResponseType.CANCEL, False)
|
||||
if callable(self.cancel_callback):
|
||||
|
@ -78,8 +91,7 @@ class ProgressIndicator(object):
|
|||
if isinstance(label, Gtk.Label):
|
||||
label.set_selectable(False)
|
||||
|
||||
self.dialog.set_response_sensitive(Gtk.ResponseType.CANCEL,
|
||||
self.cancellable)
|
||||
self.dialog.set_response_sensitive(Gtk.ResponseType.CANCEL, self.cancellable)
|
||||
|
||||
self.progressbar = Gtk.ProgressBar()
|
||||
self.progressbar.set_show_text(True)
|
||||
|
@ -100,6 +112,7 @@ class ProgressIndicator(object):
|
|||
self._update_gui()
|
||||
|
||||
# previous self.source_id timeout is removed when this returns False
|
||||
# use timeout_add, not util.idle_timeout_add, so it updates before Gtk+ redraws the dialog
|
||||
self.source_id = GLib.timeout_add(self.INTERVAL, self._update_gui)
|
||||
return False
|
||||
|
||||
|
@ -111,13 +124,6 @@ class ProgressIndicator(object):
|
|||
self.next_update = time.time() + (self.INTERVAL / 1000)
|
||||
return True
|
||||
|
||||
def update_gui(self):
|
||||
if self.dialog:
|
||||
if self.source_id:
|
||||
GLib.source_remove(self.source_id)
|
||||
self.source_id = 0
|
||||
self._update_gui()
|
||||
|
||||
def on_message(self, message):
|
||||
if self.progressbar:
|
||||
self.progressbar.set_text(message)
|
||||
|
@ -131,6 +137,39 @@ class ProgressIndicator(object):
|
|||
else:
|
||||
self._initial_progress = progress
|
||||
|
||||
def on_tick(self, final=False):
|
||||
if final:
|
||||
# Dialog is no longer cancellable
|
||||
self.cancellable = False
|
||||
if self.dialog is not None:
|
||||
self.dialog.set_response_sensitive(Gtk.ResponseType.CANCEL, False)
|
||||
self.dialog.set_deletable(False)
|
||||
elif 2 * (time.time() - (self.next_update - (self.DELAY / 1000))) > (self.DELAY / 1000):
|
||||
# Assume final operation will take as long as all ticks and open dialog
|
||||
if self.source_id:
|
||||
GLib.source_remove(self.source_id)
|
||||
self._create_progress()
|
||||
|
||||
if self.max_ticks is not None and not final:
|
||||
self.tick_counter += 1
|
||||
|
||||
if time.time() >= self.next_update or (final and self.dialog):
|
||||
if type(final) == str:
|
||||
self.on_message(final)
|
||||
self.on_progress(1.0)
|
||||
elif self.max_ticks is not None:
|
||||
self.on_message('%d / %d' % (self.tick_counter, self.max_ticks))
|
||||
self.on_progress(self.tick_counter / self.max_ticks)
|
||||
|
||||
# Allow UI to redraw.
|
||||
util.idle_add(Gtk.main_quit)
|
||||
# self._create_progress() or self._update_gui() is called by a timer to update the dialog
|
||||
Gtk.main()
|
||||
|
||||
if self.cancelled:
|
||||
return False
|
||||
return True
|
||||
|
||||
def on_finished(self):
|
||||
if self.dialog is not None:
|
||||
if self.cancel_id:
|
||||
|
|
|
@ -51,6 +51,7 @@ class SearchTree:
|
|||
if self.search_box.get_property('visible'):
|
||||
if self._search_timeout is not None:
|
||||
GLib.source_remove(self._search_timeout)
|
||||
# use timeout_add, not util.idle_timeout_add, so it updates the TreeView before background tasks
|
||||
self._search_timeout = GLib.timeout_add(
|
||||
self.config.ui.gtk.live_search_delay,
|
||||
self.set_search_term, editable.get_chars(0, -1))
|
||||
|
|
|
@ -104,6 +104,8 @@ class gPodder(BuilderWidget, dbus.service.Object):
|
|||
self.last_episode_date_refresh = None
|
||||
self.refresh_episode_dates()
|
||||
|
||||
self.on_episode_list_selection_changed_id = None
|
||||
|
||||
def new(self):
|
||||
if self.application.want_headerbar:
|
||||
# Plus menu button
|
||||
|
@ -252,7 +254,7 @@ class gPodder(BuilderWidget, dbus.service.Object):
|
|||
self.leaflet.set_visible_child(self.contentbox)
|
||||
|
||||
self.download_tasks_seen = set()
|
||||
self.download_list_update_enabled = False
|
||||
self.download_list_update_timer = None
|
||||
self.things_adding_tasks = 0
|
||||
self.download_task_monitors = set()
|
||||
|
||||
|
@ -419,6 +421,11 @@ class gPodder(BuilderWidget, dbus.service.Object):
|
|||
action.connect('activate', self.on_item_view_always_show_new_episodes_toggled)
|
||||
g.add_action(action)
|
||||
|
||||
action = Gio.SimpleAction.new_stateful(
|
||||
'viewTrimEpisodeTitlePrefix', None, GLib.Variant.new_boolean(self.config.ui.gtk.episode_list.trim_title_prefix))
|
||||
action.connect('activate', self.on_item_view_trim_episode_title_prefix_toggled)
|
||||
g.add_action(action)
|
||||
|
||||
action = Gio.SimpleAction.new_stateful(
|
||||
'viewShowEpisodeDescription', None, GLib.Variant.new_boolean(self.config.ui.gtk.episode_list.descriptions))
|
||||
action.connect('activate', self.on_item_view_show_episode_description_toggled)
|
||||
|
@ -458,6 +465,7 @@ class gPodder(BuilderWidget, dbus.service.Object):
|
|||
# ('toggleEpisodeLock', self.on_item_toggle_lock_activate),
|
||||
('openEpisodeDownloadFolder', self.on_open_episode_download_folder),
|
||||
('openChannelDownloadFolder', self.on_open_download_folder),
|
||||
('selectChannel', self.on_select_channel_of_episode),
|
||||
('findEpisode', self.on_find_episode_activate),
|
||||
('toggleShownotes', self.on_shownotes_selected_episodes),
|
||||
# Extras
|
||||
|
@ -489,6 +497,7 @@ class gPodder(BuilderWidget, dbus.service.Object):
|
|||
# self.toggle_episode_lock_action = g.lookup_action('toggleEpisodeLock')
|
||||
self.episode_new_action = g.lookup_action('episodeNew')
|
||||
self.open_episode_download_folder_action = g.lookup_action('openEpisodeDownloadFolder')
|
||||
self.select_channel_of_episode_action = g.lookup_action('selectChannel')
|
||||
# Extras
|
||||
self.auto_archive_action = g.lookup_action('channelAutoArchive')
|
||||
self.bluetooth_episodes_action = g.lookup_action('bluetoothEpisodes')
|
||||
|
@ -583,9 +592,10 @@ class gPodder(BuilderWidget, dbus.service.Object):
|
|||
def progress_callback(title, progress):
|
||||
self.partial_downloads_indicator.on_message(title)
|
||||
self.partial_downloads_indicator.on_progress(progress)
|
||||
if time.time() >= self.partial_downloads_indicator.next_update:
|
||||
self.partial_downloads_indicator.update_gui()
|
||||
self.force_ui_update()
|
||||
self.partial_downloads_indicator.on_tick() # not cancellable
|
||||
|
||||
def final_progress_callback():
|
||||
self.partial_downloads_indicator.on_tick(final=_('Cleaning up...'))
|
||||
|
||||
def finish_progress_callback(resumable_episodes):
|
||||
def offer_resuming():
|
||||
|
@ -605,6 +615,7 @@ class gPodder(BuilderWidget, dbus.service.Object):
|
|||
common.find_partial_downloads(self.channels,
|
||||
start_progress_callback,
|
||||
progress_callback,
|
||||
final_progress_callback,
|
||||
finish_progress_callback)
|
||||
|
||||
def episode_object_by_uri(self, uri):
|
||||
|
@ -1397,6 +1408,13 @@ class gPodder(BuilderWidget, dbus.service.Object):
|
|||
self._search_episodes.hide_search()
|
||||
|
||||
def on_episode_list_selection_changed(self, selection):
|
||||
# Only update the UI every 250ms to prevent lag when rapidly changing selected episode or shift-selecting episodes
|
||||
if self.on_episode_list_selection_changed_id is None:
|
||||
self.on_episode_list_selection_changed_id = util.idle_timeout_add(250, self._on_episode_list_selection_changed)
|
||||
|
||||
def _on_episode_list_selection_changed(self):
|
||||
self.on_episode_list_selection_changed_id = None
|
||||
|
||||
# Update the toolbar buttons
|
||||
self.play_or_download()
|
||||
# and the shownotes
|
||||
|
@ -1533,10 +1551,17 @@ class gPodder(BuilderWidget, dbus.service.Object):
|
|||
self.things_adding_tasks += 1
|
||||
elif state == gPodderSyncUI.DL_ADDED_TASKS:
|
||||
self.things_adding_tasks -= 1
|
||||
if not self.download_list_update_enabled:
|
||||
if self.download_list_update_timer is None:
|
||||
self.update_downloads_list()
|
||||
util.IdleTimeout(1500, self.update_downloads_list)
|
||||
self.download_list_update_enabled = True
|
||||
self.download_list_update_timer = util.IdleTimeout(1500, self.update_downloads_list).set_max_milliseconds(5000)
|
||||
|
||||
def stop_download_list_update_timer(self):
|
||||
if self.download_list_update_timer is None:
|
||||
return False
|
||||
|
||||
self.download_list_update_timer.cancel()
|
||||
self.download_list_update_timer = None
|
||||
return True
|
||||
|
||||
def cleanup_downloads(self):
|
||||
model = self.download_status_model
|
||||
|
@ -1700,7 +1725,7 @@ class gPodder(BuilderWidget, dbus.service.Object):
|
|||
self.cleanup_downloads()
|
||||
|
||||
# Stop updating the download list here
|
||||
self.download_list_update_enabled = False
|
||||
self.stop_download_list_update_timer()
|
||||
|
||||
self.gPodder.set_title(' - '.join(title))
|
||||
|
||||
|
@ -1709,7 +1734,7 @@ class gPodder(BuilderWidget, dbus.service.Object):
|
|||
if channel_urls:
|
||||
self.update_podcast_list_model(channel_urls)
|
||||
|
||||
return self.download_list_update_enabled
|
||||
return (self.download_list_update_timer is not None)
|
||||
except Exception as e:
|
||||
logger.error('Exception happened while updating download list.', exc_info=True)
|
||||
self.show_message(
|
||||
|
@ -1727,6 +1752,7 @@ class gPodder(BuilderWidget, dbus.service.Object):
|
|||
# self.toolbar.set_property('visible', new_value)
|
||||
pass
|
||||
elif name in ('ui.gtk.episode_list.descriptions',
|
||||
'ui.gtk.episode_list.trim_title_prefix',
|
||||
'ui.gtk.episode_list.always_show_new'):
|
||||
self.update_episode_list_model()
|
||||
elif name in ('auto.update.enabled', 'auto.update.frequency'):
|
||||
|
@ -2012,40 +2038,25 @@ class gPodder(BuilderWidget, dbus.service.Object):
|
|||
else:
|
||||
self.download_queue_manager.queue_task(task)
|
||||
|
||||
def force_ui_update(self):
|
||||
def callback():
|
||||
Gtk.main_quit()
|
||||
GLib.timeout_add(1, callback)
|
||||
Gtk.main()
|
||||
|
||||
def _for_each_task_set_status(self, tasks, status, force_start=False):
|
||||
count = len(tasks)
|
||||
if count:
|
||||
progress_indicator = ProgressIndicator(
|
||||
_('Queueing') if status == download.DownloadTask.QUEUED else
|
||||
_('Removing') if status is None else download.DownloadTask.STATUS_MESSAGE[status],
|
||||
'', True, self.get_dialog_parent())
|
||||
progress_indicator.on_message('0 / %d' % count)
|
||||
'', True, self.get_dialog_parent(), count)
|
||||
else:
|
||||
progress_indicator = None
|
||||
|
||||
def progress_callback(title, progress):
|
||||
progress_indicator.on_message(title)
|
||||
progress_indicator.on_progress(progress)
|
||||
if time.time() >= progress_indicator.next_update:
|
||||
progress_indicator.update_gui()
|
||||
self.force_ui_update()
|
||||
if not progress_indicator.cancellable:
|
||||
return False
|
||||
return True
|
||||
self.__for_each_task_set_status(tasks, status, force_start=force_start, progress_callback=progress_callback)
|
||||
restart_timer = self.stop_download_list_update_timer()
|
||||
self.download_queue_manager.disable()
|
||||
self.__for_each_task_set_status(tasks, status, force_start, progress_indicator, restart_timer)
|
||||
self.download_queue_manager.enable()
|
||||
|
||||
if progress_indicator:
|
||||
progress_indicator.on_finished()
|
||||
|
||||
def __for_each_task_set_status(self, tasks, status, force_start=False, progress_callback=None):
|
||||
count = len(tasks)
|
||||
n = 0
|
||||
def __for_each_task_set_status(self, tasks, status, force_start=False, progress_indicator=None, restart_timer=False):
|
||||
episode_urls = set()
|
||||
model = self.treeDownloads.get_model()
|
||||
has_queued_tasks = False
|
||||
|
@ -2095,13 +2106,14 @@ class gPodder(BuilderWidget, dbus.service.Object):
|
|||
else:
|
||||
# We can (hopefully) simply set the task status here
|
||||
task.status = status
|
||||
if progress_callback:
|
||||
n += 1
|
||||
if not progress_callback('%d / %d' % (n, count), n / count):
|
||||
if progress_indicator:
|
||||
if not progress_indicator.on_tick():
|
||||
break
|
||||
if progress_indicator:
|
||||
progress_indicator.on_tick(final=_('Updating...'))
|
||||
|
||||
# Update the tab title and downloads list
|
||||
if has_queued_tasks:
|
||||
if has_queued_tasks or restart_timer:
|
||||
self.set_download_list_state(gPodderSyncUI.DL_ONEOFF)
|
||||
else:
|
||||
self.update_downloads_list()
|
||||
|
@ -2156,9 +2168,18 @@ class gPodder(BuilderWidget, dbus.service.Object):
|
|||
assert len(episodes) == 1
|
||||
util.gui_open(episodes[0].parent.save_dir, gui=self)
|
||||
|
||||
def on_select_channel_of_episode(self, unused1=None, unused2=None):
|
||||
episodes = self.get_selected_episodes()
|
||||
assert len(episodes) == 1
|
||||
channel = episodes[0].parent
|
||||
# Focus channel list
|
||||
self.treeChannels.grab_focus()
|
||||
# Select channel in list
|
||||
path = self.podcast_list_model.get_filter_path_from_url(channel.url)
|
||||
self.treeChannels.set_cursor(path)
|
||||
|
||||
def treeview_channels_show_context_menu(self, event=None):
|
||||
treeview = self.treeChannels
|
||||
|
||||
model, paths = self.treeview_handle_context_menu_click(treeview, event)
|
||||
if not paths:
|
||||
return True
|
||||
|
@ -2465,6 +2486,7 @@ class gPodder(BuilderWidget, dbus.service.Object):
|
|||
# self.toggle_episode_lock_action.set_enabled(can_lock)
|
||||
self.episode_lock_action.set_enabled(can_play)
|
||||
self.open_episode_download_folder_action.set_enabled(len(episodes) == 1)
|
||||
self.select_channel_of_episode_action.set_enabled(len(episodes) == 1)
|
||||
|
||||
# Episodes context menu
|
||||
self.episode_new_action.set_enabled(is_episode_selected)
|
||||
|
@ -2698,7 +2720,7 @@ class gPodder(BuilderWidget, dbus.service.Object):
|
|||
if remaining_seconds > 3600:
|
||||
# timeout an hour early in the event daylight savings changes the clock forward
|
||||
remaining_seconds = remaining_seconds - 3600
|
||||
GLib.timeout_add(remaining_seconds * 1000, self.refresh_episode_dates)
|
||||
util.idle_timeout_add(remaining_seconds * 1000, self.refresh_episode_dates)
|
||||
|
||||
def update_podcast_list_model(self, urls=None, selected=False, select_url=None,
|
||||
sections_changed=False):
|
||||
|
@ -3115,6 +3137,7 @@ class gPodder(BuilderWidget, dbus.service.Object):
|
|||
def update_feed_cache_proc():
|
||||
updated_channels = []
|
||||
nr_update_errors = 0
|
||||
new_episodes = []
|
||||
for updated, channel in enumerate(channels):
|
||||
if self.feed_cache_update_cancelled:
|
||||
break
|
||||
|
@ -3128,7 +3151,7 @@ class gPodder(BuilderWidget, dbus.service.Object):
|
|||
try:
|
||||
channel._update_error = None
|
||||
util.idle_add(indicate_updating_podcast, channel)
|
||||
channel.update(max_episodes=self.config.limit.episodes)
|
||||
new_episodes.extend(channel.update(max_episodes=self.config.limit.episodes))
|
||||
self._update_cover(channel)
|
||||
except Exception as e:
|
||||
message = str(e)
|
||||
|
@ -3172,7 +3195,7 @@ class gPodder(BuilderWidget, dbus.service.Object):
|
|||
nr_update_errors) % {'count': nr_update_errors},
|
||||
_('Error while updating feeds'), widget=self.treeChannels)
|
||||
|
||||
def update_feed_cache_finish_callback():
|
||||
def update_feed_cache_finish_callback(new_episodes):
|
||||
# Process received episode actions for all updated URLs
|
||||
self.process_received_episode_actions()
|
||||
|
||||
|
@ -3185,9 +3208,11 @@ class gPodder(BuilderWidget, dbus.service.Object):
|
|||
# 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])
|
||||
# The filter extension can mark newly added episodes as old,
|
||||
# so take only episodes marked as new.
|
||||
episodes = ((e for e in new_episodes if e.check_is_new())
|
||||
if self.config.ui.gtk.only_added_are_new
|
||||
else self.get_new_episodes([c for c in updated_channels]))
|
||||
|
||||
if self.config.downloads.chronological_order:
|
||||
# download older episodes first
|
||||
|
@ -3246,7 +3271,7 @@ class gPodder(BuilderWidget, dbus.service.Object):
|
|||
|
||||
GLib.timeout_add(2000, hide_update)
|
||||
|
||||
util.idle_add(update_feed_cache_finish_callback)
|
||||
util.idle_add(update_feed_cache_finish_callback, new_episodes)
|
||||
|
||||
def on_gPodder_delete_event(self, *args):
|
||||
"""Called when the GUI wants to close the window
|
||||
|
@ -3550,34 +3575,44 @@ class gPodder(BuilderWidget, dbus.service.Object):
|
|||
self.download_episode_list(episodes, True, hide_progress=hide_progress)
|
||||
|
||||
def download_episode_list(self, episodes, add_paused=False, force_start=False, downloader=None, hide_progress=False):
|
||||
def queue_tasks(tasks, queued_existing_task):
|
||||
n = 0
|
||||
count = len(tasks)
|
||||
if count and not hide_progress:
|
||||
progress_indicator = ProgressIndicator(
|
||||
_('Queueing'),
|
||||
'', True, self.get_dialog_parent())
|
||||
progress_indicator.on_message('0 / %d' % count)
|
||||
else:
|
||||
progress_indicator = None
|
||||
# Start progress indicator to queue existing tasks
|
||||
count = len(episodes)
|
||||
if count and not hide_progress:
|
||||
progress_indicator = ProgressIndicator(
|
||||
_('Queueing'),
|
||||
'', True, self.get_dialog_parent(), count)
|
||||
else:
|
||||
progress_indicator = None
|
||||
|
||||
for task in tasks:
|
||||
with task:
|
||||
if add_paused:
|
||||
task.status = task.PAUSED
|
||||
else:
|
||||
self.mygpo_client.on_download([task.episode])
|
||||
self.queue_task(task, force_start)
|
||||
restart_timer = self.stop_download_list_update_timer()
|
||||
self.download_queue_manager.disable()
|
||||
|
||||
def queue_tasks(tasks, queued_existing_task):
|
||||
if progress_indicator is None or not progress_indicator.cancelled:
|
||||
if progress_indicator:
|
||||
n += 1
|
||||
progress_indicator.on_message('%d / %d' % (n, count))
|
||||
progress_indicator.on_progress(n / count)
|
||||
if time.time() >= progress_indicator.next_update:
|
||||
progress_indicator.update_gui()
|
||||
self.force_ui_update()
|
||||
if not progress_indicator.cancellable:
|
||||
count = len(tasks)
|
||||
if count:
|
||||
# Restart progress indicator to queue new tasks
|
||||
progress_indicator.set_max_ticks(count)
|
||||
progress_indicator.on_progress(0.0)
|
||||
|
||||
for task in tasks:
|
||||
with task:
|
||||
if add_paused:
|
||||
task.status = task.PAUSED
|
||||
else:
|
||||
self.mygpo_client.on_download([task.episode])
|
||||
self.queue_task(task, force_start)
|
||||
if progress_indicator:
|
||||
if not progress_indicator.on_tick():
|
||||
break
|
||||
if tasks or queued_existing_task:
|
||||
|
||||
if progress_indicator:
|
||||
progress_indicator.on_tick(final=_('Updating...'))
|
||||
self.download_queue_manager.enable()
|
||||
|
||||
# Update the tab title and downloads list
|
||||
if tasks or queued_existing_task or restart_timer:
|
||||
self.set_download_list_state(gPodderSyncUI.DL_ONEOFF)
|
||||
# Flush updated episode status
|
||||
if self.mygpo_client.can_access_webservice():
|
||||
|
@ -3594,6 +3629,11 @@ class gPodder(BuilderWidget, dbus.service.Object):
|
|||
episodes = list(Model.sort_episodes_by_pubdate(episodes))
|
||||
|
||||
for episode in episodes:
|
||||
if progress_indicator:
|
||||
# The continues require ticking before doing the work
|
||||
if not progress_indicator.on_tick():
|
||||
break
|
||||
|
||||
logger.debug('Downloading episode: %s', episode.title)
|
||||
if not episode.was_downloaded(and_exists=True):
|
||||
episode._download_error = None
|
||||
|
@ -3638,14 +3678,30 @@ class gPodder(BuilderWidget, dbus.service.Object):
|
|||
if not tasks:
|
||||
return
|
||||
|
||||
progress_indicator = ProgressIndicator(
|
||||
download.DownloadTask.STATUS_MESSAGE[download.DownloadTask.CANCELLING],
|
||||
'', True, self.get_dialog_parent(), len(tasks))
|
||||
|
||||
restart_timer = self.stop_download_list_update_timer()
|
||||
self.download_queue_manager.disable()
|
||||
for task in tasks:
|
||||
task.cancel()
|
||||
|
||||
if not progress_indicator.on_tick():
|
||||
break
|
||||
progress_indicator.on_tick(final=_('Updating...'))
|
||||
self.download_queue_manager.enable()
|
||||
|
||||
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()
|
||||
if restart_timer:
|
||||
self.set_download_list_state(gPodderSyncUI.DL_ONEOFF)
|
||||
else:
|
||||
self.update_downloads_list()
|
||||
|
||||
progress_indicator.on_finished()
|
||||
|
||||
def new_episodes_show(self, episodes, notification=False, selected=None):
|
||||
columns = (
|
||||
|
@ -3755,6 +3811,11 @@ class gPodder(BuilderWidget, dbus.service.Object):
|
|||
self.config.ui.gtk.episode_list.always_show_new = not state
|
||||
action.set_state(GLib.Variant.new_boolean(not state))
|
||||
|
||||
def on_item_view_trim_episode_title_prefix_toggled(self, action, param):
|
||||
state = action.get_state()
|
||||
self.config.ui.gtk.episode_list.trim_title_prefix = not state
|
||||
action.set_state(GLib.Variant.new_boolean(not state))
|
||||
|
||||
def on_item_view_show_episode_description_toggled(self, action, param):
|
||||
state = action.get_state()
|
||||
self.config.ui.gtk.episode_list.descriptions = not state
|
||||
|
@ -4300,8 +4361,7 @@ class gPodder(BuilderWidget, dbus.service.Object):
|
|||
interval = 60 * 1000 * self.config.auto.update.frequency
|
||||
logger.debug('Setting up auto update timer with interval %d.',
|
||||
self.config.auto.update.frequency)
|
||||
self._auto_update_timer_source_id = GLib.timeout_add(
|
||||
interval, self._on_auto_update_timer)
|
||||
self._auto_update_timer_source_id = util.idle_timeout_add(interval, self._on_auto_update_timer)
|
||||
|
||||
def _on_auto_update_timer(self):
|
||||
if self.config.check_connection and not util.connection_available():
|
||||
|
|
|
@ -215,10 +215,12 @@ class EpisodeListModel(Gtk.ListStore):
|
|||
# Caching config values is faster than accessing them directly from config.ui.gtk.episode_list.*
|
||||
# and is easier to maintain then threading them through every method call.
|
||||
self._config_ui_gtk_episode_list_always_show_new = False
|
||||
self._config_ui_gtk_episode_list_trim_title_prefix = False
|
||||
self._config_ui_gtk_episode_list_descriptions = False
|
||||
|
||||
def cache_config(self, config):
|
||||
self._config_ui_gtk_episode_list_always_show_new = config.ui.gtk.episode_list.always_show_new
|
||||
self._config_ui_gtk_episode_list_trim_title_prefix = config.ui.gtk.episode_list.trim_title_prefix
|
||||
self._config_ui_gtk_episode_list_descriptions = config.ui.gtk.episode_list.descriptions
|
||||
|
||||
def _format_filesize(self, episode):
|
||||
|
@ -294,7 +296,7 @@ class EpisodeListModel(Gtk.ListStore):
|
|||
def _format_description(self, episode):
|
||||
d = []
|
||||
|
||||
title = episode.trimmed_title
|
||||
title = episode.trimmed_title if self._config_ui_gtk_episode_list_trim_title_prefix else episode.title
|
||||
if episode.state != gpodder.STATE_DELETED and episode.is_new:
|
||||
d.append('<b>')
|
||||
d.append(html.escape(title))
|
||||
|
|
|
@ -292,6 +292,11 @@ class PodcastEpisode(PodcastModelObject):
|
|||
episode.title = entry['title']
|
||||
episode.link = entry['link']
|
||||
episode.episode_art_url = entry.get('episode_art_url')
|
||||
|
||||
# Only one of the two description fields should be set at a time.
|
||||
# This keeps the database from doubling in size and reduces load time from slow storage.
|
||||
# episode._text_description is initialized by episode.cache_text_description() from the set field.
|
||||
# episode.html_description() returns episode.description_html or generates from episode.description.
|
||||
if entry.get('description_html'):
|
||||
episode.description = ''
|
||||
episode.description_html = entry['description_html']
|
||||
|
@ -1018,6 +1023,9 @@ class PodcastChannel(PodcastModelObject):
|
|||
|
||||
known_files.add(filename)
|
||||
|
||||
# youtube-dl and yt-dlp create <name>.partial and <name>.partial.<ext> files while downloading.
|
||||
# On startup, the latter is reported as an unknown external file.
|
||||
# Both files are properly removed when the download completes.
|
||||
existing_files = set(filename for filename in
|
||||
glob.glob(os.path.join(self.save_dir, '*'))
|
||||
if not filename.endswith('.partial'))
|
||||
|
@ -1222,7 +1230,7 @@ class PodcastChannel(PodcastModelObject):
|
|||
next_feed = None
|
||||
|
||||
# mark episodes not new
|
||||
real_new_episode_count = 0
|
||||
real_new_episodes = []
|
||||
# Search all entries for new episodes
|
||||
for episode in new_episodes:
|
||||
# Workaround for bug 340: If the episode has been
|
||||
|
@ -1234,17 +1242,18 @@ class PodcastChannel(PodcastModelObject):
|
|||
episode.save()
|
||||
|
||||
if episode.is_new:
|
||||
real_new_episode_count += 1
|
||||
real_new_episodes.append(episode)
|
||||
|
||||
# Only allow a certain number of new episodes per update
|
||||
if (self.download_strategy == PodcastChannel.STRATEGY_LATEST
|
||||
and real_new_episode_count > 1):
|
||||
and len(real_new_episodes) > 1):
|
||||
episode.is_new = False
|
||||
episode.save()
|
||||
|
||||
self.children.extend(new_episodes)
|
||||
|
||||
self.remove_unreachable_episodes(existing, seen_guids, max_episodes)
|
||||
return real_new_episodes
|
||||
|
||||
def remove_unreachable_episodes(self, existing, seen_guids, max_episodes):
|
||||
# Remove "unreachable" episodes - episodes that have not been
|
||||
|
@ -1276,11 +1285,12 @@ class PodcastChannel(PodcastModelObject):
|
|||
|
||||
def update(self, max_episodes=0):
|
||||
max_episodes = int(max_episodes)
|
||||
new_episodes = []
|
||||
try:
|
||||
result = self.feed_fetcher.fetch_channel(self, max_episodes)
|
||||
|
||||
if result.status == feedcore.UPDATED_FEED:
|
||||
self._consume_updated_feed(result.feed, max_episodes)
|
||||
new_episodes = self._consume_updated_feed(result.feed, max_episodes)
|
||||
elif result.status == feedcore.NEW_LOCATION:
|
||||
# FIXME: could return the feed because in autodiscovery it is parsed already
|
||||
url = result.feed
|
||||
|
@ -1290,7 +1300,7 @@ class PodcastChannel(PodcastModelObject):
|
|||
self.url = url
|
||||
# With the updated URL, fetch the feed again
|
||||
self.update(max_episodes)
|
||||
return
|
||||
return new_episodes
|
||||
elif result.status == feedcore.NOT_MODIFIED:
|
||||
pass
|
||||
|
||||
|
@ -1317,6 +1327,7 @@ class PodcastChannel(PodcastModelObject):
|
|||
self._determine_common_prefix()
|
||||
|
||||
self.db.commit()
|
||||
return new_episodes
|
||||
|
||||
def delete(self):
|
||||
self.db.delete_podcast(self)
|
||||
|
|
|
@ -121,6 +121,12 @@ class Matcher(object):
|
|||
return episode.channel.title
|
||||
elif k == 'section':
|
||||
return episode.channel.section
|
||||
elif k == 'url':
|
||||
return episode.url
|
||||
elif k == 'link':
|
||||
return episode.link
|
||||
elif k == 'filename':
|
||||
return episode.download_filename
|
||||
|
||||
raise KeyError(k)
|
||||
|
||||
|
|
|
@ -256,7 +256,10 @@ class gPodderSyncUI(object):
|
|||
# if playlist doesn't exist (yet) episodes_in_playlist will be empty
|
||||
if episodes_in_playlists:
|
||||
for episode_filename in episodes_in_playlists:
|
||||
if not playlist.mountpoint.resolve_relative_path(episode_filename).query_exists():
|
||||
if ((not self._config.device_sync.playlists.use_absolute_path
|
||||
and not playlist.playlist_folder.resolve_relative_path(episode_filename).query_exists()) or
|
||||
(self._config.device_sync.playlists.use_absolute_path
|
||||
and not playlist.mountpoint.resolve_relative_path(episode_filename).query_exists())):
|
||||
# episode was synced but no longer on device
|
||||
# i.e. must have been deleted by user, so delete from gpodder
|
||||
try:
|
||||
|
|
|
@ -1320,8 +1320,25 @@ def idle_add(func, *args):
|
|||
func(*args)
|
||||
|
||||
|
||||
def idle_timeout_add(milliseconds, func, *args):
|
||||
"""Run a function in the main GUI thread at regular intervals, at idle priority
|
||||
|
||||
PRIORITY_HIGH -100
|
||||
PRIORITY_DEFAULT 0 timeout_add()
|
||||
PRIORITY_HIGH_IDLE 100
|
||||
resizing 110
|
||||
redraw 120
|
||||
PRIORITY_DEFAULT_IDLE 200 idle_add()
|
||||
PRIORITY_LOW 300
|
||||
"""
|
||||
if not gpodder.ui.gtk:
|
||||
raise Exception('util.idle_timeout_add() is only supported by Gtk+')
|
||||
from gi.repository import GLib
|
||||
return GLib.timeout_add(milliseconds, func, *args, priority=GLib.PRIORITY_DEFAULT_IDLE)
|
||||
|
||||
|
||||
class IdleTimeout(object):
|
||||
"""Run a function in the main GUI thread at regular intervals since the last run
|
||||
"""Run a function in the main GUI thread at regular intervals since the last run, at idle priority
|
||||
|
||||
A simple timeout_add() continuously calls the function if it exceeds the interval,
|
||||
which lags the UI and prevents idle_add() calls from happening. This class restarts
|
||||
|
@ -1331,15 +1348,28 @@ class IdleTimeout(object):
|
|||
if not gpodder.ui.gtk:
|
||||
raise Exception('util.IdleTimeout() is only supported by Gtk+')
|
||||
self.milliseconds = milliseconds
|
||||
self.max_milliseconds = 0
|
||||
self.func = func
|
||||
from gi.repository import GLib
|
||||
self.id = GLib.timeout_add(milliseconds, self._callback, *args)
|
||||
self.id = GLib.timeout_add(milliseconds, self._callback, *args, priority=GLib.PRIORITY_DEFAULT_IDLE)
|
||||
|
||||
def set_max_milliseconds(self, max):
|
||||
self.max_milliseconds = max
|
||||
return self
|
||||
|
||||
def _callback(self, *args):
|
||||
self.cancel()
|
||||
start_time = time.time()
|
||||
if self.func(*args):
|
||||
if self.max_milliseconds > self.milliseconds:
|
||||
duration = round((time.time() - start_time) * 1000)
|
||||
if duration > self.max_milliseconds:
|
||||
duration = self.max_milliseconds
|
||||
milliseconds = round(lerp(self.milliseconds, self.max_milliseconds, duration / self.max_milliseconds))
|
||||
else:
|
||||
milliseconds = self.milliseconds
|
||||
from gi.repository import GLib
|
||||
self.id = GLib.timeout_add(self.milliseconds, self._callback, *args)
|
||||
self.id = GLib.timeout_add(milliseconds, self._callback, *args, priority=GLib.PRIORITY_DEFAULT_IDLE)
|
||||
|
||||
def cancel(self):
|
||||
if self.id:
|
||||
|
@ -1348,6 +1378,12 @@ class IdleTimeout(object):
|
|||
self.id = 0
|
||||
|
||||
|
||||
def lerp(a, b, f):
|
||||
"""Linear interpolation between 'a' and 'b', where 'f' is between 0.0 and 1.0
|
||||
"""
|
||||
return ((1.0 - f) * a) + (f * b)
|
||||
|
||||
|
||||
def bluetooth_available():
|
||||
"""
|
||||
Returns True or False depending on the availability
|
||||
|
|
|
@ -453,14 +453,14 @@ def get_channel_id_url(url, feed_data=None):
|
|||
if m:
|
||||
channel_id = m.group(1)
|
||||
if channel_id is None:
|
||||
raise Exception('Could not retrieve youtube channel id.')
|
||||
raise Exception('Could not retrieve YouTube channel ID for URL %s.' % url)
|
||||
channel_url = 'https://www.youtube.com/channel/{}'.format(channel_id)
|
||||
return channel_url
|
||||
|
||||
except Exception:
|
||||
logger.warning('Could not retrieve youtube channel id.', exc_info=True)
|
||||
logger.warning('Could not retrieve YouTube channel ID for URL %s.' % url, exc_info=True)
|
||||
|
||||
raise Exception('Could not retrieve youtube channel id.')
|
||||
raise Exception('Could not retrieve YouTube channel ID for URL %s.' % url)
|
||||
|
||||
|
||||
def get_cover(url, feed_data=None):
|
||||
|
|
|
@ -67,13 +67,13 @@ cp -a "$checkout"/tools/mac-osx/make_cert_pem.py "$resources"/bin
|
|||
|
||||
# install gPodder hard dependencies
|
||||
$run_pip install setuptools==64.0.3 wheel || exit 1
|
||||
$run_pip install mygpoclient==1.9 podcastparser==0.6.8 requests[socks]==2.28.1 || exit 1
|
||||
$run_pip install mygpoclient==1.9 podcastparser==0.6.9 requests[socks]==2.28.1 || exit 1
|
||||
# install brotli and pycryptodomex (build from source)
|
||||
$run_pip debug -v
|
||||
$run_pip install -v brotli || exit 1
|
||||
$run_pip install -v pycryptodomex || exit 1
|
||||
# install extension dependencies; no explicit version for yt-dlp
|
||||
$run_pip install html5lib==1.1 mutagen==1.45.1 yt-dlp || exit 1
|
||||
$run_pip install html5lib==1.1 mutagen==1.46.0 yt-dlp || exit 1
|
||||
|
||||
cd "$checkout"
|
||||
touch share/applications/gpodder{,-url-handler}.desktop
|
||||
|
|
|
@ -3,11 +3,11 @@
|
|||
#
|
||||
dbus-python
|
||||
html5lib==1.1
|
||||
mutagen==1.45.1
|
||||
mutagen==1.46.0
|
||||
mygpoclient==1.9
|
||||
podcastparser==0.6.8
|
||||
podcastparser==0.6.9
|
||||
requests[socks]==2.28.1
|
||||
urllib3==1.26.10
|
||||
urllib3==1.26.13
|
||||
yt-dlp
|
||||
# eyed3 is optional and pulls in a lot of dependencies, so disable by default
|
||||
# eyed3
|
||||
|
|
|
@ -84,18 +84,18 @@ function extract_installer {
|
|||
}
|
||||
|
||||
PIP_REQUIREMENTS="\
|
||||
certifi==2022.6.15
|
||||
chardet==4.0.0
|
||||
comtypes==1.1.11
|
||||
certifi==2022.12.7
|
||||
chardet==5.1.0
|
||||
comtypes==1.1.14
|
||||
git+https://github.com/jaraco/pywin32-ctypes.git@f27d6a0
|
||||
html5lib==1.1
|
||||
idna==3.3
|
||||
mutagen==1.45.1
|
||||
idna==3.4
|
||||
mutagen==1.46.0
|
||||
mygpoclient==1.9
|
||||
podcastparser==0.6.8
|
||||
podcastparser==0.6.9
|
||||
PySocks==1.7.1
|
||||
requests==2.28.1
|
||||
urllib3==1.26.10
|
||||
urllib3==1.26.13
|
||||
webencodings==0.5.1
|
||||
yt-dlp
|
||||
"
|
||||
|
|
Loading…
Reference in New Issue