Revert "[sauceplus]support for sauceplus via floatplane extractor"

This reverts commit 3edc695b1b.
pull/14053/head
i3p9 2 days ago
parent 3edc695b1b
commit f9ac46999e

@ -657,8 +657,6 @@ from .flickr import FlickrIE
from .floatplane import ( from .floatplane import (
FloatplaneChannelIE, FloatplaneChannelIE,
FloatplaneIE, FloatplaneIE,
SauceplusChannelIE,
SauceplusIE,
) )
from .folketinget import FolketingetIE from .folketinget import FolketingetIE
from .footyroom import FootyRoomIE from .footyroom import FootyRoomIE

@ -22,33 +22,27 @@ class FloatplaneBaseIE(InfoExtractor):
post_id = self._match_id(url) post_id = self._match_id(url)
post_data = self._download_json( post_data = self._download_json(
f'{self._BASE_URL}/api/v3/content/post', f'{self._BASE_URL}/api/v3/content/post', post_id, query={'id': post_id},
post_id, note='Downloading post data', errnote='Unable to download post data',
query={'id': post_id}, impersonate=self._IMPERSONATE_TARGET)
note='Downloading post data',
errnote='Unable to download post data',
impersonate=self._IMPERSONATE_TARGET,
)
if not any(traverse_obj(post_data, ('metadata', ('hasVideo', 'hasAudio')))): if not any(traverse_obj(post_data, ('metadata', ('hasVideo', 'hasAudio')))):
raise ExtractorError('Post does not contain a video or audio track', expected=True) raise ExtractorError('Post does not contain a video or audio track', expected=True)
uploader_url = format_field(post_data, [('creator', 'urlname')], f'{self._BASE_URL}/channel/%s/home') or None uploader_url = format_field(
post_data, [('creator', 'urlname')], f'{self._BASE_URL}/channel/%s/home') or None
common_info = { common_info = {
'uploader_url': uploader_url, 'uploader_url': uploader_url,
'channel_url': urljoin(f'{uploader_url}/', traverse_obj(post_data, ('channel', 'urlname'))), 'channel_url': urljoin(f'{uploader_url}/', traverse_obj(post_data, ('channel', 'urlname'))),
'availability': self._availability(needs_subscription=True), 'availability': self._availability(needs_subscription=True),
**traverse_obj( **traverse_obj(post_data, {
post_data,
{
'uploader': ('creator', 'title', {str}), 'uploader': ('creator', 'title', {str}),
'uploader_id': ('creator', 'id', {str}), 'uploader_id': ('creator', 'id', {str}),
'channel': ('channel', 'title', {str}), 'channel': ('channel', 'title', {str}),
'channel_id': ('channel', 'id', {str}), 'channel_id': ('channel', 'id', {str}),
'release_timestamp': ('releaseDate', {parse_iso8601}), 'release_timestamp': ('releaseDate', {parse_iso8601}),
}, }),
),
} }
items = [] items = []
@ -57,23 +51,15 @@ class FloatplaneBaseIE(InfoExtractor):
media_typ = media.get('type') or 'video' media_typ = media.get('type') or 'video'
metadata = self._download_json( metadata = self._download_json(
f'{self._BASE_URL}/api/v3/content/{media_typ}', f'{self._BASE_URL}/api/v3/content/{media_typ}', media_id, query={'id': media_id},
media_id, note=f'Downloading {media_typ} metadata', impersonate=self._IMPERSONATE_TARGET)
query={'id': media_id},
note=f'Downloading {media_typ} metadata',
impersonate=self._IMPERSONATE_TARGET,
)
stream = self._download_json( stream = self._download_json(
f'{self._BASE_URL}/api/v2/cdn/delivery', f'{self._BASE_URL}/api/v2/cdn/delivery', media_id, query={
media_id,
query={
'type': 'vod' if media_typ == 'video' else 'aod', 'type': 'vod' if media_typ == 'video' else 'aod',
'guid': metadata['guid'], 'guid': metadata['guid'],
}, }, note=f'Downloading {media_typ} stream data',
note=f'Downloading {media_typ} stream data', impersonate=self._IMPERSONATE_TARGET)
impersonate=self._IMPERSONATE_TARGET,
)
path_template = traverse_obj(stream, ('resource', 'uri', {str})) path_template = traverse_obj(stream, ('resource', 'uri', {str}))
@ -85,12 +71,8 @@ class FloatplaneBaseIE(InfoExtractor):
formats = [] formats = []
for quality in traverse_obj(stream, ('resource', 'data', 'qualityLevels', ...)): for quality in traverse_obj(stream, ('resource', 'data', 'qualityLevels', ...)):
url = urljoin( url = urljoin(stream['cdn'], format_path(traverse_obj(
stream['cdn'], stream, ('resource', 'data', 'qualityLevelParams', quality['name'], {dict}))))
format_path(
traverse_obj(stream, ('resource', 'data', 'qualityLevelParams', quality['name'], {dict})),
),
)
format_id = traverse_obj(quality, ('name', {str})) format_id = traverse_obj(quality, ('name', {str}))
hls_aes = {} hls_aes = {}
m3u8_data = None m3u8_data = None
@ -98,82 +80,59 @@ class FloatplaneBaseIE(InfoExtractor):
# If we need impersonation for the API, then we need it for HLS keys too: extract in advance # If we need impersonation for the API, then we need it for HLS keys too: extract in advance
if self._IMPERSONATE_TARGET is not None: if self._IMPERSONATE_TARGET is not None:
m3u8_data = self._download_webpage( m3u8_data = self._download_webpage(
url, url, media_id, fatal=False, impersonate=self._IMPERSONATE_TARGET, headers=self._HEADERS,
media_id,
fatal=False,
impersonate=self._IMPERSONATE_TARGET,
headers=self._HEADERS,
note=join_nonempty('Downloading', format_id, 'm3u8 information', delim=' '), note=join_nonempty('Downloading', format_id, 'm3u8 information', delim=' '),
errnote=join_nonempty('Failed to download', format_id, 'm3u8 information', delim=' '), errnote=join_nonempty('Failed to download', format_id, 'm3u8 information', delim=' '))
)
if not m3u8_data: if not m3u8_data:
continue continue
key_url = self._search_regex( key_url = self._search_regex(
r'#EXT-X-KEY:METHOD=AES-128,URI="(https?://[^"]+)"', m3u8_data, 'HLS AES key URI', default=None, r'#EXT-X-KEY:METHOD=AES-128,URI="(https?://[^"]+)"',
) m3u8_data, 'HLS AES key URI', default=None)
if key_url: if key_url:
urlh = self._request_webpage( urlh = self._request_webpage(
key_url, key_url, media_id, fatal=False, impersonate=self._IMPERSONATE_TARGET, headers=self._HEADERS,
media_id,
fatal=False,
impersonate=self._IMPERSONATE_TARGET,
headers=self._HEADERS,
note=join_nonempty('Downloading', format_id, 'HLS AES key', delim=' '), note=join_nonempty('Downloading', format_id, 'HLS AES key', delim=' '),
errnote=join_nonempty('Failed to download', format_id, 'HLS AES key', delim=' '), errnote=join_nonempty('Failed to download', format_id, 'HLS AES key', delim=' '))
)
if urlh: if urlh:
hls_aes['key'] = urlh.read().hex() hls_aes['key'] = urlh.read().hex()
formats.append( formats.append({
{ **traverse_obj(quality, {
**traverse_obj(
quality,
{
'format_note': ('label', {str}), 'format_note': ('label', {str}),
'width': ('width', {int}), 'width': ('width', {int}),
'height': ('height', {int}), 'height': ('height', {int}),
}, }),
),
**parse_codecs(quality.get('codecs')), **parse_codecs(quality.get('codecs')),
'url': url, 'url': url,
'ext': determine_ext(url.partition('/chunk.m3u8')[0], 'mp4'), 'ext': determine_ext(url.partition('/chunk.m3u8')[0], 'mp4'),
'format_id': format_id, 'format_id': format_id,
'hls_media_playlist_data': m3u8_data, 'hls_media_playlist_data': m3u8_data,
'hls_aes': hls_aes or None, 'hls_aes': hls_aes or None,
}, })
) items.append({
items.append(
{
**common_info, **common_info,
'id': media_id, 'id': media_id,
**traverse_obj( **traverse_obj(metadata, {
metadata,
{
'title': ('title', {str}), 'title': ('title', {str}),
'duration': ('duration', {int_or_none}), 'duration': ('duration', {int_or_none}),
'thumbnail': ('thumbnail', 'path', {url_or_none}), 'thumbnail': ('thumbnail', 'path', {url_or_none}),
}, }),
),
'formats': formats, 'formats': formats,
}, })
)
post_info = { post_info = {
**common_info, **common_info,
'id': post_id, 'id': post_id,
'display_id': post_id, 'display_id': post_id,
**traverse_obj( **traverse_obj(post_data, {
post_data,
{
'title': ('title', {str}), 'title': ('title', {str}),
'description': ('text', {clean_html}), 'description': ('text', {clean_html}),
'like_count': ('likes', {int_or_none}), 'like_count': ('likes', {int_or_none}),
'dislike_count': ('dislikes', {int_or_none}), 'dislike_count': ('dislikes', {int_or_none}),
'comment_count': ('comments', {int_or_none}), 'comment_count': ('comments', {int_or_none}),
'thumbnail': ('thumbnail', 'path', {url_or_none}), 'thumbnail': ('thumbnail', 'path', {url_or_none}),
}, }),
),
'http_headers': self._HEADERS, 'http_headers': self._HEADERS,
} }
@ -192,8 +151,7 @@ class FloatplaneIE(FloatplaneBaseIE):
'Origin': _BASE_URL, 'Origin': _BASE_URL,
'Referer': f'{_BASE_URL}/', 'Referer': f'{_BASE_URL}/',
} }
_TESTS = [ _TESTS = [{
{
'url': 'https://www.floatplane.com/post/2Yf3UedF7C', 'url': 'https://www.floatplane.com/post/2Yf3UedF7C',
'info_dict': { 'info_dict': {
'id': 'yuleLogLTT', 'id': 'yuleLogLTT',
@ -217,9 +175,7 @@ class FloatplaneIE(FloatplaneBaseIE):
'availability': 'subscriber_only', 'availability': 'subscriber_only',
}, },
'params': {'skip_download': 'm3u8'}, 'params': {'skip_download': 'm3u8'},
'skip': 'requires LTT subscription', }, {
},
{
'url': 'https://www.floatplane.com/post/j2jqG3JmgJ', 'url': 'https://www.floatplane.com/post/j2jqG3JmgJ',
'info_dict': { 'info_dict': {
'id': 'j2jqG3JmgJ', 'id': 'j2jqG3JmgJ',
@ -240,8 +196,7 @@ class FloatplaneIE(FloatplaneBaseIE):
'availability': 'subscriber_only', 'availability': 'subscriber_only',
}, },
'playlist_count': 2, 'playlist_count': 2,
}, }, {
{
'url': 'https://www.floatplane.com/post/3tK2tInhoN', 'url': 'https://www.floatplane.com/post/3tK2tInhoN',
'info_dict': { 'info_dict': {
'id': '3tK2tInhoN', 'id': '3tK2tInhoN',
@ -262,8 +217,7 @@ class FloatplaneIE(FloatplaneBaseIE):
'availability': 'subscriber_only', 'availability': 'subscriber_only',
}, },
'playlist_count': 2, 'playlist_count': 2,
}, }, {
{
'url': 'https://beta.floatplane.com/post/d870PEFXS1', 'url': 'https://beta.floatplane.com/post/d870PEFXS1',
'info_dict': { 'info_dict': {
'id': 'bg9SuYKEww', 'id': 'bg9SuYKEww',
@ -287,12 +241,11 @@ class FloatplaneIE(FloatplaneBaseIE):
'availability': 'subscriber_only', 'availability': 'subscriber_only',
}, },
'params': {'skip_download': 'm3u8'}, 'params': {'skip_download': 'm3u8'},
}, }, {
{
'url': 'https://www.floatplane.com/post/65B5PNoBtf', 'url': 'https://www.floatplane.com/post/65B5PNoBtf',
'info_dict': { 'info_dict': {
'id': '65B5PNoBtf', 'id': '65B5PNoBtf',
'description': "I recorded the inbuilt demo mode for your 90's enjoyment, thanks for being Floaties!", 'description': 'I recorded the inbuilt demo mode for your 90\'s enjoyment, thanks for being Floaties!',
'display_id': '65B5PNoBtf', 'display_id': '65B5PNoBtf',
'like_count': int, 'like_count': int,
'release_timestamp': 1701249480, 'release_timestamp': 1701249480,
@ -310,8 +263,7 @@ class FloatplaneIE(FloatplaneBaseIE):
'release_date': '20231129', 'release_date': '20231129',
}, },
'playlist_count': 2, 'playlist_count': 2,
'playlist': [ 'playlist': [{
{
'info_dict': { 'info_dict': {
'id': 'ISPJjexylS', 'id': 'ISPJjexylS',
'ext': 'mp4', 'ext': 'mp4',
@ -328,8 +280,7 @@ class FloatplaneIE(FloatplaneBaseIE):
'channel_url': 'https://www.floatplane.com/channel/TheTrashNetwork/home/thedrumthing', 'channel_url': 'https://www.floatplane.com/channel/TheTrashNetwork/home/thedrumthing',
'uploader_url': 'https://www.floatplane.com/channel/TheTrashNetwork/home', 'uploader_url': 'https://www.floatplane.com/channel/TheTrashNetwork/home',
}, },
}, }, {
{
'info_dict': { 'info_dict': {
'id': 'qKfxu6fEpu', 'id': 'qKfxu6fEpu',
'ext': 'aac', 'ext': 'aac',
@ -345,161 +296,20 @@ class FloatplaneIE(FloatplaneBaseIE):
'channel_url': 'https://www.floatplane.com/channel/TheTrashNetwork/home/thedrumthing', 'channel_url': 'https://www.floatplane.com/channel/TheTrashNetwork/home/thedrumthing',
'uploader_url': 'https://www.floatplane.com/channel/TheTrashNetwork/home', 'uploader_url': 'https://www.floatplane.com/channel/TheTrashNetwork/home',
}, },
}, }],
],
'skip': 'requires subscription: "The Trash Network"', 'skip': 'requires subscription: "The Trash Network"',
'params': {'skip_download': 'm3u8'}, 'params': {'skip_download': 'm3u8'},
}, }]
]
def _real_initialize(self): def _real_initialize(self):
if not self._get_cookies(self._BASE_URL).get('sails.sid'): if not self._get_cookies(self._BASE_URL).get('sails.sid'):
self.raise_login_required() self.raise_login_required()
class SauceplusIE(FloatplaneBaseIE):
_VALID_URL = r'https?://(?:(?:www|beta)\.)?sauceplus\.com/post/(?P<id>\w+)'
_BASE_URL = 'https://www.sauceplus.com'
_IMPERSONATE_TARGET = None
_HEADERS = {
'Origin': _BASE_URL,
'Referer': f'{_BASE_URL}/',
}
_TESTS = [
{
'url': 'https://www.sauceplus.com/post/RMRau1tqaU',
'info_dict': {
'id': 'EbcXSMMRFc',
'ext': 'mp4',
'display_id': 'RMRau1tqaU',
'title': 'Scare the Coyote - Episode 1',
'description': 'Scare the Coyote airs Thursdays only on Sauce Plus',
'thumbnail': r're:^https?://.*\.jpe?g$',
'duration': 2676,
'comment_count': int,
'like_count': int,
'dislike_count': int,
'release_date': '20250616',
'release_timestamp': 1750083240,
'uploader': 'Scare The Coyote',
'uploader_id': '683e0a3269688656a5a49a44',
'uploader_url': 'https://www.sauceplus.com/channel/ScareTheCoyote/home',
'channel': 'Scare The Coyote',
'channel_id': '683e0a326968866ceba49a45',
'channel_url': 'https://www.sauceplus.com/channel/ScareTheCoyote/home/main',
'availability': 'subscriber_only',
},
'params': {'skip_download': 'm3u8'},
'skip': 'requires subscription: Sauceplus',
},
{
'url': 'https://www.sauceplus.com/post/W9drFVKOwh',
'info_dict': {
'id': '0GZy0KBdeQ',
'ext': 'mp4',
'display_id': 'W9drFVKOwh',
'title': 'BYSM_00190_F.mp4',
'description': '',
'thumbnail': r're:^https?://.*\.jpe?g$',
'duration': 158,
'comment_count': int,
'like_count': int,
'dislike_count': int,
'release_date': '20241207',
'release_timestamp': 1733542704,
'uploader': 'The Backyard Scientist',
'uploader_id': '6848a6ec7e317363389190b9',
'uploader_url': 'https://www.sauceplus.com/channel/TheBackyardScientist/home',
'channel': 'The Backyard Scientist',
'channel_id': '6848a6ec7e31733d359190ba',
'channel_url': 'https://www.sauceplus.com/channel/TheBackyardScientist/home/main',
'availability': 'subscriber_only',
},
'params': {'skip_download': 'm3u8'},
'skip': 'requires subscription: Sauceplus',
},
]
def _real_initialize(self):
if not self._get_cookies(self._BASE_URL).get('__Host-sp-sess'):
self.raise_login_required()
class SauceplusChannelIE(InfoExtractor):
_VALID_URL = r'https?://(?:(?:www|beta)\.)?sauceplus\.com/channel/(?P<id>[\w-]+)/home(?:/(?P<channel>[\w-]+))?'
_PAGE_SIZE = 20
_TESTS = [
{
'url': 'https://www.sauceplus.com/channel/ScareTheCoyote/home',
'info_dict': {
'id': 'ScareTheCoyote',
'title': 'Scare The Coyote',
},
'playlist_mincount': 7,
'skip': 'requires subscription: Scare The Coyote',
},
{
'url': 'https://www.sauceplus.com/channel/SafetyThird/home',
'info_dict': {
'id': 'SafetyThird',
'title': 'Safety Third',
},
'playlist_mincount': 150,
'skip': 'requires subscription: Safety Third',
},
]
def _fetch_page(self, display_id, creator_id, channel_id, page):
query = {
'id': creator_id,
'limit': self._PAGE_SIZE,
'fetchAfter': page * self._PAGE_SIZE,
}
if channel_id:
query['channel'] = channel_id
page_data = self._download_json(
'https://www.sauceplus.com/api/v3/content/creator',
display_id,
query=query,
note=f'Downloading page {page + 1}',
)
for post in page_data or []:
yield self.url_result(
f"https://www.sauceplus.com/post/{post['id']}",
SauceplusIE,
id=post['id'],
title=post.get('title'),
release_timestamp=parse_iso8601(post.get('releaseDate')),
)
def _real_extract(self, url):
creator, channel = self._match_valid_url(url).group('id', 'channel')
display_id = join_nonempty(creator, channel, delim='/')
creator_data = self._download_json(
'https://www.sauceplus.com/api/v3/creator/named', display_id, query={'creatorURL[0]': creator},
)[0]
channel_data = (
traverse_obj(creator_data, ('channels', lambda _, v: v['urlname'] == channel), get_all=False) or {}
)
return self.playlist_result(
OnDemandPagedList(
functools.partial(self._fetch_page, display_id, creator_data['id'], channel_data.get('id')),
self._PAGE_SIZE,
),
display_id,
title=channel_data.get('title') or creator_data.get('title'),
description=channel_data.get('about') or creator_data.get('about'),
)
class FloatplaneChannelIE(InfoExtractor): class FloatplaneChannelIE(InfoExtractor):
_VALID_URL = r'https?://(?:(?:www|beta)\.)?floatplane\.com/channel/(?P<id>[\w-]+)/home(?:/(?P<channel>[\w-]+))?' _VALID_URL = r'https?://(?:(?:www|beta)\.)?floatplane\.com/channel/(?P<id>[\w-]+)/home(?:/(?P<channel>[\w-]+))?'
_PAGE_SIZE = 20 _PAGE_SIZE = 20
_TESTS = [ _TESTS = [{
{
'url': 'https://www.floatplane.com/channel/linustechtips/home/ltxexpo', 'url': 'https://www.floatplane.com/channel/linustechtips/home/ltxexpo',
'info_dict': { 'info_dict': {
'id': 'linustechtips/ltxexpo', 'id': 'linustechtips/ltxexpo',
@ -507,8 +317,7 @@ class FloatplaneChannelIE(InfoExtractor):
'description': 'md5:9819002f9ebe7fd7c75a3a1d38a59149', 'description': 'md5:9819002f9ebe7fd7c75a3a1d38a59149',
}, },
'playlist_mincount': 51, 'playlist_mincount': 51,
}, }, {
{
'url': 'https://www.floatplane.com/channel/ShankMods/home', 'url': 'https://www.floatplane.com/channel/ShankMods/home',
'info_dict': { 'info_dict': {
'id': 'ShankMods', 'id': 'ShankMods',
@ -516,8 +325,7 @@ class FloatplaneChannelIE(InfoExtractor):
'description': 'md5:6dff1bb07cad8e5448e04daad9be1b30', 'description': 'md5:6dff1bb07cad8e5448e04daad9be1b30',
}, },
'playlist_mincount': 14, 'playlist_mincount': 14,
}, }, {
{
'url': 'https://beta.floatplane.com/channel/bitwit_ultra/home', 'url': 'https://beta.floatplane.com/channel/bitwit_ultra/home',
'info_dict': { 'info_dict': {
'id': 'bitwit_ultra', 'id': 'bitwit_ultra',
@ -525,8 +333,7 @@ class FloatplaneChannelIE(InfoExtractor):
'description': 'md5:1452f280bb45962976d4789200f676dd', 'description': 'md5:1452f280bb45962976d4789200f676dd',
}, },
'playlist_mincount': 200, 'playlist_mincount': 200,
}, }]
]
def _fetch_page(self, display_id, creator_id, channel_id, page): def _fetch_page(self, display_id, creator_id, channel_id, page):
query = { query = {
@ -537,38 +344,26 @@ class FloatplaneChannelIE(InfoExtractor):
if channel_id: if channel_id:
query['channel'] = channel_id query['channel'] = channel_id
page_data = self._download_json( page_data = self._download_json(
'https://www.floatplane.com/api/v3/content/creator', 'https://www.floatplane.com/api/v3/content/creator', display_id,
display_id, query=query, note=f'Downloading page {page + 1}')
query=query,
note=f'Downloading page {page + 1}',
)
for post in page_data or []: for post in page_data or []:
yield self.url_result( yield self.url_result(
f"https://www.floatplane.com/post/{post['id']}", f'https://www.floatplane.com/post/{post["id"]}',
FloatplaneIE, FloatplaneIE, id=post['id'], title=post.get('title'),
id=post['id'], release_timestamp=parse_iso8601(post.get('releaseDate')))
title=post.get('title'),
release_timestamp=parse_iso8601(post.get('releaseDate')),
)
def _real_extract(self, url): def _real_extract(self, url):
creator, channel = self._match_valid_url(url).group('id', 'channel') creator, channel = self._match_valid_url(url).group('id', 'channel')
display_id = join_nonempty(creator, channel, delim='/') display_id = join_nonempty(creator, channel, delim='/')
creator_data = self._download_json( creator_data = self._download_json(
'https://www.floatplane.com/api/v3/creator/named', display_id, query={'creatorURL[0]': creator}, 'https://www.floatplane.com/api/v3/creator/named',
)[0] display_id, query={'creatorURL[0]': creator})[0]
channel_data = ( channel_data = traverse_obj(
traverse_obj(creator_data, ('channels', lambda _, v: v['urlname'] == channel), get_all=False) or {} creator_data, ('channels', lambda _, v: v['urlname'] == channel), get_all=False) or {}
)
return self.playlist_result( return self.playlist_result(OnDemandPagedList(functools.partial(
OnDemandPagedList( self._fetch_page, display_id, creator_data['id'], channel_data.get('id')), self._PAGE_SIZE),
functools.partial(self._fetch_page, display_id, creator_data['id'], channel_data.get('id')), display_id, title=channel_data.get('title') or creator_data.get('title'),
self._PAGE_SIZE, description=channel_data.get('about') or creator_data.get('about'))
),
display_id,
title=channel_data.get('title') or creator_data.get('title'),
description=channel_data.get('about') or creator_data.get('about'),
)

Loading…
Cancel
Save