Merge branch 'master' into dev-adaptive
This commit is contained in:
commit
46dbc80d8b
|
@ -6,7 +6,7 @@ jobs:
|
|||
xcode: "13.2.1"
|
||||
shell: /bin/bash --login -o pipefail
|
||||
environment:
|
||||
- BUNDLE_TAG: 22.7.28
|
||||
- BUNDLE_TAG: 22.8.27
|
||||
steps:
|
||||
- checkout
|
||||
- run: >
|
||||
|
@ -15,7 +15,7 @@ jobs:
|
|||
saved_hash=$(awk '{print $1;}' < "pythonbase-$BUNDLE_TAG.zip.sha256");
|
||||
comp_hash=$(openssl sha256 "pythonbase-$BUNDLE_TAG.zip" | awk '{print $2;}');
|
||||
if [ "$saved_hash" != "$comp_hash" ]; then echo "E: $saved_hash != $comp_hash"; exit 1; else echo "valid hash"; fi;
|
||||
LC_CTYPE=C.UTF-8 LANG=C.UTF-8 tools/mac-osx/release_on_mac.sh "$(pwd)/pythonbase-$BUNDLE_TAG.zip";
|
||||
LC_CTYPE=C.UTF-8 LANG=C.UTF-8 tools/mac-osx/release_on_mac.sh "$(pwd)/pythonbase-$BUNDLE_TAG.zip" || exit 1;
|
||||
rm -Rf tools/mac-osx/_build/{gPodder.app,*.deps.zip*,gPodder.contents,run-*,gpo,gpodder-migrate2tres}
|
||||
- store_artifacts:
|
||||
path: tools/mac-osx/_build/
|
||||
|
|
|
@ -12,7 +12,7 @@ jobs:
|
|||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
python-version: [3.9]
|
||||
python-version: ['3.10']
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
|
|
20
bin/gpo
20
bin/gpo
|
@ -88,6 +88,7 @@ import pydoc
|
|||
import re
|
||||
import shlex
|
||||
import sys
|
||||
import textwrap
|
||||
import threading
|
||||
|
||||
try:
|
||||
|
@ -438,10 +439,20 @@ class gPodderCli(object):
|
|||
if podcast is None:
|
||||
self._error(_('You are not subscribed to %s.') % url)
|
||||
else:
|
||||
# Clean up downloads and download directories
|
||||
common.clean_up_downloads()
|
||||
|
||||
podcast.delete()
|
||||
self._db.commit()
|
||||
self._error(_('Unsubscribed from %s.') % url)
|
||||
|
||||
# Delete downloaded episodes
|
||||
podcast.remove_downloaded()
|
||||
|
||||
# TODO: subscribe and unsubscribe need to sync with mygpo
|
||||
# Upload subscription list changes to the web service
|
||||
# self.mygpo_client.on_unsubscribe([podcast.url])
|
||||
|
||||
return True
|
||||
|
||||
def is_episode_new(self, episode):
|
||||
|
@ -481,13 +492,18 @@ class gPodderCli(object):
|
|||
return "disabled"
|
||||
return "enabled"
|
||||
|
||||
title, url, status = podcast.title, podcast.url, \
|
||||
feed_update_status_msg(podcast)
|
||||
title, url, description, link, status = (
|
||||
podcast.title, podcast.url, podcast.description, podcast.link,
|
||||
feed_update_status_msg(podcast))
|
||||
description = '\n'.join(textwrap.wrap(description, subsequent_indent=' ' * 8))
|
||||
episodes = self._episodesList(podcast)
|
||||
episodes = '\n '.join(episodes)
|
||||
self._pager("""
|
||||
Title: %(title)s
|
||||
URL: %(url)s
|
||||
Description:
|
||||
%(description)s
|
||||
Link: %(link)s
|
||||
Feed update is %(status)s
|
||||
|
||||
Episodes:
|
||||
|
|
12
makefile
12
makefile
|
@ -38,8 +38,8 @@ UIFILES=$(wildcard share/gpodder/ui/gtk/*.ui \
|
|||
share/gpodder/ui/adaptive/*.ui)
|
||||
UIFILES_H=$(subst .ui,.ui.h,$(UIFILES))
|
||||
GETTEXT_SOURCE=$(wildcard src/gpodder/*.py \
|
||||
src/gpodder/gtkui/*.py \
|
||||
src/gpodder/gtkui/interface/*.py \
|
||||
src/gpodder/gtkui/*.py \
|
||||
src/gpodder/gtkui/interface/*.py \
|
||||
src/gpodder/gtkui/desktop/*.py \
|
||||
src/gpodder/plugins/*.py \
|
||||
share/gpodder/extensions/*.py)
|
||||
|
@ -72,13 +72,12 @@ lint:
|
|||
pycodestyle share src/gpodder tools bin/* *.py
|
||||
isort -q $(ISORTOPTS) || isort --df $(ISORTOPTS)
|
||||
|
||||
|
||||
release: distclean
|
||||
$(PYTHON) setup.py sdist
|
||||
|
||||
releasetest: unittest $(DESKTOP_FILES) $(POFILES)
|
||||
for f in $(DESKTOP_FILES); do desktop-file-validate $$f; done
|
||||
for f in $(POFILES); do msgfmt --check $$f; done
|
||||
for f in $(DESKTOP_FILES); do desktop-file-validate $$f || exit 1; done
|
||||
for f in $(POFILES); do msgfmt --check $$f || exit 1; done
|
||||
|
||||
$(GPODDER_SERVICE_FILE): $(GPODDER_SERVICE_FILE_IN)
|
||||
sed -e 's#__PREFIX__#$(PREFIX)#' $< >$@
|
||||
|
@ -140,7 +139,6 @@ $(MESSAGES): $(GETTEXT_SOURCE)
|
|||
messages-force:
|
||||
xgettext --from-code=utf-8 -LPython -k_:1 -kN_:1 -kN_:1,2 -kn_:1,2 -o $(MESSAGES) $(GETTEXT_SOURCE)
|
||||
|
||||
|
||||
##########################################################################
|
||||
|
||||
# This only works in a Git working commit, and assumes that the local Git
|
||||
|
@ -171,5 +169,3 @@ distclean: clean
|
|||
.PHONY: help unittest release releasetest install manpages clean distclean messages headlink lint revbump
|
||||
|
||||
##########################################################################
|
||||
|
||||
|
||||
|
|
682
po/cs_CZ.po
682
po/cs_CZ.po
File diff suppressed because it is too large
Load Diff
682
po/es_ES.po
682
po/es_ES.po
File diff suppressed because it is too large
Load Diff
682
po/es_MX.po
682
po/es_MX.po
File diff suppressed because it is too large
Load Diff
694
po/fa_IR.po
694
po/fa_IR.po
File diff suppressed because it is too large
Load Diff
690
po/id_ID.po
690
po/id_ID.po
File diff suppressed because it is too large
Load Diff
682
po/ko_KR.po
682
po/ko_KR.po
File diff suppressed because it is too large
Load Diff
690
po/messages.pot
690
po/messages.pot
File diff suppressed because it is too large
Load Diff
682
po/pt_BR.po
682
po/pt_BR.po
File diff suppressed because it is too large
Load Diff
682
po/zh_CN.po
682
po/zh_CN.po
File diff suppressed because it is too large
Load Diff
|
@ -1,4 +1,4 @@
|
|||
#!/usr/bin/python3
|
||||
#!/usr/bin/env python3
|
||||
# Example script that can be used as post-play extension in media players
|
||||
#
|
||||
# Set the configuration options "audio_played_dbus" and "video_played_dbus"
|
||||
|
|
|
@ -75,7 +75,9 @@ class gPodderExtension:
|
|||
close_fds=True)
|
||||
result = ffmpeg.wait()
|
||||
util.delete_file(list_filename)
|
||||
util.idle_add(lambda: indicator.on_finished())
|
||||
|
||||
indicator.on_finished()
|
||||
|
||||
util.idle_add(lambda: self.gpodder.show_message(
|
||||
_('Videos successfully converted') if result == 0 else
|
||||
_('Error converting videos'),
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
####
|
||||
# 01/2011 Bernd Schlapsi <brot@gmx.info>
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
# Requirements: apt-get install python-kaa-metadata ffmpeg python-dbus
|
||||
# To use, copy it as a Python script into ~/.config/gpodder/extensions/rockbox_mp4_convert.py
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
####
|
||||
# 01/2011 Bernd Schlapsi <brot@gmx.info>
|
||||
|
|
|
@ -13,7 +13,7 @@ from gpodder import util
|
|||
|
||||
import gi # isort:skip
|
||||
gi.require_version('Unity', '7.0') # isort:skip
|
||||
from gi.repository import GObject, Unity # isort:skip
|
||||
from gi.repository import GLib, Unity # isort:skip
|
||||
|
||||
|
||||
_ = gpodder.gettext
|
||||
|
@ -59,4 +59,4 @@ class gPodderExtension:
|
|||
self.launcher_entry = None
|
||||
|
||||
def on_download_progress(self, progress):
|
||||
GObject.idle_add(self.launcher_entry.set_progress, float(progress))
|
||||
GLib.idle_add(self.launcher_entry.set_progress, float(progress))
|
||||
|
|
|
@ -12,8 +12,10 @@ import time
|
|||
|
||||
try:
|
||||
import yt_dlp as youtube_dl
|
||||
program_name = 'yt-dlp'
|
||||
except:
|
||||
import youtube_dl
|
||||
program_name = 'youtube-dl'
|
||||
|
||||
import gpodder
|
||||
from gpodder import download, feedcore, model, registry, util, youtube
|
||||
|
@ -426,6 +428,14 @@ class gPodderYoutubeDL(download.CustomDownloader):
|
|||
"""
|
||||
if not self.force and not self.my_config.manage_downloads:
|
||||
return None
|
||||
|
||||
try: # Reject URLs linking to known media files
|
||||
(_, ext) = util.filename_from_url(episode.url)
|
||||
if util.file_type_by_extension(ext) is not None:
|
||||
return None
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
if self.is_supported_url(episode.url):
|
||||
return YoutubeCustomDownload(self, episode.url, episode)
|
||||
|
||||
|
@ -439,12 +449,10 @@ class gPodderExtension:
|
|||
|
||||
def on_load(self):
|
||||
self.ytdl = gPodderYoutubeDL(self.container.manager.core.config, self.container.config)
|
||||
logger.info('Registering youtube-dl.')
|
||||
logger.info('Registering youtube-dl. (using %s %s)' % (program_name, youtube_dl.version.__version__))
|
||||
registry.feed_handler.register(self.ytdl.fetch_channel)
|
||||
registry.custom_downloader.register(self.ytdl.custom_downloader)
|
||||
|
||||
logger.debug('youtube-dl %s' % youtube_dl.version.__version__)
|
||||
|
||||
if youtube_dl.utils.version_tuple(youtube_dl.version.__version__) < youtube_dl.utils.version_tuple(want_ytdl_version):
|
||||
logger.error(want_ytdl_version_msg
|
||||
% {'have_version': youtube_dl.version.__version__, 'want_version': want_ytdl_version})
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!-- Generated with glade 3.38.2 -->
|
||||
<!-- Generated with glade 3.40.0 -->
|
||||
<!--*- mode: xml -*-->
|
||||
<interface>
|
||||
<requires lib="gtk+" version="3.16"/>
|
||||
|
@ -36,7 +36,7 @@
|
|||
<property name="can-focus">False</property>
|
||||
<property name="is-important">True</property>
|
||||
<property name="label" translatable="yes">Play</property>
|
||||
<property name="icon-name">media-playback-start</property>
|
||||
<property name="icon-name">media-playback-start-symbolic</property>
|
||||
<signal name="clicked" handler="on_playback_selected_episodes" swapped="no"/>
|
||||
</object>
|
||||
<packing>
|
||||
|
@ -51,7 +51,7 @@
|
|||
<property name="can-focus">False</property>
|
||||
<property name="is-important">True</property>
|
||||
<property name="label" translatable="yes">Download</property>
|
||||
<property name="icon-name">go-down</property>
|
||||
<property name="icon-name">document-save-symbolic</property>
|
||||
<signal name="clicked" handler="on_download_selected_episodes" swapped="no"/>
|
||||
</object>
|
||||
<packing>
|
||||
|
@ -66,7 +66,7 @@
|
|||
<property name="can-focus">False</property>
|
||||
<property name="is-important">True</property>
|
||||
<property name="label" translatable="yes">Pause</property>
|
||||
<property name="icon-name">media-playback-pause</property>
|
||||
<property name="icon-name">media-playback-pause-symbolic</property>
|
||||
<signal name="clicked" handler="on_pause_selected_episodes" swapped="no"/>
|
||||
</object>
|
||||
<packing>
|
||||
|
@ -81,7 +81,7 @@
|
|||
<property name="can-focus">False</property>
|
||||
<property name="is-important">True</property>
|
||||
<property name="label" translatable="yes">Cancel</property>
|
||||
<property name="icon-name">process-stop</property>
|
||||
<property name="icon-name">process-stop-symbolic</property>
|
||||
<signal name="clicked" handler="on_item_cancel_download_activate" swapped="no"/>
|
||||
</object>
|
||||
<packing>
|
||||
|
@ -105,7 +105,7 @@
|
|||
<property name="can-focus">False</property>
|
||||
<property name="action-name">app.preferences</property>
|
||||
<property name="label" translatable="yes">Preferences</property>
|
||||
<property name="icon-name">preferences-desktop</property>
|
||||
<property name="icon-name">preferences-other-symbolic</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
|
@ -127,7 +127,7 @@
|
|||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="label" translatable="yes">Quit</property>
|
||||
<property name="icon-name">application-exit</property>
|
||||
<property name="icon-name">application-exit-symbolic</property>
|
||||
<signal name="clicked" handler="on_gPodder_delete_event" swapped="no"/>
|
||||
</object>
|
||||
<packing>
|
||||
|
@ -384,17 +384,85 @@
|
|||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<!-- n-columns=1 n-rows=2 -->
|
||||
<object class="GtkGrid" id="vboxDownloadStatusWidgets">
|
||||
<object class="GtkBox">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="border-width">5</property>
|
||||
<property name="orientation">vertical</property>
|
||||
<property name="row-spacing">5</property>
|
||||
<child>
|
||||
<object class="GtkScrolledWindow" id="scrolledwindow1">
|
||||
<object class="GtkInfoBar" id="resume_all_infobar">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="message-type">question</property>
|
||||
<property name="show-close-button">True</property>
|
||||
<property name="revealed">False</property>
|
||||
<signal name="response" handler="on_resume_all_infobar_response" swapped="no"/>
|
||||
<child internal-child="action_area">
|
||||
<object class="GtkButtonBox">
|
||||
<property name="can-focus">False</property>
|
||||
<property name="spacing">6</property>
|
||||
<property name="layout-style">end</property>
|
||||
<child>
|
||||
<object class="GtkButton" id="resume_all_button">
|
||||
<property name="label" translatable="yes">Resume all</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">True</property>
|
||||
<property name="receives-default">True</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">True</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">False</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child internal-child="content_area">
|
||||
<object class="GtkBox">
|
||||
<property name="can-focus">False</property>
|
||||
<property name="spacing">16</property>
|
||||
<child>
|
||||
<object class="GtkLabel">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="label" translatable="yes">Incomplete downloads from a previous session were found.</property>
|
||||
<property name="wrap">True</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">False</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<action-widgets>
|
||||
<action-widget response="-5">resume_all_button</action-widget>
|
||||
</action-widgets>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkScrolledWindow">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">True</property>
|
||||
<property name="margin-start">5</property>
|
||||
<property name="margin-end">5</property>
|
||||
<property name="margin-top">5</property>
|
||||
<property name="margin-bottom">5</property>
|
||||
<property name="vexpand">True</property>
|
||||
<property name="shadow-type">in</property>
|
||||
<child>
|
||||
|
@ -415,23 +483,26 @@
|
|||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="left-attach">0</property>
|
||||
<property name="top-attach">0</property>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<!-- n-columns=3 n-rows=1 -->
|
||||
<object class="GtkGrid" id="hboxDownloadSettings">
|
||||
<object class="GtkBox">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="border-width">5</property>
|
||||
<property name="column-spacing">10</property>
|
||||
<property name="margin-start">5</property>
|
||||
<property name="margin-end">5</property>
|
||||
<property name="margin-top">5</property>
|
||||
<property name="margin-bottom">10</property>
|
||||
<property name="spacing">5</property>
|
||||
<child>
|
||||
<!-- n-columns=3 n-rows=1 -->
|
||||
<object class="GtkGrid" id="hboxDownloadLimit">
|
||||
<object class="GtkBox">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="column-spacing">5</property>
|
||||
<property name="margin-start">5</property>
|
||||
<property name="spacing">5</property>
|
||||
<child>
|
||||
<object class="GtkCheckButton" id="cbLimitDownloads">
|
||||
<property name="label" translatable="yes">Limit rate to</property>
|
||||
|
@ -442,22 +513,27 @@
|
|||
<signal name="toggled" handler="on_cbLimitDownloads_toggled" swapped="no"/>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="left-attach">0</property>
|
||||
<property name="top-attach">0</property>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkSpinButton" id="spinLimitDownloads">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">True</property>
|
||||
<property name="invisible-char">●</property>
|
||||
<property name="caps-lock-warning">False</property>
|
||||
<property name="input-purpose">number</property>
|
||||
<property name="adjustment">adjustment1</property>
|
||||
<property name="climb-rate">1</property>
|
||||
<property name="digits">1</property>
|
||||
<property name="numeric">True</property>
|
||||
<property name="update-policy">if-valid</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="left-attach">1</property>
|
||||
<property name="top-attach">0</property>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
|
@ -468,33 +544,23 @@
|
|||
<property name="xalign">0</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="left-attach">2</property>
|
||||
<property name="top-attach">0</property>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">2</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="left-attach">0</property>
|
||||
<property name="top-attach">0</property>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkLabel" id="DownloadSettingsSpacer">
|
||||
<object class="GtkBox">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="hexpand">True</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="left-attach">1</property>
|
||||
<property name="top-attach">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<!-- n-columns=2 n-rows=1 -->
|
||||
<object class="GtkGrid" id="hboxDownloadRate">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="column-spacing">5</property>
|
||||
<property name="spacing">5</property>
|
||||
<child>
|
||||
<object class="GtkCheckButton" id="cbMaxDownloads">
|
||||
<property name="label" translatable="yes">Limit downloads to</property>
|
||||
|
@ -505,33 +571,42 @@
|
|||
<signal name="toggled" handler="on_cbMaxDownloads_toggled" swapped="no"/>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="left-attach">0</property>
|
||||
<property name="top-attach">0</property>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkSpinButton" id="spinMaxDownloads">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">True</property>
|
||||
<property name="invisible-char">●</property>
|
||||
<property name="caps-lock-warning">False</property>
|
||||
<property name="input-purpose">number</property>
|
||||
<property name="adjustment">adjustment2</property>
|
||||
<property name="climb-rate">1</property>
|
||||
<property name="snap-to-ticks">True</property>
|
||||
<property name="numeric">True</property>
|
||||
<property name="update-policy">if-valid</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="left-attach">1</property>
|
||||
<property name="top-attach">0</property>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="left-attach">2</property>
|
||||
<property name="top-attach">0</property>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="pack-type">end</property>
|
||||
<property name="position">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="left-attach">0</property>
|
||||
<property name="top-attach">1</property>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">2</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
|
|
|
@ -80,6 +80,7 @@
|
|||
<property name="can-focus">True</property>
|
||||
<property name="has-focus">True</property>
|
||||
<property name="activates-default">True</property>
|
||||
<property name="secondary-icon-name">edit-clear</property>
|
||||
<signal name="changed" handler="on_entry_url_changed" swapped="no"/>
|
||||
</object>
|
||||
<packing>
|
||||
|
|
|
@ -19,6 +19,10 @@
|
|||
</item>
|
||||
</section>
|
||||
<section>
|
||||
<item>
|
||||
<attribute name="label" translatable="yes">Open Logs</attribute>
|
||||
<attribute name="action">app.logs</attribute>
|
||||
</item>
|
||||
<item>
|
||||
<attribute name="label" translatable="yes">Help</attribute>
|
||||
<attribute name="action">app.help</attribute>
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!-- :indentSize=4:noTabs=true:tabSize=4: -->
|
||||
<component type="desktop-application">
|
||||
<id>org.gpodder.gpodder</id>
|
||||
<metadata_license>CC0-1.0</metadata_license>
|
||||
|
@ -10,24 +9,81 @@
|
|||
<p>gPodder lets you manage your Podcast subscriptions, discover new content and download episodes to your devices.</p>
|
||||
<p>You can also take advantage of the service gpodder.net, which lets you sync subscriptions, playback progress and starred episodes.</p>
|
||||
</description>
|
||||
<launchable type="desktop-id">gpodder.desktop</launchable>
|
||||
<launchable type="desktop-id">org.gpodder.gpodder.desktop</launchable>
|
||||
<url type="homepage">https://www.gpodder.org</url>
|
||||
<provides>
|
||||
<binary>gpodder</binary>
|
||||
<id>gpodder.desktop</id>
|
||||
</provides>
|
||||
<content_rating type="oars-1.0"/>
|
||||
<screenshots>
|
||||
<content_rating type="oars-1.1">
|
||||
<content_attribute id="violence-cartoon">none</content_attribute>
|
||||
<content_attribute id="violence-fantasy">none</content_attribute>
|
||||
<content_attribute id="violence-realistic">none</content_attribute>
|
||||
<content_attribute id="violence-bloodshed">none</content_attribute>
|
||||
<content_attribute id="violence-sexual">none</content_attribute>
|
||||
<content_attribute id="violence-desecration">none</content_attribute>
|
||||
<content_attribute id="violence-slavery">none</content_attribute>
|
||||
<content_attribute id="violence-worship">none</content_attribute>
|
||||
<content_attribute id="drugs-alcohol">none</content_attribute>
|
||||
<content_attribute id="drugs-narcotics">none</content_attribute>
|
||||
<content_attribute id="drugs-tobacco">none</content_attribute>
|
||||
<content_attribute id="sex-nudity">none</content_attribute>
|
||||
<content_attribute id="sex-themes">none</content_attribute>
|
||||
<content_attribute id="sex-homosexuality">none</content_attribute>
|
||||
<content_attribute id="sex-prostitution">none</content_attribute>
|
||||
<content_attribute id="sex-adultery">none</content_attribute>
|
||||
<content_attribute id="sex-appearance">none</content_attribute>
|
||||
<content_attribute id="language-profanity">none</content_attribute>
|
||||
<content_attribute id="language-humor">none</content_attribute>
|
||||
<content_attribute id="language-discrimination">none</content_attribute>
|
||||
<content_attribute id="social-chat">none</content_attribute>
|
||||
<content_attribute id="social-info">none</content_attribute>
|
||||
<content_attribute id="social-audio">none</content_attribute>
|
||||
<content_attribute id="social-location">none</content_attribute>
|
||||
<content_attribute id="social-contacts">none</content_attribute>
|
||||
<content_attribute id="money-purchasing">none</content_attribute>
|
||||
<content_attribute id="money-gambling">none</content_attribute>
|
||||
</content_rating>
|
||||
<screenshots>
|
||||
<screenshot type="default">
|
||||
<caption>The main window</caption>
|
||||
<image>https://raw.githubusercontent.com/flathub/org.gpodder.gpodder/master/screenshot.png</image>
|
||||
</screenshot>
|
||||
</screenshots>
|
||||
<releases>
|
||||
<release version="3.11.0" date="2022-07-30">
|
||||
<description>
|
||||
<p>This release contains a year's worth of improvements. Major changes:</p>
|
||||
<ul>
|
||||
<li>Warning: There is a database schema update (See "Moving to an older gPodder release" in the user manual at gpodder.github.io for how to rollback)</li>
|
||||
<li>Numerous bug fixes</li>
|
||||
<li>Performance improvements</li>
|
||||
<li>A new preferences dialog</li>
|
||||
<li>Support again syncing to mtp:// and iPod devices on Linux</li>
|
||||
</ul>
|
||||
</description>
|
||||
</release>
|
||||
<release version="3.10.21" date="2021-07-20"/>
|
||||
<release version="3.10.20" date="2021-06-06"/>
|
||||
<release version="3.10.19" date="2021-04-15"/>
|
||||
<release version="3.10.17" date="2020-11-23"/>
|
||||
<release version="3.10.13" date="2020-01-29"/>
|
||||
<release version="3.10.12" date="2020-01-25"/>
|
||||
<release version="3.10.11" date="2019-09-29"/>
|
||||
<release version="3.10.10" date="2019-09-27"/>
|
||||
<release version="3.10.9" date="2019-06-09"/>
|
||||
<release version="3.10.8" date="2019-04-08"/>
|
||||
<release version="3.10.7" date="2019-02-02"/>
|
||||
<release version="3.10.6" date="2018-12-29">
|
||||
<release version="3.10.5" date="2018-09-15">
|
||||
<description>This is a bugfix release, shortly after 3.10.4, for the Rename after Download extension.</description>
|
||||
<description>
|
||||
<p>This is a bugfix release, shortly after 3.10.4, for the Rename after Download extension.</p>
|
||||
</description>
|
||||
</release>
|
||||
<release version="3.10.4" date="2018-09-09"/>
|
||||
<release version="3.10.3" date="2018-06-14"/>
|
||||
<release version="3.10.2" date="2018-06-10"/>
|
||||
<release version="3.10.1" date="2018-02-19"/>
|
||||
<release version="3.10.0" date="2017-12-29"/>
|
||||
</releases>
|
||||
</component>
|
||||
|
|
|
@ -430,3 +430,13 @@ class Config(object):
|
|||
logger.debug("setting config.device_sync.max_filename_length=120"
|
||||
" (999 is bad for NTFS and ext{2-4})")
|
||||
self.device_sync.max_filename_length = 120
|
||||
|
||||
def clamp_range(self, name, min, max):
|
||||
value = getattr(self, name)
|
||||
if value < min:
|
||||
setattr(self, name, min)
|
||||
return True
|
||||
if value > max:
|
||||
setattr(self, name, max)
|
||||
return True
|
||||
return False
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# gPodder - A media aggregator and podcast client
|
||||
|
|
|
@ -95,6 +95,10 @@ class gPodderApplication(Gtk.Application):
|
|||
action.connect('activate', self.on_help_activate)
|
||||
self.add_action(action)
|
||||
|
||||
action = Gio.SimpleAction.new('logs', None)
|
||||
action.connect('activate', self.on_logs_activate)
|
||||
self.add_action(action)
|
||||
|
||||
action = Gio.SimpleAction.new('preferences', None)
|
||||
action.connect('activate', self.on_itemPreferences_activate)
|
||||
self.add_action(action)
|
||||
|
@ -259,6 +263,9 @@ class gPodderApplication(Gtk.Application):
|
|||
def on_help_activate(self, action, param):
|
||||
util.open_website('https://gpodder.github.io/docs/')
|
||||
|
||||
def on_logs_activate(self, action, param):
|
||||
util.gui_open(os.path.join(gpodder.home, 'Logs'), gui=self.window)
|
||||
|
||||
def on_itemPreferences_activate(self, action, param=None):
|
||||
gPodderPreferences(self.window.gPodder,
|
||||
_config=self.window.config,
|
||||
|
|
|
@ -22,6 +22,8 @@
|
|||
# gpodder.gtkui.config -- Config object with GTK+ support (2009-08-24)
|
||||
#
|
||||
|
||||
import logging
|
||||
|
||||
import gi # isort:skip
|
||||
gi.require_version('Gdk', '3.0') # isort:skip
|
||||
gi.require_version('Gtk', '3.0') # isort:skip
|
||||
|
@ -30,6 +32,8 @@ from gi.repository import Gdk, Gtk, Pango
|
|||
import gpodder
|
||||
from gpodder import config, util
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
_ = gpodder.gettext
|
||||
|
||||
|
||||
|
@ -156,6 +160,25 @@ class UIConfig(config.Config):
|
|||
if gpodder.ui.win32:
|
||||
window.set_gravity(Gdk.Gravity.STATIC)
|
||||
|
||||
if -1 not in (cfg.x, cfg.y, cfg.width, cfg.height):
|
||||
# get screen resolution
|
||||
screen = Gdk.Screen.get_default()
|
||||
screen_width = 0
|
||||
screen_height = 0
|
||||
for i in range(0, screen.get_n_monitors()):
|
||||
monitor = screen.get_monitor_geometry(i)
|
||||
screen_width += monitor.width
|
||||
screen_height += monitor.height
|
||||
logger.debug('Screen %d x %d' % (screen_width, screen_height))
|
||||
# reset window position if more than 50% is off-screen
|
||||
half_width = cfg.width / 2
|
||||
half_height = cfg.height / 2
|
||||
if (cfg.x + cfg.width - half_width) < 0 or (cfg.y + cfg.height - half_height) < 0 \
|
||||
or cfg.x > (screen_width - half_width) or cfg.y > (screen_height - half_height):
|
||||
logger.warning('"%s" window was off-screen at (%d, %d), resetting to default position' % (config_prefix, cfg.x, cfg.y))
|
||||
cfg.x = -1
|
||||
cfg.y = -1
|
||||
|
||||
if cfg.width != -1 and cfg.height != -1:
|
||||
window.resize(cfg.width, cfg.height)
|
||||
|
||||
|
|
|
@ -111,7 +111,7 @@ class gPodderEpisodeSelector(BuilderWidget):
|
|||
self.size_attribute = 'file_size'
|
||||
|
||||
if not hasattr(self, 'tooltip_attribute'):
|
||||
self.tooltip_attribute = 'description'
|
||||
self.tooltip_attribute = '_text_description'
|
||||
|
||||
if not hasattr(self, 'selection_buttons'):
|
||||
self.selection_buttons = {}
|
||||
|
|
|
@ -193,7 +193,10 @@ class DownloadStatusModel(Gtk.ListStore):
|
|||
# as only the main thread is allowed to manipulate the list store.
|
||||
def get_next(self):
|
||||
dqr = DequeueRequest()
|
||||
util.idle_add(self.__get_next, dqr)
|
||||
# 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)
|
||||
return dqr.dequeue()
|
||||
|
||||
def _work_gen(self):
|
||||
|
|
|
@ -41,6 +41,7 @@ class gPodderAddPodcast(BuilderWidget):
|
|||
if hasattr(self, 'preset_url'):
|
||||
self.entry_url.set_text(self.preset_url)
|
||||
self.entry_url.connect('activate', self.on_entry_url_activate)
|
||||
self.entry_url.connect('icon-press', self.on_clear_url)
|
||||
self.gPodderAddPodcast.show()
|
||||
|
||||
if not hasattr(self, 'preset_url'):
|
||||
|
@ -67,6 +68,9 @@ class gPodderAddPodcast(BuilderWidget):
|
|||
clipboard.request_text(receive_clipboard_text, True)
|
||||
clipboard.request_text(receive_clipboard_text, False)
|
||||
|
||||
def on_clear_url(self, widget, icon_position, event):
|
||||
self.entry_url.set_text('')
|
||||
|
||||
def on_btn_close_clicked(self, widget):
|
||||
self.gPodderAddPodcast.destroy()
|
||||
|
||||
|
@ -76,7 +80,7 @@ class gPodderAddPodcast(BuilderWidget):
|
|||
|
||||
def receive_clipboard_text(self, clipboard, text, data=None):
|
||||
if text is not None:
|
||||
self.entry_url.set_text(text).strip()
|
||||
self.entry_url.set_text(text.strip())
|
||||
else:
|
||||
self.show_message(_('Nothing to paste.'), _('Clipboard is empty'))
|
||||
|
||||
|
|
|
@ -17,7 +17,9 @@
|
|||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
|
||||
from gi.repository import GObject, Gtk, Pango
|
||||
import time
|
||||
|
||||
from gi.repository import GLib, Gtk, Pango
|
||||
|
||||
import gpodder
|
||||
from gpodder.gtkui.widgets import SpinningProgressIndicator
|
||||
|
@ -35,7 +37,10 @@ class ProgressIndicator(object):
|
|||
def __init__(self, title, subtitle=None, cancellable=False, parent=None):
|
||||
self.title = title
|
||||
self.subtitle = subtitle
|
||||
self.cancellable = cancellable
|
||||
self.cancellable = True if cancellable else False
|
||||
self.cancel_callback = cancellable
|
||||
self.cancel_id = 0
|
||||
self.next_update = time.time() + (self.DELAY / 1000)
|
||||
self.parent = parent
|
||||
self.dialog = None
|
||||
self.progressbar = None
|
||||
|
@ -43,11 +48,12 @@ class ProgressIndicator(object):
|
|||
self._initial_message = None
|
||||
self._initial_progress = None
|
||||
self._progress_set = False
|
||||
self.source_id = GObject.timeout_add(self.DELAY, self._create_progress)
|
||||
self.source_id = GLib.timeout_add(self.DELAY, self._create_progress)
|
||||
|
||||
def _on_delete_event(self, window, event):
|
||||
if self.cancellable:
|
||||
self.dialog.response(Gtk.ResponseType.CANCEL)
|
||||
self.cancellable = False
|
||||
return True
|
||||
|
||||
def _create_progress(self):
|
||||
|
@ -55,6 +61,14 @@ class ProgressIndicator(object):
|
|||
0, 0, Gtk.ButtonsType.CANCEL, self.subtitle or self.title)
|
||||
self.dialog.set_modal(True)
|
||||
self.dialog.connect('delete-event', self._on_delete_event)
|
||||
if self.cancellable:
|
||||
def cancel_callback(dialog, response):
|
||||
self.cancellable = False
|
||||
self.dialog.set_deletable(False)
|
||||
self.dialog.set_response_sensitive(Gtk.ResponseType.CANCEL, False)
|
||||
if callable(self.cancel_callback):
|
||||
self.cancel_callback(dialog, response)
|
||||
self.cancel_id = self.dialog.connect('response', cancel_callback)
|
||||
self.dialog.set_title(self.title)
|
||||
self.dialog.set_deletable(self.cancellable)
|
||||
|
||||
|
@ -83,8 +97,10 @@ class ProgressIndicator(object):
|
|||
self.dialog.set_image(self.indicator)
|
||||
self.dialog.show_all()
|
||||
|
||||
GObject.source_remove(self.source_id)
|
||||
self.source_id = GObject.timeout_add(self.INTERVAL, self._update_gui)
|
||||
self._update_gui()
|
||||
|
||||
# previous self.source_id timeout is removed when this returns False
|
||||
self.source_id = GLib.timeout_add(self.INTERVAL, self._update_gui)
|
||||
return False
|
||||
|
||||
def _update_gui(self):
|
||||
|
@ -92,8 +108,16 @@ class ProgressIndicator(object):
|
|||
self.indicator.step_animation()
|
||||
if not self._progress_set and self.progressbar:
|
||||
self.progressbar.pulse()
|
||||
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)
|
||||
|
@ -109,5 +133,8 @@ class ProgressIndicator(object):
|
|||
|
||||
def on_finished(self):
|
||||
if self.dialog is not None:
|
||||
if self.cancel_id:
|
||||
self.dialog.disconnect(self.cancel_id)
|
||||
self.dialog.destroy()
|
||||
GObject.source_remove(self.source_id)
|
||||
if self.source_id:
|
||||
GLib.source_remove(self.source_id)
|
||||
|
|
|
@ -20,7 +20,7 @@
|
|||
|
||||
import html
|
||||
|
||||
from gi.repository import GObject, Gtk
|
||||
from gi.repository import GLib, GObject, Gtk
|
||||
|
||||
|
||||
class TagCloud(Gtk.Layout):
|
||||
|
@ -100,7 +100,7 @@ class TagCloud(Gtk.Layout):
|
|||
def unrelayout():
|
||||
self._in_relayout = False
|
||||
return False
|
||||
GObject.idle_add(unrelayout)
|
||||
GLib.idle_add(unrelayout)
|
||||
|
||||
|
||||
GObject.type_register(TagCloud)
|
||||
|
|
|
@ -62,7 +62,7 @@ import gi # isort:skip
|
|||
gi.require_version('Gtk', '3.0') # isort:skip
|
||||
gi.require_version('Gdk', '3.0') # isort:skip
|
||||
gi.require_version('Handy', '1') # isort:skip
|
||||
from gi.repository import Gdk, GdkPixbuf, Gio, GLib, GObject, Gtk, Pango # isort:skip
|
||||
from gi.repository import Gdk, GdkPixbuf, Gio, GLib, Gtk, Pango # isort:skip
|
||||
from gi.repository import Handy # isort:skip
|
||||
|
||||
|
||||
|
@ -204,6 +204,9 @@ class gPodder(BuilderWidget, dbus.service.Object):
|
|||
self.config.connect_gtk_spinbutton('limit_rate_value', self.spinLimitDownloads)
|
||||
self.config.connect_gtk_togglebutton('limit_rate', self.cbLimitDownloads)
|
||||
|
||||
self.spinMaxDownloads.set_sensitive(self.cbMaxDownloads.get_active())
|
||||
self.spinLimitDownloads.set_sensitive(self.cbLimitDownloads.get_active())
|
||||
|
||||
# When the amount of maximum downloads changes, notify the queue manager
|
||||
def changed_cb(spinbutton):
|
||||
return self.download_queue_manager.update_max_downloads()
|
||||
|
@ -530,7 +533,7 @@ class gPodder(BuilderWidget, dbus.service.Object):
|
|||
if response_id == Gtk.ResponseType.OK:
|
||||
selection = self.treeDownloads.get_selection()
|
||||
selection.select_all()
|
||||
selected_tasks, _, _, _, _, _ = self.downloads_list_get_selection()
|
||||
selected_tasks = self.downloads_list_get_selection()[0]
|
||||
selection.unselect_all()
|
||||
self._for_each_task_set_status(selected_tasks, download.DownloadTask.QUEUED)
|
||||
self.resume_all_infobar.set_revealed(False)
|
||||
|
@ -549,14 +552,16 @@ 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()
|
||||
|
||||
def finish_progress_callback(resumable_episodes):
|
||||
def offer_resuming():
|
||||
if resumable_episodes:
|
||||
self.download_episode_list_paused(resumable_episodes)
|
||||
self.download_episode_list_paused(resumable_episodes, hide_progress=True)
|
||||
self.resume_all_infobar.set_revealed(True)
|
||||
self.on_show_progress_activate()
|
||||
|
||||
logger.debug("find_partial_downloads done, calling extensions")
|
||||
gpodder.user_extensions.on_find_partial_downloads_done()
|
||||
|
||||
|
@ -778,7 +783,7 @@ class gPodder(BuilderWidget, dbus.service.Object):
|
|||
important=True)
|
||||
util.idle_add(show_error, e)
|
||||
|
||||
util.idle_add(indicator.on_finished)
|
||||
indicator.on_finished()
|
||||
|
||||
def on_button_subscribe_clicked(self, button):
|
||||
self.on_itemImportChannels_activate(button)
|
||||
|
@ -1336,7 +1341,7 @@ class gPodder(BuilderWidget, dbus.service.Object):
|
|||
# (('text/uri-list', 0, 0),), Gdk.DragAction.COPY)
|
||||
#
|
||||
# def drag_data_get(tree, context, selection_data, info, timestamp):
|
||||
# uris = ['file://' + e.local_filename(create=False)
|
||||
# uris = ['file://' + urllib.parse.quote(e.local_filename(create=False))
|
||||
# for e in self.get_selected_episodes()
|
||||
# if e.was_downloaded(and_exists=True)]
|
||||
# selection_data.set_uris(uris)
|
||||
|
@ -1497,7 +1502,7 @@ class gPodder(BuilderWidget, dbus.service.Object):
|
|||
self.things_adding_tasks -= 1
|
||||
if not self.download_list_update_enabled:
|
||||
self.update_downloads_list()
|
||||
GObject.timeout_add(1500, self.update_downloads_list)
|
||||
util.IdleTimeout(1500, self.update_downloads_list)
|
||||
self.download_list_update_enabled = True
|
||||
|
||||
def cleanup_downloads(self):
|
||||
|
@ -1544,7 +1549,7 @@ class gPodder(BuilderWidget, dbus.service.Object):
|
|||
try:
|
||||
model = self.download_status_model
|
||||
|
||||
downloading, synchronizing, pausing, cancelling, queued, paused, failed, finished, others = (0,) * 9
|
||||
downloading, synchronizing, pausing, cancelling, queued, paused, failed, finished = (0,) * 8
|
||||
total_speed, total_size, done_size = 0, 0, 0
|
||||
files_downloading = 0
|
||||
|
||||
|
@ -1577,8 +1582,6 @@ class gPodder(BuilderWidget, dbus.service.Object):
|
|||
total_speed += speed
|
||||
elif activity == download.DownloadTask.ACTIVITY_SYNCHRONIZE:
|
||||
synchronizing += 1
|
||||
else:
|
||||
others += 1
|
||||
elif status == download.DownloadTask.PAUSING:
|
||||
pausing += 1
|
||||
if activity == download.DownloadTask.ACTIVITY_DOWNLOAD:
|
||||
|
@ -1595,10 +1598,6 @@ class gPodder(BuilderWidget, dbus.service.Object):
|
|||
failed += 1
|
||||
elif status == download.DownloadTask.DONE:
|
||||
finished += 1
|
||||
else:
|
||||
others += 1
|
||||
|
||||
# TODO: 'others' is not used
|
||||
|
||||
# Remember which tasks we have seen after this run
|
||||
self.download_tasks_seen = download_tasks_seen
|
||||
|
@ -1706,6 +1705,24 @@ class gPodder(BuilderWidget, dbus.service.Object):
|
|||
elif name == 'ui.gtk.episode_list.columns':
|
||||
# self.update_episode_list_columns_visibility()
|
||||
pass
|
||||
elif name == 'limit.downloads.concurrent_max':
|
||||
# Do not allow value to be set below 1
|
||||
if new_value < 1:
|
||||
self.config.limit.downloads.concurrent_max = 1
|
||||
return
|
||||
# Clamp current value to new maximum value
|
||||
if self.config.limit.downloads.concurrent > new_value:
|
||||
self.config.limit.downloads.concurrent = new_value
|
||||
self.spinMaxDownloads.get_adjustment().set_upper(new_value)
|
||||
elif name == 'limit.downloads.concurrent':
|
||||
if self.config.clamp_range('limit.downloads.concurrent', 1, self.config.limit.downloads.concurrent_max):
|
||||
return
|
||||
self.spinMaxDownloads.set_value(new_value)
|
||||
elif name == 'limit.bandwidth.kbps':
|
||||
adjustment = self.spinLimitDownloads.get_adjustment()
|
||||
if self.config.clamp_range('limit.bandwidth.kbps', adjustment.get_lower(), adjustment.get_upper()):
|
||||
return
|
||||
self.spinLimitDownloads.set_value(new_value)
|
||||
|
||||
def on_treeview_query_tooltip(self, treeview, x, y, keyboard_tooltip, tooltip):
|
||||
# With get_bin_window, we get the window that contains the rows without
|
||||
|
@ -1962,7 +1979,40 @@ 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)
|
||||
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)
|
||||
|
||||
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
|
||||
episode_urls = set()
|
||||
model = self.treeDownloads.get_model()
|
||||
for row_reference, task in tasks:
|
||||
|
@ -2011,6 +2061,10 @@ 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):
|
||||
break
|
||||
# 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
|
||||
|
@ -2340,10 +2394,10 @@ class gPodder(BuilderWidget, dbus.service.Object):
|
|||
|
||||
# play icon and label
|
||||
# if open_instead_of_play or not is_episode_selected:
|
||||
# self.toolPlay.set_icon_name('document-open')
|
||||
# self.toolPlay.set_icon_name('document-open-symbolic')
|
||||
# self.toolPlay.set_label(_('Open'))
|
||||
# else:
|
||||
# self.toolPlay.set_icon_name('media-playback-start')
|
||||
# self.toolPlay.set_icon_name('media-playback-start-symbolic')
|
||||
#
|
||||
# downloaded = all(e.was_downloaded(and_exists=True) for e in episodes)
|
||||
# downloading = any(e.downloading for e in episodes)
|
||||
|
@ -2601,7 +2655,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
|
||||
GObject.timeout_add(remaining_seconds * 1000, self.refresh_episode_dates)
|
||||
GLib.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):
|
||||
|
@ -2793,6 +2847,7 @@ class gPodder(BuilderWidget, dbus.service.Object):
|
|||
|
||||
def on_after_update():
|
||||
progress.on_finished()
|
||||
|
||||
# Report already-existing subscriptions to the user
|
||||
if existing:
|
||||
title = _('Existing subscriptions skipped')
|
||||
|
@ -2960,9 +3015,10 @@ class gPodder(BuilderWidget, dbus.service.Object):
|
|||
|
||||
self.mygpo_client.process_episode_actions(self.find_episode)
|
||||
|
||||
indicator.on_finished()
|
||||
self.db.commit()
|
||||
|
||||
indicator.on_finished()
|
||||
|
||||
def _update_cover(self, channel):
|
||||
if channel is not None:
|
||||
self.cover_downloader.request_cover(channel)
|
||||
|
@ -3217,7 +3273,7 @@ class gPodder(BuilderWidget, dbus.service.Object):
|
|||
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)
|
||||
self._for_each_task_set_status(selected_tasks, status=None)
|
||||
return
|
||||
|
||||
if not episodes:
|
||||
|
@ -3251,8 +3307,6 @@ class gPodder(BuilderWidget, dbus.service.Object):
|
|||
parent=self.get_dialog_parent())
|
||||
|
||||
def finish_deletion(episode_urls, channel_urls):
|
||||
progress.on_finished()
|
||||
|
||||
# Episodes have been deleted - persist the database
|
||||
self.db.commit()
|
||||
|
||||
|
@ -3261,6 +3315,8 @@ class gPodder(BuilderWidget, dbus.service.Object):
|
|||
self.update_header_bar_subtitle()
|
||||
self.play_or_download()
|
||||
|
||||
progress.on_finished()
|
||||
|
||||
@util.run_in_background
|
||||
def thread_proc():
|
||||
episode_urls = set()
|
||||
|
@ -3445,11 +3501,21 @@ class gPodder(BuilderWidget, dbus.service.Object):
|
|||
|
||||
util.idle_add(show_welcome_window)
|
||||
|
||||
def download_episode_list_paused(self, episodes):
|
||||
self.download_episode_list(episodes, True)
|
||||
def download_episode_list_paused(self, episodes, hide_progress=False):
|
||||
self.download_episode_list(episodes, True, hide_progress=hide_progress)
|
||||
|
||||
def download_episode_list(self, episodes, add_paused=False, force_start=False, downloader=None):
|
||||
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(episodes)
|
||||
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
|
||||
|
||||
for task in tasks:
|
||||
with task:
|
||||
if add_paused:
|
||||
|
@ -3457,12 +3523,24 @@ class gPodder(BuilderWidget, dbus.service.Object):
|
|||
else:
|
||||
self.mygpo_client.on_download([task.episode])
|
||||
self.queue_task(task, force_start)
|
||||
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:
|
||||
break
|
||||
if tasks or queued_existing_task:
|
||||
self.set_download_list_state(gPodderSyncUI.DL_ONEOFF)
|
||||
# Flush updated episode status
|
||||
if self.mygpo_client.can_access_webservice():
|
||||
self.mygpo_client.flush()
|
||||
|
||||
if progress_indicator:
|
||||
progress_indicator.on_finished()
|
||||
|
||||
queued_existing_task = False
|
||||
new_tasks = []
|
||||
|
||||
|
@ -3907,10 +3985,6 @@ class gPodder(BuilderWidget, dbus.service.Object):
|
|||
|
||||
def on_wNotebook_switch_page(self, notebook, page, page_num):
|
||||
self.play_or_download(in_downloads=page_num > 0)
|
||||
if page_num == 0:
|
||||
# The infobar in the downloads tab should be hidden
|
||||
# when the user switches away from the downloads tab
|
||||
self.resume_all_infobar.set_revealed(False)
|
||||
|
||||
def on_treeChannels_row_activated(self, widget, path, *args):
|
||||
self.navigate_from_shownotes()
|
||||
|
@ -4081,7 +4155,7 @@ class gPodder(BuilderWidget, dbus.service.Object):
|
|||
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=download.DownloadTask.QUEUED, force_start=False)
|
||||
self._for_each_task_set_status(selected_tasks, download.DownloadTask.QUEUED)
|
||||
else:
|
||||
episodes = [e for e in self.get_selected_episodes() if e.can_download()]
|
||||
self.download_episode_list(episodes)
|
||||
|
@ -4094,7 +4168,7 @@ class gPodder(BuilderWidget, dbus.service.Object):
|
|||
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=download.DownloadTask.PAUSING, force_start=False)
|
||||
self._for_each_task_set_status(selected_tasks, download.DownloadTask.PAUSING)
|
||||
else:
|
||||
for episode in self.get_selected_episodes():
|
||||
if episode.can_pause():
|
||||
|
@ -4158,7 +4232,7 @@ class gPodder(BuilderWidget, dbus.service.Object):
|
|||
def restart_auto_update_timer(self):
|
||||
if self._auto_update_timer_source_id is not None:
|
||||
logger.debug('Removing existing auto update timer.')
|
||||
GObject.source_remove(self._auto_update_timer_source_id)
|
||||
GLib.source_remove(self._auto_update_timer_source_id)
|
||||
self._auto_update_timer_source_id = None
|
||||
|
||||
if (self.config.auto_update_feeds and
|
||||
|
@ -4166,7 +4240,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 = GObject.timeout_add(
|
||||
self._auto_update_timer_source_id = GLib.timeout_add(
|
||||
interval, self._on_auto_update_timer)
|
||||
|
||||
def _on_auto_update_timer(self):
|
||||
|
|
|
@ -30,7 +30,7 @@ import re
|
|||
import time
|
||||
from itertools import groupby
|
||||
|
||||
from gi.repository import GdkPixbuf, GObject, Gtk
|
||||
from gi.repository import GdkPixbuf, GLib, GObject, Gtk
|
||||
|
||||
import gpodder
|
||||
from gpodder import coverart, model, query, util
|
||||
|
@ -331,10 +331,10 @@ class EpisodeListModel(Gtk.ListStore):
|
|||
|
||||
def _update_from_episodes(self, episodes, include_description):
|
||||
if self.background_update_tag is not None:
|
||||
GObject.source_remove(self.background_update_tag)
|
||||
GLib.source_remove(self.background_update_tag)
|
||||
|
||||
self.background_update = BackgroundUpdate(self, episodes, include_description)
|
||||
self.background_update_tag = GObject.idle_add(self._update_background)
|
||||
self.background_update_tag = GLib.idle_add(self._update_background)
|
||||
|
||||
def _update_background(self):
|
||||
if self.background_update is not None:
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# gPodder - A media aggregator and podcast client
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
#!/usr/bin/python
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# gPodder - A media aggregator and podcast client
|
||||
|
|
|
@ -497,7 +497,7 @@ class PodcastEpisode(PodcastModelObject):
|
|||
PAUSING and PAUSED tasks can be resumed.
|
||||
"""
|
||||
return not self.was_downloaded(and_exists=True) and (
|
||||
not self.download_task
|
||||
self.download_task is None
|
||||
or self.download_task.can_queue()
|
||||
or self.download_task.status == self.download_task.PAUSING)
|
||||
|
||||
|
@ -505,20 +505,20 @@ class PodcastEpisode(PodcastModelObject):
|
|||
"""
|
||||
gPodder.on_pause_selected_episodes() filters selection with this method.
|
||||
"""
|
||||
return self.download_task and self.download_task.can_pause()
|
||||
return self.download_task is not None 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.can_cancel()
|
||||
return self.download_task is not None and self.download_task.can_cancel()
|
||||
|
||||
def can_delete(self):
|
||||
"""
|
||||
gPodder.delete_episode_list() filters out locked episodes, and cancels all unlocked tasks in selection.
|
||||
"""
|
||||
return self.state != gpodder.STATE_DELETED and not self.archive and (
|
||||
not self.download_task or self.download_task.status == self.download_task.FAILED)
|
||||
self.download_task is None or self.download_task.status == self.download_task.FAILED)
|
||||
|
||||
def can_lock(self):
|
||||
"""
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# gPodder - A media aggregator and podcast client
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# gPodder - A media aggregator and podcast client
|
||||
|
|
|
@ -458,16 +458,20 @@ class MP3PlayerDevice(Device):
|
|||
self.destination.get_uri(), err.message)
|
||||
return False
|
||||
|
||||
if info.get_file_type() != Gio.FileType.DIRECTORY:
|
||||
logger.error('destination %s is not a directory', self.destination.get_uri())
|
||||
return False
|
||||
|
||||
# open is ok if the target is a directory, and it can be written to
|
||||
# for smb, query_info doesn't return FILE_ATTRIBUTE_ACCESS_CAN_WRITE,
|
||||
# -- if that's the case, just assume that it's writable
|
||||
if (info.get_file_type() == Gio.FileType.DIRECTORY and (
|
||||
not info.has_attribute(Gio.FILE_ATTRIBUTE_ACCESS_CAN_WRITE) or
|
||||
info.get_attribute_boolean(Gio.FILE_ATTRIBUTE_ACCESS_CAN_WRITE))):
|
||||
if (not info.has_attribute(Gio.FILE_ATTRIBUTE_ACCESS_CAN_WRITE) or
|
||||
info.get_attribute_boolean(Gio.FILE_ATTRIBUTE_ACCESS_CAN_WRITE)):
|
||||
self.notify('status', _('MP3 player opened'))
|
||||
self.tracks_list = self.get_all_tracks()
|
||||
return True
|
||||
|
||||
logger.error('destination %s is not writable', self.destination.get_uri())
|
||||
return False
|
||||
|
||||
def get_episode_folder_on_device(self, episode):
|
||||
|
|
|
@ -105,14 +105,22 @@ class gPodderSyncUI(object):
|
|||
done_callback()
|
||||
return
|
||||
|
||||
if not device.open():
|
||||
try:
|
||||
if not device.open():
|
||||
self._show_message_cannot_open()
|
||||
if done_callback:
|
||||
done_callback()
|
||||
return
|
||||
else:
|
||||
# Only set if device is configured and opened successfully
|
||||
self.device = device
|
||||
except Exception as err:
|
||||
logger.error('opening destination %s failed with %s',
|
||||
device.destination.get_uri(), err.message)
|
||||
self._show_message_cannot_open()
|
||||
if done_callback:
|
||||
done_callback()
|
||||
return
|
||||
else:
|
||||
# Only set if device is configured and opened successfully
|
||||
self.device = device
|
||||
|
||||
if episodes is None:
|
||||
force_played = False
|
||||
|
|
|
@ -1318,12 +1318,40 @@ def idle_add(func, *args):
|
|||
as possible from the main UI thread.
|
||||
"""
|
||||
if gpodder.ui.gtk:
|
||||
from gi.repository import GObject
|
||||
GObject.idle_add(func, *args)
|
||||
from gi.repository import GLib
|
||||
GLib.idle_add(func, *args)
|
||||
else:
|
||||
func(*args)
|
||||
|
||||
|
||||
class IdleTimeout(object):
|
||||
"""Run a function in the main GUI thread at regular intervals since the last run
|
||||
|
||||
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
|
||||
the timer after the function finishes, allowing other callbacks to run.
|
||||
"""
|
||||
def __init__(self, milliseconds, func, *args):
|
||||
if not gpodder.ui.gtk:
|
||||
raise Exception('util.IdleTimeout() is only supported by Gtk+')
|
||||
self.milliseconds = milliseconds
|
||||
self.func = func
|
||||
from gi.repository import GLib
|
||||
self.id = GLib.timeout_add(milliseconds, self._callback, *args)
|
||||
|
||||
def _callback(self, *args):
|
||||
self.cancel()
|
||||
if self.func(*args):
|
||||
from gi.repository import GLib
|
||||
self.id = GLib.timeout_add(self.milliseconds, self._callback, *args)
|
||||
|
||||
def cancel(self):
|
||||
if self.id:
|
||||
from gi.repository import GLib
|
||||
GLib.source_remove(self.id)
|
||||
self.id = 0
|
||||
|
||||
|
||||
def bluetooth_available():
|
||||
"""
|
||||
Returns True or False depending on the availability
|
||||
|
@ -1358,7 +1386,7 @@ def bluetooth_send_file(filename):
|
|||
return False
|
||||
|
||||
|
||||
def format_time(value):
|
||||
def format_time(seconds):
|
||||
"""Format a seconds value to a string
|
||||
|
||||
>>> format_time(0)
|
||||
|
@ -1369,12 +1397,22 @@ def format_time(value):
|
|||
'01:00:00'
|
||||
>>> format_time(10921)
|
||||
'03:02:01'
|
||||
>>> format_time(86401)
|
||||
'24:00:01'
|
||||
"""
|
||||
dt = datetime.datetime.utcfromtimestamp(value)
|
||||
if dt.hour == 0:
|
||||
return dt.strftime('%M:%S')
|
||||
hours = 0
|
||||
minutes = 0
|
||||
if seconds >= 3600:
|
||||
hours = seconds // 3600
|
||||
seconds -= hours * 3600
|
||||
if seconds >= 60:
|
||||
minutes = seconds // 60
|
||||
seconds -= minutes * 60
|
||||
|
||||
if hours == 0:
|
||||
return '%02d:%02d' % (minutes, seconds)
|
||||
else:
|
||||
return dt.strftime('%H:%M:%S')
|
||||
return '%02d:%02d:%02d' % (hours, minutes, seconds)
|
||||
|
||||
|
||||
def parse_time(value):
|
||||
|
|
|
@ -66,14 +66,14 @@ cp -a "$checkout"/tools/mac-osx/launcher.py "$resources"/
|
|||
cp -a "$checkout"/tools/mac-osx/make_cert_pem.py "$resources"/bin
|
||||
|
||||
# install gPodder hard dependencies
|
||||
$run_pip install setuptools wheel
|
||||
$run_pip install mygpoclient==1.9 podcastparser==0.6.8 requests[socks]==2.28.1
|
||||
$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
|
||||
# install brotli and pycryptodomex (build from source)
|
||||
$run_pip debug -v
|
||||
$run_pip install -v brotli
|
||||
$run_pip install -v pycryptodomex
|
||||
$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
|
||||
$run_pip install html5lib==1.1 mutagen==1.45.1 yt-dlp || exit 1
|
||||
|
||||
cd "$checkout"
|
||||
touch share/applications/gpodder{,-url-handler}.desktop
|
||||
|
|
|
@ -473,10 +473,10 @@ function build_portable_installer {
|
|||
mkdir "$PORTABLE"/config
|
||||
cp -RT "${MINGW_ROOT}" "$PORTABLE"/data
|
||||
|
||||
rm -Rf 7zout 7z1604.exe
|
||||
rm -Rf 7zout 7z2201.exe
|
||||
7z a payload.7z "$PORTABLE"
|
||||
wget -P "$DIR" -c http://www.7-zip.org/a/7z1604.exe
|
||||
7z x -o7zout 7z1604.exe
|
||||
wget -P "$DIR" -c http://www.7-zip.org/a/7z2201.exe
|
||||
7z x -o7zout 7z2201.exe
|
||||
cat 7zout/7z.sfx payload.7z > "$PORTABLE".exe
|
||||
rm -Rf 7zout 7z1604.exe payload.7z "$PORTABLE"
|
||||
rm -Rf 7zout 7z2201.exe payload.7z "$PORTABLE"
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue