Merge branch 'master' into adaptive

This commit is contained in:
Teemu Ikonen 2021-06-08 19:30:34 +03:00
commit 63e24bd8c1
46 changed files with 9066 additions and 8081 deletions

View File

@ -219,4 +219,4 @@ check if the bug still appears to see if an extension causes the bug.
- Homepage: http://gpodder.org/
- Bug tracker: https://github.com/gpodder/gpodder/issues
- Mailing list: http://freelists.org/list/gpodder
- IRC channel: #gpodder on irc.freenode.net
- IRC channel: #gpodder on irc.libera.chat

504
po/ca.po

File diff suppressed because it is too large Load Diff

508
po/cs.po

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

504
po/da.po

File diff suppressed because it is too large Load Diff

506
po/de.po

File diff suppressed because it is too large Load Diff

504
po/el.po

File diff suppressed because it is too large Load Diff

504
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

504
po/eu.po

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

504
po/fi.po

File diff suppressed because it is too large Load Diff

506
po/fr.po

File diff suppressed because it is too large Load Diff

504
po/gl.po

File diff suppressed because it is too large Load Diff

504
po/he.po

File diff suppressed because it is too large Load Diff

506
po/hu.po

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

506
po/it.po

File diff suppressed because it is too large Load Diff

504
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

504
po/nb.po

File diff suppressed because it is too large Load Diff

510
po/nl.po

File diff suppressed because it is too large Load Diff

511
po/nn.po

File diff suppressed because it is too large Load Diff

506
po/pl.po

File diff suppressed because it is too large Load Diff

504
po/pt.po

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

504
po/ro.po

File diff suppressed because it is too large Load Diff

506
po/ru.po

File diff suppressed because it is too large Load Diff

504
po/sv.po

File diff suppressed because it is too large Load Diff

506
po/tr.po

File diff suppressed because it is too large Load Diff

504
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

