registry.custom_downloader + registry.download_url

for extensions to be able to register custom downloaders,
not only resolving episode to a real download url.
refactor {escapist,youtube,vimeo}.get_real_download_url to use registry.
This commit is contained in:
Eric Le Lay 2019-08-17 16:25:00 +02:00
parent c847988531
commit a05bd85d76
7 changed files with 118 additions and 64 deletions

View File

@ -3,6 +3,7 @@
# Requirements: gPodder 3.x (or "tres" branch newer than 2011-06-08)
# (c) 2011-06-08 Thomas Perl <thp.io/about>
# Released under the same license terms as gPodder itself.
import functools
import logging
import subprocess
@ -42,8 +43,8 @@ class Player(object):
def open_files(self, filenames):
raise NotImplemented('Must be implemented by subclass')
def enqueue_episodes(self, episodes):
filenames = [episode.get_playback_url() for episode in episodes]
def enqueue_episodes(self, episodes, config=None):
filenames = [episode.get_playback_url(config=config) for episode in episodes]
self.open_files(filenames)
@ -104,8 +105,8 @@ class MPRISResumer(FreeDesktopPlayer):
return False
return util.find_command(self.command[0]) is not None
def enqueue_episodes(self, episodes):
self.do_enqueue(episodes[0].get_playback_url(),
def enqueue_episodes(self, episodes, config=None):
self.do_enqueue(episodes[0].get_playback_url(config=config),
episodes[0].current_position)
for episode in episodes:
@ -259,6 +260,7 @@ class gPodderExtension:
def __init__(self, container):
self.container = container
self.config = container.config
self.gpodder_config = self.container.manager.core.config
# Only display media players that can be found at extension load time
self.players = [player for player in PLAYERS if player.is_installed()]
@ -273,13 +275,15 @@ class gPodderExtension:
if not any(e.file_exists() for e in episodes):
return None
ret = [(p.title, p.enqueue_episodes) for p in self.players]
ret = [(p.title, functools.partial(p.enqueue_episodes, config=self.gpodder_config))
for p in self.players]
# needs dbus, doesn't handle more than 1 episode
# and no point in using DBus when episode is not played.
if not hasattr(gpodder.dbus_session_bus, 'fake') and \
len(episodes) == 1 and episodes[0].current_position > 0:
ret.extend([(p.title, p.enqueue_episodes) for p in self.resumers])
ret.extend([(p.title, functools.partial(p.enqueue_episodes, config=self.gpodder_config))
for p in self.resumers])
return ret

View File

@ -41,13 +41,43 @@ import urllib.request
from email.header import decode_header
import gpodder
from gpodder import escapist_videos, util, vimeo, youtube
from gpodder import registry, util
logger = logging.getLogger(__name__)
_ = gpodder.gettext
class CustomDownload:
""" abstract class for custom downloads. DownloadTask call retrieve_resume() on it """
def retrieve_resume(self, unused_tempname, reporthook):
"""
:param str tempname: temporary filename for the download
:param func(number, number, number) reporthook: callback for download progress (count, blockSize, totalSize)
:return dict(str, str), str: (headers, real_url)
"""
return {}, None
class CustomDownloader:
"""
abstract class for custom downloaders.
DownloadTask calls custom_downloader to get a CustomDownload
"""
def custom_downloader(self, config, episode):
"""
if this custom downloader has a custom download method (e.g. youtube-dl),
return a CustomDownload. Else return None
:param config: gpodder config (e.g. to get preferred video format)
:param model.PodcastEpisode episode: episode to download
:return CustomDownload: object used to download the episode
"""
return None
def get_header_param(headers, param, header_name):
"""Extract a HTTP header parameter from a dict
@ -746,53 +776,59 @@ class DownloadTask(object):
self.episode.download_task = self
try:
# Resolve URL and start downloading the episode
fmt_ids = youtube.get_fmt_ids(self._config.youtube)
custom_downloader = registry.custom_downloader.resolve(self._config, None, self.episode)
url = self.__episode.url
url = youtube.get_real_download_url(self.__episode.url, fmt_ids)
url = vimeo.get_real_download_url(url, self._config.vimeo.fileformat)
url = escapist_videos.get_real_download_url(url)
if custom_downloader:
logger.info('Downloading %s with %s', url, custom_downloader)
headers, real_url = custom_downloader.retrieve_resume(
self.tempname, reporthook=self.status_updated)
else:
# Resolve URL and start downloading the episode
res = registry.download_url.resolve(self._config, None, self.episode)
if res:
url = res
if url == self.__episode.url:
# don't modify custom urls (#635 - vimeo breaks if * is unescaped)
url = url.strip()
url = util.iri_to_url(url)
if url == self.__episode.url:
# don't modify custom urls (#635 - vimeo breaks if * is unescaped)
url = url.strip()
url = util.iri_to_url(url)
logger.info("Downloading %s", url)
downloader = DownloadURLOpener(self.__episode.channel)
logger.info("Downloading %s", url)
downloader = DownloadURLOpener(self.__episode.channel)
# HTTP Status codes for which we retry the download
retry_codes = (408, 418, 504, 598, 599)
max_retries = max(0, self._config.auto.retries)
# HTTP Status codes for which we retry the download
retry_codes = (408, 418, 504, 598, 599)
max_retries = max(0, self._config.auto.retries)
# Retry the download on timeout (bug 1013)
for retry in range(max_retries + 1):
if retry > 0:
logger.info('Retrying download of %s (%d)', url, retry)
time.sleep(1)
# Retry the download on timeout (bug 1013)
for retry in range(max_retries + 1):
if retry > 0:
logger.info('Retrying download of %s (%d)', url, retry)
time.sleep(1)
try:
headers, real_url = downloader.retrieve_resume(url,
self.tempname, reporthook=self.status_updated)
# If we arrive here, the download was successful
break
except urllib.error.ContentTooShortError as ctse:
if retry < max_retries:
logger.info('Content too short: %s - will retry.',
url)
continue
raise
except socket.timeout as tmout:
if retry < max_retries:
logger.info('Socket timeout: %s - will retry.', url)
continue
raise
except gPodderDownloadHTTPError as http:
if retry < max_retries and http.error_code in retry_codes:
logger.info('HTTP error %d: %s - will retry.',
http.error_code, url)
continue
raise
try:
headers, real_url = downloader.retrieve_resume(url,
self.tempname, reporthook=self.status_updated)
# If we arrive here, the download was successful
break
except urllib.error.ContentTooShortError as ctse:
if retry < max_retries:
logger.info('Content too short: %s - will retry.',
url)
continue
raise
except socket.timeout as tmout:
if retry < max_retries:
logger.info('Socket timeout: %s - will retry.', url)
continue
raise
except gPodderDownloadHTTPError as http:
if retry < max_retries and http.error_code in retry_codes:
logger.info('HTTP error %d: %s - will retry.',
http.error_code, url)
continue
raise
new_mimetype = headers.get('content-type', self.__episode.mime_type)
old_mimetype = self.__episode.mime_type

View File

@ -31,7 +31,7 @@ import urllib.parse
import urllib.request
import gpodder
from gpodder import util
from gpodder import registry, util
logger = logging.getLogger(__name__)
@ -51,6 +51,12 @@ DATA_COVERART_RE = re.compile(r'<url>(http:.+\.jpg)</url>')
class EscapistError(BaseException): pass
@registry.download_url.register
def escapist_real_download_url(unused_config, episode):
res = get_real_download_url(episode.url)
return None if res == episode.url else res
def get_real_download_url(url):
video_id = get_escapist_id(url)
if video_id is None:

View File

@ -2011,11 +2011,8 @@ class gPodder(BuilderWidget, dbus.service.Object):
episode.playback_mark()
self.mygpo_client.on_playback([episode])
fmt_ids = youtube.get_fmt_ids(self.config.youtube)
vimeo_fmt = self.config.vimeo.fileformat
allow_partial = (player != 'default')
filename = episode.get_playback_url(fmt_ids, vimeo_fmt, allow_partial)
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

View File

@ -37,8 +37,8 @@ import time
import gpodder
import podcastparser
from gpodder import (coverart, escapist_videos, feedcore, schema, util, vimeo,
youtube)
from gpodder import (coverart, escapist_videos, feedcore, registry, schema,
util, vimeo, youtube)
logger = logging.getLogger(__name__)
@ -494,7 +494,7 @@ class PodcastEpisode(PodcastModelObject):
self.set_state(gpodder.STATE_DELETED)
def get_playback_url(self, fmt_ids=None, vimeo_fmt=None, allow_partial=False):
def get_playback_url(self, config=None, allow_partial=False):
"""Local (or remote) playback/streaming filename/URL
Returns either the local filename or a streaming URL that
@ -510,11 +510,8 @@ class PodcastEpisode(PodcastModelObject):
return url + '.partial'
if url is None or not os.path.exists(url):
url = self.url
url = youtube.get_real_download_url(url, fmt_ids)
url = vimeo.get_real_download_url(url, vimeo_fmt)
url = escapist_videos.get_real_download_url(url)
# FIXME: may custom downloaders provide the real url ?
url = registry.download_url.resolve(config, self.url, self)
return url
def find_unique_file_name(self, filename, extension):

View File

@ -28,7 +28,7 @@ import logging
import re
import gpodder
from gpodder import util
from gpodder import registry, util
_ = gpodder.gettext
@ -49,6 +49,13 @@ FORMATS = tuple((x, x) for x in FILEFORMAT_RANKING)
class VimeoError(BaseException): pass
@registry.download_url.register
def vimeo_real_download_url(config, episode):
fmt = config.vimeo.fileformat if config else None
res = get_real_download_url(episode.url, preferred_fileformat=fmt)
return None if res == episode.url else res
def get_real_download_url(url, preferred_fileformat=None):
video_id = get_vimeo_id(url)

View File

@ -28,7 +28,7 @@ import xml.etree.ElementTree
from html.parser import HTMLParser
from urllib.parse import parse_qs
from gpodder import util
from gpodder import registry, util
logger = logging.getLogger(__name__)
@ -109,6 +109,13 @@ def get_fmt_ids(youtube_config):
return fmt_ids
@registry.download_url.register
def youtube_real_download_url(config, episode):
fmt_ids = get_fmt_ids(config.youtube) if config else None
res = get_real_download_url(episode.url, fmt_ids)
return None if res == episode.url else res
def get_real_download_url(url, preferred_fmt_ids=None):
if not preferred_fmt_ids:
preferred_fmt_ids, _, _ = formats_dict[22] # MP4 720p