mirror of
https://github.com/yt-dlp/yt-dlp.git
synced 2026-08-15 08:39:09 +03:00
Compare commits
13
Commits
ad9a6f25f6
...
2026.07.04
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fdec00e0bf | ||
|
|
997fa14084 | ||
|
|
b6590aaa1e | ||
|
|
bed3d58c5c | ||
|
|
8c1f07d813 | ||
|
|
e8de28e23c | ||
|
|
1472d10980 | ||
|
|
6694ef8299 | ||
|
|
fa383a9efa | ||
|
|
5aa335ecd9 | ||
|
|
26654a359d | ||
|
|
161dd9fd05 | ||
|
|
ac4c955ea9 |
@@ -18,3 +18,7 @@ paths:
|
||||
ignore:
|
||||
# SC1090 "Can't follow non-constant source": ignore when using `source` to activate venv
|
||||
- '.+SC1090.+'
|
||||
.github/workflows/label-handler.yml:
|
||||
ignore:
|
||||
# https://github.com/rhysd/actionlint/issues/657
|
||||
- 'unexpected key "queue" for "concurrency" section.+'
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
name: Label Handler
|
||||
on:
|
||||
issues:
|
||||
types: [labeled]
|
||||
pull_request_target:
|
||||
types: [labeled] # zizmor: ignore[dangerous-triggers]
|
||||
|
||||
permissions: {}
|
||||
|
||||
concurrency:
|
||||
group: label
|
||||
cancel-in-progress: false
|
||||
queue: max
|
||||
|
||||
env:
|
||||
GH_TELEMETRY: "false"
|
||||
LLM_MESSAGE: >-
|
||||
This contribution has been determined to be in violation of yt-dlp's
|
||||
[**NO AI / NO LLM POLICY**](https://github.com/yt-dlp/yt-dlp/blob/master/CONTRIBUTING.md#no-ai--no-llm-policy).
|
||||
Repeat or flagrant violations of this policy will result in a permanent ban from this repository.
|
||||
BAD_MESSAGE: >-
|
||||
This contribution has been determined to be in violation of yt-dlp's
|
||||
[policy against supporting sites that are primarily used for
|
||||
piracy](https://github.com/yt-dlp/yt-dlp/blob/master/CONTRIBUTING.md#is-the-website-primarily-used-for-piracy).
|
||||
Please consult the [yt-dlp wiki
|
||||
FAQ](https://github.com/yt-dlp/yt-dlp/wiki/FAQ#why-is-there-a-rule-against-websites-primarily-used-for-piracy)
|
||||
if you have questions.
|
||||
|
||||
jobs:
|
||||
issue-llm:
|
||||
name: Issue (ai-policy-violation)
|
||||
if: github.event.issue.state == 'open' && contains(github.event.issue.labels.*.name, 'ai-policy-violation')
|
||||
permissions:
|
||||
issues: write # Needed to comment on, close, and lock issues
|
||||
runs-on: ubuntu-slim
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
ISSUE_NUMBER: ${{ github.event.issue.number }}
|
||||
steps:
|
||||
- name: Comment, close and lock issue
|
||||
run: |
|
||||
gh issue unlock "${ISSUE_NUMBER}" --repo "${GITHUB_REPOSITORY}" || true
|
||||
gh issue close "${ISSUE_NUMBER}" --repo "${GITHUB_REPOSITORY}" --reason "not planned" --comment "${LLM_MESSAGE}"
|
||||
gh issue lock "${ISSUE_NUMBER}" --repo "${GITHUB_REPOSITORY}"
|
||||
|
||||
issue-bad:
|
||||
name: Issue (piracy/illegal)
|
||||
if: github.event.issue.state == 'open' && contains(github.event.issue.labels.*.name, 'piracy/illegal')
|
||||
permissions:
|
||||
issues: write # Needed to comment on, close, and lock issues
|
||||
runs-on: ubuntu-slim
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
ISSUE_NUMBER: ${{ github.event.issue.number }}
|
||||
steps:
|
||||
- name: Comment, close and lock issue
|
||||
run: |
|
||||
gh issue unlock "${ISSUE_NUMBER}" --repo "${GITHUB_REPOSITORY}" || true
|
||||
gh issue close "${ISSUE_NUMBER}" --repo "${GITHUB_REPOSITORY}" --reason "not planned" --comment "${BAD_MESSAGE}"
|
||||
gh issue lock "${ISSUE_NUMBER}" --repo "${GITHUB_REPOSITORY}"
|
||||
|
||||
pr-llm:
|
||||
name: PR (ai-policy-violation)
|
||||
if: github.event.pull_request.state == 'open' && contains(github.event.pull_request.labels.*.name, 'ai-policy-violation')
|
||||
permissions:
|
||||
pull-requests: write # Needed to comment on, close, and lock PRs
|
||||
runs-on: ubuntu-slim
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
PR_NUMBER: ${{ github.event.pull_request.number }}
|
||||
steps:
|
||||
- name: Comment, close and lock PR
|
||||
run: |
|
||||
gh pr unlock "${PR_NUMBER}" --repo "${GITHUB_REPOSITORY}" || true
|
||||
gh pr close "${PR_NUMBER}" --repo "${GITHUB_REPOSITORY}" --comment "${LLM_MESSAGE}"
|
||||
gh pr lock "${PR_NUMBER}" --repo "${GITHUB_REPOSITORY}"
|
||||
|
||||
pr-bad:
|
||||
name: PR (piracy/illegal)
|
||||
if: github.event.pull_request.state == 'open' && contains(github.event.pull_request.labels.*.name, 'piracy/illegal')
|
||||
permissions:
|
||||
pull-requests: write # Needed to comment on, close, and lock PRs
|
||||
runs-on: ubuntu-slim
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
PR_NUMBER: ${{ github.event.pull_request.number }}
|
||||
steps:
|
||||
- name: Comment, close and lock PR
|
||||
run: |
|
||||
gh pr unlock "${PR_NUMBER}" --repo "${GITHUB_REPOSITORY}" || true
|
||||
gh pr close "${PR_NUMBER}" --repo "${GITHUB_REPOSITORY}" --comment "${BAD_MESSAGE}"
|
||||
gh pr lock "${PR_NUMBER}" --repo "${GITHUB_REPOSITORY}"
|
||||
@@ -886,3 +886,16 @@ MemoKing34
|
||||
Suntooth
|
||||
Ventriduct
|
||||
vpertys
|
||||
billauer
|
||||
BOplaid
|
||||
dialmaster
|
||||
dreammu
|
||||
FranciscoPombal
|
||||
HybridDog
|
||||
longcharmroeun
|
||||
Masterjun3
|
||||
noseb13eds
|
||||
Sec-Wayne
|
||||
selfhoster1312
|
||||
tcely
|
||||
tewhalen
|
||||
|
||||
@@ -4,6 +4,101 @@
|
||||
# To create a release, dispatch the https://github.com/yt-dlp/yt-dlp/actions/workflows/release.yml workflow on master
|
||||
-->
|
||||
|
||||
### 2026.07.04
|
||||
|
||||
#### Important changes
|
||||
- **The minimum *recommended* Python version has been raised to 3.11**
|
||||
- Since Python 3.10 will reach its end-of-life in October 2026, support for it will be dropped soon. [Read more](https://github.com/yt-dlp/yt-dlp/issues/16916)
|
||||
- **The official Windows release binaries will soon require Windows 10 or later.** [Read more](https://github.com/yt-dlp/yt-dlp/issues/16917)
|
||||
- Security: [[CVE-2026-55404](https://nvd.nist.gov/vuln/detail/CVE-2026-55404)] [Downstream command injection via improper sanitization of --write-link output](https://github.com/yt-dlp/yt-dlp/security/advisories/GHSA-6v4j-43gg-vj32)
|
||||
- Shortcut file data is now properly validated and sanitized when the `--write-link` options are used
|
||||
|
||||
#### Core changes
|
||||
- [Always include warnings in debug output](https://github.com/yt-dlp/yt-dlp/commit/d23e6f5a387d5933bc24e1eb5437da8fd563c1f0) ([#17059](https://github.com/yt-dlp/yt-dlp/issues/17059)) by [bashonly](https://github.com/bashonly)
|
||||
- [Fix `allow-unsafe-ext` compat option](https://github.com/yt-dlp/yt-dlp/commit/e47691215f75fe7e9684080d17fadf340c9a8450) ([#16920](https://github.com/yt-dlp/yt-dlp/issues/16920)) by [bashonly](https://github.com/bashonly)
|
||||
- [Raise minimum recommended Python version to 3.11](https://github.com/yt-dlp/yt-dlp/commit/7b03011294c0210802ffc901390006c39152b999) ([#17034](https://github.com/yt-dlp/yt-dlp/issues/17034)) by [bashonly](https://github.com/bashonly)
|
||||
- [Validate and escape values in `--write-link` output](https://github.com/yt-dlp/yt-dlp/commit/b6590aaa1e3808155d69c9a79a797ae484163789) by [bashonly](https://github.com/bashonly)
|
||||
- **cookies**: [Use `'` instead of `"` for SQL string quoting](https://github.com/yt-dlp/yt-dlp/commit/7a569456f24fd9afcda09ae55c10d3bbd03f46ef) ([#17078](https://github.com/yt-dlp/yt-dlp/issues/17078)) by [Grub4K](https://github.com/Grub4K)
|
||||
- **utils**
|
||||
- [Deprecate `make_dir` in favor of `make_parent_dirs`](https://github.com/yt-dlp/yt-dlp/commit/b05b408d10ebf8f4c47c0db1236eb713b6ad7ab6) ([#16931](https://github.com/yt-dlp/yt-dlp/issues/16931)) by [doe1080](https://github.com/doe1080)
|
||||
- `HTTPHeaderDict`: [Fix `__ior__`](https://github.com/yt-dlp/yt-dlp/commit/cb309b3293c9919cfb55f5d9ffa2c8c109a5f1eb) ([#16930](https://github.com/yt-dlp/yt-dlp/issues/16930)) by [doe1080](https://github.com/doe1080)
|
||||
- `parse_duration`: [Return `int` when appropriate](https://github.com/yt-dlp/yt-dlp/commit/e584a65f2a0feee0c6c363b3309e9ebd6065f6b4) ([#13899](https://github.com/yt-dlp/yt-dlp/issues/13899)) by [doe1080](https://github.com/doe1080)
|
||||
- `parse_resolution`: [Support fps suffixes](https://github.com/yt-dlp/yt-dlp/commit/1249676e98aecf2131901f10f0230fb5e1bdc17e) ([#17073](https://github.com/yt-dlp/yt-dlp/issues/17073)) by [doe1080](https://github.com/doe1080)
|
||||
- `pkcs1pad`: [Fix invalid PKCS#1 v1.5 padding bytes](https://github.com/yt-dlp/yt-dlp/commit/25a05fc0e24ebda38578e87c3f6772a149522786) ([#17035](https://github.com/yt-dlp/yt-dlp/issues/17035)) by [doe1080](https://github.com/doe1080)
|
||||
- `qualities`: [Avoid repeated index lookups](https://github.com/yt-dlp/yt-dlp/commit/3cf981af60dd0b1b6c40807fb542d68ddb0a5f84) ([#17025](https://github.com/yt-dlp/yt-dlp/issues/17025)) by [doe1080](https://github.com/doe1080)
|
||||
- `random_user_agent`: [Bump version range 143-149 => 144-150](https://github.com/yt-dlp/yt-dlp/commit/d120d841dde18839f28cba4a8b0df6d3c4fc6761) ([#17117](https://github.com/yt-dlp/yt-dlp/issues/17117)) by [bashonly](https://github.com/bashonly), [dlp-bot](https://github.com/dlp-bot)
|
||||
|
||||
#### Extractor changes
|
||||
- [Always include `id` in request error output](https://github.com/yt-dlp/yt-dlp/commit/af8a3f34370c82f323ed30074e3fe741ebcc98ca) ([#17068](https://github.com/yt-dlp/yt-dlp/issues/17068)) by [bashonly](https://github.com/bashonly), [HybridDog](https://github.com/HybridDog)
|
||||
- [Fix request logging](https://github.com/yt-dlp/yt-dlp/commit/c13e2f8a20fc1fafb31cba4e6287c874bc0c0cc0) ([#17072](https://github.com/yt-dlp/yt-dlp/issues/17072)) by [bashonly](https://github.com/bashonly)
|
||||
- **arte**: [Fix playlist support](https://github.com/yt-dlp/yt-dlp/commit/16bdcc525e6a550781d65d6fed92a37800ad95e1) ([#13191](https://github.com/yt-dlp/yt-dlp/issues/13191)) by [1100101](https://github.com/1100101)
|
||||
- **bandcamp**: weekly: [Fix extractor](https://github.com/yt-dlp/yt-dlp/commit/a541df1ea5a593abf3ceaf94ed806e4b52a2c459) ([#16925](https://github.com/yt-dlp/yt-dlp/issues/16925)) by [bashonly](https://github.com/bashonly) (With fixes in [9055188](https://github.com/yt-dlp/yt-dlp/commit/9055188250348c3e6e29eee53e5fb3dc2c951977))
|
||||
- **bilibili**: [Fix API extraction](https://github.com/yt-dlp/yt-dlp/commit/e8de28e23c1ecb4a12b2c3dec188c07e998c412c) ([#13730](https://github.com/yt-dlp/yt-dlp/issues/13730)) by [grqz](https://github.com/grqz)
|
||||
- **instagram**
|
||||
- [Detect when cookies are invalidated](https://github.com/yt-dlp/yt-dlp/commit/ac4c955ea93f35a3a426903141436a6468bddf9f) ([#17126](https://github.com/yt-dlp/yt-dlp/issues/17126)) by [bashonly](https://github.com/bashonly)
|
||||
- [Rework extractor](https://github.com/yt-dlp/yt-dlp/commit/f49b551a0c4c25358d2afaeda4ee63989d2d56ab) ([#17075](https://github.com/yt-dlp/yt-dlp/issues/17075)) by [bashonly](https://github.com/bashonly) (With fixes in [8b8e3e3](https://github.com/yt-dlp/yt-dlp/commit/8b8e3e3cb4d3ba0dedf7b1fd00ce68f07da7e588))
|
||||
- **linkedin**: [Remove broken login support](https://github.com/yt-dlp/yt-dlp/commit/a5e0f87140c6ea73ad3f34914339e873681b4dca) ([#17039](https://github.com/yt-dlp/yt-dlp/issues/17039)) by [0xvd](https://github.com/0xvd)
|
||||
- **mgtv**: [Fix VIP stream extraction](https://github.com/yt-dlp/yt-dlp/commit/6b67e1f2b77feb6d97852be08bfc2e0ab1c8aef0) ([#16982](https://github.com/yt-dlp/yt-dlp/issues/16982)) by [longcharmroeun](https://github.com/longcharmroeun)
|
||||
- **mxplayer**
|
||||
- [Fix extractors](https://github.com/yt-dlp/yt-dlp/commit/01f4f06fdd1e1e088981fa4af3422806aefa0c2a) ([#16988](https://github.com/yt-dlp/yt-dlp/issues/16988)) by [0xvd](https://github.com/0xvd)
|
||||
- [Rework extractors](https://github.com/yt-dlp/yt-dlp/commit/55a58debec7fa5bbaa119dfbc874fb84dd48c76e) ([#17018](https://github.com/yt-dlp/yt-dlp/issues/17018)) by [doe1080](https://github.com/doe1080)
|
||||
- **niconico**
|
||||
- [Fix error detection](https://github.com/yt-dlp/yt-dlp/commit/c4f94545c9d3ce356f2f3149c8fde2134073cee2) ([#16991](https://github.com/yt-dlp/yt-dlp/issues/16991)) by [doe1080](https://github.com/doe1080)
|
||||
- [Support shorts](https://github.com/yt-dlp/yt-dlp/commit/4af5541bb56b08d462f32773dfa15c207de13b74) ([#16992](https://github.com/yt-dlp/yt-dlp/issues/16992)) by [doe1080](https://github.com/doe1080)
|
||||
- **omnyfm**: [Add extractors](https://github.com/yt-dlp/yt-dlp/commit/fa383a9efa02c2f89aba315b17e47fb6bf9e5bee) ([#15942](https://github.com/yt-dlp/yt-dlp/issues/15942)) by [doe1080](https://github.com/doe1080)
|
||||
- **openrec**: [Rework extractors](https://github.com/yt-dlp/yt-dlp/commit/5aa335ecd9d12251b63b5afb23e166ea63cd7271) ([#16857](https://github.com/yt-dlp/yt-dlp/issues/16857)) by [doe1080](https://github.com/doe1080) (With fixes in [6694ef8](https://github.com/yt-dlp/yt-dlp/commit/6694ef82993396847e5a66802634c12944c42a5e))
|
||||
- **patreon**: [Support new URL format](https://github.com/yt-dlp/yt-dlp/commit/707537a03946fbc5707e22be429545c670cd8ec2) ([#16926](https://github.com/yt-dlp/yt-dlp/issues/16926)) by [0xvd](https://github.com/0xvd)
|
||||
- **peertube**: [Support password-protected videos](https://github.com/yt-dlp/yt-dlp/commit/a2483524fbf9c1f5406774622d8d048430b320e9) ([#16873](https://github.com/yt-dlp/yt-dlp/issues/16873)) by [selfhoster1312](https://github.com/selfhoster1312)
|
||||
- **periscope**: [Improve metadata extraction](https://github.com/yt-dlp/yt-dlp/commit/2b27a203f7573cb491c8bef77cb4d944cee6f8cf) ([#16084](https://github.com/yt-dlp/yt-dlp/issues/16084)) by [doe1080](https://github.com/doe1080)
|
||||
- **reddit**: [Remove broken login support](https://github.com/yt-dlp/yt-dlp/commit/917dad55e5b4dbf88d61541477d1830740ab3115) ([#17038](https://github.com/yt-dlp/yt-dlp/issues/17038)) by [0xvd](https://github.com/0xvd)
|
||||
- **soundcloud**
|
||||
- [Extract `uploader_url` for playlists](https://github.com/yt-dlp/yt-dlp/commit/f69e64a954524518add538648f6327611349ead4) ([#16842](https://github.com/yt-dlp/yt-dlp/issues/16842)) by [noseb13eds](https://github.com/noseb13eds)
|
||||
- [Extract comments](https://github.com/yt-dlp/yt-dlp/commit/785e507ef06b709f7f68744f29c16a5cdb8942f2) ([#16938](https://github.com/yt-dlp/yt-dlp/issues/16938)) by [0xvd](https://github.com/0xvd)
|
||||
- [Improve metadata extraction](https://github.com/yt-dlp/yt-dlp/commit/8bdfbfd4461a643e5c37a232b0efd7bcd86a3091) ([#17088](https://github.com/yt-dlp/yt-dlp/issues/17088)) by [noseb13eds](https://github.com/noseb13eds)
|
||||
- **streaks**: [Fix extractor](https://github.com/yt-dlp/yt-dlp/commit/26654a359d8d8dbe3c95b8fc6b0b09aca9a17fba) ([#16413](https://github.com/yt-dlp/yt-dlp/issues/16413)) by [doe1080](https://github.com/doe1080)
|
||||
- **svt**: [Fix extractor](https://github.com/yt-dlp/yt-dlp/commit/c84b2c6736ac5fac6c3eb9a0cbbeee4d6ed84fdd) ([#16288](https://github.com/yt-dlp/yt-dlp/issues/16288)) by [billauer](https://github.com/billauer), [dirkf](https://github.com/dirkf)
|
||||
- **telewebion**: [Fix extractor](https://github.com/yt-dlp/yt-dlp/commit/24aecad5df6090ef9c2a9deff27a7182c5164f07) ([#16986](https://github.com/yt-dlp/yt-dlp/issues/16986)) by [BOplaid](https://github.com/BOplaid)
|
||||
- **trovo**: [Remove dead extractors](https://github.com/yt-dlp/yt-dlp/commit/acc995cf9137918568857d39b68a94498726543a) ([#16353](https://github.com/yt-dlp/yt-dlp/issues/16353)) by [doe1080](https://github.com/doe1080)
|
||||
- **truth**: [Support share URLs](https://github.com/yt-dlp/yt-dlp/commit/e7b5d68f372499167fbf9711de42d3180a92e4de) ([#16096](https://github.com/yt-dlp/yt-dlp/issues/16096)) by [InvalidUsernameException](https://github.com/InvalidUsernameException)
|
||||
- **tviplayer**: [Fix extractor](https://github.com/yt-dlp/yt-dlp/commit/9ae7df9a22b29e2f81825230c9bba7d444190de0) ([#16527](https://github.com/yt-dlp/yt-dlp/issues/16527)) by [FranciscoPombal](https://github.com/FranciscoPombal)
|
||||
- **unsupported**: [Update unsupported sites](https://github.com/yt-dlp/yt-dlp/commit/5678b282e2a17a8181e682a9681461b9c82ff008) ([#17085](https://github.com/yt-dlp/yt-dlp/issues/17085)) by [bashonly](https://github.com/bashonly)
|
||||
- **wrestleuniverse**: vod: [Fix extractor](https://github.com/yt-dlp/yt-dlp/commit/498e51f5da47539f3b4cc52ff85be5f33e7e9d2f) ([#16685](https://github.com/yt-dlp/yt-dlp/issues/16685)) by [0xvd](https://github.com/0xvd), [bashonly](https://github.com/bashonly), [Sec-Wayne](https://github.com/Sec-Wayne), [tewhalen](https://github.com/tewhalen)
|
||||
- **youtube**
|
||||
- [Fix `extract_relative_time` for abbreviated units](https://github.com/yt-dlp/yt-dlp/commit/6a24c96f7f61e5e651466cc3d4c6a30982318efe) ([#16687](https://github.com/yt-dlp/yt-dlp/issues/16687)) by [dialmaster](https://github.com/dialmaster)
|
||||
- [Fix detection of forced preroll wait time](https://github.com/yt-dlp/yt-dlp/commit/3c279b33cb6d1133624a468e71560c6a75039586) ([#17062](https://github.com/yt-dlp/yt-dlp/issues/17062)) by [bashonly](https://github.com/bashonly)
|
||||
- [Fix minor issues](https://github.com/yt-dlp/yt-dlp/commit/57528faa361314a69d47aead8f44c6a5380ca66a) ([#17060](https://github.com/yt-dlp/yt-dlp/issues/17060)) by [doe1080](https://github.com/doe1080)
|
||||
- [Support live adaptive formats](https://github.com/yt-dlp/yt-dlp/commit/8c1f07d813175cbcf84651f27a827bad84fd8184) ([#16771](https://github.com/yt-dlp/yt-dlp/issues/16771)) by [dreammu](https://github.com/dreammu)
|
||||
- tab
|
||||
- [Fix flat extraction of collaborators](https://github.com/yt-dlp/yt-dlp/commit/a75ba96fa48522738b91a907773a6fa9efe6e2d4) ([#17045](https://github.com/yt-dlp/yt-dlp/issues/17045)) by [bashonly](https://github.com/bashonly)
|
||||
- [Fix flat playlist metadata extraction](https://github.com/yt-dlp/yt-dlp/commit/ad6b5f4b3552472c17b3955a3f1525296bed6137) ([#16965](https://github.com/yt-dlp/yt-dlp/issues/16965)) by [bashonly](https://github.com/bashonly)
|
||||
- [Fix metadata extraction](https://github.com/yt-dlp/yt-dlp/commit/d6c411bcd0a0519a0db3b330df2530c8213eb9f0) ([#16976](https://github.com/yt-dlp/yt-dlp/issues/16976)) by [bashonly](https://github.com/bashonly)
|
||||
- [Fix pagination](https://github.com/yt-dlp/yt-dlp/commit/b23046bbc8e53f32a3853dc33138f2986f3aed06) ([#16948](https://github.com/yt-dlp/yt-dlp/issues/16948)) by [bashonly](https://github.com/bashonly)
|
||||
- **zan**: [Add extractor](https://github.com/yt-dlp/yt-dlp/commit/ad9a6f25f69344ebc060f7be223e5c19fc03e9b5) ([#16086](https://github.com/yt-dlp/yt-dlp/issues/16086)) by [doe1080](https://github.com/doe1080)
|
||||
- **zdf**: [Detect livestreams](https://github.com/yt-dlp/yt-dlp/commit/cfee151fcd9e1a0f4e5af9ca8440f1253cb70b88) ([#16954](https://github.com/yt-dlp/yt-dlp/issues/16954)) by [InvalidUsernameException](https://github.com/InvalidUsernameException)
|
||||
- **zoom**: clips: [Add extractor](https://github.com/yt-dlp/yt-dlp/commit/4a6296248fb6218d77221da4e5421c84bbc792d4) ([#17005](https://github.com/yt-dlp/yt-dlp/issues/17005)) by [Bnyro](https://github.com/Bnyro)
|
||||
|
||||
#### Downloader changes
|
||||
- **external**
|
||||
- [Fix resuming downloads with aria2c](https://github.com/yt-dlp/yt-dlp/commit/500e54cf860e4807d259bfe6a7abb47e51364a3b) ([#11698](https://github.com/yt-dlp/yt-dlp/issues/11698)) by [tcely](https://github.com/tcely)
|
||||
- `curl`: [Support development versions](https://github.com/yt-dlp/yt-dlp/commit/7937a139cf3e1e1ccd1b277f18ab73fc6dec06a7) ([#16922](https://github.com/yt-dlp/yt-dlp/issues/16922)) by [syphyr](https://github.com/syphyr)
|
||||
- **mhtml**: [Fix storyboard content-length calculation](https://github.com/yt-dlp/yt-dlp/commit/8902f6ba8c7baba8fb43fb08ea1a9ddfef77e998) ([#13998](https://github.com/yt-dlp/yt-dlp/issues/13998)) by [Masterjun3](https://github.com/Masterjun3)
|
||||
|
||||
#### Misc. changes
|
||||
- **build**
|
||||
- [Update 17 dependencies](https://github.com/yt-dlp/yt-dlp/commit/8a5f1dd3bd39ad2e917755a29b313a51b5446871) ([#17014](https://github.com/yt-dlp/yt-dlp/issues/17014)) by [dlp-bot](https://github.com/dlp-bot)
|
||||
- [Update 6 dependencies](https://github.com/yt-dlp/yt-dlp/commit/40dd052c03c2d1b5c180f393d9344be2bd718ba3) ([#17086](https://github.com/yt-dlp/yt-dlp/issues/17086)) by [dlp-bot](https://github.com/dlp-bot)
|
||||
- **ci**
|
||||
- [Add label handler workflow](https://github.com/yt-dlp/yt-dlp/commit/1472d1098020d46ea1f15940b80d515ad13eb420) ([#17128](https://github.com/yt-dlp/yt-dlp/issues/17128)) by [bashonly](https://github.com/bashonly)
|
||||
- [Bump actions/checkout v6.0.3 => v7.0.0](https://github.com/yt-dlp/yt-dlp/commit/da99b21b2d6e32690d1871afc9e9779701dd7f8c) ([#17015](https://github.com/yt-dlp/yt-dlp/issues/17015)) by [dlp-bot](https://github.com/dlp-bot)
|
||||
- [Update 5 actions in 8 workflows](https://github.com/yt-dlp/yt-dlp/commit/84db5246faab5bffb8909de60c75e7c692b5dd79) ([#17024](https://github.com/yt-dlp/yt-dlp/issues/17024)) by [dlp-bot](https://github.com/dlp-bot)
|
||||
- **cleanup**
|
||||
- [Fix invalid info dict fields](https://github.com/yt-dlp/yt-dlp/commit/98036ccd4fd9a6b15e025bbc164e27286e342be7) ([#17007](https://github.com/yt-dlp/yt-dlp/issues/17007)) by [seproDev](https://github.com/seproDev)
|
||||
- [Fix minor mistakes](https://github.com/yt-dlp/yt-dlp/commit/b0472c3bce71eb08996a8d70c4c1b1df8cad4042) ([#17083](https://github.com/yt-dlp/yt-dlp/issues/17083)) by [doe1080](https://github.com/doe1080)
|
||||
- [Remove RTSP and MMS support](https://github.com/yt-dlp/yt-dlp/commit/bed3d58c5c1ff088584c5dac3c16eea4ff01c7b5) ([#17030](https://github.com/yt-dlp/yt-dlp/issues/17030)) by [seproDev](https://github.com/seproDev)
|
||||
- [Replace dead example/test URL](https://github.com/yt-dlp/yt-dlp/commit/c102b2096525218a6918a46b415fb167786f9656) ([#17061](https://github.com/yt-dlp/yt-dlp/issues/17061)) by [bashonly](https://github.com/bashonly)
|
||||
- Miscellaneous: [997fa14](https://github.com/yt-dlp/yt-dlp/commit/997fa140840a08df3938b40da470c78049fef1f6) by [bashonly](https://github.com/bashonly), [seproDev](https://github.com/seproDev)
|
||||
- **docs**: [Make "No AI / No LLM Policy" abundantly clear](https://github.com/yt-dlp/yt-dlp/commit/249aa5d6e667308fbf95ae5cfb40eba8177a802c) ([#16285](https://github.com/yt-dlp/yt-dlp/issues/16285)) by [bashonly](https://github.com/bashonly)
|
||||
|
||||
### 2026.06.09
|
||||
|
||||
#### Important changes
|
||||
|
||||
+7
-1
@@ -78,6 +78,13 @@ Core Maintainers are responsible for reviewing and merging contributions, publis
|
||||
* Improved/fixed/added ArdMediathek, DRTV, Floatplane, MagentaMusik, Naver, Nebula, OnDemandKorea, Vbox7 etc
|
||||
|
||||
|
||||
## Maintainers
|
||||
|
||||
Maintainers are stewards of the project's codebase who can review and merge pull requests.
|
||||
|
||||
- [doe1080](https://github.com/doe1080)
|
||||
|
||||
|
||||
## Triage Maintainers
|
||||
|
||||
Triage Maintainers are frequent contributors who can manage issues and pull requests.
|
||||
@@ -86,5 +93,4 @@ Triage Maintainers are frequent contributors who can manage issues and pull requ
|
||||
- [garret1317](https://github.com/garret1317)
|
||||
- [pzhlkj6612](https://github.com/pzhlkj6612)
|
||||
- [DTrombett](https://github.com/dtrombett)
|
||||
- [doe1080](https://github.com/doe1080)
|
||||
- [grqz](https://github.com/grqz)
|
||||
|
||||
@@ -245,7 +245,6 @@ The following provide support for impersonating browser requests. This may be re
|
||||
### Deprecated
|
||||
|
||||
* [**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)
|
||||
|
||||
To use or redistribute the dependencies, you must agree to their respective licensing terms.
|
||||
|
||||
@@ -403,7 +402,7 @@ Tip: Use `CTRL`+`F` (or `Command`+`F`) to search by keywords
|
||||
(default)
|
||||
--live-from-start Download livestreams from the start.
|
||||
Currently experimental and only supported
|
||||
for YouTube, Twitch, and TVer
|
||||
for YouTube, Twitch, TVer, and mellow-fan
|
||||
--no-live-from-start Download livestreams from the current time
|
||||
(default)
|
||||
--wait-for-video MIN[-MAX] Wait for scheduled streams to become
|
||||
@@ -625,16 +624,16 @@ Tip: Use `CTRL`+`F` (or `Command`+`F`) to search by keywords
|
||||
"*10:15-inf" --download-sections "intro"
|
||||
--downloader [PROTO:]NAME Name or path of the external downloader to
|
||||
use (optionally) prefixed by the protocols
|
||||
(http, ftp, m3u8, dash, rstp, rtmp, mms) to
|
||||
use it for. Currently supports native,
|
||||
aria2c, axel, curl, ffmpeg, httpie, wget.
|
||||
You can use this option multiple times to
|
||||
set different downloaders for different
|
||||
protocols. E.g. --downloader aria2c
|
||||
--downloader "dash,m3u8:native" will use
|
||||
aria2c for http/ftp downloads, and the
|
||||
native downloader for dash/m3u8 downloads
|
||||
(Alias: --external-downloader)
|
||||
(http, ftp, m3u8, dash, rtmp) to use it for.
|
||||
Currently supports native, aria2c, axel,
|
||||
curl, ffmpeg, httpie, wget. You can use this
|
||||
option multiple times to set different
|
||||
downloaders for different protocols. E.g.
|
||||
--downloader aria2c --downloader
|
||||
"dash,m3u8:native" will use aria2c for
|
||||
http/ftp downloads, and the native
|
||||
downloader for dash/m3u8 downloads (Alias:
|
||||
--external-downloader)
|
||||
--downloader-args NAME:ARGS Give these arguments to the external
|
||||
downloader. Specify the downloader name and
|
||||
the arguments separated by a colon ":". For
|
||||
@@ -1587,7 +1586,7 @@ Also filtering work for comparisons `=` (equals), `^=` (starts with), `$=` (ends
|
||||
- `acodec`: Name of the audio codec in use
|
||||
- `vcodec`: Name of the video codec in use
|
||||
- `container`: Name of the container format
|
||||
- `protocol`: The protocol that will be used for the actual download, lower-case (`http`, `https`, `rtsp`, `rtmp`, `rtmpe`, `mms`, `f4m`, `ism`, `http_dash_segments`, `m3u8`, or `m3u8_native`)
|
||||
- `protocol`: The protocol that will be used for the actual download, lower-case (`http`, `https`, `rtmp`, `rtmpe`, `f4m`, `ism`, `http_dash_segments`, `m3u8`, or `m3u8_native`)
|
||||
- `language`: Language code
|
||||
- `dynamic_range`: The dynamic range of the video
|
||||
- `format_id`: A short description of the format
|
||||
@@ -1615,7 +1614,7 @@ The available fields are:
|
||||
- `lang`: The language preference as determined by the extractor (e.g. original language preferred over audio description)
|
||||
- `quality`: The quality of the format
|
||||
- `source`: The preference of the source
|
||||
- `proto`: Protocol used for download (`https`/`ftps` > `http`/`ftp` > `m3u8_native`/`m3u8` > `http_dash_segments`> `websocket_frag` > `mms`/`rtsp` > `f4f`/`f4m`)
|
||||
- `proto`: Protocol used for download (`https`/`ftps` > `http`/`ftp` > `m3u8_native`/`m3u8` > `http_dash_segments`> `websocket_frag` > `f4f`/`f4m`)
|
||||
- `vcodec`: Video Codec (`av01` > `vp9.2` > `vp9` > `h265` > `h264` > `vp8` > `h263` > `theora` > other)
|
||||
- `acodec`: Audio Codec (`flac`/`alac` > `wav`/`aiff` > `opus` > `vorbis` > `aac` > `mp4a` > `mp3` > `ac4` > `eac3` > `ac3` > `dts` > other)
|
||||
- `codec`: Equivalent to `vcodec,acodec`
|
||||
@@ -1871,7 +1870,7 @@ The following extractors use this feature:
|
||||
* `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,max-depth`. Default is `all,all,all,all,all`
|
||||
* A `max-depth` value of `1` will discard all replies, regardless of the `max-replies` or `max-replies-per-thread` values given
|
||||
* E.g. `all,all,1000,10,2` will get a maximum of 1000 replies total, with up to 10 replies per thread, and only 2 levels of depth (i.e. top-level comments plus their immediate replies). `1000,all,100` will get a maximum of 1000 comments, with a maximum of 100 replies total
|
||||
* `formats`: Change the types of formats to return. `dashy` (convert HTTP to DASH), `duplicate` (identical content but different URLs or protocol; includes `dashy`), `incomplete` (cannot be downloaded completely - live dash, live adaptive https, and post-live m3u8), `missing_pot` (include formats that require a PO Token but are missing one)
|
||||
* `formats`: Change the types of formats to return. `dashy` (convert HTTP to DASH), `duplicate` (identical content but different URLs or protocol; includes `dashy`), `incomplete` (cannot be downloaded completely - live and post-live dash, post-live m3u8, and live adaptive https without --live-from-start), `missing_pot` (include formats that require a PO Token but are missing one)
|
||||
* `innertube_host`: Innertube API host to use for all API requests; e.g. `studio.youtube.com`, `youtubei.googleapis.com`. Note that cookies exported from one subdomain will not work on others
|
||||
* `innertube_key`: Innertube API key to use for all API requests. By default, no API key is used
|
||||
* `raise_incomplete_data`: `Incomplete Data Received` raises an error instead of reporting a warning
|
||||
@@ -1969,6 +1968,9 @@ The following extractors use this feature:
|
||||
#### sonylivseries
|
||||
* `sort_order`: Episode sort order for series extraction - one of `asc` (ascending, oldest first) or `desc` (descending, newest first). Default is `asc`
|
||||
|
||||
#### streaks
|
||||
* `api_key`: API key for the `X-Streaks-Api-Key` header
|
||||
|
||||
#### tver
|
||||
* `backend`: Backend API to use for extraction - one of `streaks` (default) or `brightcove` (deprecated)
|
||||
|
||||
|
||||
@@ -367,5 +367,20 @@
|
||||
"action": "add",
|
||||
"when": "25056f0d2d47adbd235a8d422fa62d68d0be2bc2",
|
||||
"short": "[priority] Security: [[CVE-2026-50574](https://nvd.nist.gov/vuln/detail/CVE-2026-50574)] [Arbitrary code execution via manifest downloads with aria2c](https://github.com/yt-dlp/yt-dlp/security/advisories/GHSA-vx4q-3cr2-7cg2)\n - Impact is limited to users of `--downloader aria2c`\n - Support for downloading HLS and DASH formats with aria2c has been removed. Users affected by this change should migrate to use `-N` for concurrent fragment downloads via the native downloader"
|
||||
},
|
||||
{
|
||||
"action": "add",
|
||||
"when": "7b03011294c0210802ffc901390006c39152b999",
|
||||
"short": "[priority] **The minimum *recommended* Python version has been raised to 3.11**\n - Since Python 3.10 will reach its end-of-life in October 2026, support for it will be dropped soon. [Read more](https://github.com/yt-dlp/yt-dlp/issues/16916)"
|
||||
},
|
||||
{
|
||||
"action": "add",
|
||||
"when": "7b03011294c0210802ffc901390006c39152b999",
|
||||
"short": "[priority] **The official Windows release binaries will soon require Windows 10 or later.** [Read more](https://github.com/yt-dlp/yt-dlp/issues/16917)"
|
||||
},
|
||||
{
|
||||
"action": "add",
|
||||
"when": "b6590aaa1e3808155d69c9a79a797ae484163789",
|
||||
"short": "[priority] Security: [[CVE-2026-55404](https://nvd.nist.gov/vuln/detail/CVE-2026-55404)] [Downstream command injection via improper sanitization of --write-link output](https://github.com/yt-dlp/yt-dlp/security/advisories/GHSA-6v4j-43gg-vj32)\n - Shortcut file data is now properly validated and sanitized when the `--write-link` options are used"
|
||||
}
|
||||
]
|
||||
|
||||
+32
-28
@@ -42,7 +42,7 @@ The only reliable way to check if a site is supported is to try it.
|
||||
- **AcFunVideo**
|
||||
- **ADN**: [*animationdigitalnetwork*](## "netrc machine") Animation Digital Network
|
||||
- **ADNSeason**: [*animationdigitalnetwork*](## "netrc machine") Animation Digital Network
|
||||
- **AdobeConnect**
|
||||
- **AdobeConnect**: (**Currently broken**)
|
||||
- **adobetv**
|
||||
- **AdultSwim**
|
||||
- **aenetworks**: A+E Networks: A&E, Lifetime, History.com, FYI Network and History Vault
|
||||
@@ -326,7 +326,7 @@ The only reliable way to check if a site is supported is to try it.
|
||||
- **DeuxMNews**
|
||||
- **DHM**: Filmarchiv - Deutsches Historisches Museum (**Currently broken**)
|
||||
- **DigitalConcertHall**: [*digitalconcerthall*](## "netrc machine") DigitalConcertHall extractor
|
||||
- **DigitallySpeaking**
|
||||
- **DigitallySpeaking**: (**Currently broken**)
|
||||
- **Digiteka**
|
||||
- **Digiview**
|
||||
- **DiscogsReleasePlaylist**
|
||||
@@ -430,7 +430,7 @@ The only reliable way to check if a site is supported is to try it.
|
||||
- **Flickr**
|
||||
- **Floatplane**
|
||||
- **FloatplaneChannel**
|
||||
- **Folketinget**: Folketinget (ft.dk; Danish parliament)
|
||||
- **Folketinget**: Folketinget (ft.dk; Danish parliament) (**Currently broken**)
|
||||
- **FoodNetwork**
|
||||
- **FootyRoom**
|
||||
- **Formula1**
|
||||
@@ -504,7 +504,6 @@ The only reliable way to check if a site is supported is to try it.
|
||||
- **GoDiscovery**
|
||||
- **GodResource**
|
||||
- **GodTube**: (**Currently broken**)
|
||||
- **Gofile**
|
||||
- **Golem**
|
||||
- **goodgame:stream**
|
||||
- **GoogleDrive**
|
||||
@@ -620,7 +619,7 @@ The only reliable way to check if a site is supported is to try it.
|
||||
- **Kakao**
|
||||
- **Kaltura**
|
||||
- **KankaNews**: (**Currently broken**)
|
||||
- **Karaoketv**
|
||||
- **Karaoketv**: (**Currently broken**)
|
||||
- **Katsomo**: (**Currently broken**)
|
||||
- **KelbyOne**: (**Currently broken**)
|
||||
- **Kenh14Playlist**
|
||||
@@ -679,10 +678,10 @@ The only reliable way to check if a site is supported is to try it.
|
||||
- **life:embed**
|
||||
- **likee**
|
||||
- **likee:user**
|
||||
- **LinkedIn**: [*linkedin*](## "netrc machine")
|
||||
- **linkedin:events**: [*linkedin*](## "netrc machine")
|
||||
- **linkedin:learning**: [*linkedin*](## "netrc machine")
|
||||
- **linkedin:learning:course**: [*linkedin*](## "netrc machine")
|
||||
- **LinkedIn**
|
||||
- **linkedin:events**
|
||||
- **linkedin:learning**
|
||||
- **linkedin:learning:course**
|
||||
- **Liputan6**
|
||||
- **ListenNotes**
|
||||
- **LiTV**
|
||||
@@ -743,6 +742,12 @@ The only reliable way to check if a site is supported is to try it.
|
||||
- **megatvcom**: megatv.com videos
|
||||
- **megatvcom:embed**: megatv.com embedded videos
|
||||
- **Meipai**: 美拍
|
||||
- **mellowfan**: [*mellowfan*](## "netrc machine") mellow-fan
|
||||
- **mellowfan:capture**: [*mellowfan*](## "netrc machine")
|
||||
- **mellowfan:channel**: [*mellowfan*](## "netrc machine")
|
||||
- **mellowfan:channel:search**: [*mellowfan*](## "netrc machine")
|
||||
- **mellowfan:movie**: [*mellowfan*](## "netrc machine")
|
||||
- **mellowfan:playlist**: [*mellowfan*](## "netrc machine")
|
||||
- **MelonVOD**
|
||||
- **Metacritic**
|
||||
- **mewatch**
|
||||
@@ -797,8 +802,9 @@ The only reliable way to check if a site is supported is to try it.
|
||||
- **Mx3**
|
||||
- **Mx3Neo**
|
||||
- **Mx3Volksmusik**
|
||||
- **Mxplayer**
|
||||
- **MxplayerShow**
|
||||
- **mxplayer**: Amazon MX Player
|
||||
- **mxplayer:season**
|
||||
- **mxplayer:show**
|
||||
- **MySpace**
|
||||
- **MySpace:album**
|
||||
- **MySpass**
|
||||
@@ -939,8 +945,11 @@ The only reliable way to check if a site is supported is to try it.
|
||||
- **Odnoklassniki**
|
||||
- **OfTV**
|
||||
- **OfTVPlaylist**
|
||||
- **OktoberfestTV**
|
||||
- **OktoberfestTV**: (**Currently broken**)
|
||||
- **OlympicsReplay**
|
||||
- **Omnyfm**
|
||||
- **OmnyfmPlaylist**
|
||||
- **OmnyfmShow**
|
||||
- **on24**: ON24
|
||||
- **OnDemandChinaEpisode**
|
||||
- **OnDemandKorea**
|
||||
@@ -954,9 +963,6 @@ The only reliable way to check if a site is supported is to try it.
|
||||
- **onsen**: [*onsen*](## "netrc machine") インターネットラジオステーション<音泉>
|
||||
- **Opencast**
|
||||
- **OpencastPlaylist**
|
||||
- **openrec**
|
||||
- **openrec:capture**
|
||||
- **openrec:movie**
|
||||
- **orf:fm4:story**: fm4.orf.at stories
|
||||
- **orf:iptv**: iptv.ORF.at
|
||||
- **orf:on**
|
||||
@@ -1014,7 +1020,7 @@ The only reliable way to check if a site is supported is to try it.
|
||||
- **player.sky.it**
|
||||
- **PlayerFm**
|
||||
- **PlaySuisse**: [*playsuisse*](## "netrc machine")
|
||||
- **Playtvak**: Playtvak.cz, iDNES.cz and Lidovky.cz
|
||||
- **Playtvak**: Playtvak.cz, iDNES.cz and Lidovky.cz (**Currently broken**)
|
||||
- **PlayVids**
|
||||
- **pluralsight**: [*pluralsight*](## "netrc machine")
|
||||
- **pluralsight:course**
|
||||
@@ -1121,7 +1127,7 @@ The only reliable way to check if a site is supported is to try it.
|
||||
- **RedBullEmbed**
|
||||
- **RedBullTV**
|
||||
- **RedBullTVRrnContent**
|
||||
- **Reddit**: [*reddit*](## "netrc machine")
|
||||
- **Reddit**
|
||||
- **RedGifs**
|
||||
- **RedGifsSearch**: Redgifs search
|
||||
- **RedGifsUser**: Redgifs user
|
||||
@@ -1153,7 +1159,7 @@ The only reliable way to check if a site is supported is to try it.
|
||||
- **rtl.lu:article**
|
||||
- **rtl.lu:tele-vod**
|
||||
- **rtl.nl**: rtl.nl and rtlxl.nl
|
||||
- **rtl2**
|
||||
- **rtl2**: (**Currently broken**)
|
||||
- **RTLLuLive**
|
||||
- **RTLLuRadio**
|
||||
- **RTNews**
|
||||
@@ -1231,7 +1237,7 @@ The only reliable way to check if a site is supported is to try it.
|
||||
- **SharePoint**
|
||||
- **ShemarooMe**
|
||||
- **Shiey**
|
||||
- **ShowRoomLive**
|
||||
- **ShowRoomLive**: (**Currently broken**)
|
||||
- **ShugiinItvLive**: 衆議院インターネット審議中継
|
||||
- **ShugiinItvLiveRoom**: 衆議院インターネット審議中継 (中継)
|
||||
- **ShugiinItvVod**: 衆議院インターネット審議中継 (ビデオライブラリ)
|
||||
@@ -1369,7 +1375,7 @@ The only reliable way to check if a site is supported is to try it.
|
||||
- **TeleQuebecSquat**
|
||||
- **TeleQuebecVideo**
|
||||
- **TeleTask**: (**Currently broken**)
|
||||
- **Telewebion**: (**Currently broken**)
|
||||
- **Telewebion**
|
||||
- **TennisTV**: [*tennistv*](## "netrc machine")
|
||||
- **TF1**
|
||||
- **TFO**: (**Currently broken**)
|
||||
@@ -1414,10 +1420,6 @@ The only reliable way to check if a site is supported is to try it.
|
||||
- **Toypics**: Toypics video (**Currently broken**)
|
||||
- **ToypicsUser**: Toypics user profile (**Currently broken**)
|
||||
- **TravelChannel**
|
||||
- **Trovo**
|
||||
- **TrovoChannelClip**: All Clips of a trovo.live channel; "trovoclip:" prefix
|
||||
- **TrovoChannelVod**: All VODs of a trovo.live channel; "trovovod:" prefix
|
||||
- **TrovoVod**
|
||||
- **TrtCocukVideo**
|
||||
- **TrtWorld**
|
||||
- **TrueID**
|
||||
@@ -1591,7 +1593,7 @@ The only reliable way to check if a site is supported is to try it.
|
||||
- **VTXTV**: [*vtxtv*](## "netrc machine")
|
||||
- **VTXTVLive**: [*vtxtv*](## "netrc machine")
|
||||
- **VTXTVRecordings**: [*vtxtv*](## "netrc machine")
|
||||
- **Walla**
|
||||
- **Walla**: (**Currently broken**)
|
||||
- **WalyTV**: [*walytv*](## "netrc machine")
|
||||
- **WalyTVLive**: [*walytv*](## "netrc machine")
|
||||
- **WalyTVRecordings**: [*walytv*](## "netrc machine")
|
||||
@@ -1621,7 +1623,7 @@ The only reliable way to check if a site is supported is to try it.
|
||||
- **WeverseMediaTab**: [*weverse*](## "netrc machine")
|
||||
- **WeverseMoment**: [*weverse*](## "netrc machine")
|
||||
- **WeVidi**
|
||||
- **whowatch**
|
||||
- **whowatch**: (**Currently broken**)
|
||||
- **Whyp**
|
||||
- **wikimedia.org**
|
||||
- **Wimbledon**
|
||||
@@ -1636,8 +1638,8 @@ The only reliable way to check if a site is supported is to try it.
|
||||
- **WorldStarHipHop**
|
||||
- **wppilot**
|
||||
- **wppilot:channels**
|
||||
- **WrestleUniversePPV**: [*wrestleuniverse*](## "netrc machine")
|
||||
- **WrestleUniverseVOD**: [*wrestleuniverse*](## "netrc machine")
|
||||
- **wrestleuniverse:ppv**: [*wrestleuniverse*](## "netrc machine")
|
||||
- **wrestleuniverse:vod**: [*wrestleuniverse*](## "netrc machine")
|
||||
- **WSJ**: Wall Street Journal
|
||||
- **WSJArticle**
|
||||
- **WWE**
|
||||
@@ -1708,6 +1710,7 @@ The only reliable way to check if a site is supported is to try it.
|
||||
- **YoutubeYtBe**: [*youtube*](## "netrc machine") youtu.be
|
||||
- **Zaiko**
|
||||
- **ZaikoETicket**
|
||||
- **zan**: Z-aN
|
||||
- **Zapiks**
|
||||
- **Zattoo**: [*zattoo*](## "netrc machine")
|
||||
- **ZattooLive**: [*zattoo*](## "netrc machine")
|
||||
@@ -1730,5 +1733,6 @@ The only reliable way to check if a site is supported is to try it.
|
||||
- **zingmp3:user**
|
||||
- **zingmp3:week-chart**
|
||||
- **zoom**
|
||||
- **zoom:clips**
|
||||
- **Zype**
|
||||
- **generic**: Generic downloader that works on some sites
|
||||
|
||||
+25
-3
@@ -134,7 +134,10 @@ from yt_dlp.utils import (
|
||||
xpath_text,
|
||||
xpath_with_ns,
|
||||
)
|
||||
from yt_dlp.utils._utils import _UnsafeExtensionError
|
||||
from yt_dlp.utils._utils import (
|
||||
_UnsafeExtensionError,
|
||||
_desktop_entry_localestring,
|
||||
)
|
||||
from yt_dlp.utils.networking import (
|
||||
HTTPHeaderDict,
|
||||
escape_rfc3986,
|
||||
@@ -689,8 +692,6 @@ class TestUtil(unittest.TestCase):
|
||||
self.assertEqual(url_or_none('//foo.de'), '//foo.de')
|
||||
self.assertEqual(url_or_none('s3://foo.de'), None)
|
||||
self.assertEqual(url_or_none('rtmpte://foo.de'), 'rtmpte://foo.de')
|
||||
self.assertEqual(url_or_none('mms://foo.de'), 'mms://foo.de')
|
||||
self.assertEqual(url_or_none('rtspu://foo.de'), 'rtspu://foo.de')
|
||||
self.assertEqual(url_or_none('ftps://foo.de'), 'ftps://foo.de')
|
||||
self.assertEqual(url_or_none('ws://foo.de'), 'ws://foo.de')
|
||||
self.assertEqual(url_or_none('wss://foo.de'), 'wss://foo.de')
|
||||
@@ -1950,6 +1951,27 @@ Line 1
|
||||
self.assertEqual(
|
||||
iri_to_uri('http://导航.中国/'),
|
||||
'http://xn--fet810g.xn--fiqs8s/')
|
||||
self.assertEqual(
|
||||
iri_to_uri('file://example.org/run.exe', allowed_schemes=('file',)),
|
||||
'file://example.org/run.exe')
|
||||
self.assertRaises(ValueError, iri_to_uri, 'file://example.org/run.exe')
|
||||
|
||||
def test_desktop_entry_localestring(self):
|
||||
self.assertEqual(
|
||||
_desktop_entry_localestring('A B'),
|
||||
'A\\sB')
|
||||
self.assertEqual(
|
||||
_desktop_entry_localestring('A\nB'),
|
||||
'A\\nB')
|
||||
self.assertEqual(
|
||||
_desktop_entry_localestring('A\tB'),
|
||||
'A\\tB')
|
||||
self.assertEqual(
|
||||
_desktop_entry_localestring('A\rB'),
|
||||
'A\\rB')
|
||||
self.assertEqual(
|
||||
_desktop_entry_localestring('A\\B'),
|
||||
'A\\\\B')
|
||||
|
||||
def test_clean_podcast_url(self):
|
||||
self.assertEqual(clean_podcast_url('https://www.podtrac.com/pts/redirect.mp3/chtbl.com/track/5899E/traffic.megaphone.fm/HSW7835899191.mp3'), 'https://traffic.megaphone.fm/HSW7835899191.mp3')
|
||||
|
||||
+14
-5
@@ -170,7 +170,12 @@ from .utils import (
|
||||
write_json_file,
|
||||
write_string,
|
||||
)
|
||||
from .utils._utils import _UnsafeExtensionError, _YDLLogger, _ProgressState
|
||||
from .utils._utils import (
|
||||
_UnsafeExtensionError,
|
||||
_YDLLogger,
|
||||
_ProgressState,
|
||||
_desktop_entry_localestring,
|
||||
)
|
||||
from .utils.networking import (
|
||||
HTTPHeaderDict,
|
||||
clean_headers,
|
||||
@@ -480,7 +485,7 @@ class YoutubeDL:
|
||||
geo_bypass_country
|
||||
external_downloader: A dictionary of protocol keys and the executable of the
|
||||
external downloader to use for it. The allowed protocols
|
||||
are default|http|ftp|m3u8|dash|rtsp|rtmp|mms.
|
||||
are default|http|ftp|m3u8|dash|rtmp.
|
||||
Set the value to 'native' to use the native downloader
|
||||
compat_opts: Compatibility options. See "Differences in default behavior".
|
||||
The following options do not work when used through the API:
|
||||
@@ -1140,7 +1145,7 @@ class YoutubeDL:
|
||||
self.params['logger'].warning(message)
|
||||
elif self.params.get('no_warnings'):
|
||||
if self.params.get('verbose'):
|
||||
self.to_stderr(f'[debug:warning] {message}', only_once=only_once)
|
||||
self.to_stderr(f'[debug] WARNING: {message}', only_once=only_once)
|
||||
else:
|
||||
self.to_stderr(f'{self._format_err("WARNING:", self.Styles.WARNING)} {message}', only_once)
|
||||
|
||||
@@ -3406,10 +3411,13 @@ class YoutubeDL:
|
||||
|
||||
# Write internet shortcut files
|
||||
def _write_link_file(link_type):
|
||||
# iri_to_uri converts to ascii, percent-escapes unsafe characters & validates scheme
|
||||
# See https://github.com/yt-dlp/yt-dlp/security/advisories/GHSA-6v4j-43gg-vj32
|
||||
url = try_get(info_dict['webpage_url'], iri_to_uri)
|
||||
if not url:
|
||||
self.report_warning(
|
||||
f'Cannot write internet shortcut file because the actual URL of "{info_dict["webpage_url"]}" is unknown')
|
||||
f'Cannot write internet shortcut file because the actual URL '
|
||||
f'of "{info_dict["webpage_url"]}" is unknown or disallowed')
|
||||
return True
|
||||
linkfn = replace_extension(
|
||||
self.prepare_filename(info_dict, 'link'), link_type,
|
||||
@@ -3425,7 +3433,8 @@ class YoutubeDL:
|
||||
newline='\r\n' if link_type == 'url' else '\n') as linkfile:
|
||||
template_vars = {'url': url}
|
||||
if link_type == 'desktop':
|
||||
template_vars['filename'] = linkfn[:-(len(link_type) + 1)]
|
||||
# See https://github.com/yt-dlp/yt-dlp/security/advisories/GHSA-6v4j-43gg-vj32
|
||||
template_vars['filename'] = _desktop_entry_localestring(linkfn[:-(len(link_type) + 1)])
|
||||
linkfile.write(LINK_TEMPLATES[link_type] % template_vars)
|
||||
except OSError:
|
||||
self.report_error(f'Cannot write internet shortcut {linkfn}')
|
||||
|
||||
+1
-1
@@ -296,7 +296,7 @@ def validate_options(opts):
|
||||
default_step = start if op or limit else 0
|
||||
return lambda n: min(float(start) + float(step or default_step) * n, float(limit or 'inf'))
|
||||
|
||||
for key, expr in opts.retry_sleep.items():
|
||||
for key, expr in list(opts.retry_sleep.items()):
|
||||
if not expr:
|
||||
del opts.retry_sleep[key]
|
||||
continue
|
||||
|
||||
@@ -32,7 +32,6 @@ from .ism import IsmFD
|
||||
from .mhtml import MhtmlFD
|
||||
from .niconico import NiconicoLiveFD
|
||||
from .rtmp import RtmpFD
|
||||
from .rtsp import RtspFD
|
||||
from .websocket import WebSocketFragmentFD
|
||||
from .youtube_live_chat import YoutubeLiveChatFD
|
||||
from .bunnycdn import BunnyCdnFD
|
||||
@@ -44,8 +43,6 @@ PROTOCOL_MAP = {
|
||||
'rtmp_ffmpeg': FFmpegFD,
|
||||
'm3u8_native': HlsFD,
|
||||
'm3u8': FFmpegFD,
|
||||
'mms': RtspFD,
|
||||
'rtsp': RtspFD,
|
||||
'f4m': F4mFD,
|
||||
'http_dash_segments': DashSegmentsFD,
|
||||
'http_dash_segments_generator': DashSegmentsFD,
|
||||
|
||||
@@ -375,7 +375,7 @@ class HttpieFD(ExternalFD):
|
||||
|
||||
|
||||
class FFmpegFD(ExternalFD):
|
||||
SUPPORTED_PROTOCOLS = ('http', 'https', 'ftp', 'ftps', 'm3u8', 'm3u8_native', 'rtsp', 'rtmp', 'rtmp_ffmpeg', 'mms', 'http_dash_segments')
|
||||
SUPPORTED_PROTOCOLS = ('http', 'https', 'ftp', 'ftps', 'm3u8', 'm3u8_native', 'rtmp', 'rtmp_ffmpeg', 'http_dash_segments')
|
||||
SUPPORTED_FEATURES = (Features.TO_STDOUT, Features.MULTIPLE_FORMATS)
|
||||
|
||||
@classmethod
|
||||
|
||||
@@ -1,42 +0,0 @@
|
||||
import os
|
||||
import subprocess
|
||||
|
||||
from .common import FileDownloader
|
||||
from ..utils import check_executable
|
||||
|
||||
|
||||
class RtspFD(FileDownloader):
|
||||
def real_download(self, filename, info_dict):
|
||||
url = info_dict['url']
|
||||
self.report_destination(filename)
|
||||
tmpfilename = self.temp_name(filename)
|
||||
|
||||
if check_executable('mplayer', ['-h']):
|
||||
args = [
|
||||
'mplayer', '-really-quiet', '-vo', 'null', '-vc', 'dummy',
|
||||
'-dumpstream', '-dumpfile', tmpfilename, url]
|
||||
elif check_executable('mpv', ['-h']):
|
||||
args = [
|
||||
'mpv', '-really-quiet', '--vo=null', '--stream-dump=' + tmpfilename, url]
|
||||
else:
|
||||
self.report_error('MMS or RTSP download detected but neither "mplayer" nor "mpv" could be run. Please install one')
|
||||
return False
|
||||
|
||||
self._debug_cmd(args)
|
||||
|
||||
retval = subprocess.call(args)
|
||||
if retval == 0:
|
||||
fsize = os.path.getsize(tmpfilename)
|
||||
self.to_screen(f'\r[{args[0]}] {fsize} bytes')
|
||||
self.try_rename(tmpfilename, filename)
|
||||
self._hook_progress({
|
||||
'downloaded_bytes': fsize,
|
||||
'total_bytes': fsize,
|
||||
'filename': filename,
|
||||
'status': 'finished',
|
||||
}, info_dict)
|
||||
return True
|
||||
else:
|
||||
self.to_stderr('\n')
|
||||
self.report_error('%s exited with code %d' % (args[0], retval))
|
||||
return False
|
||||
@@ -359,7 +359,6 @@ from .commonmistakes import (
|
||||
UnicodeBOMIE,
|
||||
)
|
||||
from .commonprotocols import (
|
||||
MmsIE,
|
||||
RtmpIE,
|
||||
ViewSourceIE,
|
||||
)
|
||||
@@ -1314,6 +1313,11 @@ from .oftv import (
|
||||
)
|
||||
from .oktoberfesttv import OktoberfestTVIE
|
||||
from .olympics import OlympicsReplayIE
|
||||
from .omnyfm import (
|
||||
OmnyfmIE,
|
||||
OmnyfmPlaylistIE,
|
||||
OmnyfmShowIE,
|
||||
)
|
||||
from .on24 import On24IE
|
||||
from .ondemandkorea import (
|
||||
OnDemandKoreaIE,
|
||||
@@ -1335,8 +1339,11 @@ from .opencast import (
|
||||
)
|
||||
from .openrec import (
|
||||
OpenRecCaptureIE,
|
||||
OpenRecChannelIE,
|
||||
OpenRecChannelSearchIE,
|
||||
OpenRecIE,
|
||||
OpenRecMovieIE,
|
||||
OpenRecPlaylistIE,
|
||||
)
|
||||
from .orf import (
|
||||
ORFIPTVIE,
|
||||
|
||||
@@ -4,6 +4,7 @@ from .common import InfoExtractor
|
||||
|
||||
|
||||
class AdobeConnectIE(InfoExtractor):
|
||||
_WORKING = False
|
||||
_VALID_URL = r'https?://\w+\.adobeconnect\.com/(?P<id>[\w-]+)'
|
||||
|
||||
def _real_extract(self, url):
|
||||
|
||||
@@ -127,7 +127,7 @@ class BilibiliBaseIE(InfoExtractor):
|
||||
'format_note': ('quality', {format_names.get}),
|
||||
'duration': ('timelength', {float_or_none(scale=1000)}),
|
||||
}),
|
||||
**parse_resolution(format_names.get(play_info.get('quality'))),
|
||||
**parse_resolution(traverse_obj(play_info, ('quality', {format_names.get}))),
|
||||
})
|
||||
return formats
|
||||
|
||||
@@ -166,8 +166,58 @@ class BilibiliBaseIE(InfoExtractor):
|
||||
params['w_rid'] = hashlib.md5(f'{query}{self._get_wbi_key(video_id)}'.encode()).hexdigest()
|
||||
return params
|
||||
|
||||
def _download_playinfo(self, bvid, cid, headers=None, query=None):
|
||||
params = {'bvid': bvid, 'cid': cid, 'fnval': 4048, **(query or {})}
|
||||
@staticmethod
|
||||
@functools.cache
|
||||
def __screen_dimensions():
|
||||
dims, prefs = zip(
|
||||
((1920, 1080), 18),
|
||||
((1366, 768), 18),
|
||||
((1536, 864), 17),
|
||||
((1280, 720), 8),
|
||||
((2560, 1440), 7),
|
||||
((1440, 900), 5),
|
||||
((1600, 900), 5),
|
||||
strict=True)
|
||||
return random.choices(dims, weights=prefs)[0]
|
||||
|
||||
@property
|
||||
def _dm_params(self):
|
||||
def get_wh(width=1920, height=1080):
|
||||
res0, res1 = width, height
|
||||
rnd = math.floor(114 * random.random())
|
||||
return [2 * res0 + 2 * res1 + 3 * rnd, 4 * res0 - res1 + rnd, rnd]
|
||||
|
||||
def get_of(scroll_top=10, scroll_left=10):
|
||||
res0, res1 = scroll_top, scroll_left
|
||||
rnd = math.floor(514 * random.random())
|
||||
return [3 * res0 + 2 * res1 + rnd, 4 * res0 - 4 * res1 + 2 * rnd, rnd]
|
||||
|
||||
# Source: https://s1.hdslb.com/bfs/seed/jinkela/short/user-fingerprint/bili-user-fingerprint.min.js
|
||||
# function window.__biliUserFp__.queryUserLog
|
||||
# .dm_img_list and .dm_img_inter.ds are more troublesome as they come from mousemove/click events.
|
||||
# Leave them empty for now, since they should allow playing the video without any mousemove/click.
|
||||
return {
|
||||
'dm_img_list': '[]',
|
||||
'dm_img_str': base64.b64encode(
|
||||
''.join(random.choices(string.printable, k=random.randint(16, 64))).encode())[:-2].decode(),
|
||||
'dm_cover_img_str': base64.b64encode(
|
||||
''.join(random.choices(string.printable, k=random.randint(32, 128))).encode())[:-2].decode(),
|
||||
# Bilibili expects dm_img_inter to be a compact JSON (without spaces)
|
||||
'dm_img_inter': json.dumps({
|
||||
'ds': [],
|
||||
'wh': get_wh(*self.__screen_dimensions()),
|
||||
'of': get_of(random.randint(0, 100), 0),
|
||||
}, separators=(',', ':')),
|
||||
}
|
||||
|
||||
def _download_playinfo(self, bvid, cid, headers=None, query=None, fatal=True):
|
||||
params = {
|
||||
'bvid': bvid,
|
||||
'cid': cid,
|
||||
'fnval': 4048,
|
||||
**self._dm_params,
|
||||
**(query or {}),
|
||||
}
|
||||
if self.is_logged_in:
|
||||
params.pop('try_look', None)
|
||||
if qn := params.get('qn'):
|
||||
@@ -175,9 +225,24 @@ class BilibiliBaseIE(InfoExtractor):
|
||||
else:
|
||||
note = f'Downloading video formats for cid {cid}'
|
||||
|
||||
return self._download_json(
|
||||
playurl_raw = self._download_json(
|
||||
'https://api.bilibili.com/x/player/wbi/playurl', bvid,
|
||||
query=self._sign_wbi(params, bvid), headers=headers, note=note)['data']
|
||||
query=self._sign_wbi(params, bvid), headers=headers, note=note)
|
||||
code = traverse_obj(playurl_raw, ('code', {lambda x: x * -1}))
|
||||
if code == 0:
|
||||
return playurl_raw['data']
|
||||
else:
|
||||
msg = join_nonempty(
|
||||
'Unable to download video info', code,
|
||||
traverse_obj(playurl_raw, ('message', {str})),
|
||||
delim=': ')
|
||||
expected = code in (401, 352)
|
||||
if expected:
|
||||
msg += ', please wait and try later'
|
||||
if fatal:
|
||||
raise ExtractorError(msg, expected=expected)
|
||||
else:
|
||||
self.report_warning(msg)
|
||||
|
||||
def json2srt(self, json_data):
|
||||
srt_data = ''
|
||||
@@ -298,7 +363,7 @@ class BilibiliBaseIE(InfoExtractor):
|
||||
'title': f'{metainfo.get("title")} - {next(iter(edges.values())).get("title")}',
|
||||
'formats': self.extract_formats(play_info),
|
||||
'description': f'{json.dumps(edges, ensure_ascii=False)}\n{metainfo.get("description", "")}',
|
||||
'duration': float_or_none(play_info.get('timelength'), scale=1000),
|
||||
'duration': traverse_obj(play_info, ('timelength', {float_or_none(scale=1000)})),
|
||||
'subtitles': self.extract_subtitles(video_id, cid),
|
||||
}
|
||||
|
||||
@@ -662,7 +727,10 @@ class BiliBiliIE(BilibiliBaseIE):
|
||||
if not self._match_valid_url(urlh.url):
|
||||
return self.url_result(urlh.url)
|
||||
|
||||
headers['Referer'] = url
|
||||
headers.update({
|
||||
'Referer': 'https://www.bilibili.com/',
|
||||
'Origin': 'https://www.bilibili.com',
|
||||
})
|
||||
|
||||
initial_state = self._search_json(r'window\.__INITIAL_STATE__\s*=', webpage, 'initial state', video_id, default=None)
|
||||
if not initial_state:
|
||||
@@ -758,13 +826,12 @@ class BiliBiliIE(BilibiliBaseIE):
|
||||
duration=traverse_obj(initial_state, ('videoData', 'duration', {int_or_none})),
|
||||
__post_extractor=self.extract_comments(aid))
|
||||
|
||||
play_info = None
|
||||
if self.is_logged_in:
|
||||
play_info = traverse_obj(
|
||||
self._search_json(r'window\.__playinfo__\s*=', webpage, 'play info', video_id, default=None),
|
||||
('data', {dict}))
|
||||
if not play_info:
|
||||
play_info = self._download_playinfo(video_id, cid, headers=headers, query={'try_look': 1})
|
||||
play_info = traverse_obj(
|
||||
self._search_json(r'window\.__playinfo__\s*=', webpage, 'play info', video_id, default=None),
|
||||
('data', {dict}))
|
||||
if not self.is_logged_in or not play_info:
|
||||
if dl_play_info := self._download_playinfo(video_id, cid, headers=headers, query={'try_look': 1}, fatal=False):
|
||||
play_info = dl_play_info
|
||||
formats = self.extract_formats(play_info)
|
||||
|
||||
if video_data.get('is_upower_exclusive'):
|
||||
@@ -819,13 +886,13 @@ class BiliBiliIE(BilibiliBaseIE):
|
||||
'subtitles': self.extract_subtitles(video_id, cid) if idx == 0 else None,
|
||||
'__post_extractor': self.extract_comments(aid) if idx == 0 else None,
|
||||
} for idx, fragment in enumerate(formats[0]['fragments'])],
|
||||
'duration': float_or_none(play_info.get('timelength'), scale=1000),
|
||||
'duration': traverse_obj(play_info, ('timelength', {float_or_none(scale=1000)})),
|
||||
}
|
||||
|
||||
return {
|
||||
**metainfo,
|
||||
'formats': formats,
|
||||
'duration': float_or_none(play_info.get('timelength'), scale=1000),
|
||||
'duration': traverse_obj(play_info, ('timelength', {float_or_none(scale=1000)})),
|
||||
'chapters': self._get_chapters(aid, cid),
|
||||
'subtitles': self.extract_subtitles(video_id, cid),
|
||||
'__post_extractor': self.extract_comments(aid),
|
||||
@@ -1319,20 +1386,21 @@ class BilibiliSpaceVideoIE(BilibiliSpaceBaseIE):
|
||||
'pn': page_idx + 1,
|
||||
'ps': 30,
|
||||
'tid': 0,
|
||||
'web_location': 1550101,
|
||||
'dm_img_list': '[]',
|
||||
'dm_img_str': base64.b64encode(
|
||||
''.join(random.choices(string.printable, k=random.randint(16, 64))).encode())[:-2].decode(),
|
||||
'dm_cover_img_str': base64.b64encode(
|
||||
''.join(random.choices(string.printable, k=random.randint(32, 128))).encode())[:-2].decode(),
|
||||
'dm_img_inter': '{"ds":[],"wh":[6093,6631,31],"of":[430,760,380]}',
|
||||
'web_location': '333.1387',
|
||||
'special_type': '',
|
||||
'index': 0,
|
||||
**self._dm_params,
|
||||
}
|
||||
|
||||
try:
|
||||
response = self._download_json(
|
||||
'https://api.bilibili.com/x/space/wbi/arc/search', playlist_id,
|
||||
query=self._sign_wbi(query, playlist_id),
|
||||
note=f'Downloading space page {page_idx}', headers={'Referer': url})
|
||||
note=f'Downloading space page {page_idx}', headers={
|
||||
'Referer': url,
|
||||
'Origin': 'https://space.bilibili.com',
|
||||
'Accept-Language': 'en,zh-CN;q=0.9,zh;q=0.8',
|
||||
})
|
||||
except ExtractorError as e:
|
||||
if isinstance(e.cause, HTTPError) and e.cause.status == 412:
|
||||
raise ExtractorError(
|
||||
@@ -2030,12 +2098,9 @@ class BiliBiliDynamicIE(InfoExtractor):
|
||||
|
||||
def _real_extract(self, url):
|
||||
post_id = self._match_id(url)
|
||||
# Without the newer chrome UA, the API will return an error (-352)
|
||||
post_data = self._download_json(
|
||||
'https://api.bilibili.com/x/polymer/web-dynamic/v1/detail', post_id,
|
||||
query={'id': post_id}, headers={
|
||||
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36',
|
||||
})
|
||||
query={'id': post_id})
|
||||
video_url = traverse_obj(post_data, (
|
||||
'data', 'item', (None, 'orig'), 'modules', 'module_dynamic',
|
||||
(('major', ('archive', 'pgc')), ('additional', ('reserve', 'common'))),
|
||||
|
||||
@@ -3542,7 +3542,7 @@ class InfoExtractor:
|
||||
query = urllib.parse.urlparse(url).query
|
||||
url = re.sub(r'/(?:manifest|playlist|jwplayer)\.(?:m3u8|f4m|mpd|smil)', '', url)
|
||||
mobj = re.search(
|
||||
r'(?:(?:http|rtmp|rtsp)(?P<s>s)?:)?(?P<url>//[^?]+)', url)
|
||||
r'(?:(?:http|rtmp)(?P<s>s)?:)?(?P<url>//[^?]+)', url)
|
||||
url_base = mobj.group('url')
|
||||
http_base_url = '{}{}:{}'.format('http', mobj.group('s') or '', url_base)
|
||||
formats = []
|
||||
@@ -3567,28 +3567,16 @@ class InfoExtractor:
|
||||
video_id, mpd_id='dash', fatal=False))
|
||||
if re.search(r'(?:/smil:|\.smil)', url_base):
|
||||
if 'smil' not in skip_protocols:
|
||||
rtmp_formats = self._extract_smil_formats(
|
||||
formats.extend(self._extract_smil_formats(
|
||||
manifest_url('jwplayer.smil'),
|
||||
video_id, fatal=False)
|
||||
for rtmp_format in rtmp_formats:
|
||||
rtsp_format = rtmp_format.copy()
|
||||
rtsp_format['url'] = '{}/{}'.format(rtmp_format['url'], rtmp_format['play_path'])
|
||||
del rtsp_format['play_path']
|
||||
del rtsp_format['ext']
|
||||
rtsp_format.update({
|
||||
'url': rtsp_format['url'].replace('rtmp://', 'rtsp://'),
|
||||
'format_id': rtmp_format['format_id'].replace('rtmp', 'rtsp'),
|
||||
'protocol': 'rtsp',
|
||||
})
|
||||
formats.extend([rtmp_format, rtsp_format])
|
||||
video_id, fatal=False))
|
||||
else:
|
||||
for protocol in ('rtmp', 'rtsp'):
|
||||
if protocol not in skip_protocols:
|
||||
formats.append({
|
||||
'url': f'{protocol}:{url_base}',
|
||||
'format_id': protocol,
|
||||
'protocol': protocol,
|
||||
})
|
||||
if 'rtmp' not in skip_protocols:
|
||||
formats.append({
|
||||
'url': f'rtmp:{url_base}',
|
||||
'format_id': 'rtmp',
|
||||
'protocol': 'rtmp',
|
||||
})
|
||||
return formats
|
||||
|
||||
def _find_jwplayer_data(self, webpage, video_id=None, transform_source=js_to_json):
|
||||
|
||||
@@ -29,34 +29,6 @@ class RtmpIE(InfoExtractor):
|
||||
}
|
||||
|
||||
|
||||
class MmsIE(InfoExtractor):
|
||||
IE_DESC = False # Do not list
|
||||
_VALID_URL = r'(?i)mms://.+'
|
||||
|
||||
_TEST = {
|
||||
# Direct MMS link
|
||||
'url': 'mms://kentro.kaist.ac.kr/200907/MilesReid(0709).wmv',
|
||||
'info_dict': {
|
||||
'id': 'MilesReid(0709)',
|
||||
'ext': 'wmv',
|
||||
'title': 'MilesReid(0709)',
|
||||
},
|
||||
'params': {
|
||||
'skip_download': True, # rtsp downloads, requiring mplayer or mpv
|
||||
},
|
||||
}
|
||||
|
||||
def _real_extract(self, url):
|
||||
video_id = self._generic_id(url)
|
||||
title = self._generic_title(url)
|
||||
|
||||
return {
|
||||
'id': video_id,
|
||||
'title': title,
|
||||
'url': url,
|
||||
}
|
||||
|
||||
|
||||
class ViewSourceIE(InfoExtractor):
|
||||
IE_DESC = False
|
||||
_VALID_URL = r'view-source:(?P<url>.+)'
|
||||
|
||||
@@ -11,6 +11,7 @@ from ..utils import (
|
||||
|
||||
|
||||
class DigitallySpeakingIE(InfoExtractor):
|
||||
_WORKING = False
|
||||
_VALID_URL = r'https?://(?:s?evt\.dispeak|events\.digitallyspeaking)\.com/(?:[^/]+/)+xml/(?P<id>[^.]+)\.xml'
|
||||
|
||||
_TESTS = [{
|
||||
|
||||
@@ -138,7 +138,7 @@ class FilmOnChannelIE(InfoExtractor):
|
||||
continue
|
||||
if not is_live:
|
||||
formats.extend(self._extract_wowza_formats(
|
||||
stream_url, channel_id, skip_protocols=['dash', 'rtmp', 'rtsp']))
|
||||
stream_url, channel_id, skip_protocols=['dash', 'rtmp']))
|
||||
continue
|
||||
quality = stream.get('quality')
|
||||
formats.append({
|
||||
|
||||
@@ -10,6 +10,7 @@ from ..utils import (
|
||||
|
||||
|
||||
class FolketingetIE(InfoExtractor):
|
||||
_WORKING = False
|
||||
IE_DESC = 'Folketinget (ft.dk; Danish parliament)'
|
||||
_VALID_URL = r'https?://(?:www\.)?ft\.dk/webtv/video/[^?#]*?\.(?P<id>[0-9]+)\.aspx'
|
||||
_TEST = {
|
||||
|
||||
@@ -3,9 +3,11 @@ import hashlib
|
||||
import itertools
|
||||
import json
|
||||
import re
|
||||
import urllib.parse
|
||||
|
||||
from .common import InfoExtractor
|
||||
from ..networking.exceptions import HTTPError
|
||||
from ..networking.impersonate import ImpersonateTarget
|
||||
from ..utils import (
|
||||
ExtractorError,
|
||||
bug_reports_message,
|
||||
@@ -42,15 +44,27 @@ def _id_to_pk(shortcode):
|
||||
class InstagramBaseIE(InfoExtractor):
|
||||
_API_BASE_URL = 'https://i.instagram.com/api/v1'
|
||||
_BASE_URL = 'https://www.instagram.com/'
|
||||
_LOGIN_URL = 'https://www.instagram.com/accounts/login'
|
||||
_APP_IDS = {
|
||||
'ios': '124024574287414',
|
||||
'web': '936619743392459', # default
|
||||
}
|
||||
_AUTH_COOKIE_NAME = 'sessionid'
|
||||
_COOKIE_DOMAINS = (
|
||||
'i.instagram.com',
|
||||
'.i.instagram.com',
|
||||
'www.instagram.com',
|
||||
'.www.instagram.com',
|
||||
'instagram.com',
|
||||
'.instagram.com',
|
||||
)
|
||||
|
||||
@functools.cached_property
|
||||
def _can_impersonate(self):
|
||||
return self._downloader._impersonate_target_available(ImpersonateTarget())
|
||||
|
||||
@property
|
||||
def _is_logged_in(self):
|
||||
return bool(self._get_cookies(self._BASE_URL).get('sessionid'))
|
||||
return bool(self._get_cookies(self._BASE_URL).get(self._AUTH_COOKIE_NAME))
|
||||
|
||||
@functools.cached_property
|
||||
def _app_id(self):
|
||||
@@ -71,6 +85,10 @@ class InstagramBaseIE(InfoExtractor):
|
||||
'Accept': '*/*',
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def _is_login_redirect(url):
|
||||
return urllib.parse.urlparse(url).path.startswith('/accounts/login')
|
||||
|
||||
def _get_count(self, media, kind, *keys):
|
||||
return traverse_obj(
|
||||
media, (kind, 'count'), *((f'edge_media_{key}', 'count') for key in keys),
|
||||
@@ -397,9 +415,11 @@ class InstagramIE(InstagramBaseIE):
|
||||
|
||||
def _real_initialize(self):
|
||||
if self._is_logged_in:
|
||||
self.write_debug('Found Instagram account cookies')
|
||||
return
|
||||
if not self._lsd_token:
|
||||
webpage = self._download_webpage(self._BASE_URL, None, 'Setting up session', impersonate=True)
|
||||
webpage = self._download_webpage(
|
||||
self._BASE_URL, None, 'Setting up session', impersonate=self._can_impersonate)
|
||||
eqmc = self._search_json(
|
||||
r'<script\b[^>]*\bid="__eqmc"[^>]*>', webpage, 'eqmc JSON', None, default={})
|
||||
self._lsd_token = (
|
||||
@@ -411,15 +431,27 @@ class InstagramIE(InstagramBaseIE):
|
||||
media_id = str(_id_to_pk(video_id))
|
||||
|
||||
if self._is_logged_in:
|
||||
return self._extract_product(self._download_json(
|
||||
f'{self._API_BASE_URL}/media/{media_id}/info/', video_id,
|
||||
'Downloading video info', 'Video info extraction failed',
|
||||
impersonate=self._is_web_app, headers=self._api_headers)['items'][0])
|
||||
try:
|
||||
return self._extract_product(self._download_json(
|
||||
f'{self._API_BASE_URL}/media/{media_id}/info/', video_id,
|
||||
'Downloading video info', 'Video info extraction failed',
|
||||
impersonate=self._can_impersonate and self._is_web_app,
|
||||
headers=self._api_headers)['items'][0])
|
||||
except ExtractorError as e:
|
||||
if not (isinstance(e.cause, HTTPError) and self._is_login_redirect(e.cause.response.url)):
|
||||
raise
|
||||
|
||||
self.report_warning('The provided Instagram account cookies are no longer valid')
|
||||
# XXX: With curl-cffi, the error response may not invalidate the cookie in our jar
|
||||
for domain in self._COOKIE_DOMAINS:
|
||||
self.cookiejar.clear(domain=domain, path='/', name=self._AUTH_COOKIE_NAME)
|
||||
# Re-initialize to set lsd token for logged-out extraction
|
||||
self._real_initialize()
|
||||
|
||||
api_check = self._download_json(
|
||||
f'{self._API_BASE_URL}/web/get_ruling_for_content/', video_id,
|
||||
'Checking post accessibility', errnote=False, fatal=False,
|
||||
impersonate=True, headers=self._api_headers,
|
||||
impersonate=self._can_impersonate, headers=self._api_headers,
|
||||
query={'content_type': 'MEDIA', 'target_id': media_id}) or {}
|
||||
|
||||
csrf_token = self._get_cookies('https://www.instagram.com').get('csrftoken')
|
||||
@@ -447,7 +479,7 @@ class InstagramIE(InstagramBaseIE):
|
||||
'server_timestamps': 'true',
|
||||
'variables': json.dumps({'media_id': media_id}, separators=(',', ':')),
|
||||
'doc_id': '27130156389949648',
|
||||
}))
|
||||
})) if self._can_impersonate else None
|
||||
|
||||
media = traverse_obj(response, ('data', 'xig_polaris_media', {dict}))
|
||||
product_info = traverse_obj(media, ('if_not_gated_logged_out', {dict}))
|
||||
@@ -465,8 +497,8 @@ class InstagramIE(InstagramBaseIE):
|
||||
'This content is only available for registered users who follow this account')
|
||||
|
||||
webpage, urlh = self._download_webpage_handle(
|
||||
f'https://www.instagram.com/p/{video_id}', video_id)
|
||||
if urlh.url.startswith(self._LOGIN_URL):
|
||||
f'https://www.instagram.com/p/{video_id}', video_id, impersonate=self._can_impersonate)
|
||||
if self._is_login_redirect(urlh.url):
|
||||
self.raise_login_required(
|
||||
'The webpage request was redirected to the login page. '
|
||||
'You have exceeded the rate-limit for accessing posts anonymously')
|
||||
@@ -685,7 +717,8 @@ class InstagramStoryIE(InstagramBaseIE):
|
||||
if username == 'highlights' and not story_id: # story id is only mandatory for highlights
|
||||
raise ExtractorError('Input URL is missing a highlight ID', expected=True)
|
||||
display_id = story_id or username
|
||||
story_info = self._download_webpage(url, display_id, impersonate=self._is_web_app)
|
||||
story_info = self._download_webpage(
|
||||
url, display_id, impersonate=self._can_impersonate and self._is_web_app)
|
||||
user_info = self._search_json(r'"user":', story_info, 'user info', display_id, fatal=False)
|
||||
if not user_info:
|
||||
self.raise_login_required('This content is unreachable')
|
||||
@@ -700,8 +733,8 @@ class InstagramStoryIE(InstagramBaseIE):
|
||||
|
||||
videos = traverse_obj(self._download_json(
|
||||
f'{self._API_BASE_URL}/feed/reels_media/?reel_ids={story_info_url}',
|
||||
display_id, errnote=False, fatal=False, impersonate=self._is_web_app,
|
||||
headers=self._api_headers), 'reels')
|
||||
display_id, errnote=False, fatal=False, headers=self._api_headers,
|
||||
impersonate=self._can_impersonate and self._is_web_app), 'reels')
|
||||
if not videos:
|
||||
self.raise_login_required('You need to log in to access this content')
|
||||
user_info = traverse_obj(videos, (user_id, 'user', {dict})) or {}
|
||||
|
||||
@@ -2,6 +2,7 @@ from .common import InfoExtractor
|
||||
|
||||
|
||||
class KaraoketvIE(InfoExtractor):
|
||||
_WORKING = False
|
||||
_VALID_URL = r'https?://(?:www\.)?karaoketv\.co\.il/[^/]+/(?P<id>\d+)'
|
||||
_TEST = {
|
||||
'url': 'http://www.karaoketv.co.il/%D7%A9%D7%99%D7%A8%D7%99_%D7%A7%D7%A8%D7%99%D7%95%D7%A7%D7%99/58356/%D7%90%D7%99%D7%96%D7%95%D7%9F',
|
||||
|
||||
@@ -37,10 +37,7 @@ class MangomoloBaseIE(InfoExtractor):
|
||||
m3u8_entry_protocol = 'm3u8' if self._IS_LIVE else 'm3u8_native'
|
||||
|
||||
format_url = self._html_search_regex(
|
||||
[
|
||||
r'(?:file|src)\s*:\s*"(https?://[^"]+?/playlist\.m3u8)',
|
||||
r'<a[^>]+href="(rtsp://[^"]+)"',
|
||||
], webpage, 'format url')
|
||||
r'(?:file|src)\s*:\s*"(https?://[^"]+?/playlist\.m3u8)', webpage, 'format url')
|
||||
formats = self._extract_wowza_formats(
|
||||
format_url, page_id, m3u8_entry_protocol, ['smil'])
|
||||
|
||||
@@ -58,6 +55,18 @@ class MangomoloVideoIE(MangomoloBaseIE):
|
||||
_TYPE = 'video'
|
||||
IE_NAME = 'mangomolo:' + _TYPE
|
||||
_SLUG = r'video\?.*?\bid=(?P<id>\d+)'
|
||||
_TESTS = [{
|
||||
'url': 'https://player.mangomolo.com/v1/video?id=29431242&user_id=168&signature=b40e58d964532fe09bf81fc08d850752&autoplay=true&fullscreen=yes&base_url=aHR0cHM6Ly9heW4ub20vdmlkZW8vMjk0MzEyNDIvJUQ4JUE3JUQ5JTg0JUQ4JUEzJUQ5JTg1JUQ5JTg2JUQ5JThBJUQ4JUE3JUQ4JUFBLSVEOCVBNyVEOSU4NCVEOCVCMyVEOCVBOCVEOCVCOS0lRDglQTclRDklODQlRDglQUQlRDklODQlRDklODIlRDglQTktOQ%3D%3D&vast=true&app_id=&zone=&filter=DENY&countries=Q0M%3D&language=ar&player_profile=',
|
||||
'info_dict': {
|
||||
'id': '29431242',
|
||||
'ext': 'flv',
|
||||
'title': '29431242',
|
||||
},
|
||||
'expected_warnings': [
|
||||
'Failed to download m3u8 information',
|
||||
'Failed to download MPD manifest',
|
||||
],
|
||||
}]
|
||||
|
||||
_IS_LIVE = False
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ from .common import InfoExtractor
|
||||
|
||||
|
||||
class OktoberfestTVIE(InfoExtractor):
|
||||
_WORKING = False
|
||||
_VALID_URL = r'https?://(?:www\.)?oktoberfest-tv\.de/[^/]+/[^/]+/video/(?P<id>[^/?#]+)'
|
||||
|
||||
_TEST = {
|
||||
|
||||
@@ -0,0 +1,272 @@
|
||||
import functools
|
||||
import itertools
|
||||
|
||||
from .common import InfoExtractor
|
||||
from ..utils import (
|
||||
OnDemandPagedList,
|
||||
clean_html,
|
||||
filter_dict,
|
||||
float_or_none,
|
||||
int_or_none,
|
||||
parse_duration,
|
||||
parse_iso8601,
|
||||
parse_qs,
|
||||
update_url,
|
||||
url_or_none,
|
||||
)
|
||||
from ..utils.traversal import (
|
||||
require,
|
||||
traverse_obj,
|
||||
trim_str,
|
||||
)
|
||||
|
||||
|
||||
class OmnyfmIE(InfoExtractor):
|
||||
_VALID_URL = r'https?://omny\.fm/shows/(?P<uploader_id>[\w-]+)/(?P<id>(?!playlists(?:[/?#"\']|$))[\w-]+)(?:/embed)?(?=[?#"\']|$)'
|
||||
_EMBED_REGEX = [rf'<iframe[^>]+\bsrc\s*=\s*(["\'])(?P<url>{_VALID_URL}[^"\']*)\1']
|
||||
_TESTS = [{
|
||||
'url': 'https://omny.fm/shows/sleep-hub/cannabinoids-and-sleep',
|
||||
'md5': 'e45ec0ce43da757a0be6ca117ec01bdc',
|
||||
'info_dict': {
|
||||
'id': 'cannabinoids-and-sleep',
|
||||
'ext': 'mp3',
|
||||
'title': 'Cannabinoids and Sleep',
|
||||
'categories': 'count:1',
|
||||
'chapters': [
|
||||
{'start_time': 0, 'title': 'Introduction'},
|
||||
{'start_time': 138, 'title': 'Theme: Cannabinoids and Sleep'},
|
||||
{'start_time': 1487, 'title': 'Clinical Tip'},
|
||||
{'start_time': 1635, 'title': 'Pick of the Month'},
|
||||
{'start_time': 1795, 'title': 'What\'s Coming Up?'},
|
||||
],
|
||||
'description': 'md5:c0fd2d29f3148382d344cfbd012fb00d',
|
||||
'duration': 1840.274,
|
||||
'episode': 'Episode 48',
|
||||
'episode_number': 48,
|
||||
'modified_date': r're:\d{8}',
|
||||
'modified_timestamp': int,
|
||||
'tags': 'count:6',
|
||||
'thumbnail': r're:https?://www\.omnycontent\.com/.+',
|
||||
'timestamp': 1574013600,
|
||||
'upload_date': '20191117',
|
||||
'uploader': 'Sleep Talk',
|
||||
'uploader_id': 'sleep-hub',
|
||||
},
|
||||
}, {
|
||||
'url': 'https://omny.fm/shows/the-origin-of-things/a-song-of-hope/embed',
|
||||
'md5': 'd7600ef33e3f139ff1bb8946f3651b15',
|
||||
'info_dict': {
|
||||
'id': 'a-song-of-hope',
|
||||
'ext': 'mp3',
|
||||
'title': 'A song of hope',
|
||||
'categories': 'count:3',
|
||||
'description': 'md5:f8e710765c341a48cfda8dacd428f56d',
|
||||
'duration': 478.955,
|
||||
'episode': 'Episode 17',
|
||||
'episode_number': 17,
|
||||
'modified_date': r're:\d{8}',
|
||||
'modified_timestamp': int,
|
||||
'season': 'Season 3',
|
||||
'season_number': 3,
|
||||
'tags': 'count:27',
|
||||
'thumbnail': r're:https?://www\.omnycontent\.com/.+',
|
||||
'timestamp': 1679445000,
|
||||
'upload_date': '20230322',
|
||||
'uploader': 'The Origin Of Things',
|
||||
'uploader_id': 'the-origin-of-things',
|
||||
},
|
||||
}]
|
||||
_WEBPAGE_TESTS = [{
|
||||
'url': 'https://www.asahi.com/special/podcasts/item/?itemid=311a5f48-ad71-4548-b1f2-af5e00747fbc',
|
||||
'md5': '4c788bf03323734524a7c0f98d9956ed',
|
||||
'info_dict': {
|
||||
'id': 'sdgs-271',
|
||||
'ext': 'mp3',
|
||||
'title': '「どこかのだれかの人生のにおいがする」 SDGsを音声番組で身近に #271',
|
||||
'categories': 'count:5',
|
||||
'description': 'md5:fa086ecce764d81c51648a82d0fe4850',
|
||||
'duration': 1870.524,
|
||||
'episode': 'Episode 271',
|
||||
'episode_number': 271,
|
||||
'modified_date': r're:\d{8}',
|
||||
'modified_timestamp': int,
|
||||
'season': 'Season 1',
|
||||
'season_number': 1,
|
||||
'tags': 'count:4',
|
||||
'thumbnail': r're:https?://www\.omnycontent\.com/.+',
|
||||
'timestamp': 1670266800,
|
||||
'upload_date': '20221205',
|
||||
'uploader': '朝日新聞ポッドキャスト',
|
||||
'uploader_id': 'asahi',
|
||||
'webpage_url': 'https://omny.fm/shows/asahi/sdgs-271',
|
||||
},
|
||||
}]
|
||||
|
||||
def _real_extract(self, url):
|
||||
uploader_id, audio_id = self._match_valid_url(url).group('uploader_id', 'id')
|
||||
webpage = self._download_webpage(url, audio_id)
|
||||
nextjs_data = self._search_nextjs_data(webpage, audio_id)
|
||||
clip = traverse_obj(nextjs_data, ('props', 'pageProps', 'clip', {dict}))
|
||||
|
||||
return {
|
||||
'id': audio_id,
|
||||
'section_start': traverse_obj(url, ({parse_qs}, 't', -1, {parse_duration})),
|
||||
'uploader_id': uploader_id,
|
||||
'vcodec': 'none',
|
||||
**traverse_obj(clip, {
|
||||
'title': ('Title', {clean_html}, filter),
|
||||
'chapters': ('Chapters', lambda _, v: parse_duration(v['Position']) is not None, {
|
||||
'title': ('Name', {clean_html}, filter),
|
||||
'start_time': ('Position', {parse_duration}),
|
||||
}),
|
||||
'description': ('Description', {clean_html}, filter),
|
||||
'duration': ('DurationSeconds', {float_or_none}),
|
||||
'episode_number': ('Episode', {int_or_none}),
|
||||
'filesize': ('PublishedAudioSizeInBytes', {int_or_none}),
|
||||
'modified_timestamp': ('ModifiedAtUtc', {parse_iso8601}),
|
||||
'season_number': ('Season', {int_or_none}),
|
||||
'tags': ('Tags', ..., {clean_html}, filter, all, filter),
|
||||
'thumbnail': ('ImageUrl', {update_url(query=None)}),
|
||||
'timestamp': ('PublishedUtc', {parse_iso8601}),
|
||||
'url': ('AudioUrl', {url_or_none}, {require('audio URL')}),
|
||||
'webpage_url': ('PublishedUrl', {url_or_none}),
|
||||
}),
|
||||
**traverse_obj(clip, ('Program', {
|
||||
'categories': ('Categories', ..., {clean_html}, filter, all, filter),
|
||||
'uploader': ('Name', {clean_html}, filter),
|
||||
})),
|
||||
}
|
||||
|
||||
|
||||
class OmnyfmPlaylistBaseIE(InfoExtractor):
|
||||
_API_BASE = 'https://api.omny.fm'
|
||||
_BASE_URL = 'https://omny.fm/shows'
|
||||
_PAGE_SIZE = 100
|
||||
|
||||
def _yield_clips(self, clips, uploader_id):
|
||||
for audio_id in traverse_obj(clips, (
|
||||
'Clips', ..., 'Slug', {str},
|
||||
)):
|
||||
yield self.url_result(
|
||||
f'{self._BASE_URL}/{uploader_id}/{audio_id}', OmnyfmIE)
|
||||
|
||||
|
||||
class OmnyfmPlaylistIE(OmnyfmPlaylistBaseIE):
|
||||
_VALID_URL = r'https?://omny\.fm/shows/(?P<uploader_id>[\w-]+)/playlists(?:/(?P<id>[\w-]+))?(?:/embed)?/?(?=[?#"\']|$)'
|
||||
_EMBED_REGEX = [fr'<iframe[^>]+\bsrc=(["\'])(?P<url>{_VALID_URL}[^"\']*)\1']
|
||||
_TESTS = [{
|
||||
'url': 'https://omny.fm/shows/sleep-hub/playlists/sleep-talk',
|
||||
'info_dict': {
|
||||
'id': 'sleep-talk',
|
||||
'title': 'Sleep Talk - Talking all things sleep',
|
||||
'description': 'md5:c1d7e5bf32100a432307d2d32c4ab74a',
|
||||
'thumbnail': r're:https?://www\.omnycontent\.com/.+',
|
||||
},
|
||||
'playlist_mincount': 79,
|
||||
}, {
|
||||
'url': 'https://omny.fm/shows/bayfm-program03/playlists',
|
||||
'info_dict': {
|
||||
'id': 'bayfm-program03',
|
||||
},
|
||||
'playlist_count': 4,
|
||||
}]
|
||||
_WEBPAGE_TESTS = [{
|
||||
'url': 'https://www.asahi.com/articles/ASP763WDKP4JDIFI002.html',
|
||||
'info_dict': {
|
||||
'id': 'podcast',
|
||||
'title': 'ニュースの現場から',
|
||||
'description': 'md5:ed1f78462ebed09258ca31b1da5ff640',
|
||||
'thumbnail': r're:https?://www\.omnycontent\.com/.+',
|
||||
'webpage_url': 'https://omny.fm/shows/asahi/playlists/podcast',
|
||||
},
|
||||
'playlist_mincount': 2517,
|
||||
}]
|
||||
|
||||
def _entries(self, uploader_id, playlist_id):
|
||||
clip_id = None
|
||||
|
||||
for page in itertools.count(1):
|
||||
clips = self._download_json(
|
||||
f'{self._API_BASE}/programs/{uploader_id}/playlists/{playlist_id}/clips',
|
||||
playlist_id, f'Downloading page {page}', query=filter_dict({
|
||||
'clipId': clip_id,
|
||||
'direction': 'AfterExclusive',
|
||||
'pageSize': self._PAGE_SIZE,
|
||||
}))
|
||||
|
||||
yield from self._yield_clips(clips, uploader_id)
|
||||
|
||||
if not clips.get('NextClipsAvailable'):
|
||||
break
|
||||
|
||||
clip_id = traverse_obj(clips, ('Clips', -1, 'Id', {str}))
|
||||
if not clip_id:
|
||||
break
|
||||
|
||||
def _real_extract(self, url):
|
||||
uploader_id, playlist_id = self._match_valid_url(url).group('uploader_id', 'id')
|
||||
webpage = self._download_webpage(url, playlist_id or uploader_id)
|
||||
nextjs_data = self._search_nextjs_data(webpage, playlist_id or uploader_id)
|
||||
page_props = traverse_obj(nextjs_data, ('props', 'pageProps', {dict}))
|
||||
|
||||
if not playlist_id:
|
||||
entries = [self.url_result(
|
||||
f'{self._BASE_URL}/{uploader_id}/playlists/{playlist_id}', OmnyfmPlaylistIE,
|
||||
) for playlist_id in traverse_obj(page_props, (
|
||||
'playlistsWithClips', ..., 'playlist', 'Slug', {str},
|
||||
))]
|
||||
|
||||
return self.playlist_result(entries, uploader_id)
|
||||
|
||||
return self.playlist_result(
|
||||
self._entries(uploader_id, playlist_id), playlist_id,
|
||||
**traverse_obj(page_props, ('playlist', {
|
||||
'title': ('Title', {clean_html}, filter),
|
||||
'description': ('Description', {clean_html}, filter),
|
||||
'thumbnail': ('ArtworkUrl', {update_url(query=None)}),
|
||||
'webpage_url': ('EmbedUrl', {url_or_none}, {trim_str(end='/embed')}),
|
||||
})))
|
||||
|
||||
|
||||
class OmnyfmShowIE(OmnyfmPlaylistBaseIE):
|
||||
_VALID_URL = r'https?://omny\.fm/shows/(?P<id>[\w-]+)/?(?:[?#]|$)'
|
||||
_TESTS = [{
|
||||
'url': 'https://omny.fm/shows/the-origin-of-things',
|
||||
'info_dict': {
|
||||
'id': 'the-origin-of-things',
|
||||
'title': 'The Origin Of Things',
|
||||
'description': 'md5:52b7fba08201d050639c78ea88cc782e',
|
||||
'thumbnail': r're:https?://www\.omnycontent\.com/.+',
|
||||
},
|
||||
'playlist_mincount': 75,
|
||||
}]
|
||||
|
||||
def _fetch_page(self, uploader_id, organization_id, program_id, page):
|
||||
clips = self._download_json(
|
||||
f'{self._API_BASE}/orgs/{organization_id}/programs/{program_id}/clips',
|
||||
uploader_id, f'Downloading page {page + 1}', query={
|
||||
'cursor': page,
|
||||
'pageSize': self._PAGE_SIZE,
|
||||
})
|
||||
|
||||
yield from self._yield_clips(clips, uploader_id)
|
||||
|
||||
def _real_extract(self, url):
|
||||
uploader_id = self._match_id(url)
|
||||
webpage = self._download_webpage(url, uploader_id)
|
||||
nextjs_data = self._search_nextjs_data(webpage, uploader_id)
|
||||
program = traverse_obj(nextjs_data, ('props', 'pageProps', 'program', {dict}))
|
||||
|
||||
organization_id = traverse_obj(program, (
|
||||
'OrganizationId', {str}, {require('organization ID')}))
|
||||
program_id = traverse_obj(program, ('Id', {str}, {require('program ID')}))
|
||||
entries = OnDemandPagedList(
|
||||
functools.partial(self._fetch_page, uploader_id, organization_id, program_id), self._PAGE_SIZE)
|
||||
|
||||
return self.playlist_result(
|
||||
entries, uploader_id,
|
||||
**traverse_obj(program, {
|
||||
'title': ('Name', {clean_html}, filter),
|
||||
'description': ('Description', {clean_html}, filter),
|
||||
'thumbnail': ('ArtworkUrl', {update_url(query=None)}),
|
||||
}))
|
||||
+674
-99
@@ -1,153 +1,728 @@
|
||||
import collections
|
||||
import datetime as dt
|
||||
import functools
|
||||
import itertools
|
||||
import json
|
||||
import math
|
||||
import time
|
||||
import urllib.parse
|
||||
import xml.etree.ElementTree
|
||||
|
||||
from .common import InfoExtractor
|
||||
from ..utils import (
|
||||
ExtractorError,
|
||||
get_first,
|
||||
InAdvancePagedList,
|
||||
clean_html,
|
||||
extract_attributes,
|
||||
filter_dict,
|
||||
int_or_none,
|
||||
join_nonempty,
|
||||
parse_iso8601,
|
||||
parse_qs,
|
||||
str_or_none,
|
||||
update_url_query,
|
||||
url_or_none,
|
||||
urljoin,
|
||||
)
|
||||
from ..utils.traversal import (
|
||||
find_element,
|
||||
find_elements,
|
||||
require,
|
||||
traverse_obj,
|
||||
try_get,
|
||||
unified_strdate,
|
||||
unified_timestamp,
|
||||
)
|
||||
|
||||
|
||||
class OpenRecBaseIE(InfoExtractor):
|
||||
_M3U8_HEADERS = {'Referer': 'https://www.openrec.tv/'}
|
||||
_API_BASE = 'https://apiv5.mellow-fan.com/api/v5'
|
||||
_BASE_URL = 'https://www.mellow-fan.com'
|
||||
_HEADERS = {'Referer': f'{_BASE_URL}/'}
|
||||
_NETRC_MACHINE = 'mellowfan'
|
||||
_PUBLIC_API_BASE = 'https://public.mellow-fan.com/external/api/v5'
|
||||
|
||||
def _perform_login(self, username, password):
|
||||
if self._get_cookies(self._BASE_URL).get('access-token'):
|
||||
return
|
||||
|
||||
login = self._download_json(
|
||||
f'{self._BASE_URL}/apiv5/email/login',
|
||||
None, 'Logging in', headers={
|
||||
'Content-Type': 'application/json',
|
||||
}, data=json.dumps({
|
||||
'email': username,
|
||||
'password': password,
|
||||
}).encode())
|
||||
if traverse_obj(login, ('status', {int_or_none})) != 0:
|
||||
err_msg = traverse_obj(login, ('message', {clean_html}, filter))
|
||||
raise ExtractorError(err_msg or 'Failed to log in', expected=True)
|
||||
|
||||
def _real_initialize(self):
|
||||
cookies = self._get_cookies(self._BASE_URL)
|
||||
self._api_headers = traverse_obj(cookies, {
|
||||
'access-token': ('access_token', 'value', {str}, filter),
|
||||
'random': ('random', 'value', {str}, filter),
|
||||
'token': ('token', 'value', {str}, filter),
|
||||
'uuid': ('uuid', 'value', {str}, filter),
|
||||
})
|
||||
|
||||
def _extract_pagestore(self, webpage, video_id):
|
||||
return self._parse_json(
|
||||
self._search_regex(r'(?m)window\.pageStore\s*=\s*(\{.+?\});$', webpage, 'window.pageStore'), video_id)
|
||||
start = r'window\.pageStore\s*='
|
||||
|
||||
def _expand_media(self, video_id, media):
|
||||
for name, m3u8_url in (media or {}).items():
|
||||
if not m3u8_url:
|
||||
continue
|
||||
yield from self._extract_m3u8_formats(
|
||||
m3u8_url, video_id, ext='mp4', m3u8_id=name, headers=self._M3U8_HEADERS)
|
||||
if store := self._search_regex(
|
||||
rf'{start}\s*JSON\.parse\s*\(\s*decodeURIComponent'
|
||||
r'\s*\(\s*(?P<q>["\'])(?P<json>.*?)(?P=q)\s*\)\s*\)',
|
||||
webpage, 'encoded window pagestore', group='json', default=None,
|
||||
):
|
||||
return self._parse_json(store, video_id, transform_source=urllib.parse.unquote)
|
||||
return self._search_json(start, webpage, 'window pagestore', video_id)
|
||||
|
||||
def _extract_movie(self, webpage, video_id, name, is_live):
|
||||
window_stores = self._extract_pagestore(webpage, video_id)
|
||||
movie_stores = [
|
||||
# extract all three important data (most of data are duplicated each other, but slightly different!)
|
||||
traverse_obj(window_stores, ('v8', 'state', 'movie'), expected_type=dict),
|
||||
traverse_obj(window_stores, ('v8', 'movie'), expected_type=dict),
|
||||
traverse_obj(window_stores, 'movieStore', expected_type=dict),
|
||||
]
|
||||
if not any(movie_stores):
|
||||
raise ExtractorError(f'Failed to extract {name} info')
|
||||
def _call_api(self, path, item_id):
|
||||
return self._download_json(
|
||||
f'{self._API_BASE}/{path}', item_id,
|
||||
headers=self._api_headers, expected_status=401)
|
||||
|
||||
formats = list(self._expand_media(video_id, get_first(movie_stores, 'media')))
|
||||
if not formats:
|
||||
# archived livestreams or subscriber-only videos
|
||||
cookies = self._get_cookies('https://www.openrec.tv/')
|
||||
detail = self._download_json(
|
||||
f'https://apiv5.openrec.tv/api/v5/movies/{video_id}/detail', video_id,
|
||||
headers={
|
||||
'Origin': 'https://www.openrec.tv',
|
||||
'Referer': 'https://www.openrec.tv/',
|
||||
'access-token': try_get(cookies, lambda x: x.get('access_token').value),
|
||||
'uuid': try_get(cookies, lambda x: x.get('uuid').value),
|
||||
})
|
||||
new_media = traverse_obj(detail, ('data', 'items', ..., 'media'), get_all=False)
|
||||
formats = list(self._expand_media(video_id, new_media))
|
||||
is_live = False
|
||||
def _parse_openrec_metadata(self, page_store, video_id):
|
||||
info = traverse_obj(page_store, ('v8', 'movie', {dict}))
|
||||
|
||||
return {
|
||||
target_members = traverse_obj(info, (
|
||||
'targetMembers', ..., 'type', {str}, filter, any))
|
||||
needs_subscription = target_members == 'subscription'
|
||||
needs_auth = target_members == 'ppv'
|
||||
|
||||
me = self._call_api('users/me', video_id)
|
||||
needs_premium = traverse_obj(info, (
|
||||
'publicType', {str}, filter)) == 'premium'
|
||||
is_premium = traverse_obj(me, (
|
||||
'data', 'items', ..., 'is_premium', {bool}, any)) or False
|
||||
|
||||
detail = self._call_api(f'movies/{video_id}/detail', video_id)
|
||||
is_member = traverse_obj(detail, (
|
||||
'data', 'items', ..., 'membership', 'is_active', {bool}, any)) or False
|
||||
has_ppv = traverse_obj(detail, (
|
||||
'data', 'items', ..., 'ppv_ticket_products', ..., {dict}, any)) or False
|
||||
|
||||
need = None
|
||||
if needs_premium and not is_premium:
|
||||
need = 'premium membership'
|
||||
elif needs_subscription and not is_member:
|
||||
need = 'channel subscription'
|
||||
elif needs_auth and not has_ppv:
|
||||
need = 'PPV purchase'
|
||||
|
||||
if need:
|
||||
self.raise_login_required(
|
||||
f'This content requires a {need}', metadata_available=True)
|
||||
|
||||
return info, detail, {
|
||||
'id': video_id,
|
||||
'title': get_first(movie_stores, 'title'),
|
||||
'description': get_first(movie_stores, 'introduction'),
|
||||
'thumbnail': get_first(movie_stores, 'thumbnailUrl'),
|
||||
'formats': formats,
|
||||
'uploader': get_first(movie_stores, ('channel', 'user', 'name')),
|
||||
'uploader_id': get_first(movie_stores, ('channel', 'user', 'id')),
|
||||
'timestamp': int_or_none(get_first(movie_stores, ['publishedAt', 'time']), scale=1000) or unified_timestamp(get_first(movie_stores, 'publishedAt')),
|
||||
'is_live': is_live,
|
||||
'http_headers': self._M3U8_HEADERS,
|
||||
'availability': self._availability(
|
||||
needs_premium=needs_premium,
|
||||
needs_subscription=needs_subscription,
|
||||
needs_auth=needs_auth,
|
||||
) or 'public',
|
||||
'http_headers': self._HEADERS,
|
||||
'tags': traverse_obj(page_store, (
|
||||
'movieStore', 'tags', ..., {clean_html}, filter, all, filter)),
|
||||
**traverse_obj(info, {
|
||||
'title': ('title', {clean_html}, filter),
|
||||
'cast': ('casts', ..., 'name', {clean_html}, filter, all, filter),
|
||||
'categories': ('game', 'title', {clean_html}, filter, all, filter),
|
||||
'description': ('introduction', {clean_html}, filter),
|
||||
'duration': ('playTime', 'value', {int_or_none(scale=1000)}),
|
||||
'thumbnail': (('lThumbnailUrl', 'thumbnailUrl'), {url_or_none}, any),
|
||||
'timestamp': ('startedAt', 'time', {int_or_none(scale=1000)}),
|
||||
'view_count': ('totalViews', {int_or_none}),
|
||||
}),
|
||||
**traverse_obj(info, ('channel', 'user', {
|
||||
'channel_follower_count': ('followers', {int_or_none}),
|
||||
'channel_id': ('id', {str_or_none}),
|
||||
'channel': ('name', {clean_html}, filter),
|
||||
'channel_is_verified': ('isOfficial', {bool}),
|
||||
})),
|
||||
}
|
||||
|
||||
|
||||
class OpenRecIE(OpenRecBaseIE):
|
||||
IE_NAME = 'openrec'
|
||||
_VALID_URL = r'https?://(?:www\.)?openrec\.tv/live/(?P<id>[^/?#]+)'
|
||||
IE_NAME = 'mellowfan'
|
||||
IE_DESC = 'mellow-fan'
|
||||
|
||||
_VALID_URL = r'https?://(?:www\.)?(?:mellow-fan\.com|openrec\.tv)/(?:m/)?live/(?P<id>[^/?#]+)'
|
||||
_TESTS = [{
|
||||
'url': 'https://www.openrec.tv/live/2p8v31qe4zy',
|
||||
'only_matching': True,
|
||||
'url': 'https://www.openrec.tv/live/e2zwj0mp6ro',
|
||||
'info_dict': {
|
||||
'id': 'e2zwj0mp6ro',
|
||||
'ext': 'mp4',
|
||||
'title': '収束',
|
||||
'availability': 'public',
|
||||
'categories': ['雑談'],
|
||||
'channel': 'おおえのたかゆき',
|
||||
'channel_follower_count': int,
|
||||
'channel_id': 'oekaki',
|
||||
'channel_is_verified': True,
|
||||
'comment_count': int,
|
||||
'description': 'md5:62260f3060b40187282f3213bcd97abd',
|
||||
'duration': 14257,
|
||||
'live_status': 'was_live',
|
||||
'tags': ['雑談'],
|
||||
'thumbnail': r're:https?://.+',
|
||||
'timestamp': 1685271819,
|
||||
'upload_date': '20230528',
|
||||
'view_count': int,
|
||||
},
|
||||
'skip': '404 Not Found',
|
||||
}, {
|
||||
'url': 'https://www.openrec.tv/live/wez93eqvjzl',
|
||||
'only_matching': True,
|
||||
# SP
|
||||
'url': 'https://www.mellow-fan.com/live/2p8vv29438y',
|
||||
'info_dict': {
|
||||
'id': '2p8vv29438y',
|
||||
'ext': 'mp4',
|
||||
'title': 'それいけ加藤純一探検隊! 〜南の孤島で希少生物を探せスペシャル〜 (OPENRECプレミアム会員限定)',
|
||||
'availability': 'premium_only',
|
||||
'categories': ['雑談'],
|
||||
'channel': '加藤 純一',
|
||||
'channel_follower_count': int,
|
||||
'channel_id': 'junichi_kato_channel',
|
||||
'channel_is_verified': True,
|
||||
'comment_count': int,
|
||||
'description': 'md5:693bc0c838ff6080f9ca00d41dc5e840',
|
||||
'duration': 24336,
|
||||
'live_status': 'was_live',
|
||||
'release_date': '20251008',
|
||||
'release_timestamp': 1759892400,
|
||||
'thumbnail': r're:https?://.+',
|
||||
'timestamp': 1759891802,
|
||||
'upload_date': '20251008',
|
||||
'view_count': int,
|
||||
},
|
||||
}, {
|
||||
# Members only
|
||||
'url': 'https://www.mellow-fan.com/live/kdr7nldqgzj',
|
||||
'info_dict': {
|
||||
'id': 'kdr7nldqgzj',
|
||||
'ext': 'mp4',
|
||||
'title': '【ゲーム実況生配信】小森結梨のひきこもりゲーム部屋#20[ゲスト:梅澤めぐ]',
|
||||
'availability': 'subscriber_only',
|
||||
'categories': ['Human Fall Flat'],
|
||||
'channel': 'セカンドショットGAME部',
|
||||
'channel_follower_count': int,
|
||||
'channel_id': 'secondshot_games',
|
||||
'channel_is_verified': True,
|
||||
'chapters': [
|
||||
{'start_time': 0, 'title': 'バイオハザード RE:2', 'end_time': 443},
|
||||
{'start_time': 443, 'title': 'ぷにゃん', 'end_time': 3215},
|
||||
{'start_time': 3215, 'title': 'Human Fall Flat', 'end_time': 7163},
|
||||
],
|
||||
'comment_count': int,
|
||||
'description': 'md5:b77fac6fb0ad86048dc6377ec2c22646',
|
||||
'duration': 7163,
|
||||
'live_status': 'was_live',
|
||||
'release_date': '20260220',
|
||||
'release_timestamp': 1771581300,
|
||||
'tags': ['声優'],
|
||||
'thumbnail': r're:https?://.+',
|
||||
'timestamp': 1771581322,
|
||||
'upload_date': '20260220',
|
||||
'view_count': int,
|
||||
},
|
||||
'skip': 'Subscribers only',
|
||||
}, {
|
||||
# PPV
|
||||
'url': 'https://www.mellow-fan.com/live/e5rk93xn1zv',
|
||||
'info_dict': {
|
||||
'id': 'e5rk93xn1zv',
|
||||
'ext': 'mp4',
|
||||
'title': '三川華月生誕パーティー2026 【ゲスト:幸村恵理/北原沙弥香】',
|
||||
'availability': 'needs_auth',
|
||||
'categories': ['雑談'],
|
||||
'channel': '三川華月の開店!はるちゃん食堂',
|
||||
'channel_follower_count': int,
|
||||
'channel_id': 'haruna_harusyoku',
|
||||
'channel_is_verified': True,
|
||||
'description': 'md5:912d0c2d60d343e28300fc18bd491d6c',
|
||||
'duration': 6451,
|
||||
'live_status': 'was_live',
|
||||
'release_date': '20260222',
|
||||
'release_timestamp': 1771758000,
|
||||
'thumbnail': r're:https?://.+',
|
||||
'timestamp': 1771758062,
|
||||
'upload_date': '20260222',
|
||||
'view_count': int,
|
||||
},
|
||||
'skip': 'Paid video',
|
||||
}]
|
||||
|
||||
@staticmethod
|
||||
def _json2xml(subs, started_at):
|
||||
def filter_valid(items):
|
||||
yield from traverse_obj(items, (
|
||||
lambda _, v: clean_html(v['message']) and not v['stamp']))
|
||||
|
||||
total = collections.Counter(
|
||||
traverse_obj(subs, (..., 'posted_at', {parse_iso8601})))
|
||||
order = collections.defaultdict(int)
|
||||
|
||||
root = xml.etree.ElementTree.Element('packet')
|
||||
for i, s in enumerate(filter_valid(subs), 1):
|
||||
posted_at = traverse_obj(s, ('posted_at', {parse_iso8601}))
|
||||
offset = order[posted_at] / total[posted_at] + 1
|
||||
order[posted_at] += 1
|
||||
vpos = int_or_none((posted_at - started_at + offset) * 100)
|
||||
|
||||
xml.etree.ElementTree.SubElement(
|
||||
root, 'chat', filter_dict({
|
||||
**traverse_obj(s, ('user', {
|
||||
'premium': ('is_premium', {bool}, {lambda x: '1' if x else '0'}),
|
||||
'user_id': ('id', {str_or_none}),
|
||||
'name': ('nickname', {str}, filter),
|
||||
})),
|
||||
'no': str(i),
|
||||
'vpos': str_or_none(vpos),
|
||||
'date': str_or_none(posted_at),
|
||||
}),
|
||||
).text = traverse_obj(s, ('message', {clean_html}, filter))
|
||||
xml.etree.ElementTree.indent(root, space=' ')
|
||||
|
||||
return xml.etree.ElementTree.tostring(
|
||||
root, encoding='utf-8', xml_declaration=True).decode()
|
||||
|
||||
def _get_subtitles(self, duration, started_at, video_id):
|
||||
ended_at = started_at + duration
|
||||
timestamp = started_at
|
||||
|
||||
subs = []
|
||||
for page in itertools.count(1):
|
||||
created_at = dt.datetime.fromtimestamp(
|
||||
timestamp, dt.timezone(dt.timedelta(hours=9))).strftime('%Y-%m-%dT%H:%M:%S%z')
|
||||
|
||||
chats = self._download_json(
|
||||
f'{self._PUBLIC_API_BASE}/movies/{video_id}/chats',
|
||||
video_id, f'Downloading chats page {page}', query={
|
||||
'from_created_at': created_at,
|
||||
'is_including_system_message': 'true',
|
||||
})
|
||||
if not chats:
|
||||
break
|
||||
|
||||
subs.extend(chats)
|
||||
last_posted_at = traverse_obj(chats, (-1, 'posted_at', {parse_iso8601}))
|
||||
if ended_at < last_posted_at:
|
||||
break
|
||||
|
||||
timestamp = last_posted_at + 1
|
||||
time.sleep(0.1)
|
||||
|
||||
return {
|
||||
'chats': [{
|
||||
'data': json.dumps(subs, indent=2, ensure_ascii=False),
|
||||
'ext': 'json',
|
||||
}, {
|
||||
'data': self._json2xml(subs, started_at),
|
||||
'ext': 'xml',
|
||||
}],
|
||||
}
|
||||
|
||||
def _real_extract(self, url):
|
||||
video_id = self._match_id(url)
|
||||
webpage = self._download_webpage(f'https://www.openrec.tv/live/{video_id}', video_id)
|
||||
webpage = self._download_webpage(url, video_id, expected_status=404)
|
||||
page_store = self._extract_pagestore(webpage, video_id)
|
||||
if traverse_obj(page_store, ('movieStore', 'notFound', {bool})):
|
||||
raise ExtractorError('This video in no longer available', expected=True)
|
||||
|
||||
return self._extract_movie(webpage, video_id, 'live', True)
|
||||
info, detail, metadata = self._parse_openrec_metadata(page_store, video_id)
|
||||
live_status = {
|
||||
'ARCHIVE': 'was_live',
|
||||
'COMING_UP': 'is_upcoming',
|
||||
'LIVE_STREAMING': 'is_live',
|
||||
'UPLOADED': 'not_live',
|
||||
}.get(info.get('onAirStatus'))
|
||||
|
||||
release_timestamp = traverse_obj(page_store, ('movieStore', 'willStartAt', {parse_iso8601}))
|
||||
if live_status == 'is_upcoming':
|
||||
if release_timestamp is not None:
|
||||
start_time = dt.datetime.fromtimestamp(
|
||||
release_timestamp, dt.timezone.utc,
|
||||
).astimezone().strftime('%Y-%m-%d %H:%M:%S %Z')
|
||||
msg = f'This livestream is scheduled to start at {start_time}'
|
||||
else:
|
||||
msg = 'This livestream has not yet started'
|
||||
|
||||
self.raise_no_formats(msg, expected=True)
|
||||
|
||||
return {
|
||||
'id': video_id,
|
||||
'live_status': live_status,
|
||||
'release_timestamp': release_timestamp,
|
||||
}
|
||||
|
||||
duration = metadata['duration']
|
||||
started_at = metadata['timestamp']
|
||||
|
||||
chapters = []
|
||||
for chapter in traverse_obj(info, (
|
||||
'chapters', lambda _, v: int_or_none(v['chapterAt']['time']),
|
||||
)):
|
||||
chapter_at = traverse_obj(chapter, ('chapterAt', 'time', {int_or_none(scale=1000)}))
|
||||
chapters.append({
|
||||
'start_time': chapter_at - started_at,
|
||||
'title': traverse_obj(chapter, ('title', {clean_html}, filter)),
|
||||
})
|
||||
|
||||
formats = []
|
||||
is_dvr = live_status == 'is_live' and self.get_param('live_from_start')
|
||||
media_keys = ('url_dvr', 'url_dvr_audio') if is_dvr else ('url', 'url_audio')
|
||||
for m3u8_url in traverse_obj(detail, (
|
||||
'data', 'items', ...,
|
||||
('media', 'subs_trial_media'), media_keys, {url_or_none},
|
||||
)):
|
||||
fmts = self._extract_m3u8_formats(
|
||||
m3u8_url, video_id, 'mp4', headers=self._HEADERS)
|
||||
for fmt in fmts:
|
||||
if is_dvr:
|
||||
fmt.setdefault('downloader_options', {}).update({'ffmpeg_args': ['-live_start_index', '0']})
|
||||
fmt['is_from_start'] = True
|
||||
formats.extend(fmts)
|
||||
|
||||
return {
|
||||
'chapters': chapters or None,
|
||||
'comment_count': traverse_obj(page_store, (
|
||||
'commentStore', 'commentCount', {int_or_none})),
|
||||
'formats': formats,
|
||||
'live_status': live_status,
|
||||
'release_timestamp': release_timestamp,
|
||||
'subtitles': self.extract_subtitles(duration, started_at, video_id),
|
||||
**metadata,
|
||||
}
|
||||
|
||||
|
||||
class OpenRecCaptureIE(OpenRecBaseIE):
|
||||
IE_NAME = 'openrec:capture'
|
||||
_VALID_URL = r'https?://(?:www\.)?openrec\.tv/capture/(?P<id>[^/?#]+)'
|
||||
IE_NAME = 'mellowfan:capture'
|
||||
|
||||
_VALID_URL = r'https?://(?:www\.)?(?:mellow-fan\.com|openrec\.tv)/(?:m/)?capture/(?P<id>[^/?#]+)'
|
||||
_TESTS = [{
|
||||
'url': 'https://www.openrec.tv/capture/l9nk2x4gn14',
|
||||
'only_matching': True,
|
||||
}, {
|
||||
'url': 'https://www.openrec.tv/capture/mldjr82p7qk',
|
||||
'url': 'https://www.mellow-fan.com/capture/l2q00vxl8q8',
|
||||
'info_dict': {
|
||||
'id': 'mldjr82p7qk',
|
||||
'title': 'たいじの恥ずかしい英語力',
|
||||
'uploader': 'たいちゃんねる',
|
||||
'uploader_id': 'Yaritaiji',
|
||||
'upload_date': '20210803',
|
||||
'id': 'l2q00vxl8q8',
|
||||
'ext': 'mp4',
|
||||
'title': '????',
|
||||
'channel': '布団ちゃん',
|
||||
'channel_id': 'indegnasen',
|
||||
'duration': 89,
|
||||
'thumbnail': r're:https?://.+',
|
||||
'timestamp': 1637589871,
|
||||
'upload_date': '20211122',
|
||||
},
|
||||
}, {
|
||||
'url': 'https://www.mellow-fan.com/capture/9pdz9334vng',
|
||||
'info_dict': {
|
||||
'id': '9pdz9334vng',
|
||||
'ext': 'mp4',
|
||||
'title': 'オプレの現実',
|
||||
'channel': 'ゆゆうた&みゃこの泥沼バラエティ',
|
||||
'channel_id': 'doronuma-variety',
|
||||
'duration': 64,
|
||||
'thumbnail': r're:https?://.+',
|
||||
'timestamp': 1677585253,
|
||||
'upload_date': '20230228',
|
||||
},
|
||||
}]
|
||||
|
||||
def _real_extract(self, url):
|
||||
video_id = self._match_id(url)
|
||||
webpage = self._download_webpage(f'https://www.openrec.tv/capture/{video_id}', video_id)
|
||||
webpage = self._download_webpage(url, video_id)
|
||||
page_store = self._extract_pagestore(webpage, video_id)
|
||||
|
||||
window_stores = self._extract_pagestore(webpage, video_id)
|
||||
movie_store = window_stores.get('movie')
|
||||
|
||||
capture_data = window_stores.get('capture')
|
||||
if not capture_data:
|
||||
raise ExtractorError('Cannot extract title')
|
||||
|
||||
formats = self._extract_m3u8_formats(
|
||||
capture_data.get('source'), video_id, ext='mp4', headers=self._M3U8_HEADERS)
|
||||
capture = page_store['capture']
|
||||
m3u8_url = traverse_obj(capture, ('source', {url_or_none}))
|
||||
|
||||
return {
|
||||
'id': video_id,
|
||||
'title': capture_data.get('title'),
|
||||
'thumbnail': capture_data.get('thumbnailUrl'),
|
||||
'formats': formats,
|
||||
'timestamp': unified_timestamp(traverse_obj(movie_store, 'createdAt', expected_type=str)),
|
||||
'uploader': traverse_obj(movie_store, ('channel', 'name'), expected_type=str),
|
||||
'uploader_id': traverse_obj(movie_store, ('channel', 'id'), expected_type=str),
|
||||
'upload_date': unified_strdate(capture_data.get('createdAt')),
|
||||
'http_headers': self._M3U8_HEADERS,
|
||||
'formats': self._extract_m3u8_formats(
|
||||
m3u8_url, video_id, 'mp4', headers=self._HEADERS),
|
||||
'http_headers': self._HEADERS,
|
||||
**traverse_obj(page_store, ('movie', 'channel', {
|
||||
'channel': ('name', {clean_html}, filter),
|
||||
'channel_id': ('id', {str}),
|
||||
})),
|
||||
**traverse_obj(capture, {
|
||||
'title': ('title', {clean_html}, filter),
|
||||
'duration': ({lambda x: int_or_none(x['endTime']) - int_or_none(x['startTime'])}),
|
||||
'thumbnail': ('thumbnailUrl', {url_or_none}),
|
||||
'timestamp': ('publishedAt', {parse_iso8601}),
|
||||
}),
|
||||
}
|
||||
|
||||
|
||||
class OpenRecMovieIE(OpenRecBaseIE):
|
||||
IE_NAME = 'openrec:movie'
|
||||
_VALID_URL = r'https?://(?:www\.)?openrec\.tv/movie/(?P<id>[^/?#]+)'
|
||||
IE_NAME = 'mellowfan:movie'
|
||||
|
||||
_VALID_URL = r'https?://(?:www\.)?(?:mellow-fan\.com|openrec\.tv)/(?:m/)?movie/(?P<id>[^/?#]+)'
|
||||
_TESTS = [{
|
||||
'url': 'https://www.openrec.tv/movie/nqz5xl5km8v',
|
||||
'url': 'https://www.mellow-fan.com/movie/e5rk9k4o6zv',
|
||||
'info_dict': {
|
||||
'id': 'nqz5xl5km8v',
|
||||
'title': '限定コミュニティ(Discord)参加方法ご説明動画',
|
||||
'description': 'md5:ebd563e5f5b060cda2f02bf26b14d87f',
|
||||
'thumbnail': r're:https://.+',
|
||||
'uploader': 'タイキとカズヒロ',
|
||||
'uploader_id': 'taiki_to_kazuhiro',
|
||||
'timestamp': 1638856800,
|
||||
'id': 'e5rk9k4o6zv',
|
||||
'ext': 'mp4',
|
||||
'title': 'みゃこRaMuの企画会議#3 ~2人のやってみたいこと~',
|
||||
'availability': 'public',
|
||||
'categories': ['雑談'],
|
||||
'channel': 'みゃことRaMuの何して遊ぶ?',
|
||||
'channel_follower_count': int,
|
||||
'channel_id': 'myakoramu',
|
||||
'channel_is_verified': True,
|
||||
'description': 'md5:90924fd73356ebd574bec8d761d4fa62',
|
||||
'duration': 771,
|
||||
'tags': ['雑談'],
|
||||
'thumbnail': r're:https?://.+',
|
||||
'view_count': int,
|
||||
},
|
||||
}, {
|
||||
'url': 'https://www.openrec.tv/movie/2p8vvex548y?playlist_id=98brq96vvsgn2nd',
|
||||
'only_matching': True,
|
||||
# Members only
|
||||
'url': 'https://www.mellow-fan.com/movie/n9ze6q3eo84',
|
||||
'info_dict': {
|
||||
'id': 'n9ze6q3eo84',
|
||||
'ext': 'mp4',
|
||||
'title': '大西亜玖璃・高尾奏音のあぐのんる~むらぼ♪第123回傑作選vol.3【高画質・完全版】',
|
||||
'availability': 'subscriber_only',
|
||||
'categories': ['雑談'],
|
||||
'channel': 'セカンドショットGAME部',
|
||||
'channel_follower_count': int,
|
||||
'channel_id': 'secondshot_games',
|
||||
'channel_is_verified': True,
|
||||
'description': 'md5:b7ab5ddd71ba5edc1141cb427af7a9c6',
|
||||
'duration': 1800,
|
||||
'tags': ['声優'],
|
||||
'thumbnail': r're:https?://.+',
|
||||
'view_count': int,
|
||||
},
|
||||
'skip': 'Subscribers only',
|
||||
}, {
|
||||
# PPV
|
||||
'url': 'https://www.mellow-fan.com/movie/em8xvd4ljr2',
|
||||
'info_dict': {
|
||||
'id': 'em8xvd4ljr2',
|
||||
'ext': 'mp4',
|
||||
'title': '【PPV購入特典映像】KAWAII LAB. SESSION in OKINAWA',
|
||||
'availability': 'needs_auth',
|
||||
'categories': ['ミュージック'],
|
||||
'channel': 'KAWAII LAB.チャンネル',
|
||||
'channel_follower_count': int,
|
||||
'channel_id': 'KAWAIILAB',
|
||||
'channel_is_verified': True,
|
||||
'description': 'md5:e9f67d8648d3cbe35df1dd689afc29a8',
|
||||
'duration': 2002,
|
||||
'tags': ['アイドル'],
|
||||
'thumbnail': r're:https?://.+',
|
||||
'view_count': int,
|
||||
},
|
||||
'skip': 'Paid video',
|
||||
}]
|
||||
|
||||
def _real_extract(self, url):
|
||||
video_id = self._match_id(url)
|
||||
webpage = self._download_webpage(f'https://www.openrec.tv/movie/{video_id}', video_id)
|
||||
webpage = self._download_webpage(url, video_id)
|
||||
page_store = self._extract_pagestore(webpage, video_id)
|
||||
_, detail, metadata = self._parse_openrec_metadata(page_store, video_id)
|
||||
|
||||
return self._extract_movie(webpage, video_id, 'movie', False)
|
||||
formats = []
|
||||
for m3u8_url in traverse_obj(detail, (
|
||||
'data', 'items', ..., 'media',
|
||||
('url', 'url_audio'), {url_or_none},
|
||||
)):
|
||||
formats.extend(self._extract_m3u8_formats(
|
||||
m3u8_url, video_id, 'mp4', headers=self._HEADERS))
|
||||
|
||||
return {
|
||||
'formats': formats,
|
||||
**metadata,
|
||||
}
|
||||
|
||||
|
||||
class OpenRecPlaylistIE(OpenRecBaseIE):
|
||||
IE_NAME = 'mellowfan:playlist'
|
||||
|
||||
_VALID_URL = r'https?://(?:www\.)?(?:mellow-fan\.com|openrec\.tv)/(?:m/)?user/[^/?#]+/playlist/(?P<id>[^/?#]+)'
|
||||
_TESTS = [{
|
||||
# live
|
||||
'url': 'https://www.mellow-fan.com/user/DbD_BPF/playlist/j59svruhtua2z8t',
|
||||
'info_dict': {
|
||||
'id': 'j59svruhtua2z8t',
|
||||
'title': 'BPFのおすすめ',
|
||||
},
|
||||
'playlist_mincount': 10,
|
||||
}, {
|
||||
# capture
|
||||
'url': 'https://www.mellow-fan.com/user/sagara_mayu/playlist/xngNMzv71yLjGdW',
|
||||
'info_dict': {
|
||||
'id': 'xngNMzv71yLjGdW',
|
||||
'title': '相良茉優のFAN!FUN!FACTORY!キャプチャ',
|
||||
},
|
||||
'playlist_mincount': 4,
|
||||
}, {
|
||||
# movie
|
||||
'url': 'https://www.mellow-fan.com/user/oreranohonoka/playlist/sficoshvi9dgkqh',
|
||||
'info_dict': {
|
||||
'id': 'sficoshvi9dgkqh',
|
||||
'title': 'SP(スペシャル)映像:(短編映像)おにいたむプランだけが視聴できます',
|
||||
},
|
||||
'playlist_mincount': 111,
|
||||
}]
|
||||
|
||||
def _entries(self, items):
|
||||
for movie in traverse_obj(items, (
|
||||
'playlist_movies', ..., 'movie', 'id', {str_or_none},
|
||||
)):
|
||||
is_live = traverse_obj(movie, ('movie', 'is_live', {bool}))
|
||||
path, ie = ('live', OpenRecIE) if is_live else ('movie', OpenRecMovieIE)
|
||||
movie_id = movie['movie']['id']
|
||||
|
||||
yield self.url_result(f'{self._BASE_URL}/{path}/{movie_id}', ie)
|
||||
|
||||
for capture in traverse_obj(items, (
|
||||
'playlist_captures', ...,
|
||||
'capture_relation', 'capture', 'id', {str_or_none},
|
||||
)):
|
||||
capture_id = capture['capture_relation']['capture']['id']
|
||||
|
||||
yield self.url_result(
|
||||
f'{self._BASE_URL}/capture/{capture_id}', OpenRecCaptureIE)
|
||||
|
||||
def _real_extract(self, url):
|
||||
playlist_id = self._match_id(url)
|
||||
me = self._call_api(f'users/me/playlists/{playlist_id}', playlist_id)
|
||||
|
||||
if items := traverse_obj(me, (
|
||||
'data', 'items', ..., {dict}, any,
|
||||
)):
|
||||
return self.playlist_result(
|
||||
self._entries(items), playlist_id,
|
||||
traverse_obj(items, ('title', {clean_html}, filter)))
|
||||
|
||||
webpage = self._download_webpage(url, playlist_id)
|
||||
playlist_title = traverse_obj(webpage, (
|
||||
{find_element(cls='sc-1ak77bz-10')}, {clean_html}, filter))
|
||||
|
||||
return self.playlist_from_matches(traverse_obj(webpage, (
|
||||
{find_elements(cls='sc-vc0xhn-0')}, ...,
|
||||
{find_element(cls='sc-1ddd11y-0', html=True)},
|
||||
{extract_attributes}, 'href', {str},
|
||||
)), playlist_id, playlist_title, getter=urljoin(f'{self._BASE_URL}/'))
|
||||
|
||||
|
||||
class OpenRecChannelIE(OpenRecBaseIE):
|
||||
IE_NAME = 'mellowfan:channel'
|
||||
|
||||
_PAGE_SIZE = 40
|
||||
_VALID_URL = r'https?://(?:www\.)?(?:mellow-fan\.com|openrec\.tv)/(?:m/)?user/(?P<id>[^/?#]+)$'
|
||||
_TESTS = [{
|
||||
'url': 'https://www.mellow-fan.com/user/OPENRECPARK',
|
||||
'info_dict': {
|
||||
'id': 'OPENRECPARK',
|
||||
'title': 'OPENREC PARK',
|
||||
},
|
||||
'playlist_mincount': 40,
|
||||
}]
|
||||
|
||||
def _fetch_page(self, channel_id, page):
|
||||
page += 1
|
||||
search_movies = self._download_json(
|
||||
f'{self._PUBLIC_API_BASE}/search-movies', channel_id,
|
||||
f'Downloading page {page}', query={
|
||||
'channel_ids': channel_id,
|
||||
'include_live': 'true',
|
||||
'include_upload': 'true',
|
||||
'onair_status': '2',
|
||||
'include_deleted': 'true',
|
||||
'sort': 'published_at',
|
||||
'page': str(page),
|
||||
})
|
||||
|
||||
for movie in traverse_obj(search_movies, (
|
||||
lambda _, v: str_or_none(v['movie_type']) and str_or_none(v['id']),
|
||||
)):
|
||||
path, ie = ('live', OpenRecIE) if movie['movie_type'] == '1' else ('movie', OpenRecMovieIE)
|
||||
|
||||
yield self.url_result(f'{self._BASE_URL}/{path}/{movie["id"]}', ie)
|
||||
|
||||
def _real_extract(self, url):
|
||||
channel_id = self._match_id(url)
|
||||
webpage = self._download_webpage(url, channel_id)
|
||||
page_store = self._extract_pagestore(webpage, channel_id)
|
||||
|
||||
channel = traverse_obj(page_store, ('state', '_channel', {dict}))
|
||||
movie_count = traverse_obj(channel, ('movieCount', {int_or_none}))
|
||||
|
||||
return self.playlist_result(InAdvancePagedList(
|
||||
functools.partial(self._fetch_page, channel_id),
|
||||
math.ceil(movie_count / self._PAGE_SIZE), self._PAGE_SIZE,
|
||||
), channel_id, traverse_obj(channel, ('user', 'name', {clean_html}, filter)))
|
||||
|
||||
|
||||
class OpenRecChannelSearchIE(OpenRecBaseIE):
|
||||
IE_NAME = 'mellowfan:channel:search'
|
||||
|
||||
_VALID_URL = r'https?://(?:www\.)?(?:mellow-fan\.com|openrec\.tv)/(?:m/)?user/(?P<id>[^/?#]+)/search(?:/(?P<type>capture|movie))?(?:[/?#]|$)'
|
||||
_TESTS = [{
|
||||
'url': 'https://www.mellow-fan.com/user/indegnasen/search?search_query=%E3%82%B9%E3%82%A4%E3%82%AB',
|
||||
'info_dict': {
|
||||
'id': 'indegnasen',
|
||||
'title': 'indegnasen:スイカ',
|
||||
},
|
||||
'playlist_count': 2,
|
||||
}, {
|
||||
'url': 'https://www.mellow-fan.com/user/ofurekodesu/search/movie?search_query=%E3%82%A2%E3%83%AA%E3%82%AA%E5%85%AB%E5%B0%BE',
|
||||
'info_dict': {
|
||||
'id': 'ofurekodesu',
|
||||
'title': 'ofurekodesu:アリオ八尾:movie',
|
||||
},
|
||||
'playlist_mincount': 31,
|
||||
}, {
|
||||
'url': 'https://www.mellow-fan.com/user/DbD_BPF/search/capture?search_query=%E3%81%82%E3%81%A3%E3%81%95%E3%82%8A%E3%81%97%E3%82%87%E3%81%93',
|
||||
'info_dict': {
|
||||
'id': 'DbD_BPF',
|
||||
'title': 'DbD_BPF:あっさりしょこ:capture',
|
||||
},
|
||||
'playlist_mincount': 10,
|
||||
}]
|
||||
|
||||
def _entries(self, channel_id, search_type, search_query):
|
||||
api_url = f'{self._PUBLIC_API_BASE}/search-{search_type}s'
|
||||
type_map = {
|
||||
'capture': OpenRecCaptureIE,
|
||||
'live': OpenRecIE,
|
||||
'movie': OpenRecMovieIE,
|
||||
}
|
||||
|
||||
for page in itertools.count(1):
|
||||
search_items = self._download_json(
|
||||
api_url, channel_id, f'Downloading page {page}', query={
|
||||
'channel_ids': channel_id,
|
||||
'page': page,
|
||||
'search_query': search_query,
|
||||
})
|
||||
if not search_items:
|
||||
break
|
||||
|
||||
for item in search_items:
|
||||
item_type = 'live' if search_type == 'movie' and traverse_obj(item, ('is_live', {bool})) else search_type
|
||||
item_id = traverse_obj(item, ((None, 'capture'), 'id', {str_or_none}, any))
|
||||
|
||||
yield self.url_result(
|
||||
f'{self._BASE_URL}/{item_type}/{item_id}', type_map[item_type])
|
||||
|
||||
def _real_extract(self, url):
|
||||
channel_id, search_type = self._match_valid_url(url).group('id', 'type')
|
||||
search_query = traverse_obj(url, (
|
||||
{parse_qs}, 'search_query', -1, {str}, filter,
|
||||
{require('search query', expected=True)}))
|
||||
|
||||
if not search_type:
|
||||
entries = []
|
||||
for search_type in ('capture', 'movie'):
|
||||
search_url = update_url_query(
|
||||
f'{self._BASE_URL}/user/{channel_id}/search/{search_type}', {'search_query': search_query})
|
||||
entries.append(self.url_result(search_url, OpenRecChannelSearchIE))
|
||||
|
||||
return self.playlist_result(
|
||||
entries, channel_id, join_nonempty(channel_id, search_query, delim=':'))
|
||||
|
||||
return self.playlist_result(
|
||||
self._entries(channel_id, search_type, search_query),
|
||||
channel_id, join_nonempty(channel_id, search_query, search_type, delim=':'))
|
||||
|
||||
@@ -10,6 +10,7 @@ from ..utils import (
|
||||
|
||||
|
||||
class PlaytvakIE(InfoExtractor):
|
||||
_WORKING = False
|
||||
IE_DESC = 'Playtvak.cz, iDNES.cz and Lidovky.cz'
|
||||
_VALID_URL = r'https?://(?:.+?\.)?(?:playtvak|idnes|lidovky|metro)\.cz/.*\?(?:c|idvideo)=(?P<id>[^&]+)'
|
||||
_TESTS = [{
|
||||
|
||||
@@ -5,6 +5,7 @@ from ..utils import int_or_none
|
||||
|
||||
|
||||
class RTL2IE(InfoExtractor):
|
||||
_WORKING = False
|
||||
IE_NAME = 'rtl2'
|
||||
_VALID_URL = r'https?://(?:www\.)?rtl2\.de/sendung/[^/]+/(?:video/(?P<vico_id>\d+)[^/]+/(?P<vivi_id>\d+)-|folge/)(?P<id>[^/?#]+)'
|
||||
_TESTS = [{
|
||||
|
||||
@@ -7,6 +7,7 @@ from ..utils import (
|
||||
|
||||
|
||||
class ShowRoomLiveIE(InfoExtractor):
|
||||
_WORKING = False
|
||||
_VALID_URL = r'https?://(?:www\.)?showroom-live\.com/(?!onlive|timetable|event|campaign|news|ranking|room)(?P<id>[^/?#&]+)'
|
||||
_TEST = {
|
||||
'url': 'https://www.showroom-live.com/48_Nana_Okada',
|
||||
|
||||
+63
-42
@@ -5,12 +5,13 @@ from .common import InfoExtractor
|
||||
from ..networking.exceptions import HTTPError
|
||||
from ..utils import (
|
||||
ExtractorError,
|
||||
clean_html,
|
||||
filter_dict,
|
||||
float_or_none,
|
||||
int_or_none,
|
||||
join_nonempty,
|
||||
mimetype2ext,
|
||||
parse_iso8601,
|
||||
unsmuggle_url,
|
||||
update_url_query,
|
||||
url_or_none,
|
||||
)
|
||||
@@ -22,22 +23,28 @@ class StreaksBaseIE(InfoExtractor):
|
||||
_GEO_BYPASS = False
|
||||
_GEO_COUNTRIES = ['JP']
|
||||
|
||||
def _extract_from_streaks_api(self, project_id, media_id, headers=None, query=None, ssai=False, live_from_start=False):
|
||||
def _streaks_playback_api_url(self, project_id, media_id):
|
||||
return self._API_URL_TEMPLATE.format('playback', project_id, media_id, '')
|
||||
|
||||
def _download_streaks_playback_json(self, project_id, media_id, headers=None):
|
||||
return self._download_json(
|
||||
self._streaks_playback_api_url(project_id, media_id),
|
||||
media_id, 'Downloading STREAKS playback API JSON', headers={
|
||||
'Accept': 'application/json',
|
||||
'Origin': 'https://players.streaks.jp',
|
||||
**self.geo_verification_headers(),
|
||||
**(headers or {}),
|
||||
})
|
||||
|
||||
def _extract_from_streaks_api(self, project_id, media_id, headers=None, query=None, live_from_start=False):
|
||||
try:
|
||||
response = self._download_json(
|
||||
self._API_URL_TEMPLATE.format('playback', project_id, media_id, ''),
|
||||
media_id, 'Downloading STREAKS playback API JSON', headers={
|
||||
'Accept': 'application/json',
|
||||
'Origin': 'https://players.streaks.jp',
|
||||
**self.geo_verification_headers(),
|
||||
**(headers or {}),
|
||||
})
|
||||
response = self._download_streaks_playback_json(project_id, media_id, headers=headers)
|
||||
except ExtractorError as e:
|
||||
if isinstance(e.cause, HTTPError) and e.cause.status in (403, 404):
|
||||
error = self._parse_json(e.cause.response.read().decode(), media_id, fatal=False)
|
||||
message = traverse_obj(error, ('message', {str}))
|
||||
code = traverse_obj(error, ('code', {str}))
|
||||
error_id = traverse_obj(error, ('id', {int}))
|
||||
message = traverse_obj(error, ('message', {clean_html}, filter))
|
||||
code = traverse_obj(error, ('code', {clean_html}, filter))
|
||||
error_id = traverse_obj(error, ('id', {int_or_none}))
|
||||
if code == 'REQUEST_FAILED':
|
||||
if error_id == 124:
|
||||
self.raise_geo_restricted(countries=self._GEO_COUNTRIES)
|
||||
@@ -59,8 +66,12 @@ class StreaksBaseIE(InfoExtractor):
|
||||
|
||||
formats, subtitles = [], {}
|
||||
drm_formats = False
|
||||
sources = response['sources']
|
||||
ssai = traverse_obj(sources, (..., 'ssai', {dict}, any))
|
||||
|
||||
for source in traverse_obj(response, ('sources', lambda _, v: v['src'])):
|
||||
for source in traverse_obj(sources, (
|
||||
lambda _, v: url_or_none(v['src']),
|
||||
)):
|
||||
if source.get('key_systems'):
|
||||
drm_formats = True
|
||||
continue
|
||||
@@ -108,12 +119,12 @@ class StreaksBaseIE(InfoExtractor):
|
||||
'subtitles': subtitles,
|
||||
'uploader_id': project_id,
|
||||
**traverse_obj(response, {
|
||||
'title': ('name', {str}),
|
||||
'description': ('description', {str}, filter),
|
||||
'title': ('name', {clean_html}, filter),
|
||||
'description': ('description', {clean_html}, filter),
|
||||
'duration': ('duration', {float_or_none}),
|
||||
'modified_timestamp': ('updated_at', {parse_iso8601}),
|
||||
'tags': ('tags', ..., {str}),
|
||||
'thumbnails': (('poster', 'thumbnail'), 'src', {'url': {url_or_none}}),
|
||||
'tags': ('tags', ..., {clean_html}, filter, all, filter),
|
||||
'thumbnail': (('thumbnail', 'poster'), 'src', {url_or_none}, any),
|
||||
'timestamp': ('created_at', {parse_iso8601}),
|
||||
}),
|
||||
}
|
||||
@@ -121,26 +132,27 @@ class StreaksBaseIE(InfoExtractor):
|
||||
|
||||
class StreaksIE(StreaksBaseIE):
|
||||
_VALID_URL = [
|
||||
r'https?://players\.streaks\.jp/(?P<project_id>[\w-]+)/[\da-f]+/index\.html\?(?:[^#]+&)?m=(?P<id>(?:ref:)?[\w-]+)',
|
||||
r'https?://players\.streaks\.jp/(?P<project_id>[\w-]+)/(?P<api_key>[\da-f]+)/index\.html\?(?:[^#]+&)?m=(?P<id>(?:ref:)?[\w-]+)',
|
||||
r'https?://playback\.api\.streaks\.jp/v1/projects/(?P<project_id>[\w-]+)/medias/(?P<id>(?:ref:)?[\w-]+)',
|
||||
]
|
||||
_EMBED_REGEX = [rf'<iframe\s+[^>]*\bsrc\s*=\s*["\'](?P<url>{_VALID_URL[0]})']
|
||||
_TESTS = [{
|
||||
'url': 'https://players.streaks.jp/tipness/08155cd19dc14c12bebefb69b92eafcc/index.html?m=dbdf2df35b4d483ebaeeaeb38c594647',
|
||||
# https://online.tipness.co.jp/contents/9a064492-a62b-5d03-4668-6b40d6325b1e
|
||||
'url': 'https://players.streaks.jp/tipness/08155cd19dc14c12bebefb69b92eafcc/index.html?m=ba2c253508914d9ea061a5f26bc58b20',
|
||||
'info_dict': {
|
||||
'id': 'dbdf2df35b4d483ebaeeaeb38c594647',
|
||||
'id': 'ba2c253508914d9ea061a5f26bc58b20',
|
||||
'ext': 'mp4',
|
||||
'title': '3shunenCM_edit.mp4',
|
||||
'display_id': 'dbdf2df35b4d483ebaeeaeb38c594647',
|
||||
'duration': 47.533,
|
||||
'title': 'tarun_suimin.mp4',
|
||||
'duration': 265.344,
|
||||
'live_status': 'not_live',
|
||||
'modified_date': '20230726',
|
||||
'modified_timestamp': 1690356180,
|
||||
'timestamp': 1690355996,
|
||||
'upload_date': '20230726',
|
||||
'modified_date': '20230908',
|
||||
'modified_timestamp': 1694146842,
|
||||
'timestamp': 1694145352,
|
||||
'upload_date': '20230908',
|
||||
'uploader_id': 'tipness',
|
||||
},
|
||||
}, {
|
||||
# https://www.ktv.jp/mycoffeetime/
|
||||
'url': 'https://players.streaks.jp/ktv-web/0298e8964c164ab384c07ef6e08c444b/index.html?m=ref:mycoffeetime_250317',
|
||||
'info_dict': {
|
||||
'id': 'dccdc079e3fd41f88b0c8435e2d453ab',
|
||||
@@ -157,22 +169,24 @@ class StreaksIE(StreaksBaseIE):
|
||||
'uploader_id': 'ktv-web',
|
||||
},
|
||||
}, {
|
||||
'url': 'https://playback.api.streaks.jp/v1/projects/ktv-web/medias/b5411938e1e5435dac71edf829dd4813',
|
||||
# https://www.ktv.jp/news/articles/?id=28105
|
||||
'url': 'https://playback.api.streaks.jp/v1/projects/ktv-news/medias/714171c4c53c409bb41e1572997ebfcf',
|
||||
'info_dict': {
|
||||
'id': 'b5411938e1e5435dac71edf829dd4813',
|
||||
'id': '714171c4c53c409bb41e1572997ebfcf',
|
||||
'ext': 'mp4',
|
||||
'title': 'KANTELE_SYUSEi_0630',
|
||||
'display_id': 'b5411938e1e5435dac71edf829dd4813',
|
||||
'title': '28105.mp4',
|
||||
'duration': 49.984,
|
||||
'live_status': 'not_live',
|
||||
'modified_date': '20250122',
|
||||
'modified_timestamp': 1737522999,
|
||||
'modified_date': '20260702',
|
||||
'modified_timestamp': 1783028559,
|
||||
'thumbnail': r're:https?://.+\.jpg',
|
||||
'timestamp': 1735205137,
|
||||
'upload_date': '20241226',
|
||||
'uploader_id': 'ktv-web',
|
||||
'timestamp': 1783028407,
|
||||
'upload_date': '20260702',
|
||||
'uploader_id': 'ktv-news',
|
||||
},
|
||||
'params': {'extractor_args': {'streaks': {'api_key': ['0ff2ccfb6381401582d6ee60e3cb66a1']}}},
|
||||
}, {
|
||||
# TVer Olympics: website already down, but api remains accessible
|
||||
# https://tver.jp/olympic/paris2024/live/FBLMTEAM11------------SFNL000100--/
|
||||
'url': 'https://playback.api.streaks.jp/v1/projects/tver-olympic/medias/ref:sp_240806_1748_dvr',
|
||||
'info_dict': {
|
||||
'id': 'c10f7345adb648cf804d7578ab93b2e3',
|
||||
@@ -187,8 +201,10 @@ class StreaksIE(StreaksBaseIE):
|
||||
'upload_date': '20240804',
|
||||
'uploader_id': 'tver-olympic',
|
||||
},
|
||||
'params': {'extractor_args': {'streaks': {'api_key': ['e09168c4383d4b18949067022558f071']}}},
|
||||
'skip': 'Invalid URL',
|
||||
}, {
|
||||
# TBS FREE: 24-hour stream
|
||||
# https://cu.tbs.co.jp/simul/simul-02
|
||||
'url': 'https://playback.api.streaks.jp/v1/projects/tbs/medias/ref:simul-02',
|
||||
'info_dict': {
|
||||
'id': 'c4e83a7b48f4409a96adacec674b4e22',
|
||||
@@ -202,12 +218,14 @@ class StreaksIE(StreaksBaseIE):
|
||||
'upload_date': '20240117',
|
||||
'uploader_id': 'tbs',
|
||||
},
|
||||
'skip': 'Invalid URL',
|
||||
}, {
|
||||
# DRM protected
|
||||
'url': 'https://players.streaks.jp/sp-jbc/a12d7ee0f40c49d6a0a2bff520639677/index.html?m=5f89c62f37ee4a68be8e6e3b1396c7d8',
|
||||
'only_matching': True,
|
||||
}]
|
||||
_WEBPAGE_TESTS = [{
|
||||
# https://players.streaks.jp/play/719af2a1d2d544e89bcad3456eeae5d9/index.html?m=2d975178293140dc8074a7fc536a7604
|
||||
'url': 'https://event.play.jp/playnext2023/',
|
||||
'info_dict': {
|
||||
'id': '2d975178293140dc8074a7fc536a7604',
|
||||
@@ -222,6 +240,7 @@ class StreaksIE(StreaksBaseIE):
|
||||
'modified_date': '20250213',
|
||||
'live_status': 'not_live',
|
||||
},
|
||||
'params': {'nocheckcertificate': True},
|
||||
}, {
|
||||
'url': 'https://wowshop.jp/Page/special/cooking_goods/?bid=wowshop&srsltid=AfmBOor_phUNoPEE_UCPiGGSCMrJE5T2US397smvsbrSdLqUxwON0el4',
|
||||
'playlist_mincount': 2,
|
||||
@@ -232,13 +251,15 @@ class StreaksIE(StreaksBaseIE):
|
||||
'age_limit': 0,
|
||||
'thumbnail': 'https://wowshop.jp/Page/special/cooking_goods/images/ogp.jpg',
|
||||
},
|
||||
'skip': 'Invalid URL',
|
||||
}]
|
||||
|
||||
def _real_extract(self, url):
|
||||
url, smuggled_data = unsmuggle_url(url, {})
|
||||
project_id, media_id = self._match_valid_url(url).group('project_id', 'id')
|
||||
mobj = self._match_valid_url(url).groupdict()
|
||||
project_id, media_id = mobj['project_id'], mobj['id']
|
||||
api_key = mobj.get('api_key') or self._configuration_arg('api_key', [None])[0]
|
||||
|
||||
return self._extract_from_streaks_api(
|
||||
project_id, media_id, headers=filter_dict({
|
||||
'X-Streaks-Api-Key': smuggled_data.get('api_key'),
|
||||
'X-Streaks-Api-Key': api_key,
|
||||
}))
|
||||
|
||||
@@ -331,7 +331,7 @@ class KnownLiabilityIE(UnsupportedInfoExtractor):
|
||||
"""
|
||||
|
||||
URLS = (
|
||||
r'motherless\.com',
|
||||
r'motherless\.\w+',
|
||||
r'suno\.com',
|
||||
r'udio\.com',
|
||||
)
|
||||
|
||||
@@ -8,6 +8,7 @@ from ..utils import (
|
||||
|
||||
|
||||
class WallaIE(InfoExtractor):
|
||||
_WORKING = False
|
||||
_VALID_URL = r'https?://vod\.walla\.co\.il/[^/]+/(?P<id>\d+)/(?P<display_id>.+)'
|
||||
_TEST = {
|
||||
'url': 'http://vod.walla.co.il/movie/2642630/one-direction-all-for-one',
|
||||
|
||||
@@ -9,6 +9,7 @@ from ..utils import (
|
||||
|
||||
|
||||
class WhoWatchIE(InfoExtractor):
|
||||
_WORKING = False
|
||||
IE_NAME = 'whowatch'
|
||||
_VALID_URL = r'https?://whowatch\.tv/viewer/(?P<id>\d+)'
|
||||
|
||||
|
||||
@@ -28,7 +28,6 @@ from .jsc._director import initialize_jsc_director
|
||||
from .jsc.provider import JsChallengeRequest, JsChallengeType, NChallengeInput, SigChallengeInput
|
||||
from .pot._director import initialize_pot_director
|
||||
from .pot.provider import PoTokenContext, PoTokenRequest
|
||||
from ...networking.exceptions import HTTPError
|
||||
from ...utils import (
|
||||
NO_DEFAULT,
|
||||
ExtractorError,
|
||||
@@ -1940,6 +1939,7 @@ class YoutubeIE(YoutubeBaseInfoExtractor):
|
||||
lock = threading.Lock()
|
||||
start_time = time.time()
|
||||
formats = [f for f in formats if f.get('is_from_start')]
|
||||
adaptive_last_seq_cache = {}
|
||||
|
||||
def refetch_manifest(itag, client_name, delay):
|
||||
nonlocal formats, start_time, is_live
|
||||
@@ -1956,9 +1956,9 @@ class YoutubeIE(YoutubeBaseInfoExtractor):
|
||||
is_live = live_status == 'is_live'
|
||||
start_time = time.time()
|
||||
|
||||
def mpd_feed(itag, client_name, delay):
|
||||
def url_feed(itag, client_name, delay):
|
||||
"""
|
||||
@returns (manifest_url, manifest_stream_number, is_live) or None
|
||||
@returns (base_url, is_live) or None
|
||||
"""
|
||||
for retry in self.RetryManager(fatal=False):
|
||||
with lock:
|
||||
@@ -1969,33 +1969,39 @@ class YoutubeIE(YoutubeBaseInfoExtractor):
|
||||
if not is_live:
|
||||
retry.error = f'{video_id}: Video is no longer live'
|
||||
else:
|
||||
retry.error = f'Cannot find refreshed manifest for format {itag}{bug_reports_message()}'
|
||||
retry.error = f'Cannot find refreshed url for format {itag}{bug_reports_message()}'
|
||||
continue
|
||||
|
||||
# Formats from ended premieres will be missing a manifest_url
|
||||
# See https://github.com/yt-dlp/yt-dlp/issues/8543
|
||||
if not f.get('manifest_url'):
|
||||
if not f.get('url'):
|
||||
break
|
||||
|
||||
return f['manifest_url'], f['manifest_stream_number'], is_live
|
||||
return f['url'], is_live
|
||||
return None
|
||||
|
||||
for f in formats:
|
||||
f['is_live'] = is_live
|
||||
gen = functools.partial(self._live_dash_fragments, video_id, f['_itag'], f['_client'],
|
||||
live_start_time, mpd_feed, not is_live and f.copy())
|
||||
gen = functools.partial(
|
||||
self._live_adaptive_fragments,
|
||||
video_id,
|
||||
f['_itag'],
|
||||
f['_client'],
|
||||
live_start_time,
|
||||
url_feed if is_live else None,
|
||||
f.get('url') if not is_live else None,
|
||||
f.get('target_duration'),
|
||||
adaptive_last_seq_cache,
|
||||
)
|
||||
if is_live:
|
||||
f['fragments'] = gen
|
||||
f['protocol'] = 'http_dash_segments_generator'
|
||||
else:
|
||||
f['fragments'] = LazyList(gen({}))
|
||||
f['protocol'] = 'http_dash_segments'
|
||||
del f['is_from_start']
|
||||
|
||||
def _live_dash_fragments(self, video_id, itag, client_name, live_start_time, mpd_feed, manifestless_orig_fmt, ctx):
|
||||
def _live_adaptive_fragments(self, video_id, itag, client_name, live_start_time, url_feed, base_url, fragment_duration, last_seq_cache, ctx):
|
||||
FETCH_SPAN, MAX_DURATION = 5, 432000
|
||||
|
||||
mpd_url, stream_number, is_live = None, None, True
|
||||
|
||||
begin_index = 0
|
||||
download_start_time = ctx.get('start') or time.time()
|
||||
|
||||
@@ -2006,98 +2012,74 @@ class YoutubeIE(YoutubeBaseInfoExtractor):
|
||||
'YouTube does not have data before that. If you think this is wrong,'), only_once=True)
|
||||
lack_early_segments = True
|
||||
|
||||
known_idx, no_fragment_score, last_segment_url = begin_index, 0, None
|
||||
fragments, fragment_base_url = None, None
|
||||
|
||||
def _extract_sequence_from_mpd(refresh_sequence, immediate):
|
||||
nonlocal mpd_url, stream_number, is_live, no_fragment_score, fragments, fragment_base_url
|
||||
# Obtain from MPD's maximum seq value
|
||||
old_mpd_url = mpd_url
|
||||
last_error = ctx.pop('last_error', None)
|
||||
expire_fast = immediate or (last_error and isinstance(last_error, HTTPError) and last_error.status == 403)
|
||||
mpd_url, stream_number, is_live = (mpd_feed(itag, client_name, 5 if expire_fast else 18000)
|
||||
or (mpd_url, stream_number, False))
|
||||
if not refresh_sequence:
|
||||
if expire_fast and not is_live:
|
||||
return False, last_seq
|
||||
elif old_mpd_url == mpd_url:
|
||||
return True, last_seq
|
||||
if manifestless_orig_fmt:
|
||||
fmt_info = manifestless_orig_fmt
|
||||
else:
|
||||
try:
|
||||
fmts, _ = self._extract_mpd_formats_and_subtitles(
|
||||
mpd_url, None, note=False, errnote=False, fatal=False)
|
||||
except ExtractorError:
|
||||
fmts = None
|
||||
if not fmts:
|
||||
no_fragment_score += 2
|
||||
return False, last_seq
|
||||
fmt_info = next(x for x in fmts if x['manifest_stream_number'] == stream_number)
|
||||
fragments = fmt_info['fragments']
|
||||
fragment_base_url = fmt_info['fragment_base_url']
|
||||
assert fragment_base_url
|
||||
|
||||
_last_seq = int(re.search(r'(?:/|^)sq/(\d+)', fragments[-1]['path']).group(1))
|
||||
return True, _last_seq
|
||||
known_idx, no_fragment_score = begin_index, 0
|
||||
|
||||
self.write_debug(f'[{video_id}] Generating fragments for format {itag}')
|
||||
while is_live:
|
||||
should_iterate = True
|
||||
while should_iterate:
|
||||
fetch_time = time.time()
|
||||
if no_fragment_score > 30:
|
||||
return
|
||||
if last_segment_url:
|
||||
# Obtain from "X-Head-Seqnum" header value from each segment
|
||||
|
||||
if url_feed and (feed_results := url_feed(itag, client_name, 5 if no_fragment_score > 15 else 18000)):
|
||||
base_url, should_iterate = feed_results
|
||||
else:
|
||||
should_iterate = False
|
||||
if not base_url:
|
||||
no_fragment_score += 2
|
||||
continue
|
||||
|
||||
# Obtain from "X-Head-Seqnum" header value. The bare base URL
|
||||
# may return an empty response body, but the headers are still usable.
|
||||
cache_key = should_iterate, int(time.time() // FETCH_SPAN)
|
||||
if cache_key in last_seq_cache:
|
||||
last_seq = last_seq_cache[cache_key]
|
||||
else:
|
||||
try:
|
||||
urlh = self._request_webpage(
|
||||
last_segment_url, None, note=False, errnote=False, fatal=False)
|
||||
urlh = self._request_webpage(base_url, None, note=False, errnote=False, fatal=False)
|
||||
except ExtractorError:
|
||||
urlh = None
|
||||
last_seq = try_get(urlh, lambda x: int_or_none(x.headers['X-Head-Seqnum']))
|
||||
if last_seq is None:
|
||||
no_fragment_score += 2
|
||||
last_segment_url = None
|
||||
continue
|
||||
else:
|
||||
should_continue, last_seq = _extract_sequence_from_mpd(True, no_fragment_score > 15)
|
||||
if urlh:
|
||||
urlh.close()
|
||||
if last_seq is not None:
|
||||
last_seq_cache.clear()
|
||||
last_seq_cache[cache_key] = last_seq
|
||||
if last_seq is None:
|
||||
no_fragment_score += 2
|
||||
if not should_continue:
|
||||
continue
|
||||
continue
|
||||
|
||||
if known_idx > last_seq:
|
||||
last_segment_url = None
|
||||
no_fragment_score += 5
|
||||
if should_iterate:
|
||||
time.sleep(max(0, FETCH_SPAN + fetch_time - time.time()))
|
||||
continue
|
||||
|
||||
last_seq += 1
|
||||
|
||||
if not url_feed:
|
||||
last_seq -= 2
|
||||
|
||||
if begin_index < 0 and known_idx < 0:
|
||||
# skip from the start when it's negative value
|
||||
known_idx = last_seq + begin_index
|
||||
if lack_early_segments:
|
||||
known_idx = max(known_idx, last_seq - int(MAX_DURATION // fragments[-1]['duration']))
|
||||
try:
|
||||
for idx in range(known_idx, last_seq):
|
||||
# do not update sequence here or you'll get skipped some part of it
|
||||
should_continue, _ = _extract_sequence_from_mpd(False, False)
|
||||
if not should_continue:
|
||||
known_idx = idx - 1
|
||||
raise ExtractorError('breaking out of outer loop')
|
||||
last_segment_url = urljoin(fragment_base_url, f'sq/{idx}')
|
||||
yield {
|
||||
'url': last_segment_url,
|
||||
'fragment_count': last_seq,
|
||||
}
|
||||
if known_idx == last_seq:
|
||||
no_fragment_score += 5
|
||||
else:
|
||||
no_fragment_score = 0
|
||||
known_idx = last_seq
|
||||
except ExtractorError:
|
||||
continue
|
||||
known_idx = max(known_idx, last_seq - int(MAX_DURATION // fragment_duration))
|
||||
|
||||
if manifestless_orig_fmt:
|
||||
# Stop at the first iteration if running for post-live manifestless;
|
||||
# fragment count no longer increase since it starts
|
||||
for idx in range(known_idx, last_seq):
|
||||
yield {
|
||||
'url': update_url_query(base_url, {'sq': str(idx)}),
|
||||
'fragment_count': last_seq,
|
||||
}
|
||||
|
||||
if known_idx == last_seq:
|
||||
no_fragment_score += 5
|
||||
else:
|
||||
no_fragment_score = 0
|
||||
known_idx = last_seq
|
||||
|
||||
if not url_feed:
|
||||
# Post-live: stop after first iteration
|
||||
break
|
||||
|
||||
time.sleep(max(0, FETCH_SPAN + fetch_time - time.time()))
|
||||
@@ -3194,10 +3176,9 @@ class YoutubeIE(YoutubeBaseInfoExtractor):
|
||||
raise ExtractorError('Failed to extract any player response')
|
||||
return prs, player_url
|
||||
|
||||
def _needs_live_processing(self, live_status, duration):
|
||||
if ((live_status == 'is_live' and self.get_param('live_from_start'))
|
||||
or (live_status == 'post_live' and (duration or 0) > 2 * 3600)):
|
||||
return live_status
|
||||
def _needs_live_processing(self, live_status):
|
||||
return live_status == 'post_live' or (
|
||||
live_status == 'is_live' and self.get_param('live_from_start'))
|
||||
|
||||
def _report_pot_format_skipped(self, video_id, client_name, proto):
|
||||
msg = (
|
||||
@@ -3497,8 +3478,14 @@ class YoutubeIE(YoutubeBaseInfoExtractor):
|
||||
https_fmts = []
|
||||
|
||||
for fmt_stream in streaming_formats:
|
||||
# Live adaptive https formats are not supported: skip unless extractor-arg given
|
||||
if fmt_stream.get('targetDurationSec') and skip_bad_formats:
|
||||
if (
|
||||
# It's a live adaptive format
|
||||
fmt_stream.get('targetDurationSec')
|
||||
# The user didn't pass the formats=incomplete extractor-arg
|
||||
and skip_bad_formats
|
||||
# This is not a --live-from-start or post-live stream
|
||||
and not self._needs_live_processing(live_status)
|
||||
):
|
||||
continue
|
||||
|
||||
# FORMAT_STREAM_TYPE_OTF(otf=1) requires downloading the init fragment
|
||||
@@ -3593,6 +3580,12 @@ class YoutubeIE(YoutubeBaseInfoExtractor):
|
||||
if live_status not in ('is_live', 'post_live'):
|
||||
fmt['available_at'] = available_at
|
||||
|
||||
if fmt_stream.get('targetDurationSec') and self._needs_live_processing(live_status):
|
||||
fmt['is_from_start'] = True
|
||||
fmt['target_duration'] = fmt_stream['targetDurationSec']
|
||||
fmt['_itag'] = stream_id[0]
|
||||
fmt['_client'] = client_name
|
||||
|
||||
https_fmts.append(fmt)
|
||||
|
||||
for fmt in https_fmts:
|
||||
@@ -3609,14 +3602,16 @@ class YoutubeIE(YoutubeBaseInfoExtractor):
|
||||
|
||||
yield from process_https_formats()
|
||||
|
||||
needs_live_processing = self._needs_live_processing(live_status, duration)
|
||||
needs_live_processing = self._needs_live_processing(live_status)
|
||||
|
||||
skip_manifests = set(self._configuration_arg('skip'))
|
||||
if (needs_live_processing == 'is_live' # These will be filtered out by YoutubeDL anyway
|
||||
or (needs_live_processing and skip_bad_formats)):
|
||||
if needs_live_processing and skip_bad_formats:
|
||||
skip_manifests.add('hls')
|
||||
|
||||
if skip_bad_formats and live_status == 'is_live' and needs_live_processing != 'is_live':
|
||||
if skip_bad_formats and (
|
||||
live_status == 'is_live'
|
||||
or (live_status == 'post_live' and (duration or 0) > 2 * 3600)
|
||||
):
|
||||
skip_manifests.add('dash')
|
||||
|
||||
def process_manifest_format(f, proto, client_name, itag, missing_pot):
|
||||
@@ -3754,10 +3749,6 @@ class YoutubeIE(YoutubeBaseInfoExtractor):
|
||||
if process_manifest_format(f, 'dash', client_name, format_id, require_po_token and not po_token):
|
||||
f['filesize'] = int_or_none(self._search_regex(
|
||||
r'/clen/(\d+)', f.get('fragment_base_url') or f['url'], 'file size', default=None))
|
||||
if needs_live_processing:
|
||||
f['is_from_start'] = True
|
||||
f['_itag'] = format_id
|
||||
f['_client'] = client_name
|
||||
yield f
|
||||
yield subtitles
|
||||
|
||||
@@ -4129,7 +4120,7 @@ class YoutubeIE(YoutubeBaseInfoExtractor):
|
||||
if not duration and live_end_time and live_start_time:
|
||||
duration = live_end_time - live_start_time
|
||||
|
||||
needs_live_processing = self._needs_live_processing(live_status, duration)
|
||||
needs_live_processing = self._needs_live_processing(live_status)
|
||||
|
||||
def adjust_incomplete_format(fmt, note_suffix='(Last 2 hours)', pref_adjustment=-10):
|
||||
fmt['preference'] = (fmt.get('preference') or -1) + pref_adjustment
|
||||
@@ -4138,26 +4129,19 @@ class YoutubeIE(YoutubeBaseInfoExtractor):
|
||||
# Adjust preference and format note for incomplete live/post-live formats
|
||||
if live_status in ('is_live', 'post_live'):
|
||||
for fmt in formats:
|
||||
protocol = fmt.get('protocol')
|
||||
# Currently, protocol isn't set for adaptive https formats, but this could change
|
||||
is_adaptive = protocol in (None, 'http', 'https')
|
||||
if live_status == 'post_live' and is_adaptive:
|
||||
# Post-live adaptive formats cause HttpFD to raise "Did not get any data blocks"
|
||||
# These formats are *only* useful to external applications, so we can hide them
|
||||
# Set their preference <= -1000 so that FormatSorter flags them as 'hidden'
|
||||
adjust_incomplete_format(fmt, note_suffix='(ended)', pref_adjustment=-5000)
|
||||
# Is it live with --live-from-start? Or is it post-live and its duration is >2hrs?
|
||||
elif needs_live_processing:
|
||||
# Is it live with --live-from-start or post-live?
|
||||
if needs_live_processing:
|
||||
if not fmt.get('is_from_start'):
|
||||
# Post-live m3u8 formats for >2hr streams
|
||||
# Post-live DASH and m3u8 manifests only have the last ~2 hours
|
||||
adjust_incomplete_format(fmt)
|
||||
elif live_status == 'is_live':
|
||||
if protocol == 'http_dash_segments':
|
||||
# Live DASH formats without --live-from-start
|
||||
adjust_incomplete_format(fmt)
|
||||
elif is_adaptive:
|
||||
# Incomplete live adaptive https formats
|
||||
protocol = fmt.get('protocol')
|
||||
# Currently, protocol isn't set for incomplete (non-generated) live adaptive formats
|
||||
if protocol in (None, 'http', 'https'):
|
||||
adjust_incomplete_format(fmt, note_suffix='(incomplete)', pref_adjustment=-20)
|
||||
# Live DASH formats (no longer properly supported)
|
||||
elif protocol == 'http_dash_segments':
|
||||
adjust_incomplete_format(fmt)
|
||||
|
||||
if needs_live_processing:
|
||||
self._prepare_live_from_start_formats(
|
||||
|
||||
+3
-3
@@ -511,7 +511,7 @@ def create_parser():
|
||||
general.add_option(
|
||||
'--live-from-start',
|
||||
action='store_true', dest='live_from_start',
|
||||
help='Download livestreams from the start. Currently experimental and only supported for YouTube, Twitch, and TVer')
|
||||
help='Download livestreams from the start. Currently experimental and only supported for YouTube, Twitch, TVer, and mellow-fan')
|
||||
general.add_option(
|
||||
'--no-live-from-start',
|
||||
action='store_false', dest='live_from_start',
|
||||
@@ -1139,12 +1139,12 @@ def create_parser():
|
||||
dest='external_downloader', metavar='[PROTO:]NAME', default={}, type='str',
|
||||
action='callback', callback=_dict_from_options_callback,
|
||||
callback_kwargs={
|
||||
'allowed_keys': 'http|ftp|m3u8|dash|rtsp|rtmp|mms',
|
||||
'allowed_keys': 'http|ftp|m3u8|dash|rtmp',
|
||||
'default_key': 'default',
|
||||
'process': str.strip,
|
||||
}, help=(
|
||||
'Name or path of the external downloader to use (optionally) prefixed by '
|
||||
'the protocols (http, ftp, m3u8, dash, rstp, rtmp, mms) to use it for. '
|
||||
'the protocols (http, ftp, m3u8, dash, rtmp) to use it for. '
|
||||
f'Currently supports native, {", ".join(sorted(list_external_downloaders()))}. '
|
||||
'You can use this option multiple times to set different downloaders for different protocols. '
|
||||
'E.g. --downloader aria2c --downloader "dash,m3u8:native" will use '
|
||||
|
||||
+1
-2
@@ -181,8 +181,7 @@ def _get_system_deprecation():
|
||||
# Do not inappropriately warn for unofficial/third-party binaries
|
||||
if not ORIGIN.startswith('yt-dlp/'):
|
||||
return None
|
||||
platform_name = platform.platform()
|
||||
if any(platform_name.startswith(f'Windows-{name}') for name in ('8', '2012Server')):
|
||||
if platform.platform().startswith(('Windows-8', 'Windows-2012Server')):
|
||||
return (
|
||||
'Support for Windows 8.x and Windows Server 2012 has been deprecated. '
|
||||
'See https://github.com/yt-dlp/yt-dlp/issues/16917 for details.\n'
|
||||
|
||||
+19
-7
@@ -2076,7 +2076,7 @@ def url_or_none(url):
|
||||
if not url or not isinstance(url, str):
|
||||
return None
|
||||
url = url.strip()
|
||||
return url if re.match(r'(?:(?:https?|rt(?:m(?:pt?[es]?|fp)|sp[su]?)|mms|ftps?|wss?):)?//', url) else None
|
||||
return url if re.match(r'(?:(?:https?|rtm(?:pt?[es]?|fp)|ftps?|wss?):)?//', url) else None
|
||||
|
||||
|
||||
def strftime_or_none(timestamp, date_format='%Y%m%d', default=None):
|
||||
@@ -3195,10 +3195,6 @@ def determine_protocol(info_dict):
|
||||
url = sanitize_url(info_dict['url'])
|
||||
if url.startswith('rtmp'):
|
||||
return 'rtmp'
|
||||
elif url.startswith('mms'):
|
||||
return 'mms'
|
||||
elif url.startswith('rtsp'):
|
||||
return 'rtsp'
|
||||
|
||||
ext = determine_ext(url)
|
||||
if ext == 'm3u8':
|
||||
@@ -4635,8 +4631,21 @@ LINK_TEMPLATES = {
|
||||
'webloc': DOT_WEBLOC_LINK_TEMPLATE,
|
||||
}
|
||||
|
||||
# Ref: https://specifications.freedesktop.org/desktop-entry/latest/value-types.html
|
||||
_DESKTOP_ENTRY_TRANS = str.maketrans({
|
||||
' ': R'\s',
|
||||
'\n': R'\n',
|
||||
'\t': R'\t',
|
||||
'\r': R'\r',
|
||||
'\\': R'\\',
|
||||
})
|
||||
|
||||
def iri_to_uri(iri):
|
||||
|
||||
def _desktop_entry_localestring(s):
|
||||
return s.translate(_DESKTOP_ENTRY_TRANS)
|
||||
|
||||
|
||||
def iri_to_uri(iri, *, allowed_schemes=('http', 'https')):
|
||||
"""
|
||||
Converts an IRI (Internationalized Resource Identifier, allowing Unicode characters) to a URI (Uniform Resource Identifier, ASCII-only).
|
||||
|
||||
@@ -4645,6 +4654,9 @@ def iri_to_uri(iri):
|
||||
|
||||
iri_parts = urllib.parse.urlparse(iri)
|
||||
|
||||
if iri_parts.scheme not in allowed_schemes:
|
||||
raise ValueError(f'"{iri_parts.scheme}" is not in allowed_schemes: {", ".join(allowed_schemes)}')
|
||||
|
||||
if '[' in iri_parts.netloc:
|
||||
raise ValueError('IPv6 URIs are not, yet, supported.')
|
||||
# Querying `.netloc`, when there's only one bracket, also raises a ValueError.
|
||||
@@ -5373,7 +5385,7 @@ class FormatSorter:
|
||||
'hdr': {'type': 'ordered', 'regex': True, 'field': 'dynamic_range',
|
||||
'order': ['dv', '(hdr)?12', r'(hdr)?10\+', '(hdr)?10', 'hlg', '', 'sdr', None]},
|
||||
'proto': {'type': 'ordered', 'regex': True, 'field': 'protocol',
|
||||
'order': ['(ht|f)tps', '(ht|f)tp$', 'm3u8.*', '.*dash', 'websocket_frag', 'rtmpe?', '', 'mms|rtsp', 'ws|websocket', 'f4']},
|
||||
'order': ['(ht|f)tps', '(ht|f)tp$', 'm3u8.*', '.*dash', 'websocket_frag', 'rtmpe?', '', 'ws|websocket', 'f4']},
|
||||
'vext': {'type': 'ordered', 'field': 'video_ext',
|
||||
'order': ('mp4', 'mov', 'webm', 'flv', '', 'none'),
|
||||
'order_free': ('webm', 'mp4', 'mov', 'flv', '', 'none')},
|
||||
|
||||
+3
-3
@@ -1,8 +1,8 @@
|
||||
# Autogenerated by devscripts/update-version.py
|
||||
|
||||
__version__ = '2026.06.09'
|
||||
__version__ = '2026.07.04'
|
||||
|
||||
RELEASE_GIT_HEAD = '821bef0f00178916d60dbc86bc0bcb8cc3bae8d5'
|
||||
RELEASE_GIT_HEAD = '997fa140840a08df3938b40da470c78049fef1f6'
|
||||
|
||||
VARIANT = None
|
||||
|
||||
@@ -12,4 +12,4 @@ CHANNEL = 'stable'
|
||||
|
||||
ORIGIN = 'yt-dlp/yt-dlp'
|
||||
|
||||
_pkg_version = '2026.06.09'
|
||||
_pkg_version = '2026.07.04'
|
||||
|
||||
Reference in New Issue
Block a user