Compare commits

...

10 Commits

Author SHA1 Message Date
kclauhk
615c0c24c2
Merge 48416c3718 into eb15fd5a32 2024-11-17 17:03:36 +01:00
krichbanana
eb15fd5a32
[ie/kenh14] Add extractor (#3996)
Closes #3937
Authored by: krichbanana, pzhlkj6612

Co-authored-by: Mozi <29089388+pzhlkj6612@users.noreply.github.com>
2024-11-17 14:12:26 +00:00
sepro
7cecd299e4
[ie/chaturbate] Don't break embed detection (#11565)
Bugfix for 720b3dc453

Authored by: seproDev
2024-11-17 13:32:12 +01:00
kclauhk
48416c3718 remove trailing whitespace 2024-10-31 12:25:11 +08:00
kclauhk
2fd16fdc6e ver contains instead of exact match 2024-10-31 12:12:25 +08:00
kclauhk
245ca4e515 use one ie_key only 2024-10-31 02:42:58 +08:00
kclauhk
0f49100783 correct file ext 2024-09-28 20:39:10 +08:00
kclauhk
05b4719f6c refactor
- avoid repeatedly calling `self._configuration_arg`
- avoid unnecessary looping when 'ver' argument is not provided
- add/change error messages
2024-09-28 17:00:22 +08:00
kclauhk
52f73ffc61 use geo_bypass_country instead and modify _REQUEST_HEADERS 2024-09-21 15:23:56 +08:00
kclauhk
83dedc8369 [ie/extrememusic] Add extractor 2024-09-17 03:29:56 +08:00
4 changed files with 529 additions and 2 deletions

View File

@ -626,6 +626,11 @@ from .europeantour import EuropeanTourIE
from .eurosport import EurosportIE from .eurosport import EurosportIE
from .euscreen import EUScreenIE from .euscreen import EUScreenIE
from .expressen import ExpressenIE from .expressen import ExpressenIE
from .extrememusic import (
ExtremeMusicAIE,
ExtremeMusicIE,
ExtremeMusicPIE,
)
from .eyedotv import EyedoTVIE from .eyedotv import EyedoTVIE
from .facebook import ( from .facebook import (
FacebookAdsIE, FacebookAdsIE,
@ -946,6 +951,10 @@ from .kaltura import KalturaIE
from .kankanews import KankaNewsIE from .kankanews import KankaNewsIE
from .karaoketv import KaraoketvIE from .karaoketv import KaraoketvIE
from .kelbyone import KelbyOneIE from .kelbyone import KelbyOneIE
from .kenh14 import (
Kenh14PlaylistIE,
Kenh14VideoIE,
)
from .khanacademy import ( from .khanacademy import (
KhanAcademyIE, KhanAcademyIE,
KhanAcademyUnitIE, KhanAcademyUnitIE,

View File

@ -79,7 +79,7 @@ class ChaturbateIE(InfoExtractor):
'formats': self._extract_m3u8_formats(m3u8_url, video_id, ext='mp4', live=True), 'formats': self._extract_m3u8_formats(m3u8_url, video_id, ext='mp4', live=True),
} }
def _extract_from_webpage(self, video_id, tld): def _extract_from_html(self, video_id, tld):
webpage = self._download_webpage( webpage = self._download_webpage(
f'https://chaturbate.{tld}/{video_id}/', video_id, f'https://chaturbate.{tld}/{video_id}/', video_id,
headers=self.geo_verification_headers(), impersonate=True) headers=self.geo_verification_headers(), impersonate=True)
@ -151,4 +151,4 @@ class ChaturbateIE(InfoExtractor):
def _real_extract(self, url): def _real_extract(self, url):
video_id, tld = self._match_valid_url(url).group('id', 'tld') video_id, tld = self._match_valid_url(url).group('id', 'tld')
return self._extract_from_api(video_id, tld) or self._extract_from_webpage(video_id, tld) return self._extract_from_api(video_id, tld) or self._extract_from_html(video_id, tld)

View File

@ -0,0 +1,358 @@
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
_REQUIRE_VERSION = []
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',
'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):
album = self._download_json(f'{self._API_URL}/albums/{album_id}', video_id, fatal=fatal,
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', 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):
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': ('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 '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'))):
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):
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]
else:
self.raise_no_formats('Track data not found', video_id=track_id)
return []
class ExtremeMusicIE(ExtremeMusicBaseIE):
_VALID_URL = r'https?://(?:www\.)?extrememusic\.com/albums/(?P<album>\d+)\?(.*item=(?P<id>\d+))?(.*ver=(?P<ver>\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')
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')
class ExtremeMusicAIE(ExtremeMusicBaseIE):
IE_NAME = 'ExtremeMusic:album'
_VALID_URL = r'https?://(?:www\.)?extrememusic\.com/albums/(?P<id>\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)
self._initialize(url, album_id, self.get_param('geo_bypass_country') or 'DE')
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)
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')
class ExtremeMusicPIE(ExtremeMusicBaseIE):
IE_NAME = 'ExtremeMusic:playlist'
_VALID_URL = r'https?://(?:www\.)?extrememusic\.com/playlists/(?P<id>[^?]+)'
_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': 29,
'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': {
'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)
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}'
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=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
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')):
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')

