From a8769f672b58135dc681b87dd0bdd8073c847bf0 Mon Sep 17 00:00:00 2001 From: grqx_wsl <173253225+grqx@users.noreply.github.com> Date: Sun, 13 Oct 2024 12:46:03 +1300 Subject: [PATCH] [ie/boomplay] add extractors --- yt_dlp/extractor/_extractors.py | 7 + yt_dlp/extractor/boomplay.py | 283 ++++++++++++++++++++++++++++++++ 2 files changed, 290 insertions(+) create mode 100644 yt_dlp/extractor/boomplay.py diff --git a/yt_dlp/extractor/_extractors.py b/yt_dlp/extractor/_extractors.py index 4b1f4c316..1abca1ed9 100644 --- a/yt_dlp/extractor/_extractors.py +++ b/yt_dlp/extractor/_extractors.py @@ -280,6 +280,13 @@ from .blogger import BloggerIE from .bloomberg import BloombergIE from .bokecc import BokeCCIE from .bongacams import BongaCamsIE +from .boomplay import ( + BoomPlayEpisodeIE, + BoomPlayMusicIE, + BoomPlayPlaylistIE, + BoomPlayPodcastIE, + BoomPlayVideoIE, +) from .boosty import BoostyIE from .bostonglobe import BostonGlobeIE from .box import BoxIE diff --git a/yt_dlp/extractor/boomplay.py b/yt_dlp/extractor/boomplay.py new file mode 100644 index 000000000..dba8a1c9c --- /dev/null +++ b/yt_dlp/extractor/boomplay.py @@ -0,0 +1,283 @@ +import base64 +import functools +import json +import re + +from .common import InfoExtractor +from ..aes import aes_cbc_decrypt_bytes, aes_cbc_encrypt_bytes, unpad_pkcs7 +from ..utils import ( + ExtractorError, + clean_html, + get_element_by_attribute, + get_element_by_class, + get_elements_by_attribute, + int_or_none, + merge_dicts, + parse_duration, + strip_or_none, + unified_strdate, + url_or_none, + urlencode_postdata, +) +from ..utils.traversal import traverse_obj + + +class BoomPlayBaseIE(InfoExtractor): + # Calculated from const values, see lhx.AESUtils.encrypt, see public.js + # Note that the real key/iv differs from `lhx.AESUtils.key`/`lhx.AESUtils.iv` + _KEY = b'boomplayVr3xopAM' + _IV = b'boomplay8xIsKTn9' + + def _get_playurl(self, item_id, item_type): + resp = self._download_json( + 'https://www.boomplay.com/getResourceAddr', item_id, + note='Downloading play URL', errnote='Failed to download play URL', + data=urlencode_postdata({ + 'param': base64.b64encode(aes_cbc_encrypt_bytes(json.dumps({ + 'itemID': item_id, + 'itemType': item_type, + }).encode(), self._KEY, self._IV)).decode(), + }), headers={ + 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8', + }) + if not (source := resp.get('source')) and resp.get('code'): + raise ExtractorError(resp.get('desc') or 'Please solve the captcha') + return unpad_pkcs7( + aes_cbc_decrypt_bytes(base64.b64decode(source), self._KEY, self._IV)).decode() + + def _extract_formats(self, _id, item_type='MUSIC', **kwargs): + if url := url_or_none(self._get_playurl(_id, item_type)): + return [{ + 'format_id': '0', + 'vcodec': 'none' if item_type == 'MUSIC' else None, + 'url': url, + 'http_headers': { + 'Origin': 'https://www.boomplay.com', + 'Referer': 'https://www.boomplay.com', + 'X-Boomplay-Ref': 'Boomplay_WEBV1', + }, + **kwargs, + }] + else: + self.raise_no_formats('No formats found') + + def _extract_page_metadata(self, webpage, _id): + metadata_div = get_element_by_attribute( + 'class', r'[^\'"]*(?<=[\'"\s])summary(?=[\'"\s])[^\'"]*', webpage, + tag='div', escape_value=False) or '' + metadata_entries = re.findall(r'(?s)(?P.*?)', metadata_div) or [] + description = get_element_by_attribute( + 'class', r'[^\'"]*(?<=[\'"\s])description_content(?=[\'"\s])[^\'"]*', webpage, + tag='span', escape_value=False) or 'Listen and download music for free on Boomplay!' + description = clean_html(description.strip()) + if description == 'Listen and download music for free on Boomplay!': + description = None + + details_section = get_element_by_attribute( + 'class', r'[^\'"]*(?<=[\'"\s])songDetailInfo(?=[\'"\s])[^\'"]*', webpage, + tag='section', escape_value=False) or '' + metadata_entries.extend(re.findall(r'(?s)
  • (?P.*?)
  • ', details_section) or []) + page_metadata = { + 'id': _id, + 'title': self._html_search_regex(r'

    ([^<]+)

    ', metadata_div, 'title', default=''), + 'thumbnail': self._html_search_meta(['og:image', 'twitter:image'], + webpage, 'thumbnail', default=''), + 'like_count': int_or_none(get_element_by_class('btn_favorite', metadata_div)), + 'repost_count': int_or_none(get_element_by_class('btn_share', metadata_div)), + 'comment_count': int_or_none(get_element_by_class('btn_comment', metadata_div)), + 'duration': parse_duration(get_element_by_class('btn_duration', metadata_div)), + 'upload_date': unified_strdate(strip_or_none(get_element_by_class('btn_pubDate', metadata_div))), + 'description': description, + } + for metadata_entry in metadata_entries: + if ':' not in metadata_entry: + continue + k, v = clean_html(metadata_entry).split(':', 2) + v = v.strip() + if 'artist' in k.lower(): + page_metadata['artists'] = [v] + elif 'album' in k.lower(): + page_metadata['album'] = v + elif 'genre' in k.lower(): + page_metadata['genres'] = [v] + elif 'year of release' in k.lower(): + page_metadata['release_year'] = int_or_none(v) + return page_metadata + + extract = lambda self, url: self.write_debug(json.dumps(a := super().extract(url), indent=2)) or a # rm + + +class BoomPlayMusicIE(BoomPlayBaseIE): + _VALID_URL = r'https?://(?:www\.)?boomplay\.com/songs/(?P\d+)' + _TEST = { + 'url': 'https://www.boomplay.com/songs/165481965', + 'md5': 'c5fb4f23e6aae98064230ef3c39c2178', + 'info_dict': { + 'title': 'Rise of the Fallen Heroes', + 'ext': 'mp3', + 'id': '165481965', + 'artists': ['fatbunny'], + 'thumbnail': 'https://source.boomplaymusic.com/group10/M00/04/29/375ecda38f6f48179a93c72ab909118f_464_464.jpg', + 'channel_url': 'https://www.boomplay.com/artists/52723101', + 'duration': 125.0, + 'release_year': 2024, + 'comment_count': int, + 'like_count': int, + 'repost_count': int, + 'album': 'Legendary Battle', + 'genres': ['Metal'], + }, + } + + def _real_extract(self, url): + song_id = self._match_id(url) + webpage = self._download_webpage(url, song_id) + ld_json_meta = next(self._yield_json_ld(webpage, song_id)) + + return merge_dicts( + self._extract_page_metadata(webpage, song_id), + traverse_obj(ld_json_meta, { + 'title': 'name', + 'thumbnail': 'image', + 'channel_url': ('byArtist', 0, '@id'), + 'artists': ('byArtist', ..., 'name'), + 'duration': ('duration', {parse_duration}), + }), { + 'formats': self._extract_formats(song_id, 'MUSIC'), + }) + + +class BoomPlayVideoIE(BoomPlayBaseIE): + _VALID_URL = r'https?://(?:www\.)?boomplay\.com/video/(?P\d+)' + _TEST = { + 'url': 'https://www.boomplay.com/video/1154892', + 'md5': 'd9b67ad333d2292a82922062d065352d', + 'info_dict': { + 'id': '1154892', + 'ext': 'mp4', + 'title': 'Autumn blues', + 'thumbnail': 'https://source.boomplaymusic.com/group10/M00/10/10/2171dee9e1f8452e84021560729edb88.jpg', + 'upload_date': '20241010', + 'timestamp': 1728599214, + 'view_count': int, + 'duration': 177.0, + 'description': 'Autumn blues by Lugo', + }, + } + + def _real_extract(self, url): + video_id = self._match_id(url) + webpage = self._download_webpage(url, video_id) + return merge_dicts( + self._extract_page_metadata(webpage, video_id), + self._search_json_ld(webpage, video_id), { + 'formats': self._extract_formats(video_id, 'VIDEO', ext='mp4'), + }) + + +class BoomPlayEpisodeIE(BoomPlayBaseIE): + _VALID_URL = r'https?://(?:www\.)?boomplay\.com/episode/(?P\d+)' + _TEST = { + 'url': 'https://www.boomplay.com/episode/7132706', + 'md5': 'f26e236b764baa53d7a2cbb7e9ce6dc4', + 'info_dict': { + 'id': '7132706', + 'ext': 'mp3', + 'title': 'Letting Go', + 'repost_count': int, + 'thumbnail': 'https://source.boomplaymusic.com/group10/M00/05/06/fc535eaa25714b43a47185a9831887a5_320_320.jpg', + 'comment_count': int, + 'duration': 921.0, + 'upload_date': '20240506', + 'description': 'md5:5ec684b281fa0f9e4c31b3ee20c5e57a', + }, + } + + def _real_extract(self, url): + ep_id = self._match_id(url) + webpage = self._download_webpage(url, ep_id) + return merge_dicts( + self._extract_page_metadata(webpage, ep_id), { + 'title': self._og_search_title(webpage, fatal=True).rsplit('|', 2)[0].strip(), + 'description': self._html_search_meta( + ['description', 'og:description', 'twitter:description'], webpage), + 'formats': self._extract_formats(ep_id, 'EPISODE', vcodec='none'), + }) + + +class BoomPlayPodcastIE(BoomPlayBaseIE): + _VALID_URL = r'https?://(?:www\.)?boomplay\.com/podcasts/(?P\d+)' + _TEST = { + 'url': 'https://www.boomplay.com/podcasts/5372', + 'playlist_count': 200, + 'info_dict': { + 'id': '5372', + 'title': 'TED Talks Daily', + 'description': 'md5:541182e787ce8fd578c835534c907077', + 'thumbnail': 'https://source.boomplaymusic.com/group10/M00/12/22/6f9cf97ad6f846a0a7882c98dfcf4f8c_320_320.jpg', + 'repost_count': int, + 'comment_count': int, + }, + } + + def _real_extract(self, url): + _id = self._match_id(url) + webpage = self._download_webpage(url, _id) + song_list = get_elements_by_attribute( + 'class', r'[^\'"]*(?<=[\'"\s])morePart_musics(?=[\'"\s])[^\'"]*', webpage, + tag='ol', escape_value=False)[0] + song_list = traverse_obj(re.finditer( + r'''(?x) + <(?Pli) + (?:\s(?:[^>"']|"[^"]*"|'[^']*')*)? + \sdata-id\s*=\s*(?P<_q>['"]?)(?:(?P\d+))(?P=_q)''', + song_list), + (..., 'id', { + lambda x: self.url_result( + f'https://www.boomplay.com/episode/{x}', BoomPlayEpisodeIE, x), + })) + return self.playlist_result( + song_list, _id, + playlist_title=self._og_search_title(webpage, fatal=True).rsplit('|', 2)[0].strip(), + playlist_description=self._og_search_description(webpage, default=''), + **self._extract_page_metadata(webpage, _id)) + + +class BoomPlayPlaylistIE(BoomPlayBaseIE): + _VALID_URL = r'https?://(?:www\.)?boomplay\.com/(?:playlists|artists|albums)/(?P\d+)' + _TESTS = [{ + 'url': 'https://www.boomplay.com/playlists/33792494', + 'info_dict': { + 'id': '33792494', + 'title': 'Daily Trending Indonesia', + 'thumbnail': 'https://source.boomplaymusic.com/group10/M00/08/19/d05d431ee616412caeacd7f78f4f68f5_320_320.jpeg', + 'repost_count': int, + 'comment_count': int, + 'description': 'md5:7ebdffc5137c77acb62acb3c89248445', + }, + 'playlist_count': 10, + }, { + 'url': 'https://www.boomplay.com/artists/52723101', + 'only_matching': True, + }, { + 'url': 'https://www.boomplay.com/albums/89611238?from=home#google_vignette', + 'only_matching': True, + }] + + def _real_extract(self, url): + playlist_id = self._match_id(url) + webpage = self._download_webpage(url, playlist_id) + json_ld_metadata = next(self._yield_json_ld(webpage, playlist_id)) + # schema `MusicGroup` not supported by self._json_ld() + + return self.playlist_result(**merge_dicts( + self._extract_page_metadata(webpage, playlist_id), + traverse_obj(json_ld_metadata, { + 'entries': ('track', ..., 'url', { + functools.partial(self.url_result, ie=BoomPlayMusicIE), + }), + 'playlist_title': 'name', + 'thumbnail': 'image', + 'artists': ('byArtist', ..., 'name'), + 'channel_url': ('byArtist', 0, '@id'), + })))