Compare commits
68 commits
rustypipe-
...
main
Author | SHA1 | Date | |
---|---|---|---|
d54277a175 | |||
90f79cc887 | |||
2d3914bc4b | |||
7a019f5706 | |||
7972df0df4 | |||
ed08f9ff9a | |||
4a253e1a47 | |||
|
a445e51b54 | ||
d49ddc13c0 | |||
7b672cd5fd | |||
cad3bcd9e9 | |||
67a231d6d1 | |||
ec13cbb1f3 | |||
d933f1b2fd | |||
70c6f8c3b9 | |||
a5a50c84b7 | |||
7132cf1637 | |||
17933315d9 | |||
6009de7bdd | |||
3599acafef | |||
a3a1d9abf3 | |||
ee3ae40395 | |||
1cffb27cc0 | |||
e6715700d9 | |||
5a6b2c3a62 | |||
d0ae7961ba | |||
8692ca81d9 | |||
abb783219a | |||
43c171761d | |||
da39c64f30 | |||
f37432a48c | |||
03c4d3c392 | |||
479fac1c02 | |||
d875b5442d | |||
97904d7737 | |||
5e646afd1e | |||
8f16e5ba6e | |||
9da3b25be2 | |||
904f8215d8 | |||
d36ba595da | |||
e8324cf3b0 | |||
d053ac3eba | |||
91b020efd4 | |||
114a86a382 | |||
97fb0578b5 | |||
c6bd03fb70 | |||
e1e4fb29c1 | |||
3c83e11e75 | |||
1e1315a837 | |||
e608811e5f | |||
b6bc05c1f3 | |||
882abc53ca | |||
015bd6fcbf | |||
4743f9d8e1 | |||
37a14aa9ce | |||
2c7a3fb5cc | |||
72b5dfec69 | |||
8152ce6b08 | |||
11a0038350 | |||
fb7af3b966 | |||
821984bbd5 | |||
bbbe9b4b32 | |||
3d6de53545 | |||
90540c6aaa | |||
dd0565ba98 | |||
182826a3ac | |||
298e4def93 | |||
618a24c120 |
90 changed files with 22011 additions and 4136 deletions
|
@ -7,6 +7,14 @@ on:
|
|||
jobs:
|
||||
Test:
|
||||
runs-on: cimaster-latest
|
||||
services:
|
||||
warpproxy:
|
||||
image: thetadev256/warpproxy
|
||||
env:
|
||||
WARP_DEVICE_ID: ${{ secrets.WARP_DEVICE_ID }}
|
||||
WARP_ACCESS_TOKEN: ${{ secrets.WARP_ACCESS_TOKEN }}
|
||||
WARP_LICENSE_KEY: ${{ secrets.WARP_LICENSE_KEY }}
|
||||
WARP_PRIVATE_KEY: ${{ secrets.WARP_PRIVATE_KEY }}
|
||||
steps:
|
||||
- name: 📦 Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
@ -17,10 +25,12 @@ jobs:
|
|||
cache-on-failure: "true"
|
||||
|
||||
- name: 📎 Clippy
|
||||
run: cargo clippy --all --features=rss -- -D warnings
|
||||
run: cargo clippy --all --tests --features=rss,indicatif,audiotag -- -D warnings
|
||||
|
||||
- name: 🧪 Test
|
||||
run: cargo nextest run --config-file ~/.config/nextest.toml --profile ci --retries 2 --features rss --workspace
|
||||
env:
|
||||
ALL_PROXY: "http://warpproxy:8124"
|
||||
|
||||
- name: 💌 Upload test report
|
||||
if: always()
|
||||
|
|
|
@ -24,13 +24,8 @@ jobs:
|
|||
echo END_OF_FILE
|
||||
} >> "$GITHUB_ENV"
|
||||
|
||||
- name: 📤 Publish crate on code.thetadev.de
|
||||
run: |
|
||||
mkdir -p ~/.cargo
|
||||
printf '\n\n[registries.thetadev]\nindex = "https://code.thetadev.de/ThetaDev/_cargo-index.git"\ntoken = "Bearer ${{ secrets.FORGEJO_CI_TOKEN }}"\n' >> ~/.cargo/config.toml
|
||||
sed -i "s/^rustypipe.*=\s*{/\0 registry = \"thetadev\",/g" Cargo.toml
|
||||
cargo publish --registry thetadev --allow-dirty --package "${{ env.CRATE }}"
|
||||
git restore Cargo.toml
|
||||
- name: 📤 Publish crate on crates.io
|
||||
run: cargo publish --token ${{ secrets.CARGO_TOKEN }} --package "${{ env.CRATE }}"
|
||||
|
||||
- name: 🎉 Publish release
|
||||
uses: https://gitea.com/actions/release-action@main
|
||||
|
|
|
@ -8,6 +8,7 @@ on:
|
|||
- "renovate.json"
|
||||
schedule:
|
||||
- cron: "0 0 * * *"
|
||||
workflow_dispatch:
|
||||
|
||||
env:
|
||||
RENOVATE_REPOSITORIES: ${{ github.repository }}
|
||||
|
|
|
@ -10,4 +10,4 @@ repos:
|
|||
hooks:
|
||||
- id: cargo-fmt
|
||||
- id: cargo-clippy
|
||||
args: ["--all", "--tests", "--features=rss", "--", "-D", "warnings"]
|
||||
args: ["--all", "--tests", "--features=rss,indicatif,audiotag", "--", "-D", "warnings"]
|
||||
|
|
2
.vscode/settings.json
vendored
2
.vscode/settings.json
vendored
|
@ -1,3 +1,3 @@
|
|||
{
|
||||
"rust-analyzer.cargo.features": ["rss"]
|
||||
"rust-analyzer.cargo.features": ["rss", "indicatif", "audiotag"]
|
||||
}
|
||||
|
|
150
CHANGELOG.md
150
CHANGELOG.md
|
@ -3,77 +3,153 @@
|
|||
All notable changes to this project will be documented in this file.
|
||||
|
||||
|
||||
## [v0.2.0](https://code.thetadev.de/ThetaDev/rustypipe/compare/rustypipe/v0.1.3..rustypipe/v0.2.0) - 2024-06-27
|
||||
## [v0.4.0](https://codeberg.org/ThetaDev/rustypipe/compare/rustypipe/v0.3.0..rustypipe/v0.4.0) - 2024-09-10
|
||||
|
||||
### 🚀 Features
|
||||
|
||||
- Add text formatting (bold/italic/strikethrough) - ([b8825f9](https://code.thetadev.de/ThetaDev/rustypipe/commit/b8825f9199365c873a4f0edd98a435e986b8daa2))
|
||||
- Prefix chip-style web links (social media) with the service name - ([6c41ef2](https://code.thetadev.de/ThetaDev/rustypipe/commit/6c41ef2fb2531e10a12c271e2d48504510a3b0bf))
|
||||
- Make get_visitor_data() public - ([da1d1bd](https://code.thetadev.de/ThetaDev/rustypipe/commit/da1d1bd2a0b214da10436ae221c90a0f88697b9a))
|
||||
- Add UnavailabilityReason: IpBan - ([401d4e8](https://code.thetadev.de/ThetaDev/rustypipe/commit/401d4e8255b1e86444319fed6d114dfbd0f80bbd))
|
||||
- Add YtEntity trait - ([792e3b3](https://code.thetadev.de/ThetaDev/rustypipe/commit/792e3b31e0101087a167935baad39a2e3b4296d0))
|
||||
- Add RustyPipe version constant - ([7a019f5](https://codeberg.org/ThetaDev/rustypipe/commit/7a019f5706e19f7fe9f2e16e3b94d7b98cc8aca9))
|
||||
|
||||
### 🐛 Bug Fixes
|
||||
|
||||
- Remove Innertube API keys, update android player params - ([a8fb337](https://code.thetadev.de/ThetaDev/rustypipe/commit/a8fb337fae9cb0112e0152f9a0a19ebae49c2a4d))
|
||||
- Parsing error when no `music_related` content available - ([8fbd6b9](https://code.thetadev.de/ThetaDev/rustypipe/commit/8fbd6b95b6f01108b46f53fe60a56b0c561e40c1))
|
||||
- Parsing audiobook type in European Portuguese - ([041ce2d](https://code.thetadev.de/ThetaDev/rustypipe/commit/041ce2d08f6021c88e8890034f551f7e01b2f012))
|
||||
- Renovate ci token - ([e0759eb](https://code.thetadev.de/ThetaDev/rustypipe/commit/e0759ebce32a5520245bb2c0cb920734b04ee7dc))
|
||||
|
||||
### 🚜 Refactor
|
||||
|
||||
- [**breaking**] Rename VideoItem/VideoPlayerDetails.length to duration for consistency - ([94e8d24](https://code.thetadev.de/ThetaDev/rustypipe/commit/94e8d24c6848b8bfca70dd03a7d89547ba9d6051))
|
||||
- Show docs.rs feature flags - ([67a231d](https://codeberg.org/ThetaDev/rustypipe/commit/67a231d6d1b6427f500667729a59032f2b28cc65))
|
||||
- A/B test 15 (parsing channel shortsLockupViewModel) - ([7972df0](https://codeberg.org/ThetaDev/rustypipe/commit/7972df0df498edd7801e25037b9b2456367f9204))
|
||||
|
||||
### 📚 Documentation
|
||||
|
||||
- Add logo - ([6646078](https://code.thetadev.de/ThetaDev/rustypipe/commit/66460789449be0d5984cbdb6ec372e69323b7a88))
|
||||
- Fix license badge URL - ([4a253e1](https://codeberg.org/ThetaDev/rustypipe/commit/4a253e1a47317e9999e6ad31ac5c411956a0986a))
|
||||
|
||||
### ⚙️ Miscellaneous Tasks
|
||||
|
||||
- Changelog: fix incorrect version URLs - ([97b6f07](https://code.thetadev.de/ThetaDev/rustypipe/commit/97b6f07399e80e00a6c015d013e744568be125dd))
|
||||
- Update rstest to v0.19.0 - ([50fd1f0](https://code.thetadev.de/ThetaDev/rustypipe/commit/50fd1f08caf39c1298654e06059cc393543e925b))
|
||||
- Introduce MSRV - ([5dbb288](https://code.thetadev.de/ThetaDev/rustypipe/commit/5dbb288a496d53a299effa2026f5258af7b1f176))
|
||||
- Fix clippy lints - ([45b9f2a](https://code.thetadev.de/ThetaDev/rustypipe/commit/45b9f2a627b4e7075ba0b1c5f16efcc19aef7922))
|
||||
- Vscode: enable rss feature by default - ([e75ffbb](https://code.thetadev.de/ThetaDev/rustypipe/commit/e75ffbb5da6198086385ea96383ab9d0791592a5))
|
||||
- Configure Renovate (#3) - ([44c2deb](https://code.thetadev.de/ThetaDev/rustypipe/commit/44c2debea61f70c24ad6d827987e85e2132ed3d1))
|
||||
- *(deps)* Update rust crate tokio to 1.20.4 [security] (#4) - ([ce3ec34](https://code.thetadev.de/ThetaDev/rustypipe/commit/ce3ec34337b8acac41410ea39264aab7423d5801))
|
||||
- *(deps)* Update rust crate quick-xml to 0.34.0 (#5) - ([1e8a1af](https://code.thetadev.de/ThetaDev/rustypipe/commit/1e8a1af08c873cee7feadf63c2eff62753a78f64))
|
||||
- *(deps)* Update rust crate rstest to 0.21.0 (#7) - ([c3af918](https://code.thetadev.de/ThetaDev/rustypipe/commit/c3af918ba53c6230c0e4aef822a0cb2cf120bf3f))
|
||||
- *(deps)* Update rust crate tokio to 1.20.4 [security] (#10) - ([a445e51](https://codeberg.org/ThetaDev/rustypipe/commit/a445e51b54a9afc44cd9657260a0b3d2abddbfa6))
|
||||
|
||||
## [v0.1.3](https://code.thetadev.de/ThetaDev/rustypipe/compare/rustypipe/v0.1.2..rustypipe/v0.1.3) - 2024-04-01
|
||||
|
||||
## [v0.3.0](https://codeberg.org/ThetaDev/rustypipe/compare/rustypipe/v0.2.1..rustypipe/v0.3.0) - 2024-08-18
|
||||
|
||||
### 🚀 Features
|
||||
|
||||
- Add client_type to VideoPlayer, simplify MapResponse trait - ([90540c6](https://codeberg.org/ThetaDev/rustypipe/commit/90540c6aaad658d4ce24ed41450d8509bac711bd))
|
||||
- Add http_client method to RustyPipe and user_agent method to RustyPipeQuery - ([3d6de53](https://codeberg.org/ThetaDev/rustypipe/commit/3d6de5354599ea691351e0ca161154e53f2e0b41))
|
||||
- Add channel_id and channel_name getters to YtEntity trait - ([bbbe9b4](https://codeberg.org/ThetaDev/rustypipe/commit/bbbe9b4b322c6b5b30764772e282c6823aeea524))
|
||||
- [**breaking**] Make StreamFilter use Vec internally, remove lifetime - ([821984b](https://codeberg.org/ThetaDev/rustypipe/commit/821984bbd51d65cf96b1d14087417ef968eaf9b2))
|
||||
- Overhauled downloader - ([11a0038](https://codeberg.org/ThetaDev/rustypipe/commit/11a00383502917cd98245c3da349107289ba3aa9))
|
||||
- Add player_from_clients function to specify client order - ([72b5dfe](https://codeberg.org/ThetaDev/rustypipe/commit/72b5dfec69ec25445b94cb0976662416a5df56ef))
|
||||
- [**breaking**] Add TV client - ([e608811](https://codeberg.org/ThetaDev/rustypipe/commit/e608811e5f5615416241e67561671330097092cb))
|
||||
- Downloader: add audio tagging - ([1e1315a](https://codeberg.org/ThetaDev/rustypipe/commit/1e1315a8378bd0ad25b5f1614e83dabc4a0b40d5))
|
||||
- Add audiotag+indicatif features to downloader - ([97fb057](https://codeberg.org/ThetaDev/rustypipe/commit/97fb0578b5c4954a596d8dee0c4b6e1d773a9300))
|
||||
- Add YtEntity trait to YouTubeItem and MusicItem - ([114a86a](https://codeberg.org/ThetaDev/rustypipe/commit/114a86a3823a175875aa2aeb31a61a6799ef13bc))
|
||||
- Change default player client order - ([97904d7](https://codeberg.org/ThetaDev/rustypipe/commit/97904d77374c2c937a49dc7905759c2d8e8ef9ae))
|
||||
- [**breaking**] Update channel model, addd handle + video_count, remove tv/mobile banner - ([e671570](https://codeberg.org/ThetaDev/rustypipe/commit/e6715700d950912031d5fbc1263f8770b6ffc49c))
|
||||
- [**breaking**] Add handle to ChannelItem, remove video_count - ([1cffb27](https://codeberg.org/ThetaDev/rustypipe/commit/1cffb27cc0b64929f9627f5839df2d73b81988a4))
|
||||
- [**breaking**] Remove startpage - ([3599aca](https://codeberg.org/ThetaDev/rustypipe/commit/3599acafef1a21fa6f8dea97902eb4a3fb048c14))
|
||||
|
||||
### 🐛 Bug Fixes
|
||||
|
||||
- Parse new comment model (A/B#14 frameworkUpdates) - ([b0331f7](https://code.thetadev.de/ThetaDev/rustypipe/commit/b0331f7250f5d7d61a45209150739d2cb08b4280))
|
||||
- [**breaking**] Extracting nsig function, remove field `throttled` from Video/Audio stream model - ([dd0565b](https://codeberg.org/ThetaDev/rustypipe/commit/dd0565ba98acb3289ed220fd2a3aaf86bb8b0788))
|
||||
- Make nsig_fn regex more generic - ([fb7af3b](https://codeberg.org/ThetaDev/rustypipe/commit/fb7af3b96698b452b6b24d1e094ba13a245cb83c))
|
||||
- Improve deobfuscator (support multiple nsig name matches, error if mapping all streams fails) - ([8152ce6](https://codeberg.org/ThetaDev/rustypipe/commit/8152ce6b088b57be9b8419b754aca93805e5f34d))
|
||||
- Nsig fn extraction - ([3c83e11](https://codeberg.org/ThetaDev/rustypipe/commit/3c83e11e753f8eb6efea5d453a7c819c487b3464))
|
||||
- Add var to deobf fn assignment - ([c6bd03f](https://codeberg.org/ThetaDev/rustypipe/commit/c6bd03fb70871ae1b764be18f88e86e71818fc56))
|
||||
- Make Verification enum exhaustive - ([d053ac3](https://codeberg.org/ThetaDev/rustypipe/commit/d053ac3eba810a7241df91f2f50bcbe1fd968c86))
|
||||
- Extraction error message - ([d36ba59](https://codeberg.org/ThetaDev/rustypipe/commit/d36ba595dab0bbaef1012ebfa8930fc0e6bf8167))
|
||||
- Set tracing instrumentation level to Error - ([9da3b25](https://codeberg.org/ThetaDev/rustypipe/commit/9da3b25be2b2577f7bd0282c09d10d368ac8b73f))
|
||||
- Detect ip-ban error message - ([da39c64](https://codeberg.org/ThetaDev/rustypipe/commit/da39c64f302bc2edc4214bbe25a0a9eb54063b09))
|
||||
- Player_from_clients: fall back to TvHtml5Embed client - ([d0ae796](https://codeberg.org/ThetaDev/rustypipe/commit/d0ae7961ba91d56c8b9a8d1c545875e869b818f5))
|
||||
- Parsing channels without banner - ([5a6b2c3](https://codeberg.org/ThetaDev/rustypipe/commit/5a6b2c3a621f6b20c1324ea8b9c03426e3d8018b))
|
||||
- Get TV client version - ([ee3ae40](https://codeberg.org/ThetaDev/rustypipe/commit/ee3ae40395263c5989784c7e00038ff13bc1151a))
|
||||
|
||||
### ⚙️ Miscellaneous Tasks
|
||||
|
||||
- Renovate: disable approveMajorUpdates - ([4743f9d](https://codeberg.org/ThetaDev/rustypipe/commit/4743f9d8e101b58ad6a43548495da9f4f381b9f4))
|
||||
- Renovate: disable scheduleDaily - ([015bd6f](https://codeberg.org/ThetaDev/rustypipe/commit/015bd6fcbf04163565fcb190b163ecfdb5664e11))
|
||||
- Renovate: enable automerge - ([882abc5](https://codeberg.org/ThetaDev/rustypipe/commit/882abc53ca894229ee78ec0edaa723d9ea61bbcb))
|
||||
- *(deps)* Update rust crate quick-xml to 0.36.0 (#8) - ([b6bc05c](https://codeberg.org/ThetaDev/rustypipe/commit/b6bc05c1f39da9a846b2e3d1d24bcbccb031203b))
|
||||
- *(deps)* Update rust crate rstest to 0.22.0 (#9) - ([abb7832](https://codeberg.org/ThetaDev/rustypipe/commit/abb783219aba4b492c1dff03c2148acf1f51a55d))
|
||||
- Change repo URL to Codeberg - ([1793331](https://codeberg.org/ThetaDev/rustypipe/commit/17933315d947f76d5fe1aa52abf7ea24c3ce6381))
|
||||
- Adjust dependency versions - ([70c6f8c](https://codeberg.org/ThetaDev/rustypipe/commit/70c6f8c3b97baefd316fff90cc727524516657af))
|
||||
|
||||
### Todo
|
||||
|
||||
- Update metadata - ([8692ca8](https://codeberg.org/ThetaDev/rustypipe/commit/8692ca81d972d0d2acf6fb4da79b9e0f5ebf4daf))
|
||||
|
||||
|
||||
## [v0.2.1](https://codeberg.org/ThetaDev/rustypipe/compare/rustypipe/v0.2.0..rustypipe/v0.2.1) - 2024-07-01
|
||||
|
||||
### 🐛 Bug Fixes
|
||||
|
||||
- *(deps)* Update quick-xml to v0.35.0 - ([298e4de](https://codeberg.org/ThetaDev/rustypipe/commit/298e4def93d1595fba91be103f014aa645a08937))
|
||||
|
||||
|
||||
## [v0.2.0](https://codeberg.org/ThetaDev/rustypipe/compare/rustypipe/v0.1.3..rustypipe/v0.2.0) - 2024-06-27
|
||||
|
||||
### 🚀 Features
|
||||
|
||||
- Add text formatting (bold/italic/strikethrough) - ([b8825f9](https://codeberg.org/ThetaDev/rustypipe/commit/b8825f9199365c873a4f0edd98a435e986b8daa2))
|
||||
- Prefix chip-style web links (social media) with the service name - ([6c41ef2](https://codeberg.org/ThetaDev/rustypipe/commit/6c41ef2fb2531e10a12c271e2d48504510a3b0bf))
|
||||
- Make get_visitor_data() public - ([da1d1bd](https://codeberg.org/ThetaDev/rustypipe/commit/da1d1bd2a0b214da10436ae221c90a0f88697b9a))
|
||||
- Add UnavailabilityReason: IpBan - ([401d4e8](https://codeberg.org/ThetaDev/rustypipe/commit/401d4e8255b1e86444319fed6d114dfbd0f80bbd))
|
||||
- Add YtEntity trait - ([792e3b3](https://codeberg.org/ThetaDev/rustypipe/commit/792e3b31e0101087a167935baad39a2e3b4296d0))
|
||||
|
||||
### 🐛 Bug Fixes
|
||||
|
||||
- Remove Innertube API keys, update android player params - ([a8fb337](https://codeberg.org/ThetaDev/rustypipe/commit/a8fb337fae9cb0112e0152f9a0a19ebae49c2a4d))
|
||||
- Parsing error when no `music_related` content available - ([8fbd6b9](https://codeberg.org/ThetaDev/rustypipe/commit/8fbd6b95b6f01108b46f53fe60a56b0c561e40c1))
|
||||
- Parsing audiobook type in European Portuguese - ([041ce2d](https://codeberg.org/ThetaDev/rustypipe/commit/041ce2d08f6021c88e8890034f551f7e01b2f012))
|
||||
- Renovate ci token - ([e0759eb](https://codeberg.org/ThetaDev/rustypipe/commit/e0759ebce32a5520245bb2c0cb920734b04ee7dc))
|
||||
|
||||
### 🚜 Refactor
|
||||
|
||||
- [**breaking**] Rename VideoItem/VideoPlayerDetails.length to duration for consistency - ([94e8d24](https://codeberg.org/ThetaDev/rustypipe/commit/94e8d24c6848b8bfca70dd03a7d89547ba9d6051))
|
||||
|
||||
### 📚 Documentation
|
||||
|
||||
- Add logo - ([6646078](https://codeberg.org/ThetaDev/rustypipe/commit/66460789449be0d5984cbdb6ec372e69323b7a88))
|
||||
|
||||
### ⚙️ Miscellaneous Tasks
|
||||
|
||||
- Changelog: fix incorrect version URLs - ([97b6f07](https://codeberg.org/ThetaDev/rustypipe/commit/97b6f07399e80e00a6c015d013e744568be125dd))
|
||||
- Update rstest to v0.19.0 - ([50fd1f0](https://codeberg.org/ThetaDev/rustypipe/commit/50fd1f08caf39c1298654e06059cc393543e925b))
|
||||
- Introduce MSRV - ([5dbb288](https://codeberg.org/ThetaDev/rustypipe/commit/5dbb288a496d53a299effa2026f5258af7b1f176))
|
||||
- Fix clippy lints - ([45b9f2a](https://codeberg.org/ThetaDev/rustypipe/commit/45b9f2a627b4e7075ba0b1c5f16efcc19aef7922))
|
||||
- Vscode: enable rss feature by default - ([e75ffbb](https://codeberg.org/ThetaDev/rustypipe/commit/e75ffbb5da6198086385ea96383ab9d0791592a5))
|
||||
- Configure Renovate (#3) - ([44c2deb](https://codeberg.org/ThetaDev/rustypipe/commit/44c2debea61f70c24ad6d827987e85e2132ed3d1))
|
||||
- *(deps)* Update rust crate tokio to 1.20.4 [security] (#4) - ([ce3ec34](https://codeberg.org/ThetaDev/rustypipe/commit/ce3ec34337b8acac41410ea39264aab7423d5801))
|
||||
- *(deps)* Update rust crate quick-xml to 0.34.0 (#5) - ([1e8a1af](https://codeberg.org/ThetaDev/rustypipe/commit/1e8a1af08c873cee7feadf63c2eff62753a78f64))
|
||||
- *(deps)* Update rust crate rstest to 0.21.0 (#7) - ([c3af918](https://codeberg.org/ThetaDev/rustypipe/commit/c3af918ba53c6230c0e4aef822a0cb2cf120bf3f))
|
||||
|
||||
## [v0.1.3](https://codeberg.org/ThetaDev/rustypipe/compare/rustypipe/v0.1.2..rustypipe/v0.1.3) - 2024-04-01
|
||||
|
||||
### 🐛 Bug Fixes
|
||||
|
||||
- Parse new comment model (A/B#14 frameworkUpdates) - ([b0331f7](https://codeberg.org/ThetaDev/rustypipe/commit/b0331f7250f5d7d61a45209150739d2cb08b4280))
|
||||
|
||||
### ◀️ Revert
|
||||
|
||||
- "fix: improve VecLogErr messages" (leads to infinite loop) - ([348c852](https://code.thetadev.de/ThetaDev/rustypipe/commit/348c8523fe847f2f6ce98317375a7ab65e778ed2))
|
||||
- "fix: improve VecLogErr messages" (leads to infinite loop) - ([348c852](https://codeberg.org/ThetaDev/rustypipe/commit/348c8523fe847f2f6ce98317375a7ab65e778ed2))
|
||||
|
||||
|
||||
## [v0.1.2](https://code.thetadev.de/ThetaDev/rustypipe/compare/rustypipe/v0.1.1..rustypipe/v0.1.2) - 2024-03-26
|
||||
## [v0.1.2](https://codeberg.org/ThetaDev/rustypipe/compare/rustypipe/v0.1.1..rustypipe/v0.1.2) - 2024-03-26
|
||||
|
||||
### 🐛 Bug Fixes
|
||||
|
||||
- Correctly parse subscriber count with new channel header - ([180dd98](https://code.thetadev.de/ThetaDev/rustypipe/commit/180dd9891a14b4da9f130a73d73aecc3822fce2f))
|
||||
- Correctly parse subscriber count with new channel header - ([180dd98](https://codeberg.org/ThetaDev/rustypipe/commit/180dd9891a14b4da9f130a73d73aecc3822fce2f))
|
||||
|
||||
|
||||
## [v0.1.1](https://code.thetadev.de/ThetaDev/rustypipe/compare/rustypipe/v0.1.0..rustypipe/v0.1.1) - 2024-03-26
|
||||
## [v0.1.1](https://codeberg.org/ThetaDev/rustypipe/compare/rustypipe/v0.1.0..rustypipe/v0.1.1) - 2024-03-26
|
||||
|
||||
### 🐛 Bug Fixes
|
||||
|
||||
- Specify internal dependency versions - ([6598a23](https://code.thetadev.de/ThetaDev/rustypipe/commit/6598a23d0699e6fe298275a67e0146a19c422c88))
|
||||
- Move package attributes to workspace - ([e4b204e](https://code.thetadev.de/ThetaDev/rustypipe/commit/e4b204eae65f450471be0890b0198d2f30714b3b))
|
||||
- Parsing music details with video description tab - ([a81c3e8](https://code.thetadev.de/ThetaDev/rustypipe/commit/a81c3e83366fdf72d01dd3ee00fb2e831f7aaa26))
|
||||
- Specify internal dependency versions - ([6598a23](https://codeberg.org/ThetaDev/rustypipe/commit/6598a23d0699e6fe298275a67e0146a19c422c88))
|
||||
- Move package attributes to workspace - ([e4b204e](https://codeberg.org/ThetaDev/rustypipe/commit/e4b204eae65f450471be0890b0198d2f30714b3b))
|
||||
- Parsing music details with video description tab - ([a81c3e8](https://codeberg.org/ThetaDev/rustypipe/commit/a81c3e83366fdf72d01dd3ee00fb2e831f7aaa26))
|
||||
|
||||
### ⚙️ Miscellaneous Tasks
|
||||
|
||||
- Changes to release command - ([0bcced1](https://code.thetadev.de/ThetaDev/rustypipe/commit/0bcced1db377198a54c9c7d03b8d038125a2bfe4))
|
||||
- Update user agent (FF 115.0) - ([be314d5](https://code.thetadev.de/ThetaDev/rustypipe/commit/be314d57ea1d99bfdc80649351ee3e7845541238))
|
||||
- Fix release script (unquoted include paths) - ([78ba9cb](https://code.thetadev.de/ThetaDev/rustypipe/commit/78ba9cb34c6bba3aba177583b242d3f76ea9847d))
|
||||
- Changes to release command - ([0bcced1](https://codeberg.org/ThetaDev/rustypipe/commit/0bcced1db377198a54c9c7d03b8d038125a2bfe4))
|
||||
- Update user agent (FF 115.0) - ([be314d5](https://codeberg.org/ThetaDev/rustypipe/commit/be314d57ea1d99bfdc80649351ee3e7845541238))
|
||||
- Fix release script (unquoted include paths) - ([78ba9cb](https://codeberg.org/ThetaDev/rustypipe/commit/78ba9cb34c6bba3aba177583b242d3f76ea9847d))
|
||||
|
||||
|
||||
## [v0.1.0](https://code.thetadev.de/ThetaDev/rustypipe/commits/tag/rustypipe/v0.1.0) - 2024-03-22
|
||||
## [v0.1.0](https://codeberg.org/ThetaDev/rustypipe/commits/tag/rustypipe/v0.1.0) - 2024-03-22
|
||||
|
||||
Initial release
|
||||
|
||||
|
|
51
Cargo.toml
51
Cargo.toml
|
@ -1,6 +1,6 @@
|
|||
[package]
|
||||
name = "rustypipe"
|
||||
version = "0.2.0"
|
||||
version = "0.4.0"
|
||||
rust-version = "1.67.1"
|
||||
edition.workspace = true
|
||||
authors.workspace = true
|
||||
|
@ -10,7 +10,7 @@ keywords.workspace = true
|
|||
categories.workspace = true
|
||||
description = "Client for the public YouTube / YouTube Music API (Innertube), inspired by NewPipe"
|
||||
|
||||
include = ["/src", "README.md", "LICENSE", "!snapshots"]
|
||||
include = ["/src", "README.md", "CHANGELOG.md", "LICENSE", "!snapshots"]
|
||||
|
||||
[workspace]
|
||||
members = [".", "codegen", "downloader", "cli"]
|
||||
|
@ -19,7 +19,7 @@ members = [".", "codegen", "downloader", "cli"]
|
|||
edition = "2021"
|
||||
authors = ["ThetaDev <thetadev@magenta.de>"]
|
||||
license = "GPL-3.0"
|
||||
repository = "https://code.thetadev.de/ThetaDev/rustypipe"
|
||||
repository = "https://codeberg.org/ThetaDev/rustypipe"
|
||||
keywords = ["youtube", "video", "music"]
|
||||
categories = ["api-bindings", "multimedia"]
|
||||
|
||||
|
@ -30,8 +30,8 @@ quick-js-dtp = { version = "0.4.1", default-features = false, features = [
|
|||
once_cell = "1.12.0"
|
||||
regex = "1.6.0"
|
||||
fancy-regex = "0.13.0"
|
||||
thiserror = "1.0.36"
|
||||
url = "2.2.2"
|
||||
thiserror = "1.0.0"
|
||||
url = "2.2.0"
|
||||
reqwest = { version = "0.12.0", default-features = false }
|
||||
tokio = "1.20.4"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
|
@ -40,40 +40,44 @@ serde_with = { version = "3.0.0", default-features = false, features = [
|
|||
"alloc",
|
||||
"macros",
|
||||
] }
|
||||
serde_plain = "1.0.1"
|
||||
rand = "0.8.5"
|
||||
time = { version = "0.3.15", features = [
|
||||
serde_plain = "1.0.0"
|
||||
rand = "0.8.0"
|
||||
time = { version = "0.3.10", features = [
|
||||
"macros",
|
||||
"serde-human-readable",
|
||||
"serde-well-known",
|
||||
] }
|
||||
futures = "0.3.21"
|
||||
ress = "0.11.4"
|
||||
phf = "0.11.1"
|
||||
phf_codegen = "0.11.1"
|
||||
ress = "0.11.0"
|
||||
phf = "0.11.0"
|
||||
phf_codegen = "0.11.0"
|
||||
base64 = "0.22.0"
|
||||
urlencoding = "2.1.2"
|
||||
quick-xml = { version = "0.34.0", features = ["serialize"] }
|
||||
tracing = { version = "0.1.37", features = ["log"] }
|
||||
urlencoding = "2.1.0"
|
||||
quick-xml = { version = "0.36.0", features = ["serialize"] }
|
||||
tracing = { version = "0.1.0", features = ["log"] }
|
||||
|
||||
# CLI
|
||||
indicatif = "0.17.0"
|
||||
anyhow = "1.0"
|
||||
clap = { version = "4.0.29", features = ["derive"] }
|
||||
tracing-subscriber = "0.3.17"
|
||||
serde_yaml = "0.9.19"
|
||||
clap = { version = "4.0.0", features = ["derive"] }
|
||||
tracing-subscriber = { version = "0.3.17", features = ["env-filter"] }
|
||||
serde_yaml = "0.9.0"
|
||||
dirs = "5.0.0"
|
||||
filenamify = "0.1.0"
|
||||
|
||||
# Testing
|
||||
rstest = "0.21.0"
|
||||
rstest = "0.22.0"
|
||||
tokio-test = "0.4.2"
|
||||
insta = { version = "1.17.1", features = ["ron", "redactions"] }
|
||||
path_macro = "1.0.0"
|
||||
tracing-test = "0.2.5"
|
||||
|
||||
# Included crates
|
||||
rustypipe = { path = ".", version = "0.2.0", default-features = false }
|
||||
rustypipe-downloader = { path = "./downloader", version = "0.1.0", default-features = false }
|
||||
rustypipe = { path = ".", version = "0.4.0", default-features = false }
|
||||
rustypipe-downloader = { path = "./downloader", version = "0.2.1", default-features = false, features = [
|
||||
"indicatif",
|
||||
"audiotag",
|
||||
] }
|
||||
|
||||
[features]
|
||||
default = ["default-tls"]
|
||||
|
@ -115,3 +119,10 @@ rstest.workspace = true
|
|||
tokio-test.workspace = true
|
||||
insta.workspace = true
|
||||
path_macro.workspace = true
|
||||
tracing-test.workspace = true
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
# To build locally:
|
||||
# RUSTDOCFLAGS="--cfg docsrs" cargo +nightly doc --features rss --no-deps --open
|
||||
features = ["rss"]
|
||||
rustdoc-args = ["--cfg", "docsrs"]
|
||||
|
|
2
Justfile
2
Justfile
|
@ -1,6 +1,6 @@
|
|||
test:
|
||||
# cargo test --features=rss
|
||||
cargo nextest run --features=rss --no-fail-fast --failure-output final --retries 1
|
||||
cargo nextest run --workspace --features=rss --no-fail-fast --failure-output final --retries 1
|
||||
|
||||
unittest:
|
||||
cargo nextest run --features=rss --no-fail-fast --failure-output final --lib
|
||||
|
|
10
README.md
10
README.md
|
@ -1,7 +1,11 @@
|
|||
# ![RustyPipe](https://code.thetadev.de/ThetaDev/rustypipe/raw/branch/main/notes/logo.svg)
|
||||
# ![RustyPipe](https://codeberg.org/ThetaDev/rustypipe/raw/branch/main/notes/logo.svg)
|
||||
|
||||
Rust client for the public YouTube / YouTube Music API (Innertube), inspired by
|
||||
[NewPipe](https://github.com/TeamNewPipe/NewPipeExtractor).
|
||||
[![Current crates.io version](https://img.shields.io/crates/v/rustypipe.svg)](https://crates.io/crates/rustypipe)
|
||||
[![License](https://img.shields.io/badge/License-GPL--3-blue.svg?style=flat)](http://opensource.org/licenses/GPL-3.0)
|
||||
[![CI status](https://codeberg.org/ThetaDev/rustypipe/actions/workflows/ci.yaml/badge.svg?style=flat&label=CI)](https://codeberg.org/ThetaDev/rustypipe/actions/?workflow=ci.yaml)
|
||||
|
||||
RustyPipe is a fully featured Rust client for the public YouTube / YouTube Music API
|
||||
(Innertube), inspired by [NewPipe](https://github.com/TeamNewPipe/NewPipeExtractor).
|
||||
|
||||
## Features
|
||||
|
||||
|
|
|
@ -2,7 +2,88 @@
|
|||
|
||||
All notable changes to this project will be documented in this file.
|
||||
|
||||
## [v0.1.0](https://code.thetadev.de/ThetaDev/rustypipe/commits/tag/rustypipe-cli/v0.1.0) - 2024-03-22
|
||||
|
||||
## [v0.2.1](https://codeberg.org/ThetaDev/rustypipe/compare/rustypipe-cli/v0.2.0..rustypipe-cli/v0.2.1) - 2024-09-10
|
||||
|
||||
### 🚀 Features
|
||||
|
||||
- Add RustyPipe version constant - ([7a019f5](https://codeberg.org/ThetaDev/rustypipe/commit/7a019f5706e19f7fe9f2e16e3b94d7b98cc8aca9))
|
||||
|
||||
### 📚 Documentation
|
||||
|
||||
- Fix license badge URL - ([4a253e1](https://codeberg.org/ThetaDev/rustypipe/commit/4a253e1a47317e9999e6ad31ac5c411956a0986a))
|
||||
|
||||
### ⚙️ Miscellaneous Tasks
|
||||
|
||||
- *(deps)* Update rust crate tokio to 1.20.4 [security] (#10) - ([a445e51](https://codeberg.org/ThetaDev/rustypipe/commit/a445e51b54a9afc44cd9657260a0b3d2abddbfa6))
|
||||
|
||||
|
||||
## [v0.2.0](https://codeberg.org/ThetaDev/rustypipe/compare/rustypipe-cli/v0.1.1..rustypipe-cli/v0.2.0) - 2024-08-18
|
||||
|
||||
### 🚀 Features
|
||||
|
||||
- Overhauled downloader - ([11a0038](https://codeberg.org/ThetaDev/rustypipe/commit/11a00383502917cd98245c3da349107289ba3aa9))
|
||||
- [**breaking**] Add TV client - ([e608811](https://codeberg.org/ThetaDev/rustypipe/commit/e608811e5f5615416241e67561671330097092cb))
|
||||
- Downloader: add audio tagging - ([1e1315a](https://codeberg.org/ThetaDev/rustypipe/commit/1e1315a8378bd0ad25b5f1614e83dabc4a0b40d5))
|
||||
- Downloader: add download_track fn, improve path templates - ([e1e4fb2](https://codeberg.org/ThetaDev/rustypipe/commit/e1e4fb29c190fec07f17c59ec88bef4f1c2a76a1))
|
||||
- Add audiotag+indicatif features to downloader - ([97fb057](https://codeberg.org/ThetaDev/rustypipe/commit/97fb0578b5c4954a596d8dee0c4b6e1d773a9300))
|
||||
- Add plaintext output to CLI - ([91b020e](https://codeberg.org/ThetaDev/rustypipe/commit/91b020efd498eff6e0f354a1de39439e252a79dd))
|
||||
- Add potoken option to downloader - ([904f821](https://codeberg.org/ThetaDev/rustypipe/commit/904f8215d84c810b04e4d2134718e786a4803ad2))
|
||||
- Print error message - ([8f16e5b](https://codeberg.org/ThetaDev/rustypipe/commit/8f16e5ba6eec3fd6aba1bb6a19571c65fb69ce0e))
|
||||
- Add list of clients to downloader - ([5e646af](https://codeberg.org/ThetaDev/rustypipe/commit/5e646afd1edc6c0101501311527ea56d3bad5fd2))
|
||||
- Retry with different client after 403 error - ([d875b54](https://codeberg.org/ThetaDev/rustypipe/commit/d875b5442de9822ba7ddc6f05789f56a8962808c))
|
||||
- Add option to fetch RSS feed - ([03c4d3c](https://codeberg.org/ThetaDev/rustypipe/commit/03c4d3c392386e06f2673f0e0783e22d10087989))
|
||||
- [**breaking**] Update channel model, addd handle + video_count, remove tv/mobile banner - ([e671570](https://codeberg.org/ThetaDev/rustypipe/commit/e6715700d950912031d5fbc1263f8770b6ffc49c))
|
||||
|
||||
### 🐛 Bug Fixes
|
||||
|
||||
- *(deps)* Update quick-xml to v0.35.0 - ([298e4de](https://codeberg.org/ThetaDev/rustypipe/commit/298e4def93d1595fba91be103f014aa645a08937))
|
||||
- Improve deobfuscator (support multiple nsig name matches, error if mapping all streams fails) - ([8152ce6](https://codeberg.org/ThetaDev/rustypipe/commit/8152ce6b088b57be9b8419b754aca93805e5f34d))
|
||||
- Cli: print video ID when logging errors - ([2c7a3fb](https://codeberg.org/ThetaDev/rustypipe/commit/2c7a3fb5cc153ff0b8b5e79234ae497d916e471c))
|
||||
- Use anstream + owo-color for colorful CLI output - ([e8324cf](https://codeberg.org/ThetaDev/rustypipe/commit/e8324cf3b065cb977adbc9529b1ef5ee18c3dd47))
|
||||
- Use native tls by default for CLI - ([f37432a](https://codeberg.org/ThetaDev/rustypipe/commit/f37432a48c1f93cab5f7942f791daf7b27cb1565))
|
||||
- Detect ip-ban error message - ([da39c64](https://codeberg.org/ThetaDev/rustypipe/commit/da39c64f302bc2edc4214bbe25a0a9eb54063b09))
|
||||
- Dont store cache in current dir with --report option - ([6009de7](https://codeberg.org/ThetaDev/rustypipe/commit/6009de7bddc6031f2af17005c473c17934327c02))
|
||||
- Show docs.rs feature flags - ([67a231d](https://codeberg.org/ThetaDev/rustypipe/commit/67a231d6d1b6427f500667729a59032f2b28cc65))
|
||||
|
||||
### ⚙️ Miscellaneous Tasks
|
||||
|
||||
- *(deps)* Update rust crate quick-xml to 0.36.0 (#8) - ([b6bc05c](https://codeberg.org/ThetaDev/rustypipe/commit/b6bc05c1f39da9a846b2e3d1d24bcbccb031203b))
|
||||
- *(deps)* Update rust crate rstest to 0.22.0 (#9) - ([abb7832](https://codeberg.org/ThetaDev/rustypipe/commit/abb783219aba4b492c1dff03c2148acf1f51a55d))
|
||||
- Change repo URL to Codeberg - ([1793331](https://codeberg.org/ThetaDev/rustypipe/commit/17933315d947f76d5fe1aa52abf7ea24c3ce6381))
|
||||
- Adjust dependency versions - ([70c6f8c](https://codeberg.org/ThetaDev/rustypipe/commit/70c6f8c3b97baefd316fff90cc727524516657af))
|
||||
|
||||
### Todo
|
||||
|
||||
- Update metadata - ([8692ca8](https://codeberg.org/ThetaDev/rustypipe/commit/8692ca81d972d0d2acf6fb4da79b9e0f5ebf4daf))
|
||||
|
||||
|
||||
## [v0.1.1](https://codeberg.org/ThetaDev/rustypipe/compare/rustypipe-cli/v0.1.0..rustypipe-cli/v0.1.1) - 2024-06-27
|
||||
|
||||
### 🚀 Features
|
||||
|
||||
- CLI: setting player type - ([16e0e28](https://codeberg.org/ThetaDev/rustypipe/commit/16e0e28c4866bb69d8e4c06eef94176f329a1c27))
|
||||
|
||||
### 🐛 Bug Fixes
|
||||
|
||||
- Clippy warning - ([8420c2f](https://codeberg.org/ThetaDev/rustypipe/commit/8420c2f8dbd2791b524ceca2e19fb68e5b918bfa))
|
||||
|
||||
### 📚 Documentation
|
||||
|
||||
- Add logo - ([6646078](https://codeberg.org/ThetaDev/rustypipe/commit/66460789449be0d5984cbdb6ec372e69323b7a88))
|
||||
|
||||
### ⚙️ Miscellaneous Tasks
|
||||
|
||||
- Changelog: fix incorrect version URLs - ([97b6f07](https://codeberg.org/ThetaDev/rustypipe/commit/97b6f07399e80e00a6c015d013e744568be125dd))
|
||||
- Update rstest to v0.19.0 - ([50fd1f0](https://codeberg.org/ThetaDev/rustypipe/commit/50fd1f08caf39c1298654e06059cc393543e925b))
|
||||
- Introduce MSRV - ([5dbb288](https://codeberg.org/ThetaDev/rustypipe/commit/5dbb288a496d53a299effa2026f5258af7b1f176))
|
||||
- Fix clippy lints - ([45b9f2a](https://codeberg.org/ThetaDev/rustypipe/commit/45b9f2a627b4e7075ba0b1c5f16efcc19aef7922))
|
||||
- *(deps)* Update rust crate tokio to 1.20.4 [security] (#4) - ([ce3ec34](https://codeberg.org/ThetaDev/rustypipe/commit/ce3ec34337b8acac41410ea39264aab7423d5801))
|
||||
- *(deps)* Update rust crate quick-xml to 0.34.0 (#5) - ([1e8a1af](https://codeberg.org/ThetaDev/rustypipe/commit/1e8a1af08c873cee7feadf63c2eff62753a78f64))
|
||||
- *(deps)* Update rust crate rstest to 0.21.0 (#7) - ([c3af918](https://codeberg.org/ThetaDev/rustypipe/commit/c3af918ba53c6230c0e4aef822a0cb2cf120bf3f))
|
||||
- Update rustypipe to 0.2.0
|
||||
|
||||
## [v0.1.0](https://codeberg.org/ThetaDev/rustypipe/commits/tag/rustypipe-cli/v0.1.0) - 2024-03-22
|
||||
|
||||
Initial release
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
[package]
|
||||
name = "rustypipe-cli"
|
||||
version = "0.1.0"
|
||||
version = "0.2.1"
|
||||
rust-version = "1.70.0"
|
||||
edition.workspace = true
|
||||
authors.workspace = true
|
||||
|
@ -11,7 +11,7 @@ categories.workspace = true
|
|||
description = "CLI for RustyPipe - download videos and extract data from YouTube / YouTube Music"
|
||||
|
||||
[features]
|
||||
default = ["rustls-tls-native-roots"]
|
||||
default = ["native-tls"]
|
||||
|
||||
# Reqwest TLS options
|
||||
native-tls = [
|
||||
|
@ -41,7 +41,7 @@ rustls-tls-native-roots = [
|
|||
]
|
||||
|
||||
[dependencies]
|
||||
rustypipe.workspace = true
|
||||
rustypipe = { workspace = true, features = ["rss"] }
|
||||
rustypipe-downloader.workspace = true
|
||||
reqwest.workspace = true
|
||||
tokio = { workspace = true, features = ["macros", "rt-multi-thread"] }
|
||||
|
@ -52,6 +52,11 @@ serde_json.workspace = true
|
|||
indicatif.workspace = true
|
||||
anyhow.workspace = true
|
||||
clap.workspace = true
|
||||
tracing.workspace = true
|
||||
tracing-subscriber.workspace = true
|
||||
serde_yaml.workspace = true
|
||||
dirs.workspace = true
|
||||
|
||||
anstream = "0.6.15"
|
||||
owo-colors = "4.0.0"
|
||||
const_format = "0.2.33"
|
||||
|
|
94
cli/README.md
Normal file
94
cli/README.md
Normal file
|
@ -0,0 +1,94 @@
|
|||
# ![RustyPipe](https://codeberg.org/ThetaDev/rustypipe/raw/branch/main/notes/logo.svg) CLI
|
||||
|
||||
[![Current crates.io version](https://img.shields.io/crates/v/rustypipe-cli.svg)](https://crates.io/crates/rustypipe-cli)
|
||||
[![License](https://img.shields.io/badge/License-GPL--3-blue.svg?style=flat)](http://opensource.org/licenses/GPL-3.0)
|
||||
[![CI status](https://codeberg.org/ThetaDev/rustypipe/actions/workflows/ci.yaml/badge.svg?style=flat&label=CI)](https://codeberg.org/ThetaDev/rustypipe/actions/?workflow=ci.yaml)
|
||||
|
||||
The RustyPipe CLI is a powerful YouTube client for the command line. It allows you to
|
||||
access most of the features of the RustyPipe crate: getting data from YouTube and
|
||||
downloading videos.
|
||||
|
||||
The following subcommands are included:
|
||||
|
||||
## `get`: Fetch information
|
||||
|
||||
You can call the get command with any YouTube entity ID or URL and RustyPipe will fetch
|
||||
the associated metadata. It can fetch channels, playlists, albums and videos.
|
||||
|
||||
**Usage:** `rustypipe get UC2TXq_t06Hjdr2g_KdKpHQg`
|
||||
|
||||
- `-l`, `--limit` Limit the number of list items to fetch
|
||||
- ``-t, --tab` Channel tab (options: **videos**, shorts, live, playlists, info)
|
||||
- `-m, --music` Use the YouTube Music API
|
||||
- `--rss`Fetch the RSS feed of a channel
|
||||
- `--comments` Get comments (options: top, latest)
|
||||
- `--lyrics` Get the lyrics for YTM tracks
|
||||
- `--player` Get the player data instead of the video details when fetching videos
|
||||
- `-c, --client-type` YT clients used to fetch player data (options: desktop, tv,
|
||||
tv-embed, android, ios; if multiple clients are specified, they are attempted in
|
||||
order)
|
||||
|
||||
## `search`: Search YouTube
|
||||
|
||||
With the search command you can search the entire YouTube platform or individual
|
||||
channels. YouTube Music search is also supported.
|
||||
|
||||
Note that search filters are only supported when searching YouTube. They have no effect
|
||||
when searching YTM or individual channels.
|
||||
|
||||
**Usage:** `rustypipe search "query"`
|
||||
|
||||
### Options
|
||||
|
||||
- `-l`, `--limit` Limit the number of list items to fetch
|
||||
|
||||
- `--item-type` Filter results by item type
|
||||
- `--length` Filter results by video length
|
||||
- `--date` Filter results by upload date (options: hour, day, week, month, year)
|
||||
- `--order` Sort search results (options: rating, date, views)
|
||||
- `--channel` Channel ID for searching channel videos
|
||||
- `-m, --music` Search YouTube Music in the given category (options: all, tracks,
|
||||
videos, artists, albums, playlists-ytm, playlists-community)
|
||||
|
||||
## `dl`: Download videos
|
||||
|
||||
The downloader can download individual videos, playlists, albums and channels. Multiple
|
||||
videos can be downloaded in parallel for improved performance.
|
||||
|
||||
**Usage:** `rustypipe dl eRsGyueVLvQ`
|
||||
|
||||
### Options
|
||||
|
||||
- `-o`, `--output` Download to the given directory
|
||||
- `--output-file` Download to the given file
|
||||
- `--template` Download to a path determined by a template
|
||||
|
||||
- `-r`, `--resolution` Video resolution (e.g. 720, 1080). Set to 0 for audio-only
|
||||
- `-a`, `--audio` Download only the audio track and write track metadata + album cover
|
||||
- `-p`, `--parallel` Number of videos downloaded in parallel (default: 8)
|
||||
- `-m, --music` Use YouTube Music for downloading playlists
|
||||
- `-l`, `--limit` Limit the number of videos to download (default: 1000)
|
||||
- `-c`, `--client-type` YT clients used to fetch player data (options: desktop, tv,
|
||||
tv-embed, android, ios; if multiple clients are specified, they are attempted in
|
||||
order)
|
||||
- `--pot` token to circumvent bot detection
|
||||
|
||||
## `vdata`: Get visitor data
|
||||
|
||||
You can use the vdata command to get a new visitor data cookie. This feature may come in
|
||||
handy for testing and reproducing A/B tests.
|
||||
|
||||
## Global options
|
||||
|
||||
- **Proxy:** RustyPipe respects the environment variables `HTTP_PROXY`, `HTTPS_PROXY`
|
||||
and `ALL_PROXY`
|
||||
- **Logging:** You can change the log level with the `RUST_LOG` environment variable, it
|
||||
is set to `info` by default
|
||||
- **Visitor data:** A custom visitor data cookie can be used with the `--vdata` flag
|
||||
- `--report`
|
||||
|
||||
### Output format
|
||||
|
||||
By default, the CLI outputs YouTube data in a human-readable text format. If you want to
|
||||
store the data or process it with a script, you should choose a machine readable output
|
||||
format. You can choose both JSON and YAML with the `-f, --format` flag.
|
1107
cli/src/main.rs
1107
cli/src/main.rs
File diff suppressed because it is too large
Load diff
|
@ -14,7 +14,7 @@ All notable changes to this project will be documented in this file.\n
|
|||
# template for the changelog body
|
||||
# https://keats.github.io/tera/docs/#introduction
|
||||
body = """
|
||||
{% set repo_url = "https://code.thetadev.de/ThetaDev/rustypipe" %}\
|
||||
{% set repo_url = "https://codeberg.org/ThetaDev/rustypipe" %}\
|
||||
{% if version %}\
|
||||
{%set vname = version | split(pat="/") | last %}
|
||||
{%if previous.version %}\
|
||||
|
|
|
@ -34,6 +34,7 @@ pub enum ABTest {
|
|||
ChannelPageHeader = 12,
|
||||
MusicPlaylistTwoColumn = 13,
|
||||
CommentsFrameworkUpdate = 14,
|
||||
ChannelShortsLockup = 15,
|
||||
}
|
||||
|
||||
/// List of active A/B tests that are run when none is manually specified
|
||||
|
@ -110,6 +111,7 @@ pub async fn run_test(
|
|||
ABTest::ChannelPageHeader => channel_page_header(&query).await,
|
||||
ABTest::MusicPlaylistTwoColumn => music_playlist_two_column(&query).await,
|
||||
ABTest::CommentsFrameworkUpdate => comments_framework_update(&query).await,
|
||||
ABTest::ChannelShortsLockup => channel_shorts_lockup(&query).await,
|
||||
}
|
||||
.unwrap();
|
||||
pb.inc(1);
|
||||
|
@ -179,7 +181,7 @@ pub async fn channel_handles_in_search_results(rp: &RustyPipeQuery) -> Result<bo
|
|||
Ok(search.items.items.iter().any(|itm| match itm {
|
||||
YouTubeItem::Channel(channel) => channel
|
||||
.subscriber_count
|
||||
.map(|sc| sc > 100 && channel.video_count.is_none())
|
||||
.map(|sc| sc > 100 && channel.handle.is_some())
|
||||
.unwrap_or_default(),
|
||||
_ => false,
|
||||
}))
|
||||
|
@ -327,7 +329,7 @@ pub async fn channel_page_header(rp: &RustyPipeQuery) -> Result<bool> {
|
|||
let channel = rp
|
||||
.channel_videos_tab("UCh8gHdtzO2tXd593_bjErWg", ChannelVideoTab::Shorts)
|
||||
.await?;
|
||||
Ok(channel.mobile_banner.is_empty() && channel.tv_banner.is_empty())
|
||||
Ok(channel.video_count.is_some())
|
||||
}
|
||||
|
||||
pub async fn music_playlist_two_column(rp: &RustyPipeQuery) -> Result<bool> {
|
||||
|
@ -363,3 +365,20 @@ pub async fn comments_framework_update(rp: &RustyPipeQuery) -> Result<bool> {
|
|||
.unwrap();
|
||||
Ok(res.contains("\"frameworkUpdates\""))
|
||||
}
|
||||
|
||||
pub async fn channel_shorts_lockup(rp: &RustyPipeQuery) -> Result<bool> {
|
||||
let id = "UCh8gHdtzO2tXd593_bjErWg";
|
||||
let res = rp
|
||||
.raw(
|
||||
ClientType::Desktop,
|
||||
"browse",
|
||||
&QBrowse {
|
||||
context: rp.get_context(ClientType::Desktop, true, None).await,
|
||||
browse_id: id,
|
||||
params: Some("EgZzaG9ydHPyBgUKA5oBAA%3D%3D"),
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
Ok(res.contains("\"shortsLockupViewModel\""))
|
||||
}
|
||||
|
|
|
@ -38,8 +38,6 @@ pub async fn download_testfiles() {
|
|||
search_cont().await;
|
||||
search_playlists().await;
|
||||
search_empty().await;
|
||||
startpage().await;
|
||||
startpage_cont().await;
|
||||
trending().await;
|
||||
|
||||
music_playlist().await;
|
||||
|
@ -66,9 +64,10 @@ pub async fn download_testfiles() {
|
|||
music_genre().await;
|
||||
}
|
||||
|
||||
const CLIENT_TYPES: [ClientType; 5] = [
|
||||
const CLIENT_TYPES: [ClientType; 6] = [
|
||||
ClientType::Desktop,
|
||||
ClientType::DesktopMusic,
|
||||
ClientType::Tv,
|
||||
ClientType::TvHtml5Embed,
|
||||
ClientType::Android,
|
||||
ClientType::Ios,
|
||||
|
@ -447,29 +446,6 @@ async fn search_empty() {
|
|||
.unwrap();
|
||||
}
|
||||
|
||||
async fn startpage() {
|
||||
let json_path = path!(*TESTFILES_DIR / "trends" / "startpage.json");
|
||||
if json_path.exists() {
|
||||
return;
|
||||
}
|
||||
|
||||
let rp = rp_testfile(&json_path);
|
||||
rp.query().startpage().await.unwrap();
|
||||
}
|
||||
|
||||
async fn startpage_cont() {
|
||||
let json_path = path!(*TESTFILES_DIR / "trends" / "startpage_cont.json");
|
||||
if json_path.exists() {
|
||||
return;
|
||||
}
|
||||
|
||||
let rp = RustyPipe::new();
|
||||
let startpage = rp.query().startpage().await.unwrap();
|
||||
|
||||
let rp = rp_testfile(&json_path);
|
||||
startpage.next(rp.query()).await.unwrap();
|
||||
}
|
||||
|
||||
async fn trending() {
|
||||
let json_path = path!(*TESTFILES_DIR / "trends" / "trending_videos.json");
|
||||
if json_path.exists() {
|
||||
|
|
|
@ -3,24 +3,70 @@
|
|||
All notable changes to this project will be documented in this file.
|
||||
|
||||
|
||||
## [v0.1.1](https://code.thetadev.de/ThetaDev/rustypipe/compare/rustypipe-downloader/v0.1.0..rustypipe-downloader/v0.1.1) - 2024-06-27
|
||||
## [v0.2.1](https://codeberg.org/ThetaDev/rustypipe/compare/rustypipe-downloader/v0.2.0..rustypipe-downloader/v0.2.1) - 2024-09-10
|
||||
|
||||
### 📚 Documentation
|
||||
|
||||
- Add logo - ([6646078](https://code.thetadev.de/ThetaDev/rustypipe/commit/66460789449be0d5984cbdb6ec372e69323b7a88))
|
||||
- Fix license badge URL - ([4a253e1](https://codeberg.org/ThetaDev/rustypipe/commit/4a253e1a47317e9999e6ad31ac5c411956a0986a))
|
||||
|
||||
### ⚙️ Miscellaneous Tasks
|
||||
|
||||
- Changelog: fix incorrect version URLs - ([97b6f07](https://code.thetadev.de/ThetaDev/rustypipe/commit/97b6f07399e80e00a6c015d013e744568be125dd))
|
||||
- Update rstest to v0.19.0 - ([50fd1f0](https://code.thetadev.de/ThetaDev/rustypipe/commit/50fd1f08caf39c1298654e06059cc393543e925b))
|
||||
- Introduce MSRV - ([5dbb288](https://code.thetadev.de/ThetaDev/rustypipe/commit/5dbb288a496d53a299effa2026f5258af7b1f176))
|
||||
- Fix clippy lints - ([45b9f2a](https://code.thetadev.de/ThetaDev/rustypipe/commit/45b9f2a627b4e7075ba0b1c5f16efcc19aef7922))
|
||||
- *(deps)* Update rust crate tokio to 1.20.4 [security] (#4) - ([ce3ec34](https://code.thetadev.de/ThetaDev/rustypipe/commit/ce3ec34337b8acac41410ea39264aab7423d5801))
|
||||
- *(deps)* Update rust crate quick-xml to 0.34.0 (#5) - ([1e8a1af](https://code.thetadev.de/ThetaDev/rustypipe/commit/1e8a1af08c873cee7feadf63c2eff62753a78f64))
|
||||
- *(deps)* Update rust crate rstest to 0.21.0 (#7) - ([c3af918](https://code.thetadev.de/ThetaDev/rustypipe/commit/c3af918ba53c6230c0e4aef822a0cb2cf120bf3f))
|
||||
- *(deps)* Update rust crate tokio to 1.20.4 [security] (#10) - ([a445e51](https://codeberg.org/ThetaDev/rustypipe/commit/a445e51b54a9afc44cd9657260a0b3d2abddbfa6))
|
||||
|
||||
|
||||
## [v0.2.0](https://codeberg.org/ThetaDev/rustypipe/compare/rustypipe-downloader/v0.1.1..rustypipe-downloader/v0.2.0) - 2024-08-18
|
||||
|
||||
### 🚀 Features
|
||||
|
||||
- Overhauled downloader - ([11a0038](https://codeberg.org/ThetaDev/rustypipe/commit/11a00383502917cd98245c3da349107289ba3aa9))
|
||||
- [**breaking**] Add TV client - ([e608811](https://codeberg.org/ThetaDev/rustypipe/commit/e608811e5f5615416241e67561671330097092cb))
|
||||
- Downloader: add audio tagging - ([1e1315a](https://codeberg.org/ThetaDev/rustypipe/commit/1e1315a8378bd0ad25b5f1614e83dabc4a0b40d5))
|
||||
- Downloader: add download_track fn, improve path templates - ([e1e4fb2](https://codeberg.org/ThetaDev/rustypipe/commit/e1e4fb29c190fec07f17c59ec88bef4f1c2a76a1))
|
||||
- Add audiotag+indicatif features to downloader - ([97fb057](https://codeberg.org/ThetaDev/rustypipe/commit/97fb0578b5c4954a596d8dee0c4b6e1d773a9300))
|
||||
- Add plaintext output to CLI - ([91b020e](https://codeberg.org/ThetaDev/rustypipe/commit/91b020efd498eff6e0f354a1de39439e252a79dd))
|
||||
- Add potoken option to downloader - ([904f821](https://codeberg.org/ThetaDev/rustypipe/commit/904f8215d84c810b04e4d2134718e786a4803ad2))
|
||||
- Add list of clients to downloader - ([5e646af](https://codeberg.org/ThetaDev/rustypipe/commit/5e646afd1edc6c0101501311527ea56d3bad5fd2))
|
||||
- Retry with different client after 403 error - ([d875b54](https://codeberg.org/ThetaDev/rustypipe/commit/d875b5442de9822ba7ddc6f05789f56a8962808c))
|
||||
- [**breaking**] Update channel model, addd handle + video_count, remove tv/mobile banner - ([e671570](https://codeberg.org/ThetaDev/rustypipe/commit/e6715700d950912031d5fbc1263f8770b6ffc49c))
|
||||
|
||||
### 🐛 Bug Fixes
|
||||
|
||||
- *(deps)* Update quick-xml to v0.35.0 - ([298e4de](https://codeberg.org/ThetaDev/rustypipe/commit/298e4def93d1595fba91be103f014aa645a08937))
|
||||
- Improve deobfuscator (support multiple nsig name matches, error if mapping all streams fails) - ([8152ce6](https://codeberg.org/ThetaDev/rustypipe/commit/8152ce6b088b57be9b8419b754aca93805e5f34d))
|
||||
- Set tracing instrumentation level to Error - ([9da3b25](https://codeberg.org/ThetaDev/rustypipe/commit/9da3b25be2b2577f7bd0282c09d10d368ac8b73f))
|
||||
- Add docs.rs feature attributes - ([ec13cbb](https://codeberg.org/ThetaDev/rustypipe/commit/ec13cbb1f35081118dda0f7f35e3ef90f7ca79a8))
|
||||
- Show docs.rs feature flags - ([67a231d](https://codeberg.org/ThetaDev/rustypipe/commit/67a231d6d1b6427f500667729a59032f2b28cc65))
|
||||
|
||||
### ⚙️ Miscellaneous Tasks
|
||||
|
||||
- *(deps)* Update rust crate quick-xml to 0.36.0 (#8) - ([b6bc05c](https://codeberg.org/ThetaDev/rustypipe/commit/b6bc05c1f39da9a846b2e3d1d24bcbccb031203b))
|
||||
- *(deps)* Update rust crate rstest to 0.22.0 (#9) - ([abb7832](https://codeberg.org/ThetaDev/rustypipe/commit/abb783219aba4b492c1dff03c2148acf1f51a55d))
|
||||
- Change repo URL to Codeberg - ([1793331](https://codeberg.org/ThetaDev/rustypipe/commit/17933315d947f76d5fe1aa52abf7ea24c3ce6381))
|
||||
- Adjust dependency versions - ([70c6f8c](https://codeberg.org/ThetaDev/rustypipe/commit/70c6f8c3b97baefd316fff90cc727524516657af))
|
||||
|
||||
### Todo
|
||||
|
||||
- Update metadata - ([8692ca8](https://codeberg.org/ThetaDev/rustypipe/commit/8692ca81d972d0d2acf6fb4da79b9e0f5ebf4daf))
|
||||
|
||||
|
||||
## [v0.1.1](https://codeberg.org/ThetaDev/rustypipe/compare/rustypipe-downloader/v0.1.0..rustypipe-downloader/v0.1.1) - 2024-06-27
|
||||
|
||||
### 📚 Documentation
|
||||
|
||||
- Add logo - ([6646078](https://codeberg.org/ThetaDev/rustypipe/commit/66460789449be0d5984cbdb6ec372e69323b7a88))
|
||||
|
||||
### ⚙️ Miscellaneous Tasks
|
||||
|
||||
- Changelog: fix incorrect version URLs - ([97b6f07](https://codeberg.org/ThetaDev/rustypipe/commit/97b6f07399e80e00a6c015d013e744568be125dd))
|
||||
- Update rstest to v0.19.0 - ([50fd1f0](https://codeberg.org/ThetaDev/rustypipe/commit/50fd1f08caf39c1298654e06059cc393543e925b))
|
||||
- Introduce MSRV - ([5dbb288](https://codeberg.org/ThetaDev/rustypipe/commit/5dbb288a496d53a299effa2026f5258af7b1f176))
|
||||
- Fix clippy lints - ([45b9f2a](https://codeberg.org/ThetaDev/rustypipe/commit/45b9f2a627b4e7075ba0b1c5f16efcc19aef7922))
|
||||
- *(deps)* Update rust crate tokio to 1.20.4 [security] (#4) - ([ce3ec34](https://codeberg.org/ThetaDev/rustypipe/commit/ce3ec34337b8acac41410ea39264aab7423d5801))
|
||||
- *(deps)* Update rust crate quick-xml to 0.34.0 (#5) - ([1e8a1af](https://codeberg.org/ThetaDev/rustypipe/commit/1e8a1af08c873cee7feadf63c2eff62753a78f64))
|
||||
- *(deps)* Update rust crate rstest to 0.21.0 (#7) - ([c3af918](https://codeberg.org/ThetaDev/rustypipe/commit/c3af918ba53c6230c0e4aef822a0cb2cf120bf3f))
|
||||
- Update rustypipe to 0.2.0
|
||||
|
||||
## [v0.1.0](https://code.thetadev.de/ThetaDev/rustypipe/commits/tag/rustypipe-downloader/v0.1.0) - 2024-03-22
|
||||
## [v0.1.0](https://codeberg.org/ThetaDev/rustypipe/commits/tag/rustypipe-downloader/v0.1.0) - 2024-03-22
|
||||
|
||||
Initial release
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
[package]
|
||||
name = "rustypipe-downloader"
|
||||
version = "0.1.1"
|
||||
version = "0.2.1"
|
||||
rust-version = "1.67.1"
|
||||
edition.workspace = true
|
||||
authors.workspace = true
|
||||
|
@ -30,6 +30,8 @@ rustls-tls-native-roots = [
|
|||
"rustypipe/rustls-tls-native-roots",
|
||||
]
|
||||
|
||||
audiotag = ["dep:lofty", "dep:image", "dep:smartcrop2"]
|
||||
|
||||
[dependencies]
|
||||
rustypipe.workspace = true
|
||||
once_cell.workspace = true
|
||||
|
@ -39,6 +41,22 @@ futures.workspace = true
|
|||
reqwest = { workspace = true, features = ["stream"] }
|
||||
rand.workspace = true
|
||||
tokio = { workspace = true, features = ["macros", "fs", "process"] }
|
||||
indicatif.workspace = true
|
||||
indicatif = { workspace = true, optional = true }
|
||||
filenamify.workspace = true
|
||||
tracing.workspace = true
|
||||
time.workspace = true
|
||||
lofty = { version = "0.21.0", optional = true }
|
||||
image = { version = "0.25.0", optional = true }
|
||||
smartcrop2 = { version = "0.3.0", optional = true }
|
||||
|
||||
[dev-dependencies]
|
||||
path_macro.workspace = true
|
||||
rstest.workspace = true
|
||||
serde_json.workspace = true
|
||||
temp_testdir = "0.2.3"
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
# To build locally:
|
||||
# RUSTDOCFLAGS="--cfg docsrs" cargo +nightly doc --features indicatif,audiotag --no-deps --open
|
||||
features = ["indicatif", "audiotag"]
|
||||
rustdoc-args = ["--cfg", "docsrs"]
|
||||
|
|
46
downloader/README.md
Normal file
46
downloader/README.md
Normal file
|
@ -0,0 +1,46 @@
|
|||
# ![RustyPipe](https://codeberg.org/ThetaDev/rustypipe/raw/branch/main/notes/logo.svg) Downloader
|
||||
|
||||
[![Current crates.io version](https://img.shields.io/crates/v/rustypipe-downloader.svg)](https://crates.io/crates/rustypipe-downloader)
|
||||
[![License](https://img.shields.io/badge/License-GPL--3-blue.svg?style=flat)](http://opensource.org/licenses/GPL-3.0)
|
||||
[![CI status](https://codeberg.org/ThetaDev/rustypipe/actions/workflows/ci.yaml/badge.svg?style=flat&label=CI)](https://codeberg.org/ThetaDev/rustypipe/actions/?workflow=ci.yaml)
|
||||
|
||||
The downloader is a companion crate for RustyPipe that allows for easy and fast
|
||||
downloading of video and audio files.
|
||||
|
||||
## Features
|
||||
|
||||
- Fast download of streams, bypassing YouTube's throttling
|
||||
- Join video and audio streams using ffmpeg
|
||||
- [Indicatif](https://crates.io/crates/indicatif) support to show download progress bars
|
||||
(enable `indicatif` feature to use)
|
||||
- Tag audio files with title, album, artist, date, description and album cover (enable
|
||||
`audiotag` feature to use)
|
||||
- Album covers are automatically cropped using smartcrop to ensure they are square
|
||||
|
||||
## How to use
|
||||
|
||||
For the downloader to work, you need to have ffmpeg installed on your system. If your
|
||||
ffmpeg binary is located at a non-standard path, you can configure the location using
|
||||
[`DownloaderBuilder::ffmpeg`].
|
||||
|
||||
At first you have to instantiate and configure the downloader using either
|
||||
[`Downloader::new`] or the [`DownloaderBuilder`].
|
||||
|
||||
Then you can build a new download query with a video ID, stream filter and destination
|
||||
path and finally download the video.
|
||||
|
||||
```rust ignore
|
||||
use rustypipe::param::StreamFilter;
|
||||
use rustypipe_downloader::DownloaderBuilder;
|
||||
|
||||
let dl = DownloaderBuilder::new()
|
||||
.audio_tag()
|
||||
.crop_cover()
|
||||
.build();
|
||||
|
||||
let filter_audio = StreamFilter::new().no_video();
|
||||
dl.id("eRsGyueVLvQ").stream_filter(filter_audio).to_file("audio.opus").download().await;
|
||||
|
||||
let filter_video = StreamFilter::new().video_max_res(720);
|
||||
dl.id("eRsGyueVLvQ").stream_filter(filter_video).to_file("video.mp4").download().await;
|
||||
```
|
54
downloader/src/error.rs
Normal file
54
downloader/src/error.rs
Normal file
|
@ -0,0 +1,54 @@
|
|||
use std::{borrow::Cow, path::PathBuf};
|
||||
|
||||
use rustypipe::client::ClientType;
|
||||
|
||||
/// Error from the video downloader
|
||||
#[derive(thiserror::Error, Debug)]
|
||||
#[non_exhaustive]
|
||||
pub enum DownloadError {
|
||||
/// RustyPipe error
|
||||
#[error("{0}")]
|
||||
RustyPipe(#[from] rustypipe::error::Error),
|
||||
/// Error from the HTTP client
|
||||
#[error("http error: {0}")]
|
||||
Http(#[from] reqwest::Error),
|
||||
/// 403 error trying to download video
|
||||
#[error("YouTube returned 403 error")]
|
||||
Forbidden(ClientType),
|
||||
/// File IO error
|
||||
#[error(transparent)]
|
||||
Io(#[from] std::io::Error),
|
||||
/// FFmpeg returned an error
|
||||
#[error("FFmpeg error: {0}")]
|
||||
Ffmpeg(Cow<'static, str>),
|
||||
/// Error parsing ranges for progressive download
|
||||
#[error("Progressive download error: {0}")]
|
||||
Progressive(Cow<'static, str>),
|
||||
/// Video could not be downloaded because of invalid player data
|
||||
#[error("input error: {0}")]
|
||||
Input(Cow<'static, str>),
|
||||
/// Download target already exists
|
||||
#[error("file {0} already exists")]
|
||||
Exists(PathBuf),
|
||||
#[cfg(feature = "audiotag")]
|
||||
/// Audio tagging error
|
||||
#[error("Audio tag error: {0}")]
|
||||
AudioTag(Cow<'static, str>),
|
||||
/// Other error
|
||||
#[error("error: {0}")]
|
||||
Other(Cow<'static, str>),
|
||||
}
|
||||
|
||||
#[cfg(feature = "audiotag")]
|
||||
impl From<lofty::error::LoftyError> for DownloadError {
|
||||
fn from(value: lofty::error::LoftyError) -> Self {
|
||||
Self::AudioTag(value.to_string().into())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "audiotag")]
|
||||
impl From<image::ImageError> for DownloadError {
|
||||
fn from(value: image::ImageError) -> Self {
|
||||
Self::AudioTag(value.to_string().into())
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load diff
|
@ -1,26 +1,8 @@
|
|||
use std::{borrow::Cow, collections::BTreeMap};
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use reqwest::Url;
|
||||
|
||||
/// Error from the video downloader
|
||||
#[derive(thiserror::Error, Debug)]
|
||||
#[non_exhaustive]
|
||||
pub enum DownloadError {
|
||||
/// Error from the HTTP client
|
||||
#[error("http error: {0}")]
|
||||
Http(#[from] reqwest::Error),
|
||||
/// File IO error
|
||||
#[error(transparent)]
|
||||
Io(#[from] std::io::Error),
|
||||
#[error("FFmpeg error: {0}")]
|
||||
Ffmpeg(Cow<'static, str>),
|
||||
#[error("Progressive download error: {0}")]
|
||||
Progressive(Cow<'static, str>),
|
||||
#[error("input error: {0}")]
|
||||
Input(Cow<'static, str>),
|
||||
#[error("error: {0}")]
|
||||
Other(Cow<'static, str>),
|
||||
}
|
||||
use crate::DownloadError;
|
||||
|
||||
/// Split an URL into its base string and parameter map
|
||||
///
|
||||
|
|
113
downloader/tests/tests.rs
Normal file
113
downloader/tests/tests.rs
Normal file
|
@ -0,0 +1,113 @@
|
|||
use std::{fs, os::unix::fs::MetadataExt, path::Path, process::Command};
|
||||
|
||||
use path_macro::path;
|
||||
use rstest::{fixture, rstest};
|
||||
use rustypipe::{client::RustyPipe, model::AudioCodec, param::StreamFilter};
|
||||
use rustypipe_downloader::Downloader;
|
||||
use temp_testdir::TempDir;
|
||||
|
||||
/// Get a new RusttyPipe instance
|
||||
#[fixture]
|
||||
fn rp() -> RustyPipe {
|
||||
let vdata = std::env::var("YT_VDATA").ok();
|
||||
RustyPipe::builder()
|
||||
.strict()
|
||||
.storage_dir(path!(env!("CARGO_MANIFEST_DIR") / ".."))
|
||||
.visitor_data_opt(vdata)
|
||||
.build()
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[tokio::test]
|
||||
async fn download_video(rp: RustyPipe) {
|
||||
let td = TempDir::default();
|
||||
let td_path = td.to_path_buf();
|
||||
|
||||
let dl = Downloader::builder().rustypipe(&rp).build();
|
||||
|
||||
let res = dl
|
||||
.id("UXqq0ZvbOnk")
|
||||
.to_dir(&td_path)
|
||||
.stream_filter(StreamFilter::new().video_max_res(480))
|
||||
.download()
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
res.dest,
|
||||
path!(td_path / "CHARGE - Blender Open Movie [UXqq0ZvbOnk].mp4")
|
||||
);
|
||||
assert_eq!(res.player_data.details.id, "UXqq0ZvbOnk");
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[tokio::test]
|
||||
async fn download_music(rp: RustyPipe) {
|
||||
let td = TempDir::default();
|
||||
let td_path = td.to_path_buf();
|
||||
|
||||
let dl = Downloader::builder()
|
||||
.audio_tag()
|
||||
.crop_cover()
|
||||
.rustypipe(&rp)
|
||||
.build();
|
||||
|
||||
let res = dl
|
||||
.id("bVtv3st8bgc")
|
||||
.to_dir(&td_path)
|
||||
.stream_filter(
|
||||
StreamFilter::new()
|
||||
.no_video()
|
||||
.audio_codecs([AudioCodec::Opus]),
|
||||
)
|
||||
.download()
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
res.dest,
|
||||
path!(td_path / "Lord of the Riffs [bVtv3st8bgc].opus")
|
||||
);
|
||||
assert_eq!(res.player_data.details.id, "bVtv3st8bgc");
|
||||
let fm = fs::metadata(&res.dest).unwrap();
|
||||
assert_gte(fm.size(), 6_000_000, "file size");
|
||||
assert_audio_meta(
|
||||
&res.dest,
|
||||
"Lord of the Riffs",
|
||||
"Alexander Nakarada - CreatorChords",
|
||||
"Lord of the Riffs",
|
||||
"2022-02-05",
|
||||
);
|
||||
}
|
||||
|
||||
/// Assert that number A is greater than or equal to number B
|
||||
#[track_caller]
|
||||
fn assert_gte<T: PartialOrd + std::fmt::Display>(a: T, b: T, msg: &str) {
|
||||
assert!(a >= b, "expected >= {b} {msg}, got {a}");
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
fn assert_audio_meta(p: &Path, title: &str, artist: &str, album: &str, date: &str) {
|
||||
let res = Command::new("ffprobe")
|
||||
.args([
|
||||
"-loglevel",
|
||||
"error",
|
||||
"-show_entries",
|
||||
"stream_tags",
|
||||
"-of",
|
||||
"json",
|
||||
])
|
||||
.arg(p)
|
||||
.output()
|
||||
.unwrap();
|
||||
if !res.status.success() {
|
||||
panic!("ffprobe error\n{}", String::from_utf8_lossy(&res.stderr))
|
||||
}
|
||||
let res_json = serde_json::from_slice::<serde_json::Value>(&res.stdout).unwrap();
|
||||
let tags = &res_json["streams"][0]["tags"];
|
||||
assert_eq!(tags["TITLE"].as_str(), Some(title));
|
||||
assert_eq!(tags["ARTIST"].as_str(), Some(artist));
|
||||
assert_eq!(tags["ALBUM"].as_str(), Some(album));
|
||||
assert_eq!(tags["DATE"].as_str(), Some(date));
|
||||
}
|
|
@ -748,3 +748,42 @@ seperate framework update object
|
|||
}
|
||||
}
|
||||
```
|
||||
|
||||
## [15] Channel shorts: shortsLockupViewModel
|
||||
|
||||
- **Encountered on:** 10.09.2024
|
||||
- **Impact:** 🟢 Low
|
||||
- **Endpoint:** browse
|
||||
- **Status:** Common
|
||||
|
||||
YouTube changed the data model for the channel shorts tab
|
||||
|
||||
```json
|
||||
{
|
||||
"richItemRenderer": {
|
||||
"content": {
|
||||
"shortsLockupViewModel": {
|
||||
"entityId": "shorts-shelf-item-ovaHmfy3O6U",
|
||||
"accessibilityText": "hangover food, 17 million views - play Short",
|
||||
"thumbnail": {
|
||||
"sources": [
|
||||
{
|
||||
"url": "https://i.ytimg.com/vi/ovaHmfy3O6U/oar2.jpg?sqp=-oaymwEdCJUDENAFSFWQAgHyq4qpAwwIARUAAIhCcAHAAQY=&rs=AOn4CLBg-kG4rAi-BQ8Xkp2hOtOu-oXDLQ",
|
||||
"width": 405,
|
||||
"height": 720
|
||||
}
|
||||
]
|
||||
},
|
||||
"overlayMetadata": {
|
||||
"primaryText": {
|
||||
"content": "hangover food"
|
||||
},
|
||||
"secondaryText": {
|
||||
"content": "17M views"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
|
BIN
notes/logo.png
Normal file
BIN
notes/logo.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 3.5 KiB |
30
notes/po_token.md
Normal file
30
notes/po_token.md
Normal file
|
@ -0,0 +1,30 @@
|
|||
# About the new `pot` token
|
||||
|
||||
YouTube has implemented a new method to prevent downloaders and alternative clients from accessing
|
||||
their videos. Now requests to YouTube's video servers require a `pot` URL parameter.
|
||||
|
||||
It is currently only required in the web player. The YTM and embedded player sends the token, too, but does not require it (this may change in the future).
|
||||
|
||||
The TV player does not use the token at all and is currently the best workaround. The only downside
|
||||
is that the TV player does not return any video metadata like title and description text.
|
||||
|
||||
The first part of a video file (range: 0-1007959 bytes) can be downloaded without the token.
|
||||
Requesting more of the file requires the pot token to be set, otherwise YouTube responds with a 403
|
||||
error.
|
||||
|
||||
The pot token is base64-formatted and usually starts with a M
|
||||
|
||||
`MnToZ2brHmyo0ehfKtK_EWUq60dPYDXksNX_UsaniM_Uj6zbtiIZujCHY02hr7opxB_n3XHetJQCBV9cnNHovuhvDqrjfxsKR-sjn-eIxqv3qOZKphvyDpQzlYBnT2AXK41R-ti6iPonrvlvKIASNmYX2lhsEg==`
|
||||
|
||||
The token is generated from YouTubes Botguard script. The token is bound to the visitor data cookie
|
||||
used to fetch the player data.
|
||||
|
||||
This feature has been A/B-tested for a few weeks. During that time, refetching the player in case
|
||||
of a 403 download error often made things work again. As of 08.08.2024 this new feature seems to be
|
||||
stabilized and retrying requests does not work any more.
|
||||
|
||||
## Getting a `pot` token
|
||||
|
||||
You need a real browser environment to run YouTube's botguard and obtain a pot token. The Invidious project has created a script to
|
||||
<https://github.com/iv-org/youtube-trusted-session-generator/tree/master>.
|
||||
The script opens YouTube's embedded video player, starts playback and extracts the visitor data
|
|
@ -1,11 +1,10 @@
|
|||
{
|
||||
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
|
||||
"extends": [
|
||||
"config:best-practices",
|
||||
":approveMajorUpdates",
|
||||
"schedule:daily"
|
||||
"config:best-practices"
|
||||
],
|
||||
"semanticCommits": "enabled",
|
||||
"automerge": true,
|
||||
"automergeStrategy": "squash",
|
||||
"osvVulnerabilityAlerts": true,
|
||||
"labels": ["dependency-upgrade"],
|
||||
|
|
|
@ -16,7 +16,9 @@ use crate::{
|
|||
util::{self, timeago, ProtoBuilder},
|
||||
};
|
||||
|
||||
use super::{response, ClientType, MapResponse, QContinuation, RustyPipeQuery, YTContext};
|
||||
use super::{
|
||||
response, ClientType, MapRespCtx, MapResponse, QContinuation, RustyPipeQuery, YTContext,
|
||||
};
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
|
@ -80,7 +82,7 @@ impl RustyPipeQuery {
|
|||
}
|
||||
|
||||
/// Get the videos from a YouTube channel
|
||||
#[tracing::instrument(skip(self))]
|
||||
#[tracing::instrument(skip(self), level = "error")]
|
||||
pub async fn channel_videos<S: AsRef<str> + Debug>(
|
||||
&self,
|
||||
channel_id: S,
|
||||
|
@ -92,7 +94,7 @@ impl RustyPipeQuery {
|
|||
/// Get a ordered list of videos from a YouTube channel
|
||||
///
|
||||
/// This function does not return channel metadata.
|
||||
#[tracing::instrument(skip(self))]
|
||||
#[tracing::instrument(skip(self), level = "error")]
|
||||
pub async fn channel_videos_order<S: AsRef<str> + Debug>(
|
||||
&self,
|
||||
channel_id: S,
|
||||
|
@ -103,7 +105,7 @@ impl RustyPipeQuery {
|
|||
}
|
||||
|
||||
/// Get the videos of the given tab (Shorts, Livestreams) from a YouTube channel
|
||||
#[tracing::instrument(skip(self))]
|
||||
#[tracing::instrument(skip(self), level = "error")]
|
||||
pub async fn channel_videos_tab<S: AsRef<str> + Debug>(
|
||||
&self,
|
||||
channel_id: S,
|
||||
|
@ -116,7 +118,7 @@ impl RustyPipeQuery {
|
|||
/// Get a ordered list of videos from the given tab (Shorts, Livestreams) of a YouTube channel
|
||||
///
|
||||
/// This function does not return channel metadata.
|
||||
#[tracing::instrument(skip(self))]
|
||||
#[tracing::instrument(skip(self), level = "error")]
|
||||
pub async fn channel_videos_tab_order<S: AsRef<str> + Debug>(
|
||||
&self,
|
||||
channel_id: S,
|
||||
|
@ -134,7 +136,7 @@ impl RustyPipeQuery {
|
|||
}
|
||||
|
||||
/// Search the videos of a channel
|
||||
#[tracing::instrument(skip(self))]
|
||||
#[tracing::instrument(skip(self), level = "error")]
|
||||
pub async fn channel_search<S: AsRef<str> + Debug, S2: AsRef<str> + Debug>(
|
||||
&self,
|
||||
channel_id: S,
|
||||
|
@ -150,7 +152,7 @@ impl RustyPipeQuery {
|
|||
}
|
||||
|
||||
/// Get the playlists of a channel
|
||||
#[tracing::instrument(skip(self))]
|
||||
#[tracing::instrument(skip(self), level = "error")]
|
||||
pub async fn channel_playlists<S: AsRef<str> + Debug>(
|
||||
&self,
|
||||
channel_id: S,
|
||||
|
@ -175,7 +177,7 @@ impl RustyPipeQuery {
|
|||
}
|
||||
|
||||
/// Get additional metadata from the *About* tab of a channel
|
||||
#[tracing::instrument(skip(self))]
|
||||
#[tracing::instrument(skip(self), level = "error")]
|
||||
pub async fn channel_info<S: AsRef<str> + Debug>(
|
||||
&self,
|
||||
channel_id: S,
|
||||
|
@ -201,16 +203,13 @@ impl RustyPipeQuery {
|
|||
impl MapResponse<Channel<Paginator<VideoItem>>> for response::Channel {
|
||||
fn map_response(
|
||||
self,
|
||||
id: &str,
|
||||
lang: Language,
|
||||
_deobf: Option<&crate::deobfuscate::DeobfData>,
|
||||
vdata: Option<&str>,
|
||||
ctx: &MapRespCtx<'_>,
|
||||
) -> Result<MapResult<Channel<Paginator<VideoItem>>>, ExtractionError> {
|
||||
let content = map_channel_content(id, self.contents, self.alerts)?;
|
||||
let content = map_channel_content(ctx.id, self.contents, self.alerts)?;
|
||||
let visitor_data = self
|
||||
.response_context
|
||||
.visitor_data
|
||||
.or_else(|| vdata.map(str::to_owned));
|
||||
.or_else(|| ctx.visitor_data.map(str::to_owned));
|
||||
|
||||
let channel_data = map_channel(
|
||||
MapChannelData {
|
||||
|
@ -221,12 +220,11 @@ impl MapResponse<Channel<Paginator<VideoItem>>> for response::Channel {
|
|||
has_shorts: content.has_shorts,
|
||||
has_live: content.has_live,
|
||||
},
|
||||
id,
|
||||
lang,
|
||||
ctx,
|
||||
)?;
|
||||
|
||||
let mut mapper = response::YouTubeListMapper::<VideoItem>::with_channel(
|
||||
lang,
|
||||
ctx.lang,
|
||||
&channel_data.c,
|
||||
channel_data.warnings,
|
||||
);
|
||||
|
@ -249,16 +247,13 @@ impl MapResponse<Channel<Paginator<VideoItem>>> for response::Channel {
|
|||
impl MapResponse<Channel<Paginator<PlaylistItem>>> for response::Channel {
|
||||
fn map_response(
|
||||
self,
|
||||
id: &str,
|
||||
lang: Language,
|
||||
_deobf: Option<&crate::deobfuscate::DeobfData>,
|
||||
vdata: Option<&str>,
|
||||
ctx: &MapRespCtx<'_>,
|
||||
) -> Result<MapResult<Channel<Paginator<PlaylistItem>>>, ExtractionError> {
|
||||
let content = map_channel_content(id, self.contents, self.alerts)?;
|
||||
let content = map_channel_content(ctx.id, self.contents, self.alerts)?;
|
||||
let visitor_data = self
|
||||
.response_context
|
||||
.visitor_data
|
||||
.or_else(|| vdata.map(str::to_owned));
|
||||
.or_else(|| ctx.visitor_data.map(str::to_owned));
|
||||
|
||||
let channel_data = map_channel(
|
||||
MapChannelData {
|
||||
|
@ -269,12 +264,11 @@ impl MapResponse<Channel<Paginator<PlaylistItem>>> for response::Channel {
|
|||
has_shorts: content.has_shorts,
|
||||
has_live: content.has_live,
|
||||
},
|
||||
id,
|
||||
lang,
|
||||
ctx,
|
||||
)?;
|
||||
|
||||
let mut mapper = response::YouTubeListMapper::<PlaylistItem>::with_channel(
|
||||
lang,
|
||||
ctx.lang,
|
||||
&channel_data.c,
|
||||
channel_data.warnings,
|
||||
);
|
||||
|
@ -289,13 +283,7 @@ impl MapResponse<Channel<Paginator<PlaylistItem>>> for response::Channel {
|
|||
}
|
||||
|
||||
impl MapResponse<ChannelInfo> for response::ChannelAbout {
|
||||
fn map_response(
|
||||
self,
|
||||
id: &str,
|
||||
_lang: Language,
|
||||
_deobf: Option<&crate::deobfuscate::DeobfData>,
|
||||
_visitor_data: Option<&str>,
|
||||
) -> Result<MapResult<ChannelInfo>, ExtractionError> {
|
||||
fn map_response(self, ctx: &MapRespCtx<'_>) -> Result<MapResult<ChannelInfo>, ExtractionError> {
|
||||
// Channel info is always fetched in English. There is no localized data there
|
||||
// and it allows parsing the country name.
|
||||
let lang = Language::En;
|
||||
|
@ -309,7 +297,7 @@ impl MapResponse<ChannelInfo> for response::ChannelAbout {
|
|||
.ok_or(ExtractionError::InvalidData("no received endpoint".into()))?,
|
||||
response::ChannelAbout::Content { contents } => {
|
||||
// Handle errors (e.g. age restriction) when regular channel content was returned
|
||||
map_channel_content(id, contents, None)?;
|
||||
map_channel_content(ctx.id, contents, None)?;
|
||||
return Err(ExtractionError::InvalidData(
|
||||
"could not extract aboutData".into(),
|
||||
));
|
||||
|
@ -365,18 +353,6 @@ impl MapResponse<ChannelInfo> for response::ChannelAbout {
|
|||
}
|
||||
}
|
||||
|
||||
fn map_vanity_url(url: &str, id: &str) -> Option<String> {
|
||||
if url.contains(id) {
|
||||
return None;
|
||||
}
|
||||
|
||||
Url::parse(url).ok().map(|mut parsed_url| {
|
||||
// The vanity URL from YouTube is http for some reason
|
||||
_ = parsed_url.set_scheme("https");
|
||||
parsed_url.to_string()
|
||||
})
|
||||
}
|
||||
|
||||
struct MapChannelData {
|
||||
header: Option<response::channel::Header>,
|
||||
metadata: Option<response::channel::Metadata>,
|
||||
|
@ -388,36 +364,41 @@ struct MapChannelData {
|
|||
|
||||
fn map_channel(
|
||||
d: MapChannelData,
|
||||
id: &str,
|
||||
lang: Language,
|
||||
ctx: &MapRespCtx<'_>,
|
||||
) -> Result<MapResult<Channel<()>>, ExtractionError> {
|
||||
let header = d.header.ok_or_else(|| ExtractionError::NotFound {
|
||||
id: id.to_owned(),
|
||||
id: ctx.id.to_owned(),
|
||||
msg: "no header".into(),
|
||||
})?;
|
||||
let metadata = d
|
||||
.metadata
|
||||
.ok_or_else(|| ExtractionError::NotFound {
|
||||
id: id.to_owned(),
|
||||
id: ctx.id.to_owned(),
|
||||
msg: "no metadata".into(),
|
||||
})?
|
||||
.channel_metadata_renderer;
|
||||
let microformat = d.microformat.ok_or_else(|| ExtractionError::NotFound {
|
||||
id: id.to_owned(),
|
||||
id: ctx.id.to_owned(),
|
||||
msg: "no microformat".into(),
|
||||
})?;
|
||||
|
||||
if metadata.external_id != id {
|
||||
if metadata.external_id != ctx.id {
|
||||
return Err(ExtractionError::WrongResult(format!(
|
||||
"got wrong channel id {}, expected {}",
|
||||
metadata.external_id, id
|
||||
metadata.external_id, ctx.id
|
||||
)));
|
||||
}
|
||||
|
||||
let vanity_url = metadata
|
||||
let handle = metadata
|
||||
.vanity_channel_url
|
||||
.as_ref()
|
||||
.and_then(|url| map_vanity_url(url, id));
|
||||
.and_then(|url| Url::parse(url).ok())
|
||||
.and_then(|url| {
|
||||
url.path()
|
||||
.strip_prefix('/')
|
||||
.filter(|handle| util::CHANNEL_HANDLE_REGEX.is_match(handle))
|
||||
.map(str::to_owned)
|
||||
});
|
||||
let mut warnings = Vec::new();
|
||||
|
||||
Ok(MapResult {
|
||||
|
@ -425,17 +406,16 @@ fn map_channel(
|
|||
response::channel::Header::C4TabbedHeaderRenderer(header) => Channel {
|
||||
id: metadata.external_id,
|
||||
name: metadata.title,
|
||||
subscriber_count: header
|
||||
.subscriber_count_text
|
||||
.and_then(|txt| util::parse_large_numstr_or_warn(&txt, lang, &mut warnings)),
|
||||
handle,
|
||||
subscriber_count: header.subscriber_count_text.and_then(|txt| {
|
||||
util::parse_large_numstr_or_warn(&txt, ctx.lang, &mut warnings)
|
||||
}),
|
||||
video_count: None,
|
||||
avatar: header.avatar.into(),
|
||||
verification: header.badges.into(),
|
||||
description: metadata.description,
|
||||
tags: microformat.microformat_data_renderer.tags,
|
||||
vanity_url,
|
||||
banner: header.banner.into(),
|
||||
mobile_banner: header.mobile_banner.into(),
|
||||
tv_banner: header.tv_banner.into(),
|
||||
has_shorts: d.has_shorts,
|
||||
has_live: d.has_live,
|
||||
visitor_data: d.visitor_data,
|
||||
|
@ -456,21 +436,20 @@ fn map_channel(
|
|||
Channel {
|
||||
id: metadata.external_id,
|
||||
name: metadata.title,
|
||||
handle,
|
||||
subscriber_count: hdata.as_ref().and_then(|hdata| {
|
||||
hdata.0.as_ref().and_then(|txt| {
|
||||
util::parse_large_numstr_or_warn(txt, lang, &mut warnings)
|
||||
util::parse_large_numstr_or_warn(txt, ctx.lang, &mut warnings)
|
||||
})
|
||||
}),
|
||||
video_count: None,
|
||||
avatar: hdata.map(|hdata| hdata.1.into()).unwrap_or_default(),
|
||||
// Since the carousel header is only used for YT-internal channels or special events
|
||||
// (World Cup, Coachella, etc.) we can assume the channel to be verified
|
||||
verification: crate::model::Verification::Verified,
|
||||
description: metadata.description,
|
||||
tags: microformat.microformat_data_renderer.tags,
|
||||
vanity_url,
|
||||
banner: Vec::new(),
|
||||
mobile_banner: Vec::new(),
|
||||
tv_banner: Vec::new(),
|
||||
has_shorts: d.has_shorts,
|
||||
has_live: d.has_live,
|
||||
visitor_data: d.visitor_data,
|
||||
|
@ -481,19 +460,33 @@ fn map_channel(
|
|||
let hdata = header.content.page_header_view_model;
|
||||
// channel handle - subscriber count - video count
|
||||
let md_rows = hdata.metadata.content_metadata_view_model.metadata_rows;
|
||||
let sub_part = if md_rows.len() > 1 {
|
||||
md_rows.get(1).and_then(|md| md.metadata_parts.first())
|
||||
let (sub_part, vc_part) = if md_rows.len() > 1 {
|
||||
let mp = &md_rows[1].metadata_parts;
|
||||
(mp.first(), mp.get(1))
|
||||
} else {
|
||||
md_rows.first().and_then(|md| md.metadata_parts.get(1))
|
||||
(
|
||||
md_rows.first().and_then(|md| md.metadata_parts.get(1)),
|
||||
None,
|
||||
)
|
||||
};
|
||||
let subscriber_count = sub_part.and_then(|t| {
|
||||
util::parse_large_numstr_or_warn::<u64>(&t.text, lang, &mut warnings)
|
||||
util::parse_large_numstr_or_warn::<u64>(&t.text, ctx.lang, &mut warnings)
|
||||
});
|
||||
let video_count =
|
||||
vc_part.and_then(|t| util::parse_numeric_or_warn(&t.text, &mut warnings));
|
||||
|
||||
Channel {
|
||||
id: metadata.external_id,
|
||||
name: metadata.title,
|
||||
handle: handle.or_else(|| {
|
||||
md_rows
|
||||
.first()
|
||||
.and_then(|md| md.metadata_parts.get(1))
|
||||
.map(|txt| txt.text.to_owned())
|
||||
.filter(|txt| util::CHANNEL_HANDLE_REGEX.is_match(txt))
|
||||
}),
|
||||
subscriber_count,
|
||||
video_count,
|
||||
avatar: hdata
|
||||
.image
|
||||
.decorated_avatar_view_model
|
||||
|
@ -504,10 +497,7 @@ fn map_channel(
|
|||
verification: hdata.title.into(),
|
||||
description: metadata.description,
|
||||
tags: microformat.microformat_data_renderer.tags,
|
||||
vanity_url,
|
||||
banner: hdata.banner.image_banner_view_model.image.into(),
|
||||
mobile_banner: Vec::new(),
|
||||
tv_banner: Vec::new(),
|
||||
has_shorts: d.has_shorts,
|
||||
has_live: d.has_live,
|
||||
visitor_data: d.visitor_data,
|
||||
|
@ -617,15 +607,14 @@ fn combine_channel_data<T>(channel_data: Channel<()>, content: T) -> Channel<T>
|
|||
Channel {
|
||||
id: channel_data.id,
|
||||
name: channel_data.name,
|
||||
handle: channel_data.handle,
|
||||
subscriber_count: channel_data.subscriber_count,
|
||||
video_count: channel_data.video_count,
|
||||
avatar: channel_data.avatar,
|
||||
verification: channel_data.verification,
|
||||
description: channel_data.description,
|
||||
tags: channel_data.tags,
|
||||
vanity_url: channel_data.vanity_url,
|
||||
banner: channel_data.banner,
|
||||
mobile_banner: channel_data.mobile_banner,
|
||||
tv_banner: channel_data.tv_banner,
|
||||
has_shorts: channel_data.has_shorts,
|
||||
has_live: channel_data.has_live,
|
||||
visitor_data: channel_data.visitor_data,
|
||||
|
@ -697,10 +686,10 @@ mod tests {
|
|||
use rstest::rstest;
|
||||
|
||||
use crate::{
|
||||
client::{response, MapResponse},
|
||||
client::{response, MapRespCtx, MapResponse},
|
||||
error::{ExtractionError, UnavailabilityReason},
|
||||
model::{paginator::Paginator, Channel, ChannelInfo, PlaylistItem, VideoItem},
|
||||
param::{ChannelOrder, ChannelVideoTab, Language},
|
||||
param::{ChannelOrder, ChannelVideoTab},
|
||||
serializer::MapResult,
|
||||
util::tests::TESTFILES,
|
||||
};
|
||||
|
@ -721,6 +710,7 @@ mod tests {
|
|||
#[case::livestreams("livestreams", "UC2DjFE7Xf11URZqWBigcVOQ")]
|
||||
#[case::pageheader("shorts_20240129_pageheader", "UCh8gHdtzO2tXd593_bjErWg")]
|
||||
#[case::pageheader2("videos_20240324_pageheader2", "UC2DjFE7Xf11URZqWBigcVOQ")]
|
||||
#[case::shorts2("shorts_20240910_lockup", "UCh8gHdtzO2tXd593_bjErWg")]
|
||||
fn map_channel_videos(#[case] name: &str, #[case] id: &str) {
|
||||
let json_path = path!(*TESTFILES / "channel" / format!("channel_{name}.json"));
|
||||
let json_file = File::open(json_path).unwrap();
|
||||
|
@ -728,7 +718,7 @@ mod tests {
|
|||
let channel: response::Channel =
|
||||
serde_json::from_reader(BufReader::new(json_file)).unwrap();
|
||||
let map_res: MapResult<Channel<Paginator<VideoItem>>> =
|
||||
channel.map_response(id, Language::En, None, None).unwrap();
|
||||
channel.map_response(&MapRespCtx::test(id)).unwrap();
|
||||
|
||||
assert!(
|
||||
map_res.warnings.is_empty(),
|
||||
|
@ -755,7 +745,7 @@ mod tests {
|
|||
let channel: response::Channel =
|
||||
serde_json::from_reader(BufReader::new(json_file)).unwrap();
|
||||
let res: Result<MapResult<Channel<Paginator<VideoItem>>>, ExtractionError> =
|
||||
channel.map_response("UCbfnHqxXs_K3kvaH-WlNlig", Language::En, None, None);
|
||||
channel.map_response(&MapRespCtx::test("UCbfnHqxXs_K3kvaH-WlNlig"));
|
||||
if let Err(ExtractionError::Unavailable { reason, msg }) = res {
|
||||
assert_eq!(reason, UnavailabilityReason::AgeRestricted);
|
||||
assert!(msg.starts_with("Laphroaig Whisky: "));
|
||||
|
@ -772,7 +762,7 @@ mod tests {
|
|||
let channel: response::Channel =
|
||||
serde_json::from_reader(BufReader::new(json_file)).unwrap();
|
||||
let map_res: MapResult<Channel<Paginator<PlaylistItem>>> = channel
|
||||
.map_response("UC2DjFE7Xf11URZqWBigcVOQ", Language::En, None, None)
|
||||
.map_response(&MapRespCtx::test("UC2DjFE7Xf11URZqWBigcVOQ"))
|
||||
.unwrap();
|
||||
|
||||
assert!(
|
||||
|
@ -791,7 +781,7 @@ mod tests {
|
|||
let channel: response::ChannelAbout =
|
||||
serde_json::from_reader(BufReader::new(json_file)).unwrap();
|
||||
let map_res: MapResult<ChannelInfo> = channel
|
||||
.map_response("UC2DjFE7Xf11U-RZqWBigcVOQ", Language::En, None, None)
|
||||
.map_response(&MapRespCtx::test("UC2DjFE7Xf11U-RZqWBigcVOQ"))
|
||||
.unwrap();
|
||||
|
||||
assert!(
|
||||
|
|
|
@ -18,7 +18,7 @@ impl RustyPipeQuery {
|
|||
/// for checking a lot of channels or implementing a subscription feed.
|
||||
///
|
||||
/// The downside of using the RSS feed is that it does not provide video durations.
|
||||
#[tracing::instrument(skip(self))]
|
||||
#[tracing::instrument(skip(self), level = "error")]
|
||||
pub async fn channel_rss<S: AsRef<str> + Debug>(
|
||||
&self,
|
||||
channel_id: S,
|
||||
|
|
|
@ -61,6 +61,8 @@ pub enum ClientType {
|
|||
///
|
||||
/// can access age-restricted videos, cannot access non-embeddable videos
|
||||
TvHtml5Embed,
|
||||
/// Client used by youtube.com/tv
|
||||
Tv,
|
||||
/// Client used by the Android app
|
||||
///
|
||||
/// no obfuscated stream URLs, includes lower resolution audio streams
|
||||
|
@ -74,7 +76,10 @@ pub enum ClientType {
|
|||
impl ClientType {
|
||||
fn is_web(self) -> bool {
|
||||
match self {
|
||||
ClientType::Desktop | ClientType::DesktopMusic | ClientType::TvHtml5Embed => true,
|
||||
ClientType::Desktop
|
||||
| ClientType::DesktopMusic
|
||||
| ClientType::TvHtml5Embed
|
||||
| ClientType::Tv => true,
|
||||
ClientType::Android | ClientType::Ios => false,
|
||||
}
|
||||
}
|
||||
|
@ -183,6 +188,7 @@ struct QContinuation<'a> {
|
|||
}
|
||||
|
||||
const DEFAULT_UA: &str = "Mozilla/5.0 (X11; Linux x86_64; rv:109.0) Gecko/20100101 Firefox/115.0";
|
||||
const TV_UA: &str = "Mozilla/5.0 (SMART-TV; Linux; Tizen 5.0) AppleWebKit/538.1 (KHTML, like Gecko) Version/5.0 NativeTVAds Safari/538.1";
|
||||
|
||||
const CONSENT_COOKIE: &str = "SOCS=CAISAiAD";
|
||||
|
||||
|
@ -191,13 +197,15 @@ const YOUTUBEI_V1_GAPIS_URL: &str = "https://youtubei.googleapis.com/youtubei/v1
|
|||
const YOUTUBE_MUSIC_V1_URL: &str = "https://music.youtube.com/youtubei/v1/";
|
||||
const YOUTUBE_HOME_URL: &str = "https://www.youtube.com/";
|
||||
const YOUTUBE_MUSIC_HOME_URL: &str = "https://music.youtube.com/";
|
||||
const YOUTUBE_TV_URL: &str = "https://www.youtube.com/tv";
|
||||
|
||||
const DISABLE_PRETTY_PRINT_PARAMETER: &str = "prettyPrint=false";
|
||||
|
||||
// Desktop client
|
||||
const DESKTOP_CLIENT_VERSION: &str = "2.20230126.00.00";
|
||||
const TVHTML5_CLIENT_VERSION: &str = "2.0";
|
||||
const DESKTOP_MUSIC_CLIENT_VERSION: &str = "1.20230123.01.01";
|
||||
const TV_CLIENT_VERSION: &str = "7.20240724.13.00";
|
||||
const TVHTML5_CLIENT_VERSION: &str = "2.0";
|
||||
|
||||
// Mobile client
|
||||
const MOBILE_CLIENT_VERSION: &str = "18.03.33";
|
||||
|
@ -208,6 +216,13 @@ static CLIENT_VERSION_REGEX: Lazy<Regex> =
|
|||
static VISITOR_DATA_REGEX: Lazy<Regex> =
|
||||
Lazy::new(|| Regex::new(r#""visitorData":"([\w\d_\-%]+?)""#).unwrap());
|
||||
|
||||
/// Default order of client types when fetching player data
|
||||
///
|
||||
/// The order may change in the future in case YouTube applies changes to their
|
||||
/// platform that disable a client or make it less reliable.
|
||||
pub const DEFAULT_PLAYER_CLIENT_ORDER: &[ClientType] =
|
||||
&[ClientType::Tv, ClientType::Android, ClientType::Ios];
|
||||
|
||||
/// The RustyPipe client used to access YouTube's API
|
||||
///
|
||||
/// RustyPipe uses an [`Arc`] internally, so if you are using the client
|
||||
|
@ -225,6 +240,7 @@ struct RustyPipeRef {
|
|||
n_http_retries: u32,
|
||||
cache: CacheHolder,
|
||||
default_opts: RustyPipeOpts,
|
||||
user_agent: Cow<'static, str>,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
|
@ -356,14 +372,20 @@ impl Default for RustyPipeOpts {
|
|||
struct CacheHolder {
|
||||
desktop_client: RwLock<CacheEntry<ClientData>>,
|
||||
music_client: RwLock<CacheEntry<ClientData>>,
|
||||
tv_client: RwLock<CacheEntry<ClientData>>,
|
||||
deobf: RwLock<CacheEntry<DeobfData>>,
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(default)]
|
||||
struct CacheData {
|
||||
#[serde(skip_serializing_if = "CacheEntry::is_none")]
|
||||
desktop_client: CacheEntry<ClientData>,
|
||||
#[serde(skip_serializing_if = "CacheEntry::is_none")]
|
||||
music_client: CacheEntry<ClientData>,
|
||||
#[serde(skip_serializing_if = "CacheEntry::is_none")]
|
||||
tv_client: CacheEntry<ClientData>,
|
||||
#[serde(skip_serializing_if = "CacheEntry::is_none")]
|
||||
deobf: CacheEntry<DeobfData>,
|
||||
}
|
||||
|
||||
|
@ -414,6 +436,10 @@ impl<T> CacheEntry<T> {
|
|||
CacheEntry::None => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn is_none(&self) -> bool {
|
||||
matches!(self, Self::None)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> From<T> for CacheEntry<T> {
|
||||
|
@ -432,7 +458,7 @@ impl Default for RustyPipeBuilder {
|
|||
}
|
||||
|
||||
impl RustyPipeBuilder {
|
||||
/// Return a new `RustyPipeBuilder`.
|
||||
/// Create a new [`RustyPipeBuilder`].
|
||||
///
|
||||
/// This is the same as [`RustyPipe::builder`]
|
||||
#[must_use]
|
||||
|
@ -448,15 +474,20 @@ impl RustyPipeBuilder {
|
|||
}
|
||||
}
|
||||
|
||||
/// Return a new, configured RustyPipe instance.
|
||||
/// Create a new, configured [`RustyPipe`] instance.
|
||||
pub fn build(self) -> Result<RustyPipe, Error> {
|
||||
self.build_with_client(ClientBuilder::new())
|
||||
}
|
||||
|
||||
/// Return a new, configured RustyPipe instance using a Reqwest client builder.
|
||||
/// Create a new, configured RustyPipe instance using a Reqwest [`ClientBuilder`].
|
||||
pub fn build_with_client(self, mut client_builder: ClientBuilder) -> Result<RustyPipe, Error> {
|
||||
let user_agent = self
|
||||
.user_agent
|
||||
.map(Cow::Owned)
|
||||
.unwrap_or(Cow::Borrowed(DEFAULT_UA));
|
||||
|
||||
client_builder = client_builder
|
||||
.user_agent(self.user_agent.unwrap_or_else(|| DEFAULT_UA.to_owned()))
|
||||
.user_agent(user_agent.as_ref())
|
||||
.gzip(true)
|
||||
.brotli(true)
|
||||
.redirect(reqwest::redirect::Policy::none());
|
||||
|
@ -500,9 +531,11 @@ impl RustyPipeBuilder {
|
|||
cache: CacheHolder {
|
||||
desktop_client: RwLock::new(cdata.desktop_client),
|
||||
music_client: RwLock::new(cdata.music_client),
|
||||
tv_client: RwLock::new(cdata.tv_client),
|
||||
deobf: RwLock::new(cdata.deobf),
|
||||
},
|
||||
default_opts: self.default_opts,
|
||||
user_agent,
|
||||
}),
|
||||
})
|
||||
}
|
||||
|
@ -714,12 +747,10 @@ impl RustyPipe {
|
|||
async fn http_request(&self, request: &Request) -> Result<Response, reqwest::Error> {
|
||||
let mut last_resp = None;
|
||||
for n in 0..=self.inner.n_http_retries {
|
||||
let resp = self
|
||||
.inner
|
||||
.http
|
||||
.execute(request.try_clone().unwrap())
|
||||
.await?;
|
||||
let resp = self.inner.http.execute(request.try_clone().unwrap()).await;
|
||||
|
||||
let err = match resp {
|
||||
Ok(resp) => {
|
||||
let status = resp.status();
|
||||
// Immediately return in case of success or unrecoverable status code
|
||||
if status.is_success()
|
||||
|
@ -727,6 +758,18 @@ impl RustyPipe {
|
|||
{
|
||||
return Ok(resp);
|
||||
}
|
||||
last_resp = Some(Ok(resp));
|
||||
status.to_string()
|
||||
}
|
||||
Err(e) => {
|
||||
// Retry in case of a timeout error
|
||||
if !e.is_timeout() {
|
||||
return Err(e);
|
||||
}
|
||||
last_resp = Some(Err(e));
|
||||
"timeout".to_string()
|
||||
}
|
||||
};
|
||||
|
||||
// Retry in case of a recoverable status code (server err, too many requests)
|
||||
if n != self.inner.n_http_retries {
|
||||
|
@ -734,15 +777,13 @@ impl RustyPipe {
|
|||
tracing::warn!(
|
||||
"Retry attempt #{}. Error: {}. Waiting {} ms",
|
||||
n + 1,
|
||||
status,
|
||||
err,
|
||||
ms
|
||||
);
|
||||
tokio::time::sleep(Duration::from_millis(ms.into())).await;
|
||||
}
|
||||
|
||||
last_resp = Some(resp);
|
||||
}
|
||||
Ok(last_resp.unwrap())
|
||||
last_resp.unwrap()
|
||||
}
|
||||
|
||||
/// Execute the given http request, returning an error in case of a
|
||||
|
@ -785,6 +826,12 @@ impl RustyPipe {
|
|||
.await
|
||||
}
|
||||
|
||||
/// Extract the current version of the YouTube TV client from the website.
|
||||
async fn extract_tv_client_version(&self) -> Result<String, Error> {
|
||||
self.extract_client_version(None, YOUTUBE_TV_URL, YOUTUBE_TV_URL, Some(TV_UA))
|
||||
.await
|
||||
}
|
||||
|
||||
async fn extract_client_version(
|
||||
&self,
|
||||
sw_url: Option<&str>,
|
||||
|
@ -903,6 +950,37 @@ impl RustyPipe {
|
|||
}
|
||||
}
|
||||
|
||||
/// Get the current version of the YouTube TV client from the following sources
|
||||
///
|
||||
/// 1. from cache
|
||||
/// 2. from the YouTube TV website
|
||||
/// 3. fall back to the hardcoded version
|
||||
async fn get_tv_client_version(&self) -> String {
|
||||
// Write lock here to prevent concurrent tasks from fetching the same data
|
||||
let mut tv_client = self.inner.cache.tv_client.write().await;
|
||||
|
||||
match tv_client.get() {
|
||||
Some(cdata) => cdata.version.clone(),
|
||||
None => {
|
||||
tracing::debug!("getting TV client version");
|
||||
match self.extract_tv_client_version().await {
|
||||
Ok(version) => {
|
||||
*tv_client = CacheEntry::from(ClientData {
|
||||
version: version.clone(),
|
||||
});
|
||||
drop(tv_client);
|
||||
self.store_cache().await;
|
||||
version
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::warn!("{}, falling back to hardcoded TV client version", e);
|
||||
DESKTOP_MUSIC_CLIENT_VERSION.to_owned()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Get deobfuscation data (either from cache or extracted from YouTube's JavaScript code)
|
||||
async fn get_deobf_data(&self) -> Result<DeobfData, Error> {
|
||||
// Write lock here to prevent concurrent tasks from fetching the same data
|
||||
|
@ -944,6 +1022,7 @@ impl RustyPipe {
|
|||
let cdata = CacheData {
|
||||
desktop_client: self.inner.cache.desktop_client.read().await.clone(),
|
||||
music_client: self.inner.cache.music_client.read().await.clone(),
|
||||
tv_client: self.inner.cache.tv_client.read().await.clone(),
|
||||
deobf: self.inner.cache.deobf.read().await.clone(),
|
||||
};
|
||||
|
||||
|
@ -963,7 +1042,14 @@ impl RustyPipe {
|
|||
/// visitor data is extracted from the html page.
|
||||
async fn get_visitor_data(&self) -> Result<String, Error> {
|
||||
tracing::debug!("getting YT visitor data");
|
||||
let resp = self.inner.http.get(YOUTUBE_MUSIC_HOME_URL).send().await?;
|
||||
let resp = self
|
||||
.inner
|
||||
.http
|
||||
.get(YOUTUBE_MUSIC_HOME_URL)
|
||||
.header(header::ORIGIN, YOUTUBE_MUSIC_HOME_URL)
|
||||
.header(header::REFERER, YOUTUBE_MUSIC_HOME_URL)
|
||||
.send()
|
||||
.await?;
|
||||
|
||||
let vdata = resp
|
||||
.headers()
|
||||
|
@ -972,7 +1058,10 @@ impl RustyPipe {
|
|||
.find_map(|c| {
|
||||
if let Ok(cookie) = c.to_str() {
|
||||
if let Some(after) = cookie.strip_prefix("__Secure-YEC=") {
|
||||
return after.split_once(';').map(|s| s.0.to_owned());
|
||||
return after
|
||||
.split_once(';')
|
||||
.map(|s| s.0.to_owned())
|
||||
.filter(|s| !s.is_empty());
|
||||
}
|
||||
}
|
||||
None
|
||||
|
@ -1065,6 +1154,28 @@ impl RustyPipeQuery {
|
|||
self
|
||||
}
|
||||
|
||||
/// Get the user agent for the given client type
|
||||
///
|
||||
/// This can be used for additional HTTP requests (e.g. downloading/streaming)
|
||||
pub fn user_agent(&self, ctype: ClientType) -> Cow<'_, str> {
|
||||
match ctype {
|
||||
ClientType::Desktop | ClientType::DesktopMusic | ClientType::TvHtml5Embed => {
|
||||
Cow::Borrowed(&self.client.inner.user_agent)
|
||||
}
|
||||
ClientType::Tv => TV_UA.into(),
|
||||
ClientType::Android => format!(
|
||||
"com.google.android.youtube/{} (Linux; U; Android 12; {}) gzip",
|
||||
MOBILE_CLIENT_VERSION, self.opts.country
|
||||
)
|
||||
.into(),
|
||||
ClientType::Ios => format!(
|
||||
"com.google.ios.youtube/{} ({}; U; CPU iOS 15_4 like Mac OS X; {})",
|
||||
MOBILE_CLIENT_VERSION, IOS_DEVICE_MODEL, self.opts.country
|
||||
)
|
||||
.into(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a new context object, which is included in every request to
|
||||
/// the YouTube API and contains language, country and device parameters.
|
||||
///
|
||||
|
@ -1132,6 +1243,24 @@ impl RustyPipeQuery {
|
|||
embed_url: YOUTUBE_HOME_URL,
|
||||
}),
|
||||
},
|
||||
ClientType::Tv => YTContext {
|
||||
client: ClientInfo {
|
||||
client_name: "TVHTML5",
|
||||
client_version: Cow::Owned(self.client.get_tv_client_version().await),
|
||||
client_screen: Some("WATCH"),
|
||||
platform: "TV",
|
||||
device_model: Some("SmartTV"),
|
||||
visitor_data,
|
||||
hl,
|
||||
gl,
|
||||
..Default::default()
|
||||
},
|
||||
request: Some(RequestYT::default()),
|
||||
user: User::default(),
|
||||
third_party: Some(ThirdParty {
|
||||
embed_url: YOUTUBE_TV_URL,
|
||||
}),
|
||||
},
|
||||
ClientType::Android => YTContext {
|
||||
client: ClientInfo {
|
||||
client_name: "ANDROID",
|
||||
|
@ -1220,6 +1349,17 @@ impl RustyPipeQuery {
|
|||
.header(header::REFERER, YOUTUBE_HOME_URL)
|
||||
.header("X-YouTube-Client-Name", "1")
|
||||
.header("X-YouTube-Client-Version", TVHTML5_CLIENT_VERSION),
|
||||
ClientType::Tv => self
|
||||
.client
|
||||
.inner
|
||||
.http
|
||||
.post(format!(
|
||||
"{YOUTUBEI_V1_URL}{endpoint}?{DISABLE_PRETTY_PRINT_PARAMETER}"
|
||||
))
|
||||
.header(header::ORIGIN, YOUTUBE_HOME_URL)
|
||||
.header(header::REFERER, YOUTUBE_TV_URL)
|
||||
.header("X-YouTube-Client-Name", "7")
|
||||
.header("X-YouTube-Client-Version", TV_CLIENT_VERSION),
|
||||
ClientType::Android => self
|
||||
.client
|
||||
.inner
|
||||
|
@ -1227,13 +1367,6 @@ impl RustyPipeQuery {
|
|||
.post(format!(
|
||||
"{YOUTUBEI_V1_GAPIS_URL}{endpoint}?{DISABLE_PRETTY_PRINT_PARAMETER}"
|
||||
))
|
||||
.header(
|
||||
header::USER_AGENT,
|
||||
format!(
|
||||
"com.google.android.youtube/{} (Linux; U; Android 12; {}) gzip",
|
||||
MOBILE_CLIENT_VERSION, self.opts.country
|
||||
),
|
||||
)
|
||||
.header("X-Goog-Api-Format-Version", "2"),
|
||||
ClientType::Ios => self
|
||||
.client
|
||||
|
@ -1242,15 +1375,9 @@ impl RustyPipeQuery {
|
|||
.post(format!(
|
||||
"{YOUTUBEI_V1_GAPIS_URL}{endpoint}?{DISABLE_PRETTY_PRINT_PARAMETER}"
|
||||
))
|
||||
.header(
|
||||
header::USER_AGENT,
|
||||
format!(
|
||||
"com.google.ios.youtube/{} ({}; U; CPU iOS 15_4 like Mac OS X; {})",
|
||||
MOBILE_CLIENT_VERSION, IOS_DEVICE_MODEL, self.opts.country
|
||||
),
|
||||
)
|
||||
.header("X-Goog-Api-Format-Version", "2"),
|
||||
};
|
||||
r = r.header(header::USER_AGENT, self.user_agent(ctype).as_ref());
|
||||
if let Some(vdata) = self.opts.visitor_data.as_deref().or(visitor_data) {
|
||||
r = r.header("X-Goog-EOM-Visitor-Id", vdata);
|
||||
}
|
||||
|
@ -1268,9 +1395,7 @@ impl RustyPipeQuery {
|
|||
async fn yt_request_attempt<R: DeserializeOwned + MapResponse<M> + Debug, M>(
|
||||
&self,
|
||||
request: &Request,
|
||||
id: &str,
|
||||
visitor_data: Option<&str>,
|
||||
deobf: Option<&DeobfData>,
|
||||
ctx: &MapRespCtx<'_>,
|
||||
) -> Result<RequestResult<M>, Error> {
|
||||
let response = self
|
||||
.client
|
||||
|
@ -1289,7 +1414,7 @@ impl RustyPipeQuery {
|
|||
|
||||
Err(match status {
|
||||
StatusCode::NOT_FOUND => Error::Extraction(ExtractionError::NotFound {
|
||||
id: id.to_owned(),
|
||||
id: ctx.id.to_owned(),
|
||||
msg: error_msg.unwrap_or("404".into()),
|
||||
}),
|
||||
StatusCode::BAD_REQUEST => {
|
||||
|
@ -1299,12 +1424,7 @@ impl RustyPipeQuery {
|
|||
})
|
||||
} else {
|
||||
match serde_json::from_str::<R>(&body) {
|
||||
Ok(deserialized) => match deserialized.map_response(
|
||||
id,
|
||||
self.opts.lang,
|
||||
deobf,
|
||||
self.opts.visitor_data.as_deref().or(visitor_data),
|
||||
) {
|
||||
Ok(deserialized) => match deserialized.map_response(ctx) {
|
||||
Ok(mapres) => Ok(mapres),
|
||||
Err(e) => Err(e.into()),
|
||||
},
|
||||
|
@ -1320,15 +1440,11 @@ impl RustyPipeQuery {
|
|||
async fn yt_request<R: DeserializeOwned + MapResponse<M> + Debug, M>(
|
||||
&self,
|
||||
request: &Request,
|
||||
id: &str,
|
||||
visitor_data: Option<&str>,
|
||||
deobf: Option<&DeobfData>,
|
||||
ctx: &MapRespCtx<'_>,
|
||||
) -> Result<RequestResult<M>, Error> {
|
||||
let mut last_resp = None;
|
||||
for n in 0..=self.client.inner.n_http_retries {
|
||||
let resp = self
|
||||
.yt_request_attempt::<R, M>(request, id, visitor_data, deobf)
|
||||
.await?;
|
||||
let resp = self.yt_request_attempt::<R, M>(request, ctx).await?;
|
||||
|
||||
let err = match &resp.res {
|
||||
Ok(_) => return Ok(resp),
|
||||
|
@ -1394,9 +1510,15 @@ impl RustyPipeQuery {
|
|||
.json(body)
|
||||
.build()?;
|
||||
|
||||
let req_res = self
|
||||
.yt_request::<R, M>(&request, id, visitor_data, deobf)
|
||||
.await?;
|
||||
let ctx = MapRespCtx {
|
||||
id,
|
||||
lang: self.opts.lang,
|
||||
deobf,
|
||||
visitor_data,
|
||||
client_type: ctype,
|
||||
};
|
||||
|
||||
let req_res = self.yt_request::<R, M>(&request, &ctx).await?;
|
||||
|
||||
// Uncomment to debug response text
|
||||
// println!("{}", &req_res.body);
|
||||
|
@ -1553,6 +1675,28 @@ impl AsRef<RustyPipeQuery> for RustyPipeQuery {
|
|||
}
|
||||
}
|
||||
|
||||
struct MapRespCtx<'a> {
|
||||
id: &'a str,
|
||||
lang: Language,
|
||||
deobf: Option<&'a DeobfData>,
|
||||
visitor_data: Option<&'a str>,
|
||||
client_type: ClientType,
|
||||
}
|
||||
|
||||
impl<'a> MapRespCtx<'a> {
|
||||
/// Create a [`MapRespCtx`] for testing
|
||||
#[cfg(test)]
|
||||
fn test(id: &'a str) -> Self {
|
||||
Self {
|
||||
id,
|
||||
lang: Language::En,
|
||||
deobf: None,
|
||||
visitor_data: None,
|
||||
client_type: ClientType::Desktop,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Implement this for YouTube API response structs that need to be mapped to
|
||||
/// RustyPipe models.
|
||||
trait MapResponse<T> {
|
||||
|
@ -1569,13 +1713,7 @@ trait MapResponse<T> {
|
|||
/// - `lang`: Language of the request. Used for mapping localized information like dates.
|
||||
/// - `deobf`: Deobfuscator (if passed to the `execute_request_deobf` method)
|
||||
/// - `visitor_data`: Visitor data option of the client
|
||||
fn map_response(
|
||||
self,
|
||||
id: &str,
|
||||
lang: Language,
|
||||
deobf: Option<&DeobfData>,
|
||||
visitor_data: Option<&str>,
|
||||
) -> Result<MapResult<T>, ExtractionError>;
|
||||
fn map_response(self, ctx: &MapRespCtx<'_>) -> Result<MapResult<T>, ExtractionError>;
|
||||
}
|
||||
|
||||
fn validate_country(country: Country) -> Country {
|
||||
|
@ -1600,21 +1738,28 @@ mod tests {
|
|||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn t_extract_desktop_client_version() {
|
||||
async fn extract_desktop_client_version() {
|
||||
let rp = RustyPipe::new();
|
||||
let version = rp.extract_desktop_client_version().await.unwrap();
|
||||
assert!(get_major_version(&version) >= 2);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn t_extract_music_client_version() {
|
||||
async fn extract_music_client_version() {
|
||||
let rp = RustyPipe::new();
|
||||
let version = rp.extract_music_client_version().await.unwrap();
|
||||
assert!(get_major_version(&version) >= 1);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn t_get_visitor_data() {
|
||||
async fn extract_tv_client_version() {
|
||||
let rp = RustyPipe::new();
|
||||
let version = rp.extract_tv_client_version().await.unwrap();
|
||||
assert!(get_major_version(&version) >= 7);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn get_visitor_data() {
|
||||
let rp = RustyPipe::new();
|
||||
let visitor_data = rp.get_visitor_data().await.unwrap();
|
||||
|
||||
|
|
|
@ -14,7 +14,7 @@ use crate::{
|
|||
|
||||
use super::{
|
||||
response::{self, music_item::MusicListMapper, url_endpoint::PageType},
|
||||
ClientType, MapResponse, QBrowse, RustyPipeQuery,
|
||||
ClientType, MapRespCtx, MapResponse, QBrowse, RustyPipeQuery,
|
||||
};
|
||||
|
||||
impl RustyPipeQuery {
|
||||
|
@ -92,14 +92,8 @@ impl RustyPipeQuery {
|
|||
}
|
||||
|
||||
impl MapResponse<MusicArtist> for response::MusicArtist {
|
||||
fn map_response(
|
||||
self,
|
||||
id: &str,
|
||||
lang: crate::param::Language,
|
||||
_deobf: Option<&crate::deobfuscate::DeobfData>,
|
||||
_vdata: Option<&str>,
|
||||
) -> Result<MapResult<MusicArtist>, ExtractionError> {
|
||||
let mapped = map_artist_page(self, id, lang, false)?;
|
||||
fn map_response(self, ctx: &MapRespCtx<'_>) -> Result<MapResult<MusicArtist>, ExtractionError> {
|
||||
let mapped = map_artist_page(self, ctx, false)?;
|
||||
Ok(MapResult {
|
||||
c: mapped.c.0,
|
||||
warnings: mapped.warnings,
|
||||
|
@ -110,19 +104,15 @@ impl MapResponse<MusicArtist> for response::MusicArtist {
|
|||
impl MapResponse<(MusicArtist, bool)> for response::MusicArtist {
|
||||
fn map_response(
|
||||
self,
|
||||
id: &str,
|
||||
lang: crate::param::Language,
|
||||
_deobf: Option<&crate::deobfuscate::DeobfData>,
|
||||
_vdata: Option<&str>,
|
||||
ctx: &MapRespCtx<'_>,
|
||||
) -> Result<MapResult<(MusicArtist, bool)>, ExtractionError> {
|
||||
map_artist_page(self, id, lang, true)
|
||||
map_artist_page(self, ctx, true)
|
||||
}
|
||||
}
|
||||
|
||||
fn map_artist_page(
|
||||
res: response::MusicArtist,
|
||||
id: &str,
|
||||
lang: crate::param::Language,
|
||||
ctx: &MapRespCtx<'_>,
|
||||
skip_extendables: bool,
|
||||
) -> Result<MapResult<(MusicArtist, bool)>, ExtractionError> {
|
||||
// dbg!(&res);
|
||||
|
@ -138,7 +128,7 @@ fn map_artist_page(
|
|||
.and_then(|pb| util::string_from_pb(pb, 3));
|
||||
|
||||
if let Some(share_channel_id) = share_channel_id {
|
||||
if share_channel_id != id {
|
||||
if share_channel_id != ctx.id {
|
||||
return Err(ExtractionError::Redirect(share_channel_id));
|
||||
}
|
||||
}
|
||||
|
@ -155,9 +145,9 @@ fn map_artist_page(
|
|||
.unwrap_or_default();
|
||||
|
||||
let mut mapper = MusicListMapper::with_artist(
|
||||
lang,
|
||||
ctx.lang,
|
||||
ArtistId {
|
||||
id: Some(id.to_owned()),
|
||||
id: Some(ctx.id.to_owned()),
|
||||
name: header.title.clone(),
|
||||
},
|
||||
);
|
||||
|
@ -264,7 +254,7 @@ fn map_artist_page(
|
|||
Ok(MapResult {
|
||||
c: (
|
||||
MusicArtist {
|
||||
id: id.to_owned(),
|
||||
id: ctx.id.to_owned(),
|
||||
name: header.title,
|
||||
header_image: header.thumbnail.into(),
|
||||
description: header.description,
|
||||
|
@ -272,7 +262,7 @@ fn map_artist_page(
|
|||
subscriber_count: header.subscription_button.and_then(|btn| {
|
||||
util::parse_large_numstr_or_warn(
|
||||
&btn.subscribe_button_renderer.subscriber_count_text,
|
||||
lang,
|
||||
ctx.lang,
|
||||
&mut mapped.warnings,
|
||||
)
|
||||
}),
|
||||
|
@ -293,16 +283,13 @@ fn map_artist_page(
|
|||
impl MapResponse<Vec<AlbumItem>> for response::MusicArtistAlbums {
|
||||
fn map_response(
|
||||
self,
|
||||
id: &str,
|
||||
lang: crate::param::Language,
|
||||
_deobf: Option<&crate::deobfuscate::DeobfData>,
|
||||
_vdata: Option<&str>,
|
||||
ctx: &MapRespCtx<'_>,
|
||||
) -> Result<MapResult<Vec<AlbumItem>>, ExtractionError> {
|
||||
// dbg!(&self);
|
||||
|
||||
let Some(header) = self.header else {
|
||||
return Err(ExtractionError::NotFound {
|
||||
id: id.into(),
|
||||
id: ctx.id.into(),
|
||||
msg: "no header".into(),
|
||||
});
|
||||
};
|
||||
|
@ -320,9 +307,9 @@ impl MapResponse<Vec<AlbumItem>> for response::MusicArtistAlbums {
|
|||
.contents;
|
||||
|
||||
let mut mapper = MusicListMapper::with_artist(
|
||||
lang,
|
||||
ctx.lang,
|
||||
ArtistId {
|
||||
id: Some(id.to_owned()),
|
||||
id: Some(ctx.id.to_owned()),
|
||||
name: header.music_header_renderer.title,
|
||||
},
|
||||
);
|
||||
|
@ -347,7 +334,7 @@ mod tests {
|
|||
use path_macro::path;
|
||||
use rstest::rstest;
|
||||
|
||||
use crate::{param::Language, util::tests::TESTFILES};
|
||||
use crate::util::tests::TESTFILES;
|
||||
|
||||
use super::*;
|
||||
|
||||
|
@ -369,7 +356,7 @@ mod tests {
|
|||
let resp: response::MusicArtist =
|
||||
serde_json::from_reader(BufReader::new(json_file)).unwrap();
|
||||
let map_res: MapResult<(MusicArtist, bool)> =
|
||||
resp.map_response(id, Language::En, None, None).unwrap();
|
||||
resp.map_response(&MapRespCtx::test(id)).unwrap();
|
||||
let (mut artist, can_fetch_more) = map_res.c;
|
||||
|
||||
assert!(
|
||||
|
@ -384,7 +371,7 @@ mod tests {
|
|||
let resp: response::MusicArtistAlbums =
|
||||
serde_json::from_reader(BufReader::new(json_file)).unwrap();
|
||||
let mut map_res: MapResult<Vec<AlbumItem>> =
|
||||
resp.map_response(id, Language::En, None, None).unwrap();
|
||||
resp.map_response(&MapRespCtx::test(id)).unwrap();
|
||||
|
||||
assert!(
|
||||
map_res.warnings.is_empty(),
|
||||
|
@ -405,7 +392,7 @@ mod tests {
|
|||
let artist: response::MusicArtist =
|
||||
serde_json::from_reader(BufReader::new(json_file)).unwrap();
|
||||
let map_res: MapResult<MusicArtist> = artist
|
||||
.map_response("UClmXPfaYhXOYsNn_QUyheWQ", Language::En, None, None)
|
||||
.map_response(&MapRespCtx::test("UClmXPfaYhXOYsNn_QUyheWQ"))
|
||||
.unwrap();
|
||||
|
||||
assert!(
|
||||
|
@ -424,7 +411,7 @@ mod tests {
|
|||
let artist: response::MusicArtist =
|
||||
serde_json::from_reader(BufReader::new(json_file)).unwrap();
|
||||
let res: Result<MapResult<MusicArtist>, ExtractionError> =
|
||||
artist.map_response("UCLkAepWjdylmXSltofFvsYQ", Language::En, None, None);
|
||||
artist.map_response(&MapRespCtx::test("UCLkAepWjdylmXSltofFvsYQ"));
|
||||
let e = res.unwrap_err();
|
||||
|
||||
match e {
|
||||
|
|
|
@ -11,7 +11,7 @@ use crate::{
|
|||
|
||||
use super::{
|
||||
response::{self, music_item::MusicListMapper, url_endpoint::MusicPageType},
|
||||
ClientType, MapResponse, RustyPipeQuery, YTContext,
|
||||
ClientType, MapRespCtx, MapResponse, RustyPipeQuery, YTContext,
|
||||
};
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
|
@ -32,7 +32,7 @@ struct FormData {
|
|||
|
||||
impl RustyPipeQuery {
|
||||
/// Get the YouTube Music charts for a given country
|
||||
#[tracing::instrument(skip(self))]
|
||||
#[tracing::instrument(skip(self), level = "error")]
|
||||
pub async fn music_charts(&self, country: Option<Country>) -> Result<MusicCharts, Error> {
|
||||
let context = self.get_context(ClientType::DesktopMusic, true, None).await;
|
||||
let request_body = QCharts {
|
||||
|
@ -56,13 +56,7 @@ impl RustyPipeQuery {
|
|||
}
|
||||
|
||||
impl MapResponse<MusicCharts> for response::MusicCharts {
|
||||
fn map_response(
|
||||
self,
|
||||
_id: &str,
|
||||
lang: crate::param::Language,
|
||||
_deobf: Option<&crate::deobfuscate::DeobfData>,
|
||||
_vdata: Option<&str>,
|
||||
) -> Result<crate::serializer::MapResult<MusicCharts>, crate::error::ExtractionError> {
|
||||
fn map_response(self, ctx: &MapRespCtx<'_>) -> Result<MapResult<MusicCharts>, ExtractionError> {
|
||||
let countries = self
|
||||
.framework_updates
|
||||
.map(|fwu| {
|
||||
|
@ -77,9 +71,9 @@ impl MapResponse<MusicCharts> for response::MusicCharts {
|
|||
let mut top_playlist_id = None;
|
||||
let mut trending_playlist_id = None;
|
||||
|
||||
let mut mapper_top = MusicListMapper::new(lang);
|
||||
let mut mapper_trending = MusicListMapper::new(lang);
|
||||
let mut mapper_other = MusicListMapper::new(lang);
|
||||
let mut mapper_top = MusicListMapper::new(ctx.lang);
|
||||
let mut mapper_trending = MusicListMapper::new(ctx.lang);
|
||||
let mut mapper_other = MusicListMapper::new(ctx.lang);
|
||||
|
||||
self.contents
|
||||
.single_column_browse_results_renderer
|
||||
|
@ -151,7 +145,6 @@ mod tests {
|
|||
use rstest::rstest;
|
||||
|
||||
use super::*;
|
||||
use crate::param::Language;
|
||||
|
||||
#[rstest]
|
||||
#[case::default("global")]
|
||||
|
@ -163,8 +156,7 @@ mod tests {
|
|||
|
||||
let charts: response::MusicCharts =
|
||||
serde_json::from_reader(BufReader::new(json_file)).unwrap();
|
||||
let map_res: MapResult<MusicCharts> =
|
||||
charts.map_response("", Language::En, None, None).unwrap();
|
||||
let map_res: MapResult<MusicCharts> = charts.map_response(&MapRespCtx::test("")).unwrap();
|
||||
|
||||
assert!(
|
||||
map_res.warnings.is_empty(),
|
||||
|
|
|
@ -8,7 +8,6 @@ use crate::{
|
|||
paginator::{ContinuationEndpoint, Paginator},
|
||||
ArtistId, Lyrics, MusicRelated, TrackDetails, TrackItem,
|
||||
},
|
||||
param::Language,
|
||||
serializer::MapResult,
|
||||
};
|
||||
|
||||
|
@ -17,7 +16,7 @@ use super::{
|
|||
self,
|
||||
music_item::{map_queue_item, MusicListMapper},
|
||||
},
|
||||
ClientType, MapResponse, QBrowse, RustyPipeQuery, YTContext,
|
||||
ClientType, MapRespCtx, MapResponse, QBrowse, RustyPipeQuery, YTContext,
|
||||
};
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
|
@ -41,7 +40,7 @@ struct QRadio<'a> {
|
|||
|
||||
impl RustyPipeQuery {
|
||||
/// Get the metadata of a YouTube music track
|
||||
#[tracing::instrument(skip(self))]
|
||||
#[tracing::instrument(skip(self), level = "error")]
|
||||
pub async fn music_details<S: AsRef<str> + Debug>(
|
||||
&self,
|
||||
video_id: S,
|
||||
|
@ -69,7 +68,7 @@ impl RustyPipeQuery {
|
|||
/// Get the lyrics of a YouTube music track
|
||||
///
|
||||
/// The `lyrics_id` has to be obtained using [`RustyPipeQuery::music_details`].
|
||||
#[tracing::instrument(skip(self))]
|
||||
#[tracing::instrument(skip(self), level = "error")]
|
||||
pub async fn music_lyrics<S: AsRef<str> + Debug>(&self, lyrics_id: S) -> Result<Lyrics, Error> {
|
||||
let lyrics_id = lyrics_id.as_ref();
|
||||
let context = self.get_context(ClientType::DesktopMusic, true, None).await;
|
||||
|
@ -91,7 +90,7 @@ impl RustyPipeQuery {
|
|||
/// Get related items (tracks, playlists, artists) to a YouTube Music track
|
||||
///
|
||||
/// The `related_id` has to be obtained using [`RustyPipeQuery::music_details`].
|
||||
#[tracing::instrument(skip(self))]
|
||||
#[tracing::instrument(skip(self), level = "error")]
|
||||
pub async fn music_related<S: AsRef<str> + Debug>(
|
||||
&self,
|
||||
related_id: S,
|
||||
|
@ -116,7 +115,7 @@ impl RustyPipeQuery {
|
|||
/// Get a YouTube Music radio (a dynamically generated playlist)
|
||||
///
|
||||
/// The `radio_id` can be obtained using [`RustyPipeQuery::music_artist`] to get an artist's radio.
|
||||
#[tracing::instrument(skip(self))]
|
||||
#[tracing::instrument(skip(self), level = "error")]
|
||||
pub async fn music_radio<S: AsRef<str> + Debug>(
|
||||
&self,
|
||||
radio_id: S,
|
||||
|
@ -147,7 +146,7 @@ impl RustyPipeQuery {
|
|||
}
|
||||
|
||||
/// Get a YouTube Music radio (a dynamically generated playlist) for a track
|
||||
#[tracing::instrument(skip(self))]
|
||||
#[tracing::instrument(skip(self), level = "error")]
|
||||
pub async fn music_radio_track<S: AsRef<str> + Debug>(
|
||||
&self,
|
||||
video_id: S,
|
||||
|
@ -157,7 +156,7 @@ impl RustyPipeQuery {
|
|||
}
|
||||
|
||||
/// Get a YouTube Music radio (a dynamically generated playlist) for a playlist
|
||||
#[tracing::instrument(skip(self))]
|
||||
#[tracing::instrument(skip(self), level = "error")]
|
||||
pub async fn music_radio_playlist<S: AsRef<str> + Debug>(
|
||||
&self,
|
||||
playlist_id: S,
|
||||
|
@ -170,10 +169,7 @@ impl RustyPipeQuery {
|
|||
impl MapResponse<TrackDetails> for response::MusicDetails {
|
||||
fn map_response(
|
||||
self,
|
||||
id: &str,
|
||||
lang: Language,
|
||||
_deobf: Option<&crate::deobfuscate::DeobfData>,
|
||||
_vdata: Option<&str>,
|
||||
ctx: &MapRespCtx<'_>,
|
||||
) -> Result<MapResult<TrackDetails>, ExtractionError> {
|
||||
let tabs = self
|
||||
.contents
|
||||
|
@ -211,7 +207,7 @@ impl MapResponse<TrackDetails> for response::MusicDetails {
|
|||
}
|
||||
|
||||
let content = content.ok_or_else(|| ExtractionError::NotFound {
|
||||
id: id.to_owned(),
|
||||
id: ctx.id.to_owned(),
|
||||
msg: "no content".into(),
|
||||
})?;
|
||||
let track_item = content
|
||||
|
@ -225,7 +221,7 @@ impl MapResponse<TrackDetails> for response::MusicDetails {
|
|||
response::music_item::PlaylistPanelVideo::None => None,
|
||||
})
|
||||
.ok_or(ExtractionError::InvalidData(Cow::Borrowed("no video item")))?;
|
||||
let mut track = map_queue_item(track_item, lang);
|
||||
let mut track = map_queue_item(track_item, ctx.lang);
|
||||
|
||||
let mut warnings = content.contents.warnings;
|
||||
warnings.append(&mut track.warnings);
|
||||
|
@ -244,10 +240,7 @@ impl MapResponse<TrackDetails> for response::MusicDetails {
|
|||
impl MapResponse<Paginator<TrackItem>> for response::MusicDetails {
|
||||
fn map_response(
|
||||
self,
|
||||
id: &str,
|
||||
lang: Language,
|
||||
_deobf: Option<&crate::deobfuscate::DeobfData>,
|
||||
_vdata: Option<&str>,
|
||||
ctx: &MapRespCtx<'_>,
|
||||
) -> Result<MapResult<Paginator<TrackItem>>, ExtractionError> {
|
||||
let tabs = self
|
||||
.contents
|
||||
|
@ -260,7 +253,7 @@ impl MapResponse<Paginator<TrackItem>> for response::MusicDetails {
|
|||
.into_iter()
|
||||
.find_map(|t| t.tab_renderer.content)
|
||||
.ok_or_else(|| ExtractionError::NotFound {
|
||||
id: id.to_owned(),
|
||||
id: ctx.id.to_owned(),
|
||||
msg: "no content".into(),
|
||||
})?
|
||||
.music_queue_renderer
|
||||
|
@ -275,7 +268,7 @@ impl MapResponse<Paginator<TrackItem>> for response::MusicDetails {
|
|||
.into_iter()
|
||||
.filter_map(|item| match item {
|
||||
response::music_item::PlaylistPanelVideo::PlaylistPanelVideoRenderer(item) => {
|
||||
let mut track = map_queue_item(item, lang);
|
||||
let mut track = map_queue_item(item, ctx.lang);
|
||||
warnings.append(&mut track.warnings);
|
||||
Some(track.c)
|
||||
}
|
||||
|
@ -297,18 +290,12 @@ impl MapResponse<Paginator<TrackItem>> for response::MusicDetails {
|
|||
}
|
||||
|
||||
impl MapResponse<Lyrics> for response::MusicLyrics {
|
||||
fn map_response(
|
||||
self,
|
||||
id: &str,
|
||||
_lang: Language,
|
||||
_deobf: Option<&crate::deobfuscate::DeobfData>,
|
||||
_vdata: Option<&str>,
|
||||
) -> Result<MapResult<Lyrics>, ExtractionError> {
|
||||
fn map_response(self, ctx: &MapRespCtx<'_>) -> Result<MapResult<Lyrics>, ExtractionError> {
|
||||
let lyrics = self
|
||||
.contents
|
||||
.into_res()
|
||||
.map_err(|msg| ExtractionError::NotFound {
|
||||
id: id.to_owned(),
|
||||
id: ctx.id.to_owned(),
|
||||
msg: msg.into(),
|
||||
})?
|
||||
.into_iter()
|
||||
|
@ -328,16 +315,13 @@ impl MapResponse<Lyrics> for response::MusicLyrics {
|
|||
impl MapResponse<MusicRelated> for response::MusicRelated {
|
||||
fn map_response(
|
||||
self,
|
||||
id: &str,
|
||||
lang: Language,
|
||||
_deobf: Option<&crate::deobfuscate::DeobfData>,
|
||||
_vdata: Option<&str>,
|
||||
ctx: &MapRespCtx<'_>,
|
||||
) -> Result<MapResult<MusicRelated>, ExtractionError> {
|
||||
let contents = self
|
||||
.contents
|
||||
.into_res()
|
||||
.map_err(|msg| ExtractionError::NotFound {
|
||||
id: id.to_owned(),
|
||||
id: ctx.id.to_owned(),
|
||||
msg: msg.into(),
|
||||
})?;
|
||||
|
||||
|
@ -362,10 +346,10 @@ impl MapResponse<MusicRelated> for response::MusicRelated {
|
|||
_ => None,
|
||||
});
|
||||
|
||||
let mut mapper_tracks = MusicListMapper::new(lang);
|
||||
let mut mapper_tracks = MusicListMapper::new(ctx.lang);
|
||||
let mut mapper = match artist_id {
|
||||
Some(artist_id) => MusicListMapper::with_artist(lang, artist_id),
|
||||
None => MusicListMapper::new(lang),
|
||||
Some(artist_id) => MusicListMapper::with_artist(ctx.lang, artist_id),
|
||||
None => MusicListMapper::new(ctx.lang),
|
||||
};
|
||||
|
||||
let mut sections = contents.into_iter();
|
||||
|
@ -412,7 +396,7 @@ mod tests {
|
|||
use rstest::rstest;
|
||||
|
||||
use super::*;
|
||||
use crate::{model, param::Language, util::tests::TESTFILES};
|
||||
use crate::{model, util::tests::TESTFILES};
|
||||
|
||||
#[rstest]
|
||||
#[case::mv("mv", "ZeerrnuLi5E")]
|
||||
|
@ -424,7 +408,7 @@ mod tests {
|
|||
let details: response::MusicDetails =
|
||||
serde_json::from_reader(BufReader::new(json_file)).unwrap();
|
||||
let map_res: MapResult<model::TrackDetails> =
|
||||
details.map_response(id, Language::En, None, None).unwrap();
|
||||
details.map_response(&MapRespCtx::test(id)).unwrap();
|
||||
|
||||
assert!(
|
||||
map_res.warnings.is_empty(),
|
||||
|
@ -444,7 +428,7 @@ mod tests {
|
|||
let radio: response::MusicDetails =
|
||||
serde_json::from_reader(BufReader::new(json_file)).unwrap();
|
||||
let map_res: MapResult<Paginator<TrackItem>> =
|
||||
radio.map_response(id, Language::En, None, None).unwrap();
|
||||
radio.map_response(&MapRespCtx::test(id)).unwrap();
|
||||
|
||||
assert!(
|
||||
map_res.warnings.is_empty(),
|
||||
|
@ -461,7 +445,7 @@ mod tests {
|
|||
|
||||
let lyrics: response::MusicLyrics =
|
||||
serde_json::from_reader(BufReader::new(json_file)).unwrap();
|
||||
let map_res: MapResult<Lyrics> = lyrics.map_response("", Language::En, None, None).unwrap();
|
||||
let map_res: MapResult<Lyrics> = lyrics.map_response(&MapRespCtx::test("")).unwrap();
|
||||
|
||||
assert!(
|
||||
map_res.warnings.is_empty(),
|
||||
|
@ -478,8 +462,7 @@ mod tests {
|
|||
|
||||
let lyrics: response::MusicRelated =
|
||||
serde_json::from_reader(BufReader::new(json_file)).unwrap();
|
||||
let map_res: MapResult<MusicRelated> =
|
||||
lyrics.map_response("", Language::En, None, None).unwrap();
|
||||
let map_res: MapResult<MusicRelated> = lyrics.map_response(&MapRespCtx::test("")).unwrap();
|
||||
|
||||
assert!(
|
||||
map_res.warnings.is_empty(),
|
||||
|
|
|
@ -8,12 +8,12 @@ use crate::{
|
|||
|
||||
use super::{
|
||||
response::{self, music_item::MusicListMapper, url_endpoint::NavigationEndpoint},
|
||||
ClientType, MapResponse, QBrowse, QBrowseParams, RustyPipeQuery,
|
||||
ClientType, MapRespCtx, MapResponse, QBrowse, QBrowseParams, RustyPipeQuery,
|
||||
};
|
||||
|
||||
impl RustyPipeQuery {
|
||||
/// Get a list of moods and genres from YouTube Music
|
||||
#[tracing::instrument(skip(self))]
|
||||
#[tracing::instrument(skip(self), level = "error")]
|
||||
pub async fn music_genres(&self) -> Result<Vec<MusicGenreItem>, Error> {
|
||||
let context = self.get_context(ClientType::DesktopMusic, true, None).await;
|
||||
let request_body = QBrowse {
|
||||
|
@ -32,7 +32,7 @@ impl RustyPipeQuery {
|
|||
}
|
||||
|
||||
/// Get the playlists from a YouTube Music genre
|
||||
#[tracing::instrument(skip(self))]
|
||||
#[tracing::instrument(skip(self), level = "error")]
|
||||
pub async fn music_genre<S: AsRef<str> + Debug>(
|
||||
&self,
|
||||
genre_id: S,
|
||||
|
@ -59,11 +59,8 @@ impl RustyPipeQuery {
|
|||
impl MapResponse<Vec<MusicGenreItem>> for response::MusicGenres {
|
||||
fn map_response(
|
||||
self,
|
||||
_id: &str,
|
||||
_lang: crate::param::Language,
|
||||
_deobf: Option<&crate::deobfuscate::DeobfData>,
|
||||
_vdata: Option<&str>,
|
||||
) -> Result<crate::serializer::MapResult<Vec<MusicGenreItem>>, ExtractionError> {
|
||||
_ctx: &MapRespCtx<'_>,
|
||||
) -> Result<MapResult<Vec<MusicGenreItem>>, ExtractionError> {
|
||||
let content = self
|
||||
.contents
|
||||
.single_column_browse_results_renderer
|
||||
|
@ -111,13 +108,7 @@ impl MapResponse<Vec<MusicGenreItem>> for response::MusicGenres {
|
|||
}
|
||||
|
||||
impl MapResponse<MusicGenre> for response::MusicGenre {
|
||||
fn map_response(
|
||||
self,
|
||||
id: &str,
|
||||
lang: crate::param::Language,
|
||||
_deobf: Option<&crate::deobfuscate::DeobfData>,
|
||||
_vdata: Option<&str>,
|
||||
) -> Result<crate::serializer::MapResult<MusicGenre>, ExtractionError> {
|
||||
fn map_response(self, ctx: &MapRespCtx<'_>) -> Result<MapResult<MusicGenre>, ExtractionError> {
|
||||
// dbg!(&self);
|
||||
|
||||
let content = self
|
||||
|
@ -179,7 +170,7 @@ impl MapResponse<MusicGenre> for response::MusicGenre {
|
|||
_ => return None,
|
||||
};
|
||||
|
||||
let mut mapper = MusicListMapper::new(lang);
|
||||
let mut mapper = MusicListMapper::new(ctx.lang);
|
||||
mapper.map_response(items);
|
||||
let mut mapped = mapper.conv_items();
|
||||
warnings.append(&mut mapped.warnings);
|
||||
|
@ -194,7 +185,7 @@ impl MapResponse<MusicGenre> for response::MusicGenre {
|
|||
|
||||
Ok(MapResult {
|
||||
c: MusicGenre {
|
||||
id: id.to_owned(),
|
||||
id: ctx.id.to_owned(),
|
||||
name: self.header.music_header_renderer.title,
|
||||
sections,
|
||||
},
|
||||
|
@ -211,7 +202,7 @@ mod tests {
|
|||
use rstest::rstest;
|
||||
|
||||
use super::*;
|
||||
use crate::{model, param::Language, util::tests::TESTFILES};
|
||||
use crate::{model, util::tests::TESTFILES};
|
||||
|
||||
#[test]
|
||||
fn map_music_genres() {
|
||||
|
@ -221,7 +212,7 @@ mod tests {
|
|||
let playlist: response::MusicGenres =
|
||||
serde_json::from_reader(BufReader::new(json_file)).unwrap();
|
||||
let map_res: MapResult<Vec<model::MusicGenreItem>> =
|
||||
playlist.map_response("", Language::En, None, None).unwrap();
|
||||
playlist.map_response(&MapRespCtx::test("")).unwrap();
|
||||
|
||||
assert!(
|
||||
map_res.warnings.is_empty(),
|
||||
|
@ -241,7 +232,7 @@ mod tests {
|
|||
let playlist: response::MusicGenre =
|
||||
serde_json::from_reader(BufReader::new(json_file)).unwrap();
|
||||
let map_res: MapResult<model::MusicGenre> =
|
||||
playlist.map_response(id, Language::En, None, None).unwrap();
|
||||
playlist.map_response(&MapRespCtx::test(id)).unwrap();
|
||||
|
||||
assert!(
|
||||
map_res.warnings.is_empty(),
|
||||
|
|
|
@ -4,13 +4,14 @@ use crate::{
|
|||
client::response::music_item::MusicListMapper,
|
||||
error::{Error, ExtractionError},
|
||||
model::{traits::FromYtItem, AlbumItem, TrackItem},
|
||||
serializer::MapResult,
|
||||
};
|
||||
|
||||
use super::{response, ClientType, MapResponse, QBrowse, RustyPipeQuery};
|
||||
use super::{response, ClientType, MapRespCtx, MapResponse, QBrowse, RustyPipeQuery};
|
||||
|
||||
impl RustyPipeQuery {
|
||||
/// Get the new albums that were released on YouTube Music
|
||||
#[tracing::instrument(skip(self))]
|
||||
#[tracing::instrument(skip(self), level = "error")]
|
||||
pub async fn music_new_albums(&self) -> Result<Vec<AlbumItem>, Error> {
|
||||
let context = self.get_context(ClientType::DesktopMusic, true, None).await;
|
||||
let request_body = QBrowse {
|
||||
|
@ -29,7 +30,7 @@ impl RustyPipeQuery {
|
|||
}
|
||||
|
||||
/// Get the new music videos that were released on YouTube Music
|
||||
#[tracing::instrument(skip(self))]
|
||||
#[tracing::instrument(skip(self), level = "error")]
|
||||
pub async fn music_new_videos(&self) -> Result<Vec<TrackItem>, Error> {
|
||||
let context = self.get_context(ClientType::DesktopMusic, true, None).await;
|
||||
let request_body = QBrowse {
|
||||
|
@ -49,13 +50,7 @@ impl RustyPipeQuery {
|
|||
}
|
||||
|
||||
impl<T: FromYtItem> MapResponse<Vec<T>> for response::MusicNew {
|
||||
fn map_response(
|
||||
self,
|
||||
_id: &str,
|
||||
lang: crate::param::Language,
|
||||
_deobf: Option<&crate::deobfuscate::DeobfData>,
|
||||
_vdata: Option<&str>,
|
||||
) -> Result<crate::serializer::MapResult<Vec<T>>, ExtractionError> {
|
||||
fn map_response(self, ctx: &MapRespCtx<'_>) -> Result<MapResult<Vec<T>>, ExtractionError> {
|
||||
let items = self
|
||||
.contents
|
||||
.single_column_browse_results_renderer
|
||||
|
@ -73,7 +68,7 @@ impl<T: FromYtItem> MapResponse<Vec<T>> for response::MusicNew {
|
|||
.grid_renderer
|
||||
.items;
|
||||
|
||||
let mut mapper = MusicListMapper::new(lang);
|
||||
let mut mapper = MusicListMapper::new(ctx.lang);
|
||||
mapper.map_response(items);
|
||||
|
||||
Ok(mapper.conv_items())
|
||||
|
@ -88,7 +83,7 @@ mod tests {
|
|||
use rstest::rstest;
|
||||
|
||||
use super::*;
|
||||
use crate::{param::Language, serializer::MapResult, util::tests::TESTFILES};
|
||||
use crate::{serializer::MapResult, util::tests::TESTFILES};
|
||||
|
||||
#[rstest]
|
||||
#[case::default("default")]
|
||||
|
@ -98,9 +93,8 @@ mod tests {
|
|||
|
||||
let new_albums: response::MusicNew =
|
||||
serde_json::from_reader(BufReader::new(json_file)).unwrap();
|
||||
let map_res: MapResult<Vec<AlbumItem>> = new_albums
|
||||
.map_response("", Language::En, None, None)
|
||||
.unwrap();
|
||||
let map_res: MapResult<Vec<AlbumItem>> =
|
||||
new_albums.map_response(&MapRespCtx::test("")).unwrap();
|
||||
|
||||
assert!(
|
||||
map_res.warnings.is_empty(),
|
||||
|
@ -119,9 +113,8 @@ mod tests {
|
|||
|
||||
let new_videos: response::MusicNew =
|
||||
serde_json::from_reader(BufReader::new(json_file)).unwrap();
|
||||
let map_res: MapResult<Vec<TrackItem>> = new_videos
|
||||
.map_response("", Language::En, None, None)
|
||||
.unwrap();
|
||||
let map_res: MapResult<Vec<TrackItem>> =
|
||||
new_videos.map_response(&MapRespCtx::test("")).unwrap();
|
||||
|
||||
assert!(
|
||||
map_res.warnings.is_empty(),
|
||||
|
|
|
@ -17,12 +17,12 @@ use super::{
|
|||
self,
|
||||
music_item::{map_album_type, map_artist_id, map_artists, MusicListMapper},
|
||||
},
|
||||
ClientType, MapResponse, QBrowse, RustyPipeQuery,
|
||||
ClientType, MapRespCtx, MapResponse, QBrowse, RustyPipeQuery,
|
||||
};
|
||||
|
||||
impl RustyPipeQuery {
|
||||
/// Get a playlist from YouTube Music
|
||||
#[tracing::instrument(skip(self))]
|
||||
#[tracing::instrument(skip(self), level = "error")]
|
||||
pub async fn music_playlist<S: AsRef<str> + Debug>(
|
||||
&self,
|
||||
playlist_id: S,
|
||||
|
@ -54,7 +54,7 @@ impl RustyPipeQuery {
|
|||
}
|
||||
|
||||
/// Get an album from YouTube Music
|
||||
#[tracing::instrument(skip(self))]
|
||||
#[tracing::instrument(skip(self), level = "error")]
|
||||
pub async fn music_album<S: AsRef<str> + Debug>(
|
||||
&self,
|
||||
album_id: S,
|
||||
|
@ -138,10 +138,7 @@ impl RustyPipeQuery {
|
|||
impl MapResponse<MusicPlaylist> for response::MusicPlaylist {
|
||||
fn map_response(
|
||||
self,
|
||||
id: &str,
|
||||
lang: crate::param::Language,
|
||||
_deobf: Option<&crate::deobfuscate::DeobfData>,
|
||||
vdata: Option<&str>,
|
||||
ctx: &MapRespCtx<'_>,
|
||||
) -> Result<MapResult<MusicPlaylist>, ExtractionError> {
|
||||
// dbg!(&self);
|
||||
|
||||
|
@ -186,14 +183,15 @@ impl MapResponse<MusicPlaylist> for response::MusicPlaylist {
|
|||
)))?;
|
||||
|
||||
if let Some(playlist_id) = shelf.playlist_id {
|
||||
if playlist_id != id {
|
||||
if playlist_id != ctx.id {
|
||||
return Err(ExtractionError::WrongResult(format!(
|
||||
"got wrong playlist id {playlist_id}, expected {id}"
|
||||
"got wrong playlist id {}, expected {}",
|
||||
playlist_id, ctx.id
|
||||
)));
|
||||
}
|
||||
}
|
||||
|
||||
let mut mapper = MusicListMapper::new(lang);
|
||||
let mut mapper = MusicListMapper::new(ctx.lang);
|
||||
mapper.map_response(shelf.contents);
|
||||
let map_res = mapper.conv_items();
|
||||
|
||||
|
@ -273,7 +271,7 @@ impl MapResponse<MusicPlaylist> for response::MusicPlaylist {
|
|||
|
||||
Ok(MapResult {
|
||||
c: MusicPlaylist {
|
||||
id: id.to_owned(),
|
||||
id: ctx.id.to_owned(),
|
||||
name,
|
||||
thumbnail,
|
||||
channel,
|
||||
|
@ -284,14 +282,14 @@ impl MapResponse<MusicPlaylist> for response::MusicPlaylist {
|
|||
track_count,
|
||||
map_res.c,
|
||||
ctoken,
|
||||
vdata.map(str::to_owned),
|
||||
ctx.visitor_data.map(str::to_owned),
|
||||
ContinuationEndpoint::MusicBrowse,
|
||||
),
|
||||
related_playlists: Paginator::new_ext(
|
||||
None,
|
||||
Vec::new(),
|
||||
related_ctoken,
|
||||
vdata.map(str::to_owned),
|
||||
ctx.visitor_data.map(str::to_owned),
|
||||
ContinuationEndpoint::MusicBrowse,
|
||||
),
|
||||
},
|
||||
|
@ -301,13 +299,7 @@ impl MapResponse<MusicPlaylist> for response::MusicPlaylist {
|
|||
}
|
||||
|
||||
impl MapResponse<MusicAlbum> for response::MusicPlaylist {
|
||||
fn map_response(
|
||||
self,
|
||||
id: &str,
|
||||
lang: crate::param::Language,
|
||||
_deobf: Option<&crate::deobfuscate::DeobfData>,
|
||||
_vdata: Option<&str>,
|
||||
) -> Result<MapResult<MusicAlbum>, ExtractionError> {
|
||||
fn map_response(self, ctx: &MapRespCtx<'_>) -> Result<MapResult<MusicAlbum>, ExtractionError> {
|
||||
// dbg!(&self);
|
||||
|
||||
let (header, sections) = match self.contents {
|
||||
|
@ -401,7 +393,7 @@ impl MapResponse<MusicAlbum> for response::MusicPlaylist {
|
|||
.map(|part| part.to_string())
|
||||
.unwrap_or_default();
|
||||
|
||||
let album_type = map_album_type(album_type_txt.as_str(), lang);
|
||||
let album_type = map_album_type(album_type_txt.as_str(), ctx.lang);
|
||||
let year = year_txt.and_then(|txt| util::parse_numeric(&txt).ok());
|
||||
|
||||
fn map_playlist_id(ep: &NavigationEndpoint) -> Option<String> {
|
||||
|
@ -448,11 +440,11 @@ impl MapResponse<MusicAlbum> for response::MusicPlaylist {
|
|||
let artist_id = artist_id.or_else(|| artists.first().and_then(|a| a.id.clone()));
|
||||
|
||||
let mut mapper = MusicListMapper::with_album(
|
||||
lang,
|
||||
ctx.lang,
|
||||
artists.clone(),
|
||||
by_va,
|
||||
AlbumId {
|
||||
id: id.to_owned(),
|
||||
id: ctx.id.to_owned(),
|
||||
name: header.title.clone(),
|
||||
},
|
||||
);
|
||||
|
@ -460,7 +452,7 @@ impl MapResponse<MusicAlbum> for response::MusicPlaylist {
|
|||
let tracks_res = mapper.conv_items();
|
||||
let mut warnings = tracks_res.warnings;
|
||||
|
||||
let mut variants_mapper = MusicListMapper::new(lang);
|
||||
let mut variants_mapper = MusicListMapper::new(ctx.lang);
|
||||
if let Some(res) = album_variants {
|
||||
variants_mapper.map_response(res);
|
||||
}
|
||||
|
@ -469,7 +461,7 @@ impl MapResponse<MusicAlbum> for response::MusicPlaylist {
|
|||
|
||||
Ok(MapResult {
|
||||
c: MusicAlbum {
|
||||
id: id.to_owned(),
|
||||
id: ctx.id.to_owned(),
|
||||
playlist_id,
|
||||
name: header.title,
|
||||
cover: header.thumbnail.into(),
|
||||
|
@ -497,7 +489,7 @@ mod tests {
|
|||
use rstest::rstest;
|
||||
|
||||
use super::*;
|
||||
use crate::{model, param::Language, util::tests::TESTFILES};
|
||||
use crate::{model, util::tests::TESTFILES};
|
||||
|
||||
#[rstest]
|
||||
#[case::short("short", "RDCLAK5uy_kFQXdnqMaQCVx2wpUM4ZfbsGCDibZtkJk")]
|
||||
|
@ -512,7 +504,7 @@ mod tests {
|
|||
let playlist: response::MusicPlaylist =
|
||||
serde_json::from_reader(BufReader::new(json_file)).unwrap();
|
||||
let map_res: MapResult<model::MusicPlaylist> =
|
||||
playlist.map_response(id, Language::En, None, None).unwrap();
|
||||
playlist.map_response(&MapRespCtx::test(id)).unwrap();
|
||||
|
||||
assert!(
|
||||
map_res.warnings.is_empty(),
|
||||
|
@ -539,7 +531,7 @@ mod tests {
|
|||
let playlist: response::MusicPlaylist =
|
||||
serde_json::from_reader(BufReader::new(json_file)).unwrap();
|
||||
let map_res: MapResult<model::MusicAlbum> =
|
||||
playlist.map_response(id, Language::En, None, None).unwrap();
|
||||
playlist.map_response(&MapRespCtx::test(id)).unwrap();
|
||||
|
||||
assert!(
|
||||
map_res.warnings.is_empty(),
|
||||
|
|
|
@ -15,7 +15,7 @@ use crate::{
|
|||
serializer::MapResult,
|
||||
};
|
||||
|
||||
use super::{response, ClientType, MapResponse, RustyPipeQuery, YTContext};
|
||||
use super::{response, ClientType, MapRespCtx, MapResponse, RustyPipeQuery, YTContext};
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
|
@ -126,7 +126,7 @@ impl RustyPipeQuery {
|
|||
}
|
||||
|
||||
/// Get YouTube Music search suggestions
|
||||
#[tracing::instrument(skip(self))]
|
||||
#[tracing::instrument(skip(self), level = "error")]
|
||||
pub async fn music_search_suggestion<S: AsRef<str> + Debug>(
|
||||
&self,
|
||||
query: S,
|
||||
|
@ -152,10 +152,7 @@ impl RustyPipeQuery {
|
|||
impl<T: FromYtItem> MapResponse<MusicSearchResult<T>> for response::MusicSearch {
|
||||
fn map_response(
|
||||
self,
|
||||
_id: &str,
|
||||
lang: crate::param::Language,
|
||||
_deobf: Option<&crate::deobfuscate::DeobfData>,
|
||||
vdata: Option<&str>,
|
||||
ctx: &MapRespCtx<'_>,
|
||||
) -> Result<MapResult<MusicSearchResult<T>>, ExtractionError> {
|
||||
// dbg!(&self);
|
||||
|
||||
|
@ -171,7 +168,7 @@ impl<T: FromYtItem> MapResponse<MusicSearchResult<T>> for response::MusicSearch
|
|||
|
||||
let mut corrected_query = None;
|
||||
let mut ctoken = None;
|
||||
let mut mapper = MusicListMapper::new(lang);
|
||||
let mut mapper = MusicListMapper::new(ctx.lang);
|
||||
|
||||
sections.into_iter().for_each(|section| match section {
|
||||
response::music_search::ItemSection::MusicShelfRenderer(shelf) => {
|
||||
|
@ -199,7 +196,7 @@ impl<T: FromYtItem> MapResponse<MusicSearchResult<T>> for response::MusicSearch
|
|||
None,
|
||||
map_res.c,
|
||||
ctoken,
|
||||
vdata.map(str::to_owned),
|
||||
ctx.visitor_data.map(str::to_owned),
|
||||
ContinuationEndpoint::MusicSearch,
|
||||
),
|
||||
corrected_query,
|
||||
|
@ -212,12 +209,9 @@ impl<T: FromYtItem> MapResponse<MusicSearchResult<T>> for response::MusicSearch
|
|||
impl MapResponse<MusicSearchSuggestion> for response::MusicSearchSuggestion {
|
||||
fn map_response(
|
||||
self,
|
||||
_id: &str,
|
||||
lang: crate::param::Language,
|
||||
_deobf: Option<&crate::deobfuscate::DeobfData>,
|
||||
_vdata: Option<&str>,
|
||||
ctx: &MapRespCtx<'_>,
|
||||
) -> Result<MapResult<MusicSearchSuggestion>, ExtractionError> {
|
||||
let mut mapper = MusicListMapper::new_search_suggest(lang);
|
||||
let mut mapper = MusicListMapper::new_search_suggest(ctx.lang);
|
||||
let mut terms = Vec::new();
|
||||
|
||||
for section in self.contents {
|
||||
|
@ -256,12 +250,11 @@ mod tests {
|
|||
use rstest::rstest;
|
||||
|
||||
use crate::{
|
||||
client::{response, MapResponse},
|
||||
client::{response, MapRespCtx, MapResponse},
|
||||
model::{
|
||||
AlbumItem, ArtistItem, MusicItem, MusicPlaylistItem, MusicSearchResult,
|
||||
MusicSearchSuggestion, TrackItem,
|
||||
},
|
||||
param::Language,
|
||||
serializer::MapResult,
|
||||
util::tests::TESTFILES,
|
||||
};
|
||||
|
@ -278,7 +271,7 @@ mod tests {
|
|||
let search: response::MusicSearch =
|
||||
serde_json::from_reader(BufReader::new(json_file)).unwrap();
|
||||
let map_res: MapResult<MusicSearchResult<MusicItem>> =
|
||||
search.map_response("", Language::En, None, None).unwrap();
|
||||
search.map_response(&MapRespCtx::test("")).unwrap();
|
||||
|
||||
assert!(
|
||||
map_res.warnings.is_empty(),
|
||||
|
@ -301,7 +294,7 @@ mod tests {
|
|||
let search: response::MusicSearch =
|
||||
serde_json::from_reader(BufReader::new(json_file)).unwrap();
|
||||
let map_res: MapResult<MusicSearchResult<TrackItem>> =
|
||||
search.map_response("", Language::En, None, None).unwrap();
|
||||
search.map_response(&MapRespCtx::test("")).unwrap();
|
||||
|
||||
assert!(
|
||||
map_res.warnings.is_empty(),
|
||||
|
@ -320,7 +313,7 @@ mod tests {
|
|||
let search: response::MusicSearch =
|
||||
serde_json::from_reader(BufReader::new(json_file)).unwrap();
|
||||
let map_res: MapResult<MusicSearchResult<AlbumItem>> =
|
||||
search.map_response("", Language::En, None, None).unwrap();
|
||||
search.map_response(&MapRespCtx::test("")).unwrap();
|
||||
|
||||
assert!(
|
||||
map_res.warnings.is_empty(),
|
||||
|
@ -339,7 +332,7 @@ mod tests {
|
|||
let search: response::MusicSearch =
|
||||
serde_json::from_reader(BufReader::new(json_file)).unwrap();
|
||||
let map_res: MapResult<MusicSearchResult<ArtistItem>> =
|
||||
search.map_response("", Language::En, None, None).unwrap();
|
||||
search.map_response(&MapRespCtx::test("")).unwrap();
|
||||
|
||||
assert!(
|
||||
map_res.warnings.is_empty(),
|
||||
|
@ -360,7 +353,7 @@ mod tests {
|
|||
let search: response::MusicSearch =
|
||||
serde_json::from_reader(BufReader::new(json_file)).unwrap();
|
||||
let map_res: MapResult<MusicSearchResult<MusicPlaylistItem>> =
|
||||
search.map_response("", Language::En, None, None).unwrap();
|
||||
search.map_response(&MapRespCtx::test("")).unwrap();
|
||||
|
||||
assert!(
|
||||
map_res.warnings.is_empty(),
|
||||
|
@ -380,9 +373,8 @@ mod tests {
|
|||
|
||||
let suggestion: response::MusicSearchSuggestion =
|
||||
serde_json::from_reader(BufReader::new(json_file)).unwrap();
|
||||
let map_res: MapResult<MusicSearchSuggestion> = suggestion
|
||||
.map_response("", Language::En, None, None)
|
||||
.unwrap();
|
||||
let map_res: MapResult<MusicSearchSuggestion> =
|
||||
suggestion.map_response(&MapRespCtx::test("")).unwrap();
|
||||
|
||||
assert!(
|
||||
map_res.warnings.is_empty(),
|
||||
|
|
|
@ -10,11 +10,11 @@ use crate::model::{
|
|||
use crate::serializer::MapResult;
|
||||
|
||||
use super::response::music_item::{map_queue_item, MusicListMapper, PlaylistPanelVideo};
|
||||
use super::{response, ClientType, MapResponse, QContinuation, RustyPipeQuery};
|
||||
use super::{response, ClientType, MapRespCtx, MapResponse, QContinuation, RustyPipeQuery};
|
||||
|
||||
impl RustyPipeQuery {
|
||||
/// Get more YouTube items from the given continuation token and endpoint
|
||||
#[tracing::instrument(skip(self))]
|
||||
#[tracing::instrument(skip(self), level = "error")]
|
||||
pub async fn continuation<T: FromYtItem, S: AsRef<str> + Debug>(
|
||||
&self,
|
||||
ctoken: S,
|
||||
|
@ -103,10 +103,7 @@ fn map_ytm_paginator<T: FromYtItem>(
|
|||
impl MapResponse<Paginator<YouTubeItem>> for response::Continuation {
|
||||
fn map_response(
|
||||
self,
|
||||
_id: &str,
|
||||
lang: crate::param::Language,
|
||||
_deobf: Option<&crate::deobfuscate::DeobfData>,
|
||||
_vdata: Option<&str>,
|
||||
ctx: &MapRespCtx<'_>,
|
||||
) -> Result<MapResult<Paginator<YouTubeItem>>, ExtractionError> {
|
||||
let items = self
|
||||
.on_response_received_actions
|
||||
|
@ -126,7 +123,7 @@ impl MapResponse<Paginator<YouTubeItem>> for response::Continuation {
|
|||
})
|
||||
.unwrap_or_default();
|
||||
|
||||
let mut mapper = response::YouTubeListMapper::<YouTubeItem>::new(lang);
|
||||
let mut mapper = response::YouTubeListMapper::<YouTubeItem>::new(ctx.lang);
|
||||
mapper.map_response(items);
|
||||
|
||||
Ok(MapResult {
|
||||
|
@ -139,12 +136,9 @@ impl MapResponse<Paginator<YouTubeItem>> for response::Continuation {
|
|||
impl MapResponse<Paginator<MusicItem>> for response::MusicContinuation {
|
||||
fn map_response(
|
||||
self,
|
||||
_id: &str,
|
||||
lang: crate::param::Language,
|
||||
_deobf: Option<&crate::deobfuscate::DeobfData>,
|
||||
_vdata: Option<&str>,
|
||||
ctx: &MapRespCtx<'_>,
|
||||
) -> Result<MapResult<Paginator<MusicItem>>, ExtractionError> {
|
||||
let mut mapper = MusicListMapper::new(lang);
|
||||
let mut mapper = MusicListMapper::new(ctx.lang);
|
||||
let mut continuations = Vec::new();
|
||||
|
||||
match self.continuation_contents {
|
||||
|
@ -173,7 +167,7 @@ impl MapResponse<Paginator<MusicItem>> for response::MusicContinuation {
|
|||
mapper.add_warnings(&mut panel.contents.warnings);
|
||||
panel.contents.c.into_iter().for_each(|item| {
|
||||
if let PlaylistPanelVideo::PlaylistPanelVideoRenderer(item) = item {
|
||||
let mut track = map_queue_item(item, lang);
|
||||
let mut track = map_queue_item(item, ctx.lang);
|
||||
mapper.add_item(MusicItem::Track(track.c));
|
||||
mapper.add_warnings(&mut track.warnings);
|
||||
}
|
||||
|
@ -356,13 +350,11 @@ mod tests {
|
|||
use super::*;
|
||||
use crate::{
|
||||
model::{MusicPlaylistItem, PlaylistItem, TrackItem, VideoItem},
|
||||
param::Language,
|
||||
util::tests::TESTFILES,
|
||||
};
|
||||
|
||||
#[rstest]
|
||||
#[case::search("search", path!("search" / "cont.json"))]
|
||||
#[case::startpage("startpage", path!("trends" / "startpage_cont.json"))]
|
||||
#[case::recommendations("recommendations", path!("video_details" / "recommendations.json"))]
|
||||
fn map_continuation_items(#[case] name: &str, #[case] path: PathBuf) {
|
||||
let json_path = path!(*TESTFILES / path);
|
||||
|
@ -371,7 +363,7 @@ mod tests {
|
|||
let items: response::Continuation =
|
||||
serde_json::from_reader(BufReader::new(json_file)).unwrap();
|
||||
let map_res: MapResult<Paginator<YouTubeItem>> =
|
||||
items.map_response("", Language::En, None, None).unwrap();
|
||||
items.map_response(&MapRespCtx::test("")).unwrap();
|
||||
|
||||
assert!(
|
||||
map_res.warnings.is_empty(),
|
||||
|
@ -393,7 +385,7 @@ mod tests {
|
|||
let items: response::Continuation =
|
||||
serde_json::from_reader(BufReader::new(json_file)).unwrap();
|
||||
let map_res: MapResult<Paginator<YouTubeItem>> =
|
||||
items.map_response("", Language::En, None, None).unwrap();
|
||||
items.map_response(&MapRespCtx::test("")).unwrap();
|
||||
let paginator: Paginator<VideoItem> =
|
||||
map_yt_paginator(map_res.c, None, ContinuationEndpoint::Browse);
|
||||
|
||||
|
@ -416,7 +408,7 @@ mod tests {
|
|||
let items: response::Continuation =
|
||||
serde_json::from_reader(BufReader::new(json_file)).unwrap();
|
||||
let map_res: MapResult<Paginator<YouTubeItem>> =
|
||||
items.map_response("", Language::En, None, None).unwrap();
|
||||
items.map_response(&MapRespCtx::test("")).unwrap();
|
||||
let paginator: Paginator<PlaylistItem> =
|
||||
map_yt_paginator(map_res.c, None, ContinuationEndpoint::Browse);
|
||||
|
||||
|
@ -439,7 +431,7 @@ mod tests {
|
|||
let items: response::MusicContinuation =
|
||||
serde_json::from_reader(BufReader::new(json_file)).unwrap();
|
||||
let map_res: MapResult<Paginator<MusicItem>> =
|
||||
items.map_response("", Language::En, None, None).unwrap();
|
||||
items.map_response(&MapRespCtx::test("")).unwrap();
|
||||
let paginator: Paginator<TrackItem> =
|
||||
map_ytm_paginator(map_res.c, None, ContinuationEndpoint::MusicBrowse);
|
||||
|
||||
|
@ -460,7 +452,7 @@ mod tests {
|
|||
let items: response::MusicContinuation =
|
||||
serde_json::from_reader(BufReader::new(json_file)).unwrap();
|
||||
let map_res: MapResult<Paginator<MusicItem>> =
|
||||
items.map_response("", Language::En, None, None).unwrap();
|
||||
items.map_response(&MapRespCtx::test("")).unwrap();
|
||||
let paginator: Paginator<MusicPlaylistItem> =
|
||||
map_ytm_paginator(map_res.c, None, ContinuationEndpoint::MusicBrowse);
|
||||
|
||||
|
|
|
@ -13,16 +13,19 @@ use crate::{
|
|||
deobfuscate::Deobfuscator,
|
||||
error::{internal::DeobfError, Error, ExtractionError, UnavailabilityReason},
|
||||
model::{
|
||||
traits::QualityOrd, AudioCodec, AudioFormat, AudioStream, AudioTrack, ChannelId, Frameset,
|
||||
Subtitle, VideoCodec, VideoFormat, VideoPlayer, VideoPlayerDetails, VideoStream,
|
||||
traits::QualityOrd, AudioCodec, AudioFormat, AudioStream, AudioTrack, Frameset, Subtitle,
|
||||
VideoCodec, VideoFormat, VideoPlayer, VideoPlayerDetails, VideoStream,
|
||||
},
|
||||
param::Language,
|
||||
util,
|
||||
};
|
||||
|
||||
use super::{
|
||||
response::{self, player},
|
||||
ClientType, MapResponse, MapResult, RustyPipeQuery, YTContext,
|
||||
response::{
|
||||
self,
|
||||
player::{self, Format},
|
||||
},
|
||||
ClientType, MapRespCtx, MapResponse, MapResult, RustyPipeQuery, YTContext,
|
||||
DEFAULT_PLAYER_CLIENT_ORDER,
|
||||
};
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
|
@ -62,37 +65,62 @@ struct QContentPlaybackContext<'a> {
|
|||
|
||||
impl RustyPipeQuery {
|
||||
/// Get YouTube player data (video/audio streams + basic metadata)
|
||||
#[tracing::instrument(skip(self))]
|
||||
pub async fn player<S: AsRef<str> + Debug>(&self, video_id: S) -> Result<VideoPlayer, Error> {
|
||||
let video_id = video_id.as_ref();
|
||||
let desktop_res = self.player_from_client(video_id, ClientType::Desktop).await;
|
||||
self.player_from_clients(video_id, DEFAULT_PLAYER_CLIENT_ORDER)
|
||||
.await
|
||||
}
|
||||
|
||||
match desktop_res {
|
||||
Ok(res) => Ok(res),
|
||||
/// Get YouTube player data (video/audio streams + basic metadata) using a list of clients.
|
||||
///
|
||||
/// The clients are used in the given order. If a client cannot fetch the requested video,
|
||||
/// an attempt is made with the next one.
|
||||
///
|
||||
/// If an age-restricted video is detected, it will automatically use the [`ClientType::TvHtml5Embed`]
|
||||
/// since it is the only one that can circumvent age restrictions.
|
||||
pub async fn player_from_clients<S: AsRef<str> + Debug>(
|
||||
&self,
|
||||
video_id: S,
|
||||
clients: &[ClientType],
|
||||
) -> Result<VideoPlayer, Error> {
|
||||
let video_id = video_id.as_ref();
|
||||
let mut last_e = Error::Other("no clients".into());
|
||||
|
||||
for client in clients {
|
||||
let res = self.player_from_client(video_id, *client).await;
|
||||
match res {
|
||||
Ok(res) => return Ok(res),
|
||||
Err(Error::Extraction(e)) => {
|
||||
if e.switch_client() {
|
||||
let tv_res = self
|
||||
if let ExtractionError::Unavailable {
|
||||
reason: UnavailabilityReason::AgeRestricted,
|
||||
msg,
|
||||
} = &e
|
||||
{
|
||||
if let Ok(res) = self
|
||||
.player_from_client(video_id, ClientType::TvHtml5Embed)
|
||||
.await;
|
||||
|
||||
match tv_res {
|
||||
// Output desktop client error if the tv client is unsupported
|
||||
Err(Error::Extraction(ExtractionError::Unavailable {
|
||||
reason: UnavailabilityReason::UnsupportedClient,
|
||||
..
|
||||
})) => Err(Error::Extraction(e)),
|
||||
_ => tv_res,
|
||||
}
|
||||
.await
|
||||
{
|
||||
return Ok(res);
|
||||
} else {
|
||||
Err(Error::Extraction(e))
|
||||
return Err(Error::Extraction(ExtractionError::Unavailable {
|
||||
reason: UnavailabilityReason::AgeRestricted,
|
||||
msg: msg.to_owned(),
|
||||
}));
|
||||
}
|
||||
}
|
||||
Err(e) => Err(e),
|
||||
last_e = Error::Extraction(e);
|
||||
} else {
|
||||
return Err(Error::Extraction(e));
|
||||
}
|
||||
}
|
||||
Err(e) => return Err(e),
|
||||
}
|
||||
}
|
||||
Err(last_e)
|
||||
}
|
||||
|
||||
/// Get YouTube player data (video/audio streams + basic metadata) using the specified client
|
||||
#[tracing::instrument(skip(self))]
|
||||
#[tracing::instrument(skip(self), level = "error")]
|
||||
pub async fn player_from_client<S: AsRef<str> + Debug>(
|
||||
&self,
|
||||
video_id: S,
|
||||
|
@ -149,12 +177,8 @@ impl RustyPipeQuery {
|
|||
impl MapResponse<VideoPlayer> for response::Player {
|
||||
fn map_response(
|
||||
self,
|
||||
id: &str,
|
||||
_lang: Language,
|
||||
deobf: Option<&crate::deobfuscate::DeobfData>,
|
||||
vdata: Option<&str>,
|
||||
ctx: &MapRespCtx<'_>,
|
||||
) -> Result<super::MapResult<VideoPlayer>, ExtractionError> {
|
||||
let deobf = Deobfuscator::new(deobf.unwrap())?;
|
||||
let mut warnings = vec![];
|
||||
|
||||
// Check playability status
|
||||
|
@ -203,6 +227,7 @@ impl MapResponse<VideoPlayer> for response::Player {
|
|||
.find_map(|word| match word {
|
||||
"age" | "inappropriate" => Some(UnavailabilityReason::AgeRestricted),
|
||||
"private" => Some(UnavailabilityReason::Private),
|
||||
"bot" => Some(UnavailabilityReason::IpBan),
|
||||
_ => None,
|
||||
})
|
||||
.unwrap_or_default();
|
||||
|
@ -224,7 +249,7 @@ impl MapResponse<VideoPlayer> for response::Player {
|
|||
}
|
||||
};
|
||||
|
||||
let mut streaming_data =
|
||||
let streaming_data =
|
||||
self.streaming_data
|
||||
.ok_or(ExtractionError::InvalidData(Cow::Borrowed(
|
||||
"no streaming data",
|
||||
|
@ -235,10 +260,10 @@ impl MapResponse<VideoPlayer> for response::Player {
|
|||
"no video details",
|
||||
)))?;
|
||||
|
||||
if video_details.video_id != id {
|
||||
if video_details.video_id != ctx.id {
|
||||
return Err(ExtractionError::WrongResult(format!(
|
||||
"video id {}, expected {}",
|
||||
video_details.video_id, id
|
||||
video_details.video_id, ctx.id
|
||||
)));
|
||||
}
|
||||
|
||||
|
@ -248,64 +273,24 @@ impl MapResponse<VideoPlayer> for response::Player {
|
|||
description: video_details.short_description,
|
||||
duration: video_details.length_seconds,
|
||||
thumbnail: video_details.thumbnail.into(),
|
||||
channel: ChannelId {
|
||||
id: video_details.channel_id,
|
||||
name: video_details.author,
|
||||
},
|
||||
channel_id: video_details.channel_id,
|
||||
channel_name: video_details.author,
|
||||
view_count: video_details.view_count,
|
||||
keywords: video_details.keywords,
|
||||
is_live,
|
||||
is_live_content: video_details.is_live_content,
|
||||
};
|
||||
|
||||
let mut formats = streaming_data.formats.c;
|
||||
formats.append(&mut streaming_data.adaptive_formats.c);
|
||||
|
||||
let mut video_streams: Vec<VideoStream> = Vec::new();
|
||||
let mut video_only_streams: Vec<VideoStream> = Vec::new();
|
||||
let mut audio_streams: Vec<AudioStream> = Vec::new();
|
||||
|
||||
if !is_live {
|
||||
let mut last_nsig: [String; 2] = [String::new(), String::new()];
|
||||
|
||||
warnings.append(&mut streaming_data.formats.warnings);
|
||||
warnings.append(&mut streaming_data.adaptive_formats.warnings);
|
||||
|
||||
for f in formats {
|
||||
if f.format_type == player::FormatType::FormatStreamTypeOtf {
|
||||
continue;
|
||||
}
|
||||
|
||||
match (f.is_video(), f.is_audio()) {
|
||||
(true, true) => {
|
||||
let mut map_res = map_video_stream(f, &deobf, &mut last_nsig);
|
||||
warnings.append(&mut map_res.warnings);
|
||||
if let Some(c) = map_res.c {
|
||||
video_streams.push(c);
|
||||
let streams = if !is_live {
|
||||
let mut mapper = StreamsMapper::new(Deobfuscator::new(ctx.deobf.unwrap())?);
|
||||
mapper.map_streams(streaming_data.formats);
|
||||
mapper.map_streams(streaming_data.adaptive_formats);
|
||||
let mut res = mapper.output()?;
|
||||
warnings.append(&mut res.warnings);
|
||||
res.c
|
||||
} else {
|
||||
Streams::default()
|
||||
};
|
||||
}
|
||||
(true, false) => {
|
||||
let mut map_res = map_video_stream(f, &deobf, &mut last_nsig);
|
||||
warnings.append(&mut map_res.warnings);
|
||||
if let Some(c) = map_res.c {
|
||||
video_only_streams.push(c);
|
||||
};
|
||||
}
|
||||
(false, true) => {
|
||||
let mut map_res = map_audio_stream(f, &deobf, &mut last_nsig);
|
||||
warnings.append(&mut map_res.warnings);
|
||||
if let Some(c) = map_res.c {
|
||||
audio_streams.push(c);
|
||||
};
|
||||
}
|
||||
(false, false) => warnings.push(format!("invalid stream: itag {}", f.itag)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
video_streams.sort_by(QualityOrd::quality_cmp);
|
||||
video_only_streams.sort_by(QualityOrd::quality_cmp);
|
||||
audio_streams.sort_by(QualityOrd::quality_cmp);
|
||||
|
||||
let subtitles = self.captions.map_or(Vec::new(), |captions| {
|
||||
captions
|
||||
|
@ -374,27 +359,116 @@ impl MapResponse<VideoPlayer> for response::Player {
|
|||
Ok(MapResult {
|
||||
c: VideoPlayer {
|
||||
details: video_info,
|
||||
video_streams,
|
||||
video_only_streams,
|
||||
audio_streams,
|
||||
video_streams: streams.video_streams,
|
||||
video_only_streams: streams.video_only_streams,
|
||||
audio_streams: streams.audio_streams,
|
||||
subtitles,
|
||||
expires_in_seconds: streaming_data.expires_in_seconds,
|
||||
hls_manifest_url: streaming_data.hls_manifest_url,
|
||||
dash_manifest_url: streaming_data.dash_manifest_url,
|
||||
preview_frames,
|
||||
client_type: ctx.client_type,
|
||||
visitor_data: self
|
||||
.response_context
|
||||
.visitor_data
|
||||
.or_else(|| vdata.map(str::to_owned)),
|
||||
.or_else(|| ctx.visitor_data.map(str::to_owned)),
|
||||
},
|
||||
warnings,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
struct StreamsMapper {
|
||||
deobf: Deobfuscator,
|
||||
streams: Streams,
|
||||
warnings: Vec<String>,
|
||||
/// First stream mapping error
|
||||
first_err: Option<ExtractionError>,
|
||||
/// Last obfuscated nsig parameter (cache)
|
||||
last_nsig: String,
|
||||
/// Last deobfuscated nsig parameter
|
||||
last_nsig_deobf: String,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct Streams {
|
||||
video_streams: Vec<VideoStream>,
|
||||
video_only_streams: Vec<VideoStream>,
|
||||
audio_streams: Vec<AudioStream>,
|
||||
}
|
||||
|
||||
impl StreamsMapper {
|
||||
fn new(deobf: Deobfuscator) -> Self {
|
||||
Self {
|
||||
deobf,
|
||||
streams: Streams::default(),
|
||||
warnings: Vec::new(),
|
||||
first_err: None,
|
||||
last_nsig: String::new(),
|
||||
last_nsig_deobf: String::new(),
|
||||
}
|
||||
}
|
||||
|
||||
fn map_streams(&mut self, mut streams: MapResult<Vec<Format>>) {
|
||||
self.warnings.append(&mut streams.warnings);
|
||||
|
||||
let map_e = |m: &mut Self, e: ExtractionError| {
|
||||
m.warnings.push(e.to_string());
|
||||
if m.first_err.is_none() {
|
||||
m.first_err = Some(e);
|
||||
}
|
||||
};
|
||||
|
||||
for f in streams.c {
|
||||
if f.format_type == player::FormatType::FormatStreamTypeOtf {
|
||||
continue;
|
||||
}
|
||||
|
||||
match (f.is_video(), f.is_audio()) {
|
||||
(true, true) => match self.map_video_stream(f) {
|
||||
Ok(c) => self.streams.video_streams.push(c),
|
||||
Err(e) => map_e(self, e),
|
||||
},
|
||||
(true, false) => match self.map_video_stream(f) {
|
||||
Ok(c) => self.streams.video_only_streams.push(c),
|
||||
Err(e) => map_e(self, e),
|
||||
},
|
||||
(false, true) => match self.map_audio_stream(f) {
|
||||
Ok(c) => self.streams.audio_streams.push(c),
|
||||
Err(e) => map_e(self, e),
|
||||
},
|
||||
(false, false) => self
|
||||
.warnings
|
||||
.push(format!("invalid stream: itag {}", f.itag)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn output(mut self) -> Result<MapResult<Streams>, ExtractionError> {
|
||||
// If we did not extract any streams and there were mapping errors, fail with the first error
|
||||
if self.streams.video_streams.is_empty()
|
||||
&& (self.streams.video_only_streams.is_empty() || self.streams.audio_streams.is_empty())
|
||||
{
|
||||
if let Some(e) = self.first_err {
|
||||
return Err(e);
|
||||
}
|
||||
}
|
||||
|
||||
self.streams.video_streams.sort_by(QualityOrd::quality_cmp);
|
||||
self.streams
|
||||
.video_only_streams
|
||||
.sort_by(QualityOrd::quality_cmp);
|
||||
self.streams.audio_streams.sort_by(QualityOrd::quality_cmp);
|
||||
|
||||
Ok(MapResult {
|
||||
c: self.streams,
|
||||
warnings: self.warnings,
|
||||
})
|
||||
}
|
||||
|
||||
fn cipher_to_url_params(
|
||||
&self,
|
||||
signature_cipher: &str,
|
||||
deobf: &Deobfuscator,
|
||||
) -> Result<(Url, BTreeMap<String, String>), DeobfError> {
|
||||
let params: HashMap<Cow<str>, Cow<str>> =
|
||||
url::form_urlencoded::parse(signature_cipher.as_bytes()).collect();
|
||||
|
@ -412,25 +486,20 @@ fn cipher_to_url_params(
|
|||
let (url_base, mut url_params) =
|
||||
util::url_to_params(raw_url).or(Err(DeobfError::Extraction("url params")))?;
|
||||
|
||||
let deobf_sig = deobf.deobfuscate_sig(sig)?;
|
||||
let deobf_sig = self.deobf.deobfuscate_sig(sig)?;
|
||||
url_params.insert(sp.to_string(), deobf_sig);
|
||||
|
||||
Ok((url_base, url_params))
|
||||
}
|
||||
|
||||
fn deobf_nsig(
|
||||
url_params: &mut BTreeMap<String, String>,
|
||||
deobf: &Deobfuscator,
|
||||
last_nsig: &mut [String; 2],
|
||||
) -> Result<(), DeobfError> {
|
||||
let nsig: String;
|
||||
fn deobf_nsig(&mut self, url_params: &mut BTreeMap<String, String>) -> Result<(), DeobfError> {
|
||||
if let Some(n) = url_params.get("n") {
|
||||
nsig = if n == &last_nsig[0] {
|
||||
last_nsig[1].clone()
|
||||
let nsig = if n == &self.last_nsig {
|
||||
self.last_nsig_deobf.to_owned()
|
||||
} else {
|
||||
let nsig = deobf.deobfuscate_nsig(n)?;
|
||||
last_nsig[0] = n.to_string();
|
||||
last_nsig[1].clone_from(&nsig);
|
||||
let nsig = self.deobf.deobfuscate_nsig(n)?;
|
||||
self.last_nsig.clone_from(n);
|
||||
self.last_nsig_deobf.clone_from(&nsig);
|
||||
nsig
|
||||
};
|
||||
|
||||
|
@ -439,91 +508,60 @@ fn deobf_nsig(
|
|||
Ok(())
|
||||
}
|
||||
|
||||
struct UrlMapRes {
|
||||
url: String,
|
||||
throttled: bool,
|
||||
xtags: Option<String>,
|
||||
}
|
||||
|
||||
fn map_url(
|
||||
&mut self,
|
||||
url: &Option<String>,
|
||||
signature_cipher: &Option<String>,
|
||||
deobf: &Deobfuscator,
|
||||
last_nsig: &mut [String; 2],
|
||||
) -> MapResult<Option<UrlMapRes>> {
|
||||
let x = match url {
|
||||
Some(url) => util::url_to_params(url).map_err(|_| format!("Could not parse url `{url}`")),
|
||||
) -> Result<UrlMapRes, ExtractionError> {
|
||||
let (url_base, mut url_params) =
|
||||
match url {
|
||||
Some(url) => util::url_to_params(url).map_err(|_| {
|
||||
ExtractionError::InvalidData(format!("Could not parse url `{url}`").into())
|
||||
}),
|
||||
None => match signature_cipher {
|
||||
Some(signature_cipher) => cipher_to_url_params(signature_cipher, deobf).map_err(|e| {
|
||||
Some(signature_cipher) => {
|
||||
self.cipher_to_url_params(signature_cipher).map_err(|e| {
|
||||
ExtractionError::InvalidData(
|
||||
format!("Could not deobfuscate signatureCipher `{signature_cipher}`: {e}")
|
||||
}),
|
||||
None => Err("stream contained neither url or cipher".to_owned()),
|
||||
.into(),
|
||||
)
|
||||
})
|
||||
}
|
||||
None => Err(ExtractionError::InvalidData(
|
||||
"stream contained neither url or cipher".into(),
|
||||
)),
|
||||
},
|
||||
};
|
||||
}?;
|
||||
|
||||
let (url_base, mut url_params) = match x {
|
||||
Ok(x) => x,
|
||||
Err(e) => {
|
||||
return MapResult {
|
||||
c: None,
|
||||
warnings: vec![e],
|
||||
}
|
||||
}
|
||||
};
|
||||
self.deobf_nsig(&mut url_params)?;
|
||||
let url = Url::parse_with_params(url_base.as_str(), url_params.iter())
|
||||
.map_err(|_| ExtractionError::InvalidData("could not combine URL".into()))?;
|
||||
|
||||
let mut warnings = vec![];
|
||||
let mut throttled = false;
|
||||
deobf_nsig(&mut url_params, deobf, last_nsig).unwrap_or_else(|e| {
|
||||
warnings.push(format!(
|
||||
"Could not deobfuscate nsig (params: {url_params:?}): {e}"
|
||||
));
|
||||
throttled = true;
|
||||
});
|
||||
|
||||
match Url::parse_with_params(url_base.as_str(), url_params.iter()) {
|
||||
Ok(url) => MapResult {
|
||||
c: Some(UrlMapRes {
|
||||
Ok(UrlMapRes {
|
||||
url: url.to_string(),
|
||||
throttled,
|
||||
xtags: url_params.get("xtags").cloned(),
|
||||
}),
|
||||
warnings,
|
||||
},
|
||||
Err(_) => MapResult {
|
||||
c: None,
|
||||
warnings: vec![format!(
|
||||
"url could not be joined. url: `{url_base}` params: {url_params:?}"
|
||||
)],
|
||||
},
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn map_video_stream(
|
||||
f: player::Format,
|
||||
deobf: &Deobfuscator,
|
||||
last_nsig: &mut [String; 2],
|
||||
) -> MapResult<Option<VideoStream>> {
|
||||
fn map_video_stream(&mut self, f: player::Format) -> Result<VideoStream, ExtractionError> {
|
||||
let Some((mtype, codecs)) = parse_mime(&f.mime_type) else {
|
||||
return MapResult {
|
||||
c: None,
|
||||
warnings: vec![format!(
|
||||
return Err(ExtractionError::InvalidData(
|
||||
format!(
|
||||
"Invalid mime type `{}` in video format {:?}",
|
||||
&f.mime_type, &f
|
||||
)],
|
||||
};
|
||||
)
|
||||
.into(),
|
||||
));
|
||||
};
|
||||
let Some(format) = get_video_format(mtype) else {
|
||||
return MapResult {
|
||||
c: None,
|
||||
warnings: vec![format!("invalid video format. itag: {}", f.itag)],
|
||||
return Err(ExtractionError::InvalidData(
|
||||
format!("invalid video format. itag: {}", f.itag).into(),
|
||||
));
|
||||
};
|
||||
};
|
||||
let map_res = map_url(&f.url, &f.signature_cipher, deobf, last_nsig);
|
||||
let map_res = self.map_url(&f.url, &f.signature_cipher)?;
|
||||
|
||||
match map_res.c {
|
||||
Some(url) => MapResult {
|
||||
c: Some(VideoStream {
|
||||
url: url.url,
|
||||
Ok(VideoStream {
|
||||
url: map_res.url,
|
||||
itag: f.itag,
|
||||
bitrate: f.bitrate,
|
||||
average_bitrate: f.average_bitrate.unwrap_or(f.bitrate),
|
||||
|
@ -542,44 +580,26 @@ fn map_video_stream(
|
|||
format,
|
||||
codec: get_video_codec(codecs),
|
||||
mime: f.mime_type,
|
||||
throttled: url.throttled,
|
||||
}),
|
||||
warnings: map_res.warnings,
|
||||
},
|
||||
None => MapResult {
|
||||
c: None,
|
||||
warnings: map_res.warnings,
|
||||
},
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn map_audio_stream(
|
||||
f: player::Format,
|
||||
deobf: &Deobfuscator,
|
||||
last_nsig: &mut [String; 2],
|
||||
) -> MapResult<Option<AudioStream>> {
|
||||
fn map_audio_stream(&mut self, f: player::Format) -> Result<AudioStream, ExtractionError> {
|
||||
let Some((mtype, codecs)) = parse_mime(&f.mime_type) else {
|
||||
return MapResult {
|
||||
c: None,
|
||||
warnings: vec![format!(
|
||||
return Err(ExtractionError::InvalidData(
|
||||
format!(
|
||||
"Invalid mime type `{}` in video format {:?}",
|
||||
&f.mime_type, &f
|
||||
)],
|
||||
)
|
||||
.into(),
|
||||
));
|
||||
};
|
||||
};
|
||||
let Some(format) = get_audio_format(mtype) else {
|
||||
return MapResult {
|
||||
c: None,
|
||||
warnings: vec![format!("invalid audio format. itag: {}", f.itag)],
|
||||
};
|
||||
};
|
||||
let map_res = map_url(&f.url, &f.signature_cipher, deobf, last_nsig);
|
||||
let mut warnings = map_res.warnings;
|
||||
let format = get_audio_format(mtype).ok_or_else(|| {
|
||||
ExtractionError::InvalidData(format!("invalid audio format. itag: {}", f.itag).into())
|
||||
})?;
|
||||
let map_res = self.map_url(&f.url, &f.signature_cipher)?;
|
||||
|
||||
match map_res.c {
|
||||
Some(url) => MapResult {
|
||||
c: Some(AudioStream {
|
||||
url: url.url,
|
||||
Ok(AudioStream {
|
||||
url: map_res.url,
|
||||
itag: f.itag,
|
||||
bitrate: f.bitrate,
|
||||
average_bitrate: f.average_bitrate.unwrap_or(f.bitrate),
|
||||
|
@ -592,15 +612,54 @@ fn map_audio_stream(
|
|||
mime: f.mime_type,
|
||||
channels: f.audio_channels,
|
||||
loudness_db: f.loudness_db,
|
||||
throttled: url.throttled,
|
||||
track: f
|
||||
.audio_track
|
||||
.map(|t| map_audio_track(t, url.xtags, &mut warnings)),
|
||||
}),
|
||||
warnings,
|
||||
},
|
||||
None => MapResult { c: None, warnings },
|
||||
.map(|t| self.map_audio_track(t, map_res.xtags)),
|
||||
})
|
||||
}
|
||||
|
||||
fn map_audio_track(
|
||||
&mut self,
|
||||
track: response::player::AudioTrack,
|
||||
xtags: Option<String>,
|
||||
) -> AudioTrack {
|
||||
let mut lang = None;
|
||||
let mut track_type = None;
|
||||
|
||||
if let Some(xtags) = xtags {
|
||||
xtags
|
||||
.split(':')
|
||||
.filter_map(|param| param.split_once('='))
|
||||
.for_each(|(k, v)| match k {
|
||||
"lang" => {
|
||||
lang = Some(v.to_owned());
|
||||
}
|
||||
"acont" => match serde_plain::from_str(v) {
|
||||
Ok(v) => {
|
||||
track_type = Some(v);
|
||||
}
|
||||
Err(_) => {
|
||||
self.warnings
|
||||
.push(format!("could not parse audio track type `{v}`"));
|
||||
}
|
||||
},
|
||||
_ => {}
|
||||
});
|
||||
}
|
||||
|
||||
AudioTrack {
|
||||
id: track.id,
|
||||
lang,
|
||||
lang_name: track.display_name,
|
||||
is_default: track.audio_is_default,
|
||||
track_type,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct UrlMapRes {
|
||||
url: String,
|
||||
xtags: Option<String>,
|
||||
}
|
||||
|
||||
fn parse_mime(mime: &str) -> Option<(&str, Vec<&str>)> {
|
||||
|
@ -662,43 +721,6 @@ fn get_audio_codec(codecs: Vec<&str>) -> AudioCodec {
|
|||
AudioCodec::Unknown
|
||||
}
|
||||
|
||||
fn map_audio_track(
|
||||
track: response::player::AudioTrack,
|
||||
xtags: Option<String>,
|
||||
warnings: &mut Vec<String>,
|
||||
) -> AudioTrack {
|
||||
let mut lang = None;
|
||||
let mut track_type = None;
|
||||
|
||||
if let Some(xtags) = xtags {
|
||||
xtags
|
||||
.split(':')
|
||||
.filter_map(|param| param.split_once('='))
|
||||
.for_each(|(k, v)| match k {
|
||||
"lang" => {
|
||||
lang = Some(v.to_owned());
|
||||
}
|
||||
"acont" => match serde_plain::from_str(v) {
|
||||
Ok(v) => {
|
||||
track_type = Some(v);
|
||||
}
|
||||
Err(_) => {
|
||||
warnings.push(format!("could not parse audio track type `{v}`"));
|
||||
}
|
||||
},
|
||||
_ => {}
|
||||
});
|
||||
}
|
||||
|
||||
AudioTrack {
|
||||
id: track.id,
|
||||
lang,
|
||||
lang_name: track.display_name,
|
||||
is_default: track.audio_is_default,
|
||||
track_type,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::{fs::File, io::BufReader};
|
||||
|
@ -707,7 +729,7 @@ mod tests {
|
|||
use rstest::rstest;
|
||||
|
||||
use super::*;
|
||||
use crate::{deobfuscate::DeobfData, util::tests::TESTFILES};
|
||||
use crate::{deobfuscate::DeobfData, param::Language, util::tests::TESTFILES};
|
||||
|
||||
static DEOBF_DATA: Lazy<DeobfData> = Lazy::new(|| {
|
||||
DeobfData {
|
||||
|
@ -719,18 +741,28 @@ mod tests {
|
|||
});
|
||||
|
||||
#[rstest]
|
||||
#[case::desktop("desktop")]
|
||||
#[case::desktop_music("desktopmusic")]
|
||||
#[case::tv_html5_embed("tvhtml5embed")]
|
||||
#[case::android("android")]
|
||||
#[case::ios("ios")]
|
||||
fn map_player_data(#[case] name: &str) {
|
||||
#[case::desktop(ClientType::Desktop)]
|
||||
#[case::desktop_music(ClientType::DesktopMusic)]
|
||||
#[case::tv(ClientType::Tv)]
|
||||
#[case::tv_html5_embed(ClientType::TvHtml5Embed)]
|
||||
#[case::android(ClientType::Android)]
|
||||
#[case::ios(ClientType::Ios)]
|
||||
fn map_player_data(#[case] client_type: ClientType) {
|
||||
let name = serde_plain::to_string(&client_type)
|
||||
.unwrap()
|
||||
.replace('_', "");
|
||||
let json_path = path!(*TESTFILES / "player" / format!("{name}_video.json"));
|
||||
let json_file = File::open(json_path).unwrap();
|
||||
|
||||
let resp: response::Player = serde_json::from_reader(BufReader::new(json_file)).unwrap();
|
||||
let map_res = resp
|
||||
.map_response("pPvd8UxmSbQ", Language::En, Some(&DEOBF_DATA), None)
|
||||
.map_response(&MapRespCtx {
|
||||
id: "pPvd8UxmSbQ",
|
||||
lang: Language::En,
|
||||
deobf: Some(&DEOBF_DATA),
|
||||
visitor_data: None,
|
||||
client_type,
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
assert!(
|
||||
|
@ -755,22 +787,12 @@ mod tests {
|
|||
#[test]
|
||||
fn cipher_to_url() {
|
||||
let signature_cipher = "s=w%3DAe%3DA6aDNQLkViKS7LOm9QtxZJHKwb53riq9qEFw-ecBWJCAiA%3DcEg0tn3dty9jEHszfzh4Ud__bg9CEHVx4ix-7dKsIPAhIQRw8JQ0qOA&sp=sig&url=https://rr5---sn-h0jelnez.googlevideo.com/videoplayback%3Fexpire%3D1659376413%26ei%3Dvb7nYvH5BMK8gAfBj7ToBQ%26ip%3D2003%253Ade%253Aaf06%253A6300%253Ac750%253A1b77%253Ac74a%253A80e3%26id%3Do-AB_BABwrXZJN428ZwDxq5ScPn2AbcGODnRlTVhCQ3mj2%26itag%3D251%26source%3Dyoutube%26requiressl%3Dyes%26mh%3DhH%26mm%3D31%252C26%26mn%3Dsn-h0jelnez%252Csn-4g5ednsl%26ms%3Dau%252Conr%26mv%3Dm%26mvi%3D5%26pl%3D37%26initcwndbps%3D1588750%26spc%3DlT-Khi831z8dTejFIRCvCEwx_6romtM%26vprv%3D1%26mime%3Daudio%252Fwebm%26ns%3Db_Mq_qlTFcSGlG9RpwpM9xQH%26gir%3Dyes%26clen%3D3781277%26dur%3D229.301%26lmt%3D1655510291473933%26mt%3D1659354538%26fvip%3D5%26keepalive%3Dyes%26fexp%3D24001373%252C24007246%26c%3DWEB%26rbqsm%3Dfr%26txp%3D4532434%26n%3Dd2g6G2hVqWIXxedQ%26sparams%3Dexpire%252Cei%252Cip%252Cid%252Citag%252Csource%252Crequiressl%252Cspc%252Cvprv%252Cmime%252Cns%252Cgir%252Cclen%252Cdur%252Clmt%26lsparams%3Dmh%252Cmm%252Cmn%252Cms%252Cmv%252Cmvi%252Cpl%252Cinitcwndbps%26lsig%3DAG3C_xAwRQIgCKCGJ1iu4wlaGXy3jcJyU3inh9dr1FIfqYOZEG_MdmACIQCbungkQYFk7EhD6K2YvLaHFMjKOFWjw001_tLb0lPDtg%253D%253D";
|
||||
let mut last_nsig: [String; 2] = [String::new(), String::new()];
|
||||
let deobf = Deobfuscator::new(&DEOBF_DATA).unwrap();
|
||||
let map_res = map_url(
|
||||
&None,
|
||||
&Some(signature_cipher.to_owned()),
|
||||
&deobf,
|
||||
&mut last_nsig,
|
||||
);
|
||||
let url = map_res.c.unwrap();
|
||||
let mut mapper = StreamsMapper::new(Deobfuscator::new(&DEOBF_DATA).unwrap());
|
||||
let url = mapper
|
||||
.map_url(&None, &Some(signature_cipher.to_owned()))
|
||||
.unwrap()
|
||||
.url;
|
||||
|
||||
assert_eq!(url.url, "https://rr5---sn-h0jelnez.googlevideo.com/videoplayback?c=WEB&clen=3781277&dur=229.301&ei=vb7nYvH5BMK8gAfBj7ToBQ&expire=1659376413&fexp=24001373%2C24007246&fvip=5&gir=yes&id=o-AB_BABwrXZJN428ZwDxq5ScPn2AbcGODnRlTVhCQ3mj2&initcwndbps=1588750&ip=2003%3Ade%3Aaf06%3A6300%3Ac750%3A1b77%3Ac74a%3A80e3&itag=251&keepalive=yes&lmt=1655510291473933&lsig=AG3C_xAwRQIgCKCGJ1iu4wlaGXy3jcJyU3inh9dr1FIfqYOZEG_MdmACIQCbungkQYFk7EhD6K2YvLaHFMjKOFWjw001_tLb0lPDtg%3D%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&mh=hH&mime=audio%2Fwebm&mm=31%2C26&mn=sn-h0jelnez%2Csn-4g5ednsl&ms=au%2Conr&mt=1659354538&mv=m&mvi=5&n=XzXGSfGusw6OCQ&ns=b_Mq_qlTFcSGlG9RpwpM9xQH&pl=37&rbqsm=fr&requiressl=yes&sig=AOq0QJ8wRQIhAPIsKd7-xi4xVHEC9gb__dU4hzfzsHEj9ytd3nt0gEceAiACJWBcw-wFEq9qir35bwKHJZxtQ9mOL7SKiVkLQNDa6A%3D%3D&source=youtube&sparams=expire%2Cei%2Cip%2Cid%2Citag%2Csource%2Crequiressl%2Cspc%2Cvprv%2Cmime%2Cns%2Cgir%2Cclen%2Cdur%2Clmt&spc=lT-Khi831z8dTejFIRCvCEwx_6romtM&txp=4532434&vprv=1");
|
||||
assert!(!url.throttled);
|
||||
assert!(
|
||||
map_res.warnings.is_empty(),
|
||||
"deserialization/mapping warnings: {:?}",
|
||||
map_res.warnings
|
||||
);
|
||||
assert_eq!(url, "https://rr5---sn-h0jelnez.googlevideo.com/videoplayback?c=WEB&clen=3781277&dur=229.301&ei=vb7nYvH5BMK8gAfBj7ToBQ&expire=1659376413&fexp=24001373%2C24007246&fvip=5&gir=yes&id=o-AB_BABwrXZJN428ZwDxq5ScPn2AbcGODnRlTVhCQ3mj2&initcwndbps=1588750&ip=2003%3Ade%3Aaf06%3A6300%3Ac750%3A1b77%3Ac74a%3A80e3&itag=251&keepalive=yes&lmt=1655510291473933&lsig=AG3C_xAwRQIgCKCGJ1iu4wlaGXy3jcJyU3inh9dr1FIfqYOZEG_MdmACIQCbungkQYFk7EhD6K2YvLaHFMjKOFWjw001_tLb0lPDtg%3D%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&mh=hH&mime=audio%2Fwebm&mm=31%2C26&mn=sn-h0jelnez%2Csn-4g5ednsl&ms=au%2Conr&mt=1659354538&mv=m&mvi=5&n=XzXGSfGusw6OCQ&ns=b_Mq_qlTFcSGlG9RpwpM9xQH&pl=37&rbqsm=fr&requiressl=yes&sig=AOq0QJ8wRQIhAPIsKd7-xi4xVHEC9gb__dU4hzfzsHEj9ytd3nt0gEceAiACJWBcw-wFEq9qir35bwKHJZxtQ9mOL7SKiVkLQNDa6A%3D%3D&source=youtube&sparams=expire%2Cei%2Cip%2Cid%2Citag%2Csource%2Crequiressl%2Cspc%2Cvprv%2Cmime%2Cns%2Cgir%2Cclen%2Cdur%2Clmt&spc=lT-Khi831z8dTejFIRCvCEwx_6romtM&txp=4532434&vprv=1");
|
||||
}
|
||||
}
|
||||
|
|
|
@ -13,11 +13,11 @@ use crate::{
|
|||
util::{self, timeago, TryRemove},
|
||||
};
|
||||
|
||||
use super::{response, ClientType, MapResponse, MapResult, QBrowse, RustyPipeQuery};
|
||||
use super::{response, ClientType, MapRespCtx, MapResponse, MapResult, QBrowse, RustyPipeQuery};
|
||||
|
||||
impl RustyPipeQuery {
|
||||
/// Get a YouTube playlist
|
||||
#[tracing::instrument(skip(self))]
|
||||
#[tracing::instrument(skip(self), level = "error")]
|
||||
pub async fn playlist<S: AsRef<str> + Debug>(&self, playlist_id: S) -> Result<Playlist, Error> {
|
||||
let playlist_id = playlist_id.as_ref();
|
||||
// YTM playlists require visitor data for continuations to work
|
||||
|
@ -47,15 +47,9 @@ impl RustyPipeQuery {
|
|||
}
|
||||
|
||||
impl MapResponse<Playlist> for response::Playlist {
|
||||
fn map_response(
|
||||
self,
|
||||
id: &str,
|
||||
lang: crate::param::Language,
|
||||
_deobf: Option<&crate::deobfuscate::DeobfData>,
|
||||
vdata: Option<&str>,
|
||||
) -> Result<MapResult<Playlist>, ExtractionError> {
|
||||
fn map_response(self, ctx: &MapRespCtx<'_>) -> Result<MapResult<Playlist>, ExtractionError> {
|
||||
let (Some(contents), Some(header)) = (self.contents, self.header) else {
|
||||
return Err(response::alerts_to_err(id, self.alerts));
|
||||
return Err(response::alerts_to_err(ctx.id, self.alerts));
|
||||
};
|
||||
|
||||
let video_items = contents
|
||||
|
@ -85,7 +79,7 @@ impl MapResponse<Playlist> for response::Playlist {
|
|||
.playlist_video_list_renderer
|
||||
.contents;
|
||||
|
||||
let mut mapper = response::YouTubeListMapper::<VideoItem>::new(lang);
|
||||
let mut mapper = response::YouTubeListMapper::<VideoItem>::new(ctx.lang);
|
||||
mapper.map_response(video_items);
|
||||
|
||||
let (description, thumbnails, last_update_txt) = match self.sidebar {
|
||||
|
@ -144,9 +138,10 @@ impl MapResponse<Playlist> for response::Playlist {
|
|||
};
|
||||
|
||||
let playlist_id = header.playlist_header_renderer.playlist_id;
|
||||
if playlist_id != id {
|
||||
if playlist_id != ctx.id {
|
||||
return Err(ExtractionError::WrongResult(format!(
|
||||
"got wrong playlist id {playlist_id}, expected {id}"
|
||||
"got wrong playlist id {}, expected {}",
|
||||
playlist_id, ctx.id
|
||||
)));
|
||||
}
|
||||
|
||||
|
@ -165,7 +160,7 @@ impl MapResponse<Playlist> for response::Playlist {
|
|||
.and_then(|link| ChannelId::try_from(link).ok());
|
||||
|
||||
let last_update = last_update_txt.as_ref().and_then(|txt| {
|
||||
timeago::parse_textual_date_or_warn(lang, txt, &mut mapper.warnings)
|
||||
timeago::parse_textual_date_or_warn(ctx.lang, txt, &mut mapper.warnings)
|
||||
.map(OffsetDateTime::date)
|
||||
});
|
||||
|
||||
|
@ -177,7 +172,7 @@ impl MapResponse<Playlist> for response::Playlist {
|
|||
Some(n_videos),
|
||||
mapper.items,
|
||||
mapper.ctoken,
|
||||
vdata.map(str::to_owned),
|
||||
ctx.visitor_data.map(str::to_owned),
|
||||
ContinuationEndpoint::Browse,
|
||||
),
|
||||
video_count: n_videos,
|
||||
|
@ -189,7 +184,7 @@ impl MapResponse<Playlist> for response::Playlist {
|
|||
visitor_data: self
|
||||
.response_context
|
||||
.visitor_data
|
||||
.or_else(|| vdata.map(str::to_owned)),
|
||||
.or_else(|| ctx.visitor_data.map(str::to_owned)),
|
||||
},
|
||||
warnings: mapper.warnings,
|
||||
})
|
||||
|
@ -203,7 +198,7 @@ mod tests {
|
|||
use path_macro::path;
|
||||
use rstest::rstest;
|
||||
|
||||
use crate::{param::Language, util::tests::TESTFILES};
|
||||
use crate::util::tests::TESTFILES;
|
||||
|
||||
use super::*;
|
||||
|
||||
|
@ -218,7 +213,7 @@ mod tests {
|
|||
|
||||
let playlist: response::Playlist =
|
||||
serde_json::from_reader(BufReader::new(json_file)).unwrap();
|
||||
let map_res = playlist.map_response(id, Language::En, None, None).unwrap();
|
||||
let map_res = playlist.map_response(&MapRespCtx::test(id)).unwrap();
|
||||
|
||||
assert!(
|
||||
map_res.warnings.is_empty(),
|
||||
|
|
|
@ -95,11 +95,6 @@ pub(crate) struct HeaderRenderer {
|
|||
pub badges: Vec<ChannelBadge>,
|
||||
#[serde(default)]
|
||||
pub banner: Thumbnails,
|
||||
#[serde(default)]
|
||||
pub mobile_banner: Thumbnails,
|
||||
/// Fullscreen (16:9) channel banner
|
||||
#[serde(default)]
|
||||
pub tv_banner: Thumbnails,
|
||||
}
|
||||
|
||||
#[serde_as]
|
||||
|
@ -125,29 +120,35 @@ pub(crate) struct PageHeaderRenderer {
|
|||
pub page_header_view_model: PageHeaderRendererInner,
|
||||
}
|
||||
|
||||
#[serde_as]
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub(crate) struct PageHeaderRendererInner {
|
||||
/// Channel title (only used to extract verification badges)
|
||||
#[serde_as(as = "DefaultOnError")]
|
||||
pub title: PhTitleView,
|
||||
/// Channel avatar
|
||||
pub image: PhAvatarView,
|
||||
/// Channel metadata (subscribers, video count)
|
||||
pub metadata: PhMetadataView,
|
||||
#[serde(default)]
|
||||
pub banner: PhBannerView,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[derive(Default, Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub(crate) struct PhTitleView {
|
||||
pub dynamic_text_view_model: PhTitleView2,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[derive(Default, Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub(crate) struct PhTitleView2 {
|
||||
pub text: PhTitleView3,
|
||||
}
|
||||
|
||||
#[serde_as]
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[derive(Default, Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub(crate) struct PhTitleView3 {
|
||||
#[serde_as(as = "VecSkipError<_>")]
|
||||
|
@ -242,7 +243,7 @@ pub(crate) struct PhMetadataRow {
|
|||
pub metadata_parts: Vec<TextWrap>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[derive(Default, Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub(crate) struct PhBannerView {
|
||||
pub image_banner_view_model: ImageView,
|
||||
|
|
|
@ -34,7 +34,6 @@ pub(crate) use player::Player;
|
|||
pub(crate) use playlist::Playlist;
|
||||
pub(crate) use search::Search;
|
||||
pub(crate) use search::SearchSuggestion;
|
||||
pub(crate) use trends::Startpage;
|
||||
pub(crate) use trends::Trending;
|
||||
pub(crate) use url_endpoint::ResolvedUrl;
|
||||
pub(crate) use video_details::VideoComments;
|
||||
|
|
|
@ -236,7 +236,7 @@ pub(crate) struct CaptionTrack {
|
|||
#[serde(rename_all = "camelCase")]
|
||||
pub(crate) struct VideoDetails {
|
||||
pub video_id: String,
|
||||
pub title: String,
|
||||
pub title: Option<String>,
|
||||
#[serde_as(as = "DisplayFromStr")]
|
||||
pub length_seconds: u32,
|
||||
#[serde(default)]
|
||||
|
@ -245,9 +245,9 @@ pub(crate) struct VideoDetails {
|
|||
pub short_description: Option<String>,
|
||||
#[serde(default)]
|
||||
pub thumbnail: Thumbnails,
|
||||
#[serde_as(as = "DisplayFromStr")]
|
||||
pub view_count: u64,
|
||||
pub author: String,
|
||||
#[serde_as(as = "Option<DisplayFromStr>")]
|
||||
pub view_count: Option<u64>,
|
||||
pub author: Option<String>,
|
||||
pub is_live_content: bool,
|
||||
}
|
||||
|
||||
|
|
|
@ -1,13 +1,6 @@
|
|||
use serde::Deserialize;
|
||||
|
||||
use super::{video_item::YouTubeListRendererWrap, ResponseContext, Tab, TwoColumnBrowseResults};
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub(crate) struct Startpage {
|
||||
pub contents: Contents,
|
||||
pub response_context: ResponseContext,
|
||||
}
|
||||
use super::{video_item::YouTubeListRendererWrap, Tab, TwoColumnBrowseResults};
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
|
|
|
@ -12,7 +12,7 @@ use crate::{
|
|||
},
|
||||
param::Language,
|
||||
serializer::{
|
||||
text::{Text, TextComponent},
|
||||
text::{AttributedText, Text, TextComponent},
|
||||
MapResult,
|
||||
},
|
||||
util::{self, timeago, TryRemove},
|
||||
|
@ -25,6 +25,7 @@ pub(crate) enum YouTubeListItem {
|
|||
#[serde(alias = "gridVideoRenderer", alias = "compactVideoRenderer")]
|
||||
VideoRenderer(VideoRenderer),
|
||||
ReelItemRenderer(ReelItemRenderer),
|
||||
ShortsLockupViewModel(ShortsLockupViewModel),
|
||||
PlaylistVideoRenderer(PlaylistVideoRenderer),
|
||||
|
||||
#[serde(alias = "gridPlaylistRenderer")]
|
||||
|
@ -142,6 +143,28 @@ pub(crate) struct ReelItemRenderer {
|
|||
pub navigation_endpoint: Option<ReelNavigationEndpoint>,
|
||||
}
|
||||
|
||||
// New short video item
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub(crate) struct ShortsLockupViewModel {
|
||||
/// `shorts-shelf-item-[video_id]`
|
||||
pub entity_id: String,
|
||||
pub thumbnail: Thumbnails,
|
||||
pub overlay_metadata: ShortsOverlayMetadata,
|
||||
}
|
||||
|
||||
#[serde_as]
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub(crate) struct ShortsOverlayMetadata {
|
||||
/// Title
|
||||
#[serde_as(as = "AttributedText")]
|
||||
pub primary_text: String,
|
||||
/// View count
|
||||
#[serde_as(as = "Option<AttributedText>")]
|
||||
pub secondary_text: Option<String>,
|
||||
}
|
||||
|
||||
/// Video displayed in a playlist
|
||||
#[serde_as]
|
||||
#[derive(Debug, Deserialize)]
|
||||
|
@ -517,6 +540,31 @@ impl<T> YouTubeListMapper<T> {
|
|||
}
|
||||
}
|
||||
|
||||
fn map_short_video2(&mut self, video: ShortsLockupViewModel) -> Option<VideoItem> {
|
||||
if let Some(video_id) = video.entity_id.strip_prefix("shorts-shelf-item-") {
|
||||
Some(VideoItem {
|
||||
id: video_id.to_owned(),
|
||||
name: video.overlay_metadata.primary_text,
|
||||
duration: None,
|
||||
thumbnail: video.thumbnail.into(),
|
||||
channel: self.channel.clone(),
|
||||
publish_date: None,
|
||||
publish_date_txt: None,
|
||||
view_count: video.overlay_metadata.secondary_text.and_then(|txt| {
|
||||
util::parse_large_numstr_or_warn(&txt, self.lang, &mut self.warnings)
|
||||
}),
|
||||
is_live: false,
|
||||
is_short: true,
|
||||
is_upcoming: false,
|
||||
short_description: None,
|
||||
})
|
||||
} else {
|
||||
self.warnings
|
||||
.push(format!("invalid shorts entityId: {}", video.entity_id));
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn map_playlist_video(&mut self, video: PlaylistVideoRenderer) -> VideoItem {
|
||||
let channel = ChannelId::try_from(video.channel)
|
||||
.ok()
|
||||
|
@ -610,28 +658,26 @@ impl<T> YouTubeListMapper<T> {
|
|||
|
||||
fn map_channel(&mut self, channel: ChannelRenderer) -> ChannelItem {
|
||||
// channel handle instead of subscriber count (A/B test 3)
|
||||
let (sc_txt, vc_text) = if channel
|
||||
let (handle, sc_txt) = if channel
|
||||
.subscriber_count_text
|
||||
.as_ref()
|
||||
.map(|txt| txt.starts_with('@'))
|
||||
.unwrap_or_default()
|
||||
{
|
||||
(channel.video_count_text, None)
|
||||
} else {
|
||||
(channel.subscriber_count_text, channel.video_count_text)
|
||||
} else {
|
||||
(None, channel.subscriber_count_text)
|
||||
};
|
||||
|
||||
ChannelItem {
|
||||
id: channel.channel_id,
|
||||
name: channel.title,
|
||||
handle,
|
||||
avatar: channel.thumbnail.into(),
|
||||
verification: channel.owner_badges.into(),
|
||||
subscriber_count: sc_txt.and_then(|txt| {
|
||||
util::parse_large_numstr_or_warn(&txt, self.lang, &mut self.warnings)
|
||||
}),
|
||||
video_count: vc_text.and_then(|txt| {
|
||||
util::parse_large_numstr_or_warn(&txt, self.lang, &mut self.warnings)
|
||||
}),
|
||||
short_description: channel.description_snippet,
|
||||
}
|
||||
}
|
||||
|
@ -644,6 +690,11 @@ impl YouTubeListMapper<YouTubeItem> {
|
|||
let mapped = YouTubeItem::Video(self.map_video(video));
|
||||
self.items.push(mapped);
|
||||
}
|
||||
YouTubeListItem::ShortsLockupViewModel(video) => {
|
||||
if let Some(mapped) = self.map_short_video2(video) {
|
||||
self.items.push(YouTubeItem::Video(mapped));
|
||||
}
|
||||
}
|
||||
YouTubeListItem::ReelItemRenderer(video) => {
|
||||
let mapped = self.map_short_video(video);
|
||||
self.items.push(YouTubeItem::Video(mapped));
|
||||
|
@ -694,6 +745,11 @@ impl YouTubeListMapper<VideoItem> {
|
|||
let mapped = self.map_short_video(video);
|
||||
self.items.push(mapped);
|
||||
}
|
||||
YouTubeListItem::ShortsLockupViewModel(video) => {
|
||||
if let Some(mapped) = self.map_short_video2(video) {
|
||||
self.items.push(mapped);
|
||||
}
|
||||
}
|
||||
YouTubeListItem::PlaylistVideoRenderer(video) => {
|
||||
let mapped = self.map_playlist_video(video);
|
||||
self.items.push(mapped);
|
||||
|
|
|
@ -12,7 +12,7 @@ use crate::{
|
|||
param::search_filter::SearchFilter,
|
||||
};
|
||||
|
||||
use super::{response, ClientType, MapResponse, MapResult, RustyPipeQuery, YTContext};
|
||||
use super::{response, ClientType, MapRespCtx, MapResponse, MapResult, RustyPipeQuery, YTContext};
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
|
@ -24,7 +24,7 @@ struct QSearch<'a> {
|
|||
|
||||
impl RustyPipeQuery {
|
||||
/// Search YouTube
|
||||
#[tracing::instrument(skip(self))]
|
||||
#[tracing::instrument(skip(self), level = "error")]
|
||||
pub async fn search<T: FromYtItem, S: AsRef<str> + Debug>(
|
||||
&self,
|
||||
query: S,
|
||||
|
@ -48,7 +48,7 @@ impl RustyPipeQuery {
|
|||
}
|
||||
|
||||
/// Search YouTube using the given [`SearchFilter`]
|
||||
#[tracing::instrument(skip(self))]
|
||||
#[tracing::instrument(skip(self), level = "error")]
|
||||
pub async fn search_filter<T: FromYtItem, S: AsRef<str> + Debug>(
|
||||
&self,
|
||||
query: S,
|
||||
|
@ -73,7 +73,7 @@ impl RustyPipeQuery {
|
|||
}
|
||||
|
||||
/// Get YouTube search suggestions
|
||||
#[tracing::instrument(skip(self))]
|
||||
#[tracing::instrument(skip(self), level = "error")]
|
||||
pub async fn search_suggestion<S: AsRef<str> + Debug>(
|
||||
&self,
|
||||
query: S,
|
||||
|
@ -103,10 +103,7 @@ impl RustyPipeQuery {
|
|||
impl<T: FromYtItem> MapResponse<SearchResult<T>> for response::Search {
|
||||
fn map_response(
|
||||
self,
|
||||
_id: &str,
|
||||
lang: crate::param::Language,
|
||||
_deobf: Option<&crate::deobfuscate::DeobfData>,
|
||||
vdata: Option<&str>,
|
||||
ctx: &MapRespCtx<'_>,
|
||||
) -> Result<MapResult<SearchResult<T>>, ExtractionError> {
|
||||
let items = self
|
||||
.contents
|
||||
|
@ -115,7 +112,7 @@ impl<T: FromYtItem> MapResponse<SearchResult<T>> for response::Search {
|
|||
.section_list_renderer
|
||||
.contents;
|
||||
|
||||
let mut mapper = response::YouTubeListMapper::<YouTubeItem>::new(lang);
|
||||
let mut mapper = response::YouTubeListMapper::<YouTubeItem>::new(ctx.lang);
|
||||
mapper.map_response(items);
|
||||
|
||||
Ok(MapResult {
|
||||
|
@ -135,7 +132,7 @@ impl<T: FromYtItem> MapResponse<SearchResult<T>> for response::Search {
|
|||
visitor_data: self
|
||||
.response_context
|
||||
.visitor_data
|
||||
.or_else(|| vdata.map(str::to_owned)),
|
||||
.or_else(|| ctx.visitor_data.map(str::to_owned)),
|
||||
},
|
||||
warnings: mapper.warnings,
|
||||
})
|
||||
|
@ -150,9 +147,8 @@ mod tests {
|
|||
use rstest::rstest;
|
||||
|
||||
use crate::{
|
||||
client::{response, MapResponse},
|
||||
client::{response, MapRespCtx, MapResponse},
|
||||
model::{SearchResult, YouTubeItem},
|
||||
param::Language,
|
||||
serializer::MapResult,
|
||||
util::tests::TESTFILES,
|
||||
};
|
||||
|
@ -168,7 +164,7 @@ mod tests {
|
|||
|
||||
let search: response::Search = serde_json::from_reader(BufReader::new(json_file)).unwrap();
|
||||
let map_res: MapResult<SearchResult<YouTubeItem>> =
|
||||
search.map_response("", Language::En, None, None).unwrap();
|
||||
search.map_response(&MapRespCtx::test("")).unwrap();
|
||||
|
||||
assert!(
|
||||
map_res.warnings.is_empty(),
|
||||
|
|
|
@ -5,7 +5,9 @@ expression: map_res.c
|
|||
Channel(
|
||||
id: "UC2DjFE7Xf11URZqWBigcVOQ",
|
||||
name: "EEVblog",
|
||||
handle: None,
|
||||
subscriber_count: Some(884000),
|
||||
video_count: None,
|
||||
avatar: [
|
||||
Thumbnail(
|
||||
url: "https://yt3.ggpht.com/ytc/AMLnZu9eKk4Nd16fX4Rn1TF1G7ReluwOl6M5558FTYAM=s48-c-k-c0x00ffffff-no-rj",
|
||||
|
@ -55,7 +57,6 @@ Channel(
|
|||
"dumpster diving",
|
||||
"debunking",
|
||||
],
|
||||
vanity_url: Some("https://www.youtube.com/c/EevblogDave"),
|
||||
banner: [
|
||||
Thumbnail(
|
||||
url: "https://yt3.ggpht.com/yIJ9ad80n49rK-YUcZLe_8bLmR-aGyg5ybDH_XKIc0GDWrC6s1Wzz8lxnq3_hux_5b6NHPZ9=w1060-fcrop64=1,00005a57ffffa5a8-k-c0xffffffff-no-nd-rj",
|
||||
|
@ -88,60 +89,6 @@ Channel(
|
|||
height: 424,
|
||||
),
|
||||
],
|
||||
mobile_banner: [
|
||||
Thumbnail(
|
||||
url: "https://yt3.ggpht.com/yIJ9ad80n49rK-YUcZLe_8bLmR-aGyg5ybDH_XKIc0GDWrC6s1Wzz8lxnq3_hux_5b6NHPZ9=w320-fcrop64=1,32b75a57cd48a5a8-k-c0xffffffff-no-nd-rj",
|
||||
width: 320,
|
||||
height: 88,
|
||||
),
|
||||
Thumbnail(
|
||||
url: "https://yt3.ggpht.com/yIJ9ad80n49rK-YUcZLe_8bLmR-aGyg5ybDH_XKIc0GDWrC6s1Wzz8lxnq3_hux_5b6NHPZ9=w640-fcrop64=1,32b75a57cd48a5a8-k-c0xffffffff-no-nd-rj",
|
||||
width: 640,
|
||||
height: 175,
|
||||
),
|
||||
Thumbnail(
|
||||
url: "https://yt3.ggpht.com/yIJ9ad80n49rK-YUcZLe_8bLmR-aGyg5ybDH_XKIc0GDWrC6s1Wzz8lxnq3_hux_5b6NHPZ9=w960-fcrop64=1,32b75a57cd48a5a8-k-c0xffffffff-no-nd-rj",
|
||||
width: 960,
|
||||
height: 263,
|
||||
),
|
||||
Thumbnail(
|
||||
url: "https://yt3.ggpht.com/yIJ9ad80n49rK-YUcZLe_8bLmR-aGyg5ybDH_XKIc0GDWrC6s1Wzz8lxnq3_hux_5b6NHPZ9=w1280-fcrop64=1,32b75a57cd48a5a8-k-c0xffffffff-no-nd-rj",
|
||||
width: 1280,
|
||||
height: 351,
|
||||
),
|
||||
Thumbnail(
|
||||
url: "https://yt3.ggpht.com/yIJ9ad80n49rK-YUcZLe_8bLmR-aGyg5ybDH_XKIc0GDWrC6s1Wzz8lxnq3_hux_5b6NHPZ9=w1440-fcrop64=1,32b75a57cd48a5a8-k-c0xffffffff-no-nd-rj",
|
||||
width: 1440,
|
||||
height: 395,
|
||||
),
|
||||
],
|
||||
tv_banner: [
|
||||
Thumbnail(
|
||||
url: "https://yt3.ggpht.com/yIJ9ad80n49rK-YUcZLe_8bLmR-aGyg5ybDH_XKIc0GDWrC6s1Wzz8lxnq3_hux_5b6NHPZ9=w320-fcrop64=1,00000000ffffffff-k-c0xffffffff-no-nd-rj",
|
||||
width: 320,
|
||||
height: 180,
|
||||
),
|
||||
Thumbnail(
|
||||
url: "https://yt3.ggpht.com/yIJ9ad80n49rK-YUcZLe_8bLmR-aGyg5ybDH_XKIc0GDWrC6s1Wzz8lxnq3_hux_5b6NHPZ9=w854-fcrop64=1,00000000ffffffff-k-c0xffffffff-no-nd-rj",
|
||||
width: 854,
|
||||
height: 480,
|
||||
),
|
||||
Thumbnail(
|
||||
url: "https://yt3.ggpht.com/yIJ9ad80n49rK-YUcZLe_8bLmR-aGyg5ybDH_XKIc0GDWrC6s1Wzz8lxnq3_hux_5b6NHPZ9=w1280-fcrop64=1,00000000ffffffff-k-c0xffffffff-no-nd-rj",
|
||||
width: 1280,
|
||||
height: 720,
|
||||
),
|
||||
Thumbnail(
|
||||
url: "https://yt3.ggpht.com/yIJ9ad80n49rK-YUcZLe_8bLmR-aGyg5ybDH_XKIc0GDWrC6s1Wzz8lxnq3_hux_5b6NHPZ9=w1920-fcrop64=1,00000000ffffffff-k-c0xffffffff-no-nd-rj",
|
||||
width: 1920,
|
||||
height: 1080,
|
||||
),
|
||||
Thumbnail(
|
||||
url: "https://yt3.ggpht.com/yIJ9ad80n49rK-YUcZLe_8bLmR-aGyg5ybDH_XKIc0GDWrC6s1Wzz8lxnq3_hux_5b6NHPZ9=w2120-fcrop64=1,00000000ffffffff-k-c0xffffffff-no-nd-rj",
|
||||
width: 2120,
|
||||
height: 1192,
|
||||
),
|
||||
],
|
||||
has_shorts: false,
|
||||
has_live: true,
|
||||
visitor_data: None,
|
||||
|
|
|
@ -5,7 +5,9 @@ expression: map_res.c
|
|||
Channel(
|
||||
id: "UC2DjFE7Xf11URZqWBigcVOQ",
|
||||
name: "EEVblog",
|
||||
handle: None,
|
||||
subscriber_count: Some(881000),
|
||||
video_count: None,
|
||||
avatar: [
|
||||
Thumbnail(
|
||||
url: "https://yt3.ggpht.com/ytc/AMLnZu9eKk4Nd16fX4Rn1TF1G7ReluwOl6M5558FTYAM=s48-c-k-c0x00ffffff-no-rj",
|
||||
|
@ -55,7 +57,6 @@ Channel(
|
|||
"dumpster diving",
|
||||
"debunking",
|
||||
],
|
||||
vanity_url: Some("https://www.youtube.com/c/EevblogDave"),
|
||||
banner: [
|
||||
Thumbnail(
|
||||
url: "https://yt3.ggpht.com/yIJ9ad80n49rK-YUcZLe_8bLmR-aGyg5ybDH_XKIc0GDWrC6s1Wzz8lxnq3_hux_5b6NHPZ9=w1060-fcrop64=1,00005a57ffffa5a8-k-c0xffffffff-no-nd-rj",
|
||||
|
@ -88,60 +89,6 @@ Channel(
|
|||
height: 424,
|
||||
),
|
||||
],
|
||||
mobile_banner: [
|
||||
Thumbnail(
|
||||
url: "https://yt3.ggpht.com/yIJ9ad80n49rK-YUcZLe_8bLmR-aGyg5ybDH_XKIc0GDWrC6s1Wzz8lxnq3_hux_5b6NHPZ9=w320-fcrop64=1,32b75a57cd48a5a8-k-c0xffffffff-no-nd-rj",
|
||||
width: 320,
|
||||
height: 88,
|
||||
),
|
||||
Thumbnail(
|
||||
url: "https://yt3.ggpht.com/yIJ9ad80n49rK-YUcZLe_8bLmR-aGyg5ybDH_XKIc0GDWrC6s1Wzz8lxnq3_hux_5b6NHPZ9=w640-fcrop64=1,32b75a57cd48a5a8-k-c0xffffffff-no-nd-rj",
|
||||
width: 640,
|
||||
height: 175,
|
||||
),
|
||||
Thumbnail(
|
||||
url: "https://yt3.ggpht.com/yIJ9ad80n49rK-YUcZLe_8bLmR-aGyg5ybDH_XKIc0GDWrC6s1Wzz8lxnq3_hux_5b6NHPZ9=w960-fcrop64=1,32b75a57cd48a5a8-k-c0xffffffff-no-nd-rj",
|
||||
width: 960,
|
||||
height: 263,
|
||||
),
|
||||
Thumbnail(
|
||||
url: "https://yt3.ggpht.com/yIJ9ad80n49rK-YUcZLe_8bLmR-aGyg5ybDH_XKIc0GDWrC6s1Wzz8lxnq3_hux_5b6NHPZ9=w1280-fcrop64=1,32b75a57cd48a5a8-k-c0xffffffff-no-nd-rj",
|
||||
width: 1280,
|
||||
height: 351,
|
||||
),
|
||||
Thumbnail(
|
||||
url: "https://yt3.ggpht.com/yIJ9ad80n49rK-YUcZLe_8bLmR-aGyg5ybDH_XKIc0GDWrC6s1Wzz8lxnq3_hux_5b6NHPZ9=w1440-fcrop64=1,32b75a57cd48a5a8-k-c0xffffffff-no-nd-rj",
|
||||
width: 1440,
|
||||
height: 395,
|
||||
),
|
||||
],
|
||||
tv_banner: [
|
||||
Thumbnail(
|
||||
url: "https://yt3.ggpht.com/yIJ9ad80n49rK-YUcZLe_8bLmR-aGyg5ybDH_XKIc0GDWrC6s1Wzz8lxnq3_hux_5b6NHPZ9=w320-fcrop64=1,00000000ffffffff-k-c0xffffffff-no-nd-rj",
|
||||
width: 320,
|
||||
height: 180,
|
||||
),
|
||||
Thumbnail(
|
||||
url: "https://yt3.ggpht.com/yIJ9ad80n49rK-YUcZLe_8bLmR-aGyg5ybDH_XKIc0GDWrC6s1Wzz8lxnq3_hux_5b6NHPZ9=w854-fcrop64=1,00000000ffffffff-k-c0xffffffff-no-nd-rj",
|
||||
width: 854,
|
||||
height: 480,
|
||||
),
|
||||
Thumbnail(
|
||||
url: "https://yt3.ggpht.com/yIJ9ad80n49rK-YUcZLe_8bLmR-aGyg5ybDH_XKIc0GDWrC6s1Wzz8lxnq3_hux_5b6NHPZ9=w1280-fcrop64=1,00000000ffffffff-k-c0xffffffff-no-nd-rj",
|
||||
width: 1280,
|
||||
height: 720,
|
||||
),
|
||||
Thumbnail(
|
||||
url: "https://yt3.ggpht.com/yIJ9ad80n49rK-YUcZLe_8bLmR-aGyg5ybDH_XKIc0GDWrC6s1Wzz8lxnq3_hux_5b6NHPZ9=w1920-fcrop64=1,00000000ffffffff-k-c0xffffffff-no-nd-rj",
|
||||
width: 1920,
|
||||
height: 1080,
|
||||
),
|
||||
Thumbnail(
|
||||
url: "https://yt3.ggpht.com/yIJ9ad80n49rK-YUcZLe_8bLmR-aGyg5ybDH_XKIc0GDWrC6s1Wzz8lxnq3_hux_5b6NHPZ9=w2120-fcrop64=1,00000000ffffffff-k-c0xffffffff-no-nd-rj",
|
||||
width: 2120,
|
||||
height: 1192,
|
||||
),
|
||||
],
|
||||
has_shorts: false,
|
||||
has_live: false,
|
||||
visitor_data: Some("CgttaWpyTVpUN1AyZyioqr2ZBg%3D%3D"),
|
||||
|
|
|
@ -5,7 +5,9 @@ expression: map_res.c
|
|||
Channel(
|
||||
id: "UCh8gHdtzO2tXd593_bjErWg",
|
||||
name: "Doobydobap",
|
||||
handle: Some("@Doobydobap"),
|
||||
subscriber_count: Some(3360000),
|
||||
video_count: None,
|
||||
avatar: [
|
||||
Thumbnail(
|
||||
url: "https://yt3.googleusercontent.com/dm5Aq93xvVJz0NoVO88ieBkDXmuShCujGPlZ7qETMEPTrXvPUCFI3-BB6Xs_P-r6Uk3mnBy9zA=s48-c-k-c0x00ffffff-no-rj",
|
||||
|
@ -26,7 +28,6 @@ Channel(
|
|||
verification: Verified,
|
||||
description: "Hi, I’m Tina, aka Doobydobap!\n\nFood is the medium I use to tell stories and connect with people who share the same passion as I do. Whether it’s because you’re hungry at midnight or trying to learn how to cook, I hope you enjoy watching my content and recipes. Don\'t yuck my yum!\n\nwww.doobydobap.com\n",
|
||||
tags: [],
|
||||
vanity_url: Some("https://www.youtube.com/@Doobydobap"),
|
||||
banner: [
|
||||
Thumbnail(
|
||||
url: "https://yt3.googleusercontent.com/BvnAqgiursrXpmS9AgDLtkOSTQfOG_Dqn0KzY5hcwO9XrHTEQTVgaflI913f9KRp7d0U2qBp=w1060-fcrop64=1,00005a57ffffa5a8-k-c0xffffffff-no-nd-rj",
|
||||
|
@ -59,60 +60,6 @@ Channel(
|
|||
height: 424,
|
||||
),
|
||||
],
|
||||
mobile_banner: [
|
||||
Thumbnail(
|
||||
url: "https://yt3.googleusercontent.com/BvnAqgiursrXpmS9AgDLtkOSTQfOG_Dqn0KzY5hcwO9XrHTEQTVgaflI913f9KRp7d0U2qBp=w320-fcrop64=1,32b75a57cd48a5a8-k-c0xffffffff-no-nd-rj",
|
||||
width: 320,
|
||||
height: 88,
|
||||
),
|
||||
Thumbnail(
|
||||
url: "https://yt3.googleusercontent.com/BvnAqgiursrXpmS9AgDLtkOSTQfOG_Dqn0KzY5hcwO9XrHTEQTVgaflI913f9KRp7d0U2qBp=w640-fcrop64=1,32b75a57cd48a5a8-k-c0xffffffff-no-nd-rj",
|
||||
width: 640,
|
||||
height: 175,
|
||||
),
|
||||
Thumbnail(
|
||||
url: "https://yt3.googleusercontent.com/BvnAqgiursrXpmS9AgDLtkOSTQfOG_Dqn0KzY5hcwO9XrHTEQTVgaflI913f9KRp7d0U2qBp=w960-fcrop64=1,32b75a57cd48a5a8-k-c0xffffffff-no-nd-rj",
|
||||
width: 960,
|
||||
height: 263,
|
||||
),
|
||||
Thumbnail(
|
||||
url: "https://yt3.googleusercontent.com/BvnAqgiursrXpmS9AgDLtkOSTQfOG_Dqn0KzY5hcwO9XrHTEQTVgaflI913f9KRp7d0U2qBp=w1280-fcrop64=1,32b75a57cd48a5a8-k-c0xffffffff-no-nd-rj",
|
||||
width: 1280,
|
||||
height: 351,
|
||||
),
|
||||
Thumbnail(
|
||||
url: "https://yt3.googleusercontent.com/BvnAqgiursrXpmS9AgDLtkOSTQfOG_Dqn0KzY5hcwO9XrHTEQTVgaflI913f9KRp7d0U2qBp=w1440-fcrop64=1,32b75a57cd48a5a8-k-c0xffffffff-no-nd-rj",
|
||||
width: 1440,
|
||||
height: 395,
|
||||
),
|
||||
],
|
||||
tv_banner: [
|
||||
Thumbnail(
|
||||
url: "https://yt3.googleusercontent.com/BvnAqgiursrXpmS9AgDLtkOSTQfOG_Dqn0KzY5hcwO9XrHTEQTVgaflI913f9KRp7d0U2qBp=w320-fcrop64=1,00000000ffffffff-k-c0xffffffff-no-nd-rj",
|
||||
width: 320,
|
||||
height: 180,
|
||||
),
|
||||
Thumbnail(
|
||||
url: "https://yt3.googleusercontent.com/BvnAqgiursrXpmS9AgDLtkOSTQfOG_Dqn0KzY5hcwO9XrHTEQTVgaflI913f9KRp7d0U2qBp=w854-fcrop64=1,00000000ffffffff-k-c0xffffffff-no-nd-rj",
|
||||
width: 854,
|
||||
height: 480,
|
||||
),
|
||||
Thumbnail(
|
||||
url: "https://yt3.googleusercontent.com/BvnAqgiursrXpmS9AgDLtkOSTQfOG_Dqn0KzY5hcwO9XrHTEQTVgaflI913f9KRp7d0U2qBp=w1280-fcrop64=1,00000000ffffffff-k-c0xffffffff-no-nd-rj",
|
||||
width: 1280,
|
||||
height: 720,
|
||||
),
|
||||
Thumbnail(
|
||||
url: "https://yt3.googleusercontent.com/BvnAqgiursrXpmS9AgDLtkOSTQfOG_Dqn0KzY5hcwO9XrHTEQTVgaflI913f9KRp7d0U2qBp=w1920-fcrop64=1,00000000ffffffff-k-c0xffffffff-no-nd-rj",
|
||||
width: 1920,
|
||||
height: 1080,
|
||||
),
|
||||
Thumbnail(
|
||||
url: "https://yt3.googleusercontent.com/BvnAqgiursrXpmS9AgDLtkOSTQfOG_Dqn0KzY5hcwO9XrHTEQTVgaflI913f9KRp7d0U2qBp=w2120-fcrop64=1,00000000ffffffff-k-c0xffffffff-no-nd-rj",
|
||||
width: 2120,
|
||||
height: 1192,
|
||||
),
|
||||
],
|
||||
has_shorts: true,
|
||||
has_live: false,
|
||||
visitor_data: Some("CgtHU1dvWkR4cGRfdyjMpt6iBg%3D%3D"),
|
||||
|
|
|
@ -5,7 +5,9 @@ expression: map_res.c
|
|||
Channel(
|
||||
id: "UCh8gHdtzO2tXd593_bjErWg",
|
||||
name: "Doobydobap",
|
||||
handle: Some("@Doobydobap"),
|
||||
subscriber_count: Some(3740000),
|
||||
video_count: None,
|
||||
avatar: [
|
||||
Thumbnail(
|
||||
url: "https://yt3.googleusercontent.com/dm5Aq93xvVJz0NoVO88ieBkDXmuShCujGPlZ7qETMEPTrXvPUCFI3-BB6Xs_P-r6Uk3mnBy9zA=s72-c-k-c0x00ffffff-no-rj",
|
||||
|
@ -26,7 +28,6 @@ Channel(
|
|||
verification: Verified,
|
||||
description: "Hi, I’m Tina, aka Doobydobap!\n\nFood is the medium I use to tell stories and connect with people who share the same passion as I do. Whether it’s because you’re hungry at midnight or trying to learn how to cook, I hope you enjoy watching my content and recipes. Don\'t yuck my yum!\n\nwww.doobydobap.com\n",
|
||||
tags: [],
|
||||
vanity_url: Some("https://www.youtube.com/@Doobydobap"),
|
||||
banner: [
|
||||
Thumbnail(
|
||||
url: "https://yt3.googleusercontent.com/BvnAqgiursrXpmS9AgDLtkOSTQfOG_Dqn0KzY5hcwO9XrHTEQTVgaflI913f9KRp7d0U2qBp=w1060-fcrop64=1,00005a57ffffa5a8-k-c0xffffffff-no-nd-rj",
|
||||
|
@ -59,8 +60,6 @@ Channel(
|
|||
height: 424,
|
||||
),
|
||||
],
|
||||
mobile_banner: [],
|
||||
tv_banner: [],
|
||||
has_shorts: true,
|
||||
has_live: false,
|
||||
visitor_data: None,
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -5,7 +5,9 @@ expression: map_res.c
|
|||
Channel(
|
||||
id: "UCh8gHdtzO2tXd593_bjErWg",
|
||||
name: "Doobydobap",
|
||||
handle: None,
|
||||
subscriber_count: Some(2930000),
|
||||
video_count: None,
|
||||
avatar: [
|
||||
Thumbnail(
|
||||
url: "https://yt3.ggpht.com/dm5Aq93xvVJz0NoVO88ieBkDXmuShCujGPlZ7qETMEPTrXvPUCFI3-BB6Xs_P-r6Uk3mnBy9zA=s48-c-k-c0x00ffffff-no-rj",
|
||||
|
@ -26,7 +28,6 @@ Channel(
|
|||
verification: Verified,
|
||||
description: "Hi, I’m Tina, aka Doobydobap!\n\nFood is the medium I use to tell stories and connect with people who share the same passion as I do. Whether it’s because you’re hungry at midnight or trying to learn how to cook, I hope you enjoy watching my content and recipes. Don\'t yuck my yum!\n\nwww.doobydobap.com\n",
|
||||
tags: [],
|
||||
vanity_url: Some("https://www.youtube.com/c/Doobydobap"),
|
||||
banner: [
|
||||
Thumbnail(
|
||||
url: "https://yt3.ggpht.com/BvnAqgiursrXpmS9AgDLtkOSTQfOG_Dqn0KzY5hcwO9XrHTEQTVgaflI913f9KRp7d0U2qBp=w1060-fcrop64=1,00005a57ffffa5a8-k-c0xffffffff-no-nd-rj",
|
||||
|
@ -59,60 +60,6 @@ Channel(
|
|||
height: 424,
|
||||
),
|
||||
],
|
||||
mobile_banner: [
|
||||
Thumbnail(
|
||||
url: "https://yt3.ggpht.com/BvnAqgiursrXpmS9AgDLtkOSTQfOG_Dqn0KzY5hcwO9XrHTEQTVgaflI913f9KRp7d0U2qBp=w320-fcrop64=1,32b75a57cd48a5a8-k-c0xffffffff-no-nd-rj",
|
||||
width: 320,
|
||||
height: 88,
|
||||
),
|
||||
Thumbnail(
|
||||
url: "https://yt3.ggpht.com/BvnAqgiursrXpmS9AgDLtkOSTQfOG_Dqn0KzY5hcwO9XrHTEQTVgaflI913f9KRp7d0U2qBp=w640-fcrop64=1,32b75a57cd48a5a8-k-c0xffffffff-no-nd-rj",
|
||||
width: 640,
|
||||
height: 175,
|
||||
),
|
||||
Thumbnail(
|
||||
url: "https://yt3.ggpht.com/BvnAqgiursrXpmS9AgDLtkOSTQfOG_Dqn0KzY5hcwO9XrHTEQTVgaflI913f9KRp7d0U2qBp=w960-fcrop64=1,32b75a57cd48a5a8-k-c0xffffffff-no-nd-rj",
|
||||
width: 960,
|
||||
height: 263,
|
||||
),
|
||||
Thumbnail(
|
||||
url: "https://yt3.ggpht.com/BvnAqgiursrXpmS9AgDLtkOSTQfOG_Dqn0KzY5hcwO9XrHTEQTVgaflI913f9KRp7d0U2qBp=w1280-fcrop64=1,32b75a57cd48a5a8-k-c0xffffffff-no-nd-rj",
|
||||
width: 1280,
|
||||
height: 351,
|
||||
),
|
||||
Thumbnail(
|
||||
url: "https://yt3.ggpht.com/BvnAqgiursrXpmS9AgDLtkOSTQfOG_Dqn0KzY5hcwO9XrHTEQTVgaflI913f9KRp7d0U2qBp=w1440-fcrop64=1,32b75a57cd48a5a8-k-c0xffffffff-no-nd-rj",
|
||||
width: 1440,
|
||||
height: 395,
|
||||
),
|
||||
],
|
||||
tv_banner: [
|
||||
Thumbnail(
|
||||
url: "https://yt3.ggpht.com/BvnAqgiursrXpmS9AgDLtkOSTQfOG_Dqn0KzY5hcwO9XrHTEQTVgaflI913f9KRp7d0U2qBp=w320-fcrop64=1,00000000ffffffff-k-c0xffffffff-no-nd-rj",
|
||||
width: 320,
|
||||
height: 180,
|
||||
),
|
||||
Thumbnail(
|
||||
url: "https://yt3.ggpht.com/BvnAqgiursrXpmS9AgDLtkOSTQfOG_Dqn0KzY5hcwO9XrHTEQTVgaflI913f9KRp7d0U2qBp=w854-fcrop64=1,00000000ffffffff-k-c0xffffffff-no-nd-rj",
|
||||
width: 854,
|
||||
height: 480,
|
||||
),
|
||||
Thumbnail(
|
||||
url: "https://yt3.ggpht.com/BvnAqgiursrXpmS9AgDLtkOSTQfOG_Dqn0KzY5hcwO9XrHTEQTVgaflI913f9KRp7d0U2qBp=w1280-fcrop64=1,00000000ffffffff-k-c0xffffffff-no-nd-rj",
|
||||
width: 1280,
|
||||
height: 720,
|
||||
),
|
||||
Thumbnail(
|
||||
url: "https://yt3.ggpht.com/BvnAqgiursrXpmS9AgDLtkOSTQfOG_Dqn0KzY5hcwO9XrHTEQTVgaflI913f9KRp7d0U2qBp=w1920-fcrop64=1,00000000ffffffff-k-c0xffffffff-no-nd-rj",
|
||||
width: 1920,
|
||||
height: 1080,
|
||||
),
|
||||
Thumbnail(
|
||||
url: "https://yt3.ggpht.com/BvnAqgiursrXpmS9AgDLtkOSTQfOG_Dqn0KzY5hcwO9XrHTEQTVgaflI913f9KRp7d0U2qBp=w2120-fcrop64=1,00000000ffffffff-k-c0xffffffff-no-nd-rj",
|
||||
width: 2120,
|
||||
height: 1192,
|
||||
),
|
||||
],
|
||||
has_shorts: true,
|
||||
has_live: false,
|
||||
visitor_data: Some("CgtQdE9zVVR3NVBDbyjz0ZKaBg%3D%3D"),
|
||||
|
|
|
@ -5,7 +5,9 @@ expression: map_res.c
|
|||
Channel(
|
||||
id: "UC2DjFE7Xf11URZqWBigcVOQ",
|
||||
name: "EEVblog",
|
||||
handle: None,
|
||||
subscriber_count: Some(883000),
|
||||
video_count: None,
|
||||
avatar: [
|
||||
Thumbnail(
|
||||
url: "https://yt3.ggpht.com/ytc/AMLnZu9eKk4Nd16fX4Rn1TF1G7ReluwOl6M5558FTYAM=s48-c-k-c0x00ffffff-no-rj",
|
||||
|
@ -55,7 +57,6 @@ Channel(
|
|||
"dumpster diving",
|
||||
"debunking",
|
||||
],
|
||||
vanity_url: Some("https://www.youtube.com/c/EevblogDave"),
|
||||
banner: [
|
||||
Thumbnail(
|
||||
url: "https://yt3.ggpht.com/yIJ9ad80n49rK-YUcZLe_8bLmR-aGyg5ybDH_XKIc0GDWrC6s1Wzz8lxnq3_hux_5b6NHPZ9=w1060-fcrop64=1,00005a57ffffa5a8-k-c0xffffffff-no-nd-rj",
|
||||
|
@ -88,60 +89,6 @@ Channel(
|
|||
height: 424,
|
||||
),
|
||||
],
|
||||
mobile_banner: [
|
||||
Thumbnail(
|
||||
url: "https://yt3.ggpht.com/yIJ9ad80n49rK-YUcZLe_8bLmR-aGyg5ybDH_XKIc0GDWrC6s1Wzz8lxnq3_hux_5b6NHPZ9=w320-fcrop64=1,32b75a57cd48a5a8-k-c0xffffffff-no-nd-rj",
|
||||
width: 320,
|
||||
height: 88,
|
||||
),
|
||||
Thumbnail(
|
||||
url: "https://yt3.ggpht.com/yIJ9ad80n49rK-YUcZLe_8bLmR-aGyg5ybDH_XKIc0GDWrC6s1Wzz8lxnq3_hux_5b6NHPZ9=w640-fcrop64=1,32b75a57cd48a5a8-k-c0xffffffff-no-nd-rj",
|
||||
width: 640,
|
||||
height: 175,
|
||||
),
|
||||
Thumbnail(
|
||||
url: "https://yt3.ggpht.com/yIJ9ad80n49rK-YUcZLe_8bLmR-aGyg5ybDH_XKIc0GDWrC6s1Wzz8lxnq3_hux_5b6NHPZ9=w960-fcrop64=1,32b75a57cd48a5a8-k-c0xffffffff-no-nd-rj",
|
||||
width: 960,
|
||||
height: 263,
|
||||
),
|
||||
Thumbnail(
|
||||
url: "https://yt3.ggpht.com/yIJ9ad80n49rK-YUcZLe_8bLmR-aGyg5ybDH_XKIc0GDWrC6s1Wzz8lxnq3_hux_5b6NHPZ9=w1280-fcrop64=1,32b75a57cd48a5a8-k-c0xffffffff-no-nd-rj",
|
||||
width: 1280,
|
||||
height: 351,
|
||||
),
|
||||
Thumbnail(
|
||||
url: "https://yt3.ggpht.com/yIJ9ad80n49rK-YUcZLe_8bLmR-aGyg5ybDH_XKIc0GDWrC6s1Wzz8lxnq3_hux_5b6NHPZ9=w1440-fcrop64=1,32b75a57cd48a5a8-k-c0xffffffff-no-nd-rj",
|
||||
width: 1440,
|
||||
height: 395,
|
||||
),
|
||||
],
|
||||
tv_banner: [
|
||||
Thumbnail(
|
||||
url: "https://yt3.ggpht.com/yIJ9ad80n49rK-YUcZLe_8bLmR-aGyg5ybDH_XKIc0GDWrC6s1Wzz8lxnq3_hux_5b6NHPZ9=w320-fcrop64=1,00000000ffffffff-k-c0xffffffff-no-nd-rj",
|
||||
width: 320,
|
||||
height: 180,
|
||||
),
|
||||
Thumbnail(
|
||||
url: "https://yt3.ggpht.com/yIJ9ad80n49rK-YUcZLe_8bLmR-aGyg5ybDH_XKIc0GDWrC6s1Wzz8lxnq3_hux_5b6NHPZ9=w854-fcrop64=1,00000000ffffffff-k-c0xffffffff-no-nd-rj",
|
||||
width: 854,
|
||||
height: 480,
|
||||
),
|
||||
Thumbnail(
|
||||
url: "https://yt3.ggpht.com/yIJ9ad80n49rK-YUcZLe_8bLmR-aGyg5ybDH_XKIc0GDWrC6s1Wzz8lxnq3_hux_5b6NHPZ9=w1280-fcrop64=1,00000000ffffffff-k-c0xffffffff-no-nd-rj",
|
||||
width: 1280,
|
||||
height: 720,
|
||||
),
|
||||
Thumbnail(
|
||||
url: "https://yt3.ggpht.com/yIJ9ad80n49rK-YUcZLe_8bLmR-aGyg5ybDH_XKIc0GDWrC6s1Wzz8lxnq3_hux_5b6NHPZ9=w1920-fcrop64=1,00000000ffffffff-k-c0xffffffff-no-nd-rj",
|
||||
width: 1920,
|
||||
height: 1080,
|
||||
),
|
||||
Thumbnail(
|
||||
url: "https://yt3.ggpht.com/yIJ9ad80n49rK-YUcZLe_8bLmR-aGyg5ybDH_XKIc0GDWrC6s1Wzz8lxnq3_hux_5b6NHPZ9=w2120-fcrop64=1,00000000ffffffff-k-c0xffffffff-no-nd-rj",
|
||||
width: 2120,
|
||||
height: 1192,
|
||||
),
|
||||
],
|
||||
has_shorts: false,
|
||||
has_live: true,
|
||||
visitor_data: Some("Cgs4ZFVmMzVlU1dxbyiBqpeaBg%3D%3D"),
|
||||
|
|
|
@ -5,7 +5,9 @@ expression: map_res.c
|
|||
Channel(
|
||||
id: "UCHF66aWLOxBW4l6VkSrS3cQ",
|
||||
name: "Coachella",
|
||||
handle: Some("@Coachella"),
|
||||
subscriber_count: Some(2710000),
|
||||
video_count: None,
|
||||
avatar: [
|
||||
Thumbnail(
|
||||
url: "//yt3.googleusercontent.com/RDZ6VWFjHEMFm_QcmCCf-yG_UiGo9YWXEmVRuiHSC8SvP02dgeBEtAjd4CnEKGLo0V2gGdIRDQ=s88-c-k-c0x00ffffff-no-rj-mo",
|
||||
|
@ -31,10 +33,7 @@ Channel(
|
|||
"indio",
|
||||
"california",
|
||||
],
|
||||
vanity_url: Some("https://www.youtube.com/@Coachella"),
|
||||
banner: [],
|
||||
mobile_banner: [],
|
||||
tv_banner: [],
|
||||
has_shorts: true,
|
||||
has_live: true,
|
||||
visitor_data: Some("CgtjSUhDeVJ6SU5wNCj75uyhBg%3D%3D"),
|
||||
|
|
|
@ -5,7 +5,9 @@ expression: map_res.c
|
|||
Channel(
|
||||
id: "UC2DjFE7Xf11URZqWBigcVOQ",
|
||||
name: "EEVblog",
|
||||
handle: Some("@EEVblog"),
|
||||
subscriber_count: Some(933000),
|
||||
video_count: Some(19),
|
||||
avatar: [
|
||||
Thumbnail(
|
||||
url: "https://yt3.googleusercontent.com/ytc/AIdro_lagjGDfXbXlQXhznx3CDRitOBdxvebllQd_YP1ag=s72-c-k-c0x00ffffff-no-rj",
|
||||
|
@ -55,7 +57,6 @@ Channel(
|
|||
"dumpster diving",
|
||||
"debunking",
|
||||
],
|
||||
vanity_url: Some("https://www.youtube.com/@EEVblog"),
|
||||
banner: [
|
||||
Thumbnail(
|
||||
url: "https://yt3.googleusercontent.com/yIJ9ad80n49rK-YUcZLe_8bLmR-aGyg5ybDH_XKIc0GDWrC6s1Wzz8lxnq3_hux_5b6NHPZ9=w1060-fcrop64=1,00005a57ffffa5a8-k-c0xffffffff-no-nd-rj",
|
||||
|
@ -88,8 +89,6 @@ Channel(
|
|||
height: 424,
|
||||
),
|
||||
],
|
||||
mobile_banner: [],
|
||||
tv_banner: [],
|
||||
has_shorts: true,
|
||||
has_live: true,
|
||||
visitor_data: None,
|
||||
|
|
|
@ -5,7 +5,9 @@ expression: map_res.c
|
|||
Channel(
|
||||
id: "UC2DjFE7Xf11URZqWBigcVOQ",
|
||||
name: "EEVblog",
|
||||
handle: None,
|
||||
subscriber_count: Some(880000),
|
||||
video_count: None,
|
||||
avatar: [
|
||||
Thumbnail(
|
||||
url: "https://yt3.ggpht.com/ytc/AMLnZu9eKk4Nd16fX4Rn1TF1G7ReluwOl6M5558FTYAM=s48-c-k-c0x00ffffff-no-rj",
|
||||
|
@ -55,7 +57,6 @@ Channel(
|
|||
"dumpster diving",
|
||||
"debunking",
|
||||
],
|
||||
vanity_url: Some("https://www.youtube.com/c/EevblogDave"),
|
||||
banner: [
|
||||
Thumbnail(
|
||||
url: "https://yt3.ggpht.com/yIJ9ad80n49rK-YUcZLe_8bLmR-aGyg5ybDH_XKIc0GDWrC6s1Wzz8lxnq3_hux_5b6NHPZ9=w1060-fcrop64=1,00005a57ffffa5a8-k-c0xffffffff-no-nd-rj",
|
||||
|
@ -88,60 +89,6 @@ Channel(
|
|||
height: 424,
|
||||
),
|
||||
],
|
||||
mobile_banner: [
|
||||
Thumbnail(
|
||||
url: "https://yt3.ggpht.com/yIJ9ad80n49rK-YUcZLe_8bLmR-aGyg5ybDH_XKIc0GDWrC6s1Wzz8lxnq3_hux_5b6NHPZ9=w320-fcrop64=1,32b75a57cd48a5a8-k-c0xffffffff-no-nd-rj",
|
||||
width: 320,
|
||||
height: 88,
|
||||
),
|
||||
Thumbnail(
|
||||
url: "https://yt3.ggpht.com/yIJ9ad80n49rK-YUcZLe_8bLmR-aGyg5ybDH_XKIc0GDWrC6s1Wzz8lxnq3_hux_5b6NHPZ9=w640-fcrop64=1,32b75a57cd48a5a8-k-c0xffffffff-no-nd-rj",
|
||||
width: 640,
|
||||
height: 175,
|
||||
),
|
||||
Thumbnail(
|
||||
url: "https://yt3.ggpht.com/yIJ9ad80n49rK-YUcZLe_8bLmR-aGyg5ybDH_XKIc0GDWrC6s1Wzz8lxnq3_hux_5b6NHPZ9=w960-fcrop64=1,32b75a57cd48a5a8-k-c0xffffffff-no-nd-rj",
|
||||
width: 960,
|
||||
height: 263,
|
||||
),
|
||||
Thumbnail(
|
||||
url: "https://yt3.ggpht.com/yIJ9ad80n49rK-YUcZLe_8bLmR-aGyg5ybDH_XKIc0GDWrC6s1Wzz8lxnq3_hux_5b6NHPZ9=w1280-fcrop64=1,32b75a57cd48a5a8-k-c0xffffffff-no-nd-rj",
|
||||
width: 1280,
|
||||
height: 351,
|
||||
),
|
||||
Thumbnail(
|
||||
url: "https://yt3.ggpht.com/yIJ9ad80n49rK-YUcZLe_8bLmR-aGyg5ybDH_XKIc0GDWrC6s1Wzz8lxnq3_hux_5b6NHPZ9=w1440-fcrop64=1,32b75a57cd48a5a8-k-c0xffffffff-no-nd-rj",
|
||||
width: 1440,
|
||||
height: 395,
|
||||
),
|
||||
],
|
||||
tv_banner: [
|
||||
Thumbnail(
|
||||
url: "https://yt3.ggpht.com/yIJ9ad80n49rK-YUcZLe_8bLmR-aGyg5ybDH_XKIc0GDWrC6s1Wzz8lxnq3_hux_5b6NHPZ9=w320-fcrop64=1,00000000ffffffff-k-c0xffffffff-no-nd-rj",
|
||||
width: 320,
|
||||
height: 180,
|
||||
),
|
||||
Thumbnail(
|
||||
url: "https://yt3.ggpht.com/yIJ9ad80n49rK-YUcZLe_8bLmR-aGyg5ybDH_XKIc0GDWrC6s1Wzz8lxnq3_hux_5b6NHPZ9=w854-fcrop64=1,00000000ffffffff-k-c0xffffffff-no-nd-rj",
|
||||
width: 854,
|
||||
height: 480,
|
||||
),
|
||||
Thumbnail(
|
||||
url: "https://yt3.ggpht.com/yIJ9ad80n49rK-YUcZLe_8bLmR-aGyg5ybDH_XKIc0GDWrC6s1Wzz8lxnq3_hux_5b6NHPZ9=w1280-fcrop64=1,00000000ffffffff-k-c0xffffffff-no-nd-rj",
|
||||
width: 1280,
|
||||
height: 720,
|
||||
),
|
||||
Thumbnail(
|
||||
url: "https://yt3.ggpht.com/yIJ9ad80n49rK-YUcZLe_8bLmR-aGyg5ybDH_XKIc0GDWrC6s1Wzz8lxnq3_hux_5b6NHPZ9=w1920-fcrop64=1,00000000ffffffff-k-c0xffffffff-no-nd-rj",
|
||||
width: 1920,
|
||||
height: 1080,
|
||||
),
|
||||
Thumbnail(
|
||||
url: "https://yt3.ggpht.com/yIJ9ad80n49rK-YUcZLe_8bLmR-aGyg5ybDH_XKIc0GDWrC6s1Wzz8lxnq3_hux_5b6NHPZ9=w2120-fcrop64=1,00000000ffffffff-k-c0xffffffff-no-nd-rj",
|
||||
width: 2120,
|
||||
height: 1192,
|
||||
),
|
||||
],
|
||||
has_shorts: false,
|
||||
has_live: false,
|
||||
visitor_data: Some("CgszNU5rbDVZS2hMcyim4K2ZBg%3D%3D"),
|
||||
|
|
|
@ -5,7 +5,9 @@ expression: map_res.c
|
|||
Channel(
|
||||
id: "UCxBa895m48H5idw5li7h-0g",
|
||||
name: "Sebastian Figurroa",
|
||||
handle: None,
|
||||
subscriber_count: None,
|
||||
video_count: None,
|
||||
avatar: [
|
||||
Thumbnail(
|
||||
url: "https://yt3.ggpht.com/ytc/AMLnZu_hsZ1XlUXHzXsGNHJw0np79WhWZcC4j8eFdy-tiUCDBKAjJyJOzE5kXFRiqL2S=s48-c-k-c0x00ffffff-no-rj",
|
||||
|
@ -26,10 +28,7 @@ Channel(
|
|||
verification: None,
|
||||
description: "",
|
||||
tags: [],
|
||||
vanity_url: None,
|
||||
banner: [],
|
||||
mobile_banner: [],
|
||||
tv_banner: [],
|
||||
has_shorts: false,
|
||||
has_live: false,
|
||||
visitor_data: Some("Cgtvc2s4UllvTGl6byigxseZBg%3D%3D"),
|
||||
|
|
|
@ -5,7 +5,9 @@ expression: map_res.c
|
|||
Channel(
|
||||
id: "UChs0pSaEoNLV4mevBFGaoKA",
|
||||
name: "The Good Life Radio x Sensual Musique",
|
||||
handle: None,
|
||||
subscriber_count: Some(760000),
|
||||
video_count: None,
|
||||
avatar: [
|
||||
Thumbnail(
|
||||
url: "https://yt3.ggpht.com/ytc/AMLnZu_V9mOdHaorjNFqGXCecFeOBZhDWB8tVYG_I8gJwA=s48-c-k-c0x00ffffff-no-rj",
|
||||
|
@ -39,7 +41,6 @@ Channel(
|
|||
"tropical house",
|
||||
"house music",
|
||||
],
|
||||
vanity_url: Some("https://www.youtube.com/c/TheGoodLiferadio"),
|
||||
banner: [
|
||||
Thumbnail(
|
||||
url: "https://yt3.ggpht.com/fL4x31Q80O_BvnhVIMI9YlV3apsiFvBENwGiSA-Hw9An6djAGw92RSOFax6z2r_rJNbRWPMA=w1060-fcrop64=1,00005a57ffffa5a8-k-c0xffffffff-no-nd-rj",
|
||||
|
@ -72,60 +73,6 @@ Channel(
|
|||
height: 424,
|
||||
),
|
||||
],
|
||||
mobile_banner: [
|
||||
Thumbnail(
|
||||
url: "https://yt3.ggpht.com/fL4x31Q80O_BvnhVIMI9YlV3apsiFvBENwGiSA-Hw9An6djAGw92RSOFax6z2r_rJNbRWPMA=w320-fcrop64=1,32b75a57cd48a5a8-k-c0xffffffff-no-nd-rj",
|
||||
width: 320,
|
||||
height: 88,
|
||||
),
|
||||
Thumbnail(
|
||||
url: "https://yt3.ggpht.com/fL4x31Q80O_BvnhVIMI9YlV3apsiFvBENwGiSA-Hw9An6djAGw92RSOFax6z2r_rJNbRWPMA=w640-fcrop64=1,32b75a57cd48a5a8-k-c0xffffffff-no-nd-rj",
|
||||
width: 640,
|
||||
height: 175,
|
||||
),
|
||||
Thumbnail(
|
||||
url: "https://yt3.ggpht.com/fL4x31Q80O_BvnhVIMI9YlV3apsiFvBENwGiSA-Hw9An6djAGw92RSOFax6z2r_rJNbRWPMA=w960-fcrop64=1,32b75a57cd48a5a8-k-c0xffffffff-no-nd-rj",
|
||||
width: 960,
|
||||
height: 263,
|
||||
),
|
||||
Thumbnail(
|
||||
url: "https://yt3.ggpht.com/fL4x31Q80O_BvnhVIMI9YlV3apsiFvBENwGiSA-Hw9An6djAGw92RSOFax6z2r_rJNbRWPMA=w1280-fcrop64=1,32b75a57cd48a5a8-k-c0xffffffff-no-nd-rj",
|
||||
width: 1280,
|
||||
height: 351,
|
||||
),
|
||||
Thumbnail(
|
||||
url: "https://yt3.ggpht.com/fL4x31Q80O_BvnhVIMI9YlV3apsiFvBENwGiSA-Hw9An6djAGw92RSOFax6z2r_rJNbRWPMA=w1440-fcrop64=1,32b75a57cd48a5a8-k-c0xffffffff-no-nd-rj",
|
||||
width: 1440,
|
||||
height: 395,
|
||||
),
|
||||
],
|
||||
tv_banner: [
|
||||
Thumbnail(
|
||||
url: "https://yt3.ggpht.com/fL4x31Q80O_BvnhVIMI9YlV3apsiFvBENwGiSA-Hw9An6djAGw92RSOFax6z2r_rJNbRWPMA=w320-fcrop64=1,00000000ffffffff-k-c0xffffffff-no-nd-rj",
|
||||
width: 320,
|
||||
height: 180,
|
||||
),
|
||||
Thumbnail(
|
||||
url: "https://yt3.ggpht.com/fL4x31Q80O_BvnhVIMI9YlV3apsiFvBENwGiSA-Hw9An6djAGw92RSOFax6z2r_rJNbRWPMA=w854-fcrop64=1,00000000ffffffff-k-c0xffffffff-no-nd-rj",
|
||||
width: 854,
|
||||
height: 480,
|
||||
),
|
||||
Thumbnail(
|
||||
url: "https://yt3.ggpht.com/fL4x31Q80O_BvnhVIMI9YlV3apsiFvBENwGiSA-Hw9An6djAGw92RSOFax6z2r_rJNbRWPMA=w1280-fcrop64=1,00000000ffffffff-k-c0xffffffff-no-nd-rj",
|
||||
width: 1280,
|
||||
height: 720,
|
||||
),
|
||||
Thumbnail(
|
||||
url: "https://yt3.ggpht.com/fL4x31Q80O_BvnhVIMI9YlV3apsiFvBENwGiSA-Hw9An6djAGw92RSOFax6z2r_rJNbRWPMA=w1920-fcrop64=1,00000000ffffffff-k-c0xffffffff-no-nd-rj",
|
||||
width: 1920,
|
||||
height: 1080,
|
||||
),
|
||||
Thumbnail(
|
||||
url: "https://yt3.ggpht.com/fL4x31Q80O_BvnhVIMI9YlV3apsiFvBENwGiSA-Hw9An6djAGw92RSOFax6z2r_rJNbRWPMA=w2120-fcrop64=1,00000000ffffffff-k-c0xffffffff-no-nd-rj",
|
||||
width: 2120,
|
||||
height: 1192,
|
||||
),
|
||||
],
|
||||
has_shorts: false,
|
||||
has_live: false,
|
||||
visitor_data: Some("CgtkYXJITElwYmd4OCj85a2ZBg%3D%3D"),
|
||||
|
|
|
@ -5,7 +5,9 @@ expression: map_res.c
|
|||
Channel(
|
||||
id: "UC_vmjW5e1xEHhYjY2a0kK1A",
|
||||
name: "Oonagh - Topic",
|
||||
handle: None,
|
||||
subscriber_count: None,
|
||||
video_count: None,
|
||||
avatar: [
|
||||
Thumbnail(
|
||||
url: "https://yt3.ggpht.com/pqKv4iqSjmMKPxsMCeyklTbpROSyInGNR4XvD1DqKD0AlROlsHzvoAlTvtMTO1g1x2WxaQ2Enxw=s48-c-k-c0x00ffffff-no-rj",
|
||||
|
@ -26,7 +28,6 @@ Channel(
|
|||
verification: None,
|
||||
description: "",
|
||||
tags: [],
|
||||
vanity_url: None,
|
||||
banner: [
|
||||
Thumbnail(
|
||||
url: "https://yt3.ggpht.com/EDatBjgcL94-qSfQa5Twr8l88hYcAXQJksDrwARWbotrWzJhG03gRyZLKV1mk1a1tMI_LSg4qg=w1060-fcrop64=1,00005a57ffffa5a8-k-c0xffffffff-no-nd-rj",
|
||||
|
@ -59,60 +60,6 @@ Channel(
|
|||
height: 424,
|
||||
),
|
||||
],
|
||||
mobile_banner: [
|
||||
Thumbnail(
|
||||
url: "https://yt3.ggpht.com/EDatBjgcL94-qSfQa5Twr8l88hYcAXQJksDrwARWbotrWzJhG03gRyZLKV1mk1a1tMI_LSg4qg=w320-fcrop64=1,32b75a57cd48a5a8-k-c0xffffffff-no-nd-rj",
|
||||
width: 320,
|
||||
height: 88,
|
||||
),
|
||||
Thumbnail(
|
||||
url: "https://yt3.ggpht.com/EDatBjgcL94-qSfQa5Twr8l88hYcAXQJksDrwARWbotrWzJhG03gRyZLKV1mk1a1tMI_LSg4qg=w640-fcrop64=1,32b75a57cd48a5a8-k-c0xffffffff-no-nd-rj",
|
||||
width: 640,
|
||||
height: 175,
|
||||
),
|
||||
Thumbnail(
|
||||
url: "https://yt3.ggpht.com/EDatBjgcL94-qSfQa5Twr8l88hYcAXQJksDrwARWbotrWzJhG03gRyZLKV1mk1a1tMI_LSg4qg=w960-fcrop64=1,32b75a57cd48a5a8-k-c0xffffffff-no-nd-rj",
|
||||
width: 960,
|
||||
height: 263,
|
||||
),
|
||||
Thumbnail(
|
||||
url: "https://yt3.ggpht.com/EDatBjgcL94-qSfQa5Twr8l88hYcAXQJksDrwARWbotrWzJhG03gRyZLKV1mk1a1tMI_LSg4qg=w1280-fcrop64=1,32b75a57cd48a5a8-k-c0xffffffff-no-nd-rj",
|
||||
width: 1280,
|
||||
height: 351,
|
||||
),
|
||||
Thumbnail(
|
||||
url: "https://yt3.ggpht.com/EDatBjgcL94-qSfQa5Twr8l88hYcAXQJksDrwARWbotrWzJhG03gRyZLKV1mk1a1tMI_LSg4qg=w1440-fcrop64=1,32b75a57cd48a5a8-k-c0xffffffff-no-nd-rj",
|
||||
width: 1440,
|
||||
height: 395,
|
||||
),
|
||||
],
|
||||
tv_banner: [
|
||||
Thumbnail(
|
||||
url: "https://yt3.ggpht.com/EDatBjgcL94-qSfQa5Twr8l88hYcAXQJksDrwARWbotrWzJhG03gRyZLKV1mk1a1tMI_LSg4qg=w320-fcrop64=1,00000000ffffffff-k-c0xffffffff-no-nd-rj",
|
||||
width: 320,
|
||||
height: 180,
|
||||
),
|
||||
Thumbnail(
|
||||
url: "https://yt3.ggpht.com/EDatBjgcL94-qSfQa5Twr8l88hYcAXQJksDrwARWbotrWzJhG03gRyZLKV1mk1a1tMI_LSg4qg=w854-fcrop64=1,00000000ffffffff-k-c0xffffffff-no-nd-rj",
|
||||
width: 854,
|
||||
height: 480,
|
||||
),
|
||||
Thumbnail(
|
||||
url: "https://yt3.ggpht.com/EDatBjgcL94-qSfQa5Twr8l88hYcAXQJksDrwARWbotrWzJhG03gRyZLKV1mk1a1tMI_LSg4qg=w1280-fcrop64=1,00000000ffffffff-k-c0xffffffff-no-nd-rj",
|
||||
width: 1280,
|
||||
height: 720,
|
||||
),
|
||||
Thumbnail(
|
||||
url: "https://yt3.ggpht.com/EDatBjgcL94-qSfQa5Twr8l88hYcAXQJksDrwARWbotrWzJhG03gRyZLKV1mk1a1tMI_LSg4qg=w1920-fcrop64=1,00000000ffffffff-k-c0xffffffff-no-nd-rj",
|
||||
width: 1920,
|
||||
height: 1080,
|
||||
),
|
||||
Thumbnail(
|
||||
url: "https://yt3.ggpht.com/EDatBjgcL94-qSfQa5Twr8l88hYcAXQJksDrwARWbotrWzJhG03gRyZLKV1mk1a1tMI_LSg4qg=w2120-fcrop64=1,00000000ffffffff-k-c0xffffffff-no-nd-rj",
|
||||
width: 2120,
|
||||
height: 1192,
|
||||
),
|
||||
],
|
||||
has_shorts: false,
|
||||
has_live: false,
|
||||
visitor_data: Some("CgtCV1l2R2Rzb2ZSZyiu4a2ZBg%3D%3D"),
|
||||
|
|
|
@ -5,7 +5,9 @@ expression: map_res.c
|
|||
Channel(
|
||||
id: "UCh8gHdtzO2tXd593_bjErWg",
|
||||
name: "Doobydobap",
|
||||
handle: None,
|
||||
subscriber_count: Some(2840000),
|
||||
video_count: None,
|
||||
avatar: [
|
||||
Thumbnail(
|
||||
url: "https://yt3.ggpht.com/dm5Aq93xvVJz0NoVO88ieBkDXmuShCujGPlZ7qETMEPTrXvPUCFI3-BB6Xs_P-r6Uk3mnBy9zA=s48-c-k-c0x00ffffff-no-rj",
|
||||
|
@ -26,7 +28,6 @@ Channel(
|
|||
verification: Verified,
|
||||
description: "Hi, I’m Tina, aka Doobydobap!\n\nFood is the medium I use to tell stories and connect with people who share the same passion as I do. Whether it’s because you’re hungry at midnight or trying to learn how to cook, I hope you enjoy watching my content and recipes. Don\'t yuck my yum!\n\nwww.doobydobap.com\n",
|
||||
tags: [],
|
||||
vanity_url: Some("https://www.youtube.com/c/Doobydobap"),
|
||||
banner: [
|
||||
Thumbnail(
|
||||
url: "https://yt3.ggpht.com/BvnAqgiursrXpmS9AgDLtkOSTQfOG_Dqn0KzY5hcwO9XrHTEQTVgaflI913f9KRp7d0U2qBp=w1060-fcrop64=1,00005a57ffffa5a8-k-c0xffffffff-no-nd-rj",
|
||||
|
@ -59,60 +60,6 @@ Channel(
|
|||
height: 424,
|
||||
),
|
||||
],
|
||||
mobile_banner: [
|
||||
Thumbnail(
|
||||
url: "https://yt3.ggpht.com/BvnAqgiursrXpmS9AgDLtkOSTQfOG_Dqn0KzY5hcwO9XrHTEQTVgaflI913f9KRp7d0U2qBp=w320-fcrop64=1,32b75a57cd48a5a8-k-c0xffffffff-no-nd-rj",
|
||||
width: 320,
|
||||
height: 88,
|
||||
),
|
||||
Thumbnail(
|
||||
url: "https://yt3.ggpht.com/BvnAqgiursrXpmS9AgDLtkOSTQfOG_Dqn0KzY5hcwO9XrHTEQTVgaflI913f9KRp7d0U2qBp=w640-fcrop64=1,32b75a57cd48a5a8-k-c0xffffffff-no-nd-rj",
|
||||
width: 640,
|
||||
height: 175,
|
||||
),
|
||||
Thumbnail(
|
||||
url: "https://yt3.ggpht.com/BvnAqgiursrXpmS9AgDLtkOSTQfOG_Dqn0KzY5hcwO9XrHTEQTVgaflI913f9KRp7d0U2qBp=w960-fcrop64=1,32b75a57cd48a5a8-k-c0xffffffff-no-nd-rj",
|
||||
width: 960,
|
||||
height: 263,
|
||||
),
|
||||
Thumbnail(
|
||||
url: "https://yt3.ggpht.com/BvnAqgiursrXpmS9AgDLtkOSTQfOG_Dqn0KzY5hcwO9XrHTEQTVgaflI913f9KRp7d0U2qBp=w1280-fcrop64=1,32b75a57cd48a5a8-k-c0xffffffff-no-nd-rj",
|
||||
width: 1280,
|
||||
height: 351,
|
||||
),
|
||||
Thumbnail(
|
||||
url: "https://yt3.ggpht.com/BvnAqgiursrXpmS9AgDLtkOSTQfOG_Dqn0KzY5hcwO9XrHTEQTVgaflI913f9KRp7d0U2qBp=w1440-fcrop64=1,32b75a57cd48a5a8-k-c0xffffffff-no-nd-rj",
|
||||
width: 1440,
|
||||
height: 395,
|
||||
),
|
||||
],
|
||||
tv_banner: [
|
||||
Thumbnail(
|
||||
url: "https://yt3.ggpht.com/BvnAqgiursrXpmS9AgDLtkOSTQfOG_Dqn0KzY5hcwO9XrHTEQTVgaflI913f9KRp7d0U2qBp=w320-fcrop64=1,00000000ffffffff-k-c0xffffffff-no-nd-rj",
|
||||
width: 320,
|
||||
height: 180,
|
||||
),
|
||||
Thumbnail(
|
||||
url: "https://yt3.ggpht.com/BvnAqgiursrXpmS9AgDLtkOSTQfOG_Dqn0KzY5hcwO9XrHTEQTVgaflI913f9KRp7d0U2qBp=w854-fcrop64=1,00000000ffffffff-k-c0xffffffff-no-nd-rj",
|
||||
width: 854,
|
||||
height: 480,
|
||||
),
|
||||
Thumbnail(
|
||||
url: "https://yt3.ggpht.com/BvnAqgiursrXpmS9AgDLtkOSTQfOG_Dqn0KzY5hcwO9XrHTEQTVgaflI913f9KRp7d0U2qBp=w1280-fcrop64=1,00000000ffffffff-k-c0xffffffff-no-nd-rj",
|
||||
width: 1280,
|
||||
height: 720,
|
||||
),
|
||||
Thumbnail(
|
||||
url: "https://yt3.ggpht.com/BvnAqgiursrXpmS9AgDLtkOSTQfOG_Dqn0KzY5hcwO9XrHTEQTVgaflI913f9KRp7d0U2qBp=w1920-fcrop64=1,00000000ffffffff-k-c0xffffffff-no-nd-rj",
|
||||
width: 1920,
|
||||
height: 1080,
|
||||
),
|
||||
Thumbnail(
|
||||
url: "https://yt3.ggpht.com/BvnAqgiursrXpmS9AgDLtkOSTQfOG_Dqn0KzY5hcwO9XrHTEQTVgaflI913f9KRp7d0U2qBp=w2120-fcrop64=1,00000000ffffffff-k-c0xffffffff-no-nd-rj",
|
||||
width: 2120,
|
||||
height: 1192,
|
||||
),
|
||||
],
|
||||
has_shorts: false,
|
||||
has_live: false,
|
||||
visitor_data: Some("CgtneXVRbGtSMWtlYyj75a2ZBg%3D%3D"),
|
||||
|
|
|
@ -5,7 +5,9 @@ expression: map_res.c
|
|||
Channel(
|
||||
id: "UCcvfHa-GHSOHFAjU0-Ie57A",
|
||||
name: "Adam Something",
|
||||
handle: None,
|
||||
subscriber_count: Some(947000),
|
||||
video_count: None,
|
||||
avatar: [
|
||||
Thumbnail(
|
||||
url: "https://yt3.ggpht.com/FzV47fzr2nc8_KOeUO2FSIH-daaxCZaPDGqrgC1_Qp0_zEn0DnKmi7PiMwcssTG4IEDL1XfdTIk=s48-c-k-c0x00ffffff-no-rj",
|
||||
|
@ -43,7 +45,6 @@ Channel(
|
|||
"budapest",
|
||||
"eu",
|
||||
],
|
||||
vanity_url: Some("https://www.youtube.com/c/AdamSomething"),
|
||||
banner: [
|
||||
Thumbnail(
|
||||
url: "https://yt3.ggpht.com/Bk54VHh5FsxlwAAEltJp6rgx3VzBgxbi8naNngh5C4zQni1ijUhgTmVmDrE_I9M95SxtXTnd=w1060-fcrop64=1,00005a57ffffa5a8-k-c0xffffffff-no-nd-rj",
|
||||
|
@ -76,60 +77,6 @@ Channel(
|
|||
height: 424,
|
||||
),
|
||||
],
|
||||
mobile_banner: [
|
||||
Thumbnail(
|
||||
url: "https://yt3.ggpht.com/Bk54VHh5FsxlwAAEltJp6rgx3VzBgxbi8naNngh5C4zQni1ijUhgTmVmDrE_I9M95SxtXTnd=w320-fcrop64=1,32b75a57cd48a5a8-k-c0xffffffff-no-nd-rj",
|
||||
width: 320,
|
||||
height: 88,
|
||||
),
|
||||
Thumbnail(
|
||||
url: "https://yt3.ggpht.com/Bk54VHh5FsxlwAAEltJp6rgx3VzBgxbi8naNngh5C4zQni1ijUhgTmVmDrE_I9M95SxtXTnd=w640-fcrop64=1,32b75a57cd48a5a8-k-c0xffffffff-no-nd-rj",
|
||||
width: 640,
|
||||
height: 175,
|
||||
),
|
||||
Thumbnail(
|
||||
url: "https://yt3.ggpht.com/Bk54VHh5FsxlwAAEltJp6rgx3VzBgxbi8naNngh5C4zQni1ijUhgTmVmDrE_I9M95SxtXTnd=w960-fcrop64=1,32b75a57cd48a5a8-k-c0xffffffff-no-nd-rj",
|
||||
width: 960,
|
||||
height: 263,
|
||||
),
|
||||
Thumbnail(
|
||||
url: "https://yt3.ggpht.com/Bk54VHh5FsxlwAAEltJp6rgx3VzBgxbi8naNngh5C4zQni1ijUhgTmVmDrE_I9M95SxtXTnd=w1280-fcrop64=1,32b75a57cd48a5a8-k-c0xffffffff-no-nd-rj",
|
||||
width: 1280,
|
||||
height: 351,
|
||||
),
|
||||
Thumbnail(
|
||||
url: "https://yt3.ggpht.com/Bk54VHh5FsxlwAAEltJp6rgx3VzBgxbi8naNngh5C4zQni1ijUhgTmVmDrE_I9M95SxtXTnd=w1440-fcrop64=1,32b75a57cd48a5a8-k-c0xffffffff-no-nd-rj",
|
||||
width: 1440,
|
||||
height: 395,
|
||||
),
|
||||
],
|
||||
tv_banner: [
|
||||
Thumbnail(
|
||||
url: "https://yt3.ggpht.com/Bk54VHh5FsxlwAAEltJp6rgx3VzBgxbi8naNngh5C4zQni1ijUhgTmVmDrE_I9M95SxtXTnd=w320-fcrop64=1,00000000ffffffff-k-c0xffffffff-no-nd-rj",
|
||||
width: 320,
|
||||
height: 180,
|
||||
),
|
||||
Thumbnail(
|
||||
url: "https://yt3.ggpht.com/Bk54VHh5FsxlwAAEltJp6rgx3VzBgxbi8naNngh5C4zQni1ijUhgTmVmDrE_I9M95SxtXTnd=w854-fcrop64=1,00000000ffffffff-k-c0xffffffff-no-nd-rj",
|
||||
width: 854,
|
||||
height: 480,
|
||||
),
|
||||
Thumbnail(
|
||||
url: "https://yt3.ggpht.com/Bk54VHh5FsxlwAAEltJp6rgx3VzBgxbi8naNngh5C4zQni1ijUhgTmVmDrE_I9M95SxtXTnd=w1280-fcrop64=1,00000000ffffffff-k-c0xffffffff-no-nd-rj",
|
||||
width: 1280,
|
||||
height: 720,
|
||||
),
|
||||
Thumbnail(
|
||||
url: "https://yt3.ggpht.com/Bk54VHh5FsxlwAAEltJp6rgx3VzBgxbi8naNngh5C4zQni1ijUhgTmVmDrE_I9M95SxtXTnd=w1920-fcrop64=1,00000000ffffffff-k-c0xffffffff-no-nd-rj",
|
||||
width: 1920,
|
||||
height: 1080,
|
||||
),
|
||||
Thumbnail(
|
||||
url: "https://yt3.ggpht.com/Bk54VHh5FsxlwAAEltJp6rgx3VzBgxbi8naNngh5C4zQni1ijUhgTmVmDrE_I9M95SxtXTnd=w2120-fcrop64=1,00000000ffffffff-k-c0xffffffff-no-nd-rj",
|
||||
width: 2120,
|
||||
height: 1192,
|
||||
),
|
||||
],
|
||||
has_shorts: false,
|
||||
has_live: false,
|
||||
visitor_data: Some("Cgs4Ri1tLW1KNWozNCjGk8yZBg%3D%3D"),
|
||||
|
|
|
@ -1,884 +0,0 @@
|
|||
---
|
||||
source: src/client/pagination.rs
|
||||
expression: map_res.c
|
||||
---
|
||||
Paginator(
|
||||
count: None,
|
||||
items: [
|
||||
Video(VideoItem(
|
||||
id: "mRmlXh7Hams",
|
||||
name: "Extra 3 vom 12.10.2022 im NDR | extra 3 | NDR",
|
||||
duration: Some(1839),
|
||||
thumbnail: [
|
||||
Thumbnail(
|
||||
url: "https://i.ytimg.com/vi/mRmlXh7Hams/hqdefault.jpg?sqp=-oaymwEcCOADEI4CSFXyq4qpAw4IARUAAIhCGAFwAcABBg==&rs=AOn4CLAbO4lI0dDo_r85A1fi9XQS0rNiOQ",
|
||||
width: 480,
|
||||
height: 270,
|
||||
),
|
||||
],
|
||||
channel: Some(ChannelTag(
|
||||
id: "UCjhkuC_Pi85wGjnB0I1ydxw",
|
||||
name: "extra 3",
|
||||
avatar: [
|
||||
Thumbnail(
|
||||
url: "https://yt3.ggpht.com/N2TrlnZnU3cYFrRcXmQhQ77IriCxoEl-XTapCJQ9UkEHEkb0gMYVASjewV5Rg1P0HPUOebRoYw=s68-c-k-c0x00ffffff-no-rj",
|
||||
width: 68,
|
||||
height: 68,
|
||||
),
|
||||
],
|
||||
verification: None,
|
||||
subscriber_count: None,
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
publish_date_txt: Some("2 days ago"),
|
||||
view_count: Some(585257),
|
||||
is_live: false,
|
||||
is_short: false,
|
||||
is_upcoming: false,
|
||||
short_description: Some("Niedersachsen nach der Wahl: Schuld ist immer die Ampel | Die Grünen: Partei der erneuerbaren Prinzipien | Verhütung? Ist Frauensache! | Youtube: Handwerk mit goldenem Boden - Christian Ehring..."),
|
||||
)),
|
||||
Video(VideoItem(
|
||||
id: "LsXC5r64Pvc",
|
||||
name: "Most Rarest Plays In Baseball History",
|
||||
duration: Some(1975),
|
||||
thumbnail: [
|
||||
Thumbnail(
|
||||
url: "https://i.ytimg.com/vi/LsXC5r64Pvc/hqdefault.jpg?sqp=-oaymwEcCOADEI4CSFXyq4qpAw4IARUAAIhCGAFwAcABBg==&rs=AOn4CLB2KXmgKxrJVUy3Naqi_R-R2X92FA",
|
||||
width: 480,
|
||||
height: 270,
|
||||
),
|
||||
],
|
||||
channel: Some(ChannelTag(
|
||||
id: "UCRfKJZ7LHueFudiDgAJDr9Q",
|
||||
name: "Top All Sports",
|
||||
avatar: [
|
||||
Thumbnail(
|
||||
url: "https://yt3.ggpht.com/ytc/AMLnZu_dYWlP21FumM8m8ZxkKiTNaF9E68a2fnFnBo_q=s68-c-k-c0x00ffffff-no-rj",
|
||||
width: 68,
|
||||
height: 68,
|
||||
),
|
||||
],
|
||||
verification: None,
|
||||
subscriber_count: None,
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
publish_date_txt: Some("3 weeks ago"),
|
||||
view_count: Some(985521),
|
||||
is_live: false,
|
||||
is_short: false,
|
||||
is_upcoming: false,
|
||||
short_description: Some("#baseball #mlb #mlbb"),
|
||||
)),
|
||||
Video(VideoItem(
|
||||
id: "dwPmd1GqQHE",
|
||||
name: "90S RAP & HIPHOP MIX - Notorious B I G , Dr Dre, 50 Cent, Snoop Dogg, 2Pac, DMX, Lil Jon and more",
|
||||
duration: Some(5457),
|
||||
thumbnail: [
|
||||
Thumbnail(
|
||||
url: "https://i.ytimg.com/vi/dwPmd1GqQHE/hqdefault.jpg?sqp=-oaymwEcCOADEI4CSFXyq4qpAw4IARUAAIhCGAFwAcABBg==&rs=AOn4CLAAyGcLGzFkfdEmqqohpxZsGOM9Kw",
|
||||
width: 480,
|
||||
height: 270,
|
||||
),
|
||||
],
|
||||
channel: Some(ChannelTag(
|
||||
id: "UCKICAAGtBLJJ5zRdIxn_B4g",
|
||||
name: "#Hip Hop 2022",
|
||||
avatar: [
|
||||
Thumbnail(
|
||||
url: "https://yt3.ggpht.com/fD5u3Lvkxe7oD0J3VlZ_Ih9BWtxT10wc68XWzSbVt02L88J2QrqO4FaK2xrsOoejD1GpBE7VAaA=s68-c-k-c0x00ffffff-no-rj",
|
||||
width: 68,
|
||||
height: 68,
|
||||
),
|
||||
],
|
||||
verification: None,
|
||||
subscriber_count: None,
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
publish_date_txt: Some("5 months ago"),
|
||||
view_count: Some(1654055),
|
||||
is_live: false,
|
||||
is_short: false,
|
||||
is_upcoming: false,
|
||||
short_description: None,
|
||||
)),
|
||||
Video(VideoItem(
|
||||
id: "qxI-Ob8lpLE",
|
||||
name: "Schlatt\'s Chips Tier List",
|
||||
duration: Some(1071),
|
||||
thumbnail: [
|
||||
Thumbnail(
|
||||
url: "https://i.ytimg.com/vi/qxI-Ob8lpLE/hq720.jpg?sqp=-oaymwEcCOgCEMoBSFXyq4qpAw4IARUAAIhCGAFwAcABBg==&rs=AOn4CLBtEO5eB17tODb5Ek9GRoQwwVGtvA",
|
||||
width: 360,
|
||||
height: 202,
|
||||
),
|
||||
Thumbnail(
|
||||
url: "https://i.ytimg.com/vi/qxI-Ob8lpLE/hq720.jpg?sqp=-oaymwEcCNAFEJQDSFXyq4qpAw4IARUAAIhCGAFwAcABBg==&rs=AOn4CLAwDt0sa98qoI5O8u0kHJY7FbTrZg",
|
||||
width: 720,
|
||||
height: 404,
|
||||
),
|
||||
],
|
||||
channel: Some(ChannelTag(
|
||||
id: "UC2mP7il3YV7TxM_3m6U0bwA",
|
||||
name: "jschlattLIVE",
|
||||
avatar: [
|
||||
Thumbnail(
|
||||
url: "https://yt3.ggpht.com/Rr0aOvzRYLCyIDtIhIgkAYdQeagRlGDPzRuWoLrwGakM4VdnHPZHeSfUbiV-pJKmFbJ8LL9r5g=s68-c-k-c0x00ffffff-no-rj",
|
||||
width: 68,
|
||||
height: 68,
|
||||
),
|
||||
],
|
||||
verification: Verified,
|
||||
subscriber_count: None,
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
publish_date_txt: Some("1 year ago"),
|
||||
view_count: Some(9029628),
|
||||
is_live: false,
|
||||
is_short: false,
|
||||
is_upcoming: false,
|
||||
short_description: Some("Schlatt ranks every chip ever made.\nCREATE YOUR OWN TIER LIST: https://tiermaker.com/create/chips-for-big-guy-1146620\n\nSubscribe to me on Twitch:\nhttps://twitch.tv/jschlatt\n\nFollow me on Twitter:..."),
|
||||
)),
|
||||
Video(VideoItem(
|
||||
id: "qmrzTUmZ4UU",
|
||||
name: "850€ für den Verrat am System - UCS AT-AT LEGO® Star Wars 75313",
|
||||
duration: Some(2043),
|
||||
thumbnail: [
|
||||
Thumbnail(
|
||||
url: "https://i.ytimg.com/vi/qmrzTUmZ4UU/hq720.jpg?sqp=-oaymwEcCOgCEMoBSFXyq4qpAw4IARUAAIhCGAFwAcABBg==&rs=AOn4CLAsI3VS-wxnt1s_zS4M_YbVrV1pAg",
|
||||
width: 360,
|
||||
height: 202,
|
||||
),
|
||||
Thumbnail(
|
||||
url: "https://i.ytimg.com/vi/qmrzTUmZ4UU/hq720.jpg?sqp=-oaymwEcCNAFEJQDSFXyq4qpAw4IARUAAIhCGAFwAcABBg==&rs=AOn4CLBYk7w0qGeW4kZchFr-tbydELUChQ",
|
||||
width: 720,
|
||||
height: 404,
|
||||
),
|
||||
],
|
||||
channel: Some(ChannelTag(
|
||||
id: "UC_EZd3lsmxudu3IQzpTzOgw",
|
||||
name: "Held der Steine Inh. Thomas Panke",
|
||||
avatar: [
|
||||
Thumbnail(
|
||||
url: "https://yt3.ggpht.com/ytc/AMLnZu8g9hFxZ2HD4P9pDsUxoAvkHwbZoTVNr3yw12i8YA=s68-c-k-c0x00ffffff-no-rj",
|
||||
width: 68,
|
||||
height: 68,
|
||||
),
|
||||
],
|
||||
verification: Verified,
|
||||
subscriber_count: None,
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
publish_date_txt: Some("6 days ago"),
|
||||
view_count: Some(600516),
|
||||
is_live: false,
|
||||
is_short: false,
|
||||
is_upcoming: false,
|
||||
short_description: Some("Star Wars - erschienen 2021 - 6749 Teile\n\nDieses Set bei Amazon*:\nhttps://amzn.to/3yu9dHX\n\nErwähnt im Video*:\nTassen https://bit.ly/HdSBausteinecke\nBig Boy https://bit.ly/BBLokBigBoy\nBurg..."),
|
||||
)),
|
||||
Video(VideoItem(
|
||||
id: "4q4vpQCIZ6w",
|
||||
name: "🌉 Manhattan Jazz 💖 l Relaxing Jazz Piano Music l Background Music",
|
||||
duration: Some(23229),
|
||||
thumbnail: [
|
||||
Thumbnail(
|
||||
url: "https://i.ytimg.com/vi/4q4vpQCIZ6w/hq720.jpg?sqp=-oaymwEcCOgCEMoBSFXyq4qpAw4IARUAAIhCGAFwAcABBg==&rs=AOn4CLD4DKjgt5VJBRX2pH_KzI4Ru9AMaQ",
|
||||
width: 360,
|
||||
height: 202,
|
||||
),
|
||||
Thumbnail(
|
||||
url: "https://i.ytimg.com/vi/4q4vpQCIZ6w/hq720.jpg?sqp=-oaymwEcCNAFEJQDSFXyq4qpAw4IARUAAIhCGAFwAcABBg==&rs=AOn4CLDMm9yeUF-9LH2rhU7jaQ6td05cMg",
|
||||
width: 720,
|
||||
height: 404,
|
||||
),
|
||||
],
|
||||
channel: Some(ChannelTag(
|
||||
id: "UCBnMxlW70f0SB4ZTJx124lw",
|
||||
name: "몽키비지엠 MONKEYBGM",
|
||||
avatar: [
|
||||
Thumbnail(
|
||||
url: "https://yt3.ggpht.com/x8_XLvrLdd-Cs6z7Cmob2eZmqvbzmYdOdf6b7jLMry1z1YhdExnuqEhwRrYveu4X2airLfbv=s68-c-k-c0x00ffffff-no-rj",
|
||||
width: 68,
|
||||
height: 68,
|
||||
),
|
||||
],
|
||||
verification: Verified,
|
||||
subscriber_count: None,
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
publish_date_txt: Some("6 months ago"),
|
||||
view_count: Some(2343407),
|
||||
is_live: false,
|
||||
is_short: false,
|
||||
is_upcoming: false,
|
||||
short_description: Some("- Please Subscribe!\n\n🔺Disney OST Collection part 1 \n ➡\u{fe0f} https://youtu.be/lrzKFu85nhE\n\n🔺Disney OST Collection part 2 \n ➡\u{fe0f} https://youtu.be/EtE09lowIbk\n\n🔺Studio Ghibli..."),
|
||||
)),
|
||||
Video(VideoItem(
|
||||
id: "Z_k31kqZxaE",
|
||||
name: "1 in 1,000,000 NBA Moments",
|
||||
duration: Some(567),
|
||||
thumbnail: [
|
||||
Thumbnail(
|
||||
url: "https://i.ytimg.com/vi/Z_k31kqZxaE/hq720.jpg?sqp=-oaymwEcCOgCEMoBSFXyq4qpAw4IARUAAIhCGAFwAcABBg==&rs=AOn4CLCovxnIKW7TCP3XBcG4x-Acw10OBA",
|
||||
width: 360,
|
||||
height: 202,
|
||||
),
|
||||
Thumbnail(
|
||||
url: "https://i.ytimg.com/vi/Z_k31kqZxaE/hq720.jpg?sqp=-oaymwEcCNAFEJQDSFXyq4qpAw4IARUAAIhCGAFwAcABBg==&rs=AOn4CLBa52Ie0cfnzg44jnkfTGzrCsVfOw",
|
||||
width: 720,
|
||||
height: 404,
|
||||
),
|
||||
],
|
||||
channel: Some(ChannelTag(
|
||||
id: "UCpyoYVlp67N16Lg1_N4VnVw",
|
||||
name: "dime",
|
||||
avatar: [
|
||||
Thumbnail(
|
||||
url: "https://yt3.ggpht.com/HwpHaCaatHTI3N1imp5ZszL8_raSsxBq60UHScSpXC6e6VySeOlZ8Y3msYgum4vzCH5jmCxLvEU=s68-c-k-c0x00ffffff-no-rj",
|
||||
width: 68,
|
||||
height: 68,
|
||||
),
|
||||
],
|
||||
verification: Verified,
|
||||
subscriber_count: None,
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
publish_date_txt: Some("1 month ago"),
|
||||
view_count: Some(4334298),
|
||||
is_live: false,
|
||||
is_short: false,
|
||||
is_upcoming: false,
|
||||
short_description: Some("• Instagram - https://instagram.com/dime_nba\n• TikTok - https://tiktok.com/@dime_nba\n\ndime is a Swedish brand, founded in 2022. We produce some of the most entertaining NBA content on YouTube..."),
|
||||
)),
|
||||
Video(VideoItem(
|
||||
id: "zE-a5eqvlv8",
|
||||
name: "Dua Lipa, Coldplay, Martin Garrix & Kygo, The Chainsmokers Style - Feeling Me",
|
||||
duration: None,
|
||||
thumbnail: [
|
||||
Thumbnail(
|
||||
url: "https://i.ytimg.com/vi/zE-a5eqvlv8/hq720_live.jpg?sqp=COz4qZoG-oaymwEcCOgCEMoBSFXyq4qpAw4IARUAAIhCGAFwAcABBg==&rs=AOn4CLAbIAO-SIuWTC9f2AKu6Yp9nB0BwQ",
|
||||
width: 360,
|
||||
height: 202,
|
||||
),
|
||||
Thumbnail(
|
||||
url: "https://i.ytimg.com/vi/zE-a5eqvlv8/hq720_live.jpg?sqp=COz4qZoG-oaymwEcCNAFEJQDSFXyq4qpAw4IARUAAIhCGAFwAcABBg==&rs=AOn4CLDHdbRp6yOt4qkQk31BoFv6keTBYQ",
|
||||
width: 720,
|
||||
height: 404,
|
||||
),
|
||||
],
|
||||
channel: Some(ChannelTag(
|
||||
id: "UCX-USfenzQlhrEJR1zD5IYw",
|
||||
name: "Deep Mood.",
|
||||
avatar: [
|
||||
Thumbnail(
|
||||
url: "https://yt3.ggpht.com/8WO05hff9bGjmlyPFo_PJRMIfHEoUvN_KbTcWRVX2yqeUO3fLgkz0K4MA6W95s3_NKdNUAwjow=s68-c-k-c0x00ffffff-no-rj",
|
||||
width: 68,
|
||||
height: 68,
|
||||
),
|
||||
],
|
||||
verification: None,
|
||||
subscriber_count: None,
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
publish_date_txt: None,
|
||||
view_count: Some(889),
|
||||
is_live: true,
|
||||
is_short: false,
|
||||
is_upcoming: false,
|
||||
short_description: Some("#Summermix #DeepHouse #DeepHouseSummerMix\nDua Lipa, Coldplay, Martin Garrix & Kygo, The Chainsmokers Style - Feeling Me\n\n🎵 All songs in this spotify playlist: https://spoti.fi/2TJ4Dyj\nSubmit..."),
|
||||
)),
|
||||
Video(VideoItem(
|
||||
id: "gNlOk0LXi5M",
|
||||
name: "Soll ich dir 1g GOLD schenken? oder JEMAND anderen DOPPELT?",
|
||||
duration: Some(704),
|
||||
thumbnail: [
|
||||
Thumbnail(
|
||||
url: "https://i.ytimg.com/vi/gNlOk0LXi5M/hq720.jpg?sqp=-oaymwEcCOgCEMoBSFXyq4qpAw4IARUAAIhCGAFwAcABBg==&rs=AOn4CLAy3JbiDcqUTwF6NS69UnX715q90w",
|
||||
width: 360,
|
||||
height: 202,
|
||||
),
|
||||
Thumbnail(
|
||||
url: "https://i.ytimg.com/vi/gNlOk0LXi5M/hq720.jpg?sqp=-oaymwEcCNAFEJQDSFXyq4qpAw4IARUAAIhCGAFwAcABBg==&rs=AOn4CLDICPl-Jsul5nnhrac2s01gueUCDA",
|
||||
width: 720,
|
||||
height: 404,
|
||||
),
|
||||
],
|
||||
channel: Some(ChannelTag(
|
||||
id: "UCqcWNPTUVATZt0Dlr2jV0Wg",
|
||||
name: "Mois",
|
||||
avatar: [
|
||||
Thumbnail(
|
||||
url: "https://yt3.ggpht.com/uHDIV2MwZnJRX8guX2KfFr4-gdxXK5x9nH0tz456hcBn0DH7LurNQbkAPjP5tSKg1Tqu07y9nKw=s68-c-k-c0x00ffffff-no-rj",
|
||||
width: 68,
|
||||
height: 68,
|
||||
),
|
||||
],
|
||||
verification: Verified,
|
||||
subscriber_count: None,
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
publish_date_txt: Some("8 days ago"),
|
||||
view_count: Some(463834),
|
||||
is_live: false,
|
||||
is_short: false,
|
||||
is_upcoming: false,
|
||||
short_description: Some("Je mehr Menschen mich abonnieren desto mehr Menschen werde ich glücklich machen \n\n24 std ab, viel Glück \n\nhttps://I-Clip.com/?sPartner=Mois"),
|
||||
)),
|
||||
Video(VideoItem(
|
||||
id: "dbMvZjs8Yc8",
|
||||
name: "Brad Pitt- Die Revanche eines Sexsymbols | Doku HD | ARTE",
|
||||
duration: Some(3137),
|
||||
thumbnail: [
|
||||
Thumbnail(
|
||||
url: "https://i.ytimg.com/vi/dbMvZjs8Yc8/hq720.jpg?sqp=-oaymwEcCOgCEMoBSFXyq4qpAw4IARUAAIhCGAFwAcABBg==&rs=AOn4CLB6HnYSCQFmEQ1V5qlFf5fblOpv-g",
|
||||
width: 360,
|
||||
height: 202,
|
||||
),
|
||||
Thumbnail(
|
||||
url: "https://i.ytimg.com/vi/dbMvZjs8Yc8/hq720.jpg?sqp=-oaymwEcCNAFEJQDSFXyq4qpAw4IARUAAIhCGAFwAcABBg==&rs=AOn4CLD-AoMr1H_6EvzuWvg2whMDmbtY4A",
|
||||
width: 720,
|
||||
height: 404,
|
||||
),
|
||||
],
|
||||
channel: Some(ChannelTag(
|
||||
id: "UCsygZtQQSplGF6JA3XWvsdg",
|
||||
name: "Irgendwas mit ARTE und Kultur",
|
||||
avatar: [
|
||||
Thumbnail(
|
||||
url: "https://yt3.ggpht.com/ytc/AMLnZu9_FXs7hsEndpcy9C4D_ZsM1xZzbLLThDQIL4-Dxg=s68-c-k-c0x00ffffff-no-rj",
|
||||
width: 68,
|
||||
height: 68,
|
||||
),
|
||||
],
|
||||
verification: None,
|
||||
subscriber_count: None,
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
publish_date_txt: Some("5 days ago"),
|
||||
view_count: Some(293878),
|
||||
is_live: false,
|
||||
is_short: false,
|
||||
is_upcoming: false,
|
||||
short_description: Some("Vom „People“-Magazin wurde er mehrfach zum „Sexiest Man Alive“ gekrönt. Aber sein Aussehen ist nicht alles: In 30 Jahren Karriere drehte Brad Pitt eine Vielzahl herausragender Filme...."),
|
||||
)),
|
||||
Video(VideoItem(
|
||||
id: "mFxi3lOAcFs",
|
||||
name: "Craziest Soviet Machines You Won\'t Believe Exist - Part 1",
|
||||
duration: Some(1569),
|
||||
thumbnail: [
|
||||
Thumbnail(
|
||||
url: "https://i.ytimg.com/vi/mFxi3lOAcFs/hq720.jpg?sqp=-oaymwEcCOgCEMoBSFXyq4qpAw4IARUAAIhCGAFwAcABBg==&rs=AOn4CLCgPz_lsa3ENFNi2sC_uraWrUIuBQ",
|
||||
width: 360,
|
||||
height: 202,
|
||||
),
|
||||
Thumbnail(
|
||||
url: "https://i.ytimg.com/vi/mFxi3lOAcFs/hq720.jpg?sqp=-oaymwEcCNAFEJQDSFXyq4qpAw4IARUAAIhCGAFwAcABBg==&rs=AOn4CLA2u97RbHNrNVp_Cb5m0DSvA0P02g",
|
||||
width: 720,
|
||||
height: 404,
|
||||
),
|
||||
],
|
||||
channel: Some(ChannelTag(
|
||||
id: "UCkQO3QsgTpNTsOw6ujimT5Q",
|
||||
name: "BE AMAZED",
|
||||
avatar: [
|
||||
Thumbnail(
|
||||
url: "https://yt3.ggpht.com/ytc/AMLnZu_vmgpzJxLlR_1RA68cz8iITuzYLFFbPBvg5ULJlQ=s68-c-k-c0x00ffffff-no-rj",
|
||||
width: 68,
|
||||
height: 68,
|
||||
),
|
||||
],
|
||||
verification: Verified,
|
||||
subscriber_count: None,
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
publish_date_txt: Some("1 year ago"),
|
||||
view_count: Some(14056843),
|
||||
is_live: false,
|
||||
is_short: false,
|
||||
is_upcoming: false,
|
||||
short_description: Some("Coming up are some crazy Soviet-era machines you won\'t believe exist!\nPart 2: https://youtu.be/MBZVOJrhuHY\nSuggest a topic here to be turned into a video: http://bit.ly/2kwqhuh\nSubscribe for..."),
|
||||
)),
|
||||
Video(VideoItem(
|
||||
id: "eu7ubm7g59E",
|
||||
name: "People Hated Me For Using This Slab",
|
||||
duration: Some(1264),
|
||||
thumbnail: [
|
||||
Thumbnail(
|
||||
url: "https://i.ytimg.com/vi/eu7ubm7g59E/hq720.jpg?sqp=-oaymwEcCOgCEMoBSFXyq4qpAw4IARUAAIhCGAFwAcABBg==&rs=AOn4CLCg_b-6U2Pux_tZqAY8jkIa1JoTew",
|
||||
width: 360,
|
||||
height: 202,
|
||||
),
|
||||
Thumbnail(
|
||||
url: "https://i.ytimg.com/vi/eu7ubm7g59E/hq720.jpg?sqp=-oaymwEcCNAFEJQDSFXyq4qpAw4IARUAAIhCGAFwAcABBg==&rs=AOn4CLA9WwjUr_EpS3PPYNG3e4N8EEr9oA",
|
||||
width: 720,
|
||||
height: 404,
|
||||
),
|
||||
],
|
||||
channel: Some(ChannelTag(
|
||||
id: "UC6I0KzAD7uFTL1qzxyunkvA",
|
||||
name: "Blacktail Studio",
|
||||
avatar: [
|
||||
Thumbnail(
|
||||
url: "https://yt3.ggpht.com/ytc/AMLnZu8jg6Uevc1qmfbksQ_xdJ0dF37PmZVFHkyNhouBTA=s68-c-k-c0x00ffffff-no-rj",
|
||||
width: 68,
|
||||
height: 68,
|
||||
),
|
||||
],
|
||||
verification: Verified,
|
||||
subscriber_count: None,
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
publish_date_txt: Some("3 months ago"),
|
||||
view_count: Some(2845035),
|
||||
is_live: false,
|
||||
is_short: false,
|
||||
is_upcoming: false,
|
||||
short_description: Some("Some people were furious I used this slab, and I actually understand why. \nBlacktail bow tie jig (limited first run): https://www.blacktailstudio.com/bowtie-jig\nBlacktail epoxy table workshop:..."),
|
||||
)),
|
||||
Video(VideoItem(
|
||||
id: "TRGHIN2PGIA",
|
||||
name: "Christian Bale Breaks Down His Most Iconic Characters | GQ",
|
||||
duration: Some(1381),
|
||||
thumbnail: [
|
||||
Thumbnail(
|
||||
url: "https://i.ytimg.com/vi/TRGHIN2PGIA/hq720.jpg?sqp=-oaymwEcCOgCEMoBSFXyq4qpAw4IARUAAIhCGAFwAcABBg==&rs=AOn4CLAMxhmIbADGzAlH1jNl6RN-ZU0eEQ",
|
||||
width: 360,
|
||||
height: 202,
|
||||
),
|
||||
Thumbnail(
|
||||
url: "https://i.ytimg.com/vi/TRGHIN2PGIA/hq720.jpg?sqp=-oaymwEcCNAFEJQDSFXyq4qpAw4IARUAAIhCGAFwAcABBg==&rs=AOn4CLDxo3aBHktmxUOEuSdXJVHmlcR4-Q",
|
||||
width: 720,
|
||||
height: 404,
|
||||
),
|
||||
],
|
||||
channel: Some(ChannelTag(
|
||||
id: "UCsEukrAd64fqA7FjwkmZ_Dw",
|
||||
name: "GQ",
|
||||
avatar: [
|
||||
Thumbnail(
|
||||
url: "https://yt3.ggpht.com/ytc/AMLnZu-gTmA2HcJO9Y5kYl4IUKG-jZ8QtojL8qaQiyW9kA=s68-c-k-c0x00ffffff-no-rj",
|
||||
width: 68,
|
||||
height: 68,
|
||||
),
|
||||
],
|
||||
verification: Verified,
|
||||
subscriber_count: None,
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
publish_date_txt: Some("9 days ago"),
|
||||
view_count: Some(8044465),
|
||||
is_live: false,
|
||||
is_short: false,
|
||||
is_upcoming: false,
|
||||
short_description: Some("Christian Bale breaks down a few of his most iconic characters from \'American Psycho,\' \'The Dark Knight\' Trilogy, \'The Fighter,\' \'The Machinist,\' \'The Big Short,\' \'Vice,\' \'Empire of the Sun,\'..."),
|
||||
)),
|
||||
Video(VideoItem(
|
||||
id: "w3tENzcssDU",
|
||||
name: "NFL Trick Plays But They Get Increasingly Higher IQ",
|
||||
duration: Some(599),
|
||||
thumbnail: [
|
||||
Thumbnail(
|
||||
url: "https://i.ytimg.com/vi/w3tENzcssDU/hq720.jpg?sqp=-oaymwEcCOgCEMoBSFXyq4qpAw4IARUAAIhCGAFwAcABBg==&rs=AOn4CLCZHp6o6cV9HNNJXPlI1FKi6S58qg",
|
||||
width: 360,
|
||||
height: 202,
|
||||
),
|
||||
Thumbnail(
|
||||
url: "https://i.ytimg.com/vi/w3tENzcssDU/hq720.jpg?sqp=-oaymwEcCNAFEJQDSFXyq4qpAw4IARUAAIhCGAFwAcABBg==&rs=AOn4CLBH4K8b0AfAgX0MvL4oHlbianG8xQ",
|
||||
width: 720,
|
||||
height: 404,
|
||||
),
|
||||
],
|
||||
channel: Some(ChannelTag(
|
||||
id: "UCJka5SDh36_N4pjJd69efkg",
|
||||
name: "Savage Brick Sports",
|
||||
avatar: [
|
||||
Thumbnail(
|
||||
url: "https://yt3.ggpht.com/ytc/AMLnZu_s0H6HPGb4LYTxkE6fH1Cp5Mp8jfeOaMluW2A03Q=s68-c-k-c0x00ffffff-no-rj",
|
||||
width: 68,
|
||||
height: 68,
|
||||
),
|
||||
],
|
||||
verification: Verified,
|
||||
subscriber_count: None,
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
publish_date_txt: Some("3 months ago"),
|
||||
view_count: Some(1172372),
|
||||
is_live: false,
|
||||
is_short: false,
|
||||
is_upcoming: false,
|
||||
short_description: Some("NFL Trick Plays But They Get Increasingly Higher IQ\nCredit to CoshReport for starting this trend.\n\n(if any of the links don\'t work, check most recent video)\nTalkSports Discord: https://discord.gg/n..."),
|
||||
)),
|
||||
Video(VideoItem(
|
||||
id: "gUAd2XXzH7w",
|
||||
name: "⚓\u{fe0f}Found ABANDONED SHIP!!! Big CRUISE SHIP on a desert island☠\u{fe0f} Where did the people go?!?",
|
||||
duration: Some(2949),
|
||||
thumbnail: [
|
||||
Thumbnail(
|
||||
url: "https://i.ytimg.com/vi/gUAd2XXzH7w/hq720.jpg?sqp=-oaymwEcCOgCEMoBSFXyq4qpAw4IARUAAIhCGAFwAcABBg==&rs=AOn4CLDaBSyUxw88zjCr_Az868dEnhMrug",
|
||||
width: 360,
|
||||
height: 202,
|
||||
),
|
||||
Thumbnail(
|
||||
url: "https://i.ytimg.com/vi/gUAd2XXzH7w/hq720.jpg?sqp=-oaymwEcCNAFEJQDSFXyq4qpAw4IARUAAIhCGAFwAcABBg==&rs=AOn4CLAvfP1QR12y5cY8mvtg7Qqvl2XuTA",
|
||||
width: 720,
|
||||
height: 404,
|
||||
),
|
||||
],
|
||||
channel: Some(ChannelTag(
|
||||
id: "UClUZos7yKYtrmr0-azaD8pw",
|
||||
name: "Kreosan English",
|
||||
avatar: [
|
||||
Thumbnail(
|
||||
url: "https://yt3.ggpht.com/Rzi1oOWYL20M028wSLcD4eEkByC7kWGcBpr6WBAx0aGC9UAlIcGB_-D4rI_wkMsOHe9VnRWL3Q=s68-c-k-c0x00ffffff-no-rj",
|
||||
width: 68,
|
||||
height: 68,
|
||||
),
|
||||
],
|
||||
verification: Verified,
|
||||
subscriber_count: None,
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
publish_date_txt: Some("1 month ago"),
|
||||
view_count: Some(1883533),
|
||||
is_live: false,
|
||||
is_short: false,
|
||||
is_upcoming: false,
|
||||
short_description: Some("We are preparing a continuation of the cruise ship for you! Very soon you will be able to see the next part. If you would like to help us make a video:\n\n► Support us - https://www.patreon.com/k..."),
|
||||
)),
|
||||
Video(VideoItem(
|
||||
id: "YpGjaJ1ettI",
|
||||
name: "[Working BGM] Comfortable music that makes you feel positive -- Morning Mood -- Daily Routine",
|
||||
duration: Some(3651),
|
||||
thumbnail: [
|
||||
Thumbnail(
|
||||
url: "https://i.ytimg.com/vi/YpGjaJ1ettI/hq720.jpg?sqp=-oaymwEcCOgCEMoBSFXyq4qpAw4IARUAAIhCGAFwAcABBg==&rs=AOn4CLDjAMJifo4Bg-vXUdHXyWYRHSf-Sw",
|
||||
width: 360,
|
||||
height: 202,
|
||||
),
|
||||
Thumbnail(
|
||||
url: "https://i.ytimg.com/vi/YpGjaJ1ettI/hq720.jpg?sqp=-oaymwEcCNAFEJQDSFXyq4qpAw4IARUAAIhCGAFwAcABBg==&rs=AOn4CLAx95bizFu4fxePN4qbMdKIoNDCug",
|
||||
width: 720,
|
||||
height: 404,
|
||||
),
|
||||
],
|
||||
channel: Some(ChannelTag(
|
||||
id: "UCpxY9-3iB5Hyho31uBgzh7w",
|
||||
name: "Daily Routine",
|
||||
avatar: [
|
||||
Thumbnail(
|
||||
url: "https://yt3.ggpht.com/uci2aPM5XOEgdMt2h9aHMiN-K1-TmJQQPRdWvprNrpJpyZSLI9z0zFzyXQeQ1mNIQWl2QrjX3Rc=s68-c-k-c0x00ffffff-no-rj",
|
||||
width: 68,
|
||||
height: 68,
|
||||
),
|
||||
],
|
||||
verification: None,
|
||||
subscriber_count: None,
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
publish_date_txt: Some("2 months ago"),
|
||||
view_count: Some(1465389),
|
||||
is_live: false,
|
||||
is_short: false,
|
||||
is_upcoming: false,
|
||||
short_description: Some("Hello everyone. It\'s me again. I will stay at home and study . It\'s full of fun energy today, so it\'s ready to spread to everyone with hilarious music. 🔥🔥🔥\nHave fun together 😊😊😊..."),
|
||||
)),
|
||||
Video(VideoItem(
|
||||
id: "rPAhFD8hKxQ",
|
||||
name: "Survival Camping 9ft/3m Under Snow - Giant Winter Bushcraft Shelter and Quinzee",
|
||||
duration: Some(1301),
|
||||
thumbnail: [
|
||||
Thumbnail(
|
||||
url: "https://i.ytimg.com/vi/rPAhFD8hKxQ/hq720.jpg?sqp=-oaymwEcCOgCEMoBSFXyq4qpAw4IARUAAIhCGAFwAcABBg==&rs=AOn4CLCY0Xhznr6RKZ-EG1G5C1M34h8ugA",
|
||||
width: 360,
|
||||
height: 202,
|
||||
),
|
||||
Thumbnail(
|
||||
url: "https://i.ytimg.com/vi/rPAhFD8hKxQ/hq720.jpg?sqp=-oaymwEcCNAFEJQDSFXyq4qpAw4IARUAAIhCGAFwAcABBg==&rs=AOn4CLBiANoEaNfk7eMjCAxapIK5NiYmmQ",
|
||||
width: 720,
|
||||
height: 404,
|
||||
),
|
||||
],
|
||||
channel: Some(ChannelTag(
|
||||
id: "UCfpCQ89W9wjkHc8J_6eTbBg",
|
||||
name: "Outdoor Boys",
|
||||
avatar: [
|
||||
Thumbnail(
|
||||
url: "https://yt3.ggpht.com/ytc/AMLnZu8v_ZMJTqxqU7M__w8nHHaygAyOvsqCnFeIhjQxFw=s68-c-k-c0x00ffffff-no-rj",
|
||||
width: 68,
|
||||
height: 68,
|
||||
),
|
||||
],
|
||||
verification: Verified,
|
||||
subscriber_count: None,
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
publish_date_txt: Some("6 months ago"),
|
||||
view_count: Some(20488431),
|
||||
is_live: false,
|
||||
is_short: false,
|
||||
is_upcoming: false,
|
||||
short_description: Some("Solo winter camping and bushcraft 9 feet (3 meters) under the snow. I hiked high up into the mountains during a snow storm with 30 mph/48 kmh winds to build a deep snow bushcraft survival shelter..."),
|
||||
)),
|
||||
Video(VideoItem(
|
||||
id: "2rye4u-cCNk",
|
||||
name: "Pink Panther Fights Off Pests | 54 Minute Compilation | The Pink Panther Show",
|
||||
duration: Some(3158),
|
||||
thumbnail: [
|
||||
Thumbnail(
|
||||
url: "https://i.ytimg.com/vi/2rye4u-cCNk/hq720.jpg?sqp=-oaymwEcCOgCEMoBSFXyq4qpAw4IARUAAIhCGAFwAcABBg==&rs=AOn4CLCi4Tt2tz-kk-cumb7SEfzzgixj5A",
|
||||
width: 360,
|
||||
height: 202,
|
||||
),
|
||||
Thumbnail(
|
||||
url: "https://i.ytimg.com/vi/2rye4u-cCNk/hq720.jpg?sqp=-oaymwEcCNAFEJQDSFXyq4qpAw4IARUAAIhCGAFwAcABBg==&rs=AOn4CLD4QbHfCufvmol1UNj5wqmOtjZNvw",
|
||||
width: 720,
|
||||
height: 404,
|
||||
),
|
||||
],
|
||||
channel: Some(ChannelTag(
|
||||
id: "UCFeUyPY6W8qX8w2o6oSiRmw",
|
||||
name: "Official Pink Panther",
|
||||
avatar: [
|
||||
Thumbnail(
|
||||
url: "https://yt3.ggpht.com/ytc/AMLnZu-htKBt4jUDwmnm0r-ojGjHZMy9-H92Q1pRoAfkgw=s68-c-k-c0x00ffffff-no-rj",
|
||||
width: 68,
|
||||
height: 68,
|
||||
),
|
||||
],
|
||||
verification: Verified,
|
||||
subscriber_count: None,
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
publish_date_txt: Some("11 months ago"),
|
||||
view_count: Some(27357653),
|
||||
is_live: false,
|
||||
is_short: false,
|
||||
is_upcoming: false,
|
||||
short_description: Some("(1) Pink Pest Control\n(2) Pink-a-Boo\n(3) Little Beaux Pink\n(4) The Pink Package Plot\n(5) Come On In! The Water\'s Pink\n(6) Psychedelic Pink\n(7) Pink Posies\n(8) G.I. Pink\n\nThe Pink Panther is..."),
|
||||
)),
|
||||
Video(VideoItem(
|
||||
id: "O0xAlfSaBNQ",
|
||||
name: "FC Nantes vs. SC Freiburg – Highlights & Tore | UEFA Europa League",
|
||||
duration: Some(326),
|
||||
thumbnail: [
|
||||
Thumbnail(
|
||||
url: "https://i.ytimg.com/vi/O0xAlfSaBNQ/hq720.jpg?sqp=-oaymwEcCOgCEMoBSFXyq4qpAw4IARUAAIhCGAFwAcABBg==&rs=AOn4CLDe-1NUODMNivJw5r5J5Wd16PMsqA",
|
||||
width: 360,
|
||||
height: 202,
|
||||
),
|
||||
Thumbnail(
|
||||
url: "https://i.ytimg.com/vi/O0xAlfSaBNQ/hq720.jpg?sqp=-oaymwEcCNAFEJQDSFXyq4qpAw4IARUAAIhCGAFwAcABBg==&rs=AOn4CLAMD0BFcC-x_UYe-F5q5y4GPcGnWA",
|
||||
width: 720,
|
||||
height: 404,
|
||||
),
|
||||
],
|
||||
channel: Some(ChannelTag(
|
||||
id: "UC8WYi3XQXsf-6FNvqoEvxag",
|
||||
name: "RTL Sport",
|
||||
avatar: [
|
||||
Thumbnail(
|
||||
url: "https://yt3.ggpht.com/E1ZL4Cnc8ej3MeHR0To12hetHWrlhcupsz0nFyZmEJoWvLvJo9aOXvPOWmNMWn9tJLoMB3duRg=s68-c-k-c0x00ffffff-no-rj",
|
||||
width: 68,
|
||||
height: 68,
|
||||
),
|
||||
],
|
||||
verification: Verified,
|
||||
subscriber_count: None,
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
publish_date_txt: Some("11 hours ago"),
|
||||
view_count: Some(117395),
|
||||
is_live: false,
|
||||
is_short: false,
|
||||
is_upcoming: false,
|
||||
short_description: Some("UEFA Europa League: https://www.rtlplus.com/shows/uefa-europa-league-19818?utm_source=youtube&utm_medium=editorial&utm_campaign=beschreibung&utm_term=rtlsport \nFC Nantes vs. SC Freiburg –..."),
|
||||
)),
|
||||
Video(VideoItem(
|
||||
id: "Mhs9Sbnw19o",
|
||||
name: "Dramatisches Duell: 400 Jahre altes Kästchen erzielt zig-fachen Wunschpreis! | Bares für Rares XXL",
|
||||
duration: Some(744),
|
||||
thumbnail: [
|
||||
Thumbnail(
|
||||
url: "https://i.ytimg.com/vi/Mhs9Sbnw19o/hq720.jpg?sqp=-oaymwEcCOgCEMoBSFXyq4qpAw4IARUAAIhCGAFwAcABBg==&rs=AOn4CLBkxXdE8JNS0S6_Dhl-aY7FRmbL9g",
|
||||
width: 360,
|
||||
height: 202,
|
||||
),
|
||||
Thumbnail(
|
||||
url: "https://i.ytimg.com/vi/Mhs9Sbnw19o/hq720.jpg?sqp=-oaymwEcCNAFEJQDSFXyq4qpAw4IARUAAIhCGAFwAcABBg==&rs=AOn4CLAqbRhx4fQfK_2mVGNX_0_dZQt0YQ",
|
||||
width: 720,
|
||||
height: 404,
|
||||
),
|
||||
],
|
||||
channel: Some(ChannelTag(
|
||||
id: "UC53bIpnef1pwAx69ERmmOLA",
|
||||
name: "Bares für Rares",
|
||||
avatar: [
|
||||
Thumbnail(
|
||||
url: "https://yt3.ggpht.com/ytc/AMLnZu-ZyE4lblLYyk8iis1xoH_v64_tmhWca2Z6wmsVexk=s68-c-k-c0x00ffffff-no-rj",
|
||||
width: 68,
|
||||
height: 68,
|
||||
),
|
||||
],
|
||||
verification: Verified,
|
||||
subscriber_count: None,
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
publish_date_txt: Some("11 days ago"),
|
||||
view_count: Some(836333),
|
||||
is_live: false,
|
||||
is_short: false,
|
||||
is_upcoming: false,
|
||||
short_description: Some("Du hast Schätze im Keller, die du unseren Expert*innen präsentieren möchtest? Hier geht\'s zum Bewerbungsformular: kurz.zdf.de/lSJ/\n\nEin einmaliges Bieterduell treibt den Preis für dieses..."),
|
||||
)),
|
||||
Video(VideoItem(
|
||||
id: "Bzzp5Cay7DI",
|
||||
name: "Sweet Jazz - Cool autumn Bossa Nova & October Jazz Positive Mood",
|
||||
duration: None,
|
||||
thumbnail: [
|
||||
Thumbnail(
|
||||
url: "https://i.ytimg.com/vi/Bzzp5Cay7DI/hq720_live.jpg?sqp=COz4qZoG-oaymwEcCOgCEMoBSFXyq4qpAw4IARUAAIhCGAFwAcABBg==&rs=AOn4CLAKcYaDyG1yocH1e2_BIyl5FGKWPw",
|
||||
width: 360,
|
||||
height: 202,
|
||||
),
|
||||
Thumbnail(
|
||||
url: "https://i.ytimg.com/vi/Bzzp5Cay7DI/hq720_live.jpg?sqp=COz4qZoG-oaymwEcCNAFEJQDSFXyq4qpAw4IARUAAIhCGAFwAcABBg==&rs=AOn4CLBOaXPCJec4XuaFyJ1-6dcnJWEmrg",
|
||||
width: 720,
|
||||
height: 404,
|
||||
),
|
||||
],
|
||||
channel: Some(ChannelTag(
|
||||
id: "UCoGlllJE7aYe_VzIGP3s_wA",
|
||||
name: "Smooth Jazz Music",
|
||||
avatar: [
|
||||
Thumbnail(
|
||||
url: "https://yt3.ggpht.com/babJ-iwY1cNs3mE2CnDiBSf0IjePgGuCLNLvLGcepj6tzXNLbSAQA7rQho35fKv9qFxEVIWdCw=s68-c-k-c0x00ffffff-no-rj",
|
||||
width: 68,
|
||||
height: 68,
|
||||
),
|
||||
],
|
||||
verification: None,
|
||||
subscriber_count: None,
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
publish_date_txt: None,
|
||||
view_count: Some(1216),
|
||||
is_live: true,
|
||||
is_short: false,
|
||||
is_upcoming: false,
|
||||
short_description: Some("Sweet Jazz - Cool autumn Bossa Nova & October Jazz Positive Mood\nhttps://youtu.be/Bzzp5Cay7DI\n********************************************\nSounds available on: Jazz Bossa Nova\nOFFICIAL VIDEO:..."),
|
||||
)),
|
||||
Video(VideoItem(
|
||||
id: "SlskTqc9CEc",
|
||||
name: "The Chick-Fil-A Full Menu Challenge",
|
||||
duration: Some(613),
|
||||
thumbnail: [
|
||||
Thumbnail(
|
||||
url: "https://i.ytimg.com/vi/SlskTqc9CEc/hq720.jpg?sqp=-oaymwEcCOgCEMoBSFXyq4qpAw4IARUAAIhCGAFwAcABBg==&rs=AOn4CLBjDpJq0J5r8jvLwIQG2HCvsoj8nw",
|
||||
width: 360,
|
||||
height: 202,
|
||||
),
|
||||
Thumbnail(
|
||||
url: "https://i.ytimg.com/vi/SlskTqc9CEc/hq720.jpg?sqp=-oaymwEcCNAFEJQDSFXyq4qpAw4IARUAAIhCGAFwAcABBg==&rs=AOn4CLCnwo-jiD8xsP29kf6a5jMwIqHPEA",
|
||||
width: 720,
|
||||
height: 404,
|
||||
),
|
||||
],
|
||||
channel: Some(ChannelTag(
|
||||
id: "UCd1fLoVFooPeWqCEYVUJZqg",
|
||||
name: "Matt Stonie",
|
||||
avatar: [
|
||||
Thumbnail(
|
||||
url: "https://yt3.ggpht.com/ytc/AMLnZu9Of1-RwNeaBY6nulF3DECzDcAdZRbC_aOvZHPedw=s68-c-k-c0x00ffffff-no-rj",
|
||||
width: 68,
|
||||
height: 68,
|
||||
),
|
||||
],
|
||||
verification: Verified,
|
||||
subscriber_count: None,
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
publish_date_txt: Some("3 years ago"),
|
||||
view_count: Some(39286403),
|
||||
is_live: false,
|
||||
is_short: false,
|
||||
is_upcoming: false,
|
||||
short_description: Some("Good Video? Like/Fav & Share!!\n\nTBH this is really my 1st time trying Chick-Fil-A, legitimately. My verdict is torn, but that sauce is BOMB!\n\nChallenge\n+ Chick-Fil-A Deluxe\n+ Spicy Deluxe\n+..."),
|
||||
)),
|
||||
Video(VideoItem(
|
||||
id: "CwRvM2TfYbs",
|
||||
name: "Gentle healing music of health and to calm the nervous system, deep relaxation! Say Life Yes",
|
||||
duration: None,
|
||||
thumbnail: [
|
||||
Thumbnail(
|
||||
url: "https://i.ytimg.com/vi/CwRvM2TfYbs/hq720_live.jpg?sqp=COz4qZoG-oaymwEcCOgCEMoBSFXyq4qpAw4IARUAAIhCGAFwAcABBg==&rs=AOn4CLCj3HTq1K0KCuiuZdyh_by4VUZWeA",
|
||||
width: 360,
|
||||
height: 202,
|
||||
),
|
||||
Thumbnail(
|
||||
url: "https://i.ytimg.com/vi/CwRvM2TfYbs/hq720_live.jpg?sqp=COz4qZoG-oaymwEcCNAFEJQDSFXyq4qpAw4IARUAAIhCGAFwAcABBg==&rs=AOn4CLA-rjU_R19afFlCk22vmfHEtfFKcA",
|
||||
width: 720,
|
||||
height: 404,
|
||||
),
|
||||
],
|
||||
channel: Some(ChannelTag(
|
||||
id: "UC6jH5GNi0iOR17opA1Vowhw",
|
||||
name: "Lucid Dream",
|
||||
avatar: [
|
||||
Thumbnail(
|
||||
url: "https://yt3.ggpht.com/QlTKeA9Cx-4qajm4VaLGGGH0cCVe8Fda_c6SScCLPy8fsu0ZQkDhtBB3qcZastIZPQNew5vi-LM=s68-c-k-c0x00ffffff-no-rj",
|
||||
width: 68,
|
||||
height: 68,
|
||||
),
|
||||
],
|
||||
verification: Verified,
|
||||
subscriber_count: None,
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
publish_date_txt: None,
|
||||
view_count: Some(1416),
|
||||
is_live: true,
|
||||
is_short: false,
|
||||
is_upcoming: false,
|
||||
short_description: Some("🌿 Music for relaxation, meditation, study, reading, massage, spa or sleep. This music is ideal for dealing with anxiety, stress or insomnia as it promotes relaxation and helps eliminate..."),
|
||||
)),
|
||||
Video(VideoItem(
|
||||
id: "7jz0pXSe_kI",
|
||||
name: "Craziest \"Fine...I\'ll Do it Myself\" Moments in Sports History (PART 2)",
|
||||
duration: Some(1822),
|
||||
thumbnail: [
|
||||
Thumbnail(
|
||||
url: "https://i.ytimg.com/vi/7jz0pXSe_kI/hq720.jpg?sqp=-oaymwEcCOgCEMoBSFXyq4qpAw4IARUAAIhCGAFwAcABBg==&rs=AOn4CLDEUQzJHcD0s2BgP1znPupwsxf48w",
|
||||
width: 360,
|
||||
height: 202,
|
||||
),
|
||||
Thumbnail(
|
||||
url: "https://i.ytimg.com/vi/7jz0pXSe_kI/hq720.jpg?sqp=-oaymwEcCNAFEJQDSFXyq4qpAw4IARUAAIhCGAFwAcABBg==&rs=AOn4CLB1yzi-24jCXlAki1xIq0aDMqQY3A",
|
||||
width: 720,
|
||||
height: 404,
|
||||
),
|
||||
],
|
||||
channel: Some(ChannelTag(
|
||||
id: "UCd5hdemikI6GxwGKhJCwzww",
|
||||
name: "Highlight Reel",
|
||||
avatar: [
|
||||
Thumbnail(
|
||||
url: "https://yt3.ggpht.com/NETjJS3cNlblrg70CD4LH_Mma5lYmZSO3NlUnzi5Vd_cRD3XkVyaO1UCFTq6acK52g9XDly9-A=s68-c-k-c0x00ffffff-no-rj",
|
||||
width: 68,
|
||||
height: 68,
|
||||
),
|
||||
],
|
||||
verification: Verified,
|
||||
subscriber_count: None,
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
publish_date_txt: Some("10 months ago"),
|
||||
view_count: Some(11601863),
|
||||
is_live: false,
|
||||
is_short: false,
|
||||
is_upcoming: false,
|
||||
short_description: Some("(PART 2) of 👉🏼 Craziest \"Fine...I\'ll Do It Myself\" Moments in Sports History \n\nBIBLE VERSE OF THE DAY: Luke 12:40"),
|
||||
)),
|
||||
],
|
||||
ctoken: Some("4qmFsgKxAxIPRkV3aGF0X3RvX3dhdGNoGoADQ0RCNmxnSkhUWFpRYzJOVU1UUm1iME5OWjNOSmQzWjZOM0JPWlZWMldqZDFRVlp3ZEVOdGMwdEhXR3d3V0ROQ2FGb3lWbVpqTWpWb1kwaE9iMkl6VW1aamJWWnVZVmM1ZFZsWGQxTklNVlUwVDFSU1dXUXhUbXhXTTBaeVdsaGtSRkpGYkZCWk0yaDZWbXMxTlV4VmVGbE1XRnBSVlcxallVeFJRVUZhVnpSQlFWWldWRUZCUmtWU1VVRkNRVVZhUm1ReWFHaGtSamt3WWpFNU0xbFlVbXBoUVVGQ1FVRkZRa0ZCUVVKQlFVVkJRVUZGUWtGSFNrSkRRVUZUUlROQ2FGb3lWbVpqTWpWb1kwaE9iMkl6VW1aa1J6bHlXbGMwWVVWM2Ftb3hPRkJGT1dWSU5rRm9WVlpZWlVGTFNGaHVSMEp2ZDJsRmQycERObkZmUlRsbFNEWkJhRmRIZG1RMFMwaGxaMGhDTlZnMmJrMWxPVU5SU1VsTlVRJTNEJTNEmgIaYnJvd3NlLWZlZWRGRXdoYXRfdG9fd2F0Y2g%3D"),
|
||||
endpoint: browse,
|
||||
)
|
|
@ -5,7 +5,7 @@ expression: map_res.c
|
|||
VideoPlayer(
|
||||
details: VideoPlayerDetails(
|
||||
id: "pPvd8UxmSbQ",
|
||||
name: "Inspiring Cinematic Uplifting (Creative Commons)",
|
||||
name: Some("Inspiring Cinematic Uplifting (Creative Commons)"),
|
||||
description: Some("► Download Music: http://bit.ly/2QLufeh\nImportant to know! You can download this track for free through Patreon. You will pay only for new tracks! So join others and let\'s make next track together!\n\n► MORE MUSIC: Become my patron and get access to all our music from Patreon library. More Info here: http://bit.ly/2JJDFHb\n\n► Additional edit versions of this track you can download here: http://bit.ly/2WdRinT (5 versions)\n--------------------- \n\n►DESCRIPTION:\nInspiring Cinematic Uplifting Trailer Background - epic music for trailer video project with powerful drums, energetic orchestra and gentle piano melody. This motivational cinematic theme will work as perfect background for beautiful epic moments, landscapes, nature, drone video, motivational products and achievements.\n--------------------- \n\n► LICENSE:\n● If you need a license for your project, you can purchase it here: \nhttps://1.envato.market/ajicu (Audiojungle)\nhttps://bit.ly/3fWZZuI (Pond5)\n--------------------- \n\n► LISTEN ON:\n● Spotify - https://spoti.fi/2sHm3UH\n● Apple Music - https://apple.co/3qBjbUO\n--------------------- \n\n► SUBSCRIBE FOR MORE: \nPatreon: http://bit.ly/2JJDFHb\nYoutube: http://bit.ly/2AYBzfA\nFacebook: http://bit.ly/2T6dTx5\nInstagram: http://bit.ly/2BHJ8rB\nTwitter: http://bit.ly/2MwtOlT\nSoundCloud: http://bit.ly/2IwVVmt\nAudiojungle: https://1.envato.market/ajrsm\nPond5: https://bit.ly/2TLi1rW\n--------------------- \n►Photo by Vittorio Staffolani from Pexels\n--------------------- \n\nFAQ:\n\n► Can I use this music in my videos? \n● Sure! Just download this track and you are ready to use it! We only ask to credit us. \n-------------------- \n\n► What is \"Creative Commons\"? \nCreative Commons is a system that allows you to legally use “some rights reserved” music, movies, images, and other content — all for free. Licensees may copy, distribute, display and perform the work and make derivative works and remixes based on it only if they give the author or licensor the credits.\n-------------------- \n\n► Will I have any copyright issues with this track?\n● No, you should not have any copyright problems with this track!\n-------------------- \n\n► Is it necessary to become your patron?\n● No it\'s not necessary. But we recommend you to become our patron because you will get access to huge library of music. You will download only highest quality files. You will find additional edited versions of every track. You always be tuned with our news. You will find music not only from Roman Senyk but also from another talented authors.\n-------------------- \n\n► Why I received a copyright claim when I used this track?\n● Do not panic! This is very common situation. Content ID fingerprint system can mismatch our music. Just dispute the claim by showing our original track. Or send us the link to your video (romansenykmusic@gmail.com) and attach some screenshot with claim information. Claim will be released until 24 hours!\n\n► How to credit you in my video?\n● Just add to the description of your project information about Author, Name of Song and the link to our original track. Or copy and paste:\n\nMusic Info: Inspiring Cinematic Uplifting by RomanSenykMusic.\nMusic Link: https://youtu.be/pPvd8UxmSbQ\n--------------------- \n\n► If you have any questions, you can write in the comments for this video or by email: romansenykmusic@gmail.com\n--------------------- \n\nStay tuned! The best is yet to come! \nThanks For Listening!\nRoman Senyk"),
|
||||
duration: 163,
|
||||
thumbnail: [
|
||||
|
@ -30,11 +30,9 @@ VideoPlayer(
|
|||
height: 480,
|
||||
),
|
||||
],
|
||||
channel: ChannelId(
|
||||
id: "UCbxxEi-ImPlbLx5F-fHetEg",
|
||||
name: "RomanSenykMusic - Royalty Free Music",
|
||||
),
|
||||
view_count: 426567,
|
||||
channel_id: "UCbxxEi-ImPlbLx5F-fHetEg",
|
||||
channel_name: Some("RomanSenykMusic - Royalty Free Music"),
|
||||
view_count: Some(426567),
|
||||
keywords: [
|
||||
"no copyright music",
|
||||
"background music",
|
||||
|
@ -79,7 +77,6 @@ VideoPlayer(
|
|||
mime: "video/3gpp; codecs=\"mp4v.20.3, mp4a.40.2\"",
|
||||
format: r#3gp,
|
||||
codec: mp4v,
|
||||
throttled: false,
|
||||
),
|
||||
VideoStream(
|
||||
url: "https://rr5---sn-h0jeenek.googlevideo.com/videoplayback?c=ANDROID&clen=11439331&dur=163.096&ei=q1jpYtOPEYSBgQeHmqbwAQ&expire=1659481355&fexp=24001373%2C24007246&fvip=4&gir=yes&id=o-AEDMTCojVtwpIKOdhBaxEHE5s322qnAJHGqa2r1F46BM&initcwndbps=1527500&ip=2003%3Ade%3Aaf0e%3A2f00%3Ade47%3A297%3Aa6db%3A774e&itag=18&lmt=1580005476071743&lsig=AG3C_xAwRgIhAOiL-qJ04sA8FSOkEJfOYl3gFe4SzwYu_rAf3DMLHYigAiEA0Upi1HqqIu7NH_LTDL0jT1R5TTozQypL5FiSP9RoqtU%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&mh=mQ&mime=video%2Fmp4&mm=31%2C29&mn=sn-h0jeenek%2Csn-h0jelnez&ms=au%2Crdu&mt=1659459429&mv=m&mvi=5&pl=37&ratebypass=yes&rbqsm=fr&requiressl=yes&sig=AOq0QJ8wRQIhAJAH-tWof01vrs8phEoz51XkWwdMzQ77k1UTrdY5XiuTAiA38z-qANX0jtfCiAl4EVMZaKo1ncrzJFRrCffZ6LagrA%3D%3D&source=youtube&sparams=expire%2Cei%2Cip%2Cid%2Citag%2Csource%2Crequiressl%2Cvprv%2Cmime%2Cgir%2Cclen%2Cratebypass%2Cdur%2Clmt&txp=2211222&vprv=1",
|
||||
|
@ -98,7 +95,6 @@ VideoPlayer(
|
|||
mime: "video/mp4; codecs=\"avc1.42001E, mp4a.40.2\"",
|
||||
format: mp4,
|
||||
codec: avc1,
|
||||
throttled: false,
|
||||
),
|
||||
],
|
||||
video_only_streams: [
|
||||
|
@ -125,7 +121,6 @@ VideoPlayer(
|
|||
mime: "video/mp4; codecs=\"av01.0.00M.08\"",
|
||||
format: mp4,
|
||||
codec: av01,
|
||||
throttled: false,
|
||||
),
|
||||
VideoStream(
|
||||
url: "https://rr5---sn-h0jeenek.googlevideo.com/videoplayback?c=ANDROID&clen=2238952&dur=163.029&ei=q1jpYtOPEYSBgQeHmqbwAQ&expire=1659481355&fexp=24001373%2C24007246&fvip=4&gir=yes&id=o-AEDMTCojVtwpIKOdhBaxEHE5s322qnAJHGqa2r1F46BM&initcwndbps=1527500&ip=2003%3Ade%3Aaf0e%3A2f00%3Ade47%3A297%3Aa6db%3A774e&itag=395&keepalive=yes&lmt=1608045728968690&lsig=AG3C_xAwRgIhAOiL-qJ04sA8FSOkEJfOYl3gFe4SzwYu_rAf3DMLHYigAiEA0Upi1HqqIu7NH_LTDL0jT1R5TTozQypL5FiSP9RoqtU%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&mh=mQ&mime=video%2Fmp4&mm=31%2C29&mn=sn-h0jeenek%2Csn-h0jelnez&ms=au%2Crdu&mt=1659459429&mv=m&mvi=5&otfp=1&pl=37&rbqsm=fr&requiressl=yes&sig=AOq0QJ8wRQIhAKCXHOCh_P3VlNWebTeWw0WdSln-zYe3BjZeEm2QiltCAiAQNcJBI4G-8dK5z1IUoqBZctk6ddjkl_QYKRFAKXyOcw%3D%3D&source=youtube&sparams=expire%2Cei%2Cip%2Cid%2Citag%2Csource%2Crequiressl%2Cvprv%2Cmime%2Cgir%2Cclen%2Cotfp%2Cdur%2Clmt&txp=1311222&vprv=1",
|
||||
|
@ -150,7 +145,6 @@ VideoPlayer(
|
|||
mime: "video/mp4; codecs=\"av01.0.00M.08\"",
|
||||
format: mp4,
|
||||
codec: av01,
|
||||
throttled: false,
|
||||
),
|
||||
VideoStream(
|
||||
url: "https://rr5---sn-h0jeenek.googlevideo.com/videoplayback?c=ANDROID&clen=7808990&dur=163.029&ei=q1jpYtOPEYSBgQeHmqbwAQ&expire=1659481355&fexp=24001373%2C24007246&fvip=4&gir=yes&id=o-AEDMTCojVtwpIKOdhBaxEHE5s322qnAJHGqa2r1F46BM&initcwndbps=1527500&ip=2003%3Ade%3Aaf0e%3A2f00%3Ade47%3A297%3Aa6db%3A774e&itag=134&keepalive=yes&lmt=1580005649163759&lsig=AG3C_xAwRgIhAOiL-qJ04sA8FSOkEJfOYl3gFe4SzwYu_rAf3DMLHYigAiEA0Upi1HqqIu7NH_LTDL0jT1R5TTozQypL5FiSP9RoqtU%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&mh=mQ&mime=video%2Fmp4&mm=31%2C29&mn=sn-h0jeenek%2Csn-h0jelnez&ms=au%2Crdu&mt=1659459429&mv=m&mvi=5&otfp=1&pl=37&rbqsm=fr&requiressl=yes&sig=AOq0QJ8wRQIhAIjjrMvCEzSLlbvbrjItT4V9JdpggnO5IHye9i4PxTyzAiAmbaFCB2hH7evf9JX3JUx-tU9S6zv2IzSKz8ObGSVRjw%3D%3D&source=youtube&sparams=expire%2Cei%2Cip%2Cid%2Citag%2Csource%2Crequiressl%2Cvprv%2Cmime%2Cgir%2Cclen%2Cotfp%2Cdur%2Clmt&txp=2211222&vprv=1",
|
||||
|
@ -175,7 +169,6 @@ VideoPlayer(
|
|||
mime: "video/mp4; codecs=\"avc1.4d401e\"",
|
||||
format: mp4,
|
||||
codec: avc1,
|
||||
throttled: false,
|
||||
),
|
||||
VideoStream(
|
||||
url: "https://rr5---sn-h0jeenek.googlevideo.com/videoplayback?c=ANDROID&clen=4130385&dur=163.029&ei=q1jpYtOPEYSBgQeHmqbwAQ&expire=1659481355&fexp=24001373%2C24007246&fvip=4&gir=yes&id=o-AEDMTCojVtwpIKOdhBaxEHE5s322qnAJHGqa2r1F46BM&initcwndbps=1527500&ip=2003%3Ade%3Aaf0e%3A2f00%3Ade47%3A297%3Aa6db%3A774e&itag=396&keepalive=yes&lmt=1608045761576250&lsig=AG3C_xAwRgIhAOiL-qJ04sA8FSOkEJfOYl3gFe4SzwYu_rAf3DMLHYigAiEA0Upi1HqqIu7NH_LTDL0jT1R5TTozQypL5FiSP9RoqtU%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&mh=mQ&mime=video%2Fmp4&mm=31%2C29&mn=sn-h0jeenek%2Csn-h0jelnez&ms=au%2Crdu&mt=1659459429&mv=m&mvi=5&otfp=1&pl=37&rbqsm=fr&requiressl=yes&sig=AOq0QJ8wRQIgBrQhbygTP6RGjUk0lGbxBI5e3NdeR6C_SW8R_ckZ2PkCIQDaBg5cJxYVWfwRrrELQFgRMOJ4xS3oOOROayoQMjxaCA%3D%3D&source=youtube&sparams=expire%2Cei%2Cip%2Cid%2Citag%2Csource%2Crequiressl%2Cvprv%2Cmime%2Cgir%2Cclen%2Cotfp%2Cdur%2Clmt&txp=1311222&vprv=1",
|
||||
|
@ -200,7 +193,6 @@ VideoPlayer(
|
|||
mime: "video/mp4; codecs=\"av01.0.01M.08\"",
|
||||
format: mp4,
|
||||
codec: av01,
|
||||
throttled: false,
|
||||
),
|
||||
VideoStream(
|
||||
url: "https://rr5---sn-h0jeenek.googlevideo.com/videoplayback?c=ANDROID&clen=6873325&dur=163.029&ei=q1jpYtOPEYSBgQeHmqbwAQ&expire=1659481355&fexp=24001373%2C24007246&fvip=4&gir=yes&id=o-AEDMTCojVtwpIKOdhBaxEHE5s322qnAJHGqa2r1F46BM&initcwndbps=1527500&ip=2003%3Ade%3Aaf0e%3A2f00%3Ade47%3A297%3Aa6db%3A774e&itag=397&keepalive=yes&lmt=1608045990917419&lsig=AG3C_xAwRgIhAOiL-qJ04sA8FSOkEJfOYl3gFe4SzwYu_rAf3DMLHYigAiEA0Upi1HqqIu7NH_LTDL0jT1R5TTozQypL5FiSP9RoqtU%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&mh=mQ&mime=video%2Fmp4&mm=31%2C29&mn=sn-h0jeenek%2Csn-h0jelnez&ms=au%2Crdu&mt=1659459429&mv=m&mvi=5&otfp=1&pl=37&rbqsm=fr&requiressl=yes&sig=AOq0QJ8wRQIhAMqBb1hKVVzWl3Awrh1T8GQG9IrSWF84zW_ZfjgbAN5QAiAaP3jYyI4ox2aclcOCzYFzqWgByWCxj_FgTN-SfsARXw%3D%3D&source=youtube&sparams=expire%2Cei%2Cip%2Cid%2Citag%2Csource%2Crequiressl%2Cvprv%2Cmime%2Cgir%2Cclen%2Cotfp%2Cdur%2Clmt&txp=1311222&vprv=1",
|
||||
|
@ -225,7 +217,6 @@ VideoPlayer(
|
|||
mime: "video/mp4; codecs=\"av01.0.04M.08\"",
|
||||
format: mp4,
|
||||
codec: av01,
|
||||
throttled: false,
|
||||
),
|
||||
VideoStream(
|
||||
url: "https://rr5---sn-h0jeenek.googlevideo.com/videoplayback?c=ANDROID&dur=163.096&ei=q1jpYtOPEYSBgQeHmqbwAQ&expire=1659481355&fexp=24001373%2C24007246&fvip=4&id=o-AEDMTCojVtwpIKOdhBaxEHE5s322qnAJHGqa2r1F46BM&initcwndbps=1527500&ip=2003%3Ade%3Aaf0e%3A2f00%3Ade47%3A297%3Aa6db%3A774e&itag=22&lmt=1580005750956837&lsig=AG3C_xAwRgIhAOiL-qJ04sA8FSOkEJfOYl3gFe4SzwYu_rAf3DMLHYigAiEA0Upi1HqqIu7NH_LTDL0jT1R5TTozQypL5FiSP9RoqtU%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&mh=mQ&mime=video%2Fmp4&mm=31%2C29&mn=sn-h0jeenek%2Csn-h0jelnez&ms=au%2Crdu&mt=1659459429&mv=m&mvi=5&pl=37&ratebypass=yes&rbqsm=fr&requiressl=yes&sig=AOq0QJ8wRAIgFlQZgR63Yz9UgY9gVqiyGDVkZmSmACRP3-MmKN7CRzQCIAMHAwZbHmWL1qNH4Nu3A0pXZwErXMVPzMIt-PyxeZqa&source=youtube&sparams=expire%2Cei%2Cip%2Cid%2Citag%2Csource%2Crequiressl%2Cvprv%2Cmime%2Cratebypass%2Cdur%2Clmt&txp=2211222&vprv=1",
|
||||
|
@ -244,7 +235,6 @@ VideoPlayer(
|
|||
mime: "video/mp4; codecs=\"avc1.64001F, mp4a.40.2\"",
|
||||
format: mp4,
|
||||
codec: avc1,
|
||||
throttled: false,
|
||||
),
|
||||
VideoStream(
|
||||
url: "https://rr5---sn-h0jeenek.googlevideo.com/videoplayback?c=ANDROID&clen=22365208&dur=163.046&ei=q1jpYtOPEYSBgQeHmqbwAQ&expire=1659481355&fexp=24001373%2C24007246&fvip=4&gir=yes&id=o-AEDMTCojVtwpIKOdhBaxEHE5s322qnAJHGqa2r1F46BM&initcwndbps=1527500&ip=2003%3Ade%3Aaf0e%3A2f00%3Ade47%3A297%3Aa6db%3A774e&itag=398&keepalive=yes&lmt=1608048380553749&lsig=AG3C_xAwRgIhAOiL-qJ04sA8FSOkEJfOYl3gFe4SzwYu_rAf3DMLHYigAiEA0Upi1HqqIu7NH_LTDL0jT1R5TTozQypL5FiSP9RoqtU%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&mh=mQ&mime=video%2Fmp4&mm=31%2C29&mn=sn-h0jeenek%2Csn-h0jelnez&ms=au%2Crdu&mt=1659459429&mv=m&mvi=5&otfp=1&pl=37&rbqsm=fr&requiressl=yes&sig=AOq0QJ8wRAIgR6KqCOoig_FMl2tWKa7qHSmCjIZa9S7ABzEI16qdO2sCIFXccwql4bqV9CHlqXY4tgxyMFUsp7vW4XUjxs3AyG6H&source=youtube&sparams=expire%2Cei%2Cip%2Cid%2Citag%2Csource%2Crequiressl%2Cvprv%2Cmime%2Cgir%2Cclen%2Cotfp%2Cdur%2Clmt&txp=1311222&vprv=1",
|
||||
|
@ -269,7 +259,6 @@ VideoPlayer(
|
|||
mime: "video/mp4; codecs=\"av01.0.08M.08\"",
|
||||
format: mp4,
|
||||
codec: av01,
|
||||
throttled: false,
|
||||
),
|
||||
VideoStream(
|
||||
url: "https://rr5---sn-h0jeenek.googlevideo.com/videoplayback?c=ANDROID&clen=65400181&dur=163.046&ei=q1jpYtOPEYSBgQeHmqbwAQ&expire=1659481355&fexp=24001373%2C24007246&fvip=4&gir=yes&id=o-AEDMTCojVtwpIKOdhBaxEHE5s322qnAJHGqa2r1F46BM&initcwndbps=1527500&ip=2003%3Ade%3Aaf0e%3A2f00%3Ade47%3A297%3Aa6db%3A774e&itag=299&keepalive=yes&lmt=1580005649161486&lsig=AG3C_xAwRgIhAOiL-qJ04sA8FSOkEJfOYl3gFe4SzwYu_rAf3DMLHYigAiEA0Upi1HqqIu7NH_LTDL0jT1R5TTozQypL5FiSP9RoqtU%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&mh=mQ&mime=video%2Fmp4&mm=31%2C29&mn=sn-h0jeenek%2Csn-h0jelnez&ms=au%2Crdu&mt=1659459429&mv=m&mvi=5&otfp=1&pl=37&rbqsm=fr&requiressl=yes&sig=AOq0QJ8wRgIhAPjxbuzkozPDc1Nd_0q5X8x8H2SiDvAUFuqqMadtz3SNAiEA_3kXCeePb2kci-WB2779tzI56E6E0iKwoHnUSkKCzwU%3D&source=youtube&sparams=expire%2Cei%2Cip%2Cid%2Citag%2Csource%2Crequiressl%2Cvprv%2Cmime%2Cgir%2Cclen%2Cotfp%2Cdur%2Clmt&txp=2211222&vprv=1",
|
||||
|
@ -294,7 +283,6 @@ VideoPlayer(
|
|||
mime: "video/mp4; codecs=\"avc1.64002a\"",
|
||||
format: mp4,
|
||||
codec: avc1,
|
||||
throttled: false,
|
||||
),
|
||||
VideoStream(
|
||||
url: "https://rr5---sn-h0jeenek.googlevideo.com/videoplayback?c=ANDROID&clen=42567727&dur=163.046&ei=q1jpYtOPEYSBgQeHmqbwAQ&expire=1659481355&fexp=24001373%2C24007246&fvip=4&gir=yes&id=o-AEDMTCojVtwpIKOdhBaxEHE5s322qnAJHGqa2r1F46BM&initcwndbps=1527500&ip=2003%3Ade%3Aaf0e%3A2f00%3Ade47%3A297%3Aa6db%3A774e&itag=399&keepalive=yes&lmt=1608052932785283&lsig=AG3C_xAwRgIhAOiL-qJ04sA8FSOkEJfOYl3gFe4SzwYu_rAf3DMLHYigAiEA0Upi1HqqIu7NH_LTDL0jT1R5TTozQypL5FiSP9RoqtU%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&mh=mQ&mime=video%2Fmp4&mm=31%2C29&mn=sn-h0jeenek%2Csn-h0jelnez&ms=au%2Crdu&mt=1659459429&mv=m&mvi=5&otfp=1&pl=37&rbqsm=fr&requiressl=yes&sig=AOq0QJ8wRAIgFguw-cmBNOQegpyRRzcCScp2WaSnq_o7FB1-AiBgFpICIAGlMj9-kzNCWb3nhpg98Mc239ls6YYyoL8z1QpM8VmL&source=youtube&sparams=expire%2Cei%2Cip%2Cid%2Citag%2Csource%2Crequiressl%2Cvprv%2Cmime%2Cgir%2Cclen%2Cotfp%2Cdur%2Clmt&txp=1311222&vprv=1",
|
||||
|
@ -319,7 +307,6 @@ VideoPlayer(
|
|||
mime: "video/mp4; codecs=\"av01.0.09M.08\"",
|
||||
format: mp4,
|
||||
codec: av01,
|
||||
throttled: false,
|
||||
),
|
||||
],
|
||||
audio_streams: [
|
||||
|
@ -343,7 +330,6 @@ VideoPlayer(
|
|||
codec: mp4a,
|
||||
channels: Some(2),
|
||||
loudness_db: None,
|
||||
throttled: false,
|
||||
track: None,
|
||||
),
|
||||
AudioStream(
|
||||
|
@ -366,7 +352,6 @@ VideoPlayer(
|
|||
codec: opus,
|
||||
channels: Some(2),
|
||||
loudness_db: None,
|
||||
throttled: false,
|
||||
track: None,
|
||||
),
|
||||
AudioStream(
|
||||
|
@ -389,7 +374,6 @@ VideoPlayer(
|
|||
codec: opus,
|
||||
channels: Some(2),
|
||||
loudness_db: None,
|
||||
throttled: false,
|
||||
track: None,
|
||||
),
|
||||
AudioStream(
|
||||
|
@ -412,7 +396,6 @@ VideoPlayer(
|
|||
codec: mp4a,
|
||||
channels: Some(2),
|
||||
loudness_db: None,
|
||||
throttled: false,
|
||||
track: None,
|
||||
),
|
||||
AudioStream(
|
||||
|
@ -435,7 +418,6 @@ VideoPlayer(
|
|||
codec: opus,
|
||||
channels: Some(2),
|
||||
loudness_db: None,
|
||||
throttled: false,
|
||||
track: None,
|
||||
),
|
||||
],
|
||||
|
@ -482,5 +464,6 @@ VideoPlayer(
|
|||
frames_per_page_y: 5,
|
||||
),
|
||||
],
|
||||
client_type: android,
|
||||
visitor_data: Some("Cgt2aHFtQU5YZFBvYyirsaWXBg%3D%3D"),
|
||||
)
|
||||
|
|
|
@ -5,7 +5,7 @@ expression: map_res.c
|
|||
VideoPlayer(
|
||||
details: VideoPlayerDetails(
|
||||
id: "pPvd8UxmSbQ",
|
||||
name: "Inspiring Cinematic Uplifting (Creative Commons)",
|
||||
name: Some("Inspiring Cinematic Uplifting (Creative Commons)"),
|
||||
description: Some("► Download Music: http://bit.ly/2QLufeh\nImportant to know! You can download this track for free through Patreon. You will pay only for new tracks! So join others and let\'s make next track together!\n\n► MORE MUSIC: Become my patron and get access to all our music from Patreon library. More Info here: http://bit.ly/2JJDFHb\n\n► Additional edit versions of this track you can download here: http://bit.ly/2WdRinT (5 versions)\n--------------------- \n\n►DESCRIPTION:\nInspiring Cinematic Uplifting Trailer Background - epic music for trailer video project with powerful drums, energetic orchestra and gentle piano melody. This motivational cinematic theme will work as perfect background for beautiful epic moments, landscapes, nature, drone video, motivational products and achievements.\n--------------------- \n\n► LICENSE:\n● If you need a license for your project, you can purchase it here: \nhttps://1.envato.market/ajicu (Audiojungle)\nhttps://bit.ly/3fWZZuI (Pond5)\n--------------------- \n\n► LISTEN ON:\n● Spotify - https://spoti.fi/2sHm3UH\n● Apple Music - https://apple.co/3qBjbUO\n--------------------- \n\n► SUBSCRIBE FOR MORE: \nPatreon: http://bit.ly/2JJDFHb\nYoutube: http://bit.ly/2AYBzfA\nFacebook: http://bit.ly/2T6dTx5\nInstagram: http://bit.ly/2BHJ8rB\nTwitter: http://bit.ly/2MwtOlT\nSoundCloud: http://bit.ly/2IwVVmt\nAudiojungle: https://1.envato.market/ajrsm\nPond5: https://bit.ly/2TLi1rW\n--------------------- \n►Photo by Vittorio Staffolani from Pexels\n--------------------- \n\nFAQ:\n\n► Can I use this music in my videos? \n● Sure! Just download this track and you are ready to use it! We only ask to credit us. \n-------------------- \n\n► What is \"Creative Commons\"? \nCreative Commons is a system that allows you to legally use “some rights reserved” music, movies, images, and other content — all for free. Licensees may copy, distribute, display and perform the work and make derivative works and remixes based on it only if they give the author or licensor the credits.\n-------------------- \n\n► Will I have any copyright issues with this track?\n● No, you should not have any copyright problems with this track!\n-------------------- \n\n► Is it necessary to become your patron?\n● No it\'s not necessary. But we recommend you to become our patron because you will get access to huge library of music. You will download only highest quality files. You will find additional edited versions of every track. You always be tuned with our news. You will find music not only from Roman Senyk but also from another talented authors.\n-------------------- \n\n► Why I received a copyright claim when I used this track?\n● Do not panic! This is very common situation. Content ID fingerprint system can mismatch our music. Just dispute the claim by showing our original track. Or send us the link to your video (romansenykmusic@gmail.com) and attach some screenshot with claim information. Claim will be released until 24 hours!\n\n► How to credit you in my video?\n● Just add to the description of your project information about Author, Name of Song and the link to our original track. Or copy and paste:\n\nMusic Info: Inspiring Cinematic Uplifting by RomanSenykMusic.\nMusic Link: https://youtu.be/pPvd8UxmSbQ\n--------------------- \n\n► If you have any questions, you can write in the comments for this video or by email: romansenykmusic@gmail.com\n--------------------- \n\nStay tuned! The best is yet to come! \nThanks For Listening!\nRoman Senyk"),
|
||||
duration: 163,
|
||||
thumbnail: [
|
||||
|
@ -35,11 +35,9 @@ VideoPlayer(
|
|||
height: 1080,
|
||||
),
|
||||
],
|
||||
channel: ChannelId(
|
||||
id: "UCbxxEi-ImPlbLx5F-fHetEg",
|
||||
name: "RomanSenykMusic - Royalty Free Music",
|
||||
),
|
||||
view_count: 426567,
|
||||
channel_id: "UCbxxEi-ImPlbLx5F-fHetEg",
|
||||
channel_name: Some("RomanSenykMusic - Royalty Free Music"),
|
||||
view_count: Some(426567),
|
||||
keywords: [
|
||||
"no copyright music",
|
||||
"background music",
|
||||
|
@ -84,7 +82,6 @@ VideoPlayer(
|
|||
mime: "video/mp4; codecs=\"avc1.42001E, mp4a.40.2\"",
|
||||
format: mp4,
|
||||
codec: avc1,
|
||||
throttled: false,
|
||||
),
|
||||
],
|
||||
video_only_streams: [
|
||||
|
@ -111,7 +108,6 @@ VideoPlayer(
|
|||
mime: "video/webm; codecs=\"vp9\"",
|
||||
format: webm,
|
||||
codec: vp9,
|
||||
throttled: false,
|
||||
),
|
||||
VideoStream(
|
||||
url: "https://rr4---sn-h0jelnez.googlevideo.com/videoplayback?aitags=133%2C134%2C135%2C136%2C160%2C242%2C243%2C244%2C247%2C278%2C298%2C299%2C302%2C303%2C394%2C395%2C396%2C397%2C398%2C399&c=WEB&clen=1224002&dur=163.029&ei=q1jpYtq3BJCX1gKVyJGQDg&expire=1659481355&fexp=24001373%2C24007246&fvip=4&gir=yes&id=o-AGfBIFoT5D_NZAwXN7lVCS2VYLDMMegfaJQqvSJp-Hhy&initcwndbps=1513750&ip=2003%3Ade%3Aaf0e%3A2f00%3Ade47%3A297%3Aa6db%3A774e&itag=394&keepalive=yes&lmt=1608045375671513&lsig=AG3C_xAwRgIhANxHzq0WC6OvdTpPJp52z3eGAm-jzUX7fcKiWlJ0T9kEAiEA02Bjesi_an2-pUh0kHdKQe0s_7micbcv3JKiBlxsYGs%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&mh=mQ&mime=video%2Fmp4&mm=31%2C26&mn=sn-h0jelnez%2Csn-4g5edn6k&ms=au%2Conr&mt=1659459429&mv=m&mvi=4&n=T16m7p0RvV7UhQ&ns=tWuNfisHu8yiCA6Avm7nUlwH&otfp=1&pl=37&rbqsm=fr&requiressl=yes&sig=AOq0QJ8wRQIhAI-uoNLUkMHpH35niVh1tBvwwFLtmSbeHyknmyCvccFVAiB2XriyJd0u2q-tGIRTx5qtKt6bJCs5ndXtMsdSxOheuA%3D%3D&source=youtube&sparams=expire%2Cei%2Cip%2Cid%2Caitags%2Csource%2Crequiressl%2Cspc%2Cvprv%2Cmime%2Cns%2Cgir%2Cclen%2Cotfp%2Cdur%2Clmt&spc=lT-KhrZGE2opztWyVdAtyUNlb8dXPDs&txp=1311222&vprv=1",
|
||||
|
@ -136,7 +132,6 @@ VideoPlayer(
|
|||
mime: "video/mp4; codecs=\"av01.0.00M.08\"",
|
||||
format: mp4,
|
||||
codec: av01,
|
||||
throttled: false,
|
||||
),
|
||||
VideoStream(
|
||||
url: "https://rr4---sn-h0jelnez.googlevideo.com/videoplayback?aitags=133%2C134%2C135%2C136%2C160%2C242%2C243%2C244%2C247%2C278%2C298%2C299%2C302%2C303%2C394%2C395%2C396%2C397%2C398%2C399&c=WEB&clen=2973283&dur=163.029&ei=q1jpYtq3BJCX1gKVyJGQDg&expire=1659481355&fexp=24001373%2C24007246&fvip=4&gir=yes&id=o-AGfBIFoT5D_NZAwXN7lVCS2VYLDMMegfaJQqvSJp-Hhy&initcwndbps=1513750&ip=2003%3Ade%3Aaf0e%3A2f00%3Ade47%3A297%3Aa6db%3A774e&itag=242&keepalive=yes&lmt=1608509388282028&lsig=AG3C_xAwRgIhANxHzq0WC6OvdTpPJp52z3eGAm-jzUX7fcKiWlJ0T9kEAiEA02Bjesi_an2-pUh0kHdKQe0s_7micbcv3JKiBlxsYGs%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&mh=mQ&mime=video%2Fwebm&mm=31%2C26&mn=sn-h0jelnez%2Csn-4g5edn6k&ms=au%2Conr&mt=1659459429&mv=m&mvi=4&n=T16m7p0RvV7UhQ&ns=tWuNfisHu8yiCA6Avm7nUlwH&otfp=1&pl=37&rbqsm=fr&requiressl=yes&sig=AOq0QJ8wRQIgEleuqkeo7x7BsHur5aGPfHaT6KjKEG4c1d_xXwqlrsYCIQD85X_m050XwWyYlfLiWtZz-TX--H8H0UvfZCWKpY7m4Q%3D%3D&source=youtube&sparams=expire%2Cei%2Cip%2Cid%2Caitags%2Csource%2Crequiressl%2Cspc%2Cvprv%2Cmime%2Cns%2Cgir%2Cclen%2Cotfp%2Cdur%2Clmt&spc=lT-KhrZGE2opztWyVdAtyUNlb8dXPDs&txp=1311222&vprv=1",
|
||||
|
@ -161,7 +156,6 @@ VideoPlayer(
|
|||
mime: "video/webm; codecs=\"vp9\"",
|
||||
format: webm,
|
||||
codec: vp9,
|
||||
throttled: false,
|
||||
),
|
||||
VideoStream(
|
||||
url: "https://rr4---sn-h0jelnez.googlevideo.com/videoplayback?aitags=133%2C134%2C135%2C136%2C160%2C242%2C243%2C244%2C247%2C278%2C298%2C299%2C302%2C303%2C394%2C395%2C396%2C397%2C398%2C399&c=WEB&clen=2238952&dur=163.029&ei=q1jpYtq3BJCX1gKVyJGQDg&expire=1659481355&fexp=24001373%2C24007246&fvip=4&gir=yes&id=o-AGfBIFoT5D_NZAwXN7lVCS2VYLDMMegfaJQqvSJp-Hhy&initcwndbps=1513750&ip=2003%3Ade%3Aaf0e%3A2f00%3Ade47%3A297%3Aa6db%3A774e&itag=395&keepalive=yes&lmt=1608045728968690&lsig=AG3C_xAwRgIhANxHzq0WC6OvdTpPJp52z3eGAm-jzUX7fcKiWlJ0T9kEAiEA02Bjesi_an2-pUh0kHdKQe0s_7micbcv3JKiBlxsYGs%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&mh=mQ&mime=video%2Fmp4&mm=31%2C26&mn=sn-h0jelnez%2Csn-4g5edn6k&ms=au%2Conr&mt=1659459429&mv=m&mvi=4&n=T16m7p0RvV7UhQ&ns=tWuNfisHu8yiCA6Avm7nUlwH&otfp=1&pl=37&rbqsm=fr&requiressl=yes&sig=AOq0QJ8wRQIhAIBttTR02kTdGb4vdxQ9Gro88JOAY7u5z69nJbdmVS1sAiBr61rqkUtra4PHLdnp2w-s8ZSaN_4qZ3OEeeuIr5C13w%3D%3D&source=youtube&sparams=expire%2Cei%2Cip%2Cid%2Caitags%2Csource%2Crequiressl%2Cspc%2Cvprv%2Cmime%2Cns%2Cgir%2Cclen%2Cotfp%2Cdur%2Clmt&spc=lT-KhrZGE2opztWyVdAtyUNlb8dXPDs&txp=1311222&vprv=1",
|
||||
|
@ -186,7 +180,6 @@ VideoPlayer(
|
|||
mime: "video/mp4; codecs=\"av01.0.00M.08\"",
|
||||
format: mp4,
|
||||
codec: av01,
|
||||
throttled: false,
|
||||
),
|
||||
VideoStream(
|
||||
url: "https://rr4---sn-h0jelnez.googlevideo.com/videoplayback?aitags=133%2C134%2C135%2C136%2C160%2C242%2C243%2C244%2C247%2C278%2C298%2C299%2C302%2C303%2C394%2C395%2C396%2C397%2C398%2C399&c=WEB&clen=7808990&dur=163.029&ei=q1jpYtq3BJCX1gKVyJGQDg&expire=1659481355&fexp=24001373%2C24007246&fvip=4&gir=yes&id=o-AGfBIFoT5D_NZAwXN7lVCS2VYLDMMegfaJQqvSJp-Hhy&initcwndbps=1513750&ip=2003%3Ade%3Aaf0e%3A2f00%3Ade47%3A297%3Aa6db%3A774e&itag=134&keepalive=yes&lmt=1580005649163759&lsig=AG3C_xAwRgIhANxHzq0WC6OvdTpPJp52z3eGAm-jzUX7fcKiWlJ0T9kEAiEA02Bjesi_an2-pUh0kHdKQe0s_7micbcv3JKiBlxsYGs%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&mh=mQ&mime=video%2Fmp4&mm=31%2C26&mn=sn-h0jelnez%2Csn-4g5edn6k&ms=au%2Conr&mt=1659459429&mv=m&mvi=4&n=T16m7p0RvV7UhQ&ns=tWuNfisHu8yiCA6Avm7nUlwH&otfp=1&pl=37&rbqsm=fr&requiressl=yes&sig=AOq0QJ8wRgIhAMBRhMAZ5GXFSZHN6D-XhXRdG_EWSNwnN2eLPlwVNQ6PAiEA75eH0iJLgwRkujaABZnaJxG2ni-4irYHEGD42x6uaQg%3D&source=youtube&sparams=expire%2Cei%2Cip%2Cid%2Caitags%2Csource%2Crequiressl%2Cspc%2Cvprv%2Cmime%2Cns%2Cgir%2Cclen%2Cotfp%2Cdur%2Clmt&spc=lT-KhrZGE2opztWyVdAtyUNlb8dXPDs&txp=2211222&vprv=1",
|
||||
|
@ -211,7 +204,6 @@ VideoPlayer(
|
|||
mime: "video/mp4; codecs=\"avc1.4d401e\"",
|
||||
format: mp4,
|
||||
codec: avc1,
|
||||
throttled: false,
|
||||
),
|
||||
VideoStream(
|
||||
url: "https://rr4---sn-h0jelnez.googlevideo.com/videoplayback?aitags=133%2C134%2C135%2C136%2C160%2C242%2C243%2C244%2C247%2C278%2C298%2C299%2C302%2C303%2C394%2C395%2C396%2C397%2C398%2C399&c=WEB&clen=5169510&dur=163.029&ei=q1jpYtq3BJCX1gKVyJGQDg&expire=1659481355&fexp=24001373%2C24007246&fvip=4&gir=yes&id=o-AGfBIFoT5D_NZAwXN7lVCS2VYLDMMegfaJQqvSJp-Hhy&initcwndbps=1513750&ip=2003%3Ade%3Aaf0e%3A2f00%3Ade47%3A297%3Aa6db%3A774e&itag=243&keepalive=yes&lmt=1608509388282405&lsig=AG3C_xAwRgIhANxHzq0WC6OvdTpPJp52z3eGAm-jzUX7fcKiWlJ0T9kEAiEA02Bjesi_an2-pUh0kHdKQe0s_7micbcv3JKiBlxsYGs%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&mh=mQ&mime=video%2Fwebm&mm=31%2C26&mn=sn-h0jelnez%2Csn-4g5edn6k&ms=au%2Conr&mt=1659459429&mv=m&mvi=4&n=T16m7p0RvV7UhQ&ns=tWuNfisHu8yiCA6Avm7nUlwH&otfp=1&pl=37&rbqsm=fr&requiressl=yes&sig=AOq0QJ8wRAIgNi0fwQbep6oKsEeEGfms2Ay4x2OL2G0hUX5GFhycgKkCIANiC-j-Gz3-noxsNeSKKPxy--T9mFBu_8V7Vi5-zDYS&source=youtube&sparams=expire%2Cei%2Cip%2Cid%2Caitags%2Csource%2Crequiressl%2Cspc%2Cvprv%2Cmime%2Cns%2Cgir%2Cclen%2Cotfp%2Cdur%2Clmt&spc=lT-KhrZGE2opztWyVdAtyUNlb8dXPDs&txp=1311222&vprv=1",
|
||||
|
@ -236,7 +228,6 @@ VideoPlayer(
|
|||
mime: "video/webm; codecs=\"vp9\"",
|
||||
format: webm,
|
||||
codec: vp9,
|
||||
throttled: false,
|
||||
),
|
||||
VideoStream(
|
||||
url: "https://rr4---sn-h0jelnez.googlevideo.com/videoplayback?aitags=133%2C134%2C135%2C136%2C160%2C242%2C243%2C244%2C247%2C278%2C298%2C299%2C302%2C303%2C394%2C395%2C396%2C397%2C398%2C399&c=WEB&clen=4130385&dur=163.029&ei=q1jpYtq3BJCX1gKVyJGQDg&expire=1659481355&fexp=24001373%2C24007246&fvip=4&gir=yes&id=o-AGfBIFoT5D_NZAwXN7lVCS2VYLDMMegfaJQqvSJp-Hhy&initcwndbps=1513750&ip=2003%3Ade%3Aaf0e%3A2f00%3Ade47%3A297%3Aa6db%3A774e&itag=396&keepalive=yes&lmt=1608045761576250&lsig=AG3C_xAwRgIhANxHzq0WC6OvdTpPJp52z3eGAm-jzUX7fcKiWlJ0T9kEAiEA02Bjesi_an2-pUh0kHdKQe0s_7micbcv3JKiBlxsYGs%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&mh=mQ&mime=video%2Fmp4&mm=31%2C26&mn=sn-h0jelnez%2Csn-4g5edn6k&ms=au%2Conr&mt=1659459429&mv=m&mvi=4&n=T16m7p0RvV7UhQ&ns=tWuNfisHu8yiCA6Avm7nUlwH&otfp=1&pl=37&rbqsm=fr&requiressl=yes&sig=AOq0QJ8wRAIgFuBoOIkqwq0D1_OmnNJx3C0jmhHUyskpzPrTMoaWRYECIFZ1Y4QbQ41GsWS8yRHox8l_nGVosfXhXfKu3v18AyeT&source=youtube&sparams=expire%2Cei%2Cip%2Cid%2Caitags%2Csource%2Crequiressl%2Cspc%2Cvprv%2Cmime%2Cns%2Cgir%2Cclen%2Cotfp%2Cdur%2Clmt&spc=lT-KhrZGE2opztWyVdAtyUNlb8dXPDs&txp=1311222&vprv=1",
|
||||
|
@ -261,7 +252,6 @@ VideoPlayer(
|
|||
mime: "video/mp4; codecs=\"av01.0.01M.08\"",
|
||||
format: mp4,
|
||||
codec: av01,
|
||||
throttled: false,
|
||||
),
|
||||
VideoStream(
|
||||
url: "https://rr4---sn-h0jelnez.googlevideo.com/videoplayback?aitags=133%2C134%2C135%2C136%2C160%2C242%2C243%2C244%2C247%2C278%2C298%2C299%2C302%2C303%2C394%2C395%2C396%2C397%2C398%2C399&c=WEB&clen=8890590&dur=163.029&ei=q1jpYtq3BJCX1gKVyJGQDg&expire=1659481355&fexp=24001373%2C24007246&fvip=4&gir=yes&id=o-AGfBIFoT5D_NZAwXN7lVCS2VYLDMMegfaJQqvSJp-Hhy&initcwndbps=1513750&ip=2003%3Ade%3Aaf0e%3A2f00%3Ade47%3A297%3Aa6db%3A774e&itag=244&keepalive=yes&lmt=1608509388284632&lsig=AG3C_xAwRgIhANxHzq0WC6OvdTpPJp52z3eGAm-jzUX7fcKiWlJ0T9kEAiEA02Bjesi_an2-pUh0kHdKQe0s_7micbcv3JKiBlxsYGs%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&mh=mQ&mime=video%2Fwebm&mm=31%2C26&mn=sn-h0jelnez%2Csn-4g5edn6k&ms=au%2Conr&mt=1659459429&mv=m&mvi=4&n=T16m7p0RvV7UhQ&ns=tWuNfisHu8yiCA6Avm7nUlwH&otfp=1&pl=37&rbqsm=fr&requiressl=yes&sig=AOq0QJ8wRAIgMYF0KQQNgYI8oOhgdCwyRY6E_hvFnJiaAadyMf89MRoCIHnDnROTvUoy0iIBM3MzFAxJh_bLA-2vFl9KFDrHOf1B&source=youtube&sparams=expire%2Cei%2Cip%2Cid%2Caitags%2Csource%2Crequiressl%2Cspc%2Cvprv%2Cmime%2Cns%2Cgir%2Cclen%2Cotfp%2Cdur%2Clmt&spc=lT-KhrZGE2opztWyVdAtyUNlb8dXPDs&txp=1311222&vprv=1",
|
||||
|
@ -286,7 +276,6 @@ VideoPlayer(
|
|||
mime: "video/webm; codecs=\"vp9\"",
|
||||
format: webm,
|
||||
codec: vp9,
|
||||
throttled: false,
|
||||
),
|
||||
VideoStream(
|
||||
url: "https://rr4---sn-h0jelnez.googlevideo.com/videoplayback?aitags=133%2C134%2C135%2C136%2C160%2C242%2C243%2C244%2C247%2C278%2C298%2C299%2C302%2C303%2C394%2C395%2C396%2C397%2C398%2C399&c=WEB&clen=6873325&dur=163.029&ei=q1jpYtq3BJCX1gKVyJGQDg&expire=1659481355&fexp=24001373%2C24007246&fvip=4&gir=yes&id=o-AGfBIFoT5D_NZAwXN7lVCS2VYLDMMegfaJQqvSJp-Hhy&initcwndbps=1513750&ip=2003%3Ade%3Aaf0e%3A2f00%3Ade47%3A297%3Aa6db%3A774e&itag=397&keepalive=yes&lmt=1608045990917419&lsig=AG3C_xAwRgIhANxHzq0WC6OvdTpPJp52z3eGAm-jzUX7fcKiWlJ0T9kEAiEA02Bjesi_an2-pUh0kHdKQe0s_7micbcv3JKiBlxsYGs%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&mh=mQ&mime=video%2Fmp4&mm=31%2C26&mn=sn-h0jelnez%2Csn-4g5edn6k&ms=au%2Conr&mt=1659459429&mv=m&mvi=4&n=T16m7p0RvV7UhQ&ns=tWuNfisHu8yiCA6Avm7nUlwH&otfp=1&pl=37&rbqsm=fr&requiressl=yes&sig=AOq0QJ8wRgIhAOtLGFoFtLHIXzNRoSrR7ULbIz91OYmaVQkcSatqNKAiAiEA23ZF7h2BZZCAGc0Zdd2p3PWRotmwLDyH6yYCuQpE8xw%3D&source=youtube&sparams=expire%2Cei%2Cip%2Cid%2Caitags%2Csource%2Crequiressl%2Cspc%2Cvprv%2Cmime%2Cns%2Cgir%2Cclen%2Cotfp%2Cdur%2Clmt&spc=lT-KhrZGE2opztWyVdAtyUNlb8dXPDs&txp=1311222&vprv=1",
|
||||
|
@ -311,7 +300,6 @@ VideoPlayer(
|
|||
mime: "video/mp4; codecs=\"av01.0.04M.08\"",
|
||||
format: mp4,
|
||||
codec: av01,
|
||||
throttled: false,
|
||||
),
|
||||
VideoStream(
|
||||
url: "https://rr4---sn-h0jelnez.googlevideo.com/videoplayback?aitags=133%2C134%2C135%2C136%2C160%2C242%2C243%2C244%2C247%2C278%2C298%2C299%2C302%2C303%2C394%2C395%2C396%2C397%2C398%2C399&c=WEB&clen=16547577&dur=163.029&ei=q1jpYtq3BJCX1gKVyJGQDg&expire=1659481355&fexp=24001373%2C24007246&fvip=4&gir=yes&id=o-AGfBIFoT5D_NZAwXN7lVCS2VYLDMMegfaJQqvSJp-Hhy&initcwndbps=1513750&ip=2003%3Ade%3Aaf0e%3A2f00%3Ade47%3A297%3Aa6db%3A774e&itag=247&keepalive=yes&lmt=1608509388326822&lsig=AG3C_xAwRgIhANxHzq0WC6OvdTpPJp52z3eGAm-jzUX7fcKiWlJ0T9kEAiEA02Bjesi_an2-pUh0kHdKQe0s_7micbcv3JKiBlxsYGs%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&mh=mQ&mime=video%2Fwebm&mm=31%2C26&mn=sn-h0jelnez%2Csn-4g5edn6k&ms=au%2Conr&mt=1659459429&mv=m&mvi=4&n=T16m7p0RvV7UhQ&ns=tWuNfisHu8yiCA6Avm7nUlwH&otfp=1&pl=37&rbqsm=fr&requiressl=yes&sig=AOq0QJ8wRQIgfYKbT_196P-2EtjuqcTKdataiM480y65Ko0a73dv7WECIQC6nqWienQvu7swC1OW9HlwFWRH7VwTwj6H4yjY6FYvzg%3D%3D&source=youtube&sparams=expire%2Cei%2Cip%2Cid%2Caitags%2Csource%2Crequiressl%2Cspc%2Cvprv%2Cmime%2Cns%2Cgir%2Cclen%2Cotfp%2Cdur%2Clmt&spc=lT-KhrZGE2opztWyVdAtyUNlb8dXPDs&txp=1311222&vprv=1",
|
||||
|
@ -336,7 +324,6 @@ VideoPlayer(
|
|||
mime: "video/webm; codecs=\"vp9\"",
|
||||
format: webm,
|
||||
codec: vp9,
|
||||
throttled: false,
|
||||
),
|
||||
VideoStream(
|
||||
url: "https://rr4---sn-h0jelnez.googlevideo.com/videoplayback?aitags=133%2C134%2C135%2C136%2C160%2C242%2C243%2C244%2C247%2C278%2C298%2C299%2C302%2C303%2C394%2C395%2C396%2C397%2C398%2C399&c=WEB&clen=35955780&dur=163.046&ei=q1jpYtq3BJCX1gKVyJGQDg&expire=1659481355&fexp=24001373%2C24007246&fvip=4&gir=yes&id=o-AGfBIFoT5D_NZAwXN7lVCS2VYLDMMegfaJQqvSJp-Hhy&initcwndbps=1513750&ip=2003%3Ade%3Aaf0e%3A2f00%3Ade47%3A297%3Aa6db%3A774e&itag=302&keepalive=yes&lmt=1608509234088626&lsig=AG3C_xAwRgIhANxHzq0WC6OvdTpPJp52z3eGAm-jzUX7fcKiWlJ0T9kEAiEA02Bjesi_an2-pUh0kHdKQe0s_7micbcv3JKiBlxsYGs%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&mh=mQ&mime=video%2Fwebm&mm=31%2C26&mn=sn-h0jelnez%2Csn-4g5edn6k&ms=au%2Conr&mt=1659459429&mv=m&mvi=4&n=T16m7p0RvV7UhQ&ns=tWuNfisHu8yiCA6Avm7nUlwH&otfp=1&pl=37&rbqsm=fr&requiressl=yes&sig=AOq0QJ8wRAIgQG8GPj3w_5_Lr2apagmte66IFBY3bYcZ2KnhwnUpshYCIFgvHYIZsz8WdYGSk9adpfMNKX0pzSP_l8cW47Gq2RTi&source=youtube&sparams=expire%2Cei%2Cip%2Cid%2Caitags%2Csource%2Crequiressl%2Cspc%2Cvprv%2Cmime%2Cns%2Cgir%2Cclen%2Cotfp%2Cdur%2Clmt&spc=lT-KhrZGE2opztWyVdAtyUNlb8dXPDs&txp=1311222&vprv=1",
|
||||
|
@ -361,7 +348,6 @@ VideoPlayer(
|
|||
mime: "video/webm; codecs=\"vp9\"",
|
||||
format: webm,
|
||||
codec: vp9,
|
||||
throttled: false,
|
||||
),
|
||||
VideoStream(
|
||||
url: "https://rr4---sn-h0jelnez.googlevideo.com/videoplayback?aitags=133%2C134%2C135%2C136%2C160%2C242%2C243%2C244%2C247%2C278%2C298%2C299%2C302%2C303%2C394%2C395%2C396%2C397%2C398%2C399&c=WEB&clen=22365208&dur=163.046&ei=q1jpYtq3BJCX1gKVyJGQDg&expire=1659481355&fexp=24001373%2C24007246&fvip=4&gir=yes&id=o-AGfBIFoT5D_NZAwXN7lVCS2VYLDMMegfaJQqvSJp-Hhy&initcwndbps=1513750&ip=2003%3Ade%3Aaf0e%3A2f00%3Ade47%3A297%3Aa6db%3A774e&itag=398&keepalive=yes&lmt=1608048380553749&lsig=AG3C_xAwRgIhANxHzq0WC6OvdTpPJp52z3eGAm-jzUX7fcKiWlJ0T9kEAiEA02Bjesi_an2-pUh0kHdKQe0s_7micbcv3JKiBlxsYGs%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&mh=mQ&mime=video%2Fmp4&mm=31%2C26&mn=sn-h0jelnez%2Csn-4g5edn6k&ms=au%2Conr&mt=1659459429&mv=m&mvi=4&n=T16m7p0RvV7UhQ&ns=tWuNfisHu8yiCA6Avm7nUlwH&otfp=1&pl=37&rbqsm=fr&requiressl=yes&sig=AOq0QJ8wRgIhAI-VhcBU6o8LGmeuVYC2_zbxeGvC6XWf7yIOQ1RvjURhAiEA0YcZlVOI2ZUtKl-31__Hzax2SOUPeekCRjqjfw4m15s%3D&source=youtube&sparams=expire%2Cei%2Cip%2Cid%2Caitags%2Csource%2Crequiressl%2Cspc%2Cvprv%2Cmime%2Cns%2Cgir%2Cclen%2Cotfp%2Cdur%2Clmt&spc=lT-KhrZGE2opztWyVdAtyUNlb8dXPDs&txp=1311222&vprv=1",
|
||||
|
@ -386,7 +372,6 @@ VideoPlayer(
|
|||
mime: "video/mp4; codecs=\"av01.0.08M.08\"",
|
||||
format: mp4,
|
||||
codec: av01,
|
||||
throttled: false,
|
||||
),
|
||||
VideoStream(
|
||||
url: "https://rr4---sn-h0jelnez.googlevideo.com/videoplayback?aitags=133%2C134%2C135%2C136%2C160%2C242%2C243%2C244%2C247%2C278%2C298%2C299%2C302%2C303%2C394%2C395%2C396%2C397%2C398%2C399&c=WEB&clen=65400181&dur=163.046&ei=q1jpYtq3BJCX1gKVyJGQDg&expire=1659481355&fexp=24001373%2C24007246&fvip=4&gir=yes&id=o-AGfBIFoT5D_NZAwXN7lVCS2VYLDMMegfaJQqvSJp-Hhy&initcwndbps=1513750&ip=2003%3Ade%3Aaf0e%3A2f00%3Ade47%3A297%3Aa6db%3A774e&itag=299&keepalive=yes&lmt=1580005649161486&lsig=AG3C_xAwRgIhANxHzq0WC6OvdTpPJp52z3eGAm-jzUX7fcKiWlJ0T9kEAiEA02Bjesi_an2-pUh0kHdKQe0s_7micbcv3JKiBlxsYGs%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&mh=mQ&mime=video%2Fmp4&mm=31%2C26&mn=sn-h0jelnez%2Csn-4g5edn6k&ms=au%2Conr&mt=1659459429&mv=m&mvi=4&n=T16m7p0RvV7UhQ&ns=tWuNfisHu8yiCA6Avm7nUlwH&otfp=1&pl=37&rbqsm=fr&requiressl=yes&sig=AOq0QJ8wRgIhAIdbG-deTvLhp7mD2b-QZYQamPFv75l1bNBEEOMihrxPAiEA1NYvRlFphbRRvFIBCP-Ij9-5q8OTwUskgsL6LyIrD7c%3D&source=youtube&sparams=expire%2Cei%2Cip%2Cid%2Caitags%2Csource%2Crequiressl%2Cspc%2Cvprv%2Cmime%2Cns%2Cgir%2Cclen%2Cotfp%2Cdur%2Clmt&spc=lT-KhrZGE2opztWyVdAtyUNlb8dXPDs&txp=2211222&vprv=1",
|
||||
|
@ -411,7 +396,6 @@ VideoPlayer(
|
|||
mime: "video/mp4; codecs=\"avc1.64002a\"",
|
||||
format: mp4,
|
||||
codec: avc1,
|
||||
throttled: false,
|
||||
),
|
||||
VideoStream(
|
||||
url: "https://rr4---sn-h0jelnez.googlevideo.com/videoplayback?aitags=133%2C134%2C135%2C136%2C160%2C242%2C243%2C244%2C247%2C278%2C298%2C299%2C302%2C303%2C394%2C395%2C396%2C397%2C398%2C399&c=WEB&clen=62993617&dur=163.046&ei=q1jpYtq3BJCX1gKVyJGQDg&expire=1659481355&fexp=24001373%2C24007246&fvip=4&gir=yes&id=o-AGfBIFoT5D_NZAwXN7lVCS2VYLDMMegfaJQqvSJp-Hhy&initcwndbps=1513750&ip=2003%3Ade%3Aaf0e%3A2f00%3Ade47%3A297%3Aa6db%3A774e&itag=303&keepalive=yes&lmt=1608509371758331&lsig=AG3C_xAwRgIhANxHzq0WC6OvdTpPJp52z3eGAm-jzUX7fcKiWlJ0T9kEAiEA02Bjesi_an2-pUh0kHdKQe0s_7micbcv3JKiBlxsYGs%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&mh=mQ&mime=video%2Fwebm&mm=31%2C26&mn=sn-h0jelnez%2Csn-4g5edn6k&ms=au%2Conr&mt=1659459429&mv=m&mvi=4&n=T16m7p0RvV7UhQ&ns=tWuNfisHu8yiCA6Avm7nUlwH&otfp=1&pl=37&rbqsm=fr&requiressl=yes&sig=AOq0QJ8wRQIhAJ8n34LQhg6iEg1Ux9rDkk48e8l3vBR4WwuHeIpKnorlAiBopK4z-nq-pJTPTmrdbbKPW1Lfufdz2f9sGUKY-dzk5A%3D%3D&source=youtube&sparams=expire%2Cei%2Cip%2Cid%2Caitags%2Csource%2Crequiressl%2Cspc%2Cvprv%2Cmime%2Cns%2Cgir%2Cclen%2Cotfp%2Cdur%2Clmt&spc=lT-KhrZGE2opztWyVdAtyUNlb8dXPDs&txp=1311222&vprv=1",
|
||||
|
@ -436,7 +420,6 @@ VideoPlayer(
|
|||
mime: "video/webm; codecs=\"vp9\"",
|
||||
format: webm,
|
||||
codec: vp9,
|
||||
throttled: false,
|
||||
),
|
||||
VideoStream(
|
||||
url: "https://rr4---sn-h0jelnez.googlevideo.com/videoplayback?aitags=133%2C134%2C135%2C136%2C160%2C242%2C243%2C244%2C247%2C278%2C298%2C299%2C302%2C303%2C394%2C395%2C396%2C397%2C398%2C399&c=WEB&clen=42567727&dur=163.046&ei=q1jpYtq3BJCX1gKVyJGQDg&expire=1659481355&fexp=24001373%2C24007246&fvip=4&gir=yes&id=o-AGfBIFoT5D_NZAwXN7lVCS2VYLDMMegfaJQqvSJp-Hhy&initcwndbps=1513750&ip=2003%3Ade%3Aaf0e%3A2f00%3Ade47%3A297%3Aa6db%3A774e&itag=399&keepalive=yes&lmt=1608052932785283&lsig=AG3C_xAwRgIhANxHzq0WC6OvdTpPJp52z3eGAm-jzUX7fcKiWlJ0T9kEAiEA02Bjesi_an2-pUh0kHdKQe0s_7micbcv3JKiBlxsYGs%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&mh=mQ&mime=video%2Fmp4&mm=31%2C26&mn=sn-h0jelnez%2Csn-4g5edn6k&ms=au%2Conr&mt=1659459429&mv=m&mvi=4&n=T16m7p0RvV7UhQ&ns=tWuNfisHu8yiCA6Avm7nUlwH&otfp=1&pl=37&rbqsm=fr&requiressl=yes&sig=AOq0QJ8wRQIhAMewAT3SgJRGn7wqDaDzNWcsAfrjFRu6k0wm7O_5YJeQAiANVhGmILp_gmNXnmixDesxsZ44_72YBT2SqjLLSZV32w%3D%3D&source=youtube&sparams=expire%2Cei%2Cip%2Cid%2Caitags%2Csource%2Crequiressl%2Cspc%2Cvprv%2Cmime%2Cns%2Cgir%2Cclen%2Cotfp%2Cdur%2Clmt&spc=lT-KhrZGE2opztWyVdAtyUNlb8dXPDs&txp=1311222&vprv=1",
|
||||
|
@ -461,7 +444,6 @@ VideoPlayer(
|
|||
mime: "video/mp4; codecs=\"av01.0.09M.08\"",
|
||||
format: mp4,
|
||||
codec: av01,
|
||||
throttled: false,
|
||||
),
|
||||
],
|
||||
audio_streams: [
|
||||
|
@ -485,7 +467,6 @@ VideoPlayer(
|
|||
codec: opus,
|
||||
channels: Some(2),
|
||||
loudness_db: Some(5.2200003),
|
||||
throttled: false,
|
||||
track: None,
|
||||
),
|
||||
AudioStream(
|
||||
|
@ -508,7 +489,6 @@ VideoPlayer(
|
|||
codec: opus,
|
||||
channels: Some(2),
|
||||
loudness_db: Some(5.2200003),
|
||||
throttled: false,
|
||||
track: None,
|
||||
),
|
||||
AudioStream(
|
||||
|
@ -531,7 +511,6 @@ VideoPlayer(
|
|||
codec: mp4a,
|
||||
channels: Some(2),
|
||||
loudness_db: Some(5.2159004),
|
||||
throttled: false,
|
||||
track: None,
|
||||
),
|
||||
AudioStream(
|
||||
|
@ -554,7 +533,6 @@ VideoPlayer(
|
|||
codec: opus,
|
||||
channels: Some(2),
|
||||
loudness_db: Some(5.2200003),
|
||||
throttled: false,
|
||||
track: None,
|
||||
),
|
||||
],
|
||||
|
@ -601,5 +579,6 @@ VideoPlayer(
|
|||
frames_per_page_y: 5,
|
||||
),
|
||||
],
|
||||
client_type: desktop,
|
||||
visitor_data: Some("CgtoS1pCMVJTNUJISSirsaWXBg%3D%3D"),
|
||||
)
|
||||
|
|
|
@ -5,7 +5,7 @@ expression: map_res.c
|
|||
VideoPlayer(
|
||||
details: VideoPlayerDetails(
|
||||
id: "pPvd8UxmSbQ",
|
||||
name: "Inspiring Cinematic Uplifting",
|
||||
name: Some("Inspiring Cinematic Uplifting"),
|
||||
description: None,
|
||||
duration: 163,
|
||||
thumbnail: [
|
||||
|
@ -25,11 +25,9 @@ VideoPlayer(
|
|||
height: 480,
|
||||
),
|
||||
],
|
||||
channel: ChannelId(
|
||||
id: "UCbxxEi-ImPlbLx5F-fHetEg",
|
||||
name: "Romansenykmusic",
|
||||
),
|
||||
view_count: 426583,
|
||||
channel_id: "UCbxxEi-ImPlbLx5F-fHetEg",
|
||||
channel_name: Some("Romansenykmusic"),
|
||||
view_count: Some(426583),
|
||||
keywords: [],
|
||||
is_live: false,
|
||||
is_live_content: false,
|
||||
|
@ -52,7 +50,6 @@ VideoPlayer(
|
|||
mime: "video/mp4; codecs=\"avc1.42001E, mp4a.40.2\"",
|
||||
format: mp4,
|
||||
codec: avc1,
|
||||
throttled: false,
|
||||
),
|
||||
],
|
||||
video_only_streams: [
|
||||
|
@ -79,7 +76,6 @@ VideoPlayer(
|
|||
mime: "video/webm; codecs=\"vp9\"",
|
||||
format: webm,
|
||||
codec: vp9,
|
||||
throttled: false,
|
||||
),
|
||||
VideoStream(
|
||||
url: "https://rr5---sn-h0jeenek.googlevideo.com/videoplayback?c=WEB_REMIX&clen=2973283&dur=163.029&ei=knDpYub6BojEgAf6jbLgDw&expire=1659487474&fexp=24001373%2C24007246&fvip=4&gir=yes&id=o-AM-wcJVO-yYYbVFnuifnzM4eRnD-AG1bS1AhLoDqi_is&initcwndbps=1418750&ip=2003%3Ade%3Aaf0e%3A2f00%3Ade47%3A297%3Aa6db%3A774e&itag=242&keepalive=yes&lmt=1608509388282028&lsig=AG3C_xAwRgIhAMwYJqxve8BSujC-oaSFBbq67p-rFi7saU5V8Yb3qrjLAiEAlrMKR_sadHrkFpy7o7lGzKOCmU1OQazCNBbXjDT2a-o%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&mh=mQ&mime=video%2Fwebm&mm=31%2C29&mn=sn-h0jeenek%2Csn-h0jelnez&ms=au%2Crdu&mt=1659465669&mv=m&mvi=5&n=1taQMNHGExb_Vg&ns=UTT8RXHZNhPYTw6NgkzWMWEH&otfp=1&pl=37&rbqsm=fr&requiressl=yes&sig=AOq0QJ8wRQIhAO7DI5E91yHpLhgiWg9C99NsMoJBVOWsNTNF3os9kREQAiAr2oC8vFtXIHwkJJt45q0sdmjiJdkTO2i8VAjUodk6Xw%3D%3D&source=youtube&sparams=expire%2Cei%2Cip%2Cid%2Citag%2Csource%2Crequiressl%2Cspc%2Cvprv%2Cmime%2Cns%2Cgir%2Cclen%2Cotfp%2Cdur%2Clmt&spc=lT-Khox4YuJQ2wmH79zYALRvsWTPCUc&txp=1311222&vprv=1",
|
||||
|
@ -104,7 +100,6 @@ VideoPlayer(
|
|||
mime: "video/webm; codecs=\"vp9\"",
|
||||
format: webm,
|
||||
codec: vp9,
|
||||
throttled: false,
|
||||
),
|
||||
VideoStream(
|
||||
url: "https://rr5---sn-h0jeenek.googlevideo.com/videoplayback?c=WEB_REMIX&clen=7808990&dur=163.029&ei=knDpYub6BojEgAf6jbLgDw&expire=1659487474&fexp=24001373%2C24007246&fvip=4&gir=yes&id=o-AM-wcJVO-yYYbVFnuifnzM4eRnD-AG1bS1AhLoDqi_is&initcwndbps=1418750&ip=2003%3Ade%3Aaf0e%3A2f00%3Ade47%3A297%3Aa6db%3A774e&itag=134&keepalive=yes&lmt=1580005649163759&lsig=AG3C_xAwRgIhAMwYJqxve8BSujC-oaSFBbq67p-rFi7saU5V8Yb3qrjLAiEAlrMKR_sadHrkFpy7o7lGzKOCmU1OQazCNBbXjDT2a-o%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&mh=mQ&mime=video%2Fmp4&mm=31%2C29&mn=sn-h0jeenek%2Csn-h0jelnez&ms=au%2Crdu&mt=1659465669&mv=m&mvi=5&n=1taQMNHGExb_Vg&ns=UTT8RXHZNhPYTw6NgkzWMWEH&otfp=1&pl=37&rbqsm=fr&requiressl=yes&sig=AOq0QJ8wRAIgTkOjFd0nExEtpr8sBIaNu9HhkxWNdjhSKufHMhLR8-8CIHJAmOuCD7VBv_krH6rn5zqXFqAfsq9rQPXlC3CcQrjM&source=youtube&sparams=expire%2Cei%2Cip%2Cid%2Citag%2Csource%2Crequiressl%2Cspc%2Cvprv%2Cmime%2Cns%2Cgir%2Cclen%2Cotfp%2Cdur%2Clmt&spc=lT-Khox4YuJQ2wmH79zYALRvsWTPCUc&txp=2211222&vprv=1",
|
||||
|
@ -129,7 +124,6 @@ VideoPlayer(
|
|||
mime: "video/mp4; codecs=\"avc1.4d401e\"",
|
||||
format: mp4,
|
||||
codec: avc1,
|
||||
throttled: false,
|
||||
),
|
||||
VideoStream(
|
||||
url: "https://rr5---sn-h0jeenek.googlevideo.com/videoplayback?c=WEB_REMIX&clen=5169510&dur=163.029&ei=knDpYub6BojEgAf6jbLgDw&expire=1659487474&fexp=24001373%2C24007246&fvip=4&gir=yes&id=o-AM-wcJVO-yYYbVFnuifnzM4eRnD-AG1bS1AhLoDqi_is&initcwndbps=1418750&ip=2003%3Ade%3Aaf0e%3A2f00%3Ade47%3A297%3Aa6db%3A774e&itag=243&keepalive=yes&lmt=1608509388282405&lsig=AG3C_xAwRgIhAMwYJqxve8BSujC-oaSFBbq67p-rFi7saU5V8Yb3qrjLAiEAlrMKR_sadHrkFpy7o7lGzKOCmU1OQazCNBbXjDT2a-o%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&mh=mQ&mime=video%2Fwebm&mm=31%2C29&mn=sn-h0jeenek%2Csn-h0jelnez&ms=au%2Crdu&mt=1659465669&mv=m&mvi=5&n=1taQMNHGExb_Vg&ns=UTT8RXHZNhPYTw6NgkzWMWEH&otfp=1&pl=37&rbqsm=fr&requiressl=yes&sig=AOq0QJ8wRQIhAPqQfxwIANgIC3DrQ6avaWOhCvIMLdzMPQtFOx2gwEXNAiAwJp2mgN9-zl4vPOB2uoQXOfmGsYDB470q1zg7wRW4Sw%3D%3D&source=youtube&sparams=expire%2Cei%2Cip%2Cid%2Citag%2Csource%2Crequiressl%2Cspc%2Cvprv%2Cmime%2Cns%2Cgir%2Cclen%2Cotfp%2Cdur%2Clmt&spc=lT-Khox4YuJQ2wmH79zYALRvsWTPCUc&txp=1311222&vprv=1",
|
||||
|
@ -154,7 +148,6 @@ VideoPlayer(
|
|||
mime: "video/webm; codecs=\"vp9\"",
|
||||
format: webm,
|
||||
codec: vp9,
|
||||
throttled: false,
|
||||
),
|
||||
VideoStream(
|
||||
url: "https://rr5---sn-h0jeenek.googlevideo.com/videoplayback?c=WEB_REMIX&clen=8890590&dur=163.029&ei=knDpYub6BojEgAf6jbLgDw&expire=1659487474&fexp=24001373%2C24007246&fvip=4&gir=yes&id=o-AM-wcJVO-yYYbVFnuifnzM4eRnD-AG1bS1AhLoDqi_is&initcwndbps=1418750&ip=2003%3Ade%3Aaf0e%3A2f00%3Ade47%3A297%3Aa6db%3A774e&itag=244&keepalive=yes&lmt=1608509388284632&lsig=AG3C_xAwRgIhAMwYJqxve8BSujC-oaSFBbq67p-rFi7saU5V8Yb3qrjLAiEAlrMKR_sadHrkFpy7o7lGzKOCmU1OQazCNBbXjDT2a-o%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&mh=mQ&mime=video%2Fwebm&mm=31%2C29&mn=sn-h0jeenek%2Csn-h0jelnez&ms=au%2Crdu&mt=1659465669&mv=m&mvi=5&n=1taQMNHGExb_Vg&ns=UTT8RXHZNhPYTw6NgkzWMWEH&otfp=1&pl=37&rbqsm=fr&requiressl=yes&sig=AOq0QJ8wRQIhAIjdvhcThMxoo_v2bzEjaR_w0ryWFQDs0f0INaI5WPcVAiApQZUYTqcQJdfxZlNSsp7cl3FK8XPfDZ-qbVvj9GuauQ%3D%3D&source=youtube&sparams=expire%2Cei%2Cip%2Cid%2Citag%2Csource%2Crequiressl%2Cspc%2Cvprv%2Cmime%2Cns%2Cgir%2Cclen%2Cotfp%2Cdur%2Clmt&spc=lT-Khox4YuJQ2wmH79zYALRvsWTPCUc&txp=1311222&vprv=1",
|
||||
|
@ -179,7 +172,6 @@ VideoPlayer(
|
|||
mime: "video/webm; codecs=\"vp9\"",
|
||||
format: webm,
|
||||
codec: vp9,
|
||||
throttled: false,
|
||||
),
|
||||
VideoStream(
|
||||
url: "https://rr5---sn-h0jeenek.googlevideo.com/videoplayback?c=WEB_REMIX&clen=16547577&dur=163.029&ei=knDpYub6BojEgAf6jbLgDw&expire=1659487474&fexp=24001373%2C24007246&fvip=4&gir=yes&id=o-AM-wcJVO-yYYbVFnuifnzM4eRnD-AG1bS1AhLoDqi_is&initcwndbps=1418750&ip=2003%3Ade%3Aaf0e%3A2f00%3Ade47%3A297%3Aa6db%3A774e&itag=247&keepalive=yes&lmt=1608509388326822&lsig=AG3C_xAwRgIhAMwYJqxve8BSujC-oaSFBbq67p-rFi7saU5V8Yb3qrjLAiEAlrMKR_sadHrkFpy7o7lGzKOCmU1OQazCNBbXjDT2a-o%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&mh=mQ&mime=video%2Fwebm&mm=31%2C29&mn=sn-h0jeenek%2Csn-h0jelnez&ms=au%2Crdu&mt=1659465669&mv=m&mvi=5&n=1taQMNHGExb_Vg&ns=UTT8RXHZNhPYTw6NgkzWMWEH&otfp=1&pl=37&rbqsm=fr&requiressl=yes&sig=AOq0QJ8wRAIgBV4Oa1IQ0YNDvRrKO5ec3Pfbg65MxzmIxCcm0gOuwT0CIFysQdow6DQXzz1W9KZVuqACTdjXQ3-yiBj9GcmNw3HE&source=youtube&sparams=expire%2Cei%2Cip%2Cid%2Citag%2Csource%2Crequiressl%2Cspc%2Cvprv%2Cmime%2Cns%2Cgir%2Cclen%2Cotfp%2Cdur%2Clmt&spc=lT-Khox4YuJQ2wmH79zYALRvsWTPCUc&txp=1311222&vprv=1",
|
||||
|
@ -204,7 +196,6 @@ VideoPlayer(
|
|||
mime: "video/webm; codecs=\"vp9\"",
|
||||
format: webm,
|
||||
codec: vp9,
|
||||
throttled: false,
|
||||
),
|
||||
VideoStream(
|
||||
url: "https://rr5---sn-h0jeenek.googlevideo.com/videoplayback?c=WEB_REMIX&clen=35955780&dur=163.046&ei=knDpYub6BojEgAf6jbLgDw&expire=1659487474&fexp=24001373%2C24007246&fvip=4&gir=yes&id=o-AM-wcJVO-yYYbVFnuifnzM4eRnD-AG1bS1AhLoDqi_is&initcwndbps=1418750&ip=2003%3Ade%3Aaf0e%3A2f00%3Ade47%3A297%3Aa6db%3A774e&itag=302&keepalive=yes&lmt=1608509234088626&lsig=AG3C_xAwRgIhAMwYJqxve8BSujC-oaSFBbq67p-rFi7saU5V8Yb3qrjLAiEAlrMKR_sadHrkFpy7o7lGzKOCmU1OQazCNBbXjDT2a-o%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&mh=mQ&mime=video%2Fwebm&mm=31%2C29&mn=sn-h0jeenek%2Csn-h0jelnez&ms=au%2Crdu&mt=1659465669&mv=m&mvi=5&n=1taQMNHGExb_Vg&ns=UTT8RXHZNhPYTw6NgkzWMWEH&otfp=1&pl=37&rbqsm=fr&requiressl=yes&sig=AOq0QJ8wRQIhAOiqSNfGfOprZ9InWVMc7gY0KrTf8weLibcpK0W2Hfa6AiAFHW213qsByzlar5ivCAYttjo1rPciQnLEnh-izJ3ZhA%3D%3D&source=youtube&sparams=expire%2Cei%2Cip%2Cid%2Citag%2Csource%2Crequiressl%2Cspc%2Cvprv%2Cmime%2Cns%2Cgir%2Cclen%2Cotfp%2Cdur%2Clmt&spc=lT-Khox4YuJQ2wmH79zYALRvsWTPCUc&txp=1311222&vprv=1",
|
||||
|
@ -229,7 +220,6 @@ VideoPlayer(
|
|||
mime: "video/webm; codecs=\"vp9\"",
|
||||
format: webm,
|
||||
codec: vp9,
|
||||
throttled: false,
|
||||
),
|
||||
VideoStream(
|
||||
url: "https://rr5---sn-h0jeenek.googlevideo.com/videoplayback?c=WEB_REMIX&clen=65400181&dur=163.046&ei=knDpYub6BojEgAf6jbLgDw&expire=1659487474&fexp=24001373%2C24007246&fvip=4&gir=yes&id=o-AM-wcJVO-yYYbVFnuifnzM4eRnD-AG1bS1AhLoDqi_is&initcwndbps=1418750&ip=2003%3Ade%3Aaf0e%3A2f00%3Ade47%3A297%3Aa6db%3A774e&itag=299&keepalive=yes&lmt=1580005649161486&lsig=AG3C_xAwRgIhAMwYJqxve8BSujC-oaSFBbq67p-rFi7saU5V8Yb3qrjLAiEAlrMKR_sadHrkFpy7o7lGzKOCmU1OQazCNBbXjDT2a-o%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&mh=mQ&mime=video%2Fmp4&mm=31%2C29&mn=sn-h0jeenek%2Csn-h0jelnez&ms=au%2Crdu&mt=1659465669&mv=m&mvi=5&n=1taQMNHGExb_Vg&ns=UTT8RXHZNhPYTw6NgkzWMWEH&otfp=1&pl=37&rbqsm=fr&requiressl=yes&sig=AOq0QJ8wRAIgdkJv6w9_Azf0m6poA-ULyX0eH_GKBtSJRwUY1lNBAZgCIDCrC0lnu__ycTaIhg0pUcsRUqay60S3QMo5084EWifd&source=youtube&sparams=expire%2Cei%2Cip%2Cid%2Citag%2Csource%2Crequiressl%2Cspc%2Cvprv%2Cmime%2Cns%2Cgir%2Cclen%2Cotfp%2Cdur%2Clmt&spc=lT-Khox4YuJQ2wmH79zYALRvsWTPCUc&txp=2211222&vprv=1",
|
||||
|
@ -254,7 +244,6 @@ VideoPlayer(
|
|||
mime: "video/mp4; codecs=\"avc1.64002a\"",
|
||||
format: mp4,
|
||||
codec: avc1,
|
||||
throttled: false,
|
||||
),
|
||||
VideoStream(
|
||||
url: "https://rr5---sn-h0jeenek.googlevideo.com/videoplayback?c=WEB_REMIX&clen=62993617&dur=163.046&ei=knDpYub6BojEgAf6jbLgDw&expire=1659487474&fexp=24001373%2C24007246&fvip=4&gir=yes&id=o-AM-wcJVO-yYYbVFnuifnzM4eRnD-AG1bS1AhLoDqi_is&initcwndbps=1418750&ip=2003%3Ade%3Aaf0e%3A2f00%3Ade47%3A297%3Aa6db%3A774e&itag=303&keepalive=yes&lmt=1608509371758331&lsig=AG3C_xAwRgIhAMwYJqxve8BSujC-oaSFBbq67p-rFi7saU5V8Yb3qrjLAiEAlrMKR_sadHrkFpy7o7lGzKOCmU1OQazCNBbXjDT2a-o%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&mh=mQ&mime=video%2Fwebm&mm=31%2C29&mn=sn-h0jeenek%2Csn-h0jelnez&ms=au%2Crdu&mt=1659465669&mv=m&mvi=5&n=1taQMNHGExb_Vg&ns=UTT8RXHZNhPYTw6NgkzWMWEH&otfp=1&pl=37&rbqsm=fr&requiressl=yes&sig=AOq0QJ8wRAIgZi9dDSMWh10NID8-QNn3azIH1zw5UooZrRTPZjVn7hYCIAm9bFc6NBwJ_DzY4V2R_zGmJSpOwQl8LEsfCb7hf6i7&source=youtube&sparams=expire%2Cei%2Cip%2Cid%2Citag%2Csource%2Crequiressl%2Cspc%2Cvprv%2Cmime%2Cns%2Cgir%2Cclen%2Cotfp%2Cdur%2Clmt&spc=lT-Khox4YuJQ2wmH79zYALRvsWTPCUc&txp=1311222&vprv=1",
|
||||
|
@ -279,7 +268,6 @@ VideoPlayer(
|
|||
mime: "video/webm; codecs=\"vp9\"",
|
||||
format: webm,
|
||||
codec: vp9,
|
||||
throttled: false,
|
||||
),
|
||||
],
|
||||
audio_streams: [
|
||||
|
@ -303,7 +291,6 @@ VideoPlayer(
|
|||
codec: opus,
|
||||
channels: Some(2),
|
||||
loudness_db: Some(0.0006532669),
|
||||
throttled: false,
|
||||
track: None,
|
||||
),
|
||||
AudioStream(
|
||||
|
@ -326,7 +313,6 @@ VideoPlayer(
|
|||
codec: opus,
|
||||
channels: Some(2),
|
||||
loudness_db: Some(0.0006532669),
|
||||
throttled: false,
|
||||
track: None,
|
||||
),
|
||||
AudioStream(
|
||||
|
@ -349,7 +335,6 @@ VideoPlayer(
|
|||
codec: mp4a,
|
||||
channels: Some(2),
|
||||
loudness_db: Some(-0.003446579),
|
||||
throttled: false,
|
||||
track: None,
|
||||
),
|
||||
AudioStream(
|
||||
|
@ -372,7 +357,6 @@ VideoPlayer(
|
|||
codec: opus,
|
||||
channels: Some(2),
|
||||
loudness_db: Some(0.0006532669),
|
||||
throttled: false,
|
||||
track: None,
|
||||
),
|
||||
],
|
||||
|
@ -419,5 +403,6 @@ VideoPlayer(
|
|||
frames_per_page_y: 5,
|
||||
),
|
||||
],
|
||||
client_type: desktop_music,
|
||||
visitor_data: Some("CgszSHZWNWs0SDhpTSiS4aWXBg%3D%3D"),
|
||||
)
|
||||
|
|
|
@ -5,7 +5,7 @@ expression: map_res.c
|
|||
VideoPlayer(
|
||||
details: VideoPlayerDetails(
|
||||
id: "pPvd8UxmSbQ",
|
||||
name: "Inspiring Cinematic Uplifting (Creative Commons)",
|
||||
name: Some("Inspiring Cinematic Uplifting (Creative Commons)"),
|
||||
description: Some("► Download Music: http://bit.ly/2QLufeh\nImportant to know! You can download this track for free through Patreon. You will pay only for new tracks! So join others and let\'s make next track together!\n\n► MORE MUSIC: Become my patron and get access to all our music from Patreon library. More Info here: http://bit.ly/2JJDFHb\n\n► Additional edit versions of this track you can download here: http://bit.ly/2WdRinT (5 versions)\n--------------------- \n\n►DESCRIPTION:\nInspiring Cinematic Uplifting Trailer Background - epic music for trailer video project with powerful drums, energetic orchestra and gentle piano melody. This motivational cinematic theme will work as perfect background for beautiful epic moments, landscapes, nature, drone video, motivational products and achievements.\n--------------------- \n\n► LICENSE:\n● If you need a license for your project, you can purchase it here: \nhttps://1.envato.market/ajicu (Audiojungle)\nhttps://bit.ly/3fWZZuI (Pond5)\n--------------------- \n\n► LISTEN ON:\n● Spotify - https://spoti.fi/2sHm3UH\n● Apple Music - https://apple.co/3qBjbUO\n--------------------- \n\n► SUBSCRIBE FOR MORE: \nPatreon: http://bit.ly/2JJDFHb\nYoutube: http://bit.ly/2AYBzfA\nFacebook: http://bit.ly/2T6dTx5\nInstagram: http://bit.ly/2BHJ8rB\nTwitter: http://bit.ly/2MwtOlT\nSoundCloud: http://bit.ly/2IwVVmt\nAudiojungle: https://1.envato.market/ajrsm\nPond5: https://bit.ly/2TLi1rW\n--------------------- \n►Photo by Vittorio Staffolani from Pexels\n--------------------- \n\nFAQ:\n\n► Can I use this music in my videos? \n● Sure! Just download this track and you are ready to use it! We only ask to credit us. \n-------------------- \n\n► What is \"Creative Commons\"? \nCreative Commons is a system that allows you to legally use “some rights reserved” music, movies, images, and other content — all for free. Licensees may copy, distribute, display and perform the work and make derivative works and remixes based on it only if they give the author or licensor the credits.\n-------------------- \n\n► Will I have any copyright issues with this track?\n● No, you should not have any copyright problems with this track!\n-------------------- \n\n► Is it necessary to become your patron?\n● No it\'s not necessary. But we recommend you to become our patron because you will get access to huge library of music. You will download only highest quality files. You will find additional edited versions of every track. You always be tuned with our news. You will find music not only from Roman Senyk but also from another talented authors.\n-------------------- \n\n► Why I received a copyright claim when I used this track?\n● Do not panic! This is very common situation. Content ID fingerprint system can mismatch our music. Just dispute the claim by showing our original track. Or send us the link to your video (romansenykmusic@gmail.com) and attach some screenshot with claim information. Claim will be released until 24 hours!\n\n► How to credit you in my video?\n● Just add to the description of your project information about Author, Name of Song and the link to our original track. Or copy and paste:\n\nMusic Info: Inspiring Cinematic Uplifting by RomanSenykMusic.\nMusic Link: https://youtu.be/pPvd8UxmSbQ\n--------------------- \n\n► If you have any questions, you can write in the comments for this video or by email: romansenykmusic@gmail.com\n--------------------- \n\nStay tuned! The best is yet to come! \nThanks For Listening!\nRoman Senyk"),
|
||||
duration: 163,
|
||||
thumbnail: [
|
||||
|
@ -25,11 +25,9 @@ VideoPlayer(
|
|||
height: 480,
|
||||
),
|
||||
],
|
||||
channel: ChannelId(
|
||||
id: "UCbxxEi-ImPlbLx5F-fHetEg",
|
||||
name: "RomanSenykMusic - Royalty Free Music",
|
||||
),
|
||||
view_count: 426567,
|
||||
channel_id: "UCbxxEi-ImPlbLx5F-fHetEg",
|
||||
channel_name: Some("RomanSenykMusic - Royalty Free Music"),
|
||||
view_count: Some(426567),
|
||||
keywords: [
|
||||
"no copyright music",
|
||||
"background music",
|
||||
|
@ -81,7 +79,6 @@ VideoPlayer(
|
|||
mime: "video/mp4; codecs=\"avc1.4D401E\"",
|
||||
format: mp4,
|
||||
codec: avc1,
|
||||
throttled: false,
|
||||
),
|
||||
VideoStream(
|
||||
url: "https://rr4---sn-h0jelnez.googlevideo.com/videoplayback?c=IOS&clen=65400181&dur=163.046&ei=q1jpYq-xHs7NgQev0bfwAQ&expire=1659481355&fexp=24001373%2C24007246&fvip=5&gir=yes&id=o-ANNg3iPHI56jhLSlPQk4pi4mdub5iAby0hmJBVrtiJgY&initcwndbps=1513750&ip=2003%3Ade%3Aaf0e%3A2f00%3Ade47%3A297%3Aa6db%3A774e&itag=299&keepalive=yes&lmt=1580005649161486&lsig=AG3C_xAwRQIgWKVoDpyI6QmVnkdGzdirFtjMAXhmLex64VTO7UUJd-4CIQDoJKkT2-Kpa7j0merJJoZDs4IkkXSjdNm3bvdCL8t2Pg%3D%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&mh=mQ&mime=video%2Fmp4&mm=31%2C29&mn=sn-h0jelnez%2Csn-h0jeenek&ms=au%2Crdu&mt=1659459429&mv=m&mvi=4&otfp=1&pl=37&rbqsm=fr&requiressl=yes&sig=AOq0QJ8wRgIhAP6zxXXA18ToZWUfalauhhsgOsDHTu-R0QrqNrJR7D5kAiEAi8HBa9OkYwmA0bcRxhgvXfN9JsFlXwCWJ-x4ty6TjoY%3D&source=youtube&sparams=expire%2Cei%2Cip%2Cid%2Citag%2Csource%2Crequiressl%2Cvprv%2Csvpuc%2Cmime%2Cgir%2Cclen%2Cotfp%2Cdur%2Clmt&svpuc=1&txp=2211222&vprv=1",
|
||||
|
@ -106,7 +103,6 @@ VideoPlayer(
|
|||
mime: "video/mp4; codecs=\"avc1.64002A\"",
|
||||
format: mp4,
|
||||
codec: avc1,
|
||||
throttled: false,
|
||||
),
|
||||
],
|
||||
audio_streams: [
|
||||
|
@ -130,7 +126,6 @@ VideoPlayer(
|
|||
codec: mp4a,
|
||||
channels: Some(2),
|
||||
loudness_db: Some(5.2159004),
|
||||
throttled: false,
|
||||
track: None,
|
||||
),
|
||||
AudioStream(
|
||||
|
@ -153,7 +148,6 @@ VideoPlayer(
|
|||
codec: mp4a,
|
||||
channels: Some(2),
|
||||
loudness_db: Some(5.2159004),
|
||||
throttled: false,
|
||||
track: None,
|
||||
),
|
||||
],
|
||||
|
@ -200,5 +194,6 @@ VideoPlayer(
|
|||
frames_per_page_y: 5,
|
||||
),
|
||||
],
|
||||
client_type: ios,
|
||||
visitor_data: Some("Cgs4TXV4dk13WVEyWSirsaWXBg%3D%3D"),
|
||||
)
|
||||
|
|
|
@ -0,0 +1,518 @@
|
|||
---
|
||||
source: src/client/player.rs
|
||||
expression: map_res.c
|
||||
---
|
||||
VideoPlayer(
|
||||
details: VideoPlayerDetails(
|
||||
id: "pPvd8UxmSbQ",
|
||||
name: None,
|
||||
description: None,
|
||||
duration: 163,
|
||||
thumbnail: [
|
||||
Thumbnail(
|
||||
url: "https://i.ytimg.com/vi/pPvd8UxmSbQ/hqdefault.jpg",
|
||||
width: 480,
|
||||
height: 360,
|
||||
),
|
||||
],
|
||||
channel_id: "UCbxxEi-ImPlbLx5F-fHetEg",
|
||||
channel_name: None,
|
||||
view_count: None,
|
||||
keywords: [],
|
||||
is_live: false,
|
||||
is_live_content: false,
|
||||
),
|
||||
video_streams: [
|
||||
VideoStream(
|
||||
url: "https://rr5---sn-h0jeenek.googlevideo.com/videoplayback?bui=AXc671IT4iUCpJNJWUitTMgIi6njuKSsi3MNed1Szyf0qysTX0v1Nf6AyCvjIGbek5Fn50kuBrGtRJ5q&c=TVHTML5&clen=10262148&dur=163.096&ei=viioZtTdKteHi9oPl42KsAg&expire=1722318110&fvip=4&gir=yes&id=o-AC7iotZ_nCvg7C6fK7ofX174GXVOdwW68lsyXLLmCs0h&initcwndbps=1957500&ip=93.235.183.158&itag=18&lmt=1700885551970466&lmw=1&lsig=AGtxev0wRgIhANyFV4Ji7jlkXvfkb_czMQDZCiu6AbJ3Kzyv_s9V9WyvAiEA0o8XuM9kyh98hG1yg7h44L3I5OAUXuTpQdjxUaZ1V4A%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&mh=mQ&mime=video%2Fmp4&mm=31%2C29&mn=sn-h0jeenek%2Csn-h0jelnez&ms=au%2Crdu&mt=1722295996&mv=m&mvi=5&n=BMzwItzIOB1HhmG&ns=YmgbZhlLp0C-9ilsQWGAyUAQ&pl=26&ratebypass=yes&requiressl=yes&rqh=1&sefc=1&sig=AJfQdSswRAIgUah4qH8RqPzmo75ExCWSiRYlUlsAk0v9gl638LitVNICICxFs5lK3CsmOAja0bsXavXkyykzpdhHZKGXOZQYT1f8&source=youtube&sparams=expire%2Cei%2Cip%2Cid%2Citag%2Csource%2Crequiressl%2Cxpc%2Cbui%2Cvprv%2Csvpuc%2Cmime%2Cns%2Crqh%2Cgir%2Cclen%2Cratebypass%2Cdur%2Clmt&svpuc=1&txp=1318224&vprv=1&xpc=EgVo2aDSNQ%3D%3D",
|
||||
itag: 18,
|
||||
bitrate: 503574,
|
||||
average_bitrate: 503367,
|
||||
size: Some(10262148),
|
||||
index_range: None,
|
||||
init_range: None,
|
||||
duration_ms: Some(163096),
|
||||
width: 640,
|
||||
height: 360,
|
||||
fps: 30,
|
||||
quality: "360p",
|
||||
hdr: false,
|
||||
mime: "video/mp4; codecs=\"avc1.42001E, mp4a.40.2\"",
|
||||
format: mp4,
|
||||
codec: avc1,
|
||||
),
|
||||
],
|
||||
video_only_streams: [
|
||||
VideoStream(
|
||||
url: "https://rr5---sn-h0jeenek.googlevideo.com/videoplayback?aitags=133%2C134%2C135%2C136%2C160%2C242%2C243%2C244%2C247%2C278%2C298%2C299%2C302%2C303&bui=AXc671IvQBUNCtxNiAkj0M-Bvcb-N5cUu1XFk68f4Cnj0sFLEy1sixyW5lThzLYJXioG8kVQ2xT9KNLS&c=TVHTML5&clen=2273274&dur=163.029&ei=viioZtTdKteHi9oPl42KsAg&expire=1722318110&fvip=4&gir=yes&id=o-AC7iotZ_nCvg7C6fK7ofX174GXVOdwW68lsyXLLmCs0h&initcwndbps=1957500&ip=93.235.183.158&itag=160&keepalive=yes&lmt=1705967288821438&lmw=1&lsig=AGtxev0wRgIhANyFV4Ji7jlkXvfkb_czMQDZCiu6AbJ3Kzyv_s9V9WyvAiEA0o8XuM9kyh98hG1yg7h44L3I5OAUXuTpQdjxUaZ1V4A%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&mh=mQ&mime=video%2Fmp4&mm=31%2C29&mn=sn-h0jeenek%2Csn-h0jelnez&ms=au%2Crdu&mt=1722295996&mv=m&mvi=5&n=SWvqB0UTkUvifuM&ns=ZR8RwjQ3VJGDvQifdaM1IRMQ&pl=26&requiressl=yes&rqh=1&sefc=1&sig=AJfQdSswRAIgb8eXnQ6MSJ3PuvFVBdYIWTnFobH8mTC9zbZpBNxLbBYCICkPLKEm3gNbW5HIFXs7bwF5rSqUKHHnXNK91qMslQog&source=youtube&sparams=expire%2Cei%2Cip%2Cid%2Caitags%2Csource%2Crequiressl%2Cxpc%2Cbui%2Cvprv%2Csvpuc%2Cmime%2Cns%2Crqh%2Cgir%2Cclen%2Cdur%2Clmt&svpuc=1&txp=1308224&vprv=1&xpc=EgVo2aDSNQ%3D%3D",
|
||||
itag: 160,
|
||||
bitrate: 114816,
|
||||
average_bitrate: 111551,
|
||||
size: Some(2273274),
|
||||
index_range: Some(Range(
|
||||
start: 738,
|
||||
end: 1165,
|
||||
)),
|
||||
init_range: Some(Range(
|
||||
start: 0,
|
||||
end: 737,
|
||||
)),
|
||||
duration_ms: Some(163029),
|
||||
width: 256,
|
||||
height: 144,
|
||||
fps: 30,
|
||||
quality: "144p",
|
||||
hdr: false,
|
||||
mime: "video/mp4; codecs=\"avc1.4d400c\"",
|
||||
format: mp4,
|
||||
codec: avc1,
|
||||
),
|
||||
VideoStream(
|
||||
url: "https://rr5---sn-h0jeenek.googlevideo.com/videoplayback?aitags=133%2C134%2C135%2C136%2C160%2C242%2C243%2C244%2C247%2C278%2C298%2C299%2C302%2C303&bui=AXc671IvQBUNCtxNiAkj0M-Bvcb-N5cUu1XFk68f4Cnj0sFLEy1sixyW5lThzLYJXioG8kVQ2xT9KNLS&c=TVHTML5&clen=1151892&dur=163.029&ei=viioZtTdKteHi9oPl42KsAg&expire=1722318110&fvip=4&gir=yes&id=o-AC7iotZ_nCvg7C6fK7ofX174GXVOdwW68lsyXLLmCs0h&initcwndbps=1957500&ip=93.235.183.158&itag=278&keepalive=yes&lmt=1705966620402771&lmw=1&lsig=AGtxev0wRgIhANyFV4Ji7jlkXvfkb_czMQDZCiu6AbJ3Kzyv_s9V9WyvAiEA0o8XuM9kyh98hG1yg7h44L3I5OAUXuTpQdjxUaZ1V4A%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&mh=mQ&mime=video%2Fwebm&mm=31%2C29&mn=sn-h0jeenek%2Csn-h0jelnez&ms=au%2Crdu&mt=1722295996&mv=m&mvi=5&n=SWvqB0UTkUvifuM&ns=ZR8RwjQ3VJGDvQifdaM1IRMQ&pl=26&requiressl=yes&rqh=1&sefc=1&sig=AJfQdSswRQIhAP4IybR7cZRpx7IX1ke6UIu_hdFZN3LOuHBDywg_xv5WAiB8_XEx8VhT9OlFxmM-cY0fl6-7GT9uj3clMIPDk2w7cA%3D%3D&source=youtube&sparams=expire%2Cei%2Cip%2Cid%2Caitags%2Csource%2Crequiressl%2Cxpc%2Cbui%2Cvprv%2Csvpuc%2Cmime%2Cns%2Crqh%2Cgir%2Cclen%2Cdur%2Clmt&svpuc=1&txp=130F224&vprv=1&xpc=EgVo2aDSNQ%3D%3D",
|
||||
itag: 278,
|
||||
bitrate: 70630,
|
||||
average_bitrate: 56524,
|
||||
size: Some(1151892),
|
||||
index_range: Some(Range(
|
||||
start: 218,
|
||||
end: 767,
|
||||
)),
|
||||
init_range: Some(Range(
|
||||
start: 0,
|
||||
end: 217,
|
||||
)),
|
||||
duration_ms: Some(163029),
|
||||
width: 256,
|
||||
height: 144,
|
||||
fps: 30,
|
||||
quality: "144p",
|
||||
hdr: false,
|
||||
mime: "video/webm; codecs=\"vp9\"",
|
||||
format: webm,
|
||||
codec: vp9,
|
||||
),
|
||||
VideoStream(
|
||||
url: "https://rr5---sn-h0jeenek.googlevideo.com/videoplayback?aitags=133%2C134%2C135%2C136%2C160%2C242%2C243%2C244%2C247%2C278%2C298%2C299%2C302%2C303&bui=AXc671IvQBUNCtxNiAkj0M-Bvcb-N5cUu1XFk68f4Cnj0sFLEy1sixyW5lThzLYJXioG8kVQ2xT9KNLS&c=TVHTML5&clen=5026513&dur=163.029&ei=viioZtTdKteHi9oPl42KsAg&expire=1722318110&fvip=4&gir=yes&id=o-AC7iotZ_nCvg7C6fK7ofX174GXVOdwW68lsyXLLmCs0h&initcwndbps=1957500&ip=93.235.183.158&itag=133&keepalive=yes&lmt=1705967298859029&lmw=1&lsig=AGtxev0wRgIhANyFV4Ji7jlkXvfkb_czMQDZCiu6AbJ3Kzyv_s9V9WyvAiEA0o8XuM9kyh98hG1yg7h44L3I5OAUXuTpQdjxUaZ1V4A%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&mh=mQ&mime=video%2Fmp4&mm=31%2C29&mn=sn-h0jeenek%2Csn-h0jelnez&ms=au%2Crdu&mt=1722295996&mv=m&mvi=5&n=SWvqB0UTkUvifuM&ns=ZR8RwjQ3VJGDvQifdaM1IRMQ&pl=26&requiressl=yes&rqh=1&sefc=1&sig=AJfQdSswRAIgPF0ms4OEe15BTjOFVCkvf52UeTUf0b62_pavCfEyGjcCIH-0AoxzyT8iioWFFaX7iYjqzzaUTpo8rgAPQ0uX8DJa&source=youtube&sparams=expire%2Cei%2Cip%2Cid%2Caitags%2Csource%2Crequiressl%2Cxpc%2Cbui%2Cvprv%2Csvpuc%2Cmime%2Cns%2Crqh%2Cgir%2Cclen%2Cdur%2Clmt&svpuc=1&txp=1308224&vprv=1&xpc=EgVo2aDSNQ%3D%3D",
|
||||
itag: 133,
|
||||
bitrate: 257417,
|
||||
average_bitrate: 246656,
|
||||
size: Some(5026513),
|
||||
index_range: Some(Range(
|
||||
start: 739,
|
||||
end: 1166,
|
||||
)),
|
||||
init_range: Some(Range(
|
||||
start: 0,
|
||||
end: 738,
|
||||
)),
|
||||
duration_ms: Some(163029),
|
||||
width: 426,
|
||||
height: 240,
|
||||
fps: 30,
|
||||
quality: "240p",
|
||||
hdr: false,
|
||||
mime: "video/mp4; codecs=\"avc1.4d4015\"",
|
||||
format: mp4,
|
||||
codec: avc1,
|
||||
),
|
||||
VideoStream(
|
||||
url: "https://rr5---sn-h0jeenek.googlevideo.com/videoplayback?aitags=133%2C134%2C135%2C136%2C160%2C242%2C243%2C244%2C247%2C278%2C298%2C299%2C302%2C303&bui=AXc671IvQBUNCtxNiAkj0M-Bvcb-N5cUu1XFk68f4Cnj0sFLEy1sixyW5lThzLYJXioG8kVQ2xT9KNLS&c=TVHTML5&clen=2541351&dur=163.029&ei=viioZtTdKteHi9oPl42KsAg&expire=1722318110&fvip=4&gir=yes&id=o-AC7iotZ_nCvg7C6fK7ofX174GXVOdwW68lsyXLLmCs0h&initcwndbps=1957500&ip=93.235.183.158&itag=242&keepalive=yes&lmt=1705966614837727&lmw=1&lsig=AGtxev0wRgIhANyFV4Ji7jlkXvfkb_czMQDZCiu6AbJ3Kzyv_s9V9WyvAiEA0o8XuM9kyh98hG1yg7h44L3I5OAUXuTpQdjxUaZ1V4A%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&mh=mQ&mime=video%2Fwebm&mm=31%2C29&mn=sn-h0jeenek%2Csn-h0jelnez&ms=au%2Crdu&mt=1722295996&mv=m&mvi=5&n=SWvqB0UTkUvifuM&ns=ZR8RwjQ3VJGDvQifdaM1IRMQ&pl=26&requiressl=yes&rqh=1&sefc=1&sig=AJfQdSswRQIgKj1JyMGwYtf16zLJsmbnizz5_v3jaZSa7-j-ls8-qzECIQDKUd50iIc52h7zOX50Hf1SkbV9h-hP4QHs-wkik1fk6Q%3D%3D&source=youtube&sparams=expire%2Cei%2Cip%2Cid%2Caitags%2Csource%2Crequiressl%2Cxpc%2Cbui%2Cvprv%2Csvpuc%2Cmime%2Cns%2Crqh%2Cgir%2Cclen%2Cdur%2Clmt&svpuc=1&txp=130F224&vprv=1&xpc=EgVo2aDSNQ%3D%3D",
|
||||
itag: 242,
|
||||
bitrate: 149589,
|
||||
average_bitrate: 124706,
|
||||
size: Some(2541351),
|
||||
index_range: Some(Range(
|
||||
start: 219,
|
||||
end: 768,
|
||||
)),
|
||||
init_range: Some(Range(
|
||||
start: 0,
|
||||
end: 218,
|
||||
)),
|
||||
duration_ms: Some(163029),
|
||||
width: 426,
|
||||
height: 240,
|
||||
fps: 30,
|
||||
quality: "240p",
|
||||
hdr: false,
|
||||
mime: "video/webm; codecs=\"vp9\"",
|
||||
format: webm,
|
||||
codec: vp9,
|
||||
),
|
||||
VideoStream(
|
||||
url: "https://rr5---sn-h0jeenek.googlevideo.com/videoplayback?aitags=133%2C134%2C135%2C136%2C160%2C242%2C243%2C244%2C247%2C278%2C298%2C299%2C302%2C303&bui=AXc671IvQBUNCtxNiAkj0M-Bvcb-N5cUu1XFk68f4Cnj0sFLEy1sixyW5lThzLYJXioG8kVQ2xT9KNLS&c=TVHTML5&clen=7810925&dur=163.029&ei=viioZtTdKteHi9oPl42KsAg&expire=1722318110&fvip=4&gir=yes&id=o-AC7iotZ_nCvg7C6fK7ofX174GXVOdwW68lsyXLLmCs0h&initcwndbps=1957500&ip=93.235.183.158&itag=134&keepalive=yes&lmt=1705967286812435&lmw=1&lsig=AGtxev0wRgIhANyFV4Ji7jlkXvfkb_czMQDZCiu6AbJ3Kzyv_s9V9WyvAiEA0o8XuM9kyh98hG1yg7h44L3I5OAUXuTpQdjxUaZ1V4A%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&mh=mQ&mime=video%2Fmp4&mm=31%2C29&mn=sn-h0jeenek%2Csn-h0jelnez&ms=au%2Crdu&mt=1722295996&mv=m&mvi=5&n=SWvqB0UTkUvifuM&ns=ZR8RwjQ3VJGDvQifdaM1IRMQ&pl=26&requiressl=yes&rqh=1&sefc=1&sig=AJfQdSswRgIhAJ92IgZgdk3_WLsfzJV_ZyrSFSbzpsoJh3DkRKDHbNxzAiEA9UbnVlXQ2S3BUimLmWC5TZQfhIkc-PlLnZ81fL0S5yA%3D&source=youtube&sparams=expire%2Cei%2Cip%2Cid%2Caitags%2Csource%2Crequiressl%2Cxpc%2Cbui%2Cvprv%2Csvpuc%2Cmime%2Cns%2Crqh%2Cgir%2Cclen%2Cdur%2Clmt&svpuc=1&txp=1308224&vprv=1&xpc=EgVo2aDSNQ%3D%3D",
|
||||
itag: 134,
|
||||
bitrate: 537902,
|
||||
average_bitrate: 383290,
|
||||
size: Some(7810925),
|
||||
index_range: Some(Range(
|
||||
start: 740,
|
||||
end: 1167,
|
||||
)),
|
||||
init_range: Some(Range(
|
||||
start: 0,
|
||||
end: 739,
|
||||
)),
|
||||
duration_ms: Some(163029),
|
||||
width: 640,
|
||||
height: 360,
|
||||
fps: 30,
|
||||
quality: "360p",
|
||||
hdr: false,
|
||||
mime: "video/mp4; codecs=\"avc1.4d401e\"",
|
||||
format: mp4,
|
||||
codec: avc1,
|
||||
),
|
||||
VideoStream(
|
||||
url: "https://rr5---sn-h0jeenek.googlevideo.com/videoplayback?aitags=133%2C134%2C135%2C136%2C160%2C242%2C243%2C244%2C247%2C278%2C298%2C299%2C302%2C303&bui=AXc671IvQBUNCtxNiAkj0M-Bvcb-N5cUu1XFk68f4Cnj0sFLEy1sixyW5lThzLYJXioG8kVQ2xT9KNLS&c=TVHTML5&clen=4188954&dur=163.029&ei=viioZtTdKteHi9oPl42KsAg&expire=1722318110&fvip=4&gir=yes&id=o-AC7iotZ_nCvg7C6fK7ofX174GXVOdwW68lsyXLLmCs0h&initcwndbps=1957500&ip=93.235.183.158&itag=243&keepalive=yes&lmt=1705966624121874&lmw=1&lsig=AGtxev0wRgIhANyFV4Ji7jlkXvfkb_czMQDZCiu6AbJ3Kzyv_s9V9WyvAiEA0o8XuM9kyh98hG1yg7h44L3I5OAUXuTpQdjxUaZ1V4A%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&mh=mQ&mime=video%2Fwebm&mm=31%2C29&mn=sn-h0jeenek%2Csn-h0jelnez&ms=au%2Crdu&mt=1722295996&mv=m&mvi=5&n=SWvqB0UTkUvifuM&ns=ZR8RwjQ3VJGDvQifdaM1IRMQ&pl=26&requiressl=yes&rqh=1&sefc=1&sig=AJfQdSswRQIgSCLGQvdZKNXym0zt7c3Yw_4e0J8-wNxtPagPRRn4dRoCIQCOj0IzalNG4EcowBIyK2LC6NLFDr8Zt6sNVkqPjw6lGg%3D%3D&source=youtube&sparams=expire%2Cei%2Cip%2Cid%2Caitags%2Csource%2Crequiressl%2Cxpc%2Cbui%2Cvprv%2Csvpuc%2Cmime%2Cns%2Crqh%2Cgir%2Cclen%2Cdur%2Clmt&svpuc=1&txp=130F224&vprv=1&xpc=EgVo2aDSNQ%3D%3D",
|
||||
itag: 243,
|
||||
bitrate: 248858,
|
||||
average_bitrate: 205556,
|
||||
size: Some(4188954),
|
||||
index_range: Some(Range(
|
||||
start: 220,
|
||||
end: 770,
|
||||
)),
|
||||
init_range: Some(Range(
|
||||
start: 0,
|
||||
end: 219,
|
||||
)),
|
||||
duration_ms: Some(163029),
|
||||
width: 640,
|
||||
height: 360,
|
||||
fps: 30,
|
||||
quality: "360p",
|
||||
hdr: false,
|
||||
mime: "video/webm; codecs=\"vp9\"",
|
||||
format: webm,
|
||||
codec: vp9,
|
||||
),
|
||||
VideoStream(
|
||||
url: "https://rr5---sn-h0jeenek.googlevideo.com/videoplayback?aitags=133%2C134%2C135%2C136%2C160%2C242%2C243%2C244%2C247%2C278%2C298%2C299%2C302%2C303&bui=AXc671IvQBUNCtxNiAkj0M-Bvcb-N5cUu1XFk68f4Cnj0sFLEy1sixyW5lThzLYJXioG8kVQ2xT9KNLS&c=TVHTML5&clen=14723538&dur=163.029&ei=viioZtTdKteHi9oPl42KsAg&expire=1722318110&fvip=4&gir=yes&id=o-AC7iotZ_nCvg7C6fK7ofX174GXVOdwW68lsyXLLmCs0h&initcwndbps=1957500&ip=93.235.183.158&itag=135&keepalive=yes&lmt=1705967282545273&lmw=1&lsig=AGtxev0wRgIhANyFV4Ji7jlkXvfkb_czMQDZCiu6AbJ3Kzyv_s9V9WyvAiEA0o8XuM9kyh98hG1yg7h44L3I5OAUXuTpQdjxUaZ1V4A%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&mh=mQ&mime=video%2Fmp4&mm=31%2C29&mn=sn-h0jeenek%2Csn-h0jelnez&ms=au%2Crdu&mt=1722295996&mv=m&mvi=5&n=SWvqB0UTkUvifuM&ns=ZR8RwjQ3VJGDvQifdaM1IRMQ&pl=26&requiressl=yes&rqh=1&sefc=1&sig=AJfQdSswRgIhAM843wAa1e7Gc1S69gfXckm7hdgIKPXp0bUSh3hO6W5zAiEA-DDEPGsZBmF5N8VbPy75dhy3rLpE1F18KtWgmrUm2Pg%3D&source=youtube&sparams=expire%2Cei%2Cip%2Cid%2Caitags%2Csource%2Crequiressl%2Cxpc%2Cbui%2Cvprv%2Csvpuc%2Cmime%2Cns%2Crqh%2Cgir%2Cclen%2Cdur%2Clmt&svpuc=1&txp=1308224&vprv=1&xpc=EgVo2aDSNQ%3D%3D",
|
||||
itag: 135,
|
||||
bitrate: 978945,
|
||||
average_bitrate: 722499,
|
||||
size: Some(14723538),
|
||||
index_range: Some(Range(
|
||||
start: 740,
|
||||
end: 1167,
|
||||
)),
|
||||
init_range: Some(Range(
|
||||
start: 0,
|
||||
end: 739,
|
||||
)),
|
||||
duration_ms: Some(163029),
|
||||
width: 854,
|
||||
height: 480,
|
||||
fps: 30,
|
||||
quality: "480p",
|
||||
hdr: false,
|
||||
mime: "video/mp4; codecs=\"avc1.4d401f\"",
|
||||
format: mp4,
|
||||
codec: avc1,
|
||||
),
|
||||
VideoStream(
|
||||
url: "https://rr5---sn-h0jeenek.googlevideo.com/videoplayback?aitags=133%2C134%2C135%2C136%2C160%2C242%2C243%2C244%2C247%2C278%2C298%2C299%2C302%2C303&bui=AXc671IvQBUNCtxNiAkj0M-Bvcb-N5cUu1XFk68f4Cnj0sFLEy1sixyW5lThzLYJXioG8kVQ2xT9KNLS&c=TVHTML5&clen=7788899&dur=163.029&ei=viioZtTdKteHi9oPl42KsAg&expire=1722318110&fvip=4&gir=yes&id=o-AC7iotZ_nCvg7C6fK7ofX174GXVOdwW68lsyXLLmCs0h&initcwndbps=1957500&ip=93.235.183.158&itag=244&keepalive=yes&lmt=1705966622098793&lmw=1&lsig=AGtxev0wRgIhANyFV4Ji7jlkXvfkb_czMQDZCiu6AbJ3Kzyv_s9V9WyvAiEA0o8XuM9kyh98hG1yg7h44L3I5OAUXuTpQdjxUaZ1V4A%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&mh=mQ&mime=video%2Fwebm&mm=31%2C29&mn=sn-h0jeenek%2Csn-h0jelnez&ms=au%2Crdu&mt=1722295996&mv=m&mvi=5&n=SWvqB0UTkUvifuM&ns=ZR8RwjQ3VJGDvQifdaM1IRMQ&pl=26&requiressl=yes&rqh=1&sefc=1&sig=AJfQdSswRQIhAKGyn799bfkVHYE195sPmD60dCMppqJrBM0O-sjgYTzzAiAoBjkNAtL90sXw2YP9UTW9JrMhPSvPiBI_KiCVMJAkFQ%3D%3D&source=youtube&sparams=expire%2Cei%2Cip%2Cid%2Caitags%2Csource%2Crequiressl%2Cxpc%2Cbui%2Cvprv%2Csvpuc%2Cmime%2Cns%2Crqh%2Cgir%2Cclen%2Cdur%2Clmt&svpuc=1&txp=130F224&vprv=1&xpc=EgVo2aDSNQ%3D%3D",
|
||||
itag: 244,
|
||||
bitrate: 467884,
|
||||
average_bitrate: 382209,
|
||||
size: Some(7788899),
|
||||
index_range: Some(Range(
|
||||
start: 220,
|
||||
end: 770,
|
||||
)),
|
||||
init_range: Some(Range(
|
||||
start: 0,
|
||||
end: 219,
|
||||
)),
|
||||
duration_ms: Some(163029),
|
||||
width: 854,
|
||||
height: 480,
|
||||
fps: 30,
|
||||
quality: "480p",
|
||||
hdr: false,
|
||||
mime: "video/webm; codecs=\"vp9\"",
|
||||
format: webm,
|
||||
codec: vp9,
|
||||
),
|
||||
VideoStream(
|
||||
url: "https://rr5---sn-h0jeenek.googlevideo.com/videoplayback?aitags=133%2C134%2C135%2C136%2C160%2C242%2C243%2C244%2C247%2C278%2C298%2C299%2C302%2C303&bui=AXc671IvQBUNCtxNiAkj0M-Bvcb-N5cUu1XFk68f4Cnj0sFLEy1sixyW5lThzLYJXioG8kVQ2xT9KNLS&c=TVHTML5&clen=24616305&dur=163.029&ei=viioZtTdKteHi9oPl42KsAg&expire=1722318110&fvip=4&gir=yes&id=o-AC7iotZ_nCvg7C6fK7ofX174GXVOdwW68lsyXLLmCs0h&initcwndbps=1957500&ip=93.235.183.158&itag=136&keepalive=yes&lmt=1705967307531372&lmw=1&lsig=AGtxev0wRgIhANyFV4Ji7jlkXvfkb_czMQDZCiu6AbJ3Kzyv_s9V9WyvAiEA0o8XuM9kyh98hG1yg7h44L3I5OAUXuTpQdjxUaZ1V4A%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&mh=mQ&mime=video%2Fmp4&mm=31%2C29&mn=sn-h0jeenek%2Csn-h0jelnez&ms=au%2Crdu&mt=1722295996&mv=m&mvi=5&n=SWvqB0UTkUvifuM&ns=ZR8RwjQ3VJGDvQifdaM1IRMQ&pl=26&requiressl=yes&rqh=1&sefc=1&sig=AJfQdSswRgIhAM57L2Utesn4xVyT0HSwR9Khv_S-efx4uFAbCPkZFoRXAiEAtIu63-jF2_FZkOMmZAqGU3SRU9QgxoajRjBhMFwcOuk%3D&source=youtube&sparams=expire%2Cei%2Cip%2Cid%2Caitags%2Csource%2Crequiressl%2Cxpc%2Cbui%2Cvprv%2Csvpuc%2Cmime%2Cns%2Crqh%2Cgir%2Cclen%2Cdur%2Clmt&svpuc=1&txp=1308224&vprv=1&xpc=EgVo2aDSNQ%3D%3D",
|
||||
itag: 136,
|
||||
bitrate: 1560439,
|
||||
average_bitrate: 1207947,
|
||||
size: Some(24616305),
|
||||
index_range: Some(Range(
|
||||
start: 739,
|
||||
end: 1166,
|
||||
)),
|
||||
init_range: Some(Range(
|
||||
start: 0,
|
||||
end: 738,
|
||||
)),
|
||||
duration_ms: Some(163029),
|
||||
width: 1280,
|
||||
height: 720,
|
||||
fps: 30,
|
||||
quality: "720p",
|
||||
hdr: false,
|
||||
mime: "video/mp4; codecs=\"avc1.4d401f\"",
|
||||
format: mp4,
|
||||
codec: avc1,
|
||||
),
|
||||
VideoStream(
|
||||
url: "https://rr5---sn-h0jeenek.googlevideo.com/videoplayback?aitags=133%2C134%2C135%2C136%2C160%2C242%2C243%2C244%2C247%2C278%2C298%2C299%2C302%2C303&bui=AXc671IvQBUNCtxNiAkj0M-Bvcb-N5cUu1XFk68f4Cnj0sFLEy1sixyW5lThzLYJXioG8kVQ2xT9KNLS&c=TVHTML5&clen=34544823&dur=163.046&ei=viioZtTdKteHi9oPl42KsAg&expire=1722318110&fvip=4&gir=yes&id=o-AC7iotZ_nCvg7C6fK7ofX174GXVOdwW68lsyXLLmCs0h&initcwndbps=1957500&ip=93.235.183.158&itag=298&keepalive=yes&lmt=1705967092637061&lmw=1&lsig=AGtxev0wRgIhANyFV4Ji7jlkXvfkb_czMQDZCiu6AbJ3Kzyv_s9V9WyvAiEA0o8XuM9kyh98hG1yg7h44L3I5OAUXuTpQdjxUaZ1V4A%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&mh=mQ&mime=video%2Fmp4&mm=31%2C29&mn=sn-h0jeenek%2Csn-h0jelnez&ms=au%2Crdu&mt=1722295996&mv=m&mvi=5&n=SWvqB0UTkUvifuM&ns=ZR8RwjQ3VJGDvQifdaM1IRMQ&pl=26&requiressl=yes&rqh=1&sefc=1&sig=AJfQdSswRgIhAIIGU41JunuODw9qIlSoYQcwkCYO6k9XOVlDn1Nxqnu7AiEAoiMOgYU8s8lp01fW0L86hHrSrtlvOLSI9XA50iyIGBc%3D&source=youtube&sparams=expire%2Cei%2Cip%2Cid%2Caitags%2Csource%2Crequiressl%2Cxpc%2Cbui%2Cvprv%2Csvpuc%2Cmime%2Cns%2Crqh%2Cgir%2Cclen%2Cdur%2Clmt&svpuc=1&txp=1308224&vprv=1&xpc=EgVo2aDSNQ%3D%3D",
|
||||
itag: 298,
|
||||
bitrate: 2188961,
|
||||
average_bitrate: 1694973,
|
||||
size: Some(34544823),
|
||||
index_range: Some(Range(
|
||||
start: 739,
|
||||
end: 1166,
|
||||
)),
|
||||
init_range: Some(Range(
|
||||
start: 0,
|
||||
end: 738,
|
||||
)),
|
||||
duration_ms: Some(163046),
|
||||
width: 1280,
|
||||
height: 720,
|
||||
fps: 60,
|
||||
quality: "720p60",
|
||||
hdr: false,
|
||||
mime: "video/mp4; codecs=\"avc1.4d4020\"",
|
||||
format: mp4,
|
||||
codec: avc1,
|
||||
),
|
||||
VideoStream(
|
||||
url: "https://rr5---sn-h0jeenek.googlevideo.com/videoplayback?aitags=133%2C134%2C135%2C136%2C160%2C242%2C243%2C244%2C247%2C278%2C298%2C299%2C302%2C303&bui=AXc671IvQBUNCtxNiAkj0M-Bvcb-N5cUu1XFk68f4Cnj0sFLEy1sixyW5lThzLYJXioG8kVQ2xT9KNLS&c=TVHTML5&clen=14723992&dur=163.029&ei=viioZtTdKteHi9oPl42KsAg&expire=1722318110&fvip=4&gir=yes&id=o-AC7iotZ_nCvg7C6fK7ofX174GXVOdwW68lsyXLLmCs0h&initcwndbps=1957500&ip=93.235.183.158&itag=247&keepalive=yes&lmt=1705966613897741&lmw=1&lsig=AGtxev0wRgIhANyFV4Ji7jlkXvfkb_czMQDZCiu6AbJ3Kzyv_s9V9WyvAiEA0o8XuM9kyh98hG1yg7h44L3I5OAUXuTpQdjxUaZ1V4A%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&mh=mQ&mime=video%2Fwebm&mm=31%2C29&mn=sn-h0jeenek%2Csn-h0jelnez&ms=au%2Crdu&mt=1722295996&mv=m&mvi=5&n=SWvqB0UTkUvifuM&ns=ZR8RwjQ3VJGDvQifdaM1IRMQ&pl=26&requiressl=yes&rqh=1&sefc=1&sig=AJfQdSswRgIhAL-upITxk7r9FQL5F4WL0A6SjPw673qyyzmXIC48eKfTAiEAlkdkx7IFYtehbhKakbffvIebpPXRtxSgBWLl7WEHCrE%3D&source=youtube&sparams=expire%2Cei%2Cip%2Cid%2Caitags%2Csource%2Crequiressl%2Cxpc%2Cbui%2Cvprv%2Csvpuc%2Cmime%2Cns%2Crqh%2Cgir%2Cclen%2Cdur%2Clmt&svpuc=1&txp=130F224&vprv=1&xpc=EgVo2aDSNQ%3D%3D",
|
||||
itag: 247,
|
||||
bitrate: 929607,
|
||||
average_bitrate: 722521,
|
||||
size: Some(14723992),
|
||||
index_range: Some(Range(
|
||||
start: 220,
|
||||
end: 770,
|
||||
)),
|
||||
init_range: Some(Range(
|
||||
start: 0,
|
||||
end: 219,
|
||||
)),
|
||||
duration_ms: Some(163029),
|
||||
width: 1280,
|
||||
height: 720,
|
||||
fps: 30,
|
||||
quality: "720p",
|
||||
hdr: false,
|
||||
mime: "video/webm; codecs=\"vp9\"",
|
||||
format: webm,
|
||||
codec: vp9,
|
||||
),
|
||||
VideoStream(
|
||||
url: "https://rr5---sn-h0jeenek.googlevideo.com/videoplayback?aitags=133%2C134%2C135%2C136%2C160%2C242%2C243%2C244%2C247%2C278%2C298%2C299%2C302%2C303&bui=AXc671IvQBUNCtxNiAkj0M-Bvcb-N5cUu1XFk68f4Cnj0sFLEy1sixyW5lThzLYJXioG8kVQ2xT9KNLS&c=TVHTML5&clen=30205331&dur=163.046&ei=viioZtTdKteHi9oPl42KsAg&expire=1722318110&fvip=4&gir=yes&id=o-AC7iotZ_nCvg7C6fK7ofX174GXVOdwW68lsyXLLmCs0h&initcwndbps=1957500&ip=93.235.183.158&itag=302&keepalive=yes&lmt=1705966545733919&lmw=1&lsig=AGtxev0wRgIhANyFV4Ji7jlkXvfkb_czMQDZCiu6AbJ3Kzyv_s9V9WyvAiEA0o8XuM9kyh98hG1yg7h44L3I5OAUXuTpQdjxUaZ1V4A%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&mh=mQ&mime=video%2Fwebm&mm=31%2C29&mn=sn-h0jeenek%2Csn-h0jelnez&ms=au%2Crdu&mt=1722295996&mv=m&mvi=5&n=SWvqB0UTkUvifuM&ns=ZR8RwjQ3VJGDvQifdaM1IRMQ&pl=26&requiressl=yes&rqh=1&sefc=1&sig=AJfQdSswRQIhAL428Az_BKxxff4FlH4WleHSy4Igq3mR71NuTMOc9xU3AiBN4lXfH9DklGaQUMnOT8wAhiMuzR73bW3cwr744TSoNA%3D%3D&source=youtube&sparams=expire%2Cei%2Cip%2Cid%2Caitags%2Csource%2Crequiressl%2Cxpc%2Cbui%2Cvprv%2Csvpuc%2Cmime%2Cns%2Crqh%2Cgir%2Cclen%2Cdur%2Clmt&svpuc=1&txp=130F224&vprv=1&xpc=EgVo2aDSNQ%3D%3D",
|
||||
itag: 302,
|
||||
bitrate: 2250391,
|
||||
average_bitrate: 1482051,
|
||||
size: Some(30205331),
|
||||
index_range: Some(Range(
|
||||
start: 219,
|
||||
end: 786,
|
||||
)),
|
||||
init_range: Some(Range(
|
||||
start: 0,
|
||||
end: 218,
|
||||
)),
|
||||
duration_ms: Some(163046),
|
||||
width: 1280,
|
||||
height: 720,
|
||||
fps: 60,
|
||||
quality: "720p60",
|
||||
hdr: false,
|
||||
mime: "video/webm; codecs=\"vp9\"",
|
||||
format: webm,
|
||||
codec: vp9,
|
||||
),
|
||||
VideoStream(
|
||||
url: "https://rr5---sn-h0jeenek.googlevideo.com/videoplayback?aitags=133%2C134%2C135%2C136%2C160%2C242%2C243%2C244%2C247%2C278%2C298%2C299%2C302%2C303&bui=AXc671IvQBUNCtxNiAkj0M-Bvcb-N5cUu1XFk68f4Cnj0sFLEy1sixyW5lThzLYJXioG8kVQ2xT9KNLS&c=TVHTML5&clen=62057888&dur=163.046&ei=viioZtTdKteHi9oPl42KsAg&expire=1722318110&fvip=4&gir=yes&id=o-AC7iotZ_nCvg7C6fK7ofX174GXVOdwW68lsyXLLmCs0h&initcwndbps=1957500&ip=93.235.183.158&itag=299&keepalive=yes&lmt=1705967093743693&lmw=1&lsig=AGtxev0wRgIhANyFV4Ji7jlkXvfkb_czMQDZCiu6AbJ3Kzyv_s9V9WyvAiEA0o8XuM9kyh98hG1yg7h44L3I5OAUXuTpQdjxUaZ1V4A%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&mh=mQ&mime=video%2Fmp4&mm=31%2C29&mn=sn-h0jeenek%2Csn-h0jelnez&ms=au%2Crdu&mt=1722295996&mv=m&mvi=5&n=SWvqB0UTkUvifuM&ns=ZR8RwjQ3VJGDvQifdaM1IRMQ&pl=26&requiressl=yes&rqh=1&sefc=1&sig=AJfQdSswRQIgBEemc0Cvd3KhNooNRblgX64_fjNSP30RmWDfFwDR7qYCIQCXpQ9FO0_X93ZHcyvRZCKX5gbJuusCReaRcJbRLFsM_g%3D%3D&source=youtube&sparams=expire%2Cei%2Cip%2Cid%2Caitags%2Csource%2Crequiressl%2Cxpc%2Cbui%2Cvprv%2Csvpuc%2Cmime%2Cns%2Crqh%2Cgir%2Cclen%2Cdur%2Clmt&svpuc=1&txp=1308224&vprv=1&xpc=EgVo2aDSNQ%3D%3D",
|
||||
itag: 299,
|
||||
bitrate: 3926810,
|
||||
average_bitrate: 3044926,
|
||||
size: Some(62057888),
|
||||
index_range: Some(Range(
|
||||
start: 740,
|
||||
end: 1167,
|
||||
)),
|
||||
init_range: Some(Range(
|
||||
start: 0,
|
||||
end: 739,
|
||||
)),
|
||||
duration_ms: Some(163046),
|
||||
width: 1920,
|
||||
height: 1080,
|
||||
fps: 60,
|
||||
quality: "1080p60",
|
||||
hdr: false,
|
||||
mime: "video/mp4; codecs=\"avc1.64002a\"",
|
||||
format: mp4,
|
||||
codec: avc1,
|
||||
),
|
||||
VideoStream(
|
||||
url: "https://rr5---sn-h0jeenek.googlevideo.com/videoplayback?aitags=133%2C134%2C135%2C136%2C160%2C242%2C243%2C244%2C247%2C278%2C298%2C299%2C302%2C303&bui=AXc671IvQBUNCtxNiAkj0M-Bvcb-N5cUu1XFk68f4Cnj0sFLEy1sixyW5lThzLYJXioG8kVQ2xT9KNLS&c=TVHTML5&clen=55300085&dur=163.046&ei=viioZtTdKteHi9oPl42KsAg&expire=1722318110&fvip=4&gir=yes&id=o-AC7iotZ_nCvg7C6fK7ofX174GXVOdwW68lsyXLLmCs0h&initcwndbps=1957500&ip=93.235.183.158&itag=303&keepalive=yes&lmt=1705966651743358&lmw=1&lsig=AGtxev0wRgIhANyFV4Ji7jlkXvfkb_czMQDZCiu6AbJ3Kzyv_s9V9WyvAiEA0o8XuM9kyh98hG1yg7h44L3I5OAUXuTpQdjxUaZ1V4A%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&mh=mQ&mime=video%2Fwebm&mm=31%2C29&mn=sn-h0jeenek%2Csn-h0jelnez&ms=au%2Crdu&mt=1722295996&mv=m&mvi=5&n=SWvqB0UTkUvifuM&ns=ZR8RwjQ3VJGDvQifdaM1IRMQ&pl=26&requiressl=yes&rqh=1&sefc=1&sig=AJfQdSswRQIgTZlmOcsLYJ_a9SnVLehXnaoajtreQO97qawEIDPEi8sCIQDKFdtBWWMuQUb9X8H-x92B3q-y0g8TvAPanR95cfklXQ%3D%3D&source=youtube&sparams=expire%2Cei%2Cip%2Cid%2Caitags%2Csource%2Crequiressl%2Cxpc%2Cbui%2Cvprv%2Csvpuc%2Cmime%2Cns%2Crqh%2Cgir%2Cclen%2Cdur%2Clmt&svpuc=1&txp=130F224&vprv=1&xpc=EgVo2aDSNQ%3D%3D",
|
||||
itag: 303,
|
||||
bitrate: 3473307,
|
||||
average_bitrate: 2713348,
|
||||
size: Some(55300085),
|
||||
index_range: Some(Range(
|
||||
start: 219,
|
||||
end: 792,
|
||||
)),
|
||||
init_range: Some(Range(
|
||||
start: 0,
|
||||
end: 218,
|
||||
)),
|
||||
duration_ms: Some(163046),
|
||||
width: 1920,
|
||||
height: 1080,
|
||||
fps: 60,
|
||||
quality: "1080p60",
|
||||
hdr: false,
|
||||
mime: "video/webm; codecs=\"vp9\"",
|
||||
format: webm,
|
||||
codec: vp9,
|
||||
),
|
||||
],
|
||||
audio_streams: [
|
||||
AudioStream(
|
||||
url: "https://rr5---sn-h0jeenek.googlevideo.com/videoplayback?bui=AXc671IvQBUNCtxNiAkj0M-Bvcb-N5cUu1XFk68f4Cnj0sFLEy1sixyW5lThzLYJXioG8kVQ2xT9KNLS&c=TVHTML5&clen=934750&dur=163.061&ei=viioZtTdKteHi9oPl42KsAg&expire=1722318110&fvip=4&gir=yes&id=o-AC7iotZ_nCvg7C6fK7ofX174GXVOdwW68lsyXLLmCs0h&initcwndbps=1957500&ip=93.235.183.158&itag=249&keepalive=yes&lmt=1714877357172339&lmw=1&lsig=AGtxev0wRgIhANyFV4Ji7jlkXvfkb_czMQDZCiu6AbJ3Kzyv_s9V9WyvAiEA0o8XuM9kyh98hG1yg7h44L3I5OAUXuTpQdjxUaZ1V4A%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&mh=mQ&mime=audio%2Fwebm&mm=31%2C29&mn=sn-h0jeenek%2Csn-h0jelnez&ms=au%2Crdu&mt=1722295996&mv=m&mvi=5&n=SWvqB0UTkUvifuM&ns=ZR8RwjQ3VJGDvQifdaM1IRMQ&pl=26&requiressl=yes&rqh=1&sefc=1&sig=AJfQdSswRQIhAItfaWkRs94vqyae7GR4M1xHoQO2lduvNRFugRSf0h-IAiA9fdLOJMwPI8vAO2C13igyv2qGSpOlKQptS4sN6p5Ffw%3D%3D&source=youtube&sparams=expire%2Cei%2Cip%2Cid%2Citag%2Csource%2Crequiressl%2Cxpc%2Cbui%2Cvprv%2Csvpuc%2Cmime%2Cns%2Crqh%2Cgir%2Cclen%2Cdur%2Clmt&svpuc=1&txp=1308224&vprv=1&xpc=EgVo2aDSNQ%3D%3D",
|
||||
itag: 249,
|
||||
bitrate: 53073,
|
||||
average_bitrate: 45860,
|
||||
size: 934750,
|
||||
index_range: Some(Range(
|
||||
start: 266,
|
||||
end: 551,
|
||||
)),
|
||||
init_range: Some(Range(
|
||||
start: 0,
|
||||
end: 265,
|
||||
)),
|
||||
duration_ms: Some(163061),
|
||||
mime: "audio/webm; codecs=\"opus\"",
|
||||
format: webm,
|
||||
codec: opus,
|
||||
channels: Some(2),
|
||||
loudness_db: Some(5.21),
|
||||
track: None,
|
||||
),
|
||||
AudioStream(
|
||||
url: "https://rr5---sn-h0jeenek.googlevideo.com/videoplayback?bui=AXc671IvQBUNCtxNiAkj0M-Bvcb-N5cUu1XFk68f4Cnj0sFLEy1sixyW5lThzLYJXioG8kVQ2xT9KNLS&c=TVHTML5&clen=1245582&dur=163.061&ei=viioZtTdKteHi9oPl42KsAg&expire=1722318110&fvip=4&gir=yes&id=o-AC7iotZ_nCvg7C6fK7ofX174GXVOdwW68lsyXLLmCs0h&initcwndbps=1957500&ip=93.235.183.158&itag=250&keepalive=yes&lmt=1714877466693058&lmw=1&lsig=AGtxev0wRgIhANyFV4Ji7jlkXvfkb_czMQDZCiu6AbJ3Kzyv_s9V9WyvAiEA0o8XuM9kyh98hG1yg7h44L3I5OAUXuTpQdjxUaZ1V4A%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&mh=mQ&mime=audio%2Fwebm&mm=31%2C29&mn=sn-h0jeenek%2Csn-h0jelnez&ms=au%2Crdu&mt=1722295996&mv=m&mvi=5&n=SWvqB0UTkUvifuM&ns=ZR8RwjQ3VJGDvQifdaM1IRMQ&pl=26&requiressl=yes&rqh=1&sefc=1&sig=AJfQdSswRQIgdJ1SjWwaloQecEblSIMFp2qFmpG_kKYZP1vX_M55dE0CIQCDSfa_FsaiFRcNL-1LRTgCIRSO7dj5vrpKR1Ya-KbmMw%3D%3D&source=youtube&sparams=expire%2Cei%2Cip%2Cid%2Citag%2Csource%2Crequiressl%2Cxpc%2Cbui%2Cvprv%2Csvpuc%2Cmime%2Cns%2Crqh%2Cgir%2Cclen%2Cdur%2Clmt&svpuc=1&txp=1308224&vprv=1&xpc=EgVo2aDSNQ%3D%3D",
|
||||
itag: 250,
|
||||
bitrate: 71197,
|
||||
average_bitrate: 61109,
|
||||
size: 1245582,
|
||||
index_range: Some(Range(
|
||||
start: 266,
|
||||
end: 551,
|
||||
)),
|
||||
init_range: Some(Range(
|
||||
start: 0,
|
||||
end: 265,
|
||||
)),
|
||||
duration_ms: Some(163061),
|
||||
mime: "audio/webm; codecs=\"opus\"",
|
||||
format: webm,
|
||||
codec: opus,
|
||||
channels: Some(2),
|
||||
loudness_db: Some(5.21),
|
||||
track: None,
|
||||
),
|
||||
AudioStream(
|
||||
url: "https://rr5---sn-h0jeenek.googlevideo.com/videoplayback?bui=AXc671IvQBUNCtxNiAkj0M-Bvcb-N5cUu1XFk68f4Cnj0sFLEy1sixyW5lThzLYJXioG8kVQ2xT9KNLS&c=TVHTML5&clen=2640283&dur=163.096&ei=viioZtTdKteHi9oPl42KsAg&expire=1722318110&fvip=4&gir=yes&id=o-AC7iotZ_nCvg7C6fK7ofX174GXVOdwW68lsyXLLmCs0h&initcwndbps=1957500&ip=93.235.183.158&itag=140&keepalive=yes&lmt=1705966477945761&lmw=1&lsig=AGtxev0wRgIhANyFV4Ji7jlkXvfkb_czMQDZCiu6AbJ3Kzyv_s9V9WyvAiEA0o8XuM9kyh98hG1yg7h44L3I5OAUXuTpQdjxUaZ1V4A%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&mh=mQ&mime=audio%2Fmp4&mm=31%2C29&mn=sn-h0jeenek%2Csn-h0jelnez&ms=au%2Crdu&mt=1722295996&mv=m&mvi=5&n=SWvqB0UTkUvifuM&ns=ZR8RwjQ3VJGDvQifdaM1IRMQ&pl=26&requiressl=yes&rqh=1&sefc=1&sig=AJfQdSswRAIgSxdbLrbojMVJcyRzsI2TrzOf78LN28bWcsHpbs4QXDwCIHidfXoriWMHfuiktUCdzLuUmksU7r5vITdh6u0puNmx&source=youtube&sparams=expire%2Cei%2Cip%2Cid%2Citag%2Csource%2Crequiressl%2Cxpc%2Cbui%2Cvprv%2Csvpuc%2Cmime%2Cns%2Crqh%2Cgir%2Cclen%2Cdur%2Clmt&svpuc=1&txp=1308224&vprv=1&xpc=EgVo2aDSNQ%3D%3D",
|
||||
itag: 140,
|
||||
bitrate: 130268,
|
||||
average_bitrate: 129508,
|
||||
size: 2640283,
|
||||
index_range: Some(Range(
|
||||
start: 632,
|
||||
end: 867,
|
||||
)),
|
||||
init_range: Some(Range(
|
||||
start: 0,
|
||||
end: 631,
|
||||
)),
|
||||
duration_ms: Some(163096),
|
||||
mime: "audio/mp4; codecs=\"mp4a.40.2\"",
|
||||
format: m4a,
|
||||
codec: mp4a,
|
||||
channels: Some(2),
|
||||
loudness_db: Some(5.2200003),
|
||||
track: None,
|
||||
),
|
||||
AudioStream(
|
||||
url: "https://rr5---sn-h0jeenek.googlevideo.com/videoplayback?bui=AXc671IvQBUNCtxNiAkj0M-Bvcb-N5cUu1XFk68f4Cnj0sFLEy1sixyW5lThzLYJXioG8kVQ2xT9KNLS&c=TVHTML5&clen=2480393&dur=163.061&ei=viioZtTdKteHi9oPl42KsAg&expire=1722318110&fvip=4&gir=yes&id=o-AC7iotZ_nCvg7C6fK7ofX174GXVOdwW68lsyXLLmCs0h&initcwndbps=1957500&ip=93.235.183.158&itag=251&keepalive=yes&lmt=1714877359450110&lmw=1&lsig=AGtxev0wRgIhANyFV4Ji7jlkXvfkb_czMQDZCiu6AbJ3Kzyv_s9V9WyvAiEA0o8XuM9kyh98hG1yg7h44L3I5OAUXuTpQdjxUaZ1V4A%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&mh=mQ&mime=audio%2Fwebm&mm=31%2C29&mn=sn-h0jeenek%2Csn-h0jelnez&ms=au%2Crdu&mt=1722295996&mv=m&mvi=5&n=SWvqB0UTkUvifuM&ns=ZR8RwjQ3VJGDvQifdaM1IRMQ&pl=26&requiressl=yes&rqh=1&sefc=1&sig=AJfQdSswRQIgO0jG-x2l6AF7tjryIX_oM3np78WgNDiseezppLfbQrgCIQCVLdpDhclKc8vQgWGzKXcqsAxgNl5S3MlLT8u1Jeok2A%3D%3D&source=youtube&sparams=expire%2Cei%2Cip%2Cid%2Citag%2Csource%2Crequiressl%2Cxpc%2Cbui%2Cvprv%2Csvpuc%2Cmime%2Cns%2Crqh%2Cgir%2Cclen%2Cdur%2Clmt&svpuc=1&txp=1308224&vprv=1&xpc=EgVo2aDSNQ%3D%3D",
|
||||
itag: 251,
|
||||
bitrate: 140833,
|
||||
average_bitrate: 121691,
|
||||
size: 2480393,
|
||||
index_range: Some(Range(
|
||||
start: 266,
|
||||
end: 551,
|
||||
)),
|
||||
init_range: Some(Range(
|
||||
start: 0,
|
||||
end: 265,
|
||||
)),
|
||||
duration_ms: Some(163061),
|
||||
mime: "audio/webm; codecs=\"opus\"",
|
||||
format: webm,
|
||||
codec: opus,
|
||||
channels: Some(2),
|
||||
loudness_db: Some(5.21),
|
||||
track: None,
|
||||
),
|
||||
],
|
||||
subtitles: [
|
||||
Subtitle(
|
||||
url: "https://www.youtube.com/api/timedtext?v=pPvd8UxmSbQ&ei=viioZtTdKteHi9oPl42KsAg&caps=asr&opi=112496729&exp=xbt&xoaf=5&hl=en&ip=0.0.0.0&ipbits=0&expire=1722321710&sparams=ip,ipbits,expire,v,ei,caps,opi,exp,xoaf&signature=7B002D0C2B79781E0E46F374D5BB53C6059A5252.E7B05ECC8D799DB96F3C21B727A0161E0032CDFA&key=yt8&lang=en",
|
||||
lang: "en",
|
||||
lang_name: "English",
|
||||
auto_generated: false,
|
||||
),
|
||||
],
|
||||
expires_in_seconds: 21540,
|
||||
hls_manifest_url: None,
|
||||
dash_manifest_url: None,
|
||||
preview_frames: [
|
||||
Frameset(
|
||||
url_template: "https://i.ytimg.com/sb/pPvd8UxmSbQ/storyboard3_L0/default.jpg?sqp=-oaymwGbA0g48quKqQOSA4gBAZUBAAAEQpgBMqABPKgBBLABELABDbABDLABELABFbABH7ABJrABLbABDrABDrABD7ABErABF7ABK7ABLLABKbABD7ABDrABELABFbABH7ABKrABMrABKbABD7ABEbABFLABGLABJrABPbABOLABLbABEbABFLABHrABKrABMbABS7ABR7ABNrABFbABHLABKbABLrABObABR7ABTbABP7ABJbABLrABN7ABPbABR7ABUrABUbABRbABM7ABQLABQrABQ7ABTLABRLABRrABQ7gBEbgBEbgBFbgBI7gBRLgBQ7gBQ7gBQ7gBEbgBE7gBFrgBL7gBQ7gBQ7gBQ7gBQ7gBFbgBFrgBKbgBQ7gBQ7gBQ7gBQ7gBQ7gBI7gBL7gBQ7gBQ7gBQ7gBQ7gBQ7gBQ7gBRLgBQ7gBQ7gBQ7gBQ7gBQ7gBQrgBQrgBQ7gBQ7gBQ7gBQ7gBQ7gBQrgBQrgBQrgBQ7gBQ7gBQ7gBQ7gBQrgBQrgBQrgBQrgBQ7gBQ7gBQ7gBQrgBQrgBQrgBQrgBQqLzl_8DBgjf8LPxBQ==&sigh=rs$AOn4CLCsCT8Lprh2S0ptmCRsWH7VtDl3YQ",
|
||||
frame_width: 48,
|
||||
frame_height: 27,
|
||||
page_count: 1,
|
||||
total_count: 100,
|
||||
duration_per_frame: 0,
|
||||
frames_per_page_x: 10,
|
||||
frames_per_page_y: 10,
|
||||
),
|
||||
Frameset(
|
||||
url_template: "https://i.ytimg.com/sb/pPvd8UxmSbQ/storyboard3_L1/M$M.jpg?sqp=-oaymwGbA0g48quKqQOSA4gBAZUBAAAEQpgBMqABPKgBBLABELABDbABDLABELABFbABH7ABJrABLbABDrABDrABD7ABErABF7ABK7ABLLABKbABD7ABDrABELABFbABH7ABKrABMrABKbABD7ABEbABFLABGLABJrABPbABOLABLbABEbABFLABHrABKrABMbABS7ABR7ABNrABFbABHLABKbABLrABObABR7ABTbABP7ABJbABLrABN7ABPbABR7ABUrABUbABRbABM7ABQLABQrABQ7ABTLABRLABRrABQ7gBEbgBEbgBFbgBI7gBRLgBQ7gBQ7gBQ7gBEbgBE7gBFrgBL7gBQ7gBQ7gBQ7gBQ7gBFbgBFrgBKbgBQ7gBQ7gBQ7gBQ7gBQ7gBI7gBL7gBQ7gBQ7gBQ7gBQ7gBQ7gBQ7gBRLgBQ7gBQ7gBQ7gBQ7gBQ7gBQrgBQrgBQ7gBQ7gBQ7gBQ7gBQ7gBQrgBQrgBQrgBQ7gBQ7gBQ7gBQ7gBQrgBQrgBQrgBQrgBQ7gBQ7gBQ7gBQrgBQrgBQrgBQrgBQqLzl_8DBgjf8LPxBQ==&sigh=rs$AOn4CLBXrdgfuYV1WLnTGXqZtSAUm8oZCA",
|
||||
frame_width: 80,
|
||||
frame_height: 45,
|
||||
page_count: 1,
|
||||
total_count: 83,
|
||||
duration_per_frame: 2000,
|
||||
frames_per_page_x: 10,
|
||||
frames_per_page_y: 10,
|
||||
),
|
||||
Frameset(
|
||||
url_template: "https://i.ytimg.com/sb/pPvd8UxmSbQ/storyboard3_L2/M$M.jpg?sqp=-oaymwGbA0g48quKqQOSA4gBAZUBAAAEQpgBMqABPKgBBLABELABDbABDLABELABFbABH7ABJrABLbABDrABDrABD7ABErABF7ABK7ABLLABKbABD7ABDrABELABFbABH7ABKrABMrABKbABD7ABEbABFLABGLABJrABPbABOLABLbABEbABFLABHrABKrABMbABS7ABR7ABNrABFbABHLABKbABLrABObABR7ABTbABP7ABJbABLrABN7ABPbABR7ABUrABUbABRbABM7ABQLABQrABQ7ABTLABRLABRrABQ7gBEbgBEbgBFbgBI7gBRLgBQ7gBQ7gBQ7gBEbgBE7gBFrgBL7gBQ7gBQ7gBQ7gBQ7gBFbgBFrgBKbgBQ7gBQ7gBQ7gBQ7gBQ7gBI7gBL7gBQ7gBQ7gBQ7gBQ7gBQ7gBQ7gBRLgBQ7gBQ7gBQ7gBQ7gBQ7gBQrgBQrgBQ7gBQ7gBQ7gBQ7gBQ7gBQrgBQrgBQrgBQ7gBQ7gBQ7gBQ7gBQrgBQrgBQrgBQrgBQ7gBQ7gBQ7gBQrgBQrgBQrgBQrgBQqLzl_8DBgjf8LPxBQ==&sigh=rs$AOn4CLCRazj84zMuwJLaCCc_PiUakX_YdQ",
|
||||
frame_width: 160,
|
||||
frame_height: 90,
|
||||
page_count: 4,
|
||||
total_count: 83,
|
||||
duration_per_frame: 2000,
|
||||
frames_per_page_x: 5,
|
||||
frames_per_page_y: 5,
|
||||
),
|
||||
],
|
||||
client_type: tv,
|
||||
visitor_data: Some("CgtrbXRsWU4wUEtXbyi-0aC1BjIKCgJERRIEEgAgZg%3D%3D"),
|
||||
)
|
|
@ -5,7 +5,7 @@ expression: map_res.c
|
|||
VideoPlayer(
|
||||
details: VideoPlayerDetails(
|
||||
id: "pPvd8UxmSbQ",
|
||||
name: "Inspiring Cinematic Uplifting (Creative Commons)",
|
||||
name: Some("Inspiring Cinematic Uplifting (Creative Commons)"),
|
||||
description: Some("► Download Music: http://bit.ly/2QLufeh\nImportant to know! You can download this track for free through Patreon. You will pay only for new tracks! So join others and let\'s make next track together!\n\n► MORE MUSIC: Become my patron and get access to all our music from Patreon library. More Info here: http://bit.ly/2JJDFHb\n\n► Additional edit versions of this track you can download here: http://bit.ly/2WdRinT (5 versions)\n--------------------- \n\n►DESCRIPTION:\nInspiring Cinematic Uplifting Trailer Background - epic music for trailer video project with powerful drums, energetic orchestra and gentle piano melody. This motivational cinematic theme will work as perfect background for beautiful epic moments, landscapes, nature, drone video, motivational products and achievements.\n--------------------- \n\n► LICENSE:\n● If you need a license for your project, you can purchase it here: \nhttps://1.envato.market/ajicu (Audiojungle)\nhttps://bit.ly/3fWZZuI (Pond5)\n--------------------- \n\n► LISTEN ON:\n● Spotify - https://spoti.fi/2sHm3UH\n● Apple Music - https://apple.co/3qBjbUO\n--------------------- \n\n► SUBSCRIBE FOR MORE: \nPatreon: http://bit.ly/2JJDFHb\nYoutube: http://bit.ly/2AYBzfA\nFacebook: http://bit.ly/2T6dTx5\nInstagram: http://bit.ly/2BHJ8rB\nTwitter: http://bit.ly/2MwtOlT\nSoundCloud: http://bit.ly/2IwVVmt\nAudiojungle: https://1.envato.market/ajrsm\nPond5: https://bit.ly/2TLi1rW\n--------------------- \n►Photo by Vittorio Staffolani from Pexels\n--------------------- \n\nFAQ:\n\n► Can I use this music in my videos? \n● Sure! Just download this track and you are ready to use it! We only ask to credit us. \n-------------------- \n\n► What is \"Creative Commons\"? \nCreative Commons is a system that allows you to legally use “some rights reserved” music, movies, images, and other content — all for free. Licensees may copy, distribute, display and perform the work and make derivative works and remixes based on it only if they give the author or licensor the credits.\n-------------------- \n\n► Will I have any copyright issues with this track?\n● No, you should not have any copyright problems with this track!\n-------------------- \n\n► Is it necessary to become your patron?\n● No it\'s not necessary. But we recommend you to become our patron because you will get access to huge library of music. You will download only highest quality files. You will find additional edited versions of every track. You always be tuned with our news. You will find music not only from Roman Senyk but also from another talented authors.\n-------------------- \n\n► Why I received a copyright claim when I used this track?\n● Do not panic! This is very common situation. Content ID fingerprint system can mismatch our music. Just dispute the claim by showing our original track. Or send us the link to your video (romansenykmusic@gmail.com) and attach some screenshot with claim information. Claim will be released until 24 hours!\n\n► How to credit you in my video?\n● Just add to the description of your project information about Author, Name of Song and the link to our original track. Or copy and paste:\n\nMusic Info: Inspiring Cinematic Uplifting by RomanSenykMusic.\nMusic Link: https://youtu.be/pPvd8UxmSbQ\n--------------------- \n\n► If you have any questions, you can write in the comments for this video or by email: romansenykmusic@gmail.com\n--------------------- \n\nStay tuned! The best is yet to come! \nThanks For Listening!\nRoman Senyk"),
|
||||
duration: 163,
|
||||
thumbnail: [
|
||||
|
@ -35,11 +35,9 @@ VideoPlayer(
|
|||
height: 1080,
|
||||
),
|
||||
],
|
||||
channel: ChannelId(
|
||||
id: "UCbxxEi-ImPlbLx5F-fHetEg",
|
||||
name: "RomanSenykMusic - Royalty Free Music",
|
||||
),
|
||||
view_count: 426567,
|
||||
channel_id: "UCbxxEi-ImPlbLx5F-fHetEg",
|
||||
channel_name: Some("RomanSenykMusic - Royalty Free Music"),
|
||||
view_count: Some(426567),
|
||||
keywords: [
|
||||
"no copyright music",
|
||||
"background music",
|
||||
|
@ -84,7 +82,6 @@ VideoPlayer(
|
|||
mime: "video/mp4; codecs=\"avc1.42001E, mp4a.40.2\"",
|
||||
format: mp4,
|
||||
codec: avc1,
|
||||
throttled: false,
|
||||
),
|
||||
],
|
||||
video_only_streams: [
|
||||
|
@ -111,7 +108,6 @@ VideoPlayer(
|
|||
mime: "video/webm; codecs=\"vp9\"",
|
||||
format: webm,
|
||||
codec: vp9,
|
||||
throttled: false,
|
||||
),
|
||||
VideoStream(
|
||||
url: "https://rr4---sn-h0jelnez.googlevideo.com/videoplayback?aitags=133%2C134%2C135%2C136%2C160%2C242%2C243%2C244%2C247%2C278%2C298%2C299%2C302%2C303%2C394%2C395%2C396%2C397%2C398%2C399&c=TVHTML5_SIMPLY_EMBEDDED_PLAYER&clen=1224002&dur=163.029&ei=q1jpYv-eJ9uF6dsPhvyH8As&expire=1659481355&fexp=24001373%2C24007246&fvip=5&gir=yes&id=o-AKkOKYSoYWWfNLdrt3aQbxbIwHh4mMVyXLb2mtjc0uXQ&initcwndbps=1527500&ip=2003%3Ade%3Aaf0e%3A2f00%3Ade47%3A297%3Aa6db%3A774e&itag=394&keepalive=yes&lmt=1608045375671513&lsig=AG3C_xAwRgIhAIWRo8U-MB6jhlipPec3A3m5-StMaX64EEGBEE3LWaNiAiEA_8QPrTStO0ISMA5Jex-G2JfPpqyw-vltC8nAFXyPz98%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&mh=mQ&mime=video%2Fmp4&mm=31%2C29&mn=sn-h0jelnez%2Csn-h0jeenek&ms=au%2Crdu&mt=1659459429&mv=m&mvi=4&n=U0g9MK69PQnuYQ&ns=h4nQ-PcvhL4hLZwTU9i7QAoH&otfp=1&pcm2=yes&pl=37&rbqsm=fr&requiressl=yes&sig=AOq0QJ8wRQIhAKyA5SE5VppKcNlosTsDsa4s039Ia-Qymp9zS3hAlScmAiBzo8tirHhDQVcMHejguHQ3F5rglFmjjy1hFlopVpNe-A%3D%3D&source=youtube&sparams=expire%2Cei%2Cip%2Cid%2Caitags%2Csource%2Crequiressl%2Cpcm2%2Cvprv%2Cmime%2Cns%2Cgir%2Cclen%2Cotfp%2Cdur%2Clmt&txp=1311222&vprv=1",
|
||||
|
@ -136,7 +132,6 @@ VideoPlayer(
|
|||
mime: "video/mp4; codecs=\"av01.0.00M.08\"",
|
||||
format: mp4,
|
||||
codec: av01,
|
||||
throttled: false,
|
||||
),
|
||||
VideoStream(
|
||||
url: "https://rr4---sn-h0jelnez.googlevideo.com/videoplayback?aitags=133%2C134%2C135%2C136%2C160%2C242%2C243%2C244%2C247%2C278%2C298%2C299%2C302%2C303%2C394%2C395%2C396%2C397%2C398%2C399&c=TVHTML5_SIMPLY_EMBEDDED_PLAYER&clen=2973283&dur=163.029&ei=q1jpYv-eJ9uF6dsPhvyH8As&expire=1659481355&fexp=24001373%2C24007246&fvip=5&gir=yes&id=o-AKkOKYSoYWWfNLdrt3aQbxbIwHh4mMVyXLb2mtjc0uXQ&initcwndbps=1527500&ip=2003%3Ade%3Aaf0e%3A2f00%3Ade47%3A297%3Aa6db%3A774e&itag=242&keepalive=yes&lmt=1608509388282028&lsig=AG3C_xAwRgIhAIWRo8U-MB6jhlipPec3A3m5-StMaX64EEGBEE3LWaNiAiEA_8QPrTStO0ISMA5Jex-G2JfPpqyw-vltC8nAFXyPz98%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&mh=mQ&mime=video%2Fwebm&mm=31%2C29&mn=sn-h0jelnez%2Csn-h0jeenek&ms=au%2Crdu&mt=1659459429&mv=m&mvi=4&n=U0g9MK69PQnuYQ&ns=h4nQ-PcvhL4hLZwTU9i7QAoH&otfp=1&pcm2=yes&pl=37&rbqsm=fr&requiressl=yes&sig=AOq0QJ8wRAIgN7FPp-_Ay_e78kvW7bcBceUhHDnpgXSZKxxn-x34DTgCIEqr4KN5E3R9ZVzCFV3HGaTr6YZEGeNDRxS4ne7JFDRN&source=youtube&sparams=expire%2Cei%2Cip%2Cid%2Caitags%2Csource%2Crequiressl%2Cpcm2%2Cvprv%2Cmime%2Cns%2Cgir%2Cclen%2Cotfp%2Cdur%2Clmt&txp=1311222&vprv=1",
|
||||
|
@ -161,7 +156,6 @@ VideoPlayer(
|
|||
mime: "video/webm; codecs=\"vp9\"",
|
||||
format: webm,
|
||||
codec: vp9,
|
||||
throttled: false,
|
||||
),
|
||||
VideoStream(
|
||||
url: "https://rr4---sn-h0jelnez.googlevideo.com/videoplayback?aitags=133%2C134%2C135%2C136%2C160%2C242%2C243%2C244%2C247%2C278%2C298%2C299%2C302%2C303%2C394%2C395%2C396%2C397%2C398%2C399&c=TVHTML5_SIMPLY_EMBEDDED_PLAYER&clen=2238952&dur=163.029&ei=q1jpYv-eJ9uF6dsPhvyH8As&expire=1659481355&fexp=24001373%2C24007246&fvip=5&gir=yes&id=o-AKkOKYSoYWWfNLdrt3aQbxbIwHh4mMVyXLb2mtjc0uXQ&initcwndbps=1527500&ip=2003%3Ade%3Aaf0e%3A2f00%3Ade47%3A297%3Aa6db%3A774e&itag=395&keepalive=yes&lmt=1608045728968690&lsig=AG3C_xAwRgIhAIWRo8U-MB6jhlipPec3A3m5-StMaX64EEGBEE3LWaNiAiEA_8QPrTStO0ISMA5Jex-G2JfPpqyw-vltC8nAFXyPz98%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&mh=mQ&mime=video%2Fmp4&mm=31%2C29&mn=sn-h0jelnez%2Csn-h0jeenek&ms=au%2Crdu&mt=1659459429&mv=m&mvi=4&n=U0g9MK69PQnuYQ&ns=h4nQ-PcvhL4hLZwTU9i7QAoH&otfp=1&pcm2=yes&pl=37&rbqsm=fr&requiressl=yes&sig=AOq0QJ8wRQIhAKBPl7ZiI0t6SteLZUEX96zhu1FVKBLZz6GP-_6K-nJMAiBcWq7zKq-fNeSJbMaGcrgU8tshLKzNu2Mv0b1pFrPbMw%3D%3D&source=youtube&sparams=expire%2Cei%2Cip%2Cid%2Caitags%2Csource%2Crequiressl%2Cpcm2%2Cvprv%2Cmime%2Cns%2Cgir%2Cclen%2Cotfp%2Cdur%2Clmt&txp=1311222&vprv=1",
|
||||
|
@ -186,7 +180,6 @@ VideoPlayer(
|
|||
mime: "video/mp4; codecs=\"av01.0.00M.08\"",
|
||||
format: mp4,
|
||||
codec: av01,
|
||||
throttled: false,
|
||||
),
|
||||
VideoStream(
|
||||
url: "https://rr4---sn-h0jelnez.googlevideo.com/videoplayback?aitags=133%2C134%2C135%2C136%2C160%2C242%2C243%2C244%2C247%2C278%2C298%2C299%2C302%2C303%2C394%2C395%2C396%2C397%2C398%2C399&c=TVHTML5_SIMPLY_EMBEDDED_PLAYER&clen=7808990&dur=163.029&ei=q1jpYv-eJ9uF6dsPhvyH8As&expire=1659481355&fexp=24001373%2C24007246&fvip=5&gir=yes&id=o-AKkOKYSoYWWfNLdrt3aQbxbIwHh4mMVyXLb2mtjc0uXQ&initcwndbps=1527500&ip=2003%3Ade%3Aaf0e%3A2f00%3Ade47%3A297%3Aa6db%3A774e&itag=134&keepalive=yes&lmt=1580005649163759&lsig=AG3C_xAwRgIhAIWRo8U-MB6jhlipPec3A3m5-StMaX64EEGBEE3LWaNiAiEA_8QPrTStO0ISMA5Jex-G2JfPpqyw-vltC8nAFXyPz98%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&mh=mQ&mime=video%2Fmp4&mm=31%2C29&mn=sn-h0jelnez%2Csn-h0jeenek&ms=au%2Crdu&mt=1659459429&mv=m&mvi=4&n=U0g9MK69PQnuYQ&ns=h4nQ-PcvhL4hLZwTU9i7QAoH&otfp=1&pcm2=yes&pl=37&rbqsm=fr&requiressl=yes&sig=AOq0QJ8wRAIgLnuMRsG-Huz0E9KzrpsLbN8akn6slETHnYESZLtoJXgCIFXPrk4JyA2KRZnD8EVn7c1JRqFNUV1acExNy0Z6wfeX&source=youtube&sparams=expire%2Cei%2Cip%2Cid%2Caitags%2Csource%2Crequiressl%2Cpcm2%2Cvprv%2Cmime%2Cns%2Cgir%2Cclen%2Cotfp%2Cdur%2Clmt&txp=2211222&vprv=1",
|
||||
|
@ -211,7 +204,6 @@ VideoPlayer(
|
|||
mime: "video/mp4; codecs=\"avc1.4d401e\"",
|
||||
format: mp4,
|
||||
codec: avc1,
|
||||
throttled: false,
|
||||
),
|
||||
VideoStream(
|
||||
url: "https://rr4---sn-h0jelnez.googlevideo.com/videoplayback?aitags=133%2C134%2C135%2C136%2C160%2C242%2C243%2C244%2C247%2C278%2C298%2C299%2C302%2C303%2C394%2C395%2C396%2C397%2C398%2C399&c=TVHTML5_SIMPLY_EMBEDDED_PLAYER&clen=5169510&dur=163.029&ei=q1jpYv-eJ9uF6dsPhvyH8As&expire=1659481355&fexp=24001373%2C24007246&fvip=5&gir=yes&id=o-AKkOKYSoYWWfNLdrt3aQbxbIwHh4mMVyXLb2mtjc0uXQ&initcwndbps=1527500&ip=2003%3Ade%3Aaf0e%3A2f00%3Ade47%3A297%3Aa6db%3A774e&itag=243&keepalive=yes&lmt=1608509388282405&lsig=AG3C_xAwRgIhAIWRo8U-MB6jhlipPec3A3m5-StMaX64EEGBEE3LWaNiAiEA_8QPrTStO0ISMA5Jex-G2JfPpqyw-vltC8nAFXyPz98%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&mh=mQ&mime=video%2Fwebm&mm=31%2C29&mn=sn-h0jelnez%2Csn-h0jeenek&ms=au%2Crdu&mt=1659459429&mv=m&mvi=4&n=U0g9MK69PQnuYQ&ns=h4nQ-PcvhL4hLZwTU9i7QAoH&otfp=1&pcm2=yes&pl=37&rbqsm=fr&requiressl=yes&sig=AOq0QJ8wRgIhANJoH9RPIFwd08jukBbSBYSH-gmli5NIdZRVDZD8StFiAiEAtjCXNscOn1rgndc2QQQYV97sWCCYPwWvO0tgkUjRm74%3D&source=youtube&sparams=expire%2Cei%2Cip%2Cid%2Caitags%2Csource%2Crequiressl%2Cpcm2%2Cvprv%2Cmime%2Cns%2Cgir%2Cclen%2Cotfp%2Cdur%2Clmt&txp=1311222&vprv=1",
|
||||
|
@ -236,7 +228,6 @@ VideoPlayer(
|
|||
mime: "video/webm; codecs=\"vp9\"",
|
||||
format: webm,
|
||||
codec: vp9,
|
||||
throttled: false,
|
||||
),
|
||||
VideoStream(
|
||||
url: "https://rr4---sn-h0jelnez.googlevideo.com/videoplayback?aitags=133%2C134%2C135%2C136%2C160%2C242%2C243%2C244%2C247%2C278%2C298%2C299%2C302%2C303%2C394%2C395%2C396%2C397%2C398%2C399&c=TVHTML5_SIMPLY_EMBEDDED_PLAYER&clen=4130385&dur=163.029&ei=q1jpYv-eJ9uF6dsPhvyH8As&expire=1659481355&fexp=24001373%2C24007246&fvip=5&gir=yes&id=o-AKkOKYSoYWWfNLdrt3aQbxbIwHh4mMVyXLb2mtjc0uXQ&initcwndbps=1527500&ip=2003%3Ade%3Aaf0e%3A2f00%3Ade47%3A297%3Aa6db%3A774e&itag=396&keepalive=yes&lmt=1608045761576250&lsig=AG3C_xAwRgIhAIWRo8U-MB6jhlipPec3A3m5-StMaX64EEGBEE3LWaNiAiEA_8QPrTStO0ISMA5Jex-G2JfPpqyw-vltC8nAFXyPz98%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&mh=mQ&mime=video%2Fmp4&mm=31%2C29&mn=sn-h0jelnez%2Csn-h0jeenek&ms=au%2Crdu&mt=1659459429&mv=m&mvi=4&n=U0g9MK69PQnuYQ&ns=h4nQ-PcvhL4hLZwTU9i7QAoH&otfp=1&pcm2=yes&pl=37&rbqsm=fr&requiressl=yes&sig=AOq0QJ8wRQIgcVEF2GELVbjio4lbmnBkFmi2HT4gkRQyM-SU3Tv-bMgCIQDs8WhxxNLSj3K-0ccvv6wzpWweOuwhdj9hjCXa0-9PnQ%3D%3D&source=youtube&sparams=expire%2Cei%2Cip%2Cid%2Caitags%2Csource%2Crequiressl%2Cpcm2%2Cvprv%2Cmime%2Cns%2Cgir%2Cclen%2Cotfp%2Cdur%2Clmt&txp=1311222&vprv=1",
|
||||
|
@ -261,7 +252,6 @@ VideoPlayer(
|
|||
mime: "video/mp4; codecs=\"av01.0.01M.08\"",
|
||||
format: mp4,
|
||||
codec: av01,
|
||||
throttled: false,
|
||||
),
|
||||
VideoStream(
|
||||
url: "https://rr4---sn-h0jelnez.googlevideo.com/videoplayback?aitags=133%2C134%2C135%2C136%2C160%2C242%2C243%2C244%2C247%2C278%2C298%2C299%2C302%2C303%2C394%2C395%2C396%2C397%2C398%2C399&c=TVHTML5_SIMPLY_EMBEDDED_PLAYER&clen=8890590&dur=163.029&ei=q1jpYv-eJ9uF6dsPhvyH8As&expire=1659481355&fexp=24001373%2C24007246&fvip=5&gir=yes&id=o-AKkOKYSoYWWfNLdrt3aQbxbIwHh4mMVyXLb2mtjc0uXQ&initcwndbps=1527500&ip=2003%3Ade%3Aaf0e%3A2f00%3Ade47%3A297%3Aa6db%3A774e&itag=244&keepalive=yes&lmt=1608509388284632&lsig=AG3C_xAwRgIhAIWRo8U-MB6jhlipPec3A3m5-StMaX64EEGBEE3LWaNiAiEA_8QPrTStO0ISMA5Jex-G2JfPpqyw-vltC8nAFXyPz98%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&mh=mQ&mime=video%2Fwebm&mm=31%2C29&mn=sn-h0jelnez%2Csn-h0jeenek&ms=au%2Crdu&mt=1659459429&mv=m&mvi=4&n=U0g9MK69PQnuYQ&ns=h4nQ-PcvhL4hLZwTU9i7QAoH&otfp=1&pcm2=yes&pl=37&rbqsm=fr&requiressl=yes&sig=AOq0QJ8wRAIgEC-9_1jHyfgc_Vtpe7vuWTJYd2S_MrJaSDfYfx8cCQcCIEIPWqkLyLh3yLlAM-ZPpySBXCS9Z9Hs1Mk_dVLsnBhY&source=youtube&sparams=expire%2Cei%2Cip%2Cid%2Caitags%2Csource%2Crequiressl%2Cpcm2%2Cvprv%2Cmime%2Cns%2Cgir%2Cclen%2Cotfp%2Cdur%2Clmt&txp=1311222&vprv=1",
|
||||
|
@ -286,7 +276,6 @@ VideoPlayer(
|
|||
mime: "video/webm; codecs=\"vp9\"",
|
||||
format: webm,
|
||||
codec: vp9,
|
||||
throttled: false,
|
||||
),
|
||||
VideoStream(
|
||||
url: "https://rr4---sn-h0jelnez.googlevideo.com/videoplayback?aitags=133%2C134%2C135%2C136%2C160%2C242%2C243%2C244%2C247%2C278%2C298%2C299%2C302%2C303%2C394%2C395%2C396%2C397%2C398%2C399&c=TVHTML5_SIMPLY_EMBEDDED_PLAYER&clen=6873325&dur=163.029&ei=q1jpYv-eJ9uF6dsPhvyH8As&expire=1659481355&fexp=24001373%2C24007246&fvip=5&gir=yes&id=o-AKkOKYSoYWWfNLdrt3aQbxbIwHh4mMVyXLb2mtjc0uXQ&initcwndbps=1527500&ip=2003%3Ade%3Aaf0e%3A2f00%3Ade47%3A297%3Aa6db%3A774e&itag=397&keepalive=yes&lmt=1608045990917419&lsig=AG3C_xAwRgIhAIWRo8U-MB6jhlipPec3A3m5-StMaX64EEGBEE3LWaNiAiEA_8QPrTStO0ISMA5Jex-G2JfPpqyw-vltC8nAFXyPz98%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&mh=mQ&mime=video%2Fmp4&mm=31%2C29&mn=sn-h0jelnez%2Csn-h0jeenek&ms=au%2Crdu&mt=1659459429&mv=m&mvi=4&n=U0g9MK69PQnuYQ&ns=h4nQ-PcvhL4hLZwTU9i7QAoH&otfp=1&pcm2=yes&pl=37&rbqsm=fr&requiressl=yes&sig=AOq0QJ8wRgIhAK8Grn-QuhjptRGaHT2NYU97O15VoIXwX0EYKhl4FIFIAiEA9152IGHn7QbRCGRfk1Q0Yqfpr9Hhjp-u4e8L8vhuXtk%3D&source=youtube&sparams=expire%2Cei%2Cip%2Cid%2Caitags%2Csource%2Crequiressl%2Cpcm2%2Cvprv%2Cmime%2Cns%2Cgir%2Cclen%2Cotfp%2Cdur%2Clmt&txp=1311222&vprv=1",
|
||||
|
@ -311,7 +300,6 @@ VideoPlayer(
|
|||
mime: "video/mp4; codecs=\"av01.0.04M.08\"",
|
||||
format: mp4,
|
||||
codec: av01,
|
||||
throttled: false,
|
||||
),
|
||||
VideoStream(
|
||||
url: "https://rr4---sn-h0jelnez.googlevideo.com/videoplayback?aitags=133%2C134%2C135%2C136%2C160%2C242%2C243%2C244%2C247%2C278%2C298%2C299%2C302%2C303%2C394%2C395%2C396%2C397%2C398%2C399&c=TVHTML5_SIMPLY_EMBEDDED_PLAYER&clen=16547577&dur=163.029&ei=q1jpYv-eJ9uF6dsPhvyH8As&expire=1659481355&fexp=24001373%2C24007246&fvip=5&gir=yes&id=o-AKkOKYSoYWWfNLdrt3aQbxbIwHh4mMVyXLb2mtjc0uXQ&initcwndbps=1527500&ip=2003%3Ade%3Aaf0e%3A2f00%3Ade47%3A297%3Aa6db%3A774e&itag=247&keepalive=yes&lmt=1608509388326822&lsig=AG3C_xAwRgIhAIWRo8U-MB6jhlipPec3A3m5-StMaX64EEGBEE3LWaNiAiEA_8QPrTStO0ISMA5Jex-G2JfPpqyw-vltC8nAFXyPz98%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&mh=mQ&mime=video%2Fwebm&mm=31%2C29&mn=sn-h0jelnez%2Csn-h0jeenek&ms=au%2Crdu&mt=1659459429&mv=m&mvi=4&n=U0g9MK69PQnuYQ&ns=h4nQ-PcvhL4hLZwTU9i7QAoH&otfp=1&pcm2=yes&pl=37&rbqsm=fr&requiressl=yes&sig=AOq0QJ8wRQIgFVGnmP4_M__D1Lga0s1av1aEBTmW54m9NdJY5I88xaECIQDMMIOCWFm-Aje4sHxWihE_tFpg1qrfS0qlbGRtouR1zA%3D%3D&source=youtube&sparams=expire%2Cei%2Cip%2Cid%2Caitags%2Csource%2Crequiressl%2Cpcm2%2Cvprv%2Cmime%2Cns%2Cgir%2Cclen%2Cotfp%2Cdur%2Clmt&txp=1311222&vprv=1",
|
||||
|
@ -336,7 +324,6 @@ VideoPlayer(
|
|||
mime: "video/webm; codecs=\"vp9\"",
|
||||
format: webm,
|
||||
codec: vp9,
|
||||
throttled: false,
|
||||
),
|
||||
VideoStream(
|
||||
url: "https://rr4---sn-h0jelnez.googlevideo.com/videoplayback?aitags=133%2C134%2C135%2C136%2C160%2C242%2C243%2C244%2C247%2C278%2C298%2C299%2C302%2C303%2C394%2C395%2C396%2C397%2C398%2C399&c=TVHTML5_SIMPLY_EMBEDDED_PLAYER&clen=35955780&dur=163.046&ei=q1jpYv-eJ9uF6dsPhvyH8As&expire=1659481355&fexp=24001373%2C24007246&fvip=5&gir=yes&id=o-AKkOKYSoYWWfNLdrt3aQbxbIwHh4mMVyXLb2mtjc0uXQ&initcwndbps=1527500&ip=2003%3Ade%3Aaf0e%3A2f00%3Ade47%3A297%3Aa6db%3A774e&itag=302&keepalive=yes&lmt=1608509234088626&lsig=AG3C_xAwRgIhAIWRo8U-MB6jhlipPec3A3m5-StMaX64EEGBEE3LWaNiAiEA_8QPrTStO0ISMA5Jex-G2JfPpqyw-vltC8nAFXyPz98%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&mh=mQ&mime=video%2Fwebm&mm=31%2C29&mn=sn-h0jelnez%2Csn-h0jeenek&ms=au%2Crdu&mt=1659459429&mv=m&mvi=4&n=U0g9MK69PQnuYQ&ns=h4nQ-PcvhL4hLZwTU9i7QAoH&otfp=1&pcm2=yes&pl=37&rbqsm=fr&requiressl=yes&sig=AOq0QJ8wRgIhAKDysUcBDLlWx0vZ8CifiOcjQWBo4uc9JlogYR4z1cX0AiEA6Jgek2vwU6z3zM-aiQDh7GZXX2f19HPPKxwhZLvkshE%3D&source=youtube&sparams=expire%2Cei%2Cip%2Cid%2Caitags%2Csource%2Crequiressl%2Cpcm2%2Cvprv%2Cmime%2Cns%2Cgir%2Cclen%2Cotfp%2Cdur%2Clmt&txp=1311222&vprv=1",
|
||||
|
@ -361,7 +348,6 @@ VideoPlayer(
|
|||
mime: "video/webm; codecs=\"vp9\"",
|
||||
format: webm,
|
||||
codec: vp9,
|
||||
throttled: false,
|
||||
),
|
||||
VideoStream(
|
||||
url: "https://rr4---sn-h0jelnez.googlevideo.com/videoplayback?aitags=133%2C134%2C135%2C136%2C160%2C242%2C243%2C244%2C247%2C278%2C298%2C299%2C302%2C303%2C394%2C395%2C396%2C397%2C398%2C399&c=TVHTML5_SIMPLY_EMBEDDED_PLAYER&clen=22365208&dur=163.046&ei=q1jpYv-eJ9uF6dsPhvyH8As&expire=1659481355&fexp=24001373%2C24007246&fvip=5&gir=yes&id=o-AKkOKYSoYWWfNLdrt3aQbxbIwHh4mMVyXLb2mtjc0uXQ&initcwndbps=1527500&ip=2003%3Ade%3Aaf0e%3A2f00%3Ade47%3A297%3Aa6db%3A774e&itag=398&keepalive=yes&lmt=1608048380553749&lsig=AG3C_xAwRgIhAIWRo8U-MB6jhlipPec3A3m5-StMaX64EEGBEE3LWaNiAiEA_8QPrTStO0ISMA5Jex-G2JfPpqyw-vltC8nAFXyPz98%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&mh=mQ&mime=video%2Fmp4&mm=31%2C29&mn=sn-h0jelnez%2Csn-h0jeenek&ms=au%2Crdu&mt=1659459429&mv=m&mvi=4&n=U0g9MK69PQnuYQ&ns=h4nQ-PcvhL4hLZwTU9i7QAoH&otfp=1&pcm2=yes&pl=37&rbqsm=fr&requiressl=yes&sig=AOq0QJ8wRAIgcHUn_ogkBtSQLpq8m-l4IqLlx7EKsddusFPuwvMlLuoCIDF1FiMdigJzd_H5xIgglkW7GaS3CG5Sx9aC2O5pAtUG&source=youtube&sparams=expire%2Cei%2Cip%2Cid%2Caitags%2Csource%2Crequiressl%2Cpcm2%2Cvprv%2Cmime%2Cns%2Cgir%2Cclen%2Cotfp%2Cdur%2Clmt&txp=1311222&vprv=1",
|
||||
|
@ -386,7 +372,6 @@ VideoPlayer(
|
|||
mime: "video/mp4; codecs=\"av01.0.08M.08\"",
|
||||
format: mp4,
|
||||
codec: av01,
|
||||
throttled: false,
|
||||
),
|
||||
VideoStream(
|
||||
url: "https://rr4---sn-h0jelnez.googlevideo.com/videoplayback?aitags=133%2C134%2C135%2C136%2C160%2C242%2C243%2C244%2C247%2C278%2C298%2C299%2C302%2C303%2C394%2C395%2C396%2C397%2C398%2C399&c=TVHTML5_SIMPLY_EMBEDDED_PLAYER&clen=65400181&dur=163.046&ei=q1jpYv-eJ9uF6dsPhvyH8As&expire=1659481355&fexp=24001373%2C24007246&fvip=5&gir=yes&id=o-AKkOKYSoYWWfNLdrt3aQbxbIwHh4mMVyXLb2mtjc0uXQ&initcwndbps=1527500&ip=2003%3Ade%3Aaf0e%3A2f00%3Ade47%3A297%3Aa6db%3A774e&itag=299&keepalive=yes&lmt=1580005649161486&lsig=AG3C_xAwRgIhAIWRo8U-MB6jhlipPec3A3m5-StMaX64EEGBEE3LWaNiAiEA_8QPrTStO0ISMA5Jex-G2JfPpqyw-vltC8nAFXyPz98%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&mh=mQ&mime=video%2Fmp4&mm=31%2C29&mn=sn-h0jelnez%2Csn-h0jeenek&ms=au%2Crdu&mt=1659459429&mv=m&mvi=4&n=U0g9MK69PQnuYQ&ns=h4nQ-PcvhL4hLZwTU9i7QAoH&otfp=1&pcm2=yes&pl=37&rbqsm=fr&requiressl=yes&sig=AOq0QJ8wRAIgRoFTJHusyDU4PA4tIpFb7cNHxwiKOH_C5FGDdcx16ScCIC2SlCLt3gTJ2mUuTbav41TnZ5pVEAbiLxuY6pMV4stE&source=youtube&sparams=expire%2Cei%2Cip%2Cid%2Caitags%2Csource%2Crequiressl%2Cpcm2%2Cvprv%2Cmime%2Cns%2Cgir%2Cclen%2Cotfp%2Cdur%2Clmt&txp=2211222&vprv=1",
|
||||
|
@ -411,7 +396,6 @@ VideoPlayer(
|
|||
mime: "video/mp4; codecs=\"avc1.64002a\"",
|
||||
format: mp4,
|
||||
codec: avc1,
|
||||
throttled: false,
|
||||
),
|
||||
VideoStream(
|
||||
url: "https://rr4---sn-h0jelnez.googlevideo.com/videoplayback?aitags=133%2C134%2C135%2C136%2C160%2C242%2C243%2C244%2C247%2C278%2C298%2C299%2C302%2C303%2C394%2C395%2C396%2C397%2C398%2C399&c=TVHTML5_SIMPLY_EMBEDDED_PLAYER&clen=62993617&dur=163.046&ei=q1jpYv-eJ9uF6dsPhvyH8As&expire=1659481355&fexp=24001373%2C24007246&fvip=5&gir=yes&id=o-AKkOKYSoYWWfNLdrt3aQbxbIwHh4mMVyXLb2mtjc0uXQ&initcwndbps=1527500&ip=2003%3Ade%3Aaf0e%3A2f00%3Ade47%3A297%3Aa6db%3A774e&itag=303&keepalive=yes&lmt=1608509371758331&lsig=AG3C_xAwRgIhAIWRo8U-MB6jhlipPec3A3m5-StMaX64EEGBEE3LWaNiAiEA_8QPrTStO0ISMA5Jex-G2JfPpqyw-vltC8nAFXyPz98%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&mh=mQ&mime=video%2Fwebm&mm=31%2C29&mn=sn-h0jelnez%2Csn-h0jeenek&ms=au%2Crdu&mt=1659459429&mv=m&mvi=4&n=U0g9MK69PQnuYQ&ns=h4nQ-PcvhL4hLZwTU9i7QAoH&otfp=1&pcm2=yes&pl=37&rbqsm=fr&requiressl=yes&sig=AOq0QJ8wRQIgIChm15WPOCXfBDCY0W_4Ul3wdL8YRia4knFoPl_u8AsCIQCTSOnu_bi5-FkCPiOM0P8WTDaXo9hGJuYmxguzxbF88A%3D%3D&source=youtube&sparams=expire%2Cei%2Cip%2Cid%2Caitags%2Csource%2Crequiressl%2Cpcm2%2Cvprv%2Cmime%2Cns%2Cgir%2Cclen%2Cotfp%2Cdur%2Clmt&txp=1311222&vprv=1",
|
||||
|
@ -436,7 +420,6 @@ VideoPlayer(
|
|||
mime: "video/webm; codecs=\"vp9\"",
|
||||
format: webm,
|
||||
codec: vp9,
|
||||
throttled: false,
|
||||
),
|
||||
VideoStream(
|
||||
url: "https://rr4---sn-h0jelnez.googlevideo.com/videoplayback?aitags=133%2C134%2C135%2C136%2C160%2C242%2C243%2C244%2C247%2C278%2C298%2C299%2C302%2C303%2C394%2C395%2C396%2C397%2C398%2C399&c=TVHTML5_SIMPLY_EMBEDDED_PLAYER&clen=42567727&dur=163.046&ei=q1jpYv-eJ9uF6dsPhvyH8As&expire=1659481355&fexp=24001373%2C24007246&fvip=5&gir=yes&id=o-AKkOKYSoYWWfNLdrt3aQbxbIwHh4mMVyXLb2mtjc0uXQ&initcwndbps=1527500&ip=2003%3Ade%3Aaf0e%3A2f00%3Ade47%3A297%3Aa6db%3A774e&itag=399&keepalive=yes&lmt=1608052932785283&lsig=AG3C_xAwRgIhAIWRo8U-MB6jhlipPec3A3m5-StMaX64EEGBEE3LWaNiAiEA_8QPrTStO0ISMA5Jex-G2JfPpqyw-vltC8nAFXyPz98%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&mh=mQ&mime=video%2Fmp4&mm=31%2C29&mn=sn-h0jelnez%2Csn-h0jeenek&ms=au%2Crdu&mt=1659459429&mv=m&mvi=4&n=U0g9MK69PQnuYQ&ns=h4nQ-PcvhL4hLZwTU9i7QAoH&otfp=1&pcm2=yes&pl=37&rbqsm=fr&requiressl=yes&sig=AOq0QJ8wRQIgO3omBCES-iEOIeuiy9Jsz9wB_QfRkCuRCiCQ-N5KdqoCIQDANFWf0zfBSm1qGjA7jYJEti7hiM9klZHFZjC2CN9r9A%3D%3D&source=youtube&sparams=expire%2Cei%2Cip%2Cid%2Caitags%2Csource%2Crequiressl%2Cpcm2%2Cvprv%2Cmime%2Cns%2Cgir%2Cclen%2Cotfp%2Cdur%2Clmt&txp=1311222&vprv=1",
|
||||
|
@ -461,7 +444,6 @@ VideoPlayer(
|
|||
mime: "video/mp4; codecs=\"av01.0.09M.08\"",
|
||||
format: mp4,
|
||||
codec: av01,
|
||||
throttled: false,
|
||||
),
|
||||
],
|
||||
audio_streams: [
|
||||
|
@ -485,7 +467,6 @@ VideoPlayer(
|
|||
codec: opus,
|
||||
channels: Some(2),
|
||||
loudness_db: Some(5.2200003),
|
||||
throttled: false,
|
||||
track: None,
|
||||
),
|
||||
AudioStream(
|
||||
|
@ -508,7 +489,6 @@ VideoPlayer(
|
|||
codec: opus,
|
||||
channels: Some(2),
|
||||
loudness_db: Some(5.2200003),
|
||||
throttled: false,
|
||||
track: None,
|
||||
),
|
||||
AudioStream(
|
||||
|
@ -531,7 +511,6 @@ VideoPlayer(
|
|||
codec: mp4a,
|
||||
channels: Some(2),
|
||||
loudness_db: Some(5.2159004),
|
||||
throttled: false,
|
||||
track: None,
|
||||
),
|
||||
AudioStream(
|
||||
|
@ -554,7 +533,6 @@ VideoPlayer(
|
|||
codec: opus,
|
||||
channels: Some(2),
|
||||
loudness_db: Some(5.2200003),
|
||||
throttled: false,
|
||||
track: None,
|
||||
),
|
||||
],
|
||||
|
@ -601,5 +579,6 @@ VideoPlayer(
|
|||
frames_per_page_y: 5,
|
||||
),
|
||||
],
|
||||
client_type: tv_html5_embed,
|
||||
visitor_data: Some("CgtacUJOMG81dTI3cyirsaWXBg%3D%3D"),
|
||||
)
|
||||
|
|
|
@ -9,6 +9,7 @@ SearchResult(
|
|||
Channel(ChannelItem(
|
||||
id: "UCMwePVHRpDdfeUcwtDZu2Dw",
|
||||
name: "Monstafluff Music",
|
||||
handle: Some("@MonstafluffMusic"),
|
||||
avatar: [
|
||||
Thumbnail(
|
||||
url: "//yt3.ggpht.com/ytc/AMLnZu9YhTzdAoL6P4PYq51PCF076ITDrgLitxSDPqv6sw=s88-c-k-c0x00ffffff-no-rj-mo",
|
||||
|
@ -23,12 +24,12 @@ SearchResult(
|
|||
],
|
||||
verification: Verified,
|
||||
subscriber_count: Some(582000),
|
||||
video_count: None,
|
||||
short_description: "Music Submissions: https://monstafluff.edmdistrict.com/",
|
||||
)),
|
||||
Channel(ChannelItem(
|
||||
id: "UCLxAS02eWvfZK4icRNzWD_g",
|
||||
name: "Music Travel Love",
|
||||
handle: Some("@MusicTravelLove"),
|
||||
avatar: [
|
||||
Thumbnail(
|
||||
url: "https://yt3.ggpht.com/ytc/AMLnZu9njNDLU_VtFjfGUaTArBp4AJFhJIxb_CxP7knf3A=s88-c-k-c0x00ffffff-no-rj-mo",
|
||||
|
@ -43,12 +44,12 @@ SearchResult(
|
|||
],
|
||||
verification: Artist,
|
||||
subscriber_count: Some(4030000),
|
||||
video_count: None,
|
||||
short_description: "Welcome to the official Music Travel Love YouTube channel! We travel the world making music, friends, videos and memories!",
|
||||
)),
|
||||
Channel(ChannelItem(
|
||||
id: "UCxKxjNPyL9UO5LRWHzp5JxA",
|
||||
name: "Black&White Music",
|
||||
handle: Some("@blackwhitemusic5836"),
|
||||
avatar: [
|
||||
Thumbnail(
|
||||
url: "//yt3.ggpht.com/FDjW2-Cb6tFbtNv02D1UX4XtvP7P3eEWB93hGimeP4pb2TadVhAgxSVMZLZDp5NiBWGLT5eprA=s88-c-k-c0x00ffffff-no-rj-mo",
|
||||
|
@ -63,12 +64,12 @@ SearchResult(
|
|||
],
|
||||
verification: Verified,
|
||||
subscriber_count: Some(167000),
|
||||
video_count: None,
|
||||
short_description: "MUSIC IN HARMONY WITH YOUR LIFE!!! If any producer, label, artist or photographer has an issue with any of the music or\u{a0}...",
|
||||
)),
|
||||
Channel(ChannelItem(
|
||||
id: "UCGIygiYkKxn7g7fFNFdXskg",
|
||||
name: "HAEVN MUSIC",
|
||||
handle: Some("@HAEVNMUSIC"),
|
||||
avatar: [
|
||||
Thumbnail(
|
||||
url: "//yt3.ggpht.com/EYlGIfqhvwtfkCyi5vpqfY_kDHr6L3OeCmkudNiAyhvz6UCnTZQOQaM-8PelFDGofdIqeF7Mb4E=s88-c-k-c0x00ffffff-no-rj-mo",
|
||||
|
@ -83,12 +84,12 @@ SearchResult(
|
|||
],
|
||||
verification: Artist,
|
||||
subscriber_count: Some(411000),
|
||||
video_count: None,
|
||||
short_description: "The official YouTube channel of HAEVN Music. Receiving a piano from his grandfather had a great impact on Jorrit\'s life.",
|
||||
)),
|
||||
Channel(ChannelItem(
|
||||
id: "UClvNJkDHdc1gvFGN_Fr_qPw",
|
||||
name: "Artemis Music",
|
||||
handle: Some("@artemismusic1000"),
|
||||
avatar: [
|
||||
Thumbnail(
|
||||
url: "//yt3.ggpht.com/rGXIwYAhI49rKBQmw_pKFMv9yEt4euHnmXOE0OOCD6ApdQXGnuPmEv7TK7cDjrjt0rUXYHuw=s88-c-k-c0x00ffffff-no-rj-mo",
|
||||
|
@ -103,12 +104,12 @@ SearchResult(
|
|||
],
|
||||
verification: None,
|
||||
subscriber_count: Some(31200),
|
||||
video_count: None,
|
||||
short_description: "Hello and welcome to \"Artemis Music\"! Music can play an effective role in helping us lead a better and more productive life.",
|
||||
)),
|
||||
Channel(ChannelItem(
|
||||
id: "UC5r3j8tQsB3MYZiwQFGKrdA",
|
||||
name: "Disco Music",
|
||||
handle: Some("@discomusic9273"),
|
||||
avatar: [
|
||||
Thumbnail(
|
||||
url: "//yt3.ggpht.com/5nqhAdf26KoSKbfUB8kvhJo6rpMQw3XS345h8ZNmeXScqlB1KjJAM0T371r3QcS1mA1LZg9B1Po=s88-c-k-c0x00ffffff-no-rj-mo",
|
||||
|
@ -123,12 +124,12 @@ SearchResult(
|
|||
],
|
||||
verification: Verified,
|
||||
subscriber_count: Some(372000),
|
||||
video_count: None,
|
||||
short_description: "Music is the only language in which you cannot say a mean or sarcastic thing. Have fun listening to music.",
|
||||
)),
|
||||
Channel(ChannelItem(
|
||||
id: "UCNZYpcqym8gHcNg2GWcC6nQ",
|
||||
name: "S!X - Music",
|
||||
handle: Some("@s1x-music"),
|
||||
avatar: [
|
||||
Thumbnail(
|
||||
url: "//yt3.googleusercontent.com/ytc/AMLnZu_1NOzbZUJWZjtmD4NTsb9BR-TNIAzNoajv0TisvQ=s88-c-k-c0x00ffffff-no-rj-mo",
|
||||
|
@ -143,12 +144,12 @@ SearchResult(
|
|||
],
|
||||
verification: Verified,
|
||||
subscriber_count: Some(178000),
|
||||
video_count: None,
|
||||
short_description: "S!X - Music is an independent Hip-Hop label. Soundcloud : https://soundcloud.com/s1xmusic Facebook\u{a0}...",
|
||||
)),
|
||||
Channel(ChannelItem(
|
||||
id: "UCoEryX-WO7IHBGqTAC5r9Zw",
|
||||
name: "Shake Music",
|
||||
handle: Some("@ShakeMusic"),
|
||||
avatar: [
|
||||
Thumbnail(
|
||||
url: "//yt3.googleusercontent.com/ytc/AMLnZu9fMXUALsloNUJ_wLpqCS0ovprvc5W-XwfrpmWqIw=s88-c-k-c0x00ffffff-no-rj-mo",
|
||||
|
@ -163,12 +164,12 @@ SearchResult(
|
|||
],
|
||||
verification: Verified,
|
||||
subscriber_count: Some(1040000),
|
||||
video_count: None,
|
||||
short_description: "Welcome to Shake Music, a Trap & Bass Channel / Record Label dedicated to bringing you the best tracks. All tracks on Shake\u{a0}...",
|
||||
)),
|
||||
Channel(ChannelItem(
|
||||
id: "UCTJ9Qg-1vBu2pP_YrWUfGnQ",
|
||||
name: "Miracle Music",
|
||||
handle: Some("@miraclemusic2328"),
|
||||
avatar: [
|
||||
Thumbnail(
|
||||
url: "//yt3.ggpht.com/3RMarDSmUSIexCXWCpMUkqV64uiHDXTidBLwsObHstx5-AbB8h_n8Zy1W9JymURd7ivzlDEGFw=s88-c-k-c0x00ffffff-no-rj-mo",
|
||||
|
@ -183,12 +184,12 @@ SearchResult(
|
|||
],
|
||||
verification: Verified,
|
||||
subscriber_count: Some(822000),
|
||||
video_count: None,
|
||||
short_description: "Welcome to Miracle Music! On this channel you will find a wide variety of different Deep House, Tropical House, Chill Out, EDM,.",
|
||||
)),
|
||||
Channel(ChannelItem(
|
||||
id: "UCp6_KuNhT0kcFk-jXw9Tivg",
|
||||
name: "Magic Music",
|
||||
handle: Some("@MagicMusicGroup"),
|
||||
avatar: [
|
||||
Thumbnail(
|
||||
url: "//yt3.googleusercontent.com/ytc/AMLnZu-fgSc_lceD4fRL_y0b3MKd2k54DF-laDAR3Avbuw=s88-c-k-c0x00ffffff-no-rj-mo",
|
||||
|
@ -203,12 +204,12 @@ SearchResult(
|
|||
],
|
||||
verification: Verified,
|
||||
subscriber_count: Some(4620000),
|
||||
video_count: None,
|
||||
short_description: "",
|
||||
)),
|
||||
Channel(ChannelItem(
|
||||
id: "UCe55Gy-hFDvLZp8C8BZhBnw",
|
||||
name: "Nightblue Music",
|
||||
handle: Some("@NightblueMusic"),
|
||||
avatar: [
|
||||
Thumbnail(
|
||||
url: "//yt3.googleusercontent.com/ytc/AMLnZu-29SYt5qpqMP9Xi2A98mqL8ymI5Lg7Vzx-qpY09w=s88-c-k-c0x00ffffff-no-rj-mo",
|
||||
|
@ -223,12 +224,12 @@ SearchResult(
|
|||
],
|
||||
verification: Verified,
|
||||
subscriber_count: Some(1050000),
|
||||
video_count: None,
|
||||
short_description: "BRINGING YOU ONLY THE BEST EDM - TRAP Submit your own track for promotion here:\u{a0}...",
|
||||
)),
|
||||
Channel(ChannelItem(
|
||||
id: "UC2fVSthyWxWSjsiEAHPzriQ",
|
||||
name: "Mr_MoMo Music",
|
||||
handle: Some("@MrMoMoMusic"),
|
||||
avatar: [
|
||||
Thumbnail(
|
||||
url: "//yt3.ggpht.com/7YG4jSrhx_Mfi2TsV0rJFlFARaR8kl7ilcIyzs6gSeNjwn-J88DvDWD8PSNd5o03qJRzpvhs=s88-c-k-c0x00ffffff-no-rj-mo",
|
||||
|
@ -243,12 +244,12 @@ SearchResult(
|
|||
],
|
||||
verification: Verified,
|
||||
subscriber_count: Some(709000),
|
||||
video_count: None,
|
||||
short_description: "Hey there! I am Mr MoMo My channel focus on Japan music, lofi, trap & bass type beat and Japanese instrumental. I mindfully\u{a0}...",
|
||||
)),
|
||||
Channel(ChannelItem(
|
||||
id: "UCN31w7dRjjz8CeP0GfSIo8A",
|
||||
name: "Danit Music Official",
|
||||
handle: Some("@danitmusicofficial5734"),
|
||||
avatar: [
|
||||
Thumbnail(
|
||||
url: "//yt3.ggpht.com/ytc/AMLnZu9rUKtDsY-aSoE5WEwAQxvQTXiuAPYMBoJQ2mYTUA=s88-c-k-c0x00ffffff-no-rj-mo",
|
||||
|
@ -263,12 +264,12 @@ SearchResult(
|
|||
],
|
||||
verification: None,
|
||||
subscriber_count: Some(54400),
|
||||
video_count: None,
|
||||
short_description: "",
|
||||
)),
|
||||
Channel(ChannelItem(
|
||||
id: "UCpEHWiTMk1eEBAdzBnAb3rA",
|
||||
name: "Energy Transformation Relaxing Music ",
|
||||
handle: Some("@energytransformationrelaxi5596"),
|
||||
avatar: [
|
||||
Thumbnail(
|
||||
url: "//yt3.ggpht.com/RR7upyAvT7N0_qlZWfLlDSRPhLufX4W4X6-qahWvuvDCLn2cWCs0yh_HXB2iwGbk_MTwSqwWEQ=s88-c-k-c0x00ffffff-no-rj-mo",
|
||||
|
@ -283,12 +284,12 @@ SearchResult(
|
|||
],
|
||||
verification: None,
|
||||
subscriber_count: Some(3590),
|
||||
video_count: None,
|
||||
short_description: "Welcome to our Energy Transformation Relaxing Music . This chakra music channel will focus on developing the best chakra\u{a0}...",
|
||||
)),
|
||||
Channel(ChannelItem(
|
||||
id: "UCqswUMaC5yWUrkQszr8fuBA",
|
||||
name: "Nonstop Music",
|
||||
handle: Some("@nonstopmusic9993"),
|
||||
avatar: [
|
||||
Thumbnail(
|
||||
url: "//yt3.googleusercontent.com/ytc/AMLnZu9vLN62RxNbnpa20r5XreWRlVjHXbHf7BMcvSBxoQ=s88-c-k-c0x00ffffff-no-rj-mo",
|
||||
|
@ -303,12 +304,12 @@ SearchResult(
|
|||
],
|
||||
verification: Verified,
|
||||
subscriber_count: Some(416000),
|
||||
video_count: None,
|
||||
short_description: "Nonstop Music - Home of 1h videos of your favourite songs and mixes. Nonstop Genres: Pop • Chillout • Tropical House • Deep\u{a0}...",
|
||||
)),
|
||||
Channel(ChannelItem(
|
||||
id: "UChO8h2G8UjOVc081rgYU8XQ",
|
||||
name: "Vibe Music",
|
||||
handle: Some("@vibemusic."),
|
||||
avatar: [
|
||||
Thumbnail(
|
||||
url: "//yt3.googleusercontent.com/ytc/AMLnZu9Br5pt87kuDLRFbh1MqMXeFlCLbUrwFlDIzU4s=s88-c-k-c0x00ffffff-no-rj-mo",
|
||||
|
@ -323,12 +324,12 @@ SearchResult(
|
|||
],
|
||||
verification: Verified,
|
||||
subscriber_count: Some(3000000),
|
||||
video_count: None,
|
||||
short_description: "Vibe Music strives to bring the best lyric videos of popular Rap & Hip Hop songs. Be sure to Subscribe to see new videos we\u{a0}...",
|
||||
)),
|
||||
Channel(ChannelItem(
|
||||
id: "UClV8b2EhIhIASKw-etzegyw",
|
||||
name: "Suits Music",
|
||||
handle: Some("@SuitsMusic"),
|
||||
avatar: [
|
||||
Thumbnail(
|
||||
url: "//yt3.googleusercontent.com/ytc/AMLnZu9Aj5RtZZMdK_B_YD-8rOfi9c5ddFw5t1s4GYEeOQ=s88-c-k-c0x00ffffff-no-rj-mo",
|
||||
|
@ -343,12 +344,12 @@ SearchResult(
|
|||
],
|
||||
verification: None,
|
||||
subscriber_count: Some(120000),
|
||||
video_count: None,
|
||||
short_description: "",
|
||||
)),
|
||||
Channel(ChannelItem(
|
||||
id: "UCI2hwz3r5phXpOtViIA5inA",
|
||||
name: "Rock Music Collection",
|
||||
handle: Some("@rockmusiccollection4332"),
|
||||
avatar: [
|
||||
Thumbnail(
|
||||
url: "//yt3.ggpht.com/kB4gWvROUIWFuJN8xwIqmPl1QV2_gXMat6COAJjXZT07E3xomc4b2JwGtDg05t1MmhgqImSifhc=s88-c-k-c0x00ffffff-no-rj-mo",
|
||||
|
@ -363,12 +364,12 @@ SearchResult(
|
|||
],
|
||||
verification: None,
|
||||
subscriber_count: Some(81700),
|
||||
video_count: None,
|
||||
short_description: "",
|
||||
)),
|
||||
Channel(ChannelItem(
|
||||
id: "UC9w8My3S7h-bQZ-4R-0ZPsw",
|
||||
name: "Helios Music",
|
||||
handle: Some("@heliosmusic55"),
|
||||
avatar: [
|
||||
Thumbnail(
|
||||
url: "//yt3.ggpht.com/bi08T8zuYI1PlbM8M5fyZzjVvNJRJFFcQoonRQvS30opJ-OqGIq5OPrZ19qga29PIAit7OO3=s88-c-k-c0x00ffffff-no-rj-mo",
|
||||
|
@ -383,12 +384,12 @@ SearchResult(
|
|||
],
|
||||
verification: None,
|
||||
subscriber_count: Some(53000),
|
||||
video_count: None,
|
||||
short_description: "Welcome to my channel - Helios Music. I created this channel to help people have the most relaxing, refreshing and comfortable\u{a0}...",
|
||||
)),
|
||||
Channel(ChannelItem(
|
||||
id: "UC_ODKC5gTs2LvdHXDRdDm0w",
|
||||
name: "Music On",
|
||||
handle: Some("@MilanPavlovic91"),
|
||||
avatar: [
|
||||
Thumbnail(
|
||||
url: "//yt3.googleusercontent.com/ytc/AMLnZu8lUOYw4RdRwQf2Kz8RCExSmuWC78oetXF7VL67SA=s88-c-k-c0x00ffffff-no-rj-mo",
|
||||
|
@ -403,7 +404,6 @@ SearchResult(
|
|||
],
|
||||
verification: None,
|
||||
subscriber_count: Some(129000),
|
||||
video_count: None,
|
||||
short_description: "Music On (UNOFFICIAL CHANNEL)",
|
||||
)),
|
||||
],
|
||||
|
|
|
@ -9,6 +9,7 @@ SearchResult(
|
|||
Channel(ChannelItem(
|
||||
id: "UCh8gHdtzO2tXd593_bjErWg",
|
||||
name: "Doobydobap",
|
||||
handle: None,
|
||||
avatar: [
|
||||
Thumbnail(
|
||||
url: "//yt3.ggpht.com/dm5Aq93xvVJz0NoVO88ieBkDXmuShCujGPlZ7qETMEPTrXvPUCFI3-BB6Xs_P-r6Uk3mnBy9zA=s88-c-k-c0x00ffffff-no-rj-mo",
|
||||
|
@ -23,7 +24,6 @@ SearchResult(
|
|||
],
|
||||
verification: Verified,
|
||||
subscriber_count: Some(2920000),
|
||||
video_count: Some(219),
|
||||
short_description: "Hi, I\'m Tina, aka Doobydobap! Food is the medium I use to tell stories and connect with people who share the same passion as I\u{a0}...",
|
||||
)),
|
||||
Video(VideoItem(
|
||||
|
|
|
@ -1,784 +0,0 @@
|
|||
---
|
||||
source: src/client/trends.rs
|
||||
expression: map_res.c
|
||||
---
|
||||
Paginator(
|
||||
count: None,
|
||||
items: [
|
||||
VideoItem(
|
||||
id: "_cyJhGsXDDM",
|
||||
name: "Ultimate Criminal Canal Found Magnet Fishing! Police on the Hunt",
|
||||
duration: Some(1096),
|
||||
thumbnail: [
|
||||
Thumbnail(
|
||||
url: "https://i.ytimg.com/vi/_cyJhGsXDDM/hq720.jpg?sqp=-oaymwEcCOgCEMoBSFXyq4qpAw4IARUAAIhCGAFwAcABBg==&rs=AOn4CLBBz_ErMMfhKLRZRfcAPTlMTujziw",
|
||||
width: 360,
|
||||
height: 202,
|
||||
),
|
||||
Thumbnail(
|
||||
url: "https://i.ytimg.com/vi/_cyJhGsXDDM/hq720.jpg?sqp=-oaymwEcCNAFEJQDSFXyq4qpAw4IARUAAIhCGAFwAcABBg==&rs=AOn4CLDaUGJ6GyTv5vwllztR6mN43dlmxA",
|
||||
width: 720,
|
||||
height: 404,
|
||||
),
|
||||
],
|
||||
channel: Some(ChannelTag(
|
||||
id: "UCMLXec9-wpON8tZegnDsYLw",
|
||||
name: "Bondi Treasure Hunter",
|
||||
avatar: [
|
||||
Thumbnail(
|
||||
url: "https://yt3.ggpht.com/ytc/AMLnZu91VHy_3HvCaMLthYyMSol6zwqxebNQ9GXc7NUB=s68-c-k-c0x00ffffff-no-rj",
|
||||
width: 68,
|
||||
height: 68,
|
||||
),
|
||||
],
|
||||
verification: Verified,
|
||||
subscriber_count: None,
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
publish_date_txt: Some("1 day ago"),
|
||||
view_count: Some(700385),
|
||||
is_live: false,
|
||||
is_short: false,
|
||||
is_upcoming: false,
|
||||
short_description: Some("Subscribe for more Treasure Hunting videos: https://tinyurl.com/yyl3zerk\n\nMy Magnet! (Use Discount code \'BONDI\'): https://magnetarmagnets.com/\nMy Dive System! (Use Bonus code \'BONDI\'): https://lddy..."),
|
||||
),
|
||||
VideoItem(
|
||||
id: "36YnV9STBqc",
|
||||
name: "The Good Life Radio\u{a0}•\u{a0}24/7 Live Radio | Best Relax House, Chillout, Study, Running, Gym, Happy Music",
|
||||
duration: None,
|
||||
thumbnail: [
|
||||
Thumbnail(
|
||||
url: "https://i.ytimg.com/vi/36YnV9STBqc/hq720_live.jpg?sqp=COjxqZoG-oaymwEcCOgCEMoBSFXyq4qpAw4IARUAAIhCGAFwAcABBg==&rs=AOn4CLASUZkzmRJDiyIJmcsAdcDGan805Q",
|
||||
width: 360,
|
||||
height: 202,
|
||||
),
|
||||
Thumbnail(
|
||||
url: "https://i.ytimg.com/vi/36YnV9STBqc/hq720_live.jpg?sqp=COjxqZoG-oaymwEcCNAFEJQDSFXyq4qpAw4IARUAAIhCGAFwAcABBg==&rs=AOn4CLBDrl0k5nr9wH-_aosqOimodx0b-w",
|
||||
width: 720,
|
||||
height: 404,
|
||||
),
|
||||
],
|
||||
channel: Some(ChannelTag(
|
||||
id: "UChs0pSaEoNLV4mevBFGaoKA",
|
||||
name: "The Good Life Radio x Sensual Musique",
|
||||
avatar: [
|
||||
Thumbnail(
|
||||
url: "https://yt3.ggpht.com/ytc/AMLnZu_V9mOdHaorjNFqGXCecFeOBZhDWB8tVYG_I8gJwA=s68-c-k-c0x00ffffff-no-rj",
|
||||
width: 68,
|
||||
height: 68,
|
||||
),
|
||||
],
|
||||
verification: Verified,
|
||||
subscriber_count: None,
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
publish_date_txt: None,
|
||||
view_count: Some(7202),
|
||||
is_live: true,
|
||||
is_short: false,
|
||||
is_upcoming: false,
|
||||
short_description: Some("The Good Life is live streaming the best of Relaxing & Chill House Music, Deep House, Tropical House, EDM, Dance & Pop as well as Music for Sleep, Focus, Study, Workout, Gym, Running etc. in..."),
|
||||
),
|
||||
VideoItem(
|
||||
id: "YYD1qgH5qC4",
|
||||
name: "چند شنبه با سینــا | فصل چهـارم | قسمت 5 | با حضور نازنین انصاری مدیر روزنامه کیهان لندن",
|
||||
duration: Some(3261),
|
||||
thumbnail: [
|
||||
Thumbnail(
|
||||
url: "https://i.ytimg.com/vi/YYD1qgH5qC4/hq720.jpg?sqp=-oaymwEcCOgCEMoBSFXyq4qpAw4IARUAAIhCGAFwAcABBg==&rs=AOn4CLBkvD-kVL12hteMVVLRZvJHOdlPzQ",
|
||||
width: 360,
|
||||
height: 202,
|
||||
),
|
||||
Thumbnail(
|
||||
url: "https://i.ytimg.com/vi/YYD1qgH5qC4/hq720.jpg?sqp=-oaymwEcCNAFEJQDSFXyq4qpAw4IARUAAIhCGAFwAcABBg==&rs=AOn4CLDpO5WCJiLDPHrXOWH-xk2hTG_S3A",
|
||||
width: 720,
|
||||
height: 404,
|
||||
),
|
||||
],
|
||||
channel: Some(ChannelTag(
|
||||
id: "UCzH_7hfL6Jd1H0WpNO_eryQ",
|
||||
name: "MBC PERSIA",
|
||||
avatar: [
|
||||
Thumbnail(
|
||||
url: "https://yt3.ggpht.com/ytc/AMLnZu9lP4dhb_R_Y7e8Q4sb6dj7ve-YtalnMd2t1qP05A=s68-c-k-c0x00ffffff-no-rj",
|
||||
width: 68,
|
||||
height: 68,
|
||||
),
|
||||
],
|
||||
verification: None,
|
||||
subscriber_count: None,
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
publish_date_txt: Some("14 hours ago"),
|
||||
view_count: Some(104344),
|
||||
is_live: false,
|
||||
is_short: false,
|
||||
is_upcoming: false,
|
||||
short_description: Some("#mbcpersia\n#chandshanbeh\n#چندشنبه\n\nشبكه ام بى سى پرشيا را از حساب هاى مختلف در شبكه هاى اجتماعى دنبال كنيد\n►MBCPERSIA on Facebook:..."),
|
||||
),
|
||||
VideoItem(
|
||||
id: "BeJqgI6rw9k",
|
||||
name: "your city is full of fake buildings, here\'s why",
|
||||
duration: Some(725),
|
||||
thumbnail: [
|
||||
Thumbnail(
|
||||
url: "https://i.ytimg.com/vi/BeJqgI6rw9k/hq720.jpg?sqp=-oaymwEcCOgCEMoBSFXyq4qpAw4IARUAAIhCGAFwAcABBg==&rs=AOn4CLAvkJGHa6h2vzXrG1ueGQA8JysqEg",
|
||||
width: 360,
|
||||
height: 202,
|
||||
),
|
||||
Thumbnail(
|
||||
url: "https://i.ytimg.com/vi/BeJqgI6rw9k/hq720.jpg?sqp=-oaymwEcCNAFEJQDSFXyq4qpAw4IARUAAIhCGAFwAcABBg==&rs=AOn4CLDEJWMD2gUA572p12E7fZ1VX8qJ3A",
|
||||
width: 720,
|
||||
height: 404,
|
||||
),
|
||||
],
|
||||
channel: Some(ChannelTag(
|
||||
id: "UCqVEHtQoXHmUCfJ-9smpTSg",
|
||||
name: "Answer in Progress",
|
||||
avatar: [
|
||||
Thumbnail(
|
||||
url: "https://yt3.ggpht.com/b4TIQdFmoHYvQmcMt1XGH40m8-P5VdjyaZKb2C6nmkezGVk2Ln1csqe1PWg5aefEyk-NEFWhzg=s68-c-k-c0x00ffffff-no-rj",
|
||||
width: 68,
|
||||
height: 68,
|
||||
),
|
||||
],
|
||||
verification: Verified,
|
||||
subscriber_count: None,
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
publish_date_txt: Some("7 days ago"),
|
||||
view_count: Some(1447008),
|
||||
is_live: false,
|
||||
is_short: false,
|
||||
is_upcoming: false,
|
||||
short_description: Some("Save 33% on your first Native Deodorant Pack - normally $39, you’ll get it for $26! Click here https://bit.ly/nativeanswer1 and use my code ANSWER #AD\n\nSomewhere on your street there may..."),
|
||||
),
|
||||
VideoItem(
|
||||
id: "ma28eWd1oyA",
|
||||
name: "Post Malone, Maroon 5, Adele, Taylor Swift, Ed Sheeran, Shawn Mendes, Pop Hits 2020 Part 6",
|
||||
duration: Some(29989),
|
||||
thumbnail: [
|
||||
Thumbnail(
|
||||
url: "https://i.ytimg.com/vi/ma28eWd1oyA/hqdefault.jpg?sqp=-oaymwEcCOADEI4CSFXyq4qpAw4IARUAAIhCGAFwAcABBg==&rs=AOn4CLCznoPDMo_F1NCRBWoD4Ps5IjctxQ",
|
||||
width: 480,
|
||||
height: 270,
|
||||
),
|
||||
],
|
||||
channel: Some(ChannelTag(
|
||||
id: "UCldQuUMYTUGrjvcU2vaPSFQ",
|
||||
name: "Music Library",
|
||||
avatar: [
|
||||
Thumbnail(
|
||||
url: "https://yt3.ggpht.com/ytc/AMLnZu-4BJEmOMTfX96bjwu9AQS02gbODk5YQpZWVi5P=s68-c-k-c0x00ffffff-no-rj",
|
||||
width: 68,
|
||||
height: 68,
|
||||
),
|
||||
],
|
||||
verification: None,
|
||||
subscriber_count: None,
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
publish_date_txt: Some("Streamed 2 years ago"),
|
||||
view_count: Some(1861814),
|
||||
is_live: false,
|
||||
is_short: false,
|
||||
is_upcoming: false,
|
||||
short_description: Some("Post Malone, Maroon 5, Adele, Taylor Swift, Ed Sheeran, Shawn Mendes, Charlie Puth Pop Hits 2020\nPost Malone, Maroon 5, Adele, Taylor Swift, Ed Sheeran, Shawn Mendes, Charlie Puth Pop Hits..."),
|
||||
),
|
||||
VideoItem(
|
||||
id: "mL2LBRM5GBI",
|
||||
name: "Salahs 6-Minuten-Hattrick & Firmino-Gala: Rangers - FC Liverpool 1:7 | UEFA Champions League | DAZN",
|
||||
duration: Some(355),
|
||||
thumbnail: [
|
||||
Thumbnail(
|
||||
url: "https://i.ytimg.com/vi/mL2LBRM5GBI/hq720.jpg?sqp=-oaymwEcCOgCEMoBSFXyq4qpAw4IARUAAIhCGAFwAcABBg==&rs=AOn4CLBhsDaEALJodPurmS3DywUoRRwzwg",
|
||||
width: 360,
|
||||
height: 202,
|
||||
),
|
||||
Thumbnail(
|
||||
url: "https://i.ytimg.com/vi/mL2LBRM5GBI/hq720.jpg?sqp=-oaymwEcCNAFEJQDSFXyq4qpAw4IARUAAIhCGAFwAcABBg==&rs=AOn4CLDkvWkbocujg95phnyfNzBB9dhEYA",
|
||||
width: 720,
|
||||
height: 404,
|
||||
),
|
||||
],
|
||||
channel: Some(ChannelTag(
|
||||
id: "UCB-GdMjyokO9lZkKU_oIK6g",
|
||||
name: "DAZN UEFA Champions League",
|
||||
avatar: [
|
||||
Thumbnail(
|
||||
url: "https://yt3.ggpht.com/ytc/AMLnZu-D8LIEj-klO1gvUWMOA987HqMBBX9nn_WJS9Ka=s68-c-k-c0x00ffffff-no-rj",
|
||||
width: 68,
|
||||
height: 68,
|
||||
),
|
||||
],
|
||||
verification: Verified,
|
||||
subscriber_count: None,
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
publish_date_txt: Some("2 days ago"),
|
||||
view_count: Some(1471667),
|
||||
is_live: false,
|
||||
is_short: false,
|
||||
is_upcoming: false,
|
||||
short_description: Some("In der Liga läuft es für die Reds weiterhin nicht rund. Am vergangenen Spieltag gab es gegen Arsenal eine 2:3-Niederlage, am Sonntag trifft man auf Man City. Die Champions League soll für..."),
|
||||
),
|
||||
VideoItem(
|
||||
id: "Ang18qz2IeQ",
|
||||
name: "Satisfying Videos of Workers Doing Their Job Perfectly",
|
||||
duration: Some(1186),
|
||||
thumbnail: [
|
||||
Thumbnail(
|
||||
url: "https://i.ytimg.com/vi/Ang18qz2IeQ/hq720.jpg?sqp=-oaymwEcCOgCEMoBSFXyq4qpAw4IARUAAIhCGAFwAcABBg==&rs=AOn4CLA3Cd49wYUuSEXz2MwhO2aqCMq5ZA",
|
||||
width: 360,
|
||||
height: 202,
|
||||
),
|
||||
Thumbnail(
|
||||
url: "https://i.ytimg.com/vi/Ang18qz2IeQ/hq720.jpg?sqp=-oaymwEcCNAFEJQDSFXyq4qpAw4IARUAAIhCGAFwAcABBg==&rs=AOn4CLAWQAks0vkJyJXSiQFIs9zhc2qyTg",
|
||||
width: 720,
|
||||
height: 404,
|
||||
),
|
||||
],
|
||||
channel: Some(ChannelTag(
|
||||
id: "UCYenDLnIHsoqQ6smwKXQ7Hg",
|
||||
name: "#Mind Warehouse",
|
||||
avatar: [
|
||||
Thumbnail(
|
||||
url: "https://yt3.ggpht.com/ytc/AMLnZu8zB2zV3yx2fSYn5zDbv47rZCBr90wX3jW8EC6NBw=s68-c-k-c0x00ffffff-no-rj",
|
||||
width: 68,
|
||||
height: 68,
|
||||
),
|
||||
],
|
||||
verification: Verified,
|
||||
subscriber_count: None,
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
publish_date_txt: Some("2 days ago"),
|
||||
view_count: Some(173121),
|
||||
is_live: false,
|
||||
is_short: false,
|
||||
is_upcoming: false,
|
||||
short_description: Some("TechZone ► https://goo.gl/Gj3wZs \n\n #incrediblemoments #mindwarehouse #IncredibleMoments #CaughtOnCamera #InterestingFacts \n\nYou can endlessly watch how others work, but in this selection,..."),
|
||||
),
|
||||
VideoItem(
|
||||
id: "fjHN4jsJnEU",
|
||||
name: "I Made 200 Players Simulate Survival Island in Minecraft...",
|
||||
duration: Some(2361),
|
||||
thumbnail: [
|
||||
Thumbnail(
|
||||
url: "https://i.ytimg.com/vi/fjHN4jsJnEU/hq720.jpg?sqp=-oaymwEcCOgCEMoBSFXyq4qpAw4IARUAAIhCGAFwAcABBg==&rs=AOn4CLDwTosIfmAhNHIzU1sSXrTKT8vjNQ",
|
||||
width: 360,
|
||||
height: 202,
|
||||
),
|
||||
Thumbnail(
|
||||
url: "https://i.ytimg.com/vi/fjHN4jsJnEU/hq720.jpg?sqp=-oaymwEcCNAFEJQDSFXyq4qpAw4IARUAAIhCGAFwAcABBg==&rs=AOn4CLA4aFygGqUcm7-Hrkys95U0EAV9xA",
|
||||
width: 720,
|
||||
height: 404,
|
||||
),
|
||||
],
|
||||
channel: Some(ChannelTag(
|
||||
id: "UCqt4mmAqLmH-AwXz31URJsw",
|
||||
name: "Sword4000",
|
||||
avatar: [
|
||||
Thumbnail(
|
||||
url: "https://yt3.ggpht.com/ytc/AMLnZu_q3--WCh9Oc5o4XxAVVxxUz2narAtLR2QKuEw2lQ=s68-c-k-c0x00ffffff-no-rj",
|
||||
width: 68,
|
||||
height: 68,
|
||||
),
|
||||
],
|
||||
verification: Verified,
|
||||
subscriber_count: None,
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
publish_date_txt: Some("7 days ago"),
|
||||
view_count: Some(751909),
|
||||
is_live: false,
|
||||
is_short: false,
|
||||
is_upcoming: false,
|
||||
short_description: Some("200 Players Simulate Survival Island Civilizations in Minecraft...\n-------------------------------------------------------------------\nI invited 200 Players to a Survival Island and let them..."),
|
||||
),
|
||||
VideoItem(
|
||||
id: "FI1XrdBJIUI",
|
||||
name: "Epic Construction Fails | Expensive Fails Compilation | FailArmy",
|
||||
duration: Some(631),
|
||||
thumbnail: [
|
||||
Thumbnail(
|
||||
url: "https://i.ytimg.com/vi/FI1XrdBJIUI/hq720.jpg?sqp=-oaymwEcCOgCEMoBSFXyq4qpAw4IARUAAIhCGAFwAcABBg==&rs=AOn4CLBe2jCnLhTsXmZQefyAe-WqImk6-g",
|
||||
width: 360,
|
||||
height: 202,
|
||||
),
|
||||
Thumbnail(
|
||||
url: "https://i.ytimg.com/vi/FI1XrdBJIUI/hq720.jpg?sqp=-oaymwEcCNAFEJQDSFXyq4qpAw4IARUAAIhCGAFwAcABBg==&rs=AOn4CLD01TnIh1pH7TObDgKzx0GupXXVzw",
|
||||
width: 720,
|
||||
height: 404,
|
||||
),
|
||||
],
|
||||
channel: Some(ChannelTag(
|
||||
id: "UCPDis9pjXuqyI7RYLJ-TTSA",
|
||||
name: "FailArmy",
|
||||
avatar: [
|
||||
Thumbnail(
|
||||
url: "https://yt3.ggpht.com/PLsX6LIg5JbMJR9v7eTD7nQOPmZN16_X7h_uACw5qeWLAewiNfasZFsxQ48Dn8wZ_4McKUPZSA=s68-c-k-c0x00ffffff-no-rj",
|
||||
width: 68,
|
||||
height: 68,
|
||||
),
|
||||
],
|
||||
verification: Verified,
|
||||
subscriber_count: None,
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
publish_date_txt: Some("2 days ago"),
|
||||
view_count: Some(2226471),
|
||||
is_live: false,
|
||||
is_short: false,
|
||||
is_upcoming: false,
|
||||
short_description: Some("I don\'t think so, Tim. ►►► Submit your videos for the chance to be featured 🔗 https://www.failarmy.com/pages/submit-video ▼ Follow us for more fails! https://linktr.ee/failarmy\n#fails..."),
|
||||
),
|
||||
VideoItem(
|
||||
id: "MXdplejK8vU",
|
||||
name: "Chilly autumn Jazz ☕ Smooth September Jazz & Bossa Nova for a great relaxing weekend",
|
||||
duration: Some(86403),
|
||||
thumbnail: [
|
||||
Thumbnail(
|
||||
url: "https://i.ytimg.com/vi/MXdplejK8vU/hq720.jpg?sqp=-oaymwEcCOgCEMoBSFXyq4qpAw4IARUAAIhCGAFwAcABBg==&rs=AOn4CLAIOe93l-1elIK0DfMLk0f3nDWgSA",
|
||||
width: 360,
|
||||
height: 202,
|
||||
),
|
||||
Thumbnail(
|
||||
url: "https://i.ytimg.com/vi/MXdplejK8vU/hq720.jpg?sqp=-oaymwEcCNAFEJQDSFXyq4qpAw4IARUAAIhCGAFwAcABBg==&rs=AOn4CLByGLefQ3I9p2VQ5oZDmc5G_pCTlQ",
|
||||
width: 720,
|
||||
height: 404,
|
||||
),
|
||||
],
|
||||
channel: Some(ChannelTag(
|
||||
id: "UCeGJ6v6KQt0s88hGKMfybuw",
|
||||
name: "Cozy Jazz Music",
|
||||
avatar: [
|
||||
Thumbnail(
|
||||
url: "https://yt3.ggpht.com/tU7x6wNqEM_OIeU-jaaPcdhX3adNhnAY7WaGHsjEMfTLSzVHxm8VVBfaXRjDbf3y_LftGNJ83A=s68-c-k-c0x00ffffff-no-rj",
|
||||
width: 68,
|
||||
height: 68,
|
||||
),
|
||||
],
|
||||
verification: None,
|
||||
subscriber_count: None,
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
publish_date_txt: Some("1 month ago"),
|
||||
view_count: Some(148743),
|
||||
is_live: false,
|
||||
is_short: false,
|
||||
is_upcoming: false,
|
||||
short_description: Some("Chilly autumn Jazz ☕ Smooth September Jazz & Bossa Nova for a great relaxing weekend\nhttps://youtu.be/MXdplejK8vU\n*******************************************\nSounds available on: Jazz Bossa..."),
|
||||
),
|
||||
VideoItem(
|
||||
id: "Jri4_9vBFiQ",
|
||||
name: "Top 100 Best Classic Rock Songs Of All Time 🔥 R.E.M, Queen, Metallica,Guns N’ Roses,Bon Jovi, U2,CCR",
|
||||
duration: None,
|
||||
thumbnail: [
|
||||
Thumbnail(
|
||||
url: "https://i.ytimg.com/vi/Jri4_9vBFiQ/hq720_live.jpg?sqp=COjxqZoG-oaymwEcCOgCEMoBSFXyq4qpAw4IARUAAIhCGAFwAcABBg==&rs=AOn4CLA1ZqDfSLi3Mf5qvpUFSYyDIODNQw",
|
||||
width: 360,
|
||||
height: 202,
|
||||
),
|
||||
Thumbnail(
|
||||
url: "https://i.ytimg.com/vi/Jri4_9vBFiQ/hq720_live.jpg?sqp=COjxqZoG-oaymwEcCNAFEJQDSFXyq4qpAw4IARUAAIhCGAFwAcABBg==&rs=AOn4CLDtwgV7RdHmgDlAESZqSYbuZtFrvw",
|
||||
width: 720,
|
||||
height: 404,
|
||||
),
|
||||
],
|
||||
channel: Some(ChannelTag(
|
||||
id: "UCiIWdzEVNH8okhlapR9a-xA",
|
||||
name: "Rock Music",
|
||||
avatar: [
|
||||
Thumbnail(
|
||||
url: "https://yt3.ggpht.com/QIEcTVdBg9A2kE3un-IfjgTPiglDGMBbh9vMSXo2J5ZRICmunnVQkfpbMWNP8Kueac09DZrn=s68-c-k-c0x00ffffff-no-rj",
|
||||
width: 68,
|
||||
height: 68,
|
||||
),
|
||||
],
|
||||
verification: None,
|
||||
subscriber_count: None,
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
publish_date_txt: None,
|
||||
view_count: Some(192),
|
||||
is_live: true,
|
||||
is_short: false,
|
||||
is_upcoming: false,
|
||||
short_description: Some("Top 100 Best Classic Rock Songs Of All Time 🔥 R.E.M, Queen, Metallica,Guns N’ Roses,Bon Jovi, U2,CCR\nTop 100 Best Classic Rock Songs Of All Time 🔥 R.E.M, Queen, Metallica,Guns N’..."),
|
||||
),
|
||||
VideoItem(
|
||||
id: "ll4d5Lt-Ie8",
|
||||
name: "Relaxing Music Healing Stress, Anxiety and Depressive States Heal Mind, Body and Soul | Sleep music",
|
||||
duration: Some(42896),
|
||||
thumbnail: [
|
||||
Thumbnail(
|
||||
url: "https://i.ytimg.com/vi/ll4d5Lt-Ie8/hq720.jpg?sqp=-oaymwEcCOgCEMoBSFXyq4qpAw4IARUAAIhCGAFwAcABBg==&rs=AOn4CLAqdY2bQaQ3JHl5FYoTPuZFxXRKIQ",
|
||||
width: 360,
|
||||
height: 202,
|
||||
),
|
||||
Thumbnail(
|
||||
url: "https://i.ytimg.com/vi/ll4d5Lt-Ie8/hq720.jpg?sqp=-oaymwEcCNAFEJQDSFXyq4qpAw4IARUAAIhCGAFwAcABBg==&rs=AOn4CLA6xc8r38_2ygARU0vOR4kI6ZNz5w",
|
||||
width: 720,
|
||||
height: 404,
|
||||
),
|
||||
],
|
||||
channel: Some(ChannelTag(
|
||||
id: "UCNS3dqFGBPhxHmOigehpBeg",
|
||||
name: "Love YourSelf",
|
||||
avatar: [
|
||||
Thumbnail(
|
||||
url: "https://yt3.ggpht.com/fkgfEL2OtY2mhhyCV3xSOc3OsVK5ylQJmBev7XlBGE548dM6dqS2Z66YF-pdnbQOQpCuvZOlAdk=s68-c-k-c0x00ffffff-no-rj",
|
||||
width: 68,
|
||||
height: 68,
|
||||
),
|
||||
],
|
||||
verification: Verified,
|
||||
subscriber_count: None,
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
publish_date_txt: Some("Streamed 5 months ago"),
|
||||
view_count: Some(5363904),
|
||||
is_live: false,
|
||||
is_short: false,
|
||||
is_upcoming: false,
|
||||
short_description: Some("The study found that listening to relaxing music of the patient\'s choice resulted in \"significant pain relief and increased mobility.\" Researchers believe that music relieves pain because listening..."),
|
||||
),
|
||||
VideoItem(
|
||||
id: "Dx2wbKLokuQ",
|
||||
name: "W. Putin: Die Sehnsucht nach dem Imperium | Mit offenen Karten | ARTE",
|
||||
duration: Some(729),
|
||||
thumbnail: [
|
||||
Thumbnail(
|
||||
url: "https://i.ytimg.com/vi/Dx2wbKLokuQ/hq720.jpg?sqp=-oaymwEcCOgCEMoBSFXyq4qpAw4IARUAAIhCGAFwAcABBg==&rs=AOn4CLBHQXnaEYo6frjkJ3FFuAPkAyOCKQ",
|
||||
width: 360,
|
||||
height: 202,
|
||||
),
|
||||
Thumbnail(
|
||||
url: "https://i.ytimg.com/vi/Dx2wbKLokuQ/hq720.jpg?sqp=-oaymwEcCNAFEJQDSFXyq4qpAw4IARUAAIhCGAFwAcABBg==&rs=AOn4CLDFtWV_wy25ohVyBthH8a5HwSj6Kw",
|
||||
width: 720,
|
||||
height: 404,
|
||||
),
|
||||
],
|
||||
channel: Some(ChannelTag(
|
||||
id: "UCLLibJTCy3sXjHLVaDimnpQ",
|
||||
name: "ARTEde",
|
||||
avatar: [
|
||||
Thumbnail(
|
||||
url: "https://yt3.ggpht.com/ytc/AMLnZu-1i2jxeXFISJhBbpWWv5vVX2xE5yQbjpaZZP3HPg=s68-c-k-c0x00ffffff-no-rj",
|
||||
width: 68,
|
||||
height: 68,
|
||||
),
|
||||
],
|
||||
verification: Verified,
|
||||
subscriber_count: None,
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
publish_date_txt: Some("2 weeks ago"),
|
||||
view_count: Some(539838),
|
||||
is_live: false,
|
||||
is_short: false,
|
||||
is_upcoming: false,
|
||||
short_description: Some("Jede Woche untersucht „Mit offenen Karten“ die politischen Kräfteverhältnisse in der ganzen Welt anhand detaillierter geografischer Karten \n\nIm Februar 2022 rechtfertigte Wladimir Putin..."),
|
||||
),
|
||||
VideoItem(
|
||||
id: "jfKfPfyJRdk",
|
||||
name: "lofi hip hop radio - beats to relax/study to",
|
||||
duration: None,
|
||||
thumbnail: [
|
||||
Thumbnail(
|
||||
url: "https://i.ytimg.com/vi/jfKfPfyJRdk/hq720_live.jpg?sqp=COjxqZoG-oaymwEcCOgCEMoBSFXyq4qpAw4IARUAAIhCGAFwAcABBg==&rs=AOn4CLCR-bHqcvOP14sSUsNt9PTuf3ZI4Q",
|
||||
width: 360,
|
||||
height: 202,
|
||||
),
|
||||
Thumbnail(
|
||||
url: "https://i.ytimg.com/vi/jfKfPfyJRdk/hq720_live.jpg?sqp=COjxqZoG-oaymwEcCNAFEJQDSFXyq4qpAw4IARUAAIhCGAFwAcABBg==&rs=AOn4CLBBVEQQnwSLJFllntNgv2JAAlvSMQ",
|
||||
width: 720,
|
||||
height: 404,
|
||||
),
|
||||
],
|
||||
channel: Some(ChannelTag(
|
||||
id: "UCSJ4gkVC6NrvII8umztf0Ow",
|
||||
name: "Lofi Girl",
|
||||
avatar: [
|
||||
Thumbnail(
|
||||
url: "https://yt3.ggpht.com/KNYElmLFGAOSZoBmxYGKKXhGHrT2e7Hmz3WsBerbam5uaDXFADAmT7htj3OcC-uK1O88lC9fQg=s68-c-k-c0x00ffffff-no-rj",
|
||||
width: 68,
|
||||
height: 68,
|
||||
),
|
||||
],
|
||||
verification: Verified,
|
||||
subscriber_count: None,
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
publish_date_txt: None,
|
||||
view_count: Some(21262),
|
||||
is_live: true,
|
||||
is_short: false,
|
||||
is_upcoming: false,
|
||||
short_description: Some("🤗 Thank you for listening, I hope you will have a good time here\n\n💽 | Get the latest vinyl (limited edition)\n→ https://vinyl-lofirecords.com/\n\n🎼 | Listen on Spotify, Apple music..."),
|
||||
),
|
||||
VideoItem(
|
||||
id: "qmrzTUmZ4UU",
|
||||
name: "850€ für den Verrat am System - UCS AT-AT LEGO® Star Wars 75313",
|
||||
duration: Some(2043),
|
||||
thumbnail: [
|
||||
Thumbnail(
|
||||
url: "https://i.ytimg.com/vi/qmrzTUmZ4UU/hq720.jpg?sqp=-oaymwEcCOgCEMoBSFXyq4qpAw4IARUAAIhCGAFwAcABBg==&rs=AOn4CLAsI3VS-wxnt1s_zS4M_YbVrV1pAg",
|
||||
width: 360,
|
||||
height: 202,
|
||||
),
|
||||
Thumbnail(
|
||||
url: "https://i.ytimg.com/vi/qmrzTUmZ4UU/hq720.jpg?sqp=-oaymwEcCNAFEJQDSFXyq4qpAw4IARUAAIhCGAFwAcABBg==&rs=AOn4CLBYk7w0qGeW4kZchFr-tbydELUChQ",
|
||||
width: 720,
|
||||
height: 404,
|
||||
),
|
||||
],
|
||||
channel: Some(ChannelTag(
|
||||
id: "UC_EZd3lsmxudu3IQzpTzOgw",
|
||||
name: "Held der Steine Inh. Thomas Panke",
|
||||
avatar: [
|
||||
Thumbnail(
|
||||
url: "https://yt3.ggpht.com/ytc/AMLnZu8g9hFxZ2HD4P9pDsUxoAvkHwbZoTVNr3yw12i8YA=s68-c-k-c0x00ffffff-no-rj",
|
||||
width: 68,
|
||||
height: 68,
|
||||
),
|
||||
],
|
||||
verification: Verified,
|
||||
subscriber_count: None,
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
publish_date_txt: Some("6 days ago"),
|
||||
view_count: Some(600150),
|
||||
is_live: false,
|
||||
is_short: false,
|
||||
is_upcoming: false,
|
||||
short_description: Some("Star Wars - erschienen 2021 - 6749 Teile\n\nDieses Set bei Amazon*:\nhttps://amzn.to/3yu9dHX\n\nErwähnt im Video*:\nTassen https://bit.ly/HdSBausteinecke\nBig Boy https://bit.ly/BBLokBigBoy\nBurg..."),
|
||||
),
|
||||
VideoItem(
|
||||
id: "t0Q2otsqC4I",
|
||||
name: "Tom & Jerry | Tom & Jerry in Full Screen | Classic Cartoon Compilation | WB Kids",
|
||||
duration: Some(1298),
|
||||
thumbnail: [
|
||||
Thumbnail(
|
||||
url: "https://i.ytimg.com/vi/t0Q2otsqC4I/hq720.jpg?sqp=-oaymwEcCOgCEMoBSFXyq4qpAw4IARUAAIhCGAFwAcABBg==&rs=AOn4CLCFcrz2zM6mPUmJiCsC7c7suOzSug",
|
||||
width: 360,
|
||||
height: 202,
|
||||
),
|
||||
Thumbnail(
|
||||
url: "https://i.ytimg.com/vi/t0Q2otsqC4I/hq720.jpg?sqp=-oaymwEcCNAFEJQDSFXyq4qpAw4IARUAAIhCGAFwAcABBg==&rs=AOn4CLCVANFKKXmrdehkf7aM9issiuph5A",
|
||||
width: 720,
|
||||
height: 404,
|
||||
),
|
||||
],
|
||||
channel: Some(ChannelTag(
|
||||
id: "UC9trsD1jCTXXtN3xIOIU8gg",
|
||||
name: "WB Kids",
|
||||
avatar: [
|
||||
Thumbnail(
|
||||
url: "https://yt3.ggpht.com/ytc/AMLnZu80jIF6oehgpUILTaUbqSM5xYHWbPoc_Bz7wddxzg=s68-c-k-c0x00ffffff-no-rj",
|
||||
width: 68,
|
||||
height: 68,
|
||||
),
|
||||
],
|
||||
verification: Verified,
|
||||
subscriber_count: None,
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
publish_date_txt: Some("10 months ago"),
|
||||
view_count: Some(252381571),
|
||||
is_live: false,
|
||||
is_short: false,
|
||||
is_upcoming: false,
|
||||
short_description: Some("Did you know that there are only 25 classic Tom & Jerry episodes that were displayed in a widescreen CinemaScope from the 1950s? Enjoy a compilation filled with some of the best moments from..."),
|
||||
),
|
||||
VideoItem(
|
||||
id: "zE-a5eqvlv8",
|
||||
name: "Dua Lipa, Coldplay, Martin Garrix & Kygo, The Chainsmokers Style - Feeling Me",
|
||||
duration: None,
|
||||
thumbnail: [
|
||||
Thumbnail(
|
||||
url: "https://i.ytimg.com/vi/zE-a5eqvlv8/hq720_live.jpg?sqp=COjxqZoG-oaymwEcCOgCEMoBSFXyq4qpAw4IARUAAIhCGAFwAcABBg==&rs=AOn4CLCDyvujcpz62sEsL9Ke4ADBpXWqOA",
|
||||
width: 360,
|
||||
height: 202,
|
||||
),
|
||||
Thumbnail(
|
||||
url: "https://i.ytimg.com/vi/zE-a5eqvlv8/hq720_live.jpg?sqp=COjxqZoG-oaymwEcCNAFEJQDSFXyq4qpAw4IARUAAIhCGAFwAcABBg==&rs=AOn4CLCyJ-QdgAD1F-DqcLKivIcalBJOEg",
|
||||
width: 720,
|
||||
height: 404,
|
||||
),
|
||||
],
|
||||
channel: Some(ChannelTag(
|
||||
id: "UCX-USfenzQlhrEJR1zD5IYw",
|
||||
name: "Deep Mood.",
|
||||
avatar: [
|
||||
Thumbnail(
|
||||
url: "https://yt3.ggpht.com/8WO05hff9bGjmlyPFo_PJRMIfHEoUvN_KbTcWRVX2yqeUO3fLgkz0K4MA6W95s3_NKdNUAwjow=s68-c-k-c0x00ffffff-no-rj",
|
||||
width: 68,
|
||||
height: 68,
|
||||
),
|
||||
],
|
||||
verification: None,
|
||||
subscriber_count: None,
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
publish_date_txt: None,
|
||||
view_count: Some(955),
|
||||
is_live: true,
|
||||
is_short: false,
|
||||
is_upcoming: false,
|
||||
short_description: Some("#Summermix #DeepHouse #DeepHouseSummerMix\nDua Lipa, Coldplay, Martin Garrix & Kygo, The Chainsmokers Style - Feeling Me\n\n🎵 All songs in this spotify playlist: https://spoti.fi/2TJ4Dyj\nSubmit..."),
|
||||
),
|
||||
VideoItem(
|
||||
id: "HxCcKzRAGWk",
|
||||
name: "(Music for Man ) Relaxing Whiskey Blues Music - Modern Electric Guitar Blues - JAZZ & BLUES",
|
||||
duration: Some(42899),
|
||||
thumbnail: [
|
||||
Thumbnail(
|
||||
url: "https://i.ytimg.com/vi/HxCcKzRAGWk/hq720.jpg?sqp=-oaymwEcCOgCEMoBSFXyq4qpAw4IARUAAIhCGAFwAcABBg==&rs=AOn4CLD5CNX5XaQAKrLpPq0nxmyUjP5yUw",
|
||||
width: 360,
|
||||
height: 202,
|
||||
),
|
||||
Thumbnail(
|
||||
url: "https://i.ytimg.com/vi/HxCcKzRAGWk/hq720.jpg?sqp=-oaymwEcCNAFEJQDSFXyq4qpAw4IARUAAIhCGAFwAcABBg==&rs=AOn4CLANuDaGE9jI_-go6cS_nU3qCu6LRg",
|
||||
width: 720,
|
||||
height: 404,
|
||||
),
|
||||
],
|
||||
channel: Some(ChannelTag(
|
||||
id: "UCGr-rTYtP1m-r_-ncspdVQQ",
|
||||
name: "JAZZ & BLUES",
|
||||
avatar: [
|
||||
Thumbnail(
|
||||
url: "https://yt3.ggpht.com/zqAxVISjt1hyzRzZKxRTvJfgEc5k2Luf-aEE55ohjUvt0QvqIRvmFBNC6UKj2TxlZrzGo8QMNA=s68-c-k-c0x00ffffff-no-rj",
|
||||
width: 68,
|
||||
height: 68,
|
||||
),
|
||||
],
|
||||
verification: None,
|
||||
subscriber_count: None,
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
publish_date_txt: Some("Streamed 3 months ago"),
|
||||
view_count: Some(3156236),
|
||||
is_live: false,
|
||||
is_short: false,
|
||||
is_upcoming: false,
|
||||
short_description: Some("-----------------------------------------------------------------------------------\n✔Thanks for watching! Have a nice day!\n✔Don\'t forget LIKE - SHARE - COMMENT\n#bluesmusic#slowblues#bluesrock..."),
|
||||
),
|
||||
VideoItem(
|
||||
id: "HlHYOdZePSE",
|
||||
name: "Healing Music for Anxiety Disorders, Fears, Depression and Eliminate Negative Thoughts",
|
||||
duration: None,
|
||||
thumbnail: [
|
||||
Thumbnail(
|
||||
url: "https://i.ytimg.com/vi/HlHYOdZePSE/hq720_live.jpg?sqp=COjxqZoG-oaymwEcCOgCEMoBSFXyq4qpAw4IARUAAIhCGAFwAcABBg==&rs=AOn4CLBeqmmnli6rVdK1k7vcHlwE3kiNaw",
|
||||
width: 360,
|
||||
height: 202,
|
||||
),
|
||||
Thumbnail(
|
||||
url: "https://i.ytimg.com/vi/HlHYOdZePSE/hq720_live.jpg?sqp=COjxqZoG-oaymwEcCNAFEJQDSFXyq4qpAw4IARUAAIhCGAFwAcABBg==&rs=AOn4CLAk9H5lapp7KBhJCER7uRCr0fDRgg",
|
||||
width: 720,
|
||||
height: 404,
|
||||
),
|
||||
],
|
||||
channel: Some(ChannelTag(
|
||||
id: "UCqNYK5QArQRZSIR8v6_FCfA",
|
||||
name: "Tranquil Music",
|
||||
avatar: [
|
||||
Thumbnail(
|
||||
url: "https://yt3.ggpht.com/YJUUVEayRZKNtFzWEiYgvxp9XOBw9-ioxiYErE0cNDTYNvkxHBCiuUXse4-a_yaYfSS-GfT-MQ=s68-c-k-c0x00ffffff-no-rj",
|
||||
width: 68,
|
||||
height: 68,
|
||||
),
|
||||
],
|
||||
verification: None,
|
||||
subscriber_count: None,
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
publish_date_txt: None,
|
||||
view_count: Some(1585),
|
||||
is_live: true,
|
||||
is_short: false,
|
||||
is_upcoming: false,
|
||||
short_description: Some("Healing Music for Anxiety Disorders, Fears, Depression and Eliminate Negative Thoughts\n#HealingMusic #RelaxingMusic #TranquilMusic\n__________________________________\nMusic for:\nChakra healing...."),
|
||||
),
|
||||
VideoItem(
|
||||
id: "CJ2AH3LJeic",
|
||||
name: "Coldplay Greatest Hits Full Album 2022 New Songs of Coldplay 2022",
|
||||
duration: Some(7781),
|
||||
thumbnail: [
|
||||
Thumbnail(
|
||||
url: "https://i.ytimg.com/vi/CJ2AH3LJeic/hq720.jpg?sqp=-oaymwEcCOgCEMoBSFXyq4qpAw4IARUAAIhCGAFwAcABBg==&rs=AOn4CLC3A9sBlWQZmFUI9BYe5KzvATqiqw",
|
||||
width: 360,
|
||||
height: 202,
|
||||
),
|
||||
Thumbnail(
|
||||
url: "https://i.ytimg.com/vi/CJ2AH3LJeic/hq720.jpg?sqp=-oaymwEcCNAFEJQDSFXyq4qpAw4IARUAAIhCGAFwAcABBg==&rs=AOn4CLBaKSeSRdcDjEqQxrAfPaQmDJecvg",
|
||||
width: 720,
|
||||
height: 404,
|
||||
),
|
||||
],
|
||||
channel: Some(ChannelTag(
|
||||
id: "UCdK2lzwelugXGhR9SCWuEew",
|
||||
name: "PLAY MUSIC",
|
||||
avatar: [
|
||||
Thumbnail(
|
||||
url: "https://yt3.ggpht.com/ytc/AMLnZu8fIT4MTyobgM_deRkvcWBMIhKpAeIGfgqqob5p=s68-c-k-c0x00ffffff-no-rj",
|
||||
width: 68,
|
||||
height: 68,
|
||||
),
|
||||
],
|
||||
verification: None,
|
||||
subscriber_count: None,
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
publish_date_txt: Some("7 months ago"),
|
||||
view_count: Some(5595965),
|
||||
is_live: false,
|
||||
is_short: false,
|
||||
is_upcoming: false,
|
||||
short_description: Some("▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬\nSubscribe channel for more videos:\n🔔Subscribe: https://bit.ly/2UbIZFv\n⚡Facebook: https://bitly.com.vn/gXDsC..."),
|
||||
),
|
||||
VideoItem(
|
||||
id: "KJwzKxQ81iA",
|
||||
name: "Handmade Candy Making Collection / 수제 사탕 만들기 모음 / Korean Candy Store",
|
||||
duration: Some(3152),
|
||||
thumbnail: [
|
||||
Thumbnail(
|
||||
url: "https://i.ytimg.com/vi/KJwzKxQ81iA/hq720.jpg?sqp=-oaymwEcCOgCEMoBSFXyq4qpAw4IARUAAIhCGAFwAcABBg==&rs=AOn4CLCtm3YNbp3mK6RjsACZuz7fs-TUYA",
|
||||
width: 360,
|
||||
height: 202,
|
||||
),
|
||||
Thumbnail(
|
||||
url: "https://i.ytimg.com/vi/KJwzKxQ81iA/hq720.jpg?sqp=-oaymwEcCNAFEJQDSFXyq4qpAw4IARUAAIhCGAFwAcABBg==&rs=AOn4CLAVzCHCFbAyBRebsCKcSDxaWq0x6A",
|
||||
width: 720,
|
||||
height: 404,
|
||||
),
|
||||
],
|
||||
channel: Some(ChannelTag(
|
||||
id: "UCdGwDjTgbSwQDZ8dYOdrplg",
|
||||
name: "Soon Films 순필름",
|
||||
avatar: [
|
||||
Thumbnail(
|
||||
url: "https://yt3.ggpht.com/ytc/AMLnZu_eXMJm3sINr84rGTr3aiXD-OZ43aqx4yuNq9wjXw=s68-c-k-c0x00ffffff-no-rj",
|
||||
width: 68,
|
||||
height: 68,
|
||||
),
|
||||
],
|
||||
verification: None,
|
||||
subscriber_count: None,
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
publish_date_txt: Some("1 month ago"),
|
||||
view_count: Some(3127238),
|
||||
is_live: false,
|
||||
is_short: false,
|
||||
is_upcoming: false,
|
||||
short_description: Some("00:00 Handmade Candy Making\n13:43 Delicate Handmade Candy Making\n28:33 Rainbow Lollipop Handmade Candy Making\n39:10 Cute Handmade Candy Making"),
|
||||
),
|
||||
],
|
||||
ctoken: Some("4qmFsgKbAxIPRkV3aGF0X3RvX3dhdGNoGuoCQ0JoNmlBSk5aMjlKYjB0NmVtOWlTR3hxVFRSdlYyMHdTMkYzYjFwbFdGSm1ZMGRHYmxwV09YcGliVVozWXpKb2RtUkdPWGxhVjJSd1lqSTFhR0pDU1daWFZFSXhUbFpuZDFSV09YSldNRlp0WkRCT1JWTlZPV3BsU0U1WFZHNWtiRXhWY0ZSa1ZrSlRXbmh2ZEVGQlFteGlaMEZDVmxaTlFVRlZVa1pCUVVWQlVtdFdNMkZIUmpCWU0xSjJXRE5rYUdSSFRtOUJRVVZCUVZGRlFVRkJSVUZCVVVGQlFWRkZRVmxyUlVsQlFrbFVZMGRHYmxwV09YcGliVVozWXpKb2RtUkdPVEJpTW5Sc1ltaHZWRU5MVDNJeFpuSjROR1p2UTBaU1YwSm1RVzlrVkZWSlN6RnBTVlJEUzA5eU1XWnllRFJtYjBOR1VsZENaa0Z2WkZSVlNVc3hkbkZqZURjd1NrRm5aMW8lM0SaAhpicm93c2UtZmVlZEZFd2hhdF90b193YXRjaA%3D%3D"),
|
||||
visitor_data: Some("CgtjTXNGWnhNcjdORSiq8qmaBg%3D%3D"),
|
||||
endpoint: browse,
|
||||
)
|
|
@ -2,38 +2,15 @@ use std::borrow::Cow;
|
|||
|
||||
use crate::{
|
||||
error::{Error, ExtractionError},
|
||||
model::{
|
||||
paginator::{ContinuationEndpoint, Paginator},
|
||||
VideoItem,
|
||||
},
|
||||
param::Language,
|
||||
model::VideoItem,
|
||||
serializer::MapResult,
|
||||
};
|
||||
|
||||
use super::{response, ClientType, MapResponse, QBrowse, QBrowseParams, RustyPipeQuery};
|
||||
use super::{response, ClientType, MapRespCtx, MapResponse, QBrowseParams, RustyPipeQuery};
|
||||
|
||||
impl RustyPipeQuery {
|
||||
/// Get the videos from the YouTube startpage
|
||||
#[tracing::instrument(skip(self))]
|
||||
pub async fn startpage(&self) -> Result<Paginator<VideoItem>, Error> {
|
||||
let context = self.get_context(ClientType::Desktop, true, None).await;
|
||||
let request_body = QBrowse {
|
||||
context,
|
||||
browse_id: "FEwhat_to_watch",
|
||||
};
|
||||
|
||||
self.execute_request::<response::Startpage, _, _>(
|
||||
ClientType::Desktop,
|
||||
"startpage",
|
||||
"",
|
||||
"browse",
|
||||
&request_body,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
/// Get the videos from the YouTube trending page
|
||||
#[tracing::instrument(skip(self))]
|
||||
#[tracing::instrument(skip(self), level = "error")]
|
||||
pub async fn trending(&self) -> Result<Vec<VideoItem>, Error> {
|
||||
let context = self.get_context(ClientType::Desktop, true, None).await;
|
||||
let request_body = QBrowseParams {
|
||||
|
@ -53,43 +30,10 @@ impl RustyPipeQuery {
|
|||
}
|
||||
}
|
||||
|
||||
impl MapResponse<Paginator<VideoItem>> for response::Startpage {
|
||||
fn map_response(
|
||||
self,
|
||||
_id: &str,
|
||||
lang: crate::param::Language,
|
||||
_deobf: Option<&crate::deobfuscate::DeobfData>,
|
||||
vdata: Option<&str>,
|
||||
) -> Result<MapResult<Paginator<VideoItem>>, ExtractionError> {
|
||||
let grid = self
|
||||
.contents
|
||||
.two_column_browse_results_renderer
|
||||
.contents
|
||||
.into_iter()
|
||||
.next()
|
||||
.ok_or(ExtractionError::InvalidData(Cow::Borrowed("no contents")))?
|
||||
.tab_renderer
|
||||
.content
|
||||
.section_list_renderer
|
||||
.contents;
|
||||
|
||||
Ok(map_startpage_videos(
|
||||
grid,
|
||||
lang,
|
||||
self.response_context
|
||||
.visitor_data
|
||||
.or_else(|| vdata.map(str::to_owned)),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
impl MapResponse<Vec<VideoItem>> for response::Trending {
|
||||
fn map_response(
|
||||
self,
|
||||
_id: &str,
|
||||
lang: crate::param::Language,
|
||||
_deobf: Option<&crate::deobfuscate::DeobfData>,
|
||||
_vdata: Option<&str>,
|
||||
ctx: &MapRespCtx<'_>,
|
||||
) -> Result<MapResult<Vec<VideoItem>>, ExtractionError> {
|
||||
let items = self
|
||||
.contents
|
||||
|
@ -103,7 +47,7 @@ impl MapResponse<Vec<VideoItem>> for response::Trending {
|
|||
.section_list_renderer
|
||||
.contents;
|
||||
|
||||
let mut mapper = response::YouTubeListMapper::<VideoItem>::new(lang);
|
||||
let mut mapper = response::YouTubeListMapper::<VideoItem>::new(ctx.lang);
|
||||
mapper.map_response(items);
|
||||
|
||||
Ok(MapResult {
|
||||
|
@ -113,26 +57,6 @@ impl MapResponse<Vec<VideoItem>> for response::Trending {
|
|||
}
|
||||
}
|
||||
|
||||
fn map_startpage_videos(
|
||||
videos: MapResult<Vec<response::YouTubeListItem>>,
|
||||
lang: Language,
|
||||
visitor_data: Option<String>,
|
||||
) -> MapResult<Paginator<VideoItem>> {
|
||||
let mut mapper = response::YouTubeListMapper::<VideoItem>::new(lang);
|
||||
mapper.map_response(videos);
|
||||
|
||||
MapResult {
|
||||
c: Paginator::new_ext(
|
||||
None,
|
||||
mapper.items,
|
||||
mapper.ctoken,
|
||||
visitor_data,
|
||||
ContinuationEndpoint::Browse,
|
||||
),
|
||||
warnings: mapper.warnings,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::{fs::File, io::BufReader};
|
||||
|
@ -141,35 +65,12 @@ mod tests {
|
|||
use rstest::rstest;
|
||||
|
||||
use crate::{
|
||||
client::{response, MapResponse},
|
||||
model::{paginator::Paginator, VideoItem},
|
||||
param::Language,
|
||||
client::{response, MapRespCtx, MapResponse},
|
||||
model::VideoItem,
|
||||
serializer::MapResult,
|
||||
util::tests::TESTFILES,
|
||||
};
|
||||
|
||||
#[test]
|
||||
fn map_startpage() {
|
||||
let json_path = path!(*TESTFILES / "trends" / "startpage.json");
|
||||
let json_file = File::open(json_path).unwrap();
|
||||
|
||||
let startpage: response::Startpage =
|
||||
serde_json::from_reader(BufReader::new(json_file)).unwrap();
|
||||
let map_res: MapResult<Paginator<VideoItem>> = startpage
|
||||
.map_response("", Language::En, None, None)
|
||||
.unwrap();
|
||||
|
||||
assert!(
|
||||
map_res.warnings.is_empty(),
|
||||
"deserialization/mapping warnings: {:?}",
|
||||
map_res.warnings
|
||||
);
|
||||
|
||||
insta::assert_ron_snapshot!("map_startpage", map_res.c, {
|
||||
".items[].publish_date" => "[date]",
|
||||
});
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[case::base("videos")]
|
||||
#[case::page_header_renderer("20230501_page_header_renderer")]
|
||||
|
@ -177,11 +78,10 @@ mod tests {
|
|||
let json_path = path!(*TESTFILES / "trends" / format!("trending_{name}.json"));
|
||||
let json_file = File::open(json_path).unwrap();
|
||||
|
||||
let startpage: response::Trending =
|
||||
let trending: response::Trending =
|
||||
serde_json::from_reader(BufReader::new(json_file)).unwrap();
|
||||
let map_res: MapResult<Vec<VideoItem>> = startpage
|
||||
.map_response("", Language::En, None, None)
|
||||
.unwrap();
|
||||
let map_res: MapResult<Vec<VideoItem>> =
|
||||
trending.map_response(&MapRespCtx::test("")).unwrap();
|
||||
|
||||
assert!(
|
||||
map_res.warnings.is_empty(),
|
||||
|
|
|
@ -5,14 +5,13 @@ use serde::Serialize;
|
|||
use crate::{
|
||||
error::{Error, ExtractionError},
|
||||
model::UrlTarget,
|
||||
param::Language,
|
||||
serializer::MapResult,
|
||||
util,
|
||||
};
|
||||
|
||||
use super::{
|
||||
response::{self, url_endpoint::NavigationEndpoint},
|
||||
ClientType, MapResponse, RustyPipeQuery, YTContext,
|
||||
ClientType, MapRespCtx, MapResponse, RustyPipeQuery, YTContext,
|
||||
};
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
|
@ -59,7 +58,7 @@ impl RustyPipeQuery {
|
|||
/// );
|
||||
/// # });
|
||||
/// ```
|
||||
#[tracing::instrument(skip(self))]
|
||||
#[tracing::instrument(skip(self), level = "error")]
|
||||
pub async fn resolve_url<S: AsRef<str> + Debug>(
|
||||
self,
|
||||
url: S,
|
||||
|
@ -237,7 +236,7 @@ impl RustyPipeQuery {
|
|||
/// );
|
||||
/// # });
|
||||
/// ```
|
||||
#[tracing::instrument(skip(self))]
|
||||
#[tracing::instrument(skip(self), level = "error")]
|
||||
pub async fn resolve_string<S: AsRef<str> + Debug>(
|
||||
self,
|
||||
s: S,
|
||||
|
@ -325,13 +324,7 @@ impl RustyPipeQuery {
|
|||
}
|
||||
|
||||
impl MapResponse<UrlTarget> for response::ResolvedUrl {
|
||||
fn map_response(
|
||||
self,
|
||||
_id: &str,
|
||||
_lang: Language,
|
||||
_deobf: Option<&crate::deobfuscate::DeobfData>,
|
||||
_vdata: Option<&str>,
|
||||
) -> Result<MapResult<UrlTarget>, ExtractionError> {
|
||||
fn map_response(self, _ctx: &MapRespCtx<'_>) -> Result<MapResult<UrlTarget>, ExtractionError> {
|
||||
let pt = self.endpoint.page_type();
|
||||
if let NavigationEndpoint::Browse {
|
||||
browse_endpoint, ..
|
||||
|
|
|
@ -15,7 +15,7 @@ use crate::{
|
|||
|
||||
use super::{
|
||||
response::{self, video_details::Payload, IconType},
|
||||
ClientType, MapResponse, QContinuation, RustyPipeQuery, YTContext,
|
||||
ClientType, MapRespCtx, MapResponse, QContinuation, RustyPipeQuery, YTContext,
|
||||
};
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
|
@ -31,7 +31,7 @@ struct QVideo<'a> {
|
|||
|
||||
impl RustyPipeQuery {
|
||||
/// Get the metadata for a video
|
||||
#[tracing::instrument(skip(self))]
|
||||
#[tracing::instrument(skip(self), level = "error")]
|
||||
pub async fn video_details<S: AsRef<str> + Debug>(
|
||||
&self,
|
||||
video_id: S,
|
||||
|
@ -56,7 +56,7 @@ impl RustyPipeQuery {
|
|||
}
|
||||
|
||||
/// Get the comments for a video using the continuation token obtained from `rusty_pipe_query.video_details()`
|
||||
#[tracing::instrument(skip(self))]
|
||||
#[tracing::instrument(skip(self), level = "error")]
|
||||
pub async fn video_comments<S: AsRef<str> + Debug>(
|
||||
&self,
|
||||
ctoken: S,
|
||||
|
@ -89,28 +89,26 @@ impl RustyPipeQuery {
|
|||
impl MapResponse<VideoDetails> for response::VideoDetails {
|
||||
fn map_response(
|
||||
self,
|
||||
id: &str,
|
||||
lang: Language,
|
||||
_deobf: Option<&crate::deobfuscate::DeobfData>,
|
||||
vdata: Option<&str>,
|
||||
ctx: &MapRespCtx<'_>,
|
||||
) -> Result<MapResult<VideoDetails>, ExtractionError> {
|
||||
let mut warnings = Vec::new();
|
||||
|
||||
let contents = self.contents.ok_or_else(|| ExtractionError::NotFound {
|
||||
id: id.to_owned(),
|
||||
id: ctx.id.to_owned(),
|
||||
msg: "no content".into(),
|
||||
})?;
|
||||
let current_video_endpoint =
|
||||
self.current_video_endpoint
|
||||
.ok_or_else(|| ExtractionError::NotFound {
|
||||
id: id.to_owned(),
|
||||
id: ctx.id.to_owned(),
|
||||
msg: "no current_video_endpoint".into(),
|
||||
})?;
|
||||
|
||||
let video_id = current_video_endpoint.watch_endpoint.video_id;
|
||||
if id != video_id {
|
||||
if ctx.id != video_id {
|
||||
return Err(ExtractionError::WrongResult(format!(
|
||||
"got wrong video id {video_id}, expected {id}"
|
||||
"got wrong video id {}, expected {}",
|
||||
video_id, ctx.id
|
||||
)));
|
||||
}
|
||||
|
||||
|
@ -120,7 +118,7 @@ impl MapResponse<VideoDetails> for response::VideoDetails {
|
|||
.results
|
||||
.contents
|
||||
.ok_or_else(|| ExtractionError::NotFound {
|
||||
id: id.into(),
|
||||
id: ctx.id.into(),
|
||||
msg: "no primary_results".into(),
|
||||
})?;
|
||||
warnings.append(&mut primary_results.warnings);
|
||||
|
@ -189,7 +187,7 @@ impl MapResponse<VideoDetails> for response::VideoDetails {
|
|||
// so we ignore parse errors here for now
|
||||
like_text.and_then(|txt| util::parse_numeric(&txt).ok()),
|
||||
date_text.as_deref().and_then(|txt| {
|
||||
timeago::parse_textual_date_or_warn(lang, txt, &mut warnings)
|
||||
timeago::parse_textual_date_or_warn(ctx.lang, txt, &mut warnings)
|
||||
}),
|
||||
date_text,
|
||||
view_count
|
||||
|
@ -207,7 +205,7 @@ impl MapResponse<VideoDetails> for response::VideoDetails {
|
|||
let comment_count = comment_count_section.and_then(|s| {
|
||||
util::parse_large_numstr_or_warn::<u64>(
|
||||
&s.comments_entry_point_header_renderer.comment_count,
|
||||
lang,
|
||||
ctx.lang,
|
||||
&mut warnings,
|
||||
)
|
||||
});
|
||||
|
@ -275,7 +273,7 @@ impl MapResponse<VideoDetails> for response::VideoDetails {
|
|||
let visitor_data = self
|
||||
.response_context
|
||||
.visitor_data
|
||||
.or_else(|| vdata.map(str::to_owned));
|
||||
.or_else(|| ctx.visitor_data.map(str::to_owned));
|
||||
let recommended = contents
|
||||
.two_column_watch_next_results
|
||||
.secondary_results
|
||||
|
@ -285,7 +283,7 @@ impl MapResponse<VideoDetails> for response::VideoDetails {
|
|||
r,
|
||||
sr.secondary_results.continuations,
|
||||
visitor_data.clone(),
|
||||
lang,
|
||||
ctx.lang,
|
||||
);
|
||||
warnings.append(&mut res.warnings);
|
||||
res.c
|
||||
|
@ -350,7 +348,7 @@ impl MapResponse<VideoDetails> for response::VideoDetails {
|
|||
avatar: owner.thumbnail.into(),
|
||||
verification: owner.badges.into(),
|
||||
subscriber_count: owner.subscriber_count_text.and_then(|txt| {
|
||||
util::parse_large_numstr_or_warn(&txt, lang, &mut warnings)
|
||||
util::parse_large_numstr_or_warn(&txt, ctx.lang, &mut warnings)
|
||||
}),
|
||||
},
|
||||
view_count,
|
||||
|
@ -385,10 +383,7 @@ impl MapResponse<VideoDetails> for response::VideoDetails {
|
|||
impl MapResponse<Paginator<Comment>> for response::VideoComments {
|
||||
fn map_response(
|
||||
self,
|
||||
_id: &str,
|
||||
lang: Language,
|
||||
_deobf: Option<&crate::deobfuscate::DeobfData>,
|
||||
_vdata: Option<&str>,
|
||||
ctx: &MapRespCtx<'_>,
|
||||
) -> Result<MapResult<Paginator<Comment>>, ExtractionError> {
|
||||
let received_endpoints = self.on_response_received_endpoints;
|
||||
let mut warnings = Vec::new();
|
||||
|
@ -415,7 +410,7 @@ impl MapResponse<Paginator<Comment>> for response::VideoComments {
|
|||
comment.comment_renderer,
|
||||
Some(thread.replies),
|
||||
thread.rendering_priority,
|
||||
lang,
|
||||
ctx.lang,
|
||||
&mut warnings,
|
||||
));
|
||||
} else if let Some(vm) = thread.comment_view_model {
|
||||
|
@ -424,7 +419,7 @@ impl MapResponse<Paginator<Comment>> for response::VideoComments {
|
|||
&mut mutations,
|
||||
Some(thread.replies),
|
||||
thread.rendering_priority,
|
||||
lang,
|
||||
ctx.lang,
|
||||
&mut warnings,
|
||||
) {
|
||||
comments.push(c);
|
||||
|
@ -440,7 +435,7 @@ impl MapResponse<Paginator<Comment>> for response::VideoComments {
|
|||
comment,
|
||||
None,
|
||||
response::video_details::CommentPriority::RenderingPriorityUnknown,
|
||||
lang,
|
||||
ctx.lang,
|
||||
&mut warnings,
|
||||
));
|
||||
}
|
||||
|
@ -450,7 +445,7 @@ impl MapResponse<Paginator<Comment>> for response::VideoComments {
|
|||
&mut mutations,
|
||||
None,
|
||||
response::video_details::CommentPriority::RenderingPriorityUnknown,
|
||||
lang,
|
||||
ctx.lang,
|
||||
&mut warnings,
|
||||
) {
|
||||
comments.push(c);
|
||||
|
@ -654,8 +649,7 @@ mod tests {
|
|||
use rstest::rstest;
|
||||
|
||||
use crate::{
|
||||
client::{response, MapResponse},
|
||||
param::Language,
|
||||
client::{response, MapRespCtx, MapResponse},
|
||||
util::tests::TESTFILES,
|
||||
};
|
||||
|
||||
|
@ -676,7 +670,7 @@ mod tests {
|
|||
|
||||
let details: response::VideoDetails =
|
||||
serde_json::from_reader(BufReader::new(json_file)).unwrap();
|
||||
let map_res = details.map_response(id, Language::En, None, None).unwrap();
|
||||
let map_res = details.map_response(&MapRespCtx::test(id)).unwrap();
|
||||
|
||||
assert!(
|
||||
map_res.warnings.is_empty(),
|
||||
|
@ -696,9 +690,7 @@ mod tests {
|
|||
|
||||
let details: response::VideoDetails =
|
||||
serde_json::from_reader(BufReader::new(json_file)).unwrap();
|
||||
let err = details
|
||||
.map_response("", Language::En, None, None)
|
||||
.unwrap_err();
|
||||
let err = details.map_response(&MapRespCtx::test("")).unwrap_err();
|
||||
assert!(matches!(
|
||||
err,
|
||||
crate::error::ExtractionError::NotFound { .. }
|
||||
|
@ -716,7 +708,7 @@ mod tests {
|
|||
|
||||
let comments: response::VideoComments =
|
||||
serde_json::from_reader(BufReader::new(json_file)).unwrap();
|
||||
let map_res = comments.map_response("", Language::En, None, None).unwrap();
|
||||
let map_res = comments.map_response(&MapRespCtx::test("")).unwrap();
|
||||
|
||||
assert!(
|
||||
map_res.warnings.is_empty(),
|
||||
|
|
|
@ -57,7 +57,7 @@ impl DeobfData {
|
|||
res
|
||||
}
|
||||
|
||||
fn extract_fns(js_url: &str, player_js: &str) -> Result<Self, Error> {
|
||||
pub fn extract_fns(js_url: &str, player_js: &str) -> Result<Self, Error> {
|
||||
let sig_fn = get_sig_fn(player_js)?;
|
||||
let nsig_fn = get_nsig_fn(player_js)?;
|
||||
let sts = get_sts(player_js)?;
|
||||
|
@ -84,28 +84,19 @@ impl Deobfuscator {
|
|||
|
||||
/// Deobfuscate the `s` parameter from the `signature_cipher` field
|
||||
pub fn deobfuscate_sig(&self, sig: &str) -> Result<String, DeobfError> {
|
||||
let res = self.ctx.call_function(DEOBF_SIG_FUNC_NAME, vec![sig])?;
|
||||
let res = self.ctx.call_function(DEOBF_SIG_FUNC_NAME, [sig])?;
|
||||
|
||||
res.as_str().map_or(
|
||||
Err(DeobfError::Other("sig deobfuscation func returned null")),
|
||||
|res| {
|
||||
tracing::debug!("deobfuscated sig");
|
||||
Ok(res.to_owned())
|
||||
},
|
||||
)
|
||||
res.into_string()
|
||||
.ok_or(DeobfError::Other("sig deobfuscation fn returned no string"))
|
||||
}
|
||||
|
||||
/// Deobfuscate the `n` stream URL parameter to circumvent throttling
|
||||
pub fn deobfuscate_nsig(&self, nsig: &str) -> Result<String, DeobfError> {
|
||||
let res = self.ctx.call_function(DEOBF_NSIG_FUNC_NAME, vec![nsig])?;
|
||||
let res = self.ctx.call_function(DEOBF_NSIG_FUNC_NAME, [nsig])?;
|
||||
|
||||
res.as_str().map_or(
|
||||
Err(DeobfError::Other("nsig deobfuscation func returned null")),
|
||||
|res| {
|
||||
tracing::debug!("deobfuscated nsig");
|
||||
Ok(res.to_owned())
|
||||
},
|
||||
)
|
||||
res.into_string().ok_or(DeobfError::Other(
|
||||
"nsig deobfuscation fn returned no string",
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -144,12 +135,9 @@ fn get_sig_fn(player_js: &str) -> Result<String, DeobfError> {
|
|||
|
||||
let deobfuscate_function = format!(
|
||||
"var {};",
|
||||
function_pattern
|
||||
&function_pattern
|
||||
.captures(player_js)
|
||||
.ok_or(DeobfError::Extraction("deobf function"))?
|
||||
.get(1)
|
||||
.unwrap()
|
||||
.as_str()
|
||||
.ok_or(DeobfError::Extraction("deobf function"))?[1]
|
||||
);
|
||||
|
||||
static HELPER_OBJECT_NAME_REGEX: Lazy<Regex> =
|
||||
|
@ -168,59 +156,38 @@ fn get_sig_fn(player_js: &str) -> Result<String, DeobfError> {
|
|||
let helper_pattern = Regex::new(&helper_pattern_str)
|
||||
.map_err(|_| DeobfError::Other("could not parse helper pattern regex"))?;
|
||||
let player_js_nonl = player_js.replace('\n', "");
|
||||
let helper_object = helper_pattern
|
||||
let helper_object = &helper_pattern
|
||||
.captures(&player_js_nonl)
|
||||
.ok_or(DeobfError::Extraction("helper object"))?
|
||||
.get(1)
|
||||
.unwrap()
|
||||
.as_str();
|
||||
.ok_or(DeobfError::Extraction("helper object"))?[1];
|
||||
|
||||
Ok(helper_object.to_owned()
|
||||
let js_fn = helper_object.to_owned()
|
||||
+ &deobfuscate_function
|
||||
+ &caller_function(DEOBF_SIG_FUNC_NAME, &dfunc_name))
|
||||
+ &caller_function(DEOBF_SIG_FUNC_NAME, &dfunc_name);
|
||||
verify_fn(&js_fn, DEOBF_SIG_FUNC_NAME)?;
|
||||
tracing::debug!("successfully extracted sig fn `{dfunc_name}`");
|
||||
|
||||
Ok(js_fn)
|
||||
}
|
||||
|
||||
fn get_nsig_fn_name(player_js: &str) -> Result<String, DeobfError> {
|
||||
fn get_nsig_fn_names(player_js: &str) -> impl Iterator<Item = String> + '_ {
|
||||
static FUNCTION_NAME_REGEX: Lazy<Regex> = Lazy::new(|| {
|
||||
Regex::new(
|
||||
r#"\.get\("n"\)\)&&\([a-zA-Z0-9$_]=([a-zA-Z0-9$_]+)(?:\[(\d+)])?\([a-zA-Z0-9$_]\)"#,
|
||||
)
|
||||
.unwrap()
|
||||
// x.get( .. y=functionName[array_num](z) .. x.set(
|
||||
Regex::new(r#"(?:\w\.get\(|index\.m3u8).+\w=(\w{2,})\[(\d+)\]\(\w\).+\w\.set\("#).unwrap()
|
||||
});
|
||||
|
||||
let fname_match = FUNCTION_NAME_REGEX
|
||||
.captures(player_js)
|
||||
.ok_or(DeobfError::Extraction("n_deobf function"))?;
|
||||
FUNCTION_NAME_REGEX
|
||||
.captures_iter(player_js)
|
||||
.filter_map(|fname_match| {
|
||||
let function_name = &fname_match[1];
|
||||
|
||||
let function_name = fname_match.get(1).unwrap().as_str();
|
||||
let array_num = fname_match[2].parse::<usize>().ok()?;
|
||||
let array_pattern_str =
|
||||
format!(r#"var {}\s*=\s*\[(.+?)]"#, regex::escape(function_name));
|
||||
let array_pattern = Regex::new(&array_pattern_str).ok()?;
|
||||
|
||||
if fname_match.len() == 1 {
|
||||
return Ok(function_name.to_owned());
|
||||
}
|
||||
|
||||
let array_num = fname_match
|
||||
.get(2)
|
||||
.unwrap()
|
||||
.as_str()
|
||||
.parse::<usize>()
|
||||
.or(Err(DeobfError::Other("could not parse array_num")))?;
|
||||
let array_pattern_str = format!(r#"var {}\s*=\s*\[(.+?)]"#, regex::escape(function_name));
|
||||
let array_pattern = Regex::new(&array_pattern_str).or(Err(DeobfError::Other(
|
||||
"could not parse helper pattern regex",
|
||||
)))?;
|
||||
|
||||
let array_str = array_pattern
|
||||
.captures(player_js)
|
||||
.ok_or(DeobfError::Extraction("n_deobf array_str"))?
|
||||
.get(1)
|
||||
.unwrap()
|
||||
.as_str();
|
||||
|
||||
let mut names = array_str.split(',');
|
||||
let name = names
|
||||
.nth(array_num)
|
||||
.ok_or(DeobfError::Extraction("n_deobf function name"))?;
|
||||
Ok(name.to_owned())
|
||||
let array_str = &array_pattern.captures(player_js)?[1];
|
||||
array_str.split(',').nth(array_num).map(str::to_owned)
|
||||
})
|
||||
}
|
||||
|
||||
fn extract_js_fn(js: &str, name: &str) -> Result<String, DeobfError> {
|
||||
|
@ -275,13 +242,44 @@ fn extract_js_fn(js: &str, name: &str) -> Result<String, DeobfError> {
|
|||
Ok(js[start..end].to_owned())
|
||||
}
|
||||
|
||||
fn get_nsig_fn(player_js: &str) -> Result<String, DeobfError> {
|
||||
let function_name = get_nsig_fn_name(player_js)?;
|
||||
let function_base = function_name.clone() + "=function";
|
||||
let offset = player_js.find(&function_base).unwrap_or_default();
|
||||
/// Verify if the deobfuscation function successfully processes a random input string
|
||||
fn verify_fn(js_fn: &str, fn_name: &str) -> Result<(), DeobfError> {
|
||||
let ctx = quick_js::Context::new().or(Err(DeobfError::Other("could not create QuickJS rt")))?;
|
||||
ctx.eval(js_fn)?;
|
||||
let res = ctx
|
||||
.call_function(fn_name, [util::generate_content_playback_nonce()])?
|
||||
.into_string()
|
||||
.ok_or(DeobfError::Other("deobfuscation fn returned no string"))?;
|
||||
if res.is_empty() {
|
||||
return Err(DeobfError::Other("deobfuscation fn returned empty string"));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
extract_js_fn(&player_js[offset..], &function_name)
|
||||
.map(|s| s + ";" + &caller_function(DEOBF_NSIG_FUNC_NAME, &function_name))
|
||||
fn get_nsig_fn(player_js: &str) -> Result<String, DeobfError> {
|
||||
let extract_fn = |name: &str| -> Result<String, DeobfError> {
|
||||
let function_base = format!("{name}=function");
|
||||
let offset = player_js
|
||||
.find(&function_base)
|
||||
.ok_or(DeobfError::Extraction("could not find function base"))?;
|
||||
|
||||
let js_fn = extract_js_fn(&player_js[offset..], name)
|
||||
.map(|s| format!("var {};{}", s, caller_function(DEOBF_NSIG_FUNC_NAME, name)))?;
|
||||
verify_fn(&js_fn, DEOBF_NSIG_FUNC_NAME)?;
|
||||
tracing::debug!("successfully extracted nsig fn `{name}`");
|
||||
Ok(js_fn)
|
||||
};
|
||||
|
||||
util::find_map_or_last_err(
|
||||
get_nsig_fn_names(player_js),
|
||||
DeobfError::Extraction("nsig function name"),
|
||||
|name| {
|
||||
extract_fn(&name).map_err(|e| {
|
||||
tracing::warn!("Failed to extract nsig fn `{name}`: {e}");
|
||||
e
|
||||
})
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
async fn get_player_js_url(http: &Client) -> Result<String, Error> {
|
||||
|
@ -295,12 +293,9 @@ async fn get_player_js_url(http: &Client) -> Result<String, Error> {
|
|||
static PLAYER_HASH_PATTERN: Lazy<Regex> = Lazy::new(|| {
|
||||
Regex::new(r"https:\\/\\/www\.youtube\.com\\/s\\/player\\/([a-z0-9]{8})\\/").unwrap()
|
||||
});
|
||||
let player_hash = PLAYER_HASH_PATTERN
|
||||
let player_hash = &PLAYER_HASH_PATTERN
|
||||
.captures(&text)
|
||||
.ok_or(DeobfError::Extraction("player hash"))?
|
||||
.get(1)
|
||||
.unwrap()
|
||||
.as_str();
|
||||
.ok_or(DeobfError::Extraction("player hash"))?[1];
|
||||
|
||||
Ok(format!(
|
||||
"https://www.youtube.com/s/player/{player_hash}/player_ias.vflset/en_US/base.js"
|
||||
|
@ -318,10 +313,7 @@ fn get_sts(player_js: &str) -> Result<String, DeobfError> {
|
|||
|
||||
Ok(STS_PATTERN
|
||||
.captures(player_js)
|
||||
.ok_or(DeobfError::Extraction("sts"))?
|
||||
.get(1)
|
||||
.unwrap()
|
||||
.as_str()
|
||||
.ok_or(DeobfError::Extraction("sts"))?[1]
|
||||
.to_owned())
|
||||
}
|
||||
|
||||
|
@ -331,6 +323,7 @@ mod tests {
|
|||
use crate::util::tests::TESTFILES;
|
||||
use path_macro::path;
|
||||
use rstest::{fixture, rstest};
|
||||
use tracing_test::traced_test;
|
||||
|
||||
static TEST_JS: Lazy<String> = Lazy::new(|| {
|
||||
let js_path = path!(*TESTFILES / "deobf" / "dummy_player.js");
|
||||
|
@ -338,7 +331,7 @@ mod tests {
|
|||
});
|
||||
|
||||
const SIG_DEOBF_FUNC: &str = r#"var qB={w8:function(a){a.reverse()},EC:function(a,b){var c=a[0];a[0]=a[b%a.length];a[b%a.length]=c},Np:function(a,b){a.splice(0,b)}};var Rva=function(a){a=a.split("");qB.Np(a,3);qB.w8(a,41);qB.EC(a,55);qB.Np(a,3);qB.w8(a,33);qB.Np(a,3);qB.EC(a,48);qB.EC(a,17);qB.EC(a,43);return a.join("")};var deobf_sig=Rva;"#;
|
||||
const NSIG_DEOBF_FUNC: &str = r#"Vo=function(a){var b=a.split(""),c=[function(d,e,f){var h=f.length;d.forEach(function(l,m,n){this.push(n[m]=f[(f.indexOf(l)-f.indexOf(this[m])+m+h--)%f.length])},e.split(""))},
|
||||
const NSIG_DEOBF_FUNC: &str = r#"var Vo=function(a){var b=a.split(""),c=[function(d,e,f){var h=f.length;d.forEach(function(l,m,n){this.push(n[m]=f[(f.indexOf(l)-f.indexOf(this[m])+m+h--)%f.length])},e.split(""))},
|
||||
928409064,-595856984,1403221911,653089124,-168714481,-1883008765,158931990,1346921902,361518508,1403221911,-362174697,-233641452,function(){for(var d=64,e=[];++d-e.length-32;){switch(d){case 91:d=44;continue;case 123:d=65;break;case 65:d-=18;continue;case 58:d=96;continue;case 46:d=95}e.push(String.fromCharCode(d))}return e},
|
||||
b,158931990,791141857,-907319795,-1776185924,1595027902,-829736173,function(d,e){e=(e%d.length+d.length)%d.length;d.splice(0,1,d.splice(e,1,d[0])[0])},
|
||||
-1274951142,function(){for(var d=64,e=[];++d-e.length-32;){switch(d){case 91:d=44;continue;case 123:d=65;break;case 65:d-=18;continue;case 58:d=96;continue;case 46:d=95}e.push(String.fromCharCode(d))}return e},
|
||||
|
@ -382,9 +375,9 @@ c[36](c[8],c[32]),c[20](c[25],c[10]),c[2](c[22],c[8]),c[32](c[20],c[16]),c[32](c
|
|||
}
|
||||
|
||||
#[test]
|
||||
fn t_get_nsig_fn_name() {
|
||||
let name = get_nsig_fn_name(&TEST_JS).unwrap();
|
||||
assert_eq!(name, "Vo");
|
||||
fn t_get_nsig_fn_names() {
|
||||
let names = get_nsig_fn_names(&TEST_JS).collect::<Vec<_>>();
|
||||
assert_eq!(names, ["Vo"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
@ -435,14 +428,15 @@ c[36](c[8],c[32]),c[20](c[25],c[10]),c[2](c[22],c[8]),c[32](c[20],c[16]),c[32](c
|
|||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[traced_test]
|
||||
async fn t_update() {
|
||||
let client = Client::new();
|
||||
let deobf_data = DeobfData::extract(client, None).await.unwrap();
|
||||
let deobf = Deobfuscator::new(&deobf_data).unwrap();
|
||||
|
||||
let deobf_sig = deobf.deobfuscate_sig("GOqGOqGOq0QJ8wRAIgaryQHfplJ9xJSKFywyaSMHuuwZYsoMTAvRvfm51qIGECIA5061zWeyfMPX9hEl_U6f9J0tr7GTJMKyPf5XNrJb5fb5i").unwrap();
|
||||
println!("{deobf_sig}");
|
||||
assert!(deobf_sig.len() >= 100);
|
||||
let deobf_nsig = deobf.deobfuscate_nsig("WHbZ-Nj2TSJxder").unwrap();
|
||||
println!("{deobf_nsig}");
|
||||
assert!(deobf_nsig.len() >= 6);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
#![doc = include_str!("../README.md")]
|
||||
#![cfg_attr(docsrs, feature(doc_cfg))]
|
||||
#![forbid(unsafe_code)]
|
||||
#![warn(missing_docs, clippy::todo, clippy::dbg_macro)]
|
||||
|
||||
|
@ -18,3 +19,6 @@ pub mod model;
|
|||
pub mod param;
|
||||
pub mod report;
|
||||
pub mod validate;
|
||||
|
||||
/// Version of the RustyPipe crate
|
||||
pub const VERSION: &str = env!("CARGO_PKG_VERSION");
|
||||
|
|
|
@ -15,7 +15,7 @@ use serde::{Deserialize, Serialize};
|
|||
use time::{Date, OffsetDateTime};
|
||||
|
||||
use self::{paginator::Paginator, richtext::RichText};
|
||||
use crate::{error::Error, param::Country, validate};
|
||||
use crate::{client::ClientType, error::Error, param::Country, validate};
|
||||
|
||||
/*
|
||||
#COMMON
|
||||
|
@ -143,6 +143,8 @@ pub struct VideoPlayer {
|
|||
pub dash_manifest_url: Option<String>,
|
||||
/// Video frames for seek preview
|
||||
pub preview_frames: Vec<Frameset>,
|
||||
/// Client type with which the player was fetched
|
||||
pub client_type: ClientType,
|
||||
/// YouTube visitor data cookie
|
||||
pub visitor_data: Option<String>,
|
||||
}
|
||||
|
@ -154,7 +156,7 @@ pub struct VideoPlayerDetails {
|
|||
/// Unique YouTube video ID
|
||||
pub id: String,
|
||||
/// Video title
|
||||
pub name: String,
|
||||
pub name: Option<String>,
|
||||
/// Video description in plaintext format
|
||||
pub description: Option<String>,
|
||||
/// Video duration in seconds
|
||||
|
@ -163,10 +165,12 @@ pub struct VideoPlayerDetails {
|
|||
pub duration: u32,
|
||||
/// Video thumbnail
|
||||
pub thumbnail: Vec<Thumbnail>,
|
||||
/// Channel of the video
|
||||
pub channel: ChannelId,
|
||||
/// Channel ID of the video
|
||||
pub channel_id: String,
|
||||
/// Channel name of the video
|
||||
pub channel_name: Option<String>,
|
||||
/// Number of views / current viewers in case of a livestream.
|
||||
pub view_count: u64,
|
||||
pub view_count: Option<u64>,
|
||||
/// List of words that describe the topic of the video
|
||||
pub keywords: Vec<String>,
|
||||
/// True if the video is an active livestream
|
||||
|
@ -211,9 +215,6 @@ pub struct VideoStream {
|
|||
pub format: VideoFormat,
|
||||
/// Video codec
|
||||
pub codec: VideoCodec,
|
||||
/// True if the deobfuscation of the nsig url parameter failed
|
||||
/// and the stream will be throttled
|
||||
pub throttled: bool,
|
||||
}
|
||||
|
||||
/// Audio stream
|
||||
|
@ -259,9 +260,6 @@ pub struct AudioStream {
|
|||
///
|
||||
/// The loudness parameter is not available when using the Android client.
|
||||
pub loudness_db: Option<f32>,
|
||||
/// True if the deobfuscation of the nsig url parameter failed
|
||||
/// and the stream will be throttled
|
||||
pub throttled: bool,
|
||||
/// Audio track information
|
||||
///
|
||||
/// Videos can have multiple audio tracks (different languages).
|
||||
|
@ -636,7 +634,6 @@ pub struct ChannelTag {
|
|||
#[derive(
|
||||
Default, Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord, Hash,
|
||||
)]
|
||||
#[non_exhaustive]
|
||||
pub enum Verification {
|
||||
#[default]
|
||||
/// Unverified channel (default)
|
||||
|
@ -703,11 +700,15 @@ pub struct Channel<T> {
|
|||
pub id: String,
|
||||
/// Channel name
|
||||
pub name: String,
|
||||
/// YouTube channel handle (e.g. `@EEVblog`)
|
||||
pub handle: Option<String>,
|
||||
/// Channel subscriber count
|
||||
///
|
||||
/// [`None`] if the subscriber count was hidden by the owner
|
||||
/// or could not be parsed.
|
||||
pub subscriber_count: Option<u64>,
|
||||
/// Number of videos
|
||||
pub video_count: Option<u64>,
|
||||
/// Channel avatar / profile picture
|
||||
pub avatar: Vec<Thumbnail>,
|
||||
/// Channel verification mark
|
||||
|
@ -716,15 +717,8 @@ pub struct Channel<T> {
|
|||
pub description: String,
|
||||
/// List of words to describe the topic of the channel
|
||||
pub tags: Vec<String>,
|
||||
/// Custom URL set by the channel owner
|
||||
/// (e.g. <https://www.youtube.com/c/EevblogDave>)
|
||||
pub vanity_url: Option<String>,
|
||||
/// Banner image shown above the channel
|
||||
pub banner: Vec<Thumbnail>,
|
||||
/// Banner image shown above the channel (small format for mobile)
|
||||
pub mobile_banner: Vec<Thumbnail>,
|
||||
/// Banner image shown above the channel (16:9 fullscreen format for TV)
|
||||
pub tv_banner: Vec<Thumbnail>,
|
||||
/// Does the channel have a *Shorts* tab?
|
||||
pub has_shorts: bool,
|
||||
/// Does the channel have a *Live* tab?
|
||||
|
@ -829,7 +823,7 @@ pub enum YouTubeItem {
|
|||
Channel(ChannelItem),
|
||||
}
|
||||
|
||||
/// YouTube video list item
|
||||
/// YouTube video list item (from search results, recommendations, playlists)
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[non_exhaustive]
|
||||
pub struct VideoItem {
|
||||
|
@ -868,7 +862,7 @@ pub struct VideoItem {
|
|||
pub short_description: Option<String>,
|
||||
}
|
||||
|
||||
/// YouTube channel list item
|
||||
/// YouTube channel list item (from search results)
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[non_exhaustive]
|
||||
pub struct ChannelItem {
|
||||
|
@ -876,6 +870,8 @@ pub struct ChannelItem {
|
|||
pub id: String,
|
||||
/// Channel name
|
||||
pub name: String,
|
||||
/// YouTube channel handle (e.g. `@EEVblog`)
|
||||
pub handle: Option<String>,
|
||||
/// Channel avatar/profile picture
|
||||
pub avatar: Vec<Thumbnail>,
|
||||
/// Channel verification mark
|
||||
|
@ -884,13 +880,11 @@ pub struct ChannelItem {
|
|||
///
|
||||
/// [`None`] if hidden by the owner or not present.
|
||||
pub subscriber_count: Option<u64>,
|
||||
/// Number of videos from the channel
|
||||
pub video_count: Option<u64>,
|
||||
/// Abbreviated channel description
|
||||
pub short_description: String,
|
||||
}
|
||||
|
||||
/// YouTube playlist list item
|
||||
/// YouTube playlist list item (from search results)
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[non_exhaustive]
|
||||
pub struct PlaylistItem {
|
||||
|
|
|
@ -135,6 +135,14 @@ pub trait YtEntity {
|
|||
fn id(&self) -> &str;
|
||||
/// Name
|
||||
fn name(&self) -> &str;
|
||||
/// Channel id
|
||||
///
|
||||
/// `None` if the entity does not belong to a channel
|
||||
fn channel_id(&self) -> Option<&str>;
|
||||
/// Channel name
|
||||
///
|
||||
/// `None` if the entity does not belong to a channel
|
||||
fn channel_name(&self) -> Option<&str>;
|
||||
}
|
||||
|
||||
macro_rules! yt_entity {
|
||||
|
@ -147,18 +155,86 @@ macro_rules! yt_entity {
|
|||
fn name(&self) -> &str {
|
||||
&self.name
|
||||
}
|
||||
|
||||
fn channel_id(&self) -> Option<&str> {
|
||||
None
|
||||
}
|
||||
|
||||
fn channel_name(&self) -> Option<&str> {
|
||||
None
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
impl YtEntity for VideoPlayer {
|
||||
macro_rules! yt_entity_owner {
|
||||
($entity_type:ty) => {
|
||||
impl YtEntity for $entity_type {
|
||||
fn id(&self) -> &str {
|
||||
&self.details.id
|
||||
&self.id
|
||||
}
|
||||
|
||||
fn name(&self) -> &str {
|
||||
&self.details.name
|
||||
&self.name
|
||||
}
|
||||
|
||||
fn channel_id(&self) -> Option<&str> {
|
||||
Some(&self.channel.id)
|
||||
}
|
||||
|
||||
fn channel_name(&self) -> Option<&str> {
|
||||
Some(&self.channel.name)
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
macro_rules! yt_entity_owner_opt {
|
||||
($entity_type:ty) => {
|
||||
impl YtEntity for $entity_type {
|
||||
fn id(&self) -> &str {
|
||||
&self.id
|
||||
}
|
||||
|
||||
fn name(&self) -> &str {
|
||||
&self.name
|
||||
}
|
||||
|
||||
fn channel_id(&self) -> Option<&str> {
|
||||
self.channel.as_ref().map(|c| c.id.as_str())
|
||||
}
|
||||
|
||||
fn channel_name(&self) -> Option<&str> {
|
||||
self.channel.as_ref().map(|c| c.name.as_str())
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
macro_rules! yt_entity_owner_music {
|
||||
($entity_type:ty) => {
|
||||
impl YtEntity for $entity_type {
|
||||
fn id(&self) -> &str {
|
||||
&self.id
|
||||
}
|
||||
|
||||
fn name(&self) -> &str {
|
||||
&self.name
|
||||
}
|
||||
|
||||
fn channel_id(&self) -> Option<&str> {
|
||||
self.artists.first().and_then(|a| a.id.as_deref())
|
||||
}
|
||||
|
||||
fn channel_name(&self) -> Option<&str> {
|
||||
if self.by_va {
|
||||
Some(crate::util::VARIOUS_ARTISTS)
|
||||
} else {
|
||||
self.artists.first().map(|a| a.name.as_str())
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
impl<T> YtEntity for Channel<T> {
|
||||
|
@ -169,26 +245,105 @@ impl<T> YtEntity for Channel<T> {
|
|||
fn name(&self) -> &str {
|
||||
&self.name
|
||||
}
|
||||
|
||||
fn channel_id(&self) -> Option<&str> {
|
||||
None
|
||||
}
|
||||
|
||||
yt_entity! {VideoPlayerDetails}
|
||||
yt_entity! {Playlist}
|
||||
fn channel_name(&self) -> Option<&str> {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
impl YtEntity for YouTubeItem {
|
||||
fn id(&self) -> &str {
|
||||
match self {
|
||||
YouTubeItem::Video(v) => &v.id,
|
||||
YouTubeItem::Playlist(p) => &p.id,
|
||||
YouTubeItem::Channel(c) => &c.id,
|
||||
}
|
||||
}
|
||||
|
||||
fn name(&self) -> &str {
|
||||
match self {
|
||||
YouTubeItem::Video(v) => &v.name,
|
||||
YouTubeItem::Playlist(p) => &p.name,
|
||||
YouTubeItem::Channel(c) => &c.name,
|
||||
}
|
||||
}
|
||||
|
||||
fn channel_id(&self) -> Option<&str> {
|
||||
match self {
|
||||
YouTubeItem::Video(v) => v.channel_id(),
|
||||
YouTubeItem::Playlist(p) => p.channel_id(),
|
||||
YouTubeItem::Channel(_) => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn channel_name(&self) -> Option<&str> {
|
||||
match self {
|
||||
YouTubeItem::Video(v) => v.channel_name(),
|
||||
YouTubeItem::Playlist(p) => p.channel_name(),
|
||||
YouTubeItem::Channel(_) => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl YtEntity for MusicItem {
|
||||
fn id(&self) -> &str {
|
||||
match self {
|
||||
MusicItem::Track(t) => &t.id,
|
||||
MusicItem::Album(b) => &b.id,
|
||||
MusicItem::Artist(a) => &a.id,
|
||||
MusicItem::Playlist(p) => &p.id,
|
||||
}
|
||||
}
|
||||
|
||||
fn name(&self) -> &str {
|
||||
match self {
|
||||
MusicItem::Track(t) => &t.name,
|
||||
MusicItem::Album(b) => &b.name,
|
||||
MusicItem::Artist(a) => &a.name,
|
||||
MusicItem::Playlist(p) => &p.name,
|
||||
}
|
||||
}
|
||||
|
||||
fn channel_id(&self) -> Option<&str> {
|
||||
match self {
|
||||
MusicItem::Track(t) => t.channel_id(),
|
||||
MusicItem::Album(b) => b.channel_id(),
|
||||
MusicItem::Artist(_) => None,
|
||||
MusicItem::Playlist(p) => p.channel_id(),
|
||||
}
|
||||
}
|
||||
|
||||
fn channel_name(&self) -> Option<&str> {
|
||||
match self {
|
||||
MusicItem::Track(t) => t.channel_name(),
|
||||
MusicItem::Album(b) => b.channel_name(),
|
||||
MusicItem::Artist(_) => None,
|
||||
MusicItem::Playlist(p) => p.channel_id(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
yt_entity_owner_opt! {Playlist}
|
||||
yt_entity! {ChannelId}
|
||||
yt_entity! {VideoDetails}
|
||||
yt_entity_owner! {VideoDetails}
|
||||
yt_entity! {ChannelTag}
|
||||
yt_entity! {ChannelRss}
|
||||
yt_entity! {ChannelRssVideo}
|
||||
yt_entity! {VideoItem}
|
||||
yt_entity_owner_opt! {VideoItem}
|
||||
yt_entity! {ChannelItem}
|
||||
yt_entity! {PlaylistItem}
|
||||
yt_entity_owner_opt! {PlaylistItem}
|
||||
yt_entity! {VideoId}
|
||||
yt_entity! {TrackItem}
|
||||
yt_entity_owner_music! {TrackItem}
|
||||
yt_entity! {ArtistItem}
|
||||
yt_entity! {AlbumItem}
|
||||
yt_entity! {MusicPlaylistItem}
|
||||
yt_entity_owner_music! {AlbumItem}
|
||||
yt_entity_owner_opt! {MusicPlaylistItem}
|
||||
yt_entity! {AlbumId}
|
||||
yt_entity! {MusicPlaylist}
|
||||
yt_entity! {MusicAlbum}
|
||||
yt_entity_owner_opt! {MusicPlaylist}
|
||||
yt_entity_owner_music! {MusicAlbum}
|
||||
yt_entity! {MusicArtist}
|
||||
yt_entity! {MusicGenreItem}
|
||||
yt_entity! {MusicGenre}
|
||||
|
|
|
@ -9,15 +9,15 @@ use crate::model::{
|
|||
|
||||
/// The StreamFilter is used for selecting audio/video streams from an extracted video
|
||||
#[derive(Debug, Default, Clone)]
|
||||
pub struct StreamFilter<'a> {
|
||||
pub struct StreamFilter {
|
||||
audio_max_bitrate: Option<u32>,
|
||||
audio_formats: Option<&'a [AudioFormat]>,
|
||||
audio_codecs: Option<&'a [AudioCodec]>,
|
||||
audio_language: Option<&'a str>,
|
||||
audio_formats: Option<Vec<AudioFormat>>,
|
||||
audio_codecs: Option<Vec<AudioCodec>>,
|
||||
audio_language: Option<String>,
|
||||
video_max_res: Option<u32>,
|
||||
video_max_fps: Option<u8>,
|
||||
video_formats: Option<&'a [VideoFormat]>,
|
||||
video_codecs: Option<&'a [VideoCodec]>,
|
||||
video_formats: Option<Vec<VideoFormat>>,
|
||||
video_codecs: Option<Vec<VideoCodec>>,
|
||||
video_hdr: bool,
|
||||
video_none: bool,
|
||||
}
|
||||
|
@ -64,7 +64,7 @@ impl FilterResult {
|
|||
}
|
||||
}
|
||||
|
||||
impl<'a> StreamFilter<'a> {
|
||||
impl StreamFilter {
|
||||
/// Create a new [`StreamFilter`]
|
||||
#[must_use]
|
||||
pub fn new() -> Self {
|
||||
|
@ -90,8 +90,8 @@ impl<'a> StreamFilter<'a> {
|
|||
|
||||
/// Set the supported audio container formats
|
||||
#[must_use]
|
||||
pub fn audio_formats(mut self, formats: &'a [AudioFormat]) -> Self {
|
||||
self.audio_formats = Some(formats);
|
||||
pub fn audio_formats<F: Into<Vec<AudioFormat>>>(mut self, formats: F) -> Self {
|
||||
self.audio_formats = Some(formats.into());
|
||||
self
|
||||
}
|
||||
|
||||
|
@ -104,8 +104,8 @@ impl<'a> StreamFilter<'a> {
|
|||
|
||||
/// Set the supported audio codecs
|
||||
#[must_use]
|
||||
pub fn audio_codecs(mut self, codecs: &'a [AudioCodec]) -> Self {
|
||||
self.audio_codecs = Some(codecs);
|
||||
pub fn audio_codecs<C: Into<Vec<AudioCodec>>>(mut self, codecs: C) -> Self {
|
||||
self.audio_codecs = Some(codecs.into());
|
||||
self
|
||||
}
|
||||
|
||||
|
@ -123,8 +123,8 @@ impl<'a> StreamFilter<'a> {
|
|||
/// If this filter is unset or no stream matches,
|
||||
/// the filter returns the default audio stream.
|
||||
#[must_use]
|
||||
pub fn audio_language(mut self, language: &'a str) -> Self {
|
||||
self.audio_language = Some(language);
|
||||
pub fn audio_language<S: Into<String>>(mut self, language: S) -> Self {
|
||||
self.audio_language = Some(language.into());
|
||||
self
|
||||
}
|
||||
|
||||
|
@ -184,8 +184,8 @@ impl<'a> StreamFilter<'a> {
|
|||
|
||||
/// Set the supported video container formats
|
||||
#[must_use]
|
||||
pub fn video_formats(mut self, formats: &'a [VideoFormat]) -> Self {
|
||||
self.video_formats = Some(formats);
|
||||
pub fn video_formats<F: Into<Vec<VideoFormat>>>(mut self, formats: F) -> Self {
|
||||
self.video_formats = Some(formats.into());
|
||||
self
|
||||
}
|
||||
|
||||
|
@ -198,8 +198,8 @@ impl<'a> StreamFilter<'a> {
|
|||
|
||||
/// Set the supported video codecs
|
||||
#[must_use]
|
||||
pub fn video_codecs(mut self, codecs: &'a [VideoCodec]) -> Self {
|
||||
self.video_codecs = Some(codecs);
|
||||
pub fn video_codecs<C: Into<Vec<VideoCodec>>>(mut self, codecs: C) -> Self {
|
||||
self.video_codecs = Some(codecs.into());
|
||||
self
|
||||
}
|
||||
|
||||
|
@ -250,6 +250,11 @@ impl<'a> StreamFilter<'a> {
|
|||
),
|
||||
)
|
||||
}
|
||||
|
||||
/// Return true if no video stream should be selected
|
||||
pub fn is_video_none(&self) -> bool {
|
||||
self.video_none
|
||||
}
|
||||
}
|
||||
|
||||
impl VideoPlayer {
|
||||
|
@ -373,13 +378,13 @@ mod tests {
|
|||
#[rstest]
|
||||
#[case::default(StreamFilter::default(), Some("https://rr4---sn-h0jeener.googlevideo.com/videoplayback?c=WEB&clen=16104136&dur=1012.661&ei=6OtcZNqtBdOi7gP1upHYCQ&expire=1683832904&fexp=24007246&fvip=2&gir=yes&id=o-ABVtPh3j24hkJeXp8igjvreyODn-oV0CacOqb7pDjJoG&initcwndbps=1720000&ip=2003%3Ade%3Aaf31%3A5200%3A791a%3A897%3Ac15c%3Aae59&itag=251&keepalive=yes&lmt=1683782301237288&lsig=AG3C_xAwRQIgC7HZtYuc6dI92m6wCcoXYpdzSpVtPTIbO7jBKGpUrYMCIQCc0WNtFvN8Awqx9uuRVp5SUSe3rOt2D7M-rCKpgVv_0A%3D%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&mh=wB&mime=audio%2Fwebm&mm=31%2C29&mn=sn-h0jeener%2Csn-h0jeln7l&ms=au%2Crdu&mt=1683811031&mv=m&mvi=4&n=U8mCOo4eYD4n0A&ns=LToEdXWVFHcH53e3aTe1N7kN&pl=37&requiressl=yes&sig=AOq0QJ8wRQIhAPcUhhfkNVA_JcdU6KLTOFjRCnNl6n8gamJA-Q0PgCpIAiBTMV2k2JfHzbHBtsHxuNW7zHvSaYaUbz-dEIQC45o1eA%3D%3D&source=youtube&sparams=expire%2Cei%2Cip%2Cid%2Citag%2Csource%2Crequiressl%2Cspc%2Cvprv%2Csvpuc%2Cxtags%2Cmime%2Cns%2Cgir%2Cclen%2Cdur%2Clmt&spc=qEK7B81AP536F3aOi5JzMyLCUDiktWigtEpf9nI2xg&svpuc=1&txp=4532434&vprv=1&xtags=acont%3Doriginal%3Alang%3Den-US"))]
|
||||
#[case::bitrate(StreamFilter::default().audio_max_bitrate(100_000).clone(), Some("https://rr4---sn-h0jeener.googlevideo.com/videoplayback?c=WEB&clen=8217508&dur=1012.661&ei=6OtcZNqtBdOi7gP1upHYCQ&expire=1683832904&fexp=24007246&fvip=2&gir=yes&id=o-ABVtPh3j24hkJeXp8igjvreyODn-oV0CacOqb7pDjJoG&initcwndbps=1720000&ip=2003%3Ade%3Aaf31%3A5200%3A791a%3A897%3Ac15c%3Aae59&itag=250&keepalive=yes&lmt=1683782195315620&lsig=AG3C_xAwRQIgC7HZtYuc6dI92m6wCcoXYpdzSpVtPTIbO7jBKGpUrYMCIQCc0WNtFvN8Awqx9uuRVp5SUSe3rOt2D7M-rCKpgVv_0A%3D%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&mh=wB&mime=audio%2Fwebm&mm=31%2C29&mn=sn-h0jeener%2Csn-h0jeln7l&ms=au%2Crdu&mt=1683811031&mv=m&mvi=4&n=U8mCOo4eYD4n0A&ns=LToEdXWVFHcH53e3aTe1N7kN&pl=37&requiressl=yes&sig=AOq0QJ8wRQIga2iMQsToMxO7hTOx0gNAzhYoV1lL5PpE9lkAuBXt1nkCIQCuFuQXWNixIquEugtkT1C9khuKRP_C-wzSOiUmRp1DRg%3D%3D&source=youtube&sparams=expire%2Cei%2Cip%2Cid%2Citag%2Csource%2Crequiressl%2Cspc%2Cvprv%2Csvpuc%2Cxtags%2Cmime%2Cns%2Cgir%2Cclen%2Cdur%2Clmt&spc=qEK7B81AP536F3aOi5JzMyLCUDiktWigtEpf9nI2xg&svpuc=1&txp=4532434&vprv=1&xtags=acont%3Doriginal%3Alang%3Den-US"))]
|
||||
#[case::m4a_format(StreamFilter::default().audio_formats(&[AudioFormat::M4a]).clone(), Some("https://rr4---sn-h0jeener.googlevideo.com/videoplayback?c=WEB&clen=16390508&dur=1012.691&ei=6OtcZNqtBdOi7gP1upHYCQ&expire=1683832904&fexp=24007246&fvip=2&gir=yes&id=o-ABVtPh3j24hkJeXp8igjvreyODn-oV0CacOqb7pDjJoG&initcwndbps=1720000&ip=2003%3Ade%3Aaf31%3A5200%3A791a%3A897%3Ac15c%3Aae59&itag=140&keepalive=yes&lmt=1683782363698612&lsig=AG3C_xAwRQIgC7HZtYuc6dI92m6wCcoXYpdzSpVtPTIbO7jBKGpUrYMCIQCc0WNtFvN8Awqx9uuRVp5SUSe3rOt2D7M-rCKpgVv_0A%3D%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&mh=wB&mime=audio%2Fmp4&mm=31%2C29&mn=sn-h0jeener%2Csn-h0jeln7l&ms=au%2Crdu&mt=1683811031&mv=m&mvi=4&n=U8mCOo4eYD4n0A&ns=LToEdXWVFHcH53e3aTe1N7kN&pl=37&requiressl=yes&sig=AOq0QJ8wRgIhAMgM470I-QXq4lTRuPtXf5UInHB_tG0tTGXRhVZ6nwImAiEAn0JYRknq5dtTwcmzZheekxVOZKhZ2Rpxc_UyvX2CMRY%3D&source=youtube&sparams=expire%2Cei%2Cip%2Cid%2Citag%2Csource%2Crequiressl%2Cspc%2Cvprv%2Csvpuc%2Cxtags%2Cmime%2Cns%2Cgir%2Cclen%2Cdur%2Clmt&spc=qEK7B81AP536F3aOi5JzMyLCUDiktWigtEpf9nI2xg&svpuc=1&txp=4532434&vprv=1&xtags=acont%3Doriginal%3Alang%3Den-US"))]
|
||||
#[case::m4a_codec(StreamFilter::default().audio_codecs(&[AudioCodec::Mp4a]).clone(), Some("https://rr4---sn-h0jeener.googlevideo.com/videoplayback?c=WEB&clen=16390508&dur=1012.691&ei=6OtcZNqtBdOi7gP1upHYCQ&expire=1683832904&fexp=24007246&fvip=2&gir=yes&id=o-ABVtPh3j24hkJeXp8igjvreyODn-oV0CacOqb7pDjJoG&initcwndbps=1720000&ip=2003%3Ade%3Aaf31%3A5200%3A791a%3A897%3Ac15c%3Aae59&itag=140&keepalive=yes&lmt=1683782363698612&lsig=AG3C_xAwRQIgC7HZtYuc6dI92m6wCcoXYpdzSpVtPTIbO7jBKGpUrYMCIQCc0WNtFvN8Awqx9uuRVp5SUSe3rOt2D7M-rCKpgVv_0A%3D%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&mh=wB&mime=audio%2Fmp4&mm=31%2C29&mn=sn-h0jeener%2Csn-h0jeln7l&ms=au%2Crdu&mt=1683811031&mv=m&mvi=4&n=U8mCOo4eYD4n0A&ns=LToEdXWVFHcH53e3aTe1N7kN&pl=37&requiressl=yes&sig=AOq0QJ8wRgIhAMgM470I-QXq4lTRuPtXf5UInHB_tG0tTGXRhVZ6nwImAiEAn0JYRknq5dtTwcmzZheekxVOZKhZ2Rpxc_UyvX2CMRY%3D&source=youtube&sparams=expire%2Cei%2Cip%2Cid%2Citag%2Csource%2Crequiressl%2Cspc%2Cvprv%2Csvpuc%2Cxtags%2Cmime%2Cns%2Cgir%2Cclen%2Cdur%2Clmt&spc=qEK7B81AP536F3aOi5JzMyLCUDiktWigtEpf9nI2xg&svpuc=1&txp=4532434&vprv=1&xtags=acont%3Doriginal%3Alang%3Den-US"))]
|
||||
#[case::m4a_format(StreamFilter::default().audio_formats([AudioFormat::M4a]).clone(), Some("https://rr4---sn-h0jeener.googlevideo.com/videoplayback?c=WEB&clen=16390508&dur=1012.691&ei=6OtcZNqtBdOi7gP1upHYCQ&expire=1683832904&fexp=24007246&fvip=2&gir=yes&id=o-ABVtPh3j24hkJeXp8igjvreyODn-oV0CacOqb7pDjJoG&initcwndbps=1720000&ip=2003%3Ade%3Aaf31%3A5200%3A791a%3A897%3Ac15c%3Aae59&itag=140&keepalive=yes&lmt=1683782363698612&lsig=AG3C_xAwRQIgC7HZtYuc6dI92m6wCcoXYpdzSpVtPTIbO7jBKGpUrYMCIQCc0WNtFvN8Awqx9uuRVp5SUSe3rOt2D7M-rCKpgVv_0A%3D%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&mh=wB&mime=audio%2Fmp4&mm=31%2C29&mn=sn-h0jeener%2Csn-h0jeln7l&ms=au%2Crdu&mt=1683811031&mv=m&mvi=4&n=U8mCOo4eYD4n0A&ns=LToEdXWVFHcH53e3aTe1N7kN&pl=37&requiressl=yes&sig=AOq0QJ8wRgIhAMgM470I-QXq4lTRuPtXf5UInHB_tG0tTGXRhVZ6nwImAiEAn0JYRknq5dtTwcmzZheekxVOZKhZ2Rpxc_UyvX2CMRY%3D&source=youtube&sparams=expire%2Cei%2Cip%2Cid%2Citag%2Csource%2Crequiressl%2Cspc%2Cvprv%2Csvpuc%2Cxtags%2Cmime%2Cns%2Cgir%2Cclen%2Cdur%2Clmt&spc=qEK7B81AP536F3aOi5JzMyLCUDiktWigtEpf9nI2xg&svpuc=1&txp=4532434&vprv=1&xtags=acont%3Doriginal%3Alang%3Den-US"))]
|
||||
#[case::m4a_codec(StreamFilter::default().audio_codecs([AudioCodec::Mp4a]).clone(), Some("https://rr4---sn-h0jeener.googlevideo.com/videoplayback?c=WEB&clen=16390508&dur=1012.691&ei=6OtcZNqtBdOi7gP1upHYCQ&expire=1683832904&fexp=24007246&fvip=2&gir=yes&id=o-ABVtPh3j24hkJeXp8igjvreyODn-oV0CacOqb7pDjJoG&initcwndbps=1720000&ip=2003%3Ade%3Aaf31%3A5200%3A791a%3A897%3Ac15c%3Aae59&itag=140&keepalive=yes&lmt=1683782363698612&lsig=AG3C_xAwRQIgC7HZtYuc6dI92m6wCcoXYpdzSpVtPTIbO7jBKGpUrYMCIQCc0WNtFvN8Awqx9uuRVp5SUSe3rOt2D7M-rCKpgVv_0A%3D%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&mh=wB&mime=audio%2Fmp4&mm=31%2C29&mn=sn-h0jeener%2Csn-h0jeln7l&ms=au%2Crdu&mt=1683811031&mv=m&mvi=4&n=U8mCOo4eYD4n0A&ns=LToEdXWVFHcH53e3aTe1N7kN&pl=37&requiressl=yes&sig=AOq0QJ8wRgIhAMgM470I-QXq4lTRuPtXf5UInHB_tG0tTGXRhVZ6nwImAiEAn0JYRknq5dtTwcmzZheekxVOZKhZ2Rpxc_UyvX2CMRY%3D&source=youtube&sparams=expire%2Cei%2Cip%2Cid%2Citag%2Csource%2Crequiressl%2Cspc%2Cvprv%2Csvpuc%2Cxtags%2Cmime%2Cns%2Cgir%2Cclen%2Cdur%2Clmt&spc=qEK7B81AP536F3aOi5JzMyLCUDiktWigtEpf9nI2xg&svpuc=1&txp=4532434&vprv=1&xtags=acont%3Doriginal%3Alang%3Den-US"))]
|
||||
#[case::french(StreamFilter::default().audio_language("fr").clone(), Some("https://rr4---sn-h0jeener.googlevideo.com/videoplayback?c=WEB&clen=940286&dur=60.101&ei=6OtcZNqtBdOi7gP1upHYCQ&expire=1683832904&fexp=24007246&fvip=2&gir=yes&id=o-ABVtPh3j24hkJeXp8igjvreyODn-oV0CacOqb7pDjJoG&initcwndbps=1720000&ip=2003%3Ade%3Aaf31%3A5200%3A791a%3A897%3Ac15c%3Aae59&itag=251&keepalive=yes&lmt=1683774002236584&lsig=AG3C_xAwRQIgC7HZtYuc6dI92m6wCcoXYpdzSpVtPTIbO7jBKGpUrYMCIQCc0WNtFvN8Awqx9uuRVp5SUSe3rOt2D7M-rCKpgVv_0A%3D%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&mh=wB&mime=audio%2Fwebm&mm=31%2C29&mn=sn-h0jeener%2Csn-h0jeln7l&ms=au%2Crdu&mt=1683811031&mv=m&mvi=4&n=U8mCOo4eYD4n0A&ns=LToEdXWVFHcH53e3aTe1N7kN&pl=37&requiressl=yes&sig=AOq0QJ8wRQIhAIUUin7WZBnoVDb2p0wuTPc7HZwbF8I5sxzLrVN9WeBwAiBQTZwhxCQ1IdrUkkD1-cSGYBtMF1aKkjPZ-LWeie0aZA%3D%3D&source=youtube&sparams=expire%2Cei%2Cip%2Cid%2Citag%2Csource%2Crequiressl%2Cspc%2Cvprv%2Csvpuc%2Cxtags%2Cmime%2Cns%2Cgir%2Cclen%2Cdur%2Clmt&spc=qEK7B81AP536F3aOi5JzMyLCUDiktWigtEpf9nI2xg&svpuc=1&txp=4532434&vprv=1&xtags=acont%3Ddubbed%3Alang%3Dfr"))]
|
||||
#[case::br_fallback(StreamFilter::default().audio_max_bitrate(0).clone(), Some("https://rr4---sn-h0jeener.googlevideo.com/videoplayback?c=WEB&clen=6306327&dur=1012.661&ei=6OtcZNqtBdOi7gP1upHYCQ&expire=1683832904&fexp=24007246&fvip=2&gir=yes&id=o-ABVtPh3j24hkJeXp8igjvreyODn-oV0CacOqb7pDjJoG&initcwndbps=1720000&ip=2003%3Ade%3Aaf31%3A5200%3A791a%3A897%3Ac15c%3Aae59&itag=249&keepalive=yes&lmt=1683782187865292&lsig=AG3C_xAwRQIgC7HZtYuc6dI92m6wCcoXYpdzSpVtPTIbO7jBKGpUrYMCIQCc0WNtFvN8Awqx9uuRVp5SUSe3rOt2D7M-rCKpgVv_0A%3D%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&mh=wB&mime=audio%2Fwebm&mm=31%2C29&mn=sn-h0jeener%2Csn-h0jeln7l&ms=au%2Crdu&mt=1683811031&mv=m&mvi=4&n=U8mCOo4eYD4n0A&ns=LToEdXWVFHcH53e3aTe1N7kN&pl=37&requiressl=yes&sig=AOq0QJ8wRAIgW1DTCrLV_GyEM1rdjScgyceZE1llb73KJMFXmPm5Y04CIAYOLZuuzFX4ba5720kMOcQ1-Ld1DULs85nLxJglitCl&source=youtube&sparams=expire%2Cei%2Cip%2Cid%2Citag%2Csource%2Crequiressl%2Cspc%2Cvprv%2Csvpuc%2Cxtags%2Cmime%2Cns%2Cgir%2Cclen%2Cdur%2Clmt&spc=qEK7B81AP536F3aOi5JzMyLCUDiktWigtEpf9nI2xg&svpuc=1&txp=4532434&vprv=1&xtags=acont%3Doriginal%3Alang%3Den-US"))]
|
||||
#[case::lang_fallback(StreamFilter::default().audio_language("xx").clone(), Some("https://rr4---sn-h0jeener.googlevideo.com/videoplayback?c=WEB&clen=16104136&dur=1012.661&ei=6OtcZNqtBdOi7gP1upHYCQ&expire=1683832904&fexp=24007246&fvip=2&gir=yes&id=o-ABVtPh3j24hkJeXp8igjvreyODn-oV0CacOqb7pDjJoG&initcwndbps=1720000&ip=2003%3Ade%3Aaf31%3A5200%3A791a%3A897%3Ac15c%3Aae59&itag=251&keepalive=yes&lmt=1683782301237288&lsig=AG3C_xAwRQIgC7HZtYuc6dI92m6wCcoXYpdzSpVtPTIbO7jBKGpUrYMCIQCc0WNtFvN8Awqx9uuRVp5SUSe3rOt2D7M-rCKpgVv_0A%3D%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&mh=wB&mime=audio%2Fwebm&mm=31%2C29&mn=sn-h0jeener%2Csn-h0jeln7l&ms=au%2Crdu&mt=1683811031&mv=m&mvi=4&n=U8mCOo4eYD4n0A&ns=LToEdXWVFHcH53e3aTe1N7kN&pl=37&requiressl=yes&sig=AOq0QJ8wRQIhAPcUhhfkNVA_JcdU6KLTOFjRCnNl6n8gamJA-Q0PgCpIAiBTMV2k2JfHzbHBtsHxuNW7zHvSaYaUbz-dEIQC45o1eA%3D%3D&source=youtube&sparams=expire%2Cei%2Cip%2Cid%2Citag%2Csource%2Crequiressl%2Cspc%2Cvprv%2Csvpuc%2Cxtags%2Cmime%2Cns%2Cgir%2Cclen%2Cdur%2Clmt&spc=qEK7B81AP536F3aOi5JzMyLCUDiktWigtEpf9nI2xg&svpuc=1&txp=4532434&vprv=1&xtags=acont%3Doriginal%3Alang%3Den-US"))]
|
||||
#[case::noformat(StreamFilter::default().audio_formats(&[]).clone(), None)]
|
||||
#[case::nocodec(StreamFilter::default().audio_codecs(&[]).clone(), None)]
|
||||
#[case::noformat(StreamFilter::default().audio_formats([]).clone(), None)]
|
||||
#[case::nocodec(StreamFilter::default().audio_codecs([]).clone(), None)]
|
||||
fn t_select_audio_stream(#[case] filter: StreamFilter, #[case] expect_url: Option<&str>) {
|
||||
let selection = PLAYER_ML.select_audio_stream(&filter);
|
||||
|
||||
|
@ -395,10 +400,10 @@ mod tests {
|
|||
#[case::resolution(StreamFilter::default().video_max_res(720).clone(), Some("https://rr5---sn-h0jelne7.googlevideo.com/videoplayback?aitags=133%2C134%2C135%2C136%2C160%2C242%2C243%2C244%2C247%2C278%2C298%2C299%2C302%2C303%2C308%2C315%2C330%2C331%2C332%2C333%2C334%2C335%2C336%2C337%2C394%2C395%2C396%2C397%2C398%2C399%2C400%2C401%2C694%2C695%2C696%2C697%2C698%2C699%2C700%2C701&c=WEB&clen=76313586&dur=313.780&ei=eckIY72IKcGZ8gOMt6CwDg&expire=1661541849&fexp=24001373%2C24007246&fvip=2&gir=yes&id=o-AOqXE9lVS424yszv6LN5V_gaevdHxenJl-tYNy3Drs6g&initcwndbps=1428750&ip=2003%3Ade%3Aaf05%3A2500%3A5dad%3A319b%3Aca30%3Ae212&itag=302&keepalive=yes&lmt=1647455155369524&lsig=AG3C_xAwRQIhAMioKyc-dqs-6uvAwLViCcCTXKHn9sIbo0cbSSBXGG4kAiBQNsRBAvQrbWdOjZIsQXYrfPEb1KDpE_AlSEGQZXB9uA%3D%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&mh=NH&mime=video%2Fwebm&mm=31%2C29&mn=sn-h0jelne7%2Csn-h0jeenl6&ms=au%2Crdu&mt=1661519833&mv=m&mvi=5&n=Zd7nrOM1B2C6PA&ns=426LxLap5MonJD_YWdS4lSYH&pl=37&rbqsm=fr&requiressl=yes&sig=AOq0QJ8wRAIgW0H1434eh9Axw6zw95qezJB0D2aVd2bxEIs4T5bcfFACIDOjha9WLycp0L188FZyFGa1RBkLPoGrrJOppsaXqwDR&source=youtube&sparams=expire%2Cei%2Cip%2Cid%2Caitags%2Csource%2Crequiressl%2Cspc%2Cvprv%2Cmime%2Cns%2Cgir%2Cclen%2Cdur%2Clmt&spc=lT-KhuPtxVzL5-QbZ7S9zNeOHsWTdms&txp=4532434&vprv=1"))]
|
||||
#[case::resolution_fps(StreamFilter::default().video_max_res(720).video_max_fps(30).clone(), Some("https://rr5---sn-h0jelne7.googlevideo.com/videoplayback?aitags=133%2C134%2C135%2C136%2C160%2C242%2C243%2C244%2C247%2C278%2C298%2C299%2C302%2C303%2C308%2C315%2C330%2C331%2C332%2C333%2C334%2C335%2C336%2C337%2C394%2C395%2C396%2C397%2C398%2C399%2C400%2C401%2C694%2C695%2C696%2C697%2C698%2C699%2C700%2C701&c=WEB&clen=47531179&dur=313.780&ei=eckIY72IKcGZ8gOMt6CwDg&expire=1661541849&fexp=24001373%2C24007246&fvip=2&gir=yes&id=o-AOqXE9lVS424yszv6LN5V_gaevdHxenJl-tYNy3Drs6g&initcwndbps=1428750&ip=2003%3Ade%3Aaf05%3A2500%3A5dad%3A319b%3Aca30%3Ae212&itag=247&keepalive=yes&lmt=1647458657499381&lsig=AG3C_xAwRQIhAMioKyc-dqs-6uvAwLViCcCTXKHn9sIbo0cbSSBXGG4kAiBQNsRBAvQrbWdOjZIsQXYrfPEb1KDpE_AlSEGQZXB9uA%3D%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&mh=NH&mime=video%2Fwebm&mm=31%2C29&mn=sn-h0jelne7%2Csn-h0jeenl6&ms=au%2Crdu&mt=1661519833&mv=m&mvi=5&n=Zd7nrOM1B2C6PA&ns=426LxLap5MonJD_YWdS4lSYH&pl=37&rbqsm=fr&requiressl=yes&sig=AOq0QJ8wRgIhAMUsmcl1zgbr3YQranPWNV1kcxT5IdEoLL7FTFEDdHHPAiEAhQnrfYMU0A9xZ69MfBujWA4pXtCOQCg2Jn6ve9J_vBQ%3D&source=youtube&sparams=expire%2Cei%2Cip%2Cid%2Caitags%2Csource%2Crequiressl%2Cspc%2Cvprv%2Cmime%2Cns%2Cgir%2Cclen%2Cdur%2Clmt&spc=lT-KhuPtxVzL5-QbZ7S9zNeOHsWTdms&txp=4532434&vprv=1"))]
|
||||
#[case::res_fallback(StreamFilter::default().video_max_res(100).clone(), Some("https://rr5---sn-h0jelne7.googlevideo.com/videoplayback?aitags=133%2C134%2C135%2C136%2C160%2C242%2C243%2C244%2C247%2C278%2C298%2C299%2C302%2C303%2C308%2C315%2C330%2C331%2C332%2C333%2C334%2C335%2C336%2C337%2C394%2C395%2C396%2C397%2C398%2C399%2C400%2C401%2C694%2C695%2C696%2C697%2C698%2C699%2C700%2C701&c=WEB&clen=2763284&dur=313.780&ei=eckIY72IKcGZ8gOMt6CwDg&expire=1661541849&fexp=24001373%2C24007246&fvip=2&gir=yes&id=o-AOqXE9lVS424yszv6LN5V_gaevdHxenJl-tYNy3Drs6g&initcwndbps=1428750&ip=2003%3Ade%3Aaf05%3A2500%3A5dad%3A319b%3Aca30%3Ae212&itag=160&keepalive=yes&lmt=1647456833049253&lsig=AG3C_xAwRQIhAMioKyc-dqs-6uvAwLViCcCTXKHn9sIbo0cbSSBXGG4kAiBQNsRBAvQrbWdOjZIsQXYrfPEb1KDpE_AlSEGQZXB9uA%3D%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&mh=NH&mime=video%2Fmp4&mm=31%2C29&mn=sn-h0jelne7%2Csn-h0jeenl6&ms=au%2Crdu&mt=1661519833&mv=m&mvi=5&n=Zd7nrOM1B2C6PA&ns=426LxLap5MonJD_YWdS4lSYH&pl=37&rbqsm=fr&requiressl=yes&sig=AOq0QJ8wRQIgLPNxzLxppSSpnDEHxVblrQ38890NMbGnLXlmxljprfQCIQDn4Ir_sjYh7S3ms-Rynm-K0nJpHpQGYsz1nv4TiqeELQ%3D%3D&source=youtube&sparams=expire%2Cei%2Cip%2Cid%2Caitags%2Csource%2Crequiressl%2Cspc%2Cvprv%2Cmime%2Cns%2Cgir%2Cclen%2Cdur%2Clmt&spc=lT-KhuPtxVzL5-QbZ7S9zNeOHsWTdms&txp=4532434&vprv=1"))]
|
||||
#[case::webm_format(StreamFilter::default().video_formats(&[VideoFormat::Webm]).clone(), Some("https://rr5---sn-h0jelne7.googlevideo.com/videoplayback?aitags=133%2C134%2C135%2C136%2C160%2C242%2C243%2C244%2C247%2C278%2C298%2C299%2C302%2C303%2C308%2C315%2C330%2C331%2C332%2C333%2C334%2C335%2C336%2C337%2C394%2C395%2C396%2C397%2C398%2C399%2C400%2C401%2C694%2C695%2C696%2C697%2C698%2C699%2C700%2C701&c=WEB&clen=998696577&dur=313.780&ei=eckIY72IKcGZ8gOMt6CwDg&expire=1661541849&fexp=24001373%2C24007246&fvip=2&gir=yes&id=o-AOqXE9lVS424yszv6LN5V_gaevdHxenJl-tYNy3Drs6g&initcwndbps=1428750&ip=2003%3Ade%3Aaf05%3A2500%3A5dad%3A319b%3Aca30%3Ae212&itag=315&keepalive=yes&lmt=1647476955807851&lsig=AG3C_xAwRQIhAMioKyc-dqs-6uvAwLViCcCTXKHn9sIbo0cbSSBXGG4kAiBQNsRBAvQrbWdOjZIsQXYrfPEb1KDpE_AlSEGQZXB9uA%3D%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&mh=NH&mime=video%2Fwebm&mm=31%2C29&mn=sn-h0jelne7%2Csn-h0jeenl6&ms=au%2Crdu&mt=1661519833&mv=m&mvi=5&n=Zd7nrOM1B2C6PA&ns=426LxLap5MonJD_YWdS4lSYH&pl=37&rbqsm=fr&requiressl=yes&sig=AOq0QJ8wRAIfP4IVSo-00_kq_JIkuh032hcLoJzNEhYjvwgLiDpEzQIhALPVrvDBjRwiFddXiAyADmRtYygte4HvlJ3XOrkOf_TR&source=youtube&sparams=expire%2Cei%2Cip%2Cid%2Caitags%2Csource%2Crequiressl%2Cspc%2Cvprv%2Cmime%2Cns%2Cgir%2Cclen%2Cdur%2Clmt&spc=lT-KhuPtxVzL5-QbZ7S9zNeOHsWTdms&txp=4532434&vprv=1"))]
|
||||
#[case::vp9_codec(StreamFilter::default().video_codecs(&[VideoCodec::Vp9]).clone(), Some("https://rr5---sn-h0jelne7.googlevideo.com/videoplayback?aitags=133%2C134%2C135%2C136%2C160%2C242%2C243%2C244%2C247%2C278%2C298%2C299%2C302%2C303%2C308%2C315%2C330%2C331%2C332%2C333%2C334%2C335%2C336%2C337%2C394%2C395%2C396%2C397%2C398%2C399%2C400%2C401%2C694%2C695%2C696%2C697%2C698%2C699%2C700%2C701&c=WEB&clen=998696577&dur=313.780&ei=eckIY72IKcGZ8gOMt6CwDg&expire=1661541849&fexp=24001373%2C24007246&fvip=2&gir=yes&id=o-AOqXE9lVS424yszv6LN5V_gaevdHxenJl-tYNy3Drs6g&initcwndbps=1428750&ip=2003%3Ade%3Aaf05%3A2500%3A5dad%3A319b%3Aca30%3Ae212&itag=315&keepalive=yes&lmt=1647476955807851&lsig=AG3C_xAwRQIhAMioKyc-dqs-6uvAwLViCcCTXKHn9sIbo0cbSSBXGG4kAiBQNsRBAvQrbWdOjZIsQXYrfPEb1KDpE_AlSEGQZXB9uA%3D%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&mh=NH&mime=video%2Fwebm&mm=31%2C29&mn=sn-h0jelne7%2Csn-h0jeenl6&ms=au%2Crdu&mt=1661519833&mv=m&mvi=5&n=Zd7nrOM1B2C6PA&ns=426LxLap5MonJD_YWdS4lSYH&pl=37&rbqsm=fr&requiressl=yes&sig=AOq0QJ8wRAIfP4IVSo-00_kq_JIkuh032hcLoJzNEhYjvwgLiDpEzQIhALPVrvDBjRwiFddXiAyADmRtYygte4HvlJ3XOrkOf_TR&source=youtube&sparams=expire%2Cei%2Cip%2Cid%2Caitags%2Csource%2Crequiressl%2Cspc%2Cvprv%2Cmime%2Cns%2Cgir%2Cclen%2Cdur%2Clmt&spc=lT-KhuPtxVzL5-QbZ7S9zNeOHsWTdms&txp=4532434&vprv=1"))]
|
||||
#[case::noformat(StreamFilter::default().video_formats(&[]).clone(), None)]
|
||||
#[case::nocodec(StreamFilter::default().video_codecs(&[]).clone(), None)]
|
||||
#[case::webm_format(StreamFilter::default().video_formats([VideoFormat::Webm]).clone(), Some("https://rr5---sn-h0jelne7.googlevideo.com/videoplayback?aitags=133%2C134%2C135%2C136%2C160%2C242%2C243%2C244%2C247%2C278%2C298%2C299%2C302%2C303%2C308%2C315%2C330%2C331%2C332%2C333%2C334%2C335%2C336%2C337%2C394%2C395%2C396%2C397%2C398%2C399%2C400%2C401%2C694%2C695%2C696%2C697%2C698%2C699%2C700%2C701&c=WEB&clen=998696577&dur=313.780&ei=eckIY72IKcGZ8gOMt6CwDg&expire=1661541849&fexp=24001373%2C24007246&fvip=2&gir=yes&id=o-AOqXE9lVS424yszv6LN5V_gaevdHxenJl-tYNy3Drs6g&initcwndbps=1428750&ip=2003%3Ade%3Aaf05%3A2500%3A5dad%3A319b%3Aca30%3Ae212&itag=315&keepalive=yes&lmt=1647476955807851&lsig=AG3C_xAwRQIhAMioKyc-dqs-6uvAwLViCcCTXKHn9sIbo0cbSSBXGG4kAiBQNsRBAvQrbWdOjZIsQXYrfPEb1KDpE_AlSEGQZXB9uA%3D%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&mh=NH&mime=video%2Fwebm&mm=31%2C29&mn=sn-h0jelne7%2Csn-h0jeenl6&ms=au%2Crdu&mt=1661519833&mv=m&mvi=5&n=Zd7nrOM1B2C6PA&ns=426LxLap5MonJD_YWdS4lSYH&pl=37&rbqsm=fr&requiressl=yes&sig=AOq0QJ8wRAIfP4IVSo-00_kq_JIkuh032hcLoJzNEhYjvwgLiDpEzQIhALPVrvDBjRwiFddXiAyADmRtYygte4HvlJ3XOrkOf_TR&source=youtube&sparams=expire%2Cei%2Cip%2Cid%2Caitags%2Csource%2Crequiressl%2Cspc%2Cvprv%2Cmime%2Cns%2Cgir%2Cclen%2Cdur%2Clmt&spc=lT-KhuPtxVzL5-QbZ7S9zNeOHsWTdms&txp=4532434&vprv=1"))]
|
||||
#[case::vp9_codec(StreamFilter::default().video_codecs([VideoCodec::Vp9]).clone(), Some("https://rr5---sn-h0jelne7.googlevideo.com/videoplayback?aitags=133%2C134%2C135%2C136%2C160%2C242%2C243%2C244%2C247%2C278%2C298%2C299%2C302%2C303%2C308%2C315%2C330%2C331%2C332%2C333%2C334%2C335%2C336%2C337%2C394%2C395%2C396%2C397%2C398%2C399%2C400%2C401%2C694%2C695%2C696%2C697%2C698%2C699%2C700%2C701&c=WEB&clen=998696577&dur=313.780&ei=eckIY72IKcGZ8gOMt6CwDg&expire=1661541849&fexp=24001373%2C24007246&fvip=2&gir=yes&id=o-AOqXE9lVS424yszv6LN5V_gaevdHxenJl-tYNy3Drs6g&initcwndbps=1428750&ip=2003%3Ade%3Aaf05%3A2500%3A5dad%3A319b%3Aca30%3Ae212&itag=315&keepalive=yes&lmt=1647476955807851&lsig=AG3C_xAwRQIhAMioKyc-dqs-6uvAwLViCcCTXKHn9sIbo0cbSSBXGG4kAiBQNsRBAvQrbWdOjZIsQXYrfPEb1KDpE_AlSEGQZXB9uA%3D%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&mh=NH&mime=video%2Fwebm&mm=31%2C29&mn=sn-h0jelne7%2Csn-h0jeenl6&ms=au%2Crdu&mt=1661519833&mv=m&mvi=5&n=Zd7nrOM1B2C6PA&ns=426LxLap5MonJD_YWdS4lSYH&pl=37&rbqsm=fr&requiressl=yes&sig=AOq0QJ8wRAIfP4IVSo-00_kq_JIkuh032hcLoJzNEhYjvwgLiDpEzQIhALPVrvDBjRwiFddXiAyADmRtYygte4HvlJ3XOrkOf_TR&source=youtube&sparams=expire%2Cei%2Cip%2Cid%2Caitags%2Csource%2Crequiressl%2Cspc%2Cvprv%2Cmime%2Cns%2Cgir%2Cclen%2Cdur%2Clmt&spc=lT-KhuPtxVzL5-QbZ7S9zNeOHsWTdms&txp=4532434&vprv=1"))]
|
||||
#[case::noformat(StreamFilter::default().video_formats([]).clone(), None)]
|
||||
#[case::nocodec(StreamFilter::default().video_codecs([]).clone(), None)]
|
||||
fn t_select_video_only_stream(#[case] filter: StreamFilter, #[case] expect_url: Option<&str>) {
|
||||
let selection = PLAYER_HDR.select_video_only_stream(&filter);
|
||||
|
||||
|
@ -415,12 +420,12 @@ mod tests {
|
|||
Some("https://rr5---sn-h0jelne7.googlevideo.com/videoplayback?c=WEB&clen=5199784&dur=313.801&ei=eckIY72IKcGZ8gOMt6CwDg&expire=1661541849&fexp=24001373%2C24007246&fvip=2&gir=yes&id=o-AOqXE9lVS424yszv6LN5V_gaevdHxenJl-tYNy3Drs6g&initcwndbps=1428750&ip=2003%3Ade%3Aaf05%3A2500%3A5dad%3A319b%3Aca30%3Ae212&itag=251&keepalive=yes&lmt=1647453650291076&lsig=AG3C_xAwRQIhAMioKyc-dqs-6uvAwLViCcCTXKHn9sIbo0cbSSBXGG4kAiBQNsRBAvQrbWdOjZIsQXYrfPEb1KDpE_AlSEGQZXB9uA%3D%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&mh=NH&mime=audio%2Fwebm&mm=31%2C29&mn=sn-h0jelne7%2Csn-h0jeenl6&ms=au%2Crdu&mt=1661519833&mv=m&mvi=5&n=Zd7nrOM1B2C6PA&ns=426LxLap5MonJD_YWdS4lSYH&pl=37&rbqsm=fr&requiressl=yes&sig=AOq0QJ8wRQIhALtI3j8ZChpNb0LcyDZ3yosbWnSpqaO0-jKAe_UM_RQyAiAMwrpdeNbJEnQn3q1eveaAcRcNIwy5iJ4fIjeBW_MUfg%3D%3D&source=youtube&sparams=expire%2Cei%2Cip%2Cid%2Citag%2Csource%2Crequiressl%2Cspc%2Cvprv%2Cmime%2Cns%2Cgir%2Cclen%2Cdur%2Clmt&spc=lT-KhuPtxVzL5-QbZ7S9zNeOHsWTdms&txp=4532434&vprv=1")
|
||||
)]
|
||||
#[case::webm(
|
||||
StreamFilter::default().video_formats(&[VideoFormat::Webm]).clone(),
|
||||
StreamFilter::default().video_formats([VideoFormat::Webm]).clone(),
|
||||
Some("https://rr5---sn-h0jelne7.googlevideo.com/videoplayback?aitags=133%2C134%2C135%2C136%2C160%2C242%2C243%2C244%2C247%2C278%2C298%2C299%2C302%2C303%2C308%2C315%2C330%2C331%2C332%2C333%2C334%2C335%2C336%2C337%2C394%2C395%2C396%2C397%2C398%2C399%2C400%2C401%2C694%2C695%2C696%2C697%2C698%2C699%2C700%2C701&c=WEB&clen=998696577&dur=313.780&ei=eckIY72IKcGZ8gOMt6CwDg&expire=1661541849&fexp=24001373%2C24007246&fvip=2&gir=yes&id=o-AOqXE9lVS424yszv6LN5V_gaevdHxenJl-tYNy3Drs6g&initcwndbps=1428750&ip=2003%3Ade%3Aaf05%3A2500%3A5dad%3A319b%3Aca30%3Ae212&itag=315&keepalive=yes&lmt=1647476955807851&lsig=AG3C_xAwRQIhAMioKyc-dqs-6uvAwLViCcCTXKHn9sIbo0cbSSBXGG4kAiBQNsRBAvQrbWdOjZIsQXYrfPEb1KDpE_AlSEGQZXB9uA%3D%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&mh=NH&mime=video%2Fwebm&mm=31%2C29&mn=sn-h0jelne7%2Csn-h0jeenl6&ms=au%2Crdu&mt=1661519833&mv=m&mvi=5&n=Zd7nrOM1B2C6PA&ns=426LxLap5MonJD_YWdS4lSYH&pl=37&rbqsm=fr&requiressl=yes&sig=AOq0QJ8wRAIfP4IVSo-00_kq_JIkuh032hcLoJzNEhYjvwgLiDpEzQIhALPVrvDBjRwiFddXiAyADmRtYygte4HvlJ3XOrkOf_TR&source=youtube&sparams=expire%2Cei%2Cip%2Cid%2Caitags%2Csource%2Crequiressl%2Cspc%2Cvprv%2Cmime%2Cns%2Cgir%2Cclen%2Cdur%2Clmt&spc=lT-KhuPtxVzL5-QbZ7S9zNeOHsWTdms&txp=4532434&vprv=1"),
|
||||
Some("https://rr5---sn-h0jelne7.googlevideo.com/videoplayback?c=WEB&clen=5199784&dur=313.801&ei=eckIY72IKcGZ8gOMt6CwDg&expire=1661541849&fexp=24001373%2C24007246&fvip=2&gir=yes&id=o-AOqXE9lVS424yszv6LN5V_gaevdHxenJl-tYNy3Drs6g&initcwndbps=1428750&ip=2003%3Ade%3Aaf05%3A2500%3A5dad%3A319b%3Aca30%3Ae212&itag=251&keepalive=yes&lmt=1647453650291076&lsig=AG3C_xAwRQIhAMioKyc-dqs-6uvAwLViCcCTXKHn9sIbo0cbSSBXGG4kAiBQNsRBAvQrbWdOjZIsQXYrfPEb1KDpE_AlSEGQZXB9uA%3D%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&mh=NH&mime=audio%2Fwebm&mm=31%2C29&mn=sn-h0jelne7%2Csn-h0jeenl6&ms=au%2Crdu&mt=1661519833&mv=m&mvi=5&n=Zd7nrOM1B2C6PA&ns=426LxLap5MonJD_YWdS4lSYH&pl=37&rbqsm=fr&requiressl=yes&sig=AOq0QJ8wRQIhALtI3j8ZChpNb0LcyDZ3yosbWnSpqaO0-jKAe_UM_RQyAiAMwrpdeNbJEnQn3q1eveaAcRcNIwy5iJ4fIjeBW_MUfg%3D%3D&source=youtube&sparams=expire%2Cei%2Cip%2Cid%2Citag%2Csource%2Crequiressl%2Cspc%2Cvprv%2Cmime%2Cns%2Cgir%2Cclen%2Cdur%2Clmt&spc=lT-KhuPtxVzL5-QbZ7S9zNeOHsWTdms&txp=4532434&vprv=1")
|
||||
)]
|
||||
#[case::noaudio(
|
||||
StreamFilter::default().audio_formats(&[]).clone(),
|
||||
StreamFilter::default().audio_formats([]).clone(),
|
||||
Some("https://rr5---sn-h0jelne7.googlevideo.com/videoplayback?c=WEB&clen=23544588&dur=313.834&ei=eckIY72IKcGZ8gOMt6CwDg&expire=1661541849&fexp=24001373%2C24007246&fvip=2&gir=yes&id=o-AOqXE9lVS424yszv6LN5V_gaevdHxenJl-tYNy3Drs6g&initcwndbps=1428750&ip=2003%3Ade%3Aaf05%3A2500%3A5dad%3A319b%3Aca30%3Ae212&itag=18&lmt=1647456546485912&lsig=AG3C_xAwRQIhAMioKyc-dqs-6uvAwLViCcCTXKHn9sIbo0cbSSBXGG4kAiBQNsRBAvQrbWdOjZIsQXYrfPEb1KDpE_AlSEGQZXB9uA%3D%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&mh=NH&mime=video%2Fmp4&mm=31%2C29&mn=sn-h0jelne7%2Csn-h0jeenl6&ms=au%2Crdu&mt=1661519833&mv=m&mvi=5&n=HWZNhARNT_nJgg&ns=pLFQxzhiCbZ9F2HJmDLveKoH&pl=37&ratebypass=yes&rbqsm=fr&requiressl=yes&sig=AOq0QJ8wRQIgeCEjusAq6p33rH0NHyTAbPIRaaEkjDE32AXBFzDvR-ICIQD0LI8hQVH8oCMWu6OuADzc1FSQhIqYs5RLkxBmObIdsw%3D%3D&source=youtube&sparams=expire%2Cei%2Cip%2Cid%2Citag%2Csource%2Crequiressl%2Cspc%2Cvprv%2Cmime%2Cns%2Cgir%2Cclen%2Cratebypass%2Cdur%2Clmt&spc=lT-KhuPtxVzL5-QbZ7S9zNeOHsWTdms&txp=4530434&vprv=1"),
|
||||
None
|
||||
)]
|
||||
|
@ -429,7 +434,7 @@ mod tests {
|
|||
None,
|
||||
Some("https://rr5---sn-h0jelne7.googlevideo.com/videoplayback?c=WEB&clen=5199784&dur=313.801&ei=eckIY72IKcGZ8gOMt6CwDg&expire=1661541849&fexp=24001373%2C24007246&fvip=2&gir=yes&id=o-AOqXE9lVS424yszv6LN5V_gaevdHxenJl-tYNy3Drs6g&initcwndbps=1428750&ip=2003%3Ade%3Aaf05%3A2500%3A5dad%3A319b%3Aca30%3Ae212&itag=251&keepalive=yes&lmt=1647453650291076&lsig=AG3C_xAwRQIhAMioKyc-dqs-6uvAwLViCcCTXKHn9sIbo0cbSSBXGG4kAiBQNsRBAvQrbWdOjZIsQXYrfPEb1KDpE_AlSEGQZXB9uA%3D%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&mh=NH&mime=audio%2Fwebm&mm=31%2C29&mn=sn-h0jelne7%2Csn-h0jeenl6&ms=au%2Crdu&mt=1661519833&mv=m&mvi=5&n=Zd7nrOM1B2C6PA&ns=426LxLap5MonJD_YWdS4lSYH&pl=37&rbqsm=fr&requiressl=yes&sig=AOq0QJ8wRQIhALtI3j8ZChpNb0LcyDZ3yosbWnSpqaO0-jKAe_UM_RQyAiAMwrpdeNbJEnQn3q1eveaAcRcNIwy5iJ4fIjeBW_MUfg%3D%3D&source=youtube&sparams=expire%2Cei%2Cip%2Cid%2Citag%2Csource%2Crequiressl%2Cspc%2Cvprv%2Cmime%2Cns%2Cgir%2Cclen%2Cdur%2Clmt&spc=lT-KhuPtxVzL5-QbZ7S9zNeOHsWTdms&txp=4532434&vprv=1")
|
||||
)]
|
||||
#[case::noformat(StreamFilter::default().audio_formats(&[]).video_formats(&[]).clone(), None, None)]
|
||||
#[case::noformat(StreamFilter::default().audio_formats([]).video_formats([]).clone(), None, None)]
|
||||
fn t_select_video_audio_stream(
|
||||
#[case] filter: StreamFilter,
|
||||
#[case] expect_video_url: Option<&str>,
|
||||
|
|
|
@ -108,7 +108,7 @@ impl RustyPipeInfo<'_> {
|
|||
pub(crate) fn new(language: Option<Language>) -> Self {
|
||||
Self {
|
||||
package: env!("CARGO_PKG_NAME"),
|
||||
version: env!("CARGO_PKG_VERSION"),
|
||||
version: crate::VERSION,
|
||||
date: util::now_sec(),
|
||||
language,
|
||||
}
|
||||
|
|
|
@ -469,6 +469,19 @@ impl<'de> DeserializeAs<'de, TextComponent> for AttributedText {
|
|||
}
|
||||
}
|
||||
|
||||
impl<'de> DeserializeAs<'de, String> for AttributedText {
|
||||
fn deserialize_as<D>(deserializer: D) -> Result<String, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
let components: TextComponents = AttributedText::deserialize_as(deserializer)?;
|
||||
Ok(components
|
||||
.0
|
||||
.into_iter()
|
||||
.fold(String::new(), |acc, c| acc + c.as_str()))
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<TextComponent> for crate::model::ChannelId {
|
||||
type Error = ();
|
||||
|
||||
|
|
|
@ -32,9 +32,10 @@ pub static PLAYLIST_ID_REGEX: Lazy<Regex> =
|
|||
Lazy::new(|| Regex::new(r"^(?:PL|RD|OLAK|UU)[A-Za-z0-9_-]{5,50}$").unwrap());
|
||||
pub static ALBUM_ID_REGEX: Lazy<Regex> =
|
||||
Lazy::new(|| Regex::new(r"^MPREb_[A-Za-z0-9_-]{11}$").unwrap());
|
||||
pub static VANITY_PATH_REGEX: Lazy<Regex> = Lazy::new(|| {
|
||||
Regex::new(r"^/?(?:(?:c/|user/)?[A-z0-9]{1,100})|(?:@[A-z0-9-_.]{1,100})$").unwrap()
|
||||
});
|
||||
pub static VANITY_PATH_REGEX: Lazy<Regex> =
|
||||
Lazy::new(|| Regex::new(r"^/?(?:(?:c/|user/)?[A-z0-9]{1,100})|(?:@[\w\-\.·]{1,30})$").unwrap());
|
||||
pub static CHANNEL_HANDLE_REGEX: Lazy<Regex> =
|
||||
Lazy::new(|| Regex::new(r#"^@[\w\-\.·]{1,30}$"#).unwrap());
|
||||
|
||||
/// Separator string for YouTube Music subtitles
|
||||
pub const DOT_SEPARATOR: &str = " • ";
|
||||
|
@ -551,6 +552,24 @@ impl<'a> Iterator for SplitTokens<'a> {
|
|||
}
|
||||
}
|
||||
|
||||
/// Applies function to the elements of iterator and returns the first successful result
|
||||
/// or the last error if the function fails on all elements. If the iterator is empty, e_empty
|
||||
/// is returned.
|
||||
pub fn find_map_or_last_err<I, T, P, O, E>(mut iter: I, e_empty: E, mut f: P) -> Result<O, E>
|
||||
where
|
||||
I: Iterator<Item = T>,
|
||||
P: FnMut(T) -> Result<O, E>,
|
||||
{
|
||||
let res = iter.try_fold(e_empty, |_, itm| match f(itm) {
|
||||
Ok(o) => Err(o),
|
||||
Err(e) => Ok(e),
|
||||
});
|
||||
match res {
|
||||
Ok(e) => Err(e),
|
||||
Err(o) => Ok(o),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) mod tests {
|
||||
use std::{fs::File, io::BufReader, path::PathBuf};
|
||||
|
@ -730,4 +749,27 @@ pub(crate) mod tests {
|
|||
let res = country_from_name(name);
|
||||
assert_eq!(res, expect);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn t_find_map_or_last_err() {
|
||||
// Success
|
||||
let res = find_map_or_last_err([1, 2, 3].into_iter(), 0, |x: i32| {
|
||||
if x > 2 {
|
||||
Ok(true)
|
||||
} else {
|
||||
Err(x)
|
||||
}
|
||||
});
|
||||
assert_eq!(res, Ok(true));
|
||||
|
||||
// Error
|
||||
let res = find_map_or_last_err([1, 2, 3].into_iter(), 0, |x: i32| Err::<(), _>(x));
|
||||
assert_eq!(res, Err(3));
|
||||
|
||||
// Empty iterator
|
||||
assert_eq!(
|
||||
find_map_or_last_err(std::iter::empty(), 0, |_: i32| Ok(true)),
|
||||
Err(0)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -11,7 +11,10 @@
|
|||
//! - The validation functions of this module are meant vor validating specific data (video IDs,
|
||||
//! channel IDs, playlist IDs) and return [`true`] if the given input is valid
|
||||
|
||||
use crate::{error::Error, util};
|
||||
use crate::{
|
||||
error::Error,
|
||||
util::{self, CHANNEL_HANDLE_REGEX},
|
||||
};
|
||||
use once_cell::sync::Lazy;
|
||||
use regex::Regex;
|
||||
|
||||
|
@ -202,6 +205,32 @@ pub fn track_lyrics_id<S: AsRef<str>>(lyrics_id: S) -> Result<(), Error> {
|
|||
)
|
||||
}
|
||||
|
||||
/// Validate the given channel handle
|
||||
///
|
||||
/// YouTube channel handles can be up to 30 characters long and start with an `@`.
|
||||
/// Allowed characters are letters and numbers (Unicode), underscores (`_`), hyphens (`-`),
|
||||
/// full stops (`.`) and middle dots (`· U+00B7`)
|
||||
///
|
||||
/// There are more fine-grained rules for specific scripts. Verifying these is not implemented.
|
||||
///
|
||||
/// Reference: <https://support.google.com/youtube/answer/11585688>
|
||||
///
|
||||
/// ```
|
||||
/// # use rustypipe::validate;
|
||||
/// assert!(validate::channel_handle("@EEVBlog").is_ok());
|
||||
/// assert!(validate::channel_handle("@Āll·._-").is_ok());
|
||||
/// assert!(validate::channel_handle("@한국").is_ok());
|
||||
///
|
||||
/// assert!(validate::channel_handle("noat").is_err());
|
||||
/// assert!(validate::channel_handle("@no space").is_err());
|
||||
/// ```
|
||||
pub fn channel_handle<S: AsRef<str>>(channel_handle: S) -> Result<(), Error> {
|
||||
check(
|
||||
CHANNEL_HANDLE_REGEX.is_match(channel_handle.as_ref()),
|
||||
"invalid channel handle",
|
||||
)
|
||||
}
|
||||
|
||||
fn check(res: bool, msg: &'static str) -> Result<(), Error> {
|
||||
if res {
|
||||
Ok(())
|
||||
|
|
12760
testfiles/channel/channel_shorts_20240910_lockup.json
Normal file
12760
testfiles/channel/channel_shorts_20240910_lockup.json
Normal file
File diff suppressed because it is too large
Load diff
3150
testfiles/player/tv_video.json
Normal file
3150
testfiles/player/tv_video.json
Normal file
File diff suppressed because one or more lines are too long
|
@ -31,7 +31,8 @@
|
|||
"height": 1080
|
||||
}
|
||||
],
|
||||
"channel": { "id": "UCYq-iAOSZBvoUxvfzwKIZWA", "name": "Jacob + Katie Schwarz" },
|
||||
"channel_id": "UCYq-iAOSZBvoUxvfzwKIZWA",
|
||||
"channel_name": "Jacob + Katie Schwarz",
|
||||
"view_count": 216221243,
|
||||
"keywords": [
|
||||
"4K",
|
||||
|
@ -1125,5 +1126,6 @@
|
|||
"expires_in_seconds": 21540,
|
||||
"hls_manifest_url": null,
|
||||
"dash_manifest_url": null,
|
||||
"preview_frames": []
|
||||
"preview_frames": [],
|
||||
"client_type": "desktop"
|
||||
}
|
||||
|
|
|
@ -31,10 +31,8 @@
|
|||
"height": 1080
|
||||
}
|
||||
],
|
||||
"channel": {
|
||||
"id": "UCX6OQ3DkcsbYNE6H8uQQuVA",
|
||||
"name": "MrBeast"
|
||||
},
|
||||
"channel_id": "UCX6OQ3DkcsbYNE6H8uQQuVA",
|
||||
"channel_name": "MrBeast",
|
||||
"view_count": 136908834,
|
||||
"keywords": [],
|
||||
"is_live": false,
|
||||
|
@ -2120,5 +2118,6 @@
|
|||
"hls_manifest_url": null,
|
||||
"dash_manifest_url": null,
|
||||
"preview_frames": [],
|
||||
"visitor_data": "CgtGWDFCUllrcTdxayjo1_OiBg%3D%3D"
|
||||
"visitor_data": "CgtGWDFCUllrcTdxayjo1_OiBg%3D%3D",
|
||||
"client_type": "desktop"
|
||||
}
|
||||
|
|
|
@ -201,11 +201,11 @@ MusicAlbum(
|
|||
cover: [],
|
||||
artists: [
|
||||
ArtistId(
|
||||
id: Some("UCxByvsK9hDZk2MnnF9jsFGw"),
|
||||
id: Some("UCzXI_RZ1Uqy8L8TiurTFTIg"),
|
||||
name: "Herbrido",
|
||||
),
|
||||
],
|
||||
artist_id: Some("UCxByvsK9hDZk2MnnF9jsFGw"),
|
||||
artist_id: Some("UCzXI_RZ1Uqy8L8TiurTFTIg"),
|
||||
album: Some(AlbumId(
|
||||
id: "MPREb_Z81wHtF9fhC",
|
||||
name: "June Compilation",
|
||||
|
|
|
@ -201,11 +201,11 @@ MusicAlbum(
|
|||
cover: [],
|
||||
artists: [
|
||||
ArtistId(
|
||||
id: Some("UCxByvsK9hDZk2MnnF9jsFGw"),
|
||||
id: Some("UCzXI_RZ1Uqy8L8TiurTFTIg"),
|
||||
name: "[name]",
|
||||
),
|
||||
],
|
||||
artist_id: Some("UCxByvsK9hDZk2MnnF9jsFGw"),
|
||||
artist_id: Some("UCzXI_RZ1Uqy8L8TiurTFTIg"),
|
||||
album: Some(AlbumId(
|
||||
id: "MPREb_Z81wHtF9fhC",
|
||||
name: "[name]",
|
||||
|
|
135
tests/youtube.rs
135
tests/youtube.rs
|
@ -26,6 +26,7 @@ use rustypipe::validate;
|
|||
|
||||
#[rstest]
|
||||
#[case::desktop(ClientType::Desktop)]
|
||||
#[case::tv(ClientType::Tv)]
|
||||
#[case::tv_html5_embed(ClientType::TvHtml5Embed)]
|
||||
#[case::android(ClientType::Android)]
|
||||
#[case::ios(ClientType::Ios)]
|
||||
|
@ -40,13 +41,26 @@ async fn get_player_from_client(#[case] client_type: ClientType, rp: RustyPipe)
|
|||
// dbg!(&player_data);
|
||||
|
||||
assert_eq!(player_data.details.id, "n4tK7LYFxI0");
|
||||
assert_eq!(player_data.details.duration, 259);
|
||||
assert!(!player_data.details.thumbnail.is_empty());
|
||||
assert_eq!(player_data.details.channel_id, "UC_aEa8K-EOJ3D6gOs7HcyNg");
|
||||
assert!(!player_data.details.is_live_content);
|
||||
|
||||
// The TV client dows not output most video metadata
|
||||
if client_type != ClientType::Tv {
|
||||
assert_eq!(
|
||||
player_data.details.name,
|
||||
player_data.details.name.expect("name"),
|
||||
"Spektrem - Shine | Progressive House | NCS - Copyright Free Music"
|
||||
);
|
||||
if client_type == ClientType::DesktopMusic {
|
||||
assert!(player_data.details.description.is_none());
|
||||
} else {
|
||||
assert_eq!(
|
||||
player_data.details.channel_name.expect("channel name"),
|
||||
"NoCopyrightSounds"
|
||||
);
|
||||
assert_gte(
|
||||
player_data.details.view_count.expect("view count"),
|
||||
146_818_808,
|
||||
"view count",
|
||||
);
|
||||
assert!(player_data
|
||||
.details
|
||||
.description
|
||||
|
@ -54,15 +68,10 @@ async fn get_player_from_client(#[case] client_type: ClientType, rp: RustyPipe)
|
|||
.contains(
|
||||
"NCS (NoCopyrightSounds): Empowering Creators through Copyright / Royalty Free Music"
|
||||
));
|
||||
}
|
||||
assert_eq!(player_data.details.duration, 259);
|
||||
assert!(!player_data.details.thumbnail.is_empty());
|
||||
assert_eq!(player_data.details.channel.id, "UC_aEa8K-EOJ3D6gOs7HcyNg");
|
||||
assert_eq!(player_data.details.channel.name, "NoCopyrightSounds");
|
||||
assert_gte(player_data.details.view_count, 146_818_808, "view count");
|
||||
assert_eq!(player_data.details.keywords[0], "spektrem");
|
||||
assert!(!player_data.details.is_live_content);
|
||||
}
|
||||
|
||||
// Ios uses different A/V formats
|
||||
if client_type == ClientType::Ios {
|
||||
let video = player_data
|
||||
.video_only_streams
|
||||
|
@ -120,7 +129,6 @@ async fn get_player_from_client(#[case] client_type: ClientType, rp: RustyPipe)
|
|||
assert_eq!(video.mime, "video/mp4; codecs=\"av01.0.05M.08\"");
|
||||
assert_eq!(video.format, VideoFormat::Mp4);
|
||||
assert_eq!(video.codec, VideoCodec::Av01);
|
||||
assert!(!video.throttled);
|
||||
|
||||
assert_approx(audio.bitrate, 142_718);
|
||||
assert_approx(audio.average_bitrate, 130_708);
|
||||
|
@ -128,11 +136,13 @@ async fn get_player_from_client(#[case] client_type: ClientType, rp: RustyPipe)
|
|||
assert_eq!(audio.mime, "audio/webm; codecs=\"opus\"");
|
||||
assert_eq!(audio.format, AudioFormat::Webm);
|
||||
assert_eq!(audio.codec, AudioCodec::Opus);
|
||||
assert!(!audio.throttled);
|
||||
|
||||
// Desktop client now requires pot token so the streams cannot be tested here
|
||||
if client_type != ClientType::Desktop {
|
||||
check_video_stream(video).await;
|
||||
check_video_stream(audio).await;
|
||||
}
|
||||
}
|
||||
|
||||
assert!(player_data.expires_in_seconds > 10000);
|
||||
}
|
||||
|
@ -211,13 +221,13 @@ async fn check_video_stream(s: impl YtStream) {
|
|||
true
|
||||
)]
|
||||
#[case::agelimit(
|
||||
"laru0QoJUmI",
|
||||
"DJ Robin x Schürze - Layla (Official Video)",
|
||||
"Endlich ist es soweit! Zwei Männer aus dem Schwabenland",
|
||||
188,
|
||||
"UCkJfSrMnLonOZWh-q5os5bg",
|
||||
"Summerfield Records",
|
||||
10_000_000,
|
||||
"ZDKQmBWTRnw",
|
||||
"The Rinky Pink Pounder. Hitachi Magic Wand clone teardown.",
|
||||
"violent adult toys for disassembly",
|
||||
1333,
|
||||
"UCtM5z2gkrGRuWd0JQMx76qA",
|
||||
"bigclivedotcom",
|
||||
250_000,
|
||||
false,
|
||||
false
|
||||
)]
|
||||
|
@ -239,19 +249,25 @@ async fn get_player(
|
|||
let details = player_data.details;
|
||||
|
||||
assert_eq!(details.id, id);
|
||||
assert_eq!(details.name, name);
|
||||
let desc = details.description.expect("description");
|
||||
if let Some(n) = &details.name {
|
||||
assert_eq!(n, name);
|
||||
}
|
||||
if let Some(desc) = &details.description {
|
||||
assert!(desc.contains(description), "description: {desc}");
|
||||
}
|
||||
assert_eq!(details.duration, duration);
|
||||
assert_eq!(details.channel.id, channel_id);
|
||||
assert_eq!(details.channel.name, channel_name);
|
||||
assert_gte(details.view_count, views, "views");
|
||||
assert_eq!(details.channel_id, channel_id);
|
||||
if let Some(cn) = &details.channel_name {
|
||||
assert_eq!(cn, channel_name);
|
||||
}
|
||||
if let Some(vc) = details.view_count {
|
||||
assert_gte(vc, views, "views");
|
||||
}
|
||||
assert_eq!(details.is_live, is_live);
|
||||
assert_eq!(details.is_live_content, is_live_content);
|
||||
|
||||
if is_live {
|
||||
assert!(player_data.hls_manifest_url.is_some());
|
||||
assert!(player_data.dash_manifest_url.is_some());
|
||||
assert!(player_data.hls_manifest_url.is_some() || player_data.dash_manifest_url.is_some());
|
||||
} else {
|
||||
assert!(!player_data.video_only_streams.is_empty());
|
||||
assert!(!player_data.audio_streams.is_empty());
|
||||
|
@ -701,7 +717,7 @@ async fn get_video_details_live(rp: RustyPipe) {
|
|||
assert_eq!(details.id, "jfKfPfyJRdk");
|
||||
assert_eq!(
|
||||
details.name,
|
||||
"lofi hip hop radio 📚 - beats to relax/study to"
|
||||
"lofi hip hop radio 📚 beats to relax/study to"
|
||||
);
|
||||
let desc = details.description.to_plaintext();
|
||||
assert!(
|
||||
|
@ -736,24 +752,27 @@ async fn get_video_details_live(rp: RustyPipe) {
|
|||
#[rstest]
|
||||
#[tokio::test]
|
||||
async fn get_video_details_agegate(rp: RustyPipe) {
|
||||
let details = rp.query().video_details("laru0QoJUmI").await.unwrap();
|
||||
let details = rp.query().video_details("ZDKQmBWTRnw").await.unwrap();
|
||||
|
||||
// dbg!(&details);
|
||||
|
||||
assert_eq!(details.id, "laru0QoJUmI");
|
||||
assert_eq!(details.name, "DJ Robin x Schürze - Layla (Official Video)");
|
||||
assert_eq!(details.id, "ZDKQmBWTRnw");
|
||||
assert_eq!(
|
||||
details.name,
|
||||
"The Rinky Pink Pounder. Hitachi Magic Wand clone teardown."
|
||||
);
|
||||
insta::assert_ron_snapshot!(details.description, @"RichText([])");
|
||||
|
||||
assert_eq!(details.channel.id, "UCkJfSrMnLonOZWh-q5os5bg");
|
||||
assert_eq!(details.channel.name, "Summerfield Records");
|
||||
assert_eq!(details.channel.id, "UCtM5z2gkrGRuWd0JQMx76qA");
|
||||
assert_eq!(details.channel.name, "bigclivedotcom");
|
||||
assert!(!details.channel.avatar.is_empty(), "no channel avatars");
|
||||
assert_eq!(details.channel.verification, Verification::Verified);
|
||||
assert_gteo(details.channel.subscriber_count, 250_000, "subscribers");
|
||||
assert_gte(details.view_count, 10_000_000, "views");
|
||||
assert_gteo(details.like_count, 150_000, "likes");
|
||||
assert_gteo(details.channel.subscriber_count, 1_000_000, "subscribers");
|
||||
assert_gte(details.view_count, 250_000, "views");
|
||||
assert_gteo(details.like_count, 5_000, "likes");
|
||||
|
||||
let date = details.publish_date.expect("publish_date");
|
||||
assert_eq!(date.date(), date!(2022 - 5 - 13));
|
||||
assert_eq!(date.date(), date!(2017 - 3 - 09));
|
||||
|
||||
assert!(!details.is_live);
|
||||
assert!(!details.is_ccommons);
|
||||
|
@ -860,8 +879,11 @@ async fn channel_videos(rp: RustyPipe) {
|
|||
#[rstest]
|
||||
#[tokio::test]
|
||||
async fn channel_shorts(rp: RustyPipe) {
|
||||
let vd = rp.query().get_visitor_data().await.unwrap();
|
||||
|
||||
let channel = rp
|
||||
.query()
|
||||
.visitor_data(vd)
|
||||
.channel_videos_tab("UCh8gHdtzO2tXd593_bjErWg", ChannelVideoTab::Shorts)
|
||||
.await
|
||||
.unwrap();
|
||||
|
@ -869,16 +891,13 @@ async fn channel_shorts(rp: RustyPipe) {
|
|||
// dbg!(&channel);
|
||||
assert_eq!(channel.id, "UCh8gHdtzO2tXd593_bjErWg");
|
||||
assert_eq!(channel.name, "Doobydobap");
|
||||
assert_eq!(channel.handle.as_deref(), Some("@Doobydobap"));
|
||||
assert_gteo(channel.subscriber_count, 2_800_000, "subscribers");
|
||||
assert!(!channel.avatar.is_empty(), "got no thumbnails");
|
||||
assert_eq!(channel.verification, Verification::Verified);
|
||||
assert!(channel
|
||||
.description
|
||||
.contains("Hi, I\u{2019}m Tina, aka Doobydobap"));
|
||||
assert_eq!(
|
||||
channel.vanity_url.as_deref(),
|
||||
Some("https://www.youtube.com/@Doobydobap")
|
||||
);
|
||||
assert!(!channel.banner.is_empty(), "got no banners");
|
||||
|
||||
assert!(
|
||||
|
@ -978,15 +997,12 @@ async fn channel_search(rp: RustyPipe) {
|
|||
fn assert_channel_eevblog<T>(channel: &Channel<T>) {
|
||||
assert_eq!(channel.id, "UC2DjFE7Xf11URZqWBigcVOQ");
|
||||
assert_eq!(channel.name, "EEVblog");
|
||||
assert_eq!(channel.handle.as_deref(), Some("@EEVblog"));
|
||||
assert_gteo(channel.subscriber_count, 880_000, "subscribers");
|
||||
assert!(!channel.avatar.is_empty(), "got no thumbnails");
|
||||
assert_eq!(channel.verification, Verification::Verified);
|
||||
assert_eq!(channel.description, "NO SCRIPT, NO FEAR, ALL OPINION\nAn off-the-cuff Video Blog about Electronics Engineering, for engineers, hobbyists, enthusiasts, hackers and Makers\nHosted by Dave Jones from Sydney Australia\n\nDONATIONS:\nBitcoin: 3KqyH1U3qrMPnkLufM2oHDU7YB4zVZeFyZ\nEthereum: 0x99ccc4d2654ba40744a1f678d9868ecb15e91206\nPayPal: david@alternatezone.com\n\nPatreon: https://www.patreon.com/eevblog\n\nEEVblog2: http://www.youtube.com/EEVblog2\nEEVdiscover: https://www.youtube.com/channel/UCkGvUEt8iQLmq3aJIMjT2qQ\n\nEMAIL:\nAdvertising/Commercial: eevblog+business@gmail.com\nFan mail: eevblog+fan@gmail.com\nHate Mail: eevblog+hate@gmail.com\n\nI DON'T DO PAID VIDEO SPONSORSHIPS, DON'T ASK!\n\nPLEASE:\nDo NOT ask for personal advice on something, post it in the EEVblog forum.\nI read ALL email, but please don't be offended if I don't have time to reply, I get a LOT of email.\n\nMailbag\nPO Box 7949\nBaulkham Hills NSW 2153\nAUSTRALIA");
|
||||
assert!(!channel.tags.is_empty(), "got no tags");
|
||||
assert_eq!(
|
||||
channel.vanity_url.as_deref(),
|
||||
Some("https://www.youtube.com/@EEVblog")
|
||||
);
|
||||
assert!(!channel.banner.is_empty(), "got no banners");
|
||||
}
|
||||
|
||||
|
@ -1268,7 +1284,7 @@ mod channel_rss {
|
|||
async fn search(rp: RustyPipe, unlocalized: bool) {
|
||||
let result = rp
|
||||
.query()
|
||||
.search::<YouTubeItem, _>("doobydoobap")
|
||||
.search::<YouTubeItem, _>("arudino")
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
|
@ -1279,7 +1295,7 @@ async fn search(rp: RustyPipe, unlocalized: bool) {
|
|||
);
|
||||
|
||||
if unlocalized {
|
||||
assert_eq!(result.corrected_query.as_deref(), Some("doobydobap"));
|
||||
assert_eq!(result.corrected_query.as_deref(), Some("arduino"));
|
||||
}
|
||||
|
||||
assert_next(result.items, rp.query(), 10, 2, true).await;
|
||||
|
@ -1397,6 +1413,8 @@ async fn search_suggestion_empty(rp: RustyPipe) {
|
|||
#[case("https://music.youtube.com/browse/MPREb_GyH43gCvdM5", UrlTarget::Album {id: "MPREb_GyH43gCvdM5".to_owned()})]
|
||||
#[case("https://music.youtube.com/browse/UC5I2hjZYiW9gZPVkvzM8_Cw", UrlTarget::Channel {id: "UC5I2hjZYiW9gZPVkvzM8_Cw".to_owned()})]
|
||||
#[case("https://music.youtube.com/browse/MPADUC7cl4MmM6ZZ2TcFyMk_b4pg", UrlTarget::Channel {id: "UC7cl4MmM6ZZ2TcFyMk_b4pg".to_owned()})]
|
||||
// Music album playlist URL from regular YouTube site (redirects to music album)
|
||||
#[case("https://music.youtube.com/playlist?list=OLAK5uy_noT8bq6-DUEJ5KsdX1D4-wWcYtjiuYEnU", UrlTarget::Album {id: "MPREb_5CPCpzS3imM".to_owned()})]
|
||||
#[tokio::test]
|
||||
async fn resolve_url(#[case] url: &str, #[case] expect: UrlTarget, rp: RustyPipe) {
|
||||
let target = rp.query().resolve_url(url, true).await.unwrap();
|
||||
|
@ -1439,18 +1457,6 @@ async fn resolve_channel_not_found(rp: RustyPipe) {
|
|||
|
||||
//#TRENDS
|
||||
|
||||
#[rstest]
|
||||
#[tokio::test]
|
||||
#[ignore]
|
||||
async fn startpage(rp: RustyPipe) {
|
||||
let startpage = rp.query().startpage().await.unwrap();
|
||||
|
||||
// The startpage requires visitor data to fetch continuations
|
||||
assert!(startpage.visitor_data.is_some());
|
||||
|
||||
assert_next(startpage, rp.query(), 8, 2, true).await;
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[tokio::test]
|
||||
async fn trending(rp: RustyPipe) {
|
||||
|
@ -2153,7 +2159,7 @@ async fn music_search_playlists(rp: RustyPipe, unlocalized: bool) {
|
|||
async fn music_search_playlists_community(rp: RustyPipe) {
|
||||
let res = rp
|
||||
.query()
|
||||
.music_search_playlists("Best Pop Music Videos - Top Pop Hits Playlist", true)
|
||||
.music_search_playlists("Miku my beloved (Jaiden Animation Miku Playlist)", true)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
|
@ -2162,20 +2168,20 @@ async fn music_search_playlists_community(rp: RustyPipe) {
|
|||
.items
|
||||
.items
|
||||
.iter()
|
||||
.find(|p| p.id == "PLMC9KNkIncKtGvr2kFRuXBVmBev6cAJ2u")
|
||||
.find(|p| p.id == "PLgAAMoX4rK3KhSGmIsN0LEoC3qowEr2Lz")
|
||||
.unwrap_or_else(|| {
|
||||
panic!("could not find playlist, got {:#?}", &res.items.items);
|
||||
});
|
||||
|
||||
assert_eq!(
|
||||
playlist.name,
|
||||
"Best Pop Music Videos - Top Pop Hits Playlist"
|
||||
"Miku my beloved (Jaiden Animation Miku Playlist)"
|
||||
);
|
||||
assert!(!playlist.thumbnail.is_empty(), "got no thumbnail");
|
||||
|
||||
let channel = playlist.channel.as_ref().unwrap();
|
||||
assert_eq!(channel.id, "UCs72iRpTEuwV3y6pdWYLgiw");
|
||||
assert_eq!(channel.name, "Redlist - Just Hits");
|
||||
assert_eq!(channel.id, "UCsXOMpqp3_ZPOmk-HGKEPRg");
|
||||
assert_eq!(channel.name, "Beanie Bean");
|
||||
assert!(!playlist.from_ytm);
|
||||
}
|
||||
|
||||
|
@ -2687,6 +2693,7 @@ fn rp(lang: Language) -> RustyPipe {
|
|||
let vdata = std::env::var("YT_VDATA").ok();
|
||||
RustyPipe::builder()
|
||||
.strict()
|
||||
.storage_dir(env!("CARGO_MANIFEST_DIR"))
|
||||
.lang(lang)
|
||||
.visitor_data_opt(vdata)
|
||||
.build()
|
||||
|
|
Loading…
Reference in a new issue