Youtube live streaming support

This commit is contained in:
auouymous 2020-09-12 02:34:39 -06:00
parent 966631ecc0
commit bc62e3bb4c
7 changed files with 125 additions and 15 deletions

View File

@ -184,6 +184,7 @@
<property name="label" translatable="yes">Preferred YouTube format:</property>
</object>
<packing>
<property name="left_attach">0</property>
<property name="top_attach">0</property>
</packing>
</child>
@ -199,6 +200,30 @@
<property name="top_attach">0</property>
</packing>
</child>
<child>
<object class="GtkLabel" id="label_preferred_youtube_hls_format">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="xalign">0</property>
<property name="label" translatable="yes">Preferred YouTube HLS format:</property>
</object>
<packing>
<property name="left_attach">0</property>
<property name="top_attach">2</property>
</packing>
</child>
<child>
<object class="GtkComboBox" id="combobox_preferred_youtube_hls_format">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="hexpand">True</property>
<signal name="changed" handler="on_combobox_preferred_youtube_hls_format_changed" swapped="no"/>
</object>
<packing>
<property name="left_attach">1</property>
<property name="top_attach">2</property>
</packing>
</child>
<child>
<object class="GtkLabel" id="label_preferred_vimeo_format">
<property name="visible">True</property>
@ -207,8 +232,8 @@
<property name="label" translatable="yes">Preferred Vimeo format:</property>
</object>
<packing>
<property name="top_attach">2</property>
<property name="left_attach">0</property>
<property name="top_attach">4</property>
</packing>
</child>
<child>
@ -219,7 +244,7 @@
</object>
<packing>
<property name="left_attach">1</property>
<property name="top_attach">2</property>
<property name="top_attach">4</property>
</packing>
</child>
</object>

View File

