Merge branch 'master' into dev-adaptive

This commit is contained in:
Teemu Ikonen 2022-10-28 21:59:50 +03:00
commit 46dbc80d8b
70 changed files with 12562 additions and 11705 deletions

View File

@ -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/

View File

@ -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
View File

@ -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:

View File

@ -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
##########################################################################

690
po/ca.po

File diff suppressed because it is too large Load Diff

680
po/cs.po

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

682
po/da.po

File diff suppressed because it is too large Load Diff

682
po/de.po

File diff suppressed because it is too large Load Diff

682
po/el.po

File diff suppressed because it is too large Load Diff

682
po/es.po

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

682
po/eu.po

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

682
po/fi.po

File diff suppressed because it is too large Load Diff

1004
po/fr.po

File diff suppressed because it is too large Load Diff

682
po/gl.po

File diff suppressed because it is too large Load Diff

682
po/he.po

File diff suppressed because it is too large Load Diff

682
po/hu.po

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

682
po/it.po

File diff suppressed because it is too large Load Diff

682
po/kk.po

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

686
po/nb.po

File diff suppressed because it is too large Load Diff

680
po/nl.po

File diff suppressed because it is too large Load Diff

684
po/nn.po

File diff suppressed because it is too large Load Diff

682
po/pl.po

File diff suppressed because it is too large Load Diff

682
po/pt.po

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

696
po/ro.po

File diff suppressed because it is too large Load Diff

708
po/ru.po

File diff suppressed because it is too large Load Diff

680
po/sk.po

File diff suppressed because it is too large Load Diff

682
po/sv.po

File diff suppressed because it is too large Load Diff

682
po/tr.po

File diff suppressed because it is too large Load Diff

696
po/uk.po

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -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"

View File

@ -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'),

View File

@ -1,4 +1,3 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
####
# 01/2011 Bernd Schlapsi <brot@gmx.info>

View File

@ -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

View File

@ -1,4 +1,3 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
####
# 01/2011 Bernd Schlapsi <brot@gmx.info>

View File

@ -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))

View File

@ -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})

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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

View File

@ -1,4 +1,3 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
#
# gPodder - A media aggregator and podcast client

View File

@ -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,

View File

@ -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)

View File

@ -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 = {}

View File

@ -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):

View File

@ -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'))

View File

@ -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)

View File

@ -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)

View File

@ -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):

View File

@ -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:

View File

@ -1,4 +1,3 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
#
# gPodder - A media aggregator and podcast client

View File

@ -1,4 +1,4 @@
#!/usr/bin/python
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
#
# gPodder - A media aggregator and podcast client

View File

@ -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):
"""

View File

@ -1,4 +1,3 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
#
# gPodder - A media aggregator and podcast client

View File

@ -1,4 +1,3 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
#
# gPodder - A media aggregator and podcast client

View File

@ -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):

View File

@ -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

View File

@ -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):

View File

@ -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

View File

@ -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"
}