diff --git a/poetry.lock b/poetry.lock index 4a46eac..7607019 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 2.1.3 and should not be changed by hand. +# This file is automatically @generated by Poetry 2.4.1 and should not be changed by hand. [[package]] name = "certifi" @@ -295,6 +295,19 @@ files = [ dev = ["pre-commit", "tox"] testing = ["pytest", "pytest-benchmark"] +[[package]] +name = "pysocks" +version = "1.7.1" +description = "A Python SOCKS client module. See https://github.com/Anorov/PySocks for more information." +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +groups = ["main"] +files = [ + {file = "PySocks-1.7.1-py27-none-any.whl", hash = "sha256:08e69f092cc6dbe92a0fdd16eeb9b9ffbc13cadfe5ca4c7bd92ffb078b293299"}, + {file = "PySocks-1.7.1-py3-none-any.whl", hash = "sha256:2725bd0a9925919b9b51739eea5f9e2bae91e83288108a9ad338b2e3a4435ee5"}, + {file = "PySocks-1.7.1.tar.gz", hash = "sha256:3f8804571ebe159c380ac6de37643bb4685970655d3bba243530d6558b799aa0"}, +] + [[package]] name = "pytest" version = "8.3.5" @@ -417,6 +430,7 @@ files = [ certifi = ">=2017.4.17" charset-normalizer = ">=2,<4" idna = ">=2.5,<4" +PySocks = {version = ">=1.5.6,<1.5.7 || >1.5.7", optional = true, markers = "extra == \"socks\""} urllib3 = ">=1.21.1,<3" [package.extras] @@ -535,4 +549,4 @@ zstd = ["zstandard (>=0.18.0)"] [metadata] lock-version = "2.1" python-versions = ">=3.8,<3.15" -content-hash = "34bdc0ef7b905a807195902bdfdd5db84e0bc5ccd0f6cb1883c35004a6e06dd6" +content-hash = "0c7bc95b0a277c5883b4a83e3f8d0a2399e16425fe6ef420dc3ffa00a93ec9c0" diff --git a/pyproject.toml b/pyproject.toml index d0bec13..f6baa7c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -50,7 +50,7 @@ precommit.shell = "poe format && poe lint && poe coverage" [tool.poetry.dependencies] python = ">=3.8,<3.15" -requests = "*" +requests = { extras = ["socks"], version = "*" } defusedxml = "^0.7.1" [tool.poetry.group.test] @@ -93,4 +93,4 @@ exclude_lines = [ "@abstractclassmethod", "@abstractstaticmethod" ] -show_missing = true \ No newline at end of file +show_missing = true diff --git a/youtube_transcript_api/_cli.py b/youtube_transcript_api/_cli.py index d99b562..db403fa 100644 --- a/youtube_transcript_api/_cli.py +++ b/youtube_transcript_api/_cli.py @@ -19,10 +19,15 @@ def run(self) -> str: return "" proxy_config = None - if parsed_args.http_proxy != "" or parsed_args.https_proxy != "": + if ( + parsed_args.http_proxy != "" + or parsed_args.https_proxy != "" + or parsed_args.socks_proxy != "" + ): proxy_config = GenericProxyConfig( http_url=parsed_args.http_proxy, https_url=parsed_args.https_proxy, + socks_url=parsed_args.socks_proxy, ) if ( @@ -188,6 +193,12 @@ def _parse_args(self): metavar="URL", help="Use the specified HTTPS proxy.", ) + parser.add_argument( + "--socks-proxy", + default="", + metavar="URL", + help="Use the specified SOCKS proxy.", + ) # Cookie auth has been temporarily disabled, as it is not working properly with # YouTube's most recent changes. # parser.add_argument( diff --git a/youtube_transcript_api/proxies.py b/youtube_transcript_api/proxies.py index 62a67c5..88fe554 100644 --- a/youtube_transcript_api/proxies.py +++ b/youtube_transcript_api/proxies.py @@ -59,32 +59,41 @@ class GenericProxyConfig(ProxyConfig): documentation to get more detailed information on how to set up proxies: https://requests.readthedocs.io/en/latest/user/advanced/#proxies - If only an HTTP or an HTTPS proxy is provided, it will be used for both types of - connections. However, you will have to provide at least one of the two. + If only an HTTP, HTTPS, or SOCKS proxy is provided, it will be used for both + types of connections. However, you will have to provide at least one of the three. """ - def __init__(self, http_url: Optional[str] = None, https_url: Optional[str] = None): + def __init__( + self, + http_url: Optional[str] = None, + https_url: Optional[str] = None, + socks_url: Optional[str] = None, + ): """ - If only an HTTP or an HTTPS proxy is provided, it will be used for both types of - connections. However, you will have to provide at least one of the two. + If only an HTTP, HTTPS, or SOCKS proxy is provided, it will be used for both + types of connections. However, you will have to provide at least one of the + three. - :param http_url: the proxy URL used for HTTP requests. Defaults to `https_url` - if None. + :param http_url: the proxy URL used for HTTP requests. Defaults to `socks_url` + or `https_url` if None. :param https_url: the proxy URL used for HTTPS requests. Defaults to `http_url` - if None. + or `socks_url` if None. + :param socks_url: the proxy URL used for HTTP and HTTPS requests if `http_url` + or `https_url` are None. """ - if not http_url and not https_url: + if not http_url and not https_url and not socks_url: raise InvalidProxyConfig( - "GenericProxyConfig requires you to define at least one of the two: " - "http or https" + "GenericProxyConfig requires you to define at least one of the " + "following: http, https, or socks" ) self.http_url = http_url self.https_url = https_url + self.socks_url = socks_url def to_requests_dict(self) -> RequestsProxyConfigDict: return { - "http": self.http_url or self.https_url, - "https": self.https_url or self.http_url, + "http": self.http_url or self.socks_url or self.https_url, + "https": self.https_url or self.socks_url or self.http_url, } diff --git a/youtube_transcript_api/test/test_cli.py b/youtube_transcript_api/test/test_cli.py index 78c23c3..e6346ff 100644 --- a/youtube_transcript_api/test/test_cli.py +++ b/youtube_transcript_api/test/test_cli.py @@ -68,6 +68,7 @@ def test_argument_parsing(self): self.assertEqual(parsed_args.languages, ["de", "en"]) self.assertEqual(parsed_args.http_proxy, "") self.assertEqual(parsed_args.https_proxy, "") + self.assertEqual(parsed_args.socks_proxy, "") parsed_args = YouTubeTranscriptCli( "v1 v2 --languages de en --format json".split() @@ -77,6 +78,7 @@ def test_argument_parsing(self): self.assertEqual(parsed_args.languages, ["de", "en"]) self.assertEqual(parsed_args.http_proxy, "") self.assertEqual(parsed_args.https_proxy, "") + self.assertEqual(parsed_args.socks_proxy, "") parsed_args = YouTubeTranscriptCli( " --format json v1 v2 --languages de en".split() @@ -86,17 +88,20 @@ def test_argument_parsing(self): self.assertEqual(parsed_args.languages, ["de", "en"]) self.assertEqual(parsed_args.http_proxy, "") self.assertEqual(parsed_args.https_proxy, "") + self.assertEqual(parsed_args.socks_proxy, "") parsed_args = YouTubeTranscriptCli( "v1 v2 --languages de en --format json " "--http-proxy http://user:pass@domain:port " - "--https-proxy https://user:pass@domain:port".split() + "--https-proxy https://user:pass@domain:port " + "--socks-proxy socks5h://user:pass@domain:port".split() )._parse_args() self.assertEqual(parsed_args.video_ids, ["v1", "v2"]) self.assertEqual(parsed_args.format, "json") self.assertEqual(parsed_args.languages, ["de", "en"]) self.assertEqual(parsed_args.http_proxy, "http://user:pass@domain:port") self.assertEqual(parsed_args.https_proxy, "https://user:pass@domain:port") + self.assertEqual(parsed_args.socks_proxy, "socks5h://user:pass@domain:port") parsed_args = YouTubeTranscriptCli( "v1 v2 --languages de en --format json " @@ -127,6 +132,16 @@ def test_argument_parsing(self): self.assertEqual(parsed_args.https_proxy, "https://user:pass@domain:port") self.assertEqual(parsed_args.http_proxy, "") + parsed_args = YouTubeTranscriptCli( + "v1 v2 --languages de en --format json --socks-proxy socks5h://user:pass@domain:port".split() + )._parse_args() + self.assertEqual(parsed_args.video_ids, ["v1", "v2"]) + self.assertEqual(parsed_args.format, "json") + self.assertEqual(parsed_args.languages, ["de", "en"]) + self.assertEqual(parsed_args.socks_proxy, "socks5h://user:pass@domain:port") + self.assertEqual(parsed_args.http_proxy, "") + self.assertEqual(parsed_args.https_proxy, "") + def test_argument_parsing__only_video_ids(self): parsed_args = YouTubeTranscriptCli("v1 v2".split())._parse_args() self.assertEqual(parsed_args.video_ids, ["v1", "v2"]) @@ -174,14 +189,21 @@ def test_argument_parsing__proxies(self): self.assertEqual(parsed_args.https_proxy, "https://user:pass@domain:port") parsed_args = YouTubeTranscriptCli( - "v1 v2 --http-proxy http://user:pass@domain:port --https-proxy https://user:pass@domain:port".split() + "v1 v2 --socks-proxy socks5h://user:pass@domain:port".split() + )._parse_args() + self.assertEqual(parsed_args.socks_proxy, "socks5h://user:pass@domain:port") + + parsed_args = YouTubeTranscriptCli( + "v1 v2 --http-proxy http://user:pass@domain:port --https-proxy https://user:pass@domain:port --socks-proxy socks5h://user:pass@domain:port".split() )._parse_args() self.assertEqual(parsed_args.http_proxy, "http://user:pass@domain:port") self.assertEqual(parsed_args.https_proxy, "https://user:pass@domain:port") + self.assertEqual(parsed_args.socks_proxy, "socks5h://user:pass@domain:port") parsed_args = YouTubeTranscriptCli("v1 v2".split())._parse_args() self.assertEqual(parsed_args.http_proxy, "") self.assertEqual(parsed_args.https_proxy, "") + self.assertEqual(parsed_args.socks_proxy, "") def test_argument_parsing__list_transcripts(self): parsed_args = YouTubeTranscriptCli( @@ -329,13 +351,27 @@ def test_run__generic_proxy_config(self): self.assertEqual(proxy_config.http_url, "http://user:pass@domain:port") self.assertEqual(proxy_config.https_url, "https://user:pass@domain:port") + def test_run__generic_socks_proxy_config(self): + YouTubeTranscriptCli( + ( + "v1 v2 --languages de en --socks-proxy socks5h://user:pass@domain:port" + ).split() + ).run() + + proxy_config = YouTubeTranscriptApi.__init__.call_args.kwargs.get( + "proxy_config" + ) + + self.assertIsNotNone(proxy_config) + self.assertEqual(proxy_config.socks_url, "socks5h://user:pass@domain:port") + @pytest.mark.skip( reason="This test is temporarily disabled because cookie auth is currently not " "working due to YouTube changes." ) def test_run__cookies(self): YouTubeTranscriptCli( - ("v1 v2 --languages de en " "--cookies blahblah.txt").split() + ("v1 v2 --languages de en --cookies blahblah.txt").split() ).run() YouTubeTranscriptApi.__init__.assert_any_call( @@ -358,9 +394,9 @@ def test_version_matches_metadata(self): check=True, ).stdout.strip() - assert ( - cli_version_msg == expected_version_msg - ), f"Expected version '{expected_version_msg}', but got '{cli_version_msg}'" + assert cli_version_msg == expected_version_msg, ( + f"Expected version '{expected_version_msg}', but got '{cli_version_msg}'" + ) def test_get_version_package_not_found(self): with patch( diff --git a/youtube_transcript_api/test/test_proxies.py b/youtube_transcript_api/test/test_proxies.py index 1bf0873..2f8ee63 100644 --- a/youtube_transcript_api/test/test_proxies.py +++ b/youtube_transcript_api/test/test_proxies.py @@ -45,6 +45,31 @@ def test_to_requests_dict__only_https(self): "https": "https://myproxy.com", } + def test_to_requests_dict__only_socks(self): + proxy_config = GenericProxyConfig( + socks_url="socks5h://myproxy.com", + ) + + request_dict = proxy_config.to_requests_dict() + + assert request_dict == { + "http": "socks5h://myproxy.com", + "https": "socks5h://myproxy.com", + } + + def test_to_requests_dict__socks_as_fallback(self): + proxy_config = GenericProxyConfig( + http_url="http://myproxy.com", + socks_url="socks5h://myproxy.com", + ) + + request_dict = proxy_config.to_requests_dict() + + assert request_dict == { + "http": "http://myproxy.com", + "https": "socks5h://myproxy.com", + } + def test__invalid_config(self): with pytest.raises(InvalidProxyConfig): GenericProxyConfig()