mirror of
https://github.com/yt-dlp/yt-dlp
synced 2025-05-09 23:12:14 -05:00
Compare commits
No commits in common. "master" and "2025.02.19" have entirely different histories.
master
...
2025.02.19
6
.github/workflows/build.yml
vendored
6
.github/workflows/build.yml
vendored
@ -192,7 +192,7 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
path: ./repo
|
path: ./repo
|
||||||
- name: Virtualized Install, Prepare & Build
|
- name: Virtualized Install, Prepare & Build
|
||||||
uses: yt-dlp/run-on-arch-action@v3
|
uses: yt-dlp/run-on-arch-action@v2
|
||||||
with:
|
with:
|
||||||
# Ref: https://github.com/uraimo/run-on-arch-action/issues/55
|
# Ref: https://github.com/uraimo/run-on-arch-action/issues/55
|
||||||
env: |
|
env: |
|
||||||
@ -411,7 +411,7 @@ jobs:
|
|||||||
run: | # Custom pyinstaller built with https://github.com/yt-dlp/pyinstaller-builds
|
run: | # Custom pyinstaller built with https://github.com/yt-dlp/pyinstaller-builds
|
||||||
python devscripts/install_deps.py -o --include build
|
python devscripts/install_deps.py -o --include build
|
||||||
python devscripts/install_deps.py --include curl-cffi
|
python devscripts/install_deps.py --include curl-cffi
|
||||||
python -m pip install -U "https://yt-dlp.github.io/Pyinstaller-Builds/x86_64/pyinstaller-6.13.0-py3-none-any.whl"
|
python -m pip install -U "https://yt-dlp.github.io/Pyinstaller-Builds/x86_64/pyinstaller-6.11.1-py3-none-any.whl"
|
||||||
|
|
||||||
- name: Prepare
|
- name: Prepare
|
||||||
run: |
|
run: |
|
||||||
@ -460,7 +460,7 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
python devscripts/install_deps.py -o --include build
|
python devscripts/install_deps.py -o --include build
|
||||||
python devscripts/install_deps.py
|
python devscripts/install_deps.py
|
||||||
python -m pip install -U "https://yt-dlp.github.io/Pyinstaller-Builds/i686/pyinstaller-6.13.0-py3-none-any.whl"
|
python -m pip install -U "https://yt-dlp.github.io/Pyinstaller-Builds/i686/pyinstaller-6.11.1-py3-none-any.whl"
|
||||||
|
|
||||||
- name: Prepare
|
- name: Prepare
|
||||||
run: |
|
run: |
|
||||||
|
4
.github/workflows/core.yml
vendored
4
.github/workflows/core.yml
vendored
@ -6,7 +6,7 @@ on:
|
|||||||
- devscripts/**
|
- devscripts/**
|
||||||
- test/**
|
- test/**
|
||||||
- yt_dlp/**.py
|
- yt_dlp/**.py
|
||||||
- '!yt_dlp/extractor/**.py'
|
- '!yt_dlp/extractor/*.py'
|
||||||
- yt_dlp/extractor/__init__.py
|
- yt_dlp/extractor/__init__.py
|
||||||
- yt_dlp/extractor/common.py
|
- yt_dlp/extractor/common.py
|
||||||
- yt_dlp/extractor/extractors.py
|
- yt_dlp/extractor/extractors.py
|
||||||
@ -16,7 +16,7 @@ on:
|
|||||||
- devscripts/**
|
- devscripts/**
|
||||||
- test/**
|
- test/**
|
||||||
- yt_dlp/**.py
|
- yt_dlp/**.py
|
||||||
- '!yt_dlp/extractor/**.py'
|
- '!yt_dlp/extractor/*.py'
|
||||||
- yt_dlp/extractor/__init__.py
|
- yt_dlp/extractor/__init__.py
|
||||||
- yt_dlp/extractor/common.py
|
- yt_dlp/extractor/common.py
|
||||||
- yt_dlp/extractor/extractors.py
|
- yt_dlp/extractor/extractors.py
|
||||||
|
2
.github/workflows/quick-test.yml
vendored
2
.github/workflows/quick-test.yml
vendored
@ -38,5 +38,3 @@ jobs:
|
|||||||
run: ruff check --output-format github .
|
run: ruff check --output-format github .
|
||||||
- name: Run autopep8
|
- name: Run autopep8
|
||||||
run: autopep8 --diff .
|
run: autopep8 --diff .
|
||||||
- name: Check file mode
|
|
||||||
run: git ls-files --format="%(objectmode) %(path)" yt_dlp/ | ( ! grep -v "^100644" )
|
|
||||||
|
28
CONTRIBUTORS
28
CONTRIBUTORS
@ -742,31 +742,3 @@ lfavole
|
|||||||
mp3butcher
|
mp3butcher
|
||||||
slipinthedove
|
slipinthedove
|
||||||
YoshiTabletopGamer
|
YoshiTabletopGamer
|
||||||
Arc8ne
|
|
||||||
benfaerber
|
|
||||||
chrisellsworth
|
|
||||||
fries1234
|
|
||||||
Kenshin9977
|
|
||||||
MichaelDeBoey
|
|
||||||
msikma
|
|
||||||
pedro
|
|
||||||
pferreir
|
|
||||||
red-acid
|
|
||||||
refack
|
|
||||||
rysson
|
|
||||||
somini
|
|
||||||
thedenv
|
|
||||||
vallovic
|
|
||||||
arabcoders
|
|
||||||
mireq
|
|
||||||
mlabeeb03
|
|
||||||
1271
|
|
||||||
CasperMcFadden95
|
|
||||||
Kicer86
|
|
||||||
Kiritomo
|
|
||||||
leeblackc
|
|
||||||
meGAmeS1
|
|
||||||
NeonMan
|
|
||||||
pj47x
|
|
||||||
troex
|
|
||||||
WouterGordts
|
|
||||||
|
215
Changelog.md
215
Changelog.md
@ -4,221 +4,6 @@
|
|||||||
# To create a release, dispatch the https://github.com/yt-dlp/yt-dlp/actions/workflows/release.yml workflow on master
|
# To create a release, dispatch the https://github.com/yt-dlp/yt-dlp/actions/workflows/release.yml workflow on master
|
||||||
-->
|
-->
|
||||||
|
|
||||||
### 2025.04.30
|
|
||||||
|
|
||||||
#### Important changes
|
|
||||||
- **New option `--preset-alias`/`-t` has been added**
|
|
||||||
This provides convenient predefined aliases for common use cases. Available presets include `mp4`, `mp3`, `mkv`, `aac`, and `sleep`. See [the README](https://github.com/yt-dlp/yt-dlp/blob/master/README.md#preset-aliases) for more details.
|
|
||||||
|
|
||||||
#### Core changes
|
|
||||||
- [Add `--preset-alias` option](https://github.com/yt-dlp/yt-dlp/commit/88eb1e7a9a2720ac89d653c0d0e40292388823bb) ([#12839](https://github.com/yt-dlp/yt-dlp/issues/12839)) by [Grub4K](https://github.com/Grub4K), [seproDev](https://github.com/seproDev)
|
|
||||||
- **utils**
|
|
||||||
- `_yield_json_ld`: [Make function less fatal](https://github.com/yt-dlp/yt-dlp/commit/45f01de00e1bc076b7f676a669736326178647b1) ([#12855](https://github.com/yt-dlp/yt-dlp/issues/12855)) by [seproDev](https://github.com/seproDev)
|
|
||||||
- `url_or_none`: [Support WebSocket URLs](https://github.com/yt-dlp/yt-dlp/commit/a473e592337edb8ca40cde52c1fcaee261c54df9) ([#12848](https://github.com/yt-dlp/yt-dlp/issues/12848)) by [doe1080](https://github.com/doe1080)
|
|
||||||
|
|
||||||
#### Extractor changes
|
|
||||||
- **abematv**: [Fix thumbnail extraction](https://github.com/yt-dlp/yt-dlp/commit/f5736bb35bde62348caebf7b188668655e316deb) ([#12859](https://github.com/yt-dlp/yt-dlp/issues/12859)) by [Kiritomo](https://github.com/Kiritomo)
|
|
||||||
- **atresplayer**: [Rework extractor](https://github.com/yt-dlp/yt-dlp/commit/839d64325356310e6de6cd9cad28fb546619ca63) ([#11424](https://github.com/yt-dlp/yt-dlp/issues/11424)) by [meGAmeS1](https://github.com/meGAmeS1), [seproDev](https://github.com/seproDev)
|
|
||||||
- **bpb**: [Fix formats extraction](https://github.com/yt-dlp/yt-dlp/commit/80736b9c90818adee933a155079b8535bc06819f) ([#13015](https://github.com/yt-dlp/yt-dlp/issues/13015)) by [bashonly](https://github.com/bashonly)
|
|
||||||
- **cda**: [Fix formats extraction](https://github.com/yt-dlp/yt-dlp/commit/9032f981362ea0be90626fab51ec37934feded6d) ([#12975](https://github.com/yt-dlp/yt-dlp/issues/12975)) by [bashonly](https://github.com/bashonly)
|
|
||||||
- **cdafolder**: [Extend `_VALID_URL`](https://github.com/yt-dlp/yt-dlp/commit/cb271d445bc2d866c9a3404b1d8f59bcb77447df) ([#12919](https://github.com/yt-dlp/yt-dlp/issues/12919)) by [fireattack](https://github.com/fireattack), [Kicer86](https://github.com/Kicer86)
|
|
||||||
- **crowdbunker**: [Make format extraction non-fatal](https://github.com/yt-dlp/yt-dlp/commit/4ebf41309d04a6e196944f1c0f5f0154cff0055a) ([#12836](https://github.com/yt-dlp/yt-dlp/issues/12836)) by [seproDev](https://github.com/seproDev)
|
|
||||||
- **dacast**: [Support tokenized URLs](https://github.com/yt-dlp/yt-dlp/commit/e7e3b7a55c456da4a5a812b4fefce4dce8e6a616) ([#12979](https://github.com/yt-dlp/yt-dlp/issues/12979)) by [bashonly](https://github.com/bashonly)
|
|
||||||
- **dzen.ru**: [Rework extractors](https://github.com/yt-dlp/yt-dlp/commit/a3f2b54c2535d862de6efa9cfaa6ca9a2b2f7dd6) ([#12852](https://github.com/yt-dlp/yt-dlp/issues/12852)) by [seproDev](https://github.com/seproDev)
|
|
||||||
- **generic**: [Fix MPD extraction for `file://` URLs](https://github.com/yt-dlp/yt-dlp/commit/34a061a295d156934417c67ee98070b94943006b) ([#12978](https://github.com/yt-dlp/yt-dlp/issues/12978)) by [bashonly](https://github.com/bashonly)
|
|
||||||
- **getcourseru**: [Fix extractors](https://github.com/yt-dlp/yt-dlp/commit/741fd809bc4d301c19b53877692ae510334a6750) ([#12943](https://github.com/yt-dlp/yt-dlp/issues/12943)) by [troex](https://github.com/troex)
|
|
||||||
- **ivoox**: [Add extractor](https://github.com/yt-dlp/yt-dlp/commit/7faa18b83dcfc74a1a1e2034e6b0369c495ca645) ([#12768](https://github.com/yt-dlp/yt-dlp/issues/12768)) by [NeonMan](https://github.com/NeonMan), [seproDev](https://github.com/seproDev)
|
|
||||||
- **kika**: [Add playlist extractor](https://github.com/yt-dlp/yt-dlp/commit/3c1c75ecb8ab352f422b59af46fff2be992e4115) ([#12832](https://github.com/yt-dlp/yt-dlp/issues/12832)) by [1100101](https://github.com/1100101)
|
|
||||||
- **linkedin**
|
|
||||||
- [Support feed URLs](https://github.com/yt-dlp/yt-dlp/commit/73a26f9ee68610e33c0b4407b77355f2ab7afd0e) ([#12927](https://github.com/yt-dlp/yt-dlp/issues/12927)) by [seproDev](https://github.com/seproDev)
|
|
||||||
- events: [Add extractor](https://github.com/yt-dlp/yt-dlp/commit/b37ff4de5baf4e4e70c6a0ec34e136a279ad20af) ([#12926](https://github.com/yt-dlp/yt-dlp/issues/12926)) by [bashonly](https://github.com/bashonly), [seproDev](https://github.com/seproDev)
|
|
||||||
- **loco**: [Fix extractor](https://github.com/yt-dlp/yt-dlp/commit/f5a37ea40e20865b976ffeeff13eeae60292eb23) ([#12934](https://github.com/yt-dlp/yt-dlp/issues/12934)) by [seproDev](https://github.com/seproDev)
|
|
||||||
- **lrtradio**: [Add extractor](https://github.com/yt-dlp/yt-dlp/commit/74e90dd9b8f9c1a5c48a2515126654f4d398d687) ([#12801](https://github.com/yt-dlp/yt-dlp/issues/12801)) by [subrat-lima](https://github.com/subrat-lima)
|
|
||||||
- **manyvids**: [Fix extractor](https://github.com/yt-dlp/yt-dlp/commit/77aa15e98f34c4ad425aabf39dd1ee37b48f772c) ([#10907](https://github.com/yt-dlp/yt-dlp/issues/10907)) by [pj47x](https://github.com/pj47x)
|
|
||||||
- **mixcloud**: [Refactor extractor](https://github.com/yt-dlp/yt-dlp/commit/db6d1f145ad583e0220637726029f8f2fa6200a0) ([#12830](https://github.com/yt-dlp/yt-dlp/issues/12830)) by [seproDev](https://github.com/seproDev), [WouterGordts](https://github.com/WouterGordts)
|
|
||||||
- **mlbtv**: [Fix device ID caching](https://github.com/yt-dlp/yt-dlp/commit/36da6360e130197df927ee93409519ce3f4075f5) ([#12980](https://github.com/yt-dlp/yt-dlp/issues/12980)) by [bashonly](https://github.com/bashonly)
|
|
||||||
- **niconico**
|
|
||||||
- [Fix login support](https://github.com/yt-dlp/yt-dlp/commit/25cd7c1ecbb6cbf21dd3a6e59608e4af94715ecc) ([#13008](https://github.com/yt-dlp/yt-dlp/issues/13008)) by [doe1080](https://github.com/doe1080)
|
|
||||||
- [Remove DMC formats support](https://github.com/yt-dlp/yt-dlp/commit/7d05aa99c65352feae1cd9a3ff8784b64bfe382a) ([#12916](https://github.com/yt-dlp/yt-dlp/issues/12916)) by [doe1080](https://github.com/doe1080)
|
|
||||||
- live: [Fix extractor](https://github.com/yt-dlp/yt-dlp/commit/1d45e30537bf83e069184a440703e4c43b2e0198) ([#12809](https://github.com/yt-dlp/yt-dlp/issues/12809)) by [Snack-X](https://github.com/Snack-X)
|
|
||||||
- **panopto**: [Fix formats extraction](https://github.com/yt-dlp/yt-dlp/commit/9d26daa04ad5108257bc5e30f7f040c7f1fe7a5a) ([#12925](https://github.com/yt-dlp/yt-dlp/issues/12925)) by [seproDev](https://github.com/seproDev)
|
|
||||||
- **parti**: [Add extractors](https://github.com/yt-dlp/yt-dlp/commit/425017531fbc3369becb5a44013e26f26efabf45) ([#12769](https://github.com/yt-dlp/yt-dlp/issues/12769)) by [benfaerber](https://github.com/benfaerber)
|
|
||||||
- **raiplay**: [Fix DRM detection](https://github.com/yt-dlp/yt-dlp/commit/dce82346245e35a46fda836ca2089805d2347935) ([#12971](https://github.com/yt-dlp/yt-dlp/issues/12971)) by [DTrombett](https://github.com/DTrombett)
|
|
||||||
- **reddit**: [Support `--ignore-no-formats-error`](https://github.com/yt-dlp/yt-dlp/commit/28f04e8a5e383ff531db646190b4be45554610d6) ([#12993](https://github.com/yt-dlp/yt-dlp/issues/12993)) by [bashonly](https://github.com/bashonly)
|
|
||||||
- **royalive**: [Add extractor](https://github.com/yt-dlp/yt-dlp/commit/e1847535e28788414a25546a45bebcada2f34558) ([#12817](https://github.com/yt-dlp/yt-dlp/issues/12817)) by [CasperMcFadden95](https://github.com/CasperMcFadden95)
|
|
||||||
- **rtve**: [Rework extractors](https://github.com/yt-dlp/yt-dlp/commit/f07ee91c71920ab1187a7ea756720e81aa406a9d) ([#10388](https://github.com/yt-dlp/yt-dlp/issues/10388)) by [meGAmeS1](https://github.com/meGAmeS1), [seproDev](https://github.com/seproDev)
|
|
||||||
- **rumble**: [Improve format extraction](https://github.com/yt-dlp/yt-dlp/commit/58d0c83457b93b3c9a81eb6bc5a4c65f25e949df) ([#12838](https://github.com/yt-dlp/yt-dlp/issues/12838)) by [seproDev](https://github.com/seproDev)
|
|
||||||
- **tokfmpodcast**: [Fix formats extraction](https://github.com/yt-dlp/yt-dlp/commit/91832111a12d87499294a0f430829b8c2254c339) ([#12842](https://github.com/yt-dlp/yt-dlp/issues/12842)) by [selfisekai](https://github.com/selfisekai)
|
|
||||||
- **tv2dk**: [Fix extractor](https://github.com/yt-dlp/yt-dlp/commit/a3e91df30a45943f40759d2c1e0b6c2ca4b2a263) ([#12945](https://github.com/yt-dlp/yt-dlp/issues/12945)) by [bashonly](https://github.com/bashonly), [seproDev](https://github.com/seproDev)
|
|
||||||
- **tvp**: vod: [Improve `_VALID_URL`](https://github.com/yt-dlp/yt-dlp/commit/4e69a626cce51428bc1d66dc606a56d9498b03a5) ([#12923](https://github.com/yt-dlp/yt-dlp/issues/12923)) by [seproDev](https://github.com/seproDev)
|
|
||||||
- **tvw**: tvchannels: [Add extractor](https://github.com/yt-dlp/yt-dlp/commit/ed8ad1b4d6b9d7a1426ff5192ff924f3371e4721) ([#12721](https://github.com/yt-dlp/yt-dlp/issues/12721)) by [fries1234](https://github.com/fries1234)
|
|
||||||
- **twitcasting**: [Fix livestream extraction](https://github.com/yt-dlp/yt-dlp/commit/de271a06fd6d20d4f55597ff7f90e4d913de0a52) ([#12977](https://github.com/yt-dlp/yt-dlp/issues/12977)) by [bashonly](https://github.com/bashonly)
|
|
||||||
- **twitch**: clips: [Fix uploader metadata extraction](https://github.com/yt-dlp/yt-dlp/commit/1ae6bff564a65af41e94f1a4727892471ecdd05a) ([#13022](https://github.com/yt-dlp/yt-dlp/issues/13022)) by [1271](https://github.com/1271)
|
|
||||||
- **twitter**
|
|
||||||
- [Fix extraction when logged-in](https://github.com/yt-dlp/yt-dlp/commit/1cf39ddf3d10b6512daa7dd139e5f6c0dc548bbc) ([#13024](https://github.com/yt-dlp/yt-dlp/issues/13024)) by [bashonly](https://github.com/bashonly)
|
|
||||||
- spaces: [Improve metadata extraction](https://github.com/yt-dlp/yt-dlp/commit/70599e53b736bb75922b737e6e0d4f76e419bb20) ([#12911](https://github.com/yt-dlp/yt-dlp/issues/12911)) by [doe1080](https://github.com/doe1080)
|
|
||||||
- **vimeo**: [Extract from mobile API](https://github.com/yt-dlp/yt-dlp/commit/22ac81a0692019ac833cf282e4ef99718e9ef3fa) ([#13034](https://github.com/yt-dlp/yt-dlp/issues/13034)) by [bashonly](https://github.com/bashonly)
|
|
||||||
- **vk**
|
|
||||||
- [Fix chapters extraction](https://github.com/yt-dlp/yt-dlp/commit/5361a7c6e2933c919716e0cb1e3116c28c40419f) ([#12821](https://github.com/yt-dlp/yt-dlp/issues/12821)) by [seproDev](https://github.com/seproDev)
|
|
||||||
- [Fix uploader extraction](https://github.com/yt-dlp/yt-dlp/commit/2381881fe58a723853350a6ab750a5efc9f10c85) ([#12985](https://github.com/yt-dlp/yt-dlp/issues/12985)) by [seproDev](https://github.com/seproDev)
|
|
||||||
- **youtube**
|
|
||||||
- [Add context to video request rate limit error](https://github.com/yt-dlp/yt-dlp/commit/26feac3dd142536ad08ad1ed731378cb88e63602) ([#12958](https://github.com/yt-dlp/yt-dlp/issues/12958)) by [coletdjnz](https://github.com/coletdjnz)
|
|
||||||
- [Add extractor arg to skip "initial_data" request](https://github.com/yt-dlp/yt-dlp/commit/ed6c6d7eefbc78fa72e4e60ad6edaa3ee2acc715) ([#12865](https://github.com/yt-dlp/yt-dlp/issues/12865)) by [leeblackc](https://github.com/leeblackc)
|
|
||||||
- [Add warning on video captcha challenge](https://github.com/yt-dlp/yt-dlp/commit/f484c51599a6cd01eb078ea7dc9bbba942967774) ([#12939](https://github.com/yt-dlp/yt-dlp/issues/12939)) by [coletdjnz](https://github.com/coletdjnz)
|
|
||||||
- [Cache signature timestamps](https://github.com/yt-dlp/yt-dlp/commit/61c9a938b390b8334ee3a879fe2d93f714e30138) ([#13047](https://github.com/yt-dlp/yt-dlp/issues/13047)) by [bashonly](https://github.com/bashonly)
|
|
||||||
- [Detect and warn when account cookies are rotated](https://github.com/yt-dlp/yt-dlp/commit/8cb08028f5be2acb9835ce1670b196b9b077052f) ([#13014](https://github.com/yt-dlp/yt-dlp/issues/13014)) by [coletdjnz](https://github.com/coletdjnz)
|
|
||||||
- [Detect player JS variants for any locale](https://github.com/yt-dlp/yt-dlp/commit/c2d6659d1069f8cff97e1fd61d1c59e949e1e63d) ([#13003](https://github.com/yt-dlp/yt-dlp/issues/13003)) by [bashonly](https://github.com/bashonly)
|
|
||||||
- [Do not strictly deprioritize `missing_pot` formats](https://github.com/yt-dlp/yt-dlp/commit/74fc2ae12c24eb6b4e02c6360c89bd05f3c8f740) ([#13061](https://github.com/yt-dlp/yt-dlp/issues/13061)) by [bashonly](https://github.com/bashonly)
|
|
||||||
- [Improve warning for SABR-only/SSAP player responses](https://github.com/yt-dlp/yt-dlp/commit/fd8394bc50301ac5e930aa65aa71ab1b8372b8ab) ([#13049](https://github.com/yt-dlp/yt-dlp/issues/13049)) by [bashonly](https://github.com/bashonly)
|
|
||||||
- tab: [Extract continuation from empty page](https://github.com/yt-dlp/yt-dlp/commit/72ba4879304c2082fecbb472e6cc05ee2d154a3b) ([#12938](https://github.com/yt-dlp/yt-dlp/issues/12938)) by [coletdjnz](https://github.com/coletdjnz)
|
|
||||||
- **zdf**: [Fix extractors](https://github.com/yt-dlp/yt-dlp/commit/7be14109a6bd493a2e881da4f9e30adaf3e7e5d5) ([#12779](https://github.com/yt-dlp/yt-dlp/issues/12779)) by [bashonly](https://github.com/bashonly), [InvalidUsernameException](https://github.com/InvalidUsernameException)
|
|
||||||
|
|
||||||
#### Downloader changes
|
|
||||||
- **niconicodmc**: [Remove downloader](https://github.com/yt-dlp/yt-dlp/commit/8d127b18f81131453eaba05d3bb810d9b73adb75) ([#12916](https://github.com/yt-dlp/yt-dlp/issues/12916)) by [doe1080](https://github.com/doe1080)
|
|
||||||
|
|
||||||
#### Networking changes
|
|
||||||
- [Add PATCH request shortcut](https://github.com/yt-dlp/yt-dlp/commit/ceab4d5ed63a1f135a1816fe967c9d9a1ec7e6e8) ([#12884](https://github.com/yt-dlp/yt-dlp/issues/12884)) by [doe1080](https://github.com/doe1080)
|
|
||||||
|
|
||||||
#### Misc. changes
|
|
||||||
- **ci**: [Add file mode test to code check](https://github.com/yt-dlp/yt-dlp/commit/3690e91265d1d0bbeffaf6a9b8cc9baded1367bd) ([#13036](https://github.com/yt-dlp/yt-dlp/issues/13036)) by [Grub4K](https://github.com/Grub4K)
|
|
||||||
- **cleanup**: Miscellaneous: [505b400](https://github.com/yt-dlp/yt-dlp/commit/505b400795af557bdcfd9d4fa7e9133b26ef431c) by [bashonly](https://github.com/bashonly), [seproDev](https://github.com/seproDev)
|
|
||||||
|
|
||||||
### 2025.03.31
|
|
||||||
|
|
||||||
#### Core changes
|
|
||||||
- [Add `--compat-options 2024`](https://github.com/yt-dlp/yt-dlp/commit/22e34adbd741e1c7072015debd615dc3fb71c401) ([#12789](https://github.com/yt-dlp/yt-dlp/issues/12789)) by [seproDev](https://github.com/seproDev)
|
|
||||||
|
|
||||||
#### Extractor changes
|
|
||||||
- **francaisfacile**: [Add extractor](https://github.com/yt-dlp/yt-dlp/commit/bb321cfdc3fd4400598ddb12a15862bc2ac8fc10) ([#12787](https://github.com/yt-dlp/yt-dlp/issues/12787)) by [mlabeeb03](https://github.com/mlabeeb03)
|
|
||||||
- **generic**: [Validate response before checking m3u8 live status](https://github.com/yt-dlp/yt-dlp/commit/9a1ec1d36e172d252714cef712a6d091e0a0c4f2) ([#12784](https://github.com/yt-dlp/yt-dlp/issues/12784)) by [bashonly](https://github.com/bashonly)
|
|
||||||
- **microsoftlearnepisode**: [Extract more formats](https://github.com/yt-dlp/yt-dlp/commit/d63696f23a341ee36a3237ccb5d5e14b34c2c579) ([#12799](https://github.com/yt-dlp/yt-dlp/issues/12799)) by [bashonly](https://github.com/bashonly)
|
|
||||||
- **mlbtv**: [Fix radio-only extraction](https://github.com/yt-dlp/yt-dlp/commit/f033d86b96b36f8c5289dd7c3304f42d4d9f6ff4) ([#12792](https://github.com/yt-dlp/yt-dlp/issues/12792)) by [bashonly](https://github.com/bashonly)
|
|
||||||
- **on24**: [Support `mainEvent` URLs](https://github.com/yt-dlp/yt-dlp/commit/e465b078ead75472fcb7b86f6ccaf2b5d3bc4c21) ([#12800](https://github.com/yt-dlp/yt-dlp/issues/12800)) by [bashonly](https://github.com/bashonly)
|
|
||||||
- **sbs**: [Fix subtitles extraction](https://github.com/yt-dlp/yt-dlp/commit/29560359120f28adaaac67c86fa8442eb72daa0d) ([#12785](https://github.com/yt-dlp/yt-dlp/issues/12785)) by [bashonly](https://github.com/bashonly)
|
|
||||||
- **stvr**: [Rename extractor from RTVS to STVR](https://github.com/yt-dlp/yt-dlp/commit/5fc521cbd0ce7b2410d0935369558838728e205d) ([#12788](https://github.com/yt-dlp/yt-dlp/issues/12788)) by [mireq](https://github.com/mireq)
|
|
||||||
- **twitch**: clips: [Extract portrait formats](https://github.com/yt-dlp/yt-dlp/commit/61046c31612b30c749cbdae934b7fe26abe659d7) ([#12763](https://github.com/yt-dlp/yt-dlp/issues/12763)) by [DmitryScaletta](https://github.com/DmitryScaletta)
|
|
||||||
- **youtube**
|
|
||||||
- [Add `player_js_variant` extractor-arg](https://github.com/yt-dlp/yt-dlp/commit/07f04005e40ebdb368920c511e36e98af0077ed3) ([#12767](https://github.com/yt-dlp/yt-dlp/issues/12767)) by [bashonly](https://github.com/bashonly)
|
|
||||||
- tab: [Fix playlist continuation extraction](https://github.com/yt-dlp/yt-dlp/commit/6a6d97b2cbc78f818de05cc96edcdcfd52caa259) ([#12777](https://github.com/yt-dlp/yt-dlp/issues/12777)) by [coletdjnz](https://github.com/coletdjnz)
|
|
||||||
|
|
||||||
#### Misc. changes
|
|
||||||
- **cleanup**: Miscellaneous: [5e457af](https://github.com/yt-dlp/yt-dlp/commit/5e457af57fae9645b1b8fa0ed689229c8fb9656b) by [bashonly](https://github.com/bashonly)
|
|
||||||
|
|
||||||
### 2025.03.27
|
|
||||||
|
|
||||||
#### Core changes
|
|
||||||
- **jsinterp**: [Fix nested attributes and object extraction](https://github.com/yt-dlp/yt-dlp/commit/a8b9ff3c2a0ae25735e580173becc78545b92572) ([#12760](https://github.com/yt-dlp/yt-dlp/issues/12760)) by [bashonly](https://github.com/bashonly), [seproDev](https://github.com/seproDev)
|
|
||||||
|
|
||||||
#### Extractor changes
|
|
||||||
- **youtube**: [Make signature and nsig extraction more robust](https://github.com/yt-dlp/yt-dlp/commit/48be862b32648bff5b3e553e40fca4dcc6e88b28) ([#12761](https://github.com/yt-dlp/yt-dlp/issues/12761)) by [bashonly](https://github.com/bashonly), [seproDev](https://github.com/seproDev)
|
|
||||||
|
|
||||||
### 2025.03.26
|
|
||||||
|
|
||||||
#### Extractor changes
|
|
||||||
- **youtube**
|
|
||||||
- [Fix signature and nsig extraction for player `4fcd6e4a`](https://github.com/yt-dlp/yt-dlp/commit/a550dfc904a02843a26369ae50dbb7c0febfb30e) ([#12748](https://github.com/yt-dlp/yt-dlp/issues/12748)) by [seproDev](https://github.com/seproDev)
|
|
||||||
- [Only cache nsig code on successful decoding](https://github.com/yt-dlp/yt-dlp/commit/ecee97b4fa90d51c48f9154c3a6d5a8ffe46cd5c) ([#12750](https://github.com/yt-dlp/yt-dlp/issues/12750)) by [bashonly](https://github.com/bashonly), [seproDev](https://github.com/seproDev)
|
|
||||||
|
|
||||||
### 2025.03.25
|
|
||||||
|
|
||||||
#### Core changes
|
|
||||||
- [Fix attribute error on failed VT init](https://github.com/yt-dlp/yt-dlp/commit/b872ffec50fd50f790a5a490e006a369a28a3df3) ([#12696](https://github.com/yt-dlp/yt-dlp/issues/12696)) by [Grub4K](https://github.com/Grub4K)
|
|
||||||
- **utils**: `js_to_json`: [Make function less fatal](https://github.com/yt-dlp/yt-dlp/commit/9491b44032b330e05bd5eaa546187005d1e8538e) ([#12715](https://github.com/yt-dlp/yt-dlp/issues/12715)) by [seproDev](https://github.com/seproDev)
|
|
||||||
|
|
||||||
#### Extractor changes
|
|
||||||
- [Fix sorting of HLS audio formats by `GROUP-ID`](https://github.com/yt-dlp/yt-dlp/commit/86ab79e1a5182092321102adf6ca34195803b878) ([#12714](https://github.com/yt-dlp/yt-dlp/issues/12714)) by [bashonly](https://github.com/bashonly)
|
|
||||||
- **17live**: vod: [Add extractor](https://github.com/yt-dlp/yt-dlp/commit/3396eb50dcd245b49c0f4aecd6e80ec914095d16) ([#12723](https://github.com/yt-dlp/yt-dlp/issues/12723)) by [subrat-lima](https://github.com/subrat-lima)
|
|
||||||
- **9now.com.au**: [Fix extractor](https://github.com/yt-dlp/yt-dlp/commit/9d5e6de2e7a47226d1f72c713ad45c88ba01db68) ([#12702](https://github.com/yt-dlp/yt-dlp/issues/12702)) by [bashonly](https://github.com/bashonly)
|
|
||||||
- **chzzk**: video: [Fix extraction](https://github.com/yt-dlp/yt-dlp/commit/e2dfccaf808b406d5bcb7dd04ae9ce420752dd6f) ([#12692](https://github.com/yt-dlp/yt-dlp/issues/12692)) by [bashonly](https://github.com/bashonly), [dirkf](https://github.com/dirkf)
|
|
||||||
- **deezer**: [Remove extractors](https://github.com/yt-dlp/yt-dlp/commit/be5af3f9e91747768c2b41157851bfbe14c663f7) ([#12704](https://github.com/yt-dlp/yt-dlp/issues/12704)) by [seproDev](https://github.com/seproDev)
|
|
||||||
- **generic**: [Fix MPD base URL parsing](https://github.com/yt-dlp/yt-dlp/commit/5086d4aed6aeb3908c62f49e2d8f74cc0cb05110) ([#12718](https://github.com/yt-dlp/yt-dlp/issues/12718)) by [fireattack](https://github.com/fireattack)
|
|
||||||
- **streaks**: [Add extractor](https://github.com/yt-dlp/yt-dlp/commit/801afeac91f97dc0b58cd39cc7e8c50f619dc4e1) ([#12679](https://github.com/yt-dlp/yt-dlp/issues/12679)) by [doe1080](https://github.com/doe1080)
|
|
||||||
- **tver**: [Fix extractor](https://github.com/yt-dlp/yt-dlp/commit/66e0bab814e4a52ef3e12d81123ad992a29df50e) ([#12659](https://github.com/yt-dlp/yt-dlp/issues/12659)) by [arabcoders](https://github.com/arabcoders), [bashonly](https://github.com/bashonly)
|
|
||||||
- **viki**: [Remove extractors](https://github.com/yt-dlp/yt-dlp/commit/fe4f14b8369038e7c58f7de546d76de1ce3a91ce) ([#12703](https://github.com/yt-dlp/yt-dlp/issues/12703)) by [seproDev](https://github.com/seproDev)
|
|
||||||
- **vrsquare**: [Add extractors](https://github.com/yt-dlp/yt-dlp/commit/b7fbb5a0a16a8e8d3e29c29e26ebed677d0d6ea3) ([#12515](https://github.com/yt-dlp/yt-dlp/issues/12515)) by [doe1080](https://github.com/doe1080)
|
|
||||||
- **youtube**
|
|
||||||
- [Fix PhantomJS nsig fallback](https://github.com/yt-dlp/yt-dlp/commit/4054a2b623bd1e277b49d2e9abc3d112a4b1c7be) ([#12728](https://github.com/yt-dlp/yt-dlp/issues/12728)) by [bashonly](https://github.com/bashonly)
|
|
||||||
- [Fix signature and nsig extraction for player `363db69b`](https://github.com/yt-dlp/yt-dlp/commit/b9c979461b244713bf42691a5bc02834e2ba4b2c) ([#12725](https://github.com/yt-dlp/yt-dlp/issues/12725)) by [bashonly](https://github.com/bashonly)
|
|
||||||
|
|
||||||
#### Networking changes
|
|
||||||
- **Request Handler**: curl_cffi: [Support `curl_cffi` 0.10.x](https://github.com/yt-dlp/yt-dlp/commit/9bf23902ceb948b9685ce1dab575491571720fc6) ([#12670](https://github.com/yt-dlp/yt-dlp/issues/12670)) by [Grub4K](https://github.com/Grub4K)
|
|
||||||
|
|
||||||
#### Misc. changes
|
|
||||||
- **cleanup**: Miscellaneous: [9dde546](https://github.com/yt-dlp/yt-dlp/commit/9dde546e7ee3e1515d88ee3af08b099351455dc0) by [seproDev](https://github.com/seproDev)
|
|
||||||
|
|
||||||
### 2025.03.21
|
|
||||||
|
|
||||||
#### Core changes
|
|
||||||
- [Fix external downloader availability when using `--ffmpeg-location`](https://github.com/yt-dlp/yt-dlp/commit/9f77e04c76e36e1cbbf49bc9eb385fa6ef804b67) ([#12318](https://github.com/yt-dlp/yt-dlp/issues/12318)) by [Kenshin9977](https://github.com/Kenshin9977)
|
|
||||||
- [Load plugins on demand](https://github.com/yt-dlp/yt-dlp/commit/4445f37a7a66b248dbd8376c43137e6e441f138e) ([#11305](https://github.com/yt-dlp/yt-dlp/issues/11305)) by [coletdjnz](https://github.com/coletdjnz), [Grub4K](https://github.com/Grub4K), [pukkandan](https://github.com/pukkandan) (With fixes in [c034d65](https://github.com/yt-dlp/yt-dlp/commit/c034d655487be668222ef9476a16f374584e49a7))
|
|
||||||
- [Support emitting ConEmu progress codes](https://github.com/yt-dlp/yt-dlp/commit/f7a1f2d8132967a62b0f6d5665c6d2dde2d42c09) ([#10649](https://github.com/yt-dlp/yt-dlp/issues/10649)) by [Grub4K](https://github.com/Grub4K)
|
|
||||||
|
|
||||||
#### Extractor changes
|
|
||||||
- **azmedien**: [Fix extractor](https://github.com/yt-dlp/yt-dlp/commit/26a502fc727d0e91b2db6bf4a112823bcc672e85) ([#12375](https://github.com/yt-dlp/yt-dlp/issues/12375)) by [goggle](https://github.com/goggle)
|
|
||||||
- **bilibiliplaylist**: [Fix extractor](https://github.com/yt-dlp/yt-dlp/commit/f5fb2229e66cf59d5bf16065bc041b42a28354a0) ([#12690](https://github.com/yt-dlp/yt-dlp/issues/12690)) by [bashonly](https://github.com/bashonly)
|
|
||||||
- **bunnycdn**: [Add extractor](https://github.com/yt-dlp/yt-dlp/commit/3a1583ca75fb523cbad0e5e174387ea7b477d175) ([#11586](https://github.com/yt-dlp/yt-dlp/issues/11586)) by [Grub4K](https://github.com/Grub4K), [seproDev](https://github.com/seproDev)
|
|
||||||
- **canalsurmas**: [Add extractor](https://github.com/yt-dlp/yt-dlp/commit/01a8be4c23f186329d85f9c78db34a55f3294ac5) ([#12497](https://github.com/yt-dlp/yt-dlp/issues/12497)) by [Arc8ne](https://github.com/Arc8ne)
|
|
||||||
- **cda**: [Fix login support](https://github.com/yt-dlp/yt-dlp/commit/be0d819e1103195043f6743650781f0d4d343f6d) ([#12552](https://github.com/yt-dlp/yt-dlp/issues/12552)) by [rysson](https://github.com/rysson)
|
|
||||||
- **cultureunplugged**: [Extend `_VALID_URL`](https://github.com/yt-dlp/yt-dlp/commit/3042afb5fe342d3a00de76704cd7de611acc350e) ([#12486](https://github.com/yt-dlp/yt-dlp/issues/12486)) by [seproDev](https://github.com/seproDev)
|
|
||||||
- **dailymotion**: [Improve embed detection](https://github.com/yt-dlp/yt-dlp/commit/ad60137c141efa5023fbc0ac8579eaefe8b3d8cc) ([#12464](https://github.com/yt-dlp/yt-dlp/issues/12464)) by [seproDev](https://github.com/seproDev)
|
|
||||||
- **gem.cbc.ca**: [Fix login support](https://github.com/yt-dlp/yt-dlp/commit/eb1417786a3027b1e7290ec37ef6aaece50ebed0) ([#12414](https://github.com/yt-dlp/yt-dlp/issues/12414)) by [bashonly](https://github.com/bashonly)
|
|
||||||
- **globo**: [Fix subtitles extraction](https://github.com/yt-dlp/yt-dlp/commit/0e1697232fcbba7551f983fd1ba93bb445cbb08b) ([#12270](https://github.com/yt-dlp/yt-dlp/issues/12270)) by [pedro](https://github.com/pedro)
|
|
||||||
- **instagram**
|
|
||||||
- [Add `app_id` extractor-arg](https://github.com/yt-dlp/yt-dlp/commit/a90641c8363fa0c10800b36eb6b01ee22d3a9409) ([#12359](https://github.com/yt-dlp/yt-dlp/issues/12359)) by [chrisellsworth](https://github.com/chrisellsworth)
|
|
||||||
- [Fix extraction of older private posts](https://github.com/yt-dlp/yt-dlp/commit/a59abe0636dc49b22a67246afe35613571b86f05) ([#12451](https://github.com/yt-dlp/yt-dlp/issues/12451)) by [bashonly](https://github.com/bashonly)
|
|
||||||
- [Improve error handling](https://github.com/yt-dlp/yt-dlp/commit/480125560a3b9972d29ae0da850aba8109e6bd41) ([#12410](https://github.com/yt-dlp/yt-dlp/issues/12410)) by [bashonly](https://github.com/bashonly)
|
|
||||||
- story: [Support `--no-playlist`](https://github.com/yt-dlp/yt-dlp/commit/65c3c58c0a67463a150920203cec929045c95a24) ([#12397](https://github.com/yt-dlp/yt-dlp/issues/12397)) by [fireattack](https://github.com/fireattack)
|
|
||||||
- **jamendo**: [Fix thumbnail extraction](https://github.com/yt-dlp/yt-dlp/commit/89a68c4857ddbaf937ff22f12648baaf6b5af840) ([#12622](https://github.com/yt-dlp/yt-dlp/issues/12622)) by [bashonly](https://github.com/bashonly), [JChris246](https://github.com/JChris246)
|
|
||||||
- **ketnet**: [Remove extractor](https://github.com/yt-dlp/yt-dlp/commit/bbada3ec0779422cde34f1ce3dcf595da463b493) ([#12628](https://github.com/yt-dlp/yt-dlp/issues/12628)) by [MichaelDeBoey](https://github.com/MichaelDeBoey)
|
|
||||||
- **lbry**
|
|
||||||
- [Make m3u8 format extraction non-fatal](https://github.com/yt-dlp/yt-dlp/commit/9807181cfbf87bfa732f415c30412bdbd77cbf81) ([#12463](https://github.com/yt-dlp/yt-dlp/issues/12463)) by [bashonly](https://github.com/bashonly)
|
|
||||||
- [Raise appropriate error for non-media files](https://github.com/yt-dlp/yt-dlp/commit/7126b472601814b7fd8c9de02069e8fff1764891) ([#12462](https://github.com/yt-dlp/yt-dlp/issues/12462)) by [bashonly](https://github.com/bashonly)
|
|
||||||
- **loco**: [Add extractor](https://github.com/yt-dlp/yt-dlp/commit/983095485c731240aae27c950cb8c24a50827b56) ([#12667](https://github.com/yt-dlp/yt-dlp/issues/12667)) by [DTrombett](https://github.com/DTrombett)
|
|
||||||
- **magellantv**: [Fix extractor](https://github.com/yt-dlp/yt-dlp/commit/172d5fcd778bf2605db7647ebc56b29ed18d24ac) ([#12505](https://github.com/yt-dlp/yt-dlp/issues/12505)) by [seproDev](https://github.com/seproDev)
|
|
||||||
- **mitele**: [Fix extractor](https://github.com/yt-dlp/yt-dlp/commit/7223d29569a48a35ad132a508c115973866838d3) ([#12689](https://github.com/yt-dlp/yt-dlp/issues/12689)) by [bashonly](https://github.com/bashonly)
|
|
||||||
- **msn**: [Rework extractor](https://github.com/yt-dlp/yt-dlp/commit/4815dac131d42c51e12c1d05232db0bbbf607329) ([#12513](https://github.com/yt-dlp/yt-dlp/issues/12513)) by [seproDev](https://github.com/seproDev), [thedenv](https://github.com/thedenv)
|
|
||||||
- **n1**: [Fix extraction of newer articles](https://github.com/yt-dlp/yt-dlp/commit/9d70abe4de401175cbbaaa36017806f16b2df9af) ([#12514](https://github.com/yt-dlp/yt-dlp/issues/12514)) by [u-spec-png](https://github.com/u-spec-png)
|
|
||||||
- **nbcstations**: [Fix extractor](https://github.com/yt-dlp/yt-dlp/commit/ebac65aa9e0bf9a97c24d00f7977900d2577364b) ([#12534](https://github.com/yt-dlp/yt-dlp/issues/12534)) by [refack](https://github.com/refack)
|
|
||||||
- **niconico**
|
|
||||||
- [Fix format sorting](https://github.com/yt-dlp/yt-dlp/commit/7508e34f203e97389f1d04db92140b13401dd724) ([#12442](https://github.com/yt-dlp/yt-dlp/issues/12442)) by [xpadev-net](https://github.com/xpadev-net)
|
|
||||||
- live: [Fix thumbnail extraction](https://github.com/yt-dlp/yt-dlp/commit/c2e6e1d5f77f3b720a6266f2869eb750d20e5dc1) ([#12419](https://github.com/yt-dlp/yt-dlp/issues/12419)) by [bashonly](https://github.com/bashonly)
|
|
||||||
- **openrec**: [Fix `_VALID_URL`](https://github.com/yt-dlp/yt-dlp/commit/17504f253564cfad86244de2b6346d07d2300ca5) ([#12608](https://github.com/yt-dlp/yt-dlp/issues/12608)) by [fireattack](https://github.com/fireattack)
|
|
||||||
- **pinterest**: [Fix extractor](https://github.com/yt-dlp/yt-dlp/commit/bd0a66816934de70312eea1e71c59c13b401dc3a) ([#12538](https://github.com/yt-dlp/yt-dlp/issues/12538)) by [mikf](https://github.com/mikf)
|
|
||||||
- **playsuisse**: [Fix login support](https://github.com/yt-dlp/yt-dlp/commit/6933f5670cea9c3e2fb16c1caa1eda54d13122c5) ([#12444](https://github.com/yt-dlp/yt-dlp/issues/12444)) by [bashonly](https://github.com/bashonly)
|
|
||||||
- **reddit**: [Truncate title](https://github.com/yt-dlp/yt-dlp/commit/d9a53cc1e6fd912daf500ca4f19e9ca88994dbf9) ([#12567](https://github.com/yt-dlp/yt-dlp/issues/12567)) by [seproDev](https://github.com/seproDev)
|
|
||||||
- **rtp**: [Rework extractor](https://github.com/yt-dlp/yt-dlp/commit/8eb9c1bf3b9908cca22ef043602aa24fb9f352c6) ([#11638](https://github.com/yt-dlp/yt-dlp/issues/11638)) by [pferreir](https://github.com/pferreir), [red-acid](https://github.com/red-acid), [seproDev](https://github.com/seproDev), [somini](https://github.com/somini), [vallovic](https://github.com/vallovic)
|
|
||||||
- **softwhiteunderbelly**: [Add extractor](https://github.com/yt-dlp/yt-dlp/commit/652827d5a076c9483c36654ad2cf3fe46219baf4) ([#12281](https://github.com/yt-dlp/yt-dlp/issues/12281)) by [benfaerber](https://github.com/benfaerber)
|
|
||||||
- **soop**: [Fix timestamp extraction](https://github.com/yt-dlp/yt-dlp/commit/8305df00012ff8138a6ff95279d06b54ac607f63) ([#12609](https://github.com/yt-dlp/yt-dlp/issues/12609)) by [msikma](https://github.com/msikma)
|
|
||||||
- **soundcloud**
|
|
||||||
- [Extract tags](https://github.com/yt-dlp/yt-dlp/commit/9deed13d7cce6d3647379e50589c92de89227509) ([#12420](https://github.com/yt-dlp/yt-dlp/issues/12420)) by [bashonly](https://github.com/bashonly)
|
|
||||||
- [Fix thumbnail extraction](https://github.com/yt-dlp/yt-dlp/commit/6deeda5c11f34f613724fa0627879f0d607ba1b4) ([#12447](https://github.com/yt-dlp/yt-dlp/issues/12447)) by [bashonly](https://github.com/bashonly)
|
|
||||||
- **tiktok**
|
|
||||||
- [Improve error handling](https://github.com/yt-dlp/yt-dlp/commit/99ea2978757a431eeb2a265b3395ccbe4ce202cf) ([#12445](https://github.com/yt-dlp/yt-dlp/issues/12445)) by [bashonly](https://github.com/bashonly)
|
|
||||||
- [Truncate title](https://github.com/yt-dlp/yt-dlp/commit/83b119dadb0f267f1fb66bf7ed74c097349de79e) ([#12566](https://github.com/yt-dlp/yt-dlp/issues/12566)) by [seproDev](https://github.com/seproDev)
|
|
||||||
- **tv8.it**: [Add live and playlist extractors](https://github.com/yt-dlp/yt-dlp/commit/2ee3a0aff9be2be3bea60640d3d8a0febaf0acb6) ([#12569](https://github.com/yt-dlp/yt-dlp/issues/12569)) by [DTrombett](https://github.com/DTrombett)
|
|
||||||
- **tvw**: [Add extractor](https://github.com/yt-dlp/yt-dlp/commit/42b7440963866e31ff84a5b89030d1c596fa2e6e) ([#12271](https://github.com/yt-dlp/yt-dlp/issues/12271)) by [fries1234](https://github.com/fries1234)
|
|
||||||
- **twitter**
|
|
||||||
- [Fix syndication token generation](https://github.com/yt-dlp/yt-dlp/commit/b8b47547049f5ebc3dd680fc7de70ed0ca9c0d70) ([#12537](https://github.com/yt-dlp/yt-dlp/issues/12537)) by [bashonly](https://github.com/bashonly)
|
|
||||||
- [Truncate title](https://github.com/yt-dlp/yt-dlp/commit/06f6de78db2eceeabd062ab1a3023e0ff9d4df53) ([#12560](https://github.com/yt-dlp/yt-dlp/issues/12560)) by [seproDev](https://github.com/seproDev)
|
|
||||||
- **vk**: [Improve metadata extraction](https://github.com/yt-dlp/yt-dlp/commit/05c8023a27dd37c49163c0498bf98e3e3c1cb4b9) ([#12510](https://github.com/yt-dlp/yt-dlp/issues/12510)) by [seproDev](https://github.com/seproDev)
|
|
||||||
- **vrtmax**: [Rework extractor](https://github.com/yt-dlp/yt-dlp/commit/df9ebeec00d658693252978d1ffb885e67aa6ab6) ([#12479](https://github.com/yt-dlp/yt-dlp/issues/12479)) by [bergoid](https://github.com/bergoid), [MichaelDeBoey](https://github.com/MichaelDeBoey), [seproDev](https://github.com/seproDev)
|
|
||||||
- **weibo**: [Support playlists](https://github.com/yt-dlp/yt-dlp/commit/0bb39788626002a8a67e925580227952c563c8b9) ([#12284](https://github.com/yt-dlp/yt-dlp/issues/12284)) by [4ft35t](https://github.com/4ft35t)
|
|
||||||
- **wsj**: [Support opinion URLs and impersonation](https://github.com/yt-dlp/yt-dlp/commit/7f3006eb0c0659982bb956d71b0bc806bcb0a5f2) ([#12431](https://github.com/yt-dlp/yt-dlp/issues/12431)) by [refack](https://github.com/refack)
|
|
||||||
- **youtube**
|
|
||||||
- [Fix nsig and signature extraction for player `643afba4`](https://github.com/yt-dlp/yt-dlp/commit/9b868518a15599f3d7ef5a1c730dda164c30da9b) ([#12684](https://github.com/yt-dlp/yt-dlp/issues/12684)) by [bashonly](https://github.com/bashonly), [seproDev](https://github.com/seproDev)
|
|
||||||
- [Player client maintenance](https://github.com/yt-dlp/yt-dlp/commit/3380febe9984c21c79c3147c1d390a4cf339bc4c) ([#12603](https://github.com/yt-dlp/yt-dlp/issues/12603)) by [seproDev](https://github.com/seproDev)
|
|
||||||
- [Split into package](https://github.com/yt-dlp/yt-dlp/commit/4432a9390c79253ac830702b226d2e558b636725) ([#12557](https://github.com/yt-dlp/yt-dlp/issues/12557)) by [coletdjnz](https://github.com/coletdjnz)
|
|
||||||
- [Warn on DRM formats](https://github.com/yt-dlp/yt-dlp/commit/e67d786c7cc87bd449d22e0ddef08306891c1173) ([#12593](https://github.com/yt-dlp/yt-dlp/issues/12593)) by [coletdjnz](https://github.com/coletdjnz)
|
|
||||||
- [Warn on missing formats due to SSAP](https://github.com/yt-dlp/yt-dlp/commit/79ec2fdff75c8c1bb89b550266849ad4dec48dd3) ([#12483](https://github.com/yt-dlp/yt-dlp/issues/12483)) by [coletdjnz](https://github.com/coletdjnz)
|
|
||||||
|
|
||||||
#### Networking changes
|
|
||||||
- [Add `keep_header_casing` extension](https://github.com/yt-dlp/yt-dlp/commit/7d18fed8f1983fe6de4ddc810dfb2761ba5744ac) ([#11652](https://github.com/yt-dlp/yt-dlp/issues/11652)) by [coletdjnz](https://github.com/coletdjnz), [Grub4K](https://github.com/Grub4K)
|
|
||||||
- [Always add unsupported suffix on version mismatch](https://github.com/yt-dlp/yt-dlp/commit/95f8df2f796d0048119615200758199aedcd7cf4) ([#12626](https://github.com/yt-dlp/yt-dlp/issues/12626)) by [Grub4K](https://github.com/Grub4K)
|
|
||||||
|
|
||||||
#### Misc. changes
|
|
||||||
- **cleanup**: Miscellaneous: [f36e4b6](https://github.com/yt-dlp/yt-dlp/commit/f36e4b6e65cb8403791aae2f520697115cb88dec) by [dirkf](https://github.com/dirkf), [gamer191](https://github.com/gamer191), [Grub4K](https://github.com/Grub4K), [seproDev](https://github.com/seproDev)
|
|
||||||
- **test**: [Show all differences for `expect_value` and `expect_dict`](https://github.com/yt-dlp/yt-dlp/commit/a3e0c7d3b267abdf3933b709704a28d43bb46503) ([#12334](https://github.com/yt-dlp/yt-dlp/issues/12334)) by [Grub4K](https://github.com/Grub4K)
|
|
||||||
|
|
||||||
### 2025.02.19
|
### 2025.02.19
|
||||||
|
|
||||||
#### Core changes
|
#### Core changes
|
||||||
|
55
README.md
55
README.md
@ -337,11 +337,10 @@ If you fork the project on GitHub, you can run your fork's [build workflow](.git
|
|||||||
--plugin-dirs PATH Path to an additional directory to search
|
--plugin-dirs PATH Path to an additional directory to search
|
||||||
for plugins. This option can be used
|
for plugins. This option can be used
|
||||||
multiple times to add multiple directories.
|
multiple times to add multiple directories.
|
||||||
Use "default" to search the default plugin
|
Note that this currently only works for
|
||||||
directories (default)
|
extractor plugins; postprocessor plugins can
|
||||||
--no-plugin-dirs Clear plugin directories to search,
|
only be loaded from the default plugin
|
||||||
including defaults and those provided by
|
directories
|
||||||
previous --plugin-dirs
|
|
||||||
--flat-playlist Do not extract a playlist's URL result
|
--flat-playlist Do not extract a playlist's URL result
|
||||||
entries; some entry metadata may be missing
|
entries; some entry metadata may be missing
|
||||||
and downloading may be bypassed
|
and downloading may be bypassed
|
||||||
@ -386,12 +385,6 @@ If you fork the project on GitHub, you can run your fork's [build workflow](.git
|
|||||||
recursive options. As a safety measure, each
|
recursive options. As a safety measure, each
|
||||||
alias may be triggered a maximum of 100
|
alias may be triggered a maximum of 100
|
||||||
times. This option can be used multiple times
|
times. This option can be used multiple times
|
||||||
-t, --preset-alias PRESET Applies a predefined set of options. e.g.
|
|
||||||
--preset-alias mp3. The following presets
|
|
||||||
are available: mp3, aac, mp4, mkv, sleep.
|
|
||||||
See the "Preset Aliases" section at the end
|
|
||||||
for more info. This option can be used
|
|
||||||
multiple times
|
|
||||||
|
|
||||||
## Network Options:
|
## Network Options:
|
||||||
--proxy URL Use the specified HTTP/HTTPS/SOCKS proxy. To
|
--proxy URL Use the specified HTTP/HTTPS/SOCKS proxy. To
|
||||||
@ -1104,23 +1097,6 @@ Make chapter entries for, or remove various segments (sponsor,
|
|||||||
can use this option multiple times to give
|
can use this option multiple times to give
|
||||||
arguments for different extractors
|
arguments for different extractors
|
||||||
|
|
||||||
## Preset Aliases:
|
|
||||||
-t mp3 -f 'ba[acodec^=mp3]/ba/b' -x --audio-format
|
|
||||||
mp3
|
|
||||||
|
|
||||||
-t aac -f
|
|
||||||
'ba[acodec^=aac]/ba[acodec^=mp4a.40.]/ba/b'
|
|
||||||
-x --audio-format aac
|
|
||||||
|
|
||||||
-t mp4 --merge-output-format mp4 --remux-video mp4
|
|
||||||
-S vcodec:h264,lang,quality,res,fps,hdr:12,a
|
|
||||||
codec:aac
|
|
||||||
|
|
||||||
-t mkv --merge-output-format mkv --remux-video mkv
|
|
||||||
|
|
||||||
-t sleep --sleep-subtitles 5 --sleep-requests 0.75
|
|
||||||
--sleep-interval 10 --max-sleep-interval 20
|
|
||||||
|
|
||||||
# CONFIGURATION
|
# CONFIGURATION
|
||||||
|
|
||||||
You can configure yt-dlp by placing any supported command line option in a configuration file. The configuration is loaded from the following locations:
|
You can configure yt-dlp by placing any supported command line option in a configuration file. The configuration is loaded from the following locations:
|
||||||
@ -1792,8 +1768,8 @@ The following extractors use this feature:
|
|||||||
#### youtube
|
#### youtube
|
||||||
* `lang`: Prefer translated metadata (`title`, `description` etc) of this language code (case-sensitive). By default, the video primary language metadata is preferred, with a fallback to `en` translated. See [youtube.py](https://github.com/yt-dlp/yt-dlp/blob/c26f9b991a0681fd3ea548d535919cec1fbbd430/yt_dlp/extractor/youtube.py#L381-L390) for list of supported content language codes
|
* `lang`: Prefer translated metadata (`title`, `description` etc) of this language code (case-sensitive). By default, the video primary language metadata is preferred, with a fallback to `en` translated. See [youtube.py](https://github.com/yt-dlp/yt-dlp/blob/c26f9b991a0681fd3ea548d535919cec1fbbd430/yt_dlp/extractor/youtube.py#L381-L390) for list of supported content language codes
|
||||||
* `skip`: One or more of `hls`, `dash` or `translated_subs` to skip extraction of the m3u8 manifests, dash manifests and [auto-translated subtitles](https://github.com/yt-dlp/yt-dlp/issues/4090#issuecomment-1158102032) respectively
|
* `skip`: One or more of `hls`, `dash` or `translated_subs` to skip extraction of the m3u8 manifests, dash manifests and [auto-translated subtitles](https://github.com/yt-dlp/yt-dlp/issues/4090#issuecomment-1158102032) respectively
|
||||||
* `player_client`: Clients to extract video data from. The currently available clients are `web`, `web_safari`, `web_embedded`, `web_music`, `web_creator`, `mweb`, `ios`, `android`, `android_vr`, `tv` and `tv_embedded`. By default, `tv,ios,web` is used, or `tv,web` is used when authenticating with cookies. The `web_music` client is added for `music.youtube.com` URLs when logged-in cookies are used. The `web_embedded` client is added for age-restricted videos but only works if the video is embeddable. The `tv_embedded` and `web_creator` clients are added for age-restricted videos if account age-verification is required. Some clients, such as `web` and `web_music`, require a `po_token` for their formats to be downloadable. Some clients, such as `web_creator`, will only work with authentication. Not all clients support authentication via cookies. You can use `default` for the default clients, or you can use `all` for all clients (not recommended). You can prefix a client with `-` to exclude it, e.g. `youtube:player_client=default,-ios`
|
* `player_client`: Clients to extract video data from. The main clients are `web`, `ios` and `android`, with variants `_music` and `_creator` (e.g. `ios_creator`); and `mweb`, `android_vr`, `web_safari`, `web_embedded`, `tv` and `tv_embedded` with no variants. By default, `tv,ios,web` is used, or `tv,web` is used when authenticating with cookies. The `web_music` client is added for `music.youtube.com` URLs when logged-in cookies are used. The `tv_embedded` and `web_creator` clients are added for age-restricted videos if account age-verification is required. Some clients, such as `web` and `web_music`, require a `po_token` for their formats to be downloadable. Some clients, such as the `_creator` variants, will only work with authentication. Not all clients support authentication via cookies. You can use `default` for the default clients, or you can use `all` for all clients (not recommended). You can prefix a client with `-` to exclude it, e.g. `youtube:player_client=default,-ios`
|
||||||
* `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), `initial_data` (skip initial data/next ep request). While these options can help reduce the number of requests needed or avoid some rate-limiting, they could cause issues such as missing formats or metadata. See [#860](https://github.com/yt-dlp/yt-dlp/pull/860) and [#12826](https://github.com/yt-dlp/yt-dlp/issues/12826) 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
|
||||||
* `player_params`: YouTube player parameters to use for player requests. Will overwrite any default ones set by yt-dlp.
|
* `player_params`: YouTube player parameters to use for player requests. Will overwrite any default ones set by yt-dlp.
|
||||||
* `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)
|
||||||
* `max_comments`: Limit the amount of comments to gather. Comma-separated list of integers representing `max-comments,max-parents,max-replies,max-replies-per-thread`. Default is `all,all,all,all`
|
* `max_comments`: Limit the amount of comments to gather. Comma-separated list of integers representing `max-comments,max-parents,max-replies,max-replies-per-thread`. Default is `all,all,all,all`
|
||||||
@ -1805,7 +1781,6 @@ The following extractors use this feature:
|
|||||||
* `data_sync_id`: Overrides the account Data Sync ID used in Innertube API requests. This may be needed if you are using an account with `youtube:player_skip=webpage,configs` or `youtubetab:skip=webpage`
|
* `data_sync_id`: Overrides the account Data Sync ID used in Innertube API requests. This may be needed if you are using an account with `youtube:player_skip=webpage,configs` or `youtubetab:skip=webpage`
|
||||||
* `visitor_data`: Overrides the Visitor Data used in Innertube API requests. This should be used with `player_skip=webpage,configs` and without cookies. Note: this may have adverse effects if used improperly. If a session from a browser is wanted, you should pass cookies instead (which contain the Visitor ID)
|
* `visitor_data`: Overrides the Visitor Data used in Innertube API requests. This should be used with `player_skip=webpage,configs` and without cookies. Note: this may have adverse effects if used improperly. If a session from a browser is wanted, you should pass cookies instead (which contain the Visitor ID)
|
||||||
* `po_token`: Proof of Origin (PO) Token(s) to use. Comma seperated list of PO Tokens in the format `CLIENT.CONTEXT+PO_TOKEN`, e.g. `youtube:po_token=web.gvs+XXX,web.player=XXX,web_safari.gvs+YYY`. Context can be either `gvs` (Google Video Server URLs) or `player` (Innertube player request)
|
* `po_token`: Proof of Origin (PO) Token(s) to use. Comma seperated list of PO Tokens in the format `CLIENT.CONTEXT+PO_TOKEN`, e.g. `youtube:po_token=web.gvs+XXX,web.player=XXX,web_safari.gvs+YYY`. Context can be either `gvs` (Google Video Server URLs) or `player` (Innertube player request)
|
||||||
* `player_js_variant`: The player javascript variant to use for signature and nsig deciphering. The known variants are: `main`, `tce`, `tv`, `tv_es6`, `phone`, `tablet`. Only `main` is recommended as a possible workaround; the others are for debugging purposes. The default is to use what is prescribed by the site, and can be selected with `actual`
|
|
||||||
|
|
||||||
#### youtubetab (YouTube playlists, channels, feeds, etc.)
|
#### youtubetab (YouTube playlists, channels, feeds, etc.)
|
||||||
* `skip`: One or more of `webpage` (skip initial webpage download), `authcheck` (allow the download of playlists requiring authentication when no initial webpage is downloaded. This may cause unwanted behavior, see [#1122](https://github.com/yt-dlp/yt-dlp/pull/1122) for more details)
|
* `skip`: One or more of `webpage` (skip initial webpage download), `authcheck` (allow the download of playlists requiring authentication when no initial webpage is downloaded. This may cause unwanted behavior, see [#1122](https://github.com/yt-dlp/yt-dlp/pull/1122) for more details)
|
||||||
@ -1822,6 +1797,9 @@ The following extractors use this feature:
|
|||||||
#### vikichannel
|
#### vikichannel
|
||||||
* `video_types`: Types of videos to download - one or more of `episodes`, `movies`, `clips`, `trailers`
|
* `video_types`: Types of videos to download - one or more of `episodes`, `movies`, `clips`, `trailers`
|
||||||
|
|
||||||
|
#### niconico
|
||||||
|
* `segment_duration`: Segment duration in milliseconds for HLS-DMC formats. Use it at your own risk since this feature **may result in your account termination.**
|
||||||
|
|
||||||
#### youtubewebarchive
|
#### youtubewebarchive
|
||||||
* `check_all`: Try to check more at the cost of more requests. One or more of `thumbnails`, `captures`
|
* `check_all`: Try to check more at the cost of more requests. One or more of `thumbnails`, `captures`
|
||||||
|
|
||||||
@ -1833,9 +1811,6 @@ The following extractors use this feature:
|
|||||||
* `vcodec`: vcodec to ignore - one or more of `h264`, `h265`, `dvh265`
|
* `vcodec`: vcodec to ignore - one or more of `h264`, `h265`, `dvh265`
|
||||||
* `dr`: dynamic range to ignore - one or more of `sdr`, `hdr10`, `dv`
|
* `dr`: dynamic range to ignore - one or more of `sdr`, `hdr10`, `dv`
|
||||||
|
|
||||||
#### instagram
|
|
||||||
* `app_id`: The value of the `X-IG-App-ID` header used for API requests. Default is the web app ID, `936619743392459`
|
|
||||||
|
|
||||||
#### niconicochannelplus
|
#### niconicochannelplus
|
||||||
* `max_comments`: Maximum number of comments to extract - default is `120`
|
* `max_comments`: Maximum number of comments to extract - default is `120`
|
||||||
|
|
||||||
@ -1887,9 +1862,6 @@ The following extractors use this feature:
|
|||||||
#### sonylivseries
|
#### sonylivseries
|
||||||
* `sort_order`: Episode sort order for series extraction - one of `asc` (ascending, oldest first) or `desc` (descending, newest first). Default is `asc`
|
* `sort_order`: Episode sort order for series extraction - one of `asc` (ascending, oldest first) or `desc` (descending, newest first). Default is `asc`
|
||||||
|
|
||||||
#### tver
|
|
||||||
* `backend`: Backend API to use for extraction - one of `streaks` (default) or `brightcove` (deprecated)
|
|
||||||
|
|
||||||
**Note**: These options may be changed/removed in the future without concern for backward compatibility
|
**Note**: These options may be changed/removed in the future without concern for backward compatibility
|
||||||
|
|
||||||
<!-- MANPAGE: MOVE "INSTALLATION" SECTION HERE -->
|
<!-- MANPAGE: MOVE "INSTALLATION" SECTION HERE -->
|
||||||
@ -2173,7 +2145,7 @@ with yt_dlp.YoutubeDL(ydl_opts) as ydl:
|
|||||||
|
|
||||||
* **[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 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. 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 NicoNico livestreams are not available. See [#31](https://github.com/yt-dlp/yt-dlp/pull/31) for details.
|
||||||
|
|
||||||
* **YouTube improvements**:
|
* **YouTube improvements**:
|
||||||
* Supports Clips, Stories (`ytstories:<channel UCID>`), Search (including filters)**\***, YouTube Music Search, Channel-specific search, Search prefixes (`ytsearch:`, `ytsearchdate:`)**\***, Mixes, and Feeds (`:ytfav`, `:ytwatchlater`, `:ytsubs`, `:ythistory`, `:ytrec`, `:ytnotif`)
|
* Supports Clips, Stories (`ytstories:<channel UCID>`), Search (including filters)**\***, YouTube Music Search, Channel-specific search, Search prefixes (`ytsearch:`, `ytsearchdate:`)**\***, Mixes, and Feeds (`:ytfav`, `:ytwatchlater`, `:ytsubs`, `:ythistory`, `:ytrec`, `:ytnotif`)
|
||||||
@ -2239,7 +2211,7 @@ Some of yt-dlp's default options are different from that of youtube-dl and youtu
|
|||||||
* Live chats (if available) are considered as subtitles. Use `--sub-langs all,-live_chat` to download all subtitles except live chat. You can also use `--compat-options no-live-chat` to prevent any live chat/danmaku from downloading
|
* Live chats (if available) are considered as subtitles. Use `--sub-langs all,-live_chat` to download all subtitles except live chat. You can also use `--compat-options no-live-chat` to prevent any live chat/danmaku from downloading
|
||||||
* YouTube channel URLs download all uploads of the channel. To download only the videos in a specific tab, pass the tab's URL. If the channel does not show the requested tab, an error will be raised. Also, `/live` URLs raise an error if there are no live videos instead of silently downloading the entire channel. You may use `--compat-options no-youtube-channel-redirect` to revert all these redirections
|
* YouTube channel URLs download all uploads of the channel. To download only the videos in a specific tab, pass the tab's URL. If the channel does not show the requested tab, an error will be raised. Also, `/live` URLs raise an error if there are no live videos instead of silently downloading the entire channel. You may use `--compat-options no-youtube-channel-redirect` to revert all these redirections
|
||||||
* Unavailable videos are also listed for YouTube playlists. Use `--compat-options no-youtube-unavailable-videos` to remove this
|
* Unavailable videos are also listed for YouTube playlists. Use `--compat-options no-youtube-unavailable-videos` to remove this
|
||||||
* The upload dates extracted from YouTube are in UTC.
|
* The upload dates extracted from YouTube are in UTC [when available](https://github.com/yt-dlp/yt-dlp/blob/89e4d86171c7b7c997c77d4714542e0383bf0db0/yt_dlp/extractor/youtube.py#L3898-L3900). Use `--compat-options no-youtube-prefer-utc-upload-date` to prefer the non-UTC upload date.
|
||||||
* If `ffmpeg` is used as the downloader, the downloading and merging of formats happen in a single step when possible. Use `--compat-options no-direct-merge` to revert this
|
* If `ffmpeg` is used as the downloader, the downloading and merging of formats happen in a single step when possible. Use `--compat-options no-direct-merge` to revert this
|
||||||
* Thumbnail embedding in `mp4` is done with mutagen if possible. Use `--compat-options embed-thumbnail-atomicparsley` to force the use of AtomicParsley instead
|
* Thumbnail embedding in `mp4` is done with mutagen if possible. Use `--compat-options embed-thumbnail-atomicparsley` to force the use of AtomicParsley instead
|
||||||
* Some internal metadata such as filenames are removed by default from the infojson. Use `--no-clean-infojson` or `--compat-options no-clean-infojson` to revert this
|
* Some internal metadata such as filenames are removed by default from the infojson. Use `--no-clean-infojson` or `--compat-options no-clean-infojson` to revert this
|
||||||
@ -2258,10 +2230,9 @@ For ease of use, a few more compat options are available:
|
|||||||
* `--compat-options all`: Use all compat options (**Do NOT use this!**)
|
* `--compat-options all`: Use all compat options (**Do NOT use this!**)
|
||||||
* `--compat-options youtube-dl`: Same as `--compat-options all,-multistreams,-playlist-match-filter,-manifest-filesize-approx,-allow-unsafe-ext,-prefer-vp9-sort`
|
* `--compat-options youtube-dl`: Same as `--compat-options all,-multistreams,-playlist-match-filter,-manifest-filesize-approx,-allow-unsafe-ext,-prefer-vp9-sort`
|
||||||
* `--compat-options youtube-dlc`: Same as `--compat-options all,-no-live-chat,-no-youtube-channel-redirect,-playlist-match-filter,-manifest-filesize-approx,-allow-unsafe-ext,-prefer-vp9-sort`
|
* `--compat-options youtube-dlc`: Same as `--compat-options all,-no-live-chat,-no-youtube-channel-redirect,-playlist-match-filter,-manifest-filesize-approx,-allow-unsafe-ext,-prefer-vp9-sort`
|
||||||
* `--compat-options 2021`: Same as `--compat-options 2022,no-certifi,filename-sanitization`
|
* `--compat-options 2021`: Same as `--compat-options 2022,no-certifi,filename-sanitization,no-youtube-prefer-utc-upload-date`
|
||||||
* `--compat-options 2022`: Same as `--compat-options 2023,playlist-match-filter,no-external-downloader-progress,prefer-legacy-http-handler,manifest-filesize-approx`
|
* `--compat-options 2022`: Same as `--compat-options 2023,playlist-match-filter,no-external-downloader-progress,prefer-legacy-http-handler,manifest-filesize-approx`
|
||||||
* `--compat-options 2023`: Same as `--compat-options 2024,prefer-vp9-sort`
|
* `--compat-options 2023`: Same as `--compat-options prefer-vp9-sort`. Use this to enable all future compat options
|
||||||
* `--compat-options 2024`: Currently does nothing. Use this to enable all future compat options
|
|
||||||
|
|
||||||
The following compat options restore vulnerable behavior from before security patches:
|
The following compat options restore vulnerable behavior from before security patches:
|
||||||
|
|
||||||
|
@ -245,14 +245,5 @@
|
|||||||
"when": "76ac023ff02f06e8c003d104f02a03deeddebdcd",
|
"when": "76ac023ff02f06e8c003d104f02a03deeddebdcd",
|
||||||
"short": "[ie/youtube:tab] Improve shorts title extraction (#11997)",
|
"short": "[ie/youtube:tab] Improve shorts title extraction (#11997)",
|
||||||
"authors": ["bashonly", "d3d9"]
|
"authors": ["bashonly", "d3d9"]
|
||||||
},
|
|
||||||
{
|
|
||||||
"action": "add",
|
|
||||||
"when": "88eb1e7a9a2720ac89d653c0d0e40292388823bb",
|
|
||||||
"short": "[priority] **New option `--preset-alias`/`-t` has been added**\nThis provides convenient predefined aliases for common use cases. Available presets include `mp4`, `mp3`, `mkv`, `aac`, and `sleep`. See [the README](https://github.com/yt-dlp/yt-dlp/blob/master/README.md#preset-aliases) for more details."
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"action": "remove",
|
|
||||||
"when": "d596824c2f8428362c072518856065070616e348"
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
@ -10,9 +10,6 @@ sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
|||||||
from inspect import getsource
|
from inspect import getsource
|
||||||
|
|
||||||
from devscripts.utils import get_filename_args, read_file, write_file
|
from devscripts.utils import get_filename_args, read_file, write_file
|
||||||
from yt_dlp.extractor import import_extractors
|
|
||||||
from yt_dlp.extractor.common import InfoExtractor, SearchInfoExtractor
|
|
||||||
from yt_dlp.globals import extractors
|
|
||||||
|
|
||||||
NO_ATTR = object()
|
NO_ATTR = object()
|
||||||
STATIC_CLASS_PROPERTIES = [
|
STATIC_CLASS_PROPERTIES = [
|
||||||
@ -41,7 +38,8 @@ def main():
|
|||||||
|
|
||||||
lazy_extractors_filename = get_filename_args(default_outfile='yt_dlp/extractor/lazy_extractors.py')
|
lazy_extractors_filename = get_filename_args(default_outfile='yt_dlp/extractor/lazy_extractors.py')
|
||||||
|
|
||||||
import_extractors()
|
from yt_dlp.extractor.extractors import _ALL_CLASSES
|
||||||
|
from yt_dlp.extractor.common import InfoExtractor, SearchInfoExtractor
|
||||||
|
|
||||||
DummyInfoExtractor = type('InfoExtractor', (InfoExtractor,), {'IE_NAME': NO_ATTR})
|
DummyInfoExtractor = type('InfoExtractor', (InfoExtractor,), {'IE_NAME': NO_ATTR})
|
||||||
module_src = '\n'.join((
|
module_src = '\n'.join((
|
||||||
@ -49,7 +47,7 @@ def main():
|
|||||||
' _module = None',
|
' _module = None',
|
||||||
*extra_ie_code(DummyInfoExtractor),
|
*extra_ie_code(DummyInfoExtractor),
|
||||||
'\nclass LazyLoadSearchExtractor(LazyLoadExtractor):\n pass\n',
|
'\nclass LazyLoadSearchExtractor(LazyLoadExtractor):\n pass\n',
|
||||||
*build_ies(list(extractors.value.values()), (InfoExtractor, SearchInfoExtractor), DummyInfoExtractor),
|
*build_ies(_ALL_CLASSES, (InfoExtractor, SearchInfoExtractor), DummyInfoExtractor),
|
||||||
))
|
))
|
||||||
|
|
||||||
write_file(lazy_extractors_filename, f'{module_src}\n')
|
write_file(lazy_extractors_filename, f'{module_src}\n')
|
||||||
@ -75,7 +73,7 @@ def build_ies(ies, bases, attr_base):
|
|||||||
if ie in ies:
|
if ie in ies:
|
||||||
names.append(ie.__name__)
|
names.append(ie.__name__)
|
||||||
|
|
||||||
yield '\n_CLASS_LOOKUP = {%s}' % ', '.join(f'{name!r}: {name}' for name in names)
|
yield f'\n_ALL_CLASSES = [{", ".join(names)}]'
|
||||||
|
|
||||||
|
|
||||||
def sort_ies(ies, ignored_bases):
|
def sort_ies(ies, ignored_bases):
|
||||||
|
@ -55,7 +55,8 @@ default = [
|
|||||||
"websockets>=13.0",
|
"websockets>=13.0",
|
||||||
]
|
]
|
||||||
curl-cffi = [
|
curl-cffi = [
|
||||||
"curl-cffi>=0.5.10,!=0.6.*,!=0.7.*,!=0.8.*,!=0.9.*,<0.11; implementation_name=='cpython'",
|
"curl-cffi==0.5.10; os_name=='nt' and implementation_name=='cpython'",
|
||||||
|
"curl-cffi>=0.5.10,!=0.6.*,<0.7.2; os_name!='nt' and implementation_name=='cpython'",
|
||||||
]
|
]
|
||||||
secretstorage = [
|
secretstorage = [
|
||||||
"cffi",
|
"cffi",
|
||||||
@ -75,14 +76,14 @@ dev = [
|
|||||||
]
|
]
|
||||||
static-analysis = [
|
static-analysis = [
|
||||||
"autopep8~=2.0",
|
"autopep8~=2.0",
|
||||||
"ruff~=0.11.0",
|
"ruff~=0.9.0",
|
||||||
]
|
]
|
||||||
test = [
|
test = [
|
||||||
"pytest~=8.1",
|
"pytest~=8.1",
|
||||||
"pytest-rerunfailures~=14.0",
|
"pytest-rerunfailures~=14.0",
|
||||||
]
|
]
|
||||||
pyinstaller = [
|
pyinstaller = [
|
||||||
"pyinstaller>=6.13.0", # Windows temp cleanup fixed in 6.13.0
|
"pyinstaller>=6.11.1", # Windows temp cleanup fixed in 6.11.1
|
||||||
]
|
]
|
||||||
|
|
||||||
[project.urls]
|
[project.urls]
|
||||||
@ -383,14 +384,9 @@ select = [
|
|||||||
"W391",
|
"W391",
|
||||||
"W504",
|
"W504",
|
||||||
]
|
]
|
||||||
exclude = "*/extractor/lazy_extractors.py,*venv*,*/test/testdata/sigs/player-*.js,.idea,.vscode"
|
|
||||||
|
|
||||||
[tool.pytest.ini_options]
|
[tool.pytest.ini_options]
|
||||||
addopts = [
|
addopts = "-ra -v --strict-markers"
|
||||||
"-ra", # summary: all except passed
|
|
||||||
"--verbose",
|
|
||||||
"--strict-markers",
|
|
||||||
]
|
|
||||||
markers = [
|
markers = [
|
||||||
"download",
|
"download",
|
||||||
]
|
]
|
||||||
|
@ -7,7 +7,6 @@ The only reliable way to check if a site is supported is to try it.
|
|||||||
|
|
||||||
- **17live**
|
- **17live**
|
||||||
- **17live:clip**
|
- **17live:clip**
|
||||||
- **17live:vod**
|
|
||||||
- **1News**: 1news.co.nz article videos
|
- **1News**: 1news.co.nz article videos
|
||||||
- **1tv**: Первый канал
|
- **1tv**: Первый канал
|
||||||
- **20min**
|
- **20min**
|
||||||
@ -201,7 +200,7 @@ The only reliable way to check if a site is supported is to try it.
|
|||||||
- **blogger.com**
|
- **blogger.com**
|
||||||
- **Bloomberg**
|
- **Bloomberg**
|
||||||
- **Bluesky**
|
- **Bluesky**
|
||||||
- **BokeCC**: CC视频
|
- **BokeCC**
|
||||||
- **BongaCams**
|
- **BongaCams**
|
||||||
- **Boosty**
|
- **Boosty**
|
||||||
- **BostonGlobe**
|
- **BostonGlobe**
|
||||||
@ -225,7 +224,6 @@ The only reliable way to check if a site is supported is to try it.
|
|||||||
- **bt:vestlendingen**: Bergens Tidende - Vestlendingen
|
- **bt:vestlendingen**: Bergens Tidende - Vestlendingen
|
||||||
- **Bundesliga**
|
- **Bundesliga**
|
||||||
- **Bundestag**
|
- **Bundestag**
|
||||||
- **BunnyCdn**
|
|
||||||
- **BusinessInsider**
|
- **BusinessInsider**
|
||||||
- **BuzzFeed**
|
- **BuzzFeed**
|
||||||
- **BYUtv**: (**Currently broken**)
|
- **BYUtv**: (**Currently broken**)
|
||||||
@ -244,7 +242,6 @@ The only reliable way to check if a site is supported is to try it.
|
|||||||
- **CanalAlpha**
|
- **CanalAlpha**
|
||||||
- **canalc2.tv**
|
- **canalc2.tv**
|
||||||
- **Canalplus**: mycanal.fr and piwiplus.fr
|
- **Canalplus**: mycanal.fr and piwiplus.fr
|
||||||
- **Canalsurmas**
|
|
||||||
- **CaracolTvPlay**: [*caracoltv-play*](## "netrc machine")
|
- **CaracolTvPlay**: [*caracoltv-play*](## "netrc machine")
|
||||||
- **CartoonNetwork**
|
- **CartoonNetwork**
|
||||||
- **cbc.ca**
|
- **cbc.ca**
|
||||||
@ -348,6 +345,8 @@ The only reliable way to check if a site is supported is to try it.
|
|||||||
- **daystar:clip**
|
- **daystar:clip**
|
||||||
- **DBTV**
|
- **DBTV**
|
||||||
- **DctpTv**
|
- **DctpTv**
|
||||||
|
- **DeezerAlbum**
|
||||||
|
- **DeezerPlaylist**
|
||||||
- **democracynow**
|
- **democracynow**
|
||||||
- **DestinationAmerica**
|
- **DestinationAmerica**
|
||||||
- **DetikEmbed**
|
- **DetikEmbed**
|
||||||
@ -394,8 +393,6 @@ The only reliable way to check if a site is supported is to try it.
|
|||||||
- **dvtv**: http://video.aktualne.cz/
|
- **dvtv**: http://video.aktualne.cz/
|
||||||
- **dw**: (**Currently broken**)
|
- **dw**: (**Currently broken**)
|
||||||
- **dw:article**: (**Currently broken**)
|
- **dw:article**: (**Currently broken**)
|
||||||
- **dzen.ru**: Дзен (dzen) formerly Яндекс.Дзен (Yandex Zen)
|
|
||||||
- **dzen.ru:channel**
|
|
||||||
- **EaglePlatform**
|
- **EaglePlatform**
|
||||||
- **EbaumsWorld**
|
- **EbaumsWorld**
|
||||||
- **Ebay**
|
- **Ebay**
|
||||||
@ -474,7 +471,6 @@ The only reliable way to check if a site is supported is to try it.
|
|||||||
- **FoxNewsVideo**
|
- **FoxNewsVideo**
|
||||||
- **FoxSports**
|
- **FoxSports**
|
||||||
- **fptplay**: fptplay.vn
|
- **fptplay**: fptplay.vn
|
||||||
- **FrancaisFacile**
|
|
||||||
- **FranceCulture**
|
- **FranceCulture**
|
||||||
- **FranceInter**
|
- **FranceInter**
|
||||||
- **francetv**
|
- **francetv**
|
||||||
@ -613,10 +609,10 @@ The only reliable way to check if a site is supported is to try it.
|
|||||||
- **Inc**
|
- **Inc**
|
||||||
- **IndavideoEmbed**
|
- **IndavideoEmbed**
|
||||||
- **InfoQ**
|
- **InfoQ**
|
||||||
- **Instagram**
|
- **Instagram**: [*instagram*](## "netrc machine")
|
||||||
- **instagram:story**
|
- **instagram:story**: [*instagram*](## "netrc machine")
|
||||||
- **instagram:tag**: Instagram hashtag search URLs
|
- **instagram:tag**: [*instagram*](## "netrc machine") Instagram hashtag search URLs
|
||||||
- **instagram:user**: Instagram user profile (**Currently broken**)
|
- **instagram:user**: [*instagram*](## "netrc machine") Instagram user profile (**Currently broken**)
|
||||||
- **InstagramIOS**: IOS instagram:// URL
|
- **InstagramIOS**: IOS instagram:// URL
|
||||||
- **Internazionale**
|
- **Internazionale**
|
||||||
- **InternetVideoArchive**
|
- **InternetVideoArchive**
|
||||||
@ -636,7 +632,6 @@ The only reliable way to check if a site is supported is to try it.
|
|||||||
- **ivi**: ivi.ru
|
- **ivi**: ivi.ru
|
||||||
- **ivi:compilation**: ivi.ru compilations
|
- **ivi:compilation**: ivi.ru compilations
|
||||||
- **ivideon**: Ivideon TV
|
- **ivideon**: Ivideon TV
|
||||||
- **Ivoox**
|
|
||||||
- **IVXPlayer**
|
- **IVXPlayer**
|
||||||
- **iwara**: [*iwara*](## "netrc machine")
|
- **iwara**: [*iwara*](## "netrc machine")
|
||||||
- **iwara:playlist**: [*iwara*](## "netrc machine")
|
- **iwara:playlist**: [*iwara*](## "netrc machine")
|
||||||
@ -666,6 +661,7 @@ The only reliable way to check if a site is supported is to try it.
|
|||||||
- **KelbyOne**: (**Currently broken**)
|
- **KelbyOne**: (**Currently broken**)
|
||||||
- **Kenh14Playlist**
|
- **Kenh14Playlist**
|
||||||
- **Kenh14Video**
|
- **Kenh14Video**
|
||||||
|
- **Ketnet**
|
||||||
- **khanacademy**
|
- **khanacademy**
|
||||||
- **khanacademy:unit**
|
- **khanacademy:unit**
|
||||||
- **kick:clips**
|
- **kick:clips**
|
||||||
@ -674,7 +670,6 @@ The only reliable way to check if a site is supported is to try it.
|
|||||||
- **Kicker**
|
- **Kicker**
|
||||||
- **KickStarter**
|
- **KickStarter**
|
||||||
- **Kika**: KiKA.de
|
- **Kika**: KiKA.de
|
||||||
- **KikaPlaylist**
|
|
||||||
- **kinja:embed**
|
- **kinja:embed**
|
||||||
- **KinoPoisk**
|
- **KinoPoisk**
|
||||||
- **Kommunetv**
|
- **Kommunetv**
|
||||||
@ -727,7 +722,6 @@ The only reliable way to check if a site is supported is to try it.
|
|||||||
- **limelight:channel**
|
- **limelight:channel**
|
||||||
- **limelight:channel_list**
|
- **limelight:channel_list**
|
||||||
- **LinkedIn**: [*linkedin*](## "netrc machine")
|
- **LinkedIn**: [*linkedin*](## "netrc machine")
|
||||||
- **linkedin:events**: [*linkedin*](## "netrc machine")
|
|
||||||
- **linkedin:learning**: [*linkedin*](## "netrc machine")
|
- **linkedin:learning**: [*linkedin*](## "netrc machine")
|
||||||
- **linkedin:learning:course**: [*linkedin*](## "netrc machine")
|
- **linkedin:learning:course**: [*linkedin*](## "netrc machine")
|
||||||
- **Liputan6**
|
- **Liputan6**
|
||||||
@ -739,11 +733,9 @@ The only reliable way to check if a site is supported is to try it.
|
|||||||
- **Livestreamfails**
|
- **Livestreamfails**
|
||||||
- **Lnk**
|
- **Lnk**
|
||||||
- **loc**: Library of Congress
|
- **loc**: Library of Congress
|
||||||
- **Loco**
|
|
||||||
- **loom**
|
- **loom**
|
||||||
- **loom:folder**
|
- **loom:folder**
|
||||||
- **LoveHomePorn**
|
- **LoveHomePorn**
|
||||||
- **LRTRadio**
|
|
||||||
- **LRTStream**
|
- **LRTStream**
|
||||||
- **LRTVOD**
|
- **LRTVOD**
|
||||||
- **LSMLREmbed**
|
- **LSMLREmbed**
|
||||||
@ -765,7 +757,7 @@ The only reliable way to check if a site is supported is to try it.
|
|||||||
- **ManotoTV**: Manoto TV (Episode)
|
- **ManotoTV**: Manoto TV (Episode)
|
||||||
- **ManotoTVLive**: Manoto TV (Live)
|
- **ManotoTVLive**: Manoto TV (Live)
|
||||||
- **ManotoTVShow**: Manoto TV (Show)
|
- **ManotoTVShow**: Manoto TV (Show)
|
||||||
- **ManyVids**
|
- **ManyVids**: (**Currently broken**)
|
||||||
- **MaoriTV**
|
- **MaoriTV**
|
||||||
- **Markiza**: (**Currently broken**)
|
- **Markiza**: (**Currently broken**)
|
||||||
- **MarkizaPage**: (**Currently broken**)
|
- **MarkizaPage**: (**Currently broken**)
|
||||||
@ -835,11 +827,11 @@ The only reliable way to check if a site is supported is to try it.
|
|||||||
- **MotherlessUploader**
|
- **MotherlessUploader**
|
||||||
- **Motorsport**: motorsport.com (**Currently broken**)
|
- **Motorsport**: motorsport.com (**Currently broken**)
|
||||||
- **MovieFap**
|
- **MovieFap**
|
||||||
- **moviepilot**: Moviepilot trailer
|
- **Moviepilot**
|
||||||
- **MoviewPlay**
|
- **MoviewPlay**
|
||||||
- **Moviezine**
|
- **Moviezine**
|
||||||
- **MovingImage**
|
- **MovingImage**
|
||||||
- **MSN**
|
- **MSN**: (**Currently broken**)
|
||||||
- **mtg**: MTG services
|
- **mtg**: MTG services
|
||||||
- **mtv**
|
- **mtv**
|
||||||
- **mtv.de**: (**Currently broken**)
|
- **mtv.de**: (**Currently broken**)
|
||||||
@ -952,7 +944,7 @@ The only reliable way to check if a site is supported is to try it.
|
|||||||
- **nickelodeonru**
|
- **nickelodeonru**
|
||||||
- **niconico**: [*niconico*](## "netrc machine") ニコニコ動画
|
- **niconico**: [*niconico*](## "netrc machine") ニコニコ動画
|
||||||
- **niconico:history**: NicoNico user history or likes. Requires cookies.
|
- **niconico:history**: NicoNico user history or likes. Requires cookies.
|
||||||
- **niconico:live**: [*niconico*](## "netrc machine") ニコニコ生放送
|
- **niconico:live**: ニコニコ生放送
|
||||||
- **niconico:playlist**
|
- **niconico:playlist**
|
||||||
- **niconico:series**
|
- **niconico:series**
|
||||||
- **niconico:tag**: NicoNico video tag URLs
|
- **niconico:tag**: NicoNico video tag URLs
|
||||||
@ -1059,8 +1051,6 @@ The only reliable way to check if a site is supported is to try it.
|
|||||||
- **Parler**: Posts on parler.com
|
- **Parler**: Posts on parler.com
|
||||||
- **parliamentlive.tv**: UK parliament videos
|
- **parliamentlive.tv**: UK parliament videos
|
||||||
- **Parlview**: (**Currently broken**)
|
- **Parlview**: (**Currently broken**)
|
||||||
- **parti:livestream**
|
|
||||||
- **parti:video**
|
|
||||||
- **patreon**
|
- **patreon**
|
||||||
- **patreon:campaign**
|
- **patreon:campaign**
|
||||||
- **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)
|
||||||
@ -1235,7 +1225,6 @@ The only reliable way to check if a site is supported is to try it.
|
|||||||
- **RoosterTeeth**: [*roosterteeth*](## "netrc machine")
|
- **RoosterTeeth**: [*roosterteeth*](## "netrc machine")
|
||||||
- **RoosterTeethSeries**: [*roosterteeth*](## "netrc machine")
|
- **RoosterTeethSeries**: [*roosterteeth*](## "netrc machine")
|
||||||
- **RottenTomatoes**
|
- **RottenTomatoes**
|
||||||
- **RoyaLive**
|
|
||||||
- **Rozhlas**
|
- **Rozhlas**
|
||||||
- **RozhlasVltava**
|
- **RozhlasVltava**
|
||||||
- **RTBF**: [*rtbf*](## "netrc machine") (**Currently broken**)
|
- **RTBF**: [*rtbf*](## "netrc machine") (**Currently broken**)
|
||||||
@ -1256,10 +1245,12 @@ The only reliable way to check if a site is supported is to try it.
|
|||||||
- **RTVCKaltura**
|
- **RTVCKaltura**
|
||||||
- **RTVCPlay**
|
- **RTVCPlay**
|
||||||
- **RTVCPlayEmbed**
|
- **RTVCPlayEmbed**
|
||||||
- **rtve.es:alacarta**: RTVE a la carta and Play
|
- **rtve.es:alacarta**: RTVE a la carta
|
||||||
- **rtve.es:audio**: RTVE audio
|
- **rtve.es:audio**: RTVE audio
|
||||||
|
- **rtve.es:infantil**: RTVE infantil
|
||||||
- **rtve.es:live**: RTVE.es live streams
|
- **rtve.es:live**: RTVE.es live streams
|
||||||
- **rtve.es:television**
|
- **rtve.es:television**
|
||||||
|
- **RTVS**
|
||||||
- **rtvslo.si**
|
- **rtvslo.si**
|
||||||
- **rtvslo.si:show**
|
- **rtvslo.si:show**
|
||||||
- **RudoVideo**
|
- **RudoVideo**
|
||||||
@ -1314,8 +1305,8 @@ The only reliable way to check if a site is supported is to try it.
|
|||||||
- **sejm**
|
- **sejm**
|
||||||
- **Sen**
|
- **Sen**
|
||||||
- **SenalColombiaLive**: (**Currently broken**)
|
- **SenalColombiaLive**: (**Currently broken**)
|
||||||
- **senate.gov**
|
- **SenateGov**
|
||||||
- **senate.gov:isvp**
|
- **SenateISVP**
|
||||||
- **SendtoNews**: (**Currently broken**)
|
- **SendtoNews**: (**Currently broken**)
|
||||||
- **Servus**
|
- **Servus**
|
||||||
- **Sexu**: (**Currently broken**)
|
- **Sexu**: (**Currently broken**)
|
||||||
@ -1351,7 +1342,6 @@ The only reliable way to check if a site is supported is to try it.
|
|||||||
- **Smotrim**
|
- **Smotrim**
|
||||||
- **SnapchatSpotlight**
|
- **SnapchatSpotlight**
|
||||||
- **Snotr**
|
- **Snotr**
|
||||||
- **SoftWhiteUnderbelly**: [*softwhiteunderbelly*](## "netrc machine")
|
|
||||||
- **Sohu**
|
- **Sohu**
|
||||||
- **SohuV**
|
- **SohuV**
|
||||||
- **SonyLIV**: [*sonyliv*](## "netrc machine")
|
- **SonyLIV**: [*sonyliv*](## "netrc machine")
|
||||||
@ -1408,14 +1398,12 @@ The only reliable way to check if a site is supported is to try it.
|
|||||||
- **StoryFire**
|
- **StoryFire**
|
||||||
- **StoryFireSeries**
|
- **StoryFireSeries**
|
||||||
- **StoryFireUser**
|
- **StoryFireUser**
|
||||||
- **Streaks**
|
|
||||||
- **Streamable**
|
- **Streamable**
|
||||||
- **StreamCZ**
|
- **StreamCZ**
|
||||||
- **StreetVoice**
|
- **StreetVoice**
|
||||||
- **StretchInternet**
|
- **StretchInternet**
|
||||||
- **Stripchat**
|
- **Stripchat**
|
||||||
- **stv:player**
|
- **stv:player**
|
||||||
- **stvr**: Slovak Television and Radio (formerly RTVS)
|
|
||||||
- **Subsplash**
|
- **Subsplash**
|
||||||
- **subsplash:playlist**
|
- **subsplash:playlist**
|
||||||
- **Substack**
|
- **Substack**
|
||||||
@ -1548,8 +1536,6 @@ The only reliable way to check if a site is supported is to try it.
|
|||||||
- **tv5unis**
|
- **tv5unis**
|
||||||
- **tv5unis:video**
|
- **tv5unis:video**
|
||||||
- **tv8.it**
|
- **tv8.it**
|
||||||
- **tv8.it:live**: TV8 Live
|
|
||||||
- **tv8.it:playlist**: TV8 Playlist
|
|
||||||
- **TVANouvelles**
|
- **TVANouvelles**
|
||||||
- **TVANouvellesArticle**
|
- **TVANouvellesArticle**
|
||||||
- **tvaplus**: TVA+
|
- **tvaplus**: TVA+
|
||||||
@ -1570,8 +1556,6 @@ The only reliable way to check if a site is supported is to try it.
|
|||||||
- **tvp:vod:series**
|
- **tvp:vod:series**
|
||||||
- **TVPlayer**
|
- **TVPlayer**
|
||||||
- **TVPlayHome**
|
- **TVPlayHome**
|
||||||
- **tvw**
|
|
||||||
- **tvw:tvchannels**
|
|
||||||
- **Tweakers**
|
- **Tweakers**
|
||||||
- **TwitCasting**
|
- **TwitCasting**
|
||||||
- **TwitCastingLive**
|
- **TwitCastingLive**
|
||||||
@ -1653,6 +1637,8 @@ The only reliable way to check if a site is supported is to try it.
|
|||||||
- **viewlift**
|
- **viewlift**
|
||||||
- **viewlift:embed**
|
- **viewlift:embed**
|
||||||
- **Viidea**
|
- **Viidea**
|
||||||
|
- **viki**: [*viki*](## "netrc machine")
|
||||||
|
- **viki:channel**: [*viki*](## "netrc machine")
|
||||||
- **vimeo**: [*vimeo*](## "netrc machine")
|
- **vimeo**: [*vimeo*](## "netrc machine")
|
||||||
- **vimeo:album**: [*vimeo*](## "netrc machine")
|
- **vimeo:album**: [*vimeo*](## "netrc machine")
|
||||||
- **vimeo:channel**: [*vimeo*](## "netrc machine")
|
- **vimeo:channel**: [*vimeo*](## "netrc machine")
|
||||||
@ -1690,12 +1676,8 @@ The only reliable way to check if a site is supported is to try it.
|
|||||||
- **vpro**: npo.nl, ntr.nl, omroepwnl.nl, zapp.nl and npo3.nl
|
- **vpro**: npo.nl, ntr.nl, omroepwnl.nl, zapp.nl and npo3.nl
|
||||||
- **vqq:series**
|
- **vqq:series**
|
||||||
- **vqq:video**
|
- **vqq:video**
|
||||||
- **vrsquare**: VR SQUARE
|
|
||||||
- **vrsquare:channel**
|
|
||||||
- **vrsquare:search**
|
|
||||||
- **vrsquare:section**
|
|
||||||
- **VRT**: VRT NWS, Flanders News, Flandern Info and Sporza
|
- **VRT**: VRT NWS, Flanders News, Flandern Info and Sporza
|
||||||
- **vrtmax**: [*vrtnu*](## "netrc machine") VRT MAX (formerly VRT NU)
|
- **VrtNU**: [*vrtnu*](## "netrc machine") VRT MAX
|
||||||
- **VTM**: (**Currently broken**)
|
- **VTM**: (**Currently broken**)
|
||||||
- **VTV**
|
- **VTV**
|
||||||
- **VTVGo**
|
- **VTVGo**
|
||||||
@ -1830,12 +1812,14 @@ The only reliable way to check if a site is supported is to try it.
|
|||||||
- **ZattooLive**: [*zattoo*](## "netrc machine")
|
- **ZattooLive**: [*zattoo*](## "netrc machine")
|
||||||
- **ZattooMovies**: [*zattoo*](## "netrc machine")
|
- **ZattooMovies**: [*zattoo*](## "netrc machine")
|
||||||
- **ZattooRecordings**: [*zattoo*](## "netrc machine")
|
- **ZattooRecordings**: [*zattoo*](## "netrc machine")
|
||||||
- **zdf**
|
- **ZDF**
|
||||||
- **zdf:channel**
|
- **ZDFChannel**
|
||||||
- **Zee5**: [*zee5*](## "netrc machine")
|
- **Zee5**: [*zee5*](## "netrc machine")
|
||||||
- **zee5:series**
|
- **zee5:series**
|
||||||
- **ZeeNews**: (**Currently broken**)
|
- **ZeeNews**: (**Currently broken**)
|
||||||
- **ZenPorn**
|
- **ZenPorn**
|
||||||
|
- **ZenYandex**
|
||||||
|
- **ZenYandexChannel**
|
||||||
- **ZetlandDKArticle**
|
- **ZetlandDKArticle**
|
||||||
- **Zhihu**
|
- **Zhihu**
|
||||||
- **zingmp3**: zingmp3.vn
|
- **zingmp3**: zingmp3.vn
|
||||||
|
176
test/helper.py
176
test/helper.py
@ -101,109 +101,87 @@ def getwebpagetestcases():
|
|||||||
md5 = lambda s: hashlib.md5(s.encode()).hexdigest()
|
md5 = lambda s: hashlib.md5(s.encode()).hexdigest()
|
||||||
|
|
||||||
|
|
||||||
def _iter_differences(got, expected, field):
|
|
||||||
if isinstance(expected, str):
|
|
||||||
op, _, val = expected.partition(':')
|
|
||||||
if op in ('mincount', 'maxcount', 'count'):
|
|
||||||
if not isinstance(got, (list, dict)):
|
|
||||||
yield field, f'expected either {list.__name__} or {dict.__name__}, got {type(got).__name__}'
|
|
||||||
return
|
|
||||||
|
|
||||||
expected_num = int(val)
|
|
||||||
got_num = len(got)
|
|
||||||
if op == 'mincount':
|
|
||||||
if got_num < expected_num:
|
|
||||||
yield field, f'expected at least {val} items, got {got_num}'
|
|
||||||
return
|
|
||||||
|
|
||||||
if op == 'maxcount':
|
|
||||||
if got_num > expected_num:
|
|
||||||
yield field, f'expected at most {val} items, got {got_num}'
|
|
||||||
return
|
|
||||||
|
|
||||||
assert op == 'count'
|
|
||||||
if got_num != expected_num:
|
|
||||||
yield field, f'expected exactly {val} items, got {got_num}'
|
|
||||||
return
|
|
||||||
|
|
||||||
if not isinstance(got, str):
|
|
||||||
yield field, f'expected {str.__name__}, got {type(got).__name__}'
|
|
||||||
return
|
|
||||||
|
|
||||||
if op == 're':
|
|
||||||
if not re.match(val, got):
|
|
||||||
yield field, f'should match {val!r}, got {got!r}'
|
|
||||||
return
|
|
||||||
|
|
||||||
if op == 'startswith':
|
|
||||||
if not got.startswith(val):
|
|
||||||
yield field, f'should start with {val!r}, got {got!r}'
|
|
||||||
return
|
|
||||||
|
|
||||||
if op == 'contains':
|
|
||||||
if not val.startswith(got):
|
|
||||||
yield field, f'should contain {val!r}, got {got!r}'
|
|
||||||
return
|
|
||||||
|
|
||||||
if op == 'md5':
|
|
||||||
hash_val = md5(got)
|
|
||||||
if hash_val != val:
|
|
||||||
yield field, f'expected hash {val}, got {hash_val}'
|
|
||||||
return
|
|
||||||
|
|
||||||
if got != expected:
|
|
||||||
yield field, f'expected {expected!r}, got {got!r}'
|
|
||||||
return
|
|
||||||
|
|
||||||
if isinstance(expected, dict) and isinstance(got, dict):
|
|
||||||
for key, expected_val in expected.items():
|
|
||||||
if key not in got:
|
|
||||||
yield field, f'missing key: {key!r}'
|
|
||||||
continue
|
|
||||||
|
|
||||||
field_name = key if field is None else f'{field}.{key}'
|
|
||||||
yield from _iter_differences(got[key], expected_val, field_name)
|
|
||||||
return
|
|
||||||
|
|
||||||
if isinstance(expected, type):
|
|
||||||
if not isinstance(got, expected):
|
|
||||||
yield field, f'expected {expected.__name__}, got {type(got).__name__}'
|
|
||||||
return
|
|
||||||
|
|
||||||
if isinstance(expected, list) and isinstance(got, list):
|
|
||||||
# TODO: clever diffing algorithm lmao
|
|
||||||
if len(expected) != len(got):
|
|
||||||
yield field, f'expected length of {len(expected)}, got {len(got)}'
|
|
||||||
return
|
|
||||||
|
|
||||||
for index, (got_val, expected_val) in enumerate(zip(got, expected)):
|
|
||||||
field_name = str(index) if field is None else f'{field}.{index}'
|
|
||||||
yield from _iter_differences(got_val, expected_val, field_name)
|
|
||||||
return
|
|
||||||
|
|
||||||
if got != expected:
|
|
||||||
yield field, f'expected {expected!r}, got {got!r}'
|
|
||||||
|
|
||||||
|
|
||||||
def _expect_value(message, got, expected, field):
|
|
||||||
mismatches = list(_iter_differences(got, expected, field))
|
|
||||||
if not mismatches:
|
|
||||||
return
|
|
||||||
|
|
||||||
fields = [field for field, _ in mismatches if field is not None]
|
|
||||||
return ''.join((
|
|
||||||
message, f' ({", ".join(fields)})' if fields else '',
|
|
||||||
*(f'\n\t{field}: {message}' for field, message in mismatches)))
|
|
||||||
|
|
||||||
|
|
||||||
def expect_value(self, got, expected, field):
|
def expect_value(self, got, expected, field):
|
||||||
if message := _expect_value('values differ', got, expected, field):
|
if isinstance(expected, str) and expected.startswith('re:'):
|
||||||
self.fail(message)
|
match_str = expected[len('re:'):]
|
||||||
|
match_rex = re.compile(match_str)
|
||||||
|
|
||||||
|
self.assertTrue(
|
||||||
|
isinstance(got, str),
|
||||||
|
f'Expected a {str.__name__} object, but got {type(got).__name__} for field {field}')
|
||||||
|
self.assertTrue(
|
||||||
|
match_rex.match(got),
|
||||||
|
f'field {field} (value: {got!r}) should match {match_str!r}')
|
||||||
|
elif isinstance(expected, str) and expected.startswith('startswith:'):
|
||||||
|
start_str = expected[len('startswith:'):]
|
||||||
|
self.assertTrue(
|
||||||
|
isinstance(got, str),
|
||||||
|
f'Expected a {str.__name__} object, but got {type(got).__name__} for field {field}')
|
||||||
|
self.assertTrue(
|
||||||
|
got.startswith(start_str),
|
||||||
|
f'field {field} (value: {got!r}) should start with {start_str!r}')
|
||||||
|
elif isinstance(expected, str) and expected.startswith('contains:'):
|
||||||
|
contains_str = expected[len('contains:'):]
|
||||||
|
self.assertTrue(
|
||||||
|
isinstance(got, str),
|
||||||
|
f'Expected a {str.__name__} object, but got {type(got).__name__} for field {field}')
|
||||||
|
self.assertTrue(
|
||||||
|
contains_str in got,
|
||||||
|
f'field {field} (value: {got!r}) should contain {contains_str!r}')
|
||||||
|
elif isinstance(expected, type):
|
||||||
|
self.assertTrue(
|
||||||
|
isinstance(got, expected),
|
||||||
|
f'Expected type {expected!r} for field {field}, but got value {got!r} of type {type(got)!r}')
|
||||||
|
elif isinstance(expected, dict) and isinstance(got, dict):
|
||||||
|
expect_dict(self, got, expected)
|
||||||
|
elif isinstance(expected, list) and isinstance(got, list):
|
||||||
|
self.assertEqual(
|
||||||
|
len(expected), len(got),
|
||||||
|
f'Expect a list of length {len(expected)}, but got a list of length {len(got)} for field {field}')
|
||||||
|
for index, (item_got, item_expected) in enumerate(zip(got, expected)):
|
||||||
|
type_got = type(item_got)
|
||||||
|
type_expected = type(item_expected)
|
||||||
|
self.assertEqual(
|
||||||
|
type_expected, type_got,
|
||||||
|
f'Type mismatch for list item at index {index} for field {field}, '
|
||||||
|
f'expected {type_expected!r}, got {type_got!r}')
|
||||||
|
expect_value(self, item_got, item_expected, field)
|
||||||
|
else:
|
||||||
|
if isinstance(expected, str) and expected.startswith('md5:'):
|
||||||
|
self.assertTrue(
|
||||||
|
isinstance(got, str),
|
||||||
|
f'Expected field {field} to be a unicode object, but got value {got!r} of type {type(got)!r}')
|
||||||
|
got = 'md5:' + md5(got)
|
||||||
|
elif isinstance(expected, str) and re.match(r'^(?:min|max)?count:\d+', expected):
|
||||||
|
self.assertTrue(
|
||||||
|
isinstance(got, (list, dict)),
|
||||||
|
f'Expected field {field} to be a list or a dict, but it is of type {type(got).__name__}')
|
||||||
|
op, _, expected_num = expected.partition(':')
|
||||||
|
expected_num = int(expected_num)
|
||||||
|
if op == 'mincount':
|
||||||
|
assert_func = assertGreaterEqual
|
||||||
|
msg_tmpl = 'Expected %d items in field %s, but only got %d'
|
||||||
|
elif op == 'maxcount':
|
||||||
|
assert_func = assertLessEqual
|
||||||
|
msg_tmpl = 'Expected maximum %d items in field %s, but got %d'
|
||||||
|
elif op == 'count':
|
||||||
|
assert_func = assertEqual
|
||||||
|
msg_tmpl = 'Expected exactly %d items in field %s, but got %d'
|
||||||
|
else:
|
||||||
|
assert False
|
||||||
|
assert_func(
|
||||||
|
self, len(got), expected_num,
|
||||||
|
msg_tmpl % (expected_num, field, len(got)))
|
||||||
|
return
|
||||||
|
self.assertEqual(
|
||||||
|
expected, got,
|
||||||
|
f'Invalid value for field {field}, expected {expected!r}, got {got!r}')
|
||||||
|
|
||||||
|
|
||||||
def expect_dict(self, got_dict, expected_dict):
|
def expect_dict(self, got_dict, expected_dict):
|
||||||
if message := _expect_value('dictionaries differ', got_dict, expected_dict, None):
|
for info_field, expected in expected_dict.items():
|
||||||
self.fail(message)
|
got = got_dict.get(info_field)
|
||||||
|
expect_value(self, got, expected, info_field)
|
||||||
|
|
||||||
|
|
||||||
def sanitize_got_info_dict(got_dict):
|
def sanitize_got_info_dict(got_dict):
|
||||||
|
@ -638,7 +638,6 @@ jwplayer("mediaplayer").setup({"abouttext":"Visit Indie DB","aboutlink":"http:\/
|
|||||||
'img_bipbop_adv_example_fmp4',
|
'img_bipbop_adv_example_fmp4',
|
||||||
'https://devstreaming-cdn.apple.com/videos/streaming/examples/img_bipbop_adv_example_fmp4/master.m3u8',
|
'https://devstreaming-cdn.apple.com/videos/streaming/examples/img_bipbop_adv_example_fmp4/master.m3u8',
|
||||||
[{
|
[{
|
||||||
# 60kbps (bitrate not provided in m3u8); sorted as worst because it's grouped with lowest bitrate video track
|
|
||||||
'format_id': 'aud1-English',
|
'format_id': 'aud1-English',
|
||||||
'url': 'https://devstreaming-cdn.apple.com/videos/streaming/examples/img_bipbop_adv_example_fmp4/a1/prog_index.m3u8',
|
'url': 'https://devstreaming-cdn.apple.com/videos/streaming/examples/img_bipbop_adv_example_fmp4/a1/prog_index.m3u8',
|
||||||
'manifest_url': 'https://devstreaming-cdn.apple.com/videos/streaming/examples/img_bipbop_adv_example_fmp4/master.m3u8',
|
'manifest_url': 'https://devstreaming-cdn.apple.com/videos/streaming/examples/img_bipbop_adv_example_fmp4/master.m3u8',
|
||||||
@ -646,19 +645,7 @@ jwplayer("mediaplayer").setup({"abouttext":"Visit Indie DB","aboutlink":"http:\/
|
|||||||
'ext': 'mp4',
|
'ext': 'mp4',
|
||||||
'protocol': 'm3u8_native',
|
'protocol': 'm3u8_native',
|
||||||
'audio_ext': 'mp4',
|
'audio_ext': 'mp4',
|
||||||
'source_preference': 0,
|
|
||||||
}, {
|
}, {
|
||||||
# 192kbps (bitrate not provided in m3u8)
|
|
||||||
'format_id': 'aud3-English',
|
|
||||||
'url': 'https://devstreaming-cdn.apple.com/videos/streaming/examples/img_bipbop_adv_example_fmp4/a3/prog_index.m3u8',
|
|
||||||
'manifest_url': 'https://devstreaming-cdn.apple.com/videos/streaming/examples/img_bipbop_adv_example_fmp4/master.m3u8',
|
|
||||||
'language': 'en',
|
|
||||||
'ext': 'mp4',
|
|
||||||
'protocol': 'm3u8_native',
|
|
||||||
'audio_ext': 'mp4',
|
|
||||||
'source_preference': 1,
|
|
||||||
}, {
|
|
||||||
# 384kbps (bitrate not provided in m3u8); sorted as best because it's grouped with the highest bitrate video track
|
|
||||||
'format_id': 'aud2-English',
|
'format_id': 'aud2-English',
|
||||||
'url': 'https://devstreaming-cdn.apple.com/videos/streaming/examples/img_bipbop_adv_example_fmp4/a2/prog_index.m3u8',
|
'url': 'https://devstreaming-cdn.apple.com/videos/streaming/examples/img_bipbop_adv_example_fmp4/a2/prog_index.m3u8',
|
||||||
'manifest_url': 'https://devstreaming-cdn.apple.com/videos/streaming/examples/img_bipbop_adv_example_fmp4/master.m3u8',
|
'manifest_url': 'https://devstreaming-cdn.apple.com/videos/streaming/examples/img_bipbop_adv_example_fmp4/master.m3u8',
|
||||||
@ -666,7 +653,14 @@ jwplayer("mediaplayer").setup({"abouttext":"Visit Indie DB","aboutlink":"http:\/
|
|||||||
'ext': 'mp4',
|
'ext': 'mp4',
|
||||||
'protocol': 'm3u8_native',
|
'protocol': 'm3u8_native',
|
||||||
'audio_ext': 'mp4',
|
'audio_ext': 'mp4',
|
||||||
'source_preference': 2,
|
}, {
|
||||||
|
'format_id': 'aud3-English',
|
||||||
|
'url': 'https://devstreaming-cdn.apple.com/videos/streaming/examples/img_bipbop_adv_example_fmp4/a3/prog_index.m3u8',
|
||||||
|
'manifest_url': 'https://devstreaming-cdn.apple.com/videos/streaming/examples/img_bipbop_adv_example_fmp4/master.m3u8',
|
||||||
|
'language': 'en',
|
||||||
|
'ext': 'mp4',
|
||||||
|
'protocol': 'm3u8_native',
|
||||||
|
'audio_ext': 'mp4',
|
||||||
}, {
|
}, {
|
||||||
'format_id': '530',
|
'format_id': '530',
|
||||||
'url': 'https://devstreaming-cdn.apple.com/videos/streaming/examples/img_bipbop_adv_example_fmp4/v2/prog_index.m3u8',
|
'url': 'https://devstreaming-cdn.apple.com/videos/streaming/examples/img_bipbop_adv_example_fmp4/v2/prog_index.m3u8',
|
||||||
|
@ -6,8 +6,6 @@ import sys
|
|||||||
import unittest
|
import unittest
|
||||||
from unittest.mock import patch
|
from unittest.mock import patch
|
||||||
|
|
||||||
from yt_dlp.globals import all_plugins_loaded
|
|
||||||
|
|
||||||
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||||
|
|
||||||
|
|
||||||
@ -1429,12 +1427,6 @@ class TestYoutubeDL(unittest.TestCase):
|
|||||||
self.assertFalse(result.get('cookies'), msg='Cookies set in cookies field for wrong domain')
|
self.assertFalse(result.get('cookies'), msg='Cookies set in cookies field for wrong domain')
|
||||||
self.assertFalse(ydl.cookiejar.get_cookie_header(fmt['url']), msg='Cookies set in cookiejar for wrong domain')
|
self.assertFalse(ydl.cookiejar.get_cookie_header(fmt['url']), msg='Cookies set in cookiejar for wrong domain')
|
||||||
|
|
||||||
def test_load_plugins_compat(self):
|
|
||||||
# Should try to reload plugins if they haven't already been loaded
|
|
||||||
all_plugins_loaded.value = False
|
|
||||||
FakeYDL().close()
|
|
||||||
assert all_plugins_loaded.value
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
unittest.main()
|
unittest.main()
|
||||||
|
@ -331,6 +331,10 @@ class TestHTTPConnectProxy:
|
|||||||
assert proxy_info['proxy'] == server_address
|
assert proxy_info['proxy'] == server_address
|
||||||
assert 'Proxy-Authorization' in proxy_info['headers']
|
assert 'Proxy-Authorization' in proxy_info['headers']
|
||||||
|
|
||||||
|
@pytest.mark.skip_handler(
|
||||||
|
'Requests',
|
||||||
|
'bug in urllib3 causes unclosed socket: https://github.com/urllib3/urllib3/issues/3374',
|
||||||
|
)
|
||||||
def test_http_connect_bad_auth(self, handler, ctx):
|
def test_http_connect_bad_auth(self, handler, ctx):
|
||||||
with ctx.http_server(HTTPConnectProxyHandler, username='test', password='test') as server_address:
|
with ctx.http_server(HTTPConnectProxyHandler, username='test', password='test') as server_address:
|
||||||
with handler(verify=False, proxies={ctx.REQUEST_PROTO: f'http://test:bad@{server_address}'}) as rh:
|
with handler(verify=False, proxies={ctx.REQUEST_PROTO: f'http://test:bad@{server_address}'}) as rh:
|
||||||
|
@ -118,7 +118,6 @@ class TestJSInterpreter(unittest.TestCase):
|
|||||||
self._test('function f(){var x = 20; x = 30 + 1; return x;}', 31)
|
self._test('function f(){var x = 20; x = 30 + 1; return x;}', 31)
|
||||||
self._test('function f(){var x = 20; x += 30 + 1; return x;}', 51)
|
self._test('function f(){var x = 20; x += 30 + 1; return x;}', 51)
|
||||||
self._test('function f(){var x = 20; x -= 30 + 1; return x;}', -11)
|
self._test('function f(){var x = 20; x -= 30 + 1; return x;}', -11)
|
||||||
self._test('function f(){var x = 2; var y = ["a", "b"]; y[x%y["length"]]="z"; return y}', ['z', 'b'])
|
|
||||||
|
|
||||||
@unittest.skip('Not implemented')
|
@unittest.skip('Not implemented')
|
||||||
def test_comments(self):
|
def test_comments(self):
|
||||||
@ -385,7 +384,7 @@ class TestJSInterpreter(unittest.TestCase):
|
|||||||
@unittest.skip('Not implemented')
|
@unittest.skip('Not implemented')
|
||||||
def test_packed(self):
|
def test_packed(self):
|
||||||
jsi = JSInterpreter('''function f(p,a,c,k,e,d){while(c--)if(k[c])p=p.replace(new RegExp('\\b'+c.toString(a)+'\\b','g'),k[c]);return p}''')
|
jsi = JSInterpreter('''function f(p,a,c,k,e,d){while(c--)if(k[c])p=p.replace(new RegExp('\\b'+c.toString(a)+'\\b','g'),k[c]);return p}''')
|
||||||
self.assertEqual(jsi.call_function('f', '''h 7=g("1j");7.7h({7g:[{33:"w://7f-7e-7d-7c.v.7b/7a/79/78/77/76.74?t=73&s=2s&e=72&f=2t&71=70.0.0.1&6z=6y&6x=6w"}],6v:"w://32.v.u/6u.31",16:"r%",15:"r%",6t:"6s",6r:"",6q:"l",6p:"l",6o:"6n",6m:\'6l\',6k:"6j",9:[{33:"/2u?b=6i&n=50&6h=w://32.v.u/6g.31",6f:"6e"}],1y:{6d:1,6c:\'#6b\',6a:\'#69\',68:"67",66:30,65:r,},"64":{63:"%62 2m%m%61%5z%5y%5x.u%5w%5v%5u.2y%22 2k%m%1o%22 5t%m%1o%22 5s%m%1o%22 2j%m%5r%22 16%m%5q%22 15%m%5p%22 5o%2z%5n%5m%2z",5l:"w://v.u/d/1k/5k.2y",5j:[]},\'5i\':{"5h":"5g"},5f:"5e",5d:"w://v.u",5c:{},5b:l,1x:[0.25,0.50,0.75,1,1.25,1.5,2]});h 1m,1n,5a;h 59=0,58=0;h 7=g("1j");h 2x=0,57=0,56=0;$.55({54:{\'53-52\':\'2i-51\'}});7.j(\'4z\',6(x){c(5>0&&x.1l>=5&&1n!=1){1n=1;$(\'q.4y\').4x(\'4w\')}});7.j(\'13\',6(x){2x=x.1l});7.j(\'2g\',6(x){2w(x)});7.j(\'4v\',6(){$(\'q.2v\').4u()});6 2w(x){$(\'q.2v\').4t();c(1m)19;1m=1;17=0;c(4s.4r===l){17=1}$.4q(\'/2u?b=4p&2l=1k&4o=2t-4n-4m-2s-4l&4k=&4j=&4i=&17=\'+17,6(2r){$(\'#4h\').4g(2r)});$(\'.3-8-4f-4e:4d("4c")\').2h(6(e){2q();g().4b(0);g().4a(l)});6 2q(){h $14=$("<q />").2p({1l:"49",16:"r%",15:"r%",48:0,2n:0,2o:47,46:"45(10%, 10%, 10%, 0.4)","44-43":"42"});$("<41 />").2p({16:"60%",15:"60%",2o:40,"3z-2n":"3y"}).3x({\'2m\':\'/?b=3w&2l=1k\',\'2k\':\'0\',\'2j\':\'2i\'}).2f($14);$14.2h(6(){$(3v).3u();g().2g()});$14.2f($(\'#1j\'))}g().13(0);}6 3t(){h 9=7.1b(2e);2d.2c(9);c(9.n>1){1r(i=0;i<9.n;i++){c(9[i].1a==2e){2d.2c(\'!!=\'+i);7.1p(i)}}}}7.j(\'3s\',6(){g().1h("/2a/3r.29","3q 10 28",6(){g().13(g().27()+10)},"2b");$("q[26=2b]").23().21(\'.3-20-1z\');g().1h("/2a/3p.29","3o 10 28",6(){h 12=g().27()-10;c(12<0)12=0;g().13(12)},"24");$("q[26=24]").23().21(\'.3-20-1z\');});6 1i(){}7.j(\'3n\',6(){1i()});7.j(\'3m\',6(){1i()});7.j("k",6(y){h 9=7.1b();c(9.n<2)19;$(\'.3-8-3l-3k\').3j(6(){$(\'#3-8-a-k\').1e(\'3-8-a-z\');$(\'.3-a-k\').p(\'o-1f\',\'11\')});7.1h("/3i/3h.3g","3f 3e",6(){$(\'.3-1w\').3d(\'3-8-1v\');$(\'.3-8-1y, .3-8-1x\').p(\'o-1g\',\'11\');c($(\'.3-1w\').3c(\'3-8-1v\')){$(\'.3-a-k\').p(\'o-1g\',\'l\');$(\'.3-a-k\').p(\'o-1f\',\'l\');$(\'.3-8-a\').1e(\'3-8-a-z\');$(\'.3-8-a:1u\').3b(\'3-8-a-z\')}3a{$(\'.3-a-k\').p(\'o-1g\',\'11\');$(\'.3-a-k\').p(\'o-1f\',\'11\');$(\'.3-8-a:1u\').1e(\'3-8-a-z\')}},"39");7.j("38",6(y){1d.37(\'1c\',y.9[y.36].1a)});c(1d.1t(\'1c\')){35("1s(1d.1t(\'1c\'));",34)}});h 18;6 1s(1q){h 9=7.1b();c(9.n>1){1r(i=0;i<9.n;i++){c(9[i].1a==1q){c(i==18){19}18=i;7.1p(i)}}}}',36,270,'|||jw|||function|player|settings|tracks|submenu||if||||jwplayer|var||on|audioTracks|true|3D|length|aria|attr|div|100|||sx|filemoon|https||event|active||false|tt|seek|dd|height|width|adb|current_audio|return|name|getAudioTracks|default_audio|localStorage|removeClass|expanded|checked|addButton|callMeMaybe|vplayer|0fxcyc2ajhp1|position|vvplay|vvad|220|setCurrentAudioTrack|audio_name|for|audio_set|getItem|last|open|controls|playbackRates|captions|rewind|icon|insertAfter||detach|ff00||button|getPosition|sec|png|player8|ff11|log|console|track_name|appendTo|play|click|no|scrolling|frameborder|file_code|src|top|zIndex|css|showCCform|data|1662367683|383371|dl|video_ad|doPlay|prevt|mp4|3E||jpg|thumbs|file|300|setTimeout|currentTrack|setItem|audioTrackChanged|dualSound|else|addClass|hasClass|toggleClass|Track|Audio|svg|dualy|images|mousedown|buttons|topbar|playAttemptFailed|beforePlay|Rewind|fr|Forward|ff|ready|set_audio_track|remove|this|upload_srt|prop|50px|margin|1000001|iframe|center|align|text|rgba|background|1000000|left|absolute|pause|setCurrentCaptions|Upload|contains|item|content|html|fviews|referer|prem|embed|3e57249ef633e0d03bf76ceb8d8a4b65|216|83|hash|view|get|TokenZir|window|hide|show|complete|slow|fadeIn|video_ad_fadein|time||cache|Cache|Content|headers|ajaxSetup|v2done|tott|vastdone2|vastdone1|vvbefore|playbackRateControls|cast|aboutlink|FileMoon|abouttext|UHD|1870|qualityLabels|sites|GNOME_POWER|link|2Fiframe|3C|allowfullscreen|22360|22640|22no|marginheight|marginwidth|2FGNOME_POWER|2F0fxcyc2ajhp1|2Fe|2Ffilemoon|2F|3A||22https|3Ciframe|code|sharing|fontOpacity|backgroundOpacity|Tahoma|fontFamily|303030|backgroundColor|FFFFFF|color|userFontScale|thumbnails|kind|0fxcyc2ajhp10000|url|get_slides|start|startparam|none|preload|html5|primary|hlshtml|androidhls|duration|uniform|stretching|0fxcyc2ajhp1_xt|image|2048|sp|6871|asn|127|srv|43200|_g3XlBcu2lmD9oDexD2NLWSmah2Nu3XcDrl93m9PwXY|m3u8||master|0fxcyc2ajhp1_x|00076|01|hls2|to|s01|delivery|storage|moon|sources|setup'''.split('|'))) # noqa: SIM905
|
self.assertEqual(jsi.call_function('f', '''h 7=g("1j");7.7h({7g:[{33:"w://7f-7e-7d-7c.v.7b/7a/79/78/77/76.74?t=73&s=2s&e=72&f=2t&71=70.0.0.1&6z=6y&6x=6w"}],6v:"w://32.v.u/6u.31",16:"r%",15:"r%",6t:"6s",6r:"",6q:"l",6p:"l",6o:"6n",6m:\'6l\',6k:"6j",9:[{33:"/2u?b=6i&n=50&6h=w://32.v.u/6g.31",6f:"6e"}],1y:{6d:1,6c:\'#6b\',6a:\'#69\',68:"67",66:30,65:r,},"64":{63:"%62 2m%m%61%5z%5y%5x.u%5w%5v%5u.2y%22 2k%m%1o%22 5t%m%1o%22 5s%m%1o%22 2j%m%5r%22 16%m%5q%22 15%m%5p%22 5o%2z%5n%5m%2z",5l:"w://v.u/d/1k/5k.2y",5j:[]},\'5i\':{"5h":"5g"},5f:"5e",5d:"w://v.u",5c:{},5b:l,1x:[0.25,0.50,0.75,1,1.25,1.5,2]});h 1m,1n,5a;h 59=0,58=0;h 7=g("1j");h 2x=0,57=0,56=0;$.55({54:{\'53-52\':\'2i-51\'}});7.j(\'4z\',6(x){c(5>0&&x.1l>=5&&1n!=1){1n=1;$(\'q.4y\').4x(\'4w\')}});7.j(\'13\',6(x){2x=x.1l});7.j(\'2g\',6(x){2w(x)});7.j(\'4v\',6(){$(\'q.2v\').4u()});6 2w(x){$(\'q.2v\').4t();c(1m)19;1m=1;17=0;c(4s.4r===l){17=1}$.4q(\'/2u?b=4p&2l=1k&4o=2t-4n-4m-2s-4l&4k=&4j=&4i=&17=\'+17,6(2r){$(\'#4h\').4g(2r)});$(\'.3-8-4f-4e:4d("4c")\').2h(6(e){2q();g().4b(0);g().4a(l)});6 2q(){h $14=$("<q />").2p({1l:"49",16:"r%",15:"r%",48:0,2n:0,2o:47,46:"45(10%, 10%, 10%, 0.4)","44-43":"42"});$("<41 />").2p({16:"60%",15:"60%",2o:40,"3z-2n":"3y"}).3x({\'2m\':\'/?b=3w&2l=1k\',\'2k\':\'0\',\'2j\':\'2i\'}).2f($14);$14.2h(6(){$(3v).3u();g().2g()});$14.2f($(\'#1j\'))}g().13(0);}6 3t(){h 9=7.1b(2e);2d.2c(9);c(9.n>1){1r(i=0;i<9.n;i++){c(9[i].1a==2e){2d.2c(\'!!=\'+i);7.1p(i)}}}}7.j(\'3s\',6(){g().1h("/2a/3r.29","3q 10 28",6(){g().13(g().27()+10)},"2b");$("q[26=2b]").23().21(\'.3-20-1z\');g().1h("/2a/3p.29","3o 10 28",6(){h 12=g().27()-10;c(12<0)12=0;g().13(12)},"24");$("q[26=24]").23().21(\'.3-20-1z\');});6 1i(){}7.j(\'3n\',6(){1i()});7.j(\'3m\',6(){1i()});7.j("k",6(y){h 9=7.1b();c(9.n<2)19;$(\'.3-8-3l-3k\').3j(6(){$(\'#3-8-a-k\').1e(\'3-8-a-z\');$(\'.3-a-k\').p(\'o-1f\',\'11\')});7.1h("/3i/3h.3g","3f 3e",6(){$(\'.3-1w\').3d(\'3-8-1v\');$(\'.3-8-1y, .3-8-1x\').p(\'o-1g\',\'11\');c($(\'.3-1w\').3c(\'3-8-1v\')){$(\'.3-a-k\').p(\'o-1g\',\'l\');$(\'.3-a-k\').p(\'o-1f\',\'l\');$(\'.3-8-a\').1e(\'3-8-a-z\');$(\'.3-8-a:1u\').3b(\'3-8-a-z\')}3a{$(\'.3-a-k\').p(\'o-1g\',\'11\');$(\'.3-a-k\').p(\'o-1f\',\'11\');$(\'.3-8-a:1u\').1e(\'3-8-a-z\')}},"39");7.j("38",6(y){1d.37(\'1c\',y.9[y.36].1a)});c(1d.1t(\'1c\')){35("1s(1d.1t(\'1c\'));",34)}});h 18;6 1s(1q){h 9=7.1b();c(9.n>1){1r(i=0;i<9.n;i++){c(9[i].1a==1q){c(i==18){19}18=i;7.1p(i)}}}}',36,270,'|||jw|||function|player|settings|tracks|submenu||if||||jwplayer|var||on|audioTracks|true|3D|length|aria|attr|div|100|||sx|filemoon|https||event|active||false|tt|seek|dd|height|width|adb|current_audio|return|name|getAudioTracks|default_audio|localStorage|removeClass|expanded|checked|addButton|callMeMaybe|vplayer|0fxcyc2ajhp1|position|vvplay|vvad|220|setCurrentAudioTrack|audio_name|for|audio_set|getItem|last|open|controls|playbackRates|captions|rewind|icon|insertAfter||detach|ff00||button|getPosition|sec|png|player8|ff11|log|console|track_name|appendTo|play|click|no|scrolling|frameborder|file_code|src|top|zIndex|css|showCCform|data|1662367683|383371|dl|video_ad|doPlay|prevt|mp4|3E||jpg|thumbs|file|300|setTimeout|currentTrack|setItem|audioTrackChanged|dualSound|else|addClass|hasClass|toggleClass|Track|Audio|svg|dualy|images|mousedown|buttons|topbar|playAttemptFailed|beforePlay|Rewind|fr|Forward|ff|ready|set_audio_track|remove|this|upload_srt|prop|50px|margin|1000001|iframe|center|align|text|rgba|background|1000000|left|absolute|pause|setCurrentCaptions|Upload|contains|item|content|html|fviews|referer|prem|embed|3e57249ef633e0d03bf76ceb8d8a4b65|216|83|hash|view|get|TokenZir|window|hide|show|complete|slow|fadeIn|video_ad_fadein|time||cache|Cache|Content|headers|ajaxSetup|v2done|tott|vastdone2|vastdone1|vvbefore|playbackRateControls|cast|aboutlink|FileMoon|abouttext|UHD|1870|qualityLabels|sites|GNOME_POWER|link|2Fiframe|3C|allowfullscreen|22360|22640|22no|marginheight|marginwidth|2FGNOME_POWER|2F0fxcyc2ajhp1|2Fe|2Ffilemoon|2F|3A||22https|3Ciframe|code|sharing|fontOpacity|backgroundOpacity|Tahoma|fontFamily|303030|backgroundColor|FFFFFF|color|userFontScale|thumbnails|kind|0fxcyc2ajhp10000|url|get_slides|start|startparam|none|preload|html5|primary|hlshtml|androidhls|duration|uniform|stretching|0fxcyc2ajhp1_xt|image|2048|sp|6871|asn|127|srv|43200|_g3XlBcu2lmD9oDexD2NLWSmah2Nu3XcDrl93m9PwXY|m3u8||master|0fxcyc2ajhp1_x|00076|01|hls2|to|s01|delivery|storage|moon|sources|setup'''.split('|')))
|
||||||
|
|
||||||
def test_join(self):
|
def test_join(self):
|
||||||
test_input = list('test')
|
test_input = list('test')
|
||||||
@ -404,8 +403,6 @@ class TestJSInterpreter(unittest.TestCase):
|
|||||||
test_result = list('test')
|
test_result = list('test')
|
||||||
tests = [
|
tests = [
|
||||||
'function f(a, b){return a.split(b)}',
|
'function f(a, b){return a.split(b)}',
|
||||||
'function f(a, b){return a["split"](b)}',
|
|
||||||
'function f(a, b){let x = ["split"]; return a[x[0]](b)}',
|
|
||||||
'function f(a, b){return String.prototype.split.call(a, b)}',
|
'function f(a, b){return String.prototype.split.call(a, b)}',
|
||||||
'function f(a, b){return String.prototype.split.apply(a, [b])}',
|
'function f(a, b){return String.prototype.split.apply(a, [b])}',
|
||||||
]
|
]
|
||||||
@ -444,9 +441,6 @@ class TestJSInterpreter(unittest.TestCase):
|
|||||||
self._test('function f(){return "012345678".slice(-1, 1)}', '')
|
self._test('function f(){return "012345678".slice(-1, 1)}', '')
|
||||||
self._test('function f(){return "012345678".slice(-3, -1)}', '67')
|
self._test('function f(){return "012345678".slice(-3, -1)}', '67')
|
||||||
|
|
||||||
def test_splice(self):
|
|
||||||
self._test('function f(){var T = ["0", "1", "2"]; T["splice"](2, 1, "0")[0]; return T }', ['0', '1', '0'])
|
|
||||||
|
|
||||||
def test_js_number_to_string(self):
|
def test_js_number_to_string(self):
|
||||||
for test, radix, expected in [
|
for test, radix, expected in [
|
||||||
(0, None, '0'),
|
(0, None, '0'),
|
||||||
@ -468,16 +462,6 @@ class TestJSInterpreter(unittest.TestCase):
|
|||||||
]:
|
]:
|
||||||
assert js_number_to_string(test, radix) == expected
|
assert js_number_to_string(test, radix) == expected
|
||||||
|
|
||||||
def test_extract_function(self):
|
|
||||||
jsi = JSInterpreter('function a(b) { return b + 1; }')
|
|
||||||
func = jsi.extract_function('a')
|
|
||||||
self.assertEqual(func([2]), 3)
|
|
||||||
|
|
||||||
def test_extract_function_with_global_stack(self):
|
|
||||||
jsi = JSInterpreter('function c(d) { return d + e + f + g; }')
|
|
||||||
func = jsi.extract_function('c', {'e': 10}, {'f': 100, 'g': 1000})
|
|
||||||
self.assertEqual(func([1]), 1111)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
unittest.main()
|
unittest.main()
|
||||||
|
@ -39,7 +39,6 @@ from yt_dlp.cookies import YoutubeDLCookieJar
|
|||||||
from yt_dlp.dependencies import brotli, curl_cffi, requests, urllib3
|
from yt_dlp.dependencies import brotli, curl_cffi, requests, urllib3
|
||||||
from yt_dlp.networking import (
|
from yt_dlp.networking import (
|
||||||
HEADRequest,
|
HEADRequest,
|
||||||
PATCHRequest,
|
|
||||||
PUTRequest,
|
PUTRequest,
|
||||||
Request,
|
Request,
|
||||||
RequestDirector,
|
RequestDirector,
|
||||||
@ -615,6 +614,7 @@ class TestHTTPRequestHandler(TestRequestHandlerBase):
|
|||||||
rh, Request(f'http://127.0.0.1:{self.http_port}/source_address')).read().decode()
|
rh, Request(f'http://127.0.0.1:{self.http_port}/source_address')).read().decode()
|
||||||
assert source_address == data
|
assert source_address == data
|
||||||
|
|
||||||
|
# Not supported by CurlCFFI
|
||||||
@pytest.mark.skip_handler('CurlCFFI', 'not supported by curl-cffi')
|
@pytest.mark.skip_handler('CurlCFFI', 'not supported by curl-cffi')
|
||||||
def test_gzip_trailing_garbage(self, handler):
|
def test_gzip_trailing_garbage(self, handler):
|
||||||
with handler() as rh:
|
with handler() as rh:
|
||||||
@ -720,15 +720,6 @@ class TestHTTPRequestHandler(TestRequestHandlerBase):
|
|||||||
rh, Request(
|
rh, Request(
|
||||||
f'http://127.0.0.1:{self.http_port}/headers', proxies={'all': 'http://10.255.255.255'})).close()
|
f'http://127.0.0.1:{self.http_port}/headers', proxies={'all': 'http://10.255.255.255'})).close()
|
||||||
|
|
||||||
@pytest.mark.skip_handlers_if(lambda _, handler: handler not in ['Urllib', 'CurlCFFI'], 'handler does not support keep_header_casing')
|
|
||||||
def test_keep_header_casing(self, handler):
|
|
||||||
with handler() as rh:
|
|
||||||
res = validate_and_send(
|
|
||||||
rh, Request(
|
|
||||||
f'http://127.0.0.1:{self.http_port}/headers', headers={'X-test-heaDer': 'test'}, extensions={'keep_header_casing': True})).read().decode()
|
|
||||||
|
|
||||||
assert 'X-test-heaDer: test' in res
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize('handler', ['Urllib', 'Requests', 'CurlCFFI'], indirect=True)
|
@pytest.mark.parametrize('handler', ['Urllib', 'Requests', 'CurlCFFI'], indirect=True)
|
||||||
class TestClientCertificate:
|
class TestClientCertificate:
|
||||||
@ -1298,7 +1289,6 @@ class TestRequestHandlerValidation:
|
|||||||
({'legacy_ssl': False}, False),
|
({'legacy_ssl': False}, False),
|
||||||
({'legacy_ssl': True}, False),
|
({'legacy_ssl': True}, False),
|
||||||
({'legacy_ssl': 'notabool'}, AssertionError),
|
({'legacy_ssl': 'notabool'}, AssertionError),
|
||||||
({'keep_header_casing': True}, UnsupportedRequest),
|
|
||||||
]),
|
]),
|
||||||
('Requests', 'http', [
|
('Requests', 'http', [
|
||||||
({'cookiejar': 'notacookiejar'}, AssertionError),
|
({'cookiejar': 'notacookiejar'}, AssertionError),
|
||||||
@ -1309,9 +1299,6 @@ class TestRequestHandlerValidation:
|
|||||||
({'legacy_ssl': False}, False),
|
({'legacy_ssl': False}, False),
|
||||||
({'legacy_ssl': True}, False),
|
({'legacy_ssl': True}, False),
|
||||||
({'legacy_ssl': 'notabool'}, AssertionError),
|
({'legacy_ssl': 'notabool'}, AssertionError),
|
||||||
({'keep_header_casing': False}, False),
|
|
||||||
({'keep_header_casing': True}, False),
|
|
||||||
({'keep_header_casing': 'notabool'}, AssertionError),
|
|
||||||
]),
|
]),
|
||||||
('CurlCFFI', 'http', [
|
('CurlCFFI', 'http', [
|
||||||
({'cookiejar': 'notacookiejar'}, AssertionError),
|
({'cookiejar': 'notacookiejar'}, AssertionError),
|
||||||
@ -1857,7 +1844,6 @@ class TestRequest:
|
|||||||
|
|
||||||
def test_request_helpers(self):
|
def test_request_helpers(self):
|
||||||
assert HEADRequest('http://example.com').method == 'HEAD'
|
assert HEADRequest('http://example.com').method == 'HEAD'
|
||||||
assert PATCHRequest('http://example.com').method == 'PATCH'
|
|
||||||
assert PUTRequest('http://example.com').method == 'PUT'
|
assert PUTRequest('http://example.com').method == 'PUT'
|
||||||
|
|
||||||
def test_headers(self):
|
def test_headers(self):
|
||||||
|
@ -10,71 +10,22 @@ TEST_DATA_DIR = Path(os.path.dirname(os.path.abspath(__file__)), 'testdata')
|
|||||||
sys.path.append(str(TEST_DATA_DIR))
|
sys.path.append(str(TEST_DATA_DIR))
|
||||||
importlib.invalidate_caches()
|
importlib.invalidate_caches()
|
||||||
|
|
||||||
from yt_dlp.plugins import (
|
from yt_dlp.utils import Config
|
||||||
PACKAGE_NAME,
|
from yt_dlp.plugins import PACKAGE_NAME, directories, load_plugins
|
||||||
PluginSpec,
|
|
||||||
directories,
|
|
||||||
load_plugins,
|
|
||||||
load_all_plugins,
|
|
||||||
register_plugin_spec,
|
|
||||||
)
|
|
||||||
|
|
||||||
from yt_dlp.globals import (
|
|
||||||
extractors,
|
|
||||||
postprocessors,
|
|
||||||
plugin_dirs,
|
|
||||||
plugin_ies,
|
|
||||||
plugin_pps,
|
|
||||||
all_plugins_loaded,
|
|
||||||
plugin_specs,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
EXTRACTOR_PLUGIN_SPEC = PluginSpec(
|
|
||||||
module_name='extractor',
|
|
||||||
suffix='IE',
|
|
||||||
destination=extractors,
|
|
||||||
plugin_destination=plugin_ies,
|
|
||||||
)
|
|
||||||
|
|
||||||
POSTPROCESSOR_PLUGIN_SPEC = PluginSpec(
|
|
||||||
module_name='postprocessor',
|
|
||||||
suffix='PP',
|
|
||||||
destination=postprocessors,
|
|
||||||
plugin_destination=plugin_pps,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def reset_plugins():
|
|
||||||
plugin_ies.value = {}
|
|
||||||
plugin_pps.value = {}
|
|
||||||
plugin_dirs.value = ['default']
|
|
||||||
plugin_specs.value = {}
|
|
||||||
all_plugins_loaded.value = False
|
|
||||||
# Clearing override plugins is probably difficult
|
|
||||||
for module_name in tuple(sys.modules):
|
|
||||||
for plugin_type in ('extractor', 'postprocessor'):
|
|
||||||
if module_name.startswith(f'{PACKAGE_NAME}.{plugin_type}.'):
|
|
||||||
del sys.modules[module_name]
|
|
||||||
|
|
||||||
importlib.invalidate_caches()
|
|
||||||
|
|
||||||
|
|
||||||
class TestPlugins(unittest.TestCase):
|
class TestPlugins(unittest.TestCase):
|
||||||
|
|
||||||
TEST_PLUGIN_DIR = TEST_DATA_DIR / PACKAGE_NAME
|
TEST_PLUGIN_DIR = TEST_DATA_DIR / PACKAGE_NAME
|
||||||
|
|
||||||
def setUp(self):
|
|
||||||
reset_plugins()
|
|
||||||
|
|
||||||
def tearDown(self):
|
|
||||||
reset_plugins()
|
|
||||||
|
|
||||||
def test_directories_containing_plugins(self):
|
def test_directories_containing_plugins(self):
|
||||||
self.assertIn(self.TEST_PLUGIN_DIR, map(Path, directories()))
|
self.assertIn(self.TEST_PLUGIN_DIR, map(Path, directories()))
|
||||||
|
|
||||||
def test_extractor_classes(self):
|
def test_extractor_classes(self):
|
||||||
plugins_ie = load_plugins(EXTRACTOR_PLUGIN_SPEC)
|
for module_name in tuple(sys.modules):
|
||||||
|
if module_name.startswith(f'{PACKAGE_NAME}.extractor'):
|
||||||
|
del sys.modules[module_name]
|
||||||
|
plugins_ie = load_plugins('extractor', 'IE')
|
||||||
|
|
||||||
self.assertIn(f'{PACKAGE_NAME}.extractor.normal', sys.modules.keys())
|
self.assertIn(f'{PACKAGE_NAME}.extractor.normal', sys.modules.keys())
|
||||||
self.assertIn('NormalPluginIE', plugins_ie.keys())
|
self.assertIn('NormalPluginIE', plugins_ie.keys())
|
||||||
@ -84,29 +35,17 @@ class TestPlugins(unittest.TestCase):
|
|||||||
f'{PACKAGE_NAME}.extractor._ignore' in sys.modules,
|
f'{PACKAGE_NAME}.extractor._ignore' in sys.modules,
|
||||||
'loaded module beginning with underscore')
|
'loaded module beginning with underscore')
|
||||||
self.assertNotIn('IgnorePluginIE', plugins_ie.keys())
|
self.assertNotIn('IgnorePluginIE', plugins_ie.keys())
|
||||||
self.assertNotIn('IgnorePluginIE', plugin_ies.value)
|
|
||||||
|
|
||||||
# Don't load extractors with underscore prefix
|
# Don't load extractors with underscore prefix
|
||||||
self.assertNotIn('_IgnoreUnderscorePluginIE', plugins_ie.keys())
|
self.assertNotIn('_IgnoreUnderscorePluginIE', plugins_ie.keys())
|
||||||
self.assertNotIn('_IgnoreUnderscorePluginIE', plugin_ies.value)
|
|
||||||
|
|
||||||
# Don't load extractors not specified in __all__ (if supplied)
|
# Don't load extractors not specified in __all__ (if supplied)
|
||||||
self.assertNotIn('IgnoreNotInAllPluginIE', plugins_ie.keys())
|
self.assertNotIn('IgnoreNotInAllPluginIE', plugins_ie.keys())
|
||||||
self.assertNotIn('IgnoreNotInAllPluginIE', plugin_ies.value)
|
|
||||||
self.assertIn('InAllPluginIE', plugins_ie.keys())
|
self.assertIn('InAllPluginIE', plugins_ie.keys())
|
||||||
self.assertIn('InAllPluginIE', plugin_ies.value)
|
|
||||||
|
|
||||||
# Don't load override extractors
|
|
||||||
self.assertNotIn('OverrideGenericIE', plugins_ie.keys())
|
|
||||||
self.assertNotIn('OverrideGenericIE', plugin_ies.value)
|
|
||||||
self.assertNotIn('_UnderscoreOverrideGenericIE', plugins_ie.keys())
|
|
||||||
self.assertNotIn('_UnderscoreOverrideGenericIE', plugin_ies.value)
|
|
||||||
|
|
||||||
def test_postprocessor_classes(self):
|
def test_postprocessor_classes(self):
|
||||||
plugins_pp = load_plugins(POSTPROCESSOR_PLUGIN_SPEC)
|
plugins_pp = load_plugins('postprocessor', 'PP')
|
||||||
self.assertIn('NormalPluginPP', plugins_pp.keys())
|
self.assertIn('NormalPluginPP', plugins_pp.keys())
|
||||||
self.assertIn(f'{PACKAGE_NAME}.postprocessor.normal', sys.modules.keys())
|
|
||||||
self.assertIn('NormalPluginPP', plugin_pps.value)
|
|
||||||
|
|
||||||
def test_importing_zipped_module(self):
|
def test_importing_zipped_module(self):
|
||||||
zip_path = TEST_DATA_DIR / 'zipped_plugins.zip'
|
zip_path = TEST_DATA_DIR / 'zipped_plugins.zip'
|
||||||
@ -119,10 +58,10 @@ class TestPlugins(unittest.TestCase):
|
|||||||
package = importlib.import_module(f'{PACKAGE_NAME}.{plugin_type}')
|
package = importlib.import_module(f'{PACKAGE_NAME}.{plugin_type}')
|
||||||
self.assertIn(zip_path / PACKAGE_NAME / plugin_type, map(Path, package.__path__))
|
self.assertIn(zip_path / PACKAGE_NAME / plugin_type, map(Path, package.__path__))
|
||||||
|
|
||||||
plugins_ie = load_plugins(EXTRACTOR_PLUGIN_SPEC)
|
plugins_ie = load_plugins('extractor', 'IE')
|
||||||
self.assertIn('ZippedPluginIE', plugins_ie.keys())
|
self.assertIn('ZippedPluginIE', plugins_ie.keys())
|
||||||
|
|
||||||
plugins_pp = load_plugins(POSTPROCESSOR_PLUGIN_SPEC)
|
plugins_pp = load_plugins('postprocessor', 'PP')
|
||||||
self.assertIn('ZippedPluginPP', plugins_pp.keys())
|
self.assertIn('ZippedPluginPP', plugins_pp.keys())
|
||||||
|
|
||||||
finally:
|
finally:
|
||||||
@ -130,116 +69,23 @@ class TestPlugins(unittest.TestCase):
|
|||||||
os.remove(zip_path)
|
os.remove(zip_path)
|
||||||
importlib.invalidate_caches() # reset the import caches
|
importlib.invalidate_caches() # reset the import caches
|
||||||
|
|
||||||
def test_reloading_plugins(self):
|
def test_plugin_dirs(self):
|
||||||
reload_plugins_path = TEST_DATA_DIR / 'reload_plugins'
|
# Internal plugin dirs hack for CLI --plugin-dirs
|
||||||
load_plugins(EXTRACTOR_PLUGIN_SPEC)
|
# To be replaced with proper system later
|
||||||
load_plugins(POSTPROCESSOR_PLUGIN_SPEC)
|
custom_plugin_dir = TEST_DATA_DIR / 'plugin_packages'
|
||||||
|
Config._plugin_dirs = [str(custom_plugin_dir)]
|
||||||
|
importlib.invalidate_caches() # reset the import caches
|
||||||
|
|
||||||
# Remove default folder and add reload_plugin path
|
|
||||||
sys.path.remove(str(TEST_DATA_DIR))
|
|
||||||
sys.path.append(str(reload_plugins_path))
|
|
||||||
importlib.invalidate_caches()
|
|
||||||
try:
|
try:
|
||||||
for plugin_type in ('extractor', 'postprocessor'):
|
package = importlib.import_module(f'{PACKAGE_NAME}.extractor')
|
||||||
package = importlib.import_module(f'{PACKAGE_NAME}.{plugin_type}')
|
self.assertIn(custom_plugin_dir / 'testpackage' / PACKAGE_NAME / 'extractor', map(Path, package.__path__))
|
||||||
self.assertIn(reload_plugins_path / PACKAGE_NAME / plugin_type, map(Path, package.__path__))
|
|
||||||
|
|
||||||
plugins_ie = load_plugins(EXTRACTOR_PLUGIN_SPEC)
|
plugins_ie = load_plugins('extractor', 'IE')
|
||||||
self.assertIn('NormalPluginIE', plugins_ie.keys())
|
self.assertIn('PackagePluginIE', plugins_ie.keys())
|
||||||
self.assertTrue(
|
|
||||||
plugins_ie['NormalPluginIE'].REPLACED,
|
|
||||||
msg='Reloading has not replaced original extractor plugin')
|
|
||||||
self.assertTrue(
|
|
||||||
extractors.value['NormalPluginIE'].REPLACED,
|
|
||||||
msg='Reloading has not replaced original extractor plugin globally')
|
|
||||||
|
|
||||||
plugins_pp = load_plugins(POSTPROCESSOR_PLUGIN_SPEC)
|
|
||||||
self.assertIn('NormalPluginPP', plugins_pp.keys())
|
|
||||||
self.assertTrue(plugins_pp['NormalPluginPP'].REPLACED,
|
|
||||||
msg='Reloading has not replaced original postprocessor plugin')
|
|
||||||
self.assertTrue(
|
|
||||||
postprocessors.value['NormalPluginPP'].REPLACED,
|
|
||||||
msg='Reloading has not replaced original postprocessor plugin globally')
|
|
||||||
|
|
||||||
finally:
|
finally:
|
||||||
sys.path.remove(str(reload_plugins_path))
|
Config._plugin_dirs = []
|
||||||
sys.path.append(str(TEST_DATA_DIR))
|
importlib.invalidate_caches() # reset the import caches
|
||||||
importlib.invalidate_caches()
|
|
||||||
|
|
||||||
def test_extractor_override_plugin(self):
|
|
||||||
load_plugins(EXTRACTOR_PLUGIN_SPEC)
|
|
||||||
|
|
||||||
from yt_dlp.extractor.generic import GenericIE
|
|
||||||
|
|
||||||
self.assertEqual(GenericIE.TEST_FIELD, 'override')
|
|
||||||
self.assertEqual(GenericIE.SECONDARY_TEST_FIELD, 'underscore-override')
|
|
||||||
|
|
||||||
self.assertEqual(GenericIE.IE_NAME, 'generic+override+underscore-override')
|
|
||||||
importlib.invalidate_caches()
|
|
||||||
# test that loading a second time doesn't wrap a second time
|
|
||||||
load_plugins(EXTRACTOR_PLUGIN_SPEC)
|
|
||||||
from yt_dlp.extractor.generic import GenericIE
|
|
||||||
self.assertEqual(GenericIE.IE_NAME, 'generic+override+underscore-override')
|
|
||||||
|
|
||||||
def test_load_all_plugin_types(self):
|
|
||||||
|
|
||||||
# no plugin specs registered
|
|
||||||
load_all_plugins()
|
|
||||||
|
|
||||||
self.assertNotIn(f'{PACKAGE_NAME}.extractor.normal', sys.modules.keys())
|
|
||||||
self.assertNotIn(f'{PACKAGE_NAME}.postprocessor.normal', sys.modules.keys())
|
|
||||||
|
|
||||||
register_plugin_spec(EXTRACTOR_PLUGIN_SPEC)
|
|
||||||
register_plugin_spec(POSTPROCESSOR_PLUGIN_SPEC)
|
|
||||||
load_all_plugins()
|
|
||||||
self.assertTrue(all_plugins_loaded.value)
|
|
||||||
|
|
||||||
self.assertIn(f'{PACKAGE_NAME}.extractor.normal', sys.modules.keys())
|
|
||||||
self.assertIn(f'{PACKAGE_NAME}.postprocessor.normal', sys.modules.keys())
|
|
||||||
|
|
||||||
def test_no_plugin_dirs(self):
|
|
||||||
register_plugin_spec(EXTRACTOR_PLUGIN_SPEC)
|
|
||||||
register_plugin_spec(POSTPROCESSOR_PLUGIN_SPEC)
|
|
||||||
|
|
||||||
plugin_dirs.value = []
|
|
||||||
load_all_plugins()
|
|
||||||
|
|
||||||
self.assertNotIn(f'{PACKAGE_NAME}.extractor.normal', sys.modules.keys())
|
|
||||||
self.assertNotIn(f'{PACKAGE_NAME}.postprocessor.normal', sys.modules.keys())
|
|
||||||
|
|
||||||
def test_set_plugin_dirs(self):
|
|
||||||
custom_plugin_dir = str(TEST_DATA_DIR / 'plugin_packages')
|
|
||||||
plugin_dirs.value = [custom_plugin_dir]
|
|
||||||
|
|
||||||
load_plugins(EXTRACTOR_PLUGIN_SPEC)
|
|
||||||
|
|
||||||
self.assertIn(f'{PACKAGE_NAME}.extractor.package', sys.modules.keys())
|
|
||||||
self.assertIn('PackagePluginIE', plugin_ies.value)
|
|
||||||
|
|
||||||
def test_invalid_plugin_dir(self):
|
|
||||||
plugin_dirs.value = ['invalid_dir']
|
|
||||||
with self.assertRaises(ValueError):
|
|
||||||
load_plugins(EXTRACTOR_PLUGIN_SPEC)
|
|
||||||
|
|
||||||
def test_append_plugin_dirs(self):
|
|
||||||
custom_plugin_dir = str(TEST_DATA_DIR / 'plugin_packages')
|
|
||||||
|
|
||||||
self.assertEqual(plugin_dirs.value, ['default'])
|
|
||||||
plugin_dirs.value.append(custom_plugin_dir)
|
|
||||||
self.assertEqual(plugin_dirs.value, ['default', custom_plugin_dir])
|
|
||||||
|
|
||||||
load_plugins(EXTRACTOR_PLUGIN_SPEC)
|
|
||||||
|
|
||||||
self.assertIn(f'{PACKAGE_NAME}.extractor.package', sys.modules.keys())
|
|
||||||
self.assertIn('PackagePluginIE', plugin_ies.value)
|
|
||||||
|
|
||||||
def test_get_plugin_spec(self):
|
|
||||||
register_plugin_spec(EXTRACTOR_PLUGIN_SPEC)
|
|
||||||
register_plugin_spec(POSTPROCESSOR_PLUGIN_SPEC)
|
|
||||||
|
|
||||||
self.assertEqual(plugin_specs.value.get('extractor'), EXTRACTOR_PLUGIN_SPEC)
|
|
||||||
self.assertEqual(plugin_specs.value.get('postprocessor'), POSTPROCESSOR_PLUGIN_SPEC)
|
|
||||||
self.assertIsNone(plugin_specs.value.get('invalid'))
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
|
@ -23,6 +23,7 @@ from yt_dlp.extractor import (
|
|||||||
TedTalkIE,
|
TedTalkIE,
|
||||||
ThePlatformFeedIE,
|
ThePlatformFeedIE,
|
||||||
ThePlatformIE,
|
ThePlatformIE,
|
||||||
|
VikiIE,
|
||||||
VimeoIE,
|
VimeoIE,
|
||||||
WallaIE,
|
WallaIE,
|
||||||
YoutubeIE,
|
YoutubeIE,
|
||||||
@ -330,6 +331,20 @@ class TestRaiPlaySubtitles(BaseTestSubtitles):
|
|||||||
self.assertEqual(md5(subtitles['it']), '4b3264186fbb103508abe5311cfcb9cd')
|
self.assertEqual(md5(subtitles['it']), '4b3264186fbb103508abe5311cfcb9cd')
|
||||||
|
|
||||||
|
|
||||||
|
@is_download_test
|
||||||
|
@unittest.skip('IE broken - DRM only')
|
||||||
|
class TestVikiSubtitles(BaseTestSubtitles):
|
||||||
|
url = 'http://www.viki.com/videos/1060846v-punch-episode-18'
|
||||||
|
IE = VikiIE
|
||||||
|
|
||||||
|
def test_allsubtitles(self):
|
||||||
|
self.DL.params['writesubtitles'] = True
|
||||||
|
self.DL.params['allsubtitles'] = True
|
||||||
|
subtitles = self.getSubtitles()
|
||||||
|
self.assertEqual(set(subtitles.keys()), {'en'})
|
||||||
|
self.assertEqual(md5(subtitles['en']), '53cb083a5914b2d84ef1ab67b880d18a')
|
||||||
|
|
||||||
|
|
||||||
@is_download_test
|
@is_download_test
|
||||||
class TestThePlatformSubtitles(BaseTestSubtitles):
|
class TestThePlatformSubtitles(BaseTestSubtitles):
|
||||||
# from http://www.3playmedia.com/services-features/tools/integrations/theplatform/
|
# from http://www.3playmedia.com/services-features/tools/integrations/theplatform/
|
||||||
|
@ -3,20 +3,19 @@
|
|||||||
# Allow direct execution
|
# Allow direct execution
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
|
import unittest
|
||||||
|
import unittest.mock
|
||||||
|
import warnings
|
||||||
|
import datetime as dt
|
||||||
|
|
||||||
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||||
|
|
||||||
|
|
||||||
import contextlib
|
import contextlib
|
||||||
import datetime as dt
|
|
||||||
import io
|
import io
|
||||||
import itertools
|
import itertools
|
||||||
import json
|
import json
|
||||||
import pickle
|
|
||||||
import subprocess
|
import subprocess
|
||||||
import unittest
|
|
||||||
import unittest.mock
|
|
||||||
import warnings
|
|
||||||
import xml.etree.ElementTree
|
import xml.etree.ElementTree
|
||||||
|
|
||||||
from yt_dlp.compat import (
|
from yt_dlp.compat import (
|
||||||
@ -219,8 +218,11 @@ class TestUtil(unittest.TestCase):
|
|||||||
self.assertEqual(sanitize_filename('_BD_eEpuzXw', is_id=True), '_BD_eEpuzXw')
|
self.assertEqual(sanitize_filename('_BD_eEpuzXw', is_id=True), '_BD_eEpuzXw')
|
||||||
self.assertEqual(sanitize_filename('N0Y__7-UOdI', is_id=True), 'N0Y__7-UOdI')
|
self.assertEqual(sanitize_filename('N0Y__7-UOdI', is_id=True), 'N0Y__7-UOdI')
|
||||||
|
|
||||||
@unittest.mock.patch('sys.platform', 'win32')
|
|
||||||
def test_sanitize_path(self):
|
def test_sanitize_path(self):
|
||||||
|
with unittest.mock.patch('sys.platform', 'win32'):
|
||||||
|
self._test_sanitize_path()
|
||||||
|
|
||||||
|
def _test_sanitize_path(self):
|
||||||
self.assertEqual(sanitize_path('abc'), 'abc')
|
self.assertEqual(sanitize_path('abc'), 'abc')
|
||||||
self.assertEqual(sanitize_path('abc/def'), 'abc\\def')
|
self.assertEqual(sanitize_path('abc/def'), 'abc\\def')
|
||||||
self.assertEqual(sanitize_path('abc\\def'), 'abc\\def')
|
self.assertEqual(sanitize_path('abc\\def'), 'abc\\def')
|
||||||
@ -251,8 +253,10 @@ class TestUtil(unittest.TestCase):
|
|||||||
|
|
||||||
# Check with nt._path_normpath if available
|
# Check with nt._path_normpath if available
|
||||||
try:
|
try:
|
||||||
from nt import _path_normpath as nt_path_normpath
|
import nt
|
||||||
except ImportError:
|
|
||||||
|
nt_path_normpath = getattr(nt, '_path_normpath', None)
|
||||||
|
except Exception:
|
||||||
nt_path_normpath = None
|
nt_path_normpath = None
|
||||||
|
|
||||||
for test, expected in [
|
for test, expected in [
|
||||||
@ -659,8 +663,6 @@ class TestUtil(unittest.TestCase):
|
|||||||
self.assertEqual(url_or_none('mms://foo.de'), 'mms://foo.de')
|
self.assertEqual(url_or_none('mms://foo.de'), 'mms://foo.de')
|
||||||
self.assertEqual(url_or_none('rtspu://foo.de'), 'rtspu://foo.de')
|
self.assertEqual(url_or_none('rtspu://foo.de'), 'rtspu://foo.de')
|
||||||
self.assertEqual(url_or_none('ftps://foo.de'), 'ftps://foo.de')
|
self.assertEqual(url_or_none('ftps://foo.de'), 'ftps://foo.de')
|
||||||
self.assertEqual(url_or_none('ws://foo.de'), 'ws://foo.de')
|
|
||||||
self.assertEqual(url_or_none('wss://foo.de'), 'wss://foo.de')
|
|
||||||
|
|
||||||
def test_parse_age_limit(self):
|
def test_parse_age_limit(self):
|
||||||
self.assertEqual(parse_age_limit(None), None)
|
self.assertEqual(parse_age_limit(None), None)
|
||||||
@ -1262,7 +1264,6 @@ class TestUtil(unittest.TestCase):
|
|||||||
def test_js_to_json_malformed(self):
|
def test_js_to_json_malformed(self):
|
||||||
self.assertEqual(js_to_json('42a1'), '42"a1"')
|
self.assertEqual(js_to_json('42a1'), '42"a1"')
|
||||||
self.assertEqual(js_to_json('42a-1'), '42"a"-1')
|
self.assertEqual(js_to_json('42a-1'), '42"a"-1')
|
||||||
self.assertEqual(js_to_json('{a: `${e("")}`}'), '{"a": "\\"e\\"(\\"\\")"}')
|
|
||||||
|
|
||||||
def test_js_to_json_template_literal(self):
|
def test_js_to_json_template_literal(self):
|
||||||
self.assertEqual(js_to_json('`Hello ${name}`', {'name': '"world"'}), '"Hello world"')
|
self.assertEqual(js_to_json('`Hello ${name}`', {'name': '"world"'}), '"Hello world"')
|
||||||
@ -2086,26 +2087,21 @@ Line 1
|
|||||||
headers = HTTPHeaderDict()
|
headers = HTTPHeaderDict()
|
||||||
headers['ytdl-test'] = b'0'
|
headers['ytdl-test'] = b'0'
|
||||||
self.assertEqual(list(headers.items()), [('Ytdl-Test', '0')])
|
self.assertEqual(list(headers.items()), [('Ytdl-Test', '0')])
|
||||||
self.assertEqual(list(headers.sensitive().items()), [('ytdl-test', '0')])
|
|
||||||
headers['ytdl-test'] = 1
|
headers['ytdl-test'] = 1
|
||||||
self.assertEqual(list(headers.items()), [('Ytdl-Test', '1')])
|
self.assertEqual(list(headers.items()), [('Ytdl-Test', '1')])
|
||||||
self.assertEqual(list(headers.sensitive().items()), [('ytdl-test', '1')])
|
|
||||||
headers['Ytdl-test'] = '2'
|
headers['Ytdl-test'] = '2'
|
||||||
self.assertEqual(list(headers.items()), [('Ytdl-Test', '2')])
|
self.assertEqual(list(headers.items()), [('Ytdl-Test', '2')])
|
||||||
self.assertEqual(list(headers.sensitive().items()), [('Ytdl-test', '2')])
|
|
||||||
self.assertTrue('ytDl-Test' in headers)
|
self.assertTrue('ytDl-Test' in headers)
|
||||||
self.assertEqual(str(headers), str(dict(headers)))
|
self.assertEqual(str(headers), str(dict(headers)))
|
||||||
self.assertEqual(repr(headers), str(dict(headers)))
|
self.assertEqual(repr(headers), str(dict(headers)))
|
||||||
|
|
||||||
headers.update({'X-dlp': 'data'})
|
headers.update({'X-dlp': 'data'})
|
||||||
self.assertEqual(set(headers.items()), {('Ytdl-Test', '2'), ('X-Dlp', 'data')})
|
self.assertEqual(set(headers.items()), {('Ytdl-Test', '2'), ('X-Dlp', 'data')})
|
||||||
self.assertEqual(set(headers.sensitive().items()), {('Ytdl-test', '2'), ('X-dlp', 'data')})
|
|
||||||
self.assertEqual(dict(headers), {'Ytdl-Test': '2', 'X-Dlp': 'data'})
|
self.assertEqual(dict(headers), {'Ytdl-Test': '2', 'X-Dlp': 'data'})
|
||||||
self.assertEqual(len(headers), 2)
|
self.assertEqual(len(headers), 2)
|
||||||
self.assertEqual(headers.copy(), headers)
|
self.assertEqual(headers.copy(), headers)
|
||||||
headers2 = HTTPHeaderDict({'X-dlp': 'data3'}, headers, **{'X-dlP': 'data2'})
|
headers2 = HTTPHeaderDict({'X-dlp': 'data3'}, **headers, **{'X-dlp': 'data2'})
|
||||||
self.assertEqual(set(headers2.items()), {('Ytdl-Test', '2'), ('X-Dlp', 'data2')})
|
self.assertEqual(set(headers2.items()), {('Ytdl-Test', '2'), ('X-Dlp', 'data2')})
|
||||||
self.assertEqual(set(headers2.sensitive().items()), {('Ytdl-test', '2'), ('X-dlP', 'data2')})
|
|
||||||
self.assertEqual(len(headers2), 2)
|
self.assertEqual(len(headers2), 2)
|
||||||
headers2.clear()
|
headers2.clear()
|
||||||
self.assertEqual(len(headers2), 0)
|
self.assertEqual(len(headers2), 0)
|
||||||
@ -2113,23 +2109,16 @@ Line 1
|
|||||||
# ensure we prefer latter headers
|
# ensure we prefer latter headers
|
||||||
headers3 = HTTPHeaderDict({'Ytdl-TeSt': 1}, {'Ytdl-test': 2})
|
headers3 = HTTPHeaderDict({'Ytdl-TeSt': 1}, {'Ytdl-test': 2})
|
||||||
self.assertEqual(set(headers3.items()), {('Ytdl-Test', '2')})
|
self.assertEqual(set(headers3.items()), {('Ytdl-Test', '2')})
|
||||||
self.assertEqual(set(headers3.sensitive().items()), {('Ytdl-test', '2')})
|
|
||||||
del headers3['ytdl-tesT']
|
del headers3['ytdl-tesT']
|
||||||
self.assertEqual(dict(headers3), {})
|
self.assertEqual(dict(headers3), {})
|
||||||
|
|
||||||
headers4 = HTTPHeaderDict({'ytdl-test': 'data;'})
|
headers4 = HTTPHeaderDict({'ytdl-test': 'data;'})
|
||||||
self.assertEqual(set(headers4.items()), {('Ytdl-Test', 'data;')})
|
self.assertEqual(set(headers4.items()), {('Ytdl-Test', 'data;')})
|
||||||
self.assertEqual(set(headers4.sensitive().items()), {('ytdl-test', 'data;')})
|
|
||||||
|
|
||||||
# common mistake: strip whitespace from values
|
# common mistake: strip whitespace from values
|
||||||
# https://github.com/yt-dlp/yt-dlp/issues/8729
|
# https://github.com/yt-dlp/yt-dlp/issues/8729
|
||||||
headers5 = HTTPHeaderDict({'ytdl-test': ' data; '})
|
headers5 = HTTPHeaderDict({'ytdl-test': ' data; '})
|
||||||
self.assertEqual(set(headers5.items()), {('Ytdl-Test', 'data;')})
|
self.assertEqual(set(headers5.items()), {('Ytdl-Test', 'data;')})
|
||||||
self.assertEqual(set(headers5.sensitive().items()), {('ytdl-test', 'data;')})
|
|
||||||
|
|
||||||
# test if picklable
|
|
||||||
headers6 = HTTPHeaderDict(a=1, b=2)
|
|
||||||
self.assertEqual(pickle.loads(pickle.dumps(headers6)), headers6)
|
|
||||||
|
|
||||||
def test_extract_basic_auth(self):
|
def test_extract_basic_auth(self):
|
||||||
assert extract_basic_auth('http://:foo.bar') == ('http://:foo.bar', None)
|
assert extract_basic_auth('http://:foo.bar') == ('http://:foo.bar', None)
|
||||||
|
@ -44,7 +44,7 @@ def websocket_handler(websocket):
|
|||||||
return websocket.send('2')
|
return websocket.send('2')
|
||||||
elif isinstance(message, str):
|
elif isinstance(message, str):
|
||||||
if message == 'headers':
|
if message == 'headers':
|
||||||
return websocket.send(json.dumps(dict(websocket.request.headers.raw_items())))
|
return websocket.send(json.dumps(dict(websocket.request.headers)))
|
||||||
elif message == 'path':
|
elif message == 'path':
|
||||||
return websocket.send(websocket.request.path)
|
return websocket.send(websocket.request.path)
|
||||||
elif message == 'source_address':
|
elif message == 'source_address':
|
||||||
@ -266,18 +266,18 @@ class TestWebsSocketRequestHandlerConformance:
|
|||||||
with handler(cookiejar=cookiejar) as rh:
|
with handler(cookiejar=cookiejar) as rh:
|
||||||
ws = ws_validate_and_send(rh, Request(self.ws_base_url))
|
ws = ws_validate_and_send(rh, Request(self.ws_base_url))
|
||||||
ws.send('headers')
|
ws.send('headers')
|
||||||
assert HTTPHeaderDict(json.loads(ws.recv()))['cookie'] == 'test=ytdlp'
|
assert json.loads(ws.recv())['cookie'] == 'test=ytdlp'
|
||||||
ws.close()
|
ws.close()
|
||||||
|
|
||||||
with handler() as rh:
|
with handler() as rh:
|
||||||
ws = ws_validate_and_send(rh, Request(self.ws_base_url))
|
ws = ws_validate_and_send(rh, Request(self.ws_base_url))
|
||||||
ws.send('headers')
|
ws.send('headers')
|
||||||
assert 'cookie' not in HTTPHeaderDict(json.loads(ws.recv()))
|
assert 'cookie' not in json.loads(ws.recv())
|
||||||
ws.close()
|
ws.close()
|
||||||
|
|
||||||
ws = ws_validate_and_send(rh, Request(self.ws_base_url, extensions={'cookiejar': cookiejar}))
|
ws = ws_validate_and_send(rh, Request(self.ws_base_url, extensions={'cookiejar': cookiejar}))
|
||||||
ws.send('headers')
|
ws.send('headers')
|
||||||
assert HTTPHeaderDict(json.loads(ws.recv()))['cookie'] == 'test=ytdlp'
|
assert json.loads(ws.recv())['cookie'] == 'test=ytdlp'
|
||||||
ws.close()
|
ws.close()
|
||||||
|
|
||||||
@pytest.mark.skip_handler('Websockets', 'Set-Cookie not supported by websockets')
|
@pytest.mark.skip_handler('Websockets', 'Set-Cookie not supported by websockets')
|
||||||
@ -287,7 +287,7 @@ class TestWebsSocketRequestHandlerConformance:
|
|||||||
ws_validate_and_send(rh, Request(f'{self.ws_base_url}/get_cookie', extensions={'cookiejar': YoutubeDLCookieJar()}))
|
ws_validate_and_send(rh, Request(f'{self.ws_base_url}/get_cookie', extensions={'cookiejar': YoutubeDLCookieJar()}))
|
||||||
ws = ws_validate_and_send(rh, Request(self.ws_base_url, extensions={'cookiejar': YoutubeDLCookieJar()}))
|
ws = ws_validate_and_send(rh, Request(self.ws_base_url, extensions={'cookiejar': YoutubeDLCookieJar()}))
|
||||||
ws.send('headers')
|
ws.send('headers')
|
||||||
assert 'cookie' not in HTTPHeaderDict(json.loads(ws.recv()))
|
assert 'cookie' not in json.loads(ws.recv())
|
||||||
ws.close()
|
ws.close()
|
||||||
|
|
||||||
@pytest.mark.skip_handler('Websockets', 'Set-Cookie not supported by websockets')
|
@pytest.mark.skip_handler('Websockets', 'Set-Cookie not supported by websockets')
|
||||||
@ -298,12 +298,12 @@ class TestWebsSocketRequestHandlerConformance:
|
|||||||
ws_validate_and_send(rh, Request(f'{self.ws_base_url}/get_cookie'))
|
ws_validate_and_send(rh, Request(f'{self.ws_base_url}/get_cookie'))
|
||||||
ws = ws_validate_and_send(rh, Request(self.ws_base_url))
|
ws = ws_validate_and_send(rh, Request(self.ws_base_url))
|
||||||
ws.send('headers')
|
ws.send('headers')
|
||||||
assert HTTPHeaderDict(json.loads(ws.recv()))['cookie'] == 'test=ytdlp'
|
assert json.loads(ws.recv())['cookie'] == 'test=ytdlp'
|
||||||
ws.close()
|
ws.close()
|
||||||
cookiejar.clear_session_cookies()
|
cookiejar.clear_session_cookies()
|
||||||
ws = ws_validate_and_send(rh, Request(self.ws_base_url))
|
ws = ws_validate_and_send(rh, Request(self.ws_base_url))
|
||||||
ws.send('headers')
|
ws.send('headers')
|
||||||
assert 'cookie' not in HTTPHeaderDict(json.loads(ws.recv()))
|
assert 'cookie' not in json.loads(ws.recv())
|
||||||
ws.close()
|
ws.close()
|
||||||
|
|
||||||
def test_source_address(self, handler):
|
def test_source_address(self, handler):
|
||||||
@ -341,14 +341,6 @@ class TestWebsSocketRequestHandlerConformance:
|
|||||||
assert headers['test3'] == 'test3'
|
assert headers['test3'] == 'test3'
|
||||||
ws.close()
|
ws.close()
|
||||||
|
|
||||||
def test_keep_header_casing(self, handler):
|
|
||||||
with handler(headers=HTTPHeaderDict({'x-TeSt1': 'test'})) as rh:
|
|
||||||
ws = ws_validate_and_send(rh, Request(self.ws_base_url, headers={'x-TeSt2': 'test'}, extensions={'keep_header_casing': True}))
|
|
||||||
ws.send('headers')
|
|
||||||
headers = json.loads(ws.recv())
|
|
||||||
assert 'x-TeSt1' in headers
|
|
||||||
assert 'x-TeSt2' in headers
|
|
||||||
|
|
||||||
@pytest.mark.parametrize('client_cert', (
|
@pytest.mark.parametrize('client_cert', (
|
||||||
{'client_certificate': os.path.join(MTLS_CERT_DIR, 'clientwithkey.crt')},
|
{'client_certificate': os.path.join(MTLS_CERT_DIR, 'clientwithkey.crt')},
|
||||||
{
|
{
|
||||||
|
@ -78,61 +78,6 @@ _SIG_TESTS = [
|
|||||||
'2aq0aqSyOoJXtK73m-uME_jv7-pT15gOFC02RFkGMqWpzEICs69VdbwQ0LDp1v7j8xx92efCJlYFYb1sUkkBSPOlPmXgIARw8JQ0qOAOAA',
|
'2aq0aqSyOoJXtK73m-uME_jv7-pT15gOFC02RFkGMqWpzEICs69VdbwQ0LDp1v7j8xx92efCJlYFYb1sUkkBSPOlPmXgIARw8JQ0qOAOAA',
|
||||||
'0QJ8wRAIgXmPlOPSBkkUs1bYFYlJCfe29xxAj7v1pDL0QwbdV96sCIEzpWqMGkFR20CFOg51Tp-7vj_EMu-m37KtXJ2OySqa0q',
|
'0QJ8wRAIgXmPlOPSBkkUs1bYFYlJCfe29xxAj7v1pDL0QwbdV96sCIEzpWqMGkFR20CFOg51Tp-7vj_EMu-m37KtXJ2OySqa0q',
|
||||||
),
|
),
|
||||||
(
|
|
||||||
'https://www.youtube.com/s/player/643afba4/tv-player-ias.vflset/tv-player-ias.js',
|
|
||||||
'2aq0aqSyOoJXtK73m-uME_jv7-pT15gOFC02RFkGMqWpzEICs69VdbwQ0LDp1v7j8xx92efCJlYFYb1sUkkBSPOlPmXgIARw8JQ0qOAOAA',
|
|
||||||
'AAOAOq0QJ8wRAIgXmPlOPSBkkUs1bYFYlJCfe29xx8j7vgpDL0QwbdV06sCIEzpWqMGkFR20CFOS21Tp-7vj_EMu-m37KtXJoOy1',
|
|
||||||
),
|
|
||||||
(
|
|
||||||
'https://www.youtube.com/s/player/363db69b/player_ias.vflset/en_US/base.js',
|
|
||||||
'2aq0aqSyOoJXtK73m-uME_jv7-pT15gOFC02RFkGMqWpzEICs69VdbwQ0LDp1v7j8xx92efCJlYFYb1sUkkBSPOlPmXgIARw8JQ0qOAOAA',
|
|
||||||
'0aqSyOoJXtK73m-uME_jv7-pT15gOFC02RFkGMqWpz2ICs6EVdbwQ0LDp1v7j8xx92efCJlYFYb1sUkkBSPOlPmXgIARw8JQ0qOAOAA',
|
|
||||||
),
|
|
||||||
(
|
|
||||||
'https://www.youtube.com/s/player/363db69b/player_ias_tce.vflset/en_US/base.js',
|
|
||||||
'2aq0aqSyOoJXtK73m-uME_jv7-pT15gOFC02RFkGMqWpzEICs69VdbwQ0LDp1v7j8xx92efCJlYFYb1sUkkBSPOlPmXgIARw8JQ0qOAOAA',
|
|
||||||
'0aqSyOoJXtK73m-uME_jv7-pT15gOFC02RFkGMqWpz2ICs6EVdbwQ0LDp1v7j8xx92efCJlYFYb1sUkkBSPOlPmXgIARw8JQ0qOAOAA',
|
|
||||||
),
|
|
||||||
(
|
|
||||||
'https://www.youtube.com/s/player/4fcd6e4a/player_ias.vflset/en_US/base.js',
|
|
||||||
'2aq0aqSyOoJXtK73m-uME_jv7-pT15gOFC02RFkGMqWpzEICs69VdbwQ0LDp1v7j8xx92efCJlYFYb1sUkkBSPOlPmXgIARw8JQ0qOAOAA',
|
|
||||||
'wAOAOq0QJ8ARAIgXmPlOPSBkkUs1bYFYlJCfe29xx8q7v1pDL0QwbdV96sCIEzpWqMGkFR20CFOg51Tp-7vj_EMu-m37KtXJoOySqa0',
|
|
||||||
),
|
|
||||||
(
|
|
||||||
'https://www.youtube.com/s/player/4fcd6e4a/player_ias_tce.vflset/en_US/base.js',
|
|
||||||
'2aq0aqSyOoJXtK73m-uME_jv7-pT15gOFC02RFkGMqWpzEICs69VdbwQ0LDp1v7j8xx92efCJlYFYb1sUkkBSPOlPmXgIARw8JQ0qOAOAA',
|
|
||||||
'wAOAOq0QJ8ARAIgXmPlOPSBkkUs1bYFYlJCfe29xx8q7v1pDL0QwbdV96sCIEzpWqMGkFR20CFOg51Tp-7vj_EMu-m37KtXJoOySqa0',
|
|
||||||
),
|
|
||||||
(
|
|
||||||
'https://www.youtube.com/s/player/20830619/player_ias.vflset/en_US/base.js',
|
|
||||||
'2aq0aqSyOoJXtK73m-uME_jv7-pT15gOFC02RFkGMqWpzEICs69VdbwQ0LDp1v7j8xx92efCJlYFYb1sUkkBSPOlPmXgIARw8JQ0qOAOAA',
|
|
||||||
'7AOq0QJ8wRAIgXmPlOPSBkkAs1bYFYlJCfe29xx8jOv1pDL0Q2bdV96sCIEzpWqMGkFR20CFOg51Tp-7vj_EMu-m37KtXJoOySqa0qaw',
|
|
||||||
),
|
|
||||||
(
|
|
||||||
'https://www.youtube.com/s/player/20830619/player_ias_tce.vflset/en_US/base.js',
|
|
||||||
'2aq0aqSyOoJXtK73m-uME_jv7-pT15gOFC02RFkGMqWpzEICs69VdbwQ0LDp1v7j8xx92efCJlYFYb1sUkkBSPOlPmXgIARw8JQ0qOAOAA',
|
|
||||||
'7AOq0QJ8wRAIgXmPlOPSBkkAs1bYFYlJCfe29xx8jOv1pDL0Q2bdV96sCIEzpWqMGkFR20CFOg51Tp-7vj_EMu-m37KtXJoOySqa0qaw',
|
|
||||||
),
|
|
||||||
(
|
|
||||||
'https://www.youtube.com/s/player/20830619/player-plasma-ias-phone-en_US.vflset/base.js',
|
|
||||||
'2aq0aqSyOoJXtK73m-uME_jv7-pT15gOFC02RFkGMqWpzEICs69VdbwQ0LDp1v7j8xx92efCJlYFYb1sUkkBSPOlPmXgIARw8JQ0qOAOAA',
|
|
||||||
'7AOq0QJ8wRAIgXmPlOPSBkkAs1bYFYlJCfe29xx8jOv1pDL0Q2bdV96sCIEzpWqMGkFR20CFOg51Tp-7vj_EMu-m37KtXJoOySqa0qaw',
|
|
||||||
),
|
|
||||||
(
|
|
||||||
'https://www.youtube.com/s/player/20830619/player-plasma-ias-tablet-en_US.vflset/base.js',
|
|
||||||
'2aq0aqSyOoJXtK73m-uME_jv7-pT15gOFC02RFkGMqWpzEICs69VdbwQ0LDp1v7j8xx92efCJlYFYb1sUkkBSPOlPmXgIARw8JQ0qOAOAA',
|
|
||||||
'7AOq0QJ8wRAIgXmPlOPSBkkAs1bYFYlJCfe29xx8jOv1pDL0Q2bdV96sCIEzpWqMGkFR20CFOg51Tp-7vj_EMu-m37KtXJoOySqa0qaw',
|
|
||||||
),
|
|
||||||
(
|
|
||||||
'https://www.youtube.com/s/player/8a8ac953/player_ias_tce.vflset/en_US/base.js',
|
|
||||||
'2aq0aqSyOoJXtK73m-uME_jv7-pT15gOFC02RFkGMqWpzEICs69VdbwQ0LDp1v7j8xx92efCJlYFYb1sUkkBSPOlPmXgIARw8JQ0qOAOAA',
|
|
||||||
'IAOAOq0QJ8wRAAgXmPlOPSBkkUs1bYFYlJCfe29xx8j7v1pDL0QwbdV96sCIEzpWqMGkFR20CFOg51Tp-7vj_E2u-m37KtXJoOySqa0',
|
|
||||||
),
|
|
||||||
(
|
|
||||||
'https://www.youtube.com/s/player/8a8ac953/tv-player-es6.vflset/tv-player-es6.js',
|
|
||||||
'2aq0aqSyOoJXtK73m-uME_jv7-pT15gOFC02RFkGMqWpzEICs69VdbwQ0LDp1v7j8xx92efCJlYFYb1sUkkBSPOlPmXgIARw8JQ0qOAOAA',
|
|
||||||
'IAOAOq0QJ8wRAAgXmPlOPSBkkUs1bYFYlJCfe29xx8j7v1pDL0QwbdV96sCIEzpWqMGkFR20CFOg51Tp-7vj_E2u-m37KtXJoOySqa0',
|
|
||||||
),
|
|
||||||
]
|
]
|
||||||
|
|
||||||
_NSIG_TESTS = [
|
_NSIG_TESTS = [
|
||||||
@ -260,62 +205,6 @@ _NSIG_TESTS = [
|
|||||||
'https://www.youtube.com/s/player/9c6dfc4a/player_ias.vflset/en_US/base.js',
|
'https://www.youtube.com/s/player/9c6dfc4a/player_ias.vflset/en_US/base.js',
|
||||||
'jbu7ylIosQHyJyJV', 'uwI0ESiynAmhNg',
|
'jbu7ylIosQHyJyJV', 'uwI0ESiynAmhNg',
|
||||||
),
|
),
|
||||||
(
|
|
||||||
'https://www.youtube.com/s/player/e7567ecf/player_ias_tce.vflset/en_US/base.js',
|
|
||||||
'Sy4aDGc0VpYRR9ew_', '5UPOT1VhoZxNLQ',
|
|
||||||
),
|
|
||||||
(
|
|
||||||
'https://www.youtube.com/s/player/d50f54ef/player_ias_tce.vflset/en_US/base.js',
|
|
||||||
'Ha7507LzRmH3Utygtj', 'XFTb2HoeOE5MHg',
|
|
||||||
),
|
|
||||||
(
|
|
||||||
'https://www.youtube.com/s/player/074a8365/player_ias_tce.vflset/en_US/base.js',
|
|
||||||
'Ha7507LzRmH3Utygtj', 'ufTsrE0IVYrkl8v',
|
|
||||||
),
|
|
||||||
(
|
|
||||||
'https://www.youtube.com/s/player/643afba4/player_ias.vflset/en_US/base.js',
|
|
||||||
'N5uAlLqm0eg1GyHO', 'dCBQOejdq5s-ww',
|
|
||||||
),
|
|
||||||
(
|
|
||||||
'https://www.youtube.com/s/player/69f581a5/tv-player-ias.vflset/tv-player-ias.js',
|
|
||||||
'-qIP447rVlTTwaZjY', 'KNcGOksBAvwqQg',
|
|
||||||
),
|
|
||||||
(
|
|
||||||
'https://www.youtube.com/s/player/643afba4/tv-player-ias.vflset/tv-player-ias.js',
|
|
||||||
'ir9-V6cdbCiyKxhr', '2PL7ZDYAALMfmA',
|
|
||||||
),
|
|
||||||
(
|
|
||||||
'https://www.youtube.com/s/player/363db69b/player_ias.vflset/en_US/base.js',
|
|
||||||
'eWYu5d5YeY_4LyEDc', 'XJQqf-N7Xra3gg',
|
|
||||||
),
|
|
||||||
(
|
|
||||||
'https://www.youtube.com/s/player/4fcd6e4a/player_ias.vflset/en_US/base.js',
|
|
||||||
'o_L251jm8yhZkWtBW', 'lXoxI3XvToqn6A',
|
|
||||||
),
|
|
||||||
(
|
|
||||||
'https://www.youtube.com/s/player/4fcd6e4a/player_ias_tce.vflset/en_US/base.js',
|
|
||||||
'o_L251jm8yhZkWtBW', 'lXoxI3XvToqn6A',
|
|
||||||
),
|
|
||||||
(
|
|
||||||
'https://www.youtube.com/s/player/20830619/tv-player-ias.vflset/tv-player-ias.js',
|
|
||||||
'ir9-V6cdbCiyKxhr', '9YE85kNjZiS4',
|
|
||||||
),
|
|
||||||
(
|
|
||||||
'https://www.youtube.com/s/player/20830619/player-plasma-ias-phone-en_US.vflset/base.js',
|
|
||||||
'ir9-V6cdbCiyKxhr', '9YE85kNjZiS4',
|
|
||||||
),
|
|
||||||
(
|
|
||||||
'https://www.youtube.com/s/player/20830619/player-plasma-ias-tablet-en_US.vflset/base.js',
|
|
||||||
'ir9-V6cdbCiyKxhr', '9YE85kNjZiS4',
|
|
||||||
),
|
|
||||||
(
|
|
||||||
'https://www.youtube.com/s/player/8a8ac953/player_ias_tce.vflset/en_US/base.js',
|
|
||||||
'MiBYeXx_vRREbiCCmh', 'RtZYMVvmkE0JE',
|
|
||||||
),
|
|
||||||
(
|
|
||||||
'https://www.youtube.com/s/player/8a8ac953/tv-player-es6.vflset/tv-player-es6.js',
|
|
||||||
'MiBYeXx_vRREbiCCmh', 'RtZYMVvmkE0JE',
|
|
||||||
),
|
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
@ -329,8 +218,6 @@ class TestPlayerInfo(unittest.TestCase):
|
|||||||
('https://www.youtube.com/s/player/64dddad9/player-plasma-ias-phone-en_US.vflset/base.js', '64dddad9'),
|
('https://www.youtube.com/s/player/64dddad9/player-plasma-ias-phone-en_US.vflset/base.js', '64dddad9'),
|
||||||
('https://www.youtube.com/s/player/64dddad9/player-plasma-ias-phone-de_DE.vflset/base.js', '64dddad9'),
|
('https://www.youtube.com/s/player/64dddad9/player-plasma-ias-phone-de_DE.vflset/base.js', '64dddad9'),
|
||||||
('https://www.youtube.com/s/player/64dddad9/player-plasma-ias-tablet-en_US.vflset/base.js', '64dddad9'),
|
('https://www.youtube.com/s/player/64dddad9/player-plasma-ias-tablet-en_US.vflset/base.js', '64dddad9'),
|
||||||
('https://www.youtube.com/s/player/e7567ecf/player_ias_tce.vflset/en_US/base.js', 'e7567ecf'),
|
|
||||||
('https://www.youtube.com/s/player/643afba4/tv-player-ias.vflset/tv-player-ias.js', '643afba4'),
|
|
||||||
# obsolete
|
# obsolete
|
||||||
('https://www.youtube.com/yts/jsbin/player_ias-vfle4-e03/en_US/base.js', 'vfle4-e03'),
|
('https://www.youtube.com/yts/jsbin/player_ias-vfle4-e03/en_US/base.js', 'vfle4-e03'),
|
||||||
('https://www.youtube.com/yts/jsbin/player_ias-vfl49f_g4/en_US/base.js', 'vfl49f_g4'),
|
('https://www.youtube.com/yts/jsbin/player_ias-vfl49f_g4/en_US/base.js', 'vfl49f_g4'),
|
||||||
@ -363,51 +250,46 @@ def t_factory(name, sig_func, url_pattern):
|
|||||||
def make_tfunc(url, sig_input, expected_sig):
|
def make_tfunc(url, sig_input, expected_sig):
|
||||||
m = url_pattern.match(url)
|
m = url_pattern.match(url)
|
||||||
assert m, f'{url!r} should follow URL format'
|
assert m, f'{url!r} should follow URL format'
|
||||||
test_id = re.sub(r'[/.-]', '_', m.group('id') or m.group('compat_id'))
|
test_id = m.group('id')
|
||||||
|
|
||||||
def test_func(self):
|
def test_func(self):
|
||||||
basename = f'player-{test_id}.js'
|
basename = f'player-{name}-{test_id}.js'
|
||||||
fn = os.path.join(self.TESTDATA_DIR, basename)
|
fn = os.path.join(self.TESTDATA_DIR, basename)
|
||||||
|
|
||||||
if not os.path.exists(fn):
|
if not os.path.exists(fn):
|
||||||
urllib.request.urlretrieve(url, fn)
|
urllib.request.urlretrieve(url, fn)
|
||||||
with open(fn, encoding='utf-8') as testf:
|
with open(fn, encoding='utf-8') as testf:
|
||||||
jscode = testf.read()
|
jscode = testf.read()
|
||||||
self.assertEqual(sig_func(jscode, sig_input, url), expected_sig)
|
self.assertEqual(sig_func(jscode, sig_input), expected_sig)
|
||||||
|
|
||||||
test_func.__name__ = f'test_{name}_js_{test_id}'
|
test_func.__name__ = f'test_{name}_js_{test_id}'
|
||||||
setattr(TestSignature, test_func.__name__, test_func)
|
setattr(TestSignature, test_func.__name__, test_func)
|
||||||
return make_tfunc
|
return make_tfunc
|
||||||
|
|
||||||
|
|
||||||
def signature(jscode, sig_input, player_url):
|
def signature(jscode, sig_input):
|
||||||
func = YoutubeIE(FakeYDL())._parse_sig_js(jscode, player_url)
|
func = YoutubeIE(FakeYDL())._parse_sig_js(jscode)
|
||||||
src_sig = (
|
src_sig = (
|
||||||
str(string.printable[:sig_input])
|
str(string.printable[:sig_input])
|
||||||
if isinstance(sig_input, int) else sig_input)
|
if isinstance(sig_input, int) else sig_input)
|
||||||
return func(src_sig)
|
return func(src_sig)
|
||||||
|
|
||||||
|
|
||||||
def n_sig(jscode, sig_input, player_url):
|
def n_sig(jscode, sig_input):
|
||||||
ie = YoutubeIE(FakeYDL())
|
ie = YoutubeIE(FakeYDL())
|
||||||
funcname = ie._extract_n_function_name(jscode, player_url=player_url)
|
funcname = ie._extract_n_function_name(jscode)
|
||||||
jsi = JSInterpreter(jscode)
|
jsi = JSInterpreter(jscode)
|
||||||
func = jsi.extract_function_from_code(*ie._fixup_n_function_code(*jsi.extract_function_code(funcname), jscode, player_url))
|
func = jsi.extract_function_from_code(*ie._fixup_n_function_code(*jsi.extract_function_code(funcname)))
|
||||||
return func([sig_input])
|
return func([sig_input])
|
||||||
|
|
||||||
|
|
||||||
make_sig_test = t_factory(
|
make_sig_test = t_factory(
|
||||||
'signature', signature,
|
'signature', signature, re.compile(r'.*(?:-|/player/)(?P<id>[a-zA-Z0-9_-]+)(?:/.+\.js|(?:/watch_as3|/html5player)?\.[a-z]+)$'))
|
||||||
re.compile(r'''(?x)
|
|
||||||
.+(?:
|
|
||||||
/player/(?P<id>[a-zA-Z0-9_/.-]+)|
|
|
||||||
/html5player-(?:en_US-)?(?P<compat_id>[a-zA-Z0-9_-]+)(?:/watch_as3|/html5player)?
|
|
||||||
)\.js$'''))
|
|
||||||
for test_spec in _SIG_TESTS:
|
for test_spec in _SIG_TESTS:
|
||||||
make_sig_test(*test_spec)
|
make_sig_test(*test_spec)
|
||||||
|
|
||||||
make_nsig_test = t_factory(
|
make_nsig_test = t_factory(
|
||||||
'nsig', n_sig, re.compile(r'.+/player/(?P<id>[a-zA-Z0-9_/.-]+)\.js$'))
|
'nsig', n_sig, re.compile(r'.+/player/(?P<id>[a-zA-Z0-9_-]+)/.+.js$'))
|
||||||
for test_spec in _NSIG_TESTS:
|
for test_spec in _NSIG_TESTS:
|
||||||
make_nsig_test(*test_spec)
|
make_nsig_test(*test_spec)
|
||||||
|
|
||||||
|
@ -2,5 +2,4 @@ from yt_dlp.extractor.common import InfoExtractor
|
|||||||
|
|
||||||
|
|
||||||
class PackagePluginIE(InfoExtractor):
|
class PackagePluginIE(InfoExtractor):
|
||||||
_VALID_URL = 'package'
|
|
||||||
pass
|
pass
|
||||||
|
@ -1,10 +0,0 @@
|
|||||||
from yt_dlp.extractor.common import InfoExtractor
|
|
||||||
|
|
||||||
|
|
||||||
class NormalPluginIE(InfoExtractor):
|
|
||||||
_VALID_URL = 'normal'
|
|
||||||
REPLACED = True
|
|
||||||
|
|
||||||
|
|
||||||
class _IgnoreUnderscorePluginIE(InfoExtractor):
|
|
||||||
pass
|
|
@ -1,5 +0,0 @@
|
|||||||
from yt_dlp.postprocessor.common import PostProcessor
|
|
||||||
|
|
||||||
|
|
||||||
class NormalPluginPP(PostProcessor):
|
|
||||||
REPLACED = True
|
|
@ -6,7 +6,6 @@ class IgnoreNotInAllPluginIE(InfoExtractor):
|
|||||||
|
|
||||||
|
|
||||||
class InAllPluginIE(InfoExtractor):
|
class InAllPluginIE(InfoExtractor):
|
||||||
_VALID_URL = 'inallpluginie'
|
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
@ -2,10 +2,8 @@ from yt_dlp.extractor.common import InfoExtractor
|
|||||||
|
|
||||||
|
|
||||||
class NormalPluginIE(InfoExtractor):
|
class NormalPluginIE(InfoExtractor):
|
||||||
_VALID_URL = 'normalpluginie'
|
pass
|
||||||
REPLACED = False
|
|
||||||
|
|
||||||
|
|
||||||
class _IgnoreUnderscorePluginIE(InfoExtractor):
|
class _IgnoreUnderscorePluginIE(InfoExtractor):
|
||||||
_VALID_URL = 'ignoreunderscorepluginie'
|
|
||||||
pass
|
pass
|
||||||
|
@ -1,5 +0,0 @@
|
|||||||
from yt_dlp.extractor.generic import GenericIE
|
|
||||||
|
|
||||||
|
|
||||||
class OverrideGenericIE(GenericIE, plugin_name='override'):
|
|
||||||
TEST_FIELD = 'override'
|
|
@ -1,5 +0,0 @@
|
|||||||
from yt_dlp.extractor.generic import GenericIE
|
|
||||||
|
|
||||||
|
|
||||||
class _UnderscoreOverrideGenericIE(GenericIE, plugin_name='underscore-override'):
|
|
||||||
SECONDARY_TEST_FIELD = 'underscore-override'
|
|
@ -2,4 +2,4 @@ from yt_dlp.postprocessor.common import PostProcessor
|
|||||||
|
|
||||||
|
|
||||||
class NormalPluginPP(PostProcessor):
|
class NormalPluginPP(PostProcessor):
|
||||||
REPLACED = False
|
pass
|
||||||
|
@ -2,5 +2,4 @@ from yt_dlp.extractor.common import InfoExtractor
|
|||||||
|
|
||||||
|
|
||||||
class ZippedPluginIE(InfoExtractor):
|
class ZippedPluginIE(InfoExtractor):
|
||||||
_VALID_URL = 'zippedpluginie'
|
|
||||||
pass
|
pass
|
||||||
|
@ -30,18 +30,9 @@ from .compat import urllib_req_to_req
|
|||||||
from .cookies import CookieLoadError, LenientSimpleCookie, load_cookies
|
from .cookies import CookieLoadError, LenientSimpleCookie, load_cookies
|
||||||
from .downloader import FFmpegFD, get_suitable_downloader, shorten_protocol_name
|
from .downloader import FFmpegFD, get_suitable_downloader, shorten_protocol_name
|
||||||
from .downloader.rtmp import rtmpdump_version
|
from .downloader.rtmp import rtmpdump_version
|
||||||
from .extractor import gen_extractor_classes, get_info_extractor, import_extractors
|
from .extractor import gen_extractor_classes, get_info_extractor
|
||||||
from .extractor.common import UnsupportedURLIE
|
from .extractor.common import UnsupportedURLIE
|
||||||
from .extractor.openload import PhantomJSwrapper
|
from .extractor.openload import PhantomJSwrapper
|
||||||
from .globals import (
|
|
||||||
IN_CLI,
|
|
||||||
LAZY_EXTRACTORS,
|
|
||||||
plugin_ies,
|
|
||||||
plugin_ies_overrides,
|
|
||||||
plugin_pps,
|
|
||||||
all_plugins_loaded,
|
|
||||||
plugin_dirs,
|
|
||||||
)
|
|
||||||
from .minicurses import format_text
|
from .minicurses import format_text
|
||||||
from .networking import HEADRequest, Request, RequestDirector
|
from .networking import HEADRequest, Request, RequestDirector
|
||||||
from .networking.common import _REQUEST_HANDLERS, _RH_PREFERENCES
|
from .networking.common import _REQUEST_HANDLERS, _RH_PREFERENCES
|
||||||
@ -53,7 +44,8 @@ from .networking.exceptions import (
|
|||||||
network_exceptions,
|
network_exceptions,
|
||||||
)
|
)
|
||||||
from .networking.impersonate import ImpersonateRequestHandler
|
from .networking.impersonate import ImpersonateRequestHandler
|
||||||
from .plugins import directories as plugin_directories, load_all_plugins
|
from .plugins import directories as plugin_directories
|
||||||
|
from .postprocessor import _PLUGIN_CLASSES as plugin_pps
|
||||||
from .postprocessor import (
|
from .postprocessor import (
|
||||||
EmbedThumbnailPP,
|
EmbedThumbnailPP,
|
||||||
FFmpegFixupDuplicateMoovPP,
|
FFmpegFixupDuplicateMoovPP,
|
||||||
@ -165,7 +157,7 @@ from .utils import (
|
|||||||
write_json_file,
|
write_json_file,
|
||||||
write_string,
|
write_string,
|
||||||
)
|
)
|
||||||
from .utils._utils import _UnsafeExtensionError, _YDLLogger, _ProgressState
|
from .utils._utils import _UnsafeExtensionError, _YDLLogger
|
||||||
from .utils.networking import (
|
from .utils.networking import (
|
||||||
HTTPHeaderDict,
|
HTTPHeaderDict,
|
||||||
clean_headers,
|
clean_headers,
|
||||||
@ -650,15 +642,13 @@ class YoutubeDL:
|
|||||||
self.cache = Cache(self)
|
self.cache = Cache(self)
|
||||||
self.__header_cookies = []
|
self.__header_cookies = []
|
||||||
|
|
||||||
# compat for API: load plugins if they have not already
|
|
||||||
if not all_plugins_loaded.value:
|
|
||||||
load_all_plugins()
|
|
||||||
|
|
||||||
stdout = sys.stderr if self.params.get('logtostderr') else sys.stdout
|
stdout = sys.stderr if self.params.get('logtostderr') else sys.stdout
|
||||||
self._out_files = Namespace(
|
self._out_files = Namespace(
|
||||||
out=stdout,
|
out=stdout,
|
||||||
error=sys.stderr,
|
error=sys.stderr,
|
||||||
screen=sys.stderr if self.params.get('quiet') else stdout,
|
screen=sys.stderr if self.params.get('quiet') else stdout,
|
||||||
|
console=None if os.name == 'nt' else next(
|
||||||
|
filter(supports_terminal_sequences, (sys.stderr, sys.stdout)), None),
|
||||||
)
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@ -666,9 +656,6 @@ class YoutubeDL:
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.write_debug(f'Failed to enable VT mode: {e}')
|
self.write_debug(f'Failed to enable VT mode: {e}')
|
||||||
|
|
||||||
# hehe "immutable" namespace
|
|
||||||
self._out_files.console = next(filter(supports_terminal_sequences, (sys.stderr, sys.stdout)), None)
|
|
||||||
|
|
||||||
if self.params.get('no_color'):
|
if self.params.get('no_color'):
|
||||||
if self.params.get('color') is not None:
|
if self.params.get('color') is not None:
|
||||||
self.params.setdefault('_warnings', []).append(
|
self.params.setdefault('_warnings', []).append(
|
||||||
@ -969,22 +956,21 @@ class YoutubeDL:
|
|||||||
self._write_string(f'{self._bidi_workaround(message)}\n', self._out_files.error, only_once=only_once)
|
self._write_string(f'{self._bidi_workaround(message)}\n', self._out_files.error, only_once=only_once)
|
||||||
|
|
||||||
def _send_console_code(self, code):
|
def _send_console_code(self, code):
|
||||||
if not supports_terminal_sequences(self._out_files.console):
|
if os.name == 'nt' or not self._out_files.console:
|
||||||
return False
|
|
||||||
self._write_string(code, self._out_files.console)
|
|
||||||
return True
|
|
||||||
|
|
||||||
def to_console_title(self, message=None, progress_state=None, percent=None):
|
|
||||||
if not self.params.get('consoletitle'):
|
|
||||||
return
|
return
|
||||||
|
self._write_string(code, self._out_files.console)
|
||||||
|
|
||||||
if message:
|
def to_console_title(self, message):
|
||||||
success = self._send_console_code(f'\033]0;{remove_terminal_sequences(message)}\007')
|
if not self.params.get('consoletitle', False):
|
||||||
if not success and os.name == 'nt' and ctypes.windll.kernel32.GetConsoleWindow():
|
return
|
||||||
ctypes.windll.kernel32.SetConsoleTitleW(message)
|
message = remove_terminal_sequences(message)
|
||||||
|
if os.name == 'nt':
|
||||||
if isinstance(progress_state, _ProgressState):
|
if ctypes.windll.kernel32.GetConsoleWindow():
|
||||||
self._send_console_code(progress_state.get_ansi_escape(percent))
|
# c_wchar_p() might not be necessary if `message` is
|
||||||
|
# already of type unicode()
|
||||||
|
ctypes.windll.kernel32.SetConsoleTitleW(ctypes.c_wchar_p(message))
|
||||||
|
else:
|
||||||
|
self._send_console_code(f'\033]0;{message}\007')
|
||||||
|
|
||||||
def save_console_title(self):
|
def save_console_title(self):
|
||||||
if not self.params.get('consoletitle') or self.params.get('simulate'):
|
if not self.params.get('consoletitle') or self.params.get('simulate'):
|
||||||
@ -998,7 +984,6 @@ class YoutubeDL:
|
|||||||
|
|
||||||
def __enter__(self):
|
def __enter__(self):
|
||||||
self.save_console_title()
|
self.save_console_title()
|
||||||
self.to_console_title(progress_state=_ProgressState.INDETERMINATE)
|
|
||||||
return self
|
return self
|
||||||
|
|
||||||
def save_cookies(self):
|
def save_cookies(self):
|
||||||
@ -1007,7 +992,6 @@ class YoutubeDL:
|
|||||||
|
|
||||||
def __exit__(self, *args):
|
def __exit__(self, *args):
|
||||||
self.restore_console_title()
|
self.restore_console_title()
|
||||||
self.to_console_title(progress_state=_ProgressState.HIDDEN)
|
|
||||||
self.close()
|
self.close()
|
||||||
|
|
||||||
def close(self):
|
def close(self):
|
||||||
@ -4009,6 +3993,15 @@ class YoutubeDL:
|
|||||||
if not self.params.get('verbose'):
|
if not self.params.get('verbose'):
|
||||||
return
|
return
|
||||||
|
|
||||||
|
from . import _IN_CLI # Must be delayed import
|
||||||
|
|
||||||
|
# These imports can be slow. So import them only as needed
|
||||||
|
from .extractor.extractors import _LAZY_LOADER
|
||||||
|
from .extractor.extractors import (
|
||||||
|
_PLUGIN_CLASSES as plugin_ies,
|
||||||
|
_PLUGIN_OVERRIDES as plugin_ie_overrides,
|
||||||
|
)
|
||||||
|
|
||||||
def get_encoding(stream):
|
def get_encoding(stream):
|
||||||
ret = str(getattr(stream, 'encoding', f'missing ({type(stream).__name__})'))
|
ret = str(getattr(stream, 'encoding', f'missing ({type(stream).__name__})'))
|
||||||
additional_info = []
|
additional_info = []
|
||||||
@ -4047,18 +4040,17 @@ class YoutubeDL:
|
|||||||
_make_label(ORIGIN, CHANNEL.partition('@')[2] or __version__, __version__),
|
_make_label(ORIGIN, CHANNEL.partition('@')[2] or __version__, __version__),
|
||||||
f'[{RELEASE_GIT_HEAD[:9]}]' if RELEASE_GIT_HEAD else '',
|
f'[{RELEASE_GIT_HEAD[:9]}]' if RELEASE_GIT_HEAD else '',
|
||||||
'' if source == 'unknown' else f'({source})',
|
'' if source == 'unknown' else f'({source})',
|
||||||
'' if IN_CLI.value else 'API' if klass == YoutubeDL else f'API:{self.__module__}.{klass.__qualname__}',
|
'' if _IN_CLI else 'API' if klass == YoutubeDL else f'API:{self.__module__}.{klass.__qualname__}',
|
||||||
delim=' '))
|
delim=' '))
|
||||||
|
|
||||||
if not IN_CLI.value:
|
if not _IN_CLI:
|
||||||
write_debug(f'params: {self.params}')
|
write_debug(f'params: {self.params}')
|
||||||
|
|
||||||
import_extractors()
|
if not _LAZY_LOADER:
|
||||||
lazy_extractors = LAZY_EXTRACTORS.value
|
if os.environ.get('YTDLP_NO_LAZY_EXTRACTORS'):
|
||||||
if lazy_extractors is None:
|
|
||||||
write_debug('Lazy loading extractors is disabled')
|
|
||||||
elif not lazy_extractors:
|
|
||||||
write_debug('Lazy loading extractors is forcibly disabled')
|
write_debug('Lazy loading extractors is forcibly disabled')
|
||||||
|
else:
|
||||||
|
write_debug('Lazy loading extractors is disabled')
|
||||||
if self.params['compat_opts']:
|
if self.params['compat_opts']:
|
||||||
write_debug('Compatibility options: {}'.format(', '.join(self.params['compat_opts'])))
|
write_debug('Compatibility options: {}'.format(', '.join(self.params['compat_opts'])))
|
||||||
|
|
||||||
@ -4087,27 +4079,24 @@ class YoutubeDL:
|
|||||||
|
|
||||||
write_debug(f'Proxy map: {self.proxies}')
|
write_debug(f'Proxy map: {self.proxies}')
|
||||||
write_debug(f'Request Handlers: {", ".join(rh.RH_NAME for rh in self._request_director.handlers.values())}')
|
write_debug(f'Request Handlers: {", ".join(rh.RH_NAME for rh in self._request_director.handlers.values())}')
|
||||||
|
if os.environ.get('YTDLP_NO_PLUGINS'):
|
||||||
|
write_debug('Plugins are forcibly disabled')
|
||||||
|
return
|
||||||
|
|
||||||
for plugin_type, plugins in (('Extractor', plugin_ies), ('Post-Processor', plugin_pps)):
|
for plugin_type, plugins in {'Extractor': plugin_ies, 'Post-Processor': plugin_pps}.items():
|
||||||
display_list = [
|
display_list = ['{}{}'.format(
|
||||||
klass.__name__ if klass.__name__ == name else f'{klass.__name__} as {name}'
|
klass.__name__, '' if klass.__name__ == name else f' as {name}')
|
||||||
for name, klass in plugins.value.items()]
|
for name, klass in plugins.items()]
|
||||||
if plugin_type == 'Extractor':
|
if plugin_type == 'Extractor':
|
||||||
display_list.extend(f'{plugins[-1].IE_NAME.partition("+")[2]} ({parent.__name__})'
|
display_list.extend(f'{plugins[-1].IE_NAME.partition("+")[2]} ({parent.__name__})'
|
||||||
for parent, plugins in plugin_ies_overrides.value.items())
|
for parent, plugins in plugin_ie_overrides.items())
|
||||||
if not display_list:
|
if not display_list:
|
||||||
continue
|
continue
|
||||||
write_debug(f'{plugin_type} Plugins: {", ".join(sorted(display_list))}')
|
write_debug(f'{plugin_type} Plugins: {", ".join(sorted(display_list))}')
|
||||||
|
|
||||||
plugin_dirs_msg = 'none'
|
plugin_dirs = plugin_directories()
|
||||||
if not plugin_dirs.value:
|
if plugin_dirs:
|
||||||
plugin_dirs_msg = 'none (disabled)'
|
write_debug(f'Plugin directories: {plugin_dirs}')
|
||||||
else:
|
|
||||||
found_plugin_directories = plugin_directories()
|
|
||||||
if found_plugin_directories:
|
|
||||||
plugin_dirs_msg = ', '.join(found_plugin_directories)
|
|
||||||
|
|
||||||
write_debug(f'Plugin directories: {plugin_dirs_msg}')
|
|
||||||
|
|
||||||
@functools.cached_property
|
@functools.cached_property
|
||||||
def proxies(self):
|
def proxies(self):
|
||||||
@ -4152,7 +4141,7 @@ class YoutubeDL:
|
|||||||
(target, rh.RH_NAME)
|
(target, rh.RH_NAME)
|
||||||
for rh in self._request_director.handlers.values()
|
for rh in self._request_director.handlers.values()
|
||||||
if isinstance(rh, ImpersonateRequestHandler)
|
if isinstance(rh, ImpersonateRequestHandler)
|
||||||
for target in reversed(rh.supported_targets)
|
for target in rh.supported_targets
|
||||||
]
|
]
|
||||||
|
|
||||||
def _impersonate_target_available(self, target):
|
def _impersonate_target_available(self, target):
|
||||||
|
@ -19,9 +19,7 @@ from .downloader.external import get_external_downloader
|
|||||||
from .extractor import list_extractor_classes
|
from .extractor import list_extractor_classes
|
||||||
from .extractor.adobepass import MSO_INFO
|
from .extractor.adobepass import MSO_INFO
|
||||||
from .networking.impersonate import ImpersonateTarget
|
from .networking.impersonate import ImpersonateTarget
|
||||||
from .globals import IN_CLI, plugin_dirs
|
|
||||||
from .options import parseOpts
|
from .options import parseOpts
|
||||||
from .plugins import load_all_plugins as _load_all_plugins
|
|
||||||
from .postprocessor import (
|
from .postprocessor import (
|
||||||
FFmpegExtractAudioPP,
|
FFmpegExtractAudioPP,
|
||||||
FFmpegMergerPP,
|
FFmpegMergerPP,
|
||||||
@ -35,6 +33,7 @@ from .postprocessor import (
|
|||||||
)
|
)
|
||||||
from .update import Updater
|
from .update import Updater
|
||||||
from .utils import (
|
from .utils import (
|
||||||
|
Config,
|
||||||
NO_DEFAULT,
|
NO_DEFAULT,
|
||||||
POSTPROCESS_WHEN,
|
POSTPROCESS_WHEN,
|
||||||
DateRange,
|
DateRange,
|
||||||
@ -67,6 +66,8 @@ from .utils.networking import std_headers
|
|||||||
from .utils._utils import _UnsafeExtensionError
|
from .utils._utils import _UnsafeExtensionError
|
||||||
from .YoutubeDL import YoutubeDL
|
from .YoutubeDL import YoutubeDL
|
||||||
|
|
||||||
|
_IN_CLI = False
|
||||||
|
|
||||||
|
|
||||||
def _exit(status=0, *args):
|
def _exit(status=0, *args):
|
||||||
for msg in args:
|
for msg in args:
|
||||||
@ -432,10 +433,6 @@ def validate_options(opts):
|
|||||||
}
|
}
|
||||||
|
|
||||||
# Other options
|
# Other options
|
||||||
opts.plugin_dirs = opts.plugin_dirs
|
|
||||||
if opts.plugin_dirs is None:
|
|
||||||
opts.plugin_dirs = ['default']
|
|
||||||
|
|
||||||
if opts.playlist_items is not None:
|
if opts.playlist_items is not None:
|
||||||
try:
|
try:
|
||||||
tuple(PlaylistEntries.parse_playlist_items(opts.playlist_items))
|
tuple(PlaylistEntries.parse_playlist_items(opts.playlist_items))
|
||||||
@ -976,6 +973,11 @@ def _real_main(argv=None):
|
|||||||
|
|
||||||
parser, opts, all_urls, ydl_opts = parse_options(argv)
|
parser, opts, all_urls, ydl_opts = parse_options(argv)
|
||||||
|
|
||||||
|
# HACK: Set the plugin dirs early on
|
||||||
|
# TODO(coletdjnz): remove when plugin globals system is implemented
|
||||||
|
if opts.plugin_dirs is not None:
|
||||||
|
Config._plugin_dirs = list(map(expand_path, opts.plugin_dirs))
|
||||||
|
|
||||||
# Dump user agent
|
# Dump user agent
|
||||||
if opts.dump_user_agent:
|
if opts.dump_user_agent:
|
||||||
ua = traverse_obj(opts.headers, 'User-Agent', casesense=False, default=std_headers['User-Agent'])
|
ua = traverse_obj(opts.headers, 'User-Agent', casesense=False, default=std_headers['User-Agent'])
|
||||||
@ -990,11 +992,6 @@ def _real_main(argv=None):
|
|||||||
if opts.ffmpeg_location:
|
if opts.ffmpeg_location:
|
||||||
FFmpegPostProcessor._ffmpeg_location.set(opts.ffmpeg_location)
|
FFmpegPostProcessor._ffmpeg_location.set(opts.ffmpeg_location)
|
||||||
|
|
||||||
# load all plugins into the global lookup
|
|
||||||
plugin_dirs.value = opts.plugin_dirs
|
|
||||||
if plugin_dirs.value:
|
|
||||||
_load_all_plugins()
|
|
||||||
|
|
||||||
with YoutubeDL(ydl_opts) as ydl:
|
with YoutubeDL(ydl_opts) as ydl:
|
||||||
pre_process = opts.update_self or opts.rm_cachedir
|
pre_process = opts.update_self or opts.rm_cachedir
|
||||||
actual_use = all_urls or opts.load_info_filename
|
actual_use = all_urls or opts.load_info_filename
|
||||||
@ -1021,9 +1018,8 @@ def _real_main(argv=None):
|
|||||||
# List of simplified targets we know are supported,
|
# List of simplified targets we know are supported,
|
||||||
# to help users know what dependencies may be required.
|
# to help users know what dependencies may be required.
|
||||||
(ImpersonateTarget('chrome'), 'curl_cffi'),
|
(ImpersonateTarget('chrome'), 'curl_cffi'),
|
||||||
(ImpersonateTarget('safari'), 'curl_cffi'),
|
|
||||||
(ImpersonateTarget('firefox'), 'curl_cffi>=0.10'),
|
|
||||||
(ImpersonateTarget('edge'), 'curl_cffi'),
|
(ImpersonateTarget('edge'), 'curl_cffi'),
|
||||||
|
(ImpersonateTarget('safari'), 'curl_cffi'),
|
||||||
]
|
]
|
||||||
|
|
||||||
available_targets = ydl._get_available_impersonate_targets()
|
available_targets = ydl._get_available_impersonate_targets()
|
||||||
@ -1039,12 +1035,12 @@ def _real_main(argv=None):
|
|||||||
|
|
||||||
for known_target, known_handler in known_targets:
|
for known_target, known_handler in known_targets:
|
||||||
if not any(
|
if not any(
|
||||||
known_target in target and known_handler.startswith(handler)
|
known_target in target and handler == known_handler
|
||||||
for target, handler in available_targets
|
for target, handler in available_targets
|
||||||
):
|
):
|
||||||
rows.insert(0, [
|
rows.append([
|
||||||
ydl._format_out(text, ydl.Styles.SUPPRESS)
|
ydl._format_out(text, ydl.Styles.SUPPRESS)
|
||||||
for text in make_row(known_target, f'{known_handler} (unavailable)')
|
for text in make_row(known_target, f'{known_handler} (not available)')
|
||||||
])
|
])
|
||||||
|
|
||||||
ydl.to_screen('[info] Available impersonate targets')
|
ydl.to_screen('[info] Available impersonate targets')
|
||||||
@ -1095,7 +1091,8 @@ def _real_main(argv=None):
|
|||||||
|
|
||||||
|
|
||||||
def main(argv=None):
|
def main(argv=None):
|
||||||
IN_CLI.value = True
|
global _IN_CLI
|
||||||
|
_IN_CLI = True
|
||||||
try:
|
try:
|
||||||
_exit(*variadic(_real_main(argv)))
|
_exit(*variadic(_real_main(argv)))
|
||||||
except (CookieLoadError, DownloadError):
|
except (CookieLoadError, DownloadError):
|
||||||
|
@ -83,7 +83,7 @@ def aes_ecb_encrypt(data, key, iv=None):
|
|||||||
@returns {int[]} encrypted data
|
@returns {int[]} encrypted data
|
||||||
"""
|
"""
|
||||||
expanded_key = key_expansion(key)
|
expanded_key = key_expansion(key)
|
||||||
block_count = ceil(len(data) / BLOCK_SIZE_BYTES)
|
block_count = int(ceil(float(len(data)) / BLOCK_SIZE_BYTES))
|
||||||
|
|
||||||
encrypted_data = []
|
encrypted_data = []
|
||||||
for i in range(block_count):
|
for i in range(block_count):
|
||||||
@ -103,7 +103,7 @@ def aes_ecb_decrypt(data, key, iv=None):
|
|||||||
@returns {int[]} decrypted data
|
@returns {int[]} decrypted data
|
||||||
"""
|
"""
|
||||||
expanded_key = key_expansion(key)
|
expanded_key = key_expansion(key)
|
||||||
block_count = ceil(len(data) / BLOCK_SIZE_BYTES)
|
block_count = int(ceil(float(len(data)) / BLOCK_SIZE_BYTES))
|
||||||
|
|
||||||
encrypted_data = []
|
encrypted_data = []
|
||||||
for i in range(block_count):
|
for i in range(block_count):
|
||||||
@ -134,7 +134,7 @@ def aes_ctr_encrypt(data, key, iv):
|
|||||||
@returns {int[]} encrypted data
|
@returns {int[]} encrypted data
|
||||||
"""
|
"""
|
||||||
expanded_key = key_expansion(key)
|
expanded_key = key_expansion(key)
|
||||||
block_count = ceil(len(data) / BLOCK_SIZE_BYTES)
|
block_count = int(ceil(float(len(data)) / BLOCK_SIZE_BYTES))
|
||||||
counter = iter_vector(iv)
|
counter = iter_vector(iv)
|
||||||
|
|
||||||
encrypted_data = []
|
encrypted_data = []
|
||||||
@ -158,7 +158,7 @@ def aes_cbc_decrypt(data, key, iv):
|
|||||||
@returns {int[]} decrypted data
|
@returns {int[]} decrypted data
|
||||||
"""
|
"""
|
||||||
expanded_key = key_expansion(key)
|
expanded_key = key_expansion(key)
|
||||||
block_count = ceil(len(data) / BLOCK_SIZE_BYTES)
|
block_count = int(ceil(float(len(data)) / BLOCK_SIZE_BYTES))
|
||||||
|
|
||||||
decrypted_data = []
|
decrypted_data = []
|
||||||
previous_cipher_block = iv
|
previous_cipher_block = iv
|
||||||
@ -183,7 +183,7 @@ def aes_cbc_encrypt(data, key, iv, *, padding_mode='pkcs7'):
|
|||||||
@returns {int[]} encrypted data
|
@returns {int[]} encrypted data
|
||||||
"""
|
"""
|
||||||
expanded_key = key_expansion(key)
|
expanded_key = key_expansion(key)
|
||||||
block_count = ceil(len(data) / BLOCK_SIZE_BYTES)
|
block_count = int(ceil(float(len(data)) / BLOCK_SIZE_BYTES))
|
||||||
|
|
||||||
encrypted_data = []
|
encrypted_data = []
|
||||||
previous_cipher_block = iv
|
previous_cipher_block = iv
|
||||||
|
@ -30,12 +30,11 @@ from .hls import HlsFD
|
|||||||
from .http import HttpFD
|
from .http import HttpFD
|
||||||
from .ism import IsmFD
|
from .ism import IsmFD
|
||||||
from .mhtml import MhtmlFD
|
from .mhtml import MhtmlFD
|
||||||
from .niconico import NiconicoLiveFD
|
from .niconico import NiconicoDmcFD, NiconicoLiveFD
|
||||||
from .rtmp import RtmpFD
|
from .rtmp import RtmpFD
|
||||||
from .rtsp import RtspFD
|
from .rtsp import RtspFD
|
||||||
from .websocket import WebSocketFragmentFD
|
from .websocket import WebSocketFragmentFD
|
||||||
from .youtube_live_chat import YoutubeLiveChatFD
|
from .youtube_live_chat import YoutubeLiveChatFD
|
||||||
from .bunnycdn import BunnyCdnFD
|
|
||||||
|
|
||||||
PROTOCOL_MAP = {
|
PROTOCOL_MAP = {
|
||||||
'rtmp': RtmpFD,
|
'rtmp': RtmpFD,
|
||||||
@ -50,12 +49,12 @@ PROTOCOL_MAP = {
|
|||||||
'http_dash_segments_generator': DashSegmentsFD,
|
'http_dash_segments_generator': DashSegmentsFD,
|
||||||
'ism': IsmFD,
|
'ism': IsmFD,
|
||||||
'mhtml': MhtmlFD,
|
'mhtml': MhtmlFD,
|
||||||
|
'niconico_dmc': NiconicoDmcFD,
|
||||||
'niconico_live': NiconicoLiveFD,
|
'niconico_live': NiconicoLiveFD,
|
||||||
'fc2_live': FC2LiveFD,
|
'fc2_live': FC2LiveFD,
|
||||||
'websocket_frag': WebSocketFragmentFD,
|
'websocket_frag': WebSocketFragmentFD,
|
||||||
'youtube_live_chat': YoutubeLiveChatFD,
|
'youtube_live_chat': YoutubeLiveChatFD,
|
||||||
'youtube_live_chat_replay': YoutubeLiveChatFD,
|
'youtube_live_chat_replay': YoutubeLiveChatFD,
|
||||||
'bunnycdn': BunnyCdnFD,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -66,6 +65,7 @@ def shorten_protocol_name(proto, simplify=False):
|
|||||||
'rtmp_ffmpeg': 'rtmpF',
|
'rtmp_ffmpeg': 'rtmpF',
|
||||||
'http_dash_segments': 'dash',
|
'http_dash_segments': 'dash',
|
||||||
'http_dash_segments_generator': 'dashG',
|
'http_dash_segments_generator': 'dashG',
|
||||||
|
'niconico_dmc': 'dmc',
|
||||||
'websocket_frag': 'WSfrag',
|
'websocket_frag': 'WSfrag',
|
||||||
}
|
}
|
||||||
if simplify:
|
if simplify:
|
||||||
|
@ -1,50 +0,0 @@
|
|||||||
import hashlib
|
|
||||||
import random
|
|
||||||
import threading
|
|
||||||
|
|
||||||
from .common import FileDownloader
|
|
||||||
from . import HlsFD
|
|
||||||
from ..networking import Request
|
|
||||||
from ..networking.exceptions import network_exceptions
|
|
||||||
|
|
||||||
|
|
||||||
class BunnyCdnFD(FileDownloader):
|
|
||||||
"""
|
|
||||||
Downloads from BunnyCDN with required pings
|
|
||||||
Note, this is not a part of public API, and will be removed without notice.
|
|
||||||
DO NOT USE
|
|
||||||
"""
|
|
||||||
|
|
||||||
def real_download(self, filename, info_dict):
|
|
||||||
self.to_screen(f'[{self.FD_NAME}] Downloading from BunnyCDN')
|
|
||||||
|
|
||||||
fd = HlsFD(self.ydl, self.params)
|
|
||||||
|
|
||||||
stop_event = threading.Event()
|
|
||||||
ping_thread = threading.Thread(target=self.ping_thread, args=(stop_event,), kwargs=info_dict['_bunnycdn_ping_data'])
|
|
||||||
ping_thread.start()
|
|
||||||
|
|
||||||
try:
|
|
||||||
return fd.real_download(filename, info_dict)
|
|
||||||
finally:
|
|
||||||
stop_event.set()
|
|
||||||
|
|
||||||
def ping_thread(self, stop_event, url, headers, secret, context_id):
|
|
||||||
# Site sends ping every 4 seconds, but this throttles the download. Pinging every 2 seconds seems to work.
|
|
||||||
ping_interval = 2
|
|
||||||
# Hard coded resolution as it doesn't seem to matter
|
|
||||||
res = 1080
|
|
||||||
paused = 'false'
|
|
||||||
current_time = 0
|
|
||||||
|
|
||||||
while not stop_event.wait(ping_interval):
|
|
||||||
current_time += ping_interval
|
|
||||||
|
|
||||||
time = current_time + round(random.random(), 6)
|
|
||||||
md5_hash = hashlib.md5(f'{secret}_{context_id}_{time}_{paused}_{res}'.encode()).hexdigest()
|
|
||||||
ping_url = f'{url}?hash={md5_hash}&time={time}&paused={paused}&resolution={res}'
|
|
||||||
|
|
||||||
try:
|
|
||||||
self.ydl.urlopen(Request(ping_url, headers=headers)).read()
|
|
||||||
except network_exceptions as e:
|
|
||||||
self.to_screen(f'[{self.FD_NAME}] Ping failed: {e}')
|
|
@ -31,7 +31,6 @@ from ..utils import (
|
|||||||
timetuple_from_msec,
|
timetuple_from_msec,
|
||||||
try_call,
|
try_call,
|
||||||
)
|
)
|
||||||
from ..utils._utils import _ProgressState
|
|
||||||
|
|
||||||
|
|
||||||
class FileDownloader:
|
class FileDownloader:
|
||||||
@ -334,7 +333,7 @@ class FileDownloader:
|
|||||||
progress_dict), s.get('progress_idx') or 0)
|
progress_dict), s.get('progress_idx') or 0)
|
||||||
self.to_console_title(self.ydl.evaluate_outtmpl(
|
self.to_console_title(self.ydl.evaluate_outtmpl(
|
||||||
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), _ProgressState.from_dict(s), s.get('_percent'))
|
progress_dict))
|
||||||
|
|
||||||
def _format_progress(self, *args, **kwargs):
|
def _format_progress(self, *args, **kwargs):
|
||||||
return self.ydl._format_text(
|
return self.ydl._format_text(
|
||||||
@ -358,7 +357,6 @@ class FileDownloader:
|
|||||||
'_speed_str': self.format_speed(speed).strip(),
|
'_speed_str': self.format_speed(speed).strip(),
|
||||||
'_total_bytes_str': _format_bytes('total_bytes'),
|
'_total_bytes_str': _format_bytes('total_bytes'),
|
||||||
'_elapsed_str': self.format_seconds(s.get('elapsed')),
|
'_elapsed_str': self.format_seconds(s.get('elapsed')),
|
||||||
'_percent': 100.0,
|
|
||||||
'_percent_str': self.format_percent(100),
|
'_percent_str': self.format_percent(100),
|
||||||
})
|
})
|
||||||
self._report_progress_status(s, join_nonempty(
|
self._report_progress_status(s, join_nonempty(
|
||||||
@ -377,15 +375,13 @@ class FileDownloader:
|
|||||||
return
|
return
|
||||||
self._progress_delta_time += update_delta
|
self._progress_delta_time += update_delta
|
||||||
|
|
||||||
progress = try_call(
|
|
||||||
lambda: 100 * s['downloaded_bytes'] / s['total_bytes'],
|
|
||||||
lambda: 100 * s['downloaded_bytes'] / s['total_bytes_estimate'],
|
|
||||||
lambda: s['downloaded_bytes'] == 0 and 0)
|
|
||||||
s.update({
|
s.update({
|
||||||
'_eta_str': self.format_eta(s.get('eta')).strip(),
|
'_eta_str': self.format_eta(s.get('eta')).strip(),
|
||||||
'_speed_str': self.format_speed(s.get('speed')),
|
'_speed_str': self.format_speed(s.get('speed')),
|
||||||
'_percent': progress,
|
'_percent_str': self.format_percent(try_call(
|
||||||
'_percent_str': self.format_percent(progress),
|
lambda: 100 * s['downloaded_bytes'] / s['total_bytes'],
|
||||||
|
lambda: 100 * s['downloaded_bytes'] / s['total_bytes_estimate'],
|
||||||
|
lambda: s['downloaded_bytes'] == 0 and 0)),
|
||||||
'_total_bytes_str': _format_bytes('total_bytes'),
|
'_total_bytes_str': _format_bytes('total_bytes'),
|
||||||
'_total_bytes_estimate_str': _format_bytes('total_bytes_estimate'),
|
'_total_bytes_estimate_str': _format_bytes('total_bytes_estimate'),
|
||||||
'_downloaded_bytes_str': _format_bytes('downloaded_bytes'),
|
'_downloaded_bytes_str': _format_bytes('downloaded_bytes'),
|
||||||
|
@ -457,6 +457,8 @@ class FFmpegFD(ExternalFD):
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def available(cls, path=None):
|
def available(cls, path=None):
|
||||||
|
# TODO: Fix path for ffmpeg
|
||||||
|
# Fixme: This may be wrong when --ffmpeg-location is used
|
||||||
return FFmpegPostProcessor().available
|
return FFmpegPostProcessor().available
|
||||||
|
|
||||||
def on_process_started(self, proc, stdin):
|
def on_process_started(self, proc, stdin):
|
||||||
|
@ -2,12 +2,60 @@ import json
|
|||||||
import threading
|
import threading
|
||||||
import time
|
import time
|
||||||
|
|
||||||
|
from . import get_suitable_downloader
|
||||||
from .common import FileDownloader
|
from .common import FileDownloader
|
||||||
from .external import FFmpegFD
|
from .external import FFmpegFD
|
||||||
from ..networking import Request
|
from ..networking import Request
|
||||||
from ..utils import DownloadError, str_or_none, try_get
|
from ..utils import DownloadError, str_or_none, try_get
|
||||||
|
|
||||||
|
|
||||||
|
class NiconicoDmcFD(FileDownloader):
|
||||||
|
""" Downloading niconico douga from DMC with heartbeat """
|
||||||
|
|
||||||
|
def real_download(self, filename, info_dict):
|
||||||
|
from ..extractor.niconico import NiconicoIE
|
||||||
|
|
||||||
|
self.to_screen(f'[{self.FD_NAME}] Downloading from DMC')
|
||||||
|
ie = NiconicoIE(self.ydl)
|
||||||
|
info_dict, heartbeat_info_dict = ie._get_heartbeat_info(info_dict)
|
||||||
|
|
||||||
|
fd = get_suitable_downloader(info_dict, params=self.params)(self.ydl, self.params)
|
||||||
|
|
||||||
|
success = download_complete = False
|
||||||
|
timer = [None]
|
||||||
|
heartbeat_lock = threading.Lock()
|
||||||
|
heartbeat_url = heartbeat_info_dict['url']
|
||||||
|
heartbeat_data = heartbeat_info_dict['data'].encode()
|
||||||
|
heartbeat_interval = heartbeat_info_dict.get('interval', 30)
|
||||||
|
|
||||||
|
request = Request(heartbeat_url, heartbeat_data)
|
||||||
|
|
||||||
|
def heartbeat():
|
||||||
|
try:
|
||||||
|
self.ydl.urlopen(request).read()
|
||||||
|
except Exception:
|
||||||
|
self.to_screen(f'[{self.FD_NAME}] Heartbeat failed')
|
||||||
|
|
||||||
|
with heartbeat_lock:
|
||||||
|
if not download_complete:
|
||||||
|
timer[0] = threading.Timer(heartbeat_interval, heartbeat)
|
||||||
|
timer[0].start()
|
||||||
|
|
||||||
|
heartbeat_info_dict['ping']()
|
||||||
|
self.to_screen('[%s] Heartbeat with %d second interval ...' % (self.FD_NAME, heartbeat_interval))
|
||||||
|
try:
|
||||||
|
heartbeat()
|
||||||
|
if type(fd).__name__ == 'HlsFD':
|
||||||
|
info_dict.update(ie._extract_m3u8_formats(info_dict['url'], info_dict['id'])[0])
|
||||||
|
success = fd.real_download(filename, info_dict)
|
||||||
|
finally:
|
||||||
|
if heartbeat_lock:
|
||||||
|
with heartbeat_lock:
|
||||||
|
timer[0].cancel()
|
||||||
|
download_complete = True
|
||||||
|
return success
|
||||||
|
|
||||||
|
|
||||||
class NiconicoLiveFD(FileDownloader):
|
class NiconicoLiveFD(FileDownloader):
|
||||||
""" Downloads niconico live without being stopped """
|
""" Downloads niconico live without being stopped """
|
||||||
|
|
||||||
@ -37,7 +85,6 @@ class NiconicoLiveFD(FileDownloader):
|
|||||||
'quality': live_quality,
|
'quality': live_quality,
|
||||||
'protocol': 'hls+fmp4',
|
'protocol': 'hls+fmp4',
|
||||||
'latency': live_latency,
|
'latency': live_latency,
|
||||||
'accessRightMethod': 'single_cookie',
|
|
||||||
'chasePlay': False,
|
'chasePlay': False,
|
||||||
},
|
},
|
||||||
'room': {
|
'room': {
|
||||||
|
@ -1,25 +1,16 @@
|
|||||||
from ..compat.compat_utils import passthrough_module
|
from ..compat.compat_utils import passthrough_module
|
||||||
from ..globals import extractors as _extractors_context
|
|
||||||
from ..globals import plugin_ies as _plugin_ies_context
|
|
||||||
from ..plugins import PluginSpec, register_plugin_spec
|
|
||||||
|
|
||||||
passthrough_module(__name__, '.extractors')
|
passthrough_module(__name__, '.extractors')
|
||||||
del passthrough_module
|
del passthrough_module
|
||||||
|
|
||||||
register_plugin_spec(PluginSpec(
|
|
||||||
module_name='extractor',
|
|
||||||
suffix='IE',
|
|
||||||
destination=_extractors_context,
|
|
||||||
plugin_destination=_plugin_ies_context,
|
|
||||||
))
|
|
||||||
|
|
||||||
|
|
||||||
def gen_extractor_classes():
|
def gen_extractor_classes():
|
||||||
""" Return a list of supported extractors.
|
""" Return a list of supported extractors.
|
||||||
The order does matter; the first extractor matched is the one handling the URL.
|
The order does matter; the first extractor matched is the one handling the URL.
|
||||||
"""
|
"""
|
||||||
import_extractors()
|
from .extractors import _ALL_CLASSES
|
||||||
return list(_extractors_context.value.values())
|
|
||||||
|
return _ALL_CLASSES
|
||||||
|
|
||||||
|
|
||||||
def gen_extractors():
|
def gen_extractors():
|
||||||
@ -46,9 +37,6 @@ def list_extractors(age_limit=None):
|
|||||||
|
|
||||||
def get_info_extractor(ie_name):
|
def get_info_extractor(ie_name):
|
||||||
"""Returns the info extractor class with the given ie_name"""
|
"""Returns the info extractor class with the given ie_name"""
|
||||||
import_extractors()
|
from . import extractors
|
||||||
return _extractors_context.value[f'{ie_name}IE']
|
|
||||||
|
|
||||||
|
return getattr(extractors, f'{ie_name}IE')
|
||||||
def import_extractors():
|
|
||||||
from . import extractors # noqa: F401
|
|
||||||
|
@ -312,7 +312,6 @@ from .brilliantpala import (
|
|||||||
)
|
)
|
||||||
from .bundesliga import BundesligaIE
|
from .bundesliga import BundesligaIE
|
||||||
from .bundestag import BundestagIE
|
from .bundestag import BundestagIE
|
||||||
from .bunnycdn import BunnyCdnIE
|
|
||||||
from .businessinsider import BusinessInsiderIE
|
from .businessinsider import BusinessInsiderIE
|
||||||
from .buzzfeed import BuzzFeedIE
|
from .buzzfeed import BuzzFeedIE
|
||||||
from .byutv import BYUtvIE
|
from .byutv import BYUtvIE
|
||||||
@ -336,7 +335,6 @@ from .canal1 import Canal1IE
|
|||||||
from .canalalpha import CanalAlphaIE
|
from .canalalpha import CanalAlphaIE
|
||||||
from .canalc2 import Canalc2IE
|
from .canalc2 import Canalc2IE
|
||||||
from .canalplus import CanalplusIE
|
from .canalplus import CanalplusIE
|
||||||
from .canalsurmas import CanalsurmasIE
|
|
||||||
from .caracoltv import CaracolTvPlayIE
|
from .caracoltv import CaracolTvPlayIE
|
||||||
from .cartoonnetwork import CartoonNetworkIE
|
from .cartoonnetwork import CartoonNetworkIE
|
||||||
from .cbc import (
|
from .cbc import (
|
||||||
@ -496,6 +494,10 @@ from .daum import (
|
|||||||
from .daystar import DaystarClipIE
|
from .daystar import DaystarClipIE
|
||||||
from .dbtv import DBTVIE
|
from .dbtv import DBTVIE
|
||||||
from .dctp import DctpTvIE
|
from .dctp import DctpTvIE
|
||||||
|
from .deezer import (
|
||||||
|
DeezerAlbumIE,
|
||||||
|
DeezerPlaylistIE,
|
||||||
|
)
|
||||||
from .democracynow import DemocracynowIE
|
from .democracynow import DemocracynowIE
|
||||||
from .detik import DetikEmbedIE
|
from .detik import DetikEmbedIE
|
||||||
from .deuxm import (
|
from .deuxm import (
|
||||||
@ -683,7 +685,6 @@ from .foxnews import (
|
|||||||
)
|
)
|
||||||
from .foxsports import FoxSportsIE
|
from .foxsports import FoxSportsIE
|
||||||
from .fptplay import FptplayIE
|
from .fptplay import FptplayIE
|
||||||
from .francaisfacile import FrancaisFacileIE
|
|
||||||
from .franceinter import FranceInterIE
|
from .franceinter import FranceInterIE
|
||||||
from .francetv import (
|
from .francetv import (
|
||||||
FranceTVIE,
|
FranceTVIE,
|
||||||
@ -840,7 +841,6 @@ from .icareus import IcareusIE
|
|||||||
from .ichinanalive import (
|
from .ichinanalive import (
|
||||||
IchinanaLiveClipIE,
|
IchinanaLiveClipIE,
|
||||||
IchinanaLiveIE,
|
IchinanaLiveIE,
|
||||||
IchinanaLiveVODIE,
|
|
||||||
)
|
)
|
||||||
from .idolplus import IdolPlusIE
|
from .idolplus import IdolPlusIE
|
||||||
from .ign import (
|
from .ign import (
|
||||||
@ -903,7 +903,6 @@ from .ivi import (
|
|||||||
IviIE,
|
IviIE,
|
||||||
)
|
)
|
||||||
from .ivideon import IvideonIE
|
from .ivideon import IvideonIE
|
||||||
from .ivoox import IvooxIE
|
|
||||||
from .iwara import (
|
from .iwara import (
|
||||||
IwaraIE,
|
IwaraIE,
|
||||||
IwaraPlaylistIE,
|
IwaraPlaylistIE,
|
||||||
@ -961,10 +960,7 @@ from .kick import (
|
|||||||
)
|
)
|
||||||
from .kicker import KickerIE
|
from .kicker import KickerIE
|
||||||
from .kickstarter import KickStarterIE
|
from .kickstarter import KickStarterIE
|
||||||
from .kika import (
|
from .kika import KikaIE
|
||||||
KikaIE,
|
|
||||||
KikaPlaylistIE,
|
|
||||||
)
|
|
||||||
from .kinja import KinjaEmbedIE
|
from .kinja import KinjaEmbedIE
|
||||||
from .kinopoisk import KinoPoiskIE
|
from .kinopoisk import KinoPoiskIE
|
||||||
from .kommunetv import KommunetvIE
|
from .kommunetv import KommunetvIE
|
||||||
@ -1042,7 +1038,6 @@ from .limelight import (
|
|||||||
LimelightMediaIE,
|
LimelightMediaIE,
|
||||||
)
|
)
|
||||||
from .linkedin import (
|
from .linkedin import (
|
||||||
LinkedInEventsIE,
|
|
||||||
LinkedInIE,
|
LinkedInIE,
|
||||||
LinkedInLearningCourseIE,
|
LinkedInLearningCourseIE,
|
||||||
LinkedInLearningIE,
|
LinkedInLearningIE,
|
||||||
@ -1058,7 +1053,6 @@ from .livestream import (
|
|||||||
)
|
)
|
||||||
from .livestreamfails import LivestreamfailsIE
|
from .livestreamfails import LivestreamfailsIE
|
||||||
from .lnk import LnkIE
|
from .lnk import LnkIE
|
||||||
from .loco import LocoIE
|
|
||||||
from .loom import (
|
from .loom import (
|
||||||
LoomFolderIE,
|
LoomFolderIE,
|
||||||
LoomIE,
|
LoomIE,
|
||||||
@ -1066,7 +1060,6 @@ from .loom import (
|
|||||||
from .lovehomeporn import LoveHomePornIE
|
from .lovehomeporn import LoveHomePornIE
|
||||||
from .lrt import (
|
from .lrt import (
|
||||||
LRTVODIE,
|
LRTVODIE,
|
||||||
LRTRadioIE,
|
|
||||||
LRTStreamIE,
|
LRTStreamIE,
|
||||||
)
|
)
|
||||||
from .lsm import (
|
from .lsm import (
|
||||||
@ -1499,10 +1492,6 @@ from .paramountplus import (
|
|||||||
)
|
)
|
||||||
from .parler import ParlerIE
|
from .parler import ParlerIE
|
||||||
from .parlview import ParlviewIE
|
from .parlview import ParlviewIE
|
||||||
from .parti import (
|
|
||||||
PartiLivestreamIE,
|
|
||||||
PartiVideoIE,
|
|
||||||
)
|
|
||||||
from .patreon import (
|
from .patreon import (
|
||||||
PatreonCampaignIE,
|
PatreonCampaignIE,
|
||||||
PatreonIE,
|
PatreonIE,
|
||||||
@ -1749,7 +1738,6 @@ from .roosterteeth import (
|
|||||||
RoosterTeethSeriesIE,
|
RoosterTeethSeriesIE,
|
||||||
)
|
)
|
||||||
from .rottentomatoes import RottenTomatoesIE
|
from .rottentomatoes import RottenTomatoesIE
|
||||||
from .roya import RoyaLiveIE
|
|
||||||
from .rozhlas import (
|
from .rozhlas import (
|
||||||
MujRozhlasIE,
|
MujRozhlasIE,
|
||||||
RozhlasIE,
|
RozhlasIE,
|
||||||
@ -1784,6 +1772,7 @@ from .rtvcplay import (
|
|||||||
from .rtve import (
|
from .rtve import (
|
||||||
RTVEALaCartaIE,
|
RTVEALaCartaIE,
|
||||||
RTVEAudioIE,
|
RTVEAudioIE,
|
||||||
|
RTVEInfantilIE,
|
||||||
RTVELiveIE,
|
RTVELiveIE,
|
||||||
RTVETelevisionIE,
|
RTVETelevisionIE,
|
||||||
)
|
)
|
||||||
@ -1892,8 +1881,6 @@ from .skyit import (
|
|||||||
SkyItVideoIE,
|
SkyItVideoIE,
|
||||||
SkyItVideoLiveIE,
|
SkyItVideoLiveIE,
|
||||||
TV8ItIE,
|
TV8ItIE,
|
||||||
TV8ItLiveIE,
|
|
||||||
TV8ItPlaylistIE,
|
|
||||||
)
|
)
|
||||||
from .skylinewebcams import SkylineWebcamsIE
|
from .skylinewebcams import SkylineWebcamsIE
|
||||||
from .skynewsarabia import (
|
from .skynewsarabia import (
|
||||||
@ -1907,7 +1894,6 @@ from .slutload import SlutloadIE
|
|||||||
from .smotrim import SmotrimIE
|
from .smotrim import SmotrimIE
|
||||||
from .snapchat import SnapchatSpotlightIE
|
from .snapchat import SnapchatSpotlightIE
|
||||||
from .snotr import SnotrIE
|
from .snotr import SnotrIE
|
||||||
from .softwhiteunderbelly import SoftWhiteUnderbellyIE
|
|
||||||
from .sohu import (
|
from .sohu import (
|
||||||
SohuIE,
|
SohuIE,
|
||||||
SohuVIE,
|
SohuVIE,
|
||||||
@ -1997,7 +1983,6 @@ from .storyfire import (
|
|||||||
StoryFireSeriesIE,
|
StoryFireSeriesIE,
|
||||||
StoryFireUserIE,
|
StoryFireUserIE,
|
||||||
)
|
)
|
||||||
from .streaks import StreaksIE
|
|
||||||
from .streamable import StreamableIE
|
from .streamable import StreamableIE
|
||||||
from .streamcz import StreamCZIE
|
from .streamcz import StreamCZIE
|
||||||
from .streetvoice import StreetVoiceIE
|
from .streetvoice import StreetVoiceIE
|
||||||
@ -2237,10 +2222,6 @@ from .tvplay import (
|
|||||||
TVPlayIE,
|
TVPlayIE,
|
||||||
)
|
)
|
||||||
from .tvplayer import TVPlayerIE
|
from .tvplayer import TVPlayerIE
|
||||||
from .tvw import (
|
|
||||||
TvwIE,
|
|
||||||
TvwTvChannelsIE,
|
|
||||||
)
|
|
||||||
from .tweakers import TweakersIE
|
from .tweakers import TweakersIE
|
||||||
from .twentymin import TwentyMinutenIE
|
from .twentymin import TwentyMinutenIE
|
||||||
from .twentythreevideo import TwentyThreeVideoIE
|
from .twentythreevideo import TwentyThreeVideoIE
|
||||||
@ -2364,6 +2345,10 @@ from .viewlift import (
|
|||||||
ViewLiftIE,
|
ViewLiftIE,
|
||||||
)
|
)
|
||||||
from .viidea import ViideaIE
|
from .viidea import ViideaIE
|
||||||
|
from .viki import (
|
||||||
|
VikiChannelIE,
|
||||||
|
VikiIE,
|
||||||
|
)
|
||||||
from .vimeo import (
|
from .vimeo import (
|
||||||
VHXEmbedIE,
|
VHXEmbedIE,
|
||||||
VimeoAlbumIE,
|
VimeoAlbumIE,
|
||||||
@ -2408,15 +2393,10 @@ from .voxmedia import (
|
|||||||
VoxMediaIE,
|
VoxMediaIE,
|
||||||
VoxMediaVolumeIE,
|
VoxMediaVolumeIE,
|
||||||
)
|
)
|
||||||
from .vrsquare import (
|
|
||||||
VrSquareChannelIE,
|
|
||||||
VrSquareIE,
|
|
||||||
VrSquareSearchIE,
|
|
||||||
VrSquareSectionIE,
|
|
||||||
)
|
|
||||||
from .vrt import (
|
from .vrt import (
|
||||||
VRTIE,
|
VRTIE,
|
||||||
DagelijkseKostIE,
|
DagelijkseKostIE,
|
||||||
|
KetnetIE,
|
||||||
Radio1BeIE,
|
Radio1BeIE,
|
||||||
VrtNUIE,
|
VrtNUIE,
|
||||||
)
|
)
|
||||||
|
@ -21,7 +21,6 @@ from ..utils import (
|
|||||||
int_or_none,
|
int_or_none,
|
||||||
time_seconds,
|
time_seconds,
|
||||||
traverse_obj,
|
traverse_obj,
|
||||||
update_url,
|
|
||||||
update_url_query,
|
update_url_query,
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -418,10 +417,6 @@ class AbemaTVIE(AbemaTVBaseIE):
|
|||||||
'is_live': is_live,
|
'is_live': is_live,
|
||||||
'availability': availability,
|
'availability': availability,
|
||||||
})
|
})
|
||||||
|
|
||||||
if thumbnail := update_url(self._og_search_thumbnail(webpage, default=''), query=None):
|
|
||||||
info['thumbnails'] = [{'url': thumbnail}]
|
|
||||||
|
|
||||||
return info
|
return info
|
||||||
|
|
||||||
|
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
import datetime as dt
|
|
||||||
import functools
|
import functools
|
||||||
|
|
||||||
from .common import InfoExtractor
|
from .common import InfoExtractor
|
||||||
@ -11,7 +10,7 @@ from ..utils import (
|
|||||||
filter_dict,
|
filter_dict,
|
||||||
int_or_none,
|
int_or_none,
|
||||||
orderedSet,
|
orderedSet,
|
||||||
parse_iso8601,
|
unified_timestamp,
|
||||||
url_or_none,
|
url_or_none,
|
||||||
urlencode_postdata,
|
urlencode_postdata,
|
||||||
urljoin,
|
urljoin,
|
||||||
@ -88,9 +87,9 @@ class AfreecaTVIE(AfreecaTVBaseIE):
|
|||||||
'uploader_id': 'rlantnghks',
|
'uploader_id': 'rlantnghks',
|
||||||
'uploader': '페이즈으',
|
'uploader': '페이즈으',
|
||||||
'duration': 10840,
|
'duration': 10840,
|
||||||
'thumbnail': r're:https?://videoimg\.(?:sooplive\.co\.kr|afreecatv\.com)/.+',
|
'thumbnail': r're:https?://videoimg\.sooplive\.co/.kr/.+',
|
||||||
'upload_date': '20230108',
|
'upload_date': '20230108',
|
||||||
'timestamp': 1673186405,
|
'timestamp': 1673218805,
|
||||||
'title': '젠지 페이즈',
|
'title': '젠지 페이즈',
|
||||||
},
|
},
|
||||||
'params': {
|
'params': {
|
||||||
@ -103,7 +102,7 @@ class AfreecaTVIE(AfreecaTVBaseIE):
|
|||||||
'id': '20170411_BE689A0E_190960999_1_2_h',
|
'id': '20170411_BE689A0E_190960999_1_2_h',
|
||||||
'ext': 'mp4',
|
'ext': 'mp4',
|
||||||
'title': '혼자사는여자집',
|
'title': '혼자사는여자집',
|
||||||
'thumbnail': r're:https?://(?:video|st)img\.(?:sooplive\.co\.kr|afreecatv\.com)/.+',
|
'thumbnail': r're:https?://(?:video|st)img\.sooplive\.co\.kr/.+',
|
||||||
'uploader': '♥이슬이',
|
'uploader': '♥이슬이',
|
||||||
'uploader_id': 'dasl8121',
|
'uploader_id': 'dasl8121',
|
||||||
'upload_date': '20170411',
|
'upload_date': '20170411',
|
||||||
@ -120,7 +119,7 @@ class AfreecaTVIE(AfreecaTVBaseIE):
|
|||||||
'id': '20180327_27901457_202289533_1',
|
'id': '20180327_27901457_202289533_1',
|
||||||
'ext': 'mp4',
|
'ext': 'mp4',
|
||||||
'title': '[생]빨개요♥ (part 1)',
|
'title': '[생]빨개요♥ (part 1)',
|
||||||
'thumbnail': r're:https?://(?:video|st)img\.(?:sooplive\.co\.kr|afreecatv\.com)/.+',
|
'thumbnail': r're:https?://(?:video|st)img\.sooplive\.co\.kr/.+',
|
||||||
'uploader': '[SA]서아',
|
'uploader': '[SA]서아',
|
||||||
'uploader_id': 'bjdyrksu',
|
'uploader_id': 'bjdyrksu',
|
||||||
'upload_date': '20180327',
|
'upload_date': '20180327',
|
||||||
@ -188,7 +187,7 @@ class AfreecaTVIE(AfreecaTVBaseIE):
|
|||||||
'formats': formats,
|
'formats': formats,
|
||||||
**traverse_obj(file_element, {
|
**traverse_obj(file_element, {
|
||||||
'duration': ('duration', {int_or_none(scale=1000)}),
|
'duration': ('duration', {int_or_none(scale=1000)}),
|
||||||
'timestamp': ('file_start', {parse_iso8601(delimiter=' ', timezone=dt.timedelta(hours=9))}),
|
'timestamp': ('file_start', {unified_timestamp}),
|
||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -371,7 +370,7 @@ class AfreecaTVLiveIE(AfreecaTVBaseIE):
|
|||||||
'title': channel_info.get('TITLE') or station_info.get('station_title'),
|
'title': channel_info.get('TITLE') or station_info.get('station_title'),
|
||||||
'uploader': channel_info.get('BJNICK') or station_info.get('station_name'),
|
'uploader': channel_info.get('BJNICK') or station_info.get('station_name'),
|
||||||
'uploader_id': broadcaster_id,
|
'uploader_id': broadcaster_id,
|
||||||
'timestamp': parse_iso8601(station_info.get('broad_start'), delimiter=' ', timezone=dt.timedelta(hours=9)),
|
'timestamp': unified_timestamp(station_info.get('broad_start')),
|
||||||
'formats': formats,
|
'formats': formats,
|
||||||
'is_live': True,
|
'is_live': True,
|
||||||
'http_headers': {'Referer': url},
|
'http_headers': {'Referer': url},
|
||||||
|
@ -146,7 +146,7 @@ class TokFMPodcastIE(InfoExtractor):
|
|||||||
'url': 'https://audycje.tokfm.pl/podcast/91275,-Systemowy-rasizm-Czy-zamieszki-w-USA-po-morderstwie-w-Minneapolis-doprowadza-do-zmian-w-sluzbach-panstwowych',
|
'url': 'https://audycje.tokfm.pl/podcast/91275,-Systemowy-rasizm-Czy-zamieszki-w-USA-po-morderstwie-w-Minneapolis-doprowadza-do-zmian-w-sluzbach-panstwowych',
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
'id': '91275',
|
'id': '91275',
|
||||||
'ext': 'mp3',
|
'ext': 'aac',
|
||||||
'title': 'md5:a9b15488009065556900169fb8061cce',
|
'title': 'md5:a9b15488009065556900169fb8061cce',
|
||||||
'episode': 'md5:a9b15488009065556900169fb8061cce',
|
'episode': 'md5:a9b15488009065556900169fb8061cce',
|
||||||
'series': 'Analizy',
|
'series': 'Analizy',
|
||||||
@ -164,20 +164,23 @@ class TokFMPodcastIE(InfoExtractor):
|
|||||||
raise ExtractorError('No such podcast', expected=True)
|
raise ExtractorError('No such podcast', expected=True)
|
||||||
metadata = metadata[0]
|
metadata = metadata[0]
|
||||||
|
|
||||||
mp3_url = self._download_json(
|
formats = []
|
||||||
'https://api.podcast.radioagora.pl/api4/getSongUrl',
|
for ext in ('aac', 'mp3'):
|
||||||
media_id, 'Downloading podcast mp3 URL', query={
|
url_data = self._download_json(
|
||||||
'podcast_id': media_id,
|
f'https://api.podcast.radioagora.pl/api4/getSongUrl?podcast_id={media_id}&device_id={uuid.uuid4()}&ppre=false&audio={ext}',
|
||||||
'device_id': str(uuid.uuid4()),
|
media_id, f'Downloading podcast {ext} URL')
|
||||||
'ppre': 'false',
|
# prevents inserting the mp3 (default) multiple times
|
||||||
'audio': 'mp3',
|
if 'link_ssl' in url_data and f'.{ext}' in url_data['link_ssl']:
|
||||||
})['link_ssl']
|
formats.append({
|
||||||
|
'url': url_data['link_ssl'],
|
||||||
|
'ext': ext,
|
||||||
|
'vcodec': 'none',
|
||||||
|
'acodec': ext,
|
||||||
|
})
|
||||||
|
|
||||||
return {
|
return {
|
||||||
'id': media_id,
|
'id': media_id,
|
||||||
'url': mp3_url,
|
'formats': formats,
|
||||||
'vcodec': 'none',
|
|
||||||
'ext': 'mp3',
|
|
||||||
'title': metadata.get('podcast_name'),
|
'title': metadata.get('podcast_name'),
|
||||||
'series': metadata.get('series_name'),
|
'series': metadata.get('series_name'),
|
||||||
'episode': metadata.get('podcast_name'),
|
'episode': metadata.get('podcast_name'),
|
||||||
|
@ -1,105 +1,64 @@
|
|||||||
import urllib.parse
|
|
||||||
|
|
||||||
from .common import InfoExtractor
|
from .common import InfoExtractor
|
||||||
from ..networking.exceptions import HTTPError
|
from ..networking.exceptions import HTTPError
|
||||||
from ..utils import (
|
from ..utils import (
|
||||||
ExtractorError,
|
ExtractorError,
|
||||||
int_or_none,
|
int_or_none,
|
||||||
parse_age_limit,
|
|
||||||
url_or_none,
|
|
||||||
urlencode_postdata,
|
urlencode_postdata,
|
||||||
)
|
)
|
||||||
from ..utils.traversal import traverse_obj
|
|
||||||
|
|
||||||
|
|
||||||
class AtresPlayerIE(InfoExtractor):
|
class AtresPlayerIE(InfoExtractor):
|
||||||
_VALID_URL = r'https?://(?:www\.)?atresplayer\.com/(?:[^/?#]+/){4}(?P<display_id>.+?)_(?P<id>[0-9a-f]{24})'
|
_VALID_URL = r'https?://(?:www\.)?atresplayer\.com/[^/]+/[^/]+/[^/]+/[^/]+/(?P<display_id>.+?)_(?P<id>[0-9a-f]{24})'
|
||||||
_NETRC_MACHINE = 'atresplayer'
|
_NETRC_MACHINE = 'atresplayer'
|
||||||
_TESTS = [{
|
_TESTS = [
|
||||||
'url': 'https://www.atresplayer.com/lasexta/programas/el-objetivo/clips/mbappe-describe-como-entrenador-a-carlo-ancelotti-sabe-cuando-tiene-que-ser-padre-jefe-amigo-entrenador_67f2dfb2fb6ab0e4c7203849/',
|
{
|
||||||
|
'url': 'https://www.atresplayer.com/antena3/series/pequenas-coincidencias/temporada-1/capitulo-7-asuntos-pendientes_5d4aa2c57ed1a88fc715a615/',
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
|
'id': '5d4aa2c57ed1a88fc715a615',
|
||||||
'ext': 'mp4',
|
'ext': 'mp4',
|
||||||
'id': '67f2dfb2fb6ab0e4c7203849',
|
'title': 'Capítulo 7: Asuntos pendientes',
|
||||||
'display_id': 'md5:c203f8d4e425ed115ba56a1c6e4b3e6c',
|
'description': 'md5:7634cdcb4d50d5381bedf93efb537fbc',
|
||||||
'title': 'Mbappé describe como entrenador a Carlo Ancelotti: "Sabe cuándo tiene que ser padre, jefe, amigo, entrenador..."',
|
'duration': 3413,
|
||||||
'channel': 'laSexta',
|
|
||||||
'duration': 31,
|
|
||||||
'thumbnail': 'https://imagenes.atresplayer.com/atp/clipping/cmsimages02/2025/04/06/B02DBE1E-D59B-4683-8404-1A9595D15269/1920x1080.jpg',
|
|
||||||
'tags': ['Entrevista informativa', 'Actualidad', 'Debate informativo', 'Política', 'Economía', 'Sociedad', 'Cara a cara', 'Análisis', 'Más periodismo'],
|
|
||||||
'series': 'El Objetivo',
|
|
||||||
'season': 'Temporada 12',
|
|
||||||
'timestamp': 1743970079,
|
|
||||||
'upload_date': '20250406',
|
|
||||||
},
|
},
|
||||||
}, {
|
'skip': 'This video is only available for registered users',
|
||||||
'url': 'https://www.atresplayer.com/antena3/programas/el-hormiguero/clips/revive-la-entrevista-completa-a-miguel-bose-en-el-hormiguero_67f836baa4a5b0e4147ca59a/',
|
|
||||||
'info_dict': {
|
|
||||||
'ext': 'mp4',
|
|
||||||
'id': '67f836baa4a5b0e4147ca59a',
|
|
||||||
'display_id': 'revive-la-entrevista-completa-a-miguel-bose-en-el-hormiguero',
|
|
||||||
'title': 'Revive la entrevista completa a Miguel Bosé en El Hormiguero',
|
|
||||||
'description': 'md5:c6d2b591408d45a7bc2986dfb938eb72',
|
|
||||||
'channel': 'Antena 3',
|
|
||||||
'duration': 2556,
|
|
||||||
'thumbnail': 'https://imagenes.atresplayer.com/atp/clipping/cmsimages02/2025/04/10/9076395F-F1FD-48BE-9F18-540DBA10EBAD/1920x1080.jpg',
|
|
||||||
'tags': ['Entrevista', 'Variedades', 'Humor', 'Entretenimiento', 'Te sigo', 'Buen rollo', 'Cara a cara'],
|
|
||||||
'series': 'El Hormiguero ',
|
|
||||||
'season': 'Temporada 14',
|
|
||||||
'timestamp': 1744320111,
|
|
||||||
'upload_date': '20250410',
|
|
||||||
},
|
},
|
||||||
}, {
|
{
|
||||||
'url': 'https://www.atresplayer.com/flooxer/series/biara-proyecto-lazarus/temporada-1/capitulo-3-supervivientes_67a6038b64ceca00070f4f69/',
|
|
||||||
'info_dict': {
|
|
||||||
'ext': 'mp4',
|
|
||||||
'id': '67a6038b64ceca00070f4f69',
|
|
||||||
'display_id': 'capitulo-3-supervivientes',
|
|
||||||
'title': 'Capítulo 3: Supervivientes',
|
|
||||||
'description': 'md5:65b231f20302f776c2b0dd24594599a1',
|
|
||||||
'channel': 'Flooxer',
|
|
||||||
'duration': 1196,
|
|
||||||
'thumbnail': 'https://imagenes.atresplayer.com/atp/clipping/cmsimages01/2025/02/14/17CF90D3-FE67-40C5-A941-7825B3E13992/1920x1080.jpg',
|
|
||||||
'tags': ['Juvenil', 'Terror', 'Piel de gallina', 'Te sigo', 'Un break', 'Del tirón'],
|
|
||||||
'series': 'BIARA: Proyecto Lázarus',
|
|
||||||
'season': 'Temporada 1',
|
|
||||||
'season_number': 1,
|
|
||||||
'episode': 'Episode 3',
|
|
||||||
'episode_number': 3,
|
|
||||||
'timestamp': 1743095191,
|
|
||||||
'upload_date': '20250327',
|
|
||||||
},
|
|
||||||
}, {
|
|
||||||
'url': 'https://www.atresplayer.com/lasexta/programas/el-club-de-la-comedia/temporada-4/capitulo-10-especial-solidario-nochebuena_5ad08edf986b2855ed47adc4/',
|
'url': 'https://www.atresplayer.com/lasexta/programas/el-club-de-la-comedia/temporada-4/capitulo-10-especial-solidario-nochebuena_5ad08edf986b2855ed47adc4/',
|
||||||
'only_matching': True,
|
'only_matching': True,
|
||||||
}, {
|
},
|
||||||
|
{
|
||||||
'url': 'https://www.atresplayer.com/antena3/series/el-secreto-de-puente-viejo/el-chico-de-los-tres-lunares/capitulo-977-29-12-14_5ad51046986b2886722ccdea/',
|
'url': 'https://www.atresplayer.com/antena3/series/el-secreto-de-puente-viejo/el-chico-de-los-tres-lunares/capitulo-977-29-12-14_5ad51046986b2886722ccdea/',
|
||||||
'only_matching': True,
|
'only_matching': True,
|
||||||
}]
|
},
|
||||||
|
]
|
||||||
_API_BASE = 'https://api.atresplayer.com/'
|
_API_BASE = 'https://api.atresplayer.com/'
|
||||||
|
|
||||||
def _perform_login(self, username, password):
|
def _perform_login(self, username, password):
|
||||||
|
self._request_webpage(
|
||||||
|
self._API_BASE + 'login', None, 'Downloading login page')
|
||||||
|
|
||||||
try:
|
try:
|
||||||
self._download_webpage(
|
target_url = self._download_json(
|
||||||
'https://account.atresplayer.com/auth/v1/login', None,
|
'https://account.atresmedia.com/api/login', None,
|
||||||
'Logging in', 'Failed to log in', data=urlencode_postdata({
|
'Logging in', headers={
|
||||||
|
'Content-Type': 'application/x-www-form-urlencoded',
|
||||||
|
}, data=urlencode_postdata({
|
||||||
'username': username,
|
'username': username,
|
||||||
'password': password,
|
'password': password,
|
||||||
}))
|
}))['targetUrl']
|
||||||
except ExtractorError as e:
|
except ExtractorError as e:
|
||||||
if isinstance(e.cause, HTTPError) and e.cause.status == 400:
|
if isinstance(e.cause, HTTPError) and e.cause.status == 400:
|
||||||
raise ExtractorError('Invalid username and/or password', expected=True)
|
raise ExtractorError('Invalid username and/or password', expected=True)
|
||||||
raise
|
raise
|
||||||
|
|
||||||
|
self._request_webpage(target_url, None, 'Following Target URL')
|
||||||
|
|
||||||
def _real_extract(self, url):
|
def _real_extract(self, url):
|
||||||
display_id, video_id = self._match_valid_url(url).groups()
|
display_id, video_id = self._match_valid_url(url).groups()
|
||||||
|
|
||||||
metadata_url = self._download_json(
|
|
||||||
self._API_BASE + 'client/v1/url', video_id, 'Downloading API endpoint data',
|
|
||||||
query={'href': urllib.parse.urlparse(url).path})['href']
|
|
||||||
metadata = self._download_json(metadata_url, video_id)
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
video_data = self._download_json(metadata['urlVideo'], video_id, 'Downloading video data')
|
episode = self._download_json(
|
||||||
|
self._API_BASE + 'client/v1/player/episode/' + video_id, video_id)
|
||||||
except ExtractorError as e:
|
except ExtractorError as e:
|
||||||
if isinstance(e.cause, HTTPError) and e.cause.status == 403:
|
if isinstance(e.cause, HTTPError) and e.cause.status == 403:
|
||||||
error = self._parse_json(e.cause.response.read(), None)
|
error = self._parse_json(e.cause.response.read(), None)
|
||||||
@ -108,45 +67,37 @@ class AtresPlayerIE(InfoExtractor):
|
|||||||
raise ExtractorError(error['error_description'], expected=True)
|
raise ExtractorError(error['error_description'], expected=True)
|
||||||
raise
|
raise
|
||||||
|
|
||||||
|
title = episode['titulo']
|
||||||
|
|
||||||
formats = []
|
formats = []
|
||||||
subtitles = {}
|
subtitles = {}
|
||||||
for source in traverse_obj(video_data, ('sources', lambda _, v: url_or_none(v['src']))):
|
for source in episode.get('sources', []):
|
||||||
src_url = source['src']
|
src = source.get('src')
|
||||||
src_type = source.get('type')
|
if not src:
|
||||||
if src_type in ('application/vnd.apple.mpegurl', 'application/hls+legacy', 'application/hls+hevc'):
|
|
||||||
fmts, subs = self._extract_m3u8_formats_and_subtitles(
|
|
||||||
src_url, video_id, 'mp4', m3u8_id='hls', fatal=False)
|
|
||||||
elif src_type in ('application/dash+xml', 'application/dash+hevc'):
|
|
||||||
fmts, subs = self._extract_mpd_formats_and_subtitles(
|
|
||||||
src_url, video_id, mpd_id='dash', fatal=False)
|
|
||||||
else:
|
|
||||||
continue
|
continue
|
||||||
formats.extend(fmts)
|
src_type = source.get('type')
|
||||||
self._merge_subtitles(subs, target=subtitles)
|
if src_type == 'application/vnd.apple.mpegurl':
|
||||||
|
formats, subtitles = self._extract_m3u8_formats(
|
||||||
|
src, video_id, 'mp4', 'm3u8_native',
|
||||||
|
m3u8_id='hls', fatal=False)
|
||||||
|
elif src_type == 'application/dash+xml':
|
||||||
|
formats, subtitles = self._extract_mpd_formats(
|
||||||
|
src, video_id, mpd_id='dash', fatal=False)
|
||||||
|
|
||||||
|
heartbeat = episode.get('heartbeat') or {}
|
||||||
|
omniture = episode.get('omniture') or {}
|
||||||
|
get_meta = lambda x: heartbeat.get(x) or omniture.get(x)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
'display_id': display_id,
|
'display_id': display_id,
|
||||||
'id': video_id,
|
'id': video_id,
|
||||||
|
'title': title,
|
||||||
|
'description': episode.get('descripcion'),
|
||||||
|
'thumbnail': episode.get('imgPoster'),
|
||||||
|
'duration': int_or_none(episode.get('duration')),
|
||||||
'formats': formats,
|
'formats': formats,
|
||||||
|
'channel': get_meta('channel'),
|
||||||
|
'season': get_meta('season'),
|
||||||
|
'episode_number': int_or_none(get_meta('episodeNumber')),
|
||||||
'subtitles': subtitles,
|
'subtitles': subtitles,
|
||||||
**traverse_obj(video_data, {
|
|
||||||
'title': ('titulo', {str}),
|
|
||||||
'description': ('descripcion', {str}),
|
|
||||||
'duration': ('duration', {int_or_none}),
|
|
||||||
'thumbnail': ('imgPoster', {url_or_none}, {lambda v: f'{v}1920x1080.jpg'}),
|
|
||||||
'age_limit': ('ageRating', {parse_age_limit}),
|
|
||||||
}),
|
|
||||||
**traverse_obj(metadata, {
|
|
||||||
'title': ('title', {str}),
|
|
||||||
'description': ('description', {str}),
|
|
||||||
'duration': ('duration', {int_or_none}),
|
|
||||||
'tags': ('tags', ..., 'title', {str}),
|
|
||||||
'age_limit': ('ageRating', {parse_age_limit}),
|
|
||||||
'series': ('format', 'title', {str}),
|
|
||||||
'season': ('currentSeason', 'title', {str}),
|
|
||||||
'season_number': ('currentSeason', 'seasonNumber', {int_or_none}),
|
|
||||||
'episode_number': ('numberOfEpisode', {int_or_none}),
|
|
||||||
'timestamp': ('publicationDate', {int_or_none(scale=1000)}),
|
|
||||||
'channel': ('channel', 'title', {str}),
|
|
||||||
}),
|
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
|
import json
|
||||||
|
|
||||||
from .common import InfoExtractor
|
from .common import InfoExtractor
|
||||||
from .kaltura import KalturaIE
|
from .kaltura import KalturaIE
|
||||||
from ..utils.traversal import require, traverse_obj
|
|
||||||
|
|
||||||
|
|
||||||
class AZMedienIE(InfoExtractor):
|
class AZMedienIE(InfoExtractor):
|
||||||
@ -8,15 +9,15 @@ class AZMedienIE(InfoExtractor):
|
|||||||
_VALID_URL = r'''(?x)
|
_VALID_URL = r'''(?x)
|
||||||
https?://
|
https?://
|
||||||
(?:www\.|tv\.)?
|
(?:www\.|tv\.)?
|
||||||
(?:
|
(?P<host>
|
||||||
telezueri\.ch|
|
telezueri\.ch|
|
||||||
telebaern\.tv|
|
telebaern\.tv|
|
||||||
telem1\.ch|
|
telem1\.ch|
|
||||||
tvo-online\.ch
|
tvo-online\.ch
|
||||||
)/
|
)/
|
||||||
[^/?#]+/
|
[^/]+/
|
||||||
(?P<id>
|
(?P<id>
|
||||||
[^/?#]+-\d+
|
[^/]+-(?P<article_id>\d+)
|
||||||
)
|
)
|
||||||
(?:
|
(?:
|
||||||
\#video=
|
\#video=
|
||||||
@ -46,17 +47,19 @@ class AZMedienIE(InfoExtractor):
|
|||||||
'url': 'https://www.telebaern.tv/telebaern-news/montag-1-oktober-2018-ganze-sendung-133531189#video=0_7xjo9lf1',
|
'url': 'https://www.telebaern.tv/telebaern-news/montag-1-oktober-2018-ganze-sendung-133531189#video=0_7xjo9lf1',
|
||||||
'only_matching': True,
|
'only_matching': True,
|
||||||
}]
|
}]
|
||||||
|
_API_TEMPL = 'https://www.%s/api/pub/gql/%s/NewsArticleTeaser/a4016f65fe62b81dc6664dd9f4910e4ab40383be'
|
||||||
_PARTNER_ID = '1719221'
|
_PARTNER_ID = '1719221'
|
||||||
|
|
||||||
def _real_extract(self, url):
|
def _real_extract(self, url):
|
||||||
display_id, entry_id = self._match_valid_url(url).groups()
|
host, display_id, article_id, entry_id = self._match_valid_url(url).groups()
|
||||||
|
|
||||||
if not entry_id:
|
if not entry_id:
|
||||||
webpage = self._download_webpage(url, display_id)
|
entry_id = self._download_json(
|
||||||
data = self._search_json(
|
self._API_TEMPL % (host, host.split('.')[0]), display_id, query={
|
||||||
r'window\.__APOLLO_STATE__\s*=', webpage, 'video data', display_id)
|
'variables': json.dumps({
|
||||||
entry_id = traverse_obj(data, (
|
'contextId': 'NewsArticle:' + article_id,
|
||||||
lambda _, v: v['__typename'] == 'KalturaData', 'kalturaId', any, {require('kaltura id')}))
|
}),
|
||||||
|
})['data']['context']['mainAsset']['video']['kaltura']['kalturaId']
|
||||||
|
|
||||||
return self.url_result(
|
return self.url_result(
|
||||||
f'kaltura:{self._PARTNER_ID}:{entry_id}',
|
f'kaltura:{self._PARTNER_ID}:{entry_id}',
|
||||||
|
@ -86,7 +86,7 @@ class BandlabBaseIE(InfoExtractor):
|
|||||||
'webpage_url': (
|
'webpage_url': (
|
||||||
'id', ({value(url)}, {format_field(template='https://www.bandlab.com/post/%s')}), filter, any),
|
'id', ({value(url)}, {format_field(template='https://www.bandlab.com/post/%s')}), filter, any),
|
||||||
'url': ('video', 'url', {url_or_none}),
|
'url': ('video', 'url', {url_or_none}),
|
||||||
'title': ('caption', {lambda x: x.replace('\n', ' ')}, {truncate_string(left=72)}),
|
'title': ('caption', {lambda x: x.replace('\n', ' ')}, {truncate_string(left=50)}),
|
||||||
'description': ('caption', {str}),
|
'description': ('caption', {str}),
|
||||||
'thumbnail': ('video', 'picture', 'url', {url_or_none}),
|
'thumbnail': ('video', 'picture', 'url', {url_or_none}),
|
||||||
'view_count': ('video', 'counters', 'plays', {int_or_none}),
|
'view_count': ('video', 'counters', 'plays', {int_or_none}),
|
||||||
@ -120,7 +120,7 @@ class BandlabIE(BandlabBaseIE):
|
|||||||
'duration': 54.629999999999995,
|
'duration': 54.629999999999995,
|
||||||
'title': 'sweet black',
|
'title': 'sweet black',
|
||||||
'upload_date': '20231210',
|
'upload_date': '20231210',
|
||||||
'thumbnail': 'https://bl-prod-images.azureedge.net/v1.0/songs/fa082beb-b856-4730-9170-a57e4e32cc2c/',
|
'thumbnail': 'https://bandlabimages.azureedge.net/v1.0/songs/fa082beb-b856-4730-9170-a57e4e32cc2c/',
|
||||||
'genres': ['Lofi'],
|
'genres': ['Lofi'],
|
||||||
'uploader': 'ender milze',
|
'uploader': 'ender milze',
|
||||||
'comment_count': int,
|
'comment_count': int,
|
||||||
@ -142,7 +142,7 @@ class BandlabIE(BandlabBaseIE):
|
|||||||
'duration': 54.629999999999995,
|
'duration': 54.629999999999995,
|
||||||
'title': 'sweet black',
|
'title': 'sweet black',
|
||||||
'upload_date': '20231210',
|
'upload_date': '20231210',
|
||||||
'thumbnail': 'https://bl-prod-images.azureedge.net/v1.0/songs/fa082beb-b856-4730-9170-a57e4e32cc2c/',
|
'thumbnail': 'https://bandlabimages.azureedge.net/v1.0/songs/fa082beb-b856-4730-9170-a57e4e32cc2c/',
|
||||||
'genres': ['Lofi'],
|
'genres': ['Lofi'],
|
||||||
'uploader': 'ender milze',
|
'uploader': 'ender milze',
|
||||||
'comment_count': int,
|
'comment_count': int,
|
||||||
@ -158,7 +158,7 @@ class BandlabIE(BandlabBaseIE):
|
|||||||
'comment_count': int,
|
'comment_count': int,
|
||||||
'genres': ['Other'],
|
'genres': ['Other'],
|
||||||
'uploader_id': 'user8353034818103753',
|
'uploader_id': 'user8353034818103753',
|
||||||
'thumbnail': 'https://bl-prod-images.azureedge.net/v1.0/songs/51b18363-da23-4b9b-a29c-2933a3e561ca/',
|
'thumbnail': 'https://bandlabimages.azureedge.net/v1.0/songs/51b18363-da23-4b9b-a29c-2933a3e561ca/',
|
||||||
'timestamp': 1709625771,
|
'timestamp': 1709625771,
|
||||||
'track': 'PodcastMaerchen4b',
|
'track': 'PodcastMaerchen4b',
|
||||||
'duration': 468.14,
|
'duration': 468.14,
|
||||||
@ -178,7 +178,7 @@ class BandlabIE(BandlabBaseIE):
|
|||||||
'id': '110343fc-148b-ea11-96d2-0003ffd1fc09',
|
'id': '110343fc-148b-ea11-96d2-0003ffd1fc09',
|
||||||
'ext': 'm4a',
|
'ext': 'm4a',
|
||||||
'timestamp': 1588273294,
|
'timestamp': 1588273294,
|
||||||
'thumbnail': 'https://bl-prod-images.azureedge.net/v1.0/users/b612e533-e4f7-4542-9f50-3fcfd8dd822c/',
|
'thumbnail': 'https://bandlabimages.azureedge.net/v1.0/users/b612e533-e4f7-4542-9f50-3fcfd8dd822c/',
|
||||||
'description': 'Final Revision.',
|
'description': 'Final Revision.',
|
||||||
'title': 'Replay ( Instrumental)',
|
'title': 'Replay ( Instrumental)',
|
||||||
'uploader': 'David R Sparks',
|
'uploader': 'David R Sparks',
|
||||||
@ -200,7 +200,7 @@ class BandlabIE(BandlabBaseIE):
|
|||||||
'id': '5cdf9036-3857-ef11-991a-6045bd36e0d9',
|
'id': '5cdf9036-3857-ef11-991a-6045bd36e0d9',
|
||||||
'ext': 'mp4',
|
'ext': 'mp4',
|
||||||
'duration': 44.705,
|
'duration': 44.705,
|
||||||
'thumbnail': 'https://bl-prod-images.azureedge.net/v1.0/videos/67c6cef1-cef6-40d3-831e-a55bc1dcb972/',
|
'thumbnail': 'https://bandlabimages.azureedge.net/v1.0/videos/67c6cef1-cef6-40d3-831e-a55bc1dcb972/',
|
||||||
'comment_count': int,
|
'comment_count': int,
|
||||||
'title': 'backing vocals',
|
'title': 'backing vocals',
|
||||||
'uploader_id': 'marliashya',
|
'uploader_id': 'marliashya',
|
||||||
@ -224,7 +224,7 @@ class BandlabIE(BandlabBaseIE):
|
|||||||
'view_count': int,
|
'view_count': int,
|
||||||
'track': 'Positronic Meltdown',
|
'track': 'Positronic Meltdown',
|
||||||
'duration': 318.55,
|
'duration': 318.55,
|
||||||
'thumbnail': 'https://bl-prod-images.azureedge.net/v1.0/songs/87165bc3-5439-496e-b1f7-a9f13b541ff2/',
|
'thumbnail': 'https://bandlabimages.azureedge.net/v1.0/songs/87165bc3-5439-496e-b1f7-a9f13b541ff2/',
|
||||||
'description': 'Checkout my tracks at AOMX http://aomxsounds.com/',
|
'description': 'Checkout my tracks at AOMX http://aomxsounds.com/',
|
||||||
'uploader_id': 'microfreaks',
|
'uploader_id': 'microfreaks',
|
||||||
'title': 'Positronic Meltdown',
|
'title': 'Positronic Meltdown',
|
||||||
@ -246,7 +246,7 @@ class BandlabIE(BandlabBaseIE):
|
|||||||
'comment_count': int,
|
'comment_count': int,
|
||||||
'uploader': 'Sorakime',
|
'uploader': 'Sorakime',
|
||||||
'uploader_id': 'sorakime',
|
'uploader_id': 'sorakime',
|
||||||
'thumbnail': 'https://bl-prod-images.azureedge.net/v1.0/users/572a351a-0f3a-4c6a-ac39-1a5defdeeb1c/',
|
'thumbnail': 'https://bandlabimages.azureedge.net/v1.0/users/572a351a-0f3a-4c6a-ac39-1a5defdeeb1c/',
|
||||||
'timestamp': 1691162128,
|
'timestamp': 1691162128,
|
||||||
'upload_date': '20230804',
|
'upload_date': '20230804',
|
||||||
'media_type': 'track',
|
'media_type': 'track',
|
||||||
|
@ -1596,16 +1596,16 @@ class BilibiliPlaylistIE(BilibiliSpaceListBaseIE):
|
|||||||
|
|
||||||
webpage = self._download_webpage(url, list_id)
|
webpage = self._download_webpage(url, list_id)
|
||||||
initial_state = self._search_json(r'window\.__INITIAL_STATE__\s*=', webpage, 'initial state', list_id)
|
initial_state = self._search_json(r'window\.__INITIAL_STATE__\s*=', webpage, 'initial state', list_id)
|
||||||
error = traverse_obj(initial_state, (('error', 'listError'), all, lambda _, v: v['code'], any))
|
if traverse_obj(initial_state, ('error', 'code', {int_or_none})) != 200:
|
||||||
if error and error['code'] != 200:
|
error_code = traverse_obj(initial_state, ('error', 'trueCode', {int_or_none}))
|
||||||
error_code = error.get('trueCode')
|
error_message = traverse_obj(initial_state, ('error', 'message', {str_or_none}))
|
||||||
if error_code == -400 and list_id == 'watchlater':
|
if error_code == -400 and list_id == 'watchlater':
|
||||||
self.raise_login_required('You need to login to access your watchlater playlist')
|
self.raise_login_required('You need to login to access your watchlater playlist')
|
||||||
elif error_code == -403:
|
elif error_code == -403:
|
||||||
self.raise_login_required('This is a private playlist. You need to login as its owner')
|
self.raise_login_required('This is a private playlist. You need to login as its owner')
|
||||||
elif error_code == 11010:
|
elif error_code == 11010:
|
||||||
raise ExtractorError('Playlist is no longer available', expected=True)
|
raise ExtractorError('Playlist is no longer available', expected=True)
|
||||||
raise ExtractorError(f'Could not access playlist: {error_code} {error.get("message")}')
|
raise ExtractorError(f'Could not access playlist: {error_code} {error_message}')
|
||||||
|
|
||||||
query = {
|
query = {
|
||||||
'ps': 20,
|
'ps': 20,
|
||||||
|
@ -1,32 +1,30 @@
|
|||||||
import functools
|
import functools
|
||||||
import json
|
|
||||||
import re
|
import re
|
||||||
|
|
||||||
from .common import InfoExtractor
|
from .common import InfoExtractor
|
||||||
from ..networking import HEADRequest
|
from ..networking import HEADRequest
|
||||||
from ..networking.exceptions import HTTPError
|
|
||||||
from ..utils import (
|
from ..utils import (
|
||||||
ExtractorError,
|
ExtractorError,
|
||||||
OnDemandPagedList,
|
OnDemandPagedList,
|
||||||
clean_html,
|
clean_html,
|
||||||
determine_ext,
|
extract_attributes,
|
||||||
format_field,
|
|
||||||
get_element_by_class,
|
get_element_by_class,
|
||||||
|
get_element_by_id,
|
||||||
|
get_element_html_by_class,
|
||||||
get_elements_html_by_class,
|
get_elements_html_by_class,
|
||||||
int_or_none,
|
int_or_none,
|
||||||
orderedSet,
|
orderedSet,
|
||||||
parse_count,
|
parse_count,
|
||||||
parse_duration,
|
parse_duration,
|
||||||
parse_iso8601,
|
traverse_obj,
|
||||||
url_or_none,
|
unified_strdate,
|
||||||
urlencode_postdata,
|
urlencode_postdata,
|
||||||
urljoin,
|
urljoin,
|
||||||
)
|
)
|
||||||
from ..utils.traversal import traverse_obj
|
|
||||||
|
|
||||||
|
|
||||||
class BitChuteIE(InfoExtractor):
|
class BitChuteIE(InfoExtractor):
|
||||||
_VALID_URL = r'https?://(?:(?:www|old)\.)?bitchute\.com/(?:video|embed|torrent/[^/?#]+)/(?P<id>[^/?#&]+)'
|
_VALID_URL = r'https?://(?:(?:www|old)\.)?bitchute\.com/(?:video|embed|torrent/[^/]+)/(?P<id>[^/?#&]+)'
|
||||||
_EMBED_REGEX = [rf'<(?:script|iframe)[^>]+\bsrc=(["\'])(?P<url>{_VALID_URL})']
|
_EMBED_REGEX = [rf'<(?:script|iframe)[^>]+\bsrc=(["\'])(?P<url>{_VALID_URL})']
|
||||||
_TESTS = [{
|
_TESTS = [{
|
||||||
'url': 'https://www.bitchute.com/video/UGlrF9o9b-Q/',
|
'url': 'https://www.bitchute.com/video/UGlrF9o9b-Q/',
|
||||||
@ -36,17 +34,12 @@ class BitChuteIE(InfoExtractor):
|
|||||||
'ext': 'mp4',
|
'ext': 'mp4',
|
||||||
'title': 'This is the first video on #BitChute !',
|
'title': 'This is the first video on #BitChute !',
|
||||||
'description': 'md5:a0337e7b1fe39e32336974af8173a034',
|
'description': 'md5:a0337e7b1fe39e32336974af8173a034',
|
||||||
'thumbnail': r're:https?://.+/.+\.jpg$',
|
'thumbnail': r're:^https?://.*\.jpg$',
|
||||||
'uploader': 'BitChute',
|
'uploader': 'BitChute',
|
||||||
'upload_date': '20170103',
|
'upload_date': '20170103',
|
||||||
'uploader_url': 'https://www.bitchute.com/profile/I5NgtHZn9vPj/',
|
'uploader_url': 'https://www.bitchute.com/profile/I5NgtHZn9vPj/',
|
||||||
'channel': 'BitChute',
|
'channel': 'BitChute',
|
||||||
'channel_url': 'https://www.bitchute.com/channel/bitchute/',
|
'channel_url': 'https://www.bitchute.com/channel/bitchute/',
|
||||||
'uploader_id': 'I5NgtHZn9vPj',
|
|
||||||
'channel_id': '1VBwRfyNcKdX',
|
|
||||||
'view_count': int,
|
|
||||||
'duration': 16.0,
|
|
||||||
'timestamp': 1483425443,
|
|
||||||
},
|
},
|
||||||
}, {
|
}, {
|
||||||
# test case: video with different channel and uploader
|
# test case: video with different channel and uploader
|
||||||
@ -56,18 +49,13 @@ class BitChuteIE(InfoExtractor):
|
|||||||
'id': 'Yti_j9A-UZ4',
|
'id': 'Yti_j9A-UZ4',
|
||||||
'ext': 'mp4',
|
'ext': 'mp4',
|
||||||
'title': 'Israel at War | Full Measure',
|
'title': 'Israel at War | Full Measure',
|
||||||
'description': 'md5:e60198b89971966d6030d22b3268f08f',
|
'description': 'md5:38cf7bc6f42da1a877835539111c69ef',
|
||||||
'thumbnail': r're:https?://.+/.+\.jpg$',
|
'thumbnail': r're:^https?://.*\.jpg$',
|
||||||
'uploader': 'sharylattkisson',
|
'uploader': 'sharylattkisson',
|
||||||
'upload_date': '20231106',
|
'upload_date': '20231106',
|
||||||
'uploader_url': 'https://www.bitchute.com/profile/9K0kUWA9zmd9/',
|
'uploader_url': 'https://www.bitchute.com/profile/9K0kUWA9zmd9/',
|
||||||
'channel': 'Full Measure with Sharyl Attkisson',
|
'channel': 'Full Measure with Sharyl Attkisson',
|
||||||
'channel_url': 'https://www.bitchute.com/channel/sharylattkisson/',
|
'channel_url': 'https://www.bitchute.com/channel/sharylattkisson/',
|
||||||
'uploader_id': '9K0kUWA9zmd9',
|
|
||||||
'channel_id': 'NpdxoCRv3ZLb',
|
|
||||||
'view_count': int,
|
|
||||||
'duration': 554.0,
|
|
||||||
'timestamp': 1699296106,
|
|
||||||
},
|
},
|
||||||
}, {
|
}, {
|
||||||
# video not downloadable in browser, but we can recover it
|
# video not downloadable in browser, but we can recover it
|
||||||
@ -78,21 +66,25 @@ class BitChuteIE(InfoExtractor):
|
|||||||
'ext': 'mp4',
|
'ext': 'mp4',
|
||||||
'filesize': 71537926,
|
'filesize': 71537926,
|
||||||
'title': 'STYXHEXENHAMMER666 - Election Fraud, Clinton 2020, EU Armies, and Gun Control',
|
'title': 'STYXHEXENHAMMER666 - Election Fraud, Clinton 2020, EU Armies, and Gun Control',
|
||||||
'description': 'md5:2029c7c212ccd4b040f52bb2d036ef4e',
|
'description': 'md5:228ee93bd840a24938f536aeac9cf749',
|
||||||
'thumbnail': r're:https?://.+/.+\.jpg$',
|
'thumbnail': r're:^https?://.*\.jpg$',
|
||||||
'uploader': 'BitChute',
|
'uploader': 'BitChute',
|
||||||
'upload_date': '20181113',
|
'upload_date': '20181113',
|
||||||
'uploader_url': 'https://www.bitchute.com/profile/I5NgtHZn9vPj/',
|
'uploader_url': 'https://www.bitchute.com/profile/I5NgtHZn9vPj/',
|
||||||
'channel': 'BitChute',
|
'channel': 'BitChute',
|
||||||
'channel_url': 'https://www.bitchute.com/channel/bitchute/',
|
'channel_url': 'https://www.bitchute.com/channel/bitchute/',
|
||||||
'uploader_id': 'I5NgtHZn9vPj',
|
|
||||||
'channel_id': '1VBwRfyNcKdX',
|
|
||||||
'view_count': int,
|
|
||||||
'duration': 1701.0,
|
|
||||||
'tags': ['bitchute'],
|
|
||||||
'timestamp': 1542130287,
|
|
||||||
},
|
},
|
||||||
'params': {'check_formats': None},
|
'params': {'check_formats': None},
|
||||||
|
}, {
|
||||||
|
# restricted video
|
||||||
|
'url': 'https://www.bitchute.com/video/WEnQU7XGcTdl/',
|
||||||
|
'info_dict': {
|
||||||
|
'id': 'WEnQU7XGcTdl',
|
||||||
|
'ext': 'mp4',
|
||||||
|
'title': 'Impartial Truth - Ein Letzter Appell an die Vernunft',
|
||||||
|
},
|
||||||
|
'params': {'skip_download': True},
|
||||||
|
'skip': 'Georestricted in DE',
|
||||||
}, {
|
}, {
|
||||||
'url': 'https://www.bitchute.com/embed/lbb5G1hjPhw/',
|
'url': 'https://www.bitchute.com/embed/lbb5G1hjPhw/',
|
||||||
'only_matching': True,
|
'only_matching': True,
|
||||||
@ -104,8 +96,11 @@ class BitChuteIE(InfoExtractor):
|
|||||||
'only_matching': True,
|
'only_matching': True,
|
||||||
}]
|
}]
|
||||||
_GEO_BYPASS = False
|
_GEO_BYPASS = False
|
||||||
_UPLOADER_URL_TMPL = 'https://www.bitchute.com/profile/%s/'
|
|
||||||
_CHANNEL_URL_TMPL = 'https://www.bitchute.com/channel/%s/'
|
_HEADERS = {
|
||||||
|
'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.57 Safari/537.36',
|
||||||
|
'Referer': 'https://www.bitchute.com/',
|
||||||
|
}
|
||||||
|
|
||||||
def _check_format(self, video_url, video_id):
|
def _check_format(self, video_url, video_id):
|
||||||
urls = orderedSet(
|
urls = orderedSet(
|
||||||
@ -117,7 +112,7 @@ class BitChuteIE(InfoExtractor):
|
|||||||
for url in urls:
|
for url in urls:
|
||||||
try:
|
try:
|
||||||
response = self._request_webpage(
|
response = self._request_webpage(
|
||||||
HEADRequest(url), video_id=video_id, note=f'Checking {url}')
|
HEADRequest(url), video_id=video_id, note=f'Checking {url}', headers=self._HEADERS)
|
||||||
except ExtractorError as e:
|
except ExtractorError as e:
|
||||||
self.to_screen(f'{video_id}: URL is invalid, skipping: {e.cause}')
|
self.to_screen(f'{video_id}: URL is invalid, skipping: {e.cause}')
|
||||||
continue
|
continue
|
||||||
@ -126,79 +121,54 @@ class BitChuteIE(InfoExtractor):
|
|||||||
'filesize': int_or_none(response.headers.get('Content-Length')),
|
'filesize': int_or_none(response.headers.get('Content-Length')),
|
||||||
}
|
}
|
||||||
|
|
||||||
def _call_api(self, endpoint, data, display_id, fatal=True):
|
def _raise_if_restricted(self, webpage):
|
||||||
note = endpoint.rpartition('/')[2]
|
page_title = clean_html(get_element_by_class('page-title', webpage)) or ''
|
||||||
try:
|
if re.fullmatch(r'(?:Channel|Video) Restricted', page_title):
|
||||||
return self._download_json(
|
reason = clean_html(get_element_by_id('page-detail', webpage)) or page_title
|
||||||
f'https://api.bitchute.com/api/beta/{endpoint}', display_id,
|
self.raise_geo_restricted(reason)
|
||||||
f'Downloading {note} API JSON', f'Unable to download {note} API JSON',
|
|
||||||
data=json.dumps(data).encode(),
|
@staticmethod
|
||||||
headers={
|
def _make_url(html):
|
||||||
'Accept': 'application/json',
|
path = extract_attributes(get_element_html_by_class('spa', html) or '').get('href')
|
||||||
'Content-Type': 'application/json',
|
return urljoin('https://www.bitchute.com', path)
|
||||||
})
|
|
||||||
except ExtractorError as e:
|
|
||||||
if isinstance(e.cause, HTTPError) and e.cause.status == 403:
|
|
||||||
errors = '. '.join(traverse_obj(e.cause.response.read().decode(), (
|
|
||||||
{json.loads}, 'errors', lambda _, v: v['context'] == 'reason', 'message', {str})))
|
|
||||||
if errors and 'location' in errors:
|
|
||||||
# Can always be fatal since the video/media call will reach this code first
|
|
||||||
self.raise_geo_restricted(errors)
|
|
||||||
if fatal:
|
|
||||||
raise
|
|
||||||
self.report_warning(e.msg)
|
|
||||||
|
|
||||||
def _real_extract(self, url):
|
def _real_extract(self, url):
|
||||||
video_id = self._match_id(url)
|
video_id = self._match_id(url)
|
||||||
data = {'video_id': video_id}
|
webpage = self._download_webpage(
|
||||||
media_url = self._call_api('video/media', data, video_id)['media_url']
|
f'https://old.bitchute.com/video/{video_id}', video_id, headers=self._HEADERS)
|
||||||
|
|
||||||
|
self._raise_if_restricted(webpage)
|
||||||
|
publish_date = clean_html(get_element_by_class('video-publish-date', webpage))
|
||||||
|
entries = self._parse_html5_media_entries(url, webpage, video_id)
|
||||||
|
|
||||||
formats = []
|
formats = []
|
||||||
if determine_ext(media_url) == 'm3u8':
|
for format_ in traverse_obj(entries, (0, 'formats', ...)):
|
||||||
formats.extend(
|
|
||||||
self._extract_m3u8_formats(media_url, video_id, 'mp4', m3u8_id='hls', live=True))
|
|
||||||
else:
|
|
||||||
if self.get_param('check_formats') is not False:
|
if self.get_param('check_formats') is not False:
|
||||||
if fmt := self._check_format(media_url, video_id):
|
format_.update(self._check_format(format_.pop('url'), video_id) or {})
|
||||||
formats.append(fmt)
|
if 'url' not in format_:
|
||||||
else:
|
continue
|
||||||
formats.append({'url': media_url})
|
formats.append(format_)
|
||||||
|
|
||||||
if not formats:
|
if not formats:
|
||||||
self.raise_no_formats(
|
self.raise_no_formats(
|
||||||
'Video is unavailable. Please make sure this video is playable in the browser '
|
'Video is unavailable. Please make sure this video is playable in the browser '
|
||||||
'before reporting this issue.', expected=True, video_id=video_id)
|
'before reporting this issue.', expected=True, video_id=video_id)
|
||||||
|
|
||||||
video = self._call_api('video', data, video_id, fatal=False)
|
details = get_element_by_class('details', webpage) or ''
|
||||||
channel = None
|
uploader_html = get_element_html_by_class('creator', details) or ''
|
||||||
if channel_id := traverse_obj(video, ('channel', 'channel_id', {str})):
|
channel_html = get_element_html_by_class('name', details) or ''
|
||||||
channel = self._call_api('channel', {'channel_id': channel_id}, video_id, fatal=False)
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
**traverse_obj(video, {
|
|
||||||
'title': ('video_name', {str}),
|
|
||||||
'description': ('description', {str}),
|
|
||||||
'thumbnail': ('thumbnail_url', {url_or_none}),
|
|
||||||
'channel': ('channel', 'channel_name', {str}),
|
|
||||||
'channel_id': ('channel', 'channel_id', {str}),
|
|
||||||
'channel_url': ('channel', 'channel_url', {urljoin('https://www.bitchute.com/')}),
|
|
||||||
'uploader_id': ('profile_id', {str}),
|
|
||||||
'uploader_url': ('profile_id', {format_field(template=self._UPLOADER_URL_TMPL)}, filter),
|
|
||||||
'timestamp': ('date_published', {parse_iso8601}),
|
|
||||||
'duration': ('duration', {parse_duration}),
|
|
||||||
'tags': ('hashtags', ..., {str}, filter, all, filter),
|
|
||||||
'view_count': ('view_count', {int_or_none}),
|
|
||||||
'is_live': ('state_id', {lambda x: x == 'live'}),
|
|
||||||
}),
|
|
||||||
**traverse_obj(channel, {
|
|
||||||
'channel': ('channel_name', {str}),
|
|
||||||
'channel_id': ('channel_id', {str}),
|
|
||||||
'channel_url': ('url_slug', {format_field(template=self._CHANNEL_URL_TMPL)}, filter),
|
|
||||||
'uploader': ('profile_name', {str}),
|
|
||||||
'uploader_id': ('profile_id', {str}),
|
|
||||||
'uploader_url': ('profile_id', {format_field(template=self._UPLOADER_URL_TMPL)}, filter),
|
|
||||||
}),
|
|
||||||
'id': video_id,
|
'id': video_id,
|
||||||
|
'title': self._html_extract_title(webpage) or self._og_search_title(webpage),
|
||||||
|
'description': self._og_search_description(webpage, default=None),
|
||||||
|
'thumbnail': self._og_search_thumbnail(webpage),
|
||||||
|
'uploader': clean_html(uploader_html),
|
||||||
|
'uploader_url': self._make_url(uploader_html),
|
||||||
|
'channel': clean_html(channel_html),
|
||||||
|
'channel_url': self._make_url(channel_html),
|
||||||
|
'upload_date': unified_strdate(self._search_regex(
|
||||||
|
r'at \d+:\d+ UTC on (.+?)\.', publish_date, 'upload date', fatal=False)),
|
||||||
'formats': formats,
|
'formats': formats,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -220,7 +190,7 @@ class BitChuteChannelIE(InfoExtractor):
|
|||||||
'ext': 'mp4',
|
'ext': 'mp4',
|
||||||
'title': 'This is the first video on #BitChute !',
|
'title': 'This is the first video on #BitChute !',
|
||||||
'description': 'md5:a0337e7b1fe39e32336974af8173a034',
|
'description': 'md5:a0337e7b1fe39e32336974af8173a034',
|
||||||
'thumbnail': r're:https?://.+/.+\.jpg$',
|
'thumbnail': r're:^https?://.*\.jpg$',
|
||||||
'uploader': 'BitChute',
|
'uploader': 'BitChute',
|
||||||
'upload_date': '20170103',
|
'upload_date': '20170103',
|
||||||
'uploader_url': 'https://www.bitchute.com/profile/I5NgtHZn9vPj/',
|
'uploader_url': 'https://www.bitchute.com/profile/I5NgtHZn9vPj/',
|
||||||
@ -228,9 +198,6 @@ class BitChuteChannelIE(InfoExtractor):
|
|||||||
'channel_url': 'https://www.bitchute.com/channel/bitchute/',
|
'channel_url': 'https://www.bitchute.com/channel/bitchute/',
|
||||||
'duration': 16,
|
'duration': 16,
|
||||||
'view_count': int,
|
'view_count': int,
|
||||||
'uploader_id': 'I5NgtHZn9vPj',
|
|
||||||
'channel_id': '1VBwRfyNcKdX',
|
|
||||||
'timestamp': 1483425443,
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@ -246,7 +213,6 @@ class BitChuteChannelIE(InfoExtractor):
|
|||||||
'title': 'Bruce MacDonald and "The Light of Darkness"',
|
'title': 'Bruce MacDonald and "The Light of Darkness"',
|
||||||
'description': 'md5:747724ef404eebdfc04277714f81863e',
|
'description': 'md5:747724ef404eebdfc04277714f81863e',
|
||||||
},
|
},
|
||||||
'skip': '404 Not Found',
|
|
||||||
}, {
|
}, {
|
||||||
'url': 'https://old.bitchute.com/playlist/wV9Imujxasw9/',
|
'url': 'https://old.bitchute.com/playlist/wV9Imujxasw9/',
|
||||||
'only_matching': True,
|
'only_matching': True,
|
||||||
|
@ -53,7 +53,7 @@ class BlueskyIE(InfoExtractor):
|
|||||||
'channel_id': 'did:plc:z72i7hdynmk6r22z27h6tvur',
|
'channel_id': 'did:plc:z72i7hdynmk6r22z27h6tvur',
|
||||||
'channel_url': 'https://bsky.app/profile/did:plc:z72i7hdynmk6r22z27h6tvur',
|
'channel_url': 'https://bsky.app/profile/did:plc:z72i7hdynmk6r22z27h6tvur',
|
||||||
'thumbnail': r're:https://video.bsky.app/watch/.*\.jpg$',
|
'thumbnail': r're:https://video.bsky.app/watch/.*\.jpg$',
|
||||||
'title': 'Bluesky now has video! Update your app to version 1.91 or refresh on ...',
|
'title': 'Bluesky now has video! Update your app to versi...',
|
||||||
'alt_title': 'Bluesky video feature announcement',
|
'alt_title': 'Bluesky video feature announcement',
|
||||||
'description': r're:(?s)Bluesky now has video! .{239}',
|
'description': r're:(?s)Bluesky now has video! .{239}',
|
||||||
'upload_date': '20240911',
|
'upload_date': '20240911',
|
||||||
@ -172,7 +172,7 @@ class BlueskyIE(InfoExtractor):
|
|||||||
'channel_id': 'did:plc:z72i7hdynmk6r22z27h6tvur',
|
'channel_id': 'did:plc:z72i7hdynmk6r22z27h6tvur',
|
||||||
'channel_url': 'https://bsky.app/profile/did:plc:z72i7hdynmk6r22z27h6tvur',
|
'channel_url': 'https://bsky.app/profile/did:plc:z72i7hdynmk6r22z27h6tvur',
|
||||||
'thumbnail': r're:https://video.bsky.app/watch/.*\.jpg$',
|
'thumbnail': r're:https://video.bsky.app/watch/.*\.jpg$',
|
||||||
'title': 'Bluesky now has video! Update your app to version 1.91 or refresh on ...',
|
'title': 'Bluesky now has video! Update your app to versi...',
|
||||||
'alt_title': 'Bluesky video feature announcement',
|
'alt_title': 'Bluesky video feature announcement',
|
||||||
'description': r're:(?s)Bluesky now has video! .{239}',
|
'description': r're:(?s)Bluesky now has video! .{239}',
|
||||||
'upload_date': '20240911',
|
'upload_date': '20240911',
|
||||||
@ -191,7 +191,7 @@ class BlueskyIE(InfoExtractor):
|
|||||||
'info_dict': {
|
'info_dict': {
|
||||||
'id': '3l7rdfxhyds2f',
|
'id': '3l7rdfxhyds2f',
|
||||||
'ext': 'mp4',
|
'ext': 'mp4',
|
||||||
'uploader': 'cinnamon 🐇 🏳️⚧️',
|
'uploader': 'cinnamon',
|
||||||
'uploader_id': 'cinny.bun.how',
|
'uploader_id': 'cinny.bun.how',
|
||||||
'uploader_url': 'https://bsky.app/profile/cinny.bun.how',
|
'uploader_url': 'https://bsky.app/profile/cinny.bun.how',
|
||||||
'channel_id': 'did:plc:7x6rtuenkuvxq3zsvffp2ide',
|
'channel_id': 'did:plc:7x6rtuenkuvxq3zsvffp2ide',
|
||||||
@ -255,7 +255,7 @@ class BlueskyIE(InfoExtractor):
|
|||||||
'info_dict': {
|
'info_dict': {
|
||||||
'id': '3l77u64l7le2e',
|
'id': '3l77u64l7le2e',
|
||||||
'ext': 'mp4',
|
'ext': 'mp4',
|
||||||
'title': "hearing people on twitter say that bluesky isn't funny yet so post t...",
|
'title': 'hearing people on twitter say that bluesky isn\'...',
|
||||||
'like_count': int,
|
'like_count': int,
|
||||||
'uploader_id': 'thafnine.net',
|
'uploader_id': 'thafnine.net',
|
||||||
'uploader_url': 'https://bsky.app/profile/thafnine.net',
|
'uploader_url': 'https://bsky.app/profile/thafnine.net',
|
||||||
@ -387,7 +387,7 @@ class BlueskyIE(InfoExtractor):
|
|||||||
'age_limit': (
|
'age_limit': (
|
||||||
'labels', ..., 'val', {lambda x: 18 if x in ('sexual', 'porn', 'graphic-media') else None}, any),
|
'labels', ..., 'val', {lambda x: 18 if x in ('sexual', 'porn', 'graphic-media') else None}, any),
|
||||||
'description': (*record_path, 'text', {str}, filter),
|
'description': (*record_path, 'text', {str}, filter),
|
||||||
'title': (*record_path, 'text', {lambda x: x.replace('\n', ' ')}, {truncate_string(left=72)}),
|
'title': (*record_path, 'text', {lambda x: x.replace('\n', ' ')}, {truncate_string(left=50)}),
|
||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
return entries
|
return entries
|
||||||
|
@ -24,7 +24,7 @@ class BokeCCBaseIE(InfoExtractor):
|
|||||||
|
|
||||||
|
|
||||||
class BokeCCIE(BokeCCBaseIE):
|
class BokeCCIE(BokeCCBaseIE):
|
||||||
IE_DESC = 'CC视频'
|
_IE_DESC = 'CC视频'
|
||||||
_VALID_URL = r'https?://union\.bokecc\.com/playvideo\.bo\?(?P<query>.*)'
|
_VALID_URL = r'https?://union\.bokecc\.com/playvideo\.bo\?(?P<query>.*)'
|
||||||
|
|
||||||
_TESTS = [{
|
_TESTS = [{
|
||||||
|
@ -7,7 +7,6 @@ from ..utils import (
|
|||||||
join_nonempty,
|
join_nonempty,
|
||||||
js_to_json,
|
js_to_json,
|
||||||
mimetype2ext,
|
mimetype2ext,
|
||||||
parse_resolution,
|
|
||||||
unified_strdate,
|
unified_strdate,
|
||||||
url_or_none,
|
url_or_none,
|
||||||
urljoin,
|
urljoin,
|
||||||
@ -111,23 +110,24 @@ class BpbIE(InfoExtractor):
|
|||||||
|
|
||||||
return attributes
|
return attributes
|
||||||
|
|
||||||
def _process_source(self, source):
|
@staticmethod
|
||||||
|
def _process_source(source):
|
||||||
url = url_or_none(source['src'])
|
url = url_or_none(source['src'])
|
||||||
if not url:
|
if not url:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
source_type = source.get('type', '')
|
source_type = source.get('type', '')
|
||||||
extension = mimetype2ext(source_type)
|
extension = mimetype2ext(source_type)
|
||||||
note = self._search_regex(r'[_-]([a-z]+)\.[\da-z]+(?:$|\?)', url, 'note', default=None)
|
is_video = source_type.startswith('video')
|
||||||
|
note = url.rpartition('.')[0].rpartition('_')[2] if is_video else None
|
||||||
|
|
||||||
return {
|
return {
|
||||||
'url': url,
|
'url': url,
|
||||||
'ext': extension,
|
'ext': extension,
|
||||||
'vcodec': None if source_type.startswith('video') else 'none',
|
'vcodec': None if is_video else 'none',
|
||||||
'quality': 10 if note == 'high' else 0,
|
'quality': 10 if note == 'high' else 0,
|
||||||
'format_note': note,
|
'format_note': note,
|
||||||
'format_id': join_nonempty(extension, note),
|
'format_id': join_nonempty(extension, note),
|
||||||
**parse_resolution(source.get('label')),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
def _real_extract(self, url):
|
def _real_extract(self, url):
|
||||||
|
@ -1,178 +0,0 @@
|
|||||||
import json
|
|
||||||
|
|
||||||
from .common import InfoExtractor
|
|
||||||
from ..networking import HEADRequest
|
|
||||||
from ..utils import (
|
|
||||||
ExtractorError,
|
|
||||||
extract_attributes,
|
|
||||||
int_or_none,
|
|
||||||
parse_qs,
|
|
||||||
smuggle_url,
|
|
||||||
unsmuggle_url,
|
|
||||||
url_or_none,
|
|
||||||
urlhandle_detect_ext,
|
|
||||||
)
|
|
||||||
from ..utils.traversal import find_element, traverse_obj
|
|
||||||
|
|
||||||
|
|
||||||
class BunnyCdnIE(InfoExtractor):
|
|
||||||
_VALID_URL = r'https?://(?:iframe\.mediadelivery\.net|video\.bunnycdn\.com)/(?:embed|play)/(?P<library_id>\d+)/(?P<id>[\da-f-]+)'
|
|
||||||
_EMBED_REGEX = [rf'<iframe[^>]+src=[\'"](?P<url>{_VALID_URL}[^\'"]*)[\'"]']
|
|
||||||
_TESTS = [{
|
|
||||||
'url': 'https://iframe.mediadelivery.net/embed/113933/e73edec1-e381-4c8b-ae73-717a140e0924',
|
|
||||||
'info_dict': {
|
|
||||||
'id': 'e73edec1-e381-4c8b-ae73-717a140e0924',
|
|
||||||
'ext': 'mp4',
|
|
||||||
'title': 'mistress morgana (3).mp4',
|
|
||||||
'description': '',
|
|
||||||
'timestamp': 1693251673,
|
|
||||||
'thumbnail': r're:^https?://.*\.b-cdn\.net/e73edec1-e381-4c8b-ae73-717a140e0924/thumbnail\.jpg',
|
|
||||||
'duration': 7.0,
|
|
||||||
'upload_date': '20230828',
|
|
||||||
},
|
|
||||||
'params': {'skip_download': True},
|
|
||||||
}, {
|
|
||||||
'url': 'https://iframe.mediadelivery.net/play/136145/32e34c4b-0d72-437c-9abb-05e67657da34',
|
|
||||||
'info_dict': {
|
|
||||||
'id': '32e34c4b-0d72-437c-9abb-05e67657da34',
|
|
||||||
'ext': 'mp4',
|
|
||||||
'timestamp': 1691145748,
|
|
||||||
'thumbnail': r're:^https?://.*\.b-cdn\.net/32e34c4b-0d72-437c-9abb-05e67657da34/thumbnail_9172dc16\.jpg',
|
|
||||||
'duration': 106.0,
|
|
||||||
'description': 'md5:981a3e899a5c78352b21ed8b2f1efd81',
|
|
||||||
'upload_date': '20230804',
|
|
||||||
'title': 'Sanela ist Teil der #arbeitsmarktkraft',
|
|
||||||
},
|
|
||||||
'params': {'skip_download': True},
|
|
||||||
}, {
|
|
||||||
# Stream requires activation and pings
|
|
||||||
'url': 'https://iframe.mediadelivery.net/embed/200867/2e8545ec-509d-4571-b855-4cf0235ccd75',
|
|
||||||
'info_dict': {
|
|
||||||
'id': '2e8545ec-509d-4571-b855-4cf0235ccd75',
|
|
||||||
'ext': 'mp4',
|
|
||||||
'timestamp': 1708497752,
|
|
||||||
'title': 'netflix part 1',
|
|
||||||
'duration': 3959.0,
|
|
||||||
'description': '',
|
|
||||||
'upload_date': '20240221',
|
|
||||||
'thumbnail': r're:^https?://.*\.b-cdn\.net/2e8545ec-509d-4571-b855-4cf0235ccd75/thumbnail\.jpg',
|
|
||||||
},
|
|
||||||
'params': {'skip_download': True},
|
|
||||||
}]
|
|
||||||
_WEBPAGE_TESTS = [{
|
|
||||||
# Stream requires Referer
|
|
||||||
'url': 'https://conword.io/',
|
|
||||||
'info_dict': {
|
|
||||||
'id': '3a5d863e-9cd6-447e-b6ef-e289af50b349',
|
|
||||||
'ext': 'mp4',
|
|
||||||
'title': 'Conword bei der Stadt Köln und Stadt Dortmund',
|
|
||||||
'description': '',
|
|
||||||
'upload_date': '20231031',
|
|
||||||
'duration': 31.0,
|
|
||||||
'thumbnail': 'https://video.watchuh.com/3a5d863e-9cd6-447e-b6ef-e289af50b349/thumbnail.jpg',
|
|
||||||
'timestamp': 1698783879,
|
|
||||||
},
|
|
||||||
'params': {'skip_download': True},
|
|
||||||
}, {
|
|
||||||
# URL requires token and expires
|
|
||||||
'url': 'https://www.stockphotos.com/video/moscow-subway-the-train-is-arriving-at-the-park-kultury-station-10017830',
|
|
||||||
'info_dict': {
|
|
||||||
'id': '0b02fa20-4e8c-4140-8f87-f64d820a3386',
|
|
||||||
'ext': 'mp4',
|
|
||||||
'thumbnail': r're:^https?://.*\.b-cdn\.net/0b02fa20-4e8c-4140-8f87-f64d820a3386/thumbnail\.jpg',
|
|
||||||
'title': 'Moscow subway. The train is arriving at the Park Kultury station.',
|
|
||||||
'upload_date': '20240531',
|
|
||||||
'duration': 18.0,
|
|
||||||
'timestamp': 1717152269,
|
|
||||||
'description': '',
|
|
||||||
},
|
|
||||||
'params': {'skip_download': True},
|
|
||||||
}]
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def _extract_embed_urls(cls, url, webpage):
|
|
||||||
for embed_url in super()._extract_embed_urls(url, webpage):
|
|
||||||
yield smuggle_url(embed_url, {'Referer': url})
|
|
||||||
|
|
||||||
def _real_extract(self, url):
|
|
||||||
url, smuggled_data = unsmuggle_url(url, {})
|
|
||||||
|
|
||||||
video_id, library_id = self._match_valid_url(url).group('id', 'library_id')
|
|
||||||
webpage = self._download_webpage(
|
|
||||||
f'https://iframe.mediadelivery.net/embed/{library_id}/{video_id}', video_id,
|
|
||||||
headers=traverse_obj(smuggled_data, {'Referer': 'Referer'}),
|
|
||||||
query=traverse_obj(parse_qs(url), {'token': 'token', 'expires': 'expires'}))
|
|
||||||
|
|
||||||
if html_title := self._html_extract_title(webpage, default=None) == '403':
|
|
||||||
raise ExtractorError(
|
|
||||||
'This video is inaccessible. Setting a Referer header '
|
|
||||||
'might be required to access the video', expected=True)
|
|
||||||
elif html_title == '404':
|
|
||||||
raise ExtractorError('This video does not exist', expected=True)
|
|
||||||
|
|
||||||
headers = {'Referer': url}
|
|
||||||
|
|
||||||
info = traverse_obj(self._parse_html5_media_entries(url, webpage, video_id, _headers=headers), 0) or {}
|
|
||||||
formats = info.get('formats') or []
|
|
||||||
subtitles = info.get('subtitles') or {}
|
|
||||||
|
|
||||||
original_url = self._search_regex(
|
|
||||||
r'(?:var|const|let)\s+originalUrl\s*=\s*["\']([^"\']+)["\']', webpage, 'original url', default=None)
|
|
||||||
if url_or_none(original_url):
|
|
||||||
urlh = self._request_webpage(
|
|
||||||
HEADRequest(original_url), video_id=video_id, note='Checking original',
|
|
||||||
headers=headers, fatal=False, expected_status=(403, 404))
|
|
||||||
if urlh and urlh.status == 200:
|
|
||||||
formats.append({
|
|
||||||
'url': original_url,
|
|
||||||
'format_id': 'source',
|
|
||||||
'quality': 1,
|
|
||||||
'http_headers': headers,
|
|
||||||
'ext': urlhandle_detect_ext(urlh, default='mp4'),
|
|
||||||
'filesize': int_or_none(urlh.get_header('Content-Length')),
|
|
||||||
})
|
|
||||||
|
|
||||||
# MediaCage Streams require activation and pings
|
|
||||||
src_url = self._search_regex(
|
|
||||||
r'\.setAttribute\([\'"]src[\'"],\s*[\'"]([^\'"]+)[\'"]\)', webpage, 'src url', default=None)
|
|
||||||
activation_url = self._search_regex(
|
|
||||||
r'loadUrl\([\'"]([^\'"]+/activate)[\'"]', webpage, 'activation url', default=None)
|
|
||||||
ping_url = self._search_regex(
|
|
||||||
r'loadUrl\([\'"]([^\'"]+/ping)[\'"]', webpage, 'ping url', default=None)
|
|
||||||
secret = traverse_obj(parse_qs(src_url), ('secret', 0))
|
|
||||||
context_id = traverse_obj(parse_qs(src_url), ('contextId', 0))
|
|
||||||
ping_data = {}
|
|
||||||
if src_url and activation_url and ping_url and secret and context_id:
|
|
||||||
self._download_webpage(
|
|
||||||
activation_url, video_id, headers=headers, note='Downloading activation data')
|
|
||||||
|
|
||||||
fmts, subs = self._extract_m3u8_formats_and_subtitles(
|
|
||||||
src_url, video_id, 'mp4', headers=headers, m3u8_id='hls', fatal=False)
|
|
||||||
for fmt in fmts:
|
|
||||||
fmt.update({
|
|
||||||
'protocol': 'bunnycdn',
|
|
||||||
'http_headers': headers,
|
|
||||||
})
|
|
||||||
formats.extend(fmts)
|
|
||||||
self._merge_subtitles(subs, target=subtitles)
|
|
||||||
|
|
||||||
ping_data = {
|
|
||||||
'_bunnycdn_ping_data': {
|
|
||||||
'url': ping_url,
|
|
||||||
'headers': headers,
|
|
||||||
'secret': secret,
|
|
||||||
'context_id': context_id,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
'id': video_id,
|
|
||||||
'formats': formats,
|
|
||||||
'subtitles': subtitles,
|
|
||||||
**traverse_obj(webpage, ({find_element(id='main-video', html=True)}, {extract_attributes}, {
|
|
||||||
'title': ('data-plyr-config', {json.loads}, 'title', {str}),
|
|
||||||
'thumbnail': ('data-poster', {url_or_none}),
|
|
||||||
})),
|
|
||||||
**ping_data,
|
|
||||||
**self._search_json_ld(webpage, video_id, fatal=False),
|
|
||||||
}
|
|
@ -1,84 +0,0 @@
|
|||||||
import json
|
|
||||||
import time
|
|
||||||
|
|
||||||
from .common import InfoExtractor
|
|
||||||
from ..utils import (
|
|
||||||
determine_ext,
|
|
||||||
float_or_none,
|
|
||||||
jwt_decode_hs256,
|
|
||||||
parse_iso8601,
|
|
||||||
url_or_none,
|
|
||||||
variadic,
|
|
||||||
)
|
|
||||||
from ..utils.traversal import traverse_obj
|
|
||||||
|
|
||||||
|
|
||||||
class CanalsurmasIE(InfoExtractor):
|
|
||||||
_VALID_URL = r'https?://(?:www\.)?canalsurmas\.es/videos/(?P<id>\d+)'
|
|
||||||
_TESTS = [{
|
|
||||||
'url': 'https://www.canalsurmas.es/videos/44006-el-gran-queo-1-lora-del-rio-sevilla-20072014',
|
|
||||||
'md5': '861f86fdc1221175e15523047d0087ef',
|
|
||||||
'info_dict': {
|
|
||||||
'id': '44006',
|
|
||||||
'ext': 'mp4',
|
|
||||||
'title': 'Lora del Río (Sevilla)',
|
|
||||||
'description': 'md5:3d9ee40a9b1b26ed8259e6b71ed27b8b',
|
|
||||||
'thumbnail': 'https://cdn2.rtva.interactvty.com/content_cards/00f3e8f67b0a4f3b90a4a14618a48b0d.jpg',
|
|
||||||
'timestamp': 1648123182,
|
|
||||||
'upload_date': '20220324',
|
|
||||||
},
|
|
||||||
}]
|
|
||||||
_API_BASE = 'https://api-rtva.interactvty.com'
|
|
||||||
_access_token = None
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def _is_jwt_expired(token):
|
|
||||||
return jwt_decode_hs256(token)['exp'] - time.time() < 300
|
|
||||||
|
|
||||||
def _call_api(self, endpoint, video_id, fields=None):
|
|
||||||
if not self._access_token or self._is_jwt_expired(self._access_token):
|
|
||||||
self._access_token = self._download_json(
|
|
||||||
f'{self._API_BASE}/jwt/token/', None,
|
|
||||||
'Downloading access token', 'Failed to download access token',
|
|
||||||
headers={'Content-Type': 'application/json'},
|
|
||||||
data=json.dumps({
|
|
||||||
'username': 'canalsur_demo',
|
|
||||||
'password': 'dsUBXUcI',
|
|
||||||
}).encode())['access']
|
|
||||||
|
|
||||||
return self._download_json(
|
|
||||||
f'{self._API_BASE}/api/2.0/contents/{endpoint}/{video_id}/', video_id,
|
|
||||||
f'Downloading {endpoint} API JSON', f'Failed to download {endpoint} API JSON',
|
|
||||||
headers={'Authorization': f'jwtok {self._access_token}'},
|
|
||||||
query={'optional_fields': ','.join(variadic(fields))} if fields else None)
|
|
||||||
|
|
||||||
def _real_extract(self, url):
|
|
||||||
video_id = self._match_id(url)
|
|
||||||
video_info = self._call_api('content', video_id, fields=[
|
|
||||||
'description', 'image', 'duration', 'created_at', 'tags',
|
|
||||||
])
|
|
||||||
stream_info = self._call_api('content_resources', video_id, 'media_url')
|
|
||||||
|
|
||||||
formats, subtitles = [], {}
|
|
||||||
for stream_url in traverse_obj(stream_info, ('results', ..., 'media_url', {url_or_none})):
|
|
||||||
if determine_ext(stream_url) == 'm3u8':
|
|
||||||
fmts, subs = self._extract_m3u8_formats_and_subtitles(
|
|
||||||
stream_url, video_id, m3u8_id='hls', fatal=False)
|
|
||||||
formats.extend(fmts)
|
|
||||||
self._merge_subtitles(subs, target=subtitles)
|
|
||||||
else:
|
|
||||||
formats.append({'url': stream_url})
|
|
||||||
|
|
||||||
return {
|
|
||||||
'id': video_id,
|
|
||||||
'formats': formats,
|
|
||||||
'subtitles': subtitles,
|
|
||||||
**traverse_obj(video_info, {
|
|
||||||
'title': ('name', {str.strip}),
|
|
||||||
'description': ('description', {str}),
|
|
||||||
'thumbnail': ('image', {url_or_none}),
|
|
||||||
'duration': ('duration', {float_or_none}),
|
|
||||||
'timestamp': ('created_at', {parse_iso8601}),
|
|
||||||
'tags': ('tags', ..., {str}),
|
|
||||||
}),
|
|
||||||
}
|
|
@ -1,17 +1,17 @@
|
|||||||
|
import base64
|
||||||
import functools
|
import functools
|
||||||
|
import json
|
||||||
import re
|
import re
|
||||||
import time
|
import time
|
||||||
import urllib.parse
|
import urllib.parse
|
||||||
|
|
||||||
from .common import InfoExtractor
|
from .common import InfoExtractor
|
||||||
from ..networking import HEADRequest
|
from ..networking import HEADRequest
|
||||||
from ..networking.exceptions import HTTPError
|
|
||||||
from ..utils import (
|
from ..utils import (
|
||||||
ExtractorError,
|
ExtractorError,
|
||||||
float_or_none,
|
float_or_none,
|
||||||
int_or_none,
|
int_or_none,
|
||||||
js_to_json,
|
js_to_json,
|
||||||
jwt_decode_hs256,
|
|
||||||
mimetype2ext,
|
mimetype2ext,
|
||||||
orderedSet,
|
orderedSet,
|
||||||
parse_age_limit,
|
parse_age_limit,
|
||||||
@ -24,7 +24,6 @@ from ..utils import (
|
|||||||
update_url,
|
update_url,
|
||||||
url_basename,
|
url_basename,
|
||||||
url_or_none,
|
url_or_none,
|
||||||
urlencode_postdata,
|
|
||||||
)
|
)
|
||||||
from ..utils.traversal import require, traverse_obj, trim_str
|
from ..utils.traversal import require, traverse_obj, trim_str
|
||||||
|
|
||||||
@ -609,93 +608,81 @@ class CBCGemIE(CBCGemBaseIE):
|
|||||||
'only_matching': True,
|
'only_matching': True,
|
||||||
}]
|
}]
|
||||||
|
|
||||||
_CLIENT_ID = 'fc05b0ee-3865-4400-a3cc-3da82c330c23'
|
_TOKEN_API_KEY = '3f4beddd-2061-49b0-ae80-6f1f2ed65b37'
|
||||||
_refresh_token = None
|
|
||||||
_access_token = None
|
|
||||||
_claims_token = None
|
_claims_token = None
|
||||||
|
|
||||||
@functools.cached_property
|
def _new_claims_token(self, email, password):
|
||||||
def _ropc_settings(self):
|
data = json.dumps({
|
||||||
return self._download_json(
|
'email': email,
|
||||||
'https://services.radio-canada.ca/ott/catalog/v1/gem/settings', None,
|
|
||||||
'Downloading site settings', query={'device': 'web'})['identityManagement']['ropc']
|
|
||||||
|
|
||||||
def _is_jwt_expired(self, token):
|
|
||||||
return jwt_decode_hs256(token)['exp'] - time.time() < 300
|
|
||||||
|
|
||||||
def _call_oauth_api(self, oauth_data, note='Refreshing access token'):
|
|
||||||
response = self._download_json(
|
|
||||||
self._ropc_settings['url'], None, note, data=urlencode_postdata({
|
|
||||||
'client_id': self._CLIENT_ID,
|
|
||||||
**oauth_data,
|
|
||||||
'scope': self._ropc_settings['scopes'],
|
|
||||||
}))
|
|
||||||
self._refresh_token = response['refresh_token']
|
|
||||||
self._access_token = response['access_token']
|
|
||||||
self.cache.store(self._NETRC_MACHINE, 'token_data', [self._refresh_token, self._access_token])
|
|
||||||
|
|
||||||
def _perform_login(self, username, password):
|
|
||||||
if not self._refresh_token:
|
|
||||||
self._refresh_token, self._access_token = self.cache.load(
|
|
||||||
self._NETRC_MACHINE, 'token_data', default=[None, None])
|
|
||||||
|
|
||||||
if self._refresh_token and self._access_token:
|
|
||||||
self.write_debug('Using cached refresh token')
|
|
||||||
if not self._claims_token:
|
|
||||||
self._claims_token = self.cache.load(self._NETRC_MACHINE, 'claims_token')
|
|
||||||
return
|
|
||||||
|
|
||||||
try:
|
|
||||||
self._call_oauth_api({
|
|
||||||
'grant_type': 'password',
|
|
||||||
'username': username,
|
|
||||||
'password': password,
|
'password': password,
|
||||||
}, note='Logging in')
|
}).encode()
|
||||||
except ExtractorError as e:
|
headers = {'content-type': 'application/json'}
|
||||||
if isinstance(e.cause, HTTPError) and e.cause.status == 400:
|
query = {'apikey': self._TOKEN_API_KEY}
|
||||||
raise ExtractorError('Invalid username and/or password', expected=True)
|
resp = self._download_json('https://api.loginradius.com/identity/v2/auth/login',
|
||||||
raise
|
None, data=data, headers=headers, query=query)
|
||||||
|
access_token = resp['access_token']
|
||||||
|
|
||||||
def _fetch_access_token(self):
|
query = {
|
||||||
if self._is_jwt_expired(self._access_token):
|
'access_token': access_token,
|
||||||
try:
|
'apikey': self._TOKEN_API_KEY,
|
||||||
self._call_oauth_api({
|
'jwtapp': 'jwt',
|
||||||
'grant_type': 'refresh_token',
|
}
|
||||||
'refresh_token': self._refresh_token,
|
resp = self._download_json('https://cloud-api.loginradius.com/sso/jwt/api/token',
|
||||||
})
|
None, headers=headers, query=query)
|
||||||
except ExtractorError:
|
sig = resp['signature']
|
||||||
self._refresh_token, self._access_token = None, None
|
|
||||||
self.cache.store(self._NETRC_MACHINE, 'token_data', [None, None])
|
|
||||||
self.report_warning('Refresh token has been invalidated; retrying with credentials')
|
|
||||||
self._perform_login(*self._get_login_info())
|
|
||||||
|
|
||||||
return self._access_token
|
data = json.dumps({'jwt': sig}).encode()
|
||||||
|
headers = {'content-type': 'application/json', 'ott-device-type': 'web'}
|
||||||
|
resp = self._download_json('https://services.radio-canada.ca/ott/cbc-api/v2/token',
|
||||||
|
None, data=data, headers=headers, expected_status=426)
|
||||||
|
cbc_access_token = resp['accessToken']
|
||||||
|
|
||||||
def _fetch_claims_token(self):
|
headers = {'content-type': 'application/json', 'ott-device-type': 'web', 'ott-access-token': cbc_access_token}
|
||||||
if not self._get_login_info()[0]:
|
resp = self._download_json('https://services.radio-canada.ca/ott/cbc-api/v2/profile',
|
||||||
return None
|
None, headers=headers, expected_status=426)
|
||||||
|
return resp['claimsToken']
|
||||||
|
|
||||||
if not self._claims_token or self._is_jwt_expired(self._claims_token):
|
def _get_claims_token_expiry(self):
|
||||||
self._claims_token = self._download_json(
|
# Token is a JWT
|
||||||
'https://services.radio-canada.ca/ott/subscription/v2/gem/Subscriber/profile',
|
# JWT is decoded here and 'exp' field is extracted
|
||||||
None, 'Downloading claims token', query={'device': 'web'},
|
# It is a Unix timestamp for when the token expires
|
||||||
headers={'Authorization': f'Bearer {self._fetch_access_token()}'})['claimsToken']
|
b64_data = self._claims_token.split('.')[1]
|
||||||
|
data = base64.urlsafe_b64decode(b64_data + '==')
|
||||||
|
return json.loads(data)['exp']
|
||||||
|
|
||||||
|
def claims_token_expired(self):
|
||||||
|
exp = self._get_claims_token_expiry()
|
||||||
|
# It will expire in less than 10 seconds, or has already expired
|
||||||
|
return exp - time.time() < 10
|
||||||
|
|
||||||
|
def claims_token_valid(self):
|
||||||
|
return self._claims_token is not None and not self.claims_token_expired()
|
||||||
|
|
||||||
|
def _get_claims_token(self, email, password):
|
||||||
|
if not self.claims_token_valid():
|
||||||
|
self._claims_token = self._new_claims_token(email, password)
|
||||||
self.cache.store(self._NETRC_MACHINE, 'claims_token', self._claims_token)
|
self.cache.store(self._NETRC_MACHINE, 'claims_token', self._claims_token)
|
||||||
else:
|
|
||||||
self.write_debug('Using cached claims token')
|
|
||||||
|
|
||||||
return self._claims_token
|
return self._claims_token
|
||||||
|
|
||||||
|
def _real_initialize(self):
|
||||||
|
if self.claims_token_valid():
|
||||||
|
return
|
||||||
|
self._claims_token = self.cache.load(self._NETRC_MACHINE, 'claims_token')
|
||||||
|
|
||||||
def _real_extract(self, url):
|
def _real_extract(self, url):
|
||||||
video_id, season_number = self._match_valid_url(url).group('id', 'season')
|
video_id, season_number = self._match_valid_url(url).group('id', 'season')
|
||||||
video_info = self._call_show_api(video_id)
|
video_info = self._call_show_api(video_id)
|
||||||
item_info = traverse_obj(video_info, (
|
item_info = traverse_obj(video_info, (
|
||||||
'content', ..., 'lineups', ..., 'items',
|
'content', ..., 'lineups', ..., 'items',
|
||||||
lambda _, v: v['url'] == video_id, any, {require('item info')}))
|
lambda _, v: v['url'] == video_id, any, {require('item info')}))
|
||||||
|
media_id = item_info['idMedia']
|
||||||
|
|
||||||
|
email, password = self._get_login_info()
|
||||||
|
if email and password:
|
||||||
|
claims_token = self._get_claims_token(email, password)
|
||||||
|
headers = {'x-claims-token': claims_token}
|
||||||
|
else:
|
||||||
headers = {}
|
headers = {}
|
||||||
if claims_token := self._fetch_claims_token():
|
|
||||||
headers['x-claims-token'] = claims_token
|
|
||||||
|
|
||||||
m3u8_info = self._download_json(
|
m3u8_info = self._download_json(
|
||||||
'https://services.radio-canada.ca/media/validation/v2/',
|
'https://services.radio-canada.ca/media/validation/v2/',
|
||||||
@ -708,7 +695,7 @@ class CBCGemIE(CBCGemBaseIE):
|
|||||||
'tech': 'hls',
|
'tech': 'hls',
|
||||||
'manifestVersion': '2',
|
'manifestVersion': '2',
|
||||||
'manifestType': 'desktop',
|
'manifestType': 'desktop',
|
||||||
'idMedia': item_info['idMedia'],
|
'idMedia': media_id,
|
||||||
})
|
})
|
||||||
|
|
||||||
if m3u8_info.get('errorCode') == 1:
|
if m3u8_info.get('errorCode') == 1:
|
||||||
|
@ -13,17 +13,16 @@ from ..compat import compat_ord
|
|||||||
from ..utils import (
|
from ..utils import (
|
||||||
ExtractorError,
|
ExtractorError,
|
||||||
OnDemandPagedList,
|
OnDemandPagedList,
|
||||||
determine_ext,
|
|
||||||
float_or_none,
|
float_or_none,
|
||||||
int_or_none,
|
int_or_none,
|
||||||
merge_dicts,
|
merge_dicts,
|
||||||
multipart_encode,
|
multipart_encode,
|
||||||
parse_duration,
|
parse_duration,
|
||||||
|
traverse_obj,
|
||||||
try_call,
|
try_call,
|
||||||
url_or_none,
|
try_get,
|
||||||
urljoin,
|
urljoin,
|
||||||
)
|
)
|
||||||
from ..utils.traversal import traverse_obj
|
|
||||||
|
|
||||||
|
|
||||||
class CDAIE(InfoExtractor):
|
class CDAIE(InfoExtractor):
|
||||||
@ -122,7 +121,10 @@ class CDAIE(InfoExtractor):
|
|||||||
}, **kwargs)
|
}, **kwargs)
|
||||||
|
|
||||||
def _perform_login(self, username, password):
|
def _perform_login(self, username, password):
|
||||||
app_version = '1.2.255 build 21541'
|
app_version = random.choice((
|
||||||
|
'1.2.88 build 15306',
|
||||||
|
'1.2.174 build 18469',
|
||||||
|
))
|
||||||
android_version = random.randrange(8, 14)
|
android_version = random.randrange(8, 14)
|
||||||
phone_model = random.choice((
|
phone_model = random.choice((
|
||||||
# x-kom.pl top selling Android smartphones, as of 2022-12-26
|
# x-kom.pl top selling Android smartphones, as of 2022-12-26
|
||||||
@ -188,7 +190,7 @@ class CDAIE(InfoExtractor):
|
|||||||
meta = self._download_json(
|
meta = self._download_json(
|
||||||
f'{self._BASE_API_URL}/video/{video_id}', video_id, headers=self._API_HEADERS)['video']
|
f'{self._BASE_API_URL}/video/{video_id}', video_id, headers=self._API_HEADERS)['video']
|
||||||
|
|
||||||
uploader = traverse_obj(meta, ('author', 'login', {str}))
|
uploader = traverse_obj(meta, 'author', 'login')
|
||||||
|
|
||||||
formats = [{
|
formats = [{
|
||||||
'url': quality['file'],
|
'url': quality['file'],
|
||||||
@ -291,16 +293,15 @@ class CDAIE(InfoExtractor):
|
|||||||
if not video or 'file' not in video:
|
if not video or 'file' not in video:
|
||||||
self.report_warning(f'Unable to extract {version} version information')
|
self.report_warning(f'Unable to extract {version} version information')
|
||||||
return
|
return
|
||||||
video_quality = video.get('quality')
|
|
||||||
qualities = video.get('qualities', {})
|
|
||||||
video_quality = next((k for k, v in qualities.items() if v == video_quality), video_quality)
|
|
||||||
if video.get('file'):
|
|
||||||
if video['file'].startswith('uggc'):
|
if video['file'].startswith('uggc'):
|
||||||
video['file'] = codecs.decode(video['file'], 'rot_13')
|
video['file'] = codecs.decode(video['file'], 'rot_13')
|
||||||
if video['file'].endswith('adc.mp4'):
|
if video['file'].endswith('adc.mp4'):
|
||||||
video['file'] = video['file'].replace('adc.mp4', '.mp4')
|
video['file'] = video['file'].replace('adc.mp4', '.mp4')
|
||||||
elif not video['file'].startswith('http'):
|
elif not video['file'].startswith('http'):
|
||||||
video['file'] = decrypt_file(video['file'])
|
video['file'] = decrypt_file(video['file'])
|
||||||
|
video_quality = video.get('quality')
|
||||||
|
qualities = video.get('qualities', {})
|
||||||
|
video_quality = next((k for k, v in qualities.items() if v == video_quality), video_quality)
|
||||||
info_dict['formats'].append({
|
info_dict['formats'].append({
|
||||||
'url': video['file'],
|
'url': video['file'],
|
||||||
'format_id': video_quality,
|
'format_id': video_quality,
|
||||||
@ -312,26 +313,14 @@ class CDAIE(InfoExtractor):
|
|||||||
data = {'jsonrpc': '2.0', 'method': 'videoGetLink', 'id': 2,
|
data = {'jsonrpc': '2.0', 'method': 'videoGetLink', 'id': 2,
|
||||||
'params': [video_id, cda_quality, video.get('ts'), video.get('hash2'), {}]}
|
'params': [video_id, cda_quality, video.get('ts'), video.get('hash2'), {}]}
|
||||||
data = json.dumps(data).encode()
|
data = json.dumps(data).encode()
|
||||||
response = self._download_json(
|
video_url = self._download_json(
|
||||||
f'https://www.cda.pl/video/{video_id}', video_id, headers={
|
f'https://www.cda.pl/video/{video_id}', video_id, headers={
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
'X-Requested-With': 'XMLHttpRequest',
|
'X-Requested-With': 'XMLHttpRequest',
|
||||||
}, data=data, note=f'Fetching {quality} url',
|
}, data=data, note=f'Fetching {quality} url',
|
||||||
errnote=f'Failed to fetch {quality} url', fatal=False)
|
errnote=f'Failed to fetch {quality} url', fatal=False)
|
||||||
if (
|
if try_get(video_url, lambda x: x['result']['status']) == 'ok':
|
||||||
traverse_obj(response, ('result', 'status')) != 'ok'
|
video_url = try_get(video_url, lambda x: x['result']['resp'])
|
||||||
or not traverse_obj(response, ('result', 'resp', {url_or_none}))
|
|
||||||
):
|
|
||||||
continue
|
|
||||||
video_url = response['result']['resp']
|
|
||||||
ext = determine_ext(video_url)
|
|
||||||
if ext == 'mpd':
|
|
||||||
info_dict['formats'].extend(self._extract_mpd_formats(
|
|
||||||
video_url, video_id, mpd_id='dash', fatal=False))
|
|
||||||
elif ext == 'm3u8':
|
|
||||||
info_dict['formats'].extend(self._extract_m3u8_formats(
|
|
||||||
video_url, video_id, 'mp4', m3u8_id='hls', fatal=False))
|
|
||||||
else:
|
|
||||||
info_dict['formats'].append({
|
info_dict['formats'].append({
|
||||||
'url': video_url,
|
'url': video_url,
|
||||||
'format_id': quality,
|
'format_id': quality,
|
||||||
@ -367,7 +356,7 @@ class CDAIE(InfoExtractor):
|
|||||||
|
|
||||||
class CDAFolderIE(InfoExtractor):
|
class CDAFolderIE(InfoExtractor):
|
||||||
_MAX_PAGE_SIZE = 36
|
_MAX_PAGE_SIZE = 36
|
||||||
_VALID_URL = r'https?://(?:www\.)?cda\.pl/(?P<channel>[\w-]+)/folder/(?P<id>\d+)'
|
_VALID_URL = r'https?://(?:www\.)?cda\.pl/(?P<channel>\w+)/folder/(?P<id>\d+)'
|
||||||
_TESTS = [
|
_TESTS = [
|
||||||
{
|
{
|
||||||
'url': 'https://www.cda.pl/domino264/folder/31188385',
|
'url': 'https://www.cda.pl/domino264/folder/31188385',
|
||||||
@ -392,9 +381,6 @@ class CDAFolderIE(InfoExtractor):
|
|||||||
'title': 'TESTY KOSMETYKÓW',
|
'title': 'TESTY KOSMETYKÓW',
|
||||||
},
|
},
|
||||||
'playlist_mincount': 139,
|
'playlist_mincount': 139,
|
||||||
}, {
|
|
||||||
'url': 'https://www.cda.pl/FILMY-SERIALE-ANIME-KRESKOWKI-BAJKI/folder/18493422',
|
|
||||||
'only_matching': True,
|
|
||||||
}]
|
}]
|
||||||
|
|
||||||
def _real_extract(self, url):
|
def _real_extract(self, url):
|
||||||
|
@ -21,7 +21,7 @@ class CHZZKLiveIE(InfoExtractor):
|
|||||||
'channel': '진짜도현',
|
'channel': '진짜도현',
|
||||||
'channel_id': 'c68b8ef525fb3d2fa146344d84991753',
|
'channel_id': 'c68b8ef525fb3d2fa146344d84991753',
|
||||||
'channel_is_verified': False,
|
'channel_is_verified': False,
|
||||||
'thumbnail': r're:https?://.+/.+\.jpg',
|
'thumbnail': r're:^https?://.*\.jpg$',
|
||||||
'timestamp': 1705510344,
|
'timestamp': 1705510344,
|
||||||
'upload_date': '20240117',
|
'upload_date': '20240117',
|
||||||
'live_status': 'is_live',
|
'live_status': 'is_live',
|
||||||
@ -98,7 +98,7 @@ class CHZZKVideoIE(InfoExtractor):
|
|||||||
'channel': '침착맨',
|
'channel': '침착맨',
|
||||||
'channel_id': 'bb382c2c0cc9fa7c86ab3b037fb5799c',
|
'channel_id': 'bb382c2c0cc9fa7c86ab3b037fb5799c',
|
||||||
'channel_is_verified': False,
|
'channel_is_verified': False,
|
||||||
'thumbnail': r're:https?://.+/.+\.jpg',
|
'thumbnail': r're:^https?://.*\.jpg$',
|
||||||
'duration': 15577,
|
'duration': 15577,
|
||||||
'timestamp': 1702970505.417,
|
'timestamp': 1702970505.417,
|
||||||
'upload_date': '20231219',
|
'upload_date': '20231219',
|
||||||
@ -115,7 +115,7 @@ class CHZZKVideoIE(InfoExtractor):
|
|||||||
'channel': '라디유radiyu',
|
'channel': '라디유radiyu',
|
||||||
'channel_id': '68f895c59a1043bc5019b5e08c83a5c5',
|
'channel_id': '68f895c59a1043bc5019b5e08c83a5c5',
|
||||||
'channel_is_verified': False,
|
'channel_is_verified': False,
|
||||||
'thumbnail': r're:https?://.+/.+\.jpg',
|
'thumbnail': r're:^https?://.*\.jpg$',
|
||||||
'duration': 95,
|
'duration': 95,
|
||||||
'timestamp': 1703102631.722,
|
'timestamp': 1703102631.722,
|
||||||
'upload_date': '20231220',
|
'upload_date': '20231220',
|
||||||
@ -131,30 +131,12 @@ class CHZZKVideoIE(InfoExtractor):
|
|||||||
'channel': '강지',
|
'channel': '강지',
|
||||||
'channel_id': 'b5ed5db484d04faf4d150aedd362f34b',
|
'channel_id': 'b5ed5db484d04faf4d150aedd362f34b',
|
||||||
'channel_is_verified': True,
|
'channel_is_verified': True,
|
||||||
'thumbnail': r're:https?://.+/.+\.jpg',
|
'thumbnail': r're:^https?://.*\.jpg$',
|
||||||
'duration': 4433,
|
'duration': 4433,
|
||||||
'timestamp': 1703307460.214,
|
'timestamp': 1703307460.214,
|
||||||
'upload_date': '20231223',
|
'upload_date': '20231223',
|
||||||
'view_count': int,
|
'view_count': int,
|
||||||
},
|
},
|
||||||
}, {
|
|
||||||
# video_status == 'NONE' but is downloadable
|
|
||||||
'url': 'https://chzzk.naver.com/video/6325166',
|
|
||||||
'info_dict': {
|
|
||||||
'id': '6325166',
|
|
||||||
'ext': 'mp4',
|
|
||||||
'title': '와이프 숙제빼주기',
|
|
||||||
'channel': '이 다',
|
|
||||||
'channel_id': '0076a519f147ee9fd0959bf02f9571ca',
|
|
||||||
'channel_is_verified': False,
|
|
||||||
'view_count': int,
|
|
||||||
'duration': 28167,
|
|
||||||
'thumbnail': r're:https?://.+/.+\.jpg',
|
|
||||||
'timestamp': 1742139216.86,
|
|
||||||
'upload_date': '20250316',
|
|
||||||
'live_status': 'was_live',
|
|
||||||
},
|
|
||||||
'params': {'skip_download': 'm3u8'},
|
|
||||||
}]
|
}]
|
||||||
|
|
||||||
def _real_extract(self, url):
|
def _real_extract(self, url):
|
||||||
@ -165,7 +147,11 @@ class CHZZKVideoIE(InfoExtractor):
|
|||||||
|
|
||||||
live_status = 'was_live' if video_meta.get('liveOpenDate') else 'not_live'
|
live_status = 'was_live' if video_meta.get('liveOpenDate') else 'not_live'
|
||||||
video_status = video_meta.get('vodStatus')
|
video_status = video_meta.get('vodStatus')
|
||||||
if video_status == 'ABR_HLS':
|
if video_status == 'UPLOAD':
|
||||||
|
playback = self._parse_json(video_meta['liveRewindPlaybackJson'], video_id)
|
||||||
|
formats, subtitles = self._extract_m3u8_formats_and_subtitles(
|
||||||
|
playback['media'][0]['path'], video_id, 'mp4', m3u8_id='hls')
|
||||||
|
elif video_status == 'ABR_HLS':
|
||||||
formats, subtitles = self._extract_mpd_formats_and_subtitles(
|
formats, subtitles = self._extract_mpd_formats_and_subtitles(
|
||||||
f'https://apis.naver.com/neonplayer/vodplay/v1/playback/{video_meta["videoId"]}',
|
f'https://apis.naver.com/neonplayer/vodplay/v1/playback/{video_meta["videoId"]}',
|
||||||
video_id, query={
|
video_id, query={
|
||||||
@ -175,13 +161,6 @@ class CHZZKVideoIE(InfoExtractor):
|
|||||||
'cpl': 'en_US',
|
'cpl': 'en_US',
|
||||||
})
|
})
|
||||||
else:
|
else:
|
||||||
fatal = video_status == 'UPLOAD'
|
|
||||||
playback = self._parse_json(video_meta['liveRewindPlaybackJson'], video_id, fatal=fatal)
|
|
||||||
formats, subtitles = self._extract_m3u8_formats_and_subtitles(
|
|
||||||
traverse_obj(playback, ('media', 0, 'path')), video_id, 'mp4', m3u8_id='hls', fatal=fatal)
|
|
||||||
if formats and video_status != 'UPLOAD':
|
|
||||||
self.write_debug(f'Video found with status: "{video_status}"')
|
|
||||||
elif not formats:
|
|
||||||
self.raise_no_formats(
|
self.raise_no_formats(
|
||||||
f'Unknown video status detected: "{video_status}"', expected=True, video_id=video_id)
|
f'Unknown video status detected: "{video_status}"', expected=True, video_id=video_id)
|
||||||
formats, subtitles = [], {}
|
formats, subtitles = [], {}
|
||||||
|
@ -29,7 +29,6 @@ from ..compat import (
|
|||||||
from ..cookies import LenientSimpleCookie
|
from ..cookies import LenientSimpleCookie
|
||||||
from ..downloader.f4m import get_base_url, remove_encrypted_media
|
from ..downloader.f4m import get_base_url, remove_encrypted_media
|
||||||
from ..downloader.hls import HlsFD
|
from ..downloader.hls import HlsFD
|
||||||
from ..globals import plugin_ies_overrides
|
|
||||||
from ..networking import HEADRequest, Request
|
from ..networking import HEADRequest, Request
|
||||||
from ..networking.exceptions import (
|
from ..networking.exceptions import (
|
||||||
HTTPError,
|
HTTPError,
|
||||||
@ -78,7 +77,6 @@ from ..utils import (
|
|||||||
parse_iso8601,
|
parse_iso8601,
|
||||||
parse_m3u8_attributes,
|
parse_m3u8_attributes,
|
||||||
parse_resolution,
|
parse_resolution,
|
||||||
qualities,
|
|
||||||
sanitize_url,
|
sanitize_url,
|
||||||
smuggle_url,
|
smuggle_url,
|
||||||
str_or_none,
|
str_or_none,
|
||||||
@ -1570,8 +1568,6 @@ class InfoExtractor:
|
|||||||
"""Yield all json ld objects in the html"""
|
"""Yield all json ld objects in the html"""
|
||||||
if default is not NO_DEFAULT:
|
if default is not NO_DEFAULT:
|
||||||
fatal = False
|
fatal = False
|
||||||
if not fatal and not isinstance(html, str):
|
|
||||||
return
|
|
||||||
for mobj in re.finditer(JSON_LD_RE, html):
|
for mobj in re.finditer(JSON_LD_RE, html):
|
||||||
json_ld_item = self._parse_json(
|
json_ld_item = self._parse_json(
|
||||||
mobj.group('json_ld'), video_id, fatal=fatal,
|
mobj.group('json_ld'), video_id, fatal=fatal,
|
||||||
@ -2180,8 +2176,6 @@ class InfoExtractor:
|
|||||||
media_url = media.get('URI')
|
media_url = media.get('URI')
|
||||||
if media_url:
|
if media_url:
|
||||||
manifest_url = format_url(media_url)
|
manifest_url = format_url(media_url)
|
||||||
is_audio = media_type == 'AUDIO'
|
|
||||||
is_alternate = media.get('DEFAULT') == 'NO' or media.get('AUTOSELECT') == 'NO'
|
|
||||||
formats.extend({
|
formats.extend({
|
||||||
'format_id': join_nonempty(m3u8_id, group_id, name, idx),
|
'format_id': join_nonempty(m3u8_id, group_id, name, idx),
|
||||||
'format_note': name,
|
'format_note': name,
|
||||||
@ -2194,11 +2188,7 @@ class InfoExtractor:
|
|||||||
'preference': preference,
|
'preference': preference,
|
||||||
'quality': quality,
|
'quality': quality,
|
||||||
'has_drm': has_drm,
|
'has_drm': has_drm,
|
||||||
'vcodec': 'none' if is_audio else None,
|
'vcodec': 'none' if media_type == 'AUDIO' else None,
|
||||||
# Alternate audio formats (e.g. audio description) should be deprioritized
|
|
||||||
'source_preference': -2 if is_audio and is_alternate else None,
|
|
||||||
# Save this to assign source_preference based on associated video stream
|
|
||||||
'_audio_group_id': group_id if is_audio and not is_alternate else None,
|
|
||||||
} for idx in _extract_m3u8_playlist_indices(manifest_url))
|
} for idx in _extract_m3u8_playlist_indices(manifest_url))
|
||||||
|
|
||||||
def build_stream_name():
|
def build_stream_name():
|
||||||
@ -2293,8 +2283,6 @@ class InfoExtractor:
|
|||||||
# ignore references to rendition groups and treat them
|
# ignore references to rendition groups and treat them
|
||||||
# as complete formats.
|
# as complete formats.
|
||||||
if audio_group_id and codecs and f.get('vcodec') != 'none':
|
if audio_group_id and codecs and f.get('vcodec') != 'none':
|
||||||
# Save this to determine quality of audio formats that only have a GROUP-ID
|
|
||||||
f['_audio_group_id'] = audio_group_id
|
|
||||||
audio_group = groups.get(audio_group_id)
|
audio_group = groups.get(audio_group_id)
|
||||||
if audio_group and audio_group[0].get('URI'):
|
if audio_group and audio_group[0].get('URI'):
|
||||||
# TODO: update acodec for audio only formats with
|
# TODO: update acodec for audio only formats with
|
||||||
@ -2317,28 +2305,6 @@ class InfoExtractor:
|
|||||||
formats.append(http_f)
|
formats.append(http_f)
|
||||||
|
|
||||||
last_stream_inf = {}
|
last_stream_inf = {}
|
||||||
|
|
||||||
# Some audio-only formats only have a GROUP-ID without any other quality/bitrate/codec info
|
|
||||||
# Each audio GROUP-ID corresponds with one or more video formats' AUDIO attribute
|
|
||||||
# For sorting purposes, set source_preference based on the quality of the video formats they are grouped with
|
|
||||||
# See https://github.com/yt-dlp/yt-dlp/issues/11178
|
|
||||||
audio_groups_by_quality = orderedSet(f['_audio_group_id'] for f in sorted(
|
|
||||||
traverse_obj(formats, lambda _, v: v.get('vcodec') != 'none' and v['_audio_group_id']),
|
|
||||||
key=lambda x: (x.get('tbr') or 0, x.get('width') or 0)))
|
|
||||||
audio_quality_map = {
|
|
||||||
audio_groups_by_quality[0]: 'low',
|
|
||||||
audio_groups_by_quality[-1]: 'high',
|
|
||||||
} if len(audio_groups_by_quality) > 1 else None
|
|
||||||
audio_preference = qualities(audio_groups_by_quality)
|
|
||||||
for fmt in formats:
|
|
||||||
audio_group_id = fmt.pop('_audio_group_id', None)
|
|
||||||
if not audio_quality_map or not audio_group_id or fmt.get('vcodec') != 'none':
|
|
||||||
continue
|
|
||||||
# Use source_preference since quality and preference are set by params
|
|
||||||
fmt['source_preference'] = audio_preference(audio_group_id)
|
|
||||||
fmt['format_note'] = join_nonempty(
|
|
||||||
fmt.get('format_note'), audio_quality_map.get(audio_group_id), delim=', ')
|
|
||||||
|
|
||||||
return formats, subtitles
|
return formats, subtitles
|
||||||
|
|
||||||
def _extract_m3u8_vod_duration(
|
def _extract_m3u8_vod_duration(
|
||||||
@ -2968,7 +2934,8 @@ class InfoExtractor:
|
|||||||
segment_duration = None
|
segment_duration = None
|
||||||
if 'total_number' not in representation_ms_info and 'segment_duration' in representation_ms_info:
|
if 'total_number' not in representation_ms_info and 'segment_duration' in representation_ms_info:
|
||||||
segment_duration = float_or_none(representation_ms_info['segment_duration'], representation_ms_info['timescale'])
|
segment_duration = float_or_none(representation_ms_info['segment_duration'], representation_ms_info['timescale'])
|
||||||
representation_ms_info['total_number'] = math.ceil(float_or_none(period_duration, segment_duration, default=0))
|
representation_ms_info['total_number'] = int(math.ceil(
|
||||||
|
float_or_none(period_duration, segment_duration, default=0)))
|
||||||
representation_ms_info['fragments'] = [{
|
representation_ms_info['fragments'] = [{
|
||||||
media_location_key: media_template % {
|
media_location_key: media_template % {
|
||||||
'Number': segment_number,
|
'Number': segment_number,
|
||||||
@ -3987,18 +3954,14 @@ class InfoExtractor:
|
|||||||
def __init_subclass__(cls, *, plugin_name=None, **kwargs):
|
def __init_subclass__(cls, *, plugin_name=None, **kwargs):
|
||||||
if plugin_name:
|
if plugin_name:
|
||||||
mro = inspect.getmro(cls)
|
mro = inspect.getmro(cls)
|
||||||
next_mro_class = super_class = mro[mro.index(cls) + 1]
|
super_class = cls.__wrapped__ = mro[mro.index(cls) + 1]
|
||||||
|
cls.PLUGIN_NAME, cls.ie_key = plugin_name, super_class.ie_key
|
||||||
|
cls.IE_NAME = f'{super_class.IE_NAME}+{plugin_name}'
|
||||||
while getattr(super_class, '__wrapped__', None):
|
while getattr(super_class, '__wrapped__', None):
|
||||||
super_class = super_class.__wrapped__
|
super_class = super_class.__wrapped__
|
||||||
|
|
||||||
if not any(override.PLUGIN_NAME == plugin_name for override in plugin_ies_overrides.value[super_class]):
|
|
||||||
cls.__wrapped__ = next_mro_class
|
|
||||||
cls.PLUGIN_NAME, cls.ie_key = plugin_name, next_mro_class.ie_key
|
|
||||||
cls.IE_NAME = f'{next_mro_class.IE_NAME}+{plugin_name}'
|
|
||||||
|
|
||||||
setattr(sys.modules[super_class.__module__], super_class.__name__, cls)
|
setattr(sys.modules[super_class.__module__], super_class.__name__, cls)
|
||||||
plugin_ies_overrides.value[super_class].append(cls)
|
_PLUGIN_OVERRIDES[super_class].append(cls)
|
||||||
|
|
||||||
return super().__init_subclass__(**kwargs)
|
return super().__init_subclass__(**kwargs)
|
||||||
|
|
||||||
|
|
||||||
@ -4054,3 +4017,6 @@ class UnsupportedURLIE(InfoExtractor):
|
|||||||
|
|
||||||
def _real_extract(self, url):
|
def _real_extract(self, url):
|
||||||
raise UnsupportedError(url)
|
raise UnsupportedError(url)
|
||||||
|
|
||||||
|
|
||||||
|
_PLUGIN_OVERRIDES = collections.defaultdict(list)
|
||||||
|
@ -5,9 +5,7 @@ from ..utils import (
|
|||||||
int_or_none,
|
int_or_none,
|
||||||
try_get,
|
try_get,
|
||||||
unified_strdate,
|
unified_strdate,
|
||||||
url_or_none,
|
|
||||||
)
|
)
|
||||||
from ..utils.traversal import traverse_obj
|
|
||||||
|
|
||||||
|
|
||||||
class CrowdBunkerIE(InfoExtractor):
|
class CrowdBunkerIE(InfoExtractor):
|
||||||
@ -46,15 +44,16 @@ class CrowdBunkerIE(InfoExtractor):
|
|||||||
'url': sub_url,
|
'url': sub_url,
|
||||||
})
|
})
|
||||||
|
|
||||||
if mpd_url := traverse_obj(video_json, ('dashManifest', 'url', {url_or_none})):
|
mpd_url = try_get(video_json, lambda x: x['dashManifest']['url'])
|
||||||
fmts, subs = self._extract_mpd_formats_and_subtitles(mpd_url, video_id, mpd_id='dash', fatal=False)
|
if mpd_url:
|
||||||
|
fmts, subs = self._extract_mpd_formats_and_subtitles(mpd_url, video_id)
|
||||||
formats.extend(fmts)
|
formats.extend(fmts)
|
||||||
self._merge_subtitles(subs, target=subtitles)
|
subtitles = self._merge_subtitles(subtitles, subs)
|
||||||
|
m3u8_url = try_get(video_json, lambda x: x['hlsManifest']['url'])
|
||||||
if m3u8_url := traverse_obj(video_json, ('hlsManifest', 'url', {url_or_none})):
|
if m3u8_url:
|
||||||
fmts, subs = self._extract_m3u8_formats_and_subtitles(m3u8_url, video_id, m3u8_id='hls', fatal=False)
|
fmts, subs = self._extract_m3u8_formats_and_subtitles(mpd_url, video_id)
|
||||||
formats.extend(fmts)
|
formats.extend(fmts)
|
||||||
self._merge_subtitles(subs, target=subtitles)
|
subtitles = self._merge_subtitles(subtitles, subs)
|
||||||
|
|
||||||
thumbnails = [{
|
thumbnails = [{
|
||||||
'url': image['url'],
|
'url': image['url'],
|
||||||
|
@ -3,7 +3,7 @@ from ..utils import int_or_none
|
|||||||
|
|
||||||
|
|
||||||
class CultureUnpluggedIE(InfoExtractor):
|
class CultureUnpluggedIE(InfoExtractor):
|
||||||
_VALID_URL = r'https?://(?:www\.)?cultureunplugged\.com/(?:documentary/watch-online/)?play/(?P<id>\d+)(?:/(?P<display_id>[^/#?]+))?'
|
_VALID_URL = r'https?://(?:www\.)?cultureunplugged\.com/documentary/watch-online/play/(?P<id>\d+)(?:/(?P<display_id>[^/]+))?'
|
||||||
_TESTS = [{
|
_TESTS = [{
|
||||||
'url': 'http://www.cultureunplugged.com/documentary/watch-online/play/53662/The-Next--Best-West',
|
'url': 'http://www.cultureunplugged.com/documentary/watch-online/play/53662/The-Next--Best-West',
|
||||||
'md5': 'ac6c093b089f7d05e79934dcb3d228fc',
|
'md5': 'ac6c093b089f7d05e79934dcb3d228fc',
|
||||||
@ -12,25 +12,12 @@ class CultureUnpluggedIE(InfoExtractor):
|
|||||||
'display_id': 'The-Next--Best-West',
|
'display_id': 'The-Next--Best-West',
|
||||||
'ext': 'mp4',
|
'ext': 'mp4',
|
||||||
'title': 'The Next, Best West',
|
'title': 'The Next, Best West',
|
||||||
'description': 'md5:770033a3b7c2946a3bcfb7f1c6fb7045',
|
'description': 'md5:0423cd00833dea1519cf014e9d0903b1',
|
||||||
'thumbnail': r're:^https?://.*\.jpg$',
|
'thumbnail': r're:^https?://.*\.jpg$',
|
||||||
'creators': ['Coldstream Creative'],
|
'creator': 'Coldstream Creative',
|
||||||
'duration': 2203,
|
'duration': 2203,
|
||||||
'view_count': int,
|
'view_count': int,
|
||||||
},
|
},
|
||||||
}, {
|
|
||||||
'url': 'https://www.cultureunplugged.com/play/2833/Koi-Sunta-Hai--Journeys-with-Kumar---Kabir--Someone-is-Listening-',
|
|
||||||
'md5': 'dc2014bc470dfccba389a1c934fa29fa',
|
|
||||||
'info_dict': {
|
|
||||||
'id': '2833',
|
|
||||||
'display_id': 'Koi-Sunta-Hai--Journeys-with-Kumar---Kabir--Someone-is-Listening-',
|
|
||||||
'ext': 'mp4',
|
|
||||||
'title': 'Koi Sunta Hai: Journeys with Kumar & Kabir (Someone is Listening)',
|
|
||||||
'description': 'md5:fa94ac934927c98660362b8285b2cda5',
|
|
||||||
'view_count': int,
|
|
||||||
'thumbnail': 'https://s3.amazonaws.com/cdn.cultureunplugged.com/thumbnails_16_9/lg/2833.jpg',
|
|
||||||
'creators': ['Srishti'],
|
|
||||||
},
|
|
||||||
}, {
|
}, {
|
||||||
'url': 'http://www.cultureunplugged.com/documentary/watch-online/play/53662',
|
'url': 'http://www.cultureunplugged.com/documentary/watch-online/play/53662',
|
||||||
'only_matching': True,
|
'only_matching': True,
|
||||||
|
@ -9,7 +9,6 @@ from ..utils import (
|
|||||||
ExtractorError,
|
ExtractorError,
|
||||||
classproperty,
|
classproperty,
|
||||||
float_or_none,
|
float_or_none,
|
||||||
parse_qs,
|
|
||||||
traverse_obj,
|
traverse_obj,
|
||||||
url_or_none,
|
url_or_none,
|
||||||
)
|
)
|
||||||
@ -92,15 +91,11 @@ class DacastVODIE(DacastBaseIE):
|
|||||||
# Rotates every so often, but hardcode a fallback in case of JS change/breakage before rotation
|
# Rotates every so often, but hardcode a fallback in case of JS change/breakage before rotation
|
||||||
return self._search_regex(
|
return self._search_regex(
|
||||||
r'\bUSP_SIGNING_SECRET\s*=\s*(["\'])(?P<secret>(?:(?!\1).)+)', player_js,
|
r'\bUSP_SIGNING_SECRET\s*=\s*(["\'])(?P<secret>(?:(?!\1).)+)', player_js,
|
||||||
'usp signing secret', group='secret', fatal=False) or 'hGDtqMKYVeFdofrAfFmBcrsakaZELajI'
|
'usp signing secret', group='secret', fatal=False) or 'odnInCGqhvtyRTtIiddxtuRtawYYICZP'
|
||||||
|
|
||||||
def _real_extract(self, url):
|
def _real_extract(self, url):
|
||||||
user_id, video_id = self._match_valid_url(url).group('user_id', 'id')
|
user_id, video_id = self._match_valid_url(url).group('user_id', 'id')
|
||||||
query = {
|
query = {'contentId': f'{user_id}-vod-{video_id}', 'provider': 'universe'}
|
||||||
'contentId': f'{user_id}-vod-{video_id}',
|
|
||||||
'provider': 'universe',
|
|
||||||
**traverse_obj(url, ({parse_qs}, 'uss_token', {'signedKey': -1})),
|
|
||||||
}
|
|
||||||
info = self._download_json(self._API_INFO_URL, video_id, query=query, fatal=False)
|
info = self._download_json(self._API_INFO_URL, video_id, query=query, fatal=False)
|
||||||
access = self._download_json(
|
access = self._download_json(
|
||||||
'https://playback.dacast.com/content/access', video_id,
|
'https://playback.dacast.com/content/access', video_id,
|
||||||
|
@ -100,7 +100,7 @@ class DailymotionBaseInfoExtractor(InfoExtractor):
|
|||||||
|
|
||||||
class DailymotionIE(DailymotionBaseInfoExtractor):
|
class DailymotionIE(DailymotionBaseInfoExtractor):
|
||||||
_VALID_URL = r'''(?ix)
|
_VALID_URL = r'''(?ix)
|
||||||
(?:https?:)?//
|
https?://
|
||||||
(?:
|
(?:
|
||||||
dai\.ly/|
|
dai\.ly/|
|
||||||
(?:
|
(?:
|
||||||
@ -116,7 +116,7 @@ class DailymotionIE(DailymotionBaseInfoExtractor):
|
|||||||
(?P<id>[^/?_&#]+)(?:[\w-]*\?playlist=(?P<playlist_id>x[0-9a-z]+))?
|
(?P<id>[^/?_&#]+)(?:[\w-]*\?playlist=(?P<playlist_id>x[0-9a-z]+))?
|
||||||
'''
|
'''
|
||||||
IE_NAME = 'dailymotion'
|
IE_NAME = 'dailymotion'
|
||||||
_EMBED_REGEX = [rf'(?ix)<(?:(?:embed|iframe)[^>]+?src=|input[^>]+id=[\'"]dmcloudUrlEmissionSelect[\'"][^>]+value=)["\'](?P<url>{_VALID_URL[5:]})']
|
_EMBED_REGEX = [r'<(?:(?:embed|iframe)[^>]+?src=|input[^>]+id=[\'"]dmcloudUrlEmissionSelect[\'"][^>]+value=)(["\'])(?P<url>(?:https?:)?//(?:www\.)?dailymotion\.com/(?:embed|swf)/video/.+?)\1']
|
||||||
_TESTS = [{
|
_TESTS = [{
|
||||||
'url': 'http://www.dailymotion.com/video/x5kesuj_office-christmas-party-review-jason-bateman-olivia-munn-t-j-miller_news',
|
'url': 'http://www.dailymotion.com/video/x5kesuj_office-christmas-party-review-jason-bateman-olivia-munn-t-j-miller_news',
|
||||||
'md5': '074b95bdee76b9e3654137aee9c79dfe',
|
'md5': '074b95bdee76b9e3654137aee9c79dfe',
|
||||||
@ -308,25 +308,6 @@ class DailymotionIE(DailymotionBaseInfoExtractor):
|
|||||||
'description': 'Que lindura',
|
'description': 'Que lindura',
|
||||||
'tags': [],
|
'tags': [],
|
||||||
},
|
},
|
||||||
}, {
|
|
||||||
# //geo.dailymotion.com/player/xysxq.html?video=k2Y4Mjp7krAF9iCuINM
|
|
||||||
'url': 'https://lcp.fr/programmes/avant-la-catastrophe-la-naissance-de-la-dictature-nazie-1933-1936-346819',
|
|
||||||
'info_dict': {
|
|
||||||
'id': 'k2Y4Mjp7krAF9iCuINM',
|
|
||||||
'ext': 'mp4',
|
|
||||||
'title': 'Avant la catastrophe la naissance de la dictature nazie 1933 -1936',
|
|
||||||
'description': 'md5:7b620d5e26edbe45f27bbddc1c0257c1',
|
|
||||||
'uploader': 'LCP Assemblée nationale',
|
|
||||||
'uploader_id': 'xbz33d',
|
|
||||||
'view_count': int,
|
|
||||||
'like_count': int,
|
|
||||||
'age_limit': 0,
|
|
||||||
'duration': 3220,
|
|
||||||
'thumbnail': 'https://s1.dmcdn.net/v/Xvumk1djJBUZfjj2a/x1080',
|
|
||||||
'tags': [],
|
|
||||||
'timestamp': 1739919947,
|
|
||||||
'upload_date': '20250218',
|
|
||||||
},
|
|
||||||
}]
|
}]
|
||||||
_GEO_BYPASS = False
|
_GEO_BYPASS = False
|
||||||
_COMMON_MEDIA_FIELDS = '''description
|
_COMMON_MEDIA_FIELDS = '''description
|
||||||
|
142
yt_dlp/extractor/deezer.py
Normal file
142
yt_dlp/extractor/deezer.py
Normal file
@ -0,0 +1,142 @@
|
|||||||
|
import json
|
||||||
|
|
||||||
|
from .common import InfoExtractor
|
||||||
|
from ..utils import (
|
||||||
|
ExtractorError,
|
||||||
|
int_or_none,
|
||||||
|
orderedSet,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class DeezerBaseInfoExtractor(InfoExtractor):
|
||||||
|
def get_data(self, url):
|
||||||
|
if not self.get_param('test'):
|
||||||
|
self.report_warning('For now, this extractor only supports the 30 second previews. Patches welcome!')
|
||||||
|
|
||||||
|
mobj = self._match_valid_url(url)
|
||||||
|
data_id = mobj.group('id')
|
||||||
|
|
||||||
|
webpage = self._download_webpage(url, data_id)
|
||||||
|
geoblocking_msg = self._html_search_regex(
|
||||||
|
r'<p class="soon-txt">(.*?)</p>', webpage, 'geoblocking message',
|
||||||
|
default=None)
|
||||||
|
if geoblocking_msg is not None:
|
||||||
|
raise ExtractorError(
|
||||||
|
f'Deezer said: {geoblocking_msg}', expected=True)
|
||||||
|
|
||||||
|
data_json = self._search_regex(
|
||||||
|
(r'__DZR_APP_STATE__\s*=\s*({.+?})\s*</script>',
|
||||||
|
r'naboo\.display\(\'[^\']+\',\s*(.*?)\);\n'),
|
||||||
|
webpage, 'data JSON')
|
||||||
|
data = json.loads(data_json)
|
||||||
|
return data_id, webpage, data
|
||||||
|
|
||||||
|
|
||||||
|
class DeezerPlaylistIE(DeezerBaseInfoExtractor):
|
||||||
|
_VALID_URL = r'https?://(?:www\.)?deezer\.com/(../)?playlist/(?P<id>[0-9]+)'
|
||||||
|
_TEST = {
|
||||||
|
'url': 'http://www.deezer.com/playlist/176747451',
|
||||||
|
'info_dict': {
|
||||||
|
'id': '176747451',
|
||||||
|
'title': 'Best!',
|
||||||
|
'uploader': 'anonymous',
|
||||||
|
'thumbnail': r're:^https?://(e-)?cdns-images\.dzcdn\.net/images/cover/.*\.jpg$',
|
||||||
|
},
|
||||||
|
'playlist_count': 29,
|
||||||
|
}
|
||||||
|
|
||||||
|
def _real_extract(self, url):
|
||||||
|
playlist_id, webpage, data = self.get_data(url)
|
||||||
|
|
||||||
|
playlist_title = data.get('DATA', {}).get('TITLE')
|
||||||
|
playlist_uploader = data.get('DATA', {}).get('PARENT_USERNAME')
|
||||||
|
playlist_thumbnail = self._search_regex(
|
||||||
|
r'<img id="naboo_playlist_image".*?src="([^"]+)"', webpage,
|
||||||
|
'playlist thumbnail')
|
||||||
|
|
||||||
|
entries = []
|
||||||
|
for s in data.get('SONGS', {}).get('data'):
|
||||||
|
formats = [{
|
||||||
|
'format_id': 'preview',
|
||||||
|
'url': s.get('MEDIA', [{}])[0].get('HREF'),
|
||||||
|
'preference': -100, # Only the first 30 seconds
|
||||||
|
'ext': 'mp3',
|
||||||
|
}]
|
||||||
|
artists = ', '.join(
|
||||||
|
orderedSet(a.get('ART_NAME') for a in s.get('ARTISTS')))
|
||||||
|
entries.append({
|
||||||
|
'id': s.get('SNG_ID'),
|
||||||
|
'duration': int_or_none(s.get('DURATION')),
|
||||||
|
'title': '{} - {}'.format(artists, s.get('SNG_TITLE')),
|
||||||
|
'uploader': s.get('ART_NAME'),
|
||||||
|
'uploader_id': s.get('ART_ID'),
|
||||||
|
'age_limit': 16 if s.get('EXPLICIT_LYRICS') == '1' else 0,
|
||||||
|
'formats': formats,
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
'_type': 'playlist',
|
||||||
|
'id': playlist_id,
|
||||||
|
'title': playlist_title,
|
||||||
|
'uploader': playlist_uploader,
|
||||||
|
'thumbnail': playlist_thumbnail,
|
||||||
|
'entries': entries,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class DeezerAlbumIE(DeezerBaseInfoExtractor):
|
||||||
|
_VALID_URL = r'https?://(?:www\.)?deezer\.com/(../)?album/(?P<id>[0-9]+)'
|
||||||
|
_TEST = {
|
||||||
|
'url': 'https://www.deezer.com/fr/album/67505622',
|
||||||
|
'info_dict': {
|
||||||
|
'id': '67505622',
|
||||||
|
'title': 'Last Week',
|
||||||
|
'uploader': 'Home Brew',
|
||||||
|
'thumbnail': r're:^https?://(e-)?cdns-images\.dzcdn\.net/images/cover/.*\.jpg$',
|
||||||
|
},
|
||||||
|
'playlist_count': 7,
|
||||||
|
}
|
||||||
|
|
||||||
|
def _real_extract(self, url):
|
||||||
|
album_id, webpage, data = self.get_data(url)
|
||||||
|
|
||||||
|
album_title = data.get('DATA', {}).get('ALB_TITLE')
|
||||||
|
album_uploader = data.get('DATA', {}).get('ART_NAME')
|
||||||
|
album_thumbnail = self._search_regex(
|
||||||
|
r'<img id="naboo_album_image".*?src="([^"]+)"', webpage,
|
||||||
|
'album thumbnail')
|
||||||
|
|
||||||
|
entries = []
|
||||||
|
for s in data.get('SONGS', {}).get('data'):
|
||||||
|
formats = [{
|
||||||
|
'format_id': 'preview',
|
||||||
|
'url': s.get('MEDIA', [{}])[0].get('HREF'),
|
||||||
|
'preference': -100, # Only the first 30 seconds
|
||||||
|
'ext': 'mp3',
|
||||||
|
}]
|
||||||
|
artists = ', '.join(
|
||||||
|
orderedSet(a.get('ART_NAME') for a in s.get('ARTISTS')))
|
||||||
|
entries.append({
|
||||||
|
'id': s.get('SNG_ID'),
|
||||||
|
'duration': int_or_none(s.get('DURATION')),
|
||||||
|
'title': '{} - {}'.format(artists, s.get('SNG_TITLE')),
|
||||||
|
'uploader': s.get('ART_NAME'),
|
||||||
|
'uploader_id': s.get('ART_ID'),
|
||||||
|
'age_limit': 16 if s.get('EXPLICIT_LYRICS') == '1' else 0,
|
||||||
|
'formats': formats,
|
||||||
|
'track': s.get('SNG_TITLE'),
|
||||||
|
'track_number': int_or_none(s.get('TRACK_NUMBER')),
|
||||||
|
'track_id': s.get('SNG_ID'),
|
||||||
|
'artist': album_uploader,
|
||||||
|
'album': album_title,
|
||||||
|
'album_artist': album_uploader,
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
'_type': 'playlist',
|
||||||
|
'id': album_id,
|
||||||
|
'title': album_title,
|
||||||
|
'uploader': album_uploader,
|
||||||
|
'thumbnail': album_thumbnail,
|
||||||
|
'entries': entries,
|
||||||
|
}
|
@ -1,15 +1,9 @@
|
|||||||
from .zdf import ZDFBaseIE
|
from .zdf import ZDFBaseIE
|
||||||
from ..utils import (
|
|
||||||
int_or_none,
|
|
||||||
merge_dicts,
|
|
||||||
parse_iso8601,
|
|
||||||
)
|
|
||||||
from ..utils.traversal import require, traverse_obj
|
|
||||||
|
|
||||||
|
|
||||||
class DreiSatIE(ZDFBaseIE):
|
class DreiSatIE(ZDFBaseIE):
|
||||||
IE_NAME = '3sat'
|
IE_NAME = '3sat'
|
||||||
_VALID_URL = r'https?://(?:www\.)?3sat\.de/(?:[^/?#]+/)*(?P<id>[^/?#&]+)\.html'
|
_VALID_URL = r'https?://(?:www\.)?3sat\.de/(?:[^/]+/)*(?P<id>[^/?#&]+)\.html'
|
||||||
_TESTS = [{
|
_TESTS = [{
|
||||||
'url': 'https://www.3sat.de/dokumentation/reise/traumziele-suedostasiens-die-philippinen-und-vietnam-102.html',
|
'url': 'https://www.3sat.de/dokumentation/reise/traumziele-suedostasiens-die-philippinen-und-vietnam-102.html',
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
@ -18,59 +12,40 @@ class DreiSatIE(ZDFBaseIE):
|
|||||||
'title': 'Traumziele Südostasiens (1/2): Die Philippinen und Vietnam',
|
'title': 'Traumziele Südostasiens (1/2): Die Philippinen und Vietnam',
|
||||||
'description': 'md5:26329ce5197775b596773b939354079d',
|
'description': 'md5:26329ce5197775b596773b939354079d',
|
||||||
'duration': 2625.0,
|
'duration': 2625.0,
|
||||||
'thumbnail': 'https://www.3sat.de/assets/traumziele-suedostasiens-die-philippinen-und-vietnam-100~original?cb=1699870351148',
|
'thumbnail': 'https://www.3sat.de/assets/traumziele-suedostasiens-die-philippinen-und-vietnam-100~2400x1350?cb=1699870351148',
|
||||||
'episode': 'Traumziele Südostasiens (1/2): Die Philippinen und Vietnam',
|
'episode': 'Traumziele Südostasiens (1/2): Die Philippinen und Vietnam',
|
||||||
'episode_id': 'POS_cc7ff51c-98cf-4d12-b99d-f7a551de1c95',
|
'episode_id': 'POS_cc7ff51c-98cf-4d12-b99d-f7a551de1c95',
|
||||||
'timestamp': 1747920900,
|
'timestamp': 1738593000,
|
||||||
'upload_date': '20250522',
|
'upload_date': '20250203',
|
||||||
},
|
},
|
||||||
}, {
|
}, {
|
||||||
'url': 'https://www.3sat.de/film/ab-18/ab-18---mein-fremdes-ich-100.html',
|
# Same as https://www.zdf.de/dokumentation/ab-18/10-wochen-sommer-102.html
|
||||||
'md5': 'f92638413a11d759bdae95c9d8ec165c',
|
'url': 'https://www.3sat.de/film/ab-18/10-wochen-sommer-108.html',
|
||||||
|
'md5': '0aff3e7bc72c8813f5e0fae333316a1d',
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
'id': '221128_mein_fremdes_ich2_ab18',
|
'id': '141007_ab18_10wochensommer_film',
|
||||||
'ext': 'mp4',
|
'ext': 'mp4',
|
||||||
'title': 'Ab 18! - Mein fremdes Ich',
|
'title': 'Ab 18! - 10 Wochen Sommer',
|
||||||
'description': 'md5:cae0c0b27b7426d62ca0dda181738bf0',
|
'description': 'md5:8253f41dc99ce2c3ff892dac2d65fe26',
|
||||||
'duration': 2625.0,
|
'duration': 2660,
|
||||||
'thumbnail': 'https://www.3sat.de/assets/ab-18---mein-fremdes-ich-106~original?cb=1666081865812',
|
'timestamp': 1608604200,
|
||||||
'episode': 'Ab 18! - Mein fremdes Ich',
|
'upload_date': '20201222',
|
||||||
'episode_id': 'POS_6225d1ca-a0d5-45e3-870b-e783ee6c8a3f',
|
|
||||||
'timestamp': 1695081600,
|
|
||||||
'upload_date': '20230919',
|
|
||||||
},
|
},
|
||||||
|
'skip': '410 Gone',
|
||||||
}, {
|
}, {
|
||||||
'url': 'https://www.3sat.de/gesellschaft/37-grad-leben/aus-dem-leben-gerissen-102.html',
|
'url': 'https://www.3sat.de/gesellschaft/schweizweit/waidmannsheil-100.html',
|
||||||
'md5': 'a903eaf8d1fd635bd3317cd2ad87ec84',
|
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
'id': '250323_0903_sendung_sgl',
|
'id': '140913_sendung_schweizweit',
|
||||||
'ext': 'mp4',
|
'ext': 'mp4',
|
||||||
'title': 'Plötzlich ohne dich',
|
'title': 'Waidmannsheil',
|
||||||
'description': 'md5:380cc10659289dd91510ad8fa717c66b',
|
'description': 'md5:cce00ca1d70e21425e72c86a98a56817',
|
||||||
'duration': 1620.0,
|
'timestamp': 1410623100,
|
||||||
'thumbnail': 'https://www.3sat.de/assets/37-grad-leben-106~original?cb=1645537156810',
|
'upload_date': '20140913',
|
||||||
'episode': 'Plötzlich ohne dich',
|
|
||||||
'episode_id': 'POS_faa7a93c-c0f2-4d51-823f-ce2ac3ee191b',
|
|
||||||
'timestamp': 1743162540,
|
|
||||||
'upload_date': '20250328',
|
|
||||||
},
|
},
|
||||||
}, {
|
'params': {
|
||||||
# Video with chapters
|
'skip_download': True,
|
||||||
'url': 'https://www.3sat.de/kultur/buchmesse/dein-buch-das-beste-von-der-leipziger-buchmesse-2025-teil-1-100.html',
|
|
||||||
'md5': '6b95790ce52e75f0d050adcdd2711ee6',
|
|
||||||
'info_dict': {
|
|
||||||
'id': '250330_dein_buch1_bum',
|
|
||||||
'ext': 'mp4',
|
|
||||||
'title': 'dein buch - Das Beste von der Leipziger Buchmesse 2025 - Teil 1',
|
|
||||||
'description': 'md5:bae51bfc22f15563ce3acbf97d2e8844',
|
|
||||||
'duration': 5399.0,
|
|
||||||
'thumbnail': 'https://www.3sat.de/assets/buchmesse-kerkeling-100~original?cb=1743329640903',
|
|
||||||
'chapters': 'count:24',
|
|
||||||
'episode': 'dein buch - Das Beste von der Leipziger Buchmesse 2025 - Teil 1',
|
|
||||||
'episode_id': 'POS_1ef236cc-b390-401e-acd0-4fb4b04315fb',
|
|
||||||
'timestamp': 1743327000,
|
|
||||||
'upload_date': '20250330',
|
|
||||||
},
|
},
|
||||||
|
'skip': '404 Not Found',
|
||||||
}, {
|
}, {
|
||||||
# Same as https://www.zdf.de/filme/filme-sonstige/der-hauptmann-112.html
|
# Same as https://www.zdf.de/filme/filme-sonstige/der-hauptmann-112.html
|
||||||
'url': 'https://www.3sat.de/film/spielfilm/der-hauptmann-100.html',
|
'url': 'https://www.3sat.de/film/spielfilm/der-hauptmann-100.html',
|
||||||
@ -83,42 +58,11 @@ class DreiSatIE(ZDFBaseIE):
|
|||||||
|
|
||||||
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)
|
|
||||||
player = self._search_json(
|
|
||||||
r'data-zdfplayer-jsb=(["\'])', webpage, 'player JSON', video_id)
|
|
||||||
player_url = player['content']
|
|
||||||
api_token = f'Bearer {player["apiToken"]}'
|
|
||||||
|
|
||||||
content = self._call_api(player_url, video_id, 'video metadata', api_token)
|
webpage = self._download_webpage(url, video_id, fatal=False)
|
||||||
|
if webpage:
|
||||||
|
player = self._extract_player(webpage, url, fatal=False)
|
||||||
|
if player:
|
||||||
|
return self._extract_regular(url, player, video_id)
|
||||||
|
|
||||||
video_target = content['mainVideoContent']['http://zdf.de/rels/target']
|
return self._extract_mobile(video_id)
|
||||||
ptmd_path = traverse_obj(video_target, (
|
|
||||||
(('streams', 'default'), None),
|
|
||||||
('http://zdf.de/rels/streams/ptmd', 'http://zdf.de/rels/streams/ptmd-template'),
|
|
||||||
{str}, any, {require('ptmd path')}))
|
|
||||||
ptmd_url = self._expand_ptmd_template(player_url, ptmd_path)
|
|
||||||
aspect_ratio = self._parse_aspect_ratio(video_target.get('aspectRatio'))
|
|
||||||
info = self._extract_ptmd(ptmd_url, video_id, api_token, aspect_ratio)
|
|
||||||
|
|
||||||
return merge_dicts(info, {
|
|
||||||
**traverse_obj(content, {
|
|
||||||
'title': (('title', 'teaserHeadline'), {str}, any),
|
|
||||||
'episode': (('title', 'teaserHeadline'), {str}, any),
|
|
||||||
'description': (('leadParagraph', 'teasertext'), {str}, any),
|
|
||||||
'timestamp': ('editorialDate', {parse_iso8601}),
|
|
||||||
}),
|
|
||||||
**traverse_obj(video_target, {
|
|
||||||
'duration': ('duration', {int_or_none}),
|
|
||||||
'chapters': ('streamAnchorTag', {self._extract_chapters}),
|
|
||||||
}),
|
|
||||||
'thumbnails': self._extract_thumbnails(traverse_obj(content, ('teaserImageRef', 'layouts', {dict}))),
|
|
||||||
**traverse_obj(content, ('programmeItem', 0, 'http://zdf.de/rels/target', {
|
|
||||||
'series_id': ('http://zdf.de/rels/cmdm/series', 'seriesUuid', {str}),
|
|
||||||
'series': ('http://zdf.de/rels/cmdm/series', 'seriesTitle', {str}),
|
|
||||||
'season': ('http://zdf.de/rels/cmdm/season', 'seasonTitle', {str}),
|
|
||||||
'season_number': ('http://zdf.de/rels/cmdm/season', 'seasonNumber', {int_or_none}),
|
|
||||||
'season_id': ('http://zdf.de/rels/cmdm/season', 'seasonUuid', {str}),
|
|
||||||
'episode_number': ('episodeNumber', {int_or_none}),
|
|
||||||
'episode_id': ('contentId', {str}),
|
|
||||||
})),
|
|
||||||
})
|
|
||||||
|
@ -1,37 +1,28 @@
|
|||||||
import inspect
|
import contextlib
|
||||||
import os
|
import os
|
||||||
|
|
||||||
from ..globals import LAZY_EXTRACTORS
|
from ..plugins import load_plugins
|
||||||
from ..globals import extractors as _extractors_context
|
|
||||||
|
|
||||||
_CLASS_LOOKUP = None
|
# NB: Must be before other imports so that plugins can be correctly injected
|
||||||
if os.environ.get('YTDLP_NO_LAZY_EXTRACTORS'):
|
_PLUGIN_CLASSES = load_plugins('extractor', 'IE')
|
||||||
LAZY_EXTRACTORS.value = False
|
|
||||||
else:
|
|
||||||
try:
|
|
||||||
from .lazy_extractors import _CLASS_LOOKUP
|
|
||||||
LAZY_EXTRACTORS.value = True
|
|
||||||
except ImportError:
|
|
||||||
LAZY_EXTRACTORS.value = None
|
|
||||||
|
|
||||||
if not _CLASS_LOOKUP:
|
_LAZY_LOADER = False
|
||||||
from . import _extractors
|
if not os.environ.get('YTDLP_NO_LAZY_EXTRACTORS'):
|
||||||
|
with contextlib.suppress(ImportError):
|
||||||
|
from .lazy_extractors import * # noqa: F403
|
||||||
|
from .lazy_extractors import _ALL_CLASSES
|
||||||
|
_LAZY_LOADER = True
|
||||||
|
|
||||||
_CLASS_LOOKUP = {
|
if not _LAZY_LOADER:
|
||||||
name: value
|
from ._extractors import * # noqa: F403
|
||||||
for name, value in inspect.getmembers(_extractors)
|
_ALL_CLASSES = [ # noqa: F811
|
||||||
|
klass
|
||||||
|
for name, klass in globals().items()
|
||||||
if name.endswith('IE') and name != 'GenericIE'
|
if name.endswith('IE') and name != 'GenericIE'
|
||||||
}
|
]
|
||||||
_CLASS_LOOKUP['GenericIE'] = _extractors.GenericIE
|
_ALL_CLASSES.append(GenericIE) # noqa: F405
|
||||||
|
|
||||||
# We want to append to the main lookup
|
globals().update(_PLUGIN_CLASSES)
|
||||||
_current = _extractors_context.value
|
_ALL_CLASSES[:0] = _PLUGIN_CLASSES.values()
|
||||||
for name, ie in _CLASS_LOOKUP.items():
|
|
||||||
_current.setdefault(name, ie)
|
|
||||||
|
|
||||||
|
from .common import _PLUGIN_OVERRIDES # noqa: F401
|
||||||
def __getattr__(name):
|
|
||||||
value = _CLASS_LOOKUP.get(name)
|
|
||||||
if not value:
|
|
||||||
raise AttributeError(f'module {__name__} has no attribute {name}')
|
|
||||||
return value
|
|
||||||
|
@ -1,87 +0,0 @@
|
|||||||
import urllib.parse
|
|
||||||
|
|
||||||
from .common import InfoExtractor
|
|
||||||
from ..networking.exceptions import HTTPError
|
|
||||||
from ..utils import (
|
|
||||||
ExtractorError,
|
|
||||||
float_or_none,
|
|
||||||
url_or_none,
|
|
||||||
)
|
|
||||||
from ..utils.traversal import traverse_obj
|
|
||||||
|
|
||||||
|
|
||||||
class FrancaisFacileIE(InfoExtractor):
|
|
||||||
_VALID_URL = r'https?://francaisfacile\.rfi\.fr/[a-z]{2}/(?:actualit%C3%A9|podcasts/[^/#?]+)/(?P<id>[^/#?]+)'
|
|
||||||
_TESTS = [{
|
|
||||||
'url': 'https://francaisfacile.rfi.fr/fr/actualit%C3%A9/20250305-r%C3%A9concilier-les-jeunes-avec-la-lecture-gr%C3%A2ce-aux-r%C3%A9seaux-sociaux',
|
|
||||||
'md5': '4f33674cb205744345cc835991100afa',
|
|
||||||
'info_dict': {
|
|
||||||
'id': 'WBMZ58952-FLE-FR-20250305',
|
|
||||||
'display_id': '20250305-réconcilier-les-jeunes-avec-la-lecture-grâce-aux-réseaux-sociaux',
|
|
||||||
'title': 'Réconcilier les jeunes avec la lecture grâce aux réseaux sociaux',
|
|
||||||
'url': 'https://aod-fle.akamaized.net/fle/sounds/fr/2025/03/05/6b6af52a-f9ba-11ef-a1f8-005056a97652.mp3',
|
|
||||||
'ext': 'mp3',
|
|
||||||
'description': 'md5:b903c63d8585bd59e8cc4d5f80c4272d',
|
|
||||||
'duration': 103.15,
|
|
||||||
'timestamp': 1741177984,
|
|
||||||
'upload_date': '20250305',
|
|
||||||
},
|
|
||||||
}, {
|
|
||||||
'url': 'https://francaisfacile.rfi.fr/fr/actualit%C3%A9/20250307-argentine-le-sac-d-un-alpiniste-retrouv%C3%A9-40-ans-apr%C3%A8s-sa-mort',
|
|
||||||
'md5': 'b8c3a63652d4ae8e8092dda5700c1cd9',
|
|
||||||
'info_dict': {
|
|
||||||
'id': 'WBMZ59102-FLE-FR-20250307',
|
|
||||||
'display_id': '20250307-argentine-le-sac-d-un-alpiniste-retrouvé-40-ans-après-sa-mort',
|
|
||||||
'title': 'Argentine: le sac d\'un alpiniste retrouvé 40 ans après sa mort',
|
|
||||||
'url': 'https://aod-fle.akamaized.net/fle/sounds/fr/2025/03/07/8edf4082-fb46-11ef-8a37-005056bf762b.mp3',
|
|
||||||
'ext': 'mp3',
|
|
||||||
'description': 'md5:7fd088fbdf4a943bb68cf82462160dca',
|
|
||||||
'duration': 117.74,
|
|
||||||
'timestamp': 1741352789,
|
|
||||||
'upload_date': '20250307',
|
|
||||||
},
|
|
||||||
}, {
|
|
||||||
'url': 'https://francaisfacile.rfi.fr/fr/podcasts/un-mot-une-histoire/20250317-le-mot-de-david-foenkinos-peut-%C3%AAtre',
|
|
||||||
'md5': 'db83c2cc2589b4c24571c6b6cf14f5f1',
|
|
||||||
'info_dict': {
|
|
||||||
'id': 'WBMZ59441-FLE-FR-20250317',
|
|
||||||
'display_id': '20250317-le-mot-de-david-foenkinos-peut-être',
|
|
||||||
'title': 'Le mot de David Foenkinos: «peut-être» - Un mot, une histoire',
|
|
||||||
'url': 'https://aod-fle.akamaized.net/fle/sounds/fr/2025/03/17/4ca6cbbe-0315-11f0-a85b-005056a97652.mp3',
|
|
||||||
'ext': 'mp3',
|
|
||||||
'description': 'md5:3fe35fae035803df696bfa7af2496e49',
|
|
||||||
'duration': 198.96,
|
|
||||||
'timestamp': 1742210897,
|
|
||||||
'upload_date': '20250317',
|
|
||||||
},
|
|
||||||
}]
|
|
||||||
|
|
||||||
def _real_extract(self, url):
|
|
||||||
display_id = urllib.parse.unquote(self._match_id(url))
|
|
||||||
|
|
||||||
try: # yt-dlp's default user-agents are too old and blocked by the site
|
|
||||||
webpage = self._download_webpage(url, display_id, headers={
|
|
||||||
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; rv:136.0) Gecko/20100101 Firefox/136.0',
|
|
||||||
})
|
|
||||||
except ExtractorError as e:
|
|
||||||
if not isinstance(e.cause, HTTPError) or e.cause.status != 403:
|
|
||||||
raise
|
|
||||||
# Retry with impersonation if hardcoded UA is insufficient
|
|
||||||
webpage = self._download_webpage(url, display_id, impersonate=True)
|
|
||||||
|
|
||||||
data = self._search_json(
|
|
||||||
r'<script[^>]+\bdata-media-id=[^>]+\btype="application/json"[^>]*>',
|
|
||||||
webpage, 'audio data', display_id)
|
|
||||||
|
|
||||||
return {
|
|
||||||
'id': data['mediaId'],
|
|
||||||
'display_id': display_id,
|
|
||||||
'vcodec': 'none',
|
|
||||||
'title': self._html_extract_title(webpage),
|
|
||||||
**self._search_json_ld(webpage, display_id, fatal=False),
|
|
||||||
**traverse_obj(data, {
|
|
||||||
'title': ('title', {str}),
|
|
||||||
'url': ('sources', ..., 'url', {url_or_none}, any),
|
|
||||||
'duration': ('sources', ..., 'duration', {float_or_none}, any),
|
|
||||||
}),
|
|
||||||
}
|
|
@ -37,7 +37,6 @@ from ..utils import (
|
|||||||
unescapeHTML,
|
unescapeHTML,
|
||||||
unified_timestamp,
|
unified_timestamp,
|
||||||
unsmuggle_url,
|
unsmuggle_url,
|
||||||
update_url,
|
|
||||||
update_url_query,
|
update_url_query,
|
||||||
url_or_none,
|
url_or_none,
|
||||||
urlhandle_detect_ext,
|
urlhandle_detect_ext,
|
||||||
@ -2214,21 +2213,10 @@ class GenericIE(InfoExtractor):
|
|||||||
if is_live is not None:
|
if is_live is not None:
|
||||||
info['live_status'] = 'not_live' if is_live == 'false' else 'is_live'
|
info['live_status'] = 'not_live' if is_live == 'false' else 'is_live'
|
||||||
return
|
return
|
||||||
headers = m3u8_format.get('http_headers') or info.get('http_headers') or {}
|
headers = m3u8_format.get('http_headers') or info.get('http_headers')
|
||||||
display_id = info.get('id')
|
duration = self._extract_m3u8_vod_duration(
|
||||||
urlh = self._request_webpage(
|
m3u8_format['url'], info.get('id'), note='Checking m3u8 live status',
|
||||||
m3u8_format['url'], display_id, 'Checking m3u8 live status', errnote=False,
|
errnote='Failed to download m3u8 media playlist', headers=headers)
|
||||||
headers={**headers, 'Accept-Encoding': 'identity'}, fatal=False)
|
|
||||||
if urlh is False:
|
|
||||||
return
|
|
||||||
first_bytes = urlh.read(512)
|
|
||||||
if not first_bytes.startswith(b'#EXTM3U'):
|
|
||||||
return
|
|
||||||
m3u8_doc = self._webpage_read_content(
|
|
||||||
urlh, urlh.url, display_id, prefix=first_bytes, fatal=False, errnote=False)
|
|
||||||
if not m3u8_doc:
|
|
||||||
return
|
|
||||||
duration = self._parse_m3u8_vod_duration(m3u8_doc, display_id)
|
|
||||||
if not duration:
|
if not duration:
|
||||||
info['live_status'] = 'is_live'
|
info['live_status'] = 'is_live'
|
||||||
info['duration'] = info.get('duration') or duration
|
info['duration'] = info.get('duration') or duration
|
||||||
@ -2538,13 +2526,12 @@ class GenericIE(InfoExtractor):
|
|||||||
return self.playlist_result(
|
return self.playlist_result(
|
||||||
self._parse_xspf(
|
self._parse_xspf(
|
||||||
doc, video_id, xspf_url=url,
|
doc, video_id, xspf_url=url,
|
||||||
xspf_base_url=new_url),
|
xspf_base_url=full_response.url),
|
||||||
video_id)
|
video_id)
|
||||||
elif re.match(r'(?i)^(?:{[^}]+})?MPD$', doc.tag):
|
elif re.match(r'(?i)^(?:{[^}]+})?MPD$', doc.tag):
|
||||||
info_dict['formats'], info_dict['subtitles'] = self._parse_mpd_formats_and_subtitles(
|
info_dict['formats'], info_dict['subtitles'] = self._parse_mpd_formats_and_subtitles(
|
||||||
doc,
|
doc,
|
||||||
# Do not use yt_dlp.utils.base_url here since it will raise on file:// URLs
|
mpd_base_url=full_response.url.rpartition('/')[0],
|
||||||
mpd_base_url=update_url(new_url, query=None, fragment=None).rpartition('/')[0],
|
|
||||||
mpd_url=url)
|
mpd_url=url)
|
||||||
info_dict['live_status'] = 'is_live' if doc.get('type') == 'dynamic' else None
|
info_dict['live_status'] = 'is_live' if doc.get('type') == 'dynamic' else None
|
||||||
self._extra_manifest_info(info_dict, url)
|
self._extra_manifest_info(info_dict, url)
|
||||||
|
@ -8,7 +8,7 @@ from ..utils.traversal import traverse_obj
|
|||||||
|
|
||||||
|
|
||||||
class GetCourseRuPlayerIE(InfoExtractor):
|
class GetCourseRuPlayerIE(InfoExtractor):
|
||||||
_VALID_URL = r'https?://(?:player02\.getcourse\.ru|cf-api-2\.vhcdn\.com)/sign-player/?\?(?:[^#]+&)?json=[^#&]+'
|
_VALID_URL = r'https?://player02\.getcourse\.ru/sign-player/?\?(?:[^#]+&)?json=[^#&]+'
|
||||||
_EMBED_REGEX = [rf'<iframe[^>]+\bsrc=[\'"](?P<url>{_VALID_URL}[^\'"]*)']
|
_EMBED_REGEX = [rf'<iframe[^>]+\bsrc=[\'"](?P<url>{_VALID_URL}[^\'"]*)']
|
||||||
_TESTS = [{
|
_TESTS = [{
|
||||||
'url': 'http://player02.getcourse.ru/sign-player/?json=eyJ2aWRlb19oYXNoIjoiMTkwYmRmOTNmMWIyOTczNTMwOTg1M2E3YTE5ZTI0YjMiLCJ1c2VyX2lkIjozNTk1MjUxODMsInN1Yl9sb2dpbl91c2VyX2lkIjpudWxsLCJsZXNzb25faWQiOm51bGwsImlwIjoiNDYuMTQyLjE4Mi4yNDciLCJnY19ob3N0IjoiYWNhZGVteW1lbC5vbmxpbmUiLCJ0aW1lIjoxNzA1NDQ5NjQyLCJwYXlsb2FkIjoidV8zNTk1MjUxODMiLCJ1aV9sYW5ndWFnZSI6InJ1IiwiaXNfaGF2ZV9jdXN0b21fc3R5bGUiOnRydWV9&s=354ad2c993d95d5ac629e3133d6cefea&vh-static-feature=zigzag',
|
'url': 'http://player02.getcourse.ru/sign-player/?json=eyJ2aWRlb19oYXNoIjoiMTkwYmRmOTNmMWIyOTczNTMwOTg1M2E3YTE5ZTI0YjMiLCJ1c2VyX2lkIjozNTk1MjUxODMsInN1Yl9sb2dpbl91c2VyX2lkIjpudWxsLCJsZXNzb25faWQiOm51bGwsImlwIjoiNDYuMTQyLjE4Mi4yNDciLCJnY19ob3N0IjoiYWNhZGVteW1lbC5vbmxpbmUiLCJ0aW1lIjoxNzA1NDQ5NjQyLCJwYXlsb2FkIjoidV8zNTk1MjUxODMiLCJ1aV9sYW5ndWFnZSI6InJ1IiwiaXNfaGF2ZV9jdXN0b21fc3R5bGUiOnRydWV9&s=354ad2c993d95d5ac629e3133d6cefea&vh-static-feature=zigzag',
|
||||||
@ -20,16 +20,6 @@ class GetCourseRuPlayerIE(InfoExtractor):
|
|||||||
'duration': 1693,
|
'duration': 1693,
|
||||||
},
|
},
|
||||||
'skip': 'JWT expired',
|
'skip': 'JWT expired',
|
||||||
}, {
|
|
||||||
'url': 'https://cf-api-2.vhcdn.com/sign-player/?json=example',
|
|
||||||
'info_dict': {
|
|
||||||
'id': '435735291',
|
|
||||||
'title': '8afd7c489952108e00f019590f3711f3',
|
|
||||||
'ext': 'mp4',
|
|
||||||
'thumbnail': 'https://preview-htz.vhcdn.com/preview/8afd7c489952108e00f019590f3711f3/preview.jpg?version=1682170973&host=vh-72',
|
|
||||||
'duration': 777,
|
|
||||||
},
|
|
||||||
'skip': 'JWT expired',
|
|
||||||
}]
|
}]
|
||||||
|
|
||||||
def _real_extract(self, url):
|
def _real_extract(self, url):
|
||||||
@ -178,7 +168,7 @@ class GetCourseRuIE(InfoExtractor):
|
|||||||
|
|
||||||
playlist_id = self._search_regex(
|
playlist_id = self._search_regex(
|
||||||
r'window\.(?:lessonId|gcsObjectId)\s*=\s*(\d+)', webpage, 'playlist id', default=display_id)
|
r'window\.(?:lessonId|gcsObjectId)\s*=\s*(\d+)', webpage, 'playlist id', default=display_id)
|
||||||
title = self._og_search_title(webpage, default=None) or self._html_extract_title(webpage)
|
title = self._og_search_title(webpage) or self._html_extract_title(webpage)
|
||||||
|
|
||||||
return self.playlist_from_matches(
|
return self.playlist_from_matches(
|
||||||
re.findall(GetCourseRuPlayerIE._EMBED_REGEX[0], webpage),
|
re.findall(GetCourseRuPlayerIE._EMBED_REGEX[0], webpage),
|
||||||
|
19
yt_dlp/extractor/gigya.py
Normal file
19
yt_dlp/extractor/gigya.py
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
from .common import InfoExtractor
|
||||||
|
from ..utils import (
|
||||||
|
ExtractorError,
|
||||||
|
urlencode_postdata,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class GigyaBaseIE(InfoExtractor):
|
||||||
|
def _gigya_login(self, auth_data):
|
||||||
|
auth_info = self._download_json(
|
||||||
|
'https://accounts.eu1.gigya.com/accounts.login', None,
|
||||||
|
note='Logging in', errnote='Unable to log in',
|
||||||
|
data=urlencode_postdata(auth_data))
|
||||||
|
|
||||||
|
error_message = auth_info.get('errorDetails') or auth_info.get('errorMessage')
|
||||||
|
if error_message:
|
||||||
|
raise ExtractorError(
|
||||||
|
f'Unable to login: {error_message}', expected=True)
|
||||||
|
return auth_info
|
@ -69,13 +69,8 @@ class GloboIE(InfoExtractor):
|
|||||||
'info_dict': {
|
'info_dict': {
|
||||||
'id': '8013907',
|
'id': '8013907',
|
||||||
'ext': 'mp4',
|
'ext': 'mp4',
|
||||||
'title': 'Capítulo de 14/08/1989',
|
'title': 'Capítulo de 14⧸08⧸1989',
|
||||||
'episode': 'Episode 1',
|
|
||||||
'episode_number': 1,
|
'episode_number': 1,
|
||||||
'uploader': 'Tieta',
|
|
||||||
'uploader_id': '11895',
|
|
||||||
'duration': 2858.389,
|
|
||||||
'subtitles': 'count:1',
|
|
||||||
},
|
},
|
||||||
'params': {
|
'params': {
|
||||||
'skip_download': True,
|
'skip_download': True,
|
||||||
@ -87,12 +82,7 @@ class GloboIE(InfoExtractor):
|
|||||||
'id': '12824146',
|
'id': '12824146',
|
||||||
'ext': 'mp4',
|
'ext': 'mp4',
|
||||||
'title': 'Acordo de damas',
|
'title': 'Acordo de damas',
|
||||||
'episode': 'Episode 1',
|
|
||||||
'episode_number': 1,
|
'episode_number': 1,
|
||||||
'uploader': 'Rensga Hits!',
|
|
||||||
'uploader_id': '20481',
|
|
||||||
'duration': 1953.994,
|
|
||||||
'season': 'Season 2',
|
|
||||||
'season_number': 2,
|
'season_number': 2,
|
||||||
},
|
},
|
||||||
'params': {
|
'params': {
|
||||||
@ -146,10 +136,9 @@ class GloboIE(InfoExtractor):
|
|||||||
else:
|
else:
|
||||||
formats, subtitles = self._extract_m3u8_formats_and_subtitles(
|
formats, subtitles = self._extract_m3u8_formats_and_subtitles(
|
||||||
main_source['url'], video_id, 'mp4', m3u8_id='hls')
|
main_source['url'], video_id, 'mp4', m3u8_id='hls')
|
||||||
|
self._merge_subtitles(traverse_obj(main_source, ('text', ..., {
|
||||||
self._merge_subtitles(traverse_obj(main_source, ('text', ..., ('caption', 'subtitle'), {
|
'url': ('subtitle', 'srt', 'url', {url_or_none}),
|
||||||
'url': ('srt', 'url', {url_or_none}),
|
}, all, {subs_list_to_dict(lang='en')})), target=subtitles)
|
||||||
}, all, {subs_list_to_dict(lang='pt-BR')})), target=subtitles)
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
'id': video_id,
|
'id': video_id,
|
||||||
|
@ -6,7 +6,7 @@ from ..utils import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class HSEShowBaseIE(InfoExtractor):
|
class HSEShowBaseInfoExtractor(InfoExtractor):
|
||||||
_GEO_COUNTRIES = ['DE']
|
_GEO_COUNTRIES = ['DE']
|
||||||
|
|
||||||
def _extract_redux_data(self, url, video_id):
|
def _extract_redux_data(self, url, video_id):
|
||||||
@ -28,7 +28,7 @@ class HSEShowBaseIE(InfoExtractor):
|
|||||||
return formats, subtitles
|
return formats, subtitles
|
||||||
|
|
||||||
|
|
||||||
class HSEShowIE(HSEShowBaseIE):
|
class HSEShowIE(HSEShowBaseInfoExtractor):
|
||||||
_VALID_URL = r'https?://(?:www\.)?hse\.de/dpl/c/tv-shows/(?P<id>[0-9]+)'
|
_VALID_URL = r'https?://(?:www\.)?hse\.de/dpl/c/tv-shows/(?P<id>[0-9]+)'
|
||||||
_TESTS = [{
|
_TESTS = [{
|
||||||
'url': 'https://www.hse.de/dpl/c/tv-shows/505350',
|
'url': 'https://www.hse.de/dpl/c/tv-shows/505350',
|
||||||
@ -64,7 +64,7 @@ class HSEShowIE(HSEShowBaseIE):
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class HSEProductIE(HSEShowBaseIE):
|
class HSEProductIE(HSEShowBaseInfoExtractor):
|
||||||
_VALID_URL = r'https?://(?:www\.)?hse\.de/dpl/p/product/(?P<id>[0-9]+)'
|
_VALID_URL = r'https?://(?:www\.)?hse\.de/dpl/p/product/(?P<id>[0-9]+)'
|
||||||
_TESTS = [{
|
_TESTS = [{
|
||||||
'url': 'https://www.hse.de/dpl/p/product/408630',
|
'url': 'https://www.hse.de/dpl/p/product/408630',
|
||||||
|
@ -1,13 +1,5 @@
|
|||||||
|
|
||||||
from .common import InfoExtractor
|
from .common import InfoExtractor
|
||||||
from ..utils import (
|
from ..utils import ExtractorError, str_or_none, traverse_obj, unified_strdate
|
||||||
ExtractorError,
|
|
||||||
int_or_none,
|
|
||||||
str_or_none,
|
|
||||||
traverse_obj,
|
|
||||||
unified_strdate,
|
|
||||||
url_or_none,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class IchinanaLiveIE(InfoExtractor):
|
class IchinanaLiveIE(InfoExtractor):
|
||||||
@ -165,51 +157,3 @@ class IchinanaLiveClipIE(InfoExtractor):
|
|||||||
'description': view_data.get('caption'),
|
'description': view_data.get('caption'),
|
||||||
'upload_date': unified_strdate(str_or_none(view_data.get('createdAt'))),
|
'upload_date': unified_strdate(str_or_none(view_data.get('createdAt'))),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class IchinanaLiveVODIE(InfoExtractor):
|
|
||||||
IE_NAME = '17live:vod'
|
|
||||||
_VALID_URL = r'https?://(?:www\.)?17\.live/ja/vod/[^/?#]+/(?P<id>[^/?#]+)'
|
|
||||||
_TESTS = [{
|
|
||||||
'url': 'https://17.live/ja/vod/27323042/2cf84520-e65e-4b22-891e-1d3a00b0f068',
|
|
||||||
'md5': '3299b930d7457b069639486998a89580',
|
|
||||||
'info_dict': {
|
|
||||||
'id': '2cf84520-e65e-4b22-891e-1d3a00b0f068',
|
|
||||||
'ext': 'mp4',
|
|
||||||
'title': 'md5:b5f8cbf497d54cc6a60eb3b480182f01',
|
|
||||||
'uploader': 'md5:29fb12122ab94b5a8495586e7c3085a5',
|
|
||||||
'uploader_id': '27323042',
|
|
||||||
'channel': '🌟オールナイトニッポン アーカイブ🌟',
|
|
||||||
'channel_id': '2b4f85f1-d61e-429d-a901-68d32bdd8645',
|
|
||||||
'like_count': int,
|
|
||||||
'view_count': int,
|
|
||||||
'thumbnail': r're:https?://.+/.+\.(?:jpe?g|png)',
|
|
||||||
'duration': 549,
|
|
||||||
'description': 'md5:116f326579700f00eaaf5581aae1192e',
|
|
||||||
'timestamp': 1741058645,
|
|
||||||
'upload_date': '20250304',
|
|
||||||
},
|
|
||||||
}, {
|
|
||||||
'url': 'https://17.live/ja/vod/27323042/0de11bac-9bea-40b8-9eab-0239a7d88079',
|
|
||||||
'only_matching': True,
|
|
||||||
}]
|
|
||||||
|
|
||||||
def _real_extract(self, url):
|
|
||||||
video_id = self._match_id(url)
|
|
||||||
json_data = self._download_json(f'https://wap-api.17app.co/api/v1/vods/{video_id}', video_id)
|
|
||||||
|
|
||||||
return traverse_obj(json_data, {
|
|
||||||
'id': ('vodID', {str}),
|
|
||||||
'title': ('title', {str}),
|
|
||||||
'formats': ('vodURL', {lambda x: self._extract_m3u8_formats(x, video_id)}),
|
|
||||||
'uploader': ('userInfo', 'displayName', {str}),
|
|
||||||
'uploader_id': ('userInfo', 'roomID', {int}, {str_or_none}),
|
|
||||||
'channel': ('userInfo', 'name', {str}),
|
|
||||||
'channel_id': ('userInfo', 'userID', {str}),
|
|
||||||
'like_count': ('likeCount', {int_or_none}),
|
|
||||||
'view_count': ('viewCount', {int_or_none}),
|
|
||||||
'thumbnail': ('imageURL', {url_or_none}),
|
|
||||||
'duration': ('duration', {int_or_none}),
|
|
||||||
'description': ('description', {str}),
|
|
||||||
'timestamp': ('createdAt', {int_or_none}),
|
|
||||||
})
|
|
||||||
|
@ -2,12 +2,12 @@ import hashlib
|
|||||||
import itertools
|
import itertools
|
||||||
import json
|
import json
|
||||||
import re
|
import re
|
||||||
|
import time
|
||||||
|
|
||||||
from .common import InfoExtractor
|
from .common import InfoExtractor
|
||||||
from ..networking.exceptions import HTTPError
|
from ..networking.exceptions import HTTPError
|
||||||
from ..utils import (
|
from ..utils import (
|
||||||
ExtractorError,
|
ExtractorError,
|
||||||
bug_reports_message,
|
|
||||||
decode_base_n,
|
decode_base_n,
|
||||||
encode_base_n,
|
encode_base_n,
|
||||||
filter_dict,
|
filter_dict,
|
||||||
@ -15,12 +15,12 @@ from ..utils import (
|
|||||||
format_field,
|
format_field,
|
||||||
get_element_by_attribute,
|
get_element_by_attribute,
|
||||||
int_or_none,
|
int_or_none,
|
||||||
join_nonempty,
|
|
||||||
lowercase_escape,
|
lowercase_escape,
|
||||||
str_or_none,
|
str_or_none,
|
||||||
str_to_int,
|
str_to_int,
|
||||||
traverse_obj,
|
traverse_obj,
|
||||||
url_or_none,
|
url_or_none,
|
||||||
|
urlencode_postdata,
|
||||||
)
|
)
|
||||||
|
|
||||||
_ENCODING_CHARS = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_'
|
_ENCODING_CHARS = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_'
|
||||||
@ -28,31 +28,64 @@ _ENCODING_CHARS = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz012345678
|
|||||||
|
|
||||||
def _pk_to_id(media_id):
|
def _pk_to_id(media_id):
|
||||||
"""Source: https://stackoverflow.com/questions/24437823/getting-instagram-post-url-from-media-id"""
|
"""Source: https://stackoverflow.com/questions/24437823/getting-instagram-post-url-from-media-id"""
|
||||||
pk = int(str(media_id).split('_')[0])
|
return encode_base_n(int(media_id.split('_')[0]), table=_ENCODING_CHARS)
|
||||||
return encode_base_n(pk, table=_ENCODING_CHARS)
|
|
||||||
|
|
||||||
|
|
||||||
def _id_to_pk(shortcode):
|
def _id_to_pk(shortcode):
|
||||||
"""Convert a shortcode to a numeric value"""
|
"""Covert a shortcode to a numeric value"""
|
||||||
if len(shortcode) > 28:
|
return decode_base_n(shortcode[:11], table=_ENCODING_CHARS)
|
||||||
shortcode = shortcode[:-28]
|
|
||||||
return decode_base_n(shortcode, table=_ENCODING_CHARS)
|
|
||||||
|
|
||||||
|
|
||||||
class InstagramBaseIE(InfoExtractor):
|
class InstagramBaseIE(InfoExtractor):
|
||||||
|
_NETRC_MACHINE = 'instagram'
|
||||||
|
_IS_LOGGED_IN = False
|
||||||
|
|
||||||
_API_BASE_URL = 'https://i.instagram.com/api/v1'
|
_API_BASE_URL = 'https://i.instagram.com/api/v1'
|
||||||
_LOGIN_URL = 'https://www.instagram.com/accounts/login'
|
_LOGIN_URL = 'https://www.instagram.com/accounts/login'
|
||||||
|
_API_HEADERS = {
|
||||||
@property
|
'X-IG-App-ID': '936619743392459',
|
||||||
def _api_headers(self):
|
|
||||||
return {
|
|
||||||
'X-IG-App-ID': self._configuration_arg('app_id', ['936619743392459'], ie_key=InstagramIE)[0],
|
|
||||||
'X-ASBD-ID': '198387',
|
'X-ASBD-ID': '198387',
|
||||||
'X-IG-WWW-Claim': '0',
|
'X-IG-WWW-Claim': '0',
|
||||||
'Origin': 'https://www.instagram.com',
|
'Origin': 'https://www.instagram.com',
|
||||||
'Accept': '*/*',
|
'Accept': '*/*',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def _perform_login(self, username, password):
|
||||||
|
if self._IS_LOGGED_IN:
|
||||||
|
return
|
||||||
|
|
||||||
|
login_webpage = self._download_webpage(
|
||||||
|
self._LOGIN_URL, None, note='Downloading login webpage', errnote='Failed to download login webpage')
|
||||||
|
|
||||||
|
shared_data = self._parse_json(self._search_regex(
|
||||||
|
r'window\._sharedData\s*=\s*({.+?});', login_webpage, 'shared data', default='{}'), None)
|
||||||
|
|
||||||
|
login = self._download_json(
|
||||||
|
f'{self._LOGIN_URL}/ajax/', None, note='Logging in', headers={
|
||||||
|
**self._API_HEADERS,
|
||||||
|
'X-Requested-With': 'XMLHttpRequest',
|
||||||
|
'X-CSRFToken': shared_data['config']['csrf_token'],
|
||||||
|
'X-Instagram-AJAX': shared_data['rollout_hash'],
|
||||||
|
'Referer': 'https://www.instagram.com/',
|
||||||
|
}, data=urlencode_postdata({
|
||||||
|
'enc_password': f'#PWD_INSTAGRAM_BROWSER:0:{int(time.time())}:{password}',
|
||||||
|
'username': username,
|
||||||
|
'queryParams': '{}',
|
||||||
|
'optIntoOneTap': 'false',
|
||||||
|
'stopDeletionNonce': '',
|
||||||
|
'trustedDeviceRecords': '{}',
|
||||||
|
}))
|
||||||
|
|
||||||
|
if not login.get('authenticated'):
|
||||||
|
if login.get('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')
|
||||||
|
InstagramBaseIE._IS_LOGGED_IN = True
|
||||||
|
|
||||||
def _get_count(self, media, kind, *keys):
|
def _get_count(self, media, kind, *keys):
|
||||||
return traverse_obj(
|
return traverse_obj(
|
||||||
media, (kind, 'count'), *((f'edge_media_{key}', 'count') for key in keys),
|
media, (kind, 'count'), *((f'edge_media_{key}', 'count') for key in keys),
|
||||||
@ -176,7 +209,7 @@ class InstagramBaseIE(InfoExtractor):
|
|||||||
def _get_comments(self, video_id):
|
def _get_comments(self, video_id):
|
||||||
comments_info = self._download_json(
|
comments_info = self._download_json(
|
||||||
f'{self._API_BASE_URL}/media/{_id_to_pk(video_id)}/comments/?can_support_threading=true&permalink_enabled=false', video_id,
|
f'{self._API_BASE_URL}/media/{_id_to_pk(video_id)}/comments/?can_support_threading=true&permalink_enabled=false', video_id,
|
||||||
fatal=False, errnote='Comments extraction failed', note='Downloading comments info', headers=self._api_headers) or {}
|
fatal=False, errnote='Comments extraction failed', note='Downloading comments info', headers=self._API_HEADERS) or {}
|
||||||
|
|
||||||
comment_data = traverse_obj(comments_info, ('edge_media_to_parent_comment', 'edges'), 'comments')
|
comment_data = traverse_obj(comments_info, ('edge_media_to_parent_comment', 'edges'), 'comments')
|
||||||
for comment_dict in comment_data or []:
|
for comment_dict in comment_data or []:
|
||||||
@ -369,14 +402,14 @@ class InstagramIE(InstagramBaseIE):
|
|||||||
info = traverse_obj(self._download_json(
|
info = traverse_obj(self._download_json(
|
||||||
f'{self._API_BASE_URL}/media/{_id_to_pk(video_id)}/info/', video_id,
|
f'{self._API_BASE_URL}/media/{_id_to_pk(video_id)}/info/', video_id,
|
||||||
fatal=False, errnote='Video info extraction failed',
|
fatal=False, errnote='Video info extraction failed',
|
||||||
note='Downloading video info', headers=self._api_headers), ('items', 0))
|
note='Downloading video info', headers=self._API_HEADERS), ('items', 0))
|
||||||
if info:
|
if info:
|
||||||
media.update(info)
|
media.update(info)
|
||||||
return self._extract_product(media)
|
return self._extract_product(media)
|
||||||
|
|
||||||
api_check = self._download_json(
|
api_check = self._download_json(
|
||||||
f'{self._API_BASE_URL}/web/get_ruling_for_content/?content_type=MEDIA&target_id={_id_to_pk(video_id)}',
|
f'{self._API_BASE_URL}/web/get_ruling_for_content/?content_type=MEDIA&target_id={_id_to_pk(video_id)}',
|
||||||
video_id, headers=self._api_headers, fatal=False, note='Setting up session', errnote=False) or {}
|
video_id, headers=self._API_HEADERS, fatal=False, note='Setting up session', errnote=False) or {}
|
||||||
csrf_token = self._get_cookies('https://www.instagram.com').get('csrftoken')
|
csrf_token = self._get_cookies('https://www.instagram.com').get('csrftoken')
|
||||||
|
|
||||||
if not csrf_token:
|
if not csrf_token:
|
||||||
@ -396,7 +429,7 @@ class InstagramIE(InstagramBaseIE):
|
|||||||
general_info = self._download_json(
|
general_info = self._download_json(
|
||||||
'https://www.instagram.com/graphql/query/', video_id, fatal=False, errnote=False,
|
'https://www.instagram.com/graphql/query/', video_id, fatal=False, errnote=False,
|
||||||
headers={
|
headers={
|
||||||
**self._api_headers,
|
**self._API_HEADERS,
|
||||||
'X-CSRFToken': csrf_token or '',
|
'X-CSRFToken': csrf_token or '',
|
||||||
'X-Requested-With': 'XMLHttpRequest',
|
'X-Requested-With': 'XMLHttpRequest',
|
||||||
'Referer': url,
|
'Referer': url,
|
||||||
@ -404,6 +437,7 @@ class InstagramIE(InstagramBaseIE):
|
|||||||
'doc_id': '8845758582119845',
|
'doc_id': '8845758582119845',
|
||||||
'variables': json.dumps(variables, separators=(',', ':')),
|
'variables': json.dumps(variables, separators=(',', ':')),
|
||||||
})
|
})
|
||||||
|
media.update(traverse_obj(general_info, ('data', 'xdt_shortcode_media')) or {})
|
||||||
|
|
||||||
if not general_info:
|
if not general_info:
|
||||||
self.report_warning('General metadata extraction failed (some metadata might be missing).', video_id)
|
self.report_warning('General metadata extraction failed (some metadata might be missing).', video_id)
|
||||||
@ -432,26 +466,6 @@ class InstagramIE(InstagramBaseIE):
|
|||||||
media.update(traverse_obj(
|
media.update(traverse_obj(
|
||||||
additional_data, ('graphql', 'shortcode_media'), 'shortcode_media', expected_type=dict) or {})
|
additional_data, ('graphql', 'shortcode_media'), 'shortcode_media', expected_type=dict) or {})
|
||||||
|
|
||||||
else:
|
|
||||||
xdt_shortcode_media = traverse_obj(general_info, ('data', 'xdt_shortcode_media', {dict})) or {}
|
|
||||||
if not xdt_shortcode_media:
|
|
||||||
error = join_nonempty('title', 'description', delim=': ', from_dict=api_check)
|
|
||||||
if 'Restricted Video' in error:
|
|
||||||
self.raise_login_required(error)
|
|
||||||
elif error:
|
|
||||||
raise ExtractorError(error, expected=True)
|
|
||||||
elif len(video_id) > 28:
|
|
||||||
# It's a private post (video_id == shortcode + 28 extra characters)
|
|
||||||
# Only raise after getting empty response; sometimes "long"-shortcode posts are public
|
|
||||||
self.raise_login_required(
|
|
||||||
'This content is only available for registered users who follow this account')
|
|
||||||
raise ExtractorError(
|
|
||||||
'Instagram sent an empty media response. Check if this post is accessible in your '
|
|
||||||
f'browser without being logged-in. If it is not, then u{self._login_hint()[1:]}. '
|
|
||||||
'Otherwise, if the post is accessible in browser without being logged-in'
|
|
||||||
f'{bug_reports_message(before=",")}', expected=True)
|
|
||||||
media.update(xdt_shortcode_media)
|
|
||||||
|
|
||||||
username = traverse_obj(media, ('owner', 'username')) or self._search_regex(
|
username = traverse_obj(media, ('owner', 'username')) or self._search_regex(
|
||||||
r'"owner"\s*:\s*{\s*"username"\s*:\s*"(.+?)"', webpage, 'username', fatal=False)
|
r'"owner"\s*:\s*{\s*"username"\s*:\s*"(.+?)"', webpage, 'username', fatal=False)
|
||||||
|
|
||||||
@ -471,7 +485,8 @@ class InstagramIE(InstagramBaseIE):
|
|||||||
return self.playlist_result(
|
return self.playlist_result(
|
||||||
self._extract_nodes(nodes, True), video_id,
|
self._extract_nodes(nodes, True), video_id,
|
||||||
format_field(username, None, 'Post by %s'), description)
|
format_field(username, None, 'Post by %s'), description)
|
||||||
raise ExtractorError('There is no video in this post', expected=True)
|
|
||||||
|
video_url = self._og_search_video_url(webpage, secure=False)
|
||||||
|
|
||||||
formats = [{
|
formats = [{
|
||||||
'url': video_url,
|
'url': video_url,
|
||||||
@ -674,7 +689,7 @@ class InstagramTagIE(InstagramPlaylistBaseIE):
|
|||||||
|
|
||||||
|
|
||||||
class InstagramStoryIE(InstagramBaseIE):
|
class InstagramStoryIE(InstagramBaseIE):
|
||||||
_VALID_URL = r'https?://(?:www\.)?instagram\.com/stories/(?P<user>[^/?#]+)(?:/(?P<id>\d+))?'
|
_VALID_URL = r'https?://(?:www\.)?instagram\.com/stories/(?P<user>[^/]+)/(?P<id>\d+)'
|
||||||
IE_NAME = 'instagram:story'
|
IE_NAME = 'instagram:story'
|
||||||
|
|
||||||
_TESTS = [{
|
_TESTS = [{
|
||||||
@ -684,38 +699,25 @@ class InstagramStoryIE(InstagramBaseIE):
|
|||||||
'title': 'Rare',
|
'title': 'Rare',
|
||||||
},
|
},
|
||||||
'playlist_mincount': 50,
|
'playlist_mincount': 50,
|
||||||
}, {
|
|
||||||
'url': 'https://www.instagram.com/stories/fruits_zipper/3570766765028588805/',
|
|
||||||
'only_matching': True,
|
|
||||||
}, {
|
|
||||||
'url': 'https://www.instagram.com/stories/fruits_zipper',
|
|
||||||
'only_matching': True,
|
|
||||||
}]
|
}]
|
||||||
|
|
||||||
def _real_extract(self, url):
|
def _real_extract(self, url):
|
||||||
username, story_id = self._match_valid_url(url).group('user', 'id')
|
username, story_id = self._match_valid_url(url).groups()
|
||||||
if username == 'highlights' and not story_id: # story id is only mandatory for highlights
|
story_info = self._download_webpage(url, story_id)
|
||||||
raise ExtractorError('Input URL is missing a highlight ID', expected=True)
|
user_info = self._search_json(r'"user":', story_info, 'user info', story_id, fatal=False)
|
||||||
display_id = story_id or username
|
|
||||||
story_info = self._download_webpage(url, display_id)
|
|
||||||
user_info = self._search_json(r'"user":', story_info, 'user info', display_id, fatal=False)
|
|
||||||
if not user_info:
|
if not user_info:
|
||||||
self.raise_login_required('This content is unreachable')
|
self.raise_login_required('This content is unreachable')
|
||||||
|
|
||||||
user_id = traverse_obj(user_info, 'pk', 'id', expected_type=str)
|
user_id = traverse_obj(user_info, 'pk', 'id', expected_type=str)
|
||||||
if username == 'highlights':
|
story_info_url = user_id if username != 'highlights' else f'highlight:{story_id}'
|
||||||
story_info_url = f'highlight:{story_id}'
|
if not story_info_url: # user id is only mandatory for non-highlights
|
||||||
else:
|
|
||||||
if not user_id: # user id is only mandatory for non-highlights
|
|
||||||
raise ExtractorError('Unable to extract user id')
|
raise ExtractorError('Unable to extract user id')
|
||||||
story_info_url = user_id
|
|
||||||
|
|
||||||
videos = traverse_obj(self._download_json(
|
videos = traverse_obj(self._download_json(
|
||||||
f'{self._API_BASE_URL}/feed/reels_media/?reel_ids={story_info_url}',
|
f'{self._API_BASE_URL}/feed/reels_media/?reel_ids={story_info_url}',
|
||||||
display_id, errnote=False, fatal=False, headers=self._api_headers), 'reels')
|
story_id, errnote=False, fatal=False, headers=self._API_HEADERS), 'reels')
|
||||||
if not videos:
|
if not videos:
|
||||||
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')
|
||||||
user_info = traverse_obj(videos, (user_id, 'user', {dict})) or {}
|
|
||||||
|
|
||||||
full_name = traverse_obj(videos, (f'highlight:{story_id}', 'user', 'full_name'), (user_id, 'user', 'full_name'))
|
full_name = traverse_obj(videos, (f'highlight:{story_id}', 'user', 'full_name'), (user_id, 'user', 'full_name'))
|
||||||
story_title = traverse_obj(videos, (f'highlight:{story_id}', 'title'))
|
story_title = traverse_obj(videos, (f'highlight:{story_id}', 'title'))
|
||||||
@ -725,7 +727,6 @@ class InstagramStoryIE(InstagramBaseIE):
|
|||||||
highlights = traverse_obj(videos, (f'highlight:{story_id}', 'items'), (user_id, 'items'))
|
highlights = traverse_obj(videos, (f'highlight:{story_id}', 'items'), (user_id, 'items'))
|
||||||
info_data = []
|
info_data = []
|
||||||
for highlight in highlights:
|
for highlight in highlights:
|
||||||
highlight.setdefault('user', {}).update(user_info)
|
|
||||||
highlight_data = self._extract_product(highlight)
|
highlight_data = self._extract_product(highlight)
|
||||||
if highlight_data.get('formats'):
|
if highlight_data.get('formats'):
|
||||||
info_data.append({
|
info_data.append({
|
||||||
@ -733,7 +734,4 @@ class InstagramStoryIE(InstagramBaseIE):
|
|||||||
'uploader_id': user_id,
|
'uploader_id': user_id,
|
||||||
**filter_dict(highlight_data),
|
**filter_dict(highlight_data),
|
||||||
})
|
})
|
||||||
if username != 'highlights' and story_id and not self._yes_playlist(username, story_id):
|
|
||||||
return traverse_obj(info_data, (lambda _, v: v['id'] == _pk_to_id(story_id), any))
|
|
||||||
|
|
||||||
return self.playlist_result(info_data, playlist_id=story_id, playlist_title=story_title)
|
return self.playlist_result(info_data, playlist_id=story_id, playlist_title=story_title)
|
||||||
|
@ -1,78 +0,0 @@
|
|||||||
from .common import InfoExtractor
|
|
||||||
from ..utils import int_or_none, parse_iso8601, url_or_none, urljoin
|
|
||||||
from ..utils.traversal import traverse_obj
|
|
||||||
|
|
||||||
|
|
||||||
class IvooxIE(InfoExtractor):
|
|
||||||
_VALID_URL = (
|
|
||||||
r'https?://(?:www\.)?ivoox\.com/(?:\w{2}/)?[^/?#]+_rf_(?P<id>[0-9]+)_1\.html',
|
|
||||||
r'https?://go\.ivoox\.com/rf/(?P<id>[0-9]+)',
|
|
||||||
)
|
|
||||||
_TESTS = [{
|
|
||||||
'url': 'https://www.ivoox.com/dex-08x30-rostros-del-mal-los-asesinos-en-audios-mp3_rf_143594959_1.html',
|
|
||||||
'md5': '993f712de5b7d552459fc66aa3726885',
|
|
||||||
'info_dict': {
|
|
||||||
'id': '143594959',
|
|
||||||
'ext': 'mp3',
|
|
||||||
'timestamp': 1742731200,
|
|
||||||
'channel': 'DIAS EXTRAÑOS con Santiago Camacho',
|
|
||||||
'title': 'DEx 08x30 Rostros del mal: Los asesinos en serie que aterrorizaron España',
|
|
||||||
'description': 'md5:eae8b4b9740d0216d3871390b056bb08',
|
|
||||||
'uploader': 'Santiago Camacho',
|
|
||||||
'thumbnail': 'https://static-1.ivoox.com/audios/c/d/5/2/cd52f46783fe735000c33a803dce2554_XXL.jpg',
|
|
||||||
'upload_date': '20250323',
|
|
||||||
'episode': 'DEx 08x30 Rostros del mal: Los asesinos en serie que aterrorizaron España',
|
|
||||||
'duration': 11837,
|
|
||||||
'tags': ['españa', 'asesinos en serie', 'arropiero', 'historia criminal', 'mataviejas'],
|
|
||||||
},
|
|
||||||
}, {
|
|
||||||
'url': 'https://go.ivoox.com/rf/143594959',
|
|
||||||
'only_matching': True,
|
|
||||||
}, {
|
|
||||||
'url': 'https://www.ivoox.com/en/campodelgas-28-03-2025-audios-mp3_rf_144036942_1.html',
|
|
||||||
'only_matching': True,
|
|
||||||
}]
|
|
||||||
|
|
||||||
def _real_extract(self, url):
|
|
||||||
media_id = self._match_id(url)
|
|
||||||
webpage = self._download_webpage(url, media_id, fatal=False)
|
|
||||||
|
|
||||||
data = self._search_nuxt_data(
|
|
||||||
webpage, media_id, fatal=False, traverse=('data', 0, 'data', 'audio'))
|
|
||||||
|
|
||||||
direct_download = self._download_json(
|
|
||||||
f'https://vcore-web.ivoox.com/v1/public/audios/{media_id}/download-url', media_id, fatal=False,
|
|
||||||
note='Fetching direct download link', headers={'Referer': url})
|
|
||||||
|
|
||||||
download_paths = {
|
|
||||||
*traverse_obj(direct_download, ('data', 'downloadUrl', {str}, filter, all)),
|
|
||||||
*traverse_obj(data, (('downloadUrl', 'mediaUrl'), {str}, filter)),
|
|
||||||
}
|
|
||||||
|
|
||||||
formats = []
|
|
||||||
for path in download_paths:
|
|
||||||
formats.append({
|
|
||||||
'url': urljoin('https://ivoox.com', path),
|
|
||||||
'http_headers': {'Referer': url},
|
|
||||||
})
|
|
||||||
|
|
||||||
return {
|
|
||||||
'id': media_id,
|
|
||||||
'formats': formats,
|
|
||||||
'uploader': self._html_search_regex(r'data-prm-author="([^"]+)"', webpage, 'author', default=None),
|
|
||||||
'timestamp': parse_iso8601(
|
|
||||||
self._html_search_regex(r'data-prm-pubdate="([^"]+)"', webpage, 'timestamp', default=None)),
|
|
||||||
'channel': self._html_search_regex(r'data-prm-podname="([^"]+)"', webpage, 'channel', default=None),
|
|
||||||
'title': self._html_search_regex(r'data-prm-title="([^"]+)"', webpage, 'title', default=None),
|
|
||||||
'thumbnail': self._og_search_thumbnail(webpage, default=None),
|
|
||||||
'description': self._og_search_description(webpage, default=None),
|
|
||||||
**self._search_json_ld(webpage, media_id, default={}),
|
|
||||||
**traverse_obj(data, {
|
|
||||||
'title': ('title', {str}),
|
|
||||||
'description': ('description', {str}),
|
|
||||||
'thumbnail': ('image', {url_or_none}),
|
|
||||||
'timestamp': ('uploadDate', {parse_iso8601(delimiter=' ')}),
|
|
||||||
'duration': ('duration', {int_or_none}),
|
|
||||||
'tags': ('tags', ..., 'name', {str}),
|
|
||||||
}),
|
|
||||||
}
|
|
@ -2,12 +2,10 @@ import hashlib
|
|||||||
import random
|
import random
|
||||||
|
|
||||||
from .common import InfoExtractor
|
from .common import InfoExtractor
|
||||||
from ..networking import HEADRequest
|
|
||||||
from ..utils import (
|
from ..utils import (
|
||||||
clean_html,
|
clean_html,
|
||||||
int_or_none,
|
int_or_none,
|
||||||
try_get,
|
try_get,
|
||||||
urlhandle_detect_ext,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -29,7 +27,7 @@ class JamendoIE(InfoExtractor):
|
|||||||
'ext': 'flac',
|
'ext': 'flac',
|
||||||
# 'title': 'Maya Filipič - Stories from Emona I',
|
# 'title': 'Maya Filipič - Stories from Emona I',
|
||||||
'title': 'Stories from Emona I',
|
'title': 'Stories from Emona I',
|
||||||
'artists': ['Maya Filipič'],
|
'artist': 'Maya Filipič',
|
||||||
'album': 'Between two worlds',
|
'album': 'Between two worlds',
|
||||||
'track': 'Stories from Emona I',
|
'track': 'Stories from Emona I',
|
||||||
'duration': 210,
|
'duration': 210,
|
||||||
@ -95,15 +93,9 @@ class JamendoIE(InfoExtractor):
|
|||||||
if not cover_url or cover_url in urls:
|
if not cover_url or cover_url in urls:
|
||||||
continue
|
continue
|
||||||
urls.append(cover_url)
|
urls.append(cover_url)
|
||||||
urlh = self._request_webpage(
|
|
||||||
HEADRequest(cover_url), track_id, 'Checking thumbnail extension',
|
|
||||||
errnote=False, fatal=False)
|
|
||||||
if not urlh:
|
|
||||||
continue
|
|
||||||
size = int_or_none(cover_id.lstrip('size'))
|
size = int_or_none(cover_id.lstrip('size'))
|
||||||
thumbnails.append({
|
thumbnails.append({
|
||||||
'id': cover_id,
|
'id': cover_id,
|
||||||
'ext': urlhandle_detect_ext(urlh, default='jpg'),
|
|
||||||
'url': cover_url,
|
'url': cover_url,
|
||||||
'width': size,
|
'width': size,
|
||||||
'height': size,
|
'height': size,
|
||||||
|
@ -1,5 +1,3 @@
|
|||||||
import itertools
|
|
||||||
|
|
||||||
from .common import InfoExtractor
|
from .common import InfoExtractor
|
||||||
from ..utils import (
|
from ..utils import (
|
||||||
determine_ext,
|
determine_ext,
|
||||||
@ -126,43 +124,3 @@ class KikaIE(InfoExtractor):
|
|||||||
'vbr': ('bitrateVideo', {int_or_none}, {lambda x: None if x == -1 else x}),
|
'vbr': ('bitrateVideo', {int_or_none}, {lambda x: None if x == -1 else x}),
|
||||||
}),
|
}),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class KikaPlaylistIE(InfoExtractor):
|
|
||||||
_VALID_URL = r'https?://(?:www\.)?kika\.de/[\w-]+/(?P<id>[a-z-]+\d+)'
|
|
||||||
|
|
||||||
_TESTS = [{
|
|
||||||
'url': 'https://www.kika.de/logo/logo-die-welt-und-ich-562',
|
|
||||||
'info_dict': {
|
|
||||||
'id': 'logo-die-welt-und-ich-562',
|
|
||||||
'title': 'logo!',
|
|
||||||
'description': 'md5:7b9d7f65561b82fa512f2cfb553c397d',
|
|
||||||
},
|
|
||||||
'playlist_count': 100,
|
|
||||||
}]
|
|
||||||
|
|
||||||
def _entries(self, playlist_url, playlist_id):
|
|
||||||
for page in itertools.count(1):
|
|
||||||
data = self._download_json(playlist_url, playlist_id, note=f'Downloading page {page}')
|
|
||||||
for item in traverse_obj(data, ('content', lambda _, v: url_or_none(v['api']['url']))):
|
|
||||||
yield self.url_result(
|
|
||||||
item['api']['url'], ie=KikaIE,
|
|
||||||
**traverse_obj(item, {
|
|
||||||
'id': ('id', {str}),
|
|
||||||
'title': ('title', {str}),
|
|
||||||
'duration': ('duration', {int_or_none}),
|
|
||||||
'timestamp': ('date', {parse_iso8601}),
|
|
||||||
}))
|
|
||||||
|
|
||||||
playlist_url = traverse_obj(data, ('links', 'next', {url_or_none}))
|
|
||||||
if not playlist_url:
|
|
||||||
break
|
|
||||||
|
|
||||||
def _real_extract(self, url):
|
|
||||||
playlist_id = self._match_id(url)
|
|
||||||
brand_data = self._download_json(
|
|
||||||
f'https://www.kika.de/_next-api/proxy/v1/brands/{playlist_id}', playlist_id)
|
|
||||||
|
|
||||||
return self.playlist_result(
|
|
||||||
self._entries(brand_data['videoSubchannel']['videosPageUrl'], playlist_id),
|
|
||||||
playlist_id, title=brand_data.get('title'), description=brand_data.get('description'))
|
|
||||||
|
@ -26,7 +26,6 @@ class LBRYBaseIE(InfoExtractor):
|
|||||||
_CLAIM_ID_REGEX = r'[0-9a-f]{1,40}'
|
_CLAIM_ID_REGEX = r'[0-9a-f]{1,40}'
|
||||||
_OPT_CLAIM_ID = f'[^$@:/?#&]+(?:[:#]{_CLAIM_ID_REGEX})?'
|
_OPT_CLAIM_ID = f'[^$@:/?#&]+(?:[:#]{_CLAIM_ID_REGEX})?'
|
||||||
_SUPPORTED_STREAM_TYPES = ['video', 'audio']
|
_SUPPORTED_STREAM_TYPES = ['video', 'audio']
|
||||||
_UNSUPPORTED_STREAM_TYPES = ['binary']
|
|
||||||
_PAGE_SIZE = 50
|
_PAGE_SIZE = 50
|
||||||
|
|
||||||
def _call_api_proxy(self, method, display_id, params, resource):
|
def _call_api_proxy(self, method, display_id, params, resource):
|
||||||
@ -337,15 +336,12 @@ class LBRYIE(LBRYBaseIE):
|
|||||||
'vcodec': 'none' if stream_type == 'audio' else None,
|
'vcodec': 'none' if stream_type == 'audio' else None,
|
||||||
})
|
})
|
||||||
|
|
||||||
final_url = None
|
|
||||||
# HEAD request returns redirect response to m3u8 URL if available
|
# HEAD request returns redirect response to m3u8 URL if available
|
||||||
urlh = self._request_webpage(
|
final_url = self._request_webpage(
|
||||||
HEADRequest(streaming_url), display_id, headers=headers,
|
HEADRequest(streaming_url), display_id, headers=headers,
|
||||||
note='Downloading streaming redirect url info', fatal=False)
|
note='Downloading streaming redirect url info').url
|
||||||
if urlh:
|
|
||||||
final_url = urlh.url
|
|
||||||
|
|
||||||
elif result.get('value_type') == 'stream' and stream_type not in self._UNSUPPORTED_STREAM_TYPES:
|
elif result.get('value_type') == 'stream':
|
||||||
claim_id, is_live = result['signing_channel']['claim_id'], True
|
claim_id, is_live = result['signing_channel']['claim_id'], True
|
||||||
live_data = self._download_json(
|
live_data = self._download_json(
|
||||||
'https://api.odysee.live/livestream/is_live', claim_id,
|
'https://api.odysee.live/livestream/is_live', claim_id,
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
import itertools
|
import itertools
|
||||||
import json
|
|
||||||
import re
|
import re
|
||||||
|
|
||||||
from .common import InfoExtractor
|
from .common import InfoExtractor
|
||||||
@ -10,12 +9,12 @@ from ..utils import (
|
|||||||
int_or_none,
|
int_or_none,
|
||||||
mimetype2ext,
|
mimetype2ext,
|
||||||
srt_subtitles_timecode,
|
srt_subtitles_timecode,
|
||||||
|
traverse_obj,
|
||||||
try_get,
|
try_get,
|
||||||
url_or_none,
|
url_or_none,
|
||||||
urlencode_postdata,
|
urlencode_postdata,
|
||||||
urljoin,
|
urljoin,
|
||||||
)
|
)
|
||||||
from ..utils.traversal import find_elements, require, traverse_obj
|
|
||||||
|
|
||||||
|
|
||||||
class LinkedInBaseIE(InfoExtractor):
|
class LinkedInBaseIE(InfoExtractor):
|
||||||
@ -83,10 +82,7 @@ class LinkedInLearningBaseIE(LinkedInBaseIE):
|
|||||||
|
|
||||||
|
|
||||||
class LinkedInIE(LinkedInBaseIE):
|
class LinkedInIE(LinkedInBaseIE):
|
||||||
_VALID_URL = [
|
_VALID_URL = r'https?://(?:www\.)?linkedin\.com/posts/[^/?#]+-(?P<id>\d+)-\w{4}/?(?:[?#]|$)'
|
||||||
r'https?://(?:www\.)?linkedin\.com/posts/[^/?#]+-(?P<id>\d+)-\w{4}/?(?:[?#]|$)',
|
|
||||||
r'https?://(?:www\.)?linkedin\.com/feed/update/urn:li:activity:(?P<id>\d+)',
|
|
||||||
]
|
|
||||||
_TESTS = [{
|
_TESTS = [{
|
||||||
'url': 'https://www.linkedin.com/posts/mishalkhawaja_sendinblueviews-toronto-digitalmarketing-ugcPost-6850898786781339649-mM20',
|
'url': 'https://www.linkedin.com/posts/mishalkhawaja_sendinblueviews-toronto-digitalmarketing-ugcPost-6850898786781339649-mM20',
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
@ -110,9 +106,6 @@ class LinkedInIE(LinkedInBaseIE):
|
|||||||
'like_count': int,
|
'like_count': int,
|
||||||
'subtitles': 'mincount:1',
|
'subtitles': 'mincount:1',
|
||||||
},
|
},
|
||||||
}, {
|
|
||||||
'url': 'https://www.linkedin.com/feed/update/urn:li:activity:7016901149999955968/?utm_source=share&utm_medium=member_desktop',
|
|
||||||
'only_matching': True,
|
|
||||||
}]
|
}]
|
||||||
|
|
||||||
def _real_extract(self, url):
|
def _real_extract(self, url):
|
||||||
@ -278,110 +271,3 @@ class LinkedInLearningCourseIE(LinkedInLearningBaseIE):
|
|||||||
entries, course_slug,
|
entries, course_slug,
|
||||||
course_data.get('title'),
|
course_data.get('title'),
|
||||||
course_data.get('description'))
|
course_data.get('description'))
|
||||||
|
|
||||||
|
|
||||||
class LinkedInEventsIE(LinkedInBaseIE):
|
|
||||||
IE_NAME = 'linkedin:events'
|
|
||||||
_VALID_URL = r'https?://(?:www\.)?linkedin\.com/events/(?P<id>[\w-]+)'
|
|
||||||
_TESTS = [{
|
|
||||||
'url': 'https://www.linkedin.com/events/7084656651378536448/comments/',
|
|
||||||
'info_dict': {
|
|
||||||
'id': '7084656651378536448',
|
|
||||||
'ext': 'mp4',
|
|
||||||
'title': '#37 Aprende a hacer una entrevista en inglés para tu próximo trabajo remoto',
|
|
||||||
'description': '¡Agarra para anotar que se viene tremendo evento!',
|
|
||||||
'duration': 1765,
|
|
||||||
'timestamp': 1689113772,
|
|
||||||
'upload_date': '20230711',
|
|
||||||
'release_timestamp': 1689174012,
|
|
||||||
'release_date': '20230712',
|
|
||||||
'live_status': 'was_live',
|
|
||||||
},
|
|
||||||
}, {
|
|
||||||
'url': 'https://www.linkedin.com/events/27-02energyfreedombyenergyclub7295762520814874625/comments/',
|
|
||||||
'info_dict': {
|
|
||||||
'id': '27-02energyfreedombyenergyclub7295762520814874625',
|
|
||||||
'ext': 'mp4',
|
|
||||||
'title': '27.02 Energy Freedom by Energy Club',
|
|
||||||
'description': 'md5:1292e6f31df998914c293787a02c3b91',
|
|
||||||
'duration': 6420,
|
|
||||||
'timestamp': 1739445333,
|
|
||||||
'upload_date': '20250213',
|
|
||||||
'release_timestamp': 1740657620,
|
|
||||||
'release_date': '20250227',
|
|
||||||
'live_status': 'was_live',
|
|
||||||
},
|
|
||||||
}]
|
|
||||||
|
|
||||||
def _real_initialize(self):
|
|
||||||
if not self._get_cookies('https://www.linkedin.com/').get('li_at'):
|
|
||||||
self.raise_login_required()
|
|
||||||
|
|
||||||
def _real_extract(self, url):
|
|
||||||
event_id = self._match_id(url)
|
|
||||||
webpage = self._download_webpage(url, event_id)
|
|
||||||
|
|
||||||
base_data = traverse_obj(webpage, (
|
|
||||||
{find_elements(tag='code', attr='style', value='display: none')}, ..., {json.loads}, 'included', ...))
|
|
||||||
meta_data = traverse_obj(base_data, (
|
|
||||||
lambda _, v: v['$type'] == 'com.linkedin.voyager.dash.events.ProfessionalEvent', any)) or {}
|
|
||||||
|
|
||||||
live_status = {
|
|
||||||
'PAST': 'was_live',
|
|
||||||
'ONGOING': 'is_live',
|
|
||||||
'FUTURE': 'is_upcoming',
|
|
||||||
}.get(meta_data.get('lifecycleState'))
|
|
||||||
|
|
||||||
if live_status == 'is_upcoming':
|
|
||||||
player_data = {}
|
|
||||||
if event_time := traverse_obj(meta_data, ('displayEventTime', {str})):
|
|
||||||
message = f'This live event is scheduled for {event_time}'
|
|
||||||
else:
|
|
||||||
message = 'This live event has not yet started'
|
|
||||||
self.raise_no_formats(message, expected=True, video_id=event_id)
|
|
||||||
else:
|
|
||||||
# TODO: Add support for audio-only live events
|
|
||||||
player_data = traverse_obj(base_data, (
|
|
||||||
lambda _, v: v['$type'] == 'com.linkedin.videocontent.VideoPlayMetadata',
|
|
||||||
any, {require('video player data')}))
|
|
||||||
|
|
||||||
formats, subtitles = [], {}
|
|
||||||
for prog_fmts in traverse_obj(player_data, ('progressiveStreams', ..., {dict})):
|
|
||||||
for fmt_url in traverse_obj(prog_fmts, ('streamingLocations', ..., 'url', {url_or_none})):
|
|
||||||
formats.append({
|
|
||||||
'url': fmt_url,
|
|
||||||
**traverse_obj(prog_fmts, {
|
|
||||||
'width': ('width', {int_or_none}),
|
|
||||||
'height': ('height', {int_or_none}),
|
|
||||||
'tbr': ('bitRate', {int_or_none(scale=1000)}),
|
|
||||||
'filesize': ('size', {int_or_none}),
|
|
||||||
'ext': ('mediaType', {mimetype2ext}),
|
|
||||||
}),
|
|
||||||
})
|
|
||||||
|
|
||||||
for m3u8_url in traverse_obj(player_data, (
|
|
||||||
'adaptiveStreams', lambda _, v: v['protocol'] == 'HLS', 'masterPlaylists', ..., 'url', {url_or_none},
|
|
||||||
)):
|
|
||||||
fmts, subs = self._extract_m3u8_formats_and_subtitles(
|
|
||||||
m3u8_url, event_id, 'mp4', m3u8_id='hls', fatal=False)
|
|
||||||
formats.extend(fmts)
|
|
||||||
self._merge_subtitles(subs, target=subtitles)
|
|
||||||
|
|
||||||
return {
|
|
||||||
'id': event_id,
|
|
||||||
'formats': formats,
|
|
||||||
'subtitles': subtitles,
|
|
||||||
'live_status': live_status,
|
|
||||||
**traverse_obj(meta_data, {
|
|
||||||
'title': ('name', {str}),
|
|
||||||
'description': ('description', 'text', {str}),
|
|
||||||
'timestamp': ('createdAt', {int_or_none(scale=1000)}),
|
|
||||||
# timeRange.start is available when the stream is_upcoming
|
|
||||||
'release_timestamp': ('timeRange', 'start', {int_or_none(scale=1000)}),
|
|
||||||
}),
|
|
||||||
**traverse_obj(player_data, {
|
|
||||||
'duration': ('duration', {int_or_none(scale=1000)}),
|
|
||||||
# liveStreamCreatedAt is only available when the stream is_live or was_live
|
|
||||||
'release_timestamp': ('liveStreamCreatedAt', {int_or_none(scale=1000)}),
|
|
||||||
}),
|
|
||||||
}
|
|
||||||
|
@ -1,159 +0,0 @@
|
|||||||
import json
|
|
||||||
import random
|
|
||||||
import time
|
|
||||||
|
|
||||||
from .common import InfoExtractor
|
|
||||||
from ..utils import int_or_none, jwt_decode_hs256, try_call, url_or_none
|
|
||||||
from ..utils.traversal import require, traverse_obj
|
|
||||||
|
|
||||||
|
|
||||||
class LocoIE(InfoExtractor):
|
|
||||||
_VALID_URL = r'https?://(?:www\.)?loco\.com/(?P<type>streamers|stream)/(?P<id>[^/?#]+)'
|
|
||||||
_TESTS = [{
|
|
||||||
'url': 'https://loco.com/streamers/teuzinfps',
|
|
||||||
'info_dict': {
|
|
||||||
'id': 'teuzinfps',
|
|
||||||
'ext': 'mp4',
|
|
||||||
'title': r're:MS BOLADAO, RESENHA & GAMEPLAY ALTO NIVEL',
|
|
||||||
'description': 'bom e novo',
|
|
||||||
'uploader_id': 'RLUVE3S9JU',
|
|
||||||
'channel': 'teuzinfps',
|
|
||||||
'channel_follower_count': int,
|
|
||||||
'comment_count': int,
|
|
||||||
'view_count': int,
|
|
||||||
'concurrent_view_count': int,
|
|
||||||
'like_count': int,
|
|
||||||
'thumbnail': 'https://static.ivory.getloconow.com/default_thumb/743701a9-98ca-41ae-9a8b-70bd5da070ad.jpg',
|
|
||||||
'tags': ['MMORPG', 'Gameplay'],
|
|
||||||
'series': 'Tibia',
|
|
||||||
'timestamp': int,
|
|
||||||
'modified_timestamp': int,
|
|
||||||
'live_status': 'is_live',
|
|
||||||
'upload_date': str,
|
|
||||||
'modified_date': str,
|
|
||||||
},
|
|
||||||
'params': {
|
|
||||||
'skip_download': 'Livestream',
|
|
||||||
},
|
|
||||||
}, {
|
|
||||||
'url': 'https://loco.com/stream/c64916eb-10fb-46a9-9a19-8c4b7ed064e7',
|
|
||||||
'md5': '45ebc8a47ee1c2240178757caf8881b5',
|
|
||||||
'info_dict': {
|
|
||||||
'id': 'c64916eb-10fb-46a9-9a19-8c4b7ed064e7',
|
|
||||||
'ext': 'mp4',
|
|
||||||
'title': 'PAULINHO LOKO NA LOCO!',
|
|
||||||
'description': 'live on na loco',
|
|
||||||
'uploader_id': '2MDO7Z1DPM',
|
|
||||||
'channel': 'paulinholokobr',
|
|
||||||
'channel_follower_count': int,
|
|
||||||
'comment_count': int,
|
|
||||||
'view_count': int,
|
|
||||||
'concurrent_view_count': int,
|
|
||||||
'like_count': int,
|
|
||||||
'duration': 14491,
|
|
||||||
'thumbnail': 'https://static.ivory.getloconow.com/default_thumb/59b5970b-23c1-4518-9e96-17ce341299fe.jpg',
|
|
||||||
'tags': ['Gameplay'],
|
|
||||||
'series': 'GTA 5',
|
|
||||||
'timestamp': 1740612872,
|
|
||||||
'modified_timestamp': 1740613037,
|
|
||||||
'upload_date': '20250226',
|
|
||||||
'modified_date': '20250226',
|
|
||||||
},
|
|
||||||
}, {
|
|
||||||
# Requires video authorization
|
|
||||||
'url': 'https://loco.com/stream/ac854641-ae0f-497c-a8ea-4195f6d8cc53',
|
|
||||||
'md5': '0513edf85c1e65c9521f555f665387d5',
|
|
||||||
'info_dict': {
|
|
||||||
'id': 'ac854641-ae0f-497c-a8ea-4195f6d8cc53',
|
|
||||||
'ext': 'mp4',
|
|
||||||
'title': 'DUAS CONTAS DESAFIANTE, RUSH TOP 1 NO BRASIL!',
|
|
||||||
'description': 'md5:aa77818edd6fe00dd4b6be75cba5f826',
|
|
||||||
'uploader_id': '7Y9JNAZC3Q',
|
|
||||||
'channel': 'ayellol',
|
|
||||||
'channel_follower_count': int,
|
|
||||||
'comment_count': int,
|
|
||||||
'view_count': int,
|
|
||||||
'concurrent_view_count': int,
|
|
||||||
'like_count': int,
|
|
||||||
'duration': 1229,
|
|
||||||
'thumbnail': 'https://static.ivory.getloconow.com/default_thumb/f5aa678b-6d04-45d9-a89a-859af0a8028f.jpg',
|
|
||||||
'tags': ['Gameplay', 'Carry'],
|
|
||||||
'series': 'League of Legends',
|
|
||||||
'timestamp': 1741182253,
|
|
||||||
'upload_date': '20250305',
|
|
||||||
'modified_timestamp': 1741182419,
|
|
||||||
'modified_date': '20250305',
|
|
||||||
},
|
|
||||||
}]
|
|
||||||
|
|
||||||
# From _app.js
|
|
||||||
_CLIENT_ID = 'TlwKp1zmF6eKFpcisn3FyR18WkhcPkZtzwPVEEC3'
|
|
||||||
_CLIENT_SECRET = 'Kp7tYlUN7LXvtcSpwYvIitgYcLparbtsQSe5AdyyCdiEJBP53Vt9J8eB4AsLdChIpcO2BM19RA3HsGtqDJFjWmwoonvMSG3ZQmnS8x1YIM8yl82xMXZGbE3NKiqmgBVU'
|
|
||||||
|
|
||||||
def _is_jwt_expired(self, token):
|
|
||||||
return jwt_decode_hs256(token)['exp'] - time.time() < 300
|
|
||||||
|
|
||||||
def _get_access_token(self, video_id):
|
|
||||||
access_token = try_call(lambda: self._get_cookies('https://loco.com')['access_token'].value)
|
|
||||||
if access_token and not self._is_jwt_expired(access_token):
|
|
||||||
return access_token
|
|
||||||
access_token = traverse_obj(self._download_json(
|
|
||||||
'https://api.getloconow.com/v3/user/device_profile/', video_id,
|
|
||||||
'Downloading access token', fatal=False, data=json.dumps({
|
|
||||||
'platform': 7,
|
|
||||||
'client_id': self._CLIENT_ID,
|
|
||||||
'client_secret': self._CLIENT_SECRET,
|
|
||||||
'model': 'Mozilla',
|
|
||||||
'os_name': 'Win32',
|
|
||||||
'os_ver': '5.0 (Windows)',
|
|
||||||
'app_ver': '5.0 (Windows)',
|
|
||||||
}).encode(), headers={
|
|
||||||
'Content-Type': 'application/json;charset=utf-8',
|
|
||||||
'DEVICE-ID': ''.join(random.choices('0123456789abcdef', k=32)) + 'live',
|
|
||||||
'X-APP-LANG': 'en',
|
|
||||||
'X-APP-LOCALE': 'en-US',
|
|
||||||
'X-CLIENT-ID': self._CLIENT_ID,
|
|
||||||
'X-CLIENT-SECRET': self._CLIENT_SECRET,
|
|
||||||
'X-PLATFORM': '7',
|
|
||||||
}), 'access_token')
|
|
||||||
if access_token and not self._is_jwt_expired(access_token):
|
|
||||||
self._set_cookie('.loco.com', 'access_token', access_token)
|
|
||||||
return access_token
|
|
||||||
|
|
||||||
def _real_extract(self, url):
|
|
||||||
video_type, video_id = self._match_valid_url(url).group('type', 'id')
|
|
||||||
webpage = self._download_webpage(url, video_id)
|
|
||||||
stream = traverse_obj(self._search_nextjs_data(webpage, video_id), (
|
|
||||||
'props', 'pageProps', ('liveStreamData', 'stream', 'liveStream'), {dict}, any, {require('stream info')}))
|
|
||||||
|
|
||||||
if access_token := self._get_access_token(video_id):
|
|
||||||
self._request_webpage(
|
|
||||||
'https://drm.loco.com/v1/streams/playback/', video_id,
|
|
||||||
'Downloading video authorization', fatal=False, headers={
|
|
||||||
'authorization': access_token,
|
|
||||||
}, query={
|
|
||||||
'stream_uid': stream['uid'],
|
|
||||||
})
|
|
||||||
|
|
||||||
return {
|
|
||||||
'formats': self._extract_m3u8_formats(stream['conf']['hls'], video_id),
|
|
||||||
'id': video_id,
|
|
||||||
'is_live': video_type == 'streamers',
|
|
||||||
**traverse_obj(stream, {
|
|
||||||
'title': ('title', {str}),
|
|
||||||
'series': ('game_name', {str}),
|
|
||||||
'uploader_id': ('user_uid', {str}),
|
|
||||||
'channel': ('alias', {str}),
|
|
||||||
'description': ('description', {str}),
|
|
||||||
'concurrent_view_count': ('viewersCurrent', {int_or_none}),
|
|
||||||
'view_count': ('total_views', {int_or_none}),
|
|
||||||
'thumbnail': ('thumbnail_url_small', {url_or_none}),
|
|
||||||
'like_count': ('likes', {int_or_none}),
|
|
||||||
'tags': ('tags', ..., {str}),
|
|
||||||
'timestamp': ('started_at', {int_or_none(scale=1000)}),
|
|
||||||
'modified_timestamp': ('updated_at', {int_or_none(scale=1000)}),
|
|
||||||
'comment_count': ('comments_count', {int_or_none}),
|
|
||||||
'channel_follower_count': ('followers_count', {int_or_none}),
|
|
||||||
'duration': ('duration', {int_or_none}),
|
|
||||||
}),
|
|
||||||
}
|
|
@ -2,11 +2,8 @@ from .common import InfoExtractor
|
|||||||
from ..utils import (
|
from ..utils import (
|
||||||
clean_html,
|
clean_html,
|
||||||
merge_dicts,
|
merge_dicts,
|
||||||
str_or_none,
|
|
||||||
traverse_obj,
|
traverse_obj,
|
||||||
unified_timestamp,
|
|
||||||
url_or_none,
|
url_or_none,
|
||||||
urljoin,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -83,7 +80,7 @@ class LRTVODIE(LRTBaseIE):
|
|||||||
}]
|
}]
|
||||||
|
|
||||||
def _real_extract(self, url):
|
def _real_extract(self, url):
|
||||||
path, video_id = self._match_valid_url(url).group('path', 'id')
|
path, video_id = self._match_valid_url(url).groups()
|
||||||
webpage = self._download_webpage(url, video_id)
|
webpage = self._download_webpage(url, video_id)
|
||||||
|
|
||||||
media_url = self._extract_js_var(webpage, 'main_url', path)
|
media_url = self._extract_js_var(webpage, 'main_url', path)
|
||||||
@ -109,42 +106,3 @@ class LRTVODIE(LRTBaseIE):
|
|||||||
}
|
}
|
||||||
|
|
||||||
return merge_dicts(clean_info, jw_data, json_ld_data)
|
return merge_dicts(clean_info, jw_data, json_ld_data)
|
||||||
|
|
||||||
|
|
||||||
class LRTRadioIE(LRTBaseIE):
|
|
||||||
_VALID_URL = r'https?://(?:www\.)?lrt\.lt/radioteka/irasas/(?P<id>\d+)/(?P<path>[^?#/]+)'
|
|
||||||
_TESTS = [{
|
|
||||||
# m3u8 download
|
|
||||||
'url': 'https://www.lrt.lt/radioteka/irasas/2000359728/nemarios-eiles-apie-pragarus-ir-skaistyklas-su-aiste-kiltinaviciute',
|
|
||||||
'info_dict': {
|
|
||||||
'id': '2000359728',
|
|
||||||
'ext': 'm4a',
|
|
||||||
'title': 'Nemarios eilės: apie pragarus ir skaistyklas su Aiste Kiltinavičiūte',
|
|
||||||
'description': 'md5:5eee9a0e86a55bf547bd67596204625d',
|
|
||||||
'timestamp': 1726143120,
|
|
||||||
'upload_date': '20240912',
|
|
||||||
'tags': 'count:5',
|
|
||||||
'thumbnail': r're:https?://.+/.+\.jpe?g',
|
|
||||||
'categories': ['Daiktiniai įrodymai'],
|
|
||||||
},
|
|
||||||
}, {
|
|
||||||
'url': 'https://www.lrt.lt/radioteka/irasas/2000304654/vakaras-su-knyga-svetlana-aleksijevic-cernobylio-malda-v-dalis?season=%2Fmediateka%2Faudio%2Fvakaras-su-knyga%2F2023',
|
|
||||||
'only_matching': True,
|
|
||||||
}]
|
|
||||||
|
|
||||||
def _real_extract(self, url):
|
|
||||||
video_id, path = self._match_valid_url(url).group('id', 'path')
|
|
||||||
media = self._download_json(
|
|
||||||
'https://www.lrt.lt/radioteka/api/media', video_id,
|
|
||||||
query={'url': f'/mediateka/irasas/{video_id}/{path}'})
|
|
||||||
|
|
||||||
return traverse_obj(media, {
|
|
||||||
'id': ('id', {int}, {str_or_none}),
|
|
||||||
'title': ('title', {str}),
|
|
||||||
'tags': ('tags', ..., 'name', {str}),
|
|
||||||
'categories': ('playlist_item', 'category', {str}, filter, all, filter),
|
|
||||||
'description': ('content', {clean_html}, {str}),
|
|
||||||
'timestamp': ('date', {lambda x: x.replace('.', '/')}, {unified_timestamp}),
|
|
||||||
'thumbnail': ('playlist_item', 'image', {urljoin('https://www.lrt.lt')}),
|
|
||||||
'formats': ('playlist_item', 'file', {lambda x: self._extract_m3u8_formats(x, video_id)}),
|
|
||||||
})
|
|
||||||
|
@ -1,36 +1,35 @@
|
|||||||
from .common import InfoExtractor
|
from .common import InfoExtractor
|
||||||
from ..utils import parse_age_limit, parse_duration, url_or_none
|
from ..utils import parse_age_limit, parse_duration, traverse_obj
|
||||||
from ..utils.traversal import traverse_obj
|
|
||||||
|
|
||||||
|
|
||||||
class MagellanTVIE(InfoExtractor):
|
class MagellanTVIE(InfoExtractor):
|
||||||
_VALID_URL = r'https?://(?:www\.)?magellantv\.com/(?:watch|video)/(?P<id>[\w-]+)'
|
_VALID_URL = r'https?://(?:www\.)?magellantv\.com/(?:watch|video)/(?P<id>[\w-]+)'
|
||||||
_TESTS = [{
|
_TESTS = [{
|
||||||
'url': 'https://www.magellantv.com/watch/incas-the-new-story?type=v',
|
'url': 'https://www.magellantv.com/watch/my-dads-on-death-row?type=v',
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
'id': 'incas-the-new-story',
|
'id': 'my-dads-on-death-row',
|
||||||
'ext': 'mp4',
|
'ext': 'mp4',
|
||||||
'title': 'Incas: The New Story',
|
'title': 'My Dad\'s On Death Row',
|
||||||
'description': 'md5:936c7f6d711c02dfb9db22a067b586fe',
|
'description': 'md5:33ba23b9f0651fc4537ed19b1d5b0d7a',
|
||||||
|
'duration': 3780.0,
|
||||||
'age_limit': 14,
|
'age_limit': 14,
|
||||||
'duration': 3060.0,
|
'tags': ['Justice', 'Reality', 'United States', 'True Crime'],
|
||||||
'tags': ['Ancient History', 'Archaeology', 'Anthropology'],
|
|
||||||
},
|
},
|
||||||
'params': {'skip_download': 'm3u8'},
|
'params': {'skip_download': 'm3u8'},
|
||||||
}, {
|
}, {
|
||||||
'url': 'https://www.magellantv.com/video/tortured-to-death-murdering-the-nanny',
|
'url': 'https://www.magellantv.com/video/james-bulger-the-new-revelations',
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
'id': 'tortured-to-death-murdering-the-nanny',
|
'id': 'james-bulger-the-new-revelations',
|
||||||
'ext': 'mp4',
|
'ext': 'mp4',
|
||||||
'title': 'Tortured to Death: Murdering the Nanny',
|
'title': 'James Bulger: The New Revelations',
|
||||||
'description': 'md5:d87033594fa218af2b1a8b49f52511e5',
|
'description': 'md5:7b97922038bad1d0fe8d0470d8a189f2',
|
||||||
'age_limit': 14,
|
|
||||||
'duration': 2640.0,
|
'duration': 2640.0,
|
||||||
'tags': ['True Crime', 'Murder'],
|
'age_limit': 0,
|
||||||
|
'tags': ['Investigation', 'True Crime', 'Justice', 'Europe'],
|
||||||
},
|
},
|
||||||
'params': {'skip_download': 'm3u8'},
|
'params': {'skip_download': 'm3u8'},
|
||||||
}, {
|
}, {
|
||||||
'url': 'https://www.magellantv.com/watch/celebration-nation?type=s',
|
'url': 'https://www.magellantv.com/watch/celebration-nation',
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
'id': 'celebration-nation',
|
'id': 'celebration-nation',
|
||||||
'ext': 'mp4',
|
'ext': 'mp4',
|
||||||
@ -44,19 +43,10 @@ class MagellanTVIE(InfoExtractor):
|
|||||||
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)
|
||||||
context = self._search_nextjs_data(webpage, video_id)['props']['pageProps']['reactContext']
|
data = traverse_obj(self._search_nextjs_data(webpage, video_id), (
|
||||||
data = traverse_obj(context, ((('video', 'detail'), ('series', 'currentEpisode')), {dict}, any))
|
'props', 'pageProps', 'reactContext',
|
||||||
|
(('video', 'detail'), ('series', 'currentEpisode')), {dict}), get_all=False)
|
||||||
formats, subtitles = [], {}
|
formats, subtitles = self._extract_m3u8_formats_and_subtitles(data['jwpVideoUrl'], video_id)
|
||||||
for m3u8_url in set(traverse_obj(data, ((('manifests', ..., 'hls'), 'jwp_video_url'), {url_or_none}))):
|
|
||||||
fmts, subs = self._extract_m3u8_formats_and_subtitles(
|
|
||||||
m3u8_url, video_id, 'mp4', m3u8_id='hls', fatal=False)
|
|
||||||
formats.extend(fmts)
|
|
||||||
self._merge_subtitles(subs, target=subtitles)
|
|
||||||
if not formats and (error := traverse_obj(context, ('errorDetailPage', 'errorMessage', {str}))):
|
|
||||||
if 'available in your country' in error:
|
|
||||||
self.raise_geo_restricted(msg=error)
|
|
||||||
self.raise_no_formats(f'{self.IE_NAME} said: {error}', expected=True)
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
'id': video_id,
|
'id': video_id,
|
||||||
|
@ -1,38 +1,31 @@
|
|||||||
|
import re
|
||||||
|
|
||||||
from .common import InfoExtractor
|
from .common import InfoExtractor
|
||||||
from ..utils import (
|
from ..utils import (
|
||||||
clean_html,
|
|
||||||
determine_ext,
|
determine_ext,
|
||||||
|
extract_attributes,
|
||||||
int_or_none,
|
int_or_none,
|
||||||
join_nonempty,
|
str_to_int,
|
||||||
parse_count,
|
|
||||||
parse_duration,
|
|
||||||
parse_iso8601,
|
|
||||||
url_or_none,
|
url_or_none,
|
||||||
|
urlencode_postdata,
|
||||||
)
|
)
|
||||||
from ..utils.traversal import traverse_obj
|
|
||||||
|
|
||||||
|
|
||||||
class ManyVidsIE(InfoExtractor):
|
class ManyVidsIE(InfoExtractor):
|
||||||
|
_WORKING = False
|
||||||
_VALID_URL = r'(?i)https?://(?:www\.)?manyvids\.com/video/(?P<id>\d+)'
|
_VALID_URL = r'(?i)https?://(?:www\.)?manyvids\.com/video/(?P<id>\d+)'
|
||||||
_TESTS = [{
|
_TESTS = [{
|
||||||
# preview video
|
# preview video
|
||||||
'url': 'https://www.manyvids.com/Video/530341/mv-tips-tricks',
|
'url': 'https://www.manyvids.com/Video/133957/everthing-about-me/',
|
||||||
'md5': '738dc723f7735ee9602f7ea352a6d058',
|
'md5': '03f11bb21c52dd12a05be21a5c7dcc97',
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
'id': '530341-preview',
|
'id': '133957',
|
||||||
'ext': 'mp4',
|
'ext': 'mp4',
|
||||||
'title': 'MV Tips & Tricks (Preview)',
|
'title': 'everthing about me (Preview)',
|
||||||
'description': r're:I will take you on a tour around .{1313}$',
|
'uploader': 'ellyxxix',
|
||||||
'thumbnail': r're:https://cdn5\.manyvids\.com/php_uploads/video_images/DestinyDiaz/.+\.jpg',
|
|
||||||
'uploader': 'DestinyDiaz',
|
|
||||||
'view_count': int,
|
'view_count': int,
|
||||||
'like_count': int,
|
'like_count': int,
|
||||||
'release_timestamp': 1508419904,
|
|
||||||
'tags': ['AdultSchool', 'BBW', 'SFW', 'TeacherFetish'],
|
|
||||||
'release_date': '20171019',
|
|
||||||
'duration': 3167.0,
|
|
||||||
},
|
},
|
||||||
'expected_warnings': ['Only extracting preview'],
|
|
||||||
}, {
|
}, {
|
||||||
# full video
|
# full video
|
||||||
'url': 'https://www.manyvids.com/Video/935718/MY-FACE-REVEAL/',
|
'url': 'https://www.manyvids.com/Video/935718/MY-FACE-REVEAL/',
|
||||||
@ -41,68 +34,129 @@ class ManyVidsIE(InfoExtractor):
|
|||||||
'id': '935718',
|
'id': '935718',
|
||||||
'ext': 'mp4',
|
'ext': 'mp4',
|
||||||
'title': 'MY FACE REVEAL',
|
'title': 'MY FACE REVEAL',
|
||||||
'description': r're:Today is the day!! I am finally taking off my mask .{445}$',
|
'description': 'md5:ec5901d41808b3746fed90face161612',
|
||||||
'thumbnail': r're:https://ods\.manyvids\.com/1001061960/3aa5397f2a723ec4597e344df66ab845/screenshots/.+\.jpg',
|
|
||||||
'uploader': 'Sarah Calanthe',
|
'uploader': 'Sarah Calanthe',
|
||||||
'view_count': int,
|
'view_count': int,
|
||||||
'like_count': int,
|
'like_count': int,
|
||||||
'release_date': '20181110',
|
|
||||||
'tags': ['EyeContact', 'Interviews', 'MaskFetish', 'MouthFetish', 'Redhead'],
|
|
||||||
'release_timestamp': 1541851200,
|
|
||||||
'duration': 224.0,
|
|
||||||
},
|
},
|
||||||
}]
|
}]
|
||||||
_API_BASE = 'https://www.manyvids.com/bff/store/video'
|
|
||||||
|
|
||||||
def _real_extract(self, url):
|
def _real_extract(self, url):
|
||||||
video_id = self._match_id(url)
|
video_id = self._match_id(url)
|
||||||
video_data = self._download_json(f'{self._API_BASE}/{video_id}/private', video_id)['data']
|
|
||||||
formats, preview_only = [], True
|
|
||||||
|
|
||||||
for format_id, path in [
|
real_url = f'https://www.manyvids.com/video/{video_id}/gtm.js'
|
||||||
('preview', ['teaser', 'filepath']),
|
try:
|
||||||
('transcoded', ['transcodedFilepath']),
|
webpage = self._download_webpage(real_url, video_id)
|
||||||
('filepath', ['filepath']),
|
except Exception:
|
||||||
]:
|
# probably useless fallback
|
||||||
format_url = traverse_obj(video_data, (*path, {url_or_none}))
|
webpage = self._download_webpage(url, video_id)
|
||||||
if not format_url:
|
|
||||||
|
info = self._search_regex(
|
||||||
|
r'''(<div\b[^>]*\bid\s*=\s*(['"])pageMetaDetails\2[^>]*>)''',
|
||||||
|
webpage, 'meta details', default='')
|
||||||
|
info = extract_attributes(info)
|
||||||
|
|
||||||
|
player = self._search_regex(
|
||||||
|
r'''(<div\b[^>]*\bid\s*=\s*(['"])rmpPlayerStream\2[^>]*>)''',
|
||||||
|
webpage, 'player details', default='')
|
||||||
|
player = extract_attributes(player)
|
||||||
|
|
||||||
|
video_urls_and_ids = (
|
||||||
|
(info.get('data-meta-video'), 'video'),
|
||||||
|
(player.get('data-video-transcoded'), 'transcoded'),
|
||||||
|
(player.get('data-video-filepath'), 'filepath'),
|
||||||
|
(self._og_search_video_url(webpage, secure=False, default=None), 'og_video'),
|
||||||
|
)
|
||||||
|
|
||||||
|
def txt_or_none(s, default=None):
|
||||||
|
return (s.strip() or default) if isinstance(s, str) else default
|
||||||
|
|
||||||
|
uploader = txt_or_none(info.get('data-meta-author'))
|
||||||
|
|
||||||
|
def mung_title(s):
|
||||||
|
if uploader:
|
||||||
|
s = re.sub(rf'^\s*{re.escape(uploader)}\s+[|-]', '', s)
|
||||||
|
return txt_or_none(s)
|
||||||
|
|
||||||
|
title = (
|
||||||
|
mung_title(info.get('data-meta-title'))
|
||||||
|
or self._html_search_regex(
|
||||||
|
(r'<span[^>]+class=["\']item-title[^>]+>([^<]+)',
|
||||||
|
r'<h2[^>]+class=["\']h2 m-0["\'][^>]*>([^<]+)'),
|
||||||
|
webpage, 'title', default=None)
|
||||||
|
or self._html_search_meta(
|
||||||
|
'twitter:title', webpage, 'title', fatal=True))
|
||||||
|
|
||||||
|
title = re.sub(r'\s*[|-]\s+ManyVids\s*$', '', title) or title
|
||||||
|
|
||||||
|
if any(p in webpage for p in ('preview_videos', '_preview.mp4')):
|
||||||
|
title += ' (Preview)'
|
||||||
|
|
||||||
|
mv_token = self._search_regex(
|
||||||
|
r'data-mvtoken=(["\'])(?P<value>(?:(?!\1).)+)\1', webpage,
|
||||||
|
'mv token', default=None, group='value')
|
||||||
|
|
||||||
|
if mv_token:
|
||||||
|
# Sets some cookies
|
||||||
|
self._download_webpage(
|
||||||
|
'https://www.manyvids.com/includes/ajax_repository/you_had_me_at_hello.php',
|
||||||
|
video_id, note='Setting format cookies', fatal=False,
|
||||||
|
data=urlencode_postdata({
|
||||||
|
'mvtoken': mv_token,
|
||||||
|
'vid': video_id,
|
||||||
|
}), headers={
|
||||||
|
'Referer': url,
|
||||||
|
'X-Requested-With': 'XMLHttpRequest',
|
||||||
|
})
|
||||||
|
|
||||||
|
formats = []
|
||||||
|
for v_url, fmt in video_urls_and_ids:
|
||||||
|
v_url = url_or_none(v_url)
|
||||||
|
if not v_url:
|
||||||
continue
|
continue
|
||||||
if determine_ext(format_url) == 'm3u8':
|
if determine_ext(v_url) == 'm3u8':
|
||||||
formats.extend(self._extract_m3u8_formats(format_url, video_id, 'mp4', m3u8_id=format_id))
|
formats.extend(self._extract_m3u8_formats(
|
||||||
|
v_url, video_id, 'mp4', entry_protocol='m3u8_native',
|
||||||
|
m3u8_id='hls'))
|
||||||
else:
|
else:
|
||||||
formats.append({
|
formats.append({
|
||||||
'url': format_url,
|
'url': v_url,
|
||||||
'format_id': format_id,
|
'format_id': fmt,
|
||||||
'preference': -10 if format_id == 'preview' else None,
|
|
||||||
'quality': 10 if format_id == 'filepath' else None,
|
|
||||||
'height': int_or_none(
|
|
||||||
self._search_regex(r'_(\d{2,3}[02468])_', format_url, 'height', default=None)),
|
|
||||||
})
|
})
|
||||||
if format_id != 'preview':
|
|
||||||
preview_only = False
|
|
||||||
|
|
||||||
metadata = traverse_obj(
|
self._remove_duplicate_formats(formats)
|
||||||
self._download_json(f'{self._API_BASE}/{video_id}', video_id, fatal=False), 'data')
|
|
||||||
title = traverse_obj(metadata, ('title', {clean_html}))
|
|
||||||
|
|
||||||
if preview_only:
|
for f in formats:
|
||||||
title = join_nonempty(title, '(Preview)', delim=' ')
|
if f.get('height') is None:
|
||||||
video_id += '-preview'
|
f['height'] = int_or_none(
|
||||||
self.report_warning(
|
self._search_regex(r'_(\d{2,3}[02468])_', f['url'], 'video height', default=None))
|
||||||
f'Only extracting preview. Video may be paid or subscription only. {self._login_hint()}')
|
if '/preview/' in f['url']:
|
||||||
|
f['format_id'] = '_'.join(filter(None, (f.get('format_id'), 'preview')))
|
||||||
|
f['preference'] = -10
|
||||||
|
if 'transcoded' in f['format_id']:
|
||||||
|
f['preference'] = f.get('preference', -1) - 1
|
||||||
|
|
||||||
|
def get_likes():
|
||||||
|
likes = self._search_regex(
|
||||||
|
rf'''(<a\b[^>]*\bdata-id\s*=\s*(['"]){video_id}\2[^>]*>)''',
|
||||||
|
webpage, 'likes', default='')
|
||||||
|
likes = extract_attributes(likes)
|
||||||
|
return int_or_none(likes.get('data-likes'))
|
||||||
|
|
||||||
|
def get_views():
|
||||||
|
return str_to_int(self._html_search_regex(
|
||||||
|
r'''(?s)<span\b[^>]*\bclass\s*=["']views-wrapper\b[^>]+>.+?<span\b[^>]+>\s*(\d[\d,.]*)\s*</span>''',
|
||||||
|
webpage, 'view count', default=None))
|
||||||
|
|
||||||
return {
|
return {
|
||||||
'id': video_id,
|
'id': video_id,
|
||||||
'title': title,
|
'title': title,
|
||||||
'formats': formats,
|
'formats': formats,
|
||||||
**traverse_obj(metadata, {
|
'description': txt_or_none(info.get('data-meta-description')),
|
||||||
'description': ('description', {clean_html}),
|
'uploader': txt_or_none(info.get('data-meta-author')),
|
||||||
'uploader': ('model', 'displayName', {clean_html}),
|
'thumbnail': (
|
||||||
'thumbnail': (('screenshot', 'thumbnail'), {url_or_none}, any),
|
url_or_none(info.get('data-meta-image'))
|
||||||
'view_count': ('views', {parse_count}),
|
or url_or_none(player.get('data-video-screenshot'))),
|
||||||
'like_count': ('likes', {parse_count}),
|
'view_count': get_views(),
|
||||||
'release_timestamp': ('launchDate', {parse_iso8601}),
|
'like_count': get_likes(),
|
||||||
'duration': ('videoDuration', {parse_duration}),
|
|
||||||
'tags': ('tagList', ..., 'label', {str}, filter, all, filter),
|
|
||||||
}),
|
|
||||||
}
|
}
|
||||||
|
@ -102,10 +102,11 @@ class MedalTVIE(InfoExtractor):
|
|||||||
item_id = item_id or '%dp' % height
|
item_id = item_id or '%dp' % height
|
||||||
if item_id not in item_url:
|
if item_id not in item_url:
|
||||||
return
|
return
|
||||||
|
width = int(round(aspect_ratio * height))
|
||||||
container.append({
|
container.append({
|
||||||
'url': item_url,
|
'url': item_url,
|
||||||
id_key: item_id,
|
id_key: item_id,
|
||||||
'width': round(aspect_ratio * height),
|
'width': width,
|
||||||
'height': height,
|
'height': height,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -4,7 +4,6 @@ from .common import InfoExtractor
|
|||||||
from ..utils import (
|
from ..utils import (
|
||||||
int_or_none,
|
int_or_none,
|
||||||
parse_iso8601,
|
parse_iso8601,
|
||||||
parse_resolution,
|
|
||||||
traverse_obj,
|
traverse_obj,
|
||||||
unified_timestamp,
|
unified_timestamp,
|
||||||
url_basename,
|
url_basename,
|
||||||
@ -84,8 +83,8 @@ class MicrosoftMediusBaseIE(InfoExtractor):
|
|||||||
subtitles.setdefault(sub.pop('tag', 'und'), []).append(sub)
|
subtitles.setdefault(sub.pop('tag', 'und'), []).append(sub)
|
||||||
return subtitles
|
return subtitles
|
||||||
|
|
||||||
def _extract_ism(self, ism_url, video_id, fatal=True):
|
def _extract_ism(self, ism_url, video_id):
|
||||||
formats = self._extract_ism_formats(ism_url, video_id, fatal=fatal)
|
formats = self._extract_ism_formats(ism_url, video_id)
|
||||||
for fmt in formats:
|
for fmt in formats:
|
||||||
if fmt['language'] != 'eng' and 'English' not in fmt['format_id']:
|
if fmt['language'] != 'eng' and 'English' not in fmt['format_id']:
|
||||||
fmt['language_preference'] = -10
|
fmt['language_preference'] = -10
|
||||||
@ -219,21 +218,9 @@ class MicrosoftLearnEpisodeIE(MicrosoftMediusBaseIE):
|
|||||||
'description': 'md5:7bbbfb593d21c2cf2babc3715ade6b88',
|
'description': 'md5:7bbbfb593d21c2cf2babc3715ade6b88',
|
||||||
'timestamp': 1676339547,
|
'timestamp': 1676339547,
|
||||||
'upload_date': '20230214',
|
'upload_date': '20230214',
|
||||||
'thumbnail': r're:https://learn\.microsoft\.com/video/media/.+\.png',
|
'thumbnail': r're:https://learn\.microsoft\.com/video/media/.*\.png',
|
||||||
'subtitles': 'count:14',
|
'subtitles': 'count:14',
|
||||||
},
|
},
|
||||||
}, {
|
|
||||||
'url': 'https://learn.microsoft.com/en-gb/shows/on-demand-instructor-led-training-series/az-900-module-1',
|
|
||||||
'info_dict': {
|
|
||||||
'id': '4fe10f7c-d83c-463b-ac0e-c30a8195e01b',
|
|
||||||
'ext': 'mp4',
|
|
||||||
'title': 'AZ-900 Cloud fundamentals (1 of 6)',
|
|
||||||
'description': 'md5:3c2212ce865e9142f402c766441bd5c9',
|
|
||||||
'thumbnail': r're:https://.+/.+\.jpg',
|
|
||||||
'timestamp': 1706605184,
|
|
||||||
'upload_date': '20240130',
|
|
||||||
},
|
|
||||||
'params': {'format': 'bv[protocol=https]'},
|
|
||||||
}]
|
}]
|
||||||
|
|
||||||
def _real_extract(self, url):
|
def _real_extract(self, url):
|
||||||
@ -243,32 +230,9 @@ class MicrosoftLearnEpisodeIE(MicrosoftMediusBaseIE):
|
|||||||
entry_id = self._html_search_meta('entryId', webpage, 'entryId', fatal=True)
|
entry_id = self._html_search_meta('entryId', webpage, 'entryId', fatal=True)
|
||||||
video_info = self._download_json(
|
video_info = self._download_json(
|
||||||
f'https://learn.microsoft.com/api/video/public/v1/entries/{entry_id}', video_id)
|
f'https://learn.microsoft.com/api/video/public/v1/entries/{entry_id}', video_id)
|
||||||
|
|
||||||
formats = []
|
|
||||||
if ism_url := traverse_obj(video_info, ('publicVideo', 'adaptiveVideoUrl', {url_or_none})):
|
|
||||||
formats.extend(self._extract_ism(ism_url, video_id, fatal=False))
|
|
||||||
if hls_url := traverse_obj(video_info, ('publicVideo', 'adaptiveVideoHLSUrl', {url_or_none})):
|
|
||||||
formats.extend(self._extract_m3u8_formats(hls_url, video_id, 'mp4', m3u8_id='hls', fatal=False))
|
|
||||||
if mpd_url := traverse_obj(video_info, ('publicVideo', 'adaptiveVideoDashUrl', {url_or_none})):
|
|
||||||
formats.extend(self._extract_mpd_formats(mpd_url, video_id, mpd_id='dash', fatal=False))
|
|
||||||
for key in ('low', 'medium', 'high'):
|
|
||||||
if video_url := traverse_obj(video_info, ('publicVideo', f'{key}QualityVideoUrl', {url_or_none})):
|
|
||||||
formats.append({
|
|
||||||
'url': video_url,
|
|
||||||
'format_id': f'video-http-{key}',
|
|
||||||
'acodec': 'none',
|
|
||||||
**parse_resolution(video_url),
|
|
||||||
})
|
|
||||||
if audio_url := traverse_obj(video_info, ('publicVideo', 'audioUrl', {url_or_none})):
|
|
||||||
formats.append({
|
|
||||||
'url': audio_url,
|
|
||||||
'format_id': 'audio-http',
|
|
||||||
'vcodec': 'none',
|
|
||||||
})
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
'id': entry_id,
|
'id': entry_id,
|
||||||
'formats': formats,
|
'formats': self._extract_ism(video_info['publicVideo']['adaptiveVideoUrl'], video_id),
|
||||||
'subtitles': self._sub_to_dict(traverse_obj(video_info, (
|
'subtitles': self._sub_to_dict(traverse_obj(video_info, (
|
||||||
'publicVideo', 'captions', lambda _, v: url_or_none(v['url']), {
|
'publicVideo', 'captions', lambda _, v: url_or_none(v['url']), {
|
||||||
'tag': ('language', {str}),
|
'tag': ('language', {str}),
|
||||||
|
@ -1,7 +1,5 @@
|
|||||||
from .telecinco import TelecincoBaseIE
|
from .telecinco import TelecincoBaseIE
|
||||||
from ..networking.exceptions import HTTPError
|
|
||||||
from ..utils import (
|
from ..utils import (
|
||||||
ExtractorError,
|
|
||||||
int_or_none,
|
int_or_none,
|
||||||
parse_iso8601,
|
parse_iso8601,
|
||||||
)
|
)
|
||||||
@ -81,17 +79,7 @@ class MiTeleIE(TelecincoBaseIE):
|
|||||||
|
|
||||||
def _real_extract(self, url):
|
def _real_extract(self, url):
|
||||||
display_id = self._match_id(url)
|
display_id = self._match_id(url)
|
||||||
|
webpage = self._download_webpage(url, display_id)
|
||||||
try: # yt-dlp's default user-agents are too old and blocked by akamai
|
|
||||||
webpage = self._download_webpage(url, display_id, headers={
|
|
||||||
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; rv:136.0) Gecko/20100101 Firefox/136.0',
|
|
||||||
})
|
|
||||||
except ExtractorError as e:
|
|
||||||
if not isinstance(e.cause, HTTPError) or e.cause.status != 403:
|
|
||||||
raise
|
|
||||||
# Retry with impersonation if hardcoded UA is insufficient to bypass akamai
|
|
||||||
webpage = self._download_webpage(url, display_id, impersonate=True)
|
|
||||||
|
|
||||||
pre_player = self._search_json(
|
pre_player = self._search_json(
|
||||||
r'window\.\$REACTBASE_STATE\.prePlayer_mtweb\s*=',
|
r'window\.\$REACTBASE_STATE\.prePlayer_mtweb\s*=',
|
||||||
webpage, 'Pre Player', display_id)['prePlayer']
|
webpage, 'Pre Player', display_id)['prePlayer']
|
||||||
|
@ -10,9 +10,7 @@ from ..utils import (
|
|||||||
parse_iso8601,
|
parse_iso8601,
|
||||||
strip_or_none,
|
strip_or_none,
|
||||||
try_get,
|
try_get,
|
||||||
url_or_none,
|
|
||||||
)
|
)
|
||||||
from ..utils.traversal import traverse_obj
|
|
||||||
|
|
||||||
|
|
||||||
class MixcloudBaseIE(InfoExtractor):
|
class MixcloudBaseIE(InfoExtractor):
|
||||||
@ -39,7 +37,7 @@ class MixcloudIE(MixcloudBaseIE):
|
|||||||
'ext': 'm4a',
|
'ext': 'm4a',
|
||||||
'title': 'Cryptkeeper',
|
'title': 'Cryptkeeper',
|
||||||
'description': 'After quite a long silence from myself, finally another Drum\'n\'Bass mix with my favourite current dance floor bangers.',
|
'description': 'After quite a long silence from myself, finally another Drum\'n\'Bass mix with my favourite current dance floor bangers.',
|
||||||
'uploader': 'dholbach',
|
'uploader': 'Daniel Holbach',
|
||||||
'uploader_id': 'dholbach',
|
'uploader_id': 'dholbach',
|
||||||
'thumbnail': r're:https?://.*\.jpg',
|
'thumbnail': r're:https?://.*\.jpg',
|
||||||
'view_count': int,
|
'view_count': int,
|
||||||
@ -48,11 +46,10 @@ class MixcloudIE(MixcloudBaseIE):
|
|||||||
'uploader_url': 'https://www.mixcloud.com/dholbach/',
|
'uploader_url': 'https://www.mixcloud.com/dholbach/',
|
||||||
'artist': 'Submorphics & Chino , Telekinesis, Porter Robinson, Enei, Breakage ft Jess Mills',
|
'artist': 'Submorphics & Chino , Telekinesis, Porter Robinson, Enei, Breakage ft Jess Mills',
|
||||||
'duration': 3723,
|
'duration': 3723,
|
||||||
'tags': ['liquid drum and bass', 'drum and bass'],
|
'tags': [],
|
||||||
'comment_count': int,
|
'comment_count': int,
|
||||||
'repost_count': int,
|
'repost_count': int,
|
||||||
'like_count': int,
|
'like_count': int,
|
||||||
'artists': list,
|
|
||||||
},
|
},
|
||||||
'params': {'skip_download': 'm3u8'},
|
'params': {'skip_download': 'm3u8'},
|
||||||
}, {
|
}, {
|
||||||
@ -70,7 +67,7 @@ class MixcloudIE(MixcloudBaseIE):
|
|||||||
'upload_date': '20150203',
|
'upload_date': '20150203',
|
||||||
'uploader_url': 'https://www.mixcloud.com/gillespeterson/',
|
'uploader_url': 'https://www.mixcloud.com/gillespeterson/',
|
||||||
'duration': 2992,
|
'duration': 2992,
|
||||||
'tags': ['jazz', 'soul', 'world music', 'funk'],
|
'tags': [],
|
||||||
'comment_count': int,
|
'comment_count': int,
|
||||||
'repost_count': int,
|
'repost_count': int,
|
||||||
'like_count': int,
|
'like_count': int,
|
||||||
@ -152,6 +149,8 @@ class MixcloudIE(MixcloudBaseIE):
|
|||||||
elif reason:
|
elif reason:
|
||||||
raise ExtractorError('Track is restricted', expected=True)
|
raise ExtractorError('Track is restricted', expected=True)
|
||||||
|
|
||||||
|
title = cloudcast['name']
|
||||||
|
|
||||||
stream_info = cloudcast['streamInfo']
|
stream_info = cloudcast['streamInfo']
|
||||||
formats = []
|
formats = []
|
||||||
|
|
||||||
@ -183,39 +182,47 @@ class MixcloudIE(MixcloudBaseIE):
|
|||||||
self.raise_login_required(metadata_available=True)
|
self.raise_login_required(metadata_available=True)
|
||||||
|
|
||||||
comments = []
|
comments = []
|
||||||
for node in traverse_obj(cloudcast, ('comments', 'edges', ..., 'node', {dict})):
|
for edge in (try_get(cloudcast, lambda x: x['comments']['edges']) or []):
|
||||||
|
node = edge.get('node') or {}
|
||||||
text = strip_or_none(node.get('comment'))
|
text = strip_or_none(node.get('comment'))
|
||||||
if not text:
|
if not text:
|
||||||
continue
|
continue
|
||||||
|
user = node.get('user') or {}
|
||||||
comments.append({
|
comments.append({
|
||||||
|
'author': user.get('displayName'),
|
||||||
|
'author_id': user.get('username'),
|
||||||
'text': text,
|
'text': text,
|
||||||
**traverse_obj(node, {
|
'timestamp': parse_iso8601(node.get('created')),
|
||||||
'author': ('user', 'displayName', {str}),
|
|
||||||
'author_id': ('user', 'username', {str}),
|
|
||||||
'timestamp': ('created', {parse_iso8601}),
|
|
||||||
}),
|
|
||||||
})
|
})
|
||||||
|
|
||||||
|
tags = []
|
||||||
|
for t in cloudcast.get('tags'):
|
||||||
|
tag = try_get(t, lambda x: x['tag']['name'], str)
|
||||||
|
if not tag:
|
||||||
|
tags.append(tag)
|
||||||
|
|
||||||
|
get_count = lambda x: int_or_none(try_get(cloudcast, lambda y: y[x]['totalCount']))
|
||||||
|
|
||||||
|
owner = cloudcast.get('owner') or {}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
'id': track_id,
|
'id': track_id,
|
||||||
|
'title': title,
|
||||||
'formats': formats,
|
'formats': formats,
|
||||||
|
'description': cloudcast.get('description'),
|
||||||
|
'thumbnail': try_get(cloudcast, lambda x: x['picture']['url'], str),
|
||||||
|
'uploader': owner.get('displayName'),
|
||||||
|
'timestamp': parse_iso8601(cloudcast.get('publishDate')),
|
||||||
|
'uploader_id': owner.get('username'),
|
||||||
|
'uploader_url': owner.get('url'),
|
||||||
|
'duration': int_or_none(cloudcast.get('audioLength')),
|
||||||
|
'view_count': int_or_none(cloudcast.get('plays')),
|
||||||
|
'like_count': get_count('favorites'),
|
||||||
|
'repost_count': get_count('reposts'),
|
||||||
|
'comment_count': get_count('comments'),
|
||||||
'comments': comments,
|
'comments': comments,
|
||||||
**traverse_obj(cloudcast, {
|
'tags': tags,
|
||||||
'title': ('name', {str}),
|
'artist': ', '.join(cloudcast.get('featuringArtistList') or []) or None,
|
||||||
'description': ('description', {str}),
|
|
||||||
'thumbnail': ('picture', 'url', {url_or_none}),
|
|
||||||
'timestamp': ('publishDate', {parse_iso8601}),
|
|
||||||
'duration': ('audioLength', {int_or_none}),
|
|
||||||
'uploader': ('owner', 'displayName', {str}),
|
|
||||||
'uploader_id': ('owner', 'username', {str}),
|
|
||||||
'uploader_url': ('owner', 'url', {url_or_none}),
|
|
||||||
'view_count': ('plays', {int_or_none}),
|
|
||||||
'like_count': ('favorites', 'totalCount', {int_or_none}),
|
|
||||||
'repost_count': ('reposts', 'totalCount', {int_or_none}),
|
|
||||||
'comment_count': ('comments', 'totalCount', {int_or_none}),
|
|
||||||
'tags': ('tags', ..., 'tag', 'name', {str}, filter, all, filter),
|
|
||||||
'artists': ('featuringArtistList', ..., {str}, filter, all, filter),
|
|
||||||
}),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -288,7 +295,7 @@ class MixcloudUserIE(MixcloudPlaylistBaseIE):
|
|||||||
'url': 'http://www.mixcloud.com/dholbach/',
|
'url': 'http://www.mixcloud.com/dholbach/',
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
'id': 'dholbach_uploads',
|
'id': 'dholbach_uploads',
|
||||||
'title': 'dholbach (uploads)',
|
'title': 'Daniel Holbach (uploads)',
|
||||||
'description': 'md5:a3f468a60ac8c3e1f8616380fc469b2b',
|
'description': 'md5:a3f468a60ac8c3e1f8616380fc469b2b',
|
||||||
},
|
},
|
||||||
'playlist_mincount': 36,
|
'playlist_mincount': 36,
|
||||||
@ -296,7 +303,7 @@ class MixcloudUserIE(MixcloudPlaylistBaseIE):
|
|||||||
'url': 'http://www.mixcloud.com/dholbach/uploads/',
|
'url': 'http://www.mixcloud.com/dholbach/uploads/',
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
'id': 'dholbach_uploads',
|
'id': 'dholbach_uploads',
|
||||||
'title': 'dholbach (uploads)',
|
'title': 'Daniel Holbach (uploads)',
|
||||||
'description': 'md5:a3f468a60ac8c3e1f8616380fc469b2b',
|
'description': 'md5:a3f468a60ac8c3e1f8616380fc469b2b',
|
||||||
},
|
},
|
||||||
'playlist_mincount': 36,
|
'playlist_mincount': 36,
|
||||||
@ -304,7 +311,7 @@ class MixcloudUserIE(MixcloudPlaylistBaseIE):
|
|||||||
'url': 'http://www.mixcloud.com/dholbach/favorites/',
|
'url': 'http://www.mixcloud.com/dholbach/favorites/',
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
'id': 'dholbach_favorites',
|
'id': 'dholbach_favorites',
|
||||||
'title': 'dholbach (favorites)',
|
'title': 'Daniel Holbach (favorites)',
|
||||||
'description': 'md5:a3f468a60ac8c3e1f8616380fc469b2b',
|
'description': 'md5:a3f468a60ac8c3e1f8616380fc469b2b',
|
||||||
},
|
},
|
||||||
# 'params': {
|
# 'params': {
|
||||||
@ -330,7 +337,7 @@ class MixcloudUserIE(MixcloudPlaylistBaseIE):
|
|||||||
'title': 'First Ear (stream)',
|
'title': 'First Ear (stream)',
|
||||||
'description': 'we maraud for ears',
|
'description': 'we maraud for ears',
|
||||||
},
|
},
|
||||||
'playlist_mincount': 267,
|
'playlist_mincount': 269,
|
||||||
}]
|
}]
|
||||||
|
|
||||||
_TITLE_KEY = 'displayName'
|
_TITLE_KEY = 'displayName'
|
||||||
@ -354,7 +361,7 @@ class MixcloudPlaylistIE(MixcloudPlaylistBaseIE):
|
|||||||
'id': 'maxvibes_jazzcat-on-ness-radio',
|
'id': 'maxvibes_jazzcat-on-ness-radio',
|
||||||
'title': 'Ness Radio sessions',
|
'title': 'Ness Radio sessions',
|
||||||
},
|
},
|
||||||
'playlist_mincount': 58,
|
'playlist_mincount': 59,
|
||||||
}]
|
}]
|
||||||
_TITLE_KEY = 'name'
|
_TITLE_KEY = 'name'
|
||||||
_DESCRIPTION_KEY = 'description'
|
_DESCRIPTION_KEY = 'description'
|
||||||
|
@ -365,15 +365,13 @@ mutation initPlaybackSession(
|
|||||||
'All videos are only available to registered users', method='password')
|
'All videos are only available to registered users', method='password')
|
||||||
|
|
||||||
def _set_device_id(self, username):
|
def _set_device_id(self, username):
|
||||||
if self._device_id:
|
if not self._device_id:
|
||||||
return
|
self._device_id = self.cache.load(
|
||||||
device_id_cache = self.cache.load(self._NETRC_MACHINE, 'device_ids', default={})
|
self._NETRC_MACHINE, 'device_ids', default={}).get(username)
|
||||||
self._device_id = device_id_cache.get(username)
|
|
||||||
if self._device_id:
|
if self._device_id:
|
||||||
return
|
return
|
||||||
self._device_id = str(uuid.uuid4())
|
self._device_id = str(uuid.uuid4())
|
||||||
device_id_cache[username] = self._device_id
|
self.cache.store(self._NETRC_MACHINE, 'device_ids', {username: self._device_id})
|
||||||
self.cache.store(self._NETRC_MACHINE, 'device_ids', device_id_cache)
|
|
||||||
|
|
||||||
def _perform_login(self, username, password):
|
def _perform_login(self, username, password):
|
||||||
try:
|
try:
|
||||||
@ -451,7 +449,9 @@ mutation initPlaybackSession(
|
|||||||
|
|
||||||
if not (m3u8_url and token):
|
if not (m3u8_url and token):
|
||||||
errors = '; '.join(traverse_obj(response, ('errors', ..., 'message', {str})))
|
errors = '; '.join(traverse_obj(response, ('errors', ..., 'message', {str})))
|
||||||
if errors: # Only warn when 'blacked out' or 'not entitled'; radio formats may be available
|
if 'not entitled' in errors:
|
||||||
|
raise ExtractorError(errors, expected=True)
|
||||||
|
elif errors: # Only warn when 'blacked out' since radio formats are available
|
||||||
self.report_warning(f'API returned errors for {format_id}: {errors}')
|
self.report_warning(f'API returned errors for {format_id}: {errors}')
|
||||||
else:
|
else:
|
||||||
self.report_warning(f'No formats available for {format_id} broadcast; skipping')
|
self.report_warning(f'No formats available for {format_id} broadcast; skipping')
|
||||||
|
@ -3,8 +3,8 @@ from .dailymotion import DailymotionIE
|
|||||||
|
|
||||||
|
|
||||||
class MoviepilotIE(InfoExtractor):
|
class MoviepilotIE(InfoExtractor):
|
||||||
IE_NAME = 'moviepilot'
|
_IE_NAME = 'moviepilot'
|
||||||
IE_DESC = 'Moviepilot trailer'
|
_IE_DESC = 'Moviepilot trailer'
|
||||||
_VALID_URL = r'https?://(?:www\.)?moviepilot\.de/movies/(?P<id>[^/]+)'
|
_VALID_URL = r'https?://(?:www\.)?moviepilot\.de/movies/(?P<id>[^/]+)'
|
||||||
|
|
||||||
_TESTS = [{
|
_TESTS = [{
|
||||||
|
@ -1,215 +1,167 @@
|
|||||||
|
import re
|
||||||
|
|
||||||
from .common import InfoExtractor
|
from .common import InfoExtractor
|
||||||
from ..utils import (
|
from ..utils import (
|
||||||
ExtractorError,
|
ExtractorError,
|
||||||
clean_html,
|
|
||||||
determine_ext,
|
determine_ext,
|
||||||
int_or_none,
|
int_or_none,
|
||||||
parse_iso8601,
|
unescapeHTML,
|
||||||
url_or_none,
|
|
||||||
)
|
)
|
||||||
from ..utils.traversal import traverse_obj
|
|
||||||
|
|
||||||
|
|
||||||
class MSNIE(InfoExtractor):
|
class MSNIE(InfoExtractor):
|
||||||
_VALID_URL = r'https?://(?:(?:www|preview)\.)?msn\.com/(?P<locale>[a-z]{2}-[a-z]{2})/(?:[^/?#]+/)+(?P<display_id>[^/?#]+)/[a-z]{2}-(?P<id>[\da-zA-Z]+)'
|
_WORKING = False
|
||||||
|
_VALID_URL = r'https?://(?:(?:www|preview)\.)?msn\.com/(?:[^/]+/)+(?P<display_id>[^/]+)/[a-z]{2}-(?P<id>[\da-zA-Z]+)'
|
||||||
_TESTS = [{
|
_TESTS = [{
|
||||||
'url': 'https://www.msn.com/en-gb/video/news/president-macron-interrupts-trump-over-ukraine-funding/vi-AA1zMcD7',
|
'url': 'https://www.msn.com/en-in/money/video/7-ways-to-get-rid-of-chest-congestion/vi-BBPxU6d',
|
||||||
|
'md5': '087548191d273c5c55d05028f8d2cbcd',
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
'id': 'AA1zMcD7',
|
'id': 'BBPxU6d',
|
||||||
|
'display_id': '7-ways-to-get-rid-of-chest-congestion',
|
||||||
'ext': 'mp4',
|
'ext': 'mp4',
|
||||||
'display_id': 'president-macron-interrupts-trump-over-ukraine-funding',
|
'title': 'Seven ways to get rid of chest congestion',
|
||||||
'title': 'President Macron interrupts Trump over Ukraine funding',
|
'description': '7 Ways to Get Rid of Chest Congestion',
|
||||||
'description': 'md5:5fd3857ac25849e7a56cb25fbe1a2a8b',
|
'duration': 88,
|
||||||
'uploader': 'k! News UK',
|
'uploader': 'Health',
|
||||||
'uploader_id': 'BB1hz5Rj',
|
'uploader_id': 'BBPrMqa',
|
||||||
'duration': 59,
|
|
||||||
'thumbnail': 'https://img-s-msn-com.akamaized.net/tenant/amp/entityid/AA1zMagX.img',
|
|
||||||
'tags': 'count:14',
|
|
||||||
'timestamp': 1740510914,
|
|
||||||
'upload_date': '20250225',
|
|
||||||
'release_timestamp': 1740513600,
|
|
||||||
'release_date': '20250225',
|
|
||||||
'modified_timestamp': 1741413241,
|
|
||||||
'modified_date': '20250308',
|
|
||||||
},
|
},
|
||||||
}, {
|
}, {
|
||||||
'url': 'https://www.msn.com/en-gb/video/watch/films-success-saved-adam-pearsons-acting-career/vi-AA1znZGE?ocid=hpmsn',
|
# Article, multiple Dailymotion Embeds
|
||||||
|
'url': 'https://www.msn.com/en-in/money/sports/hottest-football-wags-greatest-footballers-turned-managers-and-more/ar-BBpc7Nl',
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
'id': 'AA1znZGE',
|
'id': 'BBpc7Nl',
|
||||||
'ext': 'mp4',
|
|
||||||
'display_id': 'films-success-saved-adam-pearsons-acting-career',
|
|
||||||
'title': "Films' success saved Adam Pearson's acting career",
|
|
||||||
'description': 'md5:98c05f7bd9ab4f9c423400f62f2d3da5',
|
|
||||||
'uploader': 'Sky News',
|
|
||||||
'uploader_id': 'AA2eki',
|
|
||||||
'duration': 52,
|
|
||||||
'thumbnail': 'https://img-s-msn-com.akamaized.net/tenant/amp/entityid/AA1zo7nU.img',
|
|
||||||
'timestamp': 1739993965,
|
|
||||||
'upload_date': '20250219',
|
|
||||||
'release_timestamp': 1739977753,
|
|
||||||
'release_date': '20250219',
|
|
||||||
'modified_timestamp': 1742076259,
|
|
||||||
'modified_date': '20250315',
|
|
||||||
},
|
},
|
||||||
|
'playlist_mincount': 4,
|
||||||
}, {
|
}, {
|
||||||
'url': 'https://www.msn.com/en-us/entertainment/news/rock-frontman-replacements-you-might-not-know-happened/vi-AA1yLVcD',
|
'url': 'http://www.msn.com/en-ae/news/offbeat/meet-the-nine-year-old-self-made-millionaire/ar-BBt6ZKf',
|
||||||
'info_dict': {
|
'only_matching': True,
|
||||||
'id': 'AA1yLVcD',
|
}, {
|
||||||
'ext': 'mp4',
|
'url': 'http://www.msn.com/en-ae/video/watch/obama-a-lot-of-people-will-be-disappointed/vi-AAhxUMH',
|
||||||
'display_id': 'rock-frontman-replacements-you-might-not-know-happened',
|
'only_matching': True,
|
||||||
'title': 'Rock Frontman Replacements You Might Not Know Happened',
|
}, {
|
||||||
'description': 'md5:451a125496ff0c9f6816055bb1808da9',
|
# geo restricted
|
||||||
'uploader': 'Grunge (Video)',
|
'url': 'http://www.msn.com/en-ae/foodanddrink/joinourtable/the-first-fart-makes-you-laugh-the-last-fart-makes-you-cry/vp-AAhzIBU',
|
||||||
'uploader_id': 'BB1oveoV',
|
'only_matching': True,
|
||||||
'duration': 596,
|
}, {
|
||||||
'thumbnail': 'https://img-s-msn-com.akamaized.net/tenant/amp/entityid/AA1yM4OJ.img',
|
'url': 'http://www.msn.com/en-ae/entertainment/bollywood/watch-how-salman-khan-reacted-when-asked-if-he-would-apologize-for-his-‘raped-woman’-comment/vi-AAhvzW6',
|
||||||
'timestamp': 1739223456,
|
'only_matching': True,
|
||||||
'upload_date': '20250210',
|
}, {
|
||||||
'release_timestamp': 1739219731,
|
# Vidible(AOL) Embed
|
||||||
'release_date': '20250210',
|
'url': 'https://www.msn.com/en-us/money/other/jupiter-is-about-to-come-so-close-you-can-see-its-moons-with-binoculars/vi-AACqsHR',
|
||||||
'modified_timestamp': 1741427272,
|
'only_matching': True,
|
||||||
'modified_date': '20250308',
|
|
||||||
},
|
|
||||||
}, {
|
}, {
|
||||||
# Dailymotion Embed
|
# Dailymotion Embed
|
||||||
'url': 'https://www.msn.com/de-de/nachrichten/other/the-first-descendant-gameplay-trailer-zu-serena-der-neuen-gefl%C3%BCgelten-nachfahrin/vi-AA1B1d06',
|
'url': 'https://www.msn.com/es-ve/entretenimiento/watch/winston-salem-paire-refait-des-siennes-en-perdant-sa-raquette-au-service/vp-AAG704L',
|
||||||
'info_dict': {
|
'only_matching': True,
|
||||||
'id': 'x9g6oli',
|
|
||||||
'ext': 'mp4',
|
|
||||||
'title': 'The First Descendant: Gameplay-Trailer zu Serena, der neuen geflügelten Nachfahrin',
|
|
||||||
'description': '',
|
|
||||||
'uploader': 'MeinMMO',
|
|
||||||
'uploader_id': 'x2mvqi4',
|
|
||||||
'view_count': int,
|
|
||||||
'like_count': int,
|
|
||||||
'age_limit': 0,
|
|
||||||
'duration': 60,
|
|
||||||
'thumbnail': 'https://s1.dmcdn.net/v/Y3fO61drj56vPB9SS/x1080',
|
|
||||||
'tags': ['MeinMMO', 'The First Descendant'],
|
|
||||||
'timestamp': 1742124877,
|
|
||||||
'upload_date': '20250316',
|
|
||||||
},
|
|
||||||
}, {
|
}, {
|
||||||
# Youtube Embed
|
# YouTube Embed
|
||||||
'url': 'https://www.msn.com/en-gb/video/webcontent/web-content/vi-AA1ybFaJ',
|
'url': 'https://www.msn.com/en-in/money/news/meet-vikram-%E2%80%94-chandrayaan-2s-lander/vi-AAGUr0v',
|
||||||
'info_dict': {
|
'only_matching': True,
|
||||||
'id': 'kQSChWu95nE',
|
|
||||||
'ext': 'mp4',
|
|
||||||
'title': '7 Daily Habits to Nurture Your Personal Growth',
|
|
||||||
'description': 'md5:6f233c68341b74dee30c8c121924e827',
|
|
||||||
'uploader': 'TopThink',
|
|
||||||
'uploader_id': '@TopThink',
|
|
||||||
'uploader_url': 'https://www.youtube.com/@TopThink',
|
|
||||||
'channel': 'TopThink',
|
|
||||||
'channel_id': 'UCMlGmHokrQRp-RaNO7aq4Uw',
|
|
||||||
'channel_url': 'https://www.youtube.com/channel/UCMlGmHokrQRp-RaNO7aq4Uw',
|
|
||||||
'channel_is_verified': True,
|
|
||||||
'channel_follower_count': int,
|
|
||||||
'comment_count': int,
|
|
||||||
'view_count': int,
|
|
||||||
'like_count': int,
|
|
||||||
'age_limit': 0,
|
|
||||||
'duration': 705,
|
|
||||||
'thumbnail': 'https://i.ytimg.com/vi/kQSChWu95nE/maxresdefault.jpg',
|
|
||||||
'categories': ['Howto & Style'],
|
|
||||||
'tags': ['topthink', 'top think', 'personal growth'],
|
|
||||||
'timestamp': 1722711620,
|
|
||||||
'upload_date': '20240803',
|
|
||||||
'playable_in_embed': True,
|
|
||||||
'availability': 'public',
|
|
||||||
'live_status': 'not_live',
|
|
||||||
},
|
|
||||||
}, {
|
}, {
|
||||||
# Article with social embed
|
# NBCSports Embed
|
||||||
'url': 'https://www.msn.com/en-in/news/techandscience/watch-earth-sets-and-rises-behind-moon-in-breathtaking-blue-ghost-video/ar-AA1zKoAc',
|
'url': 'https://www.msn.com/en-us/money/football_nfl/week-13-preview-redskins-vs-panthers/vi-BBXsCDb',
|
||||||
'info_dict': {
|
'only_matching': True,
|
||||||
'id': 'AA1zKoAc',
|
|
||||||
'title': 'Watch: Earth sets and rises behind Moon in breathtaking Blue Ghost video',
|
|
||||||
'description': 'md5:0ad51cfa77e42e7f0c46cf98a619dbbf',
|
|
||||||
'uploader': 'India Today',
|
|
||||||
'uploader_id': 'AAyFWG',
|
|
||||||
'tags': 'count:11',
|
|
||||||
'timestamp': 1740485034,
|
|
||||||
'upload_date': '20250225',
|
|
||||||
'release_timestamp': 1740484875,
|
|
||||||
'release_date': '20250225',
|
|
||||||
'modified_timestamp': 1740488561,
|
|
||||||
'modified_date': '20250225',
|
|
||||||
},
|
|
||||||
'playlist_count': 1,
|
|
||||||
}]
|
}]
|
||||||
|
|
||||||
def _real_extract(self, url):
|
def _real_extract(self, url):
|
||||||
locale, display_id, page_id = self._match_valid_url(url).group('locale', 'display_id', 'id')
|
display_id, page_id = self._match_valid_url(url).groups()
|
||||||
|
|
||||||
json_data = self._download_json(
|
webpage = self._download_webpage(url, display_id)
|
||||||
f'https://assets.msn.com/content/view/v2/Detail/{locale}/{page_id}', page_id)
|
|
||||||
|
|
||||||
common_metadata = traverse_obj(json_data, {
|
|
||||||
'title': ('title', {str}),
|
|
||||||
'description': (('abstract', ('body', {clean_html})), {str}, filter, any),
|
|
||||||
'timestamp': ('createdDateTime', {parse_iso8601}),
|
|
||||||
'release_timestamp': ('publishedDateTime', {parse_iso8601}),
|
|
||||||
'modified_timestamp': ('updatedDateTime', {parse_iso8601}),
|
|
||||||
'thumbnail': ('thumbnail', 'image', 'url', {url_or_none}),
|
|
||||||
'duration': ('videoMetadata', 'playTime', {int_or_none}),
|
|
||||||
'tags': ('keywords', ..., {str}),
|
|
||||||
'uploader': ('provider', 'name', {str}),
|
|
||||||
'uploader_id': ('provider', 'id', {str}),
|
|
||||||
})
|
|
||||||
|
|
||||||
page_type = json_data['type']
|
|
||||||
source_url = traverse_obj(json_data, ('sourceHref', {url_or_none}))
|
|
||||||
if page_type == 'video':
|
|
||||||
if traverse_obj(json_data, ('thirdPartyVideoPlayer', 'enabled')) and source_url:
|
|
||||||
return self.url_result(source_url)
|
|
||||||
formats = []
|
|
||||||
subtitles = {}
|
|
||||||
for file in traverse_obj(json_data, ('videoMetadata', 'externalVideoFiles', lambda _, v: url_or_none(v['url']))):
|
|
||||||
file_url = file['url']
|
|
||||||
ext = determine_ext(file_url)
|
|
||||||
if ext == 'm3u8':
|
|
||||||
fmts, subs = self._extract_m3u8_formats_and_subtitles(
|
|
||||||
file_url, page_id, 'mp4', m3u8_id='hls', fatal=False)
|
|
||||||
formats.extend(fmts)
|
|
||||||
self._merge_subtitles(subs, target=subtitles)
|
|
||||||
elif ext == 'mpd':
|
|
||||||
fmts, subs = self._extract_mpd_formats_and_subtitles(
|
|
||||||
file_url, page_id, mpd_id='dash', fatal=False)
|
|
||||||
formats.extend(fmts)
|
|
||||||
self._merge_subtitles(subs, target=subtitles)
|
|
||||||
else:
|
|
||||||
formats.append(
|
|
||||||
traverse_obj(file, {
|
|
||||||
'url': 'url',
|
|
||||||
'format_id': ('format', {str}),
|
|
||||||
'filesize': ('fileSize', {int_or_none}),
|
|
||||||
'height': ('height', {int_or_none}),
|
|
||||||
'width': ('width', {int_or_none}),
|
|
||||||
}))
|
|
||||||
for caption in traverse_obj(json_data, ('videoMetadata', 'closedCaptions', lambda _, v: url_or_none(v['href']))):
|
|
||||||
lang = caption.get('locale') or 'en-us'
|
|
||||||
subtitles.setdefault(lang, []).append({
|
|
||||||
'url': caption['href'],
|
|
||||||
'ext': 'ttml',
|
|
||||||
})
|
|
||||||
|
|
||||||
return {
|
|
||||||
'id': page_id,
|
|
||||||
'display_id': display_id,
|
|
||||||
'formats': formats,
|
|
||||||
'subtitles': subtitles,
|
|
||||||
**common_metadata,
|
|
||||||
}
|
|
||||||
elif page_type == 'webcontent':
|
|
||||||
if not source_url:
|
|
||||||
raise ExtractorError('Could not find source URL')
|
|
||||||
return self.url_result(source_url)
|
|
||||||
elif page_type == 'article':
|
|
||||||
entries = []
|
entries = []
|
||||||
for embed_url in traverse_obj(json_data, ('socialEmbeds', ..., 'postUrl', {url_or_none})):
|
for _, metadata in re.findall(r'data-metadata\s*=\s*(["\'])(?P<data>.+?)\1', webpage):
|
||||||
entries.append(self.url_result(embed_url))
|
video = self._parse_json(unescapeHTML(metadata), display_id)
|
||||||
|
|
||||||
return self.playlist_result(entries, page_id, **common_metadata)
|
provider_id = video.get('providerId')
|
||||||
|
player_name = video.get('playerName')
|
||||||
|
if player_name and provider_id:
|
||||||
|
entry = None
|
||||||
|
if player_name == 'AOL':
|
||||||
|
if provider_id.startswith('http'):
|
||||||
|
provider_id = self._search_regex(
|
||||||
|
r'https?://delivery\.vidible\.tv/video/redirect/([0-9a-f]{24})',
|
||||||
|
provider_id, 'vidible id')
|
||||||
|
entry = self.url_result(
|
||||||
|
'aol-video:' + provider_id, 'Aol', provider_id)
|
||||||
|
elif player_name == 'Dailymotion':
|
||||||
|
entry = self.url_result(
|
||||||
|
'https://www.dailymotion.com/video/' + provider_id,
|
||||||
|
'Dailymotion', provider_id)
|
||||||
|
elif player_name == 'YouTube':
|
||||||
|
entry = self.url_result(
|
||||||
|
provider_id, 'Youtube', provider_id)
|
||||||
|
elif player_name == 'NBCSports':
|
||||||
|
entry = self.url_result(
|
||||||
|
'http://vplayer.nbcsports.com/p/BxmELC/nbcsports_embed/select/media/' + provider_id,
|
||||||
|
'NBCSportsVPlayer', provider_id)
|
||||||
|
if entry:
|
||||||
|
entries.append(entry)
|
||||||
|
continue
|
||||||
|
|
||||||
raise ExtractorError(f'Unsupported page type: {page_type}')
|
video_id = video['uuid']
|
||||||
|
title = video['title']
|
||||||
|
|
||||||
|
formats = []
|
||||||
|
for file_ in video.get('videoFiles', []):
|
||||||
|
format_url = file_.get('url')
|
||||||
|
if not format_url:
|
||||||
|
continue
|
||||||
|
if 'format=m3u8-aapl' in format_url:
|
||||||
|
# m3u8_native should not be used here until
|
||||||
|
# https://github.com/ytdl-org/youtube-dl/issues/9913 is fixed
|
||||||
|
formats.extend(self._extract_m3u8_formats(
|
||||||
|
format_url, display_id, 'mp4',
|
||||||
|
m3u8_id='hls', fatal=False))
|
||||||
|
elif 'format=mpd-time-csf' in format_url:
|
||||||
|
formats.extend(self._extract_mpd_formats(
|
||||||
|
format_url, display_id, 'dash', fatal=False))
|
||||||
|
elif '.ism' in format_url:
|
||||||
|
if format_url.endswith('.ism'):
|
||||||
|
format_url += '/manifest'
|
||||||
|
formats.extend(self._extract_ism_formats(
|
||||||
|
format_url, display_id, 'mss', fatal=False))
|
||||||
|
else:
|
||||||
|
format_id = file_.get('formatCode')
|
||||||
|
formats.append({
|
||||||
|
'url': format_url,
|
||||||
|
'ext': 'mp4',
|
||||||
|
'format_id': format_id,
|
||||||
|
'width': int_or_none(file_.get('width')),
|
||||||
|
'height': int_or_none(file_.get('height')),
|
||||||
|
'vbr': int_or_none(self._search_regex(r'_(\d+)\.mp4', format_url, 'vbr', default=None)),
|
||||||
|
'quality': 1 if format_id == '1001' else None,
|
||||||
|
})
|
||||||
|
|
||||||
|
subtitles = {}
|
||||||
|
for file_ in video.get('files', []):
|
||||||
|
format_url = file_.get('url')
|
||||||
|
format_code = file_.get('formatCode')
|
||||||
|
if not format_url or not format_code:
|
||||||
|
continue
|
||||||
|
if str(format_code) == '3100':
|
||||||
|
subtitles.setdefault(file_.get('culture', 'en'), []).append({
|
||||||
|
'ext': determine_ext(format_url, 'ttml'),
|
||||||
|
'url': format_url,
|
||||||
|
})
|
||||||
|
|
||||||
|
entries.append({
|
||||||
|
'id': video_id,
|
||||||
|
'display_id': display_id,
|
||||||
|
'title': title,
|
||||||
|
'description': video.get('description'),
|
||||||
|
'thumbnail': video.get('headlineImage', {}).get('url'),
|
||||||
|
'duration': int_or_none(video.get('durationSecs')),
|
||||||
|
'uploader': video.get('sourceFriendly'),
|
||||||
|
'uploader_id': video.get('providerId'),
|
||||||
|
'creator': video.get('creator'),
|
||||||
|
'subtitles': subtitles,
|
||||||
|
'formats': formats,
|
||||||
|
})
|
||||||
|
|
||||||
|
if not entries:
|
||||||
|
error = unescapeHTML(self._search_regex(
|
||||||
|
r'data-error=(["\'])(?P<error>.+?)\1',
|
||||||
|
webpage, 'error', group='error'))
|
||||||
|
raise ExtractorError(f'{self.IE_NAME} said: {error}', expected=True)
|
||||||
|
|
||||||
|
return self.playlist_result(entries, page_id)
|
||||||
|
@ -4,9 +4,7 @@ from .common import InfoExtractor
|
|||||||
from ..utils import (
|
from ..utils import (
|
||||||
extract_attributes,
|
extract_attributes,
|
||||||
unified_timestamp,
|
unified_timestamp,
|
||||||
url_or_none,
|
|
||||||
)
|
)
|
||||||
from ..utils.traversal import traverse_obj
|
|
||||||
|
|
||||||
|
|
||||||
class N1InfoAssetIE(InfoExtractor):
|
class N1InfoAssetIE(InfoExtractor):
|
||||||
@ -37,9 +35,9 @@ class N1InfoIIE(InfoExtractor):
|
|||||||
IE_NAME = 'N1Info:article'
|
IE_NAME = 'N1Info:article'
|
||||||
_VALID_URL = r'https?://(?:(?:\w+\.)?n1info\.\w+|nova\.rs)/(?:[^/?#]+/){1,2}(?P<id>[^/?#]+)'
|
_VALID_URL = r'https?://(?:(?:\w+\.)?n1info\.\w+|nova\.rs)/(?:[^/?#]+/){1,2}(?P<id>[^/?#]+)'
|
||||||
_TESTS = [{
|
_TESTS = [{
|
||||||
# YouTube embedded
|
# Youtube embedded
|
||||||
'url': 'https://rs.n1info.com/sport-klub/tenis/kako-je-djokovic-propustio-istorijsku-priliku-video/',
|
'url': 'https://rs.n1info.com/sport-klub/tenis/kako-je-djokovic-propustio-istorijsku-priliku-video/',
|
||||||
'md5': '987ce6fd72acfecc453281e066b87973',
|
'md5': '01ddb6646d0fd9c4c7d990aa77fe1c5a',
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
'id': 'L5Hd4hQVUpk',
|
'id': 'L5Hd4hQVUpk',
|
||||||
'ext': 'mp4',
|
'ext': 'mp4',
|
||||||
@ -47,26 +45,7 @@ class N1InfoIIE(InfoExtractor):
|
|||||||
'title': 'Ozmo i USO21, ep. 13: Novak Đoković – Danil Medvedev | Ključevi Poraza, Budućnost | SPORT KLUB TENIS',
|
'title': 'Ozmo i USO21, ep. 13: Novak Đoković – Danil Medvedev | Ključevi Poraza, Budućnost | SPORT KLUB TENIS',
|
||||||
'description': 'md5:467f330af1effedd2e290f10dc31bb8e',
|
'description': 'md5:467f330af1effedd2e290f10dc31bb8e',
|
||||||
'uploader': 'Sport Klub',
|
'uploader': 'Sport Klub',
|
||||||
'uploader_id': '@sportklub',
|
'uploader_id': 'sportklub',
|
||||||
'uploader_url': 'https://www.youtube.com/@sportklub',
|
|
||||||
'channel': 'Sport Klub',
|
|
||||||
'channel_id': 'UChpzBje9Ro6CComXe3BgNaw',
|
|
||||||
'channel_url': 'https://www.youtube.com/channel/UChpzBje9Ro6CComXe3BgNaw',
|
|
||||||
'channel_is_verified': True,
|
|
||||||
'channel_follower_count': int,
|
|
||||||
'comment_count': int,
|
|
||||||
'view_count': int,
|
|
||||||
'like_count': int,
|
|
||||||
'age_limit': 0,
|
|
||||||
'duration': 1049,
|
|
||||||
'thumbnail': 'https://i.ytimg.com/vi/L5Hd4hQVUpk/maxresdefault.jpg',
|
|
||||||
'chapters': 'count:9',
|
|
||||||
'categories': ['Sports'],
|
|
||||||
'tags': 'count:10',
|
|
||||||
'timestamp': 1631522787,
|
|
||||||
'playable_in_embed': True,
|
|
||||||
'availability': 'public',
|
|
||||||
'live_status': 'not_live',
|
|
||||||
},
|
},
|
||||||
}, {
|
}, {
|
||||||
'url': 'https://rs.n1info.com/vesti/djilas-los-plan-za-metro-nece-resiti-nijedan-saobracajni-problem/',
|
'url': 'https://rs.n1info.com/vesti/djilas-los-plan-za-metro-nece-resiti-nijedan-saobracajni-problem/',
|
||||||
@ -76,7 +55,6 @@ class N1InfoIIE(InfoExtractor):
|
|||||||
'title': 'Đilas: Predlog izgradnje metroa besmislen; SNS odbacuje navode',
|
'title': 'Đilas: Predlog izgradnje metroa besmislen; SNS odbacuje navode',
|
||||||
'upload_date': '20210924',
|
'upload_date': '20210924',
|
||||||
'timestamp': 1632481347,
|
'timestamp': 1632481347,
|
||||||
'thumbnail': 'http://n1info.rs/wp-content/themes/ucnewsportal-n1/dist/assets/images/placeholder-image-video.jpg',
|
|
||||||
},
|
},
|
||||||
'params': {
|
'params': {
|
||||||
'skip_download': True,
|
'skip_download': True,
|
||||||
@ -89,7 +67,6 @@ class N1InfoIIE(InfoExtractor):
|
|||||||
'title': 'Zadnji dnevi na kopališču Ilirija: “Ilirija ni umrla, ubili so jo”',
|
'title': 'Zadnji dnevi na kopališču Ilirija: “Ilirija ni umrla, ubili so jo”',
|
||||||
'timestamp': 1632567630,
|
'timestamp': 1632567630,
|
||||||
'upload_date': '20210925',
|
'upload_date': '20210925',
|
||||||
'thumbnail': 'https://n1info.si/wp-content/uploads/2021/09/06/1630945843-tomaz3.png',
|
|
||||||
},
|
},
|
||||||
'params': {
|
'params': {
|
||||||
'skip_download': True,
|
'skip_download': True,
|
||||||
@ -104,14 +81,6 @@ class N1InfoIIE(InfoExtractor):
|
|||||||
'upload_date': '20210924',
|
'upload_date': '20210924',
|
||||||
'timestamp': 1632448649.0,
|
'timestamp': 1632448649.0,
|
||||||
'uploader': 'YouLotWhatDontStop',
|
'uploader': 'YouLotWhatDontStop',
|
||||||
'display_id': 'pu9wbx',
|
|
||||||
'channel_id': 'serbia',
|
|
||||||
'comment_count': int,
|
|
||||||
'like_count': int,
|
|
||||||
'dislike_count': int,
|
|
||||||
'age_limit': 0,
|
|
||||||
'duration': 134,
|
|
||||||
'thumbnail': 'https://external-preview.redd.it/5nmmawSeGx60miQM3Iq-ueC9oyCLTLjjqX-qqY8uRsc.png?format=pjpg&auto=webp&s=2f973400b04d23f871b608b178e47fc01f9b8f1d',
|
|
||||||
},
|
},
|
||||||
'params': {
|
'params': {
|
||||||
'skip_download': True,
|
'skip_download': True,
|
||||||
@ -124,7 +93,6 @@ class N1InfoIIE(InfoExtractor):
|
|||||||
'title': 'Žaklina Tatalović Ani Brnabić: Pričate laži (VIDEO)',
|
'title': 'Žaklina Tatalović Ani Brnabić: Pričate laži (VIDEO)',
|
||||||
'upload_date': '20211102',
|
'upload_date': '20211102',
|
||||||
'timestamp': 1635861677,
|
'timestamp': 1635861677,
|
||||||
'thumbnail': 'https://nova.rs/wp-content/uploads/2021/11/02/1635860298-TNJG_Ana_Brnabic_i_Zaklina_Tatalovic_100_dana_Vlade_GP.jpg',
|
|
||||||
},
|
},
|
||||||
}, {
|
}, {
|
||||||
'url': 'https://n1info.rs/vesti/cuta-biti-u-kosovskoj-mitrovici-znaci-da-te-docekaju-eksplozivnim-napravama/',
|
'url': 'https://n1info.rs/vesti/cuta-biti-u-kosovskoj-mitrovici-znaci-da-te-docekaju-eksplozivnim-napravama/',
|
||||||
@ -136,16 +104,6 @@ class N1InfoIIE(InfoExtractor):
|
|||||||
'timestamp': 1687290536,
|
'timestamp': 1687290536,
|
||||||
'thumbnail': 'https://cdn.brid.tv/live/partners/26827/snapshot/1332368_th_6492013a8356f_1687290170.jpg',
|
'thumbnail': 'https://cdn.brid.tv/live/partners/26827/snapshot/1332368_th_6492013a8356f_1687290170.jpg',
|
||||||
},
|
},
|
||||||
}, {
|
|
||||||
'url': 'https://n1info.rs/vesti/vuciceva-turneja-po-srbiji-najavljuje-kontrarevoluciju-preti-svom-narodu-vredja-novinare/',
|
|
||||||
'info_dict': {
|
|
||||||
'id': '2025974',
|
|
||||||
'ext': 'mp4',
|
|
||||||
'title': 'Vučićeva turneja po Srbiji: Najavljuje kontrarevoluciju, preti svom narodu, vređa novinare',
|
|
||||||
'thumbnail': 'https://cdn-uc.brid.tv/live/partners/26827/snapshot/2025974_fhd_67c4a23280a81_1740939826.jpg',
|
|
||||||
'timestamp': 1740939936,
|
|
||||||
'upload_date': '20250302',
|
|
||||||
},
|
|
||||||
}, {
|
}, {
|
||||||
'url': 'https://hr.n1info.com/vijesti/pravobraniteljica-o-ubojstvu-u-zagrebu-radi-se-o-doista-nezapamcenoj-situaciji/',
|
'url': 'https://hr.n1info.com/vijesti/pravobraniteljica-o-ubojstvu-u-zagrebu-radi-se-o-doista-nezapamcenoj-situaciji/',
|
||||||
'only_matching': True,
|
'only_matching': True,
|
||||||
@ -157,11 +115,11 @@ class N1InfoIIE(InfoExtractor):
|
|||||||
|
|
||||||
title = self._html_search_regex(r'<h1[^>]+>(.+?)</h1>', webpage, 'title')
|
title = self._html_search_regex(r'<h1[^>]+>(.+?)</h1>', webpage, 'title')
|
||||||
timestamp = unified_timestamp(self._html_search_meta('article:published_time', webpage))
|
timestamp = unified_timestamp(self._html_search_meta('article:published_time', webpage))
|
||||||
plugin_data = re.findall(r'\$bp\("(?:Brid|TargetVideo)_\d+",\s(.+)\);', webpage)
|
plugin_data = self._html_search_meta('BridPlugin', webpage)
|
||||||
entries = []
|
entries = []
|
||||||
if plugin_data:
|
if plugin_data:
|
||||||
site_id = self._html_search_regex(r'site:(\d+)', webpage, 'site id')
|
site_id = self._html_search_regex(r'site:(\d+)', webpage, 'site id')
|
||||||
for video_data in plugin_data:
|
for video_data in re.findall(r'\$bp\("Brid_\d+", (.+)\);', webpage):
|
||||||
video_id = self._parse_json(video_data, title)['video']
|
video_id = self._parse_json(video_data, title)['video']
|
||||||
entries.append({
|
entries.append({
|
||||||
'id': video_id,
|
'id': video_id,
|
||||||
@ -182,7 +140,7 @@ class N1InfoIIE(InfoExtractor):
|
|||||||
'url': video_data.get('data-url'),
|
'url': video_data.get('data-url'),
|
||||||
'id': video_data.get('id'),
|
'id': video_data.get('id'),
|
||||||
'title': title,
|
'title': title,
|
||||||
'thumbnail': traverse_obj(video_data, (('data-thumbnail', 'data-default_thumbnail'), {url_or_none}, any)),
|
'thumbnail': video_data.get('data-thumbnail'),
|
||||||
'timestamp': timestamp,
|
'timestamp': timestamp,
|
||||||
'ie_key': 'N1InfoAsset',
|
'ie_key': 'N1InfoAsset',
|
||||||
})
|
})
|
||||||
@ -194,7 +152,7 @@ class N1InfoIIE(InfoExtractor):
|
|||||||
if url.startswith('https://www.youtube.com'):
|
if url.startswith('https://www.youtube.com'):
|
||||||
entries.append(self.url_result(url, ie='Youtube'))
|
entries.append(self.url_result(url, ie='Youtube'))
|
||||||
elif url.startswith('https://www.redditmedia.com'):
|
elif url.startswith('https://www.redditmedia.com'):
|
||||||
entries.append(self.url_result(url, ie='Reddit'))
|
entries.append(self.url_result(url, ie='RedditR'))
|
||||||
|
|
||||||
return {
|
return {
|
||||||
'_type': 'playlist',
|
'_type': 'playlist',
|
||||||
|
@ -736,7 +736,7 @@ class NBCStationsIE(InfoExtractor):
|
|||||||
webpage = self._download_webpage(url, video_id)
|
webpage = self._download_webpage(url, video_id)
|
||||||
|
|
||||||
nbc_data = self._search_json(
|
nbc_data = self._search_json(
|
||||||
r'(?:<script>\s*var\s+nbc\s*=|Object\.assign\(nbc,)', webpage, 'NBC JSON data', video_id)
|
r'<script>\s*var\s+nbc\s*=', webpage, 'NBC JSON data', video_id)
|
||||||
pdk_acct = nbc_data.get('pdkAcct') or 'Yh1nAC'
|
pdk_acct = nbc_data.get('pdkAcct') or 'Yh1nAC'
|
||||||
fw_ssid = traverse_obj(nbc_data, ('video', 'fwSSID'))
|
fw_ssid = traverse_obj(nbc_data, ('video', 'fwSSID'))
|
||||||
|
|
||||||
|
@ -13,89 +13,30 @@ from ..utils import (
|
|||||||
ExtractorError,
|
ExtractorError,
|
||||||
OnDemandPagedList,
|
OnDemandPagedList,
|
||||||
clean_html,
|
clean_html,
|
||||||
determine_ext,
|
|
||||||
float_or_none,
|
float_or_none,
|
||||||
int_or_none,
|
int_or_none,
|
||||||
|
join_nonempty,
|
||||||
parse_duration,
|
parse_duration,
|
||||||
parse_iso8601,
|
parse_iso8601,
|
||||||
parse_qs,
|
|
||||||
parse_resolution,
|
parse_resolution,
|
||||||
qualities,
|
qualities,
|
||||||
remove_start,
|
remove_start,
|
||||||
str_or_none,
|
str_or_none,
|
||||||
|
traverse_obj,
|
||||||
try_get,
|
try_get,
|
||||||
unescapeHTML,
|
unescapeHTML,
|
||||||
unified_timestamp,
|
|
||||||
update_url_query,
|
update_url_query,
|
||||||
url_basename,
|
|
||||||
url_or_none,
|
url_or_none,
|
||||||
urlencode_postdata,
|
urlencode_postdata,
|
||||||
urljoin,
|
urljoin,
|
||||||
)
|
)
|
||||||
from ..utils.traversal import find_element, traverse_obj
|
|
||||||
|
|
||||||
|
|
||||||
class NiconicoBaseIE(InfoExtractor):
|
class NiconicoIE(InfoExtractor):
|
||||||
_GEO_BYPASS = False
|
|
||||||
_GEO_COUNTRIES = ['JP']
|
|
||||||
_LOGIN_BASE = 'https://account.nicovideo.jp'
|
|
||||||
_NETRC_MACHINE = 'niconico'
|
|
||||||
|
|
||||||
@property
|
|
||||||
def is_logged_in(self):
|
|
||||||
return bool(self._get_cookies('https://www.nicovideo.jp').get('user_session'))
|
|
||||||
|
|
||||||
def _raise_login_error(self, message, expected=True):
|
|
||||||
raise ExtractorError(f'Unable to login: {message}', expected=expected)
|
|
||||||
|
|
||||||
def _perform_login(self, username, password):
|
|
||||||
if self.is_logged_in:
|
|
||||||
return
|
|
||||||
|
|
||||||
self._request_webpage(
|
|
||||||
f'{self._LOGIN_BASE}/login', None, 'Requesting session cookies')
|
|
||||||
webpage = self._download_webpage(
|
|
||||||
f'{self._LOGIN_BASE}/login/redirector', None,
|
|
||||||
'Logging in', 'Unable to log in', headers={
|
|
||||||
'Content-Type': 'application/x-www-form-urlencoded',
|
|
||||||
'Referer': f'{self._LOGIN_BASE}/login',
|
|
||||||
}, data=urlencode_postdata({
|
|
||||||
'mail_tel': username,
|
|
||||||
'password': password,
|
|
||||||
}))
|
|
||||||
|
|
||||||
if self.is_logged_in:
|
|
||||||
return
|
|
||||||
elif err_msg := traverse_obj(webpage, (
|
|
||||||
{find_element(cls='notice error')}, {find_element(cls='notice__text')}, {clean_html},
|
|
||||||
)):
|
|
||||||
self._raise_login_error(err_msg or 'Invalid username or password')
|
|
||||||
elif 'oneTimePw' in webpage:
|
|
||||||
post_url = self._search_regex(
|
|
||||||
r'<form[^>]+action=(["\'])(?P<url>.+?)\1', webpage, 'post url', group='url')
|
|
||||||
mfa, urlh = self._download_webpage_handle(
|
|
||||||
urljoin(self._LOGIN_BASE, post_url), None,
|
|
||||||
'Performing MFA', 'Unable to complete MFA', headers={
|
|
||||||
'Content-Type': 'application/x-www-form-urlencoded',
|
|
||||||
}, data=urlencode_postdata({
|
|
||||||
'otp': self._get_tfa_info('6 digit number shown on app'),
|
|
||||||
}))
|
|
||||||
if self.is_logged_in:
|
|
||||||
return
|
|
||||||
elif 'error-code' in parse_qs(urlh.url):
|
|
||||||
err_msg = traverse_obj(mfa, ({find_element(cls='pageMainMsg')}, {clean_html}))
|
|
||||||
self._raise_login_error(err_msg or 'MFA session expired')
|
|
||||||
elif 'formError' in mfa:
|
|
||||||
err_msg = traverse_obj(mfa, (
|
|
||||||
{find_element(cls='formError')}, {find_element(tag='div')}, {clean_html}))
|
|
||||||
self._raise_login_error(err_msg or 'MFA challenge failed')
|
|
||||||
|
|
||||||
self._raise_login_error('Unexpected login error', expected=False)
|
|
||||||
|
|
||||||
|
|
||||||
class NiconicoIE(NiconicoBaseIE):
|
|
||||||
IE_NAME = 'niconico'
|
IE_NAME = 'niconico'
|
||||||
IE_DESC = 'ニコニコ動画'
|
IE_DESC = 'ニコニコ動画'
|
||||||
|
_GEO_COUNTRIES = ['JP']
|
||||||
|
_GEO_BYPASS = False
|
||||||
|
|
||||||
_TESTS = [{
|
_TESTS = [{
|
||||||
'url': 'http://www.nicovideo.jp/watch/sm22312215',
|
'url': 'http://www.nicovideo.jp/watch/sm22312215',
|
||||||
@ -235,6 +176,229 @@ class NiconicoIE(NiconicoBaseIE):
|
|||||||
}]
|
}]
|
||||||
|
|
||||||
_VALID_URL = r'https?://(?:(?:www\.|secure\.|sp\.)?nicovideo\.jp/watch|nico\.ms)/(?P<id>(?:[a-z]{2})?[0-9]+)'
|
_VALID_URL = r'https?://(?:(?:www\.|secure\.|sp\.)?nicovideo\.jp/watch|nico\.ms)/(?P<id>(?:[a-z]{2})?[0-9]+)'
|
||||||
|
_NETRC_MACHINE = 'niconico'
|
||||||
|
_API_HEADERS = {
|
||||||
|
'X-Frontend-ID': '6',
|
||||||
|
'X-Frontend-Version': '0',
|
||||||
|
'X-Niconico-Language': 'en-us',
|
||||||
|
'Referer': 'https://www.nicovideo.jp/',
|
||||||
|
'Origin': 'https://www.nicovideo.jp',
|
||||||
|
}
|
||||||
|
|
||||||
|
def _perform_login(self, username, password):
|
||||||
|
login_ok = True
|
||||||
|
login_form_strs = {
|
||||||
|
'mail_tel': username,
|
||||||
|
'password': password,
|
||||||
|
}
|
||||||
|
self._request_webpage(
|
||||||
|
'https://account.nicovideo.jp/login', None,
|
||||||
|
note='Acquiring Login session')
|
||||||
|
page = self._download_webpage(
|
||||||
|
'https://account.nicovideo.jp/login/redirector?show_button_twitter=1&site=niconico&show_button_facebook=1', None,
|
||||||
|
note='Logging in', errnote='Unable to log in',
|
||||||
|
data=urlencode_postdata(login_form_strs),
|
||||||
|
headers={
|
||||||
|
'Referer': 'https://account.nicovideo.jp/login',
|
||||||
|
'Content-Type': 'application/x-www-form-urlencoded',
|
||||||
|
})
|
||||||
|
if 'oneTimePw' in page:
|
||||||
|
post_url = self._search_regex(
|
||||||
|
r'<form[^>]+action=(["\'])(?P<url>.+?)\1', page, 'post url', group='url')
|
||||||
|
page = self._download_webpage(
|
||||||
|
urljoin('https://account.nicovideo.jp', post_url), None,
|
||||||
|
note='Performing MFA', errnote='Unable to complete MFA',
|
||||||
|
data=urlencode_postdata({
|
||||||
|
'otp': self._get_tfa_info('6 digits code'),
|
||||||
|
}), headers={
|
||||||
|
'Content-Type': 'application/x-www-form-urlencoded',
|
||||||
|
})
|
||||||
|
if 'oneTimePw' in page or 'formError' in page:
|
||||||
|
err_msg = self._html_search_regex(
|
||||||
|
r'formError["\']+>(.*?)</div>', page, 'form_error',
|
||||||
|
default='There\'s an error but the message can\'t be parsed.',
|
||||||
|
flags=re.DOTALL)
|
||||||
|
self.report_warning(f'Unable to log in: MFA challenge failed, "{err_msg}"')
|
||||||
|
return False
|
||||||
|
login_ok = 'class="notice error"' not in page
|
||||||
|
if not login_ok:
|
||||||
|
self.report_warning('Unable to log in: bad username or password')
|
||||||
|
return login_ok
|
||||||
|
|
||||||
|
def _get_heartbeat_info(self, info_dict):
|
||||||
|
video_id, video_src_id, audio_src_id = info_dict['url'].split(':')[1].split('/')
|
||||||
|
dmc_protocol = info_dict['expected_protocol']
|
||||||
|
|
||||||
|
api_data = (
|
||||||
|
info_dict.get('_api_data')
|
||||||
|
or self._parse_json(
|
||||||
|
self._html_search_regex(
|
||||||
|
'data-api-data="([^"]+)"',
|
||||||
|
self._download_webpage('https://www.nicovideo.jp/watch/' + video_id, video_id),
|
||||||
|
'API data', default='{}'),
|
||||||
|
video_id))
|
||||||
|
|
||||||
|
session_api_data = try_get(api_data, lambda x: x['media']['delivery']['movie']['session'])
|
||||||
|
session_api_endpoint = try_get(session_api_data, lambda x: x['urls'][0])
|
||||||
|
|
||||||
|
def ping():
|
||||||
|
tracking_id = traverse_obj(api_data, ('media', 'delivery', 'trackingId'))
|
||||||
|
if tracking_id:
|
||||||
|
tracking_url = update_url_query('https://nvapi.nicovideo.jp/v1/2ab0cbaa/watch', {'t': tracking_id})
|
||||||
|
watch_request_response = self._download_json(
|
||||||
|
tracking_url, video_id,
|
||||||
|
note='Acquiring permission for downloading video', fatal=False,
|
||||||
|
headers=self._API_HEADERS)
|
||||||
|
if traverse_obj(watch_request_response, ('meta', 'status')) != 200:
|
||||||
|
self.report_warning('Failed to acquire permission for playing video. Video download may fail.')
|
||||||
|
|
||||||
|
yesno = lambda x: 'yes' if x else 'no'
|
||||||
|
|
||||||
|
if dmc_protocol == 'http':
|
||||||
|
protocol = 'http'
|
||||||
|
protocol_parameters = {
|
||||||
|
'http_output_download_parameters': {
|
||||||
|
'use_ssl': yesno(session_api_data['urls'][0]['isSsl']),
|
||||||
|
'use_well_known_port': yesno(session_api_data['urls'][0]['isWellKnownPort']),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
elif dmc_protocol == 'hls':
|
||||||
|
protocol = 'm3u8'
|
||||||
|
segment_duration = try_get(self._configuration_arg('segment_duration'), lambda x: int(x[0])) or 6000
|
||||||
|
parsed_token = self._parse_json(session_api_data['token'], video_id)
|
||||||
|
encryption = traverse_obj(api_data, ('media', 'delivery', 'encryption'))
|
||||||
|
protocol_parameters = {
|
||||||
|
'hls_parameters': {
|
||||||
|
'segment_duration': segment_duration,
|
||||||
|
'transfer_preset': '',
|
||||||
|
'use_ssl': yesno(session_api_data['urls'][0]['isSsl']),
|
||||||
|
'use_well_known_port': yesno(session_api_data['urls'][0]['isWellKnownPort']),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
if 'hls_encryption' in parsed_token and encryption:
|
||||||
|
protocol_parameters['hls_parameters']['encryption'] = {
|
||||||
|
parsed_token['hls_encryption']: {
|
||||||
|
'encrypted_key': encryption['encryptedKey'],
|
||||||
|
'key_uri': encryption['keyUri'],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
protocol = 'm3u8_native'
|
||||||
|
else:
|
||||||
|
raise ExtractorError(f'Unsupported DMC protocol: {dmc_protocol}')
|
||||||
|
|
||||||
|
session_response = self._download_json(
|
||||||
|
session_api_endpoint['url'], video_id,
|
||||||
|
query={'_format': 'json'},
|
||||||
|
headers={'Content-Type': 'application/json'},
|
||||||
|
note='Downloading JSON metadata for {}'.format(info_dict['format_id']),
|
||||||
|
data=json.dumps({
|
||||||
|
'session': {
|
||||||
|
'client_info': {
|
||||||
|
'player_id': session_api_data.get('playerId'),
|
||||||
|
},
|
||||||
|
'content_auth': {
|
||||||
|
'auth_type': try_get(session_api_data, lambda x: x['authTypes'][session_api_data['protocols'][0]]),
|
||||||
|
'content_key_timeout': session_api_data.get('contentKeyTimeout'),
|
||||||
|
'service_id': 'nicovideo',
|
||||||
|
'service_user_id': session_api_data.get('serviceUserId'),
|
||||||
|
},
|
||||||
|
'content_id': session_api_data.get('contentId'),
|
||||||
|
'content_src_id_sets': [{
|
||||||
|
'content_src_ids': [{
|
||||||
|
'src_id_to_mux': {
|
||||||
|
'audio_src_ids': [audio_src_id],
|
||||||
|
'video_src_ids': [video_src_id],
|
||||||
|
},
|
||||||
|
}],
|
||||||
|
}],
|
||||||
|
'content_type': 'movie',
|
||||||
|
'content_uri': '',
|
||||||
|
'keep_method': {
|
||||||
|
'heartbeat': {
|
||||||
|
'lifetime': session_api_data.get('heartbeatLifetime'),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'priority': session_api_data['priority'],
|
||||||
|
'protocol': {
|
||||||
|
'name': 'http',
|
||||||
|
'parameters': {
|
||||||
|
'http_parameters': {
|
||||||
|
'parameters': protocol_parameters,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'recipe_id': session_api_data.get('recipeId'),
|
||||||
|
'session_operation_auth': {
|
||||||
|
'session_operation_auth_by_signature': {
|
||||||
|
'signature': session_api_data.get('signature'),
|
||||||
|
'token': session_api_data.get('token'),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'timing_constraint': 'unlimited',
|
||||||
|
},
|
||||||
|
}).encode())
|
||||||
|
|
||||||
|
info_dict['url'] = session_response['data']['session']['content_uri']
|
||||||
|
info_dict['protocol'] = protocol
|
||||||
|
|
||||||
|
# get heartbeat info
|
||||||
|
heartbeat_info_dict = {
|
||||||
|
'url': session_api_endpoint['url'] + '/' + session_response['data']['session']['id'] + '?_format=json&_method=PUT',
|
||||||
|
'data': json.dumps(session_response['data']),
|
||||||
|
# interval, convert milliseconds to seconds, then halve to make a buffer.
|
||||||
|
'interval': float_or_none(session_api_data.get('heartbeatLifetime'), scale=3000),
|
||||||
|
'ping': ping,
|
||||||
|
}
|
||||||
|
|
||||||
|
return info_dict, heartbeat_info_dict
|
||||||
|
|
||||||
|
def _extract_format_for_quality(self, video_id, audio_quality, video_quality, dmc_protocol):
|
||||||
|
|
||||||
|
if not audio_quality.get('isAvailable') or not video_quality.get('isAvailable'):
|
||||||
|
return None
|
||||||
|
|
||||||
|
format_id = '-'.join(
|
||||||
|
[remove_start(s['id'], 'archive_') for s in (video_quality, audio_quality)] + [dmc_protocol])
|
||||||
|
|
||||||
|
vid_qual_label = traverse_obj(video_quality, ('metadata', 'label'))
|
||||||
|
|
||||||
|
return {
|
||||||
|
'url': 'niconico_dmc:{}/{}/{}'.format(video_id, video_quality['id'], audio_quality['id']),
|
||||||
|
'format_id': format_id,
|
||||||
|
'format_note': join_nonempty('DMC', vid_qual_label, dmc_protocol.upper(), delim=' '),
|
||||||
|
'ext': 'mp4', # Session API are used in HTML5, which always serves mp4
|
||||||
|
'acodec': 'aac',
|
||||||
|
'vcodec': 'h264',
|
||||||
|
**traverse_obj(audio_quality, ('metadata', {
|
||||||
|
'abr': ('bitrate', {float_or_none(scale=1000)}),
|
||||||
|
'asr': ('samplingRate', {int_or_none}),
|
||||||
|
})),
|
||||||
|
**traverse_obj(video_quality, ('metadata', {
|
||||||
|
'vbr': ('bitrate', {float_or_none(scale=1000)}),
|
||||||
|
'height': ('resolution', 'height', {int_or_none}),
|
||||||
|
'width': ('resolution', 'width', {int_or_none}),
|
||||||
|
})),
|
||||||
|
'quality': -2 if 'low' in video_quality['id'] else None,
|
||||||
|
'protocol': 'niconico_dmc',
|
||||||
|
'expected_protocol': dmc_protocol, # XXX: This is not a documented field
|
||||||
|
'http_headers': {
|
||||||
|
'Origin': 'https://www.nicovideo.jp',
|
||||||
|
'Referer': 'https://www.nicovideo.jp/watch/' + video_id,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
def _yield_dmc_formats(self, api_data, video_id):
|
||||||
|
dmc_data = traverse_obj(api_data, ('media', 'delivery', 'movie'))
|
||||||
|
audios = traverse_obj(dmc_data, ('audios', ..., {dict}))
|
||||||
|
videos = traverse_obj(dmc_data, ('videos', ..., {dict}))
|
||||||
|
protocols = traverse_obj(dmc_data, ('session', 'protocols', ..., {str}))
|
||||||
|
if not all((audios, videos, protocols)):
|
||||||
|
return
|
||||||
|
|
||||||
|
for audio_quality, video_quality, protocol in itertools.product(audios, videos, protocols):
|
||||||
|
if fmt := self._extract_format_for_quality(video_id, audio_quality, video_quality, protocol):
|
||||||
|
yield fmt
|
||||||
|
|
||||||
def _yield_dms_formats(self, api_data, video_id):
|
def _yield_dms_formats(self, api_data, video_id):
|
||||||
fmt_filter = lambda _, v: v['isAvailable'] and v['id']
|
fmt_filter = lambda _, v: v['isAvailable'] and v['id']
|
||||||
@ -266,7 +430,6 @@ class NiconicoIE(NiconicoBaseIE):
|
|||||||
'format_id': ('id', {str}),
|
'format_id': ('id', {str}),
|
||||||
'abr': ('bitRate', {float_or_none(scale=1000)}),
|
'abr': ('bitRate', {float_or_none(scale=1000)}),
|
||||||
'asr': ('samplingRate', {int_or_none}),
|
'asr': ('samplingRate', {int_or_none}),
|
||||||
'quality': ('qualityLevel', {int_or_none}),
|
|
||||||
}), get_all=False),
|
}), get_all=False),
|
||||||
'acodec': 'aac',
|
'acodec': 'aac',
|
||||||
}
|
}
|
||||||
@ -278,9 +441,7 @@ class NiconicoIE(NiconicoBaseIE):
|
|||||||
min_abr = min(traverse_obj(audios, (..., 'bitRate', {float_or_none})), default=0) / 1000
|
min_abr = min(traverse_obj(audios, (..., 'bitRate', {float_or_none})), default=0) / 1000
|
||||||
for video_fmt in video_fmts:
|
for video_fmt in video_fmts:
|
||||||
video_fmt['tbr'] -= min_abr
|
video_fmt['tbr'] -= min_abr
|
||||||
video_fmt['format_id'] = url_basename(video_fmt['url']).rpartition('.')[0]
|
video_fmt['format_id'] = f'video-{video_fmt["tbr"]:.0f}'
|
||||||
video_fmt['quality'] = traverse_obj(videos, (
|
|
||||||
lambda _, v: v['id'] == video_fmt['format_id'], 'qualityLevel', {int_or_none}, any)) or -1
|
|
||||||
yield video_fmt
|
yield video_fmt
|
||||||
|
|
||||||
def _real_extract(self, url):
|
def _real_extract(self, url):
|
||||||
@ -317,8 +478,8 @@ class NiconicoIE(NiconicoBaseIE):
|
|||||||
'needs_premium': ('isPremium', {bool}),
|
'needs_premium': ('isPremium', {bool}),
|
||||||
'needs_subscription': ('isAdmission', {bool}),
|
'needs_subscription': ('isAdmission', {bool}),
|
||||||
})) or {'needs_auth': True}))
|
})) or {'needs_auth': True}))
|
||||||
|
formats = [*self._yield_dmc_formats(api_data, video_id),
|
||||||
formats = list(self._yield_dms_formats(api_data, video_id))
|
*self._yield_dms_formats(api_data, video_id)]
|
||||||
if not formats:
|
if not formats:
|
||||||
fail_msg = clean_html(self._html_search_regex(
|
fail_msg = clean_html(self._html_search_regex(
|
||||||
r'<p[^>]+\bclass="fail-message"[^>]*>(?P<msg>.+?)</p>',
|
r'<p[^>]+\bclass="fail-message"[^>]*>(?P<msg>.+?)</p>',
|
||||||
@ -753,7 +914,7 @@ class NiconicoUserIE(InfoExtractor):
|
|||||||
return self.playlist_result(self._entries(list_id), list_id)
|
return self.playlist_result(self._entries(list_id), list_id)
|
||||||
|
|
||||||
|
|
||||||
class NiconicoLiveIE(NiconicoBaseIE):
|
class NiconicoLiveIE(InfoExtractor):
|
||||||
IE_NAME = 'niconico:live'
|
IE_NAME = 'niconico:live'
|
||||||
IE_DESC = 'ニコニコ生放送'
|
IE_DESC = 'ニコニコ生放送'
|
||||||
_VALID_URL = r'https?://(?:sp\.)?live2?\.nicovideo\.jp/(?:watch|gate)/(?P<id>lv\d+)'
|
_VALID_URL = r'https?://(?:sp\.)?live2?\.nicovideo\.jp/(?:watch|gate)/(?P<id>lv\d+)'
|
||||||
@ -818,7 +979,6 @@ class NiconicoLiveIE(NiconicoBaseIE):
|
|||||||
'quality': 'abr',
|
'quality': 'abr',
|
||||||
'protocol': 'hls+fmp4',
|
'protocol': 'hls+fmp4',
|
||||||
'latency': latency,
|
'latency': latency,
|
||||||
'accessRightMethod': 'single_cookie',
|
|
||||||
'chasePlay': False,
|
'chasePlay': False,
|
||||||
},
|
},
|
||||||
'room': {
|
'room': {
|
||||||
@ -839,7 +999,6 @@ class NiconicoLiveIE(NiconicoBaseIE):
|
|||||||
if data.get('type') == 'stream':
|
if data.get('type') == 'stream':
|
||||||
m3u8_url = data['data']['uri']
|
m3u8_url = data['data']['uri']
|
||||||
qualities = data['data']['availableQualities']
|
qualities = data['data']['availableQualities']
|
||||||
cookies = data['data']['cookies']
|
|
||||||
break
|
break
|
||||||
elif data.get('type') == 'disconnect':
|
elif data.get('type') == 'disconnect':
|
||||||
self.write_debug(recv)
|
self.write_debug(recv)
|
||||||
@ -874,15 +1033,9 @@ class NiconicoLiveIE(NiconicoBaseIE):
|
|||||||
thumbnails.append({
|
thumbnails.append({
|
||||||
'id': f'{name}_{width}x{height}',
|
'id': f'{name}_{width}x{height}',
|
||||||
'url': img_url,
|
'url': img_url,
|
||||||
'ext': traverse_obj(parse_qs(img_url), ('image', 0, {determine_ext(default_ext='jpg')})),
|
|
||||||
**res,
|
**res,
|
||||||
})
|
})
|
||||||
|
|
||||||
for cookie in cookies:
|
|
||||||
self._set_cookie(
|
|
||||||
cookie['domain'], cookie['name'], cookie['value'],
|
|
||||||
expire_time=unified_timestamp(cookie['expires']), path=cookie['path'], secure=cookie['secure'])
|
|
||||||
|
|
||||||
formats = self._extract_m3u8_formats(m3u8_url, video_id, ext='mp4', live=True)
|
formats = self._extract_m3u8_formats(m3u8_url, video_id, ext='mp4', live=True)
|
||||||
for fmt, q in zip(formats, reversed(qualities[1:])):
|
for fmt, q in zip(formats, reversed(qualities[1:])):
|
||||||
fmt.update({
|
fmt.update({
|
||||||
|
@ -1,46 +1,34 @@
|
|||||||
import json
|
|
||||||
import re
|
|
||||||
|
|
||||||
from .brightcove import BrightcoveNewIE
|
|
||||||
from .common import InfoExtractor
|
from .common import InfoExtractor
|
||||||
from ..utils import (
|
from ..utils import (
|
||||||
|
ExtractorError,
|
||||||
float_or_none,
|
float_or_none,
|
||||||
int_or_none,
|
int_or_none,
|
||||||
parse_iso8601,
|
smuggle_url,
|
||||||
parse_resolution,
|
|
||||||
str_or_none,
|
str_or_none,
|
||||||
url_or_none,
|
try_get,
|
||||||
|
unified_strdate,
|
||||||
|
unified_timestamp,
|
||||||
)
|
)
|
||||||
from ..utils.traversal import require, traverse_obj, value
|
|
||||||
|
|
||||||
|
|
||||||
class NineNowIE(InfoExtractor):
|
class NineNowIE(InfoExtractor):
|
||||||
IE_NAME = '9now.com.au'
|
IE_NAME = '9now.com.au'
|
||||||
_VALID_URL = r'https?://(?:www\.)?9now\.com\.au/(?:[^/?#]+/){2}(?P<id>(?P<type>clip|episode)-[^/?#]+)'
|
_VALID_URL = r'https?://(?:www\.)?9now\.com\.au/(?:[^/]+/){2}(?P<id>[^/?#]+)'
|
||||||
_GEO_BYPASS = False
|
_GEO_COUNTRIES = ['AU']
|
||||||
_TESTS = [{
|
_TESTS = [{
|
||||||
# clip
|
# clip
|
||||||
'url': 'https://www.9now.com.au/today/season-2025/clip-cm8hw9h5z00080hquqa5hszq7',
|
'url': 'https://www.9now.com.au/afl-footy-show/2016/clip-ciql02091000g0hp5oktrnytc',
|
||||||
|
'md5': '17cf47d63ec9323e562c9957a968b565',
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
'id': '6370295582112',
|
'id': '16801',
|
||||||
'ext': 'mp4',
|
'ext': 'mp4',
|
||||||
'title': 'Would Karl Stefanovic be able to land a plane?',
|
'title': 'St. Kilda\'s Joey Montagna on the potential for a player\'s strike',
|
||||||
'description': 'The Today host\'s skills are put to the test with the latest simulation tech.',
|
'description': 'Is a boycott of the NAB Cup "on the table"?',
|
||||||
'uploader_id': '4460760524001',
|
'uploader_id': '4460760524001',
|
||||||
'duration': 197.376,
|
'upload_date': '20160713',
|
||||||
'tags': ['flights', 'technology', 'Karl Stefanovic'],
|
'timestamp': 1468421266,
|
||||||
'season': 'Season 2025',
|
|
||||||
'season_number': 2025,
|
|
||||||
'series': 'TODAY',
|
|
||||||
'timestamp': 1742507988,
|
|
||||||
'upload_date': '20250320',
|
|
||||||
'release_timestamp': 1742507983,
|
|
||||||
'release_date': '20250320',
|
|
||||||
'thumbnail': r're:https?://.+/1920x0/.+\.jpg',
|
|
||||||
},
|
|
||||||
'params': {
|
|
||||||
'skip_download': 'HLS/DASH fragments and mp4 URLs are geo-restricted; only available in AU',
|
|
||||||
},
|
},
|
||||||
|
'skip': 'Only available in Australia',
|
||||||
}, {
|
}, {
|
||||||
# episode
|
# episode
|
||||||
'url': 'https://www.9now.com.au/afl-footy-show/2016/episode-19',
|
'url': 'https://www.9now.com.au/afl-footy-show/2016/episode-19',
|
||||||
@ -53,7 +41,7 @@ class NineNowIE(InfoExtractor):
|
|||||||
# episode of series
|
# episode of series
|
||||||
'url': 'https://www.9now.com.au/lego-masters/season-3/episode-3',
|
'url': 'https://www.9now.com.au/lego-masters/season-3/episode-3',
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
'id': '6308830406112',
|
'id': '6249614030001',
|
||||||
'title': 'Episode 3',
|
'title': 'Episode 3',
|
||||||
'ext': 'mp4',
|
'ext': 'mp4',
|
||||||
'season_number': 3,
|
'season_number': 3,
|
||||||
@ -62,87 +50,72 @@ class NineNowIE(InfoExtractor):
|
|||||||
'uploader_id': '4460760524001',
|
'uploader_id': '4460760524001',
|
||||||
'timestamp': 1619002200,
|
'timestamp': 1619002200,
|
||||||
'upload_date': '20210421',
|
'upload_date': '20210421',
|
||||||
'duration': 3574.085,
|
|
||||||
'thumbnail': r're:https?://.+/1920x0/.+\.jpg',
|
|
||||||
'tags': ['episode'],
|
|
||||||
'series': 'Lego Masters',
|
|
||||||
'season': 'Season 3',
|
|
||||||
'episode': 'Episode 3',
|
|
||||||
'release_timestamp': 1619002200,
|
|
||||||
'release_date': '20210421',
|
|
||||||
},
|
},
|
||||||
|
'expected_warnings': ['Ignoring subtitle tracks'],
|
||||||
'params': {
|
'params': {
|
||||||
'skip_download': 'HLS/DASH fragments and mp4 URLs are geo-restricted; only available in AU',
|
'skip_download': True,
|
||||||
},
|
|
||||||
}, {
|
|
||||||
'url': 'https://www.9now.com.au/married-at-first-sight/season-12/episode-1',
|
|
||||||
'info_dict': {
|
|
||||||
'id': '6367798770112',
|
|
||||||
'ext': 'mp4',
|
|
||||||
'title': 'Episode 1',
|
|
||||||
'description': r're:The cultural sensation of Married At First Sight returns with our first weddings! .{90}$',
|
|
||||||
'uploader_id': '4460760524001',
|
|
||||||
'duration': 5415.079,
|
|
||||||
'thumbnail': r're:https?://.+/1920x0/.+\.png',
|
|
||||||
'tags': ['episode'],
|
|
||||||
'season': 'Season 12',
|
|
||||||
'season_number': 12,
|
|
||||||
'episode': 'Episode 1',
|
|
||||||
'episode_number': 1,
|
|
||||||
'series': 'Married at First Sight',
|
|
||||||
'timestamp': 1737973800,
|
|
||||||
'upload_date': '20250127',
|
|
||||||
'release_timestamp': 1737973800,
|
|
||||||
'release_date': '20250127',
|
|
||||||
},
|
|
||||||
'params': {
|
|
||||||
'skip_download': 'HLS/DASH fragments and mp4 URLs are geo-restricted; only available in AU',
|
|
||||||
},
|
},
|
||||||
}]
|
}]
|
||||||
BRIGHTCOVE_URL_TEMPLATE = 'http://players.brightcove.net/4460760524001/default_default/index.html?videoId={}'
|
BRIGHTCOVE_URL_TEMPLATE = 'http://players.brightcove.net/4460760524001/default_default/index.html?videoId=%s'
|
||||||
|
|
||||||
# XXX: For parsing next.js v15+ data; see also yt_dlp.extractor.francetv and yt_dlp.extractor.goplay
|
|
||||||
def _find_json(self, s):
|
|
||||||
return self._search_json(
|
|
||||||
r'\w+\s*:\s*', s, 'next js data', None, contains_pattern=r'\[(?s:.+)\]', default=None)
|
|
||||||
|
|
||||||
def _real_extract(self, url):
|
def _real_extract(self, url):
|
||||||
display_id, video_type = self._match_valid_url(url).group('id', 'type')
|
display_id = self._match_id(url)
|
||||||
webpage = self._download_webpage(url, display_id)
|
webpage = self._download_webpage(url, display_id)
|
||||||
|
page_data = self._parse_json(self._search_regex(
|
||||||
|
r'window\.__data\s*=\s*({.*?});', webpage,
|
||||||
|
'page data', default='{}'), display_id, fatal=False)
|
||||||
|
if not page_data:
|
||||||
|
page_data = self._parse_json(self._parse_json(self._search_regex(
|
||||||
|
r'window\.__data\s*=\s*JSON\.parse\s*\(\s*(".+?")\s*\)\s*;',
|
||||||
|
webpage, 'page data'), display_id), display_id)
|
||||||
|
|
||||||
common_data = traverse_obj(
|
for kind in ('episode', 'clip'):
|
||||||
re.findall(r'<script[^>]*>\s*self\.__next_f\.push\(\s*(\[.+?\])\s*\);?\s*</script>', webpage),
|
current_key = page_data.get(kind, {}).get(
|
||||||
(..., {json.loads}, ..., {self._find_json},
|
f'current{kind.capitalize()}Key')
|
||||||
lambda _, v: v['payload'][video_type]['slug'] == display_id,
|
if not current_key:
|
||||||
'payload', any, {require('video data')}))
|
continue
|
||||||
|
cache = page_data.get(kind, {}).get(f'{kind}Cache', {})
|
||||||
|
if not cache:
|
||||||
|
continue
|
||||||
|
common_data = {
|
||||||
|
'episode': (cache.get(current_key) or next(iter(cache.values())))[kind],
|
||||||
|
'season': (cache.get(current_key) or next(iter(cache.values()))).get('season', None),
|
||||||
|
}
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
raise ExtractorError('Unable to find video data')
|
||||||
|
|
||||||
if traverse_obj(common_data, (video_type, 'video', 'drm', {bool})):
|
if not self.get_param('allow_unplayable_formats') and try_get(common_data, lambda x: x['episode']['video']['drm'], bool):
|
||||||
self.report_drm(display_id)
|
self.report_drm(display_id)
|
||||||
brightcove_id = traverse_obj(common_data, (
|
brightcove_id = try_get(
|
||||||
video_type, 'video', (
|
common_data, lambda x: x['episode']['video']['brightcoveId'], str) or 'ref:{}'.format(common_data['episode']['video']['referenceId'])
|
||||||
('brightcoveId', {str}),
|
video_id = str_or_none(try_get(common_data, lambda x: x['episode']['video']['id'])) or brightcove_id
|
||||||
('referenceId', {str}, {lambda x: f'ref:{x}' if x else None}),
|
|
||||||
), any, {require('brightcove ID')}))
|
title = try_get(common_data, lambda x: x['episode']['name'], str)
|
||||||
|
season_number = try_get(common_data, lambda x: x['season']['seasonNumber'], int)
|
||||||
|
episode_number = try_get(common_data, lambda x: x['episode']['episodeNumber'], int)
|
||||||
|
timestamp = unified_timestamp(try_get(common_data, lambda x: x['episode']['airDate'], str))
|
||||||
|
release_date = unified_strdate(try_get(common_data, lambda x: x['episode']['availability'], str))
|
||||||
|
thumbnails_data = try_get(common_data, lambda x: x['episode']['image']['sizes'], dict) or {}
|
||||||
|
thumbnails = [{
|
||||||
|
'id': thumbnail_id,
|
||||||
|
'url': thumbnail_url,
|
||||||
|
'width': int_or_none(thumbnail_id[1:]),
|
||||||
|
} for thumbnail_id, thumbnail_url in thumbnails_data.items()]
|
||||||
|
|
||||||
return {
|
return {
|
||||||
'_type': 'url_transparent',
|
'_type': 'url_transparent',
|
||||||
'ie_key': BrightcoveNewIE.ie_key(),
|
'url': smuggle_url(
|
||||||
'url': self.BRIGHTCOVE_URL_TEMPLATE.format(brightcove_id),
|
self.BRIGHTCOVE_URL_TEMPLATE % brightcove_id,
|
||||||
**traverse_obj(common_data, {
|
{'geo_countries': self._GEO_COUNTRIES}),
|
||||||
'id': (video_type, 'video', 'id', {int}, ({str_or_none}, {value(brightcove_id)}), any),
|
'id': video_id,
|
||||||
'title': (video_type, 'name', {str}),
|
'title': title,
|
||||||
'description': (video_type, 'description', {str}),
|
'description': try_get(common_data, lambda x: x['episode']['description'], str),
|
||||||
'duration': (video_type, 'video', 'duration', {float_or_none(scale=1000)}),
|
'duration': float_or_none(try_get(common_data, lambda x: x['episode']['video']['duration'], float), 1000),
|
||||||
'tags': (video_type, 'tags', ..., 'name', {str}, all, filter),
|
'thumbnails': thumbnails,
|
||||||
'series': ('tvSeries', 'name', {str}),
|
'ie_key': 'BrightcoveNew',
|
||||||
'season_number': ('season', 'seasonNumber', {int_or_none}),
|
'season_number': season_number,
|
||||||
'episode_number': ('episode', 'episodeNumber', {int_or_none}),
|
'episode_number': episode_number,
|
||||||
'timestamp': ('episode', 'airDate', {parse_iso8601}),
|
'timestamp': timestamp,
|
||||||
'release_timestamp': (video_type, 'availability', {parse_iso8601}),
|
'release_date': release_date,
|
||||||
'thumbnails': (video_type, 'image', 'sizes', {dict.items}, lambda _, v: url_or_none(v[1]), {
|
|
||||||
'id': 0,
|
|
||||||
'url': 1,
|
|
||||||
'width': (1, {parse_resolution}, 'width'),
|
|
||||||
}),
|
|
||||||
}),
|
|
||||||
}
|
}
|
||||||
|
@ -181,7 +181,6 @@ class NYTimesArticleIE(NYTimesBaseIE):
|
|||||||
'thumbnail': r're:https?://\w+\.nyt.com/images/.*\.jpg',
|
'thumbnail': r're:https?://\w+\.nyt.com/images/.*\.jpg',
|
||||||
'duration': 119.0,
|
'duration': 119.0,
|
||||||
},
|
},
|
||||||
'skip': 'HTTP Error 500: Internal Server Error',
|
|
||||||
}, {
|
}, {
|
||||||
# article with audio and no video
|
# article with audio and no video
|
||||||
'url': 'https://www.nytimes.com/2023/09/29/health/mosquitoes-genetic-engineering.html',
|
'url': 'https://www.nytimes.com/2023/09/29/health/mosquitoes-genetic-engineering.html',
|
||||||
@ -191,14 +190,13 @@ class NYTimesArticleIE(NYTimesBaseIE):
|
|||||||
'ext': 'mp3',
|
'ext': 'mp3',
|
||||||
'title': 'The Gamble: Can Genetically Modified Mosquitoes End Disease?',
|
'title': 'The Gamble: Can Genetically Modified Mosquitoes End Disease?',
|
||||||
'description': 'md5:9ff8b47acbaf7f3ca8c732f5c815be2e',
|
'description': 'md5:9ff8b47acbaf7f3ca8c732f5c815be2e',
|
||||||
'timestamp': 1696008129,
|
'timestamp': 1695960700,
|
||||||
'upload_date': '20230929',
|
'upload_date': '20230929',
|
||||||
'creators': ['Stephanie Nolen', 'Natalija Gormalova'],
|
'creator': 'Stephanie Nolen, Natalija Gormalova',
|
||||||
'thumbnail': r're:https?://\w+\.nyt.com/images/.*\.jpg',
|
'thumbnail': r're:https?://\w+\.nyt.com/images/.*\.jpg',
|
||||||
'duration': 1322,
|
'duration': 1322,
|
||||||
},
|
},
|
||||||
}, {
|
}, {
|
||||||
# lede_media_block already has sourceId
|
|
||||||
'url': 'https://www.nytimes.com/2023/11/29/business/dealbook/kamala-harris-biden-voters.html',
|
'url': 'https://www.nytimes.com/2023/11/29/business/dealbook/kamala-harris-biden-voters.html',
|
||||||
'md5': '3eb5ddb1d6f86254fe4f233826778737',
|
'md5': '3eb5ddb1d6f86254fe4f233826778737',
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
@ -209,7 +207,7 @@ class NYTimesArticleIE(NYTimesBaseIE):
|
|||||||
'timestamp': 1701290997,
|
'timestamp': 1701290997,
|
||||||
'upload_date': '20231129',
|
'upload_date': '20231129',
|
||||||
'uploader': 'By The New York Times',
|
'uploader': 'By The New York Times',
|
||||||
'creators': ['Katie Rogers'],
|
'creator': 'Katie Rogers',
|
||||||
'thumbnail': r're:https?://\w+\.nyt.com/images/.*\.jpg',
|
'thumbnail': r're:https?://\w+\.nyt.com/images/.*\.jpg',
|
||||||
'duration': 97.631,
|
'duration': 97.631,
|
||||||
},
|
},
|
||||||
@ -224,22 +222,10 @@ class NYTimesArticleIE(NYTimesBaseIE):
|
|||||||
'title': 'Drunk and Asleep on the Job: Air Traffic Controllers Pushed to the Brink',
|
'title': 'Drunk and Asleep on the Job: Air Traffic Controllers Pushed to the Brink',
|
||||||
'description': 'md5:549e5a5e935bf7d048be53ba3d2c863d',
|
'description': 'md5:549e5a5e935bf7d048be53ba3d2c863d',
|
||||||
'upload_date': '20231202',
|
'upload_date': '20231202',
|
||||||
'creators': ['Emily Steel', 'Sydney Ember'],
|
'creator': 'Emily Steel, Sydney Ember',
|
||||||
'timestamp': 1701511264,
|
'timestamp': 1701511264,
|
||||||
},
|
},
|
||||||
'playlist_count': 3,
|
'playlist_count': 3,
|
||||||
}, {
|
|
||||||
# lede_media_block does not have sourceId
|
|
||||||
'url': 'https://www.nytimes.com/2025/04/30/well/move/hip-mobility-routine.html',
|
|
||||||
'info_dict': {
|
|
||||||
'id': 'hip-mobility-routine',
|
|
||||||
'title': 'Tight Hips? These Moves Can Help.',
|
|
||||||
'description': 'Sitting all day is hard on your hips. Try this simple routine for better mobility.',
|
|
||||||
'creators': ['Alyssa Ages', 'Theodore Tae'],
|
|
||||||
'timestamp': 1746003629,
|
|
||||||
'upload_date': '20250430',
|
|
||||||
},
|
|
||||||
'playlist_count': 7,
|
|
||||||
}, {
|
}, {
|
||||||
'url': 'https://www.nytimes.com/2023/12/02/business/media/netflix-squid-game-challenge.html',
|
'url': 'https://www.nytimes.com/2023/12/02/business/media/netflix-squid-game-challenge.html',
|
||||||
'only_matching': True,
|
'only_matching': True,
|
||||||
@ -270,18 +256,14 @@ class NYTimesArticleIE(NYTimesBaseIE):
|
|||||||
|
|
||||||
def _real_extract(self, url):
|
def _real_extract(self, url):
|
||||||
page_id = self._match_id(url)
|
page_id = self._match_id(url)
|
||||||
webpage = self._download_webpage(url, page_id, impersonate=True)
|
webpage = self._download_webpage(url, page_id)
|
||||||
art_json = self._search_json(
|
art_json = self._search_json(
|
||||||
r'window\.__preloadedData\s*=', webpage, 'media details', page_id,
|
r'window\.__preloadedData\s*=', webpage, 'media details', page_id,
|
||||||
transform_source=lambda x: x.replace('undefined', 'null'))['initialData']['data']['article']
|
transform_source=lambda x: x.replace('undefined', 'null'))['initialData']['data']['article']
|
||||||
content = art_json['sprinkledBody']['content']
|
|
||||||
|
|
||||||
blocks = []
|
blocks = traverse_obj(art_json, (
|
||||||
block_filter = lambda k, v: k == 'media' and v['__typename'] in ('Video', 'Audio')
|
'sprinkledBody', 'content', ..., ('ledeMedia', None),
|
||||||
if lede_media_block := traverse_obj(content, (..., 'ledeMedia', block_filter, any)):
|
lambda _, v: v['__typename'] in ('Video', 'Audio')))
|
||||||
lede_media_block.setdefault('sourceId', art_json.get('sourceId'))
|
|
||||||
blocks.append(lede_media_block)
|
|
||||||
blocks.extend(traverse_obj(content, (..., block_filter)))
|
|
||||||
if not blocks:
|
if not blocks:
|
||||||
raise ExtractorError('Unable to extract any media blocks from webpage')
|
raise ExtractorError('Unable to extract any media blocks from webpage')
|
||||||
|
|
||||||
@ -291,7 +273,8 @@ class NYTimesArticleIE(NYTimesBaseIE):
|
|||||||
'sprinkledBody', 'content', ..., 'summary', 'content', ..., 'text', {str}),
|
'sprinkledBody', 'content', ..., 'summary', 'content', ..., 'text', {str}),
|
||||||
get_all=False) or self._html_search_meta(['og:description', 'twitter:description'], webpage),
|
get_all=False) or self._html_search_meta(['og:description', 'twitter:description'], webpage),
|
||||||
'timestamp': traverse_obj(art_json, ('firstPublished', {parse_iso8601})),
|
'timestamp': traverse_obj(art_json, ('firstPublished', {parse_iso8601})),
|
||||||
'creators': traverse_obj(art_json, ('bylines', ..., 'creators', ..., 'displayName', {str})),
|
'creator': ', '.join(
|
||||||
|
traverse_obj(art_json, ('bylines', ..., 'creators', ..., 'displayName'))), # TODO: change to 'creators' (list)
|
||||||
'thumbnails': self._extract_thumbnails(traverse_obj(
|
'thumbnails': self._extract_thumbnails(traverse_obj(
|
||||||
art_json, ('promotionalMedia', 'assetCrops', ..., 'renditions', ...))),
|
art_json, ('promotionalMedia', 'assetCrops', ..., 'renditions', ...))),
|
||||||
}
|
}
|
||||||
|
@ -11,15 +11,12 @@ class On24IE(InfoExtractor):
|
|||||||
IE_NAME = 'on24'
|
IE_NAME = 'on24'
|
||||||
IE_DESC = 'ON24'
|
IE_DESC = 'ON24'
|
||||||
|
|
||||||
_ID_RE = r'(?P<id>\d{7})'
|
_VALID_URL = r'''(?x)
|
||||||
_KEY_RE = r'(?P<key>[0-9A-F]{32})'
|
https?://event\.on24\.com/(?:
|
||||||
_URL_BASE_RE = r'https?://event\.on24\.com'
|
wcc/r/(?P<id_1>\d{7})/(?P<key_1>[0-9A-F]{32})|
|
||||||
_URL_QUERY_RE = rf'(?:[^#]*&)?eventid={_ID_RE}&(?:[^#]+&)?key={_KEY_RE}'
|
eventRegistration/(?:console/EventConsoleApollo|EventLobbyServlet\?target=lobby30)
|
||||||
_VALID_URL = [
|
\.jsp\?(?:[^/#?]*&)?eventid=(?P<id_2>\d{7})[^/#?]*&key=(?P<key_2>[0-9A-F]{32})
|
||||||
rf'{_URL_BASE_RE}/wcc/r/{_ID_RE}/{_KEY_RE}',
|
)'''
|
||||||
rf'{_URL_BASE_RE}/eventRegistration/console/(?:EventConsoleApollo\.jsp|apollox/mainEvent/?)\?{_URL_QUERY_RE}',
|
|
||||||
rf'{_URL_BASE_RE}/eventRegistration/EventLobbyServlet/?\?{_URL_QUERY_RE}',
|
|
||||||
]
|
|
||||||
|
|
||||||
_TESTS = [{
|
_TESTS = [{
|
||||||
'url': 'https://event.on24.com/eventRegistration/console/EventConsoleApollo.jsp?uimode=nextgeneration&eventid=2197467&sessionid=1&key=5DF57BE53237F36A43B478DD36277A84&contenttype=A&eventuserid=305999&playerwidth=1000&playerheight=650&caller=previewLobby&text_language_id=en&format=fhaudio&newConsole=false',
|
'url': 'https://event.on24.com/eventRegistration/console/EventConsoleApollo.jsp?uimode=nextgeneration&eventid=2197467&sessionid=1&key=5DF57BE53237F36A43B478DD36277A84&contenttype=A&eventuserid=305999&playerwidth=1000&playerheight=650&caller=previewLobby&text_language_id=en&format=fhaudio&newConsole=false',
|
||||||
@ -37,16 +34,12 @@ class On24IE(InfoExtractor):
|
|||||||
}, {
|
}, {
|
||||||
'url': 'https://event.on24.com/eventRegistration/console/EventConsoleApollo.jsp?&eventid=2639291&sessionid=1&username=&partnerref=&format=fhvideo1&mobile=&flashsupportedmobiledevice=&helpcenter=&key=82829018E813065A122363877975752E&newConsole=true&nxChe=true&newTabCon=true&text_language_id=en&playerwidth=748&playerheight=526&eventuserid=338788762&contenttype=A&mediametricsessionid=384764716&mediametricid=3558192&usercd=369267058&mode=launch',
|
'url': 'https://event.on24.com/eventRegistration/console/EventConsoleApollo.jsp?&eventid=2639291&sessionid=1&username=&partnerref=&format=fhvideo1&mobile=&flashsupportedmobiledevice=&helpcenter=&key=82829018E813065A122363877975752E&newConsole=true&nxChe=true&newTabCon=true&text_language_id=en&playerwidth=748&playerheight=526&eventuserid=338788762&contenttype=A&mediametricsessionid=384764716&mediametricid=3558192&usercd=369267058&mode=launch',
|
||||||
'only_matching': True,
|
'only_matching': True,
|
||||||
}, {
|
|
||||||
'url': 'https://event.on24.com/eventRegistration/EventLobbyServlet?target=reg20.jsp&eventid=3543176&key=BC0F6B968B67C34B50D461D40FDB3E18&groupId=3143628',
|
|
||||||
'only_matching': True,
|
|
||||||
}, {
|
|
||||||
'url': 'https://event.on24.com/eventRegistration/console/apollox/mainEvent?&eventid=4843671&sessionid=1&username=&partnerref=&format=fhvideo1&mobile=&flashsupportedmobiledevice=&helpcenter=&key=4EAC9B5C564CC98FF29E619B06A2F743&newConsole=true&nxChe=true&newTabCon=true&consoleEarEventConsole=false&consoleEarCloudApi=false&text_language_id=en&playerwidth=748&playerheight=526&referrer=https%3A%2F%2Fevent.on24.com%2Finterface%2Fregistration%2Fautoreg%2Findex.html%3Fsessionid%3D1%26eventid%3D4843671%26key%3D4EAC9B5C564CC98FF29E619B06A2F743%26email%3D000a3e42-7952-4dd6-8f8a-34c38ea3cf02%2540platform%26firstname%3Ds%26lastname%3Ds%26deletecookie%3Dtrue%26event_email%3DN%26marketing_email%3DN%26std1%3D0642572014177%26std2%3D0642572014179%26std3%3D550165f7-a44e-4725-9fe6-716f89908c2b%26std4%3D0&eventuserid=745776448&contenttype=A&mediametricsessionid=640613707&mediametricid=6810717&usercd=745776448&mode=launch',
|
|
||||||
'only_matching': True,
|
|
||||||
}]
|
}]
|
||||||
|
|
||||||
def _real_extract(self, url):
|
def _real_extract(self, url):
|
||||||
event_id, event_key = self._match_valid_url(url).group('id', 'key')
|
mobj = self._match_valid_url(url)
|
||||||
|
event_id = mobj.group('id_1') or mobj.group('id_2')
|
||||||
|
event_key = mobj.group('key_1') or mobj.group('key_2')
|
||||||
|
|
||||||
event_data = self._download_json(
|
event_data = self._download_json(
|
||||||
'https://event.on24.com/apic/utilApp/EventConsoleCachedServlet',
|
'https://event.on24.com/apic/utilApp/EventConsoleCachedServlet',
|
||||||
|
@ -67,7 +67,7 @@ class OpenRecBaseIE(InfoExtractor):
|
|||||||
|
|
||||||
class OpenRecIE(OpenRecBaseIE):
|
class OpenRecIE(OpenRecBaseIE):
|
||||||
IE_NAME = 'openrec'
|
IE_NAME = 'openrec'
|
||||||
_VALID_URL = r'https?://(?:www\.)?openrec\.tv/live/(?P<id>[^/?#]+)'
|
_VALID_URL = r'https?://(?:www\.)?openrec\.tv/live/(?P<id>[^/]+)'
|
||||||
_TESTS = [{
|
_TESTS = [{
|
||||||
'url': 'https://www.openrec.tv/live/2p8v31qe4zy',
|
'url': 'https://www.openrec.tv/live/2p8v31qe4zy',
|
||||||
'only_matching': True,
|
'only_matching': True,
|
||||||
@ -85,7 +85,7 @@ class OpenRecIE(OpenRecBaseIE):
|
|||||||
|
|
||||||
class OpenRecCaptureIE(OpenRecBaseIE):
|
class OpenRecCaptureIE(OpenRecBaseIE):
|
||||||
IE_NAME = 'openrec:capture'
|
IE_NAME = 'openrec:capture'
|
||||||
_VALID_URL = r'https?://(?:www\.)?openrec\.tv/capture/(?P<id>[^/?#]+)'
|
_VALID_URL = r'https?://(?:www\.)?openrec\.tv/capture/(?P<id>[^/]+)'
|
||||||
_TESTS = [{
|
_TESTS = [{
|
||||||
'url': 'https://www.openrec.tv/capture/l9nk2x4gn14',
|
'url': 'https://www.openrec.tv/capture/l9nk2x4gn14',
|
||||||
'only_matching': True,
|
'only_matching': True,
|
||||||
@ -129,7 +129,7 @@ class OpenRecCaptureIE(OpenRecBaseIE):
|
|||||||
|
|
||||||
class OpenRecMovieIE(OpenRecBaseIE):
|
class OpenRecMovieIE(OpenRecBaseIE):
|
||||||
IE_NAME = 'openrec:movie'
|
IE_NAME = 'openrec:movie'
|
||||||
_VALID_URL = r'https?://(?:www\.)?openrec\.tv/movie/(?P<id>[^/?#]+)'
|
_VALID_URL = r'https?://(?:www\.)?openrec\.tv/movie/(?P<id>[^/]+)'
|
||||||
_TESTS = [{
|
_TESTS = [{
|
||||||
'url': 'https://www.openrec.tv/movie/nqz5xl5km8v',
|
'url': 'https://www.openrec.tv/movie/nqz5xl5km8v',
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
@ -141,9 +141,6 @@ class OpenRecMovieIE(OpenRecBaseIE):
|
|||||||
'uploader_id': 'taiki_to_kazuhiro',
|
'uploader_id': 'taiki_to_kazuhiro',
|
||||||
'timestamp': 1638856800,
|
'timestamp': 1638856800,
|
||||||
},
|
},
|
||||||
}, {
|
|
||||||
'url': 'https://www.openrec.tv/movie/2p8vvex548y?playlist_id=98brq96vvsgn2nd',
|
|
||||||
'only_matching': True,
|
|
||||||
}]
|
}]
|
||||||
|
|
||||||
def _real_extract(self, url):
|
def _real_extract(self, url):
|
||||||
|
@ -14,9 +14,8 @@ from ..utils import (
|
|||||||
int_or_none,
|
int_or_none,
|
||||||
parse_qs,
|
parse_qs,
|
||||||
srt_subtitles_timecode,
|
srt_subtitles_timecode,
|
||||||
url_or_none,
|
traverse_obj,
|
||||||
)
|
)
|
||||||
from ..utils.traversal import traverse_obj
|
|
||||||
|
|
||||||
|
|
||||||
class PanoptoBaseIE(InfoExtractor):
|
class PanoptoBaseIE(InfoExtractor):
|
||||||
@ -346,16 +345,21 @@ class PanoptoIE(PanoptoBaseIE):
|
|||||||
subtitles = {}
|
subtitles = {}
|
||||||
for stream in streams or []:
|
for stream in streams or []:
|
||||||
stream_formats = []
|
stream_formats = []
|
||||||
for stream_url in set(traverse_obj(stream, (('StreamHttpUrl', 'StreamUrl'), {url_or_none}))):
|
http_stream_url = stream.get('StreamHttpUrl')
|
||||||
|
stream_url = stream.get('StreamUrl')
|
||||||
|
|
||||||
|
if http_stream_url:
|
||||||
|
stream_formats.append({'url': http_stream_url})
|
||||||
|
|
||||||
|
if stream_url:
|
||||||
media_type = stream.get('ViewerMediaFileTypeName')
|
media_type = stream.get('ViewerMediaFileTypeName')
|
||||||
if media_type in ('hls', ):
|
if media_type in ('hls', ):
|
||||||
fmts, subs = self._extract_m3u8_formats_and_subtitles(stream_url, video_id, m3u8_id='hls', fatal=False)
|
m3u8_formats, stream_subtitles = self._extract_m3u8_formats_and_subtitles(stream_url, video_id)
|
||||||
stream_formats.extend(fmts)
|
stream_formats.extend(m3u8_formats)
|
||||||
self._merge_subtitles(subs, target=subtitles)
|
subtitles = self._merge_subtitles(subtitles, stream_subtitles)
|
||||||
else:
|
else:
|
||||||
stream_formats.append({
|
stream_formats.append({
|
||||||
'url': stream_url,
|
'url': stream_url,
|
||||||
'ext': media_type,
|
|
||||||
})
|
})
|
||||||
for fmt in stream_formats:
|
for fmt in stream_formats:
|
||||||
fmt.update({
|
fmt.update({
|
||||||
|
@ -1,101 +0,0 @@
|
|||||||
from .common import InfoExtractor
|
|
||||||
from ..utils import UserNotLive, int_or_none, parse_iso8601, url_or_none, urljoin
|
|
||||||
from ..utils.traversal import traverse_obj
|
|
||||||
|
|
||||||
|
|
||||||
class PartiBaseIE(InfoExtractor):
|
|
||||||
def _call_api(self, path, video_id, note=None):
|
|
||||||
return self._download_json(
|
|
||||||
f'https://api-backend.parti.com/parti_v2/profile/{path}', video_id, note)
|
|
||||||
|
|
||||||
|
|
||||||
class PartiVideoIE(PartiBaseIE):
|
|
||||||
IE_NAME = 'parti:video'
|
|
||||||
_VALID_URL = r'https?://(?:www\.)?parti\.com/video/(?P<id>\d+)'
|
|
||||||
_TESTS = [{
|
|
||||||
'url': 'https://parti.com/video/66284',
|
|
||||||
'info_dict': {
|
|
||||||
'id': '66284',
|
|
||||||
'ext': 'mp4',
|
|
||||||
'title': 'NOW LIVE ',
|
|
||||||
'upload_date': '20250327',
|
|
||||||
'categories': ['Gaming'],
|
|
||||||
'thumbnail': 'https://assets.parti.com/351424_eb9e5250-2821-484a-9c5f-ca99aa666c87.png',
|
|
||||||
'channel': 'ItZTMGG',
|
|
||||||
'timestamp': 1743044379,
|
|
||||||
},
|
|
||||||
'params': {'skip_download': 'm3u8'},
|
|
||||||
}]
|
|
||||||
|
|
||||||
def _real_extract(self, url):
|
|
||||||
video_id = self._match_id(url)
|
|
||||||
data = self._call_api(f'get_livestream_channel_info/recent/{video_id}', video_id)
|
|
||||||
|
|
||||||
return {
|
|
||||||
'id': video_id,
|
|
||||||
'formats': self._extract_m3u8_formats(
|
|
||||||
urljoin('https://watch.parti.com', data['livestream_recording']), video_id, 'mp4'),
|
|
||||||
**traverse_obj(data, {
|
|
||||||
'title': ('event_title', {str}),
|
|
||||||
'channel': ('user_name', {str}),
|
|
||||||
'thumbnail': ('event_file', {url_or_none}),
|
|
||||||
'categories': ('category_name', {str}, filter, all),
|
|
||||||
'timestamp': ('event_start_ts', {int_or_none}),
|
|
||||||
}),
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class PartiLivestreamIE(PartiBaseIE):
|
|
||||||
IE_NAME = 'parti:livestream'
|
|
||||||
_VALID_URL = r'https?://(?:www\.)?parti\.com/creator/(?P<service>[\w]+)/(?P<id>[\w/-]+)'
|
|
||||||
_TESTS = [{
|
|
||||||
'url': 'https://parti.com/creator/parti/Capt_Robs_Adventures',
|
|
||||||
'info_dict': {
|
|
||||||
'id': 'Capt_Robs_Adventures',
|
|
||||||
'ext': 'mp4',
|
|
||||||
'title': r"re:I'm Live on Parti \d{4}-\d{2}-\d{2} \d{2}:\d{2}",
|
|
||||||
'view_count': int,
|
|
||||||
'thumbnail': r're:https://assets\.parti\.com/.+\.png',
|
|
||||||
'timestamp': 1743879776,
|
|
||||||
'upload_date': '20250405',
|
|
||||||
'live_status': 'is_live',
|
|
||||||
},
|
|
||||||
'params': {'skip_download': 'm3u8'},
|
|
||||||
}, {
|
|
||||||
'url': 'https://parti.com/creator/discord/sazboxgaming/0',
|
|
||||||
'only_matching': True,
|
|
||||||
}]
|
|
||||||
|
|
||||||
def _real_extract(self, url):
|
|
||||||
service, creator_slug = self._match_valid_url(url).group('service', 'id')
|
|
||||||
|
|
||||||
encoded_creator_slug = creator_slug.replace('/', '%23')
|
|
||||||
creator_id = self._call_api(
|
|
||||||
f'get_user_by_social_media/{service}/{encoded_creator_slug}',
|
|
||||||
creator_slug, note='Fetching user ID')
|
|
||||||
|
|
||||||
data = self._call_api(
|
|
||||||
f'get_livestream_channel_info/{creator_id}', creator_id,
|
|
||||||
note='Fetching user profile feed')['channel_info']
|
|
||||||
|
|
||||||
if not traverse_obj(data, ('channel', 'is_live', {bool})):
|
|
||||||
raise UserNotLive(video_id=creator_id)
|
|
||||||
|
|
||||||
channel_info = data['channel']
|
|
||||||
|
|
||||||
return {
|
|
||||||
'id': creator_slug,
|
|
||||||
'formats': self._extract_m3u8_formats(
|
|
||||||
channel_info['playback_url'], creator_slug, live=True, query={
|
|
||||||
'token': channel_info['playback_auth_token'],
|
|
||||||
'player_version': '1.17.0',
|
|
||||||
}),
|
|
||||||
'is_live': True,
|
|
||||||
**traverse_obj(data, {
|
|
||||||
'title': ('livestream_event_info', 'event_name', {str}),
|
|
||||||
'description': ('livestream_event_info', 'event_description', {str}),
|
|
||||||
'thumbnail': ('livestream_event_info', 'livestream_preview_file', {url_or_none}),
|
|
||||||
'timestamp': ('stream', 'start_time', {parse_iso8601}),
|
|
||||||
'view_count': ('stream', 'viewer_count', {int_or_none}),
|
|
||||||
}),
|
|
||||||
}
|
|
@ -1,3 +1,5 @@
|
|||||||
|
import re
|
||||||
|
|
||||||
from .youtube import YoutubeIE
|
from .youtube import YoutubeIE
|
||||||
from .zdf import ZDFBaseIE
|
from .zdf import ZDFBaseIE
|
||||||
from ..utils import (
|
from ..utils import (
|
||||||
@ -5,27 +7,44 @@ from ..utils import (
|
|||||||
merge_dicts,
|
merge_dicts,
|
||||||
try_get,
|
try_get,
|
||||||
unified_timestamp,
|
unified_timestamp,
|
||||||
|
urljoin,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class PhoenixIE(ZDFBaseIE):
|
class PhoenixIE(ZDFBaseIE):
|
||||||
IE_NAME = 'phoenix.de'
|
IE_NAME = 'phoenix.de'
|
||||||
_VALID_URL = r'https?://(?:www\.)?phoenix\.de/(?:[^/?#]+/)*[^/?#&]*-a-(?P<id>\d+)\.html'
|
_VALID_URL = r'https?://(?:www\.)?phoenix\.de/(?:[^/]+/)*[^/?#&]*-a-(?P<id>\d+)\.html'
|
||||||
_TESTS = [{
|
_TESTS = [{
|
||||||
'url': 'https://www.phoenix.de/sendungen/dokumentationen/spitzbergen-a-893349.html',
|
# Same as https://www.zdf.de/politik/phoenix-sendungen/wohin-fuehrt-der-protest-in-der-pandemie-100.html
|
||||||
'md5': 'a79e86d9774d0b3f2102aff988a0bd32',
|
'url': 'https://www.phoenix.de/sendungen/ereignisse/corona-nachgehakt/wohin-fuehrt-der-protest-in-der-pandemie-a-2050630.html',
|
||||||
|
'md5': '34ec321e7eb34231fd88616c65c92db0',
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
'id': '221215_phx_spitzbergen',
|
'id': '210222_phx_nachgehakt_corona_protest',
|
||||||
'ext': 'mp4',
|
'ext': 'mp4',
|
||||||
'title': 'Spitzbergen',
|
'title': 'Wohin führt der Protest in der Pandemie?',
|
||||||
'description': 'Film von Tilmann Bünz',
|
'description': 'md5:7d643fe7f565e53a24aac036b2122fbd',
|
||||||
'duration': 728.0,
|
'duration': 1691,
|
||||||
'timestamp': 1555600500,
|
'timestamp': 1613902500,
|
||||||
'upload_date': '20190418',
|
'upload_date': '20210221',
|
||||||
'uploader': 'Phoenix',
|
'uploader': 'Phoenix',
|
||||||
'thumbnail': 'https://www.phoenix.de/sixcms/media.php/21/Bergspitzen1.png',
|
'series': 'corona nachgehakt',
|
||||||
'series': 'Dokumentationen',
|
'episode': 'Wohin führt der Protest in der Pandemie?',
|
||||||
'episode': 'Spitzbergen',
|
},
|
||||||
|
}, {
|
||||||
|
# Youtube embed
|
||||||
|
'url': 'https://www.phoenix.de/sendungen/gespraeche/phoenix-streitgut-brennglas-corona-a-1965505.html',
|
||||||
|
'info_dict': {
|
||||||
|
'id': 'hMQtqFYjomk',
|
||||||
|
'ext': 'mp4',
|
||||||
|
'title': 'phoenix streitgut: Brennglas Corona - Wie gerecht ist unsere Gesellschaft?',
|
||||||
|
'description': 'md5:ac7a02e2eb3cb17600bc372e4ab28fdd',
|
||||||
|
'duration': 3509,
|
||||||
|
'upload_date': '20201219',
|
||||||
|
'uploader': 'phoenix',
|
||||||
|
'uploader_id': 'phoenix',
|
||||||
|
},
|
||||||
|
'params': {
|
||||||
|
'skip_download': True,
|
||||||
},
|
},
|
||||||
}, {
|
}, {
|
||||||
'url': 'https://www.phoenix.de/entwicklungen-in-russland-a-2044720.html',
|
'url': 'https://www.phoenix.de/entwicklungen-in-russland-a-2044720.html',
|
||||||
@ -71,8 +90,8 @@ class PhoenixIE(ZDFBaseIE):
|
|||||||
content_id = details['tracking']['nielsen']['content']['assetid']
|
content_id = details['tracking']['nielsen']['content']['assetid']
|
||||||
|
|
||||||
info = self._extract_ptmd(
|
info = self._extract_ptmd(
|
||||||
f'https://tmd.phoenix.de/tmd/2/android_native_6/vod/ptmd/phoenix/{content_id}',
|
f'https://tmd.phoenix.de/tmd/2/ngplayer_2_3/vod/ptmd/phoenix/{content_id}',
|
||||||
content_id)
|
content_id, None, url)
|
||||||
|
|
||||||
duration = int_or_none(try_get(
|
duration = int_or_none(try_get(
|
||||||
details, lambda x: x['tracking']['nielsen']['content']['length']))
|
details, lambda x: x['tracking']['nielsen']['content']['length']))
|
||||||
@ -82,8 +101,20 @@ class PhoenixIE(ZDFBaseIE):
|
|||||||
str)
|
str)
|
||||||
episode = title if details.get('contentType') == 'episode' else None
|
episode = title if details.get('contentType') == 'episode' else None
|
||||||
|
|
||||||
|
thumbnails = []
|
||||||
teaser_images = try_get(details, lambda x: x['teaserImageRef']['layouts'], dict) or {}
|
teaser_images = try_get(details, lambda x: x['teaserImageRef']['layouts'], dict) or {}
|
||||||
thumbnails = self._extract_thumbnails(teaser_images)
|
for thumbnail_key, thumbnail_url in teaser_images.items():
|
||||||
|
thumbnail_url = urljoin(url, thumbnail_url)
|
||||||
|
if not thumbnail_url:
|
||||||
|
continue
|
||||||
|
thumbnail = {
|
||||||
|
'url': thumbnail_url,
|
||||||
|
}
|
||||||
|
m = re.match('^([0-9]+)x([0-9]+)$', thumbnail_key)
|
||||||
|
if m:
|
||||||
|
thumbnail['width'] = int(m.group(1))
|
||||||
|
thumbnail['height'] = int(m.group(2))
|
||||||
|
thumbnails.append(thumbnail)
|
||||||
|
|
||||||
return merge_dicts(info, {
|
return merge_dicts(info, {
|
||||||
'id': content_id,
|
'id': content_id,
|
||||||
|
@ -23,9 +23,9 @@ class PinterestBaseIE(InfoExtractor):
|
|||||||
def _call_api(self, resource, video_id, options):
|
def _call_api(self, resource, video_id, options):
|
||||||
return self._download_json(
|
return self._download_json(
|
||||||
f'https://www.pinterest.com/resource/{resource}Resource/get/',
|
f'https://www.pinterest.com/resource/{resource}Resource/get/',
|
||||||
video_id, f'Download {resource} JSON metadata',
|
video_id, f'Download {resource} JSON metadata', query={
|
||||||
query={'data': json.dumps({'options': options})},
|
'data': json.dumps({'options': options}),
|
||||||
headers={'X-Pinterest-PWS-Handler': 'www/[username].js'})['resource_response']
|
})['resource_response']
|
||||||
|
|
||||||
def _extract_video(self, data, extract_formats=True):
|
def _extract_video(self, data, extract_formats=True):
|
||||||
video_id = data['id']
|
video_id = data['id']
|
||||||
|
@ -1,7 +1,4 @@
|
|||||||
import base64
|
|
||||||
import hashlib
|
|
||||||
import json
|
import json
|
||||||
import uuid
|
|
||||||
|
|
||||||
from .common import InfoExtractor
|
from .common import InfoExtractor
|
||||||
from ..utils import (
|
from ..utils import (
|
||||||
@ -145,73 +142,39 @@ class PlaySuisseIE(InfoExtractor):
|
|||||||
id
|
id
|
||||||
url
|
url
|
||||||
}'''
|
}'''
|
||||||
_CLIENT_ID = '1e33f1bf-8bf3-45e4-bbd9-c9ad934b5fca'
|
_LOGIN_BASE_URL = 'https://login.srgssr.ch/srgssrlogin.onmicrosoft.com'
|
||||||
_LOGIN_BASE = 'https://account.srgssr.ch'
|
_LOGIN_PATH = 'B2C_1A__SignInV2'
|
||||||
_ID_TOKEN = None
|
_ID_TOKEN = None
|
||||||
|
|
||||||
def _perform_login(self, username, password):
|
def _perform_login(self, username, password):
|
||||||
code_verifier = uuid.uuid4().hex + uuid.uuid4().hex + uuid.uuid4().hex
|
login_page = self._download_webpage(
|
||||||
code_challenge = base64.urlsafe_b64encode(
|
'https://www.playsuisse.ch/api/sso/login', None, note='Downloading login page',
|
||||||
hashlib.sha256(code_verifier.encode()).digest()).decode().rstrip('=')
|
query={'x': 'x', 'locale': 'de', 'redirectUrl': 'https://www.playsuisse.ch/'})
|
||||||
|
settings = self._search_json(r'var\s+SETTINGS\s*=', login_page, 'settings', None)
|
||||||
|
|
||||||
request_id = parse_qs(self._request_webpage(
|
csrf_token = settings['csrf']
|
||||||
f'{self._LOGIN_BASE}/authz-srv/authz', None, 'Requesting session ID', query={
|
query = {'tx': settings['transId'], 'p': self._LOGIN_PATH}
|
||||||
'client_id': self._CLIENT_ID,
|
|
||||||
'redirect_uri': 'https://www.playsuisse.ch/auth',
|
|
||||||
'scope': 'email profile openid offline_access',
|
|
||||||
'response_type': 'code',
|
|
||||||
'code_challenge': code_challenge,
|
|
||||||
'code_challenge_method': 'S256',
|
|
||||||
'view_type': 'login',
|
|
||||||
}).url)['requestId'][0]
|
|
||||||
|
|
||||||
try:
|
status = traverse_obj(self._download_json(
|
||||||
exchange_id = self._download_json(
|
f'{self._LOGIN_BASE_URL}/{self._LOGIN_PATH}/SelfAsserted', None, 'Logging in',
|
||||||
f'{self._LOGIN_BASE}/verification-srv/v2/authenticate/initiate/password', None,
|
query=query, headers={'X-CSRF-TOKEN': csrf_token}, data=urlencode_postdata({
|
||||||
'Submitting username', headers={'content-type': 'application/json'}, data=json.dumps({
|
'request_type': 'RESPONSE',
|
||||||
'usage_type': 'INITIAL_AUTHENTICATION',
|
'signInName': username,
|
||||||
'request_id': request_id,
|
|
||||||
'medium_id': 'PASSWORD',
|
|
||||||
'type': 'password',
|
|
||||||
'identifier': username,
|
|
||||||
}).encode())['data']['exchange_id']['exchange_id']
|
|
||||||
except ExtractorError:
|
|
||||||
raise ExtractorError('Invalid username', expected=True)
|
|
||||||
|
|
||||||
try:
|
|
||||||
login_data = self._download_json(
|
|
||||||
f'{self._LOGIN_BASE}/verification-srv/v2/authenticate/authenticate/password', None,
|
|
||||||
'Submitting password', headers={'content-type': 'application/json'}, data=json.dumps({
|
|
||||||
'requestId': request_id,
|
|
||||||
'exchange_id': exchange_id,
|
|
||||||
'type': 'password',
|
|
||||||
'password': password,
|
'password': password,
|
||||||
}).encode())['data']
|
}), expected_status=400), ('status', {int_or_none}))
|
||||||
except ExtractorError:
|
if status == 400:
|
||||||
raise ExtractorError('Invalid password', expected=True)
|
raise ExtractorError('Invalid username or password', expected=True)
|
||||||
|
|
||||||
authorization_code = parse_qs(self._request_webpage(
|
urlh = self._request_webpage(
|
||||||
f'{self._LOGIN_BASE}/login-srv/verification/login', None, 'Logging in',
|
f'{self._LOGIN_BASE_URL}/{self._LOGIN_PATH}/api/CombinedSigninAndSignup/confirmed',
|
||||||
data=urlencode_postdata({
|
None, 'Downloading ID token', query={
|
||||||
'requestId': request_id,
|
'rememberMe': 'false',
|
||||||
'exchange_id': login_data['exchange_id']['exchange_id'],
|
'csrf_token': csrf_token,
|
||||||
'verificationType': 'password',
|
**query,
|
||||||
'sub': login_data['sub'],
|
'diags': '',
|
||||||
'status_id': login_data['status_id'],
|
})
|
||||||
'rememberMe': True,
|
|
||||||
'lat': '',
|
|
||||||
'lon': '',
|
|
||||||
})).url)['code'][0]
|
|
||||||
|
|
||||||
self._ID_TOKEN = self._download_json(
|
|
||||||
f'{self._LOGIN_BASE}/proxy/token', None, 'Downloading token', data=b'', query={
|
|
||||||
'client_id': self._CLIENT_ID,
|
|
||||||
'redirect_uri': 'https://www.playsuisse.ch/auth',
|
|
||||||
'code': authorization_code,
|
|
||||||
'code_verifier': code_verifier,
|
|
||||||
'grant_type': 'authorization_code',
|
|
||||||
})['id_token']
|
|
||||||
|
|
||||||
|
self._ID_TOKEN = traverse_obj(parse_qs(urlh.url), ('id_token', 0))
|
||||||
if not self._ID_TOKEN:
|
if not self._ID_TOKEN:
|
||||||
raise ExtractorError('Login failed')
|
raise ExtractorError('Login failed')
|
||||||
|
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user