Compare commits

..
122 Commits
Author SHA1 Message Date
pukkandan fac988053f Release 2021.05.11
* and some documentation improvements
2021-05-11 13:35:05 +05:30
pukkandan 61241abbb0 [generic] Respect the encoding in manifest 2021-05-11 13:32:03 +05:30
pukkandan 53ed7066ab Option --compat-options to revert some of yt-dlp's changes
* Deprecates `--list-formats-as-table`, `--list-formats-old`
2021-05-11 13:30:48 +05:30
pukkandan a61f4b287b Deprecate support for python versions < 3.6
Closes #267
2021-05-09 04:32:23 +05:30
pukkandan 486fb17975 Remove -l, -t, -A completely and disable --auto-number, --title, --literal, --id 2021-05-09 04:22:29 +05:30
pukkandan 2f567473c6 [Plugins] Prioritize plugins over standard extractors
and prevent plugins from overwriting the standard extractor classes

Closes #304
2021-05-09 04:22:27 +05:30
pukkandan 000ee7ef34 [fragment] Make sure first segment is not skipped 2021-05-09 04:22:26 +05:30
pukkandan 41d1cca328 Update to ytdl-commit-a726009
[blinkx] Remove extractor
https://github.com/ytdl-org/youtube-dl/commit/a7260099873acc6dc7d76cafad2f6b139087afd0
2021-05-06 21:31:20 +05:30
pukkandanandGitHub 717297545b Fix playlist_index and add playlist_autonumber (#302)
Now `playlist_index` is always the position of the video in the actual playlist and `playlist_autonumber` is the position of the item in the playlist queue
2021-05-06 20:56:19 +05:30
pukkandan e8e738406a Add experimental option --check-formats to test the URLs before format selection 2021-05-06 20:50:44 +05:30
pukkandan e625be0d10 Improve output template internal formatting
* Allow slicing lists/strings using `field.start:end:step`
* A field can also be used as offset like `field1+num+field2`
* A default value can be given using `field|default`
* Capture all format strings and set it to `None` if invalid. This prevents invalid fields from causing errors
2021-05-06 20:28:58 +05:30
pukkandan 12e73423f1 [plutotv] Fix format extraction for some urls
* And fallback to the first urls if ad-free urls can't be found
Closes #299
2021-05-06 20:28:57 +05:30
pukkandan 7700b37f39 [plutotv] Extract subtitles from manifests 2021-05-06 20:28:56 +05:30
AshishandGitHub c28cfda81f [SonyLiv] Fix title and series extraction (#301)
Authored by: Ashish0804
2021-05-06 20:27:43 +05:30
pukkandan 848887eb7a [downloader] Fix quiet and to_stderr 2021-05-04 22:38:10 +05:30
pukkandan 3158150cb7 [utils] Add network_exceptions 2021-05-04 22:36:18 +05:30
pukkandan 6ef6bcbd6b [fragment] Ensure the file is closed on error 2021-05-04 22:27:44 +05:30
pukkandan 06425e9621 [blinkx] Minor fix
Fixes: https://github.com/ytdl-org/youtube-dl/issues/28941
2021-05-04 22:27:44 +05:30
pukkandan 4d224a3022 [embedthumbnail] Fix bug where jpeg thumbnails were converted again
Closes #297
2021-05-04 22:18:40 +05:30
pukkandan f59ae58163 Fix number of digits in %(playlist_index)s
When used with `--playlist-(items|start|end)`, the number of digits should depend on the last index in the playlist, not number of items
2021-05-03 22:49:05 +05:30
pukkandan 0d1bb027aa Move option warnings to YoutubeDL
Previously, these warnings did not obey `--no-warnings` and did not output colors
2021-05-03 22:49:04 +05:30
pukkandan 4cd0a709aa Fix preload_download_archive writing verbose message to stdout
* And move it after all deprecated warnings
2021-05-03 22:49:03 +05:30
pukkandan 1815d1028b [zee5] Fix py2 compatibility 2021-05-03 22:49:03 +05:30
The Hatsune DaishiandGitHub 0fa9a1e236 [whowatch] Add extractor #292
closes #223

Authored by: nao20010128nao 
Modified from: https://github.com/nao20010128nao/ytdl-patched/blob/9e4a0e061a558cdb05a618e27f47ca0ac56ece94/youtube_dl/extractor/whowatch.py
2021-05-02 19:43:37 +05:30
pukkandan eb55bad5a0 [aria2c] Fix whitespace being stripped off
Closes #276
2021-05-02 14:03:13 +05:30
pukkandan cc0ec3e161 Do not strip out whitespaces in -o and -P
Related: https://github.com/yt-dlp/yt-dlp/issues/276#issuecomment-827361652
2021-05-02 14:03:12 +05:30
pukkandan 80185155a1 [ukcolumn] Add Extractor
Closes #287
2021-05-02 13:57:50 +05:30
pukkandan c755f1901f [CBS] Improve _VALID_URL to support movies
Closes #290
Tested by: BeeMuffins
2021-05-01 21:32:14 +05:30
pukkandan 68b91dc905 [youtube] Add oembed to reserved names 2021-05-01 21:24:31 +05:30
pukkandan 88f06afc0c [rmcdecouverte] Improve _VALID_URL
Closes #291
2021-05-01 21:24:31 +05:30
CXwudiandGitHub 40078a55e2 [niconico] Fix bug in thumbnail extraction #289
Bug from: 6b1d8c1e30
Authored by: CXwudi
2021-05-01 19:35:47 +05:30
pukkandan d2558234cf [utils] Escape URL while sanitizing
Closes #263

While this fixes the issue in question, it does not try to address the root-cause of the problem
Refer: 915f911e36, f5fa042c82
2021-04-29 05:20:50 +05:30
pukkandan f5fa042c82 Revert "[utils] Encode URLs in YoutubeDLCookieProcessor"
This reverts commit 915f911e36.

When the request is copied, `unredirected_hdrs` are not copied, which causes issues elsewhere
Reopens #263
2021-04-29 05:20:18 +05:30
pukkandan 07e4a40a9a [crackle] Improve extraction (See desc)
Closes #282

* Refactor authorization as an extension to `_download_json`
* Better error messages and warnings
* Respect `--ignore-no-formats-error`
* Extract subtitles from manifests
* Try with crackle's geo-location service if all hard-coded countries fail
2021-04-29 05:20:16 +05:30
pukkandan e28f1c0ae8 [cleanup] Fix linter and some typos
* Also remove inconsistent use of `"` in setup.py
2021-04-28 19:59:40 +05:30
pukkandan ef39f8600a [curiositystream] Fix collections
Closes #277

* A bug with authentication was reported in <https://github.com/yt-dlp/yt-dlp/issues/277#issuecomment-828254721> but cannot be tested without an account
2021-04-28 19:29:33 +05:30
pukkandan 2291dbce2a [niconico] Fix HLS formats
Closes #171

* The structure of the API JSON was changed
* Smile Video seems to be no longer available. So remove the warning
* Move ping to downloader
* Change heartbeat interval to 40sec
* Remove unnecessary API headers

Authored-by: CXwudi, tsukumijima, nao20010128nao, pukkandan
Tested by: tsukumijima
2021-04-28 19:18:29 +05:30
pukkandan 58f197b76c Revert "[core] be able to hand over id and title using url_result"
This reverts commit 0704d2224b.

This is a commit from `youtube-dlc`. It is not clear what the original purpose of this was. It seems to be a way for extractors to pass `title` and `id` through when the entry is processed by another extractor

* But `title` can already be passed through using `url_transparent`
* `id` is never supposed to be passed through since it could cause issues with archiving
2021-04-28 19:18:06 +05:30
pukkandan 895b0931e5 [youtube:tab] Detect playlists inside community posts 2021-04-28 19:18:06 +05:30
pukkandan 1ad047d0f7 [nebula] Move to nebula.app
Closes #272
Tested by: Lamieur
2021-04-28 19:18:06 +05:30
pukkandanandGitHub be6202f12b Subtitle extraction from streaming media manifests #247
Authored by fstirlitz
Modified from: https://github.com/ytdl-org/youtube-dl/pull/6144

Closes: #73
Fixes:
https://github.com/ytdl-org/youtube-dl/issues/6106
https://github.com/ytdl-org/youtube-dl/issues/14977
https://github.com/ytdl-org/youtube-dl/issues/21438
https://github.com/ytdl-org/youtube-dl/issues/23609
https://github.com/ytdl-org/youtube-dl/issues/28132

Might also fix (untested):
https://github.com/ytdl-org/youtube-dl/issues/15424
https://github.com/ytdl-org/youtube-dl/issues/18267
https://github.com/ytdl-org/youtube-dl/issues/23899
https://github.com/ytdl-org/youtube-dl/issues/24375
https://github.com/ytdl-org/youtube-dl/issues/24595
https://github.com/ytdl-org/youtube-dl/issues/27899

Related:
https://github.com/ytdl-org/youtube-dl/issues/22379
https://github.com/ytdl-org/youtube-dl/pull/24517
https://github.com/ytdl-org/youtube-dl/pull/24886
https://github.com/ytdl-org/youtube-dl/pull/27215

Notes:
* The functions `extractor.common._extract_..._formats` are still kept for compatibility
* Only some extractors have currently been moved to using `_extract_..._formats_and_subtitles`
* Direct subtitle manifests (without a master) are not supported and are wrongly identified as containing video formats
* AES support is untested
* The fragmented TTML subtitles extracted from DASH/ISM are valid, but are unsupported by `ffmpeg` and most video players
    * Their XML fragments can be dumped using `ffmpeg -i in.mp4 -f data -map 0 -c copy out.ttml`.
        Once the unnecessary headers are stripped out of this, it becomes a valid self-contained ttml file
    * The ttml subs downloaded from DASH manifests can also be directly opened with <https://github.com/SubtitleEdit>
* Fragmented WebVTT files extracted from DASH/ISM are also unsupported by most tools
    * Unlike the ttml files, the XML fragments of these cannot be dumped using `ffmpeg`
    * The webtt subs extracted from DASH can be parsed by <https://github.com/gpac/gpac>
    * But validity of the those extracted from ISM are untested
2021-04-28 19:02:43 +05:30
Felix S e8f834cd8d [threeqsdn] Extract subtitles from streaming manifests 2021-04-28 17:24:50 +05:30
Felix S e0e624ca7f [canvas] Extract subtitles from streaming manifests 2021-04-28 17:24:19 +05:30
Felix S ec4f374c05 [wat] Extract subtitles from streaming manifests 2021-04-28 17:24:08 +05:30
Felix S c811e8d8bd [atresplayer] Extract subtitles from streaming manifests 2021-04-28 17:23:56 +05:30
Felix S b2cd5da460 [francetv] Extract subtitles from the HLS manifest 2021-04-28 17:23:47 +05:30
Felix S 2de3b21e05 [uplynk] Extract subtitles from HLS manifests 2021-04-28 17:23:37 +05:30
Felix S 4bed436371 [twitter] Extract subtitles from HLS manifests 2021-04-28 17:23:27 +05:30
Felix S efe9dba595 [srgssr] Extract subtitles from HLS manifests 2021-04-28 17:23:16 +05:30
Felix S 47f4203dd3 [nytimes] Extract subtitles from HLS manifests 2021-04-28 17:23:05 +05:30
Felix S 015c10aeec [roosterteeth] Use common code for subtitle extraction 2021-04-28 17:22:56 +05:30
Felix S a00d781b73 [elonet] Use common code for subtitle extraction 2021-04-28 17:22:45 +05:30
Felix S 0c541b563f [tv4] Extract subtitles from streaming manifests 2021-04-28 17:22:36 +05:30
Felix S 64a5cf7929 [byutv] Extract subtitles from streaming manifests 2021-04-28 17:22:27 +05:30
Felix S 7a450a3b1c [generic] Extract subtitles from direct SSTR manifest links 2021-04-28 17:22:18 +05:30
Felix S 7de27caf16 [generic] Extract subtitles from direct DASH manifest links 2021-04-28 17:22:07 +05:30
Felix S c26326c1be [generic] Extract subtitles from direct HLS manifest links 2021-04-28 17:21:55 +05:30
Felix S 66a1b8643a [downloader/ism] Support muxing TTML subtitles 2021-04-28 17:21:45 +05:30
Felix S 15828bcf25 [downloader/hls] Handle MPEG-2 PES timestamp overflow 2021-04-28 17:21:35 +05:30
Felix S 333217f43e [downloader/hls] Remove duplicate cues using a sliding window of candidates 2021-04-28 17:21:26 +05:30
Felix S 4a2f19abbd [downloader/hls] Assemble single-file WebVTT subtitles from HLS segments 2021-04-28 17:21:14 +05:30
Felix S 5fbcebed8c [test] Test SSTR manifest parsing 2021-04-28 17:21:01 +05:30
Felix S becdc7f82c [test] Test subtitle extraction from DASH manifests 2021-04-28 17:20:49 +05:30
Felix S 73b9088a1c [test] Test subtitle extraction from HLS manifests 2021-04-28 17:20:39 +05:30
Felix S f6a1d69a87 [extractor/common] Extend _extract_akamai_formats to also extract subtitle tracks 2021-04-28 17:20:29 +05:30
Felix S fd76a14259 [extractor/common, downloader/ism] Extract SSTR subtitle tracks
_parse_ism_formats was extended into _parse_ism_formats_and_subtitles;
all direct users were updated, though _extract_ism_formats was left
as a compatibility wrapper.

The SSTR downloader was also modified in order to prepare for muxing
subtitle streams, although no support for any subtitle codecs was
added in this commit.
2021-04-28 17:20:20 +05:30
Felix S 171e59edd4 [extractor/common] Extract DASH subtitle tracks
_extract_mpd_formats and _parse_mpd_formats were extended into
_…_formats_and_subtitles; wrappers with old names are provided
for compatibility.
2021-04-28 17:20:11 +05:30
Felix S a0c3b2d5cf [extractor/common] Extract HLS subtitle tracks
_extract_m3u8_formats is renamed to _extract_m3u8_formats_and_subtitles
and extended to handle subtitle tracks instead of skipping them;
a wrapper with the old name is provided for compatibility.

_parse_m3u8_formats is likewise renamed and extended, but without adding
the compatibility wrapper; the test suite is adjusted to test the enhanced
method instead.
2021-04-28 17:19:57 +05:30
Felix S 19bb39202d [extractor/common] Generalise _merge_subtitles
This allows modifying a subtitles dictionary in-place.
2021-04-28 17:19:46 +05:30
Felix S d4553567d2 [downloader/ism] Prevent writing the header again when resuming an interrupted download 2021-04-28 17:19:37 +05:30
Felix S 4d49884c58 [downloader/fragment] Allow persisting extra state when a download is interrupted 2021-04-28 17:19:31 +05:30
Felix S 5873d4ccdd [utils] Improve bug_report_message
Add an optional argument specifying the text that should go before
the message.
2021-04-28 17:19:23 +05:30
Hadi0609andGitHub db9a564b6a [zee5] Fix extraction for some URLs (#279)
Closes: #278
2021-04-28 14:51:54 +05:30
Felix SandGitHub c72967d5de [mediasite] Generalize URL pattern (#275)
Authored by: fstirlitz
2021-04-26 17:23:20 +05:30
pukkandan 598d185db1 Fix case sensitivity of format selector
Bug introduced in f8d4ad9ab0
2021-04-26 10:56:56 +05:30
pukkandan b982cbdd0e [limelight] Obey allow_unplayable_formats 2021-04-26 10:56:55 +05:30
pukkandan 6a04a74e8b [FormatSort] Fix for when some formats have quality and others don't 2021-04-26 10:56:54 +05:30
pukkandan 88728713c8 Py2 compatibility for FileNotFoundError 2021-04-26 10:56:53 +05:30
CXwudiandGitHub 6b1d8c1e30 [niconico] Fix title and thumbnail extraction (#273)
Authored by: CXwudi
2021-04-26 08:23:57 +05:30
AshishandGitHub 87c3d06271 [Mxplayer] Add MxplayerShowIE (#270)
Authored by: Ashish0804
2021-04-26 08:12:51 +05:30
pukkandan 915f911e36 [utils] Encode URLs in YoutubeDLCookieProcessor
Closes #263
2021-04-24 19:20:07 +05:30
pukkandan cf9d6cfb0c [tubi] Raise "no video formats" error when video url is empty
Related: #266
2021-04-24 17:52:33 +05:30
pukkandan bbed5763f1 [francetvinfo] Improve video id extraction
Closes #261
2021-04-23 00:01:09 +05:30
pukkandan ca0b91b39e [version] update :ci skip all 2021-04-22 17:30:36 +05:30
pukkandan 0cf0571560 Release 2021.04.22 2021-04-22 16:58:28 +05:30
pukkandan e58c22a0f6 [documentation] Fix typos 2021-04-22 16:54:44 +05:30
pukkandan e4bdd3377d [ci] Disable fail-fast 2021-04-22 16:54:41 +05:30
pukkandan 0b2e9d2c30 [lazy_extractor] Do not load plugins 2021-04-22 16:54:08 +05:30
pukkandan 1bdae7d312 Update to ytdl-commit-7e8b3f9
[youtube] Remove unused code
https://github.com/ytdl-org/youtube-dl/commit/7e8b3f9439ebefb3a3a4e5da9c0bd2b595976438
2021-04-22 16:54:07 +05:30
Felix SandGitHub a471f21da6 [mildom] Remove proxy (#260)
Closes #251
Makes 2cff495997, ab406a1c0e, #252 obsolete

Authored by: fstirlitz
2021-04-22 16:52:22 +05:30
pukkandan 6efb071135 [BilibiliChannel] Fix pagination
Closes #222

https://github.com/nao20010128nao/ytdl-patched/commit/ccca21d7f57f1330a205f07d369e31883ba006c2
Coauthored by: nao20010128nao, pukkandan
2021-04-22 04:19:33 +05:30
pukkandan f4536226c1 [documentation] Clarify which deprecated options still work 2021-04-22 04:19:33 +05:30
pukkandan a439a3a45c Improve output template (see desc)
* Objects can be traversed like `%(field.key1.key2)s`
* A number can be added to the field as `%(field+n)s`
* Deprecates `--autonumber-start`
2021-04-22 04:19:33 +05:30
pukkandan 26e2805c3f Add option --skip-playlist-after-errors
Allows to skip the rest of a playlist after a given number of errors are encountered
2021-04-22 02:16:31 +05:30
pukkandan 3b4775e021 [go] Fix _VALID_URL
Closes #255
2021-04-21 15:43:53 +05:30
pukkandan ab406a1c0e [mildom] Warn user of proxy 2021-04-21 15:43:22 +05:30
pukkandan a3faeb7de4 [MetadataFromField] Improve regex and add tests 2021-04-21 11:12:04 +05:30
pukkandan 8c54a3051d [youtube] Bugfix in _extract_ytcfg 2021-04-21 10:37:24 +05:30
pukkandan c32b0aab8a Improve --sub-langs (see desc)
* Treat `--sub-langs` entries as regex
* `all` can be used to refer to all the subtitles
* the language code can be prefixed with `-` to exclude it
* Deprecates `--all-subs`
Closes #253
2021-04-20 02:58:03 +05:30
pukkandan 3097d9e512 [mildom:user:vod] Download only necessary amount of pages 2021-04-19 11:41:49 +05:30
pukkandan c1df120eda [mildom:vod] Remove proxy
* Proxy is needed only for live videos
2021-04-19 11:41:40 +05:30
pukkandan 2cff495997 [mildom] Change proxy
Related: #251
Closes #252
2021-04-19 11:41:33 +05:30
pukkandan d0491a1ebe [twitcasting] Fix extractor
* `Origin: https://twitcasting.tv` must be sent when requesting the webpage. Otherwise the extracted `m3u8` will always give a `502`
* Fix regex for when `data-movie-playlist` is a dict containing the needed list
* media initialization is fully supported; so change downloader to native

Closes #220
2021-04-18 17:57:46 +05:30
nixxoandGitHub b9d68c199b [rai] Add support for http formats (#208)
Authored by: nixxo
2021-04-17 22:42:28 +05:30
Felix SandGitHub 155510fe81 Improve the yt-dlp.sh script (#248)
* Quote the `$0` variable to correctly handle spaces
* Change the shebang line to `/bin/sh` to avoid unnecessarily depending on bash
* Use the `exec` command to avoid having the shell process linger unnecessarily
* Change the mode to make the script directly executable

Authored by: fstirlitz

:ci skip all
2021-04-17 19:21:25 +05:30
pukkandan 201c145953 Update to ytdl-commit-9f6c03
[cbsnews] Fix extraction for python <3.6
https://github.com/ytdl-org/youtube-dl/commit/9f6c03a00602eb1119e43a522cf50682f6d6a6dd
2021-04-17 08:40:31 +05:30
pukkandan 5d34200268 [youtube:tab] Reload with unavailable videos for all playlists
If the unavailable video is in a later page, the warning and button are not shown in the initial webpage
So we force all playlists' initial page to reload with the correct params
2021-04-17 08:40:30 +05:30
pukkandan b7da73eb19 Add option --ignore-no-formats-error
* Ignores the "no video format" and similar errors
* Experimental - Some extractors may still throw these errors
2021-04-17 08:40:30 +05:30
pukkandan 6a39ee13f7 Fix inconsistent use of report_warning 2021-04-17 04:16:41 +05:30
pukkandan 33245766ab [downloader] Fix ffmpeg selection for m3u8_native 2021-04-17 04:15:56 +05:30
coletdjnzandGitHub 358de58c4d [youtube:tab] Show unavailable videos in playlists (#242)
Closes #231

Authored by: colethedj
2021-04-17 04:09:08 +05:30
pukkandan a7191c6f57 Fix some linter and typos 2021-04-16 05:31:47 +05:30
lkhoandpukkandan baa5873942 [viu:ott] Fix extractor (see desc)
* add language_flag_id query param
* add support for premium account (untested since I dont have a premium account)
* support entire series

Code from:
https://github.com/blackjack4494/youtube-dlc/pull/211
https://github.com/ytdl-org/youtube-dl/pull/15182
https://github.com/ytdl-org/youtube-dl/pull/26775

Fixes:
https://github.com/yt-dlp/yt-dlp/issues/219
https://github.com/ytdl-org/youtube-dl/issues/27946
https://github.com/ytdl-org/youtube-dl/issues/27863
https://github.com/ytdl-org/youtube-dl/issues/27812
https://github.com/ytdl-org/youtube-dl/issues/27464
https://github.com/ytdl-org/youtube-dl/issues/26788
https://github.com/blackjack4494/yt-dlc/issues/136

Possibly also fixes (untested):
https://github.com/ytdl-org/youtube-dl/issues/16992
https://github.com/ytdl-org/youtube-dl/issues/26701

Co-authored by: lkho, pukkandan
2021-04-16 05:19:46 +05:30
pukkandan c6ce815461 [Exec] Ensure backward compatibility when the command contains % 2021-04-16 05:19:44 +05:30
coletdjnzandGitHub 79360d99d3 [youtube] Standardize API calls for tabs, mixes and search (#245)
Authored by: colethedj
2021-04-15 16:52:59 +05:30
pukkandan 46fff7105e [youtube] Ignore invalid stretch ratio
Closes #244
2021-04-14 15:22:17 +05:30
pukkandan 72e1fe969f [downloader] Fix downloader selection for m3u8
Bug introduced by: 52a8a1e1b9 and a31953b0e6
2021-04-14 12:25:42 +05:30
AshishandGitHub b5be6dd504 [TubiTv] Add TubiTvShowIE (#243)
Authored by: Ashish0804
2021-04-14 12:22:28 +05:30
coletdjnzandGitHub 8ea3f7b909 [youtube] Improve channel syncid extraction to support ytcfg (#241)
Authored by: colethedj
2021-04-14 10:37:03 +05:30
pukkandan 921b76cab8 Ensure mergeall selects best format when multistreams are disabled 2021-04-13 10:53:25 +05:30
pukkandan a31953b0e6 [downloader] Fix external downloader selection for m3u8
Closes #239
2021-04-12 22:34:11 +05:30
pukkandan 54670cf084 [version] update
:ci skip all
2021-04-12 03:30:55 +05:30
132 changed files with 5385 additions and 1332 deletions
+3 -3
View File
@@ -21,7 +21,7 @@ assignees: ''
<!-- <!--
Carefully read and work through this check list in order to prevent the most common mistakes and misuse of yt-dlp: Carefully read and work through this check list in order to prevent the most common mistakes and misuse of yt-dlp:
- First of, make sure you are using the latest version of yt-dlp. Run `yt-dlp --version` and ensure your version is 2021.04.03. If it's not, see https://github.com/yt-dlp/yt-dlp on how to update. Issues with outdated version will be REJECTED. - First of, make sure you are using the latest version of yt-dlp. Run `yt-dlp --version` and ensure your version is 2021.04.22. If it's not, see https://github.com/yt-dlp/yt-dlp on how to update. Issues with outdated version will be REJECTED.
- Make sure that all provided video/audio/playlist URLs (if any) are alive and playable in a browser. - Make sure that all provided video/audio/playlist URLs (if any) are alive and playable in a browser.
- Make sure that all URLs and arguments with special characters are properly quoted or escaped as explained in https://github.com/yt-dlp/yt-dlp. - Make sure that all URLs and arguments with special characters are properly quoted or escaped as explained in https://github.com/yt-dlp/yt-dlp.
- Search the bugtracker for similar issues: https://github.com/yt-dlp/yt-dlp. DO NOT post duplicates. - Search the bugtracker for similar issues: https://github.com/yt-dlp/yt-dlp. DO NOT post duplicates.
@@ -29,7 +29,7 @@ Carefully read and work through this check list in order to prevent the most com
--> -->
- [ ] I'm reporting a broken site support - [ ] I'm reporting a broken site support
- [ ] I've verified that I'm running yt-dlp version **2021.04.03** - [ ] I've verified that I'm running yt-dlp version **2021.04.22**
- [ ] I've checked that all provided URLs are alive and playable in a browser - [ ] I've checked that all provided URLs are alive and playable in a browser
- [ ] I've checked that all URLs and arguments with special characters are properly quoted or escaped - [ ] I've checked that all URLs and arguments with special characters are properly quoted or escaped
- [ ] I've searched the bugtracker for similar issues including closed ones - [ ] I've searched the bugtracker for similar issues including closed ones
@@ -44,7 +44,7 @@ Add the `-v` flag to your command line you run yt-dlp with (`yt-dlp -v <your com
[debug] User config: [] [debug] User config: []
[debug] Command-line args: [u'-v', u'http://www.youtube.com/watch?v=BaW_jenozKcj'] [debug] Command-line args: [u'-v', u'http://www.youtube.com/watch?v=BaW_jenozKcj']
[debug] Encodings: locale cp1251, fs mbcs, out cp866, pref cp1251 [debug] Encodings: locale cp1251, fs mbcs, out cp866, pref cp1251
[debug] yt-dlp version 2021.04.03 [debug] yt-dlp version 2021.04.22
[debug] Python version 2.7.11 - Windows-2003Server-5.2.3790-SP2 [debug] Python version 2.7.11 - Windows-2003Server-5.2.3790-SP2
[debug] exe versions: ffmpeg N-75573-g1d0487f, ffprobe N-75573-g1d0487f, rtmpdump 2.4 [debug] exe versions: ffmpeg N-75573-g1d0487f, ffprobe N-75573-g1d0487f, rtmpdump 2.4
[debug] Proxy map: {} [debug] Proxy map: {}
@@ -21,7 +21,7 @@ assignees: ''
<!-- <!--
Carefully read and work through this check list in order to prevent the most common mistakes and misuse of yt-dlp: Carefully read and work through this check list in order to prevent the most common mistakes and misuse of yt-dlp:
- First of, make sure you are using the latest version of yt-dlp. Run `yt-dlp --version` and ensure your version is 2021.04.03. If it's not, see https://github.com/yt-dlp/yt-dlp on how to update. Issues with outdated version will be REJECTED. - First of, make sure you are using the latest version of yt-dlp. Run `yt-dlp --version` and ensure your version is 2021.04.22. If it's not, see https://github.com/yt-dlp/yt-dlp on how to update. Issues with outdated version will be REJECTED.
- Make sure that all provided video/audio/playlist URLs (if any) are alive and playable in a browser. - Make sure that all provided video/audio/playlist URLs (if any) are alive and playable in a browser.
- Make sure that site you are requesting is not dedicated to copyright infringement, see https://github.com/yt-dlp/yt-dlp. yt-dlp does not support such sites. In order for site support request to be accepted all provided example URLs should not violate any copyrights. - Make sure that site you are requesting is not dedicated to copyright infringement, see https://github.com/yt-dlp/yt-dlp. yt-dlp does not support such sites. In order for site support request to be accepted all provided example URLs should not violate any copyrights.
- Search the bugtracker for similar site support requests: https://github.com/yt-dlp/yt-dlp. DO NOT post duplicates. - Search the bugtracker for similar site support requests: https://github.com/yt-dlp/yt-dlp. DO NOT post duplicates.
@@ -29,7 +29,7 @@ Carefully read and work through this check list in order to prevent the most com
--> -->
- [ ] I'm reporting a new site support request - [ ] I'm reporting a new site support request
- [ ] I've verified that I'm running yt-dlp version **2021.04.03** - [ ] I've verified that I'm running yt-dlp version **2021.04.22**
- [ ] I've checked that all provided URLs are alive and playable in a browser - [ ] I've checked that all provided URLs are alive and playable in a browser
- [ ] I've checked that none of provided URLs violate any copyrights - [ ] I've checked that none of provided URLs violate any copyrights
- [ ] I've searched the bugtracker for similar site support requests including closed ones - [ ] I've searched the bugtracker for similar site support requests including closed ones
@@ -21,13 +21,13 @@ assignees: ''
<!-- <!--
Carefully read and work through this check list in order to prevent the most common mistakes and misuse of yt-dlp: Carefully read and work through this check list in order to prevent the most common mistakes and misuse of yt-dlp:
- First of, make sure you are using the latest version of yt-dlp. Run `yt-dlp --version` and ensure your version is 2021.04.03. If it's not, see https://github.com/yt-dlp/yt-dlp on how to update. Issues with outdated version will be REJECTED. - First of, make sure you are using the latest version of yt-dlp. Run `yt-dlp --version` and ensure your version is 2021.04.22. If it's not, see https://github.com/yt-dlp/yt-dlp on how to update. Issues with outdated version will be REJECTED.
- Search the bugtracker for similar site feature requests: https://github.com/yt-dlp/yt-dlp. DO NOT post duplicates. - Search the bugtracker for similar site feature requests: https://github.com/yt-dlp/yt-dlp. DO NOT post duplicates.
- Finally, put x into all relevant boxes like this [x] (Dont forget to delete the empty space) - Finally, put x into all relevant boxes like this [x] (Dont forget to delete the empty space)
--> -->
- [ ] I'm reporting a site feature request - [ ] I'm reporting a site feature request
- [ ] I've verified that I'm running yt-dlp version **2021.04.03** - [ ] I've verified that I'm running yt-dlp version **2021.04.22**
- [ ] I've searched the bugtracker for similar site feature requests including closed ones - [ ] I've searched the bugtracker for similar site feature requests including closed ones
+3 -3
View File
@@ -21,7 +21,7 @@ assignees: ''
<!-- <!--
Carefully read and work through this check list in order to prevent the most common mistakes and misuse of yt-dlp: Carefully read and work through this check list in order to prevent the most common mistakes and misuse of yt-dlp:
- First of, make sure you are using the latest version of yt-dlp. Run `yt-dlp --version` and ensure your version is 2021.04.03. If it's not, see https://github.com/yt-dlp/yt-dlp on how to update. Issues with outdated version will be REJECTED. - First of, make sure you are using the latest version of yt-dlp. Run `yt-dlp --version` and ensure your version is 2021.04.22. If it's not, see https://github.com/yt-dlp/yt-dlp on how to update. Issues with outdated version will be REJECTED.
- Make sure that all provided video/audio/playlist URLs (if any) are alive and playable in a browser. - Make sure that all provided video/audio/playlist URLs (if any) are alive and playable in a browser.
- Make sure that all URLs and arguments with special characters are properly quoted or escaped as explained in https://github.com/yt-dlp/yt-dlp. - Make sure that all URLs and arguments with special characters are properly quoted or escaped as explained in https://github.com/yt-dlp/yt-dlp.
- Search the bugtracker for similar issues: https://github.com/yt-dlp/yt-dlp. DO NOT post duplicates. - Search the bugtracker for similar issues: https://github.com/yt-dlp/yt-dlp. DO NOT post duplicates.
@@ -30,7 +30,7 @@ Carefully read and work through this check list in order to prevent the most com
--> -->
- [ ] I'm reporting a broken site support issue - [ ] I'm reporting a broken site support issue
- [ ] I've verified that I'm running yt-dlp version **2021.04.03** - [ ] I've verified that I'm running yt-dlp version **2021.04.22**
- [ ] I've checked that all provided URLs are alive and playable in a browser - [ ] I've checked that all provided URLs are alive and playable in a browser
- [ ] I've checked that all URLs and arguments with special characters are properly quoted or escaped - [ ] I've checked that all URLs and arguments with special characters are properly quoted or escaped
- [ ] I've searched the bugtracker for similar bug reports including closed ones - [ ] I've searched the bugtracker for similar bug reports including closed ones
@@ -46,7 +46,7 @@ Add the `-v` flag to your command line you run yt-dlp with (`yt-dlp -v <your com
[debug] User config: [] [debug] User config: []
[debug] Command-line args: [u'-v', u'http://www.youtube.com/watch?v=BaW_jenozKcj'] [debug] Command-line args: [u'-v', u'http://www.youtube.com/watch?v=BaW_jenozKcj']
[debug] Encodings: locale cp1251, fs mbcs, out cp866, pref cp1251 [debug] Encodings: locale cp1251, fs mbcs, out cp866, pref cp1251
[debug] yt-dlp version 2021.04.03 [debug] yt-dlp version 2021.04.22
[debug] Python version 2.7.11 - Windows-2003Server-5.2.3790-SP2 [debug] Python version 2.7.11 - Windows-2003Server-5.2.3790-SP2
[debug] exe versions: ffmpeg N-75573-g1d0487f, ffprobe N-75573-g1d0487f, rtmpdump 2.4 [debug] exe versions: ffmpeg N-75573-g1d0487f, ffprobe N-75573-g1d0487f, rtmpdump 2.4
[debug] Proxy map: {} [debug] Proxy map: {}
+2 -2
View File
@@ -21,13 +21,13 @@ assignees: ''
<!-- <!--
Carefully read and work through this check list in order to prevent the most common mistakes and misuse of yt-dlp: Carefully read and work through this check list in order to prevent the most common mistakes and misuse of yt-dlp:
- First of, make sure you are using the latest version of yt-dlp. Run `yt-dlp --version` and ensure your version is 2021.04.03. If it's not, see https://github.com/yt-dlp/yt-dlp on how to update. Issues with outdated version will be REJECTED. - First of, make sure you are using the latest version of yt-dlp. Run `yt-dlp --version` and ensure your version is 2021.04.22. If it's not, see https://github.com/yt-dlp/yt-dlp on how to update. Issues with outdated version will be REJECTED.
- Search the bugtracker for similar feature requests: https://github.com/yt-dlp/yt-dlp. DO NOT post duplicates. - Search the bugtracker for similar feature requests: https://github.com/yt-dlp/yt-dlp. DO NOT post duplicates.
- Finally, put x into all relevant boxes like this [x] (Dont forget to delete the empty space) - Finally, put x into all relevant boxes like this [x] (Dont forget to delete the empty space)
--> -->
- [ ] I'm reporting a feature request - [ ] I'm reporting a feature request
- [ ] I've verified that I'm running yt-dlp version **2021.04.03** - [ ] I've verified that I'm running yt-dlp version **2021.04.22**
- [ ] I've searched the bugtracker for similar feature requests including closed ones - [ ] I've searched the bugtracker for similar feature requests including closed ones
+9 -2
View File
@@ -6,7 +6,7 @@ jobs:
if: "!contains(github.event.head_commit.message, 'ci skip')" if: "!contains(github.event.head_commit.message, 'ci skip')"
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}
strategy: strategy:
fail-fast: true fail-fast: false
matrix: matrix:
os: [ubuntu-18.04] os: [ubuntu-18.04]
# TODO: python 2.6 # TODO: python 2.6
@@ -41,11 +41,18 @@ jobs:
- name: Install Jython - name: Install Jython
if: ${{ matrix.python-impl == 'jython' }} if: ${{ matrix.python-impl == 'jython' }}
run: | run: |
wget http://search.maven.org/remotecontent?filepath=org/python/jython-installer/2.7.1/jython-installer-2.7.1.jar -O jython-installer.jar wget https://repo1.maven.org/maven2/org/python/jython-installer/2.7.1/jython-installer-2.7.1.jar -O jython-installer.jar
java -jar jython-installer.jar -s -d "$HOME/jython" java -jar jython-installer.jar -s -d "$HOME/jython"
echo "$HOME/jython/bin" >> $GITHUB_PATH echo "$HOME/jython/bin" >> $GITHUB_PATH
- name: Install nose - name: Install nose
if: ${{ matrix.python-impl != 'jython' }}
run: pip install nose run: pip install nose
- name: Install nose (Jython)
if: ${{ matrix.python-impl == 'jython' }}
# Working around deprecation of support for non-SNI clients at PyPI CDN (see https://status.python.org/incidents/hzmjhqsdjqgb)
run: |
wget https://files.pythonhosted.org/packages/99/4f/13fb671119e65c4dce97c60e67d3fd9e6f7f809f2b307e2611f4701205cb/nose-1.3.7-py2-none-any.whl
pip install nose-1.3.7-py2-none-any.whl
- name: Run tests - name: Run tests
continue-on-error: ${{ matrix.ytdl-test-set == 'download' || matrix.python-impl == 'jython' }} continue-on-error: ${{ matrix.ytdl-test-set == 'download' || matrix.python-impl == 'jython' }}
env: env:
+8 -1
View File
@@ -41,11 +41,18 @@ jobs:
- name: Install Jython - name: Install Jython
if: ${{ matrix.python-impl == 'jython' }} if: ${{ matrix.python-impl == 'jython' }}
run: | run: |
wget http://search.maven.org/remotecontent?filepath=org/python/jython-installer/2.7.1/jython-installer-2.7.1.jar -O jython-installer.jar wget https://repo1.maven.org/maven2/org/python/jython-installer/2.7.1/jython-installer-2.7.1.jar -O jython-installer.jar
java -jar jython-installer.jar -s -d "$HOME/jython" java -jar jython-installer.jar -s -d "$HOME/jython"
echo "$HOME/jython/bin" >> $GITHUB_PATH echo "$HOME/jython/bin" >> $GITHUB_PATH
- name: Install nose - name: Install nose
if: ${{ matrix.python-impl != 'jython' }}
run: pip install nose run: pip install nose
- name: Install nose (Jython)
if: ${{ matrix.python-impl == 'jython' }}
# Working around deprecation of support for non-SNI clients at PyPI CDN (see https://status.python.org/incidents/hzmjhqsdjqgb)
run: |
wget https://files.pythonhosted.org/packages/99/4f/13fb671119e65c4dce97c60e67d3fd9e6f7f809f2b307e2611f4701205cb/nose-1.3.7-py2-none-any.whl
pip install nose-1.3.7-py2-none-any.whl
- name: Run tests - name: Run tests
continue-on-error: ${{ matrix.ytdl-test-set == 'download' || matrix.python-impl == 'jython' }} continue-on-error: ${{ matrix.ytdl-test-set == 'download' || matrix.python-impl == 'jython' }}
env: env:
+6
View File
@@ -38,3 +38,9 @@ xtkoba
llacb47 llacb47
hheimbuerger hheimbuerger
B0pol B0pol
lkho
fstirlitz
Lamieur
tsukumijima
Hadi0609
b5eff52
+95 -1
View File
@@ -19,6 +19,100 @@
--> -->
### 2021.05.11
* **Deprecate support for python versions < 3.6**
* **Subtitle extraction from manifests** by [fstirlitz](https://github.com/fstirlitz). See [be6202f12b97858b9d716e608394b51065d0419f](https://github.com/yt-dlp/yt-dlp/commit/be6202f12b97858b9d716e608394b51065d0419f) for details
* **Improve output template:**
* Allow slicing lists/strings using `field.start:end:step`
* A field can also be used as offset like `field1+num+field2`
* A default value can be given using `field|default`
* Prevent invalid fields from causing errors
* Merge youtube-dl: Upto [commit/a726009](https://github.com/ytdl-org/youtube-dl/commit/a7260099873acc6dc7d76cafad2f6b139087afd0)
* **Remove options** `-l`, `-t`, `-A` completely and disable `--auto-number`, `--title`, `--literal`, `--id`
* [Plugins] Prioritize plugins over standard extractors and prevent plugins from overwriting the standard extractor classes
* [downloader] Fix `quiet` and `to_stderr`
* [fragment] Ensure the file is closed on error
* [fragment] Make sure first segment is not skipped
* [aria2c] Fix whitespace being stripped off
* [embedthumbnail] Fix bug where jpeg thumbnails were converted again
* [FormatSort] Fix for when some formats have quality and others don't
* [utils] Add `network_exceptions`
* [utils] Escape URL while sanitizing
* [ukcolumn] Add Extractor
* [whowatch] Add extractor by [nao20010128nao](https://github.com/nao20010128nao)
* [CBS] Improve `_VALID_URL` to support movies
* [crackle] Improve extraction
* [curiositystream] Fix collections
* [francetvinfo] Improve video id extraction
* [generic] Respect the encoding in manifest
* [limelight] Obey `allow_unplayable_formats`
* [mediasite] Generalize URL pattern by [fstirlitz](https://github.com/fstirlitz)
* [mxplayer] Add MxplayerShowIE by [Ashish0804](https://github.com/Ashish0804)
* [nebula] Move to nebula.app by [Lamieur](https://github.com/Lamieur)
* [niconico] Fix HLS formats by [CXwudi](https://github.com/CXwudi), [tsukumijima](https://github.com/tsukumijima), [nao20010128nao](https://github.com/nao20010128nao) and [pukkandan](https://github.com/pukkandan)
* [niconico] Fix title and thumbnail extraction by [CXwudi](https://github.com/CXwudi)
* [plutotv] Extract subtitles from manifests
* [plutotv] Fix format extraction for some urls
* [rmcdecouverte] Improve `_VALID_URL`
* [sonyliv] Fix `title` and `series` extraction by [Ashish0804](https://github.com/Ashish0804)
* [tubi] Raise "no video formats" error when video url is empty
* [youtube:tab] Detect playlists inside community posts
* [youtube] Add `oembed` to reserved names
* [zee5] Fix extraction for some URLs by [Hadi0609](https://github.com/Hadi0609)
* [zee5] Fix py2 compatibility
* Fix `playlist_index` and add `playlist_autonumber`. See [#302](https://github.com/yt-dlp/yt-dlp/issues/302) for details
* Add experimental option `--check-formats` to test the URLs before format selection
* Option `--compat-options` to revert some of yt-dlp's changes
* Deprecates `--list-formats-as-table`, `--list-formats-old`
* Fix number of digits in `%(playlist_index)s`
* Fix case sensitivity of format selector
* Revert "[core] be able to hand over id and title using url_result"
* Do not strip out whitespaces in `-o` and `-P`
* Fix `preload_download_archive` writing verbose message to `stdout`
* Move option warnings to `YoutubeDL`so that they obey `--no-warnings` and can output colors
* Py2 compatibility for `FileNotFoundError`
### 2021.04.22
* **Improve output template:**
* Objects can be traversed like `%(field.key1.key2)s`
* An offset can be added to numeric fields as `%(field+N)s`
* Deprecates `--autonumber-start`
* **Improve `--sub-langs`:**
* Treat `--sub-langs` entries as regex
* `all` can be used to refer to all the subtitles
* language codes can be prefixed with `-` to exclude it
* Deprecates `--all-subs`
* Add option `--ignore-no-formats-error` to ignore the "no video format" and similar errors
* Add option `--skip-playlist-after-errors` to skip the rest of a playlist after a given number of errors are encountered
* Merge youtube-dl: Upto [commit/7e8b3f9](https://github.com/ytdl-org/youtube-dl/commit/7e8b3f9439ebefb3a3a4e5da9c0bd2b595976438)
* [downloader] Fix bug in downloader selection
* [BilibiliChannel] Fix pagination by [nao20010128nao](https://github.com/nao20010128nao) and [pukkandan](https://github.com/pukkandan)
* [rai] Add support for http formats by [nixxo](https://github.com/nixxo)
* [TubiTv] Add TubiTvShowIE by [Ashish0804](https://github.com/Ashish0804)
* [twitcasting] Fix extractor
* [viu:ott] Fix extractor and support series by [lkho](https://github.com/lkho) and [pukkandan](https://github.com/pukkandan)
* [youtube:tab] Show unavailable videos in playlists by [colethedj](https://github.com/colethedj)
* [youtube:tab] Reload with unavailable videos for all playlists
* [youtube] Ignore invalid stretch ratio
* [youtube] Improve channel syncid extraction to support ytcfg by [colethedj](https://github.com/colethedj)
* [youtube] Standardize API calls for tabs, mixes and search by [colethedj](https://github.com/colethedj)
* [youtube] Bugfix in `_extract_ytcfg`
* [mildom:user:vod] Download only necessary amount of pages
* [mildom] Remove proxy completely by [fstirlitz](https://github.com/fstirlitz)
* [go] Fix `_VALID_URL`
* [MetadataFromField] Improve regex and add tests
* [Exec] Ensure backward compatibility when the command contains `%`
* [extractor] Fix inconsistent use of `report_warning`
* Ensure `mergeall` selects best format when multistreams are disabled
* Improve the yt-dlp.sh script by [fstirlitz](https://github.com/fstirlitz)
* [lazy_extractor] Do not load plugins
* [ci] Disable fail-fast
* [documentation] Clarify which deprecated options still work
* [documentation] Fix typos
### 2021.04.11 ### 2021.04.11
* Add option `--convert-thumbnails` (only jpg currently supported) * Add option `--convert-thumbnails` (only jpg currently supported)
* Format selector `mergeall` to download and merge all formats * Format selector `mergeall` to download and merge all formats
@@ -35,7 +129,7 @@
* [youtube] Fix thumbnail URL * [youtube] Fix thumbnail URL
* [youtube] Parse API parameters from initial webpage by [colethedj](https://github.com/colethedj) * [youtube] Parse API parameters from initial webpage by [colethedj](https://github.com/colethedj)
* [youtube] Extract comments' approximate timestamp by [colethedj](https://github.com/colethedj) * [youtube] Extract comments' approximate timestamp by [colethedj](https://github.com/colethedj)
* [youtube] Fix `\_extract_alerts` * [youtube] Fix alert extraction
* [bilibili] Fix uploader * [bilibili] Fix uploader
* [utils] Add `datetime_from_str` and `datetime_add_months` by [colethedj](https://github.com/colethedj) * [utils] Add `datetime_from_str` and `datetime_add_months` by [colethedj](https://github.com/colethedj)
* Run some `postprocessors` before actual download * Run some `postprocessors` before actual download
+153 -76
View File
@@ -3,17 +3,16 @@
# YT-DLP # YT-DLP
A command-line program to download videos from YouTube and many other [video platforms](supportedsites.md) A command-line program to download videos from YouTube and many other [video platforms](supportedsites.md)
<!-- GHA doesnot have for-the-badge style <!-- GHA doesn't have for-the-badge style
[![CI Status](https://github.com/yt-dlp/yt-dlp/workflows/Core%20Tests/badge.svg?branch=master)](https://github.com/yt-dlp/yt-dlp/actions) [![CI Status](https://github.com/yt-dlp/yt-dlp/workflows/Core%20Tests/badge.svg?branch=master)](https://github.com/yt-dlp/yt-dlp/actions)
--> -->
[![Release version](https://img.shields.io/github/v/release/yt-dlp/yt-dlp?color=brightgreen&label=Release&style=for-the-badge)](https://github.com/yt-dlp/yt-dlp/releases/latest) [![Release version](https://img.shields.io/github/v/release/yt-dlp/yt-dlp?color=brightgreen&label=Release&style=for-the-badge)](https://github.com/yt-dlp/yt-dlp/releases/latest)
[![License: Unlicense](https://img.shields.io/badge/License-Unlicense-blue.svg?style=for-the-badge)](LICENSE) [![License: Unlicense](https://img.shields.io/badge/License-Unlicense-blue.svg?style=for-the-badge)](LICENSE)
[![Doc Status](https://readthedocs.org/projects/yt-dlp/badge/?version=latest&style=for-the-badge)](https://yt-dlp.readthedocs.io) [![Doc Status](https://readthedocs.org/projects/yt-dlp/badge/?version=latest&style=for-the-badge)](https://yt-dlp.readthedocs.io)
[![Discord](https://img.shields.io/discord/807245652072857610?color=blue&label=discord&logo=discord&style=for-the-badge)](https://discord.gg/H5MNcFW63r) [![Discord](https://img.shields.io/discord/807245652072857610?color=blue&label=discord&logo=discord&style=for-the-badge)](https://discord.gg/H5MNcFW63r)
[![Commits](https://img.shields.io/github/commit-activity/m/yt-dlp/yt-dlp?label=commits&style=for-the-badge)](https://github.com/yt-dlp/yt-dlp/commits) [![Commits](https://img.shields.io/github/commit-activity/m/yt-dlp/yt-dlp?label=commits&style=for-the-badge)](https://github.com/yt-dlp/yt-dlp/commits)
[![Last Commit](https://img.shields.io/github/last-commit/yt-dlp/yt-dlp/master?style=for-the-badge)](https://github.com/yt-dlp/yt-dlp/commits) [![Last Commit](https://img.shields.io/github/last-commit/yt-dlp/yt-dlp/master?style=for-the-badge)](https://github.com/yt-dlp/yt-dlp/commits)
[![Downloads](https://img.shields.io/github/downloads/yt-dlp/yt-dlp/total?style=for-the-badge)](https://github.com/yt-dlp/yt-dlp/releases/latest) [![Downloads](https://img.shields.io/github/downloads/yt-dlp/yt-dlp/total?style=for-the-badge&color=blue)](https://github.com/yt-dlp/yt-dlp/releases/latest)
[![PyPi Downloads](https://img.shields.io/pypi/dm/yt-dlp?label=PyPi&style=for-the-badge)](https://pypi.org/project/yt-dlp) [![PyPi Downloads](https://img.shields.io/pypi/dm/yt-dlp?label=PyPi&style=for-the-badge)](https://pypi.org/project/yt-dlp)
</div> </div>
@@ -21,8 +20,9 @@ A command-line program to download videos from YouTube and many other [video pla
yt-dlp is a [youtube-dl](https://github.com/ytdl-org/youtube-dl) fork based on the now inactive [youtube-dlc](https://github.com/blackjack4494/yt-dlc). The main focus of this project is adding new features and patches while also keeping up to date with the original project yt-dlp is a [youtube-dl](https://github.com/ytdl-org/youtube-dl) fork based on the now inactive [youtube-dlc](https://github.com/blackjack4494/yt-dlc). The main focus of this project is adding new features and patches while also keeping up to date with the original project
* [NEW FEATURES](#new-features) * [NEW FEATURES](#new-features)
* [Differences in default behavior](#differences-in-default-behavior)
* [INSTALLATION](#installation) * [INSTALLATION](#installation)
* [Dependancies](#dependancies) * [Dependencies](#dependencies)
* [Update](#update) * [Update](#update)
* [Compile](#compile) * [Compile](#compile)
* [USAGE AND OPTIONS](#usage-and-options) * [USAGE AND OPTIONS](#usage-and-options)
@@ -66,13 +66,14 @@ The major new features from the latest release of [blackjack4494/yt-dlc](https:/
* **[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 that 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 that what is possible by simply using `--format` ([examples](#format-selection-examples))
* **Merged with youtube-dl v2021.04.07**: You get all the latest features and patches of [youtube-dl](https://github.com/ytdl-org/youtube-dl) in addition to all the features of [youtube-dlc](https://github.com/blackjack4494/yt-dlc) * **Merged with youtube-dl [commit/a726009](https://github.com/ytdl-org/youtube-dl/commit/a7260099873acc6dc7d76cafad2f6b139087afd0)**: (v2021.04.26) You get all the latest features and patches of [youtube-dl](https://github.com/ytdl-org/youtube-dl) in addition to all the features of [youtube-dlc](https://github.com/blackjack4494/yt-dlc)
* **Merged with animelover1984/youtube-dl**: You get most of the features and improvements from [animelover1984/youtube-dl](https://github.com/animelover1984/youtube-dl) including `--get-comments`, `BiliBiliSearch`, `BilibiliChannel`, Embedding thumbnail in mp4/ogg/opus, Playlist infojson etc. Note that the NicoNico improvements are not available. See [#31](https://github.com/yt-dlp/yt-dlp/pull/31) for details. * **Merged with animelover1984/youtube-dl**: You get most of the features and improvements from [animelover1984/youtube-dl](https://github.com/animelover1984/youtube-dl) including `--get-comments`, `BiliBiliSearch`, `BilibiliChannel`, Embedding thumbnail in mp4/ogg/opus, playlist infojson etc. Note that the NicoNico improvements are not available. See [#31](https://github.com/yt-dlp/yt-dlp/pull/31) for details.
* **Youtube improvements**: * **Youtube improvements**:
* All Youtube Feeds (`:ytfav`, `:ytwatchlater`, `:ytsubs`, `:ythistory`, `:ytrec`) works correctly and supports downloading multiple pages of content * All Youtube Feeds (`:ytfav`, `:ytwatchlater`, `:ytsubs`, `:ythistory`, `:ytrec`) works and supports downloading multiple pages of content
* Youtube search (`ytsearch:`, `ytsearchdate:`) along with Search URLs works correctly * Youtube search (`ytsearch:`, `ytsearchdate:`) along with Search URLs work
* Youtube mixes supports downloading multiple pages of content
* Redirect channel's home URL automatically to `/video` to preserve the old behaviour * Redirect channel's home URL automatically to `/video` to preserve the old behaviour
* **Split video by chapters**: Videos can be split into multiple files based on chapters using `--split-chapters` * **Split video by chapters**: Videos can be split into multiple files based on chapters using `--split-chapters`
@@ -81,17 +82,21 @@ The major new features from the latest release of [blackjack4494/yt-dlc](https:/
* **Aria2c with HLS/DASH**: You can use `aria2c` as the external downloader for DASH(mpd) and HLS(m3u8) formats * **Aria2c with HLS/DASH**: You can use `aria2c` as the external downloader for DASH(mpd) and HLS(m3u8) formats
* **New extractors**: AnimeLab, Philo MSO, Rcs, Gedi, bitwave.tv, mildom, audius, zee5, mtv.it, wimtv, pluto.tv, niconico users, discoveryplus.in, mediathek, NFHSNetwork, nebula * **New extractors**: AnimeLab, Philo MSO, Rcs, Gedi, bitwave.tv, mildom, audius, zee5, mtv.it, wimtv, pluto.tv, niconico users, discoveryplus.in, mediathek, NFHSNetwork, nebula, ukcolumn, whowatch, MxplayerShow
* **Fixed extractors**: archive.org, roosterteeth.com, skyit, instagram, itv, SouthparkDe, spreaker, Vlive, akamai, ina, rumble, tennistv, amcnetworks, la7 podcasts, linuxacadamy, nitter * **Fixed extractors**: archive.org, roosterteeth.com, skyit, instagram, itv, SouthparkDe, spreaker, Vlive, akamai, ina, rumble, tennistv, amcnetworks, la7 podcasts, linuxacadamy, nitter, twitcasting, viu, crackle, curiositystream, mediasite, rmcdecouverte, sonyliv, tubi
* **Subtitle extraction from manifests**: Subtitles can be extracted from streaming media manifests. See [be6202f12b97858b9d716e608394b51065d0419f](https://github.com/yt-dlp/yt-dlp/commit/be6202f12b97858b9d716e608394b51065d0419f) for details
* **Multiple paths and output templates**: You can give different [output templates](#output-template) and download paths for different types of files. You can also set a temporary path where intermediary files are downloaded to using `--paths` (`-P`) * **Multiple paths and output templates**: You can give different [output templates](#output-template) and download paths for different types of files. You can also set a temporary path where intermediary files are downloaded to using `--paths` (`-P`)
* **Portable Configuration**: Configuration files are automatically loaded from the home and root directories. See [configuration](#configuration) for details * **Portable Configuration**: Configuration files are automatically loaded from the home and root directories. See [configuration](#configuration) for details
* **Other new options**: `--parse-metadata`, `--list-formats-as-table`, `--write-link`, `--force-download-archive`, `--force-overwrites`, `--break-on-reject` etc * **Output template improvements**: Output templates can now have date-time formatting, numeric offsets, object traversal etc. See [output template](#output-template) for details. Even more advanced operations can also be done with the help of `--parse-metadata`
* **Improvements**: Multiple `--postprocessor-args` and `--external-downloader-args`, Date/time formatting in `-o`, faster archive checking, more [format selection options](#format-selection) etc * **Other new options**: `--sleep-requests`, `--convert-thumbnails`, `--write-link`, `--force-download-archive`, `--force-overwrites`, `--break-on-reject` etc
* **Improvements**: Multiple `--postprocessor-args` and `--downloader-args`, faster archive checking, more [format selection options](#format-selection) etc
* **Plugin extractors**: Extractors can be loaded from an external file. See [plugins](#plugins) for details * **Plugin extractors**: Extractors can be loaded from an external file. See [plugins](#plugins) for details
@@ -105,15 +110,40 @@ See [changelog](Changelog.md) or [commits](https://github.com/yt-dlp/yt-dlp/comm
If you are coming from [youtube-dl](https://github.com/ytdl-org/youtube-dl), the amount of changes are very large. Compare [options](#options) and [supported sites](supportedsites.md) with youtube-dl's to get an idea of the massive number of features/patches [youtube-dlc](https://github.com/blackjack4494/yt-dlc) has accumulated. If you are coming from [youtube-dl](https://github.com/ytdl-org/youtube-dl), the amount of changes are very large. Compare [options](#options) and [supported sites](supportedsites.md) with youtube-dl's to get an idea of the massive number of features/patches [youtube-dlc](https://github.com/blackjack4494/yt-dlc) has accumulated.
### Differences in default behavior
Some of yt-dlp's default options are different from that of youtube-dl and youtube-dlc.
1. The options `--id`, `--auto-number` (`-A`), `--title` (`-t`) and `--literal` (`-l`), no longer work. See [removed options](#Removed) for details
1. `avconv` is not supported as as an alternative to `ffmpeg`
1. The default [output template](#output-template) is `%(title)s [%(id)s].%(ext)s`. There is no real reason for this change. This was changed before yt-dlp was ever made public and now there are no plans to change it back to `%(title)s.%(id)s.%(ext)s`. Instead, you may use `--compat-options filename`
1. The default [format sorting](sorting-formats) is different from youtube-dl and prefers higher resolution and better codecs rather than higher bitrates. You can use the `--format-sort` option to change this to any order you prefer, or use `--compat-options format-sort` to use youtube-dl's sorting order
1. The default format selector is `bv*+ba/b`. This means that if a combined video + audio format that is better than the best video-only format is found, the former will be prefered. Use `-f bv+ba/b` or `--compat-options format-spec` to revert this
1. Unlike youtube-dlc, yt-dlp does not allow merging multiple audio/video streams into one file by default (since this conflicts with the use of `-f bv*+ba`). If needed, this feature must be enabled using `--audio-multistreams` and `--video-multistreams`. You can also use `--compat-options multistreams` to enable both
1. `--ignore-errors` is enabled by default. Use `--abort-on-error` or `--compat-options abort-on-error` to abort on errors instead
1. When writing metadata files such as thumbnails, description or infojson, the same information (if available) is also written for playlists. Use `--no-write-playlist-metafiles` or `--compat-options no-playlist-metafiles` to not write these files
1. `playlist_index` behaves differently when used with options like `--playlist-reverse` and `--playlist-items`. See [#302](https://github.com/yt-dlp/yt-dlp/issues/302) for details. You can use `--compat-options playlist-index` if you want to keep the earlier behavior
1. The output of `-F` is listed in a new format. Use `--compat-options list-formats` to revert this
1. Youtube live chat (if available) is considered as a subtitle. Use `--sub-langs all,-live_chat` to download all subtitles except live chat. You can also use `--compat-options no-live-chat` to prevent live chat from downloading
1. Youtube channel URLs are automatically redirected to `/video`. Either append a `/featured` to the URL or use `--compat-options no-youtube-channel-redirect` to download only the videos in the home page
1. Unavailable videos are also listed for youtube playlists. Use `--compat-options no-youtube-unavailable-videos` to remove this
For ease of use, a few more compat options are available:
1. `--compat-options all` = Use all compat options
1. `--compat-options youtube-dl` = `--compat-options all,-multistreams`
1. `--compat-options youtube-dlc` = `--compat-options all,-no-live-chat,-no-youtube-channel-redirect`
# INSTALLATION # INSTALLATION
yt-dlp is not platform specific. So it should work on your Unix box, on Windows or on macOS yt-dlp is not platform specific. So it should work on your Unix box, on Windows or on macOS
You can install yt-dlp using one of the following methods: You can install yt-dlp using one of the following methods:
* Download the binary from the [latest release](https://github.com/yt-dlp/yt-dlp/releases/latest) (recommended method) * Download the binary from the [latest release](https://github.com/yt-dlp/yt-dlp/releases/latest) (recommended method)
* Use [PyPI package](https://pypi.org/project/yt-dlp): `python -m pip install --upgrade yt-dlp` * Use [PyPI package](https://pypi.org/project/yt-dlp): `python3 -m pip install --upgrade yt-dlp`
* Use pip+git: `python -m pip install --upgrade git+https://github.com/yt-dlp/yt-dlp.git@release` * Use pip+git: `python3 -m pip install --upgrade git+https://github.com/yt-dlp/yt-dlp.git@release`
* Install master branch: `python -m pip install --upgrade git+https://github.com/yt-dlp/yt-dlp` * Install master branch: `python3 -m pip install --upgrade git+https://github.com/yt-dlp/yt-dlp`
Note that on some systems, you may need to use `py` or `python` instead of `python3`
UNIX users (Linux, macOS, BSD) can also install the [latest release](https://github.com/yt-dlp/yt-dlp/releases/latest) one of the following ways: UNIX users (Linux, macOS, BSD) can also install the [latest release](https://github.com/yt-dlp/yt-dlp/releases/latest) one of the following ways:
@@ -132,11 +162,12 @@ sudo aria2c https://github.com/yt-dlp/yt-dlp/releases/latest/download/yt-dlp -o
sudo chmod a+rx /usr/local/bin/yt-dlp sudo chmod a+rx /usr/local/bin/yt-dlp
``` ```
### DEPENDANCIES ### DEPENDENCIES
Python versions 3.6+ (CPython and PyPy) are officially supported. Other versions and implementations may or maynot work correctly.
Python versions 2.6, 2.7, or 3.2+ are currently supported. However, 3.2+ is strongly recommended and python2 support will be deprecated in the future. On windows, [Microsoft Visual C++ 2010 Redistributable Package (x86)](https://www.microsoft.com/en-us/download/details.aspx?id=26999) is also necessary to run yt-dlp. You probably already have this, but if the executable throws an error due to missing `MSVCR100.dll` you need to install it.
Although there are no required dependancies, `ffmpeg` and `ffprobe` are highly recommended. Other optional dependancies are `sponskrub`, `AtomicParsley`, `mutagen`, `pycryptodome` and any of the supported external downloaders. Note that the windows releases are already built with the python interpreter, mutagen and pycryptodome included. Although there are no other required dependencies, `ffmpeg` and `ffprobe` are highly recommended. Other optional dependencies are `sponskrub`, `AtomicParsley`, `mutagen`, `pycryptodome` and any of the supported external downloaders. Note that the windows releases are already built with the python interpreter, mutagen and pycryptodome included.
### UPDATE ### UPDATE
You can use `yt-dlp -U` to update if you are using the provided release. You can use `yt-dlp -U` to update if you are using the provided release.
@@ -147,14 +178,16 @@ If you are using `pip`, simply re-run the same command that was used to install
**For Windows**: **For Windows**:
To build the Windows executable, you must have pyinstaller (and optionally mutagen and pycryptodome) To build the Windows executable, you must have pyinstaller (and optionally mutagen and pycryptodome)
python -m pip install --upgrade pyinstaller mutagen pycryptodome python3 -m pip install --upgrade pyinstaller mutagen pycryptodome
Once you have all the necessary dependancies installed, just run `py pyinst.py`. The executable will be built for the same architecture (32/64 bit) as the python used to build it. It is strongly reccomended to use python3 although python2.6+ is supported. Once you have all the necessary dependencies installed, just run `py pyinst.py`. The executable will be built for the same architecture (32/64 bit) as the python used to build it.
You can also build the executable without any version info or metadata by using: You can also build the executable without any version info or metadata by using:
pyinstaller.exe yt_dlp\__main__.py --onefile --name yt-dlp pyinstaller.exe yt_dlp\__main__.py --onefile --name yt-dlp
Note that pyinstaller [does not support](https://github.com/pyinstaller/pyinstaller#requirements-and-tested-platforms) Python installed from the Windows store without using a virtual environment
**For Unix**: **For Unix**:
You will need the required build tools: `python`, `make` (GNU), `pandoc`, `zip`, `nosetests` You will need the required build tools: `python`, `make` (GNU), `pandoc`, `zip`, `nosetests`
Then simply run `make`. You can also run `make yt-dlp` instead to compile only the binary without updating any of the additional files Then simply run `make`. You can also run `make yt-dlp` instead to compile only the binary without updating any of the additional files
@@ -166,7 +199,7 @@ Then simply run `make`. You can also run `make yt-dlp` instead to compile only t
yt-dlp [OPTIONS] [--] URL [URL...] yt-dlp [OPTIONS] [--] URL [URL...]
`Ctrl+F` is your friend :D `Ctrl+F` is your friend :D
<!-- Autogenerated --> <!-- Auto generated -->
## General Options: ## General Options:
-h, --help Print this help text and exit -h, --help Print this help text and exit
@@ -211,6 +244,11 @@ Then simply run `make`. You can also run `make yt-dlp` instead to compile only t
--mark-watched Mark videos watched (YouTube only) --mark-watched Mark videos watched (YouTube only)
--no-mark-watched Do not mark videos watched (default) --no-mark-watched Do not mark videos watched (default)
--no-colors Do not emit color codes in output --no-colors Do not emit color codes in output
--compat-options OPTS Options that can help keep compatibility
with youtube-dl and youtube-dlc
configurations by reverting some of the
changes made in yt-dlp. See "Differences in
default behavior" for details
## Network Options: ## Network Options:
--proxy URL Use the specified HTTP/HTTPS/SOCKS proxy. --proxy URL Use the specified HTTP/HTTPS/SOCKS proxy.
@@ -306,6 +344,8 @@ Then simply run `make`. You can also run `make yt-dlp` instead to compile only t
a file that is in the archive a file that is in the archive
--break-on-reject Stop the download process when encountering --break-on-reject Stop the download process when encountering
a file that has been filtered out a file that has been filtered out
--skip-playlist-after-errors N Number of allowed failures until the rest
of the playlist is skipped
--no-download-archive Do not use archive file (default) --no-download-archive Do not use archive file (default)
## Download Options: ## Download Options:
@@ -394,8 +434,6 @@ Then simply run `make`. You can also run `make yt-dlp` instead to compile only t
--output-na-placeholder TEXT Placeholder value for unavailable meta --output-na-placeholder TEXT Placeholder value for unavailable meta
fields in output filename template fields in output filename template
(default: "NA") (default: "NA")
--autonumber-start NUMBER Specify the start value for %(autonumber)s
(default is 1)
--restrict-filenames Restrict filenames to only ASCII --restrict-filenames Restrict filenames to only ASCII
characters, and avoid "&" and spaces in characters, and avoid "&" and spaces in
filenames filenames
@@ -415,7 +453,7 @@ Then simply run `make`. You can also run `make yt-dlp` instead to compile only t
-c, --continue Resume partially downloaded files/fragments -c, --continue Resume partially downloaded files/fragments
(default) (default)
--no-continue Do not resume partially downloaded --no-continue Do not resume partially downloaded
fragments. If the file is unfragmented, fragments. If the file is not fragmented,
restart download of the entire file restart download of the entire file
--part Use .part files instead of writing directly --part Use .part files instead of writing directly
into output file (default) into output file (default)
@@ -487,6 +525,12 @@ Then simply run `make`. You can also run `make yt-dlp` instead to compile only t
--no-warnings Ignore warnings --no-warnings Ignore warnings
-s, --simulate Do not download the video and do not write -s, --simulate Do not download the video and do not write
anything to disk anything to disk
--ignore-no-formats-error Ignore "No video formats" error. Usefull
for extracting metadata even if the video
is not actually available for download
(experimental)
--no-ignore-no-formats-error Throw error when no downloadable video
formats are found (default)
--skip-download Do not download the video but write all --skip-download Do not download the video but write all
related files (Alias: --no-download) related files (Alias: --no-download)
-g, --get-url Simulate, quiet but print URL -g, --get-url Simulate, quiet but print URL
@@ -507,8 +551,8 @@ Then simply run `make`. You can also run `make yt-dlp` instead to compile only t
--print-json Be quiet and print the video information as --print-json Be quiet and print the video information as
JSON (video is still being downloaded) JSON (video is still being downloaded)
--force-write-archive Force download archive entries to be --force-write-archive Force download archive entries to be
written as far as no errors occur,even if written as far as no errors occur, even if
-s or another simulation switch is used -s or another simulation option is used
(Alias: --force-download-archive) (Alias: --force-download-archive)
--newline Output progress bar as new lines --newline Output progress bar as new lines
--no-progress Do not print progress bar --no-progress Do not print progress bar
@@ -572,18 +616,16 @@ Then simply run `make`. You can also run `make yt-dlp` instead to compile only t
containers irrespective of quality containers irrespective of quality
--no-prefer-free-formats Don't give any special preference to free --no-prefer-free-formats Don't give any special preference to free
containers (default) containers (default)
--check-formats Check that the formats selected are
actually downloadable (Experimental)
-F, --list-formats List all available formats of requested -F, --list-formats List all available formats of requested
videos videos
--list-formats-as-table Present the output of -F in tabular form
(default)
--list-formats-old Present the output of -F in the old form
(Alias: --no-list-formats-as-table)
--merge-output-format FORMAT If a merge is required (e.g. --merge-output-format FORMAT If a merge is required (e.g.
bestvideo+bestaudio), output to given bestvideo+bestaudio), output to given
container format. One of mkv, mp4, ogg, container format. One of mkv, mp4, ogg,
webm, flv. Ignored if no merge is required webm, flv. Ignored if no merge is required
--allow-unplayable-formats Allow unplayable formats to be listed and --allow-unplayable-formats Allow unplayable formats to be listed and
downloaded. All video postprocessing will downloaded. All video post-processing will
also be turned off also be turned off
--no-allow-unplayable-formats Do not allow unplayable formats to be --no-allow-unplayable-formats Do not allow unplayable formats to be
listed or downloaded (default) listed or downloaded (default)
@@ -595,15 +637,17 @@ Then simply run `make`. You can also run `make yt-dlp` instead to compile only t
(Alias: --write-automatic-subs) (Alias: --write-automatic-subs)
--no-write-auto-subs Do not write auto-generated subtitles --no-write-auto-subs Do not write auto-generated subtitles
(default) (Alias: --no-write-automatic-subs) (default) (Alias: --no-write-automatic-subs)
--all-subs Download all the available subtitles of the
video
--list-subs List all available subtitles for the video --list-subs List all available subtitles for the video
--sub-format FORMAT Subtitle format, accepts formats --sub-format FORMAT Subtitle format, accepts formats
preference, for example: "srt" or preference, for example: "srt" or
"ass/srt/best" "ass/srt/best"
--sub-langs LANGS Languages of the subtitles to download --sub-langs LANGS Languages of the subtitles to download (can
(optional) separated by commas, use --list- be regex) or "all" separated by commas.
subs for available language tags (Eg: --sub-langs en.*,ja) You can prefix
the language code with a "-" to exempt it
from the requested languages. (Eg: --sub-
langs all,-live_chat) Use --list-subs for a
list of available language tags
## Authentication Options: ## Authentication Options:
-u, --username USERNAME Login with this account ID -u, --username USERNAME Login with this account ID
@@ -830,9 +874,22 @@ The `-o` option is used to indicate a template for the output file names while `
**tl;dr:** [navigate me to examples](#output-template-examples). **tl;dr:** [navigate me to examples](#output-template-examples).
The basic usage of `-o` is not to set any template arguments when downloading a single file, like in `yt-dlp -o funny_video.flv "https://some/video"` (hard-coding file extension like this is not recommended). However, it may contain special sequences that will be replaced when downloading each video. The special sequences may be formatted according to [python string formatting operations](https://docs.python.org/2/library/stdtypes.html#string-formatting). For example, `%(NAME)s` or `%(NAME)05d`. To clarify, that is a percent symbol followed by a name in parentheses, followed by formatting operations. Date/time fields can also be formatted according to [strftime formatting](https://docs.python.org/3/library/datetime.html#strftime-and-strptime-format-codes) by specifying it inside the parantheses separated from the field name using a `>`. For example, `%(duration>%H-%M-%S)s`. The simplest usage of `-o` is not to set any template arguments when downloading a single file, like in `yt-dlp -o funny_video.flv "https://some/video"` (hard-coding file extension like this is _not_ recommended and could break some post-processing).
Additionally, you can set different output templates for the various metadata files separately from the general output template by specifying the type of file followed by the template separated by a colon `:`. The different filetypes supported are `subtitle`, `thumbnail`, `description`, `annotation`, `infojson`, `pl_description`, `pl_infojson`, `chapter`. For example, `-o '%(title)s.%(ext)s' -o 'thumbnail:%(title)s\%(title)s.%(ext)s'` will put the thumbnails in a folder with the same name as the video. It may however also contain special sequences that will be replaced when downloading each video. The special sequences may be formatted according to [python string formatting operations](https://docs.python.org/2/library/stdtypes.html#string-formatting). For example, `%(NAME)s` or `%(NAME)05d`. To clarify, that is a percent symbol followed by a name in parentheses, followed by formatting operations.
The field names themselves (the part inside the parenthesis) can also have some special formatting:
1. **Object traversal**: The dictionaries and lists available in metadata can be traversed by using a `.` (dot) separator. You can also do python slicing using `:`. Eg: `%(tags.0)s`, `%(subtitles.en.-1.ext)`, `%(id.3:7:-1)s`. Note that the fields that become available using this method are not listed below. Use `-j` to see such fields
1. **Addition**: Addition and subtraction of numeric fields can be done using `+` and `-` respectively. Eg: `%(playlist_index+10)03d`, `%(n_entries+1-playlist_index)d`
1. **Date/time Formatting**: Date/time fields can be formatted according to [strftime formatting](https://docs.python.org/3/library/datetime.html#strftime-and-strptime-format-codes) by specifying it separated from the field name using a `>`. Eg: `%(duration>%H-%M-%S)s`, `%(upload_date>%Y-%m-%d)s`, `%(epoch-3600>%H-%M-%S)s`
1. **Default**: A default value can be specified for when the field is empty using a `|` seperator. This overrides `--output-na-template`. Eg: `%(uploader|Unknown)s`
To summarize, the general syntax for a field is:
```
%(name[.keys][addition][>strf][|default])[flags][width][.precision][length]type
```
Additionally, you can set different output templates for the various metadata files separately from the general output template by specifying the type of file followed by the template separated by a colon `:`. The different file types supported are `subtitle`, `thumbnail`, `description`, `annotation`, `infojson`, `pl_description`, `pl_infojson`, `chapter`. For example, `-o '%(title)s.%(ext)s' -o 'thumbnail:%(title)s\%(title)s.%(ext)s'` will put the thumbnails in a folder with the same name as the video.
The available fields are: The available fields are:
@@ -1010,7 +1067,7 @@ You can also use special names to select particular edge case formats:
- `ba*`, `bestaudio*`: Select the best quality format that contains audio. It may also contain video. Equivalent to `best*[acodec!=none]` - `ba*`, `bestaudio*`: Select the best quality format that contains audio. It may also contain video. Equivalent to `best*[acodec!=none]`
- `wa*`, `worstaudio*`: Select the worst quality format that contains audio. It may also contain video. Equivalent to `worst*[acodec!=none]` - `wa*`, `worstaudio*`: Select the worst quality format that contains audio. It may also contain video. Equivalent to `worst*[acodec!=none]`
For example, to download the worst quality video-only format you can use `-f worstvideo`. It is however recomended not to use `worst` and related options. When your format selector is `worst`, the format which is worst in all respects is selected. Most of the time, what you actually want is the video with the smallest filesize instead. So it is generally better to use `-f best -S +size,+br,+res,+fps` instead of `-f worst`. See [sorting formats](#sorting-formats) for more details. For example, to download the worst quality video-only format you can use `-f worstvideo`. It is however recommended not to use `worst` and related options. When your format selector is `worst`, the format which is worst in all respects is selected. Most of the time, what you actually want is the video with the smallest filesize instead. So it is generally better to use `-f best -S +size,+br,+res,+fps` instead of `-f worst`. See [sorting formats](#sorting-formats) for more details.
You can select the n'th best format of a type by using `best<type>.<n>`. For example, `best.2` will select the 2nd best combined format. Similarly, `bv*.3` will select the 3rd best format that contains a video stream. You can select the n'th best format of a type by using `best<type>.<n>`. For example, `best.2` will select the 2nd best combined format. Similarly, `bv*.3` will select the 3rd best format that contains a video stream.
@@ -1047,7 +1104,7 @@ Also filtering work for comparisons `=` (equals), `^=` (starts with), `$=` (ends
Any string comparison may be prefixed with negation `!` in order to produce an opposite comparison, e.g. `!*=` (does not contain). Any string comparison may be prefixed with negation `!` in order to produce an opposite comparison, e.g. `!*=` (does not contain).
Note that none of the aforementioned meta fields are guaranteed to be present since this solely depends on the metadata obtained by particular extractor, i.e. the metadata offered by the video hoster. Any other field made available by the extractor can also be used for filtering. Note that none of the aforementioned meta fields are guaranteed to be present since this solely depends on the metadata obtained by particular extractor, i.e. the metadata offered by the website. Any other field made available by the extractor can also be used for filtering.
Formats for which the value is not known are excluded unless you put a question mark (`?`) after the operator. You can combine format filters, so `-f "[height<=?720][tbr>500]"` selects up to 720p videos (or videos where the height is not known) with a bitrate of at least 500 KBit/s. You can also use the filters with `all` to download all formats that satisfy the filter. For example, `-f "all[vcodec=none]"` selects all audio-only formats. Formats for which the value is not known are excluded unless you put a question mark (`?`) after the operator. You can combine format filters, so `-f "[height<=?720][tbr>500]"` selects up to 720p videos (or videos where the height is not known) with a bitrate of at least 500 KBit/s. You can also use the filters with `all` to download all formats that satisfy the filter. For example, `-f "all[vcodec=none]"` selects all audio-only formats.
@@ -1063,7 +1120,7 @@ You can change the criteria for being considered the `best` by using `-S` (`--fo
- `lang`: Language preference as given by the extractor - `lang`: Language preference as given by the extractor
- `quality`: The quality of the format as given by the extractor - `quality`: The quality of the format as given by the extractor
- `source`: Preference of the source as given by the extractor - `source`: Preference of the source as given by the extractor
- `proto`: Protocol used for download (`https`/`ftps` > `http`/`ftp` > `m3u8-native` > `m3u8` > `http-dash-segments` > other > `mms`/`rtsp` > unknown > `f4f`/`f4m`) - `proto`: Protocol used for download (`https`/`ftps` > `http`/`ftp` > `m3u8_native` > `m3u8` > `http_dash_segments` > other > `mms`/`rtsp` > unknown > `f4f`/`f4m`)
- `vcodec`: Video Codec (`av01` > `vp9.2` > `vp9` > `h265` > `h264` > `vp8` > `h263` > `theora` > other > unknown) - `vcodec`: Video Codec (`av01` > `vp9.2` > `vp9` > `h265` > `h264` > `vp8` > `h263` > `theora` > other > unknown)
- `acodec`: Audio Codec (`opus` > `vorbis` > `aac` > `mp4a` > `mp3` > `ac3` > `dts` > other > unknown) - `acodec`: Audio Codec (`opus` > `vorbis` > `aac` > `mp4a` > `mp3` > `ac3` > `dts` > other > unknown)
- `codec`: Equivalent to `vcodec,acodec` - `codec`: Equivalent to `vcodec,acodec`
@@ -1083,11 +1140,11 @@ You can change the criteria for being considered the `best` by using `-S` (`--fo
- `br`: Equivalent to using `tbr,vbr,abr` - `br`: Equivalent to using `tbr,vbr,abr`
- `asr`: Audio sample rate in Hz - `asr`: Audio sample rate in Hz
Note that any other **numerical** field made available by the extractor can also be used. All fields, unless specified otherwise, are sorted in decending order. To reverse this, prefix the field with a `+`. Eg: `+res` prefers format with the smallest resolution. Additionally, you can suffix a prefered value for the fields, separated by a `:`. Eg: `res:720` prefers larger videos, but no larger than 720p and the smallest video if there are no videos less than 720p. For `codec` and `ext`, you can provide two prefered values, the first for video and the second for audio. Eg: `+codec:avc:m4a` (equivalent to `+vcodec:avc,+acodec:m4a`) sets the video codec preference to `h264` > `h265` > `vp9` > `vp9.2` > `av01` > `vp8` > `h263` > `theora` and audio codec preference to `mp4a` > `aac` > `vorbis` > `opus` > `mp3` > `ac3` > `dts`. You can also make the sorting prefer the nearest values to the provided by using `~` as the delimiter. Eg: `filesize~1G` prefers the format with filesize closest to 1 GiB. Note that any other **numerical** field made available by the extractor can also be used. All fields, unless specified otherwise, are sorted in descending order. To reverse this, prefix the field with a `+`. Eg: `+res` prefers format with the smallest resolution. Additionally, you can suffix a preferred value for the fields, separated by a `:`. Eg: `res:720` prefers larger videos, but no larger than 720p and the smallest video if there are no videos less than 720p. For `codec` and `ext`, you can provide two preferred values, the first for video and the second for audio. Eg: `+codec:avc:m4a` (equivalent to `+vcodec:avc,+acodec:m4a`) sets the video codec preference to `h264` > `h265` > `vp9` > `vp9.2` > `av01` > `vp8` > `h263` > `theora` and audio codec preference to `mp4a` > `aac` > `vorbis` > `opus` > `mp3` > `ac3` > `dts`. You can also make the sorting prefer the nearest values to the provided by using `~` as the delimiter. Eg: `filesize~1G` prefers the format with filesize closest to 1 GiB.
The fields `hasvid`, `ie_pref`, `lang` are always given highest priority in sorting, irrespective of the user-defined order. This behaviour can be changed by using `--force-format-sort`. Apart from these, the default order used is: `quality,res,fps,codec:vp9.2,size,br,asr,proto,ext,hasaud,source,id`. Note that the extractors may override this default order, but they cannot override the user-provided order. The fields `hasvid`, `ie_pref`, `lang` are always given highest priority in sorting, irrespective of the user-defined order. This behaviour can be changed by using `--force-format-sort`. Apart from these, the default order used is: `quality,res,fps,codec:vp9.2,size,br,asr,proto,ext,hasaud,source,id`. Note that the extractors may override this default order, but they cannot override the user-provided order.
If your format selector is `worst`, the last item is selected after sorting. This means it will select the format that is worst in all repects. Most of the time, what you actually want is the video with the smallest filesize instead. So it is generally better to use `-f best -S +size,+br,+res,+fps`. If your format selector is `worst`, the last item is selected after sorting. This means it will select the format that is worst in all respects. Most of the time, what you actually want is the video with the smallest filesize instead. So it is generally better to use `-f best -S +size,+br,+res,+fps`.
**Tip**: You can use the `-v -F` to see how the formats have been sorted (worst to best). **Tip**: You can use the `-v -F` to see how the formats have been sorted (worst to best).
@@ -1096,7 +1153,7 @@ If your format selector is `worst`, the last item is selected after sorting. Thi
Note that on Windows you may need to use double quotes instead of single. Note that on Windows you may need to use double quotes instead of single.
```bash ```bash
# Download and merge the best best video-only format and the best audio-only format, # Download and merge the best video-only format and the best audio-only format,
# or download the best combined format if video-only format is not available # or download the best combined format if video-only format is not available
$ yt-dlp -f 'bv+ba/b' $ yt-dlp -f 'bv+ba/b'
@@ -1198,20 +1255,20 @@ $ yt-dlp -S '+codec:h264'
# More complex examples # More complex examples
# Download the best video no better than 720p prefering framerate greater than 30, # Download the best video no better than 720p preferring framerate greater than 30,
# or the worst video (still prefering framerate greater than 30) if there is no such video # or the worst video (still preferring framerate greater than 30) if there is no such video
$ yt-dlp -f '((bv*[fps>30]/bv*)[height<=720]/(wv*[fps>30]/wv*)) + ba / (b[fps>30]/b)[height<=720]/(w[fps>30]/w)' $ yt-dlp -f '((bv*[fps>30]/bv*)[height<=720]/(wv*[fps>30]/wv*)) + ba / (b[fps>30]/b)[height<=720]/(w[fps>30]/w)'
# Download the video with the largest resolution no better than 720p, # Download the video with the largest resolution no better than 720p,
# or the video with the smallest resolution available if there is no such video, # or the video with the smallest resolution available if there is no such video,
# prefering larger framerate for formats with the same resolution # preferring larger framerate for formats with the same resolution
$ yt-dlp -S 'res:720,fps' $ yt-dlp -S 'res:720,fps'
# Download the video with smallest resolution no worse than 480p, # Download the video with smallest resolution no worse than 480p,
# or the video with the largest resolution available if there is no such video, # or the video with the largest resolution available if there is no such video,
# prefering better codec and then larger total bitrate for the same resolution # preferring better codec and then larger total bitrate for the same resolution
$ yt-dlp -S '+res:480,codec,br' $ yt-dlp -S '+res:480,codec,br'
``` ```
@@ -1252,39 +1309,59 @@ Plugins are loaded from `<root-dir>/ytdlp_plugins/<type>/__init__.py`. Currently
These are all the deprecated options and the current alternative to achieve the same effect These are all the deprecated options and the current alternative to achieve the same effect
#### Not recommended
While these options still work, their use is not recommended since there are other alternatives to achieve the same
--all-formats -f all
--all-subs --sub-langs all --write-subs
--autonumber-size NUMBER Use string formatting. Eg: %(autonumber)03d
--autonumber-start NUMBER Use internal field formatting like %(autonumber+NUMBER)s
--metadata-from-title FORMAT --parse-metadata "%(title)s:FORMAT"
--hls-prefer-native --downloader "m3u8:native"
--hls-prefer-ffmpeg --downloader "m3u8:ffmpeg"
--list-formats-old --compat-options list-formats (Alias: --no-list-formats-as-table)
--list-formats-as-table --compat-options -list-formats [Default] (Alias: --no-list-formats-old)
--sponskrub-args ARGS --ppa "sponskrub:ARGS"
--test Used by developers for testing extractors. Not intended for the end user
#### Old aliases
These are aliases that are no longer documented for various reasons
--avconv-location --ffmpeg-location
--cn-verification-proxy URL --geo-verification-proxy URL --cn-verification-proxy URL --geo-verification-proxy URL
--dump-headers --print-traffic
--dump-intermediate-pages --dump-pages
--force-write-download-archive --force-write-archive
--load-info --load-info-json
--no-split-tracks --no-split-chapters
--no-write-srt --no-write-subs
--prefer-unsecure --prefer-insecure
--rate-limit RATE --limit-rate RATE
--split-tracks --split-chapters
--srt-lang LANGS --sub-langs LANGS
--trim-file-names LENGTH --trim-filenames LENGTH
--write-srt --write-subs
--yes-overwrites --force-overwrites
#### No longer supported
These options may no longer work as intended
--prefer-avconv avconv is not officially supported by yt-dlp (Alias: --no-prefer-ffmpeg)
--prefer-ffmpeg Default (Alias: --no-prefer-avconv)
-C, --call-home Not implemented
--no-call-home Default
--include-ads No longer supported
--no-include-ads Default
--youtube-print-sig-code No longer supported
#### Removed
These options were deprecated since 2014 and have now been entirely removed
--id -o "%(id)s.%(ext)s" --id -o "%(id)s.%(ext)s"
-A, --auto-number -o "%(autonumber)s-%(id)s.%(ext)s" -A, --auto-number -o "%(autonumber)s-%(id)s.%(ext)s"
-t, --title -o "%(title)s-%(id)s.%(ext)s" -t, --title -o "%(title)s-%(id)s.%(ext)s"
-l, --literal -o accepts literal names -l, --literal -o accepts literal names
--all-formats -f all
--autonumber-size NUMBER Use string formatting. Eg: %(autonumber)03d
--metadata-from-title FORMAT --parse-metadata "%(title)s:FORMAT"
--prefer-avconv avconv is no longer officially supported (Alias: --no-prefer-ffmpeg)
--prefer-ffmpeg Default (Alias: --no-prefer-avconv)
--hls-prefer-native --downloader "m3u8:native"
--hls-prefer-ffmpeg --downloader "m3u8:ffmpeg"
--avconv-location avconv is no longer officially supported
-C, --call-home Not implemented
--no-call-home Default
--include-ads Not implemented
--no-include-ads Default
--write-srt --write-subs
--no-write-srt --no-write-subs
--srt-lang LANGS --sub-langs LANGS
--prefer-unsecure --prefer-insecure
--rate-limit RATE --limit-rate RATE
--force-write-download-archive --force-write-archive
--dump-intermediate-pages --dump-pages
--dump-headers --print-traffic
--youtube-print-sig-code No longer supported
--trim-file-names LENGTH --trim-filenames LENGTH
--yes-overwrites --force-overwrites
--load-info --load-info-json
--split-tracks --split-chapters
--no-split-tracks --no-split-chapters
--sponskrub-args ARGS --ppa "sponskrub:ARGS"
--test Only used for testing extractors
# MORE # MORE
+5
View File
@@ -14,9 +14,14 @@ lazy_extractors_filename = sys.argv[1]
if os.path.exists(lazy_extractors_filename): if os.path.exists(lazy_extractors_filename):
os.remove(lazy_extractors_filename) os.remove(lazy_extractors_filename)
# Block plugins from loading
os.rename('ytdlp_plugins', 'ytdlp_plugins_blocked')
from yt_dlp.extractor import _ALL_CLASSES from yt_dlp.extractor import _ALL_CLASSES
from yt_dlp.extractor.common import InfoExtractor, SearchInfoExtractor from yt_dlp.extractor.common import InfoExtractor, SearchInfoExtractor
os.rename('ytdlp_plugins_blocked', 'ytdlp_plugins')
with open('devscripts/lazy_load_template.py', 'rt') as f: with open('devscripts/lazy_load_template.py', 'rt') as f:
module_template = f.read() module_template = f.read()
+1
View File
@@ -69,6 +69,7 @@ PyInstaller.__main__.run([
'--onefile', '--onefile',
'--icon=devscripts/cloud.ico', '--icon=devscripts/cloud.ico',
'--exclude-module=youtube_dl', '--exclude-module=youtube_dl',
'--exclude-module=youtube_dlc',
'--exclude-module=test', '--exclude-module=test',
'--exclude-module=ytdlp_plugins', '--exclude-module=ytdlp_plugins',
'--hidden-import=mutagen', '--hidden-import=mutagen',
+30 -30
View File
@@ -18,13 +18,13 @@ DESCRIPTION = 'Command-line program to download videos from YouTube.com and many
LONG_DESCRIPTION = '\n\n'.join(( LONG_DESCRIPTION = '\n\n'.join((
'Official repository: <https://github.com/yt-dlp/yt-dlp>', 'Official repository: <https://github.com/yt-dlp/yt-dlp>',
'**PS**: Many links in this document will not work since this is a copy of the README.md from Github', '**PS**: Many links in this document will not work since this is a copy of the README.md from Github',
open("README.md", "r", encoding="utf-8").read())) open('README.md', 'r', encoding='utf-8').read()))
REQUIREMENTS = ['mutagen', 'pycryptodome'] REQUIREMENTS = ['mutagen', 'pycryptodome']
if len(sys.argv) >= 2 and sys.argv[1] == 'py2exe': if len(sys.argv) >= 2 and sys.argv[1] == 'py2exe':
print("inv") print('inv')
else: else:
files_spec = [ files_spec = [
('share/bash-completion/completions', ['completions/bash/yt-dlp']), ('share/bash-completion/completions', ['completions/bash/yt-dlp']),
@@ -67,17 +67,17 @@ class build_lazy_extractors(Command):
) )
packages = find_packages(exclude=("youtube_dl", "test", "ytdlp_plugins")) packages = find_packages(exclude=('youtube_dl', 'test', 'ytdlp_plugins'))
setup( setup(
name="yt-dlp", name='yt-dlp',
version=__version__, version=__version__,
maintainer="pukkandan", maintainer='pukkandan',
maintainer_email="pukkandan.ytdlp@gmail.com", maintainer_email='pukkandan.ytdlp@gmail.com',
description=DESCRIPTION, description=DESCRIPTION,
long_description=LONG_DESCRIPTION, long_description=LONG_DESCRIPTION,
long_description_content_type="text/markdown", long_description_content_type='text/markdown',
url="https://github.com/yt-dlp/yt-dlp", url='https://github.com/yt-dlp/yt-dlp',
packages=packages, packages=packages,
install_requires=REQUIREMENTS, install_requires=REQUIREMENTS,
project_urls={ project_urls={
@@ -87,28 +87,28 @@ setup(
#'Funding': 'https://donate.pypi.org', #'Funding': 'https://donate.pypi.org',
}, },
classifiers=[ classifiers=[
"Topic :: Multimedia :: Video", 'Topic :: Multimedia :: Video',
"Development Status :: 5 - Production/Stable", 'Development Status :: 5 - Production/Stable',
"Environment :: Console", 'Environment :: Console',
"Programming Language :: Python", 'Programming Language :: Python',
"Programming Language :: Python :: 2", 'Programming Language :: Python :: 2',
"Programming Language :: Python :: 2.6", 'Programming Language :: Python :: 2.6',
"Programming Language :: Python :: 2.7", 'Programming Language :: Python :: 2.7',
"Programming Language :: Python :: 3", 'Programming Language :: Python :: 3',
"Programming Language :: Python :: 3.2", 'Programming Language :: Python :: 3.2',
"Programming Language :: Python :: 3.3", 'Programming Language :: Python :: 3.3',
"Programming Language :: Python :: 3.4", 'Programming Language :: Python :: 3.4',
"Programming Language :: Python :: 3.5", 'Programming Language :: Python :: 3.5',
"Programming Language :: Python :: 3.6", 'Programming Language :: Python :: 3.6',
"Programming Language :: Python :: 3.7", 'Programming Language :: Python :: 3.7',
"Programming Language :: Python :: 3.8", 'Programming Language :: Python :: 3.8',
"Programming Language :: Python :: Implementation", 'Programming Language :: Python :: Implementation',
"Programming Language :: Python :: Implementation :: CPython", 'Programming Language :: Python :: Implementation :: CPython',
"Programming Language :: Python :: Implementation :: IronPython", 'Programming Language :: Python :: Implementation :: IronPython',
"Programming Language :: Python :: Implementation :: Jython", 'Programming Language :: Python :: Implementation :: Jython',
"Programming Language :: Python :: Implementation :: PyPy", 'Programming Language :: Python :: Implementation :: PyPy',
"License :: Public Domain", 'License :: Public Domain',
"Operating System :: OS Independent", 'Operating System :: OS Independent',
], ],
python_requires='>=2.6', python_requires='>=2.6',
+7 -3
View File
@@ -130,7 +130,6 @@
- **bitwave:stream** - **bitwave:stream**
- **BleacherReport** - **BleacherReport**
- **BleacherReportCMS** - **BleacherReportCMS**
- **blinkx**
- **Bloomberg** - **Bloomberg**
- **BokeCC** - **BokeCC**
- **BongaCams** - **BongaCams**
@@ -225,7 +224,8 @@
- **Culturebox** - **Culturebox**
- **CultureUnplugged** - **CultureUnplugged**
- **curiositystream** - **curiositystream**
- **curiositystream:collection** - **curiositystream:collections**
- **curiositystream:series**
- **CWTV** - **CWTV**
- **DagelijkseKost**: dagelijksekost.een.be - **DagelijkseKost**: dagelijksekost.een.be
- **DailyMail** - **DailyMail**
@@ -584,6 +584,7 @@
- **Mwave** - **Mwave**
- **MwaveMeetGreet** - **MwaveMeetGreet**
- **Mxplayer** - **Mxplayer**
- **MxplayerShow**
- **MyChannels** - **MyChannels**
- **MySpace** - **MySpace**
- **MySpace:album** - **MySpace:album**
@@ -1016,6 +1017,7 @@
- **TruTV** - **TruTV**
- **Tube8** - **Tube8**
- **TubiTv** - **TubiTv**
- **TubiTvShow**
- **Tumblr** - **Tumblr**
- **tunein:clip** - **tunein:clip**
- **tunein:program** - **tunein:program**
@@ -1075,6 +1077,7 @@
- **UDNEmbed**: 聯合影音 - **UDNEmbed**: 聯合影音
- **UFCArabia** - **UFCArabia**
- **UFCTV** - **UFCTV**
- **ukcolumn**
- **UKTVPlay** - **UKTVPlay**
- **umg:de**: Universal Music Deutschland - **umg:de**: Universal Music Deutschland
- **Unistra** - **Unistra**
@@ -1193,6 +1196,7 @@
- **Weibo** - **Weibo**
- **WeiboMobile** - **WeiboMobile**
- **WeiqiTV**: WQTV - **WeiqiTV**: WQTV
- **whowatch**
- **WimTV** - **WimTV**
- **Wistia** - **Wistia**
- **WistiaPlaylist** - **WistiaPlaylist**
@@ -1203,7 +1207,7 @@
- **WWE** - **WWE**
- **XBef** - **XBef**
- **XboxClips** - **XboxClips**
- **XFileShare**: XFileShare based sites: Aparat, ClipWatching, GoUnlimited, GoVid, HolaVid, Streamty, TheVideoBee, Uqload, VidBom, vidlo, VidLocker, VidShare, VUp, XVideoSharing - **XFileShare**: XFileShare based sites: Aparat, ClipWatching, GoUnlimited, GoVid, HolaVid, Streamty, TheVideoBee, Uqload, VidBom, vidlo, VidLocker, VidShare, VUp, WolfStream, XVideoSharing
- **XHamster** - **XHamster**
- **XHamsterEmbed** - **XHamsterEmbed**
- **XHamsterUser** - **XHamsterUser**
+489 -8
View File
@@ -684,17 +684,186 @@ jwplayer("mediaplayer").setup({"abouttext":"Visit Indie DB","aboutlink":"http:\/
'width': 1920, 'width': 1920,
'height': 1080, 'height': 1080,
'vcodec': 'avc1.64002a', 'vcodec': 'avc1.64002a',
}] }],
{}
),
(
'bipbop_16x9',
'https://devstreaming-cdn.apple.com/videos/streaming/examples/bipbop_16x9/bipbop_16x9_variant.m3u8',
[{
"format_id": "bipbop_audio-BipBop Audio 2",
"format_index": None,
"url": "https://devstreaming-cdn.apple.com/videos/streaming/examples/bipbop_16x9/alternate_audio_aac/prog_index.m3u8",
"manifest_url": "https://devstreaming-cdn.apple.com/videos/streaming/examples/bipbop_16x9/bipbop_16x9_variant.m3u8",
"language": "eng",
"ext": "mp4",
"protocol": "m3u8",
"preference": None,
"quality": None,
"vcodec": "none",
"audio_ext": "mp4",
"video_ext": "none",
}, {
"format_id": "41",
"format_index": None,
"url": "https://devstreaming-cdn.apple.com/videos/streaming/examples/bipbop_16x9/gear0/prog_index.m3u8",
"manifest_url": "https://devstreaming-cdn.apple.com/videos/streaming/examples/bipbop_16x9/bipbop_16x9_variant.m3u8",
"tbr": 41.457,
"ext": "mp4",
"fps": None,
"protocol": "m3u8",
"preference": None,
"quality": None,
"vcodec": "none",
"acodec": "mp4a.40.2",
"audio_ext": "mp4",
"video_ext": "none",
"abr": 41.457,
}, {
"format_id": "263",
"format_index": None,
"url": "https://devstreaming-cdn.apple.com/videos/streaming/examples/bipbop_16x9/gear1/prog_index.m3u8",
"manifest_url": "https://devstreaming-cdn.apple.com/videos/streaming/examples/bipbop_16x9/bipbop_16x9_variant.m3u8",
"tbr": 263.851,
"ext": "mp4",
"fps": None,
"protocol": "m3u8",
"preference": None,
"quality": None,
"width": 416,
"height": 234,
"vcodec": "avc1.4d400d",
"acodec": "mp4a.40.2",
"video_ext": "mp4",
"audio_ext": "none",
"vbr": 263.851,
"abr": 0,
}, {
"format_id": "577",
"format_index": None,
"url": "https://devstreaming-cdn.apple.com/videos/streaming/examples/bipbop_16x9/gear2/prog_index.m3u8",
"manifest_url": "https://devstreaming-cdn.apple.com/videos/streaming/examples/bipbop_16x9/bipbop_16x9_variant.m3u8",
"tbr": 577.61,
"ext": "mp4",
"fps": None,
"protocol": "m3u8",
"preference": None,
"quality": None,
"width": 640,
"height": 360,
"vcodec": "avc1.4d401e",
"acodec": "mp4a.40.2",
"video_ext": "mp4",
"audio_ext": "none",
"vbr": 577.61,
"abr": 0,
}, {
"format_id": "915",
"format_index": None,
"url": "https://devstreaming-cdn.apple.com/videos/streaming/examples/bipbop_16x9/gear3/prog_index.m3u8",
"manifest_url": "https://devstreaming-cdn.apple.com/videos/streaming/examples/bipbop_16x9/bipbop_16x9_variant.m3u8",
"tbr": 915.905,
"ext": "mp4",
"fps": None,
"protocol": "m3u8",
"preference": None,
"quality": None,
"width": 960,
"height": 540,
"vcodec": "avc1.4d401f",
"acodec": "mp4a.40.2",
"video_ext": "mp4",
"audio_ext": "none",
"vbr": 915.905,
"abr": 0,
}, {
"format_id": "1030",
"format_index": None,
"url": "https://devstreaming-cdn.apple.com/videos/streaming/examples/bipbop_16x9/gear4/prog_index.m3u8",
"manifest_url": "https://devstreaming-cdn.apple.com/videos/streaming/examples/bipbop_16x9/bipbop_16x9_variant.m3u8",
"tbr": 1030.138,
"ext": "mp4",
"fps": None,
"protocol": "m3u8",
"preference": None,
"quality": None,
"width": 1280,
"height": 720,
"vcodec": "avc1.4d401f",
"acodec": "mp4a.40.2",
"video_ext": "mp4",
"audio_ext": "none",
"vbr": 1030.138,
"abr": 0,
}, {
"format_id": "1924",
"format_index": None,
"url": "https://devstreaming-cdn.apple.com/videos/streaming/examples/bipbop_16x9/gear5/prog_index.m3u8",
"manifest_url": "https://devstreaming-cdn.apple.com/videos/streaming/examples/bipbop_16x9/bipbop_16x9_variant.m3u8",
"tbr": 1924.009,
"ext": "mp4",
"fps": None,
"protocol": "m3u8",
"preference": None,
"quality": None,
"width": 1920,
"height": 1080,
"vcodec": "avc1.4d401f",
"acodec": "mp4a.40.2",
"video_ext": "mp4",
"audio_ext": "none",
"vbr": 1924.009,
"abr": 0,
}],
{
"en": [{
"url": "https://devstreaming-cdn.apple.com/videos/streaming/examples/bipbop_16x9/subtitles/eng/prog_index.m3u8",
"ext": "vtt",
"protocol": "m3u8_native"
}, {
"url": "https://devstreaming-cdn.apple.com/videos/streaming/examples/bipbop_16x9/subtitles/eng_forced/prog_index.m3u8",
"ext": "vtt",
"protocol": "m3u8_native"
}],
"fr": [{
"url": "https://devstreaming-cdn.apple.com/videos/streaming/examples/bipbop_16x9/subtitles/fra/prog_index.m3u8",
"ext": "vtt",
"protocol": "m3u8_native"
}, {
"url": "https://devstreaming-cdn.apple.com/videos/streaming/examples/bipbop_16x9/subtitles/fra_forced/prog_index.m3u8",
"ext": "vtt",
"protocol": "m3u8_native"
}],
"es": [{
"url": "https://devstreaming-cdn.apple.com/videos/streaming/examples/bipbop_16x9/subtitles/spa/prog_index.m3u8",
"ext": "vtt",
"protocol": "m3u8_native"
}, {
"url": "https://devstreaming-cdn.apple.com/videos/streaming/examples/bipbop_16x9/subtitles/spa_forced/prog_index.m3u8",
"ext": "vtt",
"protocol": "m3u8_native"
}],
"ja": [{
"url": "https://devstreaming-cdn.apple.com/videos/streaming/examples/bipbop_16x9/subtitles/jpn/prog_index.m3u8",
"ext": "vtt",
"protocol": "m3u8_native"
}, {
"url": "https://devstreaming-cdn.apple.com/videos/streaming/examples/bipbop_16x9/subtitles/jpn_forced/prog_index.m3u8",
"ext": "vtt",
"protocol": "m3u8_native"
}],
}
), ),
] ]
for m3u8_file, m3u8_url, expected_formats in _TEST_CASES: for m3u8_file, m3u8_url, expected_formats, expected_subs in _TEST_CASES:
with io.open('./test/testdata/m3u8/%s.m3u8' % m3u8_file, with io.open('./test/testdata/m3u8/%s.m3u8' % m3u8_file,
mode='r', encoding='utf-8') as f: mode='r', encoding='utf-8') as f:
formats = self.ie._parse_m3u8_formats( formats, subs = self.ie._parse_m3u8_formats_and_subtitles(
f.read(), m3u8_url, ext='mp4') f.read(), m3u8_url, ext='mp4')
self.ie._sort_formats(formats) self.ie._sort_formats(formats)
expect_value(self, formats, expected_formats, None) expect_value(self, formats, expected_formats, None)
expect_value(self, subs, expected_subs, None)
def test_parse_mpd_formats(self): def test_parse_mpd_formats(self):
_TEST_CASES = [ _TEST_CASES = [
@@ -780,7 +949,8 @@ jwplayer("mediaplayer").setup({"abouttext":"Visit Indie DB","aboutlink":"http:\/
'tbr': 5997.485, 'tbr': 5997.485,
'width': 1920, 'width': 1920,
'height': 1080, 'height': 1080,
}] }],
{},
), ( ), (
# https://github.com/ytdl-org/youtube-dl/pull/14844 # https://github.com/ytdl-org/youtube-dl/pull/14844
'urls_only', 'urls_only',
@@ -863,7 +1033,8 @@ jwplayer("mediaplayer").setup({"abouttext":"Visit Indie DB","aboutlink":"http:\/
'tbr': 4400, 'tbr': 4400,
'width': 1920, 'width': 1920,
'height': 1080, 'height': 1080,
}] }],
{},
), ( ), (
# https://github.com/ytdl-org/youtube-dl/issues/20346 # https://github.com/ytdl-org/youtube-dl/issues/20346
# Media considered unfragmented even though it contains # Media considered unfragmented even though it contains
@@ -909,18 +1080,328 @@ jwplayer("mediaplayer").setup({"abouttext":"Visit Indie DB","aboutlink":"http:\/
'width': 360, 'width': 360,
'height': 360, 'height': 360,
'fps': 30, 'fps': 30,
}] }],
{},
), (
'subtitles',
'https://sdn-global-streaming-cache-3qsdn.akamaized.net/stream/3144/files/17/07/672975/3144-kZT4LWMQw6Rh7Kpd.ism/manifest.mpd',
'https://sdn-global-streaming-cache-3qsdn.akamaized.net/stream/3144/files/17/07/672975/3144-kZT4LWMQw6Rh7Kpd.ism/',
[{
"format_id": "audio=128001",
"manifest_url": "https://sdn-global-streaming-cache-3qsdn.akamaized.net/stream/3144/files/17/07/672975/3144-kZT4LWMQw6Rh7Kpd.ism/manifest.mpd",
"ext": "m4a",
"tbr": 128.001,
"asr": 48000,
"format_note": "DASH audio",
"container": "m4a_dash",
"vcodec": "none",
"acodec": "mp4a.40.2",
"url": "https://sdn-global-streaming-cache-3qsdn.akamaized.net/stream/3144/files/17/07/672975/3144-kZT4LWMQw6Rh7Kpd.ism/manifest.mpd",
"fragment_base_url": "https://sdn-global-streaming-cache-3qsdn.akamaized.net/stream/3144/files/17/07/672975/3144-kZT4LWMQw6Rh7Kpd.ism/dash/",
"protocol": "http_dash_segments",
"audio_ext": "m4a",
"video_ext": "none",
"abr": 128.001,
}, {
"format_id": "video=100000",
"manifest_url": "https://sdn-global-streaming-cache-3qsdn.akamaized.net/stream/3144/files/17/07/672975/3144-kZT4LWMQw6Rh7Kpd.ism/manifest.mpd",
"ext": "mp4",
"width": 336,
"height": 144,
"tbr": 100,
"format_note": "DASH video",
"container": "mp4_dash",
"vcodec": "avc1.4D401F",
"acodec": "none",
"url": "https://sdn-global-streaming-cache-3qsdn.akamaized.net/stream/3144/files/17/07/672975/3144-kZT4LWMQw6Rh7Kpd.ism/manifest.mpd",
"fragment_base_url": "https://sdn-global-streaming-cache-3qsdn.akamaized.net/stream/3144/files/17/07/672975/3144-kZT4LWMQw6Rh7Kpd.ism/dash/",
"protocol": "http_dash_segments",
"video_ext": "mp4",
"audio_ext": "none",
"vbr": 100,
}, {
"format_id": "video=326000",
"manifest_url": "https://sdn-global-streaming-cache-3qsdn.akamaized.net/stream/3144/files/17/07/672975/3144-kZT4LWMQw6Rh7Kpd.ism/manifest.mpd",
"ext": "mp4",
"width": 562,
"height": 240,
"tbr": 326,
"format_note": "DASH video",
"container": "mp4_dash",
"vcodec": "avc1.4D401F",
"acodec": "none",
"url": "https://sdn-global-streaming-cache-3qsdn.akamaized.net/stream/3144/files/17/07/672975/3144-kZT4LWMQw6Rh7Kpd.ism/manifest.mpd",
"fragment_base_url": "https://sdn-global-streaming-cache-3qsdn.akamaized.net/stream/3144/files/17/07/672975/3144-kZT4LWMQw6Rh7Kpd.ism/dash/",
"protocol": "http_dash_segments",
"video_ext": "mp4",
"audio_ext": "none",
"vbr": 326,
}, {
"format_id": "video=698000",
"manifest_url": "https://sdn-global-streaming-cache-3qsdn.akamaized.net/stream/3144/files/17/07/672975/3144-kZT4LWMQw6Rh7Kpd.ism/manifest.mpd",
"ext": "mp4",
"width": 844,
"height": 360,
"tbr": 698,
"format_note": "DASH video",
"container": "mp4_dash",
"vcodec": "avc1.4D401F",
"acodec": "none",
"url": "https://sdn-global-streaming-cache-3qsdn.akamaized.net/stream/3144/files/17/07/672975/3144-kZT4LWMQw6Rh7Kpd.ism/manifest.mpd",
"fragment_base_url": "https://sdn-global-streaming-cache-3qsdn.akamaized.net/stream/3144/files/17/07/672975/3144-kZT4LWMQw6Rh7Kpd.ism/dash/",
"protocol": "http_dash_segments",
"video_ext": "mp4",
"audio_ext": "none",
"vbr": 698,
}, {
"format_id": "video=1493000",
"manifest_url": "https://sdn-global-streaming-cache-3qsdn.akamaized.net/stream/3144/files/17/07/672975/3144-kZT4LWMQw6Rh7Kpd.ism/manifest.mpd",
"ext": "mp4",
"width": 1126,
"height": 480,
"tbr": 1493,
"format_note": "DASH video",
"container": "mp4_dash",
"vcodec": "avc1.4D401F",
"acodec": "none",
"url": "https://sdn-global-streaming-cache-3qsdn.akamaized.net/stream/3144/files/17/07/672975/3144-kZT4LWMQw6Rh7Kpd.ism/manifest.mpd",
"fragment_base_url": "https://sdn-global-streaming-cache-3qsdn.akamaized.net/stream/3144/files/17/07/672975/3144-kZT4LWMQw6Rh7Kpd.ism/dash/",
"protocol": "http_dash_segments",
"video_ext": "mp4",
"audio_ext": "none",
"vbr": 1493,
}, {
"format_id": "video=4482000",
"manifest_url": "https://sdn-global-streaming-cache-3qsdn.akamaized.net/stream/3144/files/17/07/672975/3144-kZT4LWMQw6Rh7Kpd.ism/manifest.mpd",
"ext": "mp4",
"width": 1688,
"height": 720,
"tbr": 4482,
"format_note": "DASH video",
"container": "mp4_dash",
"vcodec": "avc1.4D401F",
"acodec": "none",
"url": "https://sdn-global-streaming-cache-3qsdn.akamaized.net/stream/3144/files/17/07/672975/3144-kZT4LWMQw6Rh7Kpd.ism/manifest.mpd",
"fragment_base_url": "https://sdn-global-streaming-cache-3qsdn.akamaized.net/stream/3144/files/17/07/672975/3144-kZT4LWMQw6Rh7Kpd.ism/dash/",
"protocol": "http_dash_segments",
"video_ext": "mp4",
"audio_ext": "none",
"vbr": 4482,
}],
{
"en": [
{
"ext": "mp4",
"manifest_url": "https://sdn-global-streaming-cache-3qsdn.akamaized.net/stream/3144/files/17/07/672975/3144-kZT4LWMQw6Rh7Kpd.ism/manifest.mpd",
"url": "https://sdn-global-streaming-cache-3qsdn.akamaized.net/stream/3144/files/17/07/672975/3144-kZT4LWMQw6Rh7Kpd.ism/manifest.mpd",
"fragment_base_url": "https://sdn-global-streaming-cache-3qsdn.akamaized.net/stream/3144/files/17/07/672975/3144-kZT4LWMQw6Rh7Kpd.ism/dash/",
"protocol": "http_dash_segments",
}
]
},
) )
] ]
for mpd_file, mpd_url, mpd_base_url, expected_formats in _TEST_CASES: for mpd_file, mpd_url, mpd_base_url, expected_formats, expected_subtitles in _TEST_CASES:
with io.open('./test/testdata/mpd/%s.mpd' % mpd_file, with io.open('./test/testdata/mpd/%s.mpd' % mpd_file,
mode='r', encoding='utf-8') as f: mode='r', encoding='utf-8') as f:
formats = self.ie._parse_mpd_formats( formats, subtitles = self.ie._parse_mpd_formats_and_subtitles(
compat_etree_fromstring(f.read().encode('utf-8')), compat_etree_fromstring(f.read().encode('utf-8')),
mpd_base_url=mpd_base_url, mpd_url=mpd_url) mpd_base_url=mpd_base_url, mpd_url=mpd_url)
self.ie._sort_formats(formats) self.ie._sort_formats(formats)
expect_value(self, formats, expected_formats, None) expect_value(self, formats, expected_formats, None)
expect_value(self, subtitles, expected_subtitles, None)
def test_parse_ism_formats(self):
_TEST_CASES = [
(
'sintel',
'https://sdn-global-streaming-cache-3qsdn.akamaized.net/stream/3144/files/17/07/672975/3144-kZT4LWMQw6Rh7Kpd.ism/Manifest',
[{
"format_id": "audio-128",
"url": "https://sdn-global-streaming-cache-3qsdn.akamaized.net/stream/3144/files/17/07/672975/3144-kZT4LWMQw6Rh7Kpd.ism/Manifest",
"manifest_url": "https://sdn-global-streaming-cache-3qsdn.akamaized.net/stream/3144/files/17/07/672975/3144-kZT4LWMQw6Rh7Kpd.ism/Manifest",
"ext": "isma",
"tbr": 128,
"asr": 48000,
"vcodec": "none",
"acodec": "AACL",
"protocol": "ism",
"_download_params": {
"stream_type": "audio",
"duration": 8880746666,
"timescale": 10000000,
"width": 0,
"height": 0,
"fourcc": "AACL",
"codec_private_data": "1190",
"sampling_rate": 48000,
"channels": 2,
"bits_per_sample": 16,
"nal_unit_length_field": 4
},
"audio_ext": "isma",
"video_ext": "none",
"abr": 128,
}, {
"format_id": "video-100",
"url": "https://sdn-global-streaming-cache-3qsdn.akamaized.net/stream/3144/files/17/07/672975/3144-kZT4LWMQw6Rh7Kpd.ism/Manifest",
"manifest_url": "https://sdn-global-streaming-cache-3qsdn.akamaized.net/stream/3144/files/17/07/672975/3144-kZT4LWMQw6Rh7Kpd.ism/Manifest",
"ext": "ismv",
"width": 336,
"height": 144,
"tbr": 100,
"vcodec": "AVC1",
"acodec": "none",
"protocol": "ism",
"_download_params": {
"stream_type": "video",
"duration": 8880746666,
"timescale": 10000000,
"width": 336,
"height": 144,
"fourcc": "AVC1",
"codec_private_data": "00000001674D401FDA0544EFFC2D002CBC40000003004000000C03C60CA80000000168EF32C8",
"channels": 2,
"bits_per_sample": 16,
"nal_unit_length_field": 4
},
"video_ext": "ismv",
"audio_ext": "none",
"vbr": 100,
}, {
"format_id": "video-326",
"url": "https://sdn-global-streaming-cache-3qsdn.akamaized.net/stream/3144/files/17/07/672975/3144-kZT4LWMQw6Rh7Kpd.ism/Manifest",
"manifest_url": "https://sdn-global-streaming-cache-3qsdn.akamaized.net/stream/3144/files/17/07/672975/3144-kZT4LWMQw6Rh7Kpd.ism/Manifest",
"ext": "ismv",
"width": 562,
"height": 240,
"tbr": 326,
"vcodec": "AVC1",
"acodec": "none",
"protocol": "ism",
"_download_params": {
"stream_type": "video",
"duration": 8880746666,
"timescale": 10000000,
"width": 562,
"height": 240,
"fourcc": "AVC1",
"codec_private_data": "00000001674D401FDA0241FE23FFC3BC83BA44000003000400000300C03C60CA800000000168EF32C8",
"channels": 2,
"bits_per_sample": 16,
"nal_unit_length_field": 4
},
"video_ext": "ismv",
"audio_ext": "none",
"vbr": 326,
}, {
"format_id": "video-698",
"url": "https://sdn-global-streaming-cache-3qsdn.akamaized.net/stream/3144/files/17/07/672975/3144-kZT4LWMQw6Rh7Kpd.ism/Manifest",
"manifest_url": "https://sdn-global-streaming-cache-3qsdn.akamaized.net/stream/3144/files/17/07/672975/3144-kZT4LWMQw6Rh7Kpd.ism/Manifest",
"ext": "ismv",
"width": 844,
"height": 360,
"tbr": 698,
"vcodec": "AVC1",
"acodec": "none",
"protocol": "ism",
"_download_params": {
"stream_type": "video",
"duration": 8880746666,
"timescale": 10000000,
"width": 844,
"height": 360,
"fourcc": "AVC1",
"codec_private_data": "00000001674D401FDA0350BFB97FF06AF06AD1000003000100000300300F1832A00000000168EF32C8",
"channels": 2,
"bits_per_sample": 16,
"nal_unit_length_field": 4
},
"video_ext": "ismv",
"audio_ext": "none",
"vbr": 698,
}, {
"format_id": "video-1493",
"url": "https://sdn-global-streaming-cache-3qsdn.akamaized.net/stream/3144/files/17/07/672975/3144-kZT4LWMQw6Rh7Kpd.ism/Manifest",
"manifest_url": "https://sdn-global-streaming-cache-3qsdn.akamaized.net/stream/3144/files/17/07/672975/3144-kZT4LWMQw6Rh7Kpd.ism/Manifest",
"ext": "ismv",
"width": 1126,
"height": 480,
"tbr": 1493,
"vcodec": "AVC1",
"acodec": "none",
"protocol": "ism",
"_download_params": {
"stream_type": "video",
"duration": 8880746666,
"timescale": 10000000,
"width": 1126,
"height": 480,
"fourcc": "AVC1",
"codec_private_data": "00000001674D401FDA011C3DE6FFF0D890D871000003000100000300300F1832A00000000168EF32C8",
"channels": 2,
"bits_per_sample": 16,
"nal_unit_length_field": 4
},
"video_ext": "ismv",
"audio_ext": "none",
"vbr": 1493,
}, {
"format_id": "video-4482",
"url": "https://sdn-global-streaming-cache-3qsdn.akamaized.net/stream/3144/files/17/07/672975/3144-kZT4LWMQw6Rh7Kpd.ism/Manifest",
"manifest_url": "https://sdn-global-streaming-cache-3qsdn.akamaized.net/stream/3144/files/17/07/672975/3144-kZT4LWMQw6Rh7Kpd.ism/Manifest",
"ext": "ismv",
"width": 1688,
"height": 720,
"tbr": 4482,
"vcodec": "AVC1",
"acodec": "none",
"protocol": "ism",
"_download_params": {
"stream_type": "video",
"duration": 8880746666,
"timescale": 10000000,
"width": 1688,
"height": 720,
"fourcc": "AVC1",
"codec_private_data": "00000001674D401FDA01A816F97FFC1ABC1AB440000003004000000C03C60CA80000000168EF32C8",
"channels": 2,
"bits_per_sample": 16,
"nal_unit_length_field": 4
},
"video_ext": "ismv",
"audio_ext": "none",
"vbr": 4482,
}],
{
"eng": [
{
"ext": "ismt",
"protocol": "ism",
"url": "https://sdn-global-streaming-cache-3qsdn.akamaized.net/stream/3144/files/17/07/672975/3144-kZT4LWMQw6Rh7Kpd.ism/Manifest",
"manifest_url": "https://sdn-global-streaming-cache-3qsdn.akamaized.net/stream/3144/files/17/07/672975/3144-kZT4LWMQw6Rh7Kpd.ism/Manifest",
"_download_params": {
"stream_type": "text",
"duration": 8880746666,
"timescale": 10000000,
"fourcc": "TTML",
"codec_private_data": ""
}
}
]
},
),
]
for ism_file, ism_url, expected_formats, expected_subtitles in _TEST_CASES:
with io.open('./test/testdata/ism/%s.Manifest' % ism_file,
mode='r', encoding='utf-8') as f:
formats, subtitles = self.ie._parse_ism_formats_and_subtitles(
compat_etree_fromstring(f.read().encode('utf-8')), ism_url=ism_url)
self.ie._sort_formats(formats)
expect_value(self, formats, expected_formats, None)
expect_value(self, subtitles, expected_subtitles, None)
def test_parse_f4m_formats(self): def test_parse_f4m_formats(self):
_TEST_CASES = [ _TEST_CASES = [
+30 -1
View File
@@ -311,8 +311,8 @@ class TestFormatSelection(unittest.TestCase):
self.assertRaises(ExtractorError, ydl.process_ie_result, info_dict.copy()) self.assertRaises(ExtractorError, ydl.process_ie_result, info_dict.copy())
def test_youtube_format_selection(self): def test_youtube_format_selection(self):
# FIXME: Rewrite in accordance with the new format sorting options
return return
# disabled for now - this needs some changes
order = [ order = [
'38', '37', '46', '22', '45', '35', '44', '18', '34', '43', '6', '5', '17', '36', '13', '38', '37', '46', '22', '45', '35', '44', '18', '34', '43', '6', '5', '17', '36', '13',
@@ -601,6 +601,26 @@ class TestYoutubeDL(unittest.TestCase):
self.assertTrue(subs) self.assertTrue(subs)
self.assertEqual(set(subs.keys()), set(['es', 'fr'])) self.assertEqual(set(subs.keys()), set(['es', 'fr']))
result = get_info({'writesubtitles': True, 'subtitleslangs': ['all', '-en']})
subs = result['requested_subtitles']
self.assertTrue(subs)
self.assertEqual(set(subs.keys()), set(['es', 'fr']))
result = get_info({'writesubtitles': True, 'subtitleslangs': ['en', 'fr', '-en']})
subs = result['requested_subtitles']
self.assertTrue(subs)
self.assertEqual(set(subs.keys()), set(['fr']))
result = get_info({'writesubtitles': True, 'subtitleslangs': ['-en', 'en']})
subs = result['requested_subtitles']
self.assertTrue(subs)
self.assertEqual(set(subs.keys()), set(['en']))
result = get_info({'writesubtitles': True, 'subtitleslangs': ['e.+']})
subs = result['requested_subtitles']
self.assertTrue(subs)
self.assertEqual(set(subs.keys()), set(['es', 'en']))
result = get_info({'writesubtitles': True, 'writeautomaticsub': True, 'subtitleslangs': ['es', 'pt']}) result = get_info({'writesubtitles': True, 'writeautomaticsub': True, 'subtitleslangs': ['es', 'pt']})
subs = result['requested_subtitles'] subs = result['requested_subtitles']
self.assertTrue(subs) self.assertTrue(subs)
@@ -635,6 +655,8 @@ class TestYoutubeDL(unittest.TestCase):
'height': 1080, 'height': 1080,
'title1': '$PATH', 'title1': '$PATH',
'title2': '%PATH%', 'title2': '%PATH%',
'timestamp': 1618488000,
'formats': [{'id': 'id1'}, {'id': 'id2'}]
} }
def fname(templ, na_placeholder='NA'): def fname(templ, na_placeholder='NA'):
@@ -651,6 +673,7 @@ class TestYoutubeDL(unittest.TestCase):
# Or by provided placeholder # Or by provided placeholder
self.assertEqual(fname(NA_TEST_OUTTMPL, na_placeholder='none'), 'none-none-1234.mp4') self.assertEqual(fname(NA_TEST_OUTTMPL, na_placeholder='none'), 'none-none-1234.mp4')
self.assertEqual(fname(NA_TEST_OUTTMPL, na_placeholder=''), '--1234.mp4') self.assertEqual(fname(NA_TEST_OUTTMPL, na_placeholder=''), '--1234.mp4')
self.assertEqual(fname('%(height)s.%(ext)s'), '1080.mp4')
self.assertEqual(fname('%(height)d.%(ext)s'), '1080.mp4') self.assertEqual(fname('%(height)d.%(ext)s'), '1080.mp4')
self.assertEqual(fname('%(height)6d.%(ext)s'), ' 1080.mp4') self.assertEqual(fname('%(height)6d.%(ext)s'), ' 1080.mp4')
self.assertEqual(fname('%(height)-6d.%(ext)s'), '1080 .mp4') self.assertEqual(fname('%(height)-6d.%(ext)s'), '1080 .mp4')
@@ -668,6 +691,12 @@ class TestYoutubeDL(unittest.TestCase):
self.assertEqual(fname('%%(width)06d.%(ext)s'), '%(width)06d.mp4') self.assertEqual(fname('%%(width)06d.%(ext)s'), '%(width)06d.mp4')
self.assertEqual(fname('Hello %(title1)s'), 'Hello $PATH') self.assertEqual(fname('Hello %(title1)s'), 'Hello $PATH')
self.assertEqual(fname('Hello %(title2)s'), 'Hello %PATH%') self.assertEqual(fname('Hello %(title2)s'), 'Hello %PATH%')
self.assertEqual(fname('%(timestamp+-1000>%H-%M-%S)s'), '11-43-20')
self.assertEqual(fname('%(id+1)05d'), '01235')
self.assertEqual(fname('%(width+100)05d'), 'NA')
self.assertEqual(fname('%(formats.0)s').replace("u", ""), "{'id' - 'id1'}")
self.assertEqual(fname('%(formats.-1.id)s'), 'id2')
self.assertEqual(fname('%(formats.2)s'), 'NA')
def test_format_note(self): def test_format_note(self):
ydl = YoutubeDL() ydl = YoutubeDL()
-9
View File
@@ -72,15 +72,6 @@ class TestAllURLsMatching(unittest.TestCase):
self.assertMatch('http://www.youtube.com/results?search_query=making+mustard', ['youtube:search_url']) self.assertMatch('http://www.youtube.com/results?search_query=making+mustard', ['youtube:search_url'])
self.assertMatch('https://www.youtube.com/results?baz=bar&search_query=youtube-dl+test+video&filters=video&lclk=video', ['youtube:search_url']) self.assertMatch('https://www.youtube.com/results?baz=bar&search_query=youtube-dl+test+video&filters=video&lclk=video', ['youtube:search_url'])
def test_youtube_extract(self):
assertExtractId = lambda url, id: self.assertEqual(YoutubeIE.extract_id(url), id)
assertExtractId('http://www.youtube.com/watch?&v=BaW_jenozKc', 'BaW_jenozKc')
assertExtractId('https://www.youtube.com/watch?&v=BaW_jenozKc', 'BaW_jenozKc')
assertExtractId('https://www.youtube.com/watch?feature=player_embedded&v=BaW_jenozKc', 'BaW_jenozKc')
assertExtractId('https://www.youtube.com/watch_popup?v=BaW_jenozKc', 'BaW_jenozKc')
assertExtractId('http://www.youtube.com/watch?v=BaW_jenozKcsharePLED17F32AD9753930', 'BaW_jenozKc')
assertExtractId('BaW_jenozKc', 'BaW_jenozKc')
def test_facebook_matching(self): def test_facebook_matching(self):
self.assertTrue(FacebookIE.suitable('https://www.facebook.com/Shiniknoh#!/photo.php?v=10153317450565268')) self.assertTrue(FacebookIE.suitable('https://www.facebook.com/Shiniknoh#!/photo.php?v=10153317450565268'))
self.assertTrue(FacebookIE.suitable('https://www.facebook.com/cindyweather?fref=ts#!/photo.php?v=10152183998945793')) self.assertTrue(FacebookIE.suitable('https://www.facebook.com/cindyweather?fref=ts#!/photo.php?v=10152183998945793'))
+10
View File
@@ -39,6 +39,16 @@ class TestExecution(unittest.TestCase):
_, stderr = p.communicate() _, stderr = p.communicate()
self.assertFalse(stderr) self.assertFalse(stderr)
def test_lazy_extractors(self):
try:
subprocess.check_call([sys.executable, 'devscripts/make_lazy_extractors.py', 'yt_dlp/extractor/lazy_extractors.py'], cwd=rootDir, stdout=_DEV_NULL)
subprocess.check_call([sys.executable, 'test/test_all_urls.py'], cwd=rootDir, stdout=_DEV_NULL)
finally:
try:
os.remove('yt_dlp/extractor/lazy_extractors.py')
except (IOError, OSError):
pass
if __name__ == '__main__': if __name__ == '__main__':
unittest.main() unittest.main()
+9
View File
@@ -16,6 +16,15 @@ class TestMetadataFromField(unittest.TestCase):
pp = MetadataFromFieldPP(None, ['title:%(title)s - %(artist)s']) pp = MetadataFromFieldPP(None, ['title:%(title)s - %(artist)s'])
self.assertEqual(pp._data[0]['regex'], r'(?P<title>.+)\ \-\ (?P<artist>.+)') self.assertEqual(pp._data[0]['regex'], r'(?P<title>.+)\ \-\ (?P<artist>.+)')
def test_field_to_outtmpl(self):
pp = MetadataFromFieldPP(None, ['title:%(title)s : %(artist)s'])
self.assertEqual(pp._data[0]['tmpl'], '%(title)s')
def test_in_out_seperation(self):
pp = MetadataFromFieldPP(None, ['%(title)s \\: %(artist)s:%(title)s : %(artist)s'])
self.assertEqual(pp._data[0]['in'], '%(title)s : %(artist)s')
self.assertEqual(pp._data[0]['out'], '%(title)s : %(artist)s')
class TestMetadataFromTitle(unittest.TestCase): class TestMetadataFromTitle(unittest.TestCase):
def test_format_to_regex(self): def test_format_to_regex(self):
+26
View File
@@ -0,0 +1,26 @@
#!/usr/bin/env python
from __future__ import unicode_literals
# Allow direct execution
import os
import sys
import unittest
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from yt_dlp.extractor import YoutubeIE
class TestYoutubeMisc(unittest.TestCase):
def test_youtube_extract(self):
assertExtractId = lambda url, id: self.assertEqual(YoutubeIE.extract_id(url), id)
assertExtractId('http://www.youtube.com/watch?&v=BaW_jenozKc', 'BaW_jenozKc')
assertExtractId('https://www.youtube.com/watch?&v=BaW_jenozKc', 'BaW_jenozKc')
assertExtractId('https://www.youtube.com/watch?feature=player_embedded&v=BaW_jenozKc', 'BaW_jenozKc')
assertExtractId('https://www.youtube.com/watch_popup?v=BaW_jenozKc', 'BaW_jenozKc')
assertExtractId('http://www.youtube.com/watch?v=BaW_jenozKcsharePLED17F32AD9753930', 'BaW_jenozKc')
assertExtractId('BaW_jenozKc', 'BaW_jenozKc')
if __name__ == '__main__':
unittest.main()
+988
View File
@@ -0,0 +1,988 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Created with Unified Streaming Platform (version=1.10.18-20255) -->
<SmoothStreamingMedia
MajorVersion="2"
MinorVersion="0"
TimeScale="10000000"
Duration="8880746666">
<StreamIndex
Type="audio"
QualityLevels="1"
TimeScale="10000000"
Name="audio"
Chunks="445"
Url="QualityLevels({bitrate})/Fragments(audio={start time})">
<QualityLevel
Index="0"
Bitrate="128001"
CodecPrivateData="1190"
SamplingRate="48000"
Channels="2"
BitsPerSample="16"
PacketSize="4"
AudioTag="255"
FourCC="AACL" />
<c t="0" d="20053333" />
<c d="20053333" />
<c d="20053334" />
<c d="19840000" />
<c d="20053333" />
<c d="20053333" />
<c d="20053334" />
<c d="19840000" />
<c d="20053333" />
<c d="20053333" />
<c d="20053334" />
<c d="19840000" />
<c d="20053333" />
<c d="20053333" />
<c d="20053334" />
<c d="19840000" />
<c d="20053333" />
<c d="20053333" />
<c d="20053334" />
<c d="19840000" />
<c d="20053333" />
<c d="20053333" />
<c d="20053334" />
<c d="19840000" />
<c d="20053333" />
<c d="20053333" />
<c d="20053334" />
<c d="19840000" />
<c d="20053333" />
<c d="20053333" />
<c d="20053334" />
<c d="19840000" />
<c d="20053333" />
<c d="20053333" />
<c d="20053334" />
<c d="19840000" />
<c d="20053333" />
<c d="20053333" />
<c d="20053334" />
<c d="19840000" />
<c d="20053333" />
<c d="20053333" />
<c d="20053334" />
<c d="19840000" />
<c d="20053333" />
<c d="20053333" />
<c d="20053334" />
<c d="19840000" />
<c d="20053333" />
<c d="20053333" />
<c d="20053334" />
<c d="19840000" />
<c d="20053333" />
<c d="20053333" />
<c d="20053334" />
<c d="19840000" />
<c d="20053333" />
<c d="20053333" />
<c d="20053334" />
<c d="19840000" />
<c d="20053333" />
<c d="20053333" />
<c d="20053334" />
<c d="19840000" />
<c d="20053333" />
<c d="20053333" />
<c d="20053334" />
<c d="19840000" />
<c d="20053333" />
<c d="20053333" />
<c d="20053334" />
<c d="19840000" />
<c d="20053333" />
<c d="20053333" />
<c d="20053334" />
<c d="19840000" />
<c d="20053333" />
<c d="20053333" />
<c d="20053334" />
<c d="19840000" />
<c d="20053333" />
<c d="20053333" />
<c d="20053334" />
<c d="19840000" />
<c d="20053333" />
<c d="20053333" />
<c d="20053334" />
<c d="19840000" />
<c d="20053333" />
<c d="20053333" />
<c d="20053334" />
<c d="19840000" />
<c d="20053333" />
<c d="20053333" />
<c d="20053334" />
<c d="19840000" />
<c d="20053333" />
<c d="20053333" />
<c d="20053334" />
<c d="19840000" />
<c d="20053333" />
<c d="20053333" />
<c d="20053334" />
<c d="19840000" />
<c d="20053333" />
<c d="20053333" />
<c d="20053334" />
<c d="19840000" />
<c d="20053333" />
<c d="20053333" />
<c d="20053334" />
<c d="19840000" />
<c d="20053333" />
<c d="20053333" />
<c d="20053334" />
<c d="19840000" />
<c d="20053333" />
<c d="20053333" />
<c d="20053334" />
<c d="19840000" />
<c d="20053333" />
<c d="20053333" />
<c d="20053334" />
<c d="19840000" />
<c d="20053333" />
<c d="20053333" />
<c d="20053334" />
<c d="19840000" />
<c d="20053333" />
<c d="20053333" />
<c d="20053334" />
<c d="19840000" />
<c d="20053333" />
<c d="20053333" />
<c d="20053334" />
<c d="19840000" />
<c d="20053333" />
<c d="20053333" />
<c d="20053334" />
<c d="19840000" />
<c d="20053333" />
<c d="20053333" />
<c d="20053334" />
<c d="19840000" />
<c d="20053333" />
<c d="20053333" />
<c d="20053334" />
<c d="19840000" />
<c d="20053333" />
<c d="20053333" />
<c d="20053334" />
<c d="19840000" />
<c d="20053333" />
<c d="20053333" />
<c d="20053334" />
<c d="19840000" />
<c d="20053333" />
<c d="20053333" />
<c d="20053334" />
<c d="19840000" />
<c d="20053333" />
<c d="20053333" />
<c d="20053334" />
<c d="19840000" />
<c d="20053333" />
<c d="20053333" />
<c d="20053334" />
<c d="19840000" />
<c d="20053333" />
<c d="20053333" />
<c d="20053334" />
<c d="19840000" />
<c d="20053333" />
<c d="20053333" />
<c d="20053334" />
<c d="19840000" />
<c d="20053333" />
<c d="20053333" />
<c d="20053334" />
<c d="19840000" />
<c d="20053333" />
<c d="20053333" />
<c d="20053334" />
<c d="19840000" />
<c d="20053333" />
<c d="20053333" />
<c d="20053334" />
<c d="19840000" />
<c d="20053333" />
<c d="20053333" />
<c d="20053334" />
<c d="19840000" />
<c d="20053333" />
<c d="20053333" />
<c d="20053334" />
<c d="19840000" />
<c d="20053333" />
<c d="20053333" />
<c d="20053334" />
<c d="19840000" />
<c d="20053333" />
<c d="20053333" />
<c d="20053334" />
<c d="19840000" />
<c d="20053333" />
<c d="20053333" />
<c d="20053334" />
<c d="19840000" />
<c d="20053333" />
<c d="20053333" />
<c d="20053334" />
<c d="19840000" />
<c d="20053333" />
<c d="20053333" />
<c d="20053334" />
<c d="19840000" />
<c d="20053333" />
<c d="20053333" />
<c d="20053334" />
<c d="19840000" />
<c d="20053333" />
<c d="20053333" />
<c d="20053334" />
<c d="19840000" />
<c d="20053333" />
<c d="20053333" />
<c d="20053334" />
<c d="19840000" />
<c d="20053333" />
<c d="20053333" />
<c d="20053334" />
<c d="19840000" />
<c d="20053333" />
<c d="20053333" />
<c d="20053334" />
<c d="19840000" />
<c d="20053333" />
<c d="20053333" />
<c d="20053334" />
<c d="19840000" />
<c d="20053333" />
<c d="20053333" />
<c d="20053334" />
<c d="19840000" />
<c d="20053333" />
<c d="20053333" />
<c d="20053334" />
<c d="19840000" />
<c d="20053333" />
<c d="20053333" />
<c d="20053334" />
<c d="19840000" />
<c d="20053333" />
<c d="20053333" />
<c d="20053334" />
<c d="19840000" />
<c d="20053333" />
<c d="20053333" />
<c d="20053334" />
<c d="19840000" />
<c d="20053333" />
<c d="20053333" />
<c d="20053334" />
<c d="19840000" />
<c d="20053333" />
<c d="20053333" />
<c d="20053334" />
<c d="19840000" />
<c d="20053333" />
<c d="20053333" />
<c d="20053334" />
<c d="19840000" />
<c d="20053333" />
<c d="20053333" />
<c d="20053334" />
<c d="19840000" />
<c d="20053333" />
<c d="20053333" />
<c d="20053334" />
<c d="19840000" />
<c d="20053333" />
<c d="20053333" />
<c d="20053334" />
<c d="19840000" />
<c d="20053333" />
<c d="20053333" />
<c d="20053334" />
<c d="19840000" />
<c d="20053333" />
<c d="20053333" />
<c d="20053334" />
<c d="19840000" />
<c d="20053333" />
<c d="20053333" />
<c d="20053334" />
<c d="19840000" />
<c d="20053333" />
<c d="20053333" />
<c d="20053334" />
<c d="19840000" />
<c d="20053333" />
<c d="20053333" />
<c d="20053334" />
<c d="19840000" />
<c d="20053333" />
<c d="20053333" />
<c d="20053334" />
<c d="19840000" />
<c d="20053333" />
<c d="20053333" />
<c d="20053334" />
<c d="19840000" />
<c d="20053333" />
<c d="20053333" />
<c d="20053334" />
<c d="19840000" />
<c d="20053333" />
<c d="20053333" />
<c d="20053334" />
<c d="19840000" />
<c d="20053333" />
<c d="20053333" />
<c d="20053334" />
<c d="19840000" />
<c d="20053333" />
<c d="20053333" />
<c d="20053334" />
<c d="19840000" />
<c d="20053333" />
<c d="20053333" />
<c d="20053334" />
<c d="19840000" />
<c d="20053333" />
<c d="20053333" />
<c d="20053334" />
<c d="19840000" />
<c d="20053333" />
<c d="20053333" />
<c d="20053334" />
<c d="19840000" />
<c d="20053333" />
<c d="20053333" />
<c d="20053334" />
<c d="19840000" />
<c d="20053333" />
<c d="20053333" />
<c d="20053334" />
<c d="19840000" />
<c d="20053333" />
<c d="20053333" />
<c d="20053334" />
<c d="19840000" />
<c d="20053333" />
<c d="20053333" />
<c d="20053334" />
<c d="19840000" />
<c d="20053333" />
<c d="20053333" />
<c d="20053334" />
<c d="19840000" />
<c d="20053333" />
<c d="20053333" />
<c d="20053334" />
<c d="19840000" />
<c d="20053333" />
<c d="20053333" />
<c d="20053334" />
<c d="19840000" />
<c d="20053333" />
<c d="20053333" />
<c d="20053334" />
<c d="19840000" />
<c d="20053333" />
<c d="20053333" />
<c d="20053334" />
<c d="19840000" />
<c d="20053333" />
<c d="20053333" />
<c d="20053334" />
<c d="19840000" />
<c d="20053333" />
<c d="20053333" />
<c d="20053334" />
<c d="19840000" />
<c d="20053333" />
<c d="20053333" />
<c d="20053334" />
<c d="19840000" />
<c d="20053333" />
<c d="20053333" />
<c d="20053334" />
<c d="19840000" />
<c d="20053333" />
<c d="20053333" />
<c d="20053334" />
<c d="19840000" />
<c d="20053333" />
<c d="20053333" />
<c d="20053334" />
<c d="19840000" />
<c d="20053333" />
<c d="20053333" />
<c d="20053334" />
<c d="19840000" />
<c d="20053333" />
<c d="20053333" />
<c d="20053334" />
<c d="19840000" />
<c d="20053333" />
<c d="20053333" />
<c d="20053334" />
<c d="19840000" />
<c d="20053333" />
<c d="20053333" />
<c d="20053334" />
<c d="19840000" />
<c d="20053333" />
<c d="20053333" />
<c d="20053334" />
<c d="19840000" />
<c d="20053333" />
<c d="20053333" />
<c d="20053334" />
<c d="19840000" />
<c d="20053333" />
<c d="20053333" />
<c d="20053334" />
<c d="19840000" />
<c d="20053333" />
<c d="20053333" />
<c d="20053334" />
<c d="19840000" />
<c d="20053333" />
<c d="20053333" />
<c d="20053334" />
<c d="19840000" />
<c d="20053333" />
<c d="20053333" />
<c d="20053334" />
<c d="19840000" />
<c d="20053333" />
<c d="20053333" />
<c d="20053334" />
<c d="19840000" />
<c d="746666" />
</StreamIndex>
<StreamIndex
Type="text"
QualityLevels="1"
TimeScale="10000000"
Language="eng"
Subtype="CAPT"
Name="textstream_eng"
Chunks="11"
Url="QualityLevels({bitrate})/Fragments(textstream_eng={start time})">
<QualityLevel
Index="0"
Bitrate="1000"
CodecPrivateData=""
FourCC="TTML" />
<c t="0" d="600000000" />
<c d="600000000" />
<c d="600000000" />
<c d="600000000" />
<c d="600000000" />
<c d="600000000" />
<c d="600000000" />
<c d="600000000" />
<c d="600000000" />
<c d="600000000" />
<c d="240000000" />
</StreamIndex>
<StreamIndex
Type="video"
QualityLevels="5"
TimeScale="10000000"
Name="video"
Chunks="444"
Url="QualityLevels({bitrate})/Fragments(video={start time})"
MaxWidth="1688"
MaxHeight="720"
DisplayWidth="1689"
DisplayHeight="720">
<QualityLevel
Index="0"
Bitrate="100000"
CodecPrivateData="00000001674D401FDA0544EFFC2D002CBC40000003004000000C03C60CA80000000168EF32C8"
MaxWidth="336"
MaxHeight="144"
FourCC="AVC1" />
<QualityLevel
Index="1"
Bitrate="326000"
CodecPrivateData="00000001674D401FDA0241FE23FFC3BC83BA44000003000400000300C03C60CA800000000168EF32C8"
MaxWidth="562"
MaxHeight="240"
FourCC="AVC1" />
<QualityLevel
Index="2"
Bitrate="698000"
CodecPrivateData="00000001674D401FDA0350BFB97FF06AF06AD1000003000100000300300F1832A00000000168EF32C8"
MaxWidth="844"
MaxHeight="360"
FourCC="AVC1" />
<QualityLevel
Index="3"
Bitrate="1493000"
CodecPrivateData="00000001674D401FDA011C3DE6FFF0D890D871000003000100000300300F1832A00000000168EF32C8"
MaxWidth="1126"
MaxHeight="480"
FourCC="AVC1" />
<QualityLevel
Index="4"
Bitrate="4482000"
CodecPrivateData="00000001674D401FDA01A816F97FFC1ABC1AB440000003004000000C03C60CA80000000168EF32C8"
MaxWidth="1688"
MaxHeight="720"
FourCC="AVC1" />
<c t="0" d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
<c d="20000000" />
</StreamIndex>
</SmoothStreamingMedia>
+38
View File
@@ -0,0 +1,38 @@
#EXTM3U
#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="bipbop_audio",LANGUAGE="eng",NAME="BipBop Audio 1",AUTOSELECT=YES,DEFAULT=YES
#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="bipbop_audio",LANGUAGE="eng",NAME="BipBop Audio 2",AUTOSELECT=NO,DEFAULT=NO,URI="alternate_audio_aac/prog_index.m3u8"
#EXT-X-MEDIA:TYPE=SUBTITLES,GROUP-ID="subs",NAME="English",DEFAULT=YES,AUTOSELECT=YES,FORCED=NO,LANGUAGE="en",CHARACTERISTICS="public.accessibility.transcribes-spoken-dialog, public.accessibility.describes-music-and-sound",URI="subtitles/eng/prog_index.m3u8"
#EXT-X-MEDIA:TYPE=SUBTITLES,GROUP-ID="subs",NAME="English (Forced)",DEFAULT=NO,AUTOSELECT=NO,FORCED=YES,LANGUAGE="en",URI="subtitles/eng_forced/prog_index.m3u8"
#EXT-X-MEDIA:TYPE=SUBTITLES,GROUP-ID="subs",NAME="Français",DEFAULT=NO,AUTOSELECT=YES,FORCED=NO,LANGUAGE="fr",CHARACTERISTICS="public.accessibility.transcribes-spoken-dialog, public.accessibility.describes-music-and-sound",URI="subtitles/fra/prog_index.m3u8"
#EXT-X-MEDIA:TYPE=SUBTITLES,GROUP-ID="subs",NAME="Français (Forced)",DEFAULT=NO,AUTOSELECT=NO,FORCED=YES,LANGUAGE="fr",URI="subtitles/fra_forced/prog_index.m3u8"
#EXT-X-MEDIA:TYPE=SUBTITLES,GROUP-ID="subs",NAME="Español",DEFAULT=NO,AUTOSELECT=YES,FORCED=NO,LANGUAGE="es",CHARACTERISTICS="public.accessibility.transcribes-spoken-dialog, public.accessibility.describes-music-and-sound",URI="subtitles/spa/prog_index.m3u8"
#EXT-X-MEDIA:TYPE=SUBTITLES,GROUP-ID="subs",NAME="Español (Forced)",DEFAULT=NO,AUTOSELECT=NO,FORCED=YES,LANGUAGE="es",URI="subtitles/spa_forced/prog_index.m3u8"
#EXT-X-MEDIA:TYPE=SUBTITLES,GROUP-ID="subs",NAME="日本語",DEFAULT=NO,AUTOSELECT=YES,FORCED=NO,LANGUAGE="ja",CHARACTERISTICS="public.accessibility.transcribes-spoken-dialog, public.accessibility.describes-music-and-sound",URI="subtitles/jpn/prog_index.m3u8"
#EXT-X-MEDIA:TYPE=SUBTITLES,GROUP-ID="subs",NAME="日本語 (Forced)",DEFAULT=NO,AUTOSELECT=NO,FORCED=YES,LANGUAGE="ja",URI="subtitles/jpn_forced/prog_index.m3u8"
#EXT-X-STREAM-INF:BANDWIDTH=263851,CODECS="mp4a.40.2, avc1.4d400d",RESOLUTION=416x234,AUDIO="bipbop_audio",SUBTITLES="subs"
gear1/prog_index.m3u8
#EXT-X-I-FRAME-STREAM-INF:BANDWIDTH=28451,CODECS="avc1.4d400d",URI="gear1/iframe_index.m3u8"
#EXT-X-STREAM-INF:BANDWIDTH=577610,CODECS="mp4a.40.2, avc1.4d401e",RESOLUTION=640x360,AUDIO="bipbop_audio",SUBTITLES="subs"
gear2/prog_index.m3u8
#EXT-X-I-FRAME-STREAM-INF:BANDWIDTH=181534,CODECS="avc1.4d401e",URI="gear2/iframe_index.m3u8"
#EXT-X-STREAM-INF:BANDWIDTH=915905,CODECS="mp4a.40.2, avc1.4d401f",RESOLUTION=960x540,AUDIO="bipbop_audio",SUBTITLES="subs"
gear3/prog_index.m3u8
#EXT-X-I-FRAME-STREAM-INF:BANDWIDTH=297056,CODECS="avc1.4d401f",URI="gear3/iframe_index.m3u8"
#EXT-X-STREAM-INF:BANDWIDTH=1030138,CODECS="mp4a.40.2, avc1.4d401f",RESOLUTION=1280x720,AUDIO="bipbop_audio",SUBTITLES="subs"
gear4/prog_index.m3u8
#EXT-X-I-FRAME-STREAM-INF:BANDWIDTH=339492,CODECS="avc1.4d401f",URI="gear4/iframe_index.m3u8"
#EXT-X-STREAM-INF:BANDWIDTH=1924009,CODECS="mp4a.40.2, avc1.4d401f",RESOLUTION=1920x1080,AUDIO="bipbop_audio",SUBTITLES="subs"
gear5/prog_index.m3u8
#EXT-X-I-FRAME-STREAM-INF:BANDWIDTH=669554,CODECS="avc1.4d401f",URI="gear5/iframe_index.m3u8"
#EXT-X-STREAM-INF:BANDWIDTH=41457,CODECS="mp4a.40.2",AUDIO="bipbop_audio",SUBTITLES="subs"
gear0/prog_index.m3u8
+351
View File
@@ -0,0 +1,351 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Created with Unified Streaming Platform (version=1.10.18-20255) -->
<MPD
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="urn:mpeg:dash:schema:mpd:2011"
xsi:schemaLocation="urn:mpeg:dash:schema:mpd:2011 http://standards.iso.org/ittf/PubliclyAvailableStandards/MPEG-DASH_schema_files/DASH-MPD.xsd"
type="static"
mediaPresentationDuration="PT14M48S"
maxSegmentDuration="PT1M"
minBufferTime="PT10S"
profiles="urn:mpeg:dash:profile:isoff-live:2011">
<Period
id="1"
duration="PT14M48S">
<BaseURL>dash/</BaseURL>
<AdaptationSet
id="1"
group="1"
contentType="audio"
segmentAlignment="true"
audioSamplingRate="48000"
mimeType="audio/mp4"
codecs="mp4a.40.2"
startWithSAP="1">
<AudioChannelConfiguration
schemeIdUri="urn:mpeg:dash:23003:3:audio_channel_configuration:2011"
value="2" />
<Role schemeIdUri="urn:mpeg:dash:role:2011" value="main" />
<SegmentTemplate
timescale="48000"
initialization="3144-kZT4LWMQw6Rh7Kpd-$RepresentationID$.dash"
media="3144-kZT4LWMQw6Rh7Kpd-$RepresentationID$-$Time$.dash">
<SegmentTimeline>
<S t="0" d="96256" r="2" />
<S d="95232" />
<S d="96256" r="2" />
<S d="95232" />
<S d="96256" r="2" />
<S d="95232" />
<S d="96256" r="2" />
<S d="95232" />
<S d="96256" r="2" />
<S d="95232" />
<S d="96256" r="2" />
<S d="95232" />
<S d="96256" r="2" />
<S d="95232" />
<S d="96256" r="2" />
<S d="95232" />
<S d="96256" r="2" />
<S d="95232" />
<S d="96256" r="2" />
<S d="95232" />
<S d="96256" r="2" />
<S d="95232" />
<S d="96256" r="2" />
<S d="95232" />
<S d="96256" r="2" />
<S d="95232" />
<S d="96256" r="2" />
<S d="95232" />
<S d="96256" r="2" />
<S d="95232" />
<S d="96256" r="2" />
<S d="95232" />
<S d="96256" r="2" />
<S d="95232" />
<S d="96256" r="2" />
<S d="95232" />
<S d="96256" r="2" />
<S d="95232" />
<S d="96256" r="2" />
<S d="95232" />
<S d="96256" r="2" />
<S d="95232" />
<S d="96256" r="2" />
<S d="95232" />
<S d="96256" r="2" />
<S d="95232" />
<S d="96256" r="2" />
<S d="95232" />
<S d="96256" r="2" />
<S d="95232" />
<S d="96256" r="2" />
<S d="95232" />
<S d="96256" r="2" />
<S d="95232" />
<S d="96256" r="2" />
<S d="95232" />
<S d="96256" r="2" />
<S d="95232" />
<S d="96256" r="2" />
<S d="95232" />
<S d="96256" r="2" />
<S d="95232" />
<S d="96256" r="2" />
<S d="95232" />
<S d="96256" r="2" />
<S d="95232" />
<S d="96256" r="2" />
<S d="95232" />
<S d="96256" r="2" />
<S d="95232" />
<S d="96256" r="2" />
<S d="95232" />
<S d="96256" r="2" />
<S d="95232" />
<S d="96256" r="2" />
<S d="95232" />
<S d="96256" r="2" />
<S d="95232" />
<S d="96256" r="2" />
<S d="95232" />
<S d="96256" r="2" />
<S d="95232" />
<S d="96256" r="2" />
<S d="95232" />
<S d="96256" r="2" />
<S d="95232" />
<S d="96256" r="2" />
<S d="95232" />
<S d="96256" r="2" />
<S d="95232" />
<S d="96256" r="2" />
<S d="95232" />
<S d="96256" r="2" />
<S d="95232" />
<S d="96256" r="2" />
<S d="95232" />
<S d="96256" r="2" />
<S d="95232" />
<S d="96256" r="2" />
<S d="95232" />
<S d="96256" r="2" />
<S d="95232" />
<S d="96256" r="2" />
<S d="95232" />
<S d="96256" r="2" />
<S d="95232" />
<S d="96256" r="2" />
<S d="95232" />
<S d="96256" r="2" />
<S d="95232" />
<S d="96256" r="2" />
<S d="95232" />
<S d="96256" r="2" />
<S d="95232" />
<S d="96256" r="2" />
<S d="95232" />
<S d="96256" r="2" />
<S d="95232" />
<S d="96256" r="2" />
<S d="95232" />
<S d="96256" r="2" />
<S d="95232" />
<S d="96256" r="2" />
<S d="95232" />
<S d="96256" r="2" />
<S d="95232" />
<S d="96256" r="2" />
<S d="95232" />
<S d="96256" r="2" />
<S d="95232" />
<S d="96256" r="2" />
<S d="95232" />
<S d="96256" r="2" />
<S d="95232" />
<S d="96256" r="2" />
<S d="95232" />
<S d="96256" r="2" />
<S d="95232" />
<S d="96256" r="2" />
<S d="95232" />
<S d="96256" r="2" />
<S d="95232" />
<S d="96256" r="2" />
<S d="95232" />
<S d="96256" r="2" />
<S d="95232" />
<S d="96256" r="2" />
<S d="95232" />
<S d="96256" r="2" />
<S d="95232" />
<S d="96256" r="2" />
<S d="95232" />
<S d="96256" r="2" />
<S d="95232" />
<S d="96256" r="2" />
<S d="95232" />
<S d="96256" r="2" />
<S d="95232" />
<S d="96256" r="2" />
<S d="95232" />
<S d="96256" r="2" />
<S d="95232" />
<S d="96256" r="2" />
<S d="95232" />
<S d="96256" r="2" />
<S d="95232" />
<S d="96256" r="2" />
<S d="95232" />
<S d="96256" r="2" />
<S d="95232" />
<S d="96256" r="2" />
<S d="95232" />
<S d="96256" r="2" />
<S d="95232" />
<S d="96256" r="2" />
<S d="95232" />
<S d="96256" r="2" />
<S d="95232" />
<S d="96256" r="2" />
<S d="95232" />
<S d="96256" r="2" />
<S d="95232" />
<S d="96256" r="2" />
<S d="95232" />
<S d="96256" r="2" />
<S d="95232" />
<S d="96256" r="2" />
<S d="95232" />
<S d="96256" r="2" />
<S d="95232" />
<S d="96256" r="2" />
<S d="95232" />
<S d="96256" r="2" />
<S d="95232" />
<S d="96256" r="2" />
<S d="95232" />
<S d="96256" r="2" />
<S d="95232" />
<S d="96256" r="2" />
<S d="95232" />
<S d="96256" r="2" />
<S d="95232" />
<S d="96256" r="2" />
<S d="95232" />
<S d="96256" r="2" />
<S d="95232" />
<S d="96256" r="2" />
<S d="95232" />
<S d="96256" r="2" />
<S d="95232" />
<S d="96256" r="2" />
<S d="95232" />
<S d="96256" r="2" />
<S d="95232" />
<S d="96256" r="2" />
<S d="95232" />
<S d="96256" r="2" />
<S d="95232" />
<S d="96256" r="2" />
<S d="95232" />
<S d="96256" r="2" />
<S d="95232" />
<S d="3584" />
</SegmentTimeline>
</SegmentTemplate>
<Representation
id="audio=128001"
bandwidth="128001">
</Representation>
</AdaptationSet>
<AdaptationSet
id="2"
group="3"
contentType="text"
lang="en"
mimeType="application/mp4"
codecs="stpp"
startWithSAP="1">
<Role schemeIdUri="urn:mpeg:dash:role:2011" value="subtitle" />
<SegmentTemplate
timescale="1000"
initialization="3144-kZT4LWMQw6Rh7Kpd-$RepresentationID$.dash"
media="3144-kZT4LWMQw6Rh7Kpd-$RepresentationID$-$Time$.dash">
<SegmentTimeline>
<S t="0" d="60000" r="9" />
<S d="24000" />
</SegmentTimeline>
</SegmentTemplate>
<Representation
id="textstream_eng=1000"
bandwidth="1000">
</Representation>
</AdaptationSet>
<AdaptationSet
id="3"
group="2"
contentType="video"
par="960:409"
minBandwidth="100000"
maxBandwidth="4482000"
maxWidth="1689"
maxHeight="720"
segmentAlignment="true"
mimeType="video/mp4"
codecs="avc1.4D401F"
startWithSAP="1">
<Role schemeIdUri="urn:mpeg:dash:role:2011" value="main" />
<SegmentTemplate
timescale="12288"
initialization="3144-kZT4LWMQw6Rh7Kpd-$RepresentationID$.dash"
media="3144-kZT4LWMQw6Rh7Kpd-$RepresentationID$-$Time$.dash">
<SegmentTimeline>
<S t="0" d="24576" r="443" />
</SegmentTimeline>
</SegmentTemplate>
<Representation
id="video=100000"
bandwidth="100000"
width="336"
height="144"
sar="2880:2863"
scanType="progressive">
</Representation>
<Representation
id="video=326000"
bandwidth="326000"
width="562"
height="240"
sar="115200:114929"
scanType="progressive">
</Representation>
<Representation
id="video=698000"
bandwidth="698000"
width="844"
height="360"
sar="86400:86299"
scanType="progressive">
</Representation>
<Representation
id="video=1493000"
bandwidth="1493000"
width="1126"
height="480"
sar="230400:230267"
scanType="progressive">
</Representation>
<Representation
id="video=4482000"
bandwidth="4482000"
width="1688"
height="720"
sar="86400:86299"
scanType="progressive">
</Representation>
</AdaptationSet>
</Period>
</MPD>
Regular → Executable
+2 -2
View File
@@ -1,2 +1,2 @@
#!/bin/bash #!/bin/sh
python3 "$(dirname $(realpath $0))/yt_dlp/__main__.py" "$@" exec python3 "$(dirname "$(realpath "$0")")/yt_dlp/__main__.py" "$@"
+291 -130
View File
@@ -19,7 +19,6 @@ import platform
import re import re
import shutil import shutil
import subprocess import subprocess
import socket
import sys import sys
import time import time
import tokenize import tokenize
@@ -33,7 +32,6 @@ from .compat import (
compat_basestring, compat_basestring,
compat_cookiejar, compat_cookiejar,
compat_get_terminal_size, compat_get_terminal_size,
compat_http_client,
compat_kwargs, compat_kwargs,
compat_numeric_types, compat_numeric_types,
compat_os_name, compat_os_name,
@@ -77,6 +75,7 @@ from .utils import (
make_dir, make_dir,
make_HTTPS_handler, make_HTTPS_handler,
MaxDownloadsReached, MaxDownloadsReached,
network_exceptions,
orderedSet, orderedSet,
PagedList, PagedList,
parse_filesize, parse_filesize,
@@ -85,6 +84,7 @@ from .utils import (
PostProcessingError, PostProcessingError,
preferredencoding, preferredencoding,
prepend_extension, prepend_extension,
random_uuidv4,
register_socks_protocols, register_socks_protocols,
render_table, render_table,
replace_extension, replace_extension,
@@ -99,6 +99,7 @@ from .utils import (
strftime_or_none, strftime_or_none,
subtitles_filename, subtitles_filename,
to_high_limit_path, to_high_limit_path,
traverse_dict,
UnavailableVideoError, UnavailableVideoError,
url_basename, url_basename,
version_tuple, version_tuple,
@@ -191,6 +192,9 @@ class YoutubeDL(object):
simulate: Do not download the video files. simulate: Do not download the video files.
format: Video format code. see "FORMAT SELECTION" for more details. format: Video format code. see "FORMAT SELECTION" for more details.
allow_unplayable_formats: Allow unplayable formats to be extracted and downloaded. allow_unplayable_formats: Allow unplayable formats to be extracted and downloaded.
ignore_no_formats_error: Ignore "No video formats" error. Usefull for
extracting metadata even if the video is not actually
available for download (experimental)
format_sort: How to sort the video formats. see "Sorting Formats" format_sort: How to sort the video formats. see "Sorting Formats"
for more details. for more details.
format_sort_force: Force the given format_sort. see "Sorting Formats" format_sort_force: Force the given format_sort. see "Sorting Formats"
@@ -211,6 +215,8 @@ class YoutubeDL(object):
ignoreerrors: Do not stop on download errors ignoreerrors: Do not stop on download errors
(Default True when running yt-dlp, (Default True when running yt-dlp,
but False when directly accessing YoutubeDL class) but False when directly accessing YoutubeDL class)
skip_playlist_after_errors: Number of allowed failures until the rest of
the playlist is skipped
force_generic_extractor: Force downloader to use the generic extractor force_generic_extractor: Force downloader to use the generic extractor
overwrites: Overwrite all video and metadata files if True, overwrites: Overwrite all video and metadata files if True,
overwrite only non-video files if None overwrite only non-video files if None
@@ -241,11 +247,15 @@ class YoutubeDL(object):
writedesktoplink: Write a Linux internet shortcut file (.desktop) writedesktoplink: Write a Linux internet shortcut file (.desktop)
writesubtitles: Write the video subtitles to a file writesubtitles: Write the video subtitles to a file
writeautomaticsub: Write the automatically generated subtitles to a file writeautomaticsub: Write the automatically generated subtitles to a file
allsubtitles: Downloads all the subtitles of the video allsubtitles: Deprecated - Use subtitlelangs = ['all']
Downloads all the subtitles of the video
(requires writesubtitles or writeautomaticsub) (requires writesubtitles or writeautomaticsub)
listsubtitles: Lists all available subtitles for the video listsubtitles: Lists all available subtitles for the video
subtitlesformat: The format code for subtitles subtitlesformat: The format code for subtitles
subtitleslangs: List of languages of the subtitles to download subtitleslangs: List of languages of the subtitles to download (can be regex).
The list may contain "all" to refer to all the available
subtitles. The language can be prefixed with a "-" to
exclude it from the requested languages. Eg: ['all', '-live_chat']
keepvideo: Keep the video file after post-processing keepvideo: Keep the video file after post-processing
daterange: A DateRange object, download only if the upload_date is in the range. daterange: A DateRange object, download only if the upload_date is in the range.
skip_download: Skip the actual download of the video file skip_download: Skip the actual download of the video file
@@ -375,6 +385,10 @@ class YoutubeDL(object):
Use the native HLS downloader instead of ffmpeg/avconv Use the native HLS downloader instead of ffmpeg/avconv
if True, otherwise use ffmpeg/avconv if False, otherwise if True, otherwise use ffmpeg/avconv if False, otherwise
use downloader suggested by extractor if None. use downloader suggested by extractor if None.
compat_opts: Compatibility options. See "Differences in default behavior".
Note that only format-sort, format-spec, no-live-chat,
playlist-index, list-formats, no-youtube-channel-redirect
and no-youtube-unavailable-videos works when used via the API
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):
@@ -452,39 +466,29 @@ class YoutubeDL(object):
} }
self.params.update(params) self.params.update(params)
self.cache = Cache(self) self.cache = Cache(self)
self.archive = set()
"""Preload the archive, if any is specified""" if sys.version_info < (3, 6):
def preload_download_archive(self): self.report_warning(
fn = self.params.get('download_archive') 'Support for Python version %d.%d have been deprecated and will break in future versions of yt-dlp! '
if fn is None: 'Update to Python 3.6 or above' % sys.version_info[:2])
return False
try:
with locked_file(fn, 'r', encoding='utf-8') as archive_file:
for line in archive_file:
self.archive.add(line.strip())
except IOError as ioe:
if ioe.errno != errno.ENOENT:
raise
return False
return True
def check_deprecated(param, option, suggestion): def check_deprecated(param, option, suggestion):
if self.params.get(param) is not None: if self.params.get(param) is not None:
self.report_warning( self.report_warning('%s is deprecated. Use %s instead' % (option, suggestion))
'%s is deprecated. Use %s instead.' % (option, suggestion))
return True return True
return False return False
if self.params.get('verbose'):
self.to_stdout('[debug] Loading archive file %r' % self.params.get('download_archive'))
preload_download_archive(self)
if check_deprecated('cn_verification_proxy', '--cn-verification-proxy', '--geo-verification-proxy'): if check_deprecated('cn_verification_proxy', '--cn-verification-proxy', '--geo-verification-proxy'):
if self.params.get('geo_verification_proxy') is None: if self.params.get('geo_verification_proxy') is None:
self.params['geo_verification_proxy'] = self.params['cn_verification_proxy'] self.params['geo_verification_proxy'] = self.params['cn_verification_proxy']
check_deprecated('autonumber', '--auto-number', '-o "%(autonumber)s-%(title)s.%(ext)s"')
check_deprecated('usetitle', '--title', '-o "%(title)s-%(id)s.%(ext)s"')
check_deprecated('useid', '--id', '-o "%(id)s.%(ext)s"')
for msg in self.params.get('warnings', []):
self.report_warning(msg)
if self.params.get('final_ext'): if self.params.get('final_ext'):
if self.params.get('merge_output_format'): if self.params.get('merge_output_format'):
self.report_warning('--merge-output-format will be ignored since --remux-video or --recode-video is given') self.report_warning('--merge-output-format will be ignored since --remux-video or --recode-video is given')
@@ -493,10 +497,6 @@ class YoutubeDL(object):
if 'overwrites' in self.params and self.params['overwrites'] is None: if 'overwrites' in self.params and self.params['overwrites'] is None:
del self.params['overwrites'] del self.params['overwrites']
check_deprecated('autonumber_size', '--autonumber-size', 'output template with %(autonumber)0Nd, where N in the number of digits')
check_deprecated('autonumber', '--auto-number', '-o "%(autonumber)s-%(title)s.%(ext)s"')
check_deprecated('usetitle', '--title', '-o "%(title)s-%(id)s.%(ext)s"')
if params.get('bidi_workaround', False): if params.get('bidi_workaround', False):
try: try:
import pty import pty
@@ -538,6 +538,25 @@ class YoutubeDL(object):
self._setup_opener() self._setup_opener()
"""Preload the archive, if any is specified"""
def preload_download_archive(fn):
if fn is None:
return False
if self.params.get('verbose'):
self._write_string('[debug] Loading archive file %r\n' % fn)
try:
with locked_file(fn, 'r', encoding='utf-8') as archive_file:
for line in archive_file:
self.archive.add(line.strip())
except IOError as ioe:
if ioe.errno != errno.ENOENT:
raise
return False
return True
self.archive = set()
preload_download_archive(self.params.get('download_archive'))
if auto_init: if auto_init:
self.print_debug_header() self.print_debug_header()
self.add_default_info_extractors() self.add_default_info_extractors()
@@ -632,16 +651,18 @@ class YoutubeDL(object):
def to_screen(self, message, skip_eol=False): def to_screen(self, message, skip_eol=False):
"""Print message to stdout if not in quiet mode.""" """Print message to stdout if not in quiet mode."""
return self.to_stdout(message, skip_eol, check_quiet=True) return self.to_stdout(
message, skip_eol,
quiet=self.params.get('quiet', False))
def _write_string(self, s, out=None): def _write_string(self, s, out=None):
write_string(s, out=out, encoding=self.params.get('encoding')) write_string(s, out=out, encoding=self.params.get('encoding'))
def to_stdout(self, message, skip_eol=False, check_quiet=False): def to_stdout(self, message, skip_eol=False, quiet=False):
"""Print message to stdout if not in quiet mode.""" """Print message to stdout if not in quiet mode."""
if self.params.get('logger'): if self.params.get('logger'):
self.params['logger'].debug(message) self.params['logger'].debug(message)
elif not check_quiet or not self.params.get('quiet', False): elif not quiet:
message = self._bidi_workaround(message) message = self._bidi_workaround(message)
terminator = ['\n', ''][skip_eol] terminator = ['\n', ''][skip_eol]
output = message + terminator output = message + terminator
@@ -787,6 +808,7 @@ class YoutubeDL(object):
def prepare_outtmpl(self, outtmpl, info_dict, sanitize=None): def prepare_outtmpl(self, outtmpl, info_dict, sanitize=None):
""" Make the template and info_dict suitable for substitution (outtmpl % info_dict)""" """ Make the template and info_dict suitable for substitution (outtmpl % info_dict)"""
template_dict = dict(info_dict) template_dict = dict(info_dict)
na = self.params.get('outtmpl_na_placeholder', 'NA')
# duration_string # duration_string
template_dict['duration_string'] = ( # %(duration>%H-%M-%S)s is wrong if duration > 24hrs template_dict['duration_string'] = ( # %(duration>%H-%M-%S)s is wrong if duration > 24hrs
@@ -812,18 +834,10 @@ class YoutubeDL(object):
elif template_dict.get('width'): elif template_dict.get('width'):
template_dict['resolution'] = '%dx?' % template_dict['width'] template_dict['resolution'] = '%dx?' % template_dict['width']
if sanitize is None:
sanitize = lambda k, v: v
template_dict = dict((k, v if isinstance(v, compat_numeric_types) else sanitize(k, v))
for k, v in template_dict.items()
if v is not None and not isinstance(v, (list, tuple, dict)))
na = self.params.get('outtmpl_na_placeholder', 'NA')
template_dict = collections.defaultdict(lambda: na, template_dict)
# For fields playlist_index and autonumber convert all occurrences # For fields playlist_index and autonumber convert all occurrences
# of %(field)s to %(field)0Nd for backward compatibility # of %(field)s to %(field)0Nd for backward compatibility
field_size_compat_map = { field_size_compat_map = {
'playlist_index': len(str(template_dict['n_entries'])), 'playlist_index': len(str(template_dict.get('_last_playlist_index') or '')),
'autonumber': autonumber_size, 'autonumber': autonumber_size,
} }
FIELD_SIZE_COMPAT_RE = r'(?<!%)%\((?P<field>autonumber|playlist_index)\)s' FIELD_SIZE_COMPAT_RE = r'(?<!%)%\((?P<field>autonumber|playlist_index)\)s'
@@ -835,32 +849,89 @@ class YoutubeDL(object):
outtmpl) outtmpl)
numeric_fields = list(self._NUMERIC_FIELDS) numeric_fields = list(self._NUMERIC_FIELDS)
if sanitize is None:
sanitize = lambda k, v: v
# Format date EXTERNAL_FORMAT_RE = FORMAT_RE.format('(?P<key>[^)]*)')
FORMAT_DATE_RE = FORMAT_RE.format(r'(?P<key>(?P<field>\w+)>(?P<format>.+?))') # Field is of the form key1.key2...
for mobj in re.finditer(FORMAT_DATE_RE, outtmpl): # where keys (except first) can be string, int or slice
conv_type, field, frmt, key = mobj.group('type', 'field', 'format', 'key') FIELD_RE = r'\w+(?:\.(?:\w+|[-\d]*(?::[-\d]*){0,2}))*'
if key in template_dict: INTERNAL_FORMAT_RE = re.compile(r'''(?x)
continue (?P<negate>-)?
value = strftime_or_none(template_dict.get(field), frmt, na) (?P<fields>{0})
if conv_type in 'crs': # string (?P<maths>(?:[-+]-?(?:\d+(?:\.\d+)?|{0}))*)
value = sanitize(field, value) (?:>(?P<strf_format>.+?))?
else: # number (?:\|(?P<default>.*?))?
numeric_fields.append(key) $'''.format(FIELD_RE))
value = float_or_none(value, default=None) MATH_OPERATORS_RE = re.compile(r'(?<![-+])([-+])')
MATH_FUNCTIONS = {
'+': float.__add__,
'-': float.__sub__,
}
for outer_mobj in re.finditer(EXTERNAL_FORMAT_RE, outtmpl):
final_key = outer_mobj.group('key')
str_type = outer_mobj.group('type')
value = None
mobj = re.match(INTERNAL_FORMAT_RE, final_key)
if mobj is not None:
mobj = mobj.groupdict()
# Object traversal
fields = mobj['fields'].split('.')
value = traverse_dict(template_dict, fields)
# Negative
if mobj['negate']:
value = float_or_none(value)
if value is not None:
value *= -1
# Do maths
if mobj['maths']:
value = float_or_none(value)
operator = None
for item in MATH_OPERATORS_RE.split(mobj['maths'])[1:]:
if item == '':
value = None
if value is None:
break
if operator:
item, multiplier = (item[1:], -1) if item[0] == '-' else (item, 1)
offset = float_or_none(item)
if offset is None:
offset = float_or_none(traverse_dict(template_dict, item.split('.')))
try:
value = operator(value, multiplier * offset)
except (TypeError, ZeroDivisionError):
value = None
operator = None
else:
operator = MATH_FUNCTIONS[item]
# Datetime formatting
if mobj['strf_format']:
value = strftime_or_none(value, mobj['strf_format'])
# Set default
if value is None and mobj['default'] is not None:
value = mobj['default']
# Sanitize
if str_type in 'crs' and value is not None: # string
value = sanitize('%{}'.format(str_type) % fields[-1], value)
else: # numeric
numeric_fields.append(final_key)
value = float_or_none(value)
if value is not None: if value is not None:
template_dict[key] = value template_dict[final_key] = value
# Missing numeric fields used together with integer presentation types # Missing numeric fields used together with integer presentation types
# in format specification will break the argument substitution since # in format specification will break the argument substitution since
# string NA placeholder is returned for missing fields. We will patch # string NA placeholder is returned for missing fields. We will patch
# output template for missing fields to meet string presentation type. # output template for missing fields to meet string presentation type.
for numeric_field in numeric_fields: for numeric_field in numeric_fields:
if numeric_field not in template_dict: if template_dict.get(numeric_field) is None:
outtmpl = re.sub( outtmpl = re.sub(
FORMAT_RE.format(re.escape(numeric_field)), FORMAT_RE.format(re.escape(numeric_field)),
r'%({0})s'.format(numeric_field), outtmpl) r'%({0})s'.format(numeric_field), outtmpl)
template_dict = collections.defaultdict(lambda: na, (
(k, v if isinstance(v, compat_numeric_types) else sanitize(k, v))
for k, v in template_dict.items() if v is not None))
return outtmpl, template_dict return outtmpl, template_dict
def _prepare_filename(self, info_dict, tmpl_type='default'): def _prepare_filename(self, info_dict, tmpl_type='default'):
@@ -991,13 +1062,22 @@ class YoutubeDL(object):
for key, value in extra_info.items(): for key, value in extra_info.items():
info_dict.setdefault(key, value) info_dict.setdefault(key, value)
def extract_info(self, url, download=True, ie_key=None, info_dict=None, extra_info={}, def extract_info(self, url, download=True, ie_key=None, extra_info={},
process=True, force_generic_extractor=False): process=True, force_generic_extractor=False):
''' """
Returns a list with a dictionary for each video we find. Return a list with a dictionary for each video extracted.
If 'download', also downloads the videos.
extra_info is a dict containing the extra values to add to each result Arguments:
''' url -- URL to extract
Keyword arguments:
download -- whether to download videos during extraction
ie_key -- extractor key hint
extra_info -- dictionary containing the extra values to add to each result
process -- whether to resolve all unresolved references (URLs, playlist items),
must be True for download to work.
force_generic_extractor -- force using the generic extractor
"""
if not ie_key and force_generic_extractor: if not ie_key and force_generic_extractor:
ie_key = 'Generic' ie_key = 'Generic'
@@ -1027,7 +1107,7 @@ class YoutubeDL(object):
self.to_screen("[%s] %s: has already been recorded in archive" % ( self.to_screen("[%s] %s: has already been recorded in archive" % (
ie_key, temp_id)) ie_key, temp_id))
break break
return self.__extract_info(url, ie, download, extra_info, process, info_dict) return self.__extract_info(url, ie, download, extra_info, process)
else: else:
self.report_error('no suitable InfoExtractor for URL %s' % url) self.report_error('no suitable InfoExtractor for URL %s' % url)
@@ -1054,7 +1134,7 @@ class YoutubeDL(object):
return wrapper return wrapper
@__handle_extraction_exceptions @__handle_extraction_exceptions
def __extract_info(self, url, ie, download, extra_info, process, info_dict): def __extract_info(self, url, ie, download, extra_info, process):
ie_result = ie.extract(url) ie_result = ie.extract(url)
if ie_result is None: # Finished already (backwards compatibility; listformats and friends should be moved here) if ie_result is None: # Finished already (backwards compatibility; listformats and friends should be moved here)
return return
@@ -1064,11 +1144,6 @@ class YoutubeDL(object):
'_type': 'compat_list', '_type': 'compat_list',
'entries': ie_result, 'entries': ie_result,
} }
if info_dict:
if info_dict.get('id'):
ie_result['id'] = info_dict['id']
if info_dict.get('title'):
ie_result['title'] = info_dict['title']
self.add_default_extra_info(ie_result, ie, url) self.add_default_extra_info(ie_result, ie, url)
if process: if process:
return self.process_ie_result(ie_result, download, extra_info) return self.process_ie_result(ie_result, download, extra_info)
@@ -1108,7 +1183,7 @@ class YoutubeDL(object):
# We have to add extra_info to the results because it may be # We have to add extra_info to the results because it may be
# contained in a playlist # contained in a playlist
return self.extract_info(ie_result['url'], return self.extract_info(ie_result['url'],
download, info_dict=ie_result, download,
ie_key=ie_result.get('ie_key'), ie_key=ie_result.get('ie_key'),
extra_info=extra_info) extra_info=extra_info)
elif result_type == 'url_transparent': elif result_type == 'url_transparent':
@@ -1278,7 +1353,7 @@ class YoutubeDL(object):
'playlist_title': ie_result.get('title'), 'playlist_title': ie_result.get('title'),
'playlist_uploader': ie_result.get('uploader'), 'playlist_uploader': ie_result.get('uploader'),
'playlist_uploader_id': ie_result.get('uploader_id'), 'playlist_uploader_id': ie_result.get('uploader_id'),
'playlist_index': 0 'playlist_index': 0,
} }
ie_copy.update(dict(ie_result)) ie_copy.update(dict(ie_result))
@@ -1312,6 +1387,11 @@ class YoutubeDL(object):
self.report_error('Cannot write playlist description file ' + descfn) self.report_error('Cannot write playlist description file ' + descfn)
return return
# Save playlist_index before re-ordering
entries = [
((playlistitems[i - 1] if playlistitems else i), entry)
for i, entry in enumerate(entries, 1)]
if self.params.get('playlistreverse', False): if self.params.get('playlistreverse', False):
entries = entries[::-1] entries = entries[::-1]
if self.params.get('playlistrandom', False): if self.params.get('playlistrandom', False):
@@ -1320,7 +1400,12 @@ class YoutubeDL(object):
x_forwarded_for = ie_result.get('__x_forwarded_for_ip') x_forwarded_for = ie_result.get('__x_forwarded_for_ip')
self.to_screen('[%s] playlist %s: %s' % (ie_result['extractor'], playlist, msg)) self.to_screen('[%s] playlist %s: %s' % (ie_result['extractor'], playlist, msg))
for i, entry in enumerate(entries, 1): failures = 0
max_failures = self.params.get('skip_playlist_after_errors') or float('inf')
for i, entry_tuple in enumerate(entries, 1):
playlist_index, entry = entry_tuple
if 'playlist_index' in self.params.get('compat_options', []):
playlist_index = playlistitems[i - 1] if playlistitems else i
self.to_screen('[download] Downloading video %s of %s' % (i, n_entries)) self.to_screen('[download] Downloading video %s of %s' % (i, n_entries))
# This __x_forwarded_for_ip thing is a bit ugly but requires # This __x_forwarded_for_ip thing is a bit ugly but requires
# minimal changes # minimal changes
@@ -1328,12 +1413,14 @@ class YoutubeDL(object):
entry['__x_forwarded_for_ip'] = x_forwarded_for entry['__x_forwarded_for_ip'] = x_forwarded_for
extra = { extra = {
'n_entries': n_entries, 'n_entries': n_entries,
'_last_playlist_index': max(playlistitems) if playlistitems else (playlistend or n_entries),
'playlist_index': playlist_index,
'playlist_autonumber': i,
'playlist': playlist, 'playlist': playlist,
'playlist_id': ie_result.get('id'), 'playlist_id': ie_result.get('id'),
'playlist_title': ie_result.get('title'), 'playlist_title': ie_result.get('title'),
'playlist_uploader': ie_result.get('uploader'), 'playlist_uploader': ie_result.get('uploader'),
'playlist_uploader_id': ie_result.get('uploader_id'), 'playlist_uploader_id': ie_result.get('uploader_id'),
'playlist_index': playlistitems[i - 1] if playlistitems else i,
'extractor': ie_result['extractor'], 'extractor': ie_result['extractor'],
'webpage_url': ie_result['webpage_url'], 'webpage_url': ie_result['webpage_url'],
'webpage_url_basename': url_basename(ie_result['webpage_url']), 'webpage_url_basename': url_basename(ie_result['webpage_url']),
@@ -1344,6 +1431,12 @@ class YoutubeDL(object):
continue continue
entry_result = self.__process_iterable_entry(entry, download, extra) entry_result = self.__process_iterable_entry(entry, download, extra)
if not entry_result:
failures += 1
if failures >= max_failures:
self.report_error(
'Skipping the remaining entries in playlist "%s" since %d items failed extraction' % (playlist, failures))
break
# TODO: skip failed (empty) entries? # TODO: skip failed (empty) entries?
playlist_results.append(entry_result) playlist_results.append(entry_result)
ie_result['entries'] = playlist_results ie_result['entries'] = playlist_results
@@ -1431,12 +1524,14 @@ class YoutubeDL(object):
not can_merge() not can_merge()
or info_dict.get('is_live', False) or info_dict.get('is_live', False)
or self.outtmpl_dict['default'] == '-')) or self.outtmpl_dict['default'] == '-'))
compat = (
prefer_best
or self.params.get('allow_multiple_audio_streams', False)
or 'format-spec' in self.params.get('compat_opts', []))
return ( return (
'best/bestvideo+bestaudio' 'best/bestvideo+bestaudio' if prefer_best
if prefer_best else 'bestvideo*+bestaudio/best' if not compat
else 'bestvideo*+bestaudio/best'
if not self.params.get('allow_multiple_audio_streams', False)
else 'bestvideo+bestaudio/best') else 'bestvideo+bestaudio/best')
def build_format_selector(self, format_spec): def build_format_selector(self, format_spec):
@@ -1455,6 +1550,8 @@ class YoutubeDL(object):
allow_multiple_streams = {'audio': self.params.get('allow_multiple_audio_streams', False), allow_multiple_streams = {'audio': self.params.get('allow_multiple_audio_streams', False),
'video': self.params.get('allow_multiple_video_streams', False)} 'video': self.params.get('allow_multiple_video_streams', False)}
check_formats = self.params.get('check_formats')
def _parse_filter(tokens): def _parse_filter(tokens):
filter_parts = [] filter_parts = []
for type, string, start, _, _ in tokens: for type, string, start, _, _ in tokens:
@@ -1612,6 +1709,22 @@ class YoutubeDL(object):
return new_dict return new_dict
def _check_formats(formats):
for f in formats:
self.to_screen('[info] Testing format %s' % f['format_id'])
paths = self.params.get('paths', {})
temp_file = os.path.join(
expand_path(paths.get('home', '').strip()),
expand_path(paths.get('temp', '').strip()),
'ytdl.%s.f%s.check-format' % (random_uuidv4(), f['format_id']))
dl, _ = self.dl(temp_file, f, test=True)
if os.path.exists(temp_file):
os.remove(temp_file)
if dl:
yield f
else:
self.to_screen('[info] Unable to download format %s. Skipping...' % f['format_id'])
def _build_selector_function(selector): def _build_selector_function(selector):
if isinstance(selector, list): # , if isinstance(selector, list): # ,
fs = [_build_selector_function(s) for s in selector] fs = [_build_selector_function(s) for s in selector]
@@ -1636,33 +1749,34 @@ class YoutubeDL(object):
return [] return []
elif selector.type == SINGLE: # atom elif selector.type == SINGLE: # atom
format_spec = (selector.selector or 'best').lower() format_spec = selector.selector or 'best'
# TODO: Add allvideo, allaudio etc by generalizing the code with best/worst selector # TODO: Add allvideo, allaudio etc by generalizing the code with best/worst selector
if format_spec == 'all': if format_spec == 'all':
def selector_function(ctx): def selector_function(ctx):
formats = list(ctx['formats']) formats = list(ctx['formats'])
if formats: if check_formats:
for f in formats: formats = _check_formats(formats)
yield f for f in formats:
yield f
elif format_spec == 'mergeall': elif format_spec == 'mergeall':
def selector_function(ctx): def selector_function(ctx):
formats = list(ctx['formats']) formats = list(_check_formats(ctx['formats']))
if not formats: if not formats:
return return
merged_format = formats[0] merged_format = formats[-1]
for f in formats[1:]: for f in formats[-2::-1]:
merged_format = _merge((merged_format, f)) merged_format = _merge((merged_format, f))
yield merged_format yield merged_format
else: else:
format_fallback = False format_fallback, format_reverse, format_idx = False, True, 1
mobj = re.match( mobj = re.match(
r'(?P<bw>best|worst|b|w)(?P<type>video|audio|v|a)?(?P<mod>\*)?(?:\.(?P<n>[1-9]\d*))?$', r'(?P<bw>best|worst|b|w)(?P<type>video|audio|v|a)?(?P<mod>\*)?(?:\.(?P<n>[1-9]\d*))?$',
format_spec) format_spec)
if mobj is not None: if mobj is not None:
format_idx = int_or_none(mobj.group('n'), default=1) format_idx = int_or_none(mobj.group('n'), default=1)
format_idx = format_idx - 1 if mobj.group('bw')[0] == 'w' else -format_idx format_reverse = mobj.group('bw')[0] == 'b'
format_type = (mobj.group('type') or [None])[0] format_type = (mobj.group('type') or [None])[0]
not_format_type = {'v': 'a', 'a': 'v'}.get(format_type) not_format_type = {'v': 'a', 'a': 'v'}.get(format_type)
format_modified = mobj.group('mod') is not None format_modified = mobj.group('mod') is not None
@@ -1677,7 +1791,6 @@ class YoutubeDL(object):
if not format_modified # b, w if not format_modified # b, w
else None) # b*, w* else None) # b*, w*
else: else:
format_idx = -1
filter_f = ((lambda f: f.get('ext') == format_spec) filter_f = ((lambda f: f.get('ext') == format_spec)
if format_spec in ['mp4', 'flv', 'webm', '3gp', 'm4a', 'mp3', 'ogg', 'aac', 'wav'] # extension if format_spec in ['mp4', 'flv', 'webm', '3gp', 'm4a', 'mp3', 'ogg', 'aac', 'wav'] # extension
else (lambda f: f.get('format_id') == format_spec)) # id else (lambda f: f.get('format_id') == format_spec)) # id
@@ -1687,16 +1800,18 @@ class YoutubeDL(object):
if not formats: if not formats:
return return
matches = list(filter(filter_f, formats)) if filter_f is not None else formats matches = list(filter(filter_f, formats)) if filter_f is not None else formats
n = len(matches) if format_fallback and ctx['incomplete_formats'] and not matches:
if -n <= format_idx < n:
yield matches[format_idx]
elif format_fallback and ctx['incomplete_formats']:
# for extractors with incomplete formats (audio only (soundcloud) # for extractors with incomplete formats (audio only (soundcloud)
# or video only (imgur)) best/worst will fallback to # or video only (imgur)) best/worst will fallback to
# best/worst {video,audio}-only format # best/worst {video,audio}-only format
n = len(formats) matches = formats
if -n <= format_idx < n: if format_reverse:
yield formats[format_idx] matches = matches[::-1]
if check_formats:
matches = list(itertools.islice(_check_formats(matches), format_idx))
n = len(matches)
if -n <= format_idx - 1 < n:
yield matches[format_idx - 1]
elif selector.type == MERGE: # + elif selector.type == MERGE: # +
selector_1, selector_2 = map(_build_selector_function, selector.selector) selector_1, selector_2 = map(_build_selector_function, selector.selector)
@@ -1884,7 +1999,10 @@ class YoutubeDL(object):
formats = info_dict['formats'] formats = info_dict['formats']
if not formats: if not formats:
raise ExtractorError('No video formats found!') if not self.params.get('ignore_no_formats_error'):
raise ExtractorError('No video formats found!')
else:
self.report_warning('No video formats found!')
def is_wellformed(f): def is_wellformed(f):
url = f.get('url') url = f.get('url')
@@ -1948,13 +2066,15 @@ class YoutubeDL(object):
# TODO Central sorting goes here # TODO Central sorting goes here
if formats[0] is not info_dict: if formats and formats[0] is not info_dict:
# only set the 'formats' fields if the original info_dict list them # only set the 'formats' fields if the original info_dict list them
# otherwise we end up with a circular reference, the first (and unique) # otherwise we end up with a circular reference, the first (and unique)
# element in the 'formats' field in info_dict is info_dict itself, # element in the 'formats' field in info_dict is info_dict itself,
# which can't be exported to json # which can't be exported to json
info_dict['formats'] = formats info_dict['formats'] = formats
if self.params.get('listformats'): if self.params.get('listformats'):
if not info_dict.get('formats'):
raise ExtractorError('No video formats found', expected=True)
self.list_formats(info_dict) self.list_formats(info_dict)
return return
@@ -1994,19 +2114,25 @@ class YoutubeDL(object):
formats_to_download = list(format_selector(ctx)) formats_to_download = list(format_selector(ctx))
if not formats_to_download: if not formats_to_download:
raise ExtractorError('requested format not available', if not self.params.get('ignore_no_formats_error'):
expected=True) raise ExtractorError('Requested format is not available', expected=True)
else:
if download: self.report_warning('Requested format is not available')
self.to_screen('[info] Downloading format(s) %s' % ", ".join([f['format_id'] for f in formats_to_download])) elif download:
self.to_screen(
'[info] %s: Downloading format(s) %s'
% (info_dict['id'], ", ".join([f['format_id'] for f in formats_to_download])))
if len(formats_to_download) > 1: if len(formats_to_download) > 1:
self.to_screen('[info] %s: downloading video in %s formats' % (info_dict['id'], len(formats_to_download))) self.to_screen(
for format in formats_to_download: '[info] %s: Downloading video in %s formats'
% (info_dict['id'], len(formats_to_download)))
for fmt in formats_to_download:
new_info = dict(info_dict) new_info = dict(info_dict)
new_info.update(format) new_info.update(fmt)
self.process_info(new_info) self.process_info(new_info)
# We update the info dict with the best quality format (backwards compatibility) # We update the info dict with the best quality format (backwards compatibility)
info_dict.update(formats_to_download[-1]) if formats_to_download:
info_dict.update(formats_to_download[-1])
return info_dict return info_dict
def process_subtitles(self, video_id, normal_subtitles, automatic_captions): def process_subtitles(self, video_id, normal_subtitles, automatic_captions):
@@ -2024,15 +2150,28 @@ class YoutubeDL(object):
available_subs): available_subs):
return None return None
all_sub_langs = available_subs.keys()
if self.params.get('allsubtitles', False): if self.params.get('allsubtitles', False):
requested_langs = available_subs.keys() requested_langs = all_sub_langs
elif self.params.get('subtitleslangs', False):
requested_langs = set()
for lang in self.params.get('subtitleslangs'):
if lang == 'all':
requested_langs.update(all_sub_langs)
continue
discard = lang[0] == '-'
if discard:
lang = lang[1:]
current_langs = filter(re.compile(lang + '$').match, all_sub_langs)
if discard:
for lang in current_langs:
requested_langs.discard(lang)
else:
requested_langs.update(current_langs)
elif 'en' in available_subs:
requested_langs = ['en']
else: else:
if self.params.get('subtitleslangs', False): requested_langs = [list(all_sub_langs)[0]]
requested_langs = self.params.get('subtitleslangs')
elif 'en' in available_subs:
requested_langs = ['en']
else:
requested_langs = [list(available_subs.keys())[0]]
formats_query = self.params.get('subtitlesformat', 'best') formats_query = self.params.get('subtitlesformat', 'best')
formats_preference = formats_query.split('/') if formats_query else [] formats_preference = formats_query.split('/') if formats_query else []
@@ -2089,6 +2228,34 @@ class YoutubeDL(object):
self.post_extract(info_dict) self.post_extract(info_dict)
self.to_stdout(json.dumps(info_dict, default=repr)) self.to_stdout(json.dumps(info_dict, default=repr))
def dl(self, name, info, subtitle=False, test=False):
if test:
verbose = self.params.get('verbose')
params = {
'test': True,
'quiet': not verbose,
'verbose': verbose,
'noprogress': not verbose,
'nopart': True,
'skip_unavailable_fragments': False,
'keep_fragments': False,
'overwrites': True,
'_no_ytdl_file': True,
}
else:
params = self.params
fd = get_suitable_downloader(info, params)(self, params)
if not test:
for ph in self._progress_hooks:
fd.add_progress_hook(ph)
if self.params.get('verbose'):
self.to_screen('[debug] Invoking downloader on %r' % info.get('url'))
new_info = dict(info)
if new_info.get('http_headers') is None:
new_info['http_headers'] = self._calc_headers(new_info)
return fd.download(name, new_info, subtitle)
def process_info(self, info_dict): def process_info(self, info_dict):
"""Process a single resolved IE result.""" """Process a single resolved IE result."""
@@ -2174,17 +2341,6 @@ class YoutubeDL(object):
self.report_error('Cannot write annotations file: ' + annofn) self.report_error('Cannot write annotations file: ' + annofn)
return return
def dl(name, info, subtitle=False):
fd = get_suitable_downloader(info, self.params)(self, self.params)
for ph in self._progress_hooks:
fd.add_progress_hook(ph)
if self.params.get('verbose'):
self.to_screen('[debug] Invoking downloader on %r' % info.get('url'))
new_info = dict(info)
if new_info.get('http_headers') is None:
new_info['http_headers'] = self._calc_headers(new_info)
return fd.download(name, new_info, subtitle)
subtitles_are_requested = any([self.params.get('writesubtitles', False), subtitles_are_requested = any([self.params.get('writesubtitles', False),
self.params.get('writeautomaticsub')]) self.params.get('writeautomaticsub')])
@@ -2217,10 +2373,10 @@ class YoutubeDL(object):
return return
else: else:
try: try:
dl(sub_filename, sub_info.copy(), subtitle=True) self.dl(sub_filename, sub_info.copy(), subtitle=True)
sub_info['filepath'] = sub_filename sub_info['filepath'] = sub_filename
files_to_move[sub_filename] = sub_filename_final files_to_move[sub_filename] = sub_filename_final
except (ExtractorError, IOError, OSError, ValueError, compat_urllib_error.URLError, compat_http_client.HTTPException, socket.error) as err: except tuple([ExtractorError, IOError, OSError, ValueError] + network_exceptions) as err:
self.report_warning('Unable to download subtitle for "%s": %s' % self.report_warning('Unable to download subtitle for "%s": %s' %
(sub_lang, error_to_compat_str(err))) (sub_lang, error_to_compat_str(err)))
continue continue
@@ -2403,7 +2559,7 @@ class YoutubeDL(object):
if not self._ensure_dir_exists(fname): if not self._ensure_dir_exists(fname):
return return
downloaded.append(fname) downloaded.append(fname)
partial_success, real_download = dl(fname, new_info) partial_success, real_download = self.dl(fname, new_info)
info_dict['__real_download'] = info_dict['__real_download'] or real_download info_dict['__real_download'] = info_dict['__real_download'] or real_download
success = success and partial_success success = success and partial_success
if merger.available and not self.params.get('allow_unplayable_formats'): if merger.available and not self.params.get('allow_unplayable_formats'):
@@ -2418,13 +2574,13 @@ class YoutubeDL(object):
# Just a single file # Just a single file
dl_filename = existing_file(full_filename, temp_filename) dl_filename = existing_file(full_filename, temp_filename)
if dl_filename is None: if dl_filename is None:
success, real_download = dl(temp_filename, info_dict) success, real_download = self.dl(temp_filename, info_dict)
info_dict['__real_download'] = real_download info_dict['__real_download'] = real_download
dl_filename = dl_filename or temp_filename dl_filename = dl_filename or temp_filename
info_dict['__finaldir'] = os.path.dirname(os.path.abspath(encodeFilename(full_filename))) info_dict['__finaldir'] = os.path.dirname(os.path.abspath(encodeFilename(full_filename)))
except (compat_urllib_error.URLError, compat_http_client.HTTPException, socket.error) as err: except network_exceptions as err:
self.report_error('unable to download video data: %s' % error_to_compat_str(err)) self.report_error('unable to download video data: %s' % error_to_compat_str(err))
return return
except (OSError, IOError) as err: except (OSError, IOError) as err:
@@ -2764,7 +2920,9 @@ class YoutubeDL(object):
def list_formats(self, info_dict): def list_formats(self, info_dict):
formats = info_dict.get('formats', [info_dict]) formats = info_dict.get('formats', [info_dict])
new_format = self.params.get('listformats_table', False) new_format = (
'list-formats' not in self.params.get('compat_opts', [])
and self.params.get('list_formats_as_table', True) is not False)
if new_format: if new_format:
table = [ table = [
[ [
@@ -2865,6 +3023,9 @@ class YoutubeDL(object):
if _PLUGIN_CLASSES: if _PLUGIN_CLASSES:
self._write_string( self._write_string(
'[debug] Plugin Extractors: %s\n' % [ie.ie_key() for ie in _PLUGIN_CLASSES]) '[debug] Plugin Extractors: %s\n' % [ie.ie_key() for ie in _PLUGIN_CLASSES])
if self.params.get('compat_opts'):
self._write_string(
'[debug] Compatibility options: %s\n' % ', '.join(self.params.get('compat_opts')))
try: try:
sp = subprocess.Popen( sp = subprocess.Popen(
['git', 'rev-parse', '--short', 'HEAD'], ['git', 'rev-parse', '--short', 'HEAD'],
@@ -3019,7 +3180,7 @@ class YoutubeDL(object):
ret.append(suffix + thumb_ext) ret.append(suffix + thumb_ext)
self.to_screen('[%s] %s: Writing thumbnail %sto: %s' % self.to_screen('[%s] %s: Writing thumbnail %sto: %s' %
(info_dict['extractor'], info_dict['id'], thumb_display_id, thumb_filename)) (info_dict['extractor'], info_dict['id'], thumb_display_id, thumb_filename))
except (compat_urllib_error.URLError, compat_http_client.HTTPException, socket.error) as err: except network_exceptions as err:
self.report_warning('Unable to download thumbnail "%s": %s' % self.report_warning('Unable to download thumbnail "%s": %s' %
(t['url'], error_to_compat_str(err))) (t['url'], error_to_compat_str(err)))
if ret and not write_all: if ret and not write_all:
+81 -26
View File
@@ -60,6 +60,7 @@ def _real_main(argv=None):
setproctitle('yt-dlp') setproctitle('yt-dlp')
parser, opts, args = parseOpts(argv) parser, opts, args = parseOpts(argv)
warnings = []
# Set user agent # Set user agent
if opts.user_agent is not None: if opts.user_agent is not None:
@@ -128,16 +129,12 @@ def _real_main(argv=None):
parser.error('account username missing\n') parser.error('account username missing\n')
if opts.ap_password is not None and opts.ap_username is None: if opts.ap_password is not None and opts.ap_username is None:
parser.error('TV Provider account username missing\n') parser.error('TV Provider account username missing\n')
if opts.outtmpl is not None and (opts.usetitle or opts.autonumber or opts.useid):
parser.error('using output template conflicts with using title, video ID or auto number')
if opts.autonumber_size is not None: if opts.autonumber_size is not None:
if opts.autonumber_size <= 0: if opts.autonumber_size <= 0:
parser.error('auto number size must be positive') parser.error('auto number size must be positive')
if opts.autonumber_start is not None: if opts.autonumber_start is not None:
if opts.autonumber_start < 0: if opts.autonumber_start < 0:
parser.error('auto number start must be positive or 0') parser.error('auto number start must be positive or 0')
if opts.usetitle and opts.useid:
parser.error('using title conflicts with using video ID')
if opts.username is not None and opts.password is None: if opts.username is not None and opts.password is None:
opts.password = compat_getpass('Type account password and press [Return]: ') opts.password = compat_getpass('Type account password and press [Return]: ')
if opts.ap_username is not None and opts.ap_password is None: if opts.ap_username is not None and opts.ap_password is None:
@@ -177,8 +174,7 @@ def _real_main(argv=None):
parser.error('requests sleep interval must be positive or 0') parser.error('requests sleep interval must be positive or 0')
if opts.ap_mso and opts.ap_mso not in MSO_INFO: if opts.ap_mso and opts.ap_mso not in MSO_INFO:
parser.error('Unsupported TV Provider, use --ap-list-mso to get a list of supported TV Providers') parser.error('Unsupported TV Provider, use --ap-list-mso to get a list of supported TV Providers')
if opts.overwrites: if opts.overwrites: # --yes-overwrites implies --no-continue
# --yes-overwrites implies --no-continue
opts.continue_dl = False opts.continue_dl = False
if opts.concurrent_fragment_downloads <= 0: if opts.concurrent_fragment_downloads <= 0:
raise ValueError('Concurrent fragments must be positive') raise ValueError('Concurrent fragments must be positive')
@@ -239,21 +235,75 @@ def _real_main(argv=None):
else: else:
date = DateRange(opts.dateafter, opts.datebefore) date = DateRange(opts.dateafter, opts.datebefore)
# Do not download videos when there are audio-only formats def parse_compat_opts():
parsed_compat_opts, compat_opts = set(), opts.compat_opts[::-1]
while compat_opts:
actual_opt = opt = compat_opts.pop().lower()
if opt == 'youtube-dl':
compat_opts.extend(['-multistreams', 'all'])
elif opt == 'youtube-dlc':
compat_opts.extend(['-no-youtube-channel-redirect', '-no-live-chat', 'all'])
elif opt == 'all':
parsed_compat_opts.update(all_compat_opts)
elif opt == '-all':
parsed_compat_opts = set()
else:
if opt[0] == '-':
opt = opt[1:]
parsed_compat_opts.discard(opt)
else:
parsed_compat_opts.update([opt])
if opt not in all_compat_opts:
parser.error('Invalid compatibility option %s' % actual_opt)
return parsed_compat_opts
all_compat_opts = [
'filename', 'format-sort', 'abort-on-error', 'format-spec', 'multistreams',
'no-playlist-metafiles', 'no-live-chat', 'playlist-index', 'list-formats',
'no-youtube-channel-redirect', 'no-youtube-unavailable-videos',
]
compat_opts = parse_compat_opts()
def _unused_compat_opt(name):
if name not in compat_opts:
return False
compat_opts.discard(name)
compat_opts.update(['*%s' % name])
return True
def set_default_compat(compat_name, opt_name, default=True, remove_compat=False):
attr = getattr(opts, opt_name)
if compat_name in compat_opts:
if attr is None:
setattr(opts, opt_name, not default)
return True
else:
if remove_compat:
_unused_compat_opt(compat_name)
return False
elif attr is None:
setattr(opts, opt_name, default)
return None
set_default_compat('abort-on-error', 'ignoreerrors')
set_default_compat('no-playlist-metafiles', 'allow_playlist_files')
if 'format-sort' in compat_opts:
opts.format_sort.extend(InfoExtractor.FormatSort.ytdl_default)
_video_multistreams_set = set_default_compat('multistreams', 'allow_multiple_video_streams', False, remove_compat=False)
_audio_multistreams_set = set_default_compat('multistreams', 'allow_multiple_audio_streams', False, remove_compat=False)
if _video_multistreams_set is False and _audio_multistreams_set is False:
_unused_compat_opt('multistreams')
outtmpl_default = opts.outtmpl.get('default')
if 'filename' in compat_opts:
if outtmpl_default is None:
outtmpl_default = '%(title)s.%(id)s.%(ext)s'
opts.outtmpl.update({'default': outtmpl_default})
else:
_unused_compat_opt('filename')
if opts.extractaudio and not opts.keepvideo and opts.format is None: if opts.extractaudio and not opts.keepvideo and opts.format is None:
opts.format = 'bestaudio/best' opts.format = 'bestaudio/best'
outtmpl = opts.outtmpl
if not outtmpl:
outtmpl = {'default': (
'%(title)s-%(id)s-%(format)s.%(ext)s' if opts.format == '-1' and opts.usetitle
else '%(id)s-%(format)s.%(ext)s' if opts.format == '-1'
else '%(autonumber)s-%(title)s-%(id)s.%(ext)s' if opts.usetitle and opts.autonumber
else '%(title)s-%(id)s.%(ext)s' if opts.usetitle
else '%(id)s.%(ext)s' if opts.useid
else '%(autonumber)s-%(id)s.%(ext)s' if opts.autonumber
else None)}
outtmpl_default = outtmpl.get('default')
if outtmpl_default is not None and not os.path.splitext(outtmpl_default)[1] and opts.extractaudio: if outtmpl_default is not None and not os.path.splitext(outtmpl_default)[1] and opts.extractaudio:
parser.error('Cannot download a video and extract audio into the same' parser.error('Cannot download a video and extract audio into the same'
' file! Use "{0}.%(ext)s" instead of "{0}" as the output' ' file! Use "{0}.%(ext)s" instead of "{0}" as the output'
@@ -281,7 +331,7 @@ def _real_main(argv=None):
opts.writeinfojson = True opts.writeinfojson = True
def report_conflict(arg1, arg2): def report_conflict(arg1, arg2):
write_string('WARNING: %s is ignored since %s was given\n' % (arg2, arg1), out=sys.stderr) warnings.append('%s is ignored since %s was given' % (arg2, arg1))
if opts.remuxvideo and opts.recodevideo: if opts.remuxvideo and opts.recodevideo:
report_conflict('--recode-video', '--remux-video') report_conflict('--recode-video', '--remux-video')
@@ -419,11 +469,10 @@ def _real_main(argv=None):
}) })
def report_args_compat(arg, name): def report_args_compat(arg, name):
write_string( warnings.append('%s given without specifying name. The arguments will be given to all %s' % (arg, name))
'WARNING: %s given without specifying name. The arguments will be given to all %s\n' % (arg, name),
out=sys.stderr)
if 'default' in opts.external_downloader_args: if 'default' in opts.external_downloader_args:
report_args_compat('--external-downloader-args', 'external downloaders') report_args_compat('--downloader-args', 'external downloaders')
if 'default-compat' in opts.postprocessor_args and 'default' not in opts.postprocessor_args: if 'default-compat' in opts.postprocessor_args and 'default' not in opts.postprocessor_args:
report_args_compat('--post-processor-args', 'post-processors') report_args_compat('--post-processor-args', 'post-processors')
@@ -466,13 +515,15 @@ def _real_main(argv=None):
'skip_download': opts.skip_download, 'skip_download': opts.skip_download,
'format': opts.format, 'format': opts.format,
'allow_unplayable_formats': opts.allow_unplayable_formats, 'allow_unplayable_formats': opts.allow_unplayable_formats,
'ignore_no_formats_error': opts.ignore_no_formats_error,
'format_sort': opts.format_sort, 'format_sort': opts.format_sort,
'format_sort_force': opts.format_sort_force, 'format_sort_force': opts.format_sort_force,
'allow_multiple_video_streams': opts.allow_multiple_video_streams, 'allow_multiple_video_streams': opts.allow_multiple_video_streams,
'allow_multiple_audio_streams': opts.allow_multiple_audio_streams, 'allow_multiple_audio_streams': opts.allow_multiple_audio_streams,
'check_formats': opts.check_formats,
'listformats': opts.listformats, 'listformats': opts.listformats,
'listformats_table': opts.listformats_table, 'listformats_table': opts.listformats_table,
'outtmpl': outtmpl, 'outtmpl': opts.outtmpl,
'outtmpl_na_placeholder': opts.outtmpl_na_placeholder, 'outtmpl_na_placeholder': opts.outtmpl_na_placeholder,
'paths': opts.paths, 'paths': opts.paths,
'autonumber_size': opts.autonumber_size, 'autonumber_size': opts.autonumber_size,
@@ -543,6 +594,7 @@ def _real_main(argv=None):
'download_archive': download_archive_fn, 'download_archive': download_archive_fn,
'break_on_existing': opts.break_on_existing, 'break_on_existing': opts.break_on_existing,
'break_on_reject': opts.break_on_reject, 'break_on_reject': opts.break_on_reject,
'skip_playlist_after_errors': opts.skip_playlist_after_errors,
'cookiefile': opts.cookiefile, 'cookiefile': opts.cookiefile,
'nocheckcertificate': opts.no_check_certificate, 'nocheckcertificate': opts.no_check_certificate,
'prefer_insecure': opts.prefer_insecure, 'prefer_insecure': opts.prefer_insecure,
@@ -586,9 +638,12 @@ def _real_main(argv=None):
'geo_bypass': opts.geo_bypass, 'geo_bypass': opts.geo_bypass,
'geo_bypass_country': opts.geo_bypass_country, 'geo_bypass_country': opts.geo_bypass_country,
'geo_bypass_ip_block': opts.geo_bypass_ip_block, 'geo_bypass_ip_block': opts.geo_bypass_ip_block,
'warnings': warnings,
'compat_opts': compat_opts,
# just for deprecation check # just for deprecation check
'autonumber': opts.autonumber if opts.autonumber is True else None, 'autonumber': opts.autonumber or None,
'usetitle': opts.usetitle if opts.usetitle is True else None, 'usetitle': opts.usetitle or None,
'useid': opts.useid or None,
} }
with YoutubeDL(ydl_opts) as ydl: with YoutubeDL(ydl_opts) as ydl:
+14
View File
@@ -3018,10 +3018,24 @@ else:
return ctypes.WINFUNCTYPE(*args, **kwargs) return ctypes.WINFUNCTYPE(*args, **kwargs)
try:
compat_Pattern = re.Pattern
except AttributeError:
compat_Pattern = type(re.compile(''))
try:
compat_Match = re.Match
except AttributeError:
compat_Match = type(re.compile('').match(''))
__all__ = [ __all__ = [
'compat_HTMLParseError', 'compat_HTMLParseError',
'compat_HTMLParser', 'compat_HTMLParser',
'compat_HTTPError', 'compat_HTTPError',
'compat_Match',
'compat_Pattern',
'compat_Struct', 'compat_Struct',
'compat_b64decode', 'compat_b64decode',
'compat_basestring', 'compat_basestring',
+5 -2
View File
@@ -31,6 +31,7 @@ from .external import (
PROTOCOL_MAP = { PROTOCOL_MAP = {
'rtmp': RtmpFD, 'rtmp': RtmpFD,
'rtmp_ffmpeg': FFmpegFD,
'm3u8_native': HlsFD, 'm3u8_native': HlsFD,
'm3u8': FFmpegFD, 'm3u8': FFmpegFD,
'mms': RtspFD, 'mms': RtspFD,
@@ -46,6 +47,7 @@ PROTOCOL_MAP = {
def shorten_protocol_name(proto, simplify=False): def shorten_protocol_name(proto, simplify=False):
short_protocol_names = { short_protocol_names = {
'm3u8_native': 'm3u8_n', 'm3u8_native': 'm3u8_n',
'rtmp_ffmpeg': 'rtmp_f',
'http_dash_segments': 'dash', 'http_dash_segments': 'dash',
'niconico_dmc': 'dmc', 'niconico_dmc': 'dmc',
} }
@@ -54,6 +56,7 @@ def shorten_protocol_name(proto, simplify=False):
'https': 'http', 'https': 'http',
'ftps': 'ftp', 'ftps': 'ftp',
'm3u8_native': 'm3u8', 'm3u8_native': 'm3u8',
'rtmp_ffmpeg': 'rtmp',
'm3u8_frag_urls': 'm3u8', 'm3u8_frag_urls': 'm3u8',
'dash_frag_urls': 'dash', 'dash_frag_urls': 'dash',
}) })
@@ -80,12 +83,12 @@ def get_suitable_downloader(info_dict, params={}, default=HttpFD):
if ed.can_download(info_dict, external_downloader): if ed.can_download(info_dict, external_downloader):
return ed return ed
if protocol.startswith('m3u8'): if protocol in ('m3u8', 'm3u8_native'):
if info_dict.get('is_live'): if info_dict.get('is_live'):
return FFmpegFD return FFmpegFD
elif external_downloader == 'native': elif external_downloader == 'native':
return HlsFD return HlsFD
elif _get_real_downloader(info_dict, 'frag_urls', params, None): elif _get_real_downloader(info_dict, 'm3u8_frag_urls', params, None):
return HlsFD return HlsFD
elif params.get('hls_prefer_native') is True: elif params.get('hls_prefer_native') is True:
return HlsFD return HlsFD
+2 -2
View File
@@ -147,10 +147,10 @@ class FileDownloader(object):
return int(round(number * multiplier)) return int(round(number * multiplier))
def to_screen(self, *args, **kargs): def to_screen(self, *args, **kargs):
self.ydl.to_screen(*args, **kargs) self.ydl.to_stdout(*args, quiet=self.params.get('quiet'), **kargs)
def to_stderr(self, message): def to_stderr(self, message):
self.ydl.to_screen(message) self.ydl.to_stderr(message)
def to_console_title(self, message): def to_console_title(self, message):
self.ydl.to_console_title(message) self.ydl.to_console_title(message)
+11 -3
View File
@@ -1,5 +1,6 @@
from __future__ import unicode_literals from __future__ import unicode_literals
import errno
try: try:
import concurrent.futures import concurrent.futures
can_threaded_download = True can_threaded_download = True
@@ -112,12 +113,14 @@ class DashSegmentsFD(FragmentFD):
if count > fragment_retries: if count > fragment_retries:
if not fatal: if not fatal:
return False, frag_index return False, frag_index
ctx['dest_stream'].close()
self.report_error('Giving up after %s fragment retries' % fragment_retries) self.report_error('Giving up after %s fragment retries' % fragment_retries)
return False, frag_index return False, frag_index
return frag_content, frag_index return frag_content, frag_index
def append_fragment(frag_content, frag_index): def append_fragment(frag_content, frag_index):
fatal = frag_index == 1 or not skip_unavailable_fragments
if frag_content: if frag_content:
fragment_filename = '%s-Frag%d' % (ctx['tmpfilename'], frag_index) fragment_filename = '%s-Frag%d' % (ctx['tmpfilename'], frag_index)
try: try:
@@ -126,19 +129,24 @@ class DashSegmentsFD(FragmentFD):
file.close() file.close()
self._append_fragment(ctx, frag_content) self._append_fragment(ctx, frag_content)
return True return True
except FileNotFoundError: except EnvironmentError as ose:
if skip_unavailable_fragments: if ose.errno != errno.ENOENT:
raise
# FileNotFoundError
if not fatal:
self.report_skip_fragment(frag_index) self.report_skip_fragment(frag_index)
return True return True
else: else:
ctx['dest_stream'].close()
self.report_error( self.report_error(
'fragment %s not found, unable to continue' % frag_index) 'fragment %s not found, unable to continue' % frag_index)
return False return False
else: else:
if skip_unavailable_fragments: if not fatal:
self.report_skip_fragment(frag_index) self.report_skip_fragment(frag_index)
return True return True
else: else:
ctx['dest_stream'].close()
self.report_error( self.report_error(
'fragment %s not found, unable to continue' % frag_index) 'fragment %s not found, unable to continue' % frag_index)
return False return False
+11 -3
View File
@@ -290,11 +290,19 @@ class Aria2cFD(ExternalFD):
cmd += self._bool_option('--remote-time', 'updatetime', 'true', 'false', '=') cmd += self._bool_option('--remote-time', 'updatetime', 'true', 'false', '=')
cmd += self._configuration_args() cmd += self._configuration_args()
# aria2c strips out spaces from the beginning/end of filenames and paths.
# We work around this issue by adding a "./" to the beginning of the
# filename and relative path, and adding a "/" at the end of the path.
# See: https://github.com/yt-dlp/yt-dlp/issues/276
# https://github.com/ytdl-org/youtube-dl/issues/20312
# https://github.com/aria2/aria2/issues/1373
dn = os.path.dirname(tmpfilename) dn = os.path.dirname(tmpfilename)
if dn: if dn:
cmd += ['--dir', dn] if not os.path.isabs(dn):
dn = '.%s%s' % (os.path.sep, dn)
cmd += ['--dir', dn + os.path.sep]
if 'fragments' not in info_dict: if 'fragments' not in info_dict:
cmd += ['--out', os.path.basename(tmpfilename)] cmd += ['--out', '.%s%s' % (os.path.sep, os.path.basename(tmpfilename))]
cmd += ['--auto-file-renaming=false'] cmd += ['--auto-file-renaming=false']
if 'fragments' in info_dict: if 'fragments' in info_dict:
@@ -330,7 +338,7 @@ class HttpieFD(ExternalFD):
class FFmpegFD(ExternalFD): class FFmpegFD(ExternalFD):
SUPPORTED_PROTOCOLS = ('http', 'https', 'ftp', 'ftps', 'm3u8', 'rtsp', 'rtmp', 'mms') SUPPORTED_PROTOCOLS = ('http', 'https', 'ftp', 'ftps', 'm3u8', 'm3u8_native', 'rtsp', 'rtmp', 'rtmp_ffmpeg', 'mms')
@classmethod @classmethod
def available(cls, path=None): def available(cls, path=None):
+9 -4
View File
@@ -31,6 +31,7 @@ class FragmentFD(FileDownloader):
Skip unavailable fragments (DASH and hlsnative only) Skip unavailable fragments (DASH and hlsnative only)
keep_fragments: Keep downloaded fragments on disk after downloading is keep_fragments: Keep downloaded fragments on disk after downloading is
finished finished
_no_ytdl_file: Don't use .ytdl file
For each incomplete fragment download yt-dlp keeps on disk a special For each incomplete fragment download yt-dlp keeps on disk a special
bookkeeping file with download state and metadata (in future such files will bookkeeping file with download state and metadata (in future such files will
@@ -69,15 +70,17 @@ class FragmentFD(FileDownloader):
self._prepare_frag_download(ctx) self._prepare_frag_download(ctx)
self._start_frag_download(ctx) self._start_frag_download(ctx)
@staticmethod def __do_ytdl_file(self, ctx):
def __do_ytdl_file(ctx): return not ctx['live'] and not ctx['tmpfilename'] == '-' and not self.params.get('_no_ytdl_file')
return not ctx['live'] and not ctx['tmpfilename'] == '-'
def _read_ytdl_file(self, ctx): def _read_ytdl_file(self, ctx):
assert 'ytdl_corrupt' not in ctx assert 'ytdl_corrupt' not in ctx
stream, _ = sanitize_open(self.ytdl_filename(ctx['filename']), 'r') stream, _ = sanitize_open(self.ytdl_filename(ctx['filename']), 'r')
try: try:
ctx['fragment_index'] = json.loads(stream.read())['downloader']['current_fragment']['index'] ytdl_data = json.loads(stream.read())
ctx['fragment_index'] = ytdl_data['downloader']['current_fragment']['index']
if 'extra_state' in ytdl_data['downloader']:
ctx['extra_state'] = ytdl_data['downloader']['extra_state']
except Exception: except Exception:
ctx['ytdl_corrupt'] = True ctx['ytdl_corrupt'] = True
finally: finally:
@@ -90,6 +93,8 @@ class FragmentFD(FileDownloader):
'index': ctx['fragment_index'], 'index': ctx['fragment_index'],
}, },
} }
if 'extra_state' in ctx:
downloader['extra_state'] = ctx['extra_state']
if ctx.get('fragment_count') is not None: if ctx.get('fragment_count') is not None:
downloader['fragment_count'] = ctx['fragment_count'] downloader['fragment_count'] = ctx['fragment_count']
frag_index_stream.write(json.dumps({'downloader': downloader})) frag_index_stream.write(json.dumps({'downloader': downloader}))
+89 -3
View File
@@ -1,6 +1,8 @@
from __future__ import unicode_literals from __future__ import unicode_literals
import errno
import re import re
import io
import binascii import binascii
try: try:
from Crypto.Cipher import AES from Crypto.Cipher import AES
@@ -26,7 +28,9 @@ from ..utils import (
parse_m3u8_attributes, parse_m3u8_attributes,
sanitize_open, sanitize_open,
update_url_query, update_url_query,
bug_reports_message,
) )
from .. import webvtt
class HlsFD(FragmentFD): class HlsFD(FragmentFD):
@@ -77,6 +81,8 @@ class HlsFD(FragmentFD):
man_url = info_dict['url'] man_url = info_dict['url']
self.to_screen('[%s] Downloading m3u8 manifest' % self.FD_NAME) self.to_screen('[%s] Downloading m3u8 manifest' % self.FD_NAME)
is_webvtt = info_dict['ext'] == 'vtt'
urlh = self.ydl.urlopen(self._prepare_url(info_dict, man_url)) urlh = self.ydl.urlopen(self._prepare_url(info_dict, man_url))
man_url = urlh.geturl() man_url = urlh.geturl()
s = urlh.read().decode('utf-8', 'ignore') s = urlh.read().decode('utf-8', 'ignore')
@@ -141,6 +147,8 @@ class HlsFD(FragmentFD):
else: else:
self._prepare_and_start_frag_download(ctx) self._prepare_and_start_frag_download(ctx)
extra_state = ctx.setdefault('extra_state', {})
fragment_retries = self.params.get('fragment_retries', 0) fragment_retries = self.params.get('fragment_retries', 0)
skip_unavailable_fragments = self.params.get('skip_unavailable_fragments', True) skip_unavailable_fragments = self.params.get('skip_unavailable_fragments', True)
test = self.params.get('test', False) test = self.params.get('test', False)
@@ -291,6 +299,7 @@ class HlsFD(FragmentFD):
if count <= fragment_retries: if count <= fragment_retries:
self.report_retry_fragment(err, frag_index, count, fragment_retries) self.report_retry_fragment(err, frag_index, count, fragment_retries)
if count > fragment_retries: if count > fragment_retries:
ctx['dest_stream'].close()
self.report_error('Giving up after %s fragment retries' % fragment_retries) self.report_error('Giving up after %s fragment retries' % fragment_retries)
return False, frag_index return False, frag_index
@@ -307,28 +316,105 @@ class HlsFD(FragmentFD):
return frag_content, frag_index return frag_content, frag_index
pack_fragment = lambda frag_content, _: frag_content
if is_webvtt:
def pack_fragment(frag_content, frag_index):
output = io.StringIO()
adjust = 0
for block in webvtt.parse_fragment(frag_content):
if isinstance(block, webvtt.CueBlock):
block.start += adjust
block.end += adjust
dedup_window = extra_state.setdefault('webvtt_dedup_window', [])
cue = block.as_json
# skip the cue if an identical one appears
# in the window of potential duplicates
# and prune the window of unviable candidates
i = 0
skip = True
while i < len(dedup_window):
window_cue = dedup_window[i]
if window_cue == cue:
break
if window_cue['end'] >= cue['start']:
i += 1
continue
del dedup_window[i]
else:
skip = False
if skip:
continue
# add the cue to the window
dedup_window.append(cue)
elif isinstance(block, webvtt.Magic):
# take care of MPEG PES timestamp overflow
if block.mpegts is None:
block.mpegts = 0
extra_state.setdefault('webvtt_mpegts_adjust', 0)
block.mpegts += extra_state['webvtt_mpegts_adjust'] << 33
if block.mpegts < extra_state.get('webvtt_mpegts_last', 0):
extra_state['webvtt_mpegts_adjust'] += 1
block.mpegts += 1 << 33
extra_state['webvtt_mpegts_last'] = block.mpegts
if frag_index == 1:
extra_state['webvtt_mpegts'] = block.mpegts or 0
extra_state['webvtt_local'] = block.local or 0
# XXX: block.local = block.mpegts = None ?
else:
if block.mpegts is not None and block.local is not None:
adjust = (
(block.mpegts - extra_state.get('webvtt_mpegts', 0))
- (block.local - extra_state.get('webvtt_local', 0))
)
continue
elif isinstance(block, webvtt.HeaderBlock):
if frag_index != 1:
# XXX: this should probably be silent as well
# or verify that all segments contain the same data
self.report_warning(bug_reports_message(
'Discarding a %s block found in the middle of the stream; '
'if the subtitles display incorrectly,'
% (type(block).__name__)))
continue
block.write_into(output)
return output.getvalue().encode('utf-8')
def append_fragment(frag_content, frag_index): def append_fragment(frag_content, frag_index):
fatal = frag_index == 1 or not skip_unavailable_fragments
if frag_content: if frag_content:
fragment_filename = '%s-Frag%d' % (ctx['tmpfilename'], frag_index) fragment_filename = '%s-Frag%d' % (ctx['tmpfilename'], frag_index)
try: try:
file, frag_sanitized = sanitize_open(fragment_filename, 'rb') file, frag_sanitized = sanitize_open(fragment_filename, 'rb')
ctx['fragment_filename_sanitized'] = frag_sanitized ctx['fragment_filename_sanitized'] = frag_sanitized
file.close() file.close()
frag_content = pack_fragment(frag_content, frag_index)
self._append_fragment(ctx, frag_content) self._append_fragment(ctx, frag_content)
return True return True
except FileNotFoundError: except EnvironmentError as ose:
if skip_unavailable_fragments: if ose.errno != errno.ENOENT:
raise
# FileNotFoundError
if not fatal:
self.report_skip_fragment(frag_index) self.report_skip_fragment(frag_index)
return True return True
else: else:
ctx['dest_stream'].close()
self.report_error( self.report_error(
'fragment %s not found, unable to continue' % frag_index) 'fragment %s not found, unable to continue' % frag_index)
return False return False
else: else:
if skip_unavailable_fragments: if not fatal:
self.report_skip_fragment(frag_index) self.report_skip_fragment(frag_index)
return True return True
else: else:
ctx['dest_stream'].close()
self.report_error( self.report_error(
'fragment %s not found, unable to continue' % frag_index) 'fragment %s not found, unable to continue' % frag_index)
return False return False
+42 -12
View File
@@ -48,7 +48,7 @@ def write_piff_header(stream, params):
language = params.get('language', 'und') language = params.get('language', 'und')
height = params.get('height', 0) height = params.get('height', 0)
width = params.get('width', 0) width = params.get('width', 0)
is_audio = width == 0 and height == 0 stream_type = params['stream_type']
creation_time = modification_time = int(time.time()) creation_time = modification_time = int(time.time())
ftyp_payload = b'isml' # major brand ftyp_payload = b'isml' # major brand
@@ -77,7 +77,7 @@ def write_piff_header(stream, params):
tkhd_payload += u32.pack(0) * 2 # reserved tkhd_payload += u32.pack(0) * 2 # reserved
tkhd_payload += s16.pack(0) # layer tkhd_payload += s16.pack(0) # layer
tkhd_payload += s16.pack(0) # alternate group tkhd_payload += s16.pack(0) # alternate group
tkhd_payload += s88.pack(1 if is_audio else 0) # volume tkhd_payload += s88.pack(1 if stream_type == 'audio' else 0) # volume
tkhd_payload += u16.pack(0) # reserved tkhd_payload += u16.pack(0) # reserved
tkhd_payload += unity_matrix tkhd_payload += unity_matrix
tkhd_payload += u1616.pack(width) tkhd_payload += u1616.pack(width)
@@ -93,19 +93,34 @@ def write_piff_header(stream, params):
mdia_payload = full_box(b'mdhd', 1, 0, mdhd_payload) # Media Header Box mdia_payload = full_box(b'mdhd', 1, 0, mdhd_payload) # Media Header Box
hdlr_payload = u32.pack(0) # pre defined hdlr_payload = u32.pack(0) # pre defined
hdlr_payload += b'soun' if is_audio else b'vide' # handler type if stream_type == 'audio': # handler type
hdlr_payload += u32.pack(0) * 3 # reserved hdlr_payload += b'soun'
hdlr_payload += (b'Sound' if is_audio else b'Video') + b'Handler\0' # name hdlr_payload += u32.pack(0) * 3 # reserved
hdlr_payload += b'SoundHandler\0' # name
elif stream_type == 'video':
hdlr_payload += b'vide'
hdlr_payload += u32.pack(0) * 3 # reserved
hdlr_payload += b'VideoHandler\0' # name
elif stream_type == 'text':
hdlr_payload += b'subt'
hdlr_payload += u32.pack(0) * 3 # reserved
hdlr_payload += b'SubtitleHandler\0' # name
else:
assert False
mdia_payload += full_box(b'hdlr', 0, 0, hdlr_payload) # Handler Reference Box mdia_payload += full_box(b'hdlr', 0, 0, hdlr_payload) # Handler Reference Box
if is_audio: if stream_type == 'audio':
smhd_payload = s88.pack(0) # balance smhd_payload = s88.pack(0) # balance
smhd_payload += u16.pack(0) # reserved smhd_payload += u16.pack(0) # reserved
media_header_box = full_box(b'smhd', 0, 0, smhd_payload) # Sound Media Header media_header_box = full_box(b'smhd', 0, 0, smhd_payload) # Sound Media Header
else: elif stream_type == 'video':
vmhd_payload = u16.pack(0) # graphics mode vmhd_payload = u16.pack(0) # graphics mode
vmhd_payload += u16.pack(0) * 3 # opcolor vmhd_payload += u16.pack(0) * 3 # opcolor
media_header_box = full_box(b'vmhd', 0, 1, vmhd_payload) # Video Media Header media_header_box = full_box(b'vmhd', 0, 1, vmhd_payload) # Video Media Header
elif stream_type == 'text':
media_header_box = full_box(b'sthd', 0, 0, b'') # Subtitle Media Header
else:
assert False
minf_payload = media_header_box minf_payload = media_header_box
dref_payload = u32.pack(1) # entry count dref_payload = u32.pack(1) # entry count
@@ -117,7 +132,7 @@ def write_piff_header(stream, params):
sample_entry_payload = u8.pack(0) * 6 # reserved sample_entry_payload = u8.pack(0) * 6 # reserved
sample_entry_payload += u16.pack(1) # data reference index sample_entry_payload += u16.pack(1) # data reference index
if is_audio: if stream_type == 'audio':
sample_entry_payload += u32.pack(0) * 2 # reserved sample_entry_payload += u32.pack(0) * 2 # reserved
sample_entry_payload += u16.pack(params.get('channels', 2)) sample_entry_payload += u16.pack(params.get('channels', 2))
sample_entry_payload += u16.pack(params.get('bits_per_sample', 16)) sample_entry_payload += u16.pack(params.get('bits_per_sample', 16))
@@ -127,7 +142,7 @@ def write_piff_header(stream, params):
if fourcc == 'AACL': if fourcc == 'AACL':
sample_entry_box = box(b'mp4a', sample_entry_payload) sample_entry_box = box(b'mp4a', sample_entry_payload)
else: elif stream_type == 'video':
sample_entry_payload += u16.pack(0) # pre defined sample_entry_payload += u16.pack(0) # pre defined
sample_entry_payload += u16.pack(0) # reserved sample_entry_payload += u16.pack(0) # reserved
sample_entry_payload += u32.pack(0) * 3 # pre defined sample_entry_payload += u32.pack(0) * 3 # pre defined
@@ -155,6 +170,18 @@ def write_piff_header(stream, params):
avcc_payload += pps avcc_payload += pps
sample_entry_payload += box(b'avcC', avcc_payload) # AVC Decoder Configuration Record sample_entry_payload += box(b'avcC', avcc_payload) # AVC Decoder Configuration Record
sample_entry_box = box(b'avc1', sample_entry_payload) # AVC Simple Entry sample_entry_box = box(b'avc1', sample_entry_payload) # AVC Simple Entry
else:
assert False
elif stream_type == 'text':
if fourcc == 'TTML':
sample_entry_payload += b'http://www.w3.org/ns/ttml\0' # namespace
sample_entry_payload += b'\0' # schema location
sample_entry_payload += b'\0' # auxilary mime types(??)
sample_entry_box = box(b'stpp', sample_entry_payload)
else:
assert False
else:
assert False
stsd_payload += sample_entry_box stsd_payload += sample_entry_box
stbl_payload = full_box(b'stsd', 0, 0, stsd_payload) # Sample Description Box stbl_payload = full_box(b'stsd', 0, 0, stsd_payload) # Sample Description Box
@@ -221,10 +248,13 @@ class IsmFD(FragmentFD):
self._prepare_and_start_frag_download(ctx) self._prepare_and_start_frag_download(ctx)
extra_state = ctx.setdefault('extra_state', {
'ism_track_written': False,
})
fragment_retries = self.params.get('fragment_retries', 0) fragment_retries = self.params.get('fragment_retries', 0)
skip_unavailable_fragments = self.params.get('skip_unavailable_fragments', True) skip_unavailable_fragments = self.params.get('skip_unavailable_fragments', True)
track_written = False
frag_index = 0 frag_index = 0
for i, segment in enumerate(segments): for i, segment in enumerate(segments):
frag_index += 1 frag_index += 1
@@ -236,11 +266,11 @@ class IsmFD(FragmentFD):
success, frag_content = self._download_fragment(ctx, segment['url'], info_dict) success, frag_content = self._download_fragment(ctx, segment['url'], info_dict)
if not success: if not success:
return False return False
if not track_written: if not extra_state['ism_track_written']:
tfhd_data = extract_box_data(frag_content, [b'moof', b'traf', b'tfhd']) tfhd_data = extract_box_data(frag_content, [b'moof', b'traf', b'tfhd'])
info_dict['_download_params']['track_id'] = u32.unpack(tfhd_data[4:8])[0] info_dict['_download_params']['track_id'] = u32.unpack(tfhd_data[4:8])[0]
write_piff_header(ctx['dest_stream'], info_dict['_download_params']) write_piff_header(ctx['dest_stream'], info_dict['_download_params'])
track_written = True extra_state['ism_track_written'] = True
self._append_fragment(ctx, frag_content) self._append_fragment(ctx, frag_content)
break break
except compat_urllib_error.HTTPError as err: except compat_urllib_error.HTTPError as err:
+6 -5
View File
@@ -24,16 +24,14 @@ class NiconicoDmcFD(FileDownloader):
success = download_complete = False success = download_complete = False
timer = [None] timer = [None]
heartbeat_lock = threading.Lock() heartbeat_lock = threading.Lock()
heartbeat_url = heartbeat_info_dict['url'] heartbeat_url = heartbeat_info_dict['url']
heartbeat_data = heartbeat_info_dict['data'] heartbeat_data = heartbeat_info_dict['data'].encode()
heartbeat_interval = heartbeat_info_dict.get('interval', 30) heartbeat_interval = heartbeat_info_dict.get('interval', 30)
self.to_screen('[%s] Heartbeat with %s second interval ...' % (self.FD_NAME, heartbeat_interval))
def heartbeat(): def heartbeat():
try: try:
compat_urllib_request.urlopen(url=heartbeat_url, data=heartbeat_data.encode()) compat_urllib_request.urlopen(url=heartbeat_url, data=heartbeat_data)
except Exception: except Exception:
self.to_screen('[%s] Heartbeat failed' % self.FD_NAME) self.to_screen('[%s] Heartbeat failed' % self.FD_NAME)
@@ -42,13 +40,16 @@ class NiconicoDmcFD(FileDownloader):
timer[0] = threading.Timer(heartbeat_interval, heartbeat) timer[0] = threading.Timer(heartbeat_interval, heartbeat)
timer[0].start() timer[0].start()
heartbeat_info_dict['ping']()
self.to_screen('[%s] Heartbeat with %d second interval ...' % (self.FD_NAME, heartbeat_interval))
try: try:
heartbeat() 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) success = fd.real_download(filename, info_dict)
finally: finally:
if heartbeat_lock: if heartbeat_lock:
with heartbeat_lock: with heartbeat_lock:
timer[0].cancel() timer[0].cancel()
download_complete = True download_complete = True
return success return success
+3 -3
View File
@@ -12,9 +12,6 @@ except ImportError:
if not _LAZY_LOADER: if not _LAZY_LOADER:
from .extractors import * from .extractors import *
_PLUGIN_CLASSES = load_plugins('extractor', 'IE', globals())
_ALL_CLASSES = [ _ALL_CLASSES = [
klass klass
for name, klass in globals().items() for name, klass in globals().items()
@@ -22,6 +19,9 @@ if not _LAZY_LOADER:
] ]
_ALL_CLASSES.append(GenericIE) _ALL_CLASSES.append(GenericIE)
_PLUGIN_CLASSES = load_plugins('extractor', 'IE', globals())
_ALL_CLASSES = _PLUGIN_CLASSES + _ALL_CLASSES
def gen_extractor_classes(): def gen_extractor_classes():
""" Return a list of supported extractors. """ Return a list of supported extractors.
+2 -2
View File
@@ -257,7 +257,7 @@ class AfreecaTVIE(InfoExtractor):
if flag and flag == 'SUCCEED': if flag and flag == 'SUCCEED':
break break
if flag == 'PARTIAL_ADULT': if flag == 'PARTIAL_ADULT':
self._downloader.report_warning( self.report_warning(
'In accordance with local laws and regulations, underage users are restricted from watching adult content. ' 'In accordance with local laws and regulations, underage users are restricted from watching adult content. '
'Only content suitable for all ages will be downloaded. ' 'Only content suitable for all ages will be downloaded. '
'Provide account credentials if you wish to download restricted content.') 'Provide account credentials if you wish to download restricted content.')
@@ -323,7 +323,7 @@ class AfreecaTVIE(InfoExtractor):
'url': file_url, 'url': file_url,
'format_id': 'http', 'format_id': 'http',
}] }]
if not formats: if not formats and not self._downloader.params.get('ignore_no_formats'):
continue continue
self._sort_formats(formats) self._sort_formats(formats)
file_info = common_entry.copy() file_info = common_entry.copy()
+2 -2
View File
@@ -36,12 +36,12 @@ class ARDMediathekBaseIE(InfoExtractor):
if not formats: if not formats:
if fsk: if fsk:
raise ExtractorError( self.raise_no_formats(
'This video is only available after 20:00', expected=True) 'This video is only available after 20:00', expected=True)
elif media_info.get('_geoblocked'): elif media_info.get('_geoblocked'):
self.raise_geo_restricted( self.raise_geo_restricted(
'This video is not available due to geoblocking', 'This video is not available due to geoblocking',
countries=self._GEO_COUNTRIES) countries=self._GEO_COUNTRIES, metadata_available=True)
self._sort_formats(formats) self._sort_formats(formats)
+6 -4
View File
@@ -86,18 +86,19 @@ class AtresPlayerIE(InfoExtractor):
title = episode['titulo'] title = episode['titulo']
formats = [] formats = []
subtitles = {}
for source in episode.get('sources', []): for source in episode.get('sources', []):
src = source.get('src') src = source.get('src')
if not src: if not src:
continue continue
src_type = source.get('type') src_type = source.get('type')
if src_type == 'application/vnd.apple.mpegurl': if src_type == 'application/vnd.apple.mpegurl':
formats.extend(self._extract_m3u8_formats( formats, subtitles = self._extract_m3u8_formats(
src, video_id, 'mp4', 'm3u8_native', src, video_id, 'mp4', 'm3u8_native',
m3u8_id='hls', fatal=False)) m3u8_id='hls', fatal=False)
elif src_type == 'application/dash+xml': elif src_type == 'application/dash+xml':
formats.extend(self._extract_mpd_formats( formats, subtitles = self._extract_mpd_formats(
src, video_id, mpd_id='dash', fatal=False)) src, video_id, mpd_id='dash', fatal=False)
self._sort_formats(formats) self._sort_formats(formats)
heartbeat = episode.get('heartbeat') or {} heartbeat = episode.get('heartbeat') or {}
@@ -115,4 +116,5 @@ class AtresPlayerIE(InfoExtractor):
'channel': get_meta('channel'), 'channel': get_meta('channel'),
'season': get_meta('season'), 'season': get_meta('season'),
'episode_number': int_or_none(get_meta('episodeNumber')), 'episode_number': int_or_none(get_meta('episodeNumber')),
'subtitles': subtitles,
} }
+31 -2
View File
@@ -11,6 +11,7 @@ from ..compat import (
compat_etree_Element, compat_etree_Element,
compat_HTTPError, compat_HTTPError,
compat_parse_qs, compat_parse_qs,
compat_str,
compat_urllib_parse_urlparse, compat_urllib_parse_urlparse,
compat_urlparse, compat_urlparse,
) )
@@ -25,8 +26,10 @@ from ..utils import (
js_to_json, js_to_json,
parse_duration, parse_duration,
parse_iso8601, parse_iso8601,
strip_or_none,
try_get, try_get,
unescapeHTML, unescapeHTML,
unified_timestamp,
url_or_none, url_or_none,
urlencode_postdata, urlencode_postdata,
urljoin, urljoin,
@@ -761,8 +764,17 @@ class BBCIE(BBCCoUkIE):
'only_matching': True, 'only_matching': True,
}, { }, {
# custom redirection to www.bbc.com # custom redirection to www.bbc.com
# also, video with window.__INITIAL_DATA__
'url': 'http://www.bbc.co.uk/news/science-environment-33661876', 'url': 'http://www.bbc.co.uk/news/science-environment-33661876',
'only_matching': True, 'info_dict': {
'id': 'p02xzws1',
'ext': 'mp4',
'title': "Pluto may have 'nitrogen glaciers'",
'description': 'md5:6a95b593f528d7a5f2605221bc56912f',
'thumbnail': r're:https?://.+/.+\.jpg',
'timestamp': 1437785037,
'upload_date': '20150725',
},
}, { }, {
# single video article embedded with data-media-vpid # single video article embedded with data-media-vpid
'url': 'http://www.bbc.co.uk/sport/rowing/35908187', 'url': 'http://www.bbc.co.uk/sport/rowing/35908187',
@@ -1164,12 +1176,29 @@ class BBCIE(BBCCoUkIE):
continue continue
formats, subtitles = self._download_media_selector(item_id) formats, subtitles = self._download_media_selector(item_id)
self._sort_formats(formats) self._sort_formats(formats)
item_desc = None
blocks = try_get(media, lambda x: x['summary']['blocks'], list)
if blocks:
summary = []
for block in blocks:
text = try_get(block, lambda x: x['model']['text'], compat_str)
if text:
summary.append(text)
if summary:
item_desc = '\n\n'.join(summary)
item_time = None
for meta in try_get(media, lambda x: x['metadata']['items'], list) or []:
if try_get(meta, lambda x: x['label']) == 'Published':
item_time = unified_timestamp(meta.get('timestamp'))
break
entries.append({ entries.append({
'id': item_id, 'id': item_id,
'title': item_title, 'title': item_title,
'thumbnail': item.get('holdingImageUrl'), 'thumbnail': item.get('holdingImageUrl'),
'formats': formats, 'formats': formats,
'subtitles': subtitles, 'subtitles': subtitles,
'timestamp': item_time,
'description': strip_or_none(item_desc),
}) })
for resp in (initial_data.get('data') or {}).values(): for resp in (initial_data.get('data') or {}).values():
name = resp.get('name') name = resp.get('name')
@@ -1242,7 +1271,7 @@ class BBCIE(BBCCoUkIE):
entries = [] entries = []
for num, media_meta in enumerate(medias, start=1): for num, media_meta in enumerate(medias, start=1):
formats, subtitles = self._extract_from_media_meta(media_meta, playlist_id) formats, subtitles = self._extract_from_media_meta(media_meta, playlist_id)
if not formats: if not formats and not self._downloader.params.get('ignore_no_formats'):
continue continue
self._sort_formats(formats) self._sort_formats(formats)
+32 -19
View File
@@ -2,6 +2,7 @@
from __future__ import unicode_literals from __future__ import unicode_literals
import hashlib import hashlib
import itertools
import json import json
import re import re
@@ -498,28 +499,40 @@ class BiliBiliBangumiIE(InfoExtractor):
class BilibiliChannelIE(InfoExtractor): class BilibiliChannelIE(InfoExtractor):
_VALID_URL = r'https?://space.bilibili\.com/(?P<id>\d+)' _VALID_URL = r'https?://space.bilibili\.com/(?P<id>\d+)'
# May need to add support for pagination? Need to find a user with many video uploads to test _API_URL = "https://api.bilibili.com/x/space/arc/search?mid=%s&pn=%d&jsonp=jsonp"
_API_URL = "https://api.bilibili.com/x/space/arc/search?mid=%s&pn=1&ps=25&jsonp=jsonp" _TESTS = [{
_TEST = {} # TODO: Add tests 'url': 'https://space.bilibili.com/3985676/video',
'info_dict': {},
'playlist_mincount': 112,
}]
def _entries(self, list_id):
count, max_count = 0, None
for page_num in itertools.count(1):
data = self._parse_json(
self._download_webpage(
self._API_URL % (list_id, page_num), list_id,
note='Downloading page %d' % page_num),
list_id)['data']
max_count = max_count or try_get(data, lambda x: x['page']['count'])
entries = try_get(data, lambda x: x['list']['vlist'])
if not entries:
return
for entry in entries:
yield self.url_result(
'https://www.bilibili.com/video/%s' % entry['bvid'],
BiliBiliIE.ie_key(), entry['bvid'])
count += len(entries)
if max_count and count >= max_count:
return
def _real_extract(self, url): def _real_extract(self, url):
list_id = self._match_id(url) list_id = self._match_id(url)
json_str = self._download_webpage(self._API_URL % list_id, "None") return self.playlist_result(self._entries(list_id), list_id)
json_parsed = json.loads(json_str)
entries = [{
'_type': 'url',
'ie_key': BiliBiliIE.ie_key(),
'url': ('https://www.bilibili.com/video/%s' %
entry['bvid']),
'id': entry['bvid'],
} for entry in json_parsed['data']['list']['vlist']]
return {
'_type': 'playlist',
'id': list_id,
'entries': entries
}
class BiliBiliSearchIE(SearchInfoExtractor): class BiliBiliSearchIE(SearchInfoExtractor):
+2 -2
View File
@@ -78,8 +78,8 @@ class BlinkxIE(InfoExtractor):
'fullid': video_id, 'fullid': video_id,
'title': data['title'], 'title': data['title'],
'formats': formats, 'formats': formats,
'uploader': data['channel_name'], 'uploader': data.get('channel_name'),
'timestamp': data['pubdate_epoch'], 'timestamp': data.get('pubdate_epoch'),
'description': data.get('description'), 'description': data.get('description'),
'thumbnails': thumbnails, 'thumbnails': thumbnails,
'duration': duration, 'duration': duration,
+1 -1
View File
@@ -114,7 +114,7 @@ class BRIE(InfoExtractor):
medias.append(media) medias.append(media)
if len(medias) > 1: if len(medias) > 1:
self._downloader.report_warning( self.report_warning(
'found multiple medias; please ' 'found multiple medias; please '
'report this with the video URL to http://yt-dl.org/bug') 'report this with the video URL to http://yt-dl.org/bug')
if not medias: if not medias:
+2 -2
View File
@@ -545,9 +545,9 @@ class BrightcoveNewIE(AdobePassIE):
errors = json_data.get('errors') errors = json_data.get('errors')
if errors: if errors:
error = errors[0] error = errors[0]
raise ExtractorError( self.raise_no_formats(
error.get('message') or error.get('error_subcode') or error['error_code'], expected=True) error.get('message') or error.get('error_subcode') or error['error_code'], expected=True)
if (not self._downloader.params.get('allow_unplayable_formats') elif (not self._downloader.params.get('allow_unplayable_formats')
and sources and num_drm_sources == len(sources)): and sources and num_drm_sources == len(sources)):
raise ExtractorError('This video is DRM protected.', expected=True) raise ExtractorError('This video is DRM protected.', expected=True)
+10 -4
View File
@@ -82,6 +82,7 @@ class BYUtvIE(InfoExtractor):
info = {} info = {}
formats = [] formats = []
subtitles = {}
for format_id, ep in video.items(): for format_id, ep in video.items():
if not isinstance(ep, dict): if not isinstance(ep, dict):
continue continue
@@ -90,12 +91,16 @@ class BYUtvIE(InfoExtractor):
continue continue
ext = determine_ext(video_url) ext = determine_ext(video_url)
if ext == 'm3u8': if ext == 'm3u8':
formats.extend(self._extract_m3u8_formats( m3u8_fmts, m3u8_subs = self._extract_m3u8_formats_and_subtitles(
video_url, video_id, 'mp4', entry_protocol='m3u8_native', video_url, video_id, 'mp4', entry_protocol='m3u8_native',
m3u8_id='hls', fatal=False)) m3u8_id='hls', fatal=False)
formats.extend(m3u8_fmts)
subtitles = self._merge_subtitles(subtitles, m3u8_subs)
elif ext == 'mpd': elif ext == 'mpd':
formats.extend(self._extract_mpd_formats( mpd_fmts, mpd_subs = self._extract_mpd_formats_and_subtitles(
video_url, video_id, mpd_id='dash', fatal=False)) video_url, video_id, mpd_id='dash', fatal=False)
formats.extend(mpd_fmts)
subtitles = self._merge_subtitles(subtitles, mpd_subs)
else: else:
formats.append({ formats.append({
'url': video_url, 'url': video_url,
@@ -114,4 +119,5 @@ class BYUtvIE(InfoExtractor):
'display_id': display_id, 'display_id': display_id,
'title': display_id, 'title': display_id,
'formats': formats, 'formats': formats,
'subtitles': subtitles,
}) })
+13 -7
View File
@@ -83,24 +83,31 @@ class CanvasIE(InfoExtractor):
description = data.get('description') description = data.get('description')
formats = [] formats = []
subtitles = {}
for target in data['targetUrls']: for target in data['targetUrls']:
format_url, format_type = url_or_none(target.get('url')), str_or_none(target.get('type')) format_url, format_type = url_or_none(target.get('url')), str_or_none(target.get('type'))
if not format_url or not format_type: if not format_url or not format_type:
continue continue
format_type = format_type.upper() format_type = format_type.upper()
if format_type in self._HLS_ENTRY_PROTOCOLS_MAP: if format_type in self._HLS_ENTRY_PROTOCOLS_MAP:
formats.extend(self._extract_m3u8_formats( fmts, subs = self._extract_m3u8_formats_and_subtitles(
format_url, video_id, 'mp4', self._HLS_ENTRY_PROTOCOLS_MAP[format_type], format_url, video_id, 'mp4', self._HLS_ENTRY_PROTOCOLS_MAP[format_type],
m3u8_id=format_type, fatal=False)) m3u8_id=format_type, fatal=False)
formats.extend(fmts)
subtitles = self._merge_subtitles(subtitles, subs)
elif format_type == 'HDS': elif format_type == 'HDS':
formats.extend(self._extract_f4m_formats( formats.extend(self._extract_f4m_formats(
format_url, video_id, f4m_id=format_type, fatal=False)) format_url, video_id, f4m_id=format_type, fatal=False))
elif format_type == 'MPEG_DASH': elif format_type == 'MPEG_DASH':
formats.extend(self._extract_mpd_formats( fmts, subs = self._extract_mpd_formats_and_subtitles(
format_url, video_id, mpd_id=format_type, fatal=False)) format_url, video_id, mpd_id=format_type, fatal=False)
formats.extend(fmts)
subtitles = self._merge_subtitles(subtitles, subs)
elif format_type == 'HSS': elif format_type == 'HSS':
formats.extend(self._extract_ism_formats( fmts, subs = self._extract_ism_formats_and_subtitles(
format_url, video_id, ism_id='mss', fatal=False)) format_url, video_id, ism_id='mss', fatal=False)
formats.extend(fmts)
subtitles = self._merge_subtitles(subtitles, subs)
else: else:
formats.append({ formats.append({
'format_id': format_type, 'format_id': format_type,
@@ -108,7 +115,6 @@ class CanvasIE(InfoExtractor):
}) })
self._sort_formats(formats) self._sort_formats(formats)
subtitles = {}
subtitle_urls = data.get('subtitleUrls') subtitle_urls = data.get('subtitleUrls')
if isinstance(subtitle_urls, list): if isinstance(subtitle_urls, list):
for subtitle in subtitle_urls: for subtitle in subtitle_urls:
+10 -1
View File
@@ -27,7 +27,13 @@ class CBSBaseIE(ThePlatformFeedIE):
class CBSIE(CBSBaseIE): class CBSIE(CBSBaseIE):
_VALID_URL = r'(?:cbs:|https?://(?:www\.)?(?:(?:cbs|paramountplus)\.com/shows/[^/]+/video|colbertlateshow\.com/(?:video|podcasts))/)(?P<id>[\w-]+)' _VALID_URL = r'''(?x)
(?:
cbs:|
https?://(?:www\.)?(?:
(?:cbs|paramountplus)\.com/(?:shows/[^/]+/video|movies/[^/]+)/|
colbertlateshow\.com/(?:video|podcasts)/)
)(?P<id>[\w-]+)'''
_TESTS = [{ _TESTS = [{
'url': 'https://www.cbs.com/shows/garth-brooks/video/_u7W953k6la293J7EPTd9oHkSPs6Xn6_/connect-chat-feat-garth-brooks/', 'url': 'https://www.cbs.com/shows/garth-brooks/video/_u7W953k6la293J7EPTd9oHkSPs6Xn6_/connect-chat-feat-garth-brooks/',
@@ -55,6 +61,9 @@ class CBSIE(CBSBaseIE):
}, { }, {
'url': 'https://www.paramountplus.com/shows/all-rise/video/QmR1WhNkh1a_IrdHZrbcRklm176X_rVc/all-rise-space/', 'url': 'https://www.paramountplus.com/shows/all-rise/video/QmR1WhNkh1a_IrdHZrbcRklm176X_rVc/all-rise-space/',
'only_matching': True, 'only_matching': True,
}, {
'url': 'https://www.paramountplus.com/movies/million-dollar-american-princesses-meghan-and-harry/C0LpgNwXYeB8txxycdWdR9TjxpJOsdCq',
'only_matching': True,
}] }]
def _extract_video_info(self, content_id, site='cbs', mpx_acc=2198311517): def _extract_video_info(self, content_id, site='cbs', mpx_acc=2198311517):
+1 -1
View File
@@ -26,7 +26,7 @@ class CBSNewsEmbedIE(CBSIE):
def _real_extract(self, url): def _real_extract(self, url):
item = self._parse_json(zlib.decompress(compat_b64decode( item = self._parse_json(zlib.decompress(compat_b64decode(
compat_urllib_parse_unquote(self._match_id(url))), compat_urllib_parse_unquote(self._match_id(url))),
-zlib.MAX_WBITS), None)['video']['items'][0] -zlib.MAX_WBITS).decode('utf-8'), None)['video']['items'][0]
return self._extract_video_info(item['mpxRefId'], 'cbsnews') return self._extract_video_info(item['mpxRefId'], 'cbsnews')
+3 -3
View File
@@ -133,6 +133,8 @@ class CDAIE(InfoExtractor):
'age_limit': 18 if need_confirm_age else 0, 'age_limit': 18 if need_confirm_age else 0,
} }
info = self._search_json_ld(webpage, video_id, default={})
# Source: https://www.cda.pl/js/player.js?t=1606154898 # Source: https://www.cda.pl/js/player.js?t=1606154898
def decrypt_file(a): def decrypt_file(a):
for p in ('_XDDD', '_CDA', '_ADC', '_CXD', '_QWE', '_Q5', '_IKSDE'): for p in ('_XDDD', '_CDA', '_ADC', '_CXD', '_QWE', '_Q5', '_IKSDE'):
@@ -197,7 +199,7 @@ class CDAIE(InfoExtractor):
handler = self._download_webpage handler = self._download_webpage
webpage = handler( webpage = handler(
self._BASE_URL + href, video_id, urljoin(self._BASE_URL, href), video_id,
'Downloading %s version information' % resolution, fatal=False) 'Downloading %s version information' % resolution, fatal=False)
if not webpage: if not webpage:
# Manually report warning because empty page is returned when # Manually report warning because empty page is returned when
@@ -209,6 +211,4 @@ class CDAIE(InfoExtractor):
self._sort_formats(formats) self._sort_formats(formats)
info = self._search_json_ld(webpage, video_id, default={})
return merge_dicts(info_dict, info) return merge_dicts(info_dict, info)
+2 -4
View File
@@ -5,7 +5,6 @@ import re
from .common import InfoExtractor from .common import InfoExtractor
from ..utils import ( from ..utils import (
clean_html, clean_html,
ExtractorError,
int_or_none, int_or_none,
parse_iso8601, parse_iso8601,
qualities, qualities,
@@ -187,14 +186,13 @@ class Channel9IE(InfoExtractor):
'quality': quality(q, q_url), 'quality': quality(q, q_url),
}) })
self._sort_formats(formats)
slides = content_data.get('Slides') slides = content_data.get('Slides')
zip_file = content_data.get('ZipFile') zip_file = content_data.get('ZipFile')
if not formats and not slides and not zip_file: if not formats and not slides and not zip_file:
raise ExtractorError( self.raise_no_formats(
'None of recording, slides or zip are available for %s' % content_path) 'None of recording, slides or zip are available for %s' % content_path)
self._sort_formats(formats)
subtitles = {} subtitles = {}
for caption in content_data.get('Captions', []): for caption in content_data.get('Captions', []):
+239 -112
View File
@@ -9,8 +9,6 @@ import netrc
import os import os
import random import random
import re import re
import socket
import ssl
import sys import sys
import time import time
import math import math
@@ -58,6 +56,7 @@ from ..utils import (
js_to_json, js_to_json,
JSON_LD_RE, JSON_LD_RE,
mimetype2ext, mimetype2ext,
network_exceptions,
orderedSet, orderedSet,
parse_bitrate, parse_bitrate,
parse_codecs, parse_codecs,
@@ -157,7 +156,7 @@ class InfoExtractor(object):
* player_url SWF Player URL (used for rtmpdump). * player_url SWF Player URL (used for rtmpdump).
* protocol The protocol that will be used for the actual * protocol The protocol that will be used for the actual
download, lower-case. download, lower-case.
"http", "https", "rtsp", "rtmp", "rtmpe", "http", "https", "rtsp", "rtmp", "rtmp_ffmpeg", "rtmpe",
"m3u8", "m3u8_native" or "http_dash_segments". "m3u8", "m3u8_native" or "http_dash_segments".
* fragment_base_url * fragment_base_url
Base URL for fragments. Each fragment's path Base URL for fragments. Each fragment's path
@@ -558,6 +557,10 @@ class InfoExtractor(object):
ie_result = self._real_extract(url) ie_result = self._real_extract(url)
if self._x_forwarded_for_ip: if self._x_forwarded_for_ip:
ie_result['__x_forwarded_for_ip'] = self._x_forwarded_for_ip ie_result['__x_forwarded_for_ip'] = self._x_forwarded_for_ip
subtitles = ie_result.get('subtitles')
if (subtitles and 'live_chat' in subtitles
and 'no-live-chat' in self._downloader.params.get('compat_opts')):
del subtitles['live_chat']
return ie_result return ie_result
except GeoRestrictedError as e: except GeoRestrictedError as e:
if self.__maybe_fake_ip_and_retry(e.countries): if self.__maybe_fake_ip_and_retry(e.countries):
@@ -659,12 +662,9 @@ class InfoExtractor(object):
url_or_request = update_url_query(url_or_request, query) url_or_request = update_url_query(url_or_request, query)
if data is not None or headers: if data is not None or headers:
url_or_request = sanitized_Request(url_or_request, data, headers) url_or_request = sanitized_Request(url_or_request, data, headers)
exceptions = [compat_urllib_error.URLError, compat_http_client.HTTPException, socket.error]
if hasattr(ssl, 'CertificateError'):
exceptions.append(ssl.CertificateError)
try: try:
return self._downloader.urlopen(url_or_request) return self._downloader.urlopen(url_or_request)
except tuple(exceptions) as err: except network_exceptions as err:
if isinstance(err, compat_urllib_error.HTTPError): if isinstance(err, compat_urllib_error.HTTPError):
if self.__can_accept_status_code(err, expected_status): if self.__can_accept_status_code(err, expected_status):
# Retain reference to error to prevent file object from # Retain reference to error to prevent file object from
@@ -683,7 +683,7 @@ class InfoExtractor(object):
if fatal: if fatal:
raise ExtractorError(errmsg, sys.exc_info()[2], cause=err) raise ExtractorError(errmsg, sys.exc_info()[2], cause=err)
else: else:
self._downloader.report_warning(errmsg) self.report_warning(errmsg)
return False return False
def _download_webpage_handle(self, url_or_request, video_id, note=None, errnote=None, fatal=True, encoding=None, data=None, headers={}, query={}, expected_status=None): def _download_webpage_handle(self, url_or_request, video_id, note=None, errnote=None, fatal=True, encoding=None, data=None, headers={}, query={}, expected_status=None):
@@ -968,15 +968,27 @@ class InfoExtractor(object):
"""Report attempt to log in.""" """Report attempt to log in."""
self.to_screen('Logging in') self.to_screen('Logging in')
@staticmethod def raise_login_required(
def raise_login_required(msg='This video is only available for registered users'): self, msg='This video is only available for registered users', metadata_available=False):
if metadata_available and self._downloader.params.get('ignore_no_formats_error'):
self.report_warning(msg)
raise ExtractorError( raise ExtractorError(
'%s. Use --username and --password or --netrc to provide account credentials.' % msg, '%s. Use --cookies, --username and --password or --netrc to provide account credentials' % msg,
expected=True) expected=True)
@staticmethod def raise_geo_restricted(
def raise_geo_restricted(msg='This video is not available from your location due to geo restriction', countries=None): self, msg='This video is not available from your location due to geo restriction',
raise GeoRestrictedError(msg, countries=countries) countries=None, metadata_available=False):
if metadata_available and self._downloader.params.get('ignore_no_formats_error'):
self.report_warning(msg)
else:
raise GeoRestrictedError(msg, countries=countries)
def raise_no_formats(self, msg, expected=False, video_id=None):
if expected and self._downloader.params.get('ignore_no_formats_error'):
self.report_warning(msg, video_id)
else:
raise ExtractorError(msg, expected=expected, video_id=video_id)
# Methods for following #608 # Methods for following #608
@staticmethod @staticmethod
@@ -1044,7 +1056,7 @@ class InfoExtractor(object):
elif fatal: elif fatal:
raise RegexNotFoundError('Unable to extract %s' % _name) raise RegexNotFoundError('Unable to extract %s' % _name)
else: else:
self._downloader.report_warning('unable to extract %s' % _name + bug_reports_message()) self.report_warning('unable to extract %s' % _name + bug_reports_message())
return None return None
def _html_search_regex(self, pattern, string, name, default=NO_DEFAULT, fatal=True, flags=0, group=None): def _html_search_regex(self, pattern, string, name, default=NO_DEFAULT, fatal=True, flags=0, group=None):
@@ -1072,7 +1084,7 @@ class InfoExtractor(object):
raise netrc.NetrcParseError( raise netrc.NetrcParseError(
'No authenticators for %s' % netrc_machine) 'No authenticators for %s' % netrc_machine)
except (IOError, netrc.NetrcParseError) as err: except (IOError, netrc.NetrcParseError) as err:
self._downloader.report_warning( self.report_warning(
'parsing .netrc: %s' % error_to_compat_str(err)) 'parsing .netrc: %s' % error_to_compat_str(err))
return username, password return username, password
@@ -1247,7 +1259,7 @@ class InfoExtractor(object):
elif fatal: elif fatal:
raise RegexNotFoundError('Unable to extract JSON-LD') raise RegexNotFoundError('Unable to extract JSON-LD')
else: else:
self._downloader.report_warning('unable to extract JSON-LD %s' % bug_reports_message()) self.report_warning('unable to extract JSON-LD %s' % bug_reports_message())
return {} return {}
def _json_ld(self, json_ld, video_id, fatal=True, expected_type=None): def _json_ld(self, json_ld, video_id, fatal=True, expected_type=None):
@@ -1407,7 +1419,10 @@ class InfoExtractor(object):
default = ('hidden', 'hasvid', 'ie_pref', 'lang', 'quality', default = ('hidden', 'hasvid', 'ie_pref', 'lang', 'quality',
'res', 'fps', 'codec:vp9.2', 'size', 'br', 'asr', 'res', 'fps', 'codec:vp9.2', 'size', 'br', 'asr',
'proto', 'ext', 'has_audio', 'source', 'format_id') # These must not be aliases 'proto', 'ext', 'hasaud', 'source', 'format_id') # These must not be aliases
ytdl_default = ('hasaud', 'quality', 'tbr', 'filesize', 'vbr',
'height', 'width', 'proto', 'vext', 'abr', 'aext',
'fps', 'fs_approx', 'source', 'format_id')
settings = { settings = {
'vcodec': {'type': 'ordered', 'regex': True, 'vcodec': {'type': 'ordered', 'regex': True,
@@ -1427,7 +1442,7 @@ class InfoExtractor(object):
'hasvid': {'priority': True, 'field': 'vcodec', 'type': 'boolean', 'not_in_list': ('none',)}, 'hasvid': {'priority': True, 'field': 'vcodec', 'type': 'boolean', 'not_in_list': ('none',)},
'hasaud': {'field': 'acodec', 'type': 'boolean', 'not_in_list': ('none',)}, 'hasaud': {'field': 'acodec', 'type': 'boolean', 'not_in_list': ('none',)},
'lang': {'priority': True, 'convert': 'ignore', 'field': 'language_preference'}, 'lang': {'priority': True, 'convert': 'ignore', 'field': 'language_preference'},
'quality': {'convert': 'float_none'}, 'quality': {'convert': 'float_none', 'default': -1},
'filesize': {'convert': 'bytes'}, 'filesize': {'convert': 'bytes'},
'fs_approx': {'convert': 'bytes', 'field': 'filesize_approx'}, 'fs_approx': {'convert': 'bytes', 'field': 'filesize_approx'},
'id': {'convert': 'string', 'field': 'format_id'}, 'id': {'convert': 'string', 'field': 'format_id'},
@@ -1581,7 +1596,7 @@ class InfoExtractor(object):
def print_verbose_info(self, to_screen): def print_verbose_info(self, to_screen):
if self._sort_user: if self._sort_user:
to_screen('[debug] Sort order given by user: %s' % ','.join(self._sort_user)) to_screen('[debug] Sort order given by user: %s' % ', '.join(self._sort_user))
if self._sort_extractor: if self._sort_extractor:
to_screen('[debug] Sort order given by extractor: %s' % ', '.join(self._sort_extractor)) to_screen('[debug] Sort order given by extractor: %s' % ', '.join(self._sort_extractor))
to_screen('[debug] Formats sorted by: %s' % ', '.join(['%s%s%s' % ( to_screen('[debug] Formats sorted by: %s' % ', '.join(['%s%s%s' % (
@@ -1609,7 +1624,7 @@ class InfoExtractor(object):
value = self._resolve_field_value(field, value, True) value = self._resolve_field_value(field, value, True)
# try to convert to number # try to convert to number
val_num = float_or_none(value) val_num = float_or_none(value, default=self._get_field_setting(field, 'default'))
is_num = self._get_field_setting(field, 'convert') != 'string' and val_num is not None is_num = self._get_field_setting(field, 'convert') != 'string' and val_num is not None
if is_num: if is_num:
value = val_num value = val_num
@@ -1670,6 +1685,8 @@ class InfoExtractor(object):
def _sort_formats(self, formats, field_preference=[]): def _sort_formats(self, formats, field_preference=[]):
if not formats: if not formats:
if self._downloader.params.get('ignore_no_formats_error'):
return
raise ExtractorError('No video formats found') raise ExtractorError('No video formats found')
format_sort = self.FormatSort() # params and to_screen are taken from the downloader format_sort = self.FormatSort() # params and to_screen are taken from the downloader
format_sort.evaluate_params(self._downloader.params, field_preference) format_sort.evaluate_params(self._downloader.params, field_preference)
@@ -1865,11 +1882,21 @@ class InfoExtractor(object):
'format_note': 'Quality selection URL', 'format_note': 'Quality selection URL',
} }
def _extract_m3u8_formats(self, m3u8_url, video_id, ext=None, def _extract_m3u8_formats(self, *args, **kwargs):
entry_protocol='m3u8', preference=None, quality=None, fmts, subs = self._extract_m3u8_formats_and_subtitles(*args, **kwargs)
m3u8_id=None, note=None, errnote=None, if subs:
fatal=True, live=False, data=None, headers={}, self.report_warning(bug_reports_message(
query={}): "Ignoring subtitle tracks found in the HLS manifest; "
"if any subtitle tracks are missing,"
))
return fmts
def _extract_m3u8_formats_and_subtitles(
self, m3u8_url, video_id, ext=None, entry_protocol='m3u8',
preference=None, quality=None, m3u8_id=None, note=None,
errnote=None, fatal=True, live=False, data=None, headers={},
query={}):
res = self._download_webpage_handle( res = self._download_webpage_handle(
m3u8_url, video_id, m3u8_url, video_id,
note=note or 'Downloading m3u8 information', note=note or 'Downloading m3u8 information',
@@ -1877,30 +1904,34 @@ class InfoExtractor(object):
fatal=fatal, data=data, headers=headers, query=query) fatal=fatal, data=data, headers=headers, query=query)
if res is False: if res is False:
return [] return [], {}
m3u8_doc, urlh = res m3u8_doc, urlh = res
m3u8_url = urlh.geturl() m3u8_url = urlh.geturl()
return self._parse_m3u8_formats( return self._parse_m3u8_formats_and_subtitles(
m3u8_doc, m3u8_url, ext=ext, entry_protocol=entry_protocol, m3u8_doc, m3u8_url, ext=ext, entry_protocol=entry_protocol,
preference=preference, quality=quality, m3u8_id=m3u8_id, preference=preference, quality=quality, m3u8_id=m3u8_id,
note=note, errnote=errnote, fatal=fatal, live=live, data=data, note=note, errnote=errnote, fatal=fatal, live=live, data=data,
headers=headers, query=query, video_id=video_id) headers=headers, query=query, video_id=video_id)
def _parse_m3u8_formats(self, m3u8_doc, m3u8_url, ext=None, def _parse_m3u8_formats_and_subtitles(
entry_protocol='m3u8', preference=None, quality=None, self, m3u8_doc, m3u8_url, ext=None, entry_protocol='m3u8',
m3u8_id=None, live=False, note=None, errnote=None, preference=None, quality=None, m3u8_id=None, live=False, note=None,
fatal=True, data=None, headers={}, query={}, video_id=None): errnote=None, fatal=True, data=None, headers={}, query={},
video_id=None):
if '#EXT-X-FAXS-CM:' in m3u8_doc: # Adobe Flash Access if '#EXT-X-FAXS-CM:' in m3u8_doc: # Adobe Flash Access
return [] return [], {}
if (not self._downloader.params.get('allow_unplayable_formats') if (not self._downloader.params.get('allow_unplayable_formats')
and re.search(r'#EXT-X-SESSION-KEY:.*?URI="skd://', m3u8_doc)): # Apple FairPlay and re.search(r'#EXT-X-SESSION-KEY:.*?URI="skd://', m3u8_doc)): # Apple FairPlay
return [] return [], {}
formats = [] formats = []
subtitles = {}
format_url = lambda u: ( format_url = lambda u: (
u u
if re.match(r'^https?://', u) if re.match(r'^https?://', u)
@@ -1987,7 +2018,7 @@ class InfoExtractor(object):
} }
formats.append(f) formats.append(f)
return formats return formats, subtitles
groups = {} groups = {}
last_stream_inf = {} last_stream_inf = {}
@@ -1999,6 +2030,21 @@ class InfoExtractor(object):
if not (media_type and group_id and name): if not (media_type and group_id and name):
return return
groups.setdefault(group_id, []).append(media) groups.setdefault(group_id, []).append(media)
# <https://tools.ietf.org/html/rfc8216#section-4.3.4.1>
if media_type == 'SUBTITLES':
lang = media['LANGUAGE'] # XXX: normalise?
url = format_url(media['URI'])
sub_info = {
'url': url,
'ext': determine_ext(url),
}
if sub_info['ext'] == 'm3u8':
# Per RFC 8216 §3.1, the only possible subtitle format m3u8
# files may contain is WebVTT:
# <https://tools.ietf.org/html/rfc8216#section-3.1>
sub_info['ext'] = 'vtt'
sub_info['protocol'] = 'm3u8_native'
subtitles.setdefault(lang, []).append(sub_info)
if media_type not in ('VIDEO', 'AUDIO'): if media_type not in ('VIDEO', 'AUDIO'):
return return
media_url = media.get('URI') media_url = media.get('URI')
@@ -2146,7 +2192,7 @@ class InfoExtractor(object):
formats.append(http_f) formats.append(http_f)
last_stream_inf = {} last_stream_inf = {}
return formats return formats, subtitles
@staticmethod @staticmethod
def _xpath_ns(path, namespace=None): def _xpath_ns(path, namespace=None):
@@ -2389,23 +2435,44 @@ class InfoExtractor(object):
}) })
return entries return entries
def _extract_mpd_formats(self, mpd_url, video_id, mpd_id=None, note=None, errnote=None, fatal=True, data=None, headers={}, query={}): def _extract_mpd_formats(self, *args, **kwargs):
fmts, subs = self._extract_mpd_formats_and_subtitles(*args, **kwargs)
if subs:
self.report_warning(bug_reports_message(
"Ignoring subtitle tracks found in the DASH manifest; "
"if any subtitle tracks are missing,"
))
return fmts
def _extract_mpd_formats_and_subtitles(
self, mpd_url, video_id, mpd_id=None, note=None, errnote=None,
fatal=True, data=None, headers={}, query={}):
res = self._download_xml_handle( res = self._download_xml_handle(
mpd_url, video_id, mpd_url, video_id,
note=note or 'Downloading MPD manifest', note=note or 'Downloading MPD manifest',
errnote=errnote or 'Failed to download MPD manifest', errnote=errnote or 'Failed to download MPD manifest',
fatal=fatal, data=data, headers=headers, query=query) fatal=fatal, data=data, headers=headers, query=query)
if res is False: if res is False:
return [] return [], {}
mpd_doc, urlh = res mpd_doc, urlh = res
if mpd_doc is None: if mpd_doc is None:
return [] return [], {}
mpd_base_url = base_url(urlh.geturl()) mpd_base_url = base_url(urlh.geturl())
return self._parse_mpd_formats( return self._parse_mpd_formats_and_subtitles(
mpd_doc, mpd_id, mpd_base_url, mpd_url) mpd_doc, mpd_id, mpd_base_url, mpd_url)
def _parse_mpd_formats(self, mpd_doc, mpd_id=None, mpd_base_url='', mpd_url=None): def _parse_mpd_formats(self, *args, **kwargs):
fmts, subs = self._parse_mpd_formats_and_subtitles(*args, **kwargs)
if subs:
self.report_warning(bug_reports_message(
"Ignoring subtitle tracks found in the DASH manifest; "
"if any subtitle tracks are missing,"
))
return fmts
def _parse_mpd_formats_and_subtitles(
self, mpd_doc, mpd_id=None, mpd_base_url='', mpd_url=None):
""" """
Parse formats from MPD manifest. Parse formats from MPD manifest.
References: References:
@@ -2415,7 +2482,7 @@ class InfoExtractor(object):
""" """
if not self._downloader.params.get('dynamic_mpd', True): if not self._downloader.params.get('dynamic_mpd', True):
if mpd_doc.get('type') == 'dynamic': if mpd_doc.get('type') == 'dynamic':
return [] return [], {}
namespace = self._search_regex(r'(?i)^{([^}]+)?}MPD$', mpd_doc.tag, 'namespace', default=None) namespace = self._search_regex(r'(?i)^{([^}]+)?}MPD$', mpd_doc.tag, 'namespace', default=None)
@@ -2487,6 +2554,7 @@ class InfoExtractor(object):
mpd_duration = parse_duration(mpd_doc.get('mediaPresentationDuration')) mpd_duration = parse_duration(mpd_doc.get('mediaPresentationDuration'))
formats = [] formats = []
subtitles = {}
for period in mpd_doc.findall(_add_ns('Period')): for period in mpd_doc.findall(_add_ns('Period')):
period_duration = parse_duration(period.get('duration')) or mpd_duration period_duration = parse_duration(period.get('duration')) or mpd_duration
period_ms_info = extract_multisegment_info(period, { period_ms_info = extract_multisegment_info(period, {
@@ -2504,11 +2572,9 @@ class InfoExtractor(object):
representation_attrib.update(representation.attrib) representation_attrib.update(representation.attrib)
# According to [1, 5.3.7.2, Table 9, page 41], @mimeType is mandatory # According to [1, 5.3.7.2, Table 9, page 41], @mimeType is mandatory
mime_type = representation_attrib['mimeType'] mime_type = representation_attrib['mimeType']
content_type = mime_type.split('/')[0] content_type = representation_attrib.get('contentType', mime_type.split('/')[0])
if content_type == 'text':
# TODO implement WebVTT downloading if content_type in ('video', 'audio', 'text'):
pass
elif content_type in ('video', 'audio'):
base_url = '' base_url = ''
for element in (representation, adaptation_set, period, mpd_doc): for element in (representation, adaptation_set, period, mpd_doc):
base_url_e = element.find(_add_ns('BaseURL')) base_url_e = element.find(_add_ns('BaseURL'))
@@ -2525,21 +2591,28 @@ class InfoExtractor(object):
url_el = representation.find(_add_ns('BaseURL')) url_el = representation.find(_add_ns('BaseURL'))
filesize = int_or_none(url_el.attrib.get('{http://youtube.com/yt/2012/10/10}contentLength') if url_el is not None else None) filesize = int_or_none(url_el.attrib.get('{http://youtube.com/yt/2012/10/10}contentLength') if url_el is not None else None)
bandwidth = int_or_none(representation_attrib.get('bandwidth')) bandwidth = int_or_none(representation_attrib.get('bandwidth'))
f = { if content_type in ('video', 'audio'):
'format_id': '%s-%s' % (mpd_id, representation_id) if mpd_id else representation_id, f = {
'manifest_url': mpd_url, 'format_id': '%s-%s' % (mpd_id, representation_id) if mpd_id else representation_id,
'ext': mimetype2ext(mime_type), 'manifest_url': mpd_url,
'width': int_or_none(representation_attrib.get('width')), 'ext': mimetype2ext(mime_type),
'height': int_or_none(representation_attrib.get('height')), 'width': int_or_none(representation_attrib.get('width')),
'tbr': float_or_none(bandwidth, 1000), 'height': int_or_none(representation_attrib.get('height')),
'asr': int_or_none(representation_attrib.get('audioSamplingRate')), 'tbr': float_or_none(bandwidth, 1000),
'fps': int_or_none(representation_attrib.get('frameRate')), 'asr': int_or_none(representation_attrib.get('audioSamplingRate')),
'language': lang if lang not in ('mul', 'und', 'zxx', 'mis') else None, 'fps': int_or_none(representation_attrib.get('frameRate')),
'format_note': 'DASH %s' % content_type, 'language': lang if lang not in ('mul', 'und', 'zxx', 'mis') else None,
'filesize': filesize, 'format_note': 'DASH %s' % content_type,
'container': mimetype2ext(mime_type) + '_dash', 'filesize': filesize,
} 'container': mimetype2ext(mime_type) + '_dash',
f.update(parse_codecs(representation_attrib.get('codecs'))) }
f.update(parse_codecs(representation_attrib.get('codecs')))
elif content_type == 'text':
f = {
'ext': mimetype2ext(mime_type),
'manifest_url': mpd_url,
'filesize': filesize,
}
representation_ms_info = extract_multisegment_info(representation, adaption_set_ms_info) representation_ms_info = extract_multisegment_info(representation, adaption_set_ms_info)
def prepare_template(template_name, identifiers): def prepare_template(template_name, identifiers):
@@ -2686,26 +2759,38 @@ class InfoExtractor(object):
else: else:
# Assuming direct URL to unfragmented media. # Assuming direct URL to unfragmented media.
f['url'] = base_url f['url'] = base_url
formats.append(f) if content_type in ('video', 'audio'):
formats.append(f)
elif content_type == 'text':
subtitles.setdefault(lang or 'und', []).append(f)
else: else:
self.report_warning('Unknown MIME type %s in DASH manifest' % mime_type) self.report_warning('Unknown MIME type %s in DASH manifest' % mime_type)
return formats return formats, subtitles
def _extract_ism_formats(self, ism_url, video_id, ism_id=None, note=None, errnote=None, fatal=True, data=None, headers={}, query={}): def _extract_ism_formats(self, *args, **kwargs):
fmts, subs = self._extract_ism_formats_and_subtitles(*args, **kwargs)
if subs:
self.report_warning(bug_reports_message(
"Ignoring subtitle tracks found in the ISM manifest; "
"if any subtitle tracks are missing,"
))
return fmts
def _extract_ism_formats_and_subtitles(self, ism_url, video_id, ism_id=None, note=None, errnote=None, fatal=True, data=None, headers={}, query={}):
res = self._download_xml_handle( res = self._download_xml_handle(
ism_url, video_id, ism_url, video_id,
note=note or 'Downloading ISM manifest', note=note or 'Downloading ISM manifest',
errnote=errnote or 'Failed to download ISM manifest', errnote=errnote or 'Failed to download ISM manifest',
fatal=fatal, data=data, headers=headers, query=query) fatal=fatal, data=data, headers=headers, query=query)
if res is False: if res is False:
return [] return [], {}
ism_doc, urlh = res ism_doc, urlh = res
if ism_doc is None: if ism_doc is None:
return [] return [], {}
return self._parse_ism_formats(ism_doc, urlh.geturl(), ism_id) return self._parse_ism_formats_and_subtitles(ism_doc, urlh.geturl(), ism_id)
def _parse_ism_formats(self, ism_doc, ism_url, ism_id=None): def _parse_ism_formats_and_subtitles(self, ism_doc, ism_url, ism_id=None):
""" """
Parse formats from ISM manifest. Parse formats from ISM manifest.
References: References:
@@ -2713,26 +2798,28 @@ class InfoExtractor(object):
https://msdn.microsoft.com/en-us/library/ff469518.aspx https://msdn.microsoft.com/en-us/library/ff469518.aspx
""" """
if ism_doc.get('IsLive') == 'TRUE': if ism_doc.get('IsLive') == 'TRUE':
return [] return [], {}
if (not self._downloader.params.get('allow_unplayable_formats') if (not self._downloader.params.get('allow_unplayable_formats')
and ism_doc.find('Protection') is not None): and ism_doc.find('Protection') is not None):
return [] return [], {}
duration = int(ism_doc.attrib['Duration']) duration = int(ism_doc.attrib['Duration'])
timescale = int_or_none(ism_doc.get('TimeScale')) or 10000000 timescale = int_or_none(ism_doc.get('TimeScale')) or 10000000
formats = [] formats = []
subtitles = {}
for stream in ism_doc.findall('StreamIndex'): for stream in ism_doc.findall('StreamIndex'):
stream_type = stream.get('Type') stream_type = stream.get('Type')
if stream_type not in ('video', 'audio'): if stream_type not in ('video', 'audio', 'text'):
continue continue
url_pattern = stream.attrib['Url'] url_pattern = stream.attrib['Url']
stream_timescale = int_or_none(stream.get('TimeScale')) or timescale stream_timescale = int_or_none(stream.get('TimeScale')) or timescale
stream_name = stream.get('Name') stream_name = stream.get('Name')
stream_language = stream.get('Language', 'und')
for track in stream.findall('QualityLevel'): for track in stream.findall('QualityLevel'):
fourcc = track.get('FourCC', 'AACL' if track.get('AudioTag') == '255' else None) fourcc = track.get('FourCC', 'AACL' if track.get('AudioTag') == '255' else None)
# TODO: add support for WVC1 and WMAP # TODO: add support for WVC1 and WMAP
if fourcc not in ('H264', 'AVC1', 'AACL'): if fourcc not in ('H264', 'AVC1', 'AACL', 'TTML'):
self.report_warning('%s is not a supported codec' % fourcc) self.report_warning('%s is not a supported codec' % fourcc)
continue continue
tbr = int(track.attrib['Bitrate']) // 1000 tbr = int(track.attrib['Bitrate']) // 1000
@@ -2775,33 +2862,52 @@ class InfoExtractor(object):
format_id.append(stream_name) format_id.append(stream_name)
format_id.append(compat_str(tbr)) format_id.append(compat_str(tbr))
formats.append({ if stream_type == 'text':
'format_id': '-'.join(format_id), subtitles.setdefault(stream_language, []).append({
'url': ism_url, 'ext': 'ismt',
'manifest_url': ism_url, 'protocol': 'ism',
'ext': 'ismv' if stream_type == 'video' else 'isma', 'url': ism_url,
'width': width, 'manifest_url': ism_url,
'height': height, 'fragments': fragments,
'tbr': tbr, '_download_params': {
'asr': sampling_rate, 'stream_type': stream_type,
'vcodec': 'none' if stream_type == 'audio' else fourcc, 'duration': duration,
'acodec': 'none' if stream_type == 'video' else fourcc, 'timescale': stream_timescale,
'protocol': 'ism', 'fourcc': fourcc,
'fragments': fragments, 'language': stream_language,
'_download_params': { 'codec_private_data': track.get('CodecPrivateData'),
'duration': duration, }
'timescale': stream_timescale, })
'width': width or 0, elif stream_type in ('video', 'audio'):
'height': height or 0, formats.append({
'fourcc': fourcc, 'format_id': '-'.join(format_id),
'codec_private_data': track.get('CodecPrivateData'), 'url': ism_url,
'sampling_rate': sampling_rate, 'manifest_url': ism_url,
'channels': int_or_none(track.get('Channels', 2)), 'ext': 'ismv' if stream_type == 'video' else 'isma',
'bits_per_sample': int_or_none(track.get('BitsPerSample', 16)), 'width': width,
'nal_unit_length_field': int_or_none(track.get('NALUnitLengthField', 4)), 'height': height,
}, 'tbr': tbr,
}) 'asr': sampling_rate,
return formats 'vcodec': 'none' if stream_type == 'audio' else fourcc,
'acodec': 'none' if stream_type == 'video' else fourcc,
'protocol': 'ism',
'fragments': fragments,
'_download_params': {
'stream_type': stream_type,
'duration': duration,
'timescale': stream_timescale,
'width': width or 0,
'height': height or 0,
'fourcc': fourcc,
'language': stream_language,
'codec_private_data': track.get('CodecPrivateData'),
'sampling_rate': sampling_rate,
'channels': int_or_none(track.get('Channels', 2)),
'bits_per_sample': int_or_none(track.get('BitsPerSample', 16)),
'nal_unit_length_field': int_or_none(track.get('NALUnitLengthField', 4)),
},
})
return formats, subtitles
def _parse_html5_media_entries(self, base_url, webpage, video_id, m3u8_id=None, m3u8_entry_protocol='m3u8', mpd_id=None, preference=None, quality=None): def _parse_html5_media_entries(self, base_url, webpage, video_id, m3u8_id=None, m3u8_entry_protocol='m3u8', mpd_id=None, preference=None, quality=None):
def absolute_url(item_url): def absolute_url(item_url):
@@ -2926,7 +3032,16 @@ class InfoExtractor(object):
entries.append(media_info) entries.append(media_info)
return entries return entries
def _extract_akamai_formats(self, manifest_url, video_id, hosts={}): def _extract_akamai_formats(self, *args, **kwargs):
fmts, subs = self._extract_akamai_formats_and_subtitles(*args, **kwargs)
if subs:
self.report_warning(bug_reports_message(
"Ignoring subtitle tracks found in the manifests; "
"if any subtitle tracks are missing,"
))
return fmts
def _extract_akamai_formats_and_subtitles(self, manifest_url, video_id, hosts={}):
signed = 'hdnea=' in manifest_url signed = 'hdnea=' in manifest_url
if not signed: if not signed:
# https://learn.akamai.com/en-us/webhelp/media-services-on-demand/stream-packaging-user-guide/GUID-BE6C0F73-1E06-483B-B0EA-57984B91B7F9.html # https://learn.akamai.com/en-us/webhelp/media-services-on-demand/stream-packaging-user-guide/GUID-BE6C0F73-1E06-483B-B0EA-57984B91B7F9.html
@@ -2935,6 +3050,7 @@ class InfoExtractor(object):
'', manifest_url).strip('?') '', manifest_url).strip('?')
formats = [] formats = []
subtitles = {}
hdcore_sign = 'hdcore=3.7.0' hdcore_sign = 'hdcore=3.7.0'
f4m_url = re.sub(r'(https?://[^/]+)/i/', r'\1/z/', manifest_url).replace('/master.m3u8', '/manifest.f4m') f4m_url = re.sub(r'(https?://[^/]+)/i/', r'\1/z/', manifest_url).replace('/master.m3u8', '/manifest.f4m')
@@ -2953,10 +3069,11 @@ class InfoExtractor(object):
hls_host = hosts.get('hls') hls_host = hosts.get('hls')
if hls_host: if hls_host:
m3u8_url = re.sub(r'(https?://)[^/]+', r'\1' + hls_host, m3u8_url) m3u8_url = re.sub(r'(https?://)[^/]+', r'\1' + hls_host, m3u8_url)
m3u8_formats = self._extract_m3u8_formats( m3u8_formats, m3u8_subtitles = self._extract_m3u8_formats_and_subtitles(
m3u8_url, video_id, 'mp4', 'm3u8_native', m3u8_url, video_id, 'mp4', 'm3u8_native',
m3u8_id='hls', fatal=False) m3u8_id='hls', fatal=False)
formats.extend(m3u8_formats) formats.extend(m3u8_formats)
subtitles = self._merge_subtitles(subtitles, m3u8_subtitles)
http_host = hosts.get('http') http_host = hosts.get('http')
if http_host and m3u8_formats and not signed: if http_host and m3u8_formats and not signed:
@@ -2980,7 +3097,7 @@ class InfoExtractor(object):
formats.append(http_f) formats.append(http_f)
i += 1 i += 1
return formats return formats, subtitles
def _extract_wowza_formats(self, url, video_id, m3u8_entry_protocol='m3u8_native', skip_protocols=[]): def _extract_wowza_formats(self, url, video_id, m3u8_entry_protocol='m3u8_native', skip_protocols=[]):
query = compat_urlparse.urlparse(url).query query = compat_urlparse.urlparse(url).query
@@ -3203,7 +3320,7 @@ class InfoExtractor(object):
if fatal: if fatal:
raise ExtractorError(msg) raise ExtractorError(msg)
else: else:
self._downloader.report_warning(msg) self.report_warning(msg)
return res return res
def _float(self, v, name, fatal=False, **kwargs): def _float(self, v, name, fatal=False, **kwargs):
@@ -3213,7 +3330,7 @@ class InfoExtractor(object):
if fatal: if fatal:
raise ExtractorError(msg) raise ExtractorError(msg)
else: else:
self._downloader.report_warning(msg) self.report_warning(msg)
return res return res
def _set_cookie(self, domain, name, value, expire_time=None, port=None, def _set_cookie(self, domain, name, value, expire_time=None, port=None,
@@ -3305,12 +3422,22 @@ class InfoExtractor(object):
return ret return ret
@classmethod @classmethod
def _merge_subtitles(cls, subtitle_dict1, subtitle_dict2): def _merge_subtitles(cls, *dicts, **kwargs):
""" Merge two subtitle dictionaries, language by language. """ """ Merge subtitle dictionaries, language by language. """
ret = dict(subtitle_dict1)
for lang in subtitle_dict2: target = (lambda target=None: target)(**kwargs)
ret[lang] = cls._merge_subtitle_items(subtitle_dict1.get(lang, []), subtitle_dict2[lang]) # The above lambda extracts the keyword argument 'target' from kwargs
return ret # while ensuring there are no stray ones. When Python 2 support
# is dropped, remove it and change the function signature to:
#
# def _merge_subtitles(cls, *dicts, target=None):
if target is None:
target = {}
for d in dicts:
for lang, subs in d.items():
target[lang] = cls._merge_subtitle_items(target.get(lang, []), subs)
return target
def extract_automatic_captions(self, *args, **kwargs): def extract_automatic_captions(self, *args, **kwargs):
if (self._downloader.params.get('writeautomaticsub', False) if (self._downloader.params.get('writeautomaticsub', False)
@@ -3389,7 +3516,7 @@ class SearchInfoExtractor(InfoExtractor):
if n <= 0: if n <= 0:
raise ExtractorError('invalid download number %s for query "%s"' % (n, query)) raise ExtractorError('invalid download number %s for query "%s"' % (n, query))
elif n > self._MAX_RESULTS: elif n > self._MAX_RESULTS:
self._downloader.report_warning('%s returns max %i results (you requested %i)' % (self._SEARCH_KEY, self._MAX_RESULTS, n)) self.report_warning('%s returns max %i results (you requested %i)' % (self._SEARCH_KEY, self._MAX_RESULTS, n))
n = self._MAX_RESULTS n = self._MAX_RESULTS
return self._get_n_results(query, n) return self._get_n_results(query, n)
+1 -1
View File
@@ -131,7 +131,7 @@ class CorusIE(ThePlatformFeedIE):
formats.extend(self._parse_smil_formats( formats.extend(self._parse_smil_formats(
smil, smil_url, video_id, namespace)) smil, smil_url, video_id, namespace))
if not formats and video.get('drm'): if not formats and video.get('drm'):
raise ExtractorError('This video is DRM protected.', expected=True) self.raise_no_formats('This video is DRM protected.', expected=True)
self._sort_formats(formats) self._sort_formats(formats)
subtitles = {} subtitles = {}
+158 -113
View File
@@ -12,6 +12,7 @@ from ..utils import (
determine_ext, determine_ext,
float_or_none, float_or_none,
int_or_none, int_or_none,
orderedSet,
parse_age_limit, parse_age_limit,
parse_duration, parse_duration,
url_or_none, url_or_none,
@@ -66,135 +67,179 @@ class CrackleIE(InfoExtractor):
}, },
} }
def _download_json(self, url, *args, **kwargs):
# Authorization generation algorithm is reverse engineered from:
# https://www.sonycrackle.com/static/js/main.ea93451f.chunk.js
timestamp = time.strftime('%Y%m%d%H%M', time.gmtime())
h = hmac.new(b'IGSLUQCBDFHEOIFM', '|'.join([url, timestamp]).encode(), hashlib.sha1).hexdigest().upper()
headers = {
'Accept': 'application/json',
'Authorization': '|'.join([h, timestamp, '117', '1']),
}
return InfoExtractor._download_json(self, url, *args, headers=headers, **kwargs)
def _real_extract(self, url): def _real_extract(self, url):
video_id = self._match_id(url) video_id = self._match_id(url)
country_code = self._downloader.params.get('geo_bypass_country', None) geo_bypass_country = self._downloader.params.get('geo_bypass_country', None)
countries = [country_code] if country_code else ( countries = orderedSet((geo_bypass_country, 'US', 'AU', 'CA', 'AS', 'FM', 'GU', 'MP', 'PR', 'PW', 'MH', 'VI', ''))
'US', 'AU', 'CA', 'AS', 'FM', 'GU', 'MP', 'PR', 'PW', 'MH', 'VI') num_countries, num = len(countries) - 1, 0
last_e = None media = {}
for num, country in enumerate(countries):
if num == 1: # start hard-coded list
self.report_warning('%s. Trying with a list of known countries' % (
'Unable to obtain video formats from %s API' % geo_bypass_country if geo_bypass_country
else 'No country code was given using --geo-bypass-country'))
elif num == num_countries: # end of list
geo_info = self._download_json(
'https://web-api-us.crackle.com/Service.svc/geo/country',
video_id, fatal=False, note='Downloading geo-location information from crackle API',
errnote='Unable to fetch geo-location information from crackle') or {}
country = geo_info.get('CountryCode')
if country is None:
continue
self.to_screen('%s identified country as %s' % (self.IE_NAME, country))
if country in countries:
self.to_screen('Downloading from %s API was already attempted. Skipping...' % country)
continue
for country in countries: if country is None:
continue
try: try:
# Authorization generation algorithm is reverse engineered from:
# https://www.sonycrackle.com/static/js/main.ea93451f.chunk.js
media_detail_url = 'https://web-api-us.crackle.com/Service.svc/details/media/%s/%s?disableProtocols=true' % (video_id, country)
timestamp = time.strftime('%Y%m%d%H%M', time.gmtime())
h = hmac.new(b'IGSLUQCBDFHEOIFM', '|'.join([media_detail_url, timestamp]).encode(), hashlib.sha1).hexdigest().upper()
media = self._download_json( media = self._download_json(
media_detail_url, video_id, 'Downloading media JSON as %s' % country, 'https://web-api-us.crackle.com/Service.svc/details/media/%s/%s?disableProtocols=true' % (video_id, country),
'Unable to download media JSON', headers={ video_id, note='Downloading media JSON from %s API' % country,
'Accept': 'application/json', errnote='Unable to download media JSON')
'Authorization': '|'.join([h, timestamp, '117', '1']),
})
except ExtractorError as e: except ExtractorError as e:
# 401 means geo restriction, trying next country # 401 means geo restriction, trying next country
if isinstance(e.cause, compat_HTTPError) and e.cause.code == 401: if isinstance(e.cause, compat_HTTPError) and e.cause.code == 401:
last_e = e
continue continue
raise raise
media_urls = media.get('MediaURLs') status = media.get('status')
if not media_urls or not isinstance(media_urls, list): if status.get('messageCode') != '0':
raise ExtractorError(
'%s said: %s %s - %s' % (
self.IE_NAME, status.get('messageCodeDescription'), status.get('messageCode'), status.get('message')),
expected=True)
# Found video formats
if isinstance(media.get('MediaURLs'), list):
break
ignore_no_formats = self._downloader.params.get('ignore_no_formats_error')
allow_unplayable_formats = self._downloader.params.get('allow_unplayable_formats')
if not media or (not media.get('MediaURLs') and not ignore_no_formats):
raise ExtractorError(
'Unable to access the crackle API. Try passing your country code '
'to --geo-bypass-country. If it still does not work and the '
'video is available in your country')
title = media['Title']
formats, subtitles = [], {}
has_drm = False
for e in media.get('MediaURLs') or []:
if e.get('UseDRM'):
has_drm = True
if not allow_unplayable_formats:
continue
format_url = url_or_none(e.get('Path'))
if not format_url:
continue continue
ext = determine_ext(format_url)
title = media['Title'] if ext == 'm3u8':
fmts, subs = self._extract_m3u8_formats_and_subtitles(
formats = [] format_url, video_id, 'mp4', entry_protocol='m3u8_native',
for e in media['MediaURLs']: m3u8_id='hls', fatal=False)
if not self._downloader.params.get('allow_unplayable_formats') and e.get('UseDRM') is True: formats.extend(fmts)
continue subtitles = self._merge_subtitles(subtitles, subs)
format_url = url_or_none(e.get('Path')) elif ext == 'mpd':
if not format_url: fmts, subs = self._extract_mpd_formats_and_subtitles(
continue format_url, video_id, mpd_id='dash', fatal=False)
ext = determine_ext(format_url) formats.extend(fmts)
if ext == 'm3u8': subtitles = self._merge_subtitles(subtitles, subs)
formats.extend(self._extract_m3u8_formats( elif format_url.endswith('.ism/Manifest'):
format_url, video_id, 'mp4', entry_protocol='m3u8_native', fmts, subs = self._extract_ism_formats_and_subtitles(
m3u8_id='hls', fatal=False)) format_url, video_id, ism_id='mss', fatal=False)
elif ext == 'mpd': formats.extend(fmts)
formats.extend(self._extract_mpd_formats( subtitles = self._merge_subtitles(subtitles, subs)
format_url, video_id, mpd_id='dash', fatal=False))
elif format_url.endswith('.ism/Manifest'):
formats.extend(self._extract_ism_formats(
format_url, video_id, ism_id='mss', fatal=False))
else:
mfs_path = e.get('Type')
mfs_info = self._MEDIA_FILE_SLOTS.get(mfs_path)
if not mfs_info:
continue
formats.append({
'url': format_url,
'format_id': 'http-' + mfs_path.split('.')[0],
'width': mfs_info['width'],
'height': mfs_info['height'],
})
self._sort_formats(formats)
description = media.get('Description')
duration = int_or_none(media.get(
'DurationInSeconds')) or parse_duration(media.get('Duration'))
view_count = int_or_none(media.get('CountViews'))
average_rating = float_or_none(media.get('UserRating'))
age_limit = parse_age_limit(media.get('Rating'))
genre = media.get('Genre')
release_year = int_or_none(media.get('ReleaseYear'))
creator = media.get('Directors')
artist = media.get('Cast')
if media.get('MediaTypeDisplayValue') == 'Full Episode':
series = media.get('ShowName')
episode = title
season_number = int_or_none(media.get('Season'))
episode_number = int_or_none(media.get('Episode'))
else: else:
series = episode = season_number = episode_number = None mfs_path = e.get('Type')
mfs_info = self._MEDIA_FILE_SLOTS.get(mfs_path)
if not mfs_info:
continue
formats.append({
'url': format_url,
'format_id': 'http-' + mfs_path.split('.')[0],
'width': mfs_info['width'],
'height': mfs_info['height'],
})
if not formats and has_drm and not ignore_no_formats:
raise ExtractorError('The video is DRM protected', expected=True)
self._sort_formats(formats)
subtitles = {} description = media.get('Description')
cc_files = media.get('ClosedCaptionFiles') duration = int_or_none(media.get(
if isinstance(cc_files, list): 'DurationInSeconds')) or parse_duration(media.get('Duration'))
for cc_file in cc_files: view_count = int_or_none(media.get('CountViews'))
if not isinstance(cc_file, dict): average_rating = float_or_none(media.get('UserRating'))
continue age_limit = parse_age_limit(media.get('Rating'))
cc_url = url_or_none(cc_file.get('Path')) genre = media.get('Genre')
if not cc_url: release_year = int_or_none(media.get('ReleaseYear'))
continue creator = media.get('Directors')
lang = cc_file.get('Locale') or 'en' artist = media.get('Cast')
subtitles.setdefault(lang, []).append({'url': cc_url})
thumbnails = [] if media.get('MediaTypeDisplayValue') == 'Full Episode':
images = media.get('Images') series = media.get('ShowName')
if isinstance(images, list): episode = title
for image_key, image_url in images.items(): season_number = int_or_none(media.get('Season'))
mobj = re.search(r'Img_(\d+)[xX](\d+)', image_key) episode_number = int_or_none(media.get('Episode'))
if not mobj: else:
continue series = episode = season_number = episode_number = None
thumbnails.append({
'url': image_url,
'width': int(mobj.group(1)),
'height': int(mobj.group(2)),
})
return { cc_files = media.get('ClosedCaptionFiles')
'id': video_id, if isinstance(cc_files, list):
'title': title, for cc_file in cc_files:
'description': description, if not isinstance(cc_file, dict):
'duration': duration, continue
'view_count': view_count, cc_url = url_or_none(cc_file.get('Path'))
'average_rating': average_rating, if not cc_url:
'age_limit': age_limit, continue
'genre': genre, lang = cc_file.get('Locale') or 'en'
'creator': creator, subtitles.setdefault(lang, []).append({'url': cc_url})
'artist': artist,
'release_year': release_year,
'series': series,
'episode': episode,
'season_number': season_number,
'episode_number': episode_number,
'thumbnails': thumbnails,
'subtitles': subtitles,
'formats': formats,
}
raise last_e thumbnails = []
images = media.get('Images')
if isinstance(images, list):
for image_key, image_url in images.items():
mobj = re.search(r'Img_(\d+)[xX](\d+)', image_key)
if not mobj:
continue
thumbnails.append({
'url': image_url,
'width': int(mobj.group(1)),
'height': int(mobj.group(2)),
})
return {
'id': video_id,
'title': title,
'description': description,
'duration': duration,
'view_count': view_count,
'average_rating': average_rating,
'age_limit': age_limit,
'genre': genre,
'creator': creator,
'artist': artist,
'release_year': release_year,
'series': series,
'episode': episode,
'season_number': season_number,
'episode_number': episode_number,
'thumbnails': thumbnails,
'subtitles': subtitles,
'formats': formats,
}
+1 -1
View File
@@ -428,7 +428,7 @@ Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text
r'<div class="showmedia-trailer-notice">(.+?)</div>', r'<div class="showmedia-trailer-notice">(.+?)</div>',
webpage, 'trailer-notice', default='') webpage, 'trailer-notice', default='')
if note_m: if note_m:
raise ExtractorError(note_m) raise ExtractorError(note_m, expected=True)
mobj = re.search(r'Page\.messaging_box_controller\.addItems\(\[(?P<msg>{.+?})\]\)', webpage) mobj = re.search(r'Page\.messaging_box_controller\.addItems\(\[(?P<msg>{.+?})\]\)', webpage)
if mobj: if mobj:
+34 -21
View File
@@ -143,32 +143,45 @@ class CuriosityStreamIE(CuriosityStreamBaseIE):
} }
class CuriosityStreamCollectionIE(CuriosityStreamBaseIE): class CuriosityStreamCollectionsIE(CuriosityStreamBaseIE):
IE_NAME = 'curiositystream:collection' IE_NAME = 'curiositystream:collections'
_VALID_URL = r'https?://(?:app\.)?curiositystream\.com/(?:collection|series)/(?P<id>\d+)' _VALID_URL = r'https?://(?:app\.)?curiositystream\.com/collections/(?P<id>\d+)'
_API_BASE_URL = 'https://api.curiositystream.com/v2/collections/'
_TESTS = [{ _TESTS = [{
'url': 'https://app.curiositystream.com/collection/2', 'url': 'https://curiositystream.com/collections/86',
'info_dict': {
'id': '86',
'title': 'Staff Picks',
'description': 'Wondering where to start? Here are a few of our favorite series and films... from our couch to yours.',
},
'playlist_mincount': 7,
}]
def _real_extract(self, url):
collection_id = self._match_id(url)
collection = self._call_api(collection_id, collection_id)
entries = []
for media in collection.get('media', []):
media_id = compat_str(media.get('id'))
media_type, ie = ('series', CuriosityStreamSeriesIE) if media.get('is_collection') else ('video', CuriosityStreamIE)
entries.append(self.url_result(
'https://curiositystream.com/%s/%s' % (media_type, media_id),
ie=ie.ie_key(), video_id=media_id))
return self.playlist_result(
entries, collection_id,
collection.get('title'), collection.get('description'))
class CuriosityStreamSeriesIE(CuriosityStreamCollectionsIE):
IE_NAME = 'curiositystream:series'
_VALID_URL = r'https?://(?:app\.)?curiositystream\.com/series/(?P<id>\d+)'
_API_BASE_URL = 'https://api.curiositystream.com/v2/series/'
_TESTS = [{
'url': 'https://app.curiositystream.com/series/2',
'info_dict': { 'info_dict': {
'id': '2', 'id': '2',
'title': 'Curious Minds: The Internet', 'title': 'Curious Minds: The Internet',
'description': 'How is the internet shaping our lives in the 21st Century?', 'description': 'How is the internet shaping our lives in the 21st Century?',
}, },
'playlist_mincount': 16, 'playlist_mincount': 16,
}, {
'url': 'https://curiositystream.com/series/2',
'only_matching': True,
}] }]
def _real_extract(self, url):
collection_id = self._match_id(url)
collection = self._call_api(
'collections/' + collection_id, collection_id)
entries = []
for media in collection.get('media', []):
media_id = compat_str(media.get('id'))
entries.append(self.url_result(
'https://curiositystream.com/video/' + media_id,
CuriosityStreamIE.ie_key(), media_id))
return self.playlist_result(
entries, collection_id,
collection.get('title'), collection.get('description'))
+1 -1
View File
@@ -14,7 +14,7 @@ from ..utils import (
class DeezerBaseInfoExtractor(InfoExtractor): class DeezerBaseInfoExtractor(InfoExtractor):
def get_data(self, url): def get_data(self, url):
if not self._downloader.params.get('test'): if not self._downloader.params.get('test'):
self._downloader.report_warning('For now, this extractor only supports the 30 second previews. Patches welcome!') self.report_warning('For now, this extractor only supports the 30 second previews. Patches welcome!')
mobj = re.match(self._VALID_URL, url) mobj = re.match(self._VALID_URL, url)
data_id = mobj.group('id') data_id = mobj.group('id')
+1 -2
View File
@@ -9,7 +9,6 @@ from ..utils import (
unified_strdate, unified_strdate,
compat_str, compat_str,
determine_ext, determine_ext,
ExtractorError,
update_url_query, update_url_query,
) )
@@ -140,7 +139,7 @@ class DisneyIE(InfoExtractor):
'vcodec': 'none' if (width == 0 and height == 0) else None, 'vcodec': 'none' if (width == 0 and height == 0) else None,
}) })
if not formats and video_data.get('expired'): if not formats and video_data.get('expired'):
raise ExtractorError( self.raise_no_formats(
'%s said: %s' % (self.IE_NAME, page_data['translations']['video_expired']), '%s said: %s' % (self.IE_NAME, page_data['translations']['video_expired']),
expected=True) expected=True)
self._sort_formats(formats) self._sort_formats(formats)
+25 -19
View File
@@ -32,6 +32,18 @@ class DigitallySpeakingIE(InfoExtractor):
# From http://www.gdcvault.com/play/1013700/Advanced-Material # From http://www.gdcvault.com/play/1013700/Advanced-Material
'url': 'http://sevt.dispeak.com/ubm/gdc/eur10/xml/11256_1282118587281VNIT.xml', 'url': 'http://sevt.dispeak.com/ubm/gdc/eur10/xml/11256_1282118587281VNIT.xml',
'only_matching': True, 'only_matching': True,
}, {
# From https://gdcvault.com/play/1016624, empty speakerVideo
'url': 'https://sevt.dispeak.com/ubm/gdc/online12/xml/201210-822101_1349794556671DDDD.xml',
'info_dict': {
'id': '201210-822101_1349794556671DDDD',
'ext': 'flv',
'title': 'Pre-launch - Preparing to Take the Plunge',
},
}, {
# From http://www.gdcvault.com/play/1014846/Conference-Keynote-Shigeru, empty slideVideo
'url': 'http://events.digitallyspeaking.com/gdc/project25/xml/p25-miyamoto1999_1282467389849HSVB.xml',
'only_matching': True,
}] }]
def _parse_mp4(self, metadata): def _parse_mp4(self, metadata):
@@ -85,25 +97,19 @@ class DigitallySpeakingIE(InfoExtractor):
'quality': 1, 'quality': 1,
'format_id': audio.get('code'), 'format_id': audio.get('code'),
}) })
slide_video_path = xpath_text(metadata, './slideVideo', fatal=True) for video_key, format_id, preference in (
formats.append({ ('slide', 'slides', -2), ('speaker', 'speaker', -1)):
'url': 'rtmp://%s/ondemand?ovpfv=1.1' % akamai_url, video_path = xpath_text(metadata, './%sVideo' % video_key)
'play_path': remove_end(slide_video_path, '.flv'), if not video_path:
'ext': 'flv', continue
'format_note': 'slide deck video', formats.append({
'quality': -2, 'url': 'rtmp://%s/ondemand?ovpfv=1.1' % akamai_url,
'format_id': 'slides', 'play_path': remove_end(video_path, '.flv'),
'acodec': 'none', 'ext': 'flv',
}) 'format_note': '%s video' % video_key,
speaker_video_path = xpath_text(metadata, './speakerVideo', fatal=True) 'quality': preference,
formats.append({ 'format_id': format_id,
'url': 'rtmp://%s/ondemand?ovpfv=1.1' % akamai_url, })
'play_path': remove_end(speaker_video_path, '.flv'),
'ext': 'flv',
'format_note': 'speaker video',
'quality': -1,
'format_id': 'speaker',
})
return formats return formats
def _real_extract(self, url): def _real_extract(self, url):
+17 -65
View File
@@ -1,9 +1,7 @@
# coding: utf-8 # coding: utf-8
from __future__ import unicode_literals from __future__ import unicode_literals
import os
import re import re
import tempfile
from .common import InfoExtractor from .common import InfoExtractor
from ..utils import ( from ..utils import (
@@ -12,12 +10,12 @@ from ..utils import (
try_get, try_get,
) )
from ..compat import compat_str from ..compat import compat_str
from ..downloader.hls import HlsFD
class ElonetIE(InfoExtractor): class ElonetIE(InfoExtractor):
_VALID_URL = r'https?://elonet\.finna\.fi/Record/kavi\.elonet_elokuva_(?P<id>[0-9]+)' _VALID_URL = r'https?://elonet\.finna\.fi/Record/kavi\.elonet_elokuva_(?P<id>[0-9]+)'
_TEST = { _TESTS = [{
# m3u8 with subtitles
'url': 'https://elonet.finna.fi/Record/kavi.elonet_elokuva_107867', 'url': 'https://elonet.finna.fi/Record/kavi.elonet_elokuva_107867',
'md5': '8efc954b96c543711707f87de757caea', 'md5': '8efc954b96c543711707f87de757caea',
'info_dict': { 'info_dict': {
@@ -27,62 +25,17 @@ class ElonetIE(InfoExtractor):
'description': 'Valkoinen peura (1952) on Erik Blombergin ohjaama ja yhdessä Mirjami Kuosmasen kanssa käsikirjoittama tarunomainen kertomus valkoisen peuran hahmossa lii...', 'description': 'Valkoinen peura (1952) on Erik Blombergin ohjaama ja yhdessä Mirjami Kuosmasen kanssa käsikirjoittama tarunomainen kertomus valkoisen peuran hahmossa lii...',
'thumbnail': 'https://elonet.finna.fi/Cover/Show?id=kavi.elonet_elokuva_107867&index=0&size=large', 'thumbnail': 'https://elonet.finna.fi/Cover/Show?id=kavi.elonet_elokuva_107867&index=0&size=large',
}, },
} }, {
# DASH with subtitles
def _download_m3u8_chunked_subtitle(self, chunklist_url): 'url': 'https://elonet.finna.fi/Record/kavi.elonet_elokuva_116539',
""" 'info_dict': {
Download VTT subtitles from pieces in manifest URL. 'id': '116539',
Return a string containing joined chunks with extra headers removed. 'ext': 'mp4',
""" 'title': 'Minulla on tiikeri',
with tempfile.NamedTemporaryFile(delete=True) as outfile: 'description': 'Pienellä pojalla, joka asuu kerrostalossa, on kotieläimenä tiikeri. Se on kuitenkin salaisuus. Kerrostalon räpätäti on Kotilaisen täti, joka on aina vali...',
fname = outfile.name 'thumbnail': 'https://elonet.finna.fi/Cover/Show?id=kavi.elonet_elokuva_116539&index=0&size=large&source=Solr',
hlsdl = HlsFD(self._downloader, {}) }
hlsdl.download(compat_str(fname), {"url": chunklist_url}) }]
with open(fname, 'r') as fin:
# Remove (some) headers
fdata = re.sub(r'X-TIMESTAMP-MAP.*\n+|WEBVTT\n+', '', fin.read())
os.remove(fname)
return "WEBVTT\n\n" + fdata
def _parse_m3u8_subtitles(self, m3u8_doc, m3u8_url):
"""
Parse subtitles from HLS / m3u8 manifest.
"""
subtitles = {}
baseurl = m3u8_url[:m3u8_url.rindex('/') + 1]
for line in m3u8_doc.split('\n'):
if 'EXT-X-MEDIA:TYPE=SUBTITLES' in line:
lang = self._search_regex(
r'LANGUAGE="(.+?)"', line, 'lang', default=False)
uri = self._search_regex(
r'URI="(.+?)"', line, 'uri', default=False)
if lang and uri:
data = self._download_m3u8_chunked_subtitle(baseurl + uri)
subtitles[lang] = [{'ext': 'vtt', 'data': data}]
return subtitles
def _parse_mpd_subtitles(self, mpd_doc):
"""
Parse subtitles from MPD manifest.
"""
ns = '{urn:mpeg:dash:schema:mpd:2011}'
subtitles = {}
for aset in mpd_doc.findall(".//%sAdaptationSet[@mimeType='text/vtt']" % (ns)):
lang = aset.attrib.get('lang', 'unk')
url = aset.find("./%sRepresentation/%sBaseURL" % (ns, ns)).text
subtitles[lang] = [{'ext': 'vtt', 'url': url}]
return subtitles
def _get_subtitles(self, fmt, doc, url):
if fmt == 'm3u8':
subs = self._parse_m3u8_subtitles(doc, url)
elif fmt == 'mpd':
subs = self._parse_mpd_subtitles(doc)
else:
self._downloader.report_warning(
"Cannot download subtitles from '%s' streams." % (fmt))
subs = {}
return subs
def _real_extract(self, url): def _real_extract(self, url):
video_id = self._match_id(url) video_id = self._match_id(url)
@@ -101,8 +54,8 @@ class ElonetIE(InfoExtractor):
self._parse_json(json_s, video_id), self._parse_json(json_s, video_id),
lambda x: x[0]["src"], compat_str) lambda x: x[0]["src"], compat_str)
formats = [] formats = []
subtitles = {}
if re.search(r'\.m3u8\??', src): if re.search(r'\.m3u8\??', src):
fmt = 'm3u8'
res = self._download_webpage_handle( res = self._download_webpage_handle(
# elonet servers have certificate problems # elonet servers have certificate problems
src.replace('https:', 'http:'), video_id, src.replace('https:', 'http:'), video_id,
@@ -111,11 +64,10 @@ class ElonetIE(InfoExtractor):
if res: if res:
doc, urlh = res doc, urlh = res
url = urlh.geturl() url = urlh.geturl()
formats = self._parse_m3u8_formats(doc, url) formats, subtitles = self._parse_m3u8_formats_and_subtitles(doc, url)
for f in formats: for f in formats:
f['ext'] = 'mp4' f['ext'] = 'mp4'
elif re.search(r'\.mpd\??', src): elif re.search(r'\.mpd\??', src):
fmt = 'mpd'
res = self._download_xml_handle( res = self._download_xml_handle(
src, video_id, src, video_id,
note='Downloading MPD manifest', note='Downloading MPD manifest',
@@ -123,7 +75,7 @@ class ElonetIE(InfoExtractor):
if res: if res:
doc, urlh = res doc, urlh = res
url = base_url(urlh.geturl()) url = base_url(urlh.geturl())
formats = self._parse_mpd_formats(doc, mpd_base_url=url) formats, subtitles = self._parse_mpd_formats_and_subtitles(doc, mpd_base_url=url)
else: else:
raise ExtractorError("Unknown streaming format") raise ExtractorError("Unknown streaming format")
@@ -133,5 +85,5 @@ class ElonetIE(InfoExtractor):
'description': description, 'description': description,
'thumbnail': thumbnail, 'thumbnail': thumbnail,
'formats': formats, 'formats': formats,
'subtitles': self.extract_subtitles(fmt, doc, url), 'subtitles': subtitles,
} }
+12 -4
View File
@@ -151,7 +151,6 @@ from .bleacherreport import (
BleacherReportIE, BleacherReportIE,
BleacherReportCMSIE, BleacherReportCMSIE,
) )
from .blinkx import BlinkxIE
from .bloomberg import BloombergIE from .bloomberg import BloombergIE
from .bokecc import BokeCCIE from .bokecc import BokeCCIE
from .bongacams import BongaCamsIE from .bongacams import BongaCamsIE
@@ -288,7 +287,8 @@ from .ctvnews import CTVNewsIE
from .cultureunplugged import CultureUnpluggedIE from .cultureunplugged import CultureUnpluggedIE
from .curiositystream import ( from .curiositystream import (
CuriosityStreamIE, CuriosityStreamIE,
CuriosityStreamCollectionIE, CuriosityStreamCollectionsIE,
CuriosityStreamSeriesIE,
) )
from .cwtv import CWTVIE from .cwtv import CWTVIE
from .dailymail import DailyMailIE from .dailymail import DailyMailIE
@@ -760,7 +760,10 @@ from .mtv import (
) )
from .muenchentv import MuenchenTVIE from .muenchentv import MuenchenTVIE
from .mwave import MwaveIE, MwaveMeetGreetIE from .mwave import MwaveIE, MwaveMeetGreetIE
from .mxplayer import MxplayerIE from .mxplayer import (
MxplayerIE,
MxplayerShowIE,
)
from .mychannels import MyChannelsIE from .mychannels import MyChannelsIE
from .myspace import MySpaceIE, MySpaceAlbumIE from .myspace import MySpaceIE, MySpaceAlbumIE
from .myspass import MySpassIE from .myspass import MySpassIE
@@ -1340,7 +1343,10 @@ from .trovo import (
from .trunews import TruNewsIE from .trunews import TruNewsIE
from .trutv import TruTVIE from .trutv import TruTVIE
from .tube8 import Tube8IE from .tube8 import Tube8IE
from .tubitv import TubiTvIE from .tubitv import (
TubiTvIE,
TubiTvShowIE,
)
from .tumblr import TumblrIE from .tumblr import TumblrIE
from .tunein import ( from .tunein import (
TuneInClipIE, TuneInClipIE,
@@ -1435,6 +1441,7 @@ from .ufctv import (
UFCTVIE, UFCTVIE,
UFCArabiaIE, UFCArabiaIE,
) )
from .ukcolumn import UkColumnIE
from .uktvplay import UKTVPlayIE from .uktvplay import UKTVPlayIE
from .digiteka import DigitekaIE from .digiteka import DigitekaIE
from .dlive import ( from .dlive import (
@@ -1594,6 +1601,7 @@ from .weibo import (
) )
from .weiqitv import WeiqiTVIE from .weiqitv import WeiqiTVIE
from .wimtv import WimTVIE from .wimtv import WimTVIE
from .whowatch import WhoWatchIE
from .wistia import ( from .wistia import (
WistiaIE, WistiaIE,
WistiaPlaylistIE, WistiaPlaylistIE,
+5 -9
View File
@@ -3,14 +3,11 @@ from __future__ import unicode_literals
import json import json
import re import re
import socket
from .common import InfoExtractor from .common import InfoExtractor
from ..compat import ( from ..compat import (
compat_etree_fromstring, compat_etree_fromstring,
compat_http_client,
compat_str, compat_str,
compat_urllib_error,
compat_urllib_parse_unquote, compat_urllib_parse_unquote,
compat_urllib_parse_unquote_plus, compat_urllib_parse_unquote_plus,
) )
@@ -23,6 +20,7 @@ from ..utils import (
int_or_none, int_or_none,
js_to_json, js_to_json,
limit_length, limit_length,
network_exceptions,
parse_count, parse_count,
qualities, qualities,
sanitized_Request, sanitized_Request,
@@ -348,7 +346,7 @@ class FacebookIE(InfoExtractor):
login_results, 'login error', default=None, group='error') login_results, 'login error', default=None, group='error')
if error: if error:
raise ExtractorError('Unable to login: %s' % error, expected=True) raise ExtractorError('Unable to login: %s' % error, expected=True)
self._downloader.report_warning('unable to log in: bad username/password, or exceeded login rate limit (~3/min). Check credentials or wait.') self.report_warning('unable to log in: bad username/password, or exceeded login rate limit (~3/min). Check credentials or wait.')
return return
fb_dtsg = self._search_regex( fb_dtsg = self._search_regex(
@@ -369,9 +367,9 @@ class FacebookIE(InfoExtractor):
check_response = self._download_webpage(check_req, None, check_response = self._download_webpage(check_req, None,
note='Confirming login') note='Confirming login')
if re.search(r'id="checkpointSubmitButton"', check_response) is not None: if re.search(r'id="checkpointSubmitButton"', check_response) is not None:
self._downloader.report_warning('Unable to confirm login, you have to login in your browser and authorize the login.') self.report_warning('Unable to confirm login, you have to login in your browser and authorize the login.')
except (compat_urllib_error.URLError, compat_http_client.HTTPException, socket.error) as err: except network_exceptions as err:
self._downloader.report_warning('unable to log in: %s' % error_to_compat_str(err)) self.report_warning('unable to log in: %s' % error_to_compat_str(err))
return return
def _real_initialize(self): def _real_initialize(self):
@@ -625,8 +623,6 @@ class FacebookIE(InfoExtractor):
subtitles_src = f[0].get('subtitles_src') subtitles_src = f[0].get('subtitles_src')
if subtitles_src: if subtitles_src:
subtitles.setdefault('en', []).append({'url': subtitles_src}) subtitles.setdefault('en', []).append({'url': subtitles_src})
if not formats:
raise ExtractorError('Cannot find video formats')
process_formats(formats) process_formats(formats)
+32 -10
View File
@@ -151,6 +151,7 @@ class FranceTVIE(InfoExtractor):
videos.append(fallback_info['video']) videos.append(fallback_info['video'])
formats = [] formats = []
subtitles = {}
for video in videos: for video in videos:
video_url = video.get('url') video_url = video.get('url')
if not video_url: if not video_url:
@@ -171,10 +172,12 @@ class FranceTVIE(InfoExtractor):
sign(video_url, format_id) + '&hdcore=3.7.0&plugin=aasp-3.7.0.39.44', sign(video_url, format_id) + '&hdcore=3.7.0&plugin=aasp-3.7.0.39.44',
video_id, f4m_id=format_id, fatal=False)) video_id, f4m_id=format_id, fatal=False))
elif ext == 'm3u8': elif ext == 'm3u8':
formats.extend(self._extract_m3u8_formats( m3u8_fmts, m3u8_subs = self._extract_m3u8_formats_and_subtitles(
sign(video_url, format_id), video_id, 'mp4', sign(video_url, format_id), video_id, 'mp4',
entry_protocol='m3u8_native', m3u8_id=format_id, entry_protocol='m3u8_native', m3u8_id=format_id,
fatal=False)) fatal=False)
formats.extend(m3u8_fmts)
subtitles = self._merge_subtitles(subtitles, m3u8_subs)
elif ext == 'mpd': elif ext == 'mpd':
formats.extend(self._extract_mpd_formats( formats.extend(self._extract_mpd_formats(
sign(video_url, format_id), video_id, mpd_id=format_id, fatal=False)) sign(video_url, format_id), video_id, mpd_id=format_id, fatal=False))
@@ -199,13 +202,12 @@ class FranceTVIE(InfoExtractor):
title += ' - %s' % subtitle title += ' - %s' % subtitle
title = title.strip() title = title.strip()
subtitles = {} subtitles.setdefault('fr', []).extend(
subtitles_list = [{ [{
'url': subformat['url'], 'url': subformat['url'],
'ext': subformat.get('format'), 'ext': subformat.get('format'),
} for subformat in info.get('subtitles', []) if subformat.get('url')] } for subformat in info.get('subtitles', []) if subformat.get('url')]
if subtitles_list: )
subtitles['fr'] = subtitles_list
return { return {
'id': video_id, 'id': video_id,
@@ -357,6 +359,22 @@ class FranceTVInfoIE(FranceTVBaseInfoExtractor):
'skip_download': True, 'skip_download': True,
}, },
'add_ie': [FranceTVIE.ie_key()], 'add_ie': [FranceTVIE.ie_key()],
}, {
'note': 'Only an image exists in initial webpage instead of the video',
'url': 'https://www.francetvinfo.fr/sante/maladie/coronavirus/covid-19-en-inde-une-situation-catastrophique-a-new-dehli_4381095.html',
'info_dict': {
'id': '7d204c9e-a2d3-11eb-9e4c-000d3a23d482',
'ext': 'mp4',
'title': 'Covid-19 : une situation catastrophique à New Dehli',
'thumbnail': str,
'duration': 76,
'timestamp': 1619028518,
'upload_date': '20210421',
},
'params': {
'skip_download': True,
},
'add_ie': [FranceTVIE.ie_key()],
}, { }, {
'url': 'http://www.francetvinfo.fr/elections/europeennes/direct-europeennes-regardez-le-debat-entre-les-candidats-a-la-presidence-de-la-commission_600639.html', 'url': 'http://www.francetvinfo.fr/elections/europeennes/direct-europeennes-regardez-le-debat-entre-les-candidats-a-la-presidence-de-la-commission_600639.html',
'only_matching': True, 'only_matching': True,
@@ -384,6 +402,10 @@ class FranceTVInfoIE(FranceTVBaseInfoExtractor):
}, { }, {
'url': 'http://france3-regions.francetvinfo.fr/limousin/emissions/jt-1213-limousin', 'url': 'http://france3-regions.francetvinfo.fr/limousin/emissions/jt-1213-limousin',
'only_matching': True, 'only_matching': True,
}, {
# "<figure id=" pattern (#28792)
'url': 'https://www.francetvinfo.fr/culture/patrimoine/incendie-de-notre-dame-de-paris/notre-dame-de-paris-de-l-incendie-de-la-cathedrale-a-sa-reconstruction_4372291.html',
'only_matching': True,
}] }]
def _real_extract(self, url): def _real_extract(self, url):
@@ -401,7 +423,7 @@ class FranceTVInfoIE(FranceTVBaseInfoExtractor):
(r'player\.load[^;]+src:\s*["\']([^"\']+)', (r'player\.load[^;]+src:\s*["\']([^"\']+)',
r'id-video=([^@]+@[^"]+)', r'id-video=([^@]+@[^"]+)',
r'<a[^>]+href="(?:https?:)?//videos\.francetv\.fr/video/([^@]+@[^"]+)"', r'<a[^>]+href="(?:https?:)?//videos\.francetv\.fr/video/([^@]+@[^"]+)"',
r'data-id=["\']([\da-f]{8}-[\da-f]{4}-[\da-f]{4}-[\da-f]{4}-[\da-f]{12})'), r'(?:data-id|<figure[^<]+\bid)=["\']([\da-f]{8}-[\da-f]{4}-[\da-f]{4}-[\da-f]{4}-[\da-f]{12})'),
webpage, 'video id') webpage, 'video id')
return self._make_url_result(video_id) return self._make_url_result(video_id)
+5 -1
View File
@@ -16,7 +16,7 @@ from ..utils import (
class FunimationIE(InfoExtractor): class FunimationIE(InfoExtractor):
_VALID_URL = r'https?://(?:www\.)?funimation(?:\.com|now\.uk)/shows/[^/]+/(?P<id>[^/?#&]+)' _VALID_URL = r'https?://(?:www\.)?funimation(?:\.com|now\.uk)/(?:[^/]+/)?shows/[^/]+/(?P<id>[^/?#&]+)'
_NETRC_MACHINE = 'funimation' _NETRC_MACHINE = 'funimation'
_TOKEN = None _TOKEN = None
@@ -51,6 +51,10 @@ class FunimationIE(InfoExtractor):
}, { }, {
'url': 'https://www.funimationnow.uk/shows/puzzle-dragons-x/drop-impact/simulcast/', 'url': 'https://www.funimationnow.uk/shows/puzzle-dragons-x/drop-impact/simulcast/',
'only_matching': True, 'only_matching': True,
}, {
# with lang code
'url': 'https://www.funimation.com/en/shows/hacksign/role-play/',
'only_matching': True,
}] }]
def _login(self): def _login(self):
+81 -18
View File
@@ -5,7 +5,10 @@ import re
from .common import InfoExtractor from .common import InfoExtractor
from .kaltura import KalturaIE from .kaltura import KalturaIE
from ..utils import ( from ..utils import (
HEADRequest,
remove_start,
sanitized_Request, sanitized_Request,
smuggle_url,
urlencode_postdata, urlencode_postdata,
) )
@@ -100,6 +103,26 @@ class GDCVaultIE(InfoExtractor):
'format': 'mp4-408', 'format': 'mp4-408',
}, },
}, },
{
# Kaltura embed, whitespace between quote and embedded URL in iframe's src
'url': 'https://www.gdcvault.com/play/1025699',
'info_dict': {
'id': '0_zagynv0a',
'ext': 'mp4',
'title': 'Tech Toolbox',
'upload_date': '20190408',
'uploader_id': 'joe@blazestreaming.com',
'timestamp': 1554764629,
},
'params': {
'skip_download': True,
},
},
{
# HTML5 video
'url': 'http://www.gdcvault.com/play/1014846/Conference-Keynote-Shigeru',
'only_matching': True,
},
] ]
def _login(self, webpage_url, display_id): def _login(self, webpage_url, display_id):
@@ -120,38 +143,78 @@ class GDCVaultIE(InfoExtractor):
request = sanitized_Request(login_url, urlencode_postdata(login_form)) request = sanitized_Request(login_url, urlencode_postdata(login_form))
request.add_header('Content-Type', 'application/x-www-form-urlencoded') request.add_header('Content-Type', 'application/x-www-form-urlencoded')
self._download_webpage(request, display_id, 'Logging in') self._download_webpage(request, display_id, 'Logging in')
webpage = self._download_webpage(webpage_url, display_id, 'Getting authenticated video page') start_page = self._download_webpage(webpage_url, display_id, 'Getting authenticated video page')
self._download_webpage(logout_url, display_id, 'Logging out') self._download_webpage(logout_url, display_id, 'Logging out')
return webpage return start_page
def _real_extract(self, url): def _real_extract(self, url):
video_id, name = re.match(self._VALID_URL, url).groups() video_id, name = re.match(self._VALID_URL, url).groups()
display_id = name or video_id display_id = name or video_id
webpage = self._download_webpage(url, display_id) webpage_url = 'http://www.gdcvault.com/play/' + video_id
start_page = self._download_webpage(webpage_url, display_id)
title = self._html_search_regex( direct_url = self._search_regex(
r'<td><strong>Session Name:?</strong></td>\s*<td>(.*?)</td>', r's1\.addVariable\("file",\s*encodeURIComponent\("(/[^"]+)"\)\);',
webpage, 'title') start_page, 'url', default=None)
if direct_url:
title = self._html_search_regex(
r'<td><strong>Session Name:?</strong></td>\s*<td>(.*?)</td>',
start_page, 'title')
video_url = 'http://www.gdcvault.com' + direct_url
# resolve the url so that we can detect the correct extension
video_url = self._request_webpage(
HEADRequest(video_url), video_id).geturl()
PLAYER_REGEX = r'<iframe src=\"(?P<manifest_url>.*?)\".*?</iframe>' return {
manifest_url = self._html_search_regex( 'id': video_id,
PLAYER_REGEX, webpage, 'manifest_url') 'display_id': display_id,
'url': video_url,
'title': title,
}
partner_id = self._search_regex( embed_url = KalturaIE._extract_url(start_page)
r'/p(?:artner_id)?/(\d+)', manifest_url, 'partner id', if embed_url:
default='1670711') embed_url = smuggle_url(embed_url, {'source_url': url})
ie_key = 'Kaltura'
else:
PLAYER_REGEX = r'<iframe src="(?P<xml_root>.+?)/(?:gdc-)?player.*?\.html.*?".*?</iframe>'
kaltura_id = self._search_regex( xml_root = self._html_search_regex(
r'entry_id=(?P<id>(?:[^&])+)', manifest_url, PLAYER_REGEX, start_page, 'xml root', default=None)
'kaltura id', group='id') if xml_root is None:
# Probably need to authenticate
login_res = self._login(webpage_url, display_id)
if login_res is None:
self.report_warning('Could not login.')
else:
start_page = login_res
# Grab the url from the authenticated page
xml_root = self._html_search_regex(
PLAYER_REGEX, start_page, 'xml root')
xml_name = self._html_search_regex(
r'<iframe src=".*?\?xml(?:=|URL=xml/)(.+?\.xml).*?".*?</iframe>',
start_page, 'xml filename', default=None)
if not xml_name:
info = self._parse_html5_media_entries(url, start_page, video_id)[0]
info.update({
'title': remove_start(self._search_regex(
r'>Session Name:\s*<.*?>\s*<td>(.+?)</td>', start_page,
'title', default=None) or self._og_search_title(
start_page, default=None), 'GDC Vault - '),
'id': video_id,
'display_id': display_id,
})
return info
embed_url = '%s/xml/%s' % (xml_root, xml_name)
ie_key = 'DigitallySpeaking'
return { return {
'_type': 'url_transparent', '_type': 'url_transparent',
'url': 'kaltura:%s:%s' % (partner_id, kaltura_id),
'ie_key': KalturaIE.ie_key(),
'id': video_id, 'id': video_id,
'display_id': display_id, 'display_id': display_id,
'title': title, 'url': embed_url,
'ie_key': ie_key,
} }
+13 -8
View File
@@ -2376,7 +2376,7 @@ class GenericIE(InfoExtractor):
if default_search in ('auto', 'auto_warning', 'fixup_error'): if default_search in ('auto', 'auto_warning', 'fixup_error'):
if re.match(r'^[^\s/]+\.[^\s/]+/', url): if re.match(r'^[^\s/]+\.[^\s/]+/', url):
self._downloader.report_warning('The url doesn\'t specify the protocol, trying with http') self.report_warning('The url doesn\'t specify the protocol, trying with http')
return self.url_result('http://' + url) return self.url_result('http://' + url)
elif default_search != 'fixup_error': elif default_search != 'fixup_error':
if default_search == 'auto_warning': if default_search == 'auto_warning':
@@ -2385,7 +2385,7 @@ class GenericIE(InfoExtractor):
'Invalid URL: %r . Call yt-dlp like this: yt-dlp -v "https://www.youtube.com/watch?v=BaW_jenozKc" ' % url, 'Invalid URL: %r . Call yt-dlp like this: yt-dlp -v "https://www.youtube.com/watch?v=BaW_jenozKc" ' % url,
expected=True) expected=True)
else: else:
self._downloader.report_warning( self.report_warning(
'Falling back to youtube search for %s . Set --default-search "auto" to suppress this warning.' % url) 'Falling back to youtube search for %s . Set --default-search "auto" to suppress this warning.' % url)
return self.url_result('ytsearch:' + url) return self.url_result('ytsearch:' + url)
@@ -2444,8 +2444,9 @@ class GenericIE(InfoExtractor):
m = re.match(r'^(?P<type>audio|video|application(?=/(?:ogg$|(?:vnd\.apple\.|x-)?mpegurl)))/(?P<format_id>[^;\s]+)', content_type) m = re.match(r'^(?P<type>audio|video|application(?=/(?:ogg$|(?:vnd\.apple\.|x-)?mpegurl)))/(?P<format_id>[^;\s]+)', content_type)
if m: if m:
format_id = compat_str(m.group('format_id')) format_id = compat_str(m.group('format_id'))
subtitles = {}
if format_id.endswith('mpegurl'): if format_id.endswith('mpegurl'):
formats = self._extract_m3u8_formats(url, video_id, 'mp4') formats, subtitles = self._extract_m3u8_formats_and_subtitles(url, video_id, 'mp4')
elif format_id == 'f4m': elif format_id == 'f4m':
formats = self._extract_f4m_formats(url, video_id) formats = self._extract_f4m_formats(url, video_id)
else: else:
@@ -2457,11 +2458,12 @@ class GenericIE(InfoExtractor):
info_dict['direct'] = True info_dict['direct'] = True
self._sort_formats(formats) self._sort_formats(formats)
info_dict['formats'] = formats info_dict['formats'] = formats
info_dict['subtitles'] = subtitles
return info_dict return info_dict
if not self._downloader.params.get('test', False) and not is_intentional: if not self._downloader.params.get('test', False) and not is_intentional:
force = self._downloader.params.get('force_generic_extractor', False) force = self._downloader.params.get('force_generic_extractor', False)
self._downloader.report_warning( self.report_warning(
'%s on generic information extractor.' % ('Forcing' if force else 'Falling back')) '%s on generic information extractor.' % ('Forcing' if force else 'Falling back'))
if not full_response: if not full_response:
@@ -2488,7 +2490,7 @@ class GenericIE(InfoExtractor):
# Maybe it's a direct link to a video? # Maybe it's a direct link to a video?
# Be careful not to download the whole thing! # Be careful not to download the whole thing!
if not is_html(first_bytes): if not is_html(first_bytes):
self._downloader.report_warning( self.report_warning(
'URL could be a direct video link, returning it as such.') 'URL could be a direct video link, returning it as such.')
info_dict.update({ info_dict.update({
'direct': True, 'direct': True,
@@ -2506,11 +2508,14 @@ class GenericIE(InfoExtractor):
# Is it an RSS feed, a SMIL file, an XSPF playlist or a MPD manifest? # Is it an RSS feed, a SMIL file, an XSPF playlist or a MPD manifest?
try: try:
doc = compat_etree_fromstring(webpage.encode('utf-8')) try:
doc = compat_etree_fromstring(webpage)
except compat_xml_parse_error:
doc = compat_etree_fromstring(webpage.encode('utf-8'))
if doc.tag == 'rss': if doc.tag == 'rss':
return self._extract_rss(url, video_id, doc) return self._extract_rss(url, video_id, doc)
elif doc.tag == 'SmoothStreamingMedia': elif doc.tag == 'SmoothStreamingMedia':
info_dict['formats'] = self._parse_ism_formats(doc, url) info_dict['formats'], info_dict['subtitles'] = self._parse_ism_formats_and_subtitles(doc, url)
self._sort_formats(info_dict['formats']) self._sort_formats(info_dict['formats'])
return info_dict return info_dict
elif re.match(r'^(?:{[^}]+})?smil$', doc.tag): elif re.match(r'^(?:{[^}]+})?smil$', doc.tag):
@@ -2524,7 +2529,7 @@ class GenericIE(InfoExtractor):
xspf_base_url=full_response.geturl()), xspf_base_url=full_response.geturl()),
video_id) video_id)
elif re.match(r'(?i)^(?:{[^}]+})?MPD$', doc.tag): elif re.match(r'(?i)^(?:{[^}]+})?MPD$', doc.tag):
info_dict['formats'] = self._parse_mpd_formats( info_dict['formats'], info_dict['subtitles'] = self._parse_mpd_formats_and_subtitles(
doc, doc,
mpd_base_url=full_response.geturl().rpartition('/')[0], mpd_base_url=full_response.geturl().rpartition('/')[0],
mpd_url=url) mpd_url=url)
+48 -17
View File
@@ -4,10 +4,14 @@ from __future__ import unicode_literals
import re import re
from .adobepass import AdobePassIE from .adobepass import AdobePassIE
from ..compat import compat_str
from ..utils import ( from ..utils import (
int_or_none, int_or_none,
determine_ext, determine_ext,
parse_age_limit, parse_age_limit,
remove_start,
remove_end,
try_get,
urlencode_postdata, urlencode_postdata,
ExtractorError, ExtractorError,
) )
@@ -46,15 +50,15 @@ class GoIE(AdobePassIE):
} }
_VALID_URL = r'''(?x) _VALID_URL = r'''(?x)
https?:// https?://
(?: (?P<sub_domain>
(?:(?P<sub_domain>%s)\.)?go| (?:%s\.)?go|fxnow\.fxnetworks|
(?P<sub_domain_2>abc|freeform|disneynow|fxnow\.fxnetworks) (?:www\.)?(?:abc|freeform|disneynow)
)\.com/ )\.com/
(?: (?:
(?:[^/]+/)*(?P<id>[Vv][Dd][Kk][Aa]\w+)| (?:[^/]+/)*(?P<id>[Vv][Dd][Kk][Aa]\w+)|
(?:[^/]+/)*(?P<display_id>[^/?\#]+) (?:[^/]+/)*(?P<display_id>[^/?\#]+)
) )
''' % '|'.join(list(_SITE_INFO.keys())) ''' % r'\.|'.join(list(_SITE_INFO.keys()))
_TESTS = [{ _TESTS = [{
'url': 'http://abc.go.com/shows/designated-survivor/video/most-recent/VDKA3807643', 'url': 'http://abc.go.com/shows/designated-survivor/video/most-recent/VDKA3807643',
'info_dict': { 'info_dict': {
@@ -116,6 +120,18 @@ class GoIE(AdobePassIE):
# m3u8 download # m3u8 download
'skip_download': True, 'skip_download': True,
}, },
}, {
'url': 'https://abc.com/shows/modern-family/episode-guide/season-01/101-pilot',
'info_dict': {
'id': 'VDKA22600213',
'ext': 'mp4',
'title': 'Pilot',
'description': 'md5:74306df917cfc199d76d061d66bebdb4',
},
'params': {
# m3u8 download
'skip_download': True,
},
}, { }, {
'url': 'http://abc.go.com/shows/the-catch/episode-guide/season-01/10-the-wedding', 'url': 'http://abc.go.com/shows/the-catch/episode-guide/season-01/10-the-wedding',
'only_matching': True, 'only_matching': True,
@@ -133,6 +149,9 @@ class GoIE(AdobePassIE):
}, { }, {
'url': 'https://disneynow.com/shows/minnies-bow-toons/video/happy-campers/vdka4872013', 'url': 'https://disneynow.com/shows/minnies-bow-toons/video/happy-campers/vdka4872013',
'only_matching': True, 'only_matching': True,
}, {
'url': 'https://www.freeform.com/shows/cruel-summer/episode-guide/season-01/01-happy-birthday-jeanette-turner',
'only_matching': True,
}] }]
def _extract_videos(self, brand, video_id='-1', show_id='-1'): def _extract_videos(self, brand, video_id='-1', show_id='-1'):
@@ -143,24 +162,36 @@ class GoIE(AdobePassIE):
def _real_extract(self, url): def _real_extract(self, url):
mobj = re.match(self._VALID_URL, url) mobj = re.match(self._VALID_URL, url)
sub_domain = mobj.group('sub_domain') or mobj.group('sub_domain_2') sub_domain = remove_start(remove_end(mobj.group('sub_domain') or '', '.go'), 'www.')
video_id, display_id = mobj.group('id', 'display_id') video_id, display_id = mobj.group('id', 'display_id')
site_info = self._SITE_INFO.get(sub_domain, {}) site_info = self._SITE_INFO.get(sub_domain, {})
brand = site_info.get('brand') brand = site_info.get('brand')
if not video_id or not site_info: if not video_id or not site_info:
webpage = self._download_webpage(url, display_id or video_id) webpage = self._download_webpage(url, display_id or video_id)
video_id = self._search_regex( data = self._parse_json(
( self._search_regex(
# There may be inner quotes, e.g. data-video-id="'VDKA3609139'" r'["\']__abc_com__["\']\s*\]\s*=\s*({.+?})\s*;', webpage,
# from http://freeform.go.com/shows/shadowhunters/episodes/season-2/1-this-guilty-blood 'data', default='{}'),
r'data-video-id=["\']*(VDKA\w+)', display_id or video_id, fatal=False)
# https://github.com/ytdl-org/youtube-dl/pull/25216/files # https://abc.com/shows/modern-family/episode-guide/season-01/101-pilot
# The following is based on the pull request on the line above. Changed the ABC.com URL to a show available now. layout = try_get(data, lambda x: x['page']['content']['video']['layout'], dict)
# https://abc.com/shows/the-rookie/episode-guide/season-02/19-the-q-word video_id = None
r'\bvideoIdCode["\']\s*:\s*["\'](vdka\w+)', if layout:
# Deprecated fallback pattern video_id = try_get(
r'\b(?:video)?id["\']\s*:\s*["\'](VDKA\w+)' layout,
), webpage, 'video id', default=video_id) (lambda x: x['videoid'], lambda x: x['video']['id']),
compat_str)
if not video_id:
video_id = self._search_regex(
(
# There may be inner quotes, e.g. data-video-id="'VDKA3609139'"
# from http://freeform.go.com/shows/shadowhunters/episodes/season-2/1-this-guilty-blood
r'data-video-id=["\']*(VDKA\w+)',
# page.analytics.videoIdCode
r'\bvideoIdCode["\']\s*:\s*["\']((?:vdka|VDKA)\w+)',
# https://abc.com/shows/the-rookie/episode-guide/season-02/03-the-bet
r'\b(?:video)?id["\']\s*:\s*["\'](VDKA\w+)'
), webpage, 'video id', default=video_id)
if not site_info: if not site_info:
brand = self._search_regex( brand = self._search_regex(
(r'data-brand=\s*["\']\s*(\d+)', (r'data-brand=\s*["\']\s*(\d+)',
+1 -1
View File
@@ -253,7 +253,7 @@ class GoogleDriveIE(InfoExtractor):
or 'unable to extract confirmation code') or 'unable to extract confirmation code')
if not formats and reason: if not formats and reason:
raise ExtractorError(reason, expected=True) self.raise_no_formats(reason, expected=True)
self._sort_formats(formats) self._sort_formats(formats)
+1 -1
View File
@@ -184,7 +184,7 @@ class HotStarIE(HotStarBaseIE):
geo_restricted = True geo_restricted = True
continue continue
if not formats and geo_restricted: if not formats and geo_restricted:
self.raise_geo_restricted(countries=['IN']) self.raise_geo_restricted(countries=['IN'], metadata_available=True)
self._sort_formats(formats) self._sort_formats(formats)
for f in formats: for f in formats:
+1 -1
View File
@@ -136,7 +136,7 @@ class IPrimaIE(InfoExtractor):
extract_formats(src) extract_formats(src)
if not formats and '>GEO_IP_NOT_ALLOWED<' in playerpage: if not formats and '>GEO_IP_NOT_ALLOWED<' in playerpage:
self.raise_geo_restricted(countries=['CZ']) self.raise_geo_restricted(countries=['CZ'], metadata_available=True)
self._sort_formats(formats) self._sort_formats(formats)
+1 -1
View File
@@ -280,7 +280,7 @@ class IqiyiIE(InfoExtractor):
msg = 'error %s' % code msg = 'error %s' % code
if validation_result.get('msg'): if validation_result.get('msg'):
msg += ': ' + validation_result['msg'] msg += ': ' + validation_result['msg']
self._downloader.report_warning('unable to log in: ' + msg) self.report_warning('unable to log in: ' + msg)
return False return False
return True return True
+7 -7
View File
@@ -120,7 +120,7 @@ class KalturaIE(InfoExtractor):
def _extract_urls(webpage): def _extract_urls(webpage):
# Embed codes: https://knowledge.kaltura.com/embedding-kaltura-media-players-your-site # Embed codes: https://knowledge.kaltura.com/embedding-kaltura-media-players-your-site
finditer = ( finditer = (
re.finditer( list(re.finditer(
r"""(?xs) r"""(?xs)
kWidget\.(?:thumb)?[Ee]mbed\( kWidget\.(?:thumb)?[Ee]mbed\(
\{.*? \{.*?
@@ -128,8 +128,8 @@ class KalturaIE(InfoExtractor):
(?P<q2>['"])_?(?P<partner_id>(?:(?!(?P=q2)).)+)(?P=q2),.*? (?P<q2>['"])_?(?P<partner_id>(?:(?!(?P=q2)).)+)(?P=q2),.*?
(?P<q3>['"])entry_?[Ii]d(?P=q3)\s*:\s* (?P<q3>['"])entry_?[Ii]d(?P=q3)\s*:\s*
(?P<q4>['"])(?P<id>(?:(?!(?P=q4)).)+)(?P=q4)(?:,|\s*\}) (?P<q4>['"])(?P<id>(?:(?!(?P=q4)).)+)(?P=q4)(?:,|\s*\})
""", webpage) """, webpage))
or re.finditer( or list(re.finditer(
r'''(?xs) r'''(?xs)
(?P<q1>["']) (?P<q1>["'])
(?:https?:)?//cdnapi(?:sec)?\.kaltura\.com(?::\d+)?/(?:(?!(?P=q1)).)*\b(?:p|partner_id)/(?P<partner_id>\d+)(?:(?!(?P=q1)).)* (?:https?:)?//cdnapi(?:sec)?\.kaltura\.com(?::\d+)?/(?:(?!(?P=q1)).)*\b(?:p|partner_id)/(?P<partner_id>\d+)(?:(?!(?P=q1)).)*
@@ -142,16 +142,16 @@ class KalturaIE(InfoExtractor):
\[\s*(?P<q2_1>["'])entry_?[Ii]d(?P=q2_1)\s*\]\s*=\s* \[\s*(?P<q2_1>["'])entry_?[Ii]d(?P=q2_1)\s*\]\s*=\s*
) )
(?P<q3>["'])(?P<id>(?:(?!(?P=q3)).)+)(?P=q3) (?P<q3>["'])(?P<id>(?:(?!(?P=q3)).)+)(?P=q3)
''', webpage) ''', webpage))
or re.finditer( or list(re.finditer(
r'''(?xs) r'''(?xs)
<(?:iframe[^>]+src|meta[^>]+\bcontent)=(?P<q1>["']) <(?:iframe[^>]+src|meta[^>]+\bcontent)=(?P<q1>["'])\s*
(?:https?:)?//(?:(?:www|cdnapi(?:sec)?)\.)?kaltura\.com/(?:(?!(?P=q1)).)*\b(?:p|partner_id)/(?P<partner_id>\d+) (?:https?:)?//(?:(?:www|cdnapi(?:sec)?)\.)?kaltura\.com/(?:(?!(?P=q1)).)*\b(?:p|partner_id)/(?P<partner_id>\d+)
(?:(?!(?P=q1)).)* (?:(?!(?P=q1)).)*
[?&;]entry_id=(?P<id>(?:(?!(?P=q1))[^&])+) [?&;]entry_id=(?P<id>(?:(?!(?P=q1))[^&])+)
(?:(?!(?P=q1)).)* (?:(?!(?P=q1)).)*
(?P=q1) (?P=q1)
''', webpage) ''', webpage))
) )
urls = [] urls = []
for mobj in finditer: for mobj in finditer:
+1 -1
View File
@@ -101,7 +101,7 @@ class KeezMoviesIE(InfoExtractor):
if not formats: if not formats:
if 'title="This video is no longer available"' in webpage: if 'title="This video is no longer available"' in webpage:
raise ExtractorError( self.raise_no_formats(
'Video %s is no longer available' % video_id, expected=True) 'Video %s is no longer available' % video_id, expected=True)
try: try:
+29 -1
View File
@@ -122,6 +122,26 @@ class LBRYIE(LBRYBaseIE):
'channel_url': 'https://lbry.tv/@LBRYFoundation:0ed629d2b9c601300cacf7eabe9da0be79010212', 'channel_url': 'https://lbry.tv/@LBRYFoundation:0ed629d2b9c601300cacf7eabe9da0be79010212',
'vcodec': 'none', 'vcodec': 'none',
} }
}, {
# HLS
'url': 'https://odysee.com/@gardeningincanada:b/plants-i-will-never-grow-again.-the:e',
'md5': 'fc82f45ea54915b1495dd7cb5cc1289f',
'info_dict': {
'id': 'e51671357333fe22ae88aad320bde2f6f96b1410',
'ext': 'mp4',
'title': 'PLANTS I WILL NEVER GROW AGAIN. THE BLACK LIST PLANTS FOR A CANADIAN GARDEN | Gardening in Canada 🍁',
'description': 'md5:9c539c6a03fb843956de61a4d5288d5e',
'timestamp': 1618254123,
'upload_date': '20210412',
'release_timestamp': 1618254002,
'release_date': '20210412',
'tags': list,
'duration': 554,
'channel': 'Gardening In Canada',
'channel_id': 'b8be0e93b423dad221abe29545fbe8ec36e806bc',
'channel_url': 'https://odysee.com/@gardeningincanada:b8be0e93b423dad221abe29545fbe8ec36e806bc',
'formats': 'mincount:3',
}
}, { }, {
'url': 'https://odysee.com/@BrodieRobertson:5/apple-is-tracking-everything-you-do-on:e', 'url': 'https://odysee.com/@BrodieRobertson:5/apple-is-tracking-everything-you-do-on:e',
'only_matching': True, 'only_matching': True,
@@ -168,10 +188,18 @@ class LBRYIE(LBRYBaseIE):
streaming_url = self._call_api_proxy( streaming_url = self._call_api_proxy(
'get', claim_id, {'uri': uri}, 'streaming url')['streaming_url'] 'get', claim_id, {'uri': uri}, 'streaming url')['streaming_url']
info = self._parse_stream(result, url) info = self._parse_stream(result, url)
urlh = self._request_webpage(
streaming_url, display_id, note='Downloading streaming redirect url info')
if determine_ext(urlh.geturl()) == 'm3u8':
info['formats'] = self._extract_m3u8_formats(
urlh.geturl(), display_id, 'mp4', entry_protocol='m3u8_native',
m3u8_id='hls')
self._sort_formats(info['formats'])
else:
info['url'] = streaming_url
info.update({ info.update({
'id': claim_id, 'id': claim_id,
'title': title, 'title': title,
'url': streaming_url,
}) })
return info return info
+4 -1
View File
@@ -160,7 +160,10 @@ class LimelightBaseIE(InfoExtractor):
for mobile_url in mobile_item.get('mobileUrls', []): for mobile_url in mobile_item.get('mobileUrls', []):
media_url = mobile_url.get('mobileUrl') media_url = mobile_url.get('mobileUrl')
format_id = mobile_url.get('targetMediaPlatform') format_id = mobile_url.get('targetMediaPlatform')
if not media_url or format_id in ('Widevine', 'SmoothStreaming') or media_url in urls: if not media_url or media_url in urls:
continue
if (format_id in ('Widevine', 'SmoothStreaming')
and not self._downloader.params.get('allow_unplayable_formats', False)):
continue continue
urls.append(media_url) urls.append(media_url)
ext = determine_ext(media_url) ext = determine_ext(media_url)
+2 -3
View File
@@ -6,7 +6,6 @@ import re
from .common import InfoExtractor from .common import InfoExtractor
from ..compat import compat_str from ..compat import compat_str
from ..utils import ( from ..utils import (
ExtractorError,
int_or_none, int_or_none,
js_to_json, js_to_json,
str_or_none, str_or_none,
@@ -77,7 +76,7 @@ class LineTVIE(InfoExtractor):
self._sort_formats(formats) self._sort_formats(formats)
if not formats[0].get('width'): if formats and not formats[0].get('width'):
formats[0]['vcodec'] = 'none' formats[0]['vcodec'] = 'none'
title = self._og_search_title(webpage) title = self._og_search_title(webpage)
@@ -183,7 +182,7 @@ class LineLiveIE(LineLiveBaseIE):
if not formats: if not formats:
archive_status = item.get('archiveStatus') archive_status = item.get('archiveStatus')
if archive_status != 'ARCHIVED': if archive_status != 'ARCHIVED':
raise ExtractorError('this video has been ' + archive_status.lower(), expected=True) self.raise_no_formats('this video has been ' + archive_status.lower(), expected=True)
self._sort_formats(formats) self._sort_formats(formats)
info['formats'] = formats info['formats'] = formats
return info return info
+1 -1
View File
@@ -331,7 +331,7 @@ class LyndaCourseIE(LyndaBaseIE):
}) })
if unaccessible_videos > 0: if unaccessible_videos > 0:
self._downloader.report_warning( self.report_warning(
'%s videos are only available for members (or paid members) and will not be downloaded. ' '%s videos are only available for members (or paid members) and will not be downloaded. '
% unaccessible_videos + self._ACCOUNT_CREDENTIALS_HINT) % unaccessible_videos + self._ACCOUNT_CREDENTIALS_HINT)
+15 -9
View File
@@ -15,33 +15,39 @@ from ..utils import (
class MedalTVIE(InfoExtractor): class MedalTVIE(InfoExtractor):
_VALID_URL = r'https?://(?:www\.)?medal\.tv/clips/(?P<id>[0-9]+)' _VALID_URL = r'https?://(?:www\.)?medal\.tv/clips/(?P<id>[^/?#&]+)'
_TESTS = [{ _TESTS = [{
'url': 'https://medal.tv/clips/34934644/3Is9zyGMoBMr', 'url': 'https://medal.tv/clips/2mA60jWAGQCBH',
'md5': '7b07b064331b1cf9e8e5c52a06ae68fa', 'md5': '7b07b064331b1cf9e8e5c52a06ae68fa',
'info_dict': { 'info_dict': {
'id': '34934644', 'id': '2mA60jWAGQCBH',
'ext': 'mp4', 'ext': 'mp4',
'title': 'Quad Cold', 'title': 'Quad Cold',
'description': 'Medal,https://medal.tv/desktop/', 'description': 'Medal,https://medal.tv/desktop/',
'uploader': 'MowgliSB', 'uploader': 'MowgliSB',
'timestamp': 1603165266, 'timestamp': 1603165266,
'upload_date': '20201020', 'upload_date': '20201020',
'uploader_id': 10619174, 'uploader_id': '10619174',
} }
}, { }, {
'url': 'https://medal.tv/clips/36787208', 'url': 'https://medal.tv/clips/2um24TWdty0NA',
'md5': 'b6dc76b78195fff0b4f8bf4a33ec2148', 'md5': 'b6dc76b78195fff0b4f8bf4a33ec2148',
'info_dict': { 'info_dict': {
'id': '36787208', 'id': '2um24TWdty0NA',
'ext': 'mp4', 'ext': 'mp4',
'title': 'u tk me i tk u bigger', 'title': 'u tk me i tk u bigger',
'description': 'Medal,https://medal.tv/desktop/', 'description': 'Medal,https://medal.tv/desktop/',
'uploader': 'Mimicc', 'uploader': 'Mimicc',
'timestamp': 1605580939, 'timestamp': 1605580939,
'upload_date': '20201117', 'upload_date': '20201117',
'uploader_id': 5156321, 'uploader_id': '5156321',
} }
}, {
'url': 'https://medal.tv/clips/37rMeFpryCC-9',
'only_matching': True,
}, {
'url': 'https://medal.tv/clips/2WRj40tpY_EU9',
'only_matching': True,
}] }]
def _real_extract(self, url): def _real_extract(self, url):
@@ -97,11 +103,11 @@ class MedalTVIE(InfoExtractor):
error = clip.get('error') error = clip.get('error')
if not formats and error: if not formats and error:
if error == 404: if error == 404:
raise ExtractorError( self.raise_no_formats(
'That clip does not exist.', 'That clip does not exist.',
expected=True, video_id=video_id) expected=True, video_id=video_id)
else: else:
raise ExtractorError( self.raise_no_formats(
'An unknown error occurred ({0}).'.format(error), 'An unknown error occurred ({0}).'.format(error),
video_id=video_id) video_id=video_id)
+1 -1
View File
@@ -26,7 +26,7 @@ _ID_RE = r'(?:[0-9a-f]{32,34}|[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0
class MediasiteIE(InfoExtractor): class MediasiteIE(InfoExtractor):
_VALID_URL = r'(?xi)https?://[^/]+/Mediasite/(?:Play|Showcase/(?:default|livebroadcast)/Presentation)/(?P<id>%s)(?P<query>\?[^#]+|)' % _ID_RE _VALID_URL = r'(?xi)https?://[^/]+/Mediasite/(?:Play|Showcase/[^/#?]+/Presentation)/(?P<id>%s)(?P<query>\?[^#]+|)' % _ID_RE
_TESTS = [ _TESTS = [
{ {
'url': 'https://hitsmediaweb.h-its.org/mediasite/Play/2db6c271681e4f199af3c60d1f82869b1d', 'url': 'https://hitsmediaweb.h-its.org/mediasite/Play/2db6c271681e4f199af3c60d1f82869b1d',
+40 -66
View File
@@ -1,22 +1,20 @@
# coding: utf-8 # coding: utf-8
from __future__ import unicode_literals from __future__ import unicode_literals
import base64
from datetime import datetime from datetime import datetime
import itertools import itertools
import json import json
import base64
import re import re
from .common import InfoExtractor from .common import InfoExtractor
from ..utils import ( from ..utils import (
ExtractorError, std_headers, std_headers,
update_url_query, update_url_query,
random_uuidv4, random_uuidv4,
try_get, try_get,
) )
from ..compat import ( from ..compat import (
compat_urlparse,
compat_urllib_parse_urlencode,
compat_str, compat_str,
) )
@@ -48,29 +46,24 @@ class MildomBaseIE(InfoExtractor):
def _fetch_dispatcher_config(self): def _fetch_dispatcher_config(self):
if not self._DISPATCHER_CONFIG: if not self._DISPATCHER_CONFIG:
try: tmp = self._download_json(
tmp = self._download_json( 'https://disp.mildom.com/serverListV2', 'initialization',
'https://disp.mildom.com/serverListV2', 'initialization', note='Downloading dispatcher_config', data=json.dumps({
note='Downloading dispatcher_config', data=json.dumps({ 'protover': 0,
'protover': 0, 'data': base64.b64encode(json.dumps({
'data': base64.b64encode(json.dumps({ 'fr': 'web',
'fr': 'web', 'sfr': 'pc',
'sfr': 'pc', 'devi': 'Windows',
'devi': 'Windows', 'la': 'ja',
'la': 'ja', 'gid': None,
'gid': None, 'loc': '',
'loc': '', 'clu': '',
'clu': '', 'wh': '1919*810',
'wh': '1919*810', 'rtm': self.iso_timestamp(),
'rtm': self.iso_timestamp(), 'ua': std_headers['User-Agent'],
'ua': std_headers['User-Agent'], }).encode('utf8')).decode('utf8').replace('\n', ''),
}).encode('utf8')).decode('utf8').replace('\n', ''), }).encode('utf8'))
}).encode('utf8')) self._DISPATCHER_CONFIG = self._parse_json(base64.b64decode(tmp['data']), 'initialization')
self._DISPATCHER_CONFIG = self._parse_json(base64.b64decode(tmp['data']), 'initialization')
except ExtractorError:
self._DISPATCHER_CONFIG = self._download_json(
'https://bookish-octo-barnacle.vercel.app/api/mildom/dispatcher_config', 'initialization',
note='Downloading dispatcher_config fallback')
return self._DISPATCHER_CONFIG return self._DISPATCHER_CONFIG
@staticmethod @staticmethod
@@ -145,15 +138,10 @@ class MildomIE(MildomBaseIE):
'Referer': 'https://www.mildom.com/', 'Referer': 'https://www.mildom.com/',
'Origin': 'https://www.mildom.com', 'Origin': 'https://www.mildom.com',
}, note='Downloading m3u8 information') }, note='Downloading m3u8 information')
del stream_query['streamReqId'], stream_query['timestamp'] del stream_query['streamReqId'], stream_query['timestamp']
for fmt in formats: for fmt in formats:
# Uses https://github.com/nao20010128nao/bookish-octo-barnacle by @nao20010128nao as a proxy fmt.setdefault('http_headers', {})['Referer'] = 'https://www.mildom.com/'
parsed = compat_urlparse.urlparse(fmt['url'])
parsed = parsed._replace(
netloc='bookish-octo-barnacle.vercel.app',
query=compat_urllib_parse_urlencode(stream_query, True),
path='/api/mildom' + parsed.path)
fmt['url'] = compat_urlparse.urlunparse(parsed)
self._sort_formats(formats) self._sort_formats(formats)
@@ -200,16 +188,16 @@ class MildomVodIE(MildomBaseIE):
lambda x: x['author_info']['login_name'], lambda x: x['author_info']['login_name'],
), compat_str) ), compat_str)
audio_formats = [{ formats = [{
'url': autoplay['audio_url'], 'url': autoplay['audio_url'],
'format_id': 'audio', 'format_id': 'audio',
'protocol': 'm3u8_native', 'protocol': 'm3u8_native',
'vcodec': 'none', 'vcodec': 'none',
'acodec': 'aac', 'acodec': 'aac',
'ext': 'm4a'
}] }]
video_formats = []
for fmt in autoplay['video_link']: for fmt in autoplay['video_link']:
video_formats.append({ formats.append({
'format_id': 'video-%s' % fmt['name'], 'format_id': 'video-%s' % fmt['name'],
'url': fmt['url'], 'url': fmt['url'],
'protocol': 'm3u8_native', 'protocol': 'm3u8_native',
@@ -217,23 +205,9 @@ class MildomVodIE(MildomBaseIE):
'height': fmt['level'], 'height': fmt['level'],
'vcodec': 'h264', 'vcodec': 'h264',
'acodec': 'aac', 'acodec': 'aac',
'ext': 'mp4'
}) })
stream_query = self._common_queries({
'is_lhls': '0',
})
del stream_query['timestamp']
formats = audio_formats + video_formats
for fmt in formats:
fmt['ext'] = 'mp4'
parsed = compat_urlparse.urlparse(fmt['url'])
stream_query['path'] = parsed.path[5:]
parsed = parsed._replace(
netloc='bookish-octo-barnacle.vercel.app',
query=compat_urllib_parse_urlencode(stream_query, True),
path='/api/mildom/vod2/proxy')
fmt['url'] = compat_urlparse.urlunparse(parsed)
self._sort_formats(formats) self._sort_formats(formats)
return { return {
@@ -259,16 +233,7 @@ class MildomUserVodIE(MildomBaseIE):
'playlist_mincount': 351, 'playlist_mincount': 351,
}] }]
def _real_extract(self, url): def _entries(self, user_id):
user_id = self._match_id(url)
self._downloader.report_warning('To download ongoing live, please use "https://www.mildom.com/%s" instead. This will list up VODs belonging to user.' % user_id)
profile = self._call_api(
'https://cloudac.mildom.com/nonolive/gappserv/user/profileV2', user_id,
query={'user_id': user_id}, note='Downloading user profile')['user_info']
results = []
for page in itertools.count(1): for page in itertools.count(1):
reply = self._call_api( reply = self._call_api(
'https://cloudac.mildom.com/nonolive/videocontent/profile/playbackList', 'https://cloudac.mildom.com/nonolive/videocontent/profile/playbackList',
@@ -279,7 +244,16 @@ class MildomUserVodIE(MildomBaseIE):
}) })
if not reply: if not reply:
break break
results.extend('https://www.mildom.com/playback/%s/%s' % (user_id, x['v_id']) for x in reply) for x in reply:
return self.playlist_result([ yield self.url_result('https://www.mildom.com/playback/%s/%s' % (user_id, x['v_id']))
self.url_result(u, ie=MildomVodIE.ie_key()) for u in results
], user_id, 'Uploads from %s' % profile['loginname']) def _real_extract(self, url):
user_id = self._match_id(url)
self.to_screen('This will download all VODs belonging to user. To download ongoing live video, use "https://www.mildom.com/%s" instead' % user_id)
profile = self._call_api(
'https://cloudac.mildom.com/nonolive/gappserv/user/profileV2', user_id,
query={'user_id': user_id}, note='Downloading user profile')['user_info']
return self.playlist_result(
self._entries(user_id), user_id, 'Uploads from %s' % profile['loginname'])
+1 -1
View File
@@ -157,7 +157,7 @@ class MixcloudIE(MixcloudBaseIE):
}) })
if not formats and cloudcast.get('isExclusive'): if not formats and cloudcast.get('isExclusive'):
self.raise_login_required() self.raise_login_required(metadata_available=True)
self._sort_formats(formats) self._sort_formats(formats)
+46 -2
View File
@@ -3,6 +3,7 @@ from __future__ import unicode_literals
import re import re
from .common import InfoExtractor from .common import InfoExtractor
from ..compat import compat_str
from ..utils import ( from ..utils import (
ExtractorError, ExtractorError,
js_to_json, js_to_json,
@@ -14,7 +15,7 @@ from ..utils import (
class MxplayerIE(InfoExtractor): class MxplayerIE(InfoExtractor):
_VALID_URL = r'https?://(?:www\.)?mxplayer\.in/(?:show|movie)/(?:(?P<display_id>[-/a-z0-9]+)-)?(?P<id>[a-z0-9]+)' _VALID_URL = r'https?://(?:www\.)?mxplayer\.in/(?:movie|show/[-\w]+/[-\w]+)/(?P<display_id>[-\w]+)-(?P<id>\w+)'
_TESTS = [{ _TESTS = [{
'url': 'https://www.mxplayer.in/movie/watch-knock-knock-hindi-dubbed-movie-online-b9fa28df3bfb8758874735bbd7d2655a?watch=true', 'url': 'https://www.mxplayer.in/movie/watch-knock-knock-hindi-dubbed-movie-online-b9fa28df3bfb8758874735bbd7d2655a?watch=true',
'info_dict': { 'info_dict': {
@@ -117,7 +118,7 @@ class MxplayerIE(InfoExtractor):
self._sort_formats(formats) self._sort_formats(formats)
return { return {
'id': video_id, 'id': video_id,
'display_id': display_id.replace('/', '-'), 'display_id': display_id,
'title': video_dict['title'] or self._og_search_title(webpage), 'title': video_dict['title'] or self._og_search_title(webpage),
'formats': formats, 'formats': formats,
'description': video_dict.get('description'), 'description': video_dict.get('description'),
@@ -125,3 +126,46 @@ class MxplayerIE(InfoExtractor):
'series': try_get(video_dict, lambda x: x['container']['container']['title']), 'series': try_get(video_dict, lambda x: x['container']['container']['title']),
'thumbnails': thumbnails, 'thumbnails': thumbnails,
} }
class MxplayerShowIE(InfoExtractor):
_VALID_URL = r'(?:https?://)(?:www\.)?mxplayer\.in/show/(?P<display_id>[-\w]+)-(?P<id>\w+)/?(?:$|[#?])'
_TESTS = [{
'url': 'https://www.mxplayer.in/show/watch-chakravartin-ashoka-samrat-series-online-a8f44e3cc0814b5601d17772cedf5417',
'playlist_mincount': 440,
'info_dict': {
'id': 'a8f44e3cc0814b5601d17772cedf5417',
'title': 'Watch Chakravartin Ashoka Samrat Series Online',
}
}]
_API_SHOW_URL = "https://api.mxplay.com/v1/web/detail/tab/tvshowseasons?type=tv_show&id={}&device-density=2&platform=com.mxplay.desktop&content-languages=hi,en"
_API_EPISODES_URL = "https://api.mxplay.com/v1/web/detail/tab/tvshowepisodes?type=season&id={}&device-density=1&platform=com.mxplay.desktop&content-languages=hi,en&{}"
def _entries(self, show_id):
show_json = self._download_json(
self._API_SHOW_URL.format(show_id),
video_id=show_id, headers={'Referer': 'https://mxplayer.in'})
page_num = 0
for season in show_json.get('items') or []:
season_id = try_get(season, lambda x: x['id'], compat_str)
next_url = ''
while next_url is not None:
page_num += 1
season_json = self._download_json(
self._API_EPISODES_URL.format(season_id, next_url),
video_id=season_id,
headers={'Referer': 'https://mxplayer.in'},
note='Downloading JSON metadata page %d' % page_num)
for episode in season_json.get('items') or []:
video_url = episode['webUrl']
yield self.url_result(
'https://mxplayer.in%s' % video_url,
ie=MxplayerIE.ie_key(), video_id=video_url.split('-')[-1])
next_url = season_json.get('next')
def _real_extract(self, url):
display_id, show_id = re.match(self._VALID_URL, url).groups()
return self.playlist_result(
self._entries(show_id), playlist_id=show_id,
playlist_title=display_id.replace('-', ' ').title())
+8 -4
View File
@@ -15,10 +15,10 @@ from ..utils import (
class NebulaIE(InfoExtractor): class NebulaIE(InfoExtractor):
_VALID_URL = r'https?://(?:www\.)?watchnebula\.com/videos/(?P<id>[-\w]+)' _VALID_URL = r'https?://(?:www\.)?(?:watchnebula\.com|nebula\.app)/videos/(?P<id>[-\w]+)'
_TESTS = [ _TESTS = [
{ {
'url': 'https://watchnebula.com/videos/that-time-disney-remade-beauty-and-the-beast', 'url': 'https://nebula.app/videos/that-time-disney-remade-beauty-and-the-beast',
'md5': 'fe79c4df8b3aa2fea98a93d027465c7e', 'md5': 'fe79c4df8b3aa2fea98a93d027465c7e',
'info_dict': { 'info_dict': {
'id': '5c271b40b13fd613090034fd', 'id': '5c271b40b13fd613090034fd',
@@ -36,7 +36,7 @@ class NebulaIE(InfoExtractor):
'skip': 'All Nebula content requires authentication', 'skip': 'All Nebula content requires authentication',
}, },
{ {
'url': 'https://watchnebula.com/videos/the-logistics-of-d-day-landing-craft-how-the-allies-got-ashore', 'url': 'https://nebula.app/videos/the-logistics-of-d-day-landing-craft-how-the-allies-got-ashore',
'md5': '6d4edd14ce65720fa63aba5c583fb328', 'md5': '6d4edd14ce65720fa63aba5c583fb328',
'info_dict': { 'info_dict': {
'id': '5e7e78171aaf320001fbd6be', 'id': '5e7e78171aaf320001fbd6be',
@@ -54,7 +54,7 @@ class NebulaIE(InfoExtractor):
'skip': 'All Nebula content requires authentication', 'skip': 'All Nebula content requires authentication',
}, },
{ {
'url': 'https://watchnebula.com/videos/money-episode-1-the-draw', 'url': 'https://nebula.app/videos/money-episode-1-the-draw',
'md5': '8c7d272910eea320f6f8e6d3084eecf5', 'md5': '8c7d272910eea320f6f8e6d3084eecf5',
'info_dict': { 'info_dict': {
'id': '5e779ebdd157bc0001d1c75a', 'id': '5e779ebdd157bc0001d1c75a',
@@ -71,6 +71,10 @@ class NebulaIE(InfoExtractor):
}, },
'skip': 'All Nebula content requires authentication', 'skip': 'All Nebula content requires authentication',
}, },
{
'url': 'https://watchnebula.com/videos/money-episode-1-the-draw',
'only_matching': True,
},
] ]
_NETRC_MACHINE = 'watchnebula' _NETRC_MACHINE = 'watchnebula'
+54 -52
View File
@@ -164,6 +164,11 @@ class NiconicoIE(InfoExtractor):
_VALID_URL = r'https?://(?:www\.|secure\.|sp\.)?nicovideo\.jp/watch/(?P<id>(?:[a-z]{2})?[0-9]+)' _VALID_URL = r'https?://(?:www\.|secure\.|sp\.)?nicovideo\.jp/watch/(?P<id>(?:[a-z]{2})?[0-9]+)'
_NETRC_MACHINE = 'niconico' _NETRC_MACHINE = 'niconico'
_API_HEADERS = {
'X-Frontend-ID': '6',
'X-Frontend-Version': '0'
}
def _real_initialize(self): def _real_initialize(self):
self._login() self._login()
@@ -190,53 +195,55 @@ class NiconicoIE(InfoExtractor):
if compat_parse_qs(parts.query).get('message', [None])[0] == 'cant_login': if compat_parse_qs(parts.query).get('message', [None])[0] == 'cant_login':
login_ok = False login_ok = False
if not login_ok: if not login_ok:
self._downloader.report_warning('unable to log in: bad username or password') self.report_warning('unable to log in: bad username or password')
return login_ok return login_ok
def _get_heartbeat_info(self, info_dict): def _get_heartbeat_info(self, info_dict):
video_id, video_src_id, audio_src_id = info_dict['url'].split(':')[1].split('/') video_id, video_src_id, audio_src_id = info_dict['url'].split(':')[1].split('/')
# Get video webpage for API data. api_data = (
webpage, handle = self._download_webpage_handle( info_dict.get('_api_data')
'http://www.nicovideo.jp/watch/' + video_id, video_id) or self._parse_json(
self._html_search_regex(
api_data = self._parse_json(self._html_search_regex( 'data-api-data="([^"]+)"',
'data-api-data="([^"]+)"', webpage, self._download_webpage('http://www.nicovideo.jp/watch/' + video_id, video_id),
'API data', default='{}'), video_id) 'API data', default='{}'),
video_id))
session_api_data = try_get(api_data, lambda x: x['media']['delivery']['movie']['session']) 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]) session_api_endpoint = try_get(session_api_data, lambda x: x['urls'][0])
# ping def ping():
self._download_json( status = try_get(
'https://nvapi.nicovideo.jp/v1/2ab0cbaa/watch', video_id, self._download_json(
query={'t': try_get(api_data, lambda x: x['video']['dmcInfo']['tracking_id'])}, 'https://nvapi.nicovideo.jp/v1/2ab0cbaa/watch', video_id,
headers={ query={'t': try_get(api_data, lambda x: x['media']['delivery']['trackingId'])},
'Origin': 'https://www.nicovideo.jp', note='Acquiring permission for downloading video',
'Referer': 'https://www.nicovideo.jp/watch/' + video_id, headers=self._API_HEADERS),
'X-Frontend-Id': '6', lambda x: x['meta']['status'])
'X-Frontend-Version': '0' if status != 200:
}) self.report_warning('Failed to acquire permission for playing video. The video may not download.')
yesno = lambda x: 'yes' if x else 'no' yesno = lambda x: 'yes' if x else 'no'
# m3u8 (encryption) # m3u8 (encryption)
if 'encryption' in (try_get(api_data, lambda x: x['media']['delivery']['movie']) or {}): if try_get(api_data, lambda x: x['media']['delivery']['encryption']) is not None:
protocol = 'm3u8' protocol = 'm3u8'
encryption = self._parse_json(session_api_data['token'], video_id)['hls_encryption']
session_api_http_parameters = { session_api_http_parameters = {
'parameters': { 'parameters': {
'hls_parameters': { 'hls_parameters': {
'encryption': { 'encryption': {
'hls_encryption_v1': { encryption: {
'encrypted_key': try_get(api_data, lambda x: x['video']['dmcInfo']['encryption']['hls_encryption_v1']['encrypted_key']), 'encrypted_key': try_get(api_data, lambda x: x['media']['delivery']['encryption']['encryptedKey']),
'key_uri': try_get(api_data, lambda x: x['video']['dmcInfo']['encryption']['hls_encryption_v1']['key_uri']) 'key_uri': try_get(api_data, lambda x: x['media']['delivery']['encryption']['keyUri'])
} }
}, },
'transfer_preset': '', 'transfer_preset': '',
'use_ssl': yesno(session_api_endpoint['is_ssl']), 'use_ssl': yesno(session_api_endpoint['isSsl']),
'use_well_known_port': yesno(session_api_endpoint['is_well_known_port']), 'use_well_known_port': yesno(session_api_endpoint['isWellKnownPort']),
'segment_duration': 6000 'segment_duration': 6000,
} }
} }
} }
@@ -310,7 +317,8 @@ class NiconicoIE(InfoExtractor):
'url': session_api_endpoint['url'] + '/' + session_response['data']['session']['id'] + '?_format=json&_method=PUT', 'url': session_api_endpoint['url'] + '/' + session_response['data']['session']['id'] + '?_format=json&_method=PUT',
'data': json.dumps(session_response['data']), 'data': json.dumps(session_response['data']),
# interval, convert milliseconds to seconds, then halve to make a buffer. # interval, convert milliseconds to seconds, then halve to make a buffer.
'interval': float_or_none(session_api_data.get('heartbeatLifetime'), scale=2000), 'interval': float_or_none(session_api_data.get('heartbeatLifetime'), scale=3000),
'ping': ping
} }
return info_dict, heartbeat_info_dict return info_dict, heartbeat_info_dict
@@ -400,7 +408,7 @@ class NiconicoIE(InfoExtractor):
# Get HTML5 videos info # Get HTML5 videos info
quality_info = try_get(api_data, lambda x: x['media']['delivery']['movie']) quality_info = try_get(api_data, lambda x: x['media']['delivery']['movie'])
if not quality_info: if not quality_info:
raise ExtractorError('The video can\'t downloaded.', expected=True) raise ExtractorError('The video can\'t be downloaded', expected=True)
for audio_quality in quality_info.get('audios') or {}: for audio_quality in quality_info.get('audios') or {}:
for video_quality in quality_info.get('videos') or {}: for video_quality in quality_info.get('videos') or {}:
@@ -412,9 +420,7 @@ class NiconicoIE(InfoExtractor):
# Get flv/swf info # Get flv/swf info
timestamp = None timestamp = None
video_real_url = try_get(api_data, lambda x: x['video']['smileInfo']['url']) video_real_url = try_get(api_data, lambda x: x['video']['smileInfo']['url'])
if not video_real_url: if video_real_url:
self.report_warning('Unable to obtain smile video information')
else:
is_economy = video_real_url.endswith('low') is_economy = video_real_url.endswith('low')
if is_economy: if is_economy:
@@ -486,14 +492,12 @@ class NiconicoIE(InfoExtractor):
'filesize': filesize 'filesize': filesize
}) })
if len(formats) == 0:
raise ExtractorError('Unable to find video info.')
self._sort_formats(formats) self._sort_formats(formats)
# Start extracting information # Start extracting information
title = ( title = (
get_video_info_web(['originalTitle', 'title']) get_video_info_xml('title') # prefer to get the untranslated original title
or get_video_info_web(['originalTitle', 'title'])
or self._og_search_title(webpage, default=None) or self._og_search_title(webpage, default=None)
or self._html_search_regex( or self._html_search_regex(
r'<span[^>]+class="videoHeaderTitle"[^>]*>([^<]+)</span>', r'<span[^>]+class="videoHeaderTitle"[^>]*>([^<]+)</span>',
@@ -507,7 +511,9 @@ class NiconicoIE(InfoExtractor):
thumbnail = ( thumbnail = (
self._html_search_regex(r'<meta property="og:image" content="([^"]+)">', webpage, 'thumbnail data', default=None) self._html_search_regex(r'<meta property="og:image" content="([^"]+)">', webpage, 'thumbnail data', default=None)
or get_video_info_web(['thumbnail_url', 'largeThumbnailURL', 'thumbnailURL']) or dict_get( # choose highest from 720p to 240p
get_video_info_web('thumbnail'),
['ogp', 'player', 'largeUrl', 'middleUrl', 'url'])
or self._html_search_meta('image', webpage, 'thumbnail', default=None) or self._html_search_meta('image', webpage, 'thumbnail', default=None)
or video_detail.get('thumbnail')) or video_detail.get('thumbnail'))
@@ -582,6 +588,7 @@ class NiconicoIE(InfoExtractor):
return { return {
'id': video_id, 'id': video_id,
'_api_data': api_data,
'title': title, 'title': title,
'formats': formats, 'formats': formats,
'thumbnail': thumbnail, 'thumbnail': thumbnail,
@@ -616,24 +623,19 @@ class NiconicoPlaylistIE(InfoExtractor):
'only_matching': True, 'only_matching': True,
}] }]
_API_HEADERS = {
'X-Frontend-ID': '6',
'X-Frontend-Version': '0'
}
def _real_extract(self, url): def _real_extract(self, url):
list_id = self._match_id(url) list_id = self._match_id(url)
webpage = self._download_webpage(url, list_id)
header = self._parse_json(self._html_search_regex(
r'data-common-header="([^"]+)"', webpage,
'webpage header'), list_id)
frontendId = header.get('initConfig').get('frontendId')
frontendVersion = header.get('initConfig').get('frontendVersion')
def get_page_data(pagenum, pagesize): def get_page_data(pagenum, pagesize):
return self._download_json( return self._download_json(
'http://nvapi.nicovideo.jp/v2/mylists/' + list_id, list_id, 'http://nvapi.nicovideo.jp/v2/mylists/' + list_id, list_id,
query={'page': 1 + pagenum, 'pageSize': pagesize}, query={'page': 1 + pagenum, 'pageSize': pagesize},
headers={ headers=self._API_HEADERS).get('data').get('mylist')
'X-Frontend-Id': frontendId,
'X-Frontend-Version': frontendVersion,
}).get('data').get('mylist')
data = get_page_data(0, 1) data = get_page_data(0, 1)
title = data.get('name') title = data.get('name')
@@ -669,20 +671,20 @@ class NiconicoUserIE(InfoExtractor):
'playlist_mincount': 101, 'playlist_mincount': 101,
} }
_API_URL = "https://nvapi.nicovideo.jp/v1/users/%s/videos?sortKey=registeredAt&sortOrder=desc&pageSize=%s&page=%s" _API_URL = "https://nvapi.nicovideo.jp/v1/users/%s/videos?sortKey=registeredAt&sortOrder=desc&pageSize=%s&page=%s"
_api_headers = {
'X-Frontend-ID': '6',
'X-Frontend-Version': '0',
'X-Niconico-Language': 'en-us'
}
_PAGE_SIZE = 100 _PAGE_SIZE = 100
_API_HEADERS = {
'X-Frontend-ID': '6',
'X-Frontend-Version': '0'
}
def _entries(self, list_id, ): def _entries(self, list_id, ):
total_count = 1 total_count = 1
count = page_num = 0 count = page_num = 0
while count < total_count: while count < total_count:
json_parsed = self._download_json( json_parsed = self._download_json(
self._API_URL % (list_id, self._PAGE_SIZE, page_num + 1), list_id, self._API_URL % (list_id, self._PAGE_SIZE, page_num + 1), list_id,
headers=self._api_headers, headers=self._API_HEADERS,
note='Downloading JSON metadata%s' % (' page %d' % page_num if page_num else '')) note='Downloading JSON metadata%s' % (' page %d' % page_num if page_num else ''))
if not page_num: if not page_num:
total_count = int_or_none(json_parsed['data'].get('totalCount')) total_count = int_or_none(json_parsed['data'].get('totalCount'))
+1 -1
View File
@@ -247,7 +247,7 @@ class NPOIE(NPOBaseIE):
if not formats: if not formats:
if not self._downloader.params.get('allow_unplayable_formats') and drm: if not self._downloader.params.get('allow_unplayable_formats') and drm:
raise ExtractorError('This video is DRM protected.', expected=True) self.raise_no_formats('This video is DRM protected.', expected=True)
return return
self._sort_formats(formats) self._sort_formats(formats)
+6 -2
View File
@@ -46,6 +46,7 @@ class NYTimesBaseIE(InfoExtractor):
urls = [] urls = []
formats = [] formats = []
subtitles = {}
for video in video_data.get('renditions', []): for video in video_data.get('renditions', []):
video_url = video.get('url') video_url = video.get('url')
format_id = video.get('type') format_id = video.get('type')
@@ -54,9 +55,11 @@ class NYTimesBaseIE(InfoExtractor):
urls.append(video_url) urls.append(video_url)
ext = mimetype2ext(video.get('mimetype')) or determine_ext(video_url) ext = mimetype2ext(video.get('mimetype')) or determine_ext(video_url)
if ext == 'm3u8': if ext == 'm3u8':
formats.extend(self._extract_m3u8_formats( m3u8_fmts, m3u8_subs = self._extract_m3u8_formats_and_subtitles(
video_url, video_id, 'mp4', 'm3u8_native', video_url, video_id, 'mp4', 'm3u8_native',
m3u8_id=format_id or 'hls', fatal=False)) m3u8_id=format_id or 'hls', fatal=False)
formats.extend(m3u8_fmts)
subtitles = self._merge_subtitles(subtitles, m3u8_subs)
elif ext == 'mpd': elif ext == 'mpd':
continue continue
# formats.extend(self._extract_mpd_formats( # formats.extend(self._extract_mpd_formats(
@@ -96,6 +99,7 @@ class NYTimesBaseIE(InfoExtractor):
'uploader': video_data.get('byline'), 'uploader': video_data.get('byline'),
'duration': float_or_none(video_data.get('duration'), 1000), 'duration': float_or_none(video_data.get('duration'), 1000),
'formats': formats, 'formats': formats,
'subtitles': subtitles,
'thumbnails': thumbnails, 'thumbnails': thumbnails,
} }
+1 -1
View File
@@ -260,7 +260,7 @@ class OdnoklassnikiIE(InfoExtractor):
if not formats: if not formats:
payment_info = metadata.get('paymentInfo') payment_info = metadata.get('paymentInfo')
if payment_info: if payment_info:
raise ExtractorError('This video is paid, subscribe to download it', expected=True) self.raise_no_formats('This video is paid, subscribe to download it', expected=True)
self._sort_formats(formats) self._sort_formats(formats)
+1 -2
View File
@@ -10,7 +10,6 @@ from ..compat import (
) )
from ..utils import ( from ..utils import (
determine_ext, determine_ext,
ExtractorError,
float_or_none, float_or_none,
int_or_none, int_or_none,
try_get, try_get,
@@ -85,7 +84,7 @@ class OoyalaBaseIE(InfoExtractor):
'fps': float_or_none(stream.get('framerate')), 'fps': float_or_none(stream.get('framerate')),
}) })
if not formats and not auth_data.get('authorized'): if not formats and not auth_data.get('authorized'):
raise ExtractorError('%s said: %s' % ( self.raise_no_formats('%s said: %s' % (
self.IE_NAME, auth_data['message']), expected=True) self.IE_NAME, auth_data['message']), expected=True)
self._sort_formats(formats) self._sort_formats(formats)
+1 -1
View File
@@ -79,7 +79,7 @@ class PhilharmonieDeParisIE(InfoExtractor):
formats.extend(self._extract_m3u8_formats( formats.extend(self._extract_m3u8_formats(
m3u8_url, video_id, 'mp4', entry_protocol='m3u8_native', m3u8_url, video_id, 'mp4', entry_protocol='m3u8_native',
m3u8_id='hls', fatal=False)) m3u8_id='hls', fatal=False))
if not formats: if not formats and not self._downloader.params.get('ignore_no_formats'):
return return
self._sort_formats(formats) self._sort_formats(formats)
return { return {
+1 -1
View File
@@ -393,7 +393,7 @@ query viewClip {
# To somewhat reduce the probability of these consequences # To somewhat reduce the probability of these consequences
# we will sleep random amount of time before each call to ViewClip. # we will sleep random amount of time before each call to ViewClip.
self._sleep( self._sleep(
random.randint(2, 5), display_id, random.randint(5, 10), display_id,
'%(video_id)s: Waiting for %(timeout)s seconds to avoid throttling') '%(video_id)s: Waiting for %(timeout)s seconds to avoid throttling')
if not viewclip: if not viewclip:
+35 -20
View File
@@ -78,45 +78,60 @@ class PlutoTVIE(InfoExtractor):
}, },
] ]
def _to_ad_free_formats(self, video_id, formats): def _to_ad_free_formats(self, video_id, formats, subtitles):
ad_free_formats = [] ad_free_formats, ad_free_subtitles, m3u8_urls = [], {}, set()
m3u8_urls = set() for fmt in formats:
for format in formats:
res = self._download_webpage( res = self._download_webpage(
format.get('url'), video_id, note='Downloading m3u8 playlist', fmt.get('url'), video_id, note='Downloading m3u8 playlist',
fatal=False) fatal=False)
if not res: if not res:
continue continue
first_segment_url = re.search( first_segment_url = re.search(
r'^(https?://.*/)0\-(end|[0-9]+)/[^/]+\.ts$', res, r'^(https?://.*/)0\-(end|[0-9]+)/[^/]+\.ts$', res,
re.MULTILINE) re.MULTILINE)
if not first_segment_url: if first_segment_url:
m3u8_urls.add(
compat_urlparse.urljoin(first_segment_url.group(1), '0-end/master.m3u8'))
continue
first_segment_url = re.search(
r'^(https?://.*/).+\-0+\.ts$', res,
re.MULTILINE)
if first_segment_url:
m3u8_urls.add(
compat_urlparse.urljoin(first_segment_url.group(1), 'master.m3u8'))
continue continue
m3u8_urls.add(
compat_urlparse.urljoin(first_segment_url.group(1), '0-end/master.m3u8'))
for m3u8_url in m3u8_urls: for m3u8_url in m3u8_urls:
ad_free_formats.extend( fmts, subs = self._extract_m3u8_formats_and_subtitles(
self._extract_m3u8_formats( m3u8_url, video_id, 'mp4', 'm3u8_native', m3u8_id='hls', fatal=False)
m3u8_url, video_id, 'mp4', 'm3u8_native', ad_free_formats.extend(fmts)
m3u8_id='hls', fatal=False)) ad_free_subtitles = self._merge_subtitles(ad_free_subtitles, subs)
self._sort_formats(ad_free_formats) if ad_free_formats:
return ad_free_formats formats, subtitles = ad_free_formats, ad_free_subtitles
else:
self._downloader.report_warning('Unable to find ad-free formats')
return formats, subtitles
def _get_video_info(self, video_json, slug, series_name=None): def _get_video_info(self, video_json, slug, series_name=None):
video_id = video_json.get('_id', slug) video_id = video_json.get('_id', slug)
formats = [] formats, subtitles = [], {}
for video_url in try_get(video_json, lambda x: x['stitched']['urls'], list) or []: for video_url in try_get(video_json, lambda x: x['stitched']['urls'], list) or []:
if video_url.get('type') != 'hls': if video_url.get('type') != 'hls':
continue continue
url = url_or_none(video_url.get('url')) url = url_or_none(video_url.get('url'))
formats.extend(
self._extract_m3u8_formats( fmts, subs = self._extract_m3u8_formats_and_subtitles(
url, video_id, 'mp4', 'm3u8_native', url, video_id, 'mp4', 'm3u8_native', m3u8_id='hls', fatal=False)
m3u8_id='hls', fatal=False)) formats.extend(fmts)
subtitles = self._merge_subtitles(subtitles, subs)
formats, subtitles = self._to_ad_free_formats(video_id, formats, subtitles)
self._sort_formats(formats)
info = { info = {
'id': video_id, 'id': video_id,
'formats': self._to_ad_free_formats(video_id, formats), 'formats': formats,
'subtitles': subtitles,
'title': video_json.get('name'), 'title': video_json.get('name'),
'description': video_json.get('description'), 'description': video_json.get('description'),
'duration': float_or_none(video_json.get('duration'), scale=1000), 'duration': float_or_none(video_json.get('duration'), scale=1000),
+109 -20
View File
@@ -5,15 +5,16 @@ import re
from .common import InfoExtractor from .common import InfoExtractor
from ..compat import ( from ..compat import (
compat_urlparse,
compat_str, compat_str,
compat_urlparse,
) )
from ..utils import ( from ..utils import (
ExtractorError,
determine_ext, determine_ext,
ExtractorError,
find_xpath_attr, find_xpath_attr,
fix_xml_ampersands, fix_xml_ampersands,
GeoRestrictedError, GeoRestrictedError,
HEADRequest,
int_or_none, int_or_none,
parse_duration, parse_duration,
remove_start, remove_start,
@@ -94,7 +95,9 @@ class RaiBaseIE(InfoExtractor):
}) })
if not formats and geoprotection is True: if not formats and geoprotection is True:
self.raise_geo_restricted(countries=self._GEO_COUNTRIES) self.raise_geo_restricted(countries=self._GEO_COUNTRIES, metadata_available=True)
formats.extend(self._create_http_urls(relinker_url, formats))
return dict((k, v) for k, v in { return dict((k, v) for k, v in {
'is_live': is_live, 'is_live': is_live,
@@ -102,6 +105,92 @@ class RaiBaseIE(InfoExtractor):
'formats': formats, 'formats': formats,
}.items() if v is not None) }.items() if v is not None)
def _create_http_urls(self, relinker_url, fmts):
_RELINKER_REG = r'https?://(?P<host>[^/]+?)/(?:i/)?(?P<extra>[^/]+?)/(?P<path>.+?)/(?P<id>\d+)(?:_(?P<quality>[\d\,]+))?(?:\.mp4|/playlist\.m3u8).+?'
_MP4_TMPL = '%s&overrideUserAgentRule=mp4-%s'
_QUALITY = {
# tbr: w, h
'250': [352, 198],
'400': [512, 288],
'700': [512, 288],
'800': [700, 394],
'1200': [736, 414],
'1800': [1024, 576],
'2400': [1280, 720],
'3200': [1440, 810],
'3600': [1440, 810],
'5000': [1920, 1080],
'10000': [1920, 1080],
}
def test_url(url):
resp = self._request_webpage(
HEADRequest(url), None, headers={'User-Agent': 'Rai'},
fatal=False, errnote=False, note=False)
if resp is False:
return False
if resp.code == 200:
return False if resp.url == url else resp.url
return None
def get_format_info(tbr):
import math
br = int_or_none(tbr)
if len(fmts) == 1 and not br:
br = fmts[0].get('tbr')
if br > 300:
tbr = compat_str(math.floor(br / 100) * 100)
else:
tbr = '250'
# try extracting info from available m3u8 formats
format_copy = None
for f in fmts:
if f.get('tbr'):
br_limit = math.floor(br / 100)
if br_limit - 1 <= math.floor(f['tbr'] / 100) <= br_limit + 1:
format_copy = f.copy()
return {
'width': format_copy.get('width'),
'height': format_copy.get('height'),
'tbr': format_copy.get('tbr'),
'vcodec': format_copy.get('vcodec'),
'acodec': format_copy.get('acodec'),
'fps': format_copy.get('fps'),
'format_id': 'https-%s' % tbr,
} if format_copy else {
'width': _QUALITY[tbr][0],
'height': _QUALITY[tbr][1],
'format_id': 'https-%s' % tbr,
'tbr': int(tbr),
}
loc = test_url(_MP4_TMPL % (relinker_url, '*'))
if not isinstance(loc, compat_str):
return []
mobj = re.match(
_RELINKER_REG,
test_url(relinker_url) or '')
if not mobj:
return []
available_qualities = mobj.group('quality').split(',') if mobj.group('quality') else ['*']
available_qualities = [i for i in available_qualities if i]
formats = []
for q in available_qualities:
fmt = {
'url': _MP4_TMPL % (relinker_url, q),
'protocol': 'https',
'ext': 'mp4',
}
fmt.update(get_format_info(q))
formats.append(fmt)
return formats
@staticmethod @staticmethod
def _extract_subtitles(url, video_data): def _extract_subtitles(url, video_data):
STL_EXT = 'stl' STL_EXT = 'stl'
@@ -151,6 +240,22 @@ class RaiPlayIE(RaiBaseIE):
'params': { 'params': {
'skip_download': True, 'skip_download': True,
}, },
}, {
# 1080p direct mp4 url
'url': 'https://www.raiplay.it/video/2021/03/Leonardo-S1E1-b5703b02-82ee-475a-85b6-c9e4a8adf642.html',
'md5': '2e501e8651d72f05ffe8f5d286ad560b',
'info_dict': {
'id': 'b5703b02-82ee-475a-85b6-c9e4a8adf642',
'ext': 'mp4',
'title': 'Leonardo - S1E1',
'alt_title': 'St 1 Ep 1 - Episodio 1',
'description': 'md5:f5360cd267d2de146e4e3879a5a47d31',
'thumbnail': r're:^https?://.*\.jpg$',
'uploader': 'Rai 1',
'duration': 3229,
'series': 'Leonardo',
'season': 'Season 1',
},
}, { }, {
'url': 'http://www.raiplay.it/video/2016/11/gazebotraindesi-efebe701-969c-4593-92f3-285f0d1ce750.html?', 'url': 'http://www.raiplay.it/video/2016/11/gazebotraindesi-efebe701-969c-4593-92f3-285f0d1ce750.html?',
'only_matching': True, 'only_matching': True,
@@ -318,7 +423,7 @@ class RaiIE(RaiBaseIE):
}, { }, {
# with ContentItem in og:url # with ContentItem in og:url
'url': 'http://www.rai.it/dl/RaiTV/programmi/media/ContentItem-efb17665-691c-45d5-a60c-5301333cbb0c.html', 'url': 'http://www.rai.it/dl/RaiTV/programmi/media/ContentItem-efb17665-691c-45d5-a60c-5301333cbb0c.html',
'md5': '6865dd00cf0bbf5772fdd89d59bd768a', 'md5': '06345bd97c932f19ffb129973d07a020',
'info_dict': { 'info_dict': {
'id': 'efb17665-691c-45d5-a60c-5301333cbb0c', 'id': 'efb17665-691c-45d5-a60c-5301333cbb0c',
'ext': 'mp4', 'ext': 'mp4',
@@ -350,22 +455,6 @@ class RaiIE(RaiBaseIE):
'params': { 'params': {
'skip_download': True, 'skip_download': True,
}, },
}, {
# ContentItem in iframe (see #12652) and subtitle at 'subtitlesUrl' key
'url': 'http://www.presadiretta.rai.it/dl/portali/site/puntata/ContentItem-3ed19d13-26c2-46ff-a551-b10828262f1b.html',
'info_dict': {
'id': '1ad6dc64-444a-42a4-9bea-e5419ad2f5fd',
'ext': 'mp4',
'title': 'Partiti acchiappavoti - Presa diretta del 13/09/2015',
'description': 'md5:d291b03407ec505f95f27970c0b025f4',
'upload_date': '20150913',
'subtitles': {
'it': 'count:2',
},
},
'params': {
'skip_download': True,
},
}, { }, {
# Direct MMS URL # Direct MMS URL
'url': 'http://www.rai.it/dl/RaiTV/programmi/media/ContentItem-b63a4089-ac28-48cf-bca5-9f5b5bc46df5.html', 'url': 'http://www.rai.it/dl/RaiTV/programmi/media/ContentItem-b63a4089-ac28-48cf-bca5-9f5b5bc46df5.html',
+16 -1
View File
@@ -13,9 +13,24 @@ from ..utils import smuggle_url
class RMCDecouverteIE(InfoExtractor): class RMCDecouverteIE(InfoExtractor):
_VALID_URL = r'https?://rmcdecouverte\.bfmtv\.com/(?:(?:[^/]+/)*program_(?P<id>\d+)|(?P<live_id>mediaplayer-direct))' _VALID_URL = r'https?://rmcdecouverte\.bfmtv\.com/(?:[^/]+/(?P<id>[^?#/]+)|(?P<live_id>mediaplayer-direct))'
_TESTS = [{ _TESTS = [{
'url': 'https://rmcdecouverte.bfmtv.com/vestiges-de-guerre_22240/les-bunkers-secrets-domaha-beach_25303/',
'info_dict': {
'id': '6250879771001',
'ext': 'mp4',
'title': 'LES BUNKERS SECRETS D´OMAHA BEACH',
'uploader_id': '1969646226001',
'description': 'md5:aed573ca24abde62a148e0eba909657d',
'timestamp': 1619622984,
'upload_date': '20210428',
},
'params': {
'format': 'bestvideo',
'skip_download': True,
},
}, {
'url': 'https://rmcdecouverte.bfmtv.com/wheeler-dealers-occasions-a-saisir/program_2566/', 'url': 'https://rmcdecouverte.bfmtv.com/wheeler-dealers-occasions-a-saisir/program_2566/',
'info_dict': { 'info_dict': {
'id': '5983675500001', 'id': '5983675500001',
+2 -29
View File
@@ -103,7 +103,7 @@ class RoosterTeethIE(InfoExtractor):
api_episode_url + '/videos', display_id, api_episode_url + '/videos', display_id,
'Downloading video JSON metadata')['data'][0] 'Downloading video JSON metadata')['data'][0]
m3u8_url = video_data['attributes']['url'] m3u8_url = video_data['attributes']['url']
subtitle_m3u8_url = video_data['links']['download'] # XXX: additional URL at video_data['links']['download']
except ExtractorError as e: except ExtractorError as e:
if isinstance(e.cause, compat_HTTPError) and e.cause.code == 403: if isinstance(e.cause, compat_HTTPError) and e.cause.code == 403:
if self._parse_json(e.cause.read().decode(), display_id).get('access') is False: if self._parse_json(e.cause.read().decode(), display_id).get('access') is False:
@@ -111,7 +111,7 @@ class RoosterTeethIE(InfoExtractor):
'%s is only available for FIRST members' % display_id) '%s is only available for FIRST members' % display_id)
raise raise
formats = self._extract_m3u8_formats( formats, subtitles = self._extract_m3u8_formats_and_subtitles(
m3u8_url, display_id, 'mp4', 'm3u8_native', m3u8_id='hls') m3u8_url, display_id, 'mp4', 'm3u8_native', m3u8_id='hls')
self._sort_formats(formats) self._sort_formats(formats)
@@ -134,33 +134,6 @@ class RoosterTeethIE(InfoExtractor):
'url': img_url, 'url': img_url,
}) })
subtitles = {}
res = self._download_webpage_handle(
subtitle_m3u8_url, display_id,
'Downloading m3u8 information',
'Failed to download m3u8 information',
fatal=True, data=None, headers={}, query={})
if res is not False:
subtitle_m3u8_doc, _ = res
for line in subtitle_m3u8_doc.split('\n'):
if 'EXT-X-MEDIA:TYPE=SUBTITLES' in line:
parts = line.split(',')
for part in parts:
if 'LANGUAGE' in part:
lang = part[part.index('=') + 2:-1]
elif 'URI' in part:
uri = part[part.index('=') + 2:-1]
res = self._download_webpage_handle(
uri, display_id,
'Downloading m3u8 information',
'Failed to download m3u8 information',
fatal=True, data=None, headers={}, query={})
doc, _ = res
for l in doc.split('\n'):
if not l.startswith('#'):
subtitles[lang] = [{'url': uri[:-uri[::-1].index('/')] + l}]
break
return { return {
'id': video_id, 'id': video_id,
'display_id': display_id, 'display_id': display_id,
+1 -1
View File
@@ -202,7 +202,7 @@ class RuutuIE(InfoExtractor):
if not formats: if not formats:
if (not self._downloader.params.get('allow_unplayable_formats') if (not self._downloader.params.get('allow_unplayable_formats')
and xpath_text(video_xml, './Clip/DRM', default=None)): and xpath_text(video_xml, './Clip/DRM', default=None)):
raise ExtractorError('This video is DRM protected.', expected=True) self.raise_no_formats('This video is DRM protected.', expected=True)
ns_st_cds = pv('ns_st_cds') ns_st_cds = pv('ns_st_cds')
if ns_st_cds != 'free': if ns_st_cds != 'free':
raise ExtractorError('This video is %s.' % ns_st_cds, expected=True) raise ExtractorError('This video is %s.' % ns_st_cds, expected=True)
+4 -7
View File
@@ -17,7 +17,7 @@ class SonyLIVIE(InfoExtractor):
_TESTS = [{ _TESTS = [{
'url': 'https://www.sonyliv.com/shows/bachelors-delight-1700000113/achaari-cheese-toast-1000022678?watch=true', 'url': 'https://www.sonyliv.com/shows/bachelors-delight-1700000113/achaari-cheese-toast-1000022678?watch=true',
'info_dict': { 'info_dict': {
'title': 'Bachelors Delight - Achaari Cheese Toast', 'title': 'Achaari Cheese Toast',
'id': '1000022678', 'id': '1000022678',
'ext': 'mp4', 'ext': 'mp4',
'upload_date': '20200411', 'upload_date': '20200411',
@@ -25,7 +25,7 @@ class SonyLIVIE(InfoExtractor):
'timestamp': 1586632091, 'timestamp': 1586632091,
'duration': 185, 'duration': 185,
'season_number': 1, 'season_number': 1,
'episode': 'Achaari Cheese Toast', 'series': 'Bachelors Delight',
'episode_number': 1, 'episode_number': 1,
'release_year': 2016, 'release_year': 2016,
}, },
@@ -92,10 +92,7 @@ class SonyLIVIE(InfoExtractor):
metadata = self._call_api( metadata = self._call_api(
'1.6', 'IN/DETAIL/' + video_id, video_id)['containers'][0]['metadata'] '1.6', 'IN/DETAIL/' + video_id, video_id)['containers'][0]['metadata']
title = metadata['title'] title = metadata['episodeTitle']
episode = metadata.get('episodeTitle')
if episode and title != episode:
title += ' - ' + episode
return { return {
'id': video_id, 'id': video_id,
@@ -106,7 +103,7 @@ class SonyLIVIE(InfoExtractor):
'timestamp': int_or_none(metadata.get('creationDate'), 1000), 'timestamp': int_or_none(metadata.get('creationDate'), 1000),
'duration': int_or_none(metadata.get('duration')), 'duration': int_or_none(metadata.get('duration')),
'season_number': int_or_none(metadata.get('season')), 'season_number': int_or_none(metadata.get('season')),
'episode': episode, 'series': metadata.get('title'),
'episode_number': int_or_none(metadata.get('episodeNumber')), 'episode_number': int_or_none(metadata.get('episodeNumber')),
'release_year': int_or_none(metadata.get('year')), 'release_year': int_or_none(metadata.get('year')),
} }
+2 -2
View File
@@ -312,7 +312,7 @@ class SoundcloudIE(InfoExtractor):
self._update_client_id() self._update_client_id()
continue continue
elif non_fatal: elif non_fatal:
self._downloader.report_warning(error_to_compat_str(e)) self.report_warning(error_to_compat_str(e))
return False return False
raise raise
@@ -498,7 +498,7 @@ class SoundcloudIE(InfoExtractor):
f['vcodec'] = 'none' f['vcodec'] = 'none'
if not formats and info.get('policy') == 'BLOCK': if not formats and info.get('policy') == 'BLOCK':
self.raise_geo_restricted() self.raise_geo_restricted(metadata_available=True)
self._sort_formats(formats) self._sort_formats(formats)
user = info.get('user') or {} user = info.get('user') or {}
+1 -1
View File
@@ -77,7 +77,7 @@ class SportDeutschlandIE(InfoExtractor):
continue continue
formats = self._extract_m3u8_formats( formats = self._extract_m3u8_formats(
video_url.replace('.smil', '.m3u8'), video_id, 'mp4', fatal=False) video_url.replace('.smil', '.m3u8'), video_id, 'mp4', fatal=False)
if not formats: if not formats and not self._downloader.params.get('ignore_no_formats'):
continue continue
yield { yield {
'id': video_id, 'id': video_id,
+9 -5
View File
@@ -87,6 +87,7 @@ class SRGSSRIE(InfoExtractor):
title = media_data['title'] title = media_data['title']
formats = [] formats = []
subtitles = {}
q = qualities(['SD', 'HD']) q = qualities(['SD', 'HD'])
for source in (media_data.get('resourceList') or []): for source in (media_data.get('resourceList') or []):
format_url = source.get('url') format_url = source.get('url')
@@ -104,12 +105,16 @@ class SRGSSRIE(InfoExtractor):
if source.get('tokenType') == 'AKAMAI': if source.get('tokenType') == 'AKAMAI':
format_url = self._get_tokenized_src( format_url = self._get_tokenized_src(
format_url, media_id, format_id) format_url, media_id, format_id)
formats.extend(self._extract_akamai_formats( fmts, subs = self._extract_akamai_formats_and_subtitles(
format_url, media_id)) format_url, media_id)
formats.extend(fmts)
subtitles = self._merge_subtitles(subtitles, subs)
elif protocol == 'HLS': elif protocol == 'HLS':
formats.extend(self._extract_m3u8_formats( m3u8_fmts, m3u8_subs = self._extract_m3u8_formats_and_subtitles(
format_url, media_id, 'mp4', 'm3u8_native', format_url, media_id, 'mp4', 'm3u8_native',
m3u8_id=format_id, fatal=False)) m3u8_id=format_id, fatal=False)
formats.extend(m3u8_fmts)
subtitles = self._merge_subtitles(subtitles, m3u8_subs)
elif protocol in ('HTTP', 'HTTPS'): elif protocol in ('HTTP', 'HTTPS'):
formats.append({ formats.append({
'format_id': format_id, 'format_id': format_id,
@@ -133,7 +138,6 @@ class SRGSSRIE(InfoExtractor):
}) })
self._sort_formats(formats) self._sort_formats(formats)
subtitles = {}
if media_type == 'video': if media_type == 'video':
for sub in (media_data.get('subtitleList') or []): for sub in (media_data.get('subtitleList') or []):
sub_url = sub.get('url') sub_url = sub.get('url')
+1 -1
View File
@@ -139,7 +139,7 @@ class SteamIE(InfoExtractor):
'format_id': ext + quality, 'format_id': ext + quality,
'url': video_url, 'url': video_url,
}) })
if not formats: if not formats and not self._downloader.params.get('ignore_no_formats'):
continue continue
entry['formats'] = formats entry['formats'] = formats
entries.append(entry) entries.append(entry)
+6 -3
View File
@@ -49,7 +49,7 @@ class SVTBaseIE(InfoExtractor):
if not formats and rights.get('geoBlockedSweden'): if not formats and rights.get('geoBlockedSweden'):
self.raise_geo_restricted( self.raise_geo_restricted(
'This video is only available in Sweden', 'This video is only available in Sweden',
countries=self._GEO_COUNTRIES) countries=self._GEO_COUNTRIES, metadata_available=True)
self._sort_formats(formats) self._sort_formats(formats)
subtitles = {} subtitles = {}
@@ -146,7 +146,7 @@ class SVTPlayIE(SVTPlayBaseIE):
) )
(?P<svt_id>[^/?#&]+)| (?P<svt_id>[^/?#&]+)|
https?://(?:www\.)?(?:svtplay|oppetarkiv)\.se/(?:video|klipp|kanaler)/(?P<id>[^/?#&]+) https?://(?:www\.)?(?:svtplay|oppetarkiv)\.se/(?:video|klipp|kanaler)/(?P<id>[^/?#&]+)
(?:.*?modalId=(?P<modal_id>[\da-zA-Z-]+))? (?:.*?(?:modalId|id)=(?P<modal_id>[\da-zA-Z-]+))?
) )
''' '''
_TESTS = [{ _TESTS = [{
@@ -177,6 +177,9 @@ class SVTPlayIE(SVTPlayBaseIE):
}, { }, {
'url': 'https://www.svtplay.se/video/30479064/husdrommar/husdrommar-sasong-8-designdrommar-i-stenungsund?modalId=8zVbDPA', 'url': 'https://www.svtplay.se/video/30479064/husdrommar/husdrommar-sasong-8-designdrommar-i-stenungsund?modalId=8zVbDPA',
'only_matching': True, 'only_matching': True,
}, {
'url': 'https://www.svtplay.se/video/30684086/rapport/rapport-24-apr-18-00-7?id=e72gVpa',
'only_matching': True,
}, { }, {
# geo restricted to Sweden # geo restricted to Sweden
'url': 'http://www.oppetarkiv.se/video/5219710/trollflojten', 'url': 'http://www.oppetarkiv.se/video/5219710/trollflojten',
@@ -259,7 +262,7 @@ class SVTPlayIE(SVTPlayBaseIE):
if not svt_id: if not svt_id:
svt_id = self._search_regex( svt_id = self._search_regex(
(r'<video[^>]+data-video-id=["\']([\da-zA-Z-]+)', (r'<video[^>]+data-video-id=["\']([\da-zA-Z-]+)',
r'<[^>]+\bdata-rt=["\']top-area-play-button["\'][^>]+\bhref=["\'][^"\']*video/%s/[^"\']*\bmodalId=([\da-zA-Z-]+)' % re.escape(video_id), r'<[^>]+\bdata-rt=["\']top-area-play-button["\'][^>]+\bhref=["\'][^"\']*video/%s/[^"\']*\b(?:modalId|id)=([\da-zA-Z-]+)' % re.escape(video_id),
r'["\']videoSvtId["\']\s*:\s*["\']([\da-zA-Z-]+)', r'["\']videoSvtId["\']\s*:\s*["\']([\da-zA-Z-]+)',
r'["\']videoSvtId\\?["\']\s*:\s*\\?["\']([\da-zA-Z-]+)', r'["\']videoSvtId\\?["\']\s*:\s*\\?["\']([\da-zA-Z-]+)',
r'"content"\s*:\s*{.*?"id"\s*:\s*"([\da-zA-Z-]+)"', r'"content"\s*:\s*{.*?"id"\s*:\s*"([\da-zA-Z-]+)"',

Some files were not shown because too many files have changed in this diff Show More