@ -203,6 +203,8 @@ defaults = {
'youtube': {
'preferred_fmt_id': 18, # default fmt_id (see fallbacks in youtube.py)
'preferred_fmt_ids': [], # for advanced uses (custom fallback sequence)
'preferred_hls_fmt_id': 93, # default fmt_id (see fallbacks in youtube.py)
'preferred_hls_fmt_ids': [], # for advanced uses (custom fallback sequence)
},
'vimeo': {

View File

@ -428,7 +428,7 @@ class DefaultDownloader(CustomDownloader):
def custom_downloader(config, episode):
url = episode.url
# Resolve URL and start downloading the episode
res = registry.download_url.resolve(config, None, episode)
res = registry.download_url.resolve(config, None, episode, False)
if res:
url = res
if url == episode.url:

View File

@ -132,6 +132,32 @@ class YouTubeVideoFormatListModel(Gtk.ListStore):
self._config.youtube.preferred_fmt_id = self[index][self.C_ID]
class YouTubeVideoHLSFormatListModel(Gtk.ListStore):
C_CAPTION, C_ID = list(range(2))
def __init__(self, config):
Gtk.ListStore.__init__(self, str, int)
self._config = config
if self._config.youtube.preferred_hls_fmt_ids:
caption = _('Custom (%(format_ids)s)') % {
'format_ids': ', '.join(str(x) for x in self._config.youtube.preferred_hls_fmt_ids),
}
self.append((caption, 0))
for id, (fmt_id, path, description) in youtube.hls_formats:
self.append((description, id))
def get_index(self):
for index, row in enumerate(self):
if self._config.youtube.preferred_hls_fmt_id == row[self.C_ID]:
return index
return 0
def set_index(self, index):
self._config.youtube.preferred_hls_fmt_id = self[index][self.C_ID]
class VimeoVideoFormatListModel(Gtk.ListStore):
C_CAPTION, C_ID = list(range(2))
@ -185,6 +211,13 @@ class gPodderPreferences(BuilderWidget):
self.combobox_preferred_youtube_format.add_attribute(cellrenderer, 'text', self.preferred_youtube_format_model.C_CAPTION)
self.combobox_preferred_youtube_format.set_active(self.preferred_youtube_format_model.get_index())
self.preferred_youtube_hls_format_model = YouTubeVideoHLSFormatListModel(self._config)
self.combobox_preferred_youtube_hls_format.set_model(self.preferred_youtube_hls_format_model)
cellrenderer = Gtk.CellRendererText()
self.combobox_preferred_youtube_hls_format.pack_start(cellrenderer, True)
self.combobox_preferred_youtube_hls_format.add_attribute(cellrenderer, 'text', self.preferred_youtube_hls_format_model.C_CAPTION)
self.combobox_preferred_youtube_hls_format.set_active(self.preferred_youtube_hls_format_model.get_index())
self.preferred_vimeo_format_model = VimeoVideoFormatListModel(self._config)
self.combobox_preferred_vimeo_format.set_model(self.preferred_vimeo_format_model)
cellrenderer = Gtk.CellRendererText()
@ -461,6 +494,10 @@ class gPodderPreferences(BuilderWidget):
index = self.combobox_preferred_youtube_format.get_active()
self.preferred_youtube_format_model.set_index(index)
def on_combobox_preferred_youtube_hls_format_changed(self, widget):
index = self.combobox_preferred_youtube_hls_format.get_active()
self.preferred_youtube_hls_format_model.set_index(index)
def on_combobox_preferred_vimeo_format_changed(self, widget):
index = self.combobox_preferred_vimeo_format.get_active()
self.preferred_vimeo_format_model.set_index(index)

View File

@ -512,7 +512,7 @@ class PodcastEpisode(PodcastModelObject):
if url is None or not os.path.exists(url):
# FIXME: may custom downloaders provide the real url ?
url = registry.download_url.resolve(config, self.url, self)
url = registry.download_url.resolve(config, self.url, self, allow_partial)
return url
def find_unique_file_name(self, filename, extension):

View File

@ -50,7 +50,7 @@ class VimeoError(BaseException): pass
@registry.download_url.register
def vimeo_real_download_url(config, episode):
def vimeo_real_download_url(config, episode, allow_partial):
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

View File

@ -38,7 +38,7 @@ _ = gpodder.gettext
# http://en.wikipedia.org/wiki/YouTube#Quality_and_formats
# https://gist.github.com/Marco01809/34d47c65b1d28829bb17c24c04a0096f
# https://github.com/ytdl-org/youtube-dl/blob/master/youtube_dl/extractor/youtube.py#L447
# adaptive audio formats
# 140 MP4 128k
@ -129,6 +129,21 @@ formats = [
]
formats_dict = dict(formats)
# streaming formats and fallbacks to lower quality
hls_144 = [91]
hls_240 = [92] + hls_144
hls_360 = [93] + hls_240
hls_480 = [94] + hls_360
hls_720 = [95] + hls_480
hls_formats = [
(95, (hls_720, '9/1280x720/9/0/115', 'MP4 720p (1280x720)')), # N/A, 256 kbps
(94, (hls_480, '9/854x480/9/0/115', 'MP4 480p (854x480)')), # N/A, 128 kbps
(93, (hls_360, '9/640x360/9/0/115', 'MP4 360p (640x360)')), # N/A, 128 kbps
(92, (hls_240, '9/426x240/9/0/115', 'MP4 240p (426x240)')), # N/A, 48 kbps
(91, (hls_144, '9/256x144/9/0/115', 'MP4 144p (256x144)')), # N/A, 48 kbps
]
hls_formats_dict = dict(hls_formats)
V3_API_ENDPOINT = 'https://www.googleapis.com/youtube/v3'
CHANNEL_VIDEOS_XML = 'https://www.youtube.com/feeds/videos.xml'
@ -137,27 +152,39 @@ class YouTubeError(Exception):
pass
def get_fmt_ids(youtube_config):
def get_fmt_ids(youtube_config, allow_partial):
if allow_partial:
if youtube_config.preferred_hls_fmt_id == 0:
hls_fmt_ids = (youtube_config.preferred_hls_fmt_ids if youtube_config.preferred_hls_fmt_ids else [])
else:
format = hls_formats_dict.get(youtube_config.preferred_hls_fmt_id)
if format is None:
hls_fmt_ids = []
else:
hls_fmt_ids, path, description = format
else:
hls_fmt_ids = []
if youtube_config.preferred_fmt_id == 0:
return (youtube_config.preferred_fmt_ids if youtube_config.preferred_fmt_ids else [])
return (youtube_config.preferred_fmt_ids + hls_fmt_ids if youtube_config.preferred_fmt_ids else hls_fmt_ids)
format = formats_dict.get(youtube_config.preferred_fmt_id)
if format is None:
return []
return hls_fmt_ids
fmt_ids, path, description = format
return fmt_ids
return fmt_ids + hls_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, duration = get_real_download_url(episode.url, fmt_ids)
def youtube_real_download_url(config, episode, allow_partial):
fmt_ids = get_fmt_ids(config.youtube, allow_partial) if config else None
res, duration = get_real_download_url(episode.url, allow_partial, fmt_ids)
if duration is not None:
episode.total_time = int(int(duration) / 1000)
return None if res == episode.url else res
def get_real_download_url(url, preferred_fmt_ids=None):
def get_real_download_url(url, allow_partial, preferred_fmt_ids=None):
if not preferred_fmt_ids:
preferred_fmt_ids, _, _ = formats_dict[22] # MP4 720p
@ -198,6 +225,25 @@ def get_real_download_url(url, preferred_fmt_ids=None):
and not playabilityStatus['liveStreamability'].get('liveStreamabilityRenderer', {}).get('displayEndscreen', False):
# playabilityStatus.liveStreamability -- video is or was a live stream
# 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()
urls = [line for line in manifest if line[0] != '#']
itag_re = re.compile('/itag/([0-9]+)/')
for url in urls:
itag = itag_re.search(url).group(1)
yield int(itag), [url, None]
return
error_message = 'live stream'
elif 'streamingData' in player_response:
# DRM videos store url inside a cipher key - not supported
@ -238,7 +284,7 @@ def get_real_download_url(url, preferred_fmt_ids=None):
continue
id = int(id)
if id in formats_available:
format = formats_dict.get(id)
format = formats_dict.get(id) or hls_formats_dict.get(id)
if format is not None:
_, _, description = format
else: