From 23525f172bdc0de75c26e3cab795472373d4feeb Mon Sep 17 00:00:00 2001 From: Adam Date: Mon, 30 Sep 2024 03:41:15 -0700 Subject: [PATCH] Retrieve max_res video stream and add independent audio stream if available. Update vlc and mpv facades with separate audio streams. --- mps_youtube/players/mplayer.py | 5 ++++ mps_youtube/players/mpv.py | 3 ++ mps_youtube/players/vlc.py | 3 ++ mps_youtube/streams.py | 53 +++++++++++++++++++++++++++++++--- 4 files changed, 60 insertions(+), 4 deletions(-) diff --git a/mps_youtube/players/mplayer.py b/mps_youtube/players/mplayer.py index 72f976b1..501f7977 100644 --- a/mps_youtube/players/mplayer.py +++ b/mps_youtube/players/mplayer.py @@ -70,6 +70,11 @@ def _generate_real_playerargs(self): util.list_update("-cache", args) util.list_update("4096", args) + # MPlayer currently only supports audio files local to the filesystem, + # so this will not work with audio streams. + if 'audio_url' in self.stream and self.stream['mtype'] == 'video_only': + util.list_update(f'-audiofile {self.stream["audio_url"]}', args) + return [self.player] + args + [self.stream['url']] def clean_up(self): diff --git a/mps_youtube/players/mpv.py b/mps_youtube/players/mpv.py index 3d427713..2bafdb99 100644 --- a/mps_youtube/players/mpv.py +++ b/mps_youtube/players/mpv.py @@ -98,6 +98,9 @@ def _generate_real_playerargs(self): util.list_update('--no-video', args) util.list_update('--vid=no', args) + if 'audio_url' in self.stream and self.stream['mtype'] == 'video_only': + util.list_update(f'--audio-file={self.stream["audio_url"]}', args) + return [self.player] + args + [self.stream['url']] def clean_up(self): diff --git a/mps_youtube/players/vlc.py b/mps_youtube/players/vlc.py index 20e97d42..7b1d560e 100644 --- a/mps_youtube/players/vlc.py +++ b/mps_youtube/players/vlc.py @@ -24,6 +24,9 @@ def _generate_real_playerargs(self): if self.subtitle_path: args.extend(('--sub-file', self.subtitle_path)) + if 'audio_url' in self.stream and self.stream['mtype'] == 'video_only': + util.list_update(f'--input-slave={self.stream["audio_url"]}', args) + util.list_update("--play-and-exit", args) return [self.player] + args + [self.stream['url']] diff --git a/mps_youtube/streams.py b/mps_youtube/streams.py index 8062b25b..e04b68ee 100644 --- a/mps_youtube/streams.py +++ b/mps_youtube/streams.py @@ -1,3 +1,4 @@ +import re import time import threading from urllib.request import urlopen @@ -37,6 +38,25 @@ def prune(): def get(vid, force=False, callback=None, threeD=False): """ Get all streams as a dict. callback function passed to get_pafy. """ + + def get_mtype(resolution, acodec, url): + """ + Return the media type of the stream as a string. + + Returns "audio" for audio only streams, "video_only" for video only + streams, and "video" for video and audio streams (for backward + compatibility with existing code). + """ + if 'audio' in resolution: + return 'audio' + if '//manifest' not in url: + if acodec and acodec != 'none': + return 'video' + else: + return 'video_only' + else: + return '?' + now = time.time() ytid = vid.ytid have_stream = g.streams.get(ytid) and (g.streams[ytid]['expiry'] > now if g.streams[ytid]['expiry'] is not None else False) @@ -64,14 +84,27 @@ def get(vid, force=False, callback=None, threeD=False): ps = p.allstreams if threeD else [x for x in p.allstreams if not x.threed] + # Fetch all the audio only streams. Sort the list by the highest + # quality stream in descending order. + audio_only_streams = [ + s for s in ps + if s.get('acodec', 'none') != 'none' + and s.get('resolution', None).lower() == 'audio only' + ] + sorted_audio_only_streams = sorted( + audio_only_streams, + key=lambda s: [s['quality'], s['abr']], + reverse=True + ) + streams = [{"url": s['url'], "ext": s['ext'], "quality": s['resolution'], "rawbitrate": s.get('bitrate',-1), - "mtype": 'audio' if 'audio' in s['resolution'] else ('video' if s['acodec'] != 'none' else '?'), + "mtype": get_mtype(s['resolution'], s.get('acodec', None), s['url']), + "audio_url": sorted_audio_only_streams[0]['url'] if len(sorted_audio_only_streams) > 0 else '', "size": int(s.get('filesize') if s.get('filesize') is not None else s.get('filesize_approx', -1))} for s in ps] - if 'manifest' in streams[0]['url']: expiry = float(streams[0]['url'].split('/expire/')[1].split('/')[0]) else: @@ -113,7 +146,15 @@ def getbitrate(x): streams = [x for x in slist if x['mtype'] == "audio"] streams = sorted(streams, key=getbitrate, reverse=True) else: - streams = [x for x in slist if x['mtype'] == "video" and okres(x)] + # Determine whether the player supports video_only files with separate + # audio streams. MPlayer currently only supports audio files local to + # the filesystem. + acceptable_video_types = ['video'] + if re.search(r'mpv|vlc', config.PLAYER.get): + acceptable_video_types.append('video_only') + + streams = [x for x in slist if x['mtype'] in acceptable_video_types and okres(x)] + if not config.VIDEO_FORMAT.get == "auto": if config.VIDEO_FORMAT.get == "mp4": streams = [x for x in streams if x['ext'] == "mp4"] @@ -122,7 +163,11 @@ def getbitrate(x): if config.VIDEO_FORMAT.get == "3gp": streams = [x for x in streams if x['ext'] == "3gp"] if not streams: - streams = [x for x in slist if x['mtype'] == "video" and okres(x)] + streams = [ + x for x + in slist + if x['mtype'] in acceptable_video_types and okres(x) + ] streams = sorted(streams, key=getq, reverse=True) util.dbg("select stream, q: %s, audio: %s, len: %s", q, audio, len(streams))