From 811d298b231cfa29e75c321b23a91d1c2b17602c Mon Sep 17 00:00:00 2001
From: coletdjnz
Date: Sat, 20 Jan 2024 15:26:50 +1300
Subject: [PATCH 001/112] [networking] Remove `_CompatHTTPError` (#8871)
Use `yt_dlp.networking.exceptions.HTTPError`.
`_CompatHTTPError` was to help with transition to the networking framework.
Authored by: coletdjnz
---
test/test_networking_utils.py | 82 ++--------------------
yt_dlp/YoutubeDL.py | 3 -
yt_dlp/compat/_legacy.py | 4 +-
yt_dlp/networking/exceptions.py | 116 +-------------------------------
4 files changed, 7 insertions(+), 198 deletions(-)
diff --git a/test/test_networking_utils.py b/test/test_networking_utils.py
index 419aae1e47..b7b71430e7 100644
--- a/test/test_networking_utils.py
+++ b/test/test_networking_utils.py
@@ -8,13 +8,9 @@ import pytest
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
-import contextlib
import io
-import platform
import random
import ssl
-import urllib.error
-import warnings
from yt_dlp.cookies import YoutubeDLCookieJar
from yt_dlp.dependencies import certifi
@@ -30,7 +26,6 @@ from yt_dlp.networking._helper import (
from yt_dlp.networking.exceptions import (
HTTPError,
IncompleteRead,
- _CompatHTTPError,
)
from yt_dlp.socks import ProxyType
from yt_dlp.utils.networking import HTTPHeaderDict
@@ -179,11 +174,10 @@ class TestNetworkingExceptions:
def create_response(status):
return Response(fp=io.BytesIO(b'test'), url='http://example.com', headers={'tesT': 'test'}, status=status)
- @pytest.mark.parametrize('http_error_class', [HTTPError, lambda r: _CompatHTTPError(HTTPError(r))])
- def test_http_error(self, http_error_class):
+ def test_http_error(self):
response = self.create_response(403)
- error = http_error_class(response)
+ error = HTTPError(response)
assert error.status == 403
assert str(error) == error.msg == 'HTTP Error 403: Forbidden'
@@ -194,80 +188,12 @@ class TestNetworkingExceptions:
assert data == b'test'
assert repr(error) == ''
- @pytest.mark.parametrize('http_error_class', [HTTPError, lambda *args, **kwargs: _CompatHTTPError(HTTPError(*args, **kwargs))])
- def test_redirect_http_error(self, http_error_class):
+ def test_redirect_http_error(self):
response = self.create_response(301)
- error = http_error_class(response, redirect_loop=True)
+ error = HTTPError(response, redirect_loop=True)
assert str(error) == error.msg == 'HTTP Error 301: Moved Permanently (redirect loop detected)'
assert error.reason == 'Moved Permanently'
- def test_compat_http_error(self):
- response = self.create_response(403)
- error = _CompatHTTPError(HTTPError(response))
- assert isinstance(error, HTTPError)
- assert isinstance(error, urllib.error.HTTPError)
-
- @contextlib.contextmanager
- def raises_deprecation_warning():
- with warnings.catch_warnings(record=True) as w:
- warnings.simplefilter('always')
- yield
-
- if len(w) == 0:
- pytest.fail('Did not raise DeprecationWarning')
- if len(w) > 1:
- pytest.fail(f'Raised multiple warnings: {w}')
-
- if not issubclass(w[-1].category, DeprecationWarning):
- pytest.fail(f'Expected DeprecationWarning, got {w[-1].category}')
- w.clear()
-
- with raises_deprecation_warning():
- assert error.code == 403
-
- with raises_deprecation_warning():
- assert error.getcode() == 403
-
- with raises_deprecation_warning():
- assert error.hdrs is error.response.headers
-
- with raises_deprecation_warning():
- assert error.info() is error.response.headers
-
- with raises_deprecation_warning():
- assert error.headers is error.response.headers
-
- with raises_deprecation_warning():
- assert error.filename == error.response.url
-
- with raises_deprecation_warning():
- assert error.url == error.response.url
-
- with raises_deprecation_warning():
- assert error.geturl() == error.response.url
-
- # Passthrough file operations
- with raises_deprecation_warning():
- assert error.read() == b'test'
-
- with raises_deprecation_warning():
- assert not error.closed
-
- with raises_deprecation_warning():
- # Technically Response operations are also passed through, which should not be used.
- assert error.get_header('test') == 'test'
-
- # Should not raise a warning
- error.close()
-
- @pytest.mark.skipif(
- platform.python_implementation() == 'PyPy', reason='garbage collector works differently in pypy')
- def test_compat_http_error_autoclose(self):
- # Compat HTTPError should not autoclose response
- response = self.create_response(403)
- _CompatHTTPError(HTTPError(response))
- assert not response.closed
-
def test_incomplete_read_error(self):
error = IncompleteRead(4, 3, cause='test')
assert isinstance(error, IncompleteRead)
diff --git a/yt_dlp/YoutubeDL.py b/yt_dlp/YoutubeDL.py
index 8d96498a67..5dcefb5b81 100644
--- a/yt_dlp/YoutubeDL.py
+++ b/yt_dlp/YoutubeDL.py
@@ -40,7 +40,6 @@ from .networking.exceptions import (
NoSupportingHandlers,
RequestError,
SSLError,
- _CompatHTTPError,
network_exceptions,
)
from .plugins import directories as plugin_directories
@@ -4110,8 +4109,6 @@ class YoutubeDL:
'SSLV3_ALERT_HANDSHAKE_FAILURE: The server may not support the current cipher list. '
'Try using --legacy-server-connect', cause=e) from e
raise
- except HTTPError as e: # TODO: Remove in a future release
- raise _CompatHTTPError(e) from e
def build_request_director(self, handlers, preferences=None):
logger = _YDLLogger(self)
diff --git a/yt_dlp/compat/_legacy.py b/yt_dlp/compat/_legacy.py
index 90ccf0f14a..7ea5d08120 100644
--- a/yt_dlp/compat/_legacy.py
+++ b/yt_dlp/compat/_legacy.py
@@ -35,6 +35,7 @@ from .compat_utils import passthrough_module
from ..dependencies import brotli as compat_brotli # noqa: F401
from ..dependencies import websockets as compat_websockets # noqa: F401
from ..dependencies.Cryptodome import AES as compat_pycrypto_AES # noqa: F401
+from ..networking.exceptions import HTTPError as compat_HTTPError # noqa: F401
passthrough_module(__name__, '...utils', ('WINDOWS_VT_MODE', 'windows_enable_vt_mode'))
@@ -70,7 +71,6 @@ compat_html_parser_HTMLParseError = compat_HTMLParseError
compat_HTMLParser = compat_html_parser_HTMLParser = html.parser.HTMLParser
compat_http_client = http.client
compat_http_server = http.server
-compat_HTTPError = urllib.error.HTTPError
compat_input = input
compat_integer_types = (int, )
compat_itertools_count = itertools.count
@@ -88,7 +88,7 @@ compat_struct_unpack = struct.unpack
compat_subprocess_get_DEVNULL = lambda: subprocess.DEVNULL
compat_tokenize_tokenize = tokenize.tokenize
compat_urllib_error = urllib.error
-compat_urllib_HTTPError = urllib.error.HTTPError
+compat_urllib_HTTPError = compat_HTTPError
compat_urllib_parse = urllib.parse
compat_urllib_parse_parse_qs = urllib.parse.parse_qs
compat_urllib_parse_quote = urllib.parse.quote
diff --git a/yt_dlp/networking/exceptions.py b/yt_dlp/networking/exceptions.py
index 12441901c9..9037f18e2a 100644
--- a/yt_dlp/networking/exceptions.py
+++ b/yt_dlp/networking/exceptions.py
@@ -1,9 +1,8 @@
from __future__ import annotations
import typing
-import urllib.error
-from ..utils import YoutubeDLError, deprecation_warning
+from ..utils import YoutubeDLError
if typing.TYPE_CHECKING:
from .common import RequestHandler, Response
@@ -101,117 +100,4 @@ class ProxyError(TransportError):
pass
-class _CompatHTTPError(urllib.error.HTTPError, HTTPError):
- """
- Provides backwards compatibility with urllib.error.HTTPError.
- Do not use this class directly, use HTTPError instead.
- """
-
- def __init__(self, http_error: HTTPError):
- super().__init__(
- url=http_error.response.url,
- code=http_error.status,
- msg=http_error.msg,
- hdrs=http_error.response.headers,
- fp=http_error.response
- )
- self._closer.close_called = True # Disable auto close
- self._http_error = http_error
- HTTPError.__init__(self, http_error.response, redirect_loop=http_error.redirect_loop)
-
- @property
- def status(self):
- return self._http_error.status
-
- @status.setter
- def status(self, value):
- return
-
- @property
- def reason(self):
- return self._http_error.reason
-
- @reason.setter
- def reason(self, value):
- return
-
- @property
- def headers(self):
- deprecation_warning('HTTPError.headers is deprecated, use HTTPError.response.headers instead')
- return self._http_error.response.headers
-
- @headers.setter
- def headers(self, value):
- return
-
- def info(self):
- deprecation_warning('HTTPError.info() is deprecated, use HTTPError.response.headers instead')
- return self.response.headers
-
- def getcode(self):
- deprecation_warning('HTTPError.getcode is deprecated, use HTTPError.status instead')
- return self.status
-
- def geturl(self):
- deprecation_warning('HTTPError.geturl is deprecated, use HTTPError.response.url instead')
- return self.response.url
-
- @property
- def code(self):
- deprecation_warning('HTTPError.code is deprecated, use HTTPError.status instead')
- return self.status
-
- @code.setter
- def code(self, value):
- return
-
- @property
- def url(self):
- deprecation_warning('HTTPError.url is deprecated, use HTTPError.response.url instead')
- return self.response.url
-
- @url.setter
- def url(self, value):
- return
-
- @property
- def hdrs(self):
- deprecation_warning('HTTPError.hdrs is deprecated, use HTTPError.response.headers instead')
- return self.response.headers
-
- @hdrs.setter
- def hdrs(self, value):
- return
-
- @property
- def filename(self):
- deprecation_warning('HTTPError.filename is deprecated, use HTTPError.response.url instead')
- return self.response.url
-
- @filename.setter
- def filename(self, value):
- return
-
- def __getattr__(self, name):
- # File operations are passed through the response.
- # Warn for some commonly used ones
- passthrough_warnings = {
- 'read': 'response.read()',
- # technically possibly due to passthrough, but we should discourage this
- 'get_header': 'response.get_header()',
- 'readable': 'response.readable()',
- 'closed': 'response.closed',
- 'tell': 'response.tell()',
- }
- if name in passthrough_warnings:
- deprecation_warning(f'HTTPError.{name} is deprecated, use HTTPError.{passthrough_warnings[name]} instead')
- return super().__getattr__(name)
-
- def __str__(self):
- return str(self._http_error)
-
- def __repr__(self):
- return repr(self._http_error)
-
-
network_exceptions = (HTTPError, TransportError)
From f24e44e8cbd88ce338d52f594a19330f64d38b50 Mon Sep 17 00:00:00 2001
From: sepro <4618135+seproDev@users.noreply.github.com>
Date: Sat, 20 Jan 2024 06:08:55 +0100
Subject: [PATCH 002/112] [webvtt] Don't parse single fragment files (#9034)
Partially addresses #5804
Authored by: seproDev
---
yt_dlp/downloader/hls.py | 7 +++++--
1 file changed, 5 insertions(+), 2 deletions(-)
diff --git a/yt_dlp/downloader/hls.py b/yt_dlp/downloader/hls.py
index d4b3f03200..4ac5d99dc0 100644
--- a/yt_dlp/downloader/hls.py
+++ b/yt_dlp/downloader/hls.py
@@ -369,7 +369,10 @@ class HlsFD(FragmentFD):
return output.getvalue().encode()
- self.download_and_append_fragments(
- ctx, fragments, info_dict, pack_func=pack_fragment, finish_func=fin_fragments)
+ if len(fragments) == 1:
+ self.download_and_append_fragments(ctx, fragments, info_dict)
+ else:
+ self.download_and_append_fragments(
+ ctx, fragments, info_dict, pack_func=pack_fragment, finish_func=fin_fragments)
else:
return self.download_and_append_fragments(ctx, fragments, info_dict)
From 35f4f764a786685ea45d84abe1cf1ad3847f4c97 Mon Sep 17 00:00:00 2001
From: coletdjnz
Date: Sun, 21 Jan 2024 10:03:33 +1300
Subject: [PATCH 003/112] [rh:requests] Apply `remove_dot_segments` to absolute
redirect locations
Fixes https://github.com/yt-dlp/yt-dlp/issues/9020
Authored by: coletdjnz
---
test/test_networking.py | 25 ++++++++++++++++---------
yt_dlp/networking/_requests.py | 5 +++++
2 files changed, 21 insertions(+), 9 deletions(-)
diff --git a/test/test_networking.py b/test/test_networking.py
index 62325aa8e0..8cadd86f5a 100644
--- a/test/test_networking.py
+++ b/test/test_networking.py
@@ -180,6 +180,12 @@ class HTTPTestRequestHandler(http.server.BaseHTTPRequestHandler):
self.send_header('Location', '/a/b/./../../headers')
self.send_header('Content-Length', '0')
self.end_headers()
+ elif self.path == '/redirect_dotsegments_absolute':
+ self.send_response(301)
+ # redirect to /headers but with dot segments before - absolute url
+ self.send_header('Location', f'http://127.0.0.1:{http_server_port(self.server)}/a/b/./../../headers')
+ self.send_header('Content-Length', '0')
+ self.end_headers()
elif self.path.startswith('/redirect_'):
self._redirect()
elif self.path.startswith('/method'):
@@ -345,16 +351,17 @@ class TestHTTPRequestHandler(TestRequestHandlerBase):
res.close()
@pytest.mark.parametrize('handler', ['Urllib', 'Requests'], indirect=True)
- def test_remove_dot_segments(self, handler):
- with handler() as rh:
+ @pytest.mark.parametrize('path', [
+ '/a/b/./../../headers',
+ '/redirect_dotsegments',
+ # https://github.com/yt-dlp/yt-dlp/issues/9020
+ '/redirect_dotsegments_absolute',
+ ])
+ def test_remove_dot_segments(self, handler, path):
+ with handler(verbose=True) as rh:
# This isn't a comprehensive test,
- # but it should be enough to check whether the handler is removing dot segments
- res = validate_and_send(rh, Request(f'http://127.0.0.1:{self.http_port}/a/b/./../../headers'))
- assert res.status == 200
- assert res.url == f'http://127.0.0.1:{self.http_port}/headers'
- res.close()
-
- res = validate_and_send(rh, Request(f'http://127.0.0.1:{self.http_port}/redirect_dotsegments'))
+ # but it should be enough to check whether the handler is removing dot segments in required scenarios
+ res = validate_and_send(rh, Request(f'http://127.0.0.1:{self.http_port}{path}'))
assert res.status == 200
assert res.url == f'http://127.0.0.1:{self.http_port}/headers'
res.close()
diff --git a/yt_dlp/networking/_requests.py b/yt_dlp/networking/_requests.py
index e129110ca4..00e4bdb490 100644
--- a/yt_dlp/networking/_requests.py
+++ b/yt_dlp/networking/_requests.py
@@ -8,6 +8,7 @@ import warnings
from ..dependencies import brotli, requests, urllib3
from ..utils import bug_reports_message, int_or_none, variadic
+from ..utils.networking import normalize_url
if requests is None:
raise ImportError('requests module is not installed')
@@ -199,6 +200,10 @@ class RequestsSession(requests.sessions.Session):
prepared_request.method = new_method
+ # Requests fails to resolve dot segments on absolute redirect locations
+ # See: https://github.com/yt-dlp/yt-dlp/issues/9020
+ prepared_request.url = normalize_url(prepared_request.url)
+
def rebuild_auth(self, prepared_request, response):
# HACK: undo status code change from rebuild_method, if applicable.
# rebuild_auth runs after requests would remove headers/body based on status code
From fcaa2e735b00b15a2b0d9f55f4187c654b4b5b39 Mon Sep 17 00:00:00 2001
From: "lauren n. liberda"
Date: Sun, 21 Jan 2024 03:22:26 +0100
Subject: [PATCH 004/112] [ie/Sejm,RedCDNLivx] Add extractors (#8676)
Authored by: selfisekai
---
yt_dlp/extractor/_extractors.py | 2 +
yt_dlp/extractor/redge.py | 135 ++++++++++++++++++++
yt_dlp/extractor/sejmpl.py | 218 ++++++++++++++++++++++++++++++++
3 files changed, 355 insertions(+)
create mode 100644 yt_dlp/extractor/redge.py
create mode 100644 yt_dlp/extractor/sejmpl.py
diff --git a/yt_dlp/extractor/_extractors.py b/yt_dlp/extractor/_extractors.py
index 3d5c3eb60f..31bef1eb5d 100644
--- a/yt_dlp/extractor/_extractors.py
+++ b/yt_dlp/extractor/_extractors.py
@@ -1593,6 +1593,7 @@ from .redbulltv import (
RedBullIE,
)
from .reddit import RedditIE
+from .redge import RedCDNLivxIE
from .redgifs import (
RedGifsIE,
RedGifsSearchIE,
@@ -1727,6 +1728,7 @@ from .scte import (
)
from .scrolller import ScrolllerIE
from .seeker import SeekerIE
+from .sejmpl import SejmIE
from .senalcolombia import SenalColombiaLiveIE
from .senategov import SenateISVPIE, SenateGovIE
from .sendtonews import SendtoNewsIE
diff --git a/yt_dlp/extractor/redge.py b/yt_dlp/extractor/redge.py
new file mode 100644
index 0000000000..875d6f8aa5
--- /dev/null
+++ b/yt_dlp/extractor/redge.py
@@ -0,0 +1,135 @@
+import functools
+
+from .common import InfoExtractor
+from ..networking import HEADRequest
+from ..utils import (
+ float_or_none,
+ int_or_none,
+ join_nonempty,
+ parse_qs,
+ update_url_query,
+)
+from ..utils.traversal import traverse_obj
+
+
+class RedCDNLivxIE(InfoExtractor):
+ _VALID_URL = r'https?://[^.]+\.(?:dcs\.redcdn|atmcdn)\.pl/(?:live(?:dash|hls|ss)|nvr)/o2/(?P[^/?#]+)/(?P[^?#]+)\.livx'
+ IE_NAME = 'redcdnlivx'
+
+ _TESTS = [{
+ 'url': 'https://r.dcs.redcdn.pl/livedash/o2/senat/ENC02/channel.livx?indexMode=true&startTime=638272860000&stopTime=638292544000',
+ 'info_dict': {
+ 'id': 'ENC02-638272860000-638292544000',
+ 'ext': 'mp4',
+ 'title': 'ENC02',
+ 'duration': 19683.982,
+ 'live_status': 'was_live',
+ },
+ }, {
+ 'url': 'https://r.dcs.redcdn.pl/livedash/o2/sejm/ENC18/live.livx?indexMode=true&startTime=722333096000&stopTime=722335562000',
+ 'info_dict': {
+ 'id': 'ENC18-722333096000-722335562000',
+ 'ext': 'mp4',
+ 'title': 'ENC18',
+ 'duration': 2463.995,
+ 'live_status': 'was_live',
+ },
+ }, {
+ 'url': 'https://r.dcs.redcdn.pl/livehls/o2/sportevolution/live/triathlon2018/warsaw.livx/playlist.m3u8?startTime=550305000000&stopTime=550327620000',
+ 'info_dict': {
+ 'id': 'triathlon2018-warsaw-550305000000-550327620000',
+ 'ext': 'mp4',
+ 'title': 'triathlon2018/warsaw',
+ 'duration': 22619.98,
+ 'live_status': 'was_live',
+ },
+ }, {
+ 'url': 'https://n-25-12.dcs.redcdn.pl/nvr/o2/sejm/Migacz-ENC01/1.livx?startTime=722347200000&stopTime=722367345000',
+ 'only_matching': True,
+ }, {
+ 'url': 'https://redir.atmcdn.pl/nvr/o2/sejm/ENC08/1.livx?startTime=503831270000&stopTime=503840040000',
+ 'only_matching': True,
+ }]
+
+ """
+ Known methods (first in url path):
+ - `livedash` - DASH MPD
+ - `livehls` - HTTP Live Streaming
+ - `livess` - IIS Smooth Streaming
+ - `nvr` - CCTV mode, directly returns a file, typically flv, avc1, aac
+ - `sc` - shoutcast/icecast (audio streams, like radio)
+ """
+
+ def _real_extract(self, url):
+ tenant, path = self._match_valid_url(url).group('tenant', 'id')
+ qs = parse_qs(url)
+ start_time = traverse_obj(qs, ('startTime', 0, {int_or_none}))
+ stop_time = traverse_obj(qs, ('stopTime', 0, {int_or_none}))
+
+ def livx_mode(mode):
+ suffix = ''
+ if mode == 'livess':
+ suffix = '/manifest'
+ elif mode == 'livehls':
+ suffix = '/playlist.m3u8'
+ file_qs = {}
+ if start_time:
+ file_qs['startTime'] = start_time
+ if stop_time:
+ file_qs['stopTime'] = stop_time
+ if mode == 'nvr':
+ file_qs['nolimit'] = 1
+ elif mode != 'sc':
+ file_qs['indexMode'] = 'true'
+ return update_url_query(f'https://r.dcs.redcdn.pl/{mode}/o2/{tenant}/{path}.livx{suffix}', file_qs)
+
+ # no id or title for a transmission. making ones up.
+ title = path \
+ .replace('/live', '').replace('live/', '') \
+ .replace('/channel', '').replace('channel/', '') \
+ .strip('/')
+ video_id = join_nonempty(title.replace('/', '-'), start_time, stop_time)
+
+ formats = []
+ # downloading the manifest separately here instead of _extract_ism_formats to also get some stream metadata
+ ism_res = self._download_xml_handle(
+ livx_mode('livess'), video_id,
+ note='Downloading ISM manifest',
+ errnote='Failed to download ISM manifest',
+ fatal=False)
+ ism_doc = None
+ if ism_res is not False:
+ ism_doc, ism_urlh = ism_res
+ formats, _ = self._parse_ism_formats_and_subtitles(ism_doc, ism_urlh.url, 'ss')
+
+ nvr_urlh = self._request_webpage(
+ HEADRequest(livx_mode('nvr')), video_id, 'Follow flv file redirect', fatal=False,
+ expected_status=lambda _: True)
+ if nvr_urlh and nvr_urlh.status == 200:
+ formats.append({
+ 'url': nvr_urlh.url,
+ 'ext': 'flv',
+ 'format_id': 'direct-0',
+ 'preference': -1, # might be slow
+ })
+ formats.extend(self._extract_mpd_formats(livx_mode('livedash'), video_id, mpd_id='dash', fatal=False))
+ formats.extend(self._extract_m3u8_formats(
+ livx_mode('livehls'), video_id, m3u8_id='hls', ext='mp4', fatal=False))
+
+ time_scale = traverse_obj(ism_doc, ('@TimeScale', {int_or_none})) or 10000000
+ duration = traverse_obj(
+ ism_doc, ('@Duration', {functools.partial(float_or_none, scale=time_scale)})) or None
+
+ live_status = None
+ if traverse_obj(ism_doc, '@IsLive') == 'TRUE':
+ live_status = 'is_live'
+ elif duration:
+ live_status = 'was_live'
+
+ return {
+ 'id': video_id,
+ 'title': title,
+ 'formats': formats,
+ 'duration': duration,
+ 'live_status': live_status,
+ }
diff --git a/yt_dlp/extractor/sejmpl.py b/yt_dlp/extractor/sejmpl.py
new file mode 100644
index 0000000000..29cb0152a2
--- /dev/null
+++ b/yt_dlp/extractor/sejmpl.py
@@ -0,0 +1,218 @@
+import datetime
+
+from .common import InfoExtractor
+from .redge import RedCDNLivxIE
+from ..utils import (
+ clean_html,
+ join_nonempty,
+ js_to_json,
+ strip_or_none,
+ update_url_query,
+)
+from ..utils.traversal import traverse_obj
+
+
+def is_dst(date):
+ last_march = datetime.datetime(date.year, 3, 31)
+ last_october = datetime.datetime(date.year, 10, 31)
+ last_sunday_march = last_march - datetime.timedelta(days=last_march.isoweekday() % 7)
+ last_sunday_october = last_october - datetime.timedelta(days=last_october.isoweekday() % 7)
+ return last_sunday_march.replace(hour=2) <= date <= last_sunday_october.replace(hour=3)
+
+
+def rfc3339_to_atende(date):
+ date = datetime.datetime.fromisoformat(date)
+ date = date + datetime.timedelta(hours=1 if is_dst(date) else 0)
+ return int((date.timestamp() - 978307200) * 1000)
+
+
+class SejmIE(InfoExtractor):
+ _VALID_URL = (
+ r'https?://(?:www\.)?sejm\.gov\.pl/[Ss]ejm(?P\d+)\.nsf/transmisje(?:_arch)?\.xsp(?:\?[^#]*)?#(?P[\dA-F]+)',
+ r'https?://(?:www\.)?sejm\.gov\.pl/[Ss]ejm(?P\d+)\.nsf/transmisje(?:_arch)?\.xsp\?(?:[^#]+&)?unid=(?P[\dA-F]+)',
+ r'https?://sejm-embed\.redcdn\.pl/[Ss]ejm(?P\d+)\.nsf/VideoFrame\.xsp/(?P[\dA-F]+)',
+ )
+ IE_NAME = 'sejm'
+
+ _TESTS = [{
+ # multiple cameras, polish SL iterpreter
+ 'url': 'https://www.sejm.gov.pl/Sejm10.nsf/transmisje_arch.xsp#6181EF1AD9CEEBB5C1258A6D006452B5',
+ 'info_dict': {
+ 'id': '6181EF1AD9CEEBB5C1258A6D006452B5',
+ 'title': '1. posiedzenie Sejmu X kadencji',
+ 'duration': 20145,
+ 'live_status': 'was_live',
+ 'location': 'Sala Posiedzeń',
+ },
+ 'playlist': [{
+ 'info_dict': {
+ 'id': 'ENC01-722340000000-722360145000',
+ 'ext': 'mp4',
+ 'duration': 20145,
+ 'title': '1. posiedzenie Sejmu X kadencji - ENC01',
+ 'live_status': 'was_live',
+ },
+ }, {
+ 'info_dict': {
+ 'id': 'ENC30-722340000000-722360145000',
+ 'ext': 'mp4',
+ 'duration': 20145,
+ 'title': '1. posiedzenie Sejmu X kadencji - ENC30',
+ 'live_status': 'was_live',
+ },
+ }, {
+ 'info_dict': {
+ 'id': 'ENC31-722340000000-722360145000',
+ 'ext': 'mp4',
+ 'duration': 20145,
+ 'title': '1. posiedzenie Sejmu X kadencji - ENC31',
+ 'live_status': 'was_live',
+ },
+ }, {
+ 'info_dict': {
+ 'id': 'ENC32-722340000000-722360145000',
+ 'ext': 'mp4',
+ 'duration': 20145,
+ 'title': '1. posiedzenie Sejmu X kadencji - ENC32',
+ 'live_status': 'was_live',
+ },
+ }, {
+ # sign lang interpreter
+ 'info_dict': {
+ 'id': 'Migacz-ENC01-1-722340000000-722360145000',
+ 'ext': 'mp4',
+ 'duration': 20145,
+ 'title': '1. posiedzenie Sejmu X kadencji - Migacz-ENC01',
+ 'live_status': 'was_live',
+ },
+ }],
+ }, {
+ 'url': 'https://www.sejm.gov.pl/Sejm8.nsf/transmisje.xsp?unid=9377A9D65518E9A5C125808E002E9FF2',
+ 'info_dict': {
+ 'id': '9377A9D65518E9A5C125808E002E9FF2',
+ 'title': 'Debata "Lepsza Polska: obywatelska"',
+ 'description': 'KP .Nowoczesna',
+ 'duration': 8770,
+ 'live_status': 'was_live',
+ 'location': 'sala kolumnowa im. Kazimierza Pużaka (bud. C-D)',
+ },
+ 'playlist': [{
+ 'info_dict': {
+ 'id': 'ENC08-1-503831270000-503840040000',
+ 'ext': 'mp4',
+ 'duration': 8770,
+ 'title': 'Debata "Lepsza Polska: obywatelska" - ENC08',
+ 'live_status': 'was_live',
+ },
+ }],
+ }, {
+ # 7th term is very special, since it does not use redcdn livx
+ 'url': 'https://www.sejm.gov.pl/sejm7.nsf/transmisje_arch.xsp?rok=2015&month=11#A6E6D475ECCC6FE5C1257EF90034817F',
+ 'info_dict': {
+ 'id': 'A6E6D475ECCC6FE5C1257EF90034817F',
+ 'title': 'Konferencja prasowa - Stanowisko SLD ws. składu nowego rządu',
+ 'description': 'SLD - Biuro Prasowe Klubu',
+ 'duration': 514,
+ 'location': 'sala 101/bud. C',
+ 'live_status': 'was_live',
+ },
+ 'playlist': [{
+ 'info_dict': {
+ 'id': 'A6E6D475ECCC6FE5C1257EF90034817F',
+ 'ext': 'mp4',
+ 'title': 'Konferencja prasowa - Stanowisko SLD ws. składu nowego rządu',
+ 'duration': 514,
+ },
+ }],
+ }, {
+ 'url': 'https://sejm-embed.redcdn.pl/Sejm10.nsf/VideoFrame.xsp/FED58EABB97FBD53C1258A7400386492',
+ 'only_matching': True,
+ }]
+
+ def _real_extract(self, url):
+ term, video_id = self._match_valid_url(url).group('term', 'id')
+ frame = self._download_webpage(
+ f'https://sejm-embed.redcdn.pl/Sejm{term}.nsf/VideoFrame.xsp/{video_id}',
+ video_id)
+ # despite it says "transmisje_arch", it works for live streams too!
+ data = self._download_json(
+ f'https://www.sejm.gov.pl/Sejm{term}.nsf/transmisje_arch.xsp/json/{video_id}',
+ video_id)
+ params = data['params']
+
+ title = strip_or_none(data.get('title'))
+
+ if data.get('status') == 'VIDEO_ENDED':
+ live_status = 'was_live'
+ elif data.get('status') == 'VIDEO_PLAYING':
+ live_status = 'is_live'
+ else:
+ live_status = None
+ self.report_warning(f'unknown status: {data.get("status")}')
+
+ start_time = rfc3339_to_atende(params['start'])
+ # current streams have a stop time of *expected* end of session, but actual times
+ # can change during the transmission. setting a stop_time would artificially
+ # end the stream at that time, while the session actually keeps going.
+ if live_status == 'was_live':
+ stop_time = rfc3339_to_atende(params['stop'])
+ duration = (stop_time - start_time) // 1000
+ else:
+ stop_time, duration = None, None
+
+ entries = []
+
+ def add_entry(file, legacy_file=False):
+ if not file:
+ return
+ file = self._proto_relative_url(file)
+ if not legacy_file:
+ file = update_url_query(file, {'startTime': start_time})
+ if stop_time is not None:
+ file = update_url_query(file, {'stopTime': stop_time})
+ stream_id = self._search_regex(r'/o2/sejm/([^/]+)/[^./]+\.livx', file, 'stream id')
+ common_info = {
+ 'url': file,
+ 'duration': duration,
+ }
+ if legacy_file:
+ entries.append({
+ **common_info,
+ 'id': video_id,
+ 'title': title,
+ })
+ else:
+ entries.append({
+ **common_info,
+ '_type': 'url_transparent',
+ 'ie_key': RedCDNLivxIE.ie_key(),
+ 'id': stream_id,
+ 'title': join_nonempty(title, stream_id, delim=' - '),
+ })
+
+ cameras = self._search_json(
+ r'var\s+cameras\s*=', frame, 'camera list', video_id,
+ contains_pattern=r'\[(?s:.+)\]', transform_source=js_to_json,
+ fatal=False) or []
+ for camera_file in traverse_obj(cameras, (..., 'file', {dict})):
+ if camera_file.get('flv'):
+ add_entry(camera_file['flv'])
+ elif camera_file.get('mp4'):
+ # this is only a thing in 7th term. no streams before, and starting 8th it's redcdn livx
+ add_entry(camera_file['mp4'], legacy_file=True)
+ else:
+ self.report_warning('Unknown camera stream type found')
+
+ if params.get('mig'):
+ add_entry(self._search_regex(r"var sliUrl\s*=\s*'([^']+)'", frame, 'sign language interpreter url', fatal=False))
+
+ return {
+ '_type': 'playlist',
+ 'entries': entries,
+ 'id': video_id,
+ 'title': title,
+ 'description': clean_html(data.get('desc')) or None,
+ 'duration': duration,
+ 'live_status': live_status,
+ 'location': strip_or_none(data.get('location')),
+ }
From 5a63454b3637b3603434026cddfeac509218b90e Mon Sep 17 00:00:00 2001
From: Martin Renold
Date: Sun, 21 Jan 2024 03:45:38 +0100
Subject: [PATCH 005/112] [ie/mx3] Add extractors (#8736)
Authored by: martinxyz
---
yt_dlp/extractor/_extractors.py | 5 +
yt_dlp/extractor/mx3.py | 171 ++++++++++++++++++++++++++++++++
2 files changed, 176 insertions(+)
create mode 100644 yt_dlp/extractor/mx3.py
diff --git a/yt_dlp/extractor/_extractors.py b/yt_dlp/extractor/_extractors.py
index 31bef1eb5d..c4f1ccb8e4 100644
--- a/yt_dlp/extractor/_extractors.py
+++ b/yt_dlp/extractor/_extractors.py
@@ -1137,6 +1137,11 @@ from .musicdex import (
MusicdexArtistIE,
MusicdexPlaylistIE,
)
+from .mx3 import (
+ Mx3IE,
+ Mx3NeoIE,
+ Mx3VolksmusikIE,
+)
from .mxplayer import (
MxplayerIE,
MxplayerShowIE,
diff --git a/yt_dlp/extractor/mx3.py b/yt_dlp/extractor/mx3.py
new file mode 100644
index 0000000000..cb9f50e0cf
--- /dev/null
+++ b/yt_dlp/extractor/mx3.py
@@ -0,0 +1,171 @@
+import re
+
+from .common import InfoExtractor
+from ..networking import HEADRequest
+from ..utils import (
+ get_element_by_class,
+ int_or_none,
+ try_call,
+ url_or_none,
+ urlhandle_detect_ext,
+)
+from ..utils.traversal import traverse_obj
+
+
+class Mx3BaseIE(InfoExtractor):
+ _VALID_URL_TMPL = r'https?://(?:www\.)?%s/t/(?P\w+)'
+ _FORMATS = [{
+ 'url': 'player_asset',
+ 'format_id': 'default',
+ 'quality': 0,
+ }, {
+ 'url': 'player_asset?quality=hd',
+ 'format_id': 'hd',
+ 'quality': 1,
+ }, {
+ 'url': 'download',
+ 'format_id': 'download',
+ 'quality': 2,
+ }, {
+ 'url': 'player_asset?quality=source',
+ 'format_id': 'source',
+ 'quality': 2,
+ }]
+
+ def _extract_formats(self, track_id):
+ formats = []
+ for fmt in self._FORMATS:
+ format_url = f'https://{self._DOMAIN}/tracks/{track_id}/{fmt["url"]}'
+ urlh = self._request_webpage(
+ HEADRequest(format_url), track_id, fatal=False, expected_status=404,
+ note=f'Checking for format {fmt["format_id"]}')
+ if urlh and urlh.status == 200:
+ formats.append({
+ **fmt,
+ 'url': format_url,
+ 'ext': urlhandle_detect_ext(urlh),
+ 'filesize': int_or_none(urlh.headers.get('Content-Length')),
+ })
+ return formats
+
+ def _real_extract(self, url):
+ track_id = self._match_id(url)
+ webpage = self._download_webpage(url, track_id)
+ more_info = get_element_by_class('single-more-info', webpage)
+ data = self._download_json(f'https://{self._DOMAIN}/t/{track_id}.json', track_id, fatal=False)
+
+ def get_info_field(name):
+ return self._html_search_regex(
+ rf']*>\s*{name}\s*\s*]*>(.*?)',
+ more_info, name, default=None, flags=re.DOTALL)
+
+ return {
+ 'id': track_id,
+ 'formats': self._extract_formats(track_id),
+ 'genre': self._html_search_regex(
+ r']+class="single-band-genre"[^>]*>([^<]+)
', webpage, 'genre', default=None),
+ 'release_year': int_or_none(get_info_field('Year of creation')),
+ 'description': get_info_field('Description'),
+ 'tags': try_call(lambda: get_info_field('Tag').split(', '), list),
+ **traverse_obj(data, {
+ 'title': ('title', {str}),
+ 'artist': (('performer_name', 'artist'), {str}),
+ 'album_artist': ('artist', {str}),
+ 'composer': ('composer_name', {str}),
+ 'thumbnail': (('picture_url_xlarge', 'picture_url'), {url_or_none}),
+ }, get_all=False),
+ }
+
+
+class Mx3IE(Mx3BaseIE):
+ _DOMAIN = 'mx3.ch'
+ _VALID_URL = Mx3BaseIE._VALID_URL_TMPL % re.escape(_DOMAIN)
+ _TESTS = [{
+ 'url': 'https://mx3.ch/t/1Cru',
+ 'md5': '7ba09e9826b4447d4e1ce9d69e0e295f',
+ 'info_dict': {
+ 'id': '1Cru',
+ 'ext': 'wav',
+ 'artist': 'Godina',
+ 'album_artist': 'Tortue Tortue',
+ 'composer': 'Olivier Godinat',
+ 'genre': 'Rock',
+ 'thumbnail': 'https://mx3.ch/pictures/mx3/file/0101/4643/square_xlarge/1-s-envoler-1.jpg?1630272813',
+ 'title': "S'envoler",
+ 'release_year': 2021,
+ 'tags': [],
+ }
+ }, {
+ 'url': 'https://mx3.ch/t/1LIY',
+ 'md5': '48293cb908342547827f963a5a2e9118',
+ 'info_dict': {
+ 'id': '1LIY',
+ 'ext': 'mov',
+ 'artist': 'Tania Kimfumu',
+ 'album_artist': 'The Broots',
+ 'composer': 'Emmanuel Diserens',
+ 'genre': 'Electro',
+ 'thumbnail': 'https://mx3.ch/pictures/mx3/file/0110/0003/video_xlarge/frame_0000.png?1686963670',
+ 'title': 'The Broots-Larytta remix "Begging For Help"',
+ 'release_year': 2023,
+ 'tags': ['the broots', 'cassata records', 'larytta'],
+ 'description': '"Begging for Help" Larytta Remix Official Video\nRealized By Kali Donkilie in 2023',
+ }
+ }, {
+ 'url': 'https://mx3.ch/t/1C6E',
+ 'md5': '1afcd578493ddb8e5008e94bb6d97e25',
+ 'info_dict': {
+ 'id': '1C6E',
+ 'ext': 'wav',
+ 'artist': 'Alien Bubblegum',
+ 'album_artist': 'Alien Bubblegum',
+ 'composer': 'Alien Bubblegum',
+ 'genre': 'Punk',
+ 'thumbnail': 'https://mx3.ch/pictures/mx3/file/0101/1551/square_xlarge/pandora-s-box-cover-with-title.png?1627054733',
+ 'title': 'Wide Awake',
+ 'release_year': 2021,
+ 'tags': ['alien bubblegum', 'bubblegum', 'alien', 'pop punk', 'poppunk'],
+ }
+ }]
+
+
+class Mx3NeoIE(Mx3BaseIE):
+ _DOMAIN = 'neo.mx3.ch'
+ _VALID_URL = Mx3BaseIE._VALID_URL_TMPL % re.escape(_DOMAIN)
+ _TESTS = [{
+ 'url': 'https://neo.mx3.ch/t/1hpd',
+ 'md5': '6d9986bbae5cac3296ec8813bf965eb2',
+ 'info_dict': {
+ 'id': '1hpd',
+ 'ext': 'wav',
+ 'artist': 'Baptiste Lopez',
+ 'album_artist': 'Kammerorchester Basel',
+ 'composer': 'Jannik Giger',
+ 'genre': 'Composition, Orchestra',
+ 'title': 'Troisième œil. Für Kammerorchester (2023)',
+ 'thumbnail': 'https://neo.mx3.ch/pictures/neo/file/0000/0241/square_xlarge/kammerorchester-basel-group-photo-2_c_-lukasz-rajchert.jpg?1560341252',
+ 'release_year': 2023,
+ 'tags': [],
+ }
+ }]
+
+
+class Mx3VolksmusikIE(Mx3BaseIE):
+ _DOMAIN = 'volksmusik.mx3.ch'
+ _VALID_URL = Mx3BaseIE._VALID_URL_TMPL % re.escape(_DOMAIN)
+ _TESTS = [{
+ 'url': 'https://volksmusik.mx3.ch/t/Zx',
+ 'md5': 'dd967a7b0c1ef898f3e072cf9c2eae3c',
+ 'info_dict': {
+ 'id': 'Zx',
+ 'ext': 'mp3',
+ 'artist': 'Ländlerkapelle GrischArt',
+ 'album_artist': 'Ländlerkapelle GrischArt',
+ 'composer': 'Urs Glauser',
+ 'genre': 'Instrumental, Graubünden',
+ 'title': 'Chämilouf',
+ 'thumbnail': 'https://volksmusik.mx3.ch/pictures/vxm/file/0000/3815/square_xlarge/grischart1.jpg?1450530120',
+ 'release_year': 2012,
+ 'tags': [],
+ }
+ }]
From 9f1e9dab21bbe651544c8f4663b0e615dc450e4d Mon Sep 17 00:00:00 2001
From: dasidiot <140998618+dasidiot@users.noreply.github.com>
Date: Sat, 20 Jan 2024 21:46:53 -0500
Subject: [PATCH 006/112] [ie/motherless] Support uploader playlists (#8994)
Authored by: dasidiot
---
yt_dlp/extractor/_extractors.py | 1 +
yt_dlp/extractor/motherless.py | 31 ++++++++++++++++++++++++++++---
2 files changed, 29 insertions(+), 3 deletions(-)
diff --git a/yt_dlp/extractor/_extractors.py b/yt_dlp/extractor/_extractors.py
index c4f1ccb8e4..a273ae0d9c 100644
--- a/yt_dlp/extractor/_extractors.py
+++ b/yt_dlp/extractor/_extractors.py
@@ -1111,6 +1111,7 @@ from .motherless import (
MotherlessIE,
MotherlessGroupIE,
MotherlessGalleryIE,
+ MotherlessUploaderIE,
)
from .motorsport import MotorsportIE
from .moviepilot import MoviepilotIE
diff --git a/yt_dlp/extractor/motherless.py b/yt_dlp/extractor/motherless.py
index e359c44e93..160150a7b6 100644
--- a/yt_dlp/extractor/motherless.py
+++ b/yt_dlp/extractor/motherless.py
@@ -177,6 +177,7 @@ class MotherlessIE(InfoExtractor):
class MotherlessPaginatedIE(InfoExtractor):
+ _EXTRA_QUERY = {}
_PAGE_SIZE = 60
def _correct_path(self, url, item_id):
@@ -199,7 +200,7 @@ class MotherlessPaginatedIE(InfoExtractor):
def get_page(idx):
page = idx + 1
current_page = webpage if not idx else self._download_webpage(
- real_url, item_id, note=f'Downloading page {page}', query={'page': page})
+ real_url, item_id, note=f'Downloading page {page}', query={'page': page, **self._EXTRA_QUERY})
yield from self._extract_entries(current_page, real_url)
return self.playlist_result(
@@ -213,7 +214,7 @@ class MotherlessGroupIE(MotherlessPaginatedIE):
'url': 'http://motherless.com/gv/movie_scenes',
'info_dict': {
'id': 'movie_scenes',
- 'title': 'Movie Scenes',
+ 'title': 'Movie Scenes - Videos - Hot and sexy scenes from "regular" movies... Beautiful actresses fully',
},
'playlist_mincount': 540,
}, {
@@ -244,7 +245,7 @@ class MotherlessGalleryIE(MotherlessPaginatedIE):
'id': '338999F',
'title': 'Random',
},
- 'playlist_mincount': 190,
+ 'playlist_mincount': 171,
}, {
'url': 'https://motherless.com/GVABD6213',
'info_dict': {
@@ -270,3 +271,27 @@ class MotherlessGalleryIE(MotherlessPaginatedIE):
def _correct_path(self, url, item_id):
return urllib.parse.urljoin(url, f'/GV{item_id}')
+
+
+class MotherlessUploaderIE(MotherlessPaginatedIE):
+ _VALID_URL = r'https?://(?:www\.)?motherless\.com/u/(?P\w+)/?(?:$|[?#])'
+ _TESTS = [{
+ 'url': 'https://motherless.com/u/Mrgo4hrs2023',
+ 'info_dict': {
+ 'id': 'Mrgo4hrs2023',
+ 'title': "Mrgo4hrs2023's Uploads - Videos",
+ },
+ 'playlist_mincount': 32,
+ }, {
+ 'url': 'https://motherless.com/u/Happy_couple?t=v',
+ 'info_dict': {
+ 'id': 'Happy_couple',
+ 'title': "Happy_couple's Uploads - Videos",
+ },
+ 'playlist_mincount': 8,
+ }]
+
+ _EXTRA_QUERY = {'t': 'v'}
+
+ def _correct_path(self, url, item_id):
+ return urllib.parse.urljoin(url, f'/u/{item_id}?t=v')
From 3e083191cdc34dd8c482da9a9b4bc682f824cb9d Mon Sep 17 00:00:00 2001
From: u-spec-png
Date: Sun, 21 Jan 2024 19:50:14 +0100
Subject: [PATCH 007/112] [ie/Newgrounds:user] Fix extractor (#9046)
Closes #7308
Authored by: u-spec-png
---
yt_dlp/extractor/newgrounds.py | 19 ++++++++-----------
1 file changed, 8 insertions(+), 11 deletions(-)
diff --git a/yt_dlp/extractor/newgrounds.py b/yt_dlp/extractor/newgrounds.py
index 9e3286dfe7..9601cd10e7 100644
--- a/yt_dlp/extractor/newgrounds.py
+++ b/yt_dlp/extractor/newgrounds.py
@@ -3,15 +3,15 @@ import re
from .common import InfoExtractor
from ..utils import (
+ OnDemandPagedList,
clean_html,
extract_attributes,
get_element_by_id,
int_or_none,
parse_count,
parse_duration,
+ traverse_obj,
unified_timestamp,
- OnDemandPagedList,
- try_get,
)
@@ -263,19 +263,16 @@ class NewgroundsUserIE(InfoExtractor):
def _fetch_page(self, channel_id, url, page):
page += 1
posts_info = self._download_json(
- f'{url}/page/{page}', channel_id,
+ f'{url}?page={page}', channel_id,
note=f'Downloading page {page}', headers={
'Accept': 'application/json, text/javascript, */*; q = 0.01',
'X-Requested-With': 'XMLHttpRequest',
})
- sequence = posts_info.get('sequence', [])
- for year in sequence:
- posts = try_get(posts_info, lambda x: x['years'][str(year)]['items'])
- for post in posts:
- path, media_id = self._search_regex(
- r']+\bhref=["\'][^"\']+((?:portal/view|audio/listen)/(\d+))[^>]+>',
- post, 'url', group=(1, 2))
- yield self.url_result(f'https://www.newgrounds.com/{path}', NewgroundsIE.ie_key(), media_id)
+ for post in traverse_obj(posts_info, ('items', ..., ..., {str})):
+ path, media_id = self._search_regex(
+ r']+\bhref=["\'][^"\']+((?:portal/view|audio/listen)/(\d+))[^>]+>',
+ post, 'url', group=(1, 2))
+ yield self.url_result(f'https://www.newgrounds.com/{path}', NewgroundsIE.ie_key(), media_id)
def _real_extract(self, url):
channel_id = self._match_id(url)
From c0ecceeefe6ebd27452d9d8f20658f83ae121d04 Mon Sep 17 00:00:00 2001
From: gmes78
Date: Sun, 21 Jan 2024 18:56:01 +0000
Subject: [PATCH 008/112] [ie/Rule34Video] Fix `_VALID_URL` (#9044)
Authored by: gmes78
---
yt_dlp/extractor/rule34video.py | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/yt_dlp/extractor/rule34video.py b/yt_dlp/extractor/rule34video.py
index e6bb4258e9..85ad7e2ff2 100644
--- a/yt_dlp/extractor/rule34video.py
+++ b/yt_dlp/extractor/rule34video.py
@@ -18,10 +18,10 @@ from ..utils.traversal import traverse_obj
class Rule34VideoIE(InfoExtractor):
- _VALID_URL = r'https?://(?:www\.)?rule34video\.com/videos/(?P\d+)'
+ _VALID_URL = r'https?://(?:www\.)?rule34video\.com/videos?/(?P\d+)'
_TESTS = [
{
- 'url': 'https://rule34video.com/videos/3065157/shot-it-mmd-hmv/',
+ 'url': 'https://rule34video.com/video/3065157/shot-it-mmd-hmv/',
'md5': 'ffccac2c23799dabbd192621ae4d04f3',
'info_dict': {
'id': '3065157',
From c099ec9392b0283dde34b290d1a04158ad8eb882 Mon Sep 17 00:00:00 2001
From: Stefan Lobbenmeier
Date: Sun, 21 Jan 2024 21:54:11 +0100
Subject: [PATCH 009/112] [ie/ard:mediathek] Support cookies to verify age
(#9037)
Closes #9035
Authored by: StefanLobbenmeier
---
yt_dlp/extractor/ard.py | 27 ++++++++++++++++++++++-----
1 file changed, 22 insertions(+), 5 deletions(-)
diff --git a/yt_dlp/extractor/ard.py b/yt_dlp/extractor/ard.py
index f4b1cd0756..46e68d61e2 100644
--- a/yt_dlp/extractor/ard.py
+++ b/yt_dlp/extractor/ard.py
@@ -8,6 +8,7 @@ from ..utils import (
determine_ext,
int_or_none,
join_nonempty,
+ jwt_decode_hs256,
make_archive_id,
parse_duration,
parse_iso8601,
@@ -238,6 +239,7 @@ class ARDBetaMediathekIE(InfoExtractor):
(?P[a-zA-Z0-9]+)
/?(?:[?#]|$)'''
_GEO_COUNTRIES = ['DE']
+ _TOKEN_URL = 'https://sso.ardmediathek.de/sso/token'
_TESTS = [{
'url': 'https://www.ardmediathek.de/video/filme-im-mdr/liebe-auf-vier-pfoten/mdr-fernsehen/Y3JpZDovL21kci5kZS9zZW5kdW5nLzI4MjA0MC80MjIwOTEtNDAyNTM0',
@@ -359,12 +361,27 @@ class ARDBetaMediathekIE(InfoExtractor):
def _real_extract(self, url):
display_id = self._match_id(url)
+ query = {'embedded': 'false', 'mcV6': 'true'}
+ headers = {}
+
+ if self._get_cookies(self._TOKEN_URL).get('ams'):
+ token = self._download_json(
+ self._TOKEN_URL, display_id, 'Fetching token for age verification',
+ 'Unable to fetch age verification token', fatal=False)
+ id_token = traverse_obj(token, ('idToken', {str}))
+ decoded_token = traverse_obj(id_token, ({jwt_decode_hs256}, {dict}))
+ user_id = traverse_obj(decoded_token, (('user_id', 'sub'), {str}), get_all=False)
+ if not user_id:
+ self.report_warning('Unable to extract token, continuing without authentication')
+ else:
+ headers['x-authorization'] = f'Bearer {id_token}'
+ query['userId'] = user_id
+ if decoded_token.get('age_rating') != 18:
+ self.report_warning('Account is not verified as 18+; video may be unavailable')
page_data = self._download_json(
- f'https://api.ardmediathek.de/page-gateway/pages/ard/item/{display_id}', display_id, query={
- 'embedded': 'false',
- 'mcV6': 'true',
- })
+ f'https://api.ardmediathek.de/page-gateway/pages/ard/item/{display_id}',
+ display_id, query=query, headers=headers)
# For user convenience we use the old contentId instead of the longer crid
# Ref: https://github.com/yt-dlp/yt-dlp/issues/8731#issuecomment-1874398283
@@ -383,7 +400,7 @@ class ARDBetaMediathekIE(InfoExtractor):
media_data = traverse_obj(player_data, ('mediaCollection', 'embedded', {dict}))
if player_data.get('blockedByFsk'):
- self.raise_no_formats('This video is only available after 22:00', expected=True)
+ self.raise_login_required('This video is only available for age verified users or after 22:00')
formats = []
subtitles = {}
From f0e8bc7c60b61fe18b63116c975609d76b904771 Mon Sep 17 00:00:00 2001
From: John Victor <37747572+johnvictorfs@users.noreply.github.com>
Date: Sun, 21 Jan 2024 19:36:59 -0300
Subject: [PATCH 010/112] [ie/patreon] Fix embedded HLS extraction (#8993)
Closes #8973
Authored by: johnvictorfs
---
yt_dlp/extractor/patreon.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/yt_dlp/extractor/patreon.py b/yt_dlp/extractor/patreon.py
index 9316789df2..d2ddb72cd4 100644
--- a/yt_dlp/extractor/patreon.py
+++ b/yt_dlp/extractor/patreon.py
@@ -275,7 +275,7 @@ class PatreonIE(PatreonBaseIE):
'ext': ext,
'url': post_file['url'],
}
- elif name == 'video':
+ elif name == 'video' or determine_ext(post_file.get('url')) == 'm3u8':
formats, subtitles = self._extract_m3u8_formats_and_subtitles(post_file['url'], video_id)
return {
**info,
From 9cd90447907a59c8a2727583f4a755fb23ed8cd3 Mon Sep 17 00:00:00 2001
From: chtk
Date: Mon, 22 Jan 2024 06:57:52 +0100
Subject: [PATCH 011/112] [ie/Floatplane] Improve metadata extraction (#8934)
Authored by: chtk
---
yt_dlp/extractor/floatplane.py | 103 +++++++++++++++++++++++++++------
1 file changed, 84 insertions(+), 19 deletions(-)
diff --git a/yt_dlp/extractor/floatplane.py b/yt_dlp/extractor/floatplane.py
index 2cf4d4e648..8676d73f60 100644
--- a/yt_dlp/extractor/floatplane.py
+++ b/yt_dlp/extractor/floatplane.py
@@ -11,6 +11,7 @@ from ..utils import (
join_nonempty,
parse_codecs,
parse_iso8601,
+ url_or_none,
urljoin,
)
from ..utils.traversal import traverse_obj
@@ -108,6 +109,64 @@ class FloatplaneIE(InfoExtractor):
'availability': 'subscriber_only',
},
'params': {'skip_download': 'm3u8'},
+ }, {
+ 'url': 'https://www.floatplane.com/post/65B5PNoBtf',
+ 'info_dict': {
+ 'id': '65B5PNoBtf',
+ 'description': 'I recorded the inbuilt demo mode for your 90\'s enjoyment, thanks for being Floaties!',
+ 'display_id': '65B5PNoBtf',
+ 'like_count': int,
+ 'release_timestamp': 1701249480,
+ 'uploader': 'The Trash Network',
+ 'availability': 'subscriber_only',
+ 'uploader_id': '61bc20c9a131fb692bf2a513',
+ 'uploader_url': 'https://www.floatplane.com/channel/TheTrashNetwork/home',
+ 'channel_url': 'https://www.floatplane.com/channel/TheTrashNetwork/home/thedrumthing',
+ 'comment_count': int,
+ 'title': 'The $50 electronic drum kit.',
+ 'channel_id': '64424fe73cd58cbcf8d8e131',
+ 'thumbnail': 'https://pbs.floatplane.com/blogPost_thumbnails/65B5PNoBtf/725555379422705_1701247052743.jpeg',
+ 'dislike_count': int,
+ 'channel': 'The Drum Thing',
+ 'release_date': '20231129',
+ },
+ 'playlist_count': 2,
+ 'playlist': [{
+ 'info_dict': {
+ 'id': 'ISPJjexylS',
+ 'ext': 'mp4',
+ 'release_date': '20231129',
+ 'release_timestamp': 1701249480,
+ 'title': 'The $50 electronic drum kit. .mov',
+ 'channel_id': '64424fe73cd58cbcf8d8e131',
+ 'thumbnail': 'https://pbs.floatplane.com/video_thumbnails/ISPJjexylS/335202812134041_1701249383392.jpeg',
+ 'availability': 'subscriber_only',
+ 'uploader': 'The Trash Network',
+ 'duration': 622,
+ 'channel': 'The Drum Thing',
+ 'uploader_id': '61bc20c9a131fb692bf2a513',
+ 'channel_url': 'https://www.floatplane.com/channel/TheTrashNetwork/home/thedrumthing',
+ 'uploader_url': 'https://www.floatplane.com/channel/TheTrashNetwork/home',
+ },
+ }, {
+ 'info_dict': {
+ 'id': 'qKfxu6fEpu',
+ 'ext': 'aac',
+ 'release_date': '20231129',
+ 'release_timestamp': 1701249480,
+ 'title': 'Roland TD-7 Demo.m4a',
+ 'channel_id': '64424fe73cd58cbcf8d8e131',
+ 'availability': 'subscriber_only',
+ 'uploader': 'The Trash Network',
+ 'duration': 114,
+ 'channel': 'The Drum Thing',
+ 'uploader_id': '61bc20c9a131fb692bf2a513',
+ 'channel_url': 'https://www.floatplane.com/channel/TheTrashNetwork/home/thedrumthing',
+ 'uploader_url': 'https://www.floatplane.com/channel/TheTrashNetwork/home',
+ },
+ }],
+ 'skip': 'requires subscription: "The Trash Network"',
+ 'params': {'skip_download': 'm3u8'},
}]
def _real_initialize(self):
@@ -124,6 +183,22 @@ class FloatplaneIE(InfoExtractor):
if not any(traverse_obj(post_data, ('metadata', ('hasVideo', 'hasAudio')))):
raise ExtractorError('Post does not contain a video or audio track', expected=True)
+ uploader_url = format_field(
+ post_data, [('creator', 'urlname')], 'https://www.floatplane.com/channel/%s/home') or None
+
+ common_info = {
+ 'uploader_url': uploader_url,
+ 'channel_url': urljoin(f'{uploader_url}/', traverse_obj(post_data, ('channel', 'urlname'))),
+ 'availability': self._availability(needs_subscription=True),
+ **traverse_obj(post_data, {
+ 'uploader': ('creator', 'title', {str}),
+ 'uploader_id': ('creator', 'id', {str}),
+ 'channel': ('channel', 'title', {str}),
+ 'channel_id': ('channel', 'id', {str}),
+ 'release_timestamp': ('releaseDate', {parse_iso8601}),
+ }),
+ }
+
items = []
for media in traverse_obj(post_data, (('videoAttachments', 'audioAttachments'), ...)):
media_id = media['id']
@@ -150,11 +225,11 @@ class FloatplaneIE(InfoExtractor):
formats = []
for quality in traverse_obj(stream, ('resource', 'data', 'qualityLevels', ...)):
url = urljoin(stream['cdn'], format_path(traverse_obj(
- stream, ('resource', 'data', 'qualityLevelParams', quality['name']))))
+ stream, ('resource', 'data', 'qualityLevelParams', quality['name'], {dict}))))
formats.append({
**traverse_obj(quality, {
- 'format_id': 'name',
- 'format_note': 'label',
+ 'format_id': ('name', {str}),
+ 'format_note': ('label', {str}),
'width': ('width', {int}),
'height': ('height', {int}),
}),
@@ -164,38 +239,28 @@ class FloatplaneIE(InfoExtractor):
})
items.append({
+ **common_info,
'id': media_id,
**traverse_obj(metadata, {
- 'title': 'title',
+ 'title': ('title', {str}),
'duration': ('duration', {int_or_none}),
- 'thumbnail': ('thumbnail', 'path'),
+ 'thumbnail': ('thumbnail', 'path', {url_or_none}),
}),
'formats': formats,
})
- uploader_url = format_field(
- post_data, [('creator', 'urlname')], 'https://www.floatplane.com/channel/%s/home') or None
- channel_url = urljoin(f'{uploader_url}/', traverse_obj(post_data, ('channel', 'urlname')))
-
post_info = {
+ **common_info,
'id': post_id,
'display_id': post_id,
**traverse_obj(post_data, {
- 'title': 'title',
+ 'title': ('title', {str}),
'description': ('text', {clean_html}),
- 'uploader': ('creator', 'title'),
- 'uploader_id': ('creator', 'id'),
- 'channel': ('channel', 'title'),
- 'channel_id': ('channel', 'id'),
'like_count': ('likes', {int_or_none}),
'dislike_count': ('dislikes', {int_or_none}),
'comment_count': ('comments', {int_or_none}),
- 'release_timestamp': ('releaseDate', {parse_iso8601}),
- 'thumbnail': ('thumbnail', 'path'),
+ 'thumbnail': ('thumbnail', 'path', {url_or_none}),
}),
- 'uploader_url': uploader_url,
- 'channel_url': channel_url,
- 'availability': self._availability(needs_subscription=True),
}
if len(items) > 1:
From a40b0070c2a00d3ed839897462171a82323aa875 Mon Sep 17 00:00:00 2001
From: kclauhk <78251477+kclauhk@users.noreply.github.com>
Date: Mon, 22 Jan 2024 14:28:11 +0800
Subject: [PATCH 012/112] [ie/facebook:ads] Add extractor (#8870)
Closes #8083
Authored by: kclauhk
---
yt_dlp/extractor/_extractors.py | 1 +
yt_dlp/extractor/facebook.py | 112 ++++++++++++++++++++++++++++++++
2 files changed, 113 insertions(+)
diff --git a/yt_dlp/extractor/_extractors.py b/yt_dlp/extractor/_extractors.py
index a273ae0d9c..f51045668b 100644
--- a/yt_dlp/extractor/_extractors.py
+++ b/yt_dlp/extractor/_extractors.py
@@ -588,6 +588,7 @@ from .facebook import (
FacebookPluginsVideoIE,
FacebookRedirectURLIE,
FacebookReelIE,
+ FacebookAdsIE,
)
from .fancode import (
FancodeVodIE,
diff --git a/yt_dlp/extractor/facebook.py b/yt_dlp/extractor/facebook.py
index a16a067abb..26cfda5384 100644
--- a/yt_dlp/extractor/facebook.py
+++ b/yt_dlp/extractor/facebook.py
@@ -20,6 +20,7 @@ from ..utils import (
get_element_by_id,
get_first,
int_or_none,
+ join_nonempty,
js_to_json,
merge_dicts,
parse_count,
@@ -907,3 +908,114 @@ class FacebookReelIE(InfoExtractor):
video_id = self._match_id(url)
return self.url_result(
f'https://m.facebook.com/watch/?v={video_id}&_rdr', FacebookIE, video_id)
+
+
+class FacebookAdsIE(InfoExtractor):
+ _VALID_URL = r'https?://(?:[\w-]+\.)?facebook\.com/ads/library/?\?(?:[^#]+&)?id=(?P\d+)'
+ IE_NAME = 'facebook:ads'
+
+ _TESTS = [{
+ 'url': 'https://www.facebook.com/ads/library/?id=899206155126718',
+ 'info_dict': {
+ 'id': '899206155126718',
+ 'ext': 'mp4',
+ 'title': 'video by Kandao',
+ 'uploader': 'Kandao',
+ 'uploader_id': '774114102743284',
+ 'uploader_url': r're:^https?://.*',
+ 'timestamp': 1702548330,
+ 'thumbnail': r're:^https?://.*',
+ 'upload_date': '20231214',
+ 'like_count': int,
+ }
+ }, {
+ 'url': 'https://www.facebook.com/ads/library/?id=893637265423481',
+ 'info_dict': {
+ 'id': '893637265423481',
+ 'title': 'Jusqu\u2019\u00e0 -25% sur une s\u00e9lection de vins p\u00e9tillants italiens ',
+ 'uploader': 'Eataly Paris Marais',
+ 'uploader_id': '2086668958314152',
+ 'uploader_url': r're:^https?://.*',
+ 'timestamp': 1703571529,
+ 'upload_date': '20231226',
+ 'like_count': int,
+ },
+ 'playlist_count': 3,
+ }, {
+ 'url': 'https://es-la.facebook.com/ads/library/?id=901230958115569',
+ 'only_matching': True,
+ }, {
+ 'url': 'https://m.facebook.com/ads/library/?id=901230958115569',
+ 'only_matching': True,
+ }]
+
+ _FORMATS_MAP = {
+ 'watermarked_video_sd_url': ('sd-wmk', 'SD, watermarked'),
+ 'video_sd_url': ('sd', None),
+ 'watermarked_video_hd_url': ('hd-wmk', 'HD, watermarked'),
+ 'video_hd_url': ('hd', None),
+ }
+
+ def _extract_formats(self, video_dict):
+ formats = []
+ for format_key, format_url in traverse_obj(video_dict, (
+ {dict.items}, lambda _, v: v[0] in self._FORMATS_MAP and url_or_none(v[1])
+ )):
+ formats.append({
+ 'format_id': self._FORMATS_MAP[format_key][0],
+ 'format_note': self._FORMATS_MAP[format_key][1],
+ 'url': format_url,
+ 'ext': 'mp4',
+ 'quality': qualities(tuple(self._FORMATS_MAP))(format_key),
+ })
+ return formats
+
+ def _real_extract(self, url):
+ video_id = self._match_id(url)
+ webpage = self._download_webpage(url, video_id)
+
+ post_data = [self._parse_json(j, video_id, fatal=False)
+ for j in re.findall(r's\.handle\(({.*})\);requireLazy\(', webpage)]
+ data = traverse_obj(post_data, (
+ ..., 'require', ..., ..., ..., 'props', 'deeplinkAdCard', 'snapshot', {dict}), get_all=False)
+ if not data:
+ raise ExtractorError('Unable to extract ad data')
+
+ title = data.get('title')
+ if not title or title == '{{product.name}}':
+ title = join_nonempty('display_format', 'page_name', delim=' by ', from_dict=data)
+
+ info_dict = traverse_obj(data, {
+ 'description': ('link_description', {str}, {lambda x: x if x != '{{product.description}}' else None}),
+ 'uploader': ('page_name', {str}),
+ 'uploader_id': ('page_id', {str_or_none}),
+ 'uploader_url': ('page_profile_uri', {url_or_none}),
+ 'timestamp': ('creation_time', {int_or_none}),
+ 'like_count': ('page_like_count', {int_or_none}),
+ })
+
+ entries = []
+ for idx, entry in enumerate(traverse_obj(
+ data, (('videos', 'cards'), lambda _, v: any([url_or_none(v[f]) for f in self._FORMATS_MAP]))), 1
+ ):
+ entries.append({
+ 'id': f'{video_id}_{idx}',
+ 'title': entry.get('title') or title,
+ 'description': entry.get('link_description') or info_dict.get('description'),
+ 'thumbnail': url_or_none(entry.get('video_preview_image_url')),
+ 'formats': self._extract_formats(entry),
+ })
+
+ if len(entries) == 1:
+ info_dict.update(entries[0])
+
+ elif len(entries) > 1:
+ info_dict.update({
+ 'title': entries[0]['title'],
+ 'entries': entries,
+ '_type': 'playlist',
+ })
+
+ info_dict['id'] = video_id
+
+ return info_dict
From 5f25f348f9eb5db842b1ec6799f95bebb7ba35a7 Mon Sep 17 00:00:00 2001
From: Simon Sawicki
Date: Tue, 23 Jan 2024 23:20:13 +0100
Subject: [PATCH 013/112] [ie/pr0gramm] Enable POL filter and provide tags
without login (#9051)
Authored by: Grub4K
---
yt_dlp/extractor/pr0gramm.py | 41 ++++++++++++++++++++++++------------
1 file changed, 27 insertions(+), 14 deletions(-)
diff --git a/yt_dlp/extractor/pr0gramm.py b/yt_dlp/extractor/pr0gramm.py
index 2a67942081..36e415f4a5 100644
--- a/yt_dlp/extractor/pr0gramm.py
+++ b/yt_dlp/extractor/pr0gramm.py
@@ -18,7 +18,6 @@ from ..utils.traversal import traverse_obj
class Pr0grammIE(InfoExtractor):
_VALID_URL = r'https?://pr0gramm\.com\/(?:[^/?#]+/)+(?P[\d]+)(?:[/?#:]|$)'
_TESTS = [{
- # Tags require account
'url': 'https://pr0gramm.com/new/video/5466437',
'info_dict': {
'id': '5466437',
@@ -36,7 +35,6 @@ class Pr0grammIE(InfoExtractor):
'_old_archive_ids': ['pr0grammstatic 5466437'],
},
}, {
- # Tags require account
'url': 'https://pr0gramm.com/new/3052805:comment28391322',
'info_dict': {
'id': '3052805',
@@ -71,6 +69,23 @@ class Pr0grammIE(InfoExtractor):
'thumbnail': r're:^https://thumb\.pr0gramm\.com/.*\.jpg',
'_old_archive_ids': ['pr0grammstatic 5848332'],
},
+ }, {
+ 'url': 'https://pr0gramm.com/top/5895149',
+ 'info_dict': {
+ 'id': '5895149',
+ 'ext': 'mp4',
+ 'title': 'pr0gramm-5895149 by algoholigSeeManThrower',
+ 'tags': 'count:19',
+ 'uploader': 'algoholigSeeManThrower',
+ 'uploader_id': 457556,
+ 'upload_timestamp': 1697580902,
+ 'upload_date': '20231018',
+ 'like_count': int,
+ 'dislike_count': int,
+ 'age_limit': 0,
+ 'thumbnail': 'https://thumb.pr0gramm.com/2023/10/18/db47bb3db5e1a1b3.jpg',
+ '_old_archive_ids': ['pr0grammstatic 5895149'],
+ },
}, {
'url': 'https://pr0gramm.com/static/5466437',
'only_matching': True,
@@ -92,15 +107,15 @@ class Pr0grammIE(InfoExtractor):
def _maximum_flags(self):
# We need to guess the flags for the content otherwise the api will raise an error
# We can guess the maximum allowed flags for the account from the cookies
- # Bitflags are (msbf): nsfp, nsfl, nsfw, sfw
- flags = 0b0001
+ # Bitflags are (msbf): pol, nsfp, nsfl, nsfw, sfw
+ flags = 0b10001
if self._is_logged_in:
- flags |= 0b1000
+ flags |= 0b01000
cookies = self._get_cookies(self.BASE_URL)
if 'me' not in cookies:
self._download_webpage(self.BASE_URL, None, 'Refreshing verification information')
if traverse_obj(cookies, ('me', {lambda x: x.value}, {unquote}, {json.loads}, 'verified')):
- flags |= 0b0110
+ flags |= 0b00110
return flags
@@ -134,14 +149,12 @@ class Pr0grammIE(InfoExtractor):
if not source or not source.endswith('mp4'):
self.raise_no_formats('Could not extract a video', expected=bool(source), video_id=video_id)
- tags = None
- if self._is_logged_in:
- metadata = self._call_api('info', video_id, {'itemId': video_id}, note='Downloading tags')
- tags = traverse_obj(metadata, ('tags', ..., 'tag', {str}))
- # Sorted by "confidence", higher confidence = earlier in list
- confidences = traverse_obj(metadata, ('tags', ..., 'confidence', ({int}, {float})))
- if confidences:
- tags = [tag for _, tag in sorted(zip(confidences, tags), reverse=True)]
+ metadata = self._call_api('info', video_id, {'itemId': video_id}, note='Downloading tags')
+ tags = traverse_obj(metadata, ('tags', ..., 'tag', {str}))
+ # Sorted by "confidence", higher confidence = earlier in list
+ confidences = traverse_obj(metadata, ('tags', ..., 'confidence', ({int}, {float})))
+ if confidences:
+ tags = [tag for _, tag in sorted(zip(confidences, tags), reverse=True)]
formats = traverse_obj(video_info, ('variants', ..., {
'format_id': ('name', {str}),
From 5dda3b291f59f388f953337e9fb09a94b64aaf34 Mon Sep 17 00:00:00 2001
From: Caesim404
Date: Sun, 28 Jan 2024 04:02:09 +0200
Subject: [PATCH 014/112] [ie/lsm,cloudycdn] Add extractors (#8643)
Closes #2978
Authored by: Caesim404
---
yt_dlp/extractor/_extractors.py | 6 +
yt_dlp/extractor/cloudycdn.py | 79 +++++++++
yt_dlp/extractor/lsm.py | 282 ++++++++++++++++++++++++++++++++
3 files changed, 367 insertions(+)
create mode 100644 yt_dlp/extractor/cloudycdn.py
create mode 100644 yt_dlp/extractor/lsm.py
diff --git a/yt_dlp/extractor/_extractors.py b/yt_dlp/extractor/_extractors.py
index f51045668b..09565055cf 100644
--- a/yt_dlp/extractor/_extractors.py
+++ b/yt_dlp/extractor/_extractors.py
@@ -369,6 +369,7 @@ from .clippit import ClippitIE
from .cliprs import ClipRsIE
from .closertotruth import CloserToTruthIE
from .cloudflarestream import CloudflareStreamIE
+from .cloudycdn import CloudyCDNIE
from .clubic import ClubicIE
from .clyp import ClypIE
from .cmt import CMTIE
@@ -1001,6 +1002,11 @@ from .lrt import (
LRTVODIE,
LRTStreamIE
)
+from .lsm import (
+ LSMLREmbedIE,
+ LSMLTVEmbedIE,
+ LSMReplayIE
+)
from .lumni import (
LumniIE
)
diff --git a/yt_dlp/extractor/cloudycdn.py b/yt_dlp/extractor/cloudycdn.py
new file mode 100644
index 0000000000..e6e470e073
--- /dev/null
+++ b/yt_dlp/extractor/cloudycdn.py
@@ -0,0 +1,79 @@
+from .common import InfoExtractor
+from ..utils import (
+ int_or_none,
+ parse_iso8601,
+ url_or_none,
+ urlencode_postdata,
+)
+from ..utils.traversal import traverse_obj
+
+
+class CloudyCDNIE(InfoExtractor):
+ _VALID_URL = r'(?:https?:)?//embed\.cloudycdn\.services/(?P[^/?#]+)/media/(?P[\w-]+)'
+ _EMBED_REGEX = [rf'
', webpage, 'author', default=None),
+ }
-class NYTimesCookingIE(NYTimesBaseIE):
- _VALID_URL = r'https?://cooking\.nytimes\.com/(?:guid|recip)es/(?P\d+)'
+
+class NYTimesCookingRecipeIE(InfoExtractor):
+ _VALID_URL = r'https?://cooking\.nytimes\.com/recipes/(?P\d+)'
_TESTS = [{
'url': 'https://cooking.nytimes.com/recipes/1017817-cranberry-curd-tart',
- 'md5': 'dab81fa2eaeb3f9ed47498bdcfcdc1d3',
+ 'md5': '579e83bbe8e61e9de67f80edba8a78a8',
'info_dict': {
- 'id': '100000004756089',
- 'ext': 'mov',
- 'timestamp': 1479383008,
- 'uploader': 'By SHAW LASH, ADAM SAEWITZ and JAMES HERRON',
- 'title': 'Cranberry Tart',
- 'upload_date': '20161117',
- 'description': 'If you are a fan of lemon curd or the classic French tarte au citron, you will love this cranberry version.',
+ 'id': '1017817',
+ 'ext': 'mp4',
+ 'title': 'Cranberry Curd Tart',
+ 'description': 'md5:ad77a3fc321db636256d4343c5742152',
+ 'timestamp': 1447804800,
+ 'upload_date': '20151118',
+ 'creator': 'David Tanis',
+ 'thumbnail': r're:https?://\w+\.nyt.com/images/.*\.jpg',
},
}, {
- 'url': 'https://cooking.nytimes.com/guides/13-how-to-cook-a-turkey',
- 'md5': '4b2e8c70530a89b8d905a2b572316eb8',
+ 'url': 'https://cooking.nytimes.com/recipes/1024781-neapolitan-checkerboard-cookies',
+ 'md5': '58df35998241dcf0620e99e646331b42',
'info_dict': {
- 'id': '100000003951728',
- 'ext': 'mov',
- 'timestamp': 1445509539,
- 'description': 'Turkey guide',
- 'upload_date': '20151022',
- 'title': 'Turkey',
- }
+ 'id': '1024781',
+ 'ext': 'mp4',
+ 'title': 'Neapolitan Checkerboard Cookies',
+ 'description': 'md5:ba12394c585ababea951cb6d2fcc6631',
+ 'timestamp': 1701302400,
+ 'upload_date': '20231130',
+ 'creator': 'Sue Li',
+ 'thumbnail': r're:https?://\w+\.nyt.com/images/.*\.jpg',
+ },
+ }, {
+ 'url': 'https://cooking.nytimes.com/recipes/1019516-overnight-oats',
+ 'md5': '2fe7965a3adc899913b8e25ada360823',
+ 'info_dict': {
+ 'id': '1019516',
+ 'ext': 'mp4',
+ 'timestamp': 1546387200,
+ 'description': 'md5:8856ce10239161bd2596ac335b9f9bfb',
+ 'upload_date': '20190102',
+ 'title': 'Overnight Oats',
+ 'creator': 'Genevieve Ko',
+ 'thumbnail': r're:https?://\w+\.nyt.com/images/.*\.jpg',
+ },
}]
def _real_extract(self, url):
page_id = self._match_id(url)
-
webpage = self._download_webpage(url, page_id)
+ recipe_data = self._search_nextjs_data(webpage, page_id)['props']['pageProps']['recipe']
- video_id = self._search_regex(
- r'data-video-id=["\'](\d+)', webpage, 'video id')
+ formats, subtitles = self._extract_m3u8_formats_and_subtitles(
+ recipe_data['videoSrc'], page_id, 'mp4', m3u8_id='hls')
- return self._extract_video_from_id(video_id)
+ return {
+ **traverse_obj(recipe_data, {
+ 'id': ('id', {str_or_none}),
+ 'title': ('title', {str}),
+ 'description': ('topnote', {clean_html}),
+ 'timestamp': ('publishedAt', {int_or_none}),
+ 'creator': ('contentAttribution', 'cardByline', {str}),
+ }),
+ 'formats': formats,
+ 'subtitles': subtitles,
+ 'thumbnails': [{'url': thumb_url} for thumb_url in traverse_obj(
+ recipe_data, ('image', 'crops', 'recipe', ..., {url_or_none}))],
+ }
From acaf806c15f0a802ba286c23af02a10cf4bd4731 Mon Sep 17 00:00:00 2001
From: DmitryScaletta
Date: Mon, 5 Feb 2024 05:17:39 +0300
Subject: [PATCH 048/112] [ie/nuum] Add extractors (#8868)
Authored by: DmitryScaletta, seproDev
Co-authored-by: sepro <4618135+seproDev@users.noreply.github.com>
---
yt_dlp/extractor/_extractors.py | 10 +-
yt_dlp/extractor/nuum.py | 199 ++++++++++++++++++++++++++++++++
yt_dlp/extractor/wasdtv.py | 159 -------------------------
3 files changed, 204 insertions(+), 164 deletions(-)
create mode 100644 yt_dlp/extractor/nuum.py
delete mode 100644 yt_dlp/extractor/wasdtv.py
diff --git a/yt_dlp/extractor/_extractors.py b/yt_dlp/extractor/_extractors.py
index 36335286c3..e7dd34c77b 100644
--- a/yt_dlp/extractor/_extractors.py
+++ b/yt_dlp/extractor/_extractors.py
@@ -1354,6 +1354,11 @@ from .nytimes import (
NYTimesCookingIE,
NYTimesCookingRecipeIE,
)
+from .nuum import (
+ NuumLiveIE,
+ NuumTabIE,
+ NuumMediaIE,
+)
from .nuvid import NuvidIE
from .nzherald import NZHeraldIE
from .nzonscreen import NZOnScreenIE
@@ -2315,11 +2320,6 @@ from .washingtonpost import (
WashingtonPostIE,
WashingtonPostArticleIE,
)
-from .wasdtv import (
- WASDTVStreamIE,
- WASDTVRecordIE,
- WASDTVClipIE,
-)
from .wat import WatIE
from .wdr import (
WDRIE,
diff --git a/yt_dlp/extractor/nuum.py b/yt_dlp/extractor/nuum.py
new file mode 100644
index 0000000000..3db663ded0
--- /dev/null
+++ b/yt_dlp/extractor/nuum.py
@@ -0,0 +1,199 @@
+import functools
+
+from .common import InfoExtractor
+from ..utils import (
+ ExtractorError,
+ OnDemandPagedList,
+ UserNotLive,
+ filter_dict,
+ int_or_none,
+ parse_iso8601,
+ str_or_none,
+ url_or_none,
+)
+from ..utils.traversal import traverse_obj
+
+
+class NuumBaseIE(InfoExtractor):
+ def _call_api(self, path, video_id, description, query={}):
+ response = self._download_json(
+ f'https://nuum.ru/api/v2/{path}', video_id, query=query,
+ note=f'Downloading {description} metadata',
+ errnote=f'Unable to download {description} metadata')
+ if error := response.get('error'):
+ raise ExtractorError(f'API returned error: {error!r}')
+ return response['result']
+
+ def _get_channel_info(self, channel_name):
+ return self._call_api(
+ 'broadcasts/public', video_id=channel_name, description='channel',
+ query={
+ 'with_extra': 'true',
+ 'channel_name': channel_name,
+ 'with_deleted': 'true',
+ })
+
+ def _parse_video_data(self, container, extract_formats=True):
+ stream = traverse_obj(container, ('media_container_streams', 0, {dict})) or {}
+ media = traverse_obj(stream, ('stream_media', 0, {dict})) or {}
+ media_url = traverse_obj(media, (
+ 'media_meta', ('media_archive_url', 'media_url'), {url_or_none}), get_all=False)
+
+ video_id = str(container['media_container_id'])
+ is_live = media.get('media_status') == 'RUNNING'
+
+ formats, subtitles = None, None
+ if extract_formats:
+ formats, subtitles = self._extract_m3u8_formats_and_subtitles(
+ media_url, video_id, 'mp4', live=is_live)
+
+ return filter_dict({
+ 'id': video_id,
+ 'is_live': is_live,
+ 'formats': formats,
+ 'subtitles': subtitles,
+ **traverse_obj(container, {
+ 'title': ('media_container_name', {str}),
+ 'description': ('media_container_description', {str}),
+ 'timestamp': ('created_at', {parse_iso8601}),
+ 'channel': ('media_container_channel', 'channel_name', {str}),
+ 'channel_id': ('media_container_channel', 'channel_id', {str_or_none}),
+ }),
+ **traverse_obj(stream, {
+ 'view_count': ('stream_total_viewers', {int_or_none}),
+ 'concurrent_view_count': ('stream_current_viewers', {int_or_none}),
+ }),
+ **traverse_obj(media, {
+ 'duration': ('media_duration', {int_or_none}),
+ 'thumbnail': ('media_meta', ('media_preview_archive_url', 'media_preview_url'), {url_or_none}),
+ }, get_all=False),
+ })
+
+
+class NuumMediaIE(NuumBaseIE):
+ IE_NAME = 'nuum:media'
+ _VALID_URL = r'https?://nuum\.ru/(?:streams|videos|clips)/(?P[\d]+)'
+ _TESTS = [{
+ 'url': 'https://nuum.ru/streams/1592713-7-days-to-die',
+ 'only_matching': True,
+ }, {
+ 'url': 'https://nuum.ru/videos/1567547-toxi-hurtz',
+ 'md5': 'f1d9118a30403e32b702a204eb03aca3',
+ 'info_dict': {
+ 'id': '1567547',
+ 'ext': 'mp4',
+ 'title': 'Toxi$ - Hurtz',
+ 'description': '',
+ 'timestamp': 1702631651,
+ 'upload_date': '20231215',
+ 'thumbnail': r're:^https?://.+\.jpg',
+ 'view_count': int,
+ 'concurrent_view_count': int,
+ 'channel_id': '6911',
+ 'channel': 'toxis',
+ 'duration': 116,
+ },
+ }, {
+ 'url': 'https://nuum.ru/clips/1552564-pro-misu',
+ 'md5': 'b248ae1565b1e55433188f11beeb0ca1',
+ 'info_dict': {
+ 'id': '1552564',
+ 'ext': 'mp4',
+ 'title': 'Про Мису 🙃',
+ 'timestamp': 1701971828,
+ 'upload_date': '20231207',
+ 'thumbnail': r're:^https?://.+\.jpg',
+ 'view_count': int,
+ 'concurrent_view_count': int,
+ 'channel_id': '3320',
+ 'channel': 'Misalelik',
+ 'duration': 41,
+ },
+ }]
+
+ def _real_extract(self, url):
+ video_id = self._match_id(url)
+ video_data = self._call_api(f'media-containers/{video_id}', video_id, 'media')
+
+ return self._parse_video_data(video_data)
+
+
+class NuumLiveIE(NuumBaseIE):
+ IE_NAME = 'nuum:live'
+ _VALID_URL = r'https?://nuum\.ru/channel/(?P[^/#?]+)/?(?:$|[#?])'
+ _TESTS = [{
+ 'url': 'https://nuum.ru/channel/mts_live',
+ 'only_matching': True,
+ }]
+
+ def _real_extract(self, url):
+ channel = self._match_id(url)
+ channel_info = self._get_channel_info(channel)
+ if traverse_obj(channel_info, ('channel', 'channel_is_live')) is False:
+ raise UserNotLive(video_id=channel)
+
+ info = self._parse_video_data(channel_info['media_container'])
+ return {
+ 'webpage_url': f'https://nuum.ru/streams/{info["id"]}',
+ 'extractor_key': NuumMediaIE.ie_key(),
+ 'extractor': NuumMediaIE.IE_NAME,
+ **info,
+ }
+
+
+class NuumTabIE(NuumBaseIE):
+ IE_NAME = 'nuum:tab'
+ _VALID_URL = r'https?://nuum\.ru/channel/(?P[^/#?]+)/(?Pstreams|videos|clips)'
+ _TESTS = [{
+ 'url': 'https://nuum.ru/channel/dankon_/clips',
+ 'info_dict': {
+ 'id': 'dankon__clips',
+ 'title': 'Dankon_',
+ },
+ 'playlist_mincount': 29,
+ }, {
+ 'url': 'https://nuum.ru/channel/dankon_/videos',
+ 'info_dict': {
+ 'id': 'dankon__videos',
+ 'title': 'Dankon_',
+ },
+ 'playlist_mincount': 2,
+ }, {
+ 'url': 'https://nuum.ru/channel/dankon_/streams',
+ 'info_dict': {
+ 'id': 'dankon__streams',
+ 'title': 'Dankon_',
+ },
+ 'playlist_mincount': 1,
+ }]
+
+ _PAGE_SIZE = 50
+
+ def _fetch_page(self, channel_id, tab_type, tab_id, page):
+ CONTAINER_TYPES = {
+ 'clips': ['SHORT_VIDEO', 'REVIEW_VIDEO'],
+ 'videos': ['LONG_VIDEO'],
+ 'streams': ['SINGLE'],
+ }
+
+ media_containers = self._call_api(
+ 'media-containers', video_id=tab_id, description=f'{tab_type} tab page {page + 1}',
+ query={
+ 'limit': self._PAGE_SIZE,
+ 'offset': page * self._PAGE_SIZE,
+ 'channel_id': channel_id,
+ 'media_container_status': 'STOPPED',
+ 'media_container_type': CONTAINER_TYPES[tab_type],
+ })
+ for container in traverse_obj(media_containers, (..., {dict})):
+ metadata = self._parse_video_data(container, extract_formats=False)
+ yield self.url_result(f'https://nuum.ru/videos/{metadata["id"]}', NuumMediaIE, **metadata)
+
+ def _real_extract(self, url):
+ channel_name, tab_type = self._match_valid_url(url).group('id', 'type')
+ tab_id = f'{channel_name}_{tab_type}'
+ channel_data = self._get_channel_info(channel_name)['channel']
+
+ return self.playlist_result(OnDemandPagedList(functools.partial(
+ self._fetch_page, channel_data['channel_id'], tab_type, tab_id), self._PAGE_SIZE),
+ playlist_id=tab_id, playlist_title=channel_data.get('channel_name'))
diff --git a/yt_dlp/extractor/wasdtv.py b/yt_dlp/extractor/wasdtv.py
deleted file mode 100644
index f57c619b5f..0000000000
--- a/yt_dlp/extractor/wasdtv.py
+++ /dev/null
@@ -1,159 +0,0 @@
-from .common import InfoExtractor
-from ..utils import (
- ExtractorError,
- int_or_none,
- parse_iso8601,
- traverse_obj,
- try_get,
-)
-
-
-class WASDTVBaseIE(InfoExtractor):
-
- def _fetch(self, path, video_id, description, query={}):
- response = self._download_json(
- f'https://wasd.tv/api/{path}', video_id, query=query,
- note=f'Downloading {description} metadata',
- errnote=f'Unable to download {description} metadata')
- error = response.get('error')
- if error:
- raise ExtractorError(f'{self.IE_NAME} returned error: {error}', expected=True)
- return response.get('result')
-
- def _extract_thumbnails(self, thumbnails_dict):
- return [{
- 'url': url,
- 'preference': index,
- } for index, url in enumerate(
- traverse_obj(thumbnails_dict, (('small', 'medium', 'large'),))) if url]
-
- def _real_extract(self, url):
- container = self._get_container(url)
- stream = traverse_obj(container, ('media_container_streams', 0))
- media = try_get(stream, lambda x: x['stream_media'][0])
- if not media:
- raise ExtractorError('Can not extract media data.', expected=True)
- media_meta = media.get('media_meta')
- media_url, is_live = self._get_media_url(media_meta)
- video_id = media.get('media_id') or container.get('media_container_id')
- formats, subtitles = self._extract_m3u8_formats_and_subtitles(media_url, video_id, 'mp4')
- return {
- 'id': str(video_id),
- 'title': container.get('media_container_name') or self._og_search_title(self._download_webpage(url, video_id)),
- 'description': container.get('media_container_description'),
- 'thumbnails': self._extract_thumbnails(media_meta.get('media_preview_images')),
- 'timestamp': parse_iso8601(container.get('created_at')),
- 'view_count': int_or_none(stream.get('stream_current_viewers' if is_live else 'stream_total_viewers')),
- 'is_live': is_live,
- 'formats': formats,
- 'subtitles': subtitles,
- }
-
- def _get_container(self, url):
- raise NotImplementedError('Subclass for get media container')
-
- def _get_media_url(self, media_meta):
- raise NotImplementedError('Subclass for get media url')
-
-
-class WASDTVStreamIE(WASDTVBaseIE):
- IE_NAME = 'wasdtv:stream'
- _VALID_URL = r'https?://wasd\.tv/(?P[^/#?]+)$'
- _TESTS = [{
- 'url': 'https://wasd.tv/24_7',
- 'info_dict': {
- 'id': '559738',
- 'ext': 'mp4',
- 'title': 'Live 24/7 Music',
- 'description': '24/7 Music',
- 'timestamp': int,
- 'upload_date': r're:^\d{8}$',
- 'is_live': True,
- 'view_count': int,
- },
- }]
-
- def _get_container(self, url):
- nickname = self._match_id(url)
- channel = self._fetch(f'channels/nicknames/{nickname}', video_id=nickname, description='channel')
- channel_id = channel.get('channel_id')
- containers = self._fetch(
- 'v2/media-containers', channel_id, 'running media containers',
- query={
- 'channel_id': channel_id,
- 'media_container_type': 'SINGLE',
- 'media_container_status': 'RUNNING',
- })
- if not containers:
- raise ExtractorError(f'{nickname} is offline', expected=True)
- return containers[0]
-
- def _get_media_url(self, media_meta):
- return media_meta['media_url'], True
-
-
-class WASDTVRecordIE(WASDTVBaseIE):
- IE_NAME = 'wasdtv:record'
- _VALID_URL = r'https?://wasd\.tv/[^/#?]+(?:/videos)?\?record=(?P\d+)$'
- _TESTS = [{
- 'url': 'https://wasd.tv/spacemita/videos?record=907755',
- 'md5': 'c9899dd85be4cc997816ff9f9ca516ce',
- 'info_dict': {
- 'id': '906825',
- 'ext': 'mp4',
- 'title': 'Музыкальный',
- 'description': 'md5:f510388d929ff60ae61d4c3cab3137cc',
- 'timestamp': 1645812079,
- 'upload_date': '20220225',
- 'thumbnail': r're:^https?://.+\.jpg',
- 'is_live': False,
- 'view_count': int,
- },
- }, {
- 'url': 'https://wasd.tv/spacemita?record=907755',
- 'only_matching': True,
- }]
-
- def _get_container(self, url):
- container_id = self._match_id(url)
- return self._fetch(
- f'v2/media-containers/{container_id}', container_id, 'media container')
-
- def _get_media_url(self, media_meta):
- media_archive_url = media_meta.get('media_archive_url')
- if media_archive_url:
- return media_archive_url, False
- return media_meta['media_url'], True
-
-
-class WASDTVClipIE(WASDTVBaseIE):
- IE_NAME = 'wasdtv:clip'
- _VALID_URL = r'https?://wasd\.tv/[^/#?]+/clips\?clip=(?P\d+)$'
- _TESTS = [{
- 'url': 'https://wasd.tv/spacemita/clips?clip=26804',
- 'md5': '818885e720143d7a4e776ff66fcff148',
- 'info_dict': {
- 'id': '26804',
- 'ext': 'mp4',
- 'title': 'Пуш флексит на голове стримера',
- 'timestamp': 1646682908,
- 'upload_date': '20220307',
- 'thumbnail': r're:^https?://.+\.jpg',
- 'view_count': int,
- },
- }]
-
- def _real_extract(self, url):
- clip_id = self._match_id(url)
- clip = self._fetch(f'v2/clips/{clip_id}', video_id=clip_id, description='clip')
- clip_data = clip.get('clip_data')
- formats, subtitles = self._extract_m3u8_formats_and_subtitles(clip_data.get('url'), video_id=clip_id, ext='mp4')
- return {
- 'id': clip_id,
- 'title': clip.get('clip_title') or self._og_search_title(self._download_webpage(url, clip_id, fatal=False)),
- 'thumbnails': self._extract_thumbnails(clip_data.get('preview')),
- 'timestamp': parse_iso8601(clip.get('created_at')),
- 'view_count': int_or_none(clip.get('clip_views_count')),
- 'formats': formats,
- 'subtitles': subtitles,
- }
From 35d96982f1033e36215d323317981ee17e8ab0d5 Mon Sep 17 00:00:00 2001
From: Chocobozzz
Date: Mon, 5 Feb 2024 20:58:32 +0100
Subject: [PATCH 049/112] [ie/peertube] Update instances (#9070)
Authored by: Chocobozzz
---
yt_dlp/extractor/peertube.py | 972 ++++++++++++++++++++++-------------
1 file changed, 610 insertions(+), 362 deletions(-)
diff --git a/yt_dlp/extractor/peertube.py b/yt_dlp/extractor/peertube.py
index 68e15737b9..730b2393e0 100644
--- a/yt_dlp/extractor/peertube.py
+++ b/yt_dlp/extractor/peertube.py
@@ -19,636 +19,902 @@ from ..utils import (
class PeerTubeIE(InfoExtractor):
_INSTANCES_RE = r'''(?:
# Taken from https://instances.joinpeertube.org/instances
- 40two\.tube|
- a\.metube\.ch|
- advtv\.ml|
- algorithmic\.tv|
- alimulama\.com|
- arcana\.fun|
- archive\.vidicon\.org|
- artefac-paris\.tv|
- auf1\.eu|
+ 0ch\.tv|
+ 3dctube\.3dcandy\.social|
+ all\.electric\.kitchen|
+ alterscope\.fr|
+ anarchy\.tube|
+ apathy\.tv|
+ apertatube\.net|
+ archive\.nocopyrightintended\.tv|
+ archive\.reclaim\.tv|
+ area51\.media|
+ astrotube-ufe\.obspm\.fr|
+ astrotube\.obspm\.fr|
+ audio\.freediverse\.com|
+ azxtube\.youssefc\.tn|
+ bark\.video|
battlepenguin\.video|
- beertube\.epgn\.ch|
- befree\.nohost\.me|
+ bava\.tv|
+ bee-tube\.fr|
+ beetoons\.tv|
+ biblion\.refchat\.net|
+ biblioteca\.theowlclub\.net|
bideoak\.argia\.eus|
- birkeundnymphe\.de|
+ bideoteka\.eus|
+ birdtu\.be|
bitcointv\.com|
- cattube\.org|
- clap\.nerv-project\.eu|
- climatejustice\.video|
+ bonn\.video|
+ breeze\.tube|
+ brioco\.live|
+ brocosoup\.fr|
+ canal\.facil\.services|
+ canard\.tube|
+ cdn01\.tilvids\.com|
+ celluloid-media\.huma-num\.fr|
+ chicago1\.peertube\.support|
+ cliptube\.org|
+ cloudtube\.ise\.fraunhofer\.de|
comf\.tube|
+ comics\.peertube\.biz|
+ commons\.tube|
+ communitymedia\.video|
conspiracydistillery\.com|
+ crank\.recoil\.org|
+ dalek\.zone|
+ dalliance\.network|
+ dangly\.parts|
darkvapor\.nohost\.me|
daschauher\.aksel\.rocks|
digitalcourage\.video|
- dreiecksnebel\.alex-detsch\.de|
- eduvid\.org|
+ displayeurope\.video|
+ ds106\.tv|
+ dud-video\.inf\.tu-dresden\.de|
+ dud175\.inf\.tu-dresden\.de|
+ dytube\.com|
+ ebildungslabor\.video|
evangelisch\.video|
- exo\.tube|
fair\.tube|
+ fedi\.video|
+ fedimovie\.com|
fediverse\.tv|
film\.k-prod\.fr|
- flim\.txmn\.tk|
+ flipboard\.video|
+ foss\.video|
+ fossfarmers\.company|
fotogramas\.politicaconciencia\.org|
- ftsi\.ru|
- gary\.vger\.cloud|
- graeber\.video|
+ freediverse\.com|
+ freesoto-u2151\.vm\.elestio\.app|
+ freesoto\.tv|
+ garr\.tv|
greatview\.video|
grypstube\.uni-greifswald\.de|
- highvoltage\.tv|
- hpstube\.fr|
- htp\.live|
- hyperreal\.tube|
+ habratube\.site|
+ ilbjach\.ru|
+ infothema\.net|
+ itvplus\.iiens\.net|
+ johnydeep\.net|
juggling\.digital|
+ jupiter\.tube|
+ kadras\.live|
kino\.kompot\.si|
kino\.schuerz\.at|
kinowolnosc\.pl|
kirche\.peertube-host\.de|
+ kiwi\.froggirl\.club|
kodcast\.com|
kolektiva\.media|
- kraut\.zone|
+ kpop\.22x22\.ru|
kumi\.tube|
+ la2\.peertube\.support|
+ la3\.peertube\.support|
+ la4\.peertube\.support|
lastbreach\.tv|
- lepetitmayennais\.fr\.nf|
- lexx\.impa\.me|
- libertynode\.tv|
- libra\.syntazia\.org|
- libremedia\.video|
+ lawsplaining\.peertube\.biz|
+ leopard\.tube|
+ live\.codinglab\.ch|
live\.libratoi\.org|
- live\.nanao\.moe|
- live\.toobnix\.org|
- livegram\.net|
- lolitube\.freedomchan\.moe|
+ live\.oldskool\.fi|
+ live\.solari\.com|
lucarne\.balsamine\.be|
- maindreieck-tv\.de|
- mani\.tube|
- manicphase\.me|
+ luxtube\.lu|
+ makertube\.net|
+ media\.econoalchemist\.com|
+ media\.exo\.cat|
media\.fsfe\.org|
media\.gzevd\.de|
- media\.inno3\.cricket|
- media\.kaitaia\.life|
+ media\.interior\.edu\.uy|
media\.krashboyz\.org|
- media\.over-world\.org|
- media\.skewed\.de|
+ media\.mzhd\.de|
+ media\.smz-ma\.de|
+ media\.theplattform\.net|
media\.undeadnetwork\.de|
+ medias\.debrouillonet\.org|
medias\.pingbase\.net|
+ mediatube\.fermalo\.fr|
melsungen\.peertube-host\.de|
- mirametube\.fr|
- mojotube\.net|
- monplaisirtube\.ddns\.net|
+ merci-la-police\.fr|
+ mindlyvideos\.com|
+ mirror\.peertube\.metalbanana\.net|
+ mirrored\.rocks|
+ mix\.video|
mountaintown\.video|
- my\.bunny\.cafe|
- myfreetube\.de|
+ movies\.metricsmaster\.eu|
+ mtube\.mooo\.com|
mytube\.kn-cloud\.de|
+ mytube\.le5emeaxe\.fr|
mytube\.madzel\.de|
- myworkoutarenapeertube\.cf|
+ nadajemy\.com|
nanawel-peertube\.dyndns\.org|
- nastub\.cz|
- offenes\.tv|
- orgdup\.media|
- ovaltube\.codinglab\.ch|
+ neat\.tube|
+ nethack\.tv|
+ nicecrew\.tv|
+ nightshift\.minnix\.dev|
+ nolog\.media|
+ nyltube\.nylarea\.com|
+ ocfedtest\.hosted\.spacebear\.ee|
+ openmedia\.edunova\.it|
p2ptv\.ru|
p\.eertu\.be|
p\.lu|
+ pastafriday\.club|
+ patriottube\.sonsofliberty\.red|
+ pcbu\.nl|
peer\.azurs\.fr|
- peertube1\.zeteo\.me|
+ peer\.d0g4\.me|
+ peer\.lukeog\.com|
+ peer\.madiator\.cloud|
+ peer\.raise-uav\.com|
+ peershare\.togart\.de|
+ peertube-blablalinux\.be|
+ peertube-demo\.learning-hub\.fr|
+ peertube-docker\.cpy\.re|
+ peertube-eu\.howlround\.com|
+ peertube-u5014\.vm\.elestio\.app|
+ peertube-us\.howlround\.com|
peertube\.020\.pl|
peertube\.0x5e\.eu|
+ peertube\.1984\.cz|
+ peertube\.2i2l\.net|
+ peertube\.adjutor\.xyz|
+ peertube\.adresse\.data\.gouv\.fr|
peertube\.alpharius\.io|
peertube\.am-networks\.fr|
peertube\.anduin\.net|
- peertube\.anzui\.dev|
- peertube\.arbleizez\.bzh|
+ peertube\.anti-logic\.com|
+ peertube\.arch-linux\.cz|
peertube\.art3mis\.de|
- peertube\.atilla\.org|
+ peertube\.artsrn\.ualberta\.ca|
+ peertube\.askan\.info|
+ peertube\.astral0pitek\.synology\.me|
peertube\.atsuchan\.page|
- peertube\.aukfood\.net|
- peertube\.aventer\.biz|
+ peertube\.automat\.click|
peertube\.b38\.rural-it\.org|
- peertube\.beeldengeluid\.nl|
peertube\.be|
+ peertube\.beeldengeluid\.nl|
peertube\.bgzashtita\.es|
- peertube\.bitsandlinux\.com|
+ peertube\.bike|
+ peertube\.bildung-ekhn\.de|
peertube\.biz|
- peertube\.boba\.best|
peertube\.br0\.fr|
peertube\.bridaahost\.ynh\.fr|
peertube\.bubbletea\.dev|
peertube\.bubuit\.net|
peertube\.cabaal\.net|
- peertube\.cats-home\.net|
- peertube\.chemnitz\.freifunk\.net|
- peertube\.chevro\.fr|
- peertube\.chrisspiegl\.com|
+ peertube\.chatinbit\.com|
+ peertube\.chaunchy\.com|
+ peertube\.chir\.rs|
+ peertube\.christianpacaud\.com|
peertube\.chtisurel\.net|
+ peertube\.chuggybumba\.com|
peertube\.cipherbliss\.com|
+ peertube\.cirkau\.art|
+ peertube\.cloud\.nerdraum\.de|
peertube\.cloud\.sans\.pub|
+ peertube\.coko\.foundation|
+ peertube\.communecter\.org|
+ peertube\.concordia\.social|
+ peertube\.corrigan\.xyz|
peertube\.cpge-brizeux\.fr|
peertube\.ctseuro\.com|
peertube\.cuatrolibertades\.org|
- peertube\.cybercirujas\.club|
- peertube\.cythin\.com|
+ peertube\.cube4fun\.net|
+ peertube\.dair-institute\.org|
peertube\.davigge\.com|
peertube\.dc\.pini\.fr|
+ peertube\.deadtom\.me|
peertube\.debian\.social|
+ peertube\.delta0189\.xyz|
peertube\.demonix\.fr|
peertube\.designersethiques\.org|
peertube\.desmu\.fr|
- peertube\.devloprog\.org|
peertube\.devol\.it|
- peertube\.dtmf\.ca|
- peertube\.ecologie\.bzh|
+ peertube\.dk|
+ peertube\.doesstuff\.social|
+ peertube\.eb8\.org|
+ peertube\.education-forum\.com|
+ peertube\.elforcer\.ru|
+ peertube\.em\.id\.lv|
+ peertube\.ethibox\.fr|
peertube\.eu\.org|
peertube\.european-pirates\.eu|
+ peertube\.eus|
peertube\.euskarabildua\.eus|
+ peertube\.expi\.studio|
+ peertube\.familie-berner\.de|
+ peertube\.familleboisteau\.fr|
+ peertube\.fedihost\.website|
peertube\.fenarinarsa\.com|
- peertube\.fomin\.site|
- peertube\.forsud\.be|
- peertube\.francoispelletier\.org|
- peertube\.freenet\.ru|
- peertube\.freetalklive\.com|
+ peertube\.festnoz\.de|
+ peertube\.forteza\.fr|
+ peertube\.freestorm\.online|
peertube\.functional\.cafe|
- peertube\.gardeludwig\.fr|
+ peertube\.gaminglinux\.fr|
peertube\.gargantia\.fr|
- peertube\.gcfamily\.fr|
+ peertube\.geekgalaxy\.fr|
+ peertube\.gemlog\.ca|
peertube\.genma\.fr|
peertube\.get-racing\.de|
+ peertube\.ghis94\.ovh|
peertube\.gidikroon\.eu|
- peertube\.gruezishop\.ch|
- peertube\.habets\.house|
- peertube\.hackerfraternity\.org|
+ peertube\.giftedmc\.com|
+ peertube\.grosist\.fr|
+ peertube\.gruntwerk\.org|
+ peertube\.gsugambit\.com|
+ peertube\.hackerfoo\.com|
+ peertube\.hellsite\.net|
+ peertube\.helvetet\.eu|
+ peertube\.histoirescrepues\.fr|
+ peertube\.home\.x0r\.fr|
+ peertube\.hyperfreedom\.org|
peertube\.ichigo\.everydayimshuflin\.com|
- peertube\.ignifi\.me|
+ peertube\.ifwo\.eu|
+ peertube\.in\.ua|
peertube\.inapurna\.org|
peertube\.informaction\.info|
peertube\.interhop\.org|
- peertube\.iselfhost\.com|
peertube\.it|
+ peertube\.it-arts\.net|
peertube\.jensdiemer\.de|
- peertube\.joffreyverd\.fr|
+ peertube\.johntheserg\.al|
+ peertube\.kaleidos\.net|
peertube\.kalua\.im|
- peertube\.kathryl\.fr|
+ peertube\.kcore\.org|
peertube\.keazilla\.net|
peertube\.klaewyss\.fr|
- peertube\.kodcast\.com|
+ peertube\.kleph\.eu|
+ peertube\.kodein\.be|
+ peertube\.kooperatywa\.tech|
+ peertube\.kriom\.net|
peertube\.kx\.studio|
+ peertube\.kyriog\.eu|
+ peertube\.la-famille-muller\.fr|
+ peertube\.labeuropereunion\.eu|
peertube\.lagvoid\.com|
- peertube\.lavallee\.tech|
- peertube\.le5emeaxe\.fr|
- peertube\.lestutosdeprocessus\.fr|
- peertube\.librenet\.co\.za|
+ peertube\.lhc\.net\.br|
+ peertube\.libresolutions\.network|
+ peertube\.libretic\.fr|
+ peertube\.librosphere\.fr|
peertube\.logilab\.fr|
+ peertube\.lon\.tv|
peertube\.louisematic\.site|
peertube\.luckow\.org|
peertube\.luga\.at|
peertube\.lyceeconnecte\.fr|
- peertube\.manalejandro\.com|
+ peertube\.madixam\.xyz|
+ peertube\.magicstone\.dev|
+ peertube\.marienschule\.de|
peertube\.marud\.fr|
- peertube\.mattone\.net|
peertube\.maxweiss\.io|
+ peertube\.miguelcr\.me|
+ peertube\.mikemestnik\.net|
+ peertube\.mobilsicher\.de|
peertube\.monlycee\.net|
peertube\.mxinfo\.fr|
- peertube\.myrasp\.eu|
- peertube\.nebelcloud\.de|
+ peertube\.naln1\.ca|
peertube\.netzbegruenung\.de|
- peertube\.newsocial\.tech|
peertube\.nicolastissot\.fr|
+ peertube\.nogafam\.fr|
+ peertube\.normalgamingcommunity\.cz|
peertube\.nz|
peertube\.offerman\.com|
+ peertube\.ohioskates\.com|
+ peertube\.onionstorm\.net|
peertube\.opencloud\.lu|
- peertube\.orthus\.link|
- peertube\.patapouf\.xyz|
- peertube\.pi2\.dev|
- peertube\.plataformess\.org|
- peertube\.pl|
- peertube\.portaesgnos\.org|
+ peertube\.otakufarms\.com|
+ peertube\.paladyn\.org|
+ peertube\.pix-n-chill\.fr|
peertube\.r2\.enst\.fr|
peertube\.r5c3\.fr|
- peertube\.radres\.xyz|
- peertube\.red|
- peertube\.robonomics\.network|
- peertube\.rtnkv\.cloud|
- peertube\.runfox\.tk|
+ peertube\.redpill-insight\.com|
+ peertube\.researchinstitute\.at|
+ peertube\.revelin\.fr|
+ peertube\.rlp\.schule|
+ peertube\.rokugan\.fr|
+ peertube\.rougevertbleu\.tv|
+ peertube\.roundpond\.net|
+ peertube\.rural-it\.org|
peertube\.satoshishop\.de|
- peertube\.scic-tetris\.org|
+ peertube\.scyldings\.com|
peertube\.securitymadein\.lu|
+ peertube\.semperpax\.com|
peertube\.semweb\.pro|
- peertube\.social\.my-wan\.de|
- peertube\.soykaf\.org|
- peertube\.stefofficiel\.me|
+ peertube\.sensin\.eu|
+ peertube\.sidh\.bzh|
+ peertube\.skorpil\.cz|
+ peertube\.smertrios\.com|
+ peertube\.sqweeb\.net|
+ peertube\.stattzeitung\.org|
peertube\.stream|
peertube\.su|
peertube\.swrs\.net|
peertube\.takeko\.cyou|
- peertube\.tangentfox\.com|
peertube\.taxinachtegel\.de|
- peertube\.thenewoil\.xyz|
+ peertube\.teftera\.com|
+ peertube\.teutronic-services\.de|
peertube\.ti-fr\.com|
peertube\.tiennot\.net|
- peertube\.troback\.com|
+ peertube\.tmp\.rcp\.tf|
peertube\.tspu\.edu\.ru|
- peertube\.tux\.ovh|
peertube\.tv|
peertube\.tweb\.tv|
- peertube\.ucy\.de|
peertube\.underworld\.fr|
- peertube\.us\.to|
- peertube\.ventresmous\.fr|
+ peertube\.vapronva\.pw|
+ peertube\.veen\.world|
+ peertube\.vesdia\.eu|
+ peertube\.virtual-assembly\.org|
+ peertube\.viviers-fibre\.net|
peertube\.vlaki\.cz|
- peertube\.w\.utnw\.de|
- peertube\.westring\.digital|
+ peertube\.wiesbaden\.social|
+ peertube\.wivodaim\.net|
+ peertube\.wtf|
+ peertube\.wtfayla\.net|
+ peertube\.xrcb\.cat|
peertube\.xwiki\.com|
+ peertube\.zd\.do|
+ peertube\.zetamc\.net|
+ peertube\.zmuuf\.org|
peertube\.zoz-serv\.org|
+ peertube\.zwindler\.fr|
peervideo\.ru|
periscope\.numenaute\.org|
- perron-tube\.de|
+ pete\.warpnine\.de|
petitlutinartube\.fr|
phijkchu\.com|
- pierre\.tube|
+ phoenixproject\.group|
piraten\.space|
- play\.rosano\.ca|
+ pirtube\.calut\.fr|
+ pityu\.flaki\.hu|
+ play\.mittdata\.se|
player\.ojamajo\.moe|
- plextube\.nl|
- pocketnetpeertube1\.nohost\.me|
- pocketnetpeertube3\.nohost\.me|
- pocketnetpeertube4\.nohost\.me|
- pocketnetpeertube5\.nohost\.me|
- pocketnetpeertube6\.nohost\.me|
- pt\.24-7\.ro|
- pt\.apathy\.top|
+ podlibre\.video|
+ portal\.digilab\.nfa\.cz|
+ private\.fedimovie\.com|
+ pt01\.lehrerfortbildung-bw\.de|
pt\.diaspodon\.fr|
- pt\.fedi\.tech|
- pt\.maciej\.website|
+ pt\.freedomwolf\.cc|
+ pt\.gordons\.gen\.nz|
+ pt\.ilyamikcoder\.com|
+ pt\.irnok\.net|
+ pt\.mezzo\.moe|
+ pt\.na4\.eu|
+ pt\.netcraft\.ch|
+ pt\.rwx\.ch|
+ pt\.sfunk1x\.com|
+ pt\.thishorsie\.rocks|
+ pt\.vern\.cc|
ptb\.lunarviews\.net|
- ptmir1\.inter21\.net|
- ptmir2\.inter21\.net|
- ptmir3\.inter21\.net|
- ptmir4\.inter21\.net|
- ptmir5\.inter21\.net|
- ptube\.horsentiers\.fr|
- ptube\.xmanifesto\.club|
- queermotion\.org|
- re-wizja\.re-medium\.com|
- regarder\.sans\.pub|
- ruraletv\.ovh|
- s1\.gegenstimme\.tv|
- s2\.veezee\.tube|
+ ptube\.de|
+ ptube\.ranranhome\.info|
+ puffy\.tube|
+ puppet\.zone|
+ qtube\.qlyoung\.net|
+ quantube\.win|
+ rankett\.net|
+ replay\.jres\.org|
+ review\.peertube\.biz|
sdmtube\.fr|
- sender-fm\.veezee\.tube|
- serv1\.wiki-tube\.de|
+ secure\.direct-live\.net|
+ secure\.scanovid\.com|
+ seka\.pona\.la|
serv3\.wiki-tube\.de|
- sickstream\.net|
- sleepy\.tube|
+ skeptube\.fr|
+ social\.fedimovie\.com|
+ socpeertube\.ru|
sovran\.video|
+ special\.videovortex\.tv|
spectra\.video|
+ stl1988\.peertube-host\.de|
+ stream\.biovisata\.lt|
+ stream\.conesphere\.cloud|
stream\.elven\.pw|
+ stream\.jurnalfm\.md|
stream\.k-prod\.fr|
- stream\.shahab\.nohost\.me|
- streamsource\.video|
+ stream\.litera\.tools|
+ stream\.nuemedia\.se|
+ stream\.rlp-media\.de|
+ stream\.vrse\.be|
studios\.racer159\.com|
- testtube\.florimond\.eu|
+ styxhexenhammer666\.com|
+ syrteplay\.obspm\.fr|
+ t\.0x0\.st|
+ tbh\.co-shaoghal\.net|
+ test-fab\.ynh\.fr|
+ testube\.distrilab\.fr|
tgi\.hosted\.spacebear\.ee|
- thaitube\.in\.th|
- the\.jokertv\.eu|
theater\.ethernia\.net|
thecool\.tube|
+ thevideoverse\.com|
tilvids\.com|
- toob\.bub\.org|
- tpaw\.video|
- truetube\.media|
- tuba\.lhub\.pl|
- tube-aix-marseille\.beta\.education\.fr|
- tube-amiens\.beta\.education\.fr|
- tube-besancon\.beta\.education\.fr|
- tube-bordeaux\.beta\.education\.fr|
- tube-clermont-ferrand\.beta\.education\.fr|
- tube-corse\.beta\.education\.fr|
- tube-creteil\.beta\.education\.fr|
- tube-dijon\.beta\.education\.fr|
- tube-education\.beta\.education\.fr|
- tube-grenoble\.beta\.education\.fr|
- tube-lille\.beta\.education\.fr|
- tube-limoges\.beta\.education\.fr|
- tube-montpellier\.beta\.education\.fr|
- tube-nancy\.beta\.education\.fr|
- tube-nantes\.beta\.education\.fr|
- tube-nice\.beta\.education\.fr|
- tube-normandie\.beta\.education\.fr|
- tube-orleans-tours\.beta\.education\.fr|
- tube-outremer\.beta\.education\.fr|
- tube-paris\.beta\.education\.fr|
- tube-poitiers\.beta\.education\.fr|
- tube-reims\.beta\.education\.fr|
- tube-rennes\.beta\.education\.fr|
- tube-strasbourg\.beta\.education\.fr|
- tube-toulouse\.beta\.education\.fr|
- tube-versailles\.beta\.education\.fr|
- tube1\.it\.tuwien\.ac\.at|
+ tinkerbetter\.tube|
+ tinsley\.video|
+ trailers\.ddigest\.com|
+ tube-action-educative\.apps\.education\.fr|
+ tube-arts-lettres-sciences-humaines\.apps\.education\.fr|
+ tube-cycle-2\.apps\.education\.fr|
+ tube-cycle-3\.apps\.education\.fr|
+ tube-education-physique-et-sportive\.apps\.education\.fr|
+ tube-enseignement-professionnel\.apps\.education\.fr|
+ tube-institutionnel\.apps\.education\.fr|
+ tube-langues-vivantes\.apps\.education\.fr|
+ tube-maternelle\.apps\.education\.fr|
+ tube-numerique-educatif\.apps\.education\.fr|
+ tube-sciences-technologies\.apps\.education\.fr|
+ tube-test\.apps\.education\.fr|
+ tube1\.perron-service\.de|
+ tube\.9minuti\.it|
tube\.abolivier\.bzh|
- tube\.ac-amiens\.fr|
- tube\.aerztefueraufklaerung\.de|
- tube\.alexx\.ml|
+ tube\.alado\.space|
tube\.amic37\.fr|
- tube\.anufrij\.de|
- tube\.apolut\.net|
- tube\.arkhalabs\.io|
+ tube\.area404\.cloud|
tube\.arthack\.nz|
- tube\.as211696\.net|
- tube\.avensio\.de|
+ tube\.asulia\.fr|
+ tube\.awkward\.company|
tube\.azbyka\.ru|
tube\.azkware\.net|
- tube\.bachaner\.fr|
- tube\.bmesh\.org|
- tube\.borked\.host|
+ tube\.bartrip\.me\.uk|
+ tube\.belowtoxic\.media|
+ tube\.bingle\.plus|
+ tube\.bit-friends\.de|
tube\.bstly\.de|
- tube\.chaoszone\.tv|
- tube\.chatelet\.ovh|
- tube\.cloud-libre\.eu|
+ tube\.chosto\.me|
tube\.cms\.garden|
- tube\.cowfee\.moe|
- tube\.cryptography\.dog|
- tube\.darknight-coffee\.org|
- tube\.dev\.lhub\.pl|
+ tube\.communia\.org|
+ tube\.cyberia\.club|
+ tube\.cybershock\.life|
+ tube\.dembased\.xyz|
+ tube\.dev\.displ\.eu|
+ tube\.digitalesozialearbeit\.de|
tube\.distrilab\.fr|
+ tube\.doortofreedom\.org|
tube\.dsocialize\.net|
+ tube\.e-jeremy\.com|
tube\.ebin\.club|
+ tube\.elemac\.fr|
+ tube\.erzbistum-hamburg\.de|
+ tube\.exozy\.me|
tube\.fdn\.fr|
- tube\.florimond\.eu|
- tube\.foxarmy\.ml|
- tube\.foxden\.party|
- tube\.frischesicht\.de|
+ tube\.fedi\.quebec|
+ tube\.fediverse\.at|
+ tube\.felinn\.org|
+ tube\.flokinet\.is|
+ tube\.foad\.me\.uk|
+ tube\.freepeople\.fr|
+ tube\.friloux\.me|
+ tube\.froth\.zone|
+ tube\.fulda\.social|
tube\.futuretic\.fr|
- tube\.gnous\.eu|
+ tube\.g1zm0\.de|
+ tube\.g4rf\.net|
+ tube\.gaiac\.io|
+ tube\.geekyboo\.net|
+ tube\.genb\.de|
+ tube\.ghk-academy\.info|
+ tube\.gi-it\.de|
tube\.grap\.coop|
tube\.graz\.social|
tube\.grin\.hu|
- tube\.hackerscop\.org|
- tube\.hordearii\.fr|
+ tube\.hokai\.lol|
+ tube\.int5\.net|
+ tube\.interhacker\.space|
+ tube\.invisible\.ch|
+ tube\.io18\.top|
+ tube\.itsg\.host|
tube\.jeena\.net|
- tube\.kai-stuht\.com|
+ tube\.kh-berlin\.de|
tube\.kockatoo\.org|
tube\.kotur\.org|
+ tube\.koweb\.fr|
+ tube\.la-dina\.net|
+ tube\.lab\.nrw|
tube\.lacaveatonton\.ovh|
+ tube\.laurent-malys\.fr|
+ tube\.leetdreams\.ch|
tube\.linkse\.media|
tube\.lokad\.com|
tube\.lucie-philou\.com|
- tube\.melonbread\.xyz|
- tube\.mfraters\.net|
- tube\.motuhake\.xyz|
- tube\.mrbesen\.de|
- tube\.nah\.re|
- tube\.nchoco\.net|
+ tube\.media-techport\.de|
+ tube\.morozoff\.pro|
+ tube\.neshweb\.net|
+ tube\.nestor\.coop|
+ tube\.network\.europa\.eu|
+ tube\.nicfab\.eu|
+ tube\.nieuwwestbrabant\.nl|
+ tube\.nogafa\.org|
tube\.novg\.net|
tube\.nox-rhea\.org|
tube\.nuagelibre\.fr|
+ tube\.numerique\.gouv\.fr|
+ tube\.nuxnik\.com|
tube\.nx12\.net|
tube\.octaplex\.net|
- tube\.odat\.xyz|
tube\.oisux\.org|
+ tube\.okcinfo\.news|
+ tube\.onlinekirche\.net|
tube\.opportunis\.me|
+ tube\.oraclefilms\.com|
tube\.org\.il|
- tube\.ortion\.xyz|
- tube\.others\.social|
+ tube\.pacapime\.ovh|
+ tube\.parinux\.org|
+ tube\.pastwind\.top|
tube\.picasoft\.net|
- tube\.plomlompom\.com|
+ tube\.pilgerweg-21\.de|
tube\.pmj\.rocks|
+ tube\.pol\.social|
+ tube\.ponsonaille\.fr|
tube\.portes-imaginaire\.org|
+ tube\.public\.apolut\.net|
+ tube\.pustule\.org|
tube\.pyngu\.com|
+ tube\.querdenken-711\.de|
tube\.rebellion\.global|
+ tube\.reseau-canope\.fr|
tube\.rhythms-of-resistance\.org|
- tube\.rita\.moe|
+ tube\.risedsky\.ovh|
+ tube\.rooty\.fr|
tube\.rsi\.cnr\.it|
- tube\.s1gm4\.eu|
- tube\.saumon\.io|
+ tube\.ryne\.moe|
tube\.schleuss\.online|
tube\.schule\.social|
- tube\.seditio\.fr|
+ tube\.sekretaerbaer\.net|
tube\.shanti\.cafe|
tube\.shela\.nu|
tube\.skrep\.in|
+ tube\.sleeping\.town|
tube\.sp-codes\.de|
- tube\.sp4ke\.com|
- tube\.superseriousbusiness\.org|
+ tube\.spdns\.org|
+ tube\.systerserver\.net|
tube\.systest\.eu|
tube\.tappret\.fr|
- tube\.tardis\.world|
- tube\.toontoet\.nl|
+ tube\.techeasy\.org|
+ tube\.thierrytalbert\.fr|
+ tube\.tinfoil-hat\.net|
+ tube\.toldi\.eu|
tube\.tpshd\.de|
+ tube\.trax\.im|
tube\.troopers\.agency|
+ tube\.ttk\.is|
+ tube\.tuxfriend\.fr|
tube\.tylerdavis\.xyz|
+ tube\.ullihome\.de|
+ tube\.ulne\.be|
tube\.undernet\.uy|
- tube\.vigilian-consulting\.nl|
- tube\.vraphim\.com|
- tube\.wehost\.lgbt|
- tube\.wien\.rocks|
+ tube\.vrpnet\.org|
tube\.wolfe\.casa|
tube\.xd0\.de|
+ tube\.xn--baw-joa\.social|
tube\.xy-space\.de|
tube\.yapbreak\.fr|
tubedu\.org|
- tubes\.jodh\.us|
- tuktube\.com|
- turkum\.me|
+ tubulus\.openlatin\.org|
+ turtleisland\.video|
tututu\.tube|
- tuvideo\.encanarias\.info|
- tv1\.cocu\.cc|
- tv1\.gomntu\.space|
- tv2\.cocu\.cc|
+ tv\.adast\.dk|
tv\.adn\.life|
+ tv\.arns\.lt|
tv\.atmx\.ca|
- tv\.bitma\.st|
- tv\.generallyrubbish\.net\.au|
+ tv\.based\.quest|
+ tv\.farewellutopia\.com|
+ tv\.filmfreedom\.net|
+ tv\.gravitons\.org|
+ tv\.io\.seg\.br|
tv\.lumbung\.space|
- tv\.mattchristiansenmedia\.com|
- tv\.netwhood\.online|
- tv\.neue\.city|
- tv\.piejacker\.net|
tv\.pirateradio\.social|
+ tv\.pirati\.cz|
+ tv\.santic-zombie\.ru|
tv\.undersco\.re|
+ tv\.zonepl\.net|
tvox\.ru|
twctube\.twc-zone\.eu|
- unfilter\.tube|
+ twobeek\.com|
+ urbanists\.video|
+ v\.9tail\.net|
v\.basspistol\.org|
+ v\.j4\.lc|
v\.kisombrella\.top|
- v\.lastorder\.xyz|
+ v\.koa\.im|
+ v\.kyaru\.xyz|
v\.lor\.sh|
- v\.phreedom\.club|
- v\.sil\.sh|
- v\.szy\.io|
- v\.xxxapex\.com|
- veezee\.tube|
- vid\.dascoyote\.xyz|
- vid\.garwood\.io|
- vid\.ncrypt\.at|
- vid\.pravdastalina\.info|
- vid\.qorg11\.net|
- vid\.rajeshtaylor\.com|
- vid\.samtripoli\.com|
- vid\.werefox\.dev|
+ v\.mkp\.ca|
+ v\.posm\.gay|
+ v\.slaycer\.top|
+ veedeo\.org|
+ vhs\.absturztau\.be|
+ vid\.cthos\.dev|
+ vid\.kinuseka\.us|
+ vid\.mkp\.ca|
+ vid\.nocogabriel\.fr|
+ vid\.norbipeti\.eu|
+ vid\.northbound\.online|
+ vid\.ohboii\.de|
+ vid\.plantplotting\.co\.uk|
+ vid\.pretok\.tv|
+ vid\.prometheus\.systems|
+ vid\.soafen\.love|
+ vid\.twhtv\.club|
vid\.wildeboer\.net|
video-cave-v2\.de|
+ video-liberty\.com|
video\.076\.ne\.jp|
video\.1146\.nohost\.me|
- video\.altertek\.org|
+ video\.9wd\.eu|
+ video\.abraum\.de|
+ video\.ados\.accoord\.fr|
+ video\.amiga-ng\.org|
video\.anartist\.org|
- video\.apps\.thedoodleproject\.net|
- video\.artist\.cx|
video\.asgardius\.company|
- video\.balsillie\.net|
+ video\.audiovisuel-participatif\.org|
video\.bards\.online|
- video\.binarydad\.com|
+ video\.barkoczy\.social|
+ video\.benetou\.fr|
+ video\.beyondwatts\.social|
+ video\.bgeneric\.net|
+ video\.bilecik\.edu\.tr|
video\.blast-info\.fr|
+ video\.bmu\.cloud|
video\.catgirl\.biz|
+ video\.causa-arcana\.com|
+ video\.chasmcity\.net|
+ video\.chbmeyer\.de|
video\.cigliola\.com|
- video\.cm-en-transition\.fr|
+ video\.citizen4\.eu|
+ video\.clumsy\.computer|
+ video\.cnnumerique\.fr|
+ video\.cnr\.it|
video\.cnt\.social|
video\.coales\.co|
- video\.codingfield\.com|
- video\.comptoir\.net|
video\.comune\.trento\.it|
- video\.cpn\.so|
+ video\.coyp\.us|
video\.csc49\.fr|
- video\.cybre\.town|
- video\.demokratischer-sommer\.de|
- video\.discord-insoumis\.fr|
- video\.dolphincastle\.com|
+ video\.davduf\.net|
+ video\.davejansen\.com|
+ video\.dlearning\.nl|
+ video\.dnfi\.no|
video\.dresden\.network|
- video\.ecole-89\.com|
- video\.elgrillolibertario\.org|
+ video\.drgnz\.club|
+ video\.dudenas\.lt|
+ video\.eientei\.org|
+ video\.ellijaymakerspace\.org|
video\.emergeheart\.info|
video\.eradicatinglove\.xyz|
- video\.ethantheenigma\.me|
- video\.exodus-privacy\.eu\.org|
- video\.fbxl\.net|
+ video\.everythingbagel\.me|
+ video\.extremelycorporate\.ca|
+ video\.fabiomanganiello\.com|
+ video\.fedi\.bzh|
video\.fhtagn\.org|
- video\.greenmycity\.eu|
- video\.guerredeclasse\.fr|
+ video\.firehawk-systems\.com|
+ video\.fox-romka\.ru|
+ video\.fuss\.bz\.it|
+ video\.glassbeadcollective\.org|
+ video\.graine-pdl\.org|
video\.gyt\.is|
- video\.hackers\.town|
+ video\.hainry\.fr|
video\.hardlimit\.com|
- video\.hooli\.co|
+ video\.hostux\.net|
video\.igem\.org|
+ video\.infojournal\.fr|
video\.internet-czas-dzialac\.pl|
+ video\.interru\.io|
+ video\.ipng\.ch|
+ video\.ironsysadmin\.com|
video\.islameye\.com|
- video\.kicik\.fr|
+ video\.jacen\.moe|
+ video\.jadin\.me|
+ video\.jeffmcbride\.net|
+ video\.jigmedatse\.com|
video\.kuba-orlik\.name|
- video\.kyushojitsu\.ca|
+ video\.lacalligramme\.fr|
+ video\.lanceurs-alerte\.fr|
+ video\.laotra\.red|
+ video\.lapineige\.fr|
+ video\.laraffinerie\.re|
video\.lavolte\.net|
- video\.lespoesiesdheloise\.fr|
video\.liberta\.vip|
- video\.liege\.bike|
+ video\.libreti\.net|
+ video\.licentia\.net|
video\.linc\.systems|
video\.linux\.it|
video\.linuxtrent\.it|
- video\.lokal\.social|
+ video\.liveitlive\.show|
video\.lono\.space|
- video\.lunasqu\.ee|
+ video\.lrose\.de|
+ video\.lunago\.net|
video\.lundi\.am|
+ video\.lycee-experimental\.org|
+ video\.maechler\.cloud|
video\.marcorennmaus\.de|
video\.mass-trespass\.uk|
+ video\.matomocamp\.org|
+ video\.medienzentrum-harburg\.de|
+ video\.mentality\.rip|
+ video\.metaversum\.wtf|
+ video\.midreality\.com|
+ video\.mttv\.it|
video\.mugoreve\.fr|
- video\.mundodesconocido\.com|
+ video\.mxtthxw\.art|
video\.mycrowd\.ca|
+ video\.niboe\.info|
video\.nogafam\.es|
- video\.odayacres\.farm|
+ video\.nstr\.no|
+ video\.occm\.cc|
+ video\.off-investigation\.fr|
+ video\.olos311\.org|
+ video\.ordinobsolete\.fr|
+ video\.osvoj\.ru|
+ video\.ourcommon\.cloud|
video\.ozgurkon\.org|
- video\.p1ng0ut\.social|
- video\.p3x\.de|
video\.pcf\.fr|
- video\.pony\.gallery|
- video\.potate\.space|
- video\.pourpenser\.pro|
- video\.progressiv\.dev|
+ video\.pcgaldo\.com|
+ video\.phyrone\.de|
+ video\.poul\.org|
+ video\.publicspaces\.net|
+ video\.pullopen\.xyz|
+ video\.r3s\.nrw|
+ video\.rainevixen\.com|
video\.resolutions\.it|
- video\.rw501\.de|
- video\.screamer\.wiki|
- video\.sdm-tools\.net|
+ video\.retroedge\.tech|
+ video\.rhizome\.org|
+ video\.rlp-media\.de|
+ video\.rs-einrich\.de|
+ video\.rubdos\.be|
+ video\.sadmin\.io|
video\.sftblw\.moe|
video\.shitposter\.club|
- video\.skyn3t\.in|
+ video\.simplex-software\.ru|
+ video\.slipfox\.xyz|
+ video\.snug\.moe|
+ video\.software-fuer-engagierte\.de|
video\.soi\.ch|
- video\.stuartbrand\.co\.uk|
+ video\.sonet\.ws|
+ video\.surazal\.net|
+ video\.taskcards\.eu|
+ video\.team-lcbs\.eu|
+ video\.techforgood\.social|
+ video\.telemillevaches\.net|
+ video\.thepolarbear\.co\.uk|
video\.thinkof\.name|
- video\.toot\.pt|
+ video\.tii\.space|
+ video\.tkz\.es|
+ video\.trankil\.info|
video\.triplea\.fr|
+ video\.tum\.social|
video\.turbo\.chat|
+ video\.uriopss-pdl\.fr|
+ video\.ustim\.ru|
+ video\.ut0pia\.org|
video\.vaku\.org\.ua|
+ video\.vegafjord\.me|
video\.veloma\.org|
video\.violoncello\.ch|
- video\.wilkie\.how|
- video\.wsf2021\.info|
- videorelay\.co|
+ video\.voidconspiracy\.band|
+ video\.wakkeren\.nl|
+ video\.windfluechter\.org|
+ video\.ziez\.eu|
videos-passages\.huma-num\.fr|
- videos\.3d-wolf\.com|
+ videos\.aadtp\.be|
videos\.ahp-numerique\.fr|
- videos\.alexandrebadalo\.pt|
+ videos\.alamaisondulibre\.org|
videos\.archigny\.net|
+ videos\.aroaduntraveled\.com|
+ videos\.b4tech\.org|
videos\.benjaminbrady\.ie|
- videos\.buceoluegoexisto\.com|
- videos\.capas\.se|
- videos\.casually\.cat|
+ videos\.bik\.opencloud\.lu|
videos\.cloudron\.io|
+ videos\.codingotaku\.com|
videos\.coletivos\.org|
+ videos\.collate\.social|
videos\.danksquad\.org|
- videos\.denshi\.live|
- videos\.fromouter\.space|
+ videos\.digitaldragons\.eu|
+ videos\.dromeadhere\.fr|
+ videos\.explain-it\.org|
+ videos\.factsonthegroundshow\.com|
+ videos\.foilen\.com|
videos\.fsci\.in|
+ videos\.gamercast\.net|
+ videos\.gianmarco\.gg|
videos\.globenet\.org|
+ videos\.grafo\.zone|
videos\.hauspie\.fr|
videos\.hush\.is|
+ videos\.hyphalfusion\.network|
+ videos\.icum\.to|
+ videos\.im\.allmendenetz\.de|
+ videos\.jacksonchen666\.com|
videos\.john-livingston\.fr|
- videos\.jordanwarne\.xyz|
- videos\.lavoixdessansvoix\.org|
+ videos\.knazarov\.com|
+ videos\.kuoushi\.com|
+ videos\.laliguepaysdelaloire\.org|
+ videos\.lemouvementassociatif-pdl\.org|
videos\.leslionsfloorball\.fr|
- videos\.lucero\.top|
- videos\.martyn\.berlin|
+ videos\.librescrum\.org|
videos\.mastodont\.cat|
- videos\.monstro1\.com|
- videos\.npo\.city|
- videos\.optoutpod\.com|
- videos\.petch\.rocks|
- videos\.pzelawski\.xyz|
+ videos\.metus\.ca|
+ videos\.miolo\.org|
+ videos\.offroad\.town|
+ videos\.openmandriva\.org|
+ videos\.parleur\.net|
+ videos\.pcorp\.us|
+ videos\.pop\.eu\.com|
videos\.rampin\.org|
+ videos\.rauten\.co\.za|
+ videos\.ritimo\.org|
+ videos\.sarcasmstardust\.com|
videos\.scanlines\.xyz|
videos\.shmalls\.pw|
- videos\.sibear\.fr|
videos\.stadtfabrikanten\.org|
- videos\.tankernn\.eu|
+ videos\.supertuxkart\.net|
videos\.testimonia\.org|
- videos\.thisishowidontdisappear\.com|
- videos\.traumaheilung\.net|
+ videos\.thinkerview\.com|
+ videos\.torrenezzi10\.xyz|
videos\.trom\.tf|
- videos\.wakkerewereld\.nu|
- videos\.weblib\.re|
+ videos\.utsukta\.org|
+ videos\.viorsan\.com|
+ videos\.wherelinux\.xyz|
+ videos\.wikilibriste\.fr|
videos\.yesil\.club|
+ videos\.yeswiki\.net|
+ videotube\.duckdns\.org|
+ vids\.capypara\.de|
vids\.roshless\.me|
+ vids\.stary\.pc\.pl|
vids\.tekdmn\.me|
- vidz\.dou\.bet|
- vod\.lumikko\.dev|
- vs\.uniter\.network|
+ vidz\.julien\.ovh|
+ views\.southfox\.me|
+ virtual-girls-are\.definitely-for\.me|
+ viste\.pt|
+ vnchich\.com|
+ vnop\.org|
+ vod\.newellijay\.tv|
+ voluntarytube\.com|
+ vtr\.chikichiki\.tube|
vulgarisation-informatique\.fr|
- watch\.breadtube\.tv|
- watch\.deranalyst\.ch|
+ watch\.easya\.solutions|
+ watch\.goodluckgabe\.life|
watch\.ignorance\.eu|
- watch\.krazy\.party|
+ watch\.jimmydore\.com|
watch\.libertaria\.space|
- watch\.rt4mn\.org|
- watch\.softinio\.com|
+ watch\.nuked\.social|
+ watch\.ocaml\.org|
+ watch\.thelema\.social|
watch\.tubelab\.video|
web-fellow\.de|
webtv\.vandoeuvre\.net|
- wechill\.space|
+ wetubevid\.online|
wikileaks\.video|
wiwi\.video|
- worldofvids\.com|
- wwtube\.net|
- www4\.mir\.inter21\.net|
- www\.birkeundnymphe\.de|
- www\.captain-german\.com|
- www\.wiki-tube\.de|
+ wow\.such\.disappointment\.fail|
+ www\.jvideos\.net|
+ www\.kotikoff\.net|
+ www\.makertube\.net|
+ www\.mypeer\.tube|
+ www\.nadajemy\.com|
+ www\.neptube\.io|
+ www\.rocaguinarda\.tv|
+ www\.vnshow\.net|
xxivproduction\.video|
- xxx\.noho\.st|
+ yt\.orokoro\.ru|
+ ytube\.retronerd\.at|
+ zumvideo\.de|
# from youtube-dl
peertube\.rainbowswingers\.net|
@@ -1305,24 +1571,6 @@ class PeerTubePlaylistIE(InfoExtractor):
(?P[^/]+)
''' % (PeerTubeIE._INSTANCES_RE, '|'.join(_TYPES.keys()))
_TESTS = [{
- 'url': 'https://peertube.tux.ovh/w/p/3af94cba-95e8-4b74-b37a-807ab6d82526',
- 'info_dict': {
- 'id': '3af94cba-95e8-4b74-b37a-807ab6d82526',
- 'description': 'playlist',
- 'timestamp': 1611171863,
- 'title': 'playlist',
- },
- 'playlist_mincount': 6,
- }, {
- 'url': 'https://peertube.tux.ovh/w/p/wkyqcQBnsvFxtUB2pkYc1e',
- 'info_dict': {
- 'id': 'wkyqcQBnsvFxtUB2pkYc1e',
- 'description': 'Cette liste de vidéos contient uniquement les jeux qui peuvent être terminés en une seule vidéo.',
- 'title': 'Let\'s Play',
- 'timestamp': 1604147331,
- },
- 'playlist_mincount': 6,
- }, {
'url': 'https://peertube.debian.social/w/p/hFdJoTuyhNJVa1cDWd1d12',
'info_dict': {
'id': 'hFdJoTuyhNJVa1cDWd1d12',
From 05420227aaab60a39c0f9ade069c5862be36b1fa Mon Sep 17 00:00:00 2001
From: SirElderling <148036781+SirElderling@users.noreply.github.com>
Date: Mon, 5 Feb 2024 20:39:07 +0000
Subject: [PATCH 050/112] [ie/nytimes] Extract timestamp (#9142)
Authored by: SirElderling
---
yt_dlp/extractor/nytimes.py | 8 ++++++--
1 file changed, 6 insertions(+), 2 deletions(-)
diff --git a/yt_dlp/extractor/nytimes.py b/yt_dlp/extractor/nytimes.py
index 354eb02c34..3019202a2e 100644
--- a/yt_dlp/extractor/nytimes.py
+++ b/yt_dlp/extractor/nytimes.py
@@ -32,6 +32,7 @@ class NYTimesBaseIE(InfoExtractor):
renderedRepresentation
}
duration
+ firstPublished
promotionalHeadline
promotionalMedia {
... on Image {
@@ -124,6 +125,7 @@ class NYTimesBaseIE(InfoExtractor):
'id': media_id,
'title': data.get('promotionalHeadline'),
'description': data.get('summary'),
+ 'timestamp': parse_iso8601(data.get('firstPublished')),
'duration': float_or_none(data.get('duration'), scale=1000),
'creator': ', '.join(traverse_obj(data, ( # TODO: change to 'creators'
'bylines', ..., 'renderedRepresentation', {lambda x: remove_start(x, 'By ')}))),
@@ -145,8 +147,8 @@ class NYTimesIE(NYTimesBaseIE):
'ext': 'mp4',
'title': 'Verbatim: What Is a Photocopier?',
'description': 'md5:93603dada88ddbda9395632fdc5da260',
- 'timestamp': 1398631707, # FIXME
- 'upload_date': '20140427', # FIXME
+ 'timestamp': 1398646132,
+ 'upload_date': '20140428',
'creator': 'Brett Weiner',
'thumbnail': r're:https?://\w+\.nyt.com/images/.+\.jpg',
'duration': 419,
@@ -310,6 +312,8 @@ class NYTimesCookingIE(NYTimesBaseIE):
'ext': 'mp4',
'title': 'How to Make Mac and Cheese',
'description': 'md5:b8f2f33ec1fb7523b21367147c9594f1',
+ 'timestamp': 1522950315,
+ 'upload_date': '20180405',
'duration': 9.51,
'creator': 'Alison Roman',
'thumbnail': r're:https?://\w+\.nyt.com/images/.*\.jpg',
From 540b68298192874c75ad5ee4589bed64d02a7d55 Mon Sep 17 00:00:00 2001
From: Dmitry Meyer
Date: Fri, 9 Feb 2024 18:34:56 +0300
Subject: [PATCH 051/112] [ie/Boosty] Add extractor (#9144)
Closes #5900, Closes #8704
Authored by: un-def
---
yt_dlp/extractor/_extractors.py | 1 +
yt_dlp/extractor/boosty.py | 209 ++++++++++++++++++++++++++++++++
2 files changed, 210 insertions(+)
create mode 100644 yt_dlp/extractor/boosty.py
diff --git a/yt_dlp/extractor/_extractors.py b/yt_dlp/extractor/_extractors.py
index e7dd34c77b..5d1dd60386 100644
--- a/yt_dlp/extractor/_extractors.py
+++ b/yt_dlp/extractor/_extractors.py
@@ -257,6 +257,7 @@ from .blogger import BloggerIE
from .bloomberg import BloombergIE
from .bokecc import BokeCCIE
from .bongacams import BongaCamsIE
+from .boosty import BoostyIE
from .bostonglobe import BostonGlobeIE
from .box import BoxIE
from .boxcast import BoxCastVideoIE
diff --git a/yt_dlp/extractor/boosty.py b/yt_dlp/extractor/boosty.py
new file mode 100644
index 0000000000..fb14ca1467
--- /dev/null
+++ b/yt_dlp/extractor/boosty.py
@@ -0,0 +1,209 @@
+from .common import InfoExtractor
+from .youtube import YoutubeIE
+from ..utils import (
+ ExtractorError,
+ int_or_none,
+ qualities,
+ str_or_none,
+ url_or_none,
+)
+from ..utils.traversal import traverse_obj
+
+
+class BoostyIE(InfoExtractor):
+ _VALID_URL = r'https?://(?:www\.)?boosty\.to/(?P[^/#?]+)/posts/(?P[^/#?]+)'
+ _TESTS = [{
+ # single ok_video
+ 'url': 'https://boosty.to/kuplinov/posts/e55d050c-e3bb-4873-a7db-ac7a49b40c38',
+ 'info_dict': {
+ 'id': 'd7473824-352e-48e2-ae53-d4aa39459968',
+ 'title': 'phasma_3',
+ 'channel': 'Kuplinov',
+ 'channel_id': '7958701',
+ 'timestamp': 1655031975,
+ 'upload_date': '20220612',
+ 'release_timestamp': 1655049000,
+ 'release_date': '20220612',
+ 'modified_timestamp': 1668680993,
+ 'modified_date': '20221117',
+ 'tags': ['куплинов', 'phasmophobia'],
+ 'like_count': int,
+ 'ext': 'mp4',
+ 'duration': 105,
+ 'view_count': int,
+ 'thumbnail': r're:^https://i\.mycdn\.me/videoPreview\?',
+ },
+ }, {
+ # multiple ok_video
+ 'url': 'https://boosty.to/maddyson/posts/0c652798-3b35-471f-8b48-a76a0b28736f',
+ 'info_dict': {
+ 'id': '0c652798-3b35-471f-8b48-a76a0b28736f',
+ 'title': 'то что не пропустил юта6',
+ 'channel': 'Илья Давыдов',
+ 'channel_id': '6808257',
+ 'timestamp': 1694017040,
+ 'upload_date': '20230906',
+ 'release_timestamp': 1694017040,
+ 'release_date': '20230906',
+ 'modified_timestamp': 1694071178,
+ 'modified_date': '20230907',
+ 'like_count': int,
+ },
+ 'playlist_count': 3,
+ 'playlist': [{
+ 'info_dict': {
+ 'id': 'cc325a9f-a563-41c6-bf47-516c1b506c9a',
+ 'title': 'то что не пропустил юта6',
+ 'channel': 'Илья Давыдов',
+ 'channel_id': '6808257',
+ 'timestamp': 1694017040,
+ 'upload_date': '20230906',
+ 'release_timestamp': 1694017040,
+ 'release_date': '20230906',
+ 'modified_timestamp': 1694071178,
+ 'modified_date': '20230907',
+ 'like_count': int,
+ 'ext': 'mp4',
+ 'duration': 31204,
+ 'view_count': int,
+ 'thumbnail': r're:^https://i\.mycdn\.me/videoPreview\?',
+ },
+ }, {
+ 'info_dict': {
+ 'id': 'd07b0a72-9493-4512-b54e-55ce468fd4b7',
+ 'title': 'то что не пропустил юта6',
+ 'channel': 'Илья Давыдов',
+ 'channel_id': '6808257',
+ 'timestamp': 1694017040,
+ 'upload_date': '20230906',
+ 'release_timestamp': 1694017040,
+ 'release_date': '20230906',
+ 'modified_timestamp': 1694071178,
+ 'modified_date': '20230907',
+ 'like_count': int,
+ 'ext': 'mp4',
+ 'duration': 25704,
+ 'view_count': int,
+ 'thumbnail': r're:^https://i\.mycdn\.me/videoPreview\?',
+ },
+ }, {
+ 'info_dict': {
+ 'id': '4a3bba32-78c8-422a-9432-2791aff60b42',
+ 'title': 'то что не пропустил юта6',
+ 'channel': 'Илья Давыдов',
+ 'channel_id': '6808257',
+ 'timestamp': 1694017040,
+ 'upload_date': '20230906',
+ 'release_timestamp': 1694017040,
+ 'release_date': '20230906',
+ 'modified_timestamp': 1694071178,
+ 'modified_date': '20230907',
+ 'like_count': int,
+ 'ext': 'mp4',
+ 'duration': 31867,
+ 'view_count': int,
+ 'thumbnail': r're:^https://i\.mycdn\.me/videoPreview\?',
+ },
+ }],
+ }, {
+ # single external video (youtube)
+ 'url': 'https://boosty.to/denischuzhoy/posts/6094a487-bcec-4cf8-a453-43313b463c38',
+ 'info_dict': {
+ 'id': 'EXelTnve5lY',
+ 'title': 'Послание Президента Федеральному Собранию | Класс народа',
+ 'upload_date': '20210425',
+ 'channel': 'Денис Чужой',
+ 'tags': 'count:10',
+ 'like_count': int,
+ 'ext': 'mp4',
+ 'duration': 816,
+ 'view_count': int,
+ 'thumbnail': r're:^https://i\.ytimg\.com/',
+ 'age_limit': 0,
+ 'availability': 'public',
+ 'categories': list,
+ 'channel_follower_count': int,
+ 'channel_id': 'UCCzVNbWZfYpBfyofCCUD_0w',
+ 'channel_is_verified': bool,
+ 'channel_url': r're:^https://www\.youtube\.com/',
+ 'comment_count': int,
+ 'description': str,
+ 'heatmap': 'count:100',
+ 'live_status': str,
+ 'playable_in_embed': bool,
+ 'uploader': str,
+ 'uploader_id': str,
+ 'uploader_url': r're:^https://www\.youtube\.com/',
+ },
+ }]
+
+ _MP4_TYPES = ('tiny', 'lowest', 'low', 'medium', 'high', 'full_hd', 'quad_hd', 'ultra_hd')
+
+ def _extract_formats(self, player_urls, video_id):
+ formats = []
+ quality = qualities(self._MP4_TYPES)
+ for player_url in traverse_obj(player_urls, lambda _, v: url_or_none(v['url'])):
+ url = player_url['url']
+ format_type = player_url.get('type')
+ if format_type in ('hls', 'hls_live', 'live_ondemand_hls', 'live_playback_hls'):
+ formats.extend(self._extract_m3u8_formats(url, video_id, m3u8_id='hls', fatal=False))
+ elif format_type in ('dash', 'dash_live', 'live_playback_dash'):
+ formats.extend(self._extract_mpd_formats(url, video_id, mpd_id='dash', fatal=False))
+ elif format_type in self._MP4_TYPES:
+ formats.append({
+ 'url': url,
+ 'ext': 'mp4',
+ 'format_id': format_type,
+ 'quality': quality(format_type),
+ })
+ else:
+ self.report_warning(f'Unknown format type: {format_type!r}')
+ return formats
+
+ def _real_extract(self, url):
+ user, post_id = self._match_valid_url(url).group('user', 'post_id')
+ post = self._download_json(
+ f'https://api.boosty.to/v1/blog/{user}/post/{post_id}', post_id,
+ note='Downloading post data', errnote='Unable to download post data')
+
+ post_title = post.get('title')
+ if not post_title:
+ self.report_warning('Unable to extract post title. Falling back to parsing html page')
+ webpage = self._download_webpage(url, video_id=post_id)
+ post_title = self._og_search_title(webpage, default=None) or self._html_extract_title(webpage)
+
+ common_metadata = {
+ 'title': post_title,
+ **traverse_obj(post, {
+ 'channel': ('user', 'name', {str}),
+ 'channel_id': ('user', 'id', {str_or_none}),
+ 'timestamp': ('createdAt', {int_or_none}),
+ 'release_timestamp': ('publishTime', {int_or_none}),
+ 'modified_timestamp': ('updatedAt', {int_or_none}),
+ 'tags': ('tags', ..., 'title', {str}),
+ 'like_count': ('count', 'likes', {int_or_none}),
+ }),
+ }
+ entries = []
+ for item in traverse_obj(post, ('data', ..., {dict})):
+ item_type = item.get('type')
+ if item_type == 'video' and url_or_none(item.get('url')):
+ entries.append(self.url_result(item['url'], YoutubeIE))
+ elif item_type == 'ok_video':
+ video_id = item.get('id') or post_id
+ entries.append({
+ 'id': video_id,
+ 'formats': self._extract_formats(item.get('playerUrls'), video_id),
+ **common_metadata,
+ **traverse_obj(item, {
+ 'title': ('title', {str}),
+ 'duration': ('duration', {int_or_none}),
+ 'view_count': ('viewsCounter', {int_or_none}),
+ 'thumbnail': (('previewUrl', 'defaultPreview'), {url_or_none}),
+ }, get_all=False)})
+
+ if not entries:
+ raise ExtractorError('No videos found', expected=True)
+ if len(entries) == 1:
+ return entries[0]
+ return self.playlist_result(entries, post_id, post_title, **common_metadata)
From 882e3b753c79c7799ce135c3a5edb72494b576af Mon Sep 17 00:00:00 2001
From: "lauren n. liberda"
Date: Sat, 10 Feb 2024 00:11:34 +0100
Subject: [PATCH 052/112] [ie/tvp] Support livestreams (#8860)
Closes #8824
Authored by: selfisekai
---
yt_dlp/extractor/tvp.py | 21 +++++++++++++++++----
1 file changed, 17 insertions(+), 4 deletions(-)
diff --git a/yt_dlp/extractor/tvp.py b/yt_dlp/extractor/tvp.py
index 2aa0dd870a..a8d00e243a 100644
--- a/yt_dlp/extractor/tvp.py
+++ b/yt_dlp/extractor/tvp.py
@@ -21,7 +21,7 @@ from ..utils import (
class TVPIE(InfoExtractor):
IE_NAME = 'tvp'
IE_DESC = 'Telewizja Polska'
- _VALID_URL = r'https?://(?:[^/]+\.)?(?:tvp(?:parlament)?\.(?:pl|info)|tvpworld\.com|swipeto\.pl)/(?:(?!\d+/)[^/]+/)*(?P\d+)'
+ _VALID_URL = r'https?://(?:[^/]+\.)?(?:tvp(?:parlament)?\.(?:pl|info)|tvpworld\.com|swipeto\.pl)/(?:(?!\d+/)[^/]+/)*(?P\d+)(?:[/?#]|$)'
_TESTS = [{
# TVPlayer 2 in js wrapper
@@ -514,7 +514,7 @@ class TVPVODBaseIE(InfoExtractor):
class TVPVODVideoIE(TVPVODBaseIE):
IE_NAME = 'tvp:vod'
- _VALID_URL = r'https?://vod\.tvp\.pl/[a-z\d-]+,\d+/[a-z\d-]+(?\d+)(?:\?[^#]+)?(?:#.+)?$'
+ _VALID_URL = r'https?://vod\.tvp\.pl/(?P[a-z\d-]+,\d+)/[a-z\d-]+(?\d+)/?(?:[?#]|$)'
_TESTS = [{
'url': 'https://vod.tvp.pl/dla-dzieci,24/laboratorium-alchemika-odcinki,309338/odcinek-24,S01E24,311357',
@@ -560,12 +560,23 @@ class TVPVODVideoIE(TVPVODBaseIE):
'thumbnail': 're:https?://.+',
},
'params': {'skip_download': 'm3u8'},
+ }, {
+ 'url': 'https://vod.tvp.pl/live,1/tvp-world,399731',
+ 'info_dict': {
+ 'id': '399731',
+ 'ext': 'mp4',
+ 'title': r're:TVP WORLD \d{4}-\d{2}-\d{2} \d{2}:\d{2}',
+ 'live_status': 'is_live',
+ 'thumbnail': 're:https?://.+',
+ },
}]
def _real_extract(self, url):
- video_id = self._match_id(url)
+ category, video_id = self._match_valid_url(url).group('category', 'id')
- info_dict = self._parse_video(self._call_api(f'vods/{video_id}', video_id), with_url=False)
+ is_live = category == 'live,1'
+ entity = 'lives' if is_live else 'vods'
+ info_dict = self._parse_video(self._call_api(f'{entity}/{video_id}', video_id), with_url=False)
playlist = self._call_api(f'{video_id}/videos/playlist', video_id, query={'videoType': 'MOVIE'})
@@ -582,6 +593,8 @@ class TVPVODVideoIE(TVPVODBaseIE):
'ext': 'ttml',
})
+ info_dict['is_live'] = is_live
+
return info_dict
From a1b778428991b1779203bac243ef4e9b6baea90c Mon Sep 17 00:00:00 2001
From: bashonly
Date: Sun, 11 Feb 2024 14:58:18 +0100
Subject: [PATCH 053/112] [build] Move bundle scripts into `bundle` submodule
Authored by: bashonly
---
.github/workflows/build.yml | 20 ++++-----
.github/workflows/release-master.yml | 2 +-
.github/workflows/release-nightly.yml | 2 +-
README.md | 24 ++++++-----
bundle/__init__.py | 1 +
bundle/py2exe.py | 59 +++++++++++++++++++++++++++
pyinst.py => bundle/pyinstaller.py | 2 +-
pyproject.toml | 3 ++
setup.py | 56 +------------------------
9 files changed, 91 insertions(+), 78 deletions(-)
create mode 100644 bundle/__init__.py
create mode 100755 bundle/py2exe.py
rename pyinst.py => bundle/pyinstaller.py (98%)
mode change 100644 => 100755
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index 036ce43489..4b05e7cf93 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -144,9 +144,9 @@ jobs:
run: |
unset LD_LIBRARY_PATH # Harmful; set by setup-python
conda activate build
- python pyinst.py --onedir
+ python -m bundle.pyinstaller --onedir
(cd ./dist/yt-dlp_linux && zip -r ../yt-dlp_linux.zip .)
- python pyinst.py
+ python -m bundle.pyinstaller
mv ./dist/yt-dlp_linux ./yt-dlp_linux
mv ./dist/yt-dlp_linux.zip ./yt-dlp_linux.zip
@@ -211,7 +211,7 @@ jobs:
python3.8 -m pip install -U Pyinstaller secretstorage -r requirements.txt # Cached version may be out of date
python3.8 devscripts/update-version.py -c "${{ inputs.channel }}" -r "${{ needs.process.outputs.origin }}" "${{ inputs.version }}"
python3.8 devscripts/make_lazy_extractors.py
- python3.8 pyinst.py
+ python3.8 -m bundle.pyinstaller
if ${{ vars.UPDATE_TO_VERIFICATION && 'true' || 'false' }}; then
arch="${{ (matrix.architecture == 'armv7' && 'armv7l') || matrix.architecture }}"
@@ -250,9 +250,9 @@ jobs:
python3 devscripts/make_lazy_extractors.py
- name: Build
run: |
- python3 pyinst.py --target-architecture universal2 --onedir
+ python3 -m bundle.pyinstaller --target-architecture universal2 --onedir
(cd ./dist/yt-dlp_macos && zip -r ../yt-dlp_macos.zip .)
- python3 pyinst.py --target-architecture universal2
+ python3 -m bundle.pyinstaller --target-architecture universal2
- name: Verify --update-to
if: vars.UPDATE_TO_VERIFICATION
@@ -302,7 +302,7 @@ jobs:
python3 devscripts/make_lazy_extractors.py
- name: Build
run: |
- python3 pyinst.py
+ python3 -m bundle.pyinstaller
mv dist/yt-dlp_macos dist/yt-dlp_macos_legacy
- name: Verify --update-to
@@ -342,10 +342,10 @@ jobs:
python devscripts/make_lazy_extractors.py
- name: Build
run: |
- python setup.py py2exe
+ python -m bundle.py2exe
Move-Item ./dist/yt-dlp.exe ./dist/yt-dlp_min.exe
- python pyinst.py
- python pyinst.py --onedir
+ python -m bundle.pyinstaller
+ python -m bundle.pyinstaller --onedir
Compress-Archive -Path ./dist/yt-dlp/* -DestinationPath ./dist/yt-dlp_win.zip
- name: Verify --update-to
@@ -391,7 +391,7 @@ jobs:
python devscripts/make_lazy_extractors.py
- name: Build
run: |
- python pyinst.py
+ python -m bundle.pyinstaller
- name: Verify --update-to
if: vars.UPDATE_TO_VERIFICATION
diff --git a/.github/workflows/release-master.yml b/.github/workflows/release-master.yml
index 0664137a94..af14b053ec 100644
--- a/.github/workflows/release-master.yml
+++ b/.github/workflows/release-master.yml
@@ -7,7 +7,7 @@ on:
- "yt_dlp/**.py"
- "!yt_dlp/version.py"
- "setup.py"
- - "pyinst.py"
+ - "bundle/*.py"
concurrency:
group: release-master
permissions:
diff --git a/.github/workflows/release-nightly.yml b/.github/workflows/release-nightly.yml
index 2e623a67c6..3f1418936a 100644
--- a/.github/workflows/release-nightly.yml
+++ b/.github/workflows/release-nightly.yml
@@ -18,7 +18,7 @@ jobs:
- name: Check for new commits
id: check_for_new_commits
run: |
- relevant_files=("yt_dlp/*.py" ':!yt_dlp/version.py' "setup.py" "pyinst.py")
+ relevant_files=("yt_dlp/*.py" ':!yt_dlp/version.py' "setup.py" "bundle/*.py")
echo "commit=$(git log --format=%H -1 --since="24 hours ago" -- "${relevant_files[@]}")" | tee "$GITHUB_OUTPUT"
release:
diff --git a/README.md b/README.md
index 7dc3bb2f6c..c74777d2f5 100644
--- a/README.md
+++ b/README.md
@@ -321,19 +321,21 @@ If you do not have the necessary dependencies for a task you are attempting, yt-
## COMPILE
### Standalone PyInstaller Builds
-To build the standalone executable, you must have Python and `pyinstaller` (plus any of yt-dlp's [optional dependencies](#dependencies) if needed). Once you have all the necessary dependencies installed, simply run `pyinst.py`. The executable will be built for the same architecture (x86/ARM, 32/64 bit) as the Python used.
+To build the standalone executable, you must have Python and `pyinstaller` (plus any of yt-dlp's [optional dependencies](#dependencies) if needed). The executable will be built for the same architecture (x86/ARM, 32/64 bit) as the Python used. You can run the following commands:
- python3 -m pip install -U pyinstaller -r requirements.txt
- python3 devscripts/make_lazy_extractors.py
- python3 pyinst.py
+```
+python3 -m pip install -U pyinstaller -r requirements.txt
+python3 devscripts/make_lazy_extractors.py
+python3 -m bundle.pyinstaller
+```
On some systems, you may need to use `py` or `python` instead of `python3`.
-`pyinst.py` accepts any arguments that can be passed to `pyinstaller`, such as `--onefile/-F` or `--onedir/-D`, which is further [documented here](https://pyinstaller.org/en/stable/usage.html#what-to-generate).
+`bundle/pyinstaller.py` accepts any arguments that can be passed to `pyinstaller`, such as `--onefile/-F` or `--onedir/-D`, which is further [documented here](https://pyinstaller.org/en/stable/usage.html#what-to-generate).
**Note**: Pyinstaller versions below 4.4 [do not support](https://github.com/pyinstaller/pyinstaller#requirements-and-tested-platforms) Python installed from the Windows store without using a virtual environment.
-**Important**: Running `pyinstaller` directly **without** using `pyinst.py` is **not** officially supported. This may or may not work correctly.
+**Important**: Running `pyinstaller` directly **without** using `bundle/pyinstaller.py` is **not** officially supported. This may or may not work correctly.
### Platform-independent Binary (UNIX)
You will need the build tools `python` (3.8+), `zip`, `make` (GNU), `pandoc`\* and `pytest`\*.
@@ -346,11 +348,13 @@ You can also run `make yt-dlp` instead to compile only the binary without updati
While we provide the option to build with [py2exe](https://www.py2exe.org), it is recommended to build [using PyInstaller](#standalone-pyinstaller-builds) instead since the py2exe builds **cannot contain `pycryptodomex`/`certifi` and needs VC++14** on the target computer to run.
-If you wish to build it anyway, install Python and py2exe, and then simply run `setup.py py2exe`
+If you wish to build it anyway, install Python (if it is not already installed) and you can run the following commands:
- py -m pip install -U py2exe -r requirements.txt
- py devscripts/make_lazy_extractors.py
- py setup.py py2exe
+```
+py -m pip install -U py2exe -r requirements.txt
+py devscripts/make_lazy_extractors.py
+py -m bundle.py2exe
+```
### Related scripts
diff --git a/bundle/__init__.py b/bundle/__init__.py
new file mode 100644
index 0000000000..932b79829c
--- /dev/null
+++ b/bundle/__init__.py
@@ -0,0 +1 @@
+# Empty file
diff --git a/bundle/py2exe.py b/bundle/py2exe.py
new file mode 100755
index 0000000000..a7e4113f1f
--- /dev/null
+++ b/bundle/py2exe.py
@@ -0,0 +1,59 @@
+#!/usr/bin/env python3
+
+# Allow execution from anywhere
+import os
+import sys
+
+sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
+
+import warnings
+
+from py2exe import freeze
+
+from devscripts.utils import read_version
+
+VERSION = read_version()
+
+
+def main():
+ warnings.warn(
+ 'py2exe builds do not support pycryptodomex and needs VC++14 to run. '
+ 'It is recommended to run "pyinst.py" to build using pyinstaller instead')
+
+ return freeze(
+ console=[{
+ 'script': './yt_dlp/__main__.py',
+ 'dest_base': 'yt-dlp',
+ 'icon_resources': [(1, 'devscripts/logo.ico')],
+ }],
+ version_info={
+ 'version': VERSION,
+ 'description': 'A youtube-dl fork with additional features and patches',
+ 'comments': 'Official repository: ',
+ 'product_name': 'yt-dlp',
+ 'product_version': VERSION,
+ },
+ options={
+ 'bundle_files': 0,
+ 'compressed': 1,
+ 'optimize': 2,
+ 'dist_dir': './dist',
+ 'excludes': [
+ # py2exe cannot import Crypto
+ 'Crypto',
+ 'Cryptodome',
+ # py2exe appears to confuse this with our socks library.
+ # We don't use pysocks and urllib3.contrib.socks would fail to import if tried.
+ 'urllib3.contrib.socks'
+ ],
+ 'dll_excludes': ['w9xpopen.exe', 'crypt32.dll'],
+ # Modules that are only imported dynamically must be added here
+ 'includes': ['yt_dlp.compat._legacy', 'yt_dlp.compat._deprecated',
+ 'yt_dlp.utils._legacy', 'yt_dlp.utils._deprecated'],
+ },
+ zipfile=None,
+ )
+
+
+if __name__ == '__main__':
+ main()
diff --git a/pyinst.py b/bundle/pyinstaller.py
old mode 100644
new mode 100755
similarity index 98%
rename from pyinst.py
rename to bundle/pyinstaller.py
index c36f6acd4f..db9dbfde51
--- a/pyinst.py
+++ b/bundle/pyinstaller.py
@@ -4,7 +4,7 @@
import os
import sys
-sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
+sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
import platform
diff --git a/pyproject.toml b/pyproject.toml
index 97718ec431..626d9aa133 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -3,3 +3,6 @@ build-backend = 'setuptools.build_meta'
# https://github.com/yt-dlp/yt-dlp/issues/5941
# https://github.com/pypa/distutils/issues/17
requires = ['setuptools > 50']
+
+[project.entry-points.pyinstaller40]
+hook-dirs = "yt_dlp.__pyinstaller:get_hook_dirs"
diff --git a/setup.py b/setup.py
index 3d9a69d10c..fc5b504683 100644
--- a/setup.py
+++ b/setup.py
@@ -7,7 +7,6 @@ import sys
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
import subprocess
-import warnings
try:
from setuptools import Command, find_packages, setup
@@ -39,46 +38,6 @@ def packages():
]
-def py2exe_params():
- warnings.warn(
- 'py2exe builds do not support pycryptodomex and needs VC++14 to run. '
- 'It is recommended to run "pyinst.py" to build using pyinstaller instead')
-
- return {
- 'console': [{
- 'script': './yt_dlp/__main__.py',
- 'dest_base': 'yt-dlp',
- 'icon_resources': [(1, 'devscripts/logo.ico')],
- }],
- 'version_info': {
- 'version': VERSION,
- 'description': DESCRIPTION,
- 'comments': LONG_DESCRIPTION.split('\n')[0],
- 'product_name': 'yt-dlp',
- 'product_version': VERSION,
- },
- 'options': {
- 'bundle_files': 0,
- 'compressed': 1,
- 'optimize': 2,
- 'dist_dir': './dist',
- 'excludes': [
- # py2exe cannot import Crypto
- 'Crypto',
- 'Cryptodome',
- # py2exe appears to confuse this with our socks library.
- # We don't use pysocks and urllib3.contrib.socks would fail to import if tried.
- 'urllib3.contrib.socks'
- ],
- 'dll_excludes': ['w9xpopen.exe', 'crypt32.dll'],
- # Modules that are only imported dynamically must be added here
- 'includes': ['yt_dlp.compat._legacy', 'yt_dlp.compat._deprecated',
- 'yt_dlp.utils._legacy', 'yt_dlp.utils._deprecated'],
- },
- 'zipfile': None,
- }
-
-
def build_params():
files_spec = [
('share/bash-completion/completions', ['completions/bash/yt-dlp']),
@@ -127,20 +86,7 @@ class build_lazy_extractors(Command):
def main():
- if sys.argv[1:2] == ['py2exe']:
- params = py2exe_params()
- try:
- from py2exe import freeze
- except ImportError:
- import py2exe # noqa: F401
- warnings.warn('You are using an outdated version of py2exe. Support for this version will be removed in the future')
- params['console'][0].update(params.pop('version_info'))
- params['options'] = {'py2exe': params.pop('options')}
- else:
- return freeze(**params)
- else:
- params = build_params()
-
+ params = build_params()
setup(
name='yt-dlp', # package name (do not change/remove comment)
version=VERSION,
From 868d2f60a7cb59b410c8cbfb452cbdb072687b81 Mon Sep 17 00:00:00 2001
From: bashonly
Date: Sun, 11 Feb 2024 15:07:45 +0100
Subject: [PATCH 054/112] [build:Makefile] Add automated `CODE_FOLDERS` and
`CODE_FILES`
Authored by: bashonly
---
Makefile | 27 ++++++++++++---------------
1 file changed, 12 insertions(+), 15 deletions(-)
diff --git a/Makefile b/Makefile
index c85b24c13e..296fc32603 100644
--- a/Makefile
+++ b/Makefile
@@ -21,7 +21,7 @@ clean-test:
*.mp4 *.mpga *.oga *.ogg *.opus *.png *.sbv *.srt *.swf *.swp *.tt *.ttml *.url *.vtt *.wav *.webloc *.webm *.webp
clean-dist:
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 .mailmap
+ yt_dlp/extractor/lazy_extractors.py *.spec CONTRIBUTING.md.tmp yt-dlp yt-dlp.exe yt_dlp.egg-info/ AUTHORS
clean-cache:
find . \( \
-type d -name .pytest_cache -o -type d -name __pycache__ -o -name "*.pyc" -o -name "*.class" \
@@ -73,24 +73,24 @@ test:
offlinetest: codetest
$(PYTHON) -m pytest -k "not download"
-# XXX: This is hard to maintain
-CODE_FOLDERS = yt_dlp yt_dlp/downloader yt_dlp/extractor yt_dlp/postprocessor yt_dlp/compat yt_dlp/compat/urllib yt_dlp/utils yt_dlp/dependencies yt_dlp/networking
-yt-dlp: yt_dlp/*.py yt_dlp/*/*.py
+CODE_FOLDERS := $(shell find yt_dlp -type d -not -name '__*' -exec sh -c 'test -e "$$1"/__init__.py' sh {} \; -print)
+CODE_FILES := $(shell for f in $(CODE_FOLDERS); do echo "$$f" | awk '{gsub(/\/[^\/]+/,"/*"); print $$1"/*.py"}'; done | sort -u)
+yt-dlp: $(CODE_FILES)
mkdir -p zip
for d in $(CODE_FOLDERS) ; do \
mkdir -p zip/$$d ;\
cp -pPR $$d/*.py zip/$$d/ ;\
done
- touch -t 200001010101 zip/yt_dlp/*.py zip/yt_dlp/*/*.py
+ cd zip ; touch -t 200001010101 $(CODE_FILES)
mv zip/yt_dlp/__main__.py zip/
- cd zip ; zip -q ../yt-dlp yt_dlp/*.py yt_dlp/*/*.py __main__.py
+ cd zip ; zip -q ../yt-dlp $(CODE_FILES) __main__.py
rm -rf zip
echo '#!$(PYTHON)' > yt-dlp
cat yt-dlp.zip >> yt-dlp
rm yt-dlp.zip
chmod a+x yt-dlp
-README.md: yt_dlp/*.py yt_dlp/*/*.py devscripts/make_readme.py
+README.md: $(CODE_FILES) devscripts/make_readme.py
COLUMNS=80 $(PYTHON) yt_dlp/__main__.py --ignore-config --help | $(PYTHON) devscripts/make_readme.py
CONTRIBUTING.md: README.md devscripts/make_contributing.py
@@ -115,15 +115,15 @@ yt-dlp.1: README.md devscripts/prepare_manpage.py
pandoc -s -f $(MARKDOWN) -t man yt-dlp.1.temp.md -o yt-dlp.1
rm -f yt-dlp.1.temp.md
-completions/bash/yt-dlp: yt_dlp/*.py yt_dlp/*/*.py devscripts/bash-completion.in
+completions/bash/yt-dlp: $(CODE_FILES) devscripts/bash-completion.in
mkdir -p completions/bash
$(PYTHON) devscripts/bash-completion.py
-completions/zsh/_yt-dlp: yt_dlp/*.py yt_dlp/*/*.py devscripts/zsh-completion.in
+completions/zsh/_yt-dlp: $(CODE_FILES) devscripts/zsh-completion.in
mkdir -p completions/zsh
$(PYTHON) devscripts/zsh-completion.py
-completions/fish/yt-dlp.fish: yt_dlp/*.py yt_dlp/*/*.py devscripts/fish-completion.in
+completions/fish/yt-dlp.fish: $(CODE_FILES) devscripts/fish-completion.in
mkdir -p completions/fish
$(PYTHON) devscripts/fish-completion.py
@@ -148,8 +148,5 @@ yt-dlp.tar.gz: all
setup.py setup.cfg yt-dlp yt_dlp requirements.txt \
devscripts test
-AUTHORS: .mailmap
- git shortlog -s -n | cut -f2 | sort > AUTHORS
-
-.mailmap:
- git shortlog -s -e -n | awk '!(out[$$NF]++) { $$1="";sub(/^[ \t]+/,""); print}' > .mailmap
+AUTHORS:
+ git shortlog -s -n HEAD | cut -f2 | sort > AUTHORS
From 775cde82dc5b1dc64ab0539a92dd8c7ba6c0ad33 Mon Sep 17 00:00:00 2001
From: bashonly
Date: Sun, 11 Feb 2024 15:13:03 +0100
Subject: [PATCH 055/112] [build] Migrate to `pyproject.toml` and `hatchling`
Authored by: bashonly
---
.github/workflows/release-master.yml | 2 +-
.github/workflows/release-nightly.yml | 2 +-
.github/workflows/release.yml | 9 +-
MANIFEST.in | 10 --
Makefile | 11 +--
pyproject.toml | 120 +++++++++++++++++++++++-
setup.cfg | 4 -
setup.py | 129 --------------------------
8 files changed, 130 insertions(+), 157 deletions(-)
delete mode 100644 MANIFEST.in
delete mode 100644 setup.py
diff --git a/.github/workflows/release-master.yml b/.github/workflows/release-master.yml
index af14b053ec..2430dc5f88 100644
--- a/.github/workflows/release-master.yml
+++ b/.github/workflows/release-master.yml
@@ -6,8 +6,8 @@ on:
paths:
- "yt_dlp/**.py"
- "!yt_dlp/version.py"
- - "setup.py"
- "bundle/*.py"
+ - "pyproject.toml"
concurrency:
group: release-master
permissions:
diff --git a/.github/workflows/release-nightly.yml b/.github/workflows/release-nightly.yml
index 3f1418936a..16d5838466 100644
--- a/.github/workflows/release-nightly.yml
+++ b/.github/workflows/release-nightly.yml
@@ -18,7 +18,7 @@ jobs:
- name: Check for new commits
id: check_for_new_commits
run: |
- relevant_files=("yt_dlp/*.py" ':!yt_dlp/version.py' "setup.py" "bundle/*.py")
+ relevant_files=("yt_dlp/*.py" ':!yt_dlp/version.py' "bundle/*.py" "pyproject.toml")
echo "commit=$(git log --format=%H -1 --since="24 hours ago" -- "${relevant_files[@]}")" | tee "$GITHUB_OUTPUT"
release:
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 69b5e31529..d1508e5e6c 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -266,14 +266,19 @@ jobs:
run: |
python devscripts/update-version.py -c "${{ env.channel }}" -r "${{ env.target_repo }}" -s "${{ env.suffix }}" "${{ env.version }}"
python devscripts/make_lazy_extractors.py
- sed -i -E "s/(name=')[^']+(', # package name)/\1${{ env.pypi_project }}\2/" setup.py
+ sed -i -E '0,/(name = ")[^"]+(")/s//\1${{ env.pypi_project }}\2/' pyproject.toml
- name: Build
run: |
rm -rf dist/*
make pypi-files
+ printf '%s\n\n' \
+ 'Official repository: ' \
+ '**PS**: Some links in this document will not work since this is a copy of the README.md from Github' > ./README.md.new
+ cat ./README.md >> ./README.md.new && mv -f ./README.md.new ./README.md
python devscripts/set-variant.py pip -M "You installed yt-dlp with pip or using the wheel from PyPi; Use that to update"
- python setup.py sdist bdist_wheel
+ make clean-cache
+ python -m build --no-isolation .
- name: Publish to PyPI
uses: pypa/gh-action-pypi-publish@release/v1
diff --git a/MANIFEST.in b/MANIFEST.in
deleted file mode 100644
index bc2f056c05..0000000000
--- a/MANIFEST.in
+++ /dev/null
@@ -1,10 +0,0 @@
-include AUTHORS
-include Changelog.md
-include LICENSE
-include README.md
-include completions/*/*
-include supportedsites.md
-include yt-dlp.1
-include requirements.txt
-recursive-include devscripts *
-recursive-include test *
diff --git a/Makefile b/Makefile
index 296fc32603..2f36c0cd13 100644
--- a/Makefile
+++ b/Makefile
@@ -6,11 +6,11 @@ doc: README.md CONTRIBUTING.md issuetemplates supportedsites
ot: offlinetest
tar: yt-dlp.tar.gz
-# Keep this list in sync with MANIFEST.in
+# Keep this list in sync with pyproject.toml includes/artifacts
# intended use: when building a source distribution,
-# make pypi-files && python setup.py sdist
+# make pypi-files && python3 -m build -sn .
pypi-files: AUTHORS Changelog.md LICENSE README.md README.txt supportedsites \
- completions yt-dlp.1 requirements.txt setup.cfg devscripts/* test/*
+ completions yt-dlp.1 pyproject.toml setup.cfg devscripts/* test/*
.PHONY: all clean install test tar pypi-files completions ot offlinetest codetest supportedsites
@@ -144,9 +144,8 @@ yt-dlp.tar.gz: all
-- \
README.md supportedsites.md Changelog.md LICENSE \
CONTRIBUTING.md Collaborators.md CONTRIBUTORS AUTHORS \
- Makefile MANIFEST.in yt-dlp.1 README.txt completions \
- setup.py setup.cfg yt-dlp yt_dlp requirements.txt \
- devscripts test
+ Makefile yt-dlp.1 README.txt completions .gitignore \
+ setup.cfg yt-dlp yt_dlp pyproject.toml devscripts test
AUTHORS:
git shortlog -s -n HEAD | cut -f2 | sort > AUTHORS
diff --git a/pyproject.toml b/pyproject.toml
index 626d9aa133..5ef013279a 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,8 +1,120 @@
[build-system]
-build-backend = 'setuptools.build_meta'
-# https://github.com/yt-dlp/yt-dlp/issues/5941
-# https://github.com/pypa/distutils/issues/17
-requires = ['setuptools > 50']
+requires = ["hatchling"]
+build-backend = "hatchling.build"
+
+[project]
+name = "yt-dlp"
+maintainers = [
+ {name = "pukkandan", email = "pukkandan.ytdlp@gmail.com"},
+ {name = "Grub4K", email = "contact@grub4k.xyz"},
+ {name = "bashonly", email = "bashonly@protonmail.com"},
+]
+description = "A youtube-dl fork with additional features and patches"
+readme = "README.md"
+requires-python = ">=3.8"
+keywords = [
+ "youtube-dl",
+ "video-downloader",
+ "youtube-downloader",
+ "sponsorblock",
+ "youtube-dlc",
+ "yt-dlp",
+]
+license = {file = "LICENSE"}
+classifiers = [
+ "Topic :: Multimedia :: Video",
+ "Development Status :: 5 - Production/Stable",
+ "Environment :: Console",
+ "Programming Language :: Python",
+ "Programming Language :: Python :: 3 :: Only",
+ "Programming Language :: Python :: 3.8",
+ "Programming Language :: Python :: 3.9",
+ "Programming Language :: Python :: 3.10",
+ "Programming Language :: Python :: 3.11",
+ "Programming Language :: Python :: 3.12",
+ "Programming Language :: Python :: Implementation",
+ "Programming Language :: Python :: Implementation :: CPython",
+ "Programming Language :: Python :: Implementation :: PyPy",
+ "License :: OSI Approved :: The Unlicense (Unlicense)",
+ "Operating System :: OS Independent",
+]
+dynamic = ["version"]
+dependencies = [
+ "brotli; implementation_name=='cpython'",
+ "brotlicffi; implementation_name!='cpython'",
+ "certifi",
+ "mutagen",
+ "pycryptodomex",
+ "requests>=2.31.0,<3",
+ "urllib3>=1.26.17,<3",
+ "websockets>=12.0",
+]
+
+[project.optional-dependencies]
+secretstorage = [
+ "cffi",
+ "secretstorage",
+]
+build = [
+ "build",
+ "hatchling",
+ "pip",
+ "wheel",
+]
+dev = [
+ "flake8",
+ "isort",
+ "pytest",
+]
+pyinstaller = ["pyinstaller>=6.3"]
+py2exe = ["py2exe>=0.12"]
+
+[project.urls]
+Documentation = "https://github.com/yt-dlp/yt-dlp#readme"
+Repository = "https://github.com/yt-dlp/yt-dlp"
+Tracker = "https://github.com/yt-dlp/yt-dlp/issues"
+Funding = "https://github.com/yt-dlp/yt-dlp/blob/master/Collaborators.md#collaborators"
+
+[project.scripts]
+yt-dlp = "yt_dlp:main"
[project.entry-points.pyinstaller40]
hook-dirs = "yt_dlp.__pyinstaller:get_hook_dirs"
+
+[tool.hatch.build.targets.sdist]
+include = [
+ "/yt_dlp",
+ "/devscripts",
+ "/test",
+ "/.gitignore", # included by default, needed for auto-excludes
+ "/Changelog.md",
+ "/LICENSE", # included as license
+ "/pyproject.toml", # included by default
+ "/README.md", # included as readme
+ "/setup.cfg",
+ "/supportedsites.md",
+]
+exclude = ["/yt_dlp/__pyinstaller"]
+artifacts = [
+ "/yt_dlp/extractor/lazy_extractors.py",
+ "/completions",
+ "/AUTHORS", # included by default
+ "/README.txt",
+ "/yt-dlp.1",
+]
+
+[tool.hatch.build.targets.wheel]
+packages = ["yt_dlp"]
+exclude = ["/yt_dlp/__pyinstaller"]
+artifacts = ["/yt_dlp/extractor/lazy_extractors.py"]
+
+[tool.hatch.build.targets.wheel.shared-data]
+"completions/bash/yt-dlp" = "share/bash-completion/completions/yt-dlp"
+"completions/zsh/_yt-dlp" = "share/zsh/site-functions/_yt-dlp"
+"completions/fish/yt-dlp.fish" = "share/fish/vendor_completions.d/yt-dlp.fish"
+"README.txt" = "share/doc/yt_dlp/README.txt"
+"yt-dlp.1" = "share/man/man1/yt-dlp.1"
+
+[tool.hatch.version]
+path = "yt_dlp/version.py"
+pattern = "_pkg_version = '(?P[^']+)'"
diff --git a/setup.cfg b/setup.cfg
index a799f7293e..aeb4cee586 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -1,7 +1,3 @@
-[wheel]
-universal = true
-
-
[flake8]
exclude = build,venv,.tox,.git,.pytest_cache
ignore = E402,E501,E731,E741,W503
diff --git a/setup.py b/setup.py
deleted file mode 100644
index fc5b504683..0000000000
--- a/setup.py
+++ /dev/null
@@ -1,129 +0,0 @@
-#!/usr/bin/env python3
-
-# Allow execution from anywhere
-import os
-import sys
-
-sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
-
-import subprocess
-
-try:
- from setuptools import Command, find_packages, setup
- setuptools_available = True
-except ImportError:
- from distutils.core import Command, setup
- setuptools_available = False
-
-from devscripts.utils import read_file, read_version
-
-VERSION = read_version(varname='_pkg_version')
-
-DESCRIPTION = 'A youtube-dl fork with additional features and patches'
-
-LONG_DESCRIPTION = '\n\n'.join((
- 'Official repository: ',
- '**PS**: Some links in this document will not work since this is a copy of the README.md from Github',
- read_file('README.md')))
-
-REQUIREMENTS = read_file('requirements.txt').splitlines()
-
-
-def packages():
- if setuptools_available:
- return find_packages(exclude=('youtube_dl', 'youtube_dlc', 'test', 'ytdlp_plugins', 'devscripts'))
-
- return [
- 'yt_dlp', 'yt_dlp.extractor', 'yt_dlp.downloader', 'yt_dlp.postprocessor', 'yt_dlp.compat',
- ]
-
-
-def build_params():
- files_spec = [
- ('share/bash-completion/completions', ['completions/bash/yt-dlp']),
- ('share/zsh/site-functions', ['completions/zsh/_yt-dlp']),
- ('share/fish/vendor_completions.d', ['completions/fish/yt-dlp.fish']),
- ('share/doc/yt_dlp', ['README.txt']),
- ('share/man/man1', ['yt-dlp.1'])
- ]
- data_files = []
- for dirname, files in files_spec:
- resfiles = []
- for fn in files:
- if not os.path.exists(fn):
- warnings.warn(f'Skipping file {fn} since it is not present. Try running " make pypi-files " first')
- else:
- resfiles.append(fn)
- data_files.append((dirname, resfiles))
-
- params = {'data_files': data_files}
-
- if setuptools_available:
- params['entry_points'] = {
- 'console_scripts': ['yt-dlp = yt_dlp:main'],
- 'pyinstaller40': ['hook-dirs = yt_dlp.__pyinstaller:get_hook_dirs'],
- }
- else:
- params['scripts'] = ['yt-dlp']
- return params
-
-
-class build_lazy_extractors(Command):
- description = 'Build the extractor lazy loading module'
- user_options = []
-
- def initialize_options(self):
- pass
-
- def finalize_options(self):
- pass
-
- def run(self):
- if self.dry_run:
- print('Skipping build of lazy extractors in dry run mode')
- return
- subprocess.run([sys.executable, 'devscripts/make_lazy_extractors.py'])
-
-
-def main():
- params = build_params()
- setup(
- name='yt-dlp', # package name (do not change/remove comment)
- version=VERSION,
- maintainer='pukkandan',
- maintainer_email='pukkandan.ytdlp@gmail.com',
- description=DESCRIPTION,
- long_description=LONG_DESCRIPTION,
- long_description_content_type='text/markdown',
- url='https://github.com/yt-dlp/yt-dlp',
- packages=packages(),
- install_requires=REQUIREMENTS,
- python_requires='>=3.8',
- project_urls={
- 'Documentation': 'https://github.com/yt-dlp/yt-dlp#readme',
- 'Source': 'https://github.com/yt-dlp/yt-dlp',
- 'Tracker': 'https://github.com/yt-dlp/yt-dlp/issues',
- 'Funding': 'https://github.com/yt-dlp/yt-dlp/blob/master/Collaborators.md#collaborators',
- },
- classifiers=[
- 'Topic :: Multimedia :: Video',
- 'Development Status :: 5 - Production/Stable',
- 'Environment :: Console',
- 'Programming Language :: Python',
- 'Programming Language :: Python :: 3.8',
- 'Programming Language :: Python :: 3.9',
- 'Programming Language :: Python :: 3.10',
- 'Programming Language :: Python :: 3.11',
- 'Programming Language :: Python :: 3.12',
- 'Programming Language :: Python :: Implementation',
- 'Programming Language :: Python :: Implementation :: CPython',
- 'Programming Language :: Python :: Implementation :: PyPy',
- 'License :: Public Domain',
- 'Operating System :: OS Independent',
- ],
- cmdclass={'build_lazy_extractors': build_lazy_extractors},
- **params
- )
-
-
-main()
From fd647775e27e030ab17387c249e2ebeba68f8ff0 Mon Sep 17 00:00:00 2001
From: Simon Sawicki
Date: Sun, 11 Feb 2024 15:14:42 +0100
Subject: [PATCH 056/112] [devscripts] `tomlparse`: Add makeshift toml parser
Authored by: Grub4K
---
devscripts/tomlparse.py | 189 ++++++++++++++++++++++++++++++++++++++++
1 file changed, 189 insertions(+)
create mode 100755 devscripts/tomlparse.py
diff --git a/devscripts/tomlparse.py b/devscripts/tomlparse.py
new file mode 100755
index 0000000000..85ac4eef78
--- /dev/null
+++ b/devscripts/tomlparse.py
@@ -0,0 +1,189 @@
+#!/usr/bin/env python3
+
+"""
+Simple parser for spec compliant toml files
+
+A simple toml parser for files that comply with the spec.
+Should only be used to parse `pyproject.toml` for `install_deps.py`.
+
+IMPORTANT: INVALID FILES OR MULTILINE STRINGS ARE NOT SUPPORTED!
+"""
+
+from __future__ import annotations
+
+import datetime
+import json
+import re
+
+WS = r'(?:[\ \t]*)'
+STRING_RE = re.compile(r'"(?:\\.|[^\\"\n])*"|\'[^\'\n]*\'')
+SINGLE_KEY_RE = re.compile(rf'{STRING_RE.pattern}|[A-Za-z0-9_-]+')
+KEY_RE = re.compile(rf'{WS}(?:{SINGLE_KEY_RE.pattern}){WS}(?:\.{WS}(?:{SINGLE_KEY_RE.pattern}){WS})*')
+EQUALS_RE = re.compile(rf'={WS}')
+WS_RE = re.compile(WS)
+
+_SUBTABLE = rf'(?P^\[(?P\[)?(?P{KEY_RE.pattern})\]\]?)'
+EXPRESSION_RE = re.compile(rf'^(?:{_SUBTABLE}|{KEY_RE.pattern}=)', re.MULTILINE)
+
+LIST_WS_RE = re.compile(rf'{WS}((#[^\n]*)?\n{WS})*')
+LEFTOVER_VALUE_RE = re.compile(r'[^,}\]\t\n#]+')
+
+
+def parse_key(value: str):
+ for match in SINGLE_KEY_RE.finditer(value):
+ if match[0][0] == '"':
+ yield json.loads(match[0])
+ elif match[0][0] == '\'':
+ yield match[0][1:-1]
+ else:
+ yield match[0]
+
+
+def get_target(root: dict, paths: list[str], is_list=False):
+ target = root
+
+ for index, key in enumerate(paths, 1):
+ use_list = is_list and index == len(paths)
+ result = target.get(key)
+ if result is None:
+ result = [] if use_list else {}
+ target[key] = result
+
+ if isinstance(result, dict):
+ target = result
+ elif use_list:
+ target = {}
+ result.append(target)
+ else:
+ target = result[-1]
+
+ assert isinstance(target, dict)
+ return target
+
+
+def parse_enclosed(data: str, index: int, end: str, ws_re: re.Pattern):
+ index += 1
+
+ if match := ws_re.match(data, index):
+ index = match.end()
+
+ while data[index] != end:
+ index = yield True, index
+
+ if match := ws_re.match(data, index):
+ index = match.end()
+
+ if data[index] == ',':
+ index += 1
+
+ if match := ws_re.match(data, index):
+ index = match.end()
+
+ assert data[index] == end
+ yield False, index + 1
+
+
+def parse_value(data: str, index: int):
+ if data[index] == '[':
+ result = []
+
+ indices = parse_enclosed(data, index, ']', LIST_WS_RE)
+ valid, index = next(indices)
+ while valid:
+ index, value = parse_value(data, index)
+ result.append(value)
+ valid, index = indices.send(index)
+
+ return index, result
+
+ if data[index] == '{':
+ result = {}
+
+ indices = parse_enclosed(data, index, '}', WS_RE)
+ valid, index = next(indices)
+ while valid:
+ valid, index = indices.send(parse_kv_pair(data, index, result))
+
+ return index, result
+
+ if match := STRING_RE.match(data, index):
+ return match.end(), json.loads(match[0]) if match[0][0] == '"' else match[0][1:-1]
+
+ match = LEFTOVER_VALUE_RE.match(data, index)
+ assert match
+ value = match[0].strip()
+ for func in [
+ int,
+ float,
+ datetime.time.fromisoformat,
+ datetime.date.fromisoformat,
+ datetime.datetime.fromisoformat,
+ {'true': True, 'false': False}.get,
+ ]:
+ try:
+ value = func(value)
+ break
+ except Exception:
+ pass
+
+ return match.end(), value
+
+
+def parse_kv_pair(data: str, index: int, target: dict):
+ match = KEY_RE.match(data, index)
+ if not match:
+ return None
+
+ *keys, key = parse_key(match[0])
+
+ match = EQUALS_RE.match(data, match.end())
+ assert match
+ index = match.end()
+
+ index, value = parse_value(data, index)
+ get_target(target, keys)[key] = value
+ return index
+
+
+def parse_toml(data: str):
+ root = {}
+ target = root
+
+ index = 0
+ while True:
+ match = EXPRESSION_RE.search(data, index)
+ if not match:
+ break
+
+ if match.group('subtable'):
+ index = match.end()
+ path, is_list = match.group('path', 'is_list')
+ target = get_target(root, list(parse_key(path)), bool(is_list))
+ continue
+
+ index = parse_kv_pair(data, match.start(), target)
+ assert index is not None
+
+ return root
+
+
+def main():
+ import argparse
+ from pathlib import Path
+
+ parser = argparse.ArgumentParser()
+ parser.add_argument('infile', type=Path, help='The TOML file to read as input')
+ args = parser.parse_args()
+
+ with args.infile.open('r', encoding='utf-8') as file:
+ data = file.read()
+
+ def default(obj):
+ if isinstance(obj, (datetime.date, datetime.time, datetime.datetime)):
+ return obj.isoformat()
+
+ print(json.dumps(parse_toml(data), default=default))
+
+
+if __name__ == '__main__':
+ main()
From b8a433aaca86b15cb9f1a451b0f69371d2fc22a9 Mon Sep 17 00:00:00 2001
From: bashonly
Date: Sun, 11 Feb 2024 15:17:08 +0100
Subject: [PATCH 057/112] [devscripts] `install_deps`: Add script and migrate
to it
Authored by: bashonly
---
.github/workflows/build.yml | 36 +++++++++--------
.github/workflows/core.yml | 2 +-
.github/workflows/download.yml | 4 +-
.github/workflows/quick-test.yml | 6 +--
.github/workflows/release.yml | 3 +-
README.md | 5 ++-
devscripts/install_deps.py | 66 ++++++++++++++++++++++++++++++++
requirements.txt | 8 ----
8 files changed, 95 insertions(+), 35 deletions(-)
create mode 100755 devscripts/install_deps.py
delete mode 100644 requirements.txt
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index 4b05e7cf93..082164c9e8 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -121,16 +121,14 @@ jobs:
- name: Install Requirements
run: |
sudo apt -y install zip pandoc man sed
- reqs=$(mktemp)
- cat > "$reqs" << EOF
+ cat > ./requirements.txt << EOF
python=3.10.*
- pyinstaller
- cffi
brotli-python
- secretstorage
EOF
- sed -E '/^(brotli|secretstorage).*/d' requirements.txt >> "$reqs"
- mamba create -n build --file "$reqs"
+ python devscripts/install_deps.py --print \
+ --exclude brotli --exclude brotlicffi \
+ --include secretstorage --include pyinstaller >> ./requirements.txt
+ mamba create -n build --file ./requirements.txt
- name: Prepare
run: |
@@ -203,12 +201,13 @@ jobs:
apt update
apt -y install zlib1g-dev python3.8 python3.8-dev python3.8-distutils python3-pip
python3.8 -m pip install -U pip setuptools wheel
- # Cannot access requirements.txt from the repo directory at this stage
+ # Cannot access any files from the repo directory at this stage
python3.8 -m pip install -U Pyinstaller mutagen pycryptodomex websockets brotli certifi secretstorage
run: |
cd repo
- python3.8 -m pip install -U Pyinstaller secretstorage -r requirements.txt # Cached version may be out of date
+ python3.8 devscripts/install_deps.py -o --include build
+ python3.8 devscripts/install_deps.py --include pyinstaller --include secretstorage # Cached version may be out of date
python3.8 devscripts/update-version.py -c "${{ inputs.channel }}" -r "${{ needs.process.outputs.origin }}" "${{ inputs.version }}"
python3.8 devscripts/make_lazy_extractors.py
python3.8 -m bundle.pyinstaller
@@ -240,9 +239,10 @@ jobs:
- name: Install Requirements
run: |
brew install coreutils
- python3 -m pip install -U --user pip setuptools wheel
+ python3 devscripts/install_deps.py --user -o --include build
+ python3 devscripts/install_deps.py --print --include pyinstaller > requirements.txt
# We need to ignore wheels otherwise we break universal2 builds
- python3 -m pip install -U --user --no-binary :all: Pyinstaller -r requirements.txt
+ python3 -m pip install -U --user --no-binary :all: -r requirements.txt
- name: Prepare
run: |
@@ -293,8 +293,8 @@ jobs:
- name: Install Requirements
run: |
brew install coreutils
- python3 -m pip install -U --user pip setuptools wheel
- python3 -m pip install -U --user Pyinstaller -r requirements.txt
+ python3 devscripts/install_deps.py --user -o --include build
+ python3 devscripts/install_deps.py --user --include pyinstaller
- name: Prepare
run: |
@@ -333,8 +333,9 @@ jobs:
python-version: "3.8"
- name: Install Requirements
run: | # Custom pyinstaller built with https://github.com/yt-dlp/pyinstaller-builds
- python -m pip install -U pip setuptools wheel py2exe
- pip install -U "https://yt-dlp.github.io/Pyinstaller-Builds/x86_64/pyinstaller-5.8.0-py3-none-any.whl" -r requirements.txt
+ python devscripts/install_deps.py -o --include build
+ python devscripts/install_deps.py --include py2exe
+ python -m pip install -U "https://yt-dlp.github.io/Pyinstaller-Builds/x86_64/pyinstaller-5.8.0-py3-none-any.whl"
- name: Prepare
run: |
@@ -382,8 +383,9 @@ jobs:
architecture: "x86"
- name: Install Requirements
run: |
- python -m pip install -U pip setuptools wheel
- pip install -U "https://yt-dlp.github.io/Pyinstaller-Builds/i686/pyinstaller-5.8.0-py3-none-any.whl" -r requirements.txt
+ python devscripts/install_deps.py -o --include build
+ python devscripts/install_deps.py
+ python -m pip install -U "https://yt-dlp.github.io/Pyinstaller-Builds/i686/pyinstaller-5.8.0-py3-none-any.whl"
- name: Prepare
run: |
diff --git a/.github/workflows/core.yml b/.github/workflows/core.yml
index eaaf03dee4..f694c9bdd1 100644
--- a/.github/workflows/core.yml
+++ b/.github/workflows/core.yml
@@ -53,7 +53,7 @@ jobs:
with:
python-version: ${{ matrix.python-version }}
- name: Install test requirements
- run: pip install pytest -r requirements.txt
+ run: python3 ./devscripts/install_deps.py --include dev
- name: Run tests
continue-on-error: False
run: |
diff --git a/.github/workflows/download.yml b/.github/workflows/download.yml
index 9f47d67187..84339d9700 100644
--- a/.github/workflows/download.yml
+++ b/.github/workflows/download.yml
@@ -15,7 +15,7 @@ jobs:
with:
python-version: 3.9
- name: Install test requirements
- run: pip install pytest -r requirements.txt
+ run: python3 ./devscripts/install_deps.py --include dev
- name: Run tests
continue-on-error: true
run: python3 ./devscripts/run_tests.py download
@@ -42,7 +42,7 @@ jobs:
with:
python-version: ${{ matrix.python-version }}
- name: Install test requirements
- run: pip install pytest -r requirements.txt
+ run: python3 ./devscripts/install_deps.py --include dev
- name: Run tests
continue-on-error: true
run: python3 ./devscripts/run_tests.py download
diff --git a/.github/workflows/quick-test.yml b/.github/workflows/quick-test.yml
index 84fca62d4d..4e9616926e 100644
--- a/.github/workflows/quick-test.yml
+++ b/.github/workflows/quick-test.yml
@@ -15,7 +15,7 @@ jobs:
with:
python-version: '3.8'
- name: Install test requirements
- run: pip install pytest -r requirements.txt
+ run: python3 ./devscripts/install_deps.py --include dev
- name: Run tests
run: |
python3 -m yt_dlp -v || true
@@ -28,8 +28,8 @@ jobs:
- uses: actions/checkout@v4
- uses: actions/setup-python@v4
- name: Install flake8
- run: pip install flake8
+ run: python3 ./devscripts/install_deps.py -o --include dev
- name: Make lazy extractors
- run: python devscripts/make_lazy_extractors.py
+ run: python3 ./devscripts/make_lazy_extractors.py
- name: Run flake8
run: flake8 .
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index d1508e5e6c..1653add4f0 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -253,8 +253,7 @@ jobs:
- name: Install Requirements
run: |
sudo apt -y install pandoc man
- python -m pip install -U pip setuptools wheel twine
- python -m pip install -U -r requirements.txt
+ python devscripts/install_deps.py -o --include build
- name: Prepare
env:
diff --git a/README.md b/README.md
index c74777d2f5..2fcb099176 100644
--- a/README.md
+++ b/README.md
@@ -324,7 +324,7 @@ If you do not have the necessary dependencies for a task you are attempting, yt-
To build the standalone executable, you must have Python and `pyinstaller` (plus any of yt-dlp's [optional dependencies](#dependencies) if needed). The executable will be built for the same architecture (x86/ARM, 32/64 bit) as the Python used. You can run the following commands:
```
-python3 -m pip install -U pyinstaller -r requirements.txt
+python3 devscripts/install_deps.py --include pyinstaller
python3 devscripts/make_lazy_extractors.py
python3 -m bundle.pyinstaller
```
@@ -351,13 +351,14 @@ While we provide the option to build with [py2exe](https://www.py2exe.org), it i
If you wish to build it anyway, install Python (if it is not already installed) and you can run the following commands:
```
-py -m pip install -U py2exe -r requirements.txt
+py devscripts/install_deps.py --include py2exe
py devscripts/make_lazy_extractors.py
py -m bundle.py2exe
```
### Related scripts
+* **`devscripts/install_deps.py`** - Install dependencies for yt-dlp.
* **`devscripts/update-version.py`** - Update the version number based on current date.
* **`devscripts/set-variant.py`** - Set the build variant of the executable.
* **`devscripts/make_changelog.py`** - Create a markdown changelog using short commit messages and update `CONTRIBUTORS` file.
diff --git a/devscripts/install_deps.py b/devscripts/install_deps.py
new file mode 100755
index 0000000000..715e5b0440
--- /dev/null
+++ b/devscripts/install_deps.py
@@ -0,0 +1,66 @@
+#!/usr/bin/env python3
+
+# Allow execution from anywhere
+import os
+import sys
+
+sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
+
+import argparse
+import re
+import subprocess
+
+from devscripts.tomlparse import parse_toml
+from devscripts.utils import read_file
+
+
+def parse_args():
+ parser = argparse.ArgumentParser(description='Install dependencies for yt-dlp')
+ parser.add_argument(
+ 'input', nargs='?', metavar='TOMLFILE', default='pyproject.toml', help='Input file (default: %(default)s)')
+ parser.add_argument(
+ '-e', '--exclude', metavar='REQUIREMENT', action='append', help='Exclude a required dependency')
+ parser.add_argument(
+ '-i', '--include', metavar='GROUP', action='append', help='Include an optional dependency group')
+ parser.add_argument(
+ '-o', '--only-optional', action='store_true', help='Only install optional dependencies')
+ parser.add_argument(
+ '-p', '--print', action='store_true', help='Only print a requirements.txt to stdout')
+ parser.add_argument(
+ '-u', '--user', action='store_true', help='Install with pip as --user')
+ return parser.parse_args()
+
+
+def main():
+ args = parse_args()
+ toml_data = parse_toml(read_file(args.input))
+ deps = toml_data['project']['dependencies']
+ targets = deps.copy() if not args.only_optional else []
+
+ for exclude in args.exclude or []:
+ for dep in deps:
+ simplified_dep = re.match(r'[\w-]+', dep)[0]
+ if dep in targets and (exclude.lower() == simplified_dep.lower() or exclude == dep):
+ targets.remove(dep)
+
+ optional_deps = toml_data['project']['optional-dependencies']
+ for include in args.include or []:
+ group = optional_deps.get(include)
+ if group:
+ targets.extend(group)
+
+ if args.print:
+ for target in targets:
+ print(target)
+ return
+
+ pip_args = [sys.executable, '-m', 'pip', 'install', '-U']
+ if args.user:
+ pip_args.append('--user')
+ pip_args.extend(targets)
+
+ return subprocess.call(pip_args)
+
+
+if __name__ == '__main__':
+ sys.exit(main())
diff --git a/requirements.txt b/requirements.txt
deleted file mode 100644
index 06ff82a800..0000000000
--- a/requirements.txt
+++ /dev/null
@@ -1,8 +0,0 @@
-mutagen
-pycryptodomex
-brotli; implementation_name=='cpython'
-brotlicffi; implementation_name!='cpython'
-certifi
-requests>=2.31.0,<3
-urllib3>=1.26.17,<3
-websockets>=12.0
From 920397634d1e84e76d2cb897bd6d69ba0c6bd5ca Mon Sep 17 00:00:00 2001
From: bashonly
Date: Sun, 11 Feb 2024 15:24:41 +0100
Subject: [PATCH 058/112] [build] Fix `secretstorage` for ARM builds
Authored by: bashonly
---
.github/workflows/build.yml | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index 082164c9e8..0c2b0f684f 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -199,10 +199,10 @@ jobs:
dockerRunArgs: --volume "${PWD}/repo:/repo"
install: | # Installing Python 3.10 from the Deadsnakes repo raises errors
apt update
- apt -y install zlib1g-dev python3.8 python3.8-dev python3.8-distutils python3-pip
+ apt -y install zlib1g-dev libffi-dev python3.8 python3.8-dev python3.8-distutils python3-pip
python3.8 -m pip install -U pip setuptools wheel
# Cannot access any files from the repo directory at this stage
- python3.8 -m pip install -U Pyinstaller mutagen pycryptodomex websockets brotli certifi secretstorage
+ python3.8 -m pip install -U Pyinstaller mutagen pycryptodomex websockets brotli certifi secretstorage cffi
run: |
cd repo
From 867f637b95b342e1cb9f1dc3c6cf0ffe727187ce Mon Sep 17 00:00:00 2001
From: bashonly
Date: Sun, 11 Feb 2024 17:35:27 +0100
Subject: [PATCH 059/112] [cleanup] Build files cleanup
- Fix `AUTHORS` file by doing an unshallow checkout
- Update triggers for nightly/master release
Authored by: bashonly
---
.github/workflows/release-master.yml | 2 ++
.github/workflows/release-nightly.yml | 9 ++++++++-
.github/workflows/release.yml | 2 ++
3 files changed, 12 insertions(+), 1 deletion(-)
diff --git a/.github/workflows/release-master.yml b/.github/workflows/release-master.yml
index 2430dc5f88..a84547580b 100644
--- a/.github/workflows/release-master.yml
+++ b/.github/workflows/release-master.yml
@@ -8,6 +8,8 @@ on:
- "!yt_dlp/version.py"
- "bundle/*.py"
- "pyproject.toml"
+ - "Makefile"
+ - ".github/workflows/build.yml"
concurrency:
group: release-master
permissions:
diff --git a/.github/workflows/release-nightly.yml b/.github/workflows/release-nightly.yml
index 16d5838466..f459a3a17e 100644
--- a/.github/workflows/release-nightly.yml
+++ b/.github/workflows/release-nightly.yml
@@ -18,7 +18,14 @@ jobs:
- name: Check for new commits
id: check_for_new_commits
run: |
- relevant_files=("yt_dlp/*.py" ':!yt_dlp/version.py' "bundle/*.py" "pyproject.toml")
+ relevant_files=(
+ "yt_dlp/*.py"
+ ':!yt_dlp/version.py'
+ "bundle/*.py"
+ "pyproject.toml"
+ "Makefile"
+ ".github/workflows/build.yml"
+ )
echo "commit=$(git log --format=%H -1 --since="24 hours ago" -- "${relevant_files[@]}")" | tee "$GITHUB_OUTPUT"
release:
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 1653add4f0..eded11a135 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -246,6 +246,8 @@ jobs:
steps:
- uses: actions/checkout@v4
+ with:
+ fetch-depth: 0
- uses: actions/setup-python@v4
with:
python-version: "3.10"
From b14e818b37f62e3224da157b3ad768b3f0815fcd Mon Sep 17 00:00:00 2001
From: bashonly
Date: Sun, 11 Feb 2024 15:47:16 +0100
Subject: [PATCH 060/112] [ci] Bump `actions/setup-python` to v5
Authored by: bashonly
---
.github/workflows/build.yml | 6 +++---
.github/workflows/core.yml | 2 +-
.github/workflows/download.yml | 4 ++--
.github/workflows/quick-test.yml | 4 ++--
.github/workflows/release.yml | 6 +++---
5 files changed, 11 insertions(+), 11 deletions(-)
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index 0c2b0f684f..4d8e8bf380 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -107,7 +107,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- - uses: actions/setup-python@v4
+ - uses: actions/setup-python@v5
with:
python-version: "3.10"
- uses: conda-incubator/setup-miniconda@v2
@@ -328,7 +328,7 @@ jobs:
steps:
- uses: actions/checkout@v4
- - uses: actions/setup-python@v4
+ - uses: actions/setup-python@v5
with: # 3.8 is used for Win7 support
python-version: "3.8"
- name: Install Requirements
@@ -377,7 +377,7 @@ jobs:
steps:
- uses: actions/checkout@v4
- - uses: actions/setup-python@v4
+ - uses: actions/setup-python@v5
with:
python-version: "3.8"
architecture: "x86"
diff --git a/.github/workflows/core.yml b/.github/workflows/core.yml
index f694c9bdd1..ba8630630c 100644
--- a/.github/workflows/core.yml
+++ b/.github/workflows/core.yml
@@ -49,7 +49,7 @@ jobs:
steps:
- uses: actions/checkout@v4
- name: Set up Python ${{ matrix.python-version }}
- uses: actions/setup-python@v4
+ uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
- name: Install test requirements
diff --git a/.github/workflows/download.yml b/.github/workflows/download.yml
index 84339d9700..7256804d93 100644
--- a/.github/workflows/download.yml
+++ b/.github/workflows/download.yml
@@ -11,7 +11,7 @@ jobs:
steps:
- uses: actions/checkout@v4
- name: Set up Python
- uses: actions/setup-python@v4
+ uses: actions/setup-python@v5
with:
python-version: 3.9
- name: Install test requirements
@@ -38,7 +38,7 @@ jobs:
steps:
- uses: actions/checkout@v4
- name: Set up Python ${{ matrix.python-version }}
- uses: actions/setup-python@v4
+ uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
- name: Install test requirements
diff --git a/.github/workflows/quick-test.yml b/.github/workflows/quick-test.yml
index 4e9616926e..3114e7bdd6 100644
--- a/.github/workflows/quick-test.yml
+++ b/.github/workflows/quick-test.yml
@@ -11,7 +11,7 @@ jobs:
steps:
- uses: actions/checkout@v4
- name: Set up Python 3.8
- uses: actions/setup-python@v4
+ uses: actions/setup-python@v5
with:
python-version: '3.8'
- name: Install test requirements
@@ -26,7 +26,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- - uses: actions/setup-python@v4
+ - uses: actions/setup-python@v5
- name: Install flake8
run: python3 ./devscripts/install_deps.py -o --include dev
- name: Make lazy extractors
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index eded11a135..fac096be7d 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -71,7 +71,7 @@ jobs:
with:
fetch-depth: 0
- - uses: actions/setup-python@v4
+ - uses: actions/setup-python@v5
with:
python-version: "3.10"
@@ -248,7 +248,7 @@ jobs:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- - uses: actions/setup-python@v4
+ - uses: actions/setup-python@v5
with:
python-version: "3.10"
@@ -297,7 +297,7 @@ jobs:
with:
fetch-depth: 0
- uses: actions/download-artifact@v3
- - uses: actions/setup-python@v4
+ - uses: actions/setup-python@v5
with:
python-version: "3.10"
From b0059f0413a6ba6ab0a3aec1f00188ce083cd8bf Mon Sep 17 00:00:00 2001
From: bashonly
Date: Sun, 11 Feb 2024 15:47:48 +0100
Subject: [PATCH 061/112] [build] Bump `conda-incubator/setup-miniconda` to v3
Authored by: bashonly
---
.github/workflows/build.yml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index 4d8e8bf380..e8a97e3f43 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -110,7 +110,7 @@ jobs:
- uses: actions/setup-python@v5
with:
python-version: "3.10"
- - uses: conda-incubator/setup-miniconda@v2
+ - uses: conda-incubator/setup-miniconda@v3
with:
miniforge-variant: Mambaforge
use-mamba: true
From 3876429d72afb35247f4b2531eb9b16cfc7e0968 Mon Sep 17 00:00:00 2001
From: bashonly
Date: Sun, 11 Feb 2024 15:48:09 +0100
Subject: [PATCH 062/112] [build] Bump `actions/upload-artifact` to v4 and
adjust workflows
Authored by: bashonly
---
.github/workflows/build.yml | 36 ++++++++++++++++++++++++++---------
.github/workflows/release.yml | 6 +++++-
2 files changed, 32 insertions(+), 10 deletions(-)
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index e8a97e3f43..cd7ead7966 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -162,13 +162,15 @@ jobs:
done
- name: Upload artifacts
- uses: actions/upload-artifact@v3
+ uses: actions/upload-artifact@v4
with:
+ name: build-${{ github.job }}
path: |
yt-dlp
yt-dlp.tar.gz
yt-dlp_linux
yt-dlp_linux.zip
+ compression-level: 0
linux_arm:
needs: process
@@ -223,10 +225,12 @@ jobs:
fi
- name: Upload artifacts
- uses: actions/upload-artifact@v3
+ uses: actions/upload-artifact@v4
with:
+ name: build-linux_${{ matrix.architecture }}
path: | # run-on-arch-action designates armv7l as armv7
repo/dist/yt-dlp_linux_${{ (matrix.architecture == 'armv7' && 'armv7l') || matrix.architecture }}
+ compression-level: 0
macos:
needs: process
@@ -265,11 +269,13 @@ jobs:
[[ "$version" != "$downgraded_version" ]]
- name: Upload artifacts
- uses: actions/upload-artifact@v3
+ uses: actions/upload-artifact@v4
with:
+ name: build-${{ github.job }}
path: |
dist/yt-dlp_macos
dist/yt-dlp_macos.zip
+ compression-level: 0
macos_legacy:
needs: process
@@ -316,10 +322,12 @@ jobs:
[[ "$version" != "$downgraded_version" ]]
- name: Upload artifacts
- uses: actions/upload-artifact@v3
+ uses: actions/upload-artifact@v4
with:
+ name: build-${{ github.job }}
path: |
dist/yt-dlp_macos_legacy
+ compression-level: 0
windows:
needs: process
@@ -363,12 +371,14 @@ jobs:
}
- name: Upload artifacts
- uses: actions/upload-artifact@v3
+ uses: actions/upload-artifact@v4
with:
+ name: build-${{ github.job }}
path: |
dist/yt-dlp.exe
dist/yt-dlp_min.exe
dist/yt-dlp_win.zip
+ compression-level: 0
windows32:
needs: process
@@ -409,10 +419,12 @@ jobs:
}
- name: Upload artifacts
- uses: actions/upload-artifact@v3
+ uses: actions/upload-artifact@v4
with:
+ name: build-${{ github.job }}
path: |
dist/yt-dlp_x86.exe
+ compression-level: 0
meta_files:
if: inputs.meta_files && always() && !cancelled()
@@ -426,7 +438,11 @@ jobs:
- windows32
runs-on: ubuntu-latest
steps:
- - uses: actions/download-artifact@v3
+ - uses: actions/download-artifact@v4
+ with:
+ path: artifact
+ pattern: build-*
+ merge-multiple: true
- name: Make SHA2-SUMS files
run: |
@@ -461,8 +477,10 @@ jobs:
done
- name: Upload artifacts
- uses: actions/upload-artifact@v3
+ uses: actions/upload-artifact@v4
with:
+ name: build-${{ github.job }}
path: |
- SHA*SUMS*
_update_spec
+ SHA*SUMS*
+ compression-level: 0
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index fac096be7d..f5c6a793e1 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -296,7 +296,11 @@ jobs:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- - uses: actions/download-artifact@v3
+ - uses: actions/download-artifact@v4
+ with:
+ path: artifact
+ pattern: build-*
+ merge-multiple: true
- uses: actions/setup-python@v5
with:
python-version: "3.10"
From 1ed5ee2f045f717e814f84ba461dadc58e712266 Mon Sep 17 00:00:00 2001
From: sepro <4618135+seproDev@users.noreply.github.com>
Date: Tue, 13 Feb 2024 04:11:17 +0100
Subject: [PATCH 063/112] [ie/Ant1NewsGrEmbed] Fix extractor (#9191)
Authored by: seproDev
---
yt_dlp/extractor/antenna.py | 10 +++++-----
1 file changed, 5 insertions(+), 5 deletions(-)
diff --git a/yt_dlp/extractor/antenna.py b/yt_dlp/extractor/antenna.py
index c78717aa9e..17a4b6900d 100644
--- a/yt_dlp/extractor/antenna.py
+++ b/yt_dlp/extractor/antenna.py
@@ -78,14 +78,14 @@ class Ant1NewsGrArticleIE(AntennaBaseIE):
_TESTS = [{
'url': 'https://www.ant1news.gr/afieromata/article/549468/o-tzeims-mpont-sta-meteora-oi-apeiles-kai-o-xesikomos-ton-kalogeron',
- 'md5': '294f18331bb516539d72d85a82887dcc',
+ 'md5': '57eb8d12181f0fa2b14b0b138e1de9b6',
'info_dict': {
'id': '_xvg/m_cmbatw=',
'ext': 'mp4',
'title': 'md5:a93e8ecf2e4073bfdffcb38f59945411',
- 'timestamp': 1603092840,
- 'upload_date': '20201019',
- 'thumbnail': 'https://ant1media.azureedge.net/imgHandler/640/756206d2-d640-40e2-b201-3555abdfc0db.jpg',
+ 'timestamp': 1666166520,
+ 'upload_date': '20221019',
+ 'thumbnail': 'https://ant1media.azureedge.net/imgHandler/1920/756206d2-d640-40e2-b201-3555abdfc0db.jpg',
},
}, {
'url': 'https://ant1news.gr/Society/article/620286/symmoria-anilikon-dikigoros-thymaton-ithelan-na-toys-apoteleiosoyn',
@@ -117,7 +117,7 @@ class Ant1NewsGrEmbedIE(AntennaBaseIE):
_BASE_PLAYER_URL_RE = r'(?:https?:)?//(?:[a-zA-Z0-9\-]+\.)?(?:antenna|ant1news)\.gr/templates/pages/player'
_VALID_URL = rf'{_BASE_PLAYER_URL_RE}\?([^#]+&)?cid=(?P[^#&]+)'
_EMBED_REGEX = [rf'