Compare commits

..
102 Commits
Author SHA1 Message Date
pukkandan a8bf9b4dc1 Release 2021.07.07 2021-07-07 05:35:20 +05:30
pukkandan 51f8a31d65 Update to ytdl-commit-a803582
[peertube] only call description endpoint if necessary
https://github.com/ytdl-org/youtube-dl/commit/a8035827177d6b59aca03bd717acb6a9bdd75ada
2021-07-07 05:17:11 +05:30
Tom-Oliver HeidelandGitHub be05d5cff1 [soundcloud] Allow login using oauth token (#469)
Authored by: blackjack4494
2021-07-07 04:21:13 +05:30
zenerdi0deandGitHub 30d569d2ac [fancode] Fix extraction, support live and allow login with refresh token (#471)
Authored-by: zenerdi0de
2021-07-07 04:02:56 +05:30
OhMyBahGoshandGitHub 08625e4125 [AdobePass] Add Spectrum MSO (#470)
From: https://github.com/ytdl-org/youtube-dl/pull/26792

Co-authored by: kevinoconnor7, ohmybahgosh
2021-07-07 03:26:51 +05:30
pukkandanandGitHub 3acf6d3856 [Funimation] Rewrite extractor (See desc) (#444)
* Support direct `/player/` URL
* Treat the different versions of an episode as different formats of a single video. So `experience_id` can no longer be used as the video `id` and the `episode_id` is used instead. This means that all existing archives will break
* Extractor options `language` and `version` to pre-select them
* Compat option `seperate-video-versions` to fall back to old behavior (including using the old video IDs)

Closes #428
2021-07-07 02:51:29 +05:30
pukkandan 46890374f7 [extractor] Minor improvements (See desc)
1. Allow removal of login hint - extractors can set their own login hint as part of `msg`
2. Cleanup `_merge_subtitles` signature
2021-07-07 02:27:53 +05:30
pukkandan 60755938b3 [extractor] Prevent unnecessary download of hls manifests
and refactor `hls_split_discontinuity` code
2021-07-07 02:24:58 +05:30
pukkandan 723d44b92b [fragment] Handle errors in threads correctly 2021-07-07 01:55:54 +05:30
pukkandan bc97cdae67 [cleanup] Fix linter and some typos
Related: https://github.com/ytdl-org/youtube-dl/pull/29398
2021-07-04 03:04:25 +05:30
nyuszika7handGitHub e010672ab5 [videa] Fix extraction (#463)
Authored by: nyuszika7h
2021-07-03 21:38:08 +05:30
pukkandan 169dbde946 Fixes for --list options (See desc)
1. Fix `--list-formats-old`
2. Allow listing with `--quiet`
3. Allow various listings to work together
4. Allow `--print` to work with listing
2021-07-03 01:16:19 +05:30
MinePlayersPEandGitHub 17f0eb66b8 [RCTIPlus] Add extractor (#443)
Authored by: MinePlayersPE
2021-07-02 19:54:41 +05:30
pukkandan 981052c9c6 Some minor fixes and refactoring (see desc)
* [utils] Fix issues with reversal
* check_formats should catch `DownloadError`, not `ExtractorError`
* Simplify format selectors with `LazyList` and `yield from`
2021-07-02 08:17:37 +05:30
pukkandan b1e60d1806 [facebook] Extract description and fix title
Partially fixes: #453
2021-07-02 08:17:37 +05:30
pukkandan 6b6c16ca6c [downloader/ffmpeg] Fix --ppa when using simultaneous download 2021-07-02 08:17:30 +05:30
krichbananaandGitHub f6745c4980 [Youtube] Choose correct Live chat API for upcoming streams (#460)
Authored by: krichbanana
2021-07-02 05:59:29 +05:30
coletdjnzandGitHub 109dd3b237 [youtube] Use new API for additional video extraction requests (#328)
Co-authored-by: colethedj, pukkandan
Closes https://github.com/yt-dlp/yt-dlp/issues/427
Workarounds for https://github.com/ytdl-org/youtube-dl/issues/29326, https://github.com/yt-dlp/yt-dlp/issues/319, https://github.com/ytdl-org/youtube-dl/issues/29086
2021-06-29 22:07:49 +00:00
siikamiikaandGitHub c2603313b1 [youtube_live_chat] use clickTrackingParams (#449)
Authored by: siikamiika
2021-06-27 04:52:32 +05:30
LEandGitHub 1e79316e20 [TBS] Support livestreams (#448)
Authored by: llacb47
2021-06-26 17:14:43 +05:30
coletdjnzandGitHub 45261e063b [youtube:comments] Fix error handling and add itct to params (#446)
Should close #439 (untested)

Authored by: colethedj
2021-06-25 23:31:10 +05:30
pukkandan 49c258e18d [youtube] Fix subtitle names for age-gated videos
Related: https://github.com/iv-org/invidious/pull/2205#issuecomment-868680486
2021-06-25 23:10:31 +05:30
pukkandan d3f62c1967 Fix --throttled-rate when using --load-info-json 2021-06-25 22:57:17 +05:30
pukkandan 5d3a0e794b Add --extractor-args to pass extractor-specific arguments 2021-06-25 20:10:28 +05:30
MeviousandGitHub 125728b038 [funimation] Add FunimationShowIE (#442)
Closes #436

Authored by: Mevious
2021-06-25 05:45:23 +05:30
pukkandan 15a4fd53d3 [thumbnailsconvertor] Treat jpeg as jpg 2021-06-25 05:36:35 +05:30
Adrikandpukkandan 4513a41a72 Process videos when using --ignore-no-formats-error (#441)
Authored by: krichbanana
2021-06-24 22:23:34 +05:30
pukkandan 6033d9808d Fix --flat-playlist when entry has no ie_key 2021-06-24 22:23:34 +05:30
pukkandan bd4d1ea398 [cleanup] Minor refactoring of fragment 2021-06-24 22:23:33 +05:30
pukkandan 8e897ed283 [fragment] Return status of download correctly 2021-06-24 22:04:23 +05:30
LEGitHubllacb47, pukkandan
412cce82b0 [yahoo] Fix extraction (#435)
Fixes: https://github.com/ytdl-org/youtube-dl/issues/28290

Co-authored-by: llacb47, pukkandan
2021-06-24 21:27:48 +05:30
siikamiikaandGitHub d534c4520b [youtube_live_chat] Fix download with cookies (#437)
Closes #417 

Authored by: siikamiika
2021-06-24 21:26:32 +05:30
pukkandan 2b18a8c590 [plutotv] Improve _VALID_URL
Closes #431
2021-06-23 07:49:09 +05:30
pukkandan dac8b87b0c [version] update :ci skip all 2021-06-23 07:37:07 +05:30
pukkandan 6aecd87106 Release 2021.06.23 2021-06-23 07:34:55 +05:30
pukkandan ed807c1837 Update to ytdl-commit-379f52a
[liveleak] Remove extractor
https://github.com/ytdl-org/youtube-dl/commit/379f52a4954013767219d25099cce9e0f9401961
2021-06-23 07:34:55 +05:30
MeviousandGitHub 29f63c9672 [funimation] Extract subtitles (#434)
Closes #420, https://github.com/ytdl-org/youtube-dl/issues/25645
Related: https://github.com/ytdl-org/youtube-dl/pull/24906

Authored by: Mevious
2021-06-23 07:27:53 +05:30
pukkandan 9fc0de5796 [hotstar] Use server time for authentication instead of local time
Closes #396
2021-06-23 06:04:42 +05:30
siikamiikaandGitHub c60ee3a218 [youtube_live_chat] Support ongoing live chat (#422)
Authored by: siikamiika
2021-06-23 05:42:39 +05:30
pukkandan 8a77e5e6bc [cleanup] Revert unnecessary changes in 51d9739f80 2021-06-23 05:34:40 +05:30
pukkandan 51d9739f80 Add option --throttled-rate below which video data is re-extracted
Currently only for HTTP downloads

Closes #430, workaround for https://github.com/ytdl-org/youtube-dl/issues/29326
2021-06-23 05:29:58 +05:30
pukkandanandGitHub 4c7853de14 [fragment] Merge during download for -N, and refactor hls/dash (#364) 2021-06-22 00:29:50 +05:30
pukkandan e6779b9400 [twitcasting] Websocket support (#399)
Closes #392
Authored by: nao20010128nao
2021-06-21 22:56:45 +05:30
pukkandan e36d50c5dd [websockets] Add WebSocketFragmentFD (#399)
Necessary for #392

Co-authored by: nao20010128nao, pukkandan
2021-06-21 22:56:36 +05:30
pukkandan ff0f78e1fe [aria2c] Lower --min-split-size for HTTP downloads
This makes downloading smaller files much faster
2021-06-20 19:28:54 +05:30
pukkandan 7e067091e8 [options] Rename --add-metadata to --embed-metadata
and clarify that it embeds chapter markers
2021-06-20 04:59:35 +05:30
pukkandan f89b3e2d7a Skip fixup of existing files and add --fixup force to force it 2021-06-20 04:59:34 +05:30
pukkandan fd7cfb6444 [cleanup] Refactor fixup 2021-06-20 04:26:11 +05:30
pukkandan 4e6767b5f2 [youtube] Temporary fix for age-gate
Related:
https://stackoverflow.com/a/67629882
https://github.com/yt-dlp/yt-dlp/issues/319
https://github.com/ytdl-org/youtube-dl/issues/29333
https://github.com/ytdl-org/youtube-dl/issues/29086
2021-06-18 20:32:52 +05:30
pukkandan 9fea350f0d Fix id sanitization in filenames
Closes #415
2021-06-17 02:32:24 +05:30
pukkandan e858a9d6d3 [EmbedThumbnail] Add compat-option embed-thumbnail-atomicparsley
to force use of atomicparsley for embedding thumbnails in mp4

Related: #411
2021-06-16 22:33:32 +05:30
pukkandan 7e87e27c52 [postprocessor] Fix _restrict_to when a codec is not set 2021-06-14 14:09:22 +05:30
pukkandan d0fb4bd16f [pornhub] Extract cast
Closes #406, https://github.com/ytdl-org/youtube-dl/pull/27384
2021-06-13 21:38:08 +05:30
felixandpukkandan 3fd4c2a543 [mediasite] Extract slides (#343)
Fixes:
https://github.com/ytdl-org/youtube-dl/issues/4974#issue-58006762
https://github.com/ytdl-org/youtube-dl/issues/4540#issuecomment-69574231
https://github.com/ytdl-org/youtube-dl/pull/11185#issuecomment-335554239

Authored by: fstirlitz
2021-06-13 20:36:40 +05:30
felixandpukkandan cdb19aa4c2 [downloader/mhtml] Add new downloader (#343)
This downloader is intended to be used for streams that consist of a
timed sequence of stand-alone images, such as slideshows or thumbnail
streams

This can be used for implementing:

https://github.com/ytdl-org/youtube-dl/issues/4974#issue-58006762
https://github.com/ytdl-org/youtube-dl/issues/4540#issuecomment-69574231
https://github.com/ytdl-org/youtube-dl/pull/11185#issuecomment-335554239

https://github.com/ytdl-org/youtube-dl/issues/9868
https://github.com/ytdl-org/youtube-dl/pull/14951


Authored by: fstirlitz
2021-06-13 20:36:40 +05:30
pukkandan 4d85fbbdbb Fix bug in 8326b00aab 2021-06-13 14:36:13 +05:30
pukkandan 551f93885e Ignore images formats from merge 2021-06-13 04:16:42 +05:30
pukkandan 8326b00aab Allow images formats
Necessary for #343.

* They are identified by `vcodec=acodec='none'`
* These formats show as the worst in `-F`
* Any postprocessor that expects audio/video will be skipped
* `b*` and all related selectors will skip such formats
* This commit also does not add any selector for downloading such formats. They have to be explicitly requested by the `format_id`. Implementation of a selector is left for when #389 is resolved
2021-06-13 03:45:53 +05:30
pukkandan b0249bcaf0 Expand --check-formats to thumbnails
Closes #402
2021-06-13 03:45:53 +05:30
pukkandan 21cd8fae49 Use NamedTemporaryFile for --check-formats 2021-06-13 03:45:53 +05:30
pukkandan 45db527fa6 [youtube] Login is not needed for :ytrec 2021-06-13 03:45:53 +05:30
pukkandan 28419ca2c8 [utils] Improve LazyList
* Add `repr` and `str` that mimics `list`
* Add `reversed`. Unlike `[::-1]`, reversed does not exhaust the iterable and modifies the `LazyList` in-place
* Add tests
2021-06-13 03:45:53 +05:30
pukkandan 8ba8714880 [EmbedThumbnail] Fix for already downloaded thumbnail 2021-06-11 19:13:24 +05:30
pukkandan 187986a857 Better error handling of syntax errors in -f 2021-06-11 19:13:22 +05:30
coletdjnzandGitHub 4ba001080f [youtube] Non-fatal alert reporting for unavailable videos page (#401)
Co-Authored by: colethedj, pukkandan
2021-06-10 21:12:56 +00:00
coletdjnzandGitHub 1974e99f4b [youtube] Improve SAPISID cookie handling (closes #393) (#395)
Author: colethedj
2021-06-10 21:02:57 +00:00
pukkandan 0181adefc6 [build] Build Windows x86 version with py3.7
and remove redundant tests
Closes #390

:ci skip

Co-authored by: pukkandan, shirt-dev
2021-06-10 01:41:04 +05:30
pukkandan fd3c633d26 [version] update
:ci skip all
2021-06-10 01:36:46 +05:30
pukkandan 0d47c278d1 Release 2021.06.09 2021-06-10 00:49:48 +05:30
pukkandan 385a27fad1 Improve offset parsing in outtmpl 2021-06-09 20:01:57 +05:30
pukkandan 5c6542ce69 [test] More rigorous tests for prepare_filename
All tests of `prepare_outtmpl` is now also run on `prepare_filename`
2021-06-09 20:01:56 +05:30
pukkandan 639f1cea92 Fix %d and empty default in outtmpl
Closes #388
2021-06-09 15:37:15 +05:30
pukkandan b5c5d84f60 Revert "[build] Build Windows x86 version with py3.8"
This reverts commit aa75e51f99.

See #390

This is being reverted instead of modified due to #388
2021-06-09 15:37:15 +05:30
pukkandan aa75e51f99 [build] Build Windows x86 version with py3.8
and remove redundant tests
:ci skip

Ao-authored by: pukkandan, shirt-dev
2021-06-09 02:18:55 +05:30
pukkandan 884ce9d05d [version] update :ci skip all 2021-06-09 02:18:55 +05:30
pukkandan 3b1fe47d84 Release 2021.06.08 2021-06-08 20:13:41 +05:30
pukkandan ed64ce5905 [build] Release yt-dlp.tar.gz
Closes #386
2021-06-08 20:12:00 +05:30
pukkandan 76a264ac9e Make outtmpl more robust and catch errors early 2021-06-08 20:11:00 +05:30
pukkandan 324ad82006 [utils] Generalize traverse_dict to traverse_obj 2021-06-08 19:26:44 +05:30
Nil AdmirariandGitHub beb982bead [build,update] Add GNU-style SHA512 and prepare updater for simlar SHA256 (#383)
Authored by: nihil-admirari <50202386+nihil-admirari@users.noreply.github.com>

Related: #385
2021-06-08 16:04:07 +05:30
pukkandan e88396f123 [build] Fix SHA256 2021-06-08 01:29:35 +05:30
pukkandan 46358f647d Update to ytdl-commit-c2350ca
Update MSVC 2010 redist URL
https://github.com/ytdl-org/youtube-dl/commit/c2350cac243ba1ec1586fe85b0d62d1b700047a2
2021-06-08 00:28:32 +05:30
pukkandan bd99f6e648 Add field original_url with the user-inputted URL
So that they can be processed by `--parse-metadata` for example

`webpage_url` is the same, but may be modified by the extractor
2021-06-08 00:20:06 +05:30
pukkandan ecb5419149 Make more fields available for --print when used with --flat-playlist 2021-06-08 00:17:53 +05:30
pukkandan cf59cd4dcd [docs] Improve documentation of dependencies
Related: #348
2021-06-08 00:16:44 +05:30
Nil AdmirariandGitHub 56ce9eb832 [pyinst] Show Python version in EXE metadata (#384)
Authored by: nihil-admirari
2021-06-07 23:02:39 +05:30
pukkandan 89ee4cf8ae [viki] Fix extraction
Closes #381
Code from: https://github.com/ytdl-org/youtube-dl/commit/59e583f7e8530ca92776c866897d895c072e2a82
2021-06-07 12:42:58 +05:30
pukkandan 87ea7dfc04 Fix filename sanitization
Bug from 752cda3880
2021-06-06 19:36:28 +05:30
pukkandan eb0f9d6838 [zoom] Extract transcripts as subtitles 2021-06-06 17:09:09 +05:30
pukkandan d3d8d8184a [extractor] Fix pre-checking archive for some extractors
The `id` regex group must be present for `_match_id` and pre-checking archive to work correctly
2021-06-06 15:05:07 +05:30
pukkandan e85a39717a [twitcasting] Add TwitCastingUserIE, TwitCastingLiveIE
Closes #374

Code adapted from: https://github.com/nao20010128nao/ytdl-patched/blob/f1fb9222bb474e4ac2333e1e8047c11a856c42d3/youtube_dl/extractor/twitcasting.py
Authored by: pukkandan, nao20010128nao
2021-06-06 03:26:33 +05:30
MinePlayersPEandGitHub f2cd7060fc [vidio] Add VidioPremierIE and VidioLiveIE (#371)
Authored-by: MinePlayersPE
2021-06-06 01:25:26 +05:30
pukkandan 752cda3880 Fix and refactor prepare_outtmpl
The following tests would have failed previously:
%(id)d %(id)r
%(ext)s-%(ext|def)d
%(width|)d
%(id)r %(height)r
%(formats.0)r
%s
2021-06-06 00:59:04 +05:30
pukkandan 9d83ad93d0 [cleanup] Mark unused files 2021-06-06 00:59:04 +05:30
felixandpukkandan cc52de4356 [cleanup] Point all shebang to python3 (#372)
Authored by: fstirlitz
2021-06-06 00:59:04 +05:30
pukkandan 14b17a551f Remove support for obsolete python versions 2021-06-06 00:59:04 +05:30
felixandpukkandan 2ec1759f9d [downloader/ffmpeg] Hide FFmpeg banner unless in verbose mode (#372)
Authored by: fstirlitz
2021-06-06 00:59:04 +05:30
felixandpukkandan e2efe599aa [common] Fix FourCC fallback when parsing ISM (#372)
In some DASH manifests, the FourCC attribute is actually present,
but empty.  We thus apply the same fallback to 'AACL' that we do
when the attribute is entirely absent.

Authored by: fstirlitz
2021-06-06 00:59:04 +05:30
pukkandan 5e1dba8ed6 Remove duplicate file trovolive.py 2021-06-06 00:59:04 +05:30
pukkandan bea742222f [youtube] Support shorts URL
Closes #375
2021-06-06 00:59:04 +05:30
pukkandan e06ca6ddac [hls] Decrypt fragment when reading from disk
Closes #373
2021-06-05 18:51:15 +05:30
pukkandan eb03899192 [version] update
:ci skip all
2021-06-01 21:08:44 +05:30
157 changed files with 3770 additions and 2158 deletions
+3
View File
@@ -1 +1,4 @@
* text=auto
Makefile* text whitespace=-tab-in-indent Makefile* text whitespace=-tab-in-indent
*.sh text eol=lf
+4 -4
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.05.20. 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.06.23. 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.05.20** - [ ] I've verified that I'm running yt-dlp version **2021.06.23**
- [ ] 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
@@ -42,9 +42,9 @@ Provide the complete verbose output of yt-dlp that clearly demonstrates the prob
Add the `-v` flag to your command line you run yt-dlp with (`yt-dlp -v <your command line>`), copy the WHOLE output and insert it below. It should look similar to this: Add the `-v` flag to your command line you run yt-dlp with (`yt-dlp -v <your command line>`), copy the WHOLE output and insert it below. It should look similar to this:
[debug] System config: [] [debug] System config: []
[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_jenozKc']
[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.05.20 [debug] yt-dlp version 2021.06.23
[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.05.20. 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.06.23. 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.05.20** - [ ] I've verified that I'm running yt-dlp version **2021.06.23**
- [ ] 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.05.20. 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.06.23. 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.05.20** - [ ] I've verified that I'm running yt-dlp version **2021.06.23**
- [ ] 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
+4 -4
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.05.20. 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.06.23. 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.05.20** - [ ] I've verified that I'm running yt-dlp version **2021.06.23**
- [ ] 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
@@ -44,9 +44,9 @@ Provide the complete verbose output of yt-dlp that clearly demonstrates the prob
Add the `-v` flag to your command line you run yt-dlp with (`yt-dlp -v <your command line>`), copy the WHOLE output and insert it below. It should look similar to this: Add the `-v` flag to your command line you run yt-dlp with (`yt-dlp -v <your command line>`), copy the WHOLE output and insert it below. It should look similar to this:
[debug] System config: [] [debug] System config: []
[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_jenozKc']
[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.05.20 [debug] yt-dlp version 2021.06.23
[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.05.20. 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.06.23. 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.05.20** - [ ] I've verified that I'm running yt-dlp version **2021.06.23**
- [ ] I've searched the bugtracker for similar feature requests including closed ones - [ ] I've searched the bugtracker for similar feature requests including closed ones
+1 -1
View File
@@ -42,7 +42,7 @@ Provide the complete verbose output of yt-dlp that clearly demonstrates the prob
Add the `-v` flag to your command line you run yt-dlp with (`yt-dlp -v <your command line>`), copy the WHOLE output and insert it below. It should look similar to this: Add the `-v` flag to your command line you run yt-dlp with (`yt-dlp -v <your command line>`), copy the WHOLE output and insert it below. It should look similar to this:
[debug] System config: [] [debug] System config: []
[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_jenozKc']
[debug] Encodings: locale cp1251, fs mbcs, out cp866, pref cp1251 [debug] Encodings: locale cp1251, fs mbcs, out cp866, pref cp1251
[debug] yt-dlp version %(version)s [debug] yt-dlp version %(version)s
[debug] Python version 2.7.11 - Windows-2003Server-5.2.3790-SP2 [debug] Python version 2.7.11 - Windows-2003Server-5.2.3790-SP2
+1 -1
View File
@@ -44,7 +44,7 @@ Provide the complete verbose output of yt-dlp that clearly demonstrates the prob
Add the `-v` flag to your command line you run yt-dlp with (`yt-dlp -v <your command line>`), copy the WHOLE output and insert it below. It should look similar to this: Add the `-v` flag to your command line you run yt-dlp with (`yt-dlp -v <your command line>`), copy the WHOLE output and insert it below. It should look similar to this:
[debug] System config: [] [debug] System config: []
[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_jenozKc']
[debug] Encodings: locale cp1251, fs mbcs, out cp866, pref cp1251 [debug] Encodings: locale cp1251, fs mbcs, out cp866, pref cp1251
[debug] yt-dlp version %(version)s [debug] yt-dlp version %(version)s
[debug] Python version 2.7.11 - Windows-2003Server-5.2.3790-SP2 [debug] Python version 2.7.11 - Windows-2003Server-5.2.3790-SP2
+75 -33
View File
@@ -7,13 +7,13 @@ on:
jobs: jobs:
build_unix: build_unix:
runs-on: ubuntu-latest runs-on: ubuntu-latest
outputs: outputs:
ytdlp_version: ${{ steps.bump_version.outputs.ytdlp_version }} ytdlp_version: ${{ steps.bump_version.outputs.ytdlp_version }}
upload_url: ${{ steps.create_release.outputs.upload_url }} upload_url: ${{ steps.create_release.outputs.upload_url }}
sha2_unix: ${{ steps.sha2_file.outputs.sha2_unix }} sha256_unix: ${{ steps.sha256_file.outputs.sha256_unix }}
sha512_unix: ${{ steps.sha512_file.outputs.sha512_unix }}
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
@@ -29,7 +29,7 @@ jobs:
- name: Print version - name: Print version
run: echo "${{ steps.bump_version.outputs.ytdlp_version }}" run: echo "${{ steps.bump_version.outputs.ytdlp_version }}"
- name: Run Make - name: Run Make
run: make run: make all tar
- name: Create Release - name: Create Release
id: create_release id: create_release
uses: actions/create-release@v1 uses: actions/create-release@v1
@@ -44,7 +44,7 @@ jobs:
draft: false draft: false
prerelease: false prerelease: false
- name: Upload yt-dlp Unix binary - name: Upload yt-dlp Unix binary
id: upload-release-asset id: upload-release-asset
uses: actions/upload-release-asset@v1 uses: actions/upload-release-asset@v1
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
@@ -53,9 +53,21 @@ jobs:
asset_path: ./yt-dlp asset_path: ./yt-dlp
asset_name: yt-dlp asset_name: yt-dlp
asset_content_type: application/octet-stream asset_content_type: application/octet-stream
- name: Upload Source tar
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: ./yt-dlp.tar.gz
asset_name: yt-dlp.tar.gz
asset_content_type: application/gzip
- name: Get SHA2-256SUMS for yt-dlp - name: Get SHA2-256SUMS for yt-dlp
id: sha2_file id: sha256_file
run: echo "::set-output name=sha2_unix::$(sha256sum yt-dlp | awk '{print $1}')" run: echo "::set-output name=sha256_unix::$(sha256sum yt-dlp | awk '{print $1}')"
- name: Get SHA2-512SUMS for yt-dlp
id: sha512_file
run: echo "::set-output name=sha512_unix::$(sha512sum yt-dlp | awk '{print $1}')"
- name: Install dependencies for pypi - name: Install dependencies for pypi
env: env:
PYPI_TOKEN: ${{ secrets.PYPI_TOKEN }} PYPI_TOKEN: ${{ secrets.PYPI_TOKEN }}
@@ -74,24 +86,24 @@ jobs:
twine upload dist/* twine upload dist/*
build_windows: build_windows:
runs-on: windows-latest runs-on: windows-latest
needs: build_unix
outputs: outputs:
sha2_windows: ${{ steps.sha2_file_win.outputs.sha2_windows }} sha256_windows: ${{ steps.sha256_file_win.outputs.sha256_windows }}
sha512_windows: ${{ steps.sha512_file_win.outputs.sha512_windows }}
needs: build_unix
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
- name: Set up Python # 3.8 is used for Win7 support
- name: Set up Python 3.8
uses: actions/setup-python@v2 uses: actions/setup-python@v2
with: with:
python-version: '3.8' python-version: '3.8'
- name: Upgrade pip and enable wheel support - name: Upgrade pip and enable wheel support
run: python -m pip install --upgrade pip setuptools wheel run: python -m pip install --upgrade pip setuptools wheel
- name: Install Requirements - name: Install Requirements
run: pip install pyinstaller mutagen pycryptodome run: pip install pyinstaller mutagen pycryptodome websockets
- name: Bump version - name: Bump version
id: bump_version id: bump_version
run: python devscripts/update-version.py run: python devscripts/update-version.py
@@ -110,29 +122,32 @@ jobs:
asset_name: yt-dlp.exe asset_name: yt-dlp.exe
asset_content_type: application/vnd.microsoft.portable-executable asset_content_type: application/vnd.microsoft.portable-executable
- name: Get SHA2-256SUMS for yt-dlp.exe - name: Get SHA2-256SUMS for yt-dlp.exe
id: sha2_file_win id: sha256_file_win
run: echo "::set-output name=sha2_windows::$((Get-FileHash dist\yt-dlp.exe -Algorithm SHA256).Hash.ToLower())" run: echo "::set-output name=sha256_windows::$((Get-FileHash dist\yt-dlp.exe -Algorithm SHA256).Hash.ToLower())"
- name: Get SHA2-512SUMS for yt-dlp.exe
id: sha512_file_win
run: echo "::set-output name=sha512_windows::$((Get-FileHash dist\yt-dlp.exe -Algorithm SHA512).Hash.ToLower())"
build_windows32: build_windows32:
runs-on: windows-latest runs-on: windows-latest
needs: [build_unix, build_windows]
outputs: outputs:
sha2_windows32: ${{ steps.sha2_file_win32.outputs.sha2_windows32 }} sha256_windows32: ${{ steps.sha256_file_win32.outputs.sha256_windows32 }}
sha512_windows32: ${{ steps.sha512_file_win32.outputs.sha512_windows32 }}
needs: [build_unix, build_windows]
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
- name: Set up Python 3.4.4 32-Bit # 3.7 is used for Vista support. See https://github.com/yt-dlp/yt-dlp/issues/390
- name: Set up Python 3.7 32-Bit
uses: actions/setup-python@v2 uses: actions/setup-python@v2
with: with:
python-version: '3.4.4' python-version: '3.7'
architecture: 'x86' architecture: 'x86'
- name: Upgrade pip and enable wheel support - name: Upgrade pip and enable wheel support
run: python -m pip install pip==19.1.1 setuptools==43.0.0 wheel==0.33.6 run: python -m pip install --upgrade pip setuptools wheel
- name: Install Requirements for 32 Bit - name: Install Requirements
run: pip install pyinstaller==3.5 mutagen==1.42.0 pycryptodome==3.9.4 pefile==2019.4.18 run: pip install pyinstaller mutagen pycryptodome websockets
- name: Bump version - name: Bump version
id: bump_version id: bump_version
run: python devscripts/update-version.py run: python devscripts/update-version.py
@@ -151,20 +166,28 @@ jobs:
asset_name: yt-dlp_x86.exe asset_name: yt-dlp_x86.exe
asset_content_type: application/vnd.microsoft.portable-executable asset_content_type: application/vnd.microsoft.portable-executable
- name: Get SHA2-256SUMS for yt-dlp_x86.exe - name: Get SHA2-256SUMS for yt-dlp_x86.exe
id: sha2_file_win32 id: sha256_file_win32
run: echo "::set-output name=sha2_windows32::$((Get-FileHash dist\yt-dlp_x86.exe -Algorithm SHA256).Hash.ToLower())" run: echo "::set-output name=sha256_windows32::$((Get-FileHash dist\yt-dlp_x86.exe -Algorithm SHA256).Hash.ToLower())"
- name: Get SHA2-512SUMS for yt-dlp_x86.exe
id: sha512_file_win32
run: echo "::set-output name=sha512_windows32::$((Get-FileHash dist\yt-dlp_x86.exe -Algorithm SHA512).Hash.ToLower())"
finish:
runs-on: ubuntu-latest
needs: [build_unix, build_windows, build_windows32]
steps:
- name: Make SHA2-256SUMS file - name: Make SHA2-256SUMS file
env: env:
SHA2_WINDOWS: ${{ needs.build_windows.outputs.sha2_windows }} SHA256_WINDOWS: ${{ needs.build_windows.outputs.sha256_windows }}
SHA2_WINDOWS32: ${{ steps.sha2_file_win32.outputs.sha2_windows32 }} SHA256_WINDOWS32: ${{ needs.build_windows32.outputs.sha256_windows32 }}
SHA2_UNIX: ${{ needs.build_unix.outputs.sha2_unix }} SHA256_UNIX: ${{ needs.build_unix.outputs.sha256_unix }}
YTDLP_VERSION: ${{ needs.build_unix.outputs.ytdlp_version }} YTDLP_VERSION: ${{ needs.build_unix.outputs.ytdlp_version }}
run: | run: |
echo "version:${env:YTDLP_VERSION}" >> SHA2-256SUMS echo "version:${{ env.YTDLP_VERSION }}" >> SHA2-256SUMS
echo "yt-dlp.exe:${env:SHA2_WINDOWS}" >> SHA2-256SUMS echo "yt-dlp.exe:${{ env.SHA256_WINDOWS }}" >> SHA2-256SUMS
echo "yt-dlp_x86.exe:${env:SHA2_WINDOWS32}" >> SHA2-256SUMS echo "yt-dlp_x86.exe:${{ env.SHA256_WINDOWS32 }}" >> SHA2-256SUMS
echo "yt-dlp:${env:SHA2_UNIX}" >> SHA2-256SUMS echo "yt-dlp:${{ env.SHA256_UNIX }}" >> SHA2-256SUMS
- name: Upload 256SUMS file - name: Upload 256SUMS file
id: upload-sums id: upload-sums
uses: actions/upload-release-asset@v1 uses: actions/upload-release-asset@v1
@@ -175,3 +198,22 @@ jobs:
asset_path: ./SHA2-256SUMS asset_path: ./SHA2-256SUMS
asset_name: SHA2-256SUMS asset_name: SHA2-256SUMS
asset_content_type: text/plain asset_content_type: text/plain
- name: Make SHA2-512SUMS file
env:
SHA512_WINDOWS: ${{ needs.build_windows.outputs.sha512_windows }}
SHA512_WINDOWS32: ${{ needs.build_windows32.outputs.sha512_windows32 }}
SHA512_UNIX: ${{ needs.build_unix.outputs.sha512_unix }}
run: |
echo "${{ env.SHA512_WINDOWS }} yt-dlp.exe" >> SHA2-512SUMS
echo "${{ env.SHA512_WINDOWS32 }} yt-dlp_x86.exe" >> SHA2-512SUMS
echo "${{ env.SHA512_UNIX }} yt-dlp" >> SHA2-512SUMS
- name: Upload 512SUMS file
id: upload-512sums
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ needs.build_unix.outputs.upload_url }}
asset_path: ./SHA2-512SUMS
asset_name: SHA2-512SUMS
asset_content_type: text/plain
+6 -34
View File
@@ -9,53 +9,25 @@ jobs:
fail-fast: false fail-fast: false
matrix: matrix:
os: [ubuntu-18.04] os: [ubuntu-18.04]
# TODO: python 2.6 # py3.9 is in quick-test
python-version: [2.7, 3.3, 3.4, 3.5, 3.6, 3.7, 3.8, 3.9, pypy-2.7, pypy-3.6, pypy-3.7] python-version: [3.7, 3.8, pypy-3.6, pypy-3.7]
python-impl: [cpython]
ytdl-test-set: [core]
run-tests-ext: [sh] run-tests-ext: [sh]
include: include:
# python 3.2 is only available on windows via setup-python # atleast one of the tests must be in windows
- os: windows-latest - os: windows-latest
python-version: 3.2 python-version: 3.6
python-impl: cpython
ytdl-test-set: core
run-tests-ext: bat run-tests-ext: bat
# jython
- os: ubuntu-latest
python-impl: jython
ytdl-test-set: core
run-tests-ext: sh
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
- name: Set up Python ${{ matrix.python-version }} - name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v2 uses: actions/setup-python@v2
if: ${{ matrix.python-impl == 'cpython' }}
with: with:
python-version: ${{ matrix.python-version }} python-version: ${{ matrix.python-version }}
- name: Set up Java 8
if: ${{ matrix.python-impl == 'jython' }}
uses: actions/setup-java@v1
with:
java-version: 8
- name: Install Jython
if: ${{ matrix.python-impl == 'jython' }}
run: |
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"
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: False
env: env:
YTDL_TEST_SET: ${{ matrix.ytdl-test-set }} YTDL_TEST_SET: core
run: ./devscripts/run_tests.${{ matrix.run-tests-ext }} run: ./devscripts/run_tests.${{ matrix.run-tests-ext }}
# Linter is in quick-test # Linter is in quick-test
+4 -34
View File
@@ -9,52 +9,22 @@ jobs:
fail-fast: true fail-fast: true
matrix: matrix:
os: [ubuntu-18.04] os: [ubuntu-18.04]
# TODO: python 2.6 python-version: [3.7, 3.8, 3.9, pypy-3.6, pypy-3.7]
python-version: [2.7, 3.3, 3.4, 3.5, 3.6, 3.7, 3.8, 3.9, pypy-2.7, pypy-3.6, pypy-3.7]
python-impl: [cpython]
ytdl-test-set: [download]
run-tests-ext: [sh] run-tests-ext: [sh]
include: include:
# python 3.2 is only available on windows via setup-python
- os: windows-latest - os: windows-latest
python-version: 3.2 python-version: 3.6
python-impl: cpython
ytdl-test-set: download
run-tests-ext: bat run-tests-ext: bat
# jython - disable for now since it takes too long to complete
# - os: ubuntu-latest
# python-impl: jython
# ytdl-test-set: download
# run-tests-ext: sh
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
- name: Set up Python ${{ matrix.python-version }} - name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v2 uses: actions/setup-python@v2
if: ${{ matrix.python-impl == 'cpython' }}
with: with:
python-version: ${{ matrix.python-version }} python-version: ${{ matrix.python-version }}
- name: Set up Java 8
if: ${{ matrix.python-impl == 'jython' }}
uses: actions/setup-java@v1
with:
java-version: 8
- name: Install Jython
if: ${{ matrix.python-impl == 'jython' }}
run: |
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"
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: true
env: env:
YTDL_TEST_SET: ${{ matrix.ytdl-test-set }} YTDL_TEST_SET: download
run: ./devscripts/run_tests.${{ matrix.run-tests-ext }} run: ./devscripts/run_tests.${{ matrix.run-tests-ext }}
+1 -1
View File
@@ -3,7 +3,7 @@
$ youtube-dl -v <your command line> $ youtube-dl -v <your command line>
[debug] System config: [] [debug] System config: []
[debug] User config: [] [debug] User config: []
[debug] Command-line args: [u'-v', u'https://www.youtube.com/watch?v=BaW_jenozKcj'] [debug] Command-line args: [u'-v', u'https://www.youtube.com/watch?v=BaW_jenozKc']
[debug] Encodings: locale cp1251, fs mbcs, out cp866, pref cp1251 [debug] Encodings: locale cp1251, fs mbcs, out cp866, pref cp1251
[debug] youtube-dl version 2015.12.06 [debug] youtube-dl version 2015.12.06
[debug] Git HEAD: 135392e [debug] Git HEAD: 135392e
+12 -2
View File
@@ -1,6 +1,7 @@
pukkandan (owner) pukkandan (owner)
shirt-dev (collaborator) shirt-dev (collaborator)
colethedj (collaborator) colethedj (collaborator)
Ashish0804 (collaborator)
h-h-h-h h-h-h-h
pauldubois98 pauldubois98
nixxo nixxo
@@ -20,11 +21,9 @@ FelixFrog
Zocker1999NET Zocker1999NET
nao20010128nao nao20010128nao
kurumigi kurumigi
tsukumi
bbepis bbepis
animelover1984 animelover1984
Pccode66 Pccode66
Ashish0804
RobinD42 RobinD42
hseg hseg
DennyDai DennyDai
@@ -48,3 +47,14 @@ craftingmod
tpikonen tpikonen
tripulse tripulse
king-millez king-millez
alex-gedeon
hhirtz
louie-github
MinePlayersPE
olifre
rhsmachine/zenerdi0de
nihil-admirari
krichbanana
ohmybahgosh
nyuszika7h
blackjack4494
+119 -2
View File
@@ -19,13 +19,130 @@
--> -->
### 2021.07.07
* Merge youtube-dl: Upto [commit/a803582](https://github.com/ytdl-org/youtube-dl/commit/a8035827177d6b59aca03bd717acb6a9bdd75ada)
* Add `--extractor-args` to pass extractor-specific arguments
* Add extractor option `skip` for `youtube`. Eg: `--extractor-args youtube:skip=hls,dash`
* Deprecates --youtube-skip-dash-manifest, --youtube-skip-hls-manifest, --youtube-include-dash-manifest, --youtube-include-hls-manifest
* Allow `--list...` options to work with `--print`, `--quiet` and other `--list...` options
* [youtube] Use `player` API for additional video extraction requests by [colethedj](https://github.com/colethedj)
* **Fixes youtube premium music** (format 141) extraction
* Adds extractor option `player_client` = `web`/`android`
* **`--extractor-args youtube:player_client=android` works around the throttling** for the time-being
* Adds extractor option `player_skip=config`
* Adds age-gate fallback using embedded client
* [youtube] Choose correct Live chat API for upcoming streams by [krichbanana](https://github.com/krichbanana)
* [youtube] Fix subtitle names for age-gated videos
* [youtube:comments] Fix error handling and add `itct` to params by [colethedj](https://github.com/colethedj)
* [youtube_live_chat] Fix download with cookies by [siikamiika](https://github.com/siikamiika)
* [youtube_live_chat] use `clickTrackingParams` by [siikamiika](https://github.com/siikamiika)
* [Funimation] Rewrite extractor
* Add `FunimationShowIE` by [Mevious](https://github.com/Mevious)
* **Treat the different versions of an episode as different formats of a single video**
* This changes the video `id` and will break break existing archives
* Compat option `seperate-video-versions` to fall back to old behavior including using the old video ids
* Support direct `/player/` URL
* Extractor options `language` and `version` to pre-select them during extraction
* These options may be removed in the future if we can extract all formats without additional network requests
* Do not rely on these for format selection and use `-f` filters instead
* [AdobePass] Add Spectrum MSO by [kevinoconnor7](https://github.com/kevinoconnor7), [ohmybahgosh](https://github.com/ohmybahgosh)
* [facebook] Extract description and fix title
* [fancode] Fix extraction, support live and allow login with refresh token by [zenerdi0de](https://github.com/zenerdi0de)
* [plutotv] Improve `_VALID_URL`
* [RCTIPlus] Add extractor by [MinePlayersPE](https://github.com/MinePlayersPE)
* [Soundcloud] Allow login using oauth token by [blackjack4494](https://github.com/blackjack4494)
* [TBS] Support livestreams by [llacb47](https://github.com/llacb47)
* [videa] Fix extraction by [nyuszika7h](https://github.com/nyuszika7h)
* [yahoo] Fix extraction by [llacb47](https://github.com/llacb47), [pukkandan](https://github.com/pukkandan)
* Process videos when using `--ignore-no-formats-error` by [krichbanana](https://github.com/krichbanana)
* Fix `--throttled-rate` when using `--load-info-json`
* Fix `--flat-playlist` when entry has no `ie_key`
* Fix `check_formats` catching `ExtractorError` instead of `DownloadError`
* Fix deprecated option `--list-formats-old`
* [downloader/ffmpeg] Fix `--ppa` when using simultaneous download
* [extractor] Prevent unnecessary download of hls manifests and refactor `hls_split_discontinuity`
* [fragment] Handle status of download and errors in threads correctly; and minor refactoring
* [thumbnailsconvertor] Treat `jpeg` as `jpg`
* [utils] Fix issues with `LazyList` reversal
* [extractor] Allow extractors to set their own login hint
* [cleanup] Simplify format selector code with `LazyList` and `yield from`
* [cleanup] Clean `extractor.common._merge_subtitles` signature
* [cleanup] Fix some typos
### 2021.06.23
* Merge youtube-dl: Upto [commit/379f52a](https://github.com/ytdl-org/youtube-dl/commit/379f52a4954013767219d25099cce9e0f9401961)
* **Add option `--throttled-rate`** below which video data is re-extracted
* [fragment] **Merge during download for `-N`**, and refactor `hls`/`dash`
* [websockets] Add `WebSocketFragmentFD` by [nao20010128nao](https://github.com/nao20010128nao), [pukkandan](https://github.com/pukkandan)
* Allow `images` formats in addition to video/audio
* [downloader/mhtml] Add new downloader for slideshows/storyboards by [fstirlitz](https://github.com/fstirlitz)
* [youtube] Temporary **fix for age-gate**
* [youtube] Support ongoing live chat by [siikamiika](https://github.com/siikamiika)
* [youtube] Improve SAPISID cookie handling by [colethedj](https://github.com/colethedj)
* [youtube] Login is not needed for `:ytrec`
* [youtube] Non-fatal alert reporting for unavailable videos page by [colethedj](https://github.com/colethedj)
* [twitcasting] Websocket support by [nao20010128nao](https://github.com/nao20010128nao)
* [mediasite] Extract slides by [fstirlitz](https://github.com/fstirlitz)
* [funimation] Extract subtitles
* [pornhub] Extract `cast`
* [hotstar] Use server time for authentication instead of local time
* [EmbedThumbnail] Fix for already downloaded thumbnail
* [EmbedThumbnail] Add compat-option `embed-thumbnail-atomicparsley`
* Expand `--check-formats` to thumbnails
* Fix id sanitization in filenames
* Skip fixup of existing files and add `--fixup force` to force it
* Better error handling of syntax errors in `-f`
* Use `NamedTemporaryFile` for `--check-formats`
* [aria2c] Lower `--min-split-size` for HTTP downloads
* [options] Rename `--add-metadata` to `--embed-metadata`
* [utils] Improve `LazyList` and add tests
* [build] Build Windows x86 version with py3.7 and remove redundant tests by [pukkandan](https://github.com/pukkandan), [shirt](https://github.com/shirt-dev)
* [docs] Clarify that `--embed-metadata` embeds chapter markers
* [cleanup] Refactor fixup
### 2021.06.09
* Fix bug where `%(field)d` in filename template throws error
* Improve offset parsing in outtmpl
* [test] More rigorous tests for `prepare_filename`
### 2021.06.08
* Remove support for obsolete Python versions: Only 3.6+ is now supported
* Merge youtube-dl: Upto [commit/c2350ca](https://github.com/ytdl-org/youtube-dl/commit/c2350cac243ba1ec1586fe85b0d62d1b700047a2)
* [hls] Fix decryption for multithreaded downloader
* [extractor] Fix pre-checking archive for some extractors
* [extractor] Fix FourCC fallback when parsing ISM by [fstirlitz](https://github.com/fstirlitz)
* [twitcasting] Add TwitCastingUserIE, TwitCastingLiveIE by [pukkandan](https://github.com/pukkandan), [nao20010128nao](https://github.com/nao20010128nao)
* [vidio] Add VidioPremierIE and VidioLiveIE by [MinePlayersPE](Https://github.com/MinePlayersPE)
* [viki] Fix extraction from [ytdl-org/youtube-dl@59e583f](https://github.com/ytdl-org/youtube-dl/commit/59e583f7e8530ca92776c866897d895c072e2a82)
* [youtube] Support shorts URL
* [zoom] Extract transcripts as subtitles
* Add field `original_url` with the user-inputted URL
* Fix and refactor `prepare_outtmpl`
* Make more fields available for `--print` when used with `--flat-playlist`
* [utils] Generalize `traverse_dict` to `traverse_obj`
* [downloader/ffmpeg] Hide FFmpeg banner unless in verbose mode by [fstirlitz](https://github.com/fstirlitz)
* [build] Release `yt-dlp.tar.gz`
* [build,update] Add GNU-style SHA512 and prepare updater for simlar SHA256 by [nihil-admirari](https://github.com/nihil-admirari)
* [pyinst] Show Python version in exe metadata by [nihil-admirari](https://github.com/nihil-admirari)
* [docs] Improve documentation of dependencies
* [cleanup] Mark unused files
* [cleanup] Point all shebang to `python3` by [fstirlitz](https://github.com/fstirlitz)
* [cleanup] Remove duplicate file `trovolive.py`
### 2021.06.01 ### 2021.06.01
* Merge youtube-dl: Upto [commit/d495292](https://github.com/ytdl-org/youtube-dl/commit/d495292852b6c2f1bd58bc2141ff2b0265c952cf) * Merge youtube-dl: Upto [commit/d495292](https://github.com/ytdl-org/youtube-dl/commit/d495292852b6c2f1bd58bc2141ff2b0265c952cf)
* Pre-check archive and filters during playlist extraction * Pre-check archive and filters during playlist extraction
* Handle Basic Auth `user:pass` in URLs by [hhirtz](https://github.com/hhirtz) and [pukkandan](https://github.com/pukkandan) * Handle Basic Auth `user:pass` in URLs by [hhirtz](https://github.com/hhirtz) and [pukkandan](https://github.com/pukkandan)
* [archiveorg] Add YoutubeWebArchiveIE by [colethedj](https://github.com/colethedj) and [alex-gedeon](https://github.com/alex-gedeon) * [archiveorg] Add YoutubeWebArchiveIE by [colethedj](https://github.com/colethedj) and [alex-gedeon](https://github.com/alex-gedeon)
* [fancode] Add extractor by [rmsmachine](https://github.com/rmsmachine) * [fancode] Add extractor by [rhsmachine](https://github.com/rhsmachine)
* [patreon] Support vimeo embeds by [rhsmachine](https://github.com/rhsmachine) * [patreon] Support vimeo embeds by [rhsmachine](https://github.com/rhsmachine)
* [Saitosan] Add new extractor by [llacb47](https://github.com/llacb47) * [Saitosan] Add new extractor by [llacb47](https://github.com/llacb47)
* [ShemarooMe] Add extractor by [Ashish0804](https://github.com/Ashish0804) and [pukkandan](https://github.com/pukkandan) * [ShemarooMe] Add extractor by [Ashish0804](https://github.com/Ashish0804) and [pukkandan](https://github.com/pukkandan)
@@ -400,7 +517,7 @@
### 2021.02.15 ### 2021.02.15
* Merge youtube-dl: Upto [2021.02.10](https://github.com/ytdl-org/youtube-dl/releases/tag/2021.02.10) (except archive.org) * Merge youtube-dl: Upto [2021.02.10](https://github.com/ytdl-org/youtube-dl/releases/tag/2021.02.10) (except archive.org)
* [niconico] Improved extraction and support encrypted/SMILE movies by [kurumigi](https://github.com/kurumigi), [tsukumi](https://github.com/tsukumi), [bbepis](https://github.com/bbepis), [pukkandan](https://github.com/pukkandan) * [niconico] Improved extraction and support encrypted/SMILE movies by [kurumigi](https://github.com/kurumigi), [tsukumijima](https://github.com/tsukumijima), [bbepis](https://github.com/bbepis), [pukkandan](https://github.com/pukkandan)
* Fix HLS AES-128 with multiple keys in external downloaders by [shirt](https://github.com/shirt-dev) * Fix HLS AES-128 with multiple keys in external downloaders by [shirt](https://github.com/shirt-dev)
* [youtube_live_chat] Fix by using POST API by [siikamiika](https://github.com/siikamiika) * [youtube_live_chat] Fix by using POST API by [siikamiika](https://github.com/siikamiika)
* [rumble] Add support for video page * [rumble] Add support for video page
+1
View File
@@ -25,6 +25,7 @@ completion-zsh: completions/zsh/_yt-dlp
lazy-extractors: yt_dlp/extractor/lazy_extractors.py lazy-extractors: yt_dlp/extractor/lazy_extractors.py
PREFIX ?= /usr/local PREFIX ?= /usr/local
DESTDIR ?= .
BINDIR ?= $(PREFIX)/bin BINDIR ?= $(PREFIX)/bin
MANDIR ?= $(PREFIX)/man MANDIR ?= $(PREFIX)/man
SHAREDIR ?= $(PREFIX)/share SHAREDIR ?= $(PREFIX)/share
+79 -40
View File
@@ -22,8 +22,8 @@ yt-dlp is a [youtube-dl](https://github.com/ytdl-org/youtube-dl) fork based on t
* [NEW FEATURES](#new-features) * [NEW FEATURES](#new-features)
* [Differences in default behavior](#differences-in-default-behavior) * [Differences in default behavior](#differences-in-default-behavior)
* [INSTALLATION](#installation) * [INSTALLATION](#installation)
* [Dependencies](#dependencies)
* [Update](#update) * [Update](#update)
* [Dependencies](#dependencies)
* [Compile](#compile) * [Compile](#compile)
* [USAGE AND OPTIONS](#usage-and-options) * [USAGE AND OPTIONS](#usage-and-options)
* [General Options](#general-options) * [General Options](#general-options)
@@ -53,6 +53,7 @@ yt-dlp is a [youtube-dl](https://github.com/ytdl-org/youtube-dl) fork based on t
* [Format Selection examples](#format-selection-examples) * [Format Selection examples](#format-selection-examples)
* [MODIFYING METADATA](#modifying-metadata) * [MODIFYING METADATA](#modifying-metadata)
* [Modifying metadata examples](#modifying-metadata-examples) * [Modifying metadata examples](#modifying-metadata-examples)
* [EXTRACTOR ARGUMENTS](#extractor-arguments)
* [PLUGINS](#plugins) * [PLUGINS](#plugins)
* [DEPRECATED OPTIONS](#deprecated-options) * [DEPRECATED OPTIONS](#deprecated-options)
* [MORE](#more) * [MORE](#more)
@@ -66,7 +67,7 @@ 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 [commit/d495292](https://github.com/ytdl-org/youtube-dl/commit/d495292852b6c2f1bd58bc2141ff2b0265c952cf)**: (v2021.05.16) 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/379f52a](https://github.com/ytdl-org/youtube-dl/commit/379f52a4954013767219d25099cce9e0f9401961)**: (v2021.06.06) You get all the latest features and patches of [youtube-dl](https://github.com/ytdl-org/youtube-dl) in addition to all the features of [youtube-dlc](https://github.com/blackjack4494/yt-dlc)
* **Merged with animelover1984/youtube-dl**: You get most of the features and improvements from [animelover1984/youtube-dl](https://github.com/animelover1984/youtube-dl) including `--write-comments`, `BiliBiliSearch`, `BilibiliChannel`, Embedding thumbnail in mp4/ogg/opus, playlist infojson etc. Note that the NicoNico improvements are not available. See [#31](https://github.com/yt-dlp/yt-dlp/pull/31) for details. * **Merged with animelover1984/youtube-dl**: You get most of the features and improvements from [animelover1984/youtube-dl](https://github.com/animelover1984/youtube-dl) including `--write-comments`, `BiliBiliSearch`, `BilibiliChannel`, Embedding thumbnail in mp4/ogg/opus, playlist infojson etc. Note that the NicoNico improvements are not available. See [#31](https://github.com/yt-dlp/yt-dlp/pull/31) for details.
@@ -84,9 +85,9 @@ 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, ukcolumn, whowatch, MxplayerShow, parlview (au), YoutubeWebArchive, fancode, Saitosan, ShemarooMe, telemundo, VootSeries, SonyLIVSeries, HotstarSeries * **New extractors**: AnimeLab, Philo MSO, Spectrum MSO, Rcs, Gedi, bitwave.tv, mildom, audius, zee5, mtv.it, wimtv, pluto.tv, niconico users, discoveryplus.in, mediathek, NFHSNetwork, nebula, ukcolumn, whowatch, MxplayerShow, parlview (au), YoutubeWebArchive, fancode, Saitosan, ShemarooMe, telemundo, VootSeries, SonyLIVSeries, HotstarSeries, VidioPremier, VidioLive, RCTIPlus, TBS Live
* **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, tenplay, patreon * **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, tenplay, patreon, videa, yahoo
* **Subtitle extraction from manifests**: Subtitles can be extracted from streaming media manifests. See [commit/be6202f](https://github.com/yt-dlp/yt-dlp/commit/be6202f12b97858b9d716e608394b51065d0419f) for details * **Subtitle extraction from manifests**: Subtitles can be extracted from streaming media manifests. See [commit/be6202f](https://github.com/yt-dlp/yt-dlp/commit/be6202f12b97858b9d716e608394b51065d0419f) for details
@@ -127,10 +128,12 @@ Some of yt-dlp's default options are different from that of youtube-dl and youtu
* `--add-metadata` attaches the `infojson` to `mkv` files in addition to writing the metadata when used with `--write-infojson`. Use `--compat-options no-attach-info-json` to revert this * `--add-metadata` attaches the `infojson` to `mkv` files in addition to writing the metadata when used with `--write-infojson`. Use `--compat-options no-attach-info-json` to revert this
* `playlist_index` behaves differently when used with options like `--playlist-reverse` and `--playlist-items`. See [#302](https://github.com/yt-dlp/yt-dlp/issues/302) for details. You can use `--compat-options playlist-index` if you want to keep the earlier behavior * `playlist_index` behaves differently when used with options like `--playlist-reverse` and `--playlist-items`. See [#302](https://github.com/yt-dlp/yt-dlp/issues/302) for details. You can use `--compat-options playlist-index` if you want to keep the earlier behavior
* The output of `-F` is listed in a new format. Use `--compat-options list-formats` to revert this * The output of `-F` is listed in a new format. Use `--compat-options list-formats` to revert this
* All *experiences* of a funimation episode are considered as a single video. This behavior breaks existing archives. Use `--compat-options seperate-video-versions` to extract information from only the default player
* 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 * 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
* Youtube channel URLs are automatically redirected to `/video`. Append a `/featured` to the URL to download only the videos in the home page. If the channel does not have a videos tab, we try to download the equivalent `UU` playlist instead. Also, `/live` URLs raise an error if there are no live videos instead of silently downloading the entire channel. You may use `--compat-options no-youtube-channel-redirect` to revert all these redirections * Youtube channel URLs are automatically redirected to `/video`. Append a `/featured` to the URL to download only the videos in the home page. If the channel does not have a videos tab, we try to download the equivalent `UU` playlist instead. Also, `/live` URLs raise an error if there are no live videos instead of silently downloading the entire channel. You may use `--compat-options no-youtube-channel-redirect` to revert all these redirections
* Unavailable videos are also listed for youtube playlists. Use `--compat-options no-youtube-unavailable-videos` to remove this * Unavailable videos are also listed for youtube playlists. Use `--compat-options no-youtube-unavailable-videos` to remove this
* If `ffmpeg` is used as the downloader, the downloading and merging of formats happen in a single step when possible. Use `--compat-options no-direct-merge` to revert this * If `ffmpeg` is used as the downloader, the downloading and merging of formats happen in a single step when possible. Use `--compat-options no-direct-merge` to revert this
* Thumbnail embedding in `mp4` is done with mutagen if possible. Use `--compat-options embed-thumbnail-atomicparsley` to force the use of AtomicParsley instead
For ease of use, a few more compat options are available: For ease of use, a few more compat options are available:
* `--compat-options all`: Use all compat options * `--compat-options all`: Use all compat options
@@ -166,23 +169,38 @@ 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
``` ```
### DEPENDENCIES
Python versions 3.6+ (CPython and PyPy) are officially supported. Other versions and implementations may or maynot work correctly.
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 other required dependencies, `ffmpeg` and `ffprobe` are highly recommended. Other optional dependencies are `sponskrub`, `AtomicParsley`, `mutagen`, `pycryptodome`, `phantomjs` 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.
If you are using `pip`, simply re-run the same command that was used to install the program. If you are using `pip`, simply re-run the same command that was used to install the program.
### DEPENDENCIES
Python versions 3.6+ (CPython and PyPy) are supported. Other versions and implementations may or may not work correctly.
<!-- https://www.microsoft.com/en-us/download/details.aspx?id=26999 -->
On windows, [Microsoft Visual C++ 2010 SP1 Redistributable Package (x86)](https://download.microsoft.com/download/1/6/5/165255E7-1014-4D0A-B094-B6A430A6BFFC/vcredist_x86.exe) 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 manually.
While all the other dependancies are optional, `ffmpeg` and `ffprobe` are highly recommended
* [**ffmpeg** and **ffprobe**](https://www.ffmpeg.org) - Required for [merging seperate video and audio files](#format-selection) as well as for various [post-processing](#post-processing-options) tasks. Licence [depends on the build](https://www.ffmpeg.org/legal.html)
* [**sponskrub**](https://github.com/faissaloo/SponSkrub) - For using the [sponskrub options](#sponskrub-sponsorblock-options). Licenced under [GPLv3+](https://github.com/faissaloo/SponSkrub/blob/master/LICENCE.md)
* [**mutagen**](https://github.com/quodlibet/mutagen) - For embedding thumbnail in certain formats. Licenced under [GPLv2+](https://github.com/quodlibet/mutagen/blob/master/COPYING)
* [**pycryptodome**](https://github.com/Legrandin/pycryptodome) - For decrypting various data. Licenced under [BSD2](https://github.com/Legrandin/pycryptodome/blob/master/LICENSE.rst)
* [**websockets**](https://github.com/aaugustin/websockets) - For downloading over websocket. Licenced under [BSD3](https://github.com/aaugustin/websockets/blob/main/LICENSE)
* [**AtomicParsley**](https://github.com/wez/atomicparsley) - For embedding thumbnail in mp4/m4a if mutagen is not present. Licenced under [GPLv2+](https://github.com/wez/atomicparsley/blob/master/COPYING)
* [**rtmpdump**](http://rtmpdump.mplayerhq.hu) - For downloading `rtmp` streams. ffmpeg will be used as a fallback. Licenced under [GPLv2+](http://rtmpdump.mplayerhq.hu)
* [**mplayer**](http://mplayerhq.hu/design7/info.html) or [**mpv**](https://mpv.io) - For downloading `rstp` streams. ffmpeg will be used as a fallback. Licenced under [GPLv2+](https://github.com/mpv-player/mpv/blob/master/Copyright)
* [**phantomjs**](https://github.com/ariya/phantomjs) - Used in extractors where javascript needs to be run. Licenced under [BSD3](https://github.com/ariya/phantomjs/blob/master/LICENSE.BSD)
* Any external downloader that you want to use with `--downloader`
To use or redistribute the dependencies, you must agree to their respective licensing terms.
Note that the windows releases are already built with the python interpreter, mutagen, pycryptodome and websockets included.
### COMPILE ### COMPILE
**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, pycryptodome, websockets)
python3 -m pip install --upgrade pyinstaller mutagen pycryptodome python3 -m pip install --upgrade pyinstaller mutagen pycryptodome websockets
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. 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.
@@ -358,6 +376,9 @@ Then simply run `make`. You can also run `make yt-dlp` instead to compile only t
(default is 1) (default is 1)
-r, --limit-rate RATE Maximum download rate in bytes per second -r, --limit-rate RATE Maximum download rate in bytes per second
(e.g. 50K or 4.2M) (e.g. 50K or 4.2M)
--throttled-rate RATE Minimum download rate in bytes per second
below which throttling is assumed and the
video data is re-extracted (e.g. 100K)
-R, --retries RETRIES Number of retries (default is 10), or -R, --retries RETRIES Number of retries (default is 10), or
"infinite" "infinite"
--fragment-retries RETRIES Number of retries for a fragment (default --fragment-retries RETRIES Number of retries for a fragment (default
@@ -414,7 +435,8 @@ Then simply run `make`. You can also run `make yt-dlp` instead to compile only t
--downloader-args NAME:ARGS Give these arguments to the external --downloader-args NAME:ARGS Give these arguments to the external
downloader. Specify the downloader name and downloader. Specify the downloader name and
the arguments separated by a colon ":". You the arguments separated by a colon ":". You
can use this option multiple times can use this option multiple times to give
different arguments to different downloaders
(Alias: --external-downloader-args) (Alias: --external-downloader-args)
## Filesystem Options: ## Filesystem Options:
@@ -500,13 +522,10 @@ Then simply run `make`. You can also run `make yt-dlp` instead to compile only t
jar in jar in
--no-cookies Do not read/dump cookies (default) --no-cookies Do not read/dump cookies (default)
--cache-dir DIR Location in the filesystem where youtube-dl --cache-dir DIR Location in the filesystem where youtube-dl
can store some downloaded information can store some downloaded information (such
permanently. By default as client ids and signatures) permanently.
$XDG_CACHE_HOME/youtube-dl or By default $XDG_CACHE_HOME/youtube-dl or
~/.cache/youtube-dl . At the moment, only ~/.cache/youtube-dl
YouTube player files (for videos with
obfuscated signatures) are cached, but that
may change
--no-cache-dir Disable filesystem caching --no-cache-dir Disable filesystem caching
--rm-cache-dir Delete all filesystem cache files --rm-cache-dir Delete all filesystem cache files
@@ -699,7 +718,8 @@ Then simply run `make`. You can also run `make yt-dlp` instead to compile only t
Metadata, EmbedSubtitle, EmbedThumbnail, Metadata, EmbedSubtitle, EmbedThumbnail,
SubtitlesConvertor, ThumbnailsConvertor, SubtitlesConvertor, ThumbnailsConvertor,
VideoRemuxer, VideoConvertor, SponSkrub, VideoRemuxer, VideoConvertor, SponSkrub,
FixupStretched, FixupM4a and FixupM3u8. The FixupStretched, FixupM4a, FixupM3u8,
FixupTimestamp and FixupDuration. The
supported executables are: AtomicParsley, supported executables are: AtomicParsley,
FFmpeg, FFprobe, and SponSkrub. You can FFmpeg, FFprobe, and SponSkrub. You can
also specify "PP+EXE:ARGS" to give the also specify "PP+EXE:ARGS" to give the
@@ -723,10 +743,13 @@ Then simply run `make`. You can also run `make yt-dlp` instead to compile only t
--embed-subs Embed subtitles in the video (only for mp4, --embed-subs Embed subtitles in the video (only for mp4,
webm and mkv videos) webm and mkv videos)
--no-embed-subs Do not embed subtitles (default) --no-embed-subs Do not embed subtitles (default)
--embed-thumbnail Embed thumbnail in the audio as cover art --embed-thumbnail Embed thumbnail in the video as cover art
--no-embed-thumbnail Do not embed thumbnail (default) --no-embed-thumbnail Do not embed thumbnail (default)
--add-metadata Write metadata to the video file --embed-metadata Embed metadata including chapter markers
--no-add-metadata Do not write metadata (default) (if supported by the format) to the video
file (Alias: --add-metadata)
--no-embed-metadata Do not write metadata (default)
(Alias: --no-add-metadata)
--parse-metadata FROM:TO Parse additional metadata like title/artist --parse-metadata FROM:TO Parse additional metadata like title/artist
from other fields; see "MODIFYING METADATA" from other fields; see "MODIFYING METADATA"
for details for details
@@ -736,7 +759,8 @@ Then simply run `make`. You can also run `make yt-dlp` instead to compile only t
file. One of never (do nothing), warn (only file. One of never (do nothing), warn (only
emit a warning), detect_or_warn (the emit a warning), detect_or_warn (the
default; fix file if we can, warn default; fix file if we can, warn
otherwise) otherwise), force (try fixing even if file
already exists
--ffmpeg-location PATH Location of the ffmpeg binary; either the --ffmpeg-location PATH Location of the ffmpeg binary; either the
path to the binary or its containing path to the binary or its containing
directory directory
@@ -795,18 +819,10 @@ Then simply run `make`. You can also run `make yt-dlp` instead to compile only t
--no-hls-split-discontinuity Do not split HLS playlists to different --no-hls-split-discontinuity Do not split HLS playlists to different
formats at discontinuities such as ad formats at discontinuities such as ad
breaks (default) breaks (default)
--youtube-include-dash-manifest Download the DASH manifests and related --extractor-args KEY:ARGS Pass these arguments to the extractor. See
data on YouTube videos (default) "EXTRACTOR ARGUMENTS" for details. You can
(Alias: --no-youtube-skip-dash-manifest) use this option multiple times to give
--youtube-skip-dash-manifest Do not download the DASH manifests and different arguments to different extractors
related data on YouTube videos
(Alias: --no-youtube-include-dash-manifest)
--youtube-include-hls-manifest Download the HLS manifests and related data
on YouTube videos (default)
(Alias: --no-youtube-skip-hls-manifest)
--youtube-skip-hls-manifest Do not download the HLS manifests and
related data on YouTube videos
(Alias: --no-youtube-include-hls-manifest)
# CONFIGURATION # CONFIGURATION
@@ -954,6 +970,8 @@ The available fields are:
- `playlist_title` (string): Playlist title - `playlist_title` (string): Playlist title
- `playlist_uploader` (string): Full name of the playlist uploader - `playlist_uploader` (string): Full name of the playlist uploader
- `playlist_uploader_id` (string): Nickname or id of the playlist uploader - `playlist_uploader_id` (string): Nickname or id of the playlist uploader
- `webpage_url` (string): A URL to the video webpage which if given to yt-dlp should allow to get the same result again
- `original_url` (string): The URL given by the user (or same as `webpage_url` for playlist entries)
Available for the video that belongs to some logical chapter or section: Available for the video that belongs to some logical chapter or section:
@@ -998,7 +1016,7 @@ Available only when used in `--print`:
Each aforementioned sequence when referenced in an output template will be replaced by the actual value corresponding to the sequence name. Note that some of the sequences are not guaranteed to be present since they depend on the metadata obtained by a particular extractor. Such sequences will be replaced with placeholder value provided with `--output-na-placeholder` (`NA` by default). Each aforementioned sequence when referenced in an output template will be replaced by the actual value corresponding to the sequence name. Note that some of the sequences are not guaranteed to be present since they depend on the metadata obtained by a particular extractor. Such sequences will be replaced with placeholder value provided with `--output-na-placeholder` (`NA` by default).
For example for `-o %(title)s-%(id)s.%(ext)s` and an mp4 video with title `yt-dlp test video` and id `BaW_jenozKcj`, this will result in a `yt-dlp test video-BaW_jenozKcj.mp4` file created in the current directory. For example for `-o %(title)s-%(id)s.%(ext)s` and an mp4 video with title `yt-dlp test video` and id `BaW_jenozKc`, this will result in a `yt-dlp test video-BaW_jenozKc.mp4` file created in the current directory.
For numeric sequences you can use numeric related formatting, for example, `%(view_count)05d` will result in a string with view count padded with zeros up to 5 characters, like in `00042`. For numeric sequences you can use numeric related formatting, for example, `%(view_count)05d` will result in a string with view count padded with zeros up to 5 characters, like in `00042`.
@@ -1127,7 +1145,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`> `websocket_frag` > 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`
@@ -1308,6 +1326,23 @@ $ yt-dlp --parse-metadata 'description:(?s)(?P<meta_comment>.+)' --add-metadata
``` ```
# EXTRACTOR ARGUMENTS
Some extractors accept additional arguments which can be passed using `--extractor-args KEY:ARGS`. `ARGS` is a `;` (semicolon) seperated string of `ARG=VAL1,VAL2`. Eg: `--extractor-args "youtube:skip=dash,hls;player_client=android" --extractor-args "funimation:version=uncut"`
The following extractors use this feature:
* **youtube**
* `skip`: `hls` or `dash` (or both) to skip download of the respective manifests
* `player_client`: `web` (default) or `android` (force use the android client fallbacks for video extraction)
* `player_skip`: `configs` - skip requests if applicable for client configs and use defaults
* **funimation**
* `language`: Languages to extract. Eg: `funimation:language=english,japanese`
* `version`: The video version to extract - `uncut` or `simulcast`
NOTE: These options may be changed/removed in the future without concern for backward compatibility
# PLUGINS # PLUGINS
Plugins are loaded from `<root-dir>/ytdlp_plugins/<type>/__init__.py`. Currently only `extractor` plugins are supported. Support for `downloader` and `postprocessor` plugins may be added in the future. See [ytdlp_plugins](ytdlp_plugins) for example. Plugins are loaded from `<root-dir>/ytdlp_plugins/<type>/__init__.py`. Currently only `extractor` plugins are supported. Support for `downloader` and `postprocessor` plugins may be added in the future. See [ytdlp_plugins](ytdlp_plugins) for example.
@@ -1339,6 +1374,10 @@ While these options still work, their use is not recommended since there are oth
--list-formats-old --compat-options list-formats (Alias: --no-list-formats-as-table) --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) --list-formats-as-table --compat-options -list-formats [Default] (Alias: --no-list-formats-old)
--sponskrub-args ARGS --ppa "sponskrub:ARGS" --sponskrub-args ARGS --ppa "sponskrub:ARGS"
--youtube-skip-dash-manifest --extractor-args "youtube:skip=dash" (Alias: --no-youtube-include-dash-manifest)
--youtube-skip-hls-manifest --extractor-args "youtube:skip=hls" (Alias: --no-youtube-include-hls-manifest)
--youtube-include-dash-manifest Default (Alias: --no-youtube-skip-dash-manifest)
--youtube-include-hls-manifest Default (Alias: --no-youtube-skip-hls-manifest)
--test Used by developers for testing extractors. Not intended for the end user --test Used by developers for testing extractors. Not intended for the end user
--youtube-print-sig-code Used for testing youtube signatures --youtube-print-sig-code Used for testing youtube signatures
+1 -1
View File
@@ -1,4 +1,4 @@
#!/usr/bin/env python #!/usr/bin/env python3
from __future__ import unicode_literals from __future__ import unicode_literals
import os import os
+2
View File
@@ -1,3 +1,5 @@
# UNUSED
#!/usr/bin/python3 #!/usr/bin/python3
import argparse import argparse
+1 -1
View File
@@ -1,4 +1,4 @@
#!/usr/bin/env python #!/usr/bin/env python3
from __future__ import unicode_literals from __future__ import unicode_literals
""" """
+1 -1
View File
@@ -1,6 +1,6 @@
# Unused # Unused
#!/usr/bin/env python #!/usr/bin/env python3
from __future__ import unicode_literals from __future__ import unicode_literals
import io import io
+1 -1
View File
@@ -1,4 +1,4 @@
#!/usr/bin/env python #!/usr/bin/env python3
from __future__ import unicode_literals from __future__ import unicode_literals
import optparse import optparse
+1
View File
@@ -1,3 +1,4 @@
#!/usr/bin/env python3
from __future__ import unicode_literals from __future__ import unicode_literals
import codecs import codecs
@@ -1,4 +1,4 @@
#!/usr/bin/env python #!/usr/bin/env python3
# coding: utf-8 # coding: utf-8
from __future__ import with_statement, unicode_literals from __future__ import with_statement, unicode_literals
+1
View File
@@ -1,3 +1,4 @@
#!/usr/bin/env python3
# coding: utf-8 # coding: utf-8
from __future__ import unicode_literals from __future__ import unicode_literals
+1 -1
View File
@@ -1,4 +1,4 @@
#!/usr/bin/env python #!/usr/bin/env python3
from __future__ import unicode_literals from __future__ import unicode_literals
# import io # import io
+1 -1
View File
@@ -1,4 +1,4 @@
#!/usr/bin/env python #!/usr/bin/env python3
from __future__ import unicode_literals from __future__ import unicode_literals
import io import io
+1
View File
@@ -1,3 +1,4 @@
#!/usr/bin/env python3
from __future__ import unicode_literals, print_function from __future__ import unicode_literals, print_function
from inspect import getsource from inspect import getsource
+5
View File
@@ -1,3 +1,8 @@
#!/usr/bin/env python3
# yt-dlp --help | make_readme.py
# This must be run in a console of correct width
from __future__ import unicode_literals from __future__ import unicode_literals
import io import io
+1 -1
View File
@@ -1,4 +1,4 @@
#!/usr/bin/env python #!/usr/bin/env python3
from __future__ import unicode_literals from __future__ import unicode_literals
import io import io
+1
View File
@@ -1,3 +1,4 @@
#!/usr/bin/env python3
from __future__ import unicode_literals from __future__ import unicode_literals
import io import io
+1
View File
@@ -1,4 +1,5 @@
# Unused # Unused
#!/bin/bash #!/bin/bash
# IMPORTANT: the following assumptions are made # IMPORTANT: the following assumptions are made
+1 -1
View File
@@ -1,6 +1,6 @@
# Unused # Unused
#!/usr/bin/env python #!/usr/bin/env python3
from __future__ import unicode_literals from __future__ import unicode_literals
import itertools import itertools
+2
View File
@@ -1,4 +1,6 @@
#!/usr/bin/env python3
from __future__ import unicode_literals from __future__ import unicode_literals
from datetime import datetime from datetime import datetime
# import urllib.request # import urllib.request
+2
View File
@@ -1,3 +1,5 @@
# UNUSED
#!/bin/bash #!/bin/bash
# Run with as parameter a setup.py that works in the current directory # Run with as parameter a setup.py that works in the current directory
+1 -1
View File
@@ -1,4 +1,4 @@
#!/usr/bin/env python #!/usr/bin/env python3
from __future__ import unicode_literals from __future__ import unicode_literals
import os import os
+10 -8
View File
@@ -1,4 +1,4 @@
#!/usr/bin/env python #!/usr/bin/env python3
# coding: utf-8 # coding: utf-8
from __future__ import unicode_literals from __future__ import unicode_literals
@@ -6,6 +6,7 @@ import sys
# import os # import os
import platform import platform
from PyInstaller.utils.hooks import collect_submodules
from PyInstaller.utils.win32.versioninfo import ( from PyInstaller.utils.win32.versioninfo import (
VarStruct, VarFileInfo, StringStruct, StringTable, VarStruct, VarFileInfo, StringStruct, StringTable,
StringFileInfo, FixedFileInfo, VSVersionInfo, SetVersion, StringFileInfo, FixedFileInfo, VSVersionInfo, SetVersion,
@@ -58,22 +59,23 @@ VERSION_FILE = VSVersionInfo(
), ),
StringStruct('OriginalFilename', 'yt-dlp%s.exe' % _x86), StringStruct('OriginalFilename', 'yt-dlp%s.exe' % _x86),
StringStruct('ProductName', 'yt-dlp%s' % _x86), StringStruct('ProductName', 'yt-dlp%s' % _x86),
StringStruct('ProductVersion', '%s%s' % (VERSION, _x86)), StringStruct(
'ProductVersion',
'%s%s on Python %s' % (VERSION, _x86, platform.python_version())),
])]), ])]),
VarFileInfo([VarStruct('Translation', [0, 1200])]) VarFileInfo([VarStruct('Translation', [0, 1200])])
] ]
) )
dependancies = ['Crypto', 'mutagen'] + collect_submodules('websockets')
excluded_modules = ['test', 'ytdlp_plugins', 'youtube-dl', 'youtube-dlc']
PyInstaller.__main__.run([ PyInstaller.__main__.run([
'--name=yt-dlp%s' % _x86, '--name=yt-dlp%s' % _x86,
'--onefile', '--onefile',
'--icon=devscripts/cloud.ico', '--icon=devscripts/cloud.ico',
'--exclude-module=youtube_dl', *[f'--exclude-module={module}' for module in excluded_modules],
'--exclude-module=youtube_dlc', *[f'--hidden-import={module}' for module in dependancies],
'--exclude-module=test',
'--exclude-module=ytdlp_plugins',
'--hidden-import=mutagen',
'--hidden-import=Crypto',
'--upx-exclude=vcruntime140.dll', '--upx-exclude=vcruntime140.dll',
'yt_dlp/__main__.py', 'yt_dlp/__main__.py',
]) ])
+1
View File
@@ -1,2 +1,3 @@
mutagen mutagen
pycryptodome pycryptodome
websockets
+3 -13
View File
@@ -1,4 +1,4 @@
#!/usr/bin/env python #!/usr/bin/env python3
# coding: utf-8 # coding: utf-8
from setuptools import setup, Command, find_packages from setuptools import setup, Command, find_packages
@@ -19,7 +19,7 @@ LONG_DESCRIPTION = '\n\n'.join((
'**PS**: Some links in this document will not work since this is a copy of the README.md from Github', '**PS**: Some 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', 'websockets']
if sys.argv[1:2] == ['py2exe']: if sys.argv[1:2] == ['py2exe']:
raise NotImplementedError('py2exe is not currently supported; instead, use "pyinst.py" to build with pyinstaller') raise NotImplementedError('py2exe is not currently supported; instead, use "pyinst.py" to build with pyinstaller')
@@ -88,26 +88,16 @@ setup(
'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.6',
'Programming Language :: Python :: 2.7',
'Programming Language :: Python :: 3',
'Programming Language :: Python :: 3.2',
'Programming Language :: Python :: 3.3',
'Programming Language :: Python :: 3.4',
'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 :: 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='>=3.6',
cmdclass={'build_lazy_extractors': build_lazy_extractors}, cmdclass={'build_lazy_extractors': build_lazy_extractors},
**params **params
+5 -4
View File
@@ -225,8 +225,7 @@
- **Culturebox** - **Culturebox**
- **CultureUnplugged** - **CultureUnplugged**
- **curiositystream** - **curiositystream**
- **curiositystream:collections** - **curiositystream:collection**
- **curiositystream:series**
- **CWTV** - **CWTV**
- **DagelijkseKost**: dagelijksekost.een.be - **DagelijkseKost**: dagelijksekost.een.be
- **DailyMail** - **DailyMail**
@@ -497,8 +496,6 @@
- **LinuxAcademy** - **LinuxAcademy**
- **LiTV** - **LiTV**
- **LiveJournal** - **LiveJournal**
- **LiveLeak**
- **LiveLeakEmbed**
- **livestream** - **livestream**
- **livestream:original** - **livestream:original**
- **LnkGo** - **LnkGo**
@@ -1069,6 +1066,8 @@
- **TVPlayHome** - **TVPlayHome**
- **Tweakers** - **Tweakers**
- **TwitCasting** - **TwitCasting**
- **TwitCastingLive**
- **TwitCastingUser**
- **twitch:clips** - **twitch:clips**
- **twitch:stream** - **twitch:stream**
- **twitch:vod** - **twitch:vod**
@@ -1130,6 +1129,8 @@
- **videomore:video** - **videomore:video**
- **VideoPress** - **VideoPress**
- **Vidio** - **Vidio**
- **VidioLive**
- **VidioPremier**
- **VidLii** - **VidLii**
- **vidme** - **vidme**
- **vidme:user** - **vidme:user**
+1 -1
View File
@@ -1,4 +1,4 @@
#!/usr/bin/env python #!/usr/bin/env python3
from __future__ import unicode_literals from __future__ import unicode_literals
+127 -53
View File
@@ -1,4 +1,4 @@
#!/usr/bin/env python #!/usr/bin/env python3
# coding: utf-8 # coding: utf-8
from __future__ import unicode_literals from __future__ import unicode_literals
@@ -17,7 +17,7 @@ from yt_dlp.compat import compat_str, compat_urllib_error
from yt_dlp.extractor import YoutubeIE from yt_dlp.extractor import YoutubeIE
from yt_dlp.extractor.common import InfoExtractor from yt_dlp.extractor.common import InfoExtractor
from yt_dlp.postprocessor.common import PostProcessor from yt_dlp.postprocessor.common import PostProcessor
from yt_dlp.utils import ExtractorError, match_filter_func from yt_dlp.utils import ExtractorError, int_or_none, match_filter_func
TEST_URL = 'http://localhost/sample.mp4' TEST_URL = 'http://localhost/sample.mp4'
@@ -461,14 +461,13 @@ class TestFormatSelection(unittest.TestCase):
def test_invalid_format_specs(self): def test_invalid_format_specs(self):
def assert_syntax_error(format_spec): def assert_syntax_error(format_spec):
ydl = YDL({'format': format_spec}) self.assertRaises(SyntaxError, YDL, {'format': format_spec})
info_dict = _make_result([{'format_id': 'foo', 'url': TEST_URL}])
self.assertRaises(SyntaxError, ydl.process_ie_result, info_dict)
assert_syntax_error('bestvideo,,best') assert_syntax_error('bestvideo,,best')
assert_syntax_error('+bestaudio') assert_syntax_error('+bestaudio')
assert_syntax_error('bestvideo+') assert_syntax_error('bestvideo+')
assert_syntax_error('/') assert_syntax_error('/')
assert_syntax_error('[720<height]')
def test_format_filtering(self): def test_format_filtering(self):
formats = [ formats = [
@@ -648,56 +647,131 @@ class TestYoutubeDL(unittest.TestCase):
self.assertEqual(test_dict['extractor'], 'Foo') self.assertEqual(test_dict['extractor'], 'Foo')
self.assertEqual(test_dict['playlist'], 'funny videos') self.assertEqual(test_dict['playlist'], 'funny videos')
def test_prepare_filename(self): outtmpl_info = {
info = { 'id': '1234',
'id': '1234', 'ext': 'mp4',
'ext': 'mp4', 'width': None,
'width': None, 'height': 1080,
'height': 1080, 'title1': '$PATH',
'title1': '$PATH', 'title2': '%PATH%',
'title2': '%PATH%', 'title3': 'foo/bar\\test',
'timestamp': 1618488000, 'timestamp': 1618488000,
'formats': [{'id': 'id1'}, {'id': 'id2'}] 'duration': 100000,
} 'playlist_index': 1,
'_last_playlist_index': 100,
'n_entries': 10,
'formats': [{'id': 'id1'}, {'id': 'id2'}, {'id': 'id3'}]
}
def fname(templ, na_placeholder='NA'): def test_prepare_outtmpl_and_filename(self):
params = {'outtmpl': templ} def test(tmpl, expected, *, info=None, **params):
if na_placeholder != 'NA': params['outtmpl'] = tmpl
params['outtmpl_na_placeholder'] = na_placeholder
ydl = YoutubeDL(params) ydl = YoutubeDL(params)
return ydl.prepare_filename(info) ydl._num_downloads = 1
self.assertEqual(fname('%(id)s.%(ext)s'), '1234.mp4') self.assertEqual(ydl.validate_outtmpl(tmpl), None)
self.assertEqual(fname('%(id)s-%(width)s.%(ext)s'), '1234-NA.mp4')
NA_TEST_OUTTMPL = '%(uploader_date)s-%(width)d-%(id)s.%(ext)s' outtmpl, tmpl_dict = ydl.prepare_outtmpl(tmpl, info or self.outtmpl_info)
# Replace missing fields with 'NA' by default out = outtmpl % tmpl_dict
self.assertEqual(fname(NA_TEST_OUTTMPL), 'NA-NA-1234.mp4') fname = ydl.prepare_filename(info or self.outtmpl_info)
# Or by provided placeholder
self.assertEqual(fname(NA_TEST_OUTTMPL, na_placeholder='none'), 'none-none-1234.mp4') if callable(expected):
self.assertEqual(fname(NA_TEST_OUTTMPL, na_placeholder=''), '--1234.mp4') self.assertTrue(expected(out))
self.assertEqual(fname('%(height)s.%(ext)s'), '1080.mp4') self.assertTrue(expected(fname))
self.assertEqual(fname('%(height)d.%(ext)s'), '1080.mp4') elif isinstance(expected, compat_str):
self.assertEqual(fname('%(height)6d.%(ext)s'), ' 1080.mp4') self.assertEqual((out, fname), (expected, expected))
self.assertEqual(fname('%(height)-6d.%(ext)s'), '1080 .mp4') else:
self.assertEqual(fname('%(height)06d.%(ext)s'), '001080.mp4') self.assertEqual((out, fname), expected)
self.assertEqual(fname('%(height) 06d.%(ext)s'), ' 01080.mp4')
self.assertEqual(fname('%(height) 06d.%(ext)s'), ' 01080.mp4') # Auto-generated fields
self.assertEqual(fname('%(height)0 6d.%(ext)s'), ' 01080.mp4') test('%(id)s.%(ext)s', '1234.mp4')
self.assertEqual(fname('%(height)0 6d.%(ext)s'), ' 01080.mp4') test('%(duration_string)s', ('27:46:40', '27-46-40'))
self.assertEqual(fname('%(height) 0 6d.%(ext)s'), ' 01080.mp4') test('%(epoch)d', int_or_none)
self.assertEqual(fname('%%'), '%') test('%(resolution)s', '1080p')
self.assertEqual(fname('%%%%'), '%%') test('%(playlist_index)s', '001')
self.assertEqual(fname('%%(height)06d.%(ext)s'), '%(height)06d.mp4') test('%(autonumber)s', '00001')
self.assertEqual(fname('%(width)06d.%(ext)s'), 'NA.mp4') test('%(autonumber+2)03d', '005', autonumber_start=3)
self.assertEqual(fname('%(width)06d.%%(ext)s'), 'NA.%(ext)s') test('%(autonumber)s', '001', autonumber_size=3)
self.assertEqual(fname('%%(width)06d.%(ext)s'), '%(width)06d.mp4')
self.assertEqual(fname('Hello %(title1)s'), 'Hello $PATH') # Escaping %
self.assertEqual(fname('Hello %(title2)s'), 'Hello %PATH%') test('%%', '%')
self.assertEqual(fname('%(timestamp+-1000>%H-%M-%S)s'), '11-43-20') test('%%%%', '%%')
self.assertEqual(fname('%(id+1)05d'), '01235') test('%%(width)06d.%(ext)s', '%(width)06d.mp4')
self.assertEqual(fname('%(width+100)05d'), 'NA') test('%(width)06d.%(ext)s', 'NA.mp4')
self.assertEqual(fname('%(formats.0)s').replace("u", ""), "{'id' - 'id1'}") test('%(width)06d.%%(ext)s', 'NA.%(ext)s')
self.assertEqual(fname('%(formats.-1.id)s'), 'id2') test('%%(width)06d.%(ext)s', '%(width)06d.mp4')
self.assertEqual(fname('%(formats.2)s'), 'NA')
# ID sanitization
test('%(id)s', '_abcd', info={'id': '_abcd'})
test('%(some_id)s', '_abcd', info={'some_id': '_abcd'})
test('%(formats.0.id)s', '_abcd', info={'formats': [{'id': '_abcd'}]})
test('%(id)s', '-abcd', info={'id': '-abcd'})
test('%(id)s', '.abcd', info={'id': '.abcd'})
test('%(id)s', 'ab__cd', info={'id': 'ab__cd'})
test('%(id)s', ('ab:cd', 'ab -cd'), info={'id': 'ab:cd'})
# Invalid templates
self.assertTrue(isinstance(YoutubeDL.validate_outtmpl('%'), ValueError))
self.assertTrue(isinstance(YoutubeDL.validate_outtmpl('%(title)'), ValueError))
test('%(invalid@tmpl|def)s', 'none', outtmpl_na_placeholder='none')
test('%()s', 'NA')
test('%s', '%s')
test('%d', '%d')
# NA placeholder
NA_TEST_OUTTMPL = '%(uploader_date)s-%(width)d-%(x|def)s-%(id)s.%(ext)s'
test(NA_TEST_OUTTMPL, 'NA-NA-def-1234.mp4')
test(NA_TEST_OUTTMPL, 'none-none-def-1234.mp4', outtmpl_na_placeholder='none')
test(NA_TEST_OUTTMPL, '--def-1234.mp4', outtmpl_na_placeholder='')
# String formatting
FMT_TEST_OUTTMPL = '%%(height)%s.%%(ext)s'
test(FMT_TEST_OUTTMPL % 's', '1080.mp4')
test(FMT_TEST_OUTTMPL % 'd', '1080.mp4')
test(FMT_TEST_OUTTMPL % '6d', ' 1080.mp4')
test(FMT_TEST_OUTTMPL % '-6d', '1080 .mp4')
test(FMT_TEST_OUTTMPL % '06d', '001080.mp4')
test(FMT_TEST_OUTTMPL % ' 06d', ' 01080.mp4')
test(FMT_TEST_OUTTMPL % ' 06d', ' 01080.mp4')
test(FMT_TEST_OUTTMPL % '0 6d', ' 01080.mp4')
test(FMT_TEST_OUTTMPL % '0 6d', ' 01080.mp4')
test(FMT_TEST_OUTTMPL % ' 0 6d', ' 01080.mp4')
# Type casting
test('%(id)d', '1234')
test('%(height)c', '1')
test('%(ext)c', 'm')
test('%(id)d %(id)r', "1234 '1234'")
test('%(id)r %(height)r', "'1234' 1080")
test('%(ext)s-%(ext|def)d', 'mp4-def')
test('%(width|0)04d', '0000')
test('a%(width|)d', 'a', outtmpl_na_placeholder='none')
# Internal formatting
FORMATS = self.outtmpl_info['formats']
test('%(timestamp-1000>%H-%M-%S)s', '11-43-20')
test('%(id+1-height+3)05d', '00158')
test('%(width+100)05d', 'NA')
test('%(formats.0) 15s', ('% 15s' % FORMATS[0], '% 15s' % str(FORMATS[0]).replace(':', ' -')))
test('%(formats.0)r', (repr(FORMATS[0]), repr(FORMATS[0]).replace(':', ' -')))
test('%(height.0)03d', '001')
test('%(-height.0)04d', '-001')
test('%(formats.-1.id)s', FORMATS[-1]['id'])
test('%(formats.0.id.-1)d', FORMATS[0]['id'][-1])
test('%(formats.3)s', 'NA')
test('%(formats.:2:-1)r', repr(FORMATS[:2:-1]))
test('%(formats.0.id.-1+id)f', '1235.000000')
test('%(formats.0.id.-1+formats.1.id.-1)d', '3')
# Empty filename
test('%(foo|)s-%(bar|)s.%(ext)s', '-.mp4')
# test('%(foo|)s.%(ext)s', ('.mp4', '_.mp4')) # fixme
# test('%(foo|)s', ('', '_')) # fixme
# Path expansion and escaping
test('Hello %(title1)s', 'Hello $PATH')
test('Hello %(title2)s', 'Hello %PATH%')
test('%(title3)s', ('foo/bar\\test', 'foo_bar_test'))
test('folder/%(title3)s', ('folder/foo/bar\\test', 'folder%sfoo_bar_test' % os.path.sep))
def test_format_note(self): def test_format_note(self):
ydl = YoutubeDL() ydl = YoutubeDL()
+1 -1
View File
@@ -1,4 +1,4 @@
#!/usr/bin/env python #!/usr/bin/env python3
# coding: utf-8 # coding: utf-8
from __future__ import unicode_literals from __future__ import unicode_literals
+1 -2
View File
@@ -1,5 +1,4 @@
#!/usr/bin/env python #!/usr/bin/env python3
from __future__ import unicode_literals from __future__ import unicode_literals
# Allow direct execution # Allow direct execution
+1 -1
View File
@@ -1,4 +1,4 @@
#!/usr/bin/env python #!/usr/bin/env python3
from __future__ import unicode_literals from __future__ import unicode_literals
# Allow direct execution # Allow direct execution
+1 -1
View File
@@ -1,4 +1,4 @@
#!/usr/bin/env python #!/usr/bin/env python3
from __future__ import unicode_literals from __future__ import unicode_literals
+1 -1
View File
@@ -1,4 +1,4 @@
#!/usr/bin/env python #!/usr/bin/env python3
# coding: utf-8 # coding: utf-8
from __future__ import unicode_literals from __future__ import unicode_literals
+1 -1
View File
@@ -1,4 +1,4 @@
#!/usr/bin/env python #!/usr/bin/env python3
# coding: utf-8 # coding: utf-8
from __future__ import unicode_literals from __future__ import unicode_literals
+1 -1
View File
@@ -1,4 +1,4 @@
#!/usr/bin/env python #!/usr/bin/env python3
from __future__ import unicode_literals from __future__ import unicode_literals
+1 -1
View File
@@ -1,4 +1,4 @@
#!/usr/bin/env python #!/usr/bin/env python3
# coding: utf-8 # coding: utf-8
from __future__ import unicode_literals from __future__ import unicode_literals
+1 -1
View File
@@ -1,4 +1,4 @@
#!/usr/bin/env python #!/usr/bin/env python3
# coding: utf-8 # coding: utf-8
from __future__ import unicode_literals from __future__ import unicode_literals
+1 -1
View File
@@ -1,4 +1,4 @@
#!/usr/bin/env python #!/usr/bin/env python3
# coding: utf-8 # coding: utf-8
from __future__ import unicode_literals from __future__ import unicode_literals
+1 -1
View File
@@ -1,4 +1,4 @@
#!/usr/bin/env python #!/usr/bin/env python3
from __future__ import unicode_literals from __future__ import unicode_literals
+1 -1
View File
@@ -1,4 +1,4 @@
#!/usr/bin/env python #!/usr/bin/env python3
from __future__ import unicode_literals from __future__ import unicode_literals
+1 -1
View File
@@ -1,4 +1,4 @@
#!/usr/bin/env python #!/usr/bin/env python3
from __future__ import unicode_literals from __future__ import unicode_literals
import os import os
+1 -1
View File
@@ -1,4 +1,4 @@
#!/usr/bin/env python #!/usr/bin/env python3
from __future__ import unicode_literals from __future__ import unicode_literals
+15 -1
View File
@@ -1,4 +1,4 @@
#!/usr/bin/env python #!/usr/bin/env python3
from __future__ import unicode_literals from __future__ import unicode_literals
@@ -8,7 +8,10 @@ import sys
import unittest import unittest
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from yt_dlp import YoutubeDL
from yt_dlp.compat import compat_shlex_quote
from yt_dlp.postprocessor import ( from yt_dlp.postprocessor import (
ExecAfterDownloadPP,
FFmpegThumbnailsConvertorPP, FFmpegThumbnailsConvertorPP,
MetadataFromFieldPP, MetadataFromFieldPP,
MetadataFromTitlePP, MetadataFromTitlePP,
@@ -55,3 +58,14 @@ class TestConvertThumbnail(unittest.TestCase):
for _, out in tests: for _, out in tests:
os.remove(file.format(out)) os.remove(file.format(out))
class TestExecAfterDownload(unittest.TestCase):
def test_parse_cmd(self):
pp = ExecAfterDownloadPP(YoutubeDL(), '')
info = {'filepath': 'file name'}
quoted_filepath = compat_shlex_quote(info['filepath'])
self.assertEqual(pp.parse_cmd('echo', info), 'echo %s' % quoted_filepath)
self.assertEqual(pp.parse_cmd('echo.{}', info), 'echo.%s' % quoted_filepath)
self.assertEqual(pp.parse_cmd('echo "%(filepath)s"', info), 'echo "%s"' % info['filepath'])
+1 -1
View File
@@ -1,4 +1,4 @@
#!/usr/bin/env python #!/usr/bin/env python3
# coding: utf-8 # coding: utf-8
from __future__ import unicode_literals from __future__ import unicode_literals
+1 -1
View File
@@ -1,4 +1,4 @@
#!/usr/bin/env python #!/usr/bin/env python3
from __future__ import unicode_literals from __future__ import unicode_literals
# Allow direct execution # Allow direct execution
@@ -1,4 +1,4 @@
#!/usr/bin/env python #!/usr/bin/env python3
from __future__ import unicode_literals from __future__ import unicode_literals
# Allow direct execution # Allow direct execution
+1 -1
View File
@@ -1,4 +1,4 @@
#!/usr/bin/env python #!/usr/bin/env python3
from __future__ import unicode_literals from __future__ import unicode_literals
+45 -1
View File
@@ -1,4 +1,4 @@
#!/usr/bin/env python #!/usr/bin/env python3
# coding: utf-8 # coding: utf-8
from __future__ import unicode_literals from __future__ import unicode_literals
@@ -12,6 +12,7 @@ sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
# Various small unit tests # Various small unit tests
import io import io
import itertools
import json import json
import xml.etree.ElementTree import xml.etree.ElementTree
@@ -108,6 +109,7 @@ from yt_dlp.utils import (
cli_bool_option, cli_bool_option,
parse_codecs, parse_codecs,
iri_to_uri, iri_to_uri,
LazyList,
) )
from yt_dlp.compat import ( from yt_dlp.compat import (
compat_chr, compat_chr,
@@ -126,6 +128,7 @@ class TestUtil(unittest.TestCase):
self.assertTrue(timeconvert('bougrg') is None) self.assertTrue(timeconvert('bougrg') is None)
def test_sanitize_filename(self): def test_sanitize_filename(self):
self.assertEqual(sanitize_filename(''), '')
self.assertEqual(sanitize_filename('abc'), 'abc') self.assertEqual(sanitize_filename('abc'), 'abc')
self.assertEqual(sanitize_filename('abc_d-e'), 'abc_d-e') self.assertEqual(sanitize_filename('abc_d-e'), 'abc_d-e')
@@ -1524,6 +1527,47 @@ Line 1
self.assertEqual(clean_podcast_url('https://www.podtrac.com/pts/redirect.mp3/chtbl.com/track/5899E/traffic.megaphone.fm/HSW7835899191.mp3'), 'https://traffic.megaphone.fm/HSW7835899191.mp3') self.assertEqual(clean_podcast_url('https://www.podtrac.com/pts/redirect.mp3/chtbl.com/track/5899E/traffic.megaphone.fm/HSW7835899191.mp3'), 'https://traffic.megaphone.fm/HSW7835899191.mp3')
self.assertEqual(clean_podcast_url('https://play.podtrac.com/npr-344098539/edge1.pod.npr.org/anon.npr-podcasts/podcast/npr/waitwait/2020/10/20201003_waitwait_wwdtmpodcast201003-015621a5-f035-4eca-a9a1-7c118d90bc3c.mp3'), 'https://edge1.pod.npr.org/anon.npr-podcasts/podcast/npr/waitwait/2020/10/20201003_waitwait_wwdtmpodcast201003-015621a5-f035-4eca-a9a1-7c118d90bc3c.mp3') self.assertEqual(clean_podcast_url('https://play.podtrac.com/npr-344098539/edge1.pod.npr.org/anon.npr-podcasts/podcast/npr/waitwait/2020/10/20201003_waitwait_wwdtmpodcast201003-015621a5-f035-4eca-a9a1-7c118d90bc3c.mp3'), 'https://edge1.pod.npr.org/anon.npr-podcasts/podcast/npr/waitwait/2020/10/20201003_waitwait_wwdtmpodcast201003-015621a5-f035-4eca-a9a1-7c118d90bc3c.mp3')
def test_LazyList(self):
it = list(range(10))
self.assertEqual(list(LazyList(it)), it)
self.assertEqual(LazyList(it).exhaust(), it)
self.assertEqual(LazyList(it)[5], it[5])
self.assertEqual(LazyList(it)[::2], it[::2])
self.assertEqual(LazyList(it)[1::2], it[1::2])
self.assertEqual(LazyList(it)[6:2:-2], it[6:2:-2])
self.assertEqual(LazyList(it)[::-1], it[::-1])
self.assertTrue(LazyList(it))
self.assertFalse(LazyList(range(0)))
self.assertEqual(len(LazyList(it)), len(it))
self.assertEqual(repr(LazyList(it)), repr(it))
self.assertEqual(str(LazyList(it)), str(it))
self.assertEqual(list(LazyList(it).reverse()), it[::-1])
self.assertEqual(list(LazyList(it).reverse()[1:3:7]), it[::-1][1:3:7])
def test_LazyList_laziness(self):
def test(ll, idx, val, cache):
self.assertEqual(ll[idx], val)
self.assertEqual(getattr(ll, '_LazyList__cache'), list(cache))
ll = LazyList(range(10))
test(ll, 0, 0, range(1))
test(ll, 5, 5, range(6))
test(ll, -3, 7, range(10))
ll = LazyList(range(10)).reverse()
test(ll, -1, 0, range(1))
test(ll, 3, 6, range(10))
ll = LazyList(itertools.count())
test(ll, 10, 10, range(11))
ll.reverse()
test(ll, -15, 14, range(15))
if __name__ == '__main__': if __name__ == '__main__':
unittest.main() unittest.main()
+1 -1
View File
@@ -1,4 +1,4 @@
#!/usr/bin/env python #!/usr/bin/env python3
# coding: utf-8 # coding: utf-8
from __future__ import unicode_literals from __future__ import unicode_literals
+1 -1
View File
@@ -1,4 +1,4 @@
#!/usr/bin/env python #!/usr/bin/env python3
# coding: utf-8 # coding: utf-8
from __future__ import unicode_literals from __future__ import unicode_literals
+1 -1
View File
@@ -1,4 +1,4 @@
#!/usr/bin/env python #!/usr/bin/env python3
from __future__ import unicode_literals from __future__ import unicode_literals
# Allow direct execution # Allow direct execution
+1 -1
View File
@@ -1,4 +1,4 @@
#!/usr/bin/env python #!/usr/bin/env python3
from __future__ import unicode_literals from __future__ import unicode_literals
# Allow direct execution # Allow direct execution
+1 -1
View File
@@ -1,4 +1,4 @@
#!/usr/bin/env python #!/usr/bin/env python3
from __future__ import unicode_literals from __future__ import unicode_literals
+325 -300
View File
@@ -1,4 +1,4 @@
#!/usr/bin/env python #!/usr/bin/env python3
# coding: utf-8 # coding: utf-8
from __future__ import absolute_import, unicode_literals from __future__ import absolute_import, unicode_literals
@@ -20,6 +20,7 @@ import re
import shutil import shutil
import subprocess import subprocess
import sys import sys
import tempfile
import time import time
import tokenize import tokenize
import traceback import traceback
@@ -64,9 +65,10 @@ from .utils import (
float_or_none, float_or_none,
format_bytes, format_bytes,
format_field, format_field,
FORMAT_RE, STR_FORMAT_RE,
formatSeconds, formatSeconds,
GeoRestrictedError, GeoRestrictedError,
HEADRequest,
int_or_none, int_or_none,
iri_to_uri, iri_to_uri,
ISO3166Utils, ISO3166Utils,
@@ -86,7 +88,6 @@ from .utils import (
preferredencoding, preferredencoding,
prepend_extension, prepend_extension,
process_communicate_or_kill, process_communicate_or_kill,
random_uuidv4,
register_socks_protocols, register_socks_protocols,
RejectedVideoReached, RejectedVideoReached,
render_table, render_table,
@@ -100,8 +101,10 @@ from .utils import (
str_or_none, str_or_none,
strftime_or_none, strftime_or_none,
subtitles_filename, subtitles_filename,
ThrottledDownload,
to_high_limit_path, to_high_limit_path,
traverse_dict, traverse_obj,
try_get,
UnavailableVideoError, UnavailableVideoError,
url_basename, url_basename,
version_tuple, version_tuple,
@@ -126,13 +129,14 @@ from .downloader import (
) )
from .downloader.rtmp import rtmpdump_version from .downloader.rtmp import rtmpdump_version
from .postprocessor import ( from .postprocessor import (
get_postprocessor,
FFmpegFixupDurationPP,
FFmpegFixupM3u8PP, FFmpegFixupM3u8PP,
FFmpegFixupM4aPP, FFmpegFixupM4aPP,
FFmpegFixupStretchedPP, FFmpegFixupStretchedPP,
FFmpegFixupTimestampPP,
FFmpegMergerPP, FFmpegMergerPP,
FFmpegPostProcessor, FFmpegPostProcessor,
# FFmpegSubtitlesConvertorPP,
get_postprocessor,
MoveFilesAfterDownloadPP, MoveFilesAfterDownloadPP,
) )
from .version import __version__ from .version import __version__
@@ -388,17 +392,15 @@ class YoutubeDL(object):
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". compat_opts: Compatibility options. See "Differences in default behavior".
Note that only format-sort, format-spec, no-live-chat, The following options do not work when used through the API:
no-attach-info-json, playlist-index, list-formats, filename, abort-on-error, multistreams, no-live-chat,
no-direct-merge, no-youtube-channel-redirect, no-playlist-metafiles. Refer __init__.py for their implementation
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):
nopart, updatetime, buffersize, ratelimit, min_filesize, max_filesize, test, nopart, updatetime, buffersize, ratelimit, throttledratelimit, min_filesize,
noresizebuffer, retries, continuedl, noprogress, consoletitle, max_filesize, test, noresizebuffer, retries, continuedl, noprogress, consoletitle,
xattr_set_filesize, external_downloader_args, hls_use_mpegts, xattr_set_filesize, external_downloader_args, hls_use_mpegts, http_chunk_size.
http_chunk_size.
The following options are used by the post processors: The following options are used by the post processors:
prefer_ffmpeg: If False, use avconv instead of ffmpeg if both are available, prefer_ffmpeg: If False, use avconv instead of ffmpeg if both are available,
@@ -416,11 +418,16 @@ class YoutubeDL(object):
dynamic_mpd: Whether to process dynamic DASH manifests (default: True) dynamic_mpd: Whether to process dynamic DASH manifests (default: True)
hls_split_discontinuity: Split HLS playlists to different formats at hls_split_discontinuity: Split HLS playlists to different formats at
discontinuities such as ad breaks (default: False) discontinuities such as ad breaks (default: False)
youtube_include_dash_manifest: If True (default), DASH manifests and related extractor_args: A dictionary of arguments to be passed to the extractors.
See "EXTRACTOR ARGUMENTS" for details.
Eg: {'youtube': {'skip': ['dash', 'hls']}}
youtube_include_dash_manifest: Deprecated - Use extractor_args instead.
If True (default), DASH manifests and related
data will be downloaded and processed by extractor. data will be downloaded and processed by extractor.
You can reduce network I/O by disabling it if you don't You can reduce network I/O by disabling it if you don't
care about DASH. (only for youtube) care about DASH. (only for youtube)
youtube_include_hls_manifest: If True (default), HLS manifests and related youtube_include_hls_manifest: Deprecated - Use extractor_args instead.
If True (default), HLS manifests and related
data will be downloaded and processed by extractor. data will be downloaded and processed by extractor.
You can reduce network I/O by disabling it if you don't You can reduce network I/O by disabling it if you don't
care about HLS. (only for youtube) care about HLS. (only for youtube)
@@ -472,8 +479,7 @@ class YoutubeDL(object):
if sys.version_info < (3, 6): if sys.version_info < (3, 6):
self.report_warning( self.report_warning(
'Support for Python version %d.%d have been deprecated and will break in future versions of yt-dlp! ' 'Python version %d.%d is not supported! Please update to Python 3.6 or above' % sys.version_info[:2])
'Update to Python 3.6 or above' % sys.version_info[:2])
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:
@@ -539,6 +545,11 @@ class YoutubeDL(object):
self.outtmpl_dict = self.parse_outtmpl() self.outtmpl_dict = self.parse_outtmpl()
# Creating format selector here allows us to catch syntax errors before the extraction
self.format_selector = (
None if self.params.get('format') is None
else self.build_format_selector(self.params['format']))
self._setup_opener() self._setup_opener()
"""Preload the archive, if any is specified""" """Preload the archive, if any is specified"""
@@ -564,14 +575,9 @@ class YoutubeDL(object):
self.add_default_info_extractors() self.add_default_info_extractors()
for pp_def_raw in self.params.get('postprocessors', []): for pp_def_raw in self.params.get('postprocessors', []):
pp_class = get_postprocessor(pp_def_raw['key'])
pp_def = dict(pp_def_raw) pp_def = dict(pp_def_raw)
del pp_def['key'] when = pp_def.pop('when', 'post_process')
if 'when' in pp_def: pp_class = get_postprocessor(pp_def.pop('key'))
when = pp_def['when']
del pp_def['when']
else:
when = 'post_process'
pp = pp_class(self, **compat_kwargs(pp_def)) pp = pp_class(self, **compat_kwargs(pp_def))
self.add_post_processor(pp, when=when) self.add_post_processor(pp, when=when)
@@ -813,134 +819,154 @@ class YoutubeDL(object):
'Put from __future__ import unicode_literals at the top of your code file or consider switching to Python 3.x.') 'Put from __future__ import unicode_literals at the top of your code file or consider switching to Python 3.x.')
return outtmpl_dict return outtmpl_dict
def get_output_path(self, dir_type='', filename=None):
paths = self.params.get('paths', {})
assert isinstance(paths, dict)
path = os.path.join(
expand_path(paths.get('home', '').strip()),
expand_path(paths.get(dir_type, '').strip()) if dir_type else '',
filename or '')
# Temporary fix for #4787
# 'Treat' all problem characters by passing filename through preferredencoding
# to workaround encoding issues with subprocess on python2 @ Windows
if sys.version_info < (3, 0) and sys.platform == 'win32':
path = encodeFilename(path, True).decode(preferredencoding())
return sanitize_path(path, force=self.params.get('windowsfilenames'))
@staticmethod
def validate_outtmpl(tmpl):
''' @return None or Exception object '''
try:
re.sub(
STR_FORMAT_RE.format(''),
lambda mobj: ('%' if not mobj.group('has_key') else '') + mobj.group(0),
tmpl
) % collections.defaultdict(int)
return None
except ValueError as err:
return err
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) info_dict = dict(info_dict)
na = self.params.get('outtmpl_na_placeholder', 'NA') na = self.params.get('outtmpl_na_placeholder', 'NA')
# duration_string info_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
formatSeconds(info_dict['duration'], '-' if sanitize else ':') formatSeconds(info_dict['duration'], '-' if sanitize else ':')
if info_dict.get('duration', None) is not None if info_dict.get('duration', None) is not None
else None) else None)
info_dict['epoch'] = int(time.time())
# epoch info_dict['autonumber'] = self.params.get('autonumber_start', 1) - 1 + self._num_downloads
template_dict['epoch'] = int(time.time()) if info_dict.get('resolution') is None:
info_dict['resolution'] = self.format_resolution(info_dict, default=None)
# autonumber
autonumber_size = self.params.get('autonumber_size')
if autonumber_size is None:
autonumber_size = 5
template_dict['autonumber'] = self.params.get('autonumber_start', 1) - 1 + self._num_downloads
# resolution if not defined
if template_dict.get('resolution') is None:
if template_dict.get('width') and template_dict.get('height'):
template_dict['resolution'] = '%dx%d' % (template_dict['width'], template_dict['height'])
elif template_dict.get('height'):
template_dict['resolution'] = '%sp' % template_dict['height']
elif template_dict.get('width'):
template_dict['resolution'] = '%dx?' % template_dict['width']
# 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.get('_last_playlist_index') or '')), 'playlist_index': len(str(info_dict.get('_last_playlist_index') or '')),
'autonumber': autonumber_size, 'autonumber': self.params.get('autonumber_size') or 5,
} }
FIELD_SIZE_COMPAT_RE = r'(?<!%)%\((?P<field>autonumber|playlist_index)\)s'
mobj = re.search(FIELD_SIZE_COMPAT_RE, outtmpl)
if mobj:
outtmpl = re.sub(
FIELD_SIZE_COMPAT_RE,
r'%%(\1)0%dd' % field_size_compat_map[mobj.group('field')],
outtmpl)
numeric_fields = list(self._NUMERIC_FIELDS) TMPL_DICT = {}
if sanitize is None: EXTERNAL_FORMAT_RE = re.compile(STR_FORMAT_RE.format('[^)]*'))
sanitize = lambda k, v: v
EXTERNAL_FORMAT_RE = FORMAT_RE.format('(?P<key>[^)]*)')
# Field is of the form key1.key2...
# where keys (except first) can be string, int or slice
FIELD_RE = r'\w+(?:\.(?:\w+|[-\d]*(?::[-\d]*){0,2}))*'
INTERNAL_FORMAT_RE = re.compile(r'''(?x)
(?P<negate>-)?
(?P<fields>{0})
(?P<maths>(?:[-+]-?(?:\d+(?:\.\d+)?|{0}))*)
(?:>(?P<strf_format>.+?))?
(?:\|(?P<default>.*?))?
$'''.format(FIELD_RE))
MATH_OPERATORS_RE = re.compile(r'(?<![-+])([-+])')
MATH_FUNCTIONS = { MATH_FUNCTIONS = {
'+': float.__add__, '+': float.__add__,
'-': float.__sub__, '-': float.__sub__,
} }
for outer_mobj in re.finditer(EXTERNAL_FORMAT_RE, outtmpl): # Field is of the form key1.key2...
final_key = outer_mobj.group('key') # where keys (except first) can be string, int or slice
str_type = outer_mobj.group('type') FIELD_RE = r'\w+(?:\.(?:\w+|{num}|{num}?(?::{num}?){{1,2}}))*'.format(num=r'(?:-?\d+)')
value = None MATH_FIELD_RE = r'''{field}|{num}'''.format(field=FIELD_RE, num=r'-?\d+(?:.\d+)?')
mobj = re.match(INTERNAL_FORMAT_RE, final_key) MATH_OPERATORS_RE = r'(?:%s)' % '|'.join(map(re.escape, MATH_FUNCTIONS.keys()))
if mobj is not None: INTERNAL_FORMAT_RE = re.compile(r'''(?x)
mobj = mobj.groupdict() (?P<negate>-)?
# Object traversal (?P<fields>{field})
fields = mobj['fields'].split('.') (?P<maths>(?:{math_op}{math_field})*)
value = traverse_dict(template_dict, fields) (?:>(?P<strf_format>.+?))?
# Negative (?:\|(?P<default>.*?))?
if mobj['negate']: $'''.format(field=FIELD_RE, math_op=MATH_OPERATORS_RE, math_field=MATH_FIELD_RE))
value = float_or_none(value)
if value is not None: get_key = lambda k: traverse_obj(
value *= -1 info_dict, k.split('.'), is_user_input=True, traverse_string=True)
# Do maths
if mobj['maths']: def get_value(mdict):
value = float_or_none(value) # Object traversal
operator = None value = get_key(mdict['fields'])
for item in MATH_OPERATORS_RE.split(mobj['maths'])[1:]: # Negative
if item == '': if mdict['negate']:
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) value = float_or_none(value)
if value is not None: if value is not None:
template_dict[final_key] = value value *= -1
# Do maths
offset_key = mdict['maths']
if offset_key:
value = float_or_none(value)
operator = None
while offset_key:
item = re.match(
MATH_FIELD_RE if operator else MATH_OPERATORS_RE,
offset_key).group(0)
offset_key = offset_key[len(item):]
if operator is None:
operator = MATH_FUNCTIONS[item]
continue
item, multiplier = (item[1:], -1) if item[0] == '-' else (item, 1)
offset = float_or_none(item)
if offset is None:
offset = float_or_none(get_key(item))
try:
value = operator(value, multiplier * offset)
except (TypeError, ZeroDivisionError):
return None
operator = None
# Datetime formatting
if mdict['strf_format']:
value = strftime_or_none(value, mdict['strf_format'])
# Missing numeric fields used together with integer presentation types return value
# in format specification will break the argument substitution since
# string NA placeholder is returned for missing fields. We will patch
# output template for missing fields to meet string presentation type.
for numeric_field in numeric_fields:
if template_dict.get(numeric_field) is None:
outtmpl = re.sub(
FORMAT_RE.format(re.escape(numeric_field)),
r'%({0})s'.format(numeric_field), outtmpl)
template_dict = collections.defaultdict(lambda: na, ( def create_key(outer_mobj):
(k, v if isinstance(v, compat_numeric_types) else sanitize(k, v)) if not outer_mobj.group('has_key'):
for k, v in template_dict.items() if v is not None)) return '%{}'.format(outer_mobj.group(0))
return outtmpl, template_dict
key = outer_mobj.group('key')
fmt = outer_mobj.group('format')
mobj = re.match(INTERNAL_FORMAT_RE, key)
if mobj is None:
value, default, mobj = None, na, {'fields': ''}
else:
mobj = mobj.groupdict()
default = mobj['default'] if mobj['default'] is not None else na
value = get_value(mobj)
if fmt == 's' and value is not None and key in field_size_compat_map.keys():
fmt = '0{:d}d'.format(field_size_compat_map[key])
value = default if value is None else value
if fmt == 'c':
value = compat_str(value)
if value is None:
value, fmt = default, 's'
else:
value = value[0]
elif fmt[-1] not in 'rs': # numeric
value = float_or_none(value)
if value is None:
value, fmt = default, 's'
if sanitize:
if fmt[-1] == 'r':
# If value is an object, sanitize might convert it to a string
# So we convert it to repr first
value, fmt = repr(value), '%ss' % fmt[:-1]
if fmt[-1] in 'csr':
value = sanitize(mobj['fields'].split('.')[-1], value)
key += '\0%s' % fmt
TMPL_DICT[key] = value
return '%({key}){fmt}'.format(key=key, fmt=fmt)
return EXTERNAL_FORMAT_RE.sub(create_key, outtmpl), TMPL_DICT
def _prepare_filename(self, info_dict, tmpl_type='default'): def _prepare_filename(self, info_dict, tmpl_type='default'):
try: try:
@@ -966,7 +992,7 @@ class YoutubeDL(object):
force_ext = OUTTMPL_TYPES.get(tmpl_type) force_ext = OUTTMPL_TYPES.get(tmpl_type)
if force_ext is not None: if force_ext is not None:
filename = replace_extension(filename, force_ext, template_dict.get('ext')) filename = replace_extension(filename, force_ext, info_dict.get('ext'))
# https://github.com/blackjack4494/youtube-dlc/issues/85 # https://github.com/blackjack4494/youtube-dlc/issues/85
trim_file_name = self.params.get('trim_file_name', False) trim_file_name = self.params.get('trim_file_name', False)
@@ -985,12 +1011,11 @@ class YoutubeDL(object):
def prepare_filename(self, info_dict, dir_type='', warn=False): def prepare_filename(self, info_dict, dir_type='', warn=False):
"""Generate the output filename.""" """Generate the output filename."""
paths = self.params.get('paths', {})
assert isinstance(paths, dict)
filename = self._prepare_filename(info_dict, dir_type or 'default') filename = self._prepare_filename(info_dict, dir_type or 'default')
if warn and not self.__prepare_filename_warned: if warn and not self.__prepare_filename_warned:
if not paths: if not self.params.get('paths'):
pass pass
elif filename == '-': elif filename == '-':
self.report_warning('--paths is ignored when an outputting to stdout') self.report_warning('--paths is ignored when an outputting to stdout')
@@ -1000,18 +1025,7 @@ class YoutubeDL(object):
if filename == '-' or not filename: if filename == '-' or not filename:
return filename return filename
homepath = expand_path(paths.get('home', '').strip()) return self.get_output_path(dir_type, filename)
assert isinstance(homepath, compat_str)
subdir = expand_path(paths.get(dir_type, '').strip()) if dir_type else ''
assert isinstance(subdir, compat_str)
path = os.path.join(homepath, subdir, filename)
# Temporary fix for #4787
# 'Treat' all problem characters by passing filename through preferredencoding
# to workaround encoding issues with subprocess on python2 @ Windows
if sys.version_info < (3, 0) and sys.platform == 'win32':
path = encodeFilename(path, True).decode(preferredencoding())
return sanitize_path(path, force=self.params.get('windowsfilenames'))
def _match_entry(self, info_dict, incomplete=False, silent=False): def _match_entry(self, info_dict, incomplete=False, silent=False):
""" Returns None if the file should be downloaded """ """ Returns None if the file should be downloaded """
@@ -1135,6 +1149,10 @@ class YoutubeDL(object):
self.report_error(msg) self.report_error(msg)
except ExtractorError as e: # An error we somewhat expected except ExtractorError as e: # An error we somewhat expected
self.report_error(compat_str(e), e.format_traceback()) self.report_error(compat_str(e), e.format_traceback())
except ThrottledDownload:
self.to_stderr('\r')
self.report_warning('The download speed is below throttle limit. Re-extracting data')
return wrapper(self, *args, **kwargs)
except (MaxDownloadsReached, ExistingVideoReached, RejectedVideoReached): except (MaxDownloadsReached, ExistingVideoReached, RejectedVideoReached):
raise raise
except Exception as e: except Exception as e:
@@ -1162,12 +1180,17 @@ class YoutubeDL(object):
return ie_result return ie_result
def add_default_extra_info(self, ie_result, ie, url): def add_default_extra_info(self, ie_result, ie, url):
self.add_extra_info(ie_result, { if url is not None:
'extractor': ie.IE_NAME, self.add_extra_info(ie_result, {
'webpage_url': url, 'webpage_url': url,
'webpage_url_basename': url_basename(url), 'original_url': url,
'extractor_key': ie.ie_key(), 'webpage_url_basename': url_basename(url),
}) })
if ie is not None:
self.add_extra_info(ie_result, {
'extractor': ie.IE_NAME,
'extractor_key': ie.ie_key(),
})
def process_ie_result(self, ie_result, download=True, extra_info={}): def process_ie_result(self, ie_result, download=True, extra_info={}):
""" """
@@ -1184,7 +1207,11 @@ class YoutubeDL(object):
extract_flat = self.params.get('extract_flat', False) extract_flat = self.params.get('extract_flat', False)
if ((extract_flat == 'in_playlist' and 'playlist' in extra_info) if ((extract_flat == 'in_playlist' and 'playlist' in extra_info)
or extract_flat is True): or extract_flat is True):
self.__forced_printings(ie_result, self.prepare_filename(ie_result), incomplete=True) info_copy = ie_result.copy()
self.add_extra_info(info_copy, extra_info)
ie = try_get(ie_result.get('ie_key'), self.get_info_extractor)
self.add_default_extra_info(info_copy, ie, ie_result['url'])
self.__forced_printings(info_copy, self.prepare_filename(info_copy), incomplete=True)
return ie_result return ie_result
if result_type == 'video': if result_type == 'video':
@@ -1478,12 +1505,11 @@ class YoutubeDL(object):
'!=': operator.ne, '!=': operator.ne,
} }
operator_rex = re.compile(r'''(?x)\s* operator_rex = re.compile(r'''(?x)\s*
(?P<key>width|height|tbr|abr|vbr|asr|filesize|filesize_approx|fps) (?P<key>width|height|tbr|abr|vbr|asr|filesize|filesize_approx|fps)\s*
\s*(?P<op>%s)(?P<none_inclusive>\s*\?)?\s* (?P<op>%s)(?P<none_inclusive>\s*\?)?\s*
(?P<value>[0-9.]+(?:[kKmMgGtTpPeEzZyY]i?[Bb]?)?) (?P<value>[0-9.]+(?:[kKmMgGtTpPeEzZyY]i?[Bb]?)?)\s*
$
''' % '|'.join(map(re.escape, OPERATORS.keys()))) ''' % '|'.join(map(re.escape, OPERATORS.keys())))
m = operator_rex.search(filter_spec) m = operator_rex.fullmatch(filter_spec)
if m: if m:
try: try:
comparison_value = int(m.group('value')) comparison_value = int(m.group('value'))
@@ -1504,13 +1530,12 @@ class YoutubeDL(object):
'$=': lambda attr, value: attr.endswith(value), '$=': lambda attr, value: attr.endswith(value),
'*=': lambda attr, value: value in attr, '*=': lambda attr, value: value in attr,
} }
str_operator_rex = re.compile(r'''(?x) str_operator_rex = re.compile(r'''(?x)\s*
\s*(?P<key>[a-zA-Z0-9._-]+) (?P<key>[a-zA-Z0-9._-]+)\s*
\s*(?P<negation>!\s*)?(?P<op>%s)(?P<none_inclusive>\s*\?)? (?P<negation>!\s*)?(?P<op>%s)(?P<none_inclusive>\s*\?)?\s*
\s*(?P<value>[a-zA-Z0-9._-]+) (?P<value>[a-zA-Z0-9._-]+)\s*
\s*$
''' % '|'.join(map(re.escape, STR_OPERATORS.keys()))) ''' % '|'.join(map(re.escape, STR_OPERATORS.keys())))
m = str_operator_rex.search(filter_spec) m = str_operator_rex.fullmatch(filter_spec)
if m: if m:
comparison_value = m.group('value') comparison_value = m.group('value')
str_op = STR_OPERATORS[m.group('op')] str_op = STR_OPERATORS[m.group('op')]
@@ -1520,7 +1545,7 @@ class YoutubeDL(object):
op = str_op op = str_op
if not m: if not m:
raise ValueError('Invalid filter specification %r' % filter_spec) raise SyntaxError('Invalid filter specification %r' % filter_spec)
def _filter(f): def _filter(f):
actual_value = f.get(m.group('key')) actual_value = f.get(m.group('key'))
@@ -1675,9 +1700,12 @@ class YoutubeDL(object):
formats_info.extend(format_2.get('requested_formats', (format_2,))) formats_info.extend(format_2.get('requested_formats', (format_2,)))
if not allow_multiple_streams['video'] or not allow_multiple_streams['audio']: if not allow_multiple_streams['video'] or not allow_multiple_streams['audio']:
get_no_more = {"video": False, "audio": False} get_no_more = {'video': False, 'audio': False}
for (i, fmt_info) in enumerate(formats_info): for (i, fmt_info) in enumerate(formats_info):
for aud_vid in ["audio", "video"]: if fmt_info.get('acodec') == fmt_info.get('vcodec') == 'none':
formats_info.pop(i)
continue
for aud_vid in ['audio', 'video']:
if not allow_multiple_streams[aud_vid] and fmt_info.get(aud_vid[0] + 'codec') != 'none': if not allow_multiple_streams[aud_vid] and fmt_info.get(aud_vid[0] + 'codec') != 'none':
if get_no_more[aud_vid]: if get_no_more[aud_vid]:
formats_info.pop(i) formats_info.pop(i)
@@ -1728,21 +1756,25 @@ class YoutubeDL(object):
return new_dict return new_dict
def _check_formats(formats): def _check_formats(formats):
if not check_formats:
yield from formats
for f in formats: for f in formats:
self.to_screen('[info] Testing format %s' % f['format_id']) self.to_screen('[info] Testing format %s' % f['format_id'])
paths = self.params.get('paths', {}) temp_file = tempfile.NamedTemporaryFile(
temp_file = os.path.join( suffix='.tmp', delete=False,
expand_path(paths.get('home', '').strip()), dir=self.get_output_path('temp') or None)
expand_path(paths.get('temp', '').strip()), temp_file.close()
'ytdl.%s.f%s.check-format' % (random_uuidv4(), f['format_id']))
try: try:
dl, _ = self.dl(temp_file, f, test=True) success, _ = self.dl(temp_file.name, f, test=True)
except (ExtractorError, IOError, OSError, ValueError) + network_exceptions: except (DownloadError, IOError, OSError, ValueError) + network_exceptions:
dl = False success = False
finally: finally:
if os.path.exists(temp_file): if os.path.exists(temp_file.name):
os.remove(temp_file) try:
if dl: os.remove(temp_file.name)
except OSError:
self.report_warning('Unable to delete temporary file "%s"' % temp_file.name)
if success:
yield f yield f
else: else:
self.to_screen('[info] Unable to download format %s. Skipping...' % f['format_id']) self.to_screen('[info] Unable to download format %s. Skipping...' % f['format_id'])
@@ -1753,8 +1785,7 @@ class YoutubeDL(object):
def selector_function(ctx): def selector_function(ctx):
for f in fs: for f in fs:
for format in f(ctx): yield from f(ctx)
yield format
return selector_function return selector_function
elif selector.type == GROUP: # () elif selector.type == GROUP: # ()
@@ -1770,17 +1801,21 @@ class YoutubeDL(object):
return picked_formats return picked_formats
return [] return []
elif selector.type == MERGE: # +
selector_1, selector_2 = map(_build_selector_function, selector.selector)
def selector_function(ctx):
for pair in itertools.product(
selector_1(copy.deepcopy(ctx)), selector_2(copy.deepcopy(ctx))):
yield _merge(pair)
elif selector.type == SINGLE: # atom elif selector.type == SINGLE: # atom
format_spec = selector.selector or 'best' 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']) yield from _check_formats(ctx['formats'])
if check_formats:
formats = _check_formats(formats)
for f in formats:
yield f
elif format_spec == 'mergeall': elif format_spec == 'mergeall':
def selector_function(ctx): def selector_function(ctx):
formats = list(_check_formats(ctx['formats'])) formats = list(_check_formats(ctx['formats']))
@@ -1804,14 +1839,16 @@ class YoutubeDL(object):
format_modified = mobj.group('mod') is not None format_modified = mobj.group('mod') is not None
format_fallback = not format_type and not format_modified # for b, w format_fallback = not format_type and not format_modified # for b, w
filter_f = ( _filter_f = (
(lambda f: f.get('%scodec' % format_type) != 'none') (lambda f: f.get('%scodec' % format_type) != 'none')
if format_type and format_modified # bv*, ba*, wv*, wa* if format_type and format_modified # bv*, ba*, wv*, wa*
else (lambda f: f.get('%scodec' % not_format_type) == 'none') else (lambda f: f.get('%scodec' % not_format_type) == 'none')
if format_type # bv, ba, wv, wa if format_type # bv, ba, wv, wa
else (lambda f: f.get('vcodec') != 'none' and f.get('acodec') != 'none') else (lambda f: f.get('vcodec') != 'none' and f.get('acodec') != 'none')
if not format_modified # b, w if not format_modified # b, w
else None) # b*, w* else lambda f: True) # b*, w*
filter_f = lambda f: _filter_f(f) and (
f.get('vcodec') != 'none' or f.get('acodec') != 'none')
else: else:
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
@@ -1819,29 +1856,17 @@ class YoutubeDL(object):
def selector_function(ctx): def selector_function(ctx):
formats = list(ctx['formats']) formats = list(ctx['formats'])
if not formats:
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
if format_fallback and ctx['incomplete_formats'] and not matches: if format_fallback and ctx['incomplete_formats'] and not matches:
# 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
matches = formats matches = formats
if format_reverse: matches = LazyList(_check_formats(matches[::-1 if format_reverse else 1]))
matches = matches[::-1] try:
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] yield matches[format_idx - 1]
except IndexError:
elif selector.type == MERGE: # + return
selector_1, selector_2 = map(_build_selector_function, selector.selector)
def selector_function(ctx):
for pair in itertools.product(
selector_1(copy.deepcopy(ctx)), selector_2(copy.deepcopy(ctx))):
yield _merge(pair)
filters = [self._build_format_filter(f) for f in selector.filters] filters = [self._build_format_filter(f) for f in selector.filters]
@@ -1904,8 +1929,7 @@ class YoutubeDL(object):
self.cookiejar.add_cookie_header(pr) self.cookiejar.add_cookie_header(pr)
return pr.get_header('Cookie') return pr.get_header('Cookie')
@staticmethod def _sanitize_thumbnails(self, info_dict):
def _sanitize_thumbnails(info_dict):
thumbnails = info_dict.get('thumbnails') thumbnails = info_dict.get('thumbnails')
if thumbnails is None: if thumbnails is None:
thumbnail = info_dict.get('thumbnail') thumbnail = info_dict.get('thumbnail')
@@ -1918,12 +1942,25 @@ class YoutubeDL(object):
t.get('height') if t.get('height') is not None else -1, t.get('height') if t.get('height') is not None else -1,
t.get('id') if t.get('id') is not None else '', t.get('id') if t.get('id') is not None else '',
t.get('url'))) t.get('url')))
def test_thumbnail(t):
self.to_screen('[info] Testing thumbnail %s' % t['id'])
try:
self.urlopen(HEADRequest(t['url']))
except network_exceptions as err:
self.to_screen('[info] Unable to connect to thumbnail %s URL "%s" - %s. Skipping...' % (
t['id'], t['url'], error_to_compat_str(err)))
return False
return True
for i, t in enumerate(thumbnails): for i, t in enumerate(thumbnails):
t['url'] = sanitize_url(t['url'])
if t.get('width') and t.get('height'):
t['resolution'] = '%dx%d' % (t['width'], t['height'])
if t.get('id') is None: if t.get('id') is None:
t['id'] = '%d' % i t['id'] = '%d' % i
if t.get('width') and t.get('height'):
t['resolution'] = '%dx%d' % (t['width'], t['height'])
t['url'] = sanitize_url(t['url'])
if self.params.get('check_formats'):
info_dict['thumbnails'] = LazyList(filter(test_thumbnail, thumbnails[::-1])).reverse()
def process_video_result(self, info_dict, download=True): def process_video_result(self, info_dict, download=True):
assert info_dict.get('_type', 'video') == 'video' assert info_dict.get('_type', 'video') == 'video'
@@ -1963,10 +2000,6 @@ class YoutubeDL(object):
self._sanitize_thumbnails(info_dict) self._sanitize_thumbnails(info_dict)
if self.params.get('list_thumbnails'):
self.list_thumbnails(info_dict)
return
thumbnail = info_dict.get('thumbnail') thumbnail = info_dict.get('thumbnail')
thumbnails = info_dict.get('thumbnails') thumbnails = info_dict.get('thumbnails')
if thumbnail: if thumbnail:
@@ -2009,13 +2042,6 @@ class YoutubeDL(object):
automatic_captions = info_dict.get('automatic_captions') automatic_captions = info_dict.get('automatic_captions')
subtitles = info_dict.get('subtitles') subtitles = info_dict.get('subtitles')
if self.params.get('listsubtitles', False):
if 'automatic_captions' in info_dict:
self.list_subtitles(
info_dict['id'], automatic_captions, 'automatic captions')
self.list_subtitles(info_dict['id'], subtitles, 'subtitles')
return
info_dict['requested_subtitles'] = self.process_subtitles( info_dict['requested_subtitles'] = self.process_subtitles(
info_dict['id'], subtitles, automatic_captions) info_dict['id'], subtitles, automatic_captions)
@@ -2103,18 +2129,27 @@ class YoutubeDL(object):
info_dict, _ = self.pre_process(info_dict) info_dict, _ = self.pre_process(info_dict)
if self.params.get('listformats'): list_only = self.params.get('list_thumbnails') or self.params.get('listformats') or self.params.get('listsubtitles')
if not info_dict.get('formats'): if list_only:
raise ExtractorError('No video formats found', expected=True) self.__forced_printings(info_dict, self.prepare_filename(info_dict), incomplete=True)
self.list_formats(info_dict) if self.params.get('list_thumbnails'):
self.list_thumbnails(info_dict)
if self.params.get('listformats'):
if not info_dict.get('formats'):
raise ExtractorError('No video formats found', expected=True)
self.list_formats(info_dict)
if self.params.get('listsubtitles'):
if 'automatic_captions' in info_dict:
self.list_subtitles(
info_dict['id'], automatic_captions, 'automatic captions')
self.list_subtitles(info_dict['id'], subtitles, 'subtitles')
return return
req_format = self.params.get('format') format_selector = self.format_selector
if req_format is None: if format_selector is None:
req_format = self._default_format_spec(info_dict, download=download) req_format = self._default_format_spec(info_dict, download=download)
self.write_debug('Default format spec: %s' % req_format) self.write_debug('Default format spec: %s' % req_format)
format_selector = self.build_format_selector(req_format)
format_selector = self.build_format_selector(req_format)
# While in format selection we may need to have an access to the original # While in format selection we may need to have an access to the original
# format set in order to calculate some metrics or do some processing. # format set in order to calculate some metrics or do some processing.
@@ -2148,6 +2183,8 @@ class YoutubeDL(object):
raise ExtractorError('Requested format is not available', expected=True) raise ExtractorError('Requested format is not available', expected=True)
else: else:
self.report_warning('Requested format is not available') self.report_warning('Requested format is not available')
# Process what we can, even without any available formats.
self.process_info(dict(info_dict))
elif download: elif download:
self.to_screen( self.to_screen(
'[info] %s: Downloading %d format(s): %s' % ( '[info] %s: Downloading %d format(s): %s' % (
@@ -2312,7 +2349,7 @@ class YoutubeDL(object):
# TODO: backward compatibility, to be removed # TODO: backward compatibility, to be removed
info_dict['fulltitle'] = info_dict['title'] info_dict['fulltitle'] = info_dict['title']
if 'format' not in info_dict: if 'format' not in info_dict and 'ext' in info_dict:
info_dict['format'] = info_dict['ext'] info_dict['format'] = info_dict['ext']
if self._match_entry(info_dict) is not None: if self._match_entry(info_dict) is not None:
@@ -2327,7 +2364,7 @@ class YoutubeDL(object):
files_to_move = {} files_to_move = {}
# Forced printings # Forced printings
self.__forced_printings(info_dict, full_filename, incomplete=False) self.__forced_printings(info_dict, full_filename, incomplete=('format' not in info_dict))
if self.params.get('simulate', False): if self.params.get('simulate', False):
if self.params.get('force_write_download_archive', False): if self.params.get('force_write_download_archive', False):
@@ -2648,65 +2685,53 @@ class YoutubeDL(object):
return return
if success and full_filename != '-': if success and full_filename != '-':
# Fixup content
fixup_policy = self.params.get('fixup')
if fixup_policy is None:
fixup_policy = 'detect_or_warn'
INSTALL_FFMPEG_MESSAGE = 'Install ffmpeg to fix this automatically.' def fixup():
do_fixup = True
fixup_policy = self.params.get('fixup')
vid = info_dict['id']
stretched_ratio = info_dict.get('stretched_ratio') if fixup_policy in ('ignore', 'never'):
if stretched_ratio is not None and stretched_ratio != 1: return
if fixup_policy == 'warn': elif fixup_policy == 'warn':
self.report_warning('%s: Non-uniform pixel ratio (%s)' % ( do_fixup = False
info_dict['id'], stretched_ratio)) elif fixup_policy != 'force':
elif fixup_policy == 'detect_or_warn': assert fixup_policy in ('detect_or_warn', None)
stretched_pp = FFmpegFixupStretchedPP(self) if not info_dict.get('__real_download'):
if stretched_pp.available: do_fixup = False
info_dict['__postprocessors'].append(stretched_pp)
def ffmpeg_fixup(cndn, msg, cls):
if not cndn:
return
if not do_fixup:
self.report_warning(f'{vid}: {msg}')
return
pp = cls(self)
if pp.available:
info_dict['__postprocessors'].append(pp)
else: else:
self.report_warning( self.report_warning(f'{vid}: {msg}. Install ffmpeg to fix this automatically')
'%s: Non-uniform pixel ratio (%s). %s'
% (info_dict['id'], stretched_ratio, INSTALL_FFMPEG_MESSAGE))
else:
assert fixup_policy in ('ignore', 'never')
if (info_dict.get('requested_formats') is None stretched_ratio = info_dict.get('stretched_ratio')
and info_dict.get('container') == 'm4a_dash' ffmpeg_fixup(
and info_dict.get('ext') == 'm4a'): stretched_ratio not in (1, None),
if fixup_policy == 'warn': f'Non-uniform pixel ratio {stretched_ratio}',
self.report_warning( FFmpegFixupStretchedPP)
'%s: writing DASH m4a. '
'Only some players support this container.'
% info_dict['id'])
elif fixup_policy == 'detect_or_warn':
fixup_pp = FFmpegFixupM4aPP(self)
if fixup_pp.available:
info_dict['__postprocessors'].append(fixup_pp)
else:
self.report_warning(
'%s: writing DASH m4a. '
'Only some players support this container. %s'
% (info_dict['id'], INSTALL_FFMPEG_MESSAGE))
else:
assert fixup_policy in ('ignore', 'never')
if ('protocol' in info_dict ffmpeg_fixup(
and get_suitable_downloader(info_dict, self.params).__name__ == 'HlsFD'): (info_dict.get('requested_formats') is None
if fixup_policy == 'warn': and info_dict.get('container') == 'm4a_dash'
self.report_warning('%s: malformed AAC bitstream detected.' % ( and info_dict.get('ext') == 'm4a'),
info_dict['id'])) 'writing DASH m4a. Only some players support this container',
elif fixup_policy == 'detect_or_warn': FFmpegFixupM4aPP)
fixup_pp = FFmpegFixupM3u8PP(self)
if fixup_pp.available:
info_dict['__postprocessors'].append(fixup_pp)
else:
self.report_warning(
'%s: malformed AAC bitstream detected. %s'
% (info_dict['id'], INSTALL_FFMPEG_MESSAGE))
else:
assert fixup_policy in ('ignore', 'never')
downloader = (get_suitable_downloader(info_dict, self.params).__name__
if 'protocol' in info_dict else None)
ffmpeg_fixup(downloader == 'HlsFD', 'malformed AAC bitstream detected', FFmpegFixupM3u8PP)
ffmpeg_fixup(downloader == 'WebSocketFragmentFD', 'malformed timestamps detected', FFmpegFixupTimestampPP)
ffmpeg_fixup(downloader == 'WebSocketFragmentFD', 'malformed duration detected', FFmpegFixupDurationPP)
fixup()
try: try:
info_dict = self.post_process(dl_filename, info_dict, files_to_move) info_dict = self.post_process(dl_filename, info_dict, files_to_move)
except PostProcessingError as err: except PostProcessingError as err:
@@ -2766,7 +2791,7 @@ class YoutubeDL(object):
info = self.filter_requested_info(json.loads('\n'.join(f)), self.params.get('clean_infojson', True)) info = self.filter_requested_info(json.loads('\n'.join(f)), self.params.get('clean_infojson', True))
try: try:
self.process_ie_result(info, download=True) self.process_ie_result(info, download=True)
except (DownloadError, EntryNotInPlaylist): except (DownloadError, EntryNotInPlaylist, ThrottledDownload):
webpage_url = info.get('webpage_url') webpage_url = info.get('webpage_url')
if webpage_url is not None: if webpage_url is not None:
self.report_warning('The info failed to download, trying with "%s"' % webpage_url) self.report_warning('The info failed to download, trying with "%s"' % webpage_url)
@@ -2780,7 +2805,7 @@ class YoutubeDL(object):
remove_keys = ['__original_infodict'] # Always remove this since this may contain a copy of the entire dict remove_keys = ['__original_infodict'] # Always remove this since this may contain a copy of the entire dict
keep_keys = ['_type'], # Always keep this to facilitate load-info-json keep_keys = ['_type'], # Always keep this to facilitate load-info-json
if actually_filter: if actually_filter:
remove_keys += ('requested_formats', 'requested_subtitles', 'requested_entries', 'filepath', 'entries') remove_keys += ('requested_formats', 'requested_subtitles', 'requested_entries', 'filepath', 'entries', 'original_url')
empty_values = (None, {}, [], set(), tuple()) empty_values = (None, {}, [], set(), tuple())
reject = lambda k, v: k not in keep_keys and ( reject = lambda k, v: k not in keep_keys and (
k.startswith('_') or k in remove_keys or v in empty_values) k.startswith('_') or k in remove_keys or v in empty_values)
@@ -2788,7 +2813,7 @@ class YoutubeDL(object):
info_dict['epoch'] = int(time.time()) info_dict['epoch'] = int(time.time())
reject = lambda k, v: k in remove_keys reject = lambda k, v: k in remove_keys
filter_fn = lambda obj: ( filter_fn = lambda obj: (
list(map(filter_fn, obj)) if isinstance(obj, (list, tuple, set)) list(map(filter_fn, obj)) if isinstance(obj, (LazyList, list, tuple, set))
else obj if not isinstance(obj, dict) else obj if not isinstance(obj, dict)
else dict((k, filter_fn(v)) for k, v in obj.items() if not reject(k, v))) else dict((k, filter_fn(v)) for k, v in obj.items() if not reject(k, v)))
return filter_fn(info_dict) return filter_fn(info_dict)
@@ -2899,6 +2924,8 @@ class YoutubeDL(object):
@staticmethod @staticmethod
def format_resolution(format, default='unknown'): def format_resolution(format, default='unknown'):
if format.get('vcodec') == 'none': if format.get('vcodec') == 'none':
if format.get('acodec') == 'none':
return 'images'
return 'audio only' return 'audio only'
if format.get('resolution') is not None: if format.get('resolution') is not None:
return format['resolution'] return format['resolution']
@@ -2983,7 +3010,7 @@ class YoutubeDL(object):
formats = info_dict.get('formats', [info_dict]) formats = info_dict.get('formats', [info_dict])
new_format = ( new_format = (
'list-formats' not in self.params.get('compat_opts', []) 'list-formats' not in self.params.get('compat_opts', [])
and self.params.get('list_formats_as_table', True) is not False) and self.params.get('listformats_table', True) is not False)
if new_format: if new_format:
table = [ table = [
[ [
@@ -3018,22 +3045,19 @@ class YoutubeDL(object):
header_line = ['format code', 'extension', 'resolution', 'note'] header_line = ['format code', 'extension', 'resolution', 'note']
self.to_screen( self.to_screen(
'[info] Available formats for %s:\n%s' % (info_dict['id'], render_table( '[info] Available formats for %s:' % info_dict['id'])
header_line, self.to_stdout(render_table(
table, header_line, table, delim=new_format, extraGap=(0 if new_format else 1), hideEmpty=new_format))
delim=new_format,
extraGap=(0 if new_format else 1),
hideEmpty=new_format)))
def list_thumbnails(self, info_dict): def list_thumbnails(self, info_dict):
thumbnails = info_dict.get('thumbnails') thumbnails = list(info_dict.get('thumbnails'))
if not thumbnails: if not thumbnails:
self.to_screen('[info] No thumbnails present for %s' % info_dict['id']) self.to_screen('[info] No thumbnails present for %s' % info_dict['id'])
return return
self.to_screen( self.to_screen(
'[info] Thumbnails for %s:' % info_dict['id']) '[info] Thumbnails for %s:' % info_dict['id'])
self.to_screen(render_table( self.to_stdout(render_table(
['ID', 'width', 'height', 'URL'], ['ID', 'width', 'height', 'URL'],
[[t['id'], t.get('width', 'unknown'), t.get('height', 'unknown'), t['url']] for t in thumbnails])) [[t['id'], t.get('width', 'unknown'), t.get('height', 'unknown'), t['url']] for t in thumbnails]))
@@ -3045,12 +3069,12 @@ class YoutubeDL(object):
'Available %s for %s:' % (name, video_id)) 'Available %s for %s:' % (name, video_id))
def _row(lang, formats): def _row(lang, formats):
exts, names = zip(*((f['ext'], f.get('name', 'unknown')) for f in reversed(formats))) exts, names = zip(*((f['ext'], f.get('name') or 'unknown') for f in reversed(formats)))
if len(set(names)) == 1: if len(set(names)) == 1:
names = [] if names[0] == 'unknown' else names[:1] names = [] if names[0] == 'unknown' else names[:1]
return [lang, ', '.join(names), ', '.join(exts)] return [lang, ', '.join(names), ', '.join(exts)]
self.to_screen(render_table( self.to_stdout(render_table(
['Language', 'Name', 'Formats'], ['Language', 'Name', 'Formats'],
[_row(lang, formats) for lang, formats in subtitles.items()], [_row(lang, formats) for lang, formats in subtitles.items()],
hideEmpty=True)) hideEmpty=True))
@@ -3228,7 +3252,7 @@ class YoutubeDL(object):
multiple = write_all and len(thumbnails) > 1 multiple = write_all and len(thumbnails) > 1
ret = [] ret = []
for t in thumbnails[::1 if write_all else -1]: for t in thumbnails[::-1]:
thumb_ext = determine_ext(t['url'], 'jpg') thumb_ext = determine_ext(t['url'], 'jpg')
suffix = '%s.' % t['id'] if multiple else '' suffix = '%s.' % t['id'] if multiple else ''
thumb_display_id = '%s ' % t['id'] if multiple else '' thumb_display_id = '%s ' % t['id'] if multiple else ''
@@ -3236,6 +3260,7 @@ class YoutubeDL(object):
if not self.params.get('overwrites', True) and os.path.exists(encodeFilename(thumb_filename)): if not self.params.get('overwrites', True) and os.path.exists(encodeFilename(thumb_filename)):
ret.append(suffix + thumb_ext) ret.append(suffix + thumb_ext)
t['filepath'] = thumb_filename
self.to_screen('[%s] %s: Thumbnail %sis already present' % self.to_screen('[%s] %s: Thumbnail %sis already present' %
(info_dict['extractor'], info_dict['id'], thumb_display_id)) (info_dict['extractor'], info_dict['id'], thumb_display_id))
else: else:
+20 -1
View File
@@ -1,4 +1,4 @@
#!/usr/bin/env python #!/usr/bin/env python3
# coding: utf-8 # coding: utf-8
from __future__ import unicode_literals from __future__ import unicode_literals
@@ -24,6 +24,7 @@ from .utils import (
DateRange, DateRange,
decodeOption, decodeOption,
DownloadError, DownloadError,
error_to_compat_str,
ExistingVideoReached, ExistingVideoReached,
expand_path, expand_path,
match_filter_func, match_filter_func,
@@ -150,6 +151,11 @@ def _real_main(argv=None):
if numeric_limit is None: if numeric_limit is None:
parser.error('invalid rate limit specified') parser.error('invalid rate limit specified')
opts.ratelimit = numeric_limit opts.ratelimit = numeric_limit
if opts.throttledratelimit is not None:
numeric_limit = FileDownloader.parse_bytes(opts.throttledratelimit)
if numeric_limit is None:
parser.error('invalid rate limit specified')
opts.throttledratelimit = numeric_limit
if opts.min_filesize is not None: if opts.min_filesize is not None:
numeric_limit = FileDownloader.parse_bytes(opts.min_filesize) numeric_limit = FileDownloader.parse_bytes(opts.min_filesize)
if numeric_limit is None: if numeric_limit is None:
@@ -267,6 +273,7 @@ def _real_main(argv=None):
'filename', 'format-sort', 'abort-on-error', 'format-spec', 'no-playlist-metafiles', 'filename', 'format-sort', 'abort-on-error', 'format-spec', 'no-playlist-metafiles',
'multistreams', 'no-live-chat', 'playlist-index', 'list-formats', 'no-direct-merge', 'multistreams', 'no-live-chat', 'playlist-index', 'list-formats', 'no-direct-merge',
'no-youtube-channel-redirect', 'no-youtube-unavailable-videos', 'no-attach-info-json', 'no-youtube-channel-redirect', 'no-youtube-unavailable-videos', 'no-attach-info-json',
'embed-thumbnail-atomicparsley', 'seperate-video-versions',
] ]
compat_opts = parse_compat_opts() compat_opts = parse_compat_opts()
@@ -307,6 +314,16 @@ def _real_main(argv=None):
else: else:
_unused_compat_opt('filename') _unused_compat_opt('filename')
def validate_outtmpl(tmpl, msg):
err = YoutubeDL.validate_outtmpl(tmpl)
if err:
parser.error('invalid %s %r: %s' % (msg, tmpl, error_to_compat_str(err)))
for k, tmpl in opts.outtmpl.items():
validate_outtmpl(tmpl, '%s output template' % k)
for tmpl in opts.forceprint:
validate_outtmpl(tmpl, 'print template')
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'
@@ -540,6 +557,7 @@ def _real_main(argv=None):
'ignoreerrors': opts.ignoreerrors, 'ignoreerrors': opts.ignoreerrors,
'force_generic_extractor': opts.force_generic_extractor, 'force_generic_extractor': opts.force_generic_extractor,
'ratelimit': opts.ratelimit, 'ratelimit': opts.ratelimit,
'throttledratelimit': opts.throttledratelimit,
'overwrites': opts.overwrites, 'overwrites': opts.overwrites,
'retries': opts.retries, 'retries': opts.retries,
'fragment_retries': opts.fragment_retries, 'fragment_retries': opts.fragment_retries,
@@ -613,6 +631,7 @@ def _real_main(argv=None):
'include_ads': opts.include_ads, 'include_ads': opts.include_ads,
'default_search': opts.default_search, 'default_search': opts.default_search,
'dynamic_mpd': opts.dynamic_mpd, 'dynamic_mpd': opts.dynamic_mpd,
'extractor_args': opts.extractor_args,
'youtube_include_dash_manifest': opts.youtube_include_dash_manifest, 'youtube_include_dash_manifest': opts.youtube_include_dash_manifest,
'youtube_include_hls_manifest': opts.youtube_include_hls_manifest, 'youtube_include_hls_manifest': opts.youtube_include_hls_manifest,
'encoding': opts.encoding, 'encoding': opts.encoding,
+1 -1
View File
@@ -1,4 +1,4 @@
#!/usr/bin/env python #!/usr/bin/env python3
from __future__ import unicode_literals from __future__ import unicode_literals
# Execute with # Execute with
+16
View File
@@ -3030,6 +3030,21 @@ except AttributeError:
compat_Match = type(re.compile('').match('')) compat_Match = type(re.compile('').match(''))
import asyncio
try:
compat_asyncio_run = asyncio.run
except AttributeError:
def compat_asyncio_run(coro):
try:
loop = asyncio.get_event_loop()
except RuntimeError:
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
loop.run_until_complete(coro)
asyncio.run = compat_asyncio_run
__all__ = [ __all__ = [
'compat_HTMLParseError', 'compat_HTMLParseError',
'compat_HTMLParser', 'compat_HTMLParser',
@@ -3037,6 +3052,7 @@ __all__ = [
'compat_Match', 'compat_Match',
'compat_Pattern', 'compat_Pattern',
'compat_Struct', 'compat_Struct',
'compat_asyncio_run',
'compat_b64decode', 'compat_b64decode',
'compat_basestring', 'compat_basestring',
'compat_chr', 'compat_chr',
+8 -2
View File
@@ -22,8 +22,10 @@ from .http import HttpFD
from .rtmp import RtmpFD from .rtmp import RtmpFD
from .rtsp import RtspFD from .rtsp import RtspFD
from .ism import IsmFD from .ism import IsmFD
from .mhtml import MhtmlFD
from .niconico import NiconicoDmcFD from .niconico import NiconicoDmcFD
from .youtube_live_chat import YoutubeLiveChatReplayFD from .websocket import WebSocketFragmentFD
from .youtube_live_chat import YoutubeLiveChatFD
from .external import ( from .external import (
get_external_downloader, get_external_downloader,
FFmpegFD, FFmpegFD,
@@ -39,8 +41,11 @@ PROTOCOL_MAP = {
'f4m': F4mFD, 'f4m': F4mFD,
'http_dash_segments': DashSegmentsFD, 'http_dash_segments': DashSegmentsFD,
'ism': IsmFD, 'ism': IsmFD,
'mhtml': MhtmlFD,
'niconico_dmc': NiconicoDmcFD, 'niconico_dmc': NiconicoDmcFD,
'youtube_live_chat_replay': YoutubeLiveChatReplayFD, 'websocket_frag': WebSocketFragmentFD,
'youtube_live_chat': YoutubeLiveChatFD,
'youtube_live_chat_replay': YoutubeLiveChatFD,
} }
@@ -50,6 +55,7 @@ def shorten_protocol_name(proto, simplify=False):
'rtmp_ffmpeg': 'rtmp_f', 'rtmp_ffmpeg': 'rtmp_f',
'http_dash_segments': 'dash', 'http_dash_segments': 'dash',
'niconico_dmc': 'dmc', 'niconico_dmc': 'dmc',
'websocket_frag': 'WSfrag',
} }
if simplify: if simplify:
short_protocol_names.update({ short_protocol_names.update({
+1
View File
@@ -32,6 +32,7 @@ class FileDownloader(object):
verbose: Print additional info to stdout. verbose: Print additional info to stdout.
quiet: Do not print messages to stdout. quiet: Do not print messages to stdout.
ratelimit: Download speed limit, in bytes/sec. ratelimit: Download speed limit, in bytes/sec.
throttledratelimit: Assume the download is being throttled below this speed (bytes/sec)
retries: Number of times to retry for HTTP error 5xx retries: Number of times to retry for HTTP error 5xx
buffersize: Size of download buffer in bytes. buffersize: Size of download buffer in bytes.
noresizebuffer: Do not automatically resize the download buffer. noresizebuffer: Do not automatically resize the download buffer.
+3 -132
View File
@@ -1,21 +1,9 @@
from __future__ import unicode_literals from __future__ import unicode_literals
import errno
try:
import concurrent.futures
can_threaded_download = True
except ImportError:
can_threaded_download = False
from ..downloader import _get_real_downloader from ..downloader import _get_real_downloader
from .fragment import FragmentFD from .fragment import FragmentFD
from ..compat import compat_urllib_error from ..utils import urljoin
from ..utils import (
DownloadError,
sanitize_open,
urljoin,
)
class DashSegmentsFD(FragmentFD): class DashSegmentsFD(FragmentFD):
@@ -43,9 +31,6 @@ class DashSegmentsFD(FragmentFD):
else: else:
self._prepare_and_start_frag_download(ctx) self._prepare_and_start_frag_download(ctx)
fragment_retries = self.params.get('fragment_retries', 0)
skip_unavailable_fragments = self.params.get('skip_unavailable_fragments', True)
fragments_to_download = [] fragments_to_download = []
frag_index = 0 frag_index = 0
for i, fragment in enumerate(fragments): for i, fragment in enumerate(fragments):
@@ -72,120 +57,6 @@ class DashSegmentsFD(FragmentFD):
# TODO: Make progress updates work without hooking twice # TODO: Make progress updates work without hooking twice
# for ph in self._progress_hooks: # for ph in self._progress_hooks:
# fd.add_progress_hook(ph) # fd.add_progress_hook(ph)
success = fd.real_download(filename, info_copy) return fd.real_download(filename, info_copy)
if not success:
return False
else:
def download_fragment(fragment):
i = fragment['index']
frag_index = fragment['frag_index']
fragment_url = fragment['url']
ctx['fragment_index'] = frag_index return self.download_and_append_fragments(ctx, fragments_to_download, info_dict)
# In DASH, the first segment contains necessary headers to
# generate a valid MP4 file, so always abort for the first segment
fatal = i == 0 or not skip_unavailable_fragments
count = 0
while count <= fragment_retries:
try:
success, frag_content = self._download_fragment(ctx, fragment_url, info_dict)
if not success:
return False, frag_index
break
except compat_urllib_error.HTTPError as err:
# YouTube may often return 404 HTTP error for a fragment causing the
# whole download to fail. However if the same fragment is immediately
# retried with the same request data this usually succeeds (1-2 attempts
# is usually enough) thus allowing to download the whole file successfully.
# To be future-proof we will retry all fragments that fail with any
# HTTP error.
count += 1
if count <= fragment_retries:
self.report_retry_fragment(err, frag_index, count, fragment_retries)
except DownloadError:
# Don't retry fragment if error occurred during HTTP downloading
# itself since it has own retry settings
if not fatal:
break
raise
if count > fragment_retries:
if not fatal:
return False, frag_index
ctx['dest_stream'].close()
self.report_error('Giving up after %s fragment retries' % fragment_retries)
return False, frag_index
return frag_content, frag_index
def append_fragment(frag_content, frag_index):
fatal = frag_index == 1 or not skip_unavailable_fragments
if frag_content:
fragment_filename = '%s-Frag%d' % (ctx['tmpfilename'], frag_index)
try:
file, frag_sanitized = sanitize_open(fragment_filename, 'rb')
ctx['fragment_filename_sanitized'] = frag_sanitized
file.close()
self._append_fragment(ctx, frag_content)
return True
except EnvironmentError as ose:
if ose.errno != errno.ENOENT:
raise
# FileNotFoundError
if not fatal:
self.report_skip_fragment(frag_index)
return True
else:
ctx['dest_stream'].close()
self.report_error(
'fragment %s not found, unable to continue' % frag_index)
return False
else:
if not fatal:
self.report_skip_fragment(frag_index)
return True
else:
ctx['dest_stream'].close()
self.report_error(
'fragment %s not found, unable to continue' % frag_index)
return False
max_workers = self.params.get('concurrent_fragment_downloads', 1)
if can_threaded_download and max_workers > 1:
self.report_warning('The download speed shown is only of one thread. This is a known issue')
_download_fragment = lambda f: (f, download_fragment(f)[1])
with concurrent.futures.ThreadPoolExecutor(max_workers) as pool:
futures = [pool.submit(_download_fragment, fragment) for fragment in fragments_to_download]
# timeout must be 0 to return instantly
done, not_done = concurrent.futures.wait(futures, timeout=0)
try:
while not_done:
# Check every 1 second for KeyboardInterrupt
freshly_done, not_done = concurrent.futures.wait(not_done, timeout=1)
done |= freshly_done
except KeyboardInterrupt:
for future in not_done:
future.cancel()
# timeout must be none to cancel
concurrent.futures.wait(not_done, timeout=None)
raise KeyboardInterrupt
for fragment, frag_index in map(lambda x: x.result(), futures):
fragment_filename = '%s-Frag%d' % (ctx['tmpfilename'], frag_index)
down, frag_sanitized = sanitize_open(fragment_filename, 'rb')
fragment['fragment_filename_sanitized'] = frag_sanitized
frag_content = down.read()
down.close()
result = append_fragment(frag_content, frag_index)
if not result:
return False
else:
for fragment in fragments_to_download:
frag_content, frag_index = download_fragment(fragment)
result = append_fragment(frag_content, frag_index)
if not result:
return False
self._finish_frag_download(ctx)
return True
+13 -4
View File
@@ -280,6 +280,8 @@ class Aria2cFD(ExternalFD):
'--file-allocation=none', '-x16', '-j16', '-s16'] '--file-allocation=none', '-x16', '-j16', '-s16']
if 'fragments' in info_dict: if 'fragments' in info_dict:
cmd += ['--allow-overwrite=true', '--allow-piece-length-change=true'] cmd += ['--allow-overwrite=true', '--allow-piece-length-change=true']
else:
cmd += ['--min-split-size', '1M']
if info_dict.get('http_headers') is not None: if info_dict.get('http_headers') is not None:
for key, val in info_dict['http_headers'].items(): for key, val in info_dict['http_headers'].items():
@@ -345,6 +347,10 @@ class FFmpegFD(ExternalFD):
# TODO: Fix path for ffmpeg # TODO: Fix path for ffmpeg
return FFmpegPostProcessor().available return FFmpegPostProcessor().available
def on_process_started(self, proc, stdin):
""" Override this in subclasses """
pass
def _call_downloader(self, tmpfilename, info_dict): def _call_downloader(self, tmpfilename, info_dict):
urls = [f['url'] for f in info_dict.get('requested_formats', [])] or [info_dict['url']] urls = [f['url'] for f in info_dict.get('requested_formats', [])] or [info_dict['url']]
ffpp = FFmpegPostProcessor(downloader=self) ffpp = FFmpegPostProcessor(downloader=self)
@@ -359,6 +365,8 @@ class FFmpegFD(ExternalFD):
if self.params.get(log_level, False): if self.params.get(log_level, False):
args += ['-loglevel', log_level] args += ['-loglevel', log_level]
break break
if not self.params.get('verbose'):
args += ['-hide_banner']
seekable = info_dict.get('_seekable') seekable = info_dict.get('_seekable')
if seekable is not None: if seekable is not None:
@@ -369,8 +377,6 @@ class FFmpegFD(ExternalFD):
# http://trac.ffmpeg.org/ticket/6125#comment:10 # http://trac.ffmpeg.org/ticket/6125#comment:10
args += ['-seekable', '1' if seekable else '0'] args += ['-seekable', '1' if seekable else '0']
args += self._configuration_args()
# start_time = info_dict.get('start_time') or 0 # start_time = info_dict.get('start_time') or 0
# if start_time: # if start_time:
# args += ['-ss', compat_str(start_time)] # args += ['-ss', compat_str(start_time)]
@@ -438,7 +444,8 @@ class FFmpegFD(ExternalFD):
for url in urls: for url in urls:
args += ['-i', url] args += ['-i', url]
args += ['-c', 'copy']
args += self._configuration_args() + ['-c', 'copy']
if info_dict.get('requested_formats'): if info_dict.get('requested_formats'):
for (i, fmt) in enumerate(info_dict['requested_formats']): for (i, fmt) in enumerate(info_dict['requested_formats']):
if fmt.get('acodec') != 'none': if fmt.get('acodec') != 'none':
@@ -470,6 +477,8 @@ class FFmpegFD(ExternalFD):
self._debug_cmd(args) self._debug_cmd(args)
proc = subprocess.Popen(args, stdin=subprocess.PIPE, env=env) proc = subprocess.Popen(args, stdin=subprocess.PIPE, env=env)
if url in ('-', 'pipe:'):
self.on_process_started(proc, proc.stdin)
try: try:
retval = proc.wait() retval = proc.wait()
except BaseException as e: except BaseException as e:
@@ -478,7 +487,7 @@ class FFmpegFD(ExternalFD):
# produces a file that is playable (this is mostly useful for live # produces a file that is playable (this is mostly useful for live
# streams). Note that Windows is not affected and produces playable # streams). Note that Windows is not affected and produces playable
# files (see https://github.com/ytdl-org/youtube-dl/issues/8300). # files (see https://github.com/ytdl-org/youtube-dl/issues/8300).
if isinstance(e, KeyboardInterrupt) and sys.platform != 'win32': if isinstance(e, KeyboardInterrupt) and sys.platform != 'win32' and url not in ('-', 'pipe:'):
process_communicate_or_kill(proc, b'q') process_communicate_or_kill(proc, b'q')
else: else:
proc.kill() proc.kill()
+122 -3
View File
@@ -4,9 +4,26 @@ import os
import time import time
import json import json
try:
from Crypto.Cipher import AES
can_decrypt_frag = True
except ImportError:
can_decrypt_frag = False
try:
import concurrent.futures
can_threaded_download = True
except ImportError:
can_threaded_download = False
from .common import FileDownloader from .common import FileDownloader
from .http import HttpFD from .http import HttpFD
from ..compat import (
compat_urllib_error,
compat_struct_pack,
)
from ..utils import ( from ..utils import (
DownloadError,
error_to_compat_str, error_to_compat_str,
encodeFilename, encodeFilename,
sanitize_open, sanitize_open,
@@ -56,7 +73,7 @@ class FragmentFD(FileDownloader):
def report_retry_fragment(self, err, frag_index, count, retries): def report_retry_fragment(self, err, frag_index, count, retries):
self.to_screen( self.to_screen(
'[download] Got server HTTP error: %s. Retrying fragment %d (attempt %d of %s) ...' '\r[download] Got server HTTP error: %s. Retrying fragment %d (attempt %d of %s) ...'
% (error_to_compat_str(err), frag_index, count, self.format_retries(retries))) % (error_to_compat_str(err), frag_index, count, self.format_retries(retries)))
def report_skip_fragment(self, frag_index): def report_skip_fragment(self, frag_index):
@@ -112,11 +129,15 @@ class FragmentFD(FileDownloader):
return False, None return False, None
if fragment_info_dict.get('filetime'): if fragment_info_dict.get('filetime'):
ctx['fragment_filetime'] = fragment_info_dict.get('filetime') ctx['fragment_filetime'] = fragment_info_dict.get('filetime')
down, frag_sanitized = sanitize_open(fragment_filename, 'rb') ctx['fragment_filename_sanitized'] = fragment_filename
return True, self._read_fragment(ctx)
def _read_fragment(self, ctx):
down, frag_sanitized = sanitize_open(ctx['fragment_filename_sanitized'], 'rb')
ctx['fragment_filename_sanitized'] = frag_sanitized ctx['fragment_filename_sanitized'] = frag_sanitized
frag_content = down.read() frag_content = down.read()
down.close() down.close()
return True, frag_content return frag_content
def _append_fragment(self, ctx, frag_content): def _append_fragment(self, ctx, frag_content):
try: try:
@@ -304,3 +325,101 @@ class FragmentFD(FileDownloader):
'tmpfilename': tmpfilename, 'tmpfilename': tmpfilename,
'fragment_index': 0, 'fragment_index': 0,
}) })
def download_and_append_fragments(self, ctx, fragments, info_dict, pack_func=None):
fragment_retries = self.params.get('fragment_retries', 0)
is_fatal = (lambda idx: idx == 0) if self.params.get('skip_unavailable_fragments', True) else (lambda _: True)
if not pack_func:
pack_func = lambda frag_content, _: frag_content
def download_fragment(fragment, ctx):
frag_index = ctx['fragment_index'] = fragment['frag_index']
headers = info_dict.get('http_headers', {})
byte_range = fragment.get('byte_range')
if byte_range:
headers['Range'] = 'bytes=%d-%d' % (byte_range['start'], byte_range['end'] - 1)
# Never skip the first fragment
fatal = is_fatal(fragment.get('index') or (frag_index - 1))
count, frag_content = 0, None
while count <= fragment_retries:
try:
success, frag_content = self._download_fragment(ctx, fragment['url'], info_dict, headers)
if not success:
return False, frag_index
break
except compat_urllib_error.HTTPError as err:
# Unavailable (possibly temporary) fragments may be served.
# First we try to retry then either skip or abort.
# See https://github.com/ytdl-org/youtube-dl/issues/10165,
# https://github.com/ytdl-org/youtube-dl/issues/10448).
count += 1
if count <= fragment_retries:
self.report_retry_fragment(err, frag_index, count, fragment_retries)
except DownloadError:
# Don't retry fragment if error occurred during HTTP downloading
# itself since it has own retry settings
if not fatal:
break
raise
if count > fragment_retries:
if not fatal:
return False, frag_index
ctx['dest_stream'].close()
self.report_error('Giving up after %s fragment retries' % fragment_retries)
return False, frag_index
return frag_content, frag_index
def decrypt_fragment(fragment, frag_content):
decrypt_info = fragment.get('decrypt_info')
if not decrypt_info or decrypt_info['METHOD'] != 'AES-128':
return frag_content
iv = decrypt_info.get('IV') or compat_struct_pack('>8xq', fragment['media_sequence'])
decrypt_info['KEY'] = decrypt_info.get('KEY') or self.ydl.urlopen(
self._prepare_url(info_dict, info_dict.get('_decryption_key_url') or decrypt_info['URI'])).read()
# Don't decrypt the content in tests since the data is explicitly truncated and it's not to a valid block
# size (see https://github.com/ytdl-org/youtube-dl/pull/27660). Tests only care that the correct data downloaded,
# not what it decrypts to.
if self.params.get('test', False):
return frag_content
return AES.new(decrypt_info['KEY'], AES.MODE_CBC, iv).decrypt(frag_content)
def append_fragment(frag_content, frag_index, ctx):
if not frag_content:
if not is_fatal(frag_index - 1):
self.report_skip_fragment(frag_index)
return True
else:
ctx['dest_stream'].close()
self.report_error(
'fragment %s not found, unable to continue' % frag_index)
return False
self._append_fragment(ctx, pack_func(frag_content, frag_index))
return True
max_workers = self.params.get('concurrent_fragment_downloads', 1)
if can_threaded_download and max_workers > 1:
def _download_fragment(fragment):
ctx_copy = ctx.copy()
frag_content, frag_index = download_fragment(fragment, ctx_copy)
return fragment, frag_content, frag_index, ctx_copy.get('fragment_filename_sanitized')
self.report_warning('The download speed shown is only of one thread. This is a known issue and patches are welcome')
with concurrent.futures.ThreadPoolExecutor(max_workers) as pool:
for fragment, frag_content, frag_index, frag_filename in pool.map(_download_fragment, fragments):
ctx['fragment_filename_sanitized'] = frag_filename
ctx['fragment_index'] = frag_index
result = append_fragment(decrypt_fragment(fragment, frag_content), frag_index, ctx)
if not result:
return False
else:
for fragment in fragments:
frag_content, frag_index = download_fragment(fragment, ctx)
result = append_fragment(decrypt_fragment(fragment, frag_content), frag_index, ctx)
if not result:
return False
self._finish_frag_download(ctx)
return True
+66 -203
View File
@@ -1,32 +1,18 @@
from __future__ import unicode_literals from __future__ import unicode_literals
import errno
import re import re
import io import io
import binascii import binascii
try:
from Crypto.Cipher import AES
can_decrypt_frag = True
except ImportError:
can_decrypt_frag = False
try:
import concurrent.futures
can_threaded_download = True
except ImportError:
can_threaded_download = False
from ..downloader import _get_real_downloader from ..downloader import _get_real_downloader
from .fragment import FragmentFD from .fragment import FragmentFD, can_decrypt_frag
from .external import FFmpegFD from .external import FFmpegFD
from ..compat import ( from ..compat import (
compat_urllib_error,
compat_urlparse, compat_urlparse,
compat_struct_pack,
) )
from ..utils import ( from ..utils import (
parse_m3u8_attributes, parse_m3u8_attributes,
sanitize_open,
update_url_query, update_url_query,
bug_reports_message, bug_reports_message,
) )
@@ -151,10 +137,6 @@ class HlsFD(FragmentFD):
extra_state = ctx.setdefault('extra_state', {}) extra_state = ctx.setdefault('extra_state', {})
fragment_retries = self.params.get('fragment_retries', 0)
skip_unavailable_fragments = self.params.get('skip_unavailable_fragments', True)
test = self.params.get('test', False)
format_index = info_dict.get('format_index') format_index = info_dict.get('format_index')
extra_query = None extra_query = None
extra_param_to_segment_url = info_dict.get('extra_param_to_segment_url') extra_param_to_segment_url = info_dict.get('extra_param_to_segment_url')
@@ -258,7 +240,7 @@ class HlsFD(FragmentFD):
media_sequence += 1 media_sequence += 1
# We only download the first fragment during the test # We only download the first fragment during the test
if test: if self.params.get('test', False):
fragments = [fragments[0] if fragments else None] fragments = [fragments[0] if fragments else None]
if real_downloader: if real_downloader:
@@ -268,194 +250,75 @@ class HlsFD(FragmentFD):
# TODO: Make progress updates work without hooking twice # TODO: Make progress updates work without hooking twice
# for ph in self._progress_hooks: # for ph in self._progress_hooks:
# fd.add_progress_hook(ph) # fd.add_progress_hook(ph)
success = fd.real_download(filename, info_copy) return fd.real_download(filename, info_copy)
if not success:
return False
else:
def download_fragment(fragment):
frag_index = fragment['frag_index']
frag_url = fragment['url']
decrypt_info = fragment['decrypt_info']
byte_range = fragment['byte_range']
media_sequence = fragment['media_sequence']
ctx['fragment_index'] = frag_index 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
count = 0 dedup_window = extra_state.setdefault('webvtt_dedup_window', [])
headers = info_dict.get('http_headers', {}) cue = block.as_json
if byte_range:
headers['Range'] = 'bytes=%d-%d' % (byte_range['start'], byte_range['end'] - 1)
while count <= fragment_retries:
try:
success, frag_content = self._download_fragment(
ctx, frag_url, info_dict, headers)
if not success:
return False, frag_index
break
except compat_urllib_error.HTTPError as err:
# Unavailable (possibly temporary) fragments may be served.
# First we try to retry then either skip or abort.
# See https://github.com/ytdl-org/youtube-dl/issues/10165,
# https://github.com/ytdl-org/youtube-dl/issues/10448).
count += 1
if count <= fragment_retries:
self.report_retry_fragment(err, frag_index, count, fragment_retries)
if count > fragment_retries:
ctx['dest_stream'].close()
self.report_error('Giving up after %s fragment retries' % fragment_retries)
return False, frag_index
if decrypt_info['METHOD'] == 'AES-128': # skip the cue if an identical one appears
iv = decrypt_info.get('IV') or compat_struct_pack('>8xq', media_sequence) # in the window of potential duplicates
decrypt_info['KEY'] = decrypt_info.get('KEY') or self.ydl.urlopen( # and prune the window of unviable candidates
self._prepare_url(info_dict, info_dict.get('_decryption_key_url') or decrypt_info['URI'])).read() i = 0
# Don't decrypt the content in tests since the data is explicitly truncated and it's not to a valid block skip = True
# size (see https://github.com/ytdl-org/youtube-dl/pull/27660). Tests only care that the correct data downloaded, while i < len(dedup_window):
# not what it decrypts to. window_cue = dedup_window[i]
if not test: if window_cue == cue:
frag_content = AES.new( break
decrypt_info['KEY'], AES.MODE_CBC, iv).decrypt(frag_content) if window_cue['end'] >= cue['start']:
i += 1
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 continue
del dedup_window[i]
# 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):
fatal = frag_index == 1 or not skip_unavailable_fragments
if frag_content:
fragment_filename = '%s-Frag%d' % (ctx['tmpfilename'], frag_index)
try:
file, frag_sanitized = sanitize_open(fragment_filename, 'rb')
ctx['fragment_filename_sanitized'] = frag_sanitized
file.close()
frag_content = pack_fragment(frag_content, frag_index)
self._append_fragment(ctx, frag_content)
return True
except EnvironmentError as ose:
if ose.errno != errno.ENOENT:
raise
# FileNotFoundError
if not fatal:
self.report_skip_fragment(frag_index)
return True
else: else:
ctx['dest_stream'].close() skip = False
self.report_error(
'fragment %s not found, unable to continue' % frag_index)
return False
else:
if not fatal:
self.report_skip_fragment(frag_index)
return True
else:
ctx['dest_stream'].close()
self.report_error(
'fragment %s not found, unable to continue' % frag_index)
return False
max_workers = self.params.get('concurrent_fragment_downloads', 1) if skip:
if can_threaded_download and max_workers > 1: continue
self.report_warning('The download speed shown is only of one thread. This is a known issue')
_download_fragment = lambda f: (f, download_fragment(f)[1])
with concurrent.futures.ThreadPoolExecutor(max_workers) as pool:
futures = [pool.submit(_download_fragment, fragment) for fragment in fragments]
# timeout must be 0 to return instantly
done, not_done = concurrent.futures.wait(futures, timeout=0)
try:
while not_done:
# Check every 1 second for KeyboardInterrupt
freshly_done, not_done = concurrent.futures.wait(not_done, timeout=1)
done |= freshly_done
except KeyboardInterrupt:
for future in not_done:
future.cancel()
# timeout must be none to cancel
concurrent.futures.wait(not_done, timeout=None)
raise KeyboardInterrupt
for fragment, frag_index in map(lambda x: x.result(), futures): # add the cue to the window
fragment_filename = '%s-Frag%d' % (ctx['tmpfilename'], frag_index) dedup_window.append(cue)
down, frag_sanitized = sanitize_open(fragment_filename, 'rb') elif isinstance(block, webvtt.Magic):
fragment['fragment_filename_sanitized'] = frag_sanitized # take care of MPEG PES timestamp overflow
frag_content = down.read() if block.mpegts is None:
down.close() block.mpegts = 0
result = append_fragment(frag_content, frag_index) extra_state.setdefault('webvtt_mpegts_adjust', 0)
if not result: block.mpegts += extra_state['webvtt_mpegts_adjust'] << 33
return False if block.mpegts < extra_state.get('webvtt_mpegts_last', 0):
else: extra_state['webvtt_mpegts_adjust'] += 1
for fragment in fragments: block.mpegts += 1 << 33
frag_content, frag_index = download_fragment(fragment) extra_state['webvtt_mpegts_last'] = block.mpegts
result = append_fragment(frag_content, frag_index)
if not result:
return False
self._finish_frag_download(ctx) if frag_index == 1:
return True 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')
else:
pack_fragment = None
return self.download_and_append_fragments(ctx, fragments, info_dict, pack_fragment)

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