From 3abaf0d18d48bcd5da9c2fd0ff1a72d7db6d9b91 Mon Sep 17 00:00:00 2001 From: lawrence Date: Sun, 19 Nov 2023 21:28:35 +0000 Subject: [PATCH] Fix mix command YouTube updates mean that `youtube-search-python` can no longer fetch mixes properly; they behave slightly differently to playlists. The solution is to split these functions out and fetch the data using `yt-dlp`. --- mps_youtube/commands/search.py | 9 +++---- mps_youtube/commands/songlist.py | 15 +++++++++++ mps_youtube/pafy.py | 43 +++++++++++++++++++++++++++++++- 3 files changed, 61 insertions(+), 6 deletions(-) diff --git a/mps_youtube/commands/search.py b/mps_youtube/commands/search.py index 196d6f4a..c400d9e8 100644 --- a/mps_youtube/commands/search.py +++ b/mps_youtube/commands/search.py @@ -21,7 +21,7 @@ from .. import c, config, content, contentquery, g, listview, screen, util from ..playlist import Playlist, Video from . import command -from .songlist import paginatesongs, plist +from .songlist import paginatesongs, plist, mixlist ISO8601_TIMEDUR_EX = re.compile(r'PT((\d{1,3})H)?((\d{1,3})M)?((\d{1,2})S)?') @@ -523,11 +523,10 @@ def mix(num): if item is None: g.message = util.F('invalid item') return - item = util.get_pafy(item) - # Mix playlists are made up of 'RD' + video_id + item = g.model[int(num) -1] try: - plist("RD" + item.videoid) - except OSError: + mixlist(item.ytid) + except KeyError: g.message = util.F('no mix') diff --git a/mps_youtube/commands/songlist.py b/mps_youtube/commands/songlist.py index ae4d72c0..29176fcc 100644 --- a/mps_youtube/commands/songlist.py +++ b/mps_youtube/commands/songlist.py @@ -94,6 +94,21 @@ def pl_seg(s, e): loadmsg = "Retrieving YouTube playlist" paginatesongs(pl_seg, length=len(ytpl.videos), msg=msg, loadmsg=loadmsg) +def mixlist(video_url): + """ Retrieve YouTube mix from video slug """ + + util.dbg("%sFetching mix using pafy%s", c.y, c.w) + # No caching here because mixes are different every time they are retrieved + ytpl = pafy.get_mix(video_url) + plitems = util.IterSlicer(ytpl.songs) + + def pl_seg(s, e): + return [Video(i["id"], i["title"], i["duration_parsed"]) for i in plitems[s:e]] + + msg = "Showing YouTube playlist %s" % (c.y + ytpl.name + c.w) + loadmsg = "Retrieving YouTube playlist" + paginatesongs(pl_seg, length=len(ytpl.songs), msg=msg, loadmsg=loadmsg) + @command(r'(rm|add)\s*(-?\d[-,\d\s]{,250})', 'rm', 'add') def songlist_rm_add(action, songrange): diff --git a/mps_youtube/pafy.py b/mps_youtube/pafy.py index db4a5c2e..b5c62ad1 100644 --- a/mps_youtube/pafy.py +++ b/mps_youtube/pafy.py @@ -8,6 +8,8 @@ import yt_dlp from youtubesearchpython import VideosSearch, ChannelsSearch, PlaylistsSearch, Suggestions, Playlist, playlist_from_channel_id, Comments, Video, Channel, ChannelSearch +from . import playlists + class MyLogger: @@ -107,6 +109,45 @@ def get_playlist(playlist_id): playlist.getNextVideos() return playlist +def get_mix(video_id): + + ''' + Get a list of all videos (max 100) in the mix of the specified video_id + ''' + + # youtubesearchpython gets playlists by parsing the HTML of the + # corresponding page, and this doesn't work for mixes. so, we need to + # enlist the help of yt-dlp instead + + # Mix playlist IDs are made up of "RD" + video_id + # In order to see the mix, we need to fetch the first video in the list + playlist_url = "https://www.youtube.com/watch?v=%s&list=RD%s" % (video_id, video_id) + + ydl_opts = { + "extract_flat": True, + "playlist_items": "1-100", + "dump_single_json": True, + "logger": MyLogger() + } + playlist_data = {} + with yt_dlp.YoutubeDL(ydl_opts) as ydl: + playlist_data = ydl.extract_info(playlist_url) + + videos = [] + for video_data in playlist_data["entries"]: + video = {} + + video["id"] = video_data["id"] + video["title"] = video_data["title"] + video["link"] = "https://www.youtube.com/watch?v=%s" % video_data["id"] + video["duration_parsed"] = video_data["duration"] + + videos.append(video) + + # NB: We use a playlists.Playlist instead of youtubesearchpython.Playlist + playlist = playlists.Playlist(playlist_data["title"], videos) + return playlist + def get_video_title_suggestions(query): suggestions = Suggestions(language = 'en', region = 'US') related_searches = suggestions.get(query)['result'] @@ -236,4 +277,4 @@ def get_subtitles(ytid, output_dir): # Download the subtitle ydl.download([url]) path = f'{outtmpl}.{lang}.vtt' - return path if os.path.isfile(path) else None \ No newline at end of file + return path if os.path.isfile(path) else None