@ -29,7 +29,7 @@ __only_for__ = 'gtk, cli'
__authors__ = 'Eric Le Lay <elelay.fr:contact>'
__doc__ = 'https://gpodder.github.io/docs/extensions/youtubedl.html'
want_ytdl_version = '2020.11.12'
want_ytdl_version = '2021.02.04'
want_ytdl_version_msg = _('Your version of youtube-dl %(have_version)s has known issues, please upgrade to %(want_version)s or newer.')
DefaultConfig = {
@ -116,6 +116,10 @@ class YoutubeCustomDownload(download.CustomDownload):
break
ext_filetype = mimetype_from_extension(dot_ext)
if ext_filetype:
# Youtube weba formats have a webm extension and get a video/webm mime-type
# but audio content has no width or height, so change it to audio/webm for correct icon and player
if ext_filetype.startswith('video/') and ('height' not in res or res['height'] is None):
ext_filetype = ext_filetype.replace('video/', 'audio/')
headers['content-type'] = ext_filetype
return headers, res.get('url', self._url)

View File

@ -1,120 +1,43 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- Generated with glade 3.38.2 -->
<interface>
<!-- interface-requires gtk+ 2.16 -->
<!-- interface-naming-policy toplevel-contextual -->
<object class="GtkDialog" id="gPodderPodcastDirectory">
<property name="visible">False</property>
<property name="can_focus">False</property>
<property name="border_width">6</property>
<requires lib="gtk+" version="3.12"/>
<object class="GtkWindow" id="gPodderPodcastDirectory">
<property name="can-focus">False</property>
<property name="border-width">6</property>
<property name="title" translatable="yes">Find new podcasts</property>
<property name="modal">True</property>
<property name="transient-for">parent_widget</property>
<property name="window_position">center-on-parent</property>
<property name="default_width">600</property>
<property name="default_height">400</property>
<property name="type_hint">dialog</property>
<child internal-child="vbox">
<property name="window-position">center-on-parent</property>
<property name="default-width">600</property>
<property name="default-height">400</property>
<property name="type-hint">dialog</property>
<child>
<object class="GtkBox" id="vb_directory">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="spacing">6</property>
<property name="can-focus">False</property>
<property name="orientation">vertical</property>
<child internal-child="action_area">
<object class="GtkHButtonBox" id="hboxBottomButtons">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="layout_style">end</property>
<child>
<object class="GtkButton" id="btnSelectAll">
<property name="label" translatable="yes">Select All</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">False</property>
<property name="use_underline">True</property>
<signal name="clicked" handler="on_btnSelectAll_clicked" swapped="no"/>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">False</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkButton" id="btnSelectNone">
<property name="label" translatable="yes">Select None</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">False</property>
<property name="use_underline">True</property>
<signal name="clicked" handler="on_btnSelectNone_clicked" swapped="no"/>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">False</property>
<property name="position">1</property>
</packing>
</child>
<child>
<object class="GtkButton" id="btnCancel">
<property name="label">gtk-cancel</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="has_focus">True</property>
<property name="can_default">True</property>
<property name="has_default">True</property>
<property name="receives_default">False</property>
<property name="use_stock">True</property>
<signal name="clicked" handler="on_btnCancel_clicked" swapped="no"/>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">False</property>
<property name="position">2</property>
</packing>
</child>
<child>
<object class="GtkButton" id="btnOK">
<property name="label">gtk-add</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">False</property>
<property name="use_stock">True</property>
<signal name="clicked" handler="on_btnOK_clicked" swapped="no"/>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">False</property>
<property name="position">3</property>
</packing>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="pack_type">end</property>
<property name="position">0</property>
</packing>
</child>
<property name="spacing">6</property>
<child>
<object class="GtkPaned" id="hpaned">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="orientation">horizontal</property>
<property name="can-focus">True</property>
<child>
<object class="GtkScrolledWindow" id="sw_providers">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="hscrollbar_policy">never</property>
<property name="vscrollbar_policy">automatic</property>
<property name="shadow_type">in</property>
<property name="can-focus">True</property>
<property name="hscrollbar-policy">never</property>
<property name="shadow-type">in</property>
<child>
<object class="GtkTreeView" id="tv_providers">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="headers_visible">False</property>
<property name="enable_search">False</property>
<signal name="row-activated" handler="on_tv_providers_row_activated" swapped="no"/>
<property name="can-focus">True</property>
<property name="headers-visible">False</property>
<property name="enable-search">False</property>
<signal name="cursor-changed" handler="on_tv_providers_cursor_changed" swapped="no"/>
<signal name="row-activated" handler="on_tv_providers_row_activated" swapped="no"/>
<child internal-child="selection">
<object class="GtkTreeSelection"/>
</child>
</object>
</child>
</object>
@ -126,19 +49,18 @@
<child>
<object class="GtkBox" id="vb_podcasts">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="spacing">5</property>
<property name="can-focus">False</property>
<property name="orientation">vertical</property>
<property name="spacing">5</property>
<child>
<object class="GtkBox" id="hb_text_entry">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="can-focus">False</property>
<property name="spacing">5</property>
<property name="orientation">horizontal</property>
<child>
<object class="GtkLabel" id="lb_search">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="can-focus">False</property>
<property name="label" translatable="yes">label</property>
</object>
<packing>
@ -150,12 +72,10 @@
<child>
<object class="GtkEntry" id="en_query">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="invisible_char">•</property>
<property name="primary_icon_activatable">False</property>
<property name="secondary_icon_activatable">False</property>
<property name="primary_icon_sensitive">True</property>
<property name="secondary_icon_sensitive">True</property>
<property name="can-focus">True</property>
<property name="invisible-char">•</property>
<property name="primary-icon-activatable">False</property>
<property name="secondary-icon-activatable">False</property>
<signal name="activate" handler="on_bt_search_clicked" swapped="no"/>
</object>
<packing>
@ -168,8 +88,8 @@
<object class="GtkButton" id="bt_search">
<property name="label" translatable="yes">...</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<property name="can-focus">True</property>
<property name="receives-default">True</property>
<signal name="clicked" handler="on_bt_search_clicked" swapped="no"/>
</object>
<packing>
@ -188,9 +108,7 @@
<child>
<object class="GtkScrolledWindow" id="sw_tagcloud">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="hscrollbar_policy">automatic</property>
<property name="vscrollbar_policy">automatic</property>
<property name="can-focus">True</property>
<child>
<placeholder/>
</child>
@ -204,15 +122,17 @@
<child>
<object class="GtkScrolledWindow" id="sw_podcasts">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="hscrollbar_policy">never</property>
<property name="vscrollbar_policy">automatic</property>
<property name="shadow_type">in</property>
<property name="can-focus">True</property>
<property name="hscrollbar-policy">never</property>
<property name="shadow-type">in</property>
<child>
<object class="GtkTreeView" id="tv_podcasts">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="headers_visible">False</property>
<property name="can-focus">True</property>
<property name="headers-visible">False</property>
<child internal-child="selection">
<object class="GtkTreeSelection"/>
</child>
</object>
</child>
</object>
@ -232,16 +152,111 @@
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkFlowBox" id="flowbox">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="min-children-per-line">1</property>
<property name="max-children-per-line">2</property>
<child>
<object class="GtkFlowBoxChild">
<property name="visible">True</property>
<property name="can-focus">True</property>
<child>
<object class="GtkButtonBox" id="selectbox">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="layout-style">start</property>
<child>
<object class="GtkButton" id="btnSelectAll">
<property name="label" translatable="yes">Select All</property>
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="receives-default">False</property>
<property name="use-underline">True</property>
<signal name="clicked" handler="on_btnSelectAll_clicked" swapped="no"/>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">False</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkButton" id="btnSelectNone">
<property name="label" translatable="yes">Select None</property>
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="receives-default">False</property>
<property name="use-underline">True</property>
<signal name="clicked" handler="on_btnSelectNone_clicked" swapped="no"/>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">False</property>
<property name="position">1</property>
</packing>
</child>
</object>
</child>
</object>
</child>
<child>
<object class="GtkFlowBoxChild">
<property name="visible">True</property>
<property name="can-focus">True</property>
<child>
<object class="GtkButtonBox" id="addbox">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="layout-style">end</property>
<child>
<object class="GtkButton" id="btnCancel">
<property name="label" translatable="yes">Cancel</property>
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="can-default">True</property>
<property name="has-default">True</property>
<property name="receives-default">False</property>
<signal name="clicked" handler="on_btnCancel_clicked" swapped="no"/>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">False</property>
<property name="pack-type">end</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkButton" id="btnOK">
<property name="label" translatable="yes">Add</property>
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="receives-default">False</property>
<signal name="clicked" handler="on_btnOK_clicked" swapped="no"/>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">False</property>
<property name="pack-type">end</property>
<property name="position">1</property>
</packing>
</child>
</object>
</child>
</object>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
</object>
</child>
<action-widgets>
<action-widget response="0">btnSelectAll</action-widget>
<action-widget response="0">btnSelectNone</action-widget>
<action-widget response="0">btnCancel</action-widget>
<action-widget response="0">btnOK</action-widget>
</action-widgets>
</object>
</interface>

View File

@ -1,4 +1,4 @@
.TH GPO "1" "April 2021" "gpodder 3.10.19" "User Commands"
.TH GPO "1" "June 2021" "gpodder 3.10.20" "User Commands"
.SH NAME
gpo \- Text mode interface of gPodder
.SH SYNOPSIS

View File

@ -1,5 +1,5 @@
.\" DO NOT MODIFY THIS FILE! It was generated by help2man 1.47.17.
.TH GPODDER "1" "April 2021" "gpodder 3.10.19" "User Commands"
.\" DO NOT MODIFY THIS FILE! It was generated by help2man 1.48.3.
.TH GPODDER "1" "June 2021" "gpodder 3.10.20" "User Commands"
.SH NAME
gpodder \- Media aggregator and podcast client
.SH SYNOPSIS

View File

@ -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.10.19'
__date__ = '2021-04-15'
__version__ = '3.10.20'
__date__ = '2021-06-06'
__copyright__ = '© 2005-2021 The gPodder Team'
__license__ = 'GNU General Public License, version 3 or later'
__url__ = 'http://gpodder.org/'

View File

@ -901,6 +901,10 @@ class DownloadTask(object):
if self.status == DownloadTask.FAILED:
self.__episode._download_error = self.error_message
# Delete empty partial files, they prevent streaming after a download failure (live stream)
if util.calculate_size(self.filename) == 0:
util.delete_file(self.tempname)
if self.status == DownloadTask.DOWNLOADING:
# Everything went well - we're done
self.status = DownloadTask.DONE

View File

@ -80,7 +80,7 @@ class gPodderChannel(BuilderWidget):
b = Gtk.TextBuffer()
if self.channel._update_error:
err = '\n\nERROR: {}'.format(self.channel._update_error)
err = '\n\n' + (_('ERROR: %s') % self.channel._update_error)
else:
err = ''
b.set_text(util.remove_html_tags(self.channel.description) + err)

View File

@ -127,6 +127,7 @@ class gPodderPodcastDirectory(BuilderWidget):
def setup_podcasts_treeview(self):
column = Gtk.TreeViewColumn('')
cell = Gtk.CellRendererToggle()
cell.set_fixed_size(48, -1)
column.pack_start(cell, False)
column.add_attribute(cell, 'active', DirectoryPodcastsModel.C_SELECTED)
cell.connect('toggled', lambda cell, path: self.podcasts_model.toggle(path))

View File

@ -2340,15 +2340,21 @@ class gPodder(BuilderWidget, dbus.service.Object):
def playback_episodes_for_real(self, episodes):
groups = collections.defaultdict(list)
for episode in episodes:
episode._download_error = None
player = self.episode_player(episode)
try:
allow_partial = (player != 'default')
filename = episode.get_playback_url(self.config, allow_partial)
except Exception as e:
episode._download_error = str(e)
continue
# Mark episode as played in the database
episode.playback_mark()
self.mygpo_client.on_playback([episode])
allow_partial = (player != 'default')
filename = episode.get_playback_url(self.config, allow_partial)
# Determine the playback resume position - if the file
# was played 100%, we simply start from the beginning
resume_position = episode.current_position
@ -3649,6 +3655,12 @@ class gPodder(BuilderWidget, dbus.service.Object):
# The remaining stuff is to be done in the GTK main thread
util.idle_add(finish_deletion, select_url)
def on_itemRefreshCover_activate(self, widget, *args):
assert self.active_channel is not None
self.podcast_list_model.clear_cover_cache(self.active_channel.url)
self.cover_downloader.replace_cover(self.active_channel, custom_url=False)
def on_itemRemoveChannel_activate(self, widget, *args):
if self.active_channel is None:
title = _('No podcast selected')

View File

@ -29,6 +29,8 @@ import xml.etree.ElementTree
from html.parser import HTMLParser
from urllib.parse import parse_qs
import requests
import gpodder
from gpodder import registry, util
@ -195,31 +197,41 @@ def get_real_download_url(url, allow_partial, preferred_fmt_ids=None):
vid = get_youtube_id(url)
if vid is not None:
page = None
url = 'https://www.youtube.com/get_video_info?&el=detailpage&video_id=' + vid
# TODO: changing 'detailpage' to 'embedded' allows age-restricted content
url = 'https://www.youtube.com/get_video_info?html5=1&el=detailpage&video_id=' + vid
r = requests.get(url)
if not r.ok:
logger.warning('Youtube get_video_info: %d %s' % (r.status_code, r.reason))
while page is None:
req = util.http_request(url, method='GET')
if 'location' in req.msg:
url = req.msg['location']
else:
page = req.read()
# TODO: watch URL does not work in europe due to GDPR cookie consent
url = 'https://www.youtube.com/watch?bpctr=9999999999&has_verified=1&v=' + vid
r = requests.get(url)
if not r.ok:
raise YouTubeError('%d %s' % (r.status_code, r.reason))
page = page.decode()
# Try to find the best video format available for this video
# (http://forum.videohelp.com/topic336882-1800.html#1912972)
ipr = re.search(r'ytInitialPlayerResponse\s*=\s*({.+?})\s*;', r.text)
if ipr is None:
raise YouTubeError('No ytInitialPlayerResponse found')
def find_urls(page):
old_page = None
new_page = ipr.group(1)
else:
old_page = r.text
new_page = None
def find_urls(old_page, new_page):
# streamingData is preferable to url_encoded_fmt_stream_map
# streamingData.formats are the same as url_encoded_fmt_stream_map
# streamingData.adaptiveFormats are audio-only and video-only formats
x = parse_qs(page)
x = parse_qs(old_page) if old_page else json.loads(new_page)
player_response = json.loads(x['player_response'][0]) if old_page and 'player_response' in x else x
error_message = None
if 'reason' in x:
# TODO: unknown if this is valid for new_page
error_message = util.remove_html_tags(x['reason'][0])
elif 'player_response' in x:
player_response = json.loads(x['player_response'][0])
elif 'playabilityStatus' in player_response:
playabilityStatus = player_response['playabilityStatus']
if 'reason' in playabilityStatus:
@ -230,15 +242,10 @@ def get_real_download_url(url, allow_partial, preferred_fmt_ids=None):
# playabilityStatus.liveStreamability.liveStreamabilityRenderer.displayEndscreen -- video has ended if present
if allow_partial and 'streamingData' in player_response and 'hlsManifestUrl' in player_response['streamingData']:
manifest = None
url = player_response['streamingData']['hlsManifestUrl']
while manifest is None:
req = util.http_request(url, method='GET')
if 'location' in req.msg:
url = req.msg['location']
else:
manifest = req.read()
manifest = manifest.decode().splitlines()
r = requests.get(player_response['streamingData']['hlsManifestUrl'])
if not r.ok:
raise YouTubeError('HLS Manifest: %d %s' % (r.status_code, r.reason))
manifest = r.text.splitlines()
urls = [line for line in manifest if line[0] != '#']
itag_re = re.compile(r'/itag/([0-9]+)/')
@ -249,41 +256,41 @@ def get_real_download_url(url, allow_partial, preferred_fmt_ids=None):
error_message = 'live stream'
elif 'streamingData' in player_response:
# DRM videos store url inside a cipher key - not supported
if 'formats' in player_response['streamingData']:
for f in player_response['streamingData']['formats']:
if 'url' in f:
if 'url' in f: # DRM videos store url inside a signatureCipher key
yield int(f['itag']), [f['url'], f.get('approxDurationMs')]
if 'adaptiveFormats' in player_response['streamingData']:
for f in player_response['streamingData']['adaptiveFormats']:
if 'url' in f:
if 'url' in f: # DRM videos store url inside a signatureCipher key
yield int(f['itag']), [f['url'], f.get('approxDurationMs')]
return
if error_message is not None:
raise YouTubeError('Cannot download video: %s' % error_message)
raise YouTubeError(('Cannot stream video: %s' if allow_partial else 'Cannot download video: %s') % error_message)
r4 = re.search(r'url_encoded_fmt_stream_map=([^&]+)', page)
if r4 is not None:
fmt_url_map = urllib.parse.unquote(r4.group(1))
for fmt_url_encoded in fmt_url_map.split(','):
video_info = parse_qs(fmt_url_encoded)
yield int(video_info['itag'][0]), [video_info['url'][0], None]
if old_page:
r4 = re.search(r'url_encoded_fmt_stream_map=([^&]+)', old_page)
if r4 is not None:
fmt_url_map = urllib.parse.unquote(r4.group(1))
for fmt_url_encoded in fmt_url_map.split(','):
video_info = parse_qs(fmt_url_encoded)
yield int(video_info['itag'][0]), [video_info['url'][0], None]
fmt_id_url_map = sorted(find_urls(page), reverse=True)
fmt_id_url_map = sorted(find_urls(old_page, new_page), reverse=True)
if not fmt_id_url_map:
drm = re.search(r'%22(cipher|signatureCipher)%22%3A', page)
drm = re.search(r'(%22(cipher|signatureCipher)%22%3A|"signatureCipher"):', old_page or new_page)
if drm is not None:
raise YouTubeError('Unsupported DRM content found for video ID "%s"' % vid)
raise YouTubeError('No formats found for video ID "%s"' % vid)
raise YouTubeError('Unsupported DRM content')
raise YouTubeError('No formats found')
formats_available = set(fmt_id for fmt_id, url in fmt_id_url_map)
fmt_id_url_map = dict(fmt_id_url_map)
for id in preferred_fmt_ids:
if re.search(r'\+', str(id)):
# skip formats that contain a + (136+140)
if re.search(r'(^best|\+)', str(id)):
# skip formats that contain 'best.*' or a + (136+140)
continue
id = int(id)
if id in formats_available:
@ -298,7 +305,7 @@ def get_real_download_url(url, allow_partial, preferred_fmt_ids=None):
url, duration = fmt_id_url_map[id]
break
else:
raise YouTubeError('No preferred formats found for video ID "%s"' % vid)
raise YouTubeError('No preferred formats found')
return url, duration
@ -366,10 +373,15 @@ def get_real_channel_url(url):
def get_channel_id_url(url):
if 'youtube.com' in url:
try:
channel_url = ''
raw_xml_data = io.BytesIO(util.urlopen(url).content)
xml_data = xml.etree.ElementTree.parse(raw_xml_data)
channel_id = xml_data.find("{http://www.youtube.com/xml/schemas/2015}channelId").text
req = util.urlopen(url)
# video page may contain corrupt HTML/XML, search for tag to avoid exception
m = re.search(r'<meta itemprop="channelId" content="([^"]+)">', req.text)
if m:
channel_id = m.group(1)
else:
raw_xml_data = io.BytesIO(req.content)
xml_data = xml.etree.ElementTree.parse(raw_xml_data)
channel_id = xml_data.find("{http://www.youtube.com/xml/schemas/2015}channelId").text
channel_url = 'https://www.youtube.com/channel/{}'.format(channel_id)
return channel_url
@ -473,27 +485,34 @@ def parse_youtube_url(url):
scheme, netloc, path, query, fragment = urllib.parse.urlsplit(url)
logger.debug("Analyzing URL: {}".format(" ".join([scheme, netloc, path, query, fragment])))
if 'youtube.com' in netloc and ('/user/' in path or '/channel/' in path or 'list=' in query):
logger.debug("Valid Youtube URL detected. Parsing...")
if 'youtube.com' in netloc:
if '/user/' in path or '/channel/' in path or 'list=' in query:
logger.debug("Valid Youtube URL detected. Parsing...")
if path.startswith('/user/'):
user_id = path.split('/')[2]
query = 'user={user_id}'.format(user_id=user_id)
if path.startswith('/user/'):
user_id = path.split('/')[2]
query = 'user={user_id}'.format(user_id=user_id)
if path.startswith('/channel/'):
channel_id = path.split('/')[2]
query = 'channel_id={channel_id}'.format(channel_id=channel_id)
if path.startswith('/channel/'):
channel_id = path.split('/')[2]
query = 'channel_id={channel_id}'.format(channel_id=channel_id)
if 'list=' in query:
playlist_query = [query_value for query_value in query.split("&") if 'list=' in query_value][0]
playlist_id = playlist_query[5:]
query = 'playlist_id={playlist_id}'.format(playlist_id=playlist_id)
if 'list=' in query:
playlist_query = [query_value for query_value in query.split("&") if 'list=' in query_value][0]
playlist_id = playlist_query[5:]
query = 'playlist_id={playlist_id}'.format(playlist_id=playlist_id)
path = '/feeds/videos.xml'
path = '/feeds/videos.xml'
new_url = urllib.parse.urlunsplit((scheme, netloc, path, query, fragment))
logger.debug("New Youtube URL: {}".format(new_url))
return new_url
else:
logger.debug("Not a valid Youtube URL: {}".format(url))
return url
new_url = urllib.parse.urlunsplit((scheme, netloc, path, query, fragment))
logger.debug("New Youtube URL: {}".format(new_url))
return new_url
# look for channel URL in page
new_url = get_channel_id_url(url)
if new_url:
logger.debug("New Youtube URL: {}".format(new_url))
return new_url
logger.debug("Not a valid Youtube URL: {}".format(url))
return url

13
tools/requirements.txt Normal file
View File

@ -0,0 +1,13 @@
# PyPI / pip requirements for Linux
# For the benefit of e.g. flatpak-pip-generator.
#
mygpoclient==1.8
podcastparser==0.6.6
requests[socks]==2.25.1
urllib3==1.26.5
html5lib==1.1
mutagen==1.45.1
dbus-python
youtube_dl
# eyed3 is optional and pulls in a lot of dependencies, so disable by default
# eyed3

View File

@ -91,9 +91,9 @@ html5lib==1.1
webencodings==0.5.1
certifi==2020.11.8
mutagen==1.45.1
youtube_dl==2021.03.25
requests==2.25.0
urllib3==1.26.4
youtube_dl
requests==2.25.1
urllib3==1.26.5
chardet==4.0.0
idna==3.1
PySocks==1.7.1