u-transcript-max 0.1.0a1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- u_transcript_max-0.1.0a1.dist-info/METADATA +67 -0
- u_transcript_max-0.1.0a1.dist-info/RECORD +77 -0
- u_transcript_max-0.1.0a1.dist-info/WHEEL +4 -0
- u_transcript_max-0.1.0a1.dist-info/entry_points.txt +2 -0
- u_transcript_max-0.1.0a1.dist-info/licenses/LICENSE +21 -0
- utmax/__init__.py +651 -0
- utmax/_version.py +1 -0
- utmax/adapters/__init__.py +1 -0
- utmax/adapters/downloader.py +479 -0
- utmax/adapters/ffmpeg.py +174 -0
- utmax/adapters/files.py +130 -0
- utmax/adapters/http.py +285 -0
- utmax/adapters/innertube.py +207 -0
- utmax/adapters/providers/__init__.py +45 -0
- utmax/adapters/providers/base.py +177 -0
- utmax/adapters/providers/claude.py +108 -0
- utmax/adapters/providers/gemini.py +133 -0
- utmax/adapters/providers/openai.py +169 -0
- utmax/adapters/providers/openrouter.py +59 -0
- utmax/adapters/watch_page.py +73 -0
- utmax/client.py +300 -0
- utmax/compat/__init__.py +134 -0
- utmax/compat/_api.py +203 -0
- utmax/compat/_bridge.py +212 -0
- utmax/compat/_errors.py +300 -0
- utmax/compat/_settings.py +13 -0
- utmax/compat/_transcripts.py +440 -0
- utmax/compat/formatters.py +241 -0
- utmax/compat/proxies.py +129 -0
- utmax/core/__init__.py +6 -0
- utmax/core/bilingual.py +107 -0
- utmax/core/browse.py +397 -0
- utmax/core/captions.py +251 -0
- utmax/core/clients.py +113 -0
- utmax/core/downloads.py +165 -0
- utmax/core/filenames.py +298 -0
- utmax/core/formats.py +198 -0
- utmax/core/ids.py +210 -0
- utmax/core/languages.py +371 -0
- utmax/core/media/__init__.py +7 -0
- utmax/core/media/boxes.py +266 -0
- utmax/core/media/fmp4.py +394 -0
- utmax/core/media/moov.py +299 -0
- utmax/core/media/mux.py +423 -0
- utmax/core/media/progressive.py +267 -0
- utmax/core/media/tables.py +129 -0
- utmax/core/media/tx3g.py +191 -0
- utmax/core/playability.py +62 -0
- utmax/core/player.py +90 -0
- utmax/core/retry.py +45 -0
- utmax/core/segmentation.py +104 -0
- utmax/core/selection.py +92 -0
- utmax/core/streams.py +264 -0
- utmax/core/translate/__init__.py +1 -0
- utmax/core/translate/batching.py +157 -0
- utmax/core/translate/data/protocol.json +17 -0
- utmax/core/translate/data/request.schema.json +49 -0
- utmax/core/translate/data/response.schema.json +19 -0
- utmax/core/translate/data/system_prompt.txt +21 -0
- utmax/core/translate/protocol.py +101 -0
- utmax/core/translate/spec.py +49 -0
- utmax/core/ytdata.py +32 -0
- utmax/errors.py +523 -0
- utmax/mcp/__init__.py +30 -0
- utmax/mcp/__main__.py +6 -0
- utmax/mcp/config.py +56 -0
- utmax/mcp/server.py +626 -0
- utmax/models.py +499 -0
- utmax/providers.py +36 -0
- utmax/py.typed +0 -0
- utmax/services/__init__.py +1 -0
- utmax/services/bulk.py +454 -0
- utmax/services/collections.py +183 -0
- utmax/services/download.py +375 -0
- utmax/services/transcripts.py +87 -0
- utmax/services/translation.py +211 -0
- utmax/transport.py +81 -0
utmax/compat/_api.py
ADDED
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
"""``YouTubeTranscriptApi``, compatible with youtube-transcript-api 1.2.4 and the class methods
|
|
2
|
+
of 0.6."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import builtins
|
|
7
|
+
import os
|
|
8
|
+
import warnings
|
|
9
|
+
from collections.abc import Iterable, Mapping
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
from utmax.compat._transcripts import FetchedTranscript, TranscriptList, TranscriptListFetcher
|
|
13
|
+
from utmax.compat.proxies import GenericProxyConfig, ProxyConfig
|
|
14
|
+
|
|
15
|
+
__all__ = [
|
|
16
|
+
"FetchedTranscript",
|
|
17
|
+
"ProxyConfig",
|
|
18
|
+
"TranscriptList",
|
|
19
|
+
"TranscriptListFetcher",
|
|
20
|
+
"YouTubeTranscriptApi",
|
|
21
|
+
]
|
|
22
|
+
|
|
23
|
+
_LegacyProxies = ProxyConfig | Mapping[str, str] | None
|
|
24
|
+
_RawTranscript = builtins.list[dict[str, Any]]
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class YouTubeTranscriptApi:
|
|
28
|
+
"""Fetches transcripts with utmax; a drop-in replacement for youtube-transcript-api's class.
|
|
29
|
+
|
|
30
|
+
Unlike youtube-transcript-api, an instance is thread-safe (unless ``http_client`` is not),
|
|
31
|
+
and every method also accepts YouTube video URLs.
|
|
32
|
+
|
|
33
|
+
Args:
|
|
34
|
+
proxy_config: proxies for all requests, such as ``GenericProxyConfig`` or
|
|
35
|
+
``WebshareProxyConfig``. Without ``http_client`` only ``http://`` proxies work.
|
|
36
|
+
http_client: a ``requests.Session``-like object that sends every request instead of
|
|
37
|
+
utmax's own HTTP code: use it for SOCKS or HTTPS proxies, custom certificates or
|
|
38
|
+
shared cookies.
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
def __init__(
|
|
42
|
+
self,
|
|
43
|
+
proxy_config: ProxyConfig | None = None,
|
|
44
|
+
http_client: Any = None,
|
|
45
|
+
) -> None:
|
|
46
|
+
if http_client is not None:
|
|
47
|
+
http_client.headers.update({"Accept-Language": "en-US"})
|
|
48
|
+
if proxy_config is not None:
|
|
49
|
+
http_client.proxies = proxy_config.to_requests_dict()
|
|
50
|
+
if proxy_config.prevent_keeping_connections_alive:
|
|
51
|
+
http_client.headers.update({"Connection": "close"})
|
|
52
|
+
self._fetcher = TranscriptListFetcher(http_client, proxy_config=proxy_config)
|
|
53
|
+
|
|
54
|
+
def fetch(
|
|
55
|
+
self,
|
|
56
|
+
video_id: str,
|
|
57
|
+
languages: Iterable[str] = ("en",),
|
|
58
|
+
preserve_formatting: bool = False,
|
|
59
|
+
) -> FetchedTranscript:
|
|
60
|
+
"""
|
|
61
|
+
Retrieves the transcript for a single video. This is just a shortcut for
|
|
62
|
+
calling:
|
|
63
|
+
`YouTubeTranscriptApi().list(video_id).find_transcript(languages).fetch(preserve_formatting=preserve_formatting)`
|
|
64
|
+
|
|
65
|
+
:param video_id: the ID or URL of the video you want to retrieve the transcript for.
|
|
66
|
+
:param languages: A list of language codes in a descending priority. For
|
|
67
|
+
example, if this is set to ["de", "en"] it will first try to fetch the
|
|
68
|
+
german transcript (de) and then fetch the english transcript (en) if
|
|
69
|
+
it fails to do so. This defaults to ["en"].
|
|
70
|
+
:param preserve_formatting: whether to keep select HTML text formatting
|
|
71
|
+
"""
|
|
72
|
+
return (
|
|
73
|
+
self.list(video_id)
|
|
74
|
+
.find_transcript(languages)
|
|
75
|
+
.fetch(preserve_formatting=preserve_formatting)
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
def list(
|
|
79
|
+
self,
|
|
80
|
+
video_id: str,
|
|
81
|
+
) -> TranscriptList:
|
|
82
|
+
"""
|
|
83
|
+
Retrieves the list of transcripts which are available for a given video. It
|
|
84
|
+
returns a `TranscriptList` object which is iterable and provides methods to
|
|
85
|
+
filter the list of transcripts for specific languages. While iterating over
|
|
86
|
+
the `TranscriptList` the individual transcripts are represented by
|
|
87
|
+
`Transcript` objects, which provide metadata and can either be fetched by
|
|
88
|
+
calling `transcript.fetch()` or translated by calling `transcript.translate(
|
|
89
|
+
'en')`.
|
|
90
|
+
|
|
91
|
+
:param video_id: the ID or URL of the video you want to retrieve the transcript for.
|
|
92
|
+
"""
|
|
93
|
+
return self._fetcher.fetch(video_id)
|
|
94
|
+
|
|
95
|
+
@classmethod
|
|
96
|
+
def list_transcripts(
|
|
97
|
+
cls,
|
|
98
|
+
video_id: str,
|
|
99
|
+
proxies: _LegacyProxies = None,
|
|
100
|
+
cookies: str | os.PathLike[str] | None = None,
|
|
101
|
+
) -> TranscriptList:
|
|
102
|
+
"""
|
|
103
|
+
DEPRECATED: use the `list` method instead!
|
|
104
|
+
|
|
105
|
+
The class method of youtube-transcript-api 0.6. ``proxies`` is a ``ProxyConfig`` or a
|
|
106
|
+
requests-style ``{"http": url, "https": url}`` mapping; ``cookies`` is ignored with a
|
|
107
|
+
warning, because cookie authentication is not supported.
|
|
108
|
+
"""
|
|
109
|
+
warnings.warn(
|
|
110
|
+
"`list_transcripts` is deprecated and will be removed in a future version. "
|
|
111
|
+
"Use the `list` method instead!",
|
|
112
|
+
DeprecationWarning,
|
|
113
|
+
stacklevel=2,
|
|
114
|
+
)
|
|
115
|
+
if cookies:
|
|
116
|
+
warnings.warn(
|
|
117
|
+
"Cookie authentication is not supported; the cookies argument is ignored.",
|
|
118
|
+
UserWarning,
|
|
119
|
+
stacklevel=2,
|
|
120
|
+
)
|
|
121
|
+
return cls(proxy_config=_legacy_proxy_config(proxies)).list(video_id)
|
|
122
|
+
|
|
123
|
+
@classmethod
|
|
124
|
+
def get_transcripts( # noqa: PLR0917
|
|
125
|
+
cls,
|
|
126
|
+
video_ids: builtins.list[str],
|
|
127
|
+
languages: Iterable[str] = ("en",),
|
|
128
|
+
continue_after_error: bool = False,
|
|
129
|
+
proxies: _LegacyProxies = None,
|
|
130
|
+
cookies: str | os.PathLike[str] | None = None,
|
|
131
|
+
preserve_formatting: bool = False,
|
|
132
|
+
) -> tuple[dict[str, _RawTranscript], builtins.list[str]]:
|
|
133
|
+
"""
|
|
134
|
+
DEPRECATED: use the `fetch` method instead!
|
|
135
|
+
|
|
136
|
+
Retrieves the transcripts for a list of videos.
|
|
137
|
+
|
|
138
|
+
:return: a tuple containing a dictionary mapping video ids onto their corresponding
|
|
139
|
+
transcripts (lists of ``{"text", "start", "duration"}`` dictionaries), and a list of
|
|
140
|
+
video ids, which could not be retrieved
|
|
141
|
+
"""
|
|
142
|
+
warnings.warn(
|
|
143
|
+
"`get_transcripts` is deprecated and will be removed in a future version. "
|
|
144
|
+
"Use the `fetch` method instead!",
|
|
145
|
+
DeprecationWarning,
|
|
146
|
+
stacklevel=2,
|
|
147
|
+
)
|
|
148
|
+
if not isinstance(video_ids, builtins.list):
|
|
149
|
+
raise AssertionError("`video_ids` must be a list of strings")
|
|
150
|
+
|
|
151
|
+
data: dict[str, _RawTranscript] = {}
|
|
152
|
+
unretrievable_videos: builtins.list[str] = []
|
|
153
|
+
|
|
154
|
+
for video_id in video_ids:
|
|
155
|
+
try:
|
|
156
|
+
data[video_id] = cls.get_transcript(
|
|
157
|
+
video_id, languages, proxies, cookies, preserve_formatting
|
|
158
|
+
)
|
|
159
|
+
except Exception:
|
|
160
|
+
if not continue_after_error:
|
|
161
|
+
raise
|
|
162
|
+
|
|
163
|
+
unretrievable_videos.append(video_id)
|
|
164
|
+
|
|
165
|
+
return data, unretrievable_videos
|
|
166
|
+
|
|
167
|
+
@classmethod
|
|
168
|
+
def get_transcript(
|
|
169
|
+
cls,
|
|
170
|
+
video_id: str,
|
|
171
|
+
languages: Iterable[str] = ("en",),
|
|
172
|
+
proxies: _LegacyProxies = None,
|
|
173
|
+
cookies: str | os.PathLike[str] | None = None,
|
|
174
|
+
preserve_formatting: bool = False,
|
|
175
|
+
) -> _RawTranscript:
|
|
176
|
+
"""
|
|
177
|
+
DEPRECATED: use the `fetch` method instead!
|
|
178
|
+
|
|
179
|
+
Retrieves the transcript for a single video as a list of ``{"text", "start",
|
|
180
|
+
"duration"}`` dictionaries.
|
|
181
|
+
"""
|
|
182
|
+
warnings.warn(
|
|
183
|
+
"`get_transcript` is deprecated and will be removed in a future version. "
|
|
184
|
+
"Use the `fetch` method instead!",
|
|
185
|
+
DeprecationWarning,
|
|
186
|
+
stacklevel=2,
|
|
187
|
+
)
|
|
188
|
+
if not isinstance(video_id, str):
|
|
189
|
+
raise AssertionError("`video_id` must be a string")
|
|
190
|
+
return (
|
|
191
|
+
cls.list_transcripts(video_id, proxies, cookies)
|
|
192
|
+
.find_transcript(languages)
|
|
193
|
+
.fetch(preserve_formatting=preserve_formatting)
|
|
194
|
+
.to_raw_data()
|
|
195
|
+
)
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def _legacy_proxy_config(proxies: _LegacyProxies) -> ProxyConfig | None:
|
|
199
|
+
if not proxies:
|
|
200
|
+
return None
|
|
201
|
+
if isinstance(proxies, ProxyConfig):
|
|
202
|
+
return proxies
|
|
203
|
+
return GenericProxyConfig(http_url=proxies.get("http"), https_url=proxies.get("https"))
|
utmax/compat/_bridge.py
ADDED
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
"""How utmax.compat reaches YouTube: utmax's transports and InnerTube client, with utmax errors
|
|
2
|
+
turned into the exceptions of youtube-transcript-api."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import re
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
from utmax import errors
|
|
10
|
+
from utmax.adapters.http import RetryingTransport, UrllibTransport, redact
|
|
11
|
+
from utmax.adapters.innertube import InnerTubeClient
|
|
12
|
+
from utmax.compat._errors import (
|
|
13
|
+
AgeRestricted,
|
|
14
|
+
CouldNotRetrieveTranscript,
|
|
15
|
+
FailedToCreateConsentCookie,
|
|
16
|
+
InvalidVideoId,
|
|
17
|
+
IpBlocked,
|
|
18
|
+
PoTokenRequired,
|
|
19
|
+
RequestBlocked,
|
|
20
|
+
TranscriptsDisabled,
|
|
21
|
+
VideoUnavailable,
|
|
22
|
+
VideoUnplayable,
|
|
23
|
+
YouTubeDataUnparsable,
|
|
24
|
+
YouTubeRequestFailed,
|
|
25
|
+
)
|
|
26
|
+
from utmax.compat.proxies import InvalidProxyConfig, ProxyConfig
|
|
27
|
+
from utmax.core.ids import parse_video_id
|
|
28
|
+
from utmax.core.player import PlayerData
|
|
29
|
+
from utmax.transport import HttpRequest, HttpResponse, Transport
|
|
30
|
+
|
|
31
|
+
__all__ = [
|
|
32
|
+
"TIMEOUT",
|
|
33
|
+
"Connection",
|
|
34
|
+
"SessionTransport",
|
|
35
|
+
"compat_error",
|
|
36
|
+
"connection_for",
|
|
37
|
+
"make_transport",
|
|
38
|
+
"video_id_of",
|
|
39
|
+
]
|
|
40
|
+
|
|
41
|
+
TIMEOUT = 30.0
|
|
42
|
+
"""Seconds before a request is abandoned (also passed to an ``http_client``)."""
|
|
43
|
+
|
|
44
|
+
_SIMPLE_ERRORS: tuple[tuple[type[errors.UTMaxError], type[CouldNotRetrieveTranscript]], ...] = (
|
|
45
|
+
(errors.VideoUnavailable, VideoUnavailable),
|
|
46
|
+
(errors.AgeRestricted, AgeRestricted),
|
|
47
|
+
(errors.PoTokenRequired, PoTokenRequired),
|
|
48
|
+
(errors.FailedToCreateConsentCookie, FailedToCreateConsentCookie),
|
|
49
|
+
(errors.YouTubeDataUnparsable, YouTubeDataUnparsable),
|
|
50
|
+
(errors.TranscriptsDisabled, TranscriptsDisabled),
|
|
51
|
+
(errors.InvalidVideoId, InvalidVideoId),
|
|
52
|
+
)
|
|
53
|
+
_DECODED_HEADERS = frozenset({"content-encoding", "content-length", "transfer-encoding"})
|
|
54
|
+
_QUERY_RUN = re.compile(r"\?[^\s)'\"]+")
|
|
55
|
+
"""A ``?`` and what follows it up to whitespace, a closing parenthesis or a quote."""
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class SessionTransport:
|
|
59
|
+
"""Sends utmax's requests through a ``requests.Session``-like ``http_client``.
|
|
60
|
+
|
|
61
|
+
The client needs ``request(method, url, headers=..., data=..., timeout=...)`` returning an
|
|
62
|
+
object with ``status_code``, ``headers`` and ``content`` (the decoded body); that is how
|
|
63
|
+
SOCKS proxies, custom certificates or shared cookies reach utmax without utmax depending on
|
|
64
|
+
requests.
|
|
65
|
+
"""
|
|
66
|
+
|
|
67
|
+
def __init__(self, session: Any, *, timeout: float = TIMEOUT) -> None:
|
|
68
|
+
self._session = session
|
|
69
|
+
self._timeout = timeout
|
|
70
|
+
|
|
71
|
+
def send(self, request: HttpRequest) -> HttpResponse:
|
|
72
|
+
try:
|
|
73
|
+
response = self._session.request(
|
|
74
|
+
request.method,
|
|
75
|
+
request.url,
|
|
76
|
+
headers=dict(request.headers),
|
|
77
|
+
data=request.body,
|
|
78
|
+
timeout=self._timeout,
|
|
79
|
+
)
|
|
80
|
+
except Exception as error: # the session's own exception types are unknown here
|
|
81
|
+
reason = _without_query(str(error), request.url)
|
|
82
|
+
raise errors.NetworkError(
|
|
83
|
+
f"Could not complete {request.method} {redact(request.url)}: {reason}"
|
|
84
|
+
) from error
|
|
85
|
+
headers = {
|
|
86
|
+
str(name): str(value)
|
|
87
|
+
for name, value in response.headers.items()
|
|
88
|
+
if str(name).lower() not in _DECODED_HEADERS
|
|
89
|
+
}
|
|
90
|
+
return HttpResponse(
|
|
91
|
+
status=int(response.status_code),
|
|
92
|
+
url=request.url,
|
|
93
|
+
headers=headers,
|
|
94
|
+
body=bytes(response.content),
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def _without_query(text: str, url: str) -> str:
|
|
99
|
+
"""``text`` without the query string of ``url`` and without any other ``?...`` run.
|
|
100
|
+
|
|
101
|
+
A session's own error text repeats the request (``Max retries exceeded with url:
|
|
102
|
+
/api/timedtext?...&sig=...``), and the query of a caption URL holds the client's IP address,
|
|
103
|
+
an expiry time and a signature, none of which belongs in a message (spec section 7).
|
|
104
|
+
"""
|
|
105
|
+
query = url.partition("?")[2].partition("#")[0]
|
|
106
|
+
if query:
|
|
107
|
+
text = text.replace(f"?{query}", "").replace(query, "")
|
|
108
|
+
return _QUERY_RUN.sub("", text)
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def make_transport(http_client: Any, proxy_config: ProxyConfig | None) -> Transport:
|
|
112
|
+
"""The transport for an ``http_client`` (used as given) or for utmax's own HTTP stack.
|
|
113
|
+
|
|
114
|
+
Raises:
|
|
115
|
+
InvalidProxyConfig: utmax's own stack cannot use the proxy of ``proxy_config``.
|
|
116
|
+
"""
|
|
117
|
+
if http_client is not None:
|
|
118
|
+
return SessionTransport(http_client)
|
|
119
|
+
proxy = None
|
|
120
|
+
if proxy_config is not None:
|
|
121
|
+
proxies = proxy_config.to_requests_dict()
|
|
122
|
+
proxy = proxies.get("https") or proxies.get("http")
|
|
123
|
+
if not proxy:
|
|
124
|
+
raise InvalidProxyConfig(
|
|
125
|
+
f"{type(proxy_config).__name__}.to_requests_dict() names no proxy URL."
|
|
126
|
+
)
|
|
127
|
+
try:
|
|
128
|
+
inner = UrllibTransport(proxy=proxy, timeout=TIMEOUT)
|
|
129
|
+
except errors.InvalidOption as error:
|
|
130
|
+
raise InvalidProxyConfig(
|
|
131
|
+
f"{error} Without an http_client only http:// proxies work; for https:// or "
|
|
132
|
+
"SOCKS proxies pass http_client=requests.Session() to YouTubeTranscriptApi."
|
|
133
|
+
) from error
|
|
134
|
+
return RetryingTransport(inner)
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
class Connection:
|
|
138
|
+
"""utmax's InnerTube client behind one ``YouTubeTranscriptApi`` and its transcripts."""
|
|
139
|
+
|
|
140
|
+
def __init__(self, transport: Transport, *, proxy_config: ProxyConfig | None = None) -> None:
|
|
141
|
+
retries = proxy_config.retries_when_blocked if proxy_config is not None else 0
|
|
142
|
+
self._innertube = InnerTubeClient(transport, block_retries=max(0, retries))
|
|
143
|
+
self.proxy_config = proxy_config
|
|
144
|
+
|
|
145
|
+
def player(self, video: str) -> tuple[str, PlayerData]:
|
|
146
|
+
"""The video ID in ``video`` and its player response, which lists subtitle tracks.
|
|
147
|
+
|
|
148
|
+
Raises:
|
|
149
|
+
CouldNotRetrieveTranscript: the subclass matching what went wrong.
|
|
150
|
+
"""
|
|
151
|
+
video_id = video_id_of(video)
|
|
152
|
+
try:
|
|
153
|
+
player = self._innertube.player(video_id, purpose="captions")
|
|
154
|
+
except errors.UTMaxError as error:
|
|
155
|
+
raise compat_error(error, video_id, self.proxy_config) from error
|
|
156
|
+
if not player.caption_tracks:
|
|
157
|
+
raise TranscriptsDisabled(video_id)
|
|
158
|
+
return video_id, player
|
|
159
|
+
|
|
160
|
+
def caption_text(self, url: str, video_id: str) -> str:
|
|
161
|
+
"""The legacy XML of the caption track at ``url``.
|
|
162
|
+
|
|
163
|
+
Raises:
|
|
164
|
+
CouldNotRetrieveTranscript: the subclass matching what went wrong.
|
|
165
|
+
"""
|
|
166
|
+
try:
|
|
167
|
+
response = self._innertube.fetch_captions(url, video_id=video_id, fmt=None)
|
|
168
|
+
except errors.UTMaxError as error:
|
|
169
|
+
raise compat_error(error, video_id) from error
|
|
170
|
+
return response.text
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def connection_for(http_client: Any) -> Connection:
|
|
174
|
+
"""The connection of a ``Transcript``: the one utmax made, or one around ``http_client``."""
|
|
175
|
+
if isinstance(http_client, Connection):
|
|
176
|
+
return http_client
|
|
177
|
+
return Connection(make_transport(http_client, None))
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def video_id_of(video: str) -> str:
|
|
181
|
+
"""The ID in ``video``, a bare video ID or any YouTube video URL.
|
|
182
|
+
|
|
183
|
+
Raises:
|
|
184
|
+
InvalidVideoId: ``video`` is a URL without a video ID.
|
|
185
|
+
VideoUnavailable: ``video`` cannot be a video ID (what YouTube would answer).
|
|
186
|
+
"""
|
|
187
|
+
try:
|
|
188
|
+
return parse_video_id(video)
|
|
189
|
+
except errors.InvalidVideoId as error:
|
|
190
|
+
if video.strip().startswith(("http://", "https://")):
|
|
191
|
+
raise InvalidVideoId(video) from error
|
|
192
|
+
raise VideoUnavailable(video) from error
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def compat_error(
|
|
196
|
+
error: errors.UTMaxError, video_id: str, proxy_config: ProxyConfig | None = None
|
|
197
|
+
) -> CouldNotRetrieveTranscript:
|
|
198
|
+
"""The youtube-transcript-api exception for a utmax error.
|
|
199
|
+
|
|
200
|
+
Blocks carry ``proxy_config`` (their message depends on it). Network failures and any other
|
|
201
|
+
error become ``YouTubeRequestFailed`` with the utmax message as the reason.
|
|
202
|
+
"""
|
|
203
|
+
if isinstance(error, errors.IpBlocked):
|
|
204
|
+
return IpBlocked(video_id).with_proxy_config(proxy_config)
|
|
205
|
+
if isinstance(error, errors.RequestBlocked):
|
|
206
|
+
return RequestBlocked(video_id).with_proxy_config(proxy_config)
|
|
207
|
+
if isinstance(error, errors.VideoUnplayable):
|
|
208
|
+
return VideoUnplayable(video_id, error.reason or None, list(error.sub_reasons))
|
|
209
|
+
for source, target in _SIMPLE_ERRORS:
|
|
210
|
+
if isinstance(error, source):
|
|
211
|
+
return target(video_id)
|
|
212
|
+
return YouTubeRequestFailed(video_id, error)
|