Youtube live streaming support
This commit is contained in:
parent
966631ecc0
commit
bc62e3bb4c
|
@ -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>
|
||||
|
|
|
@ -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': {
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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:
|
||||
|
|
Loading…
Reference in New Issue