Compare commits

..
66 Commits
Author SHA1 Message Date
github-actions[bot] 264044286d Release 2025.10.14
Created by: bashonly

:ci skip all
2025-10-14 23:29:27 +00:00
RobinandGitHub a98e7f9f58 [ie/idagio] Add extractors (#14586)
Closes #2624
Authored by: robin-mu
2025-10-15 01:23:13 +02:00
uoagandGitHub 0ea5d5882d [ie/abc.net.au] Support listen URLs (#14389)
Authored by: uoag
2025-10-14 22:02:21 +02:00
CasualYouTuber31andGitHub cdc533b114 [ie/tiktok:user] Fix private account extraction (#14585)
Closes #14565
Authored by: CasualYT31
2025-10-14 19:42:36 +00:00
bashonlyandGitHub c2e124881f [ie/slideslive] Fix extractor (#14619)
Closes #14518
Authored by: bashonly
2025-10-14 19:38:15 +00:00
bashonlyandGitHub ad55bfcfb7 [ie/10play] Handle geo-restriction errors (#14618)
Authored by: bashonly
2025-10-14 19:36:17 +00:00
739125d40f [ie/xhamster] Fix extractor (#14446)
Closes #14395
Authored by: shssoichiro, dhwz, dirkf

Co-authored-by: dhwz <3697946+dhwz@users.noreply.github.com>
Co-authored-by: dirkf <1222880+dirkf@users.noreply.github.com>
2025-10-14 19:31:07 +00:00
Sean EllinghamandGitHub 5f94f05490 [ie/vidyard] Extract chapters (#14478)
Closes #14477
Authored by: exterrestris
2025-10-14 13:53:54 +02:00
columndeeplyandGitHub 5d7678195a [ie/PrankCastPost] Rework extractor (#14445)
Authored by: columndeeply
2025-10-14 13:25:07 +02:00
eafedc2181 [ie/10play] Rework extractor (#14417)
Closes #14276
Authored by: seproDev, Sipherdrakon

Co-authored-by: Sipherdrakon <64430430+Sipherdrakon@users.noreply.github.com>
2025-10-13 00:54:26 +02:00
CeciandGitHub 8eb8695139 [ie/dropout] Update extractor for new domain (#14531)
Closes #14521
Authored by: cecilia-sanare
2025-10-12 23:53:53 +02:00
uoagandGitHub df160ab18d [ie/cbc.ca:listen] Add extractor (#14391)
Authored by: uoag
2025-10-12 23:42:39 +02:00
seproandGitHub 6d41aaf21c [ie/soundcloud] Support new API URLs (#14449)
Closes #14443
Authored by: seproDev
2025-10-12 22:21:34 +02:00
seproandGitHub a6673a8e82 Fix prefer-vp9-sort compat option (#14603)
Closes #14602
Authored by: seproDev
2025-10-12 20:30:17 +02:00
seproandGitHub 87be1bb96a [ie/musescore] Fix extractor (#14598)
Closes #14485
Authored by: seproDev
2025-10-12 08:49:15 +02:00
coletdjnzandGitHub ccc25d6710 [ie/youtube:tab] Fix approximate timestamp extraction for feeds (#14539)
Authored by: coletdjnz
2025-10-12 08:29:06 +13:00
5513036104 [ie/tiktok] Support browser impersonation (#14473)
Closes #10919, Closes #12574
Authored by: thanhtaivtt, bashonly

Co-authored-by: bashonly <88596187+bashonly@users.noreply.github.com>
2025-10-01 06:53:19 +00:00
coletdjnzandGitHub bd5ed90419 [ie/youtube] Detect experiment binding GVS PO Token to video id (#14471)
Fixes https://github.com/yt-dlp/yt-dlp/issues/14421

Authored by: coletdjnz
2025-09-29 16:25:09 +13:00
github-actions[bot] 88e2a2de8e Release 2025.09.26
Created by: bashonly

:ci skip all
2025-09-26 22:13:00 +00:00
bashonlyandGitHub 12b57d2858 [ie/youtube] Replace tv_simply with web_safari in default clients (#14465)
Closes #14456
Authored by: bashonly
2025-09-26 21:59:13 +00:00
b7b7910d96 [ie/youtube] Fix player JS overrides (#14430)
Authored by: seproDev, bashonly

Co-authored-by: bashonly <88596187+bashonly@users.noreply.github.com>
2025-09-26 21:19:57 +00:00
bashonlyandGitHub 50e452fd7d [ie/twitch:vod] Fix live_status detection (#14457)
Closes #14455
Authored by: bashonly
2025-09-26 18:27:17 +00:00
seproandGitHub 94c5622be9 [ie/youtube] Player client maintenance (#14448)
Authored by: seproDev
2025-09-26 18:13:20 +00:00
seproandGitHub 7df5acc546 [ie/youtube] Improve PO token logging (#14447)
Authored by: seproDev
2025-09-26 18:12:16 +00:00
github-actions[bot] 4429fd0450 Release 2025.09.23
Created by: bashonly

:ci skip all
2025-09-23 06:45:05 +00:00
2e81e298cd [cleanup] Misc (#14268)
Closes #13427
Authored by: bashonly, seproDev, doe1080

Co-authored-by: sepro <sepro@sepr0.com>
Co-authored-by: doe1080 <98906116+doe1080@users.noreply.github.com>
2025-09-23 06:33:30 +00:00
seproandGitHub 7f5d9f8543 [ie/youtube] Force player 0004de42 (#14398)
Closes #14400
Authored by: seproDev
2025-09-23 07:07:28 +02:00
seproandGitHub f8750504c2 [ie/twitch:clips] Fix extractor (#14397)
Closes #14396
Authored by: seproDev
2025-09-22 21:42:12 +02:00
8821682f15 [docs] Establish AI/LLM contribution policy (#14194)
Authored by: seproDev, bashonly

Co-authored-by: bashonly <88596187+bashonly@users.noreply.github.com>
2025-09-21 19:15:16 +02:00
seproandGitHub 08d7899683 [cleanup] Deprecate various options (#13821)
Closes #14198, Closes #12909
Authored by: seproDev
2025-09-21 17:10:37 +02:00
seproandGitHub 98b6b0d339 [utils] mimetype2ext: Recognize vnd.dlna.mpeg-tts (#14388)
Closes #14386
Authored by: seproDev
2025-09-21 16:29:21 +02:00
seproandGitHub bf5d18016b [ie/vk:uservideos] Support alternate URL format (#14376)
Closes #11793
Authored by: seproDev
2025-09-20 01:12:02 +02:00
seproandGitHub 4bc19adc87 [ie/ttinglive] Adapt FlexTV extractor to new domain (#14375)
Closes #14342
Authored by: seproDev
2025-09-20 00:38:34 +02:00
seproandGitHub b2c01d0498 [ie/applepodcast] Fix extractor (#14372)
Closes #14368
Authored by: seproDev
2025-09-19 22:22:47 +02:00
bashonlyandGitHub e123a48f11 [ie/telecinco] Support browser impersonation (#14351)
Closes #14349
Authored by: bashonly
2025-09-17 04:58:42 +00:00
bashonlyandGitHub 820c6e2445 [ie/mitele] Remove extractor (#14348)
Closes #13535
Authored by: bashonly
2025-09-16 23:03:09 +00:00
677997d84e [ie/xhamster] Fix extractor (#14345)
Fix a1c98226a4

Closes #14145
Authored by: arand, thegymguy

Co-authored-by: arand <183498+arand@users.noreply.github.com>
2025-09-16 22:19:08 +00:00
seproandGitHub b81e9272dc [ie/vk] Support vksport URLs (#14341)
Closes #14175
Authored by: seproDev
2025-09-16 19:43:00 +02:00
bashonlyandGitHub df4b4e8ccf [build] Use PyInstaller 6.16 for Windows (#14318)
Authored by: bashonly
2025-09-13 22:50:34 +00:00
seproandGitHub f3829463c7 [utils] random_user_agent: Bump versions (#14317)
Authored by: seproDev
2025-09-14 00:32:25 +02:00
bashonlyandGitHub ae3923b6b2 [ci] Improve workflow checks (#14316)
Authored by: bashonly
2025-09-13 21:55:35 +00:00
bashonlyandGitHub 8ab262c66b [cleanup] Remove references to setup.cfg (#14315)
Fix eb4b3a5fc7

Authored by: bashonly
2025-09-13 21:30:06 +00:00
bashonlyandGitHub e2d37bcc8e [build] Refactor Linux build jobs (#14275)
Authored by: bashonly
2025-09-13 21:20:28 +00:00
seproandGitHub eb4b3a5fc7 [cleanup] Remove setup.cfg (#14314)
Authored by: seproDev
2025-09-13 21:57:54 +02:00
bashonlyandGitHub 65e90aea29 [cleanup] Remove broken extractors (#14305)
Closes #1466, Closes #2005, Closes #4897, Closes #5118, Closes #8489, Closes #13072
Authored by: bashonly
2025-09-12 08:15:41 +00:00
doe1080andGitHub 17bfaa53ed [ie/onsen] Add extractor (#10971)
Closes #10902
Authored by: doe1080
2025-09-11 22:51:31 +00:00
8cb037c0b0 [ie/smotrim] Rework extractors (#14200)
Closes #9372, Closes #11804, Closes #13900
Authored by: doe1080, swayll

Co-authored-by: Nikolay Fedorov <40500428+swayll@users.noreply.github.com>
2025-09-11 20:59:54 +00:00
doe1080andGitHub 7d9e48b22a [ie/tunein] Fix extractors (#13981)
Authored by: doe1080
2025-09-11 20:42:01 +00:00
seproandGitHub f5cb721185 [ie/loco] Fix extractor (#14256)
Closes #14255
Authored by: seproDev
2025-09-11 21:32:35 +02:00
bashonlyandGitHub 83b8409366 [ci] Test with Python 3.14 (#13468)
Authored by: bashonly
2025-09-11 15:35:55 +00:00
bashonlyandGitHub ba80446855 [cleanup] Bump ruff to 0.13.x (#14293)
Authored by: bashonly
2025-09-11 15:18:01 +00:00
bashonlyandGitHub 22ea0688ed [ci] Bump actions/setup-python to v6 (#14282)
Authored by: bashonly
2025-09-10 21:29:12 +00:00
bashonlyandGitHub 5c1abcdc49 [ie/tiktok:live] Fix room ID extraction (#14287)
Closes #9418
Authored by: bashonly
2025-09-10 21:26:27 +00:00
doe1080andGitHub 3d9a88bd8e [ie/pixivsketch] Remove extractors (#14196)
Authored by: doe1080
2025-09-10 21:22:10 +00:00
doe1080andGitHub 9def9a4b0e [ie/newspicks] Warn when only preview is available (#14197)
Closes #14137
Authored by: doe1080
2025-09-10 21:20:03 +00:00
bashonlyandGitHub 679587dac7 [ie/vimeo] Fix login error handling (#14280)
Closes #14279
Authored by: bashonly
2025-09-10 18:39:07 +00:00
a1c98226a4 [ie/xhamster] Fix extractor (#14286)
Closes #14145
Authored by: nicolaasjan, willsmillie

Co-authored-by: nicolaasjan <14093220+nicolaasjan@users.noreply.github.com>
2025-09-10 18:17:24 +00:00
bashonlyandGitHub c8ede5f34d [build] Use new PyInstaller builds for Windows (#14273)
Authored by: bashonly
2025-09-08 22:44:36 +00:00
a183837ec8 [test:utils] Fix sanitize_path test for Windows CPython 3.11 (#13878)
Authored by: Grub4K

Co-authored-by: Simon Sawicki <contact@grub4k.dev>
2025-09-07 20:43:39 +00:00
SipherdrakonandGitHub 067062bb87 [ie/10play] Fix extractor (#14242)
Closes #14212
Authored by: Sipherdrakon
2025-09-07 21:09:23 +02:00
bashonlyandGitHub 8597a4331e [build] Fix cache warmer (#14261)
Fix 50136eeeb3

Authored by: bashonly
2025-09-07 00:57:20 +00:00
48a214bef4 [build] Use SPDX license identifier (#14260)
Authored by: cdce8p

Co-authored-by: Marc Mueller <30130371+cdce8p@users.noreply.github.com>
2025-09-07 02:02:20 +02:00
6a763a55d8 [compat] Add compat_datetime_from_timestamp (#11902)
Authored by: pzhlkj6612, seproDev

Co-authored-by: sepro <sepro@sepr0.com>
2025-09-07 01:48:24 +02:00
seproandGitHub e6e6b51214 [docs] Clarify license of PyInstaller-bundled executables (#14257)
Closes #348

Authored by: seproDev
2025-09-07 01:17:02 +02:00
bashonlyandGitHub 7c9b10ebc8 [ci] Test and lint workflows (#14249)
Authored by: bashonly
2025-09-06 22:28:11 +00:00
bashonlyandGitHub cd94e70040 [build] Post-release workflow cleanup (#14250)
Authored by: bashonly
2025-09-06 05:41:43 +00:00
116 changed files with 7265 additions and 2848 deletions
+2
View File
@@ -24,6 +24,8 @@ body:
required: true required: true
- label: I've searched [known issues](https://github.com/yt-dlp/yt-dlp/issues/3766), [the FAQ](https://github.com/yt-dlp/yt-dlp/wiki/FAQ), and the [bugtracker](https://github.com/yt-dlp/yt-dlp/issues?q=is%3Aissue%20-label%3Aspam%20%20) for similar issues **including closed ones**. DO NOT post duplicates - label: I've searched [known issues](https://github.com/yt-dlp/yt-dlp/issues/3766), [the FAQ](https://github.com/yt-dlp/yt-dlp/wiki/FAQ), and the [bugtracker](https://github.com/yt-dlp/yt-dlp/issues?q=is%3Aissue%20-label%3Aspam%20%20) for similar issues **including closed ones**. DO NOT post duplicates
required: true required: true
- label: I've read the [policy against AI/LLM contributions](https://github.com/yt-dlp/yt-dlp/blob/master/CONTRIBUTING.md#automated-contributions-ai--llm-policy) and understand I may be blocked from the repository if it is violated
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 - 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 - type: input
id: region id: region
@@ -24,6 +24,8 @@ body:
required: true required: true
- label: I've searched the [bugtracker](https://github.com/yt-dlp/yt-dlp/issues?q=is%3Aissue%20-label%3Aspam%20%20) for similar requests **including closed ones**. DO NOT post duplicates - label: I've searched the [bugtracker](https://github.com/yt-dlp/yt-dlp/issues?q=is%3Aissue%20-label%3Aspam%20%20) for similar requests **including closed ones**. DO NOT post duplicates
required: true required: true
- label: I've read the [policy against AI/LLM contributions](https://github.com/yt-dlp/yt-dlp/blob/master/CONTRIBUTING.md#automated-contributions-ai--llm-policy) and understand I may be blocked from the repository if it is violated
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 - 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 - type: input
id: region id: region
@@ -22,6 +22,8 @@ body:
required: true required: true
- label: I've searched the [bugtracker](https://github.com/yt-dlp/yt-dlp/issues?q=is%3Aissue%20-label%3Aspam%20%20) for similar requests **including closed ones**. DO NOT post duplicates - label: I've searched the [bugtracker](https://github.com/yt-dlp/yt-dlp/issues?q=is%3Aissue%20-label%3Aspam%20%20) for similar requests **including closed ones**. DO NOT post duplicates
required: true required: true
- label: I've read the [policy against AI/LLM contributions](https://github.com/yt-dlp/yt-dlp/blob/master/CONTRIBUTING.md#automated-contributions-ai--llm-policy) and understand I may be blocked from the repository if it is violated
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 - 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 - type: input
id: region id: region
+2
View File
@@ -20,6 +20,8 @@ body:
required: true required: true
- label: I've searched [known issues](https://github.com/yt-dlp/yt-dlp/issues/3766), [the FAQ](https://github.com/yt-dlp/yt-dlp/wiki/FAQ), and the [bugtracker](https://github.com/yt-dlp/yt-dlp/issues?q=is%3Aissue%20-label%3Aspam%20%20) for similar issues **including closed ones**. DO NOT post duplicates - label: I've searched [known issues](https://github.com/yt-dlp/yt-dlp/issues/3766), [the FAQ](https://github.com/yt-dlp/yt-dlp/wiki/FAQ), and the [bugtracker](https://github.com/yt-dlp/yt-dlp/issues?q=is%3Aissue%20-label%3Aspam%20%20) for similar issues **including closed ones**. DO NOT post duplicates
required: true required: true
- label: I've read the [policy against AI/LLM contributions](https://github.com/yt-dlp/yt-dlp/blob/master/CONTRIBUTING.md#automated-contributions-ai--llm-policy) and understand I may be blocked from the repository if it is violated
required: true
- type: textarea - type: textarea
id: description id: description
attributes: attributes:
@@ -22,6 +22,8 @@ body:
required: true required: true
- label: I've searched the [bugtracker](https://github.com/yt-dlp/yt-dlp/issues?q=is%3Aissue%20-label%3Aspam%20%20) for similar requests **including closed ones**. DO NOT post duplicates - label: I've searched the [bugtracker](https://github.com/yt-dlp/yt-dlp/issues?q=is%3Aissue%20-label%3Aspam%20%20) for similar requests **including closed ones**. DO NOT post duplicates
required: true required: true
- label: I've read the [policy against AI/LLM contributions](https://github.com/yt-dlp/yt-dlp/blob/master/CONTRIBUTING.md#automated-contributions-ai--llm-policy) and understand I may be blocked from the repository if it is violated
required: true
- type: textarea - type: textarea
id: description id: description
attributes: attributes:
+2
View File
@@ -28,6 +28,8 @@ body:
required: true required: true
- label: I've searched [known issues](https://github.com/yt-dlp/yt-dlp/issues/3766), [the FAQ](https://github.com/yt-dlp/yt-dlp/wiki/FAQ), and the [bugtracker](https://github.com/yt-dlp/yt-dlp/issues?q=is%3Aissue%20-label%3Aspam%20%20) for similar questions **including closed ones**. DO NOT post duplicates - label: I've searched [known issues](https://github.com/yt-dlp/yt-dlp/issues/3766), [the FAQ](https://github.com/yt-dlp/yt-dlp/wiki/FAQ), and the [bugtracker](https://github.com/yt-dlp/yt-dlp/issues?q=is%3Aissue%20-label%3Aspam%20%20) for similar questions **including closed ones**. DO NOT post duplicates
required: true required: true
- label: I've read the [policy against AI/LLM contributions](https://github.com/yt-dlp/yt-dlp/blob/master/CONTRIBUTING.md#automated-contributions-ai--llm-policy) and understand I may be blocked from the repository if it is violated
required: true
- type: textarea - type: textarea
id: question id: question
attributes: attributes:
@@ -20,6 +20,8 @@ body:
required: true required: true
- label: I've searched [known issues](https://github.com/yt-dlp/yt-dlp/issues/3766), [the FAQ](https://github.com/yt-dlp/yt-dlp/wiki/FAQ), and the [bugtracker](https://github.com/yt-dlp/yt-dlp/issues?q=is%%3Aissue%%20-label%%3Aspam%%20%%20) for similar issues **including closed ones**. DO NOT post duplicates - label: I've searched [known issues](https://github.com/yt-dlp/yt-dlp/issues/3766), [the FAQ](https://github.com/yt-dlp/yt-dlp/wiki/FAQ), and the [bugtracker](https://github.com/yt-dlp/yt-dlp/issues?q=is%%3Aissue%%20-label%%3Aspam%%20%%20) for similar issues **including closed ones**. DO NOT post duplicates
required: true required: true
- label: I've read the [policy against AI/LLM contributions](https://github.com/yt-dlp/yt-dlp/blob/master/CONTRIBUTING.md#automated-contributions-ai--llm-policy) and understand I may be blocked from the repository if it is violated
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 - 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 - type: input
id: region id: region
@@ -20,6 +20,8 @@ body:
required: true required: true
- label: I've searched the [bugtracker](https://github.com/yt-dlp/yt-dlp/issues?q=is%%3Aissue%%20-label%%3Aspam%%20%%20) for similar requests **including closed ones**. DO NOT post duplicates - label: I've searched the [bugtracker](https://github.com/yt-dlp/yt-dlp/issues?q=is%%3Aissue%%20-label%%3Aspam%%20%%20) for similar requests **including closed ones**. DO NOT post duplicates
required: true required: true
- label: I've read the [policy against AI/LLM contributions](https://github.com/yt-dlp/yt-dlp/blob/master/CONTRIBUTING.md#automated-contributions-ai--llm-policy) and understand I may be blocked from the repository if it is violated
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 - 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 - type: input
id: region id: region
@@ -18,6 +18,8 @@ body:
required: true required: true
- label: I've searched the [bugtracker](https://github.com/yt-dlp/yt-dlp/issues?q=is%%3Aissue%%20-label%%3Aspam%%20%%20) for similar requests **including closed ones**. DO NOT post duplicates - label: I've searched the [bugtracker](https://github.com/yt-dlp/yt-dlp/issues?q=is%%3Aissue%%20-label%%3Aspam%%20%%20) for similar requests **including closed ones**. DO NOT post duplicates
required: true required: true
- label: I've read the [policy against AI/LLM contributions](https://github.com/yt-dlp/yt-dlp/blob/master/CONTRIBUTING.md#automated-contributions-ai--llm-policy) and understand I may be blocked from the repository if it is violated
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 - 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 - type: input
id: region id: region
@@ -16,6 +16,8 @@ body:
required: true required: true
- label: I've searched [known issues](https://github.com/yt-dlp/yt-dlp/issues/3766), [the FAQ](https://github.com/yt-dlp/yt-dlp/wiki/FAQ), and the [bugtracker](https://github.com/yt-dlp/yt-dlp/issues?q=is%%3Aissue%%20-label%%3Aspam%%20%%20) for similar issues **including closed ones**. DO NOT post duplicates - label: I've searched [known issues](https://github.com/yt-dlp/yt-dlp/issues/3766), [the FAQ](https://github.com/yt-dlp/yt-dlp/wiki/FAQ), and the [bugtracker](https://github.com/yt-dlp/yt-dlp/issues?q=is%%3Aissue%%20-label%%3Aspam%%20%%20) for similar issues **including closed ones**. DO NOT post duplicates
required: true required: true
- label: I've read the [policy against AI/LLM contributions](https://github.com/yt-dlp/yt-dlp/blob/master/CONTRIBUTING.md#automated-contributions-ai--llm-policy) and understand I may be blocked from the repository if it is violated
required: true
- type: textarea - type: textarea
id: description id: description
attributes: attributes:
@@ -18,6 +18,8 @@ body:
required: true required: true
- label: I've searched the [bugtracker](https://github.com/yt-dlp/yt-dlp/issues?q=is%%3Aissue%%20-label%%3Aspam%%20%%20) for similar requests **including closed ones**. DO NOT post duplicates - label: I've searched the [bugtracker](https://github.com/yt-dlp/yt-dlp/issues?q=is%%3Aissue%%20-label%%3Aspam%%20%%20) for similar requests **including closed ones**. DO NOT post duplicates
required: true required: true
- label: I've read the [policy against AI/LLM contributions](https://github.com/yt-dlp/yt-dlp/blob/master/CONTRIBUTING.md#automated-contributions-ai--llm-policy) and understand I may be blocked from the repository if it is violated
required: true
- type: textarea - type: textarea
id: description id: description
attributes: attributes:
@@ -24,6 +24,8 @@ body:
required: true required: true
- label: I've searched [known issues](https://github.com/yt-dlp/yt-dlp/issues/3766), [the FAQ](https://github.com/yt-dlp/yt-dlp/wiki/FAQ), and the [bugtracker](https://github.com/yt-dlp/yt-dlp/issues?q=is%%3Aissue%%20-label%%3Aspam%%20%%20) for similar questions **including closed ones**. DO NOT post duplicates - label: I've searched [known issues](https://github.com/yt-dlp/yt-dlp/issues/3766), [the FAQ](https://github.com/yt-dlp/yt-dlp/wiki/FAQ), and the [bugtracker](https://github.com/yt-dlp/yt-dlp/issues?q=is%%3Aissue%%20-label%%3Aspam%%20%%20) for similar questions **including closed ones**. DO NOT post duplicates
required: true required: true
- label: I've read the [policy against AI/LLM contributions](https://github.com/yt-dlp/yt-dlp/blob/master/CONTRIBUTING.md#automated-contributions-ai--llm-policy) and understand I may be blocked from the repository if it is violated
required: true
- type: textarea - type: textarea
id: question id: question
attributes: attributes:
+1
View File
@@ -33,6 +33,7 @@ Fixes #
### In order to be accepted and merged into yt-dlp each piece of code must be in public domain or released under [Unlicense](http://unlicense.org/). Check those that apply and remove the others: ### In order to be accepted and merged into yt-dlp each piece of code must be in public domain or released under [Unlicense](http://unlicense.org/). Check those that apply and remove the others:
- [ ] I am the original author of the code in this PR, and I am willing to release it under [Unlicense](http://unlicense.org/) - [ ] I am the original author of the code in this PR, and I am willing to release it under [Unlicense](http://unlicense.org/)
- [ ] I am not the original author of the code in this PR, but it is in the public domain or released under [Unlicense](http://unlicense.org/) (provide reliable evidence) - [ ] I am not the original author of the code in this PR, but it is in the public domain or released under [Unlicense](http://unlicense.org/) (provide reliable evidence)
- [ ] I have read the [policy against AI/LLM contributions](https://github.com/yt-dlp/yt-dlp/blob/master/CONTRIBUTING.md#automated-contributions-ai--llm-policy) and understand I may be blocked from the repository if it is violated
### What is the purpose of your *pull request*? Check those that apply and remove the others: ### What is the purpose of your *pull request*? Check those that apply and remove the others:
- [ ] Fix or improvement to an extractor (Make sure to add/update tests) - [ ] Fix or improvement to an extractor (Make sure to add/update tests)
+28
View File
@@ -0,0 +1,28 @@
self-hosted-runner:
labels:
# Workaround for the outdated runner list in actionlint v1.7.7
# Ref: https://github.com/rhysd/actionlint/issues/533
- windows-11-arm
config-variables:
- KEEP_CACHE_WARM
- PUSH_VERSION_COMMIT
- UPDATE_TO_VERIFICATION
- PYPI_PROJECT
- PYPI_SUFFIX
- NIGHTLY_PYPI_PROJECT
- NIGHTLY_PYPI_SUFFIX
- NIGHTLY_ARCHIVE_REPO
- BUILD_NIGHTLY
- MASTER_PYPI_PROJECT
- MASTER_PYPI_SUFFIX
- MASTER_ARCHIVE_REPO
- BUILD_MASTER
- ISSUE_LOCKDOWN
- SANITIZE_COMMENT
paths:
.github/workflows/build.yml:
ignore:
# SC1090 "Can't follow non-constant source": ignore when using `source` to activate venv
- '.+SC1090.+'
+142 -144
View File
@@ -9,6 +9,9 @@ on:
required: false required: false
default: stable default: stable
type: string type: string
origin:
required: true
type: string
unix: unix:
default: true default: true
type: boolean type: boolean
@@ -27,10 +30,6 @@ on:
windows: windows:
default: true default: true
type: boolean type: boolean
origin:
required: false
default: ''
type: string
secrets: secrets:
GPG_SIGNING_KEY: GPG_SIGNING_KEY:
required: false required: false
@@ -74,13 +73,6 @@ on:
description: yt-dlp.exe, yt-dlp_win.zip, yt-dlp_x86.exe, yt-dlp_win_x86.zip, yt-dlp_arm64.exe, yt-dlp_win_arm64.zip description: yt-dlp.exe, yt-dlp_win.zip, yt-dlp_x86.exe, yt-dlp_win_x86.zip, yt-dlp_arm64.exe, yt-dlp_win_arm64.zip
default: true default: true
type: boolean type: boolean
origin:
description: Origin
required: false
default: 'current repo'
type: choice
options:
- 'current repo'
permissions: permissions:
contents: read contents: read
@@ -89,25 +81,26 @@ jobs:
process: process:
runs-on: ubuntu-latest runs-on: ubuntu-latest
outputs: outputs:
origin: ${{ steps.process_origin.outputs.origin }} origin: ${{ steps.process_inputs.outputs.origin }}
timestamp: ${{ steps.process_origin.outputs.timestamp }} timestamp: ${{ steps.process_inputs.outputs.timestamp }}
version: ${{ steps.process_origin.outputs.version }} version: ${{ steps.process_inputs.outputs.version }}
linux_matrix: ${{ steps.linux_matrix.outputs.matrix }}
steps: steps:
- name: Process origin - name: Process inputs
id: process_origin id: process_inputs
env: env:
ORIGIN: ${{ inputs.origin }} INPUTS: ${{ toJSON(inputs) }}
REPOSITORY: ${{ github.repository }} REPOSITORY: ${{ github.repository }}
VERSION: ${{ inputs.version }}
shell: python shell: python
run: | run: |
import datetime as dt import datetime as dt
import json import json
import os import os
import re import re
origin = os.environ['ORIGIN'] INPUTS = json.loads(os.environ['INPUTS'])
timestamp = dt.datetime.now(tz=dt.timezone.utc).strftime('%Y.%m.%d.%H%M%S.%f') timestamp = dt.datetime.now(tz=dt.timezone.utc).strftime('%Y.%m.%d.%H%M%S.%f')
version = os.getenv('VERSION') version = INPUTS.get('version')
if version and '.' not in version: if version and '.' not in version:
# build.yml was dispatched with only a revision as the version input value # build.yml was dispatched with only a revision as the version input value
version_parts = [*timestamp.split('.')[:3], version] version_parts = [*timestamp.split('.')[:3], version]
@@ -119,7 +112,7 @@ jobs:
version_parts = version.split('.') version_parts = version.split('.')
assert all(re.fullmatch(r'[0-9]+', part) for part in version_parts), 'Version must be numeric' assert all(re.fullmatch(r'[0-9]+', part) for part in version_parts), 'Version must be numeric'
outputs = { outputs = {
'origin': os.environ['REPOSITORY'] if origin == 'current repo' else origin, 'origin': INPUTS.get('origin') or os.environ['REPOSITORY'],
'timestamp': timestamp, 'timestamp': timestamp,
'version': '.'.join(version_parts), 'version': '.'.join(version_parts),
} }
@@ -127,6 +120,69 @@ jobs:
with open(os.environ['GITHUB_OUTPUT'], 'a') as f: with open(os.environ['GITHUB_OUTPUT'], 'a') as f:
f.write('\n'.join(f'{key}={value}' for key, value in outputs.items())) f.write('\n'.join(f'{key}={value}' for key, value in outputs.items()))
- name: Build Linux matrix
id: linux_matrix
env:
INPUTS: ${{ toJSON(inputs) }}
PYTHON_VERSION: '3.13'
UPDATE_TO: yt-dlp/yt-dlp@2025.09.05
shell: python
run: |
import json
import os
EXE_MAP = {
'linux': [{
'os': 'linux',
'arch': 'x86_64',
'runner': 'ubuntu-24.04',
}, {
'os': 'linux',
'arch': 'aarch64',
'runner': 'ubuntu-24.04-arm',
}],
'linux_armv7l': [{
'os': 'linux',
'arch': 'armv7l',
'runner': 'ubuntu-24.04-arm',
'qemu_platform': 'linux/arm/v7',
'onefile': False,
'cache_requirements': True,
'update_to': 'yt-dlp/yt-dlp@2023.03.04',
}],
'musllinux': [{
'os': 'musllinux',
'arch': 'x86_64',
'runner': 'ubuntu-24.04',
}, {
'os': 'musllinux',
'arch': 'aarch64',
'runner': 'ubuntu-24.04-arm',
}],
}
INPUTS = json.loads(os.environ['INPUTS'])
matrix = [exe for key, group in EXE_MAP.items() for exe in group if INPUTS.get(key)]
if not matrix:
# If we send an empty matrix when no linux inputs are given, the entire workflow fails
matrix = [EXE_MAP['linux'][0]]
for exe in matrix:
exe['exe'] = '_'.join(filter(None, (
'yt-dlp',
exe['os'],
exe['arch'] != 'x86_64' and exe['arch'],
)))
exe.setdefault('qemu_platform', None)
exe.setdefault('onefile', True)
exe.setdefault('onedir', True)
exe.setdefault('cache_requirements', False)
exe.setdefault('python_version', os.environ['PYTHON_VERSION'])
exe.setdefault('update_to', os.environ['UPDATE_TO'])
if not any(INPUTS.get(key) for key in EXE_MAP):
print('skipping linux job')
else:
print(json.dumps(matrix, indent=2))
with open(os.environ['GITHUB_OUTPUT'], 'a') as f:
f.write(f'matrix={json.dumps(matrix)}')
unix: unix:
needs: process needs: process
if: inputs.unix if: inputs.unix
@@ -135,33 +191,41 @@ jobs:
CHANNEL: ${{ inputs.channel }} CHANNEL: ${{ inputs.channel }}
ORIGIN: ${{ needs.process.outputs.origin }} ORIGIN: ${{ needs.process.outputs.origin }}
VERSION: ${{ needs.process.outputs.version }} VERSION: ${{ needs.process.outputs.version }}
UPDATE_TO: yt-dlp/yt-dlp@2025.09.05
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
with: with:
fetch-depth: 0 # Needed for changelog fetch-depth: 0 # Needed for changelog
- uses: actions/setup-python@v5
- uses: actions/setup-python@v6
with: with:
python-version: "3.10" python-version: "3.10"
- name: Install Requirements - name: Install Requirements
run: | run: |
sudo apt -y install zip pandoc man sed sudo apt -y install zip pandoc man sed
- name: Prepare - name: Prepare
run: | run: |
python devscripts/update-version.py -c "${CHANNEL}" -r "${ORIGIN}" "${VERSION}" python devscripts/update-version.py -c "${CHANNEL}" -r "${ORIGIN}" "${VERSION}"
python devscripts/update_changelog.py -vv python devscripts/update_changelog.py -vv
python devscripts/make_lazy_extractors.py python devscripts/make_lazy_extractors.py
- name: Build Unix platform-independent binary - name: Build Unix platform-independent binary
run: | run: |
make all tar make all tar
- name: Verify --update-to - name: Verify --update-to
if: vars.UPDATE_TO_VERIFICATION if: vars.UPDATE_TO_VERIFICATION
run: | run: |
chmod +x ./yt-dlp chmod +x ./yt-dlp
cp ./yt-dlp ./yt-dlp_downgraded cp ./yt-dlp ./yt-dlp_downgraded
version="$(./yt-dlp --version)" version="$(./yt-dlp --version)"
./yt-dlp_downgraded -v --update-to yt-dlp/yt-dlp@2023.03.04 ./yt-dlp_downgraded -v --update-to "${UPDATE_TO}"
downgraded_version="$(./yt-dlp_downgraded --version)" downgraded_version="$(./yt-dlp_downgraded --version)"
[[ "${version}" != "${downgraded_version}" ]] [[ "${version}" != "${downgraded_version}" ]]
- name: Upload artifacts - name: Upload artifacts
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v4
with: with:
@@ -172,153 +236,71 @@ jobs:
compression-level: 0 compression-level: 0
linux: linux:
name: ${{ matrix.os }} (${{ matrix.arch }})
if: inputs.linux || inputs.linux_armv7l || inputs.musllinux
needs: process needs: process
if: inputs.linux
runs-on: ${{ matrix.runner }} runs-on: ${{ matrix.runner }}
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
include: include: ${{ fromJSON(needs.process.outputs.linux_matrix) }}
- exe: yt-dlp_linux
platform: x86_64
runner: ubuntu-24.04
- exe: yt-dlp_linux_aarch64
platform: aarch64
runner: ubuntu-24.04-arm
env: env:
CHANNEL: ${{ inputs.channel }} CHANNEL: ${{ inputs.channel }}
ORIGIN: ${{ needs.process.outputs.origin }} ORIGIN: ${{ needs.process.outputs.origin }}
VERSION: ${{ needs.process.outputs.version }} VERSION: ${{ needs.process.outputs.version }}
EXE_NAME: ${{ matrix.exe }} EXE_NAME: ${{ matrix.exe }}
steps: PYTHON_VERSION: ${{ matrix.python_version }}
- uses: actions/checkout@v4 UPDATE_TO: ${{ (vars.UPDATE_TO_VERIFICATION && matrix.update_to) || '' }}
- name: Build executable SKIP_ONEDIR_BUILD: ${{ (!matrix.onedir && '1') || '' }}
env: SKIP_ONEFILE_BUILD: ${{ (!matrix.onefile && '1') || '' }}
SERVICE: linux_${{ matrix.platform }}
run: |
mkdir -p ./dist
pushd bundle/docker
docker compose up --build --exit-code-from "${SERVICE}" "${SERVICE}"
popd
sudo chown "${USER}:docker" "./dist/${EXE_NAME}"
- name: Verify executable in container
if: vars.UPDATE_TO_VERIFICATION
env:
SERVICE: linux_${{ matrix.platform }}_verify
run: |
cd bundle/docker
docker compose up --build --exit-code-from "${SERVICE}" "${SERVICE}"
- name: Verify --update-to
if: vars.UPDATE_TO_VERIFICATION
run: |
chmod +x "./dist/${EXE_NAME}"
mkdir -p ~/testing
cp "./dist/${EXE_NAME}" ~/testing/"${EXE_NAME}_downgraded"
version="$("./dist/${EXE_NAME}" --version)"
~/testing/"${EXE_NAME}_downgraded" -v --update-to yt-dlp/yt-dlp@2023.03.04
downgraded_version="$(~/testing/"${EXE_NAME}_downgraded" --version)"
[[ "${version}" != "${downgraded_version}" ]]
- name: Upload artifacts
uses: actions/upload-artifact@v4
with:
name: build-bin-${{ github.job }}_${{ matrix.platform }}
path: |
dist/${{ matrix.exe }}*
compression-level: 0
linux_armv7l:
needs: process
if: inputs.linux_armv7l
permissions:
contents: read
runs-on: ubuntu-24.04-arm
env:
CHANNEL: ${{ inputs.channel }}
ORIGIN: ${{ needs.process.outputs.origin }}
VERSION: ${{ needs.process.outputs.version }}
EXE_NAME: yt-dlp_linux_armv7l
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- name: Cache requirements - name: Cache requirements
if: matrix.cache_requirements
id: cache-venv id: cache-venv
uses: actions/cache@v4 uses: actions/cache@v4
env: env:
SEGMENT_DOWNLOAD_TIMEOUT_MINS: 1 SEGMENT_DOWNLOAD_TIMEOUT_MINS: 1
with: with:
path: | path: |
~/yt-dlp-build-venv venv
key: cache-reqs-${{ github.job }}-${{ github.ref }}-${{ needs.process.outputs.timestamp }} key: cache-reqs-${{ matrix.os }}_${{ matrix.arch }}-${{ github.ref }}-${{ needs.process.outputs.timestamp }}
restore-keys: | restore-keys: |
cache-reqs-${{ github.job }}-${{ github.ref }}- cache-reqs-${{ matrix.os }}_${{ matrix.arch }}-${{ github.ref }}-
cache-reqs-${{ github.job }}- cache-reqs-${{ matrix.os }}_${{ matrix.arch }}-
- name: Set up QEMU - name: Set up QEMU
if: matrix.qemu_platform
uses: docker/setup-qemu-action@v3 uses: docker/setup-qemu-action@v3
with: with:
platforms: linux/arm/v7 platforms: ${{ matrix.qemu_platform }}
- name: Build executable
env:
SERVICE: linux_armv7l
run: |
mkdir -p ./dist
mkdir -p ~/yt-dlp-build-venv
cd bundle/docker
docker compose up --build --exit-code-from "${SERVICE}" "${SERVICE}"
- name: Verify executable in container
if: vars.UPDATE_TO_VERIFICATION
env:
SERVICE: linux_armv7l_verify
run: |
cd bundle/docker
docker compose up --build --exit-code-from "${SERVICE}" "${SERVICE}"
- name: Upload artifacts
uses: actions/upload-artifact@v4
with:
name: build-bin-${{ github.job }}
path: |
dist/yt-dlp_linux_armv7l.zip
compression-level: 0
musllinux:
needs: process
if: inputs.musllinux
runs-on: ${{ matrix.runner }}
strategy:
fail-fast: false
matrix:
include:
- exe: yt-dlp_musllinux
platform: x86_64
runner: ubuntu-24.04
- exe: yt-dlp_musllinux_aarch64
platform: aarch64
runner: ubuntu-24.04-arm
env:
CHANNEL: ${{ inputs.channel }}
ORIGIN: ${{ needs.process.outputs.origin }}
VERSION: ${{ needs.process.outputs.version }}
EXE_NAME: ${{ matrix.exe }}
steps:
- uses: actions/checkout@v4
- name: Build executable - name: Build executable
env: env:
SERVICE: musllinux_${{ matrix.platform }} SERVICE: ${{ matrix.os }}_${{ matrix.arch }}
run: | run: |
mkdir -p ./venv
mkdir -p ./dist mkdir -p ./dist
pushd bundle/docker pushd bundle/docker
docker compose up --build --exit-code-from "${SERVICE}" "${SERVICE}" docker compose up --build --exit-code-from "${SERVICE}" "${SERVICE}"
popd popd
sudo chown "${USER}:docker" "./dist/${EXE_NAME}" if [[ -z "${SKIP_ONEFILE_BUILD}" ]]; then
sudo chown "${USER}:docker" "./dist/${EXE_NAME}"
fi
- name: Verify executable in container - name: Verify executable in container
if: vars.UPDATE_TO_VERIFICATION
env: env:
SERVICE: musllinux_${{ matrix.platform }}_verify SERVICE: ${{ matrix.os }}_${{ matrix.arch }}_verify
run: | run: |
cd bundle/docker cd bundle/docker
docker compose up --build --exit-code-from "${SERVICE}" "${SERVICE}" docker compose up --build --exit-code-from "${SERVICE}" "${SERVICE}"
- name: Upload artifacts - name: Upload artifacts
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v4
with: with:
name: build-bin-${{ github.job }}_${{ matrix.platform }} name: build-bin-${{ matrix.os }}_${{ matrix.arch }}
path: | path: |
dist/${{ matrix.exe }}* dist/${{ matrix.exe }}*
compression-level: 0 compression-level: 0
@@ -333,6 +315,7 @@ jobs:
CHANNEL: ${{ inputs.channel }} CHANNEL: ${{ inputs.channel }}
ORIGIN: ${{ needs.process.outputs.origin }} ORIGIN: ${{ needs.process.outputs.origin }}
VERSION: ${{ needs.process.outputs.version }} VERSION: ${{ needs.process.outputs.version }}
UPDATE_TO: yt-dlp/yt-dlp@2025.09.05
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
@@ -409,7 +392,7 @@ jobs:
chmod +x ./dist/yt-dlp_macos chmod +x ./dist/yt-dlp_macos
cp ./dist/yt-dlp_macos ./dist/yt-dlp_macos_downgraded cp ./dist/yt-dlp_macos ./dist/yt-dlp_macos_downgraded
version="$(./dist/yt-dlp_macos --version)" version="$(./dist/yt-dlp_macos --version)"
./dist/yt-dlp_macos_downgraded -v --update-to yt-dlp/yt-dlp@2023.03.04 ./dist/yt-dlp_macos_downgraded -v --update-to "${UPDATE_TO}"
downgraded_version="$(./dist/yt-dlp_macos_downgraded --version)" downgraded_version="$(./dist/yt-dlp_macos_downgraded --version)"
[[ "$version" != "$downgraded_version" ]] [[ "$version" != "$downgraded_version" ]]
@@ -423,6 +406,7 @@ jobs:
compression-level: 0 compression-level: 0
windows: windows:
name: windows (${{ matrix.arch }})
needs: process needs: process
if: inputs.windows if: inputs.windows
permissions: permissions:
@@ -435,27 +419,37 @@ jobs:
- arch: 'x64' - arch: 'x64'
runner: windows-2025 runner: windows-2025
python_version: '3.10' python_version: '3.10'
suffix: '' platform_tag: win_amd64
pyi_version: '6.16.0'
pyi_tag: '2025.09.13.221251'
pyi_hash: b6496c7630c3afe66900cfa824e8234a8c2e2c81704bd7facd79586abc76c0e5
- arch: 'x86' - arch: 'x86'
runner: windows-2025 runner: windows-2025
python_version: '3.10' python_version: '3.10'
suffix: '_x86' platform_tag: win32
pyi_version: '6.16.0'
pyi_tag: '2025.09.13.221251'
pyi_hash: 2d881843580efdc54f3523507fc6d9c5b6051ee49c743a6d9b7003ac5758c226
- arch: 'arm64' - arch: 'arm64'
runner: windows-11-arm runner: windows-11-arm
python_version: '3.13' # arm64 only has Python >= 3.11 available python_version: '3.13' # arm64 only has Python >= 3.11 available
suffix: '_arm64' platform_tag: win_arm64
pyi_version: '6.16.0'
pyi_tag: '2025.09.13.221251'
pyi_hash: 4250c9085e34a95c898f3ee2f764914fc36ec59f0d97c28e6a75fcf21f7b144f
env: env:
CHANNEL: ${{ inputs.channel }} CHANNEL: ${{ inputs.channel }}
ORIGIN: ${{ needs.process.outputs.origin }} ORIGIN: ${{ needs.process.outputs.origin }}
VERSION: ${{ needs.process.outputs.version }} VERSION: ${{ needs.process.outputs.version }}
SUFFIX: ${{ matrix.suffix }} SUFFIX: ${{ (matrix.arch != 'x64' && format('_{0}', matrix.arch)) || '' }}
UPDATE_TO: yt-dlp/yt-dlp@2025.09.05
BASE_CACHE_KEY: cache-reqs-${{ github.job }}_${{ matrix.arch }}-${{ matrix.python_version }} BASE_CACHE_KEY: cache-reqs-${{ github.job }}_${{ matrix.arch }}-${{ matrix.python_version }}
# Use custom PyInstaller built with https://github.com/yt-dlp/Pyinstaller-builds PYI_REPO: https://github.com/yt-dlp/Pyinstaller-Builds
PYINSTALLER_URL: https://yt-dlp.github.io/Pyinstaller-Builds/${{ matrix.arch }}/pyinstaller-6.15.0-py3-none-any.whl PYI_WHEEL: pyinstaller-${{ matrix.pyi_version }}-py3-none-${{ matrix.platform_tag }}.whl
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- uses: actions/setup-python@v5 - uses: actions/setup-python@v6
with: with:
python-version: ${{ matrix.python_version }} python-version: ${{ matrix.python_version }}
architecture: ${{ matrix.arch }} architecture: ${{ matrix.arch }}
@@ -477,17 +471,23 @@ jobs:
- name: Install Requirements - name: Install Requirements
env: env:
ARCH: ${{ matrix.arch }} ARCH: ${{ matrix.arch }}
PYI_URL: ${{ env.PYI_REPO }}/releases/download/${{ matrix.pyi_tag }}/${{ env.PYI_WHEEL }}
PYI_HASH: ${{ matrix.pyi_hash }}
shell: pwsh shell: pwsh
run: | run: |
python -m venv /yt-dlp-build-venv python -m venv /yt-dlp-build-venv
/yt-dlp-build-venv/Scripts/Activate.ps1 /yt-dlp-build-venv/Scripts/Activate.ps1
python -m pip install -U pip
# Install custom PyInstaller build and verify hash
mkdir /pyi-wheels
python -m pip download -d /pyi-wheels --no-deps --require-hashes "pyinstaller@${Env:PYI_URL}#sha256=${Env:PYI_HASH}"
python -m pip install --force-reinstall -U "/pyi-wheels/${Env:PYI_WHEEL}"
python devscripts/install_deps.py -o --include build python devscripts/install_deps.py -o --include build
if ("${Env:ARCH}" -eq "x86") { if ("${Env:ARCH}" -eq "x86") {
python devscripts/install_deps.py python devscripts/install_deps.py
} else { } else {
python devscripts/install_deps.py --include curl-cffi python devscripts/install_deps.py --include curl-cffi
} }
python -m pip install -U "${Env:PYINSTALLER_URL}"
- name: Prepare - name: Prepare
shell: pwsh shell: pwsh
@@ -510,7 +510,7 @@ jobs:
$name = "yt-dlp${Env:SUFFIX}" $name = "yt-dlp${Env:SUFFIX}"
Copy-Item "./dist/${name}.exe" "./dist/${name}_downgraded.exe" Copy-Item "./dist/${name}.exe" "./dist/${name}_downgraded.exe"
$version = & "./dist/${name}.exe" --version $version = & "./dist/${name}.exe" --version
& "./dist/${name}_downgraded.exe" -v --update-to yt-dlp/yt-dlp@2025.08.20 & "./dist/${name}_downgraded.exe" -v --update-to "${Env:UPDATE_TO}"
$downgraded_version = & "./dist/${name}_downgraded.exe" --version $downgraded_version = & "./dist/${name}_downgraded.exe" --version
if ($version -eq $downgraded_version) { if ($version -eq $downgraded_version) {
exit 1 exit 1
@@ -521,8 +521,8 @@ jobs:
with: with:
name: build-bin-${{ github.job }}-${{ matrix.arch }} name: build-bin-${{ github.job }}-${{ matrix.arch }}
path: | path: |
dist/yt-dlp${{ matrix.suffix }}.exe dist/yt-dlp${{ env.SUFFIX }}.exe
dist/yt-dlp_win${{ matrix.suffix }}.zip dist/yt-dlp_win${{ env.SUFFIX }}.zip
compression-level: 0 compression-level: 0
meta_files: meta_files:
@@ -531,8 +531,6 @@ jobs:
- process - process
- unix - unix
- linux - linux
- linux_armv7l
- musllinux
- macos - macos
- windows - windows
runs-on: ubuntu-latest runs-on: ubuntu-latest
+1
View File
@@ -12,6 +12,7 @@ jobs:
with: with:
version: '999999' version: '999999'
channel: stable channel: stable
origin: ${{ github.repository }}
unix: false unix: false
linux: false linux: false
linux_armv7l: true linux_armv7l: true
+6 -2
View File
@@ -37,23 +37,27 @@ jobs:
matrix: matrix:
os: [ubuntu-latest] os: [ubuntu-latest]
# CPython 3.9 is in quick-test # CPython 3.9 is in quick-test
python-version: ['3.10', '3.11', '3.12', '3.13', pypy-3.11] python-version: ['3.10', '3.11', '3.12', '3.13', '3.14-dev', pypy-3.11]
include: include:
# atleast one of each CPython/PyPy tests must be in windows # atleast one of each CPython/PyPy tests must be in windows
- os: windows-latest - os: windows-latest
python-version: '3.9' python-version: '3.9'
- os: windows-latest - os: windows-latest
python-version: '3.10' python-version: '3.10'
- os: windows-latest
python-version: '3.11'
- os: windows-latest - os: windows-latest
python-version: '3.12' python-version: '3.12'
- os: windows-latest - os: windows-latest
python-version: '3.13' python-version: '3.13'
- os: windows-latest
python-version: '3.14-dev'
- os: windows-latest - os: windows-latest
python-version: pypy-3.11 python-version: pypy-3.11
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- name: Set up Python ${{ matrix.python-version }} - name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v5 uses: actions/setup-python@v6
with: with:
python-version: ${{ matrix.python-version }} python-version: ${{ matrix.python-version }}
- name: Install test requirements - name: Install test requirements
+3 -3
View File
@@ -11,7 +11,7 @@ jobs:
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- name: Set up Python - name: Set up Python
uses: actions/setup-python@v5 uses: actions/setup-python@v6
with: with:
python-version: 3.9 python-version: 3.9
- name: Install test requirements - name: Install test requirements
@@ -28,7 +28,7 @@ jobs:
fail-fast: true fail-fast: true
matrix: matrix:
os: [ubuntu-latest] os: [ubuntu-latest]
python-version: ['3.10', '3.11', '3.12', '3.13', pypy-3.11] python-version: ['3.10', '3.11', '3.12', '3.13', '3.14-dev', pypy-3.11]
include: include:
# atleast one of each CPython/PyPy tests must be in windows # atleast one of each CPython/PyPy tests must be in windows
- os: windows-latest - os: windows-latest
@@ -38,7 +38,7 @@ jobs:
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- name: Set up Python ${{ matrix.python-version }} - name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v5 uses: actions/setup-python@v6
with: with:
python-version: ${{ matrix.python-version }} python-version: ${{ matrix.python-version }}
- name: Install test requirements - name: Install test requirements
+2 -2
View File
@@ -11,7 +11,7 @@ jobs:
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- name: Set up Python 3.9 - name: Set up Python 3.9
uses: actions/setup-python@v5 uses: actions/setup-python@v6
with: with:
python-version: '3.9' python-version: '3.9'
- name: Install test requirements - name: Install test requirements
@@ -27,7 +27,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- uses: actions/setup-python@v5 - uses: actions/setup-python@v6
with: with:
python-version: '3.9' python-version: '3.9'
- name: Install dev dependencies - name: Install dev dependencies
+9 -10
View File
@@ -79,9 +79,9 @@ jobs:
with: with:
fetch-depth: 0 fetch-depth: 0
- uses: actions/setup-python@v5 - uses: actions/setup-python@v6
with: with:
python-version: "3.10" python-version: "3.10" # Keep this in sync with test-workflows.yml
- name: Process inputs - name: Process inputs
id: process_inputs id: process_inputs
@@ -173,7 +173,7 @@ jobs:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
with: with:
fetch-depth: 0 fetch-depth: 0
- uses: actions/setup-python@v5 - uses: actions/setup-python@v6
with: with:
python-version: "3.10" python-version: "3.10"
@@ -241,7 +241,7 @@ jobs:
path: artifact path: artifact
pattern: build-* pattern: build-*
merge-multiple: true merge-multiple: true
- uses: actions/setup-python@v5 - uses: actions/setup-python@v6
with: with:
python-version: "3.10" python-version: "3.10"
@@ -269,12 +269,11 @@ jobs:
"[![Master](https://img.shields.io/badge/Master%20builds-lightblue.svg?style=for-the-badge)]" \ "[![Master](https://img.shields.io/badge/Master%20builds-lightblue.svg?style=for-the-badge)]" \
"(https://github.com/${MASTER_REPO}/releases/latest \"Master builds\")" >> ./RELEASE_NOTES "(https://github.com/${MASTER_REPO}/releases/latest \"Master builds\")" >> ./RELEASE_NOTES
fi fi
printf '\n\n' >> ./RELEASE_NOTES printf '\n\n%s\n\n%s%s\n\n---\n' \
cat >> ./RELEASE_NOTES << EOF "#### A description of the various files is in the [README](https://github.com/${REPOSITORY}#release-files)" \
#### A description of the various files is in the [README](https://github.com/${REPOSITORY}#release-files) "The PyInstaller-bundled executables are subject to the licenses described in " \
--- "[THIRD_PARTY_LICENSES.txt](https://github.com/${BASE_REPO}/blob/${HEAD_SHA}/THIRD_PARTY_LICENSES.txt)" >> ./RELEASE_NOTES
$(python ./devscripts/make_changelog.py -vv --collapsible) python ./devscripts/make_changelog.py -vv --collapsible >> ./RELEASE_NOTES
EOF
printf '%s\n\n' '**This is a pre-release build**' >> ./PRERELEASE_NOTES printf '%s\n\n' '**This is a pre-release build**' >> ./PRERELEASE_NOTES
cat ./RELEASE_NOTES >> ./PRERELEASE_NOTES cat ./RELEASE_NOTES >> ./PRERELEASE_NOTES
printf '%s\n\n' "Generated from: https://github.com/${REPOSITORY}/commit/${HEAD_SHA}" >> ./ARCHIVE_NOTES printf '%s\n\n' "Generated from: https://github.com/${REPOSITORY}/commit/${HEAD_SHA}" >> ./ARCHIVE_NOTES
+2 -2
View File
@@ -25,11 +25,11 @@ jobs:
fail-fast: false fail-fast: false
matrix: matrix:
os: [ubuntu-latest, windows-latest] os: [ubuntu-latest, windows-latest]
python-version: ['3.9', '3.10', '3.11', '3.12', '3.13', pypy-3.11] python-version: ['3.9', '3.10', '3.11', '3.12', '3.13', '3.14-dev', pypy-3.11]
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- name: Set up Python ${{ matrix.python-version }} - name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v5 uses: actions/setup-python@v6
with: with:
python-version: ${{ matrix.python-version }} python-version: ${{ matrix.python-version }}
- name: Install test requirements - name: Install test requirements
+52
View File
@@ -0,0 +1,52 @@
name: Test and lint workflows
on:
push:
paths:
- .github/workflows/*
- bundle/docker/linux/*.sh
- devscripts/setup_variables.py
- devscripts/setup_variables_tests.py
- devscripts/utils.py
pull_request:
paths:
- .github/workflows/*
- bundle/docker/linux/*.sh
- devscripts/setup_variables.py
- devscripts/setup_variables_tests.py
- devscripts/utils.py
permissions:
contents: read
env:
ACTIONLINT_VERSION: "1.7.7"
ACTIONLINT_SHA256SUM: 023070a287cd8cccd71515fedc843f1985bf96c436b7effaecce67290e7e0757
ACTIONLINT_REPO: https://github.com/rhysd/actionlint
jobs:
check:
name: Check workflows
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v6
with:
python-version: "3.10" # Keep this in sync with release.yml's prepare job
- name: Install requirements
env:
ACTIONLINT_TARBALL: ${{ format('actionlint_{0}_linux_amd64.tar.gz', env.ACTIONLINT_VERSION) }}
run: |
python -m devscripts.install_deps -o --include test
sudo apt -y install shellcheck
python -m pip install -U pyflakes
curl -LO "${ACTIONLINT_REPO}/releases/download/v${ACTIONLINT_VERSION}/${ACTIONLINT_TARBALL}"
printf '%s %s' "${ACTIONLINT_SHA256SUM}" "${ACTIONLINT_TARBALL}" | sha256sum -c -
tar xvzf "${ACTIONLINT_TARBALL}" actionlint
chmod +x actionlint
- name: Run actionlint
run: |
./actionlint -color
- name: Check Docker shell scripts
run: |
shellcheck bundle/docker/linux/*.sh
- name: Test GHA devscripts
run: |
pytest -Werror --tb=short --color=yes devscripts/setup_variables_tests.py
+13 -3
View File
@@ -12,6 +12,7 @@
- [Is your question about yt-dlp?](#is-your-question-about-yt-dlp) - [Is your question about yt-dlp?](#is-your-question-about-yt-dlp)
- [Are you willing to share account details if needed?](#are-you-willing-to-share-account-details-if-needed) - [Are you willing to share account details if needed?](#are-you-willing-to-share-account-details-if-needed)
- [Is the website primarily used for piracy](#is-the-website-primarily-used-for-piracy) - [Is the website primarily used for piracy](#is-the-website-primarily-used-for-piracy)
- [AUTOMATED CONTRIBUTIONS (AI / LLM) POLICY](#automated-contributions-ai--llm-policy)
- [DEVELOPER INSTRUCTIONS](#developer-instructions) - [DEVELOPER INSTRUCTIONS](#developer-instructions)
- [Adding new feature or making overarching changes](#adding-new-feature-or-making-overarching-changes) - [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) - [Adding support for a new site](#adding-support-for-a-new-site)
@@ -134,6 +135,17 @@ While these steps won't necessarily ensure that no misuse of the account takes p
We follow [youtube-dl's policy](https://github.com/ytdl-org/youtube-dl#can-you-add-support-for-this-anime-video-site-or-site-which-shows-current-movies-for-free) to not support services that is primarily used for infringing copyright. Additionally, it has been decided to not to support porn sites that specialize in fakes. We also cannot support any service that serves only [DRM protected content](https://en.wikipedia.org/wiki/Digital_rights_management). We follow [youtube-dl's policy](https://github.com/ytdl-org/youtube-dl#can-you-add-support-for-this-anime-video-site-or-site-which-shows-current-movies-for-free) to not support services that is primarily used for infringing copyright. Additionally, it has been decided to not to support porn sites that specialize in fakes. We also cannot support any service that serves only [DRM protected content](https://en.wikipedia.org/wiki/Digital_rights_management).
# AUTOMATED CONTRIBUTIONS (AI / LLM) POLICY
Please refrain from submitting issues or pull requests that have been generated by an LLM or other fully-automated tools. Any submission that is in violation of this policy will be closed, and the submitter may be blocked from this repository without warning.
If you submit an issue, you need to understand what your issue description is saying. You need to be able to answer questions about your bug report or feature request. Using an AI tool to *proofread* your issue/comment text is acceptable. Using an AI tool to *write* your issue/comment text is unacceptable.
If you submit a pull request, you need to understand what every line of code you've changed does. If you can't explain why your PR is doing something, then do not submit it. Using an AI tool to generate entire lines of code is unacceptable.
The rationale behind this policy is that automated contributions are a waste of the maintainers' time. Humans spend their time and brainpower reviewing every submission. Issues or pull requests generated by automation tools create an imbalance of effort between the submitter and the reviewer. Nobody learns anything when a maintainer reviews code written by an LLM.
Additionally, AI-generated code conflicts with this project's license (Unlicense), since you cannot truly release code into the public domain if you didn't author it yourself.
# DEVELOPER INSTRUCTIONS # DEVELOPER INSTRUCTIONS
@@ -768,12 +780,10 @@ view_count = int_or_none(video.get('views'))
``` ```
# My pull request is labeled pending-fixes ## My pull request is labeled pending-fixes
The `pending-fixes` label is added when there are changes requested to a PR. When the necessary changes are made, the label should be removed. However, despite our best efforts, it may sometimes happen that the maintainer did not see the changes or forgot to remove the label. If your PR is still marked as `pending-fixes` a few days after all requested changes have been made, feel free to ping the maintainer who labeled your issue and ask them to re-review and remove the label. The `pending-fixes` label is added when there are changes requested to a PR. When the necessary changes are made, the label should be removed. However, despite our best efforts, it may sometimes happen that the maintainer did not see the changes or forgot to remove the label. If your PR is still marked as `pending-fixes` a few days after all requested changes have been made, feel free to ping the maintainer who labeled your issue and ask them to re-review and remove the label.
# EMBEDDING YT-DLP # 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 See [README.md#embedding-yt-dlp](README.md#embedding-yt-dlp) for instructions on how to embed yt-dlp in another Python program
+10
View File
@@ -808,3 +808,13 @@ Randalix
runarmod runarmod
gitchasing gitchasing
zakaryan2004 zakaryan2004
cdce8p
nicolaasjan
willsmillie
CasualYT31
cecilia-sanare
dhwz
robin-mu
shssoichiro
thanhtaivtt
uoag
+93
View File
@@ -4,6 +4,99 @@
# To create a release, dispatch the https://github.com/yt-dlp/yt-dlp/actions/workflows/release.yml workflow on master # To create a release, dispatch the https://github.com/yt-dlp/yt-dlp/actions/workflows/release.yml workflow on master
--> -->
### 2025.10.14
#### Core changes
- [Fix `prefer-vp9-sort` compat option](https://github.com/yt-dlp/yt-dlp/commit/a6673a8e82276ea529c1773ed09e5bc4a22e822a) ([#14603](https://github.com/yt-dlp/yt-dlp/issues/14603)) by [seproDev](https://github.com/seproDev)
#### Extractor changes
- **10play**
- [Handle geo-restriction errors](https://github.com/yt-dlp/yt-dlp/commit/ad55bfcfb700fbfc1364c04e3425761d6f95c0a7) ([#14618](https://github.com/yt-dlp/yt-dlp/issues/14618)) by [bashonly](https://github.com/bashonly)
- [Rework extractor](https://github.com/yt-dlp/yt-dlp/commit/eafedc21817bb0de20e9aaccd7151a1d4c4e1ebd) ([#14417](https://github.com/yt-dlp/yt-dlp/issues/14417)) by [seproDev](https://github.com/seproDev), [Sipherdrakon](https://github.com/Sipherdrakon)
- **abc.net.au**: [Support listen URLs](https://github.com/yt-dlp/yt-dlp/commit/0ea5d5882def84415f946907cfc00ab431c18fed) ([#14389](https://github.com/yt-dlp/yt-dlp/issues/14389)) by [uoag](https://github.com/uoag)
- **cbc.ca**: listen: [Add extractor](https://github.com/yt-dlp/yt-dlp/commit/df160ab18db523f6629f2e7e20123d7a3551df28) ([#14391](https://github.com/yt-dlp/yt-dlp/issues/14391)) by [uoag](https://github.com/uoag)
- **dropout**: [Update extractor for new domain](https://github.com/yt-dlp/yt-dlp/commit/8eb8695139dece6351aac10463df63b87b45b000) ([#14531](https://github.com/yt-dlp/yt-dlp/issues/14531)) by [cecilia-sanare](https://github.com/cecilia-sanare)
- **idagio**: [Add extractors](https://github.com/yt-dlp/yt-dlp/commit/a98e7f9f58a9492d2cb216baa59c890ed8ce02f3) ([#14586](https://github.com/yt-dlp/yt-dlp/issues/14586)) by [robin-mu](https://github.com/robin-mu)
- **musescore**: [Fix extractor](https://github.com/yt-dlp/yt-dlp/commit/87be1bb96ac47abaaa4cfc6d7dd651e511b74551) ([#14598](https://github.com/yt-dlp/yt-dlp/issues/14598)) by [seproDev](https://github.com/seproDev)
- **prankcastpost**: [Rework extractor](https://github.com/yt-dlp/yt-dlp/commit/5d7678195a7d0c045a9fe0418383171a71a7ea43) ([#14445](https://github.com/yt-dlp/yt-dlp/issues/14445)) by [columndeeply](https://github.com/columndeeply)
- **slideslive**: [Fix extractor](https://github.com/yt-dlp/yt-dlp/commit/c2e124881f9aa02097589e853b3d3505e78372c4) ([#14619](https://github.com/yt-dlp/yt-dlp/issues/14619)) by [bashonly](https://github.com/bashonly)
- **soundcloud**: [Support new API URLs](https://github.com/yt-dlp/yt-dlp/commit/6d41aaf21c61a87e74564646abd0a8ee887e888d) ([#14449](https://github.com/yt-dlp/yt-dlp/issues/14449)) by [seproDev](https://github.com/seproDev)
- **tiktok**
- [Support browser impersonation](https://github.com/yt-dlp/yt-dlp/commit/5513036104ed9710f624c537fb3644b07a0680db) ([#14473](https://github.com/yt-dlp/yt-dlp/issues/14473)) by [bashonly](https://github.com/bashonly), [thanhtaivtt](https://github.com/thanhtaivtt)
- user: [Fix private account extraction](https://github.com/yt-dlp/yt-dlp/commit/cdc533b114c35ceb8a2e9dd3eb9c172a8737ae5e) ([#14585](https://github.com/yt-dlp/yt-dlp/issues/14585)) by [CasualYT31](https://github.com/CasualYT31)
- **vidyard**: [Extract chapters](https://github.com/yt-dlp/yt-dlp/commit/5f94f054907c12e68129cd9ac2508ed8aba1b223) ([#14478](https://github.com/yt-dlp/yt-dlp/issues/14478)) by [exterrestris](https://github.com/exterrestris)
- **xhamster**: [Fix extractor](https://github.com/yt-dlp/yt-dlp/commit/739125d40f8ede3beb7be68fc4df55bec0d226fd) ([#14446](https://github.com/yt-dlp/yt-dlp/issues/14446)) by [dhwz](https://github.com/dhwz), [dirkf](https://github.com/dirkf), [shssoichiro](https://github.com/shssoichiro)
- **youtube**
- [Detect experiment binding GVS PO Token to video id](https://github.com/yt-dlp/yt-dlp/commit/bd5ed90419eea18adfb2f0d8efa9d22b2029119f) ([#14471](https://github.com/yt-dlp/yt-dlp/issues/14471)) by [coletdjnz](https://github.com/coletdjnz)
- tab: [Fix approximate timestamp extraction for feeds](https://github.com/yt-dlp/yt-dlp/commit/ccc25d6710a4aa373b7e15c558e07f8a2ffae5f3) ([#14539](https://github.com/yt-dlp/yt-dlp/issues/14539)) by [coletdjnz](https://github.com/coletdjnz)
### 2025.09.26
#### Extractor changes
- **twitch**: vod: [Fix `live_status` detection](https://github.com/yt-dlp/yt-dlp/commit/50e452fd7dfb8a648bd3b9aaabc8f94f37ce2051) ([#14457](https://github.com/yt-dlp/yt-dlp/issues/14457)) by [bashonly](https://github.com/bashonly)
- **youtube**
- [Fix player JS overrides](https://github.com/yt-dlp/yt-dlp/commit/b7b7910d96359a539b7997890342ab4a59dd685d) ([#14430](https://github.com/yt-dlp/yt-dlp/issues/14430)) by [bashonly](https://github.com/bashonly), [seproDev](https://github.com/seproDev)
- [Improve PO token logging](https://github.com/yt-dlp/yt-dlp/commit/7df5acc546dccd32213c3a125d721e32b06d71b0) ([#14447](https://github.com/yt-dlp/yt-dlp/issues/14447)) by [seproDev](https://github.com/seproDev)
- [Player client maintenance](https://github.com/yt-dlp/yt-dlp/commit/94c5622be96474ca3c637e52898c4daee4d8fb69) ([#14448](https://github.com/yt-dlp/yt-dlp/issues/14448)) by [seproDev](https://github.com/seproDev)
- [Replace `tv_simply` with `web_safari` in default clients](https://github.com/yt-dlp/yt-dlp/commit/12b57d2858845c0c7fb33bf9aa8ed7be6905535d) ([#14465](https://github.com/yt-dlp/yt-dlp/issues/14465)) by [bashonly](https://github.com/bashonly)
### 2025.09.23
#### Important changes
- **Several options have been deprecated**
In order to simplify the codebase and reduce maintenance burden, various options have been deprecated. Please remove them from your commands/configurations. [Read more](https://github.com/yt-dlp/yt-dlp/issues/14198)
#### Core changes
- **compat**: [Add `compat_datetime_from_timestamp`](https://github.com/yt-dlp/yt-dlp/commit/6a763a55d8a93b2a964ecf7699248ad342485412) ([#11902](https://github.com/yt-dlp/yt-dlp/issues/11902)) by [pzhlkj6612](https://github.com/pzhlkj6612), [seproDev](https://github.com/seproDev)
- **utils**
- `mimetype2ext`: [Recognize `vnd.dlna.mpeg-tts`](https://github.com/yt-dlp/yt-dlp/commit/98b6b0d339130e955f9d45ce67c0357c633c1627) ([#14388](https://github.com/yt-dlp/yt-dlp/issues/14388)) by [seproDev](https://github.com/seproDev)
- `random_user_agent`: [Bump versions](https://github.com/yt-dlp/yt-dlp/commit/f3829463c728a5b5e62b3fc157e71c99b26edac7) ([#14317](https://github.com/yt-dlp/yt-dlp/issues/14317)) by [seproDev](https://github.com/seproDev)
#### Extractor changes
- **10play**: [Fix extractor](https://github.com/yt-dlp/yt-dlp/commit/067062bb87ac057e453ce9efdac7ca117a6a7da0) ([#14242](https://github.com/yt-dlp/yt-dlp/issues/14242)) by [Sipherdrakon](https://github.com/Sipherdrakon)
- **applepodcast**: [Fix extractor](https://github.com/yt-dlp/yt-dlp/commit/b2c01d0498653e0239c7226c5a7fcb614dd4dbc8) ([#14372](https://github.com/yt-dlp/yt-dlp/issues/14372)) by [seproDev](https://github.com/seproDev)
- **loco**: [Fix extractor](https://github.com/yt-dlp/yt-dlp/commit/f5cb721185e8725cf4eb4080e86aa9aa73ef25b3) ([#14256](https://github.com/yt-dlp/yt-dlp/issues/14256)) by [seproDev](https://github.com/seproDev)
- **mitele**: [Remove extractor](https://github.com/yt-dlp/yt-dlp/commit/820c6e244571557fcfc127d4b3680e2d07c04dca) ([#14348](https://github.com/yt-dlp/yt-dlp/issues/14348)) by [bashonly](https://github.com/bashonly)
- **newspicks**: [Warn when only preview is available](https://github.com/yt-dlp/yt-dlp/commit/9def9a4b0e958285e055eb350e5dd43b5c423336) ([#14197](https://github.com/yt-dlp/yt-dlp/issues/14197)) by [doe1080](https://github.com/doe1080)
- **onsen**: [Add extractor](https://github.com/yt-dlp/yt-dlp/commit/17bfaa53edf5c52fce73cf0cef4592f929c2462d) ([#10971](https://github.com/yt-dlp/yt-dlp/issues/10971)) by [doe1080](https://github.com/doe1080)
- **pixivsketch**: [Remove extractors](https://github.com/yt-dlp/yt-dlp/commit/3d9a88bd8ef149d781c7e569e48e61551eda395e) ([#14196](https://github.com/yt-dlp/yt-dlp/issues/14196)) by [doe1080](https://github.com/doe1080)
- **smotrim**: [Rework extractors](https://github.com/yt-dlp/yt-dlp/commit/8cb037c0b06c2815080f87d61ea2e95c412785fc) ([#14200](https://github.com/yt-dlp/yt-dlp/issues/14200)) by [doe1080](https://github.com/doe1080), [swayll](https://github.com/swayll)
- **telecinco**: [Support browser impersonation](https://github.com/yt-dlp/yt-dlp/commit/e123a48f1155703d8709a4221a42bd45c0a2b3ce) ([#14351](https://github.com/yt-dlp/yt-dlp/issues/14351)) by [bashonly](https://github.com/bashonly)
- **tiktok**: live: [Fix room ID extraction](https://github.com/yt-dlp/yt-dlp/commit/5c1abcdc49b9d23e1dcb77b95d063cf2bf93e352) ([#14287](https://github.com/yt-dlp/yt-dlp/issues/14287)) by [bashonly](https://github.com/bashonly)
- **ttinglive**: [Adapt FlexTV extractor to new domain](https://github.com/yt-dlp/yt-dlp/commit/4bc19adc8798e7564513898cf34adc432c6c5709) ([#14375](https://github.com/yt-dlp/yt-dlp/issues/14375)) by [seproDev](https://github.com/seproDev)
- **tunein**: [Fix extractors](https://github.com/yt-dlp/yt-dlp/commit/7d9e48b22a780c2e8d2d2d68940d49fd2029ab70) ([#13981](https://github.com/yt-dlp/yt-dlp/issues/13981)) by [doe1080](https://github.com/doe1080)
- **twitch**: clips: [Fix extractor](https://github.com/yt-dlp/yt-dlp/commit/f8750504c2f71b54586fb857d60dce4e354a13ea) ([#14397](https://github.com/yt-dlp/yt-dlp/issues/14397)) by [seproDev](https://github.com/seproDev)
- **vimeo**: [Fix login error handling](https://github.com/yt-dlp/yt-dlp/commit/679587dac7cd011a1472255e1f06efb017ba91b6) ([#14280](https://github.com/yt-dlp/yt-dlp/issues/14280)) by [bashonly](https://github.com/bashonly)
- **vk**
- [Support vksport URLs](https://github.com/yt-dlp/yt-dlp/commit/b81e9272dce5844e8fba371cb4b4fd95ad3ed819) ([#14341](https://github.com/yt-dlp/yt-dlp/issues/14341)) by [seproDev](https://github.com/seproDev)
- uservideos: [Support alternate URL format](https://github.com/yt-dlp/yt-dlp/commit/bf5d18016b03a3f2fd5d3494d9efe85d3f8beeac) ([#14376](https://github.com/yt-dlp/yt-dlp/issues/14376)) by [seproDev](https://github.com/seproDev)
- **xhamster**: [Fix extractor](https://github.com/yt-dlp/yt-dlp/commit/a1c98226a4e869a34cc764a9dcf7a4558516308e) ([#14286](https://github.com/yt-dlp/yt-dlp/issues/14286)) by [nicolaasjan](https://github.com/nicolaasjan), [willsmillie](https://github.com/willsmillie) (With fixes in [677997d](https://github.com/yt-dlp/yt-dlp/commit/677997d84eaec0037397f7d935386daa3025b004) by [arand](https://github.com/arand), [thegymguy](https://github.com/thegymguy))
- **youtube**: [Force player `0004de42`](https://github.com/yt-dlp/yt-dlp/commit/7f5d9f8543d19590eeec9473d54fa00151afa78a) ([#14398](https://github.com/yt-dlp/yt-dlp/issues/14398)) by [seproDev](https://github.com/seproDev)
#### Misc. changes
- **build**
- [Fix cache warmer](https://github.com/yt-dlp/yt-dlp/commit/8597a4331e8535a246d777bb8397bdcab251766c) ([#14261](https://github.com/yt-dlp/yt-dlp/issues/14261)) by [bashonly](https://github.com/bashonly)
- [Post-release workflow cleanup](https://github.com/yt-dlp/yt-dlp/commit/cd94e7004036e0149d7d3fa236c7dd44cf460788) ([#14250](https://github.com/yt-dlp/yt-dlp/issues/14250)) by [bashonly](https://github.com/bashonly)
- [Refactor Linux build jobs](https://github.com/yt-dlp/yt-dlp/commit/e2d37bcc8e84be9ce0f67fc24cb830c13963d10f) ([#14275](https://github.com/yt-dlp/yt-dlp/issues/14275)) by [bashonly](https://github.com/bashonly)
- [Use PyInstaller 6.16 for Windows](https://github.com/yt-dlp/yt-dlp/commit/df4b4e8ccf3385be6d2ad65465a0704c223dfdfb) ([#14318](https://github.com/yt-dlp/yt-dlp/issues/14318)) by [bashonly](https://github.com/bashonly)
- [Use SPDX license identifier](https://github.com/yt-dlp/yt-dlp/commit/48a214bef4bfd5984362d3d24b09dce50ba449ea) ([#14260](https://github.com/yt-dlp/yt-dlp/issues/14260)) by [cdce8p](https://github.com/cdce8p)
- [Use new PyInstaller builds for Windows](https://github.com/yt-dlp/yt-dlp/commit/c8ede5f34d6c95c442b936bb01ecbcb724aefdef) ([#14273](https://github.com/yt-dlp/yt-dlp/issues/14273)) by [bashonly](https://github.com/bashonly)
- **ci**
- [Bump actions/setup-python to v6](https://github.com/yt-dlp/yt-dlp/commit/22ea0688ed6bcdbe4c51401a84239cda3decfc9c) ([#14282](https://github.com/yt-dlp/yt-dlp/issues/14282)) by [bashonly](https://github.com/bashonly)
- [Improve workflow checks](https://github.com/yt-dlp/yt-dlp/commit/ae3923b6b23bc62115be55510d6b5842f7a46b5f) ([#14316](https://github.com/yt-dlp/yt-dlp/issues/14316)) by [bashonly](https://github.com/bashonly)
- [Test and lint workflows](https://github.com/yt-dlp/yt-dlp/commit/7c9b10ebc83907d37f9f65ea9d4bd6f5e3bd1371) ([#14249](https://github.com/yt-dlp/yt-dlp/issues/14249)) by [bashonly](https://github.com/bashonly)
- [Test with Python 3.14](https://github.com/yt-dlp/yt-dlp/commit/83b8409366d0f9554eaeae56394b244dab64a2cb) ([#13468](https://github.com/yt-dlp/yt-dlp/issues/13468)) by [bashonly](https://github.com/bashonly)
- **cleanup**
- [Bump ruff to 0.13.x](https://github.com/yt-dlp/yt-dlp/commit/ba8044685537e8e14adc6826fb4d730856fd2e2b) ([#14293](https://github.com/yt-dlp/yt-dlp/issues/14293)) by [bashonly](https://github.com/bashonly)
- [Deprecate various options](https://github.com/yt-dlp/yt-dlp/commit/08d78996831bd8e1e3c2592d740c3def00bbf548) ([#13821](https://github.com/yt-dlp/yt-dlp/issues/13821)) by [seproDev](https://github.com/seproDev)
- [Remove broken extractors](https://github.com/yt-dlp/yt-dlp/commit/65e90aea29cf3bfc9d1ae3e009fbf9a8db3a23c9) ([#14305](https://github.com/yt-dlp/yt-dlp/issues/14305)) by [bashonly](https://github.com/bashonly)
- [Remove setup.cfg](https://github.com/yt-dlp/yt-dlp/commit/eb4b3a5fc7765a6cd0370ca44ccee0d7d5111dd7) ([#14314](https://github.com/yt-dlp/yt-dlp/issues/14314)) by [seproDev](https://github.com/seproDev) (With fixes in [8ab262c](https://github.com/yt-dlp/yt-dlp/commit/8ab262c66bd3e1d8874fb2d070068ba1f0d48f16) by [bashonly](https://github.com/bashonly))
- Miscellaneous: [2e81e29](https://github.com/yt-dlp/yt-dlp/commit/2e81e298cdce23afadb06a95836284acb38f7018) by [bashonly](https://github.com/bashonly), [doe1080](https://github.com/doe1080), [seproDev](https://github.com/seproDev)
- **docs**
- [Clarify license of PyInstaller-bundled executables](https://github.com/yt-dlp/yt-dlp/commit/e6e6b512141e66b1b36058966804fe59c02a2b4d) ([#14257](https://github.com/yt-dlp/yt-dlp/issues/14257)) by [seproDev](https://github.com/seproDev)
- [Establish AI/LLM contribution policy](https://github.com/yt-dlp/yt-dlp/commit/8821682f15af59047bc1f92724ef8a9ba30d6f7e) ([#14194](https://github.com/yt-dlp/yt-dlp/issues/14194)) by [bashonly](https://github.com/bashonly), [seproDev](https://github.com/seproDev)
- **test**: utils: [Fix `sanitize_path` test for Windows CPython 3.11](https://github.com/yt-dlp/yt-dlp/commit/a183837ec8bb5e28fe6eb3a9d77ea2d0d7a106bd) ([#13878](https://github.com/yt-dlp/yt-dlp/issues/13878)) by [Grub4K](https://github.com/Grub4K)
### 2025.09.05 ### 2025.09.05
#### Core changes #### Core changes
+2 -2
View File
@@ -10,7 +10,7 @@ tar: yt-dlp.tar.gz
# intended use: when building a source distribution, # intended use: when building a source distribution,
# make pypi-files && python3 -m build -sn . # make pypi-files && python3 -m build -sn .
pypi-files: AUTHORS Changelog.md LICENSE README.md README.txt supportedsites \ pypi-files: AUTHORS Changelog.md LICENSE README.md README.txt supportedsites \
completions yt-dlp.1 pyproject.toml setup.cfg devscripts/* test/* completions yt-dlp.1 pyproject.toml devscripts/* test/*
.PHONY: all clean clean-all clean-test clean-dist clean-cache \ .PHONY: all clean clean-all clean-test clean-dist clean-cache \
completions completion-bash completion-fish completion-zsh \ completions completion-bash completion-fish completion-zsh \
@@ -159,7 +159,7 @@ yt-dlp.tar.gz: all
README.md supportedsites.md Changelog.md LICENSE \ README.md supportedsites.md Changelog.md LICENSE \
CONTRIBUTING.md Collaborators.md CONTRIBUTORS AUTHORS \ CONTRIBUTING.md Collaborators.md CONTRIBUTORS AUTHORS \
Makefile yt-dlp.1 README.txt completions .gitignore \ Makefile yt-dlp.1 README.txt completions .gitignore \
setup.cfg yt-dlp yt_dlp pyproject.toml devscripts test yt-dlp yt_dlp pyproject.toml devscripts test
AUTHORS: Changelog.md AUTHORS: Changelog.md
@if [ -d '.git' ] && command -v git > /dev/null ; then \ @if [ -d '.git' ] && command -v git > /dev/null ; then \
+30 -22
View File
@@ -138,6 +138,17 @@ curl -L https://github.com/yt-dlp/yt-dlp/raw/master/public.key | gpg --import
gpg --verify SHA2-256SUMS.sig SHA2-256SUMS gpg --verify SHA2-256SUMS.sig SHA2-256SUMS
gpg --verify SHA2-512SUMS.sig SHA2-512SUMS gpg --verify SHA2-512SUMS.sig SHA2-512SUMS
``` ```
#### Licensing
While yt-dlp is licensed under the [Unlicense](LICENSE), many of the release files contain code from other projects with different licenses.
Most notably, the PyInstaller-bundled executables include GPLv3+ licensed code, and as such the combined work is licensed under [GPLv3+](https://www.gnu.org/licenses/gpl-3.0.html).
See [THIRD_PARTY_LICENSES.txt](THIRD_PARTY_LICENSES.txt) for details.
The zipimport binary (`yt-dlp`), the source tarball (`yt-dlp.tar.gz`), and the PyPI source distribution & wheel only contain code licensed under the [Unlicense](LICENSE).
<!-- MANPAGE: END EXCLUDED SECTION --> <!-- MANPAGE: END EXCLUDED SECTION -->
**Note**: The manpages, shell completion (autocomplete) files etc. are available inside the [source tarball](https://github.com/yt-dlp/yt-dlp/releases/latest/download/yt-dlp.tar.gz) **Note**: The manpages, shell completion (autocomplete) files etc. are available inside the [source tarball](https://github.com/yt-dlp/yt-dlp/releases/latest/download/yt-dlp.tar.gz)
@@ -230,8 +241,6 @@ The following provide support for impersonating browser requests. This may be re
### Deprecated ### Deprecated
* [**avconv** and **avprobe**](https://www.libav.org) - Now **deprecated** alternative to ffmpeg. License [depends on the build](https://libav.org/legal)
* [**sponskrub**](https://github.com/faissaloo/SponSkrub) - For using the now **deprecated** [sponskrub options](#sponskrub-options). Licensed under [GPLv3+](https://github.com/faissaloo/SponSkrub/blob/master/LICENCE.md)
* [**rtmpdump**](http://rtmpdump.mplayerhq.hu) - For downloading `rtmp` streams. ffmpeg can be used instead with `--downloader ffmpeg`. Licensed under [GPLv2+](http://rtmpdump.mplayerhq.hu) * [**rtmpdump**](http://rtmpdump.mplayerhq.hu) - For downloading `rtmp` streams. ffmpeg can be used instead with `--downloader ffmpeg`. Licensed under [GPLv2+](http://rtmpdump.mplayerhq.hu)
* [**mplayer**](http://mplayerhq.hu/design7/info.html) or [**mpv**](https://mpv.io) - For downloading `rstp`/`mms` streams. ffmpeg can be used instead with `--downloader ffmpeg`. Licensed under [GPLv2+](https://github.com/mpv-player/mpv/blob/master/Copyright) * [**mplayer**](http://mplayerhq.hu/design7/info.html) or [**mpv**](https://mpv.io) - For downloading `rstp`/`mms` streams. ffmpeg can be used instead with `--downloader ffmpeg`. Licensed under [GPLv2+](https://github.com/mpv-player/mpv/blob/master/Copyright)
@@ -311,7 +320,6 @@ Tip: Use `CTRL`+`F` (or `Command`+`F`) to search by keywords
playlist (default) 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
--list-extractors List all supported extractors and exit --list-extractors List all supported extractors and exit
--extractor-descriptions Output descriptions of all supported --extractor-descriptions Output descriptions of all supported
extractors and exit extractors and exit
@@ -562,8 +570,6 @@ Tip: Use `CTRL`+`F` (or `Command`+`F`) to search by keywords
--playlist-random and --playlist-reverse --playlist-random and --playlist-reverse
--no-lazy-playlist Process videos in the playlist only after --no-lazy-playlist Process videos in the playlist only after
the entire playlist is parsed (default) the entire playlist is parsed (default)
--xattr-set-filesize Set file xattribute ytdl.filesize with
expected file size
--hls-use-mpegts Use the mpegts container for HLS videos; --hls-use-mpegts Use the mpegts container for HLS videos;
allowing some players to play the video allowing some players to play the video
while downloading, and reducing the chance while downloading, and reducing the chance
@@ -587,9 +593,9 @@ Tip: Use `CTRL`+`F` (or `Command`+`F`) to search by keywords
use (optionally) prefixed by the protocols use (optionally) prefixed by the protocols
(http, ftp, m3u8, dash, rstp, rtmp, mms) to (http, ftp, m3u8, dash, rstp, rtmp, mms) to
use it for. Currently supports native, use it for. Currently supports native,
aria2c, avconv, axel, curl, ffmpeg, httpie, aria2c, axel, curl, ffmpeg, httpie, wget.
wget. You can use this option multiple times You can use this option multiple times to
to set different downloaders for different set different downloaders for different
protocols. E.g. --downloader aria2c protocols. E.g. --downloader aria2c
--downloader "dash,m3u8:native" will use --downloader "dash,m3u8:native" will use
aria2c for http/ftp downloads, and the aria2c for http/ftp downloads, and the
@@ -1808,11 +1814,12 @@ The following extractors use this feature:
#### youtube #### youtube
* `lang`: Prefer translated metadata (`title`, `description` etc) of this language code (case-sensitive). By default, the video primary language metadata is preferred, with a fallback to `en` translated. See [youtube/_base.py](https://github.com/yt-dlp/yt-dlp/blob/415b4c9f955b1a0391204bd24a7132590e7b3bdb/yt_dlp/extractor/youtube/_base.py#L402-L409) for the list of supported content language codes * `lang`: Prefer translated metadata (`title`, `description` etc) of this language code (case-sensitive). By default, the video primary language metadata is preferred, with a fallback to `en` translated. See [youtube/_base.py](https://github.com/yt-dlp/yt-dlp/blob/415b4c9f955b1a0391204bd24a7132590e7b3bdb/yt_dlp/extractor/youtube/_base.py#L402-L409) for the list of supported content language codes
* `skip`: One or more of `hls`, `dash` or `translated_subs` to skip extraction of the m3u8 manifests, dash manifests and [auto-translated subtitles](https://github.com/yt-dlp/yt-dlp/issues/4090#issuecomment-1158102032) respectively * `skip`: One or more of `hls`, `dash` or `translated_subs` to skip extraction of the m3u8 manifests, dash manifests and [auto-translated subtitles](https://github.com/yt-dlp/yt-dlp/issues/4090#issuecomment-1158102032) respectively
* `player_client`: Clients to extract video data from. The currently available clients are `web`, `web_safari`, `web_embedded`, `web_music`, `web_creator`, `mweb`, `ios`, `android`, `android_vr`, `tv`, `tv_simply` and `tv_embedded`. By default, `tv_simply,tv,web` is used, but `tv,web_safari,web` is used when authenticating with cookies and `tv,web_creator,web` is used with premium accounts. The `web_music` client is added for `music.youtube.com` URLs when logged-in cookies are used. The `web_embedded` client is added for age-restricted videos but only works if the video is embeddable. The `tv_embedded` and `web_creator` clients are added for age-restricted videos if account age-verification is required. Some clients, such as `web` and `web_music`, require a `po_token` for their formats to be downloadable. Some clients, such as `web_creator`, will only work with authentication. Not all clients support authentication via cookies. You can use `default` for the default clients, or you can use `all` for all clients (not recommended). You can prefix a client with `-` to exclude it, e.g. `youtube:player_client=default,-ios` * `player_client`: Clients to extract video data from. The currently available clients are `web`, `web_safari`, `web_embedded`, `web_music`, `web_creator`, `mweb`, `ios`, `android`, `android_vr`, `tv`, `tv_simply` and `tv_embedded`. By default, `tv,web_safari,web` is used, and `tv,web_creator,web` is used with premium accounts. The `web_music` client is added for `music.youtube.com` URLs when logged-in cookies are used. The `web_embedded` client is added for age-restricted videos but only works if the video is embeddable. The `tv_embedded` and `web_creator` clients are added for age-restricted videos if account age-verification is required. Some clients, such as `web` and `web_music`, require a `po_token` for their formats to be downloadable. Some clients, such as `web_creator`, will only work with authentication. Not all clients support authentication via cookies. You can use `default` for the default clients, or you can use `all` for all clients (not recommended). You can prefix a client with `-` to exclude it, e.g. `youtube:player_client=default,-ios`
* `player_skip`: Skip some network requests that are generally needed for robust extraction. One or more of `configs` (skip client configs), `webpage` (skip initial webpage), `js` (skip js player), `initial_data` (skip initial data/next ep request). While these options can help reduce the number of requests needed or avoid some rate-limiting, they could cause issues such as missing formats or metadata. See [#860](https://github.com/yt-dlp/yt-dlp/pull/860) and [#12826](https://github.com/yt-dlp/yt-dlp/issues/12826) for more details * `player_skip`: Skip some network requests that are generally needed for robust extraction. One or more of `configs` (skip client configs), `webpage` (skip initial webpage), `js` (skip js player), `initial_data` (skip initial data/next ep request). While these options can help reduce the number of requests needed or avoid some rate-limiting, they could cause issues such as missing formats or metadata. See [#860](https://github.com/yt-dlp/yt-dlp/pull/860) and [#12826](https://github.com/yt-dlp/yt-dlp/issues/12826) for more details
* `webpage_skip`: Skip extraction of embedded webpage data. One or both of `player_response`, `initial_data`. These options are for testing purposes and don't skip any network requests * `webpage_skip`: Skip extraction of embedded webpage data. One or both of `player_response`, `initial_data`. These options are for testing purposes and don't skip any network requests
* `player_params`: YouTube player parameters to use for player requests. Will overwrite any default ones set by yt-dlp. * `player_params`: YouTube player parameters to use for player requests. Will overwrite any default ones set by yt-dlp.
* `player_js_variant`: The player javascript variant to use for signature and nsig deciphering. The known variants are: `main`, `tce`, `tv`, `tv_es6`, `phone`, `tablet`. The default is `main`, and the others are for debugging purposes. You can use `actual` to go with what is prescribed by the site * `player_js_variant`: The player javascript variant to use for n/sig deciphering. The known variants are: `main`, `tcc`, `tce`, `es5`, `es6`, `tv`, `tv_es6`, `phone`, `tablet`. The default is `main`, and the others are for debugging purposes. You can use `actual` to go with what is prescribed by the site
* `player_js_version`: The player javascript version to use for n/sig deciphering, in the format of `signature_timestamp@hash`. Currently, the default is to force `20348@0004de42`. You can use `actual` to go with what is prescribed by the site
* `comment_sort`: `top` or `new` (default) - choose comment sorting mode (on YouTube's side) * `comment_sort`: `top` or `new` (default) - choose comment sorting mode (on YouTube's side)
* `max_comments`: Limit the amount of comments to gather. Comma-separated list of integers representing `max-comments,max-parents,max-replies,max-replies-per-thread`. Default is `all,all,all,all` * `max_comments`: Limit the amount of comments to gather. Comma-separated list of integers representing `max-comments,max-parents,max-replies,max-replies-per-thread`. Default is `all,all,all,all`
* E.g. `all,all,1000,10` will get a maximum of 1000 replies total, with up to 10 replies per thread. `1000,all,100` will get a maximum of 1000 comments, with a maximum of 100 replies total * E.g. `all,all,1000,10` will get a maximum of 1000 replies total, with up to 10 replies per thread. `1000,all,100` will get a maximum of 1000 comments, with a maximum of 100 replies total
@@ -2207,7 +2214,6 @@ with yt_dlp.YoutubeDL(ydl_opts) as ydl:
* Fix for [n-sig based throttling](https://github.com/ytdl-org/youtube-dl/issues/29326) **\*** * Fix for [n-sig based throttling](https://github.com/ytdl-org/youtube-dl/issues/29326) **\***
* Download livestreams from the start using `--live-from-start` (*experimental*) * Download livestreams from the start using `--live-from-start` (*experimental*)
* Channel URLs download all uploads of the channel, including shorts and live * Channel URLs download all uploads of the channel, including shorts and live
* Support for [logging in with OAuth](https://github.com/yt-dlp/yt-dlp/wiki/Extractors#logging-in-with-oauth)
* **Cookies from browser**: Cookies can be automatically extracted from all major web browsers using `--cookies-from-browser BROWSER[+KEYRING][:PROFILE][::CONTAINER]` * **Cookies from browser**: Cookies can be automatically extracted from all major web browsers using `--cookies-from-browser BROWSER[+KEYRING][:PROFILE][::CONTAINER]`
@@ -2351,11 +2357,7 @@ While these options still work, their use is not recommended since there are oth
--hls-prefer-native --downloader "m3u8:native" --hls-prefer-native --downloader "m3u8:native"
--hls-prefer-ffmpeg --downloader "m3u8:ffmpeg" --hls-prefer-ffmpeg --downloader "m3u8:ffmpeg"
--list-formats-old --compat-options list-formats (Alias: --no-list-formats-as-table) --list-formats-old --compat-options list-formats (Alias: --no-list-formats-as-table)
--list-formats-as-table --compat-options -list-formats [Default] (Alias: --no-list-formats-old) --list-formats-as-table --compat-options -list-formats [Default]
--youtube-skip-dash-manifest --extractor-args "youtube:skip=dash" (Alias: --no-youtube-include-dash-manifest)
--youtube-skip-hls-manifest --extractor-args "youtube:skip=hls" (Alias: --no-youtube-include-hls-manifest)
--youtube-include-dash-manifest Default (Alias: --no-youtube-skip-dash-manifest)
--youtube-include-hls-manifest Default (Alias: --no-youtube-skip-hls-manifest)
--geo-bypass --xff "default" --geo-bypass --xff "default"
--no-geo-bypass --xff "never" --no-geo-bypass --xff "never"
--geo-bypass-country CODE --xff CODE --geo-bypass-country CODE --xff CODE
@@ -2366,18 +2368,13 @@ These options are not intended to be used by the end-user
--test Download only part of video for testing extractors --test Download only part of video for testing extractors
--load-pages Load pages dumped by --write-pages --load-pages Load pages dumped by --write-pages
--youtube-print-sig-code For testing youtube signatures
--allow-unplayable-formats List unplayable formats also --allow-unplayable-formats List unplayable formats also
--no-allow-unplayable-formats Default --no-allow-unplayable-formats Default
#### Old aliases #### Old aliases
These are aliases that are no longer documented for various reasons These are aliases that are no longer documented for various reasons
--avconv-location --ffmpeg-location
--clean-infojson --clean-info-json --clean-infojson --clean-info-json
--cn-verification-proxy URL --geo-verification-proxy URL
--dump-headers --print-traffic
--dump-intermediate-pages --dump-pages
--force-write-download-archive --force-write-archive --force-write-download-archive --force-write-archive
--no-clean-infojson --no-clean-info-json --no-clean-infojson --no-clean-info-json
--no-split-tracks --no-split-chapters --no-split-tracks --no-split-chapters
@@ -2391,7 +2388,7 @@ These are aliases that are no longer documented for various reasons
--yes-overwrites --force-overwrites --yes-overwrites --force-overwrites
#### Sponskrub Options #### Sponskrub Options
Support for [SponSkrub](https://github.com/faissaloo/SponSkrub) has been deprecated in favor of the `--sponsorblock` options Support for [SponSkrub](https://github.com/faissaloo/SponSkrub) has been removed in favor of the `--sponsorblock` options
--sponskrub --sponsorblock-mark all --sponskrub --sponsorblock-mark all
--no-sponskrub --no-sponsorblock --no-sponskrub --no-sponsorblock
@@ -2413,6 +2410,17 @@ These options may no longer work as intended
--no-include-ads Default --no-include-ads Default
--write-annotations No supported site has annotations now --write-annotations No supported site has annotations now
--no-write-annotations Default --no-write-annotations Default
--avconv-location Removed alias for --ffmpeg-location
--cn-verification-proxy URL Removed alias for --geo-verification-proxy URL
--dump-headers Removed alias for --print-traffic
--dump-intermediate-pages Removed alias for --dump-pages
--youtube-skip-dash-manifest Removed alias for --extractor-args "youtube:skip=dash" (Alias: --no-youtube-include-dash-manifest)
--youtube-skip-hls-manifest Removed alias for --extractor-args "youtube:skip=hls" (Alias: --no-youtube-include-hls-manifest)
--youtube-include-dash-manifest Default (Alias: --no-youtube-skip-dash-manifest)
--youtube-include-hls-manifest Default (Alias: --no-youtube-skip-hls-manifest)
--youtube-print-sig-code Removed testing functionality
--dump-user-agent No longer supported
--xattr-set-filesize No longer supported
--compat-options seperate-video-versions No longer needed --compat-options seperate-video-versions No longer needed
--compat-options no-youtube-prefer-utc-upload-date No longer supported --compat-options no-youtube-prefer-utc-upload-date No longer supported
File diff suppressed because it is too large Load Diff
+31 -6
View File
@@ -13,6 +13,9 @@ services:
CHANNEL: ${CHANNEL:?} CHANNEL: ${CHANNEL:?}
ORIGIN: ${ORIGIN:?} ORIGIN: ${ORIGIN:?}
VERSION: VERSION:
PYTHON_VERSION:
SKIP_ONEDIR_BUILD:
SKIP_ONEFILE_BUILD:
volumes: volumes:
- ../..:/yt-dlp - ../..:/yt-dlp
@@ -26,6 +29,9 @@ services:
VERIFYIMAGE: quay.io/pypa/manylinux2014_x86_64:latest VERIFYIMAGE: quay.io/pypa/manylinux2014_x86_64:latest
environment: environment:
EXE_NAME: ${EXE_NAME:?} EXE_NAME: ${EXE_NAME:?}
UPDATE_TO:
SKIP_ONEDIR_BUILD:
SKIP_ONEFILE_BUILD:
volumes: volumes:
- ../../dist:/build - ../../dist:/build
@@ -42,6 +48,9 @@ services:
CHANNEL: ${CHANNEL:?} CHANNEL: ${CHANNEL:?}
ORIGIN: ${ORIGIN:?} ORIGIN: ${ORIGIN:?}
VERSION: VERSION:
PYTHON_VERSION:
SKIP_ONEDIR_BUILD:
SKIP_ONEFILE_BUILD:
volumes: volumes:
- ../..:/yt-dlp - ../..:/yt-dlp
@@ -55,7 +64,9 @@ services:
VERIFYIMAGE: quay.io/pypa/manylinux2014_aarch64:latest VERIFYIMAGE: quay.io/pypa/manylinux2014_aarch64:latest
environment: environment:
EXE_NAME: ${EXE_NAME:?} EXE_NAME: ${EXE_NAME:?}
SKIP_UPDATE_TO: "1" # TODO: remove when there is a glibc2.17 aarch64 release to --update-to UPDATE_TO:
SKIP_ONEDIR_BUILD:
SKIP_ONEFILE_BUILD:
volumes: volumes:
- ../../dist:/build - ../../dist:/build
@@ -72,10 +83,12 @@ services:
CHANNEL: ${CHANNEL:?} CHANNEL: ${CHANNEL:?}
ORIGIN: ${ORIGIN:?} ORIGIN: ${ORIGIN:?}
VERSION: VERSION:
SKIP_ONEFILE_BUILD: "1" PYTHON_VERSION:
SKIP_ONEDIR_BUILD:
SKIP_ONEFILE_BUILD:
volumes: volumes:
- ../..:/yt-dlp - ../..:/yt-dlp
- ~/yt-dlp-build-venv:/yt-dlp-build-venv - ../../venv:/yt-dlp-build-venv
linux_armv7l_verify: linux_armv7l_verify:
build: build:
@@ -87,7 +100,9 @@ services:
VERIFYIMAGE: arm32v7/debian:bullseye VERIFYIMAGE: arm32v7/debian:bullseye
environment: environment:
EXE_NAME: ${EXE_NAME:?} EXE_NAME: ${EXE_NAME:?}
TEST_ONEDIR_BUILD: "1" UPDATE_TO:
SKIP_ONEDIR_BUILD:
SKIP_ONEFILE_BUILD:
volumes: volumes:
- ../../dist:/build - ../../dist:/build
@@ -104,6 +119,9 @@ services:
CHANNEL: ${CHANNEL:?} CHANNEL: ${CHANNEL:?}
ORIGIN: ${ORIGIN:?} ORIGIN: ${ORIGIN:?}
VERSION: VERSION:
PYTHON_VERSION:
SKIP_ONEDIR_BUILD:
SKIP_ONEFILE_BUILD:
volumes: volumes:
- ../..:/yt-dlp - ../..:/yt-dlp
@@ -117,7 +135,9 @@ services:
VERIFYIMAGE: alpine:3.22 VERIFYIMAGE: alpine:3.22
environment: environment:
EXE_NAME: ${EXE_NAME:?} EXE_NAME: ${EXE_NAME:?}
SKIP_UPDATE_TO: "1" # TODO: remove when there is a musllinux_aarch64 release to --update-to UPDATE_TO:
SKIP_ONEDIR_BUILD:
SKIP_ONEFILE_BUILD:
volumes: volumes:
- ../../dist:/build - ../../dist:/build
@@ -134,6 +154,9 @@ services:
CHANNEL: ${CHANNEL:?} CHANNEL: ${CHANNEL:?}
ORIGIN: ${ORIGIN:?} ORIGIN: ${ORIGIN:?}
VERSION: VERSION:
PYTHON_VERSION:
SKIP_ONEDIR_BUILD:
SKIP_ONEFILE_BUILD:
EXCLUDE_CURL_CFFI: "1" EXCLUDE_CURL_CFFI: "1"
volumes: volumes:
- ../..:/yt-dlp - ../..:/yt-dlp
@@ -148,6 +171,8 @@ services:
VERIFYIMAGE: alpine:3.22 VERIFYIMAGE: alpine:3.22
environment: environment:
EXE_NAME: ${EXE_NAME:?} EXE_NAME: ${EXE_NAME:?}
SKIP_UPDATE_TO: "1" # TODO: remove when there is a musllinux_aarch64 release to --update-to UPDATE_TO:
SKIP_ONEDIR_BUILD:
SKIP_ONEFILE_BUILD:
volumes: volumes:
- ../../dist:/build - ../../dist:/build
+6 -4
View File
@@ -1,16 +1,17 @@
#!/bin/bash #!/bin/bash
set -exuo pipefail set -exuo pipefail
if [[ -z "${USE_PYTHON_VERSION:-}" ]]; then if [[ -z "${PYTHON_VERSION:-}" ]]; then
USE_PYTHON_VERSION="3.13" PYTHON_VERSION="3.13"
echo "Defaulting to using Python ${PYTHON_VERSION}"
fi fi
function runpy { function runpy {
"/opt/shared-cpython-${USE_PYTHON_VERSION}/bin/python${USE_PYTHON_VERSION}" "$@" "/opt/shared-cpython-${PYTHON_VERSION}/bin/python${PYTHON_VERSION}" "$@"
} }
function venvpy { function venvpy {
"python${USE_PYTHON_VERSION}" "$@" "python${PYTHON_VERSION}" "$@"
} }
INCLUDES=( INCLUDES=(
@@ -23,6 +24,7 @@ if [[ -z "${EXCLUDE_CURL_CFFI:-}" ]]; then
fi fi
runpy -m venv /yt-dlp-build-venv runpy -m venv /yt-dlp-build-venv
# shellcheck disable=SC1091
source /yt-dlp-build-venv/bin/activate source /yt-dlp-build-venv/bin/activate
# Inside the venv we use venvpy instead of runpy # Inside the venv we use venvpy instead of runpy
venvpy -m ensurepip --upgrade --default-pip venvpy -m ensurepip --upgrade --default-pip
+13 -6
View File
@@ -1,7 +1,11 @@
#!/bin/sh #!/bin/sh
set -eu set -eu
if [ -n "${TEST_ONEDIR_BUILD:-}" ]; then if [ -n "${SKIP_ONEFILE_BUILD:-}" ]; then
if [ -n "${SKIP_ONEDIR_BUILD:-}" ]; then
echo "All executable builds were skipped"
exit 1
fi
echo "Extracting zip to verify onedir build" echo "Extracting zip to verify onedir build"
if command -v python3 >/dev/null 2>&1; then if command -v python3 >/dev/null 2>&1; then
python3 -m zipfile -e "/build/${EXE_NAME}.zip" ./ python3 -m zipfile -e "/build/${EXE_NAME}.zip" ./
@@ -22,14 +26,17 @@ if [ -n "${TEST_ONEDIR_BUILD:-}" ]; then
fi fi
unzip "/build/${EXE_NAME}.zip" -d ./ unzip "/build/${EXE_NAME}.zip" -d ./
fi fi
else chmod +x "./${EXE_NAME}"
echo "Verifying onefile build" "./${EXE_NAME}" -v || true
cp "/build/${EXE_NAME}" ./ "./${EXE_NAME}" --version
exit 0
fi fi
echo "Verifying onefile build"
cp "/build/${EXE_NAME}" ./
chmod +x "./${EXE_NAME}" chmod +x "./${EXE_NAME}"
if [ -n "${SKIP_UPDATE_TO:-}" ] || [ -n "${TEST_ONEDIR_BUILD:-}" ]; then if [ -z "${UPDATE_TO:-}" ]; then
"./${EXE_NAME}" -v || true "./${EXE_NAME}" -v || true
"./${EXE_NAME}" --version "./${EXE_NAME}" --version
exit 0 exit 0
@@ -37,7 +44,7 @@ fi
cp "./${EXE_NAME}" "./${EXE_NAME}_downgraded" cp "./${EXE_NAME}" "./${EXE_NAME}_downgraded"
version="$("./${EXE_NAME}" --version)" version="$("./${EXE_NAME}" --version)"
"./${EXE_NAME}_downgraded" -v --update-to yt-dlp/yt-dlp@2023.03.04 "./${EXE_NAME}_downgraded" -v --update-to "${UPDATE_TO}"
downgraded_version="$("./${EXE_NAME}_downgraded" --version)" downgraded_version="$("./${EXE_NAME}_downgraded" --version)"
if [ "${version}" = "${downgraded_version}" ]; then if [ "${version}" = "${downgraded_version}" ]; then
exit 1 exit 1
-1
View File
@@ -129,7 +129,6 @@ def windows_set_version(exe, version):
StringStruct('FileDescription', 'yt-dlp%s' % (MACHINE and f' ({MACHINE})')), StringStruct('FileDescription', 'yt-dlp%s' % (MACHINE and f' ({MACHINE})')),
StringStruct('FileVersion', version), StringStruct('FileVersion', version),
StringStruct('InternalName', f'yt-dlp{suffix}'), StringStruct('InternalName', f'yt-dlp{suffix}'),
StringStruct('LegalCopyright', 'pukkandan.ytdlp@gmail.com | UNLICENSE'),
StringStruct('OriginalFilename', f'yt-dlp{suffix}.exe'), StringStruct('OriginalFilename', f'yt-dlp{suffix}.exe'),
StringStruct('ProductName', f'yt-dlp{suffix}'), StringStruct('ProductName', f'yt-dlp{suffix}'),
StringStruct( StringStruct(
+5
View File
@@ -293,5 +293,10 @@
"action": "add", "action": "add",
"when": "c76ce28e06c816eb5b261dfb6aff6e69dd9b7382", "when": "c76ce28e06c816eb5b261dfb6aff6e69dd9b7382",
"short": "[priority] **linux_armv7l_exe builds are being discontinued**\nThis release's `yt-dlp_linux_armv7l` binary could be the last one. [Read more](https://github.com/yt-dlp/yt-dlp/issues/13976)" "short": "[priority] **linux_armv7l_exe builds are being discontinued**\nThis release's `yt-dlp_linux_armv7l` binary could be the last one. [Read more](https://github.com/yt-dlp/yt-dlp/issues/13976)"
},
{
"action": "add",
"when": "08d78996831bd8e1e3c2592d740c3def00bbf548",
"short": "[priority] **Several options have been deprecated**\nIn order to simplify the codebase and reduce maintenance burden, various options have been deprecated. Please remove them from your commands/configurations. [Read more](https://github.com/yt-dlp/yt-dlp/issues/14198)"
} }
] ]
+316
View File
@@ -0,0 +1,316 @@
import requests
from dataclasses import dataclass
from pathlib import Path
import hashlib
DEFAULT_OUTPUT = 'THIRD_PARTY_LICENSES.txt'
CACHE_LOCATION = '.license_cache'
HEADER = '''THIRD-PARTY LICENSES
This file aggregates license texts of third-party components included with the yt-dlp PyInstaller-bundled executables.
yt-dlp itself is licensed under the Unlicense (see LICENSE file).
Source code for bundled third-party components is available from the original projects.
If you cannot obtain it, the maintainers will provide it as per license obligation; maintainer emails are listed in pyproject.toml.'''
@dataclass(frozen=True)
class Dependency:
name: str
license_url: str
project_url: str = ''
license: str = ''
comment: str = ''
DEPENDENCIES: list[Dependency] = [
# Core runtime environment components
Dependency(
name='Python',
license='PSF-2.0',
license_url='https://raw.githubusercontent.com/python/cpython/refs/heads/main/LICENSE',
project_url='https://www.python.org/',
),
Dependency(
name='Microsoft Distributable Code',
license_url='https://raw.githubusercontent.com/python/cpython/refs/heads/main/PC/crtlicense.txt',
comment='Only included in Windows builds',
),
Dependency(
name='bzip2',
license='bzip2-1.0.6',
license_url='https://gitlab.com/federicomenaquintero/bzip2/-/raw/master/COPYING',
project_url='https://sourceware.org/bzip2/',
),
Dependency(
name='libffi',
license='MIT',
license_url='https://raw.githubusercontent.com/libffi/libffi/refs/heads/master/LICENSE',
project_url='https://sourceware.org/libffi/',
),
Dependency(
name='OpenSSL 3.0+',
license='Apache-2.0',
license_url='https://raw.githubusercontent.com/openssl/openssl/refs/heads/master/LICENSE.txt',
project_url='https://www.openssl.org/',
),
Dependency(
name='SQLite',
license='Public Domain', # Technically does not need to be included
license_url='https://sqlite.org/src/raw/e108e1e69ae8e8a59e93c455654b8ac9356a11720d3345df2a4743e9590fb20d?at=LICENSE.md',
project_url='https://www.sqlite.org/',
),
Dependency(
name='liblzma',
license='0BSD', # Technically does not need to be included
license_url='https://raw.githubusercontent.com/tukaani-project/xz/refs/heads/master/COPYING',
project_url='https://tukaani.org/xz/',
),
Dependency(
name='mpdecimal',
license='BSD-2-Clause',
# No official repo URL
license_url='https://gist.githubusercontent.com/seproDev/9e5dbfc08af35c3f2463e64eb9b27161/raw/61f5a98bc1a4ad7d48b1c793fc3314d4d43c2ab1/mpdecimal_COPYRIGHT.txt',
project_url='https://www.bytereef.org/mpdecimal/',
),
Dependency(
name='zlib',
license='zlib',
license_url='https://raw.githubusercontent.com/madler/zlib/refs/heads/develop/LICENSE',
project_url='https://zlib.net/',
),
Dependency(
name='Expat',
license='MIT',
license_url='https://raw.githubusercontent.com/libexpat/libexpat/refs/heads/master/COPYING',
project_url='https://libexpat.github.io/',
),
Dependency(
name='ncurses',
license='X11-distribute-modifications-variant',
license_url='https://raw.githubusercontent.com/mirror/ncurses/refs/heads/master/COPYING',
comment='Only included in Linux/macOS builds',
project_url='https://invisible-island.net/ncurses/',
),
Dependency(
name='GNU Readline',
license='GPL-3.0-or-later',
license_url='https://tiswww.case.edu/php/chet/readline/COPYING',
comment='Only included in Linux builds',
project_url='https://www.gnu.org/software/readline/',
),
Dependency(
name='libstdc++',
license='GPL-3.0-with-GCC-exception',
license_url='https://raw.githubusercontent.com/gcc-mirror/gcc/refs/heads/master/COPYING.RUNTIME',
comment='Only included in Linux builds',
project_url='https://gcc.gnu.org/onlinedocs/libstdc++/',
),
Dependency(
name='libgcc',
license='GPL-3.0-with-GCC-exception',
license_url='https://raw.githubusercontent.com/gcc-mirror/gcc/refs/heads/master/COPYING.RUNTIME',
comment='Only included in Linux builds',
project_url='https://gcc.gnu.org/',
),
Dependency(
name='libuuid',
license='BSD-3-Clause',
license_url='https://git.kernel.org/pub/scm/fs/ext2/e2fsprogs.git/plain/lib/uuid/COPYING',
comment='Only included in Linux builds',
project_url='https://git.kernel.org/pub/scm/fs/ext2/e2fsprogs.git/tree/lib/uuid',
),
Dependency(
name='libintl',
license='LGPL-2.1-or-later',
license_url='https://raw.githubusercontent.com/autotools-mirror/gettext/refs/heads/master/gettext-runtime/intl/COPYING.LIB',
comment='Only included in macOS builds',
project_url='https://www.gnu.org/software/gettext/',
),
Dependency(
name='libidn2',
license='LGPL-3.0-or-later',
license_url='https://gitlab.com/libidn/libidn2/-/raw/master/COPYING.LESSERv3',
comment='Only included in macOS builds',
project_url='https://www.gnu.org/software/libidn/',
),
Dependency(
name='libidn2 (Unicode character data files)',
license='Unicode-TOU AND Unicode-DFS-2016',
license_url='https://gitlab.com/libidn/libidn2/-/raw/master/COPYING.unicode',
comment='Only included in macOS builds',
project_url='https://www.gnu.org/software/libidn/',
),
Dependency(
name='libunistring',
license='LGPL-3.0-or-later',
license_url='https://gitweb.git.savannah.gnu.org/gitweb/?p=libunistring.git;a=blob_plain;f=COPYING.LIB;hb=HEAD',
comment='Only included in macOS builds',
project_url='https://www.gnu.org/software/libunistring/',
),
Dependency(
name='librtmp',
license='LGPL-2.1-or-later',
# No official repo URL
license_url='https://gist.githubusercontent.com/seproDev/31d8c691ccddebe37b8b379307cb232d/raw/053408e98547ea8c7d9ba3a80c965f33e163b881/librtmp_COPYING.txt',
comment='Only included in macOS builds',
project_url='https://rtmpdump.mplayerhq.hu/',
),
Dependency(
name='zstd',
license='BSD-3-Clause',
license_url='https://raw.githubusercontent.com/facebook/zstd/refs/heads/dev/LICENSE',
comment='Only included in macOS builds',
project_url='https://facebook.github.io/zstd/',
),
# Python packages
Dependency(
name='brotli',
license='MIT',
license_url='https://raw.githubusercontent.com/google/brotli/refs/heads/master/LICENSE',
project_url='https://brotli.org/',
),
Dependency(
name='curl_cffi',
license='MIT',
license_url='https://raw.githubusercontent.com/lexiforest/curl_cffi/refs/heads/main/LICENSE',
comment='Not included in `yt-dlp_x86` and `yt-dlp_musllinux_aarch64` builds',
project_url='https://curl-cffi.readthedocs.io/',
),
# Dependency of curl_cffi
Dependency(
name='curl-impersonate',
license='MIT',
license_url='https://raw.githubusercontent.com/lexiforest/curl-impersonate/refs/heads/main/LICENSE',
comment='Not included in `yt-dlp_x86` and `yt-dlp_musllinux_aarch64` builds',
project_url='https://github.com/lexiforest/curl-impersonate',
),
Dependency(
name='cffi',
license='MIT-0', # Technically does not need to be included
license_url='https://raw.githubusercontent.com/python-cffi/cffi/refs/heads/main/LICENSE',
project_url='https://cffi.readthedocs.io/',
),
# Dependecy of cffi
Dependency(
name='pycparser',
license='BSD-3-Clause',
license_url='https://raw.githubusercontent.com/eliben/pycparser/refs/heads/main/LICENSE',
project_url='https://github.com/eliben/pycparser',
),
Dependency(
name='mutagen',
license='GPL-2.0-or-later',
license_url='https://raw.githubusercontent.com/quodlibet/mutagen/refs/heads/main/COPYING',
project_url='https://mutagen.readthedocs.io/',
),
Dependency(
name='PyCryptodome',
license='Public Domain and BSD-2-Clause',
license_url='https://raw.githubusercontent.com/Legrandin/pycryptodome/refs/heads/master/LICENSE.rst',
project_url='https://www.pycryptodome.org/',
),
Dependency(
name='certifi',
license='MPL-2.0',
license_url='https://raw.githubusercontent.com/certifi/python-certifi/refs/heads/master/LICENSE',
project_url='https://github.com/certifi/python-certifi',
),
Dependency(
name='requests',
license='Apache-2.0',
license_url='https://raw.githubusercontent.com/psf/requests/refs/heads/main/LICENSE',
project_url='https://requests.readthedocs.io/',
),
# Dependency of requests
Dependency(
name='charset-normalizer',
license='MIT',
license_url='https://raw.githubusercontent.com/jawah/charset_normalizer/refs/heads/master/LICENSE',
project_url='https://charset-normalizer.readthedocs.io/',
),
# Dependency of requests
Dependency(
name='idna',
license='BSD-3-Clause',
license_url='https://raw.githubusercontent.com/kjd/idna/refs/heads/master/LICENSE.md',
project_url='https://github.com/kjd/idna',
),
Dependency(
name='urllib3',
license='MIT',
license_url='https://raw.githubusercontent.com/urllib3/urllib3/refs/heads/main/LICENSE.txt',
project_url='https://urllib3.readthedocs.io/',
),
Dependency(
name='SecretStorage',
license='BSD-3-Clause',
license_url='https://raw.githubusercontent.com/mitya57/secretstorage/refs/heads/master/LICENSE',
comment='Only included in Linux builds',
project_url='https://secretstorage.readthedocs.io/',
),
# Dependency of SecretStorage
Dependency(
name='cryptography',
license='Apache-2.0', # Also available as BSD-3-Clause
license_url='https://raw.githubusercontent.com/pyca/cryptography/refs/heads/main/LICENSE.APACHE',
comment='Only included in Linux builds',
project_url='https://cryptography.io/',
),
# Dependency of SecretStorage
Dependency(
name='Jeepney',
license='MIT',
license_url='https://gitlab.com/takluyver/jeepney/-/raw/master/LICENSE',
comment='Only included in Linux builds',
project_url='https://jeepney.readthedocs.io/',
),
Dependency(
name='websockets',
license='BSD-3-Clause',
license_url='https://raw.githubusercontent.com/python-websockets/websockets/refs/heads/main/LICENSE',
project_url='https://websockets.readthedocs.io/',
),
]
def fetch_text(dep: Dependency) -> str:
cache_dir = Path(CACHE_LOCATION)
cache_dir.mkdir(exist_ok=True)
url_hash = hashlib.sha256(dep.license_url.encode('utf-8')).hexdigest()
cache_file = cache_dir / f'{url_hash}.txt'
if cache_file.exists():
return cache_file.read_text()
# UA needed since some domains block requests default UA
req = requests.get(dep.license_url, headers={'User-Agent': 'yt-dlp license fetcher'})
req.raise_for_status()
text = req.text
cache_file.write_text(text)
return text
def build_output() -> str:
lines = [HEADER]
for d in DEPENDENCIES:
lines.append('\n')
lines.append('-' * 80)
header = f'{d.name}'
if d.license:
header += f' | {d.license}'
if d.comment:
header += f'\nNote: {d.comment}'
if d.project_url:
header += f'\nURL: {d.project_url}'
lines.append(header)
lines.append('-' * 80)
text = fetch_text(d)
lines.append(text.strip('\n') + '\n')
return '\n'.join(lines)
if __name__ == '__main__':
content = build_output()
Path(DEFAULT_OUTPUT).write_text(content)
+1 -1
View File
@@ -8,7 +8,7 @@ def main():
return # This is unused in yt-dlp 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() _, 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')
+1 -8
View File
@@ -1,4 +1,3 @@
# Allow direct execution
import os import os
import sys import sys
@@ -55,7 +54,7 @@ def _test(github_repository, note, repo_vars, repo_secrets, inputs, expected=Non
assert result == exp, f'unexpected result: {github_repository} {note}' assert result == exp, f'unexpected result: {github_repository} {note}'
def main(): def test_setup_variables():
DEFAULT_VERSION_WITH_REVISION = dt.datetime.now(tz=dt.timezone.utc).strftime('%Y.%m.%d.%H%M%S') DEFAULT_VERSION_WITH_REVISION = dt.datetime.now(tz=dt.timezone.utc).strftime('%Y.%m.%d.%H%M%S')
DEFAULT_VERSION = calculate_version() DEFAULT_VERSION = calculate_version()
BASE_REPO_VARS = { BASE_REPO_VARS = {
@@ -323,9 +322,3 @@ def main():
'pypi_project': None, 'pypi_project': None,
'pypi_suffix': None, 'pypi_suffix': None,
}, ignore_revision=True) }, ignore_revision=True)
print('all tests passed')
if __name__ == '__main__':
main()
+8 -7
View File
@@ -1,5 +1,5 @@
[build-system] [build-system]
requires = ["hatchling"] requires = ["hatchling>=1.27.0"]
build-backend = "hatchling.build" build-backend = "hatchling.build"
[project] [project]
@@ -22,7 +22,8 @@ keywords = [
"sponsorblock", "sponsorblock",
"yt-dlp", "yt-dlp",
] ]
license = {file = "LICENSE"} license = "Unlicense"
license-files = ["LICENSE"]
classifiers = [ classifiers = [
"Topic :: Multimedia :: Video", "Topic :: Multimedia :: Video",
"Development Status :: 5 - Production/Stable", "Development Status :: 5 - Production/Stable",
@@ -34,10 +35,10 @@ classifiers = [
"Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.13",
"Programming Language :: Python :: 3.14",
"Programming Language :: Python :: Implementation", "Programming Language :: Python :: Implementation",
"Programming Language :: Python :: Implementation :: CPython", "Programming Language :: Python :: Implementation :: CPython",
"Programming Language :: Python :: Implementation :: PyPy", "Programming Language :: Python :: Implementation :: PyPy",
"License :: OSI Approved :: The Unlicense (Unlicense)",
"Operating System :: OS Independent", "Operating System :: OS Independent",
] ]
dynamic = ["version"] dynamic = ["version"]
@@ -63,7 +64,7 @@ secretstorage = [
] ]
build = [ build = [
"build", "build",
"hatchling", "hatchling>=1.27.0",
"pip", "pip",
"setuptools>=71.0.2,<81", # See https://github.com/pyinstaller/pyinstaller/issues/9149 "setuptools>=71.0.2,<81", # See https://github.com/pyinstaller/pyinstaller/issues/9149
"wheel", "wheel",
@@ -75,7 +76,7 @@ dev = [
] ]
static-analysis = [ static-analysis = [
"autopep8~=2.0", "autopep8~=2.0",
"ruff~=0.12.0", "ruff~=0.13.0",
] ]
test = [ test = [
"pytest~=8.1", "pytest~=8.1",
@@ -107,7 +108,6 @@ include = [
"/LICENSE", # included as license "/LICENSE", # included as license
"/pyproject.toml", # included by default "/pyproject.toml", # included by default
"/README.md", # included as readme "/README.md", # included as readme
"/setup.cfg",
"/supportedsites.md", "/supportedsites.md",
] ]
artifacts = [ artifacts = [
@@ -173,7 +173,8 @@ python = [
"3.11", "3.11",
"3.12", "3.12",
"3.13", "3.13",
"pypy3.10", "3.14",
"pypy3.11",
] ]
[tool.ruff] [tool.ruff]
-39
View File
@@ -1,39 +0,0 @@
[flake8]
exclude = build,venv,.tox,.git,.pytest_cache
ignore = E402,E501,E731,E741,W503
max_line_length = 120
per_file_ignores =
devscripts/lazy_load_template.py: F401
[autoflake]
ignore-init-module-imports = true
ignore-pass-after-docstring = true
remove-all-unused-imports = true
remove-duplicate-keys = true
remove-unused-variables = true
[tox:tox]
skipsdist = true
envlist = py{39,310,311,312,313},pypy311
skip_missing_interpreters = true
[testenv] # tox
deps =
pytest
commands = pytest {posargs:"-m not download"}
passenv = HOME # For test_compat_expanduser
setenv =
# PYTHONWARNINGS = error # Catches PIP's warnings too
[isort]
py_version = 39
multi_line_output = VERTICAL_HANGING_INDENT
line_length = 80
reverse_relative = true
ensure_newline_before_comments = true
include_trailing_comma = true
known_first_party =
test
+16 -19
View File
@@ -20,7 +20,6 @@ The only reliable way to check if a site is supported is to try it.
- **3sat** - **3sat**
- **4tube** - **4tube**
- **56.com** - **56.com**
- **6play**
- **7plus** - **7plus**
- **8tracks** - **8tracks**
- **9c9media** - **9c9media**
@@ -243,6 +242,7 @@ The only reliable way to check if a site is supported is to try it.
- **Canalsurmas** - **Canalsurmas**
- **CaracolTvPlay**: [*caracoltv-play*](## "netrc machine") - **CaracolTvPlay**: [*caracoltv-play*](## "netrc machine")
- **cbc.ca** - **cbc.ca**
- **cbc.ca:listen**
- **cbc.ca:player** - **cbc.ca:player**
- **cbc.ca:player:playlist** - **cbc.ca:player:playlist**
- **CBS**: (**Currently broken**) - **CBS**: (**Currently broken**)
@@ -299,7 +299,6 @@ The only reliable way to check if a site is supported is to try it.
- **cpac** - **cpac**
- **cpac:playlist** - **cpac:playlist**
- **Cracked** - **Cracked**
- **Crackle**
- **Craftsy** - **Craftsy**
- **CrooksAndLiars** - **CrooksAndLiars**
- **CrowdBunker** - **CrowdBunker**
@@ -314,8 +313,6 @@ The only reliable way to check if a site is supported is to try it.
- **curiositystream**: [*curiositystream*](## "netrc machine") - **curiositystream**: [*curiositystream*](## "netrc machine")
- **curiositystream:collections**: [*curiositystream*](## "netrc machine") - **curiositystream:collections**: [*curiositystream*](## "netrc machine")
- **curiositystream:series**: [*curiositystream*](## "netrc machine") - **curiositystream:series**: [*curiositystream*](## "netrc machine")
- **cwtv**
- **cwtv:movie**
- **Cybrary**: [*cybrary*](## "netrc machine") - **Cybrary**: [*cybrary*](## "netrc machine")
- **CybraryCourse**: [*cybrary*](## "netrc machine") - **CybraryCourse**: [*cybrary*](## "netrc machine")
- **DacastPlaylist** - **DacastPlaylist**
@@ -450,7 +447,6 @@ The only reliable way to check if a site is supported is to try it.
- **Filmweb** - **Filmweb**
- **FiveThirtyEight** - **FiveThirtyEight**
- **FiveTV** - **FiveTV**
- **FlexTV**
- **Flickr** - **Flickr**
- **Floatplane** - **Floatplane**
- **FloatplaneChannel** - **FloatplaneChannel**
@@ -584,6 +580,11 @@ The only reliable way to check if a site is supported is to try it.
- **Hypem** - **Hypem**
- **Hytale** - **Hytale**
- **Icareus** - **Icareus**
- **IdagioAlbum**
- **IdagioPersonalPlaylist**
- **IdagioPlaylist**
- **IdagioRecording**
- **IdagioTrack**
- **IdolPlus** - **IdolPlus**
- **iflix:episode** - **iflix:episode**
- **IflixSeries** - **IflixSeries**
@@ -798,7 +799,6 @@ The only reliable way to check if a site is supported is to try it.
- **mirrativ** - **mirrativ**
- **mirrativ:user** - **mirrativ:user**
- **MirrorCoUK** - **MirrorCoUK**
- **MiTele**: mitele.es
- **mixch** - **mixch**
- **mixch:archive** - **mixch:archive**
- **mixch:movie** - **mixch:movie**
@@ -1009,6 +1009,7 @@ The only reliable way to check if a site is supported is to try it.
- **onet.tv:channel** - **onet.tv:channel**
- **OnetMVP** - **OnetMVP**
- **OnionStudios** - **OnionStudios**
- **onsen**: [*onsen*](## "netrc machine") インターネットラジオステーション<音泉>
- **Opencast** - **Opencast**
- **OpencastPlaylist** - **OpencastPlaylist**
- **openrec** - **openrec**
@@ -1033,8 +1034,6 @@ The only reliable way to check if a site is supported is to try it.
- **Panopto** - **Panopto**
- **PanoptoList** - **PanoptoList**
- **PanoptoPlaylist** - **PanoptoPlaylist**
- **ParamountPlus**
- **ParamountPlusSeries**
- **ParamountPressExpress** - **ParamountPressExpress**
- **Parler**: Posts on parler.com - **Parler**: Posts on parler.com
- **parliamentlive.tv**: UK parliament videos - **parliamentlive.tv**: UK parliament videos
@@ -1069,8 +1068,6 @@ The only reliable way to check if a site is supported is to try it.
- **PinterestCollection** - **PinterestCollection**
- **PiramideTV** - **PiramideTV**
- **PiramideTVChannel** - **PiramideTVChannel**
- **pixiv:sketch**
- **pixiv:sketch:user**
- **PlanetMarathi** - **PlanetMarathi**
- **Platzi**: [*platzi*](## "netrc machine") - **Platzi**: [*platzi*](## "netrc machine")
- **PlatziCourse**: [*platzi*](## "netrc machine") - **PlatziCourse**: [*platzi*](## "netrc machine")
@@ -1257,7 +1254,6 @@ The only reliable way to check if a site is supported is to try it.
- **rutube:person**: Rutube person videos - **rutube:person**: Rutube person videos
- **rutube:playlist**: Rutube playlists - **rutube:playlist**: Rutube playlists
- **rutube:tags**: Rutube tags - **rutube:tags**: Rutube tags
- **RUTV**: RUTV.RU
- **Ruutu**: (**Currently broken**) - **Ruutu**: (**Currently broken**)
- **Ruv** - **Ruv**
- **ruv.is:spila** - **ruv.is:spila**
@@ -1332,7 +1328,10 @@ The only reliable way to check if a site is supported is to try it.
- **Slideshare** - **Slideshare**
- **SlidesLive** - **SlidesLive**
- **Slutload** - **Slutload**
- **Smotrim** - **smotrim**
- **smotrim:audio**
- **smotrim:live**
- **smotrim:playlist**
- **SnapchatSpotlight** - **SnapchatSpotlight**
- **Snotr** - **Snotr**
- **SoftWhiteUnderbelly**: [*softwhiteunderbelly*](## "netrc machine") - **SoftWhiteUnderbelly**: [*softwhiteunderbelly*](## "netrc machine")
@@ -1370,8 +1369,6 @@ The only reliable way to check if a site is supported is to try it.
- **Sport5** - **Sport5**
- **SportBox**: (**Currently broken**) - **SportBox**: (**Currently broken**)
- **SportDeutschland** - **SportDeutschland**
- **spotify**: Spotify episodes (**Currently broken**)
- **spotify:show**: Spotify shows (**Currently broken**)
- **Spreaker** - **Spreaker**
- **SpreakerShow** - **SpreakerShow**
- **SpringboardPlatform** - **SpringboardPlatform**
@@ -1510,15 +1507,17 @@ The only reliable way to check if a site is supported is to try it.
- **TrueID** - **TrueID**
- **TruNews** - **TruNews**
- **Truth** - **Truth**
- **ttinglive**: 띵라이브 (formerly FlexTV)
- **Tube8**: (**Currently broken**) - **Tube8**: (**Currently broken**)
- **TubeTuGraz**: [*tubetugraz*](## "netrc machine") tube.tugraz.at - **TubeTuGraz**: [*tubetugraz*](## "netrc machine") tube.tugraz.at
- **TubeTuGrazSeries**: [*tubetugraz*](## "netrc machine") - **TubeTuGrazSeries**: [*tubetugraz*](## "netrc machine")
- **tubitv**: [*tubitv*](## "netrc machine") - **tubitv**: [*tubitv*](## "netrc machine")
- **tubitv:series** - **tubitv:series**
- **Tumblr**: [*tumblr*](## "netrc machine") - **Tumblr**: [*tumblr*](## "netrc machine")
- **TuneInPodcast** - **tunein:embed**
- **TuneInPodcastEpisode** - **tunein:podcast**
- **TuneInStation** - **tunein:podcast:program**
- **tunein:station**
- **tv.dfb.de** - **tv.dfb.de**
- **TV2** - **TV2**
- **TV2Article** - **TV2Article**
@@ -1600,7 +1599,6 @@ The only reliable way to check if a site is supported is to try it.
- **Varzesh3**: (**Currently broken**) - **Varzesh3**: (**Currently broken**)
- **Vbox7** - **Vbox7**
- **Veo** - **Veo**
- **Vesti**: Вести.Ru (**Currently broken**)
- **Vevo** - **Vevo**
- **VevoPlaylist** - **VevoPlaylist**
- **VGTV**: VGTV, BTTV, FTV, Aftenposten and Aftonbladet - **VGTV**: VGTV, BTTV, FTV, Aftenposten and Aftonbladet
@@ -1748,7 +1746,6 @@ The only reliable way to check if a site is supported is to try it.
- **wykop:dig:comment** - **wykop:dig:comment**
- **wykop:post** - **wykop:post**
- **wykop:post:comment** - **wykop:post:comment**
- **Xanimu**
- **XboxClips** - **XboxClips**
- **XHamster** - **XHamster**
- **XHamsterEmbed** - **XHamsterEmbed**
-1
View File
@@ -36,7 +36,6 @@
"verbose": true, "verbose": true,
"writedescription": false, "writedescription": false,
"writeinfojson": true, "writeinfojson": true,
"writeannotations": false,
"writelink": false, "writelink": false,
"writeurllink": false, "writeurllink": false,
"writewebloclink": false, "writewebloclink": false,
+1 -1
View File
@@ -1945,7 +1945,7 @@ jwplayer("mediaplayer").setup({"abouttext":"Visit Indie DB","aboutlink":"http:\/
server_thread.daemon = True server_thread.daemon = True
server_thread.start() server_thread.start()
(content, urlh) = self.ie._download_webpage_handle( content, _ = self.ie._download_webpage_handle(
f'http://127.0.0.1:{port}/teapot', None, f'http://127.0.0.1:{port}/teapot', None,
expected_status=TEAPOT_RESPONSE_STATUS) expected_status=TEAPOT_RESPONSE_STATUS)
self.assertEqual(content, TEAPOT_RESPONSE_BODY) self.assertEqual(content, TEAPOT_RESPONSE_BODY)
+41 -1
View File
@@ -1,6 +1,7 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
# Allow direct execution # Allow direct execution
import datetime as dt
import os import os
import sys import sys
import unittest import unittest
@@ -12,7 +13,7 @@ import struct
from yt_dlp import compat from yt_dlp import compat
from yt_dlp.compat import urllib # isort: split from yt_dlp.compat import urllib # isort: split
from yt_dlp.compat import compat_etree_fromstring, compat_expanduser from yt_dlp.compat import compat_etree_fromstring, compat_expanduser, compat_datetime_from_timestamp
from yt_dlp.compat.urllib.request import getproxies from yt_dlp.compat.urllib.request import getproxies
@@ -59,6 +60,45 @@ class TestCompat(unittest.TestCase):
def test_struct_unpack(self): def test_struct_unpack(self):
self.assertEqual(struct.unpack('!B', b'\x00'), (0,)) self.assertEqual(struct.unpack('!B', b'\x00'), (0,))
def test_compat_datetime_from_timestamp(self):
self.assertEqual(
compat_datetime_from_timestamp(0),
dt.datetime(1970, 1, 1, 0, 0, 0, tzinfo=dt.timezone.utc))
self.assertEqual(
compat_datetime_from_timestamp(1),
dt.datetime(1970, 1, 1, 0, 0, 1, tzinfo=dt.timezone.utc))
self.assertEqual(
compat_datetime_from_timestamp(3600),
dt.datetime(1970, 1, 1, 1, 0, 0, tzinfo=dt.timezone.utc))
self.assertEqual(
compat_datetime_from_timestamp(-1),
dt.datetime(1969, 12, 31, 23, 59, 59, tzinfo=dt.timezone.utc))
self.assertEqual(
compat_datetime_from_timestamp(-86400),
dt.datetime(1969, 12, 31, 0, 0, 0, tzinfo=dt.timezone.utc))
self.assertEqual(
compat_datetime_from_timestamp(0.5),
dt.datetime(1970, 1, 1, 0, 0, 0, 500000, tzinfo=dt.timezone.utc))
self.assertEqual(
compat_datetime_from_timestamp(1.000001),
dt.datetime(1970, 1, 1, 0, 0, 1, 1, tzinfo=dt.timezone.utc))
self.assertEqual(
compat_datetime_from_timestamp(-1.25),
dt.datetime(1969, 12, 31, 23, 59, 58, 750000, tzinfo=dt.timezone.utc))
self.assertEqual(
compat_datetime_from_timestamp(-1577923200),
dt.datetime(1920, 1, 1, 0, 0, 0, tzinfo=dt.timezone.utc))
self.assertEqual(
compat_datetime_from_timestamp(4102444800),
dt.datetime(2100, 1, 1, 0, 0, 0, tzinfo=dt.timezone.utc))
self.assertEqual(
compat_datetime_from_timestamp(173568960000),
dt.datetime(7470, 3, 8, 0, 0, 0, tzinfo=dt.timezone.utc))
if __name__ == '__main__': if __name__ == '__main__':
unittest.main() unittest.main()
+2 -2
View File
@@ -29,7 +29,7 @@ class TestOverwrites(unittest.TestCase):
'-o', 'test.webm', '-o', 'test.webm',
'https://www.youtube.com/watch?v=jNQXAC9IVRw', 'https://www.youtube.com/watch?v=jNQXAC9IVRw',
], cwd=root_dir, stdout=subprocess.PIPE, stderr=subprocess.PIPE) ], cwd=root_dir, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
sout, serr = outp.communicate() sout, _ = outp.communicate()
self.assertTrue(b'has already been downloaded' in sout) self.assertTrue(b'has already been downloaded' in sout)
# if the file has no content, it has not been redownloaded # if the file has no content, it has not been redownloaded
self.assertTrue(os.path.getsize(download_file) < 1) self.assertTrue(os.path.getsize(download_file) < 1)
@@ -41,7 +41,7 @@ class TestOverwrites(unittest.TestCase):
'-o', 'test.webm', '-o', 'test.webm',
'https://www.youtube.com/watch?v=jNQXAC9IVRw', 'https://www.youtube.com/watch?v=jNQXAC9IVRw',
], cwd=root_dir, stdout=subprocess.PIPE, stderr=subprocess.PIPE) ], cwd=root_dir, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
sout, serr = outp.communicate() sout, _ = outp.communicate()
self.assertTrue(b'has already been downloaded' not in sout) self.assertTrue(b'has already been downloaded' not in sout)
# if the file has no content, it has not been redownloaded # if the file has no content, it has not been redownloaded
self.assertTrue(os.path.getsize(download_file) > 1) self.assertTrue(os.path.getsize(download_file) > 1)
+5
View File
@@ -45,3 +45,8 @@ class TestGetWebPoContentBinding:
def test_invalid_base64(self, pot_request): def test_invalid_base64(self, pot_request):
pot_request.visitor_data = 'invalid-base64' pot_request.visitor_data = 'invalid-base64'
assert get_webpo_content_binding(pot_request, bind_to_visitor_id=True) == (pot_request.visitor_data, ContentBindingType.VISITOR_DATA) assert get_webpo_content_binding(pot_request, bind_to_visitor_id=True) == (pot_request.visitor_data, ContentBindingType.VISITOR_DATA)
def test_gvs_video_id_binding_experiment(self, pot_request):
pot_request.context = PoTokenContext.GVS
pot_request._gvs_bind_to_video_id = True
assert get_webpo_content_binding(pot_request) == ('example-video-id', ContentBindingType.VIDEO_ID)
+1 -1
View File
@@ -153,7 +153,7 @@ class TestPoTokenProvider:
with pytest.raises( with pytest.raises(
PoTokenProviderRejectedRequest, PoTokenProviderRejectedRequest,
match='External requests by "example" provider do not support proxy scheme "socks4". Supported proxy ' match=r'External requests by "example" provider do not support proxy scheme "socks4"\. Supported proxy '
'schemes: http, socks5h', 'schemes: http, socks5h',
): ):
provider.request_pot(pot_request) provider.request_pot(pot_request)
+23 -8
View File
@@ -12,6 +12,7 @@ import datetime as dt
import io import io
import itertools import itertools
import json import json
import ntpath
import pickle import pickle
import subprocess import subprocess
import unittest import unittest
@@ -101,11 +102,13 @@ from yt_dlp.utils import (
remove_start, remove_start,
render_table, render_table,
replace_extension, replace_extension,
datetime_round,
rot47, rot47,
sanitize_filename, sanitize_filename,
sanitize_path, sanitize_path,
sanitize_url, sanitize_url,
shell_quote, shell_quote,
strftime_or_none,
smuggle_url, smuggle_url,
str_to_int, str_to_int,
strip_jsonp, strip_jsonp,
@@ -251,12 +254,6 @@ class TestUtil(unittest.TestCase):
self.assertEqual(sanitize_path('abc.../def...'), 'abc..#\\def..#') self.assertEqual(sanitize_path('abc.../def...'), 'abc..#\\def..#')
self.assertEqual(sanitize_path('C:\\abc:%(title)s.%(ext)s'), 'C:\\abc#%(title)s.%(ext)s') self.assertEqual(sanitize_path('C:\\abc:%(title)s.%(ext)s'), 'C:\\abc#%(title)s.%(ext)s')
# Check with nt._path_normpath if available
try:
from nt import _path_normpath as nt_path_normpath
except ImportError:
nt_path_normpath = None
for test, expected in [ for test, expected in [
('C:\\', 'C:\\'), ('C:\\', 'C:\\'),
('../abc', '..\\abc'), ('../abc', '..\\abc'),
@@ -274,8 +271,7 @@ class TestUtil(unittest.TestCase):
result = sanitize_path(test) result = sanitize_path(test)
assert result == expected, f'{test} was incorrectly resolved' assert result == expected, f'{test} was incorrectly resolved'
assert result == sanitize_path(result), f'{test} changed after sanitizing again' assert result == sanitize_path(result), f'{test} changed after sanitizing again'
if nt_path_normpath: assert result == ntpath.normpath(test), f'{test} does not match ntpath.normpath'
assert result == nt_path_normpath(test), f'{test} does not match nt._path_normpath'
def test_sanitize_url(self): def test_sanitize_url(self):
self.assertEqual(sanitize_url('//foo.bar'), 'http://foo.bar') self.assertEqual(sanitize_url('//foo.bar'), 'http://foo.bar')
@@ -409,6 +405,25 @@ class TestUtil(unittest.TestCase):
self.assertEqual(datetime_from_str('now+1day', precision='hour'), datetime_from_str('now+24hours', precision='auto')) self.assertEqual(datetime_from_str('now+1day', precision='hour'), datetime_from_str('now+24hours', precision='auto'))
self.assertEqual(datetime_from_str('now+23hours', precision='hour'), datetime_from_str('now+23hours', precision='auto')) self.assertEqual(datetime_from_str('now+23hours', precision='hour'), datetime_from_str('now+23hours', precision='auto'))
def test_datetime_round(self):
self.assertEqual(datetime_round(dt.datetime.strptime('1820-05-12T01:23:45Z', '%Y-%m-%dT%H:%M:%SZ')),
dt.datetime(1820, 5, 12, tzinfo=dt.timezone.utc))
self.assertEqual(datetime_round(dt.datetime.strptime('1969-12-31T23:34:45Z', '%Y-%m-%dT%H:%M:%SZ'), 'hour'),
dt.datetime(1970, 1, 1, 0, tzinfo=dt.timezone.utc))
self.assertEqual(datetime_round(dt.datetime.strptime('2024-12-25T01:23:45Z', '%Y-%m-%dT%H:%M:%SZ'), 'minute'),
dt.datetime(2024, 12, 25, 1, 24, tzinfo=dt.timezone.utc))
self.assertEqual(datetime_round(dt.datetime.strptime('2024-12-25T01:23:45.123Z', '%Y-%m-%dT%H:%M:%S.%fZ'), 'second'),
dt.datetime(2024, 12, 25, 1, 23, 45, tzinfo=dt.timezone.utc))
self.assertEqual(datetime_round(dt.datetime.strptime('2024-12-25T01:23:45.678Z', '%Y-%m-%dT%H:%M:%S.%fZ'), 'second'),
dt.datetime(2024, 12, 25, 1, 23, 46, tzinfo=dt.timezone.utc))
def test_strftime_or_none(self):
self.assertEqual(strftime_or_none(-4722192000), '18200512')
self.assertEqual(strftime_or_none(0), '19700101')
self.assertEqual(strftime_or_none(1735084800), '20241225')
# Throws OverflowError
self.assertEqual(strftime_or_none(1735084800000), None)
def test_daterange(self): def test_daterange(self):
_20century = DateRange('19000101', '20000101') _20century = DateRange('19000101', '20000101')
self.assertFalse('17890714' in _20century) self.assertFalse('17890714' in _20century)
+4 -4
View File
@@ -22,7 +22,7 @@ class TestVerboseOutput(unittest.TestCase):
'--username', 'johnsmith@gmail.com', '--username', 'johnsmith@gmail.com',
'--password', 'my_secret_password', '--password', 'my_secret_password',
], cwd=rootDir, stdout=subprocess.PIPE, stderr=subprocess.PIPE) ], cwd=rootDir, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
sout, serr = outp.communicate() _, serr = outp.communicate()
self.assertTrue(b'--username' in serr) self.assertTrue(b'--username' in serr)
self.assertTrue(b'johnsmith' not in serr) self.assertTrue(b'johnsmith' not in serr)
self.assertTrue(b'--password' in serr) self.assertTrue(b'--password' in serr)
@@ -36,7 +36,7 @@ class TestVerboseOutput(unittest.TestCase):
'-u', 'johnsmith@gmail.com', '-u', 'johnsmith@gmail.com',
'-p', 'my_secret_password', '-p', 'my_secret_password',
], cwd=rootDir, stdout=subprocess.PIPE, stderr=subprocess.PIPE) ], cwd=rootDir, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
sout, serr = outp.communicate() _, serr = outp.communicate()
self.assertTrue(b'-u' in serr) self.assertTrue(b'-u' in serr)
self.assertTrue(b'johnsmith' not in serr) self.assertTrue(b'johnsmith' not in serr)
self.assertTrue(b'-p' in serr) self.assertTrue(b'-p' in serr)
@@ -50,7 +50,7 @@ class TestVerboseOutput(unittest.TestCase):
'--username=johnsmith@gmail.com', '--username=johnsmith@gmail.com',
'--password=my_secret_password', '--password=my_secret_password',
], cwd=rootDir, stdout=subprocess.PIPE, stderr=subprocess.PIPE) ], cwd=rootDir, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
sout, serr = outp.communicate() _, serr = outp.communicate()
self.assertTrue(b'--username' in serr) self.assertTrue(b'--username' in serr)
self.assertTrue(b'johnsmith' not in serr) self.assertTrue(b'johnsmith' not in serr)
self.assertTrue(b'--password' in serr) self.assertTrue(b'--password' in serr)
@@ -64,7 +64,7 @@ class TestVerboseOutput(unittest.TestCase):
'-u=johnsmith@gmail.com', '-u=johnsmith@gmail.com',
'-p=my_secret_password', '-p=my_secret_password',
], cwd=rootDir, stdout=subprocess.PIPE, stderr=subprocess.PIPE) ], cwd=rootDir, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
sout, serr = outp.communicate() _, serr = outp.communicate()
self.assertTrue(b'-u' in serr) self.assertTrue(b'-u' in serr)
self.assertTrue(b'johnsmith' not in serr) self.assertTrue(b'johnsmith' not in serr)
self.assertTrue(b'-p' in serr) self.assertTrue(b'-p' in serr)
+2 -1
View File
@@ -20,7 +20,7 @@ import random
import ssl import ssl
import threading import threading
from yt_dlp import socks, traverse_obj from yt_dlp import socks
from yt_dlp.cookies import YoutubeDLCookieJar from yt_dlp.cookies import YoutubeDLCookieJar
from yt_dlp.dependencies import websockets from yt_dlp.dependencies import websockets
from yt_dlp.networking import Request from yt_dlp.networking import Request
@@ -32,6 +32,7 @@ from yt_dlp.networking.exceptions import (
SSLError, SSLError,
TransportError, TransportError,
) )
from yt_dlp.utils.traversal import traverse_obj
from yt_dlp.utils.networking import HTTPHeaderDict from yt_dlp.utils.networking import HTTPHeaderDict
TEST_DIR = os.path.dirname(os.path.abspath(__file__)) TEST_DIR = os.path.dirname(os.path.abspath(__file__))
-77
View File
@@ -1,77 +0,0 @@
#!/usr/bin/env python3
# Allow direct execution
import os
import sys
import unittest
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
import xml.etree.ElementTree
import yt_dlp.extractor
import yt_dlp.YoutubeDL
from test.helper import get_params, is_download_test, try_rm
class YoutubeDL(yt_dlp.YoutubeDL):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.to_stderr = self.to_screen
params = get_params({
'writeannotations': True,
'skip_download': True,
'writeinfojson': False,
'format': 'flv',
})
TEST_ID = 'gr51aVj-mLg'
ANNOTATIONS_FILE = TEST_ID + '.annotations.xml'
EXPECTED_ANNOTATIONS = ['Speech bubble', 'Note', 'Title', 'Spotlight', 'Label']
@is_download_test
class TestAnnotations(unittest.TestCase):
def setUp(self):
# Clear old files
self.tearDown()
def test_info_json(self):
expected = list(EXPECTED_ANNOTATIONS) # Two annotations could have the same text.
ie = yt_dlp.extractor.YoutubeIE()
ydl = YoutubeDL(params)
ydl.add_info_extractor(ie)
ydl.download([TEST_ID])
self.assertTrue(os.path.exists(ANNOTATIONS_FILE))
annoxml = None
with open(ANNOTATIONS_FILE, encoding='utf-8') as annof:
annoxml = xml.etree.ElementTree.parse(annof)
self.assertTrue(annoxml is not None, 'Failed to parse annotations XML')
root = annoxml.getroot()
self.assertEqual(root.tag, 'document')
annotationsTag = root.find('annotations')
self.assertEqual(annotationsTag.tag, 'annotations')
annotations = annotationsTag.findall('annotation')
# Not all the annotations have TEXT children and the annotations are returned unsorted.
for a in annotations:
self.assertEqual(a.tag, 'annotation')
if a.get('type') == 'text':
textTag = a.find('TEXT')
text = textTag.text
self.assertTrue(text in expected) # assertIn only added in python 2.7
# remove the first occurrence, there could be more than one annotation with the same text
expected.remove(text)
# We should have seen (and removed) all the expected annotation texts.
self.assertEqual(len(expected), 0, 'Not all expected annotations were found.')
def tearDown(self):
try_rm(ANNOTATIONS_FILE)
if __name__ == '__main__':
unittest.main()
+6 -55
View File
@@ -304,7 +304,6 @@ class YoutubeDL:
clean_infojson: Remove internal metadata from the infojson clean_infojson: Remove internal metadata from the infojson
getcomments: Extract video comments. This will not be written to disk getcomments: Extract video comments. This will not be written to disk
unless writeinfojson is also given unless writeinfojson is also given
writeannotations: Write the video annotations to a .annotations.xml file
writethumbnail: Write the thumbnail image to a file writethumbnail: Write the thumbnail image to a file
allow_playlist_files: Whether to write playlists' description, infojson etc allow_playlist_files: Whether to write playlists' description, infojson etc
also to disk when using the 'write*' options also to disk when using the 'write*' options
@@ -511,11 +510,11 @@ class YoutubeDL:
the downloader (see yt_dlp/downloader/common.py): the downloader (see yt_dlp/downloader/common.py):
nopart, updatetime, buffersize, ratelimit, throttledratelimit, min_filesize, nopart, updatetime, buffersize, ratelimit, throttledratelimit, min_filesize,
max_filesize, test, noresizebuffer, retries, file_access_retries, fragment_retries, max_filesize, test, noresizebuffer, retries, file_access_retries, fragment_retries,
continuedl, xattr_set_filesize, hls_use_mpegts, http_chunk_size, continuedl, hls_use_mpegts, http_chunk_size, external_downloader_args,
external_downloader_args, concurrent_fragment_downloads, progress_delta. concurrent_fragment_downloads, progress_delta.
The following options are used by the post processors: The following options are used by the post processors:
ffmpeg_location: Location of the ffmpeg/avconv binary; either the path ffmpeg_location: Location of the ffmpeg binary; either the path
to the binary or its containing directory. to the binary or its containing directory.
postprocessor_args: A dictionary of postprocessor/executable keys (in lower case) postprocessor_args: A dictionary of postprocessor/executable keys (in lower case)
and a list of additional command-line arguments for the and a list of additional command-line arguments for the
@@ -566,32 +565,14 @@ class YoutubeDL:
allsubtitles: - Use subtitleslangs = ['all'] allsubtitles: - Use subtitleslangs = ['all']
Downloads all the subtitles of the video Downloads all the subtitles of the video
(requires writesubtitles or writeautomaticsub) (requires writesubtitles or writeautomaticsub)
include_ads: - Doesn't work
Download ads as well
call_home: - Not implemented
Boolean, true if we are allowed to contact the
yt-dlp servers for debugging.
post_hooks: - Register a custom postprocessor post_hooks: - Register a custom postprocessor
A list of functions that get called as the final step A list of functions that get called as the final step
for each video file, after all postprocessors have been for each video file, after all postprocessors have been
called. The filename will be passed as the only argument. called. The filename will be passed as the only argument.
hls_prefer_native: - Use external_downloader = {'m3u8': 'native'} or {'m3u8': 'ffmpeg'}. hls_prefer_native: - Use external_downloader = {'m3u8': 'native'} or {'m3u8': 'ffmpeg'}.
Use the native HLS downloader instead of ffmpeg/avconv Use the native HLS downloader instead of ffmpeg
if True, otherwise use ffmpeg/avconv if False, otherwise if True, otherwise use ffmpeg if False, otherwise
use downloader suggested by extractor if None. use downloader suggested by extractor if None.
prefer_ffmpeg: - avconv support is deprecated
If False, use avconv instead of ffmpeg if both are available,
otherwise prefer ffmpeg.
youtube_include_dash_manifest: - Use extractor_args
If True (default), DASH manifests and related
data will be downloaded and processed by extractor.
You can reduce network I/O by disabling it if you don't
care about DASH. (only for youtube)
youtube_include_hls_manifest: - Use extractor_args
If True (default), HLS manifests and related
data will be downloaded and processed by extractor.
You can reduce network I/O by disabling it if you don't
care about HLS. (only for youtube)
no_color: Same as `color='no_color'` no_color: Same as `color='no_color'`
no_overwrites: Same as `overwrites=False` no_overwrites: Same as `overwrites=False`
""" """
@@ -750,10 +731,6 @@ class YoutubeDL:
return True return True
return False return False
if check_deprecated('cn_verification_proxy', '--cn-verification-proxy', '--geo-verification-proxy'):
if self.params.get('geo_verification_proxy') is None:
self.params['geo_verification_proxy'] = self.params['cn_verification_proxy']
check_deprecated('useid', '--id', '-o "%(id)s.%(ext)s"') check_deprecated('useid', '--id', '-o "%(id)s.%(ext)s"')
for msg in self.params.get('_warnings', []): for msg in self.params.get('_warnings', []):
@@ -2717,11 +2694,7 @@ class YoutubeDL:
('modified_timestamp', 'modified_date'), ('modified_timestamp', 'modified_date'),
): ):
if info_dict.get(date_key) is None and info_dict.get(ts_key) is not None: if info_dict.get(date_key) is None and info_dict.get(ts_key) is not None:
# Working around out-of-range timestamp values (e.g. negative ones on Windows, info_dict[date_key] = strftime_or_none(info_dict[ts_key])
# see http://bugs.python.org/issue1646728)
with contextlib.suppress(ValueError, OverflowError, OSError):
upload_date = dt.datetime.fromtimestamp(info_dict[ts_key], dt.timezone.utc)
info_dict[date_key] = upload_date.strftime('%Y%m%d')
if not info_dict.get('release_year'): if not info_dict.get('release_year'):
info_dict['release_year'] = traverse_obj(info_dict, ('release_date', {lambda x: int(x[:4])})) info_dict['release_year'] = traverse_obj(info_dict, ('release_date', {lambda x: int(x[:4])}))
@@ -3339,28 +3312,6 @@ class YoutubeDL:
elif _infojson_written is None: elif _infojson_written is None:
return return
# Note: Annotations are deprecated
annofn = None
if self.params.get('writeannotations', False):
annofn = self.prepare_filename(info_dict, 'annotation')
if annofn:
if not self._ensure_dir_exists(annofn):
return
if not self.params.get('overwrites', True) and os.path.exists(annofn):
self.to_screen('[info] Video annotations are already present')
elif not info_dict.get('annotations'):
self.report_warning('There are no annotations to write.')
else:
try:
self.to_screen('[info] Writing video annotations to: ' + annofn)
with open(annofn, 'w', encoding='utf-8') as annofile:
annofile.write(info_dict['annotations'])
except (KeyError, TypeError):
self.report_warning('There are no annotations to write.')
except OSError:
self.report_error('Cannot write annotations file: ' + annofn)
return
# Write internet shortcut files # Write internet shortcut files
def _write_link_file(link_type): def _write_link_file(link_type):
url = try_get(info_dict['webpage_url'], iri_to_uri) url = try_get(info_dict['webpage_url'], iri_to_uri)
+6 -52
View File
@@ -59,11 +59,9 @@ from .utils import (
render_table, render_table,
setproctitle, setproctitle,
shell_quote, shell_quote,
traverse_obj,
variadic, variadic,
write_string, write_string,
) )
from .utils.networking import std_headers
from .utils._utils import _UnsafeExtensionError from .utils._utils import _UnsafeExtensionError
from .YoutubeDL import YoutubeDL from .YoutubeDL import YoutubeDL
@@ -157,7 +155,7 @@ def set_compat_opts(opts):
if 'format-sort' in opts.compat_opts: if 'format-sort' in opts.compat_opts:
opts.format_sort.extend(FormatSorter.ytdl_default) opts.format_sort.extend(FormatSorter.ytdl_default)
elif 'prefer-vp9-sort' in opts.compat_opts: elif 'prefer-vp9-sort' in opts.compat_opts:
opts.format_sort.extend(FormatSorter._prefer_vp9_sort) FormatSorter.default = FormatSorter._prefer_vp9_sort
if 'mtime-by-default' in opts.compat_opts: if 'mtime-by-default' in opts.compat_opts:
if opts.updatetime is None: if opts.updatetime is None:
@@ -523,7 +521,6 @@ def validate_options(opts):
if report_args_compat('post-processor', opts.postprocessor_args, 'default-compat', 'default'): if report_args_compat('post-processor', opts.postprocessor_args, 'default-compat', 'default'):
opts.postprocessor_args['default'] = opts.postprocessor_args.pop('default-compat') opts.postprocessor_args['default'] = opts.postprocessor_args.pop('default-compat')
opts.postprocessor_args.setdefault('sponskrub', [])
def report_conflict(arg1, opt1, arg2='--allow-unplayable-formats', opt2='allow_unplayable_formats', def report_conflict(arg1, opt1, arg2='--allow-unplayable-formats', opt2='allow_unplayable_formats',
val1=NO_DEFAULT, val2=NO_DEFAULT, default=False): val1=NO_DEFAULT, val2=NO_DEFAULT, default=False):
@@ -548,11 +545,6 @@ def validate_options(opts):
'"--exec before_dl:"', 'exec_cmd', val2=opts.exec_cmd.get('before_dl')) '"--exec before_dl:"', 'exec_cmd', val2=opts.exec_cmd.get('before_dl'))
report_conflict('--id', 'useid', '--output', 'outtmpl', val2=opts.outtmpl.get('default')) report_conflict('--id', 'useid', '--output', 'outtmpl', val2=opts.outtmpl.get('default'))
report_conflict('--remux-video', 'remuxvideo', '--recode-video', 'recodevideo') report_conflict('--remux-video', 'remuxvideo', '--recode-video', 'recodevideo')
report_conflict('--sponskrub', 'sponskrub', '--remove-chapters', 'remove_chapters')
report_conflict('--sponskrub', 'sponskrub', '--sponsorblock-mark', 'sponsorblock_mark')
report_conflict('--sponskrub', 'sponskrub', '--sponsorblock-remove', 'sponsorblock_remove')
report_conflict('--sponskrub-cut', 'sponskrub_cut', '--split-chapter', 'split_chapters',
val1=opts.sponskrub and opts.sponskrub_cut)
# Conflicts with --allow-unplayable-formats # Conflicts with --allow-unplayable-formats
report_conflict('--embed-metadata', 'addmetadata') report_conflict('--embed-metadata', 'addmetadata')
@@ -565,23 +557,15 @@ def validate_options(opts):
report_conflict('--recode-video', 'recodevideo') report_conflict('--recode-video', 'recodevideo')
report_conflict('--remove-chapters', 'remove_chapters', default=[]) report_conflict('--remove-chapters', 'remove_chapters', default=[])
report_conflict('--remux-video', 'remuxvideo') report_conflict('--remux-video', 'remuxvideo')
report_conflict('--sponskrub', 'sponskrub')
report_conflict('--sponsorblock-remove', 'sponsorblock_remove', default=set()) report_conflict('--sponsorblock-remove', 'sponsorblock_remove', default=set())
report_conflict('--xattrs', 'xattrs') report_conflict('--xattrs', 'xattrs')
# Fully deprecated options if hasattr(opts, '_deprecated_options'):
def report_deprecation(val, old, new=None):
if not val:
return
deprecation_warnings.append( deprecation_warnings.append(
f'{old} is deprecated and may be removed in a future version. Use {new} instead' if new f'The following options have been deprecated: {", ".join(opts._deprecated_options)}\n'
else f'{old} is deprecated and may not work as expected') 'Please remove them from your command/configuration to avoid future errors.\n'
'See https://github.com/yt-dlp/yt-dlp/issues/14198 for more details')
report_deprecation(opts.sponskrub, '--sponskrub', '--sponsorblock-mark or --sponsorblock-remove') del opts._deprecated_options
report_deprecation(not opts.prefer_ffmpeg, '--prefer-avconv', 'ffmpeg')
# report_deprecation(opts.include_ads, '--include-ads') # We may re-implement this in future
# report_deprecation(opts.call_home, '--call-home') # We may re-implement this in future
# report_deprecation(opts.writeannotations, '--write-annotations') # It's just that no website has it
# Dependent options # Dependent options
opts.date = DateRange.day(opts.date) if opts.date else DateRange(opts.dateafter, opts.datebefore) opts.date = DateRange.day(opts.date) if opts.date else DateRange(opts.dateafter, opts.datebefore)
@@ -712,21 +696,6 @@ def get_postprocessors(opts):
'add_metadata': opts.addmetadata, 'add_metadata': opts.addmetadata,
'add_infojson': opts.embed_infojson, 'add_infojson': opts.embed_infojson,
} }
# Deprecated
# This should be above EmbedThumbnail since sponskrub removes the thumbnail attachment
# but must be below EmbedSubtitle and FFmpegMetadata
# See https://github.com/yt-dlp/yt-dlp/issues/204 , https://github.com/faissaloo/SponSkrub/issues/29
# If opts.sponskrub is None, sponskrub is used, but it silently fails if the executable can't be found
if opts.sponskrub is not False:
yield {
'key': 'SponSkrub',
'path': opts.sponskrub_path,
'args': opts.sponskrub_args,
'cut': opts.sponskrub_cut,
'force': opts.sponskrub_force,
'ignoreerror': opts.sponskrub is None,
'_from_cli': True,
}
if opts.embedthumbnail: if opts.embedthumbnail:
yield { yield {
'key': 'EmbedThumbnail', 'key': 'EmbedThumbnail',
@@ -885,7 +854,6 @@ def parse_options(argv=None):
'nopart': opts.nopart, 'nopart': opts.nopart,
'updatetime': opts.updatetime, 'updatetime': opts.updatetime,
'writedescription': opts.writedescription, 'writedescription': opts.writedescription,
'writeannotations': opts.writeannotations,
'writeinfojson': opts.writeinfojson, 'writeinfojson': opts.writeinfojson,
'allow_playlist_files': opts.allow_playlist_files, 'allow_playlist_files': opts.allow_playlist_files,
'clean_infojson': opts.clean_infojson, 'clean_infojson': opts.clean_infojson,
@@ -919,7 +887,6 @@ def parse_options(argv=None):
'max_views': opts.max_views, 'max_views': opts.max_views,
'daterange': opts.date, 'daterange': opts.date,
'cachedir': opts.cachedir, 'cachedir': opts.cachedir,
'youtube_print_sig_code': opts.youtube_print_sig_code,
'age_limit': opts.age_limit, 'age_limit': opts.age_limit,
'download_archive': opts.download_archive, 'download_archive': opts.download_archive,
'break_on_existing': opts.break_on_existing, 'break_on_existing': opts.break_on_existing,
@@ -937,13 +904,9 @@ def parse_options(argv=None):
'socket_timeout': opts.socket_timeout, 'socket_timeout': opts.socket_timeout,
'bidi_workaround': opts.bidi_workaround, 'bidi_workaround': opts.bidi_workaround,
'debug_printtraffic': opts.debug_printtraffic, 'debug_printtraffic': opts.debug_printtraffic,
'prefer_ffmpeg': opts.prefer_ffmpeg,
'include_ads': opts.include_ads,
'default_search': opts.default_search, 'default_search': opts.default_search,
'dynamic_mpd': opts.dynamic_mpd, 'dynamic_mpd': opts.dynamic_mpd,
'extractor_args': opts.extractor_args, 'extractor_args': opts.extractor_args,
'youtube_include_dash_manifest': opts.youtube_include_dash_manifest,
'youtube_include_hls_manifest': opts.youtube_include_hls_manifest,
'encoding': opts.encoding, 'encoding': opts.encoding,
'extract_flat': opts.extract_flat, 'extract_flat': opts.extract_flat,
'live_from_start': opts.live_from_start, 'live_from_start': opts.live_from_start,
@@ -955,7 +918,6 @@ def parse_options(argv=None):
'fixup': opts.fixup, 'fixup': opts.fixup,
'source_address': opts.source_address, 'source_address': opts.source_address,
'impersonate': opts.impersonate, 'impersonate': opts.impersonate,
'call_home': opts.call_home,
'sleep_interval_requests': opts.sleep_interval_requests, 'sleep_interval_requests': opts.sleep_interval_requests,
'sleep_interval': opts.sleep_interval, 'sleep_interval': opts.sleep_interval,
'max_sleep_interval': opts.max_sleep_interval, 'max_sleep_interval': opts.max_sleep_interval,
@@ -965,7 +927,6 @@ def parse_options(argv=None):
'force_keyframes_at_cuts': opts.force_keyframes_at_cuts, 'force_keyframes_at_cuts': opts.force_keyframes_at_cuts,
'list_thumbnails': opts.list_thumbnails, 'list_thumbnails': opts.list_thumbnails,
'playlist_items': opts.playlist_items, 'playlist_items': opts.playlist_items,
'xattr_set_filesize': opts.xattr_set_filesize,
'match_filter': opts.match_filter, 'match_filter': opts.match_filter,
'color': opts.color, 'color': opts.color,
'ffmpeg_location': opts.ffmpeg_location, 'ffmpeg_location': opts.ffmpeg_location,
@@ -974,7 +935,6 @@ def parse_options(argv=None):
'hls_split_discontinuity': opts.hls_split_discontinuity, 'hls_split_discontinuity': opts.hls_split_discontinuity,
'external_downloader_args': opts.external_downloader_args, 'external_downloader_args': opts.external_downloader_args,
'postprocessor_args': opts.postprocessor_args, 'postprocessor_args': opts.postprocessor_args,
'cn_verification_proxy': opts.cn_verification_proxy,
'geo_verification_proxy': opts.geo_verification_proxy, 'geo_verification_proxy': opts.geo_verification_proxy,
'geo_bypass': opts.geo_bypass, 'geo_bypass': opts.geo_bypass,
'geo_bypass_country': opts.geo_bypass_country, 'geo_bypass_country': opts.geo_bypass_country,
@@ -992,12 +952,6 @@ def _real_main(argv=None):
parser, opts, all_urls, ydl_opts = parse_options(argv) parser, opts, all_urls, ydl_opts = parse_options(argv)
# Dump user agent
if opts.dump_user_agent:
ua = traverse_obj(opts.headers, 'User-Agent', casesense=False, default=std_headers['User-Agent'])
write_string(f'{ua}\n', out=sys.stdout)
return
if print_extractor_information(opts, all_urls): if print_extractor_information(opts, all_urls):
return return
+8
View File
@@ -1,3 +1,4 @@
import datetime as dt
import os import os
import xml.etree.ElementTree as etree import xml.etree.ElementTree as etree
@@ -27,6 +28,13 @@ def compat_ord(c):
return c if isinstance(c, int) else ord(c) return c if isinstance(c, int) else ord(c)
def compat_datetime_from_timestamp(timestamp):
# Calling dt.datetime.fromtimestamp with negative timestamps throws error in Windows
# Ref: https://github.com/yt-dlp/yt-dlp/issues/5185, https://github.com/python/cpython/issues/81708,
# https://github.com/yt-dlp/yt-dlp/issues/6706#issuecomment-1496842642
return (dt.datetime.fromtimestamp(0, dt.timezone.utc) + dt.timedelta(seconds=timestamp))
# Python 3.8+ does not honor %HOME% on windows, but this breaks compatibility with youtube-dl # 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 # See https://github.com/yt-dlp/yt-dlp/issues/792
# https://docs.python.org/3/library/os.path.html#os.path.expanduser # https://docs.python.org/3/library/os.path.html#os.path.expanduser
-1
View File
@@ -62,7 +62,6 @@ class FileDownloader:
test: Download only first bytes to test the downloader. test: Download only first bytes to test the downloader.
min_filesize: Skip files smaller than this size min_filesize: Skip files smaller than this size
max_filesize: Skip files larger than this size max_filesize: Skip files larger than this size
xattr_set_filesize: Set ytdl.filesize user xattribute with expected size.
progress_delta: The minimum time between progress output, in seconds progress_delta: The minimum time between progress output, in seconds
external_downloader_args: A dictionary of downloader keys (in lower case) external_downloader_args: A dictionary of downloader keys (in lower case)
and a list of additional command-line arguments for the and a list of additional command-line arguments for the
+1 -5
View File
@@ -563,7 +563,7 @@ class FFmpegFD(ExternalFD):
f'{cookie.name}={cookie.value}; path={cookie.path}; domain={cookie.domain};\r\n' f'{cookie.name}={cookie.value}; path={cookie.path}; domain={cookie.domain};\r\n'
for cookie in cookies)]) for cookie in cookies)])
if fmt.get('http_headers') and is_http: if fmt.get('http_headers') and is_http:
# Trailing \r\n after each HTTP header is important to prevent warning from ffmpeg/avconv: # Trailing \r\n after each HTTP header is important to prevent warning from ffmpeg:
# [http @ 00000000003d2fa0] No trailing CRLF found in HTTP header. # [http @ 00000000003d2fa0] No trailing CRLF found in HTTP header.
args.extend(['-headers', ''.join(f'{key}: {val}\r\n' for key, val in fmt['http_headers'].items())]) args.extend(['-headers', ''.join(f'{key}: {val}\r\n' for key, val in fmt['http_headers'].items())])
@@ -654,10 +654,6 @@ class FFmpegFD(ExternalFD):
return retval return retval
class AVconvFD(FFmpegFD):
pass
_BY_NAME = { _BY_NAME = {
klass.get_basename(): klass klass.get_basename(): klass
for name, klass in globals().items() for name, klass in globals().items()
+5 -5
View File
@@ -149,14 +149,14 @@ class FlvReader(io.BytesIO):
segments_count = self.read_unsigned_char() segments_count = self.read_unsigned_char()
segments = [] segments = []
for _ in range(segments_count): for _ in range(segments_count):
box_size, box_type, box_data = self.read_box_info() _box_size, box_type, box_data = self.read_box_info()
assert box_type == b'asrt' assert box_type == b'asrt'
segment = FlvReader(box_data).read_asrt() segment = FlvReader(box_data).read_asrt()
segments.append(segment) segments.append(segment)
fragments_run_count = self.read_unsigned_char() fragments_run_count = self.read_unsigned_char()
fragments = [] fragments = []
for _ in range(fragments_run_count): for _ in range(fragments_run_count):
box_size, box_type, box_data = self.read_box_info() _box_size, box_type, box_data = self.read_box_info()
assert box_type == b'afrt' assert box_type == b'afrt'
fragments.append(FlvReader(box_data).read_afrt()) fragments.append(FlvReader(box_data).read_afrt())
@@ -167,7 +167,7 @@ class FlvReader(io.BytesIO):
} }
def read_bootstrap_info(self): def read_bootstrap_info(self):
total_size, box_type, box_data = self.read_box_info() _, box_type, box_data = self.read_box_info()
assert box_type == b'abst' assert box_type == b'abst'
return FlvReader(box_data).read_abst() return FlvReader(box_data).read_abst()
@@ -324,9 +324,9 @@ class F4mFD(FragmentFD):
if requested_bitrate is None or len(formats) == 1: if requested_bitrate is None or len(formats) == 1:
# get the best format # get the best format
formats = sorted(formats, key=lambda f: f[0]) formats = sorted(formats, key=lambda f: f[0])
rate, media = formats[-1] _, media = formats[-1]
else: else:
rate, media = next(filter( _, media = next(filter(
lambda f: int(f[0]) == requested_bitrate, formats)) lambda f: int(f[0]) == requested_bitrate, formats))
# Prefer baseURL for relative URLs as per 11.2 of F4M 3.0 spec. # Prefer baseURL for relative URLs as per 11.2 of F4M 3.0 spec.
-9
View File
@@ -13,12 +13,9 @@ from ..utils import (
ContentTooShortError, ContentTooShortError,
RetryManager, RetryManager,
ThrottledDownload, ThrottledDownload,
XAttrMetadataError,
XAttrUnavailableError,
int_or_none, int_or_none,
parse_http_range, parse_http_range,
try_call, try_call,
write_xattr,
) )
from ..utils.networking import HTTPHeaderDict from ..utils.networking import HTTPHeaderDict
@@ -273,12 +270,6 @@ class HttpFD(FileDownloader):
self.report_error(f'unable to open for writing: {err}') self.report_error(f'unable to open for writing: {err}')
return False return False
if self.params.get('xattr_set_filesize', False) and data_len is not None:
try:
write_xattr(ctx.tmpfilename, 'user.ytdl.filesize', str(data_len).encode())
except (XAttrUnavailableError, XAttrMetadataError) as err:
self.report_error(f'unable to set filesize xattr: {err}')
try: try:
ctx.stream.write(data_block) ctx.stream.write(data_block)
except OSError as err: except OSError as err:
+16 -23
View File
@@ -337,6 +337,7 @@ from .cbc import (
CBCGemIE, CBCGemIE,
CBCGemLiveIE, CBCGemLiveIE,
CBCGemPlaylistIE, CBCGemPlaylistIE,
CBCListenIE,
CBCPlayerIE, CBCPlayerIE,
CBCPlayerPlaylistIE, CBCPlayerPlaylistIE,
) )
@@ -424,7 +425,6 @@ from .cpac import (
CPACPlaylistIE, CPACPlaylistIE,
) )
from .cracked import CrackedIE from .cracked import CrackedIE
from .crackle import CrackleIE
from .craftsy import CraftsyIE from .craftsy import CraftsyIE
from .crooksandliars import CrooksAndLiarsIE from .crooksandliars import CrooksAndLiarsIE
from .crowdbunker import ( from .crowdbunker import (
@@ -444,10 +444,6 @@ from .curiositystream import (
CuriosityStreamIE, CuriosityStreamIE,
CuriosityStreamSeriesIE, CuriosityStreamSeriesIE,
) )
from .cwtv import (
CWTVIE,
CWTVMovieIE,
)
from .cybrary import ( from .cybrary import (
CybraryCourseIE, CybraryCourseIE,
CybraryIE, CybraryIE,
@@ -828,6 +824,13 @@ from .ichinanalive import (
IchinanaLiveIE, IchinanaLiveIE,
IchinanaLiveVODIE, IchinanaLiveVODIE,
) )
from .idagio import (
IdagioAlbumIE,
IdagioPersonalPlaylistIE,
IdagioPlaylistIE,
IdagioRecordingIE,
IdagioTrackIE,
)
from .idolplus import IdolPlusIE from .idolplus import IdolPlusIE
from .ign import ( from .ign import (
IGNIE, IGNIE,
@@ -1141,7 +1144,6 @@ from .mit import (
OCWMITIE, OCWMITIE,
TechTVMITIE, TechTVMITIE,
) )
from .mitele import MiTeleIE
from .mixch import ( from .mixch import (
MixchArchiveIE, MixchArchiveIE,
MixchIE, MixchIE,
@@ -1433,6 +1435,7 @@ from .onet import (
OnetPlIE, OnetPlIE,
) )
from .onionstudios import OnionStudiosIE from .onionstudios import OnionStudiosIE
from .onsen import OnsenIE
from .opencast import ( from .opencast import (
OpencastIE, OpencastIE,
OpencastPlaylistIE, OpencastPlaylistIE,
@@ -1466,10 +1469,6 @@ from .panopto import (
PanoptoListIE, PanoptoListIE,
PanoptoPlaylistIE, PanoptoPlaylistIE,
) )
from .paramountplus import (
ParamountPlusIE,
ParamountPlusSeriesIE,
)
from .parler import ParlerIE from .parler import ParlerIE
from .parlview import ParlviewIE from .parlview import ParlviewIE
from .parti import ( from .parti import (
@@ -1523,10 +1522,6 @@ from .piramidetv import (
PiramideTVChannelIE, PiramideTVChannelIE,
PiramideTVIE, PiramideTVIE,
) )
from .pixivsketch import (
PixivSketchIE,
PixivSketchUserIE,
)
from .planetmarathi import PlanetMarathiIE from .planetmarathi import PlanetMarathiIE
from .platzi import ( from .platzi import (
PlatziCourseIE, PlatziCourseIE,
@@ -1783,7 +1778,6 @@ from .rutube import (
RutubePlaylistIE, RutubePlaylistIE,
RutubeTagsIE, RutubeTagsIE,
) )
from .rutv import RUTVIE
from .ruutu import RuutuIE from .ruutu import RuutuIE
from .ruv import ( from .ruv import (
RuvIE, RuvIE,
@@ -1853,7 +1847,6 @@ from .simplecast import (
SimplecastPodcastIE, SimplecastPodcastIE,
) )
from .sina import SinaIE from .sina import SinaIE
from .sixplay import SixPlayIE
from .skeb import SkebIE from .skeb import SkebIE
from .sky import ( from .sky import (
SkyNewsIE, SkyNewsIE,
@@ -1881,7 +1874,12 @@ from .skynewsau import SkyNewsAUIE
from .slideshare import SlideshareIE from .slideshare import SlideshareIE
from .slideslive import SlidesLiveIE from .slideslive import SlidesLiveIE
from .slutload import SlutloadIE from .slutload import SlutloadIE
from .smotrim import SmotrimIE from .smotrim import (
SmotrimAudioIE,
SmotrimIE,
SmotrimLiveIE,
SmotrimPlaylistIE,
)
from .snapchat import SnapchatSpotlightIE from .snapchat import SnapchatSpotlightIE
from .snotr import SnotrIE from .snotr import SnotrIE
from .softwhiteunderbelly import SoftWhiteUnderbellyIE from .softwhiteunderbelly import SoftWhiteUnderbellyIE
@@ -1929,10 +1927,6 @@ from .spiegel import SpiegelIE
from .sport5 import Sport5IE from .sport5 import Sport5IE
from .sportbox import SportBoxIE from .sportbox import SportBoxIE
from .sportdeutschland import SportDeutschlandIE from .sportdeutschland import SportDeutschlandIE
from .spotify import (
SpotifyIE,
SpotifyShowIE,
)
from .spreaker import ( from .spreaker import (
SpreakerIE, SpreakerIE,
SpreakerShowIE, SpreakerShowIE,
@@ -2153,6 +2147,7 @@ from .tubitv import (
) )
from .tumblr import TumblrIE from .tumblr import TumblrIE
from .tunein import ( from .tunein import (
TuneInEmbedIE,
TuneInPodcastEpisodeIE, TuneInPodcastEpisodeIE,
TuneInPodcastIE, TuneInPodcastIE,
TuneInShortenerIE, TuneInShortenerIE,
@@ -2287,7 +2282,6 @@ from .utreon import UtreonIE
from .varzesh3 import Varzesh3IE from .varzesh3 import Varzesh3IE
from .vbox7 import Vbox7IE from .vbox7 import Vbox7IE
from .veo import VeoIE from .veo import VeoIE
from .vesti import VestiIE
from .vevo import ( from .vevo import (
VevoIE, VevoIE,
VevoPlaylistIE, VevoPlaylistIE,
@@ -2476,7 +2470,6 @@ from .wykop import (
WykopPostCommentIE, WykopPostCommentIE,
WykopPostIE, WykopPostIE,
) )
from .xanimu import XanimuIE
from .xboxclips import XboxClipsIE from .xboxclips import XboxClipsIE
from .xhamster import ( from .xhamster import (
XHamsterEmbedIE, XHamsterEmbedIE,
+18 -6
View File
@@ -21,7 +21,7 @@ from ..utils import (
class ABCIE(InfoExtractor): class ABCIE(InfoExtractor):
IE_NAME = 'abc.net.au' IE_NAME = 'abc.net.au'
_VALID_URL = r'https?://(?:www\.)?abc\.net\.au/(?:news|btn)/(?:[^/]+/){1,4}(?P<id>\d{5,})' _VALID_URL = r'https?://(?:www\.)?abc\.net\.au/(?:news|btn|listen)/(?:[^/?#]+/){1,4}(?P<id>\d{5,})'
_TESTS = [{ _TESTS = [{
'url': 'http://www.abc.net.au/news/2014-11-05/australia-to-staff-ebola-treatment-centre-in-sierra-leone/5868334', 'url': 'http://www.abc.net.au/news/2014-11-05/australia-to-staff-ebola-treatment-centre-in-sierra-leone/5868334',
@@ -53,8 +53,9 @@ class ABCIE(InfoExtractor):
'info_dict': { 'info_dict': {
'id': '6880080', 'id': '6880080',
'ext': 'mp3', 'ext': 'mp3',
'title': 'NAB lifts interest rates, following Westpac and CBA', 'title': 'NAB lifts interest rates, following Westpac and CBA - ABC listen',
'description': 'md5:f13d8edc81e462fce4a0437c7dc04728', 'description': 'md5:f13d8edc81e462fce4a0437c7dc04728',
'thumbnail': r're:https://live-production\.wcms\.abc-cdn\.net\.au/2193d7437c84b25eafd6360c82b5fa21',
}, },
}, { }, {
'url': 'http://www.abc.net.au/news/2015-10-19/6866214', 'url': 'http://www.abc.net.au/news/2015-10-19/6866214',
@@ -64,8 +65,9 @@ class ABCIE(InfoExtractor):
'info_dict': { 'info_dict': {
'id': '10527914', 'id': '10527914',
'ext': 'mp4', 'ext': 'mp4',
'title': 'WWI Centenary', 'title': 'WWI Centenary - Behind The News',
'description': 'md5:c2379ec0ca84072e86b446e536954546', 'description': 'md5:fa4405939ff750fade46ff0cd4c66a52',
'thumbnail': r're:https://live-production\.wcms\.abc-cdn\.net\.au/bcc3433c97bf992dff32ec5a768713c9',
}, },
}, { }, {
'url': 'https://www.abc.net.au/news/programs/the-world/2020-06-10/black-lives-matter-protests-spawn-support-for/12342074', 'url': 'https://www.abc.net.au/news/programs/the-world/2020-06-10/black-lives-matter-protests-spawn-support-for/12342074',
@@ -73,7 +75,8 @@ class ABCIE(InfoExtractor):
'id': '12342074', 'id': '12342074',
'ext': 'mp4', 'ext': 'mp4',
'title': 'Black Lives Matter protests spawn support for Papuans in Indonesia', 'title': 'Black Lives Matter protests spawn support for Papuans in Indonesia',
'description': 'md5:2961a17dc53abc558589ccd0fb8edd6f', 'description': 'md5:625257209f2d14ce23cb4e3785da9beb',
'thumbnail': r're:https://live-production\.wcms\.abc-cdn\.net\.au/7ee6f190de6d7dbb04203e514bfae9ec',
}, },
}, { }, {
'url': 'https://www.abc.net.au/btn/newsbreak/btn-newsbreak-20200814/12560476', 'url': 'https://www.abc.net.au/btn/newsbreak/btn-newsbreak-20200814/12560476',
@@ -93,7 +96,16 @@ class ABCIE(InfoExtractor):
'title': 'Wagner Group retreating from Russia, leader Prigozhin to move to Belarus', 'title': 'Wagner Group retreating from Russia, leader Prigozhin to move to Belarus',
'ext': 'mp4', 'ext': 'mp4',
'description': 'Wagner troops leave Rostov-on-Don and\xa0Yevgeny Prigozhin will move to Belarus under a deal brokered by Belarusian President Alexander Lukashenko to end the mutiny.', 'description': 'Wagner troops leave Rostov-on-Don and\xa0Yevgeny Prigozhin will move to Belarus under a deal brokered by Belarusian President Alexander Lukashenko to end the mutiny.',
'thumbnail': 'https://live-production.wcms.abc-cdn.net.au/0c170f5b57f0105c432f366c0e8e267b?impolicy=wcms_crop_resize&cropH=2813&cropW=5000&xPos=0&yPos=249&width=862&height=485', 'thumbnail': r're:https://live-production\.wcm\.abc-cdn\.net\.au/0c170f5b57f0105c432f366c0e8e267b',
},
}, {
'url': 'https://www.abc.net.au/listen/programs/the-followers-madness-of-two/presents-followers-madness-of-two/105697646',
'info_dict': {
'id': '105697646',
'title': 'INTRODUCING — The Followers: Madness of Two - ABC listen',
'ext': 'mp3',
'description': 'md5:2310cd0d440a4e01656abea15db8d1f3',
'thumbnail': r're:https://live-production\.wcms\.abc-cdn\.net\.au/90d7078214e5d66553ffb7fcf0da0cda',
}, },
}] }]
+5 -3
View File
@@ -1,5 +1,6 @@
from .common import InfoExtractor from .common import InfoExtractor
from ..utils import ( from ..utils import (
clean_html,
clean_podcast_url, clean_podcast_url,
int_or_none, int_or_none,
parse_iso8601, parse_iso8601,
@@ -17,7 +18,7 @@ class ApplePodcastsIE(InfoExtractor):
'ext': 'mp3', 'ext': 'mp3',
'title': 'Ferreck Dawn - To The Break of Dawn 117', 'title': 'Ferreck Dawn - To The Break of Dawn 117',
'episode': 'Ferreck Dawn - To The Break of Dawn 117', 'episode': 'Ferreck Dawn - To The Break of Dawn 117',
'description': 'md5:1fc571102f79dbd0a77bfd71ffda23bc', 'description': 'md5:8c4f5c2c30af17ed6a98b0b9daf15b76',
'upload_date': '20240812', 'upload_date': '20240812',
'timestamp': 1723449600, 'timestamp': 1723449600,
'duration': 3596, 'duration': 3596,
@@ -58,7 +59,7 @@ class ApplePodcastsIE(InfoExtractor):
r'<script [^>]*\bid=["\']serialized-server-data["\'][^>]*>', webpage, r'<script [^>]*\bid=["\']serialized-server-data["\'][^>]*>', webpage,
'server data', episode_id, contains_pattern=r'\[{(?s:.+)}\]')[0]['data'] 'server data', episode_id, contains_pattern=r'\[{(?s:.+)}\]')[0]['data']
model_data = traverse_obj(server_data, ( model_data = traverse_obj(server_data, (
'headerButtonItems', lambda _, v: v['$kind'] == 'bookmark' and v['modelType'] == 'EpisodeOffer', 'headerButtonItems', lambda _, v: v['$kind'] == 'share' and v['modelType'] == 'EpisodeLockup',
'model', {dict}, any)) 'model', {dict}, any))
return { return {
@@ -68,7 +69,8 @@ class ApplePodcastsIE(InfoExtractor):
or self._yield_json_ld(webpage, episode_id, fatal=False), episode_id, fatal=False), or self._yield_json_ld(webpage, episode_id, fatal=False), episode_id, fatal=False),
**traverse_obj(model_data, { **traverse_obj(model_data, {
'title': ('title', {str}), 'title': ('title', {str}),
'url': ('streamUrl', {clean_podcast_url}), 'description': ('summary', {clean_html}),
'url': ('playAction', 'episodeOffer', 'streamUrl', {clean_podcast_url}),
'timestamp': ('releaseDate', {parse_iso8601}), 'timestamp': ('releaseDate', {parse_iso8601}),
'duration': ('duration', {int_or_none}), 'duration': ('duration', {int_or_none}),
}), }),
+2 -2
View File
@@ -1366,7 +1366,7 @@ class BilibiliSpaceVideoIE(BilibiliSpaceBaseIE):
else: else:
yield self.url_result(f'https://www.bilibili.com/video/{entry["bvid"]}', BiliBiliIE, entry['bvid']) yield self.url_result(f'https://www.bilibili.com/video/{entry["bvid"]}', BiliBiliIE, entry['bvid'])
metadata, paged_list = self._extract_playlist(fetch_page, get_metadata, get_entries) _, paged_list = self._extract_playlist(fetch_page, get_metadata, get_entries)
return self.playlist_result(paged_list, playlist_id) return self.playlist_result(paged_list, playlist_id)
@@ -1400,7 +1400,7 @@ class BilibiliSpaceAudioIE(BilibiliSpaceBaseIE):
for entry in page_data.get('data') or []: for entry in page_data.get('data') or []:
yield self.url_result(f'https://www.bilibili.com/audio/au{entry["id"]}', BilibiliAudioIE, entry['id']) yield self.url_result(f'https://www.bilibili.com/audio/au{entry["id"]}', BilibiliAudioIE, entry['id'])
metadata, paged_list = self._extract_playlist(fetch_page, get_metadata, get_entries) _, paged_list = self._extract_playlist(fetch_page, get_metadata, get_entries)
return self.playlist_result(paged_list, playlist_id) return self.playlist_result(paged_list, playlist_id)
+1 -1
View File
@@ -174,7 +174,7 @@ class BrainPOPLegacyBaseIE(BrainPOPBaseIE):
} }
def _real_extract(self, url): def _real_extract(self, url):
slug, display_id = self._match_valid_url(url).group('slug', 'id') display_id = self._match_id(url)
webpage = self._download_webpage(url, display_id) webpage = self._download_webpage(url, display_id)
topic_data = self._search_json( topic_data = self._search_json(
r'var\s+content\s*=\s*', webpage, 'content data', r'var\s+content\s*=\s*', webpage, 'content data',
+61 -5
View File
@@ -31,7 +31,7 @@ from ..utils.traversal import require, traverse_obj, trim_str
class CBCIE(InfoExtractor): class CBCIE(InfoExtractor):
IE_NAME = 'cbc.ca' IE_NAME = 'cbc.ca'
_VALID_URL = r'https?://(?:www\.)?cbc\.ca/(?!player/)(?:[^/]+/)+(?P<id>[^/?#]+)' _VALID_URL = r'https?://(?:www\.)?cbc\.ca/(?!player/|listen/|i/caffeine/syndicate/)(?:[^/?#]+/)+(?P<id>[^/?#]+)'
_TESTS = [{ _TESTS = [{
# with mediaId # with mediaId
'url': 'http://www.cbc.ca/22minutes/videos/clips-season-23/don-cherry-play-offs', 'url': 'http://www.cbc.ca/22minutes/videos/clips-season-23/don-cherry-play-offs',
@@ -112,10 +112,6 @@ class CBCIE(InfoExtractor):
'playlist_mincount': 6, 'playlist_mincount': 6,
}] }]
@classmethod
def suitable(cls, url):
return False if CBCPlayerIE.suitable(url) else super().suitable(url)
def _extract_player_init(self, player_init, display_id): def _extract_player_init(self, player_init, display_id):
player_info = self._parse_json(player_init, display_id, js_to_json) player_info = self._parse_json(player_init, display_id, js_to_json)
media_id = player_info.get('mediaId') media_id = player_info.get('mediaId')
@@ -913,3 +909,63 @@ class CBCGemLiveIE(InfoExtractor):
'thumbnail': ('images', 'card', 'url'), 'thumbnail': ('images', 'card', 'url'),
}), }),
} }
class CBCListenIE(InfoExtractor):
IE_NAME = 'cbc.ca:listen'
_VALID_URL = r'https?://(?:www\.)?cbc\.ca/listen/(?:cbc-podcasts|live-radio)/[\w-]+/[\w-]+/(?P<id>\d+)'
_TESTS = [{
'url': 'https://www.cbc.ca/listen/cbc-podcasts/1353-the-naked-emperor/episode/16142603-introducing-understood-who-broke-the-internet',
'info_dict': {
'id': '16142603',
'title': 'Introducing Understood: Who Broke the Internet?',
'ext': 'mp3',
'description': 'md5:c605117500084e43f08a950adc6a708c',
'duration': 229,
'timestamp': 1745812800,
'release_timestamp': 1745827200,
'release_date': '20250428',
'upload_date': '20250428',
},
}, {
'url': 'https://www.cbc.ca/listen/live-radio/1-64-the-house/clip/16170773-should-canada-suck-stand-donald-trump',
'info_dict': {
'id': '16170773',
'title': 'Should Canada suck up or stand up to Donald Trump?',
'ext': 'mp3',
'description': 'md5:7385194f1cdda8df27ba3764b35e7976',
'duration': 3159,
'timestamp': 1758340800,
'release_timestamp': 1758254400,
'release_date': '20250919',
'upload_date': '20250920',
},
}]
def _real_extract(self, url):
video_id = self._match_id(url)
response = self._download_json(
f'https://www.cbc.ca/listen/api/v1/clips/{video_id}', video_id, fatal=False)
data = traverse_obj(response, ('data', {dict}))
if not data:
self.report_warning('API failed to return data. Falling back to webpage parsing')
webpage = self._download_webpage(url, video_id)
preloaded_state = self._search_json(
r'window\.__PRELOADED_STATE__\s*=', webpage, 'preloaded state',
video_id, transform_source=js_to_json)
data = traverse_obj(preloaded_state, (
('podcastDetailData', 'showDetailData'), ..., 'episodes',
lambda _, v: str(v['clipID']) == video_id, any, {require('episode data')}))
return {
'id': video_id,
**traverse_obj(data, {
'url': (('src', 'url'), {url_or_none}, any),
'title': ('title', {str}),
'description': ('description', {str}),
'release_timestamp': ('releasedAt', {int_or_none(scale=1000)}),
'timestamp': ('airdate', {int_or_none(scale=1000)}),
'duration': ('duration', {int_or_none}),
}),
}
+1 -9
View File
@@ -5,8 +5,6 @@ import zlib
from .anvato import AnvatoIE from .anvato import AnvatoIE
from .common import InfoExtractor from .common import InfoExtractor
from .paramountplus import ParamountPlusIE
from ..networking import HEADRequest
from ..utils import ( from ..utils import (
ExtractorError, ExtractorError,
UserNotLive, UserNotLive,
@@ -132,13 +130,7 @@ class CBSNewsEmbedIE(CBSNewsBaseIE):
video_id = item['mpxRefId'] video_id = item['mpxRefId']
video_url = self._get_video_url(item) video_url = self._get_video_url(item)
if not video_url: if not video_url:
# Old embeds redirect user to ParamountPlus but most links are 404 raise ExtractorError('This video is no longer available', expected=True)
pplus_url = f'https://www.paramountplus.com/shows/video/{video_id}'
try:
self._request_webpage(HEADRequest(pplus_url), video_id)
return self.url_result(pplus_url, ParamountPlusIE)
except ExtractorError:
self.raise_no_formats('This video is no longer available', True, video_id)
return self._extract_video(item, video_url, video_id) return self._extract_video(item, video_url, video_id)
-12
View File
@@ -5,18 +5,6 @@ from ..utils import ExtractorError, make_archive_id, url_basename
class CellebriteIE(VidyardBaseIE): class CellebriteIE(VidyardBaseIE):
_VALID_URL = r'https?://cellebrite\.com/(?:\w+)?/(?P<id>[\w-]+)' _VALID_URL = r'https?://cellebrite\.com/(?:\w+)?/(?P<id>[\w-]+)'
_TESTS = [{ _TESTS = [{
'url': 'https://cellebrite.com/en/collect-data-from-android-devices-with-cellebrite-ufed/',
'info_dict': {
'id': 'ZqmUss3dQfEMGpauambPuH',
'display_id': '16025876',
'ext': 'mp4',
'title': 'Ask the Expert: Chat Capture - Collect Data from Android Devices in Cellebrite UFED',
'description': 'md5:dee48fe12bbae5c01fe6a053f7676da4',
'thumbnail': 'https://cellebrite.com/wp-content/uploads/2021/05/Chat-Capture-1024x559.png',
'duration': 455.979,
'_old_archive_ids': ['cellebrite 16025876'],
},
}, {
'url': 'https://cellebrite.com/en/how-to-lawfully-collect-the-maximum-amount-of-data-from-android-devices/', 'url': 'https://cellebrite.com/en/how-to-lawfully-collect-the-maximum-amount-of-data-from-android-devices/',
'info_dict': { 'info_dict': {
'id': 'QV1U8a2yzcxigw7VFnqKyg', 'id': 'QV1U8a2yzcxigw7VFnqKyg',
+1
View File
@@ -272,6 +272,7 @@ class CNNIndonesiaIE(InfoExtractor):
return merge_dicts(json_ld_data, { return merge_dicts(json_ld_data, {
'_type': 'url_transparent', '_type': 'url_transparent',
'url': embed_url, 'url': embed_url,
'id': video_id,
'upload_date': upload_date, 'upload_date': upload_date,
'tags': try_call(lambda: self._html_search_meta('keywords', webpage).split(', ')), 'tags': try_call(lambda: self._html_search_meta('keywords', webpage).split(', ')),
}) })
-243
View File
@@ -1,243 +0,0 @@
import hashlib
import hmac
import re
import time
from .common import InfoExtractor
from ..networking.exceptions import HTTPError
from ..utils import (
ExtractorError,
determine_ext,
float_or_none,
int_or_none,
orderedSet,
parse_age_limit,
parse_duration,
url_or_none,
)
class CrackleIE(InfoExtractor):
_VALID_URL = r'(?:crackle:|https?://(?:(?:www|m)\.)?(?:sony)?crackle\.com/(?:playlist/\d+/|(?:[^/]+/)+))(?P<id>\d+)'
_TESTS = [{
# Crackle is available in the United States and territories
'url': 'https://www.crackle.com/thanksgiving/2510064',
'info_dict': {
'id': '2510064',
'ext': 'mp4',
'title': 'Touch Football',
'description': 'md5:cfbb513cf5de41e8b56d7ab756cff4df',
'duration': 1398,
'view_count': int,
'average_rating': 0,
'age_limit': 17,
'genre': 'Comedy',
'creator': 'Daniel Powell',
'artist': 'Chris Elliott, Amy Sedaris',
'release_year': 2016,
'series': 'Thanksgiving',
'episode': 'Touch Football',
'season_number': 1,
'episode_number': 1,
},
'params': {
# m3u8 download
'skip_download': True,
},
'expected_warnings': [
'Trying with a list of known countries',
],
}, {
'url': 'https://www.sonycrackle.com/thanksgiving/2510064',
'only_matching': True,
}]
_MEDIA_FILE_SLOTS = {
'360p.mp4': {
'width': 640,
'height': 360,
},
'480p.mp4': {
'width': 768,
'height': 432,
},
'480p_1mbps.mp4': {
'width': 852,
'height': 480,
},
}
def _download_json(self, url, *args, **kwargs):
# Authorization generation algorithm is reverse engineered from:
# https://www.sonycrackle.com/static/js/main.ea93451f.chunk.js
timestamp = time.strftime('%Y%m%d%H%M', time.gmtime())
h = hmac.new(b'IGSLUQCBDFHEOIFM', '|'.join([url, timestamp]).encode(), hashlib.sha1).hexdigest().upper()
headers = {
'Accept': 'application/json',
'Authorization': '|'.join([h, timestamp, '117', '1']),
}
return InfoExtractor._download_json(self, url, *args, headers=headers, **kwargs)
def _real_extract(self, url):
video_id = self._match_id(url)
geo_bypass_country = self.get_param('geo_bypass_country', None)
countries = orderedSet((geo_bypass_country, 'US', 'AU', 'CA', 'AS', 'FM', 'GU', 'MP', 'PR', 'PW', 'MH', 'VI', ''))
num_countries, num = len(countries) - 1, 0
media = {}
for num, country in enumerate(countries):
if num == 1: # start hard-coded list
self.report_warning('%s. Trying with a list of known countries' % (
f'Unable to obtain video formats from {geo_bypass_country} API' if geo_bypass_country
else 'No country code was given using --geo-bypass-country'))
elif num == num_countries: # end of list
geo_info = self._download_json(
'https://web-api-us.crackle.com/Service.svc/geo/country',
video_id, fatal=False, note='Downloading geo-location information from crackle API',
errnote='Unable to fetch geo-location information from crackle') or {}
country = geo_info.get('CountryCode')
if country is None:
continue
self.to_screen(f'{self.IE_NAME} identified country as {country}')
if country in countries:
self.to_screen(f'Downloading from {country} API was already attempted. Skipping...')
continue
if country is None:
continue
try:
media = self._download_json(
f'https://web-api-us.crackle.com/Service.svc/details/media/{video_id}/{country}?disableProtocols=true',
video_id, note=f'Downloading media JSON from {country} API',
errnote='Unable to download media JSON')
except ExtractorError as e:
# 401 means geo restriction, trying next country
if isinstance(e.cause, HTTPError) and e.cause.status == 401:
continue
raise
status = media.get('status')
if status.get('messageCode') != '0':
raise ExtractorError(
'{} said: {} {} - {}'.format(
self.IE_NAME, status.get('messageCodeDescription'), status.get('messageCode'), status.get('message')),
expected=True)
# Found video formats
if isinstance(media.get('MediaURLs'), list):
break
ignore_no_formats = self.get_param('ignore_no_formats_error')
if not media or (not media.get('MediaURLs') and not ignore_no_formats):
raise ExtractorError(
'Unable to access the crackle API. Try passing your country code '
'to --geo-bypass-country. If it still does not work and the '
'video is available in your country')
title = media['Title']
formats, subtitles = [], {}
has_drm = False
for e in media.get('MediaURLs') or []:
if e.get('UseDRM'):
has_drm = True
format_url = url_or_none(e.get('DRMPath'))
else:
format_url = url_or_none(e.get('Path'))
if not format_url:
continue
ext = determine_ext(format_url)
if ext == 'm3u8':
fmts, subs = self._extract_m3u8_formats_and_subtitles(
format_url, video_id, 'mp4', entry_protocol='m3u8_native',
m3u8_id='hls', fatal=False)
formats.extend(fmts)
subtitles = self._merge_subtitles(subtitles, subs)
elif ext == 'mpd':
fmts, subs = self._extract_mpd_formats_and_subtitles(
format_url, video_id, mpd_id='dash', fatal=False)
formats.extend(fmts)
subtitles = self._merge_subtitles(subtitles, subs)
elif format_url.endswith('.ism/Manifest'):
fmts, subs = self._extract_ism_formats_and_subtitles(
format_url, video_id, ism_id='mss', fatal=False)
formats.extend(fmts)
subtitles = self._merge_subtitles(subtitles, subs)
else:
mfs_path = e.get('Type')
mfs_info = self._MEDIA_FILE_SLOTS.get(mfs_path)
if not mfs_info:
continue
formats.append({
'url': format_url,
'format_id': 'http-' + mfs_path.split('.')[0],
'width': mfs_info['width'],
'height': mfs_info['height'],
})
if not formats and has_drm:
self.report_drm(video_id)
description = media.get('Description')
duration = int_or_none(media.get(
'DurationInSeconds')) or parse_duration(media.get('Duration'))
view_count = int_or_none(media.get('CountViews'))
average_rating = float_or_none(media.get('UserRating'))
age_limit = parse_age_limit(media.get('Rating'))
genre = media.get('Genre')
release_year = int_or_none(media.get('ReleaseYear'))
creator = media.get('Directors')
artist = media.get('Cast')
if media.get('MediaTypeDisplayValue') == 'Full Episode':
series = media.get('ShowName')
episode = title
season_number = int_or_none(media.get('Season'))
episode_number = int_or_none(media.get('Episode'))
else:
series = episode = season_number = episode_number = None
cc_files = media.get('ClosedCaptionFiles')
if isinstance(cc_files, list):
for cc_file in cc_files:
if not isinstance(cc_file, dict):
continue
cc_url = url_or_none(cc_file.get('Path'))
if not cc_url:
continue
lang = cc_file.get('Locale') or 'en'
subtitles.setdefault(lang, []).append({'url': cc_url})
thumbnails = []
images = media.get('Images')
if isinstance(images, list):
for image_key, image_url in images.items():
mobj = re.search(r'Img_(\d+)[xX](\d+)', image_key)
if not mobj:
continue
thumbnails.append({
'url': image_url,
'width': int(mobj.group(1)),
'height': int(mobj.group(2)),
})
return {
'id': video_id,
'title': title,
'description': description,
'duration': duration,
'view_count': view_count,
'average_rating': average_rating,
'age_limit': age_limit,
'genre': genre,
'creator': creator,
'artist': artist,
'release_year': release_year,
'series': series,
'episode': episode,
'season_number': season_number,
'episode_number': episode_number,
'thumbnails': thumbnails,
'subtitles': subtitles,
'formats': formats,
}
-180
View File
@@ -1,180 +0,0 @@
import re
from .common import InfoExtractor
from ..utils import (
ExtractorError,
int_or_none,
parse_age_limit,
parse_iso8601,
parse_qs,
smuggle_url,
str_or_none,
update_url_query,
)
from ..utils.traversal import traverse_obj
class CWTVIE(InfoExtractor):
IE_NAME = 'cwtv'
_VALID_URL = r'https?://(?:www\.)?cw(?:tv(?:pr)?|seed)\.com/(?:shows/)?(?:[^/]+/)+[^?]*\?.*\b(?:play|watch|guid)=(?P<id>[a-z0-9]{8}-[a-z0-9]{4}-[a-z0-9]{4}-[a-z0-9]{4}-[a-z0-9]{12})'
_TESTS = [{
'url': 'https://www.cwtv.com/shows/continuum/a-stitch-in-time/?play=9149a1e1-4cb2-46d7-81b2-47d35bbd332b',
'info_dict': {
'id': '9149a1e1-4cb2-46d7-81b2-47d35bbd332b',
'ext': 'mp4',
'title': 'A Stitch in Time',
'description': r're:(?s)City Protective Services officer Kiera Cameron is transported from 2077.+',
'thumbnail': r're:https?://.+\.jpe?g',
'duration': 2632,
'timestamp': 1736928000,
'uploader': 'CWTV',
'chapters': 'count:5',
'series': 'Continuum',
'season_number': 1,
'episode_number': 1,
'age_limit': 14,
'upload_date': '20250115',
'season': 'Season 1',
'episode': 'Episode 1',
},
'params': {
# m3u8 download
'skip_download': True,
},
}, {
'url': 'http://cwtv.com/shows/arrow/legends-of-yesterday/?play=6b15e985-9345-4f60-baf8-56e96be57c63',
'info_dict': {
'id': '6b15e985-9345-4f60-baf8-56e96be57c63',
'ext': 'mp4',
'title': 'Legends of Yesterday',
'description': r're:(?s)Oliver and Barry Allen take Kendra Saunders and Carter Hall to a remote.+',
'duration': 2665,
'series': 'Arrow',
'season_number': 4,
'season': '4',
'episode_number': 8,
'upload_date': '20151203',
'timestamp': 1449122100,
},
'params': {
# m3u8 download
'skip_download': True,
},
'skip': 'redirect to http://cwtv.com/shows/arrow/',
}, {
'url': 'http://www.cwseed.com/shows/whose-line-is-it-anyway/jeff-davis-4/?play=24282b12-ead2-42f2-95ad-26770c2c6088',
'info_dict': {
'id': '24282b12-ead2-42f2-95ad-26770c2c6088',
'ext': 'mp4',
'title': 'Jeff Davis 4',
'description': 'Jeff Davis is back to make you laugh.',
'duration': 1263,
'series': 'Whose Line Is It Anyway?',
'season_number': 11,
'episode_number': 20,
'upload_date': '20151006',
'timestamp': 1444107300,
'age_limit': 14,
'uploader': 'CWTV',
'thumbnail': r're:https?://.+\.jpe?g',
'chapters': 'count:4',
'episode': 'Episode 20',
'season': 'Season 11',
},
'params': {
# m3u8 download
'skip_download': True,
},
}, {
'url': 'http://cwtv.com/thecw/chroniclesofcisco/?play=8adebe35-f447-465f-ab52-e863506ff6d6',
'only_matching': True,
}, {
'url': 'http://cwtvpr.com/the-cw/video?watch=9eee3f60-ef4e-440b-b3b2-49428ac9c54e',
'only_matching': True,
}, {
'url': 'http://cwtv.com/shows/arrow/legends-of-yesterday/?watch=6b15e985-9345-4f60-baf8-56e96be57c63',
'only_matching': True,
}, {
'url': 'http://www.cwtv.com/movies/play/?guid=0a8e8b5b-1356-41d5-9a6a-4eda1a6feb6c',
'only_matching': True,
}]
def _real_extract(self, url):
video_id = self._match_id(url)
data = self._download_json(
f'https://images.cwtv.com/feed/app-2/video-meta/apiversion_22/device_android/guid_{video_id}', video_id)
if traverse_obj(data, 'result') != 'ok':
raise ExtractorError(traverse_obj(data, (('error_msg', 'msg'), {str}, any)), expected=True)
video_data = data['video']
title = video_data['title']
mpx_url = update_url_query(
video_data.get('mpx_url') or f'https://link.theplatform.com/s/cwtv/media/guid/2703454149/{video_id}',
{'formats': 'M3U+none'})
season = str_or_none(video_data.get('season'))
episode = str_or_none(video_data.get('episode'))
if episode and season:
episode = episode[len(season):]
return {
'_type': 'url_transparent',
'id': video_id,
'title': title,
'url': smuggle_url(mpx_url, {'force_smil_url': True}),
'description': video_data.get('description_long'),
'duration': int_or_none(video_data.get('duration_secs')),
'series': video_data.get('series_name'),
'season_number': int_or_none(season),
'episode_number': int_or_none(episode),
'timestamp': parse_iso8601(video_data.get('start_time')),
'age_limit': parse_age_limit(video_data.get('rating')),
'ie_key': 'ThePlatform',
'thumbnail': video_data.get('large_thumbnail'),
}
class CWTVMovieIE(InfoExtractor):
IE_NAME = 'cwtv:movie'
_VALID_URL = r'https?://(?:www\.)?cwtv\.com/shows/(?P<id>[\w-]+)/?\?(?:[^#]+&)?viewContext=Movies'
_TESTS = [{
'url': 'https://www.cwtv.com/shows/the-crush/?viewContext=Movies+Swimlane',
'info_dict': {
'id': '0a8e8b5b-1356-41d5-9a6a-4eda1a6feb6c',
'ext': 'mp4',
'title': 'The Crush',
'upload_date': '20241112',
'description': 'md5:1549acd90dff4a8273acd7284458363e',
'chapters': 'count:9',
'timestamp': 1731398400,
'age_limit': 16,
'duration': 5337,
'series': 'The Crush',
'season': 'Season 1',
'uploader': 'CWTV',
'season_number': 1,
'episode': 'Episode 1',
'episode_number': 1,
'thumbnail': r're:https?://.+\.jpe?g',
},
'params': {
# m3u8 download
'skip_download': True,
},
}]
_UUID_RE = r'[\da-f]{8}-(?:[\da-f]{4}-){3}[\da-f]{12}'
def _real_extract(self, url):
display_id = self._match_id(url)
webpage = self._download_webpage(url, display_id)
app_url = (
self._html_search_meta('al:ios:url', webpage, default=None)
or self._html_search_meta('al:android:url', webpage, default=None))
video_id = (
traverse_obj(parse_qs(app_url), ('video_id', 0, {lambda x: re.fullmatch(self._UUID_RE, x)}, 0))
or self._search_regex([
rf'CWTV\.Site\.curPlayingGUID\s*=\s*["\']({self._UUID_RE})',
rf'CWTV\.Site\.viewInAppURL\s*=\s*["\']/shows/[\w-]+/watch-in-app/\?play=({self._UUID_RE})',
], webpage, 'video ID'))
return self.url_result(
f'https://www.cwtv.com/shows/{display_id}/{display_id}/?play={video_id}', CWTVIE, video_id)
+26 -26
View File
@@ -18,15 +18,15 @@ from ..utils import (
class DropoutIE(InfoExtractor): class DropoutIE(InfoExtractor):
_LOGIN_URL = 'https://www.dropout.tv/login' _LOGIN_URL = 'https://watch.dropout.tv/login'
_NETRC_MACHINE = 'dropout' _NETRC_MACHINE = 'dropout'
_VALID_URL = r'https?://(?:www\.)?dropout\.tv/(?:[^/]+/)*videos/(?P<id>[^/]+)/?$' _VALID_URL = r'https?://(?:watch\.)?dropout\.tv/(?:[^/?#]+/)*videos/(?P<id>[^/?#]+)/?(?:[?#]|$)'
_TESTS = [ _TESTS = [
{ {
'url': 'https://www.dropout.tv/game-changer/season:2/videos/yes-or-no', 'url': 'https://watch.dropout.tv/game-changer/season:2/videos/yes-or-no',
'note': 'Episode in a series', 'note': 'Episode in a series',
'md5': '5e000fdfd8d8fa46ff40456f1c2af04a', 'md5': '4b76963f904f8bc4ba22dcf0e66ada06',
'info_dict': { 'info_dict': {
'id': '738153', 'id': '738153',
'display_id': 'yes-or-no', 'display_id': 'yes-or-no',
@@ -45,35 +45,35 @@ class DropoutIE(InfoExtractor):
'uploader_url': 'https://vimeo.com/user80538407', 'uploader_url': 'https://vimeo.com/user80538407',
'uploader': 'OTT Videos', 'uploader': 'OTT Videos',
}, },
'expected_warnings': ['Ignoring subtitle tracks found in the HLS manifest'], 'expected_warnings': ['Ignoring subtitle tracks found in the HLS manifest', 'Failed to parse XML: not well-formed'],
}, },
{ {
'url': 'https://www.dropout.tv/dimension-20-fantasy-high/season:1/videos/episode-1', 'url': 'https://watch.dropout.tv/tablepop-presents-megadungeon-live/season:1/videos/enter-through-the-gift-shop',
'note': 'Episode in a series (missing release_date)', 'note': 'Episode in a series (missing release_date)',
'md5': '712caf7c191f1c47c8f1879520c2fa5c', 'md5': 'b08fb03050585ea25cd7ee092db9134c',
'info_dict': { 'info_dict': {
'id': '320562', 'id': '624270',
'display_id': 'episode-1', 'display_id': 'enter-through-the-gift-shop',
'ext': 'mp4', 'ext': 'mp4',
'title': 'The Beginning Begins', 'title': 'Enter Through the Gift Shop',
'description': 'The cast introduces their PCs, including a neurotic elf, a goblin PI, and a corn-worshipping cleric.', 'description': 'A new adventuring party explores a gift shop and runs into a friendly orc -- and some angry goblins.',
'thumbnail': 'https://vhx.imgix.net/chuncensoredstaging/assets/4421ed0d-f630-4c88-9004-5251b2b8adfa.jpg', 'thumbnail': 'https://vhx.imgix.net/chuncensoredstaging/assets/a1d876c3-3dee-4cd0-87c6-27a851b1d0ec.jpg',
'series': 'Dimension 20: Fantasy High', 'series': 'TablePop Presents: MEGADUNGEON LIVE!',
'season_number': 1, 'season_number': 1,
'season': 'Season 1', 'season': 'Season 1',
'episode_number': 1, 'episode_number': 1,
'episode': 'The Beginning Begins', 'episode': 'Enter Through the Gift Shop',
'duration': 6838, 'duration': 7101,
'uploader_id': 'user80538407', 'uploader_id': 'user80538407',
'uploader_url': 'https://vimeo.com/user80538407', 'uploader_url': 'https://vimeo.com/user80538407',
'uploader': 'OTT Videos', 'uploader': 'OTT Videos',
}, },
'expected_warnings': ['Ignoring subtitle tracks found in the HLS manifest'], 'expected_warnings': ['Ignoring subtitle tracks found in the HLS manifest', 'Failed to parse XML: not well-formed'],
}, },
{ {
'url': 'https://www.dropout.tv/videos/misfits-magic-holiday-special', 'url': 'https://watch.dropout.tv/videos/misfits-magic-holiday-special',
'note': 'Episode not in a series', 'note': 'Episode not in a series',
'md5': 'c30fa18999c5880d156339f13c953a26', 'md5': '1e6428f7756b02c93b573d39ddd789fe',
'info_dict': { 'info_dict': {
'id': '1915774', 'id': '1915774',
'display_id': 'misfits-magic-holiday-special', 'display_id': 'misfits-magic-holiday-special',
@@ -87,7 +87,7 @@ class DropoutIE(InfoExtractor):
'uploader_url': 'https://vimeo.com/user80538407', 'uploader_url': 'https://vimeo.com/user80538407',
'uploader': 'OTT Videos', 'uploader': 'OTT Videos',
}, },
'expected_warnings': ['Ignoring subtitle tracks found in the HLS manifest'], 'expected_warnings': ['Ignoring subtitle tracks found in the HLS manifest', 'Failed to parse XML: not well-formed'],
}, },
] ]
@@ -125,7 +125,7 @@ class DropoutIE(InfoExtractor):
display_id = self._match_id(url) display_id = self._match_id(url)
webpage = None webpage = None
if self._get_cookies('https://www.dropout.tv').get('_session'): if self._get_cookies('https://watch.dropout.tv').get('_session'):
webpage = self._download_webpage(url, display_id) webpage = self._download_webpage(url, display_id)
if not webpage or '<div id="watch-unauthorized"' in webpage: if not webpage or '<div id="watch-unauthorized"' in webpage:
login_err = self._login(display_id) login_err = self._login(display_id)
@@ -148,7 +148,7 @@ class DropoutIE(InfoExtractor):
return { return {
'_type': 'url_transparent', '_type': 'url_transparent',
'ie_key': VHXEmbedIE.ie_key(), 'ie_key': VHXEmbedIE.ie_key(),
'url': VHXEmbedIE._smuggle_referrer(embed_url, 'https://www.dropout.tv'), 'url': VHXEmbedIE._smuggle_referrer(embed_url, 'https://watch.dropout.tv'),
'id': self._search_regex(r'embed\.vhx\.tv/videos/(.+?)\?', embed_url, 'id'), 'id': self._search_regex(r'embed\.vhx\.tv/videos/(.+?)\?', embed_url, 'id'),
'display_id': display_id, 'display_id': display_id,
'title': title, 'title': title,
@@ -167,10 +167,10 @@ class DropoutIE(InfoExtractor):
class DropoutSeasonIE(InfoExtractor): class DropoutSeasonIE(InfoExtractor):
_PAGE_SIZE = 24 _PAGE_SIZE = 24
_VALID_URL = r'https?://(?:www\.)?dropout\.tv/(?P<id>[^\/$&?#]+)(?:/?$|/season:(?P<season>[0-9]+)/?$)' _VALID_URL = r'https?://(?:watch\.)?dropout\.tv/(?P<id>[^\/$&?#]+)(?:/?$|/season:(?P<season>[0-9]+)/?$)'
_TESTS = [ _TESTS = [
{ {
'url': 'https://www.dropout.tv/dimension-20-fantasy-high/season:1', 'url': 'https://watch.dropout.tv/dimension-20-fantasy-high/season:1',
'note': 'Multi-season series with the season in the url', 'note': 'Multi-season series with the season in the url',
'playlist_count': 24, 'playlist_count': 24,
'info_dict': { 'info_dict': {
@@ -179,7 +179,7 @@ class DropoutSeasonIE(InfoExtractor):
}, },
}, },
{ {
'url': 'https://www.dropout.tv/dimension-20-fantasy-high', 'url': 'https://watch.dropout.tv/dimension-20-fantasy-high',
'note': 'Multi-season series with the season not in the url', 'note': 'Multi-season series with the season not in the url',
'playlist_count': 24, 'playlist_count': 24,
'info_dict': { 'info_dict': {
@@ -188,7 +188,7 @@ class DropoutSeasonIE(InfoExtractor):
}, },
}, },
{ {
'url': 'https://www.dropout.tv/dimension-20-shriek-week', 'url': 'https://watch.dropout.tv/dimension-20-shriek-week',
'note': 'Single-season series', 'note': 'Single-season series',
'playlist_count': 4, 'playlist_count': 4,
'info_dict': { 'info_dict': {
@@ -197,7 +197,7 @@ class DropoutSeasonIE(InfoExtractor):
}, },
}, },
{ {
'url': 'https://www.dropout.tv/breaking-news-no-laugh-newsroom/season:3', 'url': 'https://watch.dropout.tv/breaking-news-no-laugh-newsroom/season:3',
'note': 'Multi-season series with season in the url that requires pagination', 'note': 'Multi-season series with season in the url that requires pagination',
'playlist_count': 25, 'playlist_count': 25,
'info_dict': { 'info_dict': {
+2 -2
View File
@@ -7,7 +7,7 @@ from ..utils import (
class FifaIE(InfoExtractor): class FifaIE(InfoExtractor):
_VALID_URL = r'https?://www\.fifa\.com/fifaplus/(?P<locale>\w{2})/watch/([^#?]+/)?(?P<id>\w+)' _VALID_URL = r'https?://www\.fifa\.com/fifaplus/\w{2}/watch/([^#?]+/)?(?P<id>\w+)'
_TESTS = [{ _TESTS = [{
'url': 'https://www.fifa.com/fifaplus/en/watch/7on10qPcnyLajDDU3ntg6y', 'url': 'https://www.fifa.com/fifaplus/en/watch/7on10qPcnyLajDDU3ntg6y',
'info_dict': { 'info_dict': {
@@ -51,7 +51,7 @@ class FifaIE(InfoExtractor):
}] }]
def _real_extract(self, url): def _real_extract(self, url):
video_id, locale = self._match_valid_url(url).group('id', 'locale') video_id = self._match_id(url)
webpage = self._download_webpage(url, video_id) webpage = self._download_webpage(url, video_id)
preconnect_link = self._search_regex( preconnect_link = self._search_regex(
+22 -7
View File
@@ -3,15 +3,19 @@ from ..networking.exceptions import HTTPError
from ..utils import ( from ..utils import (
ExtractorError, ExtractorError,
UserNotLive, UserNotLive,
int_or_none,
join_nonempty,
parse_iso8601, parse_iso8601,
str_or_none, str_or_none,
traverse_obj,
url_or_none, url_or_none,
) )
from ..utils.traversal import traverse_obj
class FlexTVIE(InfoExtractor): class FlexTVIE(InfoExtractor):
_VALID_URL = r'https?://(?:www\.)?flextv\.co\.kr/channels/(?P<id>\d+)/live' IE_NAME = 'ttinglive'
IE_DESC = '띵라이브 (formerly FlexTV)'
_VALID_URL = r'https?://(?:www\.)?(?:ttinglive\.com|flextv\.co\.kr)/channels/(?P<id>\d+)/live'
_TESTS = [{ _TESTS = [{
'url': 'https://www.flextv.co.kr/channels/231638/live', 'url': 'https://www.flextv.co.kr/channels/231638/live',
'info_dict': { 'info_dict': {
@@ -36,21 +40,32 @@ class FlexTVIE(InfoExtractor):
try: try:
stream_data = self._download_json( stream_data = self._download_json(
f'https://api.flextv.co.kr/api/channels/{channel_id}/stream', f'https://api.ttinglive.com/api/channels/{channel_id}/stream',
channel_id, query={'option': 'all'}) channel_id, query={'option': 'all'})
except ExtractorError as e: except ExtractorError as e:
if isinstance(e.cause, HTTPError) and e.cause.status == 400: if isinstance(e.cause, HTTPError) and e.cause.status == 400:
raise UserNotLive(video_id=channel_id) raise UserNotLive(video_id=channel_id)
raise raise
playlist_url = stream_data['sources'][0]['url'] formats = []
formats, subtitles = self._extract_m3u8_formats_and_subtitles( for stream in traverse_obj(stream_data, ('sources', ..., {dict})):
playlist_url, channel_id, 'mp4') if stream.get('format') == 'ivs' and url_or_none(stream.get('url')):
formats.extend(self._extract_m3u8_formats(
stream['url'], channel_id, 'mp4', live=True, fatal=False, m3u8_id='ivs'))
for format_type in ['hls', 'flv']:
for data in traverse_obj(stream, (
'urlDetail', format_type, 'resolution', lambda _, v: url_or_none(v['url']))):
formats.append({
'format_id': join_nonempty(format_type, data.get('suffixName'), delim=''),
'url': data['url'],
'height': int_or_none(data.get('resolution')),
'ext': 'mp4' if format_type == 'hls' else 'flv',
'protocol': 'm3u8_native' if format_type == 'hls' else 'http',
})
return { return {
'id': channel_id, 'id': channel_id,
'formats': formats, 'formats': formats,
'subtitles': subtitles,
'is_live': True, 'is_live': True,
**traverse_obj(stream_data, { **traverse_obj(stream_data, {
'title': ('stream', 'title', {str}), 'title': ('stream', 'title', {str}),
+233
View File
@@ -0,0 +1,233 @@
from .common import InfoExtractor
from ..utils import int_or_none, unified_timestamp, url_or_none
from ..utils.traversal import traverse_obj
class IdagioTrackIE(InfoExtractor):
_VALID_URL = r'https?://(?:www\.)?app\.idagio\.com/recordings/\d+\?(?:[^#]+&)?trackId=(?P<id>\d+)'
_TESTS = [{
'url': 'https://app.idagio.com/recordings/30576934?trackId=30576943',
'md5': '15148bd71804b2450a2508931a116b56',
'info_dict': {
'id': '30576943',
'ext': 'mp3',
'title': 'Theme. Andante',
'duration': 82,
'composers': ['Edward Elgar'],
'artists': ['Vasily Petrenko', 'Royal Liverpool Philharmonic Orchestra'],
'genres': ['Orchestral', 'Other Orchestral Music'],
'track': 'Theme. Andante',
'timestamp': 1554474370,
'upload_date': '20190405',
},
}, {
'url': 'https://app.idagio.com/recordings/20514467?trackId=20514478&utm_source=pcl',
'md5': '3acef2ea0feadf889123b70e5a1e7fa7',
'info_dict': {
'id': '20514478',
'ext': 'mp3',
'title': 'I. Adagio sostenuto',
'duration': 316,
'composers': ['Ludwig van Beethoven'],
'artists': [],
'genres': ['Keyboard', 'Sonata (Keyboard)'],
'track': 'I. Adagio sostenuto',
'timestamp': 1518076337,
'upload_date': '20180208',
},
}]
def _real_extract(self, url):
track_id = self._match_id(url)
track_info = self._download_json(
f'https://api.idagio.com/v2.0/metadata/tracks/{track_id}',
track_id, fatal=False, expected_status=406)
if traverse_obj(track_info, 'error_code') == 'idagio.error.blocked.location':
self.raise_geo_restricted()
content_info = self._download_json(
f'https://api.idagio.com/v1.8/content/track/{track_id}', track_id,
query={
'quality': '0',
'format': '2',
'client_type': 'web-4',
})
return {
'ext': 'mp3',
'vcodec': 'none',
'id': track_id,
'url': traverse_obj(content_info, ('url', {url_or_none})),
**traverse_obj(track_info, ('result', {
'title': ('piece', 'title', {str}),
'timestamp': ('recording', 'created_at', {int_or_none(scale=1000)}),
'location': ('recording', 'location', {str}),
'duration': ('duration', {int_or_none}),
'track': ('piece', 'title', {str}),
'artists': ('recording', ('conductor', ('ensembles', ...), ('soloists', ...)), 'name', {str}, filter),
'composers': ('piece', 'workpart', 'work', 'composer', 'name', {str}, filter, all, filter),
'genres': ('piece', 'workpart', 'work', ('genre', 'subgenre'), 'title', {str}, filter),
})),
}
class IdagioPlaylistBaseIE(InfoExtractor):
"""Subclasses must set _API_URL_TMPL and define _parse_playlist_metadata"""
_PLAYLIST_ID_KEY = 'id' # vs. 'display_id'
def _entries(self, playlist_info):
for track_data in traverse_obj(playlist_info, ('tracks', lambda _, v: v['id'] and v['recording']['id'])):
track_id = track_data['id']
recording_id = track_data['recording']['id']
yield self.url_result(
f'https://app.idagio.com/recordings/{recording_id}?trackId={track_id}',
ie=IdagioTrackIE, video_id=track_id)
def _real_extract(self, url):
playlist_id = self._match_id(url)
playlist_info = self._download_json(
self._API_URL_TMPL.format(playlist_id), playlist_id)['result']
return {
'_type': 'playlist',
self._PLAYLIST_ID_KEY: playlist_id,
'entries': self._entries(playlist_info),
**self._parse_playlist_metadata(playlist_info),
}
class IdagioRecordingIE(IdagioPlaylistBaseIE):
_VALID_URL = r'https?://(?:www\.)?app\.idagio\.com/recordings/(?P<id>\d+)(?![^#]*[&?]trackId=\d+)'
_TESTS = [{
'url': 'https://app.idagio.com/recordings/30576934',
'info_dict': {
'id': '30576934',
'title': 'Variations on an Original Theme op. 36',
'composers': ['Edward Elgar'],
'artists': ['Vasily Petrenko', 'Royal Liverpool Philharmonic Orchestra'],
'genres': ['Orchestral', 'Other Orchestral Music'],
'timestamp': 1554474370,
'modified_timestamp': 1554474370,
'modified_date': '20190405',
'upload_date': '20190405',
},
'playlist_count': 15,
}]
_API_URL_TMPL = 'https://api.idagio.com/v2.0/metadata/recordings/{}'
def _parse_playlist_metadata(self, playlist_info):
return traverse_obj(playlist_info, {
'title': ('work', 'title', {str}),
'timestamp': ('created_at', {int_or_none(scale=1000)}),
'modified_timestamp': ('created_at', {int_or_none(scale=1000)}),
'location': ('location', {str}),
'artists': (('conductor', ('ensembles', ...), ('soloists', ...)), 'name', {str}),
'composers': ('work', 'composer', 'name', {str}, all),
'genres': ('work', ('genre', 'subgenre'), 'title', {str}),
'tags': ('tags', ..., {str}),
})
class IdagioAlbumIE(IdagioPlaylistBaseIE):
_VALID_URL = r'https?://(?:www\.)?app\.idagio\.com/albums/(?P<id>[\w-]+)'
_TESTS = [{
'url': 'https://app.idagio.com/albums/elgar-enigma-variations-in-the-south-serenade-for-strings',
'info_dict': {
'id': 'a9f139b8-f70d-4b8a-a9a4-5fe8d35eaf9c',
'display_id': 'elgar-enigma-variations-in-the-south-serenade-for-strings',
'title': 'Elgar: Enigma Variations, In the South, Serenade for Strings',
'description': '',
'thumbnail': 'https://idagio-images.global.ssl.fastly.net/albums/880040420521/main.jpg',
'artists': ['Vasily Petrenko', 'Royal Liverpool Philharmonic Orchestra', 'Edward Elgar'],
'timestamp': 1553817600,
'upload_date': '20190329',
'modified_timestamp': 1562566559.0,
'modified_date': '20190708',
},
'playlist_count': 19,
}, {
'url': 'https://app.idagio.com/albums/brahms-ein-deutsches-requiem-3B403DF6-62D7-4A42-807B-47173F3E0192',
'info_dict': {
'id': '2862ad4e-4a61-45ad-9ce4-7fcf0c2626fe',
'display_id': 'brahms-ein-deutsches-requiem-3B403DF6-62D7-4A42-807B-47173F3E0192',
'title': 'Brahms: Ein deutsches Requiem',
'description': '',
'thumbnail': 'https://idagio-images.global.ssl.fastly.net/albums/3149020954522/main.jpg',
'tags': ['recent-release'],
'artists': ['Sabine Devieilhe', 'Stéphane Degout', 'Raphaël Pichon', 'Pygmalion', 'Johannes Brahms'],
'timestamp': 1760054400,
'upload_date': '20251010',
'modified_timestamp': 1760101611,
'modified_date': '20251010',
},
'playlist_count': 7,
}]
_API_URL_TMPL = 'https://api.idagio.com/v2.0/metadata/albums/{}'
_PLAYLIST_ID_KEY = 'display_id'
def _parse_playlist_metadata(self, playlist_info):
return traverse_obj(playlist_info, {
'id': ('id', {str}),
'title': ('title', {str}),
'timestamp': ('publishDate', {unified_timestamp}),
'modified_timestamp': ('lastModified', {unified_timestamp}),
'thumbnail': ('imageUrl', {url_or_none}),
'description': ('description', {str}),
'artists': ('participants', ..., 'name', {str}),
'tags': ('tags', ..., {str}),
})
class IdagioPlaylistIE(IdagioPlaylistBaseIE):
_VALID_URL = r'https?://(?:www\.)?app\.idagio\.com/playlists/(?!personal/)(?P<id>[\w-]+)'
_TESTS = [{
'url': 'https://app.idagio.com/playlists/beethoven-the-most-beautiful-piano-music',
'info_dict': {
'id': '31652bec-8c5b-460e-a3f0-cf1f69817f53',
'display_id': 'beethoven-the-most-beautiful-piano-music',
'title': 'Beethoven: the most beautiful piano music',
'description': 'md5:d41bb04b8896bb69377f5c2cd9345ad1',
'thumbnail': r're:https://.+/playlists/31652bec-8c5b-460e-a3f0-cf1f69817f53/main\.jpg',
'creators': ['IDAGIO'],
},
'playlist_mincount': 16, # one entry is geo-restricted
}]
_API_URL_TMPL = 'https://api.idagio.com/v2.0/playlists/{}'
_PLAYLIST_ID_KEY = 'display_id'
def _parse_playlist_metadata(self, playlist_info):
return traverse_obj(playlist_info, {
'id': ('id', {str}),
'title': ('title', {str}),
'thumbnail': ('imageUrl', {url_or_none}),
'description': ('description', {str}),
'creators': ('curator', 'name', {str}, all),
})
class IdagioPersonalPlaylistIE(IdagioPlaylistBaseIE):
_VALID_URL = r'https?://(?:www\.)?app\.idagio\.com/playlists/personal/(?P<id>[\da-f-]+)'
_TESTS = [{
'url': 'https://app.idagio.com/playlists/personal/99dad72e-7b3a-45a4-b216-867c08046ed8',
'info_dict': {
'id': '99dad72e-7b3a-45a4-b216-867c08046ed8',
'title': 'Test',
'creators': ['1a6f16a6-4514-4d0c-b481-3a9877835626'],
'thumbnail': r're:https://.+/artists/86371/main\.jpg',
'timestamp': 1602859138,
'modified_timestamp': 1755616667,
'upload_date': '20201016',
'modified_date': '20250819',
},
'playlist_count': 100,
}]
_API_URL_TMPL = 'https://api.idagio.com/v1.0/personal-playlists/{}'
def _parse_playlist_metadata(self, playlist_info):
return traverse_obj(playlist_info, {
'title': ('title', {str}),
'thumbnail': ('image_url', {url_or_none}),
'creators': ('user_id', {str}, all),
'timestamp': ('created_at', {int_or_none(scale=1000)}),
'modified_timestamp': ('updated_at', {int_or_none(scale=1000)}),
})
+5 -5
View File
@@ -37,7 +37,7 @@ class LocoIE(InfoExtractor):
}, },
}, { }, {
'url': 'https://loco.com/stream/c64916eb-10fb-46a9-9a19-8c4b7ed064e7', 'url': 'https://loco.com/stream/c64916eb-10fb-46a9-9a19-8c4b7ed064e7',
'md5': '45ebc8a47ee1c2240178757caf8881b5', 'md5': '8b9bda03eba4d066928ae8d71f19befb',
'info_dict': { 'info_dict': {
'id': 'c64916eb-10fb-46a9-9a19-8c4b7ed064e7', 'id': 'c64916eb-10fb-46a9-9a19-8c4b7ed064e7',
'ext': 'mp4', 'ext': 'mp4',
@@ -55,9 +55,9 @@ class LocoIE(InfoExtractor):
'tags': ['Gameplay'], 'tags': ['Gameplay'],
'series': 'GTA 5', 'series': 'GTA 5',
'timestamp': 1740612872, 'timestamp': 1740612872,
'modified_timestamp': 1740613037, 'modified_timestamp': 1750948439,
'upload_date': '20250226', 'upload_date': '20250226',
'modified_date': '20250226', 'modified_date': '20250626',
}, },
}, { }, {
# Requires video authorization # Requires video authorization
@@ -123,8 +123,8 @@ class LocoIE(InfoExtractor):
def _real_extract(self, url): def _real_extract(self, url):
video_type, video_id = self._match_valid_url(url).group('type', 'id') video_type, video_id = self._match_valid_url(url).group('type', 'id')
webpage = self._download_webpage(url, video_id) webpage = self._download_webpage(url, video_id)
stream = traverse_obj(self._search_nextjs_data(webpage, video_id), ( stream = traverse_obj(self._search_nextjs_v13_data(webpage, video_id), (
'props', 'pageProps', ('liveStreamData', 'stream', 'liveStream'), {dict}, any, {require('stream info')})) ..., (None, 'ssrData'), ('liveStreamData', 'stream', 'liveStream'), {dict}, any, {require('stream info')}))
if access_token := self._get_access_token(video_id): if access_token := self._get_access_token(video_id):
self._request_webpage( self._request_webpage(
-102
View File
@@ -1,102 +0,0 @@
from .telecinco import TelecincoBaseIE
from ..utils import (
int_or_none,
parse_iso8601,
)
class MiTeleIE(TelecincoBaseIE):
IE_DESC = 'mitele.es'
_VALID_URL = r'https?://(?:www\.)?mitele\.es/(?:[^/]+/)+(?P<id>[^/]+)/player'
_TESTS = [{
'url': 'http://www.mitele.es/programas-tv/diario-de/57b0dfb9c715da65618b4afa/player',
'info_dict': {
'id': 'FhYW1iNTE6J6H7NkQRIEzfne6t2quqPg',
'ext': 'mp4',
'title': 'Diario de La redacción Programa 144',
'description': 'md5:07c35a7b11abb05876a6a79185b58d27',
'series': 'Diario de',
'season': 'Season 14',
'season_number': 14,
'episode': 'Tor, la web invisible',
'episode_number': 3,
'thumbnail': r're:(?i)^https?://.*\.jpg$',
'duration': 2913,
'age_limit': 16,
'timestamp': 1471209401,
'upload_date': '20160814',
},
'skip': 'HTTP Error 404 Not Found',
}, {
# no explicit title
'url': 'http://www.mitele.es/programas-tv/cuarto-milenio/57b0de3dc915da14058b4876/player',
'info_dict': {
'id': 'oyNG1iNTE6TAPP-JmCjbwfwJqqMMX3Vq',
'ext': 'mp4',
'title': 'Cuarto Milenio Temporada 6 Programa 226',
'description': 'md5:5ff132013f0cd968ffbf1f5f3538a65f',
'series': 'Cuarto Milenio',
'season': 'Season 6',
'season_number': 6,
'episode': 'Episode 24',
'episode_number': 24,
'thumbnail': r're:(?i)^https?://.*\.jpg$',
'duration': 7313,
'age_limit': 12,
'timestamp': 1471209021,
'upload_date': '20160814',
},
'params': {
'skip_download': True,
},
'skip': 'HTTP Error 404 Not Found',
}, {
'url': 'https://www.mitele.es/programas-tv/horizonte/temporada-5/programa-171-40_013480051/player/',
'info_dict': {
'id': '7adbe22e-cd41-4787-afa4-36f3da7c2c6f',
'ext': 'mp4',
'title': 'Horizonte Temporada 5 Programa 171',
'description': 'md5:97f1fb712c5ac27e5693a8b3c5c0c6e3',
'episode': 'Las Zonas de Bajas Emisiones, a debate',
'episode_number': 171,
'season': 'Season 5',
'season_number': 5,
'series': 'Horizonte',
'duration': 7012,
'upload_date': '20240927',
'timestamp': 1727416450,
'thumbnail': 'https://album.mediaset.es/eimg/2024/09/27/horizonte-171_9f02.jpg',
'age_limit': 12,
},
'params': {'geo_bypass_country': 'ES'},
}, {
'url': 'http://www.mitele.es/series-online/la-que-se-avecina/57aac5c1c915da951a8b45ed/player',
'only_matching': True,
}, {
'url': 'https://www.mitele.es/programas-tv/diario-de/la-redaccion/programa-144-40_1006364575251/player/',
'only_matching': True,
}]
def _real_extract(self, url):
display_id = self._match_id(url)
webpage = self._download_webpage(url, display_id)
pre_player = self._search_json(
r'window\.\$REACTBASE_STATE\.prePlayer_mtweb\s*=',
webpage, 'Pre Player', display_id)['prePlayer']
title = pre_player['title']
video_info = self._parse_content(pre_player['video'], url)
content = pre_player.get('content') or {}
info = content.get('info') or {}
video_info.update({
'title': title,
'description': info.get('synopsis'),
'series': content.get('title'),
'season_number': int_or_none(info.get('season_number')),
'episode': content.get('subtitle'),
'episode_number': int_or_none(info.get('episode_number')),
'duration': int_or_none(info.get('duration')),
'age_limit': int_or_none(info.get('rating')),
'timestamp': parse_iso8601(pre_player.get('publishedTime')),
})
return video_info
+20 -12
View File
@@ -1,3 +1,5 @@
import hashlib
from .common import InfoExtractor from .common import InfoExtractor
@@ -9,10 +11,10 @@ class MuseScoreIE(InfoExtractor):
'id': '142975', 'id': '142975',
'ext': 'mp3', 'ext': 'mp3',
'title': 'WA Mozart Marche Turque (Turkish March fingered)', 'title': 'WA Mozart Marche Turque (Turkish March fingered)',
'description': 'md5:7ede08230e4eaabd67a4a98bb54d07be', 'description': 'md5:0ca4cf6b79d7f5868a1fee74097394ab',
'thumbnail': r're:https?://(?:www\.)?musescore\.com/.*\.png[^$]+', 'thumbnail': r're:https?://cdn\.ustatik\.com/musescore/.*\.jpg',
'uploader': 'PapyPiano', 'uploader': 'PapyPiano',
'creator': 'Wolfgang Amadeus Mozart', 'creators': ['Wolfgang Amadeus Mozart'],
}, },
}, { }, {
'url': 'https://musescore.com/user/36164500/scores/6837638', 'url': 'https://musescore.com/user/36164500/scores/6837638',
@@ -20,10 +22,10 @@ class MuseScoreIE(InfoExtractor):
'id': '6837638', 'id': '6837638',
'ext': 'mp3', 'ext': 'mp3',
'title': 'Sweet Child O\' Mine Guns N\' Roses sweet child', 'title': 'Sweet Child O\' Mine Guns N\' Roses sweet child',
'description': 'md5:4dca71191c14abc312a0a4192492eace', 'description': 'md5:2cd49bd6b4e48a75a3c469d4775d5079',
'thumbnail': r're:https?://(?:www\.)?musescore\.com/.*\.png[^$]+', 'thumbnail': r're:https?://cdn\.ustatik\.com/musescore/.*\.png',
'uploader': 'roxbelviolin', 'uploader': 'roxbelviolin',
'creator': 'Guns N´Roses Arr. Roxbel Violin', 'creators': ['Guns N´Roses Arr. Roxbel Violin'],
}, },
}, { }, {
'url': 'https://musescore.com/classicman/fur-elise', 'url': 'https://musescore.com/classicman/fur-elise',
@@ -31,22 +33,28 @@ class MuseScoreIE(InfoExtractor):
'id': '33816', 'id': '33816',
'ext': 'mp3', 'ext': 'mp3',
'title': 'Für Elise Beethoven', 'title': 'Für Elise Beethoven',
'description': 'md5:49515a3556d5ecaf9fa4b2514064ac34', 'description': 'md5:e37b241c0280b33e9ac25651b815d06e',
'thumbnail': r're:https?://(?:www\.)?musescore\.com/.*\.png[^$]+', 'thumbnail': r're:https?://cdn\.ustatik\.com/musescore/.*\.jpg',
'uploader': 'ClassicMan', 'uploader': 'ClassicMan',
'creator': 'Ludwig van Beethoven (17701827)', 'creators': ['Ludwig van Beethoven (17701827)'],
}, },
}, { }, {
'url': 'https://musescore.com/minh_cuteee/scores/6555384', 'url': 'https://musescore.com/minh_cuteee/scores/6555384',
'only_matching': True, 'only_matching': True,
}] }]
@staticmethod
def _generate_auth_token(video_id):
return hashlib.md5((video_id + 'mp30gs').encode()).hexdigest()[:4]
def _real_extract(self, url): def _real_extract(self, url):
webpage = self._download_webpage(url, None) webpage = self._download_webpage(url, None)
url = self._og_search_url(webpage) or url url = self._og_search_url(webpage) or url
video_id = self._match_id(url) video_id = self._match_id(url)
mp3_url = self._download_json(f'https://musescore.com/api/jmuse?id={video_id}&index=0&type=mp3&v2=1', video_id, mp3_url = self._download_json(
headers={'authorization': '63794e5461e4cfa046edfbdddfccc1ac16daffd2'})['info']['url'] 'https://musescore.com/api/jmuse', video_id,
headers={'authorization': self._generate_auth_token(video_id)},
query={'id': video_id, 'index': '0', 'type': 'mp3'})['info']['url']
formats = [{ formats = [{
'url': mp3_url, 'url': mp3_url,
'ext': 'mp3', 'ext': 'mp3',
@@ -57,7 +65,7 @@ class MuseScoreIE(InfoExtractor):
'id': video_id, 'id': video_id,
'formats': formats, 'formats': formats,
'title': self._og_search_title(webpage), 'title': self._og_search_title(webpage),
'description': self._og_search_description(webpage), 'description': self._html_search_meta('description', webpage, 'description'),
'thumbnail': self._og_search_thumbnail(webpage), 'thumbnail': self._og_search_thumbnail(webpage),
'uploader': self._html_search_meta('musescore:author', webpage, 'uploader'), 'uploader': self._html_search_meta('musescore:author', webpage, 'uploader'),
'creator': self._html_search_meta('musescore:composer', webpage, 'composer'), 'creator': self._html_search_meta('musescore:composer', webpage, 'composer'),
+10 -3
View File
@@ -50,7 +50,14 @@ class NewsPicksIE(InfoExtractor):
webpage = self._download_webpage(url, video_id) webpage = self._download_webpage(url, video_id)
fragment = self._search_nextjs_data(webpage, video_id)['props']['pageProps']['fragment'] fragment = self._search_nextjs_data(webpage, video_id)['props']['pageProps']['fragment']
m3u8_url = traverse_obj(fragment, ('movie', 'movieUrl', {url_or_none}, {require('m3u8 URL')})) movie = fragment['movie']
if traverse_obj(movie, ('viewable', {str})) == 'PARTIAL_FREE' and not traverse_obj(movie, ('canWatch', {bool})):
self.report_warning(
'Full video is for Premium members. Without cookies, '
f'only the preview is downloaded. {self._login_hint()}', video_id)
m3u8_url = traverse_obj(movie, ('movieUrl', {url_or_none}, {require('m3u8 URL')}))
formats, subtitles = self._extract_m3u8_formats_and_subtitles(m3u8_url, video_id, 'mp4') formats, subtitles = self._extract_m3u8_formats_and_subtitles(m3u8_url, video_id, 'mp4')
return { return {
@@ -59,12 +66,12 @@ class NewsPicksIE(InfoExtractor):
'series': traverse_obj(fragment, ('series', 'title', {str})), 'series': traverse_obj(fragment, ('series', 'title', {str})),
'series_id': series_id, 'series_id': series_id,
'subtitles': subtitles, 'subtitles': subtitles,
**traverse_obj(fragment, ('movie', { **traverse_obj(movie, {
'title': ('title', {str}), 'title': ('title', {str}),
'cast': ('relatedUsers', ..., 'displayName', {str}, filter, all, filter), 'cast': ('relatedUsers', ..., 'displayName', {str}, filter, all, filter),
'description': ('explanation', {clean_html}), 'description': ('explanation', {clean_html}),
'release_timestamp': ('onAirStartDate', {parse_iso8601}), 'release_timestamp': ('onAirStartDate', {parse_iso8601}),
'thumbnail': (('image', 'coverImageUrl'), {url_or_none}, any), 'thumbnail': (('image', 'coverImageUrl'), {url_or_none}, any),
'timestamp': ('published', {parse_iso8601}), 'timestamp': ('published', {parse_iso8601}),
})), }),
} }
+1 -1
View File
@@ -129,7 +129,7 @@ class NownessSeriesIE(NownessBaseIE):
} }
def _real_extract(self, url): def _real_extract(self, url):
display_id, series = self._api_request(url, 'series/getBySlug/%s') _, series = self._api_request(url, 'series/getBySlug/%s')
entries = [self._extract_url_result(post) for post in series['posts']] entries = [self._extract_url_result(post) for post in series['posts']]
series_title = None series_title = None
series_description = None series_description = None
+151
View File
@@ -0,0 +1,151 @@
import base64
import json
from .common import InfoExtractor
from ..networking.exceptions import HTTPError
from ..utils import (
ExtractorError,
clean_html,
int_or_none,
parse_qs,
str_or_none,
strftime_or_none,
update_url,
update_url_query,
url_or_none,
)
from ..utils.traversal import traverse_obj
class OnsenIE(InfoExtractor):
IE_NAME = 'onsen'
IE_DESC = 'インターネットラジオステーション<音泉>'
_BASE_URL = 'https://www.onsen.ag'
_HEADERS = {'Referer': f'{_BASE_URL}/'}
_NETRC_MACHINE = 'onsen'
_VALID_URL = r'https?://(?:(?:share|www)\.)onsen\.ag/program/(?P<id>[^/?#]+)'
_TESTS = [{
'url': 'https://share.onsen.ag/program/onsenking?p=90&c=MTA0NjI',
'info_dict': {
'id': '10462',
'ext': 'm4a',
'title': '第SP回',
'cast': 'count:3',
'description': 'md5:de62c80a41c4c8d84da53a1ee681ad18',
'display_id': 'MTA0NjI=',
'media_type': 'sound',
'section_start': 0,
'series': '音泉キング「下野紘」のラジオ きみはもちろん、<音泉>ファミリーだよね?',
'series_id': 'onsenking',
'tags': 'count:2',
'thumbnail': r're:https?://d3bzklg4lms4gh\.cloudfront\.net/program_info/image/default/production/.+',
'upload_date': '20220627',
'webpage_url': 'https://www.onsen.ag/program/onsenking?c=MTA0NjI=',
},
}, {
'url': 'https://share.onsen.ag/program/girls-band-cry-radio?p=370&c=MTgwMDE',
'info_dict': {
'id': '18001',
'ext': 'mp4',
'title': '第4回',
'cast': 'count:5',
'description': 'md5:bbca8a389d99c90cbbce8f383c85fedd',
'display_id': 'MTgwMDE=',
'media_type': 'movie',
'section_start': 0,
'series': 'TVアニメ『ガールズバンドクライ』WEBラジオ「ガールズバンドクライ~ラジオにも全部ぶち込め。~」',
'series_id': 'girls-band-cry-radio',
'tags': 'count:3',
'thumbnail': r're:https?://d3bzklg4lms4gh\.cloudfront\.net/program_info/image/default/production/.+',
'upload_date': '20240425',
'webpage_url': 'https://www.onsen.ag/program/girls-band-cry-radio?c=MTgwMDE=',
},
'skip': 'Only available for premium supporters',
}, {
'url': 'https://www.onsen.ag/program/uma',
'info_dict': {
'id': 'uma',
'title': 'UMA YELL RADIO',
},
'playlist_mincount': 35,
}]
@staticmethod
def _get_encoded_id(program):
return base64.urlsafe_b64encode(str(program['id']).encode()).decode()
def _perform_login(self, username, password):
sign_in = self._download_json(
f'{self._BASE_URL}/web_api/signin', None, 'Logging in', headers={
'Accept': 'application/json',
'Content-Type': 'application/json',
}, data=json.dumps({
'session': {
'email': username,
'password': password,
},
}).encode(), expected_status=401)
if sign_in.get('error'):
raise ExtractorError('Invalid username or password', expected=True)
def _real_extract(self, url):
program_id = self._match_id(url)
try:
programs = self._download_json(
f'{self._BASE_URL}/web_api/programs/{program_id}', program_id)
except ExtractorError as e:
if isinstance(e.cause, HTTPError) and e.cause.status == 404:
raise ExtractorError('Invalid URL', expected=True)
raise
query = {k: v[-1] for k, v in parse_qs(url).items() if v}
if 'c' not in query:
entries = [
self.url_result(update_url_query(url, {'c': self._get_encoded_id(program)}), OnsenIE)
for program in traverse_obj(programs, ('contents', lambda _, v: v['id']))
]
return self.playlist_result(
entries, program_id, traverse_obj(programs, ('program_info', 'title', {clean_html})))
raw_id = base64.urlsafe_b64decode(f'{query["c"]}===').decode()
p_keys = ('contents', lambda _, v: v['id'] == int(raw_id))
program = traverse_obj(programs, (*p_keys, any))
if not program:
raise ExtractorError(
'This program is no longer available', expected=True)
m3u8_url = traverse_obj(program, ('streaming_url', {url_or_none}))
if not m3u8_url:
self.raise_login_required(
'This program is only available for premium supporters')
display_id = self._get_encoded_id(program)
date_str = self._search_regex(
rf'{program_id}0?(\d{{6}})', m3u8_url, 'date string', default=None)
return {
'display_id': display_id,
'formats': self._extract_m3u8_formats(m3u8_url, raw_id, headers=self._HEADERS),
'http_headers': self._HEADERS,
'section_start': int_or_none(query.get('t', 0)),
'upload_date': strftime_or_none(f'20{date_str}'),
'webpage_url': f'{self._BASE_URL}/program/{program_id}?c={display_id}',
**traverse_obj(program, {
'id': ('id', {int}, {str_or_none}),
'title': ('title', {clean_html}),
'media_type': ('media_type', {str}),
'thumbnail': ('poster_image_url', {url_or_none}, {update_url(query=None)}),
}),
**traverse_obj(programs, {
'cast': (('performers', (*p_keys, 'guests')), ..., 'name', {str}, filter),
'series_id': ('directory_name', {str}),
}),
**traverse_obj(programs, ('program_info', {
'description': ('description', {clean_html}, filter),
'series': ('title', {clean_html}),
'tags': ('hashtag_list', ..., {str}, filter),
})),
}
-201
View File
@@ -1,201 +0,0 @@
import itertools
from .cbs import CBSBaseIE
from .common import InfoExtractor
from ..utils import (
ExtractorError,
int_or_none,
url_or_none,
)
class ParamountPlusIE(CBSBaseIE):
_VALID_URL = r'''(?x)
(?:
paramountplus:|
https?://(?:www\.)?(?:
paramountplus\.com/(?:shows|movies)/(?:video|[^/]+/video|[^/]+)/
)(?P<id>[\w-]+))'''
# All tests are blocked outside US
_TESTS = [{
'url': 'https://www.paramountplus.com/shows/video/Oe44g5_NrlgiZE3aQVONleD6vXc8kP0k/',
'info_dict': {
'id': 'Oe44g5_NrlgiZE3aQVONleD6vXc8kP0k',
'ext': 'mp4',
'title': 'CatDog - Climb Every CatDog/The Canine Mutiny',
'description': 'md5:7ac835000645a69933df226940e3c859',
'duration': 1426,
'timestamp': 920264400,
'upload_date': '19990301',
'uploader': 'CBSI-NEW',
'episode_number': 5,
'thumbnail': r're:https?://.+\.jpg$',
'season': 'Season 2',
'chapters': 'count:3',
'episode': 'Episode 5',
'season_number': 2,
'series': 'CatDog',
},
'params': {
'skip_download': 'm3u8',
},
}, {
'url': 'https://www.paramountplus.com/shows/video/6hSWYWRrR9EUTz7IEe5fJKBhYvSUfexd/',
'info_dict': {
'id': '6hSWYWRrR9EUTz7IEe5fJKBhYvSUfexd',
'ext': 'mp4',
'title': '7/23/21 WEEK IN REVIEW (Rep. Jahana Hayes/Howard Fineman/Sen. Michael Bennet/Sheera Frenkel & Cecilia Kang)',
'description': 'md5:f4adcea3e8b106192022e121f1565bae',
'duration': 2506,
'timestamp': 1627063200,
'upload_date': '20210723',
'uploader': 'CBSI-NEW',
'episode_number': 81,
'thumbnail': r're:https?://.+\.jpg$',
'season': 'Season 2',
'chapters': 'count:4',
'episode': 'Episode 81',
'season_number': 2,
'series': 'Tooning Out The News',
},
'params': {
'skip_download': 'm3u8',
},
}, {
'url': 'https://www.paramountplus.com/movies/video/vM2vm0kE6vsS2U41VhMRKTOVHyQAr6pC/',
'info_dict': {
'id': 'vM2vm0kE6vsS2U41VhMRKTOVHyQAr6pC',
'ext': 'mp4',
'title': 'Daddy\'s Home',
'upload_date': '20151225',
'description': 'md5:9a6300c504d5e12000e8707f20c54745',
'uploader': 'CBSI-NEW',
'timestamp': 1451030400,
'thumbnail': r're:https?://.+\.jpg$',
'chapters': 'count:0',
'duration': 5761,
'series': 'Paramount+ Movies',
},
'params': {
'skip_download': 'm3u8',
},
'skip': 'DRM',
}, {
'url': 'https://www.paramountplus.com/movies/video/5EKDXPOzdVf9voUqW6oRuocyAEeJGbEc/',
'info_dict': {
'id': '5EKDXPOzdVf9voUqW6oRuocyAEeJGbEc',
'ext': 'mp4',
'uploader': 'CBSI-NEW',
'description': 'md5:bc7b6fea84ba631ef77a9bda9f2ff911',
'timestamp': 1577865600,
'title': 'Sonic the Hedgehog',
'upload_date': '20200101',
'thumbnail': r're:https?://.+\.jpg$',
'chapters': 'count:0',
'duration': 5932,
'series': 'Paramount+ Movies',
},
'params': {
'skip_download': 'm3u8',
},
'skip': 'DRM',
}, {
'url': 'https://www.paramountplus.com/shows/the-real-world/video/mOVeHeL9ub9yWdyzSZFYz8Uj4ZBkVzQg/the-real-world-reunion/',
'only_matching': True,
}, {
'url': 'https://www.paramountplus.com/shows/video/mOVeHeL9ub9yWdyzSZFYz8Uj4ZBkVzQg/',
'only_matching': True,
}, {
'url': 'https://www.paramountplus.com/movies/video/W0VyStQqUnqKzJkrpSAIARuCc9YuYGNy/',
'only_matching': True,
}, {
'url': 'https://www.paramountplus.com/movies/paw-patrol-the-movie/W0VyStQqUnqKzJkrpSAIARuCc9YuYGNy/',
'only_matching': True,
}]
def _extract_video_info(self, content_id, mpx_acc=2198311517):
items_data = self._download_json(
f'https://www.paramountplus.com/apps-api/v2.0/androidtv/video/cid/{content_id}.json',
content_id, query={
'locale': 'en-us',
'at': 'ABCXgPuoStiPipsK0OHVXIVh68zNys+G4f7nW9R6qH68GDOcneW6Kg89cJXGfiQCsj0=',
}, headers=self.geo_verification_headers())
asset_types = {
item.get('assetType'): {
'format': 'SMIL',
'formats': 'M3U+none,MPEG4', # '+none' specifies ProtectionScheme (no DRM)
} for item in items_data['itemList']
}
item = items_data['itemList'][-1]
info, error = {}, None
metadata = {
'title': item.get('title'),
'series': item.get('seriesTitle'),
'season_number': int_or_none(item.get('seasonNum')),
'episode_number': int_or_none(item.get('episodeNum')),
'duration': int_or_none(item.get('duration')),
'thumbnail': url_or_none(item.get('thumbnail')),
}
try:
info = self._extract_common_video_info(content_id, asset_types, mpx_acc, extra_info=metadata)
except ExtractorError as e:
error = e
# Check for DRM formats to give appropriate error
if not info.get('formats'):
for query in asset_types.values():
query['formats'] = 'MPEG-DASH,M3U,MPEG4' # allows DRM formats
try:
drm_info = self._extract_common_video_info(content_id, asset_types, mpx_acc, extra_info=metadata)
except ExtractorError:
if error:
raise error from None
raise
if drm_info['formats']:
self.report_drm(content_id)
elif error:
raise error
return info
class ParamountPlusSeriesIE(InfoExtractor):
_VALID_URL = r'https?://(?:www\.)?paramountplus\.com/shows/(?P<id>[a-zA-Z0-9-_]+)/?(?:[#?]|$)'
_TESTS = [{
'url': 'https://www.paramountplus.com/shows/drake-josh',
'playlist_mincount': 50,
'info_dict': {
'id': 'drake-josh',
},
}, {
'url': 'https://www.paramountplus.com/shows/hawaii_five_0/',
'playlist_mincount': 240,
'info_dict': {
'id': 'hawaii_five_0',
},
}, {
'url': 'https://www.paramountplus.com/shows/spongebob-squarepants/',
'playlist_mincount': 248,
'info_dict': {
'id': 'spongebob-squarepants',
},
}]
def _entries(self, show_name):
for page in itertools.count():
show_json = self._download_json(
f'https://www.paramountplus.com/shows/{show_name}/xhr/episodes/page/{page}/size/50/xs/0/season/0', show_name)
if not show_json.get('success'):
return
for episode in show_json['result']['data']:
yield self.url_result(
'https://www.paramountplus.com{}'.format(episode['url']),
ie=ParamountPlusIE.ie_key(), video_id=episode['content_id'])
def _real_extract(self, url):
show_name = self._match_id(url)
return self.playlist_result(self._entries(show_name), playlist_id=show_name)
-119
View File
@@ -1,119 +0,0 @@
from .common import InfoExtractor
from ..networking.exceptions import HTTPError
from ..utils import (
ExtractorError,
traverse_obj,
unified_timestamp,
)
class PixivSketchBaseIE(InfoExtractor):
def _call_api(self, video_id, path, referer, note='Downloading JSON metadata'):
response = self._download_json(f'https://sketch.pixiv.net/api/{path}', video_id, note=note, headers={
'Referer': referer,
'X-Requested-With': referer,
})
errors = traverse_obj(response, ('errors', ..., 'message'))
if errors:
raise ExtractorError(' '.join(f'{e}.' for e in errors))
return response.get('data') or {}
class PixivSketchIE(PixivSketchBaseIE):
IE_NAME = 'pixiv:sketch'
_VALID_URL = r'https?://sketch\.pixiv\.net/@(?P<uploader_id>[a-zA-Z0-9_-]+)/lives/(?P<id>\d+)/?'
_TESTS = [{
'url': 'https://sketch.pixiv.net/@nuhutya/lives/3654620468641830507',
'info_dict': {
'id': '7370666691623196569',
'title': 'まにあえクリスマス!',
'uploader': 'ぬふちゃ',
'uploader_id': 'nuhutya',
'channel_id': '9844815',
'age_limit': 0,
'timestamp': 1640351536,
},
'skip': True,
}, {
# these two (age_limit > 0) requires you to login on website, but it's actually not required for download
'url': 'https://sketch.pixiv.net/@namahyou/lives/4393103321546851377',
'info_dict': {
'id': '4907995960957946943',
'title': 'クリスマスなんて知らん🖕',
'uploader': 'すゃもり',
'uploader_id': 'suya2mori2',
'channel_id': '31169300',
'age_limit': 15,
'timestamp': 1640347640,
},
'skip': True,
}, {
'url': 'https://sketch.pixiv.net/@8aki/lives/3553803162487249670',
'info_dict': {
'id': '1593420639479156945',
'title': 'おまけ本作業(リョナ有)',
'uploader': 'おぶい / Obui',
'uploader_id': 'oving',
'channel_id': '17606',
'age_limit': 18,
'timestamp': 1640330263,
},
'skip': True,
}]
def _real_extract(self, url):
video_id, uploader_id = self._match_valid_url(url).group('id', 'uploader_id')
data = self._call_api(video_id, f'lives/{video_id}.json', url)
if not traverse_obj(data, 'is_broadcasting'):
raise ExtractorError(f'This live is offline. Use https://sketch.pixiv.net/@{uploader_id} for ongoing live.', expected=True)
m3u8_url = traverse_obj(data, ('owner', 'hls_movie', 'url'))
formats = self._extract_m3u8_formats(
m3u8_url, video_id, ext='mp4',
entry_protocol='m3u8_native', m3u8_id='hls')
return {
'id': video_id,
'title': data.get('name'),
'formats': formats,
'uploader': traverse_obj(data, ('user', 'name'), ('owner', 'user', 'name')),
'uploader_id': traverse_obj(data, ('user', 'unique_name'), ('owner', 'user', 'unique_name')),
'channel_id': str(traverse_obj(data, ('user', 'pixiv_user_id'), ('owner', 'user', 'pixiv_user_id'))),
'age_limit': 18 if data.get('is_r18') else 15 if data.get('is_r15') else 0,
'timestamp': unified_timestamp(data.get('created_at')),
'is_live': True,
}
class PixivSketchUserIE(PixivSketchBaseIE):
IE_NAME = 'pixiv:sketch:user'
_VALID_URL = r'https?://sketch\.pixiv\.net/@(?P<id>[a-zA-Z0-9_-]+)/?'
_TESTS = [{
'url': 'https://sketch.pixiv.net/@nuhutya',
'only_matching': True,
}, {
'url': 'https://sketch.pixiv.net/@namahyou',
'only_matching': True,
}, {
'url': 'https://sketch.pixiv.net/@8aki',
'only_matching': True,
}]
@classmethod
def suitable(cls, url):
return super().suitable(url) and not PixivSketchIE.suitable(url)
def _real_extract(self, url):
user_id = self._match_id(url)
data = self._call_api(user_id, f'lives/users/@{user_id}.json', url)
if not traverse_obj(data, 'is_broadcasting'):
try:
self._call_api(user_id, 'users/current.json', url, 'Investigating reason for request failure')
except ExtractorError as e:
if isinstance(e.cause, HTTPError) and e.cause.status == 401:
self.raise_login_required(f'Please log in, or use direct link like https://sketch.pixiv.net/@{user_id}/1234567890', method='cookies')
raise ExtractorError('This user is offline', expected=True)
return self.url_result(f'https://sketch.pixiv.net/@{user_id}/lives/{data["id"]}')
+53 -22
View File
@@ -1,8 +1,8 @@
import json import json
from .common import InfoExtractor from .common import InfoExtractor
from ..utils import float_or_none, parse_iso8601, str_or_none, try_call from ..utils import float_or_none, parse_iso8601, str_or_none, try_call, url_or_none
from ..utils.traversal import traverse_obj from ..utils.traversal import traverse_obj, value
class PrankCastIE(InfoExtractor): class PrankCastIE(InfoExtractor):
@@ -100,9 +100,38 @@ class PrankCastPostIE(InfoExtractor):
'duration': 263.287, 'duration': 263.287,
'cast': ['despicabledogs'], 'cast': ['despicabledogs'],
'description': 'https://imgur.com/a/vtxLvKU', 'description': 'https://imgur.com/a/vtxLvKU',
'categories': [],
'upload_date': '20240104', 'upload_date': '20240104',
}, },
}, {
'url': 'https://prankcast.com/drtomservo/posts/11988-butteye-s-late-night-stank-episode-1-part-1-',
'info_dict': {
'id': '11988',
'ext': 'mp3',
'title': 'Butteye\'s Late Night Stank Episode 1 (Part 1)',
'display_id': 'butteye-s-late-night-stank-episode-1-part-1-',
'timestamp': 1754238686,
'uploader': 'DrTomServo',
'channel_id': '136',
'duration': 2176.464,
'cast': ['DrTomServo'],
'description': '',
'upload_date': '20250803',
},
}, {
'url': 'https://prankcast.com/drtomservo/posts/12105-butteye-s-late-night-stank-episode-08-16-2025-part-2',
'info_dict': {
'id': '12105',
'ext': 'mp3',
'title': 'Butteye\'s Late Night Stank Episode 08-16-2025 Part 2',
'display_id': 'butteye-s-late-night-stank-episode-08-16-2025-part-2',
'timestamp': 1755453505,
'uploader': 'DrTomServo',
'channel_id': '136',
'duration': 19018.392,
'cast': ['DrTomServo'],
'description': '',
'upload_date': '20250817',
},
}] }]
def _real_extract(self, url): def _real_extract(self, url):
@@ -112,26 +141,28 @@ class PrankCastPostIE(InfoExtractor):
post = self._search_nextjs_data(webpage, video_id)['props']['pageProps']['ssr_data_posts'] post = self._search_nextjs_data(webpage, video_id)['props']['pageProps']['ssr_data_posts']
content = self._parse_json(post['post_contents_json'], video_id)[0] content = self._parse_json(post['post_contents_json'], video_id)[0]
uploader = post.get('user_name')
guests_json = traverse_obj(content, ('guests_json', {json.loads}, {dict})) or {}
return { return {
'id': video_id, 'id': video_id,
'title': post.get('post_title') or self._og_search_title(webpage),
'display_id': display_id, 'display_id': display_id,
'url': content.get('url'), 'title': self._og_search_title(webpage),
'timestamp': parse_iso8601(content.get('start_date') or content.get('crdate'), ' '), **traverse_obj(post, {
'uploader': uploader, 'title': ('post_title', {str}),
'channel_id': str_or_none(post.get('user_id')), 'description': ('post_body', {str}),
'duration': float_or_none(content.get('duration')), 'tags': ('post_tags', {lambda x: x.split(',')}, ..., {str.strip}, filter),
'cast': list(filter(None, [uploader, *traverse_obj(guests_json, (..., 'name'))])), 'channel_id': ('user_id', {int}, {str_or_none}),
'description': post.get('post_body'), 'uploader': ('user_name', {str}),
'categories': list(filter(None, [content.get('category')])), }),
'tags': try_call(lambda: list(filter('', post['post_tags'].split(',')))), **traverse_obj(content, {
'subtitles': { 'url': (('secure_url', 'url'), {url_or_none}, any),
'live_chat': [{ 'timestamp': ((
'url': f'https://prankcast.com/api/private/chat/select-broadcast?id={post["content_id"]}&cache=', (('start_date', 'crdate'), {parse_iso8601(delimiter=' ')}),
'ext': 'json', ('created_at', {parse_iso8601}),
}], ), any),
} if post.get('content_id') else None, 'duration': ('duration', {float_or_none}),
'categories': ('category', {str}, filter, all, filter),
'cast': ((
{value(post.get('user_name'))},
('guests_json', {json.loads}, ..., 'name'),
), {str}, filter),
}),
} }
+2 -2
View File
@@ -414,7 +414,7 @@ class RadioFranceProgramScheduleIE(RadioFranceBaseIE):
_VALID_URL = rf'''(?x) _VALID_URL = rf'''(?x)
{RadioFranceBaseIE._VALID_URL_BASE} {RadioFranceBaseIE._VALID_URL_BASE}
/(?P<station>{RadioFranceBaseIE._STATIONS_RE}) /(?P<station>{RadioFranceBaseIE._STATIONS_RE})
/grille-programmes(?:\?date=(?P<date>[\d-]+))? /grille-programmes
''' '''
_TESTS = [{ _TESTS = [{
@@ -463,7 +463,7 @@ class RadioFranceProgramScheduleIE(RadioFranceBaseIE):
})) }))
def _real_extract(self, url): def _real_extract(self, url):
station, date = self._match_valid_url(url).group('station', 'date') station = self._match_valid_url(url).group('station')
webpage = self._download_webpage(url, station) webpage = self._download_webpage(url, station)
grid_data = self._extract_data_from_webpage(webpage, station, 'grid') grid_data = self._extract_data_from_webpage(webpage, station, 'grid')
upload_date = strftime_or_none(grid_data.get('date'), '%Y%m%d') upload_date = strftime_or_none(grid_data.get('date'), '%Y%m%d')
+1 -1
View File
@@ -321,7 +321,7 @@ class RCTIPlusSeriesIE(RCTIPlusBaseIE):
f'Only {video_type} will be downloaded. ' f'Only {video_type} will be downloaded. '
f'To download everything from the series, remove "/{video_type}" from the URL') f'To download everything from the series, remove "/{video_type}" from the URL')
series_meta, meta_paths = self._call_api( series_meta, _ = self._call_api(
f'https://api.rctiplus.com/api/v1/program/{series_id}/detail', display_id, 'Downloading series metadata') f'https://api.rctiplus.com/api/v1/program/{series_id}/detail', display_id, 'Downloading series metadata')
metadata = { metadata = {
'age_limit': try_get(series_meta, lambda x: self._AGE_RATINGS[x['age_restriction'][0]['code']]), 'age_limit': try_get(series_meta, lambda x: self._AGE_RATINGS[x['age_restriction'][0]['code']]),
-191
View File
@@ -1,191 +0,0 @@
import re
from .common import InfoExtractor
from ..utils import ExtractorError, int_or_none, str_to_int
class RUTVIE(InfoExtractor):
IE_DESC = 'RUTV.RU'
_VALID_URL = r'''(?x)
https?://
(?:test)?player\.(?:rutv\.ru|vgtrk\.com)/
(?P<path>
flash\d+v/container\.swf\?id=|
iframe/(?P<type>swf|video|live)/id/|
index/iframe/cast_id/
)
(?P<id>\d+)
'''
_EMBED_REGEX = [
r'<iframe[^>]+?src=(["\'])(?P<url>https?://(?:test)?player\.(?:rutv\.ru|vgtrk\.com)/(?:iframe/(?:swf|video|live)/id|index/iframe/cast_id)/.+?)\1',
r'<meta[^>]+?property=(["\'])og:video\1[^>]+?content=(["\'])(?P<url>https?://(?:test)?player\.(?:rutv\.ru|vgtrk\.com)/flash\d+v/container\.swf\?id=.+?\2)',
]
_TESTS = [{
'url': 'http://player.rutv.ru/flash2v/container.swf?id=774471&sid=kultura&fbv=true&isPlay=true&ssl=false&i=560&acc_video_id=episode_id/972347/video_id/978186/brand_id/31724',
'info_dict': {
'id': '774471',
'ext': 'mp4',
'title': 'Монологи на все времена. Концерт',
'description': 'md5:18d8b5e6a41fb1faa53819471852d5d5',
'duration': 2906,
'thumbnail': r're:https?://cdn-st2\.smotrim\.ru/.+\.jpg',
},
'params': {'skip_download': 'm3u8'},
}, {
'url': 'https://player.vgtrk.com/flash2v/container.swf?id=774016&sid=russiatv&fbv=true&isPlay=true&ssl=false&i=560&acc_video_id=episode_id/972098/video_id/977760/brand_id/57638',
'info_dict': {
'id': '774016',
'ext': 'mp4',
'title': 'Чужой в семье Сталина',
'description': '',
'duration': 2539,
},
'skip': 'Invalid URL',
}, {
'url': 'http://player.rutv.ru/iframe/swf/id/766888/sid/hitech/?acc_video_id=4000',
'info_dict': {
'id': '766888',
'ext': 'mp4',
'title': 'Вести.net: интернет-гиганты начали перетягивание программных "одеял"',
'description': 'md5:65ddd47f9830c4f42ed6475f8730c995',
'duration': 279,
'thumbnail': r're:https?://cdn-st2\.smotrim\.ru/.+\.jpg',
},
'params': {'skip_download': 'm3u8'},
}, {
'url': 'http://player.rutv.ru/iframe/video/id/771852/start_zoom/true/showZoomBtn/false/sid/russiatv/?acc_video_id=episode_id/970443/video_id/975648/brand_id/5169',
'info_dict': {
'id': '771852',
'ext': 'mp4',
'title': 'Прямой эфир. Жертвы загадочной болезни: смерть от старости в 17 лет',
'description': 'md5:b81c8c55247a4bd996b43ce17395b2d8',
'duration': 3096,
'thumbnail': r're:https?://cdn-st2\.smotrim\.ru/.+\.jpg',
},
'params': {'skip_download': 'm3u8'},
}, {
'url': 'http://player.rutv.ru/iframe/live/id/51499/showZoomBtn/false/isPlay/true/sid/sochi2014',
'info_dict': {
'id': '51499',
'ext': 'flv',
'title': 'Сочи-2014. Биатлон. Индивидуальная гонка. Мужчины ',
'description': 'md5:9e0ed5c9d2fa1efbfdfed90c9a6d179c',
},
'skip': 'Invalid URL',
}, {
'url': 'http://player.rutv.ru/iframe/live/id/21/showZoomBtn/false/isPlay/true/',
'info_dict': {
'id': '21',
'ext': 'mp4',
'title': str,
'is_live': True,
},
'skip': 'Invalid URL',
}, {
'url': 'https://testplayer.vgtrk.com/iframe/live/id/19201/showZoomBtn/false/isPlay/true/',
'only_matching': True,
}]
_WEBPAGE_TESTS = [{
'url': 'http://istoriya-teatra.ru/news/item/f00/s05/n0000545/index.shtml',
'info_dict': {
'id': '1952012',
'ext': 'mp4',
'title': 'Новости культуры. Эфир от 10.10.2019 (23:30). Театр Сатиры отмечает день рождения премьерой',
'description': 'md5:fced27112ff01ff8fc4a452fc088bad6',
'duration': 191,
'thumbnail': r're:https?://cdn-st2\.smotrim\.ru/.+\.jpg',
},
'params': {'skip_download': 'm3u8'},
}]
def _real_extract(self, url):
mobj = self._match_valid_url(url)
video_id = mobj.group('id')
video_path = mobj.group('path')
if re.match(r'flash\d+v', video_path):
video_type = 'video'
elif video_path.startswith('iframe'):
video_type = mobj.group('type')
if video_type == 'swf':
video_type = 'video'
elif video_path.startswith('index/iframe/cast_id'):
video_type = 'live'
is_live = video_type == 'live'
json_data = self._download_json(
'http://player.vgtrk.com/iframe/data{}/id/{}'.format('live' if is_live else 'video', video_id),
video_id, 'Downloading JSON')
if json_data['errors']:
raise ExtractorError('{} said: {}'.format(self.IE_NAME, json_data['errors']), expected=True)
playlist = json_data['data']['playlist']
medialist = playlist['medialist']
media = medialist[0]
if media['errors']:
raise ExtractorError('{} said: {}'.format(self.IE_NAME, media['errors']), expected=True)
view_count = int_or_none(playlist.get('count_views'))
priority_transport = playlist['priority_transport']
thumbnail = media['picture']
width = int_or_none(media['width'])
height = int_or_none(media['height'])
description = media['anons']
title = media['title']
duration = int_or_none(media.get('duration'))
formats = []
subtitles = {}
for transport, links in media['sources'].items():
for quality, url in links.items():
preference = -1 if priority_transport == transport else -2
if transport == 'rtmp':
mobj = re.search(r'^(?P<url>rtmp://[^/]+/(?P<app>.+))/(?P<playpath>.+)$', url)
if not mobj:
continue
fmt = {
'url': mobj.group('url'),
'play_path': mobj.group('playpath'),
'app': mobj.group('app'),
'page_url': 'http://player.rutv.ru',
'player_url': 'http://player.rutv.ru/flash3v/osmf.swf?i=22',
'rtmp_live': True,
'ext': 'flv',
'vbr': str_to_int(quality),
}
elif transport == 'm3u8':
fmt, subs = self._extract_m3u8_formats_and_subtitles(
url, video_id, 'mp4', quality=preference, m3u8_id='hls')
formats.extend(fmt)
self._merge_subtitles(subs, target=subtitles)
continue
else:
fmt = {
'url': url,
}
fmt.update({
'width': int_or_none(quality, default=height, invscale=width, scale=height),
'height': int_or_none(quality, default=height),
'format_id': f'{transport}-{quality}',
'source_preference': preference,
})
formats.append(fmt)
return {
'id': video_id,
'title': title,
'description': description,
'thumbnail': thumbnail,
'view_count': view_count,
'duration': duration,
'formats': formats,
'subtitles': subtitles,
'is_live': is_live,
'_format_sort_fields': ('source', ),
}
-119
View File
@@ -1,119 +0,0 @@
from .common import InfoExtractor
from ..utils import (
determine_ext,
int_or_none,
parse_qs,
qualities,
try_get,
)
class SixPlayIE(InfoExtractor):
IE_NAME = '6play'
_VALID_URL = r'(?:6play:|https?://(?:www\.)?(?P<domain>6play\.fr|rtlplay\.be|play\.rtl\.hr|rtlmost\.hu)/.+?-c_)(?P<id>[0-9]+)'
_TESTS = [{
'url': 'https://www.6play.fr/minute-par-minute-p_9533/le-but-qui-a-marque-lhistoire-du-football-francais-c_12041051',
'md5': '31fcd112637baa0c2ab92c4fcd8baf27',
'info_dict': {
'id': '12041051',
'ext': 'mp4',
'title': 'Le but qui a marqué l\'histoire du football français !',
'description': 'md5:b59e7e841d646ef1eb42a7868eb6a851',
},
}, {
'url': 'https://www.rtlplay.be/rtl-info-13h-p_8551/les-titres-du-rtlinfo-13h-c_12045869',
'only_matching': True,
}, {
'url': 'https://play.rtl.hr/pj-masks-p_9455/epizoda-34-sezona-1-catboyevo-cudo-na-dva-kotaca-c_11984989',
'only_matching': True,
}, {
'url': 'https://www.rtlmost.hu/megtorve-p_14167/megtorve-6-resz-c_12397787',
'only_matching': True,
}]
def _real_extract(self, url):
domain, video_id = self._match_valid_url(url).groups()
service, consumer_name = {
'6play.fr': ('6play', 'm6web'),
'rtlplay.be': ('rtlbe_rtl_play', 'rtlbe'),
'play.rtl.hr': ('rtlhr_rtl_play', 'rtlhr'),
'rtlmost.hu': ('rtlhu_rtl_most', 'rtlhu'),
}.get(domain, ('6play', 'm6web'))
data = self._download_json(
f'https://pc.middleware.6play.fr/6play/v2/platforms/m6group_web/services/{service}/videos/clip_{video_id}',
video_id, headers={
'x-customer-name': consumer_name,
}, query={
'csa': 5,
'with': 'clips',
})
clip_data = data['clips'][0]
title = clip_data['title']
urls = []
quality_key = qualities(['lq', 'sd', 'hq', 'hd'])
formats = []
subtitles = {}
assets = clip_data.get('assets') or []
for asset in assets:
asset_url = asset.get('full_physical_path')
protocol = asset.get('protocol')
if not asset_url or ((protocol == 'primetime' or asset.get('type') == 'usp_hlsfp_h264') and not ('_drmnp.ism/' in asset_url or '_unpnp.ism/' in asset_url)) or asset_url in urls:
continue
urls.append(asset_url)
container = asset.get('video_container')
ext = determine_ext(asset_url)
if protocol == 'http_subtitle' or ext == 'vtt':
subtitles.setdefault('fr', []).append({'url': asset_url})
continue
if container == 'm3u8' or ext == 'm3u8':
if protocol == 'usp':
if parse_qs(asset_url).get('token', [None])[0]:
urlh = self._request_webpage(
asset_url, video_id, fatal=False,
headers=self.geo_verification_headers())
if not urlh:
continue
asset_url = urlh.url
asset_url = asset_url.replace('_drmnp.ism/', '_unpnp.ism/')
for i in range(3, 0, -1):
asset_url = asset_url.replace('_sd1/', f'_sd{i}/')
m3u8_formats = self._extract_m3u8_formats(
asset_url, video_id, 'mp4', 'm3u8_native',
m3u8_id='hls', fatal=False)
formats.extend(m3u8_formats)
formats.extend(self._extract_mpd_formats(
asset_url.replace('.m3u8', '.mpd'),
video_id, mpd_id='dash', fatal=False))
if m3u8_formats:
break
else:
formats.extend(self._extract_m3u8_formats(
asset_url, video_id, 'mp4', 'm3u8_native',
m3u8_id='hls', fatal=False))
elif container == 'mp4' or ext == 'mp4':
quality = asset.get('video_quality')
formats.append({
'url': asset_url,
'format_id': quality,
'quality': quality_key(quality),
'ext': ext,
})
def get(getter):
for src in (data, clip_data):
v = try_get(src, getter, str)
if v:
return v
return {
'id': video_id,
'title': title,
'description': get(lambda x: x['description']),
'duration': int_or_none(clip_data.get('duration')),
'series': get(lambda x: x['program']['title']),
'formats': formats,
'subtitles': subtitles,
}
+8 -26
View File
@@ -248,35 +248,17 @@ class SlidesLiveIE(InfoExtractor):
'skip_download': 'm3u8', 'skip_download': 'm3u8',
}, },
}, { }, {
# /v3/ slides, .jpg and .png, service_name = youtube # /v3/ slides, .jpg and .png, formerly service_name = youtube, now native
'url': 'https://slideslive.com/embed/38932460/', 'url': 'https://slideslive.com/embed/38932460/',
'info_dict': { 'info_dict': {
'id': 'RTPdrgkyTiE', 'id': '38932460',
'display_id': '38932460',
'ext': 'mp4', 'ext': 'mp4',
'title': 'Active Learning for Hierarchical Multi-Label Classification', 'title': 'Active Learning for Hierarchical Multi-Label Classification',
'description': 'Watch full version of this video at https://slideslive.com/38932460.', 'duration': 941,
'channel': 'SlidesLive Videos - A', 'thumbnail': r're:https?://.+/.+\.(?:jpg|png)',
'channel_id': 'UC62SdArr41t_-_fX40QCLRw',
'channel_url': 'https://www.youtube.com/channel/UC62SdArr41t_-_fX40QCLRw',
'uploader': 'SlidesLive Videos - A',
'uploader_id': '@slideslivevideos-a6075',
'uploader_url': 'https://www.youtube.com/@slideslivevideos-a6075',
'upload_date': '20200903',
'timestamp': 1697805922,
'duration': 942,
'age_limit': 0,
'live_status': 'not_live',
'playable_in_embed': True,
'availability': 'unlisted',
'categories': ['People & Blogs'],
'tags': [],
'channel_follower_count': int,
'like_count': int,
'view_count': int,
'thumbnail': r're:^https?://.*\.(?:jpg|png|webp)',
'thumbnails': 'count:21',
'chapters': 'count:20', 'chapters': 'count:20',
'timestamp': 1708338974,
'upload_date': '20240219',
}, },
'params': { 'params': {
'skip_download': 'm3u8', 'skip_download': 'm3u8',
@@ -425,7 +407,7 @@ class SlidesLiveIE(InfoExtractor):
player_token = self._search_regex(r'data-player-token="([^"]+)"', webpage, 'player token') player_token = self._search_regex(r'data-player-token="([^"]+)"', webpage, 'player token')
player_data = self._download_webpage( player_data = self._download_webpage(
f'https://ben.slideslive.com/player/{video_id}', video_id, f'https://slideslive.com/player/{video_id}', video_id,
note='Downloading player info', query={'player_token': player_token}) note='Downloading player info', query={'player_token': player_token})
player_info = self._extract_custom_m3u8_info(player_data) player_info = self._extract_custom_m3u8_info(player_data)
@@ -525,7 +507,7 @@ class SlidesLiveIE(InfoExtractor):
yield info yield info
service_data = self._download_json( service_data = self._download_json(
f'https://ben.slideslive.com/player/{video_id}/slides_video_service_data', f'https://slideslive.com/player/{video_id}/slides_video_service_data',
video_id, fatal=False, query={ video_id, fatal=False, query={
'player_token': player_token, 'player_token': player_token,
'videos': ','.join(video_slides), 'videos': ','.join(video_slides),
+383 -45
View File
@@ -1,65 +1,403 @@
import functools
import json
import re
import urllib.parse
from .common import InfoExtractor from .common import InfoExtractor
from ..utils import ExtractorError from ..utils import (
OnDemandPagedList,
clean_html,
determine_ext,
extract_attributes,
int_or_none,
parse_iso8601,
str_or_none,
unescapeHTML,
url_or_none,
urljoin,
)
from ..utils.traversal import (
find_element,
find_elements,
require,
traverse_obj,
)
class SmotrimIE(InfoExtractor): class SmotrimBaseIE(InfoExtractor):
_VALID_URL = r'https?://smotrim\.ru/(?P<type>brand|video|article|live)/(?P<id>[0-9]+)' _BASE_URL = 'https://smotrim.ru'
_TESTS = [{ # video _GEO_BYPASS = False
_GEO_COUNTRIES = ['RU']
def _extract_from_smotrim_api(self, typ, item_id):
path = f'data{typ.replace("-", "")}/{"uid" if typ == "live" else "id"}'
data = self._download_json(
f'https://player.smotrim.ru/iframe/{path}/{item_id}/sid/smotrim', item_id)
media = traverse_obj(data, ('data', 'playlist', 'medialist', -1, {dict}))
if traverse_obj(media, ('locked', {bool})):
self.raise_login_required()
if error_msg := traverse_obj(media, ('errors', {clean_html})):
self.raise_geo_restricted(error_msg, countries=self._GEO_COUNTRIES)
webpage_url = traverse_obj(data, ('data', 'template', 'share_url', {url_or_none}))
webpage = self._download_webpage(webpage_url, item_id)
common = {
'thumbnail': self._html_search_meta(['og:image', 'twitter:image'], webpage, default=None),
**traverse_obj(media, {
'id': ('id', {str_or_none}),
'title': (('episodeTitle', 'title'), {clean_html}, filter, any),
'channel_id': ('channelId', {str_or_none}),
'description': ('anons', {clean_html}, filter),
'season': ('season', {clean_html}, filter),
'series': (('brand_title', 'brandTitle'), {clean_html}, filter, any),
'series_id': ('brand_id', {str_or_none}),
}),
}
if typ == 'audio':
bookmark = self._search_json(
r'class="bookmark"[^>]+value\s*=\s*"', webpage,
'bookmark', item_id, default={}, transform_source=unescapeHTML)
metadata = {
'vcodec': 'none',
**common,
**traverse_obj(media, {
'ext': ('audio_url', {determine_ext(default_ext='mp3')}),
'duration': ('duration', {int_or_none}),
'url': ('audio_url', {url_or_none}),
}),
**traverse_obj(bookmark, {
'title': ('subtitle', {clean_html}),
'timestamp': ('published', {parse_iso8601}),
}),
}
elif typ == 'audio-live':
metadata = {
'ext': 'mp3',
'url': traverse_obj(media, ('source', 'auto', {url_or_none})),
'vcodec': 'none',
**common,
}
else:
formats, subtitles = [], {}
for m3u8_url in traverse_obj(media, (
'sources', 'm3u8', {dict.values}, ..., {url_or_none},
)):
fmts, subs = self._extract_m3u8_formats_and_subtitles(
m3u8_url, item_id, 'mp4', m3u8_id='hls', fatal=False)
formats.extend(fmts)
self._merge_subtitles(subs, target=subtitles)
metadata = {
'formats': formats,
'subtitles': subtitles,
**self._search_json_ld(webpage, item_id),
**common,
}
return {
'age_limit': traverse_obj(data, ('data', 'age_restrictions', {int_or_none})),
'is_live': typ in ('audio-live', 'live'),
'tags': traverse_obj(webpage, (
{find_elements(cls='tags-list__link')}, ..., {clean_html}, filter, all, filter)),
'webpage_url': webpage_url,
**metadata,
}
class SmotrimIE(SmotrimBaseIE):
IE_NAME = 'smotrim'
_VALID_URL = r'(?:https?:)?//(?:(?:player|www)\.)?smotrim\.ru(?:/iframe)?/video(?:/id)?/(?P<id>\d+)'
_EMBED_REGEX = [fr'<iframe\b[^>]+\bsrc=["\'](?P<url>{_VALID_URL})']
_TESTS = [{
'url': 'https://smotrim.ru/video/1539617', 'url': 'https://smotrim.ru/video/1539617',
'md5': 'b1923a533c8cab09679789d720d0b1c5',
'info_dict': { 'info_dict': {
'id': '1539617', 'id': '1539617',
'ext': 'mp4', 'ext': 'mp4',
'title': 'Полиглот. Китайский с нуля за 16 часов! Урок №16', 'title': 'Урок №16',
'description': '', 'duration': 2631,
'series': 'Полиглот. Китайский с нуля за 16 часов!',
'series_id': '60562',
'tags': 'mincount:6',
'thumbnail': r're:https?://cdn-st\d+\.smotrim\.ru/.+\.(?:jpg|png)',
'timestamp': 1466771100,
'upload_date': '20160624',
'view_count': int,
}, },
'add_ie': ['RUTV'], }, {
}, { # article (geo-restricted? plays fine from the US and JP) 'url': 'https://player.smotrim.ru/iframe/video/id/2988590',
'info_dict': {
'id': '2988590',
'ext': 'mp4',
'title': 'Трейлер',
'age_limit': 16,
'description': 'md5:6af7e68ecf4ed7b8ff6720d20c4da47b',
'duration': 30,
'series': 'Мы в разводе',
'series_id': '71624',
'tags': 'mincount:5',
'thumbnail': r're:https?://cdn-st\d+\.smotrim\.ru/.+\.(?:jpg|png)',
'timestamp': 1750670040,
'upload_date': '20250623',
'view_count': int,
'webpage_url': 'https://smotrim.ru/video/2988590',
},
}]
_WEBPAGE_TESTS = [{
'url': 'https://smotrim.ru/article/2813445', 'url': 'https://smotrim.ru/article/2813445',
'md5': 'e0ac453952afbc6a2742e850b4dc8e77',
'info_dict': { 'info_dict': {
'id': '2431846', 'id': '2431846',
'ext': 'mp4', 'ext': 'mp4',
'title': 'Новости культуры. Съёмки первой программы "Большие и маленькие"', 'title': 'Съёмки первой программы "Большие и маленькие"',
'description': 'md5:94a4a22472da4252bf5587a4ee441b99', 'description': 'md5:446c9a5d334b995152a813946353f447',
'duration': 240,
'series': 'Новости культуры',
'series_id': '19725',
'tags': 'mincount:6',
'thumbnail': r're:https?://cdn-st\d+\.smotrim\.ru/.+\.(?:jpg|png)',
'timestamp': 1656054443,
'upload_date': '20220624',
'view_count': int,
'webpage_url': 'https://smotrim.ru/video/2431846',
}, },
'add_ie': ['RUTV'], }, {
}, { # brand, redirect 'url': 'https://www.vesti.ru/article/4642878',
'url': 'https://smotrim.ru/brand/64356',
'md5': '740472999ccff81d7f6df79cecd91c18',
'info_dict': { 'info_dict': {
'id': '2354523', 'id': '3007209',
'ext': 'mp4', 'ext': 'mp4',
'title': 'Большие и маленькие. Лучшее. 4-й выпуск', 'title': 'Иностранные мессенджеры используют не только мошенники, но и вербовщики',
'description': 'md5:84089e834429008371ea41ea3507b989', 'description': 'md5:74ab625a0a89b87b2e0ed98d6391b182',
'duration': 265,
'series': 'Вести. Дежурная часть',
'series_id': '5204',
'tags': 'mincount:6',
'thumbnail': r're:https?://cdn-st\d+\.smotrim\.ru/.+\.(?:jpg|png)',
'timestamp': 1754756280,
'upload_date': '20250809',
'view_count': int,
'webpage_url': 'https://smotrim.ru/video/3007209',
}, },
'add_ie': ['RUTV'],
}, { # live
'url': 'https://smotrim.ru/live/19201',
'info_dict': {
'id': '19201',
'ext': 'mp4',
# this looks like a TV channel name
'title': 'Россия Культура. Прямой эфир',
'description': '',
},
'add_ie': ['RUTV'],
}] }]
def _real_extract(self, url): def _real_extract(self, url):
video_id, typ = self._match_valid_url(url).group('id', 'type') video_id = self._match_id(url)
rutv_type = 'video'
if typ not in ('video', 'live'):
webpage = self._download_webpage(url, video_id, f'Resolving {typ} link')
# there are two cases matching regex:
# 1. "embedUrl" in JSON LD (/brand/)
# 2. "src" attribute from iframe (/article/)
video_id = self._search_regex(
r'"https://player.smotrim.ru/iframe/video/id/(?P<video_id>\d+)/',
webpage, 'video_id', default=None)
if not video_id:
raise ExtractorError('There are no video in this page.', expected=True)
elif typ == 'live':
rutv_type = 'live'
return self.url_result(f'https://player.vgtrk.com/iframe/{rutv_type}/id/{video_id}') return self._extract_from_smotrim_api('video', video_id)
class SmotrimAudioIE(SmotrimBaseIE):
IE_NAME = 'smotrim:audio'
_VALID_URL = r'https?://(?:(?:player|www)\.)?smotrim\.ru(?:/iframe)?/audio(?:/id)?/(?P<id>\d+)'
_TESTS = [{
'url': 'https://smotrim.ru/audio/2573986',
'md5': 'e28d94c20da524e242b2d00caef41a8e',
'info_dict': {
'id': '2573986',
'ext': 'mp3',
'title': 'Радиоспектакль',
'description': 'md5:4bcaaf7d532bc78f76e478fad944e388',
'duration': 3072,
'series': 'Морис Леблан. Арсен Люпен, джентльмен-грабитель',
'series_id': '66461',
'tags': 'mincount:7',
'thumbnail': r're:https?://cdn-st\d+\.smotrim\.ru/.+\.(?:jpg|png)',
'timestamp': 1624884358,
'upload_date': '20210628',
},
}, {
'url': 'https://player.smotrim.ru/iframe/audio/id/2860468',
'md5': '5a6bc1fa24c7142958be1ad9cfae58a8',
'info_dict': {
'id': '2860468',
'ext': 'mp3',
'title': 'Колобок и музыкальная игра "Терем-теремок"',
'duration': 1501,
'series': 'Веселый колобок',
'series_id': '68880',
'tags': 'mincount:4',
'thumbnail': r're:https?://cdn-st\d+\.smotrim\.ru/.+\.(?:jpg|png)',
'timestamp': 1755925800,
'upload_date': '20250823',
'webpage_url': 'https://smotrim.ru/audio/2860468',
},
}]
def _real_extract(self, url):
audio_id = self._match_id(url)
return self._extract_from_smotrim_api('audio', audio_id)
class SmotrimLiveIE(SmotrimBaseIE):
IE_NAME = 'smotrim:live'
_VALID_URL = r'''(?x:
(?:https?:)?//
(?:(?:(?:test)?player|www)\.)?
(?:
smotrim\.ru|
vgtrk\.com
)
(?:/iframe)?/
(?P<type>
channel|
(?:audio-)?live
)
(?:/u?id)?/(?P<id>[\da-f-]+)
)'''
_EMBED_REGEX = [fr'<iframe\b[^>]+\bsrc=["\'](?P<url>{_VALID_URL})']
_TESTS = [{
'url': 'https://smotrim.ru/channel/76',
'info_dict': {
'id': '1661',
'ext': 'mp4',
'title': str,
'channel_id': '76',
'description': 'Смотрим прямой эфир «Москва 24»',
'display_id': '76',
'live_status': 'is_live',
'thumbnail': r're:https?://cdn-st\d+\.smotrim\.ru/.+\.(?:jpg|png)',
'timestamp': int,
'upload_date': str,
},
'params': {'skip_download': 'Livestream'},
}, {
# Radio
'url': 'https://smotrim.ru/channel/81',
'info_dict': {
'id': '81',
'ext': 'mp3',
'title': str,
'channel_id': '81',
'live_status': 'is_live',
'thumbnail': r're:https?://cdn-st\d+\.smotrim\.ru/.+\.(?:jpg|png)',
},
'params': {'skip_download': 'Livestream'},
}, {
# Sometimes geo-restricted to Russia
'url': 'https://player.smotrim.ru/iframe/live/uid/381308c7-a066-4c4f-9656-83e2e792a7b4',
'info_dict': {
'id': '19201',
'ext': 'mp4',
'title': str,
'channel_id': '4',
'description': 'Смотрим прямой эфир «Россия К»',
'display_id': '381308c7-a066-4c4f-9656-83e2e792a7b4',
'live_status': 'is_live',
'thumbnail': r're:https?://cdn-st\d+\.smotrim\.ru/.+\.(?:jpg|png)',
'timestamp': int,
'upload_date': str,
'webpage_url': 'https://smotrim.ru/channel/4',
},
'params': {'skip_download': 'Livestream'},
}, {
'url': 'https://smotrim.ru/live/19201',
'only_matching': True,
}, {
'url': 'https://player.smotrim.ru/iframe/audio-live/id/81',
'only_matching': True,
}, {
'url': 'https://testplayer.vgtrk.com/iframe/live/id/19201',
'only_matching': True,
}]
def _real_extract(self, url):
typ, display_id = self._match_valid_url(url).group('type', 'id')
if typ == 'live' and re.fullmatch(r'[0-9]+', display_id):
url = self._request_webpage(url, display_id).url
typ = self._match_valid_url(url).group('type')
if typ == 'channel':
webpage = self._download_webpage(url, display_id)
src_url = traverse_obj(webpage, ((
({find_element(cls='main-player__frame', html=True)}, {extract_attributes}, 'src'),
({find_element(cls='audio-play-button', html=True)},
{extract_attributes}, 'value', {urllib.parse.unquote}, {json.loads}, 'source'),
), any, {self._proto_relative_url}, {url_or_none}, {require('src URL')}))
typ, video_id = self._match_valid_url(src_url).group('type', 'id')
else:
video_id = display_id
return {
'display_id': display_id,
**self._extract_from_smotrim_api(typ, video_id),
}
class SmotrimPlaylistIE(SmotrimBaseIE):
IE_NAME = 'smotrim:playlist'
_PAGE_SIZE = 15
_VALID_URL = r'https?://smotrim\.ru/(?P<type>brand|podcast)/(?P<id>\d+)/?(?P<season>[\w-]+)?'
_TESTS = [{
# Video
'url': 'https://smotrim.ru/brand/64356',
'info_dict': {
'id': '64356',
'title': 'Большие и маленькие',
},
'playlist_mincount': 55,
}, {
# Video, season
'url': 'https://smotrim.ru/brand/65293/3-sezon',
'info_dict': {
'id': '65293',
'title': 'Спасская',
'season': '3 сезон',
},
'playlist_count': 16,
}, {
# Audio
'url': 'https://smotrim.ru/brand/68880',
'info_dict': {
'id': '68880',
'title': 'Веселый колобок',
},
'playlist_mincount': 156,
}, {
# Podcast
'url': 'https://smotrim.ru/podcast/8021',
'info_dict': {
'id': '8021',
'title': 'Сила звука',
},
'playlist_mincount': 27,
}]
def _fetch_page(self, endpoint, key, playlist_id, page):
page += 1
items = self._download_json(
f'{self._BASE_URL}/api/{endpoint}', playlist_id,
f'Downloading page {page}', query={
key: playlist_id,
'limit': self._PAGE_SIZE,
'page': page,
},
)
for link in traverse_obj(items, ('contents', -1, 'list', ..., 'link', {str})):
yield self.url_result(urljoin(self._BASE_URL, link))
def _real_extract(self, url):
playlist_type, playlist_id, season = self._match_valid_url(url).group('type', 'id', 'season')
key = 'rubricId' if playlist_type == 'podcast' else 'brandId'
webpage = self._download_webpage(url, playlist_id)
playlist_title = self._html_search_meta(['og:title', 'twitter:title'], webpage, default=None)
if season:
return self.playlist_from_matches(traverse_obj(webpage, (
{find_elements(tag='a', attr='href', value=r'/video/\d+', html=True, regex=True)},
..., {extract_attributes}, 'href', {str},
)), playlist_id, playlist_title, season=traverse_obj(webpage, (
{find_element(cls='seasons__item seasons__item--selected')}, {clean_html},
)), ie=SmotrimIE, getter=urljoin(self._BASE_URL))
if traverse_obj(webpage, (
{find_element(cls='brand-main-item__videos')}, {clean_html}, filter,
)):
endpoint = 'videos'
else:
endpoint = 'audios'
return self.playlist_result(OnDemandPagedList(
functools.partial(self._fetch_page, endpoint, key, playlist_id), self._PAGE_SIZE), playlist_id, playlist_title)
+4 -1
View File
@@ -438,7 +438,7 @@ class SoundcloudIE(SoundcloudBaseIE):
(?P<title>[\w\d-]+) (?P<title>[\w\d-]+)
(?:/(?P<token>(?!(?:albums|sets|recommended))[^?]+?))? (?:/(?P<token>(?!(?:albums|sets|recommended))[^?]+?))?
(?:[?].*)?$) (?:[?].*)?$)
|(?:api(?:-v2)?\.soundcloud\.com/tracks/(?P<track_id>\d+) |(?:api(?:-v2)?\.soundcloud\.com/tracks/(?:soundcloud%3Atracks%3A)?(?P<track_id>\d+)
(?:/?\?secret_token=(?P<secret_token>[^&]+))?) (?:/?\?secret_token=(?P<secret_token>[^&]+))?)
) )
''' '''
@@ -692,6 +692,9 @@ class SoundcloudIE(SoundcloudBaseIE):
# Go+ (account with active subscription needed) # Go+ (account with active subscription needed)
'url': 'https://soundcloud.com/taylorswiftofficial/look-what-you-made-me-do', 'url': 'https://soundcloud.com/taylorswiftofficial/look-what-you-made-me-do',
'only_matching': True, 'only_matching': True,
}, {
'url': 'https://api.soundcloud.com/tracks/soundcloud%3Atracks%3A1083788353',
'only_matching': True,
}] }]
def _real_extract(self, url): def _real_extract(self, url):
-167
View File
@@ -1,167 +0,0 @@
import functools
import json
import re
from .common import InfoExtractor
from ..utils import (
OnDemandPagedList,
clean_podcast_url,
float_or_none,
int_or_none,
strip_or_none,
traverse_obj,
try_get,
unified_strdate,
)
class SpotifyBaseIE(InfoExtractor):
_WORKING = False
_ACCESS_TOKEN = None
_OPERATION_HASHES = {
'Episode': '8276d4423d709ae9b68ec1b74cc047ba0f7479059a37820be730f125189ac2bf',
'MinimalShow': '13ee079672fad3f858ea45a55eb109553b4fb0969ed793185b2e34cbb6ee7cc0',
'ShowEpisodes': 'e0e5ce27bd7748d2c59b4d44ba245a8992a05be75d6fabc3b20753fc8857444d',
}
_VALID_URL_TEMPL = r'https?://open\.spotify\.com/(?:embed-podcast/|embed/|)%s/(?P<id>[^/?&#]+)'
_EMBED_REGEX = [r'<iframe[^>]+src="(?P<url>https?://open\.spotify.com/embed/[^"]+)"']
def _real_initialize(self):
self._ACCESS_TOKEN = self._download_json(
'https://open.spotify.com/get_access_token', None)['accessToken']
def _call_api(self, operation, video_id, variables, **kwargs):
return self._download_json(
'https://api-partner.spotify.com/pathfinder/v1/query', video_id, query={
'operationName': 'query' + operation,
'variables': json.dumps(variables),
'extensions': json.dumps({
'persistedQuery': {
'sha256Hash': self._OPERATION_HASHES[operation],
},
}),
}, headers={'authorization': 'Bearer ' + self._ACCESS_TOKEN},
**kwargs)['data']
def _extract_episode(self, episode, series):
episode_id = episode['id']
title = episode['name'].strip()
formats = []
audio_preview = episode.get('audioPreview') or {}
audio_preview_url = audio_preview.get('url')
if audio_preview_url:
f = {
'url': audio_preview_url.replace('://p.scdn.co/mp3-preview/', '://anon-podcast.scdn.co/'),
'vcodec': 'none',
}
audio_preview_format = audio_preview.get('format')
if audio_preview_format:
f['format_id'] = audio_preview_format
mobj = re.match(r'([0-9A-Z]{3})_(?:[A-Z]+_)?(\d+)', audio_preview_format)
if mobj:
f.update({
'abr': int(mobj.group(2)),
'ext': mobj.group(1).lower(),
})
formats.append(f)
for item in (try_get(episode, lambda x: x['audio']['items']) or []):
item_url = item.get('url')
if not (item_url and item.get('externallyHosted')):
continue
formats.append({
'url': clean_podcast_url(item_url),
'vcodec': 'none',
})
thumbnails = []
for source in (try_get(episode, lambda x: x['coverArt']['sources']) or []):
source_url = source.get('url')
if not source_url:
continue
thumbnails.append({
'url': source_url,
'width': int_or_none(source.get('width')),
'height': int_or_none(source.get('height')),
})
return {
'id': episode_id,
'title': title,
'formats': formats,
'thumbnails': thumbnails,
'description': strip_or_none(episode.get('description')),
'duration': float_or_none(try_get(
episode, lambda x: x['duration']['totalMilliseconds']), 1000),
'release_date': unified_strdate(try_get(
episode, lambda x: x['releaseDate']['isoString'])),
'series': series,
}
class SpotifyIE(SpotifyBaseIE):
IE_NAME = 'spotify'
IE_DESC = 'Spotify episodes'
_VALID_URL = SpotifyBaseIE._VALID_URL_TEMPL % 'episode'
_TESTS = [{
'url': 'https://open.spotify.com/episode/4Z7GAJ50bgctf6uclHlWKo',
'md5': '74010a1e3fa4d9e1ab3aa7ad14e42d3b',
'info_dict': {
'id': '4Z7GAJ50bgctf6uclHlWKo',
'ext': 'mp3',
'title': 'From the archive: Why time management is ruining our lives',
'description': 'md5:b120d9c4ff4135b42aa9b6d9cde86935',
'duration': 2083.605,
'release_date': '20201217',
'series': "The Guardian's Audio Long Reads",
},
}, {
'url': 'https://open.spotify.com/embed/episode/4TvCsKKs2thXmarHigWvXE?si=7eatS8AbQb6RxqO2raIuWA',
'only_matching': True,
}]
def _real_extract(self, url):
episode_id = self._match_id(url)
episode = self._call_api('Episode', episode_id, {
'uri': 'spotify:episode:' + episode_id,
})['episode']
return self._extract_episode(
episode, try_get(episode, lambda x: x['podcast']['name']))
class SpotifyShowIE(SpotifyBaseIE):
IE_NAME = 'spotify:show'
IE_DESC = 'Spotify shows'
_VALID_URL = SpotifyBaseIE._VALID_URL_TEMPL % 'show'
_TEST = {
'url': 'https://open.spotify.com/show/4PM9Ke6l66IRNpottHKV9M',
'info_dict': {
'id': '4PM9Ke6l66IRNpottHKV9M',
'title': 'The Story from the Guardian',
'description': 'The Story podcast is dedicated to our finest audio documentaries, investigations and long form stories',
},
'playlist_mincount': 36,
}
_PER_PAGE = 100
def _fetch_page(self, show_id, page=0):
return self._call_api('ShowEpisodes', show_id, {
'limit': 100,
'offset': page * self._PER_PAGE,
'uri': f'spotify:show:{show_id}',
}, note=f'Downloading page {page + 1} JSON metadata')['podcast']
def _real_extract(self, url):
show_id = self._match_id(url)
first_page = self._fetch_page(show_id)
def _entries(page):
podcast = self._fetch_page(show_id, page) if page else first_page
yield from map(
functools.partial(self._extract_episode, series=podcast.get('name')),
traverse_obj(podcast, ('episodes', 'items', ..., 'episode')))
return self.playlist_result(
OnDemandPagedList(_entries, self._PER_PAGE),
show_id, first_page.get('name'), first_page.get('description'))
+2 -2
View File
@@ -12,7 +12,7 @@ from ..utils.traversal import traverse_obj
class SubstackIE(InfoExtractor): class SubstackIE(InfoExtractor):
_VALID_URL = r'https?://(?P<username>[\w-]+)\.substack\.com/p/(?P<id>[\w-]+)' _VALID_URL = r'https?://[\w-]+\.substack\.com/p/(?P<id>[\w-]+)'
_TESTS = [{ _TESTS = [{
'url': 'https://haleynahman.substack.com/p/i-made-a-vlog?s=r', 'url': 'https://haleynahman.substack.com/p/i-made-a-vlog?s=r',
'md5': 'f27e4fc6252001d48d479f45e65cdfd5', 'md5': 'f27e4fc6252001d48d479f45e65cdfd5',
@@ -116,7 +116,7 @@ class SubstackIE(InfoExtractor):
return formats, subtitles return formats, subtitles
def _real_extract(self, url): def _real_extract(self, url):
display_id, username = self._match_valid_url(url).group('id', 'username') display_id = self._match_id(url)
webpage = self._download_webpage(url, display_id) webpage = self._download_webpage(url, display_id)
webpage_info = self._parse_json(self._search_json( webpage_info = self._parse_json(self._search_json(
+2 -2
View File
@@ -46,7 +46,7 @@ class TelecincoBaseIE(InfoExtractor):
error_code = traverse_obj( error_code = traverse_obj(
self._webpage_read_content(error.cause.response, caronte['cerbero'], video_id, fatal=False), self._webpage_read_content(error.cause.response, caronte['cerbero'], video_id, fatal=False),
({json.loads}, 'code', {int})) ({json.loads}, 'code', {int}))
if error_code in (4038, 40313): if error_code in (4036, 4038, 40313):
self.raise_geo_restricted(countries=['ES']) self.raise_geo_restricted(countries=['ES'])
raise raise
@@ -140,7 +140,7 @@ class TelecincoIE(TelecincoBaseIE):
def _real_extract(self, url): def _real_extract(self, url):
display_id = self._match_id(url) display_id = self._match_id(url)
webpage = self._download_webpage(url, display_id) webpage = self._download_webpage(url, display_id, impersonate=True)
article = self._search_json( article = self._search_json(
r'window\.\$REACTBASE_STATE\.article(?:_multisite)?\s*=', r'window\.\$REACTBASE_STATE\.article(?:_multisite)?\s*=',
webpage, 'article', display_id)['article'] webpage, 'article', display_id)['article']
+163 -19
View File
@@ -1,8 +1,23 @@
import base64
import datetime as dt
import itertools import itertools
import json
import re
import time
from .common import InfoExtractor from .common import InfoExtractor
from ..networking import HEADRequest from ..networking.exceptions import HTTPError
from ..utils import int_or_none, traverse_obj, url_or_none, urljoin from ..utils import (
ExtractorError,
encode_data_uri,
filter_dict,
int_or_none,
jwt_decode_hs256,
url_or_none,
urlencode_postdata,
urljoin,
)
from ..utils.traversal import traverse_obj
class TenPlayIE(InfoExtractor): class TenPlayIE(InfoExtractor):
@@ -83,7 +98,7 @@ class TenPlayIE(InfoExtractor):
'only_matching': True, 'only_matching': True,
}] }]
_GEO_BYPASS = False _GEO_BYPASS = False
_GEO_COUNTRIES = ['AU']
_AUS_AGES = { _AUS_AGES = {
'G': 0, 'G': 0,
'PG': 15, 'PG': 15,
@@ -93,26 +108,155 @@ class TenPlayIE(InfoExtractor):
'R': 18, 'R': 18,
'X': 18, 'X': 18,
} }
_TOKEN_CACHE_KEY = 'token_data'
_SEGMENT_BITRATE_RE = r'(?m)-(?:300|150|75|55)0000-(\d+(?:-[\da-f]+)?)\.ts$'
_refresh_token = None
_access_token = None
@staticmethod
def _filter_ads_from_m3u8(m3u8_doc):
out = []
for line in m3u8_doc.splitlines():
if line.startswith('https://redirector.googlevideo.com/'):
out.pop()
continue
out.append(line)
return '\n'.join(out)
@staticmethod
def _generate_xnetwork_ten_auth_token():
ts = dt.datetime.now(dt.timezone.utc).strftime('%Y%m%d%H%M%S')
return base64.b64encode(ts.encode()).decode()
@staticmethod
def _is_jwt_expired(token):
return jwt_decode_hs256(token)['exp'] - time.time() < 300
def _refresh_access_token(self):
try:
refresh_data = self._download_json(
'https://10.com.au/api/token/refresh', None, 'Refreshing access token',
headers={
'Content-Type': 'application/json',
}, data=json.dumps({
'accessToken': self._access_token,
'refreshToken': self._refresh_token,
}).encode())
except ExtractorError as e:
if isinstance(e.cause, HTTPError) and e.cause.status == 400:
self._refresh_token = self._access_token = None
self.cache.store(self._NETRC_MACHINE, self._TOKEN_CACHE_KEY, [None, None])
self.report_warning('Refresh token has been invalidated; retrying with credentials')
self._perform_login(*self._get_login_info())
return
raise
self._access_token = refresh_data['accessToken']
self._refresh_token = refresh_data['refreshToken']
self.cache.store(self._NETRC_MACHINE, self._TOKEN_CACHE_KEY, [self._refresh_token, self._access_token])
def _perform_login(self, username, password):
if not self._refresh_token:
self._refresh_token, self._access_token = self.cache.load(
self._NETRC_MACHINE, self._TOKEN_CACHE_KEY, default=[None, None])
if self._refresh_token and self._access_token:
self.write_debug('Using cached refresh token')
return
try:
auth_data = self._download_json(
'https://10.com.au/api/user/auth', None, 'Logging in',
headers={
'Content-Type': 'application/json',
'X-Network-Ten-Auth': self._generate_xnetwork_ten_auth_token(),
'Referer': 'https://10.com.au/',
}, data=json.dumps({
'email': username,
'password': password,
}).encode())
except ExtractorError as e:
if isinstance(e.cause, HTTPError) and e.cause.status == 400:
raise ExtractorError('Invalid username/password', expected=True)
raise
self._refresh_token = auth_data['jwt']['refreshToken']
self._access_token = auth_data['jwt']['accessToken']
self.cache.store(self._NETRC_MACHINE, self._TOKEN_CACHE_KEY, [self._refresh_token, self._access_token])
def _call_playback_api(self, content_id):
if self._access_token and self._is_jwt_expired(self._access_token):
self._refresh_access_token()
for is_retry in (False, True):
try:
return self._download_json_handle(
f'https://10.com.au/api/v1/videos/playback/{content_id}/', content_id,
note='Downloading video JSON', query={'platform': 'samsung'},
headers=filter_dict({
'TP-AcceptFeature': 'v1/fw;v1/drm',
'Authorization': f'Bearer {self._access_token}' if self._access_token else None,
}))
except ExtractorError as e:
if not is_retry and isinstance(e.cause, HTTPError) and e.cause.status == 403:
if self._access_token:
self.to_screen('Access token has expired; refreshing')
self._refresh_access_token()
continue
elif not self._get_login_info()[0]:
self.raise_login_required('Login required to access this video', method='password')
raise
def _real_extract(self, url): def _real_extract(self, url):
content_id = self._match_id(url) content_id = self._match_id(url)
data = self._download_json( try:
'https://10.com.au/api/v1/videos/' + content_id, content_id) data = self._download_json(f'https://10.com.au/api/v1/videos/{content_id}', content_id)
except ExtractorError as e:
if (
isinstance(e.cause, HTTPError) and e.cause.status == 403
and 'Error 54113' in e.cause.response.read().decode()
):
self.raise_geo_restricted(countries=self._GEO_COUNTRIES)
raise
video_data = self._download_json( video_data, urlh = self._call_playback_api(content_id)
f'https://vod.ten.com.au/api/videos/bcquery?command=find_videos_by_id&video_id={data["altId"]}', content_source_id = video_data['dai']['contentSourceId']
content_id, 'Downloading video JSON') video_id = video_data['dai']['videoId']
m3u8_url = self._request_webpage( auth_token = urlh.get_header('x-dai-auth')
HEADRequest(video_data['items'][0]['HLSURL']), if not auth_token:
content_id, 'Checking stream URL').url raise ExtractorError('Failed to get DAI auth token')
if '10play-not-in-oz' in m3u8_url:
self.raise_geo_restricted(countries=['AU']) dai_data = self._download_json(
# Attempt to get a higher quality stream f'https://pubads.g.doubleclick.net/ondemand/hls/content/{content_source_id}/vid/{video_id}/streams',
formats = self._extract_m3u8_formats( content_id, note='Downloading DAI JSON',
m3u8_url.replace(',150,75,55,0000', ',300,150,75,55,0000'), data=urlencode_postdata({'auth-token': auth_token}))
content_id, 'mp4', fatal=False)
if not formats: # Ignore subs to avoid ad break cleanup
formats = self._extract_m3u8_formats(m3u8_url, content_id, 'mp4') formats, _ = self._extract_m3u8_formats_and_subtitles(
dai_data['stream_manifest'], content_id, 'mp4')
already_have_1080p = False
for fmt in formats:
m3u8_doc = self._download_webpage(
fmt['url'], content_id, note='Downloading m3u8 information')
m3u8_doc = self._filter_ads_from_m3u8(m3u8_doc)
fmt['hls_media_playlist_data'] = m3u8_doc
if fmt.get('height') == 1080:
already_have_1080p = True
# Attempt format upgrade
if not already_have_1080p and m3u8_doc and re.search(self._SEGMENT_BITRATE_RE, m3u8_doc):
m3u8_doc = re.sub(self._SEGMENT_BITRATE_RE, r'-5000000-\1.ts', m3u8_doc)
m3u8_doc = re.sub(r'-(?:300|150|75|55)0000\.key"', r'-5000000.key"', m3u8_doc)
formats.append({
'format_id': 'upgrade-attempt-1080p',
'url': encode_data_uri(m3u8_doc.encode(), 'application/x-mpegurl'),
'hls_media_playlist_data': m3u8_doc,
'width': 1920,
'height': 1080,
'ext': 'mp4',
'protocol': 'm3u8_native',
'__needs_testing': True,
})
return { return {
'id': content_id, 'id': content_id,
+20 -14
View File
@@ -220,7 +220,7 @@ class TikTokBaseIE(InfoExtractor):
def _extract_web_data_and_status(self, url, video_id, fatal=True): def _extract_web_data_and_status(self, url, video_id, fatal=True):
video_data, status = {}, -1 video_data, status = {}, -1
res = self._download_webpage_handle(url, video_id, fatal=fatal, headers={'User-Agent': 'Mozilla/5.0'}) res = self._download_webpage_handle(url, video_id, fatal=fatal, impersonate=True)
if res is False: if res is False:
return video_data, status return video_data, status
@@ -1071,12 +1071,15 @@ class TikTokUserIE(TikTokBaseIE):
webpage = self._download_webpage( webpage = self._download_webpage(
self._UPLOADER_URL_FORMAT % user_name, user_name, self._UPLOADER_URL_FORMAT % user_name, user_name,
'Downloading user webpage', 'Unable to download user webpage', 'Downloading user webpage', 'Unable to download user webpage',
fatal=False, headers={'User-Agent': 'Mozilla/5.0'}) or '' fatal=False, impersonate=True) or ''
detail = traverse_obj( detail = traverse_obj(
self._get_universal_data(webpage, user_name), ('webapp.user-detail', {dict})) or {} self._get_universal_data(webpage, user_name), ('webapp.user-detail', {dict})) or {}
if detail.get('statusCode') == 10222: video_count = traverse_obj(detail, ('userInfo', ('stats', 'statsV2'), 'videoCount', {int}, any))
if not video_count and detail.get('statusCode') == 10222:
self.raise_login_required( self.raise_login_required(
'This user\'s account is private. Log into an account that has access') 'This user\'s account is private. Log into an account that has access')
elif video_count == 0:
raise ExtractorError('This account does not have any videos posted', expected=True)
sec_uid = traverse_obj(detail, ('userInfo', 'user', 'secUid', {str})) sec_uid = traverse_obj(detail, ('userInfo', 'user', 'secUid', {str}))
if sec_uid: if sec_uid:
fail_early = not traverse_obj(detail, ('userInfo', 'itemList', ...)) fail_early = not traverse_obj(detail, ('userInfo', 'itemList', ...))
@@ -1518,19 +1521,22 @@ class TikTokLiveIE(TikTokBaseIE):
def _real_extract(self, url): def _real_extract(self, url):
uploader, room_id = self._match_valid_url(url).group('uploader', 'id') uploader, room_id = self._match_valid_url(url).group('uploader', 'id')
webpage = self._download_webpage( if not room_id:
url, uploader or room_id, headers={'User-Agent': 'Mozilla/5.0'}, fatal=not room_id) webpage = self._download_webpage(
format_field(uploader, None, self._UPLOADER_URL_FORMAT), uploader, impersonate=True)
room_id = traverse_obj(
self._get_universal_data(webpage, uploader),
('webapp.user-detail', 'userInfo', 'user', 'roomId', {str}))
if webpage: if not uploader or not room_id:
webpage = self._download_webpage(url, uploader or room_id, fatal=not room_id)
data = self._get_sigi_state(webpage, uploader or room_id) data = self._get_sigi_state(webpage, uploader or room_id)
room_id = ( room_id = room_id or traverse_obj(data, ((
traverse_obj(data, (( ('LiveRoom', 'liveRoomUserInfo', 'user'),
('LiveRoom', 'liveRoomUserInfo', 'user'), ('UserModule', 'users', ...)), 'roomId', {str}, any))
('UserModule', 'users', ...)), 'roomId', {str}, any)) uploader = uploader or traverse_obj(data, ((
or self._search_regex(r'snssdk\d*://live\?room_id=(\d+)', webpage, 'room ID', default=room_id)) ('LiveRoom', 'liveRoomUserInfo', 'user'),
uploader = uploader or traverse_obj( ('UserModule', 'users', ...)), 'uniqueId', {str}, any))
data, ('LiveRoom', 'liveRoomUserInfo', 'user', 'uniqueId'),
('UserModule', 'users', ..., 'uniqueId'), get_all=False, expected_type=str)
if not room_id: if not room_id:
raise UserNotLive(video_id=uploader) raise UserNotLive(video_id=uploader)
+265 -174
View File
@@ -1,244 +1,335 @@
import functools
import urllib.parse import urllib.parse
from .common import InfoExtractor from .common import InfoExtractor
from ..utils import ( from ..utils import (
OnDemandPagedList, OnDemandPagedList,
determine_ext, UnsupportedError,
clean_html,
int_or_none,
join_nonempty,
parse_iso8601, parse_iso8601,
traverse_obj, update_url_query,
url_or_none,
) )
from ..utils.traversal import traverse_obj
class TuneInBaseIE(InfoExtractor): class TuneInBaseIE(InfoExtractor):
_VALID_URL_BASE = r'https?://(?:www\.)?tunein\.com' def _call_api(self, item_id, endpoint=None, note='Downloading JSON metadata', fatal=False, query=None):
return self._download_json(
def _extract_metadata(self, webpage, content_id): join_nonempty('https://api.tunein.com/profiles', item_id, endpoint, delim='/'),
return self._search_json(r'window.INITIAL_STATE=', webpage, 'hydration', content_id, fatal=False) item_id, note=note, fatal=fatal, query=query) or {}
def _extract_formats_and_subtitles(self, content_id): def _extract_formats_and_subtitles(self, content_id):
streams = self._download_json( streams = self._download_json(
f'https://opml.radiotime.com/Tune.ashx?render=json&formats=mp3,aac,ogg,flash,hls&id={content_id}', 'https://opml.radiotime.com/Tune.ashx', content_id, query={
content_id)['body'] 'formats': 'mp3,aac,ogg,flash,hls',
'id': content_id,
'render': 'json',
})
formats, subtitles = [], {} formats, subtitles = [], {}
for stream in streams: for stream in traverse_obj(streams, ('body', lambda _, v: url_or_none(v['url']))):
if stream.get('media_type') == 'hls': if stream.get('media_type') == 'hls':
fmts, subs = self._extract_m3u8_formats_and_subtitles(stream['url'], content_id, fatal=False) fmts, subs = self._extract_m3u8_formats_and_subtitles(stream['url'], content_id, fatal=False)
formats.extend(fmts) formats.extend(fmts)
self._merge_subtitles(subs, target=subtitles) self._merge_subtitles(subs, target=subtitles)
elif determine_ext(stream['url']) == 'pls':
playlist_content = self._download_webpage(stream['url'], content_id)
formats.append({
'url': self._search_regex(r'File1=(.*)', playlist_content, 'url', fatal=False),
'abr': stream.get('bitrate'),
'ext': stream.get('media_type'),
})
else: else:
formats.append({ formats.append(traverse_obj(stream, {
'url': stream['url'], 'abr': ('bitrate', {int_or_none}),
'abr': stream.get('bitrate'), 'ext': ('media_type', {str}),
'ext': stream.get('media_type'), 'url': ('url', {self._proto_relative_url}),
}) }))
return formats, subtitles return formats, subtitles
class TuneInStationIE(TuneInBaseIE): class TuneInStationIE(TuneInBaseIE):
_VALID_URL = TuneInBaseIE._VALID_URL_BASE + r'(?:/radio/[^?#]+-|/embed/player/)(?P<id>s\d+)' IE_NAME = 'tunein:station'
_EMBED_REGEX = [r'<iframe[^>]+src=["\'](?P<url>(?:https?://)?tunein\.com/embed/player/s\d+)'] _VALID_URL = r'https?://tunein\.com/radio/[^/?#]+(?P<id>s\d+)'
_TESTS = [{ _TESTS = [{
'url': 'https://tunein.com/radio/Jazz24-885-s34682/', 'url': 'https://tunein.com/radio/Jazz24-885-s34682/',
'info_dict': { 'info_dict': {
'id': 's34682', 'id': 's34682',
'title': str,
'description': 'md5:d6d0b89063fd68d529fa7058ee98619b',
'thumbnail': r're:https?://cdn-profiles\.tunein\.com/.+',
'location': 'Seattle-Tacoma, US',
'ext': 'mp3', 'ext': 'mp3',
'title': str,
'alt_title': 'World Class Jazz',
'channel_follower_count': int,
'description': 'md5:d6d0b89063fd68d529fa7058ee98619b',
'location': r're:Seattle-Tacoma, (?:US|WA)',
'live_status': 'is_live', 'live_status': 'is_live',
'thumbnail': r're:https?://.+',
}, },
'params': { 'params': {'skip_download': 'Livestream'},
'skip_download': True,
},
}, {
'url': 'https://tunein.com/embed/player/s6404/',
'only_matching': True,
}, { }, {
'url': 'https://tunein.com/radio/BBC-Radio-1-988-s24939/', 'url': 'https://tunein.com/radio/BBC-Radio-1-988-s24939/',
'info_dict': { 'info_dict': {
'id': 's24939', 'id': 's24939',
'title': str,
'description': 'md5:ee2c56794844610d045f8caf5ff34d0c',
'thumbnail': r're:https?://cdn-profiles\.tunein\.com/.+',
'location': 'London, UK',
'ext': 'm4a', 'ext': 'm4a',
'title': str,
'alt_title': 'The biggest new pop and all-day vibes',
'channel_follower_count': int,
'description': 'md5:ee2c56794844610d045f8caf5ff34d0c',
'location': 'London, UK',
'live_status': 'is_live', 'live_status': 'is_live',
'thumbnail': r're:https?://.+',
}, },
'params': { 'params': {'skip_download': 'Livestream'},
'skip_download': True, }]
def _real_extract(self, url):
station_id = self._match_id(url)
formats, subtitles = self._extract_formats_and_subtitles(station_id)
return {
'id': station_id,
'formats': formats,
'subtitles': subtitles,
**traverse_obj(self._call_api(station_id), ('Item', {
'title': ('Title', {clean_html}),
'alt_title': ('Subtitle', {clean_html}, filter),
'channel_follower_count': ('Actions', 'Follow', 'FollowerCount', {int_or_none}),
'description': ('Description', {clean_html}, filter),
'is_live': ('Actions', 'Play', 'IsLive', {bool}),
'location': ('Properties', 'Location', 'DisplayName', {str}),
'thumbnail': ('Image', {url_or_none}),
})),
}
class TuneInPodcastIE(TuneInBaseIE):
IE_NAME = 'tunein:podcast:program'
_PAGE_SIZE = 20
_VALID_URL = r'https?://tunein\.com/podcasts(?:/[^/?#]+){1,2}(?P<id>p\d+)'
_TESTS = [{
'url': 'https://tunein.com/podcasts/Technology-Podcasts/Artificial-Intelligence-p1153019/',
'info_dict': {
'id': 'p1153019',
'title': 'Lex Fridman Podcast',
}, },
'playlist_mincount': 200,
}, {
'url': 'https://tunein.com/podcasts/World-News/BBC-News-p14/',
'info_dict': {
'id': 'p14',
'title': 'BBC News',
},
'playlist_mincount': 35,
}]
@classmethod
def suitable(cls, url):
return False if TuneInPodcastEpisodeIE.suitable(url) else super().suitable(url)
def _fetch_page(self, url, podcast_id, page=0):
items = self._call_api(
podcast_id, 'contents', f'Downloading page {page + 1}', query={
'filter': 't:free',
'limit': self._PAGE_SIZE,
'offset': page * self._PAGE_SIZE,
},
)['Items']
for item in traverse_obj(items, (..., 'GuideId', {str}, filter)):
yield self.url_result(update_url_query(url, {'topicId': item[1:]}))
def _real_extract(self, url):
podcast_id = self._match_id(url)
return self.playlist_result(OnDemandPagedList(
functools.partial(self._fetch_page, url, podcast_id), self._PAGE_SIZE),
podcast_id, traverse_obj(self._call_api(podcast_id), ('Item', 'Title', {str})))
class TuneInPodcastEpisodeIE(TuneInBaseIE):
IE_NAME = 'tunein:podcast'
_VALID_URL = r'https?://tunein\.com/podcasts(?:/[^/?#]+){1,2}(?P<series_id>p\d+)/?\?(?:[^#]+&)?(?i:topicid)=(?P<id>\d+)'
_TESTS = [{
'url': 'https://tunein.com/podcasts/Technology-Podcasts/Artificial-Intelligence-p1153019/?topicId=236404354',
'info_dict': {
'id': 't236404354',
'ext': 'mp3',
'title': '#351 MrBeast: Future of YouTube, Twitter, TikTok, and Instagram',
'alt_title': 'Technology Podcasts >',
'cast': 'count:1',
'description': 'md5:1029895354ef073ff00f20b82eb6eb71',
'display_id': '236404354',
'duration': 8330,
'thumbnail': r're:https?://.+',
'timestamp': 1673458571,
'upload_date': '20230111',
'series': 'Lex Fridman Podcast',
'series_id': 'p1153019',
},
}, {
'url': 'https://tunein.com/podcasts/The-BOB--TOM-Show-Free-Podcast-p20069/?topicId=174556405',
'info_dict': {
'id': 't174556405',
'ext': 'mp3',
'title': 'B&T Extra: Ohhh Yeah, It\'s Sexy Time',
'alt_title': 'Westwood One >',
'cast': 'count:2',
'description': 'md5:6828234f410ab88c85655495c5fcfa88',
'display_id': '174556405',
'duration': 1203,
'series': 'The BOB & TOM Show Free Podcast',
'series_id': 'p20069',
'thumbnail': r're:https?://.+',
'timestamp': 1661799600,
'upload_date': '20220829',
},
}]
def _real_extract(self, url):
series_id, display_id = self._match_valid_url(url).group('series_id', 'id')
episode_id = f't{display_id}'
formats, subtitles = self._extract_formats_and_subtitles(episode_id)
return {
'id': episode_id,
'display_id': display_id,
'formats': formats,
'series': traverse_obj(self._call_api(series_id), ('Item', 'Title', {clean_html})),
'series_id': series_id,
'subtitles': subtitles,
**traverse_obj(self._call_api(episode_id), ('Item', {
'title': ('Title', {clean_html}),
'alt_title': ('Subtitle', {clean_html}, filter),
'cast': (
'Properties', 'ParentProgram', 'Hosts', {clean_html},
{lambda x: x.split(';')}, ..., {str.strip}, filter, all, filter),
'description': ('Description', {clean_html}, filter),
'duration': ('Actions', 'Play', 'Duration', {int_or_none}),
'thumbnail': ('Image', {url_or_none}),
'timestamp': ('Actions', 'Play', 'PublishTime', {parse_iso8601}),
})),
}
class TuneInEmbedIE(TuneInBaseIE):
IE_NAME = 'tunein:embed'
_VALID_URL = r'https?://tunein\.com/embed/player/(?P<id>[^/?#]+)'
_EMBED_REGEX = [r'<iframe\b[^>]+\bsrc=["\'](?P<url>(?:https?:)?//tunein\.com/embed/player/[^/?#"\']+)']
_TESTS = [{
'url': 'https://tunein.com/embed/player/s6404/',
'info_dict': {
'id': 's6404',
'ext': 'mp3',
'title': str,
'alt_title': 'South Africa\'s News and Information Leader',
'channel_follower_count': int,
'live_status': 'is_live',
'location': 'Johannesburg, South Africa',
'thumbnail': r're:https?://.+',
},
'params': {'skip_download': 'Livestream'},
}, {
'url': 'https://tunein.com/embed/player/t236404354/',
'info_dict': {
'id': 't236404354',
'ext': 'mp3',
'title': '#351 MrBeast: Future of YouTube, Twitter, TikTok, and Instagram',
'alt_title': 'Technology Podcasts >',
'cast': 'count:1',
'description': 'md5:1029895354ef073ff00f20b82eb6eb71',
'display_id': '236404354',
'duration': 8330,
'series': 'Lex Fridman Podcast',
'series_id': 'p1153019',
'thumbnail': r're:https?://.+',
'timestamp': 1673458571,
'upload_date': '20230111',
},
}, {
'url': 'https://tunein.com/embed/player/p191660/',
'info_dict': {
'id': 'p191660',
'title': 'SBS Tamil',
},
'playlist_mincount': 195,
}] }]
_WEBPAGE_TESTS = [{ _WEBPAGE_TESTS = [{
'url': 'https://www.martiniinthemorning.com/', 'url': 'https://www.martiniinthemorning.com/',
'info_dict': { 'info_dict': {
'id': 's55412', 'id': 's55412',
'ext': 'mp3', 'ext': 'mp3',
'title': 'TuneInStation video #s55412', 'title': str,
'alt_title': 'Now that\'s music!',
'channel_follower_count': int,
'description': 'md5:41588a3e2cf34b3eafc6c33522fa611a',
'live_status': 'is_live',
'location': 'US',
'thumbnail': r're:https?://.+',
}, },
'expected_warnings': ['unable to extract hydration', 'Extractor failed to obtain "title"'], 'params': {'skip_download': 'Livestream'},
}] }]
def _real_extract(self, url): def _real_extract(self, url):
station_id = self._match_id(url) embed_id = self._match_id(url)
kind = {
'p': 'program',
's': 'station',
't': 'topic',
}.get(embed_id[:1])
webpage = self._download_webpage(url, station_id) return self.url_result(
metadata = self._extract_metadata(webpage, station_id) f'https://tunein.com/{kind}/?{kind}id={embed_id[1:]}')
formats, subtitles = self._extract_formats_and_subtitles(station_id)
return {
'id': station_id,
'title': traverse_obj(metadata, ('profiles', station_id, 'title')),
'description': traverse_obj(metadata, ('profiles', station_id, 'description')),
'thumbnail': traverse_obj(metadata, ('profiles', station_id, 'image')),
'timestamp': parse_iso8601(
traverse_obj(metadata, ('profiles', station_id, 'actions', 'play', 'publishTime'))),
'location': traverse_obj(
metadata, ('profiles', station_id, 'metadata', 'properties', 'location', 'displayName'),
('profiles', station_id, 'properties', 'location', 'displayName')),
'formats': formats,
'subtitles': subtitles,
'is_live': traverse_obj(metadata, ('profiles', station_id, 'actions', 'play', 'isLive')),
}
class TuneInPodcastIE(TuneInBaseIE):
_VALID_URL = TuneInBaseIE._VALID_URL_BASE + r'/(?:podcasts/[^?#]+-|embed/player/)(?P<id>p\d+)/?(?:#|$)'
_EMBED_REGEX = [r'<iframe[^>]+src=["\'](?P<url>(?:https?://)?tunein\.com/embed/player/p\d+)']
_TESTS = [{
'url': 'https://tunein.com/podcasts/Technology-Podcasts/Artificial-Intelligence-p1153019',
'info_dict': {
'id': 'p1153019',
'title': 'Lex Fridman Podcast',
'description': 'md5:bedc4e5f1c94f7dec6e4317b5654b00d',
},
'playlist_mincount': 200,
}, {
'url': 'https://tunein.com/embed/player/p191660/',
'only_matching': True,
}, {
'url': 'https://tunein.com/podcasts/World-News/BBC-News-p14/',
'info_dict': {
'id': 'p14',
'title': 'BBC News',
'description': 'md5:30b9622bcc4bd101d4acd6f38f284aed',
},
'playlist_mincount': 36,
}]
_PAGE_SIZE = 30
def _real_extract(self, url):
podcast_id = self._match_id(url)
webpage = self._download_webpage(url, podcast_id, fatal=False)
metadata = self._extract_metadata(webpage, podcast_id)
def page_func(page_num):
api_response = self._download_json(
f'https://api.tunein.com/profiles/{podcast_id}/contents', podcast_id,
note=f'Downloading page {page_num + 1}', query={
'filter': 't:free',
'offset': page_num * self._PAGE_SIZE,
'limit': self._PAGE_SIZE,
})
return [
self.url_result(
f'https://tunein.com/podcasts/{podcast_id}?topicId={episode["GuideId"][1:]}',
TuneInPodcastEpisodeIE, title=episode.get('Title'))
for episode in api_response['Items']]
entries = OnDemandPagedList(page_func, self._PAGE_SIZE)
return self.playlist_result(
entries, playlist_id=podcast_id, title=traverse_obj(metadata, ('profiles', podcast_id, 'title')),
description=traverse_obj(metadata, ('profiles', podcast_id, 'description')))
class TuneInPodcastEpisodeIE(TuneInBaseIE):
_VALID_URL = TuneInBaseIE._VALID_URL_BASE + r'/podcasts/(?:[^?&]+-)?(?P<podcast_id>p\d+)/?\?topicId=(?P<id>\w\d+)'
_TESTS = [{
'url': 'https://tunein.com/podcasts/Technology-Podcasts/Artificial-Intelligence-p1153019/?topicId=236404354',
'info_dict': {
'id': 't236404354',
'title': '#351 MrBeast: Future of YouTube, Twitter, TikTok, and Instagram',
'description': 'md5:2784533b98f8ac45c0820b1e4a8d8bb2',
'thumbnail': r're:https?://cdn-profiles\.tunein\.com/.+',
'timestamp': 1673458571,
'upload_date': '20230111',
'series_id': 'p1153019',
'series': 'Lex Fridman Podcast',
'ext': 'mp3',
},
}]
def _real_extract(self, url):
podcast_id, episode_id = self._match_valid_url(url).group('podcast_id', 'id')
episode_id = f't{episode_id}'
webpage = self._download_webpage(url, episode_id)
metadata = self._extract_metadata(webpage, episode_id)
formats, subtitles = self._extract_formats_and_subtitles(episode_id)
return {
'id': episode_id,
'title': traverse_obj(metadata, ('profiles', episode_id, 'title')),
'description': traverse_obj(metadata, ('profiles', episode_id, 'description')),
'thumbnail': traverse_obj(metadata, ('profiles', episode_id, 'image')),
'timestamp': parse_iso8601(
traverse_obj(metadata, ('profiles', episode_id, 'actions', 'play', 'publishTime'))),
'series_id': podcast_id,
'series': traverse_obj(metadata, ('profiles', podcast_id, 'title')),
'formats': formats,
'subtitles': subtitles,
}
class TuneInShortenerIE(InfoExtractor): class TuneInShortenerIE(InfoExtractor):
_WORKING = False
IE_NAME = 'tunein:shortener' IE_NAME = 'tunein:shortener'
IE_DESC = False # Do not list IE_DESC = False # Do not list
_VALID_URL = r'https?://tun\.in/(?P<id>[A-Za-z0-9]+)' _VALID_URL = r'https?://tun\.in/(?P<id>[^/?#]+)'
_TESTS = [{ _TESTS = [{
# test redirection
'url': 'http://tun.in/ser7s', 'url': 'http://tun.in/ser7s',
'info_dict': { 'info_dict': {
'id': 's34682', 'id': 's34682',
'title': str, 'title': str,
'description': 'md5:d6d0b89063fd68d529fa7058ee98619b',
'thumbnail': r're:https?://cdn-profiles\.tunein\.com/.+',
'location': 'Seattle-Tacoma, US',
'ext': 'mp3', 'ext': 'mp3',
'alt_title': 'World Class Jazz',
'channel_follower_count': int,
'description': 'md5:d6d0b89063fd68d529fa7058ee98619b',
'location': r're:Seattle-Tacoma, (?:US|WA)',
'live_status': 'is_live', 'live_status': 'is_live',
'thumbnail': r're:https?://.+',
}, },
'params': { 'params': {'skip_download': 'Livestream'},
'skip_download': True, # live stream }, {
'url': 'http://tun.in/tqeeFw',
'info_dict': {
'id': 't236404354',
'title': str,
'ext': 'mp3',
'alt_title': 'Technology Podcasts >',
'cast': 'count:1',
'description': 'md5:1029895354ef073ff00f20b82eb6eb71',
'display_id': '236404354',
'duration': 8330,
'series': 'Lex Fridman Podcast',
'series_id': 'p1153019',
'thumbnail': r're:https?://.+',
'timestamp': 1673458571,
'upload_date': '20230111',
}, },
'params': {'skip_download': 'Livestream'},
}, {
'url': 'http://tun.in/pei6i',
'info_dict': {
'id': 'p14',
'title': 'BBC News',
},
'playlist_mincount': 35,
}] }]
def _real_extract(self, url): def _real_extract(self, url):
redirect_id = self._match_id(url) redirect_id = self._match_id(url)
# The server doesn't support HEAD requests # The server doesn't support HEAD requests
urlh = self._request_webpage( urlh = self._request_webpage(url, redirect_id, 'Downloading redirect page')
url, redirect_id, note='Downloading redirect page') # Need to strip port from URL
parsed = urllib.parse.urlparse(urlh.url)
url = urlh.url new_url = parsed._replace(netloc=parsed.hostname).geturl()
url_parsed = urllib.parse.urlparse(url) # Prevent infinite loop in case redirect fails
if url_parsed.port == 443: if self.suitable(new_url):
url = url_parsed._replace(netloc=url_parsed.hostname).url raise UnsupportedError(new_url)
return self.url_result(new_url)
self.to_screen(f'Following redirect: {url}')
return self.url_result(url)
+9 -6
View File
@@ -44,7 +44,7 @@ class TwitchBaseIE(InfoExtractor):
'CollectionSideBar': '27111f1b382effad0b6def325caef1909c733fe6a4fbabf54f8d491ef2cf2f14', 'CollectionSideBar': '27111f1b382effad0b6def325caef1909c733fe6a4fbabf54f8d491ef2cf2f14',
'FilterableVideoTower_Videos': 'a937f1d22e269e39a03b509f65a7490f9fc247d7f83d6ac1421523e3b68042cb', 'FilterableVideoTower_Videos': 'a937f1d22e269e39a03b509f65a7490f9fc247d7f83d6ac1421523e3b68042cb',
'ClipsCards__User': 'b73ad2bfaecfd30a9e6c28fada15bd97032c83ec77a0440766a56fe0bd632777', 'ClipsCards__User': 'b73ad2bfaecfd30a9e6c28fada15bd97032c83ec77a0440766a56fe0bd632777',
'ShareClipRenderStatus': 'f130048a462a0ac86bb54d653c968c514e9ab9ca94db52368c1179e97b0f16eb', 'ShareClipRenderStatus': 'e0a46b287d760c6890a39d1ccd736af5ec9479a267d02c710e9ac33326b651d2',
'ChannelCollectionsContent': '447aec6a0cc1e8d0a8d7732d47eb0762c336a2294fdb009e9c9d854e49d484b9', 'ChannelCollectionsContent': '447aec6a0cc1e8d0a8d7732d47eb0762c336a2294fdb009e9c9d854e49d484b9',
'StreamMetadata': 'a647c2a13599e5991e175155f798ca7f1ecddde73f7f341f39009c14dbf59962', 'StreamMetadata': 'a647c2a13599e5991e175155f798ca7f1ecddde73f7f341f39009c14dbf59962',
'ComscoreStreamingQuery': 'e1edae8122517d013405f237ffcc124515dc6ded82480a88daef69c83b53ac01', 'ComscoreStreamingQuery': 'e1edae8122517d013405f237ffcc124515dc6ded82480a88daef69c83b53ac01',
@@ -514,7 +514,10 @@ class TwitchVodIE(TwitchBaseIE):
is_live = None is_live = None
if thumbnail: if thumbnail:
if re.findall(r'/404_processing_[^.?#]+\.png', thumbnail): if re.findall(r'/404_processing_[^.?#]+\.png', thumbnail):
is_live, thumbnail = True, None # False positive for is_live if info.get('broadcastType') == 'HIGHLIGHT'
# See https://github.com/yt-dlp/yt-dlp/issues/14455
is_live = info.get('broadcastType') == 'ARCHIVE'
thumbnail = None
else: else:
is_live = False is_live = False
@@ -1153,8 +1156,8 @@ class TwitchClipsIE(TwitchBaseIE):
'channel_id': '25163635', 'channel_id': '25163635',
'channel_is_verified': False, 'channel_is_verified': False,
'channel_follower_count': int, 'channel_follower_count': int,
'uploader': 'EA', 'uploader': 'stereotype_',
'uploader_id': '25163635', 'uploader_id': '43566419',
}, },
}, { }, {
'url': 'https://www.twitch.tv/xqc/clip/CulturedAmazingKuduDatSheffy-TiZ_-ixAGYR3y2Uy', 'url': 'https://www.twitch.tv/xqc/clip/CulturedAmazingKuduDatSheffy-TiZ_-ixAGYR3y2Uy',
@@ -1174,8 +1177,8 @@ class TwitchClipsIE(TwitchBaseIE):
'channel_id': '71092938', 'channel_id': '71092938',
'channel_is_verified': True, 'channel_is_verified': True,
'channel_follower_count': int, 'channel_follower_count': int,
'uploader': 'xQc', 'uploader': 'okSTFUdude',
'uploader_id': '71092938', 'uploader_id': '744085721',
'categories': ['Just Chatting'], 'categories': ['Just Chatting'],
}, },
}, { }, {
-16
View File
@@ -343,22 +343,6 @@ class TxxxIE(InfoExtractor):
'thumbnail': 'https://tn.voyeurhit.com/contents/videos_sources/332000/332875/screenshots/1.jpg', 'thumbnail': 'https://tn.voyeurhit.com/contents/videos_sources/332000/332875/screenshots/1.jpg',
}, },
}] }]
_WEBPAGE_TESTS = [{
'url': 'https://pornzog.com/video/9125519/michelle-malone-dreamgirls-wild-wet-3/',
'info_dict': {
'id': '5119660',
'display_id': '5119660',
'ext': 'mp4',
'title': 'Michelle Malone - Dreamgirls - Wild Wet 3',
'uploader': 'FallenAngel12',
'duration': 402,
'view_count': int,
'like_count': int,
'dislike_count': int,
'age_limit': 18,
'thumbnail': 'https://hctn.nv7s.com/contents/videos_sources/5119000/5119660/screenshots/1.jpg',
},
}]
def _call_api(self, url, video_id, fatal=False, **kwargs): def _call_api(self, url, video_id, fatal=False, **kwargs):
content = self._download_json(url, video_id, fatal=fatal, **kwargs) content = self._download_json(url, video_id, fatal=fatal, **kwargs)
+50 -7
View File
@@ -30,13 +30,13 @@ class KnownDRMIE(UnsupportedInfoExtractor):
r'play\.hbomax\.com', r'play\.hbomax\.com',
r'channel(?:4|5)\.com', r'channel(?:4|5)\.com',
r'peacocktv\.com', r'peacocktv\.com',
r'(?:[\w\.]+\.)?disneyplus\.com', r'(?:[\w.]+\.)?disneyplus\.com',
r'open\.spotify\.com/(?:track|playlist|album|artist)', r'open\.spotify\.com',
r'tvnz\.co\.nz', r'tvnz\.co\.nz',
r'oneplus\.ch', r'oneplus\.ch',
r'artstation\.com/learning/courses', r'artstation\.com/learning/courses',
r'philo\.com', r'philo\.com',
r'(?:[\w\.]+\.)?mech-plus\.com', r'(?:[\w.]+\.)?mech-plus\.com',
r'aha\.video', r'aha\.video',
r'mubi\.com', r'mubi\.com',
r'vootkids\.com', r'vootkids\.com',
@@ -57,6 +57,15 @@ class KnownDRMIE(UnsupportedInfoExtractor):
r'ctv\.ca', r'ctv\.ca',
r'noovo\.ca', r'noovo\.ca',
r'tsn\.ca', r'tsn\.ca',
r'paramountplus\.com',
r'(?:m\.)?(?:sony)?crackle\.com',
r'cw(?:tv(?:pr)?|seed)\.com',
r'6play\.fr',
r'rtlplay\.be',
r'play\.rtl\.hr',
r'rtlmost\.hu',
r'plus\.rtl\.de(?!/podcast/)',
r'mediasetinfinity\.es',
) )
_TESTS = [{ _TESTS = [{
@@ -78,10 +87,7 @@ class KnownDRMIE(UnsupportedInfoExtractor):
'url': r'https://www.disneyplus.com', 'url': r'https://www.disneyplus.com',
'only_matching': True, 'only_matching': True,
}, { }, {
'url': 'https://open.spotify.com/artist/', 'url': 'https://open.spotify.com',
'only_matching': True,
}, {
'url': 'https://open.spotify.com/track/',
'only_matching': True, 'only_matching': True,
}, { }, {
# https://github.com/yt-dlp/yt-dlp/issues/4122 # https://github.com/yt-dlp/yt-dlp/issues/4122
@@ -184,6 +190,42 @@ class KnownDRMIE(UnsupportedInfoExtractor):
}, { }, {
'url': 'https://www.tsn.ca/video/relaxed-oilers-look-to-put-emotional-game-2-loss-in-the-rearview%7E3148747', 'url': 'https://www.tsn.ca/video/relaxed-oilers-look-to-put-emotional-game-2-loss-in-the-rearview%7E3148747',
'only_matching': True, 'only_matching': True,
}, {
'url': 'https://www.paramountplus.com',
'only_matching': True,
}, {
'url': 'https://www.crackle.com',
'only_matching': True,
}, {
'url': 'https://m.sonycrackle.com',
'only_matching': True,
}, {
'url': 'https://www.cwtv.com',
'only_matching': True,
}, {
'url': 'https://www.cwseed.com',
'only_matching': True,
}, {
'url': 'https://cwtvpr.com',
'only_matching': True,
}, {
'url': 'https://www.6play.fr',
'only_matching': True,
}, {
'url': 'https://www.rtlplay.be',
'only_matching': True,
}, {
'url': 'https://play.rtl.hr',
'only_matching': True,
}, {
'url': 'https://www.rtlmost.hu',
'only_matching': True,
}, {
'url': 'https://plus.rtl.de/video-tv/',
'only_matching': True,
}, {
'url': 'https://www.mediasetinfinity.es/',
'only_matching': True,
}] }]
def _real_extract(self, url): def _real_extract(self, url):
@@ -222,6 +264,7 @@ class KnownPiracyIE(UnsupportedInfoExtractor):
r'91porn\.com', r'91porn\.com',
r'einthusan\.(?:tv|com|ca)', r'einthusan\.(?:tv|com|ca)',
r'yourupload\.com', r'yourupload\.com',
r'xanimu\.com',
) )
_TESTS = [{ _TESTS = [{
-119
View File
@@ -1,119 +0,0 @@
import re
from .common import InfoExtractor
from .rutv import RUTVIE
from ..utils import ExtractorError
class VestiIE(InfoExtractor):
_WORKING = False
IE_DESC = 'Вести.Ru'
_VALID_URL = r'https?://(?:.+?\.)?vesti\.ru/(?P<id>.+)'
_TESTS = [
{
'url': 'http://www.vesti.ru/videos?vid=575582&cid=1',
'info_dict': {
'id': '765035',
'ext': 'mp4',
'title': 'Вести.net: биткоины в России не являются законными',
'description': 'md5:d4bb3859dc1177b28a94c5014c35a36b',
'duration': 302,
},
'params': {
# m3u8 download
'skip_download': True,
},
},
{
'url': 'http://www.vesti.ru/doc.html?id=1349233',
'info_dict': {
'id': '773865',
'ext': 'mp4',
'title': 'Участники митинга штурмуют Донецкую областную администрацию',
'description': 'md5:1a160e98b3195379b4c849f2f4958009',
'duration': 210,
},
'params': {
# m3u8 download
'skip_download': True,
},
},
{
'url': 'http://www.vesti.ru/only_video.html?vid=576180',
'info_dict': {
'id': '766048',
'ext': 'mp4',
'title': 'США заморозило, Британию затопило',
'description': 'md5:f0ed0695ec05aed27c56a70a58dc4cc1',
'duration': 87,
},
'params': {
# m3u8 download
'skip_download': True,
},
},
{
'url': 'http://hitech.vesti.ru/news/view/id/4000',
'info_dict': {
'id': '766888',
'ext': 'mp4',
'title': 'Вести.net: интернет-гиганты начали перетягивание программных "одеял"',
'description': 'md5:65ddd47f9830c4f42ed6475f8730c995',
'duration': 279,
},
'params': {
# m3u8 download
'skip_download': True,
},
},
{
'url': 'http://sochi2014.vesti.ru/video/index/video_id/766403',
'info_dict': {
'id': '766403',
'ext': 'mp4',
'title': 'XXII зимние Олимпийские игры. Российские хоккеисты стартовали на Олимпиаде с победы',
'description': 'md5:55805dfd35763a890ff50fa9e35e31b3',
'duration': 271,
},
'params': {
# m3u8 download
'skip_download': True,
},
'skip': 'Blocked outside Russia',
},
{
'url': 'http://sochi2014.vesti.ru/live/play/live_id/301',
'info_dict': {
'id': '51499',
'ext': 'flv',
'title': 'Сочи-2014. Биатлон. Индивидуальная гонка. Мужчины ',
'description': 'md5:9e0ed5c9d2fa1efbfdfed90c9a6d179c',
},
'params': {
# rtmp download
'skip_download': True,
},
'skip': 'Translation has finished',
},
]
def _real_extract(self, url):
mobj = self._match_valid_url(url)
video_id = mobj.group('id')
page = self._download_webpage(url, video_id, 'Downloading page')
mobj = re.search(
r'<meta[^>]+?property="og:video"[^>]+?content="http://www\.vesti\.ru/i/flvplayer_videoHost\.swf\?vid=(?P<id>\d+)',
page)
if mobj:
video_id = mobj.group('id')
page = self._download_webpage(f'http://www.vesti.ru/only_video.html?vid={video_id}', video_id,
'Downloading video page')
rutv_url = RUTVIE._extract_url(page)
if rutv_url:
return self.url_result(rutv_url, 'RUTV')
raise ExtractorError('No video found', expected=True)
+60 -47
View File
@@ -58,6 +58,20 @@ class VidyardBaseIE(InfoExtractor):
return subs return subs
def _get_additional_metadata(self, video_id):
additional_metadata = self._download_json(
f'https://play.vidyard.com/video/{video_id}', video_id,
note='Downloading additional metadata', fatal=False)
return traverse_obj(additional_metadata, {
'title': ('name', {str}),
'duration': ('seconds', {int_or_none}),
'thumbnails': ('thumbnailUrl', {'url': {url_or_none}}, all),
'chapters': ('videoSections', lambda _, v: float_or_none(v['milliseconds']) is not None, {
'title': ('title', {str}),
'start_time': ('milliseconds', {float_or_none(scale=1000)}),
}),
})
def _fetch_video_json(self, video_id): def _fetch_video_json(self, video_id):
return self._download_json( return self._download_json(
f'https://play.vidyard.com/player/{video_id}.json', video_id)['payload'] f'https://play.vidyard.com/player/{video_id}.json', video_id)['payload']
@@ -67,6 +81,7 @@ class VidyardBaseIE(InfoExtractor):
self._merge_subtitles(self._get_direct_subtitles(json_data.get('captions')), target=subtitles) self._merge_subtitles(self._get_direct_subtitles(json_data.get('captions')), target=subtitles)
return { return {
**self._get_additional_metadata(json_data['facadeUuid']),
**traverse_obj(json_data, { **traverse_obj(json_data, {
'id': ('facadeUuid', {str}), 'id': ('facadeUuid', {str}),
'display_id': ('videoId', {int}, {str_or_none}), 'display_id': ('videoId', {int}, {str_or_none}),
@@ -113,6 +128,29 @@ class VidyardIE(VidyardBaseIE):
'thumbnail': 'https://cdn.vidyard.com/thumbnails/spacer.gif', 'thumbnail': 'https://cdn.vidyard.com/thumbnails/spacer.gif',
'duration': 41.186, 'duration': 41.186,
}, },
}, {
'url': 'https://share.vidyard.com/watch/wL237MtNgZUHo6e8WPiJbF',
'info_dict': {
'id': 'wL237MtNgZUHo6e8WPiJbF',
'display_id': '25926870',
'ext': 'mp4',
'title': 'Adding & Editing Video Chapters',
'thumbnail': 'https://cdn.vidyard.com/thumbnails/25926870/bvSEZS3dGY7DByQ_bzB57avIZ_hsvhr4_small.jpg',
'duration': 135.46,
'chapters': [{
'title': 'Adding new chapters',
'start_time': 0,
}, {
'title': 'Previewing your video',
'start_time': 74,
}, {
'title': 'Editing your chapters',
'start_time': 91,
}, {
'title': 'Share a link to a specific chapter',
'start_time': 105,
}],
},
}, { }, {
'url': 'https://embed.vidyard.com/share/oTDMPlUv--51Th455G5u7Q', 'url': 'https://embed.vidyard.com/share/oTDMPlUv--51Th455G5u7Q',
'info_dict': { 'info_dict': {
@@ -132,8 +170,8 @@ class VidyardIE(VidyardBaseIE):
'id': 'SyStyHtYujcBHe5PkZc5DL', 'id': 'SyStyHtYujcBHe5PkZc5DL',
'display_id': '41974005', 'display_id': '41974005',
'ext': 'mp4', 'ext': 'mp4',
'title': 'Prepare the Frame and Track for Palm Beach Polysatin Shutters With BiFold Track', 'title': 'Install Palm Beach Shutters with a Bi-Fold Track System (Video 1 of 6)',
'description': r're:In this video, you will learn how to prepare the frame.+', 'description': r're:In this video, you will learn the first step.+',
'thumbnail': 'https://cdn.vidyard.com/thumbnails/41974005/IJw7oCaJcF1h7WWu3OVZ8A_small.png', 'thumbnail': 'https://cdn.vidyard.com/thumbnails/41974005/IJw7oCaJcF1h7WWu3OVZ8A_small.png',
'duration': 258.666, 'duration': 258.666,
}, },
@@ -147,42 +185,42 @@ class VidyardIE(VidyardBaseIE):
'id': 'SyStyHtYujcBHe5PkZc5DL', 'id': 'SyStyHtYujcBHe5PkZc5DL',
'display_id': '41974005', 'display_id': '41974005',
'ext': 'mp4', 'ext': 'mp4',
'title': 'Prepare the Frame and Track for Palm Beach Polysatin Shutters With BiFold Track', 'title': 'Install Palm Beach Shutters with a Bi-Fold Track System (Video 1 of 6)',
'thumbnail': 'https://cdn.vidyard.com/thumbnails/41974005/IJw7oCaJcF1h7WWu3OVZ8A_small.png', 'thumbnail': 'https://cdn.vidyard.com/thumbnails/41974005/IJw7oCaJcF1h7WWu3OVZ8A_small.png',
'duration': 258.666, 'duration': 258.666,
}, { }, {
'id': '1Fw4B84jZTXLXWqkE71RiM', 'id': '1Fw4B84jZTXLXWqkE71RiM',
'display_id': '5861113', 'display_id': '5861113',
'ext': 'mp4', 'ext': 'mp4',
'title': 'Palm Beach - Bi-Fold Track System "Frame Installation"', 'title': 'Install Palm Beach Shutters with a Bi-Fold Track System (Video 2 of 6)',
'thumbnail': 'https://cdn.vidyard.com/thumbnails/5861113/29CJ54s5g1_aP38zkKLHew_small.jpg', 'thumbnail': 'https://cdn.vidyard.com/thumbnails/5861113/29CJ54s5g1_aP38zkKLHew_small.jpg',
'duration': 167.858, 'duration': 167.858,
}, { }, {
'id': 'DqP3wBvLXSpxrcqpT5kEeo', 'id': 'DqP3wBvLXSpxrcqpT5kEeo',
'display_id': '41976334', 'display_id': '41976334',
'ext': 'mp4', 'ext': 'mp4',
'title': 'Install the Track for Palm Beach Polysatin Shutters With BiFold Track', 'title': 'Install Palm Beach Shutters with a Bi-Fold Track System (Video 3 of 6)',
'thumbnail': 'https://cdn.vidyard.com/thumbnails/5861090/RwG2VaTylUa6KhSTED1r1Q_small.png', 'thumbnail': 'https://cdn.vidyard.com/thumbnails/5861090/RwG2VaTylUa6KhSTED1r1Q_small.png',
'duration': 94.229, 'duration': 94.229,
}, { }, {
'id': 'opfybfxpzQArxqtQYB6oBU', 'id': 'opfybfxpzQArxqtQYB6oBU',
'display_id': '41976364', 'display_id': '41976364',
'ext': 'mp4', 'ext': 'mp4',
'title': 'Install the Panel for Palm Beach Polysatin Shutters With BiFold Track', 'title': 'Install Palm Beach Shutters with a Bi-Fold Track System (Video 4 of 6)',
'thumbnail': 'https://cdn.vidyard.com/thumbnails/5860926/JIOaJR08dM4QgXi_iQ2zGA_small.png', 'thumbnail': 'https://cdn.vidyard.com/thumbnails/5860926/JIOaJR08dM4QgXi_iQ2zGA_small.png',
'duration': 191.467, 'duration': 191.467,
}, { }, {
'id': 'rWrXvkbTNNaNqD6189HJya', 'id': 'rWrXvkbTNNaNqD6189HJya',
'display_id': '41976382', 'display_id': '41976382',
'ext': 'mp4', 'ext': 'mp4',
'title': 'Adjust the Panels for Palm Beach Polysatin Shutters With BiFold Track', 'title': 'Install Palm Beach Shutters with a Bi-Fold Track System (Video 5 of 6)',
'thumbnail': 'https://cdn.vidyard.com/thumbnails/5860687/CwHxBv4UudAhOh43FVB4tw_small.png', 'thumbnail': 'https://cdn.vidyard.com/thumbnails/5860687/CwHxBv4UudAhOh43FVB4tw_small.png',
'duration': 138.155, 'duration': 138.155,
}, { }, {
'id': 'eYPTB521MZ9TPEArSethQ5', 'id': 'eYPTB521MZ9TPEArSethQ5',
'display_id': '41976409', 'display_id': '41976409',
'ext': 'mp4', 'ext': 'mp4',
'title': 'Assemble and Install the Valance for Palm Beach Polysatin Shutters With BiFold Track', 'title': 'Install Palm Beach Shutters with a Bi-Fold Track System (Video 6 of 6)',
'thumbnail': 'https://cdn.vidyard.com/thumbnails/5861425/0y68qlMU4O5VKU7bJ8i_AA_small.png', 'thumbnail': 'https://cdn.vidyard.com/thumbnails/5861425/0y68qlMU4O5VKU7bJ8i_AA_small.png',
'duration': 148.224, 'duration': 148.224,
}], }],
@@ -191,6 +229,7 @@ class VidyardIE(VidyardBaseIE):
}, { }, {
# Non hubs.vidyard.com playlist # Non hubs.vidyard.com playlist
'url': 'https://salesforce.vidyard.com/watch/d4vqPjs7Q5EzVEis5QT3jd', 'url': 'https://salesforce.vidyard.com/watch/d4vqPjs7Q5EzVEis5QT3jd',
'skip': 'URL now 404s. Alternative non hubs.vidyard.com playlist not yet available',
'info_dict': { 'info_dict': {
'id': 'd4vqPjs7Q5EzVEis5QT3jd', 'id': 'd4vqPjs7Q5EzVEis5QT3jd',
'title': 'How To: Service Cloud: Import External Content in Lightning Knowledge', 'title': 'How To: Service Cloud: Import External Content in Lightning Knowledge',
@@ -300,6 +339,7 @@ class VidyardIE(VidyardBaseIE):
}, { }, {
# <script ... id="vidyard_embed_code_DXx2sW4WaLA6hTdGFz7ja8" src="//play.vidyard.com/DXx2sW4WaLA6hTdGFz7ja8.js? # <script ... id="vidyard_embed_code_DXx2sW4WaLA6hTdGFz7ja8" src="//play.vidyard.com/DXx2sW4WaLA6hTdGFz7ja8.js?
'url': 'http://videos.vivint.com/watch/DXx2sW4WaLA6hTdGFz7ja8', 'url': 'http://videos.vivint.com/watch/DXx2sW4WaLA6hTdGFz7ja8',
'skip': 'URL certificate expired 2025-09-10. Alternative script embed test case not yet available',
'info_dict': { 'info_dict': {
'id': 'DXx2sW4WaLA6hTdGFz7ja8', 'id': 'DXx2sW4WaLA6hTdGFz7ja8',
'display_id': '2746529', 'display_id': '2746529',
@@ -317,11 +357,12 @@ class VidyardIE(VidyardBaseIE):
'ext': 'mp4', 'ext': 'mp4',
'title': 'Lesson 1 - Opening an MT4 Account', 'title': 'Lesson 1 - Opening an MT4 Account',
'description': 'Never heard of MetaTrader4? Here\'s the 411 on the popular trading platform!', 'description': 'Never heard of MetaTrader4? Here\'s the 411 on the popular trading platform!',
'duration': 168, 'duration': 168.16,
'thumbnail': 'https://cdn.vidyard.com/thumbnails/20291/IM-G2WXQR9VBLl2Cmzvftg_small.jpg', 'thumbnail': 'https://cdn.vidyard.com/thumbnails/20291/IM-G2WXQR9VBLl2Cmzvftg_small.jpg',
}, },
}, { }, {
# <iframe ... src="//play.vidyard.com/d61w8EQoZv1LDuPxDkQP2Q/type/background?preview=1" # <iframe ... src="//play.vidyard.com/d61w8EQoZv1LDuPxDkQP2Q/type/background?preview=1"
'skip': 'URL changed embed method to \'class="vidyard-player-embed"\'. An alternative iframe embed test case is not yet available',
'url': 'https://www.avaya.com/en/', 'url': 'https://www.avaya.com/en/',
'info_dict': { 'info_dict': {
# These values come from the generic extractor and don't matter # These values come from the generic extractor and don't matter
@@ -354,46 +395,18 @@ class VidyardIE(VidyardBaseIE):
}], }],
'playlist_count': 2, 'playlist_count': 2,
}, { }, {
# <div class="vidyard-player-embed" data-uuid="vpCWTVHw3qrciLtVY94YkS" # <div class="vidyard-player-embed" data-uuid="pMk8eNCYzukzJaEPoo1Hgn"
'url': 'https://www.gogoair.com/', # URL previously used iframe embeds and was used for that test case
'url': 'https://www.avaya.com/en/',
'info_dict': { 'info_dict': {
# These values come from the generic extractor and don't matter 'id': 'pMk8eNCYzukzJaEPoo1Hgn',
'id': str, 'display_id': '47074153',
'title': str, 'ext': 'mp4',
'description': str, 'title': 'Avaya Infinity Helps Redefine the Contact Center as Your Connection Center',
'age_limit': 0, 'description': r're:Our mission is to help you turn single engagements.+',
'duration': 81.55,
'thumbnail': 'https://cdn.vidyard.com/thumbnails/47074153/MZOLKhXdbiUWwp2ROnT5HaXL0oau6JtR_small.jpg',
}, },
'playlist': [{
'info_dict': {
'id': 'vpCWTVHw3qrciLtVY94YkS',
'display_id': '40780699',
'ext': 'mp4',
'title': 'Upgrade to AVANCE 100% worth it - Jason Talley, Owner and Pilot, Testimonial',
'description': 'md5:f609824839439a51990cef55ffc472aa',
'duration': 70.737,
'thumbnail': 'https://cdn.vidyard.com/thumbnails/40780699/KzjfYZz5MZl2gHF_e-4i2c6ib1cLDweQ_small.jpg',
},
}, {
'info_dict': {
'id': 'xAmV9AsLbnitCw35paLBD8',
'display_id': '31130867',
'ext': 'mp4',
'title': 'Brad Keselowski goes faster with Gogo AVANCE inflight Wi-Fi',
'duration': 132.565,
'thumbnail': 'https://cdn.vidyard.com/thumbnails/31130867/HknyDtLdm2Eih9JZ4A5XLjhfBX_6HRw5_small.jpg',
},
}, {
'info_dict': {
'id': 'RkkrFRNxfP79nwCQavecpF',
'display_id': '39009815',
'ext': 'mp4',
'title': 'Live Demo of Gogo Galileo',
'description': 'md5:e2df497236f4e12c3fef8b392b5f23e0',
'duration': 112.128,
'thumbnail': 'https://cdn.vidyard.com/thumbnails/38144873/CWLlxfUbJ4Gh0ThbUum89IsEM4yupzMb_small.jpg',
},
}],
'playlist_count': 3,
}] }]
@classmethod @classmethod
+1 -1
View File
@@ -151,7 +151,7 @@ class VimeoBaseInfoExtractor(InfoExtractor):
'Referer': self._LOGIN_URL, 'Referer': self._LOGIN_URL,
}) })
except ExtractorError as e: except ExtractorError as e:
if isinstance(e.cause, HTTPError) and e.cause.status in (405, 418): if isinstance(e.cause, HTTPError) and e.cause.status in (404, 405, 418):
raise ExtractorError( raise ExtractorError(
'Unable to log in: bad username or password', 'Unable to log in: bad username or password',
expected=True) expected=True)
+11 -4
View File
@@ -96,12 +96,12 @@ class VKIE(VKBaseIE):
https?:// https?://
(?: (?:
(?: (?:
(?:(?:m|new)\.)?vk(?:(?:video)?\.ru|\.com)/video_| (?:(?:m|new|vksport)\.)?vk(?:(?:video)?\.ru|\.com)/video_|
(?:www\.)?daxab\.com/ (?:www\.)?daxab\.com/
) )
ext\.php\?(?P<embed_query>.*?\boid=(?P<oid>-?\d+).*?\bid=(?P<id>\d+).*)| ext\.php\?(?P<embed_query>.*?\boid=(?P<oid>-?\d+).*?\bid=(?P<id>\d+).*)|
(?: (?:
(?:(?:m|new)\.)?vk(?:(?:video)?\.ru|\.com)/(?:.+?\?.*?z=)?(?:video|clip)| (?:(?:m|new|vksport)\.)?vk(?:(?:video)?\.ru|\.com)/(?:.+?\?.*?z=)?(?:video|clip)|
(?:www\.)?daxab\.com/embed/ (?:www\.)?daxab\.com/embed/
) )
(?P<videoid>-?\d+_\d+)(?:.*\blist=(?P<list_id>([\da-f]+)|(ln-[\da-zA-Z]+)))? (?P<videoid>-?\d+_\d+)(?:.*\blist=(?P<list_id>([\da-f]+)|(ln-[\da-zA-Z]+)))?
@@ -359,6 +359,10 @@ class VKIE(VKBaseIE):
'url': 'https://vk.ru/video-220754053_456242564', 'url': 'https://vk.ru/video-220754053_456242564',
'only_matching': True, 'only_matching': True,
}, },
{
'url': 'https://vksport.vkvideo.ru/video-124096712_456240773',
'only_matching': True,
},
] ]
def _real_extract(self, url): def _real_extract(self, url):
@@ -568,7 +572,7 @@ class VKUserVideosIE(VKBaseIE):
IE_DESC = "VK - User's Videos" IE_DESC = "VK - User's Videos"
_BASE_URL_RE = r'https?://(?:(?:m|new)\.)?vk(?:video\.ru|\.com/video)' _BASE_URL_RE = r'https?://(?:(?:m|new)\.)?vk(?:video\.ru|\.com/video)'
_VALID_URL = [ _VALID_URL = [
rf'{_BASE_URL_RE}/playlist/(?P<id>-?\d+_\d+)', rf'{_BASE_URL_RE}/playlist/(?P<id>-?\d+_-?\d+)',
rf'{_BASE_URL_RE}/(?P<id>@[^/?#]+)(?:/all)?/?(?!\?.*\bz=video)(?:[?#]|$)', rf'{_BASE_URL_RE}/(?P<id>@[^/?#]+)(?:/all)?/?(?!\?.*\bz=video)(?:[?#]|$)',
] ]
_TESTS = [{ _TESTS = [{
@@ -598,6 +602,9 @@ class VKUserVideosIE(VKBaseIE):
}, { }, {
'url': 'https://vk.com/video/playlist/-174476437_2', 'url': 'https://vk.com/video/playlist/-174476437_2',
'only_matching': True, 'only_matching': True,
}, {
'url': 'https://vkvideo.ru/playlist/-51890028_-2',
'only_matching': True,
}] }]
_VIDEO = collections.namedtuple('Video', ['owner_id', 'id']) _VIDEO = collections.namedtuple('Video', ['owner_id', 'id'])
@@ -723,7 +730,7 @@ class VKWallPostIE(VKBaseIE):
def _unmask_url(self, mask_url, vk_id): def _unmask_url(self, mask_url, vk_id):
if 'audio_api_unavailable' in mask_url: if 'audio_api_unavailable' in mask_url:
extra = mask_url.split('?extra=')[1].split('#') extra = mask_url.split('?extra=')[1].split('#')
func, base = self._decode(extra[1]).split(chr(11)) _, base = self._decode(extra[1]).split(chr(11))
mask_url = list(self._decode(extra[0])) mask_url = list(self._decode(extra[0]))
url_len = len(mask_url) url_len = len(mask_url)
indexes = [None] * url_len indexes = [None] * url_len

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