Compare commits

...

9 Commits

Author SHA1 Message Date
github-actions
b76e9cedb3 [version] update
Created by: pukkandan

:ci skip all :ci run dl
2022-08-19 00:11:11 +00:00
pukkandan
48c88e088c
Release 2022.08.19 2022-08-19 05:08:22 +05:30
pukkandan
a831c2ea90
[cleanup] Misc 2022-08-19 05:08:21 +05:30
pukkandan
be13a6e525
[jsinterp] Bring on-par with youtube-dl
Code from: https://github.com/ytdl-org/youtube-dl/pull/31175, https://github.com/ytdl-org/youtube-dl/pull/31182

Authored by pukkandan, dirkf
2022-08-19 05:08:21 +05:30
bashonly
8a3da4c68c
[extractor/instagram] Fix bugs in 7d3b98be4c (#4701)
Authored by: bashonly
2022-08-19 03:45:49 +05:30
nixxo
4d37d4a77c
[extractor/rai] Minor fix (#4700)
Closes #4691, #4690
2022-08-19 02:28:59 +05:30
bashonly
7d3b98be4c
[extractor/instagram] Fix extraction (#4696)
Closes #4657, #4532, #4475
Authored by: bashonly, pritam20ps05
2022-08-19 02:27:46 +05:30
Elyse
2b3e43e247
[extractor/rtbf] Fix stream extractor (#4671)
Closes #4656
Authored by: elyse0
2022-08-19 01:42:04 +05:30
Alexander Seiler
f60ef66371
[extractor/zattoo] Fix Zattoo resellers (#4675)
Closes #4630
Authored by: goggle
2022-08-19 01:27:51 +05:30
19 changed files with 978 additions and 233 deletions

View File

@ -18,7 +18,7 @@ body:
options: options:
- label: I'm reporting a broken site - label: I'm reporting a broken site
required: true required: true
- label: I've verified that I'm running yt-dlp version **2022.08.14** ([update instructions](https://github.com/yt-dlp/yt-dlp#update)) or later (specify commit) - label: I've verified that I'm running yt-dlp version **2022.08.19** ([update instructions](https://github.com/yt-dlp/yt-dlp#update)) or later (specify commit)
required: true required: true
- label: I've checked that all provided URLs are playable in a browser with the same IP and same login details - label: I've checked that all provided URLs are playable in a browser with the same IP and same login details
required: true required: true
@ -62,7 +62,7 @@ body:
[debug] Command-line config: ['-vU', 'test:youtube'] [debug] Command-line config: ['-vU', 'test:youtube']
[debug] Portable config "yt-dlp.conf": ['-i'] [debug] Portable config "yt-dlp.conf": ['-i']
[debug] Encodings: locale cp65001, fs utf-8, pref cp65001, out utf-8, error utf-8, screen utf-8 [debug] Encodings: locale cp65001, fs utf-8, pref cp65001, out utf-8, error utf-8, screen utf-8
[debug] yt-dlp version 2022.08.14 [9d339c4] (win32_exe) [debug] yt-dlp version 2022.08.19 [9d339c4] (win32_exe)
[debug] Python 3.8.10 (CPython 64bit) - Windows-10-10.0.22000-SP0 [debug] Python 3.8.10 (CPython 64bit) - Windows-10-10.0.22000-SP0
[debug] Checking exe version: ffmpeg -bsfs [debug] Checking exe version: ffmpeg -bsfs
[debug] Checking exe version: ffprobe -bsfs [debug] Checking exe version: ffprobe -bsfs
@ -70,8 +70,8 @@ body:
[debug] Optional libraries: Cryptodome-3.15.0, brotli-1.0.9, certifi-2022.06.15, mutagen-1.45.1, sqlite3-2.6.0, websockets-10.3 [debug] Optional libraries: Cryptodome-3.15.0, brotli-1.0.9, certifi-2022.06.15, mutagen-1.45.1, sqlite3-2.6.0, websockets-10.3
[debug] Proxy map: {} [debug] Proxy map: {}
[debug] Fetching release info: https://api.github.com/repos/yt-dlp/yt-dlp/releases/latest [debug] Fetching release info: https://api.github.com/repos/yt-dlp/yt-dlp/releases/latest
Latest version: 2022.08.14, Current version: 2022.08.14 Latest version: 2022.08.19, Current version: 2022.08.19
yt-dlp is up to date (2022.08.14) yt-dlp is up to date (2022.08.19)
<more lines> <more lines>
render: shell render: shell
validations: validations:

View File

@ -18,7 +18,7 @@ body:
options: options:
- label: I'm reporting a new site support request - label: I'm reporting a new site support request
required: true required: true
- label: I've verified that I'm running yt-dlp version **2022.08.14** ([update instructions](https://github.com/yt-dlp/yt-dlp#update)) or later (specify commit) - label: I've verified that I'm running yt-dlp version **2022.08.19** ([update instructions](https://github.com/yt-dlp/yt-dlp#update)) or later (specify commit)
required: true required: true
- label: I've checked that all provided URLs are playable in a browser with the same IP and same login details - label: I've checked that all provided URLs are playable in a browser with the same IP and same login details
required: true required: true
@ -74,7 +74,7 @@ body:
[debug] Command-line config: ['-vU', 'test:youtube'] [debug] Command-line config: ['-vU', 'test:youtube']
[debug] Portable config "yt-dlp.conf": ['-i'] [debug] Portable config "yt-dlp.conf": ['-i']
[debug] Encodings: locale cp65001, fs utf-8, pref cp65001, out utf-8, error utf-8, screen utf-8 [debug] Encodings: locale cp65001, fs utf-8, pref cp65001, out utf-8, error utf-8, screen utf-8
[debug] yt-dlp version 2022.08.14 [9d339c4] (win32_exe) [debug] yt-dlp version 2022.08.19 [9d339c4] (win32_exe)
[debug] Python 3.8.10 (CPython 64bit) - Windows-10-10.0.22000-SP0 [debug] Python 3.8.10 (CPython 64bit) - Windows-10-10.0.22000-SP0
[debug] Checking exe version: ffmpeg -bsfs [debug] Checking exe version: ffmpeg -bsfs
[debug] Checking exe version: ffprobe -bsfs [debug] Checking exe version: ffprobe -bsfs
@ -82,8 +82,8 @@ body:
[debug] Optional libraries: Cryptodome-3.15.0, brotli-1.0.9, certifi-2022.06.15, mutagen-1.45.1, sqlite3-2.6.0, websockets-10.3 [debug] Optional libraries: Cryptodome-3.15.0, brotli-1.0.9, certifi-2022.06.15, mutagen-1.45.1, sqlite3-2.6.0, websockets-10.3
[debug] Proxy map: {} [debug] Proxy map: {}
[debug] Fetching release info: https://api.github.com/repos/yt-dlp/yt-dlp/releases/latest [debug] Fetching release info: https://api.github.com/repos/yt-dlp/yt-dlp/releases/latest
Latest version: 2022.08.14, Current version: 2022.08.14 Latest version: 2022.08.19, Current version: 2022.08.19
yt-dlp is up to date (2022.08.14) yt-dlp is up to date (2022.08.19)
<more lines> <more lines>
render: shell render: shell
validations: validations:

View File

@ -18,7 +18,7 @@ body:
options: options:
- label: I'm requesting a site-specific feature - label: I'm requesting a site-specific feature
required: true required: true
- label: I've verified that I'm running yt-dlp version **2022.08.14** ([update instructions](https://github.com/yt-dlp/yt-dlp#update)) or later (specify commit) - label: I've verified that I'm running yt-dlp version **2022.08.19** ([update instructions](https://github.com/yt-dlp/yt-dlp#update)) or later (specify commit)
required: true required: true
- label: I've checked that all provided URLs are playable in a browser with the same IP and same login details - label: I've checked that all provided URLs are playable in a browser with the same IP and same login details
required: true required: true
@ -70,7 +70,7 @@ body:
[debug] Command-line config: ['-vU', 'test:youtube'] [debug] Command-line config: ['-vU', 'test:youtube']
[debug] Portable config "yt-dlp.conf": ['-i'] [debug] Portable config "yt-dlp.conf": ['-i']
[debug] Encodings: locale cp65001, fs utf-8, pref cp65001, out utf-8, error utf-8, screen utf-8 [debug] Encodings: locale cp65001, fs utf-8, pref cp65001, out utf-8, error utf-8, screen utf-8
[debug] yt-dlp version 2022.08.14 [9d339c4] (win32_exe) [debug] yt-dlp version 2022.08.19 [9d339c4] (win32_exe)
[debug] Python 3.8.10 (CPython 64bit) - Windows-10-10.0.22000-SP0 [debug] Python 3.8.10 (CPython 64bit) - Windows-10-10.0.22000-SP0
[debug] Checking exe version: ffmpeg -bsfs [debug] Checking exe version: ffmpeg -bsfs
[debug] Checking exe version: ffprobe -bsfs [debug] Checking exe version: ffprobe -bsfs
@ -78,8 +78,8 @@ body:
[debug] Optional libraries: Cryptodome-3.15.0, brotli-1.0.9, certifi-2022.06.15, mutagen-1.45.1, sqlite3-2.6.0, websockets-10.3 [debug] Optional libraries: Cryptodome-3.15.0, brotli-1.0.9, certifi-2022.06.15, mutagen-1.45.1, sqlite3-2.6.0, websockets-10.3
[debug] Proxy map: {} [debug] Proxy map: {}
[debug] Fetching release info: https://api.github.com/repos/yt-dlp/yt-dlp/releases/latest [debug] Fetching release info: https://api.github.com/repos/yt-dlp/yt-dlp/releases/latest
Latest version: 2022.08.14, Current version: 2022.08.14 Latest version: 2022.08.19, Current version: 2022.08.19
yt-dlp is up to date (2022.08.14) yt-dlp is up to date (2022.08.19)
<more lines> <more lines>
render: shell render: shell
validations: validations:

View File

@ -18,7 +18,7 @@ body:
options: options:
- label: I'm reporting a bug unrelated to a specific site - label: I'm reporting a bug unrelated to a specific site
required: true required: true
- label: I've verified that I'm running yt-dlp version **2022.08.14** ([update instructions](https://github.com/yt-dlp/yt-dlp#update)) or later (specify commit) - label: I've verified that I'm running yt-dlp version **2022.08.19** ([update instructions](https://github.com/yt-dlp/yt-dlp#update)) or later (specify commit)
required: true required: true
- label: I've checked that all provided URLs are playable in a browser with the same IP and same login details - label: I've checked that all provided URLs are playable in a browser with the same IP and same login details
required: true required: true
@ -55,7 +55,7 @@ body:
[debug] Command-line config: ['-vU', 'test:youtube'] [debug] Command-line config: ['-vU', 'test:youtube']
[debug] Portable config "yt-dlp.conf": ['-i'] [debug] Portable config "yt-dlp.conf": ['-i']
[debug] Encodings: locale cp65001, fs utf-8, pref cp65001, out utf-8, error utf-8, screen utf-8 [debug] Encodings: locale cp65001, fs utf-8, pref cp65001, out utf-8, error utf-8, screen utf-8
[debug] yt-dlp version 2022.08.14 [9d339c4] (win32_exe) [debug] yt-dlp version 2022.08.19 [9d339c4] (win32_exe)
[debug] Python 3.8.10 (CPython 64bit) - Windows-10-10.0.22000-SP0 [debug] Python 3.8.10 (CPython 64bit) - Windows-10-10.0.22000-SP0
[debug] Checking exe version: ffmpeg -bsfs [debug] Checking exe version: ffmpeg -bsfs
[debug] Checking exe version: ffprobe -bsfs [debug] Checking exe version: ffprobe -bsfs
@ -63,8 +63,8 @@ body:
[debug] Optional libraries: Cryptodome-3.15.0, brotli-1.0.9, certifi-2022.06.15, mutagen-1.45.1, sqlite3-2.6.0, websockets-10.3 [debug] Optional libraries: Cryptodome-3.15.0, brotli-1.0.9, certifi-2022.06.15, mutagen-1.45.1, sqlite3-2.6.0, websockets-10.3
[debug] Proxy map: {} [debug] Proxy map: {}
[debug] Fetching release info: https://api.github.com/repos/yt-dlp/yt-dlp/releases/latest [debug] Fetching release info: https://api.github.com/repos/yt-dlp/yt-dlp/releases/latest
Latest version: 2022.08.14, Current version: 2022.08.14 Latest version: 2022.08.19, Current version: 2022.08.19
yt-dlp is up to date (2022.08.14) yt-dlp is up to date (2022.08.19)
<more lines> <more lines>
render: shell render: shell
validations: validations:

View File

@ -20,7 +20,7 @@ body:
required: true required: true
- label: I've looked through the [README](https://github.com/yt-dlp/yt-dlp#readme) - label: I've looked through the [README](https://github.com/yt-dlp/yt-dlp#readme)
required: true required: true
- label: I've verified that I'm running yt-dlp version **2022.08.14** ([update instructions](https://github.com/yt-dlp/yt-dlp#update)) or later (specify commit) - label: I've verified that I'm running yt-dlp version **2022.08.19** ([update instructions](https://github.com/yt-dlp/yt-dlp#update)) or later (specify commit)
required: true required: true
- label: I've searched the [bugtracker](https://github.com/yt-dlp/yt-dlp/issues?q=) for similar issues **including closed ones**. DO NOT post duplicates - label: I've searched the [bugtracker](https://github.com/yt-dlp/yt-dlp/issues?q=) for similar issues **including closed ones**. DO NOT post duplicates
required: true required: true
@ -51,7 +51,7 @@ body:
[debug] Command-line config: ['-vU', 'test:youtube'] [debug] Command-line config: ['-vU', 'test:youtube']
[debug] Portable config "yt-dlp.conf": ['-i'] [debug] Portable config "yt-dlp.conf": ['-i']
[debug] Encodings: locale cp65001, fs utf-8, pref cp65001, out utf-8, error utf-8, screen utf-8 [debug] Encodings: locale cp65001, fs utf-8, pref cp65001, out utf-8, error utf-8, screen utf-8
[debug] yt-dlp version 2022.08.14 [9d339c4] (win32_exe) [debug] yt-dlp version 2022.08.19 [9d339c4] (win32_exe)
[debug] Python 3.8.10 (CPython 64bit) - Windows-10-10.0.22000-SP0 [debug] Python 3.8.10 (CPython 64bit) - Windows-10-10.0.22000-SP0
[debug] Checking exe version: ffmpeg -bsfs [debug] Checking exe version: ffmpeg -bsfs
[debug] Checking exe version: ffprobe -bsfs [debug] Checking exe version: ffprobe -bsfs
@ -59,7 +59,7 @@ body:
[debug] Optional libraries: Cryptodome-3.15.0, brotli-1.0.9, certifi-2022.06.15, mutagen-1.45.1, sqlite3-2.6.0, websockets-10.3 [debug] Optional libraries: Cryptodome-3.15.0, brotli-1.0.9, certifi-2022.06.15, mutagen-1.45.1, sqlite3-2.6.0, websockets-10.3
[debug] Proxy map: {} [debug] Proxy map: {}
[debug] Fetching release info: https://api.github.com/repos/yt-dlp/yt-dlp/releases/latest [debug] Fetching release info: https://api.github.com/repos/yt-dlp/yt-dlp/releases/latest
Latest version: 2022.08.14, Current version: 2022.08.14 Latest version: 2022.08.19, Current version: 2022.08.19
yt-dlp is up to date (2022.08.14) yt-dlp is up to date (2022.08.19)
<more lines> <more lines>
render: shell render: shell

View File

@ -26,7 +26,7 @@ body:
required: true required: true
- label: I've looked through the [README](https://github.com/yt-dlp/yt-dlp#readme) - label: I've looked through the [README](https://github.com/yt-dlp/yt-dlp#readme)
required: true required: true
- label: I've verified that I'm running yt-dlp version **2022.08.14** ([update instructions](https://github.com/yt-dlp/yt-dlp#update)) or later (specify commit) - label: I've verified that I'm running yt-dlp version **2022.08.19** ([update instructions](https://github.com/yt-dlp/yt-dlp#update)) or later (specify commit)
required: true required: true
- label: I've searched the [bugtracker](https://github.com/yt-dlp/yt-dlp/issues?q=) for similar questions **including closed ones**. DO NOT post duplicates - label: I've searched the [bugtracker](https://github.com/yt-dlp/yt-dlp/issues?q=) for similar questions **including closed ones**. DO NOT post duplicates
required: true required: true
@ -57,7 +57,7 @@ body:
[debug] Command-line config: ['-vU', 'test:youtube'] [debug] Command-line config: ['-vU', 'test:youtube']
[debug] Portable config "yt-dlp.conf": ['-i'] [debug] Portable config "yt-dlp.conf": ['-i']
[debug] Encodings: locale cp65001, fs utf-8, pref cp65001, out utf-8, error utf-8, screen utf-8 [debug] Encodings: locale cp65001, fs utf-8, pref cp65001, out utf-8, error utf-8, screen utf-8
[debug] yt-dlp version 2022.08.14 [9d339c4] (win32_exe) [debug] yt-dlp version 2022.08.19 [9d339c4] (win32_exe)
[debug] Python 3.8.10 (CPython 64bit) - Windows-10-10.0.22000-SP0 [debug] Python 3.8.10 (CPython 64bit) - Windows-10-10.0.22000-SP0
[debug] Checking exe version: ffmpeg -bsfs [debug] Checking exe version: ffmpeg -bsfs
[debug] Checking exe version: ffprobe -bsfs [debug] Checking exe version: ffprobe -bsfs
@ -65,7 +65,7 @@ body:
[debug] Optional libraries: Cryptodome-3.15.0, brotli-1.0.9, certifi-2022.06.15, mutagen-1.45.1, sqlite3-2.6.0, websockets-10.3 [debug] Optional libraries: Cryptodome-3.15.0, brotli-1.0.9, certifi-2022.06.15, mutagen-1.45.1, sqlite3-2.6.0, websockets-10.3
[debug] Proxy map: {} [debug] Proxy map: {}
[debug] Fetching release info: https://api.github.com/repos/yt-dlp/yt-dlp/releases/latest [debug] Fetching release info: https://api.github.com/repos/yt-dlp/yt-dlp/releases/latest
Latest version: 2022.08.14, Current version: 2022.08.14 Latest version: 2022.08.19, Current version: 2022.08.19
yt-dlp is up to date (2022.08.14) yt-dlp is up to date (2022.08.19)
<more lines> <more lines>
render: shell render: shell

View File

@ -11,6 +11,23 @@
--> -->
### 2022.08.19
* Fix bug in `--download-archive`
* [jsinterp] **Fix for new youtube players** and related improvements by [dirkf](https://github.com/dirkf), [pukkandan](https://github.com/pukkandan)
* [phantomjs] Add function to execute JS without a DOM by [MinePlayersPE](https://github.com/MinePlayersPE), [pukkandan](https://github.com/pukkandan)
* [build] Exclude devscripts from installs by [Lesmiscore](https://github.com/Lesmiscore)
* [cleanup] Misc fixes and cleanup
* [extractor/youtube] **Add fallback to phantomjs** for nsig
* [extractor/youtube] Fix error reporting of "Incomplete data"
* [extractor/youtube] Improve format sorting for IOS formats
* [extractor/youtube] Improve signature caching
* [extractor/instagram] Fix extraction by [bashonly](https://github.com/bashonly), [pritam20ps05](https://github.com/pritam20ps05)
* [extractor/rai] Minor fix by [nixxo](https://github.com/nixxo)
* [extractor/rtbf] Fix stream extractor by [elyse0](https://github.com/elyse0)
* [extractor/SovietsCloset] Fix extractor by [ChillingPepper](https://github.com/ChillingPepper)
* [extractor/zattoo] Fix Zattoo resellers by [goggle](https://github.com/goggle)
### 2022.08.14 ### 2022.08.14
* Merge youtube-dl: Upto [commit/d231b56](https://github.com/ytdl-org/youtube-dl/commit/d231b56) * Merge youtube-dl: Upto [commit/d231b56](https://github.com/ytdl-org/youtube-dl/commit/d231b56)
@ -19,8 +36,7 @@
* [extractor] Fix format sorting of `channels` * [extractor] Fix format sorting of `channels`
* [ffmpeg] Disable avconv unless `--prefer-avconv` * [ffmpeg] Disable avconv unless `--prefer-avconv`
* [ffmpeg] Smarter detection of ffprobe filename * [ffmpeg] Smarter detection of ffprobe filename
* [patreon] Ignore erroneous media attachments by [coletdjnz](https://github.com/coletdjnz) * [embedthumbnail] Detect `libatomicparsley.so`
* [postprocessor/embedthumbnail] Detect `libatomicparsley.so`
* [ThumbnailsConvertor] Fix conversion after `fixup_webp` * [ThumbnailsConvertor] Fix conversion after `fixup_webp`
* [utils] Fix `get_compatible_ext` * [utils] Fix `get_compatible_ext`
* [build] Fix changelog * [build] Fix changelog
@ -30,6 +46,7 @@
* [cleanup] Misc fixes and cleanup * [cleanup] Misc fixes and cleanup
* [extractor/moview] Add extractor by [HobbyistDev](https://github.com/HobbyistDev) * [extractor/moview] Add extractor by [HobbyistDev](https://github.com/HobbyistDev)
* [extractor/parler] Add extractor by [palewire](https://github.com/palewire) * [extractor/parler] Add extractor by [palewire](https://github.com/palewire)
* [extractor/patreon] Ignore erroneous media attachments by [coletdjnz](https://github.com/coletdjnz)
* [extractor/truth] Add extractor by [palewire](https://github.com/palewire) * [extractor/truth] Add extractor by [palewire](https://github.com/palewire)
* [extractor/aenetworks] Add formats parameter by [jacobtruman](https://github.com/jacobtruman) * [extractor/aenetworks] Add formats parameter by [jacobtruman](https://github.com/jacobtruman)
* [extractor/crunchyroll] Improve `_VALID_URL`s * [extractor/crunchyroll] Improve `_VALID_URL`s

View File

@ -71,7 +71,7 @@ yt-dlp is a [youtube-dl](https://github.com/ytdl-org/youtube-dl) fork based on t
# NEW FEATURES # NEW FEATURES
* Merged with **youtube-dl v2021.12.17+ [commit/d231b56](https://github.com/ytdl-org/youtube-dl/commit/d231b56717c73ee597d2e077d11b69ed48a1b02d)**<!--([exceptions](https://github.com/yt-dlp/yt-dlp/issues/21))--> and **youtube-dlc v2020.11.11-3+ [commit/f9401f2](https://github.com/blackjack4494/yt-dlc/commit/f9401f2a91987068139c5f757b12fc711d4c0cee)**: You get all the features and patches of [youtube-dlc](https://github.com/blackjack4494/yt-dlc) in addition to the latest [youtube-dl](https://github.com/ytdl-org/youtube-dl) * Merged with **youtube-dl v2021.12.17+ [commit/b0a60ce](https://github.com/ytdl-org/youtube-dl/commit/b0a60ce2032172aeaaf27fe3866ab72768f10cb2)**<!--([exceptions](https://github.com/yt-dlp/yt-dlp/issues/21))--> and **youtube-dlc v2020.11.11-3+ [commit/f9401f2](https://github.com/blackjack4494/yt-dlc/commit/f9401f2a91987068139c5f757b12fc711d4c0cee)**: You get all the features and patches of [youtube-dlc](https://github.com/blackjack4494/yt-dlc) in addition to the latest [youtube-dl](https://github.com/ytdl-org/youtube-dl)
* **[SponsorBlock Integration](#sponsorblock-options)**: You can mark/remove sponsor sections in youtube videos by utilizing the [SponsorBlock](https://sponsor.ajay.app) API * **[SponsorBlock Integration](#sponsorblock-options)**: You can mark/remove sponsor sections in youtube videos by utilizing the [SponsorBlock](https://sponsor.ajay.app) API
@ -329,7 +329,7 @@ You will need the build tools `python` (3.6+), `zip`, `make` (GNU), `pandoc`\* a
After installing these, simply run `make`. After installing these, simply run `make`.
You can also run `make yt-dlp` instead to compile only the binary without updating any of the additional files. (The dependencies marked with **\*** are not needed for this) You can also run `make yt-dlp` instead to compile only the binary without updating any of the additional files. (The build tools marked with **\*** are not needed for this)
### Standalone Py2Exe Builds (Windows) ### Standalone Py2Exe Builds (Windows)

View File

@ -128,6 +128,8 @@
- **bbc.co.uk:iplayer:group** - **bbc.co.uk:iplayer:group**
- **bbc.co.uk:playlist** - **bbc.co.uk:playlist**
- **BBVTV**: [<abbr title="netrc machine"><em>bbvtv</em></abbr>] - **BBVTV**: [<abbr title="netrc machine"><em>bbvtv</em></abbr>]
- **BBVTVLive**: [<abbr title="netrc machine"><em>bbvtv</em></abbr>]
- **BBVTVRecordings**: [<abbr title="netrc machine"><em>bbvtv</em></abbr>]
- **Beatport** - **Beatport**
- **Beeg** - **Beeg**
- **BehindKink** - **BehindKink**
@ -348,6 +350,8 @@
- **ehftv** - **ehftv**
- **eHow** - **eHow**
- **EinsUndEinsTV**: [<abbr title="netrc machine"><em>1und1tv</em></abbr>] - **EinsUndEinsTV**: [<abbr title="netrc machine"><em>1und1tv</em></abbr>]
- **EinsUndEinsTVLive**: [<abbr title="netrc machine"><em>1und1tv</em></abbr>]
- **EinsUndEinsTVRecordings**: [<abbr title="netrc machine"><em>1und1tv</em></abbr>]
- **Einthusan** - **Einthusan**
- **eitb.tv** - **eitb.tv**
- **EllenTube** - **EllenTube**
@ -375,6 +379,8 @@
- **EuropeanTour** - **EuropeanTour**
- **EUScreen** - **EUScreen**
- **EWETV**: [<abbr title="netrc machine"><em>ewetv</em></abbr>] - **EWETV**: [<abbr title="netrc machine"><em>ewetv</em></abbr>]
- **EWETVLive**: [<abbr title="netrc machine"><em>ewetv</em></abbr>]
- **EWETVRecordings**: [<abbr title="netrc machine"><em>ewetv</em></abbr>]
- **ExpoTV** - **ExpoTV**
- **Expressen** - **Expressen**
- **ExtremeTube** - **ExtremeTube**
@ -454,6 +460,8 @@
- **GiantBomb** - **GiantBomb**
- **Giga** - **Giga**
- **GlattvisionTV**: [<abbr title="netrc machine"><em>glattvisiontv</em></abbr>] - **GlattvisionTV**: [<abbr title="netrc machine"><em>glattvisiontv</em></abbr>]
- **GlattvisionTVLive**: [<abbr title="netrc machine"><em>glattvisiontv</em></abbr>]
- **GlattvisionTVRecordings**: [<abbr title="netrc machine"><em>glattvisiontv</em></abbr>]
- **Glide**: Glide mobile video messages (glide.me) - **Glide**: Glide mobile video messages (glide.me)
- **Globo**: [<abbr title="netrc machine"><em>globo</em></abbr>] - **Globo**: [<abbr title="netrc machine"><em>globo</em></abbr>]
- **GloboArticle** - **GloboArticle**
@ -715,6 +723,8 @@
- **MLSSoccer** - **MLSSoccer**
- **Mnet** - **Mnet**
- **MNetTV**: [<abbr title="netrc machine"><em>mnettv</em></abbr>] - **MNetTV**: [<abbr title="netrc machine"><em>mnettv</em></abbr>]
- **MNetTVLive**: [<abbr title="netrc machine"><em>mnettv</em></abbr>]
- **MNetTVRecordings**: [<abbr title="netrc machine"><em>mnettv</em></abbr>]
- **MochaVideo** - **MochaVideo**
- **MoeVideo**: LetitBit video services: moevideo.net, playreplay.net and videochart.net - **MoeVideo**: LetitBit video services: moevideo.net, playreplay.net and videochart.net
- **Mofosex** - **Mofosex**
@ -801,7 +811,9 @@
- **netease:program**: 网易云音乐 - 电台节目 - **netease:program**: 网易云音乐 - 电台节目
- **netease:singer**: 网易云音乐 - 歌手 - **netease:singer**: 网易云音乐 - 歌手
- **netease:song**: 网易云音乐 - **netease:song**: 网易云音乐
- **NetPlus**: [<abbr title="netrc machine"><em>netplus</em></abbr>] - **NetPlusTV**: [<abbr title="netrc machine"><em>netplus</em></abbr>]
- **NetPlusTVLive**: [<abbr title="netrc machine"><em>netplus</em></abbr>]
- **NetPlusTVRecordings**: [<abbr title="netrc machine"><em>netplus</em></abbr>]
- **Netverse** - **Netverse**
- **NetversePlaylist** - **NetversePlaylist**
- **Netzkino** - **Netzkino**
@ -906,6 +918,8 @@
- **orf:radio** - **orf:radio**
- **orf:tvthek**: ORF TVthek - **orf:tvthek**: ORF TVthek
- **OsnatelTV**: [<abbr title="netrc machine"><em>osnateltv</em></abbr>] - **OsnatelTV**: [<abbr title="netrc machine"><em>osnateltv</em></abbr>]
- **OsnatelTVLive**: [<abbr title="netrc machine"><em>osnateltv</em></abbr>]
- **OsnatelTVRecordings**: [<abbr title="netrc machine"><em>osnateltv</em></abbr>]
- **OutsideTV** - **OutsideTV**
- **PacktPub**: [<abbr title="netrc machine"><em>packtpub</em></abbr>] - **PacktPub**: [<abbr title="netrc machine"><em>packtpub</em></abbr>]
- **PacktPubCourse** - **PacktPubCourse**
@ -1013,6 +1027,8 @@
- **qqmusic:singer**: QQ音乐 - 歌手 - **qqmusic:singer**: QQ音乐 - 歌手
- **qqmusic:toplist**: QQ音乐 - 排行榜 - **qqmusic:toplist**: QQ音乐 - 排行榜
- **QuantumTV**: [<abbr title="netrc machine"><em>quantumtv</em></abbr>] - **QuantumTV**: [<abbr title="netrc machine"><em>quantumtv</em></abbr>]
- **QuantumTVLive**: [<abbr title="netrc machine"><em>quantumtv</em></abbr>]
- **QuantumTVRecordings**: [<abbr title="netrc machine"><em>quantumtv</em></abbr>]
- **Qub** - **Qub**
- **R7** - **R7**
- **R7Article** - **R7Article**
@ -1121,7 +1137,11 @@
- **safari:course**: [<abbr title="netrc machine"><em>safari</em></abbr>] safaribooksonline.com online courses - **safari:course**: [<abbr title="netrc machine"><em>safari</em></abbr>] safaribooksonline.com online courses
- **Saitosan** - **Saitosan**
- **SAKTV**: [<abbr title="netrc machine"><em>saktv</em></abbr>] - **SAKTV**: [<abbr title="netrc machine"><em>saktv</em></abbr>]
- **SAKTVLive**: [<abbr title="netrc machine"><em>saktv</em></abbr>]
- **SAKTVRecordings**: [<abbr title="netrc machine"><em>saktv</em></abbr>]
- **SaltTV**: [<abbr title="netrc machine"><em>salttv</em></abbr>] - **SaltTV**: [<abbr title="netrc machine"><em>salttv</em></abbr>]
- **SaltTVLive**: [<abbr title="netrc machine"><em>salttv</em></abbr>]
- **SaltTVRecordings**: [<abbr title="netrc machine"><em>salttv</em></abbr>]
- **SampleFocus** - **SampleFocus**
- **Sapo**: SAPO Vídeos - **Sapo**: SAPO Vídeos
- **savefrom.net** - **savefrom.net**
@ -1494,6 +1514,8 @@
- **VShare** - **VShare**
- **VTM** - **VTM**
- **VTXTV**: [<abbr title="netrc machine"><em>vtxtv</em></abbr>] - **VTXTV**: [<abbr title="netrc machine"><em>vtxtv</em></abbr>]
- **VTXTVLive**: [<abbr title="netrc machine"><em>vtxtv</em></abbr>]
- **VTXTVRecordings**: [<abbr title="netrc machine"><em>vtxtv</em></abbr>]
- **VuClip** - **VuClip**
- **Vupload** - **Vupload**
- **VVVVID** - **VVVVID**
@ -1503,6 +1525,8 @@
- **Wakanim** - **Wakanim**
- **Walla** - **Walla**
- **WalyTV**: [<abbr title="netrc machine"><em>walytv</em></abbr>] - **WalyTV**: [<abbr title="netrc machine"><em>walytv</em></abbr>]
- **WalyTVLive**: [<abbr title="netrc machine"><em>walytv</em></abbr>]
- **WalyTVRecordings**: [<abbr title="netrc machine"><em>walytv</em></abbr>]
- **wasdtv:clip** - **wasdtv:clip**
- **wasdtv:record** - **wasdtv:record**
- **wasdtv:stream** - **wasdtv:stream**

View File

@ -7,8 +7,10 @@ import unittest
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 math
import re
from yt_dlp.jsinterp import JSInterpreter from yt_dlp.jsinterp import JS_Undefined, JSInterpreter
class TestJSInterpreter(unittest.TestCase): class TestJSInterpreter(unittest.TestCase):
@ -66,6 +68,9 @@ class TestJSInterpreter(unittest.TestCase):
jsi = JSInterpreter('function f(){return 0 && 1 || 2;}') jsi = JSInterpreter('function f(){return 0 && 1 || 2;}')
self.assertEqual(jsi.call_function('f'), 2) self.assertEqual(jsi.call_function('f'), 2)
jsi = JSInterpreter('function f(){return 0 ?? 42;}')
self.assertEqual(jsi.call_function('f'), 0)
def test_array_access(self): def test_array_access(self):
jsi = JSInterpreter('function f(){var x = [1,2,3]; x[0] = 4; x[0] = 5; x[2.0] = 7; return x;}') jsi = JSInterpreter('function f(){var x = [1,2,3]; x[0] = 4; x[0] = 5; x[2.0] = 7; return x;}')
self.assertEqual(jsi.call_function('f'), [5, 2, 7]) self.assertEqual(jsi.call_function('f'), [5, 2, 7])
@ -229,6 +234,119 @@ class TestJSInterpreter(unittest.TestCase):
''') ''')
self.assertEqual(jsi.call_function('x')([]), 1) self.assertEqual(jsi.call_function('x')([]), 1)
def test_null(self):
jsi = JSInterpreter('''
function x() { return null; }
''')
self.assertEqual(jsi.call_function('x'), None)
jsi = JSInterpreter('''
function x() { return [null > 0, null < 0, null == 0, null === 0]; }
''')
self.assertEqual(jsi.call_function('x'), [False, False, False, False])
jsi = JSInterpreter('''
function x() { return [null >= 0, null <= 0]; }
''')
self.assertEqual(jsi.call_function('x'), [True, True])
def test_undefined(self):
jsi = JSInterpreter('''
function x() { return undefined === undefined; }
''')
self.assertEqual(jsi.call_function('x'), True)
jsi = JSInterpreter('''
function x() { return undefined; }
''')
self.assertEqual(jsi.call_function('x'), JS_Undefined)
jsi = JSInterpreter('''
function x() { let v; return v; }
''')
self.assertEqual(jsi.call_function('x'), JS_Undefined)
jsi = JSInterpreter('''
function x() { return [undefined === undefined, undefined == undefined, undefined < undefined, undefined > undefined]; }
''')
self.assertEqual(jsi.call_function('x'), [True, True, False, False])
jsi = JSInterpreter('''
function x() { return [undefined === 0, undefined == 0, undefined < 0, undefined > 0]; }
''')
self.assertEqual(jsi.call_function('x'), [False, False, False, False])
jsi = JSInterpreter('''
function x() { return [undefined >= 0, undefined <= 0]; }
''')
self.assertEqual(jsi.call_function('x'), [False, False])
jsi = JSInterpreter('''
function x() { return [undefined > null, undefined < null, undefined == null, undefined === null]; }
''')
self.assertEqual(jsi.call_function('x'), [False, False, True, False])
jsi = JSInterpreter('''
function x() { return [undefined === null, undefined == null, undefined < null, undefined > null]; }
''')
self.assertEqual(jsi.call_function('x'), [False, True, False, False])
jsi = JSInterpreter('''
function x() { let v; return [42+v, v+42, v**42, 42**v, 0**v]; }
''')
for y in jsi.call_function('x'):
self.assertTrue(math.isnan(y))
jsi = JSInterpreter('''
function x() { let v; return v**0; }
''')
self.assertEqual(jsi.call_function('x'), 1)
jsi = JSInterpreter('''
function x() { let v; return [v>42, v<=42, v&&42, 42&&v]; }
''')
self.assertEqual(jsi.call_function('x'), [False, False, JS_Undefined, JS_Undefined])
jsi = JSInterpreter('function x(){return undefined ?? 42; }')
self.assertEqual(jsi.call_function('x'), 42)
def test_object(self):
jsi = JSInterpreter('''
function x() { return {}; }
''')
self.assertEqual(jsi.call_function('x'), {})
jsi = JSInterpreter('''
function x() { let a = {m1: 42, m2: 0 }; return [a["m1"], a.m2]; }
''')
self.assertEqual(jsi.call_function('x'), [42, 0])
jsi = JSInterpreter('''
function x() { let a; return a?.qq; }
''')
self.assertEqual(jsi.call_function('x'), JS_Undefined)
jsi = JSInterpreter('''
function x() { let a = {m1: 42, m2: 0 }; return a?.qq; }
''')
self.assertEqual(jsi.call_function('x'), JS_Undefined)
def test_regex(self):
jsi = JSInterpreter('''
function x() { let a=/,,[/,913,/](,)}/; }
''')
self.assertEqual(jsi.call_function('x'), None)
jsi = JSInterpreter('''
function x() { let a=/,,[/,913,/](,)}/; return a; }
''')
self.assertIsInstance(jsi.call_function('x'), re.Pattern)
jsi = JSInterpreter('''
function x() { let a=/,,[/,913,/](,)}/i; return a; }
''')
self.assertEqual(jsi.call_function('x').flags & re.I, re.I)
if __name__ == '__main__': if __name__ == '__main__':
unittest.main() unittest.main()

View File

@ -444,6 +444,7 @@ class YoutubeDL:
* index: Section number (Optional) * index: Section number (Optional)
force_keyframes_at_cuts: Re-encode the video when downloading ranges to get precise cuts force_keyframes_at_cuts: Re-encode the video when downloading ranges to get precise cuts
noprogress: Do not print the progress bar noprogress: Do not print the progress bar
live_from_start: Whether to download livestreams videos from the start
The following parameters are not used by YoutubeDL itself, they are used by The following parameters are not used by YoutubeDL itself, they are used by
the downloader (see yt_dlp/downloader/common.py): the downloader (see yt_dlp/downloader/common.py):

View File

@ -2200,17 +2200,41 @@ from .youtube import (
from .zapiks import ZapiksIE from .zapiks import ZapiksIE
from .zattoo import ( from .zattoo import (
BBVTVIE, BBVTVIE,
BBVTVLiveIE,
BBVTVRecordingsIE,
EinsUndEinsTVIE, EinsUndEinsTVIE,
EinsUndEinsTVLiveIE,
EinsUndEinsTVRecordingsIE,
EWETVIE, EWETVIE,
EWETVLiveIE,
EWETVRecordingsIE,
GlattvisionTVIE, GlattvisionTVIE,
GlattvisionTVLiveIE,
GlattvisionTVRecordingsIE,
MNetTVIE, MNetTVIE,
NetPlusIE, MNetTVLiveIE,
MNetTVRecordingsIE,
NetPlusTVIE,
NetPlusTVLiveIE,
NetPlusTVRecordingsIE,
OsnatelTVIE, OsnatelTVIE,
OsnatelTVLiveIE,
OsnatelTVRecordingsIE,
QuantumTVIE, QuantumTVIE,
QuantumTVLiveIE,
QuantumTVRecordingsIE,
SaltTVIE, SaltTVIE,
SaltTVLiveIE,
SaltTVRecordingsIE,
SAKTVIE, SAKTVIE,
SAKTVLiveIE,
SAKTVRecordingsIE,
VTXTVIE, VTXTVIE,
VTXTVLiveIE,
VTXTVRecordingsIE,
WalyTVIE, WalyTVIE,
WalyTVLiveIE,
WalyTVRecordingsIE,
ZattooIE, ZattooIE,
ZattooLiveIE, ZattooLiveIE,
ZattooMoviesIE, ZattooMoviesIE,

View File

@ -39,37 +39,42 @@ class InstagramBaseIE(InfoExtractor):
_NETRC_MACHINE = 'instagram' _NETRC_MACHINE = 'instagram'
_IS_LOGGED_IN = False _IS_LOGGED_IN = False
_API_BASE_URL = 'https://i.instagram.com/api/v1'
_LOGIN_URL = 'https://www.instagram.com/accounts/login'
_API_HEADERS = {
'X-IG-App-ID': '936619743392459',
'X-ASBD-ID': '198387',
'X-IG-WWW-Claim': '0',
'Origin': 'https://www.instagram.com',
'Accept': '*/*',
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/104.0.0.0 Safari/537.36',
}
def _perform_login(self, username, password): def _perform_login(self, username, password):
if self._IS_LOGGED_IN: if self._IS_LOGGED_IN:
return return
login_webpage = self._download_webpage( login_webpage = self._download_webpage(
'https://www.instagram.com/accounts/login/', None, self._LOGIN_URL, None, note='Downloading login webpage', errnote='Failed to download login webpage')
note='Downloading login webpage', errnote='Failed to download login webpage')
shared_data = self._parse_json( shared_data = self._parse_json(self._search_regex(
self._search_regex( r'window\._sharedData\s*=\s*({.+?});', login_webpage, 'shared data', default='{}'), None)
r'window\._sharedData\s*=\s*({.+?});',
login_webpage, 'shared data', default='{}'),
None)
login = self._download_json('https://www.instagram.com/accounts/login/ajax/', None, note='Logging in', headers={ login = self._download_json(
'Accept': '*/*', f'{self._LOGIN_URL}/ajax/', None, note='Logging in', headers={
'X-IG-App-ID': '936619743392459', **self._API_HEADERS,
'X-ASBD-ID': '198387', 'X-Requested-With': 'XMLHttpRequest',
'X-IG-WWW-Claim': '0', 'X-CSRFToken': shared_data['config']['csrf_token'],
'X-Requested-With': 'XMLHttpRequest', 'X-Instagram-AJAX': shared_data['rollout_hash'],
'X-CSRFToken': shared_data['config']['csrf_token'], 'Referer': 'https://www.instagram.com/',
'X-Instagram-AJAX': shared_data['rollout_hash'], }, data=urlencode_postdata({
'Referer': 'https://www.instagram.com/', 'enc_password': f'#PWD_INSTAGRAM_BROWSER:0:{int(time.time())}:{password}',
}, data=urlencode_postdata({ 'username': username,
'enc_password': f'#PWD_INSTAGRAM_BROWSER:0:{int(time.time())}:{password}', 'queryParams': '{}',
'username': username, 'optIntoOneTap': 'false',
'queryParams': '{}', 'stopDeletionNonce': '',
'optIntoOneTap': 'false', 'trustedDeviceRecords': '{}',
'stopDeletionNonce': '', }))
'trustedDeviceRecords': '{}',
}))
if not login.get('authenticated'): if not login.get('authenticated'):
if login.get('message'): if login.get('message'):
@ -134,7 +139,7 @@ class InstagramBaseIE(InfoExtractor):
} }
def _extract_product_media(self, product_media): def _extract_product_media(self, product_media):
media_id = product_media.get('code') or product_media.get('id') media_id = product_media.get('code') or _pk_to_id(product_media.get('pk'))
vcodec = product_media.get('video_codec') vcodec = product_media.get('video_codec')
dash_manifest_raw = product_media.get('video_dash_manifest') dash_manifest_raw = product_media.get('video_dash_manifest')
videos_list = product_media.get('video_versions') videos_list = product_media.get('video_versions')
@ -179,7 +184,7 @@ class InstagramBaseIE(InfoExtractor):
user_info = product_info.get('user') or {} user_info = product_info.get('user') or {}
info_dict = { info_dict = {
'id': product_info.get('code') or product_info.get('id'), 'id': product_info.get('code') or _pk_to_id(product_info.get('pk')),
'title': product_info.get('title') or f'Video by {user_info.get("username")}', 'title': product_info.get('title') or f'Video by {user_info.get("username")}',
'description': traverse_obj(product_info, ('caption', 'text'), expected_type=str_or_none), 'description': traverse_obj(product_info, ('caption', 'text'), expected_type=str_or_none),
'timestamp': int_or_none(product_info.get('taken_at')), 'timestamp': int_or_none(product_info.get('taken_at')),
@ -360,49 +365,74 @@ class InstagramIE(InstagramBaseIE):
def _real_extract(self, url): def _real_extract(self, url):
video_id, url = self._match_valid_url(url).group('id', 'url') video_id, url = self._match_valid_url(url).group('id', 'url')
general_info = self._download_json( media, webpage = {}, ''
f'https://www.instagram.com/graphql/query/?query_hash=9f8827793ef34641b2fb195d4d41151c'
f'&variables=%7B"shortcode":"{video_id}",' api_check = self._download_json(
'"parent_comment_count":10,"has_threaded_comments":true}', video_id, fatal=False, errnote=False, f'{self._API_BASE_URL}/web/get_ruling_for_content/?content_type=MEDIA&target_id={_id_to_pk(video_id)}',
headers={ video_id, headers=self._API_HEADERS, fatal=False, note='Setting up session', errnote=False) or {}
'Accept': '*', csrf_token = self._get_cookies('https://www.instagram.com').get('csrftoken')
'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/103.0.0.0 Safari/537.36',
'Authority': 'www.instagram.com', if not csrf_token:
'Referer': 'https://www.instagram.com', self.report_warning('No csrf token set by Instagram API', video_id)
'x-ig-app-id': '936619743392459', elif api_check.get('status') != 'ok':
}) self.report_warning('Instagram API is not granting access', video_id)
media = traverse_obj(general_info, ('data', 'shortcode_media')) or {} else:
if self._get_cookies(url).get('sessionid'):
media.update(traverse_obj(self._download_json(
f'{self._API_BASE_URL}/media/{_id_to_pk(video_id)}/info/', video_id,
fatal=False, note='Downloading video info', headers={
**self._API_HEADERS,
'X-CSRFToken': csrf_token.value,
}), ('items', 0)) or {})
if media:
return self._extract_product(media)
variables = {
'shortcode': video_id,
'child_comment_count': 3,
'fetch_comment_count': 40,
'parent_comment_count': 24,
'has_threaded_comments': True,
}
general_info = self._download_json(
'https://www.instagram.com/graphql/query/', video_id, fatal=False,
headers={
**self._API_HEADERS,
'X-CSRFToken': csrf_token.value,
'X-Requested-With': 'XMLHttpRequest',
'Referer': url,
}, query={
'query_hash': '9f8827793ef34641b2fb195d4d41151c',
'variables': json.dumps(variables, separators=(',', ':')),
})
media.update(traverse_obj(general_info, ('data', 'shortcode_media')) or {})
if not media: if not media:
self.report_warning('General metadata extraction failed', video_id) self.report_warning('General metadata extraction failed (some metadata might be missing).', video_id)
webpage, urlh = self._download_webpage_handle(url, video_id)
shared_data = self._search_json(
r'window\._sharedData\s*=', webpage, 'shared data', video_id, fatal=False) or {}
info = self._download_json( if shared_data and self._LOGIN_URL not in urlh.geturl():
f'https://i.instagram.com/api/v1/media/{_id_to_pk(video_id)}/info/', video_id, media.update(traverse_obj(
fatal=False, note='Downloading video info', errnote=False, headers={ shared_data, ('entry_data', 'PostPage', 0, 'graphql', 'shortcode_media'),
'Accept': '*', ('entry_data', 'PostPage', 0, 'media'), expected_type=dict) or {})
'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/103.0.0.0 Safari/537.36', else:
'Authority': 'www.instagram.com', self.report_warning('Main webpage is locked behind the login page. Retrying with embed webpage')
'Referer': 'https://www.instagram.com', webpage = self._download_webpage(
'x-ig-app-id': '936619743392459', f'{url}/embed/', video_id, note='Downloading embed webpage', fatal=False)
}) additional_data = self._search_json(
if info: r'window\.__additionalDataLoaded\s*\(\s*[^,]+,\s*', webpage, 'additional data', video_id, fatal=False)
media.update(info['items'][0]) if not additional_data:
return self._extract_product(media) self.raise_login_required('Requested content is not available, rate-limit reached or login required')
webpage = self._download_webpage( product_item = traverse_obj(additional_data, ('items', 0), expected_type=dict)
f'https://www.instagram.com/p/{video_id}/embed/', video_id, if product_item:
note='Downloading embed webpage', fatal=False) media.update(product_item)
if not webpage: return self._extract_product(media)
self.raise_login_required('Requested content was not found, the content might be private')
additional_data = self._search_json( media.update(traverse_obj(
r'window\.__additionalDataLoaded\s*\(\s*[^,]+,\s*', webpage, 'additional data', video_id, fatal=False) additional_data, ('graphql', 'shortcode_media'), 'shortcode_media', expected_type=dict) or {})
product_item = traverse_obj(additional_data, ('items', 0), expected_type=dict)
if product_item:
media.update(product_item)
return self._extract_product(media)
media.update(traverse_obj(
additional_data, ('graphql', 'shortcode_media'), 'shortcode_media', expected_type=dict) or {})
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)
@ -649,12 +679,8 @@ class InstagramStoryIE(InstagramBaseIE):
story_info_url = user_id if username != 'highlights' else f'highlight:{story_id}' story_info_url = user_id if username != 'highlights' else f'highlight:{story_id}'
videos = traverse_obj(self._download_json( videos = traverse_obj(self._download_json(
f'https://i.instagram.com/api/v1/feed/reels_media/?reel_ids={story_info_url}', f'{self._API_BASE_URL}/feed/reels_media/?reel_ids={story_info_url}',
story_id, errnote=False, fatal=False, headers={ story_id, errnote=False, fatal=False, headers=self._API_HEADERS), 'reels')
'X-IG-App-ID': 936619743392459,
'X-ASBD-ID': 198387,
'X-IG-WWW-Claim': 0,
}), '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')

View File

@ -156,7 +156,7 @@ class RaiBaseIE(InfoExtractor):
br = int_or_none(tbr) br = int_or_none(tbr)
if len(fmts) == 1 and not br: if len(fmts) == 1 and not br:
br = fmts[0].get('tbr') br = fmts[0].get('tbr')
if br or 0 > 300: if br and br > 300:
tbr = compat_str(math.floor(br / 100) * 100) tbr = compat_str(math.floor(br / 100) * 100)
else: else:
tbr = '250' tbr = '250'

View File

@ -69,6 +69,10 @@ class RedBeeBaseIE(InfoExtractor):
fmts, subs = self._extract_m3u8_formats_and_subtitles( fmts, subs = self._extract_m3u8_formats_and_subtitles(
format['mediaLocator'], asset_id, fatal=False) format['mediaLocator'], asset_id, fatal=False)
if format.get('drm'):
for f in fmts:
f['has_drm'] = True
formats.extend(fmts) formats.extend(fmts)
self._merge_subtitles(subs, target=subtitles) self._merge_subtitles(subs, target=subtitles)
@ -269,8 +273,17 @@ class RTBFIE(RedBeeBaseIE):
embed_page = self._download_webpage( embed_page = self._download_webpage(
'https://www.rtbf.be/auvio/embed/' + ('direct' if live else 'media'), 'https://www.rtbf.be/auvio/embed/' + ('direct' if live else 'media'),
media_id, query={'id': media_id}) media_id, query={'id': media_id})
data = self._parse_json(self._html_search_regex(
r'data-media="([^"]+)"', embed_page, 'media data'), media_id) media_data = self._html_search_regex(r'data-media="([^"]+)"', embed_page, 'media data', fatal=False)
if not media_data:
if re.search(r'<div[^>]+id="js-error-expired"[^>]+class="(?![^"]*hidden)', embed_page):
raise ExtractorError('Livestream has ended.', expected=True)
if re.search(r'<div[^>]+id="js-sso-connect"[^>]+class="(?![^"]*hidden)', embed_page):
self.raise_login_required()
raise ExtractorError('Could not find media data')
data = self._parse_json(media_data, media_id)
error = data.get('error') error = data.get('error')
if error: if error:
@ -280,15 +293,20 @@ class RTBFIE(RedBeeBaseIE):
if provider in self._PROVIDERS: if provider in self._PROVIDERS:
return self.url_result(data['url'], self._PROVIDERS[provider]) return self.url_result(data['url'], self._PROVIDERS[provider])
title = data['subtitle'] title = traverse_obj(data, 'subtitle', 'title')
is_live = data.get('isLive') is_live = data.get('isLive')
height_re = r'-(\d+)p\.' height_re = r'-(\d+)p\.'
formats = [] formats, subtitles = [], {}
m3u8_url = data.get('urlHlsAes128') or data.get('urlHls') # The old api still returns m3u8 and mpd manifest for livestreams, but these are 'fake'
# since all they contain is a 20s video that is completely unrelated.
# https://github.com/yt-dlp/yt-dlp/issues/4656#issuecomment-1214461092
m3u8_url = None if data.get('isLive') else traverse_obj(data, 'urlHlsAes128', 'urlHls')
if m3u8_url: if m3u8_url:
formats.extend(self._extract_m3u8_formats( fmts, subs = self._extract_m3u8_formats_and_subtitles(
m3u8_url, media_id, 'mp4', m3u8_id='hls', fatal=False)) m3u8_url, media_id, 'mp4', m3u8_id='hls', fatal=False)
formats.extend(fmts)
self._merge_subtitles(subs, target=subtitles)
fix_url = lambda x: x.replace('//rtbf-vod.', '//rtbf.') if '/geo/drm/' in x else x fix_url = lambda x: x.replace('//rtbf-vod.', '//rtbf.') if '/geo/drm/' in x else x
http_url = data.get('url') http_url = data.get('url')
@ -319,10 +337,12 @@ class RTBFIE(RedBeeBaseIE):
'height': height, 'height': height,
}) })
mpd_url = data.get('urlDash') mpd_url = None if data.get('isLive') else data.get('urlDash')
if mpd_url and (self.get_param('allow_unplayable_formats') or not data.get('drm')): if mpd_url and (self.get_param('allow_unplayable_formats') or not data.get('drm')):
formats.extend(self._extract_mpd_formats( fmts, subs = self._extract_mpd_formats_and_subtitles(
mpd_url, media_id, mpd_id='dash', fatal=False)) mpd_url, media_id, mpd_id='dash', fatal=False)
formats.extend(fmts)
self._merge_subtitles(subs, target=subtitles)
audio_url = data.get('urlAudio') audio_url = data.get('urlAudio')
if audio_url: if audio_url:
@ -332,7 +352,6 @@ class RTBFIE(RedBeeBaseIE):
'vcodec': 'none', 'vcodec': 'none',
}) })
subtitles = {}
for track in (data.get('tracks') or {}).values(): for track in (data.get('tracks') or {}).values():
sub_url = track.get('url') sub_url = track.get('url')
if not sub_url: if not sub_url:
@ -342,7 +361,7 @@ class RTBFIE(RedBeeBaseIE):
}) })
if not formats: if not formats:
fmts, subs = self._get_formats_and_subtitles(url, media_id) fmts, subs = self._get_formats_and_subtitles(url, f'live_{media_id}' if is_live else media_id)
formats.extend(fmts) formats.extend(fmts)
self._merge_subtitles(subs, target=subtitles) self._merge_subtitles(subs, target=subtitles)

View File

@ -868,7 +868,7 @@ class YoutubeBaseInfoExtractor(InfoExtractor):
else None), else None),
'live_status': ('is_upcoming' if scheduled_timestamp is not None 'live_status': ('is_upcoming' if scheduled_timestamp is not None
else 'was_live' if 'streamed' in time_text.lower() else 'was_live' if 'streamed' in time_text.lower()
else 'is_live' if overlay_style is not None and overlay_style == 'LIVE' or 'live now' in badges else 'is_live' if overlay_style == 'LIVE' or 'live now' in badges
else None), else None),
'release_timestamp': scheduled_timestamp, 'release_timestamp': scheduled_timestamp,
'availability': self._availability(needs_premium='premium' in badges, needs_subscription='members only' in badges) 'availability': self._availability(needs_premium='premium' in badges, needs_subscription='members only' in badges)

View File

@ -236,32 +236,24 @@ class ZattooPlatformBaseIE(InfoExtractor):
def _real_extract(self, url): def _real_extract(self, url):
video_id, record_id = self._match_valid_url(url).groups() video_id, record_id = self._match_valid_url(url).groups()
return self._extract_video(video_id, record_id) return getattr(self, f'_extract_{self._TYPE}')(video_id or record_id)
def _make_valid_url(host): def _create_valid_url(host, match, qs, base_re=None):
return rf'https?://(?:www\.)?{re.escape(host)}/watch/[^/]+?/(?P<id>[0-9]+)[^/]+(?:/(?P<recid>[0-9]+))?' match_base = fr'|{base_re}/(?P<vid1>{match})' if base_re else '(?P<vid1>)'
return rf'''(?x)https?://(?:www\.)?{re.escape(host)}/(?:
[^?#]+\?(?:[^#]+&)?{qs}=(?P<vid2>{match})
{match_base}
)'''
class ZattooBaseIE(ZattooPlatformBaseIE): class ZattooBaseIE(ZattooPlatformBaseIE):
_NETRC_MACHINE = 'zattoo' _NETRC_MACHINE = 'zattoo'
_HOST = 'zattoo.com' _HOST = 'zattoo.com'
@staticmethod
def _create_valid_url(match, qs, base_re=None):
match_base = fr'|{base_re}/(?P<vid1>{match})' if base_re else '(?P<vid1>)'
return rf'''(?x)https?://(?:www\.)?zattoo\.com/(?:
[^?#]+\?(?:[^#]+&)?{qs}=(?P<vid2>{match})
{match_base}
)'''
def _real_extract(self, url):
vid1, vid2 = self._match_valid_url(url).group('vid1', 'vid2')
return getattr(self, f'_extract_{self._TYPE}')(vid1 or vid2)
class ZattooIE(ZattooBaseIE): class ZattooIE(ZattooBaseIE):
_VALID_URL = ZattooBaseIE._create_valid_url(r'\d+', 'program', '(?:program|watch)/[^/]+') _VALID_URL = _create_valid_url(ZattooBaseIE._HOST, r'\d+', 'program', '(?:program|watch)/[^/]+')
_TYPE = 'video' _TYPE = 'video'
_TESTS = [{ _TESTS = [{
'url': 'https://zattoo.com/program/zdf/250170418', 'url': 'https://zattoo.com/program/zdf/250170418',
@ -288,7 +280,7 @@ class ZattooIE(ZattooBaseIE):
class ZattooLiveIE(ZattooBaseIE): class ZattooLiveIE(ZattooBaseIE):
_VALID_URL = ZattooBaseIE._create_valid_url(r'[^/?&#]+', 'channel', 'live') _VALID_URL = _create_valid_url(ZattooBaseIE._HOST, r'[^/?&#]+', 'channel', 'live')
_TYPE = 'live' _TYPE = 'live'
_TESTS = [{ _TESTS = [{
'url': 'https://zattoo.com/channels/german?channel=srf_zwei', 'url': 'https://zattoo.com/channels/german?channel=srf_zwei',
@ -304,7 +296,7 @@ class ZattooLiveIE(ZattooBaseIE):
class ZattooMoviesIE(ZattooBaseIE): class ZattooMoviesIE(ZattooBaseIE):
_VALID_URL = ZattooBaseIE._create_valid_url(r'\w+', 'movie_id', 'vod/movies') _VALID_URL = _create_valid_url(ZattooBaseIE._HOST, r'\w+', 'movie_id', 'vod/movies')
_TYPE = 'ondemand' _TYPE = 'ondemand'
_TESTS = [{ _TESTS = [{
'url': 'https://zattoo.com/vod/movies/7521', 'url': 'https://zattoo.com/vod/movies/7521',
@ -316,7 +308,7 @@ class ZattooMoviesIE(ZattooBaseIE):
class ZattooRecordingsIE(ZattooBaseIE): class ZattooRecordingsIE(ZattooBaseIE):
_VALID_URL = ZattooBaseIE._create_valid_url(r'\d+', 'recording') _VALID_URL = _create_valid_url('zattoo.com', r'\d+', 'recording')
_TYPE = 'record' _TYPE = 'record'
_TESTS = [{ _TESTS = [{
'url': 'https://zattoo.com/recordings?recording=193615508', 'url': 'https://zattoo.com/recordings?recording=193615508',
@ -327,139 +319,547 @@ class ZattooRecordingsIE(ZattooBaseIE):
}] }]
class NetPlusIE(ZattooPlatformBaseIE): class NetPlusTVBaseIE(ZattooPlatformBaseIE):
_NETRC_MACHINE = 'netplus' _NETRC_MACHINE = 'netplus'
_HOST = 'netplus.tv' _HOST = 'netplus.tv'
_API_HOST = 'www.%s' % _HOST _API_HOST = 'www.%s' % _HOST
_VALID_URL = _make_valid_url(_HOST)
class NetPlusTVIE(NetPlusTVBaseIE):
_VALID_URL = _create_valid_url(NetPlusTVBaseIE._HOST, r'\d+', 'program', '(?:program|watch)/[^/]+')
_TYPE = 'video'
_TESTS = [{ _TESTS = [{
'url': 'https://www.netplus.tv/watch/abc/123-abc', 'url': 'https://netplus.tv/program/daserste/210177916',
'only_matching': True,
}, {
'url': 'https://netplus.tv/guide/german?channel=srf1&program=169860555',
'only_matching': True, 'only_matching': True,
}] }]
class MNetTVIE(ZattooPlatformBaseIE): class NetPlusTVLiveIE(NetPlusTVBaseIE):
_VALID_URL = _create_valid_url(NetPlusTVBaseIE._HOST, r'[^/?&#]+', 'channel', 'live')
_TYPE = 'live'
_TESTS = [{
'url': 'https://netplus.tv/channels/german?channel=srf_zwei',
'only_matching': True,
}, {
'url': 'https://netplus.tv/live/srf1',
'only_matching': True,
}]
@classmethod
def suitable(cls, url):
return False if NetPlusTVIE.suitable(url) else super().suitable(url)
class NetPlusTVRecordingsIE(NetPlusTVBaseIE):
_VALID_URL = _create_valid_url(NetPlusTVBaseIE._HOST, r'\d+', 'recording')
_TYPE = 'record'
_TESTS = [{
'url': 'https://netplus.tv/recordings?recording=193615508',
'only_matching': True,
}, {
'url': 'https://netplus.tv/tc/ptc_recordings_all_recordings?recording=193615420',
'only_matching': True,
}]
class MNetTVBaseIE(ZattooPlatformBaseIE):
_NETRC_MACHINE = 'mnettv' _NETRC_MACHINE = 'mnettv'
_HOST = 'tvplus.m-net.de' _HOST = 'tvplus.m-net.de'
_VALID_URL = _make_valid_url(_HOST)
class MNetTVIE(MNetTVBaseIE):
_VALID_URL = _create_valid_url(MNetTVBaseIE._HOST, r'\d+', 'program', '(?:program|watch)/[^/]+')
_TYPE = 'video'
_TESTS = [{ _TESTS = [{
'url': 'https://tvplus.m-net.de/watch/abc/123-abc', 'url': 'https://tvplus.m-net.de/program/daserste/210177916',
'only_matching': True,
}, {
'url': 'https://tvplus.m-net.de/guide/german?channel=srf1&program=169860555',
'only_matching': True, 'only_matching': True,
}] }]
class WalyTVIE(ZattooPlatformBaseIE): class MNetTVLiveIE(MNetTVBaseIE):
_VALID_URL = _create_valid_url(MNetTVBaseIE._HOST, r'[^/?&#]+', 'channel', 'live')
_TYPE = 'live'
_TESTS = [{
'url': 'https://tvplus.m-net.de/channels/german?channel=srf_zwei',
'only_matching': True,
}, {
'url': 'https://tvplus.m-net.de/live/srf1',
'only_matching': True,
}]
@classmethod
def suitable(cls, url):
return False if MNetTVIE.suitable(url) else super().suitable(url)
class MNetTVRecordingsIE(MNetTVBaseIE):
_VALID_URL = _create_valid_url(MNetTVBaseIE._HOST, r'\d+', 'recording')
_TYPE = 'record'
_TESTS = [{
'url': 'https://tvplus.m-net.de/recordings?recording=193615508',
'only_matching': True,
}, {
'url': 'https://tvplus.m-net.de/tc/ptc_recordings_all_recordings?recording=193615420',
'only_matching': True,
}]
class WalyTVBaseIE(ZattooPlatformBaseIE):
_NETRC_MACHINE = 'walytv' _NETRC_MACHINE = 'walytv'
_HOST = 'player.waly.tv' _HOST = 'player.waly.tv'
_VALID_URL = _make_valid_url(_HOST)
class WalyTVIE(WalyTVBaseIE):
_VALID_URL = _create_valid_url(WalyTVBaseIE._HOST, r'\d+', 'program', '(?:program|watch)/[^/]+')
_TYPE = 'video'
_TESTS = [{ _TESTS = [{
'url': 'https://player.waly.tv/watch/abc/123-abc', 'url': 'https://player.waly.tv/program/daserste/210177916',
'only_matching': True,
}, {
'url': 'https://player.waly.tv/guide/german?channel=srf1&program=169860555',
'only_matching': True, 'only_matching': True,
}] }]
class BBVTVIE(ZattooPlatformBaseIE): class WalyTVLiveIE(WalyTVBaseIE):
_VALID_URL = _create_valid_url(WalyTVBaseIE._HOST, r'[^/?&#]+', 'channel', 'live')
_TYPE = 'live'
_TESTS = [{
'url': 'https://player.waly.tv/channels/german?channel=srf_zwei',
'only_matching': True,
}, {
'url': 'https://player.waly.tv/live/srf1',
'only_matching': True,
}]
@classmethod
def suitable(cls, url):
return False if WalyTVIE.suitable(url) else super().suitable(url)
class WalyTVRecordingsIE(WalyTVBaseIE):
_VALID_URL = _create_valid_url(WalyTVBaseIE._HOST, r'\d+', 'recording')
_TYPE = 'record'
_TESTS = [{
'url': 'https://player.waly.tv/recordings?recording=193615508',
'only_matching': True,
}, {
'url': 'https://player.waly.tv/tc/ptc_recordings_all_recordings?recording=193615420',
'only_matching': True,
}]
class BBVTVBaseIE(ZattooPlatformBaseIE):
_NETRC_MACHINE = 'bbvtv' _NETRC_MACHINE = 'bbvtv'
_HOST = 'bbv-tv.net' _HOST = 'bbv-tv.net'
_API_HOST = 'www.%s' % _HOST _API_HOST = 'www.%s' % _HOST
_VALID_URL = _make_valid_url(_HOST)
class BBVTVIE(BBVTVBaseIE):
_VALID_URL = _create_valid_url(BBVTVBaseIE._HOST, r'\d+', 'program', '(?:program|watch)/[^/]+')
_TYPE = 'video'
_TESTS = [{ _TESTS = [{
'url': 'https://www.bbv-tv.net/watch/abc/123-abc', 'url': 'https://bbv-tv.net/program/daserste/210177916',
'only_matching': True,
}, {
'url': 'https://bbv-tv.net/guide/german?channel=srf1&program=169860555',
'only_matching': True, 'only_matching': True,
}] }]
class VTXTVIE(ZattooPlatformBaseIE): class BBVTVLiveIE(BBVTVBaseIE):
_VALID_URL = _create_valid_url(BBVTVBaseIE._HOST, r'[^/?&#]+', 'channel', 'live')
_TYPE = 'live'
_TESTS = [{
'url': 'https://bbv-tv.net/channels/german?channel=srf_zwei',
'only_matching': True,
}, {
'url': 'https://bbv-tv.net/live/srf1',
'only_matching': True,
}]
@classmethod
def suitable(cls, url):
return False if BBVTVIE.suitable(url) else super().suitable(url)
class BBVTVRecordingsIE(BBVTVBaseIE):
_VALID_URL = _create_valid_url(BBVTVBaseIE._HOST, r'\d+', 'recording')
_TYPE = 'record'
_TESTS = [{
'url': 'https://bbv-tv.net/recordings?recording=193615508',
'only_matching': True,
}, {
'url': 'https://bbv-tv.net/tc/ptc_recordings_all_recordings?recording=193615420',
'only_matching': True,
}]
class VTXTVBaseIE(ZattooPlatformBaseIE):
_NETRC_MACHINE = 'vtxtv' _NETRC_MACHINE = 'vtxtv'
_HOST = 'vtxtv.ch' _HOST = 'vtxtv.ch'
_API_HOST = 'www.%s' % _HOST _API_HOST = 'www.%s' % _HOST
_VALID_URL = _make_valid_url(_HOST)
class VTXTVIE(VTXTVBaseIE):
_VALID_URL = _create_valid_url(VTXTVBaseIE._HOST, r'\d+', 'program', '(?:program|watch)/[^/]+')
_TYPE = 'video'
_TESTS = [{ _TESTS = [{
'url': 'https://www.vtxtv.ch/watch/abc/123-abc', 'url': 'https://vtxtv.ch/program/daserste/210177916',
'only_matching': True,
}, {
'url': 'https://vtxtv.ch/guide/german?channel=srf1&program=169860555',
'only_matching': True, 'only_matching': True,
}] }]
class GlattvisionTVIE(ZattooPlatformBaseIE): class VTXTVLiveIE(VTXTVBaseIE):
_VALID_URL = _create_valid_url(VTXTVBaseIE._HOST, r'[^/?&#]+', 'channel', 'live')
_TYPE = 'live'
_TESTS = [{
'url': 'https://vtxtv.ch/channels/german?channel=srf_zwei',
'only_matching': True,
}, {
'url': 'https://vtxtv.ch/live/srf1',
'only_matching': True,
}]
@classmethod
def suitable(cls, url):
return False if VTXTVIE.suitable(url) else super().suitable(url)
class VTXTVRecordingsIE(VTXTVBaseIE):
_VALID_URL = _create_valid_url(VTXTVBaseIE._HOST, r'\d+', 'recording')
_TYPE = 'record'
_TESTS = [{
'url': 'https://vtxtv.ch/recordings?recording=193615508',
'only_matching': True,
}, {
'url': 'https://vtxtv.ch/tc/ptc_recordings_all_recordings?recording=193615420',
'only_matching': True,
}]
class GlattvisionTVBaseIE(ZattooPlatformBaseIE):
_NETRC_MACHINE = 'glattvisiontv' _NETRC_MACHINE = 'glattvisiontv'
_HOST = 'iptv.glattvision.ch' _HOST = 'iptv.glattvision.ch'
_VALID_URL = _make_valid_url(_HOST)
class GlattvisionTVIE(GlattvisionTVBaseIE):
_VALID_URL = _create_valid_url(GlattvisionTVBaseIE._HOST, r'\d+', 'program', '(?:program|watch)/[^/]+')
_TYPE = 'video'
_TESTS = [{ _TESTS = [{
'url': 'https://iptv.glattvision.ch/watch/abc/123-abc', 'url': 'https://iptv.glattvision.ch/program/daserste/210177916',
'only_matching': True,
}, {
'url': 'https://iptv.glattvision.ch/guide/german?channel=srf1&program=169860555',
'only_matching': True, 'only_matching': True,
}] }]
class SAKTVIE(ZattooPlatformBaseIE): class GlattvisionTVLiveIE(GlattvisionTVBaseIE):
_VALID_URL = _create_valid_url(GlattvisionTVBaseIE._HOST, r'[^/?&#]+', 'channel', 'live')
_TYPE = 'live'
_TESTS = [{
'url': 'https://iptv.glattvision.ch/channels/german?channel=srf_zwei',
'only_matching': True,
}, {
'url': 'https://iptv.glattvision.ch/live/srf1',
'only_matching': True,
}]
@classmethod
def suitable(cls, url):
return False if GlattvisionTVIE.suitable(url) else super().suitable(url)
class GlattvisionTVRecordingsIE(GlattvisionTVBaseIE):
_VALID_URL = _create_valid_url(GlattvisionTVBaseIE._HOST, r'\d+', 'recording')
_TYPE = 'record'
_TESTS = [{
'url': 'https://iptv.glattvision.ch/recordings?recording=193615508',
'only_matching': True,
}, {
'url': 'https://iptv.glattvision.ch/tc/ptc_recordings_all_recordings?recording=193615420',
'only_matching': True,
}]
class SAKTVBaseIE(ZattooPlatformBaseIE):
_NETRC_MACHINE = 'saktv' _NETRC_MACHINE = 'saktv'
_HOST = 'saktv.ch' _HOST = 'saktv.ch'
_API_HOST = 'www.%s' % _HOST _API_HOST = 'www.%s' % _HOST
_VALID_URL = _make_valid_url(_HOST)
class SAKTVIE(SAKTVBaseIE):
_VALID_URL = _create_valid_url(SAKTVBaseIE._HOST, r'\d+', 'program', '(?:program|watch)/[^/]+')
_TYPE = 'video'
_TESTS = [{ _TESTS = [{
'url': 'https://www.saktv.ch/watch/abc/123-abc', 'url': 'https://saktv.ch/program/daserste/210177916',
'only_matching': True,
}, {
'url': 'https://saktv.ch/guide/german?channel=srf1&program=169860555',
'only_matching': True, 'only_matching': True,
}] }]
class EWETVIE(ZattooPlatformBaseIE): class SAKTVLiveIE(SAKTVBaseIE):
_VALID_URL = _create_valid_url(SAKTVBaseIE._HOST, r'[^/?&#]+', 'channel', 'live')
_TYPE = 'live'
_TESTS = [{
'url': 'https://saktv.ch/channels/german?channel=srf_zwei',
'only_matching': True,
}, {
'url': 'https://saktv.ch/live/srf1',
'only_matching': True,
}]
@classmethod
def suitable(cls, url):
return False if SAKTVIE.suitable(url) else super().suitable(url)
class SAKTVRecordingsIE(SAKTVBaseIE):
_VALID_URL = _create_valid_url(SAKTVBaseIE._HOST, r'\d+', 'recording')
_TYPE = 'record'
_TESTS = [{
'url': 'https://saktv.ch/recordings?recording=193615508',
'only_matching': True,
}, {
'url': 'https://saktv.ch/tc/ptc_recordings_all_recordings?recording=193615420',
'only_matching': True,
}]
class EWETVBaseIE(ZattooPlatformBaseIE):
_NETRC_MACHINE = 'ewetv' _NETRC_MACHINE = 'ewetv'
_HOST = 'tvonline.ewe.de' _HOST = 'tvonline.ewe.de'
_VALID_URL = _make_valid_url(_HOST)
class EWETVIE(EWETVBaseIE):
_VALID_URL = _create_valid_url(EWETVBaseIE._HOST, r'\d+', 'program', '(?:program|watch)/[^/]+')
_TYPE = 'video'
_TESTS = [{ _TESTS = [{
'url': 'https://tvonline.ewe.de/watch/abc/123-abc', 'url': 'https://tvonline.ewe.de/program/daserste/210177916',
'only_matching': True,
}, {
'url': 'https://tvonline.ewe.de/guide/german?channel=srf1&program=169860555',
'only_matching': True, 'only_matching': True,
}] }]
class QuantumTVIE(ZattooPlatformBaseIE): class EWETVLiveIE(EWETVBaseIE):
_VALID_URL = _create_valid_url(EWETVBaseIE._HOST, r'[^/?&#]+', 'channel', 'live')
_TYPE = 'live'
_TESTS = [{
'url': 'https://tvonline.ewe.de/channels/german?channel=srf_zwei',
'only_matching': True,
}, {
'url': 'https://tvonline.ewe.de/live/srf1',
'only_matching': True,
}]
@classmethod
def suitable(cls, url):
return False if EWETVIE.suitable(url) else super().suitable(url)
class EWETVRecordingsIE(EWETVBaseIE):
_VALID_URL = _create_valid_url(EWETVBaseIE._HOST, r'\d+', 'recording')
_TYPE = 'record'
_TESTS = [{
'url': 'https://tvonline.ewe.de/recordings?recording=193615508',
'only_matching': True,
}, {
'url': 'https://tvonline.ewe.de/tc/ptc_recordings_all_recordings?recording=193615420',
'only_matching': True,
}]
class QuantumTVBaseIE(ZattooPlatformBaseIE):
_NETRC_MACHINE = 'quantumtv' _NETRC_MACHINE = 'quantumtv'
_HOST = 'quantum-tv.com' _HOST = 'quantum-tv.com'
_API_HOST = 'www.%s' % _HOST _API_HOST = 'www.%s' % _HOST
_VALID_URL = _make_valid_url(_HOST)
class QuantumTVIE(QuantumTVBaseIE):
_VALID_URL = _create_valid_url(QuantumTVBaseIE._HOST, r'\d+', 'program', '(?:program|watch)/[^/]+')
_TYPE = 'video'
_TESTS = [{ _TESTS = [{
'url': 'https://www.quantum-tv.com/watch/abc/123-abc', 'url': 'https://quantum-tv.com/program/daserste/210177916',
'only_matching': True,
}, {
'url': 'https://quantum-tv.com/guide/german?channel=srf1&program=169860555',
'only_matching': True, 'only_matching': True,
}] }]
class OsnatelTVIE(ZattooPlatformBaseIE): class QuantumTVLiveIE(QuantumTVBaseIE):
_VALID_URL = _create_valid_url(QuantumTVBaseIE._HOST, r'[^/?&#]+', 'channel', 'live')
_TYPE = 'live'
_TESTS = [{
'url': 'https://quantum-tv.com/channels/german?channel=srf_zwei',
'only_matching': True,
}, {
'url': 'https://quantum-tv.com/live/srf1',
'only_matching': True,
}]
@classmethod
def suitable(cls, url):
return False if QuantumTVIE.suitable(url) else super().suitable(url)
class QuantumTVRecordingsIE(QuantumTVBaseIE):
_VALID_URL = _create_valid_url(QuantumTVBaseIE._HOST, r'\d+', 'recording')
_TYPE = 'record'
_TESTS = [{
'url': 'https://quantum-tv.com/recordings?recording=193615508',
'only_matching': True,
}, {
'url': 'https://quantum-tv.com/tc/ptc_recordings_all_recordings?recording=193615420',
'only_matching': True,
}]
class OsnatelTVBaseIE(ZattooPlatformBaseIE):
_NETRC_MACHINE = 'osnateltv' _NETRC_MACHINE = 'osnateltv'
_HOST = 'tvonline.osnatel.de' _HOST = 'tvonline.osnatel.de'
_VALID_URL = _make_valid_url(_HOST)
class OsnatelTVIE(OsnatelTVBaseIE):
_VALID_URL = _create_valid_url(OsnatelTVBaseIE._HOST, r'\d+', 'program', '(?:program|watch)/[^/]+')
_TYPE = 'video'
_TESTS = [{ _TESTS = [{
'url': 'https://tvonline.osnatel.de/watch/abc/123-abc', 'url': 'https://tvonline.osnatel.de/program/daserste/210177916',
'only_matching': True,
}, {
'url': 'https://tvonline.osnatel.de/guide/german?channel=srf1&program=169860555',
'only_matching': True, 'only_matching': True,
}] }]
class EinsUndEinsTVIE(ZattooPlatformBaseIE): class OsnatelTVLiveIE(OsnatelTVBaseIE):
_VALID_URL = _create_valid_url(OsnatelTVBaseIE._HOST, r'[^/?&#]+', 'channel', 'live')
_TYPE = 'live'
_TESTS = [{
'url': 'https://tvonline.osnatel.de/channels/german?channel=srf_zwei',
'only_matching': True,
}, {
'url': 'https://tvonline.osnatel.de/live/srf1',
'only_matching': True,
}]
@classmethod
def suitable(cls, url):
return False if OsnatelTVIE.suitable(url) else super().suitable(url)
class OsnatelTVRecordingsIE(OsnatelTVBaseIE):
_VALID_URL = _create_valid_url(OsnatelTVBaseIE._HOST, r'\d+', 'recording')
_TYPE = 'record'
_TESTS = [{
'url': 'https://tvonline.osnatel.de/recordings?recording=193615508',
'only_matching': True,
}, {
'url': 'https://tvonline.osnatel.de/tc/ptc_recordings_all_recordings?recording=193615420',
'only_matching': True,
}]
class EinsUndEinsTVBaseIE(ZattooPlatformBaseIE):
_NETRC_MACHINE = '1und1tv' _NETRC_MACHINE = '1und1tv'
_HOST = '1und1.tv' _HOST = '1und1.tv'
_API_HOST = 'www.%s' % _HOST _API_HOST = 'www.%s' % _HOST
_VALID_URL = _make_valid_url(_HOST)
class EinsUndEinsTVIE(EinsUndEinsTVBaseIE):
_VALID_URL = _create_valid_url(EinsUndEinsTVBaseIE._HOST, r'\d+', 'program', '(?:program|watch)/[^/]+')
_TYPE = 'video'
_TESTS = [{ _TESTS = [{
'url': 'https://www.1und1.tv/watch/abc/123-abc', 'url': 'https://1und1.tv/program/daserste/210177916',
'only_matching': True,
}, {
'url': 'https://1und1.tv/guide/german?channel=srf1&program=169860555',
'only_matching': True, 'only_matching': True,
}] }]
class SaltTVIE(ZattooPlatformBaseIE): class EinsUndEinsTVLiveIE(EinsUndEinsTVBaseIE):
_VALID_URL = _create_valid_url(EinsUndEinsTVBaseIE._HOST, r'[^/?&#]+', 'channel', 'live')
_TYPE = 'live'
_TESTS = [{
'url': 'https://1und1.tv/channels/german?channel=srf_zwei',
'only_matching': True,
}, {
'url': 'https://1und1.tv/live/srf1',
'only_matching': True,
}]
@classmethod
def suitable(cls, url):
return False if EinsUndEinsTVIE.suitable(url) else super().suitable(url)
class EinsUndEinsTVRecordingsIE(EinsUndEinsTVBaseIE):
_VALID_URL = _create_valid_url(EinsUndEinsTVBaseIE._HOST, r'\d+', 'recording')
_TYPE = 'record'
_TESTS = [{
'url': 'https://1und1.tv/recordings?recording=193615508',
'only_matching': True,
}, {
'url': 'https://1und1.tv/tc/ptc_recordings_all_recordings?recording=193615420',
'only_matching': True,
}]
class SaltTVBaseIE(ZattooPlatformBaseIE):
_NETRC_MACHINE = 'salttv' _NETRC_MACHINE = 'salttv'
_HOST = 'tv.salt.ch' _HOST = 'tv.salt.ch'
_VALID_URL = _make_valid_url(_HOST)
class SaltTVIE(SaltTVBaseIE):
_VALID_URL = _create_valid_url(SaltTVBaseIE._HOST, r'\d+', 'program', '(?:program|watch)/[^/]+')
_TYPE = 'video'
_TESTS = [{ _TESTS = [{
'url': 'https://tv.salt.ch/watch/abc/123-abc', 'url': 'https://tv.salt.ch/program/daserste/210177916',
'only_matching': True,
}, {
'url': 'https://tv.salt.ch/guide/german?channel=srf1&program=169860555',
'only_matching': True,
}]
class SaltTVLiveIE(SaltTVBaseIE):
_VALID_URL = _create_valid_url(SaltTVBaseIE._HOST, r'[^/?&#]+', 'channel', 'live')
_TYPE = 'live'
_TESTS = [{
'url': 'https://tv.salt.ch/channels/german?channel=srf_zwei',
'only_matching': True,
}, {
'url': 'https://tv.salt.ch/live/srf1',
'only_matching': True,
}]
@classmethod
def suitable(cls, url):
return False if SaltTVIE.suitable(url) else super().suitable(url)
class SaltTVRecordingsIE(SaltTVBaseIE):
_VALID_URL = _create_valid_url(SaltTVBaseIE._HOST, r'\d+', 'recording')
_TYPE = 'record'
_TESTS = [{
'url': 'https://tv.salt.ch/recordings?recording=193615508',
'only_matching': True,
}, {
'url': 'https://tv.salt.ch/tc/ptc_recordings_all_recordings?recording=193615420',
'only_matching': True, 'only_matching': True,
}] }]

View File

@ -16,50 +16,69 @@ from .utils import (
write_string, write_string,
) )
_NAME_RE = r'[a-zA-Z_$][\w$]*'
# Ref: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Operator_Precedence def _js_bit_op(op):
_OPERATORS = { # None => Defined in JSInterpreter._operator def wrapped(a, b):
'?': None, def zeroise(x):
return 0 if x in (None, JS_Undefined) else x
return op(zeroise(a), zeroise(b))
'||': None, return wrapped
'&&': None,
'&': lambda a, b: (a or 0) & (b or 0),
'|': lambda a, b: (a or 0) | (b or 0),
'^': lambda a, b: (a or 0) ^ (b or 0),
'===': operator.is_,
'!==': operator.is_not,
'==': operator.eq,
'!=': operator.ne,
'<=': lambda a, b: (a or 0) <= (b or 0),
'>=': lambda a, b: (a or 0) >= (b or 0),
'<': lambda a, b: (a or 0) < (b or 0),
'>': lambda a, b: (a or 0) > (b or 0),
'>>': operator.rshift,
'<<': operator.lshift,
'+': lambda a, b: (a or 0) + (b or 0),
'-': lambda a, b: (a or 0) - (b or 0),
'*': lambda a, b: (a or 0) * (b or 0),
'/': lambda a, b: (a or 0) / b if b else float('NaN'),
'%': lambda a, b: (a or 0) % b if b else float('NaN'),
'**': operator.pow,
}
_COMP_OPERATORS = {'===', '!==', '==', '!=', '<=', '>=', '<', '>'}
_MATCHING_PARENS = dict(zip('({[', ')}]'))
_QUOTES = '\'"/'
def _ternary(cndn, if_true=True, if_false=False): def _js_arith_op(op):
def wrapped(a, b):
if JS_Undefined in (a, b):
return float('nan')
return op(a or 0, b or 0)
return wrapped
def _js_div(a, b):
if JS_Undefined in (a, b) or not (a and b):
return float('nan')
return (a or 0) / b if b else float('inf')
def _js_mod(a, b):
if JS_Undefined in (a, b) or not b:
return float('nan')
return (a or 0) % b
def _js_exp(a, b):
if not b:
return 1 # even 0 ** 0 !!
elif JS_Undefined in (a, b):
return float('nan')
return (a or 0) ** b
def _js_eq_op(op):
def wrapped(a, b):
if {a, b} <= {None, JS_Undefined}:
return op(a, a)
return op(a, b)
return wrapped
def _js_comp_op(op):
def wrapped(a, b):
if JS_Undefined in (a, b):
return False
return op(a or 0, b or 0)
return wrapped
def _js_ternary(cndn, if_true=True, if_false=False):
"""Simulate JS's ternary operator (cndn?if_true:if_false)""" """Simulate JS's ternary operator (cndn?if_true:if_false)"""
if cndn in (False, None, 0, ''): if cndn in (False, None, 0, '', JS_Undefined):
return if_false return if_false
with contextlib.suppress(TypeError): with contextlib.suppress(TypeError):
if math.isnan(cndn): # NB: NaN cannot be checked by membership if math.isnan(cndn): # NB: NaN cannot be checked by membership
@ -67,6 +86,50 @@ def _ternary(cndn, if_true=True, if_false=False):
return if_true return if_true
# Ref: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Operator_Precedence
_OPERATORS = { # None => Defined in JSInterpreter._operator
'?': None,
'??': None,
'||': None,
'&&': None,
'|': _js_bit_op(operator.or_),
'^': _js_bit_op(operator.xor),
'&': _js_bit_op(operator.and_),
'===': operator.is_,
'==': _js_eq_op(operator.eq),
'!==': operator.is_not,
'!=': _js_eq_op(operator.ne),
'<=': _js_comp_op(operator.le),
'>=': _js_comp_op(operator.ge),
'<': _js_comp_op(operator.lt),
'>': _js_comp_op(operator.gt),
'>>': _js_bit_op(operator.rshift),
'<<': _js_bit_op(operator.lshift),
'+': _js_arith_op(operator.add),
'-': _js_arith_op(operator.sub),
'*': _js_arith_op(operator.mul),
'/': _js_div,
'%': _js_mod,
'**': _js_exp,
}
_COMP_OPERATORS = {'===', '!==', '==', '!=', '<=', '>=', '<', '>'}
_NAME_RE = r'[a-zA-Z_$][\w$]*'
_MATCHING_PARENS = dict(zip(*zip('()', '{}', '[]')))
_QUOTES = '\'"/'
class JS_Undefined:
pass
class JS_Break(ExtractorError): class JS_Break(ExtractorError):
def __init__(self): def __init__(self):
ExtractorError.__init__(self, 'Invalid break') ExtractorError.__init__(self, 'Invalid break')
@ -119,6 +182,21 @@ class Debugger:
class JSInterpreter: class JSInterpreter:
__named_object_counter = 0 __named_object_counter = 0
_RE_FLAGS = {
# special knowledge: Python's re flags are bitmask values, current max 128
# invent new bitmask values well above that for literal parsing
# TODO: new pattern class to execute matches with these flags
'd': 1024, # Generate indices for substring matches
'g': 2048, # Global search
'i': re.I, # Case-insensitive search
'm': re.M, # Multi-line search
's': re.S, # Allows . to match newline characters
'u': re.U, # Treat a pattern as a sequence of unicode code points
'y': 4096, # Perform a "sticky" search that matches starting at the current position in the target string
}
_EXC_NAME = '__yt_dlp_exception__'
def __init__(self, code, objects=None): def __init__(self, code, objects=None):
self.code, self._functions = code, {} self.code, self._functions = code, {}
self._objects = {} if objects is None else objects self._objects = {} if objects is None else objects
@ -135,6 +213,17 @@ class JSInterpreter:
namespace[name] = obj namespace[name] = obj
return name return name
@classmethod
def _regex_flags(cls, expr):
flags = 0
if not expr:
return flags, expr
for idx, ch in enumerate(expr):
if ch not in cls._RE_FLAGS:
break
flags |= cls._RE_FLAGS[ch]
return flags, expr[idx + 1:]
@staticmethod @staticmethod
def _separate(expr, delim=',', max_split=None): def _separate(expr, delim=',', max_split=None):
OP_CHARS = '+-*/%&|^=<>!,;' OP_CHARS = '+-*/%&|^=<>!,;'
@ -178,10 +267,13 @@ class JSInterpreter:
def _operator(self, op, left_val, right_expr, expr, local_vars, allow_recursion): def _operator(self, op, left_val, right_expr, expr, local_vars, allow_recursion):
if op in ('||', '&&'): if op in ('||', '&&'):
if (op == '&&') ^ _ternary(left_val): if (op == '&&') ^ _js_ternary(left_val):
return left_val # short circuiting return left_val # short circuiting
elif op == '??':
if left_val not in (None, JS_Undefined):
return left_val
elif op == '?': elif op == '?':
right_expr = _ternary(left_val, *self._separate(right_expr, ':', 1)) right_expr = _js_ternary(left_val, *self._separate(right_expr, ':', 1))
right_val = self.interpret_expression(right_expr, local_vars, allow_recursion) right_val = self.interpret_expression(right_expr, local_vars, allow_recursion)
if not _OPERATORS.get(op): if not _OPERATORS.get(op):
@ -192,12 +284,14 @@ class JSInterpreter:
except Exception as e: except Exception as e:
raise self.Exception(f'Failed to evaluate {left_val!r} {op} {right_val!r}', expr, cause=e) raise self.Exception(f'Failed to evaluate {left_val!r} {op} {right_val!r}', expr, cause=e)
def _index(self, obj, idx): def _index(self, obj, idx, allow_undefined=False):
if idx == 'length': if idx == 'length':
return len(obj) return len(obj)
try: try:
return obj[int(idx)] if isinstance(obj, list) else obj[idx] return obj[int(idx)] if isinstance(obj, list) else obj[idx]
except Exception as e: except Exception as e:
if allow_undefined:
return JS_Undefined
raise self.Exception(f'Cannot get index {idx}', repr(obj), cause=e) raise self.Exception(f'Cannot get index {idx}', repr(obj), cause=e)
def _dump(self, obj, namespace): def _dump(self, obj, namespace):
@ -233,8 +327,8 @@ class JSInterpreter:
if expr[0] in _QUOTES: if expr[0] in _QUOTES:
inner, outer = self._separate(expr, expr[0], 1) inner, outer = self._separate(expr, expr[0], 1)
if expr[0] == '/': if expr[0] == '/':
inner = inner[1:].replace('"', R'\"') flags, outer = self._regex_flags(outer)
inner = re.compile(json.loads(js_to_json(f'"{inner}"', strict=True))) inner = re.compile(inner[1:], flags=flags)
else: else:
inner = json.loads(js_to_json(f'{inner}{expr[0]}', strict=True)) inner = json.loads(js_to_json(f'{inner}{expr[0]}', strict=True))
if not outer: if not outer:
@ -259,6 +353,17 @@ class JSInterpreter:
if expr.startswith('{'): if expr.startswith('{'):
inner, outer = self._separate_at_paren(expr, '}') inner, outer = self._separate_at_paren(expr, '}')
# Look for Map first
sub_expressions = [list(self._separate(sub_expr.strip(), ':', 1)) for sub_expr in self._separate(inner)]
if all(len(sub_expr) == 2 for sub_expr in sub_expressions):
def dict_item(key, val):
val = self.interpret_expression(val, local_vars, allow_recursion)
if re.match(_NAME_RE, key):
return key, val
return self.interpret_expression(key, local_vars, allow_recursion), val
return dict(dict_item(k, v) for k, v in sub_expressions), should_return
inner, should_abort = self.interpret_statement(inner, local_vars, allow_recursion) inner, should_abort = self.interpret_statement(inner, local_vars, allow_recursion)
if not outer or should_abort: if not outer or should_abort:
return inner, should_abort or should_return return inner, should_abort or should_return
@ -295,17 +400,17 @@ class JSInterpreter:
if should_abort: if should_abort:
return ret, True return ret, True
except JS_Throw as e: except JS_Throw as e:
local_vars['__ytdlp_exception__'] = e.error local_vars[self._EXC_NAME] = e.error
except Exception as e: except Exception as e:
# XXX: This works for now, but makes debugging future issues very hard # XXX: This works for now, but makes debugging future issues very hard
local_vars['__ytdlp_exception__'] = e local_vars[self._EXC_NAME] = e
ret, should_abort = self.interpret_statement(expr, local_vars, allow_recursion) ret, should_abort = self.interpret_statement(expr, local_vars, allow_recursion)
return ret, should_abort or should_return return ret, should_abort or should_return
elif m and m.group('catch'): elif m and m.group('catch'):
catch_expr, expr = self._separate_at_paren(expr[m.end():], '}') catch_expr, expr = self._separate_at_paren(expr[m.end():], '}')
if '__ytdlp_exception__' in local_vars: if self._EXC_NAME in local_vars:
catch_vars = local_vars.new_child({m.group('err'): local_vars.pop('__ytdlp_exception__')}) catch_vars = local_vars.new_child({m.group('err'): local_vars.pop(self._EXC_NAME)})
ret, should_abort = self.interpret_statement(catch_expr, catch_vars, allow_recursion) ret, should_abort = self.interpret_statement(catch_expr, catch_vars, allow_recursion)
if should_abort: if should_abort:
return ret, True return ret, True
@ -328,7 +433,7 @@ class JSInterpreter:
start, cndn, increment = self._separate(constructor, ';') start, cndn, increment = self._separate(constructor, ';')
self.interpret_expression(start, local_vars, allow_recursion) self.interpret_expression(start, local_vars, allow_recursion)
while True: while True:
if not _ternary(self.interpret_expression(cndn, local_vars, allow_recursion)): if not _js_ternary(self.interpret_expression(cndn, local_vars, allow_recursion)):
break break
try: try:
ret, should_abort = self.interpret_statement(body, local_vars, allow_recursion) ret, should_abort = self.interpret_statement(body, local_vars, allow_recursion)
@ -397,13 +502,13 @@ class JSInterpreter:
(?P<assign> (?P<assign>
(?P<out>{_NAME_RE})(?:\[(?P<index>[^\]]+?)\])?\s* (?P<out>{_NAME_RE})(?:\[(?P<index>[^\]]+?)\])?\s*
(?P<op>{"|".join(map(re.escape, set(_OPERATORS) - _COMP_OPERATORS))})? (?P<op>{"|".join(map(re.escape, set(_OPERATORS) - _COMP_OPERATORS))})?
=(?P<expr>.*)$ =(?!=)(?P<expr>.*)$
)|(?P<return> )|(?P<return>
(?!if|return|true|false|null|undefined)(?P<name>{_NAME_RE})$ (?!if|return|true|false|null|undefined)(?P<name>{_NAME_RE})$
)|(?P<indexing> )|(?P<indexing>
(?P<in>{_NAME_RE})\[(?P<idx>.+)\]$ (?P<in>{_NAME_RE})\[(?P<idx>.+)\]$
)|(?P<attribute> )|(?P<attribute>
(?P<var>{_NAME_RE})(?:\.(?P<member>[^(]+)|\[(?P<member2>[^\]]+)\])\s* (?P<var>{_NAME_RE})(?:(?P<nullish>\?)?\.(?P<member>[^(]+)|\[(?P<member2>[^\]]+)\])\s*
)|(?P<function> )|(?P<function>
(?P<fname>{_NAME_RE})\((?P<args>.*)\)$ (?P<fname>{_NAME_RE})\((?P<args>.*)\)$
)''', expr) )''', expr)
@ -414,7 +519,7 @@ class JSInterpreter:
local_vars[m.group('out')] = self._operator( local_vars[m.group('out')] = self._operator(
m.group('op'), left_val, m.group('expr'), expr, local_vars, allow_recursion) m.group('op'), left_val, m.group('expr'), expr, local_vars, allow_recursion)
return local_vars[m.group('out')], should_return return local_vars[m.group('out')], should_return
elif left_val is None: elif left_val in (None, JS_Undefined):
raise self.Exception(f'Cannot index undefined variable {m.group("out")}', expr) raise self.Exception(f'Cannot index undefined variable {m.group("out")}', expr)
idx = self.interpret_expression(m.group('index'), local_vars, allow_recursion) idx = self.interpret_expression(m.group('index'), local_vars, allow_recursion)
@ -432,9 +537,11 @@ class JSInterpreter:
raise JS_Break() raise JS_Break()
elif expr == 'continue': elif expr == 'continue':
raise JS_Continue() raise JS_Continue()
elif expr == 'undefined':
return JS_Undefined, should_return
elif m and m.group('return'): elif m and m.group('return'):
return local_vars[m.group('name')], should_return return local_vars.get(m.group('name'), JS_Undefined), should_return
with contextlib.suppress(ValueError): with contextlib.suppress(ValueError):
return json.loads(js_to_json(expr, strict=True)), should_return return json.loads(js_to_json(expr, strict=True)), should_return
@ -447,8 +554,11 @@ class JSInterpreter:
for op in _OPERATORS: for op in _OPERATORS:
separated = list(self._separate(expr, op)) separated = list(self._separate(expr, op))
right_expr = separated.pop() right_expr = separated.pop()
while op in '<>*-' and len(separated) > 1 and not separated[-1].strip(): while True:
separated.pop() if op in '?<>*-' and len(separated) > 1 and not separated[-1].strip():
separated.pop()
elif not (separated and op == '?' and right_expr.startswith('.')):
break
right_expr = f'{op}{right_expr}' right_expr = f'{op}{right_expr}'
if op != '-': if op != '-':
right_expr = f'{separated.pop()}{op}{right_expr}' right_expr = f'{separated.pop()}{op}{right_expr}'
@ -458,8 +568,7 @@ class JSInterpreter:
return self._operator(op, left_val, right_expr, expr, local_vars, allow_recursion), should_return return self._operator(op, left_val, right_expr, expr, local_vars, allow_recursion), should_return
if m and m.group('attribute'): if m and m.group('attribute'):
variable = m.group('var') variable, member, nullish = m.group('var', 'member', 'nullish')
member = m.group('member')
if not member: if not member:
member = self.interpret_expression(m.group('member2'), local_vars, allow_recursion) member = self.interpret_expression(m.group('member2'), local_vars, allow_recursion)
arg_str = expr[m.end():] arg_str = expr[m.end():]
@ -486,12 +595,19 @@ class JSInterpreter:
obj = local_vars.get(variable, types.get(variable, NO_DEFAULT)) obj = local_vars.get(variable, types.get(variable, NO_DEFAULT))
if obj is NO_DEFAULT: if obj is NO_DEFAULT:
if variable not in self._objects: if variable not in self._objects:
self._objects[variable] = self.extract_object(variable) try:
obj = self._objects[variable] self._objects[variable] = self.extract_object(variable)
except self.Exception:
if not nullish:
raise
obj = self._objects.get(variable, JS_Undefined)
if nullish and obj is JS_Undefined:
return JS_Undefined
# Member access # Member access
if arg_str is None: if arg_str is None:
return self._index(obj, member) return self._index(obj, member, nullish)
# Function call # Function call
argvals = [ argvals = [

View File

@ -1,8 +1,8 @@
# Autogenerated by devscripts/update-version.py # Autogenerated by devscripts/update-version.py
__version__ = '2022.08.14' __version__ = '2022.08.19'
RELEASE_GIT_HEAD = '55937202b' RELEASE_GIT_HEAD = '48c88e088'
VARIANT = None VARIANT = None