160
yt_dlp/extractor/kenh14.py Normal file
View File

@ -0,0 +1,160 @@
from .common import InfoExtractor
from ..utils import (
clean_html,
extract_attributes,
get_element_by_class,
get_element_html_by_attribute,
get_elements_html_by_class,
int_or_none,
parse_duration,
parse_iso8601,
remove_start,
strip_or_none,
unescapeHTML,
update_url,
url_or_none,
)
from ..utils.traversal import traverse_obj
class Kenh14VideoIE(InfoExtractor):
_VALID_URL = r'https?://video\.kenh14\.vn/(?:video/)?[\w-]+-(?P<id>[0-9]+)\.chn'
_TESTS = [{
'url': 'https://video.kenh14.vn/video/mo-hop-iphone-14-pro-max-nguon-unbox-therapy-316173.chn',
'md5': '1ed67f9c3a1e74acf15db69590cf6210',
'info_dict': {
'id': '316173',
'ext': 'mp4',
'title': 'Video mở hộp iPhone 14 Pro Max (Nguồn: Unbox Therapy)',
'description': 'Video mở hộp iPhone 14 Pro MaxVideo mở hộp iPhone 14 Pro Max (Nguồn: Unbox Therapy)',
'thumbnail': r're:^https?://videothumbs\.mediacdn\.vn/.*\.jpg$',
'tags': [],
'uploader': 'Unbox Therapy',
'upload_date': '20220517',
'view_count': int,
'duration': 722.86,
'timestamp': 1652764468,
},
}, {
'url': 'https://video.kenh14.vn/video-316174.chn',
'md5': '2b41877d2afaf4a3f487ceda8e5c7cbd',
'info_dict': {
'id': '316174',
'ext': 'mp4',
'title': 'Khoảnh khắc VĐV nằm gục khóc sau chiến thắng: 7 năm trời Việt Nam mới có HCV kiếm chém nữ, chỉ có 8 tháng để khổ luyện trước khi lên sàn đấu',
'description': 'md5:de86aa22e143e2b277bce8ec9c6f17dc',
'thumbnail': r're:^https?://videothumbs\.mediacdn\.vn/.*\.jpg$',
'tags': [],
'upload_date': '20220517',
'view_count': int,
'duration': 70.04,
'timestamp': 1652766021,
},
}, {
'url': 'https://video.kenh14.vn/0-344740.chn',
'md5': 'b843495d5e728142c8870c09b46df2a9',
'info_dict': {
'id': '344740',
'ext': 'mov',
'title': 'Kỳ Duyên đầy căng thẳng trong buổi ra quân đi Miss Universe, nghi thức tuyên thuệ lần đầu xuất hiện gây nhiều tranh cãi',
'description': 'md5:2a2dbb4a7397169fb21ee68f09160497',
'thumbnail': r're:^https?://kenh14cdn\.com/.*\.jpg$',
'tags': ['kỳ duyên', 'Kỳ Duyên tuyên thuệ', 'miss universe'],
'uploader': 'Quang Vũ',
'upload_date': '20241024',
'view_count': int,
'duration': 198.88,
'timestamp': 1729741590,
},
}]
def _real_extract(self, url):
video_id = self._match_id(url)
webpage = self._download_webpage(url, video_id)
attrs = extract_attributes(get_element_html_by_attribute('type', 'VideoStream', webpage) or '')
direct_url = attrs['data-vid']
metadata = self._download_json(
'https://api.kinghub.vn/video/api/v1/detailVideoByGet?FileName={}'.format(
remove_start(direct_url, 'kenh14cdn.com/')), video_id, fatal=False)
formats = [{'url': f'https://{direct_url}', 'format_id': 'http', 'quality': 1}]
subtitles = {}
video_data = self._download_json(
f'https://{direct_url}.json', video_id, note='Downloading video data', fatal=False)
if hls_url := traverse_obj(video_data, ('hls', {url_or_none})):
fmts, subs = self._extract_m3u8_formats_and_subtitles(
hls_url, video_id, m3u8_id='hls', fatal=False)
formats.extend(fmts)
self._merge_subtitles(subs, target=subtitles)
if dash_url := traverse_obj(video_data, ('mpd', {url_or_none})):
fmts, subs = self._extract_mpd_formats_and_subtitles(
dash_url, video_id, mpd_id='dash', fatal=False)
formats.extend(fmts)
self._merge_subtitles(subs, target=subtitles)
return {
**traverse_obj(metadata, {
'duration': ('duration', {parse_duration}),
'uploader': ('author', {strip_or_none}),
'timestamp': ('uploadtime', {parse_iso8601(delimiter=' ')}),
'view_count': ('views', {int_or_none}),
}),
'id': video_id,
'title': (
traverse_obj(metadata, ('title', {strip_or_none}))
or clean_html(self._og_search_title(webpage))
or clean_html(get_element_by_class('vdbw-title', webpage))),
'formats': formats,
'subtitles': subtitles,
'description': (
clean_html(self._og_search_description(webpage))
or clean_html(get_element_by_class('vdbw-sapo', webpage))),
'thumbnail': (self._og_search_thumbnail(webpage) or attrs.get('data-thumb')),
'tags': traverse_obj(self._html_search_meta('keywords', webpage), (
{lambda x: x.split(';')}, ..., filter)),
}
class Kenh14PlaylistIE(InfoExtractor):
_VALID_URL = r'https?://video\.kenh14\.vn/playlist/[\w-]+-(?P<id>[0-9]+)\.chn'
_TESTS = [{
'url': 'https://video.kenh14.vn/playlist/tran-tinh-naked-love-mua-2-71.chn',
'info_dict': {
'id': '71',
'title': 'Trần Tình (Naked love) mùa 2',
'description': 'md5:e9522339304956dea931722dd72eddb2',
'thumbnail': r're:^https?://kenh14cdn\.com/.*\.png$',
},
'playlist_count': 9,
}, {
'url': 'https://video.kenh14.vn/playlist/0-72.chn',
'info_dict': {
'id': '72',
'title': 'Lau Lại Đầu Từ',
'description': 'Cùng xem xưa và nay có gì khác biệt nhé!',
'thumbnail': r're:^https?://kenh14cdn\.com/.*\.png$',
},
'playlist_count': 6,
}]
def _real_extract(self, url):
playlist_id = self._match_id(url)
webpage = self._download_webpage(url, playlist_id)
category_detail = get_element_by_class('category-detail', webpage) or ''
embed_info = traverse_obj(
self._yield_json_ld(webpage, playlist_id),
(lambda _, v: v['name'] and v['alternateName'], any)) or {}
return self.playlist_from_matches(
get_elements_html_by_class('video-item', webpage), playlist_id,
(clean_html(get_element_by_class('name', category_detail)) or unescapeHTML(embed_info.get('name'))),
getter=lambda x: 'https://video.kenh14.vn/video/video-{}.chn'.format(extract_attributes(x)['data-id']),
ie=Kenh14VideoIE, playlist_description=(
clean_html(get_element_by_class('description', category_detail))
or unescapeHTML(embed_info.get('alternateName'))),
thumbnail=traverse_obj(
self._og_search_thumbnail(webpage),
({url_or_none}, {update_url(query=None)})))