diff --git a/.github/ISSUE_TEMPLATE/1_broken_site.yml b/.github/ISSUE_TEMPLATE/1_broken_site.yml
index 7117039ed7..c4bad101b3 100644
--- a/.github/ISSUE_TEMPLATE/1_broken_site.yml
+++ b/.github/ISSUE_TEMPLATE/1_broken_site.yml
@@ -2,6 +2,13 @@ name: Broken site
description: Report broken or misfunctioning site
labels: [triage, site-bug]
body:
+ - type: checkboxes
+ attributes:
+ label: DO NOT REMOVE OR SKIP THE ISSUE TEMPLATE
+ description: Fill all fields even if you think it is irrelevant for the issue
+ options:
+ - label: I understand that I will be **blocked** if I remove or skip any mandatory\* field
+ required: true
- type: checkboxes
id: checklist
attributes:
@@ -11,11 +18,11 @@ body:
options:
- label: I'm reporting a broken site
required: true
- - label: I've verified that I'm running yt-dlp version **2022.07.18** ([update instructions](https://github.com/yt-dlp/yt-dlp#update)) or later (specify commit)
+ - label: I've verified that I'm running yt-dlp version **2022.10.04** ([update instructions](https://github.com/yt-dlp/yt-dlp#update)) or later (specify commit)
required: true
- label: I've checked that all provided URLs are playable in a browser with the same IP and same login details
required: true
- - label: I've checked that all URLs and arguments with special characters are [properly quoted or escaped](https://github.com/ytdl-org/youtube-dl#video-url-contains-an-ampersand-and-im-getting-some-strange-output-1-2839-or-v-is-not-recognized-as-an-internal-or-external-command)
+ - label: I've checked that all URLs and arguments with special characters are [properly quoted or escaped](https://github.com/yt-dlp/yt-dlp/wiki/FAQ#video-url-contains-an-ampersand--and-im-getting-some-strange-output-1-2839-or-v-is-not-recognized-as-an-internal-or-external-command)
required: true
- label: I've searched the [bugtracker](https://github.com/yt-dlp/yt-dlp/issues?q=) for similar issues **including closed ones**. DO NOT post duplicates
required: true
@@ -55,7 +62,7 @@ body:
[debug] Command-line config: ['-vU', 'test:youtube']
[debug] Portable config "yt-dlp.conf": ['-i']
[debug] Encodings: locale cp65001, fs utf-8, pref cp65001, out utf-8, error utf-8, screen utf-8
- [debug] yt-dlp version 2022.07.18 [9d339c4] (win32_exe)
+ [debug] yt-dlp version 2022.10.04 [9d339c4] (win32_exe)
[debug] Python 3.8.10 (CPython 64bit) - Windows-10-10.0.22000-SP0
[debug] Checking exe version: ffmpeg -bsfs
[debug] Checking exe version: ffprobe -bsfs
@@ -63,8 +70,8 @@ body:
[debug] Optional libraries: Cryptodome-3.15.0, brotli-1.0.9, certifi-2022.06.15, mutagen-1.45.1, sqlite3-2.6.0, websockets-10.3
[debug] Proxy map: {}
[debug] Fetching release info: https://api.github.com/repos/yt-dlp/yt-dlp/releases/latest
- Latest version: 2022.07.18, Current version: 2022.07.18
- yt-dlp is up to date (2022.07.18)
+ Latest version: 2022.10.04, Current version: 2022.10.04
+ yt-dlp is up to date (2022.10.04)
render: shell
validations:
diff --git a/.github/ISSUE_TEMPLATE/2_site_support_request.yml b/.github/ISSUE_TEMPLATE/2_site_support_request.yml
index ffe8f32f0d..6cbdc8ee89 100644
--- a/.github/ISSUE_TEMPLATE/2_site_support_request.yml
+++ b/.github/ISSUE_TEMPLATE/2_site_support_request.yml
@@ -2,6 +2,13 @@ name: Site support request
description: Request support for a new site
labels: [triage, site-request]
body:
+ - type: checkboxes
+ attributes:
+ label: DO NOT REMOVE OR SKIP THE ISSUE TEMPLATE
+ description: Fill all fields even if you think it is irrelevant for the issue
+ options:
+ - label: I understand that I will be **blocked** if I remove or skip any mandatory\* field
+ required: true
- type: checkboxes
id: checklist
attributes:
@@ -11,11 +18,11 @@ body:
options:
- label: I'm reporting a new site support request
required: true
- - label: I've verified that I'm running yt-dlp version **2022.07.18** ([update instructions](https://github.com/yt-dlp/yt-dlp#update)) or later (specify commit)
+ - label: I've verified that I'm running yt-dlp version **2022.10.04** ([update instructions](https://github.com/yt-dlp/yt-dlp#update)) or later (specify commit)
required: true
- label: I've checked that all provided URLs are playable in a browser with the same IP and same login details
required: true
- - label: I've checked that none of provided URLs [violate any copyrights](https://github.com/ytdl-org/youtube-dl#can-you-add-support-for-this-anime-video-site-or-site-which-shows-current-movies-for-free) or contain any [DRM](https://en.wikipedia.org/wiki/Digital_rights_management) to the best of my knowledge
+ - label: I've checked that none of provided URLs [violate any copyrights](https://github.com/yt-dlp/yt-dlp/blob/master/CONTRIBUTING.md#is-the-website-primarily-used-for-piracy) or contain any [DRM](https://en.wikipedia.org/wiki/Digital_rights_management) to the best of my knowledge
required: true
- label: I've searched the [bugtracker](https://github.com/yt-dlp/yt-dlp/issues?q=) for similar issues **including closed ones**. DO NOT post duplicates
required: true
@@ -67,7 +74,7 @@ body:
[debug] Command-line config: ['-vU', 'test:youtube']
[debug] Portable config "yt-dlp.conf": ['-i']
[debug] Encodings: locale cp65001, fs utf-8, pref cp65001, out utf-8, error utf-8, screen utf-8
- [debug] yt-dlp version 2022.07.18 [9d339c4] (win32_exe)
+ [debug] yt-dlp version 2022.10.04 [9d339c4] (win32_exe)
[debug] Python 3.8.10 (CPython 64bit) - Windows-10-10.0.22000-SP0
[debug] Checking exe version: ffmpeg -bsfs
[debug] Checking exe version: ffprobe -bsfs
@@ -75,8 +82,8 @@ body:
[debug] Optional libraries: Cryptodome-3.15.0, brotli-1.0.9, certifi-2022.06.15, mutagen-1.45.1, sqlite3-2.6.0, websockets-10.3
[debug] Proxy map: {}
[debug] Fetching release info: https://api.github.com/repos/yt-dlp/yt-dlp/releases/latest
- Latest version: 2022.07.18, Current version: 2022.07.18
- yt-dlp is up to date (2022.07.18)
+ Latest version: 2022.10.04, Current version: 2022.10.04
+ yt-dlp is up to date (2022.10.04)
render: shell
validations:
diff --git a/.github/ISSUE_TEMPLATE/3_site_feature_request.yml b/.github/ISSUE_TEMPLATE/3_site_feature_request.yml
index 11bd109a6f..15101e885f 100644
--- a/.github/ISSUE_TEMPLATE/3_site_feature_request.yml
+++ b/.github/ISSUE_TEMPLATE/3_site_feature_request.yml
@@ -2,6 +2,13 @@ name: Site feature request
description: Request a new functionality for a supported site
labels: [triage, site-enhancement]
body:
+ - type: checkboxes
+ attributes:
+ label: DO NOT REMOVE OR SKIP THE ISSUE TEMPLATE
+ description: Fill all fields even if you think it is irrelevant for the issue
+ options:
+ - label: I understand that I will be **blocked** if I remove or skip any mandatory\* field
+ required: true
- type: checkboxes
id: checklist
attributes:
@@ -11,7 +18,7 @@ body:
options:
- label: I'm requesting a site-specific feature
required: true
- - label: I've verified that I'm running yt-dlp version **2022.07.18** ([update instructions](https://github.com/yt-dlp/yt-dlp#update)) or later (specify commit)
+ - label: I've verified that I'm running yt-dlp version **2022.10.04** ([update instructions](https://github.com/yt-dlp/yt-dlp#update)) or later (specify commit)
required: true
- label: I've checked that all provided URLs are playable in a browser with the same IP and same login details
required: true
@@ -63,7 +70,7 @@ body:
[debug] Command-line config: ['-vU', 'test:youtube']
[debug] Portable config "yt-dlp.conf": ['-i']
[debug] Encodings: locale cp65001, fs utf-8, pref cp65001, out utf-8, error utf-8, screen utf-8
- [debug] yt-dlp version 2022.07.18 [9d339c4] (win32_exe)
+ [debug] yt-dlp version 2022.10.04 [9d339c4] (win32_exe)
[debug] Python 3.8.10 (CPython 64bit) - Windows-10-10.0.22000-SP0
[debug] Checking exe version: ffmpeg -bsfs
[debug] Checking exe version: ffprobe -bsfs
@@ -71,8 +78,8 @@ body:
[debug] Optional libraries: Cryptodome-3.15.0, brotli-1.0.9, certifi-2022.06.15, mutagen-1.45.1, sqlite3-2.6.0, websockets-10.3
[debug] Proxy map: {}
[debug] Fetching release info: https://api.github.com/repos/yt-dlp/yt-dlp/releases/latest
- Latest version: 2022.07.18, Current version: 2022.07.18
- yt-dlp is up to date (2022.07.18)
+ Latest version: 2022.10.04, Current version: 2022.10.04
+ yt-dlp is up to date (2022.10.04)
render: shell
validations:
diff --git a/.github/ISSUE_TEMPLATE/4_bug_report.yml b/.github/ISSUE_TEMPLATE/4_bug_report.yml
index 412bb9757c..aa03087cf3 100644
--- a/.github/ISSUE_TEMPLATE/4_bug_report.yml
+++ b/.github/ISSUE_TEMPLATE/4_bug_report.yml
@@ -2,6 +2,13 @@ name: Bug report
description: Report a bug unrelated to any particular site or extractor
labels: [triage, bug]
body:
+ - type: checkboxes
+ attributes:
+ label: DO NOT REMOVE OR SKIP THE ISSUE TEMPLATE
+ description: Fill all fields even if you think it is irrelevant for the issue
+ options:
+ - label: I understand that I will be **blocked** if I remove or skip any mandatory\* field
+ required: true
- type: checkboxes
id: checklist
attributes:
@@ -11,11 +18,11 @@ body:
options:
- label: I'm reporting a bug unrelated to a specific site
required: true
- - label: I've verified that I'm running yt-dlp version **2022.07.18** ([update instructions](https://github.com/yt-dlp/yt-dlp#update)) or later (specify commit)
+ - label: I've verified that I'm running yt-dlp version **2022.10.04** ([update instructions](https://github.com/yt-dlp/yt-dlp#update)) or later (specify commit)
required: true
- label: I've checked that all provided URLs are playable in a browser with the same IP and same login details
required: true
- - label: I've checked that all URLs and arguments with special characters are [properly quoted or escaped](https://github.com/ytdl-org/youtube-dl#video-url-contains-an-ampersand-and-im-getting-some-strange-output-1-2839-or-v-is-not-recognized-as-an-internal-or-external-command)
+ - label: I've checked that all URLs and arguments with special characters are [properly quoted or escaped](https://github.com/yt-dlp/yt-dlp/wiki/FAQ#video-url-contains-an-ampersand--and-im-getting-some-strange-output-1-2839-or-v-is-not-recognized-as-an-internal-or-external-command)
required: true
- label: I've searched the [bugtracker](https://github.com/yt-dlp/yt-dlp/issues?q=) for similar issues **including closed ones**. DO NOT post duplicates
required: true
@@ -48,7 +55,7 @@ body:
[debug] Command-line config: ['-vU', 'test:youtube']
[debug] Portable config "yt-dlp.conf": ['-i']
[debug] Encodings: locale cp65001, fs utf-8, pref cp65001, out utf-8, error utf-8, screen utf-8
- [debug] yt-dlp version 2022.07.18 [9d339c4] (win32_exe)
+ [debug] yt-dlp version 2022.10.04 [9d339c4] (win32_exe)
[debug] Python 3.8.10 (CPython 64bit) - Windows-10-10.0.22000-SP0
[debug] Checking exe version: ffmpeg -bsfs
[debug] Checking exe version: ffprobe -bsfs
@@ -56,8 +63,8 @@ body:
[debug] Optional libraries: Cryptodome-3.15.0, brotli-1.0.9, certifi-2022.06.15, mutagen-1.45.1, sqlite3-2.6.0, websockets-10.3
[debug] Proxy map: {}
[debug] Fetching release info: https://api.github.com/repos/yt-dlp/yt-dlp/releases/latest
- Latest version: 2022.07.18, Current version: 2022.07.18
- yt-dlp is up to date (2022.07.18)
+ Latest version: 2022.10.04, Current version: 2022.10.04
+ yt-dlp is up to date (2022.10.04)
render: shell
validations:
diff --git a/.github/ISSUE_TEMPLATE/5_feature_request.yml b/.github/ISSUE_TEMPLATE/5_feature_request.yml
index c41ea85335..47f6644a4f 100644
--- a/.github/ISSUE_TEMPLATE/5_feature_request.yml
+++ b/.github/ISSUE_TEMPLATE/5_feature_request.yml
@@ -2,6 +2,13 @@ name: Feature request
description: Request a new functionality unrelated to any particular site or extractor
labels: [triage, enhancement]
body:
+ - type: checkboxes
+ attributes:
+ label: DO NOT REMOVE OR SKIP THE ISSUE TEMPLATE
+ description: Fill all fields even if you think it is irrelevant for the issue
+ options:
+ - label: I understand that I will be **blocked** if I remove or skip any mandatory\* field
+ required: true
- type: checkboxes
id: checklist
attributes:
@@ -13,7 +20,7 @@ body:
required: true
- label: I've looked through the [README](https://github.com/yt-dlp/yt-dlp#readme)
required: true
- - label: I've verified that I'm running yt-dlp version **2022.07.18** ([update instructions](https://github.com/yt-dlp/yt-dlp#update)) or later (specify commit)
+ - label: I've verified that I'm running yt-dlp version **2022.10.04** ([update instructions](https://github.com/yt-dlp/yt-dlp#update)) or later (specify commit)
required: true
- label: I've searched the [bugtracker](https://github.com/yt-dlp/yt-dlp/issues?q=) for similar issues **including closed ones**. DO NOT post duplicates
required: true
@@ -44,7 +51,7 @@ body:
[debug] Command-line config: ['-vU', 'test:youtube']
[debug] Portable config "yt-dlp.conf": ['-i']
[debug] Encodings: locale cp65001, fs utf-8, pref cp65001, out utf-8, error utf-8, screen utf-8
- [debug] yt-dlp version 2022.07.18 [9d339c4] (win32_exe)
+ [debug] yt-dlp version 2022.10.04 [9d339c4] (win32_exe)
[debug] Python 3.8.10 (CPython 64bit) - Windows-10-10.0.22000-SP0
[debug] Checking exe version: ffmpeg -bsfs
[debug] Checking exe version: ffprobe -bsfs
@@ -52,7 +59,7 @@ body:
[debug] Optional libraries: Cryptodome-3.15.0, brotli-1.0.9, certifi-2022.06.15, mutagen-1.45.1, sqlite3-2.6.0, websockets-10.3
[debug] Proxy map: {}
[debug] Fetching release info: https://api.github.com/repos/yt-dlp/yt-dlp/releases/latest
- Latest version: 2022.07.18, Current version: 2022.07.18
- yt-dlp is up to date (2022.07.18)
+ Latest version: 2022.10.04, Current version: 2022.10.04
+ yt-dlp is up to date (2022.10.04)
render: shell
diff --git a/.github/ISSUE_TEMPLATE/6_question.yml b/.github/ISSUE_TEMPLATE/6_question.yml
index edfa4c7a0d..996f90679f 100644
--- a/.github/ISSUE_TEMPLATE/6_question.yml
+++ b/.github/ISSUE_TEMPLATE/6_question.yml
@@ -2,12 +2,19 @@ name: Ask question
description: Ask yt-dlp related question
labels: [question]
body:
+ - type: checkboxes
+ attributes:
+ label: DO NOT REMOVE OR SKIP THE ISSUE TEMPLATE
+ description: Fill all fields even if you think it is irrelevant for the issue
+ options:
+ - label: I understand that I will be **blocked** if I remove or skip any mandatory\* field
+ required: true
- type: markdown
attributes:
value: |
### Make sure you are **only** asking a question and not reporting a bug or requesting a feature.
If your question contains "isn't working" or "can you add", this is most likely the wrong template.
- If you are in doubt whether this is the right template, **use another template**!
+ If you are in doubt whether this is the right template, **USE ANOTHER TEMPLATE**!
- type: checkboxes
id: checklist
attributes:
@@ -19,7 +26,7 @@ body:
required: true
- label: I've looked through the [README](https://github.com/yt-dlp/yt-dlp#readme)
required: true
- - label: I've verified that I'm running yt-dlp version **2022.07.18** ([update instructions](https://github.com/yt-dlp/yt-dlp#update)) or later (specify commit)
+ - label: I've verified that I'm running yt-dlp version **2022.10.04** ([update instructions](https://github.com/yt-dlp/yt-dlp#update)) or later (specify commit)
required: true
- label: I've searched the [bugtracker](https://github.com/yt-dlp/yt-dlp/issues?q=) for similar questions **including closed ones**. DO NOT post duplicates
required: true
@@ -50,7 +57,7 @@ body:
[debug] Command-line config: ['-vU', 'test:youtube']
[debug] Portable config "yt-dlp.conf": ['-i']
[debug] Encodings: locale cp65001, fs utf-8, pref cp65001, out utf-8, error utf-8, screen utf-8
- [debug] yt-dlp version 2022.07.18 [9d339c4] (win32_exe)
+ [debug] yt-dlp version 2022.10.04 [9d339c4] (win32_exe)
[debug] Python 3.8.10 (CPython 64bit) - Windows-10-10.0.22000-SP0
[debug] Checking exe version: ffmpeg -bsfs
[debug] Checking exe version: ffprobe -bsfs
@@ -58,7 +65,7 @@ body:
[debug] Optional libraries: Cryptodome-3.15.0, brotli-1.0.9, certifi-2022.06.15, mutagen-1.45.1, sqlite3-2.6.0, websockets-10.3
[debug] Proxy map: {}
[debug] Fetching release info: https://api.github.com/repos/yt-dlp/yt-dlp/releases/latest
- Latest version: 2022.07.18, Current version: 2022.07.18
- yt-dlp is up to date (2022.07.18)
+ Latest version: 2022.10.04, Current version: 2022.10.04
+ yt-dlp is up to date (2022.10.04)
render: shell
diff --git a/.github/ISSUE_TEMPLATE_tmpl/1_broken_site.yml b/.github/ISSUE_TEMPLATE_tmpl/1_broken_site.yml
index 35fae2be61..e1b1e51380 100644
--- a/.github/ISSUE_TEMPLATE_tmpl/1_broken_site.yml
+++ b/.github/ISSUE_TEMPLATE_tmpl/1_broken_site.yml
@@ -2,6 +2,7 @@ name: Broken site
description: Report broken or misfunctioning site
labels: [triage, site-bug]
body:
+ %(no_skip)s
- type: checkboxes
id: checklist
attributes:
@@ -15,7 +16,7 @@ body:
required: true
- label: I've checked that all provided URLs are playable in a browser with the same IP and same login details
required: true
- - label: I've checked that all URLs and arguments with special characters are [properly quoted or escaped](https://github.com/ytdl-org/youtube-dl#video-url-contains-an-ampersand-and-im-getting-some-strange-output-1-2839-or-v-is-not-recognized-as-an-internal-or-external-command)
+ - label: I've checked that all URLs and arguments with special characters are [properly quoted or escaped](https://github.com/yt-dlp/yt-dlp/wiki/FAQ#video-url-contains-an-ampersand--and-im-getting-some-strange-output-1-2839-or-v-is-not-recognized-as-an-internal-or-external-command)
required: true
- label: I've searched the [bugtracker](https://github.com/yt-dlp/yt-dlp/issues?q=) for similar issues **including closed ones**. DO NOT post duplicates
required: true
diff --git a/.github/ISSUE_TEMPLATE_tmpl/2_site_support_request.yml b/.github/ISSUE_TEMPLATE_tmpl/2_site_support_request.yml
index 02125f77df..12a1c65987 100644
--- a/.github/ISSUE_TEMPLATE_tmpl/2_site_support_request.yml
+++ b/.github/ISSUE_TEMPLATE_tmpl/2_site_support_request.yml
@@ -2,6 +2,7 @@ name: Site support request
description: Request support for a new site
labels: [triage, site-request]
body:
+ %(no_skip)s
- type: checkboxes
id: checklist
attributes:
@@ -15,7 +16,7 @@ body:
required: true
- label: I've checked that all provided URLs are playable in a browser with the same IP and same login details
required: true
- - label: I've checked that none of provided URLs [violate any copyrights](https://github.com/ytdl-org/youtube-dl#can-you-add-support-for-this-anime-video-site-or-site-which-shows-current-movies-for-free) or contain any [DRM](https://en.wikipedia.org/wiki/Digital_rights_management) to the best of my knowledge
+ - label: I've checked that none of provided URLs [violate any copyrights](https://github.com/yt-dlp/yt-dlp/blob/master/CONTRIBUTING.md#is-the-website-primarily-used-for-piracy) or contain any [DRM](https://en.wikipedia.org/wiki/Digital_rights_management) to the best of my knowledge
required: true
- label: I've searched the [bugtracker](https://github.com/yt-dlp/yt-dlp/issues?q=) for similar issues **including closed ones**. DO NOT post duplicates
required: true
diff --git a/.github/ISSUE_TEMPLATE_tmpl/3_site_feature_request.yml b/.github/ISSUE_TEMPLATE_tmpl/3_site_feature_request.yml
index 154d4e35f6..2b46650f70 100644
--- a/.github/ISSUE_TEMPLATE_tmpl/3_site_feature_request.yml
+++ b/.github/ISSUE_TEMPLATE_tmpl/3_site_feature_request.yml
@@ -2,6 +2,7 @@ name: Site feature request
description: Request a new functionality for a supported site
labels: [triage, site-enhancement]
body:
+ %(no_skip)s
- type: checkboxes
id: checklist
attributes:
diff --git a/.github/ISSUE_TEMPLATE_tmpl/4_bug_report.yml b/.github/ISSUE_TEMPLATE_tmpl/4_bug_report.yml
index 650ef208e8..377efbe338 100644
--- a/.github/ISSUE_TEMPLATE_tmpl/4_bug_report.yml
+++ b/.github/ISSUE_TEMPLATE_tmpl/4_bug_report.yml
@@ -2,6 +2,7 @@ name: Bug report
description: Report a bug unrelated to any particular site or extractor
labels: [triage, bug]
body:
+ %(no_skip)s
- type: checkboxes
id: checklist
attributes:
@@ -15,7 +16,7 @@ body:
required: true
- label: I've checked that all provided URLs are playable in a browser with the same IP and same login details
required: true
- - label: I've checked that all URLs and arguments with special characters are [properly quoted or escaped](https://github.com/ytdl-org/youtube-dl#video-url-contains-an-ampersand-and-im-getting-some-strange-output-1-2839-or-v-is-not-recognized-as-an-internal-or-external-command)
+ - label: I've checked that all URLs and arguments with special characters are [properly quoted or escaped](https://github.com/yt-dlp/yt-dlp/wiki/FAQ#video-url-contains-an-ampersand--and-im-getting-some-strange-output-1-2839-or-v-is-not-recognized-as-an-internal-or-external-command)
required: true
- label: I've searched the [bugtracker](https://github.com/yt-dlp/yt-dlp/issues?q=) for similar issues **including closed ones**. DO NOT post duplicates
required: true
diff --git a/.github/ISSUE_TEMPLATE_tmpl/5_feature_request.yml b/.github/ISSUE_TEMPLATE_tmpl/5_feature_request.yml
index 6c0ecf386d..8bbc5d733f 100644
--- a/.github/ISSUE_TEMPLATE_tmpl/5_feature_request.yml
+++ b/.github/ISSUE_TEMPLATE_tmpl/5_feature_request.yml
@@ -2,6 +2,7 @@ name: Feature request
description: Request a new functionality unrelated to any particular site or extractor
labels: [triage, enhancement]
body:
+ %(no_skip)s
- type: checkboxes
id: checklist
attributes:
diff --git a/.github/ISSUE_TEMPLATE_tmpl/6_question.yml b/.github/ISSUE_TEMPLATE_tmpl/6_question.yml
index 1df4d41db9..ee09e82a38 100644
--- a/.github/ISSUE_TEMPLATE_tmpl/6_question.yml
+++ b/.github/ISSUE_TEMPLATE_tmpl/6_question.yml
@@ -2,12 +2,13 @@ name: Ask question
description: Ask yt-dlp related question
labels: [question]
body:
+ %(no_skip)s
- type: markdown
attributes:
value: |
### Make sure you are **only** asking a question and not reporting a bug or requesting a feature.
If your question contains "isn't working" or "can you add", this is most likely the wrong template.
- If you are in doubt whether this is the right template, **use another template**!
+ If you are in doubt whether this is the right template, **USE ANOTHER TEMPLATE**!
- type: checkboxes
id: checklist
attributes:
diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md
index ec95903d65..5abc6ce41e 100644
--- a/.github/PULL_REQUEST_TEMPLATE.md
+++ b/.github/PULL_REQUEST_TEMPLATE.md
@@ -1,3 +1,5 @@
+**IMPORTANT**: PRs without the template will be CLOSED
+
### Description of your *pull request* and other information
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index 4c87f38ebd..2b4e2f46b1 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -2,18 +2,17 @@ name: Build
on: workflow_dispatch
jobs:
- create_release:
+ prepare:
runs-on: ubuntu-latest
outputs:
version_suffix: ${{ steps.version_suffix.outputs.version_suffix }}
ytdlp_version: ${{ steps.bump_version.outputs.ytdlp_version }}
- upload_url: ${{ steps.create_release.outputs.upload_url }}
- release_id: ${{ steps.create_release.outputs.id }}
+ head_sha: ${{ steps.push_release.outputs.head_sha }}
steps:
- - uses: actions/checkout@v2
+ - uses: actions/checkout@v3
with:
fetch-depth: 0
- - uses: actions/setup-python@v2
+ - uses: actions/setup-python@v4
with:
python-version: '3.10'
@@ -43,53 +42,15 @@ jobs:
PUSH_VERSION_COMMIT: ${{ secrets.PUSH_VERSION_COMMIT }}
if: "env.PUSH_VERSION_COMMIT != ''"
run: git push origin ${{ github.event.ref }}
- - name: Get Changelog
- run: |
- changelog=$(grep -oPz '(?s)(?<=### ${{ steps.bump_version.outputs.ytdlp_version }}\n{2}).+?(?=\n{2,3}###)' Changelog.md) || true
- echo "changelog<> $GITHUB_ENV
- echo "$changelog" >> $GITHUB_ENV
- echo "EOF" >> $GITHUB_ENV
-
- - name: Create Release
- id: create_release
- uses: actions/create-release@v1
- env:
- GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- with:
- tag_name: ${{ steps.bump_version.outputs.ytdlp_version }}
- release_name: yt-dlp ${{ steps.bump_version.outputs.ytdlp_version }}
- commitish: ${{ steps.push_release.outputs.head_sha }}
- draft: true
- prerelease: false
- body: |
- #### [A description of the various files]((https://github.com/yt-dlp/yt-dlp#release-files)) are in the README
-
- ---
- Changelog
-
-
- ${{ env.changelog }}
-
-
-
build_unix:
- needs: create_release
+ needs: prepare
runs-on: ubuntu-18.04 # Standalone executable should be built on minimum supported OS
- outputs:
- sha256_bin: ${{ steps.get_sha.outputs.sha256_bin }}
- sha512_bin: ${{ steps.get_sha.outputs.sha512_bin }}
- sha256_tar: ${{ steps.get_sha.outputs.sha256_tar }}
- sha512_tar: ${{ steps.get_sha.outputs.sha512_tar }}
- sha256_linux: ${{ steps.get_sha.outputs.sha256_linux }}
- sha512_linux: ${{ steps.get_sha.outputs.sha512_linux }}
- sha256_linux_zip: ${{ steps.get_sha.outputs.sha256_linux_zip }}
- sha512_linux_zip: ${{ steps.get_sha.outputs.sha512_linux_zip }}
steps:
- - uses: actions/checkout@v2
- - uses: actions/setup-python@v2
+ - uses: actions/checkout@v3
+ - uses: actions/setup-python@v4
with:
python-version: '3.10'
- name: Install Requirements
@@ -100,7 +61,7 @@ jobs:
- name: Prepare
run: |
- python devscripts/update-version.py ${{ needs.create_release.outputs.version_suffix }}
+ python devscripts/update-version.py ${{ needs.prepare.outputs.version_suffix }}
python devscripts/make_lazy_extractors.py
- name: Build Unix executables
run: |
@@ -111,51 +72,15 @@ jobs:
- name: Get SHA2-SUMS
id: get_sha
run: |
- echo "::set-output name=sha256_bin::$(sha256sum yt-dlp | awk '{print $1}')"
- echo "::set-output name=sha512_bin::$(sha512sum yt-dlp | awk '{print $1}')"
- echo "::set-output name=sha256_tar::$(sha256sum yt-dlp.tar.gz | awk '{print $1}')"
- echo "::set-output name=sha512_tar::$(sha512sum yt-dlp.tar.gz | awk '{print $1}')"
- echo "::set-output name=sha256_linux::$(sha256sum dist/yt-dlp_linux | awk '{print $1}')"
- echo "::set-output name=sha512_linux::$(sha512sum dist/yt-dlp_linux | awk '{print $1}')"
- echo "::set-output name=sha256_linux_zip::$(sha256sum dist/yt-dlp_linux.zip | awk '{print $1}')"
- echo "::set-output name=sha512_linux_zip::$(sha512sum dist/yt-dlp_linux.zip | awk '{print $1}')"
- - name: Upload zip binary
- uses: actions/upload-release-asset@v1
- env:
- GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ - name: Upload artifacts
+ uses: actions/upload-artifact@v3
with:
- upload_url: ${{ needs.create_release.outputs.upload_url }}
- asset_path: ./yt-dlp
- asset_name: yt-dlp
- asset_content_type: application/octet-stream
- - name: Upload Source tar
- uses: actions/upload-release-asset@v1
- env:
- GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- with:
- upload_url: ${{ needs.create_release.outputs.upload_url }}
- asset_path: ./yt-dlp.tar.gz
- asset_name: yt-dlp.tar.gz
- asset_content_type: application/gzip
- - name: Upload standalone binary
- uses: actions/upload-release-asset@v1
- env:
- GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- with:
- upload_url: ${{ needs.create_release.outputs.upload_url }}
- asset_path: ./dist/yt-dlp_linux
- asset_name: yt-dlp_linux
- asset_content_type: application/octet-stream
- - name: Upload onedir binary
- uses: actions/upload-release-asset@v1
- env:
- GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- with:
- upload_url: ${{ needs.create_release.outputs.upload_url }}
- asset_path: ./dist/yt-dlp_linux.zip
- asset_name: yt-dlp_linux.zip
- asset_content_type: application/zip
+ path: |
+ yt-dlp
+ yt-dlp.tar.gz
+ dist/yt-dlp_linux
+ dist/yt-dlp_linux.zip
- name: Build and publish on PyPi
env:
@@ -164,6 +89,7 @@ jobs:
if: "env.TWINE_PASSWORD != ''"
run: |
rm -rf dist/*
+ 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
twine upload dist/*
@@ -180,24 +106,19 @@ jobs:
if: "env.BREW_TOKEN != ''"
run: |
git clone git@github.com:yt-dlp/homebrew-taps taps/
- python devscripts/update-formulae.py taps/Formula/yt-dlp.rb "${{ needs.create_release.outputs.ytdlp_version }}"
+ python devscripts/update-formulae.py taps/Formula/yt-dlp.rb "${{ needs.prepare.outputs.ytdlp_version }}"
git -C taps/ config user.name github-actions
git -C taps/ config user.email github-actions@example.com
- git -C taps/ commit -am 'yt-dlp: ${{ needs.create_release.outputs.ytdlp_version }}'
+ git -C taps/ commit -am 'yt-dlp: ${{ needs.prepare.outputs.ytdlp_version }}'
git -C taps/ push
build_macos:
runs-on: macos-11
- needs: create_release
- outputs:
- sha256_macos: ${{ steps.get_sha.outputs.sha256_macos }}
- sha512_macos: ${{ steps.get_sha.outputs.sha512_macos }}
- sha256_macos_zip: ${{ steps.get_sha.outputs.sha256_macos_zip }}
- sha512_macos_zip: ${{ steps.get_sha.outputs.sha512_macos_zip }}
+ needs: prepare
steps:
- - uses: actions/checkout@v2
+ - uses: actions/checkout@v3
# NB: In order to create a universal2 application, the version of python3 in /usr/bin has to be used
- name: Install Requirements
run: |
@@ -206,50 +127,28 @@ jobs:
- name: Prepare
run: |
- /usr/bin/python3 devscripts/update-version.py ${{ needs.create_release.outputs.version_suffix }}
+ /usr/bin/python3 devscripts/update-version.py ${{ needs.prepare.outputs.version_suffix }}
/usr/bin/python3 devscripts/make_lazy_extractors.py
- name: Build
run: |
/usr/bin/python3 pyinst.py --target-architecture universal2 --onedir
(cd ./dist/yt-dlp_macos && zip -r ../yt-dlp_macos.zip .)
/usr/bin/python3 pyinst.py --target-architecture universal2
- - name: Get SHA2-SUMS
- id: get_sha
- run: |
- echo "::set-output name=sha256_macos::$(sha256sum dist/yt-dlp_macos | awk '{print $1}')"
- echo "::set-output name=sha512_macos::$(sha512sum dist/yt-dlp_macos | awk '{print $1}')"
- echo "::set-output name=sha256_macos_zip::$(sha256sum dist/yt-dlp_macos.zip | awk '{print $1}')"
- echo "::set-output name=sha512_macos_zip::$(sha512sum dist/yt-dlp_macos.zip | awk '{print $1}')"
- - name: Upload standalone binary
- uses: actions/upload-release-asset@v1
- env:
- GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ - name: Upload artifacts
+ uses: actions/upload-artifact@v3
with:
- upload_url: ${{ needs.create_release.outputs.upload_url }}
- asset_path: ./dist/yt-dlp_macos
- asset_name: yt-dlp_macos
- asset_content_type: application/octet-stream
- - name: Upload onedir binary
- uses: actions/upload-release-asset@v1
- env:
- GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- with:
- upload_url: ${{ needs.create_release.outputs.upload_url }}
- asset_path: ./dist/yt-dlp_macos.zip
- asset_name: yt-dlp_macos.zip
- asset_content_type: application/zip
+ path: |
+ dist/yt-dlp_macos
+ dist/yt-dlp_macos.zip
build_macos_legacy:
runs-on: macos-latest
- needs: create_release
- outputs:
- sha256_macos_legacy: ${{ steps.get_sha.outputs.sha256_macos_legacy }}
- sha512_macos_legacy: ${{ steps.get_sha.outputs.sha512_macos_legacy }}
+ needs: prepare
steps:
- - uses: actions/checkout@v2
+ - uses: actions/checkout@v3
- name: Install Python
# We need the official Python, because the GA ones only support newer macOS versions
env:
@@ -269,52 +168,37 @@ jobs:
- name: Prepare
run: |
- python3 devscripts/update-version.py ${{ needs.create_release.outputs.version_suffix }}
+ python3 devscripts/update-version.py ${{ needs.prepare.outputs.version_suffix }}
python3 devscripts/make_lazy_extractors.py
- name: Build
run: |
python3 pyinst.py
- - name: Get SHA2-SUMS
- id: get_sha
- run: |
- echo "::set-output name=sha256_macos_legacy::$(sha256sum dist/yt-dlp_macos | awk '{print $1}')"
- echo "::set-output name=sha512_macos_legacy::$(sha512sum dist/yt-dlp_macos | awk '{print $1}')"
+ mv dist/yt-dlp_macos dist/yt-dlp_macos_legacy
- - name: Upload standalone binary
- uses: actions/upload-release-asset@v1
- env:
- GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ - name: Upload artifacts
+ uses: actions/upload-artifact@v3
with:
- upload_url: ${{ needs.create_release.outputs.upload_url }}
- asset_path: ./dist/yt-dlp_macos
- asset_name: yt-dlp_macos_legacy
- asset_content_type: application/octet-stream
+ path: |
+ dist/yt-dlp_macos_legacy
build_windows:
runs-on: windows-latest
- needs: create_release
- outputs:
- sha256_win: ${{ steps.get_sha.outputs.sha256_win }}
- sha512_win: ${{ steps.get_sha.outputs.sha512_win }}
- sha256_py2exe: ${{ steps.get_sha.outputs.sha256_py2exe }}
- sha512_py2exe: ${{ steps.get_sha.outputs.sha512_py2exe }}
- sha256_win_zip: ${{ steps.get_sha.outputs.sha256_win_zip }}
- sha512_win_zip: ${{ steps.get_sha.outputs.sha512_win_zip }}
+ needs: prepare
steps:
- - uses: actions/checkout@v2
- - uses: actions/setup-python@v2
+ - uses: actions/checkout@v3
+ - uses: actions/setup-python@v4
with: # 3.8 is used for Win7 support
python-version: '3.8'
- name: Install Requirements
run: | # Custom pyinstaller built with https://github.com/yt-dlp/pyinstaller-builds
- python -m pip install --upgrade pip setuptools wheel py2exe
- pip install "https://yt-dlp.github.io/Pyinstaller-Builds/x86_64/pyinstaller-5.2-py3-none-any.whl" -r requirements.txt
+ python -m pip install --upgrade pip setuptools wheel "py2exe<0.12"
+ pip install "https://yt-dlp.github.io/Pyinstaller-Builds/x86_64/pyinstaller-5.3-py3-none-any.whl" -r requirements.txt
- name: Prepare
run: |
- python devscripts/update-version.py ${{ needs.create_release.outputs.version_suffix }}
+ python devscripts/update-version.py ${{ needs.prepare.outputs.version_suffix }}
python devscripts/make_lazy_extractors.py
- name: Build
run: |
@@ -323,154 +207,118 @@ jobs:
python pyinst.py
python pyinst.py --onedir
Compress-Archive -Path ./dist/yt-dlp/* -DestinationPath ./dist/yt-dlp_win.zip
- - name: Get SHA2-SUMS
- id: get_sha
- run: |
- echo "::set-output name=sha256_py2exe::$((Get-FileHash dist\yt-dlp_min.exe -Algorithm SHA256).Hash.ToLower())"
- echo "::set-output name=sha512_py2exe::$((Get-FileHash dist\yt-dlp_min.exe -Algorithm SHA512).Hash.ToLower())"
- echo "::set-output name=sha256_win::$((Get-FileHash dist\yt-dlp.exe -Algorithm SHA256).Hash.ToLower())"
- echo "::set-output name=sha512_win::$((Get-FileHash dist\yt-dlp.exe -Algorithm SHA512).Hash.ToLower())"
- echo "::set-output name=sha256_win_zip::$((Get-FileHash dist\yt-dlp_win.zip -Algorithm SHA256).Hash.ToLower())"
- echo "::set-output name=sha512_win_zip::$((Get-FileHash dist\yt-dlp_win.zip -Algorithm SHA512).Hash.ToLower())"
- - name: Upload py2exe binary
- uses: actions/upload-release-asset@v1
- env:
- GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ - name: Upload artifacts
+ uses: actions/upload-artifact@v3
with:
- upload_url: ${{ needs.create_release.outputs.upload_url }}
- asset_path: ./dist/yt-dlp_min.exe
- asset_name: yt-dlp_min.exe
- asset_content_type: application/vnd.microsoft.portable-executable
- - name: Upload standalone binary
- uses: actions/upload-release-asset@v1
- env:
- GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- with:
- upload_url: ${{ needs.create_release.outputs.upload_url }}
- asset_path: ./dist/yt-dlp.exe
- asset_name: yt-dlp.exe
- asset_content_type: application/vnd.microsoft.portable-executable
- - name: Upload onedir binary
- uses: actions/upload-release-asset@v1
- env:
- GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- with:
- upload_url: ${{ needs.create_release.outputs.upload_url }}
- asset_path: ./dist/yt-dlp_win.zip
- asset_name: yt-dlp_win.zip
- asset_content_type: application/zip
+ path: |
+ dist/yt-dlp.exe
+ dist/yt-dlp_min.exe
+ dist/yt-dlp_win.zip
build_windows32:
runs-on: windows-latest
- needs: create_release
- outputs:
- sha256_win32: ${{ steps.get_sha.outputs.sha256_win32 }}
- sha512_win32: ${{ steps.get_sha.outputs.sha512_win32 }}
+ needs: prepare
steps:
- - uses: actions/checkout@v2
- - uses: actions/setup-python@v2
+ - uses: actions/checkout@v3
+ - uses: actions/setup-python@v4
with: # 3.7 is used for Vista support. See https://github.com/yt-dlp/yt-dlp/issues/390
python-version: '3.7'
architecture: 'x86'
- name: Install Requirements
run: |
python -m pip install --upgrade pip setuptools wheel
- pip install "https://yt-dlp.github.io/Pyinstaller-Builds/i686/pyinstaller-5.2-py3-none-any.whl" -r requirements.txt
+ pip install "https://yt-dlp.github.io/Pyinstaller-Builds/i686/pyinstaller-5.3-py3-none-any.whl" -r requirements.txt
- name: Prepare
run: |
- python devscripts/update-version.py ${{ needs.create_release.outputs.version_suffix }}
+ python devscripts/update-version.py ${{ needs.prepare.outputs.version_suffix }}
python devscripts/make_lazy_extractors.py
- name: Build
run: |
python pyinst.py
- - name: Get SHA2-SUMS
- id: get_sha
- run: |
- echo "::set-output name=sha256_win32::$((Get-FileHash dist\yt-dlp_x86.exe -Algorithm SHA256).Hash.ToLower())"
- echo "::set-output name=sha512_win32::$((Get-FileHash dist\yt-dlp_x86.exe -Algorithm SHA512).Hash.ToLower())"
- - name: Upload standalone binary
- uses: actions/upload-release-asset@v1
- env:
- GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ - name: Upload artifacts
+ uses: actions/upload-artifact@v3
with:
- upload_url: ${{ needs.create_release.outputs.upload_url }}
- asset_path: ./dist/yt-dlp_x86.exe
- asset_name: yt-dlp_x86.exe
- asset_content_type: application/vnd.microsoft.portable-executable
+ path: |
+ dist/yt-dlp_x86.exe
- finish:
+ publish_release:
runs-on: ubuntu-latest
- needs: [create_release, build_unix, build_windows, build_windows32, build_macos, build_macos_legacy]
+ needs: [prepare, build_unix, build_windows, build_windows32, build_macos, build_macos_legacy]
steps:
- - name: Make SHA2-SUMS files
+ - uses: actions/checkout@v3
+ - uses: actions/download-artifact@v3
+
+ - name: Get Changelog
run: |
- echo "${{ needs.build_unix.outputs.sha256_bin }} yt-dlp" >> SHA2-256SUMS
- echo "${{ needs.build_unix.outputs.sha256_tar }} yt-dlp.tar.gz" >> SHA2-256SUMS
- echo "${{ needs.build_unix.outputs.sha256_linux }} yt-dlp_linux" >> SHA2-256SUMS
- echo "${{ needs.build_unix.outputs.sha256_linux_zip }} yt-dlp_linux.zip" >> SHA2-256SUMS
- echo "${{ needs.build_windows.outputs.sha256_win }} yt-dlp.exe" >> SHA2-256SUMS
- echo "${{ needs.build_windows.outputs.sha256_py2exe }} yt-dlp_min.exe" >> SHA2-256SUMS
- echo "${{ needs.build_windows32.outputs.sha256_win32 }} yt-dlp_x86.exe" >> SHA2-256SUMS
- echo "${{ needs.build_windows.outputs.sha256_win_zip }} yt-dlp_win.zip" >> SHA2-256SUMS
- echo "${{ needs.build_macos.outputs.sha256_macos }} yt-dlp_macos" >> SHA2-256SUMS
- echo "${{ needs.build_macos.outputs.sha256_macos_zip }} yt-dlp_macos.zip" >> SHA2-256SUMS
- echo "${{ needs.build_macos_legacy.outputs.sha256_macos_legacy }} yt-dlp_macos_legacy" >> SHA2-256SUMS
- echo "${{ needs.build_unix.outputs.sha512_bin }} yt-dlp" >> SHA2-512SUMS
- echo "${{ needs.build_unix.outputs.sha512_tar }} yt-dlp.tar.gz" >> SHA2-512SUMS
- echo "${{ needs.build_unix.outputs.sha512_linux }} yt-dlp_linux" >> SHA2-512SUMS
- echo "${{ needs.build_unix.outputs.sha512_linux_zip }} yt-dlp_linux.zip" >> SHA2-512SUMS
- echo "${{ needs.build_windows.outputs.sha512_win }} yt-dlp.exe" >> SHA2-512SUMS
- echo "${{ needs.build_windows.outputs.sha512_py2exe }} yt-dlp_min.exe" >> SHA2-512SUMS
- echo "${{ needs.build_windows32.outputs.sha512_win32 }} yt-dlp_x86.exe" >> SHA2-512SUMS
- echo "${{ needs.build_windows.outputs.sha512_win_zip }} yt-dlp_win.zip" >> SHA2-512SUMS
- echo "${{ needs.build_macos.outputs.sha512_macos }} yt-dlp_macos" >> SHA2-512SUMS
- echo "${{ needs.build_macos.outputs.sha512_macos_zip }} yt-dlp_macos.zip" >> SHA2-512SUMS
- echo "${{ needs.build_macos_legacy.outputs.sha512_macos_legacy }} yt-dlp_macos_legacy" >> SHA2-512SUMS
-
- - name: Upload SHA2-256SUMS file
- uses: actions/upload-release-asset@v1
- env:
- GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- with:
- upload_url: ${{ needs.create_release.outputs.upload_url }}
- asset_path: ./SHA2-256SUMS
- asset_name: SHA2-256SUMS
- asset_content_type: text/plain
- - name: Upload SHA2-512SUMS file
- uses: actions/upload-release-asset@v1
- env:
- GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- with:
- upload_url: ${{ needs.create_release.outputs.upload_url }}
- asset_path: ./SHA2-512SUMS
- asset_name: SHA2-512SUMS
- asset_content_type: text/plain
-
+ changelog=$(grep -oPz '(?s)(?<=### ${{ needs.prepare.outputs.ytdlp_version }}\n{2}).+?(?=\n{2,3}###)' Changelog.md) || true
+ echo "changelog<> $GITHUB_ENV
+ echo "$changelog" >> $GITHUB_ENV
+ echo "EOF" >> $GITHUB_ENV
- name: Make Update spec
run: |
echo "# This file is used for regulating self-update" >> _update_spec
echo "lock 2022.07.18 .+ Python 3.6" >> _update_spec
- - name: Upload update spec
- uses: actions/upload-release-asset@v1
- env:
- GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- with:
- upload_url: ${{ needs.create_release.outputs.upload_url }}
- asset_path: ./_update_spec
- asset_name: _update_spec
- asset_content_type: text/plain
-
- - name: Finalize release
- env:
- GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ - name: Make SHA2-SUMS files
run: |
- gh api -X PATCH -H "Accept: application/vnd.github.v3+json" \
- /repos/${{ github.repository }}/releases/${{ needs.create_release.outputs.release_id }} \
- -F draft=false
+ sha256sum artifact/yt-dlp | awk '{print $1 " yt-dlp"}' >> SHA2-256SUMS
+ sha256sum artifact/yt-dlp.tar.gz | awk '{print $1 " yt-dlp.tar.gz"}' >> SHA2-256SUMS
+ sha256sum artifact/yt-dlp.exe | awk '{print $1 " yt-dlp.exe"}' >> SHA2-256SUMS
+ sha256sum artifact/yt-dlp_win.zip | awk '{print $1 " yt-dlp_win.zip"}' >> SHA2-256SUMS
+ sha256sum artifact/yt-dlp_min.exe | awk '{print $1 " yt-dlp_min.exe"}' >> SHA2-256SUMS
+ sha256sum artifact/yt-dlp_x86.exe | awk '{print $1 " yt-dlp_x86.exe"}' >> SHA2-256SUMS
+ sha256sum artifact/yt-dlp_macos | awk '{print $1 " yt-dlp_macos"}' >> SHA2-256SUMS
+ sha256sum artifact/yt-dlp_macos.zip | awk '{print $1 " yt-dlp_macos.zip"}' >> SHA2-256SUMS
+ sha256sum artifact/yt-dlp_macos_legacy | awk '{print $1 " yt-dlp_macos_legacy"}' >> SHA2-256SUMS
+ sha256sum artifact/dist/yt-dlp_linux | awk '{print $1 " yt-dlp_linux"}' >> SHA2-256SUMS
+ sha256sum artifact/dist/yt-dlp_linux.zip | awk '{print $1 " yt-dlp_linux.zip"}' >> SHA2-256SUMS
+ sha512sum artifact/yt-dlp | awk '{print $1 " yt-dlp"}' >> SHA2-512SUMS
+ sha512sum artifact/yt-dlp.tar.gz | awk '{print $1 " yt-dlp.tar.gz"}' >> SHA2-512SUMS
+ sha512sum artifact/yt-dlp.exe | awk '{print $1 " yt-dlp.exe"}' >> SHA2-512SUMS
+ sha512sum artifact/yt-dlp_win.zip | awk '{print $1 " yt-dlp_win.zip"}' >> SHA2-512SUMS
+ sha512sum artifact/yt-dlp_min.exe | awk '{print $1 " yt-dlp_min.exe"}' >> SHA2-512SUMS
+ sha512sum artifact/yt-dlp_x86.exe | awk '{print $1 " yt-dlp_x86.exe"}' >> SHA2-512SUMS
+ sha512sum artifact/yt-dlp_macos | awk '{print $1 " yt-dlp_macos"}' >> SHA2-512SUMS
+ sha512sum artifact/yt-dlp_macos.zip | awk '{print $1 " yt-dlp_macos.zip"}' >> SHA2-512SUMS
+ sha512sum artifact/yt-dlp_macos_legacy | awk '{print $1 " yt-dlp_macos_legacy"}' >> SHA2-512SUMS
+ sha512sum artifact/dist/yt-dlp_linux | awk '{print $1 " yt-dlp_linux"}' >> SHA2-512SUMS
+ sha512sum artifact/dist/yt-dlp_linux.zip | awk '{print $1 " yt-dlp_linux.zip"}' >> SHA2-512SUMS
+
+ - name: Publish Release
+ uses: yt-dlp/action-gh-release@v1
+ with:
+ tag_name: ${{ needs.prepare.outputs.ytdlp_version }}
+ name: yt-dlp ${{ needs.prepare.outputs.ytdlp_version }}
+ target_commitish: ${{ needs.prepare.outputs.head_sha }}
+ body: |
+ #### [A description of the various files]((https://github.com/yt-dlp/yt-dlp#release-files)) are in the README
+
+ ---
+ Changelog
+
+
+ ${{ env.changelog }}
+
+
+
+ files: |
+ SHA2-256SUMS
+ SHA2-512SUMS
+ artifact/yt-dlp
+ artifact/yt-dlp.tar.gz
+ artifact/yt-dlp.exe
+ artifact/yt-dlp_win.zip
+ artifact/yt-dlp_min.exe
+ artifact/yt-dlp_x86.exe
+ artifact/yt-dlp_macos
+ artifact/yt-dlp_macos.zip
+ artifact/yt-dlp_macos_legacy
+ artifact/dist/yt-dlp_linux
+ artifact/dist/yt-dlp_linux.zip
+ _update_spec
diff --git a/.github/workflows/core.yml b/.github/workflows/core.yml
index a60e002d9e..d0e890b30e 100644
--- a/.github/workflows/core.yml
+++ b/.github/workflows/core.yml
@@ -21,9 +21,9 @@ jobs:
python-version: pypy-3.9
run-tests-ext: bat
steps:
- - uses: actions/checkout@v2
+ - uses: actions/checkout@v3
- name: Set up Python ${{ matrix.python-version }}
- uses: actions/setup-python@v2
+ uses: actions/setup-python@v4
with:
python-version: ${{ matrix.python-version }}
- name: Install pytest
diff --git a/.github/workflows/download.yml b/.github/workflows/download.yml
index e8eb1fd12e..cc2da62fae 100644
--- a/.github/workflows/download.yml
+++ b/.github/workflows/download.yml
@@ -6,9 +6,9 @@ jobs:
if: "contains(github.event.head_commit.message, 'ci run dl')"
runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@v2
+ - uses: actions/checkout@v3
- name: Set up Python
- uses: actions/setup-python@v2
+ uses: actions/setup-python@v4
with:
python-version: 3.9
- name: Install test requirements
@@ -36,9 +36,9 @@ jobs:
python-version: pypy-3.9
run-tests-ext: bat
steps:
- - uses: actions/checkout@v2
+ - uses: actions/checkout@v3
- name: Set up Python ${{ matrix.python-version }}
- uses: actions/setup-python@v2
+ uses: actions/setup-python@v4
with:
python-version: ${{ matrix.python-version }}
- name: Install pytest
diff --git a/.github/workflows/quick-test.yml b/.github/workflows/quick-test.yml
index d8e14f4705..53b74e2c75 100644
--- a/.github/workflows/quick-test.yml
+++ b/.github/workflows/quick-test.yml
@@ -6,9 +6,9 @@ jobs:
if: "!contains(github.event.head_commit.message, 'ci skip all')"
runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@v2
+ - uses: actions/checkout@v3
- name: Set up Python
- uses: actions/setup-python@v2
+ uses: actions/setup-python@v4
with:
python-version: 3.9
- name: Install test requirements
@@ -20,9 +20,9 @@ jobs:
if: "!contains(github.event.head_commit.message, 'ci skip all')"
runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@v2
+ - uses: actions/checkout@v3
- name: Set up Python
- uses: actions/setup-python@v2
+ uses: actions/setup-python@v4
with:
python-version: 3.9
- name: Install flake8
diff --git a/.gitignore b/.gitignore
index 2e84762bcd..0ce059b34d 100644
--- a/.gitignore
+++ b/.gitignore
@@ -33,13 +33,14 @@ cookies
*.jpeg
*.jpg
*.m4a
-*.mpga
*.m4v
*.mhtml
*.mkv
*.mov
*.mp3
*.mp4
+*.mpga
+*.oga
*.ogg
*.opus
*.png
@@ -47,6 +48,7 @@ cookies
*.srt
*.swf
*.swp
+*.tt
*.ttml
*.url
*.vtt
@@ -85,6 +87,7 @@ updates_key.pem
.tox
*.class
*.isorted
+*.stackdump
# Generated
AUTHORS
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 6d9546033c..a8ac671dcf 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -161,7 +161,7 @@ The same applies for changes to the documentation, code style, or overarching ch
## Adding support for a new site
-If you want to add support for a new site, first of all **make sure** this site is **not dedicated to [copyright infringement](https://www.github.com/ytdl-org/youtube-dl#can-you-add-support-for-this-anime-video-site-or-site-which-shows-current-movies-for-free)**. yt-dlp does **not support** such sites thus pull requests adding support for them **will be rejected**.
+If you want to add support for a new site, first of all **make sure** this site is **not dedicated to [copyright infringement](#is-the-website-primarily-used-for-piracy)**. yt-dlp does **not support** such sites thus pull requests adding support for them **will be rejected**.
After you have ensured this site is distributing its content legally, you can follow this quick list (assuming your service is called `yourextractor`):
@@ -195,7 +195,7 @@ After you have ensured this site is distributing its content legally, you can fo
# * A value
# * MD5 checksum; start the string with md5:
# * A regular expression; start the string with re:
- # * Any Python type (for example int or float)
+ # * Any Python type, e.g. int or float
}
}]
@@ -261,7 +261,7 @@ The aforementioned metafields are the critical data that the extraction does not
For pornographic sites, appropriate `age_limit` must also be returned.
-The extractor is allowed to return the info dict without url or formats in some special cases if it allows the user to extract usefull information with `--ignore-no-formats-error` - Eg: when the video is a live stream that has not started yet.
+The extractor is allowed to return the info dict without url or formats in some special cases if it allows the user to extract usefull information with `--ignore-no-formats-error` - e.g. when the video is a live stream that has not started yet.
[Any field](yt_dlp/extractor/common.py#219-L426) apart from the aforementioned ones are considered **optional**. That means that extraction should be **tolerant** to situations when sources for these fields can potentially be unavailable (even if they are always available at the moment) and **future-proof** in order not to break the extraction of general purpose mandatory fields.
diff --git a/CONTRIBUTORS b/CONTRIBUTORS
index 47559aa341..264c087c2f 100644
--- a/CONTRIBUTORS
+++ b/CONTRIBUTORS
@@ -285,3 +285,49 @@ odo2063
pritam20ps05
scy
sheerluck
+AxiosDeminence
+DjesonPV
+eren-kemer
+freezboltz
+Galiley
+haobinliang
+Mehavoid
+winterbird-code
+yashkc2025
+aldoridhoni
+bashonly
+jacobtruman
+masta79
+palewire
+cgrigis
+DavidH-2022
+dfaker
+jackyyf
+ohaiibuzzle
+SamantazFox
+shreyasminocha
+tejasa97
+xenov
+satan1st
+0xGodspeed
+5736d79
+587021c
+basrieter
+Bobscorn
+CNugteren
+columndeeply
+DoubleCouponDay
+Fabi019
+GautamMKGarg
+Grub4K
+itachi-19
+jeroenj
+josanabr
+LiviaMedeiros
+nikita-moor
+snapdgn
+SuperSonicHub1
+tannertechnology
+Timendum
+tobi1805
+TokyoBlackHole
diff --git a/Changelog.md b/Changelog.md
index 74311052f2..d7600b0463 100644
--- a/Changelog.md
+++ b/Changelog.md
@@ -11,6 +11,293 @@
-->
+### 2022.10.04
+
+* Allow a `set` to be passed as `download_archive` by [pukkandan](https://github.com/pukkandan), [bashonly](https://github.com/bashonly)
+* Allow open ranges for time ranges by [Lesmiscore](https://github.com/Lesmiscore)
+* Allow plugin extractors to replace the built-in ones
+* Don't download entire video when no matching `--download-sections`
+* Fix `--config-location -`
+* Improve [5736d79](https://github.com/yt-dlp/yt-dlp/pull/5044/commits/5736d79172c47ff84740d5720467370a560febad)
+* Fix for when playlists don't have `webpage_url`
+* Support environment variables in `--ffmpeg-location`
+* Workaround `libc_ver` not be available on Windows Store version of Python
+* [outtmpl] Curly braces to filter keys by [pukkandan](https://github.com/pukkandan)
+* [outtmpl] Make `%s` work in strfformat for all systems
+* [jsinterp] Workaround operator associativity issue
+* [cookies] Let `_get_mac_keyring_password` fail gracefully
+* [cookies] Parse cookies leniently by [Grub4K](https://github.com/Grub4K)
+* [phantomjs] Fix bug in [587021c](https://github.com/yt-dlp/yt-dlp/commit/587021cd9f717181b44e881941aca3f8d753758b) by [elyse0](https://github.com/elyse0)
+* [downloader/aria2c] Fix filename containing leading whitespace by [std-move](https://github.com/std-move)
+* [downloader/ism] Support ec-3 codec by [nixxo](https://github.com/nixxo)
+* [extractor] Fix `fatal=False` in `RetryManager`
+* [extractor] Improve json-ld extraction
+* [extractor] Make `_search_json` able to parse lists
+* [extractor] Escape `%` in `representation_id` of m3u8
+* [extractor/generic] Pass through referer from json-ld
+* [utils] `base_url`: URL paths can contain `&` by [elyse0](https://github.com/elyse0)
+* [utils] `js_to_json`: Improve
+* [utils] `Popen.run`: Fix default return in binary mode
+* [utils] `traverse_obj`: Rewrite, document and add tests by [Grub4K](https://github.com/Grub4K)
+* [devscripts] `make_lazy_extractors`: Fix for Docker by [josanabr](https://github.com/josanabr)
+* [docs] Misc Improvements
+* [cleanup] Misc fixes and cleanup by [pukkandan](https://github.com/pukkandan), [gamer191](https://github.com/gamer191)
+* [extractor/24tv.ua] Add extractors by [coletdjnz](https://github.com/coletdjnz)
+* [extractor/BerufeTV] Add extractor by [Fabi019](https://github.com/Fabi019)
+* [extractor/booyah] Add extractor by [HobbyistDev](https://github.com/HobbyistDev), [elyse0](https://github.com/elyse0)
+* [extractor/bundesliga] Add extractor by [Fabi019](https://github.com/Fabi019)
+* [extractor/GoPlay] Add extractor by [CNugteren](https://github.com/CNugteren), [basrieter](https://github.com/basrieter), [jeroenj](https://github.com/jeroenj)
+* [extractor/iltalehti] Add extractor by [tpikonen](https://github.com/tpikonen)
+* [extractor/IsraelNationalNews] Add extractor by [Bobscorn](https://github.com/Bobscorn)
+* [extractor/mediaworksnzvod] Add extractor by [coletdjnz](https://github.com/coletdjnz)
+* [extractor/MicrosoftEmbed] Add extractor by [DoubleCouponDay](https://github.com/DoubleCouponDay)
+* [extractor/nbc] Add NBCStations extractor by [bashonly](https://github.com/bashonly)
+* [extractor/onenewsnz] Add extractor by [coletdjnz](https://github.com/coletdjnz)
+* [extractor/prankcast] Add extractor by [HobbyistDev](https://github.com/HobbyistDev), [columndeeply](https://github.com/columndeeply)
+* [extractor/Smotrim] Add extractor by [Lesmiscore](https://github.com/Lesmiscore), [nikita-moor](https://github.com/nikita-moor)
+* [extractor/tencent] Add Iflix extractor by [elyse0](https://github.com/elyse0)
+* [extractor/unscripted] Add extractor by [HobbyistDev](https://github.com/HobbyistDev)
+* [extractor/adobepass] Add MSO AlticeOne (Optimum TV) by [CplPwnies](https://github.com/CplPwnies)
+* [extractor/youtube] **Download `post_live` videos from start** by [Lesmiscore](https://github.com/Lesmiscore), [pukkandan](https://github.com/pukkandan)
+* [extractor/youtube] Add support for Shorts audio pivot feed by [coletdjnz](https://github.com/coletdjnz), [pukkandan](https://github.com/pukkandan)
+* [extractor/youtube] Detect `lazy-load-for-videos` embeds
+* [extractor/youtube] Do not warn on duplicate chapters
+* [extractor/youtube] Fix video like count extraction by [coletdjnz](https://github.com/coletdjnz)
+* [extractor/youtube] Support changing extraction language by [coletdjnz](https://github.com/coletdjnz)
+* [extractor/youtube:tab] Improve continuation items extraction
+* [extractor/youtube:tab] Support `reporthistory` page
+* [extractor/amazonstore] Fix JSON extraction by [coletdjnz](https://github.com/coletdjnz), [pukkandan](https://github.com/pukkandan)
+* [extractor/amazonstore] Retry to avoid captcha page by [Lesmiscore](https://github.com/Lesmiscore)
+* [extractor/animeondemand] Remove extractor by [TokyoBlackHole](https://github.com/TokyoBlackHole)
+* [extractor/anvato] Fix extractor and refactor by [bashonly](https://github.com/bashonly)
+* [extractor/artetv] Remove duplicate stream urls by [Grub4K](https://github.com/Grub4K)
+* [extractor/audioboom] Support direct URLs and refactor by [pukkandan](https://github.com/pukkandan), [tpikonen](https://github.com/tpikonen)
+* [extractor/bandcamp] Extract `uploader_url`
+* [extractor/bilibili] Add space.bilibili extractors by [lockmatrix](https://github.com/lockmatrix)
+* [extractor/BilibiliSpace] Fix extractor and better error message by [lockmatrix](https://github.com/lockmatrix)
+* [extractor/BiliIntl] Support uppercase lang in `_VALID_URL` by [coletdjnz](https://github.com/coletdjnz)
+* [extractor/BiliIntlSeries] Fix `_VALID_URL`
+* [extractor/bongacams] Update `_VALID_URL` by [0xGodspeed](https://github.com/0xGodspeed)
+* [extractor/crunchyroll:beta] Improve handling of hardsubs by [Grub4K](https://github.com/Grub4K)
+* [extractor/detik] Generalize extractors by [HobbyistDev](https://github.com/HobbyistDev), [coletdjnz](https://github.com/coletdjnz)
+* [extractor/dplay:italy] Add default authentication by [Timendum](https://github.com/Timendum)
+* [extractor/heise] Fix extractor by [coletdjnz](https://github.com/coletdjnz)
+* [extractor/holodex] Fix `_VALID_URL` by [LiviaMedeiros](https://github.com/LiviaMedeiros)
+* [extractor/hrfensehen] Fix extractor by [snapdgn](https://github.com/snapdgn)
+* [extractor/hungama] Add subtitle by [GautamMKGarg](https://github.com/GautamMKGarg), [pukkandan](https://github.com/pukkandan)
+* [extractor/instagram] Extract more metadata by [pritam20ps05](https://github.com/pritam20ps05)
+* [extractor/JWPlatform] Fix extractor by [coletdjnz](https://github.com/coletdjnz)
+* [extractor/malltv] Fix video_id extraction by [HobbyistDev](https://github.com/HobbyistDev)
+* [extractor/MLBTV] Detect live streams
+* [extractor/motorsport] Support native embeds
+* [extractor/Mxplayer] Fix extractor by [itachi-19](https://github.com/itachi-19)
+* [extractor/nebula] Add nebula.tv by [tannertechnology](https://github.com/tannertechnology)
+* [extractor/nfl] Fix extractor by [bashonly](https://github.com/bashonly)
+* [extractor/ondemandkorea] Update `jw_config` regex by [julien-hadleyjack](https://github.com/julien-hadleyjack)
+* [extractor/paramountplus] Better DRM detection by [bashonly](https://github.com/bashonly)
+* [extractor/patreon] Sort formats
+* [extractor/rcs] Fix embed extraction by [coletdjnz](https://github.com/coletdjnz)
+* [extractor/redgifs] Fix extractor by [jhwgh1968](https://github.com/jhwgh1968)
+* [extractor/rutube] Fix `_EMBED_REGEX` by [coletdjnz](https://github.com/coletdjnz)
+* [extractor/RUTV] Fix warnings for livestreams by [Lesmiscore](https://github.com/Lesmiscore)
+* [extractor/soundcloud:search] More metadata in `--flat-playlist` by [SuperSonicHub1](https://github.com/SuperSonicHub1)
+* [extractor/telegraaf] Use mobile GraphQL API endpoint by [coletdjnz](https://github.com/coletdjnz)
+* [extractor/tennistv] Fix timestamp by [zenerdi0de](https://github.com/zenerdi0de)
+* [extractor/tiktok] Fix TikTokIE by [bashonly](https://github.com/bashonly)
+* [extractor/triller] Fix auth token by [bashonly](https://github.com/bashonly)
+* [extractor/trovo] Fix extractors by [Mehavoid](https://github.com/Mehavoid)
+* [extractor/tv2] Support new url format by [tobi1805](https://github.com/tobi1805)
+* [extractor/web.archive:youtube] Fix `_YT_INITIAL_PLAYER_RESPONSE_RE`
+* [extractor/wistia] Add support for channels by [coletdjnz](https://github.com/coletdjnz)
+* [extractor/wistia] Match IDs in embed URLs by [bashonly](https://github.com/bashonly)
+* [extractor/wordpress:playlist] Add generic embed extractor by [coletdjnz](https://github.com/coletdjnz)
+* [extractor/yandexvideopreview] Update `_VALID_URL` by [Grub4K](https://github.com/Grub4K)
+* [extractor/zee5] Fix `_VALID_URL` by [m4tu4g](https://github.com/m4tu4g)
+* [extractor/zee5] Generate device ids by [freezboltz](https://github.com/freezboltz)
+
+
+### 2022.09.01
+
+* Add option `--use-extractors`
+* Merge youtube-dl: Upto [commit/ed5c44e](https://github.com/ytdl-org/youtube-dl/commit/ed5c44e7)
+* Add yt-dlp version to infojson
+* Fix `--break-per-url --max-downloads`
+* Fix bug in `--alias`
+* [cookies] Support firefox container in `--cookies-from-browser` by [bashonly](https://github.com/bashonly), [coletdjnz](https://github.com/coletdjnz), [pukkandan](https://github.com/pukkandan)
+* [downloader/external] Smarter detection of executable
+* [extractor/generic] Don't return JW player without formats
+* [FormatSort] Fix `aext` for `--prefer-free-formats`
+* [jsinterp] Various improvements by [pukkandan](https://github.com/pukkandan), [dirkf](https://github.com/dirkf), [elyse0](https://github.com/elyse0)
+* [cache] Mechanism to invalidate old cache
+* [utils] Add `deprecation_warning`
+* [utils] Add `orderedSet_from_options`
+* [utils] `Popen`: Restore `LD_LIBRARY_PATH` when using PyInstaller by [Lesmiscore](https://github.com/Lesmiscore)
+* [build] `make tar` should not follow `DESTDIR` by [satan1st](https://github.com/satan1st)
+* [build] Update pyinstaller by [shirt-dev](https://github.com/shirt-dev)
+* [test] Fix `test_youtube_signature`
+* [cleanup] Misc fixes and cleanup by [DavidH-2022](https://github.com/DavidH-2022), [MrRawes](https://github.com/MrRawes), [pukkandan](https://github.com/pukkandan)
+* [extractor/epoch] Add extractor by [tejasa97](https://github.com/tejasa97)
+* [extractor/eurosport] Add extractor by [HobbyistDev](https://github.com/HobbyistDev)
+* [extractor/IslamChannel] Add extractors by [Lesmiscore](https://github.com/Lesmiscore)
+* [extractor/newspicks] Add extractor by [Lesmiscore](https://github.com/Lesmiscore)
+* [extractor/triller] Add extractor by [bashonly](https://github.com/bashonly)
+* [extractor/VQQ] Add extractors by [elyse0](https://github.com/elyse0)
+* [extractor/youtube] Improvements to nsig extraction
+* [extractor/youtube] Fix bug in format sorting
+* [extractor/youtube] Update iOS Innertube clients by [SamantazFox](https://github.com/SamantazFox)
+* [extractor/youtube] Use device-specific user agent by [coletdjnz](https://github.com/coletdjnz)
+* [extractor/youtube] Add `--compat-option no-youtube-prefer-utc-upload-date` by [coletdjnz](https://github.com/coletdjnz)
+* [extractor/arte] Bug fix by [cgrigis](https://github.com/cgrigis)
+* [extractor/bilibili] Extract `flac` with premium account by [jackyyf](https://github.com/jackyyf)
+* [extractor/BiliBiliSearch] Don't sort by date
+* [extractor/BiliBiliSearch] Fix infinite loop
+* [extractor/bitchute] Mark errors as expected
+* [extractor/crunchyroll:beta] Use anonymous access by [tejing1](https://github.com/tejing1)
+* [extractor/huya] Fix stream extraction by [ohaiibuzzle](https://github.com/ohaiibuzzle)
+* [extractor/medaltv] Fix extraction by [xenova](https://github.com/xenova)
+* [extractor/mediaset] Fix embed extraction
+* [extractor/mixcloud] All formats are audio-only
+* [extractor/rtbf] Fix jwt extraction by [elyse0](https://github.com/elyse0)
+* [extractor/screencastomatic] Support `--video-password` by [shreyasminocha](https://github.com/shreyasminocha)
+* [extractor/stripchat] Don't modify input URL by [dfaker](https://github.com/dfaker)
+* [extractor/uktv] Improve `_VALID_URL` by [dirkf](https://github.com/dirkf)
+* [extractor/vimeo:user] Fix `_VALID_URL`
+
+
+### 2022.08.19
+
+* Fix bug in `--download-archive`
+* [jsinterp] **Fix for new youtube players** and related improvements by [dirkf](https://github.com/dirkf), [pukkandan](https://github.com/pukkandan)
+* [phantomjs] Add function to execute JS without a DOM by [MinePlayersPE](https://github.com/MinePlayersPE), [pukkandan](https://github.com/pukkandan)
+* [build] Exclude devscripts from installs by [Lesmiscore](https://github.com/Lesmiscore)
+* [cleanup] Misc fixes and cleanup
+* [extractor/youtube] **Add fallback to phantomjs** for nsig
+* [extractor/youtube] Fix error reporting of "Incomplete data"
+* [extractor/youtube] Improve format sorting for IOS formats
+* [extractor/youtube] Improve signature caching
+* [extractor/instagram] Fix extraction by [bashonly](https://github.com/bashonly), [pritam20ps05](https://github.com/pritam20ps05)
+* [extractor/rai] Minor fix by [nixxo](https://github.com/nixxo)
+* [extractor/rtbf] Fix stream extractor by [elyse0](https://github.com/elyse0)
+* [extractor/SovietsCloset] Fix extractor by [ChillingPepper](https://github.com/ChillingPepper)
+* [extractor/zattoo] Fix Zattoo resellers by [goggle](https://github.com/goggle)
+
+### 2022.08.14
+
+* Merge youtube-dl: Upto [commit/d231b56](https://github.com/ytdl-org/youtube-dl/commit/d231b56)
+* [jsinterp] Handle **new youtube signature functions**
+* [jsinterp] Truncate error messages
+* [extractor] Fix format sorting of `channels`
+* [ffmpeg] Disable avconv unless `--prefer-avconv`
+* [ffmpeg] Smarter detection of ffprobe filename
+* [embedthumbnail] Detect `libatomicparsley.so`
+* [ThumbnailsConvertor] Fix conversion after `fixup_webp`
+* [utils] Fix `get_compatible_ext`
+* [build] Fix changelog
+* [update] Set executable bit-mask by [pukkandan](https://github.com/pukkandan), [Lesmiscore](https://github.com/Lesmiscore)
+* [devscripts] Fix import
+* [docs] Consistent use of `e.g.` by [Lesmiscore](https://github.com/Lesmiscore)
+* [cleanup] Misc fixes and cleanup
+* [extractor/moview] Add extractor by [HobbyistDev](https://github.com/HobbyistDev)
+* [extractor/parler] Add extractor by [palewire](https://github.com/palewire)
+* [extractor/patreon] Ignore erroneous media attachments by [coletdjnz](https://github.com/coletdjnz)
+* [extractor/truth] Add extractor by [palewire](https://github.com/palewire)
+* [extractor/aenetworks] Add formats parameter by [jacobtruman](https://github.com/jacobtruman)
+* [extractor/crunchyroll] Improve `_VALID_URL`s
+* [extractor/doodstream] Add `wf` domain by [aldoridhoni](https://github.com/aldoridhoni)
+* [extractor/facebook] Add reel support by [bashonly](https://github.com/bashonly)
+* [extractor/MLB] New extractor by [ischmidt20](https://github.com/ischmidt20)
+* [extractor/rai] Misc fixes by [nixxo](https://github.com/nixxo)
+* [extractor/toggo] Improve `_VALID_URL` by [masta79](https://github.com/masta79)
+* [extractor/tubitv] Extract additional formats by [shirt-dev](https://github.com/shirt-dev)
+* [extractor/zattoo] Potential fix for resellers
+
+
+### 2022.08.08
+
+* **Remove Python 3.6 support**
+* Determine merge container better by [pukkandan](https://github.com/pukkandan), [selfisekai](https://github.com/selfisekai)
+* Framework for embed detection by [coletdjnz](https://github.com/coletdjnz), [pukkandan](https://github.com/pukkandan)
+* Merge youtube-dl: Upto [commit/adb5294](https://github.com/ytdl-org/youtube-dl/commit/adb5294)
+* `--compat-option no-live-chat` should disable danmaku
+* Fix misleading DRM message
+* Import ctypes only when necessary
+* Minor bugfixes
+* Reject entire playlists faster with `--match-filter`
+* Remove filtered entries from `-J`
+* Standardize retry mechanism
+* Validate `--merge-output-format`
+* [downloader] Add average speed to final progress line
+* [extractor] Add field `audio_channels`
+* [extractor] Support multiple archive ids for one video
+* [ffmpeg] Set `ffmpeg_location` in a contextvar
+* [FFmpegThumbnailsConvertor] Fix conversion from GIF
+* [MetadataParser] Don't set `None` when the field didn't match
+* [outtmpl] Smarter replacing of unsupported characters
+* [outtmpl] Treat empty values as None in filenames
+* [utils] sanitize_open: Allow any IO stream as stdout
+* [build, devscripts] Add devscript to set a build variant
+* [build] Improve build process by [shirt-dev](https://github.com/shirt-dev)
+* [build] Update pyinstaller
+* [devscripts] Create `utils` and refactor
+* [docs] Clarify `best*`
+* [docs] Fix bug report issue template
+* [docs] Fix capitalization in references by [christoph-heinrich](https://github.com/christoph-heinrich)
+* [cleanup, mhtml] Use imghdr
+* [cleanup, utils] Consolidate known media extensions
+* [cleanup] Misc fixes and cleanup
+* [extractor/angel] Add extractor by [AxiosDeminence](https://github.com/AxiosDeminence)
+* [extractor/dplay] Add MotorTrend extractor by [Sipherdrakon](https://github.com/Sipherdrakon)
+* [extractor/harpodeon] Add extractor by [eren-kemer](https://github.com/eren-kemer)
+* [extractor/holodex] Add extractor by [pukkandan](https://github.com/pukkandan), [sqrtNOT](https://github.com/sqrtNOT)
+* [extractor/kompas] Add extractor by [HobbyistDev](https://github.com/HobbyistDev)
+* [extractor/rai] Add raisudtirol extractor by [nixxo](https://github.com/nixxo)
+* [extractor/tempo] Add extractor by [HobbyistDev](https://github.com/HobbyistDev)
+* [extractor/youtube] **Fixes for third party client detection** by [coletdjnz](https://github.com/coletdjnz)
+* [extractor/youtube] Add `live_status=post_live` by [lazypete365](https://github.com/lazypete365)
+* [extractor/youtube] Extract more format info
+* [extractor/youtube] Parse translated subtitles only when requested
+* [extractor/youtube, extractor/twitch] Allow waiting for channels to become live
+* [extractor/youtube, webvtt] Extract auto-subs from livestream VODs by [fstirlitz](https://github.com/fstirlitz), [pukkandan](https://github.com/pukkandan)
+* [extractor/AbemaTVTitle] Implement paging by [Lesmiscore](https://github.com/Lesmiscore)
+* [extractor/archiveorg] Improve handling of formats by [coletdjnz](https://github.com/coletdjnz), [pukkandan](https://github.com/pukkandan)
+* [extractor/arte] Fix title extraction
+* [extractor/arte] **Move to v2 API** by [fstirlitz](https://github.com/fstirlitz), [pukkandan](https://github.com/pukkandan)
+* [extractor/bbc] Fix news articles by [ajj8](https://github.com/ajj8)
+* [extractor/camtasia] Separate into own extractor by [coletdjnz](https://github.com/coletdjnz)
+* [extractor/cloudflarestream] Fix video_id padding by [haobinliang](https://github.com/haobinliang)
+* [extractor/crunchyroll] Fix conversion of thumbnail from GIF
+* [extractor/crunchyroll] Handle missing metadata correctly by [Burve](https://github.com/Burve), [pukkandan](https://github.com/pukkandan)
+* [extractor/crunchyroll:beta] Extract timestamp and fix tests by [tejing1](https://github.com/tejing1)
+* [extractor/crunchyroll:beta] Use streams API by [tejing1](https://github.com/tejing1)
+* [extractor/doodstream] Support more domains by [Galiley](https://github.com/Galiley)
+* [extractor/ESPN] Extract duration by [ischmidt20](https://github.com/ischmidt20)
+* [extractor/FIFA] Change API endpoint by [Bricio](https://github.com/Bricio), [yashkc2025](https://github.com/yashkc2025)
+* [extractor/globo:article] Remove false positives by [Bricio](https://github.com/Bricio)
+* [extractor/Go] Extract timestamp by [ischmidt20](https://github.com/ischmidt20)
+* [extractor/hidive] Fix cookie login when netrc is also given by [winterbird-code](https://github.com/winterbird-code)
+* [extractor/html5] Separate into own extractor by [coletdjnz](https://github.com/coletdjnz), [pukkandan](https://github.com/pukkandan)
+* [extractor/ina] Improve extractor by [elyse0](https://github.com/elyse0)
+* [extractor/NaverNow] Change endpoint by [ping](https://github.com/ping)
+* [extractor/ninegag] Extract uploader by [DjesonPV](https://github.com/DjesonPV)
+* [extractor/NovaPlay] Fix extractor by [Bojidarist](https://github.com/Bojidarist)
+* [extractor/orf:radio] Rewrite extractors
+* [extractor/patreon] Fix and improve extractors by [coletdjnz](https://github.com/coletdjnz), [pukkandan](https://github.com/pukkandan)
+* [extractor/rai] Fix RaiNews extraction by [nixxo](https://github.com/nixxo)
+* [extractor/redbee] Unify and update extractors by [elyse0](https://github.com/elyse0)
+* [extractor/stripchat] Fix _VALID_URL by [freezboltz](https://github.com/freezboltz)
+* [extractor/tubi] Exclude playlists from playlist entries by [sqrtNOT](https://github.com/sqrtNOT)
+* [extractor/tviplayer] Improve `_VALID_URL` by [HobbyistDev](https://github.com/HobbyistDev)
+* [extractor/twitch] Extract chapters for single chapter VODs by [mpeter50](https://github.com/mpeter50)
+* [extractor/vgtv] Support tv.vg.no by [sqrtNOT](https://github.com/sqrtNOT)
+* [extractor/vidio] Support embed link by [HobbyistDev](https://github.com/HobbyistDev)
+* [extractor/vk] Fix extractor by [Mehavoid](https://github.com/Mehavoid)
+* [extractor/WASDTV:record] Fix `_VALID_URL`
+* [extractor/xfileshare] Add Referer by [Galiley](https://github.com/Galiley)
+* [extractor/YahooJapanNews] Fix extractor by [Lesmiscore](https://github.com/Lesmiscore)
+* [extractor/yandexmusic] Extract higher quality format
+* [extractor/zee5] Update Device ID by [m4tu4g](https://github.com/m4tu4g)
+
+
### 2022.07.18
* Allow users to specify encoding in each config files by [Lesmiscore](https://github.com/Lesmiscore)
@@ -125,7 +412,7 @@
* [**Deprecate support for Python 3.6**](https://github.com/yt-dlp/yt-dlp/issues/3764#issuecomment-1154051119)
* **Add option `--download-sections` to download video partially**
- * Chapter regex and time ranges are accepted (Eg: `--download-sections *1:10-2:20`)
+ * Chapter regex and time ranges are accepted, e.g. `--download-sections *1:10-2:20`
* Add option `--alias`
* Add option `--lazy-playlist` to process entries as they are received
* Add option `--retry-sleep`
@@ -1289,7 +1576,7 @@
* Add new option `--netrc-location`
* [outtmpl] Allow alternate fields using `,`
-* [outtmpl] Add format type `B` to treat the value as bytes (eg: to limit the filename to a certain number of bytes)
+* [outtmpl] Add format type `B` to treat the value as bytes, e.g. to limit the filename to a certain number of bytes
* Separate the options `--ignore-errors` and `--no-abort-on-error`
* Basic framework for simultaneous download of multiple formats by [nao20010128nao](https://github.com/nao20010128nao)
* [17live] Add 17.live extractor by [nao20010128nao](https://github.com/nao20010128nao)
@@ -1679,7 +1966,7 @@
* Merge youtube-dl: Upto [commit/a803582](https://github.com/ytdl-org/youtube-dl/commit/a8035827177d6b59aca03bd717acb6a9bdd75ada)
* Add `--extractor-args` to pass some extractor-specific arguments. See [readme](https://github.com/yt-dlp/yt-dlp#extractor-arguments)
- * Add extractor option `skip` for `youtube`. Eg: `--extractor-args youtube:skip=hls,dash`
+ * Add extractor option `skip` for `youtube`, e.g. `--extractor-args youtube:skip=hls,dash`
* Deprecates `--youtube-skip-dash-manifest`, `--youtube-skip-hls-manifest`, `--youtube-include-dash-manifest`, `--youtube-include-hls-manifest`
* Allow `--list...` options to work with `--print`, `--quiet` and other `--list...` options
* [youtube] Use `player` API for additional video extraction requests by [coletdjnz](https://github.com/coletdjnz)
diff --git a/Collaborators.md b/Collaborators.md
index 52e3b9caea..3f24d5c476 100644
--- a/Collaborators.md
+++ b/Collaborators.md
@@ -28,12 +28,12 @@ You can also find lists of all [contributors of yt-dlp](CONTRIBUTORS) and [autho
[![gh-sponsor](https://img.shields.io/badge/_-Sponsor-red.svg?logo=githubsponsors&labelColor=555555&style=for-the-badge)](https://github.com/sponsors/coletdjnz)
* YouTube improvements including: age-gate bypass, private playlists, multiple-clients (to avoid throttling) and a lot of under-the-hood improvements
-* Added support for downloading YoutubeWebArchive videos
-* Added support for new websites MainStreaming, PRX, nzherald, etc
+* Added support for new websites YoutubeWebArchive, MainStreaming, PRX, nzherald, Mediaklikk, StarTV etc
+* Improved/fixed support for Patreon, panopto, gfycat, itv, pbs, SouthParkDE etc
-## [Ashish0804](https://github.com/Ashish0804)
+## [Ashish0804](https://github.com/Ashish0804) [Inactive]
[![ko-fi](https://img.shields.io/badge/_-Ko--fi-red.svg?logo=kofi&labelColor=555555&style=for-the-badge)](https://ko-fi.com/ashish0804)
@@ -48,4 +48,5 @@ You can also find lists of all [contributors of yt-dlp](CONTRIBUTORS) and [autho
**Monacoin**: mona1q3tf7dzvshrhfe3md379xtvt2n22duhglv5dskr
* Download live from start to end for YouTube
-* Added support for new websites mildom, PixivSketch, skeb, radiko, voicy, mirrativ, openrec, whowatch, damtomo, 17.live, mixch etc
+* Added support for new websites AbemaTV, mildom, PixivSketch, skeb, radiko, voicy, mirrativ, openrec, whowatch, damtomo, 17.live, mixch etc
+* Improved/fixed support for fc2, YahooJapanNews, tver, iwara etc
diff --git a/Makefile b/Makefile
index d6a00d332b..8f335927d0 100644
--- a/Makefile
+++ b/Makefile
@@ -17,8 +17,8 @@ pypi-files: AUTHORS Changelog.md LICENSE README.md README.txt supportedsites \
clean-test:
rm -rf test/testdata/sigs/player-*.js tmp/ *.annotations.xml *.aria2 *.description *.dump *.frag \
*.frag.aria2 *.frag.urls *.info.json *.live_chat.json *.meta *.part* *.tmp *.temp *.unknown_video *.ytdl \
- *.3gp *.ape *.ass *.avi *.desktop *.f4v *.flac *.flv *.jpeg *.jpg *.m4a *.mpga *.m4v *.mhtml *.mkv *.mov \
- *.mp3 *.mp4 *.ogg *.opus *.png *.sbv *.srt *.swf *.swp *.ttml *.url *.vtt *.wav *.webloc *.webm *.webp
+ *.3gp *.ape *.ass *.avi *.desktop *.f4v *.flac *.flv *.jpeg *.jpg *.m4a *.m4v *.mhtml *.mkv *.mov *.mp3 *.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
@@ -33,7 +33,6 @@ completion-zsh: completions/zsh/_yt-dlp
lazy-extractors: yt_dlp/extractor/lazy_extractors.py
PREFIX ?= /usr/local
-DESTDIR ?= .
BINDIR ?= $(PREFIX)/bin
MANDIR ?= $(PREFIX)/man
SHAREDIR ?= $(PREFIX)/share
@@ -75,17 +74,16 @@ 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/extractor/anvato_token_generator
+CODE_FOLDERS = yt_dlp yt_dlp/downloader yt_dlp/extractor yt_dlp/postprocessor yt_dlp/compat
yt-dlp: yt_dlp/*.py yt_dlp/*/*.py
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 zip/yt_dlp/*/*/*.py
+ touch -t 200001010101 zip/yt_dlp/*.py zip/yt_dlp/*/*.py
mv zip/yt_dlp/__main__.py zip/
- cd zip ; zip -q ../yt-dlp yt_dlp/*.py yt_dlp/*/*.py yt_dlp/*/*/*.py __main__.py
+ cd zip ; zip -q ../yt-dlp yt_dlp/*.py yt_dlp/*/*.py __main__.py
rm -rf zip
echo '#!$(PYTHON)' > yt-dlp
cat yt-dlp.zip >> yt-dlp
@@ -134,7 +132,7 @@ yt_dlp/extractor/lazy_extractors.py: devscripts/make_lazy_extractors.py devscrip
$(PYTHON) devscripts/make_lazy_extractors.py $@
yt-dlp.tar.gz: all
- @tar -czf $(DESTDIR)/yt-dlp.tar.gz --transform "s|^|yt-dlp/|" --owner 0 --group 0 \
+ @tar -czf yt-dlp.tar.gz --transform "s|^|yt-dlp/|" --owner 0 --group 0 \
--exclude '*.DS_Store' \
--exclude '*.kate-swp' \
--exclude '*.pyc' \
diff --git a/README.md b/README.md
index 285c0b78a0..4f731785d3 100644
--- a/README.md
+++ b/README.md
@@ -3,7 +3,7 @@
[![YT-DLP](https://raw.githubusercontent.com/yt-dlp/yt-dlp/master/.github/banner.svg)](#readme)
-[![Release version](https://img.shields.io/github/v/release/yt-dlp/yt-dlp?color=brightgreen&label=Download&style=for-the-badge)](#release-files "Release")
+[![Release version](https://img.shields.io/github/v/release/yt-dlp/yt-dlp?color=brightgreen&label=Download&style=for-the-badge)](#installation "Installation")
[![PyPi](https://img.shields.io/badge/-PyPi-blue.svg?logo=pypi&labelColor=555555&style=for-the-badge)](https://pypi.org/project/yt-dlp "PyPi")
[![Donate](https://img.shields.io/badge/_-Donate-red.svg?logo=githubsponsors&labelColor=555555&style=for-the-badge)](Collaborators.md#collaborators "Donate")
[![Matrix](https://img.shields.io/matrix/yt-dlp:matrix.org?color=brightgreen&labelColor=555555&label=&logo=element&style=for-the-badge)](https://matrix.to/#/#yt-dlp:matrix.org "Matrix")
@@ -25,6 +25,7 @@ yt-dlp is a [youtube-dl](https://github.com/ytdl-org/youtube-dl) fork based on t
* [NEW FEATURES](#new-features)
* [Differences in default behavior](#differences-in-default-behavior)
* [INSTALLATION](#installation)
+ * [Detailed instructions](https://github.com/yt-dlp/yt-dlp/wiki/Installation)
* [Update](#update)
* [Release Files](#release-files)
* [Dependencies](#dependencies)
@@ -47,9 +48,10 @@ yt-dlp is a [youtube-dl](https://github.com/ytdl-org/youtube-dl) fork based on t
* [SponsorBlock Options](#sponsorblock-options)
* [Extractor Options](#extractor-options)
* [CONFIGURATION](#configuration)
+ * [Configuration file encoding](#configuration-file-encoding)
* [Authentication with .netrc file](#authentication-with-netrc-file)
+ * [Notes about environment variables](#notes-about-environment-variables)
* [OUTPUT TEMPLATE](#output-template)
- * [Output template and Windows batch files](#output-template-and-windows-batch-files)
* [Output template examples](#output-template-examples)
* [FORMAT SELECTION](#format-selection)
* [Filtering Formats](#filtering-formats)
@@ -65,15 +67,16 @@ yt-dlp is a [youtube-dl](https://github.com/ytdl-org/youtube-dl) fork based on t
* [CONTRIBUTING](CONTRIBUTING.md#contributing-to-yt-dlp)
* [Opening an Issue](CONTRIBUTING.md#opening-an-issue)
* [Developer Instructions](CONTRIBUTING.md#developer-instructions)
-* [MORE](#more)
+* [WIKI](https://github.com/yt-dlp/yt-dlp/wiki)
+ * [FAQ](https://github.com/yt-dlp/yt-dlp/wiki/FAQ)
# NEW FEATURES
-* Merged with **youtube-dl v2021.12.17+ [commit/a03b977](https://github.com/ytdl-org/youtube-dl/commit/a03b9775d544b06a5b4f2aa630214c7c22fc2229)** and **youtube-dlc v2020.11.11-3+ [commit/f9401f2](https://github.com/blackjack4494/yt-dlc/commit/f9401f2a91987068139c5f757b12fc711d4c0cee)**: You get all the features and patches of [youtube-dlc](https://github.com/blackjack4494/yt-dlc) in addition to the latest [youtube-dl](https://github.com/ytdl-org/youtube-dl)
+* Merged with **youtube-dl v2021.12.17+ [commit/ed5c44e](https://github.com/ytdl-org/youtube-dl/commit/ed5c44e7b74ac77f87ca5ed6cb5e964a0c6a0678)** and **youtube-dlc v2020.11.11-3+ [commit/f9401f2](https://github.com/blackjack4494/yt-dlc/commit/f9401f2a91987068139c5f757b12fc711d4c0cee)**: You get all the features and patches of [youtube-dlc](https://github.com/blackjack4494/yt-dlc) in addition to the latest [youtube-dl](https://github.com/ytdl-org/youtube-dl)
-* **[SponsorBlock Integration](#sponsorblock-options)**: You can mark/remove sponsor sections in youtube videos by utilizing the [SponsorBlock](https://sponsor.ajay.app) API
+* **[SponsorBlock Integration](#sponsorblock-options)**: You can mark/remove sponsor sections in YouTube videos by utilizing the [SponsorBlock](https://sponsor.ajay.app) API
* **[Format Sorting](#sorting-formats)**: The default format sorting options have been changed so that higher resolution and better codecs will be now preferred instead of simply using larger bitrate. Furthermore, you can now specify the sort order using `-S`. This allows for much easier format selection than what is possible by simply using `--format` ([examples](#format-selection-examples))
@@ -87,7 +90,7 @@ yt-dlp is a [youtube-dl](https://github.com/ytdl-org/youtube-dl) fork based on t
* `255kbps` audio is extracted (if available) from YouTube Music when premium cookies are given
* Redirect channel's home URL automatically to `/video` to preserve the old behaviour
-* **Cookies from browser**: Cookies can be automatically extracted from all major web browsers using `--cookies-from-browser BROWSER[+KEYRING][:PROFILE]`
+* **Cookies from browser**: Cookies can be automatically extracted from all major web browsers using `--cookies-from-browser BROWSER[+KEYRING][:PROFILE][::CONTAINER]`
* **Download time range**: Videos can be downloaded partially based on either timestamps or chapters using `--download-sections`
@@ -139,14 +142,15 @@ Some of yt-dlp's default options are different from that of youtube-dl and youtu
* `playlist_index` behaves differently when used with options like `--playlist-reverse` and `--playlist-items`. See [#302](https://github.com/yt-dlp/yt-dlp/issues/302) for details. You can use `--compat-options playlist-index` if you want to keep the earlier behavior
* The output of `-F` is listed in a new format. Use `--compat-options list-formats` to revert this
* Live chats (if available) are considered as subtitles. Use `--sub-langs all,-live_chat` to download all subtitles except live chat. You can also use `--compat-options no-live-chat` to prevent any live chat/danmaku from downloading
-* Youtube channel URLs are automatically redirected to `/video`. Append a `/featured` to the URL to download only the videos in the home page. If the channel does not have a videos tab, we try to download the equivalent `UU` playlist instead. For all other tabs, if the channel does not show the requested tab, an error will be raised. Also, `/live` URLs raise an error if there are no live videos instead of silently downloading the entire channel. You may use `--compat-options no-youtube-channel-redirect` to revert all these redirections
-* Unavailable videos are also listed for youtube playlists. Use `--compat-options no-youtube-unavailable-videos` to remove this
+* YouTube channel URLs are automatically redirected to `/video`. Append a `/featured` to the URL to download only the videos in the home page. If the channel does not have a videos tab, we try to download the equivalent `UU` playlist instead. For all other tabs, if the channel does not show the requested tab, an error will be raised. Also, `/live` URLs raise an error if there are no live videos instead of silently downloading the entire channel. You may use `--compat-options no-youtube-channel-redirect` to revert all these redirections
+* Unavailable videos are also listed for YouTube playlists. Use `--compat-options no-youtube-unavailable-videos` to remove this
+* The upload dates extracted from YouTube are in UTC [when available](https://github.com/yt-dlp/yt-dlp/blob/89e4d86171c7b7c997c77d4714542e0383bf0db0/yt_dlp/extractor/youtube.py#L3898-L3900). Use `--compat-options no-youtube-prefer-utc-upload-date` to prefer the non-UTC upload date.
* If `ffmpeg` is used as the downloader, the downloading and merging of formats happen in a single step when possible. Use `--compat-options no-direct-merge` to revert this
* Thumbnail embedding in `mp4` is done with mutagen if possible. Use `--compat-options embed-thumbnail-atomicparsley` to force the use of AtomicParsley instead
* Some private fields such as filenames are removed by default from the infojson. Use `--no-clean-infojson` or `--compat-options no-clean-infojson` to revert this
* When `--embed-subs` and `--write-subs` are used together, the subtitles are written to disk and also embedded in the media file. You can use just `--embed-subs` to embed the subs and automatically delete the separate file. See [#630 (comment)](https://github.com/yt-dlp/yt-dlp/issues/630#issuecomment-893659460) for more info. `--compat-options no-keep-subs` can be used to revert this
* `certifi` will be used for SSL root certificates, if installed. If you want to use system certificates (e.g. self-signed), use `--compat-options no-certifi`
-* youtube-dl tries to remove some superfluous punctuations from filenames. While this can sometimes be helpful, it is often undesirable. So yt-dlp tries to keep the fields in the filenames as close to their original values as possible. You can use `--compat-options filename-sanitization` to revert to youtube-dl's behavior
+* yt-dlp's sanitization of invalid characters in filenames is different/smarter than in youtube-dl. You can use `--compat-options filename-sanitization` to revert to youtube-dl's behavior
For ease of use, a few more compat options are available:
@@ -157,76 +161,26 @@ For ease of use, a few more compat options are available:
# INSTALLATION
-You can install yt-dlp using one of the following methods:
-
-### Using the release binary
-
-You can simply download the [correct binary file](#release-files) for your OS
-
[![Windows](https://img.shields.io/badge/-Windows_x64-blue.svg?style=for-the-badge&logo=windows)](https://github.com/yt-dlp/yt-dlp/releases/latest/download/yt-dlp.exe)
-[![Linux](https://img.shields.io/badge/-Linux/BSD-red.svg?style=for-the-badge&logo=linux)](https://github.com/yt-dlp/yt-dlp/releases/latest/download/yt-dlp)
+[![Unix](https://img.shields.io/badge/-Linux/BSD-red.svg?style=for-the-badge&logo=linux)](https://github.com/yt-dlp/yt-dlp/releases/latest/download/yt-dlp)
[![MacOS](https://img.shields.io/badge/-MacOS-lightblue.svg?style=for-the-badge&logo=apple)](https://github.com/yt-dlp/yt-dlp/releases/latest/download/yt-dlp_macos)
+[![PyPi](https://img.shields.io/badge/-PyPi-blue.svg?logo=pypi&labelColor=555555&style=for-the-badge)](https://pypi.org/project/yt-dlp)
[![Source Tarball](https://img.shields.io/badge/-Source_tar-green.svg?style=for-the-badge)](https://github.com/yt-dlp/yt-dlp/releases/latest/download/yt-dlp.tar.gz)
[![Other variants](https://img.shields.io/badge/-Other-grey.svg?style=for-the-badge)](#release-files)
[![All versions](https://img.shields.io/badge/-All_Versions-lightgrey.svg?style=for-the-badge)](https://github.com/yt-dlp/yt-dlp/releases)
-Note: The manpages, shell completion files etc. are available in the [source tarball](https://github.com/yt-dlp/yt-dlp/releases/latest/download/yt-dlp.tar.gz)
+You can install yt-dlp using [the binaries](#release-files), [PIP](https://pypi.org/project/yt-dlp) or one using a third-party package manager. See [the wiki](https://github.com/yt-dlp/yt-dlp/wiki/Installation) for detailed instructions
-
-In UNIX-like OSes (MacOS, Linux, BSD), you can also install the same in one of the following ways:
-
-```
-sudo curl -L https://github.com/yt-dlp/yt-dlp/releases/latest/download/yt-dlp -o /usr/local/bin/yt-dlp
-sudo chmod a+rx /usr/local/bin/yt-dlp
-```
-
-```
-sudo wget https://github.com/yt-dlp/yt-dlp/releases/latest/download/yt-dlp -O /usr/local/bin/yt-dlp
-sudo chmod a+rx /usr/local/bin/yt-dlp
-```
-
-```
-sudo aria2c https://github.com/yt-dlp/yt-dlp/releases/latest/download/yt-dlp --dir /usr/local/bin -o yt-dlp
-sudo chmod a+rx /usr/local/bin/yt-dlp
-```
-
-
-### With [PIP](https://pypi.org/project/pip)
-
-You can install the [PyPI package](https://pypi.org/project/yt-dlp) with:
-```
-python3 -m pip install -U yt-dlp
-```
-
-You can install without any of the optional dependencies using:
-```
-python3 -m pip install --no-deps -U yt-dlp
-```
-
-If you want to be on the cutting edge, you can also install the master branch with:
-```
-python3 -m pip install --force-reinstall https://github.com/yt-dlp/yt-dlp/archive/master.tar.gz
-```
-
-On some systems, you may need to use `py` or `python` instead of `python3`
-
-
-### With [Homebrew](https://brew.sh)
-
-macOS or Linux users that are using Homebrew can also install it by:
-
-```
-brew install yt-dlp/taps/yt-dlp
-```
## UPDATE
-You can use `yt-dlp -U` to update if you are [using the provided release](#using-the-release-binary)
+You can use `yt-dlp -U` to update if you are [using the release binaries](#release-files)
-If you [installed with pip](#with-pip), simply re-run the same command that was used to install the program
+If you [installed with PIP](https://github.com/yt-dlp/yt-dlp/wiki/Installation#with-pip), simply re-run the same command that was used to install the program
+
+For other third-party package managers, see [the wiki](https://github.com/yt-dlp/yt-dlp/wiki/Installation) or refer their documentation
-If you [installed using Homebrew](#with-homebrew), run `brew upgrade yt-dlp/taps/yt-dlp`
## RELEASE FILES
@@ -255,11 +209,14 @@ File|Description
File|Description
:---|:---
-[yt-dlp.tar.gz](https://github.com/yt-dlp/yt-dlp/releases/latest/download/yt-dlp.tar.gz)|Source tarball. Also contains manpages, completions, etc
+[yt-dlp.tar.gz](https://github.com/yt-dlp/yt-dlp/releases/latest/download/yt-dlp.tar.gz)|Source tarball
[SHA2-512SUMS](https://github.com/yt-dlp/yt-dlp/releases/latest/download/SHA2-512SUMS)|GNU-style SHA512 sums
[SHA2-256SUMS](https://github.com/yt-dlp/yt-dlp/releases/latest/download/SHA2-256SUMS)|GNU-style SHA256 sums
+
+Note: The manpages, shell completion files etc. are available in the [source tarball](https://github.com/yt-dlp/yt-dlp/releases/latest/download/yt-dlp.tar.gz)
+
## DEPENDENCIES
Python versions 3.7+ (CPython and PyPy) are supported. Other versions and implementations may or may not work correctly.
@@ -295,7 +252,7 @@ While all the other dependencies are optional, `ffmpeg` and `ffprobe` are highly
* [**secretstorage**](https://github.com/mitya57/secretstorage) - For `--cookies-from-browser` to access the **Gnome** keyring while decrypting cookies of **Chromium**-based browsers on **Linux**. Licensed under [BSD-3-Clause](https://github.com/mitya57/secretstorage/blob/master/LICENSE)
* Any external downloader that you want to use with `--downloader`
-#### Deprecated
+### Deprecated
* [**avconv** and **avprobe**](https://www.libav.org) - Now **deprecated** alternative to ffmpeg. License [depends on the build](https://libav.org/legal)
* [**sponskrub**](https://github.com/faissaloo/SponSkrub) - For using the now **deprecated** [sponskrub options](#sponskrub-options). Licensed under [GPLv3+](https://github.com/faissaloo/SponSkrub/blob/master/LICENCE.md)
@@ -312,7 +269,7 @@ If you do not have the necessary dependencies for a task you are attempting, yt-
## COMPILE
### Standalone PyInstaller Builds
-To build the Windows/MacOS 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 (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). 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.
python3 -m pip install -U pyinstaller -r requirements.txt
python3 devscripts/make_lazy_extractors.py
@@ -320,16 +277,18 @@ To build the Windows/MacOS executable, you must have Python and `pyinstaller` (p
On some systems, you may need to use `py` or `python` instead of `python3`.
-Note that pyinstaller [does not support](https://github.com/pyinstaller/pyinstaller#requirements-and-tested-platforms) Python installed from the Windows store without using a virtual environment.
+`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).
+
+Note that pyinstaller with 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.
### Platform-independent Binary (UNIX)
-You will need the build tools `python` (3.6+), `zip`, `make` (GNU), `pandoc`\* and `pytest`\*.
+You will need the build tools `python` (3.7+), `zip`, `make` (GNU), `pandoc`\* and `pytest`\*.
After installing these, simply run `make`.
-You can also run `make yt-dlp` instead to compile only the binary without updating any of the additional files. (The dependencies marked with **\*** are not needed for this)
+You can also run `make yt-dlp` instead to compile only the binary without updating any of the additional files. (The build tools marked with **\*** are not needed for this)
### Standalone Py2Exe Builds (Windows)
@@ -343,10 +302,11 @@ If you wish to build it anyway, install Python and py2exe, and then simply run `
### Related scripts
-* **`devscripts/update-version.py`** - Update the version number based on current timestamp
+* **`devscripts/update-version.py [revision]`** - Update the version number based on current date
+* **`devscripts/set-variant.py variant [-M update_message]`** - Set the build variant of the executable
* **`devscripts/make_lazy_extractors.py`** - Create lazy extractors. Running this before building the binaries (any variant) will improve their startup performance. Set the environment variable `YTDLP_NO_LAZY_EXTRACTORS=1` if you wish to forcefully disable lazy extractor loading.
-You can also fork the project on github and run your fork's [build workflow](.github/workflows/build.yml) to automatically build a full release
+You can also fork the project on GitHub and run your fork's [build workflow](.github/workflows/build.yml) to automatically build a full release
# USAGE AND OPTIONS
@@ -360,8 +320,8 @@ You can also fork the project on github and run your fork's [build workflow](.gi
## General Options:
-h, --help Print this help text and exit
--version Print program version and exit
- -U, --update Update this program to latest version
- --no-update Do not update (default)
+ -U, --update Update this program to the latest version
+ --no-update Do not check for updates (default)
-i, --ignore-errors Ignore download and postprocessing errors.
The download will be considered successful
even if the postprocessing fails
@@ -374,8 +334,14 @@ You can also fork the project on github and run your fork's [build workflow](.gi
--list-extractors List all supported extractors and exit
--extractor-descriptions Output descriptions of all supported
extractors and exit
- --force-generic-extractor Force extraction to use the generic extractor
- --default-search PREFIX Use this prefix for unqualified URLs. Eg:
+ --use-extractors NAMES Extractor names to use separated by commas.
+ You can also use regexes, "all", "default"
+ and "end" (end URL matching); e.g. --ies
+ "holodex.*,end,youtube". Prefix the name
+ with a "-" to exclude it, e.g. --ies
+ default,-generic. Use --list-extractors for
+ a list of extractor names. (Alias: --ies)
+ --default-search PREFIX Use this prefix for unqualified URLs. E.g.
"gvsearch2:python" downloads two videos from
google videos for the search term "python".
Use the value "auto" to let yt-dlp guess
@@ -424,7 +390,7 @@ You can also fork the project on github and run your fork's [build workflow](.gi
an alias starts with a dash "-", it is
prefixed with "--". Arguments are parsed
according to the Python string formatting
- mini-language. Eg: --alias get-audio,-X
+ mini-language. E.g. --alias get-audio,-X
"-S=aext:{0},abr -x --audio-format {0}"
creates options "--get-audio" and "-X" that
takes an argument (ARG0) and expands to
@@ -438,10 +404,10 @@ You can also fork the project on github and run your fork's [build workflow](.gi
## Network Options:
--proxy URL Use the specified HTTP/HTTPS/SOCKS proxy. To
- enable SOCKS proxy, specify a proper scheme.
- Eg: socks5://user:pass@127.0.0.1:1080/. Pass
- in an empty string (--proxy "") for direct
- connection
+ enable SOCKS proxy, specify a proper scheme,
+ e.g. socks5://user:pass@127.0.0.1:1080/.
+ Pass in an empty string (--proxy "") for
+ direct connection
--socket-timeout SECONDS Time to wait before giving up, in seconds
--source-address IP Client-side IP address to bind to
-4, --force-ipv4 Make all connections via IPv4
@@ -470,17 +436,17 @@ You can also fork the project on github and run your fork's [build workflow](.gi
compatibility, START-STOP is also supported.
Use negative indices to count from the right
and negative STEP to download in reverse
- order. Eg: "-I 1:3,7,-5::2" used on a
+ order. E.g. "-I 1:3,7,-5::2" used on a
playlist of size 15 will download the videos
at index 1,2,3,7,11,13,15
- --min-filesize SIZE Do not download any videos smaller than SIZE
- (e.g. 50k or 44.6m)
- --max-filesize SIZE Do not download any videos larger than SIZE
- (e.g. 50k or 44.6m)
+ --min-filesize SIZE Do not download any videos smaller than
+ SIZE, e.g. 50k or 44.6M
+ --max-filesize SIZE Do not download any videos larger than SIZE,
+ e.g. 50k or 44.6M
--date DATE Download only videos uploaded on this date.
The date can be "YYYYMMDD" or in the format
[now|today|yesterday][-N[day|week|month|year]].
- Eg: --date today-2weeks
+ E.g. --date today-2weeks
--datebefore DATE Download only videos uploaded on or before
this date. The date formats accepted is the
same as --date
@@ -497,7 +463,7 @@ You can also fork the project on github and run your fork's [build workflow](.gi
conditions. Use a "\" to escape "&" or
quotes if needed. If used multiple times,
the filter matches if atleast one of the
- conditions are met. Eg: --match-filter
+ conditions are met. E.g. --match-filter
!is_live --match-filter "like_count>?100 &
description~='(?i)\bcats \& dogs\b'" matches
only videos that are not live OR those that
@@ -523,8 +489,8 @@ You can also fork the project on github and run your fork's [build workflow](.gi
a file that is in the archive
--break-on-reject Stop the download process when encountering
a file that has been filtered out
- --break-per-input Make --break-on-existing, --break-on-reject
- and --max-downloads act only on the current
+ --break-per-input --break-on-existing, --break-on-reject,
+ --max-downloads, and autonumber resets per
input URL
--no-break-per-input --break-on-existing and similar options
terminates the entire download queue
@@ -535,11 +501,11 @@ You can also fork the project on github and run your fork's [build workflow](.gi
-N, --concurrent-fragments N Number of fragments of a dash/hlsnative
video that should be downloaded concurrently
(default is 1)
- -r, --limit-rate RATE Maximum download rate in bytes per second
- (e.g. 50K or 4.2M)
+ -r, --limit-rate RATE Maximum download rate in bytes per second,
+ e.g. 50K or 4.2M
--throttled-rate RATE Minimum download rate in bytes per second
below which throttling is assumed and the
- video data is re-extracted (e.g. 100K)
+ video data is re-extracted, e.g. 100K
-R, --retries RETRIES Number of retries (default is 10), or
"infinite"
--file-access-retries RETRIES Number of times to retry on file access
@@ -553,7 +519,7 @@ You can also fork the project on github and run your fork's [build workflow](.gi
be a number, linear=START[:END[:STEP=1]] or
exp=START[:END[:BASE=2]]. This option can be
used multiple times to set the sleep for the
- different retry types. Eg: --retry-sleep
+ different retry types, e.g. --retry-sleep
linear=1::2 --retry-sleep fragment:exp=1:20
--skip-unavailable-fragments Skip unavailable fragments for DASH,
hlsnative and ISM downloads (default)
@@ -565,14 +531,14 @@ You can also fork the project on github and run your fork's [build workflow](.gi
downloading is finished
--no-keep-fragments Delete downloaded fragments after
downloading is finished (default)
- --buffer-size SIZE Size of download buffer (e.g. 1024 or 16K)
+ --buffer-size SIZE Size of download buffer, e.g. 1024 or 16K
(default is 1024)
--resize-buffer The buffer size is automatically resized
from an initial value of --buffer-size
(default)
--no-resize-buffer Do not automatically adjust the buffer size
--http-chunk-size SIZE Size of a chunk for chunk-based HTTP
- downloading (e.g. 10485760 or 10M) (default
+ downloading, e.g. 10485760 or 10M (default
is disabled). May be useful for bypassing
bandwidth throttling imposed by a webserver
(experimental)
@@ -597,10 +563,10 @@ You can also fork the project on github and run your fork's [build workflow](.gi
the given regular expression. Time ranges
prefixed by a "*" can also be used in place
of chapters to download the specified range.
- Eg: --download-sections "*10:15-15:00"
- --download-sections "intro". Needs ffmpeg.
- This option can be used multiple times to
- download multiple sections
+ Needs ffmpeg. This option can be used
+ multiple times to download multiple
+ sections, e.g. --download-sections
+ "*10:15-inf" --download-sections "intro"
--downloader [PROTO:]NAME Name or path of the external downloader to
use (optionally) prefixed by the protocols
(http, ftp, m3u8, dash, rstp, rtmp, mms) to
@@ -608,7 +574,7 @@ You can also fork the project on github and run your fork's [build workflow](.gi
aria2c, avconv, axel, curl, ffmpeg, httpie,
wget. You can use this option multiple times
to set different downloaders for different
- protocols. For example, --downloader aria2c
+ protocols. E.g. --downloader aria2c
--downloader "dash,m3u8:native" will use
aria2c for http/ftp downloads, and the
native downloader for dash/m3u8 downloads
@@ -699,24 +665,25 @@ You can also fork the project on github and run your fork's [build workflow](.gi
and dump cookie jar in
--no-cookies Do not read/dump cookies from/to file
(default)
- --cookies-from-browser BROWSER[+KEYRING][:PROFILE]
- The name of the browser and (optionally) the
- name/path of the profile to load cookies
- from, separated by a ":". Currently
- supported browsers are: brave, chrome,
- chromium, edge, firefox, opera, safari,
- vivaldi. By default, the most recently
- accessed profile is used. The keyring used
- for decrypting Chromium cookies on Linux can
- be (optionally) specified after the browser
- name separated by a "+". Currently supported
- keyrings are: basictext, gnomekeyring, kwallet
+ --cookies-from-browser BROWSER[+KEYRING][:PROFILE][::CONTAINER]
+ The name of the browser to load cookies
+ from. Currently supported browsers are:
+ brave, chrome, chromium, edge, firefox,
+ opera, safari, vivaldi. Optionally, the
+ KEYRING used for decrypting Chromium cookies
+ on Linux, the name/path of the PROFILE to
+ load cookies from, and the CONTAINER name
+ (if Firefox) ("none" for no container) can
+ be given with their respective seperators.
+ By default, all containers of the most
+ recently accessed profile are used.
+ Currently supported keyrings are: basictext,
+ gnomekeyring, kwallet
--no-cookies-from-browser Do not load cookies from browser (default)
- --cache-dir DIR Location in the filesystem where youtube-dl
- can store some downloaded information (such
- as client ids and signatures) permanently.
- By default $XDG_CACHE_HOME/yt-dlp or
- ~/.cache/yt-dlp
+ --cache-dir DIR Location in the filesystem where yt-dlp can
+ store some downloaded information (such as
+ client ids and signatures) permanently. By
+ default ${XDG_CACHE_HOME}/yt-dlp
--no-cache-dir Disable filesystem caching
--rm-cache-dir Delete all filesystem cache files
@@ -790,7 +757,7 @@ You can also fork the project on github and run your fork's [build workflow](.gi
"postprocess:", or "postprocess-title:".
The video's fields are accessible under the
"info" key and the progress attributes are
- accessible under "progress" key. E.g.:
+ accessible under "progress" key. E.g.
--console-title --progress-template
"download-title:%(info.id)s-%(progress.eta)s"
-v, --verbose Print various debugging information
@@ -859,7 +826,7 @@ You can also fork the project on github and run your fork's [build workflow](.gi
-F, --list-formats List available formats of each video.
Simulate unless --no-simulate is used
--merge-output-format FORMAT Containers that may be used when merging
- formats, separated by "/" (Eg: "mp4/mkv").
+ formats, separated by "/", e.g. "mp4/mkv".
Ignored if no merge is required. (currently
supported: avi, flv, mkv, mov, mp4, webm)
@@ -873,13 +840,13 @@ You can also fork the project on github and run your fork's [build workflow](.gi
--list-subs List available subtitles of each video.
Simulate unless --no-simulate is used
--sub-format FORMAT Subtitle format; accepts formats preference,
- Eg: "srt" or "ass/srt/best"
+ e.g. "srt" or "ass/srt/best"
--sub-langs LANGS Languages of the subtitles to download (can
- be regex) or "all" separated by commas. (Eg:
- --sub-langs "en.*,ja") You can prefix the
+ be regex) or "all" separated by commas, e.g.
+ --sub-langs "en.*,ja". You can prefix the
language code with a "-" to exclude it from
- the requested languages. (Eg: --sub-langs
- all,-live_chat) Use --list-subs for a list
+ the requested languages, e.g. --sub-langs
+ all,-live_chat. Use --list-subs for a list
of available language tags
## Authentication Options:
@@ -928,7 +895,7 @@ You can also fork the project on github and run your fork's [build workflow](.gi
m4a, mka, mp3, ogg, opus, vorbis, wav). If
target container does not support the
video/audio codec, remuxing will fail. You
- can specify multiple rules; Eg.
+ can specify multiple rules; e.g.
"aac>m4a/mov>mp4/mkv" will remux aac to m4a,
mov to mp4 and anything else to mkv
--recode-video FORMAT Re-encode the video into another format if
@@ -953,7 +920,7 @@ You can also fork the project on github and run your fork's [build workflow](.gi
for ffmpeg/ffprobe, "_i"/"_o" can be
appended to the prefix optionally followed
by a number to pass the argument before the
- specified input/output file. Eg: --ppa
+ specified input/output file, e.g. --ppa
"Merger+ffmpeg_i1:-v quiet". You can use
this option multiple times to give different
arguments to different postprocessors.
@@ -1077,10 +1044,10 @@ Make chapter entries for, or remove various segments (sponsor,
for, separated by commas. Available
categories are sponsor, intro, outro,
selfpromo, preview, filler, interaction,
- music_offtopic, poi_highlight, all and
+ music_offtopic, poi_highlight, chapter, all and
default (=all). You can prefix the category
with a "-" to exclude it. See [1] for
- description of the categories. Eg:
+ description of the categories. E.g.
--sponsorblock-mark all,-preview
[1] https://wiki.sponsor.ajay.app/w/Segment_Categories
--sponsorblock-remove CATS SponsorBlock categories to be removed from
@@ -1089,8 +1056,8 @@ Make chapter entries for, or remove various segments (sponsor,
remove takes precedence. The syntax and
available categories are the same as for
--sponsorblock-mark except that "default"
- refers to "all,-filler" and poi_highlight is
- not available
+ refers to "all,-filler" and poi_highlight and
+ chapter are not available
--sponsorblock-chapter-title TEMPLATE
An output template for the title of the
SponsorBlock chapters created by
@@ -1115,31 +1082,36 @@ Make chapter entries for, or remove various segments (sponsor,
--no-hls-split-discontinuity Do not split HLS playlists to different
formats at discontinuities such as ad breaks
(default)
- --extractor-args KEY:ARGS Pass these arguments to the extractor. See
- "EXTRACTOR ARGUMENTS" for details. You can
- use this option multiple times to give
+ --extractor-args IE_KEY:ARGS Pass ARGS arguments to the IE_KEY extractor.
+ See "EXTRACTOR ARGUMENTS" for details. You
+ can use this option multiple times to give
arguments for different extractors
# CONFIGURATION
You can configure yt-dlp by placing any supported command line option to a configuration file. The configuration is loaded from the following locations:
-1. **Main Configuration**: The file given by `--config-location`
-1. **Portable Configuration**: `yt-dlp.conf` in the same directory as the bundled binary. If you are running from source-code (`/yt_dlp/__main__.py`), the root directory is used instead.
-1. **Home Configuration**: `yt-dlp.conf` in the home path given by `-P`, or in the current directory if no such path is given
+1. **Main Configuration**:
+ * The file given by `--config-location`
+1. **Portable Configuration**: (Recommended for portable installations)
+ * If using a binary, `yt-dlp.conf` in the same directory as the binary
+ * If running from source-code, `yt-dlp.conf` in the parent directory of `yt_dlp`
+1. **Home Configuration**:
+ * `yt-dlp.conf` in the home path given by `-P`
+ * If `-P` is not given, the current directory is searched
1. **User Configuration**:
- * `%XDG_CONFIG_HOME%/yt-dlp/config` (recommended on Linux/macOS)
- * `%XDG_CONFIG_HOME%/yt-dlp.conf`
- * `%APPDATA%/yt-dlp/config` (recommended on Windows)
- * `%APPDATA%/yt-dlp/config.txt`
+ * `${XDG_CONFIG_HOME}/yt-dlp/config` (recommended on Linux/macOS)
+ * `${XDG_CONFIG_HOME}/yt-dlp.conf`
+ * `${APPDATA}/yt-dlp/config` (recommended on Windows)
+ * `${APPDATA}/yt-dlp/config.txt`
* `~/yt-dlp.conf`
* `~/yt-dlp.conf.txt`
-
- `%XDG_CONFIG_HOME%` defaults to `~/.config` if undefined. On windows, `%APPDATA%` generally points to `C:\Users\\AppData\Roaming` and `~` points to `%HOME%` if present, `%USERPROFILE%` (generally `C:\Users\`), or `%HOMEDRIVE%%HOMEPATH%`
-1. **System Configuration**: `/etc/yt-dlp.conf`
+ See also: [Notes about environment variables](#notes-about-environment-variables)
+1. **System Configuration**:
+ * `/etc/yt-dlp.conf`
-For example, with the following configuration file yt-dlp will always extract the audio, not copy the mtime, use a proxy and save all videos under `YouTube` directory in your home directory:
+E.g. with the following configuration file yt-dlp will always extract the audio, not copy the mtime, use a proxy and save all videos under `YouTube` directory in your home directory:
```
# Lines starting with # are comments
@@ -1156,35 +1128,42 @@ For example, with the following configuration file yt-dlp will always extract th
-o ~/YouTube/%(title)s.%(ext)s
```
-Note that options in configuration file are just the same options aka switches used in regular command line calls; thus there **must be no whitespace** after `-` or `--`, e.g. `-o` or `--proxy` but not `- o` or `-- proxy`.
+Note that options in configuration file are just the same options aka switches used in regular command line calls; thus there **must be no whitespace** after `-` or `--`, e.g. `-o` or `--proxy` but not `- o` or `-- proxy`. They must also be quoted when necessary as-if it were a UNIX shell.
You can use `--ignore-config` if you want to disable all configuration files for a particular yt-dlp run. If `--ignore-config` is found inside any configuration file, no further configuration will be loaded. For example, having the option in the portable configuration file prevents loading of home, user, and system configurations. Additionally, (for backward compatibility) if `--ignore-config` is found inside the system configuration file, the user configuration is not loaded.
-### Config file encoding
+### Configuration file encoding
-The config files are decoded according to the UTF BOM if present, and in the encoding from system locale otherwise.
+The configuration files are decoded according to the UTF BOM if present, and in the encoding from system locale otherwise.
If you want your file to be decoded differently, add `# coding: ENCODING` to the beginning of the file (e.g. `# coding: shift-jis`). There must be no characters before that, even spaces or BOM.
### Authentication with `.netrc` file
-You may also want to configure automatic credentials storage for extractors that support authentication (by providing login and password with `--username` and `--password`) in order not to pass credentials as command line arguments on every yt-dlp execution and prevent tracking plain text passwords in the shell command history. You can achieve this using a [`.netrc` file](https://stackoverflow.com/tags/.netrc/info) on a per extractor basis. For that you will need to create a `.netrc` file in `--netrc-location` and restrict permissions to read/write by only you:
+You may also want to configure automatic credentials storage for extractors that support authentication (by providing login and password with `--username` and `--password`) in order not to pass credentials as command line arguments on every yt-dlp execution and prevent tracking plain text passwords in the shell command history. You can achieve this using a [`.netrc` file](https://stackoverflow.com/tags/.netrc/info) on a per-extractor basis. For that you will need to create a `.netrc` file in `--netrc-location` and restrict permissions to read/write by only you:
```
-touch $HOME/.netrc
-chmod a-rwx,u+rw $HOME/.netrc
+touch ${HOME}/.netrc
+chmod a-rwx,u+rw ${HOME}/.netrc
```
After that you can add credentials for an extractor in the following format, where *extractor* is the name of the extractor in lowercase:
```
machine login password
```
-For example:
+E.g.
```
machine youtube login myaccount@gmail.com password my_youtube_password
machine twitch login my_twitch_account_name password my_twitch_password
```
To activate authentication with the `.netrc` file you should pass `--netrc` to yt-dlp or place it in the [configuration file](#configuration).
-The default location of the .netrc file is `$HOME` (`~`) in UNIX. On Windows, it is `%HOME%` if present, `%USERPROFILE%` (generally `C:\Users\`) or `%HOMEDRIVE%%HOMEPATH%`
+The default location of the .netrc file is `~` (see below).
+
+### Notes about environment variables
+* Environment variables are normally specified as `${VARIABLE}`/`$VARIABLE` on UNIX and `%VARIABLE%` on Windows; but is always shown as `${VARIABLE}` in this documentation
+* yt-dlp also allow using UNIX-style variables on Windows for path-like options; e.g. `--output`, `--config-location`
+* If unset, `${XDG_CONFIG_HOME}` defaults to `~/.config` and `${XDG_CACHE_HOME}` to `~/.cache`
+* On Windows, `~` points to `${HOME}` if present; or, `${USERPROFILE}` or `${HOMEDRIVE}${HOMEPATH}` otherwise
+* On Windows, `${USERPROFILE}` generally points to `C:\Users\` and `${APPDATA}` to `${USERPROFILE}\AppData\Roaming`
# OUTPUT TEMPLATE
@@ -1196,39 +1175,38 @@ The `-o` option is used to indicate a template for the output file names while `
The simplest usage of `-o` is not to set any template arguments when downloading a single file, like in `yt-dlp -o funny_video.flv "https://some/video"` (hard-coding file extension like this is _not_ recommended and could break some post-processing).
-It may however also contain special sequences that will be replaced when downloading each video. The special sequences may be formatted according to [Python string formatting operations](https://docs.python.org/3/library/stdtypes.html#printf-style-string-formatting). For example, `%(NAME)s` or `%(NAME)05d`. To clarify, that is a percent symbol followed by a name in parentheses, followed by formatting operations.
+It may however also contain special sequences that will be replaced when downloading each video. The special sequences may be formatted according to [Python string formatting operations](https://docs.python.org/3/library/stdtypes.html#printf-style-string-formatting), e.g. `%(NAME)s` or `%(NAME)05d`. To clarify, that is a percent symbol followed by a name in parentheses, followed by formatting operations.
The field names themselves (the part inside the parenthesis) can also have some special formatting:
-1. **Object traversal**: The dictionaries and lists available in metadata can be traversed by using a `.` (dot) separator. You can also do python slicing using `:`. Eg: `%(tags.0)s`, `%(subtitles.en.-1.ext)s`, `%(id.3:7:-1)s`, `%(formats.:.format_id)s`. `%()s` refers to the entire infodict. Note that all the fields that become available using this method are not listed below. Use `-j` to see such fields
+1. **Object traversal**: The dictionaries and lists available in metadata can be traversed by using a dot `.` separator; e.g. `%(tags.0)s`, `%(subtitles.en.-1.ext)s`. You can do Python slicing with colon `:`; E.g. `%(id.3:7:-1)s`, `%(formats.:.format_id)s`. Curly braces `{}` can be used to build dictionaries with only specific keys; e.g. `%(formats.:.{format_id,height})#j`. An empty field name `%()s` refers to the entire infodict; e.g. `%(.{id,title})s`. Note that all the fields that become available using this method are not listed below. Use `-j` to see such fields
-1. **Addition**: Addition and subtraction of numeric fields can be done using `+` and `-` respectively. Eg: `%(playlist_index+10)03d`, `%(n_entries+1-playlist_index)d`
+1. **Addition**: Addition and subtraction of numeric fields can be done using `+` and `-` respectively. E.g. `%(playlist_index+10)03d`, `%(n_entries+1-playlist_index)d`
-1. **Date/time Formatting**: Date/time fields can be formatted according to [strftime formatting](https://docs.python.org/3/library/datetime.html#strftime-and-strptime-format-codes) by specifying it separated from the field name using a `>`. Eg: `%(duration>%H-%M-%S)s`, `%(upload_date>%Y-%m-%d)s`, `%(epoch-3600>%H-%M-%S)s`
+1. **Date/time Formatting**: Date/time fields can be formatted according to [strftime formatting](https://docs.python.org/3/library/datetime.html#strftime-and-strptime-format-codes) by specifying it separated from the field name using a `>`. E.g. `%(duration>%H-%M-%S)s`, `%(upload_date>%Y-%m-%d)s`, `%(epoch-3600>%H-%M-%S)s`
-1. **Alternatives**: Alternate fields can be specified separated with a `,`. Eg: `%(release_date>%Y,upload_date>%Y|Unknown)s`
+1. **Alternatives**: Alternate fields can be specified separated with a `,`. E.g. `%(release_date>%Y,upload_date>%Y|Unknown)s`
-1. **Replacement**: A replacement value can specified using a `&` separator. If the field is *not* empty, this replacement value will be used instead of the actual field content. This is done after alternate fields are considered; thus the replacement is used if *any* of the alternative fields is *not* empty.
+1. **Replacement**: A replacement value can be specified using a `&` separator. If the field is *not* empty, this replacement value will be used instead of the actual field content. This is done after alternate fields are considered; thus the replacement is used if *any* of the alternative fields is *not* empty.
-1. **Default**: A literal default value can be specified for when the field is empty using a `|` separator. This overrides `--output-na-template`. Eg: `%(uploader|Unknown)s`
+1. **Default**: A literal default value can be specified for when the field is empty using a `|` separator. This overrides `--output-na-placeholder`. E.g. `%(uploader|Unknown)s`
-1. **More Conversions**: In addition to the normal format types `diouxXeEfFgGcrs`, yt-dlp additionally supports converting to `B` = **B**ytes, `j` = **j**son (flag `#` for pretty-printing), `h` = HTML escaping, `l` = a comma separated **l**ist (flag `#` for `\n` newline-separated), `q` = a string **q**uoted for the terminal (flag `#` to split a list into different arguments), `D` = add **D**ecimal suffixes (Eg: 10M) (flag `#` to use 1024 as factor), and `S` = **S**anitize as filename (flag `#` for restricted)
+1. **More Conversions**: In addition to the normal format types `diouxXeEfFgGcrs`, yt-dlp additionally supports converting to `B` = **B**ytes, `j` = **j**son (flag `#` for pretty-printing, `+` for Unicode), `h` = HTML escaping, `l` = a comma separated **l**ist (flag `#` for `\n` newline-separated), `q` = a string **q**uoted for the terminal (flag `#` to split a list into different arguments), `D` = add **D**ecimal suffixes (e.g. 10M) (flag `#` to use 1024 as factor), and `S` = **S**anitize as filename (flag `#` for restricted)
-1. **Unicode normalization**: The format type `U` can be used for NFC [unicode normalization](https://docs.python.org/3/library/unicodedata.html#unicodedata.normalize). The alternate form flag (`#`) changes the normalization to NFD and the conversion flag `+` can be used for NFKC/NFKD compatibility equivalence normalization. Eg: `%(title)+.100U` is NFKC
+1. **Unicode normalization**: The format type `U` can be used for NFC [Unicode normalization](https://docs.python.org/3/library/unicodedata.html#unicodedata.normalize). The alternate form flag (`#`) changes the normalization to NFD and the conversion flag `+` can be used for NFKC/NFKD compatibility equivalence normalization. E.g. `%(title)+.100U` is NFKC
To summarize, the general syntax for a field is:
```
%(name[.keys][addition][>strf][,alternate][&replacement][|default])[flags][width][.precision][length]type
```
-Additionally, you can set different output templates for the various metadata files separately from the general output template by specifying the type of file followed by the template separated by a colon `:`. The different file types supported are `subtitle`, `thumbnail`, `description`, `annotation` (deprecated), `infojson`, `link`, `pl_thumbnail`, `pl_description`, `pl_infojson`, `chapter`, `pl_video`. For example, `-o "%(title)s.%(ext)s" -o "thumbnail:%(title)s\%(title)s.%(ext)s"` will put the thumbnails in a folder with the same name as the video. If any of the templates is empty, that type of file will not be written. Eg: `--write-thumbnail -o "thumbnail:"` will write thumbnails only for playlists and not for video.
+Additionally, you can set different output templates for the various metadata files separately from the general output template by specifying the type of file followed by the template separated by a colon `:`. The different file types supported are `subtitle`, `thumbnail`, `description`, `annotation` (deprecated), `infojson`, `link`, `pl_thumbnail`, `pl_description`, `pl_infojson`, `chapter`, `pl_video`. E.g. `-o "%(title)s.%(ext)s" -o "thumbnail:%(title)s\%(title)s.%(ext)s"` will put the thumbnails in a folder with the same name as the video. If any of the templates is empty, that type of file will not be written. E.g. `--write-thumbnail -o "thumbnail:"` will write thumbnails only for playlists and not for video.
The available fields are:
- `id` (string): Video identifier
- `title` (string): Video title
- `fulltitle` (string): Video title ignoring live timestamp and generic title
- - `url` (string): Video URL
- `ext` (string): Video filename extension
- `alt_title` (string): A secondary title of the video
- `description` (string): The description of the video
@@ -1250,6 +1228,7 @@ The available fields are:
- `duration` (numeric): Length of the video in seconds
- `duration_string` (string): Length of the video (HH:mm:ss)
- `view_count` (numeric): How many users have watched the video on the platform
+ - `concurrent_view_count` (numeric): How many users are currently watching the video on the platform.
- `like_count` (numeric): Number of positive ratings of the video
- `dislike_count` (numeric): Number of negative ratings of the video
- `repost_count` (numeric): Number of reposts of the video
@@ -1263,25 +1242,6 @@ The available fields are:
- `availability` (string): Whether the video is "private", "premium_only", "subscriber_only", "needs_auth", "unlisted" or "public"
- `start_time` (numeric): Time in seconds where the reproduction should start, as specified in the URL
- `end_time` (numeric): Time in seconds where the reproduction should end, as specified in the URL
- - `format` (string): A human-readable description of the format
- - `format_id` (string): Format code specified by `--format`
- - `format_note` (string): Additional info about the format
- - `width` (numeric): Width of the video
- - `height` (numeric): Height of the video
- - `resolution` (string): Textual description of width and height
- - `tbr` (numeric): Average bitrate of audio and video in KBit/s
- - `abr` (numeric): Average audio bitrate in KBit/s
- - `acodec` (string): Name of the audio codec in use
- - `asr` (numeric): Audio sampling rate in Hertz
- - `vbr` (numeric): Average video bitrate in KBit/s
- - `fps` (numeric): Frame rate
- - `dynamic_range` (string): The dynamic range of the video
- - `stretched_ratio` (float): `width:height` of the video's pixels, if not square
- - `vcodec` (string): Name of the video codec in use
- - `container` (string): Name of the container format
- - `filesize` (numeric): The number of bytes, if known in advance
- - `filesize_approx` (numeric): An estimate for the number of bytes
- - `protocol` (string): The protocol that will be used for the actual download
- `extractor` (string): Name of the extractor
- `extractor_key` (string): Key name of the extractor
- `epoch` (numeric): Unix epoch of when the information extraction was completed
@@ -1300,6 +1260,8 @@ The available fields are:
- `webpage_url_basename` (string): The basename of the webpage URL
- `webpage_url_domain` (string): The domain of the webpage URL
- `original_url` (string): The URL given by the user (or same as `webpage_url` for playlist entries)
+
+All the fields in [Filtering Formats](#filtering-formats) can also be used
Available for the video that belongs to some logical chapter or section:
@@ -1351,18 +1313,19 @@ Available only in `--sponsorblock-chapter-title`:
- `start_time` (numeric): Start time of the chapter in seconds
- `end_time` (numeric): End time of the chapter in seconds
- - `categories` (list): The SponsorBlock categories the chapter belongs to
+ - `categories` (list): The [SponsorBlock categories](https://wiki.sponsor.ajay.app/w/Types#Category) the chapter belongs to
- `category` (string): The smallest SponsorBlock category the chapter belongs to
- `category_names` (list): Friendly names of the categories
- `name` (string): Friendly name of the smallest category
+ - `type` (string): The [SponsorBlock action type](https://wiki.sponsor.ajay.app/w/Types#Action_Type) of the chapter
-Each aforementioned sequence when referenced in an output template will be replaced by the actual value corresponding to the sequence name. For example for `-o %(title)s-%(id)s.%(ext)s` and an mp4 video with title `yt-dlp test video` and id `BaW_jenozKc`, this will result in a `yt-dlp test video-BaW_jenozKc.mp4` file created in the current directory.
+Each aforementioned sequence when referenced in an output template will be replaced by the actual value corresponding to the sequence name. E.g. for `-o %(title)s-%(id)s.%(ext)s` and an mp4 video with title `yt-dlp test video` and id `BaW_jenozKc`, this will result in a `yt-dlp test video-BaW_jenozKc.mp4` file created in the current directory.
Note that some of the sequences are not guaranteed to be present since they depend on the metadata obtained by a particular extractor. Such sequences will be replaced with placeholder value provided with `--output-na-placeholder` (`NA` by default).
**Tip**: Look at the `-j` output to identify which fields are available for the particular URL
-For numeric sequences you can use [numeric related formatting](https://docs.python.org/3/library/stdtypes.html#printf-style-string-formatting), for example, `%(view_count)05d` will result in a string with view count padded with zeros up to 5 characters, like in `00042`.
+For numeric sequences you can use [numeric related formatting](https://docs.python.org/3/library/stdtypes.html#printf-style-string-formatting); e.g. `%(view_count)05d` will result in a string with view count padded with zeros up to 5 characters, like in `00042`.
Output templates can also contain arbitrary hierarchical path, e.g. `-o "%(playlist)s/%(playlist_index)s - %(title)s.%(ext)s"` which will result in downloading each video in a directory corresponding to this path template. Any missing directory will be automatically created for you.
@@ -1372,22 +1335,16 @@ The current default template is `%(title)s [%(id)s].%(ext)s`.
In some cases, you don't want special characters such as 中, spaces, or &, such as when transferring the downloaded filename to a Windows system or the filename through an 8bit-unsafe channel. In these cases, add the `--restrict-filenames` flag to get a shorter title.
-
-#### Output template and Windows batch files
-
-If you are using an output template inside a Windows batch file then you must escape plain percent characters (`%`) by doubling, so that `-o "%(title)s-%(id)s.%(ext)s"` should become `-o "%%(title)s-%%(id)s.%%(ext)s"`. However you should not touch `%`'s that are not plain characters, e.g. environment variables for expansion should stay intact: `-o "C:\%HOMEPATH%\Desktop\%%(title)s.%%(ext)s"`.
-
-
#### Output template examples
```bash
-$ yt-dlp --get-filename -o "test video.%(ext)s" BaW_jenozKc
+$ yt-dlp --print filename -o "test video.%(ext)s" BaW_jenozKc
test video.webm # Literal name with correct extension
-$ yt-dlp --get-filename -o "%(title)s.%(ext)s" BaW_jenozKc
+$ yt-dlp --print filename -o "%(title)s.%(ext)s" BaW_jenozKc
youtube-dl test video ''_ä↭𝕐.webm # All kinds of weird characters
-$ yt-dlp --get-filename -o "%(title)s.%(ext)s" BaW_jenozKc --restrict-filenames
+$ yt-dlp --print filename -o "%(title)s.%(ext)s" BaW_jenozKc --restrict-filenames
youtube-dl_test_video_.webm # Restricted file name
# Download YouTube playlist videos in separate directory indexed by video order in a playlist
@@ -1432,7 +1389,7 @@ The general syntax for format selection is `-f FORMAT` (or `--format FORMAT`) wh
**tl;dr:** [navigate me to examples](#format-selection-examples).
-The simplest case is requesting a specific format, for example with `-f 22` you can download the format with format code equal to 22. You can get the list of available format codes for particular video using `--list-formats` or `-F`. Note that these format codes are extractor specific.
+The simplest case is requesting a specific format; e.g. with `-f 22` you can download the format with format code equal to 22. You can get the list of available format codes for particular video using `--list-formats` or `-F`. Note that these format codes are extractor specific.
You can also use a file extension (currently `3gp`, `aac`, `flv`, `m4a`, `mp3`, `mp4`, `ogg`, `wav`, `webm` are supported) to download the best quality format of a particular file extension served as a single file, e.g. `-f webm` will download the best quality format with the `webm` extension served as a single file.
@@ -1459,15 +1416,15 @@ For example, to download the worst quality video-only format you can use `-f wor
You can select the n'th best format of a type by using `best.`. For example, `best.2` will select the 2nd best combined format. Similarly, `bv*.3` will select the 3rd best format that contains a video stream.
-If you want to download multiple videos and they don't have the same formats available, you can specify the order of preference using slashes. Note that formats on the left hand side are preferred, for example `-f 22/17/18` will download format 22 if it's available, otherwise it will download format 17 if it's available, otherwise it will download format 18 if it's available, otherwise it will complain that no suitable formats are available for download.
+If you want to download multiple videos, and they don't have the same formats available, you can specify the order of preference using slashes. Note that formats on the left hand side are preferred; e.g. `-f 22/17/18` will download format 22 if it's available, otherwise it will download format 17 if it's available, otherwise it will download format 18 if it's available, otherwise it will complain that no suitable formats are available for download.
If you want to download several formats of the same video use a comma as a separator, e.g. `-f 22,17,18` will download all these three formats, of course if they are available. Or a more sophisticated example combined with the precedence feature: `-f 136/137/mp4/bestvideo,140/m4a/bestaudio`.
-You can merge the video and audio of multiple formats into a single file using `-f ++...` (requires ffmpeg installed), for example `-f bestvideo+bestaudio` will download the best video-only format, the best audio-only format and mux them together with ffmpeg.
+You can merge the video and audio of multiple formats into a single file using `-f ++...` (requires ffmpeg installed); e.g. `-f bestvideo+bestaudio` will download the best video-only format, the best audio-only format and mux them together with ffmpeg.
**Deprecation warning**: Since the *below* described behavior is complex and counter-intuitive, this will be removed and multistreams will be enabled by default in the future. A new operator will be instead added to limit formats to single audio/video
-Unless `--video-multistreams` is used, all formats with a video stream except the first one are ignored. Similarly, unless `--audio-multistreams` is used, all formats with an audio stream except the first one are ignored. For example, `-f bestvideo+best+bestaudio --video-multistreams --audio-multistreams` will download and merge all 3 given formats. The resulting file will have 2 video streams and 2 audio streams. But `-f bestvideo+best+bestaudio --no-video-multistreams` will download and merge only `bestvideo` and `bestaudio`. `best` is ignored since another format containing a video stream (`bestvideo`) has already been selected. The order of the formats is therefore important. `-f best+bestaudio --no-audio-multistreams` will download and merge both formats while `-f bestaudio+best --no-audio-multistreams` will ignore `best` and download only `bestaudio`.
+Unless `--video-multistreams` is used, all formats with a video stream except the first one are ignored. Similarly, unless `--audio-multistreams` is used, all formats with an audio stream except the first one are ignored. E.g. `-f bestvideo+best+bestaudio --video-multistreams --audio-multistreams` will download and merge all 3 given formats. The resulting file will have 2 video streams and 2 audio streams. But `-f bestvideo+best+bestaudio --no-video-multistreams` will download and merge only `bestvideo` and `bestaudio`. `best` is ignored since another format containing a video stream (`bestvideo`) has already been selected. The order of the formats is therefore important. `-f best+bestaudio --no-audio-multistreams` will download only `best` while `-f bestaudio+best --no-audio-multistreams` will ignore `best` and download only `bestaudio`.
## Filtering Formats
@@ -1476,6 +1433,7 @@ You can also filter the video formats by putting a condition in brackets, as in
The following numeric meta fields can be used with comparisons `<`, `<=`, `>`, `>=`, `=` (equals), `!=` (not equals):
- `filesize`: The number of bytes, if known in advance
+ - `filesize_approx`: An estimate for the number of bytes
- `width`: Width of the video, if known
- `height`: Height of the video, if known
- `tbr`: Average bitrate of audio and video in KBit/s
@@ -1483,24 +1441,31 @@ The following numeric meta fields can be used with comparisons `<`, `<=`, `>`, `
- `vbr`: Average video bitrate in KBit/s
- `asr`: Audio sampling rate in Hertz
- `fps`: Frame rate
+ - `audio_channels`: The number of audio channels
+ - `stretched_ratio`: `width:height` of the video's pixels, if not square
Also filtering work for comparisons `=` (equals), `^=` (starts with), `$=` (ends with), `*=` (contains), `~=` (matches regex) and following string meta fields:
+ - `url`: Video URL
- `ext`: File extension
- `acodec`: Name of the audio codec in use
- `vcodec`: Name of the video codec in use
- `container`: Name of the container format
- `protocol`: The protocol that will be used for the actual download, lower-case (`http`, `https`, `rtsp`, `rtmp`, `rtmpe`, `mms`, `f4m`, `ism`, `http_dash_segments`, `m3u8`, or `m3u8_native`)
- - `format_id`: A short description of the format
- `language`: Language code
+ - `dynamic_range`: The dynamic range of the video
+ - `format_id`: A short description of the format
+ - `format`: A human-readable description of the format
+ - `format_note`: Additional info about the format
+ - `resolution`: Textual description of width and height
Any string comparison may be prefixed with negation `!` in order to produce an opposite comparison, e.g. `!*=` (does not contain). The comparand of a string comparison needs to be quoted with either double or single quotes if it contains spaces or special characters other than `._-`.
Note that none of the aforementioned meta fields are guaranteed to be present since this solely depends on the metadata obtained by particular extractor, i.e. the metadata offered by the website. Any other field made available by the extractor can also be used for filtering.
-Formats for which the value is not known are excluded unless you put a question mark (`?`) after the operator. You can combine format filters, so `-f "[height<=?720][tbr>500]"` selects up to 720p videos (or videos where the height is not known) with a bitrate of at least 500 KBit/s. You can also use the filters with `all` to download all formats that satisfy the filter. For example, `-f "all[vcodec=none]"` selects all audio-only formats.
+Formats for which the value is not known are excluded unless you put a question mark (`?`) after the operator. You can combine format filters, so `-f "[height<=?720][tbr>500]"` selects up to 720p videos (or videos where the height is not known) with a bitrate of at least 500 KBit/s. You can also use the filters with `all` to download all formats that satisfy the filter, e.g. `-f "all[vcodec=none]"` selects all audio-only formats.
-Format selectors can also be grouped using parentheses, for example if you want to download the best pre-merged mp4 and webm formats with a height lower than 480 you can use `-f "(mp4,webm)[height<480]"`.
+Format selectors can also be grouped using parentheses; e.g. `-f "(mp4,webm)[height<480]"` will download the best pre-merged mp4 and webm formats with a height lower than 480.
## Sorting Formats
@@ -1508,8 +1473,8 @@ You can change the criteria for being considered the `best` by using `-S` (`--fo
The available fields are:
- - `hasvid`: Gives priority to formats that has a video stream
- - `hasaud`: Gives priority to formats that has a audio stream
+ - `hasvid`: Gives priority to formats that have a video stream
+ - `hasaud`: Gives priority to formats that have an audio stream
- `ie_pref`: The format preference
- `lang`: The language preference
- `quality`: The quality of the format
@@ -1519,7 +1484,7 @@ The available fields are:
- `acodec`: Audio Codec (`flac`/`alac` > `wav`/`aiff` > `opus` > `vorbis` > `aac` > `mp4a` > `mp3` > `eac3` > `ac3` > `dts` > other)
- `codec`: Equivalent to `vcodec,acodec`
- `vext`: Video Extension (`mp4` > `webm` > `flv` > other). If `--prefer-free-formats` is used, `webm` is preferred.
- - `aext`: Audio Extension (`m4a` > `aac` > `mp3` > `ogg` > `opus` > `webm` > other). If `--prefer-free-formats` is used, the order changes to `opus` > `ogg` > `webm` > `m4a` > `mp3` > `aac`.
+ - `aext`: Audio Extension (`m4a` > `aac` > `mp3` > `ogg` > `opus` > `webm` > other). If `--prefer-free-formats` is used, the order changes to `ogg` > `opus` > `webm` > `mp3` > `m4a` > `aac`
- `ext`: Equivalent to `vext,aext`
- `filesize`: Exact filesize, if known in advance
- `fs_approx`: Approximate filesize calculated from the manifests
@@ -1529,6 +1494,7 @@ The available fields are:
- `res`: Video resolution, calculated as the smallest dimension.
- `fps`: Framerate of video
- `hdr`: The dynamic range of the video (`DV` > `HDR12` > `HDR10+` > `HDR10` > `HLG` > `SDR`)
+ - `channels`: The number of audio channels
- `tbr`: Total average bitrate in KBit/s
- `vbr`: Average video bitrate in KBit/s
- `abr`: Average audio bitrate in KBit/s
@@ -1537,11 +1503,11 @@ The available fields are:
**Deprecation warning**: Many of these fields have (currently undocumented) aliases, that may be removed in a future version. It is recommended to use only the documented field names.
-All fields, unless specified otherwise, are sorted in descending order. To reverse this, prefix the field with a `+`. Eg: `+res` prefers format with the smallest resolution. Additionally, you can suffix a preferred value for the fields, separated by a `:`. Eg: `res:720` prefers larger videos, but no larger than 720p and the smallest video if there are no videos less than 720p. For `codec` and `ext`, you can provide two preferred values, the first for video and the second for audio. Eg: `+codec:avc:m4a` (equivalent to `+vcodec:avc,+acodec:m4a`) sets the video codec preference to `h264` > `h265` > `vp9` > `vp9.2` > `av01` > `vp8` > `h263` > `theora` and audio codec preference to `mp4a` > `aac` > `vorbis` > `opus` > `mp3` > `ac3` > `dts`. You can also make the sorting prefer the nearest values to the provided by using `~` as the delimiter. Eg: `filesize~1G` prefers the format with filesize closest to 1 GiB.
+All fields, unless specified otherwise, are sorted in descending order. To reverse this, prefix the field with a `+`. E.g. `+res` prefers format with the smallest resolution. Additionally, you can suffix a preferred value for the fields, separated by a `:`. E.g. `res:720` prefers larger videos, but no larger than 720p and the smallest video if there are no videos less than 720p. For `codec` and `ext`, you can provide two preferred values, the first for video and the second for audio. E.g. `+codec:avc:m4a` (equivalent to `+vcodec:avc,+acodec:m4a`) sets the video codec preference to `h264` > `h265` > `vp9` > `vp9.2` > `av01` > `vp8` > `h263` > `theora` and audio codec preference to `mp4a` > `aac` > `vorbis` > `opus` > `mp3` > `ac3` > `dts`. You can also make the sorting prefer the nearest values to the provided by using `~` as the delimiter. E.g. `filesize~1G` prefers the format with filesize closest to 1 GiB.
-The fields `hasvid` and `ie_pref` are always given highest priority in sorting, irrespective of the user-defined order. This behaviour can be changed by using `--format-sort-force`. Apart from these, the default order used is: `lang,quality,res,fps,hdr:12,codec:vp9.2,size,br,asr,proto,ext,hasaud,source,id`. The extractors may override this default order, but they cannot override the user-provided order.
+The fields `hasvid` and `ie_pref` are always given highest priority in sorting, irrespective of the user-defined order. This behaviour can be changed by using `--format-sort-force`. Apart from these, the default order used is: `lang,quality,res,fps,hdr:12,vcodec:vp9.2,channels,acodec,size,br,asr,proto,ext,hasaud,source,id`. The extractors may override this default order, but they cannot override the user-provided order.
-Note that the default has `codec:vp9.2`; i.e. `av1` is not preferred. Similarly, the default for hdr is `hdr:12`; i.e. dolby vision is not preferred. These choices are made since DV and AV1 formats are not yet fully compatible with most devices. This may be changed in the future as more devices become capable of smoothly playing back these formats.
+Note that the default has `vcodec:vp9.2`; i.e. `av1` is not preferred. Similarly, the default for hdr is `hdr:12`; i.e. dolby vision is not preferred. These choices are made since DV and AV1 formats are not yet fully compatible with most devices. This may be changed in the future as more devices become capable of smoothly playing back these formats.
If your format selector is `worst`, the last item is selected after sorting. This means it will select the format that is worst in all respects. Most of the time, what you actually want is the video with the smallest filesize instead. So it is generally better to use `-f best -S +size,+br,+res,+fps`.
@@ -1678,13 +1644,13 @@ The metadata obtained by the extractors can be modified by using `--parse-metada
The general syntax of `--parse-metadata FROM:TO` is to give the name of a field or an [output template](#output-template) to extract data from, and the format to interpret it as, separated by a colon `:`. Either a [python regular expression](https://docs.python.org/3/library/re.html#regular-expression-syntax) with named capture groups or a similar syntax to the [output template](#output-template) (only `%(field)s` formatting is supported) can be used for `TO`. The option can be used multiple times to parse and modify various fields.
-Note that any field created by this can be used in the [output template](#output-template) and will also affect the media file's metadata added when using `--add-metadata`.
+Note that any field created by this can be used in the [output template](#output-template) and will also affect the media file's metadata added when using `--embed-metadata`.
This option also has a few special uses:
-* You can download an additional URL based on the metadata of the currently downloaded video. To do this, set the field `additional_urls` to the URL that you want to download. Eg: `--parse-metadata "description:(?Phttps?://www\.vimeo\.com/\d+)` will download the first vimeo video found in the description
+* You can download an additional URL based on the metadata of the currently downloaded video. To do this, set the field `additional_urls` to the URL that you want to download. E.g. `--parse-metadata "description:(?Phttps?://www\.vimeo\.com/\d+)` will download the first vimeo video found in the description
-* You can use this to change the metadata that is embedded in the media file. To do this, set the value of the corresponding field with a `meta_` prefix. For example, any value you set to `meta_description` field will be added to the `description` field in the file. For example, you can use this to set a different "description" and "synopsis". To modify the metadata of individual streams, use the `meta_` prefix (Eg: `meta1_language`). Any value set to the `meta_` field will overwrite all default values.
+* You can use this to change the metadata that is embedded in the media file. To do this, set the value of the corresponding field with a `meta_` prefix. For example, any value you set to `meta_description` field will be added to the `description` field in the file - you can use this to set a different "description" and "synopsis". To modify the metadata of individual streams, use the `meta_` prefix (e.g. `meta1_language`). Any value set to the `meta_` field will overwrite all default values.
**Note**: Metadata modification happens before format selection, post-extraction and other post-processing operations. Some fields may be added or changed during these steps, overriding your changes.
@@ -1724,11 +1690,11 @@ $ yt-dlp --parse-metadata "description:Artist - (?P.+)"
$ yt-dlp --parse-metadata "%(series)s S%(season_number)02dE%(episode_number)02d:%(title)s"
# Prioritize uploader as the "artist" field in video metadata
-$ yt-dlp --parse-metadata "%(uploader|)s:%(meta_artist)s" --add-metadata
+$ yt-dlp --parse-metadata "%(uploader|)s:%(meta_artist)s" --embed-metadata
# Set "comment" field in video metadata using description instead of webpage_url,
# handling multiple lines correctly
-$ yt-dlp --parse-metadata "description:(?s)(?P.+)" --add-metadata
+$ yt-dlp --parse-metadata "description:(?s)(?P.+)" --embed-metadata
# Do not set any "synopsis" in the video metadata
$ yt-dlp --parse-metadata ":(?P)"
@@ -1743,39 +1709,37 @@ $ yt-dlp --replace-in-metadata "title,uploader" "[ _]" "-"
# EXTRACTOR ARGUMENTS
-Some extractors accept additional arguments which can be passed using `--extractor-args KEY:ARGS`. `ARGS` is a `;` (semicolon) separated string of `ARG=VAL1,VAL2`. Eg: `--extractor-args "youtube:player-client=android_embedded,web;include_live_dash" --extractor-args "funimation:version=uncut"`
+Some extractors accept additional arguments which can be passed using `--extractor-args KEY:ARGS`. `ARGS` is a `;` (semicolon) separated string of `ARG=VAL1,VAL2`. E.g. `--extractor-args "youtube:player-client=android_embedded,web;include_live_dash" --extractor-args "funimation:version=uncut"`
The following extractors use this feature:
#### youtube
+* `lang`: Language code to prefer translated metadata of this language (case-sensitive). By default, the video primary language metadata is preferred, with a fallback to `en` translated. See [youtube.py](https://github.com/yt-dlp/yt-dlp/blob/c26f9b991a0681fd3ea548d535919cec1fbbd430/yt_dlp/extractor/youtube.py#L381-L390) for list of supported content language codes
* `skip`: One or more of `hls`, `dash` or `translated_subs` to skip extraction of the m3u8 manifests, dash manifests and [auto-translated subtitles](https://github.com/yt-dlp/yt-dlp/issues/4090#issuecomment-1158102032) respectively
-* `player_client`: Clients to extract video data from. The main clients are `web`, `android` and `ios` with variants `_music`, `_embedded`, `_embedscreen`, `_creator` (Eg: `web_embedded`); and `mweb` and `tv_embedded` (agegate bypass) with no variants. By default, `android,web` is used, but `tv_embedded` and `creator` variants are added as required for age-gated videos. Similarly the music variants are added for `music.youtube.com` urls. You can use `all` to use all the clients, and `default` for the default clients.
+* `player_client`: Clients to extract video data from. The main clients are `web`, `android` and `ios` with variants `_music`, `_embedded`, `_embedscreen`, `_creator` (e.g. `web_embedded`); and `mweb` and `tv_embedded` (agegate bypass) with no variants. By default, `android,web` is used, but `tv_embedded` and `creator` variants are added as required for age-gated videos. Similarly, the music variants are added for `music.youtube.com` urls. You can use `all` to use all the clients, and `default` for the default clients.
* `player_skip`: Skip some network requests that are generally needed for robust extraction. One or more of `configs` (skip client configs), `webpage` (skip initial webpage), `js` (skip js player). While these options can help reduce the number of requests needed or avoid some rate-limiting, they could cause some issues. See [#860](https://github.com/yt-dlp/yt-dlp/pull/860) for more details
-* `include_live_dash`: Include live dash formats even without `--live-from-start` (These formats don't download properly)
* `comment_sort`: `top` or `new` (default) - choose comment sorting mode (on YouTube's side)
* `max_comments`: Limit the amount of comments to gather. Comma-separated list of integers representing `max-comments,max-parents,max-replies,max-replies-per-thread`. Default is `all,all,all,all`
* E.g. `all,all,1000,10` will get a maximum of 1000 replies total, with up to 10 replies per thread. `1000,all,100` will get a maximum of 1000 comments, with a maximum of 100 replies total
-* `innertube_host`: Innertube API host to use for all API requests
- * e.g. `studio.youtube.com`, `youtubei.googleapis.com`
- * Note: Cookies exported from `www.youtube.com` will not work with hosts other than `*.youtube.com`
+* `include_incomplete_formats`: Extract formats that cannot be downloaded completely (live dash and post-live m3u8)
+* `innertube_host`: Innertube API host to use for all API requests; e.g. `studio.youtube.com`, `youtubei.googleapis.com`. Note that cookies exported from one subdomain will not work on others
* `innertube_key`: Innertube API key to use for all API requests
#### youtubetab (YouTube playlists, channels, feeds, etc.)
* `skip`: One or more of `webpage` (skip initial webpage download), `authcheck` (allow the download of playlists requiring authentication when no initial webpage is downloaded. This may cause unwanted behavior, see [#1122](https://github.com/yt-dlp/yt-dlp/pull/1122) for more details)
-* `approximate_date`: Extract approximate `upload_date` in flat-playlist. This may cause date-based filters to be slightly off
+* `approximate_date`: Extract approximate `upload_date` and `timestamp` in flat-playlist. This may cause date-based filters to be slightly off
#### funimation
-* `language`: Languages to extract. Eg: `funimation:language=english,japanese`
+* `language`: Audio languages to extract, e.g. `funimation:language=english,japanese`
* `version`: The video version to extract - `uncut` or `simulcast`
#### crunchyroll
-* `language`: Languages to extract. Eg: `crunchyroll:language=jaJp`
-* `hardsub`: Which hard-sub versions to extract. Eg: `crunchyroll:hardsub=None,enUS`
+* `language`: Audio languages to extract, e.g. `crunchyroll:language=jaJp`
+* `hardsub`: Which hard-sub versions to extract, e.g. `crunchyroll:hardsub=None,enUS`
#### crunchyrollbeta
-* `format`: Which stream type(s) to extract. Default is `adaptive_hls` Eg: `crunchyrollbeta:format=vo_adaptive_hls`
- * Potentially useful values include `adaptive_hls`, `adaptive_dash`, `vo_adaptive_hls`, `vo_adaptive_dash`, `download_hls`, `download_dash`, `multitrack_adaptive_hls_v2`
-* `hardsub`: Preference order for which hardsub versions to extract. Default is `None` (no hardsubs). Eg: `crunchyrollbeta:hardsub=en-US,None`
+* `format`: Which stream type(s) to extract (default: `adaptive_hls`). Potentially useful values include `adaptive_hls`, `adaptive_dash`, `vo_adaptive_hls`, `vo_adaptive_dash`, `download_hls`, `download_dash`, `multitrack_adaptive_hls_v2`
+* `hardsub`: Preference order for which hardsub versions to extract, or `all` (default: `None` = no hardsubs), e.g. `crunchyrollbeta:hardsub=en-US,None`
#### vikichannel
* `video_types`: Types of videos to download - one or more of `episodes`, `movies`, `clips`, `trailers`
@@ -1795,11 +1759,11 @@ The following extractors use this feature:
* `dr`: dynamic range to ignore - one or more of `sdr`, `hdr10`, `dv`
#### tiktok
-* `app_version`: App version to call mobile APIs with - should be set along with `manifest_app_version`. (e.g. `20.2.1`)
-* `manifest_app_version`: Numeric app version to call mobile APIs with. (e.g. `221`)
+* `app_version`: App version to call mobile APIs with - should be set along with `manifest_app_version`, e.g. `20.2.1`
+* `manifest_app_version`: Numeric app version to call mobile APIs with, e.g. `221`
#### rokfinchannel
-* `tab`: Which tab to download. One of `new`, `top`, `videos`, `podcasts`, `streams`, `stacks`. (E.g. `rokfinchannel:tab=streams`)
+* `tab`: Which tab to download - one of `new`, `top`, `videos`, `podcasts`, `streams`, `stacks`
NOTE: These options may be changed/removed in the future without concern for backward compatibility
@@ -1819,6 +1783,8 @@ Note that **all** plugins are imported even if not invoked, and that **there are
If you are a plugin author, add [ytdlp-plugins](https://github.com/topics/ytdlp-plugins) as a topic to your repository for discoverability
+See the [wiki for some known plugins](https://github.com/yt-dlp/yt-dlp/wiki/Plugins)
+
# EMBEDDING YT-DLP
@@ -2058,12 +2024,13 @@ While these options are redundant, they are still expected to be used due to the
#### Not recommended
While these options still work, their use is not recommended since there are other alternatives to achieve the same
+ --force-generic-extractor --ies generic,default
--exec-before-download CMD --exec "before_dl:CMD"
--no-exec-before-download --no-exec
--all-formats -f all
--all-subs --sub-langs all --write-subs
--print-json -j --no-simulate
- --autonumber-size NUMBER Use string formatting. Eg: %(autonumber)03d
+ --autonumber-size NUMBER Use string formatting, e.g. %(autonumber)03d
--autonumber-start NUMBER Use internal field formatting like %(autonumber+NUMBER)s
--id -o "%(id)s.%(ext)s"
--metadata-from-title FORMAT --parse-metadata "%(title)s:FORMAT"
@@ -2142,5 +2109,5 @@ These options were deprecated since 2014 and have now been entirely removed
# CONTRIBUTING
See [CONTRIBUTING.md](CONTRIBUTING.md#contributing-to-yt-dlp) for instructions on [Opening an Issue](CONTRIBUTING.md#opening-an-issue) and [Contributing code to the project](CONTRIBUTING.md#developer-instructions)
-# MORE
-For FAQ see the [youtube-dl README](https://github.com/ytdl-org/youtube-dl#faq)
+# WIKI
+See the [Wiki](https://github.com/yt-dlp/yt-dlp/wiki) for more information
diff --git a/devscripts/__init__.py b/devscripts/__init__.py
new file mode 100644
index 0000000000..750dbdca78
--- /dev/null
+++ b/devscripts/__init__.py
@@ -0,0 +1 @@
+# Empty file needed to make devscripts.utils properly importable from outside
diff --git a/devscripts/lazy_load_template.py b/devscripts/lazy_load_template.py
index a6e26b6f63..626b85d620 100644
--- a/devscripts/lazy_load_template.py
+++ b/devscripts/lazy_load_template.py
@@ -11,14 +11,17 @@ from ..utils import (
# These bloat the lazy_extractors, so allow them to passthrough silently
ALLOWED_CLASSMETHODS = {'get_testcases', 'extract_from_webpage'}
+_WARNED = False
class LazyLoadMetaClass(type):
def __getattr__(cls, name):
- if '_real_class' not in cls.__dict__ and name not in ALLOWED_CLASSMETHODS:
- write_string(
- 'WARNING: Falling back to normal extractor since lazy extractor '
- f'{cls.__name__} does not have attribute {name}{bug_reports_message()}\n')
+ global _WARNED
+ if ('_real_class' not in cls.__dict__
+ and name not in ALLOWED_CLASSMETHODS and not _WARNED):
+ _WARNED = True
+ write_string('WARNING: Falling back to normal extractor since lazy extractor '
+ f'{cls.__name__} does not have attribute {name}{bug_reports_message()}\n')
return getattr(cls.real_class, name)
diff --git a/devscripts/make_issue_template.py b/devscripts/make_issue_template.py
index 90e7e0b43e..fd964c6c65 100644
--- a/devscripts/make_issue_template.py
+++ b/devscripts/make_issue_template.py
@@ -7,20 +7,14 @@ import sys
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
-import optparse
import re
-
-def read(fname):
- with open(fname, encoding='utf-8') as f:
- return f.read()
-
-
-# Get the version without importing the package
-def read_version(fname):
- exec(compile(read(fname), fname, 'exec'))
- return locals()['__version__']
-
+from devscripts.utils import (
+ get_filename_args,
+ read_file,
+ read_version,
+ write_file,
+)
VERBOSE_TMPL = '''
- type: checkboxes
@@ -58,20 +52,24 @@ VERBOSE_TMPL = '''
required: true
'''.strip()
+NO_SKIP = '''
+ - type: checkboxes
+ attributes:
+ label: DO NOT REMOVE OR SKIP THE ISSUE TEMPLATE
+ description: Fill all fields even if you think it is irrelevant for the issue
+ options:
+ - label: I understand that I will be **blocked** if I remove or skip any mandatory\\* field
+ required: true
+'''.strip()
+
def main():
- parser = optparse.OptionParser(usage='%prog INFILE OUTFILE')
- _, args = parser.parse_args()
- if len(args) != 2:
- parser.error('Expected an input and an output filename')
-
- fields = {'version': read_version('yt_dlp/version.py')}
+ fields = {'version': read_version(), 'no_skip': NO_SKIP}
fields['verbose'] = VERBOSE_TMPL % fields
fields['verbose_optional'] = re.sub(r'(\n\s+validations:)?\n\s+required: true', '', fields['verbose'])
- infile, outfile = args
- with open(outfile, 'w', encoding='utf-8') as outf:
- outf.write(read(infile) % fields)
+ infile, outfile = get_filename_args(has_infile=True)
+ write_file(outfile, read_file(infile) % fields)
if __name__ == '__main__':
diff --git a/devscripts/make_lazy_extractors.py b/devscripts/make_lazy_extractors.py
index c9fdfb5623..2d4530eb96 100644
--- a/devscripts/make_lazy_extractors.py
+++ b/devscripts/make_lazy_extractors.py
@@ -2,16 +2,20 @@
# Allow direct execution
import os
+import shutil
import sys
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
-import optparse
from inspect import getsource
+from devscripts.utils import get_filename_args, read_file, write_file
+
NO_ATTR = object()
-STATIC_CLASS_PROPERTIES = ['IE_NAME', 'IE_DESC', 'SEARCH_KEY', '_VALID_URL', '_WORKING', '_NETRC_MACHINE', 'age_limit']
+STATIC_CLASS_PROPERTIES = [
+ 'IE_NAME', 'IE_DESC', 'SEARCH_KEY', '_VALID_URL', '_WORKING', '_ENABLED', '_NETRC_MACHINE', 'age_limit'
+]
CLASS_METHODS = [
'ie_key', 'working', 'description', 'suitable', '_match_valid_url', '_match_id', 'get_temp_id', 'is_suitable'
]
@@ -19,17 +23,11 @@ IE_TEMPLATE = '''
class {name}({bases}):
_module = {module!r}
'''
-with open('devscripts/lazy_load_template.py', encoding='utf-8') as f:
- MODULE_TEMPLATE = f.read()
+MODULE_TEMPLATE = read_file('devscripts/lazy_load_template.py')
def main():
- parser = optparse.OptionParser(usage='%prog [OUTFILE.py]')
- args = parser.parse_args()[1] or ['yt_dlp/extractor/lazy_extractors.py']
- if len(args) != 1:
- parser.error('Expected only an output filename')
-
- lazy_extractors_filename = args[0]
+ lazy_extractors_filename = get_filename_args(default_outfile='yt_dlp/extractor/lazy_extractors.py')
if os.path.exists(lazy_extractors_filename):
os.remove(lazy_extractors_filename)
@@ -46,20 +44,20 @@ def main():
*build_ies(_ALL_CLASSES, (InfoExtractor, SearchInfoExtractor), DummyInfoExtractor),
))
- with open(lazy_extractors_filename, 'wt', encoding='utf-8') as f:
- f.write(f'{module_src}\n')
+ write_file(lazy_extractors_filename, f'{module_src}\n')
def get_all_ies():
PLUGINS_DIRNAME = 'ytdlp_plugins'
BLOCKED_DIRNAME = f'{PLUGINS_DIRNAME}_blocked'
if os.path.exists(PLUGINS_DIRNAME):
- os.rename(PLUGINS_DIRNAME, BLOCKED_DIRNAME)
+ # os.rename cannot be used, e.g. in Docker. See https://github.com/yt-dlp/yt-dlp/pull/4958
+ shutil.move(PLUGINS_DIRNAME, BLOCKED_DIRNAME)
try:
from yt_dlp.extractor.extractors import _ALL_CLASSES
finally:
if os.path.exists(BLOCKED_DIRNAME):
- os.rename(BLOCKED_DIRNAME, PLUGINS_DIRNAME)
+ shutil.move(BLOCKED_DIRNAME, PLUGINS_DIRNAME)
return _ALL_CLASSES
diff --git a/devscripts/make_readme.py b/devscripts/make_readme.py
index f2e08d7c6e..fad993a199 100755
--- a/devscripts/make_readme.py
+++ b/devscripts/make_readme.py
@@ -5,10 +5,17 @@ yt-dlp --help | make_readme.py
This must be run in a console of correct width
"""
+# Allow direct execution
+import os
+import sys
+
+sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
+
import functools
import re
-import sys
+
+from devscripts.utils import read_file, write_file
README_FILE = 'README.md'
@@ -38,6 +45,10 @@ switch_col_width = len(re.search(r'(?m)^\s{5,}', options).group())
delim = f'\n{" " * switch_col_width}'
PATCHES = (
+ ( # Standardize update message
+ r'(?m)^( -U, --update\s+).+(\n \s.+)*$',
+ r'\1Update this program to the latest version',
+ ),
( # Headings
r'(?m)^ (\w.+\n)( (?=\w))?',
r'## \1'
@@ -63,12 +74,10 @@ PATCHES = (
),
)
-with open(README_FILE, encoding='utf-8') as f:
- readme = f.read()
+readme = read_file(README_FILE)
-with open(README_FILE, 'w', encoding='utf-8') as f:
- f.write(''.join((
- take_section(readme, end=f'## {OPTIONS_START}'),
- functools.reduce(apply_patch, PATCHES, options),
- take_section(readme, f'# {OPTIONS_END}'),
- )))
+write_file(README_FILE, ''.join((
+ take_section(readme, end=f'## {OPTIONS_START}'),
+ functools.reduce(apply_patch, PATCHES, options),
+ take_section(readme, f'# {OPTIONS_END}'),
+)))
diff --git a/devscripts/make_supportedsites.py b/devscripts/make_supportedsites.py
index e46f7af565..01548ef97a 100644
--- a/devscripts/make_supportedsites.py
+++ b/devscripts/make_supportedsites.py
@@ -7,21 +7,13 @@ import sys
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
-import optparse
-
+from devscripts.utils import get_filename_args, write_file
from yt_dlp.extractor import list_extractor_classes
def main():
- parser = optparse.OptionParser(usage='%prog OUTFILE.md')
- _, args = parser.parse_args()
- if len(args) != 1:
- parser.error('Expected an output filename')
-
out = '\n'.join(ie.description() for ie in list_extractor_classes() if ie.IE_DESC is not False)
-
- with open(args[0], 'w', encoding='utf-8') as outf:
- outf.write(f'# Supported sites\n{out}\n')
+ write_file(get_filename_args(), f'# Supported sites\n{out}\n')
if __name__ == '__main__':
diff --git a/devscripts/prepare_manpage.py b/devscripts/prepare_manpage.py
index cea9349499..9b12e71e5f 100644
--- a/devscripts/prepare_manpage.py
+++ b/devscripts/prepare_manpage.py
@@ -1,9 +1,22 @@
#!/usr/bin/env python3
-import optparse
+# Allow direct execution
+import os
+import sys
+
+sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
+
+
import os.path
import re
+from devscripts.utils import (
+ compose_functions,
+ get_filename_args,
+ read_file,
+ write_file,
+)
+
ROOT_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
README_FILE = os.path.join(ROOT_DIR, 'README.md')
@@ -22,25 +35,6 @@ yt\-dlp \- A youtube-dl fork with additional features and patches
'''
-def main():
- parser = optparse.OptionParser(usage='%prog OUTFILE.md')
- _, args = parser.parse_args()
- if len(args) != 1:
- parser.error('Expected an output filename')
-
- outfile, = args
-
- with open(README_FILE, encoding='utf-8') as f:
- readme = f.read()
-
- readme = filter_excluded_sections(readme)
- readme = move_sections(readme)
- readme = filter_options(readme)
-
- with open(outfile, 'w', encoding='utf-8') as outf:
- outf.write(PREFIX + readme)
-
-
def filter_excluded_sections(readme):
EXCLUDED_SECTION_BEGIN_STRING = re.escape('')
EXCLUDED_SECTION_END_STRING = re.escape('')
@@ -92,5 +86,12 @@ def filter_options(readme):
return readme.replace(section, options, 1)
+TRANSFORM = compose_functions(filter_excluded_sections, move_sections, filter_options)
+
+
+def main():
+ write_file(get_filename_args(), PREFIX + TRANSFORM(read_file(README_FILE)))
+
+
if __name__ == '__main__':
main()
diff --git a/devscripts/run_tests.sh b/devscripts/run_tests.sh
index d496a092b1..faa642e96c 100755
--- a/devscripts/run_tests.sh
+++ b/devscripts/run_tests.sh
@@ -1,13 +1,13 @@
#!/usr/bin/env sh
-if [ -z $1 ]; then
+if [ -z "$1" ]; then
test_set='test'
-elif [ $1 = 'core' ]; then
+elif [ "$1" = 'core' ]; then
test_set="-m not download"
-elif [ $1 = 'download' ]; then
+elif [ "$1" = 'download' ]; then
test_set="-m download"
else
- echo 'Invalid test type "'$1'". Use "core" | "download"'
+ echo 'Invalid test type "'"$1"'". Use "core" | "download"'
exit 1
fi
diff --git a/devscripts/set-variant.py b/devscripts/set-variant.py
new file mode 100644
index 0000000000..10341e7444
--- /dev/null
+++ b/devscripts/set-variant.py
@@ -0,0 +1,36 @@
+#!/usr/bin/env python3
+
+# Allow direct execution
+import os
+import sys
+
+sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
+
+
+import argparse
+import functools
+import re
+
+from devscripts.utils import compose_functions, read_file, write_file
+
+VERSION_FILE = 'yt_dlp/version.py'
+
+
+def parse_options():
+ parser = argparse.ArgumentParser(description='Set the build variant of the package')
+ parser.add_argument('variant', help='Name of the variant')
+ parser.add_argument('-M', '--update-message', default=None, help='Message to show in -U')
+ return parser.parse_args()
+
+
+def property_setter(name, value):
+ return functools.partial(re.sub, rf'(?m)^{name}\s*=\s*.+$', f'{name} = {value!r}')
+
+
+opts = parse_options()
+transform = compose_functions(
+ property_setter('VARIANT', opts.variant),
+ property_setter('UPDATE_HINT', opts.update_message)
+)
+
+write_file(VERSION_FILE, transform(read_file(VERSION_FILE)))
diff --git a/devscripts/update-formulae.py b/devscripts/update-formulae.py
index 96b56b9324..e79297f530 100644
--- a/devscripts/update-formulae.py
+++ b/devscripts/update-formulae.py
@@ -1,5 +1,10 @@
#!/usr/bin/env python3
+"""
+Usage: python3 ./devscripts/update-formulae.py
+version can be either 0-aligned (yt-dlp version) or normalized (PyPi version)
+"""
+
# Allow direct execution
import os
import sys
@@ -11,8 +16,7 @@ import json
import re
import urllib.request
-# usage: python3 ./devscripts/update-formulae.py
-# version can be either 0-aligned (yt-dlp version) or normalized (PyPl version)
+from devscripts.utils import read_file, write_file
filename, version = sys.argv[1:]
@@ -27,11 +31,9 @@ tarball_file = next(x for x in pypi_release['urls'] if x['filename'].endswith('.
sha256sum = tarball_file['digests']['sha256']
url = tarball_file['url']
-with open(filename) as r:
- formulae_text = r.read()
+formulae_text = read_file(filename)
formulae_text = re.sub(r'sha256 "[0-9a-f]*?"', 'sha256 "%s"' % sha256sum, formulae_text, count=1)
formulae_text = re.sub(r'url "[^"]*?"', 'url "%s"' % url, formulae_text, count=1)
-with open(filename, 'w') as w:
- w.write(formulae_text)
+write_file(filename, formulae_text)
diff --git a/devscripts/update-version.py b/devscripts/update-version.py
index c5bc83de93..caebf42414 100644
--- a/devscripts/update-version.py
+++ b/devscripts/update-version.py
@@ -7,32 +7,35 @@ import sys
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
+import contextlib
import subprocess
import sys
from datetime import datetime
-with open('yt_dlp/version.py') as f:
- exec(compile(f.read(), 'yt_dlp/version.py', 'exec'))
-old_version = locals()['__version__']
+from devscripts.utils import read_version, write_file
-old_version_list = old_version.split('.')
-old_ver = '.'.join(old_version_list[:3])
-old_rev = old_version_list[3] if len(old_version_list) > 3 else ''
+def get_new_version(revision):
+ version = datetime.utcnow().strftime('%Y.%m.%d')
-ver = datetime.utcnow().strftime("%Y.%m.%d")
+ if revision:
+ assert revision.isdigit(), 'Revision must be a number'
+ else:
+ old_version = read_version().split('.')
+ if version.split('.') == old_version[:3]:
+ revision = str(int((old_version + [0])[3]) + 1)
-rev = (sys.argv[1:] or [''])[0] # Use first argument, if present as revision number
-if not rev:
- rev = str(int(old_rev or 0) + 1) if old_ver == ver else ''
+ return f'{version}.{revision}' if revision else version
-VERSION = '.'.join((ver, rev)) if rev else ver
-try:
- sp = subprocess.Popen(['git', 'rev-parse', '--short', 'HEAD'], stdout=subprocess.PIPE)
- GIT_HEAD = sp.communicate()[0].decode().strip() or None
-except Exception:
- GIT_HEAD = None
+def get_git_head():
+ with contextlib.suppress(Exception):
+ sp = subprocess.Popen(['git', 'rev-parse', '--short', 'HEAD'], stdout=subprocess.PIPE)
+ return sp.communicate()[0].decode().strip() or None
+
+
+VERSION = get_new_version((sys.argv + [''])[1])
+GIT_HEAD = get_git_head()
VERSION_FILE = f'''\
# Autogenerated by devscripts/update-version.py
@@ -40,10 +43,12 @@ VERSION_FILE = f'''\
__version__ = {VERSION!r}
RELEASE_GIT_HEAD = {GIT_HEAD!r}
+
+VARIANT = None
+
+UPDATE_HINT = None
'''
-with open('yt_dlp/version.py', 'wt') as f:
- f.write(VERSION_FILE)
-
-print('::set-output name=ytdlp_version::' + VERSION)
+write_file('yt_dlp/version.py', VERSION_FILE)
+print(f'::set-output name=ytdlp_version::{VERSION}')
print(f'\nVersion = {VERSION}, Git HEAD = {GIT_HEAD}')
diff --git a/devscripts/utils.py b/devscripts/utils.py
new file mode 100644
index 0000000000..aa17a5f7f6
--- /dev/null
+++ b/devscripts/utils.py
@@ -0,0 +1,35 @@
+import argparse
+import functools
+
+
+def read_file(fname):
+ with open(fname, encoding='utf-8') as f:
+ return f.read()
+
+
+def write_file(fname, content):
+ with open(fname, 'w', encoding='utf-8') as f:
+ return f.write(content)
+
+
+# Get the version without importing the package
+def read_version(fname='yt_dlp/version.py'):
+ exec(compile(read_file(fname), fname, 'exec'))
+ return locals()['__version__']
+
+
+def get_filename_args(has_infile=False, default_outfile=None):
+ parser = argparse.ArgumentParser()
+ if has_infile:
+ parser.add_argument('infile', help='Input file')
+ kwargs = {'nargs': '?', 'default': default_outfile} if default_outfile else {}
+ parser.add_argument('outfile', **kwargs, help='Output file')
+
+ opts = parser.parse_args()
+ if has_infile:
+ return opts.infile, opts.outfile
+ return opts.outfile
+
+
+def compose_functions(*functions):
+ return lambda x: functools.reduce(lambda y, f: f(y), functions, x)
diff --git a/pyinst.py b/pyinst.py
index 31854e881c..0b7c66a30f 100644
--- a/pyinst.py
+++ b/pyinst.py
@@ -1,11 +1,17 @@
#!/usr/bin/env python3
+# Allow direct execution
import os
-import platform
import sys
+sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
+
+import platform
+
from PyInstaller.__main__ import run as run_pyinstaller
+from devscripts.utils import read_version
+
OS_NAME, MACHINE, ARCH = sys.platform, platform.machine(), platform.architecture()[0][:2]
if MACHINE in ('x86_64', 'AMD64') or ('i' in MACHINE and '86' in MACHINE):
# NB: Windows x86 has MACHINE = AMD64 irrespective of bitness
@@ -13,8 +19,7 @@ if MACHINE in ('x86_64', 'AMD64') or ('i' in MACHINE and '86' in MACHINE):
def main():
- opts = parse_options()
- version = read_version('yt_dlp/version.py')
+ opts, version = parse_options(), read_version()
onedir = '--onedir' in opts or '-D' in opts
if not onedir and '-F' not in opts and '--onefile' not in opts:
@@ -53,13 +58,6 @@ def parse_options():
return opts
-# Get the version from yt_dlp/version.py without importing the package
-def read_version(fname):
- with open(fname, encoding='utf-8') as f:
- exec(compile(f.read(), fname, 'exec'))
- return locals()['__version__']
-
-
def exe(onedir):
"""@returns (name, path)"""
name = '_'.join(filter(None, (
@@ -83,7 +81,7 @@ def version_to_list(version):
def dependency_options():
# Due to the current implementation, these are auto-detected, but explicitly add them just in case
dependencies = [pycryptodome_module(), 'mutagen', 'brotli', 'certifi', 'websockets']
- excluded_modules = ['test', 'ytdlp_plugins', 'youtube_dl', 'youtube_dlc']
+ excluded_modules = ('youtube_dl', 'youtube_dlc', 'test', 'ytdlp_plugins', 'devscripts')
yield from (f'--hidden-import={module}' for module in dependencies)
yield '--collect-submodules=websockets'
diff --git a/setup.cfg b/setup.cfg
index d33c7d8549..2def390f51 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -10,6 +10,14 @@ per_file_ignores =
devscripts/lazy_load_template.py: F401
+[autoflake]
+ignore-init-module-imports = true
+ignore-pass-after-docstring = true
+remove-all-unused-imports = true
+remove-duplicate-keys = true
+remove-unused-variables = true
+
+
[tool:pytest]
addopts = -ra -v --strict-markers
markers =
diff --git a/setup.py b/setup.py
index dab09c268c..3641dfae95 100644
--- a/setup.py
+++ b/setup.py
@@ -12,37 +12,26 @@ except ImportError:
from distutils.core import Command, setup
setuptools_available = False
+from devscripts.utils import read_file, read_version
-def read(fname):
- with open(fname, encoding='utf-8') as f:
- return f.read()
-
-
-# Get the version from yt_dlp/version.py without importing the package
-def read_version(fname):
- exec(compile(read(fname), fname, 'exec'))
- return locals()['__version__']
-
-
-VERSION = read_version('yt_dlp/version.py')
+VERSION = read_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('README.md')))
+ read_file('README.md')))
-REQUIREMENTS = read('requirements.txt').splitlines()
+REQUIREMENTS = read_file('requirements.txt').splitlines()
def packages():
if setuptools_available:
- return find_packages(exclude=('youtube_dl', 'youtube_dlc', 'test', 'ytdlp_plugins'))
+ 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',
- 'yt_dlp.extractor.anvato_token_generator',
]
@@ -121,7 +110,7 @@ class build_lazy_extractors(Command):
if self.dry_run:
print('Skipping build of lazy extractors in dry run mode')
return
- subprocess.run([sys.executable, 'devscripts/make_lazy_extractors.py', 'yt_dlp/extractor/lazy_extractors.py'])
+ subprocess.run([sys.executable, 'devscripts/make_lazy_extractors.py'])
params = py2exe_params() if sys.argv[1:2] == ['py2exe'] else build_params()
diff --git a/supportedsites.md b/supportedsites.md
index d23e46e3dc..48888f61fa 100644
--- a/supportedsites.md
+++ b/supportedsites.md
@@ -3,11 +3,12 @@
- **0000studio:clip**
- **17live**
- **17live:clip**
+ - **1News**: 1news.co.nz article videos
- **1tv**: Первый канал
- - **20.detik.com**
- **20min**
- **23video**
- **247sports**
+ - **24tv.ua**
- **24video**
- **3qsdn**: 3Q SDN
- **3sat**
@@ -18,7 +19,7 @@
- **8tracks**
- **91porn**
- **9c9media**
- - **9gag**
+ - **9gag**: 9GAG
- **9now.com.au**
- **abc.net.au**
- **abc.net.au:iview**
@@ -64,8 +65,8 @@
- **AmericasTestKitchenSeason**
- **AmHistoryChannel**
- **anderetijden**: npo.nl, ntr.nl, omroepwnl.nl, zapp.nl and npo3.nl
+ - **Angel**
- **AnimalPlanet**
- - **AnimeOnDemand**: [animeondemand]
- **ant1newsgr:article**: ant1news.gr articles
- **ant1newsgr:embed**: ant1news.gr embedded videos
- **ant1newsgr:watch**: ant1news.gr videos
@@ -127,11 +128,14 @@
- **bbc.co.uk:iplayer:group**
- **bbc.co.uk:playlist**
- **BBVTV**: [bbvtv]
+ - **BBVTVLive**: [bbvtv]
+ - **BBVTVRecordings**: [bbvtv]
- **Beatport**
- **Beeg**
- **BehindKink**
- **Bellator**
- **BellMedia**
+ - **BerufeTV**
- **Bet**
- **bfi:player**
- **bfmtv**
@@ -145,9 +149,11 @@
- **Bilibili category extractor**
- **BilibiliAudio**
- **BilibiliAudioAlbum**
- - **BilibiliChannel**
- **BiliBiliPlayer**
- **BiliBiliSearch**: Bilibili video search; "bilisearch:" prefix
+ - **BilibiliSpaceAudio**
+ - **BilibiliSpacePlaylist**
+ - **BilibiliSpaceVideo**
- **BiliIntl**: [biliintl]
- **BiliIntlSeries**: [biliintl]
- **BiliLive**
@@ -165,6 +171,7 @@
- **Bloomberg**
- **BokeCC**
- **BongaCams**
+ - **BooyahClips**
- **BostonGlobe**
- **Box**
- **Bpb**: Bundeszentrale für politische Bildung
@@ -177,6 +184,7 @@
- **BRMediathek**: Bayerischer Rundfunk Mediathek
- **bt:article**: Bergens Tidende Articles
- **bt:vestlendingen**: Bergens Tidende - Vestlendingen
+ - **Bundesliga**
- **BusinessInsider**
- **BuzzFeed**
- **BYUtv**
@@ -187,6 +195,7 @@
- **Camdemy**
- **CamdemyFolder**
- **CamModels**
+ - **CamtasiaEmbed**
- **CamWithHer**
- **CanalAlpha**
- **canalc2.tv**
@@ -232,6 +241,7 @@
- **Clippit**
- **ClipRs**
- **Clipsyndicate**
+ - **ClipYouEmbed**
- **CloserToTruth**
- **CloudflareStream**
- **Cloudy**
@@ -243,6 +253,7 @@
- **CNN**
- **CNNArticle**
- **CNNBlogs**
+ - **CNNIndonesia**
- **ComedyCentral**
- **ComedyCentralTV**
- **CondeNast**: Condé Nast media group: Allure, Architectural Digest, Ars Technica, Bon Appétit, Brides, Condé Nast, Condé Nast Traveler, Details, Epicurious, GQ, Glamour, Golf Digest, SELF, Teen Vogue, The New Yorker, Vanity Fair, Vogue, W Magazine, WIRED
@@ -299,6 +310,7 @@
- **defense.gouv.fr**
- **democracynow**
- **DestinationAmerica**
+ - **DetikEmbed**
- **DHM**: Filmarchiv - Deutsches Historisches Museum
- **Digg**
- **DigitalConcertHall**: [digitalconcerthall] DigitalConcertHall extractor
@@ -345,6 +357,8 @@
- **ehftv**
- **eHow**
- **EinsUndEinsTV**: [1und1tv]
+ - **EinsUndEinsTVLive**: [1und1tv]
+ - **EinsUndEinsTVRecordings**: [1und1tv]
- **Einthusan**
- **eitb.tv**
- **EllenTube**
@@ -357,6 +371,7 @@
- **Engadget**
- **Epicon**
- **EpiconSeries**
+ - **Epoch**
- **Eporner**
- **EroProfile**: [eroprofile]
- **EroProfile:album**
@@ -370,13 +385,17 @@
- **EsriVideo**
- **Europa**
- **EuropeanTour**
+ - **Eurosport**
- **EUScreen**
- **EWETV**: [ewetv]
+ - **EWETVLive**: [ewetv]
+ - **EWETVRecordings**: [ewetv]
- **ExpoTV**
- **Expressen**
- **ExtremeTube**
- **EyedoTV**
- **facebook**: [facebook]
+ - **facebook:reel**
- **FacebookPluginsVideo**
- **fancode:live**: [fancode]
- **fancode:vod**: [fancode]
@@ -450,6 +469,8 @@
- **GiantBomb**
- **Giga**
- **GlattvisionTV**: [glattvisiontv]
+ - **GlattvisionTVLive**: [glattvisiontv]
+ - **GlattvisionTVRecordings**: [glattvisiontv]
- **Glide**: Glide mobile video messages (glide.me)
- **Globo**: [globo]
- **GloboArticle**
@@ -465,6 +486,7 @@
- **google:podcasts:feed**
- **GoogleDrive**
- **GoogleDrive:Folder**
+ - **GoPlay**: [goplay]
- **GoPro**
- **Goshgay**
- **GoToStage**
@@ -473,6 +495,7 @@
- **gronkh:feed**
- **gronkh:vods**
- **Groupon**
+ - **Harpodeon**
- **hbo**
- **HearThisAt**
- **Heise**
@@ -491,6 +514,7 @@
- **hitbox:live**
- **HitRecord**
- **hketv**: 香港教育局教育電視 (HKETV) Educational Television, Hong Kong Educational Bureau
+ - **Holodex**
- **HotNewHipHop**
- **hotstar**
- **hotstar:playlist**
@@ -502,6 +526,7 @@
- **HRTiPlaylist**: [hrti]
- **HSEProduct**
- **HSEShow**
+ - **html5**
- **Huajiao**: 花椒直播
- **HuffPost**: Huffington Post
- **Hungama**
@@ -511,11 +536,14 @@
- **Hypem**
- **Hytale**
- **Icareus**
+ - **iflix:episode**
+ - **IflixSeries**
- **ign.com**
- **IGNArticle**
- **IGNVideo**
- **IHeartRadio**
- **iheartradio:podcast**
+ - **Iltalehti**
- **imdb**: Internet Movie Database trailers
- **imdb:list**: Internet Movie Database lists
- **Imgur**
@@ -538,6 +566,9 @@
- **iq.com**: International version of iQiyi
- **iq.com:album**
- **iqiyi**: [iqiyi] 爱奇艺
+ - **IslamChannel**
+ - **IslamChannelSeries**
+ - **IsraelNationalNews**
- **ITProTV**
- **ITProTVCourse**
- **ITTF**
@@ -573,6 +604,7 @@
- **KickStarter**
- **KinjaEmbed**
- **KinoPoisk**
+ - **KompasVideo**
- **KonserthusetPlay**
- **Koo**
- **KrasView**: Красвью
@@ -669,6 +701,7 @@
- **Mediasite**
- **MediasiteCatalog**
- **MediasiteNamedCatalog**
+ - **MediaWorksNZVOD**
- **Medici**
- **megaphone.fm**: megaphone.fm embedded players
- **megatvcom**: megatv.com videos
@@ -681,6 +714,7 @@
- **mewatch**
- **Mgoon**
- **MiaoPai**
+ - **MicrosoftEmbed**
- **microsoftstream**: Microsoft Stream
- **mildom**: Record ongoing live by specific user in Mildom
- **mildom:clip**: Clip in Mildom
@@ -702,10 +736,13 @@
- **mixcloud:playlist**
- **mixcloud:user**
- **MLB**
+ - **MLBTV**: [mlb]
- **MLBVideo**
- **MLSSoccer**
- **Mnet**
- **MNetTV**: [mnettv]
+ - **MNetTVLive**: [mnettv]
+ - **MNetTVRecordings**: [mnettv]
- **MochaVideo**
- **MoeVideo**: LetitBit video services: moevideo.net, playreplay.net and videochart.net
- **Mofosex**
@@ -715,9 +752,11 @@
- **Motherless**
- **MotherlessGroup**
- **Motorsport**: motorsport.com
+ - **MotorTrend**
- **MovieClips**
- **MovieFap**
- **Moviepilot**
+ - **MoviewPlay**
- **Moviezine**
- **MovingImage**
- **MSN**
@@ -775,6 +814,7 @@
- **NBCSports**
- **NBCSportsStream**
- **NBCSportsVPlayer**
+ - **NBCStations**
- **ndr**: NDR.de - Norddeutscher Rundfunk
- **ndr:embed**
- **ndr:embed:base**
@@ -790,13 +830,16 @@
- **netease:program**: 网易云音乐 - 电台节目
- **netease:singer**: 网易云音乐 - 歌手
- **netease:song**: 网易云音乐
- - **NetPlus**: [netplus]
+ - **NetPlusTV**: [netplus]
+ - **NetPlusTVLive**: [netplus]
+ - **NetPlusTVRecordings**: [netplus]
- **Netverse**
- **NetversePlaylist**
- **Netzkino**
- **Newgrounds**
- **Newgrounds:playlist**
- **Newgrounds:user**
+ - **NewsPicks**
- **Newstube**
- **Newsy**
- **NextMedia**: 蘋果日報
@@ -806,8 +849,8 @@
- **NexxEmbed**
- **NFB**
- **NFHSNetwork**
- - **nfl.com**: (**Currently broken**)
- - **nfl.com:article**: (**Currently broken**)
+ - **nfl.com**
+ - **nfl.com:article**
- **NhkForSchoolBangumi**
- **NhkForSchoolProgramList**
- **NhkForSchoolSubject**: Portal page for each school subjects, like Japanese (kokugo, 国語) or math (sansuu/suugaku or 算数・数学)
@@ -890,22 +933,13 @@
- **openrec:capture**
- **openrec:movie**
- **OraTV**
- - **orf:burgenland**: Radio Burgenland
- - **orf:fm4**: radio FM4
- **orf:fm4:story**: fm4.orf.at stories
- **orf:iptv**: iptv.ORF.at
- - **orf:kaernten**: Radio Kärnten
- - **orf:noe**: Radio Niederösterreich
- - **orf:oberoesterreich**: Radio Oberösterreich
- - **orf:oe1**: Radio Österreich 1
- - **orf:oe3**: Radio Österreich 3
- - **orf:salzburg**: Radio Salzburg
- - **orf:steiermark**: Radio Steiermark
- - **orf:tirol**: Radio Tirol
+ - **orf:radio**
- **orf:tvthek**: ORF TVthek
- - **orf:vorarlberg**: Radio Vorarlberg
- - **orf:wien**: Radio Wien
- **OsnatelTV**: [osnateltv]
+ - **OsnatelTVLive**: [osnateltv]
+ - **OsnatelTVRecordings**: [osnateltv]
- **OutsideTV**
- **PacktPub**: [packtpub]
- **PacktPubCourse**
@@ -919,10 +953,11 @@
- **ParamountNetwork**
- **ParamountPlus**
- **ParamountPlusSeries**
+ - **Parler**: Posts on parler.com
- **parliamentlive.tv**: UK parliament videos
- **Parlview**
- **Patreon**
- - **PatreonUser**
+ - **PatreonCampaign**
- **pbs**: Public Broadcasting Service (PBS) and member stations: PBS: Public Broadcasting Service, APT - Alabama Public Television (WBIQ), GPB/Georgia Public Broadcasting (WGTV), Mississippi Public Broadcasting (WMPN), Nashville Public Television (WNPT), WFSU-TV (WFSU), WSRE (WSRE), WTCI (WTCI), WPBA/Channel 30 (WPBA), Alaska Public Media (KAKM), Arizona PBS (KAET), KNME-TV/Channel 5 (KNME), Vegas PBS (KLVX), AETN/ARKANSAS ETV NETWORK (KETS), KET (WKLE), WKNO/Channel 10 (WKNO), LPB/LOUISIANA PUBLIC BROADCASTING (WLPB), OETA (KETA), Ozarks Public Television (KOZK), WSIU Public Broadcasting (WSIU), KEET TV (KEET), KIXE/Channel 9 (KIXE), KPBS San Diego (KPBS), KQED (KQED), KVIE Public Television (KVIE), PBS SoCal/KOCE (KOCE), ValleyPBS (KVPT), CONNECTICUT PUBLIC TELEVISION (WEDH), KNPB Channel 5 (KNPB), SOPTV (KSYS), Rocky Mountain PBS (KRMA), KENW-TV3 (KENW), KUED Channel 7 (KUED), Wyoming PBS (KCWC), Colorado Public Television / KBDI 12 (KBDI), KBYU-TV (KBYU), Thirteen/WNET New York (WNET), WGBH/Channel 2 (WGBH), WGBY (WGBY), NJTV Public Media NJ (WNJT), WLIW21 (WLIW), mpt/Maryland Public Television (WMPB), WETA Television and Radio (WETA), WHYY (WHYY), PBS 39 (WLVT), WVPT - Your Source for PBS and More! (WVPT), Howard University Television (WHUT), WEDU PBS (WEDU), WGCU Public Media (WGCU), WPBT2 (WPBT), WUCF TV (WUCF), WUFT/Channel 5 (WUFT), WXEL/Channel 42 (WXEL), WLRN/Channel 17 (WLRN), WUSF Public Broadcasting (WUSF), ETV (WRLK), UNC-TV (WUNC), PBS Hawaii - Oceanic Cable Channel 10 (KHET), Idaho Public Television (KAID), KSPS (KSPS), OPB (KOPB), KWSU/Channel 10 & KTNW/Channel 31 (KWSU), WILL-TV (WILL), Network Knowledge - WSEC/Springfield (WSEC), WTTW11 (WTTW), Iowa Public Television/IPTV (KDIN), Nine Network (KETC), PBS39 Fort Wayne (WFWA), WFYI Indianapolis (WFYI), Milwaukee Public Television (WMVS), WNIN (WNIN), WNIT Public Television (WNIT), WPT (WPNE), WVUT/Channel 22 (WVUT), WEIU/Channel 51 (WEIU), WQPT-TV (WQPT), WYCC PBS Chicago (WYCC), WIPB-TV (WIPB), WTIU (WTIU), CET (WCET), ThinkTVNetwork (WPTD), WBGU-TV (WBGU), WGVU TV (WGVU), NET1 (KUON), Pioneer Public Television (KWCM), SDPB Television (KUSD), TPT (KTCA), KSMQ (KSMQ), KPTS/Channel 8 (KPTS), KTWU/Channel 11 (KTWU), East Tennessee PBS (WSJK), WCTE-TV (WCTE), WLJT, Channel 11 (WLJT), WOSU TV (WOSU), WOUB/WOUC (WOUB), WVPB (WVPB), WKYU-PBS (WKYU), KERA 13 (KERA), MPBN (WCBB), Mountain Lake PBS (WCFE), NHPTV (WENH), Vermont PBS (WETK), witf (WITF), WQED Multimedia (WQED), WMHT Educational Telecommunications (WMHT), Q-TV (WDCQ), WTVS Detroit Public TV (WTVS), CMU Public Television (WCMU), WKAR-TV (WKAR), WNMU-TV Public TV 13 (WNMU), WDSE - WRPT (WDSE), WGTE TV (WGTE), Lakeland Public Television (KAWE), KMOS-TV - Channels 6.1, 6.2 and 6.3 (KMOS), MontanaPBS (KUSM), KRWG/Channel 22 (KRWG), KACV (KACV), KCOS/Channel 13 (KCOS), WCNY/Channel 24 (WCNY), WNED (WNED), WPBS (WPBS), WSKG Public TV (WSKG), WXXI (WXXI), WPSU (WPSU), WVIA Public Media Studios (WVIA), WTVI (WTVI), Western Reserve PBS (WNEO), WVIZ/PBS ideastream (WVIZ), KCTS 9 (KCTS), Basin PBS (KPBT), KUHT / Channel 8 (KUHT), KLRN (KLRN), KLRU (KLRU), WTJX Channel 12 (WTJX), WCVE PBS (WCVE), KBTC Public Television (KBTC)
- **PearVideo**
- **PeekVids**
@@ -993,6 +1028,7 @@
- **PornoVoisines**
- **PornoXO**
- **PornTube**
+ - **PrankCast**
- **PremiershipRugby**
- **PressTV**
- **ProjectVeritas**
@@ -1012,6 +1048,8 @@
- **qqmusic:singer**: QQ音乐 - 歌手
- **qqmusic:toplist**: QQ音乐 - 排行榜
- **QuantumTV**: [quantumtv]
+ - **QuantumTVLive**: [quantumtv]
+ - **QuantumTVRecordings**: [quantumtv]
- **Qub**
- **R7**
- **R7Article**
@@ -1030,12 +1068,14 @@
- **radlive:channel**
- **radlive:season**
- **Rai**
+ - **RaiNews**
- **RaiPlay**
- **RaiPlayLive**
- **RaiPlayPlaylist**
- **RaiPlaySound**
- **RaiPlaySoundLive**
- **RaiPlaySoundPlaylist**
+ - **RaiSudtirol**
- **RayWenderlich**
- **RayWenderlichCourse**
- **RBMARadio**
@@ -1072,7 +1112,7 @@
- **RoosterTeethSeries**: [roosterteeth]
- **RottenTomatoes**
- **Rozhlas**
- - **RTBF**
+ - **RTBF**: [rtbf]
- **RTDocumentry**
- **RTDocumentryPlaylist**
- **rte**: Raidió Teilifís Éireann TV
@@ -1118,7 +1158,11 @@
- **safari:course**: [safari] safaribooksonline.com online courses
- **Saitosan**
- **SAKTV**: [saktv]
+ - **SAKTVLive**: [saktv]
+ - **SAKTVRecordings**: [saktv]
- **SaltTV**: [salttv]
+ - **SaltTVLive**: [salttv]
+ - **SaltTVRecordings**: [salttv]
- **SampleFocus**
- **Sapo**: SAPO Vídeos
- **savefrom.net**
@@ -1144,6 +1188,7 @@
- **Shahid**: [shahid]
- **ShahidShow**
- **Shared**: shared.sx
+ - **ShareVideosEmbed**
- **ShemarooMe**
- **ShowRoomLive**
- **simplecast**
@@ -1164,6 +1209,7 @@
- **Slideshare**
- **SlidesLive**
- **Slutload**
+ - **Smotrim**
- **Snotr**
- **Sohu**
- **SonyLIV**: [sonyliv]
@@ -1193,8 +1239,8 @@
- **Sport5**
- **SportBox**
- **SportDeutschland**
- - **spotify**: Spotify episodes
- - **spotify:show**: Spotify shows
+ - **spotify**: Spotify episodes (**Currently broken**)
+ - **spotify:show**: Spotify shows (**Currently broken**)
- **Spreaker**
- **SpreakerPage**
- **SpreakerShow**
@@ -1268,6 +1314,7 @@
- **TeleQuebecVideo**
- **TeleTask**
- **Telewebion**
+ - **Tempo**
- **TennisTV**: [tennistv]
- **TenPlay**: [10play]
- **TF1**
@@ -1287,10 +1334,10 @@
- **ThreeSpeak**
- **ThreeSpeakUser**
- **TikTok**
- - **tiktok:effect**
- - **tiktok:sound**
- - **tiktok:tag**
- - **tiktok:user**
+ - **tiktok:effect**: (**Currently broken**)
+ - **tiktok:sound**: (**Currently broken**)
+ - **tiktok:tag**: (**Currently broken**)
+ - **tiktok:user**: (**Currently broken**)
- **tinypic**: tinypic.com videos
- **TLC**
- **TMZ**
@@ -1306,6 +1353,8 @@
- **ToypicsUser**: Toypics user profile
- **TrailerAddict**: (**Currently broken**)
- **TravelChannel**
+ - **Triller**: [triller]
+ - **TrillerUser**: [triller]
- **Trilulilu**
- **Trovo**
- **TrovoChannelClip**: All Clips of a trovo.live channel; "trovoclip:" prefix
@@ -1313,6 +1362,7 @@
- **TrovoVod**
- **TrueID**
- **TruNews**
+ - **Truth**
- **TruTV**
- **Tube8**
- **TubeTuGraz**: [tubetugraz] tube.tugraz.at
@@ -1328,6 +1378,7 @@
- **Turbo**
- **tv.dfb.de**
- **TV2**
+ - **TV24UAGenericPassthrough**
- **TV2Article**
- **TV2DK**
- **TV2DKBornholmPlay**
@@ -1390,6 +1441,7 @@
- **umg:de**: Universal Music Deutschland
- **Unistra**
- **Unity**
+ - **UnscriptedNewsVideo**
- **uol.com.br**
- **uplynk**
- **uplynk:preplay**
@@ -1434,8 +1486,6 @@
- **VidioLive**: [vidio]
- **VidioPremier**: [vidio]
- **VidLii**
- - **vier**: [vier] vier.be and vijf.be
- - **vier:videos**
- **viewlift**
- **viewlift:embed**
- **Viidea**
@@ -1480,6 +1530,8 @@
- **VoxMedia**
- **VoxMediaVolume**
- **vpro**: npo.nl, ntr.nl, omroepwnl.nl, zapp.nl and npo3.nl
+ - **vqq:series**
+ - **vqq:video**
- **Vrak**
- **VRT**: VRT NWS, Flanders News, Flandern Info and Sporza
- **VrtNU**: [vrtnu] VrtNU.be
@@ -1488,6 +1540,8 @@
- **VShare**
- **VTM**
- **VTXTV**: [vtxtv]
+ - **VTXTVLive**: [vtxtv]
+ - **VTXTVRecordings**: [vtxtv]
- **VuClip**
- **Vupload**
- **VVVVID**
@@ -1497,6 +1551,8 @@
- **Wakanim**
- **Walla**
- **WalyTV**: [walytv]
+ - **WalyTVLive**: [walytv]
+ - **WalyTVRecordings**: [walytv]
- **wasdtv:clip**
- **wasdtv:record**
- **wasdtv:stream**
@@ -1525,8 +1581,10 @@
- **Willow**
- **WimTV**
- **Wistia**
+ - **WistiaChannel**
- **WistiaPlaylist**
- **wnl**: npo.nl, ntr.nl, omroepwnl.nl, zapp.nl and npo3.nl
+ - **wordpress:playlist**
- **WorldStarHipHop**
- **wppilot**
- **wppilot:channels**
@@ -1583,13 +1641,14 @@
- **youtube:clip**
- **youtube:favorites**: YouTube liked videos; ":ytfav" keyword (requires cookies)
- **youtube:history**: Youtube watch history; ":ythis" keyword (requires cookies)
- - **youtube:music:search_url**: YouTube music search URLs with selectable sections (Eg: #songs)
+ - **youtube:music:search_url**: YouTube music search URLs with selectable sections, e.g. #songs
- **youtube:notif**: YouTube notifications; ":ytnotif" keyword (requires cookies)
- **youtube:playlist**: YouTube playlists
- **youtube:recommended**: YouTube recommended videos; ":ytrec" keyword
- **youtube:search**: YouTube search; "ytsearch:" prefix
- **youtube:search:date**: YouTube search, newest videos first; "ytsearchdate:" prefix
- **youtube:search_url**: YouTube search URLs with sorting and filter support
+ - **youtube:shorts:pivot:audio**: YouTube Shorts audio pivot (Shorts using audio of a given video)
- **youtube:stories**: YouTube channel stories; "ytstories:" prefix
- **youtube:subscriptions**: YouTube subscriptions feed; ":ytsubs" keyword (requires cookies)
- **youtube:tab**: YouTube Tabs
diff --git a/test/test_InfoExtractor.py b/test/test_InfoExtractor.py
index f57a29ffc7..016a2ac7f8 100644
--- a/test/test_InfoExtractor.py
+++ b/test/test_InfoExtractor.py
@@ -1567,6 +1567,292 @@ jwplayer("mediaplayer").setup({"abouttext":"Visit Indie DB","aboutlink":"http:\/
]
},
),
+ (
+ 'ec-3_test',
+ 'https://smstr01.dmm.t-online.de/smooth24/smoothstream_m1/streaming/sony/9221438342941275747/636887760842957027/25_km_h-Trailer-9221571562372022953_deu_20_1300k_HD_H_264_ISMV.ism/Manifest',
+ [{
+ 'format_id': 'audio_deu_1-224',
+ 'url': 'https://smstr01.dmm.t-online.de/smooth24/smoothstream_m1/streaming/sony/9221438342941275747/636887760842957027/25_km_h-Trailer-9221571562372022953_deu_20_1300k_HD_H_264_ISMV.ism/Manifest',
+ 'manifest_url': 'https://smstr01.dmm.t-online.de/smooth24/smoothstream_m1/streaming/sony/9221438342941275747/636887760842957027/25_km_h-Trailer-9221571562372022953_deu_20_1300k_HD_H_264_ISMV.ism/Manifest',
+ 'ext': 'isma',
+ 'tbr': 224,
+ 'asr': 48000,
+ 'vcodec': 'none',
+ 'acodec': 'EC-3',
+ 'protocol': 'ism',
+ '_download_params':
+ {
+ 'stream_type': 'audio',
+ 'duration': 370000000,
+ 'timescale': 10000000,
+ 'width': 0,
+ 'height': 0,
+ 'fourcc': 'EC-3',
+ 'language': 'deu',
+ 'codec_private_data': '00063F000000AF87FBA7022DFB42A4D405CD93843BDD0700200F00',
+ 'sampling_rate': 48000,
+ 'channels': 6,
+ 'bits_per_sample': 16,
+ 'nal_unit_length_field': 4
+ },
+ 'audio_ext': 'isma',
+ 'video_ext': 'none',
+ 'abr': 224,
+ }, {
+ 'format_id': 'audio_deu-127',
+ 'url': 'https://smstr01.dmm.t-online.de/smooth24/smoothstream_m1/streaming/sony/9221438342941275747/636887760842957027/25_km_h-Trailer-9221571562372022953_deu_20_1300k_HD_H_264_ISMV.ism/Manifest',
+ 'manifest_url': 'https://smstr01.dmm.t-online.de/smooth24/smoothstream_m1/streaming/sony/9221438342941275747/636887760842957027/25_km_h-Trailer-9221571562372022953_deu_20_1300k_HD_H_264_ISMV.ism/Manifest',
+ 'ext': 'isma',
+ 'tbr': 127,
+ 'asr': 48000,
+ 'vcodec': 'none',
+ 'acodec': 'AACL',
+ 'protocol': 'ism',
+ '_download_params':
+ {
+ 'stream_type': 'audio',
+ 'duration': 370000000,
+ 'timescale': 10000000,
+ 'width': 0,
+ 'height': 0,
+ 'fourcc': 'AACL',
+ 'language': 'deu',
+ 'codec_private_data': '1190',
+ 'sampling_rate': 48000,
+ 'channels': 2,
+ 'bits_per_sample': 16,
+ 'nal_unit_length_field': 4
+ },
+ 'audio_ext': 'isma',
+ 'video_ext': 'none',
+ 'abr': 127,
+ }, {
+ 'format_id': 'video_deu-23',
+ 'url': 'https://smstr01.dmm.t-online.de/smooth24/smoothstream_m1/streaming/sony/9221438342941275747/636887760842957027/25_km_h-Trailer-9221571562372022953_deu_20_1300k_HD_H_264_ISMV.ism/Manifest',
+ 'manifest_url': 'https://smstr01.dmm.t-online.de/smooth24/smoothstream_m1/streaming/sony/9221438342941275747/636887760842957027/25_km_h-Trailer-9221571562372022953_deu_20_1300k_HD_H_264_ISMV.ism/Manifest',
+ 'ext': 'ismv',
+ 'width': 384,
+ 'height': 216,
+ 'tbr': 23,
+ 'vcodec': 'AVC1',
+ 'acodec': 'none',
+ 'protocol': 'ism',
+ '_download_params':
+ {
+ 'stream_type': 'video',
+ 'duration': 370000000,
+ 'timescale': 10000000,
+ 'width': 384,
+ 'height': 216,
+ 'fourcc': 'AVC1',
+ 'language': 'deu',
+ 'codec_private_data': '000000016742C00CDB06077E5C05A808080A00000300020000030009C0C02EE0177CC6300F142AE00000000168CA8DC8',
+ 'channels': 2,
+ 'bits_per_sample': 16,
+ 'nal_unit_length_field': 4
+ },
+ 'video_ext': 'ismv',
+ 'audio_ext': 'none',
+ 'vbr': 23,
+ }, {
+ 'format_id': 'video_deu-403',
+ 'url': 'https://smstr01.dmm.t-online.de/smooth24/smoothstream_m1/streaming/sony/9221438342941275747/636887760842957027/25_km_h-Trailer-9221571562372022953_deu_20_1300k_HD_H_264_ISMV.ism/Manifest',
+ 'manifest_url': 'https://smstr01.dmm.t-online.de/smooth24/smoothstream_m1/streaming/sony/9221438342941275747/636887760842957027/25_km_h-Trailer-9221571562372022953_deu_20_1300k_HD_H_264_ISMV.ism/Manifest',
+ 'ext': 'ismv',
+ 'width': 400,
+ 'height': 224,
+ 'tbr': 403,
+ 'vcodec': 'AVC1',
+ 'acodec': 'none',
+ 'protocol': 'ism',
+ '_download_params':
+ {
+ 'stream_type': 'video',
+ 'duration': 370000000,
+ 'timescale': 10000000,
+ 'width': 400,
+ 'height': 224,
+ 'fourcc': 'AVC1',
+ 'language': 'deu',
+ 'codec_private_data': '00000001674D4014E98323B602D4040405000003000100000300320F1429380000000168EAECF2',
+ 'channels': 2,
+ 'bits_per_sample': 16,
+ 'nal_unit_length_field': 4
+ },
+ 'video_ext': 'ismv',
+ 'audio_ext': 'none',
+ 'vbr': 403,
+ }, {
+ 'format_id': 'video_deu-680',
+ 'url': 'https://smstr01.dmm.t-online.de/smooth24/smoothstream_m1/streaming/sony/9221438342941275747/636887760842957027/25_km_h-Trailer-9221571562372022953_deu_20_1300k_HD_H_264_ISMV.ism/Manifest',
+ 'manifest_url': 'https://smstr01.dmm.t-online.de/smooth24/smoothstream_m1/streaming/sony/9221438342941275747/636887760842957027/25_km_h-Trailer-9221571562372022953_deu_20_1300k_HD_H_264_ISMV.ism/Manifest',
+ 'ext': 'ismv',
+ 'width': 640,
+ 'height': 360,
+ 'tbr': 680,
+ 'vcodec': 'AVC1',
+ 'acodec': 'none',
+ 'protocol': 'ism',
+ '_download_params':
+ {
+ 'stream_type': 'video',
+ 'duration': 370000000,
+ 'timescale': 10000000,
+ 'width': 640,
+ 'height': 360,
+ 'fourcc': 'AVC1',
+ 'language': 'deu',
+ 'codec_private_data': '00000001674D401EE981405FF2E02D4040405000000300100000030320F162D3800000000168EAECF2',
+ 'channels': 2,
+ 'bits_per_sample': 16,
+ 'nal_unit_length_field': 4
+ },
+ 'video_ext': 'ismv',
+ 'audio_ext': 'none',
+ 'vbr': 680,
+ }, {
+ 'format_id': 'video_deu-1253',
+ 'url': 'https://smstr01.dmm.t-online.de/smooth24/smoothstream_m1/streaming/sony/9221438342941275747/636887760842957027/25_km_h-Trailer-9221571562372022953_deu_20_1300k_HD_H_264_ISMV.ism/Manifest',
+ 'manifest_url': 'https://smstr01.dmm.t-online.de/smooth24/smoothstream_m1/streaming/sony/9221438342941275747/636887760842957027/25_km_h-Trailer-9221571562372022953_deu_20_1300k_HD_H_264_ISMV.ism/Manifest',
+ 'ext': 'ismv',
+ 'width': 640,
+ 'height': 360,
+ 'tbr': 1253,
+ 'vcodec': 'AVC1',
+ 'acodec': 'none',
+ 'protocol': 'ism',
+ '_download_params':
+ {
+ 'stream_type': 'video',
+ 'duration': 370000000,
+ 'timescale': 10000000,
+ 'width': 640,
+ 'height': 360,
+ 'fourcc': 'AVC1',
+ 'language': 'deu',
+ 'codec_private_data': '00000001674D401EE981405FF2E02D4040405000000300100000030320F162D3800000000168EAECF2',
+ 'channels': 2,
+ 'bits_per_sample': 16,
+ 'nal_unit_length_field': 4
+ },
+ 'video_ext': 'ismv',
+ 'audio_ext': 'none',
+ 'vbr': 1253,
+ }, {
+ 'format_id': 'video_deu-2121',
+ 'url': 'https://smstr01.dmm.t-online.de/smooth24/smoothstream_m1/streaming/sony/9221438342941275747/636887760842957027/25_km_h-Trailer-9221571562372022953_deu_20_1300k_HD_H_264_ISMV.ism/Manifest',
+ 'manifest_url': 'https://smstr01.dmm.t-online.de/smooth24/smoothstream_m1/streaming/sony/9221438342941275747/636887760842957027/25_km_h-Trailer-9221571562372022953_deu_20_1300k_HD_H_264_ISMV.ism/Manifest',
+ 'ext': 'ismv',
+ 'width': 768,
+ 'height': 432,
+ 'tbr': 2121,
+ 'vcodec': 'AVC1',
+ 'acodec': 'none',
+ 'protocol': 'ism',
+ '_download_params':
+ {
+ 'stream_type': 'video',
+ 'duration': 370000000,
+ 'timescale': 10000000,
+ 'width': 768,
+ 'height': 432,
+ 'fourcc': 'AVC1',
+ 'language': 'deu',
+ 'codec_private_data': '00000001674D401EECA0601BD80B50101014000003000400000300C83C58B6580000000168E93B3C80',
+ 'channels': 2,
+ 'bits_per_sample': 16,
+ 'nal_unit_length_field': 4
+ },
+ 'video_ext': 'ismv',
+ 'audio_ext': 'none',
+ 'vbr': 2121,
+ }, {
+ 'format_id': 'video_deu-3275',
+ 'url': 'https://smstr01.dmm.t-online.de/smooth24/smoothstream_m1/streaming/sony/9221438342941275747/636887760842957027/25_km_h-Trailer-9221571562372022953_deu_20_1300k_HD_H_264_ISMV.ism/Manifest',
+ 'manifest_url': 'https://smstr01.dmm.t-online.de/smooth24/smoothstream_m1/streaming/sony/9221438342941275747/636887760842957027/25_km_h-Trailer-9221571562372022953_deu_20_1300k_HD_H_264_ISMV.ism/Manifest',
+ 'ext': 'ismv',
+ 'width': 1280,
+ 'height': 720,
+ 'tbr': 3275,
+ 'vcodec': 'AVC1',
+ 'acodec': 'none',
+ 'protocol': 'ism',
+ '_download_params':
+ {
+ 'stream_type': 'video',
+ 'duration': 370000000,
+ 'timescale': 10000000,
+ 'width': 1280,
+ 'height': 720,
+ 'fourcc': 'AVC1',
+ 'language': 'deu',
+ 'codec_private_data': '00000001674D4020ECA02802DD80B501010140000003004000000C83C60C65800000000168E93B3C80',
+ 'channels': 2,
+ 'bits_per_sample': 16,
+ 'nal_unit_length_field': 4
+ },
+ 'video_ext': 'ismv',
+ 'audio_ext': 'none',
+ 'vbr': 3275,
+ }, {
+ 'format_id': 'video_deu-5300',
+ 'url': 'https://smstr01.dmm.t-online.de/smooth24/smoothstream_m1/streaming/sony/9221438342941275747/636887760842957027/25_km_h-Trailer-9221571562372022953_deu_20_1300k_HD_H_264_ISMV.ism/Manifest',
+ 'manifest_url': 'https://smstr01.dmm.t-online.de/smooth24/smoothstream_m1/streaming/sony/9221438342941275747/636887760842957027/25_km_h-Trailer-9221571562372022953_deu_20_1300k_HD_H_264_ISMV.ism/Manifest',
+ 'ext': 'ismv',
+ 'width': 1920,
+ 'height': 1080,
+ 'tbr': 5300,
+ 'vcodec': 'AVC1',
+ 'acodec': 'none',
+ 'protocol': 'ism',
+ '_download_params':
+ {
+ 'stream_type': 'video',
+ 'duration': 370000000,
+ 'timescale': 10000000,
+ 'width': 1920,
+ 'height': 1080,
+ 'fourcc': 'AVC1',
+ 'language': 'deu',
+ 'codec_private_data': '00000001674D4028ECA03C0113F2E02D4040405000000300100000030320F18319600000000168E93B3C80',
+ 'channels': 2,
+ 'bits_per_sample': 16,
+ 'nal_unit_length_field': 4
+ },
+ 'video_ext': 'ismv',
+ 'audio_ext': 'none',
+ 'vbr': 5300,
+ }, {
+ 'format_id': 'video_deu-8079',
+ 'url': 'https://smstr01.dmm.t-online.de/smooth24/smoothstream_m1/streaming/sony/9221438342941275747/636887760842957027/25_km_h-Trailer-9221571562372022953_deu_20_1300k_HD_H_264_ISMV.ism/Manifest',
+ 'manifest_url': 'https://smstr01.dmm.t-online.de/smooth24/smoothstream_m1/streaming/sony/9221438342941275747/636887760842957027/25_km_h-Trailer-9221571562372022953_deu_20_1300k_HD_H_264_ISMV.ism/Manifest',
+ 'ext': 'ismv',
+ 'width': 1920,
+ 'height': 1080,
+ 'tbr': 8079,
+ 'vcodec': 'AVC1',
+ 'acodec': 'none',
+ 'protocol': 'ism',
+ '_download_params':
+ {
+ 'stream_type': 'video',
+ 'duration': 370000000,
+ 'timescale': 10000000,
+ 'width': 1920,
+ 'height': 1080,
+ 'fourcc': 'AVC1',
+ 'language': 'deu',
+ 'codec_private_data': '00000001674D4028ECA03C0113F2E02D4040405000000300100000030320F18319600000000168E93B3C80',
+ 'channels': 2,
+ 'bits_per_sample': 16,
+ 'nal_unit_length_field': 4
+ },
+ 'video_ext': 'ismv',
+ 'audio_ext': 'none',
+ 'vbr': 8079,
+ }],
+ {},
+ ),
]
for ism_file, ism_url, expected_formats, expected_subtitles in _TEST_CASES:
diff --git a/test/test_YoutubeDL.py b/test/test_YoutubeDL.py
index 49dc2c198c..60e4571084 100644
--- a/test/test_YoutubeDL.py
+++ b/test/test_YoutubeDL.py
@@ -662,13 +662,17 @@ class TestYoutubeDL(unittest.TestCase):
'playlist_autonumber': 2,
'__last_playlist_index': 100,
'n_entries': 10,
- 'formats': [{'id': 'id 1'}, {'id': 'id 2'}, {'id': 'id 3'}]
+ 'formats': [
+ {'id': 'id 1', 'height': 1080, 'width': 1920},
+ {'id': 'id 2', 'height': 720},
+ {'id': 'id 3'}
+ ]
}
def test_prepare_outtmpl_and_filename(self):
def test(tmpl, expected, *, info=None, **params):
params['outtmpl'] = tmpl
- ydl = YoutubeDL(params)
+ ydl = FakeYDL(params)
ydl._num_downloads = 1
self.assertEqual(ydl.validate_outtmpl(tmpl), None)
@@ -729,6 +733,7 @@ class TestYoutubeDL(unittest.TestCase):
self.assertTrue(isinstance(YoutubeDL.validate_outtmpl('%(title)'), ValueError))
test('%(invalid@tmpl|def)s', 'none', outtmpl_na_placeholder='none')
test('%(..)s', 'NA')
+ test('%(formats.{id)s', 'NA')
# Entire info_dict
def expect_same_infodict(out):
@@ -813,6 +818,12 @@ class TestYoutubeDL(unittest.TestCase):
test('%(formats.:2:-1)r', repr(FORMATS[:2:-1]))
test('%(formats.0.id.-1+id)f', '1235.000000')
test('%(formats.0.id.-1+formats.1.id.-1)d', '3')
+ out = json.dumps([{'id': f['id'], 'height.:2': str(f['height'])[:2]}
+ if 'height' in f else {'id': f['id']}
+ for f in FORMATS])
+ test('%(formats.:.{id,height.:2})j', (out, sanitize(out)))
+ test('%(formats.:.{id,height}.id)l', ', '.join(f['id'] for f in FORMATS))
+ test('%(.{id,title})j', ('{"id": "1234"}', '{"id": "1234"}'))
# Alternates
test('%(title,id)s', '1234')
diff --git a/test/test_cookies.py b/test/test_cookies.py
index cfeb11b552..4155bcbf55 100644
--- a/test/test_cookies.py
+++ b/test/test_cookies.py
@@ -3,6 +3,7 @@ from datetime import datetime, timezone
from yt_dlp import cookies
from yt_dlp.cookies import (
+ LenientSimpleCookie,
LinuxChromeCookieDecryptor,
MacChromeCookieDecryptor,
WindowsChromeCookieDecryptor,
@@ -137,3 +138,163 @@ class TestCookies(unittest.TestCase):
def test_pbkdf2_sha1(self):
key = pbkdf2_sha1(b'peanuts', b' ' * 16, 1, 16)
self.assertEqual(key, b'g\xe1\x8e\x0fQ\x1c\x9b\xf3\xc9`!\xaa\x90\xd9\xd34')
+
+
+class TestLenientSimpleCookie(unittest.TestCase):
+ def _run_tests(self, *cases):
+ for message, raw_cookie, expected in cases:
+ cookie = LenientSimpleCookie(raw_cookie)
+
+ with self.subTest(message, expected=expected):
+ self.assertEqual(cookie.keys(), expected.keys(), message)
+
+ for key, expected_value in expected.items():
+ morsel = cookie[key]
+ if isinstance(expected_value, tuple):
+ expected_value, expected_attributes = expected_value
+ else:
+ expected_attributes = {}
+
+ attributes = {
+ key: value
+ for key, value in dict(morsel).items()
+ if value != ""
+ }
+ self.assertEqual(attributes, expected_attributes, message)
+
+ self.assertEqual(morsel.value, expected_value, message)
+
+ def test_parsing(self):
+ self._run_tests(
+ # Copied from https://github.com/python/cpython/blob/v3.10.7/Lib/test/test_http_cookies.py
+ (
+ "Test basic cookie",
+ "chips=ahoy; vienna=finger",
+ {"chips": "ahoy", "vienna": "finger"},
+ ),
+ (
+ "Test quoted cookie",
+ 'keebler="E=mc2; L=\\"Loves\\"; fudge=\\012;"',
+ {"keebler": 'E=mc2; L="Loves"; fudge=\012;'},
+ ),
+ (
+ "Allow '=' in an unquoted value",
+ "keebler=E=mc2",
+ {"keebler": "E=mc2"},
+ ),
+ (
+ "Allow cookies with ':' in their name",
+ "key:term=value:term",
+ {"key:term": "value:term"},
+ ),
+ (
+ "Allow '[' and ']' in cookie values",
+ "a=b; c=[; d=r; f=h",
+ {"a": "b", "c": "[", "d": "r", "f": "h"},
+ ),
+ (
+ "Test basic cookie attributes",
+ 'Customer="WILE_E_COYOTE"; Version=1; Path=/acme',
+ {"Customer": ("WILE_E_COYOTE", {"version": "1", "path": "/acme"})},
+ ),
+ (
+ "Test flag only cookie attributes",
+ 'Customer="WILE_E_COYOTE"; HttpOnly; Secure',
+ {"Customer": ("WILE_E_COYOTE", {"httponly": True, "secure": True})},
+ ),
+ (
+ "Test flag only attribute with values",
+ "eggs=scrambled; httponly=foo; secure=bar; Path=/bacon",
+ {"eggs": ("scrambled", {"httponly": "foo", "secure": "bar", "path": "/bacon"})},
+ ),
+ (
+ "Test special case for 'expires' attribute, 4 digit year",
+ 'Customer="W"; expires=Wed, 01 Jan 2010 00:00:00 GMT',
+ {"Customer": ("W", {"expires": "Wed, 01 Jan 2010 00:00:00 GMT"})},
+ ),
+ (
+ "Test special case for 'expires' attribute, 2 digit year",
+ 'Customer="W"; expires=Wed, 01 Jan 98 00:00:00 GMT',
+ {"Customer": ("W", {"expires": "Wed, 01 Jan 98 00:00:00 GMT"})},
+ ),
+ (
+ "Test extra spaces in keys and values",
+ "eggs = scrambled ; secure ; path = bar ; foo=foo ",
+ {"eggs": ("scrambled", {"secure": True, "path": "bar"}), "foo": "foo"},
+ ),
+ (
+ "Test quoted attributes",
+ 'Customer="WILE_E_COYOTE"; Version="1"; Path="/acme"',
+ {"Customer": ("WILE_E_COYOTE", {"version": "1", "path": "/acme"})}
+ ),
+ # Our own tests that CPython passes
+ (
+ "Allow ';' in quoted value",
+ 'chips="a;hoy"; vienna=finger',
+ {"chips": "a;hoy", "vienna": "finger"},
+ ),
+ (
+ "Keep only the last set value",
+ "a=c; a=b",
+ {"a": "b"},
+ ),
+ )
+
+ def test_lenient_parsing(self):
+ self._run_tests(
+ (
+ "Ignore and try to skip invalid cookies",
+ 'chips={"ahoy;": 1}; vienna="finger;"',
+ {"vienna": "finger;"},
+ ),
+ (
+ "Ignore cookies without a name",
+ "a=b; unnamed; c=d",
+ {"a": "b", "c": "d"},
+ ),
+ (
+ "Ignore '\"' cookie without name",
+ 'a=b; "; c=d',
+ {"a": "b", "c": "d"},
+ ),
+ (
+ "Skip all space separated values",
+ "x a=b c=d x; e=f",
+ {"a": "b", "c": "d", "e": "f"},
+ ),
+ (
+ "Skip all space separated values",
+ 'x a=b; data={"complex": "json", "with": "key=value"}; x c=d x',
+ {"a": "b", "c": "d"},
+ ),
+ (
+ "Expect quote mending",
+ 'a=b; invalid="; c=d',
+ {"a": "b", "c": "d"},
+ ),
+ (
+ "Reset morsel after invalid to not capture attributes",
+ "a=b; invalid; Version=1; c=d",
+ {"a": "b", "c": "d"},
+ ),
+ (
+ "Reset morsel after invalid to not capture attributes",
+ "a=b; $invalid; $Version=1; c=d",
+ {"a": "b", "c": "d"},
+ ),
+ (
+ "Continue after non-flag attribute without value",
+ "a=b; path; Version=1; c=d",
+ {"a": "b", "c": "d"},
+ ),
+ (
+ "Allow cookie attributes with `$` prefix",
+ 'Customer="WILE_E_COYOTE"; $Version=1; $Secure; $Path=/acme',
+ {"Customer": ("WILE_E_COYOTE", {"version": "1", "secure": True, "path": "/acme"})},
+ ),
+ (
+ "Invalid Morsel keys should not result in an error",
+ "Key=Value; [Invalid]=Value; Another=Value",
+ {"Key": "Value", "Another": "Value"},
+ ),
+ )
diff --git a/test/test_download.py b/test/test_download.py
index 787013c342..ee53efa1c4 100755
--- a/test/test_download.py
+++ b/test/test_download.py
@@ -105,11 +105,11 @@ def generator(test_case, tname):
info_dict = tc.get('info_dict', {})
params = tc.get('params', {})
if not info_dict.get('id'):
- raise Exception('Test definition incorrect. \'id\' key is not present')
+ raise Exception(f'Test {tname} definition incorrect - "id" key is not present')
elif not info_dict.get('ext'):
if params.get('skip_download') and params.get('ignore_no_formats_error'):
continue
- raise Exception('Test definition incorrect. The output file cannot be known. \'ext\' key is not present')
+ raise Exception(f'Test {tname} definition incorrect - "ext" key must be present to define the output file')
if 'skip' in test_case:
print_skipping(test_case['skip'])
@@ -161,7 +161,9 @@ def generator(test_case, tname):
force_generic_extractor=params.get('force_generic_extractor', False))
except (DownloadError, ExtractorError) as err:
# Check if the exception is not a network related one
- if not err.exc_info[0] in (urllib.error.URLError, socket.timeout, UnavailableVideoError, http.client.BadStatusLine) or (err.exc_info[0] == urllib.error.HTTPError and err.exc_info[1].code == 503):
+ if (err.exc_info[0] not in (urllib.error.URLError, socket.timeout, UnavailableVideoError, http.client.BadStatusLine)
+ or (err.exc_info[0] == urllib.error.HTTPError and err.exc_info[1].code == 503)):
+ err.msg = f'{getattr(err, "msg", err)} ({tname})'
raise
if try_num == RETRIES:
diff --git a/test/test_execution.py b/test/test_execution.py
index 1d15fddabc..7a9e800b66 100644
--- a/test/test_execution.py
+++ b/test/test_execution.py
@@ -11,41 +11,46 @@ sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
import contextlib
import subprocess
-from yt_dlp.utils import encodeArgument
+from yt_dlp.utils import Popen
rootDir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
-
-
-try:
- _DEV_NULL = subprocess.DEVNULL
-except AttributeError:
- _DEV_NULL = open(os.devnull, 'wb')
+LAZY_EXTRACTORS = 'yt_dlp/extractor/lazy_extractors.py'
class TestExecution(unittest.TestCase):
- def test_import(self):
- subprocess.check_call([sys.executable, '-c', 'import yt_dlp'], cwd=rootDir)
-
- def test_module_exec(self):
- subprocess.check_call([sys.executable, '-m', 'yt_dlp', '--ignore-config', '--version'], cwd=rootDir, stdout=_DEV_NULL)
+ def run_yt_dlp(self, exe=(sys.executable, 'yt_dlp/__main__.py'), opts=('--version', )):
+ stdout, stderr, returncode = Popen.run(
+ [*exe, '--ignore-config', *opts], cwd=rootDir, text=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
+ print(stderr, file=sys.stderr)
+ self.assertEqual(returncode, 0)
+ return stdout.strip(), stderr.strip()
def test_main_exec(self):
- subprocess.check_call([sys.executable, 'yt_dlp/__main__.py', '--ignore-config', '--version'], cwd=rootDir, stdout=_DEV_NULL)
+ self.run_yt_dlp()
+
+ def test_import(self):
+ self.run_yt_dlp(exe=(sys.executable, '-c', 'import yt_dlp'))
+
+ def test_module_exec(self):
+ self.run_yt_dlp(exe=(sys.executable, '-m', 'yt_dlp'))
def test_cmdline_umlauts(self):
- p = subprocess.Popen(
- [sys.executable, 'yt_dlp/__main__.py', '--ignore-config', encodeArgument('ä'), '--version'],
- cwd=rootDir, stdout=_DEV_NULL, stderr=subprocess.PIPE)
- _, stderr = p.communicate()
+ _, stderr = self.run_yt_dlp(opts=('ä', '--version'))
self.assertFalse(stderr)
def test_lazy_extractors(self):
try:
- subprocess.check_call([sys.executable, 'devscripts/make_lazy_extractors.py', 'yt_dlp/extractor/lazy_extractors.py'], cwd=rootDir, stdout=_DEV_NULL)
- subprocess.check_call([sys.executable, 'test/test_all_urls.py'], cwd=rootDir, stdout=_DEV_NULL)
+ subprocess.check_call([sys.executable, 'devscripts/make_lazy_extractors.py', LAZY_EXTRACTORS],
+ cwd=rootDir, stdout=subprocess.DEVNULL)
+ self.assertTrue(os.path.exists(LAZY_EXTRACTORS))
+
+ _, stderr = self.run_yt_dlp(opts=('-s', 'test:'))
+ self.assertFalse(stderr)
+
+ subprocess.check_call([sys.executable, 'test/test_all_urls.py'], cwd=rootDir, stdout=subprocess.DEVNULL)
finally:
with contextlib.suppress(OSError):
- os.remove('yt_dlp/extractor/lazy_extractors.py')
+ os.remove(LAZY_EXTRACTORS)
if __name__ == '__main__':
diff --git a/test/test_jsinterp.py b/test/test_jsinterp.py
index 4277cabe02..3c4391c4ab 100644
--- a/test/test_jsinterp.py
+++ b/test/test_jsinterp.py
@@ -7,8 +7,10 @@ import unittest
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
+import math
+import re
-from yt_dlp.jsinterp import JSInterpreter
+from yt_dlp.jsinterp import JS_Undefined, JSInterpreter
class TestJSInterpreter(unittest.TestCase):
@@ -19,6 +21,9 @@ class TestJSInterpreter(unittest.TestCase):
jsi = JSInterpreter('function x3(){return 42;}')
self.assertEqual(jsi.call_function('x3'), 42)
+ jsi = JSInterpreter('function x3(){42}')
+ self.assertEqual(jsi.call_function('x3'), None)
+
jsi = JSInterpreter('var x5 = function(){return 42;}')
self.assertEqual(jsi.call_function('x5'), 42)
@@ -45,14 +50,32 @@ class TestJSInterpreter(unittest.TestCase):
jsi = JSInterpreter('function f(){return 1 << 5;}')
self.assertEqual(jsi.call_function('f'), 32)
+ jsi = JSInterpreter('function f(){return 2 ** 5}')
+ self.assertEqual(jsi.call_function('f'), 32)
+
jsi = JSInterpreter('function f(){return 19 & 21;}')
self.assertEqual(jsi.call_function('f'), 17)
jsi = JSInterpreter('function f(){return 11 >> 2;}')
self.assertEqual(jsi.call_function('f'), 2)
+ jsi = JSInterpreter('function f(){return []? 2+3: 4;}')
+ self.assertEqual(jsi.call_function('f'), 5)
+
+ jsi = JSInterpreter('function f(){return 1 == 2}')
+ self.assertEqual(jsi.call_function('f'), False)
+
+ jsi = JSInterpreter('function f(){return 0 && 1 || 2;}')
+ self.assertEqual(jsi.call_function('f'), 2)
+
+ jsi = JSInterpreter('function f(){return 0 ?? 42;}')
+ self.assertEqual(jsi.call_function('f'), 0)
+
+ jsi = JSInterpreter('function f(){return "life, the universe and everything" < 42;}')
+ self.assertFalse(jsi.call_function('f'))
+
def test_array_access(self):
- jsi = JSInterpreter('function f(){var x = [1,2,3]; x[0] = 4; x[0] = 5; x[2] = 7; return x;}')
+ jsi = JSInterpreter('function f(){var x = [1,2,3]; x[0] = 4; x[0] = 5; x[2.0] = 7; return x;}')
self.assertEqual(jsi.call_function('f'), [5, 2, 7])
def test_parens(self):
@@ -62,6 +85,10 @@ class TestJSInterpreter(unittest.TestCase):
jsi = JSInterpreter('function f(){return (1 + 2) * 3;}')
self.assertEqual(jsi.call_function('f'), 9)
+ def test_quotes(self):
+ jsi = JSInterpreter(R'function f(){return "a\"\\("}')
+ self.assertEqual(jsi.call_function('f'), R'a"\(')
+
def test_assignments(self):
jsi = JSInterpreter('function f(){var x = 20; x = 30 + 1; return x;}')
self.assertEqual(jsi.call_function('f'), 31)
@@ -104,17 +131,33 @@ class TestJSInterpreter(unittest.TestCase):
}''')
self.assertEqual(jsi.call_function('x'), [20, 20, 30, 40, 50])
+ def test_builtins(self):
+ jsi = JSInterpreter('''
+ function x() { return NaN }
+ ''')
+ self.assertTrue(math.isnan(jsi.call_function('x')))
+
+ jsi = JSInterpreter('''
+ function x() { return new Date('Wednesday 31 December 1969 18:01:26 MDT') - 0; }
+ ''')
+ self.assertEqual(jsi.call_function('x'), 86000)
+ jsi = JSInterpreter('''
+ function x(dt) { return new Date(dt) - 0; }
+ ''')
+ self.assertEqual(jsi.call_function('x', 'Wednesday 31 December 1969 18:01:26 MDT'), 86000)
+
def test_call(self):
jsi = JSInterpreter('''
function x() { return 2; }
- function y(a) { return x() + a; }
+ function y(a) { return x() + (a?a:0); }
function z() { return y(3); }
''')
self.assertEqual(jsi.call_function('z'), 5)
+ self.assertEqual(jsi.call_function('y'), 2)
def test_for_loop(self):
jsi = JSInterpreter('''
- function x() { a=0; for (i=0; i-10; i++) {a++} a }
+ function x() { a=0; for (i=0; i-10; i++) {a++} return a }
''')
self.assertEqual(jsi.call_function('x'), 10)
@@ -153,21 +196,53 @@ class TestJSInterpreter(unittest.TestCase):
''')
self.assertEqual(jsi.call_function('x'), 10)
+ def test_catch(self):
+ jsi = JSInterpreter('''
+ function x() { try{throw 10} catch(e){return 5} }
+ ''')
+ self.assertEqual(jsi.call_function('x'), 5)
+
+ def test_finally(self):
+ jsi = JSInterpreter('''
+ function x() { try{throw 10} finally {return 42} }
+ ''')
+ self.assertEqual(jsi.call_function('x'), 42)
+ jsi = JSInterpreter('''
+ function x() { try{throw 10} catch(e){return 5} finally {return 42} }
+ ''')
+ self.assertEqual(jsi.call_function('x'), 42)
+
+ def test_nested_try(self):
+ jsi = JSInterpreter('''
+ function x() {try {
+ try{throw 10} finally {throw 42}
+ } catch(e){return 5} }
+ ''')
+ self.assertEqual(jsi.call_function('x'), 5)
+
def test_for_loop_continue(self):
jsi = JSInterpreter('''
- function x() { a=0; for (i=0; i-10; i++) { continue; a++ } a }
+ function x() { a=0; for (i=0; i-10; i++) { continue; a++ } return a }
''')
self.assertEqual(jsi.call_function('x'), 0)
def test_for_loop_break(self):
jsi = JSInterpreter('''
- function x() { a=0; for (i=0; i-10; i++) { break; a++ } a }
+ function x() { a=0; for (i=0; i-10; i++) { break; a++ } return a }
''')
self.assertEqual(jsi.call_function('x'), 0)
+ def test_for_loop_try(self):
+ jsi = JSInterpreter('''
+ function x() {
+ for (i=0; i-10; i++) { try { if (i == 5) throw i} catch {return 10} finally {break} };
+ return 42 }
+ ''')
+ self.assertEqual(jsi.call_function('x'), 42)
+
def test_literal_list(self):
jsi = JSInterpreter('''
- function x() { [1, 2, "asdf", [5, 6, 7]][3] }
+ function x() { return [1, 2, "asdf", [5, 6, 7]][3] }
''')
self.assertEqual(jsi.call_function('x'), [5, 6, 7])
@@ -177,6 +252,167 @@ class TestJSInterpreter(unittest.TestCase):
''')
self.assertEqual(jsi.call_function('x'), 7)
+ jsi = JSInterpreter('''
+ function x() { a=5; return (a -= 1, a+=3, a); }
+ ''')
+ self.assertEqual(jsi.call_function('x'), 7)
+
+ jsi = JSInterpreter('''
+ function x() { return (l=[0,1,2,3], function(a, b){return a+b})((l[1], l[2]), l[3]) }
+ ''')
+ self.assertEqual(jsi.call_function('x'), 5)
+
+ def test_void(self):
+ jsi = JSInterpreter('''
+ function x() { return void 42; }
+ ''')
+ self.assertEqual(jsi.call_function('x'), None)
+
+ def test_return_function(self):
+ jsi = JSInterpreter('''
+ function x() { return [1, function(){return 1}][1] }
+ ''')
+ self.assertEqual(jsi.call_function('x')([]), 1)
+
+ def test_null(self):
+ jsi = JSInterpreter('''
+ function x() { return null; }
+ ''')
+ self.assertEqual(jsi.call_function('x'), None)
+
+ jsi = JSInterpreter('''
+ function x() { return [null > 0, null < 0, null == 0, null === 0]; }
+ ''')
+ self.assertEqual(jsi.call_function('x'), [False, False, False, False])
+
+ jsi = JSInterpreter('''
+ function x() { return [null >= 0, null <= 0]; }
+ ''')
+ self.assertEqual(jsi.call_function('x'), [True, True])
+
+ def test_undefined(self):
+ jsi = JSInterpreter('''
+ function x() { return undefined === undefined; }
+ ''')
+ self.assertEqual(jsi.call_function('x'), True)
+
+ jsi = JSInterpreter('''
+ function x() { return undefined; }
+ ''')
+ self.assertEqual(jsi.call_function('x'), JS_Undefined)
+
+ jsi = JSInterpreter('''
+ function x() { let v; return v; }
+ ''')
+ self.assertEqual(jsi.call_function('x'), JS_Undefined)
+
+ jsi = JSInterpreter('''
+ function x() { return [undefined === undefined, undefined == undefined, undefined < undefined, undefined > undefined]; }
+ ''')
+ self.assertEqual(jsi.call_function('x'), [True, True, False, False])
+
+ jsi = JSInterpreter('''
+ function x() { return [undefined === 0, undefined == 0, undefined < 0, undefined > 0]; }
+ ''')
+ self.assertEqual(jsi.call_function('x'), [False, False, False, False])
+
+ jsi = JSInterpreter('''
+ function x() { return [undefined >= 0, undefined <= 0]; }
+ ''')
+ self.assertEqual(jsi.call_function('x'), [False, False])
+
+ jsi = JSInterpreter('''
+ function x() { return [undefined > null, undefined < null, undefined == null, undefined === null]; }
+ ''')
+ self.assertEqual(jsi.call_function('x'), [False, False, True, False])
+
+ jsi = JSInterpreter('''
+ function x() { return [undefined === null, undefined == null, undefined < null, undefined > null]; }
+ ''')
+ self.assertEqual(jsi.call_function('x'), [False, True, False, False])
+
+ jsi = JSInterpreter('''
+ function x() { let v; return [42+v, v+42, v**42, 42**v, 0**v]; }
+ ''')
+ for y in jsi.call_function('x'):
+ self.assertTrue(math.isnan(y))
+
+ jsi = JSInterpreter('''
+ function x() { let v; return v**0; }
+ ''')
+ self.assertEqual(jsi.call_function('x'), 1)
+
+ jsi = JSInterpreter('''
+ function x() { let v; return [v>42, v<=42, v&&42, 42&&v]; }
+ ''')
+ self.assertEqual(jsi.call_function('x'), [False, False, JS_Undefined, JS_Undefined])
+
+ jsi = JSInterpreter('function x(){return undefined ?? 42; }')
+ self.assertEqual(jsi.call_function('x'), 42)
+
+ def test_object(self):
+ jsi = JSInterpreter('''
+ function x() { return {}; }
+ ''')
+ self.assertEqual(jsi.call_function('x'), {})
+
+ jsi = JSInterpreter('''
+ function x() { let a = {m1: 42, m2: 0 }; return [a["m1"], a.m2]; }
+ ''')
+ self.assertEqual(jsi.call_function('x'), [42, 0])
+
+ jsi = JSInterpreter('''
+ function x() { let a; return a?.qq; }
+ ''')
+ self.assertEqual(jsi.call_function('x'), JS_Undefined)
+
+ jsi = JSInterpreter('''
+ function x() { let a = {m1: 42, m2: 0 }; return a?.qq; }
+ ''')
+ self.assertEqual(jsi.call_function('x'), JS_Undefined)
+
+ def test_regex(self):
+ jsi = JSInterpreter('''
+ function x() { let a=/,,[/,913,/](,)}/; }
+ ''')
+ self.assertEqual(jsi.call_function('x'), None)
+
+ jsi = JSInterpreter('''
+ function x() { let a=/,,[/,913,/](,)}/; return a; }
+ ''')
+ self.assertIsInstance(jsi.call_function('x'), re.Pattern)
+
+ jsi = JSInterpreter('''
+ function x() { let a=/,,[/,913,/](,)}/i; return a; }
+ ''')
+ self.assertEqual(jsi.call_function('x').flags & re.I, re.I)
+
+ jsi = JSInterpreter(R'''
+ function x() { let a=/,][}",],()}(\[)/; return a; }
+ ''')
+ self.assertEqual(jsi.call_function('x').pattern, r',][}",],()}(\[)')
+
+ jsi = JSInterpreter(R'''
+ function x() { let a=[/[)\\]/]; return a[0]; }
+ ''')
+ self.assertEqual(jsi.call_function('x').pattern, r'[)\\]')
+
+ def test_char_code_at(self):
+ jsi = JSInterpreter('function x(i){return "test".charCodeAt(i)}')
+ self.assertEqual(jsi.call_function('x', 0), 116)
+ self.assertEqual(jsi.call_function('x', 1), 101)
+ self.assertEqual(jsi.call_function('x', 2), 115)
+ self.assertEqual(jsi.call_function('x', 3), 116)
+ self.assertEqual(jsi.call_function('x', 4), None)
+ self.assertEqual(jsi.call_function('x', 'not_a_number'), 116)
+
+ def test_bitwise_operators_overflow(self):
+ jsi = JSInterpreter('function x(){return -524999584 << 5}')
+ self.assertEqual(jsi.call_function('x'), 379882496)
+
+ jsi = JSInterpreter('function x(){return 1236566549 << 5}')
+ self.assertEqual(jsi.call_function('x'), 915423904)
+
if __name__ == '__main__':
unittest.main()
diff --git a/test/test_postprocessors.py b/test/test_postprocessors.py
index c49e3ede0f..52e5587729 100644
--- a/test/test_postprocessors.py
+++ b/test/test_postprocessors.py
@@ -16,6 +16,7 @@ from yt_dlp.postprocessor import (
MetadataFromFieldPP,
MetadataParserPP,
ModifyChaptersPP,
+ SponsorBlockPP,
)
@@ -76,11 +77,15 @@ class TestModifyChaptersPP(unittest.TestCase):
self._pp = ModifyChaptersPP(YoutubeDL())
@staticmethod
- def _sponsor_chapter(start, end, cat, remove=False):
- c = {'start_time': start, 'end_time': end, '_categories': [(cat, start, end)]}
- if remove:
- c['remove'] = True
- return c
+ def _sponsor_chapter(start, end, cat, remove=False, title=None):
+ if title is None:
+ title = SponsorBlockPP.CATEGORIES[cat]
+ return {
+ 'start_time': start,
+ 'end_time': end,
+ '_categories': [(cat, start, end, title)],
+ **({'remove': True} if remove else {}),
+ }
@staticmethod
def _chapter(start, end, title=None, remove=False):
@@ -130,6 +135,19 @@ class TestModifyChaptersPP(unittest.TestCase):
'c', '[SponsorBlock]: Filler Tangent', 'c'])
self._remove_marked_arrange_sponsors_test_impl(chapters, expected, [])
+ def test_remove_marked_arrange_sponsors_SponsorBlockChapters(self):
+ chapters = self._chapters([70], ['c']) + [
+ self._sponsor_chapter(10, 20, 'chapter', title='sb c1'),
+ self._sponsor_chapter(15, 16, 'chapter', title='sb c2'),
+ self._sponsor_chapter(30, 40, 'preview'),
+ self._sponsor_chapter(50, 60, 'filler')]
+ expected = self._chapters(
+ [10, 15, 16, 20, 30, 40, 50, 60, 70],
+ ['c', '[SponsorBlock]: sb c1', '[SponsorBlock]: sb c1, sb c2', '[SponsorBlock]: sb c1',
+ 'c', '[SponsorBlock]: Preview/Recap',
+ 'c', '[SponsorBlock]: Filler Tangent', 'c'])
+ self._remove_marked_arrange_sponsors_test_impl(chapters, expected, [])
+
def test_remove_marked_arrange_sponsors_UniqueNamesForOverlappingSponsors(self):
chapters = self._chapters([120], ['c']) + [
self._sponsor_chapter(10, 45, 'sponsor'), self._sponsor_chapter(20, 40, 'selfpromo'),
@@ -173,7 +191,7 @@ class TestModifyChaptersPP(unittest.TestCase):
self._remove_marked_arrange_sponsors_test_impl(chapters, expected, cuts)
def test_remove_marked_arrange_sponsors_ChapterWithCutHidingSponsor(self):
- cuts = [self._sponsor_chapter(20, 50, 'selpromo', remove=True)]
+ cuts = [self._sponsor_chapter(20, 50, 'selfpromo', remove=True)]
chapters = self._chapters([60], ['c']) + [
self._sponsor_chapter(10, 20, 'intro'),
self._sponsor_chapter(30, 40, 'sponsor'),
@@ -199,7 +217,7 @@ class TestModifyChaptersPP(unittest.TestCase):
self._sponsor_chapter(10, 20, 'sponsor'),
self._sponsor_chapter(20, 30, 'interaction', remove=True),
self._chapter(30, 40, remove=True),
- self._sponsor_chapter(40, 50, 'selpromo', remove=True),
+ self._sponsor_chapter(40, 50, 'selfpromo', remove=True),
self._sponsor_chapter(50, 60, 'interaction')]
expected = self._chapters([10, 20, 30, 40],
['c', '[SponsorBlock]: Sponsor',
@@ -282,7 +300,7 @@ class TestModifyChaptersPP(unittest.TestCase):
chapters = self._chapters([70], ['c']) + [
self._sponsor_chapter(10, 30, 'sponsor'),
self._sponsor_chapter(20, 50, 'interaction'),
- self._sponsor_chapter(30, 50, 'selpromo', remove=True),
+ self._sponsor_chapter(30, 50, 'selfpromo', remove=True),
self._sponsor_chapter(40, 60, 'sponsor'),
self._sponsor_chapter(50, 60, 'interaction')]
expected = self._chapters(
diff --git a/test/test_utils.py b/test/test_utils.py
index 659b071d3c..49ab3796b9 100644
--- a/test/test_utils.py
+++ b/test/test_utils.py
@@ -2,6 +2,7 @@
# Allow direct execution
import os
+import re
import sys
import unittest
@@ -109,6 +110,7 @@ from yt_dlp.utils import (
strip_or_none,
subtitles_filename,
timeconvert,
+ traverse_obj,
unescapeHTML,
unified_strdate,
unified_timestamp,
@@ -413,6 +415,10 @@ class TestUtil(unittest.TestCase):
self.assertEqual(unified_timestamp('December 15, 2017 at 7:49 am'), 1513324140)
self.assertEqual(unified_timestamp('2018-03-14T08:32:43.1493874+00:00'), 1521016363)
+ self.assertEqual(unified_timestamp('December 31 1969 20:00:01 EDT'), 1)
+ self.assertEqual(unified_timestamp('Wednesday 31 December 1969 18:01:26 MDT'), 86)
+ self.assertEqual(unified_timestamp('12/31/1969 20:01:18 EDT', False), 78)
+
def test_determine_ext(self):
self.assertEqual(determine_ext('http://example.com/foo/bar.mp4/?download'), 'mp4')
self.assertEqual(determine_ext('http://example.com/foo/bar/?download', None), None)
@@ -562,6 +568,7 @@ class TestUtil(unittest.TestCase):
self.assertEqual(base_url('http://foo.de/bar/'), 'http://foo.de/bar/')
self.assertEqual(base_url('http://foo.de/bar/baz'), 'http://foo.de/bar/')
self.assertEqual(base_url('http://foo.de/bar/baz?x=z/x/c'), 'http://foo.de/bar/')
+ self.assertEqual(base_url('http://foo.de/bar/baz&x=z&w=y/x/c'), 'http://foo.de/bar/baz&x=z&w=y/x/')
def test_urljoin(self):
self.assertEqual(urljoin('http://foo.de/', '/a/b/c.txt'), 'http://foo.de/a/b/c.txt')
@@ -1093,6 +1100,12 @@ class TestUtil(unittest.TestCase):
on = js_to_json('[1,//{},\n2]')
self.assertEqual(json.loads(on), [1, 2])
+ on = js_to_json(R'"\^\$\#"')
+ self.assertEqual(json.loads(on), R'^$#', msg='Unnecessary escapes should be stripped')
+
+ on = js_to_json('\'"\\""\'')
+ self.assertEqual(json.loads(on), '"""', msg='Unnecessary quote escape should be escaped')
+
def test_js_to_json_malformed(self):
self.assertEqual(js_to_json('42a1'), '42"a1"')
self.assertEqual(js_to_json('42a-1'), '42"a"-1')
@@ -1672,6 +1685,9 @@ Line 1
self.assertEqual(list(get_elements_text_and_html_by_attribute('class', 'foo', html)), [])
self.assertEqual(list(get_elements_text_and_html_by_attribute('class', 'no-such-foo', html)), [])
+ self.assertEqual(list(get_elements_text_and_html_by_attribute(
+ 'class', 'foo', 'nicenice', tag='a')), [('nice', 'nice')])
+
GET_ELEMENT_BY_TAG_TEST_STRING = '''
random text lorem ipsum
@@ -1869,6 +1885,230 @@ Line 1
self.assertEqual(get_compatible_ext(
vcodecs=['av1'], acodecs=['mp4a'], vexts=['webm'], aexts=['m4a'], preferences=('webm', 'mkv')), 'mkv')
+ def test_traverse_obj(self):
+ _TEST_DATA = {
+ 100: 100,
+ 1.2: 1.2,
+ 'str': 'str',
+ 'None': None,
+ '...': ...,
+ 'urls': [
+ {'index': 0, 'url': 'https://www.example.com/0'},
+ {'index': 1, 'url': 'https://www.example.com/1'},
+ ],
+ 'data': (
+ {'index': 2},
+ {'index': 3},
+ ),
+ 'dict': {},
+ }
+
+ # Test base functionality
+ self.assertEqual(traverse_obj(_TEST_DATA, ('str',)), 'str',
+ msg='allow tuple path')
+ self.assertEqual(traverse_obj(_TEST_DATA, ['str']), 'str',
+ msg='allow list path')
+ self.assertEqual(traverse_obj(_TEST_DATA, (value for value in ("str",))), 'str',
+ msg='allow iterable path')
+ self.assertEqual(traverse_obj(_TEST_DATA, 'str'), 'str',
+ msg='single items should be treated as a path')
+ self.assertEqual(traverse_obj(_TEST_DATA, None), _TEST_DATA)
+ self.assertEqual(traverse_obj(_TEST_DATA, 100), 100)
+ self.assertEqual(traverse_obj(_TEST_DATA, 1.2), 1.2)
+
+ # Test Ellipsis behavior
+ self.assertCountEqual(traverse_obj(_TEST_DATA, ...),
+ (item for item in _TEST_DATA.values() if item is not None),
+ msg='`...` should give all values except `None`')
+ self.assertCountEqual(traverse_obj(_TEST_DATA, ('urls', 0, ...)), _TEST_DATA['urls'][0].values(),
+ msg='`...` selection for dicts should select all values')
+ self.assertEqual(traverse_obj(_TEST_DATA, (..., ..., 'url')),
+ ['https://www.example.com/0', 'https://www.example.com/1'],
+ msg='nested `...` queries should work')
+ self.assertCountEqual(traverse_obj(_TEST_DATA, (..., ..., 'index')), range(4),
+ msg='`...` query result should be flattened')
+
+ # Test function as key
+ self.assertEqual(traverse_obj(_TEST_DATA, lambda x, y: x == 'urls' and isinstance(y, list)),
+ [_TEST_DATA['urls']],
+ msg='function as query key should perform a filter based on (key, value)')
+ self.assertCountEqual(traverse_obj(_TEST_DATA, lambda _, x: isinstance(x[0], str)), {'str'},
+ msg='exceptions in the query function should be catched')
+
+ # Test alternative paths
+ self.assertEqual(traverse_obj(_TEST_DATA, 'fail', 'str'), 'str',
+ msg='multiple `paths` should be treated as alternative paths')
+ self.assertEqual(traverse_obj(_TEST_DATA, 'str', 100), 'str',
+ msg='alternatives should exit early')
+ self.assertEqual(traverse_obj(_TEST_DATA, 'fail', 'fail'), None,
+ msg='alternatives should return `default` if exhausted')
+ self.assertEqual(traverse_obj(_TEST_DATA, (..., 'fail'), 100), 100,
+ msg='alternatives should track their own branching return')
+ self.assertEqual(traverse_obj(_TEST_DATA, ('dict', ...), ('data', ...)), list(_TEST_DATA['data']),
+ msg='alternatives on empty objects should search further')
+
+ # Test branch and path nesting
+ self.assertEqual(traverse_obj(_TEST_DATA, ('urls', (3, 0), 'url')), ['https://www.example.com/0'],
+ msg='tuple as key should be treated as branches')
+ self.assertEqual(traverse_obj(_TEST_DATA, ('urls', [3, 0], 'url')), ['https://www.example.com/0'],
+ msg='list as key should be treated as branches')
+ self.assertEqual(traverse_obj(_TEST_DATA, ('urls', ((1, 'fail'), (0, 'url')))), ['https://www.example.com/0'],
+ msg='double nesting in path should be treated as paths')
+ self.assertEqual(traverse_obj(['0', [1, 2]], [(0, 1), 0]), [1],
+ msg='do not fail early on branching')
+ self.assertCountEqual(traverse_obj(_TEST_DATA, ('urls', ((1, ('fail', 'url')), (0, 'url')))),
+ ['https://www.example.com/0', 'https://www.example.com/1'],
+ msg='tripple nesting in path should be treated as branches')
+ self.assertEqual(traverse_obj(_TEST_DATA, ('urls', ('fail', (..., 'url')))),
+ ['https://www.example.com/0', 'https://www.example.com/1'],
+ msg='ellipsis as branch path start gets flattened')
+
+ # Test dictionary as key
+ self.assertEqual(traverse_obj(_TEST_DATA, {0: 100, 1: 1.2}), {0: 100, 1: 1.2},
+ msg='dict key should result in a dict with the same keys')
+ self.assertEqual(traverse_obj(_TEST_DATA, {0: ('urls', 0, 'url')}),
+ {0: 'https://www.example.com/0'},
+ msg='dict key should allow paths')
+ self.assertEqual(traverse_obj(_TEST_DATA, {0: ('urls', (3, 0), 'url')}),
+ {0: ['https://www.example.com/0']},
+ msg='tuple in dict path should be treated as branches')
+ self.assertEqual(traverse_obj(_TEST_DATA, {0: ('urls', ((1, 'fail'), (0, 'url')))}),
+ {0: ['https://www.example.com/0']},
+ msg='double nesting in dict path should be treated as paths')
+ self.assertEqual(traverse_obj(_TEST_DATA, {0: ('urls', ((1, ('fail', 'url')), (0, 'url')))}),
+ {0: ['https://www.example.com/1', 'https://www.example.com/0']},
+ msg='tripple nesting in dict path should be treated as branches')
+ self.assertEqual(traverse_obj(_TEST_DATA, {0: 'fail'}), {},
+ msg='remove `None` values when dict key')
+ self.assertEqual(traverse_obj(_TEST_DATA, {0: 'fail'}, default=...), {0: ...},
+ msg='do not remove `None` values if `default`')
+ self.assertEqual(traverse_obj(_TEST_DATA, {0: 'dict'}), {0: {}},
+ msg='do not remove empty values when dict key')
+ self.assertEqual(traverse_obj(_TEST_DATA, {0: 'dict'}, default=...), {0: {}},
+ msg='do not remove empty values when dict key and a default')
+ self.assertEqual(traverse_obj(_TEST_DATA, {0: ('dict', ...)}), {0: []},
+ msg='if branch in dict key not successful, return `[]`')
+
+ # Testing default parameter behavior
+ _DEFAULT_DATA = {'None': None, 'int': 0, 'list': []}
+ self.assertEqual(traverse_obj(_DEFAULT_DATA, 'fail'), None,
+ msg='default value should be `None`')
+ self.assertEqual(traverse_obj(_DEFAULT_DATA, 'fail', 'fail', default=...), ...,
+ msg='chained fails should result in default')
+ self.assertEqual(traverse_obj(_DEFAULT_DATA, 'None', 'int'), 0,
+ msg='should not short cirquit on `None`')
+ self.assertEqual(traverse_obj(_DEFAULT_DATA, 'fail', default=1), 1,
+ msg='invalid dict key should result in `default`')
+ self.assertEqual(traverse_obj(_DEFAULT_DATA, 'None', default=1), 1,
+ msg='`None` is a deliberate sentinel and should become `default`')
+ self.assertEqual(traverse_obj(_DEFAULT_DATA, ('list', 10)), None,
+ msg='`IndexError` should result in `default`')
+ self.assertEqual(traverse_obj(_DEFAULT_DATA, (..., 'fail'), default=1), 1,
+ msg='if branched but not successful return `default` if defined, not `[]`')
+ self.assertEqual(traverse_obj(_DEFAULT_DATA, (..., 'fail'), default=None), None,
+ msg='if branched but not successful return `default` even if `default` is `None`')
+ self.assertEqual(traverse_obj(_DEFAULT_DATA, (..., 'fail')), [],
+ msg='if branched but not successful return `[]`, not `default`')
+ self.assertEqual(traverse_obj(_DEFAULT_DATA, ('list', ...)), [],
+ msg='if branched but object is empty return `[]`, not `default`')
+
+ # Testing expected_type behavior
+ _EXPECTED_TYPE_DATA = {'str': 'str', 'int': 0}
+ self.assertEqual(traverse_obj(_EXPECTED_TYPE_DATA, 'str', expected_type=str), 'str',
+ msg='accept matching `expected_type` type')
+ self.assertEqual(traverse_obj(_EXPECTED_TYPE_DATA, 'str', expected_type=int), None,
+ msg='reject non matching `expected_type` type')
+ self.assertEqual(traverse_obj(_EXPECTED_TYPE_DATA, 'int', expected_type=lambda x: str(x)), '0',
+ msg='transform type using type function')
+ self.assertEqual(traverse_obj(_EXPECTED_TYPE_DATA, 'str',
+ expected_type=lambda _: 1 / 0), None,
+ msg='wrap expected_type fuction in try_call')
+ self.assertEqual(traverse_obj(_EXPECTED_TYPE_DATA, ..., expected_type=str), ['str'],
+ msg='eliminate items that expected_type fails on')
+
+ # Test get_all behavior
+ _GET_ALL_DATA = {'key': [0, 1, 2]}
+ self.assertEqual(traverse_obj(_GET_ALL_DATA, ('key', ...), get_all=False), 0,
+ msg='if not `get_all`, return only first matching value')
+ self.assertEqual(traverse_obj(_GET_ALL_DATA, ..., get_all=False), [0, 1, 2],
+ msg='do not overflatten if not `get_all`')
+
+ # Test casesense behavior
+ _CASESENSE_DATA = {
+ 'KeY': 'value0',
+ 0: {
+ 'KeY': 'value1',
+ 0: {'KeY': 'value2'},
+ },
+ }
+ self.assertEqual(traverse_obj(_CASESENSE_DATA, 'key'), None,
+ msg='dict keys should be case sensitive unless `casesense`')
+ self.assertEqual(traverse_obj(_CASESENSE_DATA, 'keY',
+ casesense=False), 'value0',
+ msg='allow non matching key case if `casesense`')
+ self.assertEqual(traverse_obj(_CASESENSE_DATA, (0, ('keY',)),
+ casesense=False), ['value1'],
+ msg='allow non matching key case in branch if `casesense`')
+ self.assertEqual(traverse_obj(_CASESENSE_DATA, (0, ((0, 'keY'),)),
+ casesense=False), ['value2'],
+ msg='allow non matching key case in branch path if `casesense`')
+
+ # Test traverse_string behavior
+ _TRAVERSE_STRING_DATA = {'str': 'str', 1.2: 1.2}
+ self.assertEqual(traverse_obj(_TRAVERSE_STRING_DATA, ('str', 0)), None,
+ msg='do not traverse into string if not `traverse_string`')
+ self.assertEqual(traverse_obj(_TRAVERSE_STRING_DATA, ('str', 0),
+ traverse_string=True), 's',
+ msg='traverse into string if `traverse_string`')
+ self.assertEqual(traverse_obj(_TRAVERSE_STRING_DATA, (1.2, 1),
+ traverse_string=True), '.',
+ msg='traverse into converted data if `traverse_string`')
+ self.assertEqual(traverse_obj(_TRAVERSE_STRING_DATA, ('str', ...),
+ traverse_string=True), list('str'),
+ msg='`...` branching into string should result in list')
+ self.assertEqual(traverse_obj(_TRAVERSE_STRING_DATA, ('str', (0, 2)),
+ traverse_string=True), ['s', 'r'],
+ msg='branching into string should result in list')
+ self.assertEqual(traverse_obj(_TRAVERSE_STRING_DATA, ('str', lambda _, x: x),
+ traverse_string=True), list('str'),
+ msg='function branching into string should result in list')
+
+ # Test is_user_input behavior
+ _IS_USER_INPUT_DATA = {'range8': list(range(8))}
+ self.assertEqual(traverse_obj(_IS_USER_INPUT_DATA, ('range8', '3'),
+ is_user_input=True), 3,
+ msg='allow for string indexing if `is_user_input`')
+ self.assertCountEqual(traverse_obj(_IS_USER_INPUT_DATA, ('range8', '3:'),
+ is_user_input=True), tuple(range(8))[3:],
+ msg='allow for string slice if `is_user_input`')
+ self.assertCountEqual(traverse_obj(_IS_USER_INPUT_DATA, ('range8', ':4:2'),
+ is_user_input=True), tuple(range(8))[:4:2],
+ msg='allow step in string slice if `is_user_input`')
+ self.assertCountEqual(traverse_obj(_IS_USER_INPUT_DATA, ('range8', ':'),
+ is_user_input=True), range(8),
+ msg='`:` should be treated as `...` if `is_user_input`')
+ with self.assertRaises(TypeError, msg='too many params should result in error'):
+ traverse_obj(_IS_USER_INPUT_DATA, ('range8', ':::'), is_user_input=True)
+
+ # Test re.Match as input obj
+ mobj = re.fullmatch(r'0(12)(?P
3)(4)?', '0123')
+ self.assertEqual(traverse_obj(mobj, ...), [x for x in mobj.groups() if x is not None],
+ msg='`...` on a `re.Match` should give its `groups()`')
+ self.assertEqual(traverse_obj(mobj, lambda k, _: k in (0, 2)), ['0123', '3'],
+ msg='function on a `re.Match` should give groupno, value starting at 0')
+ self.assertEqual(traverse_obj(mobj, 'group'), '3',
+ msg='str key on a `re.Match` should give group with that name')
+ self.assertEqual(traverse_obj(mobj, 2), '3',
+ msg='int key on a `re.Match` should give group with that name')
+ self.assertEqual(traverse_obj(mobj, 'gRoUp', casesense=False), '3',
+ msg='str key on a `re.Match` should respect casesense')
+ self.assertEqual(traverse_obj(mobj, 'fail'), None,
+ msg='failing str key on a `re.Match` should return `default`')
+ self.assertEqual(traverse_obj(mobj, 'gRoUpS', casesense=False), None,
+ msg='failing str key on a `re.Match` should return `default`')
+ self.assertEqual(traverse_obj(mobj, 8), None,
+ msg='failing int key on a `re.Match` should return `default`')
+
if __name__ == '__main__':
unittest.main()
diff --git a/test/test_youtube_signature.py b/test/test_youtube_signature.py
index 4fc2917e59..6d753fbf09 100644
--- a/test/test_youtube_signature.py
+++ b/test/test_youtube_signature.py
@@ -94,6 +94,46 @@ _NSIG_TESTS = [
'https://www.youtube.com/s/player/5dd88d1d/player-plasma-ias-phone-en_US.vflset/base.js',
'kSxKFLeqzv_ZyHSAt', 'n8gS8oRlHOxPFA',
),
+ (
+ 'https://www.youtube.com/s/player/324f67b9/player_ias.vflset/en_US/base.js',
+ 'xdftNy7dh9QGnhW', '22qLGxrmX8F1rA',
+ ),
+ (
+ 'https://www.youtube.com/s/player/4c3f79c5/player_ias.vflset/en_US/base.js',
+ 'TDCstCG66tEAO5pR9o', 'dbxNtZ14c-yWyw',
+ ),
+ (
+ 'https://www.youtube.com/s/player/c81bbb4a/player_ias.vflset/en_US/base.js',
+ 'gre3EcLurNY2vqp94', 'Z9DfGxWP115WTg',
+ ),
+ (
+ 'https://www.youtube.com/s/player/1f7d5369/player_ias.vflset/en_US/base.js',
+ 'batNX7sYqIJdkJ', 'IhOkL_zxbkOZBw',
+ ),
+ (
+ 'https://www.youtube.com/s/player/009f1d77/player_ias.vflset/en_US/base.js',
+ '5dwFHw8aFWQUQtffRq', 'audescmLUzI3jw',
+ ),
+ (
+ 'https://www.youtube.com/s/player/dc0c6770/player_ias.vflset/en_US/base.js',
+ '5EHDMgYLV6HPGk_Mu-kk', 'n9lUJLHbxUI0GQ',
+ ),
+ (
+ 'https://www.youtube.com/s/player/113ca41c/player_ias.vflset/en_US/base.js',
+ 'cgYl-tlYkhjT7A', 'hI7BBr2zUgcmMg',
+ ),
+ (
+ 'https://www.youtube.com/s/player/c57c113c/player_ias.vflset/en_US/base.js',
+ 'M92UUMHa8PdvPd3wyM', '3hPqLJsiNZx7yA',
+ ),
+ (
+ 'https://www.youtube.com/s/player/5a3b6271/player_ias.vflset/en_US/base.js',
+ 'B2j7f_UPT4rfje85Lu_e', 'm5DmNymaGQ5RdQ',
+ ),
+ (
+ 'https://www.youtube.com/s/player/7a062b77/player_ias.vflset/en_US/base.js',
+ 'NRcE3y3mVtm_cV-W', 'VbsCYUATvqlt5w',
+ ),
]
@@ -101,6 +141,7 @@ _NSIG_TESTS = [
class TestPlayerInfo(unittest.TestCase):
def test_youtube_extract_player_info(self):
PLAYER_URLS = (
+ ('https://www.youtube.com/s/player/4c3f79c5/player_ias.vflset/en_US/base.js', '4c3f79c5'),
('https://www.youtube.com/s/player/64dddad9/player_ias.vflset/en_US/base.js', '64dddad9'),
('https://www.youtube.com/s/player/64dddad9/player_ias.vflset/fr_FR/base.js', '64dddad9'),
('https://www.youtube.com/s/player/64dddad9/player-plasma-ias-phone-en_US.vflset/base.js', '64dddad9'),
diff --git a/test/testdata/ism/ec-3_test.Manifest b/test/testdata/ism/ec-3_test.Manifest
new file mode 100644
index 0000000000..45f95de73f
--- /dev/null
+++ b/test/testdata/ism/ec-3_test.Manifest
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/yt_dlp/YoutubeDL.py b/yt_dlp/YoutubeDL.py
index 25473611ba..42780e7941 100644
--- a/yt_dlp/YoutubeDL.py
+++ b/yt_dlp/YoutubeDL.py
@@ -29,6 +29,7 @@ from .cookies import load_cookies
from .downloader import FFmpegFD, get_suitable_downloader, shorten_protocol_name
from .downloader.rtmp import rtmpdump_version
from .extractor import gen_extractor_classes, get_info_extractor
+from .extractor.common import UnsupportedURLIE
from .extractor.openload import PhantomJSwrapper
from .minicurses import format_text
from .postprocessor import _PLUGIN_CLASSES as plugin_postprocessors
@@ -47,7 +48,7 @@ from .postprocessor import (
get_postprocessor,
)
from .postprocessor.ffmpeg import resolve_mapping as resolve_recode_mapping
-from .update import detect_variant
+from .update import REPOSITORY, current_git_head, detect_variant
from .utils import (
DEFAULT_OUTTMPL,
IDENTITY,
@@ -89,6 +90,7 @@ from .utils import (
args_to_str,
bug_reports_message,
date_from_str,
+ deprecation_warning,
determine_ext,
determine_protocol,
encode_compat_str,
@@ -106,6 +108,7 @@ from .utils import (
get_domain,
int_or_none,
iri_to_uri,
+ is_path_like,
join_nonempty,
locked_file,
make_archive_id,
@@ -115,6 +118,7 @@ from .utils import (
network_exceptions,
number_of_digits,
orderedSet,
+ orderedSet_from_options,
parse_filesize,
preferredencoding,
prepend_extension,
@@ -144,7 +148,7 @@ from .utils import (
write_json_file,
write_string,
)
-from .version import RELEASE_GIT_HEAD, __version__
+from .version import RELEASE_GIT_HEAD, VARIANT, __version__
if compat_os_name == 'nt':
import ctypes
@@ -236,7 +240,7 @@ class YoutubeDL:
Default is 'only_download' for CLI, but False for API
skip_playlist_after_errors: Number of allowed failures until the rest of
the playlist is skipped
- force_generic_extractor: Force downloader to use the generic extractor
+ allowed_extractors: List of regexes to match against extractor names that are allowed
overwrites: Overwrite all video and metadata files if True,
overwrite only non-video files if None
and don't overwrite any file if False
@@ -248,8 +252,8 @@ class YoutubeDL:
matchtitle: Download only matching titles.
rejecttitle: Reject downloads for matching titles.
logger: Log messages to a logging.Logger instance.
- logtostderr: Log messages to stderr instead of stdout.
- consoletitle: Display progress in console window's titlebar.
+ logtostderr: Print everything to stderr instead of stdout.
+ consoletitle: Display progress in console window's titlebar.
writedescription: Write the video description to a .description file
writeinfojson: Write the video description to a .info.json file
clean_infojson: Remove private fields from the infojson
@@ -272,7 +276,7 @@ class YoutubeDL:
subtitleslangs: List of languages of the subtitles to download (can be regex).
The list may contain "all" to refer to all the available
subtitles. The language can be prefixed with a "-" to
- exclude it from the requested languages. Eg: ['all', '-live_chat']
+ exclude it from the requested languages, e.g. ['all', '-live_chat']
keepvideo: Keep the video file after post-processing
daterange: A DateRange object, download only if the upload_date is in the range.
skip_download: Skip the actual download of the video file
@@ -290,9 +294,8 @@ class YoutubeDL:
downloaded.
Videos without view count information are always
downloaded. None for no limit.
- download_archive: File name of a file where all downloads are recorded.
- Videos already present in the file are not downloaded
- again.
+ download_archive: A set, or the name of a file where all downloads are recorded.
+ Videos already present in the file are not downloaded again.
break_on_existing: Stop the download process after attempting to download a
file that is in the archive.
break_on_reject: Stop the download process when encountering a video that
@@ -301,8 +304,9 @@ class YoutubeDL:
should act on each input URL as opposed to for the entire queue
cookiefile: File name or text stream from where cookies should be read and dumped to
cookiesfrombrowser: A tuple containing the name of the browser, the profile
- name/pathfrom where cookies are loaded, and the name of the
- keyring. Eg: ('chrome', ) or ('vivaldi', 'default', 'BASICTEXT')
+ name/path from where cookies are loaded, the name of the keyring,
+ and the container name, e.g. ('chrome', ) or
+ ('vivaldi', 'default', 'BASICTEXT') or ('firefox', 'default', None, 'Meta')
legacyserverconnect: Explicitly allow HTTPS connection to servers that do not
support RFC 5746 secure renegotiation
nocheckcertificate: Do not verify SSL certificates
@@ -444,6 +448,7 @@ class YoutubeDL:
* index: Section number (Optional)
force_keyframes_at_cuts: Re-encode the video when downloading ranges to get precise cuts
noprogress: Do not print the progress bar
+ live_from_start: Whether to download livestreams videos from the start
The following parameters are not used by YoutubeDL itself, they are used by
the downloader (see yt_dlp/downloader/common.py):
@@ -470,11 +475,13 @@ class YoutubeDL:
discontinuities such as ad breaks (default: False)
extractor_args: A dictionary of arguments to be passed to the extractors.
See "EXTRACTOR ARGUMENTS" for details.
- Eg: {'youtube': {'skip': ['dash', 'hls']}}
+ E.g. {'youtube': {'skip': ['dash', 'hls']}}
mark_watched: Mark videos watched (even with --simulate). Only for YouTube
The following options are deprecated and may be removed in the future:
+ force_generic_extractor: Force downloader to use the generic extractor
+ - Use allowed_extractors = ['generic', 'default']
playliststart: - Use playlist_items
Playlist item to start at.
playlistend: - Use playlist_items
@@ -527,7 +534,8 @@ class YoutubeDL:
"""
_NUMERIC_FIELDS = {
- 'width', 'height', 'tbr', 'abr', 'asr', 'vbr', 'fps', 'filesize', 'filesize_approx',
+ 'width', 'height', 'asr', 'audio_channels', 'fps',
+ 'tbr', 'abr', 'vbr', 'filesize', 'filesize_approx',
'timestamp', 'release_timestamp',
'duration', 'view_count', 'like_count', 'dislike_count', 'repost_count',
'average_rating', 'comment_count', 'age_limit',
@@ -539,8 +547,8 @@ class YoutubeDL:
_format_fields = {
# NB: Keep in sync with the docstring of extractor/common.py
'url', 'manifest_url', 'manifest_stream_number', 'ext', 'format', 'format_id', 'format_note',
- 'width', 'height', 'resolution', 'dynamic_range', 'tbr', 'abr', 'acodec', 'asr',
- 'vbr', 'fps', 'vcodec', 'container', 'filesize', 'filesize_approx',
+ 'width', 'height', 'resolution', 'dynamic_range', 'tbr', 'abr', 'acodec', 'asr', 'audio_channels',
+ 'vbr', 'fps', 'vcodec', 'container', 'filesize', 'filesize_approx', 'rows', 'columns',
'player_url', 'protocol', 'fragment_base_url', 'fragments', 'is_from_start',
'preference', 'language', 'language_preference', 'quality', 'source_preference',
'http_headers', 'stretched_ratio', 'no_resume', 'has_drm', 'downloader_options',
@@ -625,7 +633,7 @@ class YoutubeDL:
for msg in self.params.get('_warnings', []):
self.report_warning(msg)
for msg in self.params.get('_deprecation_warnings', []):
- self.deprecation_warning(msg)
+ self.deprecated_feature(msg)
self.params['compat_opts'] = set(self.params.get('compat_opts', ()))
if 'list-formats' in self.params['compat_opts']:
@@ -715,21 +723,23 @@ class YoutubeDL:
def preload_download_archive(fn):
"""Preload the archive, if any is specified"""
+ archive = set()
if fn is None:
- return False
+ return archive
+ elif not is_path_like(fn):
+ return fn
+
self.write_debug(f'Loading archive file {fn!r}')
try:
with locked_file(fn, 'r', encoding='utf-8') as archive_file:
for line in archive_file:
- self.archive.add(line.strip())
+ archive.add(line.strip())
except OSError as ioe:
if ioe.errno != errno.ENOENT:
raise
- return False
- return True
+ return archive
- self.archive = set()
- preload_download_archive(self.params.get('download_archive'))
+ self.archive = preload_download_archive(self.params.get('download_archive'))
def warn_if_short_id(self, argv):
# short YouTube ID starting with dash?
@@ -755,13 +765,6 @@ class YoutubeDL:
self._ies_instances[ie_key] = ie
ie.set_downloader(self)
- def _get_info_extractor_class(self, ie_key):
- ie = self._ies.get(ie_key)
- if ie is None:
- ie = get_info_extractor(ie_key)
- self.add_info_extractor(ie)
- return ie
-
def get_info_extractor(self, ie_key):
"""
Get an instance of an IE with name ie_key, it will try to get one from
@@ -778,8 +781,19 @@ class YoutubeDL:
"""
Add the InfoExtractors returned by gen_extractors to the end of the list
"""
- for ie in gen_extractor_classes():
- self.add_info_extractor(ie)
+ all_ies = {ie.IE_NAME.lower(): ie for ie in gen_extractor_classes()}
+ all_ies['end'] = UnsupportedURLIE()
+ try:
+ ie_names = orderedSet_from_options(
+ self.params.get('allowed_extractors', ['default']), {
+ 'all': list(all_ies),
+ 'default': [name for name, ie in all_ies.items() if ie._ENABLED],
+ }, use_regex=True)
+ except re.error as e:
+ raise ValueError(f'Wrong regex for allowed_extractors: {e.pattern}')
+ for name in ie_names:
+ self.add_info_extractor(all_ies[name])
+ self.write_debug(f'Loaded {len(ie_names)} extractors')
def add_post_processor(self, pp, when='post_process'):
"""Add a PostProcessor object to the end of the chain."""
@@ -825,12 +839,14 @@ class YoutubeDL:
def to_stdout(self, message, skip_eol=False, quiet=None):
"""Print message to stdout"""
if quiet is not None:
- self.deprecation_warning('"YoutubeDL.to_stdout" no longer accepts the argument quiet. Use "YoutubeDL.to_screen" instead')
+ self.deprecation_warning('"YoutubeDL.to_stdout" no longer accepts the argument quiet. '
+ 'Use "YoutubeDL.to_screen" instead')
if skip_eol is not False:
- self.deprecation_warning('"YoutubeDL.to_stdout" no longer accepts the argument skip_eol. Use "YoutubeDL.to_screen" instead')
+ self.deprecation_warning('"YoutubeDL.to_stdout" no longer accepts the argument skip_eol. '
+ 'Use "YoutubeDL.to_screen" instead')
self._write_string(f'{self._bidi_workaround(message)}\n', self._out_files.out)
- def to_screen(self, message, skip_eol=False, quiet=None):
+ def to_screen(self, message, skip_eol=False, quiet=None, only_once=False):
"""Print message to screen if not in quiet mode"""
if self.params.get('logger'):
self.params['logger'].debug(message)
@@ -839,7 +855,7 @@ class YoutubeDL:
return
self._write_string(
'%s%s' % (self._bidi_workaround(message), ('' if skip_eol else '\n')),
- self._out_files.screen)
+ self._out_files.screen, only_once=only_once)
def to_stderr(self, message, only_once=False):
"""Print message to stderr"""
@@ -963,11 +979,14 @@ class YoutubeDL:
return
self.to_stderr(f'{self._format_err("WARNING:", self.Styles.WARNING)} {message}', only_once)
- def deprecation_warning(self, message):
+ def deprecation_warning(self, message, *, stacklevel=0):
+ deprecation_warning(
+ message, stacklevel=stacklevel + 1, printer=self.report_error, is_error=False)
+
+ def deprecated_feature(self, message):
if self.params.get('logger') is not None:
- self.params['logger'].warning(f'DeprecationWarning: {message}')
- else:
- self.to_stderr(f'{self._format_err("DeprecationWarning:", self.Styles.ERROR)} {message}', True)
+ self.params['logger'].warning(f'Deprecated Feature: {message}')
+ self.to_stderr(f'{self._format_err("Deprecated Feature:", self.Styles.ERROR)} {message}', True)
def report_error(self, message, *args, **kwargs):
'''
@@ -1027,7 +1046,7 @@ class YoutubeDL:
def get_output_path(self, dir_type='', filename=None):
paths = self.params.get('paths', {})
- assert isinstance(paths, dict)
+ assert isinstance(paths, dict), '"paths" parameter must be a dictionary'
path = os.path.join(
expand_path(paths.get('home', '').strip()),
expand_path(paths.get(dir_type, '').strip()) if dir_type else '',
@@ -1045,7 +1064,7 @@ class YoutubeDL:
# outtmpl should be expand_path'ed before template dict substitution
# because meta fields may contain env variables we don't want to
- # be expanded. For example, for outtmpl "%(title)s.%(ext)s" and
+ # be expanded. E.g. for outtmpl "%(title)s.%(ext)s" and
# title "Hello $PATH", we don't want `$PATH` to be expanded.
return expand_path(outtmpl).replace(sep, '')
@@ -1110,8 +1129,12 @@ class YoutubeDL:
'-': float.__sub__,
}
# Field is of the form key1.key2...
- # where keys (except first) can be string, int or slice
- FIELD_RE = r'\w*(?:\.(?:\w+|{num}|{num}?(?::{num}?){{1,2}}))*'.format(num=r'(?:-?\d+)')
+ # where keys (except first) can be string, int, slice or "{field, ...}"
+ FIELD_INNER_RE = r'(?:\w+|%(num)s|%(num)s?(?::%(num)s?){1,2})' % {'num': r'(?:-?\d+)'}
+ FIELD_RE = r'\w*(?:\.(?:%(inner)s|{%(field)s(?:,%(field)s)*}))*' % {
+ 'inner': FIELD_INNER_RE,
+ 'field': rf'\w*(?:\.{FIELD_INNER_RE})*'
+ }
MATH_FIELD_RE = rf'(?:{FIELD_RE}|-?{NUMBER_RE})'
MATH_OPERATORS_RE = r'(?:%s)' % '|'.join(map(re.escape, MATH_FUNCTIONS.keys()))
INTERNAL_FORMAT_RE = re.compile(rf'''(?x)
@@ -1125,11 +1148,20 @@ class YoutubeDL:
(?:\|(?P.*?))?
)$''')
- def _traverse_infodict(k):
- k = k.split('.')
- if k[0] == '':
- k.pop(0)
- return traverse_obj(info_dict, k, is_user_input=True, traverse_string=True)
+ def _traverse_infodict(fields):
+ fields = [f for x in re.split(r'\.({.+?})\.?', fields)
+ for f in ([x] if x.startswith('{') else x.split('.'))]
+ for i in (0, -1):
+ if fields and not fields[i]:
+ fields.pop(i)
+
+ for i, f in enumerate(fields):
+ if not f.startswith('{'):
+ continue
+ assert f.endswith('}'), f'No closing brace for {f} in {fields}'
+ fields[i] = {k: k.split('.') for k in f[1:-1].split(',')}
+
+ return traverse_obj(info_dict, fields, is_user_input=True, traverse_string=True)
def get_value(mdict):
# Object traversal
@@ -1215,9 +1247,11 @@ class YoutubeDL:
delim = '\n' if '#' in flags else ', '
value, fmt = delim.join(map(str, variadic(value, allowed_types=(str, bytes)))), str_fmt
elif fmt[-1] == 'j': # json
- value, fmt = json.dumps(value, default=_dumpjson_default, indent=4 if '#' in flags else None), str_fmt
+ value, fmt = json.dumps(
+ value, default=_dumpjson_default,
+ indent=4 if '#' in flags else None, ensure_ascii='+' not in flags), str_fmt
elif fmt[-1] == 'h': # html
- value, fmt = escapeHTML(value), str_fmt
+ value, fmt = escapeHTML(str(value)), str_fmt
elif fmt[-1] == 'q': # quoted
value = map(str, variadic(value) if '#' in flags else [value])
value, fmt = ' '.join(map(compat_shlex_quote, value)), str_fmt
@@ -1389,18 +1423,19 @@ class YoutubeDL:
def extract_info(self, url, download=True, ie_key=None, extra_info=None,
process=True, force_generic_extractor=False):
"""
- Return a list with a dictionary for each video extracted.
+ Extract and return the information dictionary of the URL
Arguments:
- url -- URL to extract
+ @param url URL to extract
Keyword arguments:
- download -- whether to download videos during extraction
- ie_key -- extractor key hint
- extra_info -- dictionary containing the extra values to add to each result
- process -- whether to resolve all unresolved references (URLs, playlist items),
- must be True for download to work.
- force_generic_extractor -- force using the generic extractor
+ @param download Whether to download videos
+ @param process Whether to resolve all unresolved references (URLs, playlist items).
+ Must be True for download to work
+ @param ie_key Use only the extractor with this key
+
+ @param extra_info Dictionary containing the extra values to add to the info (For internal use only)
+ @force_generic_extractor Force using the generic extractor (Deprecated; use ie_key='Generic')
"""
if extra_info is None:
@@ -1410,11 +1445,11 @@ class YoutubeDL:
ie_key = 'Generic'
if ie_key:
- ies = {ie_key: self._get_info_extractor_class(ie_key)}
+ ies = {ie_key: self._ies[ie_key]} if ie_key in self._ies else {}
else:
ies = self._ies
- for ie_key, ie in ies.items():
+ for key, ie in ies.items():
if not ie.suitable(url):
continue
@@ -1423,14 +1458,16 @@ class YoutubeDL:
'and will probably not work.')
temp_id = ie.get_temp_id(url)
- if temp_id is not None and self.in_download_archive({'id': temp_id, 'ie_key': ie_key}):
- self.to_screen(f'[{ie_key}] {temp_id}: has already been recorded in the archive')
+ if temp_id is not None and self.in_download_archive({'id': temp_id, 'ie_key': key}):
+ self.to_screen(f'[{key}] {temp_id}: has already been recorded in the archive')
if self.params.get('break_on_existing', False):
raise ExistingVideoReached()
break
- return self.__extract_info(url, self.get_info_extractor(ie_key), download, extra_info, process)
+ return self.__extract_info(url, self.get_info_extractor(key), download, extra_info, process)
else:
- self.report_error('no suitable InfoExtractor for URL %s' % url)
+ extractors_restricted = self.params.get('allowed_extractors') not in (None, ['default'])
+ self.report_error(f'No suitable extractor{format_field(ie_key, None, " (%s)")} found for URL {url}',
+ tb=False if extractors_restricted else None)
def _handle_extraction_exceptions(func):
@functools.wraps(func)
@@ -1584,6 +1621,7 @@ class YoutubeDL:
self.add_default_extra_info(info_copy, ie, ie_result['url'])
self.add_extra_info(info_copy, extra_info)
info_copy, _ = self.pre_process(info_copy)
+ self._fill_common_fields(info_copy, False)
self.__forced_printings(info_copy, self.prepare_filename(info_copy), incomplete=True)
self._raise_pending_errors(info_copy)
if self.params.get('force_write_download_archive', False):
@@ -1650,8 +1688,8 @@ class YoutubeDL:
elif result_type in ('playlist', 'multi_video'):
# Protect from infinite recursion due to recursively nested playlists
# (see https://github.com/ytdl-org/youtube-dl/issues/27833)
- webpage_url = ie_result['webpage_url']
- if webpage_url in self._playlist_urls:
+ webpage_url = ie_result.get('webpage_url') # Playlists maynot have webpage_url
+ if webpage_url and webpage_url in self._playlist_urls:
self.to_screen(
'[download] Skipping already downloaded playlist: %s'
% ie_result.get('title') or ie_result.get('id'))
@@ -1705,14 +1743,17 @@ class YoutubeDL:
}
if strict:
return info
+ if ie_result.get('webpage_url'):
+ info.update({
+ 'webpage_url': ie_result['webpage_url'],
+ 'webpage_url_basename': url_basename(ie_result['webpage_url']),
+ 'webpage_url_domain': get_domain(ie_result['webpage_url']),
+ })
return {
**info,
'playlist_index': 0,
'__last_playlist_index': max(ie_result['requested_entries'] or (0, 0)),
'extractor': ie_result['extractor'],
- 'webpage_url': ie_result['webpage_url'],
- 'webpage_url_basename': url_basename(ie_result['webpage_url']),
- 'webpage_url_domain': get_domain(ie_result['webpage_url']),
'extractor_key': ie_result['extractor_key'],
}
@@ -1796,6 +1837,8 @@ class YoutubeDL:
})
if self._match_entry(entry_copy, incomplete=True) is not None:
+ # For compatabilty with youtube-dl. See https://github.com/yt-dlp/yt-dlp/issues/4369
+ resolved_entries[i] = (playlist_index, NO_DEFAULT)
continue
self.to_screen('[download] Downloading video %s of %s' % (
@@ -1816,7 +1859,8 @@ class YoutubeDL:
resolved_entries[i] = (playlist_index, entry_result)
# Update with processed data
- ie_result['requested_entries'], ie_result['entries'] = tuple(zip(*resolved_entries)) or ([], [])
+ ie_result['requested_entries'] = [i for i, e in resolved_entries if e is not NO_DEFAULT]
+ ie_result['entries'] = [e for _, e in resolved_entries if e is not NO_DEFAULT]
# Write the updated info to json
if _infojson_written is True and self._write_info_json(
@@ -1973,8 +2017,8 @@ class YoutubeDL:
filter_parts.append(string)
def _remove_unused_ops(tokens):
- # Remove operators that we don't use and join them with the surrounding strings
- # for example: 'mp4' '-' 'baseline' '-' '16x9' is converted to 'mp4-baseline-16x9'
+ # Remove operators that we don't use and join them with the surrounding strings.
+ # E.g. 'mp4' '-' 'baseline' '-' '16x9' is converted to 'mp4-baseline-16x9'
ALLOWED_OPS = ('/', '+', ',', '(', ')')
last_string, last_start, last_end, last_line = None, None, None, None
for type, string, start, end, line in tokens:
@@ -2129,6 +2173,7 @@ class YoutubeDL:
'acodec': the_only_audio.get('acodec'),
'abr': the_only_audio.get('abr'),
'asr': the_only_audio.get('asr'),
+ 'audio_channels': the_only_audio.get('audio_channels')
})
return new_dict
@@ -2335,10 +2380,9 @@ class YoutubeDL:
else:
info_dict['thumbnails'] = thumbnails
- def _fill_common_fields(self, info_dict, is_video=True):
+ def _fill_common_fields(self, info_dict, final=True):
# TODO: move sanitization here
- if is_video:
- # playlists are allowed to lack "title"
+ if final:
title = info_dict.get('title', NO_DEFAULT)
if title is NO_DEFAULT:
raise ExtractorError('Missing "title" field in extractor result',
@@ -2382,11 +2426,13 @@ class YoutubeDL:
for key in live_keys:
if info_dict.get(key) is None:
info_dict[key] = (live_status == key)
+ if live_status == 'post_live':
+ info_dict['was_live'] = True
# Auto generate title fields corresponding to the *_number fields when missing
# in order to always have clean titles. This is very common for TV series.
for field in ('chapter', 'season', 'episode'):
- if info_dict.get('%s_number' % field) is not None and not info_dict.get(field):
+ if final and info_dict.get('%s_number' % field) is not None and not info_dict.get(field):
info_dict[field] = '%s %d' % (field.capitalize(), info_dict['%s_number' % field])
def _raise_pending_errors(self, info):
@@ -2479,21 +2525,17 @@ class YoutubeDL:
info_dict['requested_subtitles'] = self.process_subtitles(
info_dict['id'], subtitles, automatic_captions)
- if info_dict.get('formats') is None:
- # There's only one format available
- formats = [info_dict]
- else:
- formats = info_dict['formats']
+ formats = self._get_formats(info_dict)
# or None ensures --clean-infojson removes it
info_dict['_has_drm'] = any(f.get('has_drm') for f in formats) or None
if not self.params.get('allow_unplayable_formats'):
formats = [f for f in formats if not f.get('has_drm')]
- if info_dict['_has_drm'] and formats and all(
- f.get('acodec') == f.get('vcodec') == 'none' for f in formats):
- self.report_warning(
- 'This video is DRM protected and only images are available for download. '
- 'Use --list-formats to see them')
+
+ if formats and all(f.get('acodec') == f.get('vcodec') == 'none' for f in formats):
+ self.report_warning(
+ f'{"This video is DRM protected and " if info_dict["_has_drm"] else ""}'
+ 'only images are available for download. Use --list-formats to see them'.capitalize())
get_from_start = not info_dict.get('is_live') or bool(self.params.get('live_from_start'))
if not get_from_start:
@@ -2505,9 +2547,6 @@ class YoutubeDL:
'--live-from-start is passed, but there are no formats that can be downloaded from the start. '
'If you want to download from the current time, use --no-live-from-start'))
- if not formats:
- self.raise_no_formats(info_dict)
-
def is_wellformed(f):
url = f.get('url')
if not url:
@@ -2520,7 +2559,10 @@ class YoutubeDL:
return True
# Filter out malformed formats for better extraction robustness
- formats = list(filter(is_wellformed, formats))
+ formats = list(filter(is_wellformed, formats or []))
+
+ if not formats:
+ self.raise_no_formats(info_dict)
formats_dict = {}
@@ -2598,7 +2640,7 @@ class YoutubeDL:
info_dict, _ = self.pre_process(info_dict, 'after_filter')
# The pre-processors may have modified the formats
- formats = info_dict.get('formats', [info_dict])
+ formats = self._get_formats(info_dict)
list_only = self.params.get('simulate') is None and (
self.params.get('list_thumbnails') or self.params.get('listformats') or self.params.get('listsubtitles'))
@@ -2656,31 +2698,30 @@ class YoutubeDL:
# Process what we can, even without any available formats.
formats_to_download = [{}]
- requested_ranges = self.params.get('download_ranges')
- if requested_ranges:
- requested_ranges = tuple(requested_ranges(info_dict, self))
-
+ requested_ranges = tuple(self.params.get('download_ranges', lambda *_: [{}])(info_dict, self))
best_format, downloaded_formats = formats_to_download[-1], []
if download:
- if best_format:
+ if best_format and requested_ranges:
def to_screen(*msg):
self.to_screen(f'[info] {info_dict["id"]}: {" ".join(", ".join(variadic(m)) for m in msg)}')
to_screen(f'Downloading {len(formats_to_download)} format(s):',
(f['format_id'] for f in formats_to_download))
- if requested_ranges:
+ if requested_ranges != ({}, ):
to_screen(f'Downloading {len(requested_ranges)} time ranges:',
- (f'{int(c["start_time"])}-{int(c["end_time"])}' for c in requested_ranges))
+ (f'{c["start_time"]:.1f}-{c["end_time"]:.1f}' for c in requested_ranges))
max_downloads_reached = False
- for fmt, chapter in itertools.product(formats_to_download, requested_ranges or [{}]):
+ for fmt, chapter in itertools.product(formats_to_download, requested_ranges):
new_info = self._copy_infodict(info_dict)
new_info.update(fmt)
offset, duration = info_dict.get('section_start') or 0, info_dict.get('duration') or float('inf')
+ end_time = offset + min(chapter.get('end_time', duration), duration)
if chapter or offset:
new_info.update({
'section_start': offset + chapter.get('start_time', 0),
- 'section_end': offset + min(chapter.get('end_time', duration), duration),
+ # duration may not be accurate. So allow deviations <1sec
+ 'section_end': end_time if end_time <= offset + duration + 1 else None,
'section_title': chapter.get('title'),
'section_number': chapter.get('index'),
})
@@ -2722,42 +2763,26 @@ class YoutubeDL:
if lang not in available_subs:
available_subs[lang] = cap_info
- if (not self.params.get('writesubtitles') and not
- self.params.get('writeautomaticsub') or not
- available_subs):
+ if not available_subs or (
+ not self.params.get('writesubtitles')
+ and not self.params.get('writeautomaticsub')):
return None
all_sub_langs = tuple(available_subs.keys())
if self.params.get('allsubtitles', False):
requested_langs = all_sub_langs
elif self.params.get('subtitleslangs', False):
- # A list is used so that the order of languages will be the same as
- # given in subtitleslangs. See https://github.com/yt-dlp/yt-dlp/issues/1041
- requested_langs = []
- for lang_re in self.params.get('subtitleslangs'):
- discard = lang_re[0] == '-'
- if discard:
- lang_re = lang_re[1:]
- if lang_re == 'all':
- if discard:
- requested_langs = []
- else:
- requested_langs.extend(all_sub_langs)
- continue
- current_langs = filter(re.compile(lang_re + '$').match, all_sub_langs)
- if discard:
- for lang in current_langs:
- while lang in requested_langs:
- requested_langs.remove(lang)
- else:
- requested_langs.extend(current_langs)
- requested_langs = orderedSet(requested_langs)
+ try:
+ requested_langs = orderedSet_from_options(
+ self.params.get('subtitleslangs'), {'all': all_sub_langs}, use_regex=True)
+ except re.error as e:
+ raise ValueError(f'Wrong regex for subtitlelangs: {e.pattern}')
elif normal_sub_langs:
requested_langs = ['en'] if 'en' in normal_sub_langs else normal_sub_langs[:1]
else:
requested_langs = ['en'] if 'en' in all_sub_langs else all_sub_langs[:1]
if requested_langs:
- self.write_debug('Downloading subtitles: %s' % ', '.join(requested_langs))
+ self.to_screen(f'[info] {video_id}: Downloading subtitles: {", ".join(requested_langs)}')
formats_query = self.params.get('subtitlesformat', 'best')
formats_preference = formats_query.split('/') if formats_query else []
@@ -2793,12 +2818,16 @@ class YoutubeDL:
info_copy['automatic_captions_table'] = self.render_subtitles_table(info_dict.get('id'), info_dict.get('automatic_captions'))
def format_tmpl(tmpl):
- mobj = re.match(r'\w+(=?)$', tmpl)
- if mobj and mobj.group(1):
- return f'{tmpl[:-1]} = %({tmpl[:-1]})r'
- elif mobj:
- return f'%({tmpl})s'
- return tmpl
+ mobj = re.fullmatch(r'([\w.:,]|-\d|(?P{([\w.:,]|-\d)+}))+=?', tmpl)
+ if not mobj:
+ return tmpl
+
+ fmt = '%({})s'
+ if tmpl.startswith('{'):
+ tmpl = f'.{tmpl}'
+ if tmpl.endswith('='):
+ tmpl, fmt = tmpl[:-1], '{0} = %({0})#j'
+ return '\n'.join(map(fmt.format, [tmpl] if mobj.group('dict') else tmpl.split(',')))
for tmpl in self.params['forceprint'].get(key, []):
self.to_stdout(self.evaluate_outtmpl(format_tmpl(tmpl), info_copy))
@@ -3265,6 +3294,7 @@ class YoutubeDL:
self.to_screen(f'[info] {e}')
if not self.params.get('break_per_url'):
raise
+ self._num_downloads = 0
else:
if self.params.get('dump_single_json', False):
self.post_extract(res)
@@ -3313,6 +3343,12 @@ class YoutubeDL:
return info_dict
info_dict.setdefault('epoch', int(time.time()))
info_dict.setdefault('_type', 'video')
+ info_dict.setdefault('_version', {
+ 'version': __version__,
+ 'current_git_head': current_git_head(),
+ 'release_git_head': RELEASE_GIT_HEAD,
+ 'repository': REPOSITORY,
+ })
if remove_private_keys:
reject = lambda k, v: v is None or k.startswith('__') or k in {
@@ -3433,12 +3469,11 @@ class YoutubeDL:
return make_archive_id(extractor, video_id)
def in_download_archive(self, info_dict):
- fn = self.params.get('download_archive')
- if fn is None:
+ if not self.archive:
return False
vid_ids = [self._make_archive_id(info_dict)]
- vid_ids.extend(info_dict.get('_old_archive_ids', []))
+ vid_ids.extend(info_dict.get('_old_archive_ids') or [])
return any(id_ in self.archive for id_ in vid_ids)
def record_download_archive(self, info_dict):
@@ -3447,9 +3482,11 @@ class YoutubeDL:
return
vid_id = self._make_archive_id(info_dict)
assert vid_id
+
self.write_debug(f'Adding to archive: {vid_id}')
- with locked_file(fn, 'a', encoding='utf-8') as archive_file:
- archive_file.write(vid_id + '\n')
+ if is_path_like(fn):
+ with locked_file(fn, 'a', encoding='utf-8') as archive_file:
+ archive_file.write(vid_id + '\n')
self.archive.add(vid_id)
@staticmethod
@@ -3531,11 +3568,17 @@ class YoutubeDL:
res += '~' + format_bytes(fdict['filesize_approx'])
return res
- def render_formats_table(self, info_dict):
- if not info_dict.get('formats') and not info_dict.get('url'):
- return None
+ def _get_formats(self, info_dict):
+ if info_dict.get('formats') is None:
+ if info_dict.get('url') and info_dict.get('_type', 'video') == 'video':
+ return [info_dict]
+ return []
+ return info_dict['formats']
- formats = info_dict.get('formats', [info_dict])
+ def render_formats_table(self, info_dict):
+ formats = self._get_formats(info_dict)
+ if not formats:
+ return
if not self.params.get('listformats_table', True) is not False:
table = [
[
@@ -3543,7 +3586,7 @@ class YoutubeDL:
format_field(f, 'ext'),
self.format_resolution(f),
self._format_note(f)
- ] for f in formats if f.get('preference') is None or f['preference'] >= -1000]
+ ] for f in formats if (f.get('preference') or 0) >= -1000]
return render_table(['format code', 'extension', 'resolution', 'note'], table, extra_gap=1)
def simplified_codec(f, field):
@@ -3569,6 +3612,7 @@ class YoutubeDL:
format_field(f, func=self.format_resolution, ignore=('audio only', 'images')),
format_field(f, 'fps', '\t%d', func=round),
format_field(f, 'dynamic_range', '%s', ignore=(None, 'SDR')).replace('HDR', ''),
+ format_field(f, 'audio_channels', '\t%s'),
delim,
format_field(f, 'filesize', ' \t%s', func=format_bytes) + format_field(f, 'filesize_approx', '~\t%s', func=format_bytes),
format_field(f, 'tbr', '\t%dk', func=round),
@@ -3588,7 +3632,7 @@ class YoutubeDL:
delim=' '),
] for f in formats if f.get('preference') is None or f['preference'] >= -1000]
header_line = self._list_format_headers(
- 'ID', 'EXT', 'RESOLUTION', '\tFPS', 'HDR', delim, '\tFILESIZE', '\tTBR', 'PROTO',
+ 'ID', 'EXT', 'RESOLUTION', '\tFPS', 'HDR', 'CH', delim, '\tFILESIZE', '\tTBR', 'PROTO',
delim, 'VCODEC', '\tVBR', 'ACODEC', '\tABR', '\tASR', 'MORE INFO')
return render_table(
@@ -3601,7 +3645,7 @@ class YoutubeDL:
return None
return render_table(
self._list_format_headers('ID', 'Width', 'Height', 'URL'),
- [[t.get('id'), t.get('width', 'unknown'), t.get('height', 'unknown'), t['url']] for t in thumbnails])
+ [[t.get('id'), t.get('width') or 'unknown', t.get('height') or 'unknown', t['url']] for t in thumbnails])
def render_subtitles_table(self, video_id, subtitles):
def _row(lang, formats):
@@ -3644,6 +3688,8 @@ class YoutubeDL:
if not self.params.get('verbose'):
return
+ from . import _IN_CLI # Must be delayed import
+
# These imports can be slow. So import them only as needed
from .extractor.extractors import _LAZY_LOADER
from .extractor.extractors import _PLUGIN_CLASSES as plugin_extractors
@@ -3673,10 +3719,14 @@ class YoutubeDL:
write_debug = lambda msg: self._write_string(f'[debug] {msg}\n')
source = detect_variant()
+ if VARIANT not in (None, 'pip'):
+ source += '*'
write_debug(join_nonempty(
- 'yt-dlp version', __version__,
+ f'{"yt-dlp" if REPOSITORY == "yt-dlp/yt-dlp" else REPOSITORY} version',
+ __version__,
f'[{RELEASE_GIT_HEAD}]' if RELEASE_GIT_HEAD else '',
'' if source == 'unknown' else f'({source})',
+ '' if _IN_CLI else 'API',
delim=' '))
if not _LAZY_LOADER:
if os.environ.get('YTDLP_NO_LAZY_EXTRACTORS'):
@@ -3690,18 +3740,8 @@ class YoutubeDL:
if self.params['compat_opts']:
write_debug('Compatibility options: %s' % ', '.join(self.params['compat_opts']))
- if source == 'source':
- try:
- stdout, _, _ = Popen.run(
- ['git', 'rev-parse', '--short', 'HEAD'],
- text=True, cwd=os.path.dirname(os.path.abspath(__file__)),
- stdout=subprocess.PIPE, stderr=subprocess.PIPE)
- if re.fullmatch('[0-9a-f]+', stdout.strip()):
- write_debug(f'Git HEAD: {stdout.strip()}')
- except Exception:
- with contextlib.suppress(Exception):
- sys.exc_clear()
-
+ if current_git_head():
+ write_debug(f'Git HEAD: {current_git_head()}')
write_debug(system_identifier())
exe_versions, ffmpeg_features = FFmpegPostProcessor.get_versions_and_features(self)
diff --git a/yt_dlp/__init__.py b/yt_dlp/__init__.py
index 317dd26231..726fb0685c 100644
--- a/yt_dlp/__init__.py
+++ b/yt_dlp/__init__.py
@@ -63,6 +63,8 @@ from .utils import (
)
from .YoutubeDL import YoutubeDL
+_IN_CLI = False
+
def _exit(status=0, *args):
for msg in args:
@@ -324,14 +326,15 @@ def validate_options(opts):
def parse_chapters(name, value):
chapters, ranges = [], []
+ parse_timestamp = lambda x: float('inf') if x in ('inf', 'infinite') else parse_duration(x)
for regex in value or []:
if regex.startswith('*'):
- for range in regex[1:].split(','):
- dur = tuple(map(parse_duration, range.strip().split('-')))
- if len(dur) == 2 and all(t is not None for t in dur):
- ranges.append(dur)
- else:
+ for range_ in map(str.strip, regex[1:].split(',')):
+ mobj = range_ != '-' and re.fullmatch(r'([^-]+)?\s*-\s*([^-]+)?', range_)
+ dur = mobj and (parse_timestamp(mobj.group(1) or '0'), parse_timestamp(mobj.group(2) or 'inf'))
+ if None in (dur or [None]):
raise ValueError(f'invalid {name} time range "{regex}". Must be of the form *start-end')
+ ranges.append(dur)
continue
try:
chapters.append(re.compile(regex))
@@ -344,10 +347,16 @@ def validate_options(opts):
# Cookies from browser
if opts.cookiesfrombrowser:
- mobj = re.match(r'(?P[^+:]+)(\s*\+\s*(?P[^:]+))?(\s*:(?P.+))?', opts.cookiesfrombrowser)
+ container = None
+ mobj = re.fullmatch(r'''(?x)
+ (?P[^+:]+)
+ (?:\s*\+\s*(?P[^:]+))?
+ (?:\s*:\s*(?P.+?))?
+ (?:\s*::\s*(?P.+))?
+ ''', opts.cookiesfrombrowser)
if mobj is None:
raise ValueError(f'invalid cookies from browser arguments: {opts.cookiesfrombrowser}')
- browser_name, keyring, profile = mobj.group('name', 'keyring', 'profile')
+ browser_name, keyring, profile, container = mobj.group('name', 'keyring', 'profile', 'container')
browser_name = browser_name.lower()
if browser_name not in SUPPORTED_BROWSERS:
raise ValueError(f'unsupported browser specified for cookies: "{browser_name}". '
@@ -357,7 +366,7 @@ def validate_options(opts):
if keyring not in SUPPORTED_KEYRINGS:
raise ValueError(f'unsupported keyring specified for cookies: "{keyring}". '
f'Supported keyrings are: {", ".join(sorted(SUPPORTED_KEYRINGS))}')
- opts.cookiesfrombrowser = (browser_name, profile, keyring)
+ opts.cookiesfrombrowser = (browser_name, profile, keyring, container)
# MetadataParser
def metadataparser_actions(f):
@@ -402,6 +411,9 @@ def validate_options(opts):
if opts.download_archive is not None:
opts.download_archive = expand_path(opts.download_archive)
+ if opts.ffmpeg_location is not None:
+ opts.ffmpeg_location = expand_path(opts.ffmpeg_location)
+
if opts.user_agent is not None:
opts.headers.setdefault('User-Agent', opts.user_agent)
if opts.referer is not None:
@@ -477,7 +489,7 @@ def validate_options(opts):
val1=opts.sponskrub and opts.sponskrub_cut)
# Conflicts with --allow-unplayable-formats
- report_conflict('--add-metadata', 'addmetadata')
+ report_conflict('--embed-metadata', 'addmetadata')
report_conflict('--embed-chapters', 'addchapters')
report_conflict('--embed-info-json', 'embed_infojson')
report_conflict('--embed-subs', 'embedsubtitles')
@@ -766,6 +778,7 @@ def parse_options(argv=None):
'windowsfilenames': opts.windowsfilenames,
'ignoreerrors': opts.ignoreerrors,
'force_generic_extractor': opts.force_generic_extractor,
+ 'allowed_extractors': opts.allowed_extractors or ['default'],
'ratelimit': opts.ratelimit,
'throttledratelimit': opts.throttledratelimit,
'overwrites': opts.overwrites,
@@ -949,6 +962,8 @@ def _real_main(argv=None):
def main(argv=None):
+ global _IN_CLI
+ _IN_CLI = True
try:
_exit(*variadic(_real_main(argv)))
except DownloadError:
diff --git a/yt_dlp/cache.py b/yt_dlp/cache.py
index 83351b7976..4f9fb78d37 100644
--- a/yt_dlp/cache.py
+++ b/yt_dlp/cache.py
@@ -6,7 +6,8 @@ import re
import shutil
import traceback
-from .utils import expand_path, write_json_file
+from .utils import expand_path, traverse_obj, version_tuple, write_json_file
+from .version import __version__
class Cache:
@@ -45,12 +46,20 @@ class Cache:
if ose.errno != errno.EEXIST:
raise
self._ydl.write_debug(f'Saving {section}.{key} to cache')
- write_json_file(data, fn)
+ write_json_file({'yt-dlp_version': __version__, 'data': data}, fn)
except Exception:
tb = traceback.format_exc()
self._ydl.report_warning(f'Writing cache to {fn!r} failed: {tb}')
- def load(self, section, key, dtype='json', default=None):
+ def _validate(self, data, min_ver):
+ version = traverse_obj(data, 'yt-dlp_version')
+ if not version: # Backward compatibility
+ data, version = {'data': data}, '2022.08.19'
+ if not min_ver or version_tuple(version) >= version_tuple(min_ver):
+ return data['data']
+ self._ydl.write_debug(f'Discarding old cache from version {version} (needs {min_ver})')
+
+ def load(self, section, key, dtype='json', default=None, *, min_ver=None):
assert dtype in ('json',)
if not self.enabled:
@@ -61,8 +70,8 @@ class Cache:
try:
with open(cache_fn, encoding='utf-8') as cachef:
self._ydl.write_debug(f'Loading {section}.{key} from cache')
- return json.load(cachef)
- except ValueError:
+ return self._validate(json.load(cachef), min_ver)
+ except (ValueError, KeyError):
try:
file_size = os.path.getsize(cache_fn)
except OSError as oe:
diff --git a/yt_dlp/cookies.py b/yt_dlp/cookies.py
index 1a164bb31a..8ca7cea2ce 100644
--- a/yt_dlp/cookies.py
+++ b/yt_dlp/cookies.py
@@ -1,8 +1,10 @@
import base64
import contextlib
import http.cookiejar
+import http.cookies
import json
import os
+import re
import shutil
import struct
import subprocess
@@ -24,7 +26,14 @@ from .dependencies import (
sqlite3,
)
from .minicurses import MultilinePrinter, QuietMultilinePrinter
-from .utils import Popen, YoutubeDLCookieJar, error_to_str, expand_path
+from .utils import (
+ Popen,
+ YoutubeDLCookieJar,
+ error_to_str,
+ expand_path,
+ is_path_like,
+ try_call,
+)
CHROMIUM_BASED_BROWSERS = {'brave', 'chrome', 'chromium', 'edge', 'opera', 'vivaldi'}
SUPPORTED_BROWSERS = CHROMIUM_BASED_BROWSERS | {'firefox', 'safari'}
@@ -85,11 +94,12 @@ def _create_progress_bar(logger):
def load_cookies(cookie_file, browser_specification, ydl):
cookie_jars = []
if browser_specification is not None:
- browser_name, profile, keyring = _parse_browser_specification(*browser_specification)
- cookie_jars.append(extract_cookies_from_browser(browser_name, profile, YDLLogger(ydl), keyring=keyring))
+ browser_name, profile, keyring, container = _parse_browser_specification(*browser_specification)
+ cookie_jars.append(
+ extract_cookies_from_browser(browser_name, profile, YDLLogger(ydl), keyring=keyring, container=container))
if cookie_file is not None:
- is_filename = YoutubeDLCookieJar.is_path(cookie_file)
+ is_filename = is_path_like(cookie_file)
if is_filename:
cookie_file = expand_path(cookie_file)
@@ -101,9 +111,9 @@ def load_cookies(cookie_file, browser_specification, ydl):
return _merge_cookie_jars(cookie_jars)
-def extract_cookies_from_browser(browser_name, profile=None, logger=YDLLogger(), *, keyring=None):
+def extract_cookies_from_browser(browser_name, profile=None, logger=YDLLogger(), *, keyring=None, container=None):
if browser_name == 'firefox':
- return _extract_firefox_cookies(profile, logger)
+ return _extract_firefox_cookies(profile, container, logger)
elif browser_name == 'safari':
return _extract_safari_cookies(profile, logger)
elif browser_name in CHROMIUM_BASED_BROWSERS:
@@ -112,7 +122,7 @@ def extract_cookies_from_browser(browser_name, profile=None, logger=YDLLogger(),
raise ValueError(f'unknown browser: {browser_name}')
-def _extract_firefox_cookies(profile, logger):
+def _extract_firefox_cookies(profile, container, logger):
logger.info('Extracting cookies from firefox')
if not sqlite3:
logger.warning('Cannot extract cookies from firefox without sqlite3 support. '
@@ -131,11 +141,36 @@ def _extract_firefox_cookies(profile, logger):
raise FileNotFoundError(f'could not find firefox cookies database in {search_root}')
logger.debug(f'Extracting cookies from: "{cookie_database_path}"')
+ container_id = None
+ if container not in (None, 'none'):
+ containers_path = os.path.join(os.path.dirname(cookie_database_path), 'containers.json')
+ if not os.path.isfile(containers_path) or not os.access(containers_path, os.R_OK):
+ raise FileNotFoundError(f'could not read containers.json in {search_root}')
+ with open(containers_path) as containers:
+ identities = json.load(containers).get('identities', [])
+ container_id = next((context.get('userContextId') for context in identities if container in (
+ context.get('name'),
+ try_call(lambda: re.fullmatch(r'userContext([^\.]+)\.label', context['l10nID']).group())
+ )), None)
+ if not isinstance(container_id, int):
+ raise ValueError(f'could not find firefox container "{container}" in containers.json')
+
with tempfile.TemporaryDirectory(prefix='yt_dlp') as tmpdir:
cursor = None
try:
cursor = _open_database_copy(cookie_database_path, tmpdir)
- cursor.execute('SELECT host, name, value, path, expiry, isSecure FROM moz_cookies')
+ if isinstance(container_id, int):
+ logger.debug(
+ f'Only loading cookies from firefox container "{container}", ID {container_id}')
+ cursor.execute(
+ 'SELECT host, name, value, path, expiry, isSecure FROM moz_cookies WHERE originAttributes LIKE ? OR originAttributes LIKE ?',
+ (f'%userContextId={container_id}', f'%userContextId={container_id}&%'))
+ elif container == 'none':
+ logger.debug('Only loading cookies not belonging to any container')
+ cursor.execute(
+ 'SELECT host, name, value, path, expiry, isSecure FROM moz_cookies WHERE NOT INSTR(originAttributes,"userContextId=")')
+ else:
+ cursor.execute('SELECT host, name, value, path, expiry, isSecure FROM moz_cookies')
jar = YoutubeDLCookieJar()
with _create_progress_bar(logger) as progress_bar:
table = cursor.fetchall()
@@ -810,12 +845,15 @@ def _get_linux_keyring_password(browser_keyring_name, keyring, logger):
def _get_mac_keyring_password(browser_keyring_name, logger):
logger.debug('using find-generic-password to obtain password from OSX keychain')
try:
- stdout, _, _ = Popen.run(
+ stdout, _, returncode = Popen.run(
['security', 'find-generic-password',
'-w', # write password to stdout
'-a', browser_keyring_name, # match 'account'
'-s', f'{browser_keyring_name} Safe Storage'], # match 'service'
stdout=subprocess.PIPE, stderr=subprocess.DEVNULL)
+ if returncode:
+ logger.warning('find-generic-password failed')
+ return None
return stdout.rstrip(b'\n')
except Exception as e:
logger.warning(f'exception running find-generic-password: {error_to_str(e)}')
@@ -948,11 +986,102 @@ def _is_path(value):
return os.path.sep in value
-def _parse_browser_specification(browser_name, profile=None, keyring=None):
+def _parse_browser_specification(browser_name, profile=None, keyring=None, container=None):
if browser_name not in SUPPORTED_BROWSERS:
raise ValueError(f'unsupported browser: "{browser_name}"')
if keyring not in (None, *SUPPORTED_KEYRINGS):
raise ValueError(f'unsupported keyring: "{keyring}"')
- if profile is not None and _is_path(profile):
- profile = os.path.expanduser(profile)
- return browser_name, profile, keyring
+ if profile is not None and _is_path(expand_path(profile)):
+ profile = expand_path(profile)
+ return browser_name, profile, keyring, container
+
+
+class LenientSimpleCookie(http.cookies.SimpleCookie):
+ """More lenient version of http.cookies.SimpleCookie"""
+ # From https://github.com/python/cpython/blob/v3.10.7/Lib/http/cookies.py
+ # We use Morsel's legal key chars to avoid errors on setting values
+ _LEGAL_KEY_CHARS = r'\w\d' + re.escape('!#$%&\'*+-.:^_`|~')
+ _LEGAL_VALUE_CHARS = _LEGAL_KEY_CHARS + re.escape('(),/<=>?@[]{}')
+
+ _RESERVED = {
+ "expires",
+ "path",
+ "comment",
+ "domain",
+ "max-age",
+ "secure",
+ "httponly",
+ "version",
+ "samesite",
+ }
+
+ _FLAGS = {"secure", "httponly"}
+
+ # Added 'bad' group to catch the remaining value
+ _COOKIE_PATTERN = re.compile(r"""
+ \s* # Optional whitespace at start of cookie
+ (?P # Start of group 'key'
+ [""" + _LEGAL_KEY_CHARS + r"""]+?# Any word of at least one letter
+ ) # End of group 'key'
+ ( # Optional group: there may not be a value.
+ \s*=\s* # Equal Sign
+ ( # Start of potential value
+ (?P # Start of group 'val'
+ "(?:[^\\"]|\\.)*" # Any doublequoted string
+ | # or
+ \w{3},\s[\w\d\s-]{9,11}\s[\d:]{8}\sGMT # Special case for "expires" attr
+ | # or
+ [""" + _LEGAL_VALUE_CHARS + r"""]* # Any word or empty string
+ ) # End of group 'val'
+ | # or
+ (?P(?:\\;|[^;])*?) # 'bad' group fallback for invalid values
+ ) # End of potential value
+ )? # End of optional value group
+ \s* # Any number of spaces.
+ (\s+|;|$) # Ending either at space, semicolon, or EOS.
+ """, re.ASCII | re.VERBOSE)
+
+ def load(self, data):
+ # Workaround for https://github.com/yt-dlp/yt-dlp/issues/4776
+ if not isinstance(data, str):
+ return super().load(data)
+
+ morsel = None
+ for match in self._COOKIE_PATTERN.finditer(data):
+ if match.group('bad'):
+ morsel = None
+ continue
+
+ key, value = match.group('key', 'val')
+
+ is_attribute = False
+ if key.startswith('$'):
+ key = key[1:]
+ is_attribute = True
+
+ lower_key = key.lower()
+ if lower_key in self._RESERVED:
+ if morsel is None:
+ continue
+
+ if value is None:
+ if lower_key not in self._FLAGS:
+ morsel = None
+ continue
+ value = True
+ else:
+ value, _ = self.value_decode(value)
+
+ morsel[key] = value
+
+ elif is_attribute:
+ morsel = None
+
+ elif value is not None:
+ morsel = self.get(key, http.cookies.Morsel())
+ real_value, coded_value = self.value_decode(value)
+ morsel.set(key, real_value, coded_value)
+ self[key] = morsel
+
+ else:
+ morsel = None
diff --git a/yt_dlp/downloader/common.py b/yt_dlp/downloader/common.py
index 4962c0cf8b..8d110c3747 100644
--- a/yt_dlp/downloader/common.py
+++ b/yt_dlp/downloader/common.py
@@ -24,6 +24,7 @@ from ..utils import (
encodeFilename,
format_bytes,
join_nonempty,
+ remove_start,
sanitize_open,
shell_quote,
timeconvert,
@@ -92,6 +93,7 @@ class FileDownloader:
for func in (
'deprecation_warning',
+ 'deprecated_feature',
'report_error',
'report_file_already_downloaded',
'report_warning',
@@ -119,11 +121,11 @@ class FileDownloader:
time = timetuple_from_msec(seconds * 1000)
if time.hours > 99:
return '--:--:--'
- if not time.hours:
- return '%02d:%02d' % time[1:-1]
return '%02d:%02d:%02d' % time[:-1]
- format_eta = format_seconds
+ @classmethod
+ def format_eta(cls, seconds):
+ return f'{remove_start(cls.format_seconds(seconds), "00:"):>8s}'
@staticmethod
def calc_percent(byte_counter, data_len):
@@ -331,6 +333,8 @@ class FileDownloader:
return tmpl
return default
+ _format_bytes = lambda k: f'{format_bytes(s.get(k)):>10s}'
+
if s['status'] == 'finished':
if self.params.get('noprogress'):
self.to_screen('[download] Download completed')
@@ -338,7 +342,7 @@ class FileDownloader:
s.update({
'speed': speed,
'_speed_str': self.format_speed(speed).strip(),
- '_total_bytes_str': format_bytes(s.get('total_bytes')),
+ '_total_bytes_str': _format_bytes('total_bytes'),
'_elapsed_str': self.format_seconds(s.get('elapsed')),
'_percent_str': self.format_percent(100),
})
@@ -353,15 +357,15 @@ class FileDownloader:
return
s.update({
- '_eta_str': self.format_eta(s.get('eta')),
+ '_eta_str': self.format_eta(s.get('eta')).strip(),
'_speed_str': self.format_speed(s.get('speed')),
'_percent_str': self.format_percent(try_call(
lambda: 100 * s['downloaded_bytes'] / s['total_bytes'],
lambda: 100 * s['downloaded_bytes'] / s['total_bytes_estimate'],
lambda: s['downloaded_bytes'] == 0 and 0)),
- '_total_bytes_str': format_bytes(s.get('total_bytes')),
- '_total_bytes_estimate_str': format_bytes(s.get('total_bytes_estimate')),
- '_downloaded_bytes_str': format_bytes(s.get('downloaded_bytes')),
+ '_total_bytes_str': _format_bytes('total_bytes'),
+ '_total_bytes_estimate_str': _format_bytes('total_bytes_estimate'),
+ '_downloaded_bytes_str': _format_bytes('downloaded_bytes'),
'_elapsed_str': self.format_seconds(s.get('elapsed')),
})
diff --git a/yt_dlp/downloader/dash.py b/yt_dlp/downloader/dash.py
index a6da26f09d..8723e10689 100644
--- a/yt_dlp/downloader/dash.py
+++ b/yt_dlp/downloader/dash.py
@@ -51,7 +51,7 @@ class DashSegmentsFD(FragmentFD):
args.append([ctx, fragments_to_download, fmt])
- return self.download_and_append_fragments_multiple(*args)
+ return self.download_and_append_fragments_multiple(*args, is_fatal=lambda idx: idx == 0)
def _resolve_fragments(self, fragments, ctx):
fragments = fragments(ctx) if callable(fragments) else fragments
diff --git a/yt_dlp/downloader/external.py b/yt_dlp/downloader/external.py
index 9859a7b333..895390d6cf 100644
--- a/yt_dlp/downloader/external.py
+++ b/yt_dlp/downloader/external.py
@@ -252,6 +252,10 @@ class Aria2cFD(ExternalFD):
check_results = (not re.search(feature, manifest) for feature in UNSUPPORTED_FEATURES)
return all(check_results)
+ @staticmethod
+ def _aria2c_filename(fn):
+ return fn if os.path.isabs(fn) else f'.{os.path.sep}{fn}'
+
def _make_cmd(self, tmpfilename, info_dict):
cmd = [self.exe, '-c',
'--console-log-level=warn', '--summary-interval=0', '--download-result=hide',
@@ -280,11 +284,9 @@ class Aria2cFD(ExternalFD):
# https://github.com/aria2/aria2/issues/1373
dn = os.path.dirname(tmpfilename)
if dn:
- if not os.path.isabs(dn):
- dn = f'.{os.path.sep}{dn}'
- cmd += ['--dir', dn + os.path.sep]
+ cmd += ['--dir', self._aria2c_filename(dn) + os.path.sep]
if 'fragments' not in info_dict:
- cmd += ['--out', f'.{os.path.sep}{os.path.basename(tmpfilename)}']
+ cmd += ['--out', self._aria2c_filename(os.path.basename(tmpfilename))]
cmd += ['--auto-file-renaming=false']
if 'fragments' in info_dict:
@@ -293,11 +295,11 @@ class Aria2cFD(ExternalFD):
url_list = []
for frag_index, fragment in enumerate(info_dict['fragments']):
fragment_filename = '%s-Frag%d' % (os.path.basename(tmpfilename), frag_index)
- url_list.append('%s\n\tout=%s' % (fragment['url'], fragment_filename))
+ url_list.append('%s\n\tout=%s' % (fragment['url'], self._aria2c_filename(fragment_filename)))
stream, _ = self.sanitize_open(url_list_file, 'wb')
stream.write('\n'.join(url_list).encode())
stream.close()
- cmd += ['-i', url_list_file]
+ cmd += ['-i', self._aria2c_filename(url_list_file)]
else:
cmd += ['--', info_dict['url']]
return cmd
@@ -515,16 +517,14 @@ _BY_NAME = {
if name.endswith('FD') and name not in ('ExternalFD', 'FragmentFD')
}
-_BY_EXE = {klass.EXE_NAME: klass for klass in _BY_NAME.values()}
-
def list_external_downloaders():
return sorted(_BY_NAME.keys())
def get_external_downloader(external_downloader):
- """ Given the name of the executable, see whether we support the given
- downloader . """
- # Drop .exe extension on Windows
+ """ Given the name of the executable, see whether we support the given downloader """
bn = os.path.splitext(os.path.basename(external_downloader))[0]
- return _BY_NAME.get(bn, _BY_EXE.get(bn))
+ return _BY_NAME.get(bn) or next((
+ klass for klass in _BY_NAME.values() if klass.EXE_NAME in bn
+ ), None)
diff --git a/yt_dlp/downloader/f4m.py b/yt_dlp/downloader/f4m.py
index 770354de77..306f92192f 100644
--- a/yt_dlp/downloader/f4m.py
+++ b/yt_dlp/downloader/f4m.py
@@ -184,7 +184,7 @@ def build_fragments_list(boot_info):
first_frag_number = fragment_run_entry_table[0]['first']
fragments_counter = itertools.count(first_frag_number)
for segment, fragments_count in segment_run_table['segment_run']:
- # In some live HDS streams (for example Rai), `fragments_count` is
+ # In some live HDS streams (e.g. Rai), `fragments_count` is
# abnormal and causing out-of-memory errors. It's OK to change the
# number of fragments for live streams as they are updated periodically
if fragments_count == 4294967295 and boot_info['live']:
@@ -424,6 +424,4 @@ class F4mFD(FragmentFD):
msg = 'Missed %d fragments' % (fragments_list[0][1] - (frag_i + 1))
self.report_warning(msg)
- self._finish_frag_download(ctx, info_dict)
-
- return True
+ return self._finish_frag_download(ctx, info_dict)
diff --git a/yt_dlp/downloader/fragment.py b/yt_dlp/downloader/fragment.py
index b1d3127c32..83f7870edb 100644
--- a/yt_dlp/downloader/fragment.py
+++ b/yt_dlp/downloader/fragment.py
@@ -65,8 +65,8 @@ class FragmentFD(FileDownloader):
"""
def report_retry_fragment(self, err, frag_index, count, retries):
- self.deprecation_warning(
- 'yt_dlp.downloader.FragmentFD.report_retry_fragment is deprecated. Use yt_dlp.downloader.FileDownloader.report_retry instead')
+ self.deprecation_warning('yt_dlp.downloader.FragmentFD.report_retry_fragment is deprecated. '
+ 'Use yt_dlp.downloader.FileDownloader.report_retry instead')
return self.report_retry(err, count, retries, frag_index)
def report_skip_fragment(self, frag_index, err=None):
@@ -295,16 +295,23 @@ class FragmentFD(FileDownloader):
self.try_remove(ytdl_filename)
elapsed = time.time() - ctx['started']
- if ctx['tmpfilename'] == '-':
- downloaded_bytes = ctx['complete_frags_downloaded_bytes']
+ to_file = ctx['tmpfilename'] != '-'
+ if to_file:
+ downloaded_bytes = os.path.getsize(encodeFilename(ctx['tmpfilename']))
else:
+ downloaded_bytes = ctx['complete_frags_downloaded_bytes']
+
+ if not downloaded_bytes:
+ if to_file:
+ self.try_remove(ctx['tmpfilename'])
+ self.report_error('The downloaded file is empty')
+ return False
+ elif to_file:
self.try_rename(ctx['tmpfilename'], ctx['filename'])
- if self.params.get('updatetime', True):
- filetime = ctx.get('fragment_filetime')
- if filetime:
- with contextlib.suppress(Exception):
- os.utime(ctx['filename'], (time.time(), filetime))
- downloaded_bytes = os.path.getsize(encodeFilename(ctx['filename']))
+ filetime = ctx.get('fragment_filetime')
+ if self.params.get('updatetime', True) and filetime:
+ with contextlib.suppress(Exception):
+ os.utime(ctx['filename'], (time.time(), filetime))
self._hook_progress({
'downloaded_bytes': downloaded_bytes,
@@ -316,6 +323,7 @@ class FragmentFD(FileDownloader):
'max_progress': ctx.get('max_progress'),
'progress_idx': ctx.get('progress_idx'),
}, info_dict)
+ return True
def _prepare_external_frag_download(self, ctx):
if 'live' not in ctx:
@@ -362,7 +370,7 @@ class FragmentFD(FileDownloader):
return decrypt_fragment
- def download_and_append_fragments_multiple(self, *args, pack_func=None, finish_func=None):
+ def download_and_append_fragments_multiple(self, *args, **kwargs):
'''
@params (ctx1, fragments1, info_dict1), (ctx2, fragments2, info_dict2), ...
all args must be either tuple or list
@@ -370,7 +378,7 @@ class FragmentFD(FileDownloader):
interrupt_trigger = [True]
max_progress = len(args)
if max_progress == 1:
- return self.download_and_append_fragments(*args[0], pack_func=pack_func, finish_func=finish_func)
+ return self.download_and_append_fragments(*args[0], **kwargs)
max_workers = self.params.get('concurrent_fragment_downloads', 1)
if max_progress > 1:
self._prepare_multiline_status(max_progress)
@@ -380,8 +388,7 @@ class FragmentFD(FileDownloader):
ctx['max_progress'] = max_progress
ctx['progress_idx'] = idx
return self.download_and_append_fragments(
- ctx, fragments, info_dict, pack_func=pack_func, finish_func=finish_func,
- tpe=tpe, interrupt_trigger=interrupt_trigger)
+ ctx, fragments, info_dict, **kwargs, tpe=tpe, interrupt_trigger=interrupt_trigger)
class FTPE(concurrent.futures.ThreadPoolExecutor):
# has to stop this or it's going to wait on the worker thread itself
@@ -428,17 +435,12 @@ class FragmentFD(FileDownloader):
return result
def download_and_append_fragments(
- self, ctx, fragments, info_dict, *, pack_func=None, finish_func=None,
- tpe=None, interrupt_trigger=None):
- if not interrupt_trigger:
- interrupt_trigger = (True, )
+ self, ctx, fragments, info_dict, *, is_fatal=(lambda idx: False),
+ pack_func=(lambda content, idx: content), finish_func=None,
+ tpe=None, interrupt_trigger=(True, )):
- is_fatal = (
- ((lambda _: False) if info_dict.get('is_live') else (lambda idx: idx == 0))
- if self.params.get('skip_unavailable_fragments', True) else (lambda _: True))
-
- if not pack_func:
- pack_func = lambda frag_content, _: frag_content
+ if not self.params.get('skip_unavailable_fragments', True):
+ is_fatal = lambda _: True
def download_fragment(fragment, ctx):
if not interrupt_trigger[0]:
@@ -527,5 +529,4 @@ class FragmentFD(FileDownloader):
if finish_func is not None:
ctx['dest_stream'].write(finish_func())
ctx['dest_stream'].flush()
- self._finish_frag_download(ctx, info_dict)
- return True
+ return self._finish_frag_download(ctx, info_dict)
diff --git a/yt_dlp/downloader/ism.py b/yt_dlp/downloader/ism.py
index 801b5af813..a157a8ad93 100644
--- a/yt_dlp/downloader/ism.py
+++ b/yt_dlp/downloader/ism.py
@@ -138,6 +138,8 @@ def write_piff_header(stream, params):
if fourcc == 'AACL':
sample_entry_box = box(b'mp4a', sample_entry_payload)
+ if fourcc == 'EC-3':
+ sample_entry_box = box(b'ec-3', sample_entry_payload)
elif stream_type == 'video':
sample_entry_payload += u16.pack(0) # pre defined
sample_entry_payload += u16.pack(0) # reserved
@@ -278,5 +280,4 @@ class IsmFD(FragmentFD):
return False
self.report_skip_fragment(frag_index)
- self._finish_frag_download(ctx, info_dict)
- return True
+ return self._finish_frag_download(ctx, info_dict)
diff --git a/yt_dlp/downloader/mhtml.py b/yt_dlp/downloader/mhtml.py
index ed076e09ed..d977dcec31 100644
--- a/yt_dlp/downloader/mhtml.py
+++ b/yt_dlp/downloader/mhtml.py
@@ -186,5 +186,4 @@ body > figure > img {
ctx['dest_stream'].write(
b'--%b--\r\n\r\n' % frag_boundary.encode('us-ascii'))
- self._finish_frag_download(ctx, info_dict)
- return True
+ return self._finish_frag_download(ctx, info_dict)
diff --git a/yt_dlp/downloader/youtube_live_chat.py b/yt_dlp/downloader/youtube_live_chat.py
index 1bc3209dc4..5928fecf0b 100644
--- a/yt_dlp/downloader/youtube_live_chat.py
+++ b/yt_dlp/downloader/youtube_live_chat.py
@@ -191,8 +191,7 @@ class YoutubeLiveChatFD(FragmentFD):
if test:
break
- self._finish_frag_download(ctx, info_dict)
- return True
+ return self._finish_frag_download(ctx, info_dict)
@staticmethod
def parse_live_timestamp(action):
diff --git a/yt_dlp/extractor/_extractors.py b/yt_dlp/extractor/_extractors.py
index 3abae19b01..8652ec54e5 100644
--- a/yt_dlp/extractor/_extractors.py
+++ b/yt_dlp/extractor/_extractors.py
@@ -1,5 +1,29 @@
# flake8: noqa: F401
+from .youtube import ( # Youtube is moved to the top to improve performance
+ YoutubeIE,
+ YoutubeClipIE,
+ YoutubeFavouritesIE,
+ YoutubeNotificationsIE,
+ YoutubeHistoryIE,
+ YoutubeTabIE,
+ YoutubeLivestreamEmbedIE,
+ YoutubePlaylistIE,
+ YoutubeRecommendedIE,
+ YoutubeSearchDateIE,
+ YoutubeSearchIE,
+ YoutubeSearchURLIE,
+ YoutubeMusicSearchURLIE,
+ YoutubeSubscriptionsIE,
+ YoutubeStoriesIE,
+ YoutubeTruncatedIDIE,
+ YoutubeTruncatedURLIE,
+ YoutubeYtBeIE,
+ YoutubeYtUserIE,
+ YoutubeWatchLaterIE,
+ YoutubeShortsAudioPivotIE
+)
+
from .abc import (
ABCIE,
ABCIViewIE,
@@ -41,6 +65,7 @@ from .aenetworks import (
HistoryPlayerIE,
BiographyIE,
)
+from .aeonco import AeonCoIE
from .afreecatv import (
AfreecaTVIE,
AfreecaTVLiveIE,
@@ -61,7 +86,6 @@ from .americastestkitchen import (
AmericasTestKitchenSeasonIE,
)
from .angel import AngelIE
-from .animeondemand import AnimeOnDemandIE
from .anvato import AnvatoIE
from .aol import AolIE
from .allocine import AllocineIE
@@ -149,6 +173,7 @@ from .beeg import BeegIE
from .behindkink import BehindKinkIE
from .bellmedia import BellMediaIE
from .beatport import BeatportIE
+from .berufetv import BerufeTVIE
from .bet import BetIE
from .bfi import BFIPlayerIE
from .bfmtv import (
@@ -168,7 +193,9 @@ from .bilibili import (
BilibiliAudioIE,
BilibiliAudioAlbumIE,
BiliBiliPlayerIE,
- BilibiliChannelIE,
+ BilibiliSpaceVideoIE,
+ BilibiliSpaceAudioIE,
+ BilibiliSpacePlaylistIE,
BiliIntlIE,
BiliIntlSeriesIE,
BiliLiveIE,
@@ -194,6 +221,7 @@ from .bokecc import BokeCCIE
from .bongacams import BongaCamsIE
from .bostonglobe import BostonGlobeIE
from .box import BoxIE
+from .booyah import BooyahClipsIE
from .bpb import BpbIE
from .br import (
BRIE,
@@ -207,6 +235,7 @@ from .brightcove import (
BrightcoveNewIE,
)
from .businessinsider import BusinessInsiderIE
+from .bundesliga import BundesligaIE
from .buzzfeed import BuzzFeedIE
from .byutv import BYUtvIE
from .c56 import C56IE
@@ -306,6 +335,7 @@ from .cnn import (
CNNIE,
CNNBlogsIE,
CNNArticleIE,
+ CNNIndonesiaIE,
)
from .coub import CoubIE
from .comedycentral import (
@@ -384,7 +414,7 @@ from .deezer import (
DeezerAlbumIE,
)
from .democracynow import DemocracynowIE
-from .detik import Detik20IE
+from .detik import DetikEmbedIE
from .dfb import DFBIE
from .dhm import DHMIE
from .digg import DiggIE
@@ -411,6 +441,7 @@ from .dplay import (
AnimalPlanetIE,
TLCIE,
MotorTrendIE,
+ MotorTrendOnDemandIE,
DiscoveryPlusIndiaIE,
DiscoveryNetworksDeIE,
DiscoveryPlusItalyIE,
@@ -470,6 +501,7 @@ from .epicon import (
EpiconIE,
EpiconSeriesIE,
)
+from .epoch import EpochIE
from .eporner import EpornerIE
from .eroprofile import (
EroProfileIE,
@@ -491,6 +523,7 @@ from .espn import (
from .esri import EsriVideoIE
from .europa import EuropaIE
from .europeantour import EuropeanTourIE
+from .eurosport import EurosportIE
from .euscreen import EUScreenIE
from .expotv import ExpoTVIE
from .expressen import ExpressenIE
@@ -500,6 +533,7 @@ from .facebook import (
FacebookIE,
FacebookPluginsVideoIE,
FacebookRedirectURLIE,
+ FacebookReelIE,
)
from .fancode import (
FancodeVodIE,
@@ -622,6 +656,7 @@ from .googlepodcasts import (
)
from .googlesearch import GoogleSearchIE
from .gopro import GoProIE
+from .goplay import GoPlayIE
from .goshgay import GoshgayIE
from .gotostage import GoToStageIE
from .gputechconf import GPUTechConfIE
@@ -631,6 +666,7 @@ from .gronkh import (
GronkhVodsIE
)
from .groupon import GrouponIE
+from .harpodeon import HarpodeonIE
from .hbo import HBOIE
from .hearthisat import HearThisAtIE
from .heise import HeiseIE
@@ -662,7 +698,10 @@ from .hse import (
HSEShowIE,
HSEProductIE,
)
-from .genericembeds import HTML5MediaEmbedIE
+from .genericembeds import (
+ HTML5MediaEmbedIE,
+ QuotedHTMLIE,
+)
from .huajiao import HuajiaoIE
from .huya import HuyaLiveIE
from .huffpost import HuffPostIE
@@ -687,6 +726,7 @@ from .iheart import (
IHeartRadioIE,
IHeartRadioPodcastIE,
)
+from .iltalehti import IltalehtiIE
from .imdb import (
ImdbIE,
ImdbListIE
@@ -718,6 +758,11 @@ from .iqiyi import (
IqIE,
IqAlbumIE
)
+from .islamchannel import (
+ IslamChannelIE,
+ IslamChannelSeriesIE,
+)
+from .israelnationalnews import IsraelNationalNewsIE
from .itprotv import (
ITProTVIE,
ITProTVCourseIE
@@ -907,6 +952,7 @@ from .mediasite import (
MediasiteCatalogIE,
MediasiteNamedCatalogIE,
)
+from .mediaworksnz import MediaWorksNZVODIE
from .medici import MediciIE
from .megaphone import MegaphoneIE
from .meipai import MeipaiIE
@@ -922,6 +968,7 @@ from .microsoftvirtualacademy import (
MicrosoftVirtualAcademyIE,
MicrosoftVirtualAcademyCourseIE,
)
+from .microsoftembed import MicrosoftEmbedIE
from .mildom import (
MildomIE,
MildomVodIE,
@@ -955,6 +1002,7 @@ from .mixcloud import (
from .mlb import (
MLBIE,
MLBVideoIE,
+ MLBTVIE,
)
from .mlssoccer import MLSSoccerIE
from .mnet import MnetIE
@@ -973,6 +1021,7 @@ from .motherless import (
from .motorsport import MotorsportIE
from .movieclips import MovieClipsIE
from .moviepilot import MoviepilotIE
+from .moview import MoviewPlayIE
from .moviezine import MoviezineIE
from .movingimage import MovingImageIE
from .msn import MSNIE
@@ -1041,6 +1090,7 @@ from .nbc import (
NBCSportsIE,
NBCSportsStreamIE,
NBCSportsVPlayerIE,
+ NBCStationsIE,
)
from .ndr import (
NDRIE,
@@ -1075,6 +1125,7 @@ from .newgrounds import (
NewgroundsPlaylistIE,
NewgroundsUserIE,
)
+from .newspicks import NewsPicksIE
from .newstube import NewstubeIE
from .newsy import NewsyIE
from .nextmedia import (
@@ -1134,6 +1185,7 @@ from .noodlemagazine import NoodleMagazineIE
from .noovo import NoovoIE
from .normalboots import NormalbootsIE
from .nosvideo import NosVideoIE
+from .nosnl import NOSNLArticleIE
from .nova import (
NovaEmbedIE,
NovaIE,
@@ -1188,6 +1240,7 @@ from .olympics import OlympicsReplayIE
from .on24 import On24IE
from .ondemandkorea import OnDemandKoreaIE
from .onefootball import OneFootballIE
+from .onenewsnz import OneNewsNZIE
from .onet import (
OnetIE,
OnetChannelIE,
@@ -1235,7 +1288,7 @@ from .paramountplus import (
ParamountPlusIE,
ParamountPlusSeriesIE,
)
-from .parliamentliveuk import ParliamentLiveUKIE
+from .parler import ParlerIE
from .parlview import ParlviewIE
from .patreon import (
PatreonIE,
@@ -1296,6 +1349,7 @@ from .pluralsight import (
PluralsightIE,
PluralsightCourseIE,
)
+from .podbayfm import PodbayFMIE, PodbayFMChannelIE
from .podchaser import PodchaserIE
from .podomatic import PodomaticIE
from .pokemon import (
@@ -1336,6 +1390,7 @@ from .puhutv import (
PuhuTVIE,
PuhuTVSerieIE,
)
+from .prankcast import PrankCastIE
from .premiershiprugby import PremiershipRugbyIE
from .presstv import PressTVIE
from .projectveritas import ProjectVeritasIE
@@ -1406,6 +1461,7 @@ from .rcti import (
RCTIPlusTVIE,
)
from .rds import RDSIE
+from .redbee import ParliamentLiveUKIE, RTBFIE
from .redbulltv import (
RedBullTVIE,
RedBullEmbedIE,
@@ -1439,7 +1495,6 @@ from .rokfin import (
from .roosterteeth import RoosterTeethIE, RoosterTeethSeriesIE
from .rottentomatoes import RottenTomatoesIE
from .rozhlas import RozhlasIE
-from .rtbf import RTBFIE
from .rte import RteIE, RteRadioIE
from .rtlnl import (
RtlNlIE,
@@ -1516,6 +1571,7 @@ from .samplefocus import SampleFocusIE
from .sapo import SapoIE
from .savefrom import SaveFromIE
from .sbs import SBSIE
+from .screen9 import Screen9IE
from .screencast import ScreencastIE
from .screencastomatic import ScreencastOMaticIE
from .scrippsnetworks import (
@@ -1581,6 +1637,7 @@ from .sky import (
from .slideshare import SlideshareIE
from .slideslive import SlidesLiveIE
from .slutload import SlutloadIE
+from .smotrim import SmotrimIE
from .snotr import SnotrIE
from .sohu import SohuIE
from .sonyliv import (
@@ -1724,6 +1781,14 @@ from .telequebec import (
from .teletask import TeleTaskIE
from .telewebion import TelewebionIE
from .tempo import TempoIE
+from .tencent import (
+ IflixEpisodeIE,
+ IflixSeriesIE,
+ VQQSeriesIE,
+ VQQVideoIE,
+ WeTvEpisodeIE,
+ WeTvSeriesIE,
+)
from .tennistv import TennisTVIE
from .tenplay import TenPlayIE
from .testurl import TestURLIE
@@ -1783,6 +1848,10 @@ from .toongoggles import ToonGogglesIE
from .toutv import TouTvIE
from .toypics import ToypicsUserIE, ToypicsIE
from .traileraddict import TrailerAddictIE
+from .triller import (
+ TrillerIE,
+ TrillerUserIE,
+)
from .trilulilu import TriluliluIE
from .trovo import (
TrovoIE,
@@ -1792,6 +1861,7 @@ from .trovo import (
)
from .trueid import TrueIDIE
from .trunews import TruNewsIE
+from .truth import TruthIE
from .trutv import TruTVIE
from .tube8 import Tube8IE
from .tubetugraz import TubeTuGrazIE, TubeTuGrazSeriesIE
@@ -1815,6 +1885,9 @@ from .tv2 import (
KatsomoIE,
MTVUutisetArticleIE,
)
+from .tv24ua import (
+ TV24UAVideoIE,
+)
from .tv2dk import (
TV2DKIE,
TV2DKBornholmPlayIE,
@@ -1917,6 +1990,7 @@ from .drooble import DroobleIE
from .umg import UMGDeIE
from .unistra import UnistraIE
from .unity import UnityIE
+from .unscripted import UnscriptedNewsVideoIE
from .uol import UOLIE
from .uplynk import (
UplynkIE,
@@ -1974,7 +2048,6 @@ from .vidio import (
VidioLiveIE
)
from .vidlii import VidLiiIE
-from .vier import VierIE, VierVideosIE
from .viewlift import (
ViewLiftIE,
ViewLiftEmbedIE,
@@ -2087,7 +2160,6 @@ from .weibo import (
WeiboMobileIE
)
from .weiqitv import WeiqiTVIE
-from .wetv import WeTvEpisodeIE, WeTvSeriesIE
from .wikimedia import WikimediaIE
from .willow import WillowIE
from .wimtv import WimTVIE
@@ -2095,6 +2167,11 @@ from .whowatch import WhoWatchIE
from .wistia import (
WistiaIE,
WistiaPlaylistIE,
+ WistiaChannelIE,
+)
+from .wordpress import (
+ WordpressPlaylistEmbedIE,
+ WordpressMiniAudioPlayerEmbedIE,
)
from .worldstarhiphop import WorldStarHipHopIE
from .wppilot import (
@@ -2170,42 +2247,44 @@ from .younow import (
from .youporn import YouPornIE
from .yourporn import YourPornIE
from .yourupload import YourUploadIE
-from .youtube import (
- YoutubeIE,
- YoutubeClipIE,
- YoutubeFavouritesIE,
- YoutubeNotificationsIE,
- YoutubeHistoryIE,
- YoutubeTabIE,
- YoutubeLivestreamEmbedIE,
- YoutubePlaylistIE,
- YoutubeRecommendedIE,
- YoutubeSearchDateIE,
- YoutubeSearchIE,
- YoutubeSearchURLIE,
- YoutubeMusicSearchURLIE,
- YoutubeSubscriptionsIE,
- YoutubeStoriesIE,
- YoutubeTruncatedIDIE,
- YoutubeTruncatedURLIE,
- YoutubeYtBeIE,
- YoutubeYtUserIE,
- YoutubeWatchLaterIE,
-)
from .zapiks import ZapiksIE
from .zattoo import (
BBVTVIE,
+ BBVTVLiveIE,
+ BBVTVRecordingsIE,
EinsUndEinsTVIE,
+ EinsUndEinsTVLiveIE,
+ EinsUndEinsTVRecordingsIE,
EWETVIE,
+ EWETVLiveIE,
+ EWETVRecordingsIE,
GlattvisionTVIE,
+ GlattvisionTVLiveIE,
+ GlattvisionTVRecordingsIE,
MNetTVIE,
- NetPlusIE,
+ MNetTVLiveIE,
+ MNetTVRecordingsIE,
+ NetPlusTVIE,
+ NetPlusTVLiveIE,
+ NetPlusTVRecordingsIE,
OsnatelTVIE,
+ OsnatelTVLiveIE,
+ OsnatelTVRecordingsIE,
QuantumTVIE,
+ QuantumTVLiveIE,
+ QuantumTVRecordingsIE,
SaltTVIE,
+ SaltTVLiveIE,
+ SaltTVRecordingsIE,
SAKTVIE,
+ SAKTVLiveIE,
+ SAKTVRecordingsIE,
VTXTVIE,
+ VTXTVLiveIE,
+ VTXTVRecordingsIE,
WalyTVIE,
+ WalyTVLiveIE,
+ WalyTVRecordingsIE,
ZattooIE,
ZattooLiveIE,
ZattooMoviesIE,
diff --git a/yt_dlp/extractor/abematv.py b/yt_dlp/extractor/abematv.py
index d8ad78705c..9955fb289f 100644
--- a/yt_dlp/extractor/abematv.py
+++ b/yt_dlp/extractor/abematv.py
@@ -365,7 +365,7 @@ class AbemaTVIE(AbemaTVBaseIE):
# read breadcrumb on top of page
breadcrumb = self._extract_breadcrumb_list(webpage, video_id)
if breadcrumb:
- # breadcrumb list translates to: (example is 1st test for this IE)
+ # breadcrumb list translates to: (e.g. 1st test for this IE)
# Home > Anime (genre) > Isekai Shokudo 2 (series name) > Episode 1 "Cheese cakes" "Morning again" (episode title)
# hence this works
info['series'] = breadcrumb[-2]
diff --git a/yt_dlp/extractor/acfun.py b/yt_dlp/extractor/acfun.py
index 615efd9bb0..92b905fa7c 100644
--- a/yt_dlp/extractor/acfun.py
+++ b/yt_dlp/extractor/acfun.py
@@ -84,7 +84,7 @@ class AcFunVideoIE(AcFunVideoBaseIE):
video_id = self._match_id(url)
webpage = self._download_webpage(url, video_id)
- json_all = self._search_json(r'window.videoInfo\s*=\s*', webpage, 'videoInfo', video_id)
+ json_all = self._search_json(r'window.videoInfo\s*=', webpage, 'videoInfo', video_id)
title = json_all.get('title')
video_list = json_all.get('videoList') or []
@@ -164,7 +164,7 @@ class AcFunBangumiIE(AcFunVideoBaseIE):
video_id = f'{video_id}{format_field(ac_idx, template="__%s")}'
webpage = self._download_webpage(url, video_id)
- json_bangumi_data = self._search_json(r'window.bangumiData\s*=\s*', webpage, 'bangumiData', video_id)
+ json_bangumi_data = self._search_json(r'window.bangumiData\s*=', webpage, 'bangumiData', video_id)
if ac_idx:
video_info = json_bangumi_data['hlVideoInfo']
@@ -181,7 +181,7 @@ class AcFunBangumiIE(AcFunVideoBaseIE):
if v.get('id') == season_id), 1)
json_bangumi_list = self._search_json(
- r'window\.bangumiList\s*=\s*', webpage, 'bangumiList', video_id, fatal=False)
+ r'window\.bangumiList\s*=', webpage, 'bangumiList', video_id, fatal=False)
video_internal_id = int_or_none(traverse_obj(json_bangumi_data, ('currentVideoInfo', 'id')))
episode_number = video_internal_id and next((
idx for idx, v in enumerate(json_bangumi_list.get('items') or [], 1)
diff --git a/yt_dlp/extractor/adobepass.py b/yt_dlp/extractor/adobepass.py
index a2666c2b83..ec1be008a3 100644
--- a/yt_dlp/extractor/adobepass.py
+++ b/yt_dlp/extractor/adobepass.py
@@ -1344,6 +1344,11 @@ MSO_INFO = {
'username_field': 'username',
'password_field': 'password',
},
+ 'AlticeOne': {
+ 'name': 'Optimum TV',
+ 'username_field': 'j_username',
+ 'password_field': 'j_password',
+ },
}
@@ -1705,7 +1710,7 @@ class AdobePassIE(InfoExtractor):
mso_info.get('username_field', 'username'): username,
mso_info.get('password_field', 'password'): password
}
- if mso_id == 'Cablevision':
+ if mso_id in ('Cablevision', 'AlticeOne'):
form_data['_eventId_proceed'] = ''
mvpd_confirm_page_res = post_form(provider_login_page_res, 'Logging in', form_data)
if mso_id != 'Rogers':
diff --git a/yt_dlp/extractor/aenetworks.py b/yt_dlp/extractor/aenetworks.py
index 86a10f2dcd..516cb6302c 100644
--- a/yt_dlp/extractor/aenetworks.py
+++ b/yt_dlp/extractor/aenetworks.py
@@ -28,14 +28,17 @@ class AENetworksBaseIE(ThePlatformIE):
}
def _extract_aen_smil(self, smil_url, video_id, auth=None):
- query = {'mbr': 'true'}
+ query = {
+ 'mbr': 'true',
+ 'formats': 'M3U+none,MPEG-DASH+none,MPEG4,MP3',
+ }
if auth:
query['auth'] = auth
TP_SMIL_QUERY = [{
'assetTypes': 'high_video_ak',
- 'switch': 'hls_high_ak'
+ 'switch': 'hls_high_ak',
}, {
- 'assetTypes': 'high_video_s3'
+ 'assetTypes': 'high_video_s3',
}, {
'assetTypes': 'high_video_s3',
'switch': 'hls_high_fastly',
diff --git a/yt_dlp/extractor/aeonco.py b/yt_dlp/extractor/aeonco.py
new file mode 100644
index 0000000000..4655862e3f
--- /dev/null
+++ b/yt_dlp/extractor/aeonco.py
@@ -0,0 +1,40 @@
+from .common import InfoExtractor
+from .vimeo import VimeoIE
+
+
+class AeonCoIE(InfoExtractor):
+ _VALID_URL = r'https?://(?:www\.)?aeon\.co/videos/(?P[^/?]+)'
+ _TESTS = [{
+ 'url': 'https://aeon.co/videos/raw-solar-storm-footage-is-the-punk-rock-antidote-to-sleek-james-webb-imagery',
+ 'md5': 'e5884d80552c9b6ea8d268a258753362',
+ 'info_dict': {
+ 'id': '1284717',
+ 'ext': 'mp4',
+ 'title': 'Brilliant Noise',
+ 'thumbnail': 'https://i.vimeocdn.com/video/21006315-1a1e49da8b07fd908384a982b4ba9ff0268c509a474576ebdf7b1392f4acae3b-d_960',
+ 'uploader': 'Semiconductor',
+ 'uploader_id': 'semiconductor',
+ 'uploader_url': 'https://vimeo.com/semiconductor',
+ 'duration': 348
+ }
+ }, {
+ 'url': 'https://aeon.co/videos/dazzling-timelapse-shows-how-microbes-spoil-our-food-and-sometimes-enrich-it',
+ 'md5': '4e5f3dad9dbda0dbfa2da41a851e631e',
+ 'info_dict': {
+ 'id': '728595228',
+ 'ext': 'mp4',
+ 'title': 'Wrought',
+ 'thumbnail': 'https://i.vimeocdn.com/video/1484618528-c91452611f9a4e4497735a533da60d45b2fe472deb0c880f0afaab0cd2efb22a-d_1280',
+ 'uploader': 'Biofilm Productions',
+ 'uploader_id': 'user140352216',
+ 'uploader_url': 'https://vimeo.com/user140352216',
+ 'duration': 1344
+ }
+ }]
+
+ def _real_extract(self, url):
+ video_id = self._match_id(url)
+ webpage = self._download_webpage(url, video_id)
+ vimeo_id = self._search_regex(r'hosterId":\s*"(?P[0-9]+)', webpage, 'vimeo id')
+ vimeo_url = VimeoIE._smuggle_referrer(f'https://player.vimeo.com/video/{vimeo_id}', 'https://aeon.co')
+ return self.url_result(vimeo_url, VimeoIE)
diff --git a/yt_dlp/extractor/amazon.py b/yt_dlp/extractor/amazon.py
index de4917adcc..4d3170683a 100644
--- a/yt_dlp/extractor/amazon.py
+++ b/yt_dlp/extractor/amazon.py
@@ -1,5 +1,5 @@
from .common import InfoExtractor
-from ..utils import int_or_none
+from ..utils import ExtractorError, int_or_none
class AmazonStoreIE(InfoExtractor):
@@ -9,7 +9,7 @@ class AmazonStoreIE(InfoExtractor):
'url': 'https://www.amazon.co.uk/dp/B098XNCHLD/',
'info_dict': {
'id': 'B098XNCHLD',
- 'title': 'md5:5f3194dbf75a8dcfc83079bd63a2abed',
+ 'title': 'md5:dae240564cbb2642170c02f7f0d7e472',
},
'playlist_mincount': 1,
'playlist': [{
@@ -18,28 +18,44 @@ class AmazonStoreIE(InfoExtractor):
'ext': 'mp4',
'title': 'mcdodo usb c cable 100W 5a',
'thumbnail': r're:^https?://.*\.jpg$',
+ 'duration': 34,
},
}]
}, {
'url': 'https://www.amazon.in/Sony-WH-1000XM4-Cancelling-Headphones-Bluetooth/dp/B0863TXGM3',
'info_dict': {
'id': 'B0863TXGM3',
- 'title': 'md5:b0bde4881d3cfd40d63af19f7898b8ff',
+ 'title': 'md5:d1d3352428f8f015706c84b31e132169',
},
'playlist_mincount': 4,
}, {
'url': 'https://www.amazon.com/dp/B0845NXCXF/',
'info_dict': {
'id': 'B0845NXCXF',
- 'title': 'md5:2145cd4e3c7782f1ee73649a3cff1171',
+ 'title': 'md5:f3fa12779bf62ddb6a6ec86a360a858e',
},
'playlist-mincount': 1,
+ }, {
+ 'url': 'https://www.amazon.es/Samsung-Smartphone-s-AMOLED-Quad-c%C3%A1mara-espa%C3%B1ola/dp/B08WX337PQ',
+ 'info_dict': {
+ 'id': 'B08WX337PQ',
+ 'title': 'md5:f3fa12779bf62ddb6a6ec86a360a858e',
+ },
+ 'playlist_mincount': 1,
}]
def _real_extract(self, url):
id = self._match_id(url)
- webpage = self._download_webpage(url, id)
- data_json = self._parse_json(self._html_search_regex(r'var\s?obj\s?=\s?jQuery\.parseJSON\(\'(.*)\'\)', webpage, 'data'), id)
+
+ for retry in self.RetryManager():
+ webpage = self._download_webpage(url, id)
+ try:
+ data_json = self._search_json(
+ r'var\s?obj\s?=\s?jQuery\.parseJSON\(\'', webpage, 'data', id,
+ transform_source=lambda x: x.replace(R'\\u', R'\u'))
+ except ExtractorError as e:
+ retry.error = e
+
entries = [{
'id': video['marketPlaceID'],
'url': video['url'],
@@ -49,4 +65,4 @@ class AmazonStoreIE(InfoExtractor):
'height': int_or_none(video.get('videoHeight')),
'width': int_or_none(video.get('videoWidth')),
} for video in (data_json.get('videos') or []) if video.get('isVideo') and video.get('url')]
- return self.playlist_result(entries, playlist_id=id, playlist_title=data_json['title'])
+ return self.playlist_result(entries, playlist_id=id, playlist_title=data_json.get('title'))
diff --git a/yt_dlp/extractor/animeondemand.py b/yt_dlp/extractor/animeondemand.py
deleted file mode 100644
index de49db4ea7..0000000000
--- a/yt_dlp/extractor/animeondemand.py
+++ /dev/null
@@ -1,282 +0,0 @@
-import re
-
-from .common import InfoExtractor
-from ..compat import compat_str
-from ..utils import (
- determine_ext,
- extract_attributes,
- ExtractorError,
- join_nonempty,
- url_or_none,
- urlencode_postdata,
- urljoin,
-)
-
-
-class AnimeOnDemandIE(InfoExtractor):
- _VALID_URL = r'https?://(?:www\.)?anime-on-demand\.de/anime/(?P\d+)'
- _LOGIN_URL = 'https://www.anime-on-demand.de/users/sign_in'
- _APPLY_HTML5_URL = 'https://www.anime-on-demand.de/html5apply'
- _NETRC_MACHINE = 'animeondemand'
- # German-speaking countries of Europe
- _GEO_COUNTRIES = ['AT', 'CH', 'DE', 'LI', 'LU']
- _TESTS = [{
- # jap, OmU
- 'url': 'https://www.anime-on-demand.de/anime/161',
- 'info_dict': {
- 'id': '161',
- 'title': 'Grimgar, Ashes and Illusions (OmU)',
- 'description': 'md5:6681ce3c07c7189d255ac6ab23812d31',
- },
- 'playlist_mincount': 4,
- }, {
- # Film wording is used instead of Episode, ger/jap, Dub/OmU
- 'url': 'https://www.anime-on-demand.de/anime/39',
- 'only_matching': True,
- }, {
- # Episodes without titles, jap, OmU
- 'url': 'https://www.anime-on-demand.de/anime/162',
- 'only_matching': True,
- }, {
- # ger/jap, Dub/OmU, account required
- 'url': 'https://www.anime-on-demand.de/anime/169',
- 'only_matching': True,
- }, {
- # Full length film, non-series, ger/jap, Dub/OmU, account required
- 'url': 'https://www.anime-on-demand.de/anime/185',
- 'only_matching': True,
- }, {
- # Flash videos
- 'url': 'https://www.anime-on-demand.de/anime/12',
- 'only_matching': True,
- }]
-
- def _perform_login(self, username, password):
- login_page = self._download_webpage(
- self._LOGIN_URL, None, 'Downloading login page')
-
- if '>Our licensing terms allow the distribution of animes only to German-speaking countries of Europe' in login_page:
- self.raise_geo_restricted(
- '%s is only available in German-speaking countries of Europe' % self.IE_NAME)
-
- login_form = self._form_hidden_inputs('new_user', login_page)
-
- login_form.update({
- 'user[login]': username,
- 'user[password]': password,
- })
-
- post_url = self._search_regex(
- r'