mirror of
https://github.com/yt-dlp/yt-dlp.git
synced 2026-01-30 10:42:05 +00:00
[ie/soop] Support subscription-only VODs (#15523)
* Add custom downloader SoopVodFD Closes #13636 Authored by: thematuu
This commit is contained in:
@@ -36,6 +36,7 @@ from .rtsp import RtspFD
|
|||||||
from .websocket import WebSocketFragmentFD
|
from .websocket import WebSocketFragmentFD
|
||||||
from .youtube_live_chat import YoutubeLiveChatFD
|
from .youtube_live_chat import YoutubeLiveChatFD
|
||||||
from .bunnycdn import BunnyCdnFD
|
from .bunnycdn import BunnyCdnFD
|
||||||
|
from .soop import SoopVodFD
|
||||||
|
|
||||||
PROTOCOL_MAP = {
|
PROTOCOL_MAP = {
|
||||||
'rtmp': RtmpFD,
|
'rtmp': RtmpFD,
|
||||||
@@ -56,6 +57,7 @@ PROTOCOL_MAP = {
|
|||||||
'youtube_live_chat': YoutubeLiveChatFD,
|
'youtube_live_chat': YoutubeLiveChatFD,
|
||||||
'youtube_live_chat_replay': YoutubeLiveChatFD,
|
'youtube_live_chat_replay': YoutubeLiveChatFD,
|
||||||
'bunnycdn': BunnyCdnFD,
|
'bunnycdn': BunnyCdnFD,
|
||||||
|
'soopvod': SoopVodFD,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
61
yt_dlp/downloader/soop.py
Normal file
61
yt_dlp/downloader/soop.py
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
import threading
|
||||||
|
import time
|
||||||
|
|
||||||
|
from .common import FileDownloader
|
||||||
|
from . import HlsFD
|
||||||
|
from ..extractor.afreecatv import _cloudfront_auth_request
|
||||||
|
from ..networking.exceptions import network_exceptions
|
||||||
|
|
||||||
|
|
||||||
|
class SoopVodFD(FileDownloader):
|
||||||
|
"""
|
||||||
|
Downloads Soop subscription VODs with required cookie refresh requests
|
||||||
|
Note, this is not a part of public API, and will be removed without notice.
|
||||||
|
DO NOT USE
|
||||||
|
"""
|
||||||
|
|
||||||
|
def real_download(self, filename, info_dict):
|
||||||
|
self.to_screen(f'[{self.FD_NAME}] Downloading Soop subscription VOD HLS')
|
||||||
|
fd = HlsFD(self.ydl, self.params)
|
||||||
|
refresh_params = info_dict['_cookie_refresh_params']
|
||||||
|
referer_url = info_dict['webpage_url']
|
||||||
|
|
||||||
|
stop_event = threading.Event()
|
||||||
|
refresh_thread = threading.Thread(
|
||||||
|
target=self._cookie_refresh_thread,
|
||||||
|
args=(stop_event, refresh_params, referer_url),
|
||||||
|
)
|
||||||
|
refresh_thread.start()
|
||||||
|
|
||||||
|
try:
|
||||||
|
return fd.real_download(filename, info_dict)
|
||||||
|
finally:
|
||||||
|
stop_event.set()
|
||||||
|
|
||||||
|
def _cookie_refresh_thread(self, stop_event, refresh_params, referer_url):
|
||||||
|
m3u8_url = refresh_params['m3u8_url']
|
||||||
|
strm_id = refresh_params['strm_id']
|
||||||
|
video_id = refresh_params['video_id']
|
||||||
|
|
||||||
|
def _get_cloudfront_cookie_expiration(m3u8_url):
|
||||||
|
cookies = self.ydl.cookiejar.get_cookies_for_url(m3u8_url)
|
||||||
|
return min((cookie.expires for cookie in cookies if 'CloudFront' in cookie.name and cookie.expires), default=0)
|
||||||
|
|
||||||
|
while not stop_event.wait(5):
|
||||||
|
current_time = time.time()
|
||||||
|
expiration_time = _get_cloudfront_cookie_expiration(m3u8_url)
|
||||||
|
last_refresh_check = refresh_params.get('_last_refresh', 0)
|
||||||
|
|
||||||
|
# Cookie TTL is 90 seconds, but let's give ourselves a 15-second cushion
|
||||||
|
should_refresh = (
|
||||||
|
(expiration_time and current_time >= expiration_time - 15)
|
||||||
|
or (not expiration_time and current_time - last_refresh_check >= 75)
|
||||||
|
)
|
||||||
|
|
||||||
|
if should_refresh:
|
||||||
|
try:
|
||||||
|
self.ydl.urlopen(_cloudfront_auth_request(
|
||||||
|
m3u8_url, strm_id, video_id, referer_url)).read()
|
||||||
|
refresh_params['_last_refresh'] = current_time
|
||||||
|
except network_exceptions as e:
|
||||||
|
self.to_screen(f'[{self.FD_NAME}] Cookie refresh attempt failed: {e}')
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import datetime as dt
|
import datetime as dt
|
||||||
import functools
|
import functools
|
||||||
|
import time
|
||||||
|
|
||||||
from .common import InfoExtractor
|
from .common import InfoExtractor
|
||||||
from ..networking import Request
|
from ..networking import Request
|
||||||
@@ -16,7 +17,23 @@ from ..utils import (
|
|||||||
urlencode_postdata,
|
urlencode_postdata,
|
||||||
urljoin,
|
urljoin,
|
||||||
)
|
)
|
||||||
from ..utils.traversal import traverse_obj
|
from ..utils.traversal import require, traverse_obj
|
||||||
|
|
||||||
|
|
||||||
|
def _cloudfront_auth_request(m3u8_url, strm_id, video_id, referer_url):
|
||||||
|
return Request(
|
||||||
|
'https://live.sooplive.co.kr/api/private_auth.php',
|
||||||
|
method='POST',
|
||||||
|
headers={
|
||||||
|
'Referer': referer_url,
|
||||||
|
'Origin': 'https://vod.sooplive.co.kr',
|
||||||
|
},
|
||||||
|
data=urlencode_postdata({
|
||||||
|
'type': 'vod',
|
||||||
|
'strm_id': strm_id,
|
||||||
|
'title_no': video_id,
|
||||||
|
'url': m3u8_url,
|
||||||
|
}))
|
||||||
|
|
||||||
|
|
||||||
class AfreecaTVBaseIE(InfoExtractor):
|
class AfreecaTVBaseIE(InfoExtractor):
|
||||||
@@ -153,6 +170,13 @@ class AfreecaTVIE(AfreecaTVBaseIE):
|
|||||||
'nApiLevel': 10,
|
'nApiLevel': 10,
|
||||||
}))['data']
|
}))['data']
|
||||||
|
|
||||||
|
initial_refresh_time = 0
|
||||||
|
strm_id = None
|
||||||
|
# For subscriber-only VODs, we need to call private_auth.php to get CloudFront cookies
|
||||||
|
needs_private_auth = traverse_obj(data, ('sub_upload_type', {str}))
|
||||||
|
if needs_private_auth:
|
||||||
|
strm_id = traverse_obj(data, ('bj_id', {str}, {require('stream ID')}))
|
||||||
|
|
||||||
error_code = traverse_obj(data, ('code', {int}))
|
error_code = traverse_obj(data, ('code', {int}))
|
||||||
if error_code == -6221:
|
if error_code == -6221:
|
||||||
raise ExtractorError('The VOD does not exist', expected=True)
|
raise ExtractorError('The VOD does not exist', expected=True)
|
||||||
@@ -172,9 +196,23 @@ class AfreecaTVIE(AfreecaTVBaseIE):
|
|||||||
traverse_obj(data, ('files', lambda _, v: url_or_none(v['file']))), start=1):
|
traverse_obj(data, ('files', lambda _, v: url_or_none(v['file']))), start=1):
|
||||||
file_url = file_element['file']
|
file_url = file_element['file']
|
||||||
if determine_ext(file_url) == 'm3u8':
|
if determine_ext(file_url) == 'm3u8':
|
||||||
|
if needs_private_auth:
|
||||||
|
self._request_webpage(
|
||||||
|
_cloudfront_auth_request(file_url, strm_id, video_id, url),
|
||||||
|
video_id, 'Requesting CloudFront cookies', 'Failed to get CloudFront cookies')
|
||||||
|
initial_refresh_time = time.time()
|
||||||
formats = self._extract_m3u8_formats(
|
formats = self._extract_m3u8_formats(
|
||||||
file_url, video_id, 'mp4', m3u8_id='hls',
|
file_url, video_id, 'mp4', m3u8_id='hls',
|
||||||
note=f'Downloading part {file_num} m3u8 information')
|
note=f'Downloading part {file_num} m3u8 information')
|
||||||
|
if needs_private_auth:
|
||||||
|
for fmt in formats:
|
||||||
|
fmt['protocol'] = 'soopvod'
|
||||||
|
fmt['_cookie_refresh_params'] = {
|
||||||
|
'm3u8_url': file_url,
|
||||||
|
'strm_id': strm_id,
|
||||||
|
'video_id': video_id,
|
||||||
|
'_last_refresh': initial_refresh_time,
|
||||||
|
}
|
||||||
else:
|
else:
|
||||||
formats = [{
|
formats = [{
|
||||||
'url': file_url,
|
'url': file_url,
|
||||||
|
|||||||
Reference in New Issue
Block a user