From aaf7405ba3a45b32c59f160426efc9b561af035a Mon Sep 17 00:00:00 2001 From: bashonly <88596187+bashonly@users.noreply.github.com> Date: Tue, 21 Jul 2026 18:09:21 -0500 Subject: [PATCH] [ie/apple] Rework extractors (#17277) * Add API fallback for ApplePodcastsIE * Fix AppleConnectIE token extraction * Refactor and add AppleBaseIE Closes #17266 Authored by: bashonly --- yt_dlp/extractor/appleconnect.py | 54 ++++----------- yt_dlp/extractor/applepodcasts.py | 107 +++++++++++++++++++++++++++--- 2 files changed, 110 insertions(+), 51 deletions(-) diff --git a/yt_dlp/extractor/appleconnect.py b/yt_dlp/extractor/appleconnect.py index 786719447a..d275a3bdb1 100644 --- a/yt_dlp/extractor/appleconnect.py +++ b/yt_dlp/extractor/appleconnect.py @@ -1,39 +1,22 @@ -import time - -from .common import InfoExtractor +from .applepodcasts import AppleBaseIE from ..utils import ( - ExtractorError, - extract_attributes, float_or_none, - jwt_decode_hs256, - jwt_encode, parse_resolution, qualities, unified_strdate, update_url, url_or_none, - urljoin, ) from ..utils.traversal import ( - find_element, require, traverse_obj, ) -class AppleConnectIE(InfoExtractor): +class AppleConnectIE(AppleBaseIE): IE_NAME = 'apple:music:connect' IE_DESC = 'Apple Music Connect' - _BASE_URL = 'https://music.apple.com' - _QUALITIES = { - 'provisionalUploadVideo': None, - 'sdVideo': 480, - 'sdVideoWithPlusAudio': 480, - 'sd480pVideo': 480, - '720pHdVideo': 720, - '1080pHdVideo': 1080, - } _VALID_URL = r'https?://music\.apple\.com/[\w-]+/post/(?P\d+)' _TESTS = [{ 'url': 'https://music.apple.com/us/post/1018290019', @@ -59,29 +42,16 @@ class AppleConnectIE(InfoExtractor): }, }] - _jwt = None - - @staticmethod - def _jwt_is_expired(token): - return jwt_decode_hs256(token)['exp'] - time.time() < 120 - - def _get_token(self, webpage, video_id): - if self._jwt and not self._jwt_is_expired(self._jwt): - return self._jwt - - js_url = traverse_obj(webpage, ( - {find_element(tag='script', attr='crossorigin', value='', html=True)}, - {extract_attributes}, 'src', {urljoin(self._BASE_URL)}, {require('JS URL')})) - js = self._download_webpage( - js_url, video_id, 'Downloading token JS', 'Unable to download token JS') - - header = jwt_encode({}, '', headers={'alg': 'ES256', 'kid': 'WebPlayKid'}).split('.')[0] - self._jwt = self._search_regex( - fr'(["\'])(?P{header}(?:\.[\w-]+){{2}})\1', js, 'JSON Web Token', group='jwt') - if self._jwt_is_expired(self._jwt): - raise ExtractorError('The fetched token is already expired') - - return self._jwt + _BASE_URL = 'https://music.apple.com' + _JWT_KEY_ID = 'WebPlayKid' + _QUALITIES = { + 'provisionalUploadVideo': None, + 'sdVideo': 480, + 'sdVideoWithPlusAudio': 480, + 'sd480pVideo': 480, + '720pHdVideo': 720, + '1080pHdVideo': 1080, + } def _real_extract(self, url): video_id = self._match_id(url) diff --git a/yt_dlp/extractor/applepodcasts.py b/yt_dlp/extractor/applepodcasts.py index eb30fb2319..d4535ca836 100644 --- a/yt_dlp/extractor/applepodcasts.py +++ b/yt_dlp/extractor/applepodcasts.py @@ -1,15 +1,55 @@ +import time + from .common import InfoExtractor from ..utils import ( + ExtractorError, clean_html, clean_podcast_url, int_or_none, + jwt_decode_hs256, + jwt_encode, parse_iso8601, + try_call, + update_url, + url_or_none, + urljoin, ) from ..utils.traversal import traverse_obj -class ApplePodcastsIE(InfoExtractor): - _VALID_URL = r'https?://podcasts\.apple\.com/(?:[^/]+/)?podcast(?:/[^/]+){1,2}.*?\bi=(?P\d+)' +class AppleBaseIE(InfoExtractor): + """Subclasses must set _BASE_URL and _JWT_KEY_ID""" + + _jwt_cache = {} + + @staticmethod + def _jwt_is_expired(token): + return jwt_decode_hs256(token)['exp'] - time.time() < 120 + + def _get_token(self, webpage, episode_id): + if self._jwt_cache.get(self._BASE_URL) and not self._jwt_is_expired(self._jwt_cache[self._BASE_URL]): + return self._jwt + + js_path = self._search_regex( + r'