mirror of
https://github.com/yt-dlp/yt-dlp.git
synced 2026-05-01 04:56:13 +00:00
Compare commits
4 Commits
201812100f
...
3fe72e9eea
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3fe72e9eea | ||
|
|
d30a49742c | ||
|
|
6d265388c6 | ||
|
|
a9b3700698 |
2
.gitignore
vendored
2
.gitignore
vendored
@ -105,6 +105,8 @@ README.txt
|
|||||||
*.zsh
|
*.zsh
|
||||||
*.spec
|
*.spec
|
||||||
test/testdata/sigs/player-*.js
|
test/testdata/sigs/player-*.js
|
||||||
|
test/testdata/thumbnails/empty.webp
|
||||||
|
test/testdata/thumbnails/foo\ %d\ bar/foo_%d.*
|
||||||
|
|
||||||
# Binary
|
# Binary
|
||||||
/youtube-dl
|
/youtube-dl
|
||||||
|
|||||||
5
Makefile
5
Makefile
@ -18,10 +18,11 @@ pypi-files: AUTHORS Changelog.md LICENSE README.md README.txt supportedsites \
|
|||||||
tar pypi-files lazy-extractors install uninstall
|
tar pypi-files lazy-extractors install uninstall
|
||||||
|
|
||||||
clean-test:
|
clean-test:
|
||||||
rm -rf test/testdata/sigs/player-*.js tmp/ *.annotations.xml *.aria2 *.description *.dump *.frag \
|
rm -rf tmp/ *.annotations.xml *.aria2 *.description *.dump *.frag \
|
||||||
*.frag.aria2 *.frag.urls *.info.json *.live_chat.json *.meta *.part* *.tmp *.temp *.unknown_video *.ytdl \
|
*.frag.aria2 *.frag.urls *.info.json *.live_chat.json *.meta *.part* *.tmp *.temp *.unknown_video *.ytdl \
|
||||||
*.3gp *.ape *.ass *.avi *.desktop *.f4v *.flac *.flv *.gif *.jpeg *.jpg *.lrc *.m4a *.m4v *.mhtml *.mkv *.mov *.mp3 *.mp4 \
|
*.3gp *.ape *.ass *.avi *.desktop *.f4v *.flac *.flv *.gif *.jpeg *.jpg *.lrc *.m4a *.m4v *.mhtml *.mkv *.mov *.mp3 *.mp4 \
|
||||||
*.mpg *.mpga *.oga *.ogg *.opus *.png *.sbv *.srt *.ssa *.swf *.tt *.ttml *.url *.vtt *.wav *.webloc *.webm *.webp
|
*.mpg *.mpga *.oga *.ogg *.opus *.png *.sbv *.srt *.ssa *.swf *.tt *.ttml *.url *.vtt *.wav *.webloc *.webm *.webp \
|
||||||
|
test/testdata/sigs/player-*.js test/testdata/thumbnails/empty.webp "test/testdata/thumbnails/foo %d bar/foo_%d."*
|
||||||
clean-dist:
|
clean-dist:
|
||||||
rm -rf yt-dlp.1.temp.md yt-dlp.1 README.txt MANIFEST build/ dist/ .coverage cover/ yt-dlp.tar.gz completions/ \
|
rm -rf yt-dlp.1.temp.md yt-dlp.1 README.txt MANIFEST build/ dist/ .coverage cover/ yt-dlp.tar.gz completions/ \
|
||||||
yt_dlp/extractor/lazy_extractors.py *.spec CONTRIBUTING.md.tmp yt-dlp yt-dlp.exe yt_dlp.egg-info/ AUTHORS
|
yt_dlp/extractor/lazy_extractors.py *.spec CONTRIBUTING.md.tmp yt-dlp yt-dlp.exe yt_dlp.egg-info/ AUTHORS
|
||||||
|
|||||||
@ -8,6 +8,8 @@ import unittest
|
|||||||
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||||
|
|
||||||
|
|
||||||
|
import subprocess
|
||||||
|
|
||||||
from yt_dlp import YoutubeDL
|
from yt_dlp import YoutubeDL
|
||||||
from yt_dlp.utils import shell_quote
|
from yt_dlp.utils import shell_quote
|
||||||
from yt_dlp.postprocessor import (
|
from yt_dlp.postprocessor import (
|
||||||
@ -47,7 +49,18 @@ class TestConvertThumbnail(unittest.TestCase):
|
|||||||
print('Skipping: ffmpeg not found')
|
print('Skipping: ffmpeg not found')
|
||||||
return
|
return
|
||||||
|
|
||||||
file = 'test/testdata/thumbnails/foo %d bar/foo_%d.{}'
|
test_data_dir = 'test/testdata/thumbnails'
|
||||||
|
generated_file = f'{test_data_dir}/empty.webp'
|
||||||
|
|
||||||
|
subprocess.check_call([
|
||||||
|
pp.executable, '-y', '-f', 'lavfi', '-i', 'color=c=black:s=320x320',
|
||||||
|
'-c:v', 'libwebp', '-pix_fmt', 'yuv420p', '-vframes', '1', generated_file,
|
||||||
|
], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
||||||
|
|
||||||
|
file = test_data_dir + '/foo %d bar/foo_%d.{}'
|
||||||
|
initial_file = file.format('webp')
|
||||||
|
os.replace(generated_file, initial_file)
|
||||||
|
|
||||||
tests = (('webp', 'png'), ('png', 'jpg'))
|
tests = (('webp', 'png'), ('png', 'jpg'))
|
||||||
|
|
||||||
for inp, out in tests:
|
for inp, out in tests:
|
||||||
@ -55,11 +68,13 @@ class TestConvertThumbnail(unittest.TestCase):
|
|||||||
if os.path.exists(out_file):
|
if os.path.exists(out_file):
|
||||||
os.remove(out_file)
|
os.remove(out_file)
|
||||||
pp.convert_thumbnail(file.format(inp), out)
|
pp.convert_thumbnail(file.format(inp), out)
|
||||||
assert os.path.exists(out_file)
|
self.assertTrue(os.path.exists(out_file))
|
||||||
|
|
||||||
for _, out in tests:
|
for _, out in tests:
|
||||||
os.remove(file.format(out))
|
os.remove(file.format(out))
|
||||||
|
|
||||||
|
os.remove(initial_file)
|
||||||
|
|
||||||
|
|
||||||
class TestExec(unittest.TestCase):
|
class TestExec(unittest.TestCase):
|
||||||
def test_parse_cmd(self):
|
def test_parse_cmd(self):
|
||||||
@ -610,3 +625,7 @@ outpoint 10.000000
|
|||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
r"'special '\'' characters '\'' galore'\'\'\'",
|
r"'special '\'' characters '\'' galore'\'\'\'",
|
||||||
self._pp._quote_for_ffmpeg("special ' characters ' galore'''"))
|
self._pp._quote_for_ffmpeg("special ' characters ' galore'''"))
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
unittest.main()
|
||||||
|
|||||||
BIN
test/testdata/thumbnails/foo %d bar/foo_%d.webp
vendored
BIN
test/testdata/thumbnails/foo %d bar/foo_%d.webp
vendored
Binary file not shown.
|
Before Width: | Height: | Size: 3.8 KiB |
0
test/testdata/thumbnails/foo %d bar/placeholder
vendored
Normal file
0
test/testdata/thumbnails/foo %d bar/placeholder
vendored
Normal file
@ -6,32 +6,32 @@ from ..utils import int_or_none, traverse_obj, url_or_none, urljoin
|
|||||||
|
|
||||||
|
|
||||||
class TenPlayIE(InfoExtractor):
|
class TenPlayIE(InfoExtractor):
|
||||||
_VALID_URL = r'https?://(?:www\.)?10play\.com\.au/(?:[^/]+/)+(?P<id>tpv\d{6}[a-z]{5})'
|
IE_NAME = '10play'
|
||||||
|
_VALID_URL = r'https?://(?:www\.)?10play\.com\.au/(?:[^/?#]+/)+(?P<id>tpv\d{6}[a-z]{5})'
|
||||||
_NETRC_MACHINE = '10play'
|
_NETRC_MACHINE = '10play'
|
||||||
_TESTS = [{
|
_TESTS = [{
|
||||||
'url': 'https://10play.com.au/neighbours/web-extras/season-41/heres-a-first-look-at-mischa-bartons-neighbours-debut/tpv230911hyxnz',
|
# Geo-restricted to Australia
|
||||||
|
'url': 'https://10play.com.au/australian-survivor/web-extras/season-10-brains-v-brawn-ii/myless-journey/tpv250414jdmtf',
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
'id': '6336940246112',
|
'id': '7440980000013868',
|
||||||
'ext': 'mp4',
|
'ext': 'mp4',
|
||||||
'title': 'Here\'s A First Look At Mischa Barton\'s Neighbours Debut',
|
'title': 'Myles\'s Journey',
|
||||||
'alt_title': 'Here\'s A First Look At Mischa Barton\'s Neighbours Debut',
|
'alt_title': 'Myles\'s Journey',
|
||||||
'description': 'Neighbours Premieres Monday, September 18 At 4:30pm On 10 And 10 Play And 6:30pm On 10 Peach',
|
'description': 'Relive Myles\'s epic Brains V Brawn II journey to reach the game\'s final two',
|
||||||
'duration': 74,
|
|
||||||
'season': 'Season 41',
|
|
||||||
'season_number': 41,
|
|
||||||
'series': 'Neighbours',
|
|
||||||
'thumbnail': r're:https://.*\.jpg',
|
|
||||||
'uploader': 'Channel 10',
|
'uploader': 'Channel 10',
|
||||||
'age_limit': 15,
|
|
||||||
'timestamp': 1694386800,
|
|
||||||
'upload_date': '20230910',
|
|
||||||
'uploader_id': '2199827728001',
|
'uploader_id': '2199827728001',
|
||||||
|
'age_limit': 15,
|
||||||
|
'duration': 249,
|
||||||
|
'thumbnail': r're:https://.+/.+\.jpg',
|
||||||
|
'series': 'Australian Survivor',
|
||||||
|
'season': 'Season 10',
|
||||||
|
'season_number': 10,
|
||||||
|
'timestamp': 1744629420,
|
||||||
|
'upload_date': '20250414',
|
||||||
},
|
},
|
||||||
'params': {
|
'params': {'skip_download': 'm3u8'},
|
||||||
'skip_download': True,
|
|
||||||
},
|
|
||||||
'skip': 'Only available in Australia',
|
|
||||||
}, {
|
}, {
|
||||||
|
# Geo-restricted to Australia
|
||||||
'url': 'https://10play.com.au/neighbours/episodes/season-42/episode-9107/tpv240902nzqyp',
|
'url': 'https://10play.com.au/neighbours/episodes/season-42/episode-9107/tpv240902nzqyp',
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
'id': '9000000000091177',
|
'id': '9000000000091177',
|
||||||
@ -45,17 +45,38 @@ class TenPlayIE(InfoExtractor):
|
|||||||
'season': 'Season 42',
|
'season': 'Season 42',
|
||||||
'season_number': 42,
|
'season_number': 42,
|
||||||
'series': 'Neighbours',
|
'series': 'Neighbours',
|
||||||
'thumbnail': r're:https://.*\.jpg',
|
'thumbnail': r're:https://.+/.+\.jpg',
|
||||||
'age_limit': 15,
|
'age_limit': 15,
|
||||||
'timestamp': 1725517860,
|
'timestamp': 1725517860,
|
||||||
'upload_date': '20240905',
|
'upload_date': '20240905',
|
||||||
'uploader': 'Channel 10',
|
'uploader': 'Channel 10',
|
||||||
'uploader_id': '2199827728001',
|
'uploader_id': '2199827728001',
|
||||||
},
|
},
|
||||||
'params': {
|
'params': {'skip_download': 'm3u8'},
|
||||||
'skip_download': True,
|
}, {
|
||||||
|
# Geo-restricted to Australia; upgrading the m3u8 quality fails and we need the fallback
|
||||||
|
'url': 'https://10play.com.au/tiny-chef-show/episodes/season-1/episode-2/tpv240228pofvt',
|
||||||
|
'info_dict': {
|
||||||
|
'id': '9000000000084116',
|
||||||
|
'ext': 'mp4',
|
||||||
|
'uploader': 'Channel 10',
|
||||||
|
'uploader_id': '2199827728001',
|
||||||
|
'duration': 1297,
|
||||||
|
'title': 'The Tiny Chef Show - S1 Ep. 2',
|
||||||
|
'alt_title': 'S1 Ep. 2 - Popcorn/banana',
|
||||||
|
'description': 'md5:d4758b52b5375dfaa67a78261dcb5763',
|
||||||
|
'age_limit': 0,
|
||||||
|
'series': 'The Tiny Chef Show',
|
||||||
|
'season_number': 1,
|
||||||
|
'episode_number': 2,
|
||||||
|
'timestamp': 1747957740,
|
||||||
|
'thumbnail': r're:https://.+/.+\.jpg',
|
||||||
|
'upload_date': '20250522',
|
||||||
|
'season': 'Season 1',
|
||||||
|
'episode': 'Episode 2',
|
||||||
},
|
},
|
||||||
'skip': 'Only available in Australia',
|
'params': {'skip_download': 'm3u8'},
|
||||||
|
'expected_warnings': ['Failed to download m3u8 information: HTTP Error 502'],
|
||||||
}, {
|
}, {
|
||||||
'url': 'https://10play.com.au/how-to-stay-married/web-extras/season-1/terrys-talks-ep-1-embracing-change/tpv190915ylupc',
|
'url': 'https://10play.com.au/how-to-stay-married/web-extras/season-1/terrys-talks-ep-1-embracing-change/tpv190915ylupc',
|
||||||
'only_matching': True,
|
'only_matching': True,
|
||||||
@ -86,8 +107,11 @@ class TenPlayIE(InfoExtractor):
|
|||||||
if '10play-not-in-oz' in m3u8_url:
|
if '10play-not-in-oz' in m3u8_url:
|
||||||
self.raise_geo_restricted(countries=['AU'])
|
self.raise_geo_restricted(countries=['AU'])
|
||||||
# Attempt to get a higher quality stream
|
# Attempt to get a higher quality stream
|
||||||
m3u8_url = m3u8_url.replace(',150,75,55,0000', ',300,150,75,55,0000')
|
formats = self._extract_m3u8_formats(
|
||||||
formats = self._extract_m3u8_formats(m3u8_url, content_id, 'mp4')
|
m3u8_url.replace(',150,75,55,0000', ',300,150,75,55,0000'),
|
||||||
|
content_id, 'mp4', fatal=False)
|
||||||
|
if not formats:
|
||||||
|
formats = self._extract_m3u8_formats(m3u8_url, content_id, 'mp4')
|
||||||
|
|
||||||
return {
|
return {
|
||||||
'id': content_id,
|
'id': content_id,
|
||||||
@ -112,21 +136,22 @@ class TenPlayIE(InfoExtractor):
|
|||||||
|
|
||||||
|
|
||||||
class TenPlaySeasonIE(InfoExtractor):
|
class TenPlaySeasonIE(InfoExtractor):
|
||||||
|
IE_NAME = '10play:season'
|
||||||
_VALID_URL = r'https?://(?:www\.)?10play\.com\.au/(?P<show>[^/?#]+)/episodes/(?P<season>[^/?#]+)/?(?:$|[?#])'
|
_VALID_URL = r'https?://(?:www\.)?10play\.com\.au/(?P<show>[^/?#]+)/episodes/(?P<season>[^/?#]+)/?(?:$|[?#])'
|
||||||
_TESTS = [{
|
_TESTS = [{
|
||||||
'url': 'https://10play.com.au/masterchef/episodes/season-14',
|
'url': 'https://10play.com.au/masterchef/episodes/season-15',
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
'title': 'Season 14',
|
'title': 'Season 15',
|
||||||
'id': 'MjMyOTIy',
|
'id': 'MTQ2NjMxOQ==',
|
||||||
},
|
},
|
||||||
'playlist_mincount': 64,
|
'playlist_mincount': 50,
|
||||||
}, {
|
}, {
|
||||||
'url': 'https://10play.com.au/the-bold-and-the-beautiful-fast-tracked/episodes/season-2022',
|
'url': 'https://10play.com.au/the-bold-and-the-beautiful-fast-tracked/episodes/season-2024',
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
'title': 'Season 2022',
|
'title': 'Season 2024',
|
||||||
'id': 'Mjc0OTIw',
|
'id': 'Mjc0OTIw',
|
||||||
},
|
},
|
||||||
'playlist_mincount': 256,
|
'playlist_mincount': 159,
|
||||||
}]
|
}]
|
||||||
|
|
||||||
def _entries(self, load_more_url, display_id=None):
|
def _entries(self, load_more_url, display_id=None):
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
import base64
|
import base64
|
||||||
|
import functools
|
||||||
import hashlib
|
import hashlib
|
||||||
import hmac
|
import hmac
|
||||||
import itertools
|
import itertools
|
||||||
@ -17,99 +18,227 @@ from ..utils import (
|
|||||||
UserNotLive,
|
UserNotLive,
|
||||||
float_or_none,
|
float_or_none,
|
||||||
int_or_none,
|
int_or_none,
|
||||||
|
join_nonempty,
|
||||||
|
jwt_decode_hs256,
|
||||||
str_or_none,
|
str_or_none,
|
||||||
traverse_obj,
|
|
||||||
try_call,
|
try_call,
|
||||||
update_url_query,
|
update_url_query,
|
||||||
url_or_none,
|
url_or_none,
|
||||||
)
|
)
|
||||||
|
from ..utils.traversal import require, traverse_obj
|
||||||
|
|
||||||
|
|
||||||
class WeverseBaseIE(InfoExtractor):
|
class WeverseBaseIE(InfoExtractor):
|
||||||
_NETRC_MACHINE = 'weverse'
|
_NETRC_MACHINE = 'weverse'
|
||||||
_ACCOUNT_API_BASE = 'https://accountapi.weverse.io/web/api'
|
_ACCOUNT_API_BASE = 'https://accountapi.weverse.io'
|
||||||
|
_CLIENT_PLATFORM = 'WEB'
|
||||||
|
_SIGNING_KEY = b'1b9cb6378d959b45714bec49971ade22e6e24e42'
|
||||||
|
_ACCESS_TOKEN_KEY = 'we2_access_token'
|
||||||
|
_REFRESH_TOKEN_KEY = 'we2_refresh_token'
|
||||||
|
_DEVICE_ID_KEY = 'we2_device_id'
|
||||||
_API_HEADERS = {
|
_API_HEADERS = {
|
||||||
'Accept': 'application/json',
|
'Accept': 'application/json',
|
||||||
|
'Origin': 'https://weverse.io',
|
||||||
'Referer': 'https://weverse.io/',
|
'Referer': 'https://weverse.io/',
|
||||||
'WEV-device-Id': str(uuid.uuid4()),
|
|
||||||
}
|
}
|
||||||
|
_LOGIN_HINT_TMPL = (
|
||||||
|
'You can log in using your refresh token with --username "{}" --password "REFRESH_TOKEN" '
|
||||||
|
'(replace REFRESH_TOKEN with the actual value of the "{}" cookie found in your web browser). '
|
||||||
|
'You can add an optional username suffix, e.g. --username "{}" , '
|
||||||
|
'if you need to manage multiple accounts. ')
|
||||||
|
_LOGIN_ERRORS_MAP = {
|
||||||
|
'login_required': 'This content is only available for logged-in users. ',
|
||||||
|
'invalid_username': '"{}" is not valid login username for this extractor. ',
|
||||||
|
'invalid_password': (
|
||||||
|
'Your password is not a valid refresh token. Make sure that '
|
||||||
|
'you are passing the refresh token, and NOT the access token. '),
|
||||||
|
'no_refresh_token': (
|
||||||
|
'Your access token has expired and there is no refresh token available. '
|
||||||
|
'Refresh your session/cookies in the web browser and try again. '),
|
||||||
|
'expired_refresh_token': (
|
||||||
|
'Your refresh token has expired. Log in to the site again using '
|
||||||
|
'your web browser to get a new refresh token or export fresh cookies. '),
|
||||||
|
}
|
||||||
|
_OAUTH_PREFIX = 'oauth'
|
||||||
|
_oauth_tokens = {}
|
||||||
|
_device_id = None
|
||||||
|
|
||||||
def _perform_login(self, username, password):
|
@property
|
||||||
if self._API_HEADERS.get('Authorization'):
|
def _oauth_headers(self):
|
||||||
return
|
return {
|
||||||
|
**self._API_HEADERS,
|
||||||
headers = {
|
'X-ACC-APP-SECRET': '5419526f1c624b38b10787e5c10b2a7a',
|
||||||
'x-acc-app-secret': '5419526f1c624b38b10787e5c10b2a7a',
|
'X-ACC-SERVICE-ID': 'weverse',
|
||||||
'x-acc-app-version': '3.3.6',
|
'X-ACC-TRACE-ID': str(uuid.uuid4()),
|
||||||
'x-acc-language': 'en',
|
|
||||||
'x-acc-service-id': 'weverse',
|
|
||||||
'x-acc-trace-id': str(uuid.uuid4()),
|
|
||||||
'x-clog-user-device-id': str(uuid.uuid4()),
|
|
||||||
}
|
}
|
||||||
valid_username = traverse_obj(self._download_json(
|
|
||||||
f'{self._ACCOUNT_API_BASE}/v2/signup/email/status', None, note='Checking username',
|
|
||||||
query={'email': username}, headers=headers, expected_status=(400, 404)), 'hasPassword')
|
|
||||||
if not valid_username:
|
|
||||||
raise ExtractorError('Invalid username provided', expected=True)
|
|
||||||
|
|
||||||
headers['content-type'] = 'application/json'
|
@functools.cached_property
|
||||||
|
def _oauth_cache_key(self):
|
||||||
|
username = self._get_login_info()[0]
|
||||||
|
if not username:
|
||||||
|
return 'cookies'
|
||||||
|
return join_nonempty(self._OAUTH_PREFIX, username.partition('+')[2])
|
||||||
|
|
||||||
|
@property
|
||||||
|
def _is_logged_in(self):
|
||||||
|
return bool(self._oauth_tokens.get(self._ACCESS_TOKEN_KEY))
|
||||||
|
|
||||||
|
def _access_token_is_valid(self):
|
||||||
|
response = self._download_json(
|
||||||
|
f'{self._ACCOUNT_API_BASE}/api/v1/token/validate', None,
|
||||||
|
'Validating access token', 'Unable to valid access token',
|
||||||
|
expected_status=401, headers={
|
||||||
|
**self._oauth_headers,
|
||||||
|
'Authorization': f'Bearer {self._oauth_tokens[self._ACCESS_TOKEN_KEY]}',
|
||||||
|
})
|
||||||
|
return traverse_obj(response, ('expiresIn', {int}), default=0) > 60
|
||||||
|
|
||||||
|
def _token_is_expired(self, key):
|
||||||
|
is_expired = jwt_decode_hs256(self._oauth_tokens[key])['exp'] - time.time() < 3600
|
||||||
|
if key == self._REFRESH_TOKEN_KEY or not is_expired:
|
||||||
|
return is_expired
|
||||||
|
return not self._access_token_is_valid()
|
||||||
|
|
||||||
|
def _refresh_access_token(self):
|
||||||
|
if not self._oauth_tokens.get(self._REFRESH_TOKEN_KEY):
|
||||||
|
self._report_login_error('no_refresh_token')
|
||||||
|
if self._token_is_expired(self._REFRESH_TOKEN_KEY):
|
||||||
|
self._report_login_error('expired_refresh_token')
|
||||||
|
|
||||||
|
headers = {'Content-Type': 'application/json'}
|
||||||
|
if self._is_logged_in:
|
||||||
|
headers['Authorization'] = f'Bearer {self._oauth_tokens[self._ACCESS_TOKEN_KEY]}'
|
||||||
|
|
||||||
try:
|
try:
|
||||||
auth = self._download_json(
|
response = self._download_json(
|
||||||
f'{self._ACCOUNT_API_BASE}/v3/auth/token/by-credentials', None, data=json.dumps({
|
f'{self._ACCOUNT_API_BASE}/api/v1/token/refresh', None,
|
||||||
'email': username,
|
'Refreshing access token', 'Unable to refresh access token',
|
||||||
'otpSessionId': 'BY_PASS',
|
headers={**self._oauth_headers, **headers},
|
||||||
'password': password,
|
data=json.dumps({
|
||||||
}, separators=(',', ':')).encode(), headers=headers, note='Logging in')
|
'refreshToken': self._oauth_tokens[self._REFRESH_TOKEN_KEY],
|
||||||
|
}, separators=(',', ':')).encode())
|
||||||
except ExtractorError as e:
|
except ExtractorError as e:
|
||||||
if isinstance(e.cause, HTTPError) and e.cause.status == 401:
|
if isinstance(e.cause, HTTPError) and e.cause.status == 401:
|
||||||
raise ExtractorError('Invalid password provided', expected=True)
|
self._oauth_tokens.clear()
|
||||||
|
if self._oauth_cache_key == 'cookies':
|
||||||
|
self.cookiejar.clear(domain='.weverse.io', path='/', name=self._ACCESS_TOKEN_KEY)
|
||||||
|
self.cookiejar.clear(domain='.weverse.io', path='/', name=self._REFRESH_TOKEN_KEY)
|
||||||
|
else:
|
||||||
|
self.cache.store(self._NETRC_MACHINE, self._oauth_cache_key, self._oauth_tokens)
|
||||||
|
self._report_login_error('expired_refresh_token')
|
||||||
raise
|
raise
|
||||||
|
|
||||||
WeverseBaseIE._API_HEADERS['Authorization'] = f'Bearer {auth["accessToken"]}'
|
self._oauth_tokens.update(traverse_obj(response, {
|
||||||
|
self._ACCESS_TOKEN_KEY: ('accessToken', {str}, {require('access token')}),
|
||||||
|
self._REFRESH_TOKEN_KEY: ('refreshToken', {str}, {require('refresh token')}),
|
||||||
|
}))
|
||||||
|
|
||||||
def _real_initialize(self):
|
if self._oauth_cache_key == 'cookies':
|
||||||
if self._API_HEADERS.get('Authorization'):
|
self._set_cookie('.weverse.io', self._ACCESS_TOKEN_KEY, self._oauth_tokens[self._ACCESS_TOKEN_KEY])
|
||||||
|
self._set_cookie('.weverse.io', self._REFRESH_TOKEN_KEY, self._oauth_tokens[self._REFRESH_TOKEN_KEY])
|
||||||
|
else:
|
||||||
|
self.cache.store(self._NETRC_MACHINE, self._oauth_cache_key, self._oauth_tokens)
|
||||||
|
|
||||||
|
def _get_authorization_header(self):
|
||||||
|
if not self._is_logged_in:
|
||||||
|
return {}
|
||||||
|
if self._token_is_expired(self._ACCESS_TOKEN_KEY):
|
||||||
|
self._refresh_access_token()
|
||||||
|
return {'Authorization': f'Bearer {self._oauth_tokens[self._ACCESS_TOKEN_KEY]}'}
|
||||||
|
|
||||||
|
def _report_login_error(self, error_id):
|
||||||
|
error_msg = self._LOGIN_ERRORS_MAP[error_id]
|
||||||
|
username = self._get_login_info()[0]
|
||||||
|
|
||||||
|
if error_id == 'invalid_username':
|
||||||
|
error_msg = error_msg.format(username)
|
||||||
|
username = f'{self._OAUTH_PREFIX}+{username}'
|
||||||
|
elif not username:
|
||||||
|
username = f'{self._OAUTH_PREFIX}+USERNAME'
|
||||||
|
|
||||||
|
raise ExtractorError(join_nonempty(
|
||||||
|
error_msg, self._LOGIN_HINT_TMPL.format(self._OAUTH_PREFIX, self._REFRESH_TOKEN_KEY, username),
|
||||||
|
'Or else you can u', self._login_hint(method='session_cookies')[1:], delim=''), expected=True)
|
||||||
|
|
||||||
|
def _perform_login(self, username, password):
|
||||||
|
if self._is_logged_in:
|
||||||
return
|
return
|
||||||
|
|
||||||
token = try_call(lambda: self._get_cookies('https://weverse.io/')['we2_access_token'].value)
|
if username.partition('+')[0] != self._OAUTH_PREFIX:
|
||||||
if token:
|
self._report_login_error('invalid_username')
|
||||||
WeverseBaseIE._API_HEADERS['Authorization'] = f'Bearer {token}'
|
|
||||||
|
self._oauth_tokens.update(self.cache.load(self._NETRC_MACHINE, self._oauth_cache_key, default={}))
|
||||||
|
if self._is_logged_in and self._access_token_is_valid():
|
||||||
|
return
|
||||||
|
|
||||||
|
rt_key = self._REFRESH_TOKEN_KEY
|
||||||
|
if not self._oauth_tokens.get(rt_key) or self._token_is_expired(rt_key):
|
||||||
|
if try_call(lambda: jwt_decode_hs256(password)['scope']) != 'refresh':
|
||||||
|
self._report_login_error('invalid_password')
|
||||||
|
self._oauth_tokens[rt_key] = password
|
||||||
|
|
||||||
|
self._refresh_access_token()
|
||||||
|
|
||||||
|
def _real_initialize(self):
|
||||||
|
cookies = self._get_cookies('https://weverse.io/')
|
||||||
|
|
||||||
|
if not self._device_id:
|
||||||
|
self._device_id = traverse_obj(cookies, (self._DEVICE_ID_KEY, 'value')) or str(uuid.uuid4())
|
||||||
|
|
||||||
|
if self._is_logged_in:
|
||||||
|
return
|
||||||
|
|
||||||
|
self._oauth_tokens.update(traverse_obj(cookies, {
|
||||||
|
self._ACCESS_TOKEN_KEY: (self._ACCESS_TOKEN_KEY, 'value'),
|
||||||
|
self._REFRESH_TOKEN_KEY: (self._REFRESH_TOKEN_KEY, 'value'),
|
||||||
|
}))
|
||||||
|
if self._is_logged_in and not self._access_token_is_valid():
|
||||||
|
self._refresh_access_token()
|
||||||
|
|
||||||
def _call_api(self, ep, video_id, data=None, note='Downloading API JSON'):
|
def _call_api(self, ep, video_id, data=None, note='Downloading API JSON'):
|
||||||
# Ref: https://ssl.pstatic.net/static/wevweb/2_3_2_11101725/public/static/js/2488.a09b41ff.chunk.js
|
# Ref: https://ssl.pstatic.net/static/wevweb/2_3_2_11101725/public/static/js/2488.a09b41ff.chunk.js
|
||||||
# From https://ssl.pstatic.net/static/wevweb/2_3_2_11101725/public/static/js/main.e206f7c1.js:
|
# From https://ssl.pstatic.net/static/wevweb/2_3_2_11101725/public/static/js/main.e206f7c1.js:
|
||||||
key = b'1b9cb6378d959b45714bec49971ade22e6e24e42'
|
|
||||||
api_path = update_url_query(ep, {
|
api_path = update_url_query(ep, {
|
||||||
# 'gcc': 'US',
|
# 'gcc': 'US',
|
||||||
'appId': 'be4d79eb8fc7bd008ee82c8ec4ff6fd4',
|
'appId': 'be4d79eb8fc7bd008ee82c8ec4ff6fd4',
|
||||||
'language': 'en',
|
'language': 'en',
|
||||||
'os': 'WEB',
|
'os': self._CLIENT_PLATFORM,
|
||||||
'platform': 'WEB',
|
'platform': self._CLIENT_PLATFORM,
|
||||||
'wpf': 'pc',
|
'wpf': 'pc',
|
||||||
})
|
})
|
||||||
wmsgpad = int(time.time() * 1000)
|
for is_retry in (False, True):
|
||||||
wmd = base64.b64encode(hmac.HMAC(
|
wmsgpad = int(time.time() * 1000)
|
||||||
key, f'{api_path[:255]}{wmsgpad}'.encode(), digestmod=hashlib.sha1).digest()).decode()
|
wmd = base64.b64encode(hmac.HMAC(
|
||||||
headers = {'Content-Type': 'application/json'} if data else {}
|
self._SIGNING_KEY, f'{api_path[:255]}{wmsgpad}'.encode(),
|
||||||
try:
|
digestmod=hashlib.sha1).digest()).decode()
|
||||||
return self._download_json(
|
|
||||||
f'https://global.apis.naver.com/weverse/wevweb{api_path}', video_id, note=note,
|
try:
|
||||||
data=data, headers={**self._API_HEADERS, **headers}, query={
|
return self._download_json(
|
||||||
'wmsgpad': wmsgpad,
|
f'https://global.apis.naver.com/weverse/wevweb{api_path}', video_id, note=note,
|
||||||
'wmd': wmd,
|
data=data, headers={
|
||||||
})
|
**self._API_HEADERS,
|
||||||
except ExtractorError as e:
|
**self._get_authorization_header(),
|
||||||
if isinstance(e.cause, HTTPError) and e.cause.status == 401:
|
**({'Content-Type': 'application/json'} if data else {}),
|
||||||
self.raise_login_required(
|
'WEV-device-Id': self._device_id,
|
||||||
'Session token has expired. Log in again or refresh cookies in browser')
|
}, query={
|
||||||
elif isinstance(e.cause, HTTPError) and e.cause.status == 403:
|
'wmsgpad': wmsgpad,
|
||||||
if 'Authorization' in self._API_HEADERS:
|
'wmd': wmd,
|
||||||
raise ExtractorError('Your account does not have access to this content', expected=True)
|
})
|
||||||
self.raise_login_required()
|
except ExtractorError as e:
|
||||||
raise
|
if is_retry or not isinstance(e.cause, HTTPError):
|
||||||
|
raise
|
||||||
|
elif self._is_logged_in and e.cause.status == 401:
|
||||||
|
self._refresh_access_token()
|
||||||
|
continue
|
||||||
|
elif e.cause.status == 403:
|
||||||
|
if self._is_logged_in:
|
||||||
|
raise ExtractorError(
|
||||||
|
'Your account does not have access to this content', expected=True)
|
||||||
|
self._report_login_error('login_required')
|
||||||
|
raise
|
||||||
|
|
||||||
def _call_post_api(self, video_id):
|
def _call_post_api(self, video_id):
|
||||||
path = '' if 'Authorization' in self._API_HEADERS else '/preview'
|
path = '' if self._is_logged_in else '/preview'
|
||||||
return self._call_api(f'/post/v1.0/post-{video_id}{path}?fieldSet=postV1', video_id)
|
return self._call_api(f'/post/v1.0/post-{video_id}{path}?fieldSet=postV1', video_id)
|
||||||
|
|
||||||
def _get_community_id(self, channel):
|
def _get_community_id(self, channel):
|
||||||
|
|||||||
@ -3398,8 +3398,15 @@ class YoutubeIE(YoutubeBaseInfoExtractor):
|
|||||||
self._decrypt_signature(encrypted_sig, video_id, player_url),
|
self._decrypt_signature(encrypted_sig, video_id, player_url),
|
||||||
)
|
)
|
||||||
except ExtractorError as e:
|
except ExtractorError as e:
|
||||||
self.report_warning('Signature extraction failed: Some formats may be missing',
|
self.report_warning(
|
||||||
video_id=video_id, only_once=True)
|
f'Signature extraction failed: Some formats may be missing\n'
|
||||||
|
f' player = {player_url}\n'
|
||||||
|
f' {bug_reports_message(before="")}',
|
||||||
|
video_id=video_id, only_once=True)
|
||||||
|
self.write_debug(
|
||||||
|
f'{video_id}: Signature extraction failure info:\n'
|
||||||
|
f' encrypted sig = {encrypted_sig}\n'
|
||||||
|
f' player = {player_url}')
|
||||||
self.write_debug(e, only_once=True)
|
self.write_debug(e, only_once=True)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user