Compare commits

...

269 Commits

Author SHA1 Message Date
pukkandan
1117579b94 [version] update
:ci skip all
2021-10-22 20:47:18 +00:00
pukkandan
0676afb126 Release 2021.10.22 2021-10-23 02:09:15 +05:30
pukkandan
49a57e70a9 [cleanup] misc 2021-10-23 02:09:10 +05:30
pukkandan
457f6d6866 [vlive:channel] Fix extraction
Based on https://github.com/ytdl-org/youtube-dl/pull/29866
Closes #749, #927, https://github.com/ytdl-org/youtube-dl/issues/29837
Authored by kikuyan, pukkandan
2021-10-22 23:19:38 +05:30
pukkandan
ad0090d0d2 [cookies] Local State should be opened as utf-8
Closes #1276
2021-10-22 23:19:37 +05:30
makeworld
d183af3cc1 [CBC] Support CBC Gem member content (#1294)
Authored by: makeworld-the-better-one
2021-10-22 06:28:32 +05:30
makeworld
3c239332b0 [CBC] Fix Gem livestream (#1289)
Authored by: makeworld-the-better-one
2021-10-22 06:26:29 +05:30
u-spec-png
ab2ffab22d [Instagram] Add login (#1288)
Authored by: u-spec-png
2021-10-22 06:23:45 +05:30
zenerdi0de
f656a23cb1 [patreon] Fix vimeo player regex (#1332)
Closes #1323
Authored by: zenerdi0de
2021-10-22 06:20:49 +05:30
pukkandan
58ab5cbc58 [vimeo] Fix embedded player.vimeo URL
Closes #1138, partially fixes #1323
Cherry-picked from upstream commit 3ae9c0f410b1d4f63e8bada67dd62a8d2852be32
2021-10-22 06:15:51 +05:30
Damiano Amatruda
17ec8bcfa9 [microsoftstream] Add extractor (#1201)
Based on: https://github.com/ytdl-org/youtube-dl/pull/24649
Fixes: https://github.com/ytdl-org/youtube-dl/issues/24440
Authored by: damianoamatruda, nixklai
2021-10-22 05:34:00 +05:30
u-spec-png
0f6e60bb57 [tagesschau] Fix extractor (#1227)
Closes #1124
Authored by: u-spec-png
2021-10-22 05:09:50 +05:30
pukkandan
ef58c47637 [SponsorBlock] Obey extractor-retries and sleep-requests 2021-10-22 04:42:44 +05:30
pukkandan
19b824f693 Re-implement deprecated option --id
Despite `--title`, `--literal` etc being deprecated,
`--id` is still documented in youtube-dl and so should be kept
2021-10-22 04:42:24 +05:30
jfogelman
f0ded3dad3 [AdobePass] Fix RCN MSO (#1349)
Authored by: jfogelman
2021-10-22 01:06:03 +05:30
pukkandan
733d8e8f99 [build] Refactor pyinst.py and misc cleanup
Closes #1361
2021-10-21 20:11:05 +05:30
pukkandan
386cdfdb5b [build] Release windows exe built with py2exe
Closes: #855
Related: #661, #705, #890, #1024, #1160
2021-10-21 20:11:05 +05:30
pukkandan
6e21fdd279 [build] Enable lazy-extractors in releases
Set the environment variable `YTDLP_NO_LAZY_EXTRACTORS`
to forcefully disable lazy extractor loading
2021-10-21 19:41:33 +05:30
Ricardo
0e5927eebf [build] Build standalone MacOS packages (#1221)
Closes #1075 
Authored by: smplayer-dev
2021-10-21 16:18:46 +05:30
Ashish Gupta
27f817a84b [docs] Migrate issues to use forms (#1302)
Authored by: Ashish0804
2021-10-21 15:26:36 +05:30
pukkandan
d3c93ec2b7 Don't create console for subprocesses on Windows (#1261)
Closes #1251
2021-10-20 21:49:40 +05:30
pukkandan
b4b855ebc7 [fragment] Print error message when skipping fragment 2021-10-19 22:58:26 +05:30
pukkandan
2cda6b401d Revert "[fragments] Pad fragments before decrypting (#1298)"
This reverts commit 373475f035.
2021-10-19 22:58:25 +05:30
pukkandan
aa7785f860 [utils] Standardize timestamp formatting code
Closes #1285
2021-10-19 22:58:25 +05:30
pukkandan
9fab498fbf [http] Retry on socket timeout
Closes #1222
2021-10-19 22:58:24 +05:30
Nil Admirari
e619d8a752 [ModifyChapters] Do not mutate original chapters (#1322)
Closes #1295 
Authored by: nihil-admirari
2021-10-19 14:21:05 +05:30
Zirro
1e520b5535 Add option --no-batch-file (#1335)
Authored by: Zirro
2021-10-19 00:41:07 +05:30
pukkandan
176f1866cb Add HDR information to formats 2021-10-18 18:35:02 +05:30
pukkandan
17bddf3e95 Reduce default --socket-timeout 2021-10-18 16:40:12 +05:30
pukkandan
2d9ec70423 [ModifyChapters] Allow removing sections by timestamp
Eg: --remove-chapters "*10:15-15:00".
The `*` prefix is used so as to avoid any conflicts with other valid regex
2021-10-18 16:06:51 +05:30
pukkandan
e820fbaa6f Do not verify thumbnail URLs by default
Partially reverts cca80fe611 and 0ba692acc8

Unless `--check-formats` is specified, this causes yt-dlp to return incorrect thumbnail urls.
See https://github.com/yt-dlp/yt-dlp/issues/340#issuecomment-877909966, #402

But the overhead in general use is not worth it

Closes #694, #725
2021-10-18 15:44:47 +05:30
pukkandan
b11d210156 [EmbedMetadata] Allow overwriting all default metadata
with `meta_default` key
2021-10-18 10:31:56 +05:30
pukkandan
24b0a72b30 [cleanup] Remove broken youtube login code 2021-10-18 09:25:51 +05:30
coletdjnz
aae16f6ed9 [youtube:comments] Fix comment section not being extracted in new layouts (#1324)
Co-authored-by: coletdjnz, pukkandan
2021-10-18 02:58:42 +00:00
shirt
373475f035 [fragments] Pad fragments before decrypting (#1298)
Closes #197, #1297, #1007
Authored by: shirt-dev
2021-10-18 08:14:20 +05:30
Ashish Gupta
920134b2e5 [Gronkh] Add extractor (#1299)
Closes #1293
Authored by: Ashish0804
2021-10-18 08:11:31 +05:30
Ashish Gupta
72ab768719 [SkyNewsAU] Add extractor (#1308)
Closes #1287
Authored by: Ashish0804
2021-10-18 08:09:50 +05:30
LE
01b052b2b1 [tbs] Add tbs live streams (#1326)
Authored by: llacb47
2021-10-18 07:58:20 +05:30
Ákos Sülyi
019a94f7d6 [utils] Use importlib to load plugins (#1277)
Authored by: sulyi
2021-10-18 07:16:49 +05:30
nyuszika7h
e69585f8c6 [7plus] Add cookie based authentication (#1202)
Closes #1103
Authored by: nyuszika7h
2021-10-18 07:04:56 +05:30
Damiano Amatruda
693ec74401 [on24] Add extractor (#1200)
Authored by: damianoamatruda
2021-10-18 07:02:46 +05:30
pukkandan
239df02103 Make duration_string and resolution available in --match-filter
Related: #1309
2021-10-17 17:39:33 +05:30
pukkandan
18f96d129b [utils] Allow duration strings in filter
Closes #1309
2021-10-17 17:39:33 +05:30
pukkandan
ec3f6640c1 [crunchyroll] Add season to flat-playlist
Closes #1319
2021-10-17 17:39:23 +05:30
pukkandan
dd078970ba [crunchyroll] Add support for beta.crunchyroll URLs
and fix series URLs with language code
2021-10-17 17:38:57 +05:30
pukkandan
71ce444a3f Fix --restrict-filename when used with default template 2021-10-17 01:03:04 +05:30
pukkandan
580d3274e5 [youtube] Expose different formats with same itag 2021-10-16 20:28:17 +05:30
pukkandan
03b4de722a [downloader] Fix slow progress hooks
Closes #1301
2021-10-16 20:02:40 +05:30
pukkandan
48ee10ee8a Fix conflict b/w id and ext in format selection
Closes #1282
2021-10-16 20:02:30 +05:30
Ashish Gupta
6ff34542d2 [Hotstar] Raise appropriate error for DRM 2021-10-16 14:08:52 +05:30
gustaf
e3950399e4 [Viafree] add support for Finland (#1253)
Authored by: 18928172992817182 (gustaf)
2021-10-14 17:34:40 +05:30
Ashish Gupta
974208e151 [trovo] Support channel clips and VODs (#1246)
Closes #229
Authored by: Ashish0804
2021-10-14 17:32:48 +05:30
pukkandan
883d4b1eec [YoutubeDL] Write verbose header to logger 2021-10-14 14:44:30 +05:30
pukkandan
a0c716bb61 [instagram] Show appropriate error when login is needed
Closes #1264
2021-10-14 14:44:29 +05:30
pukkandan
d5a39f0bad [http] Show the last encountered error
Closes #1262
2021-10-14 14:44:28 +05:30
Ashish Gupta
a64907d0ac [Hotstar] Mention Dynamic Range in format id (#1265)
Authored by: Ashish0804
2021-10-14 14:44:14 +05:30
pukkandan
6993f78d1b [extractor,utils] Detect more codecs/mimetypes
Fixes: https://github.com/ytdl-org/youtube-dl/issues/29943
2021-10-13 05:05:29 +05:30
pukkandan
993191c0d5 Fix bug in c111cefa5d 2021-10-13 04:43:26 +05:30
pukkandan
fc5c8b6492 [eria2c] Fix --skip-unavailable fragment 2021-10-13 04:14:12 +05:30
pukkandan
b836dc94f2 [outtmpl] Fix bug in expanding environment variables 2021-10-13 04:14:11 +05:30
pukkandan
c111cefa5d [downloader/ffmpeg] Improve simultaneous download and merge 2021-10-13 04:14:11 +05:30
pukkandan
975a0d0df9 Calculate more fields for merged formats
Closes #947
2021-10-13 04:14:11 +05:30
Ákos Sülyi
a387b69a7c [devscripts/run_tests] Use markers to filter tests (#1258)
`-k` filters using a substring match on test name.
`-m` checks markers for an exact match.
Authored by: sulyi
2021-10-13 00:24:27 +05:30
pukkandan
ecdc9049c0 [YouTube] Add auto-translated subtitles
Closes #1245
2021-10-12 15:21:32 +05:30
pukkandan
7b38649845 Fix verbose head not showing custom configs 2021-10-12 15:21:31 +05:30
pukkandan
e88d44c6ee [cleanup] Cleanup bilibili code
Closes #1169
Authored by pukkandan, u-spec-png
2021-10-12 15:21:31 +05:30
pukkandan
a2160aa45f [extractor] Generalize getcomments implementation 2021-10-12 15:21:30 +05:30
pukkandan
cc16383ff3 [extractor] Simplify search extractors 2021-10-12 15:21:30 +05:30
pukkandan
a903d8285c Fix bug in storyboards
Caused by 9359f3d4f0
2021-10-11 17:27:39 +05:30
pukkandan
9dda99f2fc [Merger] Do not add aac_adtstoasc to non-hls audio 2021-10-11 17:09:28 +05:30
pukkandan
ba10757412 [extractor] Detect EXT-X-KEY Apple FairPlay 2021-10-11 17:09:21 +05:30
pukkandan
e6faf2be36 [update] Clean up error reporting
Closes #1224
2021-10-11 09:58:24 +05:30
pukkandan
ed39cac53d Load archive only after printing verbose head
If there is some issue in loading archive, the verbose head should be visible in the logs
2021-10-11 09:49:52 +05:30
pukkandan
a169858f24 Fix check_formats output being written to stdout when -qv
Closes #1229
2021-10-11 09:49:52 +05:30
pukkandan
0481e266f5 [tiktok] Fix typo in 943d5ab133
and update tests
Closes #1226
2021-10-11 09:49:51 +05:30
Ashish Gupta
2c4bba96ac [EUScreen] Add Extractor (#1219)
Closes #1207
Authored by: Ashish0804
2021-10-11 03:36:27 +05:30
pukkandan
e8f726a57f [hidive] Fix typo in b5ae35ee6d 2021-10-10 11:44:44 +05:30
pukkandan
8063de5109 [version] update
:ci skip all
2021-10-10 04:03:13 +00:00
pukkandan
dec0d56fa9 Release 2021.10.10 2021-10-10 09:32:01 +05:30
pukkandan
21186af70a [downloader] Fix throttledratelimit
The timer should not reset at start of each block
2021-10-10 09:32:00 +05:30
pukkandan
84999521c8 [build] Allow to release without changelog
so that forks can build using GHA easily
2021-10-10 09:32:00 +05:30
pukkandan
d1d5c08f29 [minicurses] Fix when printing to file
Closes #1215
2021-10-10 09:31:59 +05:30
Bojidar Qnkov
2e01ba6218 [NovaPlay] Add extractor (#1209)
Authored by: Bojidarist
2021-10-10 05:41:10 +05:30
pukkandan
c9652aa418 [docs] Remove incorrect dependency on VC++10
Closes #1163
2021-10-10 04:47:48 +05:30
pukkandan
91b6c884c9 Revert "[ffmpeg] Set max probesize to workaround AAC HLS stream issues (#1109)"
This reverts commit 250a938de8.

This is no longer necessary since 7687c8ac6e
2021-10-10 04:47:48 +05:30
Felix S
28fe35b4e3 [francetv] Update extractor (#1096)
Original PR: https://github.com/ytdl-org/youtube-dl/pull/29996
Closes: https://github.com/yt-dlp/yt-dlp/issues/970, https://github.com/ytdl-org/youtube-dl/issues/29956, https://github.com/ytdl-org/youtube-dl/issues/29957, https://github.com/ytdl-org/youtube-dl/issues/29969, https://github.com/ytdl-org/youtube-dl/issues/29990, https://github.com/ytdl-org/youtube-dl/issues/30010

Authored by: fstirlitz, sarnoud
2021-10-10 03:20:17 +05:30
pukkandan
aa9a92fdbb [downloader/ffmpeg] Fix bug in initializing FFmpegPostProcessor
When `FFmpegFD` initializes the PP, it passes `self` as the `downloader`
But it does not have a `_postprocessor_hooks` attribute

Closes #1211
2021-10-10 02:23:50 +05:30
pukkandan
a170527e1f [version] update
:ci skip all
2021-10-09 19:11:24 +00:00
pukkandan
90d55df330 Release 2021.10.09 2021-10-10 00:40:35 +05:30
Ashish Gupta
81bcd43a03 [HotStarSeries] Fix cookies (#1187)
Authored by: Ashish0804
2021-10-09 23:57:08 +05:30
pukkandan
b5ae35ee6d [cleanup] Misc cleanup 2021-10-09 22:32:00 +05:30
pukkandan
4e3b637d5b Merge webm formats into mkv if thumbnails are to be embedded
This was originally implemented in 4d971a16b8 (#173) by @damianoamatruda
but was reverted in 3b297919e0
since it was unintentionally being triggered for `write_thumbnail` (See #500)
2021-10-09 22:19:23 +05:30
Jules-A
8cd69fc407 [Funimation] Fix for /v/ urls (#1196)
Closes #993 
Authored by: pukkandan, Jules-A
2021-10-09 20:51:41 +05:30
pukkandan
2614f64600 [utils] Let traverse_obj accept functions as keys 2021-10-09 20:49:07 +05:30
pukkandan
b922db9fe5 [http] Respect user-provided chunk size over extractor's 2021-10-09 20:49:07 +05:30
pukkandan
f2cad2e496 [Hidive] Fix subtitles broken by 705e7c2005 2021-10-09 20:49:00 +05:30
u-spec-png
d6124e191e [bilibili] Fix bug in efc947fb3e
Authored by: u-spec-png
2021-10-09 07:34:02 +05:30
timethrow
8c6f4daa4c [docs] Write embedding and contributing documentation (#528)
Authored by: pukkandan, timethrow
2021-10-09 06:38:01 +05:30
coletdjnz
ac56cf38a4 [youtube:tab] Fallback to API when webpage fails to download (#1122)
and add some extractor_args to force this mode
Authored by: coletdjnz
2021-10-09 02:49:25 +05:30
Damiano Amatruda
c08b8873ea [ciscowebex] Add extractor (#1199)
Authored by: damianoamatruda
2021-10-09 01:06:27 +05:30
pukkandan
819e05319b Improved progress reporting (See desc) (#1125)
* Separate `--console-title` and `--no-progress`
* Add option `--progress` to show progress-bar even in quiet mode
* Fix and refactor `minicurses`
* Use `minicurses` for all progress reporting
* Standardize use of terminal sequences and enable color support for windows 10
* Add option `--progress-template` to customize progress-bar and console-title
* Add postprocessor hooks and progress reporting

Closes: #906, #901, #1085, #1170
2021-10-09 00:41:59 +05:30
u-spec-png
fee3f44f5f [Streamable] Add codecs (#1189)
Authored by: u-spec-png
2021-10-07 20:02:42 +05:30
pukkandan
705e7c2005 [Hidive] Fix duplicate and incorrect formats 2021-10-06 11:23:48 +05:30
pukkandan
49e7e9c3ce [docs,build] Change all pycryptodome references to pycryptodomex 2021-10-06 06:45:45 +05:30
pukkandan
8472674399 [FixupM3u8] Do not run if merge is needed
We pass the relevant arguments to the merger, so separate fixup in redundant
2021-10-06 05:45:19 +05:30
pukkandan
1276a43a77 [youtube] Fix non-fatal errors in fetching player 2021-10-06 05:45:19 +05:30
pukkandan
519804a92f bugfix for 80c03fa98f 2021-10-06 05:45:18 +05:30
pukkandan
1b6bb4a85a [reddit] bugfix for 8e3fd7e034 2021-10-06 05:45:18 +05:30
pukkandan
644149afec [soundcloud:playlist] Detect last page correctly
Closes #1168
2021-10-06 05:45:17 +05:30
pukkandan
4e3d1898a8 Workaround ssl errors in mingw python
Closes #1151
2021-10-06 05:45:16 +05:30
shirt
f85e6be42e [build] Use pycryptodomex for PyInstaller (#1179) 2021-10-05 13:37:58 -04:00
coletdjnz
762e509d91 [Mediaite] Relax valid url (#1158)
Closes #1131
Authored by: coletdjnz
2021-10-05 01:00:57 +05:30
i6t
d92125aeba [GoPro] Add extractor (#1167)
Fixes: https://github.com/ytdl-org/youtube-dl/issues/30044
Authored by: i6t
2021-10-05 00:53:37 +05:30
makeworld
0f0ac87be3 [CBC] Cleanup tests (#1162)
Related: #1013 
Authored by: makeworld-the-better-one
2021-10-05 00:41:00 +05:30
u-spec-png
755203fc3f [parliamentlive.tv] Fix extractor (#1153)
Closes #1139 
Authored by: u-spec-png
2021-10-05 00:39:00 +05:30
MinePlayersPE
943d5ab133 [Douyin] Rewrite extractor (#1157)
Closes #1121
Authored by: MinePlayersPE
2021-10-05 00:31:33 +05:30
u-spec-png
3001a84dca [Newgrounds] Add age_limit and fix duration (#1156)
Authored by: u-spec-png
2021-10-05 00:28:02 +05:30
u-spec-png
ebf2fb4d61 [Vupload] Add extractor (#1146)
Fixes: https://github.com/ytdl-org/youtube-dl/issues/29877
Authored by: u-spec-png
2021-10-05 00:12:24 +05:30
u-spec-png
efc947fb3e [Bilibili] Add subtitle converter (#1144)
Closes #1015
Based on https://github.com/y2361547758/bcc2ass
Authored by: u-spec-png
2021-10-05 00:07:05 +05:30
pukkandan
b11c04a8ae Fix -f mp4 behaving differently from youtube-dl 2021-10-04 03:08:28 +05:30
pukkandan
5d535b4a55 [build] Allow building with py2exe (and misc fixes)
py2exe config is copied from youtube-dl
Closes #1160
2021-10-04 03:08:27 +05:30
pukkandan
a1c3967307 [EmbedSubtitle, SubtitlesConvertor] Fix error when subtitle file is missing
Closes #1152, #1134
Bug from 8e25d624df
2021-10-04 03:08:26 +05:30
pukkandan
e919569e67 [funimation] Sort formats according to the relevant extractor-args 2021-10-04 03:08:26 +05:30
Ákos Sülyi
ff1dec819a [aes] Improve performance slightly (#1135)
Authored by: sulyi
2021-10-03 00:20:39 +05:30
Felix S
9359f3d4f0 [extractor] Extract storyboards from SMIL manifests (#1128)
Authored by: fstirlitz
2021-10-03 00:13:42 +05:30
Aleri Kaisattera
0eaec13ba6 [Theta] Add video extractor (#1137)
Authored by: alerikaisattera
2021-10-02 00:15:15 +05:30
jfogelman
ad095c4283 [adobepass] Add RCN as MSO (#1129)
Authored by: jfogelman
2021-09-30 21:14:20 +05:30
pukkandan
e6f21b3d92 [docs,cleanup] Some minor refactoring and improve docs 2021-09-30 03:32:52 +05:30
pukkandan
d710cc6d36 [docs] Add note about our custom ffmpeg builds 2021-09-30 03:32:49 +05:30
pukkandan
3ae5e79774 [postprocessor] Add plugin support
Adds option `--use-postprocessor` to enable them
2021-09-30 03:32:46 +05:30
pukkandan
8e3fd7e034 [reddit] Fix 429 by generating a random reddit_session
Related: a76e2e0f88, #1014, https://github.com/ytdl-org/youtube-dl/issues/29986
Original PR: https://github.com/ytdl-org/youtube-dl/pull/30017
Authored by: AjaxGb
2021-09-30 03:32:44 +05:30
pukkandan
80c03fa98f Allow empty output template to skip a type of file
Closes #760, #1111
2021-09-30 03:32:43 +05:30
pukkandan
1f2a268bd3 [embedsubtitle] Fix error when duration is unknown 2021-09-30 03:32:41 +05:30
pukkandan
804ca01cc7 [build] Add more files to the tarball
Closes #1099
2021-09-30 03:32:38 +05:30
i6t
851876095b [Gettr] Add extractor (#1120)
Fixes: https://github.com/ytdl-org/youtube-dl/issues/29589
Authored by: i6t
2021-09-29 15:53:56 +05:30
ajj8
2d997542ca [bbc] Extract better quality videos (#1113)
mobile-tablet-main only provides 540p25, so it shouldn't be used for the first attempt. Instead pc provides up to 720p50

Authored by: ajj8
2021-09-29 04:07:33 +05:30
pukkandan
7756277882 Workaround for bug in ssl.SSLContext.load_default_certs (#1118)
* Remove old compat code
* Load certificates only when not using nocheckcertificate
* Load each certificate individually

Closes #1060
Related bugs.python.org/issue35665, bugs.python.org/issue4531
2021-09-29 03:07:23 +05:30
shirt
7687c8ac6e [HLS] Fix decryption issues (#1117)
* Unpad HLS fragments with PKCS#7 according to datatracker.ietf.org/doc/html/rfc8216
* media_sequence should only be incremented in for media fragments
* The native decryption should only be used if ffmpeg is unavailable since it is significantly slower. Closes #1086

Authored by: shirt-dev, pukkandan
2021-09-29 00:23:24 +05:30
Ashish Gupta
80c360d7aa [LinkedInLearning] Fix newline bug in subtitles (#1104)
Authored by: Ashish0804
2021-09-28 16:06:31 +05:30
shirt
250a938de8 [ffmpeg] Set max probesize to workaround AAC HLS stream issues (#1109)
Fixes: #618, #998, #1039

Authored by: shirt-dev
2021-09-28 04:12:33 +05:30
Ashish Gupta
f1d42a83ab [Rumble] Add RumbleChannelIE (#1088)
Authored by: Ashish0804
2021-09-28 02:31:23 +05:30
ChillingPepper
3cf4b91dc5 [SovietsCloset] Add duration from m3u8 (#908)
Authored by: ChillingPepper
2021-09-28 02:30:41 +05:30
u-spec-png
fecb20a503 [N1] Add extractor (#1080)
Authored by: u-spec-png
2021-09-28 01:40:51 +05:30
pukkandan
360167b9fc Fix --flat-playlist when neither IE nor id is known 2021-09-27 11:29:17 +05:30
pukkandan
28234287f1 [update] Check for new version even if not updateable 2021-09-27 11:29:17 +05:30
pukkandan
91dd88b90f [outtmpl] Alternate form of format type l for \n delimited list 2021-09-27 11:29:16 +05:30
Aleri Kaisattera
d31dab7084 [vidme] Remove extractor (#1095)
Authored by: alerikaisattera
2021-09-27 07:42:44 +05:30
u-spec-png
c470901ccf [reddit] Add embedded url (#1090)
Authored by: u-spec-png
2021-09-26 18:58:22 +05:30
i6t
2333ea1029 [Veo] Add extractor (#1084)
Fixes: https://github.com/ytdl-org/youtube-dl/issues/29445
Authored by: i6t
2021-09-26 04:09:45 +05:30
u-spec-png
9a13345439 [PolskieRadio] Fix extractors (#1082)
Closes #1033
Authored by: jakubadamw, u-spec-png
2021-09-26 04:00:22 +05:30
pukkandan
524e2e4fda [outtmpl] Format type U for unicode normalization 2021-09-26 01:41:01 +05:30
Matt Broadway
f440b14f87 [cookies] Fix keyring fallback (#1078)
The password returned by `security find-generic-password` has a newline at the end

Closes #1073
Authored by: mbway
2021-09-25 21:04:16 +05:30
Ashish Gupta
8dc831f715 [LinkedInLearning] Add subtitles (#1077)
Authored by: Ashish0804
Closes #1072
2021-09-25 16:55:33 +05:30
u-spec-png
e99b2d2771 [Newgrounds] Fix view count on songs (#1071)
Authored by: u-spec-png
2021-09-25 06:42:30 +05:30
pukkandan
1fed277349 [version] update
:ci skip all
2021-09-25 00:59:59 +00:00
pukkandan
0ef787d773 Release 2021.09.25 2021-09-25 06:28:05 +05:30
pukkandan
a5de4099cb [build] Fix brew tap 2021-09-25 06:28:05 +05:30
pukkandan
ff1c7fc9d3 Allow 0 in --playlist-items 2021-09-25 03:31:35 +05:30
pukkandan
600e900300 [zdf] Improve format sorting
Closes #910
2021-09-24 07:47:00 +05:30
f4pp3rk1ng
20b91b9b63 [SpankBang] Fix uploader (#892)
Closes #833 
Authored by: f4pp3rk1ng, coletdjnz
2021-09-24 06:36:30 +05:30
pukkandan
4c88ff87fc [build] Improve release process (#880)
* Automate more of the release process by animelover1984, pukkandan - closes #823
* Fix sha256 by nihil-admirari - closes #385
* Bring back brew taps by nao20010128nao #865
* Provide `--onedir` zip for windows by pukkandan - Closes #1024, #661, #705 and #890

Authored by: pukkandan, animelover1984, nihil-admirari, nao20010128nao
2021-09-24 06:31:43 +05:30
renalid
e27cc5d864 [Arte] Improve description extraction (#1046)
Authored by: renalid
2021-09-24 06:26:15 +05:30
Aleri Kaisattera
eb6d4ad1ca [Theta] Add extractor (#1068)
Authored by: alerikaisattera
2021-09-24 06:23:51 +05:30
coletdjnz
99e9e001de [youtube] Cleanup authentication code (#786)
Authored by: coletdjnz
2021-09-24 06:22:17 +05:30
pukkandan
51ff9ca0b0 [xattr] bugfix for b19404591a 2021-09-24 06:20:42 +05:30
pukkandan
b19404591a Separate the options --ignore-errors and --no-abort-on-error
In youtube-dl, `-i` ignores both download and post-processing error, and
treats the download as successful even if the post-processor fails.

yt-dlp used to skip the entire video on either error and there was no
option to ignore the post-processing errors like youtube-dl does.

By splitting the option into two, now either just the download errors
(--no-abort-on-error, default on CLI) or all errors (--ignore-errors)
can be ignored as per the users' needs

Closes #893
2021-09-24 06:05:35 +05:30
pukkandan
1f8471e22c Ignore empty entries in _list_from_options_callback 2021-09-24 05:14:19 +05:30
pukkandan
77c4a9ef68 Download subtitles in order of --sub-langs
Closes #1041
2021-09-24 05:14:19 +05:30
pukkandan
8f70b0b82f [cbs] Report appropriate error for DRM
Closes #1056
2021-09-24 05:14:18 +05:30
pukkandan
be867b03f5 bugfix for bd50a52b0d 2021-09-24 05:14:16 +05:30
pukkandan
1813a6ccd4 [youtube] Fix --mark-watched with --cookies-from-browser
Closes #1019
2021-09-24 05:14:16 +05:30
pukkandan
8100c77223 [lbry] Show error message from API response 2021-09-24 05:14:15 +05:30
Ashish Gupta
9ada988bfc [Koo] Add extractor (#1044)
Authored by: Ashish0804
2021-09-23 23:45:17 +05:30
Ashish Gupta
d1a7768432 [Chingari] Add extractors (#1038)
Authored by: Ashish0804
2021-09-23 23:31:55 +05:30
NeroBurner
49fa4d9af7 [atv.at] Use jwt for API (#1012)
The jwt token is implemented according to RFC7519

Closes #988
Authored by: NeroBurner
2021-09-23 23:10:51 +05:30
The Hatsune Daishi
ee2b3563f3 [downloader/niconico] Pass custom headers (#1063)
Closes #1057
Authored by: nao20010128nao
2021-09-23 14:36:48 +05:30
Glenn Slayden
bdc196a444 [cleanup] Fix line endings for nebula.py (#1064)
:ci skip
Authored by: glenn-slayden
2021-09-23 14:35:01 +05:30
Ashish Gupta
388bc4a640 [Hotstar] Add referer for subs (#1062)
Authored by: Ashish0804
2021-09-23 14:30:49 +05:30
pukkandan
50eff38c1c bugfix for a21e0ab1a1
Closes #1061
2021-09-23 11:49:00 +05:30
nixxo
4be9dbdc24 [comedycentral] Support collection-playlist (#1058)
Authored by: nixxo
2021-09-23 11:45:54 +05:30
pukkandan
a21e0ab1a1 [ffmpeg] Add aac_adtstoasc when merging if needed
Related: #1039
2021-09-22 19:51:58 +05:30
pukkandan
a76e2e0f88 [reddit] Workaround for 429 by redirecting to old.reddit.com
Closes #1014
2021-09-22 19:51:57 +05:30
The Hatsune Daishi
bd50a52b0d Basic framework for simultaneous download of multiple formats (#1036)
Authored by: nao20010128nao
2021-09-22 19:42:04 +05:30
Sipherdrakon
c12977bdc4 [AnimalPlanet] Fix extractor (#1050)
Authored by: Sipherdrakon
2021-09-22 19:39:45 +05:30
ChillingPepper
f6d8776d34 [SovietsCloset] Fix playlists for games with only named categories
Authored by: ConquerorDopy
2021-09-22 07:40:02 +05:30
pukkandan
d806c9fd97 [docs,cleanup] Add deprecation warning in docs
for some counter intuitive behaviour that may be removed in future.

and fix linter
2021-09-22 05:50:11 +05:30
pukkandan
5e3f2f8fc4 [youtube] Return full URL instead of just ID 2021-09-22 05:37:41 +05:30
pukkandan
1009f67c2a [fragment,aria2c] Generalize and refactor some code 2021-09-22 05:27:07 +05:30
pukkandan
bd6f722de8 dump files should obey --trim-filename (#1043)
Authored by: sulyi
2021-09-22 05:25:17 +05:30
pukkandan
d9d8b85747 [fragment] Fix range header when using -N and media sequence (#1048)
Authored by: shirt
2021-09-22 04:19:45 +05:30
pukkandan
daf7ac2b92 [fragment] Avoid repeated request for AES key 2021-09-22 01:15:16 +05:30
pukkandan
96933fc1b6 [aria2c] Fix IV for some AES-128 streams
Authored by: shirt
2021-09-22 00:20:41 +05:30
makeworld
0d32e124c6 [CBC] Fix CBC Gem extractors (#1013)
Closes #936
Authored by: makeworld-the-better-one
2021-09-20 03:43:26 +05:30
u-spec-png
cb2ec90e91 [Peertube] Add channel extractor (#1023)
Authored by: u-spec-png
2021-09-19 23:17:41 +05:30
pukkandan
3cd786dbd7 [youtube] Warn when trying to download clips 2021-09-19 19:41:10 +05:30
pukkandan
1b629e1b4c [test/cookies] Improve logging 2021-09-19 19:41:09 +05:30
u-spec-png
8f8e8eba24 [Nuvid] Fix extractor (#1022)
Fixes: https://github.com/ytdl-org/youtube-dl/issues/29886
Authored by: u-spec-png
2021-09-19 17:56:29 +05:30
Ákos Sülyi
09906f554d [aes] Add aes_gcm_decrypt_and_verify (#1020)
Authored by: sulyi, pukkandan
2021-09-19 17:52:31 +05:30
Yuan Chao
a63d9bd0b0 [CGTN] Add extractor (#981)
Authored by: chao813
2021-09-19 17:48:22 +05:30
pukkandan
f137e4c27c [utils] Improve extract_timezone
Code taken from: https://github.com/ytdl-org/youtube-dl/pull/29845
Fixes: https://github.com/ytdl-org/youtube-dl/issues/29948
Authored by: dirkf
2021-09-19 17:45:49 +05:30
nyuszika7h
4762621925 [videa] Fix some extraction errors (#1028)
Authored by: nyuszika7h
2021-09-19 17:07:50 +05:30
pukkandan
57aa7b8511 [hls] Byterange + AES128 is supported by native downloader 2021-09-19 14:20:54 +05:30
pukkandan
9c1c3ec016 [Oreilly] Bugfix for 7738bd3272 2021-09-19 14:20:53 +05:30
DigitalDJ
f9cc0161e6 [extractor] Fix root-relative URLs in MPD (#1006)
Authored by: DigitalDJ
2021-09-19 14:07:57 +05:30
Nil Admirari
c6af2dd8e5 [SponsorBlock] Improve merge algorithm (#999)
Authored by: nihil-admirari
2021-09-19 08:38:50 +05:30
Mohammad Khaled AbouElSherbini
7738bd3272 [Oreilly] Handle new web url (#990)
The change in URL is most likely a server side issue. But we can work around it by a simple substitution

Authored by: MKSherbini
2021-09-18 17:03:06 +05:30
pukkandan
7c37ff97d3 Allow alternate fields in outtmpl
Closes #899, #1004
2021-09-18 16:41:01 +05:30
The Hatsune Daishi
d47f46e17e [damtomo] Add extractor (#992)
Authored by: nao20010128nao
2021-09-18 11:25:17 +05:30
coletdjnz
298bf1d275 [itv] Prefer last matching featureset (#1001)
Bug fix for #986
Authored by: coletdjnz
2021-09-18 02:25:49 +05:30
Aleri Kaisattera
d1b39ad844 [CAM4] Add extractor (#1010)
Authored by: alerikaisattera
2021-09-18 02:24:17 +05:30
pukkandan
edf65256aa [hls,aes] Fallback to native implementation for AES-CBC
and detect `Cryptodome` in addition to `Crypto`

Closes #935
Related: #938
2021-09-18 00:55:58 +05:30
pukkandan
7303f84abe [options] Fix --no-config and refactor reading of config files
Closes #912, #914
2021-09-18 00:11:11 +05:30
pukkandan
f5aa5cfbff Add format type B for outtmpl to treat the value as bytes
This is useful to limit the filename to a certain number of bytes rather than characters
Closes #1003
2021-09-18 00:11:11 +05:30
Aleri Kaisattera
f1f6ca78b4 [Streamanity] Add Extractor (#984)
Authored by: alerikaisattera
2021-09-16 23:45:10 +05:30
Ashish Gupta
2fac2e9136 [Mediaite] Add Extractor (#973)
Closes #969 
Authored by: Ashish0804
2021-09-16 23:42:45 +05:30
Ashish Gupta
23dd2d9a32 [NDR] Rewrite NDRIE (#962)
Closes #959 
Authored by: Ashish0804
2021-09-16 23:41:55 +05:30
Ashish Gupta
b89378a69a [globo] Fix GloboIE (#994)
Closes #991 
Authored by: Ashish0804
2021-09-16 23:01:39 +05:30
pukkandan
0001fcb586 Add option --netrc-location
Closes #792, #963
2021-09-16 01:28:55 +05:30
pukkandan
c589c1d395 [compat] Don't ignore HOME (if set) on windows
Related: #792
2021-09-16 01:28:54 +05:30
pukkandan
f7590d4764 [vrv] Don't raise error when thumbnails are missing
Closes #983
2021-09-16 01:28:53 +05:30
pukkandan
dbf7eca917 [soundcloud] Update _CLIENT_ID
Related: #975
2021-09-16 01:28:52 +05:30
pukkandan
d21bba7853 [options] Strip spaces in list-like switches 2021-09-16 01:28:51 +05:30
Ashish Gupta
a8cb7eca61 [HiDive] Fix extractor (#958)
Closes #952, #408
Authored by: Ashish0804
2021-09-15 07:34:54 +05:30
nyuszika7h
92790da2bb [radlive] Add new extractor (#870)
Closes #312
Authored by: nyuszika7h
2021-09-15 07:15:10 +05:30
Sipherdrakon
b5a39ed43b [DIYNetwork] Support new format (#934)
Authored by: Sipherdrakon
2021-09-15 05:55:03 +05:30
LE
cc33cc4395 [VrtNU] Handle login errors (#977)
Authored by: llacb47
2021-09-15 02:28:49 +05:30
Ashish Gupta
1722099ded [Mxplayer] Use mobile API (#966)
Authored by: Ashish0804
2021-09-15 02:23:36 +05:30
Ákos Sülyi
40b18348e7 [cleanup] Improve make clean-test (#972)
Authored by: sulyi
2021-09-14 23:53:47 +05:30
u-spec-png
e9a30b181e [Peertube] Add playlist extractor (#957)
Authored by: u-spec-png
2021-09-14 09:25:26 +05:30
zenerdi0de
9c95ac677e [Fancode] Fix live streams (#961)
Authored by: zenerdi0de
2021-09-13 21:10:32 +05:30
coletdjnz
ea706726d6 [ITV] Fix extractor, add subtitles and thumbnails (#913)
Original PR: https://github.com/ytdl-org/youtube-dl/pull/28955 (see also https://github.com/ytdl-org/youtube-dl/issues/28906#issuecomment-831008270)

Closes #861, https://github.com/ytdl-org/youtube-dl/issues/28906, https://github.com/ytdl-org/youtube-dl/issues/29337, https://github.com/ytdl-org/youtube-dl/issues/29190, https://github.com/ytdl-org/youtube-dl/issues/28939, https://github.com/ytdl-org/youtube-dl/issues/29620

Authored-by: coletdjnz, sleaux-meaux, Vangelis66
2021-09-13 02:26:19 +05:30
pukkandan
f60990ddfc [peertube] Update instances (#957)
Authored by: u-spec-png
2021-09-13 02:02:28 +05:30
pukkandan
ad226b1dc9 [funimation] Fix for locations outside US
Closes #868
Authored by: Jules-A, pukkandan
2021-09-12 21:40:37 +05:30
pukkandan
ca46b94134 [cookies] Make browser names case insensitive 2021-09-12 21:40:37 +05:30
pukkandan
67ad7759af [brightcove] Extract subtitles from manifests 2021-09-12 21:40:36 +05:30
pukkandan
d5fe04f5c7 Fix --compat-option no-direct-merge 2021-09-12 21:40:28 +05:30
dalan
03c862794f [9Now] handle episodes of series (#896)
Authored by: dalanmiller
2021-09-12 17:41:24 +05:30
MinePlayersPE
0fd6661edb [TikTokUser] Fix extractor using mobile API (#925)
and misc cleanup

Closes #859
Authored by: MinePlayersPE, llacb47
2021-09-12 11:51:59 +05:30
u-spec-png
02c7ae8104 [Newgrounds] Add NewgroundsUserIE and improve extractor (#942)
Authored by: u-spec-png
2021-09-12 11:07:44 +05:30
Ashish Gupta
16f7e6be3a [bilibili]Add BiliIntlIE and BiliIntlSeriesIE (#907)
Closes #611 
Authored by: Ashish0804
2021-09-11 18:59:48 +05:30
Ashish Gupta
ffecd3034b [MuseScore] Add Extractor (#918)
Closes #911 
Authored by: Ashish0804
2021-09-11 18:51:11 +05:30
Felix S
1c5ce74c04 [zype] Extract subtitles from the m3u8 manifest (#948)
Closes #929
Authored by: fstirlitz
2021-09-11 15:46:03 +05:30
pukkandan
81a136b80f [WebVTT] Adjust parser to accommodate PBS subtitles (#922)
Closes #921
2021-09-08 16:10:10 +05:30
coletdjnz
eab3f867e2 [nzherald] Add NZHeraldIE (#909)
Authored-by: coletdjnz

Related: https://github.com/ytdl-org/youtube-dl/issues/28267
2021-09-07 22:49:57 +00:00
coletdjnz
a7e999beec [pbs] Fix subtitle extraction (#813)
Original PR: https://github.com/ytdl-org/youtube-dl/pull/24430, https://github.com/ytdl-org/youtube-dl/pull/17434
Closes: #836, https://github.com/ytdl-org/youtube-dl/issues/18796, https://github.com/ytdl-org/youtube-dl/issues/17273
Authored-by: coletdjnz, gesa, raphaeldore
2021-09-08 02:29:20 +05:30
Ashish
71407b3eca [Olympics] Add replay extractor (#905)
Closes #897 
Authored by: Ashish0804
2021-09-07 23:05:27 +05:30
Ashish
dc9de9cbd2 [Yandex] Add ZenYandexIE and ZenYandexChannelIE (#900)
Authored by: Ashish0804
2021-09-07 23:03:19 +05:30
Poschi
92ddaa415e [gotostage] Add extractor (#883)
Authored by: poschi3
2021-09-07 22:41:56 +05:30
coletdjnz
b6de707d13 [youtube] Improvements to JS player extraction (See desc) (#860)
* fallback player url extraction when it fails to be extracted from the webpage
* don't download js player unnecessarily for clients that don't require it
* try to extract js player url from any additional client configs
* ability to skip the js player usage/download using `player_skip=js`
* ability to skip the initial webpage download using `player_skip=webpage`

known issue:
* authentication for multi-channel accounts and multi-account cookies may not work correctly if the webpage or client configs are skipped
*  formats from the web client requiring signature decryption will be skipped if player js extraction is skipped

Authored by: coletdjnz
2021-09-06 12:56:41 +05:30
coletdjnz
bccdbd22d5 [Mediaklikk] Add Extractor (#867)
Original PR: https://github.com/ytdl-org/youtube-dl/pull/17453, https://github.com/ytdl-org/youtube-dl/pull/25098
Fixes: https://github.com/ytdl-org/youtube-dl/issues/21431
Authored-by: tmarki, mrx23dot, coletdjnz
2021-09-06 12:22:38 +05:30
MinePlayersPE
bd9ff55bcd [tiktok] Use API to fetch higher quality video (#843)
Authored by: MinePlayersPE, llacb47
2021-09-05 11:16:27 +05:30
pukkandan
526d74ec5a [cleanup] Misc 2021-09-05 11:16:23 +05:30
pukkandan
e04a1ff92e [soundcloud] Retry playlist pages on 502 error
Closes #872
2021-09-05 10:48:40 +05:30
pukkandan
aa6c25309a [soundcloud] Make playlist extraction lazy 2021-09-05 10:28:28 +05:30
pukkandan
d98b006b85 [dw] Fix extractor
Closes #830
2021-09-05 10:28:28 +05:30
pukkandan
265a7a8ee5 [redtube] Fix exts
Closes #464
2021-09-05 06:32:11 +05:30
pukkandan
826446bd82 [plutotv] Fix extractor for URLs with /en
Closes #431
2021-09-05 06:32:10 +05:30
The Hatsune Daishi
bc79491368 [17live] Add 17.live extractor (#866)
Authored by: nao20010128nao
2021-09-05 04:07:28 +05:30
ChillingPepper
421ddcb8b4 [SovietsCloset] Add extractor (#884)
Authored by: ChillingPepper
2021-09-04 17:59:35 +05:30
coletdjnz
c0ac49bcca [youtube] Retry on 'Unknown Error' (#854)
and do not repeat unimportant alerts

Closes #839
Authored by: coletdjnz
2021-09-04 08:03:42 +05:30
coletdjnz
02def2714c [southpark] Fix SouthParkDE (#812)
This was broken by ee1e05581e
Authored by: coletdjnz
2021-09-04 08:01:47 +05:30
pukkandan
f9be9cb9fd [cookies] Print warning for cookie decoding error only once
Closes #889
2021-09-04 07:52:47 +05:30
pukkandan
4614bc22c1 Allow --force-write-archive to work with --flat-playlist
Related: #876
2021-09-04 03:07:29 +05:30
pukkandan
8e5fecc88c Handle more playlist errors with -i 2021-09-04 03:07:27 +05:30
pukkandan
165efb823b [ModifyChapters] fixes (See desc)
* [docs] Fix typo
* Do not enable `sponskrub` by default
* Fix `--force-keyframes-at-cuts`
* Don't embed subtitles if the video has been cut. Previously, running `--remove-chapters` with `--embed-subs` multiple times caused repeated cuts and out-of-sync subtitles
* Store `_real_duration` to prevent running ffprobe multiple times
2021-09-04 01:39:31 +05:30
pukkandan
dd594deb2a Fix --no-get-comments
Closes #882
2021-09-04 01:39:30 +05:30
pukkandan
409e18286e Fix extra_info being reused across runs
58adec4677 was supposed to solve this, but ended up being an incomplete fix
Closes #727
2021-09-04 01:39:29 +05:30
pukkandan
8113999995 Fix --compat-option playlist-index 2021-09-04 01:39:27 +05:30
pukkandan
8026e50152 [version] update
:ci skip all
2021-09-02 05:33:38 +05:30
187 changed files with 11288 additions and 5499 deletions

View File

@@ -1,70 +0,0 @@
---
name: Broken site support
about: Report broken or misfunctioning site
title: "[Broken]"
labels: Broken
assignees: ''
---
<!--
######################################################################
WARNING!
IGNORING THE FOLLOWING TEMPLATE WILL RESULT IN ISSUE CLOSED AS INCOMPLETE
######################################################################
-->
## Checklist
<!--
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.08.10. 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 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.
- Finally, put x into all relevant boxes like this [x] (Dont forget to delete the empty space)
-->
- [ ] I'm reporting a broken site support
- [ ] I've verified that I'm running yt-dlp version **2021.08.10**
- [ ] 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 searched the bugtracker for similar issues including closed ones
## Verbose log
<!--
Provide the complete verbose output of yt-dlp that clearly demonstrates the problem.
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] User config: []
[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] yt-dlp version 2021.08.10
[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] Proxy map: {}
<more lines>
-->
```
PASTE VERBOSE LOG HERE
```
<!--
Do not remove the above ```
-->
## Description
<!--
Provide an explanation of your issue in an arbitrary form. Provide any additional information, suggested solution and as much context and examples as possible.
If work on your issue requires account credentials please provide them or explain how one can obtain them.
-->
WRITE DESCRIPTION HERE

View File

@@ -0,0 +1,63 @@
name: Broken site support
description: Report broken or misfunctioning site
labels: [triage, extractor-bug]
body:
- type: checkboxes
id: checklist
attributes:
label: Checklist
description: |
Carefully read and work through this check list in order to prevent the most common mistakes and misuse of yt-dlp:
options:
- label: I'm reporting a broken site
required: true
- label: I've verified that I'm running yt-dlp version **2021.10.22**. ([update instructions](https://github.com/yt-dlp/yt-dlp#update))
required: true
- label: I've checked that all provided URLs are alive and playable in a browser
required: true
- label: I've checked that all URLs and arguments with special characters are [properly quoted or escaped](https://github.com/ytdl-org/youtube-dl#video-url-contains-an-ampersand-and-im-getting-some-strange-output-1-2839-or-v-is-not-recognized-as-an-internal-or-external-command)
required: true
- label: I've searched the [bugtracker](https://github.com/yt-dlp/yt-dlp/issues?q=) for similar issues including closed ones. DO NOT post duplicates
required: true
- label: I've read the [guidelines for opening an issue](https://github.com/yt-dlp/yt-dlp/blob/master/CONTRIBUTING.md#opening-an-issue)
required: true
- label: I've read about [sharing account credentials](https://github.com/yt-dlp/yt-dlp/blob/master/CONTRIBUTING.md#are-you-willing-to-share-account-details-if-needed) and I'm willing to share it if required
- type: input
id: region
attributes:
label: Region
description: "Enter the region the site is accessible from"
placeholder: "India"
- type: textarea
id: description
attributes:
label: Description
description: |
Provide an explanation of your issue in an arbitrary form.
Provide any additional information, any suggested solutions, and as much context and examples as possible
placeholder: WRITE DESCRIPTION HERE
validations:
required: true
- type: textarea
id: log
attributes:
label: Verbose log
description: |
Provide the complete verbose output of yt-dlp that clearly demonstrates the problem.
Add the `-Uv` flag to your command line you run yt-dlp with (`yt-dlp -Uv <your command line>`), copy the WHOLE output and insert it below.
It should look similar to this:
placeholder: |
[debug] Command-line config: ['-Uv', 'http://www.youtube.com/watch?v=BaW_jenozKc']
[debug] Portable config file: yt-dlp.conf
[debug] Portable config: ['-i']
[debug] Encodings: locale cp1252, fs utf-8, stdout utf-8, stderr utf-8, pref cp1252
[debug] yt-dlp version 2021.10.22 (exe)
[debug] Python version 3.8.8 (CPython 64bit) - Windows-10-10.0.19041-SP0
[debug] exe versions: ffmpeg 3.0.1, ffprobe 3.0.1
[debug] Optional libraries: Cryptodome, keyring, mutagen, sqlite, websockets
[debug] Proxy map: {}
yt-dlp is up to date (2021.10.22)
<more lines>
render: shell
validations:
required: true

View File

@@ -1,57 +0,0 @@
---
name: Site support request
about: Request support for a new site
title: "[Site Request]"
labels: Request
assignees: ''
---
<!--
######################################################################
WARNING!
IGNORING THE FOLLOWING TEMPLATE WILL RESULT IN ISSUE CLOSED AS INCOMPLETE
######################################################################
-->
## Checklist
<!--
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.08.10. 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 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.
- Finally, put x into all relevant boxes like this [x] (Dont forget to delete the empty space)
-->
- [ ] I'm reporting a new site support request
- [ ] I've verified that I'm running yt-dlp version **2021.08.10**
- [ ] 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
- [ ] The provided URLs do not contain any DRM to the best of my knowledge
- [ ] I've searched the bugtracker for similar site support requests including closed ones
## Example URLs
<!--
Provide all kinds of example URLs support for which should be included. Replace following example URLs by yours.
-->
- Single video: https://www.youtube.com/watch?v=BaW_jenozKc
- Single video: https://youtu.be/BaW_jenozKc
- Playlist: https://www.youtube.com/playlist?list=PL4lCao7KL_QFVb7Iudeipvc2BCavECqzc
## Description
<!--
Provide any additional information.
If work on your issue requires account credentials please provide them or explain how one can obtain them.
-->
WRITE DESCRIPTION HERE

View File

@@ -0,0 +1,74 @@
name: Site support request
description: Request support for a new site
labels: [triage, site-request]
body:
- type: checkboxes
id: checklist
attributes:
label: Checklist
description: |
Carefully read and work through this check list in order to prevent the most common mistakes and misuse of yt-dlp:
options:
- label: I'm reporting a new site support request
required: true
- label: I've verified that I'm running yt-dlp version **2021.10.22**. ([update instructions](https://github.com/yt-dlp/yt-dlp#update))
required: true
- label: I've checked that all provided URLs are alive and playable in a browser
required: true
- label: I've checked that none of provided URLs [violate any copyrights](https://github.com/ytdl-org/youtube-dl#can-you-add-support-for-this-anime-video-site-or-site-which-shows-current-movies-for-free) or contain any [DRM](https://en.wikipedia.org/wiki/Digital_rights_management) to the best of my knowledge
required: true
- label: I've searched the [bugtracker](https://github.com/yt-dlp/yt-dlp/issues?q=) for similar issues including closed ones. DO NOT post duplicates
required: true
- label: I've read the [guidelines for opening an issue](https://github.com/yt-dlp/yt-dlp/blob/master/CONTRIBUTING.md#opening-an-issue)
required: true
- label: I've read about [sharing account credentials](https://github.com/yt-dlp/yt-dlp/blob/master/CONTRIBUTING.md#are-you-willing-to-share-account-details-if-needed) and am willing to share it if required
- type: input
id: region
attributes:
label: Region
description: "Enter the region the site is accessible from"
placeholder: "India"
- type: textarea
id: example-urls
attributes:
label: Example URLs
description: |
Provide all kinds of example URLs for which support should be added
value: |
- Single video: https://www.youtube.com/watch?v=BaW_jenozKc
- Single video: https://youtu.be/BaW_jenozKc
- Playlist: https://www.youtube.com/playlist?list=PL4lCao7KL_QFVb7Iudeipvc2BCavECqzc
validations:
required: true
- type: textarea
id: description
attributes:
label: Description
description: |
Provide any additional information
placeholder: WRITE DESCRIPTION HERE
validations:
required: true
- type: textarea
id: log
attributes:
label: Verbose log
description: |
Provide the complete verbose output using one of the example URLs provided above.
Add the `-Uv` flag to your command line you run yt-dlp with (`yt-dlp -Uv <your command line>`), copy the WHOLE output and insert it below.
It should look similar to this:
placeholder: |
[debug] Command-line config: ['-Uv', 'http://www.youtube.com/watch?v=BaW_jenozKc']
[debug] Portable config file: yt-dlp.conf
[debug] Portable config: ['-i']
[debug] Encodings: locale cp1252, fs utf-8, stdout utf-8, stderr utf-8, pref cp1252
[debug] yt-dlp version 2021.10.22 (exe)
[debug] Python version 3.8.8 (CPython 64bit) - Windows-10-10.0.19041-SP0
[debug] exe versions: ffmpeg 3.0.1, ffprobe 3.0.1
[debug] Optional libraries: Cryptodome, keyring, mutagen, sqlite, websockets
[debug] Proxy map: {}
yt-dlp is up to date (2021.10.22)
<more lines>
render: shell
validations:
required: true

View File

@@ -1,40 +0,0 @@
---
name: Site feature request
about: Request a new functionality for a site
title: "[Site Request]"
labels: Request
assignees: ''
---
<!--
######################################################################
WARNING!
IGNORING THE FOLLOWING TEMPLATE WILL RESULT IN ISSUE CLOSED AS INCOMPLETE
######################################################################
-->
## Checklist
<!--
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.08.10. 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.
- 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've verified that I'm running yt-dlp version **2021.08.10**
- [ ] I've searched the bugtracker for similar site feature requests including closed ones
## Description
<!--
Provide an explanation of your site feature request in an arbitrary form. Please make sure the description is worded well enough to be understood, see https://github.com/ytdl-org/youtube-dl#is-the-description-of-the-issue-itself-sufficient. Provide any additional information, suggested solution and as much context and examples as possible.
-->
WRITE DESCRIPTION HERE

View File

@@ -0,0 +1,49 @@
name: Site feature request
description: Request a new functionality for a site
labels: [triage, site-enhancement]
body:
- type: checkboxes
id: checklist
attributes:
label: Checklist
description: |
Carefully read and work through this check list in order to prevent the most common mistakes and misuse of yt-dlp:
options:
- label: I'm reporting a site feature request
required: true
- label: I've verified that I'm running yt-dlp version **2021.10.22**. ([update instructions](https://github.com/yt-dlp/yt-dlp#update))
required: true
- label: I've checked that all provided URLs are alive and playable in a browser
required: true
- label: I've searched the [bugtracker](https://github.com/yt-dlp/yt-dlp/issues?q=) for similar issues including closed ones. DO NOT post duplicates
required: true
- label: I've read the [guidelines for opening an issue](https://github.com/yt-dlp/yt-dlp/blob/master/CONTRIBUTING.md#opening-an-issue)
required: true
- label: I've read about [sharing account credentials](https://github.com/yt-dlp/yt-dlp/blob/master/CONTRIBUTING.md#are-you-willing-to-share-account-details-if-needed) and I'm willing to share it if required
- type: input
id: region
attributes:
label: Region
description: "Enter the region the site is accessible from"
placeholder: "India"
- type: textarea
id: example-urls
attributes:
label: Example URLs
description: |
Example URLs that can be used to demonstrate the requested feature
value: |
https://www.youtube.com/watch?v=BaW_jenozKc
validations:
required: true
- type: textarea
id: description
attributes:
label: Description
description: |
Provide an explanation of your site feature request in an arbitrary form.
Please make sure the description is worded well enough to be understood, see [is-the-description-of-the-issue-itself-sufficient](https://github.com/ytdl-org/youtube-dl#is-the-description-of-the-issue-itself-sufficient).
Provide any additional information, any suggested solutions, and as much context and examples as possible
placeholder: WRITE DESCRIPTION HERE
validations:
required: true

View File

@@ -1,73 +0,0 @@
---
name: Bug report
about: Report a bug unrelated to any particular site or extractor
title: ''
labels: ''
assignees: ''
---
<!--
######################################################################
WARNING!
IGNORING THE FOLLOWING TEMPLATE WILL RESULT IN ISSUE CLOSED AS INCOMPLETE
######################################################################
-->
## Checklist
<!--
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.08.10. 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 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.
- Read bugs section in FAQ: https://github.com/yt-dlp/yt-dlp
- Finally, put x into all relevant boxes like this [x] (Dont forget to delete the empty space)
-->
- [ ] I'm reporting a bug unrelated to a specific site
- [ ] I've verified that I'm running yt-dlp version **2021.08.10**
- [ ] I've checked that all provided URLs are alive and playable in a browser
- [ ] The provided URLs do not contain any DRM to the best of my knowledge
- [ ] 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 read bugs section in FAQ
## Verbose log
<!--
Provide the complete verbose output of yt-dlp that clearly demonstrates the problem.
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] User config: []
[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] yt-dlp version 2021.08.10
[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] Proxy map: {}
<more lines>
-->
```
PASTE VERBOSE LOG HERE
```
<!--
Do not remove the above ```
-->
## Description
<!--
Provide an explanation of your issue in an arbitrary form. Please make sure the description is worded well enough to be understood, see https://github.com/ytdl-org/youtube-dl#is-the-description-of-the-issue-itself-sufficient. Provide any additional information, suggested solution and as much context and examples as possible.
If work on your issue requires account credentials please provide them or explain how one can obtain them.
-->
WRITE DESCRIPTION HERE

57
.github/ISSUE_TEMPLATE/4_bug_report.yml vendored Normal file
View File

@@ -0,0 +1,57 @@
name: Bug report
description: Report a bug unrelated to any particular site or extractor
labels: [triage,bug]
body:
- type: checkboxes
id: checklist
attributes:
label: Checklist
description: |
Carefully read and work through this check list in order to prevent the most common mistakes and misuse of yt-dlp:
options:
- label: I'm reporting a bug unrelated to a specific site
required: true
- label: I've verified that I'm running yt-dlp version **2021.10.22**. ([update instructions](https://github.com/yt-dlp/yt-dlp#update))
required: true
- label: I've checked that all provided URLs are alive and playable in a browser
required: true
- label: I've checked that all URLs and arguments with special characters are [properly quoted or escaped](https://github.com/ytdl-org/youtube-dl#video-url-contains-an-ampersand-and-im-getting-some-strange-output-1-2839-or-v-is-not-recognized-as-an-internal-or-external-command)
required: true
- label: I've searched the [bugtracker](https://github.com/yt-dlp/yt-dlp/issues?q=) for similar issues including closed ones. DO NOT post duplicates
required: true
- label: I've read the [guidelines for opening an issue](https://github.com/yt-dlp/yt-dlp/blob/master/CONTRIBUTING.md#opening-an-issue)
required: true
- type: textarea
id: description
attributes:
label: Description
description: |
Provide an explanation of your issue in an arbitrary form.
Please make sure the description is worded well enough to be understood, see [is-the-description-of-the-issue-itself-sufficient](https://github.com/ytdl-org/youtube-dl#is-the-description-of-the-issue-itself-sufficient).
Provide any additional information, any suggested solutions, and as much context and examples as possible
placeholder: WRITE DESCRIPTION HERE
validations:
required: true
- type: textarea
id: log
attributes:
label: Verbose log
description: |
Provide the complete verbose output of yt-dlp that clearly demonstrates the problem.
Add the `-Uv` flag to your command line you run yt-dlp with (`yt-dlp -Uv <your command line>`), copy the WHOLE output and insert it below.
It should look similar to this:
placeholder: |
[debug] Command-line config: ['-Uv', 'http://www.youtube.com/watch?v=BaW_jenozKc']
[debug] Portable config file: yt-dlp.conf
[debug] Portable config: ['-i']
[debug] Encodings: locale cp1252, fs utf-8, stdout utf-8, stderr utf-8, pref cp1252
[debug] yt-dlp version 2021.10.22 (exe)
[debug] Python version 3.8.8 (CPython 64bit) - Windows-10-10.0.19041-SP0
[debug] exe versions: ffmpeg 3.0.1, ffprobe 3.0.1
[debug] Optional libraries: Cryptodome, keyring, mutagen, sqlite, websockets
[debug] Proxy map: {}
yt-dlp is up to date (2021.10.22)
<more lines>
render: shell
validations:
required: true

View File

@@ -1,40 +0,0 @@
---
name: Feature request
about: Request a new functionality unrelated to any particular site or extractor
title: "[Feature Request]"
labels: Request
assignees: ''
---
<!--
######################################################################
WARNING!
IGNORING THE FOLLOWING TEMPLATE WILL RESULT IN ISSUE CLOSED AS INCOMPLETE
######################################################################
-->
## Checklist
<!--
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.08.10. 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.
- Finally, put x into all relevant boxes like this [x] (Dont forget to delete the empty space)
-->
- [ ] I'm reporting a feature request
- [ ] I've verified that I'm running yt-dlp version **2021.08.10**
- [ ] I've searched the bugtracker for similar feature requests including closed ones
## Description
<!--
Provide an explanation of your issue in an arbitrary form. Please make sure the description is worded well enough to be understood, see https://github.com/ytdl-org/youtube-dl#is-the-description-of-the-issue-itself-sufficient. Provide any additional information, suggested solution and as much context and examples as possible.
-->
WRITE DESCRIPTION HERE

View File

@@ -0,0 +1,30 @@
name: Feature request request
description: Request a new functionality unrelated to any particular site or extractor
labels: [triage, enhancement]
body:
- type: checkboxes
id: checklist
attributes:
label: Checklist
description: |
Carefully read and work through this check list in order to prevent the most common mistakes and misuse of yt-dlp:
options:
- label: I'm reporting a feature request
required: true
- label: I've verified that I'm running yt-dlp version **2021.10.22**. ([update instructions](https://github.com/yt-dlp/yt-dlp#update))
required: true
- label: I've searched the [bugtracker](https://github.com/yt-dlp/yt-dlp/issues?q=) for similar issues including closed ones. DO NOT post duplicates
required: true
- label: I've read the [guidelines for opening an issue](https://github.com/yt-dlp/yt-dlp/blob/master/CONTRIBUTING.md#opening-an-issue)
required: true
- type: textarea
id: description
attributes:
label: Description
description: |
Provide an explanation of your site feature request in an arbitrary form.
Please make sure the description is worded well enough to be understood, see [is-the-description-of-the-issue-itself-sufficient](https://github.com/ytdl-org/youtube-dl#is-the-description-of-the-issue-itself-sufficient).
Provide any additional information, any suggested solutions, and as much context and examples as possible
placeholder: WRITE DESCRIPTION HERE
validations:
required: true

View File

@@ -1,40 +0,0 @@
---
name: Ask question
about: Ask yt-dlp related question
title: "[Question]"
labels: question
assignees: ''
---
<!--
######################################################################
WARNING!
IGNORING THE FOLLOWING TEMPLATE WILL RESULT IN ISSUE CLOSED AS INCOMPLETE
######################################################################
-->
## Checklist
<!--
Carefully read and work through this check list in order to prevent the most common mistakes and misuse of yt-dlp:
- Look through the README (https://github.com/yt-dlp/yt-dlp) and FAQ (https://github.com/yt-dlp/yt-dlp) for similar questions
- Search the bugtracker for similar questions: https://github.com/yt-dlp/yt-dlp
- Finally, put x into all relevant boxes like this [x] (Dont forget to delete the empty space)
-->
- [ ] I'm asking a question
- [ ] I've looked through the README and FAQ for similar questions
- [ ] I've searched the bugtracker for similar questions including closed ones
## Question
<!--
Ask your question in an arbitrary form. Please make sure it's worded well enough to be understood, see https://github.com/yt-dlp/yt-dlp.
-->
WRITE QUESTION HERE

30
.github/ISSUE_TEMPLATE/6_question.yml vendored Normal file
View File

@@ -0,0 +1,30 @@
name: Ask question
description: Ask yt-dlp related question
labels: [question]
body:
- type: checkboxes
id: checklist
attributes:
label: Checklist
description: |
Carefully read and work through this check list in order to prevent the most common mistakes and misuse of yt-dlp:
options:
- label: I'm asking a question and not reporting a bug/feature request
required: true
- label: I've looked through the [README](https://github.com/yt-dlp/yt-dlp#readme)
required: true
- label: I've read the [guidelines for opening an issue](https://github.com/yt-dlp/yt-dlp/blob/master/CONTRIBUTING.md#opening-an-issue)
required: true
- label: I've searched the [bugtracker](https://github.com/yt-dlp/yt-dlp/issues?q=) for similar questions including closed ones
required: true
- type: textarea
id: question
attributes:
label: Question
description: |
Ask your question in an arbitrary form.
Please make sure it's worded well enough to be understood, see [is-the-description-of-the-issue-itself-sufficient](https://github.com/ytdl-org/youtube-dl#is-the-description-of-the-issue-itself-sufficient).
Provide any additional information and as much context and examples as possible
placeholder: WRITE QUESTION HERE
validations:
required: true

5
.github/ISSUE_TEMPLATE/config.yml vendored Normal file
View File

@@ -0,0 +1,5 @@
blank_issues_enabled: false
contact_links:
- name: Get help from the community on Discord
url: https://discord.gg/H5MNcFW63r
about: Join the yt-dlp Discord for community-powered support!

View File

@@ -1,70 +0,0 @@
---
name: Broken site support
about: Report broken or misfunctioning site
title: "[Broken]"
labels: Broken
assignees: ''
---
<!--
######################################################################
WARNING!
IGNORING THE FOLLOWING TEMPLATE WILL RESULT IN ISSUE CLOSED AS INCOMPLETE
######################################################################
-->
## Checklist
<!--
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 %(version)s. 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 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.
- Finally, put x into all relevant boxes like this [x] (Dont forget to delete the empty space)
-->
- [ ] I'm reporting a broken site support
- [ ] I've verified that I'm running yt-dlp version **%(version)s**
- [ ] 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 searched the bugtracker for similar issues including closed ones
## Verbose log
<!--
Provide the complete verbose output of yt-dlp that clearly demonstrates the problem.
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] User config: []
[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] yt-dlp version %(version)s
[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] Proxy map: {}
<more lines>
-->
```
PASTE VERBOSE LOG HERE
```
<!--
Do not remove the above ```
-->
## Description
<!--
Provide an explanation of your issue in an arbitrary form. Provide any additional information, suggested solution and as much context and examples as possible.
If work on your issue requires account credentials please provide them or explain how one can obtain them.
-->
WRITE DESCRIPTION HERE

View File

@@ -0,0 +1,63 @@
name: Broken site support
description: Report broken or misfunctioning site
labels: [triage, extractor-bug]
body:
- type: checkboxes
id: checklist
attributes:
label: Checklist
description: |
Carefully read and work through this check list in order to prevent the most common mistakes and misuse of yt-dlp:
options:
- label: I'm reporting a broken site
required: true
- label: I've verified that I'm running yt-dlp version **%(version)s**. ([update instructions](https://github.com/yt-dlp/yt-dlp#update))
required: true
- label: I've checked that all provided URLs are alive and playable in a browser
required: true
- label: I've checked that all URLs and arguments with special characters are [properly quoted or escaped](https://github.com/ytdl-org/youtube-dl#video-url-contains-an-ampersand-and-im-getting-some-strange-output-1-2839-or-v-is-not-recognized-as-an-internal-or-external-command)
required: true
- label: I've searched the [bugtracker](https://github.com/yt-dlp/yt-dlp/issues?q=) for similar issues including closed ones. DO NOT post duplicates
required: true
- label: I've read the [guidelines for opening an issue](https://github.com/yt-dlp/yt-dlp/blob/master/CONTRIBUTING.md#opening-an-issue)
required: true
- label: I've read about [sharing account credentials](https://github.com/yt-dlp/yt-dlp/blob/master/CONTRIBUTING.md#are-you-willing-to-share-account-details-if-needed) and I'm willing to share it if required
- type: input
id: region
attributes:
label: Region
description: "Enter the region the site is accessible from"
placeholder: "India"
- type: textarea
id: description
attributes:
label: Description
description: |
Provide an explanation of your issue in an arbitrary form.
Provide any additional information, any suggested solutions, and as much context and examples as possible
placeholder: WRITE DESCRIPTION HERE
validations:
required: true
- type: textarea
id: log
attributes:
label: Verbose log
description: |
Provide the complete verbose output of yt-dlp that clearly demonstrates the problem.
Add the `-Uv` flag to your command line you run yt-dlp with (`yt-dlp -Uv <your command line>`), copy the WHOLE output and insert it below.
It should look similar to this:
placeholder: |
[debug] Command-line config: ['-Uv', 'http://www.youtube.com/watch?v=BaW_jenozKc']
[debug] Portable config file: yt-dlp.conf
[debug] Portable config: ['-i']
[debug] Encodings: locale cp1252, fs utf-8, stdout utf-8, stderr utf-8, pref cp1252
[debug] yt-dlp version %(version)s (exe)
[debug] Python version 3.8.8 (CPython 64bit) - Windows-10-10.0.19041-SP0
[debug] exe versions: ffmpeg 3.0.1, ffprobe 3.0.1
[debug] Optional libraries: Cryptodome, keyring, mutagen, sqlite, websockets
[debug] Proxy map: {}
yt-dlp is up to date (%(version)s)
<more lines>
render: shell
validations:
required: true

View File

@@ -1,57 +0,0 @@
---
name: Site support request
about: Request support for a new site
title: "[Site Request]"
labels: Request
assignees: ''
---
<!--
######################################################################
WARNING!
IGNORING THE FOLLOWING TEMPLATE WILL RESULT IN ISSUE CLOSED AS INCOMPLETE
######################################################################
-->
## Checklist
<!--
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 %(version)s. 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 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.
- Finally, put x into all relevant boxes like this [x] (Dont forget to delete the empty space)
-->
- [ ] I'm reporting a new site support request
- [ ] I've verified that I'm running yt-dlp version **%(version)s**
- [ ] 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
- [ ] The provided URLs do not contain any DRM to the best of my knowledge
- [ ] I've searched the bugtracker for similar site support requests including closed ones
## Example URLs
<!--
Provide all kinds of example URLs support for which should be included. Replace following example URLs by yours.
-->
- Single video: https://www.youtube.com/watch?v=BaW_jenozKc
- Single video: https://youtu.be/BaW_jenozKc
- Playlist: https://www.youtube.com/playlist?list=PL4lCao7KL_QFVb7Iudeipvc2BCavECqzc
## Description
<!--
Provide any additional information.
If work on your issue requires account credentials please provide them or explain how one can obtain them.
-->
WRITE DESCRIPTION HERE

View File

@@ -0,0 +1,74 @@
name: Site support request
description: Request support for a new site
labels: [triage, site-request]
body:
- type: checkboxes
id: checklist
attributes:
label: Checklist
description: |
Carefully read and work through this check list in order to prevent the most common mistakes and misuse of yt-dlp:
options:
- label: I'm reporting a new site support request
required: true
- label: I've verified that I'm running yt-dlp version **%(version)s**. ([update instructions](https://github.com/yt-dlp/yt-dlp#update))
required: true
- label: I've checked that all provided URLs are alive and playable in a browser
required: true
- label: I've checked that none of provided URLs [violate any copyrights](https://github.com/ytdl-org/youtube-dl#can-you-add-support-for-this-anime-video-site-or-site-which-shows-current-movies-for-free) or contain any [DRM](https://en.wikipedia.org/wiki/Digital_rights_management) to the best of my knowledge
required: true
- label: I've searched the [bugtracker](https://github.com/yt-dlp/yt-dlp/issues?q=) for similar issues including closed ones. DO NOT post duplicates
required: true
- label: I've read the [guidelines for opening an issue](https://github.com/yt-dlp/yt-dlp/blob/master/CONTRIBUTING.md#opening-an-issue)
required: true
- label: I've read about [sharing account credentials](https://github.com/yt-dlp/yt-dlp/blob/master/CONTRIBUTING.md#are-you-willing-to-share-account-details-if-needed) and am willing to share it if required
- type: input
id: region
attributes:
label: Region
description: "Enter the region the site is accessible from"
placeholder: "India"
- type: textarea
id: example-urls
attributes:
label: Example URLs
description: |
Provide all kinds of example URLs for which support should be added
value: |
- Single video: https://www.youtube.com/watch?v=BaW_jenozKc
- Single video: https://youtu.be/BaW_jenozKc
- Playlist: https://www.youtube.com/playlist?list=PL4lCao7KL_QFVb7Iudeipvc2BCavECqzc
validations:
required: true
- type: textarea
id: description
attributes:
label: Description
description: |
Provide any additional information
placeholder: WRITE DESCRIPTION HERE
validations:
required: true
- type: textarea
id: log
attributes:
label: Verbose log
description: |
Provide the complete verbose output using one of the example URLs provided above.
Add the `-Uv` flag to your command line you run yt-dlp with (`yt-dlp -Uv <your command line>`), copy the WHOLE output and insert it below.
It should look similar to this:
placeholder: |
[debug] Command-line config: ['-Uv', 'http://www.youtube.com/watch?v=BaW_jenozKc']
[debug] Portable config file: yt-dlp.conf
[debug] Portable config: ['-i']
[debug] Encodings: locale cp1252, fs utf-8, stdout utf-8, stderr utf-8, pref cp1252
[debug] yt-dlp version %(version)s (exe)
[debug] Python version 3.8.8 (CPython 64bit) - Windows-10-10.0.19041-SP0
[debug] exe versions: ffmpeg 3.0.1, ffprobe 3.0.1
[debug] Optional libraries: Cryptodome, keyring, mutagen, sqlite, websockets
[debug] Proxy map: {}
yt-dlp is up to date (%(version)s)
<more lines>
render: shell
validations:
required: true

View File

@@ -1,40 +0,0 @@
---
name: Site feature request
about: Request a new functionality for a site
title: "[Site Request]"
labels: Request
assignees: ''
---
<!--
######################################################################
WARNING!
IGNORING THE FOLLOWING TEMPLATE WILL RESULT IN ISSUE CLOSED AS INCOMPLETE
######################################################################
-->
## Checklist
<!--
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 %(version)s. 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.
- 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've verified that I'm running yt-dlp version **%(version)s**
- [ ] I've searched the bugtracker for similar site feature requests including closed ones
## Description
<!--
Provide an explanation of your site feature request in an arbitrary form. Please make sure the description is worded well enough to be understood, see https://github.com/ytdl-org/youtube-dl#is-the-description-of-the-issue-itself-sufficient. Provide any additional information, suggested solution and as much context and examples as possible.
-->
WRITE DESCRIPTION HERE

View File

@@ -0,0 +1,49 @@
name: Site feature request
description: Request a new functionality for a site
labels: [triage, site-enhancement]
body:
- type: checkboxes
id: checklist
attributes:
label: Checklist
description: |
Carefully read and work through this check list in order to prevent the most common mistakes and misuse of yt-dlp:
options:
- label: I'm reporting a site feature request
required: true
- label: I've verified that I'm running yt-dlp version **%(version)s**. ([update instructions](https://github.com/yt-dlp/yt-dlp#update))
required: true
- label: I've checked that all provided URLs are alive and playable in a browser
required: true
- label: I've searched the [bugtracker](https://github.com/yt-dlp/yt-dlp/issues?q=) for similar issues including closed ones. DO NOT post duplicates
required: true
- label: I've read the [guidelines for opening an issue](https://github.com/yt-dlp/yt-dlp/blob/master/CONTRIBUTING.md#opening-an-issue)
required: true
- label: I've read about [sharing account credentials](https://github.com/yt-dlp/yt-dlp/blob/master/CONTRIBUTING.md#are-you-willing-to-share-account-details-if-needed) and I'm willing to share it if required
- type: input
id: region
attributes:
label: Region
description: "Enter the region the site is accessible from"
placeholder: "India"
- type: textarea
id: example-urls
attributes:
label: Example URLs
description: |
Example URLs that can be used to demonstrate the requested feature
value: |
https://www.youtube.com/watch?v=BaW_jenozKc
validations:
required: true
- type: textarea
id: description
attributes:
label: Description
description: |
Provide an explanation of your site feature request in an arbitrary form.
Please make sure the description is worded well enough to be understood, see [is-the-description-of-the-issue-itself-sufficient](https://github.com/ytdl-org/youtube-dl#is-the-description-of-the-issue-itself-sufficient).
Provide any additional information, any suggested solutions, and as much context and examples as possible
placeholder: WRITE DESCRIPTION HERE
validations:
required: true

View File

@@ -1,73 +0,0 @@
---
name: Bug report
about: Report a bug unrelated to any particular site or extractor
title: ''
labels: ''
assignees: ''
---
<!--
######################################################################
WARNING!
IGNORING THE FOLLOWING TEMPLATE WILL RESULT IN ISSUE CLOSED AS INCOMPLETE
######################################################################
-->
## Checklist
<!--
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 %(version)s. 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 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.
- Read bugs section in FAQ: https://github.com/yt-dlp/yt-dlp
- Finally, put x into all relevant boxes like this [x] (Dont forget to delete the empty space)
-->
- [ ] I'm reporting a bug unrelated to a specific site
- [ ] I've verified that I'm running yt-dlp version **%(version)s**
- [ ] I've checked that all provided URLs are alive and playable in a browser
- [ ] The provided URLs do not contain any DRM to the best of my knowledge
- [ ] 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 read bugs section in FAQ
## Verbose log
<!--
Provide the complete verbose output of yt-dlp that clearly demonstrates the problem.
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] User config: []
[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] yt-dlp version %(version)s
[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] Proxy map: {}
<more lines>
-->
```
PASTE VERBOSE LOG HERE
```
<!--
Do not remove the above ```
-->
## Description
<!--
Provide an explanation of your issue in an arbitrary form. Please make sure the description is worded well enough to be understood, see https://github.com/ytdl-org/youtube-dl#is-the-description-of-the-issue-itself-sufficient. Provide any additional information, suggested solution and as much context and examples as possible.
If work on your issue requires account credentials please provide them or explain how one can obtain them.
-->
WRITE DESCRIPTION HERE

View File

@@ -0,0 +1,57 @@
name: Bug report
description: Report a bug unrelated to any particular site or extractor
labels: [triage,bug]
body:
- type: checkboxes
id: checklist
attributes:
label: Checklist
description: |
Carefully read and work through this check list in order to prevent the most common mistakes and misuse of yt-dlp:
options:
- label: I'm reporting a bug unrelated to a specific site
required: true
- label: I've verified that I'm running yt-dlp version **%(version)s**. ([update instructions](https://github.com/yt-dlp/yt-dlp#update))
required: true
- label: I've checked that all provided URLs are alive and playable in a browser
required: true
- label: I've checked that all URLs and arguments with special characters are [properly quoted or escaped](https://github.com/ytdl-org/youtube-dl#video-url-contains-an-ampersand-and-im-getting-some-strange-output-1-2839-or-v-is-not-recognized-as-an-internal-or-external-command)
required: true
- label: I've searched the [bugtracker](https://github.com/yt-dlp/yt-dlp/issues?q=) for similar issues including closed ones. DO NOT post duplicates
required: true
- label: I've read the [guidelines for opening an issue](https://github.com/yt-dlp/yt-dlp/blob/master/CONTRIBUTING.md#opening-an-issue)
required: true
- type: textarea
id: description
attributes:
label: Description
description: |
Provide an explanation of your issue in an arbitrary form.
Please make sure the description is worded well enough to be understood, see [is-the-description-of-the-issue-itself-sufficient](https://github.com/ytdl-org/youtube-dl#is-the-description-of-the-issue-itself-sufficient).
Provide any additional information, any suggested solutions, and as much context and examples as possible
placeholder: WRITE DESCRIPTION HERE
validations:
required: true
- type: textarea
id: log
attributes:
label: Verbose log
description: |
Provide the complete verbose output of yt-dlp that clearly demonstrates the problem.
Add the `-Uv` flag to your command line you run yt-dlp with (`yt-dlp -Uv <your command line>`), copy the WHOLE output and insert it below.
It should look similar to this:
placeholder: |
[debug] Command-line config: ['-Uv', 'http://www.youtube.com/watch?v=BaW_jenozKc']
[debug] Portable config file: yt-dlp.conf
[debug] Portable config: ['-i']
[debug] Encodings: locale cp1252, fs utf-8, stdout utf-8, stderr utf-8, pref cp1252
[debug] yt-dlp version %(version)s (exe)
[debug] Python version 3.8.8 (CPython 64bit) - Windows-10-10.0.19041-SP0
[debug] exe versions: ffmpeg 3.0.1, ffprobe 3.0.1
[debug] Optional libraries: Cryptodome, keyring, mutagen, sqlite, websockets
[debug] Proxy map: {}
yt-dlp is up to date (%(version)s)
<more lines>
render: shell
validations:
required: true

View File

@@ -1,40 +0,0 @@
---
name: Feature request
about: Request a new functionality unrelated to any particular site or extractor
title: "[Feature Request]"
labels: Request
assignees: ''
---
<!--
######################################################################
WARNING!
IGNORING THE FOLLOWING TEMPLATE WILL RESULT IN ISSUE CLOSED AS INCOMPLETE
######################################################################
-->
## Checklist
<!--
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 %(version)s. 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.
- Finally, put x into all relevant boxes like this [x] (Dont forget to delete the empty space)
-->
- [ ] I'm reporting a feature request
- [ ] I've verified that I'm running yt-dlp version **%(version)s**
- [ ] I've searched the bugtracker for similar feature requests including closed ones
## Description
<!--
Provide an explanation of your issue in an arbitrary form. Please make sure the description is worded well enough to be understood, see https://github.com/ytdl-org/youtube-dl#is-the-description-of-the-issue-itself-sufficient. Provide any additional information, suggested solution and as much context and examples as possible.
-->
WRITE DESCRIPTION HERE

View File

@@ -0,0 +1,30 @@
name: Feature request request
description: Request a new functionality unrelated to any particular site or extractor
labels: [triage, enhancement]
body:
- type: checkboxes
id: checklist
attributes:
label: Checklist
description: |
Carefully read and work through this check list in order to prevent the most common mistakes and misuse of yt-dlp:
options:
- label: I'm reporting a feature request
required: true
- label: I've verified that I'm running yt-dlp version **%(version)s**. ([update instructions](https://github.com/yt-dlp/yt-dlp#update))
required: true
- label: I've searched the [bugtracker](https://github.com/yt-dlp/yt-dlp/issues?q=) for similar issues including closed ones. DO NOT post duplicates
required: true
- label: I've read the [guidelines for opening an issue](https://github.com/yt-dlp/yt-dlp/blob/master/CONTRIBUTING.md#opening-an-issue)
required: true
- type: textarea
id: description
attributes:
label: Description
description: |
Provide an explanation of your site feature request in an arbitrary form.
Please make sure the description is worded well enough to be understood, see [is-the-description-of-the-issue-itself-sufficient](https://github.com/ytdl-org/youtube-dl#is-the-description-of-the-issue-itself-sufficient).
Provide any additional information, any suggested solutions, and as much context and examples as possible
placeholder: WRITE DESCRIPTION HERE
validations:
required: true

View File

@@ -0,0 +1,30 @@
name: Ask question
description: Ask yt-dlp related question
labels: [question]
body:
- type: checkboxes
id: checklist
attributes:
label: Checklist
description: |
Carefully read and work through this check list in order to prevent the most common mistakes and misuse of yt-dlp:
options:
- label: I'm asking a question and not reporting a bug/feature request
required: true
- label: I've looked through the [README](https://github.com/yt-dlp/yt-dlp#readme)
required: true
- label: I've read the [guidelines for opening an issue](https://github.com/yt-dlp/yt-dlp/blob/master/CONTRIBUTING.md#opening-an-issue)
required: true
- label: I've searched the [bugtracker](https://github.com/yt-dlp/yt-dlp/issues?q=) for similar questions including closed ones
required: true
- type: textarea
id: question
attributes:
label: Question
description: |
Ask your question in an arbitrary form.
Please make sure it's worded well enough to be understood, see [is-the-description-of-the-issue-itself-sufficient](https://github.com/ytdl-org/youtube-dl#is-the-description-of-the-issue-itself-sufficient).
Provide any additional information and as much context and examples as possible
placeholder: WRITE QUESTION HERE
validations:
required: true

View File

@@ -7,7 +7,7 @@
--- ---
### Before submitting a *pull request* make sure you have: ### Before submitting a *pull request* make sure you have:
- [ ] At least skimmed through [adding new extractor tutorial](https://github.com/ytdl-org/youtube-dl#adding-support-for-a-new-site) and [youtube-dl coding conventions](https://github.com/ytdl-org/youtube-dl#youtube-dl-coding-conventions) sections - [ ] At least skimmed through [contributing guidelines](https://github.com/yt-dlp/yt-dlp/blob/master/CONTRIBUTING.md#developer-instructions) including [yt-dlp coding conventions](https://github.com/yt-dlp/yt-dlp/blob/master/CONTRIBUTING.md#yt-dlp-coding-conventions)
- [ ] [Searched](https://github.com/yt-dlp/yt-dlp/search?q=is%3Apr&type=Issues) the bugtracker for similar pull requests - [ ] [Searched](https://github.com/yt-dlp/yt-dlp/search?q=is%3Apr&type=Issues) the bugtracker for similar pull requests
- [ ] Checked the code with [flake8](https://pypi.python.org/pypi/flake8) - [ ] Checked the code with [flake8](https://pypi.python.org/pypi/flake8)

View File

@@ -8,15 +8,18 @@ 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 }}
sha256_unix: ${{ steps.sha256_file.outputs.sha256_unix }} sha256_bin: ${{ steps.sha256_bin.outputs.sha256_bin }}
sha512_unix: ${{ steps.sha512_file.outputs.sha512_unix }} sha512_bin: ${{ steps.sha512_bin.outputs.sha512_bin }}
sha256_tar: ${{ steps.sha256_tar.outputs.sha256_tar }}
sha512_tar: ${{ steps.sha512_tar.outputs.sha512_tar }}
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
with:
fetch-depth: 0
- name: Set up Python - name: Set up Python
uses: actions/setup-python@v2 uses: actions/setup-python@v2
with: with:
@@ -25,11 +28,83 @@ jobs:
run: sudo apt-get -y install zip pandoc man run: sudo apt-get -y install zip pandoc man
- name: Bump version - name: Bump version
id: bump_version id: bump_version
run: python devscripts/update-version.py run: |
python devscripts/update-version.py
make issuetemplates
- name: Print version - name: Print version
run: echo "${{ steps.bump_version.outputs.ytdlp_version }}" run: echo "${{ steps.bump_version.outputs.ytdlp_version }}"
- name: Update master
id: push_update
run: |
git config --global user.email "${{ github.event.pusher.email }}"
git config --global user.name "${{ github.event.pusher.name }}"
git add -u
git commit -m "[version] update" -m ":ci skip all"
git pull --rebase origin ${{ github.event.repository.master_branch }}
git push origin ${{ github.event.ref }}:${{ github.event.repository.master_branch }}
echo ::set-output name=head_sha::$(git rev-parse HEAD)
- name: Get Changelog
id: get_changelog
run: |
changelog=$(cat Changelog.md | grep -oPz '(?s)(?<=### ${{ steps.bump_version.outputs.ytdlp_version }}\n{2}).+?(?=\n{2,3}###)') || true
echo "changelog<<EOF" >> $GITHUB_ENV
echo "$changelog" >> $GITHUB_ENV
echo "EOF" >> $GITHUB_ENV
- name: Build lazy extractors
id: lazy_extractors
run: python devscripts/make_lazy_extractors.py
- name: Run Make - name: Run Make
run: make all tar run: make all tar
- name: Get SHA2-256SUMS for yt-dlp
id: sha256_bin
run: echo "::set-output name=sha256_bin::$(sha256sum yt-dlp | awk '{print $1}')"
- name: Get SHA2-256SUMS for yt-dlp.tar.gz
id: sha256_tar
run: echo "::set-output name=sha256_tar::$(sha256sum yt-dlp.tar.gz | awk '{print $1}')"
- name: Get SHA2-512SUMS for yt-dlp
id: sha512_bin
run: echo "::set-output name=sha512_bin::$(sha512sum yt-dlp | awk '{print $1}')"
- name: Get SHA2-512SUMS for yt-dlp.tar.gz
id: sha512_tar
run: echo "::set-output name=sha512_tar::$(sha512sum yt-dlp.tar.gz | awk '{print $1}')"
- name: Install dependencies for pypi
env:
PYPI_TOKEN: ${{ secrets.PYPI_TOKEN }}
if: "env.PYPI_TOKEN != ''"
run: |
python -m pip install --upgrade pip
pip install setuptools wheel twine
- name: Build and publish on pypi
env:
TWINE_USERNAME: __token__
TWINE_PASSWORD: ${{ secrets.PYPI_TOKEN }}
if: "env.TWINE_PASSWORD != ''"
run: |
rm -rf dist/*
python setup.py sdist bdist_wheel
twine upload dist/*
- name: Install SSH private key
env:
BREW_TOKEN: ${{ secrets.BREW_TOKEN }}
if: "env.BREW_TOKEN != ''"
uses: webfactory/ssh-agent@v0.5.3
with:
ssh-private-key: ${{ env.BREW_TOKEN }}
- name: Update Homebrew Formulae
env:
BREW_TOKEN: ${{ secrets.BREW_TOKEN }}
if: "env.BREW_TOKEN != ''"
run: |
git clone git@github.com:yt-dlp/homebrew-taps taps/
python3 devscripts/update-formulae.py taps/Formula/yt-dlp.rb "${{ steps.bump_version.outputs.ytdlp_version }}"
git -C taps/ config user.name github-actions
git -C taps/ config user.email github-actions@example.com
git -C taps/ commit -am 'yt-dlp: ${{ steps.bump_version.outputs.ytdlp_version }}'
git -C taps/ push
- name: Create Release - name: Create Release
id: create_release id: create_release
uses: actions/create-release@v1 uses: actions/create-release@v1
@@ -38,9 +113,14 @@ jobs:
with: with:
tag_name: ${{ steps.bump_version.outputs.ytdlp_version }} tag_name: ${{ steps.bump_version.outputs.ytdlp_version }}
release_name: yt-dlp ${{ steps.bump_version.outputs.ytdlp_version }} release_name: yt-dlp ${{ steps.bump_version.outputs.ytdlp_version }}
commitish: ${{ steps.push_update.outputs.head_sha }}
body: | body: |
Changelog: ### Changelog:
PLACEHOLDER ${{ env.changelog }}
---
### See [this](https://github.com/yt-dlp/yt-dlp#release-files) for a description of the release files
draft: false draft: false
prerelease: false prerelease: false
- name: Upload yt-dlp Unix binary - name: Upload yt-dlp Unix binary
@@ -62,36 +142,81 @@ jobs:
asset_path: ./yt-dlp.tar.gz asset_path: ./yt-dlp.tar.gz
asset_name: yt-dlp.tar.gz asset_name: yt-dlp.tar.gz
asset_content_type: application/gzip asset_content_type: application/gzip
- name: Get SHA2-256SUMS for yt-dlp
id: sha256_file build_macos:
run: echo "::set-output name=sha256_unix::$(sha256sum yt-dlp | awk '{print $1}')" runs-on: macos-11
- name: Get SHA2-512SUMS for yt-dlp needs: build_unix
id: sha512_file outputs:
run: echo "::set-output name=sha512_unix::$(sha512sum yt-dlp | awk '{print $1}')" sha256_macos: ${{ steps.sha256_macos.outputs.sha256_macos }}
- name: Install dependencies for pypi sha512_macos: ${{ steps.sha512_macos.outputs.sha512_macos }}
env: sha256_macos_zip: ${{ steps.sha256_macos_zip.outputs.sha256_macos_zip }}
PYPI_TOKEN: ${{ secrets.PYPI_TOKEN }} sha512_macos_zip: ${{ steps.sha512_macos_zip.outputs.sha512_macos_zip }}
if: "env.PYPI_TOKEN != ''"
steps:
- uses: actions/checkout@v2
# In order to create a universal2 application, the version of python3 in /usr/bin has to be used
- name: Install Requirements
run: | run: |
python -m pip install --upgrade pip brew install coreutils
pip install setuptools wheel twine /usr/bin/python3 -m pip install -U --user pip Pyinstaller mutagen pycryptodomex websockets
- name: Build and publish on pypi - name: Bump version
id: bump_version
run: /usr/bin/python3 devscripts/update-version.py
- name: Build lazy extractors
id: lazy_extractors
run: /usr/bin/python3 devscripts/make_lazy_extractors.py
- name: Run PyInstaller Script
run: /usr/bin/python3 pyinst.py --target-architecture universal2 --onefile
- name: Upload yt-dlp MacOS binary
id: upload-release-macos
uses: actions/upload-release-asset@v1
env: env:
TWINE_USERNAME: __token__ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
TWINE_PASSWORD: ${{ secrets.PYPI_TOKEN }} with:
if: "env.TWINE_PASSWORD != ''" upload_url: ${{ needs.build_unix.outputs.upload_url }}
run: | asset_path: ./dist/yt-dlp_macos
rm -rf dist/* asset_name: yt-dlp_macos
python setup.py sdist bdist_wheel asset_content_type: application/octet-stream
twine upload dist/* - name: Get SHA2-256SUMS for yt-dlp_macos
id: sha256_macos
run: echo "::set-output name=sha256_macos::$(sha256sum dist/yt-dlp_macos | awk '{print $1}')"
- name: Get SHA2-512SUMS for yt-dlp_macos
id: sha512_macos
run: echo "::set-output name=sha512_macos::$(sha512sum dist/yt-dlp_macos | awk '{print $1}')"
- name: Run PyInstaller Script with --onedir
run: /usr/bin/python3 pyinst.py --target-architecture universal2 --onedir
- uses: papeloto/action-zip@v1
with:
files: ./dist/yt-dlp_macos
dest: ./dist/yt-dlp_macos.zip
- name: Upload yt-dlp MacOS onedir
id: upload-release-macos-zip
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ needs.build_unix.outputs.upload_url }}
asset_path: ./dist/yt-dlp_macos.zip
asset_name: yt-dlp_macos.zip
asset_content_type: application/zip
- name: Get SHA2-256SUMS for yt-dlp_macos.zip
id: sha256_macos_zip
run: echo "::set-output name=sha256_macos_zip::$(sha256sum dist/yt-dlp_macos.zip | awk '{print $1}')"
- name: Get SHA2-512SUMS for yt-dlp_macos
id: sha512_macos_zip
run: echo "::set-output name=sha512_macos_zip::$(sha512sum dist/yt-dlp_macos.zip | awk '{print $1}')"
build_windows: build_windows:
runs-on: windows-latest runs-on: windows-latest
needs: build_unix needs: build_unix
outputs: outputs:
sha256_windows: ${{ steps.sha256_file_win.outputs.sha256_windows }} sha256_win: ${{ steps.sha256_win.outputs.sha256_win }}
sha512_windows: ${{ steps.sha512_file_win.outputs.sha512_windows }} sha512_win: ${{ steps.sha512_win.outputs.sha512_win }}
sha256_py2exe: ${{ steps.sha256_py2exe.outputs.sha256_py2exe }}
sha512_py2exe: ${{ steps.sha512_py2exe.outputs.sha512_py2exe }}
sha256_win_zip: ${{ steps.sha256_win_zip.outputs.sha256_win_zip }}
sha512_win_zip: ${{ steps.sha512_win_zip.outputs.sha512_win_zip }}
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
@@ -100,18 +225,19 @@ jobs:
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
run: python -m pip install --upgrade pip setuptools wheel
- name: Install Requirements - name: Install Requirements
# Custom pyinstaller built with https://github.com/yt-dlp/pyinstaller-builds # Custom pyinstaller built with https://github.com/yt-dlp/pyinstaller-builds
run: pip install "https://yt-dlp.github.io/pyinstaller-builds/x86_64/pyinstaller-4.5.1-py3-none-any.whl" mutagen pycryptodome websockets run: |
python -m pip install --upgrade pip setuptools wheel py2exe
pip install "https://yt-dlp.github.io/Pyinstaller-Builds/x86_64/pyinstaller-4.5.1-py3-none-any.whl" mutagen pycryptodomex websockets
- name: Bump version - name: Bump version
id: bump_version id: bump_version
run: python devscripts/update-version.py run: python devscripts/update-version.py
- name: Print version - name: Build lazy extractors
run: echo "${{ steps.bump_version.outputs.ytdlp_version }}" id: lazy_extractors
run: python devscripts/make_lazy_extractors.py
- name: Run PyInstaller Script - name: Run PyInstaller Script
run: python pyinst.py 64 run: python pyinst.py
- name: Upload yt-dlp.exe Windows binary - name: Upload yt-dlp.exe Windows binary
id: upload-release-windows id: upload-release-windows
uses: actions/upload-release-asset@v1 uses: actions/upload-release-asset@v1
@@ -123,19 +249,61 @@ 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: sha256_file_win id: sha256_win
run: echo "::set-output name=sha256_windows::$((Get-FileHash dist\yt-dlp.exe -Algorithm SHA256).Hash.ToLower())" run: echo "::set-output name=sha256_win::$((Get-FileHash dist\yt-dlp.exe -Algorithm SHA256).Hash.ToLower())"
- name: Get SHA2-512SUMS for yt-dlp.exe - name: Get SHA2-512SUMS for yt-dlp.exe
id: sha512_file_win id: sha512_win
run: echo "::set-output name=sha512_windows::$((Get-FileHash dist\yt-dlp.exe -Algorithm SHA512).Hash.ToLower())" run: echo "::set-output name=sha512_win::$((Get-FileHash dist\yt-dlp.exe -Algorithm SHA512).Hash.ToLower())"
- name: Run PyInstaller Script with --onedir
run: python pyinst.py --onedir
- uses: papeloto/action-zip@v1
with:
files: ./dist/yt-dlp
dest: ./dist/yt-dlp_win.zip
- name: Upload yt-dlp Windows onedir
id: upload-release-windows-zip
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ needs.build_unix.outputs.upload_url }}
asset_path: ./dist/yt-dlp_win.zip
asset_name: yt-dlp_win.zip
asset_content_type: application/zip
- name: Get SHA2-256SUMS for yt-dlp_win.zip
id: sha256_win_zip
run: echo "::set-output name=sha256_win_zip::$((Get-FileHash dist\yt-dlp_win.zip -Algorithm SHA256).Hash.ToLower())"
- name: Get SHA2-512SUMS for yt-dlp_win.zip
id: sha512_win_zip
run: echo "::set-output name=sha512_win_zip::$((Get-FileHash dist\yt-dlp_win.zip -Algorithm SHA512).Hash.ToLower())"
- name: Run py2exe Script
run: python setup.py py2exe
- name: Upload yt-dlp_min.exe Windows binary
id: upload-release-windows-py2exe
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ needs.build_unix.outputs.upload_url }}
asset_path: ./dist/yt-dlp.exe
asset_name: yt-dlp_min.exe
asset_content_type: application/vnd.microsoft.portable-executable
- name: Get SHA2-256SUMS for yt-dlp_min.exe
id: sha256_py2exe
run: echo "::set-output name=sha256_py2exe::$((Get-FileHash dist\yt-dlp.exe -Algorithm SHA256).Hash.ToLower())"
- name: Get SHA2-512SUMS for yt-dlp_min.exe
id: sha512_py2exe
run: echo "::set-output name=sha512_py2exe::$((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] needs: build_unix
outputs: outputs:
sha256_windows32: ${{ steps.sha256_file_win32.outputs.sha256_windows32 }} sha256_win32: ${{ steps.sha256_win32.outputs.sha256_win32 }}
sha512_windows32: ${{ steps.sha512_file_win32.outputs.sha512_windows32 }} sha512_win32: ${{ steps.sha512_win32.outputs.sha512_win32 }}
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
@@ -145,17 +313,18 @@ jobs:
with: with:
python-version: '3.7' python-version: '3.7'
architecture: 'x86' architecture: 'x86'
- name: Upgrade pip and enable wheel support
run: python -m pip install --upgrade pip setuptools wheel
- name: Install Requirements - name: Install Requirements
run: pip install "https://yt-dlp.github.io/pyinstaller-builds/i686/pyinstaller-4.5.1-py3-none-any.whl" mutagen pycryptodome websockets run: |
python -m pip install --upgrade pip setuptools wheel
pip install "https://yt-dlp.github.io/Pyinstaller-Builds/i686/pyinstaller-4.5.1-py3-none-any.whl" mutagen pycryptodomex websockets
- name: Bump version - name: Bump version
id: bump_version id: bump_version
run: python devscripts/update-version.py run: python devscripts/update-version.py
- name: Print version - name: Build lazy extractors
run: echo "${{ steps.bump_version.outputs.ytdlp_version }}" id: lazy_extractors
run: python devscripts/make_lazy_extractors.py
- name: Run PyInstaller Script for 32 Bit - name: Run PyInstaller Script for 32 Bit
run: python pyinst.py 32 run: python pyinst.py
- name: Upload Executable yt-dlp_x86.exe - name: Upload Executable yt-dlp_x86.exe
id: upload-release-windows32 id: upload-release-windows32
uses: actions/upload-release-asset@v1 uses: actions/upload-release-asset@v1
@@ -167,28 +336,36 @@ 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: sha256_file_win32 id: sha256_win32
run: echo "::set-output name=sha256_windows32::$((Get-FileHash dist\yt-dlp_x86.exe -Algorithm SHA256).Hash.ToLower())" run: echo "::set-output name=sha256_win32::$((Get-FileHash dist\yt-dlp_x86.exe -Algorithm SHA256).Hash.ToLower())"
- name: Get SHA2-512SUMS for yt-dlp_x86.exe - name: Get SHA2-512SUMS for yt-dlp_x86.exe
id: sha512_file_win32 id: sha512_win32
run: echo "::set-output name=sha512_windows32::$((Get-FileHash dist\yt-dlp_x86.exe -Algorithm SHA512).Hash.ToLower())" run: echo "::set-output name=sha512_win32::$((Get-FileHash dist\yt-dlp_x86.exe -Algorithm SHA512).Hash.ToLower())"
finish: finish:
runs-on: ubuntu-latest runs-on: ubuntu-latest
needs: [build_unix, build_windows, build_windows32] needs: [build_unix, build_windows, build_windows32, build_macos]
steps: steps:
- name: Make SHA2-256SUMS file - name: Make SHA2-256SUMS file
env: env:
SHA256_WINDOWS: ${{ needs.build_windows.outputs.sha256_windows }} SHA256_BIN: ${{ needs.build_unix.outputs.sha256_bin }}
SHA256_WINDOWS32: ${{ needs.build_windows32.outputs.sha256_windows32 }} SHA256_TAR: ${{ needs.build_unix.outputs.sha256_tar }}
SHA256_UNIX: ${{ needs.build_unix.outputs.sha256_unix }} SHA256_WIN: ${{ needs.build_windows.outputs.sha256_win }}
YTDLP_VERSION: ${{ needs.build_unix.outputs.ytdlp_version }} SHA256_PY2EXE: ${{ needs.build_windows.outputs.sha256_py2exe }}
SHA256_WIN_ZIP: ${{ needs.build_windows.outputs.sha256_win_zip }}
SHA256_WIN32: ${{ needs.build_windows32.outputs.sha256_win32 }}
SHA256_MACOS: ${{ needs.build_macos.outputs.sha256_macos }}
SHA256_MACOS_ZIP: ${{ needs.build_macos.outputs.sha256_macos_zip }}
run: | run: |
echo "version:${{ env.YTDLP_VERSION }}" >> SHA2-256SUMS echo "${{ env.SHA256_BIN }} yt-dlp" >> SHA2-256SUMS
echo "yt-dlp.exe:${{ env.SHA256_WINDOWS }}" >> SHA2-256SUMS echo "${{ env.SHA256_TAR }} yt-dlp.tar.gz" >> SHA2-256SUMS
echo "yt-dlp_x86.exe:${{ env.SHA256_WINDOWS32 }}" >> SHA2-256SUMS echo "${{ env.SHA256_WIN }} yt-dlp.exe" >> SHA2-256SUMS
echo "yt-dlp:${{ env.SHA256_UNIX }}" >> SHA2-256SUMS echo "${{ env.SHA256_PY2EXE }} yt-dlp_min.exe" >> SHA2-256SUMS
echo "${{ env.SHA256_WIN32 }} yt-dlp_x86.exe" >> SHA2-256SUMS
echo "${{ env.SHA256_WIN_ZIP }} yt-dlp_win.zip" >> SHA2-256SUMS
echo "${{ env.SHA256_MACOS }} yt-dlp_macos" >> SHA2-256SUMS
echo "${{ env.SHA256_MACOS_ZIP }} yt-dlp_macos.zip" >> 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
@@ -201,13 +378,23 @@ jobs:
asset_content_type: text/plain asset_content_type: text/plain
- name: Make SHA2-512SUMS file - name: Make SHA2-512SUMS file
env: env:
SHA512_WINDOWS: ${{ needs.build_windows.outputs.sha512_windows }} SHA512_BIN: ${{ needs.build_unix.outputs.sha512_bin }}
SHA512_WINDOWS32: ${{ needs.build_windows32.outputs.sha512_windows32 }} SHA512_TAR: ${{ needs.build_unix.outputs.sha512_tar }}
SHA512_UNIX: ${{ needs.build_unix.outputs.sha512_unix }} SHA512_WIN: ${{ needs.build_windows.outputs.sha512_win }}
SHA512_PY2EXE: ${{ needs.build_windows.outputs.sha512_py2exe }}
SHA512_WIN_ZIP: ${{ needs.build_windows.outputs.sha512_win_zip }}
SHA512_WIN32: ${{ needs.build_windows32.outputs.sha512_win32 }}
SHA512_MACOS: ${{ needs.build_macos.outputs.sha512_macos }}
SHA512_MACOS_ZIP: ${{ needs.build_macos.outputs.sha512_macos_zip }}
run: | run: |
echo "${{ env.SHA512_WINDOWS }} yt-dlp.exe" >> SHA2-512SUMS echo "${{ env.SHA512_BIN }} yt-dlp" >> SHA2-512SUMS
echo "${{ env.SHA512_WINDOWS32 }} yt-dlp_x86.exe" >> SHA2-512SUMS echo "${{ env.SHA512_TAR }} yt-dlp.tar.gz" >> SHA2-512SUMS
echo "${{ env.SHA512_UNIX }} yt-dlp" >> SHA2-512SUMS echo "${{ env.SHA512_WIN }} yt-dlp.exe" >> SHA2-512SUMS
echo "${{ env.SHA512_WIN_ZIP }} yt-dlp_win.zip" >> SHA2-512SUMS
echo "${{ env.SHA512_PY2EXE }} yt-dlp_min.exe" >> SHA2-512SUMS
echo "${{ env.SHA512_WIN32 }} yt-dlp_x86.exe" >> SHA2-512SUMS
echo "${{ env.SHA512_MACOS }} yt-dlp_macos" >> SHA2-512SUMS
echo "${{ env.SHA512_MACOS_ZIP }} yt-dlp_macos.zip" >> SHA2-512SUMS
- name: Upload 512SUMS file - name: Upload 512SUMS file
id: upload-512sums id: upload-512sums
uses: actions/upload-release-asset@v1 uses: actions/upload-release-asset@v1

View File

@@ -12,7 +12,7 @@ jobs:
with: with:
python-version: 3.9 python-version: 3.9
- name: Install test requirements - name: Install test requirements
run: pip install pytest pycryptodome run: pip install pytest pycryptodomex
- name: Run tests - name: Run tests
run: ./devscripts/run_tests.sh core run: ./devscripts/run_tests.sh core
flake8: flake8:
@@ -28,6 +28,6 @@ jobs:
- name: Install flake8 - name: Install flake8
run: pip install flake8 run: pip install flake8
- name: Make lazy extractors - name: Make lazy extractors
run: python devscripts/make_lazy_extractors.py yt_dlp/extractor/lazy_extractors.py run: python devscripts/make_lazy_extractors.py
- name: Run flake8 - name: Run flake8
run: flake8 . run: flake8 .

5
.gitignore vendored
View File

@@ -2,7 +2,8 @@
*.conf *.conf
*.spec *.spec
cookies cookies
cookies.txt *cookies.txt
.netrc
# Downloaded # Downloaded
*.srt *.srt
@@ -42,7 +43,7 @@ cookies.txt
*.description *.description
# Allow config/media files in testdata # Allow config/media files in testdata
!test/testdata/** !test/**
# Python # Python
*.pyc *.pyc

View File

@@ -1,26 +1,59 @@
**Please include the full output of youtube-dl when run with `-v`**, i.e. **add** `-v` flag to **your command line**, copy the **whole** output and post it in the issue body wrapped in \`\`\` for better formatting. It should look similar to this: # CONTRIBUTING TO YT-DLP
- [OPENING AN ISSUE](#opening-an-issue)
- [Is the description of the issue itself sufficient?](#is-the-description-of-the-issue-itself-sufficient)
- [Are you using the latest version?](#are-you-using-the-latest-version)
- [Is the issue already documented?](#is-the-issue-already-documented)
- [Why are existing options not enough?](#why-are-existing-options-not-enough)
- [Have you read and understood the changes, between youtube-dl and yt-dlp](#have-you-read-and-understood-the-changes-between-youtube-dl-and-yt-dlp)
- [Is there enough context in your bug report?](#is-there-enough-context-in-your-bug-report)
- [Does the issue involve one problem, and one problem only?](#does-the-issue-involve-one-problem-and-one-problem-only)
- [Is anyone going to need the feature?](#is-anyone-going-to-need-the-feature)
- [Is your question about yt-dlp?](#is-your-question-about-yt-dlp)
- [DEVELOPER INSTRUCTIONS](#developer-instructions)
- [Adding new feature or making overarching changes](#adding-new-feature-or-making-overarching-changes)
- [Adding support for a new site](#adding-support-for-a-new-site)
- [yt-dlp coding conventions](#yt-dlp-coding-conventions)
- [Mandatory and optional metafields](#mandatory-and-optional-metafields)
- [Provide fallbacks](#provide-fallbacks)
- [Regular expressions](#regular-expressions)
- [Long lines policy](#long-lines-policy)
- [Inline values](#inline-values)
- [Collapse fallbacks](#collapse-fallbacks)
- [Trailing parentheses](#trailing-parentheses)
- [Use convenience conversion and parsing functions](#use-convenience-conversion-and-parsing-functions)
- [EMBEDDING YT-DLP](README.md#embedding-yt-dlp)
# OPENING AN ISSUE
Bugs and suggestions should be reported at: [yt-dlp/yt-dlp/issues](https://github.com/yt-dlp/yt-dlp/issues). Unless you were prompted to or there is another pertinent reason (e.g. GitHub fails to accept the bug report), please do not send bug reports via personal email. For discussions, join us in our [discord server](https://discord.gg/H5MNcFW63r).
**Please include the full output of yt-dlp when run with `-Uv`**, i.e. **add** `-Uv` flag to **your command line**, copy the **whole** output and post it in the issue body wrapped in \`\`\` for better formatting. It should look similar to this:
``` ```
$ youtube-dl -v <your command line> $ yt-dlp -Uv <your command line>
[debug] System config: [] [debug] Command-line config: ['-v', 'demo.com']
[debug] User config: [] [debug] Encodings: locale UTF-8, fs utf-8, out utf-8, pref UTF-8
[debug] Command-line args: [u'-v', u'https://www.youtube.com/watch?v=BaW_jenozKc'] [debug] yt-dlp version 2021.09.25 (zip)
[debug] Encodings: locale cp1251, fs mbcs, out cp866, pref cp1251 [debug] Python version 3.8.10 (CPython 64bit) - Linux-5.4.0-74-generic-x86_64-with-glibc2.29
[debug] youtube-dl version 2015.12.06 [debug] exe versions: ffmpeg 4.2.4, ffprobe 4.2.4
[debug] Git HEAD: 135392e
[debug] Python version 2.6.6 - Windows-2003Server-5.2.3790-SP2
[debug] exe versions: ffmpeg N-75573-g1d0487f, ffprobe N-75573-g1d0487f, rtmpdump 2.4
[debug] Proxy map: {} [debug] Proxy map: {}
Current Build Hash 25cc412d1d3c0725a1f2f5b7e4682f6fb40e6d15f7024e96f7afd572e9919535
yt-dlp is up to date (2021.09.25)
... ...
``` ```
**Do not post screenshots of verbose logs; only plain text is acceptable.** **Do not post screenshots of verbose logs; only plain text is acceptable.**
The output (including the first lines) contains important debugging information. Issues without the full output are often not reproducible and therefore do not get solved in short order, if ever. The output (including the first lines) contains important debugging information. Issues without the full output are often not reproducible and therefore will be closed as `incomplete`.
The templates provided for the Issues, should be completed and **not removed**, this helps aide the resolution of the issue.
Please re-read your issue once again to avoid a couple of common mistakes (you can and should use this as a checklist): Please re-read your issue once again to avoid a couple of common mistakes (you can and should use this as a checklist):
### Is the description of the issue itself sufficient? ### Is the description of the issue itself sufficient?
We often get issue reports that we cannot really decipher. While in most cases we eventually get the required information after asking back multiple times, this poses an unnecessary drain on our resources. Many contributors, including myself, are also not native speakers, so we may misread some parts. We often get issue reports that we cannot really decipher. While in most cases we eventually get the required information after asking back multiple times, this poses an unnecessary drain on our resources.
So please elaborate on what feature you are requesting, or what bug you want to be fixed. Make sure that it's obvious So please elaborate on what feature you are requesting, or what bug you want to be fixed. Make sure that it's obvious
@@ -28,25 +61,31 @@ So please elaborate on what feature you are requesting, or what bug you want to
- How it could be fixed - How it could be fixed
- How your proposed solution would look like - How your proposed solution would look like
If your report is shorter than two lines, it is almost certainly missing some of these, which makes it hard for us to respond to it. We're often too polite to close the issue outright, but the missing info makes misinterpretation likely. As a committer myself, I often get frustrated by these issues, since the only possible way for me to move forward on them is to ask for clarification over and over. If your report is shorter than two lines, it is almost certainly missing some of these, which makes it hard for us to respond to it. We're often too polite to close the issue outright, but the missing info makes misinterpretation likely. We often get frustrated by these issues, since the only possible way for us to move forward on them is to ask for clarification over and over.
For bug reports, this means that your report should contain the *complete* output of youtube-dl when called with the `-v` flag. The error message you get for (most) bugs even says so, but you would not believe how many of our bug reports do not contain this information. For bug reports, this means that your report should contain the **complete** output of yt-dlp when called with the `-Uv` flag. The error message you get for (most) bugs even says so, but you would not believe how many of our bug reports do not contain this information.
If your server has multiple IPs or you suspect censorship, adding `--call-home` may be a good idea to get more diagnostics. If the error is `ERROR: Unable to extract ...` and you cannot reproduce it from multiple countries, add `--dump-pages` (warning: this will yield a rather large output, redirect it to the file `log.txt` by adding `>log.txt 2>&1` to your command-line) or upload the `.dump` files you get when you add `--write-pages` [somewhere](https://gist.github.com/). If the error is `ERROR: Unable to extract ...` and you cannot reproduce it from multiple countries, add `--write-pages` and upload the `.dump` files you get [somewhere](https://gist.github.com).
**Site support requests must contain an example URL**. An example URL is a URL you might want to download, like `https://www.youtube.com/watch?v=BaW_jenozKc`. There should be an obvious video present. Except under very special circumstances, the main page of a video service (e.g. `https://www.youtube.com/`) is *not* an example URL. **Site support requests must contain an example URL**. An example URL is a URL you might want to download, like `https://www.youtube.com/watch?v=BaW_jenozKc`. There should be an obvious video present. Except under very special circumstances, the main page of a video service (e.g. `https://www.youtube.com/`) is *not* an example URL.
### Are you using the latest version? ### Are you using the latest version?
Before reporting any issue, type `youtube-dl -U`. This should report that you're up-to-date. About 20% of the reports we receive are already fixed, but people are using outdated versions. This goes for feature requests as well. Before reporting any issue, type `yt-dlp -U`. This should report that you're up-to-date. This goes for feature requests as well.
### Is the issue already documented? ### Is the issue already documented?
Make sure that someone has not already opened the issue you're trying to open. Search at the top of the window or browse the [GitHub Issues](https://github.com/ytdl-org/youtube-dl/search?type=Issues) of this repository. If there is an issue, feel free to write something along the lines of "This affects me as well, with version 2015.01.01. Here is some more information on the issue: ...". While some issues may be old, a new post into them often spurs rapid activity. Make sure that someone has not already opened the issue you're trying to open. Search at the top of the window or browse the [GitHub Issues](https://github.com/yt-dlp/yt-dlp/search?type=Issues) of this repository. If there is an issue, feel free to write something along the lines of "This affects me as well, with version 2021.01.01. Here is some more information on the issue: ...". While some issues may be old, a new post into them often spurs rapid activity.
Additionally, it is also helpful to see if the issue has already been documented in the [youtube-dl issue tracker](https://github.com/ytdl-org/youtube-dl/issues). If similar issues have already been reported in youtube-dl (but not in our issue tracker), links to them can be included in your issue report here.
### Why are existing options not enough? ### Why are existing options not enough?
Before requesting a new feature, please have a quick peek at [the list of supported options](https://github.com/ytdl-org/youtube-dl/blob/master/README.md#options). Many feature requests are for features that actually exist already! Please, absolutely do show off your work in the issue report and detail how the existing similar options do *not* solve your problem. Before requesting a new feature, please have a quick peek at [the list of supported options](README.md#usage-and-options). Many feature requests are for features that actually exist already! Please, absolutely do show off your work in the issue report and detail how the existing similar options do *not* solve your problem.
### Have you read and understood the changes, between youtube-dl and yt-dlp
There are many changes between youtube-dl and yt-dlp [(changes to default behavior)](README.md#differences-in-default-behavior), and some of the options available have a different behaviour in yt-dlp, or have been removed all together [(list of changes to options)](README.md#deprecated-options). Make sure you have read and understand the differences in the options and how this may impact your downloads before opening an issue.
### Is there enough context in your bug report? ### Is there enough context in your bug report?
@@ -58,23 +97,40 @@ We are then presented with a very complicated request when the original problem
Some of our users seem to think there is a limit of issues they can or should open. There is no limit of issues they can or should open. While it may seem appealing to be able to dump all your issues into one ticket, that means that someone who solves one of your issues cannot mark the issue as closed. Typically, reporting a bunch of issues leads to the ticket lingering since nobody wants to attack that behemoth, until someone mercifully splits the issue into multiple ones. Some of our users seem to think there is a limit of issues they can or should open. There is no limit of issues they can or should open. While it may seem appealing to be able to dump all your issues into one ticket, that means that someone who solves one of your issues cannot mark the issue as closed. Typically, reporting a bunch of issues leads to the ticket lingering since nobody wants to attack that behemoth, until someone mercifully splits the issue into multiple ones.
In particular, every site support request issue should only pertain to services at one site (generally under a common domain, but always using the same backend technology). Do not request support for vimeo user videos, White house podcasts, and Google Plus pages in the same issue. Also, make sure that you don't post bug reports alongside feature requests. As a rule of thumb, a feature request does not include outputs of youtube-dl that are not immediately related to the feature at hand. Do not post reports of a network error alongside the request for a new video service. In particular, every site support request issue should only pertain to services at one site (generally under a common domain, but always using the same backend technology). Do not request support for vimeo user videos, White house podcasts, and Google Plus pages in the same issue. Also, make sure that you don't post bug reports alongside feature requests. As a rule of thumb, a feature request does not include outputs of yt-dlp that are not immediately related to the feature at hand. Do not post reports of a network error alongside the request for a new video service.
### Is anyone going to need the feature? ### Is anyone going to need the feature?
Only post features that you (or an incapacitated friend you can personally talk to) require. Do not post features because they seem like a good idea. If they are really useful, they will be requested by someone who requires them. Only post features that you (or an incapacitated friend you can personally talk to) require. Do not post features because they seem like a good idea. If they are really useful, they will be requested by someone who requires them.
### Is your question about youtube-dl? ### Is your question about yt-dlp?
Some bug reports are completely unrelated to yt-dlp and relate to a different, or even the reporter's own, application. Please make sure that you are actually using yt-dlp. If you are using a UI for yt-dlp, report the bug to the maintainer of the actual application providing the UI. On the other hand, if your UI for yt-dlp fails in some way you believe is related to yt-dlp, by all means, go ahead and report the bug.
If the issue is with `youtube-dl` (the upstream fork of yt-dlp) and not with yt-dlp, the issue should be raised in the youtube-dl project.
### Are you willing to share account details if needed?
The maintainers and potential contributors of the project often do not have an account for the website you are asking support for. So any developer interested in solving your issue may ask you for account details. It is your personal discression whether you are willing to share the account in order for the developer to try and solve your issue. However, if you are unwilling or unable to provide details, they obviously cannot work on the issue and it cannot be solved unless some developer who both has an account and is willing/able to contribute decides to solve it.
By sharing an account with anyone, you agree to bear all risks associated with it. The maintainers and yt-dlp can't be held responsible for any misuse of the credentials.
While these steps won't necessarily ensure that no misuse of the account takes place, these are still some good practices to follow.
- Look for people with `Member` or `Contributor` tag on their messages.
- Change the password before sharing the account to something random (use [this](https://passwordsgenerator.net/) if you don't have a random password generator).
- Change the password after receiving the account back.
It may sound strange, but some bug reports we receive are completely unrelated to youtube-dl and relate to a different, or even the reporter's own, application. Please make sure that you are actually using youtube-dl. If you are using a UI for youtube-dl, report the bug to the maintainer of the actual application providing the UI. On the other hand, if your UI for youtube-dl fails in some way you believe is related to youtube-dl, by all means, go ahead and report the bug.
# DEVELOPER INSTRUCTIONS # DEVELOPER INSTRUCTIONS
Most users do not need to build youtube-dl and can [download the builds](https://ytdl-org.github.io/youtube-dl/download.html) or get them from their distribution. Most users do not need to build yt-dlp and can [download the builds](https://github.com/yt-dlp/yt-dlp/releases) or get them via [the other installation methods](README.md#installation).
To run youtube-dl as a developer, you don't need to build anything either. Simply execute To run yt-dlp as a developer, you don't need to build anything either. Simply execute
python -m youtube_dl python -m yt_dlp
To run the test, simply invoke your favorite test runner, or execute a test file directly; any of the following work: To run the test, simply invoke your favorite test runner, or execute a test file directly; any of the following work:
@@ -85,42 +141,42 @@ To run the test, simply invoke your favorite test runner, or execute a test file
See item 6 of [new extractor tutorial](#adding-support-for-a-new-site) for how to run extractor specific test cases. See item 6 of [new extractor tutorial](#adding-support-for-a-new-site) for how to run extractor specific test cases.
If you want to create a build of youtube-dl yourself, you'll need If you want to create a build of yt-dlp yourself, you can follow the instructions [here](README.md#compile).
* python3
* make (only GNU make is supported)
* pandoc
* zip
* pytest
### Adding support for a new site ## Adding new feature or making overarching changes
If you want to add support for a new site, first of all **make sure** this site is **not dedicated to [copyright infringement](README.md#can-you-add-support-for-this-anime-video-site-or-site-which-shows-current-movies-for-free)**. youtube-dl does **not support** such sites thus pull requests adding support for them **will be rejected**. Before you start writing code for implementing a new feature, open an issue explaining your feature request and atleast one use case. This allows the maintainers to decide whether such a feature is desired for the project in the first place, and will provide an avenue to discuss some implementation details. If you open a pull request for a new feature without discussing with us first, do not be surprised when we ask for large changes to the code, or even reject it outright.
The same applies for overarching changes to the architecture, documentation or code style
## Adding support for a new site
If you want to add support for a new site, first of all **make sure** this site is **not dedicated to [copyright infringement](https://www.github.com/ytdl-org/youtube-dl#can-you-add-support-for-this-anime-video-site-or-site-which-shows-current-movies-for-free)**. yt-dlp does **not support** such sites thus pull requests adding support for them **will be rejected**.
After you have ensured this site is distributing its content legally, you can follow this quick list (assuming your service is called `yourextractor`): After you have ensured this site is distributing its content legally, you can follow this quick list (assuming your service is called `yourextractor`):
1. [Fork this repository](https://github.com/ytdl-org/youtube-dl/fork) 1. [Fork this repository](https://github.com/yt-dlp/yt-dlp/fork)
2. Check out the source code with: 1. Check out the source code with:
git clone git@github.com:YOUR_GITHUB_USERNAME/youtube-dl.git git clone git@github.com:YOUR_GITHUB_USERNAME/yt-dlp.git
3. Start a new git branch with 1. Start a new git branch with
cd youtube-dl cd yt-dlp
git checkout -b yourextractor git checkout -b yourextractor
4. Start with this simple template and save it to `youtube_dl/extractor/yourextractor.py`: 1. Start with this simple template and save it to `yt_dlp/extractor/yourextractor.py`:
```python ```python
# coding: utf-8 # coding: utf-8
from __future__ import unicode_literals
from .common import InfoExtractor from .common import InfoExtractor
class YourExtractorIE(InfoExtractor): class YourExtractorIE(InfoExtractor):
_VALID_URL = r'https?://(?:www\.)?yourextractor\.com/watch/(?P<id>[0-9]+)' _VALID_URL = r'https?://(?:www\.)?yourextractor\.com/watch/(?P<id>[0-9]+)'
_TEST = { _TESTS = [{
'url': 'https://yourextractor.com/watch/42', 'url': 'https://yourextractor.com/watch/42',
'md5': 'TODO: md5 sum of the first 10241 bytes of the video file (use --test)', 'md5': 'TODO: md5 sum of the first 10241 bytes of the video file (use --test)',
'info_dict': { 'info_dict': {
@@ -134,7 +190,7 @@ After you have ensured this site is distributing its content legally, you can fo
# * A regular expression; start the string with re: # * A regular expression; start the string with re:
# * Any Python type (for example int or float) # * Any Python type (for example int or float)
} }
} }]
def _real_extract(self, url): def _real_extract(self, url):
video_id = self._match_id(url) video_id = self._match_id(url)
@@ -148,45 +204,48 @@ After you have ensured this site is distributing its content legally, you can fo
'title': title, 'title': title,
'description': self._og_search_description(webpage), 'description': self._og_search_description(webpage),
'uploader': self._search_regex(r'<div[^>]+id="uploader"[^>]*>([^<]+)<', webpage, 'uploader', fatal=False), 'uploader': self._search_regex(r'<div[^>]+id="uploader"[^>]*>([^<]+)<', webpage, 'uploader', fatal=False),
# TODO more properties (see youtube_dl/extractor/common.py) # TODO more properties (see yt_dlp/extractor/common.py)
} }
``` ```
5. Add an import in [`youtube_dl/extractor/extractors.py`](https://github.com/ytdl-org/youtube-dl/blob/master/youtube_dl/extractor/extractors.py). 1. Add an import in [`yt_dlp/extractor/extractors.py`](yt_dlp/extractor/extractors.py).
6. Run `python test/test_download.py TestDownload.test_YourExtractor`. This *should fail* at first, but you can continually re-run it until you're done. If you decide to add more than one test, then rename ``_TEST`` to ``_TESTS`` and make it into a list of dictionaries. The tests will then be named `TestDownload.test_YourExtractor`, `TestDownload.test_YourExtractor_1`, `TestDownload.test_YourExtractor_2`, etc. Note that tests with `only_matching` key in test's dict are not counted in. 1. Run `python test/test_download.py TestDownload.test_YourExtractor`. This *should fail* at first, but you can continually re-run it until you're done. If you decide to add more than one test, the tests will then be named `TestDownload.test_YourExtractor`, `TestDownload.test_YourExtractor_1`, `TestDownload.test_YourExtractor_2`, etc. Note that tests with `only_matching` key in test's dict are not counted in. You can also run all the tests in one go with `TestDownload.test_YourExtractor_all`
7. Have a look at [`youtube_dl/extractor/common.py`](https://github.com/ytdl-org/youtube-dl/blob/master/youtube_dl/extractor/common.py) for possible helper methods and a [detailed description of what your extractor should and may return](https://github.com/ytdl-org/youtube-dl/blob/7f41a598b3fba1bcab2817de64a08941200aa3c8/youtube_dl/extractor/common.py#L94-L303). Add tests and code for as many as you want. 1. Make sure you have atleast one test for your extractor. Even if all videos covered by the extractor are expected to be inaccessible for automated testing, tests should still be added with a `skip` parameter indicating why the purticular test is disabled from running.
8. Make sure your code follows [youtube-dl coding conventions](#youtube-dl-coding-conventions) and check the code with [flake8](https://flake8.pycqa.org/en/latest/index.html#quickstart): 1. Have a look at [`yt_dlp/extractor/common.py`](yt_dlp/extractor/common.py) for possible helper methods and a [detailed description of what your extractor should and may return](yt_dlp/extractor/common.py#L91-L426). Add tests and code for as many as you want.
1. Make sure your code follows [yt-dlp coding conventions](#yt-dlp-coding-conventions) and check the code with [flake8](https://flake8.pycqa.org/en/latest/index.html#quickstart):
$ flake8 youtube_dl/extractor/yourextractor.py $ flake8 yt_dlp/extractor/yourextractor.py
9. Make sure your code works under all [Python](https://www.python.org/) versions claimed supported by youtube-dl, namely 2.6, 2.7, and 3.2+. 1. Make sure your code works under all [Python](https://www.python.org/) versions supported by yt-dlp, namely CPython and PyPy for Python 3.6 and above. Backward compatability is not required for even older versions of Python.
10. When the tests pass, [add](https://git-scm.com/docs/git-add) the new files and [commit](https://git-scm.com/docs/git-commit) them and [push](https://git-scm.com/docs/git-push) the result, like this: 1. When the tests pass, [add](https://git-scm.com/docs/git-add) the new files, [commit](https://git-scm.com/docs/git-commit) them and [push](https://git-scm.com/docs/git-push) the result, like this:
$ git add youtube_dl/extractor/extractors.py $ git add yt_dlp/extractor/extractors.py
$ git add youtube_dl/extractor/yourextractor.py $ git add yt_dlp/extractor/yourextractor.py
$ git commit -m '[yourextractor] Add new extractor' $ git commit -m '[yourextractor] Add extractor'
$ git push origin yourextractor $ git push origin yourextractor
11. Finally, [create a pull request](https://help.github.com/articles/creating-a-pull-request). We'll then review and merge it. 1. Finally, [create a pull request](https://help.github.com/articles/creating-a-pull-request). We'll then review and merge it.
In any case, thank you very much for your contributions! In any case, thank you very much for your contributions!
## youtube-dl coding conventions
## yt-dlp coding conventions
This section introduces a guide lines for writing idiomatic, robust and future-proof extractor code. This section introduces a guide lines for writing idiomatic, robust and future-proof extractor code.
Extractors are very fragile by nature since they depend on the layout of the source data provided by 3rd party media hosters out of your control and this layout tends to change. As an extractor implementer your task is not only to write code that will extract media links and metadata correctly but also to minimize dependency on the source's layout and even to make the code foresee potential future changes and be ready for that. This is important because it will allow the extractor not to break on minor layout changes thus keeping old youtube-dl versions working. Even though this breakage issue is easily fixed by emitting a new version of youtube-dl with a fix incorporated, all the previous versions become broken in all repositories and distros' packages that may not be so prompt in fetching the update from us. Needless to say, some non rolling release distros may never receive an update at all. Extractors are very fragile by nature since they depend on the layout of the source data provided by 3rd party media hosters out of your control and this layout tends to change. As an extractor implementer your task is not only to write code that will extract media links and metadata correctly but also to minimize dependency on the source's layout and even to make the code foresee potential future changes and be ready for that. This is important because it will allow the extractor not to break on minor layout changes thus keeping old yt-dlp versions working. Even though this breakage issue may be easily fixed by a new version of yt-dlp, this could take some time, during which the the extractor will remain broken.
### Mandatory and optional metafields ### Mandatory and optional metafields
For extraction to work youtube-dl relies on metadata your extractor extracts and provides to youtube-dl expressed by an [information dictionary](https://github.com/ytdl-org/youtube-dl/blob/7f41a598b3fba1bcab2817de64a08941200aa3c8/youtube_dl/extractor/common.py#L94-L303) or simply *info dict*. Only the following meta fields in the *info dict* are considered mandatory for a successful extraction process by youtube-dl: For extraction to work yt-dlp relies on metadata your extractor extracts and provides to yt-dlp expressed by an [information dictionary](yt_dlp/extractor/common.py#L91-L426) or simply *info dict*. Only the following meta fields in the *info dict* are considered mandatory for a successful extraction process by yt-dlp:
- `id` (media identifier) - `id` (media identifier)
- `title` (media title) - `title` (media title)
- `url` (media download URL) or `formats` - `url` (media download URL) or `formats`
In fact only the last option is technically mandatory (i.e. if you can't figure out the download location of the media the extraction does not make any sense). But by convention youtube-dl also treats `id` and `title` as mandatory. Thus the aforementioned metafields are the critical data that the extraction does not make any sense without and if any of them fail to be extracted then the extractor is considered completely broken. The aforementioned metafields are the critical data that the extraction does not make any sense without and if any of them fail to be extracted then the extractor is considered completely broken. While, in fact, only `id` is technically mandatory, due to compatability reasons, yt-dlp also treats `title` as mandatory. The extractor is allowed to return the info dict without url or formats in some special cases if it allows the user to extract usefull information with `--ignore-no-formats-error` - Eg: when the video is a live stream that has not started yet.
[Any field](https://github.com/ytdl-org/youtube-dl/blob/7f41a598b3fba1bcab2817de64a08941200aa3c8/youtube_dl/extractor/common.py#L188-L303) apart from the aforementioned ones are considered **optional**. That means that extraction should be **tolerant** to situations when sources for these fields can potentially be unavailable (even if they are always available at the moment) and **future-proof** in order not to break the extraction of general purpose mandatory fields. [Any field](yt_dlp/extractor/common.py#219-L426) apart from the aforementioned ones are considered **optional**. That means that extraction should be **tolerant** to situations when sources for these fields can potentially be unavailable (even if they are always available at the moment) and **future-proof** in order not to break the extraction of general purpose mandatory fields.
#### Example #### Example
@@ -200,8 +259,10 @@ Assume at this point `meta`'s layout is:
```python ```python
{ {
...
"summary": "some fancy summary text", "summary": "some fancy summary text",
"user": {
"name": "uploader name"
},
... ...
} }
``` ```
@@ -220,6 +281,30 @@ description = meta['summary'] # incorrect
The latter will break extraction process with `KeyError` if `summary` disappears from `meta` at some later time but with the former approach extraction will just go ahead with `description` set to `None` which is perfectly fine (remember `None` is equivalent to the absence of data). The latter will break extraction process with `KeyError` if `summary` disappears from `meta` at some later time but with the former approach extraction will just go ahead with `description` set to `None` which is perfectly fine (remember `None` is equivalent to the absence of data).
If the data is nested, do not use `.get` chains, but instead make use of the utility functions `try_get` or `traverse_obj`
Considering the above `meta` again, assume you want to extract `["user"]["name"]` and put it in the resulting info dict as `uploader`
```python
uploader = try_get(meta, lambda x: x['user']['name']) # correct
```
or
```python
uploader = traverse_obj(meta, ('user', 'name')) # correct
```
and not like:
```python
uploader = meta['user']['name'] # incorrect
```
or
```python
uploader = meta.get('user', {}).get('name') # incorrect
```
Similarly, you should pass `fatal=False` when extracting optional data from a webpage with `_search_regex`, `_html_search_regex` or similar methods, for instance: Similarly, you should pass `fatal=False` when extracting optional data from a webpage with `_search_regex`, `_html_search_regex` or similar methods, for instance:
```python ```python
@@ -240,10 +325,35 @@ description = self._search_regex(
On failure this code will silently continue the extraction with `description` set to `None`. That is useful for metafields that may or may not be present. On failure this code will silently continue the extraction with `description` set to `None`. That is useful for metafields that may or may not be present.
Another thing to remember is not to try to iterate over `None`
Say you extracted a list of thumbnails into `thumbnail_data` using `try_get` and now want to iterate over them
```python
thumbnail_data = try_get(...)
thumbnails = [{
'url': item['url']
} for item in thumbnail_data or []] # correct
```
and not like:
```python
thumbnail_data = try_get(...)
thumbnails = [{
'url': item['url']
} for item in thumbnail_data] # incorrect
```
In the later case, `thumbnail_data` will be `None` if the field was not found and this will cause the loop `for item in thumbnail_data` to raise a fatal error. Using `for item in thumbnail_data or []` avoids this error and results in setting an empty list in `thumbnails` instead.
### Provide fallbacks ### Provide fallbacks
When extracting metadata try to do so from multiple sources. For example if `title` is present in several places, try extracting from at least some of them. This makes it more future-proof in case some of the sources become unavailable. When extracting metadata try to do so from multiple sources. For example if `title` is present in several places, try extracting from at least some of them. This makes it more future-proof in case some of the sources become unavailable.
#### Example #### Example
Say `meta` from the previous example has a `title` and you are about to extract it. Since `title` is a mandatory meta field you should end up with something like: Say `meta` from the previous example has a `title` and you are about to extract it. Since `title` is a mandatory meta field you should end up with something like:
@@ -262,6 +372,7 @@ title = meta.get('title') or self._og_search_title(webpage)
This code will try to extract from `meta` first and if it fails it will try extracting `og:title` from a `webpage`. This code will try to extract from `meta` first and if it fails it will try extracting `og:title` from a `webpage`.
### Regular expressions ### Regular expressions
#### Don't capture groups you don't use #### Don't capture groups you don't use
@@ -283,7 +394,6 @@ Incorrect:
r'(id|ID)=(?P<id>\d+)' r'(id|ID)=(?P<id>\d+)'
``` ```
#### Make regular expressions relaxed and flexible #### Make regular expressions relaxed and flexible
When using regular expressions try to write them fuzzy, relaxed and flexible, skipping insignificant parts that are more likely to change, allowing both single and double quotes for quoted values and so on. When using regular expressions try to write them fuzzy, relaxed and flexible, skipping insignificant parts that are more likely to change, allowing both single and double quotes for quoted values and so on.
@@ -299,14 +409,14 @@ Say you need to extract `title` from the following HTML code:
The code for that task should look similar to: The code for that task should look similar to:
```python ```python
title = self._search_regex( title = self._search_regex( # correct
r'<span[^>]+class="title"[^>]*>([^<]+)', webpage, 'title') r'<span[^>]+class="title"[^>]*>([^<]+)', webpage, 'title')
``` ```
Or even better: Or even better:
```python ```python
title = self._search_regex( title = self._search_regex( # correct
r'<span[^>]+class=(["\'])title\1[^>]*>(?P<title>[^<]+)', r'<span[^>]+class=(["\'])title\1[^>]*>(?P<title>[^<]+)',
webpage, 'title', group='title') webpage, 'title', group='title')
``` ```
@@ -316,14 +426,25 @@ Note how you tolerate potential changes in the `style` attribute's value or swit
The code definitely should not look like: The code definitely should not look like:
```python ```python
title = self._search_regex( title = self._search_regex( # incorrect
r'<span style="position: absolute; left: 910px; width: 90px; float: right; z-index: 9999;" class="title">(.*?)</span>', r'<span style="position: absolute; left: 910px; width: 90px; float: right; z-index: 9999;" class="title">(.*?)</span>',
webpage, 'title', group='title') webpage, 'title', group='title')
``` ```
or even
```python
title = self._search_regex( # incorrect
r'<span style=".*?" class="title">(.*?)</span>',
webpage, 'title', group='title')
```
Here the presence or absence of other attributes including `style` is irrelevent for the data we need, and so the regex must not depend on it
### Long lines policy ### Long lines policy
There is a soft limit to keep lines of code under 80 characters long. This means it should be respected if possible and if it does not make readability and code maintenance worse. There is a soft limit to keep lines of code under 100 characters long. This means it should be respected if possible and if it does not make readability and code maintenance worse. Sometimes, it may be reasonable to go upto 120 characters and sometimes even 80 can be unreadable. Keep in mind that this is not a hard limit and is just one of many tools to make the code more readable
For example, you should **never** split long string literals like URLs or some other often copied entities over multiple lines to fit this limit: For example, you should **never** split long string literals like URLs or some other often copied entities over multiple lines to fit this limit:
@@ -360,6 +481,7 @@ TITLE_RE = r'<title>([^<]+)</title>'
title = self._html_search_regex(TITLE_RE, webpage, 'title') title = self._html_search_regex(TITLE_RE, webpage, 'title')
``` ```
### Collapse fallbacks ### Collapse fallbacks
Multiple fallback values can quickly become unwieldy. Collapse multiple fallback values into a single expression via a list of patterns. Multiple fallback values can quickly become unwieldy. Collapse multiple fallback values into a single expression via a list of patterns.
@@ -385,10 +507,13 @@ description = (
Methods supporting list of patterns are: `_search_regex`, `_html_search_regex`, `_og_search_property`, `_html_search_meta`. Methods supporting list of patterns are: `_search_regex`, `_html_search_regex`, `_og_search_property`, `_html_search_meta`.
### Trailing parentheses ### Trailing parentheses
Always move trailing parentheses after the last argument. Always move trailing parentheses after the last argument.
Note that this *does not* apply to braces `}` or square brackets `]` both of which should closed be in a new line
#### Example #### Example
Correct: Correct:
@@ -406,30 +531,36 @@ Incorrect:
) )
``` ```
### Use convenience conversion and parsing functions ### Use convenience conversion and parsing functions
Wrap all extracted numeric data into safe functions from [`youtube_dl/utils.py`](https://github.com/ytdl-org/youtube-dl/blob/master/youtube_dl/utils.py): `int_or_none`, `float_or_none`. Use them for string to number conversions as well. Wrap all extracted numeric data into safe functions from [`yt_dlp/utils.py`](yt_dlp/utils.py): `int_or_none`, `float_or_none`. Use them for string to number conversions as well.
Use `url_or_none` for safe URL processing. Use `url_or_none` for safe URL processing.
Use `try_get` for safe metadata extraction from parsed JSON. Use `try_get`, `dict_get` and `traverse_obj` for safe metadata extraction from parsed JSON.
Use `unified_strdate` for uniform `upload_date` or any `YYYYMMDD` meta field extraction, `unified_timestamp` for uniform `timestamp` extraction, `parse_filesize` for `filesize` extraction, `parse_count` for count meta fields extraction, `parse_resolution`, `parse_duration` for `duration` extraction, `parse_age_limit` for `age_limit` extraction. Use `unified_strdate` for uniform `upload_date` or any `YYYYMMDD` meta field extraction, `unified_timestamp` for uniform `timestamp` extraction, `parse_filesize` for `filesize` extraction, `parse_count` for count meta fields extraction, `parse_resolution`, `parse_duration` for `duration` extraction, `parse_age_limit` for `age_limit` extraction.
Explore [`youtube_dl/utils.py`](https://github.com/ytdl-org/youtube-dl/blob/master/youtube_dl/utils.py) for more useful convenience functions. Explore [`yt_dlp/utils.py`](yt_dlp/utils.py) for more useful convenience functions.
#### More examples #### More examples
##### Safely extract optional description from parsed JSON ##### Safely extract optional description from parsed JSON
```python ```python
description = try_get(response, lambda x: x['result']['video'][0]['summary'], compat_str) description = traverse_obj(response, ('result', 'video', 'summary'), expected_type=str)
``` ```
##### Safely extract more optional metadata ##### Safely extract more optional metadata
```python ```python
video = try_get(response, lambda x: x['result']['video'][0], dict) or {} video = traverse_obj(response, ('result', 'video', 0), default={}, expected_type=dict)
description = video.get('summary') description = video.get('summary')
duration = float_or_none(video.get('durationMs'), scale=1000) duration = float_or_none(video.get('durationMs'), scale=1000)
view_count = int_or_none(video.get('views')) view_count = int_or_none(video.get('views'))
``` ```
# EMBEDDING YT-DLP
See [README.md#embedding-yt-dlp](README.md#embedding-yt-dlp) for instructions on how to embed yt-dlp in another Python program

View File

@@ -100,3 +100,32 @@ korli
octotherp octotherp
CeruleanSky CeruleanSky
zootedb0t zootedb0t
chao813
ChillingPepper
ConquerorDopy
dalanmiller
DigitalDJ
f4pp3rk1ng
gesa
Jules-A
makeworld-the-better-one
MKSherbini
mrx23dot
poschi3
raphaeldore
renalid
sleaux-meaux
sulyi
tmarki
Vangelis66
AjaxGb
ajj8
jakubadamw
jfogelman
timethrow
sarnoud
Bojidarist
18928172992817182/gustaf
nixklai
smplayer-dev
Zirro

View File

@@ -7,18 +7,278 @@
* Update Changelog.md and CONTRIBUTORS * Update Changelog.md and CONTRIBUTORS
* Change "Merged with ytdl" version in Readme.md if needed * Change "Merged with ytdl" version in Readme.md if needed
* Add new/fixed extractors in "new features" section of Readme.md * Add new/fixed extractors in "new features" section of Readme.md
* Commit to master as `Release <version>` * Commit as `Release <version>`
* Push to origin/release using `git push origin master:release` * Push to origin/release using `git push origin master:release`
build task will now run build task will now run
* Update version.py using `devscripts\update-version.py`
* Run `make issuetemplates`
* Commit to master as `[version] update :ci skip all`
* Push to origin/master
* Update changelog in /releases
--> -->
### 2021.10.22
* [build] Improvements
* Build standalone MacOS packages by [smplayer-dev](https://github.com/smplayer-dev)
* Release windows exe built with `py2exe`
* Enable lazy-extractors in releases.
* Set env var `YTDLP_NO_LAZY_EXTRACTORS` to forcefully disable this (experimental)
* Clean up error reporting in update
* Refactor `pyinst.py`, misc cleanup and improve docs
* [docs] Migrate issues to use forms by [Ashish0804](https://github.com/Ashish0804)
* [downloader] **Fix slow progress hooks**
* This was causing HLS/DASH downloads to be extremely slow in some situations
* [downloader/ffmpeg] Improve simultaneous download and merge
* [EmbedMetadata] Allow overwriting all default metadata with `meta_default` key
* [ModifyChapters] Add ability for `--remove-chapters` to remove sections by timestamp
* [utils] Allow duration strings in `--match-filter`
* Add HDR information to formats
* Add negative option `--no-batch-file` by [Zirro](https://github.com/Zirro)
* Calculate more fields for merged formats
* Do not verify thumbnail URLs unless `--check-formats` is specified
* Don't create console for subprocesses on Windows
* Fix `--restrict-filename` when used with default template
* Fix `check_formats` output being written to stdout when `-qv`
* Fix bug in storyboards
* Fix conflict b/w id and ext in format selection
* Fix verbose head not showing custom configs
* Load archive only after printing verbose head
* Make `duration_string` and `resolution` available in --match-filter
* Re-implement deprecated option `--id`
* Reduce default `--socket-timeout`
* Write verbose header to logger
* [outtmpl] Fix bug in expanding environment variables
* [cookies] Local State should be opened as utf-8
* [extractor,utils] Detect more codecs/mimetypes
* [extractor] Detect `EXT-X-KEY` Apple FairPlay
* [utils] Use `importlib` to load plugins by [sulyi](https://github.com/sulyi)
* [http] Retry on socket timeout and show the last encountered error
* [fragment] Print error message when skipping fragment
* [aria2c] Fix `--skip-unavailable-fragment`
* [SponsorBlock] Obey `extractor-retries` and `sleep-requests`
* [Merger] Do not add `aac_adtstoasc` to non-hls audio
* [ModifyChapters] Do not mutate original chapters by [nihil-admirari](https://github.com/nihil-admirari)
* [devscripts/run_tests] Use markers to filter tests by [sulyi](https://github.com/sulyi)
* [7plus] Add cookie based authentication by [nyuszika7h](https://github.com/nyuszika7h)
* [AdobePass] Fix RCN MSO by [jfogelman](https://github.com/jfogelman)
* [CBC] Fix Gem livestream by [makeworld-the-better-one](https://github.com/makeworld-the-better-one)
* [CBC] Support CBC Gem member content by [makeworld-the-better-one](https://github.com/makeworld-the-better-one)
* [crunchyroll] Add season to flat-playlist Closes #1319
* [crunchyroll] Add support for `beta.crunchyroll` URLs and fix series URLs with language code
* [EUScreen] Add Extractor by [Ashish0804](https://github.com/Ashish0804)
* [Gronkh] Add extractor by [Ashish0804](https://github.com/Ashish0804)
* [hidive] Fix typo
* [Hotstar] Mention Dynamic Range in `format_id` by [Ashish0804](https://github.com/Ashish0804)
* [Hotstar] Raise appropriate error for DRM
* [instagram] Add login by [u-spec-png](https://github.com/u-spec-png)
* [instagram] Show appropriate error when login is needed
* [microsoftstream] Add extractor by [damianoamatruda](https://github.com/damianoamatruda), [nixklai](https://github.com/nixklai)
* [on24] Add extractor by [damianoamatruda](https://github.com/damianoamatruda)
* [patreon] Fix vimeo player regex by [zenerdi0de](https://github.com/zenerdi0de)
* [SkyNewsAU] Add extractor by [Ashish0804](https://github.com/Ashish0804)
* [tagesschau] Fix extractor by [u-spec-png](https://github.com/u-spec-png)
* [tbs] Add tbs live streams by [llacb47](https://github.com/llacb47)
* [tiktok] Fix typo and update tests
* [trovo] Support channel clips and VODs by [Ashish0804](https://github.com/Ashish0804)
* [Viafree] Add support for Finland by [18928172992817182](https://github.com/18928172992817182)
* [vimeo] Fix embedded `player.vimeo`
* [vlive:channel] Fix extraction by [kikuyan](https://github.com/kikuyan), [pukkandan](https://github.com/pukkandan)
* [youtube] Add auto-translated subtitles
* [youtube] Expose different formats with same itag
* [youtube:comments] Fix for new layout by [coletdjnz](https://github.com/coletdjnz)
* [cleanup] Cleanup bilibili code by [pukkandan](https://github.com/pukkandan), [u-spec-png](https://github.com/u-spec-png)
* [cleanup] Remove broken youtube login code
* [cleanup] Standardize timestamp formatting code
* [cleanup] Generalize `getcomments` implementation for extractors
* [cleanup] Simplify search extractors code
* [cleanup] misc
### 2021.10.10
* [downloader/ffmpeg] Fix bug in initializing `FFmpegPostProcessor`
* [minicurses] Fix when printing to file
* [downloader] Fix throttledratelimit
* [francetv] Fix extractor by [fstirlitz](https://github.com/fstirlitz), [sarnoud](https://github.com/sarnoud)
* [NovaPlay] Add extractor by [Bojidarist](https://github.com/Bojidarist)
* [ffmpeg] Revert "Set max probesize" - No longer needed
* [docs] Remove incorrect dependency on VC++10
* [build] Allow to release without changelog
### 2021.10.09
* Improved progress reporting
* Separate `--console-title` and `--no-progress`
* Add option `--progress` to show progress-bar even in quiet mode
* Fix and refactor `minicurses` and use it for all progress reporting
* Standardize use of terminal sequences and enable color support for windows 10
* Add option `--progress-template` to customize progress-bar and console-title
* Add postprocessor hooks and progress reporting
* [postprocessor] Add plugin support with option `--use-postprocessor`
* [extractor] Extract storyboards from SMIL manifests by [fstirlitz](https://github.com/fstirlitz)
* [outtmpl] Alternate form of format type `l` for `\n` delimited list
* [outtmpl] Format type `U` for unicode normalization
* [outtmpl] Allow empty output template to skip a type of file
* Merge webm formats into mkv if thumbnails are to be embedded
* [adobepass] Add RCN as MSO by [jfogelman](https://github.com/jfogelman)
* [ciscowebex] Add extractor by [damianoamatruda](https://github.com/damianoamatruda)
* [Gettr] Add extractor by [i6t](https://github.com/i6t)
* [GoPro] Add extractor by [i6t](https://github.com/i6t)
* [N1] Add extractor by [u-spec-png](https://github.com/u-spec-png)
* [Theta] Add video extractor by [alerikaisattera](https://github.com/alerikaisattera)
* [Veo] Add extractor by [i6t](https://github.com/i6t)
* [Vupload] Add extractor by [u-spec-png](https://github.com/u-spec-png)
* [bbc] Extract better quality videos by [ajj8](https://github.com/ajj8)
* [Bilibili] Add subtitle converter by [u-spec-png](https://github.com/u-spec-png)
* [CBC] Cleanup tests by [makeworld-the-better-one](https://github.com/makeworld-the-better-one)
* [Douyin] Rewrite extractor by [MinePlayersPE](https://github.com/MinePlayersPE)
* [Funimation] Fix for /v/ urls by [pukkandan](https://github.com/pukkandan), [Jules-A](https://github.com/Jules-A)
* [Funimation] Sort formats according to the relevant extractor-args
* [Hidive] Fix duplicate and incorrect formats
* [HotStarSeries] Fix cookies by [Ashish0804](https://github.com/Ashish0804)
* [LinkedInLearning] Add subtitles by [Ashish0804](https://github.com/Ashish0804)
* [Mediaite] Relax valid url by [coletdjnz](https://github.com/coletdjnz)
* [Newgrounds] Add age_limit and fix duration by [u-spec-png](https://github.com/u-spec-png)
* [Newgrounds] Fix view count on songs by [u-spec-png](https://github.com/u-spec-png)
* [parliamentlive.tv] Fix extractor by [u-spec-png](https://github.com/u-spec-png)
* [PolskieRadio] Fix extractors by [jakubadamw](https://github.com/jakubadamw), [u-spec-png](https://github.com/u-spec-png)
* [reddit] Add embedded url by [u-spec-png](https://github.com/u-spec-png)
* [reddit] Fix 429 by generating a random `reddit_session` by [AjaxGb](https://github.com/AjaxGb)
* [Rumble] Add RumbleChannelIE by [Ashish0804](https://github.com/Ashish0804)
* [soundcloud:playlist] Detect last page correctly
* [SovietsCloset] Add duration from m3u8 by [ChillingPepper](https://github.com/ChillingPepper)
* [Streamable] Add codecs by [u-spec-png](https://github.com/u-spec-png)
* [vidme] Remove extractor by [alerikaisattera](https://github.com/alerikaisattera)
* [youtube:tab] Fallback to API when webpage fails to download by [coletdjnz](https://github.com/coletdjnz)
* [youtube] Fix non-fatal errors in fetching player
* Fix `--flat-playlist` when neither IE nor id is known
* Fix `-f mp4` behaving differently from youtube-dl
* Workaround for bug in `ssl.SSLContext.load_default_certs`
* [aes] Improve performance slightly by [sulyi](https://github.com/sulyi)
* [cookies] Fix keyring fallback by [mbway](https://github.com/mbway)
* [embedsubtitle] Fix error when duration is unknown
* [ffmpeg] Fix error when subtitle file is missing
* [ffmpeg] Set max probesize to workaround AAC HLS stream issues by [shirt](https://github.com/shirt-dev)
* [FixupM3u8] Remove redundant run if merged is needed
* [hls] Fix decryption issues by [shirt](https://github.com/shirt-dev), [pukkandan](https://github.com/pukkandan)
* [http] Respect user-provided chunk size over extractor's
* [utils] Let traverse_obj accept functions as keys
* [docs] Add note about our custom ffmpeg builds
* [docs] Write embedding and contributing documentation by [pukkandan](https://github.com/pukkandan), [timethrow](https://github.com/timethrow)
* [update] Check for new version even if not updateable
* [build] Add more files to the tarball
* [build] Allow building with py2exe (and misc fixes)
* [build] Use pycryptodomex by [shirt](https://github.com/shirt-dev), [pukkandan](https://github.com/pukkandan)
* [cleanup] Some minor refactoring, improve docs and misc cleanup
### 2021.09.25
* Add new option `--netrc-location`
* [outtmpl] Allow alternate fields using `,`
* [outtmpl] Add format type `B` to treat the value as bytes (eg: to limit the filename to a certain number of bytes)
* Separate the options `--ignore-errors` and `--no-abort-on-error`
* Basic framework for simultaneous download of multiple formats by [nao20010128nao](https://github.com/nao20010128nao)
* [17live] Add 17.live extractor by [nao20010128nao](https://github.com/nao20010128nao)
* [bilibili] Add BiliIntlIE and BiliIntlSeriesIE by [Ashish0804](https://github.com/Ashish0804)
* [CAM4] Add extractor by [alerikaisattera](https://github.com/alerikaisattera)
* [Chingari] Add extractors by [Ashish0804](https://github.com/Ashish0804)
* [CGTN] Add extractor by [chao813](https://github.com/chao813)
* [damtomo] Add extractor by [nao20010128nao](https://github.com/nao20010128nao)
* [gotostage] Add extractor by [poschi3](https://github.com/poschi3)
* [Koo] Add extractor by [Ashish0804](https://github.com/Ashish0804)
* [Mediaite] Add Extractor by [Ashish0804](https://github.com/Ashish0804)
* [Mediaklikk] Add Extractor by [tmarki](https://github.com/tmarki), [mrx23dot](https://github.com/mrx23dot), [coletdjnz](https://github.com/coletdjnz)
* [MuseScore] Add Extractor by [Ashish0804](https://github.com/Ashish0804)
* [Newgrounds] Add NewgroundsUserIE and improve extractor by [u-spec-png](https://github.com/u-spec-png)
* [nzherald] Add NZHeraldIE by [coletdjnz](https://github.com/coletdjnz)
* [Olympics] Add replay extractor by [Ashish0804](https://github.com/Ashish0804)
* [Peertube] Add channel and playlist extractors by [u-spec-png](https://github.com/u-spec-png)
* [radlive] Add extractor by [nyuszika7h](https://github.com/nyuszika7h)
* [SovietsCloset] Add extractor by [ChillingPepper](https://github.com/ChillingPepper)
* [Streamanity] Add Extractor by [alerikaisattera](https://github.com/alerikaisattera)
* [Theta] Add extractor by [alerikaisattera](https://github.com/alerikaisattera)
* [Yandex] Add ZenYandexIE and ZenYandexChannelIE by [Ashish0804](https://github.com/Ashish0804)
* [9Now] handle episodes of series by [dalanmiller](https://github.com/dalanmiller)
* [AnimalPlanet] Fix extractor by [Sipherdrakon](https://github.com/Sipherdrakon)
* [Arte] Improve description extraction by [renalid](https://github.com/renalid)
* [atv.at] Use jwt for API by [NeroBurner](https://github.com/NeroBurner)
* [brightcove] Extract subtitles from manifests
* [CBC] Fix CBC Gem extractors by [makeworld-the-better-one](https://github.com/makeworld-the-better-one)
* [cbs] Report appropriate error for DRM
* [comedycentral] Support `collection-playlist` by [nixxo](https://github.com/nixxo)
* [DIYNetwork] Support new format by [Sipherdrakon](https://github.com/Sipherdrakon)
* [downloader/niconico] Pass custom headers by [nao20010128nao](https://github.com/nao20010128nao)
* [dw] Fix extractor
* [Fancode] Fix live streams by [zenerdi0de](https://github.com/zenerdi0de)
* [funimation] Fix for locations outside US by [Jules-A](https://github.com/Jules-A), [pukkandan](https://github.com/pukkandan)
* [globo] Fix GloboIE by [Ashish0804](https://github.com/Ashish0804)
* [HiDive] Fix extractor by [Ashish0804](https://github.com/Ashish0804)
* [Hotstar] Add referer for subs by [Ashish0804](https://github.com/Ashish0804)
* [itv] Fix extractor, add subtitles and thumbnails by [coletdjnz](https://github.com/coletdjnz), [sleaux-meaux](https://github.com/sleaux-meaux), [Vangelis66](https://github.com/Vangelis66)
* [lbry] Show error message from API response
* [Mxplayer] Use mobile API by [Ashish0804](https://github.com/Ashish0804)
* [NDR] Rewrite NDRIE by [Ashish0804](https://github.com/Ashish0804)
* [Nuvid] Fix extractor by [u-spec-png](https://github.com/u-spec-png)
* [Oreilly] Handle new web url by [MKSherbini](https://github.com/MKSherbini)
* [pbs] Fix subtitle extraction by [coletdjnz](https://github.com/coletdjnz), [gesa](https://github.com/gesa), [raphaeldore](https://github.com/raphaeldore)
* [peertube] Update instances by [u-spec-png](https://github.com/u-spec-png)
* [plutotv] Fix extractor for URLs with `/en`
* [reddit] Workaround for 429 by redirecting to old.reddit.com
* [redtube] Fix exts
* [soundcloud] Make playlist extraction lazy
* [soundcloud] Retry playlist pages on `502` error and update `_CLIENT_ID`
* [southpark] Fix SouthParkDE by [coletdjnz](https://github.com/coletdjnz)
* [SovietsCloset] Fix playlists for games with only named categories by [ConquerorDopy](https://github.com/ConquerorDopy)
* [SpankBang] Fix uploader by [f4pp3rk1ng](https://github.com/f4pp3rk1ng), [coletdjnz](https://github.com/coletdjnz)
* [tiktok] Use API to fetch higher quality video by [MinePlayersPE](https://github.com/MinePlayersPE), [llacb47](https://github.com/llacb47)
* [TikTokUser] Fix extractor using mobile API by [MinePlayersPE](https://github.com/MinePlayersPE), [llacb47](https://github.com/llacb47)
* [videa] Fix some extraction errors by [nyuszika7h](https://github.com/nyuszika7h)
* [VrtNU] Handle login errors by [llacb47](https://github.com/llacb47)
* [vrv] Don't raise error when thumbnails are missing
* [youtube] Cleanup authentication code by [coletdjnz](https://github.com/coletdjnz)
* [youtube] Fix `--mark-watched` with `--cookies-from-browser`
* [youtube] Improvements to JS player extraction and add extractor-args to skip it by [coletdjnz](https://github.com/coletdjnz)
* [youtube] Retry on 'Unknown Error' by [coletdjnz](https://github.com/coletdjnz)
* [youtube] Return full URL instead of just ID
* [youtube] Warn when trying to download clips
* [zdf] Improve format sorting
* [zype] Extract subtitles from the m3u8 manifest by [fstirlitz](https://github.com/fstirlitz)
* Allow `--force-write-archive` to work with `--flat-playlist`
* Download subtitles in order of `--sub-langs`
* Allow `0` in `--playlist-items`
* Handle more playlist errors with `-i`
* Fix `--no-get-comments`
* Fix `extra_info` being reused across runs
* Fix compat options `no-direct-merge` and `playlist-index`
* Dump files should obey `--trim-filename` by [sulyi](https://github.com/sulyi)
* [aes] Add `aes_gcm_decrypt_and_verify` by [sulyi](https://github.com/sulyi), [pukkandan](https://github.com/pukkandan)
* [aria2c] Fix IV for some AES-128 streams by [shirt](https://github.com/shirt-dev)
* [compat] Don't ignore `HOME` (if set) on windows
* [cookies] Make browser names case insensitive
* [cookies] Print warning for cookie decoding error only once
* [extractor] Fix root-relative URLs in MPD by [DigitalDJ](https://github.com/DigitalDJ)
* [ffmpeg] Add `aac_adtstoasc` when merging if needed
* [fragment,aria2c] Generalize and refactor some code
* [fragment] Avoid repeated request for AES key
* [fragment] Fix range header when using `-N` and media sequence by [shirt](https://github.com/shirt-dev)
* [hls,aes] Fallback to native implementation for AES-CBC and detect `Cryptodome` in addition to `Crypto`
* [hls] Byterange + AES128 is supported by native downloader
* [ModifyChapters] Improve sponsor chapter merge algorithm by [nihil-admirari](https://github.com/nihil-admirari)
* [ModifyChapters] Minor fixes
* [WebVTT] Adjust parser to accommodate PBS subtitles
* [utils] Improve `extract_timezone` by [dirkf](https://github.com/dirkf)
* [options] Fix `--no-config` and refactor reading of config files
* [options] Strip spaces and ignore empty entries in list-like switches
* [test/cookies] Improve logging
* [build] Automate more of the release process by [animelover1984](https://github.com/animelover1984), [pukkandan](https://github.com/pukkandan)
* [build] Fix sha256 by [nihil-admirari](https://github.com/nihil-admirari)
* [build] Bring back brew taps by [nao20010128nao](https://github.com/nao20010128nao)
* [build] Provide `--onedir` zip for windows by [pukkandan](https://github.com/pukkandan)
* [cleanup,docs] Add deprecation warning in docs for some counter intuitive behaviour
* [cleanup] Fix line endings for `nebula.py` by [glenn-slayden](https://github.com/glenn-slayden)
* [cleanup] Improve `make clean-test` by [sulyi](https://github.com/sulyi)
* [cleanup] Misc
### 2021.09.02 ### 2021.09.02
* **Native SponsorBlock** implementation by [nihil-admirari](https://github.com/nihil-admirari), [pukkandan](https://github.com/pukkandan) * **Native SponsorBlock** implementation by [nihil-admirari](https://github.com/nihil-admirari), [pukkandan](https://github.com/pukkandan)
@@ -30,7 +290,7 @@
* The fetched sponsor sections are written to the infojson * The fetched sponsor sections are written to the infojson
* Deprecates: `--sponskrub`, `--no-sponskrub`, `--sponskrub-cut`, `--no-sponskrub-cut`, `--sponskrub-force`, `--no-sponskrub-force`, `--sponskrub-location`, `--sponskrub-args` * Deprecates: `--sponskrub`, `--no-sponskrub`, `--sponskrub-cut`, `--no-sponskrub-cut`, `--sponskrub-force`, `--no-sponskrub-force`, `--sponskrub-location`, `--sponskrub-args`
* Split `--embed-chapters` from `--embed-metadata` (it still implies the former by default) * Split `--embed-chapters` from `--embed-metadata` (it still implies the former by default)
* Add option `--remove-chapters` to remove arbitrary chapters by [nihil-admirari](https://github.com/nihil-admirari), pukkandan * Add option `--remove-chapters` to remove arbitrary chapters by [nihil-admirari](https://github.com/nihil-admirari), [pukkandan](https://github.com/pukkandan)
* Add option `--force-keyframes-at-cuts` for more accurate cuts when removing and splitting chapters by [nihil-admirari](https://github.com/nihil-admirari) * Add option `--force-keyframes-at-cuts` for more accurate cuts when removing and splitting chapters by [nihil-admirari](https://github.com/nihil-admirari)
* Let `--match-filter` reject entries early * Let `--match-filter` reject entries early
* Makes redundant: `--match-title`, `--reject-title`, `--min-views`, `--max-views` * Makes redundant: `--match-title`, `--reject-title`, `--min-views`, `--max-views`
@@ -42,7 +302,6 @@
* [downloader/ffmpeg] Experimental support for DASH manifests (including live) * [downloader/ffmpeg] Experimental support for DASH manifests (including live)
* Your ffmpeg must have [this patch](https://github.com/FFmpeg/FFmpeg/commit/3249c757aed678780e22e99a1a49f4672851bca9) applied for YouTube DASH to work * Your ffmpeg must have [this patch](https://github.com/FFmpeg/FFmpeg/commit/3249c757aed678780e22e99a1a49f4672851bca9) applied for YouTube DASH to work
* [downloader/ffmpeg] Allow passing custom arguments before `-i` * [downloader/ffmpeg] Allow passing custom arguments before `-i`
* [BannedVideo] Add extractor by [smege1001](https://github.com/smege1001), [blackjack4494](https://github.com/blackjack4494), [pukkandan](https://github.com/pukkandan) * [BannedVideo] Add extractor by [smege1001](https://github.com/smege1001), [blackjack4494](https://github.com/blackjack4494), [pukkandan](https://github.com/pukkandan)
* [bilibili] Add category extractor by [animelover1984](https://github.com/animelover1984) * [bilibili] Add category extractor by [animelover1984](https://github.com/animelover1984)
* [Epicon] Add extractors by [Ashish0804](https://github.com/Ashish0804) * [Epicon] Add extractors by [Ashish0804](https://github.com/Ashish0804)
@@ -60,7 +319,6 @@
* [Tokentube] Add extractor by [u-spec-png](https://github.com/u-spec-png) * [Tokentube] Add extractor by [u-spec-png](https://github.com/u-spec-png)
* [TV2Hu] Fix `TV2HuIE` and add `TV2HuSeriesIE` by [Ashish0804](https://github.com/Ashish0804) * [TV2Hu] Fix `TV2HuIE` and add `TV2HuSeriesIE` by [Ashish0804](https://github.com/Ashish0804)
* [voicy] Add extractor by [nao20010128nao](https://github.com/nao20010128nao) * [voicy] Add extractor by [nao20010128nao](https://github.com/nao20010128nao)
* [adobepass] Fix Verizon SAML login by [nyuszika7h](https://github.com/nyuszika7h), [ParadoxGBB](https://github.com/ParadoxGBB) * [adobepass] Fix Verizon SAML login by [nyuszika7h](https://github.com/nyuszika7h), [ParadoxGBB](https://github.com/ParadoxGBB)
* [afreecatv] Fix adult VODs by [wlritchi](https://github.com/wlritchi) * [afreecatv] Fix adult VODs by [wlritchi](https://github.com/wlritchi)
* [afreecatv] Tolerate failure to parse date string by [wlritchi](https://github.com/wlritchi) * [afreecatv] Tolerate failure to parse date string by [wlritchi](https://github.com/wlritchi)
@@ -100,7 +358,6 @@
* [youtube] Prefer audio stream that YouTube considers default * [youtube] Prefer audio stream that YouTube considers default
* [youtube] Remove annotations and deprecate `--write-annotations` by [coletdjnz](https://github.com/coletdjnz) * [youtube] Remove annotations and deprecate `--write-annotations` by [coletdjnz](https://github.com/coletdjnz)
* [Zee5] Fix extractor and add subtitles by [Ashish0804](https://github.com/Ashish0804) * [Zee5] Fix extractor and add subtitles by [Ashish0804](https://github.com/Ashish0804)
* [aria2c] Obey `--rate-limit` * [aria2c] Obey `--rate-limit`
* [EmbedSubtitle] Continue even if some files are missing * [EmbedSubtitle] Continue even if some files are missing
* [extractor] Better error message for DRM * [extractor] Better error message for DRM
@@ -185,8 +442,8 @@
### 2021.08.02 ### 2021.08.02
* Add logo, banner and donate links * Add logo, banner and donate links
* Expand and escape environment variables correctly in output template * [outtmpl] Expand and escape environment variables
* Add format types `j` (json), `l` (comma delimited list), `q` (quoted for terminal) in output template * [outtmpl] Add format types `j` (json), `l` (comma delimited list), `q` (quoted for terminal)
* [downloader] Allow streaming some unmerged formats to stdout using ffmpeg * [downloader] Allow streaming some unmerged formats to stdout using ffmpeg
* [youtube] **Age-gate bypass** * [youtube] **Age-gate bypass**
* Add `agegate` clients by [pukkandan](https://github.com/pukkandan), [MinePlayersPE](https://github.com/MinePlayersPE) * Add `agegate` clients by [pukkandan](https://github.com/pukkandan), [MinePlayersPE](https://github.com/MinePlayersPE)
@@ -391,7 +648,7 @@
### 2021.06.09 ### 2021.06.09
* Fix bug where `%(field)d` in filename template throws error * Fix bug where `%(field)d` in filename template throws error
* Improve offset parsing in outtmpl * [outtmpl] Improve offset parsing
* [test] More rigorous tests for `prepare_filename` * [test] More rigorous tests for `prepare_filename`
### 2021.06.08 ### 2021.06.08

View File

@@ -1,4 +1,4 @@
all: yt-dlp doc pypi-files all: lazy-extractors yt-dlp doc pypi-files
clean: clean-test clean-dist clean-cache clean: clean-test clean-dist clean-cache
completions: completion-bash completion-fish completion-zsh completions: completion-bash completion-fish completion-zsh
doc: README.md CONTRIBUTING.md issuetemplates supportedsites doc: README.md CONTRIBUTING.md issuetemplates supportedsites
@@ -13,7 +13,9 @@ pypi-files: AUTHORS Changelog.md LICENSE README.md README.txt supportedsites com
.PHONY: all clean install test tar pypi-files completions ot offlinetest codetest supportedsites .PHONY: all clean install test tar pypi-files completions ot offlinetest codetest supportedsites
clean-test: clean-test:
rm -rf *.dump *.part* *.ytdl *.info.json *.mp4 *.m4a *.flv *.mp3 *.avi *.mkv *.webm *.3gp *.wav *.ape *.swf *.jpg *.png *.frag *.frag.urls *.frag.aria2 test/testdata/player-*.js *.opus *.webp *.ttml *.vtt *.jpeg rm -rf *.3gp *.annotations.xml *.ape *.avi *.description *.dump *.flac *.flv *.frag *.frag.aria2 *.frag.urls \
*.info.json *.jpeg *.jpg *.live_chat.json *.m4a *.m4v *.mkv *.mp3 *.mp4 *.ogg *.opus *.part* *.png *.sbv *.srt \
*.swf *.swp *.ttml *.vtt *.wav *.webm *.webp *.ytdl test/testdata/player-*.js
clean-dist: clean-dist:
rm -rf yt-dlp.1.temp.md yt-dlp.1 README.txt MANIFEST build/ dist/ .coverage cover/ yt-dlp.tar.gz completions/ yt_dlp/extractor/lazy_extractors.py *.spec CONTRIBUTING.md.tmp yt-dlp yt-dlp.exe yt_dlp.egg-info/ AUTHORS .mailmap rm -rf yt-dlp.1.temp.md yt-dlp.1 README.txt MANIFEST build/ dist/ .coverage cover/ yt-dlp.tar.gz completions/ yt_dlp/extractor/lazy_extractors.py *.spec CONTRIBUTING.md.tmp yt-dlp yt-dlp.exe yt_dlp.egg-info/ AUTHORS .mailmap
clean-cache: clean-cache:
@@ -38,9 +40,9 @@ SYSCONFDIR = $(shell if [ $(PREFIX) = /usr -o $(PREFIX) = /usr/local ]; then ech
# set markdown input format to "markdown-smart" for pandoc version 2 and to "markdown" for pandoc prior to version 2 # set markdown input format to "markdown-smart" for pandoc version 2 and to "markdown" for pandoc prior to version 2
MARKDOWN = $(shell if [ `pandoc -v | head -n1 | cut -d" " -f2 | head -c1` = "2" ]; then echo markdown-smart; else echo markdown; fi) MARKDOWN = $(shell if [ `pandoc -v | head -n1 | cut -d" " -f2 | head -c1` = "2" ]; then echo markdown-smart; else echo markdown; fi)
install: yt-dlp yt-dlp.1 completions install: lazy-extractors yt-dlp yt-dlp.1 completions
install -Dm755 yt-dlp $(DESTDIR)$(BINDIR) install -Dm755 yt-dlp $(DESTDIR)$(BINDIR)/yt-dlp
install -Dm644 yt-dlp.1 $(DESTDIR)$(MANDIR)/man1 install -Dm644 yt-dlp.1 $(DESTDIR)$(MANDIR)/man1/yt-dlp.1
install -Dm644 completions/bash/yt-dlp $(DESTDIR)$(SHAREDIR)/bash-completion/completions/yt-dlp install -Dm644 completions/bash/yt-dlp $(DESTDIR)$(SHAREDIR)/bash-completion/completions/yt-dlp
install -Dm644 completions/zsh/_yt-dlp $(DESTDIR)$(SHAREDIR)/zsh/site-functions/_yt-dlp install -Dm644 completions/zsh/_yt-dlp $(DESTDIR)$(SHAREDIR)/zsh/site-functions/_yt-dlp
install -Dm644 completions/fish/yt-dlp.fish $(DESTDIR)$(SHAREDIR)/fish/vendor_completions.d/yt-dlp.fish install -Dm644 completions/fish/yt-dlp.fish $(DESTDIR)$(SHAREDIR)/fish/vendor_completions.d/yt-dlp.fish
@@ -76,12 +78,13 @@ README.md: yt_dlp/*.py yt_dlp/*/*.py
CONTRIBUTING.md: README.md CONTRIBUTING.md: README.md
$(PYTHON) devscripts/make_contributing.py README.md CONTRIBUTING.md $(PYTHON) devscripts/make_contributing.py README.md CONTRIBUTING.md
issuetemplates: devscripts/make_issue_template.py .github/ISSUE_TEMPLATE_tmpl/1_broken_site.md .github/ISSUE_TEMPLATE_tmpl/2_site_support_request.md .github/ISSUE_TEMPLATE_tmpl/3_site_feature_request.md .github/ISSUE_TEMPLATE_tmpl/4_bug_report.md .github/ISSUE_TEMPLATE_tmpl/5_feature_request.md yt_dlp/version.py issuetemplates: devscripts/make_issue_template.py .github/ISSUE_TEMPLATE_tmpl/1_broken_site.yml .github/ISSUE_TEMPLATE_tmpl/2_site_support_request.yml .github/ISSUE_TEMPLATE_tmpl/3_site_feature_request.yml .github/ISSUE_TEMPLATE_tmpl/4_bug_report.yml .github/ISSUE_TEMPLATE_tmpl/5_feature_request.yml yt_dlp/version.py
$(PYTHON) devscripts/make_issue_template.py .github/ISSUE_TEMPLATE_tmpl/1_broken_site.md .github/ISSUE_TEMPLATE/1_broken_site.md $(PYTHON) devscripts/make_issue_template.py .github/ISSUE_TEMPLATE_tmpl/1_broken_site.yml .github/ISSUE_TEMPLATE/1_broken_site.yml
$(PYTHON) devscripts/make_issue_template.py .github/ISSUE_TEMPLATE_tmpl/2_site_support_request.md .github/ISSUE_TEMPLATE/2_site_support_request.md $(PYTHON) devscripts/make_issue_template.py .github/ISSUE_TEMPLATE_tmpl/2_site_support_request.yml .github/ISSUE_TEMPLATE/2_site_support_request.yml
$(PYTHON) devscripts/make_issue_template.py .github/ISSUE_TEMPLATE_tmpl/3_site_feature_request.md .github/ISSUE_TEMPLATE/3_site_feature_request.md $(PYTHON) devscripts/make_issue_template.py .github/ISSUE_TEMPLATE_tmpl/3_site_feature_request.yml .github/ISSUE_TEMPLATE/3_site_feature_request.yml
$(PYTHON) devscripts/make_issue_template.py .github/ISSUE_TEMPLATE_tmpl/4_bug_report.md .github/ISSUE_TEMPLATE/4_bug_report.md $(PYTHON) devscripts/make_issue_template.py .github/ISSUE_TEMPLATE_tmpl/4_bug_report.yml .github/ISSUE_TEMPLATE/4_bug_report.yml
$(PYTHON) devscripts/make_issue_template.py .github/ISSUE_TEMPLATE_tmpl/5_feature_request.md .github/ISSUE_TEMPLATE/5_feature_request.md $(PYTHON) devscripts/make_issue_template.py .github/ISSUE_TEMPLATE_tmpl/5_feature_request.yml .github/ISSUE_TEMPLATE/5_feature_request.yml
$(PYTHON) devscripts/make_issue_template.py .github/ISSUE_TEMPLATE_tmpl/6_question.yml .github/ISSUE_TEMPLATE/6_question.yml
supportedsites: supportedsites:
$(PYTHON) devscripts/make_supportedsites.py supportedsites.md $(PYTHON) devscripts/make_supportedsites.py supportedsites.md
@@ -110,7 +113,7 @@ _EXTRACTOR_FILES = $(shell find yt_dlp/extractor -iname '*.py' -and -not -iname
yt_dlp/extractor/lazy_extractors.py: devscripts/make_lazy_extractors.py devscripts/lazy_load_template.py $(_EXTRACTOR_FILES) yt_dlp/extractor/lazy_extractors.py: devscripts/make_lazy_extractors.py devscripts/lazy_load_template.py $(_EXTRACTOR_FILES)
$(PYTHON) devscripts/make_lazy_extractors.py $@ $(PYTHON) devscripts/make_lazy_extractors.py $@
yt-dlp.tar.gz: yt-dlp README.md supportedsites.md yt-dlp.1 completions Changelog.md AUTHORS yt-dlp.tar.gz: all
@tar -czf $(DESTDIR)/yt-dlp.tar.gz --transform "s|^|yt-dlp/|" --owner 0 --group 0 \ @tar -czf $(DESTDIR)/yt-dlp.tar.gz --transform "s|^|yt-dlp/|" --owner 0 --group 0 \
--exclude '*.DS_Store' \ --exclude '*.DS_Store' \
--exclude '*.kate-swp' \ --exclude '*.kate-swp' \
@@ -119,12 +122,12 @@ yt-dlp.tar.gz: yt-dlp README.md supportedsites.md yt-dlp.1 completions Changelog
--exclude '*~' \ --exclude '*~' \
--exclude '__pycache__' \ --exclude '__pycache__' \
--exclude '.git' \ --exclude '.git' \
--exclude 'docs/_build' \
-- \ -- \
devscripts test \ README.md supportedsites.md Changelog.md LICENSE \
Changelog.md AUTHORS LICENSE README.md supportedsites.md \ CONTRIBUTING.md Collaborators.md CONTRIBUTORS AUTHORS \
Makefile MANIFEST.in yt-dlp.1 completions \ Makefile MANIFEST.in yt-dlp.1 README.txt completions \
setup.py setup.cfg yt-dlp yt_dlp setup.py setup.cfg yt-dlp yt_dlp requirements.txt \
devscripts test tox.ini pytest.ini
AUTHORS: .mailmap AUTHORS: .mailmap
git shortlog -s -n | cut -f2 | sort > AUTHORS git shortlog -s -n | cut -f2 | sort > AUTHORS

281
README.md
View File

@@ -22,6 +22,7 @@ yt-dlp is a [youtube-dl](https://github.com/ytdl-org/youtube-dl) fork based on t
* [Differences in default behavior](#differences-in-default-behavior) * [Differences in default behavior](#differences-in-default-behavior)
* [INSTALLATION](#installation) * [INSTALLATION](#installation)
* [Update](#update) * [Update](#update)
* [Release Files](#release-files)
* [Dependencies](#dependencies) * [Dependencies](#dependencies)
* [Compile](#compile) * [Compile](#compile)
* [USAGE AND OPTIONS](#usage-and-options) * [USAGE AND OPTIONS](#usage-and-options)
@@ -54,7 +55,11 @@ yt-dlp is a [youtube-dl](https://github.com/ytdl-org/youtube-dl) fork based on t
* [Modifying metadata examples](#modifying-metadata-examples) * [Modifying metadata examples](#modifying-metadata-examples)
* [EXTRACTOR ARGUMENTS](#extractor-arguments) * [EXTRACTOR ARGUMENTS](#extractor-arguments)
* [PLUGINS](#plugins) * [PLUGINS](#plugins)
* [EMBEDDING YT-DLP](#embedding-yt-dlp)
* [DEPRECATED OPTIONS](#deprecated-options) * [DEPRECATED OPTIONS](#deprecated-options)
* [CONTRIBUTING](CONTRIBUTING.md#contributing-to-yt-dlp)
* [Opening an Issue](CONTRIBUTING.md#opening-an-issue)
* [Developer Instructions](CONTRIBUTING.md#developer-instructions)
* [MORE](#more) * [MORE](#more)
</div> </div>
@@ -77,7 +82,7 @@ The major new features from the latest release of [blackjack4494/yt-dlc](https:/
* Most (but not all) age-gated content can be downloaded without cookies * Most (but not all) age-gated content can be downloaded without cookies
* Partial workaround for throttling issue * Partial workaround for throttling issue
* Redirect channel's home URL automatically to `/video` to preserve the old behaviour * Redirect channel's home URL automatically to `/video` to preserve the old behaviour
* `255kbps` audio is extracted from youtube music if premium cookies are given * `255kbps` audio is extracted (if available) from youtube music when premium cookies are given
* Youtube music Albums, channels etc can be downloaded ([except self-uploaded music](https://github.com/yt-dlp/yt-dlp/issues/723)) * Youtube music Albums, channels etc can be downloaded ([except self-uploaded music](https://github.com/yt-dlp/yt-dlp/issues/723))
* **Cookies from browser**: Cookies can be automatically extracted from all major web browsers using `--cookies-from-browser BROWSER[:PROFILE]` * **Cookies from browser**: Cookies can be automatically extracted from all major web browsers using `--cookies-from-browser BROWSER[:PROFILE]`
@@ -88,9 +93,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, Spectrum MSO, SlingTV MSO, Cablevision 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, douyin, pornflip, ParamountPlusSeries, ScienceChannel, Utreon, OpenRec, BandcampMusic, blackboardcollaborate, eroprofile albums, mirrativ, BannedVideo, bilibili categories, Epicon, filmmodu, GabTV, HungamaAlbum, ManotoTV, Niconico search, Patreon User, peloton, ProjectVeritas, radiko, StarTV, tiktok user, Tokentube, voicy, TV2HuSeries * **New extractors**: AnimeLab, Philo MSO, Spectrum MSO, SlingTV MSO, Cablevision MSO, RCN 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, douyin, pornflip, ParamountPlusSeries, ScienceChannel, Utreon, OpenRec, BandcampMusic, blackboardcollaborate, eroprofile albums, mirrativ, BannedVideo, bilibili categories, Epicon, filmmodu, GabTV, HungamaAlbum, ManotoTV, Niconico search, Patreon User, peloton, ProjectVeritas, radiko, StarTV, tiktok user, Tokentube, voicy, TV2HuSeries, biliintl, 17live, NewgroundsUser, peertube channel/playlist, ZenYandex, CAM4, CGTN, damtomo, gotostage, Koo, Mediaite, Mediaklikk, MuseScore, nzherald, Olympics replay, radlive, SovietsCloset, Streamanity, Theta, Chingari, ciscowebex, Gettr, GoPro, N1, Theta, Veo, Vupload, NovaPlay, SkyNewsAU, EUScreen, Gronkh, microsoftstream, on24, trovo channels
* **Fixed/improved 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, BravoTV, crunchyroll playlist, RTP, viki, Hotstar, vidio, vimeo, mediaset, Mxplayer, nbcolympics, ParamountPlus, Newgrounds, SAML Verizon login, Hungama, afreecatv, aljazeera, ATV, bitchute, camtube, CDA, eroprofile, facebook, HearThisAtIE, iwara, kakao, Motherless, Nova, peertube, pornhub, reddit, tiktok, TV2, TV2Hu, tv5mondeplus, VH1, Viafree, XHamster * **Fixed/improved 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, BravoTV, crunchyroll, RTP, viki, Hotstar, vidio, vimeo, mediaset, Mxplayer, nbcolympics, ParamountPlus, Newgrounds, SAML Verizon login, Hungama, afreecatv, aljazeera, ATV, bitchute, camtube, CDA, eroprofile, facebook, HearThisAtIE, iwara, kakao, Motherless, Nova, peertube, pornhub, reddit, tiktok, TV2, TV2Hu, tv5mondeplus, VH1, Viafree, XHamster, 9Now, AnimalPlanet, Arte, CBC, Chingari, comedycentral, DIYNetwork, niconico, dw, funimation, globo, HiDive, NDR, Nuvid, Oreilly, pbs, plutotv, reddit, redtube, soundcloud, SpankBang, VrtNU, bbc, Bilibili, LinkedInLearning, parliamentlive, PolskieRadio, Streamable, vidme, francetv, 7plus, tagesschau
* **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
@@ -150,11 +155,10 @@ For ease of use, a few more compat options are available:
yt-dlp is not platform specific. So it should work on your Unix box, on Windows or on macOS yt-dlp is not platform specific. So it should work on your Unix box, on Windows or on macOS
You can install yt-dlp using one of the following methods: You can install yt-dlp using one of the following methods:
* Download the binary from the [latest release](https://github.com/yt-dlp/yt-dlp/releases/latest) (recommended method) * Download [the binary](#release-files) from the [latest release](https://github.com/yt-dlp/yt-dlp/releases/latest)
* With Homebrew, `brew install yt-dlp/taps/yt-dlp` * With Homebrew, `brew install yt-dlp/taps/yt-dlp`
* Use [PyPI package](https://pypi.org/project/yt-dlp): `python3 -m pip install --upgrade yt-dlp` * Use [PyPI package](https://pypi.org/project/yt-dlp): `python3 -m pip install --upgrade yt-dlp`
* Use pip+git: `python3 -m pip install --upgrade git+https://github.com/yt-dlp/yt-dlp.git@release` * Install master branch: `python3 -m pip3 install -U https://github.com/yt-dlp/yt-dlp/archive/master.zip`
* Install master branch: `python3 -m pip install --upgrade git+https://github.com/yt-dlp/yt-dlp`
Note that on some systems, you may need to use `py` or `python` instead of `python3` Note that on some systems, you may need to use `py` or `python` instead of `python3`
@@ -186,16 +190,45 @@ 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.
If you have installed using Homebrew, run `brew upgrade yt-dlp/taps/yt-dlp` If you have installed using Homebrew, run `brew upgrade yt-dlp/taps/yt-dlp`
### RELEASE FILES
#### Recommended
File|Description
:---|:---
[yt-dlp](https://github.com/yt-dlp/yt-dlp/releases/latest/download/yt-dlp)|Platform independant binary. Needs Python (Recommended for **UNIX-like systems**)
[yt-dlp.exe](https://github.com/yt-dlp/yt-dlp/releases/latest/download/yt-dlp.exe)|Windows standalone x64 binary (Recommended for **Windows**)
#### Alternatives
File|Description
:---|:---
[yt-dlp_macos](https://github.com/yt-dlp/yt-dlp/releases/latest/download/yt-dlp_macos)|MacOS standalone executable
[yt-dlp_x86.exe](https://github.com/yt-dlp/yt-dlp/releases/latest/download/yt-dlp_x86.exe)|Windows standalone x86 (32bit) binary
[yt-dlp_min.exe](https://github.com/yt-dlp/yt-dlp/releases/latest/download/yt-dlp_min.exe)|Windows standalone x64 binary built with `py2exe`.<br/> Does not contain `pycryptodomex`, needs VC++14
[yt-dlp_win.zip](https://github.com/yt-dlp/yt-dlp/releases/latest/download/yt-dlp_win.zip)|Unpackaged windows executable (No auto-update)
[yt-dlp_macos.zip](https://github.com/yt-dlp/yt-dlp/releases/latest/download/yt-dlp_macos.zip)|Unpackaged MacOS executable (No auto-update)
#### Misc
File|Description
:---|:---
[yt-dlp.tar.gz](https://github.com/yt-dlp/yt-dlp/releases/latest/download/yt-dlp.tar.gz)|Source tarball. Also contains manpages, completions, etc
[SHA2-512SUMS](https://github.com/yt-dlp/yt-dlp/releases/latest/download/SHA2-512SUMS)|GNU-style SHA512 sums
[SHA2-256SUMS](https://github.com/yt-dlp/yt-dlp/releases/latest/download/SHA2-256SUMS)|GNU-style SHA256 sums
### DEPENDENCIES ### DEPENDENCIES
Python versions 3.6+ (CPython and PyPy) are supported. Other versions and implementations may or may not work correctly. 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 --> <!-- Python 3.5+ uses VC++14 and it is already embedded in the binary created
<!x-- https://www.microsoft.com/en-us/download/details.aspx?id=26999 --x>
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. 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 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) * [**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)
* [**mutagen**](https://github.com/quodlibet/mutagen) - For embedding thumbnail in certain formats. Licenced under [GPLv2+](https://github.com/quodlibet/mutagen/blob/master/COPYING) * [**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) * [**pycryptodomex**](https://github.com/Legrandin/pycryptodome) - For decrypting AES-128 HLS streams and various other 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) * [**websockets**](https://github.com/aaugustin/websockets) - For downloading over websocket. Licenced under [BSD3](https://github.com/aaugustin/websockets/blob/main/LICENSE)
* [**keyring**](https://github.com/jaraco/keyring) - For decrypting cookies of chromium-based browsers on Linux. Licenced under [MIT](https://github.com/jaraco/keyring/blob/main/LICENSE) * [**keyring**](https://github.com/jaraco/keyring) - For decrypting cookies of chromium-based browsers on Linux. Licenced under [MIT](https://github.com/jaraco/keyring/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) * [**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)
@@ -207,20 +240,19 @@ While all the other dependancies are optional, `ffmpeg` and `ffprobe` are highly
To use or redistribute the dependencies, you must agree to their respective licensing terms. 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. The windows releases are already built with the python interpreter, mutagen, pycryptodomex and websockets included.
**Note**: There are some regressions in newer ffmpeg versions that causes various issues when used alongside yt-dlp. Since ffmpeg is such an important dependancy, we provide [custom builds](https://github.com/yt-dlp/FFmpeg-Builds/wiki/Latest#latest-autobuilds) with patches for these issues at [yt-dlp/FFmpeg-Builds](https://github.com/yt-dlp/FFmpeg-Builds). See [the readme](https://github.com/yt-dlp/FFmpeg-Builds#patches-applied) for details on the specifc issues solved by these builds
### COMPILE ### COMPILE
**For Windows**: **For Windows**:
To build the Windows executable, you must have pyinstaller (and optionally mutagen, pycryptodome, websockets) To build the Windows executable, you must have pyinstaller (and optionally mutagen, pycryptodomex, websockets). Once you have all the necessary dependencies installed, (optionally) build lazy extractors using `devscripts/make_lazy_extractors.py`, and then just run `pyinst.py`. The executable will be built for the same architecture (32/64 bit) as the python used to build it.
python3 -m pip install --upgrade pyinstaller mutagen pycryptodome websockets py -m pip install -U pyinstaller -r requirements.txt
py devscripts/make_lazy_extractors.py
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. py pyinst.py
You can also build the executable without any version info or metadata by using:
pyinstaller.exe yt_dlp\__main__.py --onefile --name yt-dlp
Note that pyinstaller [does not support](https://github.com/pyinstaller/pyinstaller#requirements-and-tested-platforms) Python installed from the Windows store without using a virtual environment Note that pyinstaller [does not support](https://github.com/pyinstaller/pyinstaller#requirements-and-tested-platforms) Python installed from the Windows store without using a virtual environment
@@ -228,7 +260,7 @@ Note that pyinstaller [does not support](https://github.com/pyinstaller/pyinstal
You will need the required build tools: `python`, `make` (GNU), `pandoc`, `zip`, `pytest` You will need the required build tools: `python`, `make` (GNU), `pandoc`, `zip`, `pytest`
Then simply run `make`. You can also run `make yt-dlp` instead to compile only the binary without updating any of the additional files Then simply run `make`. You can also run `make yt-dlp` instead to compile only the binary without updating any of the additional files
**Note**: In either platform, `devscripts\update-version.py` can be used to automatically update the version number **Note**: In either platform, `devscripts/update-version.py` can be used to automatically update the version number
# USAGE AND OPTIONS # USAGE AND OPTIONS
@@ -243,9 +275,12 @@ Then simply run `make`. You can also run `make yt-dlp` instead to compile only t
-U, --update Update this program to latest version. Make -U, --update Update this program to latest version. Make
sure that you have sufficient permissions sure that you have sufficient permissions
(run with sudo if needed) (run with sudo if needed)
-i, --ignore-errors Continue on download errors, for example to -i, --ignore-errors Ignore download and postprocessing errors.
skip unavailable videos in a playlist The download will be considered successfull
(default) (Alias: --no-abort-on-error) even if the postprocessing fails
--no-abort-on-error Continue with next video on download
errors; e.g. to skip unavailable videos in
a playlist (default)
--abort-on-error Abort downloading of further videos if an --abort-on-error Abort downloading of further videos if an
error occurs (Alias: --no-ignore-errors) error occurs (Alias: --no-ignore-errors)
--dump-user-agent Display the current user-agent and exit --dump-user-agent Display the current user-agent and exit
@@ -277,7 +312,8 @@ Then simply run `make`. You can also run `make yt-dlp` instead to compile only t
--flat-playlist Do not extract the videos of a playlist, --flat-playlist Do not extract the videos of a playlist,
only list them only list them
--no-flat-playlist Extract the videos of a playlist --no-flat-playlist Extract the videos of a playlist
--mark-watched Mark videos watched (YouTube only) --mark-watched Mark videos watched (even with --simulate).
Currently only supported for YouTube
--no-mark-watched Do not mark videos watched (default) --no-mark-watched Do not mark videos watched (default)
--no-colors Do not emit color codes in output --no-colors Do not emit color codes in output
--compat-options OPTS Options that can help keep compatibility --compat-options OPTS Options that can help keep compatibility
@@ -452,6 +488,7 @@ Then simply run `make`. You can also run `make yt-dlp` instead to compile only t
stdin), one URL per line. Lines starting stdin), one URL per line. Lines starting
with '#', ';' or ']' are considered as with '#', ';' or ']' are considered as
comments and ignored comments and ignored
--no-batch-file Do not read URLs from batch file (default)
-P, --paths [TYPES:]PATH The paths where the files should be -P, --paths [TYPES:]PATH The paths where the files should be
downloaded. Specify the type of file and downloaded. Specify the type of file and
the path separated by a colon ":". All the the path separated by a colon ":". All the
@@ -530,10 +567,10 @@ Then simply run `make`. You can also run `make yt-dlp` instead to compile only t
--cookies-from-browser BROWSER[:PROFILE] --cookies-from-browser BROWSER[:PROFILE]
Load cookies from a user profile of the Load cookies from a user profile of the
given web browser. Currently supported given web browser. Currently supported
browsers are: brave|chrome|chromium|edge|fi browsers are: brave, chrome, chromium,
refox|opera|safari|vivaldi. You can specify edge, firefox, opera, safari, vivaldi. You
the user profile name or directory using can specify the user profile name or
"BROWSER:PROFILE_NAME" or directory using "BROWSER:PROFILE_NAME" or
"BROWSER:PROFILE_PATH". If no profile is "BROWSER:PROFILE_PATH". If no profile is
given, the most recently accessed one is given, the most recently accessed one is
used used
@@ -598,7 +635,18 @@ Then simply run `make`. You can also run `make yt-dlp` instead to compile only t
(Alias: --force-download-archive) (Alias: --force-download-archive)
--newline Output progress bar as new lines --newline Output progress bar as new lines
--no-progress Do not print progress bar --no-progress Do not print progress bar
--progress Show progress bar, even if in quiet mode
--console-title Display progress in console titlebar --console-title Display progress in console titlebar
--progress-template [TYPES:]TEMPLATE
Template for progress outputs, optionally
prefixed with one of "download:" (default),
"download-title:" (the console title),
"postprocess:", or "postprocess-title:".
The video's fields are accessible under the
"info" key and the progress attributes are
accessible under "progress" key. Eg:
--console-title --progress-template
"download-title:%(info.id)s-%(progress.eta)s"
-v, --verbose Print various debugging information -v, --verbose Print various debugging information
--dump-pages Print downloaded pages encoded using base64 --dump-pages Print downloaded pages encoded using base64
to debug problems (very verbose) to debug problems (very verbose)
@@ -695,6 +743,9 @@ Then simply run `make`. You can also run `make yt-dlp` instead to compile only t
out, yt-dlp will ask interactively out, yt-dlp will ask interactively
-2, --twofactor TWOFACTOR Two-factor authentication code -2, --twofactor TWOFACTOR Two-factor authentication code
-n, --netrc Use .netrc authentication data -n, --netrc Use .netrc authentication data
--netrc-location PATH Location of .netrc authentication data;
either the path or its containing
directory. Defaults to ~/.netrc
--video-password PASSWORD Video password (vimeo, youku) --video-password PASSWORD Video password (vimeo, youku)
--ap-mso MSO Adobe Pass multiple-system operator (TV --ap-mso MSO Adobe Pass multiple-system operator (TV
provider) identifier, use --ap-list-mso for provider) identifier, use --ap-list-mso for
@@ -820,7 +871,11 @@ Then simply run `make`. You can also run `make yt-dlp` instead to compile only t
--no-split-chapters Do not split video based on chapters --no-split-chapters Do not split video based on chapters
(default) (default)
--remove-chapters REGEX Remove chapters whose title matches the --remove-chapters REGEX Remove chapters whose title matches the
given regular expression. This option can given regular expression. Time ranges
prefixed by a "*" can also be used in place
of chapters to remove the specified range.
Eg: --remove-chapters "*10:15-15:00"
--remove-chapters "intro". This option can
be used multiple times be used multiple times
--no-remove-chapters Do not remove any chapters from the file --no-remove-chapters Do not remove any chapters from the file
(default) (default)
@@ -831,6 +886,20 @@ Then simply run `make`. You can also run `make yt-dlp` instead to compile only t
around the cuts around the cuts
--no-force-keyframes-at-cuts Do not force keyframes around the chapters --no-force-keyframes-at-cuts Do not force keyframes around the chapters
when cutting/splitting (default) when cutting/splitting (default)
--use-postprocessor NAME[:ARGS] The (case sensitive) name of plugin
postprocessors to be enabled, and
(optionally) arguments to be passed to it,
seperated by a colon ":". ARGS are a
semicolon ";" delimited list of NAME=VALUE.
The "when" argument determines when the
postprocessor is invoked. It can be one of
"pre_process" (after extraction),
"before_dl" (before video download),
"post_process" (after video download;
default) or "after_move" (after moving file
to their final locations). This option can
be used multiple times to add different
postprocessors
## SponsorBlock Options: ## SponsorBlock Options:
Make chapter entries for, or remove various segments (sponsor, Make chapter entries for, or remove various segments (sponsor,
@@ -897,7 +966,7 @@ You can configure yt-dlp by placing any supported command line option to a confi
* `~/yt-dlp.conf` * `~/yt-dlp.conf`
* `~/yt-dlp.conf.txt` * `~/yt-dlp.conf.txt`
Note that `~` points to `C:\Users\<user name>` on windows. Also, `%XDG_CONFIG_HOME%` defaults to `~/.config` if undefined `%XDG_CONFIG_HOME%` defaults to `~/.config` if undefined. On windows, `~` points to %HOME% if present, `%USERPROFILE%` (generally `C:\Users\<user name>`) or `%HOMEDRIVE%%HOMEPATH%`.
1. **System Configuration**: `/etc/yt-dlp.conf` 1. **System Configuration**: `/etc/yt-dlp.conf`
For example, with the following configuration file yt-dlp will always extract the audio, not copy the mtime, use a proxy and save all videos under `YouTube` directory in your home directory: For example, with the following configuration file yt-dlp will always extract the audio, not copy the mtime, use a proxy and save all videos under `YouTube` directory in your home directory:
@@ -923,14 +992,14 @@ You can use `--ignore-config` if you want to disable all configuration files for
### Authentication with `.netrc` file ### Authentication with `.netrc` file
You may also want to configure automatic credentials storage for extractors that support authentication (by providing login and password with `--username` and `--password`) in order not to pass credentials as command line arguments on every yt-dlp execution and prevent tracking plain text passwords in the shell command history. You can achieve this using a [`.netrc` file](https://stackoverflow.com/tags/.netrc/info) on a per extractor basis. For that you will need to create a `.netrc` file in your `$HOME` and restrict permissions to read/write by only you: You may also want to configure automatic credentials storage for extractors that support authentication (by providing login and password with `--username` and `--password`) in order not to pass credentials as command line arguments on every yt-dlp execution and prevent tracking plain text passwords in the shell command history. You can achieve this using a [`.netrc` file](https://stackoverflow.com/tags/.netrc/info) on a per extractor basis. For that you will need to create a `.netrc` file in `--netrc-location` and restrict permissions to read/write by only you:
``` ```
touch $HOME/.netrc touch $HOME/.netrc
chmod a-rwx,u+rw $HOME/.netrc chmod a-rwx,u+rw $HOME/.netrc
``` ```
After that you can add credentials for an extractor in the following format, where *extractor* is the name of the extractor in lowercase: After that you can add credentials for an extractor in the following format, where *extractor* is the name of the extractor in lowercase:
``` ```
machine <extractor> login <login> password <password> machine <extractor> login <username> password <password>
``` ```
For example: For example:
``` ```
@@ -939,10 +1008,7 @@ machine twitch login my_twitch_account_name password my_twitch_password
``` ```
To activate authentication with the `.netrc` file you should pass `--netrc` to yt-dlp or place it in the [configuration file](#configuration). To activate authentication with the `.netrc` file you should pass `--netrc` to yt-dlp or place it in the [configuration file](#configuration).
On Windows you may also need to setup the `%HOME%` environment variable manually. For example: The default location of the .netrc file is `$HOME` (`~`) in UNIX. On Windows, it is `%HOME%` if present, `%USERPROFILE%` (generally `C:\Users\<user name>`) or `%HOMEDRIVE%%HOMEPATH%`
```
set HOME=%USERPROFILE%
```
# OUTPUT TEMPLATE # OUTPUT TEMPLATE
@@ -952,21 +1018,23 @@ The `-o` option is used to indicate a template for the output file names while `
The simplest usage of `-o` is not to set any template arguments when downloading a single file, like in `yt-dlp -o funny_video.flv "https://some/video"` (hard-coding file extension like this is _not_ recommended and could break some post-processing). The simplest usage of `-o` is not to set any template arguments when downloading a single file, like in `yt-dlp -o funny_video.flv "https://some/video"` (hard-coding file extension like this is _not_ recommended and could break some post-processing).
It may however also contain special sequences that will be replaced when downloading each video. The special sequences may be formatted according to [python string formatting operations](https://docs.python.org/2/library/stdtypes.html#string-formatting). For example, `%(NAME)s` or `%(NAME)05d`. To clarify, that is a percent symbol followed by a name in parentheses, followed by formatting operations. It may however also contain special sequences that will be replaced when downloading each video. The special sequences may be formatted according to [python string formatting operations](https://docs.python.org/3/library/stdtypes.html#printf-style-string-formatting). For example, `%(NAME)s` or `%(NAME)05d`. To clarify, that is a percent symbol followed by a name in parentheses, followed by formatting operations.
The field names themselves (the part inside the parenthesis) can also have some special formatting: The field names themselves (the part inside the parenthesis) can also have some special formatting:
1. **Object traversal**: The dictionaries and lists available in metadata can be traversed by using a `.` (dot) separator. You can also do python slicing using `:`. Eg: `%(tags.0)s`, `%(subtitles.en.-1.ext)s`, `%(id.3:7:-1)s`, `%(formats.:.format_id)s`. `%()s` refers to the entire infodict. Note that all the fields that become available using this method are not listed below. Use `-j` to see such fields 1. **Object traversal**: The dictionaries and lists available in metadata can be traversed by using a `.` (dot) separator. You can also do python slicing using `:`. Eg: `%(tags.0)s`, `%(subtitles.en.-1.ext)s`, `%(id.3:7:-1)s`, `%(formats.:.format_id)s`. `%()s` refers to the entire infodict. Note that all the fields that become available using this method are not listed below. Use `-j` to see such fields
1. **Addition**: Addition and subtraction of numeric fields can be done using `+` and `-` respectively. Eg: `%(playlist_index+10)03d`, `%(n_entries+1-playlist_index)d` 1. **Addition**: Addition and subtraction of numeric fields can be done using `+` and `-` respectively. Eg: `%(playlist_index+10)03d`, `%(n_entries+1-playlist_index)d`
1. **Date/time Formatting**: Date/time fields can be formatted according to [strftime formatting](https://docs.python.org/3/library/datetime.html#strftime-and-strptime-format-codes) by specifying it separated from the field name using a `>`. Eg: `%(duration>%H-%M-%S)s`, `%(upload_date>%Y-%m-%d)s`, `%(epoch-3600>%H-%M-%S)s` 1. **Date/time Formatting**: Date/time fields can be formatted according to [strftime formatting](https://docs.python.org/3/library/datetime.html#strftime-and-strptime-format-codes) by specifying it separated from the field name using a `>`. Eg: `%(duration>%H-%M-%S)s`, `%(upload_date>%Y-%m-%d)s`, `%(epoch-3600>%H-%M-%S)s`
1. **Default**: A default value can be specified for when the field is empty using a `|` seperator. This overrides `--output-na-template`. Eg: `%(uploader|Unknown)s` 1. **Alternatives**: Alternate fields can be specified seperated with a `,`. Eg: `%(release_date>%Y,upload_date>%Y|Unknown)s`
1. **More Conversions**: In addition to the normal format types `diouxXeEfFgGcrs`, `j`, `l`, `q` can be used for converting to **j**son, a comma seperated **l**ist and a string **q**uoted for the terminal respectively 1. **Default**: A literal default value can be specified for when the field is empty using a `|` seperator. This overrides `--output-na-template`. Eg: `%(uploader|Unknown)s`
1. **More Conversions**: In addition to the normal format types `diouxXeEfFgGcrs`, `B`, `j`, `l`, `q` can be used for converting to **B**ytes, **j**son, a comma seperated **l**ist (alternate form flag `#` makes it new line `\n` seperated) and a string **q**uoted for the terminal, respectively
1. **Unicode normalization**: The format type `U` can be used for NFC [unicode normalization](https://docs.python.org/3/library/unicodedata.html#unicodedata.normalize). The alternate form flag (`#`) changes the normalization to NFD and the conversion flag `+` can be used for NFKC/NFKD compatibility equivalence normalization. Eg: `%(title)+.100U` is NFKC
To summarize, the general syntax for a field is: To summarize, the general syntax for a field is:
``` ```
%(name[.keys][addition][>strf][|default])[flags][width][.precision][length]type %(name[.keys][addition][>strf][,alternate][|default])[flags][width][.precision][length]type
``` ```
Additionally, you can set different output templates for the various metadata files separately from the general output template by specifying the type of file followed by the template separated by a colon `:`. The different file types supported are `subtitle`, `thumbnail`, `description`, `annotation`, `infojson`, `pl_thumbnail`, `pl_description`, `pl_infojson`, `chapter`. For example, `-o '%(title)s.%(ext)s' -o 'thumbnail:%(title)s\%(title)s.%(ext)s'` will put the thumbnails in a folder with the same name as the video. Additionally, you can set different output templates for the various metadata files separately from the general output template by specifying the type of file followed by the template separated by a colon `:`. The different file types supported are `subtitle`, `thumbnail`, `description`, `annotation` (deprecated), `infojson`, `pl_thumbnail`, `pl_description`, `pl_infojson`, `chapter`. For example, `-o '%(title)s.%(ext)s' -o 'thumbnail:%(title)s\%(title)s.%(ext)s'` will put the thumbnails in a folder with the same name as the video. If any of the templates (except default) is empty, that type of file will not be written. Eg: `--write-thumbnail -o "thumbnail:"` will write thumbnails only for playlists and not for video.
The available fields are: The available fields are:
@@ -980,9 +1048,10 @@ The available fields are:
- `uploader` (string): Full name of the video uploader - `uploader` (string): Full name of the video uploader
- `license` (string): License name the video is licensed under - `license` (string): License name the video is licensed under
- `creator` (string): The creator of the video - `creator` (string): The creator of the video
- `release_date` (string): The date (YYYYMMDD) when the video was released
- `timestamp` (numeric): UNIX timestamp of the moment the video became available - `timestamp` (numeric): UNIX timestamp of the moment the video became available
- `upload_date` (string): Video upload date (YYYYMMDD) - `upload_date` (string): Video upload date (YYYYMMDD)
- `release_date` (string): The date (YYYYMMDD) when the video was released
- `release_timestamp` (numeric): UNIX timestamp of the moment the video was released
- `uploader_id` (string): Nickname or id of the video uploader - `uploader_id` (string): Nickname or id of the video uploader
- `channel` (string): Full name of the channel the video is uploaded on - `channel` (string): Full name of the channel the video is uploaded on
- `channel_id` (string): Id of the channel - `channel_id` (string): Id of the channel
@@ -1015,6 +1084,7 @@ The available fields are:
- `asr` (numeric): Audio sampling rate in Hertz - `asr` (numeric): Audio sampling rate in Hertz
- `vbr` (numeric): Average video bitrate in KBit/s - `vbr` (numeric): Average video bitrate in KBit/s
- `fps` (numeric): Frame rate - `fps` (numeric): Frame rate
- `dynamic_range` (string): The dynamic range of the video
- `vcodec` (string): Name of the video codec in use - `vcodec` (string): Name of the video codec in use
- `container` (string): Name of the container format - `container` (string): Name of the container format
- `filesize` (numeric): The number of bytes, if known in advance - `filesize` (numeric): The number of bytes, if known in advance
@@ -1024,8 +1094,10 @@ The available fields are:
- `extractor_key` (string): Key name of the extractor - `extractor_key` (string): Key name of the extractor
- `epoch` (numeric): Unix epoch when creating the file - `epoch` (numeric): Unix epoch when creating the file
- `autonumber` (numeric): Number that will be increased with each download, starting at `--autonumber-start` - `autonumber` (numeric): Number that will be increased with each download, starting at `--autonumber-start`
- `n_entries` (numeric): Total number of extracted items in the playlist
- `playlist` (string): Name or id of the playlist that contains the video - `playlist` (string): Name or id of the playlist that contains the video
- `playlist_index` (numeric): Index of the video in the playlist padded with leading zeros according to the total length of the playlist - `playlist_index` (numeric): Index of the video in the playlist padded with leading zeros according the final index
- `playlist_autonumber` (numeric): Position of the video in the playlist download queue padded with leading zeros according to the total length of the playlist
- `playlist_id` (string): Playlist identifier - `playlist_id` (string): Playlist identifier
- `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
@@ -1083,11 +1155,13 @@ Available only in `--sponsorblock-chapter-title`:
- `category_names` (list): Friendly names of the categories - `category_names` (list): Friendly names of the categories
- `name` (string): Friendly name of the smallest category - `name` (string): Friendly name of the smallest category
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. 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 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. 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 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`. **Tip**: Look at the `-j` output to identify which fields are available for the purticular URL
For numeric sequences you can use [numeric related formatting](https://docs.python.org/3/library/stdtypes.html#printf-style-string-formatting), for example, `%(view_count)05d` will result in a string with view count padded with zeros up to 5 characters, like in `00042`.
Output templates can also contain arbitrary hierarchical path, e.g. `-o '%(playlist)s/%(playlist_index)s - %(title)s.%(ext)s'` which will result in downloading each video in a directory corresponding to this path template. Any missing directory will be automatically created for you. Output templates can also contain arbitrary hierarchical path, e.g. `-o '%(playlist)s/%(playlist_index)s - %(title)s.%(ext)s'` which will result in downloading each video in a directory corresponding to this path template. Any missing directory will be automatically created for you.
@@ -1136,6 +1210,8 @@ $ yt-dlp -o - BaW_jenozKc
By default, yt-dlp tries to download the best available quality if you **don't** pass any options. By default, yt-dlp tries to download the best available quality if you **don't** pass any options.
This is generally equivalent to using `-f bestvideo*+bestaudio/best`. However, if multiple audiostreams is enabled (`--audio-multistreams`), the default format changes to `-f bestvideo+bestaudio/best`. Similarly, if ffmpeg is unavailable, or if you use yt-dlp to stream to `stdout` (`-o -`), the default becomes `-f best/bestvideo+bestaudio`. This is generally equivalent to using `-f bestvideo*+bestaudio/best`. However, if multiple audiostreams is enabled (`--audio-multistreams`), the default format changes to `-f bestvideo+bestaudio/best`. Similarly, if ffmpeg is unavailable, or if you use yt-dlp to stream to `stdout` (`-o -`), the default becomes `-f best/bestvideo+bestaudio`.
**Deprecation warning**: Latest versions of yt-dlp can stream multiple formats to the stdout simultaneously using ffmpeg. So, in future versions, the default for this will be set to `-f bv*+ba/b` similar to normal downloads. If you want to preserve the `-f b/bv+ba` setting, it is recommended to explicitly specify it in the configuration options.
The general syntax for format selection is `-f FORMAT` (or `--format FORMAT`) where `FORMAT` is a *selector expression*, i.e. an expression that describes format or formats you would like to download. The general syntax for format selection is `-f FORMAT` (or `--format FORMAT`) where `FORMAT` is a *selector expression*, i.e. an expression that describes format or formats you would like to download.
**tl;dr:** [navigate me to examples](#format-selection-examples). **tl;dr:** [navigate me to examples](#format-selection-examples).
@@ -1169,7 +1245,11 @@ If you want to download multiple videos and they don't have the same formats ava
If you want to download several formats of the same video use a comma as a separator, e.g. `-f 22,17,18` will download all these three formats, of course if they are available. Or a more sophisticated example combined with the precedence feature: `-f 136/137/mp4/bestvideo,140/m4a/bestaudio`. If you want to download several formats of the same video use a comma as a separator, e.g. `-f 22,17,18` will download all these three formats, of course if they are available. Or a more sophisticated example combined with the precedence feature: `-f 136/137/mp4/bestvideo,140/m4a/bestaudio`.
You can merge the video and audio of multiple formats into a single file using `-f <format1>+<format2>+...` (requires ffmpeg installed), for example `-f bestvideo+bestaudio` will download the best video-only format, the best audio-only format and mux them together with ffmpeg. Unless `--video-multistreams` is used, all formats with a video stream except the first one are ignored. Similarly, unless `--audio-multistreams` is used, all formats with an audio stream except the first one are ignored. For example, `-f bestvideo+best+bestaudio --video-multistreams --audio-multistreams` will download and merge all 3 given formats. The resulting file will have 2 video streams and 2 audio streams. But `-f bestvideo+best+bestaudio --no-video-multistreams` will download and merge only `bestvideo` and `bestaudio`. `best` is ignored since another format containing a video stream (`bestvideo`) has already been selected. The order of the formats is therefore important. `-f best+bestaudio --no-audio-multistreams` will download and merge both formats while `-f bestaudio+best --no-audio-multistreams` will ignore `best` and download only `bestaudio`. You can merge the video and audio of multiple formats into a single file using `-f <format1>+<format2>+...` (requires ffmpeg installed), for example `-f bestvideo+bestaudio` will download the best video-only format, the best audio-only format and mux them together with ffmpeg.
**Deprecation warning**: Since the *below* described behavior is complex and counter-intuitive, this will be removed and multistreams will be enabled by default in the future. A new operator will be instead added to limit formats to single audio/video
Unless `--video-multistreams` is used, all formats with a video stream except the first one are ignored. Similarly, unless `--audio-multistreams` is used, all formats with an audio stream except the first one are ignored. For example, `-f bestvideo+best+bestaudio --video-multistreams --audio-multistreams` will download and merge all 3 given formats. The resulting file will have 2 video streams and 2 audio streams. But `-f bestvideo+best+bestaudio --no-video-multistreams` will download and merge only `bestvideo` and `bestaudio`. `best` is ignored since another format containing a video stream (`bestvideo`) has already been selected. The order of the formats is therefore important. `-f best+bestaudio --no-audio-multistreams` will download and merge both formats while `-f bestaudio+best --no-audio-multistreams` will ignore `best` and download only `bestaudio`.
## Filtering Formats ## Filtering Formats
@@ -1230,17 +1310,20 @@ The available fields are:
- `width`: Width of video - `width`: Width of video
- `res`: Video resolution, calculated as the smallest dimension. - `res`: Video resolution, calculated as the smallest dimension.
- `fps`: Framerate of video - `fps`: Framerate of video
- `hdr`: The dynamic range of the video (`DV` > `HDR12` > `HDR10+` > `HDR10` > `HLG` > `SDR`)
- `tbr`: Total average bitrate in KBit/s - `tbr`: Total average bitrate in KBit/s
- `vbr`: Average video bitrate in KBit/s - `vbr`: Average video bitrate in KBit/s
- `abr`: Average audio bitrate in KBit/s - `abr`: Average audio bitrate in KBit/s
- `br`: Equivalent to using `tbr,vbr,abr` - `br`: Equivalent to using `tbr,vbr,abr`
- `asr`: Audio sample rate in Hz - `asr`: Audio sample rate in Hz
**Deprecation warning**: Many of these fields have (currently undocumented) aliases, that may be removed in a future version. It is recommended to use only the documented field names.
All fields, unless specified otherwise, are sorted in descending order. To reverse this, prefix the field with a `+`. Eg: `+res` prefers format with the smallest resolution. Additionally, you can suffix a preferred value for the fields, separated by a `:`. Eg: `res:720` prefers larger videos, but no larger than 720p and the smallest video if there are no videos less than 720p. For `codec` and `ext`, you can provide two preferred values, the first for video and the second for audio. Eg: `+codec:avc:m4a` (equivalent to `+vcodec:avc,+acodec:m4a`) sets the video codec preference to `h264` > `h265` > `vp9` > `vp9.2` > `av01` > `vp8` > `h263` > `theora` and audio codec preference to `mp4a` > `aac` > `vorbis` > `opus` > `mp3` > `ac3` > `dts`. You can also make the sorting prefer the nearest values to the provided by using `~` as the delimiter. Eg: `filesize~1G` prefers the format with filesize closest to 1 GiB. All fields, unless specified otherwise, are sorted in descending order. To reverse this, prefix the field with a `+`. Eg: `+res` prefers format with the smallest resolution. Additionally, you can suffix a preferred value for the fields, separated by a `:`. Eg: `res:720` prefers larger videos, but no larger than 720p and the smallest video if there are no videos less than 720p. For `codec` and `ext`, you can provide two preferred values, the first for video and the second for audio. Eg: `+codec:avc:m4a` (equivalent to `+vcodec:avc,+acodec:m4a`) sets the video codec preference to `h264` > `h265` > `vp9` > `vp9.2` > `av01` > `vp8` > `h263` > `theora` and audio codec preference to `mp4a` > `aac` > `vorbis` > `opus` > `mp3` > `ac3` > `dts`. You can also make the sorting prefer the nearest values to the provided by using `~` as the delimiter. Eg: `filesize~1G` prefers the format with filesize closest to 1 GiB.
The fields `hasvid` and `ie_pref` are always given highest priority in sorting, irrespective of the user-defined order. This behaviour can be changed by using `--force-format-sort`. Apart from these, the default order used is: `lang,quality,res,fps,codec:vp9.2,size,br,asr,proto,ext,hasaud,source,id`. The extractors may override this default order, but they cannot override the user-provided order. The fields `hasvid` and `ie_pref` are always given highest priority in sorting, irrespective of the user-defined order. This behaviour can be changed by using `--format-sort-force`. Apart from these, the default order used is: `lang,quality,res,fps,hdr:12,codec:vp9.2,size,br,asr,proto,ext,hasaud,source,id`. The extractors may override this default order, but they cannot override the user-provided order.
Note that the default has `codec:vp9.2`; i.e. `av1` is not prefered Note that the default has `codec:vp9.2`; i.e. `av1` is not prefered. Similarly, the default for hdr is `hdr:12`; i.e. dolby vision is not prefered. These choices are made since DV and AV1 formats are not yet fully compatible with most devices. This may be changed in the future as more devices become capable of smoothly playing back these formats.
If your format selector is `worst`, the last item is selected after sorting. This means it will select the format that is worst in all respects. Most of the time, what you actually want is the video with the smallest filesize instead. So it is generally better to use `-f best -S +size,+br,+res,+fps`. If your format selector is `worst`, the last item is selected after sorting. This means it will select the format that is worst in all respects. Most of the time, what you actually want is the video with the smallest filesize instead. So it is generally better to use `-f best -S +size,+br,+res,+fps`.
@@ -1382,7 +1465,7 @@ Note that any field created by this can be used in the [output template](#output
This option also has a few special uses: This option also has a few special uses:
* You can download an additional URL based on the metadata of the currently downloaded video. To do this, set the field `additional_urls` to the URL that you want to download. Eg: `--parse-metadata "description:(?P<additional_urls>https?://www\.vimeo\.com/\d+)` will download the first vimeo video found in the description * You can download an additional URL based on the metadata of the currently downloaded video. To do this, set the field `additional_urls` to the URL that you want to download. Eg: `--parse-metadata "description:(?P<additional_urls>https?://www\.vimeo\.com/\d+)` will download the first vimeo video found in the description
* You can use this to change the metadata that is embedded in the media file. To do this, set the value of the corresponding field with a `meta_` prefix. For example, any value you set to `meta_description` field will be added to the `description` field in the file. For example, you can use this to set a different "description" and "synopsis" * You can use this to change the metadata that is embedded in the media file. To do this, set the value of the corresponding field with a `meta_` prefix. For example, any value you set to `meta_description` field will be added to the `description` field in the file. For example, you can use this to set a different "description" and "synopsis". Any value set to the `meta_` field will overwrite all default values.
For reference, these are the fields yt-dlp adds by default to the file metadata: For reference, these are the fields yt-dlp adds by default to the file metadata:
@@ -1436,11 +1519,14 @@ The following extractors use this feature:
* **youtube** * **youtube**
* `skip`: `hls` or `dash` (or both) to skip download of the respective manifests * `skip`: `hls` or `dash` (or both) to skip download of the respective manifests
* `player_client`: Clients to extract video data from. The main clients are `web`, `android`, `ios`, `mweb`. These also have `_music`, `_embedded`, `_agegate`, and `_creator` variants (Eg: `web_embedded`) (`mweb` has only `_agegate`). By default, `android,web` is used, but the agegate and creator variants are added as required for age-gated videos. Similarly the music variants are added for `music.youtube.com` urls. You can also use `all` to use all the clients * `player_client`: Clients to extract video data from. The main clients are `web`, `android`, `ios`, `mweb`. These also have `_music`, `_embedded`, `_agegate`, and `_creator` variants (Eg: `web_embedded`) (`mweb` has only `_agegate`). By default, `android,web` is used, but the agegate and creator variants are added as required for age-gated videos. Similarly the music variants are added for `music.youtube.com` urls. You can also use `all` to use all the clients
* `player_skip`: `configs` - skip any requests for client configs and use defaults * `player_skip`: Skip some network requests that are generally needed for robust extraction. One or more of `configs` (skip client configs), `webpage` (skip initial webpage), `js` (skip js player). While these options can help reduce the number of requests needed or avoid some rate-limiting, they could cause some issues. See [#860](https://github.com/yt-dlp/yt-dlp/pull/860) for more details
* `include_live_dash`: Include live dash formats (These formats don't download properly) * `include_live_dash`: Include live dash formats (These formats don't download properly)
* `comment_sort`: `top` or `new` (default) - choose comment sorting mode (on YouTube's side). * `comment_sort`: `top` or `new` (default) - choose comment sorting mode (on YouTube's side).
* `max_comments`: Maximum amount of comments to download (default all). * `max_comments`: Maximum amount of comments to download (default all).
* `max_comment_depth`: Maximum depth for nested comments. YouTube supports depths 1 or 2 (default). * `max_comment_depth`: Maximum depth for nested comments. YouTube supports depths 1 or 2 (default).
* **youtubetab**
(YouTube playlists, channels, feeds, etc.)
* `skip`: One or more of `webpage` (skip initial webpage download), `authcheck` (allow the download of playlists requiring authentication when no initial webpage is downloaded. This may cause unwanted behavior, see [#1122](https://github.com/yt-dlp/yt-dlp/pull/1122) for more details)
* **funimation** * **funimation**
* `language`: Languages to extract. Eg: `funimation:language=english,japanese` * `language`: Languages to extract. Eg: `funimation:language=english,japanese`
@@ -1454,9 +1540,96 @@ NOTE: These options may be changed/removed in the future without concern for bac
# 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`; where `<root-dir>` is the directory of the binary (`<root-dir>/yt-dlp`), or the root directory of the module if you are running directly from source-code (`<root dir>/yt_dlp/__main__.py`). Plugins are currently not supported for the `pip` version
Plugins can be of `<type>`s `extractor` or `postprocessor`. Extractor plugins do not need to be enabled from the CLI and are automatically invoked when the input URL is suitable for it. Postprocessor plugins can be invoked using `--use-postprocessor NAME`.
See [ytdlp_plugins](ytdlp_plugins) for example plugins.
Note that **all** plugins are imported even if not invoked, and that **there are no checks** performed on plugin code. Use plugins at your own risk and only if you trust the code
If you are a plugin author, add [ytdlp-plugins](https://github.com/topics/ytdlp-plugins) as a topic to your repository for discoverability
# EMBEDDING YT-DLP
yt-dlp makes the best effort to be a good command-line program, and thus should be callable from any programming language.
Your program should avoid parsing the normal stdout since they may change in future versions. Instead they should use options such as `-J`, `--print`, `--progress-template`, `--exec` etc to create console output that you can reliably reproduce and parse.
From a Python program, you can embed yt-dlp in a more powerful fashion, like this:
```python
import yt_dlp
ydl_opts = {}
with yt_dlp.YoutubeDL(ydl_opts) as ydl:
ydl.download(['https://www.youtube.com/watch?v=BaW_jenozKc'])
```
Most likely, you'll want to use various options. For a list of options available, have a look at [`yt_dlp/YoutubeDL.py`](yt_dlp/YoutubeDL.py#L154-L452).
Here's a more complete example of a program that outputs only errors (and a short message after the download is finished), converts the video to an mp3 file, implements a custom postprocessor and prints the final info_dict as json:
```python
import json
import yt_dlp
from yt_dlp.postprocessor.common import PostProcessor
class MyLogger:
def debug(self, msg):
# For compatability with youtube-dl, both debug and info are passed into debug
# You can distinguish them by the prefix '[debug] '
if msg.startswith('[debug] '):
pass
else:
self.info(msg)
def info(self, msg):
pass
def warning(self, msg):
pass
def error(self, msg):
print(msg)
class MyCustomPP(PostProcessor):
def run(self, info):
self.to_screen('Doing stuff')
return [], info
def my_hook(d):
if d['status'] == 'finished':
print('Done downloading, now converting ...')
ydl_opts = {
'format': 'bestaudio/best',
'postprocessors': [{
'key': 'FFmpegExtractAudio',
'preferredcodec': 'mp3',
'preferredquality': '192',
}],
'logger': MyLogger(),
'progress_hooks': [my_hook],
}
with yt_dlp.YoutubeDL(ydl_opts) as ydl:
ydl.add_post_processor(MyCustomPP())
info = ydl.extract_info('https://www.youtube.com/watch?v=BaW_jenozKc')
print(json.dumps(ydl.sanitize_info(info)))
```
See the public functions in [`yt_dlp/YoutubeDL.py`](yt_dlp/YoutubeDL.py) for other available functions. Eg: `ydl.download`, `ydl.download_with_info_file`
**Tip**: If you are porting your code from youtube-dl to yt-dlp, one important point to look out for is that we do not guarantee the return value of `YoutubeDL.extract_info` to be json serializable, or even be a dictionary. It will be dictionary-like, but if you want to ensure it is a serializable dictionary, pass it through `YoutubeDL.sanitize_info` as shown in the example above
**Note**: `<root-dir>` is the directory of the binary (`<root-dir>/yt-dlp`), or the root directory of the module if you are running directly from source-code (`<root dir>/yt_dlp/__main__.py`)
# DEPRECATED OPTIONS # DEPRECATED OPTIONS
@@ -1488,6 +1661,7 @@ While these options still work, their use is not recommended since there are oth
--print-json -j --no-simulate --print-json -j --no-simulate
--autonumber-size NUMBER Use string formatting. Eg: %(autonumber)03d --autonumber-size NUMBER Use string formatting. Eg: %(autonumber)03d
--autonumber-start NUMBER Use internal field formatting like %(autonumber+NUMBER)s --autonumber-start NUMBER Use internal field formatting like %(autonumber+NUMBER)s
--id -o "%(id)s.%(ext)s"
--metadata-from-title FORMAT --parse-metadata "%(title)s:FORMAT" --metadata-from-title FORMAT --parse-metadata "%(title)s:FORMAT"
--hls-prefer-native --downloader "m3u8:native" --hls-prefer-native --downloader "m3u8:native"
--hls-prefer-ffmpeg --downloader "m3u8:ffmpeg" --hls-prefer-ffmpeg --downloader "m3u8:ffmpeg"
@@ -1554,11 +1728,12 @@ These options may no longer work as intended
#### Removed #### Removed
These options were deprecated since 2014 and have now been entirely removed These options were deprecated since 2014 and have now been entirely removed
--id -o "%(id)s.%(ext)s"
-A, --auto-number -o "%(autonumber)s-%(id)s.%(ext)s" -A, --auto-number -o "%(autonumber)s-%(id)s.%(ext)s"
-t, --title -o "%(title)s-%(id)s.%(ext)s" -t, --title -o "%(title)s-%(id)s.%(ext)s"
-l, --literal -o accepts literal names -l, --literal -o accepts literal names
# CONTRIBUTING
See [CONTRIBUTING.md](CONTRIBUTING.md#contributing-to-yt-dlp) for instructions on [Opening an Issue](CONTRIBUTING.md#opening-an-issue) and [Contributing code to the project](CONTRIBUTING.md#developer-instructions)
# MORE # MORE
For FAQ, Developer Instructions etc., see the [original README](https://github.com/ytdl-org/youtube-dl#faq) For FAQ see the [youtube-dl README](https://github.com/ytdl-org/youtube-dl#faq)

View File

@@ -1,9 +1,15 @@
# coding: utf-8 # coding: utf-8
import re import re
from ..utils import bug_reports_message, write_string
class LazyLoadMetaClass(type): class LazyLoadMetaClass(type):
def __getattr__(cls, name): def __getattr__(cls, name):
if '_real_class' not in cls.__dict__:
write_string(
f'WARNING: Falling back to normal extractor since lazy extractor '
f'{cls.__name__} does not have attribute {name}{bug_reports_message()}')
return getattr(cls._get_real_class(), name) return getattr(cls._get_real_class(), name)
@@ -13,10 +19,10 @@ class LazyLoadExtractor(metaclass=LazyLoadMetaClass):
@classmethod @classmethod
def _get_real_class(cls): def _get_real_class(cls):
if '__real_class' not in cls.__dict__: if '_real_class' not in cls.__dict__:
mod = __import__(cls._module, fromlist=(cls.__name__,)) mod = __import__(cls._module, fromlist=(cls.__name__,))
cls.__real_class = getattr(mod, cls.__name__) cls._real_class = getattr(mod, cls.__name__)
return cls.__real_class return cls._real_class
def __new__(cls, *args, **kwargs): def __new__(cls, *args, **kwargs):
real_cls = cls._get_real_class() real_cls = cls._get_real_class()

View File

@@ -1,33 +1,34 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
from __future__ import unicode_literals from __future__ import unicode_literals
# import io import io
import optparse import optparse
# import re import re
def main(): def main():
return # This is unused in yt-dlp
parser = optparse.OptionParser(usage='%prog INFILE OUTFILE') parser = optparse.OptionParser(usage='%prog INFILE OUTFILE')
options, args = parser.parse_args() options, args = parser.parse_args()
if len(args) != 2: if len(args) != 2:
parser.error('Expected an input and an output filename') parser.error('Expected an input and an output filename')
infile, outfile = args
""" infile, outfile = args
with io.open(infile, encoding='utf-8') as inf: with io.open(infile, encoding='utf-8') as inf:
readme = inf.read() readme = inf.read()
bug_text = re.search( """ bug_text = re.search(
# r'(?s)#\s*BUGS\s*[^\n]*\s*(.*?)#\s*COPYRIGHT', readme).group(1) r'(?s)#\s*BUGS\s*[^\n]*\s*(.*?)#\s*COPYRIGHT', readme).group(1)
# dev_text = re.search( dev_text = re.search(
# r'(?s)(#\s*DEVELOPER INSTRUCTIONS.*?)#\s*EMBEDDING yt-dlp', r'(?s)(#\s*DEVELOPER INSTRUCTIONS.*?)#\s*EMBEDDING yt-dlp', readme).group(1)
""" readme).group(1)
out = bug_text + dev_text out = bug_text + dev_text
with io.open(outfile, 'w', encoding='utf-8') as outf: with io.open(outfile, 'w', encoding='utf-8') as outf:
outf.write(out) """ outf.write(out)
if __name__ == '__main__': if __name__ == '__main__':
main() main()

View File

@@ -7,11 +7,9 @@ import os
from os.path import dirname as dirn from os.path import dirname as dirn
import sys import sys
print('WARNING: Lazy loading extractors is an experimental feature that may not always work', file=sys.stderr)
sys.path.insert(0, dirn(dirn((os.path.abspath(__file__))))) sys.path.insert(0, dirn(dirn((os.path.abspath(__file__)))))
lazy_extractors_filename = sys.argv[1] lazy_extractors_filename = sys.argv[1] if len(sys.argv) > 1 else 'yt_dlp/extractor/lazy_extractors.py'
if os.path.exists(lazy_extractors_filename): if os.path.exists(lazy_extractors_filename):
os.remove(lazy_extractors_filename) os.remove(lazy_extractors_filename)

View File

@@ -3,11 +3,11 @@
cd /d %~dp0.. cd /d %~dp0..
if ["%~1"]==[""] ( if ["%~1"]==[""] (
set "test_set=" set "test_set="test""
) else if ["%~1"]==["core"] ( ) else if ["%~1"]==["core"] (
set "test_set=-k "not download"" set "test_set="-m not download""
) else if ["%~1"]==["download"] ( ) else if ["%~1"]==["download"] (
set "test_set=-k download" set "test_set="-m "download""
) else ( ) else (
echo.Invalid test type "%~1". Use "core" ^| "download" echo.Invalid test type "%~1". Use "core" ^| "download"
exit /b 1 exit /b 1

View File

@@ -3,12 +3,12 @@
if [ -z $1 ]; then if [ -z $1 ]; then
test_set='test' test_set='test'
elif [ $1 = 'core' ]; then elif [ $1 = 'core' ]; then
test_set='not download' test_set="-m not download"
elif [ $1 = 'download' ]; then elif [ $1 = 'download' ]; then
test_set='download' test_set="-m download"
else else
echo 'Invalid test type "'$1'". Use "core" | "download"' echo 'Invalid test type "'$1'". Use "core" | "download"'
exit 1 exit 1
fi fi
python3 -m pytest -k "$test_set" python3 -m pytest "$test_set"

163
pyinst.py
View File

@@ -1,42 +1,112 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
# coding: utf-8 # coding: utf-8
import os
from __future__ import unicode_literals
import sys
# import os
import platform import platform
import sys
from PyInstaller.utils.hooks import collect_submodules from PyInstaller.utils.hooks import collect_submodules
from PyInstaller.utils.win32.versioninfo import (
OS_NAME = platform.system()
if OS_NAME == 'Windows':
from PyInstaller.utils.win32.versioninfo import (
VarStruct, VarFileInfo, StringStruct, StringTable, VarStruct, VarFileInfo, StringStruct, StringTable,
StringFileInfo, FixedFileInfo, VSVersionInfo, SetVersion, StringFileInfo, FixedFileInfo, VSVersionInfo, SetVersion,
) )
import PyInstaller.__main__ elif OS_NAME == 'Darwin':
pass
else:
raise Exception('{OS_NAME} is not supported')
arch = sys.argv[1] if len(sys.argv) > 1 else platform.architecture()[0][:2] ARCH = platform.architecture()[0][:2]
assert arch in ('32', '64')
print('Building %sbit version' % arch)
_x86 = '_x86' if arch == '32' else ''
FILE_DESCRIPTION = 'yt-dlp%s' % (' (32 Bit)' if _x86 else '')
# root_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), '..')) def main():
# print('Changing working directory to %s' % root_dir) opts = parse_options()
# os.chdir(root_dir) version = read_version()
exec(compile(open('yt_dlp/version.py').read(), 'yt_dlp/version.py', 'exec')) suffix = '_macos' if OS_NAME == 'Darwin' else '_x86' if ARCH == '32' else ''
VERSION = locals()['__version__'] final_file = 'dist/%syt-dlp%s%s' % (
'yt-dlp/' if '--onedir' in opts else '', suffix, '.exe' if OS_NAME == 'Windows' else '')
VERSION_LIST = VERSION.split('.') print(f'Building yt-dlp v{version} {ARCH}bit for {OS_NAME} with options {opts}')
VERSION_LIST = list(map(int, VERSION_LIST)) + [0] * (4 - len(VERSION_LIST)) print('Remember to update the version using "devscripts/update-version.py"')
if not os.path.isfile('yt_dlp/extractor/lazy_extractors.py'):
print('WARNING: Building without lazy_extractors. Run '
'"devscripts/make_lazy_extractors.py" to build lazy extractors', file=sys.stderr)
print(f'Destination: {final_file}\n')
print('Version: %s%s' % (VERSION, _x86)) opts = [
print('Remember to update the version using devscipts\\update-version.py') f'--name=yt-dlp{suffix}',
'--icon=devscripts/logo.ico',
'--upx-exclude=vcruntime140.dll',
'--noconfirm',
*dependancy_options(),
*opts,
'yt_dlp/__main__.py',
]
print(f'Running PyInstaller with {opts}')
VERSION_FILE = VSVersionInfo( import PyInstaller.__main__
PyInstaller.__main__.run(opts)
set_version_info(final_file, version)
def parse_options():
# Compatability with older arguments
opts = sys.argv[1:]
if opts[0:1] in (['32'], ['64']):
if ARCH != opts[0]:
raise Exception(f'{opts[0]}bit executable cannot be built on a {ARCH}bit system')
opts = opts[1:]
return opts or ['--onefile']
def read_version():
exec(compile(open('yt_dlp/version.py').read(), 'yt_dlp/version.py', 'exec'))
return locals()['__version__']
def version_to_list(version):
version_list = version.split('.')
return list(map(int, version_list)) + [0] * (4 - len(version_list))
def dependancy_options():
dependancies = [pycryptodome_module(), 'mutagen'] + collect_submodules('websockets')
excluded_modules = ['test', 'ytdlp_plugins', 'youtube-dl', 'youtube-dlc']
yield from (f'--hidden-import={module}' for module in dependancies)
yield from (f'--exclude-module={module}' for module in excluded_modules)
def pycryptodome_module():
try:
import Cryptodome # noqa: F401
except ImportError:
try:
import Crypto # noqa: F401
print('WARNING: Using Crypto since Cryptodome is not available. '
'Install with: pip install pycryptodomex', file=sys.stderr)
return 'Crypto'
except ImportError:
pass
return 'Cryptodome'
def set_version_info(exe, version):
if OS_NAME == 'Windows':
windows_set_version(exe, version)
def windows_set_version(exe, version):
version_list = version_to_list(version)
suffix = '_x86' if ARCH == '32' else ''
SetVersion(exe, VSVersionInfo(
ffi=FixedFileInfo( ffi=FixedFileInfo(
filevers=VERSION_LIST, filevers=version_list,
prodvers=VERSION_LIST, prodvers=version_list,
mask=0x3F, mask=0x3F,
flags=0x0, flags=0x0,
OS=0x4, OS=0x4,
@@ -45,38 +115,21 @@ VERSION_FILE = VSVersionInfo(
date=(0, 0), date=(0, 0),
), ),
kids=[ kids=[
StringFileInfo([ StringFileInfo([StringTable('040904B0', [
StringTable( StringStruct('Comments', 'yt-dlp%s Command Line Interface.' % suffix),
'040904B0', [
StringStruct('Comments', 'yt-dlp%s Command Line Interface.' % _x86),
StringStruct('CompanyName', 'https://github.com/yt-dlp'), StringStruct('CompanyName', 'https://github.com/yt-dlp'),
StringStruct('FileDescription', FILE_DESCRIPTION), StringStruct('FileDescription', 'yt-dlp%s' % (' (32 Bit)' if ARCH == '32' else '')),
StringStruct('FileVersion', VERSION), StringStruct('FileVersion', version),
StringStruct('InternalName', 'yt-dlp%s' % _x86), StringStruct('InternalName', f'yt-dlp{suffix}'),
StringStruct('LegalCopyright', 'pukkandan.ytdlp@gmail.com | UNLICENSE'),
StringStruct('OriginalFilename', f'yt-dlp{suffix}.exe'),
StringStruct('ProductName', f'yt-dlp{suffix}'),
StringStruct( StringStruct(
'LegalCopyright', 'ProductVersion', f'{version}{suffix} on Python {platform.python_version()}'),
'pukkandan.ytdlp@gmail.com | UNLICENSE', ])]), VarFileInfo([VarStruct('Translation', [0, 1200])])
),
StringStruct('OriginalFilename', 'yt-dlp%s.exe' % _x86),
StringStruct('ProductName', 'yt-dlp%s' % _x86),
StringStruct(
'ProductVersion',
'%s%s on Python %s' % (VERSION, _x86, platform.python_version())),
])]),
VarFileInfo([VarStruct('Translation', [0, 1200])])
] ]
) ))
dependancies = ['Crypto', 'mutagen'] + collect_submodules('websockets')
excluded_modules = ['test', 'ytdlp_plugins', 'youtube-dl', 'youtube-dlc']
PyInstaller.__main__.run([ if __name__ == '__main__':
'--name=yt-dlp%s' % _x86, main()
'--onefile',
'--icon=devscripts/logo.ico',
*[f'--exclude-module={module}' for module in excluded_modules],
*[f'--hidden-import={module}' for module in dependancies],
'--upx-exclude=vcruntime140.dll',
'yt_dlp/__main__.py',
])
SetVersion('dist/yt-dlp%s.exe' % _x86, VERSION_FILE)

View File

@@ -1,3 +1,3 @@
mutagen mutagen
pycryptodome pycryptodomex
websockets websockets

View File

@@ -1,12 +1,16 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
# coding: utf-8 # coding: utf-8
from setuptools import setup, Command, find_packages
import os.path import os.path
import warnings import warnings
import sys import sys
from distutils.spawn import spawn
try:
from setuptools import setup, Command, find_packages
setuptools_available = True
except ImportError:
from distutils.core import setup, Command
setuptools_available = False
from distutils.spawn import spawn
# Get the version from yt_dlp/version.py without importing the package # Get the version from yt_dlp/version.py without importing the package
exec(compile(open('yt_dlp/version.py').read(), 'yt_dlp/version.py', 'exec')) exec(compile(open('yt_dlp/version.py').read(), 'yt_dlp/version.py', 'exec'))
@@ -19,22 +23,48 @@ 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', 'websockets'] REQUIREMENTS = ['mutagen', 'pycryptodomex', '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') import py2exe
warnings.warn(
'py2exe builds do not support pycryptodomex and needs VC++14 to run. '
'The recommended way is to use "pyinst.py" to build using pyinstaller')
params = {
'console': [{
'script': './yt_dlp/__main__.py',
'dest_base': 'yt-dlp',
'version': __version__,
'description': DESCRIPTION,
'comments': LONG_DESCRIPTION.split('\n')[0],
'product_name': 'yt-dlp',
'product_version': __version__,
}],
'options': {
'py2exe': {
'bundle_files': 0,
'compressed': 1,
'optimize': 2,
'dist_dir': './dist',
'excludes': ['Crypto', 'Cryptodome'], # py2exe cannot import Crypto
'dll_excludes': ['w9xpopen.exe', 'crypt32.dll'],
}
},
'zipfile': None
}
else:
files_spec = [ files_spec = [
('share/bash-completion/completions', ['completions/bash/yt-dlp']), ('share/bash-completion/completions', ['completions/bash/yt-dlp']),
('share/zsh/site-functions', ['completions/zsh/_yt-dlp']), ('share/zsh/site-functions', ['completions/zsh/_yt-dlp']),
('share/fish/vendor_completions.d', ['completions/fish/yt-dlp.fish']), ('share/fish/vendor_completions.d', ['completions/fish/yt-dlp.fish']),
('share/doc/yt_dlp', ['README.txt']), ('share/doc/yt_dlp', ['README.txt']),
('share/man/man1', ['yt-dlp.1']) ('share/man/man1', ['yt-dlp.1'])
] ]
root = os.path.dirname(os.path.abspath(__file__)) root = os.path.dirname(os.path.abspath(__file__))
data_files = [] data_files = []
for dirname, files in files_spec: for dirname, files in files_spec:
resfiles = [] resfiles = []
for fn in files: for fn in files:
if not os.path.exists(fn): if not os.path.exists(fn):
@@ -43,10 +73,14 @@ for dirname, files in files_spec:
resfiles.append(fn) resfiles.append(fn)
data_files.append((dirname, resfiles)) data_files.append((dirname, resfiles))
params = { params = {
'data_files': data_files, 'data_files': data_files,
} }
params['entry_points'] = {'console_scripts': ['yt-dlp = yt_dlp:main']}
if setuptools_available:
params['entry_points'] = {'console_scripts': ['yt-dlp = yt_dlp:main']}
else:
params['scripts'] = ['yt-dlp']
class build_lazy_extractors(Command): class build_lazy_extractors(Command):
@@ -64,7 +98,11 @@ class build_lazy_extractors(Command):
dry_run=self.dry_run) dry_run=self.dry_run)
packages = find_packages(exclude=('youtube_dl', 'test', 'ytdlp_plugins')) if setuptools_available:
packages = find_packages(exclude=('youtube_dl', 'youtube_dlc', 'test', 'ytdlp_plugins'))
else:
packages = ['yt_dlp', 'yt_dlp.downloader', 'yt_dlp.extractor', 'yt_dlp.postprocessor']
setup( setup(
name='yt-dlp', name='yt-dlp',
@@ -81,7 +119,7 @@ setup(
'Documentation': 'https://yt-dlp.readthedocs.io', 'Documentation': 'https://yt-dlp.readthedocs.io',
'Source': 'https://github.com/yt-dlp/yt-dlp', 'Source': 'https://github.com/yt-dlp/yt-dlp',
'Tracker': 'https://github.com/yt-dlp/yt-dlp/issues', 'Tracker': 'https://github.com/yt-dlp/yt-dlp/issues',
#'Funding': 'https://donate.pypi.org', 'Funding': 'https://github.com/yt-dlp/yt-dlp/blob/master/Collaborators.md#collaborators',
}, },
classifiers=[ classifiers=[
'Topic :: Multimedia :: Video', 'Topic :: Multimedia :: Video',

View File

@@ -1,4 +1,6 @@
# Supported sites # Supported sites
- **17live**
- **17live:clip**
- **1tv**: Первый канал - **1tv**: Первый канал
- **20min** - **20min**
- **220.ro** - **220.ro**
@@ -50,6 +52,7 @@
- **AmericasTestKitchen** - **AmericasTestKitchen**
- **AmericasTestKitchenSeason** - **AmericasTestKitchenSeason**
- **anderetijden**: npo.nl, ntr.nl, omroepwnl.nl, zapp.nl and npo3.nl - **anderetijden**: npo.nl, ntr.nl, omroepwnl.nl, zapp.nl and npo3.nl
- **AnimalPlanet**
- **AnimeLab** - **AnimeLab**
- **AnimeLabShows** - **AnimeLabShows**
- **AnimeOnDemand** - **AnimeOnDemand**
@@ -125,6 +128,8 @@
- **BilibiliChannel** - **BilibiliChannel**
- **BiliBiliPlayer** - **BiliBiliPlayer**
- **BiliBiliSearch**: Bilibili video search, "bilisearch" keyword - **BiliBiliSearch**: Bilibili video search, "bilisearch" keyword
- **BiliIntl**
- **BiliIntlSeries**
- **BioBioChileTV** - **BioBioChileTV**
- **Biography** - **Biography**
- **BIQLE** - **BIQLE**
@@ -152,6 +157,7 @@
- **BusinessInsider** - **BusinessInsider**
- **BuzzFeed** - **BuzzFeed**
- **BYUtv** - **BYUtv**
- **CAM4**
- **Camdemy** - **Camdemy**
- **CamdemyFolder** - **CamdemyFolder**
- **CamModels** - **CamModels**
@@ -164,10 +170,7 @@
- **CarambaTVPage** - **CarambaTVPage**
- **CartoonNetwork** - **CartoonNetwork**
- **cbc.ca** - **cbc.ca**
- **cbc.ca:olympics**
- **cbc.ca:player** - **cbc.ca:player**
- **cbc.ca:watch**
- **cbc.ca:watch:video**
- **CBS** - **CBS**
- **CBSInteractive** - **CBSInteractive**
- **CBSLocal** - **CBSLocal**
@@ -182,10 +185,13 @@
- **CDA** - **CDA**
- **CeskaTelevize** - **CeskaTelevize**
- **CeskaTelevizePorady** - **CeskaTelevizePorady**
- **CGTN**
- **channel9**: Channel 9 - **channel9**: Channel 9
- **CharlieRose** - **CharlieRose**
- **Chaturbate** - **Chaturbate**
- **Chilloutzone** - **Chilloutzone**
- **Chingari**
- **ChingariUser**
- **chirbit** - **chirbit**
- **chirbit:profile** - **chirbit:profile**
- **cielotv.it** - **cielotv.it**
@@ -193,6 +199,7 @@
- **Cinemax** - **Cinemax**
- **CiscoLiveSearch** - **CiscoLiveSearch**
- **CiscoLiveSession** - **CiscoLiveSession**
- **ciscowebex**: Cisco Webex
- **CJSW** - **CJSW**
- **cliphunter** - **cliphunter**
- **Clippit** - **Clippit**
@@ -219,13 +226,14 @@
- **Crackle** - **Crackle**
- **CrooksAndLiars** - **CrooksAndLiars**
- **crunchyroll** - **crunchyroll**
- **crunchyroll:beta**
- **crunchyroll:playlist** - **crunchyroll:playlist**
- **crunchyroll:playlist:beta**
- **CSpan**: C-SPAN - **CSpan**: C-SPAN
- **CtsNews**: 華視新聞 - **CtsNews**: 華視新聞
- **CTV** - **CTV**
- **CTVNews** - **CTVNews**
- **cu.ntv.co.jp**: Nippon Television Network - **cu.ntv.co.jp**: Nippon Television Network
- **Culturebox**
- **CultureUnplugged** - **CultureUnplugged**
- **curiositystream** - **curiositystream**
- **curiositystream:collection** - **curiositystream:collection**
@@ -235,6 +243,8 @@
- **dailymotion** - **dailymotion**
- **dailymotion:playlist** - **dailymotion:playlist**
- **dailymotion:user** - **dailymotion:user**
- **damtomo:record**
- **damtomo:video**
- **daum.net** - **daum.net**
- **daum.net:clip** - **daum.net:clip**
- **daum.net:playlist** - **daum.net:playlist**
@@ -258,6 +268,7 @@
- **DiscoveryPlusIndiaShow** - **DiscoveryPlusIndiaShow**
- **DiscoveryVR** - **DiscoveryVR**
- **Disney** - **Disney**
- **DIYNetwork**
- **dlive:stream** - **dlive:stream**
- **dlive:vod** - **dlive:vod**
- **DoodStream** - **DoodStream**
@@ -306,6 +317,7 @@
- **ESPNArticle** - **ESPNArticle**
- **EsriVideo** - **EsriVideo**
- **Europa** - **Europa**
- **EUScreen**
- **EWETV** - **EWETV**
- **ExpoTV** - **ExpoTV**
- **Expressen** - **Expressen**
@@ -336,13 +348,10 @@
- **foxnews**: Fox News and Fox Business Video - **foxnews**: Fox News and Fox Business Video
- **foxnews:article** - **foxnews:article**
- **FoxSports** - **FoxSports**
- **france2.fr:generation-what**
- **FranceCulture** - **FranceCulture**
- **FranceInter** - **FranceInter**
- **FranceTV** - **FranceTV**
- **FranceTVEmbed**
- **francetvinfo.fr** - **francetvinfo.fr**
- **FranceTVJeunesse**
- **FranceTVSite** - **FranceTVSite**
- **Freesound** - **Freesound**
- **freespeech.org** - **freespeech.org**
@@ -366,7 +375,11 @@
- **Gazeta** - **Gazeta**
- **GDCVault** - **GDCVault**
- **GediDigital** - **GediDigital**
- **gem.cbc.ca**
- **gem.cbc.ca:live**
- **gem.cbc.ca:playlist**
- **generic**: Generic downloader that works on some sites - **generic**: Generic downloader that works on some sites
- **Gettr**
- **Gfycat** - **Gfycat**
- **GiantBomb** - **GiantBomb**
- **Giga** - **Giga**
@@ -380,8 +393,11 @@
- **google:podcasts** - **google:podcasts**
- **google:podcasts:feed** - **google:podcasts:feed**
- **GoogleDrive** - **GoogleDrive**
- **GoPro**
- **Goshgay** - **Goshgay**
- **GoToStage**
- **GPUTechConf** - **GPUTechConf**
- **Gronkh**
- **Groupon** - **Groupon**
- **hbo** - **hbo**
- **HearThisAt** - **HearThisAt**
@@ -466,6 +482,7 @@
- **KinjaEmbed** - **KinjaEmbed**
- **KinoPoisk** - **KinoPoisk**
- **KonserthusetPlay** - **KonserthusetPlay**
- **Koo**
- **KrasView**: Красвью - **KrasView**: Красвью
- **Ku6** - **Ku6**
- **KUSI** - **KUSI**
@@ -539,6 +556,8 @@
- **MedalTV** - **MedalTV**
- **media.ccc.de** - **media.ccc.de**
- **media.ccc.de:lists** - **media.ccc.de:lists**
- **Mediaite**
- **MediaKlikk**
- **Medialaan** - **Medialaan**
- **Mediaset** - **Mediaset**
- **Mediasite** - **Mediasite**
@@ -555,6 +574,7 @@
- **Mgoon** - **Mgoon**
- **MGTV**: 芒果TV - **MGTV**: 芒果TV
- **MiaoPai** - **MiaoPai**
- **microsoftstream**: Microsoft Stream
- **mildom**: Record ongoing live by specific user in Mildom - **mildom**: Record ongoing live by specific user in Mildom
- **mildom:user:vod**: Download all VODs from specific user in Mildom - **mildom:user:vod**: Download all VODs from specific user in Mildom
- **mildom:vod**: Download a VOD in Mildom - **mildom:vod**: Download a VOD in Mildom
@@ -597,6 +617,7 @@
- **mtvservices:embedded** - **mtvservices:embedded**
- **MTVUutisetArticle** - **MTVUutisetArticle**
- **MuenchenTV**: münchen.tv - **MuenchenTV**: münchen.tv
- **MuseScore**
- **mva**: Microsoft Virtual Academy videos - **mva**: Microsoft Virtual Academy videos
- **mva:course**: Microsoft Virtual Academy courses - **mva:course**: Microsoft Virtual Academy courses
- **Mwave** - **Mwave**
@@ -613,6 +634,8 @@
- **MyviEmbed** - **MyviEmbed**
- **MyVisionTV** - **MyVisionTV**
- **n-tv.de** - **n-tv.de**
- **N1Info:article**
- **N1InfoAsset**
- **natgeo:video** - **natgeo:video**
- **NationalGeographicTV** - **NationalGeographicTV**
- **Naver** - **Naver**
@@ -646,7 +669,8 @@
- **NetPlus** - **NetPlus**
- **Netzkino** - **Netzkino**
- **Newgrounds** - **Newgrounds**
- **NewgroundsPlaylist** - **Newgrounds:playlist**
- **Newgrounds:user**
- **Newstube** - **Newstube**
- **NextMedia**: 蘋果日報 - **NextMedia**: 蘋果日報
- **NextMediaActionNews**: 蘋果日報 - 動新聞 - **NextMediaActionNews**: 蘋果日報 - 動新聞
@@ -682,6 +706,7 @@
- **NosVideo** - **NosVideo**
- **Nova**: TN.cz, Prásk.tv, Nova.cz, Novaplus.cz, FANDA.tv, Krásná.cz and Doma.cz - **Nova**: TN.cz, Prásk.tv, Nova.cz, Novaplus.cz, FANDA.tv, Krásná.cz and Doma.cz
- **NovaEmbed** - **NovaEmbed**
- **NovaPlay**
- **nowness** - **nowness**
- **nowness:playlist** - **nowness:playlist**
- **nowness:series** - **nowness:series**
@@ -707,11 +732,14 @@
- **NYTimes** - **NYTimes**
- **NYTimesArticle** - **NYTimesArticle**
- **NYTimesCooking** - **NYTimesCooking**
- **nzherald**
- **NZZ** - **NZZ**
- **ocw.mit.edu** - **ocw.mit.edu**
- **OdaTV** - **OdaTV**
- **Odnoklassniki** - **Odnoklassniki**
- **OktoberfestTV** - **OktoberfestTV**
- **OlympicsReplay**
- **on24**: ON24
- **OnDemandKorea** - **OnDemandKorea**
- **onet.pl** - **onet.pl**
- **onet.tv** - **onet.tv**
@@ -756,6 +784,7 @@
- **pbs**: Public Broadcasting Service (PBS) and member stations: PBS: Public Broadcasting Service, APT - Alabama Public Television (WBIQ), GPB/Georgia Public Broadcasting (WGTV), Mississippi Public Broadcasting (WMPN), Nashville Public Television (WNPT), WFSU-TV (WFSU), WSRE (WSRE), WTCI (WTCI), WPBA/Channel 30 (WPBA), Alaska Public Media (KAKM), Arizona PBS (KAET), KNME-TV/Channel 5 (KNME), Vegas PBS (KLVX), AETN/ARKANSAS ETV NETWORK (KETS), KET (WKLE), WKNO/Channel 10 (WKNO), LPB/LOUISIANA PUBLIC BROADCASTING (WLPB), OETA (KETA), Ozarks Public Television (KOZK), WSIU Public Broadcasting (WSIU), KEET TV (KEET), KIXE/Channel 9 (KIXE), KPBS San Diego (KPBS), KQED (KQED), KVIE Public Television (KVIE), PBS SoCal/KOCE (KOCE), ValleyPBS (KVPT), CONNECTICUT PUBLIC TELEVISION (WEDH), KNPB Channel 5 (KNPB), SOPTV (KSYS), Rocky Mountain PBS (KRMA), KENW-TV3 (KENW), KUED Channel 7 (KUED), Wyoming PBS (KCWC), Colorado Public Television / KBDI 12 (KBDI), KBYU-TV (KBYU), Thirteen/WNET New York (WNET), WGBH/Channel 2 (WGBH), WGBY (WGBY), NJTV Public Media NJ (WNJT), WLIW21 (WLIW), mpt/Maryland Public Television (WMPB), WETA Television and Radio (WETA), WHYY (WHYY), PBS 39 (WLVT), WVPT - Your Source for PBS and More! (WVPT), Howard University Television (WHUT), WEDU PBS (WEDU), WGCU Public Media (WGCU), WPBT2 (WPBT), WUCF TV (WUCF), WUFT/Channel 5 (WUFT), WXEL/Channel 42 (WXEL), WLRN/Channel 17 (WLRN), WUSF Public Broadcasting (WUSF), ETV (WRLK), UNC-TV (WUNC), PBS Hawaii - Oceanic Cable Channel 10 (KHET), Idaho Public Television (KAID), KSPS (KSPS), OPB (KOPB), KWSU/Channel 10 & KTNW/Channel 31 (KWSU), WILL-TV (WILL), Network Knowledge - WSEC/Springfield (WSEC), WTTW11 (WTTW), Iowa Public Television/IPTV (KDIN), Nine Network (KETC), PBS39 Fort Wayne (WFWA), WFYI Indianapolis (WFYI), Milwaukee Public Television (WMVS), WNIN (WNIN), WNIT Public Television (WNIT), WPT (WPNE), WVUT/Channel 22 (WVUT), WEIU/Channel 51 (WEIU), WQPT-TV (WQPT), WYCC PBS Chicago (WYCC), WIPB-TV (WIPB), WTIU (WTIU), CET (WCET), ThinkTVNetwork (WPTD), WBGU-TV (WBGU), WGVU TV (WGVU), NET1 (KUON), Pioneer Public Television (KWCM), SDPB Television (KUSD), TPT (KTCA), KSMQ (KSMQ), KPTS/Channel 8 (KPTS), KTWU/Channel 11 (KTWU), East Tennessee PBS (WSJK), WCTE-TV (WCTE), WLJT, Channel 11 (WLJT), WOSU TV (WOSU), WOUB/WOUC (WOUB), WVPB (WVPB), WKYU-PBS (WKYU), KERA 13 (KERA), MPBN (WCBB), Mountain Lake PBS (WCFE), NHPTV (WENH), Vermont PBS (WETK), witf (WITF), WQED Multimedia (WQED), WMHT Educational Telecommunications (WMHT), Q-TV (WDCQ), WTVS Detroit Public TV (WTVS), CMU Public Television (WCMU), WKAR-TV (WKAR), WNMU-TV Public TV 13 (WNMU), WDSE - WRPT (WDSE), WGTE TV (WGTE), Lakeland Public Television (KAWE), KMOS-TV - Channels 6.1, 6.2 and 6.3 (KMOS), MontanaPBS (KUSM), KRWG/Channel 22 (KRWG), KACV (KACV), KCOS/Channel 13 (KCOS), WCNY/Channel 24 (WCNY), WNED (WNED), WPBS (WPBS), WSKG Public TV (WSKG), WXXI (WXXI), WPSU (WPSU), WVIA Public Media Studios (WVIA), WTVI (WTVI), Western Reserve PBS (WNEO), WVIZ/PBS ideastream (WVIZ), KCTS 9 (KCTS), Basin PBS (KPBT), KUHT / Channel 8 (KUHT), KLRN (KLRN), KLRU (KLRU), WTJX Channel 12 (WTJX), WCVE PBS (WCVE), KBTC Public Television (KBTC) - **pbs**: Public Broadcasting Service (PBS) and member stations: PBS: Public Broadcasting Service, APT - Alabama Public Television (WBIQ), GPB/Georgia Public Broadcasting (WGTV), Mississippi Public Broadcasting (WMPN), Nashville Public Television (WNPT), WFSU-TV (WFSU), WSRE (WSRE), WTCI (WTCI), WPBA/Channel 30 (WPBA), Alaska Public Media (KAKM), Arizona PBS (KAET), KNME-TV/Channel 5 (KNME), Vegas PBS (KLVX), AETN/ARKANSAS ETV NETWORK (KETS), KET (WKLE), WKNO/Channel 10 (WKNO), LPB/LOUISIANA PUBLIC BROADCASTING (WLPB), OETA (KETA), Ozarks Public Television (KOZK), WSIU Public Broadcasting (WSIU), KEET TV (KEET), KIXE/Channel 9 (KIXE), KPBS San Diego (KPBS), KQED (KQED), KVIE Public Television (KVIE), PBS SoCal/KOCE (KOCE), ValleyPBS (KVPT), CONNECTICUT PUBLIC TELEVISION (WEDH), KNPB Channel 5 (KNPB), SOPTV (KSYS), Rocky Mountain PBS (KRMA), KENW-TV3 (KENW), KUED Channel 7 (KUED), Wyoming PBS (KCWC), Colorado Public Television / KBDI 12 (KBDI), KBYU-TV (KBYU), Thirteen/WNET New York (WNET), WGBH/Channel 2 (WGBH), WGBY (WGBY), NJTV Public Media NJ (WNJT), WLIW21 (WLIW), mpt/Maryland Public Television (WMPB), WETA Television and Radio (WETA), WHYY (WHYY), PBS 39 (WLVT), WVPT - Your Source for PBS and More! (WVPT), Howard University Television (WHUT), WEDU PBS (WEDU), WGCU Public Media (WGCU), WPBT2 (WPBT), WUCF TV (WUCF), WUFT/Channel 5 (WUFT), WXEL/Channel 42 (WXEL), WLRN/Channel 17 (WLRN), WUSF Public Broadcasting (WUSF), ETV (WRLK), UNC-TV (WUNC), PBS Hawaii - Oceanic Cable Channel 10 (KHET), Idaho Public Television (KAID), KSPS (KSPS), OPB (KOPB), KWSU/Channel 10 & KTNW/Channel 31 (KWSU), WILL-TV (WILL), Network Knowledge - WSEC/Springfield (WSEC), WTTW11 (WTTW), Iowa Public Television/IPTV (KDIN), Nine Network (KETC), PBS39 Fort Wayne (WFWA), WFYI Indianapolis (WFYI), Milwaukee Public Television (WMVS), WNIN (WNIN), WNIT Public Television (WNIT), WPT (WPNE), WVUT/Channel 22 (WVUT), WEIU/Channel 51 (WEIU), WQPT-TV (WQPT), WYCC PBS Chicago (WYCC), WIPB-TV (WIPB), WTIU (WTIU), CET (WCET), ThinkTVNetwork (WPTD), WBGU-TV (WBGU), WGVU TV (WGVU), NET1 (KUON), Pioneer Public Television (KWCM), SDPB Television (KUSD), TPT (KTCA), KSMQ (KSMQ), KPTS/Channel 8 (KPTS), KTWU/Channel 11 (KTWU), East Tennessee PBS (WSJK), WCTE-TV (WCTE), WLJT, Channel 11 (WLJT), WOSU TV (WOSU), WOUB/WOUC (WOUB), WVPB (WVPB), WKYU-PBS (WKYU), KERA 13 (KERA), MPBN (WCBB), Mountain Lake PBS (WCFE), NHPTV (WENH), Vermont PBS (WETK), witf (WITF), WQED Multimedia (WQED), WMHT Educational Telecommunications (WMHT), Q-TV (WDCQ), WTVS Detroit Public TV (WTVS), CMU Public Television (WCMU), WKAR-TV (WKAR), WNMU-TV Public TV 13 (WNMU), WDSE - WRPT (WDSE), WGTE TV (WGTE), Lakeland Public Television (KAWE), KMOS-TV - Channels 6.1, 6.2 and 6.3 (KMOS), MontanaPBS (KUSM), KRWG/Channel 22 (KRWG), KACV (KACV), KCOS/Channel 13 (KCOS), WCNY/Channel 24 (WCNY), WNED (WNED), WPBS (WPBS), WSKG Public TV (WSKG), WXXI (WXXI), WPSU (WPSU), WVIA Public Media Studios (WVIA), WTVI (WTVI), Western Reserve PBS (WNEO), WVIZ/PBS ideastream (WVIZ), KCTS 9 (KCTS), Basin PBS (KPBT), KUHT / Channel 8 (KUHT), KLRN (KLRN), KLRU (KLRU), WTJX Channel 12 (WTJX), WCVE PBS (WCVE), KBTC Public Television (KBTC)
- **PearVideo** - **PearVideo**
- **PeerTube** - **PeerTube**
- **PeerTube:Playlist**
- **peloton** - **peloton**
- **peloton:live**: Peloton Live - **peloton:live**: Peloton Live
- **People** - **People**
@@ -831,6 +860,9 @@
- **radiocanada:audiovideo** - **radiocanada:audiovideo**
- **radiofrance** - **radiofrance**
- **RadioJavan** - **RadioJavan**
- **radlive**
- **radlive:channel**
- **radlive:season**
- **Rai** - **Rai**
- **RaiPlay** - **RaiPlay**
- **RaiPlayLive** - **RaiPlayLive**
@@ -881,6 +913,7 @@
- **RTVNH** - **RTVNH**
- **RTVS** - **RTVS**
- **RUHD** - **RUHD**
- **RumbleChannel**
- **RumbleEmbed** - **RumbleEmbed**
- **rutube**: Rutube videos - **rutube**: Rutube videos
- **rutube:channel**: Rutube channels - **rutube:channel**: Rutube channels
@@ -934,6 +967,7 @@
- **SkylineWebcams** - **SkylineWebcams**
- **skynewsarabia:article** - **skynewsarabia:article**
- **skynewsarabia:video** - **skynewsarabia:video**
- **SkyNewsAU**
- **Slideshare** - **Slideshare**
- **SlidesLive** - **SlidesLive**
- **Slutload** - **Slutload**
@@ -943,7 +977,7 @@
- **SonyLIVSeries** - **SonyLIVSeries**
- **soundcloud** - **soundcloud**
- **soundcloud:playlist** - **soundcloud:playlist**
- **soundcloud:search**: Soundcloud search - **soundcloud:search**: Soundcloud search, "scsearch" keyword
- **soundcloud:set** - **soundcloud:set**
- **soundcloud:trackstation** - **soundcloud:trackstation**
- **soundcloud:user** - **soundcloud:user**
@@ -955,11 +989,12 @@
- **southpark.de** - **southpark.de**
- **southpark.nl** - **southpark.nl**
- **southparkstudios.dk** - **southparkstudios.dk**
- **SovietsCloset**
- **SovietsClosetPlaylist**
- **SpankBang** - **SpankBang**
- **SpankBangPlaylist** - **SpankBangPlaylist**
- **Spankwire** - **Spankwire**
- **Spiegel** - **Spiegel**
- **sport.francetvinfo.fr**
- **Sport5** - **Sport5**
- **SportBox** - **SportBox**
- **SportDeutschland** - **SportDeutschland**
@@ -983,6 +1018,7 @@
- **StoryFireSeries** - **StoryFireSeries**
- **StoryFireUser** - **StoryFireUser**
- **Streamable** - **Streamable**
- **Streamanity**
- **streamcloud.eu** - **streamcloud.eu**
- **StreamCZ** - **StreamCZ**
- **StreetVoice** - **StreetVoice**
@@ -1000,7 +1036,6 @@
- **SztvHu** - **SztvHu**
- **t-online.de** - **t-online.de**
- **Tagesschau** - **Tagesschau**
- **tagesschau:player**
- **Tass** - **Tass**
- **TBS** - **TBS**
- **TDSLifeway** - **TDSLifeway**
@@ -1038,6 +1073,8 @@
- **TheScene** - **TheScene**
- **TheStar** - **TheStar**
- **TheSun** - **TheSun**
- **ThetaStream**
- **ThetaVideo**
- **TheWeatherChannel** - **TheWeatherChannel**
- **ThisAmericanLife** - **ThisAmericanLife**
- **ThisAV** - **ThisAV**
@@ -1058,6 +1095,8 @@
- **TrailerAddict** (Currently broken) - **TrailerAddict** (Currently broken)
- **Trilulilu** - **Trilulilu**
- **Trovo** - **Trovo**
- **TrovoChannelClip**: All Clips of a trovo.live channel, "trovoclip" keyword
- **TrovoChannelVod**: All VODs of a trovo.live channel, "trovovod" keyword
- **TrovoVod** - **TrovoVod**
- **TruNews** - **TruNews**
- **TruTV** - **TruTV**
@@ -1146,6 +1185,7 @@
- **Varzesh3** - **Varzesh3**
- **Vbox7** - **Vbox7**
- **VeeHD** - **VeeHD**
- **Veo**
- **Veoh** - **Veoh**
- **Vesti**: Вести.Ru - **Vesti**: Вести.Ru
- **Vevo** - **Vevo**
@@ -1161,7 +1201,7 @@
- **Viddler** - **Viddler**
- **Videa** - **Videa**
- **video.arnes.si**: Arnes Video - **video.arnes.si**: Arnes Video
- **video.google:search**: Google Video search - **video.google:search**: Google Video search (Currently broken)
- **video.sky.it** - **video.sky.it**
- **video.sky.it:live** - **video.sky.it:live**
- **VideoDetective** - **VideoDetective**
@@ -1174,9 +1214,6 @@
- **VidioLive** - **VidioLive**
- **VidioPremier** - **VidioPremier**
- **VidLii** - **VidLii**
- **vidme**
- **vidme:user**
- **vidme:user:likes**
- **vier**: vier.be and vijf.be - **vier**: vier.be and vijf.be
- **vier:videos** - **vier:videos**
- **viewlift** - **viewlift**
@@ -1228,6 +1265,7 @@
- **VTXTV** - **VTXTV**
- **vube**: Vube.com - **vube**: Vube.com
- **VuClip** - **VuClip**
- **Vupload**
- **VVVVID** - **VVVVID**
- **VVVVIDShow** - **VVVVIDShow**
- **VyboryMos** - **VyboryMos**
@@ -1325,6 +1363,8 @@
- **ZDFChannel** - **ZDFChannel**
- **Zee5** - **Zee5**
- **zee5:series** - **zee5:series**
- **ZenYandex**
- **ZenYandexChannel**
- **Zhihu** - **Zhihu**
- **zingmp3**: mp3.zing.vn - **zingmp3**: mp3.zing.vn
- **zingmp3:album** - **zingmp3:album**

View File

@@ -22,7 +22,7 @@ from yt_dlp.utils import (
) )
if "pytest" in sys.modules: if 'pytest' in sys.modules:
import pytest import pytest
is_download_test = pytest.mark.download is_download_test = pytest.mark.download
else: else:
@@ -32,9 +32,9 @@ else:
def get_params(override=None): def get_params(override=None):
PARAMETERS_FILE = os.path.join(os.path.dirname(os.path.abspath(__file__)), PARAMETERS_FILE = os.path.join(os.path.dirname(os.path.abspath(__file__)),
"parameters.json") 'parameters.json')
LOCAL_PARAMETERS_FILE = os.path.join(os.path.dirname(os.path.abspath(__file__)), LOCAL_PARAMETERS_FILE = os.path.join(os.path.dirname(os.path.abspath(__file__)),
"local_parameters.json") 'local_parameters.json')
with io.open(PARAMETERS_FILE, encoding='utf-8') as pf: with io.open(PARAMETERS_FILE, encoding='utf-8') as pf:
parameters = json.load(pf) parameters = json.load(pf)
if os.path.exists(LOCAL_PARAMETERS_FILE): if os.path.exists(LOCAL_PARAMETERS_FILE):

View File

@@ -44,6 +44,5 @@
"writesubtitles": false, "writesubtitles": false,
"allsubtitles": false, "allsubtitles": false,
"listsubtitles": false, "listsubtitles": false,
"socket_timeout": 20,
"fixup": "never" "fixup": "never"
} }

View File

@@ -649,9 +649,11 @@ class TestYoutubeDL(unittest.TestCase):
'title2': '%PATH%', 'title2': '%PATH%',
'title3': 'foo/bar\\test', 'title3': 'foo/bar\\test',
'title4': 'foo "bar" test', 'title4': 'foo "bar" test',
'title5': 'áéí 𝐀',
'timestamp': 1618488000, 'timestamp': 1618488000,
'duration': 100000, 'duration': 100000,
'playlist_index': 1, 'playlist_index': 1,
'playlist_autonumber': 2,
'_last_playlist_index': 100, '_last_playlist_index': 100,
'n_entries': 10, 'n_entries': 10,
'formats': [{'id': 'id1'}, {'id': 'id2'}, {'id': 'id3'}] 'formats': [{'id': 'id1'}, {'id': 'id2'}, {'id': 'id3'}]
@@ -664,8 +666,7 @@ class TestYoutubeDL(unittest.TestCase):
ydl._num_downloads = 1 ydl._num_downloads = 1
self.assertEqual(ydl.validate_outtmpl(tmpl), None) self.assertEqual(ydl.validate_outtmpl(tmpl), None)
outtmpl, tmpl_dict = ydl.prepare_outtmpl(tmpl, info or self.outtmpl_info) out = ydl.evaluate_outtmpl(tmpl, info or self.outtmpl_info)
out = ydl.escape_outtmpl(outtmpl) % tmpl_dict
fname = ydl.prepare_filename(info or self.outtmpl_info) fname = ydl.prepare_filename(info or self.outtmpl_info)
if not isinstance(expected, (list, tuple)): if not isinstance(expected, (list, tuple)):
@@ -689,6 +690,7 @@ class TestYoutubeDL(unittest.TestCase):
test('%(duration_string)s', ('27:46:40', '27-46-40')) test('%(duration_string)s', ('27:46:40', '27-46-40'))
test('%(resolution)s', '1080p') test('%(resolution)s', '1080p')
test('%(playlist_index)s', '001') test('%(playlist_index)s', '001')
test('%(playlist_autonumber)s', '02')
test('%(autonumber)s', '00001') test('%(autonumber)s', '00001')
test('%(autonumber+2)03d', '005', autonumber_start=3) test('%(autonumber+2)03d', '005', autonumber_start=3)
test('%(autonumber)s', '001', autonumber_size=3) test('%(autonumber)s', '001', autonumber_size=3)
@@ -764,9 +766,15 @@ class TestYoutubeDL(unittest.TestCase):
# Custom type casting # Custom type casting
test('%(formats.:.id)l', 'id1, id2, id3') test('%(formats.:.id)l', 'id1, id2, id3')
test('%(formats.:.id)#l', ('id1\nid2\nid3', 'id1 id2 id3'))
test('%(ext)l', 'mp4') test('%(ext)l', 'mp4')
test('%(formats.:.id) 15l', ' id1, id2, id3') test('%(formats.:.id) 15l', ' id1, id2, id3')
test('%(formats)j', (json.dumps(FORMATS), sanitize(json.dumps(FORMATS)))) test('%(formats)j', (json.dumps(FORMATS), sanitize(json.dumps(FORMATS))))
test('%(title5).3B', 'á')
test('%(title5)U', 'áéí 𝐀')
test('%(title5)#U', 'a\u0301e\u0301i\u0301 𝐀')
test('%(title5)+U', 'áéí A')
test('%(title5)+#U', 'a\u0301e\u0301i\u0301 A')
if compat_os_name == 'nt': if compat_os_name == 'nt':
test('%(title4)q', ('"foo \\"bar\\" test"', "'foo _'bar_' test'")) test('%(title4)q', ('"foo \\"bar\\" test"', "'foo _'bar_' test'"))
else: else:
@@ -788,6 +796,12 @@ class TestYoutubeDL(unittest.TestCase):
test('%(formats.0.id.-1+id)f', '1235.000000') test('%(formats.0.id.-1+id)f', '1235.000000')
test('%(formats.0.id.-1+formats.1.id.-1)d', '3') test('%(formats.0.id.-1+formats.1.id.-1)d', '3')
# Alternates
test('%(title,id)s', '1234')
test('%(width-100,height+20|def)d', '1100')
test('%(width-100,height+width|def)s', 'def')
test('%(timestamp-x>%H\\,%M\\,%S,timestamp>%H\\,%M\\,%S)s', '12,00,00')
# Laziness # Laziness
def gen(): def gen():
yield from range(5) yield from range(5)
@@ -803,6 +817,12 @@ class TestYoutubeDL(unittest.TestCase):
compat_setenv('__yt_dlp_var', 'expanded') compat_setenv('__yt_dlp_var', 'expanded')
envvar = '%__yt_dlp_var%' if compat_os_name == 'nt' else '$__yt_dlp_var' envvar = '%__yt_dlp_var%' if compat_os_name == 'nt' else '$__yt_dlp_var'
test(envvar, (envvar, 'expanded')) test(envvar, (envvar, 'expanded'))
if compat_os_name == 'nt':
test('%s%', ('%s%', '%s%'))
compat_setenv('s', 'expanded')
test('%s%', ('%s%', 'expanded')) # %s% should be expanded before escaping %s
compat_setenv('(test)s', 'expanded')
test('%(test)s%', ('NA%', 'expanded')) # Environment should take priority over template
# Path expansion and escaping # Path expansion and escaping
test('Hello %(title1)s', 'Hello $PATH') test('Hello %(title1)s', 'Hello $PATH')
@@ -992,6 +1012,7 @@ class TestYoutubeDL(unittest.TestCase):
test_selection({'playlist_items': '2-4'}, [2, 3, 4]) test_selection({'playlist_items': '2-4'}, [2, 3, 4])
test_selection({'playlist_items': '2,4'}, [2, 4]) test_selection({'playlist_items': '2,4'}, [2, 4])
test_selection({'playlist_items': '10'}, []) test_selection({'playlist_items': '10'}, [])
test_selection({'playlist_items': '0'}, [])
# Tests for https://github.com/ytdl-org/youtube-dl/issues/10591 # Tests for https://github.com/ytdl-org/youtube-dl/issues/10591
test_selection({'playlist_items': '2-4,3-4,3'}, [2, 3, 4]) test_selection({'playlist_items': '2-4,3-4,3'}, [2, 3, 4])

View File

@@ -7,7 +7,19 @@ 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.aes import aes_decrypt, aes_encrypt, aes_cbc_decrypt, aes_cbc_encrypt, aes_decrypt_text from yt_dlp.aes import (
aes_decrypt,
aes_encrypt,
aes_cbc_decrypt,
aes_cbc_decrypt_bytes,
aes_cbc_encrypt,
aes_ctr_decrypt,
aes_ctr_encrypt,
aes_gcm_decrypt_and_verify,
aes_gcm_decrypt_and_verify_bytes,
aes_decrypt_text
)
from yt_dlp.compat import compat_pycrypto_AES
from yt_dlp.utils import bytes_to_intlist, intlist_to_bytes from yt_dlp.utils import bytes_to_intlist, intlist_to_bytes
import base64 import base64
@@ -27,10 +39,11 @@ class TestAES(unittest.TestCase):
self.assertEqual(decrypted, msg) self.assertEqual(decrypted, msg)
def test_cbc_decrypt(self): def test_cbc_decrypt(self):
data = bytes_to_intlist( data = b'\x97\x92+\xe5\x0b\xc3\x18\x91ky9m&\xb3\xb5@\xe6\x27\xc2\x96.\xc8u\x88\xab9-[\x9e|\xf1\xcd'
b"\x97\x92+\xe5\x0b\xc3\x18\x91ky9m&\xb3\xb5@\xe6'\xc2\x96.\xc8u\x88\xab9-[\x9e|\xf1\xcd" decrypted = intlist_to_bytes(aes_cbc_decrypt(bytes_to_intlist(data), self.key, self.iv))
) self.assertEqual(decrypted.rstrip(b'\x08'), self.secret_msg)
decrypted = intlist_to_bytes(aes_cbc_decrypt(data, self.key, self.iv)) if compat_pycrypto_AES:
decrypted = aes_cbc_decrypt_bytes(data, intlist_to_bytes(self.key), intlist_to_bytes(self.iv))
self.assertEqual(decrypted.rstrip(b'\x08'), self.secret_msg) self.assertEqual(decrypted.rstrip(b'\x08'), self.secret_msg)
def test_cbc_encrypt(self): def test_cbc_encrypt(self):
@@ -38,7 +51,31 @@ class TestAES(unittest.TestCase):
encrypted = intlist_to_bytes(aes_cbc_encrypt(data, self.key, self.iv)) encrypted = intlist_to_bytes(aes_cbc_encrypt(data, self.key, self.iv))
self.assertEqual( self.assertEqual(
encrypted, encrypted,
b"\x97\x92+\xe5\x0b\xc3\x18\x91ky9m&\xb3\xb5@\xe6'\xc2\x96.\xc8u\x88\xab9-[\x9e|\xf1\xcd") b'\x97\x92+\xe5\x0b\xc3\x18\x91ky9m&\xb3\xb5@\xe6\'\xc2\x96.\xc8u\x88\xab9-[\x9e|\xf1\xcd')
def test_ctr_decrypt(self):
data = bytes_to_intlist(b'\x03\xc7\xdd\xd4\x8e\xb3\xbc\x1a*O\xdc1\x12+8Aio\xd1z\xb5#\xaf\x08')
decrypted = intlist_to_bytes(aes_ctr_decrypt(data, self.key, self.iv))
self.assertEqual(decrypted.rstrip(b'\x08'), self.secret_msg)
def test_ctr_encrypt(self):
data = bytes_to_intlist(self.secret_msg)
encrypted = intlist_to_bytes(aes_ctr_encrypt(data, self.key, self.iv))
self.assertEqual(
encrypted,
b'\x03\xc7\xdd\xd4\x8e\xb3\xbc\x1a*O\xdc1\x12+8Aio\xd1z\xb5#\xaf\x08')
def test_gcm_decrypt(self):
data = b'\x159Y\xcf5eud\x90\x9c\x85&]\x14\x1d\x0f.\x08\xb4T\xe4/\x17\xbd'
authentication_tag = b'\xe8&I\x80rI\x07\x9d}YWuU@:e'
decrypted = intlist_to_bytes(aes_gcm_decrypt_and_verify(
bytes_to_intlist(data), self.key, bytes_to_intlist(authentication_tag), self.iv[:12]))
self.assertEqual(decrypted.rstrip(b'\x08'), self.secret_msg)
if compat_pycrypto_AES:
decrypted = aes_gcm_decrypt_and_verify_bytes(
data, intlist_to_bytes(self.key), authentication_tag, intlist_to_bytes(self.iv[:12]))
self.assertEqual(decrypted.rstrip(b'\x08'), self.secret_msg)
def test_decrypt_text(self): def test_decrypt_text(self):
password = intlist_to_bytes(self.key).decode('utf-8') password = intlist_to_bytes(self.key).decode('utf-8')

View File

@@ -3,16 +3,28 @@ from datetime import datetime, timezone
from yt_dlp import cookies from yt_dlp import cookies
from yt_dlp.cookies import ( from yt_dlp.cookies import (
CRYPTO_AVAILABLE,
LinuxChromeCookieDecryptor, LinuxChromeCookieDecryptor,
MacChromeCookieDecryptor, MacChromeCookieDecryptor,
WindowsChromeCookieDecryptor, WindowsChromeCookieDecryptor,
YDLLogger,
parse_safari_cookies, parse_safari_cookies,
pbkdf2_sha1, pbkdf2_sha1,
) )
class Logger:
def debug(self, message):
print(f'[verbose] {message}')
def info(self, message):
print(message)
def warning(self, message, only_once=False):
self.error(message)
def error(self, message):
raise Exception(message)
class MonkeyPatch: class MonkeyPatch:
def __init__(self, module, temporary_values): def __init__(self, module, temporary_values):
self._module = module self._module = module
@@ -42,7 +54,7 @@ class TestCookies(unittest.TestCase):
with MonkeyPatch(cookies, {'_get_linux_keyring_password': lambda *args, **kwargs: b''}): with MonkeyPatch(cookies, {'_get_linux_keyring_password': lambda *args, **kwargs: b''}):
encrypted_value = b'v10\xccW%\xcd\xe6\xe6\x9fM" \xa7\xb0\xca\xe4\x07\xd6' encrypted_value = b'v10\xccW%\xcd\xe6\xe6\x9fM" \xa7\xb0\xca\xe4\x07\xd6'
value = 'USD' value = 'USD'
decryptor = LinuxChromeCookieDecryptor('Chrome', YDLLogger()) decryptor = LinuxChromeCookieDecryptor('Chrome', Logger())
self.assertEqual(decryptor.decrypt(encrypted_value), value) self.assertEqual(decryptor.decrypt(encrypted_value), value)
def test_chrome_cookie_decryptor_linux_v11(self): def test_chrome_cookie_decryptor_linux_v11(self):
@@ -50,24 +62,23 @@ class TestCookies(unittest.TestCase):
'KEYRING_AVAILABLE': True}): 'KEYRING_AVAILABLE': True}):
encrypted_value = b'v11#\x81\x10>`w\x8f)\xc0\xb2\xc1\r\xf4\x1al\xdd\x93\xfd\xf8\xf8N\xf2\xa9\x83\xf1\xe9o\x0elVQd' encrypted_value = b'v11#\x81\x10>`w\x8f)\xc0\xb2\xc1\r\xf4\x1al\xdd\x93\xfd\xf8\xf8N\xf2\xa9\x83\xf1\xe9o\x0elVQd'
value = 'tz=Europe.London' value = 'tz=Europe.London'
decryptor = LinuxChromeCookieDecryptor('Chrome', YDLLogger()) decryptor = LinuxChromeCookieDecryptor('Chrome', Logger())
self.assertEqual(decryptor.decrypt(encrypted_value), value) self.assertEqual(decryptor.decrypt(encrypted_value), value)
@unittest.skipIf(not CRYPTO_AVAILABLE, 'cryptography library not available')
def test_chrome_cookie_decryptor_windows_v10(self): def test_chrome_cookie_decryptor_windows_v10(self):
with MonkeyPatch(cookies, { with MonkeyPatch(cookies, {
'_get_windows_v10_key': lambda *args, **kwargs: b'Y\xef\xad\xad\xeerp\xf0Y\xe6\x9b\x12\xc2<z\x16]\n\xbb\xb8\xcb\xd7\x9bA\xc3\x14e\x99{\xd6\xf4&' '_get_windows_v10_key': lambda *args, **kwargs: b'Y\xef\xad\xad\xeerp\xf0Y\xe6\x9b\x12\xc2<z\x16]\n\xbb\xb8\xcb\xd7\x9bA\xc3\x14e\x99{\xd6\xf4&'
}): }):
encrypted_value = b'v10T\xb8\xf3\xb8\x01\xa7TtcV\xfc\x88\xb8\xb8\xef\x05\xb5\xfd\x18\xc90\x009\xab\xb1\x893\x85)\x87\xe1\xa9-\xa3\xad=' encrypted_value = b'v10T\xb8\xf3\xb8\x01\xa7TtcV\xfc\x88\xb8\xb8\xef\x05\xb5\xfd\x18\xc90\x009\xab\xb1\x893\x85)\x87\xe1\xa9-\xa3\xad='
value = '32101439' value = '32101439'
decryptor = WindowsChromeCookieDecryptor('', YDLLogger()) decryptor = WindowsChromeCookieDecryptor('', Logger())
self.assertEqual(decryptor.decrypt(encrypted_value), value) self.assertEqual(decryptor.decrypt(encrypted_value), value)
def test_chrome_cookie_decryptor_mac_v10(self): def test_chrome_cookie_decryptor_mac_v10(self):
with MonkeyPatch(cookies, {'_get_mac_keyring_password': lambda *args, **kwargs: b'6eIDUdtKAacvlHwBVwvg/Q=='}): with MonkeyPatch(cookies, {'_get_mac_keyring_password': lambda *args, **kwargs: b'6eIDUdtKAacvlHwBVwvg/Q=='}):
encrypted_value = b'v10\xb3\xbe\xad\xa1[\x9fC\xa1\x98\xe0\x9a\x01\xd9\xcf\xbfc' encrypted_value = b'v10\xb3\xbe\xad\xa1[\x9fC\xa1\x98\xe0\x9a\x01\xd9\xcf\xbfc'
value = '2021-06-01-22' value = '2021-06-01-22'
decryptor = MacChromeCookieDecryptor('', YDLLogger()) decryptor = MacChromeCookieDecryptor('', Logger())
self.assertEqual(decryptor.decrypt(encrypted_value), value) self.assertEqual(decryptor.decrypt(encrypted_value), value)
def test_safari_cookie_parsing(self): def test_safari_cookie_parsing(self):

View File

@@ -461,11 +461,23 @@ class TestModifyChaptersPP(unittest.TestCase):
self._remove_marked_arrange_sponsors_test_impl( self._remove_marked_arrange_sponsors_test_impl(
chapters, self._chapters([2, 2.5], ['c1', 'c3']), cuts) chapters, self._chapters([2, 2.5], ['c1', 'c3']), cuts)
def test_remove_marked_arrange_sponsors_SingleTinyChapterIsPreserved(self):
cuts = [self._chapter(0.5, 2, remove=True)]
chapters = self._chapters([2], ['c']) + cuts
self._remove_marked_arrange_sponsors_test_impl(
chapters, self._chapters([0.5], ['c']), cuts)
def test_remove_marked_arrange_sponsors_TinyChapterAtTheStartPrependedToTheNext(self):
cuts = [self._chapter(0.5, 2, remove=True)]
chapters = self._chapters([2, 4], ['c1', 'c2']) + cuts
self._remove_marked_arrange_sponsors_test_impl(
chapters, self._chapters([2.5], ['c2']), cuts)
def test_remove_marked_arrange_sponsors_TinyChaptersResultingFromSponsorOverlapAreIgnored(self): def test_remove_marked_arrange_sponsors_TinyChaptersResultingFromSponsorOverlapAreIgnored(self):
chapters = self._chapters([1, 3, 4], ['c1', 'c2', 'c3']) + [ chapters = self._chapters([1, 3, 4], ['c1', 'c2', 'c3']) + [
self._sponsor_chapter(1.5, 2.5, 'sponsor')] self._sponsor_chapter(1.5, 2.5, 'sponsor')]
self._remove_marked_arrange_sponsors_test_impl( self._remove_marked_arrange_sponsors_test_impl(
chapters, self._chapters([1.5, 3, 4], ['c1', '[SponsorBlock]: Sponsor', 'c3']), []) chapters, self._chapters([1.5, 2.5, 4], ['c1', '[SponsorBlock]: Sponsor', 'c3']), [])
def test_remove_marked_arrange_sponsors_TinySponsorsOverlapsAreIgnored(self): def test_remove_marked_arrange_sponsors_TinySponsorsOverlapsAreIgnored(self):
chapters = self._chapters([2, 3, 5], ['c1', 'c2', 'c3']) + [ chapters = self._chapters([2, 3, 5], ['c1', 'c2', 'c3']) + [
@@ -476,6 +488,26 @@ class TestModifyChaptersPP(unittest.TestCase):
chapters, self._chapters([1, 3, 4, 5], [ chapters, self._chapters([1, 3, 4, 5], [
'c1', '[SponsorBlock]: Sponsor', '[SponsorBlock]: Unpaid/Self Promotion', 'c3']), []) 'c1', '[SponsorBlock]: Sponsor', '[SponsorBlock]: Unpaid/Self Promotion', 'c3']), [])
def test_remove_marked_arrange_sponsors_TinySponsorsPrependedToTheNextSponsor(self):
chapters = self._chapters([4], ['c']) + [
self._sponsor_chapter(1.5, 2, 'sponsor'),
self._sponsor_chapter(2, 4, 'selfpromo')
]
self._remove_marked_arrange_sponsors_test_impl(
chapters, self._chapters([1.5, 4], ['c', '[SponsorBlock]: Unpaid/Self Promotion']), [])
def test_remove_marked_arrange_sponsors_SmallestSponsorInTheOverlapGetsNamed(self):
self._pp._sponsorblock_chapter_title = '[SponsorBlock]: %(name)s'
chapters = self._chapters([10], ['c']) + [
self._sponsor_chapter(2, 8, 'sponsor'),
self._sponsor_chapter(4, 6, 'selfpromo')
]
self._remove_marked_arrange_sponsors_test_impl(
chapters, self._chapters([2, 4, 6, 8, 10], [
'c', '[SponsorBlock]: Sponsor', '[SponsorBlock]: Unpaid/Self Promotion',
'[SponsorBlock]: Sponsor', 'c'
]), [])
def test_make_concat_opts_CommonCase(self): def test_make_concat_opts_CommonCase(self):
sponsor_chapters = [self._chapter(1, 2, 's1'), self._chapter(10, 20, 's2')] sponsor_chapters = [self._chapter(1, 2, 's1'), self._chapter(10, 20, 's2')]
expected = '''ffconcat version 1.0 expected = '''ffconcat version 1.0

View File

@@ -19,6 +19,7 @@ from yt_dlp.extractor import (
CeskaTelevizeIE, CeskaTelevizeIE,
LyndaIE, LyndaIE,
NPOIE, NPOIE,
PBSIE,
ComedyCentralIE, ComedyCentralIE,
NRKTVIE, NRKTVIE,
RaiPlayIE, RaiPlayIE,
@@ -372,5 +373,42 @@ class TestDemocracynowSubtitles(BaseTestSubtitles):
self.assertEqual(md5(subtitles['en']), 'acaca989e24a9e45a6719c9b3d60815c') self.assertEqual(md5(subtitles['en']), 'acaca989e24a9e45a6719c9b3d60815c')
@is_download_test
class TestPBSSubtitles(BaseTestSubtitles):
url = 'https://www.pbs.org/video/how-fantasy-reflects-our-world-picecq/'
IE = PBSIE
def test_allsubtitles(self):
self.DL.params['writesubtitles'] = True
self.DL.params['allsubtitles'] = True
subtitles = self.getSubtitles()
self.assertEqual(set(subtitles.keys()), set(['en']))
def test_subtitles_dfxp_format(self):
self.DL.params['writesubtitles'] = True
self.DL.params['subtitlesformat'] = 'dfxp'
subtitles = self.getSubtitles()
self.assertIn(md5(subtitles['en']), ['643b034254cdc3768ff1e750b6b5873b'])
def test_subtitles_vtt_format(self):
self.DL.params['writesubtitles'] = True
self.DL.params['subtitlesformat'] = 'vtt'
subtitles = self.getSubtitles()
self.assertIn(
md5(subtitles['en']), ['937a05711555b165d4c55a9667017045', 'f49ea998d6824d94959c8152a368ff73'])
def test_subtitles_srt_format(self):
self.DL.params['writesubtitles'] = True
self.DL.params['subtitlesformat'] = 'srt'
subtitles = self.getSubtitles()
self.assertIn(md5(subtitles['en']), ['2082c21b43759d9bf172931b2f2ca371'])
def test_subtitles_sami_format(self):
self.DL.params['writesubtitles'] = True
self.DL.params['subtitlesformat'] = 'sami'
subtitles = self.getSubtitles()
self.assertIn(md5(subtitles['en']), ['4256b16ac7da6a6780fafd04294e85cd'])
if __name__ == '__main__': if __name__ == '__main__':
unittest.main() unittest.main()

View File

@@ -848,30 +848,52 @@ class TestUtil(unittest.TestCase):
self.assertEqual(parse_codecs('avc1.77.30, mp4a.40.2'), { self.assertEqual(parse_codecs('avc1.77.30, mp4a.40.2'), {
'vcodec': 'avc1.77.30', 'vcodec': 'avc1.77.30',
'acodec': 'mp4a.40.2', 'acodec': 'mp4a.40.2',
'dynamic_range': None,
}) })
self.assertEqual(parse_codecs('mp4a.40.2'), { self.assertEqual(parse_codecs('mp4a.40.2'), {
'vcodec': 'none', 'vcodec': 'none',
'acodec': 'mp4a.40.2', 'acodec': 'mp4a.40.2',
'dynamic_range': None,
}) })
self.assertEqual(parse_codecs('mp4a.40.5,avc1.42001e'), { self.assertEqual(parse_codecs('mp4a.40.5,avc1.42001e'), {
'vcodec': 'avc1.42001e', 'vcodec': 'avc1.42001e',
'acodec': 'mp4a.40.5', 'acodec': 'mp4a.40.5',
'dynamic_range': None,
}) })
self.assertEqual(parse_codecs('avc3.640028'), { self.assertEqual(parse_codecs('avc3.640028'), {
'vcodec': 'avc3.640028', 'vcodec': 'avc3.640028',
'acodec': 'none', 'acodec': 'none',
'dynamic_range': None,
}) })
self.assertEqual(parse_codecs(', h264,,newcodec,aac'), { self.assertEqual(parse_codecs(', h264,,newcodec,aac'), {
'vcodec': 'h264', 'vcodec': 'h264',
'acodec': 'aac', 'acodec': 'aac',
'dynamic_range': None,
}) })
self.assertEqual(parse_codecs('av01.0.05M.08'), { self.assertEqual(parse_codecs('av01.0.05M.08'), {
'vcodec': 'av01.0.05M.08', 'vcodec': 'av01.0.05M.08',
'acodec': 'none', 'acodec': 'none',
'dynamic_range': None,
})
self.assertEqual(parse_codecs('vp9.2'), {
'vcodec': 'vp9.2',
'acodec': 'none',
'dynamic_range': 'HDR10',
})
self.assertEqual(parse_codecs('av01.0.12M.10.0.110.09.16.09.0'), {
'vcodec': 'av01.0.12M.10',
'acodec': 'none',
'dynamic_range': 'HDR10',
})
self.assertEqual(parse_codecs('dvhe'), {
'vcodec': 'dvhe',
'acodec': 'none',
'dynamic_range': 'DV',
}) })
self.assertEqual(parse_codecs('theora, vorbis'), { self.assertEqual(parse_codecs('theora, vorbis'), {
'vcodec': 'theora', 'vcodec': 'theora',
'acodec': 'vorbis', 'acodec': 'vorbis',
'dynamic_range': None,
}) })
self.assertEqual(parse_codecs('unknownvcodec, unknownacodec'), { self.assertEqual(parse_codecs('unknownvcodec, unknownacodec'), {
'vcodec': 'unknownvcodec', 'vcodec': 'unknownvcodec',
@@ -1141,12 +1163,15 @@ class TestUtil(unittest.TestCase):
def test_parse_resolution(self): def test_parse_resolution(self):
self.assertEqual(parse_resolution(None), {}) self.assertEqual(parse_resolution(None), {})
self.assertEqual(parse_resolution(''), {}) self.assertEqual(parse_resolution(''), {})
self.assertEqual(parse_resolution('1920x1080'), {'width': 1920, 'height': 1080}) self.assertEqual(parse_resolution(' 1920x1080'), {'width': 1920, 'height': 1080})
self.assertEqual(parse_resolution('1920×1080'), {'width': 1920, 'height': 1080}) self.assertEqual(parse_resolution('1920×1080 '), {'width': 1920, 'height': 1080})
self.assertEqual(parse_resolution('1920 x 1080'), {'width': 1920, 'height': 1080}) self.assertEqual(parse_resolution('1920 x 1080'), {'width': 1920, 'height': 1080})
self.assertEqual(parse_resolution('720p'), {'height': 720}) self.assertEqual(parse_resolution('720p'), {'height': 720})
self.assertEqual(parse_resolution('4k'), {'height': 2160}) self.assertEqual(parse_resolution('4k'), {'height': 2160})
self.assertEqual(parse_resolution('8K'), {'height': 4320}) self.assertEqual(parse_resolution('8K'), {'height': 4320})
self.assertEqual(parse_resolution('pre_1920x1080_post'), {'width': 1920, 'height': 1080})
self.assertEqual(parse_resolution('ep1x2'), {})
self.assertEqual(parse_resolution('1920, 1080'), {'width': 1920, 'height': 1080})
def test_parse_bitrate(self): def test_parse_bitrate(self):
self.assertEqual(parse_bitrate(None), None) self.assertEqual(parse_bitrate(None), None)
@@ -1231,6 +1256,7 @@ ffmpeg version 2.4.4 Copyright (c) 2000-2014 the FFmpeg ...'''), '2.4.4')
self.assertFalse(match_str('x>2K', {'x': 1200})) self.assertFalse(match_str('x>2K', {'x': 1200}))
self.assertTrue(match_str('x>=1200 & x < 1300', {'x': 1200})) self.assertTrue(match_str('x>=1200 & x < 1300', {'x': 1200}))
self.assertFalse(match_str('x>=1100 & x < 1200', {'x': 1200})) self.assertFalse(match_str('x>=1100 & x < 1200', {'x': 1200}))
self.assertTrue(match_str('x > 1:0:0', {'x': 3700}))
# String # String
self.assertFalse(match_str('y=a212', {'y': 'foobar42'})) self.assertFalse(match_str('y=a212', {'y': 'foobar42'}))
@@ -1367,21 +1393,21 @@ The first line
</body> </body>
</tt>'''.encode('utf-8') </tt>'''.encode('utf-8')
srt_data = '''1 srt_data = '''1
00:00:02,080 --> 00:00:05,839 00:00:02,080 --> 00:00:05,840
<font color="white" face="sansSerif" size="16">default style<font color="red">custom style</font></font> <font color="white" face="sansSerif" size="16">default style<font color="red">custom style</font></font>
2 2
00:00:02,080 --> 00:00:05,839 00:00:02,080 --> 00:00:05,840
<b><font color="cyan" face="sansSerif" size="16"><font color="lime">part 1 <b><font color="cyan" face="sansSerif" size="16"><font color="lime">part 1
</font>part 2</font></b> </font>part 2</font></b>
3 3
00:00:05,839 --> 00:00:09,560 00:00:05,840 --> 00:00:09,560
<u><font color="lime">line 3 <u><font color="lime">line 3
part 3</font></u> part 3</font></u>
4 4
00:00:09,560 --> 00:00:12,359 00:00:09,560 --> 00:00:12,360
<i><u><font color="yellow"><font color="lime">inner <i><u><font color="yellow"><font color="lime">inner
</font>style</font></u></i> </font>style</font></u></i>

File diff suppressed because it is too large Load Diff

View File

@@ -31,6 +31,7 @@ from .utils import (
expand_path, expand_path,
match_filter_func, match_filter_func,
MaxDownloadsReached, MaxDownloadsReached,
parse_duration,
preferredencoding, preferredencoding,
read_batch_urls, read_batch_urls,
RejectedVideoReached, RejectedVideoReached,
@@ -248,7 +249,7 @@ def _real_main(argv=None):
if opts.cookiesfrombrowser is not None: if opts.cookiesfrombrowser is not None:
opts.cookiesfrombrowser = [ opts.cookiesfrombrowser = [
part.strip() or None for part in opts.cookiesfrombrowser.split(':', 1)] part.strip() or None for part in opts.cookiesfrombrowser.split(':', 1)]
if opts.cookiesfrombrowser[0] not in SUPPORTED_BROWSERS: if opts.cookiesfrombrowser[0].lower() not in SUPPORTED_BROWSERS:
parser.error('unsupported browser specified for cookies') parser.error('unsupported browser specified for cookies')
if opts.date is not None: if opts.date is not None:
@@ -258,6 +259,9 @@ def _real_main(argv=None):
compat_opts = opts.compat_opts compat_opts = opts.compat_opts
def report_conflict(arg1, arg2):
warnings.append(f'{arg2} is ignored since {arg1} was given')
def _unused_compat_opt(name): def _unused_compat_opt(name):
if name not in compat_opts: if name not in compat_opts:
return False return False
@@ -279,7 +283,7 @@ def _real_main(argv=None):
setattr(opts, opt_name, default) setattr(opts, opt_name, default)
return None return None
set_default_compat('abort-on-error', 'ignoreerrors') set_default_compat('abort-on-error', 'ignoreerrors', 'only_download')
set_default_compat('no-playlist-metafiles', 'allow_playlist_files') set_default_compat('no-playlist-metafiles', 'allow_playlist_files')
set_default_compat('no-clean-infojson', 'clean_infojson') set_default_compat('no-clean-infojson', 'clean_infojson')
if 'format-sort' in compat_opts: if 'format-sort' in compat_opts:
@@ -289,10 +293,14 @@ def _real_main(argv=None):
if _video_multistreams_set is False and _audio_multistreams_set is False: if _video_multistreams_set is False and _audio_multistreams_set is False:
_unused_compat_opt('multistreams') _unused_compat_opt('multistreams')
outtmpl_default = opts.outtmpl.get('default') outtmpl_default = opts.outtmpl.get('default')
if opts.useid:
if outtmpl_default is None:
outtmpl_default = opts.outtmpl['default'] = '%(id)s.%(ext)s'
else:
report_conflict('--output', '--id')
if 'filename' in compat_opts: if 'filename' in compat_opts:
if outtmpl_default is None: if outtmpl_default is None:
outtmpl_default = '%(title)s-%(id)s.%(ext)s' outtmpl_default = opts.outtmpl['default'] = '%(title)s-%(id)s.%(ext)s'
opts.outtmpl.update({'default': outtmpl_default})
else: else:
_unused_compat_opt('filename') _unused_compat_opt('filename')
@@ -302,11 +310,14 @@ def _real_main(argv=None):
parser.error('invalid %s %r: %s' % (msg, tmpl, error_to_compat_str(err))) parser.error('invalid %s %r: %s' % (msg, tmpl, error_to_compat_str(err)))
for k, tmpl in opts.outtmpl.items(): for k, tmpl in opts.outtmpl.items():
validate_outtmpl(tmpl, '%s output template' % k) validate_outtmpl(tmpl, f'{k} output template')
opts.forceprint = opts.forceprint or [] opts.forceprint = opts.forceprint or []
for tmpl in opts.forceprint or []: for tmpl in opts.forceprint or []:
validate_outtmpl(tmpl, 'print template') validate_outtmpl(tmpl, 'print template')
validate_outtmpl(opts.sponsorblock_chapter_title, 'SponsorBlock chapter title') validate_outtmpl(opts.sponsorblock_chapter_title, 'SponsorBlock chapter title')
for k, tmpl in opts.progress_template.items():
k = f'{k[:-6]} console title' if '-title' in k else f'{k} progress'
validate_outtmpl(tmpl, f'{k} 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'
@@ -362,9 +373,6 @@ def _real_main(argv=None):
opts.addchapters = True opts.addchapters = True
opts.remove_chapters = opts.remove_chapters or [] opts.remove_chapters = opts.remove_chapters or []
def report_conflict(arg1, arg2):
warnings.append('%s is ignored since %s was given' % (arg2, arg1))
if (opts.remove_chapters or sponsorblock_query) and opts.sponskrub is not False: if (opts.remove_chapters or sponsorblock_query) and opts.sponskrub is not False:
if opts.sponskrub: if opts.sponskrub:
if opts.remove_chapters: if opts.remove_chapters:
@@ -418,7 +426,7 @@ def _real_main(argv=None):
opts.sponskrub = False opts.sponskrub = False
# PostProcessors # PostProcessors
postprocessors = [] postprocessors = list(opts.add_postprocessors)
if sponsorblock_query: if sponsorblock_query:
postprocessors.append({ postprocessors.append({
'key': 'SponsorBlock', 'key': 'SponsorBlock',
@@ -487,8 +495,14 @@ def _real_main(argv=None):
if opts.allsubtitles and not opts.writeautomaticsub: if opts.allsubtitles and not opts.writeautomaticsub:
opts.writesubtitles = True opts.writesubtitles = True
# ModifyChapters must run before FFmpegMetadataPP # ModifyChapters must run before FFmpegMetadataPP
remove_chapters_patterns = [] remove_chapters_patterns, remove_ranges = [], []
for regex in opts.remove_chapters: for regex in opts.remove_chapters:
if regex.startswith('*'):
dur = list(map(parse_duration, regex[1:].split('-')))
if len(dur) == 2 and all(t is not None for t in dur):
remove_ranges.append(tuple(dur))
continue
parser.error(f'invalid --remove-chapters time range {regex!r}. Must be of the form ?start-end')
try: try:
remove_chapters_patterns.append(re.compile(regex)) remove_chapters_patterns.append(re.compile(regex))
except re.error as err: except re.error as err:
@@ -498,6 +512,7 @@ def _real_main(argv=None):
'key': 'ModifyChapters', 'key': 'ModifyChapters',
'remove_chapters_patterns': remove_chapters_patterns, 'remove_chapters_patterns': remove_chapters_patterns,
'remove_sponsor_segments': opts.sponsorblock_remove, 'remove_sponsor_segments': opts.sponsorblock_remove,
'remove_ranges': remove_ranges,
'sponsorblock_chapter_title': opts.sponsorblock_chapter_title, 'sponsorblock_chapter_title': opts.sponsorblock_chapter_title,
'force_keyframes': opts.force_keyframes_at_cuts 'force_keyframes': opts.force_keyframes_at_cuts
}) })
@@ -513,6 +528,7 @@ def _real_main(argv=None):
'add_chapters': opts.addchapters, 'add_chapters': opts.addchapters,
'add_metadata': opts.addmetadata, 'add_metadata': opts.addmetadata,
}) })
# Note: Deprecated
# This should be above EmbedThumbnail since sponskrub removes the thumbnail attachment # This should be above EmbedThumbnail since sponskrub removes the thumbnail attachment
# but must be below EmbedSubtitle and FFmpegMetadata # but must be below EmbedSubtitle and FFmpegMetadata
# See https://github.com/yt-dlp/yt-dlp/issues/204 , https://github.com/faissaloo/SponSkrub/issues/29 # See https://github.com/yt-dlp/yt-dlp/issues/204 , https://github.com/faissaloo/SponSkrub/issues/29
@@ -535,6 +551,7 @@ def _real_main(argv=None):
}) })
if not already_have_thumbnail: if not already_have_thumbnail:
opts.writethumbnail = True opts.writethumbnail = True
opts.outtmpl['pl_thumbnail'] = ''
if opts.split_chapters: if opts.split_chapters:
postprocessors.append({ postprocessors.append({
'key': 'FFmpegSplitChapters', 'key': 'FFmpegSplitChapters',
@@ -575,6 +592,7 @@ def _real_main(argv=None):
ydl_opts = { ydl_opts = {
'usenetrc': opts.usenetrc, 'usenetrc': opts.usenetrc,
'netrc_location': opts.netrc_location,
'username': opts.username, 'username': opts.username,
'password': opts.password, 'password': opts.password,
'twofactor': opts.twofactor, 'twofactor': opts.twofactor,
@@ -630,8 +648,9 @@ def _real_main(argv=None):
'noresizebuffer': opts.noresizebuffer, 'noresizebuffer': opts.noresizebuffer,
'http_chunk_size': opts.http_chunk_size, 'http_chunk_size': opts.http_chunk_size,
'continuedl': opts.continue_dl, 'continuedl': opts.continue_dl,
'noprogress': opts.noprogress, 'noprogress': opts.quiet if opts.noprogress is None else opts.noprogress,
'progress_with_newline': opts.progress_with_newline, 'progress_with_newline': opts.progress_with_newline,
'progress_template': opts.progress_template,
'playliststart': opts.playliststart, 'playliststart': opts.playliststart,
'playlistend': opts.playlistend, 'playlistend': opts.playlistend,
'playlistreverse': opts.playlist_reverse, 'playlistreverse': opts.playlist_reverse,
@@ -726,12 +745,8 @@ def _real_main(argv=None):
'geo_bypass': opts.geo_bypass, 'geo_bypass': opts.geo_bypass,
'geo_bypass_country': opts.geo_bypass_country, 'geo_bypass_country': opts.geo_bypass_country,
'geo_bypass_ip_block': opts.geo_bypass_ip_block, 'geo_bypass_ip_block': opts.geo_bypass_ip_block,
'warnings': warnings, '_warnings': warnings,
'compat_opts': compat_opts, 'compat_opts': compat_opts,
# just for deprecation check
'autonumber': opts.autonumber or None,
'usetitle': opts.usetitle or None,
'useid': opts.useid or None,
} }
with YoutubeDL(ydl_opts) as ydl: with YoutubeDL(ydl_opts) as ydl:

View File

@@ -2,36 +2,68 @@ from __future__ import unicode_literals
from math import ceil from math import ceil
from .compat import compat_b64decode from .compat import compat_b64decode, compat_pycrypto_AES
from .utils import bytes_to_intlist, intlist_to_bytes from .utils import bytes_to_intlist, intlist_to_bytes
if compat_pycrypto_AES:
def aes_cbc_decrypt_bytes(data, key, iv):
""" Decrypt bytes with AES-CBC using pycryptodome """
return compat_pycrypto_AES.new(key, compat_pycrypto_AES.MODE_CBC, iv).decrypt(data)
def aes_gcm_decrypt_and_verify_bytes(data, key, tag, nonce):
""" Decrypt bytes with AES-GCM using pycryptodome """
return compat_pycrypto_AES.new(key, compat_pycrypto_AES.MODE_GCM, nonce).decrypt_and_verify(data, tag)
else:
def aes_cbc_decrypt_bytes(data, key, iv):
""" Decrypt bytes with AES-CBC using native implementation since pycryptodome is unavailable """
return intlist_to_bytes(aes_cbc_decrypt(*map(bytes_to_intlist, (data, key, iv))))
def aes_gcm_decrypt_and_verify_bytes(data, key, tag, nonce):
""" Decrypt bytes with AES-GCM using native implementation since pycryptodome is unavailable """
return intlist_to_bytes(aes_gcm_decrypt_and_verify(*map(bytes_to_intlist, (data, key, tag, nonce))))
BLOCK_SIZE_BYTES = 16 BLOCK_SIZE_BYTES = 16
def aes_ctr_decrypt(data, key, counter): def aes_ctr_decrypt(data, key, iv):
""" """
Decrypt with aes in counter mode Decrypt with aes in counter mode
@param {int[]} data cipher @param {int[]} data cipher
@param {int[]} key 16/24/32-Byte cipher key @param {int[]} key 16/24/32-Byte cipher key
@param {instance} counter Instance whose next_value function (@returns {int[]} 16-Byte block) @param {int[]} iv 16-Byte initialization vector
returns the next counter block
@returns {int[]} decrypted data @returns {int[]} decrypted data
""" """
return aes_ctr_encrypt(data, key, iv)
def aes_ctr_encrypt(data, key, iv):
"""
Encrypt with aes in counter mode
@param {int[]} data cleartext
@param {int[]} key 16/24/32-Byte cipher key
@param {int[]} iv 16-Byte initialization vector
@returns {int[]} encrypted data
"""
expanded_key = key_expansion(key) expanded_key = key_expansion(key)
block_count = int(ceil(float(len(data)) / BLOCK_SIZE_BYTES)) block_count = int(ceil(float(len(data)) / BLOCK_SIZE_BYTES))
counter = iter_vector(iv)
decrypted_data = [] encrypted_data = []
for i in range(block_count): for i in range(block_count):
counter_block = counter.next_value() counter_block = next(counter)
block = data[i * BLOCK_SIZE_BYTES: (i + 1) * BLOCK_SIZE_BYTES] block = data[i * BLOCK_SIZE_BYTES: (i + 1) * BLOCK_SIZE_BYTES]
block += [0] * (BLOCK_SIZE_BYTES - len(block)) block += [0] * (BLOCK_SIZE_BYTES - len(block))
cipher_counter_block = aes_encrypt(counter_block, expanded_key) cipher_counter_block = aes_encrypt(counter_block, expanded_key)
decrypted_data += xor(block, cipher_counter_block) encrypted_data += xor(block, cipher_counter_block)
decrypted_data = decrypted_data[:len(data)] encrypted_data = encrypted_data[:len(data)]
return decrypted_data return encrypted_data
def aes_cbc_decrypt(data, key, iv): def aes_cbc_decrypt(data, key, iv):
@@ -88,39 +120,47 @@ def aes_cbc_encrypt(data, key, iv):
return encrypted_data return encrypted_data
def key_expansion(data): def aes_gcm_decrypt_and_verify(data, key, tag, nonce):
""" """
Generate key schedule Decrypt with aes in GBM mode and checks authenticity using tag
@param {int[]} data 16/24/32-Byte cipher key @param {int[]} data cipher
@returns {int[]} 176/208/240-Byte expanded key @param {int[]} key 16-Byte cipher key
@param {int[]} tag authentication tag
@param {int[]} nonce IV (recommended 12-Byte)
@returns {int[]} decrypted data
""" """
data = data[:] # copy
rcon_iteration = 1
key_size_bytes = len(data)
expanded_key_size_bytes = (key_size_bytes // 4 + 7) * BLOCK_SIZE_BYTES
while len(data) < expanded_key_size_bytes: # XXX: check aes, gcm param
temp = data[-4:]
temp = key_schedule_core(temp, rcon_iteration)
rcon_iteration += 1
data += xor(temp, data[-key_size_bytes: 4 - key_size_bytes])
for _ in range(3): hash_subkey = aes_encrypt([0] * BLOCK_SIZE_BYTES, key_expansion(key))
temp = data[-4:]
data += xor(temp, data[-key_size_bytes: 4 - key_size_bytes])
if key_size_bytes == 32: if len(nonce) == 12:
temp = data[-4:] j0 = nonce + [0, 0, 0, 1]
temp = sub_bytes(temp) else:
data += xor(temp, data[-key_size_bytes: 4 - key_size_bytes]) fill = (BLOCK_SIZE_BYTES - (len(nonce) % BLOCK_SIZE_BYTES)) % BLOCK_SIZE_BYTES + 8
ghash_in = nonce + [0] * fill + bytes_to_intlist((8 * len(nonce)).to_bytes(8, 'big'))
j0 = ghash(hash_subkey, ghash_in)
for _ in range(3 if key_size_bytes == 32 else 2 if key_size_bytes == 24 else 0): # TODO: add nonce support to aes_ctr_decrypt
temp = data[-4:]
data += xor(temp, data[-key_size_bytes: 4 - key_size_bytes])
data = data[:expanded_key_size_bytes]
return data # nonce_ctr = j0[:12]
iv_ctr = inc(j0)
decrypted_data = aes_ctr_decrypt(data, key, iv_ctr + [0] * (BLOCK_SIZE_BYTES - len(iv_ctr)))
pad_len = len(data) // 16 * 16
s_tag = ghash(
hash_subkey,
data
+ [0] * (BLOCK_SIZE_BYTES - len(data) + pad_len) # pad
+ bytes_to_intlist((0 * 8).to_bytes(8, 'big') # length of associated data
+ ((len(data) * 8).to_bytes(8, 'big'))) # length of data
)
if tag != aes_ctr_encrypt(s_tag, key, j0):
raise ValueError("Mismatching authentication tag")
return decrypted_data
def aes_encrypt(data, expanded_key): def aes_encrypt(data, expanded_key):
@@ -138,7 +178,7 @@ def aes_encrypt(data, expanded_key):
data = sub_bytes(data) data = sub_bytes(data)
data = shift_rows(data) data = shift_rows(data)
if i != rounds: if i != rounds:
data = mix_columns(data) data = list(iter_mix_columns(data, MIX_COLUMN_MATRIX))
data = xor(data, expanded_key[i * BLOCK_SIZE_BYTES: (i + 1) * BLOCK_SIZE_BYTES]) data = xor(data, expanded_key[i * BLOCK_SIZE_BYTES: (i + 1) * BLOCK_SIZE_BYTES])
return data return data
@@ -157,7 +197,7 @@ def aes_decrypt(data, expanded_key):
for i in range(rounds, 0, -1): for i in range(rounds, 0, -1):
data = xor(data, expanded_key[i * BLOCK_SIZE_BYTES: (i + 1) * BLOCK_SIZE_BYTES]) data = xor(data, expanded_key[i * BLOCK_SIZE_BYTES: (i + 1) * BLOCK_SIZE_BYTES])
if i != rounds: if i != rounds:
data = mix_columns_inv(data) data = list(iter_mix_columns(data, MIX_COLUMN_MATRIX_INV))
data = shift_rows_inv(data) data = shift_rows_inv(data)
data = sub_bytes_inv(data) data = sub_bytes_inv(data)
data = xor(data, expanded_key[:BLOCK_SIZE_BYTES]) data = xor(data, expanded_key[:BLOCK_SIZE_BYTES])
@@ -189,15 +229,7 @@ def aes_decrypt_text(data, password, key_size_bytes):
nonce = data[:NONCE_LENGTH_BYTES] nonce = data[:NONCE_LENGTH_BYTES]
cipher = data[NONCE_LENGTH_BYTES:] cipher = data[NONCE_LENGTH_BYTES:]
class Counter(object): decrypted_data = aes_ctr_decrypt(cipher, key, nonce + [0] * (BLOCK_SIZE_BYTES - NONCE_LENGTH_BYTES))
__value = nonce + [0] * (BLOCK_SIZE_BYTES - NONCE_LENGTH_BYTES)
def next_value(self):
temp = self.__value
self.__value = inc(self.__value)
return temp
decrypted_data = aes_ctr_decrypt(cipher, key, Counter())
plaintext = intlist_to_bytes(decrypted_data) plaintext = intlist_to_bytes(decrypted_data)
return plaintext return plaintext
@@ -278,6 +310,47 @@ RIJNDAEL_LOG_TABLE = (0x00, 0x00, 0x19, 0x01, 0x32, 0x02, 0x1a, 0xc6, 0x4b, 0xc7
0x67, 0x4a, 0xed, 0xde, 0xc5, 0x31, 0xfe, 0x18, 0x0d, 0x63, 0x8c, 0x80, 0xc0, 0xf7, 0x70, 0x07) 0x67, 0x4a, 0xed, 0xde, 0xc5, 0x31, 0xfe, 0x18, 0x0d, 0x63, 0x8c, 0x80, 0xc0, 0xf7, 0x70, 0x07)
def key_expansion(data):
"""
Generate key schedule
@param {int[]} data 16/24/32-Byte cipher key
@returns {int[]} 176/208/240-Byte expanded key
"""
data = data[:] # copy
rcon_iteration = 1
key_size_bytes = len(data)
expanded_key_size_bytes = (key_size_bytes // 4 + 7) * BLOCK_SIZE_BYTES
while len(data) < expanded_key_size_bytes:
temp = data[-4:]
temp = key_schedule_core(temp, rcon_iteration)
rcon_iteration += 1
data += xor(temp, data[-key_size_bytes: 4 - key_size_bytes])
for _ in range(3):
temp = data[-4:]
data += xor(temp, data[-key_size_bytes: 4 - key_size_bytes])
if key_size_bytes == 32:
temp = data[-4:]
temp = sub_bytes(temp)
data += xor(temp, data[-key_size_bytes: 4 - key_size_bytes])
for _ in range(3 if key_size_bytes == 32 else 2 if key_size_bytes == 24 else 0):
temp = data[-4:]
data += xor(temp, data[-key_size_bytes: 4 - key_size_bytes])
data = data[:expanded_key_size_bytes]
return data
def iter_vector(iv):
while True:
yield iv
iv = inc(iv)
def sub_bytes(data): def sub_bytes(data):
return [SBOX[x] for x in data] return [SBOX[x] for x in data]
@@ -302,48 +375,36 @@ def xor(data1, data2):
return [x ^ y for x, y in zip(data1, data2)] return [x ^ y for x, y in zip(data1, data2)]
def rijndael_mul(a, b): def iter_mix_columns(data, matrix):
if(a == 0 or b == 0): for i in (0, 4, 8, 12):
return 0 for row in matrix:
return RIJNDAEL_EXP_TABLE[(RIJNDAEL_LOG_TABLE[a] + RIJNDAEL_LOG_TABLE[b]) % 0xFF]
def mix_column(data, matrix):
data_mixed = []
for row in range(4):
mixed = 0 mixed = 0
for column in range(4): for j in range(4):
# xor is (+) and (-) # xor is (+) and (-)
mixed ^= rijndael_mul(data[column], matrix[row][column]) mixed ^= (0 if data[i:i + 4][j] == 0 or row[j] == 0 else
data_mixed.append(mixed) RIJNDAEL_EXP_TABLE[(RIJNDAEL_LOG_TABLE[data[i + j]] + RIJNDAEL_LOG_TABLE[row[j]]) % 0xFF])
return data_mixed yield mixed
def mix_columns(data, matrix=MIX_COLUMN_MATRIX):
data_mixed = []
for i in range(4):
column = data[i * 4: (i + 1) * 4]
data_mixed += mix_column(column, matrix)
return data_mixed
def mix_columns_inv(data):
return mix_columns(data, MIX_COLUMN_MATRIX_INV)
def shift_rows(data): def shift_rows(data):
data_shifted = [] return [data[((column + row) & 0b11) * 4 + row] for column in range(4) for row in range(4)]
for column in range(4):
for row in range(4):
data_shifted.append(data[((column + row) & 0b11) * 4 + row])
return data_shifted
def shift_rows_inv(data): def shift_rows_inv(data):
return [data[((column - row) & 0b11) * 4 + row] for column in range(4) for row in range(4)]
def shift_block(data):
data_shifted = [] data_shifted = []
for column in range(4):
for row in range(4): bit = 0
data_shifted.append(data[((column - row) & 0b11) * 4 + row]) for n in data:
if bit:
n |= 0x100
bit = n & 1
n >>= 1
data_shifted.append(n)
return data_shifted return data_shifted
@@ -358,4 +419,50 @@ def inc(data):
return data return data
__all__ = ['aes_encrypt', 'key_expansion', 'aes_ctr_decrypt', 'aes_cbc_decrypt', 'aes_decrypt_text'] def block_product(block_x, block_y):
# NIST SP 800-38D, Algorithm 1
if len(block_x) != BLOCK_SIZE_BYTES or len(block_y) != BLOCK_SIZE_BYTES:
raise ValueError("Length of blocks need to be %d bytes" % BLOCK_SIZE_BYTES)
block_r = [0xE1] + [0] * (BLOCK_SIZE_BYTES - 1)
block_v = block_y[:]
block_z = [0] * BLOCK_SIZE_BYTES
for i in block_x:
for bit in range(7, -1, -1):
if i & (1 << bit):
block_z = xor(block_z, block_v)
do_xor = block_v[-1] & 1
block_v = shift_block(block_v)
if do_xor:
block_v = xor(block_v, block_r)
return block_z
def ghash(subkey, data):
# NIST SP 800-38D, Algorithm 2
if len(data) % BLOCK_SIZE_BYTES:
raise ValueError("Length of data should be %d bytes" % BLOCK_SIZE_BYTES)
last_y = [0] * BLOCK_SIZE_BYTES
for i in range(0, len(data), BLOCK_SIZE_BYTES):
block = data[i : i + BLOCK_SIZE_BYTES] # noqa: E203
last_y = block_product(xor(last_y, block), subkey)
return last_y
__all__ = [
'aes_ctr_decrypt',
'aes_cbc_decrypt',
'aes_cbc_decrypt_bytes',
'aes_decrypt_text',
'aes_encrypt',
'aes_gcm_decrypt_and_verify',
'aes_gcm_decrypt_and_verify_bytes',
'key_expansion'
]

View File

@@ -50,6 +50,7 @@ class Cache(object):
except OSError as ose: except OSError as ose:
if ose.errno != errno.EEXIST: if ose.errno != errno.EEXIST:
raise raise
self._ydl.write_debug(f'Saving {section}.{key} to cache')
write_json_file(data, fn) write_json_file(data, fn)
except Exception: except Exception:
tb = traceback.format_exc() tb = traceback.format_exc()
@@ -66,6 +67,7 @@ class Cache(object):
try: try:
try: try:
with io.open(cache_fn, 'r', encoding='utf-8') as cachef: with io.open(cache_fn, 'r', encoding='utf-8') as cachef:
self._ydl.write_debug(f'Loading {section}.{key} from cache')
return json.load(cachef) return json.load(cachef)
except ValueError: except ValueError:
try: try:

View File

@@ -33,6 +33,8 @@ class compat_HTMLParseError(Exception):
pass pass
# compat_ctypes_WINFUNCTYPE = ctypes.WINFUNCTYPE
# will not work since ctypes.WINFUNCTYPE does not exist in UNIX machines
def compat_ctypes_WINFUNCTYPE(*args, **kwargs): def compat_ctypes_WINFUNCTYPE(*args, **kwargs):
return ctypes.WINFUNCTYPE(*args, **kwargs) return ctypes.WINFUNCTYPE(*args, **kwargs)
@@ -130,6 +132,39 @@ except AttributeError:
asyncio.run = compat_asyncio_run asyncio.run = compat_asyncio_run
# Python 3.8+ does not honor %HOME% on windows, but this breaks compatibility with youtube-dl
# See https://github.com/yt-dlp/yt-dlp/issues/792
# https://docs.python.org/3/library/os.path.html#os.path.expanduser
if compat_os_name in ('nt', 'ce') and 'HOME' in os.environ:
_userhome = os.environ['HOME']
def compat_expanduser(path):
if not path.startswith('~'):
return path
i = path.replace('\\', '/', 1).find('/') # ~user
if i < 0:
i = len(path)
userhome = os.path.join(os.path.dirname(_userhome), path[1:i]) if i > 1 else _userhome
return userhome + path[i:]
else:
compat_expanduser = os.path.expanduser
try:
from Cryptodome.Cipher import AES as compat_pycrypto_AES
except ImportError:
try:
from Crypto.Cipher import AES as compat_pycrypto_AES
except ImportError:
compat_pycrypto_AES = None
def windows_enable_vt_mode(): # TODO: Do this the proper way https://bugs.python.org/issue30075
if compat_os_name != 'nt':
return
os.system('')
# Deprecated # Deprecated
compat_basestring = str compat_basestring = str
@@ -152,7 +187,6 @@ compat_cookies = http.cookies
compat_cookies_SimpleCookie = compat_cookies.SimpleCookie compat_cookies_SimpleCookie = compat_cookies.SimpleCookie
compat_etree_Element = etree.Element compat_etree_Element = etree.Element
compat_etree_register_namespace = etree.register_namespace compat_etree_register_namespace = etree.register_namespace
compat_expanduser = os.path.expanduser
compat_get_terminal_size = shutil.get_terminal_size compat_get_terminal_size = shutil.get_terminal_size
compat_getenv = os.getenv compat_getenv = os.getenv
compat_getpass = getpass.getpass compat_getpass = getpass.getpass
@@ -224,6 +258,7 @@ __all__ = [
'compat_os_name', 'compat_os_name',
'compat_parse_qs', 'compat_parse_qs',
'compat_print', 'compat_print',
'compat_pycrypto_AES',
'compat_realpath', 'compat_realpath',
'compat_setenv', 'compat_setenv',
'compat_shlex_quote', 'compat_shlex_quote',
@@ -252,5 +287,6 @@ __all__ = [
'compat_xml_parse_error', 'compat_xml_parse_error',
'compat_xpath', 'compat_xpath',
'compat_zip', 'compat_zip',
'windows_enable_vt_mode',
'workaround_optparse_bug9161', 'workaround_optparse_bug9161',
] ]

View File

@@ -9,17 +9,15 @@ import tempfile
from datetime import datetime, timedelta, timezone from datetime import datetime, timedelta, timezone
from hashlib import pbkdf2_hmac from hashlib import pbkdf2_hmac
from yt_dlp.aes import aes_cbc_decrypt from .aes import aes_cbc_decrypt_bytes, aes_gcm_decrypt_and_verify_bytes
from yt_dlp.compat import ( from .compat import (
compat_b64decode, compat_b64decode,
compat_cookiejar_Cookie, compat_cookiejar_Cookie,
) )
from yt_dlp.utils import ( from .utils import (
bug_reports_message, bug_reports_message,
bytes_to_intlist,
expand_path, expand_path,
intlist_to_bytes, Popen,
process_communicate_or_kill,
YoutubeDLCookieJar, YoutubeDLCookieJar,
) )
@@ -32,12 +30,6 @@ except ImportError:
SQLITE_AVAILABLE = False SQLITE_AVAILABLE = False
try:
from Crypto.Cipher import AES
CRYPTO_AVAILABLE = True
except ImportError:
CRYPTO_AVAILABLE = False
try: try:
import keyring import keyring
KEYRING_AVAILABLE = True KEYRING_AVAILABLE = True
@@ -123,7 +115,7 @@ def _extract_firefox_cookies(profile, logger):
cookie_database_path = _find_most_recently_used_file(search_root, 'cookies.sqlite') cookie_database_path = _find_most_recently_used_file(search_root, 'cookies.sqlite')
if cookie_database_path is None: if cookie_database_path is None:
raise FileNotFoundError('could not find firefox cookies database in {}'.format(search_root)) raise FileNotFoundError('could not find firefox cookies database in {}'.format(search_root))
logger.debug('extracting from: "{}"'.format(cookie_database_path)) logger.debug('Extracting cookies from: "{}"'.format(cookie_database_path))
with tempfile.TemporaryDirectory(prefix='youtube_dl') as tmpdir: with tempfile.TemporaryDirectory(prefix='youtube_dl') as tmpdir:
cursor = None cursor = None
@@ -240,7 +232,7 @@ def _extract_chrome_cookies(browser_name, profile, logger):
cookie_database_path = _find_most_recently_used_file(search_root, 'Cookies') cookie_database_path = _find_most_recently_used_file(search_root, 'Cookies')
if cookie_database_path is None: if cookie_database_path is None:
raise FileNotFoundError('could not find {} cookies database in "{}"'.format(browser_name, search_root)) raise FileNotFoundError('could not find {} cookies database in "{}"'.format(browser_name, search_root))
logger.debug('extracting from: "{}"'.format(cookie_database_path)) logger.debug('Extracting cookies from: "{}"'.format(cookie_database_path))
decryptor = get_cookie_decryptor(config['browser_dir'], config['keyring_name'], logger) decryptor = get_cookie_decryptor(config['browser_dir'], config['keyring_name'], logger)
@@ -361,7 +353,7 @@ class LinuxChromeCookieDecryptor(ChromeCookieDecryptor):
class MacChromeCookieDecryptor(ChromeCookieDecryptor): class MacChromeCookieDecryptor(ChromeCookieDecryptor):
def __init__(self, browser_keyring_name, logger): def __init__(self, browser_keyring_name, logger):
self._logger = logger self._logger = logger
password = _get_mac_keyring_password(browser_keyring_name) password = _get_mac_keyring_password(browser_keyring_name, logger)
self._v10_key = None if password is None else self.derive_key(password) self._v10_key = None if password is None else self.derive_key(password)
@staticmethod @staticmethod
@@ -400,11 +392,6 @@ class WindowsChromeCookieDecryptor(ChromeCookieDecryptor):
if self._v10_key is None: if self._v10_key is None:
self._logger.warning('cannot decrypt v10 cookies: no key found', only_once=True) self._logger.warning('cannot decrypt v10 cookies: no key found', only_once=True)
return None return None
elif not CRYPTO_AVAILABLE:
self._logger.warning('cannot decrypt cookie as the `pycryptodome` module is not installed. '
'Please install by running `python3 -m pip install pycryptodome`',
only_once=True)
return None
# https://chromium.googlesource.com/chromium/src/+/refs/heads/main/components/os_crypt/os_crypt_win.cc # https://chromium.googlesource.com/chromium/src/+/refs/heads/main/components/os_crypt/os_crypt_win.cc
# kNonceLength # kNonceLength
@@ -559,7 +546,7 @@ def _parse_safari_cookies_record(data, jar, logger):
p.skip_to(value_offset) p.skip_to(value_offset)
value = p.read_cstring() value = p.read_cstring()
except UnicodeDecodeError: except UnicodeDecodeError:
logger.warning('failed to parse cookie because UTF-8 decoding failed') logger.warning('failed to parse Safari cookie because UTF-8 decoding failed', only_once=True)
return record_size return record_size
p.skip_to(record_size, 'space at the end of the record') p.skip_to(record_size, 'space at the end of the record')
@@ -605,21 +592,26 @@ def _get_linux_keyring_password(browser_keyring_name):
return password.encode('utf-8') return password.encode('utf-8')
def _get_mac_keyring_password(browser_keyring_name): def _get_mac_keyring_password(browser_keyring_name, logger):
if KEYRING_AVAILABLE: if KEYRING_AVAILABLE:
logger.debug('using keyring to obtain password')
password = keyring.get_password('{} Safe Storage'.format(browser_keyring_name), browser_keyring_name) password = keyring.get_password('{} Safe Storage'.format(browser_keyring_name), browser_keyring_name)
return password.encode('utf-8') return password.encode('utf-8')
else: else:
proc = subprocess.Popen(['security', 'find-generic-password', logger.debug('using find-generic-password to obtain password')
proc = Popen(
['security', 'find-generic-password',
'-w', # write password to stdout '-w', # write password to stdout
'-a', browser_keyring_name, # match 'account' '-a', browser_keyring_name, # match 'account'
'-s', '{} Safe Storage'.format(browser_keyring_name)], # match 'service' '-s', '{} Safe Storage'.format(browser_keyring_name)], # match 'service'
stdout=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.DEVNULL)
stderr=subprocess.DEVNULL)
try: try:
stdout, stderr = process_communicate_or_kill(proc) stdout, stderr = proc.communicate_or_kill()
if stdout[-1:] == b'\n':
stdout = stdout[:-1]
return stdout return stdout
except BaseException: except BaseException as e:
logger.warning(f'exception running find-generic-password: {type(e).__name__}({e})')
return None return None
@@ -628,7 +620,7 @@ def _get_windows_v10_key(browser_root, logger):
if path is None: if path is None:
logger.error('could not find local state file') logger.error('could not find local state file')
return None return None
with open(path, 'r') as f: with open(path, 'r', encoding='utf8') as f:
data = json.load(f) data = json.load(f)
try: try:
base64_key = data['os_crypt']['encrypted_key'] base64_key = data['os_crypt']['encrypted_key']
@@ -648,29 +640,26 @@ def pbkdf2_sha1(password, salt, iterations, key_length):
def _decrypt_aes_cbc(ciphertext, key, logger, initialization_vector=b' ' * 16): def _decrypt_aes_cbc(ciphertext, key, logger, initialization_vector=b' ' * 16):
plaintext = aes_cbc_decrypt(bytes_to_intlist(ciphertext), plaintext = aes_cbc_decrypt_bytes(ciphertext, key, initialization_vector)
bytes_to_intlist(key),
bytes_to_intlist(initialization_vector))
padding_length = plaintext[-1] padding_length = plaintext[-1]
try: try:
return intlist_to_bytes(plaintext[:-padding_length]).decode('utf-8') return plaintext[:-padding_length].decode('utf-8')
except UnicodeDecodeError: except UnicodeDecodeError:
logger.warning('failed to decrypt cookie because UTF-8 decoding failed. Possibly the key is wrong?') logger.warning('failed to decrypt cookie (AES-CBC) because UTF-8 decoding failed. Possibly the key is wrong?', only_once=True)
return None return None
def _decrypt_aes_gcm(ciphertext, key, nonce, authentication_tag, logger): def _decrypt_aes_gcm(ciphertext, key, nonce, authentication_tag, logger):
cipher = AES.new(key, AES.MODE_GCM, nonce)
try: try:
plaintext = cipher.decrypt_and_verify(ciphertext, authentication_tag) plaintext = aes_gcm_decrypt_and_verify_bytes(ciphertext, key, authentication_tag, nonce)
except ValueError: except ValueError:
logger.warning('failed to decrypt cookie because the MAC check failed. Possibly the key is wrong?') logger.warning('failed to decrypt cookie (AES-GCM) because the MAC check failed. Possibly the key is wrong?', only_once=True)
return None return None
try: try:
return plaintext.decode('utf-8') return plaintext.decode('utf-8')
except UnicodeDecodeError: except UnicodeDecodeError:
logger.warning('failed to decrypt cookie because UTF-8 decoding failed. Possibly the key is wrong?') logger.warning('failed to decrypt cookie (AES-GCM) because UTF-8 decoding failed. Possibly the key is wrong?', only_once=True)
return None return None
@@ -698,7 +687,7 @@ def _decrypt_windows_dpapi(ciphertext, logger):
ctypes.byref(blob_out) # pDataOut ctypes.byref(blob_out) # pDataOut
) )
if not ret: if not ret:
logger.warning('failed to decrypt with DPAPI') logger.warning('failed to decrypt with DPAPI', only_once=True)
return None return None
result = ctypes.string_at(blob_out.pbData, blob_out.cbData) result = ctypes.string_at(blob_out.pbData, blob_out.cbData)
@@ -748,6 +737,7 @@ def _is_path(value):
def _parse_browser_specification(browser_name, profile=None): def _parse_browser_specification(browser_name, profile=None):
browser_name = browser_name.lower()
if browser_name not in SUPPORTED_BROWSERS: if browser_name not in SUPPORTED_BROWSERS:
raise ValueError(f'unsupported browser: "{browser_name}"') raise ValueError(f'unsupported browser: "{browser_name}"')
if profile is not None and _is_path(profile): if profile is not None and _is_path(profile):

View File

@@ -10,10 +10,15 @@ from ..utils import (
def get_suitable_downloader(info_dict, params={}, default=NO_DEFAULT, protocol=None, to_stdout=False): def get_suitable_downloader(info_dict, params={}, default=NO_DEFAULT, protocol=None, to_stdout=False):
info_dict['protocol'] = determine_protocol(info_dict) info_dict['protocol'] = determine_protocol(info_dict)
info_copy = info_dict.copy() info_copy = info_dict.copy()
if protocol:
info_copy['protocol'] = protocol
info_copy['to_stdout'] = to_stdout info_copy['to_stdout'] = to_stdout
return _get_suitable_downloader(info_copy, params, default)
downloaders = [_get_suitable_downloader(info_copy, proto, params, default)
for proto in (protocol or info_copy['protocol']).split('+')]
if set(downloaders) == {FFmpegFD} and FFmpegFD.can_merge_formats(info_copy, params):
return FFmpegFD
elif len(downloaders) == 1:
return downloaders[0]
return None
# Some of these require get_suitable_downloader # Some of these require get_suitable_downloader
@@ -72,7 +77,7 @@ def shorten_protocol_name(proto, simplify=False):
return short_protocol_names.get(proto, proto) return short_protocol_names.get(proto, proto)
def _get_suitable_downloader(info_dict, params, default): def _get_suitable_downloader(info_dict, protocol, params, default):
"""Get the downloader class that can handle the info dict.""" """Get the downloader class that can handle the info dict."""
if default is NO_DEFAULT: if default is NO_DEFAULT:
default = HttpFD default = HttpFD
@@ -80,7 +85,7 @@ def _get_suitable_downloader(info_dict, params, default):
# if (info_dict.get('start_time') or info_dict.get('end_time')) and not info_dict.get('requested_formats') and FFmpegFD.can_download(info_dict): # if (info_dict.get('start_time') or info_dict.get('end_time')) and not info_dict.get('requested_formats') and FFmpegFD.can_download(info_dict):
# return FFmpegFD # return FFmpegFD
protocol = info_dict['protocol'] info_dict['protocol'] = protocol
downloaders = params.get('external_downloader') downloaders = params.get('external_downloader')
external_downloader = ( external_downloader = (
downloaders if isinstance(downloaders, compat_str) or downloaders is None downloaders if isinstance(downloaders, compat_str) or downloaders is None

View File

@@ -1,13 +1,10 @@
from __future__ import division, unicode_literals from __future__ import division, unicode_literals
import copy
import os import os
import re import re
import sys
import time import time
import random import random
from ..compat import compat_os_name
from ..utils import ( from ..utils import (
decodeArgument, decodeArgument,
encodeFilename, encodeFilename,
@@ -15,6 +12,13 @@ from ..utils import (
format_bytes, format_bytes,
shell_quote, shell_quote,
timeconvert, timeconvert,
timetuple_from_msec,
)
from ..minicurses import (
MultilineLogger,
MultilinePrinter,
QuietMultilinePrinter,
BreaklineStatusPrinter
) )
@@ -39,8 +43,6 @@ class FileDownloader(object):
noresizebuffer: Do not automatically resize the download buffer. noresizebuffer: Do not automatically resize the download buffer.
continuedl: Try to continue downloads if possible. continuedl: Try to continue downloads if possible.
noprogress: Do not print the progress bar. noprogress: Do not print the progress bar.
logtostderr: Log messages to stderr instead of stdout.
consoletitle: Display progress in console window's titlebar.
nopart: Do not use temporary .part files. nopart: Do not use temporary .part files.
updatetime: Use the Last-modified header to set output file timestamps. updatetime: Use the Last-modified header to set output file timestamps.
test: Download only first bytes to test the downloader. test: Download only first bytes to test the downloader.
@@ -56,6 +58,7 @@ class FileDownloader(object):
http_chunk_size: Size of a chunk for chunk-based HTTP downloading. May be http_chunk_size: Size of a chunk for chunk-based HTTP downloading. May be
useful for bypassing bandwidth throttling imposed by useful for bypassing bandwidth throttling imposed by
a webserver (experimental) a webserver (experimental)
progress_template: See YoutubeDL.py
Subclasses of this one must re-define the real_download method. Subclasses of this one must re-define the real_download method.
""" """
@@ -68,18 +71,17 @@ class FileDownloader(object):
self.ydl = ydl self.ydl = ydl
self._progress_hooks = [] self._progress_hooks = []
self.params = params self.params = params
self._prepare_multiline_status()
self.add_progress_hook(self.report_progress) self.add_progress_hook(self.report_progress)
@staticmethod @staticmethod
def format_seconds(seconds): def format_seconds(seconds):
(mins, secs) = divmod(seconds, 60) time = timetuple_from_msec(seconds * 1000)
(hours, mins) = divmod(mins, 60) if time.hours > 99:
if hours > 99:
return '--:--:--' return '--:--:--'
if hours == 0: if not time.hours:
return '%02d:%02d' % (mins, secs) return '%02d:%02d' % time[1:-1]
else: return '%02d:%02d:%02d' % time[:-1]
return '%02d:%02d:%02d' % (hours, mins, secs)
@staticmethod @staticmethod
def calc_percent(byte_counter, data_len): def calc_percent(byte_counter, data_len):
@@ -236,28 +238,36 @@ class FileDownloader(object):
"""Report destination filename.""" """Report destination filename."""
self.to_screen('[download] Destination: ' + filename) self.to_screen('[download] Destination: ' + filename)
def _report_progress_status(self, msg, is_last_line=False): def _prepare_multiline_status(self, lines=1):
fullmsg = '[download] ' + msg if self.params.get('noprogress'):
if self.params.get('progress_with_newline', False): self._multiline = QuietMultilinePrinter()
self.to_screen(fullmsg) elif self.ydl.params.get('logger'):
self._multiline = MultilineLogger(self.ydl.params['logger'], lines)
elif self.params.get('progress_with_newline'):
self._multiline = BreaklineStatusPrinter(self.ydl._screen_file, lines)
else: else:
if compat_os_name == 'nt': self._multiline = MultilinePrinter(self.ydl._screen_file, lines, not self.params.get('quiet'))
prev_len = getattr(self, '_report_progress_prev_line_length',
0) def _finish_multiline_status(self):
if prev_len > len(fullmsg): self._multiline.end()
fullmsg += ' ' * (prev_len - len(fullmsg))
self._report_progress_prev_line_length = len(fullmsg) def _report_progress_status(self, s):
clear_line = '\r' progress_dict = s.copy()
else: progress_dict.pop('info_dict')
clear_line = ('\r\x1b[K' if sys.stderr.isatty() else '\r') progress_dict = {'info': s['info_dict'], 'progress': progress_dict}
self.to_screen(clear_line + fullmsg, skip_eol=not is_last_line)
self.to_console_title('yt-dlp ' + msg) progress_template = self.params.get('progress_template', {})
self._multiline.print_at_line(self.ydl.evaluate_outtmpl(
progress_template.get('download') or '[download] %(progress._default_template)s',
progress_dict), s.get('progress_idx') or 0)
self.to_console_title(self.ydl.evaluate_outtmpl(
progress_template.get('download-title') or 'yt-dlp %(progress._default_template)s',
progress_dict))
def report_progress(self, s): def report_progress(self, s):
if s['status'] == 'finished': if s['status'] == 'finished':
if self.params.get('noprogress', False): if self.params.get('noprogress'):
self.to_screen('[download] Download completed') self.to_screen('[download] Download completed')
else:
msg_template = '100%%' msg_template = '100%%'
if s.get('total_bytes') is not None: if s.get('total_bytes') is not None:
s['_total_bytes_str'] = format_bytes(s['total_bytes']) s['_total_bytes_str'] = format_bytes(s['total_bytes'])
@@ -265,10 +275,9 @@ class FileDownloader(object):
if s.get('elapsed') is not None: if s.get('elapsed') is not None:
s['_elapsed_str'] = self.format_seconds(s['elapsed']) s['_elapsed_str'] = self.format_seconds(s['elapsed'])
msg_template += ' in %(_elapsed_str)s' msg_template += ' in %(_elapsed_str)s'
self._report_progress_status( s['_percent_str'] = self.format_percent(100)
msg_template % s, is_last_line=True) s['_default_template'] = msg_template % s
self._report_progress_status(s)
if self.params.get('noprogress'):
return return
if s['status'] != 'downloading': if s['status'] != 'downloading':
@@ -310,8 +319,8 @@ class FileDownloader(object):
msg_template = '%(_downloaded_bytes_str)s at %(_speed_str)s' msg_template = '%(_downloaded_bytes_str)s at %(_speed_str)s'
else: else:
msg_template = '%(_percent_str)s % at %(_speed_str)s ETA %(_eta_str)s' msg_template = '%(_percent_str)s % at %(_speed_str)s ETA %(_eta_str)s'
s['_default_template'] = msg_template % s
self._report_progress_status(msg_template % s) self._report_progress_status(s)
def report_resuming_byte(self, resume_len): def report_resuming_byte(self, resume_len):
"""Report attempt to resume at given byte.""" """Report attempt to resume at given byte."""
@@ -383,7 +392,9 @@ class FileDownloader(object):
'[download] Sleeping %s seconds ...' % ( '[download] Sleeping %s seconds ...' % (
sleep_interval_sub)) sleep_interval_sub))
time.sleep(sleep_interval_sub) time.sleep(sleep_interval_sub)
return self.real_download(filename, info_dict), True ret = self.real_download(filename, info_dict)
self._finish_multiline_status()
return ret, True
def real_download(self, filename, info_dict): def real_download(self, filename, info_dict):
"""Real download process. Redefine in subclasses.""" """Real download process. Redefine in subclasses."""
@@ -392,13 +403,10 @@ class FileDownloader(object):
def _hook_progress(self, status, info_dict): def _hook_progress(self, status, info_dict):
if not self._progress_hooks: if not self._progress_hooks:
return return
info_dict = dict(info_dict) status['info_dict'] = info_dict
for key in ('__original_infodict', '__postprocessors'):
info_dict.pop(key, None)
# youtube-dl passes the same status object to all the hooks. # youtube-dl passes the same status object to all the hooks.
# Some third party scripts seems to be relying on this. # Some third party scripts seems to be relying on this.
# So keep this behavior if possible # So keep this behavior if possible
status['info_dict'] = copy.deepcopy(info_dict)
for ph in self._progress_hooks: for ph in self._progress_hooks:
ph(status) ph(status)

View File

@@ -55,9 +55,8 @@ class DashSegmentsFD(FragmentFD):
if real_downloader: if real_downloader:
self.to_screen( self.to_screen(
'[%s] Fragment downloads will be delegated to %s' % (self.FD_NAME, real_downloader.get_basename())) '[%s] Fragment downloads will be delegated to %s' % (self.FD_NAME, real_downloader.get_basename()))
info_copy = info_dict.copy() info_dict['fragments'] = fragments_to_download
info_copy['fragments'] = fragments_to_download
fd = real_downloader(self.ydl, self.params) fd = real_downloader(self.ydl, self.params)
return fd.real_download(filename, info_copy) return fd.real_download(filename, info_dict)
return self.download_and_append_fragments(ctx, fragments_to_download, info_dict) return self.download_and_append_fragments(ctx, fragments_to_download, info_dict)

View File

@@ -6,13 +6,7 @@ import subprocess
import sys import sys
import time import time
try: from .fragment import FragmentFD
from Crypto.Cipher import AES
can_decrypt_frag = True
except ImportError:
can_decrypt_frag = False
from .common import FileDownloader
from ..compat import ( from ..compat import (
compat_setenv, compat_setenv,
compat_str, compat_str,
@@ -28,13 +22,12 @@ from ..utils import (
handle_youtubedl_headers, handle_youtubedl_headers,
check_executable, check_executable,
is_outdated_version, is_outdated_version,
process_communicate_or_kill, Popen,
sanitized_Request,
sanitize_open, sanitize_open,
) )
class ExternalFD(FileDownloader): class ExternalFD(FragmentFD):
SUPPORTED_PROTOCOLS = ('http', 'https', 'ftp', 'ftps') SUPPORTED_PROTOCOLS = ('http', 'https', 'ftp', 'ftps')
can_download_to_stdout = False can_download_to_stdout = False
@@ -122,15 +115,20 @@ class ExternalFD(FileDownloader):
self._debug_cmd(cmd) self._debug_cmd(cmd)
if 'fragments' in info_dict: if 'fragments' not in info_dict:
p = Popen(cmd, stderr=subprocess.PIPE)
_, stderr = p.communicate_or_kill()
if p.returncode != 0:
self.to_stderr(stderr.decode('utf-8', 'replace'))
return p.returncode
fragment_retries = self.params.get('fragment_retries', 0) fragment_retries = self.params.get('fragment_retries', 0)
skip_unavailable_fragments = self.params.get('skip_unavailable_fragments', True) skip_unavailable_fragments = self.params.get('skip_unavailable_fragments', True)
count = 0 count = 0
while count <= fragment_retries: while count <= fragment_retries:
p = subprocess.Popen( p = Popen(cmd, stderr=subprocess.PIPE)
cmd, stderr=subprocess.PIPE) _, stderr = p.communicate_or_kill()
_, stderr = process_communicate_or_kill(p)
if p.returncode == 0: if p.returncode == 0:
break break
# TODO: Decide whether to retry based on error code # TODO: Decide whether to retry based on error code
@@ -146,49 +144,25 @@ class ExternalFD(FileDownloader):
self.report_error('Giving up after %s fragment retries' % fragment_retries) self.report_error('Giving up after %s fragment retries' % fragment_retries)
return -1 return -1
decrypt_fragment = self.decrypter(info_dict)
dest, _ = sanitize_open(tmpfilename, 'wb') dest, _ = sanitize_open(tmpfilename, 'wb')
for frag_index, fragment in enumerate(info_dict['fragments']): for frag_index, fragment in enumerate(info_dict['fragments']):
fragment_filename = '%s-Frag%d' % (tmpfilename, frag_index) fragment_filename = '%s-Frag%d' % (tmpfilename, frag_index)
try: try:
src, _ = sanitize_open(fragment_filename, 'rb') src, _ = sanitize_open(fragment_filename, 'rb')
except IOError: except IOError as err:
if skip_unavailable_fragments and frag_index > 1: if skip_unavailable_fragments and frag_index > 1:
self.to_screen('[%s] Skipping fragment %d ...' % (self.get_basename(), frag_index)) self.report_skip_fragment(frag_index, err)
continue continue
self.report_error('Unable to open fragment %d' % frag_index) self.report_error(f'Unable to open fragment {frag_index}; {err}')
return -1 return -1
decrypt_info = fragment.get('decrypt_info') dest.write(decrypt_fragment(fragment, src.read()))
if decrypt_info:
if decrypt_info['METHOD'] == 'AES-128':
iv = decrypt_info.get('IV')
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()
encrypted_data = src.read()
decrypted_data = AES.new(
decrypt_info['KEY'], AES.MODE_CBC, iv).decrypt(encrypted_data)
dest.write(decrypted_data)
else:
fragment_data = src.read()
dest.write(fragment_data)
else:
fragment_data = src.read()
dest.write(fragment_data)
src.close() src.close()
if not self.params.get('keep_fragments', False): if not self.params.get('keep_fragments', False):
os.remove(encodeFilename(fragment_filename)) os.remove(encodeFilename(fragment_filename))
dest.close() dest.close()
os.remove(encodeFilename('%s.frag.urls' % tmpfilename)) os.remove(encodeFilename('%s.frag.urls' % tmpfilename))
else: return 0
p = subprocess.Popen(
cmd, stderr=subprocess.PIPE)
_, stderr = process_communicate_or_kill(p)
if p.returncode != 0:
self.to_stderr(stderr.decode('utf-8', 'replace'))
return p.returncode
def _prepare_url(self, info_dict, url):
headers = info_dict.get('http_headers')
return sanitized_Request(url, None, headers) if headers else url
class CurlFD(ExternalFD): class CurlFD(ExternalFD):
@@ -223,8 +197,8 @@ class CurlFD(ExternalFD):
self._debug_cmd(cmd) self._debug_cmd(cmd)
# curl writes the progress to stderr so don't capture it. # curl writes the progress to stderr so don't capture it.
p = subprocess.Popen(cmd) p = Popen(cmd)
process_communicate_or_kill(p) p.communicate_or_kill()
return p.returncode return p.returncode
@@ -352,12 +326,16 @@ class FFmpegFD(ExternalFD):
# Fixme: This may be wrong when --ffmpeg-location is used # Fixme: This may be wrong when --ffmpeg-location is used
return FFmpegPostProcessor().available return FFmpegPostProcessor().available
@classmethod
def supports(cls, info_dict):
return all(proto in cls.SUPPORTED_PROTOCOLS for proto in info_dict['protocol'].split('+'))
def on_process_started(self, proc, stdin): def on_process_started(self, proc, stdin):
""" Override this in subclasses """ """ Override this in subclasses """
pass pass
@classmethod @classmethod
def can_merge_formats(cls, info_dict, params={}): def can_merge_formats(cls, info_dict, params):
return ( return (
info_dict.get('requested_formats') info_dict.get('requested_formats')
and info_dict.get('protocol') and info_dict.get('protocol')
@@ -496,7 +474,7 @@ class FFmpegFD(ExternalFD):
args.append(encodeFilename(ffpp._ffmpeg_filename_argument(tmpfilename), True)) args.append(encodeFilename(ffpp._ffmpeg_filename_argument(tmpfilename), True))
self._debug_cmd(args) self._debug_cmd(args)
proc = subprocess.Popen(args, stdin=subprocess.PIPE, env=env) proc = Popen(args, stdin=subprocess.PIPE, env=env)
if url in ('-', 'pipe:'): if url in ('-', 'pipe:'):
self.on_process_started(proc, proc.stdin) self.on_process_started(proc, proc.stdin)
try: try:
@@ -508,7 +486,7 @@ class FFmpegFD(ExternalFD):
# 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' and url not in ('-', 'pipe:'): if isinstance(e, KeyboardInterrupt) and sys.platform != 'win32' and url not in ('-', 'pipe:'):
process_communicate_or_kill(proc, b'q') proc.communicate_or_kill(b'q')
else: else:
proc.kill() proc.kill()
proc.wait() proc.wait()
@@ -523,7 +501,7 @@ class AVconvFD(FFmpegFD):
_BY_NAME = dict( _BY_NAME = dict(
(klass.get_basename(), klass) (klass.get_basename(), klass)
for name, klass in globals().items() for name, klass in globals().items()
if name.endswith('FD') and name != 'ExternalFD' if name.endswith('FD') and name not in ('ExternalFD', 'FragmentFD')
) )

View File

@@ -3,12 +3,7 @@ from __future__ import division, unicode_literals
import os import os
import time import time
import json import json
from math import ceil
try:
from Crypto.Cipher import AES
can_decrypt_frag = True
except ImportError:
can_decrypt_frag = False
try: try:
import concurrent.futures import concurrent.futures
@@ -18,6 +13,7 @@ except ImportError:
from .common import FileDownloader from .common import FileDownloader
from .http import HttpFD from .http import HttpFD
from ..aes import aes_cbc_decrypt_bytes
from ..compat import ( from ..compat import (
compat_urllib_error, compat_urllib_error,
compat_struct_pack, compat_struct_pack,
@@ -76,8 +72,9 @@ class FragmentFD(FileDownloader):
'\r[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, err=None):
self.to_screen('[download] Skipping fragment %d ...' % frag_index) err = f' {err};' if err else ''
self.to_screen(f'[download]{err} Skipping fragment {frag_index:d} ...')
def _prepare_url(self, info_dict, url): def _prepare_url(self, info_dict, url):
headers = info_dict.get('http_headers') headers = info_dict.get('http_headers')
@@ -125,6 +122,7 @@ class FragmentFD(FileDownloader):
'url': frag_url, 'url': frag_url,
'http_headers': headers or info_dict.get('http_headers'), 'http_headers': headers or info_dict.get('http_headers'),
'request_data': request_data, 'request_data': request_data,
'ctx_id': ctx.get('ctx_id'),
} }
success = ctx['dl'].download(fragment_filename, fragment_info_dict) success = ctx['dl'].download(fragment_filename, fragment_info_dict)
if not success: if not success:
@@ -224,6 +222,7 @@ class FragmentFD(FileDownloader):
def _start_frag_download(self, ctx, info_dict): def _start_frag_download(self, ctx, info_dict):
resume_len = ctx['complete_frags_downloaded_bytes'] resume_len = ctx['complete_frags_downloaded_bytes']
total_frags = ctx['total_frags'] total_frags = ctx['total_frags']
ctx_id = ctx.get('ctx_id')
# This dict stores the download progress, it's updated by the progress # This dict stores the download progress, it's updated by the progress
# hook # hook
state = { state = {
@@ -247,6 +246,12 @@ class FragmentFD(FileDownloader):
if s['status'] not in ('downloading', 'finished'): if s['status'] not in ('downloading', 'finished'):
return return
if ctx_id is not None and s.get('ctx_id') != ctx_id:
return
state['max_progress'] = ctx.get('max_progress')
state['progress_idx'] = ctx.get('progress_idx')
time_now = time.time() time_now = time.time()
state['elapsed'] = time_now - start state['elapsed'] = time_now - start
frag_total_bytes = s.get('total_bytes') or 0 frag_total_bytes = s.get('total_bytes') or 0
@@ -306,6 +311,9 @@ class FragmentFD(FileDownloader):
'filename': ctx['filename'], 'filename': ctx['filename'],
'status': 'finished', 'status': 'finished',
'elapsed': elapsed, 'elapsed': elapsed,
'ctx_id': ctx.get('ctx_id'),
'max_progress': ctx.get('max_progress'),
'progress_idx': ctx.get('progress_idx'),
}, info_dict) }, info_dict)
def _prepare_external_frag_download(self, ctx): def _prepare_external_frag_download(self, ctx):
@@ -329,7 +337,67 @@ class FragmentFD(FileDownloader):
'fragment_index': 0, 'fragment_index': 0,
}) })
def download_and_append_fragments(self, ctx, fragments, info_dict, *, pack_func=None, finish_func=None): def decrypter(self, info_dict):
_key_cache = {}
def _get_key(url):
if url not in _key_cache:
_key_cache[url] = self.ydl.urlopen(self._prepare_url(info_dict, url)).read()
return _key_cache[url]
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 _get_key(info_dict.get('_decryption_key_url') or decrypt_info['URI'])
# 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
decrypted_data = aes_cbc_decrypt_bytes(frag_content, decrypt_info['KEY'], iv)
return decrypted_data[:-decrypted_data[-1]]
return decrypt_fragment
def download_and_append_fragments_multiple(self, *args, pack_func=None, finish_func=None):
'''
@params (ctx1, fragments1, info_dict1), (ctx2, fragments2, info_dict2), ...
all args must be either tuple or list
'''
max_progress = len(args)
if max_progress == 1:
return self.download_and_append_fragments(*args[0], pack_func=pack_func, finish_func=finish_func)
max_workers = self.params.get('concurrent_fragment_downloads', max_progress)
if max_progress > 1:
self._prepare_multiline_status(max_progress)
def thread_func(idx, ctx, fragments, info_dict, tpe):
ctx['max_progress'] = max_progress
ctx['progress_idx'] = idx
return self.download_and_append_fragments(ctx, fragments, info_dict, pack_func=pack_func, finish_func=finish_func, tpe=tpe)
class FTPE(concurrent.futures.ThreadPoolExecutor):
# has to stop this or it's going to wait on the worker thread itself
def __exit__(self, exc_type, exc_val, exc_tb):
pass
spins = []
for idx, (ctx, fragments, info_dict) in enumerate(args):
tpe = FTPE(ceil(max_workers / max_progress))
job = tpe.submit(thread_func, idx, ctx, fragments, info_dict, tpe)
spins.append((tpe, job))
result = True
for tpe, job in spins:
try:
result = result and job.result()
finally:
tpe.shutdown(wait=True)
return result
def download_and_append_fragments(self, ctx, fragments, info_dict, *, pack_func=None, finish_func=None, tpe=None):
fragment_retries = self.params.get('fragment_retries', 0) 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) is_fatal = (lambda idx: idx == 0) if self.params.get('skip_unavailable_fragments', True) else (lambda _: True)
if not pack_func: if not pack_func:
@@ -337,7 +405,7 @@ class FragmentFD(FileDownloader):
def download_fragment(fragment, ctx): def download_fragment(fragment, ctx):
frag_index = ctx['fragment_index'] = fragment['frag_index'] frag_index = ctx['fragment_index'] = fragment['frag_index']
headers = info_dict.get('http_headers', {}) headers = info_dict.get('http_headers', {}).copy()
byte_range = fragment.get('byte_range') byte_range = fragment.get('byte_range')
if byte_range: if byte_range:
headers['Range'] = 'bytes=%d-%d' % (byte_range['start'], byte_range['end'] - 1) headers['Range'] = 'bytes=%d-%d' % (byte_range['start'], byte_range['end'] - 1)
@@ -374,24 +442,10 @@ class FragmentFD(FileDownloader):
return False, frag_index return False, frag_index
return frag_content, 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): def append_fragment(frag_content, frag_index, ctx):
if not frag_content: if not frag_content:
if not is_fatal(frag_index - 1): if not is_fatal(frag_index - 1):
self.report_skip_fragment(frag_index) self.report_skip_fragment(frag_index, 'fragment not found')
return True return True
else: else:
ctx['dest_stream'].close() ctx['dest_stream'].close()
@@ -401,6 +455,8 @@ class FragmentFD(FileDownloader):
self._append_fragment(ctx, pack_func(frag_content, frag_index)) self._append_fragment(ctx, pack_func(frag_content, frag_index))
return True return True
decrypt_fragment = self.decrypter(info_dict)
max_workers = self.params.get('concurrent_fragment_downloads', 1) max_workers = self.params.get('concurrent_fragment_downloads', 1)
if can_threaded_download and max_workers > 1: if can_threaded_download and max_workers > 1:
@@ -410,7 +466,7 @@ class FragmentFD(FileDownloader):
return fragment, frag_content, frag_index, ctx_copy.get('fragment_filename_sanitized') 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') 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: with tpe or concurrent.futures.ThreadPoolExecutor(max_workers) as pool:
for fragment, frag_content, frag_index, frag_filename in pool.map(_download_fragment, fragments): for fragment, frag_content, frag_index, frag_filename in pool.map(_download_fragment, fragments):
ctx['fragment_filename_sanitized'] = frag_filename ctx['fragment_filename_sanitized'] = frag_filename
ctx['fragment_index'] = frag_index ctx['fragment_index'] = frag_index

View File

@@ -5,10 +5,11 @@ import io
import binascii import binascii
from ..downloader import get_suitable_downloader from ..downloader import get_suitable_downloader
from .fragment import FragmentFD, can_decrypt_frag from .fragment import FragmentFD
from .external import FFmpegFD from .external import FFmpegFD
from ..compat import ( from ..compat import (
compat_pycrypto_AES,
compat_urlparse, compat_urlparse,
) )
from ..utils import ( from ..utils import (
@@ -29,7 +30,7 @@ class HlsFD(FragmentFD):
FD_NAME = 'hlsnative' FD_NAME = 'hlsnative'
@staticmethod @staticmethod
def can_download(manifest, info_dict, allow_unplayable_formats=False, with_crypto=can_decrypt_frag): def can_download(manifest, info_dict, allow_unplayable_formats=False):
UNSUPPORTED_FEATURES = [ UNSUPPORTED_FEATURES = [
# r'#EXT-X-BYTERANGE', # playlists composed of byte ranges of media files [2] # r'#EXT-X-BYTERANGE', # playlists composed of byte ranges of media files [2]
@@ -56,9 +57,6 @@ class HlsFD(FragmentFD):
def check_results(): def check_results():
yield not info_dict.get('is_live') yield not info_dict.get('is_live')
is_aes128_enc = '#EXT-X-KEY:METHOD=AES-128' in manifest
yield with_crypto or not is_aes128_enc
yield not (is_aes128_enc and r'#EXT-X-BYTERANGE' in manifest)
for feature in UNSUPPORTED_FEATURES: for feature in UNSUPPORTED_FEATURES:
yield not re.search(feature, manifest) yield not re.search(feature, manifest)
return all(check_results()) return all(check_results())
@@ -71,16 +69,20 @@ class HlsFD(FragmentFD):
man_url = urlh.geturl() man_url = urlh.geturl()
s = urlh.read().decode('utf-8', 'ignore') s = urlh.read().decode('utf-8', 'ignore')
if not self.can_download(s, info_dict, self.params.get('allow_unplayable_formats')): can_download, message = self.can_download(s, info_dict, self.params.get('allow_unplayable_formats')), None
if info_dict.get('extra_param_to_segment_url') or info_dict.get('_decryption_key_url'): if can_download and not compat_pycrypto_AES and '#EXT-X-KEY:METHOD=AES-128' in s:
self.report_error('pycryptodome not found. Please install') if FFmpegFD.available():
return False can_download, message = False, 'The stream has AES-128 encryption and pycryptodomex is not available'
if self.can_download(s, info_dict, with_crypto=True): else:
self.report_warning('pycryptodome is needed to download this file natively') message = ('The stream has AES-128 encryption and neither ffmpeg nor pycryptodomex are available; '
'Decryption will be performed natively, but will be extremely slow')
if not can_download:
message = message or 'Unsupported features have been detected'
fd = FFmpegFD(self.ydl, self.params) fd = FFmpegFD(self.ydl, self.params)
self.report_warning( self.report_warning(f'{message}; extraction will be delegated to {fd.get_basename()}')
'%s detected unsupported features; extraction will be delegated to %s' % (self.FD_NAME, fd.get_basename()))
return fd.real_download(filename, info_dict) return fd.real_download(filename, info_dict)
elif message:
self.report_warning(message)
is_webvtt = info_dict['ext'] == 'vtt' is_webvtt = info_dict['ext'] == 'vtt'
if is_webvtt: if is_webvtt:
@@ -172,6 +174,7 @@ class HlsFD(FragmentFD):
'byte_range': byte_range, 'byte_range': byte_range,
'media_sequence': media_sequence, 'media_sequence': media_sequence,
}) })
media_sequence += 1
elif line.startswith('#EXT-X-MAP'): elif line.startswith('#EXT-X-MAP'):
if format_index and discontinuity_count != format_index: if format_index and discontinuity_count != format_index:
@@ -196,6 +199,7 @@ class HlsFD(FragmentFD):
'byte_range': byte_range, 'byte_range': byte_range,
'media_sequence': media_sequence 'media_sequence': media_sequence
}) })
media_sequence += 1
if map_info.get('BYTERANGE'): if map_info.get('BYTERANGE'):
splitted_byte_range = map_info.get('BYTERANGE').split('@') splitted_byte_range = map_info.get('BYTERANGE').split('@')
@@ -235,20 +239,18 @@ class HlsFD(FragmentFD):
elif line.startswith('#EXT-X-DISCONTINUITY'): elif line.startswith('#EXT-X-DISCONTINUITY'):
discontinuity_count += 1 discontinuity_count += 1
i += 1 i += 1
media_sequence += 1
# We only download the first fragment during the test # We only download the first fragment during the test
if self.params.get('test', False): 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:
info_copy = info_dict.copy() info_dict['fragments'] = fragments
info_copy['fragments'] = fragments
fd = real_downloader(self.ydl, self.params) fd = real_downloader(self.ydl, self.params)
# 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)
return fd.real_download(filename, info_copy) return fd.real_download(filename, info_dict)
if is_webvtt: if is_webvtt:
def pack_fragment(frag_content, frag_index): def pack_fragment(frag_content, frag_index):

View File

@@ -48,8 +48,9 @@ class HttpFD(FileDownloader):
is_test = self.params.get('test', False) is_test = self.params.get('test', False)
chunk_size = self._TEST_FILE_SIZE if is_test else ( chunk_size = self._TEST_FILE_SIZE if is_test else (
info_dict.get('downloader_options', {}).get('http_chunk_size') self.params.get('http_chunk_size')
or self.params.get('http_chunk_size') or 0) or info_dict.get('downloader_options', {}).get('http_chunk_size')
or 0)
ctx.open_mode = 'wb' ctx.open_mode = 'wb'
ctx.resume_len = 0 ctx.resume_len = 0
@@ -57,6 +58,7 @@ class HttpFD(FileDownloader):
ctx.block_size = self.params.get('buffersize', 1024) ctx.block_size = self.params.get('buffersize', 1024)
ctx.start_time = time.time() ctx.start_time = time.time()
ctx.chunk_size = None ctx.chunk_size = None
throttle_start = None
if self.params.get('continuedl', True): if self.params.get('continuedl', True):
# Establish possible resume length # Establish possible resume length
@@ -189,13 +191,16 @@ class HttpFD(FileDownloader):
# Unexpected HTTP error # Unexpected HTTP error
raise raise
raise RetryDownload(err) raise RetryDownload(err)
except socket.error as err: except socket.timeout as err:
if err.errno != errno.ECONNRESET:
# Connection reset is no problem, just retry
raise
raise RetryDownload(err) raise RetryDownload(err)
except socket.error as err:
if err.errno in (errno.ECONNRESET, errno.ETIMEDOUT):
# Connection reset is no problem, just retry
raise RetryDownload(err)
raise
def download(): def download():
nonlocal throttle_start
data_len = ctx.data.info().get('Content-length', None) data_len = ctx.data.info().get('Content-length', None)
# Range HTTP header may be ignored/unsupported by a webserver # Range HTTP header may be ignored/unsupported by a webserver
@@ -224,7 +229,6 @@ class HttpFD(FileDownloader):
# measure time over whole while-loop, so slow_down() and best_block_size() work together properly # measure time over whole while-loop, so slow_down() and best_block_size() work together properly
now = None # needed for slow_down() in the first loop run now = None # needed for slow_down() in the first loop run
before = start # start measuring before = start # start measuring
throttle_start = None
def retry(e): def retry(e):
to_stdout = ctx.tmpfilename == '-' to_stdout = ctx.tmpfilename == '-'
@@ -310,6 +314,7 @@ class HttpFD(FileDownloader):
'eta': eta, 'eta': eta,
'speed': speed, 'speed': speed,
'elapsed': now - ctx.start_time, 'elapsed': now - ctx.start_time,
'ctx_id': info_dict.get('ctx_id'),
}, info_dict) }, info_dict)
if data_len is not None and byte_counter == data_len: if data_len is not None and byte_counter == data_len:
@@ -324,7 +329,7 @@ class HttpFD(FileDownloader):
if ctx.stream is not None and ctx.tmpfilename != '-': if ctx.stream is not None and ctx.tmpfilename != '-':
ctx.stream.close() ctx.stream.close()
raise ThrottledDownload() raise ThrottledDownload()
else: elif speed:
throttle_start = None throttle_start = None
if not is_test and ctx.chunk_size and ctx.data_len is not None and byte_counter < ctx.data_len: if not is_test and ctx.chunk_size and ctx.data_len is not None and byte_counter < ctx.data_len:
@@ -357,6 +362,7 @@ class HttpFD(FileDownloader):
'filename': ctx.filename, 'filename': ctx.filename,
'status': 'finished', 'status': 'finished',
'elapsed': time.time() - ctx.start_time, 'elapsed': time.time() - ctx.start_time,
'ctx_id': info_dict.get('ctx_id'),
}, info_dict) }, info_dict)
return True return True
@@ -369,6 +375,8 @@ class HttpFD(FileDownloader):
count += 1 count += 1
if count <= retries: if count <= retries:
self.report_retry(e.source_error, count, retries) self.report_retry(e.source_error, count, retries)
else:
self.to_screen(f'[download] Got server HTTP error: {e.source_error}')
continue continue
except NextFragment: except NextFragment:
continue continue

View File

@@ -6,7 +6,7 @@ import threading
from .common import FileDownloader from .common import FileDownloader
from ..downloader import get_suitable_downloader from ..downloader import get_suitable_downloader
from ..extractor.niconico import NiconicoIE from ..extractor.niconico import NiconicoIE
from ..compat import compat_urllib_request from ..utils import sanitized_Request
class NiconicoDmcFD(FileDownloader): class NiconicoDmcFD(FileDownloader):
@@ -29,9 +29,11 @@ class NiconicoDmcFD(FileDownloader):
heartbeat_data = heartbeat_info_dict['data'].encode() heartbeat_data = heartbeat_info_dict['data'].encode()
heartbeat_interval = heartbeat_info_dict.get('interval', 30) heartbeat_interval = heartbeat_info_dict.get('interval', 30)
request = sanitized_Request(heartbeat_url, heartbeat_data)
def heartbeat(): def heartbeat():
try: try:
compat_urllib_request.urlopen(url=heartbeat_url, data=heartbeat_data) self.ydl.urlopen(request).read()
except Exception: except Exception:
self.to_screen('[%s] Heartbeat failed' % self.FD_NAME) self.to_screen('[%s] Heartbeat failed' % self.FD_NAME)

View File

@@ -12,6 +12,7 @@ from ..utils import (
encodeFilename, encodeFilename,
encodeArgument, encodeArgument,
get_exe_version, get_exe_version,
Popen,
) )
@@ -26,7 +27,7 @@ class RtmpFD(FileDownloader):
start = time.time() start = time.time()
resume_percent = None resume_percent = None
resume_downloaded_data_len = None resume_downloaded_data_len = None
proc = subprocess.Popen(args, stderr=subprocess.PIPE) proc = Popen(args, stderr=subprocess.PIPE)
cursor_in_new_line = True cursor_in_new_line = True
proc_stderr_closed = False proc_stderr_closed = False
try: try:

View File

@@ -183,7 +183,7 @@ class YoutubeLiveChatFD(FragmentFD):
request_data['currentPlayerState'] = {'playerOffsetMs': str(max(offset - 5000, 0))} request_data['currentPlayerState'] = {'playerOffsetMs': str(max(offset - 5000, 0))}
if click_tracking_params: if click_tracking_params:
request_data['context']['clickTracking'] = {'clickTrackingParams': click_tracking_params} request_data['context']['clickTracking'] = {'clickTrackingParams': click_tracking_params}
headers = ie.generate_api_headers(ytcfg, visitor_data=visitor_data) headers = ie.generate_api_headers(ytcfg=ytcfg, visitor_data=visitor_data)
headers.update({'content-type': 'application/json'}) headers.update({'content-type': 'application/json'})
fragment_request_data = json.dumps(request_data, ensure_ascii=False).encode('utf-8') + b'\n' fragment_request_data = json.dumps(request_data, ensure_ascii=False).encode('utf-8') + b'\n'
success, continuation_id, offset, click_tracking_params = download_and_parse_fragment( success, continuation_id, offset, click_tracking_params = download_and_parse_fragment(

View File

@@ -1,14 +1,15 @@
from __future__ import unicode_literals import os
from ..utils import load_plugins from ..utils import load_plugins
try: _LAZY_LOADER = False
if not os.environ.get('YTDLP_NO_LAZY_EXTRACTORS'):
try:
from .lazy_extractors import * from .lazy_extractors import *
from .lazy_extractors import _ALL_CLASSES from .lazy_extractors import _ALL_CLASSES
_LAZY_LOADER = True _LAZY_LOADER = True
_PLUGIN_CLASSES = [] except ImportError:
except ImportError: pass
_LAZY_LOADER = False
if not _LAZY_LOADER: if not _LAZY_LOADER:
from .extractors import * from .extractors import *
@@ -19,8 +20,8 @@ if not _LAZY_LOADER:
] ]
_ALL_CLASSES.append(GenericIE) _ALL_CLASSES.append(GenericIE)
_PLUGIN_CLASSES = load_plugins('extractor', 'IE', globals()) _PLUGIN_CLASSES = load_plugins('extractor', 'IE', globals())
_ALL_CLASSES = _PLUGIN_CLASSES + _ALL_CLASSES _ALL_CLASSES = list(_PLUGIN_CLASSES.values()) + _ALL_CLASSES
def gen_extractor_classes(): def gen_extractor_classes():

View File

@@ -15,6 +15,7 @@ from ..compat import (
compat_ord, compat_ord,
) )
from ..utils import ( from ..utils import (
ass_subtitles_timecode,
bytes_to_intlist, bytes_to_intlist,
bytes_to_long, bytes_to_long,
ExtractorError, ExtractorError,
@@ -68,10 +69,6 @@ class ADNIE(InfoExtractor):
'end': 4, 'end': 4,
} }
@staticmethod
def _ass_subtitles_timecode(seconds):
return '%01d:%02d:%02d.%02d' % (seconds / 3600, (seconds % 3600) / 60, seconds % 60, (seconds % 1) * 100)
def _get_subtitles(self, sub_url, video_id): def _get_subtitles(self, sub_url, video_id):
if not sub_url: if not sub_url:
return None return None
@@ -117,8 +114,8 @@ Format: Marked,Start,End,Style,Name,MarginL,MarginR,MarginV,Effect,Text'''
continue continue
alignment = self._POS_ALIGN_MAP.get(position_align, 2) + self._LINE_ALIGN_MAP.get(line_align, 0) alignment = self._POS_ALIGN_MAP.get(position_align, 2) + self._LINE_ALIGN_MAP.get(line_align, 0)
ssa += os.linesep + 'Dialogue: Marked=0,%s,%s,Default,,0,0,0,,%s%s' % ( ssa += os.linesep + 'Dialogue: Marked=0,%s,%s,Default,,0,0,0,,%s%s' % (
self._ass_subtitles_timecode(start), ass_subtitles_timecode(start),
self._ass_subtitles_timecode(end), ass_subtitles_timecode(end),
'{\\a%d}' % alignment if alignment != 2 else '', '{\\a%d}' % alignment if alignment != 2 else '',
text.replace('\n', '\\N').replace('<i>', '{\\i1}').replace('</i>', '{\\i0}')) text.replace('\n', '\\N').replace('<i>', '{\\i1}').replace('</i>', '{\\i0}'))

View File

@@ -37,6 +37,11 @@ MSO_INFO = {
'username_field': 'email', 'username_field': 'email',
'password_field': 'loginpassword', 'password_field': 'loginpassword',
}, },
'RCN': {
'name': 'RCN',
'username_field': 'username',
'password_field': 'password',
},
'Rogers': { 'Rogers': {
'name': 'Rogers', 'name': 'Rogers',
'username_field': 'UserName', 'username_field': 'UserName',

View File

@@ -174,7 +174,7 @@ class ArteTVIE(ArteTVBaseIE):
return { return {
'id': player_info.get('VID') or video_id, 'id': player_info.get('VID') or video_id,
'title': title, 'title': title,
'description': player_info.get('VDE'), 'description': player_info.get('VDE') or player_info.get('V7T'),
'upload_date': unified_strdate(upload_date_str), 'upload_date': unified_strdate(upload_date_str),
'thumbnail': player_info.get('programImage') or player_info.get('VTU', {}).get('IUR'), 'thumbnail': player_info.get('programImage') or player_info.get('VTU', {}).get('IUR'),
'formats': formats, 'formats': formats,

View File

@@ -1,74 +1,106 @@
# coding: utf-8 # coding: utf-8
from __future__ import unicode_literals from __future__ import unicode_literals
import datetime
from .common import InfoExtractor from .common import InfoExtractor
from ..utils import ( from ..utils import (
determine_ext, float_or_none,
dict_get, jwt_encode_hs256,
int_or_none, try_get,
unescapeHTML,
) )
class ATVAtIE(InfoExtractor): class ATVAtIE(InfoExtractor):
_VALID_URL = r'https?://(?:www\.)?atv\.at/(?:[^/]+/){2}(?P<id>[dv]\d+)' _VALID_URL = r'https?://(?:www\.)?atv\.at/tv/(?:[^/]+/){2,3}(?P<id>.*)'
_TESTS = [{ _TESTS = [{
'url': 'https://www.atv.at/bauer-sucht-frau-die-zweite-chance/folge-1/d3390693/', 'url': 'https://www.atv.at/tv/bauer-sucht-frau/staffel-18/bauer-sucht-frau/bauer-sucht-frau-staffel-18-folge-3-die-hofwochen',
'md5': 'c471605591009dfb6e6c54f7e62e2807', 'md5': '3c3b4aaca9f63e32b35e04a9c2515903',
'info_dict': { 'info_dict': {
'id': '3390684', 'id': 'v-ce9cgn1e70n5-1',
'ext': 'mp4', 'ext': 'mp4',
'title': 'Bauer sucht Frau - Die zweite Chance Folge 1', 'title': 'Bauer sucht Frau - Staffel 18 Folge 3 - Die Hofwochen',
} }
}, { }, {
'url': 'https://www.atv.at/bauer-sucht-frau-staffel-17/fuenfte-eventfolge/d3339537/', 'url': 'https://www.atv.at/tv/bauer-sucht-frau/staffel-18/episode-01/bauer-sucht-frau-staffel-18-vorstellungsfolge-1',
'only_matching': True, 'only_matching': True,
}] }]
def _process_source_entry(self, source, part_id): # extracted from bootstrap.js function (search for e.encryption_key and use your browser's debugger)
source_url = source.get('url') _ACCESS_ID = 'x_atv'
if not source_url: _ENCRYPTION_KEY = 'Hohnaekeishoogh2omaeghooquooshia'
return
if determine_ext(source_url) == 'm3u8':
return self._extract_m3u8_formats(
source_url, part_id, 'mp4', 'm3u8_native',
m3u8_id='hls', fatal=False)
else:
return [{
'url': source_url,
}]
def _process_entry(self, entry): def _extract_video_info(self, url, content, video):
part_id = entry.get('id') clip_id = content.get('splitId', content['id'])
if not part_id:
return
formats = [] formats = []
for source in entry.get('sources', []): clip_urls = video['urls']
formats.extend(self._process_source_entry(source, part_id) or []) for protocol, variant in clip_urls.items():
source_url = try_get(variant, lambda x: x['clear']['url'])
if not source_url:
continue
if protocol == 'dash':
formats.extend(self._extract_mpd_formats(
source_url, clip_id, mpd_id=protocol, fatal=False))
elif protocol == 'hls':
formats.extend(self._extract_m3u8_formats(
source_url, clip_id, 'mp4', 'm3u8_native',
m3u8_id=protocol, fatal=False))
else:
formats.append({
'url': source_url,
'format_id': protocol,
})
self._sort_formats(formats) self._sort_formats(formats)
return { return {
'id': part_id, 'id': clip_id,
'title': entry.get('title'), 'title': content.get('title'),
'duration': int_or_none(entry.get('duration')), 'duration': float_or_none(content.get('duration')),
'formats': formats 'series': content.get('tvShowTitle'),
'formats': formats,
} }
def _real_extract(self, url): def _real_extract(self, url):
display_id = self._match_id(url) video_id = self._match_id(url)
webpage = self._download_webpage(url, display_id) webpage = self._download_webpage(url, video_id)
video_data = self._parse_json(unescapeHTML(self._search_regex( json_data = self._parse_json(
r'var\splaylist\s*=\s*(?P<json>\[.*\]);', self._search_regex(r'<script id="state" type="text/plain">(.*)</script>', webpage, 'json_data'),
webpage, 'player data', group='json')), video_id=video_id)
display_id)
first_video = video_data[0] video_title = json_data['views']['default']['page']['title']
video_id = first_video['id'] contentResource = json_data['views']['default']['page']['contentResource']
video_title = dict_get(first_video, ('tvShowTitle', 'title')) content_id = contentResource[0]['id']
content_ids = [{'id': id, 'subclip_start': content['start'], 'subclip_end': content['end']}
for id, content in enumerate(contentResource)]
time_of_request = datetime.datetime.now()
not_before = time_of_request - datetime.timedelta(minutes=5)
expire = time_of_request + datetime.timedelta(minutes=5)
payload = {
'content_ids': {
content_id: content_ids,
},
'secure_delivery': True,
'iat': int(time_of_request.timestamp()),
'nbf': int(not_before.timestamp()),
'exp': int(expire.timestamp()),
}
jwt_token = jwt_encode_hs256(payload, self._ENCRYPTION_KEY, headers={'kid': self._ACCESS_ID})
videos = self._download_json(
'https://vas-v4.p7s1video.net/4.0/getsources',
content_id, 'Downloading videos JSON', query={
'token': jwt_token.decode('utf-8')
})
video_id, videos_data = list(videos['data'].items())[0]
entries = [
self._extract_video_info(url, contentResource[video['id']], video)
for video in videos_data]
return { return {
'_type': 'multi_video', '_type': 'multi_video',
'id': video_id, 'id': video_id,
'title': video_title, 'title': video_title,
'entries': (self._process_entry(entry) for entry in video_data), 'entries': entries,
} }

View File

@@ -97,21 +97,16 @@ query GetCommentReplies($id: String!) {
'query': self._GRAPHQL_QUERIES[operation] 'query': self._GRAPHQL_QUERIES[operation]
}).encode('utf8')).get('data') }).encode('utf8')).get('data')
def _extract_comments(self, video_id, comments, comment_data): def _get_comments(self, video_id, comments, comment_data):
yield from comments
for comment in comment_data.copy(): for comment in comment_data.copy():
comment_id = comment.get('_id') comment_id = comment.get('_id')
if comment.get('replyCount') > 0: if comment.get('replyCount') > 0:
reply_json = self._call_api( reply_json = self._call_api(
video_id, comment_id, 'GetCommentReplies', video_id, comment_id, 'GetCommentReplies',
f'Downloading replies for comment {comment_id}') f'Downloading replies for comment {comment_id}')
comments.extend( for reply in reply_json.get('getCommentReplies'):
self._parse_comment(reply, comment_id) yield self._parse_comment(reply, comment_id)
for reply in reply_json.get('getCommentReplies'))
return {
'comments': comments,
'comment_count': len(comments),
}
@staticmethod @staticmethod
def _parse_comment(comment_data, parent): def _parse_comment(comment_data, parent):
@@ -159,7 +154,5 @@ query GetCommentReplies($id: String!) {
'tags': [tag.get('name') for tag in video_info.get('tags')], 'tags': [tag.get('name') for tag in video_info.get('tags')],
'availability': self._availability(is_unlisted=video_info.get('unlisted')), 'availability': self._availability(is_unlisted=video_info.get('unlisted')),
'comments': comments, 'comments': comments,
'__post_extractor': ( '__post_extractor': self.extract_comments(video_id, comments, video_json.get('getVideoComments'))
(lambda: self._extract_comments(video_id, comments, video_json.get('getVideoComments')))
if self.get_param('getcomments') else None)
} }

View File

@@ -588,8 +588,8 @@ class BBCIE(BBCCoUkIE):
_VALID_URL = r'https?://(?:www\.)?bbc\.(?:com|co\.uk)/(?:[^/]+/)+(?P<id>[^/#?]+)' _VALID_URL = r'https?://(?:www\.)?bbc\.(?:com|co\.uk)/(?:[^/]+/)+(?P<id>[^/#?]+)'
_MEDIA_SETS = [ _MEDIA_SETS = [
'mobile-tablet-main',
'pc', 'pc',
'mobile-tablet-main',
] ]
_TESTS = [{ _TESTS = [{

View File

@@ -1,16 +1,13 @@
# coding: utf-8 # coding: utf-8
from __future__ import unicode_literals
import hashlib import hashlib
import itertools import itertools
import json
import functools import functools
import re import re
import math import math
from .common import InfoExtractor, SearchInfoExtractor from .common import InfoExtractor, SearchInfoExtractor
from ..compat import ( from ..compat import (
compat_str,
compat_parse_qs, compat_parse_qs,
compat_urlparse, compat_urlparse,
compat_urllib_parse_urlparse compat_urllib_parse_urlparse
@@ -20,9 +17,12 @@ from ..utils import (
int_or_none, int_or_none,
float_or_none, float_or_none,
parse_iso8601, parse_iso8601,
traverse_obj,
try_get, try_get,
smuggle_url, smuggle_url,
srt_subtitles_timecode,
str_or_none, str_or_none,
str_to_int,
strip_jsonp, strip_jsonp,
unified_timestamp, unified_timestamp,
unsmuggle_url, unsmuggle_url,
@@ -99,7 +99,7 @@ class BiliBiliIE(InfoExtractor):
'upload_date': '20170301', 'upload_date': '20170301',
}, },
'params': { 'params': {
'skip_download': True, # Test metadata only 'skip_download': True,
}, },
}, { }, {
'info_dict': { 'info_dict': {
@@ -113,7 +113,7 @@ class BiliBiliIE(InfoExtractor):
'upload_date': '20170301', 'upload_date': '20170301',
}, },
'params': { 'params': {
'skip_download': True, # Test metadata only 'skip_download': True,
}, },
}] }]
}, { }, {
@@ -167,7 +167,7 @@ class BiliBiliIE(InfoExtractor):
if 'anime/' not in url: if 'anime/' not in url:
cid = self._search_regex( cid = self._search_regex(
r'\bcid(?:["\']:|=)(\d+),["\']page(?:["\']:|=)' + compat_str(page_id), webpage, 'cid', r'\bcid(?:["\']:|=)(\d+),["\']page(?:["\']:|=)' + str(page_id), webpage, 'cid',
default=None default=None
) or self._search_regex( ) or self._search_regex(
r'\bcid(?:["\']:|=)(\d+)', webpage, 'cid', r'\bcid(?:["\']:|=)(\d+)', webpage, 'cid',
@@ -257,7 +257,7 @@ class BiliBiliIE(InfoExtractor):
# TODO: The json is already downloaded by _extract_anthology_entries. Don't redownload for each video # TODO: The json is already downloaded by _extract_anthology_entries. Don't redownload for each video
part_title = try_get( part_title = try_get(
self._download_json( self._download_json(
"https://api.bilibili.com/x/player/pagelist?bvid=%s&jsonp=jsonp" % bv_id, f'https://api.bilibili.com/x/player/pagelist?bvid={bv_id}&jsonp=jsonp',
video_id, note='Extracting videos in anthology'), video_id, note='Extracting videos in anthology'),
lambda x: x['data'][int(page_id) - 1]['part']) lambda x: x['data'][int(page_id) - 1]['part'])
title = part_title or title title = part_title or title
@@ -271,7 +271,7 @@ class BiliBiliIE(InfoExtractor):
# TODO 'view_count' requires deobfuscating Javascript # TODO 'view_count' requires deobfuscating Javascript
info = { info = {
'id': compat_str(video_id) if page_id is None else '%s_p%s' % (video_id, page_id), 'id': str(video_id) if page_id is None else '%s_part%s' % (video_id, page_id),
'cid': cid, 'cid': cid,
'title': title, 'title': title,
'description': description, 'description': description,
@@ -293,29 +293,25 @@ class BiliBiliIE(InfoExtractor):
info['uploader'] = self._html_search_meta( info['uploader'] = self._html_search_meta(
'author', webpage, 'uploader', default=None) 'author', webpage, 'uploader', default=None)
raw_danmaku = self._get_raw_danmaku(video_id, cid)
raw_tags = self._get_tags(video_id)
tags = list(map(lambda x: x['tag_name'], raw_tags))
top_level_info = { top_level_info = {
'raw_danmaku': raw_danmaku, 'tags': traverse_obj(self._download_json(
'tags': tags, f'https://api.bilibili.com/x/tag/archive/tags?aid={video_id}',
'raw_tags': raw_tags, video_id, fatal=False, note='Downloading tags'), ('data', ..., 'tag_name')),
}
if self.get_param('getcomments', False):
def get_comments():
comments = self._get_all_comment_pages(video_id)
return {
'comments': comments,
'comment_count': len(comments)
} }
top_level_info['__post_extractor'] = get_comments entries[0]['subtitles'] = {
'danmaku': [{
'ext': 'xml',
'url': f'https://comment.bilibili.com/{cid}.xml',
}]
}
''' r'''
# Requires https://github.com/m13253/danmaku2ass which is licenced under GPL3 # Requires https://github.com/m13253/danmaku2ass which is licenced under GPL3
# See https://github.com/animelover1984/youtube-dl # See https://github.com/animelover1984/youtube-dl
raw_danmaku = self._download_webpage(
f'https://comment.bilibili.com/{cid}.xml', video_id, fatal=False, note='Downloading danmaku comments')
danmaku = NiconicoIE.CreateDanmaku(raw_danmaku, commentType='Bilibili', x=1024, y=576) danmaku = NiconicoIE.CreateDanmaku(raw_danmaku, commentType='Bilibili', x=1024, y=576)
entries[0]['subtitles'] = { entries[0]['subtitles'] = {
'danmaku': [{ 'danmaku': [{
@@ -325,40 +321,38 @@ class BiliBiliIE(InfoExtractor):
} }
''' '''
top_level_info['__post_extractor'] = self.extract_comments(video_id)
for entry in entries: for entry in entries:
entry.update(info) entry.update(info)
if len(entries) == 1: if len(entries) == 1:
entries[0].update(top_level_info) entries[0].update(top_level_info)
return entries[0] return entries[0]
else:
for idx, entry in enumerate(entries): for idx, entry in enumerate(entries):
entry['id'] = '%s_part%d' % (video_id, (idx + 1)) entry['id'] = '%s_part%d' % (video_id, (idx + 1))
global_info = { return {
'_type': 'multi_video', '_type': 'multi_video',
'id': compat_str(video_id), 'id': str(video_id),
'bv_id': bv_id, 'bv_id': bv_id,
'title': title, 'title': title,
'description': description, 'description': description,
'entries': entries, 'entries': entries,
**info, **top_level_info
} }
global_info.update(info)
global_info.update(top_level_info)
return global_info
def _extract_anthology_entries(self, bv_id, video_id, webpage): def _extract_anthology_entries(self, bv_id, video_id, webpage):
title = self._html_search_regex( title = self._html_search_regex(
(r'<h1[^>]+\btitle=(["\'])(?P<title>(?:(?!\1).)+)\1', (r'<h1[^>]+\btitle=(["\'])(?P<title>(?:(?!\1).)+)\1',
r'(?s)<h1[^>]*>(?P<title>.+?)</h1>'), webpage, 'title', r'(?s)<h1[^>]*>(?P<title>.+?)</h1>'), webpage, 'title',
group='title') group='title')
json_data = self._download_json( json_data = self._download_json(
"https://api.bilibili.com/x/player/pagelist?bvid=%s&jsonp=jsonp" % bv_id, f'https://api.bilibili.com/x/player/pagelist?bvid={bv_id}&jsonp=jsonp',
video_id, note='Extracting videos in anthology') video_id, note='Extracting videos in anthology')
if len(json_data['data']) > 1: if json_data['data']:
return self.playlist_from_matches( return self.playlist_from_matches(
json_data['data'], bv_id, title, ie=BiliBiliIE.ie_key(), json_data['data'], bv_id, title, ie=BiliBiliIE.ie_key(),
getter=lambda entry: 'https://www.bilibili.com/video/%s?p=%d' % (bv_id, entry['page'])) getter=lambda entry: 'https://www.bilibili.com/video/%s?p=%d' % (bv_id, entry['page']))
@@ -373,65 +367,31 @@ class BiliBiliIE(InfoExtractor):
if response['code'] == -400: if response['code'] == -400:
raise ExtractorError('Video ID does not exist', expected=True, video_id=id) raise ExtractorError('Video ID does not exist', expected=True, video_id=id)
elif response['code'] != 0: elif response['code'] != 0:
raise ExtractorError('Unknown error occurred during API check (code %s)' % response['code'], expected=True, video_id=id) raise ExtractorError(f'Unknown error occurred during API check (code {response["code"]})',
return (response['data']['aid'], response['data']['bvid']) expected=True, video_id=id)
return response['data']['aid'], response['data']['bvid']
# recursive solution to getting every page of comments for the video def _get_comments(self, video_id, commentPageNumber=0):
# we can stop when we reach a page without any comments for idx in itertools.count(1):
def _get_all_comment_pages(self, video_id, commentPageNumber=0): replies = traverse_obj(
comment_url = "https://api.bilibili.com/x/v2/reply?jsonp=jsonp&pn=%s&type=1&oid=%s&sort=2&_=1567227301685" % (commentPageNumber, video_id) self._download_json(
json_str = self._download_webpage( f'https://api.bilibili.com/x/v2/reply?pn={idx}&oid={video_id}&type=1&jsonp=jsonp&sort=2&_=1567227301685',
comment_url, video_id, video_id, note=f'Extracting comments from page {idx}'),
note='Extracting comments from page %s' % (commentPageNumber)) ('data', 'replies')) or []
replies = json.loads(json_str)['data']['replies'] for children in map(self._get_all_children, replies):
if replies is None: yield from children
return []
return self._get_all_children(replies) + self._get_all_comment_pages(video_id, commentPageNumber + 1)
# extracts all comments in the tree def _get_all_children(self, reply):
def _get_all_children(self, replies): yield {
if replies is None: 'author': traverse_obj(reply, ('member', 'uname')),
return [] 'author_id': traverse_obj(reply, ('member', 'mid')),
'id': reply.get('rpid'),
ret = [] 'text': traverse_obj(reply, ('content', 'message')),
for reply in replies: 'timestamp': reply.get('ctime'),
author = reply['member']['uname'] 'parent': reply.get('parent') or 'root',
author_id = reply['member']['mid']
id = reply['rpid']
text = reply['content']['message']
timestamp = reply['ctime']
parent = reply['parent'] if reply['parent'] != 0 else 'root'
comment = {
"author": author,
"author_id": author_id,
"id": id,
"text": text,
"timestamp": timestamp,
"parent": parent,
} }
ret.append(comment) for children in map(self._get_all_children, reply.get('replies') or []):
yield from children
# from the JSON, the comment structure seems arbitrarily deep, but I could be wrong.
# Regardless, this should work.
ret += self._get_all_children(reply['replies'])
return ret
def _get_raw_danmaku(self, video_id, cid):
# This will be useful if I decide to scrape all pages instead of doing them individually
# cid_url = "https://www.bilibili.com/widget/getPageList?aid=%s" % (video_id)
# cid_str = self._download_webpage(cid_url, video_id, note=False)
# cid = json.loads(cid_str)[0]['cid']
danmaku_url = "https://comment.bilibili.com/%s.xml" % (cid)
danmaku = self._download_webpage(danmaku_url, video_id, note='Downloading danmaku comments')
return danmaku
def _get_tags(self, video_id):
tags_url = "https://api.bilibili.com/x/tag/archive/tags?aid=%s" % (video_id)
tags_json = self._download_json(tags_url, video_id, note='Downloading tags')
return tags_json['data']
class BiliBiliBangumiIE(InfoExtractor): class BiliBiliBangumiIE(InfoExtractor):
@@ -514,11 +474,8 @@ class BilibiliChannelIE(InfoExtractor):
count, max_count = 0, None count, max_count = 0, None
for page_num in itertools.count(1): for page_num in itertools.count(1):
data = self._parse_json( data = self._download_json(
self._download_webpage( self._API_URL % (list_id, page_num), list_id, note=f'Downloading page {page_num}')['data']
self._API_URL % (list_id, page_num), list_id,
note='Downloading page %d' % page_num),
list_id)['data']
max_count = max_count or try_get(data, lambda x: x['page']['count']) max_count = max_count or try_get(data, lambda x: x['page']['count'])
@@ -581,11 +538,11 @@ class BilibiliCategoryIE(InfoExtractor):
} }
if category not in rid_map: if category not in rid_map:
raise ExtractorError('The supplied category, %s, is not supported. List of supported categories: %s' % (category, list(rid_map.keys()))) raise ExtractorError(
f'The category {category} isn\'t supported. Supported categories: {list(rid_map.keys())}')
if subcategory not in rid_map[category]: if subcategory not in rid_map[category]:
raise ExtractorError('The subcategory, %s, isn\'t supported for this category. Supported subcategories: %s' % (subcategory, list(rid_map[category].keys()))) raise ExtractorError(
f'The subcategory {subcategory} isn\'t supported for this category. Supported subcategories: {list(rid_map[category].keys())}')
rid_value = rid_map[category][subcategory] rid_value = rid_map[category][subcategory]
api_url = 'https://api.bilibili.com/x/web-interface/newlist?rid=%d&type=1&ps=20&jsonp=jsonp' % rid_value api_url = 'https://api.bilibili.com/x/web-interface/newlist?rid=%d&type=1&ps=20&jsonp=jsonp' % rid_value
@@ -612,41 +569,26 @@ class BiliBiliSearchIE(SearchInfoExtractor):
IE_DESC = 'Bilibili video search, "bilisearch" keyword' IE_DESC = 'Bilibili video search, "bilisearch" keyword'
_MAX_RESULTS = 100000 _MAX_RESULTS = 100000
_SEARCH_KEY = 'bilisearch' _SEARCH_KEY = 'bilisearch'
MAX_NUMBER_OF_RESULTS = 1000
def _get_n_results(self, query, n): def _search_results(self, query):
"""Get a specified number of results for a query""" for page_num in itertools.count(1):
videos = self._download_json(
entries = [] 'https://api.bilibili.com/x/web-interface/search/type', query,
pageNumber = 0 note=f'Extracting results from page {page_num}', query={
while True: 'Search_key': query,
pageNumber += 1 'keyword': query,
# FIXME 'page': page_num,
api_url = "https://api.bilibili.com/x/web-interface/search/type?context=&page=%s&order=pubdate&keyword=%s&duration=0&tids_2=&__refresh__=true&search_type=video&tids=0&highlight=1" % (pageNumber, query) 'context': '',
json_str = self._download_webpage( 'order': 'pubdate',
api_url, "None", query={"Search_key": query}, 'duration': 0,
note='Extracting results from page %s' % pageNumber) 'tids_2': '',
data = json.loads(json_str)['data'] '__refresh__': 'true',
'search_type': 'video',
# FIXME: this is hideous 'tids': 0,
if "result" not in data: 'highlight': 1,
return { })['data'].get('result') or []
'_type': 'playlist',
'id': query,
'entries': entries[:n]
}
videos = data['result']
for video in videos: for video in videos:
e = self.url_result(video['arcurl'], 'BiliBili', compat_str(video['aid'])) yield self.url_result(video['arcurl'], 'BiliBili', str(video['aid']))
entries.append(e)
if(len(entries) >= n or len(videos) >= BiliBiliSearchIE.MAX_NUMBER_OF_RESULTS):
return {
'_type': 'playlist',
'id': query,
'entries': entries[:n]
}
class BilibiliAudioBaseIE(InfoExtractor): class BilibiliAudioBaseIE(InfoExtractor):
@@ -774,3 +716,152 @@ class BiliBiliPlayerIE(InfoExtractor):
return self.url_result( return self.url_result(
'http://www.bilibili.tv/video/av%s/' % video_id, 'http://www.bilibili.tv/video/av%s/' % video_id,
ie=BiliBiliIE.ie_key(), video_id=video_id) ie=BiliBiliIE.ie_key(), video_id=video_id)
class BiliIntlBaseIE(InfoExtractor):
_API_URL = 'https://api.bili{}/intl/gateway{}'
def _call_api(self, type, endpoint, id):
return self._download_json(self._API_URL.format(type, endpoint), id)['data']
def json2srt(self, json):
data = '\n\n'.join(
f'{i + 1}\n{srt_subtitles_timecode(line["from"])} --> {srt_subtitles_timecode(line["to"])}\n{line["content"]}'
for i, line in enumerate(json['body']))
return data
def _get_subtitles(self, type, ep_id):
sub_json = self._call_api(type, f'/m/subtitle?ep_id={ep_id}&platform=web', ep_id)
subtitles = {}
for sub in sub_json.get('subtitles', []):
sub_url = sub.get('url')
if not sub_url:
continue
sub_data = self._download_json(sub_url, ep_id, fatal=False)
if not sub_data:
continue
subtitles.setdefault(sub.get('key', 'en'), []).append({
'ext': 'srt',
'data': self.json2srt(sub_data)
})
return subtitles
def _get_formats(self, type, ep_id):
video_json = self._call_api(type, f'/web/playurl?ep_id={ep_id}&platform=web', ep_id)
if not video_json:
self.raise_login_required(method='cookies')
video_json = video_json['playurl']
formats = []
for vid in video_json.get('video', []):
video_res = vid.get('video_resource') or {}
video_info = vid.get('stream_info') or {}
if not video_res.get('url'):
continue
formats.append({
'url': video_res['url'],
'ext': 'mp4',
'format_note': video_info.get('desc_words'),
'width': video_res.get('width'),
'height': video_res.get('height'),
'vbr': video_res.get('bandwidth'),
'acodec': 'none',
'vcodec': video_res.get('codecs'),
'filesize': video_res.get('size'),
})
for aud in video_json.get('audio_resource', []):
if not aud.get('url'):
continue
formats.append({
'url': aud['url'],
'ext': 'mp4',
'abr': aud.get('bandwidth'),
'acodec': aud.get('codecs'),
'vcodec': 'none',
'filesize': aud.get('size'),
})
self._sort_formats(formats)
return formats
def _extract_ep_info(self, type, episode_data, ep_id):
return {
'id': ep_id,
'title': episode_data.get('long_title') or episode_data['title'],
'thumbnail': episode_data.get('cover'),
'episode_number': str_to_int(episode_data.get('title')),
'formats': self._get_formats(type, ep_id),
'subtitles': self._get_subtitles(type, ep_id),
'extractor_key': BiliIntlIE.ie_key(),
}
class BiliIntlIE(BiliIntlBaseIE):
_VALID_URL = r'https?://(?:www\.)?bili(?P<type>bili\.tv|intl.com)/(?:[a-z]{2}/)?play/(?P<season_id>\d+)/(?P<id>\d+)'
_TESTS = [{
'url': 'https://www.bilibili.tv/en/play/34613/341736',
'info_dict': {
'id': '341736',
'ext': 'mp4',
'title': 'The First Night',
'thumbnail': 'https://i0.hdslb.com/bfs/intl/management/91e30e5521235d9b163339a26a0b030ebda54310.png',
'episode_number': 2,
},
'params': {
'format': 'bv',
},
}, {
'url': 'https://www.biliintl.com/en/play/34613/341736',
'info_dict': {
'id': '341736',
'ext': 'mp4',
'title': 'The First Night',
'thumbnail': 'https://i0.hdslb.com/bfs/intl/management/91e30e5521235d9b163339a26a0b030ebda54310.png',
'episode_number': 2,
},
'params': {
'format': 'bv',
},
}]
def _real_extract(self, url):
type, season_id, id = self._match_valid_url(url).groups()
data_json = self._call_api(type, f'/web/view/ogv_collection?season_id={season_id}', id)
episode_data = next(
episode for episode in data_json.get('episodes', [])
if str(episode.get('ep_id')) == id)
return self._extract_ep_info(type, episode_data, id)
class BiliIntlSeriesIE(BiliIntlBaseIE):
_VALID_URL = r'https?://(?:www\.)?bili(?P<type>bili\.tv|intl.com)/(?:[a-z]{2}/)?play/(?P<id>\d+)$'
_TESTS = [{
'url': 'https://www.bilibili.tv/en/play/34613',
'playlist_mincount': 15,
'info_dict': {
'id': '34613',
},
'params': {
'skip_download': True,
'format': 'bv',
},
}, {
'url': 'https://www.biliintl.com/en/play/34613',
'playlist_mincount': 15,
'info_dict': {
'id': '34613',
},
'params': {
'skip_download': True,
'format': 'bv',
},
}]
def _entries(self, id, type):
data_json = self._call_api(type, f'/web/view/ogv_collection?season_id={id}', id)
for episode in data_json.get('episodes', []):
episode_id = str(episode.get('ep_id'))
yield self._extract_ep_info(type, episode, episode_id)
def _real_extract(self, url):
type, id = self._match_valid_url(url).groups()
return self.playlist_result(self._entries(id, type), playlist_id=id)

View File

@@ -472,7 +472,7 @@ class BrightcoveNewIE(AdobePassIE):
title = json_data['name'].strip() title = json_data['name'].strip()
num_drm_sources = 0 num_drm_sources = 0
formats = [] formats, subtitles = [], {}
sources = json_data.get('sources') or [] sources = json_data.get('sources') or []
for source in sources: for source in sources:
container = source.get('container') container = source.get('container')
@@ -488,12 +488,16 @@ class BrightcoveNewIE(AdobePassIE):
elif ext == 'm3u8' or container == 'M2TS': elif ext == 'm3u8' or container == 'M2TS':
if not src: if not src:
continue continue
formats.extend(self._extract_m3u8_formats( f, subs = self._extract_m3u8_formats_and_subtitles(
src, video_id, 'mp4', 'm3u8_native', m3u8_id='hls', fatal=False)) src, video_id, 'mp4', 'm3u8_native', m3u8_id='hls', fatal=False)
formats.extend(f)
subtitles = self._merge_subtitles(subtitles, subs)
elif ext == 'mpd': elif ext == 'mpd':
if not src: if not src:
continue continue
formats.extend(self._extract_mpd_formats(src, video_id, 'dash', fatal=False)) f, subs = self._extract_mpd_formats_and_subtitles(src, video_id, 'dash', fatal=False)
formats.extend(f)
subtitles = self._merge_subtitles(subtitles, subs)
else: else:
streaming_src = source.get('streaming_src') streaming_src = source.get('streaming_src')
stream_name, app_name = source.get('stream_name'), source.get('app_name') stream_name, app_name = source.get('stream_name'), source.get('app_name')
@@ -556,7 +560,6 @@ class BrightcoveNewIE(AdobePassIE):
for f in formats: for f in formats:
f.setdefault('http_headers', {}).update(headers) f.setdefault('http_headers', {}).update(headers)
subtitles = {}
for text_track in json_data.get('text_tracks', []): for text_track in json_data.get('text_tracks', []):
if text_track.get('kind') != 'captions': if text_track.get('kind') != 'captions':
continue continue

32
yt_dlp/extractor/cam4.py Normal file
View File

@@ -0,0 +1,32 @@
# coding: utf-8
from __future__ import unicode_literals
from .common import InfoExtractor
class CAM4IE(InfoExtractor):
_VALID_URL = r'https?://(?:[^/]+\.)?cam4\.com/(?P<id>[a-z0-9_]+)'
_TEST = {
'url': 'https://www.cam4.com/foxynesss',
'info_dict': {
'id': 'foxynesss',
'ext': 'mp4',
'title': 're:^foxynesss [0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}$',
'age_limit': 18,
}
}
def _real_extract(self, url):
channel_id = self._match_id(url)
m3u8_playlist = self._download_json('https://www.cam4.com/rest/v1.0/profile/{}/streamInfo'.format(channel_id), channel_id).get('cdnURL')
formats = self._extract_m3u8_formats(m3u8_playlist, channel_id, 'mp4', m3u8_id='hls', live=True)
self._sort_formats(formats)
return {
'id': channel_id,
'title': self._live_title(channel_id),
'is_live': True,
'age_limit': 18,
'formats': formats,
}

View File

@@ -286,6 +286,9 @@ class VrtNUIE(GigyaBaseIE):
'targetEnv': 'jssdk', 'targetEnv': 'jssdk',
})) }))
if auth_info.get('errorDetails'):
raise ExtractorError('Unable to login: VrtNU said: ' + auth_info.get('errorDetails'), expected=True)
# Sometimes authentication fails for no good reason, retry # Sometimes authentication fails for no good reason, retry
login_attempt = 1 login_attempt = 1
while login_attempt <= 3: while login_attempt <= 3:

View File

@@ -1,30 +1,21 @@
# coding: utf-8 # coding: utf-8
from __future__ import unicode_literals from __future__ import unicode_literals
import hashlib
import json
import re import re
from xml.sax.saxutils import escape import json
import base64
import time
from .common import InfoExtractor from .common import InfoExtractor
from ..compat import ( from ..compat import (
compat_str, compat_str,
compat_HTTPError,
) )
from ..utils import ( from ..utils import (
js_to_json, js_to_json,
smuggle_url, smuggle_url,
try_get, try_get,
xpath_text,
xpath_element,
xpath_with_ns,
find_xpath_attr,
orderedSet, orderedSet,
parse_duration,
parse_iso8601,
parse_age_limit,
strip_or_none, strip_or_none,
int_or_none,
ExtractorError, ExtractorError,
) )
@@ -59,6 +50,7 @@ class CBCIE(InfoExtractor):
'uploader': 'CBCC-NEW', 'uploader': 'CBCC-NEW',
'timestamp': 1382717907, 'timestamp': 1382717907,
}, },
'skip': 'No longer available',
}, { }, {
# with clipId, feed only available via tpfeed.cbc.ca # with clipId, feed only available via tpfeed.cbc.ca
'url': 'http://www.cbc.ca/archives/entry/1978-robin-williams-freestyles-on-90-minutes-live', 'url': 'http://www.cbc.ca/archives/entry/1978-robin-williams-freestyles-on-90-minutes-live',
@@ -209,289 +201,287 @@ class CBCPlayerIE(InfoExtractor):
} }
class CBCWatchBaseIE(InfoExtractor): class CBCGemIE(InfoExtractor):
_device_id = None IE_NAME = 'gem.cbc.ca'
_device_token = None _VALID_URL = r'https?://gem\.cbc\.ca/media/(?P<id>[0-9a-z-]+/s[0-9]+[a-z][0-9]+)'
_API_BASE_URL = 'https://api-cbc.cloud.clearleap.com/cloffice/client/' _TESTS = [{
_NS_MAP = { # This is a normal, public, TV show video
'media': 'http://search.yahoo.com/mrss/', 'url': 'https://gem.cbc.ca/media/schitts-creek/s06e01',
'clearleap': 'http://www.clearleap.com/namespace/clearleap/1.0/', 'md5': '93dbb31c74a8e45b378cf13bd3f6f11e',
} 'info_dict': {
_GEO_COUNTRIES = ['CA'] 'id': 'schitts-creek/s06e01',
_LOGIN_URL = 'https://api.loginradius.com/identity/v2/auth/login' 'ext': 'mp4',
_TOKEN_URL = 'https://cloud-api.loginradius.com/sso/jwt/api/token' 'title': 'Smoke Signals',
_API_KEY = '3f4beddd-2061-49b0-ae80-6f1f2ed65b37' 'description': 'md5:929868d20021c924020641769eb3e7f1',
_NETRC_MACHINE = 'cbcwatch' 'thumbnail': 'https://images.radio-canada.ca/v1/synps-cbc/episode/perso/cbc_schitts_creek_season_06e01_thumbnail_v01.jpg?im=Resize=(Size)',
'duration': 1314,
'categories': ['comedy'],
'series': 'Schitt\'s Creek',
'season': 'Season 6',
'season_number': 6,
'episode': 'Smoke Signals',
'episode_number': 1,
'episode_id': 'schitts-creek/s06e01',
},
'params': {'format': 'bv'},
'skip': 'Geo-restricted to Canada',
}, {
# This video requires an account in the browser, but works fine in yt-dlp
'url': 'https://gem.cbc.ca/media/schitts-creek/s01e01',
'md5': '297a9600f554f2258aed01514226a697',
'info_dict': {
'id': 'schitts-creek/s01e01',
'ext': 'mp4',
'title': 'The Cup Runneth Over',
'description': 'md5:9bca14ea49ab808097530eb05a29e797',
'thumbnail': 'https://images.radio-canada.ca/v1/synps-cbc/episode/perso/cbc_schitts_creek_season_01e01_thumbnail_v01.jpg?im=Resize=(Size)',
'series': 'Schitt\'s Creek',
'season_number': 1,
'season': 'Season 1',
'episode_number': 1,
'episode': 'The Cup Runneth Over',
'episode_id': 'schitts-creek/s01e01',
'duration': 1309,
'categories': ['comedy'],
},
'params': {'format': 'bv'},
'skip': 'Geo-restricted to Canada',
}]
def _signature(self, email, password): _GEO_COUNTRIES = ['CA']
_TOKEN_API_KEY = '3f4beddd-2061-49b0-ae80-6f1f2ed65b37'
_NETRC_MACHINE = 'cbcgem'
_claims_token = None
def _new_claims_token(self, email, password):
data = json.dumps({ data = json.dumps({
'email': email, 'email': email,
'password': password, 'password': password,
}).encode() }).encode()
headers = {'content-type': 'application/json'} headers = {'content-type': 'application/json'}
query = {'apikey': self._API_KEY} query = {'apikey': self._TOKEN_API_KEY}
resp = self._download_json(self._LOGIN_URL, None, data=data, headers=headers, query=query) resp = self._download_json('https://api.loginradius.com/identity/v2/auth/login',
None, data=data, headers=headers, query=query)
access_token = resp['access_token'] access_token = resp['access_token']
# token
query = { query = {
'access_token': access_token, 'access_token': access_token,
'apikey': self._API_KEY, 'apikey': self._TOKEN_API_KEY,
'jwtapp': 'jwt', 'jwtapp': 'jwt',
} }
resp = self._download_json(self._TOKEN_URL, None, headers=headers, query=query) resp = self._download_json('https://cloud-api.loginradius.com/sso/jwt/api/token',
return resp['signature'] None, headers=headers, query=query)
sig = resp['signature']
def _call_api(self, path, video_id): data = json.dumps({'jwt': sig}).encode()
url = path if path.startswith('http') else self._API_BASE_URL + path headers = {'content-type': 'application/json', 'ott-device-type': 'web'}
for _ in range(2): resp = self._download_json('https://services.radio-canada.ca/ott/cbc-api/v2/token',
try: None, data=data, headers=headers)
result = self._download_xml(url, video_id, headers={ cbc_access_token = resp['accessToken']
'X-Clearleap-DeviceId': self._device_id,
'X-Clearleap-DeviceToken': self._device_token, headers = {'content-type': 'application/json', 'ott-device-type': 'web', 'ott-access-token': cbc_access_token}
}) resp = self._download_json('https://services.radio-canada.ca/ott/cbc-api/v2/profile',
except ExtractorError as e: None, headers=headers)
if isinstance(e.cause, compat_HTTPError) and e.cause.code == 401: return resp['claimsToken']
# Device token has expired, re-acquiring device token
self._register_device() def _get_claims_token_expiry(self):
continue # Token is a JWT
raise # JWT is decoded here and 'exp' field is extracted
error_message = xpath_text(result, 'userMessage') or xpath_text(result, 'systemMessage') # It is a Unix timestamp for when the token expires
if error_message: b64_data = self._claims_token.split('.')[1]
raise ExtractorError('%s said: %s' % (self.IE_NAME, error_message)) data = base64.urlsafe_b64decode(b64_data + "==")
return result return json.loads(data)['exp']
def claims_token_expired(self):
exp = self._get_claims_token_expiry()
if exp - time.time() < 10:
# It will expire in less than 10 seconds, or has already expired
return True
return False
def claims_token_valid(self):
return self._claims_token is not None and not self.claims_token_expired()
def _get_claims_token(self, email, password):
if not self.claims_token_valid():
self._claims_token = self._new_claims_token(email, password)
self._downloader.cache.store(self._NETRC_MACHINE, 'claims_token', self._claims_token)
return self._claims_token
def _real_initialize(self): def _real_initialize(self):
if self._valid_device_token(): if self.claims_token_valid():
return return
device = self._downloader.cache.load( self._claims_token = self._downloader.cache.load(self._NETRC_MACHINE, 'claims_token')
'cbcwatch', self._cache_device_key()) or {}
self._device_id, self._device_token = device.get('id'), device.get('token')
if self._valid_device_token():
return
self._register_device()
def _valid_device_token(self): def _real_extract(self, url):
return self._device_id and self._device_token video_id = self._match_id(url)
video_info = self._download_json('https://services.radio-canada.ca/ott/cbc-api/v2/assets/' + video_id, video_id)
def _cache_device_key(self):
email, _ = self._get_login_info()
return '%s_device' % hashlib.sha256(email.encode()).hexdigest() if email else 'device'
def _register_device(self):
result = self._download_xml(
self._API_BASE_URL + 'device/register',
None, 'Acquiring device token',
data=b'<device><type>web</type></device>')
self._device_id = xpath_text(result, 'deviceId', fatal=True)
email, password = self._get_login_info() email, password = self._get_login_info()
if email and password: if email and password:
signature = self._signature(email, password) claims_token = self._get_claims_token(email, password)
data = '<login><token>{0}</token><device><deviceId>{1}</deviceId><type>web</type></device></login>'.format( headers = {'x-claims-token': claims_token}
escape(signature), escape(self._device_id)).encode()
url = self._API_BASE_URL + 'device/login'
result = self._download_xml(
url, None, data=data,
headers={'content-type': 'application/xml'})
self._device_token = xpath_text(result, 'token', fatal=True)
else: else:
self._device_token = xpath_text(result, 'deviceToken', fatal=True) headers = {}
self._downloader.cache.store( m3u8_info = self._download_json(video_info['playSession']['url'], video_id, headers=headers)
'cbcwatch', self._cache_device_key(), { m3u8_url = m3u8_info.get('url')
'id': self._device_id,
'token': self._device_token,
})
def _parse_rss_feed(self, rss): if m3u8_info.get('errorCode') == 1:
channel = xpath_element(rss, 'channel', fatal=True) self.raise_geo_restricted(countries=['CA'])
elif m3u8_info.get('errorCode') == 35:
self.raise_login_required(method='password')
elif m3u8_info.get('errorCode') != 0:
raise ExtractorError(f'{self.IE_NAME} said: {m3u8_info.get("errorCode")} - {m3u8_info.get("message")}')
def _add_ns(path): formats = self._extract_m3u8_formats(m3u8_url, video_id, m3u8_id='hls')
return xpath_with_ns(path, self._NS_MAP) self._remove_duplicate_formats(formats)
entries = [] for format in formats:
for item in channel.findall('item'): if format.get('vcodec') == 'none':
guid = xpath_text(item, 'guid', fatal=True) if format.get('ext') is None:
title = xpath_text(item, 'title', fatal=True) format['ext'] = 'm4a'
if format.get('acodec') is None:
format['acodec'] = 'mp4a.40.2'
media_group = xpath_element(item, _add_ns('media:group'), fatal=True) # Put described audio at the beginning of the list, so that it
content = xpath_element(media_group, _add_ns('media:content'), fatal=True) # isn't chosen by default, as most people won't want it.
content_url = content.attrib['url'] if 'descriptive' in format['format_id'].lower():
format['preference'] = -2
thumbnails = []
for thumbnail in media_group.findall(_add_ns('media:thumbnail')):
thumbnail_url = thumbnail.get('url')
if not thumbnail_url:
continue
thumbnails.append({
'id': thumbnail.get('profile'),
'url': thumbnail_url,
'width': int_or_none(thumbnail.get('width')),
'height': int_or_none(thumbnail.get('height')),
})
timestamp = None
release_date = find_xpath_attr(
item, _add_ns('media:credit'), 'role', 'releaseDate')
if release_date is not None:
timestamp = parse_iso8601(release_date.text)
entries.append({
'_type': 'url_transparent',
'url': content_url,
'id': guid,
'title': title,
'description': xpath_text(item, 'description'),
'timestamp': timestamp,
'duration': int_or_none(content.get('duration')),
'age_limit': parse_age_limit(xpath_text(item, _add_ns('media:rating'))),
'episode': xpath_text(item, _add_ns('clearleap:episode')),
'episode_number': int_or_none(xpath_text(item, _add_ns('clearleap:episodeInSeason'))),
'series': xpath_text(item, _add_ns('clearleap:series')),
'season_number': int_or_none(xpath_text(item, _add_ns('clearleap:season'))),
'thumbnails': thumbnails,
'ie_key': 'CBCWatchVideo',
})
return self.playlist_result(
entries, xpath_text(channel, 'guid'),
xpath_text(channel, 'title'),
xpath_text(channel, 'description'))
class CBCWatchVideoIE(CBCWatchBaseIE):
IE_NAME = 'cbc.ca:watch:video'
_VALID_URL = r'https?://api-cbc\.cloud\.clearleap\.com/cloffice/client/web/play/?\?.*?\bcontentId=(?P<id>[\da-f]{8}-[\da-f]{4}-[\da-f]{4}-[\da-f]{4}-[\da-f]{12})'
_TEST = {
# geo-restricted to Canada, bypassable
'url': 'https://api-cbc.cloud.clearleap.com/cloffice/client/web/play/?contentId=3c84472a-1eea-4dee-9267-2655d5055dcf&categoryId=ebc258f5-ee40-4cca-b66b-ba6bd55b7235',
'only_matching': True,
}
def _real_extract(self, url):
video_id = self._match_id(url)
result = self._call_api(url, video_id)
m3u8_url = xpath_text(result, 'url', fatal=True)
formats = self._extract_m3u8_formats(re.sub(r'/([^/]+)/[^/?]+\.m3u8', r'/\1/\1.m3u8', m3u8_url), video_id, 'mp4', fatal=False)
if len(formats) < 2:
formats = self._extract_m3u8_formats(m3u8_url, video_id, 'mp4')
for f in formats:
format_id = f.get('format_id')
if format_id.startswith('AAC'):
f['acodec'] = 'aac'
elif format_id.startswith('AC3'):
f['acodec'] = 'ac-3'
self._sort_formats(formats)
info = {
'id': video_id,
'title': video_id,
'formats': formats,
}
rss = xpath_element(result, 'rss')
if rss:
info.update(self._parse_rss_feed(rss)['entries'][0])
del info['url']
del info['_type']
del info['ie_key']
return info
class CBCWatchIE(CBCWatchBaseIE):
IE_NAME = 'cbc.ca:watch'
_VALID_URL = r'https?://(?:gem|watch)\.cbc\.ca/(?:[^/]+/)+(?P<id>[0-9a-f-]+)'
_TESTS = [{
# geo-restricted to Canada, bypassable
'url': 'http://watch.cbc.ca/doc-zone/season-6/customer-disservice/38e815a-009e3ab12e4',
'info_dict': {
'id': '9673749a-5e77-484c-8b62-a1092a6b5168',
'ext': 'mp4',
'title': 'Customer (Dis)Service',
'description': 'md5:8bdd6913a0fe03d4b2a17ebe169c7c87',
'upload_date': '20160219',
'timestamp': 1455840000,
},
'params': {
# m3u8 download
'skip_download': True,
'format': 'bestvideo',
},
}, {
# geo-restricted to Canada, bypassable
'url': 'http://watch.cbc.ca/arthur/all/1ed4b385-cd84-49cf-95f0-80f004680057',
'info_dict': {
'id': '1ed4b385-cd84-49cf-95f0-80f004680057',
'title': 'Arthur',
'description': 'Arthur, the sweetest 8-year-old aardvark, and his pals solve all kinds of problems with humour, kindness and teamwork.',
},
'playlist_mincount': 30,
}, {
'url': 'https://gem.cbc.ca/media/this-hour-has-22-minutes/season-26/episode-20/38e815a-0108c6c6a42',
'only_matching': True,
}]
def _real_extract(self, url):
video_id = self._match_id(url)
rss = self._call_api('web/browse/' + video_id, video_id)
return self._parse_rss_feed(rss)
class CBCOlympicsIE(InfoExtractor):
IE_NAME = 'cbc.ca:olympics'
_VALID_URL = r'https?://olympics\.cbc\.ca/video/[^/]+/(?P<id>[^/?#]+)'
_TESTS = [{
'url': 'https://olympics.cbc.ca/video/whats-on-tv/olympic-morning-featuring-the-opening-ceremony/',
'only_matching': True,
}]
def _real_extract(self, url):
display_id = self._match_id(url)
webpage = self._download_webpage(url, display_id)
video_id = self._hidden_inputs(webpage)['videoId']
video_doc = self._download_xml(
'https://olympics.cbc.ca/videodata/%s.xml' % video_id, video_id)
title = xpath_text(video_doc, 'title', fatal=True)
is_live = xpath_text(video_doc, 'kind') == 'Live'
if is_live:
title = self._live_title(title)
formats = []
for video_source in video_doc.findall('videoSources/videoSource'):
uri = xpath_text(video_source, 'uri')
if not uri:
continue
tokenize = self._download_json(
'https://olympics.cbc.ca/api/api-akamai/tokenize',
video_id, data=json.dumps({
'VideoSource': uri,
}).encode(), headers={
'Content-Type': 'application/json',
'Referer': url,
# d3.VideoPlayer._init in https://olympics.cbc.ca/components/script/base.js
'Cookie': '_dvp=TK:C0ObxjerU', # AKAMAI CDN cookie
}, fatal=False)
if not tokenize:
continue
content_url = tokenize['ContentUrl']
video_source_format = video_source.get('format')
if video_source_format == 'IIS':
formats.extend(self._extract_ism_formats(
content_url, video_id, ism_id=video_source_format, fatal=False))
else:
formats.extend(self._extract_m3u8_formats(
content_url, video_id, 'mp4',
'm3u8' if is_live else 'm3u8_native',
m3u8_id=video_source_format, fatal=False))
self._sort_formats(formats) self._sort_formats(formats)
return { return {
'id': video_id, 'id': video_id,
'display_id': display_id, 'title': video_info['title'],
'title': title, 'description': video_info.get('description'),
'description': xpath_text(video_doc, 'description'), 'thumbnail': video_info.get('image'),
'thumbnail': xpath_text(video_doc, 'thumbnailUrl'), 'series': video_info.get('series'),
'duration': parse_duration(xpath_text(video_doc, 'duration')), 'season_number': video_info.get('season'),
'season': f'Season {video_info.get("season")}',
'episode_number': video_info.get('episode'),
'episode': video_info.get('title'),
'episode_id': video_id,
'duration': video_info.get('duration'),
'categories': [video_info.get('category')],
'formats': formats, 'formats': formats,
'is_live': is_live, 'release_timestamp': video_info.get('airDate'),
'timestamp': video_info.get('availableDate'),
}
class CBCGemPlaylistIE(InfoExtractor):
IE_NAME = 'gem.cbc.ca:playlist'
_VALID_URL = r'https?://gem\.cbc\.ca/media/(?P<id>(?P<show>[0-9a-z-]+)/s(?P<season>[0-9]+))/?(?:[?#]|$)'
_TESTS = [{
# TV show playlist, all public videos
'url': 'https://gem.cbc.ca/media/schitts-creek/s06',
'playlist_count': 16,
'info_dict': {
'id': 'schitts-creek/s06',
'title': 'Season 6',
'description': 'md5:6a92104a56cbeb5818cc47884d4326a2',
},
}]
_API_BASE = 'https://services.radio-canada.ca/ott/cbc-api/v2/shows/'
def _real_extract(self, url):
match = self._match_valid_url(url)
season_id = match.group('id')
show = match.group('show')
show_info = self._download_json(self._API_BASE + show, season_id)
season = int(match.group('season'))
season_info = try_get(show_info, lambda x: x['seasons'][season - 1])
if season_info is None:
raise ExtractorError(f'Couldn\'t find season {season} of {show}')
episodes = []
for episode in season_info['assets']:
episodes.append({
'_type': 'url_transparent',
'ie_key': 'CBCGem',
'url': 'https://gem.cbc.ca/media/' + episode['id'],
'id': episode['id'],
'title': episode.get('title'),
'description': episode.get('description'),
'thumbnail': episode.get('image'),
'series': episode.get('series'),
'season_number': episode.get('season'),
'season': season_info['title'],
'season_id': season_info.get('id'),
'episode_number': episode.get('episode'),
'episode': episode.get('title'),
'episode_id': episode['id'],
'duration': episode.get('duration'),
'categories': [episode.get('category')],
})
thumbnail = None
tn_uri = season_info.get('image')
# the-national was observed to use a "data:image/png;base64"
# URI for their 'image' value. The image was 1x1, and is
# probably just a placeholder, so it is ignored.
if tn_uri is not None and not tn_uri.startswith('data:'):
thumbnail = tn_uri
return {
'_type': 'playlist',
'entries': episodes,
'id': season_id,
'title': season_info['title'],
'description': season_info.get('description'),
'thumbnail': thumbnail,
'series': show_info.get('title'),
'season_number': season_info.get('season'),
'season': season_info['title'],
}
class CBCGemLiveIE(InfoExtractor):
IE_NAME = 'gem.cbc.ca:live'
_VALID_URL = r'https?://gem\.cbc\.ca/live/(?P<id>\d+)'
_TEST = {
'url': 'https://gem.cbc.ca/live/920604739687',
'info_dict': {
'title': 'Ottawa',
'description': 'The live TV channel and local programming from Ottawa',
'thumbnail': 'https://thumbnails.cbc.ca/maven_legacy/thumbnails/CBC_OTT_VMS/Live_Channel_Static_Images/Ottawa_2880x1620.jpg',
'is_live': True,
'id': 'AyqZwxRqh8EH',
'ext': 'mp4',
'timestamp': 1492106160,
'upload_date': '20170413',
'uploader': 'CBCC-NEW',
},
'skip': 'Live might have ended',
}
# It's unclear where the chars at the end come from, but they appear to be
# constant. Might need updating in the future.
# There are two URLs, some livestreams are in one, and some
# in the other. The JSON schema is the same for both.
_API_URLS = ['https://tpfeed.cbc.ca/f/ExhSPC/t_t3UKJR6MAT', 'https://tpfeed.cbc.ca/f/ExhSPC/FNiv9xQx_BnT']
def _real_extract(self, url):
video_id = self._match_id(url)
for api_url in self._API_URLS:
video_info = next((
stream for stream in self._download_json(api_url, video_id)['entries']
if stream.get('guid') == video_id), None)
if video_info:
break
else:
raise ExtractorError('Couldn\'t find video metadata, maybe this livestream is now offline', expected=True)
return {
'_type': 'url_transparent',
'ie_key': 'ThePlatform',
'url': video_info['content'][0]['url'],
'id': video_id,
'title': video_info.get('title'),
'description': video_info.get('description'),
'tags': try_get(video_info, lambda x: x['keywords'].split(', ')),
'thumbnail': video_info.get('cbc$staticImage'),
'is_live': True,
} }

View File

@@ -130,6 +130,7 @@ class CBSIE(CBSBaseIE):
title = xpath_text(video_data, 'videoTitle', 'title') or xpath_text(video_data, 'videotitle', 'title') title = xpath_text(video_data, 'videoTitle', 'title') or xpath_text(video_data, 'videotitle', 'title')
asset_types = {} asset_types = {}
has_drm = False
for item in items_data.findall('.//item'): for item in items_data.findall('.//item'):
asset_type = xpath_text(item, 'assetType') asset_type = xpath_text(item, 'assetType')
query = { query = {
@@ -144,6 +145,8 @@ class CBSIE(CBSBaseIE):
if asset_type in asset_types: if asset_type in asset_types:
continue continue
elif any(excluded in asset_type for excluded in ('HLS_FPS', 'DASH_CENC', 'OnceURL')): elif any(excluded in asset_type for excluded in ('HLS_FPS', 'DASH_CENC', 'OnceURL')):
if 'DASH_CENC' in asset_type:
has_drm = True
continue continue
if asset_type.startswith('HLS') or 'StreamPack' in asset_type: if asset_type.startswith('HLS') or 'StreamPack' in asset_type:
query['formats'] = 'MPEG4,M3U' query['formats'] = 'MPEG4,M3U'
@@ -151,6 +154,9 @@ class CBSIE(CBSBaseIE):
query['formats'] = 'MPEG4,FLV' query['formats'] = 'MPEG4,FLV'
asset_types[asset_type] = query asset_types[asset_type] = query
if not asset_types and has_drm:
self.report_drm(content_id)
return self._extract_common_video_info(content_id, asset_types, mpx_acc, extra_info={ return self._extract_common_video_info(content_id, asset_types, mpx_acc, extra_info={
'title': title, 'title': title,
'series': xpath_text(video_data, 'seriesTitle'), 'series': xpath_text(video_data, 'seriesTitle'),

64
yt_dlp/extractor/cgtn.py Normal file
View File

@@ -0,0 +1,64 @@
# coding: utf-8
from __future__ import unicode_literals
from .common import InfoExtractor
from ..utils import (
try_get,
unified_timestamp,
)
class CGTNIE(InfoExtractor):
_VALID_URL = r'https?://news\.cgtn\.com/news/[0-9]{4}-[0-9]{2}-[0-9]{2}/[a-zA-Z0-9-]+-(?P<id>[a-zA-Z0-9-]+)/index\.html'
_TESTS = [
{
'url': 'https://news.cgtn.com/news/2021-03-09/Up-and-Out-of-Poverty-Ep-1-A-solemn-promise-YuOUaOzGQU/index.html',
'info_dict': {
'id': 'YuOUaOzGQU',
'ext': 'mp4',
'title': 'Up and Out of Poverty Ep. 1: A solemn promise',
'thumbnail': r're:^https?://.*\.jpg$',
'timestamp': 1615295940,
'upload_date': '20210309',
},
'params': {
'skip_download': True
}
}, {
'url': 'https://news.cgtn.com/news/2021-06-06/China-Indonesia-vow-to-further-deepen-maritime-cooperation-10REvJCewCY/index.html',
'info_dict': {
'id': '10REvJCewCY',
'ext': 'mp4',
'title': 'China, Indonesia vow to further deepen maritime cooperation',
'thumbnail': r're:^https?://.*\.png$',
'description': 'China and Indonesia vowed to upgrade their cooperation into the maritime sector and also for political security, economy, and cultural and people-to-people exchanges.',
'author': 'CGTN',
'category': 'China',
'timestamp': 1622950200,
'upload_date': '20210606',
},
'params': {
'skip_download': False
}
}
]
def _real_extract(self, url):
video_id = self._match_id(url)
webpage = self._download_webpage(url, video_id)
download_url = self._html_search_regex(r'data-video ="(?P<url>.+m3u8)"', webpage, 'download_url')
datetime_str = self._html_search_regex(r'<span class="date">\s*(.+?)\s*</span>', webpage, 'datetime_str', fatal=False)
return {
'id': video_id,
'title': self._og_search_title(webpage),
'description': self._og_search_description(webpage, default=None),
'thumbnail': self._og_search_thumbnail(webpage),
'formats': self._extract_m3u8_formats(download_url, video_id, 'mp4', 'm3u8_native', m3u8_id='hls'),
'category': self._html_search_regex(r'<span class="section">\s*(.+?)\s*</span>',
webpage, 'category', fatal=False),
'author': self._html_search_regex(r'<div class="news-author-name">\s*(.+?)\s*</div>',
webpage, 'author', default=None, fatal=False),
'timestamp': try_get(unified_timestamp(datetime_str), lambda x: x - 8 * 3600),
}

View File

@@ -0,0 +1,209 @@
# coding: utf-8
from __future__ import unicode_literals
import itertools
import json
from .common import InfoExtractor
from ..compat import compat_urllib_parse_unquote_plus
from ..utils import (
clean_html,
ExtractorError,
int_or_none,
str_to_int,
url_or_none,
)
class ChingariBaseIE(InfoExtractor):
def _get_post(self, id, post_data):
media_data = post_data['mediaLocation']
base_url = media_data['base']
author_data = post_data.get('authorData', {})
song_data = post_data.get('song', {}) # revist this in future for differentiating b/w 'art' and 'author'
formats = [{
'format_id': frmt,
'width': str_to_int(frmt[1:]),
'url': base_url + frmt_path,
} for frmt, frmt_path in media_data.get('transcoded', {}).items()]
if media_data.get('path'):
formats.append({
'format_id': 'original',
'format_note': 'Direct video.',
'url': base_url + '/apipublic' + media_data['path'],
'quality': 10,
})
self._sort_formats(formats)
timestamp = str_to_int(post_data.get('created_at'))
if timestamp:
timestamp = int_or_none(timestamp, 1000)
thumbnail, uploader_url = None, None
if media_data.get('thumbnail'):
thumbnail = base_url + media_data.get('thumbnail')
if author_data.get('username'):
uploader_url = 'https://chingari.io/' + author_data.get('username')
return {
'id': id,
'title': compat_urllib_parse_unquote_plus(clean_html(post_data.get('caption'))),
'description': compat_urllib_parse_unquote_plus(clean_html(post_data.get('caption'))),
'duration': media_data.get('duration'),
'thumbnail': url_or_none(thumbnail),
'like_count': post_data.get('likeCount'),
'view_count': post_data.get('viewsCount'),
'comment_count': post_data.get('commentCount'),
'repost_count': post_data.get('shareCount'),
'timestamp': timestamp,
'uploader_id': post_data.get('userId') or author_data.get('_id'),
'uploader': author_data.get('name'),
'uploader_url': url_or_none(uploader_url),
'track': song_data.get('title'),
'artist': song_data.get('author'),
'formats': formats,
}
class ChingariIE(ChingariBaseIE):
_VALID_URL = r'(?:https?://)(?:www\.)?chingari\.io/share/post\?id=(?P<id>[^&/#?]+)'
_TESTS = [{
'url': 'https://chingari.io/share/post?id=612f8f4ce1dc57090e8a7beb',
'info_dict': {
'id': '612f8f4ce1dc57090e8a7beb',
'ext': 'mp4',
'title': 'Happy birthday Srila Prabhupada',
'description': 'md5:c7080ebfdfeb06016e638c286d6bc3fa',
'duration': 0,
'thumbnail': 'https://media.chingari.io/uploads/c41d30e2-06b6-4e3b-9b4b-edbb929cec06-1630506826911/thumbnail/198f993f-ce87-4623-82c6-cd071bd6d4f4-1630506828016.jpg',
'like_count': int,
'view_count': int,
'comment_count': int,
'repost_count': int,
'timestamp': 1630506828,
'upload_date': '20210901',
'uploader_id': '5f0403982c8bd344f4813f8c',
'uploader': 'ISKCON,Inc.',
'uploader_url': 'https://chingari.io/iskcon,inc',
'track': None,
'artist': None,
},
'params': {'skip_download': True}
}]
def _real_extract(self, url):
id = self._match_id(url)
post_json = self._download_json(f'https://api.chingari.io/post/post_details/{id}', id)
if post_json['code'] != 200:
raise ExtractorError(post_json['message'], expected=True)
post_data = post_json['data']
return self._get_post(id, post_data)
class ChingariUserIE(ChingariBaseIE):
_VALID_URL = r'(?:https?://)(?:www\.)?chingari\.io/(?!share/post)(?P<id>[^/?]+)'
_TESTS = [{
'url': 'https://chingari.io/dada1023',
'playlist_mincount': 3,
'info_dict': {
'id': 'dada1023',
},
'entries': [{
'url': 'https://chingari.io/share/post?id=614781f3ade60b3a0bfff42a',
'info_dict': {
'id': '614781f3ade60b3a0bfff42a',
'ext': 'mp4',
'title': '#chingaribappa ',
'description': 'md5:d1df21d84088770468fa63afe3b17857',
'duration': 7,
'thumbnail': 'https://media.chingari.io/uploads/346d86d4-abb2-474e-a164-ffccf2bbcb72-1632076273717/thumbnail/b0b3aac2-2b86-4dd1-909d-9ed6e57cf77c-1632076275552.jpg',
'like_count': int,
'view_count': int,
'comment_count': int,
'repost_count': int,
'timestamp': 1632076275,
'upload_date': '20210919',
'uploader_id': '5efc4b12cca35c3d1794c2d3',
'uploader': 'dada (girish) dhawale',
'uploader_url': 'https://chingari.io/dada1023',
'track': None,
'artist': None
},
'params': {'skip_download': True}
}, {
'url': 'https://chingari.io/share/post?id=6146b132bcbf860959e12cba',
'info_dict': {
'id': '6146b132bcbf860959e12cba',
'ext': 'mp4',
'title': 'Tactor harvesting',
'description': 'md5:8403f12dce68828b77ecee7eb7e887b7',
'duration': 59.3,
'thumbnail': 'https://media.chingari.io/uploads/b353ca70-7a87-400d-93a6-fa561afaec86-1632022814584/thumbnail/c09302e3-2043-41b1-a2fe-77d97e5bd676-1632022834260.jpg',
'like_count': int,
'view_count': int,
'comment_count': int,
'repost_count': int,
'timestamp': 1632022834,
'upload_date': '20210919',
'uploader_id': '5efc4b12cca35c3d1794c2d3',
'uploader': 'dada (girish) dhawale',
'uploader_url': 'https://chingari.io/dada1023',
'track': None,
'artist': None
},
'params': {'skip_download': True}
}, {
'url': 'https://chingari.io/share/post?id=6145651b74cb030a64c40b82',
'info_dict': {
'id': '6145651b74cb030a64c40b82',
'ext': 'mp4',
'title': '#odiabhajan ',
'description': 'md5:687ea36835b9276cf2af90f25e7654cb',
'duration': 56.67,
'thumbnail': 'https://media.chingari.io/uploads/6cbf216b-babc-4cce-87fe-ceaac8d706ac-1631937782708/thumbnail/8855754f-6669-48ce-b269-8cc0699ed6da-1631937819522.jpg',
'like_count': int,
'view_count': int,
'comment_count': int,
'repost_count': int,
'timestamp': 1631937819,
'upload_date': '20210918',
'uploader_id': '5efc4b12cca35c3d1794c2d3',
'uploader': 'dada (girish) dhawale',
'uploader_url': 'https://chingari.io/dada1023',
'track': None,
'artist': None
},
'params': {'skip_download': True}
}],
}, {
'url': 'https://chingari.io/iskcon%2Cinc',
'playlist_mincount': 1025,
'info_dict': {
'id': 'iskcon%2Cinc',
},
}]
def _entries(self, id):
skip = 0
has_more = True
for page in itertools.count():
posts = self._download_json('https://api.chingari.io/users/getPosts', id,
data=json.dumps({'userId': id, 'ownerId': id, 'skip': skip, 'limit': 20}).encode(),
headers={'content-type': 'application/json;charset=UTF-8'},
note='Downloading page %s' % page)
for post in posts.get('data', []):
post_data = post['post']
yield self._get_post(post_data['_id'], post_data)
skip += 20
has_more = posts['hasMoreData']
if not has_more:
break
def _real_extract(self, url):
alt_id = self._match_id(url)
post_json = self._download_json(f'https://api.chingari.io/user/{alt_id}', alt_id)
if post_json['code'] != 200:
raise ExtractorError(post_json['message'], expected=True)
id = post_json['data']['_id']
return self.playlist_result(self._entries(id), playlist_id=alt_id)

View File

@@ -0,0 +1,90 @@
# coding: utf-8
from __future__ import unicode_literals
from .common import InfoExtractor
from ..utils import (
int_or_none,
try_get,
unified_timestamp,
)
class CiscoWebexIE(InfoExtractor):
IE_NAME = 'ciscowebex'
IE_DESC = 'Cisco Webex'
_VALID_URL = r'''(?x)
(?P<url>https?://(?P<subdomain>[^/#?]*)\.webex\.com/(?:
(?P<siteurl_1>[^/#?]*)/(?:ldr|lsr).php\?(?:[^#]*&)*RCID=(?P<rcid>[0-9a-f]{32})|
(?:recordingservice|webappng)/sites/(?P<siteurl_2>[^/#?]*)/recording/(?:playback/|play/)?(?P<id>[0-9a-f]{32})
))'''
_TESTS = [{
'url': 'https://demosubdomain.webex.com/demositeurl/ldr.php?RCID=e58e803bc0f766bb5f6376d2e86adb5b',
'only_matching': True,
}, {
'url': 'http://demosubdomain.webex.com/demositeurl/lsr.php?RCID=bc04b4a7b5ea2cc3a493d5ae6aaff5d7',
'only_matching': True,
}, {
'url': 'https://demosubdomain.webex.com/recordingservice/sites/demositeurl/recording/88e7a42f7b19f5b423c54754aecc2ce9/playback',
'only_matching': True,
}]
def _real_extract(self, url):
mobj = self._match_valid_url(url)
rcid = mobj.group('rcid')
if rcid:
webpage = self._download_webpage(url, None, note='Getting video ID')
url = self._search_regex(self._VALID_URL, webpage, 'redirection url', group='url')
url = self._request_webpage(url, None, note='Resolving final URL').geturl()
mobj = self._match_valid_url(url)
subdomain = mobj.group('subdomain')
siteurl = mobj.group('siteurl_1') or mobj.group('siteurl_2')
video_id = mobj.group('id')
stream = self._download_json(
'https://%s.webex.com/webappng/api/v1/recordings/%s/stream' % (subdomain, video_id),
video_id, fatal=False, query={'siteurl': siteurl})
if not stream:
self.raise_login_required(method='cookies')
video_id = stream.get('recordUUID') or video_id
formats = [{
'format_id': 'video',
'url': stream['fallbackPlaySrc'],
'ext': 'mp4',
'vcodec': 'avc1.640028',
'acodec': 'mp4a.40.2',
}]
if stream.get('preventDownload') is False:
mp4url = try_get(stream, lambda x: x['downloadRecordingInfo']['downloadInfo']['mp4URL'])
if mp4url:
formats.append({
'format_id': 'video',
'url': mp4url,
'ext': 'mp4',
'vcodec': 'avc1.640028',
'acodec': 'mp4a.40.2',
})
audiourl = try_get(stream, lambda x: x['downloadRecordingInfo']['downloadInfo']['audioURL'])
if audiourl:
formats.append({
'format_id': 'audio',
'url': audiourl,
'ext': 'mp3',
'vcodec': 'none',
'acodec': 'mp3',
})
self._sort_formats(formats)
return {
'id': video_id,
'title': stream['recordName'],
'description': stream.get('description'),
'uploader': stream.get('ownerDisplayName'),
'uploader_id': stream.get('ownerUserName') or stream.get('ownerId'), # mail or id
'timestamp': unified_timestamp(stream.get('createTime')),
'duration': int_or_none(stream.get('duration'), 1000),
'webpage_url': 'https://%s.webex.com/recordingservice/sites/%s/recording/playback/%s' % (subdomain, siteurl, video_id),
'formats': formats,
}

View File

@@ -4,7 +4,7 @@ from .mtv import MTVServicesInfoExtractor
class ComedyCentralIE(MTVServicesInfoExtractor): class ComedyCentralIE(MTVServicesInfoExtractor):
_VALID_URL = r'https?://(?:www\.)?cc\.com/(?:episodes|video(?:-clips)?)/(?P<id>[0-9a-z]{6})' _VALID_URL = r'https?://(?:www\.)?cc\.com/(?:episodes|video(?:-clips)?|collection-playlist)/(?P<id>[0-9a-z]{6})'
_FEED_URL = 'http://comedycentral.com/feeds/mrss/' _FEED_URL = 'http://comedycentral.com/feeds/mrss/'
_TESTS = [{ _TESTS = [{
@@ -24,6 +24,9 @@ class ComedyCentralIE(MTVServicesInfoExtractor):
}, { }, {
'url': 'https://www.cc.com/video/k3sdvm/the-daily-show-with-jon-stewart-exclusive-the-fourth-estate', 'url': 'https://www.cc.com/video/k3sdvm/the-daily-show-with-jon-stewart-exclusive-the-fourth-estate',
'only_matching': True, 'only_matching': True,
}, {
'url': 'https://www.cc.com/collection-playlist/cosnej/stand-up-specials/t6vtjb',
'only_matching': True,
}] }]

View File

@@ -4,6 +4,7 @@ from __future__ import unicode_literals
import base64 import base64
import datetime import datetime
import hashlib import hashlib
import itertools
import json import json
import netrc import netrc
import os import os
@@ -18,6 +19,7 @@ from ..compat import (
compat_cookies_SimpleCookie, compat_cookies_SimpleCookie,
compat_etree_Element, compat_etree_Element,
compat_etree_fromstring, compat_etree_fromstring,
compat_expanduser,
compat_getpass, compat_getpass,
compat_http_client, compat_http_client,
compat_os_name, compat_os_name,
@@ -145,6 +147,8 @@ class InfoExtractor(object):
* width Width of the video, if known * width Width of the video, if known
* height Height of the video, if known * height Height of the video, if known
* resolution Textual description of width and height * resolution Textual description of width and height
* dynamic_range The dynamic range of the video. One of:
"SDR" (None), "HDR10", "HDR10+, "HDR12", "HLG, "DV"
* tbr Average bitrate of audio and video in KBit/s * tbr Average bitrate of audio and video in KBit/s
* abr Average audio bitrate in KBit/s * abr Average audio bitrate in KBit/s
* acodec Name of the audio codec in use * acodec Name of the audio codec in use
@@ -231,7 +235,6 @@ class InfoExtractor(object):
* "resolution" (optional, string "{width}x{height}", * "resolution" (optional, string "{width}x{height}",
deprecated) deprecated)
* "filesize" (optional, int) * "filesize" (optional, int)
* "_test_url" (optional, bool) - If true, test the URL
thumbnail: Full URL to a video thumbnail image. thumbnail: Full URL to a video thumbnail image.
description: Full video description. description: Full video description.
uploader: Full name of the video uploader. uploader: Full name of the video uploader.
@@ -405,6 +408,10 @@ class InfoExtractor(object):
_real_extract() methods and define a _VALID_URL regexp. _real_extract() methods and define a _VALID_URL regexp.
Probably, they should also be added to the list of extractors. Probably, they should also be added to the list of extractors.
Subclasses may also override suitable() if necessary, but ensure the function
signature is preserved and that this function imports everything it needs
(except other extractors), so that lazy_extractors works correctly
_GEO_BYPASS attribute may be set to False in order to disable _GEO_BYPASS attribute may be set to False in order to disable
geo restriction bypass mechanisms for a particular extractor. geo restriction bypass mechanisms for a particular extractor.
Though it won't disable explicit geo restriction bypass based on Though it won't disable explicit geo restriction bypass based on
@@ -420,7 +427,7 @@ class InfoExtractor(object):
will be used by geo restriction bypass mechanism similarly will be used by geo restriction bypass mechanism similarly
to _GEO_COUNTRIES. to _GEO_COUNTRIES.
Finally, the _WORKING attribute should be set to False for broken IEs The _WORKING attribute should be set to False for broken IEs
in order to warn the users and skip the tests. in order to warn the users and skip the tests.
""" """
@@ -435,13 +442,15 @@ class InfoExtractor(object):
_LOGIN_HINTS = { _LOGIN_HINTS = {
'any': 'Use --cookies, --username and --password or --netrc to provide account credentials', 'any': 'Use --cookies, --username and --password or --netrc to provide account credentials',
'cookies': ( 'cookies': (
'Use --cookies for the authentication. ' 'Use --cookies-from-browser or --cookies for the authentication. '
'See https://github.com/ytdl-org/youtube-dl#how-do-i-pass-cookies-to-youtube-dl for how to pass cookies'), 'See https://github.com/ytdl-org/youtube-dl#how-do-i-pass-cookies-to-youtube-dl for how to manually pass cookies'),
'password': 'Use --username and --password or --netrc to provide account credentials', 'password': 'Use --username and --password or --netrc to provide account credentials',
} }
def __init__(self, downloader=None): def __init__(self, downloader=None):
"""Constructor. Receives an optional downloader.""" """Constructor. Receives an optional downloader (a YoutubeDL instance).
If a downloader is not passed during initialization,
it must be set using "set_downloader()" before "extract()" is called"""
self._ready = False self._ready = False
self._x_forwarded_for_ip = None self._x_forwarded_for_ip = None
self._printed_messages = set() self._printed_messages = set()
@@ -657,7 +666,7 @@ class InfoExtractor(object):
See _download_webpage docstring for arguments specification. See _download_webpage docstring for arguments specification.
""" """
if not self._downloader._first_webpage_request: if not self._downloader._first_webpage_request:
sleep_interval = float_or_none(self.get_param('sleep_interval_requests')) or 0 sleep_interval = self.get_param('sleep_interval_requests') or 0
if sleep_interval > 0: if sleep_interval > 0:
self.to_screen('Sleeping %s seconds ...' % sleep_interval) self.to_screen('Sleeping %s seconds ...' % sleep_interval)
time.sleep(sleep_interval) time.sleep(sleep_interval)
@@ -788,9 +797,10 @@ class InfoExtractor(object):
self._downloader.to_screen(dump) self._downloader.to_screen(dump)
if self.get_param('write_pages', False): if self.get_param('write_pages', False):
basen = '%s_%s' % (video_id, urlh.geturl()) basen = '%s_%s' % (video_id, urlh.geturl())
if len(basen) > 240: trim_length = self.get_param('trim_file_name') or 240
if len(basen) > trim_length:
h = '___' + hashlib.md5(basen.encode('utf-8')).hexdigest() h = '___' + hashlib.md5(basen.encode('utf-8')).hexdigest()
basen = basen[:240 - len(h)] + h basen = basen[:trim_length - len(h)] + h
raw_filename = basen + '.dump' raw_filename = basen + '.dump'
filename = sanitize_filename(raw_filename, restricted=True) filename = sanitize_filename(raw_filename, restricted=True)
self.to_screen('Saving request to ' + filename) self.to_screen('Saving request to ' + filename)
@@ -1080,12 +1090,13 @@ class InfoExtractor(object):
# Methods for following #608 # Methods for following #608
@staticmethod @staticmethod
def url_result(url, ie=None, video_id=None, video_title=None): def url_result(url, ie=None, video_id=None, video_title=None, **kwargs):
"""Returns a URL that points to a page that should be processed""" """Returns a URL that points to a page that should be processed"""
# TODO: ie should be the class used for getting the info # TODO: ie should be the class used for getting the info
video_info = {'_type': 'url', video_info = {'_type': 'url',
'url': url, 'url': url,
'ie_key': ie} 'ie_key': ie}
video_info.update(kwargs)
if video_id is not None: if video_id is not None:
video_info['id'] = video_id video_info['id'] = video_id
if video_title is not None: if video_title is not None:
@@ -1128,10 +1139,7 @@ class InfoExtractor(object):
if mobj: if mobj:
break break
if not self.get_param('no_color') and compat_os_name != 'nt' and sys.stderr.isatty(): _name = self._downloader._color_text(name, 'blue')
_name = '\033[0;34m%s\033[0m' % name
else:
_name = name
if mobj: if mobj:
if group is None: if group is None:
@@ -1166,7 +1174,10 @@ class InfoExtractor(object):
if self.get_param('usenetrc', False): if self.get_param('usenetrc', False):
try: try:
info = netrc.netrc().authenticators(netrc_machine) netrc_file = compat_expanduser(self.get_param('netrc_location') or '~')
if os.path.isdir(netrc_file):
netrc_file = os.path.join(netrc_file, '.netrc')
info = netrc.netrc(file=netrc_file).authenticators(netrc_machine)
if info is not None: if info is not None:
username = info[0] username = info[0]
password = info[2] password = info[2]
@@ -1500,7 +1511,7 @@ class InfoExtractor(object):
regex = r' *((?P<reverse>\+)?(?P<field>[a-zA-Z0-9_]+)((?P<separator>[~:])(?P<limit>.*?))?)? *$' regex = r' *((?P<reverse>\+)?(?P<field>[a-zA-Z0-9_]+)((?P<separator>[~:])(?P<limit>.*?))?)? *$'
default = ('hidden', 'aud_or_vid', 'hasvid', 'ie_pref', 'lang', 'quality', default = ('hidden', 'aud_or_vid', 'hasvid', 'ie_pref', 'lang', 'quality',
'res', 'fps', 'codec:vp9.2', 'size', 'br', 'asr', 'res', 'fps', 'hdr:12', 'codec:vp9.2', 'size', 'br', 'asr',
'proto', 'ext', 'hasaud', 'source', 'format_id') # These must not be aliases 'proto', 'ext', 'hasaud', 'source', 'format_id') # These must not be aliases
ytdl_default = ('hasaud', 'lang', 'quality', 'tbr', 'filesize', 'vbr', ytdl_default = ('hasaud', 'lang', 'quality', 'tbr', 'filesize', 'vbr',
'height', 'width', 'proto', 'vext', 'abr', 'aext', 'height', 'width', 'proto', 'vext', 'abr', 'aext',
@@ -1511,6 +1522,8 @@ class InfoExtractor(object):
'order': ['av0?1', 'vp0?9.2', 'vp0?9', '[hx]265|he?vc?', '[hx]264|avc', 'vp0?8', 'mp4v|h263', 'theora', '', None, 'none']}, 'order': ['av0?1', 'vp0?9.2', 'vp0?9', '[hx]265|he?vc?', '[hx]264|avc', 'vp0?8', 'mp4v|h263', 'theora', '', None, 'none']},
'acodec': {'type': 'ordered', 'regex': True, 'acodec': {'type': 'ordered', 'regex': True,
'order': ['opus', 'vorbis', 'aac', 'mp?4a?', 'mp3', 'e?a?c-?3', 'dts', '', None, 'none']}, 'order': ['opus', 'vorbis', 'aac', 'mp?4a?', 'mp3', 'e?a?c-?3', 'dts', '', None, 'none']},
'hdr': {'type': 'ordered', 'regex': True, 'field': 'dynamic_range',
'order': ['dv', '(hdr)?12', r'(hdr)?10\+', '(hdr)?10', 'hlg', '', 'sdr', None]},
'proto': {'type': 'ordered', 'regex': True, 'field': 'protocol', 'proto': {'type': 'ordered', 'regex': True, 'field': 'protocol',
'order': ['(ht|f)tps', '(ht|f)tp$', 'm3u8.+', '.*dash', 'ws|websocket', '', 'mms|rtsp', 'none', 'f4']}, 'order': ['(ht|f)tps', '(ht|f)tp$', 'm3u8.+', '.*dash', 'ws|websocket', '', 'mms|rtsp', 'none', 'f4']},
'vext': {'type': 'ordered', 'field': 'video_ext', 'vext': {'type': 'ordered', 'field': 'video_ext',
@@ -1672,7 +1685,7 @@ class InfoExtractor(object):
has_multiple_limits = has_limit and has_multiple_fields and not self._get_field_setting(field, 'same_limit') has_multiple_limits = has_limit and has_multiple_fields and not self._get_field_setting(field, 'same_limit')
fields = self._get_field_setting(field, 'field') if has_multiple_fields else (field,) fields = self._get_field_setting(field, 'field') if has_multiple_fields else (field,)
limits = limit_text.split(":") if has_multiple_limits else (limit_text,) if has_limit else tuple() limits = limit_text.split(':') if has_multiple_limits else (limit_text,) if has_limit else tuple()
limit_count = len(limits) limit_count = len(limits)
for (i, f) in enumerate(fields): for (i, f) in enumerate(fields):
add_item(f, reverse, closest, add_item(f, reverse, closest,
@@ -1756,9 +1769,9 @@ class InfoExtractor(object):
if format.get('vbr') is not None and format.get('abr') is not None: if format.get('vbr') is not None and format.get('abr') is not None:
format['tbr'] = format.get('vbr', 0) + format.get('abr', 0) format['tbr'] = format.get('vbr', 0) + format.get('abr', 0)
else: else:
if format.get('vcodec') != "none" and format.get('vbr') is None: if format.get('vcodec') != 'none' and format.get('vbr') is None:
format['vbr'] = format.get('tbr') - format.get('abr', 0) format['vbr'] = format.get('tbr') - format.get('abr', 0)
if format.get('acodec') != "none" and format.get('abr') is None: if format.get('acodec') != 'none' and format.get('abr') is None:
format['abr'] = format.get('tbr') - format.get('vbr', 0) format['abr'] = format.get('tbr') - format.get('vbr', 0)
return tuple(self._calculate_field_preference(format, field) for field in self._order) return tuple(self._calculate_field_preference(format, field) for field in self._order)
@@ -1960,13 +1973,16 @@ class InfoExtractor(object):
'format_note': 'Quality selection URL', 'format_note': 'Quality selection URL',
} }
def _report_ignoring_subs(self, name):
self.report_warning(bug_reports_message(
f'Ignoring subtitle tracks found in the {name} manifest; '
'if any subtitle tracks are missing,'
), only_once=True)
def _extract_m3u8_formats(self, *args, **kwargs): def _extract_m3u8_formats(self, *args, **kwargs):
fmts, subs = self._extract_m3u8_formats_and_subtitles(*args, **kwargs) fmts, subs = self._extract_m3u8_formats_and_subtitles(*args, **kwargs)
if subs: if subs:
self.report_warning(bug_reports_message( self._report_ignoring_subs('HLS')
"Ignoring subtitle tracks found in the HLS manifest; "
"if any subtitle tracks are missing,"
), only_once=True)
return fmts return fmts
def _extract_m3u8_formats_and_subtitles( def _extract_m3u8_formats_and_subtitles(
@@ -2003,7 +2019,7 @@ class InfoExtractor(object):
if '#EXT-X-FAXS-CM:' in m3u8_doc: # Adobe Flash Access if '#EXT-X-FAXS-CM:' in m3u8_doc: # Adobe Flash Access
return formats, subtitles return formats, subtitles
has_drm = re.search(r'#EXT-X-SESSION-KEY:.*?URI="skd://', m3u8_doc) has_drm = re.search(r'#EXT-X-(?:SESSION-)?KEY:.*?URI="skd://', m3u8_doc)
def format_url(url): def format_url(url):
return url if re.match(r'^https?://', url) else compat_urlparse.urljoin(m3u8_url, url) return url if re.match(r'^https?://', url) else compat_urlparse.urljoin(m3u8_url, url)
@@ -2214,6 +2230,25 @@ class InfoExtractor(object):
last_stream_inf = {} last_stream_inf = {}
return formats, subtitles return formats, subtitles
def _extract_m3u8_vod_duration(
self, m3u8_vod_url, video_id, note=None, errnote=None, data=None, headers={}, query={}):
m3u8_vod = self._download_webpage(
m3u8_vod_url, video_id,
note='Downloading m3u8 VOD manifest' if note is None else note,
errnote='Failed to download VOD manifest' if errnote is None else errnote,
fatal=False, data=data, headers=headers, query=query)
return self._parse_m3u8_vod_duration(m3u8_vod or '', video_id)
def _parse_m3u8_vod_duration(self, m3u8_vod, video_id):
if '#EXT-X-PLAYLIST-TYPE:VOD' not in m3u8_vod:
return None
return int(sum(
float(line[len('#EXTINF:'):].split(',')[0])
for line in m3u8_vod.splitlines() if line.startswith('#EXTINF:'))) or None
@staticmethod @staticmethod
def _xpath_ns(path, namespace=None): def _xpath_ns(path, namespace=None):
if not namespace: if not namespace:
@@ -2245,10 +2280,7 @@ class InfoExtractor(object):
def _extract_smil_formats(self, *args, **kwargs): def _extract_smil_formats(self, *args, **kwargs):
fmts, subs = self._extract_smil_formats_and_subtitles(*args, **kwargs) fmts, subs = self._extract_smil_formats_and_subtitles(*args, **kwargs)
if subs: if subs:
self.report_warning(bug_reports_message( self._report_ignoring_subs('SMIL')
"Ignoring subtitle tracks found in the SMIL manifest; "
"if any subtitle tracks are missing,"
), only_once=True)
return fmts return fmts
def _extract_smil_info(self, smil_url, video_id, fatal=True, f4m_params=None): def _extract_smil_info(self, smil_url, video_id, fatal=True, f4m_params=None):
@@ -2318,14 +2350,15 @@ class InfoExtractor(object):
rtmp_count = 0 rtmp_count = 0
http_count = 0 http_count = 0
m3u8_count = 0 m3u8_count = 0
imgs_count = 0
srcs = [] srcs = set()
media = smil.findall(self._xpath_ns('.//video', namespace)) + smil.findall(self._xpath_ns('.//audio', namespace)) media = smil.findall(self._xpath_ns('.//video', namespace)) + smil.findall(self._xpath_ns('.//audio', namespace))
for medium in media: for medium in media:
src = medium.get('src') src = medium.get('src')
if not src or src in srcs: if not src or src in srcs:
continue continue
srcs.append(src) srcs.add(src)
bitrate = float_or_none(medium.get('system-bitrate') or medium.get('systemBitrate'), 1000) bitrate = float_or_none(medium.get('system-bitrate') or medium.get('systemBitrate'), 1000)
filesize = int_or_none(medium.get('size') or medium.get('fileSize')) filesize = int_or_none(medium.get('size') or medium.get('fileSize'))
@@ -2399,6 +2432,24 @@ class InfoExtractor(object):
'height': height, 'height': height,
}) })
for medium in smil.findall(self._xpath_ns('.//imagestream', namespace)):
src = medium.get('src')
if not src or src in srcs:
continue
srcs.add(src)
imgs_count += 1
formats.append({
'format_id': 'imagestream-%d' % (imgs_count),
'url': src,
'ext': mimetype2ext(medium.get('type')),
'acodec': 'none',
'vcodec': 'none',
'width': int_or_none(medium.get('width')),
'height': int_or_none(medium.get('height')),
'format_note': 'SMIL storyboards',
})
return formats return formats
def _parse_smil_subtitles(self, smil, namespace=None, subtitles_lang='en'): def _parse_smil_subtitles(self, smil, namespace=None, subtitles_lang='en'):
@@ -2471,10 +2522,7 @@ class InfoExtractor(object):
def _extract_mpd_formats(self, *args, **kwargs): def _extract_mpd_formats(self, *args, **kwargs):
fmts, subs = self._extract_mpd_formats_and_subtitles(*args, **kwargs) fmts, subs = self._extract_mpd_formats_and_subtitles(*args, **kwargs)
if subs: if subs:
self.report_warning(bug_reports_message( self._report_ignoring_subs('DASH')
"Ignoring subtitle tracks found in the DASH manifest; "
"if any subtitle tracks are missing,"
), only_once=True)
return fmts return fmts
def _extract_mpd_formats_and_subtitles( def _extract_mpd_formats_and_subtitles(
@@ -2498,10 +2546,7 @@ class InfoExtractor(object):
def _parse_mpd_formats(self, *args, **kwargs): def _parse_mpd_formats(self, *args, **kwargs):
fmts, subs = self._parse_mpd_formats_and_subtitles(*args, **kwargs) fmts, subs = self._parse_mpd_formats_and_subtitles(*args, **kwargs)
if subs: if subs:
self.report_warning(bug_reports_message( self._report_ignoring_subs('DASH')
"Ignoring subtitle tracks found in the DASH manifest; "
"if any subtitle tracks are missing,"
), only_once=True)
return fmts return fmts
def _parse_mpd_formats_and_subtitles( def _parse_mpd_formats_and_subtitles(
@@ -2607,6 +2652,8 @@ class InfoExtractor(object):
content_type = mime_type content_type = mime_type
elif codecs.split('.')[0] == 'stpp': elif codecs.split('.')[0] == 'stpp':
content_type = 'text' content_type = 'text'
elif mimetype2ext(mime_type) in ('tt', 'dfxp', 'ttml', 'xml', 'json'):
content_type = 'text'
else: else:
self.report_warning('Unknown MIME type %s in DASH manifest' % mime_type) self.report_warning('Unknown MIME type %s in DASH manifest' % mime_type)
continue continue
@@ -2618,8 +2665,10 @@ class InfoExtractor(object):
base_url = base_url_e.text + base_url base_url = base_url_e.text + base_url
if re.match(r'^https?://', base_url): if re.match(r'^https?://', base_url):
break break
if mpd_base_url and not re.match(r'^https?://', base_url): if mpd_base_url and base_url.startswith('/'):
if not mpd_base_url.endswith('/') and not base_url.startswith('/'): base_url = compat_urlparse.urljoin(mpd_base_url, base_url)
elif mpd_base_url and not re.match(r'^https?://', base_url):
if not mpd_base_url.endswith('/'):
mpd_base_url += '/' mpd_base_url += '/'
base_url = mpd_base_url + base_url base_url = mpd_base_url + base_url
representation_id = representation_attrib.get('id') representation_id = representation_attrib.get('id')
@@ -2827,10 +2876,7 @@ class InfoExtractor(object):
def _extract_ism_formats(self, *args, **kwargs): def _extract_ism_formats(self, *args, **kwargs):
fmts, subs = self._extract_ism_formats_and_subtitles(*args, **kwargs) fmts, subs = self._extract_ism_formats_and_subtitles(*args, **kwargs)
if subs: if subs:
self.report_warning(bug_reports_message( self._report_ignoring_subs('ISM')
"Ignoring subtitle tracks found in the ISM manifest; "
"if any subtitle tracks are missing,"
))
return fmts return fmts
def _extract_ism_formats_and_subtitles(self, ism_url, video_id, ism_id=None, note=None, errnote=None, fatal=True, data=None, headers={}, query={}): def _extract_ism_formats_and_subtitles(self, ism_url, video_id, ism_id=None, note=None, errnote=None, fatal=True, data=None, headers={}, query={}):
@@ -3090,10 +3136,7 @@ class InfoExtractor(object):
def _extract_akamai_formats(self, *args, **kwargs): def _extract_akamai_formats(self, *args, **kwargs):
fmts, subs = self._extract_akamai_formats_and_subtitles(*args, **kwargs) fmts, subs = self._extract_akamai_formats_and_subtitles(*args, **kwargs)
if subs: if subs:
self.report_warning(bug_reports_message( self._report_ignoring_subs('akamai')
"Ignoring subtitle tracks found in the manifests; "
"if any subtitle tracks are missing,"
))
return fmts return fmts
def _extract_akamai_formats_and_subtitles(self, manifest_url, video_id, hosts={}): def _extract_akamai_formats_and_subtitles(self, manifest_url, video_id, hosts={}):
@@ -3467,6 +3510,32 @@ class InfoExtractor(object):
def _get_subtitles(self, *args, **kwargs): def _get_subtitles(self, *args, **kwargs):
raise NotImplementedError('This method must be implemented by subclasses') raise NotImplementedError('This method must be implemented by subclasses')
def extract_comments(self, *args, **kwargs):
if not self.get_param('getcomments'):
return None
generator = self._get_comments(*args, **kwargs)
def extractor():
comments = []
try:
while True:
comments.append(next(generator))
except KeyboardInterrupt:
interrupted = True
self.to_screen('Interrupted by user')
except StopIteration:
interrupted = False
comment_count = len(comments)
self.to_screen(f'Extracted {comment_count} comments')
return {
'comments': comments,
'comment_count': None if interrupted else comment_count
}
return extractor
def _get_comments(self, *args, **kwargs):
raise NotImplementedError('This method must be implemented by subclasses')
@staticmethod @staticmethod
def _merge_subtitle_items(subtitle_list1, subtitle_list2): def _merge_subtitle_items(subtitle_list1, subtitle_list2):
""" Merge subtitle items for one language. Items with duplicated URLs """ Merge subtitle items for one language. Items with duplicated URLs
@@ -3496,9 +3565,11 @@ class InfoExtractor(object):
raise NotImplementedError('This method must be implemented by subclasses') raise NotImplementedError('This method must be implemented by subclasses')
def mark_watched(self, *args, **kwargs): def mark_watched(self, *args, **kwargs):
if (self.get_param('mark_watched', False) if not self.get_param('mark_watched', False):
and (self._get_login_info()[0] is not None return
or self.get_param('cookiefile') is not None)): if (self._get_login_info()[0] is not None
or self.get_param('cookiefile')
or self.get_param('cookiesfrombrowser')):
self._mark_watched(*args, **kwargs) self._mark_watched(*args, **kwargs)
def _mark_watched(self, *args, **kwargs): def _mark_watched(self, *args, **kwargs):
@@ -3581,7 +3652,14 @@ class SearchInfoExtractor(InfoExtractor):
return self._get_n_results(query, n) return self._get_n_results(query, n)
def _get_n_results(self, query, n): def _get_n_results(self, query, n):
"""Get a specified number of results for a query""" """Get a specified number of results for a query.
Either this function or _search_results must be overridden by subclasses """
return self.playlist_result(
itertools.islice(self._search_results(query), 0, None if n == float('inf') else n),
query, query)
def _search_results(self, query):
"""Returns an iterator of search results"""
raise NotImplementedError('This method must be implemented by subclasses') raise NotImplementedError('This method must be implemented by subclasses')
@property @property

View File

@@ -650,7 +650,7 @@ Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text
class CrunchyrollShowPlaylistIE(CrunchyrollBaseIE): class CrunchyrollShowPlaylistIE(CrunchyrollBaseIE):
IE_NAME = 'crunchyroll:playlist' IE_NAME = 'crunchyroll:playlist'
_VALID_URL = r'https?://(?:(?P<prefix>www|m)\.)?(?P<url>crunchyroll\.com/(?!(?:news|anime-news|library|forum|launchcalendar|lineup|store|comics|freetrial|login|media-\d+))(?P<id>[\w\-]+))/?(?:\?|$)' _VALID_URL = r'https?://(?:(?P<prefix>www|m)\.)?(?P<url>crunchyroll\.com/(?:\w{1,2}/)?(?!(?:news|anime-news|library|forum|launchcalendar|lineup|store|comics|freetrial|login|media-\d+))(?P<id>[\w\-]+))/?(?:\?|$)'
_TESTS = [{ _TESTS = [{
'url': 'https://www.crunchyroll.com/a-bridge-to-the-starry-skies-hoshizora-e-kakaru-hashi', 'url': 'https://www.crunchyroll.com/a-bridge-to-the-starry-skies-hoshizora-e-kakaru-hashi',
@@ -672,6 +672,9 @@ class CrunchyrollShowPlaylistIE(CrunchyrollBaseIE):
# geo-restricted (US), 18+ maturity wall, non-premium will be available since 2015.11.14 # geo-restricted (US), 18+ maturity wall, non-premium will be available since 2015.11.14
'url': 'http://www.crunchyroll.com/ladies-versus-butlers?skip_wall=1', 'url': 'http://www.crunchyroll.com/ladies-versus-butlers?skip_wall=1',
'only_matching': True, 'only_matching': True,
}, {
'url': 'http://www.crunchyroll.com/fr/ladies-versus-butlers',
'only_matching': True,
}] }]
def _real_extract(self, url): def _real_extract(self, url):
@@ -683,18 +686,72 @@ class CrunchyrollShowPlaylistIE(CrunchyrollBaseIE):
headers=self.geo_verification_headers()) headers=self.geo_verification_headers())
title = self._html_search_meta('name', webpage, default=None) title = self._html_search_meta('name', webpage, default=None)
episode_paths = re.findall( episode_re = r'<li id="showview_videos_media_(\d+)"[^>]+>.*?<a href="([^"]+)"'
r'(?s)<li id="showview_videos_media_(\d+)"[^>]+>.*?<a href="([^"]+)"', season_re = r'<a [^>]+season-dropdown[^>]+>([^<]+)'
webpage) paths = re.findall(f'(?s){episode_re}|{season_re}', webpage)
entries = [
self.url_result('http://www.crunchyroll.com' + ep, 'Crunchyroll', ep_id) entries, current_season = [], None
for ep_id, ep in episode_paths for ep_id, ep, season in paths:
] if season:
entries.reverse() current_season = season
continue
entries.append(self.url_result(
f'http://www.crunchyroll.com{ep}', CrunchyrollIE.ie_key(), ep_id, season=current_season))
return { return {
'_type': 'playlist', '_type': 'playlist',
'id': show_id, 'id': show_id,
'title': title, 'title': title,
'entries': entries, 'entries': reversed(entries),
} }
class CrunchyrollBetaIE(CrunchyrollBaseIE):
IE_NAME = 'crunchyroll:beta'
_VALID_URL = r'https?://beta\.crunchyroll\.com/(?P<lang>(?:\w{1,2}/)?)watch/(?P<internal_id>\w+)/(?P<id>[\w\-]+)/?(?:\?|$)'
_TESTS = [{
'url': 'https://beta.crunchyroll.com/watch/GY2P1Q98Y/to-the-future',
'info_dict': {
'id': '696363',
'ext': 'mp4',
'timestamp': 1459610100,
'description': 'md5:a022fbec4fbb023d43631032c91ed64b',
'uploader': 'Toei Animation',
'title': 'World Trigger Episode 73 To the Future',
'upload_date': '20160402',
},
'params': {'skip_download': 'm3u8'},
'expected_warnings': ['Unable to download XML']
}]
def _real_extract(self, url):
lang, internal_id, display_id = self._match_valid_url(url).group('lang', 'internal_id', 'id')
webpage = self._download_webpage(url, display_id)
episode_data = self._parse_json(
self._search_regex(r'__INITIAL_STATE__\s*=\s*({.+?})\s*;', webpage, 'episode data'),
display_id)['content']['byId'][internal_id]
video_id = episode_data['external_id'].split('.')[1]
series_id = episode_data['episode_metadata']['series_slug_title']
return self.url_result(f'https://www.crunchyroll.com/{lang}{series_id}/{display_id}-{video_id}',
CrunchyrollIE.ie_key(), video_id)
class CrunchyrollBetaShowIE(CrunchyrollBaseIE):
IE_NAME = 'crunchyroll:playlist:beta'
_VALID_URL = r'https?://beta\.crunchyroll\.com/(?P<lang>(?:\w{1,2}/)?)series/\w+/(?P<id>[\w\-]+)/?(?:\?|$)'
_TESTS = [{
'url': 'https://beta.crunchyroll.com/series/GY19NQ2QR/Girl-Friend-BETA',
'info_dict': {
'id': 'girl-friend-beta',
'title': 'Girl Friend BETA',
},
'playlist_mincount': 10,
}, {
'url': 'https://beta.crunchyroll.com/it/series/GY19NQ2QR/Girl-Friend-BETA',
'only_matching': True,
}]
def _real_extract(self, url):
lang, series_id = self._match_valid_url(url).group('lang', 'id')
return self.url_result(f'https://www.crunchyroll.com/{lang}{series_id.lower()}',
CrunchyrollShowPlaylistIE.ie_key(), series_id)

113
yt_dlp/extractor/damtomo.py Normal file
View File

@@ -0,0 +1,113 @@
# coding: utf-8
from __future__ import unicode_literals
import re
from .common import InfoExtractor
from ..utils import ExtractorError, clean_html, int_or_none, try_get, unified_strdate
from ..compat import compat_str
class DamtomoBaseIE(InfoExtractor):
def _real_extract(self, url):
video_id = self._match_id(url)
webpage, handle = self._download_webpage_handle(self._WEBPAGE_URL_TMPL % video_id, video_id, encoding='sjis')
if handle.url == 'https://www.clubdam.com/sorry/':
raise ExtractorError('You are rate-limited. Try again later.', expected=True)
if '<h2>予期せぬエラーが発生しました。</h2>' in webpage:
raise ExtractorError('There is an error on server-side. Try again later.', expected=True)
description = self._search_regex(r'(?m)<div id="public_comment">\s*<p>\s*([^<]*?)\s*</p>', webpage, 'description', default=None)
uploader_id = self._search_regex(r'<a href="https://www\.clubdam\.com/app/damtomo/member/info/Profile\.do\?damtomoId=([^"]+)"', webpage, 'uploader_id', default=None)
data_dict = {
mobj.group('class'): re.sub(r'\s+', ' ', clean_html(mobj.group('value')))
for mobj in re.finditer(r'(?s)<(p|div)\s+class="(?P<class>[^" ]+?)">(?P<value>.+?)</\1>', webpage)}
# since videos do not have title, give the name of song instead
data_dict['user_name'] = re.sub(r'\s*さん\s*$', '', data_dict['user_name'])
title = data_dict.get('song_title')
stream_tree = self._download_xml(
self._DKML_XML_URL % video_id, video_id, note='Requesting stream information', encoding='sjis',
# doing this has no problem since there is no character outside ASCII,
# and never likely to happen in the future
transform_source=lambda x: re.sub(r'\s*encoding="[^"]+?"', '', x))
m3u8_url = try_get(stream_tree, lambda x: x.find(
'.//d:streamingUrl', {'d': self._DKML_XML_NS}).text.strip(), compat_str)
if not m3u8_url:
raise ExtractorError('Failed to obtain m3u8 URL')
formats = self._extract_m3u8_formats(m3u8_url, video_id, ext='mp4')
self._sort_formats(formats)
return {
'id': video_id,
'title': title,
'uploader_id': uploader_id,
'description': description,
'uploader': data_dict.get('user_name'),
'upload_date': unified_strdate(self._search_regex(r'(\d{4}/\d{2}/\d{2})', data_dict.get('date'), 'upload_date', default=None)),
'view_count': int_or_none(self._search_regex(r'(\d+)', data_dict['audience'], 'view_count', default=None)),
'like_count': int_or_none(self._search_regex(r'(\d+)', data_dict['nice'], 'like_count', default=None)),
'track': title,
'artist': data_dict.get('song_artist'),
'formats': formats,
}
class DamtomoVideoIE(DamtomoBaseIE):
IE_NAME = 'damtomo:video'
_VALID_URL = r'https?://(?:www\.)?clubdam\.com/app/damtomo/(?:SP/)?karaokeMovie/StreamingDkm\.do\?karaokeMovieId=(?P<id>\d+)'
_WEBPAGE_URL_TMPL = 'https://www.clubdam.com/app/damtomo/karaokeMovie/StreamingDkm.do?karaokeMovieId=%s'
_DKML_XML_URL = 'https://www.clubdam.com/app/damtomo/karaokeMovie/GetStreamingDkmUrlXML.do?movieSelectFlg=2&karaokeMovieId=%s'
_DKML_XML_NS = 'https://www.clubdam.com/app/damtomo/karaokeMovie/GetStreamingDkmUrlXML'
_TESTS = [{
'url': 'https://www.clubdam.com/app/damtomo/karaokeMovie/StreamingDkm.do?karaokeMovieId=2414316',
'info_dict': {
'id': '2414316',
'title': 'Get Wild',
'uploader': 'Kドロン',
'uploader_id': 'ODk5NTQwMzQ',
'track': 'Get Wild',
'artist': 'TM NETWORK(TMN)',
'upload_date': '20201226',
}
}]
class DamtomoRecordIE(DamtomoBaseIE):
IE_NAME = 'damtomo:record'
_VALID_URL = r'https?://(?:www\.)?clubdam\.com/app/damtomo/(?:SP/)?karaokePost/StreamingKrk\.do\?karaokeContributeId=(?P<id>\d+)'
_WEBPAGE_URL_TMPL = 'https://www.clubdam.com/app/damtomo/karaokePost/StreamingKrk.do?karaokeContributeId=%s'
_DKML_XML_URL = 'https://www.clubdam.com/app/damtomo/karaokePost/GetStreamingKrkUrlXML.do?karaokeContributeId=%s'
_DKML_XML_NS = 'https://www.clubdam.com/app/damtomo/karaokePost/GetStreamingKrkUrlXML'
_TESTS = [{
'url': 'https://www.clubdam.com/app/damtomo/karaokePost/StreamingKrk.do?karaokeContributeId=27376862',
'info_dict': {
'id': '27376862',
'title': 'イカSUMMER [良音]',
'description': None,
'uploader': '',
'uploader_id': 'MzAyMDExNTY',
'upload_date': '20210721',
'view_count': 4,
'like_count': 1,
'track': 'イカSUMMER [良音]',
'artist': 'ORANGE RANGE',
}
}, {
'url': 'https://www.clubdam.com/app/damtomo/karaokePost/StreamingKrk.do?karaokeContributeId=27489418',
'info_dict': {
'id': '27489418',
'title': '心みだれて〜say it with flowers〜(生音)',
'uploader_id': 'NjI1MjI2MjU',
'description': 'やっぱりキーを下げて正解だった感じ。リベンジ成功ということで。',
'uploader': '箱の「中の人」',
'upload_date': '20210815',
'view_count': 5,
'like_count': 3,
'track': '心みだれて〜say it with flowers〜(生音)',
'artist': '小林明子',
}
}]

View File

@@ -1,145 +0,0 @@
# coding: utf-8
from ..utils import (
int_or_none,
traverse_obj,
url_or_none,
)
from .common import (
InfoExtractor,
compat_urllib_parse_unquote,
)
class DouyinIE(InfoExtractor):
_VALID_URL = r'https?://(?:www\.)?douyin\.com/video/(?P<id>[0-9]+)'
_TESTS = [{
'url': 'https://www.douyin.com/video/6961737553342991651',
'md5': '10523312c8b8100f353620ac9dc8f067',
'info_dict': {
'id': '6961737553342991651',
'ext': 'mp4',
'title': '#杨超越 小小水手带你去远航❤️',
'uploader': '杨超越',
'upload_date': '20210513',
'timestamp': 1620905839,
'uploader_id': '110403406559',
'view_count': int,
'like_count': int,
'repost_count': int,
'comment_count': int,
}
}, {
'url': 'https://www.douyin.com/video/6982497745948921092',
'md5': 'd78408c984b9b5102904cf6b6bc2d712',
'info_dict': {
'id': '6982497745948921092',
'ext': 'mp4',
'title': '这个夏日和小羊@杨超越 一起遇见白色幻想',
'uploader': '杨超越工作室',
'upload_date': '20210708',
'timestamp': 1625739481,
'uploader_id': '408654318141572',
'view_count': int,
'like_count': int,
'repost_count': int,
'comment_count': int,
}
}, {
'url': 'https://www.douyin.com/video/6953975910773099811',
'md5': '72e882e24f75064c218b76c8b713c185',
'info_dict': {
'id': '6953975910773099811',
'ext': 'mp4',
'title': '#一起看海 出现在你的夏日里',
'uploader': '杨超越',
'upload_date': '20210422',
'timestamp': 1619098692,
'uploader_id': '110403406559',
'view_count': int,
'like_count': int,
'repost_count': int,
'comment_count': int,
}
}, {
'url': 'https://www.douyin.com/video/6950251282489675042',
'md5': 'b4db86aec367ef810ddd38b1737d2fed',
'info_dict': {
'id': '6950251282489675042',
'ext': 'mp4',
'title': '哈哈哈,成功了哈哈哈哈哈哈',
'uploader': '杨超越',
'upload_date': '20210412',
'timestamp': 1618231483,
'uploader_id': '110403406559',
'view_count': int,
'like_count': int,
'repost_count': int,
'comment_count': int,
}
}, {
'url': 'https://www.douyin.com/video/6963263655114722595',
'md5': '1abe1c477d05ee62efb40bf2329957cf',
'info_dict': {
'id': '6963263655114722595',
'ext': 'mp4',
'title': '#哪个爱豆的105度最甜 换个角度看看我哈哈',
'uploader': '杨超越',
'upload_date': '20210517',
'timestamp': 1621261163,
'uploader_id': '110403406559',
'view_count': int,
'like_count': int,
'repost_count': int,
'comment_count': int,
}
}]
def _real_extract(self, url):
video_id = self._match_id(url)
webpage = self._download_webpage(url, video_id)
render_data = self._parse_json(
self._search_regex(
r'<script [^>]*\bid=[\'"]RENDER_DATA[\'"][^>]*>(%7B.+%7D)</script>',
webpage, 'render data'),
video_id, transform_source=compat_urllib_parse_unquote)
details = traverse_obj(render_data, (..., 'aweme', 'detail'), get_all=False)
thumbnails = [{'url': self._proto_relative_url(url)} for url in traverse_obj(
details, ('video', ('cover', 'dynamicCover', 'originCover')), expected_type=url_or_none, default=[])]
common = {
'width': traverse_obj(details, ('video', 'width'), expected_type=int),
'height': traverse_obj(details, ('video', 'height'), expected_type=int),
'ext': 'mp4',
}
formats = [{**common, 'url': self._proto_relative_url(url)} for url in traverse_obj(
details, ('video', 'playAddr', ..., 'src'), expected_type=url_or_none, default=[]) if url]
self._remove_duplicate_formats(formats)
download_url = traverse_obj(details, ('download', 'url'), expected_type=url_or_none)
if download_url:
formats.append({
**common,
'format_id': 'download',
'url': self._proto_relative_url(download_url),
'quality': 1,
})
self._sort_formats(formats)
return {
'id': video_id,
'title': details.get('desc') or self._html_search_meta('title', webpage),
'formats': formats,
'thumbnails': thumbnails,
'uploader': traverse_obj(details, ('authorInfo', 'nickname'), expected_type=str),
'uploader_id': traverse_obj(details, ('authorInfo', 'uid'), expected_type=str),
'uploader_url': 'https://www.douyin.com/user/%s' % traverse_obj(
details, ('authorInfo', 'secUid'), expected_type=str),
'timestamp': int_or_none(details.get('createTime')),
'duration': traverse_obj(details, ('video', 'duration'), expected_type=int),
'view_count': traverse_obj(details, ('stats', 'playCount'), expected_type=int),
'like_count': traverse_obj(details, ('stats', 'diggCount'), expected_type=int),
'repost_count': traverse_obj(details, ('stats', 'shareCount'), expected_type=int),
'comment_count': traverse_obj(details, ('stats', 'commentCount'), expected_type=int),
}

View File

@@ -349,7 +349,7 @@ class DiscoveryPlusIE(DPlayIE):
_API_URL = 'us1-prod-direct.discoveryplus.com' _API_URL = 'us1-prod-direct.discoveryplus.com'
def _update_disco_api_headers(self, headers, disco_base, display_id, realm): def _update_disco_api_headers(self, headers, disco_base, display_id, realm):
headers['x-disco-client'] = f'WEB:UNKNOWN:{self._PRODUCT}:15.0.0' headers['x-disco-client'] = f'WEB:UNKNOWN:{self._PRODUCT}:25.2.6'
def _download_video_playback_info(self, disco_base, video_id, headers): def _download_video_playback_info(self, disco_base, video_id, headers):
return self._download_json( return self._download_json(
@@ -389,3 +389,43 @@ class ScienceChannelIE(DiscoveryPlusIE):
_PRODUCT = 'sci' _PRODUCT = 'sci'
_API_URL = 'us1-prod-direct.sciencechannel.com' _API_URL = 'us1-prod-direct.sciencechannel.com'
class DIYNetworkIE(DiscoveryPlusIE):
_VALID_URL = r'https?://(?:watch\.)?diynetwork\.com/video' + DPlayIE._PATH_REGEX
_TESTS = [{
'url': 'https://watch.diynetwork.com/video/pool-kings-diy-network/bringing-beach-life-to-texas',
'info_dict': {
'id': '2309730',
'display_id': 'pool-kings-diy-network/bringing-beach-life-to-texas',
'ext': 'mp4',
'title': 'Bringing Beach Life to Texas',
'description': 'The Pool Kings give a family a day at the beach in their own backyard.',
'season_number': 10,
'episode_number': 2,
},
'skip': 'Available for Premium users',
}]
_PRODUCT = 'diy'
_API_URL = 'us1-prod-direct.watch.diynetwork.com'
class AnimalPlanetIE(DiscoveryPlusIE):
_VALID_URL = r'https?://(?:www\.)?animalplanet\.com/video' + DPlayIE._PATH_REGEX
_TESTS = [{
'url': 'https://www.animalplanet.com/video/north-woods-law-animal-planet/squirrel-showdown',
'info_dict': {
'id': '3338923',
'display_id': 'north-woods-law-animal-planet/squirrel-showdown',
'ext': 'mp4',
'title': 'Squirrel Showdown',
'description': 'A woman is suspected of being in possession of flying squirrel kits.',
'season_number': 16,
'episode_number': 11,
},
'skip': 'Available for Premium users',
}]
_PRODUCT = 'apl'
_API_URL = 'us1-prod-direct.animalplanet.com'

View File

@@ -5,6 +5,7 @@ from .common import InfoExtractor
from ..utils import ( from ..utils import (
int_or_none, int_or_none,
unified_strdate, unified_strdate,
url_or_none,
) )
from ..compat import compat_urlparse from ..compat import compat_urlparse
@@ -15,13 +16,13 @@ class DWIE(InfoExtractor):
_TESTS = [{ _TESTS = [{
# video # video
'url': 'http://www.dw.com/en/intelligent-light/av-19112290', 'url': 'http://www.dw.com/en/intelligent-light/av-19112290',
'md5': '7372046e1815c5a534b43f3c3c36e6e9', 'md5': 'fb9dfd9520811d3ece80f04befd73428',
'info_dict': { 'info_dict': {
'id': '19112290', 'id': '19112290',
'ext': 'mp4', 'ext': 'mp4',
'title': 'Intelligent light', 'title': 'Intelligent light',
'description': 'md5:90e00d5881719f2a6a5827cb74985af1', 'description': 'md5:90e00d5881719f2a6a5827cb74985af1',
'upload_date': '20160311', 'upload_date': '20160605',
} }
}, { }, {
# audio # audio
@@ -55,15 +56,16 @@ class DWIE(InfoExtractor):
title = hidden_inputs['media_title'] title = hidden_inputs['media_title']
media_id = hidden_inputs.get('media_id') or media_id media_id = hidden_inputs.get('media_id') or media_id
if hidden_inputs.get('player_type') == 'video' and hidden_inputs.get('stream_file') == '1': direct_url = url_or_none(hidden_inputs.get('file_name'))
if direct_url:
formats = [{'url': hidden_inputs['file_name']}]
else:
formats = self._extract_smil_formats( formats = self._extract_smil_formats(
'http://www.dw.com/smil/v-%s' % media_id, media_id, 'http://www.dw.com/smil/v-%s' % media_id, media_id,
transform_source=lambda s: s.replace( transform_source=lambda s: s.replace(
'rtmp://tv-od.dw.de/flash/', 'rtmp://tv-od.dw.de/flash/',
'http://tv-download.dw.de/dwtv_video/flv/')) 'http://tv-download.dw.de/dwtv_video/flv/'))
self._sort_formats(formats) self._sort_formats(formats)
else:
formats = [{'url': hidden_inputs['file_name']}]
upload_date = hidden_inputs.get('display_date') upload_date = hidden_inputs.get('display_date')
if not upload_date: if not upload_date:

View File

@@ -0,0 +1,64 @@
# coding: utf-8
from __future__ import unicode_literals
from .common import InfoExtractor
from ..utils import (
parse_duration,
js_to_json,
)
class EUScreenIE(InfoExtractor):
_VALID_URL = r'(?:https?://)(?:www\.)?euscreen\.eu/item.html\?id=(?P<id>[^&?$/]+)'
_TESTS = [{
'url': 'https://euscreen.eu/item.html?id=EUS_0EBCBF356BFC4E12A014023BA41BD98C',
'info_dict': {
'id': 'EUS_0EBCBF356BFC4E12A014023BA41BD98C',
'ext': 'mp4',
'title': "L'effondrement du stade du Heysel",
'alt_title': 'Collapse of the Heysel Stadium',
'duration': 318.0,
'description': 'md5:f0ffffdfce6821139357a1b8359d6152',
'series': 'JA2 DERNIERE',
'episode': '-',
'uploader': 'INA / France',
'thumbnail': 'http://images3.noterik.com/domain/euscreenxl/user/eu_ina/video/EUS_0EBCBF356BFC4E12A014023BA41BD98C/image.jpg'
},
'params': {'skip_download': True}
}]
_payload = b'<fsxml><screen><properties><screenId>-1</screenId></properties><capabilities id="1"><properties><platform>Win32</platform><appcodename>Mozilla</appcodename><appname>Netscape</appname><appversion>5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/94.0.4606.71 Safari/537.36</appversion><useragent>Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/94.0.4606.71 Safari/537.36</useragent><cookiesenabled>true</cookiesenabled><screenwidth>784</screenwidth><screenheight>758</screenheight><orientation>undefined</orientation><smt_browserid>Sat, 07 Oct 2021 08:56:50 GMT</smt_browserid><smt_sessionid>1633769810758</smt_sessionid></properties></capabilities></screen></fsxml>'
def _real_extract(self, url):
id = self._match_id(url)
args_for_js_request = self._download_webpage(
'https://euscreen.eu/lou/LouServlet/domain/euscreenxl/html5application/euscreenxlitem',
id, data=self._payload, query={'actionlist': 'itempage', 'id': id})
info_js = self._download_webpage(
'https://euscreen.eu/lou/LouServlet/domain/euscreenxl/html5application/euscreenxlitem',
id, data=args_for_js_request.replace('screenid', 'screenId').encode())
video_json = self._parse_json(
self._search_regex(r'setVideo\(({.+})\)\(\$end\$\)put', info_js, 'Video JSON'),
id, transform_source=js_to_json)
meta_json = self._parse_json(
self._search_regex(r'setData\(({.+})\)\(\$end\$\)', info_js, 'Metadata JSON'),
id, transform_source=js_to_json)
formats = [{
'url': source['src'],
} for source in video_json.get('sources', [])]
self._sort_formats(formats)
return {
'id': id,
'title': meta_json.get('originalTitle'),
'alt_title': meta_json.get('title'),
'duration': parse_duration(meta_json.get('duration')),
'description': '%s\n%s' % (meta_json.get('summaryOriginal', ''), meta_json.get('summaryEnglish', '')),
'series': meta_json.get('series') or meta_json.get('seriesEnglish'),
'episode': meta_json.get('episodeNumber'),
'uploader': meta_json.get('provider'),
'thumbnail': meta_json.get('screenshot') or video_json.get('screenshot'),
'formats': formats,
}

View File

@@ -147,6 +147,8 @@ from .bilibili import (
BilibiliAudioAlbumIE, BilibiliAudioAlbumIE,
BiliBiliPlayerIE, BiliBiliPlayerIE,
BilibiliChannelIE, BilibiliChannelIE,
BiliIntlIE,
BiliIntlSeriesIE,
) )
from .biobiochiletv import BioBioChileTVIE from .biobiochiletv import BioBioChileTVIE
from .bitchute import ( from .bitchute import (
@@ -183,6 +185,7 @@ from .businessinsider import BusinessInsiderIE
from .buzzfeed import BuzzFeedIE from .buzzfeed import BuzzFeedIE
from .byutv import BYUtvIE from .byutv import BYUtvIE
from .c56 import C56IE from .c56 import C56IE
from .cam4 import CAM4IE
from .camdemy import ( from .camdemy import (
CamdemyIE, CamdemyIE,
CamdemyFolderIE CamdemyFolderIE
@@ -205,9 +208,9 @@ from .cartoonnetwork import CartoonNetworkIE
from .cbc import ( from .cbc import (
CBCIE, CBCIE,
CBCPlayerIE, CBCPlayerIE,
CBCWatchVideoIE, CBCGemIE,
CBCWatchIE, CBCGemPlaylistIE,
CBCOlympicsIE, CBCGemLiveIE,
) )
from .cbs import CBSIE from .cbs import CBSIE
from .cbslocal import ( from .cbslocal import (
@@ -236,10 +239,15 @@ from .ceskatelevize import (
CeskaTelevizeIE, CeskaTelevizeIE,
CeskaTelevizePoradyIE, CeskaTelevizePoradyIE,
) )
from .cgtn import CGTNIE
from .channel9 import Channel9IE from .channel9 import Channel9IE
from .charlierose import CharlieRoseIE from .charlierose import CharlieRoseIE
from .chaturbate import ChaturbateIE from .chaturbate import ChaturbateIE
from .chilloutzone import ChilloutzoneIE from .chilloutzone import ChilloutzoneIE
from .chingari import (
ChingariIE,
ChingariUserIE,
)
from .chirbit import ( from .chirbit import (
ChirbitIE, ChirbitIE,
ChirbitProfileIE, ChirbitProfileIE,
@@ -250,6 +258,7 @@ from .ciscolive import (
CiscoLiveSessionIE, CiscoLiveSessionIE,
CiscoLiveSearchIE, CiscoLiveSearchIE,
) )
from .ciscowebex import CiscoWebexIE
from .cjsw import CJSWIE from .cjsw import CJSWIE
from .cliphunter import CliphunterIE from .cliphunter import CliphunterIE
from .clippit import ClippitIE from .clippit import ClippitIE
@@ -289,7 +298,9 @@ from .crackle import CrackleIE
from .crooksandliars import CrooksAndLiarsIE from .crooksandliars import CrooksAndLiarsIE
from .crunchyroll import ( from .crunchyroll import (
CrunchyrollIE, CrunchyrollIE,
CrunchyrollShowPlaylistIE CrunchyrollShowPlaylistIE,
CrunchyrollBetaIE,
CrunchyrollBetaShowIE,
) )
from .cspan import CSpanIE from .cspan import CSpanIE
from .ctsnews import CtsNewsIE from .ctsnews import CtsNewsIE
@@ -307,6 +318,10 @@ from .dailymotion import (
DailymotionPlaylistIE, DailymotionPlaylistIE,
DailymotionUserIE, DailymotionUserIE,
) )
from .damtomo import (
DamtomoRecordIE,
DamtomoVideoIE,
)
from .daum import ( from .daum import (
DaumIE, DaumIE,
DaumClipIE, DaumClipIE,
@@ -328,7 +343,6 @@ from .discoveryplusindia import (
DiscoveryPlusIndiaShowIE, DiscoveryPlusIndiaShowIE,
) )
from .dotsub import DotsubIE from .dotsub import DotsubIE
from .douyin import DouyinIE
from .douyutv import ( from .douyutv import (
DouyuShowIE, DouyuShowIE,
DouyuTVIE, DouyuTVIE,
@@ -337,7 +351,9 @@ from .dplay import (
DPlayIE, DPlayIE,
DiscoveryPlusIE, DiscoveryPlusIE,
HGTVDeIE, HGTVDeIE,
ScienceChannelIE ScienceChannelIE,
DIYNetworkIE,
AnimalPlanetIE
) )
from .dreisat import DreiSatIE from .dreisat import DreiSatIE
from .drbonanza import DRBonanzaIE from .drbonanza import DRBonanzaIE
@@ -406,6 +422,7 @@ from .espn import (
) )
from .esri import EsriVideoIE from .esri import EsriVideoIE
from .europa import EuropaIE from .europa import EuropaIE
from .euscreen import EUScreenIE
from .expotv import ExpoTVIE from .expotv import ExpoTVIE
from .expressen import ExpressenIE from .expressen import ExpressenIE
from .extremetube import ExtremeTubeIE from .extremetube import ExtremeTubeIE
@@ -460,12 +477,7 @@ from .franceinter import FranceInterIE
from .francetv import ( from .francetv import (
FranceTVIE, FranceTVIE,
FranceTVSiteIE, FranceTVSiteIE,
FranceTVEmbedIE,
FranceTVInfoIE, FranceTVInfoIE,
FranceTVInfoSportIE,
FranceTVJeunesseIE,
GenerationWhatIE,
CultureboxIE,
) )
from .freesound import FreesoundIE from .freesound import FreesoundIE
from .freespeech import FreespeechIE from .freespeech import FreespeechIE
@@ -493,6 +505,7 @@ from .gazeta import GazetaIE
from .gdcvault import GDCVaultIE from .gdcvault import GDCVaultIE
from .gedidigital import GediDigitalIE from .gedidigital import GediDigitalIE
from .generic import GenericIE from .generic import GenericIE
from .gettr import GettrIE
from .gfycat import GfycatIE from .gfycat import GfycatIE
from .giantbomb import GiantBombIE from .giantbomb import GiantBombIE
from .giga import GigaIE from .giga import GigaIE
@@ -510,8 +523,11 @@ from .googlepodcasts import (
GooglePodcastsFeedIE, GooglePodcastsFeedIE,
) )
from .googlesearch import GoogleSearchIE from .googlesearch import GoogleSearchIE
from .gopro import GoProIE
from .goshgay import GoshgayIE from .goshgay import GoshgayIE
from .gotostage import GoToStageIE
from .gputechconf import GPUTechConfIE from .gputechconf import GPUTechConfIE
from .gronkh import GronkhIE
from .groupon import GrouponIE from .groupon import GrouponIE
from .hbo import HBOIE from .hbo import HBOIE
from .hearthisat import HearThisAtIE from .hearthisat import HearThisAtIE
@@ -547,6 +563,10 @@ from .hungama import (
HungamaAlbumPlaylistIE, HungamaAlbumPlaylistIE,
) )
from .hypem import HypemIE from .hypem import HypemIE
from .ichinanalive import (
IchinanaLiveIE,
IchinanaLiveClipIE,
)
from .ign import ( from .ign import (
IGNIE, IGNIE,
IGNVideoIE, IGNVideoIE,
@@ -613,6 +633,7 @@ from .kickstarter import KickStarterIE
from .kinja import KinjaEmbedIE from .kinja import KinjaEmbedIE
from .kinopoisk import KinoPoiskIE from .kinopoisk import KinoPoiskIE
from .konserthusetplay import KonserthusetPlayIE from .konserthusetplay import KonserthusetPlayIE
from .koo import KooIE
from .krasview import KrasViewIE from .krasview import KrasViewIE
from .ku6 import Ku6IE from .ku6 import Ku6IE
from .kusi import KUSIIE from .kusi import KUSIIE
@@ -721,6 +742,8 @@ from .massengeschmacktv import MassengeschmackTVIE
from .matchtv import MatchTVIE from .matchtv import MatchTVIE
from .mdr import MDRIE from .mdr import MDRIE
from .medaltv import MedalTVIE from .medaltv import MedalTVIE
from .mediaite import MediaiteIE
from .mediaklikk import MediaKlikkIE
from .mediaset import MediasetIE from .mediaset import MediasetIE
from .mediasite import ( from .mediasite import (
MediasiteIE, MediasiteIE,
@@ -737,6 +760,7 @@ from .metacritic import MetacriticIE
from .mgoon import MgoonIE from .mgoon import MgoonIE
from .mgtv import MGTVIE from .mgtv import MGTVIE
from .miaopai import MiaoPaiIE from .miaopai import MiaoPaiIE
from .microsoftstream import MicrosoftStreamIE
from .microsoftvirtualacademy import ( from .microsoftvirtualacademy import (
MicrosoftVirtualAcademyIE, MicrosoftVirtualAcademyIE,
MicrosoftVirtualAcademyCourseIE, MicrosoftVirtualAcademyCourseIE,
@@ -796,6 +820,7 @@ from .mtv import (
MTVItaliaProgrammaIE, MTVItaliaProgrammaIE,
) )
from .muenchentv import MuenchenTVIE from .muenchentv import MuenchenTVIE
from .musescore import MuseScoreIE
from .mwave import MwaveIE, MwaveMeetGreetIE from .mwave import MwaveIE, MwaveMeetGreetIE
from .mxplayer import ( from .mxplayer import (
MxplayerIE, MxplayerIE,
@@ -810,6 +835,7 @@ from .myvi import (
) )
from .myvideoge import MyVideoGeIE from .myvideoge import MyVideoGeIE
from .myvidster import MyVidsterIE from .myvidster import MyVidsterIE
from .n1 import N1InfoIIE, N1InfoAssetIE
from .nationalgeographic import ( from .nationalgeographic import (
NationalGeographicVideoIE, NationalGeographicVideoIE,
NationalGeographicTVIE, NationalGeographicTVIE,
@@ -858,6 +884,7 @@ from .neteasemusic import (
from .newgrounds import ( from .newgrounds import (
NewgroundsIE, NewgroundsIE,
NewgroundsPlaylistIE, NewgroundsPlaylistIE,
NewgroundsUserIE,
) )
from .newstube import NewstubeIE from .newstube import NewstubeIE
from .nextmedia import ( from .nextmedia import (
@@ -911,6 +938,7 @@ from .nova import (
NovaEmbedIE, NovaEmbedIE,
NovaIE, NovaIE,
) )
from .novaplay import NovaPlayIE
from .nowness import ( from .nowness import (
NownessIE, NownessIE,
NownessPlaylistIE, NownessPlaylistIE,
@@ -951,10 +979,13 @@ from .nytimes import (
NYTimesCookingIE, NYTimesCookingIE,
) )
from .nuvid import NuvidIE from .nuvid import NuvidIE
from .nzherald import NZHeraldIE
from .nzz import NZZIE from .nzz import NZZIE
from .odatv import OdaTVIE from .odatv import OdaTVIE
from .odnoklassniki import OdnoklassnikiIE from .odnoklassniki import OdnoklassnikiIE
from .oktoberfesttv import OktoberfestTVIE from .oktoberfesttv import OktoberfestTVIE
from .olympics import OlympicsReplayIE
from .on24 import On24IE
from .ondemandkorea import OnDemandKoreaIE from .ondemandkorea import OnDemandKoreaIE
from .onet import ( from .onet import (
OnetIE, OnetIE,
@@ -1012,7 +1043,10 @@ from .patreon import (
) )
from .pbs import PBSIE from .pbs import PBSIE
from .pearvideo import PearVideoIE from .pearvideo import PearVideoIE
from .peertube import PeerTubeIE from .peertube import (
PeerTubeIE,
PeerTubePlaylistIE,
)
from .peloton import ( from .peloton import (
PelotonIE, PelotonIE,
PelotonLiveIE PelotonLiveIE
@@ -1107,6 +1141,11 @@ from .radiode import RadioDeIE
from .radiojavan import RadioJavanIE from .radiojavan import RadioJavanIE
from .radiobremen import RadioBremenIE from .radiobremen import RadioBremenIE
from .radiofrance import RadioFranceIE from .radiofrance import RadioFranceIE
from .radlive import (
RadLiveIE,
RadLiveChannelIE,
RadLiveSeasonIE,
)
from .rai import ( from .rai import (
RaiPlayIE, RaiPlayIE,
RaiPlayLiveIE, RaiPlayLiveIE,
@@ -1170,7 +1209,10 @@ from .rtve import RTVEALaCartaIE, RTVELiveIE, RTVEInfantilIE, RTVELiveIE, RTVETe
from .rtvnh import RTVNHIE from .rtvnh import RTVNHIE
from .rtvs import RTVSIE from .rtvs import RTVSIE
from .ruhd import RUHDIE from .ruhd import RUHDIE
from .rumble import RumbleEmbedIE from .rumble import (
RumbleEmbedIE,
RumbleChannelIE,
)
from .rutube import ( from .rutube import (
RutubeIE, RutubeIE,
RutubeChannelIE, RutubeChannelIE,
@@ -1244,6 +1286,7 @@ from .skynewsarabia import (
SkyNewsArabiaIE, SkyNewsArabiaIE,
SkyNewsArabiaArticleIE, SkyNewsArabiaArticleIE,
) )
from .skynewsau import SkyNewsAUIE
from .sky import ( from .sky import (
SkyNewsIE, SkyNewsIE,
SkySportsIE, SkySportsIE,
@@ -1278,6 +1321,10 @@ from .southpark import (
SouthParkEsIE, SouthParkEsIE,
SouthParkNlIE SouthParkNlIE
) )
from .sovietscloset import (
SovietsClosetIE,
SovietsClosetPlaylistIE
)
from .spankbang import ( from .spankbang import (
SpankBangIE, SpankBangIE,
SpankBangPlaylistIE, SpankBangPlaylistIE,
@@ -1321,6 +1368,7 @@ from .storyfire import (
StoryFireSeriesIE, StoryFireSeriesIE,
) )
from .streamable import StreamableIE from .streamable import StreamableIE
from .streamanity import StreamanityIE
from .streamcloud import StreamcloudIE from .streamcloud import StreamcloudIE
from .streamcz import StreamCZIE from .streamcz import StreamCZIE
from .streetvoice import StreetVoiceIE from .streetvoice import StreetVoiceIE
@@ -1340,10 +1388,7 @@ from .svt import (
from .swrmediathek import SWRMediathekIE from .swrmediathek import SWRMediathekIE
from .syfy import SyfyIE from .syfy import SyfyIE
from .sztvhu import SztvHuIE from .sztvhu import SztvHuIE
from .tagesschau import ( from .tagesschau import TagesschauIE
TagesschauPlayerIE,
TagesschauIE,
)
from .tass import TassIE from .tass import TassIE
from .tbs import TBSIE from .tbs import TBSIE
from .tdslifeway import TDSLifewayIE from .tdslifeway import TDSLifewayIE
@@ -1389,6 +1434,10 @@ from .theplatform import (
from .thescene import TheSceneIE from .thescene import TheSceneIE
from .thestar import TheStarIE from .thestar import TheStarIE
from .thesun import TheSunIE from .thesun import TheSunIE
from .theta import (
ThetaVideoIE,
ThetaStreamIE,
)
from .theweatherchannel import TheWeatherChannelIE from .theweatherchannel import TheWeatherChannelIE
from .thisamericanlife import ThisAmericanLifeIE from .thisamericanlife import ThisAmericanLifeIE
from .thisav import ThisAVIE from .thisav import ThisAVIE
@@ -1397,6 +1446,7 @@ from .threeqsdn import ThreeQSDNIE
from .tiktok import ( from .tiktok import (
TikTokIE, TikTokIE,
TikTokUserIE, TikTokUserIE,
DouyinIE,
) )
from .tinypic import TinyPicIE from .tinypic import TinyPicIE
from .tmz import TMZIE from .tmz import TMZIE
@@ -1423,6 +1473,8 @@ from .trilulilu import TriluliluIE
from .trovo import ( from .trovo import (
TrovoIE, TrovoIE,
TrovoVodIE, TrovoVodIE,
TrovoChannelVodIE,
TrovoChannelClipIE,
) )
from .trunews import TruNewsIE from .trunews import TruNewsIE
from .trutv import TruTVIE from .trutv import TruTVIE
@@ -1560,6 +1612,7 @@ from .utreon import UtreonIE
from .varzesh3 import Varzesh3IE from .varzesh3 import Varzesh3IE
from .vbox7 import Vbox7IE from .vbox7 import Vbox7IE
from .veehd import VeeHDIE from .veehd import VeeHDIE
from .veo import VeoIE
from .veoh import VeohIE from .veoh import VeohIE
from .vesti import VestiIE from .vesti import VestiIE
from .vevo import ( from .vevo import (
@@ -1594,11 +1647,6 @@ from .vidio import (
VidioLiveIE VidioLiveIE
) )
from .vidlii import VidLiiIE from .vidlii import VidLiiIE
from .vidme import (
VidmeIE,
VidmeUserIE,
VidmeUserLikesIE,
)
from .vier import VierIE, VierVideosIE from .vier import VierIE, VierVideosIE
from .viewlift import ( from .viewlift import (
ViewLiftIE, ViewLiftIE,
@@ -1669,6 +1717,7 @@ from .vtm import VTMIE
from .medialaan import MedialaanIE from .medialaan import MedialaanIE
from .vube import VubeIE from .vube import VubeIE
from .vuclip import VuClipIE from .vuclip import VuClipIE
from .vupload import VuploadIE
from .vvvvid import ( from .vvvvid import (
VVVVIDIE, VVVVIDIE,
VVVVIDShowIE, VVVVIDShowIE,
@@ -1755,7 +1804,11 @@ from .yandexmusic import (
YandexMusicArtistTracksIE, YandexMusicArtistTracksIE,
YandexMusicArtistAlbumsIE, YandexMusicArtistAlbumsIE,
) )
from .yandexvideo import YandexVideoIE from .yandexvideo import (
YandexVideoIE,
ZenYandexIE,
ZenYandexChannelIE,
)
from .yapfiles import YapFilesIE from .yapfiles import YapFilesIE
from .yesjapan import YesJapanIE from .yesjapan import YesJapanIE
from .yinyuetai import YinYueTaiIE from .yinyuetai import YinYueTaiIE
@@ -1775,6 +1828,7 @@ from .yourporn import YourPornIE
from .yourupload import YourUploadIE from .yourupload import YourUploadIE
from .youtube import ( from .youtube import (
YoutubeIE, YoutubeIE,
YoutubeClipIE,
YoutubeFavouritesIE, YoutubeFavouritesIE,
YoutubeHistoryIE, YoutubeHistoryIE,
YoutubeTabIE, YoutubeTabIE,

View File

@@ -173,7 +173,7 @@ class FancodeLiveIE(FancodeVodIE):
match_info = try_get(info_json, lambda x: x['data']['match']) match_info = try_get(info_json, lambda x: x['data']['match'])
if match_info.get('status') != "LIVE": if match_info.get('streamingStatus') != "STARTED":
raise ExtractorError('The stream can\'t be accessed', expected=True) raise ExtractorError('The stream can\'t be accessed', expected=True)
self._check_login_required(match_info.get('isUserEntitled'), True) # all live streams are premium only self._check_login_required(match_info.get('isUserEntitled'), True) # all live streams are premium only

View File

@@ -4,19 +4,12 @@ from __future__ import unicode_literals
from .common import InfoExtractor from .common import InfoExtractor
from ..compat import (
compat_str,
)
from ..utils import ( from ..utils import (
clean_html,
determine_ext, determine_ext,
ExtractorError, ExtractorError,
int_or_none, format_field,
parse_duration, parse_iso8601,
parse_qs, parse_qs,
try_get,
url_or_none,
urljoin,
) )
from .dailymotion import DailymotionIE from .dailymotion import DailymotionIE
@@ -89,97 +82,81 @@ class FranceTVIE(InfoExtractor):
# Videos are identified by idDiffusion so catalogue part is optional. # Videos are identified by idDiffusion so catalogue part is optional.
# However when provided, some extra formats may be returned so we pass # However when provided, some extra formats may be returned so we pass
# it if available. # it if available.
info = self._download_json(
'https://sivideo.webservices.francetelevisions.fr/tools/getInfosOeuvre/v2/',
video_id, 'Downloading video JSON', query={
'idDiffusion': video_id,
'catalogue': catalogue or '',
})
if info.get('status') == 'NOK':
raise ExtractorError(
'%s returned error: %s' % (self.IE_NAME, info['message']),
expected=True)
allowed_countries = info['videos'][0].get('geoblocage')
if allowed_countries:
georestricted = True
geo_info = self._download_json(
'http://geo.francetv.fr/ws/edgescape.json', video_id,
'Downloading geo restriction info')
country = geo_info['reponse']['geo_info']['country_code']
if country not in allowed_countries:
raise ExtractorError(
'The video is not available from your location',
expected=True)
else:
georestricted = False
def sign(manifest_url, manifest_id):
for host in ('hdfauthftv-a.akamaihd.net', 'hdfauth.francetv.fr'):
signed_url = url_or_none(self._download_webpage(
'https://%s/esi/TA' % host, video_id,
'Downloading signed %s manifest URL' % manifest_id,
fatal=False, query={
'url': manifest_url,
}))
if signed_url:
return signed_url
return manifest_url
is_live = None is_live = None
videos = [] videos = []
title = None
subtitle = None
image = None
duration = None
timestamp = None
spritesheets = None
for video in (info.get('videos') or []): for device_type in ('desktop', 'mobile'):
if video.get('statut') != 'ONLINE': dinfo = self._download_json(
continue
if not video.get('url'):
continue
videos.append(video)
if not videos:
for device_type in ['desktop', 'mobile']:
fallback_info = self._download_json(
'https://player.webservices.francetelevisions.fr/v1/videos/%s' % video_id, 'https://player.webservices.francetelevisions.fr/v1/videos/%s' % video_id,
video_id, 'Downloading fallback %s video JSON' % device_type, query={ video_id, 'Downloading %s video JSON' % device_type, query={
'device_type': device_type, 'device_type': device_type,
'browser': 'chrome', 'browser': 'chrome',
}, fatal=False) }, fatal=False)
if fallback_info and fallback_info.get('video'): if not dinfo:
videos.append(fallback_info['video']) continue
video = dinfo.get('video')
if video:
videos.append(video)
if duration is None:
duration = video.get('duration')
if is_live is None:
is_live = video.get('is_live')
if spritesheets is None:
spritesheets = video.get('spritesheets')
meta = dinfo.get('meta')
if meta:
if title is None:
title = meta.get('title')
# XXX: what is meta['pre_title']?
if subtitle is None:
subtitle = meta.get('additional_title')
if image is None:
image = meta.get('image_url')
if timestamp is None:
timestamp = parse_iso8601(meta.get('broadcasted_at'))
formats = [] formats = []
subtitles = {} subtitles = {}
for video in videos: for video in videos:
video_url = video.get('url')
if not video_url:
continue
if is_live is None:
is_live = (try_get(
video, lambda x: x['plages_ouverture'][0]['direct'], bool) is True
or video.get('is_live') is True
or '/live.francetv.fr/' in video_url)
format_id = video.get('format') format_id = video.get('format')
video_url = None
if video.get('workflow') == 'token-akamai':
token_url = video.get('token')
if token_url:
token_json = self._download_json(
token_url, video_id,
'Downloading signed %s manifest URL' % format_id)
if token_json:
video_url = token_json.get('url')
if not video_url:
video_url = video.get('url')
ext = determine_ext(video_url) ext = determine_ext(video_url)
if ext == 'f4m': if ext == 'f4m':
if georestricted:
# See https://github.com/ytdl-org/youtube-dl/issues/3963
# m3u8 urls work fine
continue
formats.extend(self._extract_f4m_formats( formats.extend(self._extract_f4m_formats(
sign(video_url, format_id) + '&hdcore=3.7.0&plugin=aasp-3.7.0.39.44', video_url, video_id, f4m_id=format_id, fatal=False))
video_id, f4m_id=format_id, fatal=False))
elif ext == 'm3u8': elif ext == 'm3u8':
m3u8_fmts, m3u8_subs = self._extract_m3u8_formats_and_subtitles( fmts, subs = self._extract_m3u8_formats_and_subtitles(
sign(video_url, format_id), video_id, 'mp4', video_url, video_id, 'mp4',
entry_protocol='m3u8_native', m3u8_id=format_id, entry_protocol='m3u8_native', m3u8_id=format_id,
fatal=False) fatal=False)
formats.extend(m3u8_fmts) formats.extend(fmts)
subtitles = self._merge_subtitles(subtitles, m3u8_subs) self._merge_subtitles(subs, target=subtitles)
elif ext == 'mpd': elif ext == 'mpd':
formats.extend(self._extract_mpd_formats( fmts, subs = self._extract_mpd_formats_and_subtitles(
sign(video_url, format_id), video_id, mpd_id=format_id, fatal=False)) video_url, video_id, mpd_id=format_id, fatal=False)
formats.extend(fmts)
self._merge_subtitles(subs, target=subtitles)
elif video_url.startswith('rtmp'): elif video_url.startswith('rtmp'):
formats.append({ formats.append({
'url': video_url, 'url': video_url,
@@ -193,28 +170,43 @@ class FranceTVIE(InfoExtractor):
'format_id': format_id, 'format_id': format_id,
}) })
# XXX: what is video['captions']?
for f in formats:
if f.get('acodec') != 'none' and f.get('language') in ('qtz', 'qad'):
f['language_preference'] = -10
f['format_note'] = 'audio description%s' % format_field(f, 'format_note', ', %s')
if spritesheets:
formats.append({
'format_id': 'spritesheets',
'format_note': 'storyboard',
'acodec': 'none',
'vcodec': 'none',
'ext': 'mhtml',
'protocol': 'mhtml',
'url': 'about:dummy',
'fragments': [{
'path': sheet,
# XXX: not entirely accurate; each spritesheet seems to be
# a 10×10 grid of thumbnails corresponding to approximately
# 2 seconds of the video; the last spritesheet may be shorter
'duration': 200,
} for sheet in spritesheets]
})
self._sort_formats(formats) self._sort_formats(formats)
title = info['titre']
subtitle = info.get('sous_titre')
if subtitle: if subtitle:
title += ' - %s' % subtitle title += ' - %s' % subtitle
title = title.strip() title = title.strip()
subtitles.setdefault('fr', []).extend(
[{
'url': subformat['url'],
'ext': subformat.get('format'),
} for subformat in info.get('subtitles', []) if subformat.get('url')]
)
return { return {
'id': video_id, 'id': video_id,
'title': self._live_title(title) if is_live else title, 'title': self._live_title(title) if is_live else title,
'description': clean_html(info.get('synopsis')), 'thumbnail': image,
'thumbnail': urljoin('https://sivideo.webservices.francetelevisions.fr', info.get('image')), 'duration': duration,
'duration': int_or_none(info.get('real_duration')) or parse_duration(info.get('duree')), 'timestamp': timestamp,
'timestamp': int_or_none(try_get(info, lambda x: x['diffusion']['timestamp'])),
'is_live': is_live, 'is_live': is_live,
'formats': formats, 'formats': formats,
'subtitles': subtitles, 'subtitles': subtitles,
@@ -308,35 +300,6 @@ class FranceTVSiteIE(FranceTVBaseInfoExtractor):
return self._make_url_result(video_id, catalogue) return self._make_url_result(video_id, catalogue)
class FranceTVEmbedIE(FranceTVBaseInfoExtractor):
_VALID_URL = r'https?://embed\.francetv\.fr/*\?.*?\bue=(?P<id>[^&]+)'
_TESTS = [{
'url': 'http://embed.francetv.fr/?ue=7fd581a2ccf59d2fc5719c5c13cf6961',
'info_dict': {
'id': 'NI_983319',
'ext': 'mp4',
'title': 'Le Pen Reims',
'upload_date': '20170505',
'timestamp': 1493981780,
'duration': 16,
},
'params': {
'skip_download': True,
},
'add_ie': [FranceTVIE.ie_key()],
}]
def _real_extract(self, url):
video_id = self._match_id(url)
video = self._download_json(
'http://api-embed.webservices.francetelevisions.fr/key/%s' % video_id,
video_id)
return self._make_url_result(video['video_id'], video.get('catalog'))
class FranceTVInfoIE(FranceTVBaseInfoExtractor): class FranceTVInfoIE(FranceTVBaseInfoExtractor):
IE_NAME = 'francetvinfo.fr' IE_NAME = 'francetvinfo.fr'
_VALID_URL = r'https?://(?:www|mobile|france3-regions)\.francetvinfo\.fr/(?:[^/]+/)*(?P<id>[^/?#&.]+)' _VALID_URL = r'https?://(?:www|mobile|france3-regions)\.francetvinfo\.fr/(?:[^/]+/)*(?P<id>[^/?#&.]+)'
@@ -426,139 +389,3 @@ class FranceTVInfoIE(FranceTVBaseInfoExtractor):
webpage, 'video id') webpage, 'video id')
return self._make_url_result(video_id) return self._make_url_result(video_id)
class FranceTVInfoSportIE(FranceTVBaseInfoExtractor):
IE_NAME = 'sport.francetvinfo.fr'
_VALID_URL = r'https?://sport\.francetvinfo\.fr/(?:[^/]+/)*(?P<id>[^/?#&]+)'
_TESTS = [{
'url': 'https://sport.francetvinfo.fr/les-jeux-olympiques/retour-sur-les-meilleurs-moments-de-pyeongchang-2018',
'info_dict': {
'id': '6e49080e-3f45-11e8-b459-000d3a2439ea',
'ext': 'mp4',
'title': 'Retour sur les meilleurs moments de Pyeongchang 2018',
'timestamp': 1523639962,
'upload_date': '20180413',
},
'params': {
'skip_download': True,
},
'add_ie': [FranceTVIE.ie_key()],
}]
def _real_extract(self, url):
display_id = self._match_id(url)
webpage = self._download_webpage(url, display_id)
video_id = self._search_regex(r'data-video="([^"]+)"', webpage, 'video_id')
return self._make_url_result(video_id, 'Sport-web')
class GenerationWhatIE(InfoExtractor):
IE_NAME = 'france2.fr:generation-what'
_VALID_URL = r'https?://generation-what\.francetv\.fr/[^/]+/video/(?P<id>[^/?#&]+)'
_TESTS = [{
'url': 'http://generation-what.francetv.fr/portrait/video/present-arms',
'info_dict': {
'id': 'wtvKYUG45iw',
'ext': 'mp4',
'title': 'Generation What - Garde à vous - FRA',
'uploader': 'Generation What',
'uploader_id': 'UCHH9p1eetWCgt4kXBYCb3_w',
'upload_date': '20160411',
},
'params': {
'skip_download': True,
},
'add_ie': ['Youtube'],
}, {
'url': 'http://generation-what.francetv.fr/europe/video/present-arms',
'only_matching': True,
}]
def _real_extract(self, url):
display_id = self._match_id(url)
webpage = self._download_webpage(url, display_id)
youtube_id = self._search_regex(
r"window\.videoURL\s*=\s*'([0-9A-Za-z_-]{11})';",
webpage, 'youtube id')
return self.url_result(youtube_id, ie='Youtube', video_id=youtube_id)
class CultureboxIE(FranceTVBaseInfoExtractor):
_VALID_URL = r'https?://(?:m\.)?culturebox\.francetvinfo\.fr/(?:[^/]+/)*(?P<id>[^/?#&]+)'
_TESTS = [{
'url': 'https://culturebox.francetvinfo.fr/opera-classique/musique-classique/c-est-baroque/concerts/cantates-bwv-4-106-et-131-de-bach-par-raphael-pichon-57-268689',
'info_dict': {
'id': 'EV_134885',
'ext': 'mp4',
'title': 'Cantates BWV 4, 106 et 131 de Bach par Raphaël Pichon 5/7',
'description': 'md5:19c44af004b88219f4daa50fa9a351d4',
'upload_date': '20180206',
'timestamp': 1517945220,
'duration': 5981,
},
'params': {
'skip_download': True,
},
'add_ie': [FranceTVIE.ie_key()],
}]
def _real_extract(self, url):
display_id = self._match_id(url)
webpage = self._download_webpage(url, display_id)
if ">Ce live n'est plus disponible en replay<" in webpage:
raise ExtractorError(
'Video %s is not available' % display_id, expected=True)
video_id, catalogue = self._search_regex(
r'["\'>]https?://videos\.francetv\.fr/video/([^@]+@.+?)["\'<]',
webpage, 'video id').split('@')
return self._make_url_result(video_id, catalogue)
class FranceTVJeunesseIE(FranceTVBaseInfoExtractor):
_VALID_URL = r'(?P<url>https?://(?:www\.)?(?:zouzous|ludo)\.fr/heros/(?P<id>[^/?#&]+))'
_TESTS = [{
'url': 'https://www.zouzous.fr/heros/simon',
'info_dict': {
'id': 'simon',
},
'playlist_count': 9,
}, {
'url': 'https://www.ludo.fr/heros/ninjago',
'info_dict': {
'id': 'ninjago',
},
'playlist_count': 10,
}, {
'url': 'https://www.zouzous.fr/heros/simon?abc',
'only_matching': True,
}]
def _real_extract(self, url):
mobj = self._match_valid_url(url)
playlist_id = mobj.group('id')
playlist = self._download_json(
'%s/%s' % (mobj.group('url'), 'playlist'), playlist_id)
if not playlist.get('count'):
raise ExtractorError(
'%s is not available' % playlist_id, expected=True)
entries = []
for item in playlist['items']:
identity = item.get('identity')
if identity and isinstance(identity, compat_str):
entries.append(self._make_url_result(identity))
return self.playlist_result(entries, playlist_id)

View File

@@ -2,25 +2,61 @@
from __future__ import unicode_literals from __future__ import unicode_literals
import random import random
import re
import string import string
from .common import InfoExtractor from .common import InfoExtractor
from ..compat import compat_HTTPError from ..compat import compat_HTTPError
from ..utils import ( from ..utils import (
determine_ext, determine_ext,
dict_get,
int_or_none, int_or_none,
js_to_json, js_to_json,
orderedSet,
qualities,
str_or_none, str_or_none,
traverse_obj,
try_get, try_get,
urlencode_postdata, urlencode_postdata,
ExtractorError, ExtractorError,
) )
class FunimationPageIE(InfoExtractor): class FunimationBaseIE(InfoExtractor):
_NETRC_MACHINE = 'funimation'
_REGION = None
_TOKEN = None
def _get_region(self):
region_cookie = self._get_cookies('https://www.funimation.com').get('region')
region = region_cookie.value if region_cookie else self.get_param('geo_bypass_country')
return region or traverse_obj(
self._download_json(
'https://geo-service.prd.funimationsvc.com/geo/v1/region/check', None, fatal=False,
note='Checking geo-location', errnote='Unable to fetch geo-location information'),
'region') or 'US'
def _login(self):
username, password = self._get_login_info()
if username is None:
return
try:
data = self._download_json(
'https://prod-api-funimationnow.dadcdigital.com/api/auth/login/',
None, 'Logging in', data=urlencode_postdata({
'username': username,
'password': password,
}))
return data['token']
except ExtractorError as e:
if isinstance(e.cause, compat_HTTPError) and e.cause.code == 401:
error = self._parse_json(e.cause.read().decode(), None)['error']
raise ExtractorError(error, expected=True)
raise
class FunimationPageIE(FunimationBaseIE):
IE_NAME = 'funimation:page' IE_NAME = 'funimation:page'
_VALID_URL = r'(?P<origin>https?://(?:www\.)?funimation(?:\.com|now\.uk))/(?P<lang>[^/]+/)?(?P<path>shows/(?P<id>[^/]+/[^/?#&]+).*$)' _VALID_URL = r'https?://(?:www\.)?funimation(?:\.com|now\.uk)/(?:(?P<lang>[^/]+)/)?(?:shows|v)/(?P<show>[^/]+)/(?P<episode>[^/?#&]+)'
_TESTS = [{ _TESTS = [{
'url': 'https://www.funimation.com/shows/attack-on-titan-junior-high/broadcast-dub-preview/', 'url': 'https://www.funimation.com/shows/attack-on-titan-junior-high/broadcast-dub-preview/',
@@ -45,38 +81,34 @@ class FunimationPageIE(InfoExtractor):
}, { }, {
'url': 'https://www.funimationnow.uk/shows/puzzle-dragons-x/drop-impact/simulcast/', 'url': 'https://www.funimationnow.uk/shows/puzzle-dragons-x/drop-impact/simulcast/',
'only_matching': True, 'only_matching': True,
}, {
'url': 'https://www.funimation.com/v/a-certain-scientific-railgun/super-powered-level-5',
'only_matching': True,
}] }]
def _real_initialize(self):
if not self._REGION:
FunimationBaseIE._REGION = self._get_region()
if not self._TOKEN:
FunimationBaseIE._TOKEN = self._login()
def _real_extract(self, url): def _real_extract(self, url):
mobj = self._match_valid_url(url) locale, show, episode = self._match_valid_url(url).group('lang', 'show', 'episode')
display_id = mobj.group('id').replace('/', '_')
if not mobj.group('lang'):
url = '%s/en/%s' % (mobj.group('origin'), mobj.group('path'))
webpage = self._download_webpage(url, display_id) video_id = traverse_obj(self._download_json(
title_data = self._parse_json(self._search_regex( f'https://title-api.prd.funimationsvc.com/v1/shows/{show}/episodes/{episode}',
r'TITLE_DATA\s*=\s*({[^}]+})', f'{show}_{episode}', query={
webpage, 'title data', default=''), 'deviceType': 'web',
display_id, js_to_json, fatal=False) or {} 'region': self._REGION,
'locale': locale or 'en'
}), ('videoList', ..., 'id'), get_all=False)
video_id = (
title_data.get('id')
or self._search_regex(
(r"KANE_customdimensions.videoID\s*=\s*'(\d+)';", r'<iframe[^>]+src="/player/(\d+)'),
webpage, 'video_id', default=None)
or self._search_regex(
r'/player/(\d+)',
self._html_search_meta(['al:web:url', 'og:video:url', 'og:video:secure_url'], webpage, fatal=True),
'video id'))
return self.url_result(f'https://www.funimation.com/player/{video_id}', FunimationIE.ie_key(), video_id) return self.url_result(f'https://www.funimation.com/player/{video_id}', FunimationIE.ie_key(), video_id)
class FunimationIE(InfoExtractor): class FunimationIE(FunimationBaseIE):
_VALID_URL = r'https?://(?:www\.)?funimation\.com/player/(?P<id>\d+)' _VALID_URL = r'https?://(?:www\.)?funimation\.com/player/(?P<id>\d+)'
_NETRC_MACHINE = 'funimation'
_TOKEN = None
_TESTS = [{ _TESTS = [{
'url': 'https://www.funimation.com/player/210051', 'url': 'https://www.funimation.com/player/210051',
'info_dict': { 'info_dict': {
@@ -92,7 +124,7 @@ class FunimationIE(InfoExtractor):
'season_number': 99, 'season_number': 99,
'series': 'Attack on Titan: Junior High', 'series': 'Attack on Titan: Junior High',
'description': '', 'description': '',
'duration': 154, 'duration': 155,
}, },
'params': { 'params': {
'skip_download': 'm3u8', 'skip_download': 'm3u8',
@@ -113,7 +145,7 @@ class FunimationIE(InfoExtractor):
'season_number': 99, 'season_number': 99,
'series': 'Attack on Titan: Junior High', 'series': 'Attack on Titan: Junior High',
'description': '', 'description': '',
'duration': 154, 'duration': 155,
}, },
'params': { 'params': {
'skip_download': 'm3u8', 'skip_download': 'm3u8',
@@ -121,26 +153,9 @@ class FunimationIE(InfoExtractor):
}, },
}] }]
def _login(self):
username, password = self._get_login_info()
if username is None:
return
try:
data = self._download_json(
'https://prod-api-funimationnow.dadcdigital.com/api/auth/login/',
None, 'Logging in', data=urlencode_postdata({
'username': username,
'password': password,
}))
self._TOKEN = data['token']
except ExtractorError as e:
if isinstance(e.cause, compat_HTTPError) and e.cause.code == 401:
error = self._parse_json(e.cause.read().decode(), None)['error']
raise ExtractorError(error, expected=True)
raise
def _real_initialize(self): def _real_initialize(self):
self._login() if not self._TOKEN:
FunimationBaseIE._TOKEN = self._login()
@staticmethod @staticmethod
def _get_experiences(episode): def _get_experiences(episode):
@@ -180,6 +195,8 @@ class FunimationIE(InfoExtractor):
formats, subtitles, thumbnails, duration = [], {}, [], 0 formats, subtitles, thumbnails, duration = [], {}, [], 0
requested_languages, requested_versions = self._configuration_arg('language'), self._configuration_arg('version') requested_languages, requested_versions = self._configuration_arg('language'), self._configuration_arg('version')
language_preference = qualities((requested_languages or [''])[::-1])
source_preference = qualities((requested_versions or ['uncut', 'simulcast'])[::-1])
only_initial_experience = 'seperate-video-versions' in self.get_param('compat_opts', []) only_initial_experience = 'seperate-video-versions' in self.get_param('compat_opts', [])
for lang, version, fmt in self._get_experiences(episode): for lang, version, fmt in self._get_experiences(episode):
@@ -227,10 +244,15 @@ class FunimationIE(InfoExtractor):
}) })
for f in current_formats: for f in current_formats:
# TODO: Convert language to code # TODO: Convert language to code
f.update({'language': lang, 'format_note': version}) f.update({
'language': lang,
'format_note': version,
'source_preference': source_preference(version.lower()),
'language_preference': language_preference(lang.lower()),
})
formats.extend(current_formats) formats.extend(current_formats)
self._remove_duplicate_formats(formats) self._remove_duplicate_formats(formats)
self._sort_formats(formats) self._sort_formats(formats, ('lang', 'source'))
return { return {
'id': initial_experience_id if only_initial_experience else episode_id, 'id': initial_experience_id if only_initial_experience else episode_id,
@@ -275,7 +297,7 @@ class FunimationIE(InfoExtractor):
return subtitles return subtitles
class FunimationShowIE(FunimationIE): class FunimationShowIE(FunimationBaseIE):
IE_NAME = 'funimation:show' IE_NAME = 'funimation:show'
_VALID_URL = r'(?P<url>https?://(?:www\.)?funimation(?:\.com|now\.uk)/(?P<locale>[^/]+)?/?shows/(?P<id>[^/?#&]+))/?(?:[?#]|$)' _VALID_URL = r'(?P<url>https?://(?:www\.)?funimation(?:\.com|now\.uk)/(?P<locale>[^/]+)?/?shows/(?P<id>[^/?#&]+))/?(?:[?#]|$)'
@@ -302,24 +324,29 @@ class FunimationShowIE(FunimationIE):
}, },
}] }]
def _real_initialize(self):
if not self._REGION:
FunimationBaseIE._REGION = self._get_region()
def _real_extract(self, url): def _real_extract(self, url):
base_url, locale, display_id = self._match_valid_url(url).groups() base_url, locale, display_id = self._match_valid_url(url).groups()
show_info = self._download_json( show_info = self._download_json(
'https://title-api.prd.funimationsvc.com/v2/shows/%s?region=US&deviceType=web&locale=%s' 'https://title-api.prd.funimationsvc.com/v2/shows/%s?region=%s&deviceType=web&locale=%s'
% (display_id, locale or 'en'), display_id) % (display_id, self._REGION, locale or 'en'), display_id)
items = self._download_json( items_info = self._download_json(
'https://prod-api-funimationnow.dadcdigital.com/api/funimation/episodes/?limit=99999&title_id=%s' 'https://prod-api-funimationnow.dadcdigital.com/api/funimation/episodes/?limit=99999&title_id=%s'
% show_info.get('id'), display_id).get('items') % show_info.get('id'), display_id)
vod_items = map(lambda k: dict_get(k, ('mostRecentSvod', 'mostRecentAvod')).get('item'), items)
vod_items = traverse_obj(items_info, ('items', ..., re.compile('(?i)mostRecent[AS]vod').match, 'item'))
return { return {
'_type': 'playlist', '_type': 'playlist',
'id': show_info['id'], 'id': show_info['id'],
'title': show_info['name'], 'title': show_info['name'],
'entries': [ 'entries': orderedSet(
self.url_result( self.url_result(
'%s/%s' % (base_url, vod_item.get('episodeSlug')), FunimationPageIE.ie_key(), '%s/%s' % (base_url, vod_item.get('episodeSlug')), FunimationPageIE.ie_key(),
vod_item.get('episodeId'), vod_item.get('episodeName')) vod_item.get('episodeId'), vod_item.get('episodeName'))
for vod_item in sorted(vod_items, key=lambda x: x.get('episodeOrder'))], for vod_item in sorted(vod_items, key=lambda x: x.get('episodeOrder', -1))),
} }

View File

@@ -1215,14 +1215,13 @@ class GenericIE(InfoExtractor):
}, },
{ {
# JWPlatform iframe # JWPlatform iframe
'url': 'https://www.mediaite.com/tv/dem-senator-claims-gary-cohn-faked-a-bad-connection-during-trump-call-to-get-him-off-the-phone/', 'url': 'https://www.covermagazine.co.uk/feature/2465255/business-protection-involved',
'md5': 'ca00a040364b5b439230e7ebfd02c4e9',
'info_dict': { 'info_dict': {
'id': 'O0c5JcKT', 'id': 'AG26UQXM',
'ext': 'mp4', 'ext': 'mp4',
'upload_date': '20171122', 'upload_date': '20160719',
'timestamp': 1511366290, 'timestamp': 468923808,
'title': 'Dem Senator Claims Gary Cohn Faked a Bad Connection During Trump Call to Get Him Off the Phone', 'title': '2016_05_18 Cover L&G Business Protection V1 FINAL.mp4',
}, },
'add_ie': [JWPlatformIE.ie_key()], 'add_ie': [JWPlatformIE.ie_key()],
}, },
@@ -2756,12 +2755,6 @@ class GenericIE(InfoExtractor):
if vhx_url: if vhx_url:
return self.url_result(vhx_url, VHXEmbedIE.ie_key()) return self.url_result(vhx_url, VHXEmbedIE.ie_key())
vid_me_embed_url = self._search_regex(
r'src=[\'"](https?://vid\.me/[^\'"]+)[\'"]',
webpage, 'vid.me embed', default=None)
if vid_me_embed_url is not None:
return self.url_result(vid_me_embed_url, 'Vidme')
# Invidious Instances # Invidious Instances
# https://github.com/yt-dlp/yt-dlp/issues/195 # https://github.com/yt-dlp/yt-dlp/issues/195
# https://github.com/iv-org/invidious/pull/1730 # https://github.com/iv-org/invidious/pull/1730

110
yt_dlp/extractor/gettr.py Normal file
View File

@@ -0,0 +1,110 @@
# coding: utf-8
from __future__ import unicode_literals
from .common import InfoExtractor
from ..utils import (
ExtractorError,
dict_get,
float_or_none,
int_or_none,
remove_end,
str_or_none,
try_get,
url_or_none,
urljoin,
)
class GettrIE(InfoExtractor):
_VALID_URL = r'https?://(www\.)?gettr\.com/post/(?P<id>[a-z0-9]+)'
_MEDIA_BASE_URL = 'https://media.gettr.com/'
_TESTS = [{
'url': 'https://www.gettr.com/post/pcf6uv838f',
'info_dict': {
'id': 'pcf6uv838f',
'title': 'md5:9086a646bbd06c41c4fe8e52b3c93454',
'description': 'md5:be0577f1e4caadc06de4a002da2bf287',
'ext': 'mp4',
'uploader': 'EpochTV',
'uploader_id': 'epochtv',
'thumbnail': r're:^https?://.+/out\.jpg',
'timestamp': 1632782451058,
'duration': 58.5585,
}
}, {
'url': 'https://gettr.com/post/p4iahp',
'info_dict': {
'id': 'p4iahp',
'title': 'md5:b03c07883db6fbc1aab88877a6c3b149',
'description': 'md5:741b7419d991c403196ed2ea7749a39d',
'ext': 'mp4',
'uploader': 'Neues Forum Freiheit',
'uploader_id': 'nf_freiheit',
'thumbnail': r're:^https?://.+/out\.jpg',
'timestamp': 1626594455017,
'duration': 23,
}
}]
def _real_extract(self, url):
post_id = self._match_id(url)
webpage = self._download_webpage(url, post_id)
api_data = self._download_json(
'https://api.gettr.com/u/post/%s?incl="poststats|userinfo"' % post_id, post_id)
post_data = try_get(api_data, lambda x: x['result']['data'])
user_data = try_get(api_data, lambda x: x['result']['aux']['uinf'][post_data['uid']]) or {}
if post_data.get('nfound'):
raise ExtractorError(post_data.get('txt'), expected=True)
title = description = str_or_none(
post_data.get('txt') or self._og_search_description(webpage))
uploader = str_or_none(
user_data.get('nickname')
or remove_end(self._og_search_title(webpage), ' on GETTR'))
if uploader:
title = '%s - %s' % (uploader, title)
if not dict_get(post_data, ['vid', 'ovid']):
raise ExtractorError('There\'s no video in this post.')
vid = post_data.get('vid')
ovid = post_data.get('ovid')
formats = self._extract_m3u8_formats(
urljoin(self._MEDIA_BASE_URL, vid), post_id, 'mp4',
entry_protocol='m3u8_native', m3u8_id='hls') if vid else []
if ovid:
formats.append({
'url': urljoin(self._MEDIA_BASE_URL, ovid),
'format_id': 'ovid',
'ext': 'mp4',
'width': int_or_none(post_data.get('vid_wid')),
'height': int_or_none(post_data.get('vid_hgt')),
'source_preference': 1,
'quality': 1,
})
self._sort_formats(formats)
return {
'id': post_id,
'title': title,
'description': description,
'thumbnail': url_or_none(
urljoin(self._MEDIA_BASE_URL, post_data.get('main'))
or self._og_search_thumbnail(webpage)),
'timestamp': int_or_none(post_data.get('cdate')),
'uploader_id': str_or_none(
dict_get(user_data, ['_id', 'username'])
or post_data.get('uid')),
'uploader': uploader,
'formats': formats,
'duration': float_or_none(post_data.get('vid_dur')),
'tags': post_data.get('htgs'),
}

View File

@@ -9,15 +9,14 @@ import re
from .common import InfoExtractor from .common import InfoExtractor
from ..compat import ( from ..compat import (
compat_HTTPError,
compat_str, compat_str,
) )
from ..utils import ( from ..utils import (
ExtractorError, ExtractorError,
float_or_none, float_or_none,
int_or_none,
orderedSet, orderedSet,
str_or_none, str_or_none,
try_get,
) )
@@ -26,18 +25,19 @@ class GloboIE(InfoExtractor):
_NETRC_MACHINE = 'globo' _NETRC_MACHINE = 'globo'
_TESTS = [{ _TESTS = [{
'url': 'http://g1.globo.com/carros/autoesporte/videos/t/exclusivos-do-g1/v/mercedes-benz-gla-passa-por-teste-de-colisao-na-europa/3607726/', 'url': 'http://g1.globo.com/carros/autoesporte/videos/t/exclusivos-do-g1/v/mercedes-benz-gla-passa-por-teste-de-colisao-na-europa/3607726/',
'md5': 'b3ccc801f75cd04a914d51dadb83a78d',
'info_dict': { 'info_dict': {
'id': '3607726', 'id': '3607726',
'ext': 'mp4', 'ext': 'mp4',
'title': 'Mercedes-Benz GLA passa por teste de colisão na Europa', 'title': 'Mercedes-Benz GLA passa por teste de colisão na Europa',
'duration': 103.204, 'duration': 103.204,
'uploader': 'Globo.com', 'uploader': 'G1',
'uploader_id': '265', 'uploader_id': '2015',
},
'params': {
'skip_download': True,
}, },
}, { }, {
'url': 'http://globoplay.globo.com/v/4581987/', 'url': 'http://globoplay.globo.com/v/4581987/',
'md5': 'f36a1ecd6a50da1577eee6dd17f67eff',
'info_dict': { 'info_dict': {
'id': '4581987', 'id': '4581987',
'ext': 'mp4', 'ext': 'mp4',
@@ -46,6 +46,9 @@ class GloboIE(InfoExtractor):
'uploader': 'Rede Globo', 'uploader': 'Rede Globo',
'uploader_id': '196', 'uploader_id': '196',
}, },
'params': {
'skip_download': True,
},
}, { }, {
'url': 'http://canalbrasil.globo.com/programas/sangue-latino/videos/3928201.html', 'url': 'http://canalbrasil.globo.com/programas/sangue-latino/videos/3928201.html',
'only_matching': True, 'only_matching': True,
@@ -66,30 +69,6 @@ class GloboIE(InfoExtractor):
'only_matching': True, 'only_matching': True,
}] }]
def _real_initialize(self):
email, password = self._get_login_info()
if email is None:
return
try:
glb_id = (self._download_json(
'https://login.globo.com/api/authentication', None, data=json.dumps({
'payload': {
'email': email,
'password': password,
'serviceId': 4654,
},
}).encode(), headers={
'Content-Type': 'application/json; charset=utf-8',
}) or {}).get('glbId')
if glb_id:
self._set_cookie('.globo.com', 'GLBID', glb_id)
except ExtractorError as e:
if isinstance(e.cause, compat_HTTPError) and e.cause.code == 401:
resp = self._parse_json(e.cause.read(), None)
raise ExtractorError(resp.get('userMessage') or resp['id'], expected=True)
raise
def _real_extract(self, url): def _real_extract(self, url):
video_id = self._match_id(url) video_id = self._match_id(url)
@@ -102,35 +81,23 @@ class GloboIE(InfoExtractor):
title = video['title'] title = video['title']
formats = [] formats = []
subtitles = {}
for resource in video['resources']:
resource_id = resource.get('_id')
resource_url = resource.get('url')
resource_type = resource.get('type')
if not resource_url or (resource_type == 'media' and not resource_id) or resource_type not in ('subtitle', 'media'):
continue
if resource_type == 'subtitle':
subtitles.setdefault(resource.get('language') or 'por', []).append({
'url': resource_url,
})
continue
security = self._download_json( security = self._download_json(
'http://security.video.globo.com/videos/%s/hash' % video_id, 'https://playback.video.globo.com/v1/video-session', video_id, 'Downloading security hash for %s' % video_id,
video_id, 'Downloading security hash for %s' % resource_id, query={ headers={'content-type': 'application/json'}, data=json.dumps({
'player': 'desktop', "player_type": "desktop",
'version': '5.19.1', "video_id": video_id,
'resource_id': resource_id, "quality": "max",
}) "content_protection": "widevine",
"vsid": "581b986b-4c40-71f0-5a58-803e579d5fa2",
"tz": "-3.0:00"
}).encode())
security_hash = security.get('hash') security_hash = security['source']['token']
if not security_hash: if not security_hash:
message = security.get('message') message = security.get('message')
if message: if message:
raise ExtractorError( raise ExtractorError(
'%s returned error: %s' % (self.IE_NAME, message), expected=True) '%s returned error: %s' % (self.IE_NAME, message), expected=True)
continue
hash_code = security_hash[:2] hash_code = security_hash[:2]
padding = '%010d' % random.randint(1, 10000000000) padding = '%010d' % random.randint(1, 10000000000)
@@ -148,27 +115,33 @@ class GloboIE(InfoExtractor):
md5_data = (received_md5 + padded_sign_time + '0xAC10FD').encode() md5_data = (received_md5 + padded_sign_time + '0xAC10FD').encode()
signed_md5 = base64.urlsafe_b64encode(hashlib.md5(md5_data).digest()).decode().strip('=') signed_md5 = base64.urlsafe_b64encode(hashlib.md5(md5_data).digest()).decode().strip('=')
signed_hash = hash_prefix + padded_sign_time + signed_md5 signed_hash = hash_prefix + padded_sign_time + signed_md5
signed_url = '%s?h=%s&k=html5&a=%s&u=%s' % (resource_url, signed_hash, 'F' if video.get('subscriber_only') else 'A', security.get('user') or '') source = security['source']['url_parts']
resource_url = source['scheme'] + '://' + source['domain'] + source['path']
signed_url = '%s?h=%s&k=html5&a=%s' % (resource_url, signed_hash, 'F' if video.get('subscriber_only') else 'A')
if resource_id.endswith('m3u8') or resource_url.endswith('.m3u8'):
formats.extend(self._extract_m3u8_formats( formats.extend(self._extract_m3u8_formats(
signed_url, resource_id, 'mp4', entry_protocol='m3u8_native', signed_url, video_id, 'mp4', entry_protocol='m3u8_native', m3u8_id='hls', fatal=False))
m3u8_id='hls', fatal=False))
elif resource_id.endswith('mpd') or resource_url.endswith('.mpd'):
formats.extend(self._extract_mpd_formats(
signed_url, resource_id, mpd_id='dash', fatal=False))
elif resource_id.endswith('manifest') or resource_url.endswith('/manifest'):
formats.extend(self._extract_ism_formats(
signed_url, resource_id, ism_id='mss', fatal=False))
else:
formats.append({
'url': signed_url,
'format_id': 'http-%s' % resource_id,
'height': int_or_none(resource.get('height')),
})
self._sort_formats(formats) self._sort_formats(formats)
subtitles = {}
for resource in video['resources']:
if resource.get('type') == 'subtitle':
subtitles.setdefault(resource.get('language') or 'por', []).append({
'url': resource.get('url'),
})
subs = try_get(security, lambda x: x['source']['subtitles'], expected_type=dict) or {}
for sub_lang, sub_url in subs.items():
if sub_url:
subtitles.setdefault(sub_lang or 'por', []).append({
'url': sub_url,
})
subs = try_get(security, lambda x: x['source']['subtitles_webvtt'], expected_type=dict) or {}
for sub_lang, sub_url in subs.items():
if sub_url:
subtitles.setdefault(sub_lang or 'por', []).append({
'url': sub_url,
})
duration = float_or_none(video.get('duration'), 1000) duration = float_or_none(video.get('duration'), 1000)
uploader = video.get('channel') uploader = video.get('channel')
uploader_id = str_or_none(video.get('channel_id')) uploader_id = str_or_none(video.get('channel_id'))

View File

@@ -11,6 +11,7 @@ class GoogleSearchIE(SearchInfoExtractor):
_MAX_RESULTS = 1000 _MAX_RESULTS = 1000
IE_NAME = 'video.google:search' IE_NAME = 'video.google:search'
_SEARCH_KEY = 'gvsearch' _SEARCH_KEY = 'gvsearch'
_WORKING = False
_TEST = { _TEST = {
'url': 'gvsearch15:python language', 'url': 'gvsearch15:python language',
'info_dict': { 'info_dict': {
@@ -20,16 +21,7 @@ class GoogleSearchIE(SearchInfoExtractor):
'playlist_count': 15, 'playlist_count': 15,
} }
def _get_n_results(self, query, n): def _search_results(self, query):
"""Get a specified number of results for a query"""
entries = []
res = {
'_type': 'playlist',
'id': query,
'title': query,
}
for pagenum in itertools.count(): for pagenum in itertools.count():
webpage = self._download_webpage( webpage = self._download_webpage(
'http://www.google.com/search', 'http://www.google.com/search',
@@ -44,16 +36,8 @@ class GoogleSearchIE(SearchInfoExtractor):
for hit_idx, mobj in enumerate(re.finditer( for hit_idx, mobj in enumerate(re.finditer(
r'<h3 class="r"><a href="([^"]+)"', webpage)): r'<h3 class="r"><a href="([^"]+)"', webpage)):
if re.search(f'id="vidthumb{hit_idx + 1}"', webpage):
yield self.url_result(mobj.group(1))
# Skip playlists if not re.search(r'id="pnnext"', webpage):
if not re.search(r'id="vidthumb%d"' % (hit_idx + 1), webpage): return
continue
entries.append({
'_type': 'url',
'url': mobj.group(1)
})
if (len(entries) >= n) or not re.search(r'id="pnnext"', webpage):
res['entries'] = entries[:n]
return res

110
yt_dlp/extractor/gopro.py Normal file
View File

@@ -0,0 +1,110 @@
# coding: utf-8
from __future__ import unicode_literals
from .common import InfoExtractor
from ..utils import (
int_or_none,
remove_end,
str_or_none,
try_get,
unified_timestamp,
url_or_none,
)
class GoProIE(InfoExtractor):
_VALID_URL = r'https?://(www\.)?gopro\.com/v/(?P<id>[A-Za-z0-9]+)'
_TESTS = [{
'url': 'https://gopro.com/v/ZNVvED8QDzR5V',
'info_dict': {
'id': 'ZNVvED8QDzR5V',
'title': 'My GoPro Adventure - 9/19/21',
'thumbnail': r're:https?://.+',
'ext': 'mp4',
'timestamp': 1632072947,
'upload_date': '20210919',
'uploader_id': 'fireydive30018',
'duration': 396062,
}
}, {
'url': 'https://gopro.com/v/KRm6Vgp2peg4e',
'info_dict': {
'id': 'KRm6Vgp2peg4e',
'title': 'じゃがいも カリカリ オーブン焼き',
'thumbnail': r're:https?://.+',
'ext': 'mp4',
'timestamp': 1607231125,
'upload_date': '20201206',
'uploader_id': 'dc9bcb8b-47d2-47c6-afbc-4c48f9a3769e',
'duration': 45187,
'track': 'The Sky Machine',
}
}, {
'url': 'https://gopro.com/v/kVrK9wlJvBMwn',
'info_dict': {
'id': 'kVrK9wlJvBMwn',
'title': 'DARKNESS',
'thumbnail': r're:https?://.+',
'ext': 'mp4',
'timestamp': 1594183735,
'upload_date': '20200708',
'uploader_id': '闇夜乃皇帝',
'duration': 313075,
'track': 'Battery (Live)',
'artist': 'Metallica',
}
}]
def _real_extract(self, url):
video_id = self._match_id(url)
webpage = self._download_webpage(url, video_id)
metadata = self._parse_json(
self._html_search_regex(r'window\.__reflectData\s*=\s*([^;]+)', webpage, 'metadata'), video_id)
video_info = metadata['collectionMedia'][0]
media_data = self._download_json(
'https://api.gopro.com/media/%s/download' % video_info['id'], video_id)
formats = []
for fmt in try_get(media_data, lambda x: x['_embedded']['variations']) or []:
format_url = url_or_none(fmt.get('url'))
if not format_url:
continue
formats.append({
'url': format_url,
'format_id': str_or_none(fmt.get('quality')),
'format_note': str_or_none(fmt.get('label')),
'ext': str_or_none(fmt.get('type')),
'width': int_or_none(fmt.get('width')),
'height': int_or_none(fmt.get('height')),
})
self._sort_formats(formats)
title = str_or_none(
try_get(metadata, lambda x: x['collection']['title'])
or self._html_search_meta(['og:title', 'twitter:title'], webpage)
or remove_end(self._html_search_regex(
r'<title[^>]*>([^<]+)</title>', webpage, 'title', fatal=False), ' | GoPro'))
if title:
title = title.replace('\n', ' ')
return {
'id': video_id,
'title': title,
'formats': formats,
'thumbnail': url_or_none(
self._html_search_meta(['og:image', 'twitter:image'], webpage)),
'timestamp': unified_timestamp(
try_get(metadata, lambda x: x['collection']['created_at'])),
'uploader_id': str_or_none(
try_get(metadata, lambda x: x['account']['nickname'])),
'duration': int_or_none(
video_info.get('source_duration')),
'artist': str_or_none(
video_info.get('music_track_artist')),
'track': str_or_none(
video_info.get('music_track_name')),
}

View File

@@ -0,0 +1,73 @@
# coding: utf-8
from __future__ import unicode_literals
from .common import InfoExtractor
from ..compat import compat_str
from ..utils import (
try_get,
url_or_none
)
import json
class GoToStageIE(InfoExtractor):
_VALID_URL = r'https?://(?:www\.)?gotostage\.com/channel/[a-z0-9]+/recording/(?P<id>[a-z0-9]+)/watch'
_TESTS = [{
'url': 'https://www.gotostage.com/channel/8901680603948959494/recording/60bb55548d434f21b9ce4f0e225c4895/watch',
'md5': 'ca72ce990cdcd7a2bd152f7217e319a2',
'info_dict': {
'id': '60bb55548d434f21b9ce4f0e225c4895',
'ext': 'mp4',
'title': 'What is GoToStage?',
'thumbnail': r're:^https?://.*\.jpg$',
'duration': 93.924711
}
}, {
'url': 'https://www.gotostage.com/channel/bacc3d3535b34bafacc3f4ef8d4df78a/recording/831e74cd3e0042be96defba627b6f676/watch?source=HOMEPAGE',
'only_matching': True,
}]
def _real_extract(self, url):
video_id = self._match_id(url)
metadata = self._download_json(
'https://api.gotostage.com/contents?ids=%s' % video_id,
video_id,
note='Downloading video metadata',
errnote='Unable to download video metadata')[0]
registration_data = {
'product': metadata['product'],
'resourceType': metadata['contentType'],
'productReferenceKey': metadata['productRefKey'],
'firstName': 'foo',
'lastName': 'bar',
'email': 'foobar@example.com'
}
registration_response = self._download_json(
'https://api-registrations.logmeininc.com/registrations',
video_id,
data=json.dumps(registration_data).encode(),
expected_status=409,
headers={'Content-Type': 'application/json'},
note='Register user',
errnote='Unable to register user')
content_response = self._download_json(
'https://api.gotostage.com/contents/%s/asset' % video_id,
video_id,
headers={'x-registrantkey': registration_response['registrationKey']},
note='Get download url',
errnote='Unable to get download url')
return {
'id': video_id,
'title': try_get(metadata, lambda x: x['title'], compat_str),
'url': try_get(content_response, lambda x: x['cdnLocation'], compat_str),
'ext': 'mp4',
'thumbnail': url_or_none(try_get(metadata, lambda x: x['thumbnail']['location'])),
'duration': try_get(metadata, lambda x: x['duration'], float),
'categories': [try_get(metadata, lambda x: x['category'], compat_str)],
'is_live': False
}

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