From 83dedc83691ab82c625131082b45f03bb3704b3d Mon Sep 17 00:00:00 2001 From: kclauhk Date: Tue, 17 Sep 2024 03:29:56 +0800 Subject: [PATCH 1/8] [ie/extrememusic] Add extractor --- yt_dlp/extractor/_extractors.py | 5 + yt_dlp/extractor/extrememusic.py | 344 +++++++++++++++++++++++++++++++ 2 files changed, 349 insertions(+) create mode 100644 yt_dlp/extractor/extrememusic.py diff --git a/yt_dlp/extractor/_extractors.py b/yt_dlp/extractor/_extractors.py index d8abf0b5d..a6773869b 100644 --- a/yt_dlp/extractor/_extractors.py +++ b/yt_dlp/extractor/_extractors.py @@ -620,6 +620,11 @@ from .eurosport import EurosportIE from .euscreen import EUScreenIE from .expressen import ExpressenIE +from .extrememusic import ( + ExtremeMusicAIE, + ExtremeMusicIE, + ExtremeMusicPIE, +) from .eyedotv import EyedoTVIE from .facebook import ( FacebookAdsIE, diff --git a/yt_dlp/extractor/extrememusic.py b/yt_dlp/extractor/extrememusic.py new file mode 100644 index 000000000..5523a520f --- /dev/null +++ b/yt_dlp/extractor/extrememusic.py @@ -0,0 +1,344 @@ +import itertools +import re + +from .common import InfoExtractor +from ..utils import ( + determine_ext, + int_or_none, + join_nonempty, + merge_dicts, + str_or_none, + traverse_obj, + unified_strdate, + url_or_none, +) + + +class ExtremeMusicBaseIE(InfoExtractor): + _API_URL = 'https://snapi.extrememusic.com' + _REQUEST_HEADERS = None + + def _process_version_arg(self, arg): + self._version_requested = arg + + def _set_request_headers(self, video_id, country=None): + if not self._REQUEST_HEADERS: + # the site serves different versions of the same playlist id according to ISO country code, + # so use user's own country code or user-provided country code (extractor argument "country") + if not country: + country = [self._download_webpage('https://ipapi.co/country_code', video_id)] + env = self._download_json('https://www.extrememusic.com/env', video_id) + self._REQUEST_HEADERS = { + 'X-API-Auth': env['token'], + 'X-Viewer-Country': country[0], + } + return self._REQUEST_HEADERS + + def _get_album_data(self, album_id, video_id): + self._process_version_arg(self._configuration_arg('ver') or self._configuration_arg('version')) + headers = self._set_request_headers(video_id) + album = self._download_json(f'{self._API_URL}/albums/{album_id}', video_id, + note='Downloading album data', headers=headers) + if video_id == album_id: + bio = self._download_json(f'{self._API_URL}/albums/{album_id}/bio', video_id, fatal=False, + note='Downloading album data', headers=headers) + return merge_dicts(album, bio or {}) + else: + return album + + def _extract_track(self, album_data, track_id=None, version_id=None): + album_data = album_data or {} + if 'tracks' in album_data and 'track_sounds' in album_data: + if not track_id and version_id: + track_id = traverse_obj(album_data['track_sounds'], + (lambda _, v: v['id'] == int(version_id), 'track_id', {int}), get_all=False) + track = traverse_obj(album_data['tracks'], + (lambda _, v: v['id'] == int(track_id), {dict}), get_all=False) + info = {**traverse_obj(track, { + 'track': ('title', {str}), + 'track_number': ('sort_order', {lambda v: v + 1}, {int}), + 'track_id': ('track_no', {str}), + 'description': ('description', {lambda v: str_or_none(v) or None}), + 'artists': ('artists', {lambda v: v or traverse_obj(album_data, ('album', 'artist'))}, + {lambda v: (v if isinstance(v, list) else [v]) if v else None}), + 'composers': ('composers', ..., 'name'), + 'genres': (('genre', 'subgenre'), ..., 'label'), + 'tag': ('keywords', ..., 'label'), + 'album': ('album_title', {lambda v: str_or_none(v) or None}), + }), **traverse_obj(album_data, { + 'album_artists': ('album', 'artist', {lambda v: [v] if v else None}), + 'upload_date': ('album', 'created', {unified_strdate}), + })} + entries, thumbnails = [], [] + for image in traverse_obj(track, ('images', 'default')): + thumbnails.append(traverse_obj(image, { + 'url': ('url', {url_or_none}), + 'width': ('width', {int_or_none}), + 'height': ('height', {int_or_none}), + })) + for idx, sound_id in enumerate([version_id] if version_id else track['track_sound_ids']): + if sound := traverse_obj(album_data['track_sounds'], + (lambda _, v: v['id'] == int(sound_id) and v['track_id'] == int(track_id), + {dict}), get_all=False): + if (version_id + or (not version_id and ((not self._version_requested and idx == 0) + or 'all' in self._version_requested + or sound['version_type'].lower() in self._version_requested))): + formats = [] + for audio_url in traverse_obj(sound, ('assets', 'audio', ('preview_url', + 'preview_url_hls'))): + if determine_ext(audio_url) == 'm3u8': + m3u8_url = re.sub(r'\.m3u8\?.*', '/HLS/128_v4.m3u8', audio_url) + for f in self._extract_m3u8_formats(m3u8_url, sound_id, 'mpeg', fatal=False): + formats.append({ + **f, + 'vcodec': 'none', + 'perference': -2, + }) + else: + formats.append({ + 'url': audio_url, + 'vcodec': 'none', + }) + entries.append({ + 'id': str(sound_id), + 'title': join_nonempty('title', 'version_type', from_dict=sound, delim=' - '), + 'alt_title': sound['version_type'], + **info, + 'thumbnails': thumbnails, + 'duration': sound.get('duration'), + 'formats': formats, + 'webpage_url': f"https://www.extrememusic.com/albums/{track['album_id']}?item={track_id}&ver={sound_id}", + }) + + if len(entries) > 1: + return { + 'id': track_id, + **info, + 'entries': entries, + '_type': 'playlist', + } + elif len(entries) == 1: + return entries[0] + return [] + + +class ExtremeMusicIE(ExtremeMusicBaseIE): + _VALID_URL = r'https?://(?:www\.)?extrememusic\.com/albums/(?P\d+)\?(.*item=(?P\d+))?(.*ver=(?P\d+))?' + _TESTS = [{ + 'url': 'https://www.extrememusic.com/albums/15875?item=263381&ver=1265009&sharedTrack=dHJ1ZQ==', + 'info_dict': { + 'id': '1265009', + 'ext': 'mp3', + 'title': 'FOLLOW - Instrumental', + 'alt_title': 'Instrumental', + 'track': 'FOLLOW', + 'track_number': 5, + 'track_id': 'HPE316_05', + 'artists': ['PRAERS'], + 'composers': ['Joseph Andrew Banfi', 'Thomas Louis James White'], + 'genres': ['POP', 'DREAM', 'INDIE'], + 'tag': 'count:7', + 'album': 'AVALON', + 'album_artists': ['PRAERS'], + 'upload_date': '20240729', + 'thumbnail': 'https://d2oet5a29f64lj.cloudfront.net/img-data/w/2480/album/600/HPE316.jpg', + 'duration': 246, + }, + }, { + 'url': 'https://www.extrememusic.com/albums/15823?ver=1262087', + 'info_dict': { + 'id': '1262087', + 'ext': 'mp3', + 'title': 'MAGICAL HIGHWAY - VOCALS', + 'alt_title': 'VOCALS', + 'track': 'MAGICAL HIGHWAY', + 'track_number': 2, + 'track_id': 'ASM0002_02', + 'description': 'Full version - a fun, happy and upbeat pop track with a medium - fast tempo - electronic, bouncy, bright', + 'composers': ['ENB'], + 'genres': ['POP', 'ELECTRO', 'JPOP'], + 'tag': 'count:8', + 'album': 'TOKYO POPPIN\'', + 'upload_date': '20240709', + 'thumbnail': 'https://d2oet5a29f64lj.cloudfront.net/img-data/w/2480/album/600/ASM0002.jpg', + 'duration': 265, + }, + }, { + 'url': 'https://www.extrememusic.com/albums/15064?item=254704', + 'info_dict': { + 'id': '1178851', + 'ext': 'mp3', + 'title': 'SWEET TOOTH - Full Version', + 'alt_title': 'Full Version', + 'track': 'SWEET TOOTH', + 'track_number': 2, + 'track_id': 'HPE263_02', + 'artists': ['PILOT PAISLEY-ROSE'], + 'composers': ['PILOT PAISLEY ROSE SARACENO', 'SAMUEL JAMES BRANDT'], + 'genres': ['POP', 'ELECTRO', 'ROCK'], + 'tag': 'count:7', + 'album': 'ADDICTED', + 'album_artists': ['PILOT PAISLEY-ROSE'], + 'upload_date': '20230629', + 'thumbnail': 'https://d2oet5a29f64lj.cloudfront.net/img-data/w/2480/album/600/HPE263.jpg', + 'duration': 161, + }, + }, { + 'url': 'https://www.extrememusic.com/albums/1315?item=24795', + 'info_dict': { + 'id': '61003', + 'ext': 'mp3', + 'title': 'JOY TO THE WORLD (INST) - Instrumental', + 'alt_title': 'Instrumental', + 'track': 'JOY TO THE WORLD (INST)', + 'track_number': 6, + 'track_id': 'XEL016_06', + 'composers': ['TRADITIONAL'], + 'genres': ['HOLIDAY', 'CHRISTMAS'], + 'tag': 'count:5', + 'album': 'CHRISTMAS SPARKLE', + 'upload_date': '20041001', + 'thumbnail': 'https://d2oet5a29f64lj.cloudfront.net/img-data/w/2480/album/600/XEL016.jpg', + 'duration': 132, + }, + }] + + def _real_extract(self, url): + album_id, track_id, version_id = self._match_valid_url(url).group('album', 'id', 'ver') + album_data = self._get_album_data(album_id, track_id or version_id) + if result := self._extract_track(album_data, track_id, version_id): + return result + else: + self.raise_no_formats('No formats were found') + + +class ExtremeMusicAIE(ExtremeMusicBaseIE): + IE_NAME = 'ExtremeMusic:album' + _VALID_URL = r'https?://(?:www\.)?extrememusic\.com/albums/(?P\d+)(?!.*(item|ver)=)' + _TESTS = [{ + 'url': 'https://www.extrememusic.com/albums/6778', + 'info_dict': { + 'id': '6778', + 'album': 'Ethereal Voices', + }, + 'playlist_count': 11, + }, { + 'url': 'https://www.extrememusic.com/albums/15835', + 'info_dict': { + 'id': '15835', + 'album': 'BIGGEST BANG', + 'description': 'Minus Aura, a minimalist duo who create deep drama and emotion to put you under their spell.', + 'artists': ['MINUS AURA'], + 'genres': ['ELECTRONICA', 'POP', 'SYNTH'], + 'tag': ['ELECTRONIC', 'STRUGGLE'], + }, + 'playlist_count': 4, + }] + + def _real_extract(self, url): + album_id = self._match_id(url) + album_data = self._get_album_data(album_id, album_id) + + entries = [] + for track_id in traverse_obj(album_data, ('tracks', ..., 'id')): + if track := self._extract_track(album_data, track_id=track_id): + if track.get('entries'): + entries.extend(track['entries']) + else: + entries.append(track) + + subgenres = traverse_obj(album_data, ('album', 'subgenres', {str_or_none})) + if entries: + return merge_dicts(traverse_obj(album_data.get('album'), { + 'id': ('id', {lambda v: str(v)}), + 'album': ('title', {str_or_none}), + 'description': ('description', {lambda v: str_or_none(v) or None}), + 'artists': ('artist', {lambda v: [v] if v else None}), + 'genres': ('genres', {str_or_none}, {lambda v: join_nonempty(v, subgenres, delim=', ')}, + {lambda v: v.split(', ') if v else None}), + 'tag': ('keywords', {lambda v: v.split(', ') if v else None}), + }), { + 'description': traverse_obj(album_data, ('bio', 'description', {lambda v: str_or_none(v) or None})), + 'entries': entries, + '_type': 'playlist', + }) + else: + self.raise_no_formats('No formats were found') + + +class ExtremeMusicPIE(ExtremeMusicBaseIE): + IE_NAME = 'ExtremeMusic:playlist' + _VALID_URL = r'https?://(?:www\.)?extrememusic\.com/playlists/(?P[^?]+)' + _TESTS = [{ + 'url': 'https://www.extrememusic.com/playlists/Kf3fAppAKK2UpAUUp7KK1pBDBMrC62c_Kf8UKAAppUUKppK2UAp92K7Appp8xMx', + 'info_dict': { + 'id': 'Kf3fAppAKK2UpAUUp7KK1pBDBMrC62c_Kf8UKAAppUUKppK2UAp92K7Appp8xMx', + 'title': 'NICE', + 'thumbnail': 'https://d2oet5a29f64lj.cloudfront.net/img-data/w/2480/featureditem/square/thumbnail_PLAYLIST_Nice-square-(formerly ChristmasTraditional).jpg', + }, + 'playlist_mincount': 35, + }, { + 'url': 'https://www.extrememusic.com/playlists/fUKKU5KAfK61pAAKp4U4KpKUxsRk2ki_fU117KpUUAAUKAUfpA6UAfAKK8Ul5ji', + 'info_dict': { + 'id': 'fUKKU5KAfK61pAAKp4U4KpKUxsRk2ki_fU117KpUUAAUKAUfpA6UAfAKK8Ul5ji', + 'title': 'NEO CLASSICAL', + 'thumbnail': 'https://d2oet5a29f64lj.cloudfront.net/img-data/w/2480/featureditem/square/NeoClassical.jpg', + }, + 'playlist_mincount': 50, + }] + + def _real_extract(self, url): + playlist_id = self._match_id(url) + headers = self._set_request_headers(playlist_id, self._configuration_arg('country')) + + def playlist_query(playlist_id, offset, limit): + # playlist api: https://snapi.extrememusic.com/playlists?id={playlist_id}&range={offset}%2C{limit}' + return self._download_json( + 'https://snapi.extrememusic.com/playlists', playlist_id, + note=f'Downloading item {offset + 1}-{offset + limit}', query={ + 'id': playlist_id, + 'range': f'{offset},{limit}', + }, headers=headers) + + thumbnails, entries = [], [] + album_data, track_done, limit = {}, [], 50 + for i in itertools.count(): + playlist = playlist_query(playlist_id, i * limit, limit) + if len(playlist['playlist_items']) == 0: + break + else: + track_ids = traverse_obj(playlist, ('playlist_items', ..., 'track_id')) + for track_id in list(dict.fromkeys(track_ids)): + if track_id not in track_done: + album_id = traverse_obj(playlist, + ('tracks', lambda _, v: v['id'] == track_id, 'album_id', {int}), get_all=False) + if album_id not in album_data: + album_data[album_id] = self._get_album_data(album_id, track_id) + playlist['album'] = traverse_obj(album_data, (album_id, 'album', {dict})) + if track := self._extract_track(playlist, track_id=track_id): + if track.get('entries'): + entries.extend(track['entries']) + else: + entries.append(track) + track_done.append(track_id) + + if entries: + for image in traverse_obj(playlist['playlist'], ('images', 'square')): + thumbnails.append(traverse_obj(image, { + 'url': ('url', {url_or_none}), + 'width': ('width', {int_or_none}), + 'height': ('height', {int_or_none}), + })) + + return {k: v for k, v in { + 'id': playlist['playlist']['id'], + 'title': playlist['playlist']['title'], + 'thumbnail': traverse_obj(thumbnails, (0, 'url', {url_or_none})), + 'thumbnails': thumbnails, + 'uploader': playlist['playlist']['owner_name'], + 'entries': entries, + '_type': 'playlist', + }.items() if v} + else: + self.raise_no_formats('No formats were found') From 52f73ffc61175fa1c6798c000384267f7ee99c48 Mon Sep 17 00:00:00 2001 From: kclauhk <78251477+kclauhk@users.noreply.github.com> Date: Sat, 21 Sep 2024 15:23:56 +0800 Subject: [PATCH 2/8] use geo_bypass_country instead and modify _REQUEST_HEADERS --- yt_dlp/extractor/extrememusic.py | 40 +++++++++++++++++++++----------- 1 file changed, 26 insertions(+), 14 deletions(-) diff --git a/yt_dlp/extractor/extrememusic.py b/yt_dlp/extractor/extrememusic.py index 5523a520f..d49512ee6 100644 --- a/yt_dlp/extractor/extrememusic.py +++ b/yt_dlp/extractor/extrememusic.py @@ -21,27 +21,31 @@ class ExtremeMusicBaseIE(InfoExtractor): def _process_version_arg(self, arg): self._version_requested = arg - def _set_request_headers(self, video_id, country=None): + def _set_request_headers(self, url, video_id, country=None): if not self._REQUEST_HEADERS: - # the site serves different versions of the same playlist id according to ISO country code, - # so use user's own country code or user-provided country code (extractor argument "country") + # the site serves different versions of the same playlist id due to geo-restriction, + # so use user's own country code or geo_bypass_country code if not country: - country = [self._download_webpage('https://ipapi.co/country_code', video_id)] + country = self._download_webpage('https://ipapi.co/country_code', video_id) env = self._download_json('https://www.extrememusic.com/env', video_id) self._REQUEST_HEADERS = { + 'Accept': 'application/json', + 'Origin': 'https://www.extrememusic.com', + 'Referer': url, + 'Sec-Fetch-Mode': 'cors', 'X-API-Auth': env['token'], - 'X-Viewer-Country': country[0], + 'X-Site-Id': 4, + 'X-Viewer-Country': country.upper(), } return self._REQUEST_HEADERS - def _get_album_data(self, album_id, video_id): + def _get_album_data(self, album_id, video_id, fatal=True): self._process_version_arg(self._configuration_arg('ver') or self._configuration_arg('version')) - headers = self._set_request_headers(video_id) - album = self._download_json(f'{self._API_URL}/albums/{album_id}', video_id, - note='Downloading album data', headers=headers) + album = self._download_json(f'{self._API_URL}/albums/{album_id}', video_id, fatal=fatal, + note='Downloading album data', headers=self._REQUEST_HEADERS) if video_id == album_id: bio = self._download_json(f'{self._API_URL}/albums/{album_id}/bio', video_id, fatal=False, - note='Downloading album data', headers=headers) + note='Downloading album data', headers=self._REQUEST_HEADERS) return merge_dicts(album, bio or {}) else: return album @@ -206,6 +210,7 @@ class ExtremeMusicIE(ExtremeMusicBaseIE): def _real_extract(self, url): album_id, track_id, version_id = self._match_valid_url(url).group('album', 'id', 'ver') + self._set_request_headers(url, track_id or version_id, self.get_param('geo_bypass_country') or 'DE') album_data = self._get_album_data(album_id, track_id or version_id) if result := self._extract_track(album_data, track_id, version_id): return result @@ -238,6 +243,7 @@ class ExtremeMusicAIE(ExtremeMusicBaseIE): def _real_extract(self, url): album_id = self._match_id(url) + self._set_request_headers(url, album_id, self.get_param('geo_bypass_country') or 'DE') album_data = self._get_album_data(album_id, album_id) entries = [] @@ -248,8 +254,8 @@ def _real_extract(self, url): else: entries.append(track) - subgenres = traverse_obj(album_data, ('album', 'subgenres', {str_or_none})) if entries: + subgenres = traverse_obj(album_data, ('album', 'subgenres', {str_or_none})) return merge_dicts(traverse_obj(album_data.get('album'), { 'id': ('id', {lambda v: str(v)}), 'album': ('title', {str_or_none}), @@ -277,7 +283,8 @@ class ExtremeMusicPIE(ExtremeMusicBaseIE): 'title': 'NICE', 'thumbnail': 'https://d2oet5a29f64lj.cloudfront.net/img-data/w/2480/featureditem/square/thumbnail_PLAYLIST_Nice-square-(formerly ChristmasTraditional).jpg', }, - 'playlist_mincount': 35, + 'playlist_mincount': 29, + 'expected_warnings': ['This playlist has geo-restricted items. Try using --xff to specify a different country code.'], }, { 'url': 'https://www.extrememusic.com/playlists/fUKKU5KAfK61pAAKp4U4KpKUxsRk2ki_fU117KpUUAAUKAUfpA6UAfAKK8Ul5ji', 'info_dict': { @@ -290,7 +297,7 @@ class ExtremeMusicPIE(ExtremeMusicBaseIE): def _real_extract(self, url): playlist_id = self._match_id(url) - headers = self._set_request_headers(playlist_id, self._configuration_arg('country')) + headers = self._set_request_headers(url, playlist_id, self.get_param('geo_bypass_country')) def playlist_query(playlist_id, offset, limit): # playlist api: https://snapi.extrememusic.com/playlists?id={playlist_id}&range={offset}%2C{limit}' @@ -314,7 +321,7 @@ def playlist_query(playlist_id, offset, limit): album_id = traverse_obj(playlist, ('tracks', lambda _, v: v['id'] == track_id, 'album_id', {int}), get_all=False) if album_id not in album_data: - album_data[album_id] = self._get_album_data(album_id, track_id) + album_data[album_id] = self._get_album_data(album_id, track_id, fatal=False) playlist['album'] = traverse_obj(album_data, (album_id, 'album', {dict})) if track := self._extract_track(playlist, track_id=track_id): if track.get('entries'): @@ -322,8 +329,13 @@ def playlist_query(playlist_id, offset, limit): else: entries.append(track) track_done.append(track_id) + if len(entries) >= playlist['playlist']['playlist_items_count']: + break if entries: + if len(entries) < playlist['playlist']['playlist_items_count']: + self.report_warning('This playlist has geo-restricted items. Try using --xff to specify a different country code.') + for image in traverse_obj(playlist['playlist'], ('images', 'square')): thumbnails.append(traverse_obj(image, { 'url': ('url', {url_or_none}), From 05b4719f6cb49a13d31f64aab12ac7d292f23b10 Mon Sep 17 00:00:00 2001 From: kclauhk <78251477+kclauhk@users.noreply.github.com> Date: Sat, 28 Sep 2024 17:00:22 +0800 Subject: [PATCH 3/8] refactor - avoid repeatedly calling `self._configuration_arg` - avoid unnecessary looping when 'ver' argument is not provided - add/change error messages --- yt_dlp/extractor/extrememusic.py | 188 +++++++++++++++---------------- 1 file changed, 93 insertions(+), 95 deletions(-) diff --git a/yt_dlp/extractor/extrememusic.py b/yt_dlp/extractor/extrememusic.py index d49512ee6..aed6545a5 100644 --- a/yt_dlp/extractor/extrememusic.py +++ b/yt_dlp/extractor/extrememusic.py @@ -17,113 +17,111 @@ class ExtremeMusicBaseIE(InfoExtractor): _API_URL = 'https://snapi.extrememusic.com' _REQUEST_HEADERS = None + _REQUIRE_VERSION = [] - def _process_version_arg(self, arg): - self._version_requested = arg - - def _set_request_headers(self, url, video_id, country=None): - if not self._REQUEST_HEADERS: - # the site serves different versions of the same playlist id due to geo-restriction, - # so use user's own country code or geo_bypass_country code - if not country: - country = self._download_webpage('https://ipapi.co/country_code', video_id) - env = self._download_json('https://www.extrememusic.com/env', video_id) - self._REQUEST_HEADERS = { - 'Accept': 'application/json', - 'Origin': 'https://www.extrememusic.com', - 'Referer': url, - 'Sec-Fetch-Mode': 'cors', - 'X-API-Auth': env['token'], - 'X-Site-Id': 4, - 'X-Viewer-Country': country.upper(), - } - return self._REQUEST_HEADERS + def _initialize(self, url, video_id, country=None): + self._REQUIRE_VERSION = self._configuration_arg('ver') or self._configuration_arg('version') + # This site serves different versions of the same playlist id due to geo-restriction + # use user's own country code if no code (geo_bypass_country or pre-defined country code) is provided + if not country: + country = self._download_webpage('https://ipapi.co/country_code', video_id) + env = self._download_json('https://www.extrememusic.com/env', video_id) + self._REQUEST_HEADERS = { + 'Accept': 'application/json', + 'Origin': 'https://www.extrememusic.com', + 'Referer': url, + 'Sec-Fetch-Mode': 'cors', + 'X-API-Auth': env['token'], + 'X-Site-Id': 4, + 'X-Viewer-Country': country.upper(), + } def _get_album_data(self, album_id, video_id, fatal=True): - self._process_version_arg(self._configuration_arg('ver') or self._configuration_arg('version')) album = self._download_json(f'{self._API_URL}/albums/{album_id}', video_id, fatal=fatal, - note='Downloading album data', headers=self._REQUEST_HEADERS) + note='Downloading album data', errnote='Unable to download album data', + headers=self._REQUEST_HEADERS) or {} if video_id == album_id: bio = self._download_json(f'{self._API_URL}/albums/{album_id}/bio', video_id, fatal=False, - note='Downloading album data', headers=self._REQUEST_HEADERS) - return merge_dicts(album, bio or {}) + note='Downloading album data', errnote='Unable to download album data', + headers=self._REQUEST_HEADERS) or {} + return merge_dicts(album, bio) else: return album def _extract_track(self, album_data, track_id=None, version_id=None): - album_data = album_data or {} if 'tracks' in album_data and 'track_sounds' in album_data: if not track_id and version_id: track_id = traverse_obj(album_data['track_sounds'], (lambda _, v: v['id'] == int(version_id), 'track_id', {int}), get_all=False) - track = traverse_obj(album_data['tracks'], - (lambda _, v: v['id'] == int(track_id), {dict}), get_all=False) - info = {**traverse_obj(track, { - 'track': ('title', {str}), - 'track_number': ('sort_order', {lambda v: v + 1}, {int}), - 'track_id': ('track_no', {str}), - 'description': ('description', {lambda v: str_or_none(v) or None}), - 'artists': ('artists', {lambda v: v or traverse_obj(album_data, ('album', 'artist'))}, - {lambda v: (v if isinstance(v, list) else [v]) if v else None}), - 'composers': ('composers', ..., 'name'), - 'genres': (('genre', 'subgenre'), ..., 'label'), - 'tag': ('keywords', ..., 'label'), - 'album': ('album_title', {lambda v: str_or_none(v) or None}), - }), **traverse_obj(album_data, { - 'album_artists': ('album', 'artist', {lambda v: [v] if v else None}), - 'upload_date': ('album', 'created', {unified_strdate}), - })} - entries, thumbnails = [], [] - for image in traverse_obj(track, ('images', 'default')): - thumbnails.append(traverse_obj(image, { - 'url': ('url', {url_or_none}), - 'width': ('width', {int_or_none}), - 'height': ('height', {int_or_none}), - })) - for idx, sound_id in enumerate([version_id] if version_id else track['track_sound_ids']): - if sound := traverse_obj(album_data['track_sounds'], - (lambda _, v: v['id'] == int(sound_id) and v['track_id'] == int(track_id), - {dict}), get_all=False): - if (version_id - or (not version_id and ((not self._version_requested and idx == 0) - or 'all' in self._version_requested - or sound['version_type'].lower() in self._version_requested))): - formats = [] - for audio_url in traverse_obj(sound, ('assets', 'audio', ('preview_url', - 'preview_url_hls'))): - if determine_ext(audio_url) == 'm3u8': - m3u8_url = re.sub(r'\.m3u8\?.*', '/HLS/128_v4.m3u8', audio_url) - for f in self._extract_m3u8_formats(m3u8_url, sound_id, 'mpeg', fatal=False): + if track := traverse_obj(album_data['tracks'], + (lambda _, v: v['id'] == int(track_id), {dict}), get_all=False): + info = {**traverse_obj(track, { + 'track': ('title', {str}), + 'track_number': ('sort_order', {lambda v: v + 1}, {int}), + 'track_id': ('track_no', {str}), + 'description': ('description', {lambda v: str_or_none(v) or None}), + 'artists': ('artists', {lambda v: v or traverse_obj(album_data, ('album', 'artist'))}, + {lambda v: (v if isinstance(v, list) else [v]) if v else None}), + 'composers': ('composers', ..., 'name'), + 'genres': (('genre', 'subgenre'), ..., 'label'), + 'tag': ('keywords', ..., 'label'), + 'album': ('album_title', {lambda v: str_or_none(v) or None}), + }), **traverse_obj(album_data, { + 'album_artists': ('album', 'artist', {lambda v: [v] if v else None}), + 'upload_date': ('album', 'created', {unified_strdate}), + })} + entries, thumbnails = [], [] + for image in traverse_obj(track, ('images', 'default')): + thumbnails.append(traverse_obj(image, { + 'url': ('url', {url_or_none}), + 'width': ('width', {int_or_none}), + 'height': ('height', {int_or_none}), + })) + if not self._REQUIRE_VERSION: + version_id = version_id or traverse_obj(track, 'default_track_sound_id', ('track_sound_ids', 0)) + for sound_id in [version_id] if version_id else track['track_sound_ids']: + if sound := traverse_obj(album_data['track_sounds'], + (lambda _, v: v['id'] == int(sound_id) and v['track_id'] == int(track_id), + {dict}), get_all=False): + if version_id or any(x in self._REQUIRE_VERSION for x in ['all', sound['version_type'].lower()]): + formats = [] + for audio_url in traverse_obj(sound, ('assets', 'audio', ('preview_url', + 'preview_url_hls'))): + if determine_ext(audio_url) == 'm3u8': + m3u8_url = re.sub(r'\.m3u8\?.*', '/HLS/128_v4.m3u8', audio_url) + for f in self._extract_m3u8_formats(m3u8_url, sound_id, 'mpeg', fatal=False): + formats.append({ + **f, + 'vcodec': 'none', + 'perference': -2, + }) + else: formats.append({ - **f, + 'url': audio_url, 'vcodec': 'none', - 'perference': -2, }) - else: - formats.append({ - 'url': audio_url, - 'vcodec': 'none', - }) - entries.append({ - 'id': str(sound_id), - 'title': join_nonempty('title', 'version_type', from_dict=sound, delim=' - '), - 'alt_title': sound['version_type'], - **info, - 'thumbnails': thumbnails, - 'duration': sound.get('duration'), - 'formats': formats, - 'webpage_url': f"https://www.extrememusic.com/albums/{track['album_id']}?item={track_id}&ver={sound_id}", - }) + entries.append({ + 'id': str(sound_id), + 'title': join_nonempty('title', 'version_type', from_dict=sound, delim=' - '), + 'alt_title': sound['version_type'], + **info, + 'thumbnails': thumbnails, + 'duration': sound.get('duration'), + 'formats': formats, + 'webpage_url': f"https://www.extrememusic.com/albums/{track['album_id']}?item={track_id}&ver={sound_id}", + }) - if len(entries) > 1: - return { - 'id': track_id, - **info, - 'entries': entries, - '_type': 'playlist', - } - elif len(entries) == 1: - return entries[0] + if len(entries) > 1: + return { + 'id': track_id, + **info, + 'entries': entries, + '_type': 'playlist', + } + elif len(entries) == 1: + return entries[0] + else: + self.raise_no_formats('Track data not found', video_id=track_id) return [] @@ -210,7 +208,7 @@ class ExtremeMusicIE(ExtremeMusicBaseIE): def _real_extract(self, url): album_id, track_id, version_id = self._match_valid_url(url).group('album', 'id', 'ver') - self._set_request_headers(url, track_id or version_id, self.get_param('geo_bypass_country') or 'DE') + self._initialize(url, track_id or version_id, self.get_param('geo_bypass_country') or 'DE') album_data = self._get_album_data(album_id, track_id or version_id) if result := self._extract_track(album_data, track_id, version_id): return result @@ -243,7 +241,7 @@ class ExtremeMusicAIE(ExtremeMusicBaseIE): def _real_extract(self, url): album_id = self._match_id(url) - self._set_request_headers(url, album_id, self.get_param('geo_bypass_country') or 'DE') + self._initialize(url, album_id, self.get_param('geo_bypass_country') or 'DE') album_data = self._get_album_data(album_id, album_id) entries = [] @@ -297,7 +295,7 @@ class ExtremeMusicPIE(ExtremeMusicBaseIE): def _real_extract(self, url): playlist_id = self._match_id(url) - headers = self._set_request_headers(url, playlist_id, self.get_param('geo_bypass_country')) + self._initialize(url, playlist_id, self.get_param('geo_bypass_country')) def playlist_query(playlist_id, offset, limit): # playlist api: https://snapi.extrememusic.com/playlists?id={playlist_id}&range={offset}%2C{limit}' @@ -306,7 +304,7 @@ def playlist_query(playlist_id, offset, limit): note=f'Downloading item {offset + 1}-{offset + limit}', query={ 'id': playlist_id, 'range': f'{offset},{limit}', - }, headers=headers) + }, headers=self._REQUEST_HEADERS) thumbnails, entries = [], [] album_data, track_done, limit = {}, [], 50 @@ -329,11 +327,11 @@ def playlist_query(playlist_id, offset, limit): else: entries.append(track) track_done.append(track_id) - if len(entries) >= playlist['playlist']['playlist_items_count']: + if len(track_done) >= playlist['playlist']['playlist_items_count']: break if entries: - if len(entries) < playlist['playlist']['playlist_items_count']: + if len(track_done) < playlist['playlist']['playlist_items_count']: self.report_warning('This playlist has geo-restricted items. Try using --xff to specify a different country code.') for image in traverse_obj(playlist['playlist'], ('images', 'square')): From 0f49100783e7f189bffd965dda28d2d057164592 Mon Sep 17 00:00:00 2001 From: kclauhk <78251477+kclauhk@users.noreply.github.com> Date: Sat, 28 Sep 2024 20:39:10 +0800 Subject: [PATCH 4/8] correct file ext --- yt_dlp/extractor/extrememusic.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/yt_dlp/extractor/extrememusic.py b/yt_dlp/extractor/extrememusic.py index aed6545a5..e75706139 100644 --- a/yt_dlp/extractor/extrememusic.py +++ b/yt_dlp/extractor/extrememusic.py @@ -89,7 +89,7 @@ def _extract_track(self, album_data, track_id=None, version_id=None): 'preview_url_hls'))): if determine_ext(audio_url) == 'm3u8': m3u8_url = re.sub(r'\.m3u8\?.*', '/HLS/128_v4.m3u8', audio_url) - for f in self._extract_m3u8_formats(m3u8_url, sound_id, 'mpeg', fatal=False): + for f in self._extract_m3u8_formats(m3u8_url, sound_id, 'm4a', fatal=False): formats.append({ **f, 'vcodec': 'none', @@ -208,8 +208,8 @@ class ExtremeMusicIE(ExtremeMusicBaseIE): def _real_extract(self, url): album_id, track_id, version_id = self._match_valid_url(url).group('album', 'id', 'ver') - self._initialize(url, track_id or version_id, self.get_param('geo_bypass_country') or 'DE') - album_data = self._get_album_data(album_id, track_id or version_id) + self._initialize(url, version_id or track_id, self.get_param('geo_bypass_country') or 'DE') + album_data = self._get_album_data(album_id, version_id or track_id) if result := self._extract_track(album_data, track_id, version_id): return result else: From 245ca4e51581321f7cd2e51c45f87bb110fbd37b Mon Sep 17 00:00:00 2001 From: kclauhk Date: Thu, 31 Oct 2024 02:42:58 +0800 Subject: [PATCH 5/8] use one ie_key only --- yt_dlp/extractor/extrememusic.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/yt_dlp/extractor/extrememusic.py b/yt_dlp/extractor/extrememusic.py index e75706139..7959737b8 100644 --- a/yt_dlp/extractor/extrememusic.py +++ b/yt_dlp/extractor/extrememusic.py @@ -20,7 +20,8 @@ class ExtremeMusicBaseIE(InfoExtractor): _REQUIRE_VERSION = [] def _initialize(self, url, video_id, country=None): - self._REQUIRE_VERSION = self._configuration_arg('ver') or self._configuration_arg('version') + self._REQUIRE_VERSION = (self._configuration_arg('ver', ie_key='extrememusic') + or self._configuration_arg('version', ie_key='extrememusic')) # This site serves different versions of the same playlist id due to geo-restriction # use user's own country code if no code (geo_bypass_country or pre-defined country code) is provided if not country: From 2fd16fdc6ec1550e7eea78861ec227618a09f5af Mon Sep 17 00:00:00 2001 From: kclauhk <78251477+kclauhk@users.noreply.github.com> Date: Thu, 31 Oct 2024 12:12:25 +0800 Subject: [PATCH 6/8] ver contains instead of exact match --- yt_dlp/extractor/extrememusic.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/yt_dlp/extractor/extrememusic.py b/yt_dlp/extractor/extrememusic.py index 7959737b8..94f8cf30f 100644 --- a/yt_dlp/extractor/extrememusic.py +++ b/yt_dlp/extractor/extrememusic.py @@ -26,6 +26,7 @@ def _initialize(self, url, video_id, country=None): # use user's own country code if no code (geo_bypass_country or pre-defined country code) is provided if not country: country = self._download_webpage('https://ipapi.co/country_code', video_id) + self.to_screen(f'Set country code to {country}') env = self._download_json('https://www.extrememusic.com/env', video_id) self._REQUEST_HEADERS = { 'Accept': 'application/json', @@ -84,7 +85,9 @@ def _extract_track(self, album_data, track_id=None, version_id=None): if sound := traverse_obj(album_data['track_sounds'], (lambda _, v: v['id'] == int(sound_id) and v['track_id'] == int(track_id), {dict}), get_all=False): - if version_id or any(x in self._REQUIRE_VERSION for x in ['all', sound['version_type'].lower()]): + if (version_id + or 'all' in self._REQUIRE_VERSION + or any(x in sound['version_type'].lower() for x in self._REQUIRE_VERSION)): formats = [] for audio_url in traverse_obj(sound, ('assets', 'audio', ('preview_url', 'preview_url_hls'))): @@ -283,7 +286,7 @@ class ExtremeMusicPIE(ExtremeMusicBaseIE): 'thumbnail': 'https://d2oet5a29f64lj.cloudfront.net/img-data/w/2480/featureditem/square/thumbnail_PLAYLIST_Nice-square-(formerly ChristmasTraditional).jpg', }, 'playlist_mincount': 29, - 'expected_warnings': ['This playlist has geo-restricted items. Try using --xff to specify a different country code.'], + 'expected_warnings': ['This playlist has geo-restricted items. Try using --xff to specify a different country code, e.g. DE'], }, { 'url': 'https://www.extrememusic.com/playlists/fUKKU5KAfK61pAAKp4U4KpKUxsRk2ki_fU117KpUUAAUKAUfpA6UAfAKK8Ul5ji', 'info_dict': { @@ -333,7 +336,7 @@ def playlist_query(playlist_id, offset, limit): if entries: if len(track_done) < playlist['playlist']['playlist_items_count']: - self.report_warning('This playlist has geo-restricted items. Try using --xff to specify a different country code.') + self.report_warning('This playlist has geo-restricted items. Try using --xff to specify a different country code, e.g. DE') for image in traverse_obj(playlist['playlist'], ('images', 'square')): thumbnails.append(traverse_obj(image, { From 48416c37184feabaf96e4df121413fa87bc2eca2 Mon Sep 17 00:00:00 2001 From: kclauhk <78251477+kclauhk@users.noreply.github.com> Date: Thu, 31 Oct 2024 12:25:11 +0800 Subject: [PATCH 7/8] remove trailing whitespace --- yt_dlp/extractor/extrememusic.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/yt_dlp/extractor/extrememusic.py b/yt_dlp/extractor/extrememusic.py index 94f8cf30f..de31c496f 100644 --- a/yt_dlp/extractor/extrememusic.py +++ b/yt_dlp/extractor/extrememusic.py @@ -86,7 +86,7 @@ def _extract_track(self, album_data, track_id=None, version_id=None): (lambda _, v: v['id'] == int(sound_id) and v['track_id'] == int(track_id), {dict}), get_all=False): if (version_id - or 'all' in self._REQUIRE_VERSION + or 'all' in self._REQUIRE_VERSION or any(x in sound['version_type'].lower() for x in self._REQUIRE_VERSION)): formats = [] for audio_url in traverse_obj(sound, ('assets', 'audio', ('preview_url', From adb26de9b4c8cfc33a2aa9c80d25ad93fc02fab5 Mon Sep 17 00:00:00 2001 From: kclauhk <78251477+kclauhk@users.noreply.github.com> Date: Wed, 18 Dec 2024 00:31:37 +0800 Subject: [PATCH 8/8] support lazy playlist & change geo restriction handling --- yt_dlp/extractor/extrememusic.py | 208 +++++++++++++++---------------- 1 file changed, 100 insertions(+), 108 deletions(-) diff --git a/yt_dlp/extractor/extrememusic.py b/yt_dlp/extractor/extrememusic.py index de31c496f..9394de1a7 100644 --- a/yt_dlp/extractor/extrememusic.py +++ b/yt_dlp/extractor/extrememusic.py @@ -22,11 +22,6 @@ class ExtremeMusicBaseIE(InfoExtractor): def _initialize(self, url, video_id, country=None): self._REQUIRE_VERSION = (self._configuration_arg('ver', ie_key='extrememusic') or self._configuration_arg('version', ie_key='extrememusic')) - # This site serves different versions of the same playlist id due to geo-restriction - # use user's own country code if no code (geo_bypass_country or pre-defined country code) is provided - if not country: - country = self._download_webpage('https://ipapi.co/country_code', video_id) - self.to_screen(f'Set country code to {country}') env = self._download_json('https://www.extrememusic.com/env', video_id) self._REQUEST_HEADERS = { 'Accept': 'application/json', @@ -53,11 +48,11 @@ def _get_album_data(self, album_id, video_id, fatal=True): def _extract_track(self, album_data, track_id=None, version_id=None): if 'tracks' in album_data and 'track_sounds' in album_data: if not track_id and version_id: - track_id = traverse_obj(album_data['track_sounds'], - (lambda _, v: v['id'] == int(version_id), 'track_id', {int}), get_all=False) - if track := traverse_obj(album_data['tracks'], - (lambda _, v: v['id'] == int(track_id), {dict}), get_all=False): - info = {**traverse_obj(track, { + track_id = traverse_obj(album_data, ( + 'track_sounds', lambda _, v: v['id'] == int(version_id), 'track_id', {int}), get_all=False) + if track := traverse_obj(album_data, ( + 'tracks', lambda _, v: v['id'] == int(track_id), {dict}), get_all=False): + track_info = {**traverse_obj(track, { 'track': ('title', {str}), 'track_number': ('sort_order', {lambda v: v + 1}, {int}), 'track_id': ('track_no', {str}), @@ -68,10 +63,10 @@ def _extract_track(self, album_data, track_id=None, version_id=None): 'genres': (('genre', 'subgenre'), ..., 'label'), 'tag': ('keywords', ..., 'label'), 'album': ('album_title', {lambda v: str_or_none(v) or None}), - }), **traverse_obj(album_data, { - 'album_artists': ('album', 'artist', {lambda v: [v] if v else None}), - 'upload_date': ('album', 'created', {unified_strdate}), - })} + }), **traverse_obj(album_data, ('album', { + 'album_artists': ('artist', {lambda v: [v] if v else None}), + 'upload_date': ('created', {unified_strdate}), + }))} entries, thumbnails = [], [] for image in traverse_obj(track, ('images', 'default')): thumbnails.append(traverse_obj(image, { @@ -80,17 +75,18 @@ def _extract_track(self, album_data, track_id=None, version_id=None): 'height': ('height', {int_or_none}), })) if not self._REQUIRE_VERSION: - version_id = version_id or traverse_obj(track, 'default_track_sound_id', ('track_sound_ids', 0)) + version_id = (version_id + or traverse_obj(track, 'default_track_sound_id', ('track_sound_ids', 0))) for sound_id in [version_id] if version_id else track['track_sound_ids']: - if sound := traverse_obj(album_data['track_sounds'], - (lambda _, v: v['id'] == int(sound_id) and v['track_id'] == int(track_id), - {dict}), get_all=False): + if sound := traverse_obj(album_data, ( + 'track_sounds', lambda _, v: v['id'] == int(sound_id) and v['track_id'] == int(track_id), + {dict}), get_all=False): if (version_id or 'all' in self._REQUIRE_VERSION or any(x in sound['version_type'].lower() for x in self._REQUIRE_VERSION)): formats = [] - for audio_url in traverse_obj(sound, ('assets', 'audio', ('preview_url', - 'preview_url_hls'))): + for audio_url in traverse_obj( + sound, ('assets', 'audio', ('preview_url', 'preview_url_hls'))): if determine_ext(audio_url) == 'm3u8': m3u8_url = re.sub(r'\.m3u8\?.*', '/HLS/128_v4.m3u8', audio_url) for f in self._extract_m3u8_formats(m3u8_url, sound_id, 'm4a', fatal=False): @@ -108,25 +104,18 @@ def _extract_track(self, album_data, track_id=None, version_id=None): 'id': str(sound_id), 'title': join_nonempty('title', 'version_type', from_dict=sound, delim=' - '), 'alt_title': sound['version_type'], - **info, + **track_info, 'thumbnails': thumbnails, 'duration': sound.get('duration'), 'formats': formats, 'webpage_url': f"https://www.extrememusic.com/albums/{track['album_id']}?item={track_id}&ver={sound_id}", }) - - if len(entries) > 1: - return { - 'id': track_id, - **info, - 'entries': entries, - '_type': 'playlist', - } - elif len(entries) == 1: - return entries[0] - else: - self.raise_no_formats('Track data not found', video_id=track_id) - return [] + return [entries, { + 'id': track_id, + 'title': track_info['track'], + **track_info, + }] + self.raise_no_formats('Track data not found', video_id=track_id) class ExtremeMusicIE(ExtremeMusicBaseIE): @@ -214,10 +203,12 @@ def _real_extract(self, url): album_id, track_id, version_id = self._match_valid_url(url).group('album', 'id', 'ver') self._initialize(url, version_id or track_id, self.get_param('geo_bypass_country') or 'DE') album_data = self._get_album_data(album_id, version_id or track_id) - if result := self._extract_track(album_data, track_id, version_id): - return result - else: - self.raise_no_formats('No formats were found') + if track := self._extract_track(album_data, track_id, version_id): + if len(track[0]) > 1: + return self.playlist_result(track[0], **track[1]) + elif len(track[0]) == 1: + return track[0][0] + self.raise_no_formats('No formats were found') class ExtremeMusicAIE(ExtremeMusicBaseIE): @@ -247,32 +238,26 @@ def _real_extract(self, url): album_id = self._match_id(url) self._initialize(url, album_id, self.get_param('geo_bypass_country') or 'DE') album_data = self._get_album_data(album_id, album_id) + subgenres = traverse_obj(album_data, ('album', 'subgenres', {str_or_none})) + album_info = merge_dicts(traverse_obj(album_data, ('album', { + 'id': ('id', {lambda v: str(v)}), + 'album': ('title', {str_or_none}), + 'description': ('description', {lambda v: str_or_none(v) or None}), + 'artists': ('artist', {lambda v: [v] if v else None}), + 'genres': ('genres', {str_or_none}, + {lambda v: join_nonempty(v, subgenres, delim=', ')}, + {lambda v: v.split(', ') if v else None}), + 'tag': ('keywords', {lambda v: v.split(', ') if v else None}), + })), { + 'description': traverse_obj( + album_data, ('bio', 'description', {lambda v: str_or_none(v) or None})), + }) - entries = [] - for track_id in traverse_obj(album_data, ('tracks', ..., 'id')): - if track := self._extract_track(album_data, track_id=track_id): - if track.get('entries'): - entries.extend(track['entries']) - else: - entries.append(track) - - if entries: - subgenres = traverse_obj(album_data, ('album', 'subgenres', {str_or_none})) - return merge_dicts(traverse_obj(album_data.get('album'), { - 'id': ('id', {lambda v: str(v)}), - 'album': ('title', {str_or_none}), - 'description': ('description', {lambda v: str_or_none(v) or None}), - 'artists': ('artist', {lambda v: [v] if v else None}), - 'genres': ('genres', {str_or_none}, {lambda v: join_nonempty(v, subgenres, delim=', ')}, - {lambda v: v.split(', ') if v else None}), - 'tag': ('keywords', {lambda v: v.split(', ') if v else None}), - }), { - 'description': traverse_obj(album_data, ('bio', 'description', {lambda v: str_or_none(v) or None})), - 'entries': entries, - '_type': 'playlist', - }) - else: - self.raise_no_formats('No formats were found') + return self.playlist_result( + (entry for sounds in (track[0] for track in (self._extract_track(album_data, track_id=track_id) + for track_id in traverse_obj(album_data, ('tracks', ..., 'id')))) + for entry in sounds), + **album_info) class ExtremeMusicPIE(ExtremeMusicBaseIE): @@ -285,8 +270,7 @@ class ExtremeMusicPIE(ExtremeMusicBaseIE): 'title': 'NICE', 'thumbnail': 'https://d2oet5a29f64lj.cloudfront.net/img-data/w/2480/featureditem/square/thumbnail_PLAYLIST_Nice-square-(formerly ChristmasTraditional).jpg', }, - 'playlist_mincount': 29, - 'expected_warnings': ['This playlist has geo-restricted items. Try using --xff to specify a different country code, e.g. DE'], + 'playlist_count': 40, }, { 'url': 'https://www.extrememusic.com/playlists/fUKKU5KAfK61pAAKp4U4KpKUxsRk2ki_fU117KpUUAAUKAUfpA6UAfAKK8Ul5ji', 'info_dict': { @@ -299,60 +283,68 @@ class ExtremeMusicPIE(ExtremeMusicBaseIE): def _real_extract(self, url): playlist_id = self._match_id(url) - self._initialize(url, playlist_id, self.get_param('geo_bypass_country')) + self._initialize(url, playlist_id, self.get_param('geo_bypass_country') or 'DE') - def playlist_query(playlist_id, offset, limit): + def playlist_query(playlist_id, offset, limit, note=None): # playlist api: https://snapi.extrememusic.com/playlists?id={playlist_id}&range={offset}%2C{limit}' return self._download_json( - 'https://snapi.extrememusic.com/playlists', playlist_id, - note=f'Downloading item {offset + 1}-{offset + limit}', query={ + f'{self._API_URL}/playlists', playlist_id, note=note, query={ 'id': playlist_id, - 'range': f'{offset},{limit}', + 'range': f'{offset + 1},{limit}', }, headers=self._REQUEST_HEADERS) - thumbnails, entries = [], [] - album_data, track_done, limit = {}, [], 50 - for i in itertools.count(): - playlist = playlist_query(playlist_id, i * limit, limit) - if len(playlist['playlist_items']) == 0: - break - else: - track_ids = traverse_obj(playlist, ('playlist_items', ..., 'track_id')) - for track_id in list(dict.fromkeys(track_ids)): - if track_id not in track_done: - album_id = traverse_obj(playlist, - ('tracks', lambda _, v: v['id'] == track_id, 'album_id', {int}), get_all=False) - if album_id not in album_data: - album_data[album_id] = self._get_album_data(album_id, track_id, fatal=False) - playlist['album'] = traverse_obj(album_data, (album_id, 'album', {dict})) - if track := self._extract_track(playlist, track_id=track_id): - if track.get('entries'): - entries.extend(track['entries']) - else: - entries.append(track) - track_done.append(track_id) - if len(track_done) >= playlist['playlist']['playlist_items_count']: - break + def extract_playlist(playlist_id): + albums, tracks_done, items_count, limit = {}, [], 0, 25 + for i in itertools.count(): + try: + # try to tackle geo restriction by shortening playlist id + playlist = playlist_query(playlist_id.split('_')[0], i * limit, limit, + note=f'Downloading item {i * limit + 1}-{i * limit + limit}') + except Exception: + playlist = playlist_query(playlist_id, i * limit, limit, + note=f'Downloading item {i * limit + 1}-{i * limit + limit}') + if playlist_items_count := traverse_obj(playlist, ('playlist', 'playlist_items_count')): + if len(playlist.get('tracks', [])) == 0: + if items_count < playlist_items_count: + self.report_warning('This playlist has geo-restricted items. Try using --xff to specify a different country code') + break + else: + track_ids = traverse_obj(playlist, ('tracks', ..., 'id')) + items_count += len(track_ids) + for track_id in list(dict.fromkeys(track_ids)): + if track_id not in tracks_done: + album_id = traverse_obj( + playlist, ('tracks', lambda _, v: v['id'] == track_id, + 'album_id', {int}), get_all=False) + if album_id not in albums: + albums[album_id] = self._get_album_data(album_id, track_id, fatal=False) + playlist['album'] = traverse_obj(albums, (album_id, 'album', {dict})) + if track := self._extract_track(playlist, track_id=track_id): + if len(track[0]) > 1: + yield from track[0] + elif len(track[0]) == 1: + yield track[0][0] + tracks_done.append(track_id) + if items_count >= playlist_items_count: + break + else: + return [] - if entries: - if len(track_done) < playlist['playlist']['playlist_items_count']: - self.report_warning('This playlist has geo-restricted items. Try using --xff to specify a different country code, e.g. DE') - - for image in traverse_obj(playlist['playlist'], ('images', 'square')): + playlist_info, thumbnails = {}, [] + if playlist := playlist_query(playlist_id, 0, 1, note='Downloading JSON metadata'): + for image in traverse_obj(playlist, ('playlist', 'images', 'square')): thumbnails.append(traverse_obj(image, { 'url': ('url', {url_or_none}), 'width': ('width', {int_or_none}), 'height': ('height', {int_or_none}), })) - - return {k: v for k, v in { - 'id': playlist['playlist']['id'], - 'title': playlist['playlist']['title'], + playlist_info = { + **traverse_obj(playlist, ('playlist', { + 'id': ('id', {str}), + 'title': ('title', {str_or_none}), + 'uploader': ('owner_name', {str_or_none}), + })), 'thumbnail': traverse_obj(thumbnails, (0, 'url', {url_or_none})), 'thumbnails': thumbnails, - 'uploader': playlist['playlist']['owner_name'], - 'entries': entries, - '_type': 'playlist', - }.items() if v} - else: - self.raise_no_formats('No formats were found') + } + return self.playlist_result(extract_playlist(playlist_id), **playlist_info)