From 6d2de79b7a419e822ed3e2f1308fde121b413094 Mon Sep 17 00:00:00 2001
From: grqx_wsl <173253225+grqx@users.noreply.github.com>
Date: Sun, 13 Oct 2024 23:07:33 +1300
Subject: [PATCH] BoomPlayGenericPlaylistIE, BoomPlaySearchIE
---
yt_dlp/extractor/_extractors.py | 2 +
yt_dlp/extractor/boomplay.py | 152 ++++++++++++++++++++++++++++++--
2 files changed, 146 insertions(+), 8 deletions(-)
diff --git a/yt_dlp/extractor/_extractors.py b/yt_dlp/extractor/_extractors.py
index 1abca1ed93..5208639e3a 100644
--- a/yt_dlp/extractor/_extractors.py
+++ b/yt_dlp/extractor/_extractors.py
@@ -282,9 +282,11 @@ from .bokecc import BokeCCIE
from .bongacams import BongaCamsIE
from .boomplay import (
BoomPlayEpisodeIE,
+ BoomPlayGenericPlaylistIE,
BoomPlayMusicIE,
BoomPlayPlaylistIE,
BoomPlayPodcastIE,
+ BoomPlaySearchIE,
BoomPlayVideoIE,
)
from .boosty import BoostyIE
diff --git a/yt_dlp/extractor/boomplay.py b/yt_dlp/extractor/boomplay.py
index dba8a1c9ca..692f4d98b4 100644
--- a/yt_dlp/extractor/boomplay.py
+++ b/yt_dlp/extractor/boomplay.py
@@ -2,22 +2,29 @@ import base64
import functools
import json
import re
+import urllib.parse
-from .common import InfoExtractor
+from .common import InfoExtractor, SearchInfoExtractor
from ..aes import aes_cbc_decrypt_bytes, aes_cbc_encrypt_bytes, unpad_pkcs7
from ..utils import (
ExtractorError,
clean_html,
+ extract_attributes,
get_element_by_attribute,
get_element_by_class,
get_elements_by_attribute,
int_or_none,
+ join_nonempty,
merge_dicts,
+ orderedSet,
+ parse_count,
parse_duration,
strip_or_none,
unified_strdate,
url_or_none,
urlencode_postdata,
+ urljoin,
+ variadic,
)
from ..utils.traversal import traverse_obj
@@ -27,6 +34,14 @@ class BoomPlayBaseIE(InfoExtractor):
# Note that the real key/iv differs from `lhx.AESUtils.key`/`lhx.AESUtils.iv`
_KEY = b'boomplayVr3xopAM'
_IV = b'boomplay8xIsKTn9'
+ _BASE = 'https://www.boomplay.com'
+ _MEDIA_TYPES = ('songs', 'video', 'episode', 'podcasts', 'playlists', 'artists', 'albums')
+
+ @classmethod
+ def _urljoin(cls, path):
+ if not hasattr(path, 'startswith') or path.startswith('javascript:'):
+ return None
+ return url_or_none(urljoin(base=cls._BASE, path=path))
def _get_playurl(self, item_id, item_type):
resp = self._download_json(
@@ -49,7 +64,6 @@ class BoomPlayBaseIE(InfoExtractor):
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',
@@ -79,12 +93,12 @@ class BoomPlayBaseIE(InfoExtractor):
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=''),
+ 'title': self._html_search_regex(r'([^<]+)
', metadata_div, 'title', default=None),
'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)),
+ 'like_count': parse_count(get_element_by_class('btn_favorite', metadata_div)),
+ 'repost_count': parse_count(get_element_by_class('btn_share', metadata_div)),
+ 'comment_count': parse_count(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,
@@ -104,7 +118,55 @@ class BoomPlayBaseIE(InfoExtractor):
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
+ def _extract_suitable_links(self, webpage, media_types):
+ if not media_types:
+ media_types = self._MEDIA_TYPES
+ media_types = list(variadic(media_types))
+
+ for idx, v in enumerate(media_types):
+ media_types[idx] = re.escape(v) if v in self._MEDIA_TYPES else ''
+ media_types = join_nonempty(*media_types, delim='|')
+ return orderedSet(traverse_obj(re.finditer(
+ rf'''(?x)
+ "']|"[^"]*"|'[^']*')*)?
+ (?<=\s)href\s*=\s*(?P<_q>['"])
+ (?:
+ (?!javascript:)(?P/(?:{media_types})/\d+?)
+ )
+ (?P=_q)
+ (?:\s(?:[^>"']|"[^"]*"|'[^']*')*)?
+ ''', webpage), (..., 'link', {self._urljoin}, {self.url_result})))
+
+ def _extract_playlist_entries(self, webpage, media_types, warn=True):
+ song_list = strip_or_none(
+ get_element_by_attribute(
+ 'class', r'[^\'"]*(?<=[\'"\s])morePart_musics(?=[\'"\s])[^\'"]*', webpage,
+ tag='ol', escape_value=False)
+ or get_element_by_attribute(
+ 'class', r'[^\'"]*(?<=[\'"\s])morePart(?=[\'"\s])[^\'"]*', webpage,
+ tag='ol', escape_value=False)
+ or '')
+
+ entries = traverse_obj(re.finditer(
+ r'''(?x)
+ "']|"[^"]*"|'[^']*')*)?
+ (?<=\s)class\s*=\s*(?P<_q>['"])
+ (?:
+ [^\'"]*(?<=[\'"\s])songName(?=[\'"\s])[^\'"]*
+ )
+ (?P=_q)
+ (?:\s(?:[^>"']|"[^"]*"|'[^']*')*)?
+ >
+ ''', song_list),
+ (..., 0, {extract_attributes}, 'href', {self._urljoin}, {self.url_result}))
+ if not entries:
+ if warn:
+ self.report_warning('Failed to extract playlist entries, finding suitable links instead!')
+ return self._extract_suitable_links(webpage, media_types)
+
+ return entries
class BoomPlayMusicIE(BoomPlayBaseIE):
@@ -143,7 +205,7 @@ class BoomPlayMusicIE(BoomPlayBaseIE):
'artists': ('byArtist', ..., 'name'),
'duration': ('duration', {parse_duration}),
}), {
- 'formats': self._extract_formats(song_id, 'MUSIC'),
+ 'formats': self._extract_formats(song_id, 'MUSIC', vcodec='none'),
})
@@ -217,6 +279,7 @@ class BoomPlayPodcastIE(BoomPlayBaseIE):
'thumbnail': 'https://source.boomplaymusic.com/group10/M00/12/22/6f9cf97ad6f846a0a7882c98dfcf4f8c_320_320.jpg',
'repost_count': int,
'comment_count': int,
+ 'like_count': int,
},
}
@@ -253,6 +316,7 @@ class BoomPlayPlaylistIE(BoomPlayBaseIE):
'thumbnail': 'https://source.boomplaymusic.com/group10/M00/08/19/d05d431ee616412caeacd7f78f4f68f5_320_320.jpeg',
'repost_count': int,
'comment_count': int,
+ 'like_count': int,
'description': 'md5:7ebdffc5137c77acb62acb3c89248445',
},
'playlist_count': 10,
@@ -281,3 +345,75 @@ class BoomPlayPlaylistIE(BoomPlayBaseIE):
'artists': ('byArtist', ..., 'name'),
'channel_url': ('byArtist', 0, '@id'),
})))
+
+
+class BoomPlayGenericPlaylistIE(BoomPlayBaseIE):
+ _VALID_URL = r'https?://(?:www\.)?boomplay\.com/.+'
+ _TESTS = [{
+ 'url': 'https://www.boomplay.com/search/default/Rise%20of%20the%20Fallen%20Heroes',
+ 'md5': 'c5fb4f23e6aae98064230ef3c39c2178',
+ 'info_dict': {
+ 'id': '165481965',
+ 'ext': 'mp3',
+ 'title': 'Rise of the Fallen Heroes',
+ 'duration': 125.0,
+ 'genres': ['Metal'],
+ 'artists': ['fatbunny'],
+ 'thumbnail': 'https://source.boomplaymusic.com/group10/M00/04/29/375ecda38f6f48179a93c72ab909118f_464_464.jpg',
+ 'channel_url': 'https://www.boomplay.com/artists/52723101',
+ 'comment_count': int,
+ 'repost_count': int,
+ 'album': 'Legendary Battle',
+ 'release_year': 2024,
+ 'like_count': int,
+ },
+ }, {
+ 'url': 'https://www.boomplay.com/search/video/%20Autumn%20blues',
+ 'md5': 'd9b67ad333d2292a82922062d065352d',
+ 'info_dict': {
+ 'id': '1154892',
+ 'title': 'Autumn blues',
+ 'ext': 'mp4',
+ 'timestamp': 1728599214,
+ 'view_count': int,
+ 'thumbnail': 'https://source.boomplaymusic.com/group10/M00/10/10/2171dee9e1f8452e84021560729edb88.jpg',
+ 'description': 'Autumn blues by Lugo',
+ 'upload_date': '20241010',
+ 'duration': 177.0,
+ },
+ 'expected_warnings': ['Failed to extract playlist entries, finding suitable links instead!'],
+ 'params': {'playlist_items': '1'},
+ }]
+
+ @classmethod
+ def suitable(cls, url):
+ if not any(ie.suitable(url) for ie in (
+ BoomPlayEpisodeIE,
+ BoomPlayMusicIE,
+ BoomPlayPlaylistIE,
+ BoomPlayPodcastIE,
+ BoomPlayVideoIE,
+ )):
+ return super().suitable(url)
+ return False
+
+ def _real_extract(self, url):
+ _id = self._generic_id(url)
+ webpage = self._download_webpage(url, _id)
+ # TODO: pass media types based on search types
+ return self.playlist_result(
+ self._extract_playlist_entries(webpage, self._MEDIA_TYPES),
+ **self._extract_page_metadata(webpage, _id))
+
+
+class BoomPlaySearchIE(SearchInfoExtractor):
+ _SEARCH_KEY = 'boomplaysearch'
+ _RETURN_TYPE = 'url'
+ _TEST = {
+ 'url': 'boomplaysearch:rise of the fallen heroes',
+ 'only_matching': True,
+ }
+
+ def _search_results(self, query):
+ yield self.url_result(
+ f'https://www.boomplay.com/search/default/{urllib.parse.quote(query)}')