mirror of
https://github.com/yt-dlp/yt-dlp.git
synced 2026-08-15 08:39:09 +03:00
Compare commits
2
Commits
e584a65f2a
...
8bdfbfd446
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8bdfbfd446 | ||
|
|
acc995cf91 |
@@ -1986,12 +1986,6 @@ from .toypics import (
|
||||
ToypicsIE,
|
||||
ToypicsUserIE,
|
||||
)
|
||||
from .trovo import (
|
||||
TrovoChannelClipIE,
|
||||
TrovoChannelVodIE,
|
||||
TrovoIE,
|
||||
TrovoVodIE,
|
||||
)
|
||||
from .trtcocuk import TrtCocukVideoIE
|
||||
from .trtworld import TrtWorldIE
|
||||
from .trueid import TrueIDIE
|
||||
|
||||
+177
-53
@@ -378,56 +378,30 @@ class SoundcloudBaseIE(InfoExtractor):
|
||||
if info.get('policy') == 'BLOCK':
|
||||
self.raise_geo_restricted(metadata_available=True)
|
||||
|
||||
user = info.get('user') or {}
|
||||
|
||||
thumbnails = []
|
||||
artwork_url = info.get('artwork_url')
|
||||
thumbnail = artwork_url or user.get('avatar_url')
|
||||
if url_or_none(thumbnail):
|
||||
if mobj := re.search(self._IMAGE_REPL_RE, thumbnail):
|
||||
for image_id, size in self._ARTWORK_MAP.items():
|
||||
# Soundcloud serves JPEG regardless of URL's ext *except* for "original" thumb
|
||||
ext = mobj.group('ext') if image_id == 'original' else 'jpg'
|
||||
i = {
|
||||
'id': image_id,
|
||||
'url': re.sub(self._IMAGE_REPL_RE, f'-{image_id}.{ext}', thumbnail),
|
||||
}
|
||||
if image_id == 'tiny' and not artwork_url:
|
||||
size = 18
|
||||
elif image_id == 'original':
|
||||
i['preference'] = 10
|
||||
if size:
|
||||
i.update({
|
||||
'width': size,
|
||||
'height': size,
|
||||
})
|
||||
thumbnails.append(i)
|
||||
else:
|
||||
thumbnails = [{'url': thumbnail}]
|
||||
|
||||
def extract_count(key):
|
||||
return int_or_none(info.get(f'{key}_count'))
|
||||
|
||||
return {
|
||||
**traverse_obj(info, {
|
||||
'uploader': ('user', 'username', {str}),
|
||||
'uploader_id': ('user', ('id', 'permalink'), {str_or_none}, any),
|
||||
'uploader_url': ('user', 'permalink_url', {url_or_none}),
|
||||
'timestamp': ('created_at', {unified_timestamp}),
|
||||
'title': ('title', {str}),
|
||||
'track': ('title', {str}),
|
||||
'description': ('description', {str}),
|
||||
'duration': ('duration', {float_or_none(scale=1000)}),
|
||||
'webpage_url': ('permalink_url', {url_or_none}),
|
||||
'license': ('license', {str}),
|
||||
'view_count': ('playback_count', {int_or_none}),
|
||||
'like_count': (('favoritings_count', 'likes_count'), {int_or_none}, any),
|
||||
'comment_count': ('comment_count', {int_or_none}),
|
||||
'repost_count': ('reposts_count', {int_or_none}),
|
||||
'release_timestamp': ('release_date', {unified_timestamp}),
|
||||
'modified_timestamp': ('last_modified', {unified_timestamp}),
|
||||
'genres': ('genre', {str}, filter, all, filter),
|
||||
'tags': ('tag_list', {self._TAGS_RE.findall}, ..., ..., filter),
|
||||
'artists': ('publisher_metadata', 'artist', {str}, filter, all, filter),
|
||||
}),
|
||||
'id': track_id,
|
||||
'uploader': user.get('username'),
|
||||
'uploader_id': str_or_none(user.get('id')) or user.get('permalink'),
|
||||
'uploader_url': user.get('permalink_url'),
|
||||
'timestamp': unified_timestamp(info.get('created_at')),
|
||||
'title': info.get('title'),
|
||||
'track': info.get('title'),
|
||||
'description': info.get('description'),
|
||||
'thumbnails': thumbnails,
|
||||
'duration': float_or_none(info.get('duration'), 1000),
|
||||
'webpage_url': info.get('permalink_url'),
|
||||
'license': info.get('license'),
|
||||
'view_count': extract_count('playback'),
|
||||
'like_count': extract_count('favoritings') or extract_count('likes'),
|
||||
'comment_count': extract_count('comment'),
|
||||
'repost_count': extract_count('reposts'),
|
||||
'genres': traverse_obj(info, ('genre', {str}, filter, all, filter)),
|
||||
'tags': traverse_obj(info, ('tag_list', {self._TAGS_RE.findall}, ..., ..., filter)),
|
||||
'artists': traverse_obj(info, ('publisher_metadata', 'artist', {str}, filter, all, filter)),
|
||||
'thumbnails': self._extract_thumbnails(info),
|
||||
'formats': formats if not extract_flat else None,
|
||||
'__post_extractor': self.extract_comments(track_id),
|
||||
}
|
||||
@@ -478,6 +452,43 @@ class SoundcloudBaseIE(InfoExtractor):
|
||||
if not next_url:
|
||||
break
|
||||
|
||||
def _extract_thumbnails(self, info):
|
||||
artwork_url = traverse_obj(info, ('artwork_url', {url_or_none}))
|
||||
thumbnail_url = artwork_url or traverse_obj(info, ('user', 'avatar_url', {url_or_none}))
|
||||
if not thumbnail_url:
|
||||
return None
|
||||
|
||||
thumbnails = []
|
||||
if mobj := re.search(self._IMAGE_REPL_RE, thumbnail_url):
|
||||
for image_id, size in self._ARTWORK_MAP.items():
|
||||
# Soundcloud serves JPEG regardless of URL's ext *except* for "original" thumb
|
||||
ext = mobj.group('ext') if image_id == 'original' else 'jpg'
|
||||
thumbnail = {
|
||||
'id': image_id,
|
||||
'url': re.sub(self._IMAGE_REPL_RE, f'-{image_id}.{ext}', thumbnail_url),
|
||||
}
|
||||
if image_id == 'tiny' and not artwork_url:
|
||||
size = 18
|
||||
elif image_id == 'original':
|
||||
thumbnail['preference'] = 10
|
||||
# "original" thumb ext doesn't always match ext used for other thumbs, check with HEAD req
|
||||
req = self._request_webpage(
|
||||
HEADRequest(thumbnail['url']), str(info['id']), note='Checking thumbnail extension',
|
||||
errnote=False, fatal=False, headers=self._HEADERS)
|
||||
if not req:
|
||||
# If "original" thumb doesn't exist, assume different ext
|
||||
ext = 'jpg' if ext == 'png' else 'png'
|
||||
thumbnail['url'] = re.sub(self._IMAGE_REPL_RE, f'-{image_id}.{ext}', thumbnail_url)
|
||||
if size:
|
||||
thumbnail.update({
|
||||
'width': size,
|
||||
'height': size,
|
||||
})
|
||||
thumbnails.append(thumbnail)
|
||||
else:
|
||||
thumbnails = [{'url': thumbnail_url}]
|
||||
return thumbnails
|
||||
|
||||
|
||||
class SoundcloudIE(SoundcloudBaseIE):
|
||||
"""Information extractor for soundcloud.com
|
||||
@@ -522,6 +533,8 @@ class SoundcloudIE(SoundcloudBaseIE):
|
||||
'thumbnail': r're:https?://[ai]1\.sndcdn\.com/.+\.(?:jpg|png)',
|
||||
'uploader_url': 'https://soundcloud.com/ethmusic',
|
||||
'tags': 'count:14',
|
||||
'modified_timestamp': 1350184468,
|
||||
'modified_date': '20121014',
|
||||
},
|
||||
'params': {'skip_download': 'm3u8'},
|
||||
}, {
|
||||
@@ -547,7 +560,8 @@ class SoundcloudIE(SoundcloudBaseIE):
|
||||
'uploader_url': 'https://soundcloud.com/jaimemf',
|
||||
'thumbnail': r're:https?://[ai]1\.sndcdn\.com/.+\.(?:jpg|png)',
|
||||
'genres': ['youtubedl'],
|
||||
'tags': [],
|
||||
'modified_timestamp': 1386604920,
|
||||
'modified_date': '20131209',
|
||||
},
|
||||
}, {
|
||||
# private link (alt format)
|
||||
@@ -572,7 +586,8 @@ class SoundcloudIE(SoundcloudBaseIE):
|
||||
'uploader_url': 'https://soundcloud.com/jaimemf',
|
||||
'thumbnail': r're:https?://[ai]1\.sndcdn\.com/.+\.(?:jpg|png)',
|
||||
'genres': ['youtubedl'],
|
||||
'tags': [],
|
||||
'modified_timestamp': 1386604920,
|
||||
'modified_date': '20131209',
|
||||
},
|
||||
}, {
|
||||
# downloadable song
|
||||
@@ -598,6 +613,10 @@ class SoundcloudIE(SoundcloudBaseIE):
|
||||
'genres': ['Dance & EDM'],
|
||||
'artists': ['80M'],
|
||||
'tags': 'count:4',
|
||||
'release_timestamp': 1506384000,
|
||||
'release_date': '20170926',
|
||||
'modified_timestamp': 1647390150,
|
||||
'modified_date': '20220316',
|
||||
},
|
||||
'params': {'skip_download': 'm3u8'},
|
||||
'expected_warnings': ['Original download format is only available for registered users'],
|
||||
@@ -627,6 +646,8 @@ class SoundcloudIE(SoundcloudBaseIE):
|
||||
'genres': ['Trance'],
|
||||
'artists': ['Ori Uplift'],
|
||||
'tags': 'count:6',
|
||||
'modified_timestamp': 1504258507,
|
||||
'modified_date': '20170901',
|
||||
},
|
||||
'expected_warnings': ['Original download format is only available for registered users'],
|
||||
}, {
|
||||
@@ -652,7 +673,8 @@ class SoundcloudIE(SoundcloudBaseIE):
|
||||
'repost_count': int,
|
||||
'uploader_url': 'https://soundcloud.com/garyvee',
|
||||
'artists': ['MadReal'],
|
||||
'tags': [],
|
||||
'modified_timestamp': 1488293034,
|
||||
'modified_date': '20170228',
|
||||
},
|
||||
'params': {'skip_download': 'm3u8'},
|
||||
}, {
|
||||
@@ -678,6 +700,8 @@ class SoundcloudIE(SoundcloudBaseIE):
|
||||
'genres': ['Piano'],
|
||||
'uploader_url': 'https://soundcloud.com/giovannisarani',
|
||||
'tags': 'count:10',
|
||||
'modified_timestamp': 1692623663,
|
||||
'modified_date': '20230821',
|
||||
},
|
||||
'params': {'skip_download': 'm3u8'},
|
||||
}, {
|
||||
@@ -696,12 +720,15 @@ class SoundcloudIE(SoundcloudBaseIE):
|
||||
'like_count': int,
|
||||
'repost_count': int,
|
||||
'duration': 213.469,
|
||||
'tags': [],
|
||||
'artists': ['$KORXH'],
|
||||
'track': 'audio dealer',
|
||||
'timestamp': 1737143201,
|
||||
'upload_date': '20250117',
|
||||
'license': 'all-rights-reserved',
|
||||
'release_timestamp': 1736985600,
|
||||
'release_date': '20250116',
|
||||
'modified_timestamp': 1737143467,
|
||||
'modified_date': '20250117',
|
||||
'thumbnail': r're:https?://[ai]1\.sndcdn\.com/.+\.(?:jpg|png)',
|
||||
'thumbnails': [
|
||||
{'id': 'mini', 'url': 'https://i1.sndcdn.com/artworks-a1wKGMYNreDLTMrT-fGjRiw-mini.jpg'},
|
||||
@@ -733,12 +760,13 @@ class SoundcloudIE(SoundcloudBaseIE):
|
||||
'repost_count': int,
|
||||
'duration': 241.601,
|
||||
'thumbnail': 'https://i1.sndcdn.com/artworks-000209893581-orfv6t-original.jpg',
|
||||
'tags': [],
|
||||
'artists': ['BENDY AND THE INK MACHINE SONG (Build Our Machine) INSTRUMENTAL '],
|
||||
'track': 'BENDY AND THE INK MACHINE SONG (Build Our Machine) INSTRUMENTAL by DAGAMES',
|
||||
'timestamp': 1488232827,
|
||||
'upload_date': '20170227',
|
||||
'license': 'all-rights-reserved',
|
||||
'modified_timestamp': 1645028949,
|
||||
'modified_date': '20220216',
|
||||
},
|
||||
'params': {'get_comments': True, 'skip_download': 'm3u8'},
|
||||
}, {
|
||||
@@ -827,7 +855,17 @@ class SoundcloudPlaylistBaseIE(SoundcloudBaseIE):
|
||||
'uploader': ('user', 'username', {str}),
|
||||
'uploader_id': ('user', 'id', {str_or_none}),
|
||||
'uploader_url': ('user', 'permalink_url', {url_or_none}),
|
||||
'timestamp': ('created_at', {unified_timestamp}),
|
||||
'release_timestamp': (('release_date', 'published_at'), {unified_timestamp}, any),
|
||||
'modified_timestamp': ('last_modified', {unified_timestamp}),
|
||||
'duration': ('duration', {float_or_none(scale=1000)}),
|
||||
'license': ('license', {str}),
|
||||
'like_count': ('likes_count', {int_or_none}),
|
||||
'repost_count': ('reposts_count', {int_or_none}),
|
||||
'genres': ('genre', {str}, filter, all, filter),
|
||||
'tags': ('tag_list', {self._TAGS_RE.findall}, ..., ..., filter),
|
||||
}),
|
||||
thumbnails=self._extract_thumbnails(playlist),
|
||||
)
|
||||
|
||||
|
||||
@@ -835,6 +873,7 @@ class SoundcloudSetIE(SoundcloudPlaylistBaseIE):
|
||||
_VALID_URL = r'https?://(?:(?:www|m)\.)?soundcloud\.com/(?P<uploader>[\w\d-]+)/sets/(?P<slug_title>[:\w\d-]+)(?:/(?P<token>[^?/]+))?'
|
||||
IE_NAME = 'soundcloud:set'
|
||||
_TESTS = [{
|
||||
# No release date, no tags
|
||||
'url': 'https://soundcloud.com/the-concept-band/sets/the-royal-concept-ep',
|
||||
'info_dict': {
|
||||
'id': '2284613',
|
||||
@@ -846,8 +885,68 @@ class SoundcloudSetIE(SoundcloudPlaylistBaseIE):
|
||||
'album': 'The Royal Concept EP',
|
||||
'album_artists': ['The Royal Concept'],
|
||||
'album_type': 'ep',
|
||||
'timestamp': 1343497860,
|
||||
'upload_date': '20120728',
|
||||
'modified_timestamp': 1358471457,
|
||||
'modified_date': '20130118',
|
||||
'duration': 1398.595,
|
||||
'license': 'all-rights-reserved',
|
||||
'like_count': 482,
|
||||
'repost_count': 99,
|
||||
'genres': ['Indie/pop'],
|
||||
'thumbnails': [
|
||||
{'id': 'mini', 'url': 'https://i1.sndcdn.com/artworks-000030896212-o16m9v-mini.jpg'},
|
||||
{'id': 'tiny', 'url': 'https://i1.sndcdn.com/artworks-000030896212-o16m9v-tiny.jpg'},
|
||||
{'id': 'small', 'url': 'https://i1.sndcdn.com/artworks-000030896212-o16m9v-small.jpg'},
|
||||
{'id': 'badge', 'url': 'https://i1.sndcdn.com/artworks-000030896212-o16m9v-badge.jpg'},
|
||||
{'id': 't67x67', 'url': 'https://i1.sndcdn.com/artworks-000030896212-o16m9v-t67x67.jpg'},
|
||||
{'id': 'large', 'url': 'https://i1.sndcdn.com/artworks-000030896212-o16m9v-large.jpg'},
|
||||
{'id': 't300x300', 'url': 'https://i1.sndcdn.com/artworks-000030896212-o16m9v-t300x300.jpg'},
|
||||
{'id': 'crop', 'url': 'https://i1.sndcdn.com/artworks-000030896212-o16m9v-crop.jpg'},
|
||||
{'id': 't500x500', 'url': 'https://i1.sndcdn.com/artworks-000030896212-o16m9v-t500x500.jpg'},
|
||||
{'id': 'original', 'url': 'https://i1.sndcdn.com/artworks-000030896212-o16m9v-original.jpg'},
|
||||
],
|
||||
},
|
||||
'playlist_mincount': 5,
|
||||
}, {
|
||||
# Release date, multiple tags, empty desc
|
||||
'url': 'https://soundcloud.com/leviryan/sets/out-of-spite',
|
||||
'info_dict': {
|
||||
'id': '1524158182',
|
||||
'title': 'out of spite',
|
||||
'description': '',
|
||||
'uploader': 'Levi Ryan',
|
||||
'uploader_id': '229146182',
|
||||
'uploader_url': 'https://soundcloud.com/leviryan',
|
||||
'album': 'out of spite',
|
||||
'album_artists': ['Levi Ryan'],
|
||||
'album_type': 'album',
|
||||
'timestamp': 1667935849,
|
||||
'upload_date': '20221108',
|
||||
'release_timestamp': 1667865600,
|
||||
'release_date': '20221108',
|
||||
'modified_timestamp': 1667935903,
|
||||
'modified_date': '20221108',
|
||||
'duration': 1531.376,
|
||||
'license': 'all-rights-reserved',
|
||||
'like_count': 185,
|
||||
'repost_count': 40,
|
||||
'genres': ['Hip-hop & Rap'],
|
||||
'tags': ['Drum & Bass', 'Alternative', 'Ambient'],
|
||||
'thumbnails': [
|
||||
{'id': 'mini', 'url': 'https://i1.sndcdn.com/artworks-2hmuDCrcvCzzCaXZ-1rztZA-mini.jpg'},
|
||||
{'id': 'tiny', 'url': 'https://i1.sndcdn.com/artworks-2hmuDCrcvCzzCaXZ-1rztZA-tiny.jpg'},
|
||||
{'id': 'small', 'url': 'https://i1.sndcdn.com/artworks-2hmuDCrcvCzzCaXZ-1rztZA-small.jpg'},
|
||||
{'id': 'badge', 'url': 'https://i1.sndcdn.com/artworks-2hmuDCrcvCzzCaXZ-1rztZA-badge.jpg'},
|
||||
{'id': 't67x67', 'url': 'https://i1.sndcdn.com/artworks-2hmuDCrcvCzzCaXZ-1rztZA-t67x67.jpg'},
|
||||
{'id': 'large', 'url': 'https://i1.sndcdn.com/artworks-2hmuDCrcvCzzCaXZ-1rztZA-large.jpg'},
|
||||
{'id': 't300x300', 'url': 'https://i1.sndcdn.com/artworks-2hmuDCrcvCzzCaXZ-1rztZA-t300x300.jpg'},
|
||||
{'id': 'crop', 'url': 'https://i1.sndcdn.com/artworks-2hmuDCrcvCzzCaXZ-1rztZA-crop.jpg'},
|
||||
{'id': 't500x500', 'url': 'https://i1.sndcdn.com/artworks-2hmuDCrcvCzzCaXZ-1rztZA-t500x500.jpg'},
|
||||
{'id': 'original', 'url': 'https://i1.sndcdn.com/artworks-2hmuDCrcvCzzCaXZ-1rztZA-original.jpg'},
|
||||
],
|
||||
},
|
||||
'playlist_count': 8,
|
||||
}, {
|
||||
'url': 'https://soundcloud.com/the-concept-band/sets/the-royal-concept-ep/token',
|
||||
'only_matching': True,
|
||||
@@ -1163,6 +1262,31 @@ class SoundcloudPlaylistIE(SoundcloudPlaylistBaseIE):
|
||||
'album_artists': ['Non-Site Records'],
|
||||
'album_type': 'playlist',
|
||||
'album': 'TILT Brass - Bowery Poetry Club, August \'03 [Non-Site SCR 02]',
|
||||
'timestamp': 1363395687,
|
||||
'upload_date': '20130316',
|
||||
'release_timestamp': 1363392000,
|
||||
'release_date': '20130316',
|
||||
'modified_timestamp': 1444746489,
|
||||
'modified_date': '20151013',
|
||||
'duration': 2152.685,
|
||||
'license': 'all-rights-reserved',
|
||||
'like_count': 2,
|
||||
'repost_count': 2,
|
||||
'genres': ['Downtown'],
|
||||
'tags': ['Non-Site Records', 'TILT Brass', 'TILT Creative Brass Band', 'Bowery Poetry Club',
|
||||
'Nick Didkovsky', 'Tom Waits', 'Dave Ballou', 'Elliott Sharp', 'AFKA Prince', 'NPG'],
|
||||
'thumbnails': [
|
||||
{'id': 'mini', 'url': 'https://i1.sndcdn.com/artworks-000043059944-9zwy8g-mini.jpg'},
|
||||
{'id': 'tiny', 'url': 'https://i1.sndcdn.com/artworks-000043059944-9zwy8g-tiny.jpg'},
|
||||
{'id': 'small', 'url': 'https://i1.sndcdn.com/artworks-000043059944-9zwy8g-small.jpg'},
|
||||
{'id': 'badge', 'url': 'https://i1.sndcdn.com/artworks-000043059944-9zwy8g-badge.jpg'},
|
||||
{'id': 't67x67', 'url': 'https://i1.sndcdn.com/artworks-000043059944-9zwy8g-t67x67.jpg'},
|
||||
{'id': 'large', 'url': 'https://i1.sndcdn.com/artworks-000043059944-9zwy8g-large.jpg'},
|
||||
{'id': 't300x300', 'url': 'https://i1.sndcdn.com/artworks-000043059944-9zwy8g-t300x300.jpg'},
|
||||
{'id': 'crop', 'url': 'https://i1.sndcdn.com/artworks-000043059944-9zwy8g-crop.jpg'},
|
||||
{'id': 't500x500', 'url': 'https://i1.sndcdn.com/artworks-000043059944-9zwy8g-t500x500.jpg'},
|
||||
{'id': 'original', 'url': 'https://i1.sndcdn.com/artworks-000043059944-9zwy8g-original.png'},
|
||||
],
|
||||
},
|
||||
'playlist_count': 6,
|
||||
}, {
|
||||
|
||||
@@ -1,342 +0,0 @@
|
||||
import itertools
|
||||
import json
|
||||
import random
|
||||
import string
|
||||
|
||||
from .common import InfoExtractor
|
||||
from ..utils import (
|
||||
ExtractorError,
|
||||
format_field,
|
||||
int_or_none,
|
||||
str_or_none,
|
||||
traverse_obj,
|
||||
try_get,
|
||||
)
|
||||
|
||||
|
||||
class TrovoBaseIE(InfoExtractor):
|
||||
_VALID_URL_BASE = r'https?://(?:www\.)?trovo\.live/'
|
||||
_HEADERS = {'Origin': 'https://trovo.live'}
|
||||
|
||||
def _call_api(self, video_id, data):
|
||||
if 'persistedQuery' in data.get('extensions', {}):
|
||||
url = 'https://gql.trovo.live'
|
||||
else:
|
||||
url = 'https://api-web.trovo.live/graphql'
|
||||
|
||||
resp = self._download_json(
|
||||
url, video_id, data=json.dumps([data]).encode(), headers={'Accept': 'application/json'},
|
||||
query={
|
||||
'qid': ''.join(random.choices(string.ascii_uppercase + string.digits, k=16)),
|
||||
})[0]
|
||||
if 'errors' in resp:
|
||||
raise ExtractorError(f'Trovo said: {resp["errors"][0]["message"]}')
|
||||
return resp['data'][data['operationName']]
|
||||
|
||||
def _extract_streamer_info(self, data):
|
||||
streamer_info = data.get('streamerInfo') or {}
|
||||
username = streamer_info.get('userName')
|
||||
return {
|
||||
'uploader': streamer_info.get('nickName'),
|
||||
'uploader_id': str_or_none(streamer_info.get('uid')),
|
||||
'uploader_url': format_field(username, None, 'https://trovo.live/%s'),
|
||||
}
|
||||
|
||||
|
||||
class TrovoIE(TrovoBaseIE):
|
||||
_VALID_URL = TrovoBaseIE._VALID_URL_BASE + r'(?:s/)?(?!(?:clip|video)/)(?P<id>(?!s/)[^/?&#]+(?![^#]+[?&]vid=))'
|
||||
_TESTS = [{
|
||||
'url': 'https://trovo.live/Exsl',
|
||||
'only_matching': True,
|
||||
}, {
|
||||
'url': 'https://trovo.live/s/SkenonSLive/549759191497',
|
||||
'only_matching': True,
|
||||
}, {
|
||||
'url': 'https://trovo.live/s/zijo987/208251706',
|
||||
'info_dict': {
|
||||
'id': '104125853_104125853_1656439572',
|
||||
'ext': 'flv',
|
||||
'uploader_url': 'https://trovo.live/zijo987',
|
||||
'uploader_id': '104125853',
|
||||
'thumbnail': 'https://livecover.trovo.live/screenshot/73846_104125853_104125853-2022-06-29-04-00-22-852x480.jpg',
|
||||
'uploader': 'zijo987',
|
||||
'title': '💥IGRAMO IGRICE UPADAJTE💥2500/5000 2022-06-28 22:01',
|
||||
'live_status': 'is_live',
|
||||
},
|
||||
'skip': 'May not be live',
|
||||
}]
|
||||
|
||||
def _real_extract(self, url):
|
||||
username = self._match_id(url)
|
||||
live_info = self._call_api(username, data={
|
||||
'operationName': 'live_LiveReaderService_GetLiveInfo',
|
||||
'variables': {
|
||||
'params': {
|
||||
'userName': username,
|
||||
},
|
||||
},
|
||||
})
|
||||
if live_info.get('isLive') == 0:
|
||||
raise ExtractorError(f'{username} is offline', expected=True)
|
||||
program_info = live_info['programInfo']
|
||||
program_id = program_info['id']
|
||||
title = program_info['title']
|
||||
|
||||
formats = []
|
||||
for stream_info in (program_info.get('streamInfo') or []):
|
||||
play_url = stream_info.get('playUrl')
|
||||
if not play_url:
|
||||
continue
|
||||
format_id = stream_info.get('desc')
|
||||
formats.append({
|
||||
'format_id': format_id,
|
||||
'height': int_or_none(format_id[:-1]) if format_id else None,
|
||||
'url': play_url,
|
||||
'tbr': stream_info.get('bitrate'),
|
||||
'http_headers': self._HEADERS,
|
||||
})
|
||||
|
||||
info = {
|
||||
'id': program_id,
|
||||
'title': title,
|
||||
'formats': formats,
|
||||
'thumbnail': program_info.get('coverUrl'),
|
||||
'is_live': True,
|
||||
}
|
||||
info.update(self._extract_streamer_info(live_info))
|
||||
return info
|
||||
|
||||
|
||||
class TrovoVodIE(TrovoBaseIE):
|
||||
_VALID_URL = TrovoBaseIE._VALID_URL_BASE + r'(?:clip|video|s)/(?:[^/]+/\d+[^#]*[?&]vid=)?(?P<id>(?<!/s/)[^/?&#]+)'
|
||||
_TESTS = [{
|
||||
'url': 'https://trovo.live/clip/lc-5285890818705062210?ltab=videos',
|
||||
'params': {'getcomments': True},
|
||||
'info_dict': {
|
||||
'id': 'lc-5285890818705062210',
|
||||
'ext': 'mp4',
|
||||
'title': 'fatal moaning for a super good🤣🤣',
|
||||
'uploader': 'OneTappedYou',
|
||||
'timestamp': 1621628019,
|
||||
'upload_date': '20210521',
|
||||
'uploader_id': '100719456',
|
||||
'duration': 31,
|
||||
'view_count': int,
|
||||
'like_count': int,
|
||||
'comment_count': int,
|
||||
'comments': 'mincount:1',
|
||||
'categories': ['Call of Duty: Mobile'],
|
||||
'uploader_url': 'https://trovo.live/OneTappedYou',
|
||||
'thumbnail': r're:^https?://.*\.jpg',
|
||||
},
|
||||
}, {
|
||||
'url': 'https://trovo.live/s/SkenonSLive/549759191497?vid=ltv-100829718_100829718_387702301737980280',
|
||||
'info_dict': {
|
||||
'id': 'ltv-100829718_100829718_387702301737980280',
|
||||
'ext': 'mp4',
|
||||
'timestamp': 1654909624,
|
||||
'thumbnail': 'http://vod.trovo.live/1f09baf0vodtransger1301120758/ef9ea3f0387702301737980280/coverBySnapshot/coverBySnapshot_10_0.jpg',
|
||||
'uploader_id': '100829718',
|
||||
'uploader': 'SkenonSLive',
|
||||
'title': 'Trovo u secanju, uz par modova i muzike :)',
|
||||
'uploader_url': 'https://trovo.live/SkenonSLive',
|
||||
'duration': 10830,
|
||||
'view_count': int,
|
||||
'like_count': int,
|
||||
'upload_date': '20220611',
|
||||
'comment_count': int,
|
||||
'categories': ['Minecraft'],
|
||||
},
|
||||
'skip': 'Not available',
|
||||
}, {
|
||||
'url': 'https://trovo.live/s/Trovo/549756886599?vid=ltv-100264059_100264059_387702304241698583',
|
||||
'info_dict': {
|
||||
'id': 'ltv-100264059_100264059_387702304241698583',
|
||||
'ext': 'mp4',
|
||||
'timestamp': 1661479563,
|
||||
'thumbnail': 'http://vod.trovo.live/be5ae591vodtransusw1301120758/cccb9915387702304241698583/coverBySnapshot/coverBySnapshot_10_0.jpg',
|
||||
'uploader_id': '100264059',
|
||||
'uploader': 'Trovo',
|
||||
'title': 'Dev Corner 8/25',
|
||||
'uploader_url': 'https://trovo.live/Trovo',
|
||||
'duration': 3753,
|
||||
'view_count': int,
|
||||
'like_count': int,
|
||||
'upload_date': '20220826',
|
||||
'comment_count': int,
|
||||
'categories': ['Talk Shows'],
|
||||
},
|
||||
}, {
|
||||
'url': 'https://trovo.live/video/ltv-100095501_100095501_1609596043',
|
||||
'only_matching': True,
|
||||
}, {
|
||||
'url': 'https://trovo.live/s/SkenonSLive/549759191497?foo=bar&vid=ltv-100829718_100829718_387702301737980280',
|
||||
'only_matching': True,
|
||||
}]
|
||||
|
||||
def _real_extract(self, url):
|
||||
vid = self._match_id(url)
|
||||
|
||||
# NOTE: It is also possible to extract this info from the Nuxt data on the website,
|
||||
# however that seems unreliable - sometimes it randomly doesn't return the data,
|
||||
# at least when using a non-residential IP.
|
||||
resp = self._call_api(vid, data={
|
||||
'operationName': 'vod_VodReaderService_BatchGetVodDetailInfo',
|
||||
'variables': {
|
||||
'params': {
|
||||
'vids': [vid],
|
||||
},
|
||||
},
|
||||
'extensions': {},
|
||||
})
|
||||
|
||||
vod_detail_info = traverse_obj(resp, ('VodDetailInfos', vid), expected_type=dict)
|
||||
if not vod_detail_info:
|
||||
raise ExtractorError('This video not found or not available anymore', expected=True)
|
||||
vod_info = vod_detail_info.get('vodInfo')
|
||||
title = vod_info.get('title')
|
||||
|
||||
if try_get(vod_info, lambda x: x['playbackRights']['playbackRights'] != 'Normal'):
|
||||
playback_rights_setting = vod_info['playbackRights']['playbackRightsSetting']
|
||||
if playback_rights_setting == 'SubscriberOnly':
|
||||
raise ExtractorError('This video is only available for subscribers', expected=True)
|
||||
else:
|
||||
raise ExtractorError(f'This video is not available ({playback_rights_setting})', expected=True)
|
||||
|
||||
language = vod_info.get('languageName')
|
||||
formats = []
|
||||
for play_info in (vod_info.get('playInfos') or []):
|
||||
play_url = play_info.get('playUrl')
|
||||
if not play_url:
|
||||
continue
|
||||
format_id = play_info.get('desc')
|
||||
formats.append({
|
||||
'ext': 'mp4',
|
||||
'filesize': int_or_none(play_info.get('fileSize')),
|
||||
'format_id': format_id,
|
||||
'height': int_or_none(format_id[:-1]) if format_id else None,
|
||||
'language': language,
|
||||
'protocol': 'm3u8_native',
|
||||
'tbr': int_or_none(play_info.get('bitrate')),
|
||||
'url': play_url,
|
||||
'http_headers': self._HEADERS,
|
||||
})
|
||||
|
||||
category = vod_info.get('categoryName')
|
||||
get_count = lambda x: int_or_none(vod_info.get(x + 'Num'))
|
||||
|
||||
info = {
|
||||
'id': vid,
|
||||
'title': title,
|
||||
'formats': formats,
|
||||
'thumbnail': vod_info.get('coverUrl'),
|
||||
'timestamp': int_or_none(vod_info.get('publishTs')),
|
||||
'duration': int_or_none(vod_info.get('duration')),
|
||||
'view_count': get_count('watch'),
|
||||
'like_count': get_count('like'),
|
||||
'comment_count': get_count('comment'),
|
||||
'categories': [category] if category else None,
|
||||
'__post_extractor': self.extract_comments(vid),
|
||||
}
|
||||
info.update(self._extract_streamer_info(vod_detail_info))
|
||||
return info
|
||||
|
||||
def _get_comments(self, vid):
|
||||
for page in itertools.count(1):
|
||||
comments_json = self._call_api(vid, data={
|
||||
'operationName': 'public_CommentProxyService_GetCommentList',
|
||||
'variables': {
|
||||
'params': {
|
||||
'appInfo': {
|
||||
'postID': vid,
|
||||
},
|
||||
'preview': {},
|
||||
'pageSize': 99,
|
||||
'page': page,
|
||||
},
|
||||
},
|
||||
'extensions': {
|
||||
'singleReq': 'true',
|
||||
},
|
||||
})
|
||||
for comment in comments_json['commentList']:
|
||||
content = comment.get('content')
|
||||
if not content:
|
||||
continue
|
||||
author = comment.get('author') or {}
|
||||
parent = comment.get('parentID')
|
||||
yield {
|
||||
'author': author.get('nickName'),
|
||||
'author_id': str_or_none(author.get('uid')),
|
||||
'id': str_or_none(comment.get('commentID')),
|
||||
'text': content,
|
||||
'timestamp': int_or_none(comment.get('createdAt')),
|
||||
'parent': 'root' if parent == 0 else str_or_none(parent),
|
||||
}
|
||||
|
||||
if comments_json['lastPage']:
|
||||
break
|
||||
|
||||
|
||||
class TrovoChannelBaseIE(TrovoBaseIE):
|
||||
def _entries(self, spacename):
|
||||
for page in itertools.count(1):
|
||||
vod_json = self._call_api(spacename, data={
|
||||
'operationName': self._OPERATION,
|
||||
'variables': {
|
||||
'params': {
|
||||
'terminalSpaceID': {
|
||||
'spaceName': spacename,
|
||||
},
|
||||
'currPage': page,
|
||||
'pageSize': 99,
|
||||
},
|
||||
},
|
||||
'extensions': {
|
||||
'singleReq': 'true',
|
||||
},
|
||||
})
|
||||
vods = vod_json.get('vodInfos', [])
|
||||
for vod in vods:
|
||||
vid = vod.get('vid')
|
||||
room = traverse_obj(vod, ('spaceInfo', 'roomID'))
|
||||
yield self.url_result(
|
||||
f'https://trovo.live/s/{spacename}/{room}?vid={vid}',
|
||||
ie=TrovoVodIE.ie_key())
|
||||
has_more = vod_json.get('hasMore')
|
||||
if not has_more:
|
||||
break
|
||||
|
||||
def _real_extract(self, url):
|
||||
spacename = self._match_id(url)
|
||||
return self.playlist_result(self._entries(spacename), playlist_id=spacename)
|
||||
|
||||
|
||||
class TrovoChannelVodIE(TrovoChannelBaseIE):
|
||||
_VALID_URL = r'trovovod:(?P<id>[^\s]+)'
|
||||
IE_DESC = 'All VODs of a trovo.live channel; "trovovod:" prefix'
|
||||
|
||||
_TESTS = [{
|
||||
'url': 'trovovod:OneTappedYou',
|
||||
'playlist_mincount': 24,
|
||||
'info_dict': {
|
||||
'id': 'OneTappedYou',
|
||||
},
|
||||
}]
|
||||
|
||||
_OPERATION = 'vod_VodReaderService_GetChannelLtvVideoInfos'
|
||||
|
||||
|
||||
class TrovoChannelClipIE(TrovoChannelBaseIE):
|
||||
_VALID_URL = r'trovoclip:(?P<id>[^\s]+)'
|
||||
IE_DESC = 'All Clips of a trovo.live channel; "trovoclip:" prefix'
|
||||
|
||||
_TESTS = [{
|
||||
'url': 'trovoclip:OneTappedYou',
|
||||
'playlist_mincount': 29,
|
||||
'info_dict': {
|
||||
'id': 'OneTappedYou',
|
||||
},
|
||||
}]
|
||||
|
||||
_OPERATION = 'vod_VodReaderService_GetChannelClipVideoInfos'
|
||||
Reference in New Issue
Block a user