Merge tag '3.11.2' into dev-adaptive
gPodder 3.11.2 release
This commit is contained in:
commit
490d5695a9
|
@ -3,7 +3,7 @@ version: 2
|
|||
jobs:
|
||||
release-from-macos:
|
||||
macos:
|
||||
xcode: "13.2.1"
|
||||
xcode: "13.4.1"
|
||||
shell: /bin/bash --login -o pipefail
|
||||
environment:
|
||||
- BUNDLE_TAG: 22.8.27
|
||||
|
|
|
@ -12,7 +12,7 @@ jobs:
|
|||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
python-version: ['3.10']
|
||||
python-version: ['3.7', '3.8', '3.9', '3.10', '3.11']
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
@ -24,7 +24,7 @@ jobs:
|
|||
run: |
|
||||
sudo apt-get update -q
|
||||
sudo apt-get install intltool desktop-file-utils
|
||||
pip3 install pytest-cov minimock pycodestyle isort requests pytest pytest-httpserver
|
||||
pip3 install pytest-cov minimock pycodestyle isort codespell requests pytest pytest-httpserver
|
||||
pip3 install podcastparser mygpoclient
|
||||
- name: Lint
|
||||
run: make lint
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
## Getting started <!-- omit in toc -->
|
||||
|
||||
Before you begin:
|
||||
- Ensure you are using Python 3.5+
|
||||
- Ensure you are using Python 3.7+
|
||||
- Check out the [existing issues](https://github.com/gpodder/gpodder/issues)
|
||||
|
||||
Contributions are made to this repo via Issues and Pull Requests (PRs). Make sure to search for existing Issues and PRs before creating your own.
|
||||
|
|
|
@ -26,7 +26,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|||
|
||||
## Dependencies
|
||||
|
||||
- [Python 3.5](http://python.org/) or newer
|
||||
- [Python 3.7](http://python.org/) or newer
|
||||
- [Podcastparser](http://gpodder.org/podcastparser/) 0.6.0 or newer
|
||||
- [mygpoclient](http://gpodder.org/mygpoclient/) 1.7 or newer
|
||||
- [requests](https://requests.readthedocs.io) 2.24.0 or newer
|
||||
|
@ -135,7 +135,7 @@ into an alternative root (default /) and prefix (default /usr):
|
|||
[*Debian*](https://wiki.debian.org/Python#Deviations_from_upstream) and *Ubuntu* use `dist-packages`
|
||||
instead of `site-packages` for third party installs, so you'll want something like:
|
||||
|
||||
sudo python3 setup.py install --root / --prefix /usr/local --optimize=1 --install-lib=/usr/local/lib/python3.5/dist-packages
|
||||
sudo python3 setup.py install --root / --prefix /usr/local --optimize=1 --install-lib=/usr/local/lib/python3.10/dist-packages
|
||||
|
||||
In fact, first try running `python -c "import sys; print(sys.path)"` to check what is the exact path.
|
||||
It depends on your version of python.
|
||||
|
|
4
makefile
4
makefile
|
@ -69,8 +69,12 @@ unittest:
|
|||
|
||||
ISORTOPTS := -c share src/gpodder tools bin/* *.py
|
||||
lint:
|
||||
pycodestyle --version
|
||||
pycodestyle share src/gpodder tools bin/* *.py
|
||||
|
||||
isort --version
|
||||
isort -q $(ISORTOPTS) || isort --df $(ISORTOPTS)
|
||||
codespell --quiet-level 3 --skip "./.git,*.po,./share/applications/gpodder.desktop"
|
||||
|
||||
release: distclean
|
||||
$(PYTHON) setup.py sdist
|
||||
|
|
490
po/cs_CZ.po
490
po/cs_CZ.po
File diff suppressed because it is too large
Load Diff
490
po/es_ES.po
490
po/es_ES.po
File diff suppressed because it is too large
Load Diff
490
po/es_MX.po
490
po/es_MX.po
File diff suppressed because it is too large
Load Diff
488
po/fa_IR.po
488
po/fa_IR.po
File diff suppressed because it is too large
Load Diff
488
po/id_ID.po
488
po/id_ID.po
File diff suppressed because it is too large
Load Diff
490
po/ko_KR.po
490
po/ko_KR.po
File diff suppressed because it is too large
Load Diff
488
po/messages.pot
488
po/messages.pot
File diff suppressed because it is too large
Load Diff
490
po/pt_BR.po
490
po/pt_BR.po
File diff suppressed because it is too large
Load Diff
490
po/zh_CN.po
490
po/zh_CN.po
File diff suppressed because it is too large
Load Diff
2
setup.py
2
setup.py
|
@ -25,7 +25,7 @@ from distutils.core import setup
|
|||
|
||||
installing = ('install' in sys.argv and '--help' not in sys.argv)
|
||||
|
||||
# distutils depends on setup.py beeing executed from the same dir.
|
||||
# distutils depends on setup.py being executed from the same dir.
|
||||
# Most of our custom commands work either way, but this makes
|
||||
# it work in all cases.
|
||||
os.chdir(os.path.dirname(os.path.realpath(__file__)))
|
||||
|
|
|
@ -60,7 +60,7 @@ class gPodderExtension:
|
|||
#
|
||||
# https://docs.python.org/3/library/subprocess.html#subprocess.Popen
|
||||
#
|
||||
# This is expecially important for extensions responding to
|
||||
# This is especially important for extensions responding to
|
||||
# on_episode_downloaded(), which runs whenever a download finishes.
|
||||
#
|
||||
# Otherwise that process will inherit ALL file descriptors gPodder
|
||||
|
|
|
@ -242,7 +242,7 @@ RESUMERS = [
|
|||
# with https://github.com/Serranya/deadbeef-mpris2-plugin
|
||||
MPRISResumer('resume in deadbeef', 'DeaDBeeF', ['deadbeef'], 'org.mpris.MediaPlayer2.DeaDBeeF'),
|
||||
|
||||
# the gPodder Dowloads directory must be in gmusicbrowser's library
|
||||
# the gPodder Downloads directory must be in gmusicbrowser's library
|
||||
MPRISResumer('resume in gmusicbrowser', 'gmusicbrowser', ['gmusicbrowser'], 'org.mpris.MediaPlayer2.gmusicbrowser'),
|
||||
|
||||
# Audacious doesn't implement MPRIS2.OpenUri
|
||||
|
|
|
@ -100,7 +100,7 @@ try {{
|
|||
[Windows.UI.Notifications.ToastNotificationManager]::CreateToastNotifier($APP_ID).Show($toast)
|
||||
Remove-Item -LiteralPath $MyInvocation.MyCommand.Path -Force # Delete this script temp file.
|
||||
}} else {{
|
||||
# use older Baloon notification when not on Windows 10
|
||||
# use older Balloon notification when not on Windows 10
|
||||
[System.Reflection.Assembly]::LoadWithPartialName("System.Windows.Forms")
|
||||
$o = New-Object System.Windows.Forms.NotifyIcon
|
||||
|
||||
|
|
|
@ -39,7 +39,7 @@ class gPodderExtension:
|
|||
device_folder = device.get_episode_folder_on_device(episode)
|
||||
episode_art = os.path.join(episode_folder, "folder.jpg")
|
||||
device_art = os.path.join(device_folder, self.config.art_name_on_device)
|
||||
# make sure we have art to copy and it doesnt already exist
|
||||
# make sure we have art to copy and it doesn't already exist
|
||||
if os.path.isfile(episode_art) and not os.path.isfile(device_art):
|
||||
logger.info('Syncing cover art for %s', episode.channel.title)
|
||||
# copy and rename art
|
||||
|
|
|
@ -131,7 +131,7 @@ class AudioFile(object):
|
|||
|
||||
def get_cover_picture(self, cover):
|
||||
""" Returns mutagen Picture class for the cover image
|
||||
Usefull for OGG and FLAC format
|
||||
Useful for OGG and FLAC format
|
||||
|
||||
Picture type = cover image
|
||||
see http://flac.sourceforge.net/documentation_tools_flac.html#encoding_options
|
||||
|
|
|
@ -9,11 +9,12 @@ import os
|
|||
import re
|
||||
import sys
|
||||
import time
|
||||
from collections.abc import Iterable
|
||||
|
||||
try:
|
||||
import yt_dlp as youtube_dl
|
||||
program_name = 'yt-dlp'
|
||||
want_ytdl_version = '2023.02.17'
|
||||
want_ytdl_version = '2023.06.22'
|
||||
except:
|
||||
import youtube_dl
|
||||
program_name = 'youtube-dl'
|
||||
|
@ -41,7 +42,7 @@ __doc__ = 'https://gpodder.github.io/docs/extensions/youtubedl.html'
|
|||
want_ytdl_version_msg = _('Your version of youtube-dl/yt-dlp %(have_version)s has known issues, please upgrade to %(want_version)s or newer.')
|
||||
|
||||
DefaultConfig = {
|
||||
# youtube-dl downloads and parses each video page to get informations about it, which is very slow.
|
||||
# youtube-dl downloads and parses each video page to get information about it, which is very slow.
|
||||
# Set to False to fall back to the fast but limited (only 15 episodes) gpodder code
|
||||
'manage_channel': True,
|
||||
# If for some reason youtube-dl download doesn't work for you, you can fallback to gpodder code.
|
||||
|
@ -108,7 +109,7 @@ class YoutubeCustomDownload(download.CustomDownload):
|
|||
outtmpl = tempname.replace('%', '%%')
|
||||
info, opts = self._ytdl.fetch_info(self._url, outtmpl, self._my_hook)
|
||||
if program_name == 'yt-dlp':
|
||||
default = opts['outtmpl']['default'] if type(opts['outtmpl']) == dict else opts['outtmpl']
|
||||
default = opts['outtmpl']['default'] if isinstance(opts['outtmpl'], dict) else opts['outtmpl']
|
||||
self.partial_filename = os.path.join(opts['paths']['home'], default) % info
|
||||
elif program_name == 'youtube-dl':
|
||||
self.partial_filename = opts['outtmpl'] % info
|
||||
|
@ -296,9 +297,14 @@ class gPodderYoutubeDL(download.CustomDownloader):
|
|||
os.makedirs(cachedir, exist_ok=True)
|
||||
self._ydl_opts = {
|
||||
'cachedir': cachedir,
|
||||
'no_color': True, # prevent escape codes in desktop notifications on errors
|
||||
'noprogress': True, # prevent progress bar from appearing in console
|
||||
}
|
||||
# prevent escape codes in desktop notifications on errors
|
||||
if program_name == 'yt-dlp':
|
||||
self._ydl_opts['color'] = 'no_color'
|
||||
else:
|
||||
self._ydl_opts['no_color'] = True
|
||||
|
||||
if gpodder.verbose:
|
||||
self._ydl_opts['verbose'] = True
|
||||
else:
|
||||
|
@ -306,7 +312,7 @@ class gPodderYoutubeDL(download.CustomDownloader):
|
|||
# Don't create downloaders for URLs supported by these youtube-dl extractors
|
||||
self.ie_blacklist = ["Generic"]
|
||||
# Cache URL regexes from youtube-dl matches here, seed with youtube regex
|
||||
self.regex_cache = [re.compile(r'https://www.youtube.com/watch\?v=.+')]
|
||||
self.regex_cache = [(re.compile(r'https://www.youtube.com/watch\?v=.+'),)]
|
||||
# #686 on windows without a console, sys.stdout is None, causing exceptions
|
||||
# when adding podcasts.
|
||||
# See https://docs.python.org/3/library/sys.html#sys.__stderr__ Note
|
||||
|
@ -384,7 +390,7 @@ class gPodderYoutubeDL(download.CustomDownloader):
|
|||
"""
|
||||
Fetch a channel or playlist contents.
|
||||
|
||||
Doesn't yet fetch video entry informations, so we only get the video id and title.
|
||||
Doesn't yet fetch video entry information, so we only get the video id and title.
|
||||
"""
|
||||
# Duplicate a bit of the YoutubeDL machinery here because we only
|
||||
# want to parse the channel/playlist first, not to fetch video entries.
|
||||
|
@ -448,21 +454,22 @@ class gPodderYoutubeDL(download.CustomDownloader):
|
|||
def is_supported_url(self, url):
|
||||
if url is None:
|
||||
return False
|
||||
if self.regex_cache[0].match(url) is not None:
|
||||
return True
|
||||
for r in self.regex_cache[1:]:
|
||||
if r.match(url) is not None:
|
||||
self.regex_cache.remove(r)
|
||||
self.regex_cache.insert(0, r)
|
||||
for i, res in enumerate(self.regex_cache):
|
||||
if next(filter(None, (r.match(url) for r in res)), None) is not None:
|
||||
if i > 0:
|
||||
self.regex_cache.remove(res)
|
||||
self.regex_cache.insert(0, res)
|
||||
return True
|
||||
with youtube_dl.YoutubeDL(self._ydl_opts) as ydl:
|
||||
# youtube-dl returns a list, yt-dlp returns a dict
|
||||
ies = ydl._ies
|
||||
if type(ydl._ies) == dict:
|
||||
if isinstance(ydl._ies, dict):
|
||||
ies = ydl._ies.values()
|
||||
for ie in ies:
|
||||
if ie.suitable(url) and ie.ie_key() not in self.ie_blacklist:
|
||||
self.regex_cache.insert(0, ie._VALID_URL_RE)
|
||||
self.regex_cache.insert(
|
||||
0, (ie._VALID_URL_RE if isinstance(ie._VALID_URL_RE, Iterable)
|
||||
else (ie._VALID_URL_RE,)))
|
||||
return True
|
||||
return False
|
||||
|
||||
|
@ -583,7 +590,7 @@ class gPodderExtension:
|
|||
|
||||
box.pack_start(Gtk.HSeparator(), False, False, 0)
|
||||
|
||||
checkbox = Gtk.CheckButton(_('Embed all available subtitles to downloaded video'))
|
||||
checkbox = Gtk.CheckButton(_('Embed all available subtitles in downloaded video'))
|
||||
checkbox.set_active(self.container.config.embed_subtitles)
|
||||
checkbox.connect('toggled', self.toggle_embed_subtitles)
|
||||
box.pack_start(checkbox, False, False, 0)
|
||||
|
|
|
@ -241,6 +241,14 @@
|
|||
<attribute name="label" translatable="yes">Episode descriptions</attribute>
|
||||
<attribute name="accel"><Primary>d</attribute>
|
||||
</item>
|
||||
<item>
|
||||
<attribute name="action">win.viewShowEpisodeReleasedTime</attribute>
|
||||
<attribute name="label" translatable="yes">Show episode released time</attribute>
|
||||
</item>
|
||||
<item>
|
||||
<attribute name="action">win.viewRightAlignEpisodeReleasedColumn</attribute>
|
||||
<attribute name="label" translatable="yes">Right align episode released column</attribute>
|
||||
</item>
|
||||
<item>
|
||||
<attribute name="action">win.viewCtrlClickToSortEpisodes</attribute>
|
||||
<attribute name="label" translatable="yes">Require control click to sort episodes</attribute>
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
.TH GPO "1" "February 2023" "gpodder 3.11.1" "User Commands"
|
||||
.TH GPO "1" "August 2023" "gpodder 3.11.2" "User Commands"
|
||||
.SH NAME
|
||||
gpo \- Text mode interface of gPodder
|
||||
.SH SYNOPSIS
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
.\" DO NOT MODIFY THIS FILE! It was generated by help2man 1.48.5.
|
||||
.TH GPODDER "1" "February 2023" "gpodder 3.11.1" "User Commands"
|
||||
.\" DO NOT MODIFY THIS FILE! It was generated by help2man 1.49.3.
|
||||
.TH GPODDER "1" "August 2023" "gpodder 3.11.2" "User Commands"
|
||||
.SH NAME
|
||||
gpodder \- Media aggregator and podcast client
|
||||
.SH SYNOPSIS
|
||||
|
|
|
@ -44,13 +44,34 @@
|
|||
<content_attribute id="money-purchasing">none</content_attribute>
|
||||
<content_attribute id="money-gambling">none</content_attribute>
|
||||
</content_rating>
|
||||
<screenshots>
|
||||
<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.2" date="2023-08-13">
|
||||
<description>
|
||||
<p>Major changes:</p>
|
||||
<ul>
|
||||
<li>show episode release time</li>
|
||||
<li>fix crash when syncing</li>
|
||||
<li>don't reposition maximised windows</li>
|
||||
</ul>
|
||||
</description>
|
||||
</release>
|
||||
<release version="3.11.1" date="2023-02-18">
|
||||
<description>
|
||||
<p>Major changes:</p>
|
||||
<ul>
|
||||
<li>new yt-dlp to fix the recent YouTube change</li>
|
||||
<li>fix multiple bugs that caused gPodder to freeze or appear frozen</li>
|
||||
<li>embed subtitles in videos with youtube-dl extension</li>
|
||||
<li>performance improvements</li>
|
||||
</ul>
|
||||
</description>
|
||||
</release>
|
||||
<release version="3.11.0" date="2022-07-30">
|
||||
<description>
|
||||
<p>This release contains a year's worth of improvements. Major changes:</p>
|
||||
|
@ -74,7 +95,7 @@
|
|||
<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.6" date="2018-12-29"/>
|
||||
<release version="3.10.5" date="2018-09-15">
|
||||
<description>
|
||||
<p>This is a bugfix release, shortly after 3.10.4, for the Rename after Download extension.</p>
|
||||
|
@ -85,5 +106,12 @@
|
|||
<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"/>
|
||||
<release version="3.9.6" date="2017-12-29"/>
|
||||
<release version="3.9.5" date="2017-12-16"/>
|
||||
<release version="3.9.4" date="2017-12-16"/>
|
||||
<release version="3.9.3" date="2016-12-22"/>
|
||||
<release version="3.9.2" date="2016-11-30"/>
|
||||
<release version="3.9.1" date="2016-08-31"/>
|
||||
<release version="3.9.0" date="2016-02-03"/>
|
||||
</releases>
|
||||
</component>
|
||||
|
|
|
@ -20,8 +20,8 @@
|
|||
# This metadata block gets parsed by setup.py - use single quotes only
|
||||
__tagline__ = 'Media aggregator and podcast client'
|
||||
__author__ = 'Thomas Perl <thp@gpodder.org>'
|
||||
__version__ = '3.11.1+1'
|
||||
__date__ = '2023-02-28'
|
||||
__version__ = '3.11.2'
|
||||
__date__ = '2023-08-13'
|
||||
__copyright__ = '© 2005-2023 The gPodder Team'
|
||||
__license__ = 'GNU General Public License, version 3 or later'
|
||||
__url__ = 'http://gpodder.org/'
|
||||
|
|
|
@ -171,6 +171,8 @@ defaults = {
|
|||
'always_show_new': True,
|
||||
'trim_title_prefix': True,
|
||||
'descriptions': True,
|
||||
'show_released_time': False,
|
||||
'right_align_released_column': False,
|
||||
'ctrl_click_to_sort': False,
|
||||
'columns': int('110', 2), # bitfield of visible columns
|
||||
},
|
||||
|
|
|
@ -86,8 +86,12 @@ def directory_entry_from_opml(url):
|
|||
|
||||
|
||||
def directory_entry_from_mygpo_json(url):
|
||||
r = util.urlopen(url)
|
||||
if not r.ok:
|
||||
raise Exception('%s: %d %s' % (url, r.status_code, r.reason))
|
||||
|
||||
return [DirectoryEntry(d['title'], d['url'], d['logo_url'], d['subscribers'], d['description'])
|
||||
for d in util.urlopen(url).json()]
|
||||
for d in r.json()]
|
||||
|
||||
|
||||
class GPodderNetSearchProvider(Provider):
|
||||
|
@ -150,7 +154,13 @@ class GPodderNetTagsProvider(Provider):
|
|||
return directory_entry_from_mygpo_json('http://gpodder.net/api/2/tag/%s/50.json' % urllib.parse.quote(tag))
|
||||
|
||||
def get_tags(self):
|
||||
return [DirectoryTag(d['tag'], d['usage']) for d in util.urlopen('http://gpodder.net/api/2/tags/40.json').json()]
|
||||
url = 'http://gpodder.net/api/2/tags/40.json'
|
||||
|
||||
r = util.urlopen(url)
|
||||
if not r.ok:
|
||||
raise Exception('%s: %d %s' % (url, r.status_code, r.reason))
|
||||
|
||||
return [DirectoryTag(d['tag'], d['usage']) for d in r.json()]
|
||||
|
||||
|
||||
class SoundcloudSearchProvider(Provider):
|
||||
|
|
|
@ -170,7 +170,7 @@ class ContentRange(object):
|
|||
return None
|
||||
value = value.strip()
|
||||
if not value.startswith('bytes '):
|
||||
# Unparseable
|
||||
# Unparsable
|
||||
return None
|
||||
value = value[len('bytes '):].strip()
|
||||
if '/' not in value:
|
||||
|
@ -539,7 +539,7 @@ class DownloadTask(object):
|
|||
of downloading data, this can take a while when the Internet is
|
||||
busy).
|
||||
|
||||
The "status_changed" attribute gets set to True everytime the
|
||||
The "status_changed" attribute gets set to True every time the
|
||||
"status" attribute changes its value. After you get the value of
|
||||
the "status_changed" attribute, it is always reset to False:
|
||||
|
||||
|
@ -588,7 +588,7 @@ class DownloadTask(object):
|
|||
_('Finished'), _('Failed'), _('Cancelling'), _('Cancelled'), _('Pausing'), _('Paused'))
|
||||
(NEW, QUEUED, DOWNLOADING, DONE, FAILED, CANCELLING, CANCELLED, PAUSING, PAUSED) = list(range(9))
|
||||
|
||||
# Wheter this task represents a file download or a device sync operation
|
||||
# Whether this task represents a file download or a device sync operation
|
||||
ACTIVITY_DOWNLOAD, ACTIVITY_SYNCHRONIZE = list(range(2))
|
||||
|
||||
# Minimum time between progress updates (in seconds)
|
||||
|
|
|
@ -74,7 +74,7 @@ class GtkBuilderWidget(object):
|
|||
Convert widget names to attributes of this object.
|
||||
|
||||
It means a widget named vbox-dialog in GtkBuilder
|
||||
is refered using self.vbox_dialog in the code.
|
||||
is referred using self.vbox_dialog in the code.
|
||||
"""
|
||||
for widget in self.builder.get_objects():
|
||||
# Just to be safe - every widget from the builder is buildable
|
||||
|
@ -100,7 +100,7 @@ class GtkBuilderWidget(object):
|
|||
def new(self):
|
||||
"""
|
||||
Method called when the user interface is loaded and ready to be used.
|
||||
At this moment, the widgets are loaded and can be refered as self.widget_name
|
||||
At this moment, the widgets are loaded and can be referred as self.widget_name
|
||||
"""
|
||||
|
||||
def main(self):
|
||||
|
@ -132,7 +132,7 @@ class GtkBuilderWidget(object):
|
|||
"""
|
||||
Starts the main loop of processing events checking for Control-C.
|
||||
|
||||
The default implementation checks wheter a Control-C is pressed,
|
||||
The default implementation checks whether a Control-C is pressed,
|
||||
then calls on_keyboard_interrupt().
|
||||
|
||||
Use this method for starting programs.
|
||||
|
|
|
@ -192,8 +192,10 @@ class UIConfig(config.Config):
|
|||
# positions (instead using a user-defined placement algorithm) and honor
|
||||
# requests after the window has already been shown.
|
||||
# Move it a second time after the window has been shown.
|
||||
# The first move reduces chance of window jumping.
|
||||
util.idle_add(window.move, cfg.x, cfg.y)
|
||||
# The first move reduces chance of window jumping,
|
||||
# and gives the window manager a position to unmaximize to.
|
||||
if not cfg.maximized:
|
||||
util.idle_add(window.move, cfg.x, cfg.y)
|
||||
|
||||
# Ignore events while we're connecting to the window
|
||||
self.__ignore_window_events = True
|
||||
|
@ -201,14 +203,18 @@ class UIConfig(config.Config):
|
|||
# Get window state, correct size comes from window.get_size(),
|
||||
# see https://developer.gnome.org/SaveWindowState/
|
||||
def _receive_configure_event(widget, event):
|
||||
x_pos, y_pos = widget.get_position()
|
||||
width_size, height_size = widget.get_size()
|
||||
maximized = bool(event.window.get_state() & Gdk.WindowState.MAXIMIZED)
|
||||
if not self.__ignore_window_events and not maximized:
|
||||
cfg.x = x_pos
|
||||
cfg.y = y_pos
|
||||
cfg.width = width_size
|
||||
cfg.height = height_size
|
||||
if not self.__ignore_window_events:
|
||||
# TODO: The maximize event might arrive after the configure event.
|
||||
# This causes the maximized size to be saved, and restoring the
|
||||
# window will not save its smaller size. Delaying the save with
|
||||
# idle_add() is not enough time for the state event to arrive.
|
||||
if not bool(event.window.get_state() & Gdk.WindowState.MAXIMIZED):
|
||||
x_pos, y_pos = widget.get_position()
|
||||
width_size, height_size = widget.get_size()
|
||||
cfg.x = x_pos
|
||||
cfg.y = y_pos
|
||||
cfg.width = width_size
|
||||
cfg.height = height_size
|
||||
|
||||
window.connect('configure-event', _receive_configure_event)
|
||||
|
||||
|
|
|
@ -291,12 +291,15 @@ class gPodderPodcastDirectory(BuilderWidget):
|
|||
|
||||
@self.obtain_podcasts_with
|
||||
def load_data():
|
||||
if self.current_provider.kind == directory.Provider.PROVIDER_SEARCH:
|
||||
return self.current_provider.on_search(query)
|
||||
elif self.current_provider.kind == directory.Provider.PROVIDER_URL:
|
||||
return self.current_provider.on_url(query)
|
||||
elif self.current_provider.kind == directory.Provider.PROVIDER_FILE:
|
||||
return self.current_provider.on_file(query)
|
||||
try:
|
||||
if self.current_provider.kind == directory.Provider.PROVIDER_SEARCH:
|
||||
return self.current_provider.on_search(query)
|
||||
elif self.current_provider.kind == directory.Provider.PROVIDER_URL:
|
||||
return self.current_provider.on_url(query)
|
||||
elif self.current_provider.kind == directory.Provider.PROVIDER_FILE:
|
||||
return self.current_provider.on_file(query)
|
||||
except Exception as e:
|
||||
logger.warning('Got exception while loading podcasts: %s', e)
|
||||
|
||||
def on_can_subscribe_changed(self, can_subscribe):
|
||||
self.btnOK.set_sensitive(can_subscribe)
|
||||
|
|
|
@ -77,7 +77,7 @@ def draw_rounded_rectangle(ctx, x, y, w, h, r=10, left_side_width=None,
|
|||
|
||||
|
||||
def rounded_rectangle(ctx, x, y, width, height, radius=4.):
|
||||
"""Simple rounded rectangle algorithmn
|
||||
"""Simple rounded rectangle algorithm
|
||||
|
||||
http://www.cairographics.org/samples/rounded_rectangle/
|
||||
"""
|
||||
|
@ -126,7 +126,7 @@ def draw_text_box_centered(ctx, widget, w_width, w_height, text, font_desc=None,
|
|||
|
||||
def draw_cake(percentage, text=None, emblem=None, size=None):
|
||||
# Download percentage bar icon - it turns out the cake is a lie (d'oh!)
|
||||
# ..but the inital idea was to have a cake-style indicator, but that
|
||||
# ..but the initial idea was to have a cake-style indicator, but that
|
||||
# didn't work as well as the progress bar, but the name stuck..
|
||||
|
||||
if size is None:
|
||||
|
|
|
@ -154,7 +154,7 @@ class ProgressIndicator(object):
|
|||
self.tick_counter += 1
|
||||
|
||||
if time.time() >= self.next_update or (final and self.dialog):
|
||||
if type(final) == str:
|
||||
if isinstance(final, str):
|
||||
self.on_message(final)
|
||||
self.on_progress(1.0)
|
||||
elif self.max_ticks is not None:
|
||||
|
|
|
@ -242,6 +242,8 @@ class gPodder(BuilderWidget, dbus.service.Object):
|
|||
|
||||
self.create_actions()
|
||||
|
||||
self.releasecell = None
|
||||
|
||||
# Init the treeviews that we use
|
||||
self.init_podcast_list_treeview()
|
||||
self.init_episode_list_treeview()
|
||||
|
@ -431,6 +433,17 @@ class gPodder(BuilderWidget, dbus.service.Object):
|
|||
action.connect('activate', self.on_item_view_show_episode_description_toggled)
|
||||
g.add_action(action)
|
||||
|
||||
action = Gio.SimpleAction.new_stateful(
|
||||
'viewShowEpisodeReleasedTime', None, GLib.Variant.new_boolean(self.config.ui.gtk.episode_list.show_released_time))
|
||||
action.connect('activate', self.on_item_view_show_episode_released_time_toggled)
|
||||
g.add_action(action)
|
||||
|
||||
action = Gio.SimpleAction.new_stateful(
|
||||
'viewRightAlignEpisodeReleasedColumn', None,
|
||||
GLib.Variant.new_boolean(self.config.ui.gtk.episode_list.right_align_released_column))
|
||||
action.connect('activate', self.on_item_view_right_align_episode_released_column_toggled)
|
||||
g.add_action(action)
|
||||
|
||||
action = Gio.SimpleAction.new_stateful(
|
||||
'viewCtrlClickToSortEpisodes', None, GLib.Variant.new_boolean(self.config.ui.gtk.episode_list.ctrl_click_to_sort))
|
||||
action.connect('activate', self.on_item_view_ctrl_click_to_sort_episodes_toggled)
|
||||
|
@ -541,7 +554,7 @@ class gPodder(BuilderWidget, dbus.service.Object):
|
|||
# NOTE: Not used with popover menus in adaptive version
|
||||
"""
|
||||
Update Extras/Extensions menu.
|
||||
Called at startup and when en/dis-abling extenstions.
|
||||
Called at startup and when en/dis-abling extensions.
|
||||
"""
|
||||
def gen_callback(label, callback):
|
||||
return lambda action, param: callback()
|
||||
|
@ -1141,6 +1154,14 @@ class gPodder(BuilderWidget, dbus.service.Object):
|
|||
|
||||
return False
|
||||
|
||||
def align_releasecell(self):
|
||||
if self.config.ui.gtk.episode_list.right_align_released_column:
|
||||
self.releasecell.set_property('xalign', 1)
|
||||
self.releasecell.set_property('alignment', Pango.Alignment.RIGHT)
|
||||
else:
|
||||
self.releasecell.set_property('xalign', 0)
|
||||
self.releasecell.set_property('alignment', Pango.Alignment.LEFT)
|
||||
|
||||
def init_episode_list_treeview(self):
|
||||
self.episode_list_model.set_view_mode(self.config.ui.gtk.episode_list.view_mode)
|
||||
|
||||
|
@ -1224,23 +1245,43 @@ class gPodder(BuilderWidget, dbus.service.Object):
|
|||
# timecolumn = Gtk.TreeViewColumn(_('Duration'), timecell, text=EpisodeListModel.C_TIME)
|
||||
# timecolumn.set_sort_column_id(EpisodeListModel.C_TOTAL_TIME)
|
||||
|
||||
releasecell = Gtk.CellRendererText()
|
||||
# releasecolumn = Gtk.TreeViewColumn(_('Released'), releasecell, text=EpisodeListModel.C_PUBLISHED_TEXT)
|
||||
self.releasecell = Gtk.CellRendererText()
|
||||
self.align_releasecell()
|
||||
# releasecolumn = Gtk.TreeViewColumn(_('Released'))
|
||||
# releasecolumn.pack_start(self.releasecell, True)
|
||||
# releasecolumn.add_attribute(self.releasecell, 'markup', EpisodeListModel.C_PUBLISHED_TEXT)
|
||||
# releasecolumn.set_sort_column_id(EpisodeListModel.C_PUBLISHED)
|
||||
namecolumn.pack_end(releasecell, True)
|
||||
namecolumn.add_attribute(releasecell, 'text', EpisodeListModel.C_TIME_AND_PUBLISHED_TEXT)
|
||||
|
||||
namecolumn.set_sizing(Gtk.TreeViewColumnSizing.AUTOSIZE)
|
||||
namecolumn.set_resizable(True)
|
||||
namecolumn.set_expand(True)
|
||||
# sizetimecell = Gtk.CellRendererText()
|
||||
# sizetimecell.set_property('xalign', 1)
|
||||
# sizetimecell.set_property('alignment', Pango.Alignment.RIGHT)
|
||||
# sizetimecolumn = Gtk.TreeViewColumn(_('Size+'))
|
||||
# sizetimecolumn.pack_start(sizetimecell, True)
|
||||
# sizetimecolumn.add_attribute(sizetimecell, 'markup', EpisodeListModel.C_FILESIZE_AND_TIME_TEXT)
|
||||
# sizetimecolumn.set_sort_column_id(EpisodeListModel.C_FILESIZE_AND_TIME)
|
||||
|
||||
# timesizecell = Gtk.CellRendererText()
|
||||
# timesizecell.set_property('xalign', 1)
|
||||
# timesizecell.set_property('alignment', Pango.Alignment.RIGHT)
|
||||
# timesizecolumn = Gtk.TreeViewColumn(_('Duration+'))
|
||||
# timesizecolumn.pack_start(timesizecell, True)
|
||||
# timesizecolumn.add_attribute(timesizecell, 'markup', EpisodeListModel.C_TIME_AND_SIZE)
|
||||
# timesizecolumn.set_sort_column_id(EpisodeListModel.C_TOTAL_TIME_AND_SIZE)
|
||||
|
||||
namecolumn.pack_end(self.releasecell, True)
|
||||
namecolumn.add_attribute(self.releasecell, 'text', EpisodeListModel.C_TIME_AND_PUBLISHED_TEXT)
|
||||
|
||||
namecolumn.set_reorderable(True)
|
||||
self.treeAvailable.append_column(namecolumn)
|
||||
|
||||
if not self.config.ui.gtk.state.main_window.episode_column_sort_id:
|
||||
self.config.ui.gtk.state.main_window.episode_column_sort_id = EpisodeListModel.C_PUBLISHED
|
||||
|
||||
# EpisodeListModel.C_PUBLISHED is not available in config.py, set it here on first run
|
||||
if not self.config.ui.gtk.state.main_window.episode_column_sort_id:
|
||||
self.config.ui.gtk.state.main_window.episode_column_sort_id = EpisodeListModel.C_PUBLISHED
|
||||
|
||||
# for itemcolumn in (sizecolumn, timecolumn, releasecolumn):
|
||||
# for itemcolumn in (sizecolumn, timecolumn, releasecolumn, sizetimecolumn, timesizecolumn):
|
||||
# itemcolumn.set_reorderable(True)
|
||||
# self.treeAvailable.append_column(itemcolumn)
|
||||
# TreeViewHelper.register_column(self.treeAvailable, itemcolumn)
|
||||
|
@ -1256,7 +1297,7 @@ class gPodder(BuilderWidget, dbus.service.Object):
|
|||
# w = w.get_parent()
|
||||
#
|
||||
# w.connect('button-release-event', self.on_episode_list_header_clicked)
|
||||
#
|
||||
|
||||
# # Restore column sorting
|
||||
# if column.get_sort_column_id() == self.config.ui.gtk.state.main_window.episode_column_sort_id:
|
||||
# self.episode_list_model._sorter.set_sort_column_id(Gtk.TREE_SORTABLE_UNSORTED_SORT_COLUMN_ID,
|
||||
|
@ -1751,7 +1792,8 @@ class gPodder(BuilderWidget, dbus.service.Object):
|
|||
if name == 'ui.gtk.toolbar':
|
||||
# self.toolbar.set_property('visible', new_value)
|
||||
pass
|
||||
elif name in ('ui.gtk.episode_list.descriptions',
|
||||
elif name in ('ui.gtk.episode_list.show_released_time',
|
||||
'ui.gtk.episode_list.descriptions',
|
||||
'ui.gtk.episode_list.trim_title_prefix',
|
||||
'ui.gtk.episode_list.always_show_new'):
|
||||
self.update_episode_list_model()
|
||||
|
@ -2647,7 +2689,7 @@ class gPodder(BuilderWidget, dbus.service.Object):
|
|||
if episode is None:
|
||||
logger.info('Invalid episode at path %s', str(path))
|
||||
continue
|
||||
except TypeError as te:
|
||||
except TypeError as e:
|
||||
logger.error('Invalid episode at path %s', str(path))
|
||||
continue
|
||||
|
||||
|
@ -2680,7 +2722,7 @@ class gPodder(BuilderWidget, dbus.service.Object):
|
|||
if task is None:
|
||||
logger.info('Invalid task at path %s', str(path))
|
||||
continue
|
||||
except TypeError as te:
|
||||
except TypeError as e:
|
||||
logger.error('Invalid task at path %s', str(path))
|
||||
continue
|
||||
|
||||
|
@ -3160,7 +3202,7 @@ class gPodder(BuilderWidget, dbus.service.Object):
|
|||
else:
|
||||
channel._update_error = '?'
|
||||
nr_update_errors += 1
|
||||
logger.error('Error: %s', message, exc_info=(e.__class__ not in [
|
||||
logger.error('Error updating feed: %s: %s', channel.title, message, exc_info=(e.__class__ not in [
|
||||
gpodder.feedcore.BadRequest,
|
||||
gpodder.feedcore.AuthenticationRequired,
|
||||
gpodder.feedcore.Unsubscribe,
|
||||
|
@ -3821,6 +3863,18 @@ class gPodder(BuilderWidget, dbus.service.Object):
|
|||
self.config.ui.gtk.episode_list.descriptions = not state
|
||||
action.set_state(GLib.Variant.new_boolean(not state))
|
||||
|
||||
def on_item_view_show_episode_released_time_toggled(self, action, param):
|
||||
state = action.get_state()
|
||||
self.config.ui.gtk.episode_list.show_released_time = not state
|
||||
action.set_state(GLib.Variant.new_boolean(not state))
|
||||
|
||||
def on_item_view_right_align_episode_released_column_toggled(self, action, param):
|
||||
state = action.get_state()
|
||||
self.config.ui.gtk.episode_list.right_align_released_column = not state
|
||||
action.set_state(GLib.Variant.new_boolean(not state))
|
||||
self.align_releasecell()
|
||||
self.treeAvailable.queue_draw()
|
||||
|
||||
def on_item_view_ctrl_click_to_sort_episodes_toggled(self, action, param):
|
||||
state = action.get_state()
|
||||
self.config.ui.gtk.episode_list.ctrl_click_to_sort = not state
|
||||
|
|
|
@ -128,7 +128,7 @@ class BackgroundUpdate(object):
|
|||
model.C_URL, episode.url,
|
||||
model.C_TITLE, episode.title,
|
||||
model.C_EPISODE, episode,
|
||||
model.C_PUBLISHED_TEXT, episode.cute_pubdate(),
|
||||
model.C_PUBLISHED_TEXT, episode.cute_pubdate(show_time=self.model._config_ui_gtk_episode_list_show_released_time),
|
||||
model.C_PUBLISHED, episode.published,
|
||||
)
|
||||
update_fields = model.get_update_fields(episode)
|
||||
|
@ -217,11 +217,13 @@ class EpisodeListModel(Gtk.ListStore):
|
|||
self._config_ui_gtk_episode_list_always_show_new = False
|
||||
self._config_ui_gtk_episode_list_trim_title_prefix = False
|
||||
self._config_ui_gtk_episode_list_descriptions = False
|
||||
self._config_ui_gtk_episode_list_show_released_time = False
|
||||
|
||||
def cache_config(self, config):
|
||||
self._config_ui_gtk_episode_list_always_show_new = config.ui.gtk.episode_list.always_show_new
|
||||
self._config_ui_gtk_episode_list_trim_title_prefix = config.ui.gtk.episode_list.trim_title_prefix
|
||||
self._config_ui_gtk_episode_list_descriptions = config.ui.gtk.episode_list.descriptions
|
||||
self._config_ui_gtk_episode_list_show_released_time = config.ui.gtk.episode_list.show_released_time
|
||||
|
||||
def _format_filesize(self, episode):
|
||||
if episode.file_size > 0:
|
||||
|
|
|
@ -16,6 +16,7 @@
|
|||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
import datetime
|
||||
import html
|
||||
import logging
|
||||
from urllib.parse import urlparse
|
||||
|
@ -226,7 +227,8 @@ class gPodderShownotesText(gPodderShownotes):
|
|||
heading = episode.title
|
||||
subheading = _('from %s') % (episode.channel.title)
|
||||
details = self.details_fmt % {
|
||||
'date': util.format_date(episode.published),
|
||||
'date': '{} {}'.format(datetime.datetime.fromtimestamp(episode.published).strftime('%H:%M'),
|
||||
util.format_date(episode.published)),
|
||||
'size': util.format_filesize(episode.file_size, digits=1)
|
||||
if episode.file_size > 0 else "-",
|
||||
'duration': episode.get_play_info_string()}
|
||||
|
@ -371,7 +373,8 @@ class gPodderShownotesHTML(gPodderShownotes):
|
|||
heading = '<h3>%s</h3>' % html.escape(episode.title)
|
||||
subheading = _('from %s') % html.escape(episode.channel.title)
|
||||
details = '<small>%s</small>' % html.escape(self.details_fmt % {
|
||||
'date': util.format_date(episode.published),
|
||||
'date': '{} {}'.format(datetime.datetime.fromtimestamp(episode.published).strftime('%H:%M'),
|
||||
util.format_date(episode.published)),
|
||||
'size': util.format_filesize(episode.file_size, digits=1)
|
||||
if episode.file_size > 0 else "-",
|
||||
'duration': episode.get_play_info_string()})
|
||||
|
|
|
@ -161,7 +161,7 @@ class JsonConfig(object):
|
|||
work_queue.append((data[key], value))
|
||||
elif type(value) != type(data[key]): # noqa
|
||||
# Type mismatch of current value and default
|
||||
if type(value) == int and type(data[key]) == float:
|
||||
if isinstance(value, int) and isinstance(data[key], float):
|
||||
# Convert float to int if default value is int
|
||||
data[key] = int(data[key])
|
||||
|
||||
|
|
|
@ -842,11 +842,18 @@ class PodcastEpisode(PodcastModelObject):
|
|||
self.title,
|
||||
self.cute_pubdate())
|
||||
|
||||
def cute_pubdate(self):
|
||||
def cute_pubdate(self, show_time=False):
|
||||
result = util.format_date(self.published)
|
||||
if result is None:
|
||||
return '(%s)' % _('unknown')
|
||||
else:
|
||||
|
||||
try:
|
||||
if show_time:
|
||||
timestamp = datetime.datetime.fromtimestamp(self.published)
|
||||
return '<small>{}</small>\n{}'.format(timestamp.strftime('%H:%M'), result)
|
||||
else:
|
||||
return result
|
||||
except:
|
||||
return result
|
||||
|
||||
pubdate_prop = property(fget=cute_pubdate)
|
||||
|
@ -1276,7 +1283,7 @@ class PodcastChannel(PodcastModelObject):
|
|||
|
||||
# This *might* cause episodes to be skipped if there were more than
|
||||
# limit.episodes items added to the feed between updates.
|
||||
# The benefit is that it prevents old episodes from apearing as new
|
||||
# The benefit is that it prevents old episodes from appearing as new
|
||||
# in certain situations (see bug #340).
|
||||
self.db.purge(max_episodes, self.id) # TODO: Remove from self.children!
|
||||
|
||||
|
|
|
@ -101,6 +101,11 @@ class Matcher(object):
|
|||
return episode.file_type() == k
|
||||
elif k == 'torrent':
|
||||
return episode.url.endswith('.torrent') or 'torrent' in episode.mime_type
|
||||
elif k == 'paused':
|
||||
return (episode.download_task is not None
|
||||
and episode.download_task.status in (episode.download_task.PAUSED, episode.download_task.PAUSING))
|
||||
elif k == 'failed':
|
||||
return (episode.download_task is not None and episode.download_task.status == episode.download_task.FAILED)
|
||||
|
||||
# Nouns (for comparisons)
|
||||
if k in ('megabytes', 'mb'):
|
||||
|
|
|
@ -66,7 +66,7 @@ class Resolver(object):
|
|||
|
||||
def unregister_instance(self, klass):
|
||||
logger.debug('Unregistering {} resolver instance: {}'.format(self._name, klass))
|
||||
self._resolvers = [r for r in self._resolvers if type(r) != klass]
|
||||
self._resolvers = [r for r in self._resolvers if not isinstance(r, klass)]
|
||||
|
||||
def _info(self, resolver):
|
||||
return '%s from %s' % (resolver.__name__ if hasattr(resolver, '__name__')
|
||||
|
|
|
@ -113,7 +113,7 @@ def episode_filename_on_device(config, episode):
|
|||
"""
|
||||
# get the local file
|
||||
from_file = episode.local_filename(create=False)
|
||||
# get the formated base name
|
||||
# get the formatted base name
|
||||
filename_base = util.sanitize_filename(episode.sync_filename(
|
||||
config.device_sync.custom_sync_name_enabled,
|
||||
config.device_sync.custom_sync_name),
|
||||
|
@ -536,9 +536,9 @@ class MP3PlayerDevice(Device):
|
|||
# Assume same size and don't sync again
|
||||
pass
|
||||
if not to_file_exists or from_size != to_size:
|
||||
logger.info('Copying %s => %s',
|
||||
os.path.basename(from_file),
|
||||
to_file.get_uri())
|
||||
logger.info('Copying %s (%d bytes) => %s (%d bytes)',
|
||||
os.path.basename(from_file), from_size,
|
||||
to_file.get_uri(), to_size)
|
||||
from_file = Gio.File.new_for_path(from_file)
|
||||
try:
|
||||
def hookconvert(current_bytes, total_bytes, user_data):
|
||||
|
@ -745,6 +745,7 @@ class SyncTask(download.DownloadTask):
|
|||
self.speed = 0.0
|
||||
self.progress = 0.0
|
||||
self.error_message = None
|
||||
self.custom_downloader = None
|
||||
|
||||
# Have we already shown this task in a notification?
|
||||
self._notification_shown = False
|
||||
|
@ -828,7 +829,7 @@ class SyncTask(download.DownloadTask):
|
|||
if self.status != SyncTask.DOWNLOADING:
|
||||
return False
|
||||
|
||||
# We are synching this file right now
|
||||
# We are syncing this file right now
|
||||
self._notification_shown = False
|
||||
|
||||
sync_result = SyncTask.DOWNLOADING
|
||||
|
|
|
@ -294,7 +294,7 @@ def normalize_feed_url(url):
|
|||
if scheme not in ('http', 'https', 'ftp', 'file'):
|
||||
return None
|
||||
|
||||
# urlunsplit might return "a slighty different, but equivalent URL"
|
||||
# urlunsplit might return "a slightly different, but equivalent URL"
|
||||
return urllib.parse.urlunsplit((scheme, netloc, path, query, fragment))
|
||||
|
||||
|
||||
|
@ -543,10 +543,10 @@ def format_date(timestamp):
|
|||
yesterday = time.localtime(time.time() - seconds_in_a_day)[:3]
|
||||
try:
|
||||
timestamp_date = time.localtime(timestamp)[:3]
|
||||
except ValueError as ve:
|
||||
except ValueError as e:
|
||||
logger.warning('Cannot convert timestamp', exc_info=True)
|
||||
return None
|
||||
except TypeError as te:
|
||||
except TypeError as e:
|
||||
logger.warning('Cannot convert timestamp', exc_info=True)
|
||||
return None
|
||||
|
||||
|
@ -679,7 +679,7 @@ def remove_html_tags(html):
|
|||
return result.strip()
|
||||
|
||||
|
||||
class HyperlinkExtracter(object):
|
||||
class HyperlinkExtractor(object):
|
||||
def __init__(self):
|
||||
self.parts = []
|
||||
self.target_stack = [None]
|
||||
|
@ -773,9 +773,9 @@ class HyperlinkExtracter(object):
|
|||
|
||||
class ExtractHyperlinkedText(object):
|
||||
def __call__(self, document):
|
||||
self.extracter = HyperlinkExtracter()
|
||||
self.extractor = HyperlinkExtractor()
|
||||
self.visit(document)
|
||||
return self.extracter.get_result()
|
||||
return self.extractor.get_result()
|
||||
|
||||
def visit(self, element):
|
||||
# skip functions generated by html5lib for comments in the HTML
|
||||
|
@ -784,42 +784,42 @@ class ExtractHyperlinkedText(object):
|
|||
|
||||
NS = '{http://www.w3.org/1999/xhtml}'
|
||||
tag_name = (element.tag[len(NS):] if element.tag.startswith(NS) else element.tag).lower()
|
||||
self.extracter.handle_starttag(tag_name, list(element.items()))
|
||||
self.extractor.handle_starttag(tag_name, list(element.items()))
|
||||
|
||||
if element.text is not None:
|
||||
self.extracter.handle_data(element.text)
|
||||
self.extractor.handle_data(element.text)
|
||||
|
||||
for child in element:
|
||||
self.visit(child)
|
||||
|
||||
if child.tail is not None:
|
||||
self.extracter.handle_data(child.tail)
|
||||
self.extractor.handle_data(child.tail)
|
||||
|
||||
self.extracter.handle_endtag(tag_name)
|
||||
self.extractor.handle_endtag(tag_name)
|
||||
|
||||
|
||||
class ExtractHyperlinkedTextHTMLParser(HTMLParser):
|
||||
def __call__(self, html):
|
||||
self.extracter = HyperlinkExtracter()
|
||||
self.extractor = HyperlinkExtractor()
|
||||
self.target_stack = [None]
|
||||
self.feed(html)
|
||||
self.close()
|
||||
return self.extracter.get_result()
|
||||
return self.extractor.get_result()
|
||||
|
||||
def handle_starttag(self, tag, attrs):
|
||||
self.extracter.handle_starttag(tag, attrs)
|
||||
self.extractor.handle_starttag(tag, attrs)
|
||||
|
||||
def handle_endtag(self, tag):
|
||||
self.extracter.handle_endtag(tag)
|
||||
self.extractor.handle_endtag(tag)
|
||||
|
||||
def handle_data(self, data):
|
||||
self.extracter.handle_data(data)
|
||||
self.extractor.handle_data(data)
|
||||
|
||||
def handle_entityref(self, name):
|
||||
self.extracter.handle_entityref(name)
|
||||
self.extractor.handle_entityref(name)
|
||||
|
||||
def handle_charref(self, name):
|
||||
self.extracter.handle_charref(name)
|
||||
self.extractor.handle_charref(name)
|
||||
|
||||
|
||||
def extract_hyperlinked_text(html):
|
||||
|
@ -842,7 +842,7 @@ def extract_hyperlinked_text(html):
|
|||
|
||||
def nice_html_description(img, description):
|
||||
"""
|
||||
basic html formating + hyperlink highlighting + video thumbnail
|
||||
basic html formatting + hyperlink highlighting + video thumbnail
|
||||
"""
|
||||
description = re.sub(r'''https?://[^\s]+''',
|
||||
r'''<a href="\g<0>">\g<0></a>''',
|
||||
|
@ -1002,7 +1002,7 @@ def filename_from_url(url):
|
|||
http://server/get.jsp?file=/episode0815.MOV => ("episode0815", ".mov")
|
||||
http://s/redirect.mp4?http://serv2/test.mp4 => ("test", ".mp4")
|
||||
"""
|
||||
(scheme, netloc, path, para, query, fragid) = urllib.parse.urlparse(url)
|
||||
(scheme, netloc, path, params, query, fragment) = urllib.parse.urlparse(url)
|
||||
(filename, extension) = os.path.splitext(
|
||||
os.path.basename(urllib.parse.unquote(path)))
|
||||
|
||||
|
@ -1278,7 +1278,7 @@ def get_real_url(url):
|
|||
def find_command(command):
|
||||
"""
|
||||
Searches the system's PATH for a specific command that is
|
||||
executable by the user. Returns the first occurence of an
|
||||
executable by the user. Returns the first occurrence of an
|
||||
executable binary in the PATH, or None if the command is
|
||||
not available.
|
||||
|
||||
|
@ -1536,7 +1536,7 @@ def format_seconds_to_hour_min_sec(seconds):
|
|||
|
||||
|
||||
def http_request(url, method='HEAD'):
|
||||
(scheme, netloc, path, parms, qry, fragid) = urllib.parse.urlparse(url)
|
||||
(scheme, netloc, path, params, query, fragment) = urllib.parse.urlparse(url)
|
||||
if scheme == 'https':
|
||||
conn = http.client.HTTPSConnection(netloc)
|
||||
else:
|
||||
|
@ -2229,7 +2229,7 @@ class Popen(subprocess.Popen):
|
|||
|
||||
|
||||
def _parse_mimetype_sorted_dictitems(mimetype):
|
||||
""" python 3.5 unorderd dict compat for doctest. don't use! """
|
||||
""" python 3.5 unordered dict compat for doctest. don't use! """
|
||||
r = parse_mimetype(mimetype)
|
||||
return r[0], r[1], sorted(r[2].items())
|
||||
|
||||
|
|
|
@ -347,13 +347,11 @@ def install(domain, localedir):
|
|||
|
||||
# initialize Python's gettext interface
|
||||
gettext.bindtextdomain(domain, localedir)
|
||||
gettext.bind_textdomain_codeset(domain, 'UTF-8')
|
||||
|
||||
# on windows systems, initialize libintl
|
||||
if sys.platform == 'win32' or sys.platform == 'nt':
|
||||
from ctypes import cdll
|
||||
libintl = cdll.LoadLibrary('libintl-8.dll')
|
||||
libintl.bindtextdomain(domain.encode('mbcs'), localedir.encode('mbcs'))
|
||||
libintl.bind_textdomain_codeset(domain.encode('mbcs'), 'UTF-8'.encode('mbcs'))
|
||||
|
||||
del libintl
|
||||
|
|
|
@ -440,7 +440,7 @@ def get_channel_id_url(url, feed_data=None):
|
|||
else:
|
||||
r = feed_data
|
||||
# video page may contain corrupt HTML/XML, search for tag to avoid exception
|
||||
m = re.search(r'<meta itemprop="channelId" content="([^"]+)">', r.text)
|
||||
m = re.search(r'channel_id=([^"]+)">', r.text)
|
||||
if m:
|
||||
channel_id = m.group(1)
|
||||
else:
|
||||
|
|
|
@ -83,11 +83,11 @@ def checksums():
|
|||
m = hashlib.md5()
|
||||
s = hashlib.sha256()
|
||||
with open(archive, "rb") as f:
|
||||
bloc = f.read(4096)
|
||||
while bloc:
|
||||
m.update(bloc)
|
||||
s.update(bloc)
|
||||
bloc = f.read(4096)
|
||||
block = f.read(4096)
|
||||
while block:
|
||||
m.update(block)
|
||||
s.update(block)
|
||||
block = f.read(4096)
|
||||
ret[os.path.basename(archive)] = dict(md5=m.hexdigest(), sha256=s.hexdigest())
|
||||
return ret
|
||||
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<?xml version="1.0" ?>
|
||||
<!-- this stylesheet ajusts menu item accelerators:
|
||||
<!-- this stylesheet adjusts menu item accelerators:
|
||||
- Command-, for preferences
|
||||
- Command-? for user manual
|
||||
|
||||
|
|
|
@ -110,7 +110,7 @@ print('System Path:\n', '\n'.join(sys.path))
|
|||
|
||||
# see https://gpodder.github.io/docs/user-manual.html#gpodder-home-folder-and-download-location
|
||||
# To override gPodder home and/or download directory:
|
||||
# 1. uncomment (remove the pound sign and space) at the begining of the relevant line
|
||||
# 1. uncomment (remove the pound sign and space) at the beginning of the relevant line
|
||||
# 2. replace ~/gPodderData or ~/gPodderDownloads with the path you want for your gPodder home
|
||||
# (you can move the original folder in the Finder first,
|
||||
# then drag and drop to the launcher.py in TextEdit to ensure the correct path is set)
|
||||
|
@ -125,7 +125,7 @@ for k, v in os.environ.items():
|
|||
|
||||
def gpodder_home():
|
||||
# don't inadvertently create the new gPodder home,
|
||||
# it would be prefered to the old one
|
||||
# it would be preferred to the old one
|
||||
default_path = join(os.environ['HOME'], 'Library', 'Application Support', 'gPodder')
|
||||
cands = [
|
||||
os.environ.get('GPODDER_HOME'),
|
||||
|
|
|
@ -67,7 +67,7 @@ cp -a "$checkout"/tools/mac-osx/make_cert_pem.py "$resources"/bin
|
|||
|
||||
# install gPodder hard dependencies
|
||||
$run_pip install setuptools==64.0.3 wheel || exit 1
|
||||
$run_pip install mygpoclient==1.9 podcastparser==0.6.9 requests[socks]==2.28.1 || exit 1
|
||||
$run_pip install mygpoclient==1.9 podcastparser==0.6.10 requests[socks]==2.31.0 || exit 1
|
||||
# install brotli and pycryptodomex (build from source)
|
||||
$run_pip debug -v
|
||||
$run_pip install -v brotli || exit 1
|
||||
|
|
|
@ -5,9 +5,9 @@ dbus-python
|
|||
html5lib==1.1
|
||||
mutagen==1.46.0
|
||||
mygpoclient==1.9
|
||||
podcastparser==0.6.9
|
||||
requests[socks]==2.28.1
|
||||
urllib3==1.26.13
|
||||
podcastparser==0.6.10
|
||||
requests[socks]==2.31.0
|
||||
urllib3==2.0.4
|
||||
yt-dlp
|
||||
# eyed3 is optional and pulls in a lot of dependencies, so disable by default
|
||||
# eyed3
|
||||
|
|
|
@ -84,18 +84,18 @@ function extract_installer {
|
|||
}
|
||||
|
||||
PIP_REQUIREMENTS="\
|
||||
certifi==2022.12.7
|
||||
certifi==2023.7.22
|
||||
chardet==5.1.0
|
||||
comtypes==1.1.14
|
||||
comtypes==1.2.0
|
||||
git+https://github.com/jaraco/pywin32-ctypes.git@f27d6a0
|
||||
html5lib==1.1
|
||||
idna==3.4
|
||||
mutagen==1.46.0
|
||||
mygpoclient==1.9
|
||||
podcastparser==0.6.9
|
||||
podcastparser==0.6.10
|
||||
PySocks==1.7.1
|
||||
requests==2.28.1
|
||||
urllib3==1.26.13
|
||||
requests==2.31.0
|
||||
urllib3==2.0.4
|
||||
webencodings==0.5.1
|
||||
yt-dlp
|
||||
"
|
||||
|
|
|
@ -199,7 +199,7 @@ Function custom_gui_init
|
|||
; uninstall failed
|
||||
Abort
|
||||
rm_instdir:
|
||||
; either the uninstaller was successfull or
|
||||
; either the uninstaller was successful or
|
||||
; the uninstaller.exe wasn't found
|
||||
RMDir /r "$INSTDIR"
|
||||
do_continue:
|
||||
|
|
Loading…
Reference in New Issue