mirror of
https://github.com/yt-dlp/yt-dlp.git
synced 2026-08-09 21:58:34 +03:00
Compare commits
110
Commits
2021.11.10
...
2021.12.01
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
91f071af60 | ||
|
|
2aa5e2cc01 | ||
|
|
1bad50eced | ||
|
|
ac0efabf12 | ||
|
|
73f035e1fe | ||
|
|
0cbed930c8 | ||
|
|
5118d2ec58 | ||
|
|
717216b093 | ||
|
|
5c22c63da3 | ||
|
|
ee8dd27a73 | ||
|
|
f304da8a29 | ||
|
|
06dfe0a0a2 | ||
|
|
75b725a7cc | ||
|
|
13ab5fa586 | ||
|
|
36eaf3039a | ||
|
|
f2ebc5c7be | ||
|
|
b222c27145 | ||
|
|
5e5be0c0b2 | ||
|
|
7578d77d8c | ||
|
|
b29165267f | ||
|
|
bc104778d6 | ||
|
|
d298d33fe6 | ||
|
|
bf57cfa8b7 | ||
|
|
3c2208f82d | ||
|
|
93e597ba28 | ||
|
|
b28cdcc0e4 | ||
|
|
a33c0d9c5d | ||
|
|
75689fe59b | ||
|
|
5ce1d13eba | ||
|
|
e04b003e64 | ||
|
|
909b0d66f4 | ||
|
|
dfd78699f5 | ||
|
|
639f80c1f9 | ||
|
|
896a88c5c6 | ||
|
|
4e4ba1d75f | ||
|
|
2abf081554 | ||
|
|
359df0fc42 | ||
|
|
3938a9212c | ||
|
|
cf1f13b817 | ||
|
|
18d6dd4e01 | ||
|
|
883ecd5494 | ||
|
|
eb56d132d2 | ||
|
|
17b4540662 | ||
|
|
da27aeea5c | ||
|
|
fec41d17a5 | ||
|
|
a61fd4cf6f | ||
|
|
a6213a4925 | ||
|
|
9941a1e127 | ||
|
|
ff51ed588f | ||
|
|
57dbe8077f | ||
|
|
e5d731f35d | ||
|
|
d52cd2f5cd | ||
|
|
bc8ab44ea0 | ||
|
|
8f122fa070 | ||
|
|
14a086058a | ||
|
|
0e6b018a10 | ||
|
|
f7b558df4d | ||
|
|
1ee34c76bb | ||
|
|
234416e4bf | ||
|
|
c98d4df23b | ||
|
|
849d699a8b | ||
|
|
77fcc65158 | ||
|
|
545ad64988 | ||
|
|
d76991ab07 | ||
|
|
282f570918 | ||
|
|
c07a39ae8e | ||
|
|
c5e3f84972 | ||
|
|
c45b87419f | ||
|
|
7333296ff5 | ||
|
|
a04e005521 | ||
|
|
6b993ca765 | ||
|
|
dd2a987d3f | ||
|
|
9222c38182 | ||
|
|
467b6b8387 | ||
|
|
8863c8f09e | ||
|
|
e16fefd869 | ||
|
|
c6118ca2cc | ||
|
|
764f5de2f4 | ||
|
|
cfcaf64a4b | ||
|
|
402cd603a4 | ||
|
|
22a510ff44 | ||
|
|
61be785a67 | ||
|
|
11852843e7 | ||
|
|
525d9e0c7d | ||
|
|
9d63137eac | ||
|
|
266a1b5d52 | ||
|
|
450bdf69bc | ||
|
|
720c309932 | ||
|
|
d8cf8d97a8 | ||
|
|
d0d012d4e7 | ||
|
|
013b50b794 | ||
|
|
dac5df5a98 | ||
|
|
f279aaee8e | ||
|
|
d0e6121adf | ||
|
|
9ac24e235e | ||
|
|
7c7f7161fc | ||
|
|
e339d25a0d | ||
|
|
39c04074e7 | ||
|
|
92775d8a40 | ||
|
|
df03de2c02 | ||
|
|
48e9310660 | ||
|
|
c1dc0ee56e | ||
|
|
bf5f605e76 | ||
|
|
e08a85d865 | ||
|
|
093a17107e | ||
|
|
44bcb8d122 | ||
|
|
013ae2e503 | ||
|
|
b47d236d72 | ||
|
|
9ebf3c6ab9 | ||
|
|
7144b697fc |
@@ -11,7 +11,7 @@ body:
|
|||||||
options:
|
options:
|
||||||
- label: I'm reporting a broken site
|
- label: I'm reporting a broken site
|
||||||
required: true
|
required: true
|
||||||
- label: I've verified that I'm running yt-dlp version **2021.11.10**. ([update instructions](https://github.com/yt-dlp/yt-dlp#update))
|
- label: I've verified that I'm running yt-dlp version **2021.11.10.1**. ([update instructions](https://github.com/yt-dlp/yt-dlp#update))
|
||||||
required: true
|
required: true
|
||||||
- label: I've checked that all provided URLs are alive and playable in a browser
|
- label: I've checked that all provided URLs are alive and playable in a browser
|
||||||
required: true
|
required: true
|
||||||
@@ -51,12 +51,12 @@ body:
|
|||||||
[debug] Portable config file: yt-dlp.conf
|
[debug] Portable config file: yt-dlp.conf
|
||||||
[debug] Portable config: ['-i']
|
[debug] Portable config: ['-i']
|
||||||
[debug] Encodings: locale cp1252, fs utf-8, stdout utf-8, stderr utf-8, pref cp1252
|
[debug] Encodings: locale cp1252, fs utf-8, stdout utf-8, stderr utf-8, pref cp1252
|
||||||
[debug] yt-dlp version 2021.11.10 (exe)
|
[debug] yt-dlp version 2021.11.10.1 (exe)
|
||||||
[debug] Python version 3.8.8 (CPython 64bit) - Windows-10-10.0.19041-SP0
|
[debug] Python version 3.8.8 (CPython 64bit) - Windows-10-10.0.19041-SP0
|
||||||
[debug] exe versions: ffmpeg 3.0.1, ffprobe 3.0.1
|
[debug] exe versions: ffmpeg 3.0.1, ffprobe 3.0.1
|
||||||
[debug] Optional libraries: Cryptodome, keyring, mutagen, sqlite, websockets
|
[debug] Optional libraries: Cryptodome, keyring, mutagen, sqlite, websockets
|
||||||
[debug] Proxy map: {}
|
[debug] Proxy map: {}
|
||||||
yt-dlp is up to date (2021.11.10)
|
yt-dlp is up to date (2021.11.10.1)
|
||||||
<more lines>
|
<more lines>
|
||||||
render: shell
|
render: shell
|
||||||
validations:
|
validations:
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ body:
|
|||||||
options:
|
options:
|
||||||
- label: I'm reporting a new site support request
|
- label: I'm reporting a new site support request
|
||||||
required: true
|
required: true
|
||||||
- label: I've verified that I'm running yt-dlp version **2021.11.10**. ([update instructions](https://github.com/yt-dlp/yt-dlp#update))
|
- label: I've verified that I'm running yt-dlp version **2021.11.10.1**. ([update instructions](https://github.com/yt-dlp/yt-dlp#update))
|
||||||
required: true
|
required: true
|
||||||
- label: I've checked that all provided URLs are alive and playable in a browser
|
- label: I've checked that all provided URLs are alive and playable in a browser
|
||||||
required: true
|
required: true
|
||||||
@@ -62,12 +62,12 @@ body:
|
|||||||
[debug] Portable config file: yt-dlp.conf
|
[debug] Portable config file: yt-dlp.conf
|
||||||
[debug] Portable config: ['-i']
|
[debug] Portable config: ['-i']
|
||||||
[debug] Encodings: locale cp1252, fs utf-8, stdout utf-8, stderr utf-8, pref cp1252
|
[debug] Encodings: locale cp1252, fs utf-8, stdout utf-8, stderr utf-8, pref cp1252
|
||||||
[debug] yt-dlp version 2021.11.10 (exe)
|
[debug] yt-dlp version 2021.11.10.1 (exe)
|
||||||
[debug] Python version 3.8.8 (CPython 64bit) - Windows-10-10.0.19041-SP0
|
[debug] Python version 3.8.8 (CPython 64bit) - Windows-10-10.0.19041-SP0
|
||||||
[debug] exe versions: ffmpeg 3.0.1, ffprobe 3.0.1
|
[debug] exe versions: ffmpeg 3.0.1, ffprobe 3.0.1
|
||||||
[debug] Optional libraries: Cryptodome, keyring, mutagen, sqlite, websockets
|
[debug] Optional libraries: Cryptodome, keyring, mutagen, sqlite, websockets
|
||||||
[debug] Proxy map: {}
|
[debug] Proxy map: {}
|
||||||
yt-dlp is up to date (2021.11.10)
|
yt-dlp is up to date (2021.11.10.1)
|
||||||
<more lines>
|
<more lines>
|
||||||
render: shell
|
render: shell
|
||||||
validations:
|
validations:
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ body:
|
|||||||
options:
|
options:
|
||||||
- label: I'm reporting a site feature request
|
- label: I'm reporting a site feature request
|
||||||
required: true
|
required: true
|
||||||
- label: I've verified that I'm running yt-dlp version **2021.11.10**. ([update instructions](https://github.com/yt-dlp/yt-dlp#update))
|
- label: I've verified that I'm running yt-dlp version **2021.11.10.1**. ([update instructions](https://github.com/yt-dlp/yt-dlp#update))
|
||||||
required: true
|
required: true
|
||||||
- label: I've checked that all provided URLs are alive and playable in a browser
|
- label: I've checked that all provided URLs are alive and playable in a browser
|
||||||
required: true
|
required: true
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ body:
|
|||||||
options:
|
options:
|
||||||
- label: I'm reporting a bug unrelated to a specific site
|
- label: I'm reporting a bug unrelated to a specific site
|
||||||
required: true
|
required: true
|
||||||
- label: I've verified that I'm running yt-dlp version **2021.11.10**. ([update instructions](https://github.com/yt-dlp/yt-dlp#update))
|
- label: I've verified that I'm running yt-dlp version **2021.11.10.1**. ([update instructions](https://github.com/yt-dlp/yt-dlp#update))
|
||||||
required: true
|
required: true
|
||||||
- label: I've checked that all provided URLs are alive and playable in a browser
|
- label: I've checked that all provided URLs are alive and playable in a browser
|
||||||
required: true
|
required: true
|
||||||
@@ -45,12 +45,12 @@ body:
|
|||||||
[debug] Portable config file: yt-dlp.conf
|
[debug] Portable config file: yt-dlp.conf
|
||||||
[debug] Portable config: ['-i']
|
[debug] Portable config: ['-i']
|
||||||
[debug] Encodings: locale cp1252, fs utf-8, stdout utf-8, stderr utf-8, pref cp1252
|
[debug] Encodings: locale cp1252, fs utf-8, stdout utf-8, stderr utf-8, pref cp1252
|
||||||
[debug] yt-dlp version 2021.11.10 (exe)
|
[debug] yt-dlp version 2021.11.10.1 (exe)
|
||||||
[debug] Python version 3.8.8 (CPython 64bit) - Windows-10-10.0.19041-SP0
|
[debug] Python version 3.8.8 (CPython 64bit) - Windows-10-10.0.19041-SP0
|
||||||
[debug] exe versions: ffmpeg 3.0.1, ffprobe 3.0.1
|
[debug] exe versions: ffmpeg 3.0.1, ffprobe 3.0.1
|
||||||
[debug] Optional libraries: Cryptodome, keyring, mutagen, sqlite, websockets
|
[debug] Optional libraries: Cryptodome, keyring, mutagen, sqlite, websockets
|
||||||
[debug] Proxy map: {}
|
[debug] Proxy map: {}
|
||||||
yt-dlp is up to date (2021.11.10)
|
yt-dlp is up to date (2021.11.10.1)
|
||||||
<more lines>
|
<more lines>
|
||||||
render: shell
|
render: shell
|
||||||
validations:
|
validations:
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ body:
|
|||||||
options:
|
options:
|
||||||
- label: I'm reporting a feature request
|
- label: I'm reporting a feature request
|
||||||
required: true
|
required: true
|
||||||
- label: I've verified that I'm running yt-dlp version **2021.11.10**. ([update instructions](https://github.com/yt-dlp/yt-dlp#update))
|
- label: I've verified that I'm running yt-dlp version **2021.11.10.1**. ([update instructions](https://github.com/yt-dlp/yt-dlp#update))
|
||||||
required: true
|
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
|
- 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
|
required: true
|
||||||
|
|||||||
+29
-18
@@ -1,14 +1,11 @@
|
|||||||
name: Build
|
name: Build
|
||||||
|
on: workflow_dispatch
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- release
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build_unix:
|
build_unix:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
outputs:
|
outputs:
|
||||||
|
version_suffix: ${{ steps.version_suffix.outputs.version_suffix }}
|
||||||
ytdlp_version: ${{ steps.bump_version.outputs.ytdlp_version }}
|
ytdlp_version: ${{ steps.bump_version.outputs.ytdlp_version }}
|
||||||
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
||||||
sha256_bin: ${{ steps.sha256_bin.outputs.sha256_bin }}
|
sha256_bin: ${{ steps.sha256_bin.outputs.sha256_bin }}
|
||||||
@@ -26,23 +23,32 @@ jobs:
|
|||||||
python-version: '3.8'
|
python-version: '3.8'
|
||||||
- name: Install packages
|
- name: Install packages
|
||||||
run: sudo apt-get -y install zip pandoc man
|
run: sudo apt-get -y install zip pandoc man
|
||||||
|
- name: Set version suffix
|
||||||
|
id: version_suffix
|
||||||
|
env:
|
||||||
|
PUSH_VERSION_COMMIT: ${{ secrets.PUSH_VERSION_COMMIT }}
|
||||||
|
if: "env.PUSH_VERSION_COMMIT == ''"
|
||||||
|
run: echo ::set-output name=version_suffix::$(date -u +"%H%M%S")
|
||||||
- name: Bump version
|
- name: Bump version
|
||||||
id: bump_version
|
id: bump_version
|
||||||
run: |
|
run: |
|
||||||
python devscripts/update-version.py
|
python devscripts/update-version.py ${{ steps.version_suffix.outputs.version_suffix }}
|
||||||
make issuetemplates
|
make issuetemplates
|
||||||
- name: Print version
|
- name: Push to release
|
||||||
run: echo "${{ steps.bump_version.outputs.ytdlp_version }}"
|
id: push_release
|
||||||
- name: Update master
|
|
||||||
id: push_update
|
|
||||||
run: |
|
run: |
|
||||||
git config --global user.email "${{ github.event.pusher.email }}"
|
git config --global user.name github-actions
|
||||||
git config --global user.name "${{ github.event.pusher.name }}"
|
git config --global user.email github-actions@example.com
|
||||||
git add -u
|
git add -u
|
||||||
git commit -m "[version] update" -m ":ci skip all"
|
git commit -m "[version] update" -m "Created by: ${{ github.event.sender.login }}" -m ":ci skip all"
|
||||||
git pull --rebase origin ${{ github.event.repository.master_branch }}
|
git push origin --force ${{ github.event.ref }}:release
|
||||||
git push origin ${{ github.event.ref }}:${{ github.event.repository.master_branch }}
|
|
||||||
echo ::set-output name=head_sha::$(git rev-parse HEAD)
|
echo ::set-output name=head_sha::$(git rev-parse HEAD)
|
||||||
|
- name: Update master
|
||||||
|
id: push_master
|
||||||
|
env:
|
||||||
|
PUSH_VERSION_COMMIT: ${{ secrets.PUSH_VERSION_COMMIT }}
|
||||||
|
if: "env.PUSH_VERSION_COMMIT != ''"
|
||||||
|
run: git push origin ${{ github.event.ref }}
|
||||||
- name: Get Changelog
|
- name: Get Changelog
|
||||||
id: get_changelog
|
id: get_changelog
|
||||||
run: |
|
run: |
|
||||||
@@ -155,10 +161,11 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v2
|
||||||
# In order to create a universal2 application, the version of python3 in /usr/bin has to be used
|
# In order to create a universal2 application, the version of python3 in /usr/bin has to be used
|
||||||
|
# Pyinstaller is pinned to 4.5.1 because the builds are failing in 4.6, 4.7
|
||||||
- name: Install Requirements
|
- name: Install Requirements
|
||||||
run: |
|
run: |
|
||||||
brew install coreutils
|
brew install coreutils
|
||||||
/usr/bin/python3 -m pip install -U --user pip Pyinstaller mutagen pycryptodomex websockets
|
/usr/bin/python3 -m pip install -U --user pip Pyinstaller==4.5.1 mutagen pycryptodomex websockets
|
||||||
- name: Bump version
|
- name: Bump version
|
||||||
id: bump_version
|
id: bump_version
|
||||||
run: /usr/bin/python3 devscripts/update-version.py
|
run: /usr/bin/python3 devscripts/update-version.py
|
||||||
@@ -232,7 +239,9 @@ jobs:
|
|||||||
pip install "https://yt-dlp.github.io/Pyinstaller-Builds/x86_64/pyinstaller-4.5.1-py3-none-any.whl" mutagen pycryptodomex websockets
|
pip install "https://yt-dlp.github.io/Pyinstaller-Builds/x86_64/pyinstaller-4.5.1-py3-none-any.whl" mutagen pycryptodomex websockets
|
||||||
- name: Bump version
|
- name: Bump version
|
||||||
id: bump_version
|
id: bump_version
|
||||||
run: python devscripts/update-version.py
|
env:
|
||||||
|
version_suffix: ${{ needs.build_unix.outputs.version_suffix }}
|
||||||
|
run: python devscripts/update-version.py ${{ env.version_suffix }}
|
||||||
- name: Build lazy extractors
|
- name: Build lazy extractors
|
||||||
id: lazy_extractors
|
id: lazy_extractors
|
||||||
run: python devscripts/make_lazy_extractors.py
|
run: python devscripts/make_lazy_extractors.py
|
||||||
@@ -319,7 +328,9 @@ jobs:
|
|||||||
pip install "https://yt-dlp.github.io/Pyinstaller-Builds/i686/pyinstaller-4.5.1-py3-none-any.whl" mutagen pycryptodomex websockets
|
pip install "https://yt-dlp.github.io/Pyinstaller-Builds/i686/pyinstaller-4.5.1-py3-none-any.whl" mutagen pycryptodomex websockets
|
||||||
- name: Bump version
|
- name: Bump version
|
||||||
id: bump_version
|
id: bump_version
|
||||||
run: python devscripts/update-version.py
|
env:
|
||||||
|
version_suffix: ${{ needs.build_unix.outputs.version_suffix }}
|
||||||
|
run: python devscripts/update-version.py ${{ env.version_suffix }}
|
||||||
- name: Build lazy extractors
|
- name: Build lazy extractors
|
||||||
id: lazy_extractors
|
id: lazy_extractors
|
||||||
run: python devscripts/make_lazy_extractors.py
|
run: python devscripts/make_lazy_extractors.py
|
||||||
|
|||||||
+31
-25
@@ -6,41 +6,47 @@ cookies
|
|||||||
.netrc
|
.netrc
|
||||||
|
|
||||||
# Downloaded
|
# Downloaded
|
||||||
*.srt
|
|
||||||
*.ttml
|
|
||||||
*.sbv
|
|
||||||
*.vtt
|
|
||||||
*.flv
|
|
||||||
*.mp4
|
|
||||||
*.m4a
|
|
||||||
*.m4v
|
|
||||||
*.mp3
|
|
||||||
*.3gp
|
*.3gp
|
||||||
*.webm
|
*.annotations.xml
|
||||||
*.wav
|
|
||||||
*.ape
|
*.ape
|
||||||
*.mkv
|
*.aria2
|
||||||
*.flac
|
|
||||||
*.avi
|
*.avi
|
||||||
*.swf
|
*.description
|
||||||
*.part
|
*.desktop
|
||||||
*.part-*
|
|
||||||
*.ytdl
|
|
||||||
*.dump
|
*.dump
|
||||||
|
*.flac
|
||||||
|
*.flv
|
||||||
*.frag
|
*.frag
|
||||||
*.frag.urls
|
*.frag.urls
|
||||||
*.aria2
|
*.info.json
|
||||||
*.swp
|
*.jpeg
|
||||||
|
*.jpg
|
||||||
|
*.live_chat.json
|
||||||
|
*.m4a
|
||||||
|
*.m4v
|
||||||
|
*.mhtml
|
||||||
|
*.mkv
|
||||||
|
*.mov
|
||||||
|
*.mp3
|
||||||
|
*.mp4
|
||||||
*.ogg
|
*.ogg
|
||||||
*.opus
|
*.opus
|
||||||
*.info.json
|
*.part
|
||||||
*.live_chat.json
|
*.part-*
|
||||||
*.jpg
|
|
||||||
*.jpeg
|
|
||||||
*.png
|
*.png
|
||||||
|
*.sbv
|
||||||
|
*.srt
|
||||||
|
*.swf
|
||||||
|
*.swp
|
||||||
|
*.ttml
|
||||||
|
*.unknown_video
|
||||||
|
*.url
|
||||||
|
*.vtt
|
||||||
|
*.wav
|
||||||
|
*.webloc
|
||||||
|
*.webm
|
||||||
*.webp
|
*.webp
|
||||||
*.annotations.xml
|
*.ytdl
|
||||||
*.description
|
|
||||||
.cache/
|
.cache/
|
||||||
|
|
||||||
# Allow config/media files in testdata
|
# Allow config/media files in testdata
|
||||||
|
|||||||
+1
-1
@@ -209,7 +209,7 @@ After you have ensured this site is distributing its content legally, you can fo
|
|||||||
```
|
```
|
||||||
1. Add an import in [`yt_dlp/extractor/extractors.py`](yt_dlp/extractor/extractors.py).
|
1. Add an import in [`yt_dlp/extractor/extractors.py`](yt_dlp/extractor/extractors.py).
|
||||||
1. Run `python test/test_download.py TestDownload.test_YourExtractor`. This *should fail* at first, but you can continually re-run it until you're done. If you decide to add more than one test, the tests will then be named `TestDownload.test_YourExtractor`, `TestDownload.test_YourExtractor_1`, `TestDownload.test_YourExtractor_2`, etc. Note that tests with `only_matching` key in test's dict are not counted in. You can also run all the tests in one go with `TestDownload.test_YourExtractor_all`
|
1. Run `python test/test_download.py TestDownload.test_YourExtractor`. This *should fail* at first, but you can continually re-run it until you're done. If you decide to add more than one test, the tests will then be named `TestDownload.test_YourExtractor`, `TestDownload.test_YourExtractor_1`, `TestDownload.test_YourExtractor_2`, etc. Note that tests with `only_matching` key in test's dict are not counted in. You can also run all the tests in one go with `TestDownload.test_YourExtractor_all`
|
||||||
1. Make sure you have atleast one test for your extractor. Even if all videos covered by the extractor are expected to be inaccessible for automated testing, tests should still be added with a `skip` parameter indicating why the purticular test is disabled from running.
|
1. Make sure you have atleast one test for your extractor. Even if all videos covered by the extractor are expected to be inaccessible for automated testing, tests should still be added with a `skip` parameter indicating why the particular test is disabled from running.
|
||||||
1. Have a look at [`yt_dlp/extractor/common.py`](yt_dlp/extractor/common.py) for possible helper methods and a [detailed description of what your extractor should and may return](yt_dlp/extractor/common.py#L91-L426). Add tests and code for as many as you want.
|
1. Have a look at [`yt_dlp/extractor/common.py`](yt_dlp/extractor/common.py) for possible helper methods and a [detailed description of what your extractor should and may return](yt_dlp/extractor/common.py#L91-L426). Add tests and code for as many as you want.
|
||||||
1. Make sure your code follows [yt-dlp coding conventions](#yt-dlp-coding-conventions) and check the code with [flake8](https://flake8.pycqa.org/en/latest/index.html#quickstart):
|
1. Make sure your code follows [yt-dlp coding conventions](#yt-dlp-coding-conventions) and check the code with [flake8](https://flake8.pycqa.org/en/latest/index.html#quickstart):
|
||||||
|
|
||||||
|
|||||||
@@ -139,3 +139,19 @@ rhendric
|
|||||||
sdomi
|
sdomi
|
||||||
selfisekai
|
selfisekai
|
||||||
stanoarn
|
stanoarn
|
||||||
|
0xA7404A/Aurora
|
||||||
|
4a1e2y5
|
||||||
|
aarubui
|
||||||
|
chio0hai
|
||||||
|
cntrl-s
|
||||||
|
Deer-Spangle
|
||||||
|
DEvmIb
|
||||||
|
Grabien
|
||||||
|
j54vc1bk
|
||||||
|
mpeter50
|
||||||
|
mrpapersonic
|
||||||
|
pabs3
|
||||||
|
staubichsauger
|
||||||
|
xenova
|
||||||
|
Yakabuff
|
||||||
|
zulaport
|
||||||
|
|||||||
+216
-9
@@ -5,14 +5,120 @@
|
|||||||
|
|
||||||
* Run `make doc`
|
* Run `make doc`
|
||||||
* Update Changelog.md and CONTRIBUTORS
|
* Update Changelog.md and CONTRIBUTORS
|
||||||
* Change "Merged with ytdl" version in Readme.md if needed
|
* Change "Based on ytdl" version in Readme.md if needed
|
||||||
* Add new/fixed extractors in "new features" section of Readme.md
|
* Commit as `Release <version>` and push to master
|
||||||
* Commit as `Release <version>`
|
* Dispatch the workflow https://github.com/yt-dlp/yt-dlp/actions/workflows/build.yml on master
|
||||||
* Push to origin/release using `git push origin master:release`
|
|
||||||
build task will now run
|
|
||||||
|
|
||||||
-->
|
-->
|
||||||
|
|
||||||
|
### 2021.12.01
|
||||||
|
|
||||||
|
* **Add option `--wait-for-video` to wait for scheduled streams**
|
||||||
|
* Add option `--break-per-input` to apply --break-on... to each input URL
|
||||||
|
* Add option `--embed-info-json` to embed info.json in mkv
|
||||||
|
* Add compat-option `embed-metadata`
|
||||||
|
* Allow using a custom format selector through API
|
||||||
|
* [AES] Add ECB mode by [nao20010128nao](https://github.com/nao20010128nao)
|
||||||
|
* [build] Fix MacOS Build
|
||||||
|
* [build] Save Git HEAD at release alongside version info
|
||||||
|
* [build] Use `workflow_dispatch` for release
|
||||||
|
* [downloader/ffmpeg] Fix for direct videos inside mpd manifests
|
||||||
|
* [downloader] Add colors to download progress
|
||||||
|
* [EmbedSubtitles] Slightly relax duration check and related cleanup
|
||||||
|
* [ExtractAudio] Fix conversion to `wav` and `vorbis`
|
||||||
|
* [ExtractAudio] Support `alac`
|
||||||
|
* [extractor] Extract `average_rating` from JSON-LD
|
||||||
|
* [FixupM3u8] Fixup MPEG-TS in MP4 container
|
||||||
|
* [generic] Support mpd manifests without extension by [shirt](https://github.com/shirt-dev)
|
||||||
|
* [hls] Better FairPlay DRM detection by [nyuszika7h](https://github.com/nyuszika7h)
|
||||||
|
* [jsinterp] Fix splice to handle float (for youtube js player f1ca6900)
|
||||||
|
* [utils] Allow alignment in `render_table` and add tests
|
||||||
|
* [utils] Fix `PagedList`
|
||||||
|
* [utils] Fix error when copying `LazyList`
|
||||||
|
* Clarify video/audio-only formats in -F
|
||||||
|
* Ensure directory exists when checking formats
|
||||||
|
* Ensure path for link files exists by [Zirro](https://github.com/Zirro)
|
||||||
|
* Ensure same config file is not loaded multiple times
|
||||||
|
* Fix 'postprocessor_hooks`
|
||||||
|
* Fix `--break-on-archive` when pre-checking
|
||||||
|
* Fix `--check-formats` for `mhtml`
|
||||||
|
* Fix `--load-info-json` of playlists with failed entries
|
||||||
|
* Fix `--trim-filename` when filename has `.`
|
||||||
|
* Fix bug in parsing `--add-header`
|
||||||
|
* Fix error in `report_unplayable_conflict` by [shirt](https://github.com/shirt-dev)
|
||||||
|
* Fix writing playlist infojson with `--no-clean-infojson`
|
||||||
|
* Validate --get-bypass-country
|
||||||
|
* [blogger] Add extractor by [pabs3](https://github.com/pabs3)
|
||||||
|
* [breitbart] Add extractor by [Grabien](https://github.com/Grabien)
|
||||||
|
* [CableAV] Add extractor by [j54vc1bk](https://github.com/j54vc1bk)
|
||||||
|
* [CanalAlpha] Add extractor by [Ashish0804](https://github.com/Ashish0804)
|
||||||
|
* [CozyTV] Add extractor by [Ashish0804](https://github.com/Ashish0804)
|
||||||
|
* [CPTwentyFour] Add extractor by [Ashish0804](https://github.com/Ashish0804)
|
||||||
|
* [DiscoveryPlus] Add `DiscoveryPlusItalyShowIE` by [Ashish0804](https://github.com/Ashish0804)
|
||||||
|
* [ESPNCricInfo] Add extractor by [Ashish0804](https://github.com/Ashish0804)
|
||||||
|
* [LinkedIn] Add extractor by [u-spec-png](https://github.com/u-spec-png)
|
||||||
|
* [mixch] Add extractor by [nao20010128nao](https://github.com/nao20010128nao)
|
||||||
|
* [nebula] Add `NebulaCollectionIE` and rewrite extractor by [hheimbuerger](https://github.com/hheimbuerger)
|
||||||
|
* [OneFootball] Add extractor by [Ashish0804](https://github.com/Ashish0804)
|
||||||
|
* [peer.tv] Add extractor by [u-spec-png](https://github.com/u-spec-png)
|
||||||
|
* [radiozet] Add extractor by [0xA7404A](https://github.com/0xA7404A) (Aurora)
|
||||||
|
* [redgifs] Add extractor by [chio0hai](https://github.com/chio0hai)
|
||||||
|
* [RedGifs] Add Search and User extractors by [Deer-Spangle](https://github.com/Deer-Spangle)
|
||||||
|
* [rtrfm] Add extractor by [pabs3](https://github.com/pabs3)
|
||||||
|
* [Streamff] Add extractor by [cntrl-s](https://github.com/cntrl-s)
|
||||||
|
* [Stripchat] Add extractor by [zulaport](https://github.com/zulaport)
|
||||||
|
* [Aljazeera] Fix extractor by [u-spec-png](https://github.com/u-spec-png)
|
||||||
|
* [AmazonStoreIE] Fix regex to not match vdp urls by [Ashish0804](https://github.com/Ashish0804)
|
||||||
|
* [ARDBetaMediathek] Handle new URLs
|
||||||
|
* [bbc] Get all available formats by [nyuszika7h](https://github.com/nyuszika7h)
|
||||||
|
* [Bilibili] Fix title extraction by [u-spec-png](https://github.com/u-spec-png)
|
||||||
|
* [CBC Gem] Fix for shows that don't have all seasons by [makeworld-the-better-one](https://github.com/makeworld-the-better-one)
|
||||||
|
* [curiositystream] Add more metadata
|
||||||
|
* [CuriosityStream] Fix series
|
||||||
|
* [DiscoveryPlus] Rewrite extractors by [Ashish0804](https://github.com/Ashish0804), [pukkandan](https://github.com/pukkandan)
|
||||||
|
* [HotStar] Set language field from tags by [Ashish0804](https://github.com/Ashish0804)
|
||||||
|
* [instagram, cleanup] Refactor extractors
|
||||||
|
* [Instagram] Display more login errors by [MinePlayersPE](https://github.com/MinePlayersPE)
|
||||||
|
* [itv] Fix extractor by [staubichsauger](https://github.com/staubichsauger), [pukkandan](https://github.com/pukkandan)
|
||||||
|
* [mediaklikk] Expand valid URL
|
||||||
|
* [MTV] Improve mgid extraction by [Sipherdrakon](https://github.com/Sipherdrakon), [kikuyan](https://github.com/kikuyan)
|
||||||
|
* [nexx] Better error message for unsupported format
|
||||||
|
* [NovaEmbed] Fix extractor by [pukkandan](https://github.com/pukkandan), [std-move](https://github.com/std-move)
|
||||||
|
* [PatreonUser] Do not capture RSS URLs
|
||||||
|
* [Reddit] Add support for 1080p videos by [xenova](https://github.com/xenova)
|
||||||
|
* [RoosterTeethSeries] Fix for multiple pages by [MinePlayersPE](https://github.com/MinePlayersPE)
|
||||||
|
* [sbs] Fix for movies and livestreams
|
||||||
|
* [Senate.gov] Add SenateGovIE and fix SenateISVPIE by [Grabien](https://github.com/Grabien), [pukkandan](https://github.com/pukkandan)
|
||||||
|
* [soundcloud:search] Fix pagination
|
||||||
|
* [tiktok:user] Set `webpage_url` correctly
|
||||||
|
* [Tokentube] Fix description by [u-spec-png](https://github.com/u-spec-png)
|
||||||
|
* [trovo] Fix extractor by [nyuszika7h](https://github.com/nyuszika7h)
|
||||||
|
* [tv2] Expand valid URL
|
||||||
|
* [Tvplayhome] Fix extractor by [pukkandan](https://github.com/pukkandan), [18928172992817182](https://github.com/18928172992817182)
|
||||||
|
* [Twitch:vod] Add chapters by [mpeter50](https://github.com/mpeter50)
|
||||||
|
* [twitch:vod] Extract live status by [DEvmIb](https://github.com/DEvmIb)
|
||||||
|
* [VidLii] Add 720p support by [mrpapersonic](https://github.com/mrpapersonic)
|
||||||
|
* [vimeo] Add fallback for config URL
|
||||||
|
* [vimeo] Sort http formats higher
|
||||||
|
* [WDR] Expand valid URL
|
||||||
|
* [willow] Add extractor by [aarubui](https://github.com/aarubui)
|
||||||
|
* [xvideos] Detect embed URLs by [4a1e2y5](https://github.com/4a1e2y5)
|
||||||
|
* [xvideos] Fix extractor by [Yakabuff](https://github.com/Yakabuff)
|
||||||
|
* [youtube, cleanup] Reorganize Tab and Search extractor inheritances
|
||||||
|
* [youtube:search_url] Add playlist/channel support
|
||||||
|
* [youtube] Add `default` player client by [coletdjnz](https://github.com/coletdjnz)
|
||||||
|
* [youtube] Add storyboard formats
|
||||||
|
* [youtube] Decrypt n-sig for URLs with `ratebypass`
|
||||||
|
* [youtube] Minor improvement to format sorting
|
||||||
|
* [cleanup] Add deprecation warnings
|
||||||
|
* [cleanup] Minor cleanup
|
||||||
|
* [cleanup] Misc cleanup
|
||||||
|
* [cleanup] Refactor `JSInterpreter._seperate`
|
||||||
|
* [Cleanup] Remove some unnecessary groups in regexes by [Ashish0804](https://github.com/Ashish0804)
|
||||||
|
|
||||||
|
|
||||||
|
### 2021.11.10.1
|
||||||
|
|
||||||
|
* Temporarily disable MacOS Build
|
||||||
|
|
||||||
### 2021.11.10
|
### 2021.11.10
|
||||||
|
|
||||||
@@ -36,7 +142,7 @@
|
|||||||
* [fragment] Fix progress display in fragmented downloads
|
* [fragment] Fix progress display in fragmented downloads
|
||||||
* [downloader/ffmpeg] Fix vtt download with ffmpeg
|
* [downloader/ffmpeg] Fix vtt download with ffmpeg
|
||||||
* [ffmpeg] Detect presence of setts and libavformat version
|
* [ffmpeg] Detect presence of setts and libavformat version
|
||||||
* [ExtractAudio] Rescale --audio-quality correctly by [CrypticSignal](https://github.com/CrypticSignal), [pukkandan](https://github.com/pukkandan)
|
* [ExtractAudio] Rescale `--audio-quality` correctly by [CrypticSignal](https://github.com/CrypticSignal), [pukkandan](https://github.com/pukkandan)
|
||||||
* [ExtractAudio] Use `libfdk_aac` if available by [CrypticSignal](https://github.com/CrypticSignal)
|
* [ExtractAudio] Use `libfdk_aac` if available by [CrypticSignal](https://github.com/CrypticSignal)
|
||||||
* [FormatSort] `eac3` is better than `ac3`
|
* [FormatSort] `eac3` is better than `ac3`
|
||||||
* [FormatSort] Fix some fields' defaults
|
* [FormatSort] Fix some fields' defaults
|
||||||
@@ -1368,9 +1474,8 @@
|
|||||||
* Cleaned up the fork for public use
|
* Cleaned up the fork for public use
|
||||||
|
|
||||||
|
|
||||||
**PS**: All uncredited changes above this point are authored by [pukkandan](https://github.com/pukkandan)
|
**Note**: All uncredited changes above this point are authored by [pukkandan](https://github.com/pukkandan)
|
||||||
|
|
||||||
### Unreleased changes in [blackjack4494/yt-dlc](https://github.com/blackjack4494/yt-dlc)
|
|
||||||
* Updated to youtube-dl release 2020.11.26 by [pukkandan](https://github.com/pukkandan)
|
* Updated to youtube-dl release 2020.11.26 by [pukkandan](https://github.com/pukkandan)
|
||||||
* Youtube improvements by [pukkandan](https://github.com/pukkandan)
|
* Youtube improvements by [pukkandan](https://github.com/pukkandan)
|
||||||
* Implemented all Youtube Feeds (ytfav, ytwatchlater, ytsubs, ythistory, ytrec) and SearchURL
|
* Implemented all Youtube Feeds (ytfav, ytwatchlater, ytsubs, ythistory, ytrec) and SearchURL
|
||||||
@@ -1393,8 +1498,110 @@
|
|||||||
* [spreaker] fix SpreakerShowIE test URL by [pukkandan](https://github.com/pukkandan)
|
* [spreaker] fix SpreakerShowIE test URL by [pukkandan](https://github.com/pukkandan)
|
||||||
* [Vlive] Fix playlist handling when downloading a channel by [kyuyeunk](https://github.com/kyuyeunk)
|
* [Vlive] Fix playlist handling when downloading a channel by [kyuyeunk](https://github.com/kyuyeunk)
|
||||||
* [tmz] Fix extractor by [diegorodriguezv](https://github.com/diegorodriguezv)
|
* [tmz] Fix extractor by [diegorodriguezv](https://github.com/diegorodriguezv)
|
||||||
|
* [ITV] BTCC URL update by [WolfganP](https://github.com/WolfganP)
|
||||||
* [generic] Detect embedded bitchute videos by [pukkandan](https://github.com/pukkandan)
|
* [generic] Detect embedded bitchute videos by [pukkandan](https://github.com/pukkandan)
|
||||||
* [generic] Extract embedded youtube and twitter videos by [diegorodriguezv](https://github.com/diegorodriguezv)
|
* [generic] Extract embedded youtube and twitter videos by [diegorodriguezv](https://github.com/diegorodriguezv)
|
||||||
* [ffmpeg] Ensure all streams are copied by [pukkandan](https://github.com/pukkandan)
|
* [ffmpeg] Ensure all streams are copied by [pukkandan](https://github.com/pukkandan)
|
||||||
* [embedthumbnail] Fix for os.rename error by [pukkandan](https://github.com/pukkandan)
|
* [embedthumbnail] Fix for os.rename error by [pukkandan](https://github.com/pukkandan)
|
||||||
* make_win.bat: don't use UPX to pack vcruntime140.dll by [jbruchon](https://github.com/jbruchon)
|
* make_win.bat: don't use UPX to pack vcruntime140.dll by [jbruchon](https://github.com/jbruchon)
|
||||||
|
|
||||||
|
|
||||||
|
### Changelog of [blackjack4494/yt-dlc](https://github.com/blackjack4494/yt-dlc) till release 2020.11.11-3
|
||||||
|
|
||||||
|
**Note**: This was constructed from the merge commit messages and may not be entirely accurate
|
||||||
|
|
||||||
|
* [bandcamp] fix failing test. remove subclass hack by [insaneracist](https://github.com/insaneracist)
|
||||||
|
* [bandcamp] restore album downloads by [insaneracist](https://github.com/insaneracist)
|
||||||
|
* [francetv] fix extractor by [Surkal](https://github.com/Surkal)
|
||||||
|
* [gdcvault] fix extractor by [blackjack4494](https://github.com/blackjack4494)
|
||||||
|
* [hotstar] Move to API v1 by [theincognito-inc](https://github.com/theincognito-inc)
|
||||||
|
* [hrfernsehen] add extractor by [blocktrron](https://github.com/blocktrron)
|
||||||
|
* [kakao] new apis by [blackjack4494](https://github.com/blackjack4494)
|
||||||
|
* [la7] fix missing protocol by [nixxo](https://github.com/nixxo)
|
||||||
|
* [mailru] removed escaped braces, use urljoin, added tests by [nixxo](https://github.com/nixxo)
|
||||||
|
* [MTV/Nick] universal mgid extractor + fix nick.de feed by [blackjack4494](https://github.com/blackjack4494)
|
||||||
|
* [mtv] Fix a missing match_id by [nixxo](https://github.com/nixxo)
|
||||||
|
* [Mtv] updated extractor logic & more by [blackjack4494](https://github.com/blackjack4494)
|
||||||
|
* [ndr] support Daserste ndr by [blackjack4494](https://github.com/blackjack4494)
|
||||||
|
* [Netzkino] Only use video id to find metadata by [TobiX](https://github.com/TobiX)
|
||||||
|
* [newgrounds] fix: video download by [insaneracist](https://github.com/insaneracist)
|
||||||
|
* [nitter] Add new extractor by [B0pol](https://github.com/B0pol)
|
||||||
|
* [soundcloud] Resolve audio/x-wav by [tfvlrue](https://github.com/tfvlrue)
|
||||||
|
* [soundcloud] sets pattern and tests by [blackjack4494](https://github.com/blackjack4494)
|
||||||
|
* [SouthparkDE/MTV] another mgid extraction (mtv_base) feed url updated by [blackjack4494](https://github.com/blackjack4494)
|
||||||
|
* [StoryFire] Add new extractor by [sgstair](https://github.com/sgstair)
|
||||||
|
* [twitch] by [geauxlo](https://github.com/geauxlo)
|
||||||
|
* [videa] Adapt to updates by [adrianheine](https://github.com/adrianheine)
|
||||||
|
* [Viki] subtitles, formats by [blackjack4494](https://github.com/blackjack4494)
|
||||||
|
* [vlive] fix extractor for revamped website by [exwm](https://github.com/exwm)
|
||||||
|
* [xtube] fix extractor by [insaneracist](https://github.com/insaneracist)
|
||||||
|
* [youtube] Convert subs when download is skipped by [blackjack4494](https://github.com/blackjack4494)
|
||||||
|
* [youtube] Fix age gate detection by [random-nick](https://github.com/random-nick)
|
||||||
|
* [youtube] fix yt-only playback when age restricted/gated - requires cookies by [blackjack4494](https://github.com/blackjack4494)
|
||||||
|
* [youtube] fix: extract artist metadata from ytInitialData by [insaneracist](https://github.com/insaneracist)
|
||||||
|
* [youtube] fix: extract mix playlist ids from ytInitialData by [insaneracist](https://github.com/insaneracist)
|
||||||
|
* [youtube] fix: mix playlist title by [insaneracist](https://github.com/insaneracist)
|
||||||
|
* [youtube] fix: Youtube Music playlists by [insaneracist](https://github.com/insaneracist)
|
||||||
|
* [Youtube] Fixed problem with new youtube player by [peet1993](https://github.com/peet1993)
|
||||||
|
* [zoom] Fix url parsing for url's containing /share/ and dots by [Romern](https://github.com/Romern)
|
||||||
|
* [zoom] new extractor by [insaneracist](https://github.com/insaneracist)
|
||||||
|
* abc by [adrianheine](https://github.com/adrianheine)
|
||||||
|
* Added Comcast_SSO fix by [merval](https://github.com/merval)
|
||||||
|
* Added DRM logic to brightcove by [merval](https://github.com/merval)
|
||||||
|
* Added regex for ABC.com site. by [kucksdorfs](https://github.com/kucksdorfs)
|
||||||
|
* alura by [hugohaa](https://github.com/hugohaa)
|
||||||
|
* Arbitrary merges by [fstirlitz](https://github.com/fstirlitz)
|
||||||
|
* ard.py_add_playlist_support by [martin54](https://github.com/martin54)
|
||||||
|
* Bugfix/youtube/chapters fix extractor by [gschizas](https://github.com/gschizas)
|
||||||
|
* bugfix_youtube_like_extraction by [RedpointsBots](https://github.com/RedpointsBots)
|
||||||
|
* Create build workflow by [blackjack4494](https://github.com/blackjack4494)
|
||||||
|
* deezer by [LucBerge](https://github.com/LucBerge)
|
||||||
|
* Detect embedded bitchute videos by [pukkandan](https://github.com/pukkandan)
|
||||||
|
* Don't install tests by [l29ah](https://github.com/l29ah)
|
||||||
|
* Don't try to embed/convert json subtitles generated by [youtube](https://github.com/youtube) livechat by [pukkandan](https://github.com/pukkandan)
|
||||||
|
* Doodstream by [sxvghd](https://github.com/sxvghd)
|
||||||
|
* duboku by [lkho](https://github.com/lkho)
|
||||||
|
* elonet by [tpikonen](https://github.com/tpikonen)
|
||||||
|
* ext/remuxe-video by [Zocker1999NET](https://github.com/Zocker1999NET)
|
||||||
|
* fall-back to the old way to fetch subtitles, if needed by [RobinD42](https://github.com/RobinD42)
|
||||||
|
* feature_subscriber_count by [RedpointsBots](https://github.com/RedpointsBots)
|
||||||
|
* Fix external downloader when there is no http_header by [pukkandan](https://github.com/pukkandan)
|
||||||
|
* Fix issue triggered by [tubeup](https://github.com/tubeup) by [nsapa](https://github.com/nsapa)
|
||||||
|
* Fix YoutubePlaylistsIE by [ZenulAbidin](https://github.com/ZenulAbidin)
|
||||||
|
* fix-mitele' by [DjMoren](https://github.com/DjMoren)
|
||||||
|
* fix/google-drive-cookie-issue by [legraphista](https://github.com/legraphista)
|
||||||
|
* fix_tiktok by [mervel-mervel](https://github.com/mervel-mervel)
|
||||||
|
* Fixed problem with JS player URL by [peet1993](https://github.com/peet1993)
|
||||||
|
* fixYTSearch by [xarantolus](https://github.com/xarantolus)
|
||||||
|
* FliegendeWurst-3sat-zdf-merger-bugfix-feature
|
||||||
|
* gilou-bandcamp_update
|
||||||
|
* implement ThisVid extractor by [rigstot](https://github.com/rigstot)
|
||||||
|
* JensTimmerman-patch-1 by [JensTimmerman](https://github.com/JensTimmerman)
|
||||||
|
* Keep download archive in memory for better performance by [jbruchon](https://github.com/jbruchon)
|
||||||
|
* la7-fix by [iamleot](https://github.com/iamleot)
|
||||||
|
* magenta by [adrianheine](https://github.com/adrianheine)
|
||||||
|
* Merge 26564 from [adrianheine](https://github.com/adrianheine)
|
||||||
|
* Merge code from [ddland](https://github.com/ddland)
|
||||||
|
* Merge code from [nixxo](https://github.com/nixxo)
|
||||||
|
* Merge code from [ssaqua](https://github.com/ssaqua)
|
||||||
|
* Merge code from [zubearc](https://github.com/zubearc)
|
||||||
|
* mkvthumbnail by [MrDoritos](https://github.com/MrDoritos)
|
||||||
|
* myvideo_ge by [fonkap](https://github.com/fonkap)
|
||||||
|
* naver by [SeonjaeHyeon](https://github.com/SeonjaeHyeon)
|
||||||
|
* ondemandkorea by [julien-hadleyjack](https://github.com/julien-hadleyjack)
|
||||||
|
* rai-update by [iamleot](https://github.com/iamleot)
|
||||||
|
* RFC: youtube: Polymer UI and JSON endpoints for playlists by [wlritchi](https://github.com/wlritchi)
|
||||||
|
* rutv by [adrianheine](https://github.com/adrianheine)
|
||||||
|
* Sc extractor web auth by [blackjack4494](https://github.com/blackjack4494)
|
||||||
|
* Switch from binary search tree to Python sets by [jbruchon](https://github.com/jbruchon)
|
||||||
|
* tiktok by [skyme5](https://github.com/skyme5)
|
||||||
|
* tvnow by [TinyToweringTree](https://github.com/TinyToweringTree)
|
||||||
|
* twitch-fix by [lel-amri](https://github.com/lel-amri)
|
||||||
|
* Twitter shortener by [blackjack4494](https://github.com/blackjack4494)
|
||||||
|
* Update README.md by [JensTimmerman](https://github.com/JensTimmerman)
|
||||||
|
* Update to reflect website changes. by [amigatomte](https://github.com/amigatomte)
|
||||||
|
* use webarchive to fix a dead link in README by [B0pol](https://github.com/B0pol)
|
||||||
|
* Viki the second by [blackjack4494](https://github.com/blackjack4494)
|
||||||
|
* wdr-subtitles by [mrtnmtth](https://github.com/mrtnmtth)
|
||||||
|
* Webpfix by [alexmerkel](https://github.com/alexmerkel)
|
||||||
|
* Youtube live chat by [siikamiika](https://github.com/siikamiika)
|
||||||
|
|||||||
@@ -15,9 +15,11 @@ pypi-files: AUTHORS Changelog.md LICENSE README.md README.txt supportedsites com
|
|||||||
clean-test:
|
clean-test:
|
||||||
rm -rf *.3gp *.annotations.xml *.ape *.avi *.description *.dump *.flac *.flv *.frag *.frag.aria2 *.frag.urls \
|
rm -rf *.3gp *.annotations.xml *.ape *.avi *.description *.dump *.flac *.flv *.frag *.frag.aria2 *.frag.urls \
|
||||||
*.info.json *.jpeg *.jpg *.live_chat.json *.m4a *.m4v *.mkv *.mp3 *.mp4 *.ogg *.opus *.part* *.png *.sbv *.srt \
|
*.info.json *.jpeg *.jpg *.live_chat.json *.m4a *.m4v *.mkv *.mp3 *.mp4 *.ogg *.opus *.part* *.png *.sbv *.srt \
|
||||||
*.swf *.swp *.ttml *.vtt *.wav *.webm *.webp *.ytdl test/testdata/player-*.js
|
*.swf *.swp *.ttml *.vtt *.wav *.webm *.webp *.mhtml *.mov *.unknown_video *.desktop *.url *.webloc *.ytdl \
|
||||||
|
test/testdata/player-*.js tmp/
|
||||||
clean-dist:
|
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
|
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
|
||||||
clean-cache:
|
clean-cache:
|
||||||
find . -name "*.pyc" -o -name "*.class" -delete
|
find . -name "*.pyc" -o -name "*.class" -delete
|
||||||
|
|
||||||
@@ -31,7 +33,6 @@ DESTDIR ?= .
|
|||||||
BINDIR ?= $(PREFIX)/bin
|
BINDIR ?= $(PREFIX)/bin
|
||||||
MANDIR ?= $(PREFIX)/man
|
MANDIR ?= $(PREFIX)/man
|
||||||
SHAREDIR ?= $(PREFIX)/share
|
SHAREDIR ?= $(PREFIX)/share
|
||||||
# make_supportedsites.py doesnot work correctly in python2
|
|
||||||
PYTHON ?= /usr/bin/env python3
|
PYTHON ?= /usr/bin/env python3
|
||||||
|
|
||||||
# set SYSCONFDIR to /etc if PREFIX=/usr or PREFIX=/usr/local
|
# set SYSCONFDIR to /etc if PREFIX=/usr or PREFIX=/usr/local
|
||||||
|
|||||||
@@ -64,14 +64,13 @@ yt-dlp is a [youtube-dl](https://github.com/ytdl-org/youtube-dl) fork based on t
|
|||||||
|
|
||||||
|
|
||||||
# NEW FEATURES
|
# NEW FEATURES
|
||||||
The major new features from the latest release of [blackjack4494/yt-dlc](https://github.com/blackjack4494/yt-dlc) are:
|
|
||||||
|
* Based on **youtube-dl 2021.06.06 [commit/379f52a](https://github.com/ytdl-org/youtube-dl/commit/379f52a4954013767219d25099cce9e0f9401961)** and **youtube-dlc 2020.11.11-3 [commit/98e248f](https://github.com/blackjack4494/yt-dlc/commit/98e248faa49e69d795abc60f7cdefcf91e2612aa)**: 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))
|
* **[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))
|
||||||
|
|
||||||
* **Merged with youtube-dl [commit/379f52a](https://github.com/ytdl-org/youtube-dl/commit/379f52a4954013767219d25099cce9e0f9401961)**: (v2021.06.06) You get all the latest features and patches of [youtube-dl](https://github.com/ytdl-org/youtube-dl) in addition to all the features of [youtube-dlc](https://github.com/blackjack4494/yt-dlc)
|
|
||||||
|
|
||||||
* **Merged with animelover1984/youtube-dl**: You get most of the features and improvements from [animelover1984/youtube-dl](https://github.com/animelover1984/youtube-dl) including `--write-comments`, `BiliBiliSearch`, `BilibiliChannel`, Embedding thumbnail in mp4/ogg/opus, playlist infojson etc. Note that the NicoNico improvements are not available. See [#31](https://github.com/yt-dlp/yt-dlp/pull/31) for details.
|
* **Merged with animelover1984/youtube-dl**: You get most of the features and improvements from [animelover1984/youtube-dl](https://github.com/animelover1984/youtube-dl) including `--write-comments`, `BiliBiliSearch`, `BilibiliChannel`, Embedding thumbnail in mp4/ogg/opus, playlist infojson etc. Note that the NicoNico improvements are not available. See [#31](https://github.com/yt-dlp/yt-dlp/pull/31) for details.
|
||||||
|
|
||||||
* **Youtube improvements**:
|
* **Youtube improvements**:
|
||||||
@@ -92,11 +91,7 @@ The major new features from the latest release of [blackjack4494/yt-dlc](https:/
|
|||||||
|
|
||||||
* **Aria2c with HLS/DASH**: You can use `aria2c` as the external downloader for DASH(mpd) and HLS(m3u8) formats
|
* **Aria2c with HLS/DASH**: You can use `aria2c` as the external downloader for DASH(mpd) and HLS(m3u8) formats
|
||||||
|
|
||||||
* **New extractors**: 17live, 3speak, amazonstore, animelab, audius, bandcampmusic, bannedvideo, biliintl, bitwave.tv, blackboardcollaborate, cam4, cgtn, chingari, ciscowebex, damtomo, discoveryplus.in, douyin, epicon, euscreen, fancode, filmmodu, gab, gedi, gettr, gopro, gotostage, gronkh, koo, manototv, mediaite, mediaklikk, mediasetshow, mediathek, microsoftstream, mildom, mirrativ, mlsscoccer, mtv.it, musescore, mxplayershow, n1, nebula, nfhsnetwork, novaplay, nzherald, olympics replay, on24, openrec, parlview-AU, peloton, planetmarathi, pluto.tv, polsatgo, polskieradio, pornflip, projectveritas, radiko, radiokapital, radlive, raiplayradio, rcs, rctiplus, saitosan, sciencechannel, shemaroome, skynews-AU, skynews-story, sovietscloset, startv, streamanity, telemundo, theta, theta, tokentube, tv2huseries, ukcolumn, utreon, veo, vidiolive, vidiopremier, voicy, vupload, whowatch, wim.tv, wppilot, youtube webarchive, zee5, zen.yandex
|
* **New and fixed extractors**: Many new extractors have been added and a lot of exisiting ones have been fixed. See the [changelog](Changelog.md) or the [list of supported sites](supportedsites.md)
|
||||||
|
|
||||||
* **New playlist extractors**: bilibili categories, eroprofile albums, hotstar series, hungama albums, newgrounds user, niconico search/users, paramountplus series, patreon user, peertube playlist/channels, roosterteeth series, sonyliv series, tiktok user, trovo channels, voot series
|
|
||||||
|
|
||||||
* **Fixed/improved extractors**: 7plus, 9now, afreecatv, akamai, aljazeera, amcnetworks, animalplanet, archive.org, arte, atv, bbc, bilibili, bitchute, bravotv, camtube, cbc, cda, ceskatelevize, chingari, comedycentral, coub, crackle, crunchyroll, curiositystream, diynetwork, dw, eroprofile, facebook, francetv, funimation, globo, hearthisatie, hidive, hotstar, hungama, imdb, ina, instagram, iprima, itv, iwara, kakao, la7, linkedinlearning, linuxacadamy, mediaset, mediasite, motherless, mxplayer, nbcolympics, ndr, newgrounds, niconico, nitter, nova, nrk, nuvid, oreilly, paramountplus, parliamentlive, patreon, pbs, peertube, plutotv, polskieradio, pornhub, reddit, reddit, redtube, rmcdecouverte, roosterteeth, rtp, rumble, saml verizon login, skyit, sonyliv, soundcloud, southparkde, spankbang, spreaker, streamable, tagesschau, tbs, tennistv, tenplay, tiktok, tubi, tv2, tv2hu, tv5mondeplus, tvp, twitcasting, vh1, viafree, videa, vidio, vidme, viewlift, viki, vimeo, viu, vk, vlive, vrt, wakanim, xhamster, yahoo
|
|
||||||
|
|
||||||
* **New MSOs**: Philo, Spectrum, SlingTV, Cablevision, RCN
|
* **New MSOs**: Philo, Spectrum, SlingTV, Cablevision, RCN
|
||||||
|
|
||||||
@@ -108,22 +103,16 @@ The major new features from the latest release of [blackjack4494/yt-dlc](https:/
|
|||||||
|
|
||||||
* **Output template improvements**: Output templates can now have date-time formatting, numeric offsets, object traversal etc. See [output template](#output-template) for details. Even more advanced operations can also be done with the help of `--parse-metadata` and `--replace-in-metadata`
|
* **Output template improvements**: Output templates can now have date-time formatting, numeric offsets, object traversal etc. See [output template](#output-template) for details. Even more advanced operations can also be done with the help of `--parse-metadata` and `--replace-in-metadata`
|
||||||
|
|
||||||
* **Other new options**: `--print`, `--sleep-requests`, `--convert-thumbnails`, `--write-link`, `--force-download-archive`, `--force-overwrites`, `--break-on-reject` etc
|
* **Other new options**: Many new options have been added such as `--print`, `--wait-for-video`, `--sleep-requests`, `--convert-thumbnails`, `--write-link`, `--force-download-archive`, `--force-overwrites`, `--break-on-reject` etc
|
||||||
|
|
||||||
* **Improvements**: Regex and other operators in `--match-filter`, multiple `--postprocessor-args` and `--downloader-args`, faster archive checking, more [format selection options](#format-selection) etc
|
* **Improvements**: Regex and other operators in `--match-filter`, multiple `--postprocessor-args` and `--downloader-args`, faster archive checking, more [format selection options](#format-selection), merge multi-video/audio etc
|
||||||
|
|
||||||
* **Plugins**: Extractors and PostProcessors can be loaded from an external file. See [plugins](#plugins) for details
|
* **Plugins**: Extractors and PostProcessors can be loaded from an external file. See [plugins](#plugins) for details
|
||||||
|
|
||||||
* **Self-updater**: The releases can be updated using `yt-dlp -U`
|
* **Self-updater**: The releases can be updated using `yt-dlp -U`
|
||||||
|
|
||||||
|
|
||||||
See [changelog](Changelog.md) or [commits](https://github.com/yt-dlp/yt-dlp/commits) for the full list of changes
|
See [changelog](Changelog.md) or [commits](https://github.com/yt-dlp/yt-dlp/commits) for the full list of changes
|
||||||
|
|
||||||
|
|
||||||
**PS**: Some of these changes are already in youtube-dlc, but are still unreleased. See [this](Changelog.md#unreleased-changes-in-blackjack4494yt-dlc) for details
|
|
||||||
|
|
||||||
If you are coming from [youtube-dl](https://github.com/ytdl-org/youtube-dl), the amount of changes are very large. Compare [options](#options) and [supported sites](supportedsites.md) with youtube-dl's to get an idea of the massive number of features/patches [youtube-dlc](https://github.com/blackjack4494/yt-dlc) has accumulated.
|
|
||||||
|
|
||||||
### Differences in default behavior
|
### Differences in default behavior
|
||||||
|
|
||||||
Some of yt-dlp's default options are different from that of youtube-dl and youtube-dlc:
|
Some of yt-dlp's default options are different from that of youtube-dl and youtube-dlc:
|
||||||
@@ -136,7 +125,8 @@ Some of yt-dlp's default options are different from that of youtube-dl and youtu
|
|||||||
* Unlike youtube-dlc, yt-dlp does not allow merging multiple audio/video streams into one file by default (since this conflicts with the use of `-f bv*+ba`). If needed, this feature must be enabled using `--audio-multistreams` and `--video-multistreams`. You can also use `--compat-options multistreams` to enable both
|
* Unlike youtube-dlc, yt-dlp does not allow merging multiple audio/video streams into one file by default (since this conflicts with the use of `-f bv*+ba`). If needed, this feature must be enabled using `--audio-multistreams` and `--video-multistreams`. You can also use `--compat-options multistreams` to enable both
|
||||||
* `--ignore-errors` is enabled by default. Use `--abort-on-error` or `--compat-options abort-on-error` to abort on errors instead
|
* `--ignore-errors` is enabled by default. Use `--abort-on-error` or `--compat-options abort-on-error` to abort on errors instead
|
||||||
* When writing metadata files such as thumbnails, description or infojson, the same information (if available) is also written for playlists. Use `--no-write-playlist-metafiles` or `--compat-options no-playlist-metafiles` to not write these files
|
* When writing metadata files such as thumbnails, description or infojson, the same information (if available) is also written for playlists. Use `--no-write-playlist-metafiles` or `--compat-options no-playlist-metafiles` to not write these files
|
||||||
* `--add-metadata` attaches the `infojson` to `mkv` files in addition to writing the metadata when used with `--write-infojson`. Use `--compat-options no-attach-info-json` to revert this
|
* `--add-metadata` attaches the `infojson` to `mkv` files in addition to writing the metadata when used with `--write-info-json`. Use `--no-embed-info-json` or `--compat-options no-attach-info-json` to revert this
|
||||||
|
* Some metadata are embedded into different fields when using `--add-metadata` as compared to youtube-dl. Most notably, `comment` field contains the `webpage_url` and `synopsis` contains the `description`. You can [use `--parse-metadata`](https://github.com/yt-dlp/yt-dlp#modifying-metadata) to modify this to your liking or use `--compat-options embed-metadata` to revert this
|
||||||
* `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
|
* `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
|
* The output of `-F` is listed in a new format. Use `--compat-options list-formats` to revert this
|
||||||
* All *experiences* of a funimation episode are considered as a single video. This behavior breaks existing archives. Use `--compat-options seperate-video-versions` to extract information from only the default player
|
* All *experiences* of a funimation episode are considered as a single video. This behavior breaks existing archives. Use `--compat-options seperate-video-versions` to extract information from only the default player
|
||||||
@@ -195,7 +185,7 @@ 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:
|
If you want to be on the cutting edge, you can also install the master branch with:
|
||||||
```
|
```
|
||||||
python3 -m pip3 install --force-reinstall https://github.com/yt-dlp/yt-dlp/archive/master.zip
|
python3 -m pip install --force-reinstall https://github.com/yt-dlp/yt-dlp/archive/master.zip
|
||||||
```
|
```
|
||||||
|
|
||||||
Note that on some systems, you may need to use `py` or `python` instead of `python3`
|
Note that on some systems, you may need to use `py` or `python` instead of `python3`
|
||||||
@@ -287,7 +277,7 @@ Then simply run `make`. You can also run `make yt-dlp` instead to compile only t
|
|||||||
|
|
||||||
**Note**: In either platform, `devscripts/update-version.py` can be used to automatically update the version number
|
**Note**: In either platform, `devscripts/update-version.py` can be used to automatically update the version number
|
||||||
|
|
||||||
You can also fork the project on github and push it to a release branch in your fork for the [build workflow](https://github.com/yt-dlp/yt-dlp/blob/master/.github/workflows/build.yml) to automatically make a release for you
|
You can also fork the project on github and run your fork's [build workflow](.github/workflows/build.yml) to automatically build a release
|
||||||
|
|
||||||
# USAGE AND OPTIONS
|
# USAGE AND OPTIONS
|
||||||
|
|
||||||
@@ -339,6 +329,10 @@ You can also fork the project on github and push it to a release branch in your
|
|||||||
--flat-playlist Do not extract the videos of a playlist,
|
--flat-playlist Do not extract the videos of a playlist,
|
||||||
only list them
|
only list them
|
||||||
--no-flat-playlist Extract the videos of a playlist
|
--no-flat-playlist Extract the videos of a playlist
|
||||||
|
--wait-for-video MIN[-MAX] Wait for scheduled streams to become
|
||||||
|
available. Pass the minimum number of
|
||||||
|
seconds (or range) to wait between retries
|
||||||
|
--no-wait-for-video Do not wait for scheduled streams (default)
|
||||||
--mark-watched Mark videos watched (even with --simulate).
|
--mark-watched Mark videos watched (even with --simulate).
|
||||||
Currently only supported for YouTube
|
Currently only supported for YouTube
|
||||||
--no-mark-watched Do not mark videos watched (default)
|
--no-mark-watched Do not mark videos watched (default)
|
||||||
@@ -388,7 +382,6 @@ You can also fork the project on github and push it to a release branch in your
|
|||||||
specify range: "--playlist-items
|
specify range: "--playlist-items
|
||||||
1-3,7,10-13", it will download the videos
|
1-3,7,10-13", it will download the videos
|
||||||
at index 1, 2, 3, 7, 10, 11, 12 and 13
|
at index 1, 2, 3, 7, 10, 11, 12 and 13
|
||||||
--max-downloads NUMBER Abort after downloading NUMBER files
|
|
||||||
--min-filesize SIZE Do not download any videos smaller than
|
--min-filesize SIZE Do not download any videos smaller than
|
||||||
SIZE (e.g. 50k or 44.6m)
|
SIZE (e.g. 50k or 44.6m)
|
||||||
--max-filesize SIZE Do not download any videos larger than SIZE
|
--max-filesize SIZE Do not download any videos larger than SIZE
|
||||||
@@ -430,13 +423,18 @@ You can also fork the project on github and push it to a release branch in your
|
|||||||
--download-archive FILE Download only videos not listed in the
|
--download-archive FILE Download only videos not listed in the
|
||||||
archive file. Record the IDs of all
|
archive file. Record the IDs of all
|
||||||
downloaded videos in it
|
downloaded videos in it
|
||||||
|
--no-download-archive Do not use archive file (default)
|
||||||
|
--max-downloads NUMBER Abort after downloading NUMBER files
|
||||||
--break-on-existing Stop the download process when encountering
|
--break-on-existing Stop the download process when encountering
|
||||||
a file that is in the archive
|
a file that is in the archive
|
||||||
--break-on-reject Stop the download process when encountering
|
--break-on-reject Stop the download process when encountering
|
||||||
a file that has been filtered out
|
a file that has been filtered out
|
||||||
|
--break-per-input Make --break-on-existing and --break-on-
|
||||||
|
reject act only on the current input URL
|
||||||
|
--no-break-per-input --break-on-existing and --break-on-reject
|
||||||
|
terminates the entire download queue
|
||||||
--skip-playlist-after-errors N Number of allowed failures until the rest
|
--skip-playlist-after-errors N Number of allowed failures until the rest
|
||||||
of the playlist is skipped
|
of the playlist is skipped
|
||||||
--no-download-archive Do not use archive file (default)
|
|
||||||
|
|
||||||
## Download Options:
|
## Download Options:
|
||||||
-N, --concurrent-fragments N Number of fragments of a dash/hlsnative
|
-N, --concurrent-fragments N Number of fragments of a dash/hlsnative
|
||||||
@@ -587,8 +585,8 @@ You can also fork the project on github and push it to a release branch in your
|
|||||||
--load-info-json FILE JSON file containing the video information
|
--load-info-json FILE JSON file containing the video information
|
||||||
(created with the "--write-info-json"
|
(created with the "--write-info-json"
|
||||||
option)
|
option)
|
||||||
--cookies FILE File to read cookies from and dump cookie
|
--cookies FILE Netscape formatted file to read cookies
|
||||||
jar in
|
from and dump cookie jar in
|
||||||
--no-cookies Do not read/dump cookies from/to file
|
--no-cookies Do not read/dump cookies from/to file
|
||||||
(default)
|
(default)
|
||||||
--cookies-from-browser BROWSER[:PROFILE]
|
--cookies-from-browser BROWSER[:PROFILE]
|
||||||
@@ -792,7 +790,7 @@ You can also fork the project on github and push it to a release branch in your
|
|||||||
--audio-format FORMAT Specify audio format to convert the audio
|
--audio-format FORMAT Specify audio format to convert the audio
|
||||||
to when -x is used. Currently supported
|
to when -x is used. Currently supported
|
||||||
formats are: best (default) or one of
|
formats are: best (default) or one of
|
||||||
best|aac|flac|mp3|m4a|opus|vorbis|wav
|
best|aac|flac|mp3|m4a|opus|vorbis|wav|alac
|
||||||
--audio-quality QUALITY Specify ffmpeg audio quality, insert a
|
--audio-quality QUALITY Specify ffmpeg audio quality, insert a
|
||||||
value between 0 (best) and 10 (worst) for
|
value between 0 (best) and 10 (worst) for
|
||||||
VBR or a specific bitrate like 128K
|
VBR or a specific bitrate like 128K
|
||||||
@@ -843,15 +841,20 @@ You can also fork the project on github and push it to a release branch in your
|
|||||||
--no-embed-subs Do not embed subtitles (default)
|
--no-embed-subs Do not embed subtitles (default)
|
||||||
--embed-thumbnail Embed thumbnail in the video as cover art
|
--embed-thumbnail Embed thumbnail in the video as cover art
|
||||||
--no-embed-thumbnail Do not embed thumbnail (default)
|
--no-embed-thumbnail Do not embed thumbnail (default)
|
||||||
--embed-metadata Embed metadata to the video file. Also adds
|
--embed-metadata Embed metadata to the video file. Also
|
||||||
chapters to file unless --no-add-chapters
|
embeds chapters/infojson if present unless
|
||||||
is used (Alias: --add-metadata)
|
--no-embed-chapters/--no-embed-info-json
|
||||||
|
are used (Alias: --add-metadata)
|
||||||
--no-embed-metadata Do not add metadata to file (default)
|
--no-embed-metadata Do not add metadata to file (default)
|
||||||
(Alias: --no-add-metadata)
|
(Alias: --no-add-metadata)
|
||||||
--embed-chapters Add chapter markers to the video file
|
--embed-chapters Add chapter markers to the video file
|
||||||
(Alias: --add-chapters)
|
(Alias: --add-chapters)
|
||||||
--no-embed-chapters Do not add chapter markers (default)
|
--no-embed-chapters Do not add chapter markers (default)
|
||||||
(Alias: --no-add-chapters)
|
(Alias: --no-add-chapters)
|
||||||
|
--embed-info-json Embed the infojson as an attachment to
|
||||||
|
mkv/mka video files
|
||||||
|
--no-embed-info-json Do not embed the infojson as an attachment
|
||||||
|
to the video file
|
||||||
--parse-metadata FROM:TO Parse additional metadata like title/artist
|
--parse-metadata FROM:TO Parse additional metadata like title/artist
|
||||||
from other fields; see "MODIFYING METADATA"
|
from other fields; see "MODIFYING METADATA"
|
||||||
for details
|
for details
|
||||||
@@ -1198,7 +1201,7 @@ To use percent literals in an output template use `%%`. To output to stdout use
|
|||||||
|
|
||||||
The current default template is `%(title)s [%(id)s].%(ext)s`.
|
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:
|
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
|
#### Output template and Windows batch files
|
||||||
|
|
||||||
@@ -1209,11 +1212,14 @@ If you are using an output template inside a Windows batch file then you must es
|
|||||||
Note that on Windows you need to use double quotes instead of single.
|
Note that on Windows you need to use double quotes instead of single.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
$ yt-dlp --get-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 --get-filename -o '%(title)s.%(ext)s' BaW_jenozKc
|
||||||
youtube-dl test video ''_ä↭𝕐.mp4 # All kinds of weird characters
|
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 --get-filename -o '%(title)s.%(ext)s' BaW_jenozKc --restrict-filenames
|
||||||
youtube-dl_test_video_.mp4 # A simple file name
|
youtube-dl_test_video_.webm # Restricted file name
|
||||||
|
|
||||||
# Download YouTube playlist videos in separate directory indexed by video order in a playlist
|
# Download YouTube playlist videos in separate directory indexed by video order in a playlist
|
||||||
$ yt-dlp -o '%(playlist)s/%(playlist_index)s - %(title)s.%(ext)s' https://www.youtube.com/playlist?list=PLwiyx1dc3P2JR9N8gQaQN_BCvlSlap7re
|
$ yt-dlp -o '%(playlist)s/%(playlist_index)s - %(title)s.%(ext)s' https://www.youtube.com/playlist?list=PLwiyx1dc3P2JR9N8gQaQN_BCvlSlap7re
|
||||||
@@ -1551,7 +1557,7 @@ The following extractors use this feature:
|
|||||||
|
|
||||||
#### youtube
|
#### youtube
|
||||||
* `skip`: `hls` or `dash` (or both) to skip download of the respective manifests
|
* `skip`: `hls` or `dash` (or both) to skip download of the respective manifests
|
||||||
* `player_client`: Clients to extract video data from. The main clients are `web`, `android`, `ios`, `mweb`. These also have `_music`, `_embedded`, `_agegate`, and `_creator` variants (Eg: `web_embedded`) (`mweb` has only `_agegate`). By default, `android,web` is used, but the agegate and creator variants are added as required for age-gated videos. Similarly the music variants are added for `music.youtube.com` urls. You can also use `all` to use all the clients
|
* `player_client`: Clients to extract video data from. The main clients are `web`, `android`, `ios`, `mweb`. These also have `_music`, `_embedded`, `_agegate`, and `_creator` variants (Eg: `web_embedded`) (`mweb` has only `_agegate`). By default, `android,web` is used, but the agegate and creator variants are added as required for age-gated videos. Similarly the music variants are added for `music.youtube.com` urls. You can also 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
|
* `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 (These formats don't download properly)
|
* `include_live_dash`: Include live dash formats (These formats don't download properly)
|
||||||
* `comment_sort`: `top` or `new` (default) - choose comment sorting mode (on YouTube's side)
|
* `comment_sort`: `top` or `new` (default) - choose comment sorting mode (on YouTube's side)
|
||||||
@@ -1600,14 +1606,14 @@ From a Python program, you can embed yt-dlp in a more powerful fashion, like thi
|
|||||||
```python
|
```python
|
||||||
from yt_dlp import YoutubeDL
|
from yt_dlp import YoutubeDL
|
||||||
|
|
||||||
ydl_opts = {}
|
ydl_opts = {'format': 'bestaudio'}
|
||||||
with YoutubeDL(ydl_opts) as ydl:
|
with YoutubeDL(ydl_opts) as ydl:
|
||||||
ydl.download(['https://www.youtube.com/watch?v=BaW_jenozKc'])
|
ydl.download(['https://www.youtube.com/watch?v=BaW_jenozKc'])
|
||||||
```
|
```
|
||||||
|
|
||||||
Most likely, you'll want to use various options. For a list of options available, have a look at [`yt_dlp/YoutubeDL.py`](yt_dlp/YoutubeDL.py#L154-L452).
|
Most likely, you'll want to use various options. For a list of options available, have a look at [`yt_dlp/YoutubeDL.py`](yt_dlp/YoutubeDL.py#L162).
|
||||||
|
|
||||||
Here's a more complete example of a program that outputs only errors (and a short message after the download is finished), converts the video to an mp3 file, implements a custom postprocessor and prints the final info_dict as json:
|
Here's a more complete example demonstrating various functionality:
|
||||||
|
|
||||||
```python
|
```python
|
||||||
import json
|
import json
|
||||||
@@ -1633,23 +1639,56 @@ class MyLogger:
|
|||||||
print(msg)
|
print(msg)
|
||||||
|
|
||||||
|
|
||||||
|
# ℹ️ See the docstring of yt_dlp.postprocessor.common.PostProcessor
|
||||||
class MyCustomPP(yt_dlp.postprocessor.PostProcessor):
|
class MyCustomPP(yt_dlp.postprocessor.PostProcessor):
|
||||||
|
# ℹ️ See docstring of yt_dlp.postprocessor.common.PostProcessor.run
|
||||||
def run(self, info):
|
def run(self, info):
|
||||||
self.to_screen('Doing stuff')
|
self.to_screen('Doing stuff')
|
||||||
return [], info
|
return [], info
|
||||||
|
|
||||||
|
|
||||||
|
# ℹ️ See "progress_hooks" in the docstring of yt_dlp.YoutubeDL
|
||||||
def my_hook(d):
|
def my_hook(d):
|
||||||
if d['status'] == 'finished':
|
if d['status'] == 'finished':
|
||||||
print('Done downloading, now converting ...')
|
print('Done downloading, now converting ...')
|
||||||
|
|
||||||
|
|
||||||
|
def format_selector(ctx):
|
||||||
|
""" Select the best video and the best audio that won't result in an mkv.
|
||||||
|
This is just an example and does not handle all cases """
|
||||||
|
|
||||||
|
# formats are already sorted worst to best
|
||||||
|
formats = ctx.get('formats')[::-1]
|
||||||
|
|
||||||
|
# acodec='none' means there is no audio
|
||||||
|
best_video = next(f for f in formats
|
||||||
|
if f['vcodec'] != 'none' and f['acodec'] == 'none')
|
||||||
|
|
||||||
|
# find compatible audio extension
|
||||||
|
audio_ext = {'mp4': 'm4a', 'webm': 'webm'}[best_video['ext']]
|
||||||
|
# vcodec='none' means there is no video
|
||||||
|
best_audio = next(f for f in formats if (
|
||||||
|
f['acodec'] != 'none' and f['vcodec'] == 'none' and f['ext'] == audio_ext))
|
||||||
|
|
||||||
|
yield {
|
||||||
|
# These are the minimum required fields for a merged format
|
||||||
|
'format_id': f'{best_video["format_id"]}+{best_audio["format_id"]}',
|
||||||
|
'ext': best_video['ext'],
|
||||||
|
'requested_formats': [best_video, best_audio],
|
||||||
|
# Must be + seperated list of protocols
|
||||||
|
'protocol': f'{best_video["protocol"]}+{best_audio["protocol"]}'
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# ℹ️ See docstring of yt_dlp.YoutubeDL for a description of the options
|
||||||
ydl_opts = {
|
ydl_opts = {
|
||||||
'format': 'bestaudio/best',
|
'format': format_selector,
|
||||||
'postprocessors': [{
|
'postprocessors': [{
|
||||||
'key': 'FFmpegExtractAudio',
|
# Embed metadata in video using ffmpeg.
|
||||||
'preferredcodec': 'mp3',
|
# ℹ️ See yt_dlp.postprocessor.FFmpegMetadataPP for the arguments it accepts
|
||||||
'preferredquality': '192',
|
'key': 'FFmpegMetadata',
|
||||||
|
'add_chapters': True,
|
||||||
|
'add_metadata': True,
|
||||||
}],
|
}],
|
||||||
'logger': MyLogger(),
|
'logger': MyLogger(),
|
||||||
'progress_hooks': [my_hook],
|
'progress_hooks': [my_hook],
|
||||||
@@ -1659,14 +1698,16 @@ ydl_opts = {
|
|||||||
# Add custom headers
|
# Add custom headers
|
||||||
yt_dlp.utils.std_headers.update({'Referer': 'https://www.google.com'})
|
yt_dlp.utils.std_headers.update({'Referer': 'https://www.google.com'})
|
||||||
|
|
||||||
|
# ℹ️ See the public functions in yt_dlp.YoutubeDL for for other available functions.
|
||||||
|
# Eg: "ydl.download", "ydl.download_with_info_file"
|
||||||
with yt_dlp.YoutubeDL(ydl_opts) as ydl:
|
with yt_dlp.YoutubeDL(ydl_opts) as ydl:
|
||||||
ydl.add_post_processor(MyCustomPP())
|
ydl.add_post_processor(MyCustomPP())
|
||||||
info = ydl.extract_info('https://www.youtube.com/watch?v=BaW_jenozKc')
|
info = ydl.extract_info('https://www.youtube.com/watch?v=BaW_jenozKc')
|
||||||
|
|
||||||
|
# ℹ️ ydl.sanitize_info makes the info json-serializable
|
||||||
print(json.dumps(ydl.sanitize_info(info)))
|
print(json.dumps(ydl.sanitize_info(info)))
|
||||||
```
|
```
|
||||||
|
|
||||||
See the public functions in [`yt_dlp/YoutubeDL.py`](yt_dlp/YoutubeDL.py) for other available functions. Eg: `ydl.download`, `ydl.download_with_info_file`
|
|
||||||
|
|
||||||
**Tip**: If you are porting your code from youtube-dl to yt-dlp, one important point to look out for is that we do not guarantee the return value of `YoutubeDL.extract_info` to be json serializable, or even be a dictionary. It will be dictionary-like, but if you want to ensure it is a serializable dictionary, pass it through `YoutubeDL.sanitize_info` as shown in the example above
|
**Tip**: If you are porting your code from youtube-dl to yt-dlp, one important point to look out for is that we do not guarantee the return value of `YoutubeDL.extract_info` to be json serializable, or even be a dictionary. It will be dictionary-like, but if you want to ensure it is a serializable dictionary, pass it through `YoutubeDL.sanitize_info` as shown in the example above
|
||||||
|
|
||||||
|
|
||||||
@@ -1741,7 +1782,7 @@ These are aliases that are no longer documented for various reasons
|
|||||||
--yes-overwrites --force-overwrites
|
--yes-overwrites --force-overwrites
|
||||||
|
|
||||||
#### Sponskrub Options
|
#### Sponskrub Options
|
||||||
Support for [SponSkrub](https://github.com/faissaloo/SponSkrub) has been deprecated in favor of `--sponsorblock`
|
Support for [SponSkrub](https://github.com/faissaloo/SponSkrub) has been deprecated in favor of the `--sponsorblock` options
|
||||||
|
|
||||||
--sponskrub --sponsorblock-mark all
|
--sponskrub --sponsorblock-mark all
|
||||||
--no-sponskrub --no-sponsorblock
|
--no-sponskrub --no-sponsorblock
|
||||||
|
|||||||
@@ -1,33 +1,42 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
from __future__ import unicode_literals
|
|
||||||
|
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
# import urllib.request
|
import sys
|
||||||
|
import subprocess
|
||||||
|
|
||||||
# response = urllib.request.urlopen('https://blackjack4494.github.io/youtube-dlc/update/LATEST_VERSION')
|
|
||||||
# old_version = response.read().decode('utf-8')
|
|
||||||
|
|
||||||
exec(compile(open('yt_dlp/version.py').read(), 'yt_dlp/version.py', 'exec'))
|
with open('yt_dlp/version.py', 'rt') as f:
|
||||||
|
exec(compile(f.read(), 'yt_dlp/version.py', 'exec'))
|
||||||
old_version = locals()['__version__']
|
old_version = locals()['__version__']
|
||||||
|
|
||||||
old_version_list = old_version.split(".", 4)
|
old_version_list = old_version.split('.')
|
||||||
|
|
||||||
old_ver = '.'.join(old_version_list[:3])
|
old_ver = '.'.join(old_version_list[:3])
|
||||||
old_rev = old_version_list[3] if len(old_version_list) > 3 else ''
|
old_rev = old_version_list[3] if len(old_version_list) > 3 else ''
|
||||||
|
|
||||||
ver = datetime.utcnow().strftime("%Y.%m.%d")
|
ver = datetime.utcnow().strftime("%Y.%m.%d")
|
||||||
rev = str(int(old_rev or 0) + 1) if old_ver == ver else ''
|
|
||||||
|
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 ''
|
||||||
|
|
||||||
VERSION = '.'.join((ver, rev)) if rev else ver
|
VERSION = '.'.join((ver, rev)) if rev else ver
|
||||||
# VERSION_LIST = [(int(v) for v in ver.split(".") + [rev or 0])]
|
|
||||||
|
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
|
||||||
|
|
||||||
|
VERSION_FILE = f'''
|
||||||
|
# Autogenerated by devscripts/update-version.py
|
||||||
|
|
||||||
|
__version__ = {VERSION!r}
|
||||||
|
|
||||||
|
RELEASE_GIT_HEAD = {GIT_HEAD!r}
|
||||||
|
'''.lstrip()
|
||||||
|
|
||||||
|
with open('yt_dlp/version.py', 'wt') as f:
|
||||||
|
f.write(VERSION_FILE)
|
||||||
|
|
||||||
print('::set-output name=ytdlp_version::' + VERSION)
|
print('::set-output name=ytdlp_version::' + VERSION)
|
||||||
|
print(f'\nVersion = {VERSION}, Git HEAD = {GIT_HEAD}')
|
||||||
file_version_py = open('yt_dlp/version.py', 'rt')
|
|
||||||
data = file_version_py.read()
|
|
||||||
data = data.replace(old_version, VERSION)
|
|
||||||
file_version_py.close()
|
|
||||||
|
|
||||||
file_version_py = open('yt_dlp/version.py', 'wt')
|
|
||||||
file_version_py.write(data)
|
|
||||||
file_version_py.close()
|
|
||||||
|
|||||||
+29
-7
@@ -141,6 +141,7 @@
|
|||||||
- **BlackboardCollaborate**
|
- **BlackboardCollaborate**
|
||||||
- **BleacherReport**
|
- **BleacherReport**
|
||||||
- **BleacherReportCMS**
|
- **BleacherReportCMS**
|
||||||
|
- **blogger.com**
|
||||||
- **Bloomberg**
|
- **Bloomberg**
|
||||||
- **BokeCC**
|
- **BokeCC**
|
||||||
- **BongaCams**
|
- **BongaCams**
|
||||||
@@ -150,6 +151,7 @@
|
|||||||
- **BR**: Bayerischer Rundfunk
|
- **BR**: Bayerischer Rundfunk
|
||||||
- **BravoTV**
|
- **BravoTV**
|
||||||
- **Break**
|
- **Break**
|
||||||
|
- **BreitBart**
|
||||||
- **brightcove:legacy**
|
- **brightcove:legacy**
|
||||||
- **brightcove:new**
|
- **brightcove:new**
|
||||||
- **BRMediathek**: Bayerischer Rundfunk Mediathek
|
- **BRMediathek**: Bayerischer Rundfunk Mediathek
|
||||||
@@ -158,11 +160,13 @@
|
|||||||
- **BusinessInsider**
|
- **BusinessInsider**
|
||||||
- **BuzzFeed**
|
- **BuzzFeed**
|
||||||
- **BYUtv**
|
- **BYUtv**
|
||||||
|
- **CableAV**
|
||||||
- **CAM4**
|
- **CAM4**
|
||||||
- **Camdemy**
|
- **Camdemy**
|
||||||
- **CamdemyFolder**
|
- **CamdemyFolder**
|
||||||
- **CamModels**
|
- **CamModels**
|
||||||
- **CamWithHer**
|
- **CamWithHer**
|
||||||
|
- **CanalAlpha**
|
||||||
- **canalc2.tv**
|
- **canalc2.tv**
|
||||||
- **Canalplus**: mycanal.fr and piwiplus.fr
|
- **Canalplus**: mycanal.fr and piwiplus.fr
|
||||||
- **Canvas**
|
- **Canvas**
|
||||||
@@ -222,6 +226,8 @@
|
|||||||
- **CONtv**
|
- **CONtv**
|
||||||
- **Corus**
|
- **Corus**
|
||||||
- **Coub**
|
- **Coub**
|
||||||
|
- **CozyTV**
|
||||||
|
- **cp24**
|
||||||
- **Cracked**
|
- **Cracked**
|
||||||
- **Crackle**
|
- **Crackle**
|
||||||
- **CrooksAndLiars**
|
- **CrooksAndLiars**
|
||||||
@@ -236,7 +242,8 @@
|
|||||||
- **cu.ntv.co.jp**: Nippon Television Network
|
- **cu.ntv.co.jp**: Nippon Television Network
|
||||||
- **CultureUnplugged**
|
- **CultureUnplugged**
|
||||||
- **curiositystream**
|
- **curiositystream**
|
||||||
- **curiositystream:collection**
|
- **curiositystream:collections**
|
||||||
|
- **curiositystream:series**
|
||||||
- **CWTV**
|
- **CWTV**
|
||||||
- **DagelijkseKost**: dagelijksekost.een.be
|
- **DagelijkseKost**: dagelijksekost.een.be
|
||||||
- **DailyMail**
|
- **DailyMail**
|
||||||
@@ -266,6 +273,7 @@
|
|||||||
- **DiscoveryPlus**
|
- **DiscoveryPlus**
|
||||||
- **DiscoveryPlusIndia**
|
- **DiscoveryPlusIndia**
|
||||||
- **DiscoveryPlusIndiaShow**
|
- **DiscoveryPlusIndiaShow**
|
||||||
|
- **DiscoveryPlusItalyShow**
|
||||||
- **DiscoveryVR**
|
- **DiscoveryVR**
|
||||||
- **Disney**
|
- **Disney**
|
||||||
- **DIYNetwork**
|
- **DIYNetwork**
|
||||||
@@ -315,6 +323,7 @@
|
|||||||
- **Escapist**
|
- **Escapist**
|
||||||
- **ESPN**
|
- **ESPN**
|
||||||
- **ESPNArticle**
|
- **ESPNArticle**
|
||||||
|
- **ESPNCricInfo**
|
||||||
- **EsriVideo**
|
- **EsriVideo**
|
||||||
- **Europa**
|
- **Europa**
|
||||||
- **EUScreen**
|
- **EUScreen**
|
||||||
@@ -448,7 +457,7 @@
|
|||||||
- **IndavideoEmbed**
|
- **IndavideoEmbed**
|
||||||
- **InfoQ**
|
- **InfoQ**
|
||||||
- **Instagram**
|
- **Instagram**
|
||||||
- **instagram:tag**: Instagram hashtag search
|
- **instagram:tag**: Instagram hashtag search URLs
|
||||||
- **instagram:user**: Instagram user profile
|
- **instagram:user**: Instagram user profile
|
||||||
- **InstagramIOS**: IOS instagram:// URL
|
- **InstagramIOS**: IOS instagram:// URL
|
||||||
- **Internazionale**
|
- **Internazionale**
|
||||||
@@ -524,6 +533,7 @@
|
|||||||
- **LineLive**
|
- **LineLive**
|
||||||
- **LineLiveChannel**
|
- **LineLiveChannel**
|
||||||
- **LineTV**
|
- **LineTV**
|
||||||
|
- **LinkedIn**
|
||||||
- **linkedin:learning**
|
- **linkedin:learning**
|
||||||
- **linkedin:learning:course**
|
- **linkedin:learning:course**
|
||||||
- **LinuxAcademy**
|
- **LinuxAcademy**
|
||||||
@@ -591,6 +601,7 @@
|
|||||||
- **mirrativ**
|
- **mirrativ**
|
||||||
- **mirrativ:user**
|
- **mirrativ:user**
|
||||||
- **MiTele**: mitele.es
|
- **MiTele**: mitele.es
|
||||||
|
- **mixch**
|
||||||
- **mixcloud**
|
- **mixcloud**
|
||||||
- **mixcloud:playlist**
|
- **mixcloud:playlist**
|
||||||
- **mixcloud:user**
|
- **mixcloud:user**
|
||||||
@@ -663,6 +674,7 @@
|
|||||||
- **ndr:embed:base**
|
- **ndr:embed:base**
|
||||||
- **NDTV**
|
- **NDTV**
|
||||||
- **Nebula**
|
- **Nebula**
|
||||||
|
- **nebula:collection**
|
||||||
- **NerdCubedFeed**
|
- **NerdCubedFeed**
|
||||||
- **netease:album**: 网易云音乐 - 专辑
|
- **netease:album**: 网易云音乐 - 专辑
|
||||||
- **netease:djradio**: 网易云音乐 - 电台
|
- **netease:djradio**: 网易云音乐 - 电台
|
||||||
@@ -696,8 +708,8 @@
|
|||||||
- **niconico**: ニコニコ動画
|
- **niconico**: ニコニコ動画
|
||||||
- **NiconicoPlaylist**
|
- **NiconicoPlaylist**
|
||||||
- **NiconicoUser**
|
- **NiconicoUser**
|
||||||
- **nicovideo:search**: Nico video searches; "nicosearch:" prefix
|
- **nicovideo:search**: Nico video search; "nicosearch:" prefix
|
||||||
- **nicovideo:search:date**: Nico video searches, newest first; "nicosearchdate:" prefix
|
- **nicovideo:search:date**: Nico video search, newest first; "nicosearchdate:" prefix
|
||||||
- **nicovideo:search_url**: Nico video search URLs
|
- **nicovideo:search_url**: Nico video search URLs
|
||||||
- **Nintendo**
|
- **Nintendo**
|
||||||
- **Nitter**
|
- **Nitter**
|
||||||
@@ -746,6 +758,7 @@
|
|||||||
- **OlympicsReplay**
|
- **OlympicsReplay**
|
||||||
- **on24**: ON24
|
- **on24**: ON24
|
||||||
- **OnDemandKorea**
|
- **OnDemandKorea**
|
||||||
|
- **OneFootball**
|
||||||
- **onet.pl**
|
- **onet.pl**
|
||||||
- **onet.tv**
|
- **onet.tv**
|
||||||
- **onet.tv:channel**
|
- **onet.tv:channel**
|
||||||
@@ -788,6 +801,7 @@
|
|||||||
- **PatreonUser**
|
- **PatreonUser**
|
||||||
- **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)
|
- **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**
|
- **PearVideo**
|
||||||
|
- **peer.tv**
|
||||||
- **PeerTube**
|
- **PeerTube**
|
||||||
- **PeerTube:Playlist**
|
- **PeerTube:Playlist**
|
||||||
- **peloton**
|
- **peloton**
|
||||||
@@ -873,6 +887,7 @@
|
|||||||
- **RadioJavan**
|
- **RadioJavan**
|
||||||
- **radiokapital**
|
- **radiokapital**
|
||||||
- **radiokapital:show**
|
- **radiokapital:show**
|
||||||
|
- **RadioZetPodcast**
|
||||||
- **radlive**
|
- **radlive**
|
||||||
- **radlive:channel**
|
- **radlive:channel**
|
||||||
- **radlive:season**
|
- **radlive:season**
|
||||||
@@ -897,7 +912,9 @@
|
|||||||
- **RedBullTV**
|
- **RedBullTV**
|
||||||
- **RedBullTVRrnContent**
|
- **RedBullTVRrnContent**
|
||||||
- **Reddit**
|
- **Reddit**
|
||||||
- **RedditR**
|
- **RedGifs**
|
||||||
|
- **RedGifsSearch**: Redgifs search
|
||||||
|
- **RedGifsUser**: Redgifs user
|
||||||
- **RedTube**
|
- **RedTube**
|
||||||
- **RegioTV**
|
- **RegioTV**
|
||||||
- **RENTV**
|
- **RENTV**
|
||||||
@@ -921,6 +938,7 @@
|
|||||||
- **rtl2:you**
|
- **rtl2:you**
|
||||||
- **rtl2:you:series**
|
- **rtl2:you:series**
|
||||||
- **RTP**
|
- **RTP**
|
||||||
|
- **RTRFM**
|
||||||
- **RTS**: RTS.ch
|
- **RTS**: RTS.ch
|
||||||
- **rtve.es:alacarta**: RTVE a la carta
|
- **rtve.es:alacarta**: RTVE a la carta
|
||||||
- **rtve.es:infantil**: RTVE infantil
|
- **rtve.es:infantil**: RTVE infantil
|
||||||
@@ -960,6 +978,7 @@
|
|||||||
- **SCTE**
|
- **SCTE**
|
||||||
- **SCTECourse**
|
- **SCTECourse**
|
||||||
- **Seeker**
|
- **Seeker**
|
||||||
|
- **SenateGov**
|
||||||
- **SenateISVP**
|
- **SenateISVP**
|
||||||
- **SendtoNews**
|
- **SendtoNews**
|
||||||
- **Servus**
|
- **Servus**
|
||||||
@@ -1038,8 +1057,10 @@
|
|||||||
- **Streamanity**
|
- **Streamanity**
|
||||||
- **streamcloud.eu**
|
- **streamcloud.eu**
|
||||||
- **StreamCZ**
|
- **StreamCZ**
|
||||||
|
- **StreamFF**
|
||||||
- **StreetVoice**
|
- **StreetVoice**
|
||||||
- **StretchInternet**
|
- **StretchInternet**
|
||||||
|
- **Stripchat**
|
||||||
- **stv:player**
|
- **stv:player**
|
||||||
- **SunPorno**
|
- **SunPorno**
|
||||||
- **sverigesradio:episode**
|
- **sverigesradio:episode**
|
||||||
@@ -1311,6 +1332,7 @@
|
|||||||
- **WeiboMobile**
|
- **WeiboMobile**
|
||||||
- **WeiqiTV**: WQTV
|
- **WeiqiTV**: WQTV
|
||||||
- **whowatch**
|
- **whowatch**
|
||||||
|
- **Willow**
|
||||||
- **WimTV**
|
- **WimTV**
|
||||||
- **Wistia**
|
- **Wistia**
|
||||||
- **WistiaPlaylist**
|
- **WistiaPlaylist**
|
||||||
@@ -1370,8 +1392,8 @@
|
|||||||
- **youtube:history**: Youtube watch history; ":ythis" keyword (requires cookies)
|
- **youtube:history**: Youtube watch history; ":ythis" keyword (requires cookies)
|
||||||
- **youtube:playlist**: YouTube playlists
|
- **youtube:playlist**: YouTube playlists
|
||||||
- **youtube:recommended**: YouTube recommended videos; ":ytrec" keyword
|
- **youtube:recommended**: YouTube recommended videos; ":ytrec" keyword
|
||||||
- **youtube:search**: YouTube searches; "ytsearch:" prefix
|
- **youtube:search**: YouTube search; "ytsearch:" prefix
|
||||||
- **youtube:search:date**: YouTube searches, newest videos first; "ytsearchdate:" prefix
|
- **youtube:search:date**: YouTube search, newest videos first; "ytsearchdate:" prefix
|
||||||
- **youtube:search_url**: YouTube search URLs with sorting and filter support
|
- **youtube:search_url**: YouTube search URLs with sorting and filter support
|
||||||
- **youtube:subscriptions**: YouTube subscriptions feed; ":ytsubs" keyword (requires cookies)
|
- **youtube:subscriptions**: YouTube subscriptions feed; ":ytsubs" keyword (requires cookies)
|
||||||
- **youtube:tab**: YouTube Tabs
|
- **youtube:tab**: YouTube Tabs
|
||||||
|
|||||||
@@ -137,7 +137,7 @@ class TestFormatSelection(unittest.TestCase):
|
|||||||
test('webm/mp4', '47')
|
test('webm/mp4', '47')
|
||||||
test('3gp/40/mp4', '35')
|
test('3gp/40/mp4', '35')
|
||||||
test('example-with-dashes', 'example-with-dashes')
|
test('example-with-dashes', 'example-with-dashes')
|
||||||
test('all', '35', 'example-with-dashes', '45', '47', '2') # Order doesn't actually matter for this
|
test('all', '2', '47', '45', 'example-with-dashes', '35')
|
||||||
test('mergeall', '2+47+45+example-with-dashes+35', multi=True)
|
test('mergeall', '2+47+45+example-with-dashes+35', multi=True)
|
||||||
|
|
||||||
def test_format_selection_audio(self):
|
def test_format_selection_audio(self):
|
||||||
@@ -520,7 +520,7 @@ class TestFormatSelection(unittest.TestCase):
|
|||||||
ydl = YDL({'format': 'all[width>=400][width<=600]'})
|
ydl = YDL({'format': 'all[width>=400][width<=600]'})
|
||||||
ydl.process_ie_result(info_dict)
|
ydl.process_ie_result(info_dict)
|
||||||
downloaded_ids = [info['format_id'] for info in ydl.downloaded_info_dicts]
|
downloaded_ids = [info['format_id'] for info in ydl.downloaded_info_dicts]
|
||||||
self.assertEqual(downloaded_ids, ['B', 'C', 'D'])
|
self.assertEqual(downloaded_ids, ['D', 'C', 'B'])
|
||||||
|
|
||||||
ydl = YDL({'format': 'best[height<40]'})
|
ydl = YDL({'format': 'best[height<40]'})
|
||||||
try:
|
try:
|
||||||
|
|||||||
+17
-1
@@ -10,6 +10,8 @@ sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
|||||||
from yt_dlp.aes import (
|
from yt_dlp.aes import (
|
||||||
aes_decrypt,
|
aes_decrypt,
|
||||||
aes_encrypt,
|
aes_encrypt,
|
||||||
|
aes_ecb_encrypt,
|
||||||
|
aes_ecb_decrypt,
|
||||||
aes_cbc_decrypt,
|
aes_cbc_decrypt,
|
||||||
aes_cbc_decrypt_bytes,
|
aes_cbc_decrypt_bytes,
|
||||||
aes_cbc_encrypt,
|
aes_cbc_encrypt,
|
||||||
@@ -17,7 +19,8 @@ from yt_dlp.aes import (
|
|||||||
aes_ctr_encrypt,
|
aes_ctr_encrypt,
|
||||||
aes_gcm_decrypt_and_verify,
|
aes_gcm_decrypt_and_verify,
|
||||||
aes_gcm_decrypt_and_verify_bytes,
|
aes_gcm_decrypt_and_verify_bytes,
|
||||||
aes_decrypt_text
|
aes_decrypt_text,
|
||||||
|
BLOCK_SIZE_BYTES,
|
||||||
)
|
)
|
||||||
from yt_dlp.compat import compat_pycrypto_AES
|
from yt_dlp.compat import compat_pycrypto_AES
|
||||||
from yt_dlp.utils import bytes_to_intlist, intlist_to_bytes
|
from yt_dlp.utils import bytes_to_intlist, intlist_to_bytes
|
||||||
@@ -94,6 +97,19 @@ class TestAES(unittest.TestCase):
|
|||||||
decrypted = (aes_decrypt_text(encrypted, password, 32))
|
decrypted = (aes_decrypt_text(encrypted, password, 32))
|
||||||
self.assertEqual(decrypted, self.secret_msg)
|
self.assertEqual(decrypted, self.secret_msg)
|
||||||
|
|
||||||
|
def test_ecb_encrypt(self):
|
||||||
|
data = bytes_to_intlist(self.secret_msg)
|
||||||
|
data += [0x08] * (BLOCK_SIZE_BYTES - len(data) % BLOCK_SIZE_BYTES)
|
||||||
|
encrypted = intlist_to_bytes(aes_ecb_encrypt(data, self.key, self.iv))
|
||||||
|
self.assertEqual(
|
||||||
|
encrypted,
|
||||||
|
b'\xaa\x86]\x81\x97>\x02\x92\x9d\x1bR[[L/u\xd3&\xd1(h\xde{\x81\x94\xba\x02\xae\xbd\xa6\xd0:')
|
||||||
|
|
||||||
|
def test_ecb_decrypt(self):
|
||||||
|
data = bytes_to_intlist(b'\xaa\x86]\x81\x97>\x02\x92\x9d\x1bR[[L/u\xd3&\xd1(h\xde{\x81\x94\xba\x02\xae\xbd\xa6\xd0:')
|
||||||
|
decrypted = intlist_to_bytes(aes_ecb_decrypt(data, self.key, self.iv))
|
||||||
|
self.assertEqual(decrypted.rstrip(b'\x08'), self.secret_msg)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
unittest.main()
|
unittest.main()
|
||||||
|
|||||||
@@ -38,7 +38,6 @@ class TestAllURLsMatching(unittest.TestCase):
|
|||||||
assertTab('https://www.youtube.com/AsapSCIENCE')
|
assertTab('https://www.youtube.com/AsapSCIENCE')
|
||||||
assertTab('https://www.youtube.com/embedded')
|
assertTab('https://www.youtube.com/embedded')
|
||||||
assertTab('https://www.youtube.com/playlist?list=UUBABnxM4Ar9ten8Mdjj1j0Q')
|
assertTab('https://www.youtube.com/playlist?list=UUBABnxM4Ar9ten8Mdjj1j0Q')
|
||||||
assertTab('https://www.youtube.com/course?list=ECUl4u3cNGP61MdtwGTqZA0MreSaDybji8')
|
|
||||||
assertTab('https://www.youtube.com/playlist?list=PLwP_SiAcdui0KVebT0mU9Apz359a4ubsC')
|
assertTab('https://www.youtube.com/playlist?list=PLwP_SiAcdui0KVebT0mU9Apz359a4ubsC')
|
||||||
assertTab('https://www.youtube.com/watch?v=AV6J6_AeFEQ&playnext=1&list=PL4023E734DA416012') # 668
|
assertTab('https://www.youtube.com/watch?v=AV6J6_AeFEQ&playnext=1&list=PL4023E734DA416012') # 668
|
||||||
self.assertFalse('youtube:playlist' in self.matching_ies('PLtS2H6bU1M'))
|
self.assertFalse('youtube:playlist' in self.matching_ies('PLtS2H6bU1M'))
|
||||||
|
|||||||
+44
-7
@@ -1222,12 +1222,49 @@ ffmpeg version 2.4.4 Copyright (c) 2000-2014 the FFmpeg ...'''), '2.4.4')
|
|||||||
def test_render_table(self):
|
def test_render_table(self):
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
render_table(
|
render_table(
|
||||||
['a', 'bcd'],
|
['a', 'empty', 'bcd'],
|
||||||
[[123, 4], [9999, 51]]),
|
[[123, '', 4], [9999, '', 51]]),
|
||||||
|
'a empty bcd\n'
|
||||||
|
'123 4\n'
|
||||||
|
'9999 51')
|
||||||
|
|
||||||
|
self.assertEqual(
|
||||||
|
render_table(
|
||||||
|
['a', 'empty', 'bcd'],
|
||||||
|
[[123, '', 4], [9999, '', 51]],
|
||||||
|
hide_empty=True),
|
||||||
'a bcd\n'
|
'a bcd\n'
|
||||||
'123 4\n'
|
'123 4\n'
|
||||||
'9999 51')
|
'9999 51')
|
||||||
|
|
||||||
|
self.assertEqual(
|
||||||
|
render_table(
|
||||||
|
['\ta', 'bcd'],
|
||||||
|
[['1\t23', 4], ['\t9999', 51]]),
|
||||||
|
' a bcd\n'
|
||||||
|
'1 23 4\n'
|
||||||
|
'9999 51')
|
||||||
|
|
||||||
|
self.assertEqual(
|
||||||
|
render_table(
|
||||||
|
['a', 'bcd'],
|
||||||
|
[[123, 4], [9999, 51]],
|
||||||
|
delim='-'),
|
||||||
|
'a bcd\n'
|
||||||
|
'--------\n'
|
||||||
|
'123 4\n'
|
||||||
|
'9999 51')
|
||||||
|
|
||||||
|
self.assertEqual(
|
||||||
|
render_table(
|
||||||
|
['a', 'bcd'],
|
||||||
|
[[123, 4], [9999, 51]],
|
||||||
|
delim='-', extra_gap=2),
|
||||||
|
'a bcd\n'
|
||||||
|
'----------\n'
|
||||||
|
'123 4\n'
|
||||||
|
'9999 51')
|
||||||
|
|
||||||
def test_match_str(self):
|
def test_match_str(self):
|
||||||
# Unary
|
# Unary
|
||||||
self.assertFalse(match_str('xy', {'x': 1200}))
|
self.assertFalse(match_str('xy', {'x': 1200}))
|
||||||
@@ -1620,9 +1657,9 @@ Line 1
|
|||||||
self.assertEqual(repr(LazyList(it)), repr(it))
|
self.assertEqual(repr(LazyList(it)), repr(it))
|
||||||
self.assertEqual(str(LazyList(it)), str(it))
|
self.assertEqual(str(LazyList(it)), str(it))
|
||||||
|
|
||||||
self.assertEqual(list(LazyList(it).reverse()), it[::-1])
|
self.assertEqual(list(LazyList(it, reverse=True)), it[::-1])
|
||||||
self.assertEqual(list(LazyList(it).reverse()[1:3:7]), it[::-1][1:3:7])
|
self.assertEqual(list(reversed(LazyList(it))[::-1]), it)
|
||||||
self.assertEqual(list(LazyList(it).reverse()[::-1]), it)
|
self.assertEqual(list(reversed(LazyList(it))[1:3:7]), it[::-1][1:3:7])
|
||||||
|
|
||||||
def test_LazyList_laziness(self):
|
def test_LazyList_laziness(self):
|
||||||
|
|
||||||
@@ -1635,13 +1672,13 @@ Line 1
|
|||||||
test(ll, 5, 5, range(6))
|
test(ll, 5, 5, range(6))
|
||||||
test(ll, -3, 7, range(10))
|
test(ll, -3, 7, range(10))
|
||||||
|
|
||||||
ll = LazyList(range(10)).reverse()
|
ll = LazyList(range(10), reverse=True)
|
||||||
test(ll, -1, 0, range(1))
|
test(ll, -1, 0, range(1))
|
||||||
test(ll, 3, 6, range(10))
|
test(ll, 3, 6, range(10))
|
||||||
|
|
||||||
ll = LazyList(itertools.count())
|
ll = LazyList(itertools.count())
|
||||||
test(ll, 10, 10, range(11))
|
test(ll, 10, 10, range(11))
|
||||||
ll.reverse()
|
ll = reversed(ll)
|
||||||
test(ll, -15, 14, range(15))
|
test(ll, -15, 14, range(15))
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+12
-10
@@ -26,29 +26,31 @@ class TestYoutubeLists(unittest.TestCase):
|
|||||||
def test_youtube_playlist_noplaylist(self):
|
def test_youtube_playlist_noplaylist(self):
|
||||||
dl = FakeYDL()
|
dl = FakeYDL()
|
||||||
dl.params['noplaylist'] = True
|
dl.params['noplaylist'] = True
|
||||||
ie = YoutubePlaylistIE(dl)
|
ie = YoutubeTabIE(dl)
|
||||||
result = ie.extract('https://www.youtube.com/watch?v=FXxLjLQi3Fg&list=PLwiyx1dc3P2JR9N8gQaQN_BCvlSlap7re')
|
result = ie.extract('https://www.youtube.com/watch?v=FXxLjLQi3Fg&list=PLwiyx1dc3P2JR9N8gQaQN_BCvlSlap7re')
|
||||||
self.assertEqual(result['_type'], 'url')
|
self.assertEqual(result['_type'], 'url')
|
||||||
self.assertEqual(YoutubeIE().extract_id(result['url']), 'FXxLjLQi3Fg')
|
self.assertEqual(YoutubeIE.extract_id(result['url']), 'FXxLjLQi3Fg')
|
||||||
|
|
||||||
def test_youtube_course(self):
|
def test_youtube_course(self):
|
||||||
|
print('Skipping: Course URLs no longer exists')
|
||||||
|
return
|
||||||
dl = FakeYDL()
|
dl = FakeYDL()
|
||||||
ie = YoutubePlaylistIE(dl)
|
ie = YoutubePlaylistIE(dl)
|
||||||
# TODO find a > 100 (paginating?) videos course
|
# TODO find a > 100 (paginating?) videos course
|
||||||
result = ie.extract('https://www.youtube.com/course?list=ECUl4u3cNGP61MdtwGTqZA0MreSaDybji8')
|
result = ie.extract('https://www.youtube.com/course?list=ECUl4u3cNGP61MdtwGTqZA0MreSaDybji8')
|
||||||
entries = list(result['entries'])
|
entries = list(result['entries'])
|
||||||
self.assertEqual(YoutubeIE().extract_id(entries[0]['url']), 'j9WZyLZCBzs')
|
self.assertEqual(YoutubeIE.extract_id(entries[0]['url']), 'j9WZyLZCBzs')
|
||||||
self.assertEqual(len(entries), 25)
|
self.assertEqual(len(entries), 25)
|
||||||
self.assertEqual(YoutubeIE().extract_id(entries[-1]['url']), 'rYefUsYuEp0')
|
self.assertEqual(YoutubeIE.extract_id(entries[-1]['url']), 'rYefUsYuEp0')
|
||||||
|
|
||||||
def test_youtube_mix(self):
|
def test_youtube_mix(self):
|
||||||
dl = FakeYDL()
|
dl = FakeYDL()
|
||||||
ie = YoutubePlaylistIE(dl)
|
ie = YoutubeTabIE(dl)
|
||||||
result = ie.extract('https://www.youtube.com/watch?v=W01L70IGBgE&index=2&list=RDOQpdSVF_k_w')
|
result = ie.extract('https://www.youtube.com/watch?v=tyITL_exICo&list=RDCLAK5uy_kLWIr9gv1XLlPbaDS965-Db4TrBoUTxQ8')
|
||||||
entries = result['entries']
|
entries = list(result['entries'])
|
||||||
self.assertTrue(len(entries) >= 50)
|
self.assertTrue(len(entries) >= 50)
|
||||||
original_video = entries[0]
|
original_video = entries[0]
|
||||||
self.assertEqual(original_video['id'], 'OQpdSVF_k_w')
|
self.assertEqual(original_video['id'], 'tyITL_exICo')
|
||||||
|
|
||||||
def test_youtube_toptracks(self):
|
def test_youtube_toptracks(self):
|
||||||
print('Skipping: The playlist page gives error 500')
|
print('Skipping: The playlist page gives error 500')
|
||||||
@@ -68,10 +70,10 @@ class TestYoutubeLists(unittest.TestCase):
|
|||||||
entries = list(result['entries'])
|
entries = list(result['entries'])
|
||||||
self.assertTrue(len(entries) == 1)
|
self.assertTrue(len(entries) == 1)
|
||||||
video = entries[0]
|
video = entries[0]
|
||||||
self.assertEqual(video['_type'], 'url_transparent')
|
self.assertEqual(video['_type'], 'url')
|
||||||
self.assertEqual(video['ie_key'], 'Youtube')
|
self.assertEqual(video['ie_key'], 'Youtube')
|
||||||
self.assertEqual(video['id'], 'BaW_jenozKc')
|
self.assertEqual(video['id'], 'BaW_jenozKc')
|
||||||
self.assertEqual(video['url'], 'BaW_jenozKc')
|
self.assertEqual(video['url'], 'https://www.youtube.com/watch?v=BaW_jenozKc')
|
||||||
self.assertEqual(video['title'], 'youtube-dl test video "\'/\\ä↭𝕐')
|
self.assertEqual(video['title'], 'youtube-dl test video "\'/\\ä↭𝕐')
|
||||||
self.assertEqual(video['duration'], 10)
|
self.assertEqual(video['duration'], 10)
|
||||||
self.assertEqual(video['uploader'], 'Philipp Hagemeister')
|
self.assertEqual(video['uploader'], 'Philipp Hagemeister')
|
||||||
|
|||||||
@@ -74,6 +74,14 @@ _NSIG_TESTS = [
|
|||||||
'https://www.youtube.com/s/player/f8cb7a3b/player_ias.vflset/en_US/base.js',
|
'https://www.youtube.com/s/player/f8cb7a3b/player_ias.vflset/en_US/base.js',
|
||||||
'oBo2h5euWy6osrUt', 'ivXHpm7qJjJN',
|
'oBo2h5euWy6osrUt', 'ivXHpm7qJjJN',
|
||||||
),
|
),
|
||||||
|
(
|
||||||
|
'https://www.youtube.com/s/player/2dfe380c/player_ias.vflset/en_US/base.js',
|
||||||
|
'oBo2h5euWy6osrUt', '3DIBbn3qdQ',
|
||||||
|
),
|
||||||
|
(
|
||||||
|
'https://www.youtube.com/s/player/f1ca6900/player_ias.vflset/en_US/base.js',
|
||||||
|
'cu3wyu6LQn2hse', 'jvxetvmlI9AN9Q',
|
||||||
|
),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+202
-104
@@ -93,6 +93,7 @@ from .utils import (
|
|||||||
PostProcessingError,
|
PostProcessingError,
|
||||||
preferredencoding,
|
preferredencoding,
|
||||||
prepend_extension,
|
prepend_extension,
|
||||||
|
ReExtractInfo,
|
||||||
register_socks_protocols,
|
register_socks_protocols,
|
||||||
RejectedVideoReached,
|
RejectedVideoReached,
|
||||||
render_table,
|
render_table,
|
||||||
@@ -109,7 +110,7 @@ from .utils import (
|
|||||||
strftime_or_none,
|
strftime_or_none,
|
||||||
subtitles_filename,
|
subtitles_filename,
|
||||||
supports_terminal_sequences,
|
supports_terminal_sequences,
|
||||||
ThrottledDownload,
|
timetuple_from_msec,
|
||||||
to_high_limit_path,
|
to_high_limit_path,
|
||||||
traverse_obj,
|
traverse_obj,
|
||||||
try_get,
|
try_get,
|
||||||
@@ -152,7 +153,7 @@ from .postprocessor import (
|
|||||||
_PLUGIN_CLASSES as plugin_postprocessors
|
_PLUGIN_CLASSES as plugin_postprocessors
|
||||||
)
|
)
|
||||||
from .update import detect_variant
|
from .update import detect_variant
|
||||||
from .version import __version__
|
from .version import __version__, RELEASE_GIT_HEAD
|
||||||
|
|
||||||
if compat_os_name == 'nt':
|
if compat_os_name == 'nt':
|
||||||
import ctypes
|
import ctypes
|
||||||
@@ -211,6 +212,9 @@ class YoutubeDL(object):
|
|||||||
simulate: Do not download the video files. If unset (or None),
|
simulate: Do not download the video files. If unset (or None),
|
||||||
simulate only if listsubtitles, listformats or list_thumbnails is used
|
simulate only if listsubtitles, listformats or list_thumbnails is used
|
||||||
format: Video format code. see "FORMAT SELECTION" for more details.
|
format: Video format code. see "FORMAT SELECTION" for more details.
|
||||||
|
You can also pass a function. The function takes 'ctx' as
|
||||||
|
argument and returns the formats to download.
|
||||||
|
See "build_format_selector" for an implementation
|
||||||
allow_unplayable_formats: Allow unplayable formats to be extracted and downloaded.
|
allow_unplayable_formats: Allow unplayable formats to be extracted and downloaded.
|
||||||
ignore_no_formats_error: Ignore "No video formats" error. Usefull for
|
ignore_no_formats_error: Ignore "No video formats" error. Usefull for
|
||||||
extracting metadata even if the video is not actually
|
extracting metadata even if the video is not actually
|
||||||
@@ -307,6 +311,8 @@ class YoutubeDL(object):
|
|||||||
file that is in the archive.
|
file that is in the archive.
|
||||||
break_on_reject: Stop the download process when encountering a video that
|
break_on_reject: Stop the download process when encountering a video that
|
||||||
has been filtered out.
|
has been filtered out.
|
||||||
|
break_per_url: Whether break_on_reject and break_on_existing
|
||||||
|
should act on each input URL as opposed to for the entire queue
|
||||||
cookiefile: File name where cookies should be read from and dumped to
|
cookiefile: File name where cookies should be read from and dumped to
|
||||||
cookiesfrombrowser: A tuple containing the name of the browser and the profile
|
cookiesfrombrowser: A tuple containing the name of the browser and the profile
|
||||||
name/path from where cookies are loaded.
|
name/path from where cookies are loaded.
|
||||||
@@ -321,13 +327,16 @@ class YoutubeDL(object):
|
|||||||
bidi_workaround: Work around buggy terminals without bidirectional text
|
bidi_workaround: Work around buggy terminals without bidirectional text
|
||||||
support, using fridibi
|
support, using fridibi
|
||||||
debug_printtraffic:Print out sent and received HTTP traffic
|
debug_printtraffic:Print out sent and received HTTP traffic
|
||||||
include_ads: Download ads as well
|
include_ads: Download ads as well (deprecated)
|
||||||
default_search: Prepend this string if an input url is not valid.
|
default_search: Prepend this string if an input url is not valid.
|
||||||
'auto' for elaborate guessing
|
'auto' for elaborate guessing
|
||||||
encoding: Use this encoding instead of the system-specified.
|
encoding: Use this encoding instead of the system-specified.
|
||||||
extract_flat: Do not resolve URLs, return the immediate result.
|
extract_flat: Do not resolve URLs, return the immediate result.
|
||||||
Pass in 'in_playlist' to only show this behavior for
|
Pass in 'in_playlist' to only show this behavior for
|
||||||
playlist items.
|
playlist items.
|
||||||
|
wait_for_video: If given, wait for scheduled streams to become available.
|
||||||
|
The value should be a tuple containing the range
|
||||||
|
(min_secs, max_secs) to wait between retries
|
||||||
postprocessors: A list of dictionaries, each with an entry
|
postprocessors: A list of dictionaries, each with an entry
|
||||||
* key: The name of the postprocessor. See
|
* key: The name of the postprocessor. See
|
||||||
yt_dlp/postprocessor/__init__.py for a list.
|
yt_dlp/postprocessor/__init__.py for a list.
|
||||||
@@ -428,7 +437,7 @@ class YoutubeDL(object):
|
|||||||
compat_opts: Compatibility options. See "Differences in default behavior".
|
compat_opts: Compatibility options. See "Differences in default behavior".
|
||||||
The following options do not work when used through the API:
|
The following options do not work when used through the API:
|
||||||
filename, abort-on-error, multistreams, no-live-chat, format-sort
|
filename, abort-on-error, multistreams, no-live-chat, format-sort
|
||||||
no-clean-infojson, no-playlist-metafiles, no-keep-subs.
|
no-clean-infojson, no-playlist-metafiles, no-keep-subs, no-attach-info-json.
|
||||||
Refer __init__.py for their implementation
|
Refer __init__.py for their implementation
|
||||||
progress_template: Dictionary of templates for progress outputs.
|
progress_template: Dictionary of templates for progress outputs.
|
||||||
Allowed keys are 'download', 'postprocess',
|
Allowed keys are 'download', 'postprocess',
|
||||||
@@ -525,7 +534,6 @@ class YoutubeDL(object):
|
|||||||
self.cache = Cache(self)
|
self.cache = Cache(self)
|
||||||
|
|
||||||
windows_enable_vt_mode()
|
windows_enable_vt_mode()
|
||||||
# FIXME: This will break if we ever print color to stdout
|
|
||||||
self._allow_colors = {
|
self._allow_colors = {
|
||||||
'screen': not self.params.get('no_color') and supports_terminal_sequences(self._screen_file),
|
'screen': not self.params.get('no_color') and supports_terminal_sequences(self._screen_file),
|
||||||
'err': not self.params.get('no_color') and supports_terminal_sequences(self._err_file),
|
'err': not self.params.get('no_color') and supports_terminal_sequences(self._err_file),
|
||||||
@@ -558,6 +566,8 @@ class YoutubeDL(object):
|
|||||||
|
|
||||||
for msg in self.params.get('_warnings', []):
|
for msg in self.params.get('_warnings', []):
|
||||||
self.report_warning(msg)
|
self.report_warning(msg)
|
||||||
|
for msg in self.params.get('_deprecation_warnings', []):
|
||||||
|
self.deprecation_warning(msg)
|
||||||
|
|
||||||
if 'list-formats' in self.params.get('compat_opts', []):
|
if 'list-formats' in self.params.get('compat_opts', []):
|
||||||
self.params['listformats_table'] = False
|
self.params['listformats_table'] = False
|
||||||
@@ -613,6 +623,7 @@ class YoutubeDL(object):
|
|||||||
# Creating format selector here allows us to catch syntax errors before the extraction
|
# Creating format selector here allows us to catch syntax errors before the extraction
|
||||||
self.format_selector = (
|
self.format_selector = (
|
||||||
None if self.params.get('format') is None
|
None if self.params.get('format') is None
|
||||||
|
else self.params['format'] if callable(self.params['format'])
|
||||||
else self.build_format_selector(self.params['format']))
|
else self.build_format_selector(self.params['format']))
|
||||||
|
|
||||||
self._setup_opener()
|
self._setup_opener()
|
||||||
@@ -629,11 +640,14 @@ class YoutubeDL(object):
|
|||||||
pp = pp_class(self, **compat_kwargs(pp_def))
|
pp = pp_class(self, **compat_kwargs(pp_def))
|
||||||
self.add_post_processor(pp, when=when)
|
self.add_post_processor(pp, when=when)
|
||||||
|
|
||||||
for ph in self.params.get('post_hooks', []):
|
hooks = {
|
||||||
self.add_post_hook(ph)
|
'post_hooks': self.add_post_hook,
|
||||||
|
'progress_hooks': self.add_progress_hook,
|
||||||
for ph in self.params.get('progress_hooks', []):
|
'postprocessor_hooks': self.add_postprocessor_hook,
|
||||||
self.add_progress_hook(ph)
|
}
|
||||||
|
for opt, fn in hooks.items():
|
||||||
|
for ph in self.params.get(opt, []):
|
||||||
|
fn(ph)
|
||||||
|
|
||||||
register_socks_protocols()
|
register_socks_protocols()
|
||||||
|
|
||||||
@@ -836,30 +850,31 @@ class YoutubeDL(object):
|
|||||||
|
|
||||||
class Styles(Enum):
|
class Styles(Enum):
|
||||||
HEADERS = 'yellow'
|
HEADERS = 'yellow'
|
||||||
EMPHASIS = 'blue'
|
EMPHASIS = 'light blue'
|
||||||
ID = 'green'
|
ID = 'green'
|
||||||
DELIM = 'blue'
|
DELIM = 'blue'
|
||||||
ERROR = 'red'
|
ERROR = 'red'
|
||||||
WARNING = 'yellow'
|
WARNING = 'yellow'
|
||||||
|
SUPPRESS = 'light black'
|
||||||
|
|
||||||
def __format_text(self, out, text, f, fallback=None, *, test_encoding=False):
|
def _format_text(self, handle, allow_colors, text, f, fallback=None, *, test_encoding=False):
|
||||||
assert out in ('screen', 'err')
|
|
||||||
if test_encoding:
|
if test_encoding:
|
||||||
original_text = text
|
original_text = text
|
||||||
handle = self._screen_file if out == 'screen' else self._err_file
|
|
||||||
encoding = self.params.get('encoding') or getattr(handle, 'encoding', 'ascii')
|
encoding = self.params.get('encoding') or getattr(handle, 'encoding', 'ascii')
|
||||||
text = text.encode(encoding, 'ignore').decode(encoding)
|
text = text.encode(encoding, 'ignore').decode(encoding)
|
||||||
if fallback is not None and text != original_text:
|
if fallback is not None and text != original_text:
|
||||||
text = fallback
|
text = fallback
|
||||||
if isinstance(f, self.Styles):
|
if isinstance(f, self.Styles):
|
||||||
f = f._value_
|
f = f.value
|
||||||
return format_text(text, f) if self._allow_colors[out] else text if fallback is None else fallback
|
return format_text(text, f) if allow_colors else text if fallback is None else fallback
|
||||||
|
|
||||||
def _format_screen(self, *args, **kwargs):
|
def _format_screen(self, *args, **kwargs):
|
||||||
return self.__format_text('screen', *args, **kwargs)
|
return self._format_text(
|
||||||
|
self._screen_file, self._allow_colors['screen'], *args, **kwargs)
|
||||||
|
|
||||||
def _format_err(self, *args, **kwargs):
|
def _format_err(self, *args, **kwargs):
|
||||||
return self.__format_text('err', *args, **kwargs)
|
return self._format_text(
|
||||||
|
self._err_file, self._allow_colors['err'], *args, **kwargs)
|
||||||
|
|
||||||
def report_warning(self, message, only_once=False):
|
def report_warning(self, message, only_once=False):
|
||||||
'''
|
'''
|
||||||
@@ -873,6 +888,12 @@ class YoutubeDL(object):
|
|||||||
return
|
return
|
||||||
self.to_stderr(f'{self._format_err("WARNING:", self.Styles.WARNING)} {message}', only_once)
|
self.to_stderr(f'{self._format_err("WARNING:", self.Styles.WARNING)} {message}', only_once)
|
||||||
|
|
||||||
|
def deprecation_warning(self, message):
|
||||||
|
if self.params.get('logger') is not None:
|
||||||
|
self.params['logger'].warning('DeprecationWarning: {message}')
|
||||||
|
else:
|
||||||
|
self.to_stderr(f'{self._format_err("DeprecationWarning:", self.Styles.ERROR)} {message}', True)
|
||||||
|
|
||||||
def report_error(self, message, tb=None):
|
def report_error(self, message, tb=None):
|
||||||
'''
|
'''
|
||||||
Do the same as trouble, but prefixes the message with 'ERROR:', colored
|
Do the same as trouble, but prefixes the message with 'ERROR:', colored
|
||||||
@@ -1165,12 +1186,8 @@ class YoutubeDL(object):
|
|||||||
# https://github.com/blackjack4494/youtube-dlc/issues/85
|
# https://github.com/blackjack4494/youtube-dlc/issues/85
|
||||||
trim_file_name = self.params.get('trim_file_name', False)
|
trim_file_name = self.params.get('trim_file_name', False)
|
||||||
if trim_file_name:
|
if trim_file_name:
|
||||||
fn_groups = filename.rsplit('.')
|
no_ext, *ext = filename.rsplit('.', 2)
|
||||||
ext = fn_groups[-1]
|
filename = join_nonempty(no_ext[:trim_file_name], *ext, delim='.')
|
||||||
sub_ext = ''
|
|
||||||
if len(fn_groups) > 2:
|
|
||||||
sub_ext = fn_groups[-2]
|
|
||||||
filename = join_nonempty(fn_groups[0][:trim_file_name], sub_ext, ext, delim='.')
|
|
||||||
|
|
||||||
return filename
|
return filename
|
||||||
except ValueError as err:
|
except ValueError as err:
|
||||||
@@ -1297,8 +1314,9 @@ class YoutubeDL(object):
|
|||||||
|
|
||||||
temp_id = ie.get_temp_id(url)
|
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}):
|
if temp_id is not None and self.in_download_archive({'id': temp_id, 'ie_key': ie_key}):
|
||||||
self.to_screen("[%s] %s: has already been recorded in archive" % (
|
self.to_screen(f'[{ie_key}] {temp_id}: has already been recorded in the archive')
|
||||||
ie_key, temp_id))
|
if self.params.get('break_on_existing', False):
|
||||||
|
raise ExistingVideoReached()
|
||||||
break
|
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(ie_key), download, extra_info, process)
|
||||||
else:
|
else:
|
||||||
@@ -1318,11 +1336,14 @@ class YoutubeDL(object):
|
|||||||
self.report_error(msg)
|
self.report_error(msg)
|
||||||
except ExtractorError as e: # An error we somewhat expected
|
except ExtractorError as e: # An error we somewhat expected
|
||||||
self.report_error(compat_str(e), e.format_traceback())
|
self.report_error(compat_str(e), e.format_traceback())
|
||||||
except ThrottledDownload as e:
|
except ReExtractInfo as e:
|
||||||
self.to_stderr('\r')
|
if e.expected:
|
||||||
self.report_warning(f'{e}; Re-extracting data')
|
self.to_screen(f'{e}; Re-extracting data')
|
||||||
|
else:
|
||||||
|
self.to_stderr('\r')
|
||||||
|
self.report_warning(f'{e}; Re-extracting data')
|
||||||
return wrapper(self, *args, **kwargs)
|
return wrapper(self, *args, **kwargs)
|
||||||
except (DownloadCancelled, LazyList.IndexError):
|
except (DownloadCancelled, LazyList.IndexError, PagedList.IndexError):
|
||||||
raise
|
raise
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
if self.params.get('ignoreerrors'):
|
if self.params.get('ignoreerrors'):
|
||||||
@@ -1331,6 +1352,47 @@ class YoutubeDL(object):
|
|||||||
raise
|
raise
|
||||||
return wrapper
|
return wrapper
|
||||||
|
|
||||||
|
def _wait_for_video(self, ie_result):
|
||||||
|
if (not self.params.get('wait_for_video')
|
||||||
|
or ie_result.get('_type', 'video') != 'video'
|
||||||
|
or ie_result.get('formats') or ie_result.get('url')):
|
||||||
|
return
|
||||||
|
|
||||||
|
format_dur = lambda dur: '%02d:%02d:%02d' % timetuple_from_msec(dur * 1000)[:-1]
|
||||||
|
last_msg = ''
|
||||||
|
|
||||||
|
def progress(msg):
|
||||||
|
nonlocal last_msg
|
||||||
|
self.to_screen(msg + ' ' * (len(last_msg) - len(msg)) + '\r', skip_eol=True)
|
||||||
|
last_msg = msg
|
||||||
|
|
||||||
|
min_wait, max_wait = self.params.get('wait_for_video')
|
||||||
|
diff = try_get(ie_result, lambda x: x['release_timestamp'] - time.time())
|
||||||
|
if diff is None and ie_result.get('live_status') == 'is_upcoming':
|
||||||
|
diff = random.randrange(min_wait or 0, max_wait) if max_wait else min_wait
|
||||||
|
self.report_warning('Release time of video is not known')
|
||||||
|
elif (diff or 0) <= 0:
|
||||||
|
self.report_warning('Video should already be available according to extracted info')
|
||||||
|
diff = min(max(diff, min_wait or 0), max_wait or float('inf'))
|
||||||
|
self.to_screen(f'[wait] Waiting for {format_dur(diff)} - Press Ctrl+C to try now')
|
||||||
|
|
||||||
|
wait_till = time.time() + diff
|
||||||
|
try:
|
||||||
|
while True:
|
||||||
|
diff = wait_till - time.time()
|
||||||
|
if diff <= 0:
|
||||||
|
progress('')
|
||||||
|
raise ReExtractInfo('[wait] Wait period ended', expected=True)
|
||||||
|
progress(f'[wait] Remaining time until next attempt: {self._format_screen(format_dur(diff), self.Styles.EMPHASIS)}')
|
||||||
|
time.sleep(1)
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
progress('')
|
||||||
|
raise ReExtractInfo('[wait] Interrupted by user', expected=True)
|
||||||
|
except BaseException as e:
|
||||||
|
if not isinstance(e, ReExtractInfo):
|
||||||
|
self.to_screen('')
|
||||||
|
raise
|
||||||
|
|
||||||
@__handle_extraction_exceptions
|
@__handle_extraction_exceptions
|
||||||
def __extract_info(self, url, ie, download, extra_info, process):
|
def __extract_info(self, url, ie, download, extra_info, process):
|
||||||
ie_result = ie.extract(url)
|
ie_result = ie.extract(url)
|
||||||
@@ -1346,6 +1408,7 @@ class YoutubeDL(object):
|
|||||||
ie_result.setdefault('original_url', extra_info['original_url'])
|
ie_result.setdefault('original_url', extra_info['original_url'])
|
||||||
self.add_default_extra_info(ie_result, ie, url)
|
self.add_default_extra_info(ie_result, ie, url)
|
||||||
if process:
|
if process:
|
||||||
|
self._wait_for_video(ie_result)
|
||||||
return self.process_ie_result(ie_result, download, extra_info)
|
return self.process_ie_result(ie_result, download, extra_info)
|
||||||
else:
|
else:
|
||||||
return ie_result
|
return ie_result
|
||||||
@@ -1500,11 +1563,13 @@ class YoutubeDL(object):
|
|||||||
|
|
||||||
if 'entries' not in ie_result:
|
if 'entries' not in ie_result:
|
||||||
raise EntryNotInPlaylist('There are no entries')
|
raise EntryNotInPlaylist('There are no entries')
|
||||||
|
|
||||||
|
MissingEntry = object()
|
||||||
incomplete_entries = bool(ie_result.get('requested_entries'))
|
incomplete_entries = bool(ie_result.get('requested_entries'))
|
||||||
if incomplete_entries:
|
if incomplete_entries:
|
||||||
def fill_missing_entries(entries, indexes):
|
def fill_missing_entries(entries, indices):
|
||||||
ret = [None] * max(*indexes)
|
ret = [MissingEntry] * max(indices)
|
||||||
for i, entry in zip(indexes, entries):
|
for i, entry in zip(indices, entries):
|
||||||
ret[i - 1] = entry
|
ret[i - 1] = entry
|
||||||
return ret
|
return ret
|
||||||
ie_result['entries'] = fill_missing_entries(ie_result['entries'], ie_result['requested_entries'])
|
ie_result['entries'] = fill_missing_entries(ie_result['entries'], ie_result['requested_entries'])
|
||||||
@@ -1557,7 +1622,7 @@ class YoutubeDL(object):
|
|||||||
entry = None
|
entry = None
|
||||||
try:
|
try:
|
||||||
entry = get_entry(i)
|
entry = get_entry(i)
|
||||||
if entry is None:
|
if entry is MissingEntry:
|
||||||
raise EntryNotInPlaylist()
|
raise EntryNotInPlaylist()
|
||||||
except (IndexError, EntryNotInPlaylist):
|
except (IndexError, EntryNotInPlaylist):
|
||||||
if incomplete_entries:
|
if incomplete_entries:
|
||||||
@@ -1579,10 +1644,11 @@ class YoutubeDL(object):
|
|||||||
if entry is not None]
|
if entry is not None]
|
||||||
n_entries = len(entries)
|
n_entries = len(entries)
|
||||||
|
|
||||||
if not playlistitems and (playliststart or playlistend):
|
if not playlistitems and (playliststart != 1 or playlistend):
|
||||||
playlistitems = list(range(playliststart, playliststart + n_entries))
|
playlistitems = list(range(playliststart, playliststart + n_entries))
|
||||||
ie_result['requested_entries'] = playlistitems
|
ie_result['requested_entries'] = playlistitems
|
||||||
|
|
||||||
|
_infojson_written = False
|
||||||
if not self.params.get('simulate') and self.params.get('allow_playlist_files', True):
|
if not self.params.get('simulate') and self.params.get('allow_playlist_files', True):
|
||||||
ie_copy = {
|
ie_copy = {
|
||||||
'playlist': playlist,
|
'playlist': playlist,
|
||||||
@@ -1595,8 +1661,9 @@ class YoutubeDL(object):
|
|||||||
}
|
}
|
||||||
ie_copy.update(dict(ie_result))
|
ie_copy.update(dict(ie_result))
|
||||||
|
|
||||||
if self._write_info_json('playlist', ie_result,
|
_infojson_written = self._write_info_json(
|
||||||
self.prepare_filename(ie_copy, 'pl_infojson')) is None:
|
'playlist', ie_result, self.prepare_filename(ie_copy, 'pl_infojson'))
|
||||||
|
if _infojson_written is None:
|
||||||
return
|
return
|
||||||
if self._write_description('playlist', ie_result,
|
if self._write_description('playlist', ie_result,
|
||||||
self.prepare_filename(ie_copy, 'pl_description')) is None:
|
self.prepare_filename(ie_copy, 'pl_description')) is None:
|
||||||
@@ -1649,9 +1716,14 @@ class YoutubeDL(object):
|
|||||||
self.report_error(
|
self.report_error(
|
||||||
'Skipping the remaining entries in playlist "%s" since %d items failed extraction' % (playlist, failures))
|
'Skipping the remaining entries in playlist "%s" since %d items failed extraction' % (playlist, failures))
|
||||||
break
|
break
|
||||||
# TODO: skip failed (empty) entries?
|
|
||||||
playlist_results.append(entry_result)
|
playlist_results.append(entry_result)
|
||||||
ie_result['entries'] = playlist_results
|
ie_result['entries'] = playlist_results
|
||||||
|
|
||||||
|
# Write the updated info to json
|
||||||
|
if _infojson_written and self._write_info_json(
|
||||||
|
'updated playlist', ie_result,
|
||||||
|
self.prepare_filename(ie_copy, 'pl_infojson'), overwrite=True) is None:
|
||||||
|
return
|
||||||
self.to_screen('[download] Finished downloading playlist: %s' % playlist)
|
self.to_screen('[download] Finished downloading playlist: %s' % playlist)
|
||||||
return ie_result
|
return ie_result
|
||||||
|
|
||||||
@@ -1724,9 +1796,10 @@ class YoutubeDL(object):
|
|||||||
def _check_formats(self, formats):
|
def _check_formats(self, formats):
|
||||||
for f in formats:
|
for f in formats:
|
||||||
self.to_screen('[info] Testing format %s' % f['format_id'])
|
self.to_screen('[info] Testing format %s' % f['format_id'])
|
||||||
temp_file = tempfile.NamedTemporaryFile(
|
path = self.get_output_path('temp')
|
||||||
suffix='.tmp', delete=False,
|
if not self._ensure_dir_exists(f'{path}/'):
|
||||||
dir=self.get_output_path('temp') or None)
|
continue
|
||||||
|
temp_file = tempfile.NamedTemporaryFile(suffix='.tmp', delete=False, dir=path or None)
|
||||||
temp_file.close()
|
temp_file.close()
|
||||||
try:
|
try:
|
||||||
success, _ = self.dl(temp_file.name, f, test=True)
|
success, _ = self.dl(temp_file.name, f, test=True)
|
||||||
@@ -1927,9 +2000,9 @@ class YoutubeDL(object):
|
|||||||
'format_id': '+'.join(filtered('format_id')),
|
'format_id': '+'.join(filtered('format_id')),
|
||||||
'ext': output_ext,
|
'ext': output_ext,
|
||||||
'protocol': '+'.join(map(determine_protocol, formats_info)),
|
'protocol': '+'.join(map(determine_protocol, formats_info)),
|
||||||
'language': '+'.join(orderedSet(filtered('language'))),
|
'language': '+'.join(orderedSet(filtered('language'))) or None,
|
||||||
'format_note': '+'.join(orderedSet(filtered('format_note'))),
|
'format_note': '+'.join(orderedSet(filtered('format_note'))) or None,
|
||||||
'filesize_approx': sum(filtered('filesize', 'filesize_approx')),
|
'filesize_approx': sum(filtered('filesize', 'filesize_approx')) or None,
|
||||||
'tbr': sum(filtered('tbr', 'vbr', 'abr')),
|
'tbr': sum(filtered('tbr', 'vbr', 'abr')),
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1996,7 +2069,7 @@ class YoutubeDL(object):
|
|||||||
# TODO: Add allvideo, allaudio etc by generalizing the code with best/worst selector
|
# TODO: Add allvideo, allaudio etc by generalizing the code with best/worst selector
|
||||||
if format_spec == 'all':
|
if format_spec == 'all':
|
||||||
def selector_function(ctx):
|
def selector_function(ctx):
|
||||||
yield from _check_formats(ctx['formats'])
|
yield from _check_formats(ctx['formats'][::-1])
|
||||||
elif format_spec == 'mergeall':
|
elif format_spec == 'mergeall':
|
||||||
def selector_function(ctx):
|
def selector_function(ctx):
|
||||||
formats = list(_check_formats(ctx['formats']))
|
formats = list(_check_formats(ctx['formats']))
|
||||||
@@ -2151,7 +2224,7 @@ class YoutubeDL(object):
|
|||||||
t['url'] = sanitize_url(t['url'])
|
t['url'] = sanitize_url(t['url'])
|
||||||
|
|
||||||
if self.params.get('check_formats') is True:
|
if self.params.get('check_formats') is True:
|
||||||
info_dict['thumbnails'] = LazyList(check_thumbnails(thumbnails[::-1])).reverse()
|
info_dict['thumbnails'] = LazyList(check_thumbnails(thumbnails[::-1]), reverse=True)
|
||||||
else:
|
else:
|
||||||
info_dict['thumbnails'] = thumbnails
|
info_dict['thumbnails'] = thumbnails
|
||||||
|
|
||||||
@@ -2346,7 +2419,7 @@ class YoutubeDL(object):
|
|||||||
# TODO Central sorting goes here
|
# TODO Central sorting goes here
|
||||||
|
|
||||||
if self.params.get('check_formats') is True:
|
if self.params.get('check_formats') is True:
|
||||||
formats = LazyList(self._check_formats(formats[::-1])).reverse()
|
formats = LazyList(self._check_formats(formats[::-1]), reverse=True)
|
||||||
|
|
||||||
if not formats or formats[0] is not info_dict:
|
if not formats or formats[0] is not info_dict:
|
||||||
# only set the 'formats' fields if the original info_dict list them
|
# only set the 'formats' fields if the original info_dict list them
|
||||||
@@ -2357,6 +2430,9 @@ class YoutubeDL(object):
|
|||||||
|
|
||||||
info_dict, _ = self.pre_process(info_dict)
|
info_dict, _ = self.pre_process(info_dict)
|
||||||
|
|
||||||
|
# The pre-processors may have modified the formats
|
||||||
|
formats = info_dict.get('formats', [info_dict])
|
||||||
|
|
||||||
if self.params.get('list_thumbnails'):
|
if self.params.get('list_thumbnails'):
|
||||||
self.list_thumbnails(info_dict)
|
self.list_thumbnails(info_dict)
|
||||||
if self.params.get('listformats'):
|
if self.params.get('listformats'):
|
||||||
@@ -2638,6 +2714,8 @@ class YoutubeDL(object):
|
|||||||
infofn = self.prepare_filename(info_dict, 'infojson')
|
infofn = self.prepare_filename(info_dict, 'infojson')
|
||||||
_infojson_written = self._write_info_json('video', info_dict, infofn)
|
_infojson_written = self._write_info_json('video', info_dict, infofn)
|
||||||
if _infojson_written:
|
if _infojson_written:
|
||||||
|
info_dict['infojson_filename'] = infofn
|
||||||
|
# For backward compatability, even though it was a private field
|
||||||
info_dict['__infojson_filename'] = infofn
|
info_dict['__infojson_filename'] = infofn
|
||||||
elif _infojson_written is None:
|
elif _infojson_written is None:
|
||||||
return
|
return
|
||||||
@@ -2670,6 +2748,8 @@ class YoutubeDL(object):
|
|||||||
self.report_error('Cannot write internet shortcut file because the "webpage_url" field is missing in the media information')
|
self.report_error('Cannot write internet shortcut file because the "webpage_url" field is missing in the media information')
|
||||||
return False
|
return False
|
||||||
linkfn = replace_extension(self.prepare_filename(info_dict, 'link'), link_type, info_dict.get('ext'))
|
linkfn = replace_extension(self.prepare_filename(info_dict, 'link'), link_type, info_dict.get('ext'))
|
||||||
|
if not self._ensure_dir_exists(encodeFilename(linkfn)):
|
||||||
|
return False
|
||||||
if self.params.get('overwrites', True) and os.path.exists(encodeFilename(linkfn)):
|
if self.params.get('overwrites', True) and os.path.exists(encodeFilename(linkfn)):
|
||||||
self.to_screen(f'[info] Internet shortcut (.{link_type}) is already present')
|
self.to_screen(f'[info] Internet shortcut (.{link_type}) is already present')
|
||||||
return True
|
return True
|
||||||
@@ -2911,9 +2991,10 @@ class YoutubeDL(object):
|
|||||||
downloader = get_suitable_downloader(info_dict, self.params) if 'protocol' in info_dict else None
|
downloader = get_suitable_downloader(info_dict, self.params) if 'protocol' in info_dict else None
|
||||||
downloader = downloader.__name__ if downloader else None
|
downloader = downloader.__name__ if downloader else None
|
||||||
ffmpeg_fixup(info_dict.get('requested_formats') is None and downloader == 'HlsFD',
|
ffmpeg_fixup(info_dict.get('requested_formats') is None and downloader == 'HlsFD',
|
||||||
'malformed AAC bitstream detected', FFmpegFixupM3u8PP)
|
'Possible MPEG-TS in MP4 container or malformed AAC timestamps',
|
||||||
ffmpeg_fixup(downloader == 'WebSocketFragmentFD', 'malformed timestamps detected', FFmpegFixupTimestampPP)
|
FFmpegFixupM3u8PP)
|
||||||
ffmpeg_fixup(downloader == 'WebSocketFragmentFD', 'malformed duration detected', FFmpegFixupDurationPP)
|
ffmpeg_fixup(downloader == 'WebSocketFragmentFD', 'Malformed timestamps detected', FFmpegFixupTimestampPP)
|
||||||
|
ffmpeg_fixup(downloader == 'WebSocketFragmentFD', 'Malformed duration detected', FFmpegFixupDurationPP)
|
||||||
|
|
||||||
fixup()
|
fixup()
|
||||||
try:
|
try:
|
||||||
@@ -2942,9 +3023,13 @@ class YoutubeDL(object):
|
|||||||
res = func(*args, **kwargs)
|
res = func(*args, **kwargs)
|
||||||
except UnavailableVideoError as e:
|
except UnavailableVideoError as e:
|
||||||
self.report_error(e)
|
self.report_error(e)
|
||||||
except DownloadCancelled as e:
|
except MaxDownloadsReached as e:
|
||||||
self.to_screen(f'[info] {e}')
|
self.to_screen(f'[info] {e}')
|
||||||
raise
|
raise
|
||||||
|
except DownloadCancelled as e:
|
||||||
|
self.to_screen(f'[info] {e}')
|
||||||
|
if not self.params.get('break_per_url'):
|
||||||
|
raise
|
||||||
else:
|
else:
|
||||||
if self.params.get('dump_single_json', False):
|
if self.params.get('dump_single_json', False):
|
||||||
self.post_extract(res)
|
self.post_extract(res)
|
||||||
@@ -2975,8 +3060,9 @@ class YoutubeDL(object):
|
|||||||
info = self.sanitize_info(json.loads('\n'.join(f)), self.params.get('clean_infojson', True))
|
info = self.sanitize_info(json.loads('\n'.join(f)), self.params.get('clean_infojson', True))
|
||||||
try:
|
try:
|
||||||
self.__download_wrapper(self.process_ie_result)(info, download=True)
|
self.__download_wrapper(self.process_ie_result)(info, download=True)
|
||||||
except (DownloadError, EntryNotInPlaylist, ThrottledDownload) as e:
|
except (DownloadError, EntryNotInPlaylist, ReExtractInfo) as e:
|
||||||
self.to_stderr('\r')
|
if not isinstance(e, EntryNotInPlaylist):
|
||||||
|
self.to_stderr('\r')
|
||||||
webpage_url = info.get('webpage_url')
|
webpage_url = info.get('webpage_url')
|
||||||
if webpage_url is not None:
|
if webpage_url is not None:
|
||||||
self.report_warning(f'The info failed to download: {e}; trying with URL {webpage_url}')
|
self.report_warning(f'The info failed to download: {e}; trying with URL {webpage_url}')
|
||||||
@@ -2995,8 +3081,8 @@ class YoutubeDL(object):
|
|||||||
keep_keys = ['_type'] # Always keep this to facilitate load-info-json
|
keep_keys = ['_type'] # Always keep this to facilitate load-info-json
|
||||||
if remove_private_keys:
|
if remove_private_keys:
|
||||||
remove_keys |= {
|
remove_keys |= {
|
||||||
'requested_formats', 'requested_subtitles', 'requested_entries',
|
'requested_formats', 'requested_subtitles', 'requested_entries', 'entries',
|
||||||
'filepath', 'entries', 'original_url', 'playlist_autonumber',
|
'filepath', 'infojson_filename', 'original_url', 'playlist_autonumber',
|
||||||
}
|
}
|
||||||
empty_values = (None, {}, [], set(), tuple())
|
empty_values = (None, {}, [], set(), tuple())
|
||||||
reject = lambda k, v: k not in keep_keys and (
|
reject = lambda k, v: k not in keep_keys and (
|
||||||
@@ -3126,35 +3212,34 @@ class YoutubeDL(object):
|
|||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def format_resolution(format, default='unknown'):
|
def format_resolution(format, default='unknown'):
|
||||||
is_images = format.get('vcodec') == 'none' and format.get('acodec') == 'none'
|
|
||||||
if format.get('vcodec') == 'none' and format.get('acodec') != 'none':
|
if format.get('vcodec') == 'none' and format.get('acodec') != 'none':
|
||||||
return 'audio only'
|
return 'audio only'
|
||||||
if format.get('resolution') is not None:
|
if format.get('resolution') is not None:
|
||||||
return format['resolution']
|
return format['resolution']
|
||||||
if format.get('width') and format.get('height'):
|
if format.get('width') and format.get('height'):
|
||||||
res = '%dx%d' % (format['width'], format['height'])
|
return '%dx%d' % (format['width'], format['height'])
|
||||||
elif format.get('height'):
|
elif format.get('height'):
|
||||||
res = '%sp' % format['height']
|
return '%sp' % format['height']
|
||||||
elif format.get('width'):
|
elif format.get('width'):
|
||||||
res = '%dx?' % format['width']
|
return '%dx?' % format['width']
|
||||||
elif is_images:
|
return default
|
||||||
return 'images'
|
|
||||||
else:
|
|
||||||
return default
|
|
||||||
return f'{res} images' if is_images else res
|
|
||||||
|
|
||||||
def _format_note(self, fdict):
|
def _format_note(self, fdict):
|
||||||
res = ''
|
res = ''
|
||||||
if fdict.get('ext') in ['f4f', 'f4m']:
|
if fdict.get('ext') in ['f4f', 'f4m']:
|
||||||
res += '(unsupported) '
|
res += '(unsupported)'
|
||||||
if fdict.get('language'):
|
if fdict.get('language'):
|
||||||
if res:
|
if res:
|
||||||
res += ' '
|
res += ' '
|
||||||
res += '[%s] ' % fdict['language']
|
res += '[%s]' % fdict['language']
|
||||||
if fdict.get('format_note') is not None:
|
if fdict.get('format_note') is not None:
|
||||||
res += fdict['format_note'] + ' '
|
if res:
|
||||||
|
res += ' '
|
||||||
|
res += fdict['format_note']
|
||||||
if fdict.get('tbr') is not None:
|
if fdict.get('tbr') is not None:
|
||||||
res += '%4dk ' % fdict['tbr']
|
if res:
|
||||||
|
res += ', '
|
||||||
|
res += '%4dk' % fdict['tbr']
|
||||||
if fdict.get('container') is not None:
|
if fdict.get('container') is not None:
|
||||||
if res:
|
if res:
|
||||||
res += ', '
|
res += ', '
|
||||||
@@ -3208,37 +3293,42 @@ class YoutubeDL(object):
|
|||||||
formats = info_dict.get('formats', [info_dict])
|
formats = info_dict.get('formats', [info_dict])
|
||||||
new_format = self.params.get('listformats_table', True) is not False
|
new_format = self.params.get('listformats_table', True) is not False
|
||||||
if new_format:
|
if new_format:
|
||||||
tbr_digits = number_of_digits(max(f.get('tbr') or 0 for f in formats))
|
|
||||||
vbr_digits = number_of_digits(max(f.get('vbr') or 0 for f in formats))
|
|
||||||
abr_digits = number_of_digits(max(f.get('abr') or 0 for f in formats))
|
|
||||||
delim = self._format_screen('\u2502', self.Styles.DELIM, '|', test_encoding=True)
|
delim = self._format_screen('\u2502', self.Styles.DELIM, '|', test_encoding=True)
|
||||||
table = [
|
table = [
|
||||||
[
|
[
|
||||||
self._format_screen(format_field(f, 'format_id'), self.Styles.ID),
|
self._format_screen(format_field(f, 'format_id'), self.Styles.ID),
|
||||||
format_field(f, 'ext'),
|
format_field(f, 'ext'),
|
||||||
self.format_resolution(f),
|
format_field(f, func=self.format_resolution, ignore=('audio only', 'images')),
|
||||||
format_field(f, 'fps', '%3d'),
|
format_field(f, 'fps', '\t%d'),
|
||||||
format_field(f, 'dynamic_range', '%s', ignore=(None, 'SDR')).replace('HDR', ''),
|
format_field(f, 'dynamic_range', '%s', ignore=(None, 'SDR')).replace('HDR', ''),
|
||||||
delim,
|
delim,
|
||||||
format_field(f, 'filesize', ' %s', func=format_bytes) + format_field(f, 'filesize_approx', '~%s', func=format_bytes),
|
format_field(f, 'filesize', ' \t%s', func=format_bytes) + format_field(f, 'filesize_approx', '~\t%s', func=format_bytes),
|
||||||
format_field(f, 'tbr', f'%{tbr_digits}dk'),
|
format_field(f, 'tbr', '\t%dk'),
|
||||||
shorten_protocol_name(f.get('protocol', '').replace("native", "n")),
|
shorten_protocol_name(f.get('protocol', '').replace('native', 'n')),
|
||||||
delim,
|
delim,
|
||||||
format_field(f, 'vcodec', default='unknown').replace('none', ''),
|
format_field(f, 'vcodec', default='unknown').replace(
|
||||||
format_field(f, 'vbr', f'%{vbr_digits}dk'),
|
'none',
|
||||||
format_field(f, 'acodec', default='unknown').replace('none', ''),
|
'images' if f.get('acodec') == 'none'
|
||||||
format_field(f, 'abr', f'%{abr_digits}dk'),
|
else self._format_screen('audio only', self.Styles.SUPPRESS)),
|
||||||
format_field(f, 'asr', '%5dHz'),
|
format_field(f, 'vbr', '\t%dk'),
|
||||||
|
format_field(f, 'acodec', default='unknown').replace(
|
||||||
|
'none',
|
||||||
|
'' if f.get('vcodec') == 'none'
|
||||||
|
else self._format_screen('video only', self.Styles.SUPPRESS)),
|
||||||
|
format_field(f, 'abr', '\t%dk'),
|
||||||
|
format_field(f, 'asr', '\t%dHz'),
|
||||||
join_nonempty(
|
join_nonempty(
|
||||||
self._format_screen('UNSUPPORTED', 'light red') if f.get('ext') in ('f4f', 'f4m') else None,
|
self._format_screen('UNSUPPORTED', 'light red') if f.get('ext') in ('f4f', 'f4m') else None,
|
||||||
format_field(f, 'language', '[%s]'),
|
format_field(f, 'language', '[%s]'),
|
||||||
format_field(f, 'format_note'),
|
join_nonempty(
|
||||||
format_field(f, 'container', ignore=(None, f.get('ext'))),
|
format_field(f, 'format_note'),
|
||||||
delim=', '),
|
format_field(f, 'container', ignore=(None, f.get('ext'))),
|
||||||
|
delim=', '),
|
||||||
|
delim=' '),
|
||||||
] for f in formats if f.get('preference') is None or f['preference'] >= -1000]
|
] for f in formats if f.get('preference') is None or f['preference'] >= -1000]
|
||||||
header_line = self._list_format_headers(
|
header_line = self._list_format_headers(
|
||||||
'ID', 'EXT', 'RESOLUTION', 'FPS', 'HDR', delim, ' FILESIZE', ' TBR', 'PROTO',
|
'ID', 'EXT', 'RESOLUTION', '\tFPS', 'HDR', delim, '\tFILESIZE', '\tTBR', 'PROTO',
|
||||||
delim, 'VCODEC', ' VBR', 'ACODEC', ' ABR', ' ASR', 'MORE INFO')
|
delim, 'VCODEC', '\tVBR', 'ACODEC', '\tABR', '\tASR', 'MORE INFO')
|
||||||
else:
|
else:
|
||||||
table = [
|
table = [
|
||||||
[
|
[
|
||||||
@@ -3254,8 +3344,8 @@ class YoutubeDL(object):
|
|||||||
'[info] Available formats for %s:' % info_dict['id'])
|
'[info] Available formats for %s:' % info_dict['id'])
|
||||||
self.to_stdout(render_table(
|
self.to_stdout(render_table(
|
||||||
header_line, table,
|
header_line, table,
|
||||||
extraGap=(0 if new_format else 1),
|
extra_gap=(0 if new_format else 1),
|
||||||
hideEmpty=new_format,
|
hide_empty=new_format,
|
||||||
delim=new_format and self._format_screen('\u2500', self.Styles.DELIM, '-', test_encoding=True)))
|
delim=new_format and self._format_screen('\u2500', self.Styles.DELIM, '-', test_encoding=True)))
|
||||||
|
|
||||||
def list_thumbnails(self, info_dict):
|
def list_thumbnails(self, info_dict):
|
||||||
@@ -3286,7 +3376,7 @@ class YoutubeDL(object):
|
|||||||
self.to_stdout(render_table(
|
self.to_stdout(render_table(
|
||||||
self._list_format_headers('Language', 'Name', 'Formats'),
|
self._list_format_headers('Language', 'Name', 'Formats'),
|
||||||
[_row(lang, formats) for lang, formats in subtitles.items()],
|
[_row(lang, formats) for lang, formats in subtitles.items()],
|
||||||
hideEmpty=True))
|
hide_empty=True))
|
||||||
|
|
||||||
def urlopen(self, req):
|
def urlopen(self, req):
|
||||||
""" Start an HTTP download """
|
""" Start an HTTP download """
|
||||||
@@ -3319,7 +3409,11 @@ class YoutubeDL(object):
|
|||||||
write_debug = lambda msg: self._write_string(f'[debug] {msg}\n')
|
write_debug = lambda msg: self._write_string(f'[debug] {msg}\n')
|
||||||
|
|
||||||
source = detect_variant()
|
source = detect_variant()
|
||||||
write_debug('yt-dlp version %s%s' % (__version__, '' if source == 'unknown' else f' ({source})'))
|
write_debug(join_nonempty(
|
||||||
|
'yt-dlp version', __version__,
|
||||||
|
f'[{RELEASE_GIT_HEAD}]' if RELEASE_GIT_HEAD else '',
|
||||||
|
'' if source == 'unknown' else f'({source})',
|
||||||
|
delim=' '))
|
||||||
if not _LAZY_LOADER:
|
if not _LAZY_LOADER:
|
||||||
if os.environ.get('YTDLP_NO_LAZY_EXTRACTORS'):
|
if os.environ.get('YTDLP_NO_LAZY_EXTRACTORS'):
|
||||||
write_debug('Lazy loading extractors is forcibly disabled')
|
write_debug('Lazy loading extractors is forcibly disabled')
|
||||||
@@ -3331,20 +3425,22 @@ class YoutubeDL(object):
|
|||||||
for name, klass in itertools.chain(plugin_extractors.items(), plugin_postprocessors.items())])
|
for name, klass in itertools.chain(plugin_extractors.items(), plugin_postprocessors.items())])
|
||||||
if self.params.get('compat_opts'):
|
if self.params.get('compat_opts'):
|
||||||
write_debug('Compatibility options: %s' % ', '.join(self.params.get('compat_opts')))
|
write_debug('Compatibility options: %s' % ', '.join(self.params.get('compat_opts')))
|
||||||
try:
|
|
||||||
sp = Popen(
|
if source == 'source':
|
||||||
['git', 'rev-parse', '--short', 'HEAD'],
|
|
||||||
stdout=subprocess.PIPE, stderr=subprocess.PIPE,
|
|
||||||
cwd=os.path.dirname(os.path.abspath(__file__)))
|
|
||||||
out, err = sp.communicate_or_kill()
|
|
||||||
out = out.decode().strip()
|
|
||||||
if re.match('[0-9a-f]+', out):
|
|
||||||
write_debug('Git HEAD: %s' % out)
|
|
||||||
except Exception:
|
|
||||||
try:
|
try:
|
||||||
sys.exc_clear()
|
sp = Popen(
|
||||||
|
['git', 'rev-parse', '--short', 'HEAD'],
|
||||||
|
stdout=subprocess.PIPE, stderr=subprocess.PIPE,
|
||||||
|
cwd=os.path.dirname(os.path.abspath(__file__)))
|
||||||
|
out, err = sp.communicate_or_kill()
|
||||||
|
out = out.decode().strip()
|
||||||
|
if re.match('[0-9a-f]+', out):
|
||||||
|
write_debug('Git HEAD: %s' % out)
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
try:
|
||||||
|
sys.exc_clear()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
def python_implementation():
|
def python_implementation():
|
||||||
impl_name = platform.python_implementation()
|
impl_name = platform.python_implementation()
|
||||||
@@ -3465,8 +3561,10 @@ class YoutubeDL(object):
|
|||||||
encoding = preferredencoding()
|
encoding = preferredencoding()
|
||||||
return encoding
|
return encoding
|
||||||
|
|
||||||
def _write_info_json(self, label, ie_result, infofn):
|
def _write_info_json(self, label, ie_result, infofn, overwrite=None):
|
||||||
''' Write infojson and returns True = written, False = skip, None = error '''
|
''' Write infojson and returns True = written, False = skip, None = error '''
|
||||||
|
if overwrite is None:
|
||||||
|
overwrite = self.params.get('overwrites', True)
|
||||||
if not self.params.get('writeinfojson'):
|
if not self.params.get('writeinfojson'):
|
||||||
return False
|
return False
|
||||||
elif not infofn:
|
elif not infofn:
|
||||||
@@ -3474,7 +3572,7 @@ class YoutubeDL(object):
|
|||||||
return False
|
return False
|
||||||
elif not self._ensure_dir_exists(infofn):
|
elif not self._ensure_dir_exists(infofn):
|
||||||
return None
|
return None
|
||||||
elif not self.params.get('overwrites', True) and os.path.exists(infofn):
|
elif not overwrite and os.path.exists(infofn):
|
||||||
self.to_screen(f'[info] {label.title()} metadata is already present')
|
self.to_screen(f'[info] {label.title()} metadata is already present')
|
||||||
else:
|
else:
|
||||||
self.to_screen(f'[info] Writing {label} metadata as JSON to: {infofn}')
|
self.to_screen(f'[info] Writing {label} metadata as JSON to: {infofn}')
|
||||||
|
|||||||
+73
-45
@@ -25,18 +25,17 @@ from .cookies import SUPPORTED_BROWSERS
|
|||||||
from .utils import (
|
from .utils import (
|
||||||
DateRange,
|
DateRange,
|
||||||
decodeOption,
|
decodeOption,
|
||||||
|
DownloadCancelled,
|
||||||
DownloadError,
|
DownloadError,
|
||||||
error_to_compat_str,
|
error_to_compat_str,
|
||||||
ExistingVideoReached,
|
|
||||||
expand_path,
|
expand_path,
|
||||||
|
GeoUtils,
|
||||||
float_or_none,
|
float_or_none,
|
||||||
int_or_none,
|
int_or_none,
|
||||||
match_filter_func,
|
match_filter_func,
|
||||||
MaxDownloadsReached,
|
|
||||||
parse_duration,
|
parse_duration,
|
||||||
preferredencoding,
|
preferredencoding,
|
||||||
read_batch_urls,
|
read_batch_urls,
|
||||||
RejectedVideoReached,
|
|
||||||
render_table,
|
render_table,
|
||||||
SameFileError,
|
SameFileError,
|
||||||
setproctitle,
|
setproctitle,
|
||||||
@@ -73,7 +72,7 @@ def _real_main(argv=None):
|
|||||||
setproctitle('yt-dlp')
|
setproctitle('yt-dlp')
|
||||||
|
|
||||||
parser, opts, args = parseOpts(argv)
|
parser, opts, args = parseOpts(argv)
|
||||||
warnings = []
|
warnings, deprecation_warnings = [], []
|
||||||
|
|
||||||
# Set user agent
|
# Set user agent
|
||||||
if opts.user_agent is not None:
|
if opts.user_agent is not None:
|
||||||
@@ -96,6 +95,7 @@ def _real_main(argv=None):
|
|||||||
if opts.batchfile is not None:
|
if opts.batchfile is not None:
|
||||||
try:
|
try:
|
||||||
if opts.batchfile == '-':
|
if opts.batchfile == '-':
|
||||||
|
write_string('Reading URLs from stdin:\n')
|
||||||
batchfd = sys.stdin
|
batchfd = sys.stdin
|
||||||
else:
|
else:
|
||||||
batchfd = io.open(
|
batchfd = io.open(
|
||||||
@@ -195,7 +195,15 @@ def _real_main(argv=None):
|
|||||||
if opts.overwrites: # --yes-overwrites implies --no-continue
|
if opts.overwrites: # --yes-overwrites implies --no-continue
|
||||||
opts.continue_dl = False
|
opts.continue_dl = False
|
||||||
if opts.concurrent_fragment_downloads <= 0:
|
if opts.concurrent_fragment_downloads <= 0:
|
||||||
raise ValueError('Concurrent fragments must be positive')
|
parser.error('Concurrent fragments must be positive')
|
||||||
|
if opts.wait_for_video is not None:
|
||||||
|
mobj = re.match(r'(?P<min>\d+)(?:-(?P<max>\d+))?$', opts.wait_for_video)
|
||||||
|
if not mobj:
|
||||||
|
parser.error('Invalid time range to wait')
|
||||||
|
min_wait, max_wait = map(int_or_none, mobj.group('min', 'max'))
|
||||||
|
if max_wait is not None and max_wait < min_wait:
|
||||||
|
parser.error('Invalid time range to wait')
|
||||||
|
opts.wait_for_video = (min_wait, max_wait)
|
||||||
|
|
||||||
def parse_retries(retries, name=''):
|
def parse_retries(retries, name=''):
|
||||||
if retries in ('inf', 'infinite'):
|
if retries in ('inf', 'infinite'):
|
||||||
@@ -223,9 +231,9 @@ def _real_main(argv=None):
|
|||||||
parser.error('invalid http chunk size specified')
|
parser.error('invalid http chunk size specified')
|
||||||
opts.http_chunk_size = numeric_chunksize
|
opts.http_chunk_size = numeric_chunksize
|
||||||
if opts.playliststart <= 0:
|
if opts.playliststart <= 0:
|
||||||
raise ValueError('Playlist start must be positive')
|
raise parser.error('Playlist start must be positive')
|
||||||
if opts.playlistend not in (-1, None) and opts.playlistend < opts.playliststart:
|
if opts.playlistend not in (-1, None) and opts.playlistend < opts.playliststart:
|
||||||
raise ValueError('Playlist end must be greater than playlist start')
|
raise parser.error('Playlist end must be greater than playlist start')
|
||||||
if opts.extractaudio:
|
if opts.extractaudio:
|
||||||
opts.audioformat = opts.audioformat.lower()
|
opts.audioformat = opts.audioformat.lower()
|
||||||
if opts.audioformat not in ['best'] + list(FFmpegExtractAudioPP.SUPPORTED_EXTS):
|
if opts.audioformat not in ['best'] + list(FFmpegExtractAudioPP.SUPPORTED_EXTS):
|
||||||
@@ -249,12 +257,17 @@ def _real_main(argv=None):
|
|||||||
if opts.convertthumbnails is not None:
|
if opts.convertthumbnails is not None:
|
||||||
if opts.convertthumbnails not in FFmpegThumbnailsConvertorPP.SUPPORTED_EXTS:
|
if opts.convertthumbnails not in FFmpegThumbnailsConvertorPP.SUPPORTED_EXTS:
|
||||||
parser.error('invalid thumbnail format specified')
|
parser.error('invalid thumbnail format specified')
|
||||||
|
|
||||||
if opts.cookiesfrombrowser is not None:
|
if opts.cookiesfrombrowser is not None:
|
||||||
opts.cookiesfrombrowser = [
|
opts.cookiesfrombrowser = [
|
||||||
part.strip() or None for part in opts.cookiesfrombrowser.split(':', 1)]
|
part.strip() or None for part in opts.cookiesfrombrowser.split(':', 1)]
|
||||||
if opts.cookiesfrombrowser[0].lower() not in SUPPORTED_BROWSERS:
|
if opts.cookiesfrombrowser[0].lower() not in SUPPORTED_BROWSERS:
|
||||||
parser.error('unsupported browser specified for cookies')
|
parser.error('unsupported browser specified for cookies')
|
||||||
|
geo_bypass_code = opts.geo_bypass_ip_block or opts.geo_bypass_country
|
||||||
|
if geo_bypass_code is not None:
|
||||||
|
try:
|
||||||
|
GeoUtils.random_ipv4(geo_bypass_code)
|
||||||
|
except Exception:
|
||||||
|
parser.error('unsupported geo-bypass country or ip-block')
|
||||||
|
|
||||||
if opts.date is not None:
|
if opts.date is not None:
|
||||||
date = DateRange.day(opts.date)
|
date = DateRange.day(opts.date)
|
||||||
@@ -290,6 +303,11 @@ def _real_main(argv=None):
|
|||||||
set_default_compat('abort-on-error', 'ignoreerrors', 'only_download')
|
set_default_compat('abort-on-error', 'ignoreerrors', 'only_download')
|
||||||
set_default_compat('no-playlist-metafiles', 'allow_playlist_files')
|
set_default_compat('no-playlist-metafiles', 'allow_playlist_files')
|
||||||
set_default_compat('no-clean-infojson', 'clean_infojson')
|
set_default_compat('no-clean-infojson', 'clean_infojson')
|
||||||
|
if 'no-attach-info-json' in compat_opts:
|
||||||
|
if opts.embed_infojson:
|
||||||
|
_unused_compat_opt('no-attach-info-json')
|
||||||
|
else:
|
||||||
|
opts.embed_infojson = False
|
||||||
if 'format-sort' in compat_opts:
|
if 'format-sort' in compat_opts:
|
||||||
opts.format_sort.extend(InfoExtractor.FormatSort.ytdl_default)
|
opts.format_sort.extend(InfoExtractor.FormatSort.ytdl_default)
|
||||||
_video_multistreams_set = set_default_compat('multistreams', 'allow_multiple_video_streams', False, remove_compat=False)
|
_video_multistreams_set = set_default_compat('multistreams', 'allow_multiple_video_streams', False, remove_compat=False)
|
||||||
@@ -373,8 +391,6 @@ def _real_main(argv=None):
|
|||||||
opts.sponsorblock_remove = set()
|
opts.sponsorblock_remove = set()
|
||||||
sponsorblock_query = opts.sponsorblock_mark | opts.sponsorblock_remove
|
sponsorblock_query = opts.sponsorblock_mark | opts.sponsorblock_remove
|
||||||
|
|
||||||
if (opts.addmetadata or opts.sponsorblock_mark) and opts.addchapters is None:
|
|
||||||
opts.addchapters = True
|
|
||||||
opts.remove_chapters = opts.remove_chapters or []
|
opts.remove_chapters = opts.remove_chapters or []
|
||||||
|
|
||||||
if (opts.remove_chapters or sponsorblock_query) and opts.sponskrub is not False:
|
if (opts.remove_chapters or sponsorblock_query) and opts.sponskrub is not False:
|
||||||
@@ -395,40 +411,32 @@ def _real_main(argv=None):
|
|||||||
opts.remuxvideo = False
|
opts.remuxvideo = False
|
||||||
|
|
||||||
if opts.allow_unplayable_formats:
|
if opts.allow_unplayable_formats:
|
||||||
if opts.extractaudio:
|
def report_unplayable_conflict(opt_name, arg, default=False, allowed=None):
|
||||||
report_conflict('--allow-unplayable-formats', '--extract-audio')
|
val = getattr(opts, opt_name)
|
||||||
opts.extractaudio = False
|
if (not allowed and val) or (allowed and not allowed(val)):
|
||||||
if opts.remuxvideo:
|
report_conflict('--allow-unplayable-formats', arg)
|
||||||
report_conflict('--allow-unplayable-formats', '--remux-video')
|
setattr(opts, opt_name, default)
|
||||||
opts.remuxvideo = False
|
|
||||||
if opts.recodevideo:
|
report_unplayable_conflict('extractaudio', '--extract-audio')
|
||||||
report_conflict('--allow-unplayable-formats', '--recode-video')
|
report_unplayable_conflict('remuxvideo', '--remux-video')
|
||||||
opts.recodevideo = False
|
report_unplayable_conflict('recodevideo', '--recode-video')
|
||||||
if opts.addmetadata:
|
report_unplayable_conflict('addmetadata', '--embed-metadata')
|
||||||
report_conflict('--allow-unplayable-formats', '--add-metadata')
|
report_unplayable_conflict('addchapters', '--embed-chapters')
|
||||||
opts.addmetadata = False
|
report_unplayable_conflict('embed_infojson', '--embed-info-json')
|
||||||
if opts.embedsubtitles:
|
opts.embed_infojson = False
|
||||||
report_conflict('--allow-unplayable-formats', '--embed-subs')
|
report_unplayable_conflict('embedsubtitles', '--embed-subs')
|
||||||
opts.embedsubtitles = False
|
report_unplayable_conflict('embedthumbnail', '--embed-thumbnail')
|
||||||
if opts.embedthumbnail:
|
report_unplayable_conflict('xattrs', '--xattrs')
|
||||||
report_conflict('--allow-unplayable-formats', '--embed-thumbnail')
|
report_unplayable_conflict('fixup', '--fixup', default='never', allowed=lambda x: x in (None, 'never', 'ignore'))
|
||||||
opts.embedthumbnail = False
|
|
||||||
if opts.xattrs:
|
|
||||||
report_conflict('--allow-unplayable-formats', '--xattrs')
|
|
||||||
opts.xattrs = False
|
|
||||||
if opts.fixup and opts.fixup.lower() not in ('never', 'ignore'):
|
|
||||||
report_conflict('--allow-unplayable-formats', '--fixup')
|
|
||||||
opts.fixup = 'never'
|
opts.fixup = 'never'
|
||||||
if opts.remove_chapters:
|
report_unplayable_conflict('remove_chapters', '--remove-chapters', default=[])
|
||||||
report_conflict('--allow-unplayable-formats', '--remove-chapters')
|
report_unplayable_conflict('sponsorblock_remove', '--sponsorblock-remove', default=set())
|
||||||
opts.remove_chapters = []
|
report_unplayable_conflict('sponskrub', '--sponskrub', default=set())
|
||||||
if opts.sponsorblock_remove:
|
|
||||||
report_conflict('--allow-unplayable-formats', '--sponsorblock-remove')
|
|
||||||
opts.sponsorblock_remove = set()
|
|
||||||
if opts.sponskrub:
|
|
||||||
report_conflict('--allow-unplayable-formats', '--sponskrub')
|
|
||||||
opts.sponskrub = False
|
opts.sponskrub = False
|
||||||
|
|
||||||
|
if (opts.addmetadata or opts.sponsorblock_mark) and opts.addchapters is None:
|
||||||
|
opts.addchapters = True
|
||||||
|
|
||||||
# PostProcessors
|
# PostProcessors
|
||||||
postprocessors = list(opts.add_postprocessors)
|
postprocessors = list(opts.add_postprocessors)
|
||||||
if sponsorblock_query:
|
if sponsorblock_query:
|
||||||
@@ -526,13 +534,16 @@ def _real_main(argv=None):
|
|||||||
# By default ffmpeg preserves metadata applicable for both
|
# By default ffmpeg preserves metadata applicable for both
|
||||||
# source and target containers. From this point the container won't change,
|
# source and target containers. From this point the container won't change,
|
||||||
# so metadata can be added here.
|
# so metadata can be added here.
|
||||||
if opts.addmetadata or opts.addchapters:
|
if opts.addmetadata or opts.addchapters or opts.embed_infojson:
|
||||||
|
if opts.embed_infojson is None:
|
||||||
|
opts.embed_infojson = 'if_exists'
|
||||||
postprocessors.append({
|
postprocessors.append({
|
||||||
'key': 'FFmpegMetadata',
|
'key': 'FFmpegMetadata',
|
||||||
'add_chapters': opts.addchapters,
|
'add_chapters': opts.addchapters,
|
||||||
'add_metadata': opts.addmetadata,
|
'add_metadata': opts.addmetadata,
|
||||||
|
'add_infojson': opts.embed_infojson,
|
||||||
})
|
})
|
||||||
# Note: Deprecated
|
# Deprecated
|
||||||
# This should be above EmbedThumbnail since sponskrub removes the thumbnail attachment
|
# This should be above EmbedThumbnail since sponskrub removes the thumbnail attachment
|
||||||
# but must be below EmbedSubtitle and FFmpegMetadata
|
# but must be below EmbedSubtitle and FFmpegMetadata
|
||||||
# See https://github.com/yt-dlp/yt-dlp/issues/204 , https://github.com/faissaloo/SponSkrub/issues/29
|
# See https://github.com/yt-dlp/yt-dlp/issues/204 , https://github.com/faissaloo/SponSkrub/issues/29
|
||||||
@@ -545,6 +556,7 @@ def _real_main(argv=None):
|
|||||||
'cut': opts.sponskrub_cut,
|
'cut': opts.sponskrub_cut,
|
||||||
'force': opts.sponskrub_force,
|
'force': opts.sponskrub_force,
|
||||||
'ignoreerror': opts.sponskrub is None,
|
'ignoreerror': opts.sponskrub is None,
|
||||||
|
'_from_cli': True,
|
||||||
})
|
})
|
||||||
if opts.embedthumbnail:
|
if opts.embedthumbnail:
|
||||||
already_have_thumbnail = opts.writethumbnail or opts.write_all_thumbnails
|
already_have_thumbnail = opts.writethumbnail or opts.write_all_thumbnails
|
||||||
@@ -584,6 +596,19 @@ def _real_main(argv=None):
|
|||||||
opts.postprocessor_args.setdefault('sponskrub', [])
|
opts.postprocessor_args.setdefault('sponskrub', [])
|
||||||
opts.postprocessor_args['default'] = opts.postprocessor_args['default-compat']
|
opts.postprocessor_args['default'] = opts.postprocessor_args['default-compat']
|
||||||
|
|
||||||
|
def report_deprecation(val, old, new=None):
|
||||||
|
if not val:
|
||||||
|
return
|
||||||
|
deprecation_warnings.append(
|
||||||
|
f'{old} is deprecated and may be removed in a future version. Use {new} instead' if new
|
||||||
|
else f'{old} is deprecated and may not work as expected')
|
||||||
|
|
||||||
|
report_deprecation(opts.sponskrub, '--sponskrub', '--sponsorblock-mark or --sponsorblock-remove')
|
||||||
|
report_deprecation(not opts.prefer_ffmpeg, '--prefer-avconv', 'ffmpeg')
|
||||||
|
report_deprecation(opts.include_ads, '--include-ads')
|
||||||
|
# report_deprecation(opts.call_home, '--call-home') # We may re-implement this in future
|
||||||
|
# report_deprecation(opts.writeannotations, '--write-annotations') # It's just that no website has it
|
||||||
|
|
||||||
final_ext = (
|
final_ext = (
|
||||||
opts.recodevideo if opts.recodevideo in FFmpegVideoConvertorPP.SUPPORTED_EXTS
|
opts.recodevideo if opts.recodevideo in FFmpegVideoConvertorPP.SUPPORTED_EXTS
|
||||||
else opts.remuxvideo if opts.remuxvideo in FFmpegVideoRemuxerPP.SUPPORTED_EXTS
|
else opts.remuxvideo if opts.remuxvideo in FFmpegVideoRemuxerPP.SUPPORTED_EXTS
|
||||||
@@ -703,6 +728,7 @@ def _real_main(argv=None):
|
|||||||
'download_archive': download_archive_fn,
|
'download_archive': download_archive_fn,
|
||||||
'break_on_existing': opts.break_on_existing,
|
'break_on_existing': opts.break_on_existing,
|
||||||
'break_on_reject': opts.break_on_reject,
|
'break_on_reject': opts.break_on_reject,
|
||||||
|
'break_per_url': opts.break_per_url,
|
||||||
'skip_playlist_after_errors': opts.skip_playlist_after_errors,
|
'skip_playlist_after_errors': opts.skip_playlist_after_errors,
|
||||||
'cookiefile': opts.cookiefile,
|
'cookiefile': opts.cookiefile,
|
||||||
'cookiesfrombrowser': opts.cookiesfrombrowser,
|
'cookiesfrombrowser': opts.cookiesfrombrowser,
|
||||||
@@ -721,6 +747,7 @@ def _real_main(argv=None):
|
|||||||
'youtube_include_hls_manifest': opts.youtube_include_hls_manifest,
|
'youtube_include_hls_manifest': opts.youtube_include_hls_manifest,
|
||||||
'encoding': opts.encoding,
|
'encoding': opts.encoding,
|
||||||
'extract_flat': opts.extract_flat,
|
'extract_flat': opts.extract_flat,
|
||||||
|
'wait_for_video': opts.wait_for_video,
|
||||||
'mark_watched': opts.mark_watched,
|
'mark_watched': opts.mark_watched,
|
||||||
'merge_output_format': opts.merge_output_format,
|
'merge_output_format': opts.merge_output_format,
|
||||||
'final_ext': final_ext,
|
'final_ext': final_ext,
|
||||||
@@ -750,11 +777,12 @@ def _real_main(argv=None):
|
|||||||
'geo_bypass_country': opts.geo_bypass_country,
|
'geo_bypass_country': opts.geo_bypass_country,
|
||||||
'geo_bypass_ip_block': opts.geo_bypass_ip_block,
|
'geo_bypass_ip_block': opts.geo_bypass_ip_block,
|
||||||
'_warnings': warnings,
|
'_warnings': warnings,
|
||||||
|
'_deprecation_warnings': deprecation_warnings,
|
||||||
'compat_opts': compat_opts,
|
'compat_opts': compat_opts,
|
||||||
}
|
}
|
||||||
|
|
||||||
with YoutubeDL(ydl_opts) as ydl:
|
with YoutubeDL(ydl_opts) as ydl:
|
||||||
actual_use = len(all_urls) or opts.load_info_filename
|
actual_use = all_urls or opts.load_info_filename
|
||||||
|
|
||||||
# Remove cache dir
|
# Remove cache dir
|
||||||
if opts.rm_cachedir:
|
if opts.rm_cachedir:
|
||||||
@@ -783,7 +811,7 @@ def _real_main(argv=None):
|
|||||||
retcode = ydl.download_with_info_file(expand_path(opts.load_info_filename))
|
retcode = ydl.download_with_info_file(expand_path(opts.load_info_filename))
|
||||||
else:
|
else:
|
||||||
retcode = ydl.download(all_urls)
|
retcode = ydl.download(all_urls)
|
||||||
except (MaxDownloadsReached, ExistingVideoReached, RejectedVideoReached):
|
except DownloadCancelled:
|
||||||
ydl.to_screen('Aborting remaining downloads')
|
ydl.to_screen('Aborting remaining downloads')
|
||||||
retcode = 101
|
retcode = 101
|
||||||
|
|
||||||
|
|||||||
@@ -28,6 +28,48 @@ else:
|
|||||||
BLOCK_SIZE_BYTES = 16
|
BLOCK_SIZE_BYTES = 16
|
||||||
|
|
||||||
|
|
||||||
|
def aes_ecb_encrypt(data, key, iv=None):
|
||||||
|
"""
|
||||||
|
Encrypt with aes in ECB mode
|
||||||
|
|
||||||
|
@param {int[]} data cleartext
|
||||||
|
@param {int[]} key 16/24/32-Byte cipher key
|
||||||
|
@param {int[]} iv Unused for this mode
|
||||||
|
@returns {int[]} encrypted data
|
||||||
|
"""
|
||||||
|
expanded_key = key_expansion(key)
|
||||||
|
block_count = int(ceil(float(len(data)) / BLOCK_SIZE_BYTES))
|
||||||
|
|
||||||
|
encrypted_data = []
|
||||||
|
for i in range(block_count):
|
||||||
|
block = data[i * BLOCK_SIZE_BYTES: (i + 1) * BLOCK_SIZE_BYTES]
|
||||||
|
encrypted_data += aes_encrypt(block, expanded_key)
|
||||||
|
encrypted_data = encrypted_data[:len(data)]
|
||||||
|
|
||||||
|
return encrypted_data
|
||||||
|
|
||||||
|
|
||||||
|
def aes_ecb_decrypt(data, key, iv=None):
|
||||||
|
"""
|
||||||
|
Decrypt with aes in ECB mode
|
||||||
|
|
||||||
|
@param {int[]} data cleartext
|
||||||
|
@param {int[]} key 16/24/32-Byte cipher key
|
||||||
|
@param {int[]} iv Unused for this mode
|
||||||
|
@returns {int[]} decrypted data
|
||||||
|
"""
|
||||||
|
expanded_key = key_expansion(key)
|
||||||
|
block_count = int(ceil(float(len(data)) / BLOCK_SIZE_BYTES))
|
||||||
|
|
||||||
|
encrypted_data = []
|
||||||
|
for i in range(block_count):
|
||||||
|
block = data[i * BLOCK_SIZE_BYTES: (i + 1) * BLOCK_SIZE_BYTES]
|
||||||
|
encrypted_data += aes_decrypt(block, expanded_key)
|
||||||
|
encrypted_data = encrypted_data[:len(data)]
|
||||||
|
|
||||||
|
return encrypted_data
|
||||||
|
|
||||||
|
|
||||||
def aes_ctr_decrypt(data, key, iv):
|
def aes_ctr_decrypt(data, key, iv):
|
||||||
"""
|
"""
|
||||||
Decrypt with aes in counter mode
|
Decrypt with aes in counter mode
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ from .external import (
|
|||||||
|
|
||||||
PROTOCOL_MAP = {
|
PROTOCOL_MAP = {
|
||||||
'rtmp': RtmpFD,
|
'rtmp': RtmpFD,
|
||||||
|
'rtmpe': RtmpFD,
|
||||||
'rtmp_ffmpeg': FFmpegFD,
|
'rtmp_ffmpeg': FFmpegFD,
|
||||||
'm3u8_native': HlsFD,
|
'm3u8_native': HlsFD,
|
||||||
'm3u8': FFmpegFD,
|
'm3u8': FFmpegFD,
|
||||||
|
|||||||
@@ -93,6 +93,8 @@ class FileDownloader(object):
|
|||||||
def format_percent(percent):
|
def format_percent(percent):
|
||||||
if percent is None:
|
if percent is None:
|
||||||
return '---.-%'
|
return '---.-%'
|
||||||
|
elif percent == 100:
|
||||||
|
return '100%'
|
||||||
return '%6s' % ('%3.1f%%' % percent)
|
return '%6s' % ('%3.1f%%' % percent)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
@@ -247,11 +249,29 @@ class FileDownloader(object):
|
|||||||
self._multiline = BreaklineStatusPrinter(self.ydl._screen_file, lines)
|
self._multiline = BreaklineStatusPrinter(self.ydl._screen_file, lines)
|
||||||
else:
|
else:
|
||||||
self._multiline = MultilinePrinter(self.ydl._screen_file, lines, not self.params.get('quiet'))
|
self._multiline = MultilinePrinter(self.ydl._screen_file, lines, not self.params.get('quiet'))
|
||||||
|
self._multiline.allow_colors = self._multiline._HAVE_FULLCAP and not self.params.get('no_color')
|
||||||
|
|
||||||
def _finish_multiline_status(self):
|
def _finish_multiline_status(self):
|
||||||
self._multiline.end()
|
self._multiline.end()
|
||||||
|
|
||||||
def _report_progress_status(self, s):
|
_progress_styles = {
|
||||||
|
'downloaded_bytes': 'light blue',
|
||||||
|
'percent': 'light blue',
|
||||||
|
'eta': 'yellow',
|
||||||
|
'speed': 'green',
|
||||||
|
'elapsed': 'bold white',
|
||||||
|
'total_bytes': '',
|
||||||
|
'total_bytes_estimate': '',
|
||||||
|
}
|
||||||
|
|
||||||
|
def _report_progress_status(self, s, default_template):
|
||||||
|
for name, style in self._progress_styles.items():
|
||||||
|
name = f'_{name}_str'
|
||||||
|
if name not in s:
|
||||||
|
continue
|
||||||
|
s[name] = self._format_progress(s[name], style)
|
||||||
|
s['_default_template'] = default_template % s
|
||||||
|
|
||||||
progress_dict = s.copy()
|
progress_dict = s.copy()
|
||||||
progress_dict.pop('info_dict')
|
progress_dict.pop('info_dict')
|
||||||
progress_dict = {'info': s['info_dict'], 'progress': progress_dict}
|
progress_dict = {'info': s['info_dict'], 'progress': progress_dict}
|
||||||
@@ -264,6 +284,10 @@ class FileDownloader(object):
|
|||||||
progress_template.get('download-title') or 'yt-dlp %(progress._default_template)s',
|
progress_template.get('download-title') or 'yt-dlp %(progress._default_template)s',
|
||||||
progress_dict))
|
progress_dict))
|
||||||
|
|
||||||
|
def _format_progress(self, *args, **kwargs):
|
||||||
|
return self.ydl._format_text(
|
||||||
|
self._multiline.stream, self._multiline.allow_colors, *args, **kwargs)
|
||||||
|
|
||||||
def report_progress(self, s):
|
def report_progress(self, s):
|
||||||
if s['status'] == 'finished':
|
if s['status'] == 'finished':
|
||||||
if self.params.get('noprogress'):
|
if self.params.get('noprogress'):
|
||||||
@@ -276,8 +300,7 @@ class FileDownloader(object):
|
|||||||
s['_elapsed_str'] = self.format_seconds(s['elapsed'])
|
s['_elapsed_str'] = self.format_seconds(s['elapsed'])
|
||||||
msg_template += ' in %(_elapsed_str)s'
|
msg_template += ' in %(_elapsed_str)s'
|
||||||
s['_percent_str'] = self.format_percent(100)
|
s['_percent_str'] = self.format_percent(100)
|
||||||
s['_default_template'] = msg_template % s
|
self._report_progress_status(s, msg_template)
|
||||||
self._report_progress_status(s)
|
|
||||||
return
|
return
|
||||||
|
|
||||||
if s['status'] != 'downloading':
|
if s['status'] != 'downloading':
|
||||||
@@ -286,7 +309,7 @@ class FileDownloader(object):
|
|||||||
if s.get('eta') is not None:
|
if s.get('eta') is not None:
|
||||||
s['_eta_str'] = self.format_eta(s['eta'])
|
s['_eta_str'] = self.format_eta(s['eta'])
|
||||||
else:
|
else:
|
||||||
s['_eta_str'] = 'Unknown ETA'
|
s['_eta_str'] = 'Unknown'
|
||||||
|
|
||||||
if s.get('total_bytes') and s.get('downloaded_bytes') is not None:
|
if s.get('total_bytes') and s.get('downloaded_bytes') is not None:
|
||||||
s['_percent_str'] = self.format_percent(100 * s['downloaded_bytes'] / s['total_bytes'])
|
s['_percent_str'] = self.format_percent(100 * s['downloaded_bytes'] / s['total_bytes'])
|
||||||
@@ -318,13 +341,12 @@ class FileDownloader(object):
|
|||||||
else:
|
else:
|
||||||
msg_template = '%(_downloaded_bytes_str)s at %(_speed_str)s'
|
msg_template = '%(_downloaded_bytes_str)s at %(_speed_str)s'
|
||||||
else:
|
else:
|
||||||
msg_template = '%(_percent_str)s % at %(_speed_str)s ETA %(_eta_str)s'
|
msg_template = '%(_percent_str)s at %(_speed_str)s ETA %(_eta_str)s'
|
||||||
if s.get('fragment_index') and s.get('fragment_count'):
|
if s.get('fragment_index') and s.get('fragment_count'):
|
||||||
msg_template += ' (frag %(fragment_index)s/%(fragment_count)s)'
|
msg_template += ' (frag %(fragment_index)s/%(fragment_count)s)'
|
||||||
elif s.get('fragment_index'):
|
elif s.get('fragment_index'):
|
||||||
msg_template += ' (frag %(fragment_index)s)'
|
msg_template += ' (frag %(fragment_index)s)'
|
||||||
s['_default_template'] = msg_template % s
|
self._report_progress_status(s, msg_template)
|
||||||
self._report_progress_status(s)
|
|
||||||
|
|
||||||
def report_resuming_byte(self, resume_len):
|
def report_resuming_byte(self, resume_len):
|
||||||
"""Report attempt to resume at given byte."""
|
"""Report attempt to resume at given byte."""
|
||||||
|
|||||||
@@ -443,8 +443,7 @@ class FFmpegFD(ExternalFD):
|
|||||||
if info_dict.get('requested_formats') or protocol == 'http_dash_segments':
|
if info_dict.get('requested_formats') or protocol == 'http_dash_segments':
|
||||||
for (i, fmt) in enumerate(info_dict.get('requested_formats') or [info_dict]):
|
for (i, fmt) in enumerate(info_dict.get('requested_formats') or [info_dict]):
|
||||||
stream_number = fmt.get('manifest_stream_number', 0)
|
stream_number = fmt.get('manifest_stream_number', 0)
|
||||||
a_or_v = 'a' if fmt.get('acodec') != 'none' else 'v'
|
args.extend(['-map', f'{i}:{stream_number}'])
|
||||||
args.extend(['-map', f'{i}:{a_or_v}:{stream_number}'])
|
|
||||||
|
|
||||||
if self.params.get('test', False):
|
if self.params.get('test', False):
|
||||||
args += ['-fs', compat_str(self._TEST_FILE_SIZE)]
|
args += ['-fs', compat_str(self._TEST_FILE_SIZE)]
|
||||||
|
|||||||
@@ -77,6 +77,15 @@ class HlsFD(FragmentFD):
|
|||||||
message = ('The stream has AES-128 encryption and neither ffmpeg nor pycryptodomex are available; '
|
message = ('The stream has AES-128 encryption and neither ffmpeg nor pycryptodomex are available; '
|
||||||
'Decryption will be performed natively, but will be extremely slow')
|
'Decryption will be performed natively, but will be extremely slow')
|
||||||
if not can_download:
|
if not can_download:
|
||||||
|
has_drm = re.search('|'.join([
|
||||||
|
r'#EXT-X-FAXS-CM:', # Adobe Flash Access
|
||||||
|
r'#EXT-X-(?:SESSION-)?KEY:.*?URI="skd://', # Apple FairPlay
|
||||||
|
]), s)
|
||||||
|
if has_drm and not self.params.get('allow_unplayable_formats'):
|
||||||
|
self.report_error(
|
||||||
|
'This video is DRM protected; Try selecting another format with --format or '
|
||||||
|
'add --check-formats to automatically fallback to the next best format')
|
||||||
|
return False
|
||||||
message = message or 'Unsupported features have been detected'
|
message = message or 'Unsupported features have been detected'
|
||||||
fd = FFmpegFD(self.ydl, self.params)
|
fd = FFmpegFD(self.ydl, self.params)
|
||||||
self.report_warning(f'{message}; extraction will be delegated to {fd.get_basename()}')
|
self.report_warning(f'{message}; extraction will be delegated to {fd.get_basename()}')
|
||||||
|
|||||||
@@ -114,8 +114,8 @@ body > figure > img {
|
|||||||
fragment_base_url = info_dict.get('fragment_base_url')
|
fragment_base_url = info_dict.get('fragment_base_url')
|
||||||
fragments = info_dict['fragments'][:1] if self.params.get(
|
fragments = info_dict['fragments'][:1] if self.params.get(
|
||||||
'test', False) else info_dict['fragments']
|
'test', False) else info_dict['fragments']
|
||||||
title = info_dict['title']
|
title = info_dict.get('title', info_dict['format_id'])
|
||||||
origin = info_dict['webpage_url']
|
origin = info_dict.get('webpage_url', info_dict['url'])
|
||||||
|
|
||||||
ctx = {
|
ctx = {
|
||||||
'filename': filename,
|
'filename': filename,
|
||||||
|
|||||||
@@ -1,55 +1,86 @@
|
|||||||
|
# coding: utf-8
|
||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
import json
|
import json
|
||||||
|
|
||||||
from .common import InfoExtractor
|
from .common import InfoExtractor
|
||||||
|
from ..utils import (
|
||||||
|
try_get,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class AlJazeeraIE(InfoExtractor):
|
class AlJazeeraIE(InfoExtractor):
|
||||||
_VALID_URL = r'https?://(?:www\.)?aljazeera\.com/(?P<type>program/[^/]+|(?:feature|video)s)/\d{4}/\d{1,2}/\d{1,2}/(?P<id>[^/?&#]+)'
|
_VALID_URL = r'https?://(?P<base>\w+\.aljazeera\.\w+)/(?P<type>programs?/[^/]+|(?:feature|video|new)s)?/\d{4}/\d{1,2}/\d{1,2}/(?P<id>[^/?&#]+)'
|
||||||
|
|
||||||
_TESTS = [{
|
_TESTS = [{
|
||||||
'url': 'https://www.aljazeera.com/program/episode/2014/9/19/deliverance',
|
'url': 'https://balkans.aljazeera.net/videos/2021/11/6/pojedini-domovi-u-sarajevu-jos-pod-vodom-mjestanima-se-dostavlja-hrana',
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
'id': '3792260579001',
|
'id': '6280641530001',
|
||||||
'ext': 'mp4',
|
'ext': 'mp4',
|
||||||
'title': 'The Slum - Episode 1: Deliverance',
|
'title': 'Pojedini domovi u Sarajevu još pod vodom, mještanima se dostavlja hrana',
|
||||||
'description': 'As a birth attendant advocating for family planning, Remy is on the frontline of Tondo\'s battle with overcrowding.',
|
'timestamp': 1636219149,
|
||||||
'uploader_id': '665003303001',
|
'description': 'U sarajevskim naseljima Rajlovac i Reljevo stambeni objekti, ali i industrijska postrojenja i dalje su pod vodom.',
|
||||||
'timestamp': 1411116829,
|
'upload_date': '20211106',
|
||||||
'upload_date': '20140919',
|
}
|
||||||
|
}, {
|
||||||
|
'url': 'https://balkans.aljazeera.net/videos/2021/11/6/djokovic-usao-u-finale-mastersa-u-parizu',
|
||||||
|
'info_dict': {
|
||||||
|
'id': '6280654936001',
|
||||||
|
'ext': 'mp4',
|
||||||
|
'title': 'Đoković ušao u finale Mastersa u Parizu',
|
||||||
|
'timestamp': 1636221686,
|
||||||
|
'description': 'Novak Đoković je u polufinalu Mastersa u Parizu nakon preokreta pobijedio Poljaka Huberta Hurkacza.',
|
||||||
|
'upload_date': '20211106',
|
||||||
},
|
},
|
||||||
'add_ie': ['BrightcoveNew'],
|
|
||||||
'skip': 'Not accessible from Travis CI server',
|
|
||||||
}, {
|
|
||||||
'url': 'https://www.aljazeera.com/videos/2017/5/11/sierra-leone-709-carat-diamond-to-be-auctioned-off',
|
|
||||||
'only_matching': True,
|
|
||||||
}, {
|
|
||||||
'url': 'https://www.aljazeera.com/features/2017/8/21/transforming-pakistans-buses-into-art',
|
|
||||||
'only_matching': True,
|
|
||||||
}]
|
}]
|
||||||
BRIGHTCOVE_URL_TEMPLATE = 'http://players.brightcove.net/%s/%s_default/index.html?videoId=%s'
|
BRIGHTCOVE_URL_RE = r'https?://players.brightcove.net/(?P<account>\d+)/(?P<player_id>[a-zA-Z0-9]+)_(?P<embed>[^/]+)/index.html\?videoId=(?P<id>\d+)'
|
||||||
|
|
||||||
def _real_extract(self, url):
|
def _real_extract(self, url):
|
||||||
post_type, name = self._match_valid_url(url).groups()
|
base, post_type, id = self._match_valid_url(url).groups()
|
||||||
|
wp = {
|
||||||
|
'balkans.aljazeera.net': 'ajb',
|
||||||
|
'chinese.aljazeera.net': 'chinese',
|
||||||
|
'mubasher.aljazeera.net': 'ajm',
|
||||||
|
}.get(base) or 'aje'
|
||||||
post_type = {
|
post_type = {
|
||||||
'features': 'post',
|
'features': 'post',
|
||||||
'program': 'episode',
|
'program': 'episode',
|
||||||
|
'programs': 'episode',
|
||||||
'videos': 'video',
|
'videos': 'video',
|
||||||
|
'news': 'news',
|
||||||
}[post_type.split('/')[0]]
|
}[post_type.split('/')[0]]
|
||||||
video = self._download_json(
|
video = self._download_json(
|
||||||
'https://www.aljazeera.com/graphql', name, query={
|
f'https://{base}/graphql', id, query={
|
||||||
|
'wp-site': wp,
|
||||||
'operationName': 'ArchipelagoSingleArticleQuery',
|
'operationName': 'ArchipelagoSingleArticleQuery',
|
||||||
'variables': json.dumps({
|
'variables': json.dumps({
|
||||||
'name': name,
|
'name': id,
|
||||||
'postType': post_type,
|
'postType': post_type,
|
||||||
}),
|
}),
|
||||||
}, headers={
|
}, headers={
|
||||||
'wp-site': 'aje',
|
'wp-site': wp,
|
||||||
})['data']['article']['video']
|
})
|
||||||
video_id = video['id']
|
video = try_get(video, lambda x: x['data']['article']['video']) or {}
|
||||||
account_id = video.get('accountId') or '665003303001'
|
video_id = video.get('id')
|
||||||
player_id = video.get('playerId') or 'BkeSH5BDb'
|
account = video.get('accountId') or '911432371001'
|
||||||
return self.url_result(
|
player_id = video.get('playerId') or 'csvTfAlKW'
|
||||||
self.BRIGHTCOVE_URL_TEMPLATE % (account_id, player_id, video_id),
|
embed = 'default'
|
||||||
'BrightcoveNew', video_id)
|
|
||||||
|
if video_id is None:
|
||||||
|
webpage = self._download_webpage(url, id)
|
||||||
|
|
||||||
|
account, player_id, embed, video_id = self._search_regex(self.BRIGHTCOVE_URL_RE, webpage, 'video id',
|
||||||
|
group=(1, 2, 3, 4), default=(None, None, None, None))
|
||||||
|
|
||||||
|
if video_id is None:
|
||||||
|
return {
|
||||||
|
'_type': 'url_transparent',
|
||||||
|
'url': url,
|
||||||
|
'ie_key': 'Generic'
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
'_type': 'url_transparent',
|
||||||
|
'url': f'https://players.brightcove.net/{account}/{player_id}_{embed}/index.html?videoId={video_id}',
|
||||||
|
'ie_key': 'BrightcoveNew'
|
||||||
|
}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ from ..utils import int_or_none
|
|||||||
|
|
||||||
|
|
||||||
class AmazonStoreIE(InfoExtractor):
|
class AmazonStoreIE(InfoExtractor):
|
||||||
_VALID_URL = r'(?:https?://)(?:www\.)?amazon\.(?:[a-z]{2,3})(?:\.[a-z]{2})?/[^/]*/?(?:dp|gp/product)/(?P<id>[^/&#$?]+)'
|
_VALID_URL = r'https?://(?:www\.)?amazon\.(?:[a-z]{2,3})(?:\.[a-z]{2})?/(?:[^/]+/)?(?:dp|gp/product)/(?P<id>[^/&#$?]+)'
|
||||||
|
|
||||||
_TESTS = [{
|
_TESTS = [{
|
||||||
'url': 'https://www.amazon.co.uk/dp/B098XNCHLD/',
|
'url': 'https://www.amazon.co.uk/dp/B098XNCHLD/',
|
||||||
|
|||||||
+31
-15
@@ -388,7 +388,13 @@ class ARDIE(InfoExtractor):
|
|||||||
|
|
||||||
|
|
||||||
class ARDBetaMediathekIE(ARDMediathekBaseIE):
|
class ARDBetaMediathekIE(ARDMediathekBaseIE):
|
||||||
_VALID_URL = r'https://(?:(?:beta|www)\.)?ardmediathek\.de/(?P<client>[^/]+)/(?P<mode>player|live|video|sendung|sammlung)/(?P<display_id>(?:[^/]+/)*)(?P<video_id>[a-zA-Z0-9]+)'
|
_VALID_URL = r'''(?x)https://
|
||||||
|
(?:(?:beta|www)\.)?ardmediathek\.de/
|
||||||
|
(?:(?P<client>[^/]+)/)?
|
||||||
|
(?:player|live|video|(?P<playlist>sendung|sammlung))/
|
||||||
|
(?:(?P<display_id>[^?#]+)/)?
|
||||||
|
(?P<id>(?(playlist)|Y3JpZDovL)[a-zA-Z0-9]+)'''
|
||||||
|
|
||||||
_TESTS = [{
|
_TESTS = [{
|
||||||
'url': 'https://www.ardmediathek.de/mdr/video/die-robuste-roswita/Y3JpZDovL21kci5kZS9iZWl0cmFnL2Ntcy84MWMxN2MzZC0wMjkxLTRmMzUtODk4ZS0wYzhlOWQxODE2NGI/',
|
'url': 'https://www.ardmediathek.de/mdr/video/die-robuste-roswita/Y3JpZDovL21kci5kZS9iZWl0cmFnL2Ntcy84MWMxN2MzZC0wMjkxLTRmMzUtODk4ZS0wYzhlOWQxODE2NGI/',
|
||||||
'md5': 'a1dc75a39c61601b980648f7c9f9f71d',
|
'md5': 'a1dc75a39c61601b980648f7c9f9f71d',
|
||||||
@@ -403,6 +409,18 @@ class ARDBetaMediathekIE(ARDMediathekBaseIE):
|
|||||||
'upload_date': '20200805',
|
'upload_date': '20200805',
|
||||||
'ext': 'mp4',
|
'ext': 'mp4',
|
||||||
},
|
},
|
||||||
|
'skip': 'Error',
|
||||||
|
}, {
|
||||||
|
'url': 'https://www.ardmediathek.de/video/tagesschau-oder-tagesschau-20-00-uhr/das-erste/Y3JpZDovL2Rhc2Vyc3RlLmRlL3RhZ2Vzc2NoYXUvZmM4ZDUxMjgtOTE0ZC00Y2MzLTgzNzAtNDZkNGNiZWJkOTll',
|
||||||
|
'md5': 'f1837e563323b8a642a8ddeff0131f51',
|
||||||
|
'info_dict': {
|
||||||
|
'id': '10049223',
|
||||||
|
'ext': 'mp4',
|
||||||
|
'title': 'tagesschau, 20:00 Uhr',
|
||||||
|
'timestamp': 1636398000,
|
||||||
|
'description': 'md5:39578c7b96c9fe50afdf5674ad985e6b',
|
||||||
|
'upload_date': '20211108',
|
||||||
|
},
|
||||||
}, {
|
}, {
|
||||||
'url': 'https://beta.ardmediathek.de/ard/video/Y3JpZDovL2Rhc2Vyc3RlLmRlL3RhdG9ydC9mYmM4NGM1NC0xNzU4LTRmZGYtYWFhZS0wYzcyZTIxNGEyMDE',
|
'url': 'https://beta.ardmediathek.de/ard/video/Y3JpZDovL2Rhc2Vyc3RlLmRlL3RhdG9ydC9mYmM4NGM1NC0xNzU4LTRmZGYtYWFhZS0wYzcyZTIxNGEyMDE',
|
||||||
'only_matching': True,
|
'only_matching': True,
|
||||||
@@ -426,6 +444,12 @@ class ARDBetaMediathekIE(ARDMediathekBaseIE):
|
|||||||
# playlist of type 'sammlung'
|
# playlist of type 'sammlung'
|
||||||
'url': 'https://www.ardmediathek.de/ard/sammlung/team-muenster/5JpTzLSbWUAK8184IOvEir/',
|
'url': 'https://www.ardmediathek.de/ard/sammlung/team-muenster/5JpTzLSbWUAK8184IOvEir/',
|
||||||
'only_matching': True,
|
'only_matching': True,
|
||||||
|
}, {
|
||||||
|
'url': 'https://www.ardmediathek.de/video/coronavirus-update-ndr-info/astrazeneca-kurz-lockdown-und-pims-syndrom-81/ndr/Y3JpZDovL25kci5kZS84NzE0M2FjNi0wMWEwLTQ5ODEtOTE5NS1mOGZhNzdhOTFmOTI/',
|
||||||
|
'only_matching': True,
|
||||||
|
}, {
|
||||||
|
'url': 'https://www.ardmediathek.de/ard/player/Y3JpZDovL3dkci5kZS9CZWl0cmFnLWQ2NDJjYWEzLTMwZWYtNGI4NS1iMTI2LTU1N2UxYTcxOGIzOQ/tatort-duo-koeln-leipzig-ihr-kinderlein-kommet',
|
||||||
|
'only_matching': True,
|
||||||
}]
|
}]
|
||||||
|
|
||||||
def _ARD_load_playlist_snipped(self, playlist_id, display_id, client, mode, pageNumber):
|
def _ARD_load_playlist_snipped(self, playlist_id, display_id, client, mode, pageNumber):
|
||||||
@@ -525,20 +549,12 @@ class ARDBetaMediathekIE(ARDMediathekBaseIE):
|
|||||||
return self.playlist_result(entries, playlist_title=display_id)
|
return self.playlist_result(entries, playlist_title=display_id)
|
||||||
|
|
||||||
def _real_extract(self, url):
|
def _real_extract(self, url):
|
||||||
mobj = self._match_valid_url(url)
|
video_id, display_id, playlist_type, client = self._match_valid_url(url).group(
|
||||||
video_id = mobj.group('video_id')
|
'id', 'display_id', 'playlist', 'client')
|
||||||
display_id = mobj.group('display_id')
|
display_id, client = display_id or video_id, client or 'ard'
|
||||||
if display_id:
|
|
||||||
display_id = display_id.rstrip('/')
|
|
||||||
if not display_id:
|
|
||||||
display_id = video_id
|
|
||||||
|
|
||||||
if mobj.group('mode') in ('sendung', 'sammlung'):
|
if playlist_type:
|
||||||
# this is a playlist-URL
|
return self._ARD_extract_playlist(url, video_id, display_id, client, playlist_type)
|
||||||
return self._ARD_extract_playlist(
|
|
||||||
url, video_id, display_id,
|
|
||||||
mobj.group('client'),
|
|
||||||
mobj.group('mode'))
|
|
||||||
|
|
||||||
player_page = self._download_json(
|
player_page = self._download_json(
|
||||||
'https://api.ardmediathek.de/public-gateway',
|
'https://api.ardmediathek.de/public-gateway',
|
||||||
@@ -574,7 +590,7 @@ class ARDBetaMediathekIE(ARDMediathekBaseIE):
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}''' % (mobj.group('client'), video_id),
|
}''' % (client, video_id),
|
||||||
}).encode(), headers={
|
}).encode(), headers={
|
||||||
'Content-Type': 'application/json'
|
'Content-Type': 'application/json'
|
||||||
})['data']['playerPage']
|
})['data']['playerPage']
|
||||||
|
|||||||
+14
-4
@@ -451,9 +451,10 @@ class BBCCoUkIE(InfoExtractor):
|
|||||||
playlist = self._download_json(
|
playlist = self._download_json(
|
||||||
'http://www.bbc.co.uk/programmes/%s/playlist.json' % playlist_id,
|
'http://www.bbc.co.uk/programmes/%s/playlist.json' % playlist_id,
|
||||||
playlist_id, 'Downloading playlist JSON')
|
playlist_id, 'Downloading playlist JSON')
|
||||||
|
formats = []
|
||||||
|
subtitles = {}
|
||||||
|
|
||||||
version = playlist.get('defaultAvailableVersion')
|
for version in playlist.get('allAvailableVersions', []):
|
||||||
if version:
|
|
||||||
smp_config = version['smpConfig']
|
smp_config = version['smpConfig']
|
||||||
title = smp_config['title']
|
title = smp_config['title']
|
||||||
description = smp_config['summary']
|
description = smp_config['summary']
|
||||||
@@ -463,8 +464,17 @@ class BBCCoUkIE(InfoExtractor):
|
|||||||
continue
|
continue
|
||||||
programme_id = item.get('vpid')
|
programme_id = item.get('vpid')
|
||||||
duration = int_or_none(item.get('duration'))
|
duration = int_or_none(item.get('duration'))
|
||||||
formats, subtitles = self._download_media_selector(programme_id)
|
version_formats, version_subtitles = self._download_media_selector(programme_id)
|
||||||
return programme_id, title, description, duration, formats, subtitles
|
types = version['types']
|
||||||
|
for f in version_formats:
|
||||||
|
f['format_note'] = ', '.join(types)
|
||||||
|
if any('AudioDescribed' in x for x in types):
|
||||||
|
f['language_preference'] = -10
|
||||||
|
formats += version_formats
|
||||||
|
for tag, subformats in (version_subtitles or {}).items():
|
||||||
|
subtitles.setdefault(tag, []).extend(subformats)
|
||||||
|
|
||||||
|
return programme_id, title, description, duration, formats, subtitles
|
||||||
except ExtractorError as ee:
|
except ExtractorError as ee:
|
||||||
if not (isinstance(ee.cause, compat_HTTPError) and ee.cause.code == 404):
|
if not (isinstance(ee.cause, compat_HTTPError) and ee.cause.code == 404):
|
||||||
raise
|
raise
|
||||||
|
|||||||
@@ -346,7 +346,8 @@ class BiliBiliIE(InfoExtractor):
|
|||||||
def _extract_anthology_entries(self, bv_id, video_id, webpage):
|
def _extract_anthology_entries(self, bv_id, video_id, webpage):
|
||||||
title = self._html_search_regex(
|
title = self._html_search_regex(
|
||||||
(r'<h1[^>]+\btitle=(["\'])(?P<title>(?:(?!\1).)+)\1',
|
(r'<h1[^>]+\btitle=(["\'])(?P<title>(?:(?!\1).)+)\1',
|
||||||
r'(?s)<h1[^>]*>(?P<title>.+?)</h1>'), webpage, 'title',
|
r'(?s)<h1[^>]*>(?P<title>.+?)</h1>',
|
||||||
|
r'<title>(?P<title>.+?)</title>'), webpage, 'title',
|
||||||
group='title')
|
group='title')
|
||||||
json_data = self._download_json(
|
json_data = self._download_json(
|
||||||
f'https://api.bilibili.com/x/player/pagelist?bvid={bv_id}&jsonp=jsonp',
|
f'https://api.bilibili.com/x/player/pagelist?bvid={bv_id}&jsonp=jsonp',
|
||||||
|
|||||||
@@ -0,0 +1,54 @@
|
|||||||
|
# coding: utf-8
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
import re
|
||||||
|
|
||||||
|
from ..utils import (
|
||||||
|
mimetype2ext,
|
||||||
|
parse_duration,
|
||||||
|
parse_qs,
|
||||||
|
str_or_none,
|
||||||
|
traverse_obj,
|
||||||
|
)
|
||||||
|
from .common import InfoExtractor
|
||||||
|
|
||||||
|
|
||||||
|
class BloggerIE(InfoExtractor):
|
||||||
|
IE_NAME = 'blogger.com'
|
||||||
|
_VALID_URL = r'https?://(?:www\.)?blogger\.com/video\.g\?token=(?P<id>.+)'
|
||||||
|
_VALID_EMBED = r'''<iframe[^>]+src=["']((?:https?:)?//(?:www\.)?blogger\.com/video\.g\?token=[^"']+)["']'''
|
||||||
|
_TESTS = [{
|
||||||
|
'url': 'https://www.blogger.com/video.g?token=AD6v5dzEe9hfcARr5Hlq1WTkYy6t-fXH3BBahVhGvVHe5szdEUBEloSEDSTA8-b111089KbfWuBvTN7fnbxMtymsHhXAXwVvyzHH4Qch2cfLQdGxKQrrEuFpC1amSl_9GuLWODjPgw',
|
||||||
|
'md5': 'f1bc19b6ea1b0fd1d81e84ca9ec467ac',
|
||||||
|
'info_dict': {
|
||||||
|
'id': 'BLOGGER-video-3c740e3a49197e16-796',
|
||||||
|
'title': 'BLOGGER-video-3c740e3a49197e16-796',
|
||||||
|
'ext': 'mp4',
|
||||||
|
'thumbnail': r're:^https?://.*',
|
||||||
|
'duration': 76.068,
|
||||||
|
}
|
||||||
|
}]
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _extract_urls(webpage):
|
||||||
|
return re.findall(BloggerIE._VALID_EMBED, webpage)
|
||||||
|
|
||||||
|
def _real_extract(self, url):
|
||||||
|
token_id = self._match_id(url)
|
||||||
|
webpage = self._download_webpage(url, token_id)
|
||||||
|
data_json = self._search_regex(r'var\s+VIDEO_CONFIG\s*=\s*(\{.*)', webpage, 'JSON data')
|
||||||
|
data = self._parse_json(data_json.encode('utf-8').decode('unicode_escape'), token_id)
|
||||||
|
streams = data['streams']
|
||||||
|
formats = [{
|
||||||
|
'ext': mimetype2ext(traverse_obj(parse_qs(stream['play_url']), ('mime', 0))),
|
||||||
|
'url': stream['play_url'],
|
||||||
|
'format_id': str_or_none(stream.get('format_id')),
|
||||||
|
} for stream in streams]
|
||||||
|
|
||||||
|
return {
|
||||||
|
'id': data.get('iframe_id', token_id),
|
||||||
|
'title': data.get('iframe_id', token_id),
|
||||||
|
'formats': formats,
|
||||||
|
'thumbnail': data.get('thumbnail'),
|
||||||
|
'duration': parse_duration(traverse_obj(parse_qs(streams[0]['play_url']), ('dur', 0))),
|
||||||
|
}
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from .common import InfoExtractor
|
||||||
|
|
||||||
|
|
||||||
|
class BreitBartIE(InfoExtractor):
|
||||||
|
_VALID_URL = r'https?:\/\/(?:www\.)breitbart.com/videos/v/(?P<id>[^/]+)'
|
||||||
|
_TESTS = [{
|
||||||
|
'url': 'https://www.breitbart.com/videos/v/5cOz1yup/?pl=Ij6NDOji',
|
||||||
|
'md5': '0aa6d1d6e183ac5ca09207fe49f17ade',
|
||||||
|
'info_dict': {
|
||||||
|
'id': '5cOz1yup',
|
||||||
|
'ext': 'mp4',
|
||||||
|
'title': 'Watch \u2013 Clyburn: Statues in Congress Have to Go Because they Are Honoring Slavery',
|
||||||
|
'description': 'md5:bac35eb0256d1cb17f517f54c79404d5',
|
||||||
|
'thumbnail': 'https://cdn.jwplayer.com/thumbs/5cOz1yup-1920.jpg',
|
||||||
|
'age_limit': 0,
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
'url': 'https://www.breitbart.com/videos/v/eaiZjVOn/',
|
||||||
|
'only_matching': True,
|
||||||
|
}]
|
||||||
|
|
||||||
|
def _real_extract(self, url):
|
||||||
|
video_id = self._match_id(url)
|
||||||
|
webpage = self._download_webpage(url, video_id)
|
||||||
|
|
||||||
|
formats = self._extract_m3u8_formats(f'https://cdn.jwplayer.com/manifests/{video_id}.m3u8', video_id, ext='mp4')
|
||||||
|
self._sort_formats(formats)
|
||||||
|
return {
|
||||||
|
'id': video_id,
|
||||||
|
'title': self._og_search_title(
|
||||||
|
webpage, default=None) or self._html_search_regex(
|
||||||
|
r'(?s)<title>(.*?)</title>', webpage, 'video title'),
|
||||||
|
'description': self._og_search_description(webpage),
|
||||||
|
'thumbnail': self._og_search_thumbnail(webpage),
|
||||||
|
'age_limit': self._rta_search(webpage),
|
||||||
|
'formats': formats
|
||||||
|
}
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
# coding: utf-8
|
||||||
|
from .common import InfoExtractor
|
||||||
|
|
||||||
|
|
||||||
|
class CableAVIE(InfoExtractor):
|
||||||
|
_VALID_URL = r'https://cableav\.tv/(?P<id>[a-zA-Z0-9]+)'
|
||||||
|
_TESTS = [{
|
||||||
|
'url': 'https://cableav.tv/lS4iR9lWjN8/',
|
||||||
|
'md5': '7e3fe5e49d61c4233b7f5b0f69b15e18',
|
||||||
|
'info_dict': {
|
||||||
|
'id': 'lS4iR9lWjN8',
|
||||||
|
'ext': 'mp4',
|
||||||
|
'title': '國產麻豆AV 叮叮映畫 DDF001 情欲小說家 - CableAV',
|
||||||
|
'description': '國產AV 480p, 720p 国产麻豆AV 叮叮映画 DDF001 情欲小说家',
|
||||||
|
'thumbnail': r're:^https?://.*\.jpg$',
|
||||||
|
}
|
||||||
|
}]
|
||||||
|
|
||||||
|
def _real_extract(self, url):
|
||||||
|
video_id = self._match_id(url)
|
||||||
|
webpage = self._download_webpage(url, video_id)
|
||||||
|
|
||||||
|
video_url = self._og_search_video_url(webpage, secure=False)
|
||||||
|
|
||||||
|
formats = self._extract_m3u8_formats(video_url, video_id, 'mp4')
|
||||||
|
self._sort_formats(formats)
|
||||||
|
|
||||||
|
return {
|
||||||
|
'id': video_id,
|
||||||
|
'title': self._og_search_title(webpage),
|
||||||
|
'description': self._og_search_description(webpage),
|
||||||
|
'thumbnail': self._og_search_thumbnail(webpage),
|
||||||
|
'formats': formats,
|
||||||
|
}
|
||||||
@@ -0,0 +1,98 @@
|
|||||||
|
# coding: utf-8
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from .common import InfoExtractor
|
||||||
|
from ..utils import (
|
||||||
|
clean_html,
|
||||||
|
dict_get,
|
||||||
|
try_get,
|
||||||
|
unified_strdate,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class CanalAlphaIE(InfoExtractor):
|
||||||
|
_VALID_URL = r'https?://(?:www\.)?canalalpha\.ch/play/[^/]+/[^/]+/(?P<id>\d+)/?.*'
|
||||||
|
|
||||||
|
_TESTS = [{
|
||||||
|
'url': 'https://www.canalalpha.ch/play/le-journal/episode/24520/jeudi-28-octobre-2021',
|
||||||
|
'info_dict': {
|
||||||
|
'id': '24520',
|
||||||
|
'ext': 'mp4',
|
||||||
|
'title': 'Jeudi 28 octobre 2021',
|
||||||
|
'description': 'md5:d30c6c3e53f8ad40d405379601973b30',
|
||||||
|
'thumbnail': 'https://static.canalalpha.ch/poster/journal/journal_20211028.jpg',
|
||||||
|
'upload_date': '20211028',
|
||||||
|
'duration': 1125,
|
||||||
|
},
|
||||||
|
'params': {'skip_download': True}
|
||||||
|
}, {
|
||||||
|
'url': 'https://www.canalalpha.ch/play/le-journal/topic/24512/la-poste-fait-de-neuchatel-un-pole-cryptographique',
|
||||||
|
'info_dict': {
|
||||||
|
'id': '24512',
|
||||||
|
'ext': 'mp4',
|
||||||
|
'title': 'La Poste fait de Neuchâtel un pôle cryptographique',
|
||||||
|
'description': 'md5:4ba63ae78a0974d1a53d6703b6e1dedf',
|
||||||
|
'thumbnail': 'https://static.canalalpha.ch/poster/news/news_39712.jpg',
|
||||||
|
'upload_date': '20211028',
|
||||||
|
'duration': 138,
|
||||||
|
},
|
||||||
|
'params': {'skip_download': True}
|
||||||
|
}, {
|
||||||
|
'url': 'https://www.canalalpha.ch/play/eureka/episode/24484/ces-innovations-qui-veulent-rendre-lagriculture-plus-durable',
|
||||||
|
'info_dict': {
|
||||||
|
'id': '24484',
|
||||||
|
'ext': 'mp4',
|
||||||
|
'title': 'Ces innovations qui veulent rendre l’agriculture plus durable',
|
||||||
|
'description': 'md5:3de3f151180684621e85be7c10e4e613',
|
||||||
|
'thumbnail': 'https://static.canalalpha.ch/poster/magazine/magazine_10236.jpg',
|
||||||
|
'upload_date': '20211026',
|
||||||
|
'duration': 360,
|
||||||
|
},
|
||||||
|
'params': {'skip_download': True}
|
||||||
|
}, {
|
||||||
|
'url': 'https://www.canalalpha.ch/play/avec-le-temps/episode/23516/redonner-de-leclat-grace-au-polissage',
|
||||||
|
'info_dict': {
|
||||||
|
'id': '23516',
|
||||||
|
'ext': 'mp4',
|
||||||
|
'title': 'Redonner de l\'éclat grâce au polissage',
|
||||||
|
'description': 'md5:0d8fbcda1a5a4d6f6daa3165402177e1',
|
||||||
|
'thumbnail': 'https://static.canalalpha.ch/poster/magazine/magazine_9990.png',
|
||||||
|
'upload_date': '20210726',
|
||||||
|
'duration': 360,
|
||||||
|
},
|
||||||
|
'params': {'skip_download': True}
|
||||||
|
}]
|
||||||
|
|
||||||
|
def _real_extract(self, url):
|
||||||
|
id = self._match_id(url)
|
||||||
|
webpage = self._download_webpage(url, id)
|
||||||
|
data_json = self._parse_json(self._search_regex(
|
||||||
|
r'window\.__SERVER_STATE__\s?=\s?({(?:(?!};)[^"]|"([^"]|\\")*")+})\s?;',
|
||||||
|
webpage, 'data_json'), id)['1']['data']['data']
|
||||||
|
manifests = try_get(data_json, lambda x: x['video']['manifests'], expected_type=dict) or {}
|
||||||
|
subtitles = {}
|
||||||
|
formats = [{
|
||||||
|
'url': video['$url'],
|
||||||
|
'ext': 'mp4',
|
||||||
|
'width': try_get(video, lambda x: x['res']['width'], expected_type=int),
|
||||||
|
'height': try_get(video, lambda x: x['res']['height'], expected_type=int),
|
||||||
|
} for video in try_get(data_json, lambda x: x['video']['mp4'], expected_type=list) or [] if video.get('$url')]
|
||||||
|
if manifests.get('hls'):
|
||||||
|
m3u8_frmts, m3u8_subs = self._parse_m3u8_formats_and_subtitles(manifests['hls'], id)
|
||||||
|
formats.extend(m3u8_frmts)
|
||||||
|
subtitles = self._merge_subtitles(subtitles, m3u8_subs)
|
||||||
|
if manifests.get('dash'):
|
||||||
|
dash_frmts, dash_subs = self._parse_mpd_formats_and_subtitles(manifests['dash'], id)
|
||||||
|
formats.extend(dash_frmts)
|
||||||
|
subtitles = self._merge_subtitles(subtitles, dash_subs)
|
||||||
|
self._sort_formats(formats)
|
||||||
|
return {
|
||||||
|
'id': id,
|
||||||
|
'title': data_json.get('title').strip(),
|
||||||
|
'description': clean_html(dict_get(data_json, ('longDesc', 'shortDesc'))),
|
||||||
|
'thumbnail': data_json.get('poster'),
|
||||||
|
'upload_date': unified_strdate(dict_get(data_json, ('webPublishAt', 'featuredAt', 'diffusionDate'))),
|
||||||
|
'duration': try_get(data_json, lambda x: x['video']['duration'], expected_type=int),
|
||||||
|
'formats': formats,
|
||||||
|
'subtitles': subtitles,
|
||||||
|
}
|
||||||
@@ -390,7 +390,8 @@ class CBCGemPlaylistIE(InfoExtractor):
|
|||||||
show = match.group('show')
|
show = match.group('show')
|
||||||
show_info = self._download_json(self._API_BASE + show, season_id)
|
show_info = self._download_json(self._API_BASE + show, season_id)
|
||||||
season = int(match.group('season'))
|
season = int(match.group('season'))
|
||||||
season_info = try_get(show_info, lambda x: x['seasons'][season - 1])
|
|
||||||
|
season_info = next((s for s in show_info['seasons'] if s.get('season') == season), None)
|
||||||
|
|
||||||
if season_info is None:
|
if season_info is None:
|
||||||
raise ExtractorError(f'Couldn\'t find season {season} of {show}')
|
raise ExtractorError(f'Couldn\'t find season {season} of {show}')
|
||||||
|
|||||||
@@ -67,7 +67,7 @@ class ChingariBaseIE(InfoExtractor):
|
|||||||
|
|
||||||
|
|
||||||
class ChingariIE(ChingariBaseIE):
|
class ChingariIE(ChingariBaseIE):
|
||||||
_VALID_URL = r'(?:https?://)(?:www\.)?chingari\.io/share/post\?id=(?P<id>[^&/#?]+)'
|
_VALID_URL = r'https?://(?:www\.)?chingari\.io/share/post\?id=(?P<id>[^&/#?]+)'
|
||||||
_TESTS = [{
|
_TESTS = [{
|
||||||
'url': 'https://chingari.io/share/post?id=612f8f4ce1dc57090e8a7beb',
|
'url': 'https://chingari.io/share/post?id=612f8f4ce1dc57090e8a7beb',
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
@@ -102,7 +102,7 @@ class ChingariIE(ChingariBaseIE):
|
|||||||
|
|
||||||
|
|
||||||
class ChingariUserIE(ChingariBaseIE):
|
class ChingariUserIE(ChingariBaseIE):
|
||||||
_VALID_URL = r'(?:https?://)(?:www\.)?chingari\.io/(?!share/post)(?P<id>[^/?]+)'
|
_VALID_URL = r'https?://(?:www\.)?chingari\.io/(?!share/post)(?P<id>[^/?]+)'
|
||||||
_TESTS = [{
|
_TESTS = [{
|
||||||
'url': 'https://chingari.io/dada1023',
|
'url': 'https://chingari.io/dada1023',
|
||||||
'playlist_mincount': 3,
|
'playlist_mincount': 3,
|
||||||
|
|||||||
+40
-22
@@ -2,6 +2,7 @@
|
|||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
import base64
|
import base64
|
||||||
|
import collections
|
||||||
import datetime
|
import datetime
|
||||||
import hashlib
|
import hashlib
|
||||||
import itertools
|
import itertools
|
||||||
@@ -342,6 +343,7 @@ class InfoExtractor(object):
|
|||||||
series, programme or podcast:
|
series, programme or podcast:
|
||||||
|
|
||||||
series: Title of the series or programme the video episode belongs to.
|
series: Title of the series or programme the video episode belongs to.
|
||||||
|
series_id: Id of the series or programme the video episode belongs to, as a unicode string.
|
||||||
season: Title of the season the video episode belongs to.
|
season: Title of the season the video episode belongs to.
|
||||||
season_number: Number of the season the video episode belongs to, as an integer.
|
season_number: Number of the season the video episode belongs to, as an integer.
|
||||||
season_id: Id of the season the video episode belongs to, as a unicode string.
|
season_id: Id of the season the video episode belongs to, as a unicode string.
|
||||||
@@ -1077,7 +1079,8 @@ class InfoExtractor(object):
|
|||||||
def raise_login_required(
|
def raise_login_required(
|
||||||
self, msg='This video is only available for registered users',
|
self, msg='This video is only available for registered users',
|
||||||
metadata_available=False, method='any'):
|
metadata_available=False, method='any'):
|
||||||
if metadata_available and self.get_param('ignore_no_formats_error'):
|
if metadata_available and (
|
||||||
|
self.get_param('ignore_no_formats_error') or self.get_param('wait_for_video')):
|
||||||
self.report_warning(msg)
|
self.report_warning(msg)
|
||||||
if method is not None:
|
if method is not None:
|
||||||
msg = '%s. %s' % (msg, self._LOGIN_HINTS[method])
|
msg = '%s. %s' % (msg, self._LOGIN_HINTS[method])
|
||||||
@@ -1086,13 +1089,15 @@ class InfoExtractor(object):
|
|||||||
def raise_geo_restricted(
|
def raise_geo_restricted(
|
||||||
self, msg='This video is not available from your location due to geo restriction',
|
self, msg='This video is not available from your location due to geo restriction',
|
||||||
countries=None, metadata_available=False):
|
countries=None, metadata_available=False):
|
||||||
if metadata_available and self.get_param('ignore_no_formats_error'):
|
if metadata_available and (
|
||||||
|
self.get_param('ignore_no_formats_error') or self.get_param('wait_for_video')):
|
||||||
self.report_warning(msg)
|
self.report_warning(msg)
|
||||||
else:
|
else:
|
||||||
raise GeoRestrictedError(msg, countries=countries)
|
raise GeoRestrictedError(msg, countries=countries)
|
||||||
|
|
||||||
def raise_no_formats(self, msg, expected=False, video_id=None):
|
def raise_no_formats(self, msg, expected=False, video_id=None):
|
||||||
if expected and self.get_param('ignore_no_formats_error'):
|
if expected and (
|
||||||
|
self.get_param('ignore_no_formats_error') or self.get_param('wait_for_video')):
|
||||||
self.report_warning(msg, video_id)
|
self.report_warning(msg, video_id)
|
||||||
elif isinstance(msg, ExtractorError):
|
elif isinstance(msg, ExtractorError):
|
||||||
raise msg
|
raise msg
|
||||||
@@ -1450,6 +1455,9 @@ class InfoExtractor(object):
|
|||||||
item_type = e.get('@type')
|
item_type = e.get('@type')
|
||||||
if expected_type is not None and expected_type != item_type:
|
if expected_type is not None and expected_type != item_type:
|
||||||
continue
|
continue
|
||||||
|
rating = traverse_obj(e, ('aggregateRating', 'ratingValue'), expected_type=float_or_none)
|
||||||
|
if rating is not None:
|
||||||
|
info['average_rating'] = rating
|
||||||
if item_type in ('TVEpisode', 'Episode'):
|
if item_type in ('TVEpisode', 'Episode'):
|
||||||
episode_name = unescapeHTML(e.get('name'))
|
episode_name = unescapeHTML(e.get('name'))
|
||||||
info.update({
|
info.update({
|
||||||
@@ -1530,10 +1538,10 @@ class InfoExtractor(object):
|
|||||||
|
|
||||||
default = ('hidden', 'aud_or_vid', 'hasvid', 'ie_pref', 'lang', 'quality',
|
default = ('hidden', 'aud_or_vid', 'hasvid', 'ie_pref', 'lang', 'quality',
|
||||||
'res', 'fps', 'hdr:12', 'codec:vp9.2', 'size', 'br', 'asr',
|
'res', 'fps', 'hdr:12', 'codec:vp9.2', 'size', 'br', 'asr',
|
||||||
'proto', 'ext', 'hasaud', 'source', 'format_id') # These must not be aliases
|
'proto', 'ext', 'hasaud', 'source', 'id') # These must not be aliases
|
||||||
ytdl_default = ('hasaud', 'lang', 'quality', 'tbr', 'filesize', 'vbr',
|
ytdl_default = ('hasaud', 'lang', 'quality', 'tbr', 'filesize', 'vbr',
|
||||||
'height', 'width', 'proto', 'vext', 'abr', 'aext',
|
'height', 'width', 'proto', 'vext', 'abr', 'aext',
|
||||||
'fps', 'fs_approx', 'source', 'format_id')
|
'fps', 'fs_approx', 'source', 'id')
|
||||||
|
|
||||||
settings = {
|
settings = {
|
||||||
'vcodec': {'type': 'ordered', 'regex': True,
|
'vcodec': {'type': 'ordered', 'regex': True,
|
||||||
@@ -1543,7 +1551,7 @@ class InfoExtractor(object):
|
|||||||
'hdr': {'type': 'ordered', 'regex': True, 'field': 'dynamic_range',
|
'hdr': {'type': 'ordered', 'regex': True, 'field': 'dynamic_range',
|
||||||
'order': ['dv', '(hdr)?12', r'(hdr)?10\+', '(hdr)?10', 'hlg', '', 'sdr', None]},
|
'order': ['dv', '(hdr)?12', r'(hdr)?10\+', '(hdr)?10', 'hlg', '', 'sdr', None]},
|
||||||
'proto': {'type': 'ordered', 'regex': True, 'field': 'protocol',
|
'proto': {'type': 'ordered', 'regex': True, 'field': 'protocol',
|
||||||
'order': ['(ht|f)tps', '(ht|f)tp$', 'm3u8.+', '.*dash', 'ws|websocket', '', 'mms|rtsp', 'none', 'f4']},
|
'order': ['(ht|f)tps', '(ht|f)tp$', 'm3u8.*', '.*dash', 'websocket_frag', 'rtmpe?', '', 'mms|rtsp', 'ws|websocket', 'f4']},
|
||||||
'vext': {'type': 'ordered', 'field': 'video_ext',
|
'vext': {'type': 'ordered', 'field': 'video_ext',
|
||||||
'order': ('mp4', 'webm', 'flv', '', 'none'),
|
'order': ('mp4', 'webm', 'flv', '', 'none'),
|
||||||
'order_free': ('webm', 'mp4', 'flv', '', 'none')},
|
'order_free': ('webm', 'mp4', 'flv', '', 'none')},
|
||||||
@@ -1578,7 +1586,7 @@ class InfoExtractor(object):
|
|||||||
'res': {'type': 'multiple', 'field': ('height', 'width'),
|
'res': {'type': 'multiple', 'field': ('height', 'width'),
|
||||||
'function': lambda it: (lambda l: min(l) if l else 0)(tuple(filter(None, it)))},
|
'function': lambda it: (lambda l: min(l) if l else 0)(tuple(filter(None, it)))},
|
||||||
|
|
||||||
# Most of these exist only for compatibility reasons
|
# Deprecated
|
||||||
'dimension': {'type': 'alias', 'field': 'res'},
|
'dimension': {'type': 'alias', 'field': 'res'},
|
||||||
'resolution': {'type': 'alias', 'field': 'res'},
|
'resolution': {'type': 'alias', 'field': 'res'},
|
||||||
'extension': {'type': 'alias', 'field': 'ext'},
|
'extension': {'type': 'alias', 'field': 'ext'},
|
||||||
@@ -1587,7 +1595,7 @@ class InfoExtractor(object):
|
|||||||
'video_bitrate': {'type': 'alias', 'field': 'vbr'},
|
'video_bitrate': {'type': 'alias', 'field': 'vbr'},
|
||||||
'audio_bitrate': {'type': 'alias', 'field': 'abr'},
|
'audio_bitrate': {'type': 'alias', 'field': 'abr'},
|
||||||
'framerate': {'type': 'alias', 'field': 'fps'},
|
'framerate': {'type': 'alias', 'field': 'fps'},
|
||||||
'language_preference': {'type': 'alias', 'field': 'lang'}, # not named as 'language' because such a field exists
|
'language_preference': {'type': 'alias', 'field': 'lang'},
|
||||||
'protocol': {'type': 'alias', 'field': 'proto'},
|
'protocol': {'type': 'alias', 'field': 'proto'},
|
||||||
'source_preference': {'type': 'alias', 'field': 'source'},
|
'source_preference': {'type': 'alias', 'field': 'source'},
|
||||||
'filesize_approx': {'type': 'alias', 'field': 'fs_approx'},
|
'filesize_approx': {'type': 'alias', 'field': 'fs_approx'},
|
||||||
@@ -1607,10 +1615,20 @@ class InfoExtractor(object):
|
|||||||
'format_id': {'type': 'alias', 'field': 'id'},
|
'format_id': {'type': 'alias', 'field': 'id'},
|
||||||
}
|
}
|
||||||
|
|
||||||
_order = []
|
def __init__(self, ie, field_preference):
|
||||||
|
self._order = []
|
||||||
|
self.ydl = ie._downloader
|
||||||
|
self.evaluate_params(self.ydl.params, field_preference)
|
||||||
|
if ie.get_param('verbose'):
|
||||||
|
self.print_verbose_info(self.ydl.write_debug)
|
||||||
|
|
||||||
def _get_field_setting(self, field, key):
|
def _get_field_setting(self, field, key):
|
||||||
if field not in self.settings:
|
if field not in self.settings:
|
||||||
|
if key in ('forced', 'priority'):
|
||||||
|
return False
|
||||||
|
self.ydl.deprecation_warning(
|
||||||
|
f'Using arbitrary fields ({field}) for format sorting is deprecated '
|
||||||
|
'and may be removed in a future version')
|
||||||
self.settings[field] = {}
|
self.settings[field] = {}
|
||||||
propObj = self.settings[field]
|
propObj = self.settings[field]
|
||||||
if key not in propObj:
|
if key not in propObj:
|
||||||
@@ -1693,7 +1711,10 @@ class InfoExtractor(object):
|
|||||||
if field is None:
|
if field is None:
|
||||||
continue
|
continue
|
||||||
if self._get_field_setting(field, 'type') == 'alias':
|
if self._get_field_setting(field, 'type') == 'alias':
|
||||||
field = self._get_field_setting(field, 'field')
|
alias, field = field, self._get_field_setting(field, 'field')
|
||||||
|
self.ydl.deprecation_warning(
|
||||||
|
f'Format sorting alias {alias} is deprecated '
|
||||||
|
f'and may be removed in a future version. Please use {field} instead')
|
||||||
reverse = match.group('reverse') is not None
|
reverse = match.group('reverse') is not None
|
||||||
closest = match.group('separator') == '~'
|
closest = match.group('separator') == '~'
|
||||||
limit_text = match.group('limit')
|
limit_text = match.group('limit')
|
||||||
@@ -1797,10 +1818,7 @@ class InfoExtractor(object):
|
|||||||
def _sort_formats(self, formats, field_preference=[]):
|
def _sort_formats(self, formats, field_preference=[]):
|
||||||
if not formats:
|
if not formats:
|
||||||
return
|
return
|
||||||
format_sort = self.FormatSort() # params and to_screen are taken from the downloader
|
format_sort = self.FormatSort(self, field_preference)
|
||||||
format_sort.evaluate_params(self._downloader.params, field_preference)
|
|
||||||
if self.get_param('verbose', False):
|
|
||||||
format_sort.print_verbose_info(self._downloader.write_debug)
|
|
||||||
formats.sort(key=lambda f: format_sort.calculate_preference(f))
|
formats.sort(key=lambda f: format_sort.calculate_preference(f))
|
||||||
|
|
||||||
def _check_formats(self, formats, video_id):
|
def _check_formats(self, formats, video_id):
|
||||||
@@ -2034,10 +2052,10 @@ class InfoExtractor(object):
|
|||||||
video_id=None):
|
video_id=None):
|
||||||
formats, subtitles = [], {}
|
formats, subtitles = [], {}
|
||||||
|
|
||||||
if '#EXT-X-FAXS-CM:' in m3u8_doc: # Adobe Flash Access
|
has_drm = re.search('|'.join([
|
||||||
return formats, subtitles
|
r'#EXT-X-FAXS-CM:', # Adobe Flash Access
|
||||||
|
r'#EXT-X-(?:SESSION-)?KEY:.*?URI="skd://', # Apple FairPlay
|
||||||
has_drm = re.search(r'#EXT-X-(?:SESSION-)?KEY:.*?URI="skd://', m3u8_doc)
|
]), m3u8_doc)
|
||||||
|
|
||||||
def format_url(url):
|
def format_url(url):
|
||||||
return url if re.match(r'^https?://', url) else compat_urlparse.urljoin(m3u8_url, url)
|
return url if re.match(r'^https?://', url) else compat_urlparse.urljoin(m3u8_url, url)
|
||||||
@@ -2648,7 +2666,7 @@ class InfoExtractor(object):
|
|||||||
|
|
||||||
mpd_duration = parse_duration(mpd_doc.get('mediaPresentationDuration'))
|
mpd_duration = parse_duration(mpd_doc.get('mediaPresentationDuration'))
|
||||||
formats, subtitles = [], {}
|
formats, subtitles = [], {}
|
||||||
stream_numbers = {'audio': 0, 'video': 0}
|
stream_numbers = collections.defaultdict(int)
|
||||||
for period in mpd_doc.findall(_add_ns('Period')):
|
for period in mpd_doc.findall(_add_ns('Period')):
|
||||||
period_duration = parse_duration(period.get('duration')) or mpd_duration
|
period_duration = parse_duration(period.get('duration')) or mpd_duration
|
||||||
period_ms_info = extract_multisegment_info(period, {
|
period_ms_info = extract_multisegment_info(period, {
|
||||||
@@ -2714,10 +2732,8 @@ class InfoExtractor(object):
|
|||||||
'format_note': 'DASH %s' % content_type,
|
'format_note': 'DASH %s' % content_type,
|
||||||
'filesize': filesize,
|
'filesize': filesize,
|
||||||
'container': mimetype2ext(mime_type) + '_dash',
|
'container': mimetype2ext(mime_type) + '_dash',
|
||||||
'manifest_stream_number': stream_numbers[content_type]
|
|
||||||
}
|
}
|
||||||
f.update(parse_codecs(codecs))
|
f.update(parse_codecs(codecs))
|
||||||
stream_numbers[content_type] += 1
|
|
||||||
elif content_type == 'text':
|
elif content_type == 'text':
|
||||||
f = {
|
f = {
|
||||||
'ext': mimetype2ext(mime_type),
|
'ext': mimetype2ext(mime_type),
|
||||||
@@ -2884,7 +2900,9 @@ class InfoExtractor(object):
|
|||||||
else:
|
else:
|
||||||
# Assuming direct URL to unfragmented media.
|
# Assuming direct URL to unfragmented media.
|
||||||
f['url'] = base_url
|
f['url'] = base_url
|
||||||
if content_type in ('video', 'audio') or mime_type == 'image/jpeg':
|
if content_type in ('video', 'audio', 'image/jpeg'):
|
||||||
|
f['manifest_stream_number'] = stream_numbers[f['url']]
|
||||||
|
stream_numbers[f['url']] += 1
|
||||||
formats.append(f)
|
formats.append(f)
|
||||||
elif content_type == 'text':
|
elif content_type == 'text':
|
||||||
subtitles.setdefault(lang or 'und', []).append(f)
|
subtitles.setdefault(lang or 'und', []).append(f)
|
||||||
|
|||||||
@@ -0,0 +1,40 @@
|
|||||||
|
# coding: utf-8
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from .common import InfoExtractor
|
||||||
|
from ..utils import unified_strdate
|
||||||
|
|
||||||
|
|
||||||
|
class CozyTVIE(InfoExtractor):
|
||||||
|
_VALID_URL = r'https?://(?:www\.)?cozy\.tv/(?P<uploader>[^/]+)/replays/(?P<id>[^/$#&?]+)'
|
||||||
|
|
||||||
|
_TESTS = [{
|
||||||
|
'url': 'https://cozy.tv/beardson/replays/2021-11-19_1',
|
||||||
|
'info_dict': {
|
||||||
|
'id': 'beardson-2021-11-19_1',
|
||||||
|
'ext': 'mp4',
|
||||||
|
'title': 'pokemon pt2',
|
||||||
|
'uploader': 'beardson',
|
||||||
|
'upload_date': '20211119',
|
||||||
|
'was_live': True,
|
||||||
|
'duration': 7981,
|
||||||
|
},
|
||||||
|
'params': {'skip_download': True}
|
||||||
|
}]
|
||||||
|
|
||||||
|
def _real_extract(self, url):
|
||||||
|
uploader, date = self._match_valid_url(url).groups()
|
||||||
|
id = f'{uploader}-{date}'
|
||||||
|
data_json = self._download_json(f'https://api.cozy.tv/cache/{uploader}/replay/{date}', id)
|
||||||
|
formats, subtitles = self._extract_m3u8_formats_and_subtitles(
|
||||||
|
f'https://cozycdn.foxtrotstream.xyz/replays/{uploader}/{date}/index.m3u8', id, ext='mp4')
|
||||||
|
return {
|
||||||
|
'id': id,
|
||||||
|
'title': data_json.get('title'),
|
||||||
|
'uploader': data_json.get('user') or uploader,
|
||||||
|
'upload_date': unified_strdate(data_json.get('date')),
|
||||||
|
'was_live': True,
|
||||||
|
'duration': data_json.get('duration'),
|
||||||
|
'formats': formats,
|
||||||
|
'subtitles': subtitles,
|
||||||
|
}
|
||||||
@@ -18,7 +18,7 @@ from ..utils import (
|
|||||||
str_to_int,
|
str_to_int,
|
||||||
unescapeHTML,
|
unescapeHTML,
|
||||||
)
|
)
|
||||||
from .senateisvp import SenateISVPIE
|
from .senategov import SenateISVPIE
|
||||||
from .ustream import UstreamIE
|
from .ustream import UstreamIE
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -15,7 +15,6 @@ from ..utils import (
|
|||||||
class CuriosityStreamBaseIE(InfoExtractor):
|
class CuriosityStreamBaseIE(InfoExtractor):
|
||||||
_NETRC_MACHINE = 'curiositystream'
|
_NETRC_MACHINE = 'curiositystream'
|
||||||
_auth_token = None
|
_auth_token = None
|
||||||
_API_BASE_URL = 'https://api.curiositystream.com/v1/'
|
|
||||||
|
|
||||||
def _handle_errors(self, result):
|
def _handle_errors(self, result):
|
||||||
error = result.get('error', {}).get('message')
|
error = result.get('error', {}).get('message')
|
||||||
@@ -39,37 +38,44 @@ class CuriosityStreamBaseIE(InfoExtractor):
|
|||||||
if email is None:
|
if email is None:
|
||||||
return
|
return
|
||||||
result = self._download_json(
|
result = self._download_json(
|
||||||
self._API_BASE_URL + 'login', None, data=urlencode_postdata({
|
'https://api.curiositystream.com/v1/login', None,
|
||||||
|
note='Logging in', data=urlencode_postdata({
|
||||||
'email': email,
|
'email': email,
|
||||||
'password': password,
|
'password': password,
|
||||||
}))
|
}))
|
||||||
self._handle_errors(result)
|
self._handle_errors(result)
|
||||||
self._auth_token = result['message']['auth_token']
|
CuriosityStreamBaseIE._auth_token = result['message']['auth_token']
|
||||||
|
|
||||||
|
|
||||||
class CuriosityStreamIE(CuriosityStreamBaseIE):
|
class CuriosityStreamIE(CuriosityStreamBaseIE):
|
||||||
IE_NAME = 'curiositystream'
|
IE_NAME = 'curiositystream'
|
||||||
_VALID_URL = r'https?://(?:app\.)?curiositystream\.com/video/(?P<id>\d+)'
|
_VALID_URL = r'https?://(?:app\.)?curiositystream\.com/video/(?P<id>\d+)'
|
||||||
_TEST = {
|
_TESTS = [{
|
||||||
'url': 'https://app.curiositystream.com/video/2',
|
'url': 'https://app.curiositystream.com/video/2',
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
'id': '2',
|
'id': '2',
|
||||||
'ext': 'mp4',
|
'ext': 'mp4',
|
||||||
'title': 'How Did You Develop The Internet?',
|
'title': 'How Did You Develop The Internet?',
|
||||||
'description': 'Vint Cerf, Google\'s Chief Internet Evangelist, describes how he and Bob Kahn created the internet.',
|
'description': 'Vint Cerf, Google\'s Chief Internet Evangelist, describes how he and Bob Kahn created the internet.',
|
||||||
|
'channel': 'Curiosity Stream',
|
||||||
|
'categories': ['Technology', 'Interview'],
|
||||||
|
'average_rating': 96.79,
|
||||||
|
'series_id': '2',
|
||||||
},
|
},
|
||||||
'params': {
|
'params': {
|
||||||
# m3u8 download
|
# m3u8 download
|
||||||
'skip_download': True,
|
'skip_download': True,
|
||||||
},
|
},
|
||||||
}
|
}]
|
||||||
|
|
||||||
|
_API_BASE_URL = 'https://api.curiositystream.com/v1/media/'
|
||||||
|
|
||||||
def _real_extract(self, url):
|
def _real_extract(self, url):
|
||||||
video_id = self._match_id(url)
|
video_id = self._match_id(url)
|
||||||
|
|
||||||
formats = []
|
formats = []
|
||||||
for encoding_format in ('m3u8', 'mpd'):
|
for encoding_format in ('m3u8', 'mpd'):
|
||||||
media = self._call_api('media/' + video_id, video_id, query={
|
media = self._call_api(video_id, video_id, query={
|
||||||
'encodingsNew': 'true',
|
'encodingsNew': 'true',
|
||||||
'encodingsFormat': encoding_format,
|
'encodingsFormat': encoding_format,
|
||||||
})
|
})
|
||||||
@@ -139,12 +145,33 @@ class CuriosityStreamIE(CuriosityStreamBaseIE):
|
|||||||
'duration': int_or_none(media.get('duration')),
|
'duration': int_or_none(media.get('duration')),
|
||||||
'tags': media.get('tags'),
|
'tags': media.get('tags'),
|
||||||
'subtitles': subtitles,
|
'subtitles': subtitles,
|
||||||
|
'channel': media.get('producer'),
|
||||||
|
'categories': [media.get('primary_category'), media.get('type')],
|
||||||
|
'average_rating': media.get('rating_percentage'),
|
||||||
|
'series_id': str(media.get('collection_id') or '') or None,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class CuriosityStreamCollectionIE(CuriosityStreamBaseIE):
|
class CuriosityStreamCollectionBaseIE(CuriosityStreamBaseIE):
|
||||||
IE_NAME = 'curiositystream:collection'
|
|
||||||
_VALID_URL = r'https?://(?:app\.)?curiositystream\.com/(?:collections?|series)/(?P<id>\d+)'
|
def _real_extract(self, url):
|
||||||
|
collection_id = self._match_id(url)
|
||||||
|
collection = self._call_api(collection_id, collection_id)
|
||||||
|
entries = []
|
||||||
|
for media in collection.get('media', []):
|
||||||
|
media_id = compat_str(media.get('id'))
|
||||||
|
media_type, ie = ('series', CuriosityStreamSeriesIE) if media.get('is_collection') else ('video', CuriosityStreamIE)
|
||||||
|
entries.append(self.url_result(
|
||||||
|
'https://curiositystream.com/%s/%s' % (media_type, media_id),
|
||||||
|
ie=ie.ie_key(), video_id=media_id))
|
||||||
|
return self.playlist_result(
|
||||||
|
entries, collection_id,
|
||||||
|
collection.get('title'), collection.get('description'))
|
||||||
|
|
||||||
|
|
||||||
|
class CuriosityStreamCollectionsIE(CuriosityStreamCollectionBaseIE):
|
||||||
|
IE_NAME = 'curiositystream:collections'
|
||||||
|
_VALID_URL = r'https?://(?:app\.)?curiositystream\.com/collections/(?P<id>\d+)'
|
||||||
_API_BASE_URL = 'https://api.curiositystream.com/v2/collections/'
|
_API_BASE_URL = 'https://api.curiositystream.com/v2/collections/'
|
||||||
_TESTS = [{
|
_TESTS = [{
|
||||||
'url': 'https://curiositystream.com/collections/86',
|
'url': 'https://curiositystream.com/collections/86',
|
||||||
@@ -155,7 +182,17 @@ class CuriosityStreamCollectionIE(CuriosityStreamBaseIE):
|
|||||||
},
|
},
|
||||||
'playlist_mincount': 7,
|
'playlist_mincount': 7,
|
||||||
}, {
|
}, {
|
||||||
'url': 'https://app.curiositystream.com/collection/2',
|
'url': 'https://curiositystream.com/collections/36',
|
||||||
|
'only_matching': True,
|
||||||
|
}]
|
||||||
|
|
||||||
|
|
||||||
|
class CuriosityStreamSeriesIE(CuriosityStreamCollectionBaseIE):
|
||||||
|
IE_NAME = 'curiositystream:series'
|
||||||
|
_VALID_URL = r'https?://(?:app\.)?curiositystream\.com/(?:series|collection)/(?P<id>\d+)'
|
||||||
|
_API_BASE_URL = 'https://api.curiositystream.com/v2/series/'
|
||||||
|
_TESTS = [{
|
||||||
|
'url': 'https://curiositystream.com/series/2',
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
'id': '2',
|
'id': '2',
|
||||||
'title': 'Curious Minds: The Internet',
|
'title': 'Curious Minds: The Internet',
|
||||||
@@ -163,23 +200,6 @@ class CuriosityStreamCollectionIE(CuriosityStreamBaseIE):
|
|||||||
},
|
},
|
||||||
'playlist_mincount': 16,
|
'playlist_mincount': 16,
|
||||||
}, {
|
}, {
|
||||||
'url': 'https://curiositystream.com/series/2',
|
'url': 'https://curiositystream.com/collection/2',
|
||||||
'only_matching': True,
|
|
||||||
}, {
|
|
||||||
'url': 'https://curiositystream.com/collections/36',
|
|
||||||
'only_matching': True,
|
'only_matching': True,
|
||||||
}]
|
}]
|
||||||
|
|
||||||
def _real_extract(self, url):
|
|
||||||
collection_id = self._match_id(url)
|
|
||||||
collection = self._call_api(collection_id, collection_id)
|
|
||||||
entries = []
|
|
||||||
for media in collection.get('media', []):
|
|
||||||
media_id = compat_str(media.get('id'))
|
|
||||||
media_type, ie = ('series', CuriosityStreamCollectionIE) if media.get('is_collection') else ('video', CuriosityStreamIE)
|
|
||||||
entries.append(self.url_result(
|
|
||||||
'https://curiositystream.com/%s/%s' % (media_type, media_id),
|
|
||||||
ie=ie.ie_key(), video_id=media_id))
|
|
||||||
return self.playlist_result(
|
|
||||||
entries, collection_id,
|
|
||||||
collection.get('title'), collection.get('description'))
|
|
||||||
|
|||||||
@@ -1,41 +0,0 @@
|
|||||||
# coding: utf-8
|
|
||||||
from __future__ import unicode_literals
|
|
||||||
|
|
||||||
|
|
||||||
from .dplay import DPlayIE
|
|
||||||
|
|
||||||
|
|
||||||
class DiscoveryNetworksDeIE(DPlayIE):
|
|
||||||
_VALID_URL = r'https?://(?:www\.)?(?P<domain>(?:tlc|dmax)\.de|dplay\.co\.uk)/(?:programme|show|sendungen)/(?P<programme>[^/]+)/(?:video/)?(?P<alternate_id>[^/]+)'
|
|
||||||
|
|
||||||
_TESTS = [{
|
|
||||||
'url': 'https://www.tlc.de/programme/breaking-amish/video/die-welt-da-drauen/DCB331270001100',
|
|
||||||
'info_dict': {
|
|
||||||
'id': '78867',
|
|
||||||
'ext': 'mp4',
|
|
||||||
'title': 'Die Welt da draußen',
|
|
||||||
'description': 'md5:61033c12b73286e409d99a41742ef608',
|
|
||||||
'timestamp': 1554069600,
|
|
||||||
'upload_date': '20190331',
|
|
||||||
},
|
|
||||||
'params': {
|
|
||||||
'skip_download': True,
|
|
||||||
},
|
|
||||||
}, {
|
|
||||||
'url': 'https://www.dmax.de/programme/dmax-highlights/video/tuning-star-sidney-hoffmann-exklusiv-bei-dmax/191023082312316',
|
|
||||||
'only_matching': True,
|
|
||||||
}, {
|
|
||||||
'url': 'https://www.dplay.co.uk/show/ghost-adventures/video/hotel-leger-103620/EHD_280313B',
|
|
||||||
'only_matching': True,
|
|
||||||
}, {
|
|
||||||
'url': 'https://tlc.de/sendungen/breaking-amish/die-welt-da-drauen/',
|
|
||||||
'only_matching': True,
|
|
||||||
}]
|
|
||||||
|
|
||||||
def _real_extract(self, url):
|
|
||||||
domain, programme, alternate_id = self._match_valid_url(url).groups()
|
|
||||||
country = 'GB' if domain == 'dplay.co.uk' else 'DE'
|
|
||||||
realm = 'questuk' if country == 'GB' else domain.replace('.', '')
|
|
||||||
return self._get_disco_api_info(
|
|
||||||
url, '%s/%s' % (programme, alternate_id),
|
|
||||||
'sonic-eu1-prod.disco-api.com', realm, country)
|
|
||||||
@@ -1,97 +0,0 @@
|
|||||||
# coding: utf-8
|
|
||||||
from __future__ import unicode_literals
|
|
||||||
|
|
||||||
import json
|
|
||||||
|
|
||||||
from ..compat import compat_str
|
|
||||||
from ..utils import try_get
|
|
||||||
from .common import InfoExtractor
|
|
||||||
from .dplay import DPlayIE
|
|
||||||
|
|
||||||
|
|
||||||
class DiscoveryPlusIndiaIE(DPlayIE):
|
|
||||||
_VALID_URL = r'https?://(?:www\.)?discoveryplus\.in/videos?' + DPlayIE._PATH_REGEX
|
|
||||||
_TESTS = [{
|
|
||||||
'url': 'https://www.discoveryplus.in/videos/how-do-they-do-it/fugu-and-more?seasonId=8&type=EPISODE',
|
|
||||||
'info_dict': {
|
|
||||||
'id': '27104',
|
|
||||||
'ext': 'mp4',
|
|
||||||
'display_id': 'how-do-they-do-it/fugu-and-more',
|
|
||||||
'title': 'Fugu and More',
|
|
||||||
'description': 'The Japanese catch, prepare and eat the deadliest fish on the planet.',
|
|
||||||
'duration': 1319,
|
|
||||||
'timestamp': 1582309800,
|
|
||||||
'upload_date': '20200221',
|
|
||||||
'series': 'How Do They Do It?',
|
|
||||||
'season_number': 8,
|
|
||||||
'episode_number': 2,
|
|
||||||
'creator': 'Discovery Channel',
|
|
||||||
},
|
|
||||||
'params': {
|
|
||||||
'skip_download': True,
|
|
||||||
},
|
|
||||||
'skip': 'Cookies (not necessarily logged in) are needed'
|
|
||||||
}]
|
|
||||||
|
|
||||||
def _update_disco_api_headers(self, headers, disco_base, display_id, realm):
|
|
||||||
headers['x-disco-params'] = 'realm=%s' % realm
|
|
||||||
headers['x-disco-client'] = 'WEB:UNKNOWN:dplus-india:17.0.0'
|
|
||||||
|
|
||||||
def _download_video_playback_info(self, disco_base, video_id, headers):
|
|
||||||
return self._download_json(
|
|
||||||
disco_base + 'playback/v3/videoPlaybackInfo',
|
|
||||||
video_id, headers=headers, data=json.dumps({
|
|
||||||
'deviceInfo': {
|
|
||||||
'adBlocker': False,
|
|
||||||
},
|
|
||||||
'videoId': video_id,
|
|
||||||
}).encode('utf-8'))['data']['attributes']['streaming']
|
|
||||||
|
|
||||||
def _real_extract(self, url):
|
|
||||||
display_id = self._match_id(url)
|
|
||||||
return self._get_disco_api_info(
|
|
||||||
url, display_id, 'ap2-prod-direct.discoveryplus.in', 'dplusindia', 'in')
|
|
||||||
|
|
||||||
|
|
||||||
class DiscoveryPlusIndiaShowIE(InfoExtractor):
|
|
||||||
_VALID_URL = r'https?://(?:www\.)?discoveryplus\.in/show/(?P<show_name>[^/]+)/?(?:[?#]|$)'
|
|
||||||
_TESTS = [{
|
|
||||||
'url': 'https://www.discoveryplus.in/show/how-do-they-do-it',
|
|
||||||
'playlist_mincount': 140,
|
|
||||||
'info_dict': {
|
|
||||||
'id': 'how-do-they-do-it',
|
|
||||||
},
|
|
||||||
}]
|
|
||||||
|
|
||||||
def _entries(self, show_name):
|
|
||||||
headers = {
|
|
||||||
'x-disco-client': 'WEB:UNKNOWN:dplus-india:prod',
|
|
||||||
'x-disco-params': 'realm=dplusindia',
|
|
||||||
'referer': 'https://www.discoveryplus.in/',
|
|
||||||
}
|
|
||||||
show_url = 'https://ap2-prod-direct.discoveryplus.in/cms/routes/show/{}?include=default'.format(show_name)
|
|
||||||
show_json = self._download_json(show_url,
|
|
||||||
video_id=show_name,
|
|
||||||
headers=headers)['included'][4]['attributes']['component']
|
|
||||||
show_id = show_json['mandatoryParams'].split('=')[-1]
|
|
||||||
season_url = 'https://ap2-prod-direct.discoveryplus.in/content/videos?sort=episodeNumber&filter[seasonNumber]={}&filter[show.id]={}&page[size]=100&page[number]={}'
|
|
||||||
for season in show_json['filters'][0]['options']:
|
|
||||||
season_id = season['id']
|
|
||||||
total_pages, page_num = 1, 0
|
|
||||||
while page_num < total_pages:
|
|
||||||
season_json = self._download_json(season_url.format(season_id, show_id, compat_str(page_num + 1)),
|
|
||||||
video_id=show_id, headers=headers,
|
|
||||||
note='Downloading JSON metadata%s' % (' page %d' % page_num if page_num else ''))
|
|
||||||
if page_num == 0:
|
|
||||||
total_pages = try_get(season_json, lambda x: x['meta']['totalPages'], int) or 1
|
|
||||||
episodes_json = season_json['data']
|
|
||||||
for episode in episodes_json:
|
|
||||||
video_id = episode['attributes']['path']
|
|
||||||
yield self.url_result(
|
|
||||||
'https://discoveryplus.in/videos/%s' % video_id,
|
|
||||||
ie=DiscoveryPlusIndiaIE.ie_key(), video_id=video_id)
|
|
||||||
page_num += 1
|
|
||||||
|
|
||||||
def _real_extract(self, url):
|
|
||||||
show_name = self._match_valid_url(url).group('show_name')
|
|
||||||
return self.playlist_result(self._entries(show_name), playlist_id=show_name)
|
|
||||||
+327
-142
@@ -2,6 +2,7 @@
|
|||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
import json
|
import json
|
||||||
|
import uuid
|
||||||
|
|
||||||
from .common import InfoExtractor
|
from .common import InfoExtractor
|
||||||
from ..compat import compat_HTTPError
|
from ..compat import compat_HTTPError
|
||||||
@@ -11,12 +12,172 @@ from ..utils import (
|
|||||||
float_or_none,
|
float_or_none,
|
||||||
int_or_none,
|
int_or_none,
|
||||||
strip_or_none,
|
strip_or_none,
|
||||||
|
try_get,
|
||||||
unified_timestamp,
|
unified_timestamp,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class DPlayIE(InfoExtractor):
|
class DPlayBaseIE(InfoExtractor):
|
||||||
_PATH_REGEX = r'/(?P<id>[^/]+/[^/?#]+)'
|
_PATH_REGEX = r'/(?P<id>[^/]+/[^/?#]+)'
|
||||||
|
_auth_token_cache = {}
|
||||||
|
|
||||||
|
def _get_auth(self, disco_base, display_id, realm, needs_device_id=True):
|
||||||
|
key = (disco_base, realm)
|
||||||
|
st = self._get_cookies(disco_base).get('st')
|
||||||
|
token = (st and st.value) or self._auth_token_cache.get(key)
|
||||||
|
|
||||||
|
if not token:
|
||||||
|
query = {'realm': realm}
|
||||||
|
if needs_device_id:
|
||||||
|
query['deviceId'] = uuid.uuid4().hex
|
||||||
|
token = self._download_json(
|
||||||
|
disco_base + 'token', display_id, 'Downloading token',
|
||||||
|
query=query)['data']['attributes']['token']
|
||||||
|
|
||||||
|
# Save cache only if cookies are not being set
|
||||||
|
if not self._get_cookies(disco_base).get('st'):
|
||||||
|
self._auth_token_cache[key] = token
|
||||||
|
|
||||||
|
return f'Bearer {token}'
|
||||||
|
|
||||||
|
def _process_errors(self, e, geo_countries):
|
||||||
|
info = self._parse_json(e.cause.read().decode('utf-8'), None)
|
||||||
|
error = info['errors'][0]
|
||||||
|
error_code = error.get('code')
|
||||||
|
if error_code == 'access.denied.geoblocked':
|
||||||
|
self.raise_geo_restricted(countries=geo_countries)
|
||||||
|
elif error_code in ('access.denied.missingpackage', 'invalid.token'):
|
||||||
|
raise ExtractorError(
|
||||||
|
'This video is only available for registered users. You may want to use --cookies.', expected=True)
|
||||||
|
raise ExtractorError(info['errors'][0]['detail'], expected=True)
|
||||||
|
|
||||||
|
def _update_disco_api_headers(self, headers, disco_base, display_id, realm):
|
||||||
|
headers['Authorization'] = self._get_auth(disco_base, display_id, realm, False)
|
||||||
|
|
||||||
|
def _download_video_playback_info(self, disco_base, video_id, headers):
|
||||||
|
streaming = self._download_json(
|
||||||
|
disco_base + 'playback/videoPlaybackInfo/' + video_id,
|
||||||
|
video_id, headers=headers)['data']['attributes']['streaming']
|
||||||
|
streaming_list = []
|
||||||
|
for format_id, format_dict in streaming.items():
|
||||||
|
streaming_list.append({
|
||||||
|
'type': format_id,
|
||||||
|
'url': format_dict.get('url'),
|
||||||
|
})
|
||||||
|
return streaming_list
|
||||||
|
|
||||||
|
def _get_disco_api_info(self, url, display_id, disco_host, realm, country, domain=''):
|
||||||
|
geo_countries = [country.upper()]
|
||||||
|
self._initialize_geo_bypass({
|
||||||
|
'countries': geo_countries,
|
||||||
|
})
|
||||||
|
disco_base = 'https://%s/' % disco_host
|
||||||
|
headers = {
|
||||||
|
'Referer': url,
|
||||||
|
}
|
||||||
|
self._update_disco_api_headers(headers, disco_base, display_id, realm)
|
||||||
|
try:
|
||||||
|
video = self._download_json(
|
||||||
|
disco_base + 'content/videos/' + display_id, display_id,
|
||||||
|
headers=headers, query={
|
||||||
|
'fields[channel]': 'name',
|
||||||
|
'fields[image]': 'height,src,width',
|
||||||
|
'fields[show]': 'name',
|
||||||
|
'fields[tag]': 'name',
|
||||||
|
'fields[video]': 'description,episodeNumber,name,publishStart,seasonNumber,videoDuration',
|
||||||
|
'include': 'images,primaryChannel,show,tags'
|
||||||
|
})
|
||||||
|
except ExtractorError as e:
|
||||||
|
if isinstance(e.cause, compat_HTTPError) and e.cause.code == 400:
|
||||||
|
self._process_errors(e, geo_countries)
|
||||||
|
raise
|
||||||
|
video_id = video['data']['id']
|
||||||
|
info = video['data']['attributes']
|
||||||
|
title = info['name'].strip()
|
||||||
|
formats = []
|
||||||
|
subtitles = {}
|
||||||
|
try:
|
||||||
|
streaming = self._download_video_playback_info(
|
||||||
|
disco_base, video_id, headers)
|
||||||
|
except ExtractorError as e:
|
||||||
|
if isinstance(e.cause, compat_HTTPError) and e.cause.code == 403:
|
||||||
|
self._process_errors(e, geo_countries)
|
||||||
|
raise
|
||||||
|
for format_dict in streaming:
|
||||||
|
if not isinstance(format_dict, dict):
|
||||||
|
continue
|
||||||
|
format_url = format_dict.get('url')
|
||||||
|
if not format_url:
|
||||||
|
continue
|
||||||
|
format_id = format_dict.get('type')
|
||||||
|
ext = determine_ext(format_url)
|
||||||
|
if format_id == 'dash' or ext == 'mpd':
|
||||||
|
dash_fmts, dash_subs = self._extract_mpd_formats_and_subtitles(
|
||||||
|
format_url, display_id, mpd_id='dash', fatal=False)
|
||||||
|
formats.extend(dash_fmts)
|
||||||
|
subtitles = self._merge_subtitles(subtitles, dash_subs)
|
||||||
|
elif format_id == 'hls' or ext == 'm3u8':
|
||||||
|
m3u8_fmts, m3u8_subs = self._extract_m3u8_formats_and_subtitles(
|
||||||
|
format_url, display_id, 'mp4',
|
||||||
|
entry_protocol='m3u8_native', m3u8_id='hls',
|
||||||
|
fatal=False)
|
||||||
|
formats.extend(m3u8_fmts)
|
||||||
|
subtitles = self._merge_subtitles(subtitles, m3u8_subs)
|
||||||
|
else:
|
||||||
|
formats.append({
|
||||||
|
'url': format_url,
|
||||||
|
'format_id': format_id,
|
||||||
|
})
|
||||||
|
self._sort_formats(formats)
|
||||||
|
|
||||||
|
creator = series = None
|
||||||
|
tags = []
|
||||||
|
thumbnails = []
|
||||||
|
included = video.get('included') or []
|
||||||
|
if isinstance(included, list):
|
||||||
|
for e in included:
|
||||||
|
attributes = e.get('attributes')
|
||||||
|
if not attributes:
|
||||||
|
continue
|
||||||
|
e_type = e.get('type')
|
||||||
|
if e_type == 'channel':
|
||||||
|
creator = attributes.get('name')
|
||||||
|
elif e_type == 'image':
|
||||||
|
src = attributes.get('src')
|
||||||
|
if src:
|
||||||
|
thumbnails.append({
|
||||||
|
'url': src,
|
||||||
|
'width': int_or_none(attributes.get('width')),
|
||||||
|
'height': int_or_none(attributes.get('height')),
|
||||||
|
})
|
||||||
|
if e_type == 'show':
|
||||||
|
series = attributes.get('name')
|
||||||
|
elif e_type == 'tag':
|
||||||
|
name = attributes.get('name')
|
||||||
|
if name:
|
||||||
|
tags.append(name)
|
||||||
|
return {
|
||||||
|
'id': video_id,
|
||||||
|
'display_id': display_id,
|
||||||
|
'title': title,
|
||||||
|
'description': strip_or_none(info.get('description')),
|
||||||
|
'duration': float_or_none(info.get('videoDuration'), 1000),
|
||||||
|
'timestamp': unified_timestamp(info.get('publishStart')),
|
||||||
|
'series': series,
|
||||||
|
'season_number': int_or_none(info.get('seasonNumber')),
|
||||||
|
'episode_number': int_or_none(info.get('episodeNumber')),
|
||||||
|
'creator': creator,
|
||||||
|
'tags': tags,
|
||||||
|
'thumbnails': thumbnails,
|
||||||
|
'formats': formats,
|
||||||
|
'subtitles': subtitles,
|
||||||
|
'http_headers': {
|
||||||
|
'referer': domain,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class DPlayIE(DPlayBaseIE):
|
||||||
_VALID_URL = r'''(?x)https?://
|
_VALID_URL = r'''(?x)https?://
|
||||||
(?P<domain>
|
(?P<domain>
|
||||||
(?:www\.)?(?P<host>d
|
(?:www\.)?(?P<host>d
|
||||||
@@ -26,7 +187,7 @@ class DPlayIE(InfoExtractor):
|
|||||||
)
|
)
|
||||||
)|
|
)|
|
||||||
(?P<subdomain_country>es|it)\.dplay\.com
|
(?P<subdomain_country>es|it)\.dplay\.com
|
||||||
)/[^/]+''' + _PATH_REGEX
|
)/[^/]+''' + DPlayBaseIE._PATH_REGEX
|
||||||
|
|
||||||
_TESTS = [{
|
_TESTS = [{
|
||||||
# non geo restricted, via secure api, unsigned download hls URL
|
# non geo restricted, via secure api, unsigned download hls URL
|
||||||
@@ -150,138 +311,6 @@ class DPlayIE(InfoExtractor):
|
|||||||
'only_matching': True,
|
'only_matching': True,
|
||||||
}]
|
}]
|
||||||
|
|
||||||
def _process_errors(self, e, geo_countries):
|
|
||||||
info = self._parse_json(e.cause.read().decode('utf-8'), None)
|
|
||||||
error = info['errors'][0]
|
|
||||||
error_code = error.get('code')
|
|
||||||
if error_code == 'access.denied.geoblocked':
|
|
||||||
self.raise_geo_restricted(countries=geo_countries)
|
|
||||||
elif error_code in ('access.denied.missingpackage', 'invalid.token'):
|
|
||||||
raise ExtractorError(
|
|
||||||
'This video is only available for registered users. You may want to use --cookies.', expected=True)
|
|
||||||
raise ExtractorError(info['errors'][0]['detail'], expected=True)
|
|
||||||
|
|
||||||
def _update_disco_api_headers(self, headers, disco_base, display_id, realm):
|
|
||||||
headers['Authorization'] = 'Bearer ' + self._download_json(
|
|
||||||
disco_base + 'token', display_id, 'Downloading token',
|
|
||||||
query={
|
|
||||||
'realm': realm,
|
|
||||||
})['data']['attributes']['token']
|
|
||||||
|
|
||||||
def _download_video_playback_info(self, disco_base, video_id, headers):
|
|
||||||
streaming = self._download_json(
|
|
||||||
disco_base + 'playback/videoPlaybackInfo/' + video_id,
|
|
||||||
video_id, headers=headers)['data']['attributes']['streaming']
|
|
||||||
streaming_list = []
|
|
||||||
for format_id, format_dict in streaming.items():
|
|
||||||
streaming_list.append({
|
|
||||||
'type': format_id,
|
|
||||||
'url': format_dict.get('url'),
|
|
||||||
})
|
|
||||||
return streaming_list
|
|
||||||
|
|
||||||
def _get_disco_api_info(self, url, display_id, disco_host, realm, country):
|
|
||||||
geo_countries = [country.upper()]
|
|
||||||
self._initialize_geo_bypass({
|
|
||||||
'countries': geo_countries,
|
|
||||||
})
|
|
||||||
disco_base = 'https://%s/' % disco_host
|
|
||||||
headers = {
|
|
||||||
'Referer': url,
|
|
||||||
}
|
|
||||||
self._update_disco_api_headers(headers, disco_base, display_id, realm)
|
|
||||||
try:
|
|
||||||
video = self._download_json(
|
|
||||||
disco_base + 'content/videos/' + display_id, display_id,
|
|
||||||
headers=headers, query={
|
|
||||||
'fields[channel]': 'name',
|
|
||||||
'fields[image]': 'height,src,width',
|
|
||||||
'fields[show]': 'name',
|
|
||||||
'fields[tag]': 'name',
|
|
||||||
'fields[video]': 'description,episodeNumber,name,publishStart,seasonNumber,videoDuration',
|
|
||||||
'include': 'images,primaryChannel,show,tags'
|
|
||||||
})
|
|
||||||
except ExtractorError as e:
|
|
||||||
if isinstance(e.cause, compat_HTTPError) and e.cause.code == 400:
|
|
||||||
self._process_errors(e, geo_countries)
|
|
||||||
raise
|
|
||||||
video_id = video['data']['id']
|
|
||||||
info = video['data']['attributes']
|
|
||||||
title = info['name'].strip()
|
|
||||||
formats = []
|
|
||||||
try:
|
|
||||||
streaming = self._download_video_playback_info(
|
|
||||||
disco_base, video_id, headers)
|
|
||||||
except ExtractorError as e:
|
|
||||||
if isinstance(e.cause, compat_HTTPError) and e.cause.code == 403:
|
|
||||||
self._process_errors(e, geo_countries)
|
|
||||||
raise
|
|
||||||
for format_dict in streaming:
|
|
||||||
if not isinstance(format_dict, dict):
|
|
||||||
continue
|
|
||||||
format_url = format_dict.get('url')
|
|
||||||
if not format_url:
|
|
||||||
continue
|
|
||||||
format_id = format_dict.get('type')
|
|
||||||
ext = determine_ext(format_url)
|
|
||||||
if format_id == 'dash' or ext == 'mpd':
|
|
||||||
formats.extend(self._extract_mpd_formats(
|
|
||||||
format_url, display_id, mpd_id='dash', fatal=False))
|
|
||||||
elif format_id == 'hls' or ext == 'm3u8':
|
|
||||||
formats.extend(self._extract_m3u8_formats(
|
|
||||||
format_url, display_id, 'mp4',
|
|
||||||
entry_protocol='m3u8_native', m3u8_id='hls',
|
|
||||||
fatal=False))
|
|
||||||
else:
|
|
||||||
formats.append({
|
|
||||||
'url': format_url,
|
|
||||||
'format_id': format_id,
|
|
||||||
})
|
|
||||||
self._sort_formats(formats)
|
|
||||||
|
|
||||||
creator = series = None
|
|
||||||
tags = []
|
|
||||||
thumbnails = []
|
|
||||||
included = video.get('included') or []
|
|
||||||
if isinstance(included, list):
|
|
||||||
for e in included:
|
|
||||||
attributes = e.get('attributes')
|
|
||||||
if not attributes:
|
|
||||||
continue
|
|
||||||
e_type = e.get('type')
|
|
||||||
if e_type == 'channel':
|
|
||||||
creator = attributes.get('name')
|
|
||||||
elif e_type == 'image':
|
|
||||||
src = attributes.get('src')
|
|
||||||
if src:
|
|
||||||
thumbnails.append({
|
|
||||||
'url': src,
|
|
||||||
'width': int_or_none(attributes.get('width')),
|
|
||||||
'height': int_or_none(attributes.get('height')),
|
|
||||||
})
|
|
||||||
if e_type == 'show':
|
|
||||||
series = attributes.get('name')
|
|
||||||
elif e_type == 'tag':
|
|
||||||
name = attributes.get('name')
|
|
||||||
if name:
|
|
||||||
tags.append(name)
|
|
||||||
|
|
||||||
return {
|
|
||||||
'id': video_id,
|
|
||||||
'display_id': display_id,
|
|
||||||
'title': title,
|
|
||||||
'description': strip_or_none(info.get('description')),
|
|
||||||
'duration': float_or_none(info.get('videoDuration'), 1000),
|
|
||||||
'timestamp': unified_timestamp(info.get('publishStart')),
|
|
||||||
'series': series,
|
|
||||||
'season_number': int_or_none(info.get('seasonNumber')),
|
|
||||||
'episode_number': int_or_none(info.get('episodeNumber')),
|
|
||||||
'creator': creator,
|
|
||||||
'tags': tags,
|
|
||||||
'thumbnails': thumbnails,
|
|
||||||
'formats': formats,
|
|
||||||
}
|
|
||||||
|
|
||||||
def _real_extract(self, url):
|
def _real_extract(self, url):
|
||||||
mobj = self._match_valid_url(url)
|
mobj = self._match_valid_url(url)
|
||||||
display_id = mobj.group('id')
|
display_id = mobj.group('id')
|
||||||
@@ -289,11 +318,11 @@ class DPlayIE(InfoExtractor):
|
|||||||
country = mobj.group('country') or mobj.group('subdomain_country') or mobj.group('plus_country')
|
country = mobj.group('country') or mobj.group('subdomain_country') or mobj.group('plus_country')
|
||||||
host = 'disco-api.' + domain if domain[0] == 'd' else 'eu2-prod.disco-api.com'
|
host = 'disco-api.' + domain if domain[0] == 'd' else 'eu2-prod.disco-api.com'
|
||||||
return self._get_disco_api_info(
|
return self._get_disco_api_info(
|
||||||
url, display_id, host, 'dplay' + country, country)
|
url, display_id, host, 'dplay' + country, country, domain)
|
||||||
|
|
||||||
|
|
||||||
class HGTVDeIE(DPlayIE):
|
class HGTVDeIE(DPlayBaseIE):
|
||||||
_VALID_URL = r'https?://de\.hgtv\.com/sendungen' + DPlayIE._PATH_REGEX
|
_VALID_URL = r'https?://de\.hgtv\.com/sendungen' + DPlayBaseIE._PATH_REGEX
|
||||||
_TESTS = [{
|
_TESTS = [{
|
||||||
'url': 'https://de.hgtv.com/sendungen/tiny-house-klein-aber-oho/wer-braucht-schon-eine-toilette/',
|
'url': 'https://de.hgtv.com/sendungen/tiny-house-klein-aber-oho/wer-braucht-schon-eine-toilette/',
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
@@ -318,8 +347,8 @@ class HGTVDeIE(DPlayIE):
|
|||||||
url, display_id, 'eu1-prod.disco-api.com', 'hgtv', 'de')
|
url, display_id, 'eu1-prod.disco-api.com', 'hgtv', 'de')
|
||||||
|
|
||||||
|
|
||||||
class DiscoveryPlusIE(DPlayIE):
|
class DiscoveryPlusIE(DPlayBaseIE):
|
||||||
_VALID_URL = r'https?://(?:www\.)?discoveryplus\.com/(?:\w{2}/)?video' + DPlayIE._PATH_REGEX
|
_VALID_URL = r'https?://(?:www\.)?discoveryplus\.com/(?:\w{2}/)?video' + DPlayBaseIE._PATH_REGEX
|
||||||
_TESTS = [{
|
_TESTS = [{
|
||||||
'url': 'https://www.discoveryplus.com/video/property-brothers-forever-home/food-and-family',
|
'url': 'https://www.discoveryplus.com/video/property-brothers-forever-home/food-and-family',
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
@@ -369,7 +398,7 @@ class DiscoveryPlusIE(DPlayIE):
|
|||||||
|
|
||||||
|
|
||||||
class ScienceChannelIE(DiscoveryPlusIE):
|
class ScienceChannelIE(DiscoveryPlusIE):
|
||||||
_VALID_URL = r'https?://(?:www\.)?sciencechannel\.com/video' + DPlayIE._PATH_REGEX
|
_VALID_URL = r'https?://(?:www\.)?sciencechannel\.com/video' + DPlayBaseIE._PATH_REGEX
|
||||||
_TESTS = [{
|
_TESTS = [{
|
||||||
'url': 'https://www.sciencechannel.com/video/strangest-things-science-atve-us/nazi-mystery-machine',
|
'url': 'https://www.sciencechannel.com/video/strangest-things-science-atve-us/nazi-mystery-machine',
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
@@ -389,7 +418,7 @@ class ScienceChannelIE(DiscoveryPlusIE):
|
|||||||
|
|
||||||
|
|
||||||
class DIYNetworkIE(DiscoveryPlusIE):
|
class DIYNetworkIE(DiscoveryPlusIE):
|
||||||
_VALID_URL = r'https?://(?:watch\.)?diynetwork\.com/video' + DPlayIE._PATH_REGEX
|
_VALID_URL = r'https?://(?:watch\.)?diynetwork\.com/video' + DPlayBaseIE._PATH_REGEX
|
||||||
_TESTS = [{
|
_TESTS = [{
|
||||||
'url': 'https://watch.diynetwork.com/video/pool-kings-diy-network/bringing-beach-life-to-texas',
|
'url': 'https://watch.diynetwork.com/video/pool-kings-diy-network/bringing-beach-life-to-texas',
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
@@ -409,7 +438,7 @@ class DIYNetworkIE(DiscoveryPlusIE):
|
|||||||
|
|
||||||
|
|
||||||
class AnimalPlanetIE(DiscoveryPlusIE):
|
class AnimalPlanetIE(DiscoveryPlusIE):
|
||||||
_VALID_URL = r'https?://(?:www\.)?animalplanet\.com/video' + DPlayIE._PATH_REGEX
|
_VALID_URL = r'https?://(?:www\.)?animalplanet\.com/video' + DPlayBaseIE._PATH_REGEX
|
||||||
_TESTS = [{
|
_TESTS = [{
|
||||||
'url': 'https://www.animalplanet.com/video/north-woods-law-animal-planet/squirrel-showdown',
|
'url': 'https://www.animalplanet.com/video/north-woods-law-animal-planet/squirrel-showdown',
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
@@ -426,3 +455,159 @@ class AnimalPlanetIE(DiscoveryPlusIE):
|
|||||||
|
|
||||||
_PRODUCT = 'apl'
|
_PRODUCT = 'apl'
|
||||||
_API_URL = 'us1-prod-direct.animalplanet.com'
|
_API_URL = 'us1-prod-direct.animalplanet.com'
|
||||||
|
|
||||||
|
|
||||||
|
class DiscoveryPlusIndiaIE(DPlayBaseIE):
|
||||||
|
_VALID_URL = r'https?://(?:www\.)?discoveryplus\.in/videos?' + DPlayBaseIE._PATH_REGEX
|
||||||
|
_TESTS = [{
|
||||||
|
'url': 'https://www.discoveryplus.in/videos/how-do-they-do-it/fugu-and-more?seasonId=8&type=EPISODE',
|
||||||
|
'info_dict': {
|
||||||
|
'id': '27104',
|
||||||
|
'ext': 'mp4',
|
||||||
|
'display_id': 'how-do-they-do-it/fugu-and-more',
|
||||||
|
'title': 'Fugu and More',
|
||||||
|
'description': 'The Japanese catch, prepare and eat the deadliest fish on the planet.',
|
||||||
|
'duration': 1319,
|
||||||
|
'timestamp': 1582309800,
|
||||||
|
'upload_date': '20200221',
|
||||||
|
'series': 'How Do They Do It?',
|
||||||
|
'season_number': 8,
|
||||||
|
'episode_number': 2,
|
||||||
|
'creator': 'Discovery Channel',
|
||||||
|
},
|
||||||
|
'params': {
|
||||||
|
'skip_download': True,
|
||||||
|
}
|
||||||
|
}]
|
||||||
|
|
||||||
|
def _update_disco_api_headers(self, headers, disco_base, display_id, realm):
|
||||||
|
headers.update({
|
||||||
|
'x-disco-params': 'realm=%s' % realm,
|
||||||
|
'x-disco-client': 'WEB:UNKNOWN:dplus-india:17.0.0',
|
||||||
|
'Authorization': self._get_auth(disco_base, display_id, realm),
|
||||||
|
})
|
||||||
|
|
||||||
|
def _download_video_playback_info(self, disco_base, video_id, headers):
|
||||||
|
return self._download_json(
|
||||||
|
disco_base + 'playback/v3/videoPlaybackInfo',
|
||||||
|
video_id, headers=headers, data=json.dumps({
|
||||||
|
'deviceInfo': {
|
||||||
|
'adBlocker': False,
|
||||||
|
},
|
||||||
|
'videoId': video_id,
|
||||||
|
}).encode('utf-8'))['data']['attributes']['streaming']
|
||||||
|
|
||||||
|
def _real_extract(self, url):
|
||||||
|
display_id = self._match_id(url)
|
||||||
|
return self._get_disco_api_info(
|
||||||
|
url, display_id, 'ap2-prod-direct.discoveryplus.in', 'dplusindia', 'in', 'https://www.discoveryplus.in/')
|
||||||
|
|
||||||
|
|
||||||
|
class DiscoveryNetworksDeIE(DPlayBaseIE):
|
||||||
|
_VALID_URL = r'https?://(?:www\.)?(?P<domain>(?:tlc|dmax)\.de|dplay\.co\.uk)/(?:programme|show|sendungen)/(?P<programme>[^/]+)/(?:video/)?(?P<alternate_id>[^/]+)'
|
||||||
|
|
||||||
|
_TESTS = [{
|
||||||
|
'url': 'https://www.tlc.de/programme/breaking-amish/video/die-welt-da-drauen/DCB331270001100',
|
||||||
|
'info_dict': {
|
||||||
|
'id': '78867',
|
||||||
|
'ext': 'mp4',
|
||||||
|
'title': 'Die Welt da draußen',
|
||||||
|
'description': 'md5:61033c12b73286e409d99a41742ef608',
|
||||||
|
'timestamp': 1554069600,
|
||||||
|
'upload_date': '20190331',
|
||||||
|
},
|
||||||
|
'params': {
|
||||||
|
'skip_download': True,
|
||||||
|
},
|
||||||
|
}, {
|
||||||
|
'url': 'https://www.dmax.de/programme/dmax-highlights/video/tuning-star-sidney-hoffmann-exklusiv-bei-dmax/191023082312316',
|
||||||
|
'only_matching': True,
|
||||||
|
}, {
|
||||||
|
'url': 'https://www.dplay.co.uk/show/ghost-adventures/video/hotel-leger-103620/EHD_280313B',
|
||||||
|
'only_matching': True,
|
||||||
|
}, {
|
||||||
|
'url': 'https://tlc.de/sendungen/breaking-amish/die-welt-da-drauen/',
|
||||||
|
'only_matching': True,
|
||||||
|
}]
|
||||||
|
|
||||||
|
def _real_extract(self, url):
|
||||||
|
domain, programme, alternate_id = self._match_valid_url(url).groups()
|
||||||
|
country = 'GB' if domain == 'dplay.co.uk' else 'DE'
|
||||||
|
realm = 'questuk' if country == 'GB' else domain.replace('.', '')
|
||||||
|
return self._get_disco_api_info(
|
||||||
|
url, '%s/%s' % (programme, alternate_id),
|
||||||
|
'sonic-eu1-prod.disco-api.com', realm, country)
|
||||||
|
|
||||||
|
|
||||||
|
class DiscoveryPlusShowBaseIE(DPlayBaseIE):
|
||||||
|
|
||||||
|
def _entries(self, show_name):
|
||||||
|
headers = {
|
||||||
|
'x-disco-client': self._X_CLIENT,
|
||||||
|
'x-disco-params': f'realm={self._REALM}',
|
||||||
|
'referer': self._DOMAIN,
|
||||||
|
'Authentication': self._get_auth(self._BASE_API, None, self._REALM),
|
||||||
|
}
|
||||||
|
show_json = self._download_json(
|
||||||
|
f'{self._BASE_API}cms/routes/{self._SHOW_STR}/{show_name}?include=default',
|
||||||
|
video_id=show_name, headers=headers)['included'][self._INDEX]['attributes']['component']
|
||||||
|
show_id = show_json['mandatoryParams'].split('=')[-1]
|
||||||
|
season_url = self._BASE_API + 'content/videos?sort=episodeNumber&filter[seasonNumber]={}&filter[show.id]={}&page[size]=100&page[number]={}'
|
||||||
|
for season in show_json['filters'][0]['options']:
|
||||||
|
season_id = season['id']
|
||||||
|
total_pages, page_num = 1, 0
|
||||||
|
while page_num < total_pages:
|
||||||
|
season_json = self._download_json(
|
||||||
|
season_url.format(season_id, show_id, str(page_num + 1)), show_name, headers=headers,
|
||||||
|
note='Downloading season %s JSON metadata%s' % (season_id, ' page %d' % page_num if page_num else ''))
|
||||||
|
if page_num == 0:
|
||||||
|
total_pages = try_get(season_json, lambda x: x['meta']['totalPages'], int) or 1
|
||||||
|
episodes_json = season_json['data']
|
||||||
|
for episode in episodes_json:
|
||||||
|
video_id = episode['attributes']['path']
|
||||||
|
yield self.url_result(
|
||||||
|
'%svideos/%s' % (self._DOMAIN, video_id),
|
||||||
|
ie=self._VIDEO_IE.ie_key(), video_id=video_id)
|
||||||
|
page_num += 1
|
||||||
|
|
||||||
|
def _real_extract(self, url):
|
||||||
|
show_name = self._match_valid_url(url).group('show_name')
|
||||||
|
return self.playlist_result(self._entries(show_name), playlist_id=show_name)
|
||||||
|
|
||||||
|
|
||||||
|
class DiscoveryPlusItalyShowIE(DiscoveryPlusShowBaseIE):
|
||||||
|
_VALID_URL = r'https?://(?:www\.)?discoveryplus\.it/programmi/(?P<show_name>[^/]+)/?(?:[?#]|$)'
|
||||||
|
_TESTS = [{
|
||||||
|
'url': 'https://www.discoveryplus.it/programmi/deal-with-it-stai-al-gioco',
|
||||||
|
'playlist_mincount': 168,
|
||||||
|
'info_dict': {
|
||||||
|
'id': 'deal-with-it-stai-al-gioco',
|
||||||
|
},
|
||||||
|
}]
|
||||||
|
|
||||||
|
_BASE_API = 'https://disco-api.discoveryplus.it/'
|
||||||
|
_DOMAIN = 'https://www.discoveryplus.it/'
|
||||||
|
_X_CLIENT = 'WEB:UNKNOWN:dplay-client:2.6.0'
|
||||||
|
_REALM = 'dplayit'
|
||||||
|
_SHOW_STR = 'programmi'
|
||||||
|
_INDEX = 1
|
||||||
|
_VIDEO_IE = DPlayIE
|
||||||
|
|
||||||
|
|
||||||
|
class DiscoveryPlusIndiaShowIE(DiscoveryPlusShowBaseIE):
|
||||||
|
_VALID_URL = r'https?://(?:www\.)?discoveryplus\.in/show/(?P<show_name>[^/]+)/?(?:[?#]|$)'
|
||||||
|
_TESTS = [{
|
||||||
|
'url': 'https://www.discoveryplus.in/show/how-do-they-do-it',
|
||||||
|
'playlist_mincount': 140,
|
||||||
|
'info_dict': {
|
||||||
|
'id': 'how-do-they-do-it',
|
||||||
|
},
|
||||||
|
}]
|
||||||
|
|
||||||
|
_BASE_API = 'https://ap2-prod-direct.discoveryplus.in/'
|
||||||
|
_DOMAIN = 'https://www.discoveryplus.in/'
|
||||||
|
_X_CLIENT = 'WEB:UNKNOWN:dplus-india:prod'
|
||||||
|
_REALM = 'dplusindia'
|
||||||
|
_SHOW_STR = 'show'
|
||||||
|
_INDEX = 4
|
||||||
|
_VIDEO_IE = DiscoveryPlusIndiaIE
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ from ..utils import ExtractorError
|
|||||||
|
|
||||||
|
|
||||||
class EpiconIE(InfoExtractor):
|
class EpiconIE(InfoExtractor):
|
||||||
_VALID_URL = r'(?:https?://)(?:www\.)?epicon\.in/(?:documentaries|movies|tv-shows/[^/?#]+/[^/?#]+)/(?P<id>[^/?#]+)'
|
_VALID_URL = r'https?://(?:www\.)?epicon\.in/(?:documentaries|movies|tv-shows/[^/?#]+/[^/?#]+)/(?P<id>[^/?#]+)'
|
||||||
_TESTS = [{
|
_TESTS = [{
|
||||||
'url': 'https://www.epicon.in/documentaries/air-battle-of-srinagar',
|
'url': 'https://www.epicon.in/documentaries/air-battle-of-srinagar',
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
@@ -84,7 +84,7 @@ class EpiconIE(InfoExtractor):
|
|||||||
|
|
||||||
|
|
||||||
class EpiconSeriesIE(InfoExtractor):
|
class EpiconSeriesIE(InfoExtractor):
|
||||||
_VALID_URL = r'(?!.*season)(?:https?://)(?:www\.)?epicon\.in/tv-shows/(?P<id>[^/?#]+)'
|
_VALID_URL = r'(?!.*season)https?://(?:www\.)?epicon\.in/tv-shows/(?P<id>[^/?#]+)'
|
||||||
_TESTS = [{
|
_TESTS = [{
|
||||||
'url': 'https://www.epicon.in/tv-shows/1-of-something',
|
'url': 'https://www.epicon.in/tv-shows/1-of-something',
|
||||||
'playlist_mincount': 5,
|
'playlist_mincount': 5,
|
||||||
|
|||||||
@@ -7,7 +7,9 @@ from .once import OnceIE
|
|||||||
from ..compat import compat_str
|
from ..compat import compat_str
|
||||||
from ..utils import (
|
from ..utils import (
|
||||||
determine_ext,
|
determine_ext,
|
||||||
|
dict_get,
|
||||||
int_or_none,
|
int_or_none,
|
||||||
|
unified_strdate,
|
||||||
unified_timestamp,
|
unified_timestamp,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -236,3 +238,44 @@ class FiveThirtyEightIE(InfoExtractor):
|
|||||||
webpage, 'embed url')
|
webpage, 'embed url')
|
||||||
|
|
||||||
return self.url_result(embed_url, 'AbcNewsVideo')
|
return self.url_result(embed_url, 'AbcNewsVideo')
|
||||||
|
|
||||||
|
|
||||||
|
class ESPNCricInfoIE(InfoExtractor):
|
||||||
|
_VALID_URL = r'https?://(?:www\.)?espncricinfo\.com/video/[^#$&?/]+-(?P<id>\d+)'
|
||||||
|
_TESTS = [{
|
||||||
|
'url': 'https://www.espncricinfo.com/video/finch-chasing-comes-with-risks-despite-world-cup-trend-1289135',
|
||||||
|
'info_dict': {
|
||||||
|
'id': '1289135',
|
||||||
|
'ext': 'mp4',
|
||||||
|
'title': 'Finch: Chasing comes with \'risks\' despite World Cup trend',
|
||||||
|
'description': 'md5:ea32373303e25efbb146efdfc8a37829',
|
||||||
|
'upload_date': '20211113',
|
||||||
|
'duration': 96,
|
||||||
|
},
|
||||||
|
'params': {'skip_download': True}
|
||||||
|
}]
|
||||||
|
|
||||||
|
def _real_extract(self, url):
|
||||||
|
id = self._match_id(url)
|
||||||
|
data_json = self._download_json(f'https://hs-consumer-api.espncricinfo.com/v1/pages/video/video-details?videoId={id}', id)['video']
|
||||||
|
formats, subtitles = [], {}
|
||||||
|
for item in data_json.get('playbacks') or []:
|
||||||
|
if item.get('type') == 'HLS' and item.get('url'):
|
||||||
|
m3u8_frmts, m3u8_subs = self._extract_m3u8_formats_and_subtitles(item['url'], id)
|
||||||
|
formats.extend(m3u8_frmts)
|
||||||
|
subtitles = self._merge_subtitles(subtitles, m3u8_subs)
|
||||||
|
elif item.get('type') == 'AUDIO' and item.get('url'):
|
||||||
|
formats.append({
|
||||||
|
'url': item['url'],
|
||||||
|
'vcodec': 'none',
|
||||||
|
})
|
||||||
|
self._sort_formats(formats)
|
||||||
|
return {
|
||||||
|
'id': id,
|
||||||
|
'title': data_json.get('title'),
|
||||||
|
'description': data_json.get('summary'),
|
||||||
|
'upload_date': unified_strdate(dict_get(data_json, ('publishedAt', 'recordedAt'))),
|
||||||
|
'duration': data_json.get('duration'),
|
||||||
|
'formats': formats,
|
||||||
|
'subtitles': subtitles,
|
||||||
|
}
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ from ..utils import (
|
|||||||
|
|
||||||
|
|
||||||
class EUScreenIE(InfoExtractor):
|
class EUScreenIE(InfoExtractor):
|
||||||
_VALID_URL = r'(?:https?://)(?:www\.)?euscreen\.eu/item.html\?id=(?P<id>[^&?$/]+)'
|
_VALID_URL = r'https?://(?:www\.)?euscreen\.eu/item.html\?id=(?P<id>[^&?$/]+)'
|
||||||
|
|
||||||
_TESTS = [{
|
_TESTS = [{
|
||||||
'url': 'https://euscreen.eu/item.html?id=EUS_0EBCBF356BFC4E12A014023BA41BD98C',
|
'url': 'https://euscreen.eu/item.html?id=EUS_0EBCBF356BFC4E12A014023BA41BD98C',
|
||||||
|
|||||||
@@ -166,6 +166,7 @@ from .bleacherreport import (
|
|||||||
BleacherReportIE,
|
BleacherReportIE,
|
||||||
BleacherReportCMSIE,
|
BleacherReportCMSIE,
|
||||||
)
|
)
|
||||||
|
from .blogger import BloggerIE
|
||||||
from .bloomberg import BloombergIE
|
from .bloomberg import BloombergIE
|
||||||
from .bokecc import BokeCCIE
|
from .bokecc import BokeCCIE
|
||||||
from .bongacams import BongaCamsIE
|
from .bongacams import BongaCamsIE
|
||||||
@@ -178,6 +179,7 @@ from .br import (
|
|||||||
)
|
)
|
||||||
from .bravotv import BravoTVIE
|
from .bravotv import BravoTVIE
|
||||||
from .breakcom import BreakIE
|
from .breakcom import BreakIE
|
||||||
|
from .breitbart import BreitBartIE
|
||||||
from .brightcove import (
|
from .brightcove import (
|
||||||
BrightcoveLegacyIE,
|
BrightcoveLegacyIE,
|
||||||
BrightcoveNewIE,
|
BrightcoveNewIE,
|
||||||
@@ -186,6 +188,7 @@ from .businessinsider import BusinessInsiderIE
|
|||||||
from .buzzfeed import BuzzFeedIE
|
from .buzzfeed import BuzzFeedIE
|
||||||
from .byutv import BYUtvIE
|
from .byutv import BYUtvIE
|
||||||
from .c56 import C56IE
|
from .c56 import C56IE
|
||||||
|
from .cableav import CableAVIE
|
||||||
from .cam4 import CAM4IE
|
from .cam4 import CAM4IE
|
||||||
from .camdemy import (
|
from .camdemy import (
|
||||||
CamdemyIE,
|
CamdemyIE,
|
||||||
@@ -193,6 +196,7 @@ from .camdemy import (
|
|||||||
)
|
)
|
||||||
from .cammodels import CamModelsIE
|
from .cammodels import CamModelsIE
|
||||||
from .camwithher import CamWithHerIE
|
from .camwithher import CamWithHerIE
|
||||||
|
from .canalalpha import CanalAlphaIE
|
||||||
from .canalplus import CanalplusIE
|
from .canalplus import CanalplusIE
|
||||||
from .canalc2 import Canalc2IE
|
from .canalc2 import Canalc2IE
|
||||||
from .canvas import (
|
from .canvas import (
|
||||||
@@ -291,6 +295,7 @@ from .commonprotocols import (
|
|||||||
from .condenast import CondeNastIE
|
from .condenast import CondeNastIE
|
||||||
from .contv import CONtvIE
|
from .contv import CONtvIE
|
||||||
from .corus import CorusIE
|
from .corus import CorusIE
|
||||||
|
from .cozytv import CozyTVIE
|
||||||
from .cracked import CrackedIE
|
from .cracked import CrackedIE
|
||||||
from .crackle import CrackleIE
|
from .crackle import CrackleIE
|
||||||
from .crooksandliars import CrooksAndLiarsIE
|
from .crooksandliars import CrooksAndLiarsIE
|
||||||
@@ -307,7 +312,8 @@ from .ctvnews import CTVNewsIE
|
|||||||
from .cultureunplugged import CultureUnpluggedIE
|
from .cultureunplugged import CultureUnpluggedIE
|
||||||
from .curiositystream import (
|
from .curiositystream import (
|
||||||
CuriosityStreamIE,
|
CuriosityStreamIE,
|
||||||
CuriosityStreamCollectionIE,
|
CuriosityStreamCollectionsIE,
|
||||||
|
CuriosityStreamSeriesIE,
|
||||||
)
|
)
|
||||||
from .cwtv import CWTVIE
|
from .cwtv import CWTVIE
|
||||||
from .dailymail import DailyMailIE
|
from .dailymail import DailyMailIE
|
||||||
@@ -336,10 +342,6 @@ from .democracynow import DemocracynowIE
|
|||||||
from .dfb import DFBIE
|
from .dfb import DFBIE
|
||||||
from .dhm import DHMIE
|
from .dhm import DHMIE
|
||||||
from .digg import DiggIE
|
from .digg import DiggIE
|
||||||
from .discoveryplusindia import (
|
|
||||||
DiscoveryPlusIndiaIE,
|
|
||||||
DiscoveryPlusIndiaShowIE,
|
|
||||||
)
|
|
||||||
from .dotsub import DotsubIE
|
from .dotsub import DotsubIE
|
||||||
from .douyutv import (
|
from .douyutv import (
|
||||||
DouyuShowIE,
|
DouyuShowIE,
|
||||||
@@ -351,7 +353,11 @@ from .dplay import (
|
|||||||
HGTVDeIE,
|
HGTVDeIE,
|
||||||
ScienceChannelIE,
|
ScienceChannelIE,
|
||||||
DIYNetworkIE,
|
DIYNetworkIE,
|
||||||
AnimalPlanetIE
|
AnimalPlanetIE,
|
||||||
|
DiscoveryPlusIndiaIE,
|
||||||
|
DiscoveryNetworksDeIE,
|
||||||
|
DiscoveryPlusItalyShowIE,
|
||||||
|
DiscoveryPlusIndiaShowIE,
|
||||||
)
|
)
|
||||||
from .dreisat import DreiSatIE
|
from .dreisat import DreiSatIE
|
||||||
from .drbonanza import DRBonanzaIE
|
from .drbonanza import DRBonanzaIE
|
||||||
@@ -373,7 +379,6 @@ from .discoverygo import (
|
|||||||
DiscoveryGoIE,
|
DiscoveryGoIE,
|
||||||
DiscoveryGoPlaylistIE,
|
DiscoveryGoPlaylistIE,
|
||||||
)
|
)
|
||||||
from .discoverynetworks import DiscoveryNetworksDeIE
|
|
||||||
from .discoveryvr import DiscoveryVRIE
|
from .discoveryvr import DiscoveryVRIE
|
||||||
from .disney import DisneyIE
|
from .disney import DisneyIE
|
||||||
from .dispeak import DigitallySpeakingIE
|
from .dispeak import DigitallySpeakingIE
|
||||||
@@ -417,6 +422,7 @@ from .espn import (
|
|||||||
ESPNIE,
|
ESPNIE,
|
||||||
ESPNArticleIE,
|
ESPNArticleIE,
|
||||||
FiveThirtyEightIE,
|
FiveThirtyEightIE,
|
||||||
|
ESPNCricInfoIE,
|
||||||
)
|
)
|
||||||
from .esri import EsriVideoIE
|
from .esri import EsriVideoIE
|
||||||
from .europa import EuropaIE
|
from .europa import EuropaIE
|
||||||
@@ -701,6 +707,7 @@ from .line import (
|
|||||||
LineLiveChannelIE,
|
LineLiveChannelIE,
|
||||||
)
|
)
|
||||||
from .linkedin import (
|
from .linkedin import (
|
||||||
|
LinkedInIE,
|
||||||
LinkedInLearningIE,
|
LinkedInLearningIE,
|
||||||
LinkedInLearningCourseIE,
|
LinkedInLearningCourseIE,
|
||||||
)
|
)
|
||||||
@@ -792,6 +799,7 @@ from .mirrativ import (
|
|||||||
)
|
)
|
||||||
from .mit import TechTVMITIE, OCWMITIE
|
from .mit import TechTVMITIE, OCWMITIE
|
||||||
from .mitele import MiTeleIE
|
from .mitele import MiTeleIE
|
||||||
|
from .mixch import MixchIE
|
||||||
from .mixcloud import (
|
from .mixcloud import (
|
||||||
MixcloudIE,
|
MixcloudIE,
|
||||||
MixcloudUserIE,
|
MixcloudUserIE,
|
||||||
@@ -881,7 +889,10 @@ from .ndr import (
|
|||||||
NJoyEmbedIE,
|
NJoyEmbedIE,
|
||||||
)
|
)
|
||||||
from .ndtv import NDTVIE
|
from .ndtv import NDTVIE
|
||||||
from .nebula import NebulaIE
|
from .nebula import (
|
||||||
|
NebulaIE,
|
||||||
|
NebulaCollectionIE,
|
||||||
|
)
|
||||||
from .nerdcubed import NerdCubedFeedIE
|
from .nerdcubed import NerdCubedFeedIE
|
||||||
from .netzkino import NetzkinoIE
|
from .netzkino import NetzkinoIE
|
||||||
from .neteasemusic import (
|
from .neteasemusic import (
|
||||||
@@ -935,7 +946,10 @@ from .niconico import (
|
|||||||
NicovideoSearchIE,
|
NicovideoSearchIE,
|
||||||
NicovideoSearchURLIE,
|
NicovideoSearchURLIE,
|
||||||
)
|
)
|
||||||
from .ninecninemedia import NineCNineMediaIE
|
from .ninecninemedia import (
|
||||||
|
NineCNineMediaIE,
|
||||||
|
CPTwentyFourIE,
|
||||||
|
)
|
||||||
from .ninegag import NineGagIE
|
from .ninegag import NineGagIE
|
||||||
from .ninenow import NineNowIE
|
from .ninenow import NineNowIE
|
||||||
from .nintendo import NintendoIE
|
from .nintendo import NintendoIE
|
||||||
@@ -999,6 +1013,7 @@ from .oktoberfesttv import OktoberfestTVIE
|
|||||||
from .olympics import OlympicsReplayIE
|
from .olympics import OlympicsReplayIE
|
||||||
from .on24 import On24IE
|
from .on24 import On24IE
|
||||||
from .ondemandkorea import OnDemandKoreaIE
|
from .ondemandkorea import OnDemandKoreaIE
|
||||||
|
from .onefootball import OneFootballIE
|
||||||
from .onet import (
|
from .onet import (
|
||||||
OnetIE,
|
OnetIE,
|
||||||
OnetChannelIE,
|
OnetChannelIE,
|
||||||
@@ -1059,6 +1074,7 @@ from .peertube import (
|
|||||||
PeerTubeIE,
|
PeerTubeIE,
|
||||||
PeerTubePlaylistIE,
|
PeerTubePlaylistIE,
|
||||||
)
|
)
|
||||||
|
from .peertv import PeerTVIE
|
||||||
from .peloton import (
|
from .peloton import (
|
||||||
PelotonIE,
|
PelotonIE,
|
||||||
PelotonLiveIE
|
PelotonLiveIE
|
||||||
@@ -1159,6 +1175,7 @@ from .radiode import RadioDeIE
|
|||||||
from .radiojavan import RadioJavanIE
|
from .radiojavan import RadioJavanIE
|
||||||
from .radiobremen import RadioBremenIE
|
from .radiobremen import RadioBremenIE
|
||||||
from .radiofrance import RadioFranceIE
|
from .radiofrance import RadioFranceIE
|
||||||
|
from .radiozet import RadioZetPodcastIE
|
||||||
from .radiokapital import (
|
from .radiokapital import (
|
||||||
RadioKapitalIE,
|
RadioKapitalIE,
|
||||||
RadioKapitalShowIE,
|
RadioKapitalShowIE,
|
||||||
@@ -1198,9 +1215,11 @@ from .redbulltv import (
|
|||||||
RedBullTVRrnContentIE,
|
RedBullTVRrnContentIE,
|
||||||
RedBullIE,
|
RedBullIE,
|
||||||
)
|
)
|
||||||
from .reddit import (
|
from .reddit import RedditIE
|
||||||
RedditIE,
|
from .redgifs import (
|
||||||
RedditRIE,
|
RedGifsIE,
|
||||||
|
RedGifsSearchIE,
|
||||||
|
RedGifsUserIE,
|
||||||
)
|
)
|
||||||
from .redtube import RedTubeIE
|
from .redtube import RedTubeIE
|
||||||
from .regiotv import RegioTVIE
|
from .regiotv import RegioTVIE
|
||||||
@@ -1228,6 +1247,7 @@ from .rtl2 import (
|
|||||||
RTL2YouSeriesIE,
|
RTL2YouSeriesIE,
|
||||||
)
|
)
|
||||||
from .rtp import RTPIE
|
from .rtp import RTPIE
|
||||||
|
from .rtrfm import RTRFMIE
|
||||||
from .rts import RTSIE
|
from .rts import RTSIE
|
||||||
from .rtve import RTVEALaCartaIE, RTVELiveIE, RTVEInfantilIE, RTVELiveIE, RTVETelevisionIE
|
from .rtve import RTVEALaCartaIE, RTVELiveIE, RTVEInfantilIE, RTVELiveIE, RTVETelevisionIE
|
||||||
from .rtvnh import RTVNHIE
|
from .rtvnh import RTVNHIE
|
||||||
@@ -1269,7 +1289,7 @@ from .scte import (
|
|||||||
SCTECourseIE,
|
SCTECourseIE,
|
||||||
)
|
)
|
||||||
from .seeker import SeekerIE
|
from .seeker import SeekerIE
|
||||||
from .senateisvp import SenateISVPIE
|
from .senategov import SenateISVPIE, SenateGovIE
|
||||||
from .sendtonews import SendtoNewsIE
|
from .sendtonews import SendtoNewsIE
|
||||||
from .servus import ServusIE
|
from .servus import ServusIE
|
||||||
from .sevenplus import SevenPlusIE
|
from .sevenplus import SevenPlusIE
|
||||||
@@ -1396,8 +1416,10 @@ from .streamable import StreamableIE
|
|||||||
from .streamanity import StreamanityIE
|
from .streamanity import StreamanityIE
|
||||||
from .streamcloud import StreamcloudIE
|
from .streamcloud import StreamcloudIE
|
||||||
from .streamcz import StreamCZIE
|
from .streamcz import StreamCZIE
|
||||||
|
from .streamff import StreamFFIE
|
||||||
from .streetvoice import StreetVoiceIE
|
from .streetvoice import StreetVoiceIE
|
||||||
from .stretchinternet import StretchInternetIE
|
from .stretchinternet import StretchInternetIE
|
||||||
|
from .stripchat import StripchatIE
|
||||||
from .stv import STVPlayerIE
|
from .stv import STVPlayerIE
|
||||||
from .sunporno import SunPornoIE
|
from .sunporno import SunPornoIE
|
||||||
from .sverigesradio import (
|
from .sverigesradio import (
|
||||||
@@ -1782,6 +1804,7 @@ from .weibo import (
|
|||||||
WeiboMobileIE
|
WeiboMobileIE
|
||||||
)
|
)
|
||||||
from .weiqitv import WeiqiTVIE
|
from .weiqitv import WeiqiTVIE
|
||||||
|
from .willow import WillowIE
|
||||||
from .wimtv import WimTVIE
|
from .wimtv import WimTVIE
|
||||||
from .whowatch import WhoWatchIE
|
from .whowatch import WhoWatchIE
|
||||||
from .wistia import (
|
from .wistia import (
|
||||||
|
|||||||
@@ -185,7 +185,7 @@ class FranceTVIE(InfoExtractor):
|
|||||||
'vcodec': 'none',
|
'vcodec': 'none',
|
||||||
'ext': 'mhtml',
|
'ext': 'mhtml',
|
||||||
'protocol': 'mhtml',
|
'protocol': 'mhtml',
|
||||||
'url': 'about:dummy',
|
'url': 'about:invalid',
|
||||||
'fragments': [{
|
'fragments': [{
|
||||||
'path': sheet,
|
'path': sheet,
|
||||||
# XXX: not entirely accurate; each spritesheet seems to be
|
# XXX: not entirely accurate; each spritesheet seems to be
|
||||||
|
|||||||
@@ -276,7 +276,7 @@ class FunimationIE(FunimationBaseIE):
|
|||||||
def _get_subtitles(self, subtitles, experience_id, episode, display_id, format_name):
|
def _get_subtitles(self, subtitles, experience_id, episode, display_id, format_name):
|
||||||
if isinstance(episode, str):
|
if isinstance(episode, str):
|
||||||
webpage = self._download_webpage(
|
webpage = self._download_webpage(
|
||||||
f'https://www.funimation.com/player/{experience_id}', display_id,
|
f'https://www.funimation.com/player/{experience_id}/', display_id,
|
||||||
fatal=False, note=f'Downloading player webpage for {format_name}')
|
fatal=False, note=f'Downloading player webpage for {format_name}')
|
||||||
episode, _, _ = self._get_episode(webpage, episode_id=episode, fatal=False)
|
episode, _, _ = self._get_episode(webpage, episode_id=episode, fatal=False)
|
||||||
|
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ from ..utils import (
|
|||||||
|
|
||||||
|
|
||||||
class GabTVIE(InfoExtractor):
|
class GabTVIE(InfoExtractor):
|
||||||
_VALID_URL = r'(?:https?://)tv.gab.com/channel/[^/]+/view/(?P<id>[a-z0-9-]+)'
|
_VALID_URL = r'https?://tv\.gab\.com/channel/[^/]+/view/(?P<id>[a-z0-9-]+)'
|
||||||
_TESTS = [{
|
_TESTS = [{
|
||||||
'url': 'https://tv.gab.com/channel/wurzelroot/view/why-was-america-in-afghanistan-61217eacea5665de450d0488',
|
'url': 'https://tv.gab.com/channel/wurzelroot/view/why-was-america-in-afghanistan-61217eacea5665de450d0488',
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ from .sportbox import SportBoxIE
|
|||||||
from .myvi import MyviIE
|
from .myvi import MyviIE
|
||||||
from .condenast import CondeNastIE
|
from .condenast import CondeNastIE
|
||||||
from .udn import UDNEmbedIE
|
from .udn import UDNEmbedIE
|
||||||
from .senateisvp import SenateISVPIE
|
from .senategov import SenateISVPIE
|
||||||
from .svt import SVTIE
|
from .svt import SVTIE
|
||||||
from .pornhub import PornHubIE
|
from .pornhub import PornHubIE
|
||||||
from .xhamster import XHamsterEmbedIE
|
from .xhamster import XHamsterEmbedIE
|
||||||
@@ -136,6 +136,7 @@ from .medialaan import MedialaanIE
|
|||||||
from .simplecast import SimplecastIE
|
from .simplecast import SimplecastIE
|
||||||
from .wimtv import WimTVIE
|
from .wimtv import WimTVIE
|
||||||
from .tvp import TVPEmbedIE
|
from .tvp import TVPEmbedIE
|
||||||
|
from .blogger import BloggerIE
|
||||||
|
|
||||||
|
|
||||||
class GenericIE(InfoExtractor):
|
class GenericIE(InfoExtractor):
|
||||||
@@ -2173,6 +2174,17 @@ class GenericIE(InfoExtractor):
|
|||||||
'skip_download': True,
|
'skip_download': True,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
# blogger embed
|
||||||
|
'url': 'https://blog.tomeuvizoso.net/2019/01/a-panfrost-milestone.html',
|
||||||
|
'md5': 'f1bc19b6ea1b0fd1d81e84ca9ec467ac',
|
||||||
|
'info_dict': {
|
||||||
|
'id': 'BLOGGER-video-3c740e3a49197e16-796',
|
||||||
|
'ext': 'mp4',
|
||||||
|
'title': 'Blogger',
|
||||||
|
'thumbnail': r're:^https?://.*',
|
||||||
|
},
|
||||||
|
},
|
||||||
# {
|
# {
|
||||||
# # TODO: find another test
|
# # TODO: find another test
|
||||||
# # http://schema.org/VideoObject
|
# # http://schema.org/VideoObject
|
||||||
@@ -2332,6 +2344,34 @@ class GenericIE(InfoExtractor):
|
|||||||
'thumbnail': 'https://bogmedia.org/contents/videos_screenshots/21000/21217/preview_480p.mp4.jpg',
|
'thumbnail': 'https://bogmedia.org/contents/videos_screenshots/21000/21217/preview_480p.mp4.jpg',
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
# Reddit-hosted video that will redirect and be processed by RedditIE
|
||||||
|
# Redirects to https://www.reddit.com/r/videos/comments/6rrwyj/that_small_heart_attack/
|
||||||
|
'url': 'https://v.redd.it/zv89llsvexdz',
|
||||||
|
'md5': '87f5f02f6c1582654146f830f21f8662',
|
||||||
|
'info_dict': {
|
||||||
|
'id': 'zv89llsvexdz',
|
||||||
|
'ext': 'mp4',
|
||||||
|
'timestamp': 1501941939.0,
|
||||||
|
'title': 'That small heart attack.',
|
||||||
|
'upload_date': '20170805',
|
||||||
|
'uploader': 'Antw87'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
# 1080p Reddit-hosted video that will redirect and be processed by RedditIE
|
||||||
|
'url': 'https://v.redd.it/33hgok7dfbz71/',
|
||||||
|
'md5': '7a1d587940242c9bb3bd6eb320b39258',
|
||||||
|
'info_dict': {
|
||||||
|
'id': '33hgok7dfbz71',
|
||||||
|
'ext': 'mp4',
|
||||||
|
'title': "The game Didn't want me to Knife that Guy I guess",
|
||||||
|
'uploader': 'paraf1ve',
|
||||||
|
'timestamp': 1636788683.0,
|
||||||
|
'upload_date': '20211113'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#
|
||||||
]
|
]
|
||||||
|
|
||||||
def report_following_redirect(self, new_url):
|
def report_following_redirect(self, new_url):
|
||||||
@@ -2561,6 +2601,8 @@ class GenericIE(InfoExtractor):
|
|||||||
subtitles = {}
|
subtitles = {}
|
||||||
if format_id.endswith('mpegurl'):
|
if format_id.endswith('mpegurl'):
|
||||||
formats, subtitles = self._extract_m3u8_formats_and_subtitles(url, video_id, 'mp4')
|
formats, subtitles = self._extract_m3u8_formats_and_subtitles(url, video_id, 'mp4')
|
||||||
|
elif format_id.endswith('mpd') or format_id.endswith('dash+xml'):
|
||||||
|
formats, subtitles = self._extract_mpd_formats_and_subtitles(url, video_id)
|
||||||
elif format_id == 'f4m':
|
elif format_id == 'f4m':
|
||||||
formats = self._extract_f4m_formats(url, video_id)
|
formats = self._extract_f4m_formats(url, video_id)
|
||||||
else:
|
else:
|
||||||
@@ -3216,6 +3258,11 @@ class GenericIE(InfoExtractor):
|
|||||||
if onionstudios_url:
|
if onionstudios_url:
|
||||||
return self.url_result(onionstudios_url)
|
return self.url_result(onionstudios_url)
|
||||||
|
|
||||||
|
# Look for Blogger embeds
|
||||||
|
blogger_urls = BloggerIE._extract_urls(webpage)
|
||||||
|
if blogger_urls:
|
||||||
|
return self.playlist_from_matches(blogger_urls, video_id, video_title, ie=BloggerIE.ie_key())
|
||||||
|
|
||||||
# Look for ViewLift embeds
|
# Look for ViewLift embeds
|
||||||
viewlift_url = ViewLiftEmbedIE._extract_url(webpage)
|
viewlift_url = ViewLiftEmbedIE._extract_url(webpage)
|
||||||
if viewlift_url:
|
if viewlift_url:
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ from ..utils import unified_strdate
|
|||||||
|
|
||||||
|
|
||||||
class GronkhIE(InfoExtractor):
|
class GronkhIE(InfoExtractor):
|
||||||
_VALID_URL = r'(?:https?://)(?:www\.)?gronkh\.tv/stream/(?P<id>\d+)'
|
_VALID_URL = r'https?://(?:www\.)?gronkh\.tv/stream/(?P<id>\d+)'
|
||||||
|
|
||||||
_TESTS = [{
|
_TESTS = [{
|
||||||
'url': 'https://gronkh.tv/stream/536',
|
'url': 'https://gronkh.tv/stream/536',
|
||||||
|
|||||||
@@ -230,6 +230,11 @@ class HotStarIE(HotStarBaseIE):
|
|||||||
if tags and 'encryption:plain' not in tags:
|
if tags and 'encryption:plain' not in tags:
|
||||||
for f in current_formats:
|
for f in current_formats:
|
||||||
f['has_drm'] = True
|
f['has_drm'] = True
|
||||||
|
if tags and 'language' in tags:
|
||||||
|
lang = re.search(r'language:(?P<lang>[a-z]+)', tags).group('lang')
|
||||||
|
for f in current_formats:
|
||||||
|
if not f.get('langauge'):
|
||||||
|
f['language'] = lang
|
||||||
formats.extend(current_formats)
|
formats.extend(current_formats)
|
||||||
subs = self._merge_subtitles(subs, current_subs)
|
subs = self._merge_subtitles(subs, current_subs)
|
||||||
if not formats and geo_restricted:
|
if not formats and geo_restricted:
|
||||||
@@ -291,7 +296,7 @@ class HotStarPlaylistIE(HotStarBaseIE):
|
|||||||
|
|
||||||
class HotStarSeriesIE(HotStarBaseIE):
|
class HotStarSeriesIE(HotStarBaseIE):
|
||||||
IE_NAME = 'hotstar:series'
|
IE_NAME = 'hotstar:series'
|
||||||
_VALID_URL = r'(?P<url>(?:https?://)(?:www\.)?hotstar\.com(?:/in)?/tv/[^/]+/(?P<id>\d+))'
|
_VALID_URL = r'(?P<url>https?://(?:www\.)?hotstar\.com(?:/in)?/tv/[^/]+/(?P<id>\d+))'
|
||||||
_TESTS = [{
|
_TESTS = [{
|
||||||
'url': 'https://www.hotstar.com/in/tv/radhakrishn/1260000646',
|
'url': 'https://www.hotstar.com/in/tv/radhakrishn/1260000646',
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
|
|||||||
+122
-175
@@ -1,5 +1,4 @@
|
|||||||
# coding: utf-8
|
# coding: utf-8
|
||||||
from __future__ import unicode_literals
|
|
||||||
|
|
||||||
import itertools
|
import itertools
|
||||||
import hashlib
|
import hashlib
|
||||||
@@ -9,7 +8,6 @@ import time
|
|||||||
|
|
||||||
from .common import InfoExtractor
|
from .common import InfoExtractor
|
||||||
from ..compat import (
|
from ..compat import (
|
||||||
compat_str,
|
|
||||||
compat_HTTPError,
|
compat_HTTPError,
|
||||||
)
|
)
|
||||||
from ..utils import (
|
from ..utils import (
|
||||||
@@ -19,9 +17,8 @@ from ..utils import (
|
|||||||
int_or_none,
|
int_or_none,
|
||||||
lowercase_escape,
|
lowercase_escape,
|
||||||
std_headers,
|
std_headers,
|
||||||
try_get,
|
traverse_obj,
|
||||||
url_or_none,
|
url_or_none,
|
||||||
variadic,
|
|
||||||
urlencode_postdata,
|
urlencode_postdata,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -66,12 +63,68 @@ class InstagramBaseIE(InfoExtractor):
|
|||||||
if not login.get('authenticated'):
|
if not login.get('authenticated'):
|
||||||
if login.get('message'):
|
if login.get('message'):
|
||||||
raise ExtractorError(f'Unable to login: {login["message"]}')
|
raise ExtractorError(f'Unable to login: {login["message"]}')
|
||||||
|
elif login.get('user'):
|
||||||
|
raise ExtractorError('Unable to login: Sorry, your password was incorrect. Please double-check your password.', expected=True)
|
||||||
|
elif login.get('user') is False:
|
||||||
|
raise ExtractorError('Unable to login: The username you entered doesn\'t belong to an account. Please check your username and try again.', expected=True)
|
||||||
raise ExtractorError('Unable to login')
|
raise ExtractorError('Unable to login')
|
||||||
InstagramBaseIE._IS_LOGGED_IN = True
|
InstagramBaseIE._IS_LOGGED_IN = True
|
||||||
|
|
||||||
def _real_initialize(self):
|
def _real_initialize(self):
|
||||||
self._login()
|
self._login()
|
||||||
|
|
||||||
|
def _get_count(self, media, kind, *keys):
|
||||||
|
return traverse_obj(
|
||||||
|
media, (kind, 'count'), *((f'edge_media_{key}', 'count') for key in keys),
|
||||||
|
expected_type=int_or_none)
|
||||||
|
|
||||||
|
def _get_dimension(self, name, media, webpage=None):
|
||||||
|
return (
|
||||||
|
traverse_obj(media, ('dimensions', name), expected_type=int_or_none)
|
||||||
|
or int_or_none(self._html_search_meta(
|
||||||
|
(f'og:video:{name}', f'video:{name}'), webpage or '', default=None)))
|
||||||
|
|
||||||
|
def _extract_nodes(self, nodes, is_direct=False):
|
||||||
|
for idx, node in enumerate(nodes, start=1):
|
||||||
|
if node.get('__typename') != 'GraphVideo' and node.get('is_video') is not True:
|
||||||
|
continue
|
||||||
|
|
||||||
|
video_id = node.get('shortcode')
|
||||||
|
|
||||||
|
if is_direct:
|
||||||
|
info = {
|
||||||
|
'id': video_id or node['id'],
|
||||||
|
'url': node.get('video_url'),
|
||||||
|
'width': self._get_dimension('width', node),
|
||||||
|
'height': self._get_dimension('height', node),
|
||||||
|
'http_headers': {
|
||||||
|
'Referer': 'https://www.instagram.com/',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
elif not video_id:
|
||||||
|
continue
|
||||||
|
else:
|
||||||
|
info = {
|
||||||
|
'_type': 'url',
|
||||||
|
'ie_key': 'Instagram',
|
||||||
|
'id': video_id,
|
||||||
|
'url': f'https://instagram.com/p/{video_id}',
|
||||||
|
}
|
||||||
|
|
||||||
|
yield {
|
||||||
|
**info,
|
||||||
|
'title': node.get('title') or (f'Video {idx}' if is_direct else None),
|
||||||
|
'description': traverse_obj(
|
||||||
|
node, ('edge_media_to_caption', 'edges', 0, 'node', 'text'), expected_type=str),
|
||||||
|
'thumbnail': traverse_obj(
|
||||||
|
node, 'display_url', 'thumbnail_src', 'display_src', expected_type=url_or_none),
|
||||||
|
'duration': float_or_none(node.get('video_duration')),
|
||||||
|
'timestamp': int_or_none(node.get('taken_at_timestamp')),
|
||||||
|
'view_count': int_or_none(node.get('video_view_count')),
|
||||||
|
'comment_count': self._get_count(node, 'comments', 'preview_comment', 'to_comment', 'to_parent_comment'),
|
||||||
|
'like_count': self._get_count(node, 'likes', 'preview_like'),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
class InstagramIOSIE(InfoExtractor):
|
class InstagramIOSIE(InfoExtractor):
|
||||||
IE_DESC = 'IOS instagram:// URL'
|
IE_DESC = 'IOS instagram:// URL'
|
||||||
@@ -234,29 +287,22 @@ class InstagramIE(InstagramBaseIE):
|
|||||||
return mobj.group('link')
|
return mobj.group('link')
|
||||||
|
|
||||||
def _real_extract(self, url):
|
def _real_extract(self, url):
|
||||||
mobj = self._match_valid_url(url)
|
video_id, url = self._match_valid_url(url).group('id', 'url')
|
||||||
video_id = mobj.group('id')
|
|
||||||
url = mobj.group('url')
|
|
||||||
|
|
||||||
webpage, urlh = self._download_webpage_handle(url, video_id)
|
webpage, urlh = self._download_webpage_handle(url, video_id)
|
||||||
if 'www.instagram.com/accounts/login' in urlh.geturl().rstrip('/'):
|
if 'www.instagram.com/accounts/login' in urlh.geturl():
|
||||||
self.raise_login_required('You need to log in to access this content')
|
self.raise_login_required('You need to log in to access this content')
|
||||||
|
|
||||||
(media, video_url, description, thumbnails, timestamp, uploader,
|
|
||||||
uploader_id, like_count, comment_count, comments, height,
|
|
||||||
width) = [None] * 12
|
|
||||||
|
|
||||||
shared_data = self._parse_json(
|
shared_data = self._parse_json(
|
||||||
self._search_regex(
|
self._search_regex(
|
||||||
r'window\._sharedData\s*=\s*({.+?});',
|
r'window\._sharedData\s*=\s*({.+?});',
|
||||||
webpage, 'shared data', default='{}'),
|
webpage, 'shared data', default='{}'),
|
||||||
video_id, fatal=False)
|
video_id, fatal=False)
|
||||||
if shared_data:
|
media = traverse_obj(
|
||||||
media = try_get(
|
shared_data,
|
||||||
shared_data,
|
('entry_data', 'PostPage', 0, 'graphql', 'shortcode_media'),
|
||||||
(lambda x: x['entry_data']['PostPage'][0]['graphql']['shortcode_media'],
|
('entry_data', 'PostPage', 0, 'media'),
|
||||||
lambda x: x['entry_data']['PostPage'][0]['media']),
|
expected_type=dict)
|
||||||
dict)
|
|
||||||
# _sharedData.entry_data.PostPage is empty when authenticated (see
|
# _sharedData.entry_data.PostPage is empty when authenticated (see
|
||||||
# https://github.com/ytdl-org/youtube-dl/pull/22880)
|
# https://github.com/ytdl-org/youtube-dl/pull/22880)
|
||||||
if not media:
|
if not media:
|
||||||
@@ -265,125 +311,71 @@ class InstagramIE(InstagramBaseIE):
|
|||||||
r'window\.__additionalDataLoaded\s*\(\s*[^,]+,\s*({.+?})\s*\)\s*;',
|
r'window\.__additionalDataLoaded\s*\(\s*[^,]+,\s*({.+?})\s*\)\s*;',
|
||||||
webpage, 'additional data', default='{}'),
|
webpage, 'additional data', default='{}'),
|
||||||
video_id, fatal=False)
|
video_id, fatal=False)
|
||||||
if additional_data:
|
media = traverse_obj(additional_data, ('graphql', 'shortcode_media'), expected_type=dict) or {}
|
||||||
media = try_get(
|
|
||||||
additional_data, lambda x: x['graphql']['shortcode_media'],
|
|
||||||
dict)
|
|
||||||
if media:
|
|
||||||
video_url = media.get('video_url')
|
|
||||||
height = int_or_none(self._html_search_meta(('og:video:height', 'video:height'), webpage)) or try_get(media, lambda x: x['dimensions']['height'])
|
|
||||||
width = int_or_none(self._html_search_meta(('og:video:width', 'video:width'), webpage)) or try_get(media, lambda x: x['dimensions']['width'])
|
|
||||||
description = try_get(
|
|
||||||
media, lambda x: x['edge_media_to_caption']['edges'][0]['node']['text'],
|
|
||||||
compat_str) or media.get('caption')
|
|
||||||
title = media.get('title')
|
|
||||||
display_resources = media.get('display_resources')
|
|
||||||
if not display_resources:
|
|
||||||
display_resources = [{'src': media.get('display_src')}, {'src': media.get('display_url')}]
|
|
||||||
duration = float_or_none(media.get('video_duration'))
|
|
||||||
timestamp = int_or_none(media.get('taken_at_timestamp') or media.get('date'))
|
|
||||||
uploader = try_get(media, lambda x: x['owner']['full_name'])
|
|
||||||
uploader_id = try_get(media, lambda x: x['owner']['username'])
|
|
||||||
|
|
||||||
def get_count(keys, kind):
|
uploader_id = traverse_obj(media, ('owner', 'username')) or self._search_regex(
|
||||||
for key in variadic(keys):
|
r'"owner"\s*:\s*{\s*"username"\s*:\s*"(.+?)"', webpage, 'uploader id', fatal=False)
|
||||||
count = int_or_none(try_get(
|
|
||||||
media, (lambda x: x['edge_media_%s' % key]['count'],
|
|
||||||
lambda x: x['%ss' % kind]['count'])))
|
|
||||||
if count is not None:
|
|
||||||
return count
|
|
||||||
|
|
||||||
like_count = get_count('preview_like', 'like')
|
|
||||||
comment_count = get_count(
|
|
||||||
('preview_comment', 'to_comment', 'to_parent_comment'), 'comment')
|
|
||||||
|
|
||||||
thumbnails = [{
|
|
||||||
'url': thumbnail['src'],
|
|
||||||
'width': thumbnail.get('config_width'),
|
|
||||||
'height': thumbnail.get('config_height'),
|
|
||||||
} for thumbnail in display_resources if thumbnail.get('src')]
|
|
||||||
|
|
||||||
comments = []
|
|
||||||
for comment in try_get(media, lambda x: x['edge_media_to_parent_comment']['edges']):
|
|
||||||
comment_dict = comment.get('node', {})
|
|
||||||
comment_text = comment_dict.get('text')
|
|
||||||
if comment_text:
|
|
||||||
comments.append({
|
|
||||||
'author': try_get(comment_dict, lambda x: x['owner']['username']),
|
|
||||||
'author_id': try_get(comment_dict, lambda x: x['owner']['id']),
|
|
||||||
'id': comment_dict.get('id'),
|
|
||||||
'text': comment_text,
|
|
||||||
'timestamp': int_or_none(comment_dict.get('created_at')),
|
|
||||||
})
|
|
||||||
if not video_url:
|
|
||||||
edges = try_get(
|
|
||||||
media, lambda x: x['edge_sidecar_to_children']['edges'],
|
|
||||||
list) or []
|
|
||||||
if edges:
|
|
||||||
entries = []
|
|
||||||
for edge_num, edge in enumerate(edges, start=1):
|
|
||||||
node = try_get(edge, lambda x: x['node'], dict)
|
|
||||||
if not node:
|
|
||||||
continue
|
|
||||||
node_video_url = url_or_none(node.get('video_url'))
|
|
||||||
if not node_video_url:
|
|
||||||
continue
|
|
||||||
entries.append({
|
|
||||||
'id': node.get('shortcode') or node['id'],
|
|
||||||
'title': node.get('title') or 'Video %d' % edge_num,
|
|
||||||
'url': node_video_url,
|
|
||||||
'thumbnail': node.get('display_url'),
|
|
||||||
'duration': float_or_none(node.get('video_duration')),
|
|
||||||
'width': int_or_none(try_get(node, lambda x: x['dimensions']['width'])),
|
|
||||||
'height': int_or_none(try_get(node, lambda x: x['dimensions']['height'])),
|
|
||||||
'view_count': int_or_none(node.get('video_view_count')),
|
|
||||||
})
|
|
||||||
return self.playlist_result(
|
|
||||||
entries, video_id,
|
|
||||||
'Post by %s' % uploader_id if uploader_id else None,
|
|
||||||
description)
|
|
||||||
|
|
||||||
if not video_url:
|
|
||||||
video_url = self._og_search_video_url(webpage, secure=False)
|
|
||||||
|
|
||||||
formats = [{
|
|
||||||
'url': video_url,
|
|
||||||
'width': width,
|
|
||||||
'height': height,
|
|
||||||
}]
|
|
||||||
dash = try_get(media, lambda x: x['dash_info']['video_dash_manifest'])
|
|
||||||
if dash:
|
|
||||||
formats.extend(self._parse_mpd_formats(self._parse_xml(dash, video_id), mpd_id='dash'))
|
|
||||||
self._sort_formats(formats)
|
|
||||||
|
|
||||||
if not uploader_id:
|
|
||||||
uploader_id = self._search_regex(
|
|
||||||
r'"owner"\s*:\s*{\s*"username"\s*:\s*"(.+?)"',
|
|
||||||
webpage, 'uploader id', fatal=False)
|
|
||||||
|
|
||||||
|
description = (
|
||||||
|
traverse_obj(media, ('edge_media_to_caption', 'edges', 0, 'node', 'text'), expected_type=str)
|
||||||
|
or media.get('caption'))
|
||||||
if not description:
|
if not description:
|
||||||
description = self._search_regex(
|
description = self._search_regex(
|
||||||
r'"caption"\s*:\s*"(.+?)"', webpage, 'description', default=None)
|
r'"caption"\s*:\s*"(.+?)"', webpage, 'description', default=None)
|
||||||
if description is not None:
|
if description is not None:
|
||||||
description = lowercase_escape(description)
|
description = lowercase_escape(description)
|
||||||
|
|
||||||
if not thumbnails:
|
video_url = media.get('video_url')
|
||||||
thumbnails = self._og_search_thumbnail(webpage)
|
if not video_url:
|
||||||
|
nodes = traverse_obj(media, ('edge_sidecar_to_children', 'edges', ..., 'node'), expected_type=dict) or []
|
||||||
|
if nodes:
|
||||||
|
return self.playlist_result(
|
||||||
|
self._extract_nodes(nodes, True), video_id,
|
||||||
|
'Post by %s' % uploader_id if uploader_id else None, description)
|
||||||
|
|
||||||
|
video_url = self._og_search_video_url(webpage, secure=False)
|
||||||
|
|
||||||
|
formats = [{
|
||||||
|
'url': video_url,
|
||||||
|
'width': self._get_dimension('width', media, webpage),
|
||||||
|
'height': self._get_dimension('height', media, webpage),
|
||||||
|
}]
|
||||||
|
dash = traverse_obj(media, ('dash_info', 'video_dash_manifest'))
|
||||||
|
if dash:
|
||||||
|
formats.extend(self._parse_mpd_formats(self._parse_xml(dash, video_id), mpd_id='dash'))
|
||||||
|
self._sort_formats(formats)
|
||||||
|
|
||||||
|
comments = [{
|
||||||
|
'author': traverse_obj(comment_dict, ('node', 'owner', 'username')),
|
||||||
|
'author_id': traverse_obj(comment_dict, ('node', 'owner', 'id')),
|
||||||
|
'id': traverse_obj(comment_dict, ('node', 'id')),
|
||||||
|
'text': traverse_obj(comment_dict, ('node', 'text')),
|
||||||
|
'timestamp': traverse_obj(comment_dict, ('node', 'created_at'), expected_type=int_or_none),
|
||||||
|
} for comment_dict in traverse_obj(media, ('edge_media_to_parent_comment', 'edges'))]
|
||||||
|
|
||||||
|
display_resources = (
|
||||||
|
media.get('display_resources')
|
||||||
|
or [{'src': media.get(key)} for key in ('display_src', 'display_url')]
|
||||||
|
or [{'src': self._og_search_thumbnail(webpage)}])
|
||||||
|
thumbnails = [{
|
||||||
|
'url': thumbnail['src'],
|
||||||
|
'width': thumbnail.get('config_width'),
|
||||||
|
'height': thumbnail.get('config_height'),
|
||||||
|
} for thumbnail in display_resources if thumbnail.get('src')]
|
||||||
|
|
||||||
return {
|
return {
|
||||||
'id': video_id,
|
'id': video_id,
|
||||||
'formats': formats,
|
'formats': formats,
|
||||||
'ext': 'mp4',
|
'title': media.get('title') or 'Video by %s' % uploader_id,
|
||||||
'title': title or 'Video by %s' % uploader_id,
|
|
||||||
'description': description,
|
'description': description,
|
||||||
'duration': duration,
|
'duration': float_or_none(media.get('video_duration')),
|
||||||
'thumbnails': thumbnails,
|
'timestamp': traverse_obj(media, 'taken_at_timestamp', 'date', expected_type=int_or_none),
|
||||||
'timestamp': timestamp,
|
|
||||||
'uploader_id': uploader_id,
|
'uploader_id': uploader_id,
|
||||||
'uploader': uploader,
|
'uploader': traverse_obj(media, ('owner', 'full_name')),
|
||||||
'like_count': like_count,
|
'like_count': self._get_count(media, 'likes', 'preview_like'),
|
||||||
'comment_count': comment_count,
|
'comment_count': self._get_count(media, 'comments', 'preview_comment', 'to_comment', 'to_parent_comment'),
|
||||||
'comments': comments,
|
'comments': comments,
|
||||||
|
'thumbnails': thumbnails,
|
||||||
'http_headers': {
|
'http_headers': {
|
||||||
'Referer': 'https://www.instagram.com/',
|
'Referer': 'https://www.instagram.com/',
|
||||||
}
|
}
|
||||||
@@ -402,10 +394,6 @@ class InstagramPlaylistBaseIE(InstagramBaseIE):
|
|||||||
|
|
||||||
def _extract_graphql(self, data, url):
|
def _extract_graphql(self, data, url):
|
||||||
# Parses GraphQL queries containing videos and generates a playlist.
|
# Parses GraphQL queries containing videos and generates a playlist.
|
||||||
def get_count(suffix):
|
|
||||||
return int_or_none(try_get(
|
|
||||||
node, lambda x: x['edge_media_' + suffix]['count']))
|
|
||||||
|
|
||||||
uploader_id = self._match_id(url)
|
uploader_id = self._match_id(url)
|
||||||
csrf_token = data['config']['csrf_token']
|
csrf_token = data['config']['csrf_token']
|
||||||
rhx_gis = data.get('rhx_gis') or '3c7ca9dcefcf966d11dacf1f151335e8'
|
rhx_gis = data.get('rhx_gis') or '3c7ca9dcefcf966d11dacf1f151335e8'
|
||||||
@@ -454,55 +442,14 @@ class InstagramPlaylistBaseIE(InstagramBaseIE):
|
|||||||
continue
|
continue
|
||||||
raise
|
raise
|
||||||
|
|
||||||
edges = media.get('edges')
|
nodes = traverse_obj(media, ('edges', ..., 'node'), expected_type=dict) or []
|
||||||
if not edges or not isinstance(edges, list):
|
if not nodes:
|
||||||
break
|
break
|
||||||
|
yield from self._extract_nodes(nodes)
|
||||||
|
|
||||||
for edge in edges:
|
has_next_page = traverse_obj(media, ('page_info', 'has_next_page'))
|
||||||
node = edge.get('node')
|
cursor = traverse_obj(media, ('page_info', 'end_cursor'), expected_type=str)
|
||||||
if not node or not isinstance(node, dict):
|
if not has_next_page or not cursor:
|
||||||
continue
|
|
||||||
if node.get('__typename') != 'GraphVideo' and node.get('is_video') is not True:
|
|
||||||
continue
|
|
||||||
video_id = node.get('shortcode')
|
|
||||||
if not video_id:
|
|
||||||
continue
|
|
||||||
|
|
||||||
info = self.url_result(
|
|
||||||
'https://instagram.com/p/%s/' % video_id,
|
|
||||||
ie=InstagramIE.ie_key(), video_id=video_id)
|
|
||||||
|
|
||||||
description = try_get(
|
|
||||||
node, lambda x: x['edge_media_to_caption']['edges'][0]['node']['text'],
|
|
||||||
compat_str)
|
|
||||||
thumbnail = node.get('thumbnail_src') or node.get('display_src')
|
|
||||||
timestamp = int_or_none(node.get('taken_at_timestamp'))
|
|
||||||
|
|
||||||
comment_count = get_count('to_comment')
|
|
||||||
like_count = get_count('preview_like')
|
|
||||||
view_count = int_or_none(node.get('video_view_count'))
|
|
||||||
|
|
||||||
info.update({
|
|
||||||
'description': description,
|
|
||||||
'thumbnail': thumbnail,
|
|
||||||
'timestamp': timestamp,
|
|
||||||
'comment_count': comment_count,
|
|
||||||
'like_count': like_count,
|
|
||||||
'view_count': view_count,
|
|
||||||
})
|
|
||||||
|
|
||||||
yield info
|
|
||||||
|
|
||||||
page_info = media.get('page_info')
|
|
||||||
if not page_info or not isinstance(page_info, dict):
|
|
||||||
break
|
|
||||||
|
|
||||||
has_next_page = page_info.get('has_next_page')
|
|
||||||
if not has_next_page:
|
|
||||||
break
|
|
||||||
|
|
||||||
cursor = page_info.get('end_cursor')
|
|
||||||
if not cursor or not isinstance(cursor, compat_str):
|
|
||||||
break
|
break
|
||||||
|
|
||||||
def _real_extract(self, url):
|
def _real_extract(self, url):
|
||||||
@@ -552,7 +499,7 @@ class InstagramUserIE(InstagramPlaylistBaseIE):
|
|||||||
|
|
||||||
class InstagramTagIE(InstagramPlaylistBaseIE):
|
class InstagramTagIE(InstagramPlaylistBaseIE):
|
||||||
_VALID_URL = r'https?://(?:www\.)?instagram\.com/explore/tags/(?P<id>[^/]+)'
|
_VALID_URL = r'https?://(?:www\.)?instagram\.com/explore/tags/(?P<id>[^/]+)'
|
||||||
IE_DESC = 'Instagram hashtag search'
|
IE_DESC = 'Instagram hashtag search URLs'
|
||||||
IE_NAME = 'instagram:tag'
|
IE_NAME = 'instagram:tag'
|
||||||
_TESTS = [{
|
_TESTS = [{
|
||||||
'url': 'https://instagram.com/explore/tags/lolcats',
|
'url': 'https://instagram.com/explore/tags/lolcats',
|
||||||
|
|||||||
@@ -117,7 +117,7 @@ class ITVIE(InfoExtractor):
|
|||||||
# See: https://github.com/yt-dlp/yt-dlp/issues/986
|
# See: https://github.com/yt-dlp/yt-dlp/issues/986
|
||||||
platform_tag_subs, featureset_subs = next(
|
platform_tag_subs, featureset_subs = next(
|
||||||
((platform_tag, featureset)
|
((platform_tag, featureset)
|
||||||
for platform_tag, featuresets in reversed(variants.items()) for featureset in featuresets
|
for platform_tag, featuresets in reversed(list(variants.items())) for featureset in featuresets
|
||||||
if try_get(featureset, lambda x: x[2]) == 'outband-webvtt'),
|
if try_get(featureset, lambda x: x[2]) == 'outband-webvtt'),
|
||||||
(None, None))
|
(None, None))
|
||||||
|
|
||||||
@@ -146,8 +146,8 @@ class ITVIE(InfoExtractor):
|
|||||||
# See: https://github.com/yt-dlp/yt-dlp/issues/986
|
# See: https://github.com/yt-dlp/yt-dlp/issues/986
|
||||||
platform_tag_video, featureset_video = next(
|
platform_tag_video, featureset_video = next(
|
||||||
((platform_tag, featureset)
|
((platform_tag, featureset)
|
||||||
for platform_tag, featuresets in reversed(variants.items()) for featureset in featuresets
|
for platform_tag, featuresets in reversed(list(variants.items())) for featureset in featuresets
|
||||||
if try_get(featureset, lambda x: x[:2]) == ['hls', 'aes']),
|
if set(try_get(featureset, lambda x: x[:2]) or []) == {'aes', 'hls'}),
|
||||||
(None, None))
|
(None, None))
|
||||||
if not platform_tag_video or not featureset_video:
|
if not platform_tag_video or not featureset_video:
|
||||||
raise ExtractorError('No downloads available', expected=True, video_id=video_id)
|
raise ExtractorError('No downloads available', expected=True, video_id=video_id)
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ from ..utils import (
|
|||||||
|
|
||||||
|
|
||||||
class KooIE(InfoExtractor):
|
class KooIE(InfoExtractor):
|
||||||
_VALID_URL = r'(?:https?://)(?:www\.)?kooapp\.com/koo/[^/]+/(?P<id>[^/&#$?]+)'
|
_VALID_URL = r'https?://(?:www\.)?kooapp\.com/koo/[^/]+/(?P<id>[^/&#$?]+)'
|
||||||
_TESTS = [{ # Test for video in the comments
|
_TESTS = [{ # Test for video in the comments
|
||||||
'url': 'https://www.kooapp.com/koo/ytdlpTestAccount/946c4189-bc2d-4524-b95b-43f641e2adde',
|
'url': 'https://www.kooapp.com/koo/ytdlpTestAccount/946c4189-bc2d-4524-b95b-43f641e2adde',
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
|
|||||||
@@ -6,21 +6,56 @@ import re
|
|||||||
|
|
||||||
from .common import InfoExtractor
|
from .common import InfoExtractor
|
||||||
from ..utils import (
|
from ..utils import (
|
||||||
|
clean_html,
|
||||||
|
extract_attributes,
|
||||||
ExtractorError,
|
ExtractorError,
|
||||||
float_or_none,
|
float_or_none,
|
||||||
|
get_element_by_class,
|
||||||
int_or_none,
|
int_or_none,
|
||||||
srt_subtitles_timecode,
|
srt_subtitles_timecode,
|
||||||
|
strip_or_none,
|
||||||
|
mimetype2ext,
|
||||||
try_get,
|
try_get,
|
||||||
urlencode_postdata,
|
urlencode_postdata,
|
||||||
urljoin,
|
urljoin,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class LinkedInLearningBaseIE(InfoExtractor):
|
class LinkedInBaseIE(InfoExtractor):
|
||||||
_NETRC_MACHINE = 'linkedin'
|
_NETRC_MACHINE = 'linkedin'
|
||||||
_LOGIN_URL = 'https://www.linkedin.com/uas/login?trk=learning'
|
|
||||||
_logged_in = False
|
_logged_in = False
|
||||||
|
|
||||||
|
def _real_initialize(self):
|
||||||
|
if self._logged_in:
|
||||||
|
return
|
||||||
|
email, password = self._get_login_info()
|
||||||
|
if email is None:
|
||||||
|
return
|
||||||
|
|
||||||
|
login_page = self._download_webpage(
|
||||||
|
self._LOGIN_URL, None, 'Downloading login page')
|
||||||
|
action_url = urljoin(self._LOGIN_URL, self._search_regex(
|
||||||
|
r'<form[^>]+action=(["\'])(?P<url>.+?)\1', login_page, 'post url',
|
||||||
|
default='https://www.linkedin.com/uas/login-submit', group='url'))
|
||||||
|
data = self._hidden_inputs(login_page)
|
||||||
|
data.update({
|
||||||
|
'session_key': email,
|
||||||
|
'session_password': password,
|
||||||
|
})
|
||||||
|
login_submit_page = self._download_webpage(
|
||||||
|
action_url, None, 'Logging in',
|
||||||
|
data=urlencode_postdata(data))
|
||||||
|
error = self._search_regex(
|
||||||
|
r'<span[^>]+class="error"[^>]*>\s*(.+?)\s*</span>',
|
||||||
|
login_submit_page, 'error', default=None)
|
||||||
|
if error:
|
||||||
|
raise ExtractorError(error, expected=True)
|
||||||
|
LinkedInBaseIE._logged_in = True
|
||||||
|
|
||||||
|
|
||||||
|
class LinkedInLearningBaseIE(LinkedInBaseIE):
|
||||||
|
_LOGIN_URL = 'https://www.linkedin.com/uas/login?trk=learning'
|
||||||
|
|
||||||
def _call_api(self, course_slug, fields, video_slug=None, resolution=None):
|
def _call_api(self, course_slug, fields, video_slug=None, resolution=None):
|
||||||
query = {
|
query = {
|
||||||
'courseSlug': course_slug,
|
'courseSlug': course_slug,
|
||||||
@@ -52,32 +87,47 @@ class LinkedInLearningBaseIE(InfoExtractor):
|
|||||||
def _get_video_id(self, video_data, course_slug, video_slug):
|
def _get_video_id(self, video_data, course_slug, video_slug):
|
||||||
return self._get_urn_id(video_data) or '%s/%s' % (course_slug, video_slug)
|
return self._get_urn_id(video_data) or '%s/%s' % (course_slug, video_slug)
|
||||||
|
|
||||||
def _real_initialize(self):
|
|
||||||
if self._logged_in:
|
|
||||||
return
|
|
||||||
email, password = self._get_login_info()
|
|
||||||
if email is None:
|
|
||||||
return
|
|
||||||
|
|
||||||
login_page = self._download_webpage(
|
class LinkedInIE(LinkedInBaseIE):
|
||||||
self._LOGIN_URL, None, 'Downloading login page')
|
_VALID_URL = r'https?://(?:www\.)?linkedin\.com/posts/.+?(?P<id>\d+)'
|
||||||
action_url = urljoin(self._LOGIN_URL, self._search_regex(
|
_TESTS = [{
|
||||||
r'<form[^>]+action=(["\'])(?P<url>.+?)\1', login_page, 'post url',
|
'url': 'https://www.linkedin.com/posts/mishalkhawaja_sendinblueviews-toronto-digitalmarketing-ugcPost-6850898786781339649-mM20',
|
||||||
default='https://www.linkedin.com/uas/login-submit', group='url'))
|
'info_dict': {
|
||||||
data = self._hidden_inputs(login_page)
|
'id': '6850898786781339649',
|
||||||
data.update({
|
'ext': 'mp4',
|
||||||
'session_key': email,
|
'title': 'Mishal K. on LinkedIn: #sendinblueviews #toronto #digitalmarketing',
|
||||||
'session_password': password,
|
'description': 'md5:be125430bab1c574f16aeb186a4d5b19',
|
||||||
})
|
'creator': 'Mishal K.'
|
||||||
login_submit_page = self._download_webpage(
|
},
|
||||||
action_url, None, 'Logging in',
|
}]
|
||||||
data=urlencode_postdata(data))
|
|
||||||
error = self._search_regex(
|
def _real_extract(self, url):
|
||||||
r'<span[^>]+class="error"[^>]*>\s*(.+?)\s*</span>',
|
video_id = self._match_id(url)
|
||||||
login_submit_page, 'error', default=None)
|
webpage = self._download_webpage(url, video_id)
|
||||||
if error:
|
|
||||||
raise ExtractorError(error, expected=True)
|
title = self._html_search_regex(r'<title>([^<]+)</title>', webpage, 'title')
|
||||||
LinkedInLearningBaseIE._logged_in = True
|
description = clean_html(get_element_by_class('share-update-card__update-text', webpage))
|
||||||
|
like_count = int_or_none(get_element_by_class('social-counts-reactions__social-counts-numRections', webpage))
|
||||||
|
creator = strip_or_none(clean_html(get_element_by_class('comment__actor-name', webpage)))
|
||||||
|
|
||||||
|
sources = self._parse_json(extract_attributes(self._search_regex(r'(<video[^>]+>)', webpage, 'video'))['data-sources'], video_id)
|
||||||
|
formats = [{
|
||||||
|
'url': source['src'],
|
||||||
|
'ext': mimetype2ext(source.get('type')),
|
||||||
|
'tbr': float_or_none(source.get('data-bitrate'), scale=1000),
|
||||||
|
} for source in sources]
|
||||||
|
|
||||||
|
self._sort_formats(formats)
|
||||||
|
|
||||||
|
return {
|
||||||
|
'id': video_id,
|
||||||
|
'formats': formats,
|
||||||
|
'title': title,
|
||||||
|
'like_count': like_count,
|
||||||
|
'creator': creator,
|
||||||
|
'thumbnail': self._og_search_thumbnail(webpage),
|
||||||
|
'description': description,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
class LinkedInLearningIE(LinkedInLearningBaseIE):
|
class LinkedInLearningIE(LinkedInLearningBaseIE):
|
||||||
@@ -108,7 +158,6 @@ class LinkedInLearningIE(LinkedInLearningBaseIE):
|
|||||||
def _real_extract(self, url):
|
def _real_extract(self, url):
|
||||||
course_slug, video_slug = self._match_valid_url(url).groups()
|
course_slug, video_slug = self._match_valid_url(url).groups()
|
||||||
|
|
||||||
video_data = None
|
|
||||||
formats = []
|
formats = []
|
||||||
for width, height in ((640, 360), (960, 540), (1280, 720)):
|
for width, height in ((640, 360), (960, 540), (1280, 720)):
|
||||||
video_data = self._call_api(
|
video_data = self._call_api(
|
||||||
|
|||||||
@@ -12,8 +12,8 @@ from ..compat import (
|
|||||||
|
|
||||||
|
|
||||||
class MediaKlikkIE(InfoExtractor):
|
class MediaKlikkIE(InfoExtractor):
|
||||||
_VALID_URL = r'''(?x)^https?:\/\/(?:www\.)?
|
_VALID_URL = r'''(?x)https?://(?:www\.)?
|
||||||
(?:mediaklikk|m4sport|hirado|petofilive)\.hu\/.*?videok?\/
|
(?:mediaklikk|m4sport|hirado|petofilive)\.hu/.*?(?:videok?|cikk)/
|
||||||
(?:(?P<year>[0-9]{4})/(?P<month>[0-9]{1,2})/(?P<day>[0-9]{1,2})/)?
|
(?:(?P<year>[0-9]{4})/(?P<month>[0-9]{1,2})/(?P<day>[0-9]{1,2})/)?
|
||||||
(?P<id>[^/#?_]+)'''
|
(?P<id>[^/#?_]+)'''
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,55 @@
|
|||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from .common import InfoExtractor
|
||||||
|
from ..utils import (
|
||||||
|
ExtractorError,
|
||||||
|
traverse_obj,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class MixchIE(InfoExtractor):
|
||||||
|
IE_NAME = 'mixch'
|
||||||
|
_VALID_URL = r'https?://(?:www\.)?mixch\.tv/u/(?P<id>\d+)'
|
||||||
|
|
||||||
|
TESTS = [{
|
||||||
|
'url': 'https://mixch.tv/u/16236849/live',
|
||||||
|
'skip': 'don\'t know if this live persists',
|
||||||
|
'info_dict': {
|
||||||
|
'id': '16236849',
|
||||||
|
'title': '24配信シェア⭕️投票🙏💦',
|
||||||
|
'comment_count': 13145,
|
||||||
|
'view_count': 28348,
|
||||||
|
'timestamp': 1636189377,
|
||||||
|
'uploader': '🦥伊咲👶🏻#フレアワ',
|
||||||
|
'uploader_id': '16236849',
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
'url': 'https://mixch.tv/u/16137876/live',
|
||||||
|
'only_matching': True,
|
||||||
|
}]
|
||||||
|
|
||||||
|
def _real_extract(self, url):
|
||||||
|
video_id = self._match_id(url)
|
||||||
|
webpage = self._download_webpage(f'https://mixch.tv/u/{video_id}/live', video_id)
|
||||||
|
|
||||||
|
initial_js_state = self._parse_json(self._search_regex(
|
||||||
|
r'(?m)^\s*window\.__INITIAL_JS_STATE__\s*=\s*(\{.+?\});\s*$', webpage, 'initial JS state'), video_id)
|
||||||
|
if not initial_js_state.get('liveInfo'):
|
||||||
|
raise ExtractorError('Livestream has ended.', expected=True)
|
||||||
|
|
||||||
|
return {
|
||||||
|
'id': video_id,
|
||||||
|
'title': traverse_obj(initial_js_state, ('liveInfo', 'title')),
|
||||||
|
'comment_count': traverse_obj(initial_js_state, ('liveInfo', 'comments')),
|
||||||
|
'view_count': traverse_obj(initial_js_state, ('liveInfo', 'visitor')),
|
||||||
|
'timestamp': traverse_obj(initial_js_state, ('liveInfo', 'created')),
|
||||||
|
'uploader': traverse_obj(initial_js_state, ('broadcasterInfo', 'name')),
|
||||||
|
'uploader_id': video_id,
|
||||||
|
'formats': [{
|
||||||
|
'format_id': 'hls',
|
||||||
|
'url': traverse_obj(initial_js_state, ('liveInfo', 'hls')) or 'https://d1hd0ww6piyb43.cloudfront.net/hls/torte_%s.m3u8' % video_id,
|
||||||
|
'ext': 'mp4',
|
||||||
|
'protocol': 'm3u8',
|
||||||
|
}],
|
||||||
|
'is_live': True,
|
||||||
|
}
|
||||||
@@ -6,7 +6,7 @@ from .common import InfoExtractor
|
|||||||
|
|
||||||
class MLSSoccerIE(InfoExtractor):
|
class MLSSoccerIE(InfoExtractor):
|
||||||
_VALID_DOMAINS = r'(?:(?:cfmontreal|intermiamicf|lagalaxy|lafc|houstondynamofc|dcunited|atlutd|mlssoccer|fcdallas|columbuscrew|coloradorapids|fccincinnati|chicagofirefc|austinfc|nashvillesc|whitecapsfc|sportingkc|soundersfc|sjearthquakes|rsl|timbers|philadelphiaunion|orlandocitysc|newyorkredbulls|nycfc)\.com|(?:torontofc)\.ca|(?:revolutionsoccer)\.net)'
|
_VALID_DOMAINS = r'(?:(?:cfmontreal|intermiamicf|lagalaxy|lafc|houstondynamofc|dcunited|atlutd|mlssoccer|fcdallas|columbuscrew|coloradorapids|fccincinnati|chicagofirefc|austinfc|nashvillesc|whitecapsfc|sportingkc|soundersfc|sjearthquakes|rsl|timbers|philadelphiaunion|orlandocitysc|newyorkredbulls|nycfc)\.com|(?:torontofc)\.ca|(?:revolutionsoccer)\.net)'
|
||||||
_VALID_URL = r'(?:https?://)(?:www\.)?%s/video/#?(?P<id>[^/&$#?]+)' % _VALID_DOMAINS
|
_VALID_URL = r'https?://(?:www\.)?%s/video/#?(?P<id>[^/&$#?]+)' % _VALID_DOMAINS
|
||||||
|
|
||||||
_TESTS = [{
|
_TESTS = [{
|
||||||
'url': 'https://www.mlssoccer.com/video/the-octagon-can-alphonso-davies-lead-canada-to-first-world-cup-since-1986#the-octagon-can-alphonso-davies-lead-canada-to-first-world-cup-since-1986',
|
'url': 'https://www.mlssoccer.com/video/the-octagon-can-alphonso-davies-lead-canada-to-first-world-cup-since-1986#the-octagon-can-alphonso-davies-lead-canada-to-first-world-cup-since-1986',
|
||||||
@@ -21,7 +21,6 @@ class MLSSoccerIE(InfoExtractor):
|
|||||||
'uploader_id': '5530036772001',
|
'uploader_id': '5530036772001',
|
||||||
'tags': ['club/canada'],
|
'tags': ['club/canada'],
|
||||||
'is_live': False,
|
'is_live': False,
|
||||||
'duration_string': '5:50',
|
|
||||||
'upload_date': '20211007',
|
'upload_date': '20211007',
|
||||||
'filesize_approx': 255193528.83200002
|
'filesize_approx': 255193528.83200002
|
||||||
},
|
},
|
||||||
|
|||||||
+11
-9
@@ -306,21 +306,23 @@ class MTVServicesInfoExtractor(InfoExtractor):
|
|||||||
if not mgid:
|
if not mgid:
|
||||||
mgid = self._extract_triforce_mgid(webpage)
|
mgid = self._extract_triforce_mgid(webpage)
|
||||||
|
|
||||||
if not mgid:
|
|
||||||
mgid = self._search_regex(
|
|
||||||
r'"videoConfig":{"videoId":"(mgid:.*?)"', webpage, 'mgid', default=None)
|
|
||||||
|
|
||||||
if not mgid:
|
|
||||||
mgid = self._search_regex(
|
|
||||||
r'"media":{"video":{"config":{"uri":"(mgid:.*?)"', webpage, 'mgid', default=None)
|
|
||||||
|
|
||||||
if not mgid:
|
if not mgid:
|
||||||
data = self._parse_json(self._search_regex(
|
data = self._parse_json(self._search_regex(
|
||||||
r'__DATA__\s*=\s*({.+?});', webpage, 'data'), None)
|
r'__DATA__\s*=\s*({.+?});', webpage, 'data'), None)
|
||||||
main_container = self._extract_child_with_type(data, 'MainContainer')
|
main_container = self._extract_child_with_type(data, 'MainContainer')
|
||||||
ab_testing = self._extract_child_with_type(main_container, 'ABTesting')
|
ab_testing = self._extract_child_with_type(main_container, 'ABTesting')
|
||||||
video_player = self._extract_child_with_type(ab_testing or main_container, 'VideoPlayer')
|
video_player = self._extract_child_with_type(ab_testing or main_container, 'VideoPlayer')
|
||||||
mgid = video_player['props']['media']['video']['config']['uri']
|
if video_player:
|
||||||
|
mgid = try_get(video_player, lambda x: x['props']['media']['video']['config']['uri'])
|
||||||
|
else:
|
||||||
|
flex_wrapper = self._extract_child_with_type(ab_testing or main_container, 'FlexWrapper')
|
||||||
|
auth_suite_wrapper = self._extract_child_with_type(flex_wrapper, 'AuthSuiteWrapper')
|
||||||
|
player = self._extract_child_with_type(auth_suite_wrapper or flex_wrapper, 'Player')
|
||||||
|
if player:
|
||||||
|
mgid = try_get(player, lambda x: x['props']['videoDetail']['mgid'])
|
||||||
|
|
||||||
|
if not mgid:
|
||||||
|
raise ExtractorError('Could not extract mgid')
|
||||||
|
|
||||||
return mgid
|
return mgid
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ from .common import InfoExtractor
|
|||||||
|
|
||||||
|
|
||||||
class MuseScoreIE(InfoExtractor):
|
class MuseScoreIE(InfoExtractor):
|
||||||
_VALID_URL = r'(?:https?://)(?:www\.)?musescore\.com/(?:user/\d+|[^/]+)(?:/scores)?/(?P<id>[^#&?]+)'
|
_VALID_URL = r'https?://(?:www\.)?musescore\.com/(?:user/\d+|[^/]+)(?:/scores)?/(?P<id>[^#&?]+)'
|
||||||
_TESTS = [{
|
_TESTS = [{
|
||||||
'url': 'https://musescore.com/user/73797/scores/142975',
|
'url': 'https://musescore.com/user/73797/scores/142975',
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
@@ -13,7 +13,7 @@ class MuseScoreIE(InfoExtractor):
|
|||||||
'ext': 'mp3',
|
'ext': 'mp3',
|
||||||
'title': 'WA Mozart Marche Turque (Turkish March fingered)',
|
'title': 'WA Mozart Marche Turque (Turkish March fingered)',
|
||||||
'description': 'md5:7ede08230e4eaabd67a4a98bb54d07be',
|
'description': 'md5:7ede08230e4eaabd67a4a98bb54d07be',
|
||||||
'thumbnail': r're:(?:https?://)(?:www\.)?musescore\.com/.*\.png[^$]+',
|
'thumbnail': r're:https?://(?:www\.)?musescore\.com/.*\.png[^$]+',
|
||||||
'uploader': 'PapyPiano',
|
'uploader': 'PapyPiano',
|
||||||
'creator': 'Wolfgang Amadeus Mozart',
|
'creator': 'Wolfgang Amadeus Mozart',
|
||||||
}
|
}
|
||||||
@@ -24,7 +24,7 @@ class MuseScoreIE(InfoExtractor):
|
|||||||
'ext': 'mp3',
|
'ext': 'mp3',
|
||||||
'title': 'Sweet Child O\' Mine – Guns N\' Roses sweet child',
|
'title': 'Sweet Child O\' Mine – Guns N\' Roses sweet child',
|
||||||
'description': 'md5:4dca71191c14abc312a0a4192492eace',
|
'description': 'md5:4dca71191c14abc312a0a4192492eace',
|
||||||
'thumbnail': r're:(?:https?://)(?:www\.)?musescore\.com/.*\.png[^$]+',
|
'thumbnail': r're:https?://(?:www\.)?musescore\.com/.*\.png[^$]+',
|
||||||
'uploader': 'roxbelviolin',
|
'uploader': 'roxbelviolin',
|
||||||
'creator': 'Guns N´Roses Arr. Roxbel Violin',
|
'creator': 'Guns N´Roses Arr. Roxbel Violin',
|
||||||
}
|
}
|
||||||
@@ -35,7 +35,7 @@ class MuseScoreIE(InfoExtractor):
|
|||||||
'ext': 'mp3',
|
'ext': 'mp3',
|
||||||
'title': 'Für Elise – Beethoven',
|
'title': 'Für Elise – Beethoven',
|
||||||
'description': 'md5:49515a3556d5ecaf9fa4b2514064ac34',
|
'description': 'md5:49515a3556d5ecaf9fa4b2514064ac34',
|
||||||
'thumbnail': r're:(?:https?://)(?:www\.)?musescore\.com/.*\.png[^$]+',
|
'thumbnail': r're:https?://(?:www\.)?musescore\.com/.*\.png[^$]+',
|
||||||
'uploader': 'ClassicMan',
|
'uploader': 'ClassicMan',
|
||||||
'creator': 'Ludwig van Beethoven (1770–1827)',
|
'creator': 'Ludwig van Beethoven (1770–1827)',
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -180,7 +180,7 @@ class MxplayerIE(InfoExtractor):
|
|||||||
|
|
||||||
|
|
||||||
class MxplayerShowIE(InfoExtractor):
|
class MxplayerShowIE(InfoExtractor):
|
||||||
_VALID_URL = r'(?:https?://)(?:www\.)?mxplayer\.in/show/(?P<display_id>[-\w]+)-(?P<id>\w+)/?(?:$|[#?])'
|
_VALID_URL = r'https?://(?:www\.)?mxplayer\.in/show/(?P<display_id>[-\w]+)-(?P<id>\w+)/?(?:$|[#?])'
|
||||||
_TESTS = [{
|
_TESTS = [{
|
||||||
'url': 'https://www.mxplayer.in/show/watch-chakravartin-ashoka-samrat-series-online-a8f44e3cc0814b5601d17772cedf5417',
|
'url': 'https://www.mxplayer.in/show/watch-chakravartin-ashoka-samrat-series-online-a8f44e3cc0814b5601d17772cedf5417',
|
||||||
'playlist_mincount': 440,
|
'playlist_mincount': 440,
|
||||||
|
|||||||
+213
-161
@@ -1,22 +1,163 @@
|
|||||||
# coding: utf-8
|
# coding: utf-8
|
||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
import itertools
|
||||||
import json
|
import json
|
||||||
import time
|
import time
|
||||||
|
import urllib
|
||||||
|
|
||||||
from urllib.error import HTTPError
|
|
||||||
from .common import InfoExtractor
|
|
||||||
from ..compat import compat_str, compat_urllib_parse_unquote, compat_urllib_parse_quote
|
|
||||||
from ..utils import (
|
from ..utils import (
|
||||||
ExtractorError,
|
ExtractorError,
|
||||||
parse_iso8601,
|
parse_iso8601,
|
||||||
try_get,
|
try_get,
|
||||||
urljoin,
|
|
||||||
)
|
)
|
||||||
|
from .common import InfoExtractor
|
||||||
|
|
||||||
|
|
||||||
class NebulaIE(InfoExtractor):
|
class NebulaBaseIE(InfoExtractor):
|
||||||
|
_NETRC_MACHINE = 'watchnebula'
|
||||||
|
|
||||||
|
_nebula_api_token = None
|
||||||
|
_nebula_bearer_token = None
|
||||||
|
_zype_access_token = None
|
||||||
|
|
||||||
|
def _perform_nebula_auth(self):
|
||||||
|
username, password = self._get_login_info()
|
||||||
|
if not (username and password):
|
||||||
|
self.raise_login_required()
|
||||||
|
|
||||||
|
data = json.dumps({'email': username, 'password': password}).encode('utf8')
|
||||||
|
response = self._download_json(
|
||||||
|
'https://api.watchnebula.com/api/v1/auth/login/',
|
||||||
|
data=data, fatal=False, video_id=None,
|
||||||
|
headers={
|
||||||
|
'content-type': 'application/json',
|
||||||
|
# Submitting the 'sessionid' cookie always causes a 403 on auth endpoint
|
||||||
|
'cookie': ''
|
||||||
|
},
|
||||||
|
note='Logging in to Nebula with supplied credentials',
|
||||||
|
errnote='Authentication failed or rejected')
|
||||||
|
if not response or not response.get('key'):
|
||||||
|
self.raise_login_required()
|
||||||
|
|
||||||
|
# save nebula token as cookie
|
||||||
|
self._set_cookie(
|
||||||
|
'nebula.app', 'nebula-auth',
|
||||||
|
urllib.parse.quote(
|
||||||
|
json.dumps({
|
||||||
|
"apiToken": response["key"],
|
||||||
|
"isLoggingIn": False,
|
||||||
|
"isLoggingOut": False,
|
||||||
|
}, separators=(",", ":"))),
|
||||||
|
expire_time=int(time.time()) + 86400 * 365,
|
||||||
|
)
|
||||||
|
|
||||||
|
return response['key']
|
||||||
|
|
||||||
|
def _retrieve_nebula_api_token(self):
|
||||||
|
"""
|
||||||
|
Check cookie jar for valid token. Try to authenticate using credentials if no valid token
|
||||||
|
can be found in the cookie jar.
|
||||||
|
"""
|
||||||
|
nebula_cookies = self._get_cookies('https://nebula.app')
|
||||||
|
nebula_cookie = nebula_cookies.get('nebula-auth')
|
||||||
|
if nebula_cookie:
|
||||||
|
self.to_screen('Authenticating to Nebula with token from cookie jar')
|
||||||
|
nebula_cookie_value = urllib.parse.unquote(nebula_cookie.value)
|
||||||
|
nebula_api_token = self._parse_json(nebula_cookie_value, None).get('apiToken')
|
||||||
|
if nebula_api_token:
|
||||||
|
return nebula_api_token
|
||||||
|
|
||||||
|
return self._perform_nebula_auth()
|
||||||
|
|
||||||
|
def _call_nebula_api(self, url, video_id=None, method='GET', auth_type='api', note=''):
|
||||||
|
assert method in ('GET', 'POST',)
|
||||||
|
assert auth_type in ('api', 'bearer',)
|
||||||
|
|
||||||
|
def inner_call():
|
||||||
|
authorization = f'Token {self._nebula_api_token}' if auth_type == 'api' else f'Bearer {self._nebula_bearer_token}'
|
||||||
|
return self._download_json(
|
||||||
|
url, video_id, note=note, headers={'Authorization': authorization},
|
||||||
|
data=b'' if method == 'POST' else None)
|
||||||
|
|
||||||
|
try:
|
||||||
|
return inner_call()
|
||||||
|
except ExtractorError as exc:
|
||||||
|
# if 401 or 403, attempt credential re-auth and retry
|
||||||
|
if exc.cause and isinstance(exc.cause, urllib.error.HTTPError) and exc.cause.code in (401, 403):
|
||||||
|
self.to_screen(f'Reauthenticating to Nebula and retrying, because last {auth_type} call resulted in error {exc.cause.code}')
|
||||||
|
self._login()
|
||||||
|
return inner_call()
|
||||||
|
else:
|
||||||
|
raise
|
||||||
|
|
||||||
|
def _fetch_nebula_bearer_token(self):
|
||||||
|
"""
|
||||||
|
Get a Bearer token for the Nebula API. This will be required to fetch video meta data.
|
||||||
|
"""
|
||||||
|
response = self._call_nebula_api('https://api.watchnebula.com/api/v1/authorization/',
|
||||||
|
method='POST',
|
||||||
|
note='Authorizing to Nebula')
|
||||||
|
return response['token']
|
||||||
|
|
||||||
|
def _fetch_zype_access_token(self):
|
||||||
|
"""
|
||||||
|
Get a Zype access token, which is required to access video streams -- in our case: to
|
||||||
|
generate video URLs.
|
||||||
|
"""
|
||||||
|
user_object = self._call_nebula_api('https://api.watchnebula.com/api/v1/auth/user/', note='Retrieving Zype access token')
|
||||||
|
|
||||||
|
access_token = try_get(user_object, lambda x: x['zype_auth_info']['access_token'], str)
|
||||||
|
if not access_token:
|
||||||
|
if try_get(user_object, lambda x: x['is_subscribed'], bool):
|
||||||
|
# TODO: Reimplement the same Zype token polling the Nebula frontend implements
|
||||||
|
# see https://github.com/ytdl-org/youtube-dl/pull/24805#issuecomment-749231532
|
||||||
|
raise ExtractorError(
|
||||||
|
'Unable to extract Zype access token from Nebula API authentication endpoint. '
|
||||||
|
'Open an arbitrary video in a browser with this account to generate a token',
|
||||||
|
expected=True)
|
||||||
|
raise ExtractorError('Unable to extract Zype access token from Nebula API authentication endpoint')
|
||||||
|
return access_token
|
||||||
|
|
||||||
|
def _build_video_info(self, episode):
|
||||||
|
zype_id = episode['zype_id']
|
||||||
|
zype_video_url = f'https://player.zype.com/embed/{zype_id}.html?access_token={self._zype_access_token}'
|
||||||
|
channel_slug = episode['channel_slug']
|
||||||
|
return {
|
||||||
|
'id': episode['zype_id'],
|
||||||
|
'display_id': episode['slug'],
|
||||||
|
'_type': 'url_transparent',
|
||||||
|
'ie_key': 'Zype',
|
||||||
|
'url': zype_video_url,
|
||||||
|
'title': episode['title'],
|
||||||
|
'description': episode['description'],
|
||||||
|
'timestamp': parse_iso8601(episode['published_at']),
|
||||||
|
'thumbnails': [{
|
||||||
|
# 'id': tn.get('name'), # this appears to be null
|
||||||
|
'url': tn['original'],
|
||||||
|
'height': key,
|
||||||
|
} for key, tn in episode['assets']['thumbnail'].items()],
|
||||||
|
'duration': episode['duration'],
|
||||||
|
'channel': episode['channel_title'],
|
||||||
|
'channel_id': channel_slug,
|
||||||
|
'channel_url': f'https://nebula.app/{channel_slug}',
|
||||||
|
'uploader': episode['channel_title'],
|
||||||
|
'uploader_id': channel_slug,
|
||||||
|
'uploader_url': f'https://nebula.app/{channel_slug}',
|
||||||
|
'series': episode['channel_title'],
|
||||||
|
'creator': episode['channel_title'],
|
||||||
|
}
|
||||||
|
|
||||||
|
def _login(self):
|
||||||
|
self._nebula_api_token = self._retrieve_nebula_api_token()
|
||||||
|
self._nebula_bearer_token = self._fetch_nebula_bearer_token()
|
||||||
|
self._zype_access_token = self._fetch_zype_access_token()
|
||||||
|
|
||||||
|
def _real_initialize(self):
|
||||||
|
self._login()
|
||||||
|
|
||||||
|
|
||||||
|
class NebulaIE(NebulaBaseIE):
|
||||||
_VALID_URL = r'https?://(?:www\.)?(?:watchnebula\.com|nebula\.app)/videos/(?P<id>[-\w]+)'
|
_VALID_URL = r'https?://(?:www\.)?(?:watchnebula\.com|nebula\.app)/videos/(?P<id>[-\w]+)'
|
||||||
_TESTS = [
|
_TESTS = [
|
||||||
{
|
{
|
||||||
@@ -30,12 +171,13 @@ class NebulaIE(InfoExtractor):
|
|||||||
'upload_date': '20180731',
|
'upload_date': '20180731',
|
||||||
'timestamp': 1533009600,
|
'timestamp': 1533009600,
|
||||||
'channel': 'Lindsay Ellis',
|
'channel': 'Lindsay Ellis',
|
||||||
|
'channel_id': 'lindsayellis',
|
||||||
'uploader': 'Lindsay Ellis',
|
'uploader': 'Lindsay Ellis',
|
||||||
|
'uploader_id': 'lindsayellis',
|
||||||
},
|
},
|
||||||
'params': {
|
'params': {
|
||||||
'usenetrc': True,
|
'usenetrc': True,
|
||||||
},
|
},
|
||||||
'skip': 'All Nebula content requires authentication',
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'url': 'https://nebula.app/videos/the-logistics-of-d-day-landing-craft-how-the-allies-got-ashore',
|
'url': 'https://nebula.app/videos/the-logistics-of-d-day-landing-craft-how-the-allies-got-ashore',
|
||||||
@@ -47,13 +189,14 @@ class NebulaIE(InfoExtractor):
|
|||||||
'description': r're:^In this episode we explore the unsung heroes of D-Day, the landing craft.',
|
'description': r're:^In this episode we explore the unsung heroes of D-Day, the landing craft.',
|
||||||
'upload_date': '20200327',
|
'upload_date': '20200327',
|
||||||
'timestamp': 1585348140,
|
'timestamp': 1585348140,
|
||||||
'channel': 'The Logistics of D-Day',
|
'channel': 'Real Engineering',
|
||||||
'uploader': 'The Logistics of D-Day',
|
'channel_id': 'realengineering',
|
||||||
|
'uploader': 'Real Engineering',
|
||||||
|
'uploader_id': 'realengineering',
|
||||||
},
|
},
|
||||||
'params': {
|
'params': {
|
||||||
'usenetrc': True,
|
'usenetrc': True,
|
||||||
},
|
},
|
||||||
'skip': 'All Nebula content requires authentication',
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'url': 'https://nebula.app/videos/money-episode-1-the-draw',
|
'url': 'https://nebula.app/videos/money-episode-1-the-draw',
|
||||||
@@ -66,173 +209,82 @@ class NebulaIE(InfoExtractor):
|
|||||||
'upload_date': '20200323',
|
'upload_date': '20200323',
|
||||||
'timestamp': 1584980400,
|
'timestamp': 1584980400,
|
||||||
'channel': 'Tom Scott Presents: Money',
|
'channel': 'Tom Scott Presents: Money',
|
||||||
|
'channel_id': 'tom-scott-presents-money',
|
||||||
'uploader': 'Tom Scott Presents: Money',
|
'uploader': 'Tom Scott Presents: Money',
|
||||||
|
'uploader_id': 'tom-scott-presents-money',
|
||||||
},
|
},
|
||||||
'params': {
|
'params': {
|
||||||
'usenetrc': True,
|
'usenetrc': True,
|
||||||
},
|
},
|
||||||
'skip': 'All Nebula content requires authentication',
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'url': 'https://watchnebula.com/videos/money-episode-1-the-draw',
|
'url': 'https://watchnebula.com/videos/money-episode-1-the-draw',
|
||||||
'only_matching': True,
|
'only_matching': True,
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
_NETRC_MACHINE = 'watchnebula'
|
|
||||||
|
|
||||||
_nebula_token = None
|
def _fetch_video_metadata(self, slug):
|
||||||
|
return self._call_nebula_api(f'https://content.watchnebula.com/video/{slug}/',
|
||||||
def _retrieve_nebula_auth(self):
|
video_id=slug,
|
||||||
"""
|
auth_type='bearer',
|
||||||
Log in to Nebula, and returns a Nebula API token
|
note='Fetching video meta data')
|
||||||
"""
|
|
||||||
|
|
||||||
username, password = self._get_login_info()
|
|
||||||
if not (username and password):
|
|
||||||
self.raise_login_required()
|
|
||||||
|
|
||||||
self.report_login()
|
|
||||||
data = json.dumps({'email': username, 'password': password}).encode('utf8')
|
|
||||||
response = self._download_json(
|
|
||||||
'https://api.watchnebula.com/api/v1/auth/login/',
|
|
||||||
data=data, fatal=False, video_id=None,
|
|
||||||
headers={
|
|
||||||
'content-type': 'application/json',
|
|
||||||
# Submitting the 'sessionid' cookie always causes a 403 on auth endpoint
|
|
||||||
'cookie': ''
|
|
||||||
},
|
|
||||||
note='Authenticating to Nebula with supplied credentials',
|
|
||||||
errnote='Authentication failed or rejected')
|
|
||||||
if not response or not response.get('key'):
|
|
||||||
self.raise_login_required()
|
|
||||||
|
|
||||||
# save nebula token as cookie
|
|
||||||
self._set_cookie(
|
|
||||||
'nebula.app', 'nebula-auth',
|
|
||||||
compat_urllib_parse_quote(
|
|
||||||
json.dumps({
|
|
||||||
"apiToken": response["key"],
|
|
||||||
"isLoggingIn": False,
|
|
||||||
"isLoggingOut": False,
|
|
||||||
}, separators=(",", ":"))),
|
|
||||||
expire_time=int(time.time()) + 86400 * 365,
|
|
||||||
)
|
|
||||||
|
|
||||||
return response['key']
|
|
||||||
|
|
||||||
def _retrieve_zype_api_key(self, page_url, display_id):
|
|
||||||
"""
|
|
||||||
Retrieves the Zype API key
|
|
||||||
"""
|
|
||||||
|
|
||||||
# Find the js that has the API key from the webpage and download it
|
|
||||||
webpage = self._download_webpage(page_url, video_id=display_id)
|
|
||||||
main_script_relpath = self._search_regex(
|
|
||||||
r'<script[^>]*src="(?P<script_relpath>[^"]*main.[0-9a-f]*.chunk.js)"[^>]*>', webpage,
|
|
||||||
group='script_relpath', name='script relative path', fatal=True)
|
|
||||||
main_script_abspath = urljoin(page_url, main_script_relpath)
|
|
||||||
main_script = self._download_webpage(main_script_abspath, video_id=display_id,
|
|
||||||
note='Retrieving Zype API key')
|
|
||||||
|
|
||||||
api_key = self._search_regex(
|
|
||||||
r'REACT_APP_ZYPE_API_KEY\s*:\s*"(?P<api_key>[\w-]*)"', main_script,
|
|
||||||
group='api_key', name='API key', fatal=True)
|
|
||||||
|
|
||||||
return api_key
|
|
||||||
|
|
||||||
def _call_zype_api(self, path, params, video_id, api_key, note):
|
|
||||||
"""
|
|
||||||
A helper for making calls to the Zype API.
|
|
||||||
"""
|
|
||||||
query = {'api_key': api_key, 'per_page': 1}
|
|
||||||
query.update(params)
|
|
||||||
return self._download_json('https://api.zype.com' + path, video_id, query=query, note=note)
|
|
||||||
|
|
||||||
def _call_nebula_api(self, path, video_id, access_token, note):
|
|
||||||
"""
|
|
||||||
A helper for making calls to the Nebula API.
|
|
||||||
"""
|
|
||||||
return self._download_json('https://api.watchnebula.com/api/v1' + path, video_id, headers={
|
|
||||||
'Authorization': 'Token {access_token}'.format(access_token=access_token)
|
|
||||||
}, note=note)
|
|
||||||
|
|
||||||
def _fetch_zype_access_token(self, video_id):
|
|
||||||
try:
|
|
||||||
user_object = self._call_nebula_api('/auth/user/', video_id, self._nebula_token, note='Retrieving Zype access token')
|
|
||||||
except ExtractorError as exc:
|
|
||||||
# if 401, attempt credential auth and retry
|
|
||||||
if exc.cause and isinstance(exc.cause, HTTPError) and exc.cause.code == 401:
|
|
||||||
self._nebula_token = self._retrieve_nebula_auth()
|
|
||||||
user_object = self._call_nebula_api('/auth/user/', video_id, self._nebula_token, note='Retrieving Zype access token')
|
|
||||||
else:
|
|
||||||
raise
|
|
||||||
|
|
||||||
access_token = try_get(user_object, lambda x: x['zype_auth_info']['access_token'], compat_str)
|
|
||||||
if not access_token:
|
|
||||||
if try_get(user_object, lambda x: x['is_subscribed'], bool):
|
|
||||||
# TODO: Reimplement the same Zype token polling the Nebula frontend implements
|
|
||||||
# see https://github.com/ytdl-org/youtube-dl/pull/24805#issuecomment-749231532
|
|
||||||
raise ExtractorError(
|
|
||||||
'Unable to extract Zype access token from Nebula API authentication endpoint. '
|
|
||||||
'Open an arbitrary video in a browser with this account to generate a token',
|
|
||||||
expected=True)
|
|
||||||
raise ExtractorError('Unable to extract Zype access token from Nebula API authentication endpoint')
|
|
||||||
return access_token
|
|
||||||
|
|
||||||
def _extract_channel_title(self, video_meta):
|
|
||||||
# TODO: Implement the API calls giving us the channel list,
|
|
||||||
# so that we can do the title lookup and then figure out the channel URL
|
|
||||||
categories = video_meta.get('categories', []) if video_meta else []
|
|
||||||
# the channel name is the value of the first category
|
|
||||||
for category in categories:
|
|
||||||
if category.get('value'):
|
|
||||||
return category['value'][0]
|
|
||||||
|
|
||||||
def _real_initialize(self):
|
|
||||||
# check cookie jar for valid token
|
|
||||||
nebula_cookies = self._get_cookies('https://nebula.app')
|
|
||||||
nebula_cookie = nebula_cookies.get('nebula-auth')
|
|
||||||
if nebula_cookie:
|
|
||||||
self.to_screen('Authenticating to Nebula with token from cookie jar')
|
|
||||||
nebula_cookie_value = compat_urllib_parse_unquote(nebula_cookie.value)
|
|
||||||
self._nebula_token = self._parse_json(nebula_cookie_value, None).get('apiToken')
|
|
||||||
|
|
||||||
# try to authenticate using credentials if no valid token has been found
|
|
||||||
if not self._nebula_token:
|
|
||||||
self._nebula_token = self._retrieve_nebula_auth()
|
|
||||||
|
|
||||||
def _real_extract(self, url):
|
def _real_extract(self, url):
|
||||||
display_id = self._match_id(url)
|
slug = self._match_id(url)
|
||||||
api_key = self._retrieve_zype_api_key(url, display_id)
|
video = self._fetch_video_metadata(slug)
|
||||||
|
return self._build_video_info(video)
|
||||||
|
|
||||||
response = self._call_zype_api('/videos', {'friendly_title': display_id},
|
|
||||||
display_id, api_key, note='Retrieving metadata from Zype')
|
|
||||||
if len(response.get('response') or []) != 1:
|
|
||||||
raise ExtractorError('Unable to find video on Zype API')
|
|
||||||
video_meta = response['response'][0]
|
|
||||||
|
|
||||||
video_id = video_meta['_id']
|
class NebulaCollectionIE(NebulaBaseIE):
|
||||||
zype_access_token = self._fetch_zype_access_token(display_id)
|
IE_NAME = 'nebula:collection'
|
||||||
|
_VALID_URL = r'https?://(?:www\.)?(?:watchnebula\.com|nebula\.app)/(?!videos/)(?P<id>[-\w]+)'
|
||||||
|
_TESTS = [
|
||||||
|
{
|
||||||
|
'url': 'https://nebula.app/tom-scott-presents-money',
|
||||||
|
'info_dict': {
|
||||||
|
'id': 'tom-scott-presents-money',
|
||||||
|
'title': 'Tom Scott Presents: Money',
|
||||||
|
'description': 'Tom Scott hosts a series all about trust, negotiation and money.',
|
||||||
|
},
|
||||||
|
'playlist_count': 5,
|
||||||
|
'params': {
|
||||||
|
'usenetrc': True,
|
||||||
|
},
|
||||||
|
}, {
|
||||||
|
'url': 'https://nebula.app/lindsayellis',
|
||||||
|
'info_dict': {
|
||||||
|
'id': 'lindsayellis',
|
||||||
|
'title': 'Lindsay Ellis',
|
||||||
|
'description': 'Enjoy these hottest of takes on Disney, Transformers, and Musicals.',
|
||||||
|
},
|
||||||
|
'playlist_mincount': 100,
|
||||||
|
'params': {
|
||||||
|
'usenetrc': True,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
channel_title = self._extract_channel_title(video_meta)
|
def _generate_playlist_entries(self, collection_id, channel):
|
||||||
|
episodes = channel['episodes']['results']
|
||||||
|
for page_num in itertools.count(2):
|
||||||
|
for episode in episodes:
|
||||||
|
yield self._build_video_info(episode)
|
||||||
|
next_url = channel['episodes']['next']
|
||||||
|
if not next_url:
|
||||||
|
break
|
||||||
|
channel = self._call_nebula_api(next_url, collection_id, auth_type='bearer',
|
||||||
|
note=f'Retrieving channel page {page_num}')
|
||||||
|
episodes = channel['episodes']['results']
|
||||||
|
|
||||||
return {
|
def _real_extract(self, url):
|
||||||
'id': video_id,
|
collection_id = self._match_id(url)
|
||||||
'display_id': display_id,
|
channel_url = f'https://content.watchnebula.com/video/channels/{collection_id}/'
|
||||||
'_type': 'url_transparent',
|
channel = self._call_nebula_api(channel_url, collection_id, auth_type='bearer', note='Retrieving channel')
|
||||||
'ie_key': 'Zype',
|
channel_details = channel['details']
|
||||||
'url': 'https://player.zype.com/embed/%s.html?access_token=%s' % (video_id, zype_access_token),
|
|
||||||
'title': video_meta.get('title'),
|
return self.playlist_result(
|
||||||
'description': video_meta.get('description'),
|
entries=self._generate_playlist_entries(collection_id, channel),
|
||||||
'timestamp': parse_iso8601(video_meta.get('published_at')),
|
playlist_id=collection_id,
|
||||||
'thumbnails': [{
|
playlist_title=channel_details['title'],
|
||||||
'id': tn.get('name'), # this appears to be null
|
playlist_description=channel_details['description']
|
||||||
'url': tn['url'],
|
)
|
||||||
'width': tn.get('width'),
|
|
||||||
'height': tn.get('height'),
|
|
||||||
} for tn in video_meta.get('thumbnails', [])],
|
|
||||||
'duration': video_meta.get('duration'),
|
|
||||||
'channel': channel_title,
|
|
||||||
'uploader': channel_title, # we chose uploader = channel name
|
|
||||||
# TODO: uploader_url, channel_id, channel_url
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -385,8 +385,7 @@ class NexxIE(InfoExtractor):
|
|||||||
elif cdn == 'free':
|
elif cdn == 'free':
|
||||||
formats = self._extract_free_formats(video, video_id)
|
formats = self._extract_free_formats(video, video_id)
|
||||||
else:
|
else:
|
||||||
# TODO: reverse more cdns
|
self.raise_no_formats(f'{cdn} formats are currently not supported', video_id)
|
||||||
assert False
|
|
||||||
|
|
||||||
self._sort_formats(formats)
|
self._sort_formats(formats)
|
||||||
|
|
||||||
|
|||||||
@@ -703,7 +703,7 @@ class NicovideoSearchURLIE(InfoExtractor):
|
|||||||
|
|
||||||
|
|
||||||
class NicovideoSearchIE(SearchInfoExtractor, NicovideoSearchURLIE):
|
class NicovideoSearchIE(SearchInfoExtractor, NicovideoSearchURLIE):
|
||||||
IE_DESC = 'Nico video searches'
|
IE_DESC = 'Nico video search'
|
||||||
IE_NAME = NicovideoSearchIE_NAME
|
IE_NAME = NicovideoSearchIE_NAME
|
||||||
_SEARCH_KEY = 'nicosearch'
|
_SEARCH_KEY = 'nicosearch'
|
||||||
_TESTS = []
|
_TESTS = []
|
||||||
@@ -714,7 +714,7 @@ class NicovideoSearchIE(SearchInfoExtractor, NicovideoSearchURLIE):
|
|||||||
|
|
||||||
|
|
||||||
class NicovideoSearchDateIE(NicovideoSearchIE):
|
class NicovideoSearchDateIE(NicovideoSearchIE):
|
||||||
IE_DESC = 'Nico video searches, newest first'
|
IE_DESC = 'Nico video search, newest first'
|
||||||
IE_NAME = f'{NicovideoSearchIE_NAME}:date'
|
IE_NAME = f'{NicovideoSearchIE_NAME}:date'
|
||||||
_SEARCH_KEY = 'nicosearchdate'
|
_SEARCH_KEY = 'nicosearchdate'
|
||||||
_TESTS = [{
|
_TESTS = [{
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
# coding: utf-8
|
# coding: utf-8
|
||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
|
||||||
from .common import InfoExtractor
|
from .common import InfoExtractor
|
||||||
from ..utils import (
|
from ..utils import (
|
||||||
float_or_none,
|
float_or_none,
|
||||||
@@ -99,3 +98,37 @@ class NineCNineMediaIE(InfoExtractor):
|
|||||||
}
|
}
|
||||||
|
|
||||||
return info
|
return info
|
||||||
|
|
||||||
|
|
||||||
|
class CPTwentyFourIE(InfoExtractor):
|
||||||
|
IE_NAME = 'cp24'
|
||||||
|
_GEO_COUNTRIES = ['CA']
|
||||||
|
_VALID_URL = r'https?://(?:www\.)?cp24\.com/news/(?P<id>[^?#]+)'
|
||||||
|
|
||||||
|
_TESTS = [{
|
||||||
|
'url': 'https://www.cp24.com/news/video-shows-atm-being-ripped-out-of-business-by-pickup-truck-driver-in-mississauga-1.5676877',
|
||||||
|
'info_dict': {
|
||||||
|
'id': '2328005',
|
||||||
|
'ext': 'mp4',
|
||||||
|
'title': 'WATCH: Truck rips ATM from Mississauga business',
|
||||||
|
'description': 'md5:cf7498480885f080a754389a2b2f7073',
|
||||||
|
'timestamp': 1637618377,
|
||||||
|
'episode_number': None,
|
||||||
|
'season': 'Season 0',
|
||||||
|
'season_number': 0,
|
||||||
|
'season_id': 57974,
|
||||||
|
'series': 'CTV News Toronto',
|
||||||
|
'duration': 26.86,
|
||||||
|
'thumbnail': 'http://images2.9c9media.com/image_asset/2014_11_5_2eb609a0-475b-0132-fbd6-34b52f6f1279_jpg_2000x1125.jpg',
|
||||||
|
'upload_date': '20211122',
|
||||||
|
},
|
||||||
|
'params': {'skip_download': True, 'format': 'bv'}
|
||||||
|
}]
|
||||||
|
|
||||||
|
def _real_extract(self, url):
|
||||||
|
display_id = self._match_id(url)
|
||||||
|
webpage = self._download_webpage(url, display_id)
|
||||||
|
id, destination = self._search_regex(
|
||||||
|
r'getAuthStates\("(?P<id>[^"]+)",\s?"(?P<destination>[^"]+)"\);',
|
||||||
|
webpage, 'video id and destination', group=('id', 'destination'))
|
||||||
|
return self.url_result(f'9c9media:{destination}:{id}', ie=NineCNineMediaIE.ie_key(), video_id=id)
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ from ..utils import (
|
|||||||
int_or_none,
|
int_or_none,
|
||||||
js_to_json,
|
js_to_json,
|
||||||
qualities,
|
qualities,
|
||||||
|
traverse_obj,
|
||||||
unified_strdate,
|
unified_strdate,
|
||||||
url_or_none,
|
url_or_none,
|
||||||
)
|
)
|
||||||
@@ -17,30 +18,45 @@ from ..utils import (
|
|||||||
|
|
||||||
class NovaEmbedIE(InfoExtractor):
|
class NovaEmbedIE(InfoExtractor):
|
||||||
_VALID_URL = r'https?://media\.cms\.nova\.cz/embed/(?P<id>[^/?#&]+)'
|
_VALID_URL = r'https?://media\.cms\.nova\.cz/embed/(?P<id>[^/?#&]+)'
|
||||||
_TEST = {
|
_TESTS = [{
|
||||||
'url': 'https://media.cms.nova.cz/embed/8o0n0r?autoplay=1',
|
'url': 'https://media.cms.nova.cz/embed/8o0n0r?autoplay=1',
|
||||||
'md5': 'ee009bafcc794541570edd44b71cbea3',
|
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
'id': '8o0n0r',
|
'id': '8o0n0r',
|
||||||
'ext': 'mp4',
|
|
||||||
'title': '2180. díl',
|
'title': '2180. díl',
|
||||||
'thumbnail': r're:^https?://.*\.jpg',
|
'thumbnail': r're:^https?://.*\.jpg',
|
||||||
'duration': 2578,
|
'duration': 2578,
|
||||||
},
|
},
|
||||||
}
|
'params': {
|
||||||
|
'skip_download': True,
|
||||||
|
'ignore_no_formats_error': True,
|
||||||
|
},
|
||||||
|
'expected_warnings': ['DRM protected', 'Requested format is not available'],
|
||||||
|
}, {
|
||||||
|
'url': 'https://media.cms.nova.cz/embed/KybpWYvcgOa',
|
||||||
|
'info_dict': {
|
||||||
|
'id': 'KybpWYvcgOa',
|
||||||
|
'ext': 'mp4',
|
||||||
|
'title': 'Borhyová oslavila 60? Soutěžící z pořadu odboural moderátora Ondřeje Sokola',
|
||||||
|
'thumbnail': r're:^https?://.*\.jpg',
|
||||||
|
'duration': 114,
|
||||||
|
},
|
||||||
|
'params': {'skip_download': 'm3u8'},
|
||||||
|
}]
|
||||||
|
|
||||||
def _real_extract(self, url):
|
def _real_extract(self, url):
|
||||||
video_id = self._match_id(url)
|
video_id = self._match_id(url)
|
||||||
|
|
||||||
webpage = self._download_webpage(url, video_id)
|
webpage = self._download_webpage(url, video_id)
|
||||||
|
|
||||||
|
has_drm = False
|
||||||
duration = None
|
duration = None
|
||||||
formats = []
|
formats = []
|
||||||
|
|
||||||
player = self._parse_json(
|
player = self._parse_json(
|
||||||
self._search_regex(
|
self._search_regex(
|
||||||
r'Player\.init\s*\([^,]+,\s*(?:\w+\s*\?\s*{.+?}\s*:\s*)?({.+})\s*,\s*{.+?}\s*\)\s*;',
|
(r'(?:replacePlaceholders.*?:\s*)?replacePlaceholders\s*\(\s*(?P<json>{.*})\s*\)(?:\s*\))?\s*,',
|
||||||
webpage, 'player', default='{}'), video_id, fatal=False)
|
r'Player\.init\s*\([^,]+,(?P<cndn>\s*\w+\s*\?)?\s*(?P<json>{(?(cndn).+?|.+)})\s*(?(cndn):|,\s*{.+?}\s*\)\s*;)'),
|
||||||
|
webpage, 'player', default='{}', group='json'), video_id, fatal=False)
|
||||||
if player:
|
if player:
|
||||||
for format_id, format_list in player['tracks'].items():
|
for format_id, format_list in player['tracks'].items():
|
||||||
if not isinstance(format_list, list):
|
if not isinstance(format_list, list):
|
||||||
@@ -48,6 +64,10 @@ class NovaEmbedIE(InfoExtractor):
|
|||||||
for format_dict in format_list:
|
for format_dict in format_list:
|
||||||
if not isinstance(format_dict, dict):
|
if not isinstance(format_dict, dict):
|
||||||
continue
|
continue
|
||||||
|
if (not self.get_param('allow_unplayable_formats')
|
||||||
|
and traverse_obj(format_dict, ('drm', 'keySystem'))):
|
||||||
|
has_drm = True
|
||||||
|
continue
|
||||||
format_url = url_or_none(format_dict.get('src'))
|
format_url = url_or_none(format_dict.get('src'))
|
||||||
format_type = format_dict.get('type')
|
format_type = format_dict.get('type')
|
||||||
ext = determine_ext(format_url)
|
ext = determine_ext(format_url)
|
||||||
@@ -104,6 +124,8 @@ class NovaEmbedIE(InfoExtractor):
|
|||||||
f['format_id'] = f_id
|
f['format_id'] = f_id
|
||||||
formats.append(f)
|
formats.append(f)
|
||||||
|
|
||||||
|
if not formats and has_drm:
|
||||||
|
self.report_drm(video_id)
|
||||||
self._sort_formats(formats)
|
self._sort_formats(formats)
|
||||||
|
|
||||||
title = self._og_search_title(
|
title = self._og_search_title(
|
||||||
|
|||||||
@@ -0,0 +1,51 @@
|
|||||||
|
# coding: utf-8
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from .common import InfoExtractor
|
||||||
|
|
||||||
|
|
||||||
|
class OneFootballIE(InfoExtractor):
|
||||||
|
_VALID_URL = r'https?://(?:www\.)?onefootball\.com/[a-z]{2}/video/[^/&?#]+-(?P<id>\d+)'
|
||||||
|
|
||||||
|
_TESTS = [{
|
||||||
|
'url': 'https://onefootball.com/en/video/highlights-fc-zuerich-3-3-fc-basel-34012334',
|
||||||
|
'info_dict': {
|
||||||
|
'id': '34012334',
|
||||||
|
'ext': 'mp4',
|
||||||
|
'title': 'Highlights: FC Zürich 3-3 FC Basel',
|
||||||
|
'description': 'md5:33d9855cb790702c4fe42a513700aba8',
|
||||||
|
'thumbnail': 'https://photobooth-api.onefootball.com/api/screenshot/https:%2F%2Fperegrine-api.onefootball.com%2Fv2%2Fphotobooth%2Fcms%2Fen%2F34012334',
|
||||||
|
'timestamp': 1635874604,
|
||||||
|
'upload_date': '20211102'
|
||||||
|
},
|
||||||
|
'params': {'skip_download': True}
|
||||||
|
}, {
|
||||||
|
'url': 'https://onefootball.com/en/video/klopp-fumes-at-var-decisions-in-west-ham-defeat-34041020',
|
||||||
|
'info_dict': {
|
||||||
|
'id': '34041020',
|
||||||
|
'ext': 'mp4',
|
||||||
|
'title': 'Klopp fumes at VAR decisions in West Ham defeat',
|
||||||
|
'description': 'md5:9c50371095a01ad3f63311c73d8f51a5',
|
||||||
|
'thumbnail': 'https://photobooth-api.onefootball.com/api/screenshot/https:%2F%2Fperegrine-api.onefootball.com%2Fv2%2Fphotobooth%2Fcms%2Fen%2F34041020',
|
||||||
|
'timestamp': 1636314103,
|
||||||
|
'upload_date': '20211107'
|
||||||
|
},
|
||||||
|
'params': {'skip_download': True}
|
||||||
|
}]
|
||||||
|
|
||||||
|
def _real_extract(self, url):
|
||||||
|
id = self._match_id(url)
|
||||||
|
webpage = self._download_webpage(url, id)
|
||||||
|
data_json = self._search_json_ld(webpage, id)
|
||||||
|
m3u8_url = self._html_search_regex(r'(https://cdn\.jwplayer\.com/manifests/.+\.m3u8)', webpage, 'm3u8_url')
|
||||||
|
formats, subtitles = self._extract_m3u8_formats_and_subtitles(m3u8_url, id)
|
||||||
|
self._sort_formats(formats)
|
||||||
|
return {
|
||||||
|
'id': id,
|
||||||
|
'title': data_json.get('title'),
|
||||||
|
'description': data_json.get('description'),
|
||||||
|
'thumbnail': data_json.get('thumbnail'),
|
||||||
|
'timestamp': data_json.get('timestamp'),
|
||||||
|
'formats': formats,
|
||||||
|
'subtitles': subtitles,
|
||||||
|
}
|
||||||
@@ -191,7 +191,7 @@ class PatreonIE(InfoExtractor):
|
|||||||
|
|
||||||
class PatreonUserIE(InfoExtractor):
|
class PatreonUserIE(InfoExtractor):
|
||||||
|
|
||||||
_VALID_URL = r'https?://(?:www\.)?patreon\.com/(?P<id>[-_\w\d]+)/?(?:posts/?)?'
|
_VALID_URL = r'https?://(?:www\.)?patreon\.com/(?!rss)(?P<id>[-\w]+)'
|
||||||
|
|
||||||
_TESTS = [{
|
_TESTS = [{
|
||||||
'url': 'https://www.patreon.com/dissonancepod/',
|
'url': 'https://www.patreon.com/dissonancepod/',
|
||||||
|
|||||||
@@ -193,7 +193,7 @@ class PBSIE(InfoExtractor):
|
|||||||
# Article with embedded player (or direct video)
|
# Article with embedded player (or direct video)
|
||||||
(?:www\.)?pbs\.org/(?:[^/]+/){1,5}(?P<presumptive_id>[^/]+?)(?:\.html)?/?(?:$|[?\#]) |
|
(?:www\.)?pbs\.org/(?:[^/]+/){1,5}(?P<presumptive_id>[^/]+?)(?:\.html)?/?(?:$|[?\#]) |
|
||||||
# Player
|
# Player
|
||||||
(?:video|player)\.pbs\.org/(?:widget/)?partnerplayer/(?P<player_id>[^/]+)/
|
(?:video|player)\.pbs\.org/(?:widget/)?partnerplayer/(?P<player_id>[^/]+)
|
||||||
)
|
)
|
||||||
''' % '|'.join(list(zip(*_STATIONS))[0])
|
''' % '|'.join(list(zip(*_STATIONS))[0])
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,57 @@
|
|||||||
|
# coding: utf-8
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from .common import InfoExtractor
|
||||||
|
from ..utils import js_to_json
|
||||||
|
|
||||||
|
|
||||||
|
class PeerTVIE(InfoExtractor):
|
||||||
|
IE_NAME = 'peer.tv'
|
||||||
|
_VALID_URL = r'https?://(?:www\.)?peer\.tv/(?:de|it|en)/(?P<id>\d+)'
|
||||||
|
_TESTS = [{
|
||||||
|
'url': 'https://www.peer.tv/de/841',
|
||||||
|
'info_dict': {
|
||||||
|
'id': '841',
|
||||||
|
'ext': 'mp4',
|
||||||
|
'title': 'Die Brunnenburg',
|
||||||
|
'description': 'md5:4395f6142b090338340ab88a3aae24ed',
|
||||||
|
},
|
||||||
|
}, {
|
||||||
|
'url': 'https://www.peer.tv/it/404',
|
||||||
|
'info_dict': {
|
||||||
|
'id': '404',
|
||||||
|
'ext': 'mp4',
|
||||||
|
'title': 'Cascate di ghiaccio in Val Gardena',
|
||||||
|
'description': 'md5:e8e5907f236171842674e8090e3577b8',
|
||||||
|
},
|
||||||
|
}]
|
||||||
|
|
||||||
|
def _real_extract(self, url):
|
||||||
|
video_id = self._match_id(url)
|
||||||
|
webpage = self._download_webpage(url, video_id)
|
||||||
|
|
||||||
|
video_key = self._html_search_regex(r'player\.peer\.tv/js/([a-zA-Z0-9]+)', webpage, 'video key')
|
||||||
|
|
||||||
|
js = self._download_webpage(f'https://player.peer.tv/js/{video_key}/', video_id,
|
||||||
|
headers={'Referer': 'https://www.peer.tv/'}, note='Downloading session id')
|
||||||
|
|
||||||
|
session_id = self._search_regex(r'["\']session_id["\']:\s*["\']([a-zA-Z0-9]+)["\']', js, 'session id')
|
||||||
|
|
||||||
|
player_webpage = self._download_webpage(
|
||||||
|
f'https://player.peer.tv/jsc/{video_key}/{session_id}?jsr=aHR0cHM6Ly93d3cucGVlci50di9kZS84NDE=&cs=UTF-8&mq=2&ua=0&webm=p&mp4=p&hls=1',
|
||||||
|
video_id, note='Downloading player webpage')
|
||||||
|
|
||||||
|
m3u8_url = self._search_regex(r'["\']playlist_url["\']:\s*(["\'][^"\']+["\'])', player_webpage, 'm3u8 url')
|
||||||
|
m3u8_url = self._parse_json(m3u8_url, video_id, transform_source=js_to_json)
|
||||||
|
|
||||||
|
formats = self._extract_m3u8_formats(m3u8_url, video_id, m3u8_id='hls')
|
||||||
|
|
||||||
|
self._sort_formats(formats)
|
||||||
|
|
||||||
|
return {
|
||||||
|
'id': video_id,
|
||||||
|
'title': self._html_search_regex(r'<h1>(.+?)</h1>', webpage, 'title').replace('\xa0', ' '),
|
||||||
|
'formats': formats,
|
||||||
|
'description': self._html_search_meta(('og:description', 'description'), webpage),
|
||||||
|
'thumbnail': self._html_search_meta(('og:image', 'image'), webpage)
|
||||||
|
}
|
||||||
@@ -9,7 +9,7 @@ from ..utils import (
|
|||||||
|
|
||||||
|
|
||||||
class PlanetMarathiIE(InfoExtractor):
|
class PlanetMarathiIE(InfoExtractor):
|
||||||
_VALID_URL = r'(?:https?://)(?:www\.)?planetmarathi\.com/titles/(?P<id>[^/#&?$]+)'
|
_VALID_URL = r'https?://(?:www\.)?planetmarathi\.com/titles/(?P<id>[^/#&?$]+)'
|
||||||
_TESTS = [{
|
_TESTS = [{
|
||||||
'url': 'https://www.planetmarathi.com/titles/ek-unad-divas',
|
'url': 'https://www.planetmarathi.com/titles/ek-unad-divas',
|
||||||
'playlist_mincount': 2,
|
'playlist_mincount': 2,
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ from ..utils import (
|
|||||||
|
|
||||||
|
|
||||||
class ProjectVeritasIE(InfoExtractor):
|
class ProjectVeritasIE(InfoExtractor):
|
||||||
_VALID_URL = r'(?:https?://)(?:www\.)?projectveritas\.com/(?P<type>news|video)/(?P<id>[^/?#]+)'
|
_VALID_URL = r'https?://(?:www\.)?projectveritas\.com/(?P<type>news|video)/(?P<id>[^/?#]+)'
|
||||||
_TESTS = [{
|
_TESTS = [{
|
||||||
'url': 'https://www.projectveritas.com/news/exclusive-inside-the-new-york-and-new-jersey-hospitals-battling-coronavirus/',
|
'url': 'https://www.projectveritas.com/news/exclusive-inside-the-new-york-and-new-jersey-hospitals-battling-coronavirus/',
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
|
|||||||
@@ -0,0 +1,51 @@
|
|||||||
|
# coding: utf-8
|
||||||
|
from .common import InfoExtractor
|
||||||
|
from ..utils import (
|
||||||
|
traverse_obj,
|
||||||
|
strip_or_none,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class RadioZetPodcastIE(InfoExtractor):
|
||||||
|
_VALID_URL = r'https?://player\.radiozet\.pl\/Podcasty/.*?/(?P<id>.+)'
|
||||||
|
_TEST = {
|
||||||
|
'url': 'https://player.radiozet.pl/Podcasty/Nie-Ma-Za-Co/O-przedmiotach-szkolnych-ktore-przydaja-sie-w-zyciu',
|
||||||
|
'md5': 'e03665c316b4fbc5f6a8f232948bbba3',
|
||||||
|
'info_dict': {
|
||||||
|
'id': '42154',
|
||||||
|
'display_id': 'O-przedmiotach-szkolnych-ktore-przydaja-sie-w-zyciu',
|
||||||
|
'title': 'O przedmiotach szkolnych, które przydają się w życiu',
|
||||||
|
'description': 'md5:fa72bed49da334b09e5b2f79851f185c',
|
||||||
|
'release_timestamp': 1592985480,
|
||||||
|
'ext': 'mp3',
|
||||||
|
'thumbnail': r're:^https?://.*\.png$',
|
||||||
|
'duration': 83,
|
||||||
|
'series': 'Nie Ma Za Co',
|
||||||
|
'creator': 'Katarzyna Pakosińska',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def _call_api(self, podcast_id, display_id):
|
||||||
|
return self._download_json(
|
||||||
|
f'https://player.radiozet.pl/api/podcasts/getPodcast/(node)/{podcast_id}/(station)/radiozet',
|
||||||
|
display_id)
|
||||||
|
|
||||||
|
def _real_extract(self, url):
|
||||||
|
display_id = self._match_id(url)
|
||||||
|
webpage = self._download_webpage(url, display_id)
|
||||||
|
podcast_id = self._html_search_regex(r'<div.*?\sid="player".*?\sdata-id=[\'"]([^\'"]+)[\'"]',
|
||||||
|
webpage, 'podcast id')
|
||||||
|
data = self._call_api(podcast_id, display_id)['data'][0]
|
||||||
|
|
||||||
|
return {
|
||||||
|
'id': podcast_id,
|
||||||
|
'display_id': display_id,
|
||||||
|
'title': strip_or_none(data.get('title')),
|
||||||
|
'description': strip_or_none(traverse_obj(data, ('program', 'desc'))),
|
||||||
|
'release_timestamp': data.get('published_date'),
|
||||||
|
'url': traverse_obj(data, ('player', 'stream')),
|
||||||
|
'thumbnail': traverse_obj(data, ('program', 'image', 'original')),
|
||||||
|
'duration': traverse_obj(data, ('player', 'duration')),
|
||||||
|
'series': strip_or_none(traverse_obj(data, ('program', 'title'))),
|
||||||
|
'creator': strip_or_none(traverse_obj(data, ('presenter', 0, 'title'))),
|
||||||
|
}
|
||||||
+42
-40
@@ -8,43 +8,11 @@ from ..utils import (
|
|||||||
try_get,
|
try_get,
|
||||||
unescapeHTML,
|
unescapeHTML,
|
||||||
url_or_none,
|
url_or_none,
|
||||||
|
traverse_obj
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class RedditIE(InfoExtractor):
|
class RedditIE(InfoExtractor):
|
||||||
_VALID_URL = r'https?://v\.redd\.it/(?P<id>[^/?#&]+)'
|
|
||||||
_TEST = {
|
|
||||||
# from https://www.reddit.com/r/videos/comments/6rrwyj/that_small_heart_attack/
|
|
||||||
'url': 'https://v.redd.it/zv89llsvexdz',
|
|
||||||
'md5': '0a070c53eba7ec4534d95a5a1259e253',
|
|
||||||
'info_dict': {
|
|
||||||
'id': 'zv89llsvexdz',
|
|
||||||
'ext': 'mp4',
|
|
||||||
'title': 'zv89llsvexdz',
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
def _real_extract(self, url):
|
|
||||||
video_id = self._match_id(url)
|
|
||||||
|
|
||||||
formats = self._extract_m3u8_formats(
|
|
||||||
'https://v.redd.it/%s/HLSPlaylist.m3u8' % video_id, video_id,
|
|
||||||
'mp4', entry_protocol='m3u8_native', m3u8_id='hls', fatal=False)
|
|
||||||
|
|
||||||
formats.extend(self._extract_mpd_formats(
|
|
||||||
'https://v.redd.it/%s/DASHPlaylist.mpd' % video_id, video_id,
|
|
||||||
mpd_id='dash', fatal=False))
|
|
||||||
|
|
||||||
self._sort_formats(formats)
|
|
||||||
|
|
||||||
return {
|
|
||||||
'id': video_id,
|
|
||||||
'title': video_id,
|
|
||||||
'formats': formats,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class RedditRIE(InfoExtractor):
|
|
||||||
_VALID_URL = r'https?://(?P<subdomain>[^/]+\.)?reddit(?:media)?\.com/r/(?P<slug>[^/]+/comments/(?P<id>[^/?#&]+))'
|
_VALID_URL = r'https?://(?P<subdomain>[^/]+\.)?reddit(?:media)?\.com/r/(?P<slug>[^/]+/comments/(?P<id>[^/?#&]+))'
|
||||||
_TESTS = [{
|
_TESTS = [{
|
||||||
'url': 'https://www.reddit.com/r/videos/comments/6rrwyj/that_small_heart_attack/',
|
'url': 'https://www.reddit.com/r/videos/comments/6rrwyj/that_small_heart_attack/',
|
||||||
@@ -147,19 +115,53 @@ class RedditRIE(InfoExtractor):
|
|||||||
for resolution in resolutions:
|
for resolution in resolutions:
|
||||||
add_thumbnail(resolution)
|
add_thumbnail(resolution)
|
||||||
|
|
||||||
return {
|
info = {
|
||||||
'_type': 'url_transparent',
|
|
||||||
'url': video_url,
|
|
||||||
'title': data.get('title'),
|
'title': data.get('title'),
|
||||||
'thumbnails': thumbnails,
|
'thumbnails': thumbnails,
|
||||||
'timestamp': float_or_none(data.get('created_utc')),
|
'timestamp': float_or_none(data.get('created_utc')),
|
||||||
'uploader': data.get('author'),
|
'uploader': data.get('author'),
|
||||||
'duration': int_or_none(try_get(
|
|
||||||
data,
|
|
||||||
(lambda x: x['media']['reddit_video']['duration'],
|
|
||||||
lambda x: x['secure_media']['reddit_video']['duration']))),
|
|
||||||
'like_count': int_or_none(data.get('ups')),
|
'like_count': int_or_none(data.get('ups')),
|
||||||
'dislike_count': int_or_none(data.get('downs')),
|
'dislike_count': int_or_none(data.get('downs')),
|
||||||
'comment_count': int_or_none(data.get('num_comments')),
|
'comment_count': int_or_none(data.get('num_comments')),
|
||||||
'age_limit': age_limit,
|
'age_limit': age_limit,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Check if media is hosted on reddit:
|
||||||
|
reddit_video = traverse_obj(data, (('media', 'secure_media'), 'reddit_video'), get_all=False)
|
||||||
|
if reddit_video:
|
||||||
|
playlist_urls = [
|
||||||
|
try_get(reddit_video, lambda x: unescapeHTML(x[y]))
|
||||||
|
for y in ('dash_url', 'hls_url')
|
||||||
|
]
|
||||||
|
|
||||||
|
# Update video_id
|
||||||
|
display_id = video_id
|
||||||
|
video_id = self._search_regex(
|
||||||
|
r'https?://v\.redd\.it/(?P<id>[^/?#&]+)', reddit_video['fallback_url'],
|
||||||
|
'video_id', default=display_id)
|
||||||
|
|
||||||
|
dash_playlist_url = playlist_urls[0] or f'https://v.redd.it/{video_id}/DASHPlaylist.mpd'
|
||||||
|
hls_playlist_url = playlist_urls[1] or f'https://v.redd.it/{video_id}/HLSPlaylist.m3u8'
|
||||||
|
|
||||||
|
formats = self._extract_m3u8_formats(
|
||||||
|
hls_playlist_url, display_id, 'mp4',
|
||||||
|
entry_protocol='m3u8_native', m3u8_id='hls', fatal=False)
|
||||||
|
formats.extend(self._extract_mpd_formats(
|
||||||
|
dash_playlist_url, display_id, mpd_id='dash', fatal=False))
|
||||||
|
self._sort_formats(formats)
|
||||||
|
|
||||||
|
return {
|
||||||
|
**info,
|
||||||
|
'id': video_id,
|
||||||
|
'display_id': display_id,
|
||||||
|
'formats': formats,
|
||||||
|
'duration': int_or_none(reddit_video.get('duration')),
|
||||||
|
}
|
||||||
|
|
||||||
|
# Not hosted on reddit, must continue extraction
|
||||||
|
return {
|
||||||
|
**info,
|
||||||
|
'display_id': video_id,
|
||||||
|
'_type': 'url_transparent',
|
||||||
|
'url': video_url,
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,232 @@
|
|||||||
|
# coding: utf-8
|
||||||
|
import functools
|
||||||
|
|
||||||
|
from .common import InfoExtractor
|
||||||
|
from ..compat import compat_parse_qs
|
||||||
|
from ..utils import (
|
||||||
|
ExtractorError,
|
||||||
|
int_or_none,
|
||||||
|
qualities,
|
||||||
|
try_get,
|
||||||
|
OnDemandPagedList,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class RedGifsBaseInfoExtractor(InfoExtractor):
|
||||||
|
_FORMATS = {
|
||||||
|
'gif': 250,
|
||||||
|
'sd': 480,
|
||||||
|
'hd': None,
|
||||||
|
}
|
||||||
|
|
||||||
|
def _parse_gif_data(self, gif_data):
|
||||||
|
video_id = gif_data.get('id')
|
||||||
|
quality = qualities(tuple(self._FORMATS.keys()))
|
||||||
|
|
||||||
|
orig_height = int_or_none(gif_data.get('height'))
|
||||||
|
aspect_ratio = try_get(gif_data, lambda x: orig_height / x['width'])
|
||||||
|
|
||||||
|
formats = []
|
||||||
|
for format_id, height in self._FORMATS.items():
|
||||||
|
video_url = gif_data['urls'].get(format_id)
|
||||||
|
if not video_url:
|
||||||
|
continue
|
||||||
|
height = min(orig_height, height or orig_height)
|
||||||
|
formats.append({
|
||||||
|
'url': video_url,
|
||||||
|
'format_id': format_id,
|
||||||
|
'width': height * aspect_ratio if aspect_ratio else None,
|
||||||
|
'height': height,
|
||||||
|
'quality': quality(format_id),
|
||||||
|
})
|
||||||
|
self._sort_formats(formats)
|
||||||
|
|
||||||
|
return {
|
||||||
|
'id': video_id,
|
||||||
|
'webpage_url': f'https://redgifs.com/watch/{video_id}',
|
||||||
|
'ie_key': RedGifsIE.ie_key(),
|
||||||
|
'extractor': 'RedGifs',
|
||||||
|
'title': ' '.join(gif_data.get('tags') or []) or 'RedGifs',
|
||||||
|
'timestamp': int_or_none(gif_data.get('createDate')),
|
||||||
|
'uploader': gif_data.get('userName'),
|
||||||
|
'duration': int_or_none(gif_data.get('duration')),
|
||||||
|
'view_count': int_or_none(gif_data.get('views')),
|
||||||
|
'like_count': int_or_none(gif_data.get('likes')),
|
||||||
|
'categories': gif_data.get('tags') or [],
|
||||||
|
'tags': gif_data.get('tags'),
|
||||||
|
'age_limit': 18,
|
||||||
|
'formats': formats,
|
||||||
|
}
|
||||||
|
|
||||||
|
def _call_api(self, ep, video_id, *args, **kwargs):
|
||||||
|
data = self._download_json(
|
||||||
|
f'https://api.redgifs.com/v2/{ep}', video_id, *args, **kwargs)
|
||||||
|
if 'error' in data:
|
||||||
|
raise ExtractorError(f'RedGifs said: {data["error"]}', expected=True, video_id=video_id)
|
||||||
|
return data
|
||||||
|
|
||||||
|
def _fetch_page(self, ep, video_id, query, page):
|
||||||
|
query['page'] = page + 1
|
||||||
|
data = self._call_api(
|
||||||
|
ep, video_id, query=query, note=f'Downloading JSON metadata page {page + 1}')
|
||||||
|
|
||||||
|
for entry in data['gifs']:
|
||||||
|
yield self._parse_gif_data(entry)
|
||||||
|
|
||||||
|
def _prepare_api_query(self, query, fields):
|
||||||
|
api_query = [
|
||||||
|
(field_name, query.get(field_name, (default,))[0])
|
||||||
|
for field_name, default in fields.items()]
|
||||||
|
|
||||||
|
return {key: val for key, val in api_query if val is not None}
|
||||||
|
|
||||||
|
def _paged_entries(self, ep, item_id, query, fields):
|
||||||
|
page = int_or_none(query.get('page', (None,))[0])
|
||||||
|
page_fetcher = functools.partial(
|
||||||
|
self._fetch_page, ep, item_id, self._prepare_api_query(query, fields))
|
||||||
|
return page_fetcher(page) if page else OnDemandPagedList(page_fetcher, self._PAGE_SIZE)
|
||||||
|
|
||||||
|
|
||||||
|
class RedGifsIE(RedGifsBaseInfoExtractor):
|
||||||
|
_VALID_URL = r'https?://(?:(?:www\.)?redgifs\.com/watch/|thumbs2\.redgifs\.com/)(?P<id>[^-/?#\.]+)'
|
||||||
|
_TESTS = [{
|
||||||
|
'url': 'https://www.redgifs.com/watch/squeakyhelplesswisent',
|
||||||
|
'info_dict': {
|
||||||
|
'id': 'squeakyhelplesswisent',
|
||||||
|
'ext': 'mp4',
|
||||||
|
'title': 'Hotwife Legs Thick',
|
||||||
|
'timestamp': 1636287915,
|
||||||
|
'upload_date': '20211107',
|
||||||
|
'uploader': 'ignored52',
|
||||||
|
'duration': 16,
|
||||||
|
'view_count': int,
|
||||||
|
'like_count': int,
|
||||||
|
'categories': list,
|
||||||
|
'age_limit': 18,
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
'url': 'https://thumbs2.redgifs.com/SqueakyHelplessWisent-mobile.mp4#t=0',
|
||||||
|
'info_dict': {
|
||||||
|
'id': 'squeakyhelplesswisent',
|
||||||
|
'ext': 'mp4',
|
||||||
|
'title': 'Hotwife Legs Thick',
|
||||||
|
'timestamp': 1636287915,
|
||||||
|
'upload_date': '20211107',
|
||||||
|
'uploader': 'ignored52',
|
||||||
|
'duration': 16,
|
||||||
|
'view_count': int,
|
||||||
|
'like_count': int,
|
||||||
|
'categories': list,
|
||||||
|
'age_limit': 18,
|
||||||
|
}
|
||||||
|
}]
|
||||||
|
|
||||||
|
def _real_extract(self, url):
|
||||||
|
video_id = self._match_id(url).lower()
|
||||||
|
video_info = self._call_api(
|
||||||
|
f'gifs/{video_id}', video_id, note='Downloading video info')
|
||||||
|
return self._parse_gif_data(video_info['gif'])
|
||||||
|
|
||||||
|
|
||||||
|
class RedGifsSearchIE(RedGifsBaseInfoExtractor):
|
||||||
|
IE_DESC = 'Redgifs search'
|
||||||
|
_VALID_URL = r'https?://(?:www\.)?redgifs\.com/browse\?(?P<query>[^#]+)'
|
||||||
|
_PAGE_SIZE = 80
|
||||||
|
_TESTS = [
|
||||||
|
{
|
||||||
|
'url': 'https://www.redgifs.com/browse?tags=Lesbian',
|
||||||
|
'info_dict': {
|
||||||
|
'id': 'tags=Lesbian',
|
||||||
|
'title': 'Lesbian',
|
||||||
|
'description': 'RedGifs search for Lesbian, ordered by trending'
|
||||||
|
},
|
||||||
|
'playlist_mincount': 100,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'url': 'https://www.redgifs.com/browse?type=g&order=latest&tags=Lesbian',
|
||||||
|
'info_dict': {
|
||||||
|
'id': 'type=g&order=latest&tags=Lesbian',
|
||||||
|
'title': 'Lesbian',
|
||||||
|
'description': 'RedGifs search for Lesbian, ordered by latest'
|
||||||
|
},
|
||||||
|
'playlist_mincount': 100,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'url': 'https://www.redgifs.com/browse?type=g&order=latest&tags=Lesbian&page=2',
|
||||||
|
'info_dict': {
|
||||||
|
'id': 'type=g&order=latest&tags=Lesbian&page=2',
|
||||||
|
'title': 'Lesbian',
|
||||||
|
'description': 'RedGifs search for Lesbian, ordered by latest'
|
||||||
|
},
|
||||||
|
'playlist_count': 80,
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
def _real_extract(self, url):
|
||||||
|
query_str = self._match_valid_url(url).group('query')
|
||||||
|
query = compat_parse_qs(query_str)
|
||||||
|
if not query.get('tags'):
|
||||||
|
raise ExtractorError('Invalid query tags', expected=True)
|
||||||
|
|
||||||
|
tags = query.get('tags')[0]
|
||||||
|
order = query.get('order', ('trending',))[0]
|
||||||
|
|
||||||
|
query['search_text'] = [tags]
|
||||||
|
entries = self._paged_entries('gifs/search', query_str, query, {
|
||||||
|
'search_text': None,
|
||||||
|
'order': 'trending',
|
||||||
|
'type': None,
|
||||||
|
})
|
||||||
|
|
||||||
|
return self.playlist_result(
|
||||||
|
entries, query_str, tags, f'RedGifs search for {tags}, ordered by {order}')
|
||||||
|
|
||||||
|
|
||||||
|
class RedGifsUserIE(RedGifsBaseInfoExtractor):
|
||||||
|
IE_DESC = 'Redgifs user'
|
||||||
|
_VALID_URL = r'https?://(?:www\.)?redgifs\.com/users/(?P<username>[^/?#]+)(?:\?(?P<query>[^#]+))?'
|
||||||
|
_PAGE_SIZE = 30
|
||||||
|
_TESTS = [
|
||||||
|
{
|
||||||
|
'url': 'https://www.redgifs.com/users/lamsinka89',
|
||||||
|
'info_dict': {
|
||||||
|
'id': 'lamsinka89',
|
||||||
|
'title': 'lamsinka89',
|
||||||
|
'description': 'RedGifs user lamsinka89, ordered by recent'
|
||||||
|
},
|
||||||
|
'playlist_mincount': 100,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'url': 'https://www.redgifs.com/users/lamsinka89?page=3',
|
||||||
|
'info_dict': {
|
||||||
|
'id': 'lamsinka89?page=3',
|
||||||
|
'title': 'lamsinka89',
|
||||||
|
'description': 'RedGifs user lamsinka89, ordered by recent'
|
||||||
|
},
|
||||||
|
'playlist_count': 30,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'url': 'https://www.redgifs.com/users/lamsinka89?order=best&type=g',
|
||||||
|
'info_dict': {
|
||||||
|
'id': 'lamsinka89?order=best&type=g',
|
||||||
|
'title': 'lamsinka89',
|
||||||
|
'description': 'RedGifs user lamsinka89, ordered by best'
|
||||||
|
},
|
||||||
|
'playlist_mincount': 100,
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
def _real_extract(self, url):
|
||||||
|
username, query_str = self._match_valid_url(url).group('username', 'query')
|
||||||
|
playlist_id = f'{username}?{query_str}' if query_str else username
|
||||||
|
|
||||||
|
query = compat_parse_qs(query_str)
|
||||||
|
order = query.get('order', ('recent',))[0]
|
||||||
|
|
||||||
|
entries = self._paged_entries(f'users/{username}/search', playlist_id, query, {
|
||||||
|
'order': 'recent',
|
||||||
|
'type': None,
|
||||||
|
})
|
||||||
|
|
||||||
|
return self.playlist_result(
|
||||||
|
entries, playlist_id, username, f'RedGifs user {username}, ordered by {order}')
|
||||||
@@ -12,6 +12,7 @@ from ..utils import (
|
|||||||
url_or_none,
|
url_or_none,
|
||||||
urlencode_postdata,
|
urlencode_postdata,
|
||||||
urljoin,
|
urljoin,
|
||||||
|
update_url_query,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -182,6 +183,13 @@ class RoosterTeethSeriesIE(RoosterTeethBaseIE):
|
|||||||
'id': 'role-initiative',
|
'id': 'role-initiative',
|
||||||
'title': 'Role Initiative',
|
'title': 'Role Initiative',
|
||||||
}
|
}
|
||||||
|
}, {
|
||||||
|
'url': 'https://roosterteeth.com/series/let-s-play-minecraft?season=9',
|
||||||
|
'playlist_mincount': 50,
|
||||||
|
'info_dict': {
|
||||||
|
'id': 'let-s-play-minecraft-9',
|
||||||
|
'title': 'Let\'s Play Minecraft - Season 9',
|
||||||
|
}
|
||||||
}]
|
}]
|
||||||
|
|
||||||
def _entries(self, series_id, season_number):
|
def _entries(self, series_id, season_number):
|
||||||
@@ -192,7 +200,7 @@ class RoosterTeethSeriesIE(RoosterTeethBaseIE):
|
|||||||
idx = traverse_obj(data, ('attributes', 'number'))
|
idx = traverse_obj(data, ('attributes', 'number'))
|
||||||
if season_number and idx != season_number:
|
if season_number and idx != season_number:
|
||||||
continue
|
continue
|
||||||
season_url = urljoin(self._API_BASE, data['links']['episodes'])
|
season_url = update_url_query(urljoin(self._API_BASE, data['links']['episodes']), {'per_page': 1000})
|
||||||
season = self._download_json(season_url, display_id, f'Downloading season {idx} JSON metadata')['data']
|
season = self._download_json(season_url, display_id, f'Downloading season {idx} JSON metadata')['data']
|
||||||
for episode in season:
|
for episode in season:
|
||||||
yield self.url_result(
|
yield self.url_result(
|
||||||
|
|||||||
@@ -0,0 +1,67 @@
|
|||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from .common import InfoExtractor
|
||||||
|
|
||||||
|
|
||||||
|
class RTRFMIE(InfoExtractor):
|
||||||
|
_VALID_URL = r'https?://(?:www\.)?rtrfm\.com\.au/(?:shows|show-episode)/(?P<id>[^/?\#&]+)'
|
||||||
|
_TESTS = [
|
||||||
|
{
|
||||||
|
'url': 'https://rtrfm.com.au/shows/breakfast/',
|
||||||
|
'md5': '46168394d3a5ce237cf47e85d0745413',
|
||||||
|
'info_dict': {
|
||||||
|
'id': 'breakfast-2021-11-16',
|
||||||
|
'ext': 'mp3',
|
||||||
|
'series': 'Breakfast with Taylah',
|
||||||
|
'title': r're:^Breakfast with Taylah \d{4}-\d{2}-\d{2}$',
|
||||||
|
'description': 'md5:0979c3ab1febfbec3f1ccb743633c611',
|
||||||
|
},
|
||||||
|
'skip': 'ID and md5 changes daily',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'url': 'https://rtrfm.com.au/show-episode/breakfast-2021-11-11/',
|
||||||
|
'md5': '396bedf1e40f96c62b30d4999202a790',
|
||||||
|
'info_dict': {
|
||||||
|
'id': 'breakfast-2021-11-11',
|
||||||
|
'ext': 'mp3',
|
||||||
|
'series': 'Breakfast with Taylah',
|
||||||
|
'title': 'Breakfast with Taylah 2021-11-11',
|
||||||
|
'description': 'md5:0979c3ab1febfbec3f1ccb743633c611',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'url': 'https://rtrfm.com.au/show-episode/breakfast-2020-06-01/',
|
||||||
|
'md5': '594027f513ec36a24b15d65007a24dff',
|
||||||
|
'info_dict': {
|
||||||
|
'id': 'breakfast-2020-06-01',
|
||||||
|
'ext': 'mp3',
|
||||||
|
'series': 'Breakfast with Taylah',
|
||||||
|
'title': 'Breakfast with Taylah 2020-06-01',
|
||||||
|
'description': r're:^Breakfast with Taylah ',
|
||||||
|
},
|
||||||
|
'skip': 'This audio has expired',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
def _real_extract(self, url):
|
||||||
|
display_id = self._match_id(url)
|
||||||
|
webpage = self._download_webpage(url, display_id)
|
||||||
|
show, date, title = self._search_regex(
|
||||||
|
r'''\.playShow(?:From)?\(['"](?P<show>[^'"]+)['"],\s*['"](?P<date>[0-9]{4}-[0-9]{2}-[0-9]{2})['"],\s*['"](?P<title>[^'"]+)['"]''',
|
||||||
|
webpage, 'details', group=('show', 'date', 'title'))
|
||||||
|
url = self._download_json(
|
||||||
|
'https://restreams.rtrfm.com.au/rzz',
|
||||||
|
show, 'Downloading MP3 URL', query={'n': show, 'd': date})['u']
|
||||||
|
# This is the only indicator of an error until trying to download the URL and
|
||||||
|
# downloads of mp4 URLs always fail (403 for current episodes, 404 for missing).
|
||||||
|
if '.mp4' in url:
|
||||||
|
url = None
|
||||||
|
self.raise_no_formats('Expired or no episode on this date', expected=True)
|
||||||
|
return {
|
||||||
|
'id': '%s-%s' % (show, date),
|
||||||
|
'title': '%s %s' % (title, date),
|
||||||
|
'series': title,
|
||||||
|
'url': url,
|
||||||
|
'release_date': date,
|
||||||
|
'description': self._og_search_description(webpage),
|
||||||
|
}
|
||||||
+16
-1
@@ -10,7 +10,14 @@ from ..utils import (
|
|||||||
|
|
||||||
class SBSIE(InfoExtractor):
|
class SBSIE(InfoExtractor):
|
||||||
IE_DESC = 'sbs.com.au'
|
IE_DESC = 'sbs.com.au'
|
||||||
_VALID_URL = r'https?://(?:www\.)?sbs\.com\.au/(?:ondemand(?:/video/(?:single/)?|.*?\bplay=|/watch/)|news/(?:embeds/)?video/)(?P<id>[0-9]+)'
|
_VALID_URL = r'''(?x)
|
||||||
|
https?://(?:www\.)?sbs\.com\.au/(?:
|
||||||
|
ondemand(?:
|
||||||
|
/video/(?:single/)?|
|
||||||
|
/movie/[^/]+/|
|
||||||
|
.*?\bplay=|/watch/
|
||||||
|
)|news/(?:embeds/)?video/
|
||||||
|
)(?P<id>[0-9]+)'''
|
||||||
|
|
||||||
_TESTS = [{
|
_TESTS = [{
|
||||||
# Original URL is handled by the generic IE which finds the iframe:
|
# Original URL is handled by the generic IE which finds the iframe:
|
||||||
@@ -46,6 +53,13 @@ class SBSIE(InfoExtractor):
|
|||||||
}, {
|
}, {
|
||||||
'url': 'https://www.sbs.com.au/ondemand/watch/1698704451971',
|
'url': 'https://www.sbs.com.au/ondemand/watch/1698704451971',
|
||||||
'only_matching': True,
|
'only_matching': True,
|
||||||
|
}, {
|
||||||
|
'url': 'https://www.sbs.com.au/ondemand/movie/coherence/1469404227931',
|
||||||
|
'only_matching': True,
|
||||||
|
}, {
|
||||||
|
'note': 'Live stream',
|
||||||
|
'url': 'https://www.sbs.com.au/ondemand/video/1726824003663/sbs-24x7-live-stream-nsw',
|
||||||
|
'only_matching': True,
|
||||||
}]
|
}]
|
||||||
|
|
||||||
def _real_extract(self, url):
|
def _real_extract(self, url):
|
||||||
@@ -75,4 +89,5 @@ class SBSIE(InfoExtractor):
|
|||||||
'ie_key': 'ThePlatform',
|
'ie_key': 'ThePlatform',
|
||||||
'id': video_id,
|
'id': video_id,
|
||||||
'url': smuggle_url(self._proto_relative_url(theplatform_url), {'force_smil_url': True}),
|
'url': smuggle_url(self._proto_relative_url(theplatform_url), {'force_smil_url': True}),
|
||||||
|
'is_live': player_params.get('streamType') == 'live',
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,213 @@
|
|||||||
|
# coding: utf-8
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
import re
|
||||||
|
|
||||||
|
from .common import InfoExtractor
|
||||||
|
from ..compat import (
|
||||||
|
compat_parse_qs,
|
||||||
|
compat_urlparse,
|
||||||
|
)
|
||||||
|
from ..utils import (
|
||||||
|
ExtractorError,
|
||||||
|
parse_qs,
|
||||||
|
unsmuggle_url,
|
||||||
|
)
|
||||||
|
|
||||||
|
_COMMITTEES = {
|
||||||
|
'ag': ('76440', 'http://ag-f.akamaihd.net'),
|
||||||
|
'aging': ('76442', 'http://aging-f.akamaihd.net'),
|
||||||
|
'approps': ('76441', 'http://approps-f.akamaihd.net'),
|
||||||
|
'arch': ('', 'http://ussenate-f.akamaihd.net'),
|
||||||
|
'armed': ('76445', 'http://armed-f.akamaihd.net'),
|
||||||
|
'banking': ('76446', 'http://banking-f.akamaihd.net'),
|
||||||
|
'budget': ('76447', 'http://budget-f.akamaihd.net'),
|
||||||
|
'cecc': ('76486', 'http://srs-f.akamaihd.net'),
|
||||||
|
'commerce': ('80177', 'http://commerce1-f.akamaihd.net'),
|
||||||
|
'csce': ('75229', 'http://srs-f.akamaihd.net'),
|
||||||
|
'dpc': ('76590', 'http://dpc-f.akamaihd.net'),
|
||||||
|
'energy': ('76448', 'http://energy-f.akamaihd.net'),
|
||||||
|
'epw': ('76478', 'http://epw-f.akamaihd.net'),
|
||||||
|
'ethics': ('76449', 'http://ethics-f.akamaihd.net'),
|
||||||
|
'finance': ('76450', 'http://finance-f.akamaihd.net'),
|
||||||
|
'foreign': ('76451', 'http://foreign-f.akamaihd.net'),
|
||||||
|
'govtaff': ('76453', 'http://govtaff-f.akamaihd.net'),
|
||||||
|
'help': ('76452', 'http://help-f.akamaihd.net'),
|
||||||
|
'indian': ('76455', 'http://indian-f.akamaihd.net'),
|
||||||
|
'intel': ('76456', 'http://intel-f.akamaihd.net'),
|
||||||
|
'intlnarc': ('76457', 'http://intlnarc-f.akamaihd.net'),
|
||||||
|
'jccic': ('85180', 'http://jccic-f.akamaihd.net'),
|
||||||
|
'jec': ('76458', 'http://jec-f.akamaihd.net'),
|
||||||
|
'judiciary': ('76459', 'http://judiciary-f.akamaihd.net'),
|
||||||
|
'rpc': ('76591', 'http://rpc-f.akamaihd.net'),
|
||||||
|
'rules': ('76460', 'http://rules-f.akamaihd.net'),
|
||||||
|
'saa': ('76489', 'http://srs-f.akamaihd.net'),
|
||||||
|
'smbiz': ('76461', 'http://smbiz-f.akamaihd.net'),
|
||||||
|
'srs': ('75229', 'http://srs-f.akamaihd.net'),
|
||||||
|
'uscc': ('76487', 'http://srs-f.akamaihd.net'),
|
||||||
|
'vetaff': ('76462', 'http://vetaff-f.akamaihd.net'),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class SenateISVPIE(InfoExtractor):
|
||||||
|
_IE_NAME = 'senate.gov:isvp'
|
||||||
|
_VALID_URL = r'https?://(?:www\.)?senate\.gov/isvp/?\?(?P<qs>.+)'
|
||||||
|
|
||||||
|
_TESTS = [{
|
||||||
|
'url': 'http://www.senate.gov/isvp/?comm=judiciary&type=live&stt=&filename=judiciary031715&auto_play=false&wmode=transparent&poster=http%3A%2F%2Fwww.judiciary.senate.gov%2Fthemes%2Fjudiciary%2Fimages%2Fvideo-poster-flash-fit.png',
|
||||||
|
'info_dict': {
|
||||||
|
'id': 'judiciary031715',
|
||||||
|
'ext': 'mp4',
|
||||||
|
'title': 'Integrated Senate Video Player',
|
||||||
|
'thumbnail': r're:^https?://.*\.(?:jpg|png)$',
|
||||||
|
},
|
||||||
|
'params': {
|
||||||
|
# m3u8 download
|
||||||
|
'skip_download': True,
|
||||||
|
},
|
||||||
|
}, {
|
||||||
|
'url': 'http://www.senate.gov/isvp/?type=live&comm=commerce&filename=commerce011514.mp4&auto_play=false',
|
||||||
|
'info_dict': {
|
||||||
|
'id': 'commerce011514',
|
||||||
|
'ext': 'mp4',
|
||||||
|
'title': 'Integrated Senate Video Player'
|
||||||
|
},
|
||||||
|
'params': {
|
||||||
|
# m3u8 download
|
||||||
|
'skip_download': True,
|
||||||
|
},
|
||||||
|
}, {
|
||||||
|
'url': 'http://www.senate.gov/isvp/?type=arch&comm=intel&filename=intel090613&hc_location=ufi',
|
||||||
|
# checksum differs each time
|
||||||
|
'info_dict': {
|
||||||
|
'id': 'intel090613',
|
||||||
|
'ext': 'mp4',
|
||||||
|
'title': 'Integrated Senate Video Player'
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
# From http://www.c-span.org/video/?96791-1
|
||||||
|
'url': 'http://www.senate.gov/isvp?type=live&comm=banking&filename=banking012715',
|
||||||
|
'only_matching': True,
|
||||||
|
}]
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _search_iframe_url(webpage):
|
||||||
|
mobj = re.search(
|
||||||
|
r"<iframe[^>]+src=['\"](?P<url>https?://www\.senate\.gov/isvp/?\?[^'\"]+)['\"]",
|
||||||
|
webpage)
|
||||||
|
if mobj:
|
||||||
|
return mobj.group('url')
|
||||||
|
|
||||||
|
def _real_extract(self, url):
|
||||||
|
url, smuggled_data = unsmuggle_url(url, {})
|
||||||
|
|
||||||
|
qs = compat_parse_qs(self._match_valid_url(url).group('qs'))
|
||||||
|
if not qs.get('filename') or not qs.get('type') or not qs.get('comm'):
|
||||||
|
raise ExtractorError('Invalid URL', expected=True)
|
||||||
|
|
||||||
|
video_id = re.sub(r'.mp4$', '', qs['filename'][0])
|
||||||
|
|
||||||
|
webpage = self._download_webpage(url, video_id)
|
||||||
|
|
||||||
|
if smuggled_data.get('force_title'):
|
||||||
|
title = smuggled_data['force_title']
|
||||||
|
else:
|
||||||
|
title = self._html_search_regex(r'<title>([^<]+)</title>', webpage, video_id)
|
||||||
|
poster = qs.get('poster')
|
||||||
|
thumbnail = poster[0] if poster else None
|
||||||
|
|
||||||
|
video_type = qs['type'][0]
|
||||||
|
committee = video_type if video_type == 'arch' else qs['comm'][0]
|
||||||
|
|
||||||
|
stream_num, domain = _COMMITTEES[committee]
|
||||||
|
|
||||||
|
formats = []
|
||||||
|
if video_type == 'arch':
|
||||||
|
filename = video_id if '.' in video_id else video_id + '.mp4'
|
||||||
|
m3u8_url = compat_urlparse.urljoin(domain, 'i/' + filename + '/master.m3u8')
|
||||||
|
formats = self._extract_m3u8_formats(m3u8_url, video_id, ext='mp4', m3u8_id='m3u8')
|
||||||
|
else:
|
||||||
|
hdcore_sign = 'hdcore=3.1.0'
|
||||||
|
url_params = (domain, video_id, stream_num)
|
||||||
|
f4m_url = f'%s/z/%s_1@%s/manifest.f4m?{hdcore_sign}' % url_params
|
||||||
|
m3u8_url = '%s/i/%s_1@%s/master.m3u8' % url_params
|
||||||
|
for entry in self._extract_f4m_formats(f4m_url, video_id, f4m_id='f4m'):
|
||||||
|
# URLs without the extra param induce an 404 error
|
||||||
|
entry.update({'extra_param_to_segment_url': hdcore_sign})
|
||||||
|
formats.append(entry)
|
||||||
|
for entry in self._extract_m3u8_formats(m3u8_url, video_id, ext='mp4', m3u8_id='m3u8'):
|
||||||
|
mobj = re.search(r'(?P<tag>(?:-p|-b)).m3u8', entry['url'])
|
||||||
|
if mobj:
|
||||||
|
entry['format_id'] += mobj.group('tag')
|
||||||
|
formats.append(entry)
|
||||||
|
|
||||||
|
self._sort_formats(formats)
|
||||||
|
|
||||||
|
return {
|
||||||
|
'id': video_id,
|
||||||
|
'title': title,
|
||||||
|
'formats': formats,
|
||||||
|
'thumbnail': thumbnail,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class SenateGovIE(InfoExtractor):
|
||||||
|
_IE_NAME = 'senate.gov'
|
||||||
|
_VALID_URL = r'https?:\/\/(?:www\.)?(help|appropriations|judiciary|banking|armed-services|finance)\.senate\.gov'
|
||||||
|
_TESTS = [{
|
||||||
|
'url': 'https://www.help.senate.gov/hearings/vaccines-saving-lives-ensuring-confidence-and-protecting-public-health',
|
||||||
|
'info_dict': {
|
||||||
|
'id': 'help090920',
|
||||||
|
'display_id': 'vaccines-saving-lives-ensuring-confidence-and-protecting-public-health',
|
||||||
|
'title': 'Vaccines: Saving Lives, Ensuring Confidence, and Protecting Public Health',
|
||||||
|
'description': 'The U.S. Senate Committee on Health, Education, Labor & Pensions',
|
||||||
|
'ext': 'mp4',
|
||||||
|
},
|
||||||
|
'params': {'skip_download': 'm3u8'},
|
||||||
|
}, {
|
||||||
|
'url': 'https://www.appropriations.senate.gov/hearings/watch?hearingid=B8A25434-5056-A066-6020-1F68CB75F0CD',
|
||||||
|
'info_dict': {
|
||||||
|
'id': 'appropsA051518',
|
||||||
|
'display_id': 'watch?hearingid=B8A25434-5056-A066-6020-1F68CB75F0CD',
|
||||||
|
'title': 'Review of the FY2019 Budget Request for the U.S. Army',
|
||||||
|
'ext': 'mp4',
|
||||||
|
},
|
||||||
|
'params': {'skip_download': 'm3u8'},
|
||||||
|
}, {
|
||||||
|
'url': 'https://www.banking.senate.gov/hearings/21st-century-communities-public-transportation-infrastructure-investment-and-fast-act-reauthorization',
|
||||||
|
'info_dict': {
|
||||||
|
'id': 'banking041521',
|
||||||
|
'display_id': '21st-century-communities-public-transportation-infrastructure-investment-and-fast-act-reauthorization',
|
||||||
|
'title': '21st Century Communities: Public Transportation Infrastructure Investment and FAST Act Reauthorization',
|
||||||
|
'description': 'The Official website of The United States Committee on Banking, Housing, and Urban Affairs',
|
||||||
|
'ext': 'mp4',
|
||||||
|
},
|
||||||
|
'params': {'skip_download': 'm3u8'},
|
||||||
|
}]
|
||||||
|
|
||||||
|
def _real_extract(self, url):
|
||||||
|
display_id = self._generic_id(url)
|
||||||
|
webpage = self._download_webpage(url, display_id)
|
||||||
|
parse_info = parse_qs(self._search_regex(
|
||||||
|
r'<iframe class="[^>"]*streaminghearing[^>"]*"\s[^>]*\bsrc="([^">]*)', webpage, 'hearing URL'))
|
||||||
|
|
||||||
|
stream_num, stream_domain = _COMMITTEES[parse_info['comm'][-1]]
|
||||||
|
filename = parse_info['filename'][-1]
|
||||||
|
|
||||||
|
formats = self._extract_m3u8_formats(
|
||||||
|
f'{stream_domain}/i/{filename}_1@{stream_num}/master.m3u8',
|
||||||
|
display_id, ext='mp4')
|
||||||
|
self._sort_formats(formats)
|
||||||
|
|
||||||
|
title = self._html_search_regex(
|
||||||
|
(*self._og_regexes('title'), r'(?s)<title>([^<]*?)</title>'), webpage, 'video title')
|
||||||
|
|
||||||
|
return {
|
||||||
|
'id': re.sub(r'.mp4$', '', filename),
|
||||||
|
'display_id': display_id,
|
||||||
|
'title': re.sub(r'\s+', ' ', title.split('|')[0]).strip(),
|
||||||
|
'description': self._og_search_description(webpage, default=None),
|
||||||
|
'thumbnail': self._og_search_thumbnail(webpage, default=None),
|
||||||
|
'age_limit': self._rta_search(webpage),
|
||||||
|
'formats': formats
|
||||||
|
}
|
||||||
@@ -1,153 +0,0 @@
|
|||||||
# coding: utf-8
|
|
||||||
from __future__ import unicode_literals
|
|
||||||
|
|
||||||
import re
|
|
||||||
from .common import InfoExtractor
|
|
||||||
from ..utils import (
|
|
||||||
ExtractorError,
|
|
||||||
unsmuggle_url,
|
|
||||||
)
|
|
||||||
from ..compat import (
|
|
||||||
compat_parse_qs,
|
|
||||||
compat_urlparse,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class SenateISVPIE(InfoExtractor):
|
|
||||||
_COMM_MAP = [
|
|
||||||
['ag', '76440', 'http://ag-f.akamaihd.net'],
|
|
||||||
['aging', '76442', 'http://aging-f.akamaihd.net'],
|
|
||||||
['approps', '76441', 'http://approps-f.akamaihd.net'],
|
|
||||||
['armed', '76445', 'http://armed-f.akamaihd.net'],
|
|
||||||
['banking', '76446', 'http://banking-f.akamaihd.net'],
|
|
||||||
['budget', '76447', 'http://budget-f.akamaihd.net'],
|
|
||||||
['cecc', '76486', 'http://srs-f.akamaihd.net'],
|
|
||||||
['commerce', '80177', 'http://commerce1-f.akamaihd.net'],
|
|
||||||
['csce', '75229', 'http://srs-f.akamaihd.net'],
|
|
||||||
['dpc', '76590', 'http://dpc-f.akamaihd.net'],
|
|
||||||
['energy', '76448', 'http://energy-f.akamaihd.net'],
|
|
||||||
['epw', '76478', 'http://epw-f.akamaihd.net'],
|
|
||||||
['ethics', '76449', 'http://ethics-f.akamaihd.net'],
|
|
||||||
['finance', '76450', 'http://finance-f.akamaihd.net'],
|
|
||||||
['foreign', '76451', 'http://foreign-f.akamaihd.net'],
|
|
||||||
['govtaff', '76453', 'http://govtaff-f.akamaihd.net'],
|
|
||||||
['help', '76452', 'http://help-f.akamaihd.net'],
|
|
||||||
['indian', '76455', 'http://indian-f.akamaihd.net'],
|
|
||||||
['intel', '76456', 'http://intel-f.akamaihd.net'],
|
|
||||||
['intlnarc', '76457', 'http://intlnarc-f.akamaihd.net'],
|
|
||||||
['jccic', '85180', 'http://jccic-f.akamaihd.net'],
|
|
||||||
['jec', '76458', 'http://jec-f.akamaihd.net'],
|
|
||||||
['judiciary', '76459', 'http://judiciary-f.akamaihd.net'],
|
|
||||||
['rpc', '76591', 'http://rpc-f.akamaihd.net'],
|
|
||||||
['rules', '76460', 'http://rules-f.akamaihd.net'],
|
|
||||||
['saa', '76489', 'http://srs-f.akamaihd.net'],
|
|
||||||
['smbiz', '76461', 'http://smbiz-f.akamaihd.net'],
|
|
||||||
['srs', '75229', 'http://srs-f.akamaihd.net'],
|
|
||||||
['uscc', '76487', 'http://srs-f.akamaihd.net'],
|
|
||||||
['vetaff', '76462', 'http://vetaff-f.akamaihd.net'],
|
|
||||||
['arch', '', 'http://ussenate-f.akamaihd.net/']
|
|
||||||
]
|
|
||||||
_IE_NAME = 'senate.gov'
|
|
||||||
_VALID_URL = r'https?://(?:www\.)?senate\.gov/isvp/?\?(?P<qs>.+)'
|
|
||||||
_TESTS = [{
|
|
||||||
'url': 'http://www.senate.gov/isvp/?comm=judiciary&type=live&stt=&filename=judiciary031715&auto_play=false&wmode=transparent&poster=http%3A%2F%2Fwww.judiciary.senate.gov%2Fthemes%2Fjudiciary%2Fimages%2Fvideo-poster-flash-fit.png',
|
|
||||||
'info_dict': {
|
|
||||||
'id': 'judiciary031715',
|
|
||||||
'ext': 'mp4',
|
|
||||||
'title': 'Integrated Senate Video Player',
|
|
||||||
'thumbnail': r're:^https?://.*\.(?:jpg|png)$',
|
|
||||||
},
|
|
||||||
'params': {
|
|
||||||
# m3u8 download
|
|
||||||
'skip_download': True,
|
|
||||||
},
|
|
||||||
}, {
|
|
||||||
'url': 'http://www.senate.gov/isvp/?type=live&comm=commerce&filename=commerce011514.mp4&auto_play=false',
|
|
||||||
'info_dict': {
|
|
||||||
'id': 'commerce011514',
|
|
||||||
'ext': 'mp4',
|
|
||||||
'title': 'Integrated Senate Video Player'
|
|
||||||
},
|
|
||||||
'params': {
|
|
||||||
# m3u8 download
|
|
||||||
'skip_download': True,
|
|
||||||
},
|
|
||||||
}, {
|
|
||||||
'url': 'http://www.senate.gov/isvp/?type=arch&comm=intel&filename=intel090613&hc_location=ufi',
|
|
||||||
# checksum differs each time
|
|
||||||
'info_dict': {
|
|
||||||
'id': 'intel090613',
|
|
||||||
'ext': 'mp4',
|
|
||||||
'title': 'Integrated Senate Video Player'
|
|
||||||
}
|
|
||||||
}, {
|
|
||||||
# From http://www.c-span.org/video/?96791-1
|
|
||||||
'url': 'http://www.senate.gov/isvp?type=live&comm=banking&filename=banking012715',
|
|
||||||
'only_matching': True,
|
|
||||||
}]
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def _search_iframe_url(webpage):
|
|
||||||
mobj = re.search(
|
|
||||||
r"<iframe[^>]+src=['\"](?P<url>https?://www\.senate\.gov/isvp/?\?[^'\"]+)['\"]",
|
|
||||||
webpage)
|
|
||||||
if mobj:
|
|
||||||
return mobj.group('url')
|
|
||||||
|
|
||||||
def _get_info_for_comm(self, committee):
|
|
||||||
for entry in self._COMM_MAP:
|
|
||||||
if entry[0] == committee:
|
|
||||||
return entry[1:]
|
|
||||||
|
|
||||||
def _real_extract(self, url):
|
|
||||||
url, smuggled_data = unsmuggle_url(url, {})
|
|
||||||
|
|
||||||
qs = compat_parse_qs(self._match_valid_url(url).group('qs'))
|
|
||||||
if not qs.get('filename') or not qs.get('type') or not qs.get('comm'):
|
|
||||||
raise ExtractorError('Invalid URL', expected=True)
|
|
||||||
|
|
||||||
video_id = re.sub(r'.mp4$', '', qs['filename'][0])
|
|
||||||
|
|
||||||
webpage = self._download_webpage(url, video_id)
|
|
||||||
|
|
||||||
if smuggled_data.get('force_title'):
|
|
||||||
title = smuggled_data['force_title']
|
|
||||||
else:
|
|
||||||
title = self._html_search_regex(r'<title>([^<]+)</title>', webpage, video_id)
|
|
||||||
poster = qs.get('poster')
|
|
||||||
thumbnail = poster[0] if poster else None
|
|
||||||
|
|
||||||
video_type = qs['type'][0]
|
|
||||||
committee = video_type if video_type == 'arch' else qs['comm'][0]
|
|
||||||
stream_num, domain = self._get_info_for_comm(committee)
|
|
||||||
|
|
||||||
formats = []
|
|
||||||
if video_type == 'arch':
|
|
||||||
filename = video_id if '.' in video_id else video_id + '.mp4'
|
|
||||||
formats = [{
|
|
||||||
# All parameters in the query string are necessary to prevent a 403 error
|
|
||||||
'url': compat_urlparse.urljoin(domain, filename) + '?v=3.1.0&fp=&r=&g=',
|
|
||||||
}]
|
|
||||||
else:
|
|
||||||
hdcore_sign = 'hdcore=3.1.0'
|
|
||||||
url_params = (domain, video_id, stream_num)
|
|
||||||
f4m_url = '%s/z/%s_1@%s/manifest.f4m?' % url_params + hdcore_sign
|
|
||||||
m3u8_url = '%s/i/%s_1@%s/master.m3u8' % url_params
|
|
||||||
for entry in self._extract_f4m_formats(f4m_url, video_id, f4m_id='f4m'):
|
|
||||||
# URLs without the extra param induce an 404 error
|
|
||||||
entry.update({'extra_param_to_segment_url': hdcore_sign})
|
|
||||||
formats.append(entry)
|
|
||||||
for entry in self._extract_m3u8_formats(m3u8_url, video_id, ext='mp4', m3u8_id='m3u8'):
|
|
||||||
mobj = re.search(r'(?P<tag>(?:-p|-b)).m3u8', entry['url'])
|
|
||||||
if mobj:
|
|
||||||
entry['format_id'] += mobj.group('tag')
|
|
||||||
formats.append(entry)
|
|
||||||
|
|
||||||
self._sort_formats(formats)
|
|
||||||
|
|
||||||
return {
|
|
||||||
'id': video_id,
|
|
||||||
'title': title,
|
|
||||||
'formats': formats,
|
|
||||||
'thumbnail': thumbnail,
|
|
||||||
}
|
|
||||||
@@ -16,7 +16,7 @@ from ..utils import (
|
|||||||
|
|
||||||
|
|
||||||
class ShemarooMeIE(InfoExtractor):
|
class ShemarooMeIE(InfoExtractor):
|
||||||
_VALID_URL = r'(?:https?://)(?:www\.)?shemaroome\.com/(?:movies|shows)/(?P<id>[^?#]+)'
|
_VALID_URL = r'https?://(?:www\.)?shemaroome\.com/(?:movies|shows)/(?P<id>[^?#]+)'
|
||||||
_TESTS = [{
|
_TESTS = [{
|
||||||
'url': 'https://www.shemaroome.com/movies/dil-hai-tumhaara',
|
'url': 'https://www.shemaroome.com/movies/dil-hai-tumhaara',
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
@@ -78,7 +78,7 @@ class ShemarooMeIE(InfoExtractor):
|
|||||||
iv = [0] * 16
|
iv = [0] * 16
|
||||||
m3u8_url = intlist_to_bytes(aes_cbc_decrypt(url_data, key, iv))
|
m3u8_url = intlist_to_bytes(aes_cbc_decrypt(url_data, key, iv))
|
||||||
m3u8_url = m3u8_url[:-compat_ord((m3u8_url[-1]))].decode('ascii')
|
m3u8_url = m3u8_url[:-compat_ord((m3u8_url[-1]))].decode('ascii')
|
||||||
formats = self._extract_m3u8_formats(m3u8_url, video_id, fatal=False, headers={'stream_key': data_json['stream_key']})
|
formats, m3u8_subs = self._extract_m3u8_formats_and_subtitles(m3u8_url, video_id, fatal=False, headers={'stream_key': data_json['stream_key']})
|
||||||
self._sort_formats(formats)
|
self._sort_formats(formats)
|
||||||
|
|
||||||
release_date = self._html_search_regex(
|
release_date = self._html_search_regex(
|
||||||
@@ -91,6 +91,7 @@ class ShemarooMeIE(InfoExtractor):
|
|||||||
subtitles.setdefault('EN', []).append({
|
subtitles.setdefault('EN', []).append({
|
||||||
'url': self._proto_relative_url(sub_url),
|
'url': self._proto_relative_url(sub_url),
|
||||||
})
|
})
|
||||||
|
subtitles = self._merge_subtitles(subtitles, m3u8_subs)
|
||||||
description = self._html_search_regex(r'(?s)>Synopsis(</.+?)</', webpage, 'description', fatal=False)
|
description = self._html_search_regex(r'(?s)>Synopsis(</.+?)</', webpage, 'description', fatal=False)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ from ..utils import (
|
|||||||
|
|
||||||
|
|
||||||
class SkyNewsAUIE(InfoExtractor):
|
class SkyNewsAUIE(InfoExtractor):
|
||||||
_VALID_URL = r'(?:https?://)(?:www\.)?skynews\.com\.au/[^/]+/[^/]+/[^/]+/video/(?P<id>[a-z0-9]+)'
|
_VALID_URL = r'https?://(?:www\.)?skynews\.com\.au/[^/]+/[^/]+/[^/]+/video/(?P<id>[a-z0-9]+)'
|
||||||
|
|
||||||
_TESTS = [{
|
_TESTS = [{
|
||||||
'url': 'https://www.skynews.com.au/world-news/united-states/incredible-vision-shows-lava-overflowing-from-spains-la-palma-volcano/video/0f4c6243d6903502c01251f228b91a71',
|
'url': 'https://www.skynews.com.au/world-news/united-states/incredible-vision-shows-lava-overflowing-from-spains-la-palma-volcano/video/0f4c6243d6903502c01251f228b91a71',
|
||||||
|
|||||||
@@ -893,5 +893,6 @@ class SoundcloudSearchIE(SearchInfoExtractor, SoundcloudIE):
|
|||||||
break
|
break
|
||||||
|
|
||||||
def _get_n_results(self, query, n):
|
def _get_n_results(self, query, n):
|
||||||
tracks = self._get_collection('search/tracks', query, limit=n, q=query)
|
return self.playlist_result(itertools.islice(
|
||||||
return self.playlist_result(tracks, query, query)
|
self._get_collection('search/tracks', query, limit=n, q=query),
|
||||||
|
0, None if n == float('inf') else n), query, query)
|
||||||
|
|||||||
@@ -6,19 +6,18 @@ from .mtv import MTVServicesInfoExtractor
|
|||||||
|
|
||||||
class SouthParkIE(MTVServicesInfoExtractor):
|
class SouthParkIE(MTVServicesInfoExtractor):
|
||||||
IE_NAME = 'southpark.cc.com'
|
IE_NAME = 'southpark.cc.com'
|
||||||
_VALID_URL = r'https?://(?:www\.)?(?P<url>southpark(?:\.cc|studios)\.com/(?:clips|(?:full-)?episodes|collections)/(?P<id>.+?)(\?|#|$))'
|
_VALID_URL = r'https?://(?:www\.)?(?P<url>southpark(?:\.cc|studios)\.com/((?:video-)?clips|(?:full-)?episodes|collections)/(?P<id>.+?)(\?|#|$))'
|
||||||
|
|
||||||
_FEED_URL = 'http://feeds.mtvnservices.com/od/feed/intl-mrss-player-feed'
|
_FEED_URL = 'http://feeds.mtvnservices.com/od/feed/intl-mrss-player-feed'
|
||||||
|
|
||||||
_TESTS = [{
|
_TESTS = [{
|
||||||
'url': 'http://southpark.cc.com/clips/104437/bat-daded#tab=featured',
|
'url': 'https://southpark.cc.com/video-clips/d7wr06/south-park-you-all-agreed-to-counseling',
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
'id': 'a7bff6c2-ed00-11e0-aca6-0026b9414f30',
|
|
||||||
'ext': 'mp4',
|
'ext': 'mp4',
|
||||||
'title': 'South Park|Bat Daded',
|
'title': 'You All Agreed to Counseling',
|
||||||
'description': 'Randy disqualifies South Park by getting into a fight with Bat Dad.',
|
'description': 'Kenny, Cartman, Stan, and Kyle visit Mr. Mackey and ask for his help getting Mrs. Nelson to come back. Mr. Mackey reveals the only way to get things back to normal is to get the teachers vaccinated.',
|
||||||
'timestamp': 1112760000,
|
'timestamp': 1615352400,
|
||||||
'upload_date': '20050406',
|
'upload_date': '20210310',
|
||||||
},
|
},
|
||||||
}, {
|
}, {
|
||||||
'url': 'http://southpark.cc.com/collections/7758/fan-favorites/1',
|
'url': 'http://southpark.cc.com/collections/7758/fan-favorites/1',
|
||||||
@@ -40,11 +39,11 @@ class SouthParkIE(MTVServicesInfoExtractor):
|
|||||||
|
|
||||||
class SouthParkEsIE(SouthParkIE):
|
class SouthParkEsIE(SouthParkIE):
|
||||||
IE_NAME = 'southpark.cc.com:español'
|
IE_NAME = 'southpark.cc.com:español'
|
||||||
_VALID_URL = r'https?://(?:www\.)?(?P<url>southpark\.cc\.com/episodios-en-espanol/(?P<id>.+?)(\?|#|$))'
|
_VALID_URL = r'https?://(?:www\.)?(?P<url>southpark\.cc\.com/es/episodios/(?P<id>.+?)(\?|#|$))'
|
||||||
_LANG = 'es'
|
_LANG = 'es'
|
||||||
|
|
||||||
_TESTS = [{
|
_TESTS = [{
|
||||||
'url': 'http://southpark.cc.com/episodios-en-espanol/s01e01-cartman-consigue-una-sonda-anal#source=351c1323-0b96-402d-a8b9-40d01b2e9bde&position=1&sort=!airdate',
|
'url': 'http://southpark.cc.com/es/episodios/s01e01-cartman-consigue-una-sonda-anal#source=351c1323-0b96-402d-a8b9-40d01b2e9bde&position=1&sort=!airdate',
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
'title': 'Cartman Consigue Una Sonda Anal',
|
'title': 'Cartman Consigue Una Sonda Anal',
|
||||||
'description': 'Cartman Consigue Una Sonda Anal',
|
'description': 'Cartman Consigue Una Sonda Anal',
|
||||||
|
|||||||
@@ -0,0 +1,31 @@
|
|||||||
|
# coding: utf-8
|
||||||
|
from .common import InfoExtractor
|
||||||
|
from ..utils import int_or_none, parse_iso8601
|
||||||
|
|
||||||
|
|
||||||
|
class StreamFFIE(InfoExtractor):
|
||||||
|
_VALID_URL = r'https?://(?:www\.)?streamff\.com/v/(?P<id>[a-zA-Z0-9]+)'
|
||||||
|
|
||||||
|
_TESTS = [{
|
||||||
|
'url': 'https://streamff.com/v/55cc94',
|
||||||
|
'md5': '8745a67bb5e5c570738efe7983826370',
|
||||||
|
'info_dict': {
|
||||||
|
'id': '55cc94',
|
||||||
|
'ext': 'mp4',
|
||||||
|
'title': '55cc94',
|
||||||
|
'timestamp': 1634764643,
|
||||||
|
'upload_date': '20211020',
|
||||||
|
'view_count': int,
|
||||||
|
}
|
||||||
|
}]
|
||||||
|
|
||||||
|
def _real_extract(self, url):
|
||||||
|
video_id = self._match_id(url)
|
||||||
|
json_data = self._download_json(f'https://streamff.com/api/videos/{video_id}', video_id)
|
||||||
|
return {
|
||||||
|
'id': video_id,
|
||||||
|
'title': json_data.get('name') or video_id,
|
||||||
|
'url': 'https://streamff.com/%s' % json_data['videoLink'],
|
||||||
|
'view_count': int_or_none(json_data.get('views')),
|
||||||
|
'timestamp': parse_iso8601(json_data.get('date')),
|
||||||
|
}
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
# coding: utf-8
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from .common import InfoExtractor
|
||||||
|
from ..compat import (
|
||||||
|
compat_str,
|
||||||
|
)
|
||||||
|
from ..utils import (
|
||||||
|
ExtractorError,
|
||||||
|
lowercase_escape,
|
||||||
|
try_get,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class StripchatIE(InfoExtractor):
|
||||||
|
_VALID_URL = r'https?://stripchat\.com/(?P<id>[0-9A-Za-z-_]+)'
|
||||||
|
_TESTS = [{
|
||||||
|
'url': 'https://stripchat.com/feel_me',
|
||||||
|
'info_dict': {
|
||||||
|
'id': 'feel_me',
|
||||||
|
'ext': 'mp4',
|
||||||
|
'title': 're:^feel_me [0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}$',
|
||||||
|
'description': str,
|
||||||
|
'is_live': True,
|
||||||
|
'age_limit': 18,
|
||||||
|
},
|
||||||
|
'skip': 'Room is offline',
|
||||||
|
}]
|
||||||
|
|
||||||
|
def _real_extract(self, url):
|
||||||
|
video_id = self._match_id(url)
|
||||||
|
webpage = self._download_webpage(
|
||||||
|
'https://stripchat.com/%s/' % video_id, video_id,
|
||||||
|
headers=self.geo_verification_headers())
|
||||||
|
|
||||||
|
data = self._parse_json(
|
||||||
|
self._search_regex(
|
||||||
|
r'<script\b[^>]*>\s*window\.__PRELOADED_STATE__\s*=(?P<value>.*?)<\/script>',
|
||||||
|
webpage, 'data', default='{}', group='value'),
|
||||||
|
video_id, transform_source=lowercase_escape, fatal=False)
|
||||||
|
if not data:
|
||||||
|
raise ExtractorError('Unable to find configuration for stream.')
|
||||||
|
|
||||||
|
if try_get(data, lambda x: x['viewCam']['show'], dict):
|
||||||
|
raise ExtractorError('Model is in private show', expected=True)
|
||||||
|
elif not try_get(data, lambda x: x['viewCam']['model']['isLive'], bool):
|
||||||
|
raise ExtractorError('Model is offline', expected=True)
|
||||||
|
|
||||||
|
server = try_get(data, lambda x: x['viewCam']['viewServers']['flashphoner-hls'], compat_str)
|
||||||
|
host = try_get(data, lambda x: x['config']['data']['hlsStreamHost'], compat_str)
|
||||||
|
model_id = try_get(data, lambda x: x['viewCam']['model']['id'], int)
|
||||||
|
|
||||||
|
formats = self._extract_m3u8_formats(
|
||||||
|
'https://b-%s.%s/hls/%d/%d.m3u8' % (server, host, model_id, model_id),
|
||||||
|
video_id, ext='mp4', m3u8_id='hls', fatal=False, live=True)
|
||||||
|
self._sort_formats(formats)
|
||||||
|
|
||||||
|
return {
|
||||||
|
'id': video_id,
|
||||||
|
'title': self._live_title(video_id),
|
||||||
|
'description': self._og_search_description(webpage),
|
||||||
|
'is_live': True,
|
||||||
|
'formats': formats,
|
||||||
|
# Stripchat declares the RTA meta-tag, but in an non-standard format so _rta_search() can't be used
|
||||||
|
'age_limit': 18,
|
||||||
|
}
|
||||||
@@ -58,7 +58,7 @@ class TenPlayIE(InfoExtractor):
|
|||||||
'email': username,
|
'email': username,
|
||||||
'password': password,
|
'password': password,
|
||||||
}))
|
}))
|
||||||
return "Bearer " + data['jwt']['accessToken']
|
return 'Bearer ' + data['jwt']['accessToken']
|
||||||
|
|
||||||
def _real_extract(self, url):
|
def _real_extract(self, url):
|
||||||
content_id = self._match_id(url)
|
content_id = self._match_id(url)
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ from ..utils import (
|
|||||||
|
|
||||||
|
|
||||||
class ThreeSpeakIE(InfoExtractor):
|
class ThreeSpeakIE(InfoExtractor):
|
||||||
_VALID_URL = r'(?:https?://)(?:www\.)?3speak\.tv/watch\?v\=[^/]+/(?P<id>[^/$&#?]+)'
|
_VALID_URL = r'https?://(?:www\.)?3speak\.tv/watch\?v\=[^/]+/(?P<id>[^/$&#?]+)'
|
||||||
|
|
||||||
_TESTS = [{
|
_TESTS = [{
|
||||||
'url': 'https://3speak.tv/watch?v=dannyshine/wjgoxyfy',
|
'url': 'https://3speak.tv/watch?v=dannyshine/wjgoxyfy',
|
||||||
@@ -75,7 +75,7 @@ class ThreeSpeakIE(InfoExtractor):
|
|||||||
|
|
||||||
|
|
||||||
class ThreeSpeakUserIE(InfoExtractor):
|
class ThreeSpeakUserIE(InfoExtractor):
|
||||||
_VALID_URL = r'(?:https?://)(?:www\.)?3speak\.tv/user/(?P<id>[^/$&?#]+)'
|
_VALID_URL = r'https?://(?:www\.)?3speak\.tv/user/(?P<id>[^/$&?#]+)'
|
||||||
|
|
||||||
_TESTS = [{
|
_TESTS = [{
|
||||||
'url': 'https://3speak.tv/user/theycallmedan',
|
'url': 'https://3speak.tv/user/theycallmedan',
|
||||||
|
|||||||
@@ -438,6 +438,7 @@ class TikTokUserIE(TikTokBaseIE):
|
|||||||
**self._parse_aweme_video_app(video),
|
**self._parse_aweme_video_app(video),
|
||||||
'ie_key': TikTokIE.ie_key(),
|
'ie_key': TikTokIE.ie_key(),
|
||||||
'extractor': 'TikTok',
|
'extractor': 'TikTok',
|
||||||
|
'webpage_url': f'https://tiktok.com/@{user_id}/video/{video["aweme_id"]}',
|
||||||
}
|
}
|
||||||
if not post_list.get('has_more'):
|
if not post_list.get('has_more'):
|
||||||
break
|
break
|
||||||
|
|||||||
@@ -6,7 +6,10 @@ import re
|
|||||||
|
|
||||||
from .common import InfoExtractor
|
from .common import InfoExtractor
|
||||||
from ..utils import (
|
from ..utils import (
|
||||||
|
clean_html,
|
||||||
|
get_element_by_class,
|
||||||
parse_count,
|
parse_count,
|
||||||
|
remove_end,
|
||||||
unified_strdate,
|
unified_strdate,
|
||||||
js_to_json,
|
js_to_json,
|
||||||
OnDemandPagedList,
|
OnDemandPagedList,
|
||||||
@@ -35,7 +38,7 @@ class TokentubeIE(InfoExtractor):
|
|||||||
'id': '3950239124',
|
'id': '3950239124',
|
||||||
'ext': 'mp4',
|
'ext': 'mp4',
|
||||||
'title': 'Linux Ubuntu Studio perus käyttö',
|
'title': 'Linux Ubuntu Studio perus käyttö',
|
||||||
'description': 'md5:854ff1dc732ff708976de2880ea32050',
|
'description': 'md5:46077d0daaba1974f2dc381257f9d64c',
|
||||||
'uploader': 'jyrilehtonen',
|
'uploader': 'jyrilehtonen',
|
||||||
'upload_date': '20210825',
|
'upload_date': '20210825',
|
||||||
},
|
},
|
||||||
@@ -45,7 +48,7 @@ class TokentubeIE(InfoExtractor):
|
|||||||
'id': '3582463289',
|
'id': '3582463289',
|
||||||
'ext': 'mp4',
|
'ext': 'mp4',
|
||||||
'title': 'Police for Freedom - toiminta aloitetaan Suomessa ❤️??',
|
'title': 'Police for Freedom - toiminta aloitetaan Suomessa ❤️??',
|
||||||
'description': 'md5:cd92e620d7f5fa162e8410d0fc9a08be',
|
'description': 'md5:37ebf1cb44264e0bf23ed98b337ee63e',
|
||||||
'uploader': 'Voitontie',
|
'uploader': 'Voitontie',
|
||||||
'upload_date': '20210428',
|
'upload_date': '20210428',
|
||||||
}
|
}
|
||||||
@@ -90,7 +93,10 @@ class TokentubeIE(InfoExtractor):
|
|||||||
r'<a\s*class="place-left"[^>]+>(.+?)</a>',
|
r'<a\s*class="place-left"[^>]+>(.+?)</a>',
|
||||||
webpage, 'uploader', fatal=False)
|
webpage, 'uploader', fatal=False)
|
||||||
|
|
||||||
description = self._html_search_meta('description', webpage)
|
description = (clean_html(get_element_by_class('p-d-txt', webpage))
|
||||||
|
or self._html_search_meta(('og:description', 'description', 'twitter:description'), webpage))
|
||||||
|
|
||||||
|
description = remove_end(description, 'Category')
|
||||||
|
|
||||||
self._sort_formats(formats)
|
self._sort_formats(formats)
|
||||||
|
|
||||||
|
|||||||
+16
-15
@@ -17,6 +17,11 @@ class TrovoBaseIE(InfoExtractor):
|
|||||||
_VALID_URL_BASE = r'https?://(?:www\.)?trovo\.live/'
|
_VALID_URL_BASE = r'https?://(?:www\.)?trovo\.live/'
|
||||||
_HEADERS = {'Origin': 'https://trovo.live'}
|
_HEADERS = {'Origin': 'https://trovo.live'}
|
||||||
|
|
||||||
|
def _call_api(self, video_id, query=None, data=None):
|
||||||
|
return self._download_json(
|
||||||
|
'https://gql.trovo.live/', video_id, query=query, data=data,
|
||||||
|
headers={'Accept': 'application/json'})
|
||||||
|
|
||||||
def _extract_streamer_info(self, data):
|
def _extract_streamer_info(self, data):
|
||||||
streamer_info = data.get('streamerInfo') or {}
|
streamer_info = data.get('streamerInfo') or {}
|
||||||
username = streamer_info.get('userName')
|
username = streamer_info.get('userName')
|
||||||
@@ -32,9 +37,8 @@ class TrovoIE(TrovoBaseIE):
|
|||||||
|
|
||||||
def _real_extract(self, url):
|
def _real_extract(self, url):
|
||||||
username = self._match_id(url)
|
username = self._match_id(url)
|
||||||
live_info = self._download_json(
|
live_info = self._call_api(username, query={
|
||||||
'https://gql.trovo.live/', username, query={
|
'query': '''{
|
||||||
'query': '''{
|
|
||||||
getLiveInfo(params: {userName: "%s"}) {
|
getLiveInfo(params: {userName: "%s"}) {
|
||||||
isLive
|
isLive
|
||||||
programInfo {
|
programInfo {
|
||||||
@@ -53,7 +57,7 @@ class TrovoIE(TrovoBaseIE):
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}''' % username,
|
}''' % username,
|
||||||
})['data']['getLiveInfo']
|
})['data']['getLiveInfo']
|
||||||
if live_info.get('isLive') == 0:
|
if live_info.get('isLive') == 0:
|
||||||
raise ExtractorError('%s is offline' % username, expected=True)
|
raise ExtractorError('%s is offline' % username, expected=True)
|
||||||
program_info = live_info['programInfo']
|
program_info = live_info['programInfo']
|
||||||
@@ -111,15 +115,14 @@ class TrovoVodIE(TrovoBaseIE):
|
|||||||
|
|
||||||
def _real_extract(self, url):
|
def _real_extract(self, url):
|
||||||
vid = self._match_id(url)
|
vid = self._match_id(url)
|
||||||
resp = self._download_json(
|
resp = self._call_api(vid, data=json.dumps([{
|
||||||
'https://gql.trovo.live/', vid, data=json.dumps([{
|
'query': '''{
|
||||||
'query': '''{
|
|
||||||
batchGetVodDetailInfo(params: {vids: ["%s"]}) {
|
batchGetVodDetailInfo(params: {vids: ["%s"]}) {
|
||||||
VodDetailInfos
|
VodDetailInfos
|
||||||
}
|
}
|
||||||
}''' % vid,
|
}''' % vid,
|
||||||
}, {
|
}, {
|
||||||
'query': '''{
|
'query': '''{
|
||||||
getCommentList(params: {appInfo: {postID: "%s"}, pageSize: 1000000000, preview: {}}) {
|
getCommentList(params: {appInfo: {postID: "%s"}, pageSize: 1000000000, preview: {}}) {
|
||||||
commentList {
|
commentList {
|
||||||
author {
|
author {
|
||||||
@@ -133,9 +136,7 @@ class TrovoVodIE(TrovoBaseIE):
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}''' % vid,
|
}''' % vid,
|
||||||
}]).encode(), headers={
|
}]).encode())
|
||||||
'Content-Type': 'application/json',
|
|
||||||
})
|
|
||||||
vod_detail_info = resp[0]['data']['batchGetVodDetailInfo']['VodDetailInfos'][vid]
|
vod_detail_info = resp[0]['data']['batchGetVodDetailInfo']['VodDetailInfos'][vid]
|
||||||
vod_info = vod_detail_info['vodInfo']
|
vod_info = vod_detail_info['vodInfo']
|
||||||
title = vod_info['title']
|
title = vod_info['title']
|
||||||
@@ -215,7 +216,7 @@ class TrovoChannelBaseIE(InfoExtractor):
|
|||||||
|
|
||||||
def _real_extract(self, url):
|
def _real_extract(self, url):
|
||||||
id = self._match_id(url)
|
id = self._match_id(url)
|
||||||
uid = str(self._download_json('https://gql.trovo.live/', id, query={
|
uid = str(self._call_api(id, query={
|
||||||
'query': '{getLiveInfo(params:{userName:"%s"}){streamerInfo{uid}}}' % id
|
'query': '{getLiveInfo(params:{userName:"%s"}){streamerInfo{uid}}}' % id
|
||||||
})['data']['getLiveInfo']['streamerInfo']['uid'])
|
})['data']['getLiveInfo']['streamerInfo']['uid'])
|
||||||
return self.playlist_result(self._entries(uid), playlist_id=uid)
|
return self.playlist_result(self._entries(uid), playlist_id=uid)
|
||||||
@@ -237,7 +238,7 @@ class TrovoChannelVodIE(TrovoChannelBaseIE):
|
|||||||
_TYPE = 'video'
|
_TYPE = 'video'
|
||||||
|
|
||||||
def _get_vod_json(self, page, uid):
|
def _get_vod_json(self, page, uid):
|
||||||
return self._download_json('https://gql.trovo.live/', uid, query={
|
return self._call_api(uid, query={
|
||||||
'query': self._QUERY % (page, uid)
|
'query': self._QUERY % (page, uid)
|
||||||
})['data']['getChannelLtvVideoInfos']
|
})['data']['getChannelLtvVideoInfos']
|
||||||
|
|
||||||
@@ -258,6 +259,6 @@ class TrovoChannelClipIE(TrovoChannelBaseIE):
|
|||||||
_TYPE = 'clip'
|
_TYPE = 'clip'
|
||||||
|
|
||||||
def _get_vod_json(self, page, uid):
|
def _get_vod_json(self, page, uid):
|
||||||
return self._download_json('https://gql.trovo.live/', uid, query={
|
return self._call_api(uid, query={
|
||||||
'query': self._QUERY % (page, uid)
|
'query': self._QUERY % (page, uid)
|
||||||
})['data']['getChannelClipVideoInfos']
|
})['data']['getChannelClipVideoInfos']
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ from ..utils import (
|
|||||||
|
|
||||||
|
|
||||||
class TV2IE(InfoExtractor):
|
class TV2IE(InfoExtractor):
|
||||||
_VALID_URL = r'https?://(?:www\.)?tv2\.no/v/(?P<id>\d+)'
|
_VALID_URL = r'https?://(?:www\.)?tv2\.no/v\d*/(?P<id>\d+)'
|
||||||
_TESTS = [{
|
_TESTS = [{
|
||||||
'url': 'http://www.tv2.no/v/916509/',
|
'url': 'http://www.tv2.no/v/916509/',
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
@@ -33,6 +33,9 @@ class TV2IE(InfoExtractor):
|
|||||||
'view_count': int,
|
'view_count': int,
|
||||||
'categories': list,
|
'categories': list,
|
||||||
},
|
},
|
||||||
|
}, {
|
||||||
|
'url': 'http://www.tv2.no/v2/916509',
|
||||||
|
'only_matching': True,
|
||||||
}]
|
}]
|
||||||
_PROTOCOLS = ('HLS', 'DASH')
|
_PROTOCOLS = ('HLS', 'DASH')
|
||||||
_GEO_COUNTRIES = ['NO']
|
_GEO_COUNTRIES = ['NO']
|
||||||
|
|||||||
+67
-48
@@ -12,9 +12,9 @@ from ..utils import (
|
|||||||
determine_ext,
|
determine_ext,
|
||||||
ExtractorError,
|
ExtractorError,
|
||||||
int_or_none,
|
int_or_none,
|
||||||
parse_duration,
|
|
||||||
parse_iso8601,
|
parse_iso8601,
|
||||||
qualities,
|
qualities,
|
||||||
|
traverse_obj,
|
||||||
try_get,
|
try_get,
|
||||||
update_url_query,
|
update_url_query,
|
||||||
url_or_none,
|
url_or_none,
|
||||||
@@ -431,77 +431,96 @@ class ViafreeIE(InfoExtractor):
|
|||||||
|
|
||||||
|
|
||||||
class TVPlayHomeIE(InfoExtractor):
|
class TVPlayHomeIE(InfoExtractor):
|
||||||
_VALID_URL = r'https?://(?:tv3?)?play\.(?:tv3\.lt|skaties\.lv|tv3\.ee)/(?:[^/]+/)*[^/?#&]+-(?P<id>\d+)'
|
_VALID_URL = r'''(?x)
|
||||||
|
https?://
|
||||||
|
(?:tv3?)?
|
||||||
|
play\.(?:tv3|skaties)\.(?P<country>lv|lt|ee)/
|
||||||
|
(?P<live>lives/)?
|
||||||
|
[^?#&]+(?:episode|programme|clip)-(?P<id>\d+)
|
||||||
|
'''
|
||||||
_TESTS = [{
|
_TESTS = [{
|
||||||
'url': 'https://tvplay.tv3.lt/aferistai-n-7/aferistai-10047125/',
|
'url': 'https://play.tv3.lt/series/gauju-karai-karveliai,serial-2343791/serija-8,episode-2343828',
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
'id': '366367',
|
'id': '2343828',
|
||||||
'ext': 'mp4',
|
'ext': 'mp4',
|
||||||
'title': 'Aferistai',
|
'title': 'Gaujų karai. Karveliai (2021) | S01E08: Serija 8',
|
||||||
'description': 'Aferistai. Kalėdinė pasaka.',
|
'description': 'md5:f6fcfbb236429f05531131640dfa7c81',
|
||||||
'series': 'Aferistai [N-7]',
|
'duration': 2710,
|
||||||
'season': '1 sezonas',
|
'season': 'Gaujų karai. Karveliai',
|
||||||
'season_number': 1,
|
'season_number': 1,
|
||||||
'duration': 464,
|
'release_year': 2021,
|
||||||
'timestamp': 1394209658,
|
'episode': 'Serija 8',
|
||||||
'upload_date': '20140307',
|
'episode_number': 8,
|
||||||
'age_limit': 18,
|
|
||||||
},
|
},
|
||||||
'params': {
|
'params': {
|
||||||
'skip_download': True,
|
'skip_download': 'm3u8',
|
||||||
},
|
},
|
||||||
}, {
|
}, {
|
||||||
'url': 'https://tvplay.skaties.lv/vinas-melo-labak/vinas-melo-labak-10280317/',
|
'url': 'https://play.tv3.lt/series/moterys-meluoja-geriau-n-7,serial-2574652/serija-25,episode-3284937',
|
||||||
|
'info_dict': {
|
||||||
|
'id': '3284937',
|
||||||
|
'ext': 'mp4',
|
||||||
|
'season': 'Moterys meluoja geriau [N-7]',
|
||||||
|
'season_number': 14,
|
||||||
|
'release_year': 2021,
|
||||||
|
'episode': 'Serija 25',
|
||||||
|
'episode_number': 25,
|
||||||
|
'title': 'Moterys meluoja geriau [N-7] (2021) | S14|E25: Serija 25',
|
||||||
|
'description': 'md5:c6926e9710f1a126f028fbe121eddb79',
|
||||||
|
'duration': 2440,
|
||||||
|
},
|
||||||
|
'skip': '404'
|
||||||
|
}, {
|
||||||
|
'url': 'https://play.tv3.lt/lives/tv6-lt,live-2838694/optibet-a-lygos-rungtynes-marijampoles-suduva--vilniaus-riteriai,programme-3422014',
|
||||||
'only_matching': True,
|
'only_matching': True,
|
||||||
}, {
|
}, {
|
||||||
'url': 'https://tvplay.tv3.ee/cool-d-ga-mehhikosse/cool-d-ga-mehhikosse-10044354/',
|
'url': 'https://tv3play.skaties.lv/series/women-lie-better-lv,serial-1024464/women-lie-better-lv,episode-1038762',
|
||||||
'only_matching': True,
|
'only_matching': True,
|
||||||
}, {
|
}, {
|
||||||
'url': 'https://play.tv3.lt/aferistai-10047125',
|
'url': 'https://play.tv3.ee/series/_,serial-2654462/_,episode-2654474',
|
||||||
'only_matching': True,
|
'only_matching': True,
|
||||||
}, {
|
}, {
|
||||||
'url': 'https://tv3play.skaties.lv/vinas-melo-labak-10280317',
|
'url': 'https://tv3play.skaties.lv/clips/tv3-zinas-valsti-lidz-15novembrim-bus-majsede,clip-3464509',
|
||||||
'only_matching': True,
|
|
||||||
}, {
|
|
||||||
'url': 'https://play.tv3.ee/cool-d-ga-mehhikosse-10044354',
|
|
||||||
'only_matching': True,
|
'only_matching': True,
|
||||||
}]
|
}]
|
||||||
|
|
||||||
def _real_extract(self, url):
|
def _real_extract(self, url):
|
||||||
video_id = self._match_id(url)
|
country, is_live, video_id = self._match_valid_url(url).groups()
|
||||||
|
|
||||||
asset = self._download_json(
|
api_path = 'lives/programmes' if is_live else 'vods'
|
||||||
urljoin(url, '/sb/public/asset/' + video_id), video_id)
|
data = self._download_json(
|
||||||
|
urljoin(url, f'/api/products/{api_path}/{video_id}?platform=BROWSER&lang={country.upper()}'),
|
||||||
|
video_id)
|
||||||
|
|
||||||
m3u8_url = asset['movie']['contentUrl']
|
video_type = 'CATCHUP' if is_live else 'MOVIE'
|
||||||
video_id = asset['assetId']
|
stream_id = data['programRecordingId'] if is_live else video_id
|
||||||
asset_title = asset['title']
|
stream = self._download_json(
|
||||||
title = asset_title['title']
|
urljoin(url, f'/api/products/{stream_id}/videos/playlist?videoType={video_type}&platform=BROWSER'), video_id)
|
||||||
|
formats, subtitles = self._extract_m3u8_formats_and_subtitles(
|
||||||
formats = self._extract_m3u8_formats(
|
stream['sources']['HLS'][0]['src'], video_id, 'mp4', 'm3u8_native', m3u8_id='hls')
|
||||||
m3u8_url, video_id, 'mp4', 'm3u8_native', m3u8_id='hls')
|
|
||||||
self._sort_formats(formats)
|
self._sort_formats(formats)
|
||||||
|
|
||||||
thumbnails = None
|
thumbnails = set(traverse_obj(
|
||||||
image_url = asset.get('imageUrl')
|
data, (('galary', 'images', 'artworks'), ..., ..., ('miniUrl', 'mainUrl')), expected_type=url_or_none))
|
||||||
if image_url:
|
|
||||||
thumbnails = [{
|
|
||||||
'url': urljoin(url, image_url),
|
|
||||||
'ext': 'jpg',
|
|
||||||
}]
|
|
||||||
|
|
||||||
metadata = asset.get('metadata') or {}
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
'id': video_id,
|
'id': video_id,
|
||||||
'title': title,
|
'title': self._resolve_title(data),
|
||||||
'description': asset_title.get('summaryLong') or asset_title.get('summaryShort'),
|
'description': traverse_obj(data, 'description', 'lead'),
|
||||||
'thumbnails': thumbnails,
|
'duration': int_or_none(data.get('duration')),
|
||||||
'duration': parse_duration(asset_title.get('runTime')),
|
'season': traverse_obj(data, ('season', 'serial', 'title')),
|
||||||
'series': asset.get('tvSeriesTitle'),
|
'season_number': int_or_none(traverse_obj(data, ('season', 'number'))),
|
||||||
'season': asset.get('tvSeasonTitle'),
|
'episode': data.get('title'),
|
||||||
'season_number': int_or_none(metadata.get('seasonNumber')),
|
'episode_number': int_or_none(data.get('episode')),
|
||||||
'episode': asset_title.get('titleBrief'),
|
'release_year': int_or_none(traverse_obj(data, ('season', 'serial', 'year'))),
|
||||||
'episode_number': int_or_none(metadata.get('episodeNumber')),
|
'thumbnails': [{'url': url, 'ext': 'jpg'} for url in thumbnails],
|
||||||
'formats': formats,
|
'formats': formats,
|
||||||
|
'subtitles': subtitles,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _resolve_title(data):
|
||||||
|
return try_get(data, lambda x: (
|
||||||
|
f'{data["season"]["serial"]["title"]} ({data["season"]["serial"]["year"]}) | '
|
||||||
|
f'S{data["season"]["number"]:02d}E{data["episode"]:02d}: {data["title"]}'
|
||||||
|
)) or data.get('title')
|
||||||
|
|||||||
@@ -24,6 +24,8 @@ from ..utils import (
|
|||||||
parse_iso8601,
|
parse_iso8601,
|
||||||
parse_qs,
|
parse_qs,
|
||||||
qualities,
|
qualities,
|
||||||
|
str_or_none,
|
||||||
|
traverse_obj,
|
||||||
try_get,
|
try_get,
|
||||||
unified_timestamp,
|
unified_timestamp,
|
||||||
update_url_query,
|
update_url_query,
|
||||||
@@ -52,6 +54,7 @@ class TwitchBaseIE(InfoExtractor):
|
|||||||
'VideoAccessToken_Clip': '36b89d2507fce29e5ca551df756d27c1cfe079e2609642b4390aa4c35796eb11',
|
'VideoAccessToken_Clip': '36b89d2507fce29e5ca551df756d27c1cfe079e2609642b4390aa4c35796eb11',
|
||||||
'VideoPreviewOverlay': '3006e77e51b128d838fa4e835723ca4dc9a05c5efd4466c1085215c6e437e65c',
|
'VideoPreviewOverlay': '3006e77e51b128d838fa4e835723ca4dc9a05c5efd4466c1085215c6e437e65c',
|
||||||
'VideoMetadata': '226edb3e692509f727fd56821f5653c05740242c82b0388883e0c0e75dcbf687',
|
'VideoMetadata': '226edb3e692509f727fd56821f5653c05740242c82b0388883e0c0e75dcbf687',
|
||||||
|
'VideoPlayer_ChapterSelectButtonVideo': '8d2793384aac3773beab5e59bd5d6f585aedb923d292800119e03d40cd0f9b41',
|
||||||
}
|
}
|
||||||
|
|
||||||
def _real_initialize(self):
|
def _real_initialize(self):
|
||||||
@@ -249,6 +252,38 @@ class TwitchVodIE(TwitchBaseIE):
|
|||||||
}, {
|
}, {
|
||||||
'url': 'https://player.twitch.tv/?video=480452374',
|
'url': 'https://player.twitch.tv/?video=480452374',
|
||||||
'only_matching': True,
|
'only_matching': True,
|
||||||
|
}, {
|
||||||
|
'url': 'https://www.twitch.tv/videos/635475444',
|
||||||
|
'info_dict': {
|
||||||
|
'id': 'v635475444',
|
||||||
|
'ext': 'mp4',
|
||||||
|
'title': 'Riot Games',
|
||||||
|
'duration': 11643,
|
||||||
|
'uploader': 'Riot Games',
|
||||||
|
'uploader_id': 'riotgames',
|
||||||
|
'timestamp': 1590770569,
|
||||||
|
'upload_date': '20200529',
|
||||||
|
'chapters': [
|
||||||
|
{
|
||||||
|
'start_time': 0,
|
||||||
|
'end_time': 573,
|
||||||
|
'title': 'League of Legends'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'start_time': 573,
|
||||||
|
'end_time': 3922,
|
||||||
|
'title': 'Legends of Runeterra'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'start_time': 3922,
|
||||||
|
'end_time': 11643,
|
||||||
|
'title': 'Art'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
},
|
||||||
|
'params': {
|
||||||
|
'skip_download': True
|
||||||
|
}
|
||||||
}]
|
}]
|
||||||
|
|
||||||
def _download_info(self, item_id):
|
def _download_info(self, item_id):
|
||||||
@@ -259,16 +294,24 @@ class TwitchVodIE(TwitchBaseIE):
|
|||||||
'channelLogin': '',
|
'channelLogin': '',
|
||||||
'videoID': item_id,
|
'videoID': item_id,
|
||||||
},
|
},
|
||||||
|
}, {
|
||||||
|
'operationName': 'VideoPlayer_ChapterSelectButtonVideo',
|
||||||
|
'variables': {
|
||||||
|
'includePrivate': False,
|
||||||
|
'videoID': item_id,
|
||||||
|
},
|
||||||
}],
|
}],
|
||||||
'Downloading stream metadata GraphQL')[0]['data']
|
'Downloading stream metadata GraphQL')
|
||||||
video = data.get('video')
|
|
||||||
|
video = traverse_obj(data, (0, 'data', 'video'))
|
||||||
|
video['moments'] = traverse_obj(data, (1, 'data', 'video', 'moments', 'edges', ..., 'node'))
|
||||||
|
|
||||||
if video is None:
|
if video is None:
|
||||||
raise ExtractorError(
|
raise ExtractorError(
|
||||||
'Video %s does not exist' % item_id, expected=True)
|
'Video %s does not exist' % item_id, expected=True)
|
||||||
return self._extract_info_gql(video, item_id)
|
return self._extract_info_gql(video, item_id)
|
||||||
|
|
||||||
@staticmethod
|
def _extract_info(self, info):
|
||||||
def _extract_info(info):
|
|
||||||
status = info.get('status')
|
status = info.get('status')
|
||||||
if status == 'recording':
|
if status == 'recording':
|
||||||
is_live = True
|
is_live = True
|
||||||
@@ -302,18 +345,39 @@ class TwitchVodIE(TwitchBaseIE):
|
|||||||
'timestamp': parse_iso8601(info.get('recorded_at')),
|
'timestamp': parse_iso8601(info.get('recorded_at')),
|
||||||
'view_count': int_or_none(info.get('views')),
|
'view_count': int_or_none(info.get('views')),
|
||||||
'is_live': is_live,
|
'is_live': is_live,
|
||||||
|
'was_live': True,
|
||||||
}
|
}
|
||||||
|
|
||||||
@staticmethod
|
def _extract_moments(self, info, item_id):
|
||||||
def _extract_info_gql(info, item_id):
|
for moment in info.get('moments') or []:
|
||||||
|
start_time = int_or_none(moment.get('positionMilliseconds'), 1000)
|
||||||
|
duration = int_or_none(moment.get('durationMilliseconds'), 1000)
|
||||||
|
name = str_or_none(moment.get('description'))
|
||||||
|
|
||||||
|
if start_time is None or duration is None:
|
||||||
|
self.report_warning(f'Important chapter information missing for chapter {name}', item_id)
|
||||||
|
continue
|
||||||
|
yield {
|
||||||
|
'start_time': start_time,
|
||||||
|
'end_time': start_time + duration,
|
||||||
|
'title': name,
|
||||||
|
}
|
||||||
|
|
||||||
|
def _extract_info_gql(self, info, item_id):
|
||||||
vod_id = info.get('id') or item_id
|
vod_id = info.get('id') or item_id
|
||||||
# id backward compatibility for download archives
|
# id backward compatibility for download archives
|
||||||
if vod_id[0] != 'v':
|
if vod_id[0] != 'v':
|
||||||
vod_id = 'v%s' % vod_id
|
vod_id = 'v%s' % vod_id
|
||||||
thumbnail = url_or_none(info.get('previewThumbnailURL'))
|
thumbnail = url_or_none(info.get('previewThumbnailURL'))
|
||||||
|
is_live = None
|
||||||
if thumbnail:
|
if thumbnail:
|
||||||
for p in ('width', 'height'):
|
if thumbnail.endswith('/404_processing_{width}x{height}.png'):
|
||||||
thumbnail = thumbnail.replace('{%s}' % p, '0')
|
is_live, thumbnail = True, None
|
||||||
|
else:
|
||||||
|
is_live = False
|
||||||
|
for p in ('width', 'height'):
|
||||||
|
thumbnail = thumbnail.replace('{%s}' % p, '0')
|
||||||
|
|
||||||
return {
|
return {
|
||||||
'id': vod_id,
|
'id': vod_id,
|
||||||
'title': info.get('title') or 'Untitled Broadcast',
|
'title': info.get('title') or 'Untitled Broadcast',
|
||||||
@@ -324,6 +388,9 @@ class TwitchVodIE(TwitchBaseIE):
|
|||||||
'uploader_id': try_get(info, lambda x: x['owner']['login'], compat_str),
|
'uploader_id': try_get(info, lambda x: x['owner']['login'], compat_str),
|
||||||
'timestamp': unified_timestamp(info.get('publishedAt')),
|
'timestamp': unified_timestamp(info.get('publishedAt')),
|
||||||
'view_count': int_or_none(info.get('viewCount')),
|
'view_count': int_or_none(info.get('viewCount')),
|
||||||
|
'chapters': list(self._extract_moments(info, item_id)),
|
||||||
|
'is_live': is_live,
|
||||||
|
'was_live': True,
|
||||||
}
|
}
|
||||||
|
|
||||||
def _real_extract(self, url):
|
def _real_extract(self, url):
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ from ..utils import (
|
|||||||
|
|
||||||
|
|
||||||
class UtreonIE(InfoExtractor):
|
class UtreonIE(InfoExtractor):
|
||||||
_VALID_URL = r'(?:https?://)(?:www\.)?utreon.com/v/(?P<id>[a-zA-Z0-9_-]+)'
|
_VALID_URL = r'https?://(?:www\.)?utreon.com/v/(?P<id>[a-zA-Z0-9_-]+)'
|
||||||
_TESTS = [{
|
_TESTS = [{
|
||||||
'url': 'https://utreon.com/v/z_I7ikQbuDw',
|
'url': 'https://utreon.com/v/z_I7ikQbuDw',
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user