Compare commits
6 commits
main
...
feat/oauth
Author | SHA1 | Date | |
---|---|---|---|
4e5fb1d898 | |||
a9b33fb882 | |||
5459b88660 | |||
e457733244 | |||
e5ca368d3f | |||
d2df0f2b06 |
258 changed files with 17396 additions and 317109 deletions
|
@ -24,41 +24,26 @@ jobs:
|
|||
with:
|
||||
cache-on-failure: "true"
|
||||
|
||||
- name: Download rustypipe-botguard
|
||||
run: |
|
||||
TARGET=$(rustc --version --verbose | grep "host:" | sed -e 's/^host: //')
|
||||
cd ~
|
||||
curl -SsL -o rustypipe-botguard.tar.xz "https://codeberg.org/ThetaDev/rustypipe-botguard/releases/download/v0.1.1/rustypipe-botguard-v0.1.1-${TARGET}.tar.xz"
|
||||
cd /usr/local/bin
|
||||
sudo tar -xJf ~/rustypipe-botguard.tar.xz
|
||||
rm ~/rustypipe-botguard.tar.xz
|
||||
rustypipe-botguard --version
|
||||
|
||||
- name: 📎 Clippy
|
||||
run: |
|
||||
cargo clippy --all --tests --features=rss,userdata,indicatif,audiotag -- -D warnings
|
||||
cargo clippy --package=rustypipe --tests -- -D warnings
|
||||
cargo clippy --package=rustypipe-downloader -- -D warnings
|
||||
cargo clippy --package=rustypipe-cli -- -D warnings
|
||||
cargo clippy --package=rustypipe-cli --features=timezone -- -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,userdata --workspace -- --skip 'user_data::'
|
||||
run: |
|
||||
printf "$RUSTYPIPE_CACHE" > rustypipe_cache.json
|
||||
ls
|
||||
head -n 1 rustypipe_cache.json
|
||||
RUST_LOG=debug cargo test --package rustypipe --test youtube --features rss -- user_login --exact --nocapture
|
||||
cargo nextest run --config-file ~/.config/nextest.toml --profile ci --retries 2 --features rss --workspace
|
||||
env:
|
||||
ALL_PROXY: "http://warpproxy:8124"
|
||||
|
||||
- name: Move test report
|
||||
if: always()
|
||||
run: mv target/nextest/ci/junit.xml junit.xml || true
|
||||
RUSTYPIPE_CACHE: "${{ secrets.RUSTYPIPE_CACHE }}"
|
||||
|
||||
- name: 💌 Upload test report
|
||||
if: always()
|
||||
uses: https://code.forgejo.org/forgejo/upload-artifact@v4
|
||||
with:
|
||||
name: test
|
||||
path: |
|
||||
junit.xml
|
||||
rustypipe_reports
|
||||
path: target/nextest/ci/junit.xml
|
||||
|
||||
- name: 🔗 Artifactview PR comment
|
||||
if: ${{ always() && github.event_name == 'pull_request' }}
|
||||
|
|
|
@ -1,69 +0,0 @@
|
|||
name: Release CLI
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- "rustypipe-cli/v*.*.*"
|
||||
|
||||
jobs:
|
||||
Release:
|
||||
runs-on: cimaster-latest
|
||||
steps:
|
||||
- name: 📦 Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup cross compilation
|
||||
run: |
|
||||
rustup target add x86_64-pc-windows-msvc x86_64-apple-darwin aarch64-apple-darwin
|
||||
cargo install cargo-xwin
|
||||
|
||||
# https://wapl.es/rust/2019/02/17/rust-cross-compile-linux-to-macos.html/
|
||||
sudo apt-get install -y llvm clang cmake
|
||||
cd ~
|
||||
git clone https://github.com/tpoechtrager/osxcross
|
||||
cd osxcross
|
||||
wget -nc "https://github.com/joseluisq/macosx-sdks/releases/download/12.3/MacOSX12.3.sdk.tar.xz"
|
||||
mv MacOSX12.3.sdk.tar.xz tarballs/
|
||||
UNATTENDED=yes OSX_VERSION_MIN=12.3 ./build.sh
|
||||
OSXCROSS_BIN="$(pwd)/target/bin"
|
||||
|
||||
echo "CARGO_TARGET_X86_64_APPLE_DARWIN_LINKER=$(find "$OSXCROSS_BIN" -name "x86_64-apple-darwin*-clang")" >> $GITHUB_ENV
|
||||
echo "CARGO_TARGET_X86_64_APPLE_DARWIN_RUSTFLAGS=-Car=$(find "$OSXCROSS_BIN" -name "x86_64-apple-darwin*-ar"),-Clink-arg=-undefined,-Clink-arg=dynamic_lookup" >> $GITHUB_ENV
|
||||
echo "CARGO_TARGET_AARCH64_APPLE_DARWIN_LINKER=$(find "$OSXCROSS_BIN" -name "aarch64-apple-darwin*-clang")" >> $GITHUB_ENV
|
||||
echo "CARGO_TARGET_AARCH64_APPLE_DARWIN_RUSTFLAGS=-Car=$(find "$OSXCROSS_BIN" -name "aarch64-apple-darwin*-ar"),-Clink-arg=-undefined,-Clink-arg=dynamic_lookup" >> $GITHUB_ENV
|
||||
|
||||
- name: ⚒️ Build application
|
||||
run: |
|
||||
export PATH="$PATH:$HOME/osxcross/target/bin"
|
||||
CRATE="rustypipe-cli"
|
||||
PKG_CONFIG_SYSROOT_DIR=/usr/x86_64-linux-gnu cargo build --release --package=$CRATE --target x86_64-unknown-linux-gnu
|
||||
PKG_CONFIG_SYSROOT_DIR=/usr/aarch64-linux-gnu cargo build --release --package=$CRATE --target aarch64-unknown-linux-gnu
|
||||
CC="$CARGO_TARGET_X86_64_APPLE_DARWIN_LINKER" CXX="$CARGO_TARGET_X86_64_APPLE_DARWIN_LINKER++" cargo build --release --package=$CRATE --target x86_64-apple-darwin
|
||||
CC="$CARGO_TARGET_AARCH64_APPLE_DARWIN_LINKER" CXX="$CARGO_TARGET_AARCH64_APPLE_DARWIN_LINKER++" cargo build --release --package=$CRATE --target aarch64-apple-darwin
|
||||
cargo xwin build --release --package=$CRATE --target x86_64-pc-windows-msvc
|
||||
|
||||
- name: Prepare release
|
||||
run: |
|
||||
CRATE="rustypipe-cli"
|
||||
BIN="rustypipe"
|
||||
echo "CRATE=$CRATE" >> "$GITHUB_ENV"
|
||||
echo "CRATE_VERSION=$(echo '${{ github.ref_name }}' | awk 'BEGIN{RS="/"} NR==2{print}')" >> "$GITHUB_ENV"
|
||||
CL_PATH="cli/CHANGELOG.md"
|
||||
{
|
||||
echo 'CHANGELOG<<END_OF_FILE'
|
||||
awk 'BEGIN{RS="(^|\n)## [^\n]+\n*"} NR==2 { print }' "$CL_PATH"
|
||||
echo END_OF_FILE
|
||||
} >> "$GITHUB_ENV"
|
||||
|
||||
mkdir dist
|
||||
|
||||
for arch in x86_64-unknown-linux-gnu aarch64-unknown-linux-gnu x86_64-apple-darwin aarch64-apple-darwin; do
|
||||
tar -cJf "dist/${BIN}-${CRATE_VERSION}-${arch}.tar.xz" -C target/${arch}/release "${BIN}"
|
||||
done
|
||||
(cd target/x86_64-pc-windows-msvc/release && zip -9 "../../../dist/${BIN}-${CRATE_VERSION}-x86_64-pc-windows-msvc.zip" "${BIN}.exe")
|
||||
|
||||
- name: 🎉 Publish release
|
||||
uses: https://gitea.com/actions/release-action@main
|
||||
with:
|
||||
title: "${{ env.CRATE }} ${{ env.CRATE_VERSION }}"
|
||||
body: "${{ env.CHANGELOG }}"
|
||||
files: dist/*
|
|
@ -17,7 +17,7 @@ jobs:
|
|||
renovate:
|
||||
runs-on: docker
|
||||
container:
|
||||
image: renovate/renovate:39
|
||||
image: renovate/renovate:latest
|
||||
|
||||
steps:
|
||||
- name: Load renovate repo cache
|
3
.gitignore
vendored
3
.gitignore
vendored
|
@ -4,5 +4,4 @@
|
|||
*.snap.new
|
||||
|
||||
rustypipe_reports
|
||||
rustypipe_cache*.json
|
||||
bg_snapshot.bin
|
||||
rustypipe_cache.json
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
repos:
|
||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||
rev: v5.0.0
|
||||
rev: v4.3.0
|
||||
hooks:
|
||||
- id: end-of-file-fixer
|
||||
- id: check-json
|
||||
|
@ -10,8 +10,4 @@ repos:
|
|||
hooks:
|
||||
- id: cargo-fmt
|
||||
- id: cargo-clippy
|
||||
name: cargo-clippy rustypipe
|
||||
args: ["--package=rustypipe", "--tests", "--", "-D", "warnings"]
|
||||
- id: cargo-clippy
|
||||
name: cargo-clippy workspace
|
||||
args: ["--all", "--tests", "--features=rss,userdata,indicatif,audiotag", "--", "-D", "warnings"]
|
||||
args: ["--all", "--tests", "--features=rss,indicatif,audiotag", "--", "-D", "warnings"]
|
||||
|
|
3
.vscode/settings.json
vendored
Normal file
3
.vscode/settings.json
vendored
Normal file
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"rust-analyzer.cargo.features": ["rss", "indicatif", "audiotag"]
|
||||
}
|
10
.woodpecker.yml
Normal file
10
.woodpecker.yml
Normal file
|
@ -0,0 +1,10 @@
|
|||
steps:
|
||||
test:
|
||||
image: rust:latest
|
||||
environment:
|
||||
- CARGO_REGISTRIES_CRATES_IO_PROTOCOL=sparse
|
||||
commands:
|
||||
- rustup component add rustfmt clippy
|
||||
- cargo fmt --all --check
|
||||
- cargo clippy --all --features=rss -- -D warnings
|
||||
- cargo test --features=rss --workspace
|
212
CHANGELOG.md
212
CHANGELOG.md
|
@ -3,218 +3,6 @@
|
|||
All notable changes to this project will be documented in this file.
|
||||
|
||||
|
||||
## [v0.11.3](https://codeberg.org/ThetaDev/rustypipe/compare/rustypipe/v0.11.2..rustypipe/v0.11.3) - 2025-04-03
|
||||
|
||||
### 🐛 Bug Fixes
|
||||
|
||||
- Deobfuscator: global variable extraction fixed - ([ac44e95](https://codeberg.org/ThetaDev/rustypipe/commit/ac44e95a88d95f9d2d1ec672f86ca9d31d6991b9))
|
||||
- Deobfuscator: small simplification - ([189ba81](https://codeberg.org/ThetaDev/rustypipe/commit/189ba81a42e6c09f6af4d2768c449c22b864101e))
|
||||
- Deobfuscator: handle global functions as well - ([939a7ae](https://codeberg.org/ThetaDev/rustypipe/commit/939a7aea61a3eee4c1e67bfbfc835f0ce3934171))
|
||||
- Handle music playlist/album not found - ([ea80717](https://codeberg.org/ThetaDev/rustypipe/commit/ea80717f692b2c45b5063c362c9fa8ebca5a3471))
|
||||
- Switch client if no adaptive stream URLs were returned - ([187bf1c](https://codeberg.org/ThetaDev/rustypipe/commit/187bf1c9a0e846bff205e0d71a19c5a1ce7b1943))
|
||||
- Handle music artist not found - ([daf3d03](https://codeberg.org/ThetaDev/rustypipe/commit/daf3d035be38b59aef1ae205ac91c2bbdda2fe66))
|
||||
|
||||
### ⚙️ Miscellaneous Tasks
|
||||
|
||||
- *(deps)* Update rust crate rand to 0.9.0 - ([af415dd](https://codeberg.org/ThetaDev/rustypipe/commit/af415ddf8f94f00edb918f271d8e6336503e9faf))
|
||||
|
||||
|
||||
## [v0.11.2](https://codeberg.org/ThetaDev/rustypipe/compare/rustypipe/v0.11.1..rustypipe/v0.11.2) - 2025-03-24
|
||||
|
||||
### 🐛 Bug Fixes
|
||||
|
||||
- A/B test 22: commandExecutorCommand for playlist continuations - ([e8acbfb](https://codeberg.org/ThetaDev/rustypipe/commit/e8acbfbbcf5d31b5ac34410ddf334e5534e3762f))
|
||||
- Extract deobf data with global strings variable - ([4ce6746](https://codeberg.org/ThetaDev/rustypipe/commit/4ce6746be538564e79f7e3c67d7a91aaa53f48ea))
|
||||
- Handle player returning no adaptive stream URLs - ([07db7b1](https://codeberg.org/ThetaDev/rustypipe/commit/07db7b1166e912e1554f98f2ae20c2c356fed38f))
|
||||
|
||||
|
||||
## [v0.11.1](https://codeberg.org/ThetaDev/rustypipe/compare/rustypipe/v0.11.0..rustypipe/v0.11.1) - 2025-03-16
|
||||
|
||||
### 🐛 Bug Fixes
|
||||
|
||||
- Simplify get_player_from_clients logic - ([c04b606](https://codeberg.org/ThetaDev/rustypipe/commit/c04b60604d2628bf8f0e3de453c243adbb966e57))
|
||||
- Desktop client: generate PO token from user_syncid when authenticated - ([8342cae](https://codeberg.org/ThetaDev/rustypipe/commit/8342caeb0f566a38060a6ec69f3ca65b9a2afcd6))
|
||||
- Always skip failed clients - ([63a6f50](https://codeberg.org/ThetaDev/rustypipe/commit/63a6f50a8b5ad6bb984282335c1481ae3cd2fe83))
|
||||
|
||||
### ⚙️ Miscellaneous Tasks
|
||||
|
||||
- *(deps)* Update rust crate rstest to 0.25.0 - ([9ed1306](https://codeberg.org/ThetaDev/rustypipe/commit/9ed1306f3aaeb993c409997ddfbc47499e4f4d22))
|
||||
|
||||
|
||||
## [v0.11.0](https://codeberg.org/ThetaDev/rustypipe/compare/rustypipe/v0.10.0..rustypipe/v0.11.0) - 2025-02-26
|
||||
|
||||
### 🚀 Features
|
||||
|
||||
- Add original album track count, fix fetching albums with more than 200 tracks - ([544782f](https://codeberg.org/ThetaDev/rustypipe/commit/544782f8de728cda0aca9a1cb95837cdfbd001f1))
|
||||
|
||||
### 🐛 Bug Fixes
|
||||
|
||||
- A/B test 21: music album recommendations - ([6737512](https://codeberg.org/ThetaDev/rustypipe/commit/6737512f5f67c8cd05d4552dd0e0f24381035b35))
|
||||
|
||||
|
||||
## [v0.10.0](https://codeberg.org/ThetaDev/rustypipe/compare/rustypipe/v0.9.0..rustypipe/v0.10.0) - 2025-02-09
|
||||
|
||||
### 🚀 Features
|
||||
|
||||
- Add visitor data cache, remove random visitor data - ([b12f4c5](https://codeberg.org/ThetaDev/rustypipe/commit/b12f4c5d821a9189d7ed8410ad860824b6d052ef))
|
||||
- Add support for rustypipe-botguard to get PO tokens - ([b90a252](https://codeberg.org/ThetaDev/rustypipe/commit/b90a252a5e1bf05a5294168b0ec16a73cbb88f42))
|
||||
- Add session po token cache - ([b72b501](https://codeberg.org/ThetaDev/rustypipe/commit/b72b501b6dbcf4333b24cd80e7c8c61b0c21ec91))
|
||||
- Check rustypipe-botguard-api version - ([8385b87](https://codeberg.org/ThetaDev/rustypipe/commit/8385b87c63677f32a240679a78702f53072e517a))
|
||||
- Rewrite request attempt system, retry with different visitor data - ([dfd03ed](https://codeberg.org/ThetaDev/rustypipe/commit/dfd03edfadff2657e9cfbf04e5d313ba409520ac))
|
||||
- Log failed player fetch attempts with player_from_clients - ([8e35358](https://codeberg.org/ThetaDev/rustypipe/commit/8e35358c8941301f6ebf7646a11ab22711082569))
|
||||
- Add timezone query option - ([3a2370b](https://codeberg.org/ThetaDev/rustypipe/commit/3a2370b97ca3d0f40d72d66a23295557317d29fb))
|
||||
- [**breaking**] Add userdata feature for all personal data queries (playback history, subscriptions) - ([65cb424](https://codeberg.org/ThetaDev/rustypipe/commit/65cb4244c6ab547f53d0cb12af802c4189188c86))
|
||||
- Add RustyPipe::version_botguard fn, detect rustypipe-botguard in current dir, add botguard version to report - ([1d755b7](https://codeberg.org/ThetaDev/rustypipe/commit/1d755b76bf4569f7d0bb90a65494ac8e7aae499a))
|
||||
|
||||
### 🐛 Bug Fixes
|
||||
|
||||
- Parsing history dates - ([af7dc10](https://codeberg.org/ThetaDev/rustypipe/commit/af7dc1016322a87dd8fec0b739939c2b12b6f400))
|
||||
- A/V streams incorrectly recognized as video-only - ([2b891ca](https://codeberg.org/ThetaDev/rustypipe/commit/2b891ca0788f91f16dbb9203191cb3d2092ecc74))
|
||||
- Update iOS client - ([e915416](https://codeberg.org/ThetaDev/rustypipe/commit/e91541629d6c944c1001f5883e3c1264aeeb3969))
|
||||
- A/B test 20: music continuation item renderer - ([9c67f8f](https://codeberg.org/ThetaDev/rustypipe/commit/9c67f8f85bef8214848dc9d17bff6cff252e015e))
|
||||
- Include whole request body in report - ([15245c1](https://codeberg.org/ThetaDev/rustypipe/commit/15245c18b584e42523762b94fcc7284d483660a0))
|
||||
- Extracting nsig fn when outside variable starts with $ - ([eda16e3](https://codeberg.org/ThetaDev/rustypipe/commit/eda16e378730a3b57c4982a626df1622a93c574a))
|
||||
- Retry updating deobf data after a RustyPipe update - ([50ab1f7](https://codeberg.org/ThetaDev/rustypipe/commit/50ab1f7a5d8aeaa3720264b4a4b27805bb0e8121))
|
||||
- Allow player data to be fetched without botguard - ([29c854b](https://codeberg.org/ThetaDev/rustypipe/commit/29c854b20d7a6677415b1744e7ba7ecd4f594ea5))
|
||||
- Output full request body in reports, clean up `get_player_po_token` - ([a0d850f](https://codeberg.org/ThetaDev/rustypipe/commit/a0d850f8e01428a73bbd66397d0dbf797b45958f))
|
||||
- Correct timezone offset for parsed dates, add timezone_local option - ([a5a7be5](https://codeberg.org/ThetaDev/rustypipe/commit/a5a7be5b4e0a0b73d7e1dc802ebd7bd48dafc76d))
|
||||
- Use localzone crate to get local tz - ([5acbf0e](https://codeberg.org/ThetaDev/rustypipe/commit/5acbf0e456b1f10707e0a56125d993a8129eee3a))
|
||||
- Only use cached potokens with min. 10min lifetime - ([0c94267](https://codeberg.org/ThetaDev/rustypipe/commit/0c94267d0371b2b26c7b5c9abfa156d5cde2153e))
|
||||
|
||||
### 📚 Documentation
|
||||
|
||||
- Add Botguard info to README - ([9957add](https://codeberg.org/ThetaDev/rustypipe/commit/9957add2b5d6391b2c1869d2019fd7dd91b8cd41))
|
||||
|
||||
### ⚙️ Miscellaneous Tasks
|
||||
|
||||
- *(deps)* Update rust crate rquickjs to 0.9.0 (#33) - ([2c8ac41](https://codeberg.org/ThetaDev/rustypipe/commit/2c8ac410aa535d83f8bcc7181f81914b13bceb77))
|
||||
|
||||
|
||||
## [v0.9.0](https://codeberg.org/ThetaDev/rustypipe/compare/rustypipe/v0.8.0..rustypipe/v0.9.0) - 2025-01-16
|
||||
|
||||
### 🚀 Features
|
||||
|
||||
- Add functions to fetch a user's history and subscriptions - ([14e3995](https://codeberg.org/ThetaDev/rustypipe/commit/14e399594f97a1228a8c2991a14dd8745af1beb7))
|
||||
- Add history item dates, extend timeago parser - ([320a8c2](https://codeberg.org/ThetaDev/rustypipe/commit/320a8c2c24217ad5697f0424c4f994bbbe31f3aa))
|
||||
- Add session headers when using cookie auth - ([3c95b52](https://codeberg.org/ThetaDev/rustypipe/commit/3c95b52ceaf0df2d67ee0d2f2ac658f666f29836))
|
||||
- Add cookies.txt parser, add cookie auth + history cmds to CLI - ([cf498e4](https://codeberg.org/ThetaDev/rustypipe/commit/cf498e4a8f9318b0197bc3f0cbaf7043c53adb9d))
|
||||
- Add method to get saved_playlists - ([27f64fc](https://codeberg.org/ThetaDev/rustypipe/commit/27f64fc412e833d5bd19ad72913aae19358e98b9))
|
||||
- Extract player DRM data - ([2af4001](https://codeberg.org/ThetaDev/rustypipe/commit/2af4001c75f2ff4f7c891aa59ac22c2c6b7902a2))
|
||||
- Add Dolby audio codecs (ac-3, ec-3) - ([a7f8c78](https://codeberg.org/ThetaDev/rustypipe/commit/a7f8c789b1a34710274c4630e027ef868397aea2))
|
||||
- Add DRM and audio channel number filtering to StreamFilter - ([d5abee2](https://codeberg.org/ThetaDev/rustypipe/commit/d5abee275300ab1bc10fc8d6c35a4e3813fd2bd4))
|
||||
- Set cache file permissions to 600 - ([dee8a99](https://codeberg.org/ThetaDev/rustypipe/commit/dee8a99e7a8d071c987709a01f02ee8fecf2d776))
|
||||
|
||||
### 🐛 Bug Fixes
|
||||
|
||||
- Dont leak authorization and cookie header in reports - ([75fce91](https://codeberg.org/ThetaDev/rustypipe/commit/75fce91353c02cd498f27d21b08261c23ea03d70))
|
||||
- Require new time crate version which added Month::length - ([ec7a195](https://codeberg.org/ThetaDev/rustypipe/commit/ec7a195c98f39346c4c8db875212c3843580450e))
|
||||
- Parsing numbers (it), dates (kn) - ([63f86b6](https://codeberg.org/ThetaDev/rustypipe/commit/63f86b6e186aa1d2dcaf7e9169ccebb2265e5905))
|
||||
- Accept user-specific playlist ids (LL, WL) - ([97c3f30](https://codeberg.org/ThetaDev/rustypipe/commit/97c3f30d180d3e62b7e19f22d191d7fd7614daca))
|
||||
- Only use auth-enabled clients for fetching player with auth option enabled - ([2b2b4af](https://codeberg.org/ThetaDev/rustypipe/commit/2b2b4af0b26cdd0d4bf2218d3f527abd88658abf))
|
||||
- A/B test 19: Music artist album groups reordered - ([5daad1b](https://codeberg.org/ThetaDev/rustypipe/commit/5daad1b700e8dcf1f3e803db1685f08f27794898))
|
||||
- Switch to rquickjs crate for deobfuscator - ([75c3746](https://codeberg.org/ThetaDev/rustypipe/commit/75c3746890f3428f3314b7b10c9ec816ad275836))
|
||||
- Player_from_clients method not send/sync - ([9c512c3](https://codeberg.org/ThetaDev/rustypipe/commit/9c512c3c4dbec0fc3b973536733d61ba61125a92))
|
||||
|
||||
### 📚 Documentation
|
||||
|
||||
- Update README - ([0432477](https://codeberg.org/ThetaDev/rustypipe/commit/0432477451ecd5f64145d65239c721f4e44826c0))
|
||||
- Fix README - ([11442df](https://codeberg.org/ThetaDev/rustypipe/commit/11442dfd369599396357f5b7a7a4268a7b537f57))
|
||||
|
||||
### ⚙️ Miscellaneous Tasks
|
||||
|
||||
- *(deps)* Update rust crate rstest to 0.24.0 (#20) - ([ab19034](https://codeberg.org/ThetaDev/rustypipe/commit/ab19034ab19baf090e83eada056559676ffdadce))
|
||||
- *(deps)* Update rust crate dirs to v6 (#24) - ([6a60425](https://codeberg.org/ThetaDev/rustypipe/commit/6a604252b1af7a9388db5dc170f737069cc31051))
|
||||
- Update pre-commit hooks - ([7cd9246](https://codeberg.org/ThetaDev/rustypipe/commit/7cd9246260493d7839018cb39a2dfb4dded8b343))
|
||||
|
||||
|
||||
## [v0.8.0](https://codeberg.org/ThetaDev/rustypipe/compare/rustypipe/v0.7.2..rustypipe/v0.8.0) - 2024-12-20
|
||||
|
||||
### 🚀 Features
|
||||
|
||||
- Log warning when generating report - ([258f18a](https://codeberg.org/ThetaDev/rustypipe/commit/258f18a99d848ae7e6808beddad054037a3b3799))
|
||||
- Add auto-dubbed audio tracks, improved StreamFilter - ([1d1ae17](https://codeberg.org/ThetaDev/rustypipe/commit/1d1ae17ffc16724667d43142aa57abda2e6468e4))
|
||||
|
||||
### 🐛 Bug Fixes
|
||||
|
||||
- Replace deprecated call to `time::util::days_in_year_month` - ([69ef6ae](https://codeberg.org/ThetaDev/rustypipe/commit/69ef6ae51e9b09a9b9c06057e717bf6f054c9803))
|
||||
- Nsig fn extra variable extraction - ([8014741](https://codeberg.org/ThetaDev/rustypipe/commit/80147413ee3190bb530f8f6b02738bcc787a6444))
|
||||
- Deobf function extraction, allow $ in variable names - ([8cadbc1](https://codeberg.org/ThetaDev/rustypipe/commit/8cadbc1a4c865d085e30249dba0f353472456a32))
|
||||
- Remove leading zero-width-space from comments, ensure space after links - ([162959c](https://codeberg.org/ThetaDev/rustypipe/commit/162959ca4513a03496776fae905b4bf20c79899c))
|
||||
- Update client versions, enable Opus audio with iOS client - ([1b60c97](https://codeberg.org/ThetaDev/rustypipe/commit/1b60c97a183b9d74b92df14b5b113c61aba1be7f))
|
||||
- Extract transcript from comment voice replies - ([30f60c3](https://codeberg.org/ThetaDev/rustypipe/commit/30f60c30f9d87d39585db93c1c9e274f48d688ba))
|
||||
- Error 400 when fetching player with login - ([5ce84c4](https://codeberg.org/ThetaDev/rustypipe/commit/5ce84c44a6844f692258066c83e04df875e0aa91))
|
||||
|
||||
### ⚙️ Miscellaneous Tasks
|
||||
|
||||
- Update user agent - ([53e5846](https://codeberg.org/ThetaDev/rustypipe/commit/53e5846286e8db920622152c2a0a57ddc7c41d25))
|
||||
|
||||
|
||||
## [v0.7.2](https://codeberg.org/ThetaDev/rustypipe/compare/rustypipe/v0.7.1..rustypipe/v0.7.2) - 2024-12-13
|
||||
|
||||
### 🐛 Bug Fixes
|
||||
|
||||
- Replace futures dependency with futures-util - ([5c39bf4](https://codeberg.org/ThetaDev/rustypipe/commit/5c39bf4842b13d37a4277ea5506e15c179892ce5))
|
||||
- Lifetime-related lints - ([c4feff3](https://codeberg.org/ThetaDev/rustypipe/commit/c4feff37a5989097b575c43d89c26427d92d77b9))
|
||||
- Limit retry attempts to fetch client versions and deobf data - ([44ae456](https://codeberg.org/ThetaDev/rustypipe/commit/44ae456d2c654679837da8ec44932c44b1b01195))
|
||||
- Deobfuscation function extraction - ([f5437aa](https://codeberg.org/ThetaDev/rustypipe/commit/f5437aa127b2b7c5a08839643e30ea1ec989d30b))
|
||||
|
||||
|
||||
## [v0.7.1](https://codeberg.org/ThetaDev/rustypipe/compare/rustypipe/v0.7.0..rustypipe/v0.7.1) - 2024-11-25
|
||||
|
||||
### 🐛 Bug Fixes
|
||||
|
||||
- Disable Android client - ([a846b72](https://codeberg.org/ThetaDev/rustypipe/commit/a846b729e3519e3d5e62bdf028d9b48a7f8ea2ce))
|
||||
- A/B test 18: music playlist facepile avatar model - ([6c8108c](https://codeberg.org/ThetaDev/rustypipe/commit/6c8108c94acf9ca2336381bdca7c97b24a809521))
|
||||
|
||||
### ⚙️ Miscellaneous Tasks
|
||||
|
||||
- Add docs badge to README - ([706e881](https://codeberg.org/ThetaDev/rustypipe/commit/706e88134c0e94ce7d880735e9d31b3ff531a4f9))
|
||||
|
||||
|
||||
## [v0.7.0](https://codeberg.org/ThetaDev/rustypipe/compare/rustypipe/v0.6.0..rustypipe/v0.7.0) - 2024-11-10
|
||||
|
||||
### 🚀 Features
|
||||
|
||||
- Allow searching for YTM users - ([50010b7](https://codeberg.org/ThetaDev/rustypipe/commit/50010b7b0856d3ce05fe7a9d5989e526089bc2ef))
|
||||
- [**breaking**] Replace `TrackItem::is_video` attr with TrackType enum; serde lowercase AlbumType enum for consistency - ([044094a](https://codeberg.org/ThetaDev/rustypipe/commit/044094a4b70f05c46a459fa1597e23f4224b7b0b))
|
||||
|
||||
### 🐛 Bug Fixes
|
||||
|
||||
- Fetch unlocalized player data to interpret errors correctly; regression introduced with v0.6.0 - ([0919cbd](https://codeberg.org/ThetaDev/rustypipe/commit/0919cbd0dfe28ea00610c67a694e5f319e80635f))
|
||||
- A/B test 17: channel playlists lockupViewModel - ([342119d](https://codeberg.org/ThetaDev/rustypipe/commit/342119dba6f3dc2152eef1fc9841264a9e56b9f0))
|
||||
- [**breaking**] Serde: lowercase Verification enum - ([badb3ae](https://codeberg.org/ThetaDev/rustypipe/commit/badb3aef8249315909160b8ff73df3019f07cf97))
|
||||
- Parsing videos using LockupViewModel (Music video recommendations) - ([870ff79](https://codeberg.org/ThetaDev/rustypipe/commit/870ff79ee07dfab1f4f2be3a401cd5320ed587da))
|
||||
- Parsing lockup playlists with "MIX" instead of view count - ([ac8fbc3](https://codeberg.org/ThetaDev/rustypipe/commit/ac8fbc3e679819189e2791c323975acaf1b43035))
|
||||
|
||||
### ⚙️ Miscellaneous Tasks
|
||||
|
||||
- *(deps)* Update rust crate thiserror to v2 (#16) - ([e1e1687](https://codeberg.org/ThetaDev/rustypipe/commit/e1e1687605603686ac5fd5deeb6aa8fecaf92494))
|
||||
|
||||
|
||||
## [v0.6.0](https://codeberg.org/ThetaDev/rustypipe/compare/rustypipe/v0.5.0..rustypipe/v0.6.0) - 2024-10-28
|
||||
|
||||
### 🚀 Features
|
||||
|
||||
- [**breaking**] Remove TvHtml5Embed client as it got disabled - ([9e835c8](https://codeberg.org/ThetaDev/rustypipe/commit/9e835c8f38a3dd28c65561b2f9bb7a0f530c24f1))
|
||||
- [**breaking**] Generate random visitorData, remove `RustyPipeQuery::get_context` and `YTContext<'a>` from public API - ([7c4f44d](https://codeberg.org/ThetaDev/rustypipe/commit/7c4f44d09c4d813efff9e7d1059ddacd226b9e9d))
|
||||
- Add OAuth user login to access age-restricted videos - ([1cc3f9a](https://codeberg.org/ThetaDev/rustypipe/commit/1cc3f9ad74908d33e247ba6243103bfc22540164))
|
||||
- Add user_auth_logout method - ([9e2fe61](https://codeberg.org/ThetaDev/rustypipe/commit/9e2fe61267846ce216e0c498d8fa9ee672e03cbf))
|
||||
- Revoke OAuth token when logging out - ([62f8a92](https://codeberg.org/ThetaDev/rustypipe/commit/62f8a9210c23e1f02c711a2294af8766ca6b70e2))
|
||||
|
||||
### 🐛 Bug Fixes
|
||||
|
||||
- Skip serializing empty cache entries - ([be18d89](https://codeberg.org/ThetaDev/rustypipe/commit/be18d89ea65e35ddcf0f31bea3360e5db209fb9f))
|
||||
- Fetch artist albums continuation - ([b589061](https://codeberg.org/ThetaDev/rustypipe/commit/b589061a40245637b4fe619a26892291d87d25e6))
|
||||
- Update channel order tokens - ([79a6281](https://codeberg.org/ThetaDev/rustypipe/commit/79a62816ff62d94e5c706f45b1ce5971e5e58a81))
|
||||
- Handle auth errors - ([512223f](https://codeberg.org/ThetaDev/rustypipe/commit/512223fd83fb1ba2ba7ad96ed050a70bb7ec294d))
|
||||
- Use same visitor data for fetching artist album continuations - ([7b0499f](https://codeberg.org/ThetaDev/rustypipe/commit/7b0499f6b7cbf6ac4b83695adadfebb3f30349c7))
|
||||
|
||||
### ⚙️ Miscellaneous Tasks
|
||||
|
||||
- *(deps)* Update rust crate fancy-regex to 0.14.0 (#14) - ([94194e0](https://codeberg.org/ThetaDev/rustypipe/commit/94194e019c46ca49c343086e80e8eb75c52f4bc6))
|
||||
- *(deps)* Update rust crate quick-xml to 0.37.0 (#15) - ([0662b5c](https://codeberg.org/ThetaDev/rustypipe/commit/0662b5ccfccc922b28629f11ea52c3eb35f9efd2))
|
||||
|
||||
|
||||
## [v0.5.0](https://codeberg.org/ThetaDev/rustypipe/compare/rustypipe/v0.4.0..rustypipe/v0.5.0) - 2024-10-13
|
||||
|
||||
### 🚀 Features
|
||||
|
|
46
Cargo.toml
46
Cargo.toml
|
@ -1,6 +1,6 @@
|
|||
[package]
|
||||
name = "rustypipe"
|
||||
version = "0.11.3"
|
||||
version = "0.5.0"
|
||||
rust-version = "1.67.1"
|
||||
edition.workspace = true
|
||||
authors.workspace = true
|
||||
|
@ -24,11 +24,13 @@ keywords = ["youtube", "video", "music"]
|
|||
categories = ["api-bindings", "multimedia"]
|
||||
|
||||
[workspace.dependencies]
|
||||
rquickjs = "0.9.0"
|
||||
quick-js-dtp = { version = "0.4.1", default-features = false, features = [
|
||||
"patch-dateparser",
|
||||
] }
|
||||
once_cell = "1.12.0"
|
||||
regex = "1.6.0"
|
||||
fancy-regex = "0.14.0"
|
||||
thiserror = "2.0.0"
|
||||
fancy-regex = "0.13.0"
|
||||
thiserror = "1.0.0"
|
||||
url = "2.2.0"
|
||||
reqwest = { version = "0.12.0", default-features = false }
|
||||
tokio = "1.20.4"
|
||||
|
@ -39,23 +41,20 @@ serde_with = { version = "3.0.0", default-features = false, features = [
|
|||
"macros",
|
||||
] }
|
||||
serde_plain = "1.0.0"
|
||||
sha1 = "0.10.0"
|
||||
rand = "0.9.0"
|
||||
time = { version = "0.3.37", features = [
|
||||
rand = "0.8.0"
|
||||
time = { version = "0.3.10", features = [
|
||||
"macros",
|
||||
"serde-human-readable",
|
||||
"serde-well-known",
|
||||
"local-offset",
|
||||
] }
|
||||
futures-util = "0.3.31"
|
||||
futures = "0.3.21"
|
||||
ress = "0.11.0"
|
||||
phf = "0.11.0"
|
||||
phf_codegen = "0.11.0"
|
||||
data-encoding = "2.0.0"
|
||||
base64 = "0.22.0"
|
||||
urlencoding = "2.1.0"
|
||||
quick-xml = { version = "0.37.0", features = ["serialize"] }
|
||||
quick-xml = { version = "0.36.0", features = ["serialize"] }
|
||||
tracing = { version = "0.1.0", features = ["log"] }
|
||||
localzone = "0.3.1"
|
||||
|
||||
# CLI
|
||||
indicatif = "0.17.0"
|
||||
|
@ -63,19 +62,19 @@ anyhow = "1.0"
|
|||
clap = { version = "4.0.0", features = ["derive"] }
|
||||
tracing-subscriber = { version = "0.3.17", features = ["env-filter"] }
|
||||
serde_yaml = "0.9.0"
|
||||
dirs = "6.0.0"
|
||||
dirs = "5.0.0"
|
||||
filenamify = "0.1.0"
|
||||
|
||||
# Testing
|
||||
rstest = "0.25.0"
|
||||
rstest = "0.23.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.11.3", default-features = false }
|
||||
rustypipe-downloader = { path = "./downloader", version = "0.3.1", default-features = false, features = [
|
||||
rustypipe = { path = ".", version = "0.5.0", default-features = false }
|
||||
rustypipe-downloader = { path = "./downloader", version = "0.2.1", default-features = false, features = [
|
||||
"indicatif",
|
||||
"audiotag",
|
||||
] }
|
||||
|
@ -83,8 +82,7 @@ rustypipe-downloader = { path = "./downloader", version = "0.3.1", default-featu
|
|||
[features]
|
||||
default = ["default-tls"]
|
||||
|
||||
rss = ["dep:quick-xml"]
|
||||
userdata = []
|
||||
rss = ["quick-xml"]
|
||||
|
||||
# Reqwest TLS options
|
||||
default-tls = ["reqwest/default-tls"]
|
||||
|
@ -95,27 +93,25 @@ rustls-tls-webpki-roots = ["reqwest/rustls-tls-webpki-roots"]
|
|||
rustls-tls-native-roots = ["reqwest/rustls-tls-native-roots"]
|
||||
|
||||
[dependencies]
|
||||
rquickjs.workspace = true
|
||||
quick-js-dtp.workspace = true
|
||||
once_cell.workspace = true
|
||||
regex.workspace = true
|
||||
fancy-regex.workspace = true
|
||||
thiserror.workspace = true
|
||||
url.workspace = true
|
||||
reqwest = { workspace = true, features = ["json", "gzip", "brotli"] }
|
||||
tokio = { workspace = true, features = ["macros", "time", "process"] }
|
||||
tokio = { workspace = true, features = ["macros", "time"] }
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
serde_with.workspace = true
|
||||
serde_plain.workspace = true
|
||||
sha1.workspace = true
|
||||
rand.workspace = true
|
||||
time.workspace = true
|
||||
ress.workspace = true
|
||||
phf.workspace = true
|
||||
data-encoding.workspace = true
|
||||
base64.workspace = true
|
||||
urlencoding.workspace = true
|
||||
tracing.workspace = true
|
||||
localzone.workspace = true
|
||||
quick-xml = { workspace = true, optional = true }
|
||||
|
||||
[dev-dependencies]
|
||||
|
@ -127,6 +123,6 @@ tracing-test.workspace = true
|
|||
|
||||
[package.metadata.docs.rs]
|
||||
# To build locally:
|
||||
# RUSTDOCFLAGS="--cfg docsrs" cargo +nightly doc --features rss,userdata --no-deps --open
|
||||
features = ["rss", "userdata"]
|
||||
# RUSTDOCFLAGS="--cfg docsrs" cargo +nightly doc --features rss --no-deps --open
|
||||
features = ["rss"]
|
||||
rustdoc-args = ["--cfg", "docsrs"]
|
||||
|
|
|
@ -1,26 +0,0 @@
|
|||
## Development
|
||||
|
||||
**Requirements:**
|
||||
|
||||
- Current version of stable Rust
|
||||
- [`just`](https://github.com/casey/just) task runner
|
||||
- [`nextest`](https://nexte.st) test runner
|
||||
- [`pre-commit`](https://pre-commit.com/)
|
||||
- yq (YAML processor)
|
||||
|
||||
### Tasks
|
||||
|
||||
**Testing**
|
||||
|
||||
- `just test` Run unit+integration tests
|
||||
- `just unittest` Run unit tests
|
||||
- `just testyt` Run YouTube integration tests
|
||||
- `just testintl` Run YouTube integration tests for all supported languages (this takes
|
||||
a long time and is therefore not run in CI)
|
||||
- `YT_LANG=de just testyt` Run YouTube integration tests for a specific language
|
||||
|
||||
**Tools**
|
||||
|
||||
- `just testfiles` Download missing testfiles for unit tests
|
||||
- `just report2yaml` Convert RustyPipe reports into a more readable yaml format
|
||||
(requires `yq`)
|
17
Justfile
17
Justfile
|
@ -1,19 +1,15 @@
|
|||
test:
|
||||
# cargo test --features=rss,userdata
|
||||
cargo nextest run --workspace --features=rss,userdata --no-fail-fast --retries 1 -- --skip 'user_data::'
|
||||
# cargo test --features=rss
|
||||
cargo nextest run --workspace --features=rss --no-fail-fast --failure-output final --retries 1
|
||||
|
||||
unittest:
|
||||
cargo nextest run --features=rss,userdata --no-fail-fast --lib
|
||||
cargo nextest run --features=rss --no-fail-fast --failure-output final --lib
|
||||
|
||||
testyt:
|
||||
cargo nextest run --features=rss,userdata --no-fail-fast --retries 1 --test youtube -- --skip 'user_data::'
|
||||
|
||||
testyt-cookie:
|
||||
cargo nextest run --features=rss,userdata --no-fail-fast --retries 1 --test youtube
|
||||
cargo nextest run --features=rss --no-fail-fast --failure-output final --retries 1 --test youtube
|
||||
|
||||
testyt-localized:
|
||||
YT_LANG=th cargo nextest run --features=rss,userdata --no-fail-fast --retries 1 --test youtube -- \
|
||||
--skip 'user_data::' --skip 'search_suggestion' --skip 'isrc_search_languages'
|
||||
YT_LANG=th cargo nextest run --features=rss --no-fail-fast --failure-output final --retries 1 --test youtube
|
||||
|
||||
testintl:
|
||||
#!/usr/bin/env bash
|
||||
|
@ -32,8 +28,7 @@ testintl:
|
|||
for YT_LANG in "${LANGUAGES[@]}"; do
|
||||
echo "---TESTS FOR $YT_LANG ---"
|
||||
|
||||
if YT_LANG="$YT_LANG" cargo nextest run --no-fail-fast --retries 1 --test-threads 4 --test youtube -- \
|
||||
--skip 'user_data::' --skip 'search_suggestion' --skip 'isrc_search_languages' --skip 'resolve_'; then
|
||||
if YT_LANG="$YT_LANG" cargo nextest run --no-fail-fast --failure-output final --retries 1 --test-threads 4 --test youtube -E 'not test(/^resolve/)'; then
|
||||
echo "--- $YT_LANG COMPLETED ---"
|
||||
else
|
||||
echo "--- $YT_LANG FAILED ---"
|
||||
|
|
140
README.md
140
README.md
|
@ -1,8 +1,7 @@
|
|||
# 
|
||||
|
||||
[](https://crates.io/crates/rustypipe)
|
||||
[](https://opensource.org/licenses/GPL-3.0)
|
||||
[](https://docs.rs/rustypipe)
|
||||
[](http://opensource.org/licenses/GPL-3.0)
|
||||
[](https://codeberg.org/ThetaDev/rustypipe/actions/?workflow=ci.yaml)
|
||||
|
||||
RustyPipe is a fully featured Rust client for the public YouTube / YouTube Music API
|
||||
|
@ -21,8 +20,6 @@ RustyPipe is a fully featured Rust client for the public YouTube / YouTube Music
|
|||
- **Search suggestions**
|
||||
- **Trending**
|
||||
- **URL resolver**
|
||||
- **Subscriptions**
|
||||
- **Playback history**
|
||||
|
||||
### YouTube Music
|
||||
|
||||
|
@ -36,30 +33,9 @@ RustyPipe is a fully featured Rust client for the public YouTube / YouTube Music
|
|||
- **Moods/Genres**
|
||||
- **Charts**
|
||||
- **New** (albums, music videos)
|
||||
- **Saved items**
|
||||
- **Playback history**
|
||||
|
||||
## Getting started
|
||||
|
||||
The RustyPipe library works as follows: at first you have to instantiate a RustyPipe
|
||||
client. You can either create it with default options or use the `RustyPipe::builder()`
|
||||
to customize it.
|
||||
|
||||
For fetching data you have to start with a new RustyPipe query object (`rp.query()`).
|
||||
The query object holds options for an individual query (e.g. content language or
|
||||
country). You can adjust these options with setter methods. Finally call your query
|
||||
method to fetch the data you need.
|
||||
|
||||
All query methods are async, you need the tokio runtime to execute them.
|
||||
|
||||
```rust ignore
|
||||
let rp = RustyPipe::new();
|
||||
let rp = RustyPipe::builder().storage_dir("/app/data").build().unwrap();
|
||||
let channel = rp.query().lang(Language::De).channel_videos("UCl2mFZoRqjw_ELax4Yisf6w").await.unwrap();
|
||||
```
|
||||
|
||||
Here are a few examples to get you started:
|
||||
|
||||
### Cargo.toml
|
||||
|
||||
```toml
|
||||
|
@ -181,105 +157,29 @@ Subscribers: 1780000
|
|||
...
|
||||
```
|
||||
|
||||
## Crate features
|
||||
## Development
|
||||
|
||||
Some features of RustyPipe are gated behind features to avoid compiling unneeded
|
||||
dependencies.
|
||||
**Requirements:**
|
||||
|
||||
- `rss` Fetch a channel's RSS feed, which is faster than fetching the channel page
|
||||
- `userdata` Add functions to fetch YouTube user data (watch history, subscriptions,
|
||||
music library)
|
||||
- Current version of stable Rust
|
||||
- [`just`](https://github.com/casey/just) task runner
|
||||
- [`nextest`](https://nexte.st) test runner
|
||||
- [`pre-commit`](https://pre-commit.com/)
|
||||
- yq (YAML processor)
|
||||
|
||||
You can also choose the TLS library used for making web requests using the same features
|
||||
as the reqwest crate (`default-tls`, `native-tls`, `native-tls-alpn`,
|
||||
`native-tls-vendored`, `rustls-tls-webpki-roots`, `rustls-tls-native-roots`).
|
||||
### Tasks
|
||||
|
||||
## Cache storage
|
||||
**Testing**
|
||||
|
||||
The RustyPipe cache holds the current version numbers for all clients, the JavaScript
|
||||
code used to deobfuscate video URLs and the authentication token/cookies. Never share
|
||||
the contents of the cache if you are using authentication.
|
||||
- `just test` Run unit+integration tests
|
||||
- `just unittest` Run unit tests
|
||||
- `just testyt` Run YouTube integration tests
|
||||
- `just testintl` Run YouTube integration tests for all supported languages (this takes
|
||||
a long time and is therefore not run in CI)
|
||||
- `YT_LANG=de just testyt` Run YouTube integration tests for a specific language
|
||||
|
||||
By default the cache is written to a JSON file named `rustypipe_cache.json` in the
|
||||
current working directory. This path can be changed with the `storage_dir` option of the
|
||||
RustyPipeBuilder. The RustyPipe CLI stores its cache in the userdata folder. The full
|
||||
path on Linux is `~/.local/share/rustypipe/rustypipe_cache.json`.
|
||||
**Tools**
|
||||
|
||||
You can integrate your own cache storage backend (e.g. database storage) by implementing
|
||||
the `CacheStorage` trait.
|
||||
|
||||
## Reports
|
||||
|
||||
RustyPipe has a builtin error reporting system. If a YouTube response cannot be
|
||||
deserialized or parsed, the original response data along with some request metadata is
|
||||
written to a JSON file in the folder `rustypipe_reports`, located in RustyPipe's storage
|
||||
directory (current folder by default, `~/.local/share/rustypipe` for the CLI).
|
||||
|
||||
When submitting a bug report to the RustyPipe project, you can share this report to help
|
||||
resolve the issue.
|
||||
|
||||
RustyPipe reports come in 3 severity levels:
|
||||
|
||||
- DBG (no error occurred, report creation was enabled by the `RustyPipeQuery::report`
|
||||
query option)
|
||||
- WRN (parts of the response could not be deserialized/parsed, response data may be
|
||||
incomplete)
|
||||
- ERR (entire response could not be deserialized/parsed, RustyPipe returned an error)
|
||||
|
||||
## PO tokens
|
||||
|
||||
Since August 2024 YouTube requires PO tokens to access streams from web-based clients
|
||||
(Desktop, Mobile). Otherwise streams will return a 403 error.
|
||||
|
||||
Generating PO tokens requires a simulated browser environment, which would be too large
|
||||
to include in RustyPipe directly.
|
||||
|
||||
Therefore, the PO token generation is handled by a seperate CLI application
|
||||
([rustypipe-botguard](https://codeberg.org/ThetaDev/rustypipe-botguard)) which is called
|
||||
by the RustyPipe crate. RustyPipe automatically detects the rustypipe-botguard binary if
|
||||
it is located in PATH or the current working directory. If your rustypipe-botguard
|
||||
binary is located at a different path, you can specify it with the `.botguard_bin(path)`
|
||||
option.
|
||||
|
||||
## Authentication
|
||||
|
||||
RustyPipe supports authenticating with your YouTube account to access
|
||||
age-restricted/private videos and user information. There are 2 supported authentication
|
||||
methods: OAuth and cookies.
|
||||
|
||||
To execute a query with authentication, use the `.authenticated()` query option. This
|
||||
option is enabled by default for queries that always require authentication like
|
||||
fetching user data. RustyPipe may automatically use authentication in case a video is
|
||||
age-restricted or your IP address is banned by YouTube. If you never want to use
|
||||
authentication, set the `.unauthenticated()` query option.
|
||||
|
||||
### OAuth
|
||||
|
||||
OAuth is the authentication method used by the YouTube TV client. It is more
|
||||
user-friendly than extracting cookies, however it only works with the TV client. This
|
||||
means that you can only fetch videos and not access any user data.
|
||||
|
||||
To login using OAuth, you first have to get a new device code using the
|
||||
`rp.user_auth_get_code()` function. You can then enter the code on
|
||||
<https://google.com/device> and log in with your Google account. After generating the
|
||||
code, you can call the `rp.user_auth_wait_for_login()` function which waits until the
|
||||
user has logged in and stores the authentication token in the cache.
|
||||
|
||||
### Cookies
|
||||
|
||||
Authenticating with cookies allows you to use the functionality of the YouTube/YouTube
|
||||
Music Desktop client. You can fetch your subscribed channels, playlists and your music
|
||||
collection. You can also fetch videos using the Desktop client, including private
|
||||
videos, as long as you have access to them.
|
||||
|
||||
To authenticate with cookies you have to log into YouTube in a fresh browser session
|
||||
(open Incognito/Private mode). Then extract the cookies from the developer tools or by
|
||||
using browser plugins like "Get cookies.txt LOCALLY"
|
||||
([Firefox](https://addons.mozilla.org/de/firefox/addon/get-cookies-txt-locally/))
|
||||
([Chromium](https://chromewebstore.google.com/detail/get-cookiestxt-locally/cclelndahbckbenkjhflpdbgdldlbecc)).
|
||||
Close the browser window after extracting the cookies to prevent YouTube from rotating
|
||||
the cookies.
|
||||
|
||||
You can then add the cookies to your RustyPipe client using the `user_auth_set_cookie`
|
||||
or `user_auth_set_cookie_txt` function. The cookies are stored in the cache file. To log
|
||||
out, use the function `user_auth_remove_cookie`.
|
||||
- `just testfiles` Download missing testfiles for unit tests
|
||||
- `just report2yaml` Convert RustyPipe reports into a more readable yaml format
|
||||
(requires `yq`)
|
||||
|
|
105
cli/CHANGELOG.md
105
cli/CHANGELOG.md
|
@ -3,111 +3,6 @@
|
|||
All notable changes to this project will be documented in this file.
|
||||
|
||||
|
||||
## [v0.7.2](https://codeberg.org/ThetaDev/rustypipe/compare/rustypipe-cli/v0.7.1..rustypipe-cli/v0.7.2) - 2025-03-16
|
||||
|
||||
### ⚙️ Miscellaneous Tasks
|
||||
|
||||
- *(deps)* Update rustypipe to 0.11.1
|
||||
- *(deps)* Update rustypipe-downloader to 0.3.1
|
||||
- *(deps)* Update rust crate rstest to 0.25.0 - ([9ed1306](https://codeberg.org/ThetaDev/rustypipe/commit/9ed1306f3aaeb993c409997ddfbc47499e4f4d22))
|
||||
|
||||
|
||||
## [v0.7.1](https://codeberg.org/ThetaDev/rustypipe/compare/rustypipe-cli/v0.7.0..rustypipe-cli/v0.7.1) - 2025-02-26
|
||||
|
||||
### ⚙️ Miscellaneous Tasks
|
||||
|
||||
- *(deps)* Update rustypipe to 0.11.0 - ([035c07f](https://codeberg.org/ThetaDev/rustypipe/commit/035c07f170aa293bcc626f27998c2b2b28660881))
|
||||
|
||||
|
||||
## [v0.7.0](https://codeberg.org/ThetaDev/rustypipe/compare/rustypipe-cli/v0.6.0..rustypipe-cli/v0.7.0) - 2025-02-09
|
||||
|
||||
### 🚀 Features
|
||||
|
||||
- Add support for rustypipe-botguard to get PO tokens - ([b90a252](https://codeberg.org/ThetaDev/rustypipe/commit/b90a252a5e1bf05a5294168b0ec16a73cbb88f42))
|
||||
- [**breaking**] Remove manual PO token options from downloader/cli, add new rustypipe-botguard options - ([cddb32f](https://codeberg.org/ThetaDev/rustypipe/commit/cddb32f190276265258c6ab45b3d43a8891c4b39))
|
||||
- Add session po token cache - ([b72b501](https://codeberg.org/ThetaDev/rustypipe/commit/b72b501b6dbcf4333b24cd80e7c8c61b0c21ec91))
|
||||
- Add timezone query option - ([3a2370b](https://codeberg.org/ThetaDev/rustypipe/commit/3a2370b97ca3d0f40d72d66a23295557317d29fb))
|
||||
- Add --timezone-local CLI option - ([4f2bb47](https://codeberg.org/ThetaDev/rustypipe/commit/4f2bb47ab42ae0c68a64f3b3c2831fa7850b6f56))
|
||||
- Add verbose flag - ([629b590](https://codeberg.org/ThetaDev/rustypipe/commit/629b5905da653c6fe0f3c6b5814dd2f49030e7ed))
|
||||
|
||||
### 🐛 Bug Fixes
|
||||
|
||||
- Parsing mixed-case language codes like zh-CN - ([9c73ed4](https://codeberg.org/ThetaDev/rustypipe/commit/9c73ed4b3008cb093c0fa7fd94fd9f1ba8cd3627))
|
||||
|
||||
### 🚜 Refactor
|
||||
|
||||
- [**breaking**] Add client_type field to DownloadError, rename cli option po-token-cache to pot-cache - ([594e675](https://codeberg.org/ThetaDev/rustypipe/commit/594e675b39efc5fbcdbd5e920a4d2cdee64f718e))
|
||||
- Rename rustypipe-cli binary to rustypipe - ([c1a872e](https://codeberg.org/ThetaDev/rustypipe/commit/c1a872e1c14ea0956053bd7c65f6875b1cb3bc55))
|
||||
|
||||
### 📚 Documentation
|
||||
|
||||
- Add Botguard info to README - ([9957add](https://codeberg.org/ThetaDev/rustypipe/commit/9957add2b5d6391b2c1869d2019fd7dd91b8cd41))
|
||||
|
||||
### ⚙️ Miscellaneous Tasks
|
||||
|
||||
- *(deps)* Update rustypipe to 0.10.0
|
||||
- *(deps)* Update rust crate rquickjs to 0.9.0 (#33) - ([2c8ac41](https://codeberg.org/ThetaDev/rustypipe/commit/2c8ac410aa535d83f8bcc7181f81914b13bceb77))
|
||||
|
||||
|
||||
## [v0.6.0](https://codeberg.org/ThetaDev/rustypipe/compare/rustypipe-cli/v0.5.0..rustypipe-cli/v0.6.0) - 2025-01-16
|
||||
|
||||
### 🚀 Features
|
||||
|
||||
- Add functions to fetch a user's history and subscriptions - ([14e3995](https://codeberg.org/ThetaDev/rustypipe/commit/14e399594f97a1228a8c2991a14dd8745af1beb7))
|
||||
- Add history item dates, extend timeago parser - ([320a8c2](https://codeberg.org/ThetaDev/rustypipe/commit/320a8c2c24217ad5697f0424c4f994bbbe31f3aa))
|
||||
- Add cookies.txt parser, add cookie auth + history cmds to CLI - ([cf498e4](https://codeberg.org/ThetaDev/rustypipe/commit/cf498e4a8f9318b0197bc3f0cbaf7043c53adb9d))
|
||||
- Add CLI commands to fetch user library and YTM releases/charts - ([a1b43ad](https://codeberg.org/ThetaDev/rustypipe/commit/a1b43ad70a66cfcbaba8ef302ac8699f243e56e7))
|
||||
- Export subscriptions as OPML / NewPipe JSON - ([c90d966](https://codeberg.org/ThetaDev/rustypipe/commit/c90d966b17eab24e957d980695888a459707055c))
|
||||
|
||||
### 📚 Documentation
|
||||
|
||||
- Update README - ([0432477](https://codeberg.org/ThetaDev/rustypipe/commit/0432477451ecd5f64145d65239c721f4e44826c0))
|
||||
- Fix README - ([11442df](https://codeberg.org/ThetaDev/rustypipe/commit/11442dfd369599396357f5b7a7a4268a7b537f57))
|
||||
|
||||
### ⚙️ Miscellaneous Tasks
|
||||
|
||||
- *(deps)* Update rust crate rstest to 0.24.0 (#20) - ([ab19034](https://codeberg.org/ThetaDev/rustypipe/commit/ab19034ab19baf090e83eada056559676ffdadce))
|
||||
- *(deps)* Update rust crate dirs to v6 (#24) - ([6a60425](https://codeberg.org/ThetaDev/rustypipe/commit/6a604252b1af7a9388db5dc170f737069cc31051))
|
||||
|
||||
|
||||
## [v0.5.0](https://codeberg.org/ThetaDev/rustypipe/compare/rustypipe-cli/v0.4.0..rustypipe-cli/v0.5.0) - 2024-12-20
|
||||
|
||||
### 🚀 Features
|
||||
|
||||
- Get comment replies, rich text formatting - ([dceba44](https://codeberg.org/ThetaDev/rustypipe/commit/dceba442fe1a1d5d8d2a6d9422ff699593131f6d))
|
||||
|
||||
### 🐛 Bug Fixes
|
||||
|
||||
- Replace futures dependency with futures-util - ([5c39bf4](https://codeberg.org/ThetaDev/rustypipe/commit/5c39bf4842b13d37a4277ea5506e15c179892ce5))
|
||||
- Error 400 when fetching player with login - ([5ce84c4](https://codeberg.org/ThetaDev/rustypipe/commit/5ce84c44a6844f692258066c83e04df875e0aa91))
|
||||
|
||||
### ⚙️ Miscellaneous Tasks
|
||||
|
||||
- Add docs badge to README - ([706e881](https://codeberg.org/ThetaDev/rustypipe/commit/706e88134c0e94ce7d880735e9d31b3ff531a4f9))
|
||||
- *(deps)* Update rustypipe to 0.8.0
|
||||
|
||||
|
||||
## [v0.4.0](https://codeberg.org/ThetaDev/rustypipe/compare/rustypipe-cli/v0.3.0..rustypipe-cli/v0.4.0) - 2024-11-10
|
||||
|
||||
### 🚀 Features
|
||||
|
||||
- Allow searching for YTM users - ([50010b7](https://codeberg.org/ThetaDev/rustypipe/commit/50010b7b0856d3ce05fe7a9d5989e526089bc2ef))
|
||||
- [**breaking**] Replace `TrackItem::is_video` attr with TrackType enum; serde lowercase AlbumType enum for consistency - ([044094a](https://codeberg.org/ThetaDev/rustypipe/commit/044094a4b70f05c46a459fa1597e23f4224b7b0b))
|
||||
|
||||
### ⚙️ Miscellaneous Tasks
|
||||
|
||||
- *(deps)* Update rust crate thiserror to v2 (#16) - ([e1e1687](https://codeberg.org/ThetaDev/rustypipe/commit/e1e1687605603686ac5fd5deeb6aa8fecaf92494))
|
||||
|
||||
|
||||
## [v0.3.0](https://codeberg.org/ThetaDev/rustypipe/compare/rustypipe-cli/v0.2.2..rustypipe-cli/v0.3.0) - 2024-10-28
|
||||
|
||||
### 🚀 Features
|
||||
|
||||
- [**breaking**] Remove TvHtml5Embed client as it got disabled - ([9e835c8](https://codeberg.org/ThetaDev/rustypipe/commit/9e835c8f38a3dd28c65561b2f9bb7a0f530c24f1))
|
||||
- Add OAuth user login to access age-restricted videos - ([1cc3f9a](https://codeberg.org/ThetaDev/rustypipe/commit/1cc3f9ad74908d33e247ba6243103bfc22540164))
|
||||
- Revoke OAuth token when logging out - ([62f8a92](https://codeberg.org/ThetaDev/rustypipe/commit/62f8a9210c23e1f02c711a2294af8766ca6b70e2))
|
||||
|
||||
|
||||
## [v0.2.2](https://codeberg.org/ThetaDev/rustypipe/compare/rustypipe-cli/v0.2.1..rustypipe-cli/v0.2.2) - 2024-10-13
|
||||
|
||||
### 🚀 Features
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
[package]
|
||||
name = "rustypipe-cli"
|
||||
version = "0.7.2"
|
||||
version = "0.2.2"
|
||||
rust-version = "1.70.0"
|
||||
edition.workspace = true
|
||||
authors.workspace = true
|
||||
|
@ -12,7 +12,6 @@ description = "CLI for RustyPipe - download videos and extract data from YouTube
|
|||
|
||||
[features]
|
||||
default = ["native-tls"]
|
||||
timezone = ["dep:time", "dep:time-tz"]
|
||||
|
||||
# Reqwest TLS options
|
||||
native-tls = [
|
||||
|
@ -42,16 +41,13 @@ rustls-tls-native-roots = [
|
|||
]
|
||||
|
||||
[dependencies]
|
||||
rustypipe = { workspace = true, features = ["rss", "userdata"] }
|
||||
rustypipe = { workspace = true, features = ["rss"] }
|
||||
rustypipe-downloader.workspace = true
|
||||
reqwest.workspace = true
|
||||
tokio = { workspace = true, features = ["macros", "rt-multi-thread"] }
|
||||
futures-util.workspace = true
|
||||
futures.workspace = true
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
quick-xml.workspace = true
|
||||
time = { workspace = true, optional = true }
|
||||
time-tz = { version = "2.0.0", optional = true }
|
||||
|
||||
indicatif.workspace = true
|
||||
anyhow.workspace = true
|
||||
|
@ -64,7 +60,3 @@ dirs.workspace = true
|
|||
anstream = "0.6.15"
|
||||
owo-colors = "4.0.0"
|
||||
const_format = "0.2.33"
|
||||
|
||||
[[bin]]
|
||||
name = "rustypipe"
|
||||
path = "src/main.rs"
|
||||
|
|
104
cli/README.md
104
cli/README.md
|
@ -1,26 +1,14 @@
|
|||
#  CLI
|
||||
|
||||
[](https://crates.io/crates/rustypipe-cli)
|
||||
[](https://opensource.org/licenses/GPL-3.0)
|
||||
[](http://opensource.org/licenses/GPL-3.0)
|
||||
[](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.
|
||||
|
||||
## Installation
|
||||
|
||||
You can download a compiled version of RustyPipe here:
|
||||
<https://codeberg.org/ThetaDev/rustypipe/releases>
|
||||
|
||||
Alternatively, you can compile it yourself by installing [Rust](https://rustup.rs/) and
|
||||
running `cargo install rustypipe-cli`.
|
||||
|
||||
To be able to access streams from web-based clients (Desktop, Mobile) you need to
|
||||
download [rustypipe-botguard](https://codeberg.org/ThetaDev/rustypipe-botguard/releases)
|
||||
and place the binary either in the PATH or the current working directory.
|
||||
|
||||
For downloading videos you also need to have ffmpeg installed.
|
||||
The following subcommands are included:
|
||||
|
||||
## `get`: Fetch information
|
||||
|
||||
|
@ -30,13 +18,13 @@ 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)
|
||||
- ``-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,
|
||||
- `-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)
|
||||
|
||||
|
@ -59,7 +47,7 @@ when searching YTM or individual channels.
|
|||
- `--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,
|
||||
- `-m, --music` Search YouTube Music in the given category (options: all, tracks,
|
||||
videos, artists, albums, playlists-ytm, playlists-community)
|
||||
|
||||
## `dl`: Download videos
|
||||
|
@ -78,94 +66,26 @@ videos can be downloaded in parallel for improved performance.
|
|||
- `-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
|
||||
- `-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 ID. This feature may come in
|
||||
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.
|
||||
|
||||
## `releases` Get YouTube Music new releases
|
||||
|
||||
Get a list of new albums or music videos on YouTube Music
|
||||
|
||||
**Usage:** `rustypipe releases` or `rustypipe releases --videos`
|
||||
|
||||
## `charts`: Get YouTube Music charts
|
||||
|
||||
Get a list of the most popular tracks and artists for a given country
|
||||
|
||||
**Usage:** `rustypipe charts DE`
|
||||
|
||||
## `history`: Get YouTube playback history
|
||||
|
||||
Get a list of recently played videos or tracks
|
||||
|
||||
### Options
|
||||
|
||||
- `-l`, `--limit` Limit the number of list items to fetch
|
||||
- `--search` Search the playback history (unavailable on YouTube Music)
|
||||
- `-m`, `--music` Get the YouTube Music playback history
|
||||
|
||||
## `subscriptions`: Get subscribed channels
|
||||
|
||||
You can use the RustyPipe CLI to get a list of the channels you subscribed to. With the
|
||||
`--format` flag you can export then in different formats, including OPML and NewPipe
|
||||
JSON.
|
||||
|
||||
With the `--feed` option you can output a list of the latest videos from your
|
||||
subscription feed instead.
|
||||
|
||||
### Options
|
||||
|
||||
- `-l`, `--limit` Limit the number of list items to fetch
|
||||
- `-m`, `--music` Get a list of subscribed YouTube Music artists
|
||||
- `--feed` Output YouTube Music subscription feed
|
||||
|
||||
## `playlists`, `albums`, `tracks`: Get your YouTube library
|
||||
|
||||
Fetch a list of all the items saved in your YouTube/YouTube Music profile.
|
||||
|
||||
### Options
|
||||
|
||||
- `-l`, `--limit` Limit the number of list items to fetch
|
||||
- `-m`, `--music` (only for playlists): Get your YouTube Music playlists
|
||||
|
||||
## Global options
|
||||
|
||||
- **Proxy:** RustyPipe respects the environment variables `HTTP_PROXY`, `HTTPS_PROXY`
|
||||
and `ALL_PROXY`
|
||||
- **Logging:** Enable debug logging with the `-v` (verbose) flag. If you want more
|
||||
fine-grained control, use the `RUST_LOG` environment variable.
|
||||
- **Visitor data:** A custom visitor data ID can be used with the `--vdata` flag
|
||||
- **Authentication:** Use the commands `rustypipe login` and `rustypipe login --cookie`
|
||||
to log into your Google account using either OAuth or YouTube cookies. With the
|
||||
`--auth` flag you can use authentication for any request.
|
||||
- `--lang` Change the YouTube content language
|
||||
- `--country` Change the YouTube content country
|
||||
- `--tz` Use a specific
|
||||
[timezone](https://en.wikipedia.org/wiki/List_of_tz_database_time_zones) (e.g.
|
||||
Europe/Berlin, Australia/Sydney)
|
||||
|
||||
**Note:** this requires building rustypipe-cli with the `timezone` feature
|
||||
|
||||
- `--local-tz` Use the local timezone instead of UTC
|
||||
- `--report` Generate a report on every request and store it in a `rustypipe_reports`
|
||||
folder in the current directory
|
||||
- `--cache-file` Change the RustyPipe cache file location (Default:
|
||||
`~/.local/share/rustypipe/rustypipe_cache.json`)
|
||||
- `--report-dir` Change the RustyPipe report directory location (Default:
|
||||
`~/.local/share/rustypipe/rustypipe_reports`)
|
||||
- `--botguard-bin` Use a
|
||||
[rustypipe-botguard](https://codeberg.org/ThetaDev/rustypipe-botguard) binary from the
|
||||
given path for generating PO tokens
|
||||
- `--no-botguard` Disable Botguard, only download videos using clients that dont require
|
||||
it
|
||||
- `--pot-cache` Enable caching for session-bound PO tokens
|
||||
- **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
|
||||
|
||||
|
|
782
cli/src/main.rs
782
cli/src/main.rs
File diff suppressed because it is too large
Load diff
|
@ -9,13 +9,12 @@ repository.workspace = true
|
|||
publish = false
|
||||
|
||||
[dependencies]
|
||||
rustypipe = { path = "../", features = ["userdata"] }
|
||||
rustypipe = { path = "../" }
|
||||
reqwest.workspace = true
|
||||
tokio = { workspace = true, features = ["rt-multi-thread"] }
|
||||
futures-util.workspace = true
|
||||
futures.workspace = true
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
serde_plain.workspace = true
|
||||
serde_with.workspace = true
|
||||
once_cell.workspace = true
|
||||
regex.workspace = true
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
use std::collections::BTreeMap;
|
||||
|
||||
use anyhow::{bail, Result};
|
||||
use futures_util::{stream, StreamExt};
|
||||
use futures::{stream, StreamExt};
|
||||
use indicatif::{ProgressBar, ProgressStyle};
|
||||
use num_enum::TryFromPrimitive;
|
||||
use once_cell::sync::Lazy;
|
||||
|
@ -36,19 +36,13 @@ pub enum ABTest {
|
|||
CommentsFrameworkUpdate = 14,
|
||||
ChannelShortsLockup = 15,
|
||||
PlaylistPageHeader = 16,
|
||||
ChannelPlaylistsLockup = 17,
|
||||
MusicPlaylistFacepile = 18,
|
||||
MusicAlbumGroupsReordered = 19,
|
||||
MusicContinuationItemRenderer = 20,
|
||||
AlbumRecommends = 21,
|
||||
CommandExecutorCommand = 22,
|
||||
}
|
||||
|
||||
/// List of active A/B tests that are run when none is manually specified
|
||||
const TESTS_TO_RUN: &[ABTest] = &[
|
||||
ABTest::MusicAlbumGroupsReordered,
|
||||
ABTest::AlbumRecommends,
|
||||
ABTest::CommandExecutorCommand,
|
||||
const TESTS_TO_RUN: [ABTest; 3] = [
|
||||
ABTest::ChannelPageHeader,
|
||||
ABTest::MusicPlaylistTwoColumn,
|
||||
ABTest::CommentsFrameworkUpdate,
|
||||
];
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
|
@ -97,7 +91,7 @@ pub async fn run_test(
|
|||
let rp = rp.clone();
|
||||
let pb = pb.clone();
|
||||
async move {
|
||||
let visitor_data = rp.query().get_visitor_data(true).await.unwrap();
|
||||
let visitor_data = rp.query().get_visitor_data().await.unwrap();
|
||||
let query = rp.query().visitor_data(&visitor_data);
|
||||
let is_present = match ab {
|
||||
ABTest::AttributedTextDescription => attributed_text_description(&query).await,
|
||||
|
@ -118,14 +112,6 @@ pub async fn run_test(
|
|||
ABTest::CommentsFrameworkUpdate => comments_framework_update(&query).await,
|
||||
ABTest::ChannelShortsLockup => channel_shorts_lockup(&query).await,
|
||||
ABTest::PlaylistPageHeader => playlist_page_header_renderer(&query).await,
|
||||
ABTest::ChannelPlaylistsLockup => channel_playlists_lockup(&query).await,
|
||||
ABTest::MusicPlaylistFacepile => music_playlist_facepile(&query).await,
|
||||
ABTest::MusicAlbumGroupsReordered => music_album_groups_reordered(&query).await,
|
||||
ABTest::MusicContinuationItemRenderer => {
|
||||
music_continuation_item_renderer(&query).await
|
||||
}
|
||||
ABTest::AlbumRecommends => album_recommends(&query).await,
|
||||
ABTest::CommandExecutorCommand => command_executor_command(&query).await,
|
||||
}
|
||||
.unwrap();
|
||||
pb.inc(1);
|
||||
|
@ -151,10 +137,10 @@ pub async fn run_all_tests(n: usize, concurrency: usize) -> Vec<ABTestRes> {
|
|||
let mut results = Vec::new();
|
||||
|
||||
for ab in TESTS_TO_RUN {
|
||||
let (occurrences, vd_present, vd_absent) = run_test(*ab, n, concurrency).await;
|
||||
let (occurrences, vd_present, vd_absent) = run_test(ab, n, concurrency).await;
|
||||
results.push(ABTestRes {
|
||||
id: *ab as u16,
|
||||
name: *ab,
|
||||
id: ab as u16,
|
||||
name: ab,
|
||||
tests: n,
|
||||
occurrences,
|
||||
vd_present,
|
||||
|
@ -170,7 +156,7 @@ pub async fn attributed_text_description(rp: &RustyPipeQuery) -> Result<bool> {
|
|||
content_check_ok: false,
|
||||
racy_check_ok: false,
|
||||
};
|
||||
let response_txt = rp.raw(ClientType::Desktop, "next", &q).await?;
|
||||
let response_txt = rp.raw(ClientType::Desktop, "next", &q).await.unwrap();
|
||||
|
||||
if !response_txt.contains("\"Black Mamba\"") {
|
||||
bail!("invalid response data");
|
||||
|
@ -180,7 +166,7 @@ pub async fn attributed_text_description(rp: &RustyPipeQuery) -> Result<bool> {
|
|||
}
|
||||
|
||||
pub async fn three_tab_channel_layout(rp: &RustyPipeQuery) -> Result<bool> {
|
||||
let channel = rp.channel_videos("UCR-DXc1voovS8nhAvccRZhg").await?;
|
||||
let channel = rp.channel_videos("UCR-DXc1voovS8nhAvccRZhg").await.unwrap();
|
||||
Ok(channel.has_live || channel.has_shorts)
|
||||
}
|
||||
|
||||
|
@ -247,7 +233,8 @@ pub async fn discography_page(rp: &RustyPipeQuery) -> Result<bool> {
|
|||
params: None,
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
.await
|
||||
.unwrap();
|
||||
Ok(res.contains(&format!("\"MPAD{id}\"")))
|
||||
}
|
||||
|
||||
|
@ -309,7 +296,8 @@ pub async fn channel_about_modal(rp: &RustyPipeQuery) -> Result<bool> {
|
|||
params: None,
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
.await
|
||||
.unwrap();
|
||||
Ok(!res.contains("\"EgVhYm91dPIGBAoCEgA%3D\""))
|
||||
}
|
||||
|
||||
|
@ -346,7 +334,8 @@ pub async fn music_playlist_two_column(rp: &RustyPipeQuery) -> Result<bool> {
|
|||
params: None,
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
.await
|
||||
.unwrap();
|
||||
Ok(res.contains("\"musicResponsiveHeaderRenderer\""))
|
||||
}
|
||||
|
||||
|
@ -355,7 +344,8 @@ pub async fn comments_framework_update(rp: &RustyPipeQuery) -> Result<bool> {
|
|||
"Eg0SC3dMZHBSN2d1S3k4GAYyJSIRIgt3TGRwUjdndUt5ODAAeAJCEGNvbW1lbnRzLXNlY3Rpb24%3D";
|
||||
let res = rp
|
||||
.raw(ClientType::Desktop, "next", &QCont { continuation })
|
||||
.await?;
|
||||
.await
|
||||
.unwrap();
|
||||
Ok(res.contains("\"frameworkUpdates\""))
|
||||
}
|
||||
|
||||
|
@ -370,7 +360,8 @@ pub async fn channel_shorts_lockup(rp: &RustyPipeQuery) -> Result<bool> {
|
|||
params: Some("EgZzaG9ydHPyBgUKA5oBAA%3D%3D"),
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
.await
|
||||
.unwrap();
|
||||
Ok(res.contains("\"shortsLockupViewModel\""))
|
||||
}
|
||||
|
||||
|
@ -385,96 +376,7 @@ pub async fn playlist_page_header_renderer(rp: &RustyPipeQuery) -> Result<bool>
|
|||
params: None,
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
.await
|
||||
.unwrap();
|
||||
Ok(res.contains("\"pageHeaderRenderer\""))
|
||||
}
|
||||
|
||||
pub async fn channel_playlists_lockup(rp: &RustyPipeQuery) -> Result<bool> {
|
||||
let id = "UC2DjFE7Xf11URZqWBigcVOQ";
|
||||
let res = rp
|
||||
.raw(
|
||||
ClientType::Desktop,
|
||||
"browse",
|
||||
&QBrowse {
|
||||
browse_id: id,
|
||||
params: Some("EglwbGF5bGlzdHMgAQ%3D%3D"),
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
Ok(res.contains("\"lockupViewModel\""))
|
||||
}
|
||||
|
||||
pub async fn music_playlist_facepile(rp: &RustyPipeQuery) -> Result<bool> {
|
||||
let id = "VLPL1J-6JOckZtE_P9Xx8D3b2O6w0idhuKBe";
|
||||
let res = rp
|
||||
.raw(
|
||||
ClientType::DesktopMusic,
|
||||
"browse",
|
||||
&QBrowse {
|
||||
browse_id: id,
|
||||
params: None,
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
Ok(res.contains("\"facepile\""))
|
||||
}
|
||||
|
||||
pub async fn music_album_groups_reordered(rp: &RustyPipeQuery) -> Result<bool> {
|
||||
let id = "UCOR4_bSVIXPsGa4BbCSt60Q";
|
||||
let res = rp
|
||||
.raw(
|
||||
ClientType::DesktopMusic,
|
||||
"browse",
|
||||
&QBrowse {
|
||||
browse_id: id,
|
||||
params: None,
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
Ok(res.contains("\"Singles & EPs\""))
|
||||
}
|
||||
|
||||
pub async fn music_continuation_item_renderer(rp: &RustyPipeQuery) -> Result<bool> {
|
||||
let id = "VLPLbZIPy20-1pN7mqjckepWF78ndb6ci_qi";
|
||||
let res = rp
|
||||
.raw(
|
||||
ClientType::DesktopMusic,
|
||||
"browse",
|
||||
&QBrowse {
|
||||
browse_id: id,
|
||||
params: None,
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
Ok(res.contains("\"continuationItemRenderer\""))
|
||||
}
|
||||
|
||||
pub async fn album_recommends(rp: &RustyPipeQuery) -> Result<bool> {
|
||||
let id = "MPREb_u1I69lSAe5v";
|
||||
let res = rp
|
||||
.raw(
|
||||
ClientType::DesktopMusic,
|
||||
"browse",
|
||||
&QBrowse {
|
||||
browse_id: id,
|
||||
params: None,
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
Ok(res.contains("\"musicCarouselShelfRenderer\""))
|
||||
}
|
||||
|
||||
pub async fn command_executor_command(rp: &RustyPipeQuery) -> Result<bool> {
|
||||
let id = "VLPLbZIPy20-1pN7mqjckepWF78ndb6ci_qi";
|
||||
let res = rp
|
||||
.raw(
|
||||
ClientType::Desktop,
|
||||
"browse",
|
||||
&QBrowse {
|
||||
browse_id: id,
|
||||
params: None,
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
Ok(res.contains("\"commandExecutorCommand\""))
|
||||
}
|
||||
|
|
|
@ -1,41 +1,28 @@
|
|||
use std::{collections::BTreeMap, fs::File, io::BufReader};
|
||||
|
||||
use futures_util::stream::{self, StreamExt};
|
||||
use futures::stream::{self, StreamExt};
|
||||
use path_macro::path;
|
||||
use rustypipe::{
|
||||
client::{ClientType, RustyPipe, RustyPipeQuery},
|
||||
model::AlbumType,
|
||||
param::{Language, LANGUAGES},
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_with::rust::deserialize_ignore_any;
|
||||
use serde::Deserialize;
|
||||
|
||||
use crate::{
|
||||
model::{ContentsRenderer, QBrowse, SectionList, Tab, TextRuns},
|
||||
model::{QBrowse, TextRuns},
|
||||
util::{self, DICT_DIR},
|
||||
};
|
||||
|
||||
#[derive(Debug, Copy, Clone, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
enum AlbumTypeX {
|
||||
Album,
|
||||
Ep,
|
||||
Single,
|
||||
Audiobook,
|
||||
Show,
|
||||
AlbumRow,
|
||||
SingleRow,
|
||||
}
|
||||
|
||||
pub async fn collect_album_types(concurrency: usize) {
|
||||
let json_path = path!(*DICT_DIR / "album_type_samples.json");
|
||||
|
||||
let album_types = [
|
||||
(AlbumTypeX::Album, "MPREb_nlBWQROfvjo"),
|
||||
(AlbumTypeX::Single, "MPREb_bHfHGoy7vuv"),
|
||||
(AlbumTypeX::Ep, "MPREb_u1I69lSAe5v"),
|
||||
(AlbumTypeX::Audiobook, "MPREb_gaoNzsQHedo"),
|
||||
(AlbumTypeX::Show, "MPREb_cwzk8EUwypZ"),
|
||||
(AlbumType::Album, "MPREb_nlBWQROfvjo"),
|
||||
(AlbumType::Single, "MPREb_bHfHGoy7vuv"),
|
||||
(AlbumType::Ep, "MPREb_u1I69lSAe5v"),
|
||||
(AlbumType::Audiobook, "MPREb_gaoNzsQHedo"),
|
||||
(AlbumType::Show, "MPREb_cwzk8EUwypZ"),
|
||||
];
|
||||
|
||||
let rp = RustyPipe::new();
|
||||
|
@ -45,7 +32,7 @@ pub async fn collect_album_types(concurrency: usize) {
|
|||
let rp = rp.clone();
|
||||
async move {
|
||||
let query = rp.query().lang(lang);
|
||||
let mut data: BTreeMap<AlbumTypeX, String> = BTreeMap::new();
|
||||
let mut data: BTreeMap<AlbumType, String> = BTreeMap::new();
|
||||
|
||||
for (album_type, id) in album_types {
|
||||
let atype_txt = get_album_type(&query, id).await;
|
||||
|
@ -53,22 +40,6 @@ pub async fn collect_album_types(concurrency: usize) {
|
|||
data.insert(album_type, atype_txt);
|
||||
}
|
||||
|
||||
let (albums_txt, singles_txt) = get_album_groups(&query).await;
|
||||
println!(
|
||||
"collected {}-{:?} ({})",
|
||||
lang,
|
||||
AlbumTypeX::AlbumRow,
|
||||
&albums_txt
|
||||
);
|
||||
println!(
|
||||
"collected {}-{:?} ({})",
|
||||
lang,
|
||||
AlbumTypeX::SingleRow,
|
||||
&singles_txt
|
||||
);
|
||||
data.insert(AlbumTypeX::AlbumRow, albums_txt);
|
||||
data.insert(AlbumTypeX::SingleRow, singles_txt);
|
||||
|
||||
(lang, data)
|
||||
}
|
||||
})
|
||||
|
@ -84,7 +55,7 @@ pub fn write_samples_to_dict() {
|
|||
let json_path = path!(*DICT_DIR / "album_type_samples.json");
|
||||
|
||||
let json_file = File::open(json_path).unwrap();
|
||||
let collected: BTreeMap<Language, BTreeMap<String, String>> =
|
||||
let collected: BTreeMap<Language, BTreeMap<AlbumType, String>> =
|
||||
serde_json::from_reader(BufReader::new(json_file)).unwrap();
|
||||
let mut dict = util::read_dict();
|
||||
let langs = dict.keys().copied().collect::<Vec<_>>();
|
||||
|
@ -96,12 +67,10 @@ pub fn write_samples_to_dict() {
|
|||
e_langs.push(lang);
|
||||
|
||||
for lang in &e_langs {
|
||||
collected.get(lang).unwrap().iter().for_each(|(t_str, v)| {
|
||||
let t =
|
||||
serde_plain::from_str::<AlbumType>(t_str.split('_').next().unwrap()).unwrap();
|
||||
collected.get(lang).unwrap().iter().for_each(|(t, v)| {
|
||||
dict_entry
|
||||
.album_types
|
||||
.insert(v.to_lowercase().trim().to_owned(), t);
|
||||
.insert(v.to_lowercase().trim().to_owned(), *t);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -111,19 +80,13 @@ pub fn write_samples_to_dict() {
|
|||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct AlbumData {
|
||||
contents: AlbumContents,
|
||||
header: Header,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct AlbumContents {
|
||||
two_column_browse_results_renderer: ContentsRenderer<Tab<SectionList<AlbumHeader>>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct AlbumHeader {
|
||||
music_responsive_header_renderer: HeaderRenderer,
|
||||
struct Header {
|
||||
music_detail_header_renderer: HeaderRenderer,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
|
@ -143,20 +106,8 @@ async fn get_album_type(query: &RustyPipeQuery, id: &str) -> String {
|
|||
let album = serde_json::from_str::<AlbumData>(&response_txt).unwrap();
|
||||
|
||||
album
|
||||
.contents
|
||||
.two_column_browse_results_renderer
|
||||
.contents
|
||||
.into_iter()
|
||||
.next()
|
||||
.unwrap()
|
||||
.tab_renderer
|
||||
.content
|
||||
.section_list_renderer
|
||||
.contents
|
||||
.into_iter()
|
||||
.next()
|
||||
.unwrap()
|
||||
.music_responsive_header_renderer
|
||||
.header
|
||||
.music_detail_header_renderer
|
||||
.subtitle
|
||||
.runs
|
||||
.into_iter()
|
||||
|
@ -164,84 +115,3 @@ async fn get_album_type(query: &RustyPipeQuery, id: &str) -> String {
|
|||
.unwrap()
|
||||
.text
|
||||
}
|
||||
|
||||
async fn get_album_groups(query: &RustyPipeQuery) -> (String, String) {
|
||||
let body = QBrowse {
|
||||
browse_id: "UCOR4_bSVIXPsGa4BbCSt60Q",
|
||||
params: None,
|
||||
};
|
||||
let response_txt = query
|
||||
.clone()
|
||||
.visitor_data("CgtwbzJZcS1XZWc1QSjM2JG8BjIKCgJERRIEEgAgCw%3D%3D")
|
||||
.raw(ClientType::DesktopMusic, "browse", &body)
|
||||
.await
|
||||
.unwrap();
|
||||
let artist = serde_json::from_str::<ArtistData>(&response_txt).unwrap();
|
||||
|
||||
let sections = artist
|
||||
.contents
|
||||
.single_column_browse_results_renderer
|
||||
.contents
|
||||
.into_iter()
|
||||
.next()
|
||||
.map(|c| c.tab_renderer.content.section_list_renderer.contents)
|
||||
.unwrap();
|
||||
let titles = sections
|
||||
.into_iter()
|
||||
.filter_map(|s| {
|
||||
if let ItemSection::MusicCarouselShelfRenderer(r) = s {
|
||||
r.header
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.map(|h| {
|
||||
h.music_carousel_shelf_basic_header_renderer
|
||||
.title
|
||||
.runs
|
||||
.into_iter()
|
||||
.next()
|
||||
.unwrap()
|
||||
.text
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
assert!(titles.len() >= 2, "too few sections");
|
||||
|
||||
let mut titles_it = titles.into_iter();
|
||||
(titles_it.next().unwrap(), titles_it.next().unwrap())
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct ArtistData {
|
||||
contents: ArtistDataContents,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct ArtistDataContents {
|
||||
single_column_browse_results_renderer: ContentsRenderer<Tab<SectionList<ItemSection>>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
enum ItemSection {
|
||||
MusicCarouselShelfRenderer(MusicCarouselShelf),
|
||||
#[serde(other, deserialize_with = "deserialize_ignore_any")]
|
||||
None,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct MusicCarouselShelf {
|
||||
header: Option<MusicCarouselShelfHeader>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct MusicCarouselShelfHeader {
|
||||
music_carousel_shelf_basic_header_renderer: MusicCarouselShelfHeaderRenderer,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct MusicCarouselShelfHeaderRenderer {
|
||||
title: TextRuns,
|
||||
}
|
||||
|
|
|
@ -1,130 +0,0 @@
|
|||
use std::{collections::BTreeMap, fs::File, io::BufReader};
|
||||
|
||||
use path_macro::path;
|
||||
use rustypipe::{
|
||||
client::{ClientType, RustyPipe},
|
||||
param::{Language, LANGUAGES},
|
||||
};
|
||||
use serde::Deserialize;
|
||||
use serde_with::rust::deserialize_ignore_any;
|
||||
|
||||
use crate::{
|
||||
model::{QBrowse, SectionList, TextRuns},
|
||||
util::{self, DICT_DIR},
|
||||
};
|
||||
|
||||
pub async fn collect_album_versions_titles() {
|
||||
let json_path = path!(*DICT_DIR / "other_versions_titles.json");
|
||||
let mut res = BTreeMap::new();
|
||||
|
||||
let rp = RustyPipe::new();
|
||||
|
||||
for lang in LANGUAGES {
|
||||
let query = QBrowse {
|
||||
browse_id: "MPREb_nlBWQROfvjo",
|
||||
params: None,
|
||||
};
|
||||
let raw_resp = rp
|
||||
.query()
|
||||
.lang(lang)
|
||||
.raw(ClientType::DesktopMusic, "browse", &query)
|
||||
.await
|
||||
.unwrap();
|
||||
let data = serde_json::from_str::<AlbumData>(&raw_resp).unwrap();
|
||||
let title = data
|
||||
.contents
|
||||
.two_column_browse_results_renderer
|
||||
.secondary_contents
|
||||
.section_list_renderer
|
||||
.contents
|
||||
.into_iter()
|
||||
.find_map(|x| match x {
|
||||
ItemSection::MusicCarouselShelfRenderer(music_carousel_shelf) => {
|
||||
Some(music_carousel_shelf)
|
||||
}
|
||||
ItemSection::None => None,
|
||||
})
|
||||
.expect("other versions")
|
||||
.header
|
||||
.expect("header")
|
||||
.music_carousel_shelf_basic_header_renderer
|
||||
.title
|
||||
.runs
|
||||
.into_iter()
|
||||
.next()
|
||||
.unwrap()
|
||||
.text;
|
||||
println!("{lang}: {title}");
|
||||
res.insert(lang, title);
|
||||
}
|
||||
|
||||
let file = File::create(json_path).unwrap();
|
||||
serde_json::to_writer_pretty(file, &res).unwrap();
|
||||
}
|
||||
|
||||
pub fn write_samples_to_dict() {
|
||||
let json_path = path!(*DICT_DIR / "other_versions_titles.json");
|
||||
let json_file = File::open(json_path).unwrap();
|
||||
let collected: BTreeMap<Language, String> =
|
||||
serde_json::from_reader(BufReader::new(json_file)).unwrap();
|
||||
let mut dict = util::read_dict();
|
||||
let langs = dict.keys().copied().collect::<Vec<_>>();
|
||||
|
||||
for lang in langs {
|
||||
let dict_entry = dict.entry(lang).or_default();
|
||||
|
||||
let e = collected.get(&lang).unwrap();
|
||||
assert_eq!(e, e.trim());
|
||||
dict_entry.album_versions_title = e.to_owned();
|
||||
|
||||
for lang in &dict_entry.equivalent {
|
||||
let ee = collected.get(lang).unwrap();
|
||||
if ee != e {
|
||||
panic!("equivalent lang conflict, lang: {lang}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
util::write_dict(dict);
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct AlbumData {
|
||||
contents: AlbumDataContents,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct AlbumDataContents {
|
||||
two_column_browse_results_renderer: X1,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct X1 {
|
||||
secondary_contents: SectionList<ItemSection>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
enum ItemSection {
|
||||
MusicCarouselShelfRenderer(MusicCarouselShelf),
|
||||
#[serde(other, deserialize_with = "deserialize_ignore_any")]
|
||||
None,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct MusicCarouselShelf {
|
||||
header: Option<MusicCarouselShelfHeader>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct MusicCarouselShelfHeader {
|
||||
music_carousel_shelf_basic_header_renderer: MusicCarouselShelfHeaderRenderer,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct MusicCarouselShelfHeaderRenderer {
|
||||
title: TextRuns,
|
||||
}
|
|
@ -1,110 +0,0 @@
|
|||
use std::{collections::BTreeMap, fs::File, io::BufReader};
|
||||
|
||||
use path_macro::path;
|
||||
use rustypipe::{
|
||||
client::RustyPipe,
|
||||
param::{Language, LANGUAGES},
|
||||
};
|
||||
|
||||
use crate::util::{self, DICT_DIR};
|
||||
|
||||
type CollectedDates = BTreeMap<Language, BTreeMap<String, String>>;
|
||||
|
||||
const THIS_WEEK: &str = "this_week";
|
||||
const LAST_WEEK: &str = "last_week";
|
||||
|
||||
pub async fn collect_dates_music() {
|
||||
let json_path = path!(*DICT_DIR / "history_date_samples.json");
|
||||
let rp = RustyPipe::builder()
|
||||
.storage_dir(path!(env!("CARGO_MANIFEST_DIR") / ".."))
|
||||
.build()
|
||||
.unwrap();
|
||||
|
||||
let mut res: CollectedDates = {
|
||||
let json_file = File::open(&json_path).unwrap();
|
||||
serde_json::from_reader(BufReader::new(json_file)).unwrap()
|
||||
};
|
||||
|
||||
for lang in LANGUAGES {
|
||||
println!("{lang}");
|
||||
let history = rp.query().lang(lang).music_history().await.unwrap();
|
||||
if history.items.len() < 3 {
|
||||
panic!("{lang} empty history")
|
||||
}
|
||||
|
||||
// The indexes have to be adapted before running
|
||||
let entry = res.entry(lang).or_default();
|
||||
entry.insert(
|
||||
THIS_WEEK.to_owned(),
|
||||
history.items[0].playback_date_txt.clone().unwrap(),
|
||||
);
|
||||
entry.insert(
|
||||
LAST_WEEK.to_owned(),
|
||||
history.items[18].playback_date_txt.clone().unwrap(),
|
||||
);
|
||||
}
|
||||
|
||||
let file = File::create(&json_path).unwrap();
|
||||
serde_json::to_writer_pretty(file, &res).unwrap();
|
||||
}
|
||||
|
||||
pub async fn collect_dates() {
|
||||
let json_path = path!(*DICT_DIR / "history_date_samples.json");
|
||||
let rp = RustyPipe::builder()
|
||||
.storage_dir(path!(env!("CARGO_MANIFEST_DIR") / ".."))
|
||||
.build()
|
||||
.unwrap();
|
||||
|
||||
let mut res: CollectedDates = {
|
||||
let json_file = File::open(&json_path).unwrap();
|
||||
serde_json::from_reader(BufReader::new(json_file)).unwrap()
|
||||
};
|
||||
|
||||
for lang in LANGUAGES {
|
||||
println!("{lang}");
|
||||
let history = rp.query().lang(lang).history().await.unwrap();
|
||||
if history.items.len() < 3 {
|
||||
panic!("{lang} empty history")
|
||||
}
|
||||
|
||||
let entry = res.entry(lang).or_default();
|
||||
entry.insert(
|
||||
"tuesday".to_owned(),
|
||||
history.items[0].playback_date_txt.clone().unwrap(),
|
||||
);
|
||||
entry.insert(
|
||||
"0000-01-06".to_owned(),
|
||||
history.items[1].playback_date_txt.clone().unwrap(),
|
||||
);
|
||||
entry.insert(
|
||||
"2024-12-28".to_owned(),
|
||||
history.items[15].playback_date_txt.clone().unwrap(),
|
||||
);
|
||||
}
|
||||
|
||||
let file = File::create(&json_path).unwrap();
|
||||
serde_json::to_writer_pretty(file, &res).unwrap();
|
||||
}
|
||||
|
||||
pub fn write_samples_to_dict() {
|
||||
let json_path = path!(*DICT_DIR / "history_date_samples.json");
|
||||
|
||||
let json_file = File::open(json_path).unwrap();
|
||||
let collected_dates: CollectedDates =
|
||||
serde_json::from_reader(BufReader::new(json_file)).unwrap();
|
||||
let mut dict = util::read_dict();
|
||||
let langs = dict.keys().copied().collect::<Vec<_>>();
|
||||
|
||||
for lang in langs {
|
||||
let dict_entry = dict.entry(lang).or_default();
|
||||
let cd = &collected_dates[&lang];
|
||||
dict_entry
|
||||
.timeago_nd_tokens
|
||||
.insert(util::filter_datestr(&cd[THIS_WEEK]), "0Wl".to_owned());
|
||||
dict_entry
|
||||
.timeago_nd_tokens
|
||||
.insert(util::filter_datestr(&cd[LAST_WEEK]), "1Wl".to_owned());
|
||||
}
|
||||
|
||||
util::write_dict(dict);
|
||||
}
|
|
@ -6,7 +6,7 @@ use std::{
|
|||
};
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
use futures_util::{stream, StreamExt};
|
||||
use futures::{stream, StreamExt};
|
||||
use once_cell::sync::Lazy;
|
||||
use path_macro::path;
|
||||
use regex::Regex;
|
||||
|
|
|
@ -5,7 +5,7 @@ use std::{
|
|||
io::BufReader,
|
||||
};
|
||||
|
||||
use futures_util::{stream, StreamExt};
|
||||
use futures::{stream, StreamExt};
|
||||
use ordered_hash_map::OrderedHashMap;
|
||||
use path_macro::path;
|
||||
use rustypipe::{
|
||||
|
|
|
@ -3,7 +3,7 @@ use std::{
|
|||
fs::File,
|
||||
};
|
||||
|
||||
use futures_util::{stream, StreamExt};
|
||||
use futures::{stream, StreamExt};
|
||||
use path_macro::path;
|
||||
use rustypipe::{
|
||||
client::{RustyPipe, RustyPipeQuery},
|
||||
|
|
|
@ -5,7 +5,7 @@ use std::{
|
|||
};
|
||||
|
||||
use anyhow::Result;
|
||||
use futures_util::{stream, StreamExt};
|
||||
use futures::{stream, StreamExt};
|
||||
use path_macro::path;
|
||||
use rustypipe::{
|
||||
client::{ClientType, RustyPipe, RustyPipeQuery},
|
||||
|
@ -204,6 +204,8 @@ pub fn parse_video_durations() {
|
|||
parse(&mut words, lang, dict_entry.by_char, txt, *d);
|
||||
}
|
||||
|
||||
// dbg!(&words);
|
||||
|
||||
for (k, v) in words {
|
||||
if let Some(v) = v {
|
||||
dict_entry.timeago_tokens.insert(k, v.to_string());
|
||||
|
|
|
@ -62,17 +62,6 @@ pub async fn download_testfiles() {
|
|||
music_charts().await;
|
||||
music_genres().await;
|
||||
music_genre().await;
|
||||
|
||||
// User data
|
||||
history().await;
|
||||
subscriptions().await;
|
||||
subscription_feed().await;
|
||||
|
||||
music_history().await;
|
||||
music_saved_artists().await;
|
||||
music_saved_albums().await;
|
||||
music_saved_tracks().await;
|
||||
music_saved_playlists().await;
|
||||
}
|
||||
|
||||
const CLIENT_TYPES: [ClientType; 5] = [
|
||||
|
@ -466,36 +455,6 @@ async fn trending() {
|
|||
rp.query().trending().await.unwrap();
|
||||
}
|
||||
|
||||
async fn history() {
|
||||
let json_path = path!(*TESTFILES_DIR / "userdata" / "history.json");
|
||||
if json_path.exists() {
|
||||
return;
|
||||
}
|
||||
|
||||
let rp = rp_testfile(&json_path);
|
||||
rp.query().history().await.unwrap();
|
||||
}
|
||||
|
||||
async fn subscriptions() {
|
||||
let json_path = path!(*TESTFILES_DIR / "userdata" / "subscriptions.json");
|
||||
if json_path.exists() {
|
||||
return;
|
||||
}
|
||||
|
||||
let rp = rp_testfile(&json_path);
|
||||
rp.query().subscriptions().await.unwrap();
|
||||
}
|
||||
|
||||
async fn subscription_feed() {
|
||||
let json_path = path!(*TESTFILES_DIR / "userdata" / "subscription_feed.json");
|
||||
if json_path.exists() {
|
||||
return;
|
||||
}
|
||||
|
||||
let rp = rp_testfile(&json_path);
|
||||
rp.query().subscription_feed().await.unwrap();
|
||||
}
|
||||
|
||||
async fn music_playlist() {
|
||||
for (name, id) in [
|
||||
("short", "RDCLAK5uy_kFQXdnqMaQCVx2wpUM4ZfbsGCDibZtkJk"),
|
||||
|
@ -817,53 +776,3 @@ async fn music_genre() {
|
|||
rp.query().music_genre(id).await.unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
async fn music_history() {
|
||||
let json_path = path!(*TESTFILES_DIR / "music_userdata" / "music_history.json");
|
||||
if json_path.exists() {
|
||||
return;
|
||||
}
|
||||
|
||||
let rp = rp_testfile(&json_path);
|
||||
rp.query().music_history().await.unwrap();
|
||||
}
|
||||
|
||||
async fn music_saved_artists() {
|
||||
let json_path = path!(*TESTFILES_DIR / "music_userdata" / "saved_artists.json");
|
||||
if json_path.exists() {
|
||||
return;
|
||||
}
|
||||
|
||||
let rp = rp_testfile(&json_path);
|
||||
rp.query().music_saved_artists().await.unwrap();
|
||||
}
|
||||
|
||||
async fn music_saved_albums() {
|
||||
let json_path = path!(*TESTFILES_DIR / "music_userdata" / "saved_albums.json");
|
||||
if json_path.exists() {
|
||||
return;
|
||||
}
|
||||
|
||||
let rp = rp_testfile(&json_path);
|
||||
rp.query().music_saved_albums().await.unwrap();
|
||||
}
|
||||
|
||||
async fn music_saved_tracks() {
|
||||
let json_path = path!(*TESTFILES_DIR / "music_userdata" / "saved_tracks.json");
|
||||
if json_path.exists() {
|
||||
return;
|
||||
}
|
||||
|
||||
let rp = rp_testfile(&json_path);
|
||||
rp.query().music_saved_tracks().await.unwrap();
|
||||
}
|
||||
|
||||
async fn music_saved_playlists() {
|
||||
let json_path = path!(*TESTFILES_DIR / "music_userdata" / "saved_playlists.json");
|
||||
if json_path.exists() {
|
||||
return;
|
||||
}
|
||||
|
||||
let rp = rp_testfile(&json_path);
|
||||
rp.query().music_saved_playlists().await.unwrap();
|
||||
}
|
||||
|
|
|
@ -10,7 +10,7 @@ use crate::{
|
|||
};
|
||||
|
||||
fn parse_tu(tu: &str) -> (u8, Option<TimeUnit>) {
|
||||
static TU_PATTERN: Lazy<Regex> = Lazy::new(|| Regex::new(r"^(\d*)(\w*)$").unwrap());
|
||||
static TU_PATTERN: Lazy<Regex> = Lazy::new(|| Regex::new(r"^(\d*)(\w?)$").unwrap());
|
||||
match TU_PATTERN.captures(tu) {
|
||||
Some(cap) => (
|
||||
cap.get(1).unwrap().as_str().parse().unwrap_or(1),
|
||||
|
@ -22,8 +22,6 @@ fn parse_tu(tu: &str) -> (u8, Option<TimeUnit>) {
|
|||
"W" => Some(TimeUnit::Week),
|
||||
"M" => Some(TimeUnit::Month),
|
||||
"Y" => Some(TimeUnit::Year),
|
||||
"Wl" => Some(TimeUnit::LastWeek),
|
||||
"Wd" => Some(TimeUnit::LastWeekday),
|
||||
"" => None,
|
||||
_ => panic!("invalid time unit: {tu}"),
|
||||
},
|
||||
|
@ -45,7 +43,7 @@ pub fn generate_dictionary() {
|
|||
use crate::{
|
||||
model::AlbumType,
|
||||
param::Language,
|
||||
util::timeago::{TaToken, TimeUnit},
|
||||
util::timeago::{DateCmp, TaToken, TimeUnit},
|
||||
};
|
||||
|
||||
/// Dictionary entry containing language-specific parsing information
|
||||
|
@ -57,13 +55,14 @@ pub(crate) struct Entry {
|
|||
/// Identifiers: `Y`(ear), `M`(month), `W`(eek), `D`(ay),
|
||||
/// `h`(our), `m`(inute), `s`(econd)
|
||||
pub timeago_tokens: phf::Map<&'static str, TaToken>,
|
||||
/// True if the month has to be parsed before the day
|
||||
/// Order in which to parse numeric date components. Formatted as
|
||||
/// a string of date identifiers (Y, M, D).
|
||||
///
|
||||
/// Examples:
|
||||
///
|
||||
/// - 03.01.2020 => DMY => false
|
||||
/// - 01/03/2020 => MDY => true
|
||||
pub month_before_day: bool,
|
||||
/// - 03.01.2020 => `"DMY"`
|
||||
/// - Jan 3, 2020 => `"DY"`
|
||||
pub date_order: &'static [DateCmp],
|
||||
/// Tokens for parsing month names.
|
||||
///
|
||||
/// Format: Parsed token -> Month number (starting from 1)
|
||||
|
@ -90,8 +89,6 @@ pub(crate) struct Entry {
|
|||
pub chan_prefix: &'static str,
|
||||
/// Channel name suffix on playlist pages
|
||||
pub chan_suffix: &'static str,
|
||||
/// "Other versions" title on album pages
|
||||
pub album_versions_title: &'static str,
|
||||
}
|
||||
"#;
|
||||
|
||||
|
@ -140,6 +137,13 @@ pub(crate) fn entry(lang: Language) -> Entry {
|
|||
};
|
||||
});
|
||||
|
||||
// Date order
|
||||
let mut date_order = "&[".to_owned();
|
||||
entry.date_order.chars().for_each(|c| {
|
||||
write!(date_order, "DateCmp::{c}, ").unwrap();
|
||||
});
|
||||
date_order = date_order.trim_end_matches([' ', ',']).to_owned() + "]";
|
||||
|
||||
// Number tokens
|
||||
let mut number_tokens = phf_codegen::Map::<&str>::new();
|
||||
entry.number_tokens.iter().for_each(|(txt, mag)| {
|
||||
|
@ -180,8 +184,8 @@ pub(crate) fn entry(lang: Language) -> Entry {
|
|||
.to_string()
|
||||
.replace('\n', "\n ");
|
||||
|
||||
write!(code_timeago_tokens, "{} => Entry {{\n timeago_tokens: {},\n month_before_day: {:?},\n months: {},\n timeago_nd_tokens: {},\n comma_decimal: {:?},\n number_tokens: {},\n number_nd_tokens: {},\n album_types: {},\n chan_prefix: {:?},\n chan_suffix: {:?},\n album_versions_title: {:?},\n }},\n ",
|
||||
selector, code_ta_tokens, entry.month_before_day, code_months, code_ta_nd_tokens, entry.comma_decimal, code_number_tokens, code_number_nd_tokens, code_album_types, entry.chan_prefix, entry.chan_suffix, entry.album_versions_title).unwrap();
|
||||
write!(code_timeago_tokens, "{} => Entry {{\n timeago_tokens: {},\n date_order: {},\n months: {},\n timeago_nd_tokens: {},\n comma_decimal: {:?},\n number_tokens: {},\n number_nd_tokens: {},\n album_types: {},\n chan_prefix: {:?},\n chan_suffix: {:?},\n }},\n ",
|
||||
selector, code_ta_tokens, date_order, code_months, code_ta_nd_tokens, entry.comma_decimal, code_number_tokens, code_number_nd_tokens, code_album_types, entry.chan_prefix, entry.chan_suffix).unwrap();
|
||||
}
|
||||
|
||||
code_timeago_tokens = code_timeago_tokens.trim_end().to_owned() + "\n }\n}\n";
|
||||
|
|
|
@ -2,9 +2,7 @@
|
|||
|
||||
mod abtest;
|
||||
mod collect_album_types;
|
||||
mod collect_album_versions_titles;
|
||||
mod collect_chan_prefixes;
|
||||
mod collect_history_dates;
|
||||
mod collect_large_numbers;
|
||||
mod collect_playlist_dates;
|
||||
mod collect_video_dates;
|
||||
|
@ -32,17 +30,12 @@ enum Commands {
|
|||
CollectAlbumTypes,
|
||||
CollectVideoDurations,
|
||||
CollectVideoDates,
|
||||
CollectHistoryDates,
|
||||
CollectMusicHistoryDates,
|
||||
CollectChanPrefixes,
|
||||
CollectAlbumVersionsTitles,
|
||||
ParsePlaylistDates,
|
||||
ParseHistoryDates,
|
||||
ParseLargeNumbers,
|
||||
ParseAlbumTypes,
|
||||
ParseVideoDurations,
|
||||
ParseChanPrefixes,
|
||||
ParseAlbumVersionsTitles,
|
||||
GenLocales,
|
||||
GenDict,
|
||||
DownloadTestfiles,
|
||||
|
@ -61,36 +54,31 @@ async fn main() {
|
|||
|
||||
match cli.command {
|
||||
Commands::CollectPlaylistDates => {
|
||||
collect_playlist_dates::collect_dates(cli.concurrency).await
|
||||
collect_playlist_dates::collect_dates(cli.concurrency).await;
|
||||
}
|
||||
Commands::CollectLargeNumbers => {
|
||||
collect_large_numbers::collect_large_numbers(cli.concurrency).await
|
||||
collect_large_numbers::collect_large_numbers(cli.concurrency).await;
|
||||
}
|
||||
Commands::CollectAlbumTypes => {
|
||||
collect_album_types::collect_album_types(cli.concurrency).await
|
||||
collect_album_types::collect_album_types(cli.concurrency).await;
|
||||
}
|
||||
Commands::CollectVideoDurations => {
|
||||
collect_video_durations::collect_video_durations(cli.concurrency).await
|
||||
collect_video_durations::collect_video_durations(cli.concurrency).await;
|
||||
}
|
||||
Commands::CollectVideoDates => {
|
||||
collect_video_dates::collect_video_dates(cli.concurrency).await
|
||||
collect_video_dates::collect_video_dates(cli.concurrency).await;
|
||||
}
|
||||
Commands::CollectHistoryDates => collect_history_dates::collect_dates().await,
|
||||
Commands::CollectMusicHistoryDates => collect_history_dates::collect_dates_music().await,
|
||||
Commands::CollectChanPrefixes => collect_chan_prefixes::collect_chan_prefixes().await,
|
||||
Commands::CollectAlbumVersionsTitles => {
|
||||
collect_album_versions_titles::collect_album_versions_titles().await
|
||||
Commands::CollectChanPrefixes => {
|
||||
collect_chan_prefixes::collect_chan_prefixes().await;
|
||||
}
|
||||
Commands::ParsePlaylistDates => collect_playlist_dates::write_samples_to_dict(),
|
||||
Commands::ParseHistoryDates => collect_history_dates::write_samples_to_dict(),
|
||||
Commands::ParseLargeNumbers => collect_large_numbers::write_samples_to_dict(),
|
||||
Commands::ParseAlbumTypes => collect_album_types::write_samples_to_dict(),
|
||||
Commands::ParseVideoDurations => collect_video_durations::parse_video_durations(),
|
||||
Commands::ParseChanPrefixes => collect_chan_prefixes::write_samples_to_dict(),
|
||||
Commands::ParseAlbumVersionsTitles => {
|
||||
collect_album_versions_titles::write_samples_to_dict()
|
||||
Commands::GenLocales => {
|
||||
gen_locales::generate_locales().await;
|
||||
}
|
||||
Commands::GenLocales => gen_locales::generate_locales().await,
|
||||
Commands::GenDict => gen_dictionary::generate_dictionary(),
|
||||
Commands::DownloadTestfiles => download_testfiles::download_testfiles().await,
|
||||
Commands::AbTest { id, n } => {
|
||||
|
|
|
@ -13,13 +13,6 @@ pub struct DictEntry {
|
|||
/// Should the language be parsed by character instead of by word?
|
||||
/// (e.g. Chinese/Japanese)
|
||||
pub by_char: bool,
|
||||
/// True if the month has to be parsed before the day
|
||||
///
|
||||
/// Examples:
|
||||
///
|
||||
/// - 03.01.2020 => DMY => false
|
||||
/// - 01/03/2020 => MDY => true
|
||||
pub month_before_day: bool,
|
||||
/// Tokens for parsing timeago strings.
|
||||
///
|
||||
/// Format: Parsed token -> \[Quantity\] Identifier
|
||||
|
@ -61,8 +54,6 @@ pub struct DictEntry {
|
|||
pub chan_prefix: String,
|
||||
/// Channel name suffix on playlist pages
|
||||
pub chan_suffix: String,
|
||||
/// "Other versions" title on album pages
|
||||
pub album_versions_title: String,
|
||||
}
|
||||
|
||||
/// Parsed TimeAgo string, contains amount and time unit.
|
||||
|
@ -97,8 +88,6 @@ pub enum TimeUnit {
|
|||
Week,
|
||||
Month,
|
||||
Year,
|
||||
LastWeek,
|
||||
LastWeekday,
|
||||
}
|
||||
|
||||
impl TimeUnit {
|
||||
|
@ -111,8 +100,6 @@ impl TimeUnit {
|
|||
TimeUnit::Week => "W",
|
||||
TimeUnit::Month => "M",
|
||||
TimeUnit::Year => "Y",
|
||||
TimeUnit::LastWeek => "Wl",
|
||||
TimeUnit::LastWeekday => "Wd",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -154,7 +141,7 @@ pub struct Text {
|
|||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Channel {
|
||||
pub contents: TwoColumnBrowseResults,
|
||||
pub contents: Contents,
|
||||
pub header: ChannelHeader,
|
||||
}
|
||||
|
||||
|
@ -172,7 +159,7 @@ pub struct HeaderRenderer {
|
|||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct TwoColumnBrowseResults {
|
||||
pub struct Contents {
|
||||
pub two_column_browse_results_renderer: TabsRenderer,
|
||||
}
|
||||
|
||||
|
@ -181,37 +168,24 @@ pub struct TwoColumnBrowseResults {
|
|||
#[serde(rename_all = "camelCase")]
|
||||
pub struct TabsRenderer {
|
||||
#[serde_as(as = "VecSkipError<_>")]
|
||||
pub tabs: Vec<Tab<RichGrid>>,
|
||||
pub tabs: Vec<TabRendererWrap>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ContentsRenderer<T> {
|
||||
#[serde(alias = "tabs")]
|
||||
pub contents: Vec<T>,
|
||||
pub struct TabRendererWrap {
|
||||
pub tab_renderer: TabRenderer,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Tab<T> {
|
||||
pub tab_renderer: TabRenderer<T>,
|
||||
pub struct TabRenderer {
|
||||
pub content: RichGridRendererWrap,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct TabRenderer<T> {
|
||||
pub content: T,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub(crate) struct SectionList<T> {
|
||||
pub section_list_renderer: ContentsRenderer<T>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct RichGrid {
|
||||
pub struct RichGridRendererWrap {
|
||||
pub rich_grid_renderer: RichGridRenderer,
|
||||
}
|
||||
|
||||
|
|
|
@ -77,7 +77,7 @@ pub fn filter_datestr(string: &str) -> String {
|
|||
.to_lowercase()
|
||||
.chars()
|
||||
.filter_map(|c| {
|
||||
if matches!(c, '\u{200b}' | '.' | ',') || c.is_ascii_digit() {
|
||||
if c == '\u{200b}' || c.is_ascii_digit() {
|
||||
None
|
||||
} else if c == '-' {
|
||||
Some(' ')
|
||||
|
|
|
@ -3,100 +3,6 @@
|
|||
All notable changes to this project will be documented in this file.
|
||||
|
||||
|
||||
## [v0.3.1](https://codeberg.org/ThetaDev/rustypipe/compare/rustypipe-downloader/v0.3.0..rustypipe-downloader/v0.3.1) - 2024-12-20
|
||||
|
||||
### ⚙️ Miscellaneous Tasks
|
||||
|
||||
- *(deps)* Update rustypipe to 0.11.0
|
||||
|
||||
|
||||
## [v0.3.0](https://codeberg.org/ThetaDev/rustypipe/compare/rustypipe-downloader/v0.2.7..rustypipe-downloader/v0.3.0) - 2025-02-09
|
||||
|
||||
### 🚀 Features
|
||||
|
||||
- [**breaking**] Remove manual PO token options from downloader in favor of rustypipe-botguard - ([cddb32f](https://codeberg.org/ThetaDev/rustypipe/commit/cddb32f190276265258c6ab45b3d43a8891c4b39))
|
||||
|
||||
### 🐛 Bug Fixes
|
||||
|
||||
- Ensure downloader futures are send - ([812ff4c](https://codeberg.org/ThetaDev/rustypipe/commit/812ff4c5bafffc5708a6d5066f1ebadb6d9fc958))
|
||||
- Download audio with dolby codec - ([9234005](https://codeberg.org/ThetaDev/rustypipe/commit/92340056f868007beccb64e9e26eb39abc40f7aa))
|
||||
|
||||
### 🚜 Refactor
|
||||
|
||||
- [**breaking**] Add client_type field to DownloadError, rename cli option po-token-cache to pot-cache - ([594e675](https://codeberg.org/ThetaDev/rustypipe/commit/594e675b39efc5fbcdbd5e920a4d2cdee64f718e))
|
||||
|
||||
### 📚 Documentation
|
||||
|
||||
- Add Botguard info to README - ([9957add](https://codeberg.org/ThetaDev/rustypipe/commit/9957add2b5d6391b2c1869d2019fd7dd91b8cd41))
|
||||
|
||||
### ⚙️ Miscellaneous Tasks
|
||||
|
||||
- *(deps)* Update rustypipe to 0.10.0
|
||||
|
||||
|
||||
## [v0.2.7](https://codeberg.org/ThetaDev/rustypipe/compare/rustypipe-downloader/v0.2.6..rustypipe-downloader/v0.2.7) - 2025-01-16
|
||||
|
||||
### 🚀 Features
|
||||
|
||||
- Extract player DRM data - ([2af4001](https://codeberg.org/ThetaDev/rustypipe/commit/2af4001c75f2ff4f7c891aa59ac22c2c6b7902a2))
|
||||
- Prefer maxresdefault.jpg thumbnail if available - ([a8e97f4](https://codeberg.org/ThetaDev/rustypipe/commit/a8e97f411a1e769e52d8cbde11f0a4ca1535f7ef))
|
||||
- Add DRM and audio channel number filtering to StreamFilter - ([d5abee2](https://codeberg.org/ThetaDev/rustypipe/commit/d5abee275300ab1bc10fc8d6c35a4e3813fd2bd4))
|
||||
|
||||
### 🐛 Bug Fixes
|
||||
|
||||
- Remove Unix file metadata usage (Windows compatibility) - ([5c6d992](https://codeberg.org/ThetaDev/rustypipe/commit/5c6d992939f55a203ac1784f1e9175ac1d498ce8))
|
||||
|
||||
### 📚 Documentation
|
||||
|
||||
- Update README - ([0432477](https://codeberg.org/ThetaDev/rustypipe/commit/0432477451ecd5f64145d65239c721f4e44826c0))
|
||||
- Fix README - ([11442df](https://codeberg.org/ThetaDev/rustypipe/commit/11442dfd369599396357f5b7a7a4268a7b537f57))
|
||||
|
||||
### ⚙️ Miscellaneous Tasks
|
||||
|
||||
- *(deps)* Update rustypipe to 0.9.0
|
||||
- *(deps)* Update rust crate rstest to 0.24.0 (#20) - ([ab19034](https://codeberg.org/ThetaDev/rustypipe/commit/ab19034ab19baf090e83eada056559676ffdadce))
|
||||
- *(deps)* Update rust crate lofty to 0.22.0 - ([addeb82](https://codeberg.org/ThetaDev/rustypipe/commit/addeb821101aa968b95455604bc13bd24f50328f))
|
||||
- *(deps)* Update rust crate dirs to v6 (#24) - ([6a60425](https://codeberg.org/ThetaDev/rustypipe/commit/6a604252b1af7a9388db5dc170f737069cc31051))
|
||||
|
||||
|
||||
## [v0.2.6](https://codeberg.org/ThetaDev/rustypipe/compare/rustypipe-downloader/v0.2.5..rustypipe-downloader/v0.2.6) - 2024-12-20
|
||||
|
||||
### ⚙️ Miscellaneous Tasks
|
||||
|
||||
- *(deps)* Update rustypipe to 0.8.0
|
||||
|
||||
|
||||
## [v0.2.5](https://codeberg.org/ThetaDev/rustypipe/compare/rustypipe-downloader/v0.2.4..rustypipe-downloader/v0.2.5) - 2024-12-13
|
||||
|
||||
### 🐛 Bug Fixes
|
||||
|
||||
- Replace futures dependency with futures-util - ([5c39bf4](https://codeberg.org/ThetaDev/rustypipe/commit/5c39bf4842b13d37a4277ea5506e15c179892ce5))
|
||||
- Remove empty tempfile after unsuccessful download - ([5262bec](https://codeberg.org/ThetaDev/rustypipe/commit/5262becca1e9e3e8262833764ef18c23bc401172))
|
||||
|
||||
### ⚙️ Miscellaneous Tasks
|
||||
|
||||
- Add docs badge to README - ([706e881](https://codeberg.org/ThetaDev/rustypipe/commit/706e88134c0e94ce7d880735e9d31b3ff531a4f9))
|
||||
|
||||
|
||||
## [v0.2.4](https://codeberg.org/ThetaDev/rustypipe/compare/rustypipe-downloader/v0.2.3..rustypipe-downloader/v0.2.4) - 2024-11-10
|
||||
|
||||
### ⚙️ Miscellaneous Tasks
|
||||
|
||||
- *(deps)* Update rust crate thiserror to v2 (#16) - ([e1e1687](https://codeberg.org/ThetaDev/rustypipe/commit/e1e1687605603686ac5fd5deeb6aa8fecaf92494))
|
||||
- *(deps)* Update rustypipe to 0.7.0
|
||||
|
||||
|
||||
## [v0.2.3](https://codeberg.org/ThetaDev/rustypipe/compare/rustypipe-downloader/v0.2.2..rustypipe-downloader/v0.2.3) - 2024-10-28
|
||||
|
||||
### 🐛 Bug Fixes
|
||||
|
||||
- Remove unnecessary image.rs dependencies - ([1b08166](https://codeberg.org/ThetaDev/rustypipe/commit/1b08166399cccb8394d2fdd82d54162c1a9e01be))
|
||||
|
||||
### ⚙️ Miscellaneous Tasks
|
||||
|
||||
- *(deps)* Update rustypipe to 0.6.0
|
||||
|
||||
|
||||
## [v0.2.2](https://codeberg.org/ThetaDev/rustypipe/compare/rustypipe-downloader/v0.2.1..rustypipe-downloader/v0.2.2) - 2024-10-13
|
||||
|
||||
### ⚙️ Miscellaneous Tasks
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
[package]
|
||||
name = "rustypipe-downloader"
|
||||
version = "0.3.1"
|
||||
version = "0.2.2"
|
||||
rust-version = "1.67.1"
|
||||
edition.workspace = true
|
||||
authors.workspace = true
|
||||
|
@ -37,7 +37,7 @@ rustypipe.workspace = true
|
|||
once_cell.workspace = true
|
||||
regex.workspace = true
|
||||
thiserror.workspace = true
|
||||
futures-util.workspace = true
|
||||
futures.workspace = true
|
||||
reqwest = { workspace = true, features = ["stream"] }
|
||||
rand.workspace = true
|
||||
tokio = { workspace = true, features = ["macros", "fs", "process"] }
|
||||
|
@ -45,13 +45,9 @@ indicatif = { workspace = true, optional = true }
|
|||
filenamify.workspace = true
|
||||
tracing.workspace = true
|
||||
time.workspace = true
|
||||
lofty = { version = "0.22.0", optional = true }
|
||||
image = { version = "0.25.0", optional = true, default-features = false, features = [
|
||||
"rayon",
|
||||
"jpeg",
|
||||
"webp",
|
||||
] }
|
||||
smartcrop2 = { version = "0.4.0", optional = 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
|
||||
|
|
|
@ -1,8 +1,7 @@
|
|||
#  Downloader
|
||||
|
||||
[](https://crates.io/crates/rustypipe-downloader)
|
||||
[](https://opensource.org/licenses/GPL-3.0)
|
||||
[](https://docs.rs/rustypipe-downloader)
|
||||
[](http://opensource.org/licenses/GPL-3.0)
|
||||
[](https://codeberg.org/ThetaDev/rustypipe/actions/?workflow=ci.yaml)
|
||||
|
||||
The downloader is a companion crate for RustyPipe that allows for easy and fast
|
||||
|
|
|
@ -13,13 +13,8 @@ pub enum DownloadError {
|
|||
#[error("http error: {0}")]
|
||||
Http(#[from] reqwest::Error),
|
||||
/// 403 error trying to download video
|
||||
#[error("YouTube returned 403 error; visitor_data={}", .visitor_data.as_deref().unwrap_or_default())]
|
||||
Forbidden {
|
||||
/// Client type used to fetch the failed stream
|
||||
client_type: ClientType,
|
||||
/// Visitor data used to fetch the failed stream
|
||||
visitor_data: Option<String>,
|
||||
},
|
||||
#[error("YouTube returned 403 error")]
|
||||
Forbidden(ClientType),
|
||||
/// File IO error
|
||||
#[error(transparent)]
|
||||
Io(#[from] std::io::Error),
|
||||
|
@ -30,8 +25,8 @@ pub enum DownloadError {
|
|||
#[error("Progressive download error: {0}")]
|
||||
Progressive(Cow<'static, str>),
|
||||
/// Video could not be downloaded because of invalid player data
|
||||
#[error("source error: {0}")]
|
||||
Source(Cow<'static, str>),
|
||||
#[error("input error: {0}")]
|
||||
Input(Cow<'static, str>),
|
||||
/// Download target already exists
|
||||
#[error("file {0} already exists")]
|
||||
Exists(PathBuf),
|
||||
|
|
|
@ -15,13 +15,13 @@ use std::{
|
|||
time::Duration,
|
||||
};
|
||||
|
||||
use futures_util::stream::{self, StreamExt, TryStreamExt};
|
||||
use futures::stream::{self, StreamExt};
|
||||
use once_cell::sync::Lazy;
|
||||
use rand::Rng;
|
||||
use regex::Regex;
|
||||
use reqwest::{header, Client, StatusCode, Url};
|
||||
use rustypipe::{
|
||||
client::{ClientType, RustyPipe},
|
||||
client::{ClientType, RustyPipe, DEFAULT_PLAYER_CLIENT_ORDER},
|
||||
model::{
|
||||
traits::{FileFormat, YtEntity},
|
||||
AudioCodec, TrackItem, VideoCodec, VideoPlayer,
|
||||
|
@ -77,6 +77,7 @@ pub struct DownloaderBuilder {
|
|||
#[cfg(feature = "audiotag")]
|
||||
crop_cover: bool,
|
||||
client_types: Option<Vec<ClientType>>,
|
||||
pot: Option<String>,
|
||||
}
|
||||
|
||||
struct DownloaderInner {
|
||||
|
@ -108,6 +109,8 @@ struct DownloaderInner {
|
|||
crop_cover: bool,
|
||||
/// Client types for fetching videos
|
||||
client_types: Option<Vec<ClientType>>,
|
||||
/// Pot token to circumvent bot detection
|
||||
pot: Option<String>,
|
||||
}
|
||||
|
||||
/// Download query
|
||||
|
@ -127,6 +130,8 @@ pub struct DownloadQuery {
|
|||
video_format: Option<DownloadVideoFormat>,
|
||||
/// Client types for fetching videos
|
||||
client_types: Option<Vec<ClientType>>,
|
||||
/// Pot token to circumvent bot detection
|
||||
pot: Option<String>,
|
||||
}
|
||||
|
||||
/// Video to be downloaded
|
||||
|
@ -293,6 +298,7 @@ impl Default for DownloaderBuilder {
|
|||
#[cfg(feature = "audiotag")]
|
||||
crop_cover: false,
|
||||
client_types: None,
|
||||
pot: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -411,6 +417,21 @@ impl DownloaderBuilder {
|
|||
self
|
||||
}
|
||||
|
||||
/// Set the `pot` token to circumvent bot detection
|
||||
///
|
||||
/// YouTube has implemented the token to prevent other clients from downloading YouTube videos.
|
||||
/// The token is generated using YouTube's botguard. Therefore you need a full browser environment
|
||||
/// to obtain one.
|
||||
///
|
||||
/// The Invidious project has created a script to extract this token: <https://github.com/iv-org/youtube-trusted-session-generator>
|
||||
///
|
||||
/// The `pot` token is only used for the [`ClientType::Desktop`] and [`ClientType::DesktopMusic`] clients.
|
||||
#[must_use]
|
||||
pub fn pot<S: Into<String>>(mut self, pot: S) -> Self {
|
||||
self.pot = Some(pot.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// Create a new, configured [`Downloader`] instance
|
||||
pub fn build(self) -> Downloader {
|
||||
self.build_with_client(
|
||||
|
@ -445,6 +466,7 @@ impl DownloaderBuilder {
|
|||
#[cfg(feature = "audiotag")]
|
||||
crop_cover: self.crop_cover,
|
||||
client_types: self.client_types,
|
||||
pot: self.pot,
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
@ -479,6 +501,7 @@ impl Downloader {
|
|||
filter: None,
|
||||
video_format: None,
|
||||
client_types: None,
|
||||
pot: None,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -506,7 +529,7 @@ impl Downloader {
|
|||
self.query(DownloadVideo::from_entity(video))
|
||||
}
|
||||
|
||||
/// Download a video from a [`TrackItem`] (YouTube Music album/playlist item)
|
||||
/// Download a video from a [`TrackItem`] (YouTube music album/playlist item)
|
||||
///
|
||||
/// Providing an entity has the advantage that the download path can be determined before the video
|
||||
/// is fetched, so already downloaded videos get skipped right away.
|
||||
|
@ -629,6 +652,21 @@ impl DownloadQuery {
|
|||
self
|
||||
}
|
||||
|
||||
/// Set the `pot` token to circumvent bot detection
|
||||
///
|
||||
/// YouTube has implemented the token to prevent other clients from downloading YouTube videos.
|
||||
/// The token is generated using YouTube's botguard. Therefore you need a full browser environment
|
||||
/// to obtain one.
|
||||
///
|
||||
/// The Invidious project has created a script to extract this token: <https://github.com/iv-org/youtube-trusted-session-generator>
|
||||
///
|
||||
/// The `pot` token is only used for the [`ClientType::Desktop`] and [`ClientType::DesktopMusic`] clients.
|
||||
#[must_use]
|
||||
pub fn pot<S: Into<String>>(mut self, pot: S) -> Self {
|
||||
self.pot = Some(pot.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// Download the video
|
||||
///
|
||||
/// If no download path is set, the video is downloaded to the current directory
|
||||
|
@ -660,15 +698,9 @@ impl DownloadQuery {
|
|||
.await
|
||||
{
|
||||
Ok(res) => return Ok(res),
|
||||
Err(DownloadError::Forbidden {
|
||||
client_type,
|
||||
visitor_data,
|
||||
}) => {
|
||||
failed_client = Some(client_type);
|
||||
DownloadError::Forbidden {
|
||||
client_type,
|
||||
visitor_data,
|
||||
}
|
||||
Err(DownloadError::Forbidden(c)) => {
|
||||
failed_client = Some(c);
|
||||
DownloadError::Forbidden(c)
|
||||
}
|
||||
Err(DownloadError::Http(e)) => {
|
||||
if !e.is_timeout() {
|
||||
|
@ -738,7 +770,7 @@ impl DownloadQuery {
|
|||
.as_ref()
|
||||
.or(self.dl.i.client_types.as_ref())
|
||||
.map(Vec::as_slice)
|
||||
.unwrap_or(q.player_client_order()),
|
||||
.unwrap_or(DEFAULT_PLAYER_CLIENT_ORDER),
|
||||
);
|
||||
|
||||
// If the last download failed, try another client if possible
|
||||
|
@ -755,15 +787,20 @@ impl DownloadQuery {
|
|||
|
||||
let player_data = q.player_from_clients(&self.video.id, &client_types).await?;
|
||||
let user_agent = q.user_agent(player_data.client_type);
|
||||
let pot = if matches!(
|
||||
player_data.client_type,
|
||||
ClientType::Desktop | ClientType::DesktopMusic
|
||||
) {
|
||||
self.pot.as_deref().or(self.dl.i.pot.as_deref())
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
// Select streams to download
|
||||
let (video, audio) = player_data.select_video_audio_stream(filter);
|
||||
|
||||
if video.is_none() && audio.is_none() {
|
||||
if player_data.drm.is_some() {
|
||||
return Err(DownloadError::Source("video is DRM-protected".into()));
|
||||
}
|
||||
return Err(DownloadError::Source("no stream found".into()));
|
||||
return Err(DownloadError::Input("no stream found".into()));
|
||||
}
|
||||
|
||||
let extension = match video {
|
||||
|
@ -772,9 +809,7 @@ impl DownloadQuery {
|
|||
Some(audio) => match audio.codec {
|
||||
AudioCodec::Mp4a => "m4a",
|
||||
AudioCodec::Opus => "opus",
|
||||
AudioCodec::Ac3 => "ac3",
|
||||
AudioCodec::Ec3 => "eac3",
|
||||
_ => return Err(DownloadError::Source("unknown audio codec".into())),
|
||||
_ => return Err(DownloadError::Input("unknown audio codec".into())),
|
||||
},
|
||||
None => unreachable!(),
|
||||
},
|
||||
|
@ -833,10 +868,11 @@ impl DownloadQuery {
|
|||
if let Some(pb) = pb {
|
||||
pb.set_message(format!("Downloading {name}{attempt_suffix}"))
|
||||
}
|
||||
let downloads = download_streams(
|
||||
downloads,
|
||||
download_streams(
|
||||
&downloads,
|
||||
&self.dl.i.http,
|
||||
&user_agent,
|
||||
pot,
|
||||
#[cfg(feature = "indicatif")]
|
||||
pb.clone(),
|
||||
)
|
||||
|
@ -844,14 +880,7 @@ impl DownloadQuery {
|
|||
.map_err(|e| {
|
||||
if let DownloadError::Http(e) = &e {
|
||||
if e.status() == Some(StatusCode::FORBIDDEN) {
|
||||
// 403 errors may occur due to bad visitor data IDs
|
||||
if let Some(vd) = &player_data.visitor_data {
|
||||
q.remove_visitor_data(vd);
|
||||
}
|
||||
return DownloadError::Forbidden {
|
||||
client_type: player_data.client_type,
|
||||
visitor_data: player_data.visitor_data.clone(),
|
||||
};
|
||||
return DownloadError::Forbidden(player_data.client_type);
|
||||
}
|
||||
}
|
||||
e
|
||||
|
@ -871,7 +900,7 @@ impl DownloadQuery {
|
|||
|
||||
// Tag audio file
|
||||
#[cfg(feature = "audiotag")]
|
||||
if self.dl.i.audio_tag && video.is_none() && matches!(extension, "m4a" | "opus") {
|
||||
if self.dl.i.audio_tag && video.is_none() {
|
||||
let (details, track) = match details {
|
||||
Some(d) => (d, self.dl.i.rp.query().music_details(&self.video.id).await?),
|
||||
None => {
|
||||
|
@ -898,9 +927,13 @@ impl DownloadQuery {
|
|||
}
|
||||
|
||||
// Delete original files
|
||||
for d in &downloads {
|
||||
fs::remove_file(&d.file).await?;
|
||||
}
|
||||
stream::iter(&downloads)
|
||||
.map(|d| fs::remove_file(d.file.clone()))
|
||||
.buffer_unordered(downloads.len())
|
||||
.collect::<Vec<_>>()
|
||||
.await
|
||||
.into_iter()
|
||||
.collect::<core::result::Result<(), _>>()?;
|
||||
|
||||
#[cfg(feature = "indicatif")]
|
||||
if let Some(pb) = pb {
|
||||
|
@ -986,37 +1019,14 @@ impl DownloadQuery {
|
|||
};
|
||||
|
||||
if let Some(thumbnail) = thumbnail {
|
||||
// Attempt to get the higher resolution, uncropped maxresdefault.jpg thumbnail if available
|
||||
let mut resp = None;
|
||||
if thumbnail.height != thumbnail.width {
|
||||
if let Ok(x) = self
|
||||
.dl
|
||||
.i
|
||||
.http
|
||||
.get(format!(
|
||||
"https://i.ytimg.com/vi/{}/maxresdefault.jpg",
|
||||
track.id
|
||||
))
|
||||
.send()
|
||||
.await?
|
||||
.error_for_status()
|
||||
{
|
||||
resp = Some(x);
|
||||
}
|
||||
}
|
||||
|
||||
let resp = match resp {
|
||||
Some(resp) => resp,
|
||||
None => self
|
||||
.dl
|
||||
.i
|
||||
.http
|
||||
.get(thumbnail.url)
|
||||
.send()
|
||||
.await?
|
||||
.error_for_status()?,
|
||||
};
|
||||
|
||||
let resp = self
|
||||
.dl
|
||||
.i
|
||||
.http
|
||||
.get(thumbnail.url)
|
||||
.send()
|
||||
.await?
|
||||
.error_for_status()?;
|
||||
let img_type = resp
|
||||
.headers()
|
||||
.get(header::CONTENT_TYPE)
|
||||
|
@ -1033,13 +1043,9 @@ impl DownloadQuery {
|
|||
image::load_from_memory(&img_bts)?
|
||||
};
|
||||
|
||||
let crop = smartcrop::find_best_crop_no_borders(
|
||||
&img,
|
||||
NonZeroU32::MIN,
|
||||
NonZeroU32::MIN,
|
||||
)
|
||||
.map_err(|e| DownloadError::AudioTag(format!("image crop: {e}").into()))?
|
||||
.crop;
|
||||
let crop = smartcrop::find_best_crop(&img, NonZeroU32::MIN, NonZeroU32::MIN)
|
||||
.map_err(|e| DownloadError::AudioTag(format!("image crop: {e}").into()))?
|
||||
.crop;
|
||||
img = img.crop_imm(crop.x, crop.y, crop.width, crop.height);
|
||||
let mut enc_bts = Vec::new();
|
||||
img.write_with_encoder(image::codecs::jpeg::JpegEncoder::new_with_quality(
|
||||
|
@ -1067,8 +1073,8 @@ impl DownloadQuery {
|
|||
}
|
||||
|
||||
fn get_download_range(offset: u64, size: Option<u64>) -> Range<u64> {
|
||||
let mut rng = rand::rng();
|
||||
let chunk_size = rng.random_range(CHUNK_SIZE_MIN..CHUNK_SIZE_MAX);
|
||||
let mut rng = rand::thread_rng();
|
||||
let chunk_size = rng.gen_range(CHUNK_SIZE_MIN..CHUNK_SIZE_MAX);
|
||||
let mut chunk_end = offset + chunk_size;
|
||||
|
||||
if let Some(size) = size {
|
||||
|
@ -1119,6 +1125,7 @@ async fn download_single_file(
|
|||
output: &Path,
|
||||
http: &Client,
|
||||
user_agent: &str,
|
||||
pot: Option<&str>,
|
||||
#[cfg(feature = "indicatif")] pb: Option<ProgressBar>,
|
||||
) -> Result<()> {
|
||||
// Check if file is already downloaded
|
||||
|
@ -1201,15 +1208,13 @@ async fn download_single_file(
|
|||
}
|
||||
}
|
||||
|
||||
tracing::debug!("downloading {} to {}", url, output.to_string_lossy());
|
||||
|
||||
let mut file = fs::OpenOptions::new()
|
||||
.append(true)
|
||||
.create(true)
|
||||
.open(&output_path_tmp)
|
||||
.await?;
|
||||
|
||||
let res = if is_gvideo && size.is_some() {
|
||||
if is_gvideo && size.is_some() {
|
||||
download_chunks_by_param(
|
||||
http,
|
||||
&mut file,
|
||||
|
@ -1217,10 +1222,11 @@ async fn download_single_file(
|
|||
size.unwrap(),
|
||||
offset,
|
||||
user_agent,
|
||||
pot,
|
||||
#[cfg(feature = "indicatif")]
|
||||
pb,
|
||||
)
|
||||
.await
|
||||
.await?;
|
||||
} else {
|
||||
download_chunks_by_header(
|
||||
http,
|
||||
|
@ -1232,19 +1238,7 @@ async fn download_single_file(
|
|||
#[cfg(feature = "indicatif")]
|
||||
pb,
|
||||
)
|
||||
.await
|
||||
};
|
||||
|
||||
drop(file);
|
||||
if let Err(e) = res {
|
||||
// Remove temporary file if nothing was downloaded (e.g. 403 error)
|
||||
if std::fs::metadata(&output_path_tmp)
|
||||
.map(|md| md.len() == 0)
|
||||
.unwrap_or_default()
|
||||
{
|
||||
_ = std::fs::remove_file(&output_path_tmp);
|
||||
}
|
||||
return Err(e);
|
||||
.await?;
|
||||
}
|
||||
|
||||
fs::rename(&output_path_tmp, &output_path).await?;
|
||||
|
@ -1344,6 +1338,7 @@ async fn download_chunks_by_param(
|
|||
size: u64,
|
||||
offset: u64,
|
||||
user_agent: &str,
|
||||
pot: Option<&str>,
|
||||
#[cfg(feature = "indicatif")] pb: Option<ProgressBar>,
|
||||
) -> Result<()> {
|
||||
let mut offset = offset;
|
||||
|
@ -1356,9 +1351,12 @@ async fn download_chunks_by_param(
|
|||
let range = get_download_range(offset, Some(size));
|
||||
tracing::debug!("Fetching range {}-{}", range.start, range.end);
|
||||
|
||||
let urlp =
|
||||
let mut urlp =
|
||||
Url::parse_with_params(url, [("range", &format!("{}-{}", range.start, range.end))])
|
||||
.map_err(|e| DownloadError::Progressive(format!("url parsing: {e}").into()))?;
|
||||
if let Some(pot) = pot {
|
||||
urlp.query_pairs_mut().append_pair("pot", pot);
|
||||
}
|
||||
|
||||
let res = http
|
||||
.get(urlp)
|
||||
|
@ -1376,6 +1374,7 @@ async fn download_chunks_by_param(
|
|||
));
|
||||
}
|
||||
|
||||
tracing::debug!("Retrieving chunks...");
|
||||
let mut stream = res.bytes_stream();
|
||||
while let Some(item) = stream.next().await {
|
||||
// Retrieve chunk.
|
||||
|
@ -1405,30 +1404,33 @@ struct StreamDownload {
|
|||
}
|
||||
|
||||
async fn download_streams(
|
||||
downloads: Vec<StreamDownload>,
|
||||
downloads: &Vec<StreamDownload>,
|
||||
http: &Client,
|
||||
user_agent: &str,
|
||||
pot: Option<&str>,
|
||||
#[cfg(feature = "indicatif")] pb: Option<ProgressBar>,
|
||||
) -> Result<Vec<StreamDownload>> {
|
||||
stream::iter(downloads.iter().map(Ok))
|
||||
.try_for_each_concurrent(2, |d| {
|
||||
#[cfg(feature = "indicatif")]
|
||||
let pb = pb.clone();
|
||||
async move {
|
||||
download_single_file(
|
||||
&d.url,
|
||||
&d.file,
|
||||
http,
|
||||
user_agent,
|
||||
#[cfg(feature = "indicatif")]
|
||||
pb,
|
||||
)
|
||||
.await
|
||||
}
|
||||
})
|
||||
.await?;
|
||||
) -> Result<()> {
|
||||
let n = downloads.len();
|
||||
|
||||
Ok(downloads)
|
||||
stream::iter(downloads)
|
||||
.map(|d| {
|
||||
download_single_file(
|
||||
&d.url,
|
||||
&d.file,
|
||||
http,
|
||||
user_agent,
|
||||
pot,
|
||||
#[cfg(feature = "indicatif")]
|
||||
pb.clone(),
|
||||
)
|
||||
})
|
||||
.buffer_unordered(n)
|
||||
.collect::<Vec<_>>()
|
||||
.await
|
||||
.into_iter()
|
||||
.collect::<Result<Vec<_>>>()?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn convert_streams(
|
||||
|
|
|
@ -47,13 +47,11 @@ async fn download_music(rp: RustyPipe) {
|
|||
let td = TempDir::default();
|
||||
let td_path = td.to_path_buf();
|
||||
|
||||
#[allow(unused_mut)]
|
||||
let mut dl = Downloader::builder().rustypipe(&rp);
|
||||
#[cfg(feature = "audiotag")]
|
||||
{
|
||||
dl = dl.audio_tag().crop_cover();
|
||||
}
|
||||
let dl = dl.build();
|
||||
let dl = Downloader::builder()
|
||||
.audio_tag()
|
||||
.crop_cover()
|
||||
.rustypipe(&rp)
|
||||
.build();
|
||||
|
||||
let res = dl
|
||||
.id("bVtv3st8bgc")
|
||||
|
@ -77,7 +75,7 @@ async fn download_music(rp: RustyPipe) {
|
|||
assert_audio_meta(
|
||||
&res.dest,
|
||||
"Lord of the Riffs",
|
||||
"Alexander Nakarada",
|
||||
"Alexander Nakarada - CreatorChords",
|
||||
"Lord of the Riffs",
|
||||
"2022-02-05",
|
||||
);
|
||||
|
@ -113,15 +111,3 @@ fn assert_audio_meta(p: &Path, title: &str, artist: &str, album: &str, date: &st
|
|||
assert_eq!(tags["ALBUM"].as_str(), Some(album));
|
||||
assert_eq!(tags["DATE"].as_str(), Some(date));
|
||||
}
|
||||
|
||||
/// This is just a static check to make sure all RustyPipe futures can be sent
|
||||
/// between threads safely.
|
||||
/// Otherwise this may cause issues when integrating RustyPipe into async projects.
|
||||
#[allow(unused)]
|
||||
async fn all_send_and_sync() {
|
||||
fn send_and_sync<T: Send + Sync>(t: T) {}
|
||||
|
||||
let dl = Downloader::default();
|
||||
let dlq = dl.id("");
|
||||
send_and_sync(dlq.download());
|
||||
}
|
||||
|
|
|
@ -3,13 +3,13 @@
|
|||
When YouTube introduces a new feature, it does so gradually. When a user creates a new
|
||||
session, YouTube decided randomly which new features should be enabled.
|
||||
|
||||
YouTube sessions are identified by the visitor data ID. This cookie is sent with every
|
||||
API request using the `context.client.visitor_data` JSON parameter. It is also returned
|
||||
in the `responseContext.visitorData` response parameter and stored as the `__SECURE-YEC`
|
||||
cookie.
|
||||
YouTube sessions are identified by the visitor data cookie. This cookie is sent with
|
||||
every API request using the `context.client.visitor_data` JSON parameter. It is also
|
||||
returned in the `responseContext.visitorData` response parameter and stored as the
|
||||
`__SECURE-YEC` cookie.
|
||||
|
||||
By sending the same visitor data ID, A/B tests can be reproduced, which is important for
|
||||
testing alternative YouTube clients.
|
||||
By sending the same visitor data cookie, A/B tests can be reproduced, which is important
|
||||
for testing alternative YouTube clients.
|
||||
|
||||
This page lists all A/B tests that were encountered while maintaining the RustyPipe
|
||||
client.
|
||||
|
@ -381,7 +381,7 @@ YouTube also changed the way the full discography page is fetched, surprisingly
|
|||
it easier for alternative clients. The discography page now has its own content ID in
|
||||
the format of `MPAD<channel id>` (Music Page Artist Discography). This page can be
|
||||
fetched with a regular browse request without requiring parameters to be parsed or a
|
||||
visitor data ID to be set, as it was the case with the old system.
|
||||
visitor data cookie to be set, as it was the case with the old system.
|
||||
|
||||
**OLD**
|
||||
|
||||
|
@ -793,7 +793,7 @@ YouTube changed the data model for the channel shorts tab
|
|||
- **Encountered on:** 11.10.2024
|
||||
- **Impact:** 🟢 Low
|
||||
- **Endpoint:** browse
|
||||
- **Status:** Stabilized
|
||||
- **Status:** Common (99%)
|
||||
|
||||
```json
|
||||
{
|
||||
|
@ -899,205 +899,3 @@ YouTube changed the data model for the channel shorts tab
|
|||
}
|
||||
}
|
||||
```
|
||||
|
||||
## [17] Channel playlists: lockupViewModel
|
||||
|
||||
- **Encountered on:** 09.11.2024
|
||||
- **Impact:** 🟢 Low
|
||||
- **Endpoint:** browse
|
||||
- **Status:** Stabilized
|
||||
|
||||
YouTube changed the data model for the channel playlists / podcasts / albums tab
|
||||
|
||||
```json
|
||||
{
|
||||
"lockupViewModel": {
|
||||
"contentImage": {
|
||||
"collectionThumbnailViewModel": {
|
||||
"primaryThumbnail": {
|
||||
"thumbnailViewModel": {
|
||||
"image": {
|
||||
"sources": [
|
||||
{
|
||||
"url": "https://i.ytimg.com/vi/XYdmX8w8xwI/hqdefault.jpg?sqp=-oaymwEXCOADEI4CSFryq4qpAwkIARUAAIhCGAE=&rs=AOn4CLCqmf6TGfDinNXhgU29ZxOkv2u9sQ",
|
||||
"width": 480,
|
||||
"height": 270
|
||||
}
|
||||
]
|
||||
},
|
||||
"overlays": [
|
||||
{
|
||||
"thumbnailOverlayBadgeViewModel": {
|
||||
"thumbnailBadges": [
|
||||
{
|
||||
"thumbnailBadgeViewModel": {
|
||||
"icon": {
|
||||
"sources": [
|
||||
{
|
||||
"clientResource": {
|
||||
"imageName": "PLAYLISTS"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"text": "5 videos",
|
||||
"badgeStyle": "THUMBNAIL_OVERLAY_BADGE_STYLE_DEFAULT",
|
||||
"backgroundColor": {
|
||||
"lightTheme": 2370867,
|
||||
"darkTheme": 2370867
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"position": "THUMBNAIL_OVERLAY_BADGE_POSITION_BOTTOM_END"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"metadata": {
|
||||
"lockupMetadataViewModel": {
|
||||
"title": {
|
||||
"content": "Jellybean Components Series"
|
||||
}
|
||||
}
|
||||
},
|
||||
"contentId": "PLvOlSehNtuHv268f0mW5m1t_hq_RVGRSA",
|
||||
"contentType": "LOCKUP_CONTENT_TYPE_PLAYLIST"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## [18] Music playlists facepile avatar
|
||||
|
||||
- **Encountered on:** 25.11.2024
|
||||
- **Impact:** 🟢 Low
|
||||
- **Endpoint:** browse (YTM)
|
||||
- **Status:** Stabilized
|
||||
|
||||
YouTube changed the data model for the channel playlist owner avatar into a `facepile`
|
||||
object. It now also contains the channel avatar.
|
||||
|
||||
The model is also used for playlists owned by YouTube Music (with the avatar and
|
||||
commandContext missing).
|
||||
|
||||
```json
|
||||
{
|
||||
"facepile": {
|
||||
"avatarStackViewModel": {
|
||||
"avatars": [
|
||||
{
|
||||
"avatarViewModel": {
|
||||
"image": {
|
||||
"sources": [
|
||||
{
|
||||
"url": "https://yt3.ggpht.com/ytc/AIdro_n9ALaLETwQH6_2WlXitIaIKV-IqBDWWquvyI2jucNAZaQ=s48-c-k-c0x00000000-no-cc-rj-rp"
|
||||
}
|
||||
]
|
||||
},
|
||||
"avatarImageSize": "AVATAR_SIZE_XS"
|
||||
}
|
||||
}
|
||||
],
|
||||
"text": {
|
||||
"content": "Chaosflo44"
|
||||
},
|
||||
"rendererContext": {
|
||||
"commandContext": {
|
||||
"onTap": {
|
||||
"innertubeCommand": {
|
||||
"browseEndpoint": {
|
||||
"browseId": "UCQM0bS4_04-Y4JuYrgmnpZQ",
|
||||
"browseEndpointContextSupportedConfigs": {
|
||||
"browseEndpointContextMusicConfig": {
|
||||
"pageType": "MUSIC_PAGE_TYPE_USER_CHANNEL"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## [19] Music artist album groups reordered
|
||||
|
||||
- **Encountered on:** 13.01.2025
|
||||
- **Impact:** 🟢 Low
|
||||
- **Endpoint:** browse (YTM)
|
||||
- **Status:** Frequent (59%)
|
||||
|
||||
YouTube Music used to group artist albums into 2 rows: "Albums" and "Singles".
|
||||
|
||||
These groups were changed into "Albums" and "Singles & EPs". Now the "Album" label is
|
||||
omitted for albums in their group, while singles and EPs have a label with their type.
|
||||
|
||||
## [20] Music continuation item renderer
|
||||
|
||||
- **Encountered on:** 25.01.2025
|
||||
- **Impact:** 🟢 Low
|
||||
- **Endpoint:** browse (YTM)
|
||||
- **Status:** Stabilized
|
||||
|
||||
YouTube Music now uses a `continuationItemRenderer` for music playlists instead of
|
||||
putting the continuations in a separate attribute of the MusicShelf.
|
||||
|
||||
The continuation response now uses a `onResponseReceivedActions` field for its music
|
||||
items.
|
||||
|
||||
YouTube Music now also sends a random 16-character string as a `clientScreenNonce` in
|
||||
the request context. This is not mandatory though.
|
||||
|
||||
## [21] Music album recommendations
|
||||
|
||||
- **Encountered on:** 26.02.2025
|
||||
- **Impact:** 🟢 Low
|
||||
- **Endpoint:** browse (YTM)
|
||||
- **Status:** Common (15%)
|
||||
|
||||

|
||||
|
||||
YouTube Music has added "Recommended" and "More from \<Artist\>" carousels to album
|
||||
pages. The difficulty is distinguishing them reliably for parsing the album variants.
|
||||
|
||||
The current solution is adding the "Other versions" title in all languages to the
|
||||
dictionary and comparing it.
|
||||
|
||||
## [22] commandExecutorCommand for continuations
|
||||
|
||||
- **Encountered on:** 16.03.2025
|
||||
- **Impact:** 🟢 Low
|
||||
- **Endpoint:** browse (YTM)
|
||||
- **Status:** Experimental (1%)
|
||||
|
||||
YouTube playlists may use a commandExecutorCommand which holds a list of commands: the
|
||||
`continuationCommand` that needs to be extracted as well as a `playlistVotingRefreshPopupCommand`.
|
||||
|
||||
```json
|
||||
{
|
||||
"continuationItemRenderer": {
|
||||
"continuationEndpoint": {
|
||||
"commandExecutorCommand": {
|
||||
"commands": [
|
||||
{
|
||||
"playlistVotingRefreshPopupCommand": {
|
||||
"command": {}
|
||||
}
|
||||
},
|
||||
{
|
||||
"continuationCommand": {
|
||||
"request": "CONTINUATION_REQUEST_TYPE_BROWSE",
|
||||
"token": "4qmFsgKBARIkVkxQTGJaSVB5MjAtMXBON21xamNrZXBXRjc4bmRiNmNpX3FpGjRDQUY2SGxCVU9rTklTV2xGUkVreVVtdEZOVTVFU1hsU2FrWkRVa1JKZWs1NldRJTNEJTNEmgIiUExiWklQeTIwLTFwTjdtcWpja2VwV0Y3OG5kYjZjaV9xaQ%3D%3D"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
|
Binary file not shown.
Before Width: | Height: | Size: 290 KiB |
|
@ -16,7 +16,7 @@ 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 ID
|
||||
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
|
||||
|
|
|
@ -1,6 +1,9 @@
|
|||
{
|
||||
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
|
||||
"extends": ["config:best-practices", ":preserveSemverRanges"],
|
||||
"extends": [
|
||||
"config:best-practices",
|
||||
":semanticCommitTypeAll(chore)"
|
||||
],
|
||||
"semanticCommits": "enabled",
|
||||
"automerge": true,
|
||||
"automergeStrategy": "squash",
|
||||
|
|
21
src/cache.rs
21
src/cache.rs
|
@ -16,8 +16,7 @@
|
|||
//! the cache as a JSON file.
|
||||
|
||||
use std::{
|
||||
fs::File,
|
||||
io::Write,
|
||||
fs,
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
|
||||
|
@ -69,21 +68,7 @@ impl Default for FileStorage {
|
|||
|
||||
impl CacheStorage for FileStorage {
|
||||
fn write(&self, data: &str) {
|
||||
fn _write(path: &Path, data: &str) -> Result<(), std::io::Error> {
|
||||
let mut f = File::create(path)?;
|
||||
// Set cache file permissions to 0600 on Unix-based systems
|
||||
#[cfg(target_family = "unix")]
|
||||
{
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
let metadata = f.metadata()?;
|
||||
let mut permissions = metadata.permissions();
|
||||
permissions.set_mode(0o600);
|
||||
std::fs::set_permissions(path, permissions)?;
|
||||
}
|
||||
f.write_all(data.as_bytes())
|
||||
}
|
||||
|
||||
_write(&self.path, data).unwrap_or_else(|e| {
|
||||
fs::write(&self.path, data).unwrap_or_else(|e| {
|
||||
error!(
|
||||
"Could not write cache to file `{}`. Error: {}",
|
||||
self.path.to_string_lossy(),
|
||||
|
@ -97,7 +82,7 @@ impl CacheStorage for FileStorage {
|
|||
return None;
|
||||
}
|
||||
|
||||
match std::fs::read_to_string(&self.path) {
|
||||
match fs::read_to_string(&self.path) {
|
||||
Ok(data) => Some(data),
|
||||
Err(e) => {
|
||||
error!(
|
||||
|
|
|
@ -9,16 +9,14 @@ use crate::{
|
|||
error::{Error, ExtractionError},
|
||||
model::{
|
||||
paginator::{ContinuationEndpoint, Paginator},
|
||||
Channel, ChannelInfo, PlaylistItem, Verification, VideoItem,
|
||||
Channel, ChannelInfo, PlaylistItem, VideoItem,
|
||||
},
|
||||
param::{ChannelOrder, ChannelVideoTab, Language},
|
||||
serializer::{text::TextComponent, MapResult},
|
||||
util::{self, timeago, ProtoBuilder},
|
||||
};
|
||||
|
||||
use super::{
|
||||
response, ClientType, MapRespCtx, MapRespOptions, MapResponse, QContinuation, RustyPipeQuery,
|
||||
};
|
||||
use super::{response, ClientType, MapRespCtx, MapResponse, QContinuation, RustyPipeQuery};
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
|
@ -180,16 +178,12 @@ impl RustyPipeQuery {
|
|||
continuation: &channel_info_ctoken(channel_id, &random_target()),
|
||||
};
|
||||
|
||||
self.execute_request_ctx::<response::ChannelAbout, _, _>(
|
||||
self.execute_request::<response::ChannelAbout, _, _>(
|
||||
ClientType::Desktop,
|
||||
"channel_info",
|
||||
channel_id,
|
||||
"browse",
|
||||
&request_body,
|
||||
MapRespOptions {
|
||||
unlocalized: true,
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
@ -230,7 +224,6 @@ impl MapResponse<Channel<Paginator<VideoItem>>> for response::Channel {
|
|||
mapper.ctoken,
|
||||
visitor_data,
|
||||
ContinuationEndpoint::Browse,
|
||||
false,
|
||||
);
|
||||
|
||||
Ok(MapResult {
|
||||
|
@ -280,7 +273,7 @@ impl MapResponse<Channel<Paginator<PlaylistItem>>> for response::Channel {
|
|||
|
||||
impl MapResponse<ChannelInfo> for response::ChannelAbout {
|
||||
fn map_response(self, ctx: &MapRespCtx<'_>) -> Result<MapResult<ChannelInfo>, ExtractionError> {
|
||||
// Channel info is always fetched in English. There is no localized data
|
||||
// Channel info is always fetched in English. There is no localized data there
|
||||
// and it allows parsing the country name.
|
||||
let lang = Language::En;
|
||||
|
||||
|
@ -335,7 +328,7 @@ impl MapResponse<ChannelInfo> for response::ChannelAbout {
|
|||
.video_count_text
|
||||
.and_then(|txt| util::parse_numeric_or_warn(&txt, &mut warnings)),
|
||||
create_date: about.joined_date_text.and_then(|txt| {
|
||||
timeago::parse_textual_date_or_warn(lang, ctx.utc_offset, &txt, &mut warnings)
|
||||
timeago::parse_textual_date_or_warn(lang, &txt, &mut warnings)
|
||||
.map(OffsetDateTime::date)
|
||||
}),
|
||||
view_count: about
|
||||
|
@ -490,7 +483,7 @@ fn map_channel(
|
|||
.avatar_view_model
|
||||
.image
|
||||
.into(),
|
||||
verification: hdata.title.map(Verification::from).unwrap_or_default(),
|
||||
verification: hdata.title.into(),
|
||||
description: metadata.description,
|
||||
tags: microformat.microformat_data_renderer.tags,
|
||||
banner: hdata.banner.image_banner_view_model.image.into(),
|
||||
|
@ -777,10 +770,8 @@ mod tests {
|
|||
}
|
||||
|
||||
#[rstest]
|
||||
#[case::base("base")]
|
||||
#[case::lockup("20241109_lockup")]
|
||||
fn map_channel_playlists(#[case] name: &str) {
|
||||
let json_path = path!(*TESTFILES / "channel" / format!("channel_playlists_{name}.json"));
|
||||
fn map_channel_playlists() {
|
||||
let json_path = path!(*TESTFILES / "channel" / "channel_playlists.json");
|
||||
let json_file = File::open(json_path).unwrap();
|
||||
|
||||
let channel: response::Channel =
|
||||
|
@ -794,7 +785,7 @@ mod tests {
|
|||
"deserialization/mapping warnings: {:?}",
|
||||
map_res.warnings
|
||||
);
|
||||
insta::assert_ron_snapshot!(format!("map_channel_playlists_{name}"), map_res.c);
|
||||
insta::assert_ron_snapshot!("map_channel_playlists", map_res.c);
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
|
|
|
@ -3,7 +3,7 @@ use std::fmt::Debug;
|
|||
use crate::{
|
||||
error::{Error, ExtractionError},
|
||||
model::ChannelRss,
|
||||
report::Report,
|
||||
report::{Report, RustyPipeInfo},
|
||||
util,
|
||||
};
|
||||
|
||||
|
@ -45,7 +45,7 @@ impl RustyPipeQuery {
|
|||
Err(e) => {
|
||||
if let Some(reporter) = &self.client.inner.reporter {
|
||||
let report = Report {
|
||||
info: self.rp_info(),
|
||||
info: RustyPipeInfo::new(Some(self.opts.lang)),
|
||||
level: crate::report::Level::ERR,
|
||||
operation: "channel_rss",
|
||||
error: Some(e.to_string()),
|
||||
|
|
1326
src/client/mod.rs
1326
src/client/mod.rs
File diff suppressed because it is too large
Load diff
|
@ -5,14 +5,10 @@ use regex::Regex;
|
|||
use tracing::debug;
|
||||
|
||||
use crate::{
|
||||
client::{
|
||||
response::{music_item::map_album_type, url_endpoint::NavigationEndpoint},
|
||||
MapRespOptions, QContinuation,
|
||||
},
|
||||
client::{response::url_endpoint::NavigationEndpoint, MapRespCtxSource, QContinuation},
|
||||
error::{Error, ExtractionError},
|
||||
model::{
|
||||
paginator::Paginator, traits::FromYtItem, AlbumItem, AlbumType, ArtistId, MusicArtist,
|
||||
MusicItem,
|
||||
paginator::Paginator, traits::FromYtItem, AlbumItem, ArtistId, MusicArtist, MusicItem,
|
||||
},
|
||||
param::{AlbumFilter, AlbumOrder},
|
||||
serializer::MapResult,
|
||||
|
@ -113,9 +109,8 @@ impl RustyPipeQuery {
|
|||
artist_id,
|
||||
"browse",
|
||||
&request_body,
|
||||
MapRespOptions {
|
||||
MapRespCtxSource {
|
||||
artist: Some(first_page.artist.clone()),
|
||||
visitor_data: first_page.visitor_data.as_deref(),
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
|
@ -154,24 +149,9 @@ fn map_artist_page(
|
|||
ctx: &MapRespCtx<'_>,
|
||||
skip_extendables: bool,
|
||||
) -> Result<MapResult<(MusicArtist, bool)>, ExtractionError> {
|
||||
let contents = match res.contents {
|
||||
Some(c) => c,
|
||||
None => {
|
||||
if res.microformat.microformat_data_renderer.noindex {
|
||||
return Err(ExtractionError::NotFound {
|
||||
id: ctx.id.to_owned(),
|
||||
msg: "no contents".into(),
|
||||
});
|
||||
} else {
|
||||
return Err(ExtractionError::InvalidData("no contents".into()));
|
||||
}
|
||||
}
|
||||
};
|
||||
// dbg!(&res);
|
||||
|
||||
let header = res
|
||||
.header
|
||||
.ok_or(ExtractionError::InvalidData("no header".into()))?
|
||||
.music_immersive_header_renderer;
|
||||
let header = res.header.music_immersive_header_renderer;
|
||||
|
||||
if let Some(share) = header.share_endpoint {
|
||||
let pb = share.share_entity_endpoint.serialized_share_entity;
|
||||
|
@ -188,12 +168,14 @@ fn map_artist_page(
|
|||
}
|
||||
}
|
||||
|
||||
let sections = contents
|
||||
let sections = res
|
||||
.contents
|
||||
.single_column_browse_results_renderer
|
||||
.contents
|
||||
.into_iter()
|
||||
.next()
|
||||
.map(|c| c.tab_renderer.content.section_list_renderer.contents)
|
||||
.and_then(|tab| tab.tab_renderer.content)
|
||||
.map(|c| c.section_list_renderer.contents)
|
||||
.unwrap_or_default();
|
||||
|
||||
let mut mapper = MusicListMapper::with_artist(
|
||||
|
@ -224,12 +206,11 @@ fn map_artist_page(
|
|||
}
|
||||
}
|
||||
}
|
||||
mapper.album_type = AlbumType::Single;
|
||||
|
||||
mapper.map_response(shelf.contents);
|
||||
}
|
||||
response::music_item::ItemSection::MusicCarouselShelfRenderer(shelf) => {
|
||||
let mut extendable_albums = false;
|
||||
mapper.album_type = AlbumType::Single;
|
||||
if let Some(h) = shelf.header {
|
||||
if let Some(button) = h
|
||||
.music_carousel_shelf_basic_header_renderer
|
||||
|
@ -268,12 +249,6 @@ fn map_artist_page(
|
|||
}
|
||||
}
|
||||
}
|
||||
mapper.album_type = map_album_type(
|
||||
h.music_carousel_shelf_basic_header_renderer
|
||||
.title
|
||||
.first_str(),
|
||||
ctx.lang,
|
||||
);
|
||||
}
|
||||
|
||||
if !skip_extendables || !extendable_albums {
|
||||
|
@ -344,7 +319,6 @@ struct FirstAlbumPage {
|
|||
albums: Vec<AlbumItem>,
|
||||
ctoken: Option<String>,
|
||||
artist: ArtistId,
|
||||
visitor_data: Option<String>,
|
||||
}
|
||||
|
||||
impl MapResponse<FirstAlbumPage> for response::MusicArtistAlbums {
|
||||
|
@ -352,6 +326,8 @@ impl MapResponse<FirstAlbumPage> for response::MusicArtistAlbums {
|
|||
self,
|
||||
ctx: &MapRespCtx<'_>,
|
||||
) -> Result<MapResult<FirstAlbumPage>, ExtractionError> {
|
||||
// dbg!(&self);
|
||||
|
||||
let Some(header) = self.header else {
|
||||
return Err(ExtractionError::NotFound {
|
||||
id: ctx.id.into(),
|
||||
|
@ -396,7 +372,6 @@ impl MapResponse<FirstAlbumPage> for response::MusicArtistAlbums {
|
|||
albums: mapped.c.albums,
|
||||
ctoken,
|
||||
artist: artist_id,
|
||||
visitor_data: ctx.visitor_data.map(str::to_owned),
|
||||
},
|
||||
warnings: mapped.warnings,
|
||||
})
|
||||
|
@ -437,7 +412,6 @@ mod tests {
|
|||
#[case::only_singles("only_singles", "UCfwCE5VhPMGxNPFxtVv7lRw")]
|
||||
#[case::no_artist("no_artist", "UCh8gHdtzO2tXd593_bjErWg")]
|
||||
#[case::only_more_singles("only_more_singles", "UC0aXrjVxG5pZr99v77wZdPQ")]
|
||||
#[case::grouped_albums("20250113_grouped_albums", "UCOR4_bSVIXPsGa4BbCSt60Q")]
|
||||
fn map_music_artist(#[case] name: &str, #[case] id: &str) {
|
||||
let json_path = path!(*TESTFILES / "music_artist" / format!("artist_{name}.json"));
|
||||
let json_file = File::open(json_path).unwrap();
|
||||
|
|
|
@ -91,7 +91,7 @@ impl MapResponse<MusicCharts> for response::MusicCharts {
|
|||
.and_then(|btn| btn.button_renderer.navigation_endpoint.music_page())
|
||||
.map(|mp| (mp.typ, mp.id))
|
||||
}) {
|
||||
Some((MusicPageType::Playlist { .. }, id)) => {
|
||||
Some((MusicPageType::Playlist, id)) => {
|
||||
// Top music videos (first shelf with associated playlist)
|
||||
if top_playlist_id.is_none() {
|
||||
mapper_top.map_response(shelf.contents);
|
||||
|
@ -113,12 +113,12 @@ impl MapResponse<MusicCharts> for response::MusicCharts {
|
|||
});
|
||||
|
||||
let mapped_top = mapper_top.conv_items::<TrackItem>();
|
||||
let mapped_trending = mapper_trending.conv_items::<TrackItem>();
|
||||
let mapped_other = mapper_other.group_items();
|
||||
let mut mapped_trending = mapper_trending.conv_items::<TrackItem>();
|
||||
let mut mapped_other = mapper_other.group_items();
|
||||
|
||||
let mut warnings = mapped_top.warnings;
|
||||
warnings.extend(mapped_trending.warnings);
|
||||
warnings.extend(mapped_other.warnings);
|
||||
warnings.append(&mut mapped_trending.warnings);
|
||||
warnings.append(&mut mapped_other.warnings);
|
||||
|
||||
Ok(MapResult {
|
||||
c: MusicCharts {
|
||||
|
|
|
@ -37,7 +37,7 @@ struct QRadio<'a> {
|
|||
}
|
||||
|
||||
impl RustyPipeQuery {
|
||||
/// Get the metadata of a YouTube Music track
|
||||
/// Get the metadata of a YouTube music track
|
||||
#[tracing::instrument(skip(self), level = "error")]
|
||||
pub async fn music_details<S: AsRef<str> + Debug>(
|
||||
&self,
|
||||
|
@ -61,7 +61,7 @@ impl RustyPipeQuery {
|
|||
.await
|
||||
}
|
||||
|
||||
/// Get the lyrics of a YouTube Music track
|
||||
/// Get the lyrics of a YouTube music track
|
||||
///
|
||||
/// The `lyrics_id` has to be obtained using [`RustyPipeQuery::music_details`].
|
||||
#[tracing::instrument(skip(self), level = "error")]
|
||||
|
@ -269,14 +269,7 @@ impl MapResponse<Paginator<TrackItem>> for response::MusicDetails {
|
|||
.map(|c| c.next_continuation_data.continuation);
|
||||
|
||||
Ok(MapResult {
|
||||
c: Paginator::new_ext(
|
||||
None,
|
||||
tracks,
|
||||
ctoken,
|
||||
None,
|
||||
ContinuationEndpoint::MusicNext,
|
||||
false,
|
||||
),
|
||||
c: Paginator::new_ext(None, tracks, ctoken, None, ContinuationEndpoint::MusicNext),
|
||||
warnings,
|
||||
})
|
||||
}
|
||||
|
|
|
@ -105,6 +105,8 @@ impl MapResponse<Vec<MusicGenreItem>> for response::MusicGenres {
|
|||
|
||||
impl MapResponse<MusicGenre> for response::MusicGenre {
|
||||
fn map_response(self, ctx: &MapRespCtx<'_>) -> Result<MapResult<MusicGenre>, ExtractionError> {
|
||||
// dbg!(&self);
|
||||
|
||||
let content = self
|
||||
.contents
|
||||
.single_column_browse_results_renderer
|
||||
|
|
|
@ -6,14 +6,12 @@ use crate::{
|
|||
model::{
|
||||
paginator::{ContinuationEndpoint, Paginator},
|
||||
richtext::RichText,
|
||||
AlbumId, ChannelId, MusicAlbum, MusicPlaylist, TrackItem, TrackType,
|
||||
AlbumId, ChannelId, MusicAlbum, MusicPlaylist, TrackItem,
|
||||
},
|
||||
serializer::{text::TextComponents, MapResult},
|
||||
util::{self, dictionary, TryRemove, DOT_SEPARATOR},
|
||||
util::{self, TryRemove, DOT_SEPARATOR},
|
||||
};
|
||||
|
||||
use self::response::url_endpoint::MusicPageType;
|
||||
|
||||
use super::{
|
||||
response::{
|
||||
self,
|
||||
|
@ -87,7 +85,7 @@ impl RustyPipeQuery {
|
|||
.iter()
|
||||
.enumerate()
|
||||
.filter_map(|(i, track)| {
|
||||
if track.track_type.is_video() {
|
||||
if track.is_video {
|
||||
Some((i, track.name.clone()))
|
||||
} else {
|
||||
None
|
||||
|
@ -95,26 +93,16 @@ impl RustyPipeQuery {
|
|||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let last_tn = album
|
||||
.tracks
|
||||
.last()
|
||||
.and_then(|t| t.track_nr)
|
||||
.unwrap_or_default();
|
||||
if !to_replace.is_empty() || last_tn < album.track_count {
|
||||
tracing::debug!(
|
||||
"fetching album playlist ({} tracks, {} to replace)",
|
||||
album.track_count,
|
||||
to_replace.len()
|
||||
);
|
||||
if !to_replace.is_empty() {
|
||||
let mut playlist = self.music_playlist(playlist_id).await?;
|
||||
playlist
|
||||
.tracks
|
||||
.extend_limit(&self, album.track_count.into())
|
||||
.extend_limit(&self, album.tracks.len())
|
||||
.await?;
|
||||
|
||||
for (i, title) in to_replace {
|
||||
let found_track = playlist.tracks.items.iter().find_map(|track| {
|
||||
if track.name == title && track.track_type.is_track() {
|
||||
if track.name == title && !track.is_video {
|
||||
Some((track.id.clone(), track.duration))
|
||||
} else {
|
||||
None
|
||||
|
@ -125,19 +113,7 @@ impl RustyPipeQuery {
|
|||
if let Some(duration) = duration {
|
||||
album.tracks[i].duration = Some(duration);
|
||||
}
|
||||
album.tracks[i].track_type = TrackType::Track;
|
||||
}
|
||||
}
|
||||
|
||||
// Extend the list of album tracks with the ones from the playlist if the playlist returned more tracks
|
||||
// This is the case for albums with more than 200 tracks (e.g. audiobooks)
|
||||
if album.tracks.len() < playlist.tracks.items.len() {
|
||||
let mut tn = last_tn;
|
||||
for mut t in playlist.tracks.items.into_iter().skip(album.tracks.len()) {
|
||||
tn += 1;
|
||||
t.album = album.tracks.first().and_then(|t| t.album.clone());
|
||||
t.track_nr = Some(tn);
|
||||
album.tracks.push(t);
|
||||
album.tracks[i].is_video = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -151,21 +127,9 @@ impl MapResponse<MusicPlaylist> for response::MusicPlaylist {
|
|||
self,
|
||||
ctx: &MapRespCtx<'_>,
|
||||
) -> Result<MapResult<MusicPlaylist>, ExtractionError> {
|
||||
let contents = match self.contents {
|
||||
Some(c) => c,
|
||||
None => {
|
||||
if self.microformat.microformat_data_renderer.noindex {
|
||||
return Err(ExtractionError::NotFound {
|
||||
id: ctx.id.to_owned(),
|
||||
msg: "no contents".into(),
|
||||
});
|
||||
} else {
|
||||
return Err(ExtractionError::InvalidData("no contents".into()));
|
||||
}
|
||||
}
|
||||
};
|
||||
// dbg!(&self);
|
||||
|
||||
let (header, music_contents) = match contents {
|
||||
let (header, music_contents) = match self.contents {
|
||||
response::music_playlist::Contents::SingleColumnBrowseResultsRenderer(c) => (
|
||||
self.header,
|
||||
c.contents
|
||||
|
@ -216,16 +180,14 @@ impl MapResponse<MusicPlaylist> for response::MusicPlaylist {
|
|||
|
||||
let mut mapper = MusicListMapper::new(ctx.lang);
|
||||
mapper.map_response(shelf.contents);
|
||||
|
||||
let ctoken = mapper.ctoken.clone().or_else(|| {
|
||||
shelf
|
||||
.continuations
|
||||
.into_iter()
|
||||
.next()
|
||||
.map(|cont| cont.next_continuation_data.continuation)
|
||||
});
|
||||
let map_res = mapper.conv_items();
|
||||
|
||||
let ctoken = shelf
|
||||
.continuations
|
||||
.into_iter()
|
||||
.next()
|
||||
.map(|cont| cont.next_continuation_data.continuation);
|
||||
|
||||
let track_count = if ctoken.is_some() {
|
||||
header.as_ref().and_then(|h| {
|
||||
let parts = h
|
||||
|
@ -251,39 +213,14 @@ impl MapResponse<MusicPlaylist> for response::MusicPlaylist {
|
|||
Some(header) => {
|
||||
let h = header.music_detail_header_renderer;
|
||||
|
||||
let (from_ytm, channel) = match h.facepile {
|
||||
Some(facepile) => {
|
||||
let from_ytm = facepile.avatar_stack_view_model.text.starts_with("YouTube");
|
||||
let channel = facepile
|
||||
.avatar_stack_view_model
|
||||
.renderer_context
|
||||
.command_context
|
||||
.and_then(|c| {
|
||||
c.on_tap
|
||||
.innertube_command
|
||||
.music_page()
|
||||
.filter(|p| p.typ == MusicPageType::User)
|
||||
.map(|p| p.id)
|
||||
})
|
||||
.map(|id| ChannelId {
|
||||
id,
|
||||
name: facepile.avatar_stack_view_model.text,
|
||||
});
|
||||
|
||||
(from_ytm && channel.is_none(), channel)
|
||||
}
|
||||
None => {
|
||||
let st = match h.strapline_text_one {
|
||||
Some(s) => s,
|
||||
None => h.subtitle,
|
||||
};
|
||||
|
||||
let from_ytm = st.0.iter().any(util::is_ytm);
|
||||
let channel = st.0.into_iter().find_map(|c| ChannelId::try_from(c).ok());
|
||||
(from_ytm, channel)
|
||||
}
|
||||
let st = match h.strapline_text_one {
|
||||
Some(s) => s,
|
||||
None => h.subtitle,
|
||||
};
|
||||
|
||||
let from_ytm = st.0.iter().any(util::is_ytm);
|
||||
let channel = st.0.into_iter().find_map(|c| ChannelId::try_from(c).ok());
|
||||
|
||||
(
|
||||
from_ytm,
|
||||
channel,
|
||||
|
@ -334,7 +271,6 @@ impl MapResponse<MusicPlaylist> for response::MusicPlaylist {
|
|||
ctoken,
|
||||
ctx.visitor_data.map(str::to_owned),
|
||||
ContinuationEndpoint::MusicBrowse,
|
||||
ctx.authenticated,
|
||||
),
|
||||
related_playlists: Paginator::new_ext(
|
||||
None,
|
||||
|
@ -342,7 +278,6 @@ impl MapResponse<MusicPlaylist> for response::MusicPlaylist {
|
|||
related_ctoken,
|
||||
ctx.visitor_data.map(str::to_owned),
|
||||
ContinuationEndpoint::MusicBrowse,
|
||||
ctx.authenticated,
|
||||
),
|
||||
},
|
||||
warnings: map_res.warnings,
|
||||
|
@ -352,21 +287,9 @@ impl MapResponse<MusicPlaylist> for response::MusicPlaylist {
|
|||
|
||||
impl MapResponse<MusicAlbum> for response::MusicPlaylist {
|
||||
fn map_response(self, ctx: &MapRespCtx<'_>) -> Result<MapResult<MusicAlbum>, ExtractionError> {
|
||||
let contents = match self.contents {
|
||||
Some(c) => c,
|
||||
None => {
|
||||
if self.microformat.microformat_data_renderer.noindex {
|
||||
return Err(ExtractionError::NotFound {
|
||||
id: ctx.id.to_owned(),
|
||||
msg: "no contents".into(),
|
||||
});
|
||||
} else {
|
||||
return Err(ExtractionError::InvalidData("no contents".into()));
|
||||
}
|
||||
}
|
||||
};
|
||||
// dbg!(&self);
|
||||
|
||||
let (header, sections) = match contents {
|
||||
let (header, sections) = match self.contents {
|
||||
response::music_playlist::Contents::SingleColumnBrowseResultsRenderer(c) => (
|
||||
self.header,
|
||||
c.contents
|
||||
|
@ -406,18 +329,7 @@ impl MapResponse<MusicAlbum> for response::MusicPlaylist {
|
|||
match section {
|
||||
response::music_item::ItemSection::MusicShelfRenderer(sh) => shelf = Some(sh),
|
||||
response::music_item::ItemSection::MusicCarouselShelfRenderer(sh) => {
|
||||
if sh
|
||||
.header
|
||||
.map(|h| {
|
||||
h.music_carousel_shelf_basic_header_renderer
|
||||
.title
|
||||
.first_str()
|
||||
== dictionary::entry(ctx.lang).album_versions_title
|
||||
})
|
||||
.unwrap_or_default()
|
||||
{
|
||||
album_variants = Some(sh.contents);
|
||||
}
|
||||
album_variants = Some(sh.contents);
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
|
@ -482,14 +394,12 @@ impl MapResponse<MusicAlbum> for response::MusicPlaylist {
|
|||
}
|
||||
}
|
||||
|
||||
let playlist_id = self
|
||||
.microformat
|
||||
.microformat_data_renderer
|
||||
.url_canonical
|
||||
.and_then(|x| {
|
||||
x.strip_prefix("https://music.youtube.com/playlist?list=")
|
||||
.map(str::to_owned)
|
||||
});
|
||||
let playlist_id = self.microformat.and_then(|mf| {
|
||||
mf.microformat_data_renderer
|
||||
.url_canonical
|
||||
.strip_prefix("https://music.youtube.com/playlist?list=")
|
||||
.map(str::to_owned)
|
||||
});
|
||||
let (playlist_id, artist_id) = header
|
||||
.menu
|
||||
.or_else(|| header.buttons.into_iter().next())
|
||||
|
@ -516,14 +426,6 @@ impl MapResponse<MusicAlbum> for response::MusicPlaylist {
|
|||
.unwrap_or_default();
|
||||
let artist_id = artist_id.or_else(|| artists.first().and_then(|a| a.id.clone()));
|
||||
|
||||
let second_subtitle_parts = header
|
||||
.second_subtitle
|
||||
.split(|p| p == DOT_SEPARATOR)
|
||||
.collect::<Vec<_>>();
|
||||
let track_count = second_subtitle_parts
|
||||
.get(usize::from(second_subtitle_parts.len() > 2))
|
||||
.and_then(|txt| util::parse_numeric::<u16>(&txt[0]).ok());
|
||||
|
||||
let mut mapper = MusicListMapper::with_album(
|
||||
ctx.lang,
|
||||
artists.clone(),
|
||||
|
@ -558,7 +460,6 @@ impl MapResponse<MusicAlbum> for response::MusicPlaylist {
|
|||
album_type,
|
||||
year,
|
||||
by_va,
|
||||
track_count: track_count.unwrap_or(tracks_res.c.len() as u16),
|
||||
tracks: tracks_res.c,
|
||||
variants: variants_res.c,
|
||||
},
|
||||
|
@ -583,7 +484,6 @@ mod tests {
|
|||
#[case::nomusic("nomusic", "PL1J-6JOckZtE_P9Xx8D3b2O6w0idhuKBe")]
|
||||
#[case::two_columns("20240228_twoColumns", "RDCLAK5uy_kb7EBi6y3GrtJri4_ZH56Ms786DFEimbM")]
|
||||
#[case::n_album("20240228_album", "OLAK5uy_kdSWBZ-9AZDkYkuy0QCc3p0KO9DEHVNH0")]
|
||||
#[case::facepile("20241125_facepile", "PL1J-6JOckZtE_P9Xx8D3b2O6w0idhuKBe")]
|
||||
fn map_music_playlist(#[case] name: &str, #[case] id: &str) {
|
||||
let json_path = path!(*TESTFILES / "music_playlist" / format!("playlist_{name}.json"));
|
||||
let json_file = File::open(json_path).unwrap();
|
||||
|
@ -609,8 +509,8 @@ mod tests {
|
|||
#[case::single("single", "MPREb_bHfHGoy7vuv")]
|
||||
#[case::description("description", "MPREb_PiyfuVl6aYd")]
|
||||
#[case::unavailable("unavailable", "MPREb_AzuWg8qAVVl")]
|
||||
#[case::unavailable("unavailable", "MPREb_AzuWg8qAVVl")]
|
||||
#[case::two_columns("20240228_twoColumns", "MPREb_bHfHGoy7vuv")]
|
||||
#[case::recommends("20250225_recommends", "MPREb_u1I69lSAe5v")]
|
||||
fn map_music_album(#[case] name: &str, #[case] id: &str) {
|
||||
let json_path = path!(*TESTFILES / "music_playlist" / format!("album_{name}.json"));
|
||||
let json_file = File::open(json_path).unwrap();
|
||||
|
|
|
@ -9,7 +9,7 @@ use crate::{
|
|||
paginator::{ContinuationEndpoint, Paginator},
|
||||
traits::FromYtItem,
|
||||
AlbumItem, ArtistItem, MusicItem, MusicPlaylistItem, MusicSearchResult,
|
||||
MusicSearchSuggestion, TrackItem, UserItem,
|
||||
MusicSearchSuggestion, TrackItem,
|
||||
},
|
||||
param::search_filter::MusicSearchFilter,
|
||||
serializer::MapResult,
|
||||
|
@ -57,7 +57,7 @@ impl RustyPipeQuery {
|
|||
.await
|
||||
}
|
||||
|
||||
/// Search YouTube Music and return items of all types
|
||||
/// Search YouTube music and return items of all types
|
||||
pub async fn music_search_main<S: AsRef<str>>(
|
||||
&self,
|
||||
query: S,
|
||||
|
@ -121,15 +121,6 @@ impl RustyPipeQuery {
|
|||
.await
|
||||
}
|
||||
|
||||
/// Search YouTube Music users
|
||||
pub async fn music_search_users<S: AsRef<str>>(
|
||||
&self,
|
||||
query: S,
|
||||
) -> Result<MusicSearchResult<UserItem>, Error> {
|
||||
self.music_search(query, Some(MusicSearchFilter::Users))
|
||||
.await
|
||||
}
|
||||
|
||||
/// Get YouTube Music search suggestions
|
||||
#[tracing::instrument(skip(self), level = "error")]
|
||||
pub async fn music_search_suggestion<S: AsRef<str> + Debug>(
|
||||
|
@ -155,6 +146,8 @@ impl<T: FromYtItem> MapResponse<MusicSearchResult<T>> for response::MusicSearch
|
|||
self,
|
||||
ctx: &MapRespCtx<'_>,
|
||||
) -> Result<MapResult<MusicSearchResult<T>>, ExtractionError> {
|
||||
// dbg!(&self);
|
||||
|
||||
let tabs = self.contents.tabbed_search_results_renderer.contents;
|
||||
let sections = tabs
|
||||
.into_iter()
|
||||
|
@ -187,7 +180,6 @@ impl<T: FromYtItem> MapResponse<MusicSearchResult<T>> for response::MusicSearch
|
|||
response::music_search::ItemSection::None => {}
|
||||
});
|
||||
|
||||
let ctoken = ctoken.or(mapper.ctoken.clone());
|
||||
let map_res = mapper.conv_items();
|
||||
|
||||
Ok(MapResult {
|
||||
|
@ -198,7 +190,6 @@ impl<T: FromYtItem> MapResponse<MusicSearchResult<T>> for response::MusicSearch
|
|||
ctoken,
|
||||
ctx.visitor_data.map(str::to_owned),
|
||||
ContinuationEndpoint::MusicSearch,
|
||||
false,
|
||||
),
|
||||
corrected_query,
|
||||
},
|
||||
|
|
|
@ -1,228 +0,0 @@
|
|||
use std::fmt::Debug;
|
||||
|
||||
use crate::{
|
||||
client::{
|
||||
response::{self, music_item::MusicListMapper},
|
||||
ClientType, MapResponse, QBrowseParams, RustyPipeQuery,
|
||||
},
|
||||
error::{Error, ExtractionError},
|
||||
model::{
|
||||
paginator::{ContinuationEndpoint, Paginator},
|
||||
AlbumItem, ArtistItem, HistoryItem, MusicPlaylist, MusicPlaylistItem, TrackItem,
|
||||
},
|
||||
serializer::MapResult,
|
||||
};
|
||||
|
||||
use super::{MapRespCtx, MapRespOptions, QContinuation};
|
||||
|
||||
impl RustyPipeQuery {
|
||||
/// Get a list of tracks from YouTube Music which the current user recently played
|
||||
///
|
||||
/// Requires authentication cookies.
|
||||
#[tracing::instrument(skip(self), level = "error")]
|
||||
pub async fn music_history(&self) -> Result<Paginator<HistoryItem<TrackItem>>, Error> {
|
||||
let request_body = QBrowseParams {
|
||||
browse_id: "FEmusic_history",
|
||||
params: "oggECgIIAQ%3D%3D",
|
||||
};
|
||||
|
||||
self.clone()
|
||||
.authenticated()
|
||||
.execute_request::<response::MusicHistory, _, _>(
|
||||
ClientType::DesktopMusic,
|
||||
"music_history",
|
||||
"",
|
||||
"browse",
|
||||
&request_body,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
/// Get more YouTube Music history items from the given continuation token
|
||||
#[tracing::instrument(skip(self), level = "error")]
|
||||
pub async fn music_history_continuation<S: AsRef<str> + Debug>(
|
||||
&self,
|
||||
ctoken: S,
|
||||
visitor_data: Option<&str>,
|
||||
) -> Result<Paginator<HistoryItem<TrackItem>>, Error> {
|
||||
let ctoken = ctoken.as_ref();
|
||||
let request_body = QContinuation {
|
||||
continuation: ctoken,
|
||||
};
|
||||
|
||||
self.clone()
|
||||
.authenticated()
|
||||
.execute_request_ctx::<response::MusicContinuation, _, _>(
|
||||
ClientType::Desktop,
|
||||
"history_continuation",
|
||||
ctoken,
|
||||
"browse",
|
||||
&request_body,
|
||||
MapRespOptions {
|
||||
visitor_data,
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
/// Get a list of YouTube Music artists which the current user subscribed to
|
||||
///
|
||||
/// Requires authentication cookies.
|
||||
#[tracing::instrument(skip(self), level = "error")]
|
||||
pub async fn music_saved_artists(&self) -> Result<Paginator<ArtistItem>, Error> {
|
||||
self.clone()
|
||||
.authenticated()
|
||||
.continuation(
|
||||
"4qmFsgIyEh5GRW11c2ljX2xpYnJhcnlfY29ycHVzX2FydGlzdHMaEGdnTUdLZ1FJQUJBQm9BWUI%3D",
|
||||
ContinuationEndpoint::MusicBrowse,
|
||||
None,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
/// Get a list of YouTube Music albums which the current user has added to their collection
|
||||
///
|
||||
/// Requires authentication cookies.
|
||||
#[tracing::instrument(skip(self), level = "error")]
|
||||
pub async fn music_saved_albums(&self) -> Result<Paginator<AlbumItem>, Error> {
|
||||
self.clone()
|
||||
.authenticated()
|
||||
.continuation(
|
||||
"4qmFsgIoEhRGRW11c2ljX2xpa2VkX2FsYnVtcxoQZ2dNR0tnUUlBQkFCb0FZQg%3D%3D",
|
||||
ContinuationEndpoint::MusicBrowse,
|
||||
None,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
/// Get a list of YouTube Music tracks which the current user has added to their collection
|
||||
///
|
||||
/// Contains both liked tracks and tracks from saved albums.
|
||||
///
|
||||
/// Requires authentication cookies.
|
||||
#[tracing::instrument(skip(self), level = "error")]
|
||||
pub async fn music_saved_tracks(&self) -> Result<Paginator<TrackItem>, Error> {
|
||||
self.clone()
|
||||
.authenticated()
|
||||
.continuation(
|
||||
"4qmFsgIoEhRGRW11c2ljX2xpa2VkX3ZpZGVvcxoQZ2dNR0tnUUlBQkFCb0FZQg%3D%3D",
|
||||
ContinuationEndpoint::MusicBrowse,
|
||||
None,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
/// Get a list of YouTube Music playlists which the current user has added to their collection
|
||||
///
|
||||
/// Requires authentication cookies.
|
||||
#[tracing::instrument(skip(self), level = "error")]
|
||||
pub async fn music_saved_playlists(&self) -> Result<Paginator<MusicPlaylistItem>, Error> {
|
||||
self.clone()
|
||||
.authenticated()
|
||||
.continuation(
|
||||
"4qmFsgIrEhdGRW11c2ljX2xpa2VkX3BsYXlsaXN0cxoQZ2dNR0tnUUlBQkFCb0FZQg%3D%3D",
|
||||
ContinuationEndpoint::MusicBrowse,
|
||||
None,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
/// Get all liked YouTube Music tracks of the logged-in user
|
||||
///
|
||||
/// The difference to [`RustyPipeQuery::music_saved_tracks`] is that this function only returns
|
||||
/// tracks that were explicitly liked by the user.
|
||||
///
|
||||
/// Requires authentication cookies.
|
||||
pub async fn music_liked_tracks(&self) -> Result<MusicPlaylist, Error> {
|
||||
self.clone()
|
||||
.authenticated()
|
||||
.music_playlist("LM")
|
||||
.await
|
||||
.map_err(crate::util::map_internal_playlist_err)
|
||||
}
|
||||
}
|
||||
|
||||
impl MapResponse<Paginator<HistoryItem<TrackItem>>> for response::MusicHistory {
|
||||
fn map_response(
|
||||
self,
|
||||
ctx: &MapRespCtx<'_>,
|
||||
) -> Result<MapResult<Paginator<HistoryItem<TrackItem>>>, ExtractionError> {
|
||||
let contents = match self.contents {
|
||||
response::music_playlist::Contents::SingleColumnBrowseResultsRenderer(c) => {
|
||||
c.contents
|
||||
.into_iter()
|
||||
.next()
|
||||
.ok_or(ExtractionError::InvalidData("no content".into()))?
|
||||
.tab_renderer
|
||||
.content
|
||||
.section_list_renderer
|
||||
}
|
||||
response::music_playlist::Contents::TwoColumnBrowseResultsRenderer {
|
||||
secondary_contents,
|
||||
..
|
||||
} => secondary_contents.section_list_renderer,
|
||||
};
|
||||
|
||||
let mut map_res = MapResult::default();
|
||||
|
||||
for shelf in contents.contents {
|
||||
let shelf = if let response::music_item::ItemSection::MusicShelfRenderer(s) = shelf {
|
||||
s
|
||||
} else {
|
||||
continue;
|
||||
};
|
||||
let mut mapper = MusicListMapper::new(ctx.lang);
|
||||
mapper.map_response(shelf.contents);
|
||||
mapper.conv_history_items(shelf.title, ctx.utc_offset, &mut map_res);
|
||||
}
|
||||
|
||||
let ctoken = contents
|
||||
.continuations
|
||||
.into_iter()
|
||||
.next()
|
||||
.map(|c| c.next_continuation_data.continuation);
|
||||
|
||||
Ok(MapResult {
|
||||
c: Paginator::new_ext(
|
||||
None,
|
||||
map_res.c,
|
||||
ctoken,
|
||||
ctx.visitor_data.map(str::to_owned),
|
||||
ContinuationEndpoint::MusicBrowse,
|
||||
true,
|
||||
),
|
||||
warnings: map_res.warnings,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::{fs::File, io::BufReader};
|
||||
|
||||
use path_macro::path;
|
||||
|
||||
use crate::util::tests::TESTFILES;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn map_history() {
|
||||
let json_path = path!(*TESTFILES / "music_userdata" / "music_history.json");
|
||||
let json_file = File::open(json_path).unwrap();
|
||||
|
||||
let history: response::MusicHistory =
|
||||
serde_json::from_reader(BufReader::new(json_file)).unwrap();
|
||||
let map_res = history.map_response(&MapRespCtx::test("")).unwrap();
|
||||
|
||||
assert!(
|
||||
map_res.warnings.is_empty(),
|
||||
"deserialization/mapping warnings: {:?}",
|
||||
map_res.warnings
|
||||
);
|
||||
insta::assert_ron_snapshot!(map_res.c, {
|
||||
".items[].playback_date" => "[date]",
|
||||
});
|
||||
}
|
||||
}
|
|
@ -8,15 +8,9 @@ use crate::model::{
|
|||
};
|
||||
use crate::serializer::MapResult;
|
||||
|
||||
#[cfg(feature = "userdata")]
|
||||
use crate::model::{HistoryItem, TrackItem, VideoItem};
|
||||
|
||||
use super::response::{
|
||||
music_item::{map_queue_item, MusicListMapper, PlaylistPanelVideo},
|
||||
YouTubeListItem,
|
||||
};
|
||||
use super::response::music_item::{map_queue_item, MusicListMapper, PlaylistPanelVideo};
|
||||
use super::{
|
||||
response, ClientType, MapRespCtx, MapRespOptions, MapResponse, QContinuation, RustyPipeQuery,
|
||||
response, ClientType, MapRespCtx, MapRespCtxSource, MapResponse, QContinuation, RustyPipeQuery,
|
||||
};
|
||||
|
||||
impl RustyPipeQuery {
|
||||
|
@ -41,7 +35,7 @@ impl RustyPipeQuery {
|
|||
ctoken,
|
||||
endpoint.as_str(),
|
||||
&request_body,
|
||||
MapRespOptions {
|
||||
MapRespCtxSource {
|
||||
visitor_data,
|
||||
..Default::default()
|
||||
},
|
||||
|
@ -61,7 +55,7 @@ impl RustyPipeQuery {
|
|||
ctoken,
|
||||
endpoint.as_str(),
|
||||
&request_body,
|
||||
MapRespOptions {
|
||||
MapRespCtxSource {
|
||||
visitor_data,
|
||||
..Default::default()
|
||||
},
|
||||
|
@ -83,7 +77,6 @@ fn map_yt_paginator<T: FromYtItem>(
|
|||
ctoken: p.ctoken,
|
||||
visitor_data: p.visitor_data,
|
||||
endpoint,
|
||||
authenticated: p.authenticated,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -97,51 +90,37 @@ fn map_ytm_paginator<T: FromYtItem>(
|
|||
ctoken: p.ctoken,
|
||||
visitor_data: p.visitor_data,
|
||||
endpoint,
|
||||
authenticated: p.authenticated,
|
||||
}
|
||||
}
|
||||
|
||||
fn continuation_items(response: response::Continuation) -> MapResult<Vec<YouTubeListItem>> {
|
||||
response
|
||||
.on_response_received_actions
|
||||
.and_then(|actions| {
|
||||
actions
|
||||
.into_iter()
|
||||
.map(|action| action.append_continuation_items_action.continuation_items)
|
||||
.reduce(|mut acc, mut items| {
|
||||
acc.c.append(&mut items.c);
|
||||
acc.warnings.append(&mut items.warnings);
|
||||
acc
|
||||
})
|
||||
})
|
||||
.or_else(|| {
|
||||
response
|
||||
.continuation_contents
|
||||
.map(|contents| contents.rich_grid_continuation.contents)
|
||||
})
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
impl MapResponse<Paginator<YouTubeItem>> for response::Continuation {
|
||||
fn map_response(
|
||||
self,
|
||||
ctx: &MapRespCtx<'_>,
|
||||
) -> Result<MapResult<Paginator<YouTubeItem>>, ExtractionError> {
|
||||
let estimated_results = self.estimated_results;
|
||||
let items = continuation_items(self);
|
||||
let items = self
|
||||
.on_response_received_actions
|
||||
.and_then(|actions| {
|
||||
actions
|
||||
.into_iter()
|
||||
.map(|action| action.append_continuation_items_action.continuation_items)
|
||||
.reduce(|mut acc, mut items| {
|
||||
acc.c.append(&mut items.c);
|
||||
acc.warnings.append(&mut items.warnings);
|
||||
acc
|
||||
})
|
||||
})
|
||||
.or_else(|| {
|
||||
self.continuation_contents
|
||||
.map(|contents| contents.rich_grid_continuation.contents)
|
||||
})
|
||||
.unwrap_or_default();
|
||||
|
||||
let mut mapper = response::YouTubeListMapper::<YouTubeItem>::new(ctx.lang);
|
||||
mapper.map_response(items);
|
||||
|
||||
Ok(MapResult {
|
||||
c: Paginator::new_ext(
|
||||
estimated_results,
|
||||
mapper.items,
|
||||
mapper.ctoken,
|
||||
ctx.visitor_data.map(str::to_owned),
|
||||
ContinuationEndpoint::Browse,
|
||||
ctx.authenticated,
|
||||
),
|
||||
c: Paginator::new(self.estimated_results, mapper.items, mapper.ctoken),
|
||||
warnings: mapper.warnings,
|
||||
})
|
||||
}
|
||||
|
@ -202,120 +181,14 @@ impl MapResponse<Paginator<MusicItem>> for response::MusicContinuation {
|
|||
None => {}
|
||||
}
|
||||
|
||||
for a in self.on_response_received_actions {
|
||||
mapper.map_response(a.append_continuation_items_action.continuation_items);
|
||||
}
|
||||
|
||||
let ctoken = mapper.ctoken.clone().or_else(|| {
|
||||
continuations
|
||||
.into_iter()
|
||||
.next()
|
||||
.map(|cont| cont.next_continuation_data.continuation)
|
||||
});
|
||||
let map_res = mapper.items();
|
||||
|
||||
Ok(MapResult {
|
||||
c: Paginator::new_ext(
|
||||
None,
|
||||
map_res.c,
|
||||
ctoken,
|
||||
ctx.visitor_data.map(str::to_owned),
|
||||
ContinuationEndpoint::MusicBrowse,
|
||||
ctx.authenticated,
|
||||
),
|
||||
warnings: map_res.warnings,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "userdata")]
|
||||
impl MapResponse<Paginator<HistoryItem<VideoItem>>> for response::Continuation {
|
||||
fn map_response(
|
||||
self,
|
||||
ctx: &MapRespCtx<'_>,
|
||||
) -> Result<MapResult<Paginator<HistoryItem<VideoItem>>>, ExtractionError> {
|
||||
let mut map_res = MapResult::default();
|
||||
let mut ctoken = None;
|
||||
|
||||
let items = continuation_items(self);
|
||||
for item in items.c {
|
||||
match item {
|
||||
response::YouTubeListItem::ItemSectionRenderer { header, contents } => {
|
||||
let mut mapper = response::YouTubeListMapper::<VideoItem>::new(ctx.lang);
|
||||
mapper.map_response(contents);
|
||||
mapper.conv_history_items(
|
||||
header.map(|h| h.item_section_header_renderer.title),
|
||||
ctx.utc_offset,
|
||||
&mut map_res,
|
||||
);
|
||||
}
|
||||
response::YouTubeListItem::ContinuationItemRenderer(ep) => {
|
||||
if ctoken.is_none() {
|
||||
ctoken = ep.continuation_endpoint.into_token();
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(MapResult {
|
||||
c: Paginator::new_ext(
|
||||
None,
|
||||
map_res.c,
|
||||
ctoken,
|
||||
ctx.visitor_data.map(str::to_owned),
|
||||
ContinuationEndpoint::Browse,
|
||||
ctx.authenticated,
|
||||
),
|
||||
warnings: map_res.warnings,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "userdata")]
|
||||
impl MapResponse<Paginator<HistoryItem<TrackItem>>> for response::MusicContinuation {
|
||||
fn map_response(
|
||||
self,
|
||||
ctx: &MapRespCtx<'_>,
|
||||
) -> Result<MapResult<Paginator<HistoryItem<TrackItem>>>, ExtractionError> {
|
||||
let mut map_res = MapResult::default();
|
||||
let mut continuations = Vec::new();
|
||||
|
||||
let mut map_shelf = |shelf: response::music_item::MusicShelf| {
|
||||
let mut mapper = MusicListMapper::new(ctx.lang);
|
||||
mapper.map_response(shelf.contents);
|
||||
mapper.conv_history_items(shelf.title, ctx.utc_offset, &mut map_res);
|
||||
continuations.extend(shelf.continuations);
|
||||
};
|
||||
|
||||
match self.continuation_contents {
|
||||
Some(response::music_item::ContinuationContents::MusicShelfContinuation(shelf)) => {
|
||||
map_shelf(shelf);
|
||||
}
|
||||
Some(response::music_item::ContinuationContents::SectionListContinuation(contents)) => {
|
||||
for c in contents.contents {
|
||||
if let response::music_item::ItemSection::MusicShelfRenderer(shelf) = c {
|
||||
map_shelf(shelf);
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
let ctoken = continuations
|
||||
.into_iter()
|
||||
.next()
|
||||
.map(|cont| cont.next_continuation_data.continuation);
|
||||
|
||||
Ok(MapResult {
|
||||
c: Paginator::new_ext(
|
||||
None,
|
||||
map_res.c,
|
||||
ctoken,
|
||||
ctx.visitor_data.map(str::to_owned),
|
||||
ContinuationEndpoint::MusicBrowse,
|
||||
ctx.authenticated,
|
||||
),
|
||||
c: Paginator::new(None, map_res.c, ctoken),
|
||||
warnings: map_res.warnings,
|
||||
})
|
||||
}
|
||||
|
@ -325,18 +198,12 @@ impl<T: FromYtItem> Paginator<T> {
|
|||
/// Get the next page from the paginator (or `None` if the paginator is exhausted)
|
||||
pub async fn next<Q: AsRef<RustyPipeQuery>>(&self, query: Q) -> Result<Option<Self>, Error> {
|
||||
Ok(match &self.ctoken {
|
||||
Some(ctoken) => {
|
||||
let q = if self.authenticated {
|
||||
&query.as_ref().clone().authenticated()
|
||||
} else {
|
||||
query.as_ref()
|
||||
};
|
||||
|
||||
Some(
|
||||
q.continuation(ctoken, self.endpoint, self.visitor_data.as_deref())
|
||||
.await?,
|
||||
)
|
||||
}
|
||||
Some(ctoken) => Some(
|
||||
query
|
||||
.as_ref()
|
||||
.continuation(ctoken, self.endpoint, self.visitor_data.as_deref())
|
||||
.await?,
|
||||
),
|
||||
_ => None,
|
||||
})
|
||||
}
|
||||
|
@ -425,40 +292,6 @@ impl Paginator<Comment> {
|
|||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "userdata")]
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "userdata")))]
|
||||
impl Paginator<HistoryItem<VideoItem>> {
|
||||
/// Get the next page from the paginator (or `None` if the paginator is exhausted)
|
||||
pub async fn next<Q: AsRef<RustyPipeQuery>>(&self, query: Q) -> Result<Option<Self>, Error> {
|
||||
Ok(match &self.ctoken {
|
||||
Some(ctoken) => Some(
|
||||
query
|
||||
.as_ref()
|
||||
.history_continuation(ctoken, self.visitor_data.as_deref())
|
||||
.await?,
|
||||
),
|
||||
_ => None,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "userdata")]
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "userdata")))]
|
||||
impl Paginator<HistoryItem<TrackItem>> {
|
||||
/// Get the next page from the paginator (or `None` if the paginator is exhausted)
|
||||
pub async fn next<Q: AsRef<RustyPipeQuery>>(&self, query: Q) -> Result<Option<Self>, Error> {
|
||||
Ok(match &self.ctoken {
|
||||
Some(ctoken) => Some(
|
||||
query
|
||||
.as_ref()
|
||||
.music_history_continuation(ctoken, self.visitor_data.as_deref())
|
||||
.await?,
|
||||
),
|
||||
_ => None,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
macro_rules! paginator {
|
||||
($entity_type:ty) => {
|
||||
impl Paginator<$entity_type> {
|
||||
|
@ -540,12 +373,6 @@ macro_rules! paginator {
|
|||
}
|
||||
|
||||
paginator!(Comment);
|
||||
#[cfg(feature = "userdata")]
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "userdata")))]
|
||||
paginator!(HistoryItem<VideoItem>);
|
||||
#[cfg(feature = "userdata")]
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "userdata")))]
|
||||
paginator!(HistoryItem<TrackItem>);
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
|
@ -556,10 +383,7 @@ mod tests {
|
|||
|
||||
use super::*;
|
||||
use crate::{
|
||||
model::{
|
||||
AlbumItem, ArtistItem, ChannelItem, MusicPlaylistItem, PlaylistItem, TrackItem,
|
||||
VideoItem,
|
||||
},
|
||||
model::{MusicPlaylistItem, PlaylistItem, TrackItem, VideoItem},
|
||||
util::tests::TESTFILES,
|
||||
};
|
||||
|
||||
|
@ -630,32 +454,10 @@ mod tests {
|
|||
insta::assert_ron_snapshot!(format!("map_{name}"), paginator);
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[case::subscriptions("subscriptions", path!("userdata" / "subscriptions.json"))]
|
||||
fn map_continuation_channels(#[case] name: &str, #[case] path: PathBuf) {
|
||||
let json_path = path!(*TESTFILES / path);
|
||||
let json_file = File::open(json_path).unwrap();
|
||||
|
||||
let items: response::Continuation =
|
||||
serde_json::from_reader(BufReader::new(json_file)).unwrap();
|
||||
let map_res: MapResult<Paginator<YouTubeItem>> =
|
||||
items.map_response(&MapRespCtx::test("")).unwrap();
|
||||
let paginator: Paginator<ChannelItem> =
|
||||
map_yt_paginator(map_res.c, ContinuationEndpoint::Browse);
|
||||
|
||||
assert!(
|
||||
map_res.warnings.is_empty(),
|
||||
"deserialization/mapping warnings: {:?}",
|
||||
map_res.warnings
|
||||
);
|
||||
insta::assert_ron_snapshot!(format!("map_{name}"), paginator);
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[case::playlist_tracks("playlist_tracks", path!("music_playlist" / "playlist_cont.json"))]
|
||||
#[case::search_tracks("search_tracks", path!("music_search" / "tracks_cont.json"))]
|
||||
#[case::radio_tracks("radio_tracks", path!("music_details" / "radio_cont.json"))]
|
||||
#[case::saved_tracks("saved_tracks", path!("music_userdata" / "saved_tracks.json"))]
|
||||
fn map_continuation_tracks(#[case] name: &str, #[case] path: PathBuf) {
|
||||
let json_path = path!(*TESTFILES / path);
|
||||
let json_file = File::open(json_path).unwrap();
|
||||
|
@ -675,51 +477,8 @@ mod tests {
|
|||
insta::assert_ron_snapshot!(format!("map_{name}"), paginator);
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[case::saved_artists("saved_artists", path!("music_userdata" / "saved_artists.json"))]
|
||||
fn map_continuation_artists(#[case] name: &str, #[case] path: PathBuf) {
|
||||
let json_path = path!(*TESTFILES / path);
|
||||
let json_file = File::open(json_path).unwrap();
|
||||
|
||||
let items: response::MusicContinuation =
|
||||
serde_json::from_reader(BufReader::new(json_file)).unwrap();
|
||||
let map_res: MapResult<Paginator<MusicItem>> =
|
||||
items.map_response(&MapRespCtx::test("")).unwrap();
|
||||
let paginator: Paginator<ArtistItem> =
|
||||
map_ytm_paginator(map_res.c, ContinuationEndpoint::MusicBrowse);
|
||||
|
||||
assert!(
|
||||
map_res.warnings.is_empty(),
|
||||
"deserialization/mapping warnings: {:?}",
|
||||
map_res.warnings
|
||||
);
|
||||
insta::assert_ron_snapshot!(format!("map_{name}"), paginator);
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[case::saved_albums("saved_albums", path!("music_userdata" / "saved_albums.json"))]
|
||||
fn map_continuation_albums(#[case] name: &str, #[case] path: PathBuf) {
|
||||
let json_path = path!(*TESTFILES / path);
|
||||
let json_file = File::open(json_path).unwrap();
|
||||
|
||||
let items: response::MusicContinuation =
|
||||
serde_json::from_reader(BufReader::new(json_file)).unwrap();
|
||||
let map_res: MapResult<Paginator<MusicItem>> =
|
||||
items.map_response(&MapRespCtx::test("")).unwrap();
|
||||
let paginator: Paginator<AlbumItem> =
|
||||
map_ytm_paginator(map_res.c, ContinuationEndpoint::MusicBrowse);
|
||||
|
||||
assert!(
|
||||
map_res.warnings.is_empty(),
|
||||
"deserialization/mapping warnings: {:?}",
|
||||
map_res.warnings
|
||||
);
|
||||
insta::assert_ron_snapshot!(format!("map_{name}"), paginator);
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[case::playlist_related("playlist_related", path!("music_playlist" / "playlist_related.json"))]
|
||||
#[case::saved_playlists("saved_playlists", path!("music_userdata" / "saved_playlists.json"))]
|
||||
fn map_continuation_music_playlists(#[case] name: &str, #[case] path: PathBuf) {
|
||||
let json_path = path!(*TESTFILES / path);
|
||||
let json_file = File::open(json_path).unwrap();
|
||||
|
|
|
@ -1,22 +1,20 @@
|
|||
use std::{
|
||||
borrow::Cow,
|
||||
collections::{BTreeMap, HashMap, HashSet},
|
||||
collections::{BTreeMap, HashMap},
|
||||
fmt::Debug,
|
||||
};
|
||||
|
||||
use once_cell::sync::Lazy;
|
||||
use regex::Regex;
|
||||
use serde::Serialize;
|
||||
use time::OffsetDateTime;
|
||||
use url::Url;
|
||||
|
||||
use crate::{
|
||||
deobfuscate::{DeobfData, Deobfuscator},
|
||||
error::{internal::DeobfError, AuthError, Error, ExtractionError, UnavailabilityReason},
|
||||
deobfuscate::Deobfuscator,
|
||||
error::{internal::DeobfError, Error, ExtractionError, UnavailabilityReason},
|
||||
model::{
|
||||
traits::QualityOrd, AudioCodec, AudioFormat, AudioStream, AudioTrack, DrmLicense,
|
||||
DrmSystem, Frameset, Subtitle, VideoCodec, VideoFormat, VideoPlayer, VideoPlayerDetails,
|
||||
VideoPlayerDrm, VideoStream,
|
||||
traits::QualityOrd, AudioCodec, AudioFormat, AudioStream, AudioTrack, Frameset, Subtitle,
|
||||
VideoCodec, VideoFormat, VideoPlayer, VideoPlayerDetails, VideoStream,
|
||||
},
|
||||
util,
|
||||
};
|
||||
|
@ -26,7 +24,8 @@ use super::{
|
|||
self,
|
||||
player::{self, Format},
|
||||
},
|
||||
ClientType, MapRespCtx, MapRespOptions, MapResponse, MapResult, PoToken, RustyPipeQuery,
|
||||
ClientType, MapRespCtx, MapRespCtxSource, MapResponse, MapResult, RustyPipeQuery,
|
||||
DEFAULT_PLAYER_CLIENT_ORDER,
|
||||
};
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
|
@ -35,15 +34,17 @@ struct QPlayer<'a> {
|
|||
/// Website playback context
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
playback_context: Option<QPlaybackContext<'a>>,
|
||||
/// Content playback nonce (mobile only, 16 random chars)
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
cpn: Option<String>,
|
||||
/// YouTube video ID
|
||||
video_id: &'a str,
|
||||
/// Set to true to allow extraction of streams with sensitive content
|
||||
content_check_ok: bool,
|
||||
/// Probably refers to allowing sensitive content, too
|
||||
racy_check_ok: bool,
|
||||
/// Botguard data
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
service_integrity_dimensions: Option<ServiceIntegrity>,
|
||||
params: Option<&'a str>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
|
@ -61,35 +62,10 @@ struct QContentPlaybackContext<'a> {
|
|||
referer: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct QDrmLicense<'a> {
|
||||
drm_system: &'a str,
|
||||
video_id: &'a str,
|
||||
cpn: &'a str,
|
||||
session_id: &'a str,
|
||||
license_request: &'a str,
|
||||
drm_params: &'a str,
|
||||
drm_video_feature: &'a str,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct ServiceIntegrity {
|
||||
po_token: String,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct PlayerPoToken {
|
||||
visitor_data: Option<String>,
|
||||
session_po_token: Option<PoToken>,
|
||||
content_po_token: Option<ServiceIntegrity>,
|
||||
}
|
||||
|
||||
impl RustyPipeQuery {
|
||||
/// Get YouTube player data (video/audio streams + basic metadata)
|
||||
pub async fn player<S: AsRef<str> + Debug>(&self, video_id: S) -> Result<VideoPlayer, Error> {
|
||||
self.player_from_clients(video_id, self.player_client_order())
|
||||
self.player_from_clients(video_id, DEFAULT_PLAYER_CLIENT_ORDER)
|
||||
.await
|
||||
}
|
||||
|
||||
|
@ -97,84 +73,48 @@ impl RustyPipeQuery {
|
|||
///
|
||||
/// 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 = None;
|
||||
let mut query = Cow::Borrowed(self);
|
||||
let mut clients_iter = clients.iter().peekable();
|
||||
let mut failed_clients = HashSet::new();
|
||||
let mut last_e = Error::Other("no clients".into());
|
||||
|
||||
while let Some(client) = clients_iter.next() {
|
||||
if query.opts.auth == Some(true) && !self.auth_enabled(*client) {
|
||||
// If no client has auth enabled, return NoLogin error instead of "no clients"
|
||||
if last_e.is_none() {
|
||||
last_e = Some(Error::Auth(AuthError::NoLogin));
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if failed_clients.contains(client) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let res = query.player_from_client(video_id, *client).await;
|
||||
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.use_login() && query.opts.auth.is_none() {
|
||||
clients_iter = clients.iter().peekable();
|
||||
query = Cow::Owned(self.clone().authenticated());
|
||||
if e.use_login() && self.auth_enabled() {
|
||||
tracing::info!("{e}; fetching player with login");
|
||||
|
||||
match self
|
||||
.clone()
|
||||
.authenticated()
|
||||
.player_from_client(video_id, *client)
|
||||
.await
|
||||
{
|
||||
Ok(res) => return Ok(res),
|
||||
Err(Error::Extraction(e)) => {
|
||||
if !e.switch_client() {
|
||||
return Err(Error::Extraction(e));
|
||||
}
|
||||
}
|
||||
Err(e) => return Err(e),
|
||||
}
|
||||
last_e = Error::Extraction(e);
|
||||
} else if !e.switch_client() {
|
||||
return Err(Error::Extraction(e));
|
||||
}
|
||||
if let Some(next_client) = clients_iter.peek() {
|
||||
tracing::warn!("error fetching player with {client:?} client: {e}; retrying with {next_client:?} client");
|
||||
}
|
||||
last_e = Some(Error::Extraction(e));
|
||||
failed_clients.insert(*client);
|
||||
}
|
||||
Err(e) => return Err(e),
|
||||
}
|
||||
}
|
||||
Err(last_e.unwrap_or(Error::Other("no clients".into())))
|
||||
}
|
||||
|
||||
async fn get_player_po_token(&self, video_id: &str) -> Result<PlayerPoToken, Error> {
|
||||
if let Some(bg) = &self.client.inner.botguard {
|
||||
let (ident, visitor_data) = if self.opts.auth == Some(true) {
|
||||
(self.client.user_auth_datasync_id()?, None)
|
||||
} else {
|
||||
let visitor_data = self.get_visitor_data(false).await?;
|
||||
(visitor_data.to_owned(), Some(visitor_data))
|
||||
};
|
||||
|
||||
if bg.po_token_cache {
|
||||
let session_token = self.get_session_po_token(&ident).await?;
|
||||
Ok(PlayerPoToken {
|
||||
visitor_data,
|
||||
session_po_token: Some(session_token),
|
||||
content_po_token: None,
|
||||
})
|
||||
} else {
|
||||
let (po_tokens, valid_until) = self.get_po_tokens(&[video_id, &ident]).await?;
|
||||
let mut po_tokens = po_tokens.into_iter();
|
||||
let po_token = po_tokens.next().unwrap();
|
||||
let session_po_token = po_tokens.next().unwrap();
|
||||
Ok(PlayerPoToken {
|
||||
visitor_data,
|
||||
session_po_token: Some(PoToken {
|
||||
po_token: session_po_token,
|
||||
valid_until,
|
||||
}),
|
||||
content_po_token: Some(ServiceIntegrity { po_token }),
|
||||
})
|
||||
}
|
||||
} else {
|
||||
Ok(PlayerPoToken::default())
|
||||
}
|
||||
Err(last_e)
|
||||
}
|
||||
|
||||
/// Get YouTube player data (video/audio streams + basic metadata) using the specified client
|
||||
|
@ -184,43 +124,33 @@ impl RustyPipeQuery {
|
|||
video_id: S,
|
||||
client_type: ClientType,
|
||||
) -> Result<VideoPlayer, Error> {
|
||||
if self.opts.auth == Some(true) {
|
||||
tracing::info!("fetching {client_type:?} player with login");
|
||||
} else {
|
||||
tracing::debug!("fetching {client_type:?} player");
|
||||
}
|
||||
let video_id = video_id.as_ref();
|
||||
let deobf = self.client.get_deobf_data().await?;
|
||||
|
||||
let (deobf, player_po) = tokio::try_join!(
|
||||
async {
|
||||
if client_type.needs_deobf() {
|
||||
Ok::<_, Error>(Some(self.client.get_deobf_data().await?))
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
},
|
||||
async {
|
||||
if client_type.needs_po_token() {
|
||||
self.get_player_po_token(video_id).await
|
||||
} else {
|
||||
Ok(PlayerPoToken::default())
|
||||
}
|
||||
let request_body = if client_type.is_web() {
|
||||
QPlayer {
|
||||
playback_context: Some(QPlaybackContext {
|
||||
content_playback_context: QContentPlaybackContext {
|
||||
signature_timestamp: &deobf.sts,
|
||||
referer: format!("https://www.youtube.com/watch?v={video_id}"),
|
||||
},
|
||||
}),
|
||||
cpn: None,
|
||||
video_id,
|
||||
content_check_ok: true,
|
||||
racy_check_ok: true,
|
||||
params: None,
|
||||
}
|
||||
} else {
|
||||
QPlayer {
|
||||
playback_context: None,
|
||||
cpn: Some(util::generate_content_playback_nonce()),
|
||||
video_id,
|
||||
content_check_ok: true,
|
||||
racy_check_ok: true,
|
||||
// Source: https://github.com/TeamNewPipe/NewPipeExtractor/pull/1168
|
||||
params: Some("CgIIAQ%3D%3D").filter(|_| client_type == ClientType::Android),
|
||||
}
|
||||
)?;
|
||||
|
||||
let playback_context = deobf.as_ref().map(|deobf| QPlaybackContext {
|
||||
content_playback_context: QContentPlaybackContext {
|
||||
signature_timestamp: &deobf.sts,
|
||||
referer: format!("https://www.youtube.com/watch?v={video_id}"),
|
||||
},
|
||||
});
|
||||
|
||||
let request_body = QPlayer {
|
||||
playback_context,
|
||||
video_id,
|
||||
content_check_ok: true,
|
||||
racy_check_ok: true,
|
||||
service_integrity_dimensions: player_po.content_po_token,
|
||||
};
|
||||
|
||||
self.execute_request_ctx::<response::Player, _, _>(
|
||||
|
@ -229,65 +159,13 @@ impl RustyPipeQuery {
|
|||
video_id,
|
||||
"player",
|
||||
&request_body,
|
||||
MapRespOptions {
|
||||
visitor_data: player_po.visitor_data.as_deref(),
|
||||
deobf: deobf.as_ref(),
|
||||
unlocalized: true,
|
||||
session_po_token: player_po.session_po_token,
|
||||
MapRespCtxSource {
|
||||
deobf: Some(&deobf),
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
/// Get the 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 fn player_client_order(&self) -> &'static [ClientType] {
|
||||
if self.client.inner.botguard.is_some() {
|
||||
&[ClientType::Desktop, ClientType::Ios, ClientType::Tv]
|
||||
} else {
|
||||
&[ClientType::Ios, ClientType::Tv]
|
||||
}
|
||||
}
|
||||
|
||||
/// Get a license to play back DRM protected videos
|
||||
///
|
||||
/// Requires authentication (either via OAuth or cookies).
|
||||
#[tracing::instrument(skip(self), level = "error")]
|
||||
pub async fn drm_license(
|
||||
&self,
|
||||
video_id: &str,
|
||||
drm_system: DrmSystem,
|
||||
session_id: &str,
|
||||
drm_params: &str,
|
||||
license_request: &[u8],
|
||||
) -> Result<DrmLicense, Error> {
|
||||
let client_type = self
|
||||
.auth_enabled_client(&[ClientType::Desktop, ClientType::Tv])
|
||||
.ok_or(Error::Auth(AuthError::NoLogin))?;
|
||||
let request_body = QDrmLicense {
|
||||
drm_system: drm_system.req_param(),
|
||||
video_id,
|
||||
cpn: &util::generate_content_playback_nonce(),
|
||||
session_id,
|
||||
license_request: &data_encoding::BASE64.encode(license_request),
|
||||
drm_params,
|
||||
drm_video_feature: "DRM_VIDEO_FEATURE_SDR",
|
||||
};
|
||||
|
||||
self.clone()
|
||||
.authenticated()
|
||||
.execute_request::<response::DrmLicense, _, _>(
|
||||
client_type,
|
||||
"drm_license",
|
||||
video_id,
|
||||
"player/get_drm_license",
|
||||
&request_body,
|
||||
)
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
impl MapResponse<VideoPlayer> for response::Player {
|
||||
|
@ -319,9 +197,8 @@ impl MapResponse<VideoPlayer> for response::Player {
|
|||
"Premium" => Some(UnavailabilityReason::Premium),
|
||||
"members-only" => Some(UnavailabilityReason::MembersOnly),
|
||||
"country" => Some(UnavailabilityReason::Geoblocked),
|
||||
"version" | "websites" => Some(UnavailabilityReason::UnsupportedClient),
|
||||
"Android" | "websites" => Some(UnavailabilityReason::UnsupportedClient),
|
||||
"bot" => Some(UnavailabilityReason::IpBan),
|
||||
"later." => Some(UnavailabilityReason::TryAgain),
|
||||
_ => None,
|
||||
})
|
||||
.unwrap_or_default();
|
||||
|
@ -383,21 +260,6 @@ impl MapResponse<VideoPlayer> for response::Player {
|
|||
video_details.video_id, ctx.id
|
||||
)));
|
||||
}
|
||||
// Sometimes YouTube Desktop does not output any URLs for adaptive streams.
|
||||
// Since this is currently rare, it is best to retry the request in this case.
|
||||
if !is_live
|
||||
&& !streaming_data.adaptive_formats.c.is_empty()
|
||||
&& streaming_data
|
||||
.adaptive_formats
|
||||
.c
|
||||
.iter()
|
||||
.all(|f| f.url.is_none() && f.signature_cipher.is_none())
|
||||
{
|
||||
return Err(ExtractionError::Unavailable {
|
||||
reason: UnavailabilityReason::TryAgain,
|
||||
msg: "no adaptive stream URLs".to_owned(),
|
||||
});
|
||||
}
|
||||
|
||||
let video_info = VideoPlayerDetails {
|
||||
id: video_details.video_id,
|
||||
|
@ -414,10 +276,7 @@ impl MapResponse<VideoPlayer> for response::Player {
|
|||
};
|
||||
|
||||
let streams = if !is_live {
|
||||
let mut mapper = StreamsMapper::new(
|
||||
ctx.deobf,
|
||||
ctx.session_po_token.as_ref().map(|t| t.po_token.as_str()),
|
||||
)?;
|
||||
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()?;
|
||||
|
@ -491,30 +350,6 @@ impl MapResponse<VideoPlayer> for response::Player {
|
|||
})
|
||||
.unwrap_or_default();
|
||||
|
||||
let drm = streaming_data
|
||||
.drm_params
|
||||
.zip(self.heartbeat_params.drm_session_id)
|
||||
.map(|(drm_params, drm_session_id)| VideoPlayerDrm {
|
||||
widevine_service_cert: self
|
||||
.player_config
|
||||
.web_drm_config
|
||||
.and_then(|c| c.widevine_service_cert)
|
||||
.and_then(|c| data_encoding::BASE64URL.decode(c.as_bytes()).ok()),
|
||||
drm_params,
|
||||
authorized_track_types: streaming_data
|
||||
.initial_authorized_drm_track_types
|
||||
.into_iter()
|
||||
.map(|t| t.into())
|
||||
.collect(),
|
||||
drm_session_id,
|
||||
});
|
||||
|
||||
let mut valid_until = OffsetDateTime::now_utc()
|
||||
+ time::Duration::seconds(streaming_data.expires_in_seconds.into());
|
||||
if let Some(pot) = &ctx.session_po_token {
|
||||
valid_until = valid_until.min(pot.valid_until);
|
||||
}
|
||||
|
||||
Ok(MapResult {
|
||||
c: VideoPlayer {
|
||||
details: video_info,
|
||||
|
@ -523,11 +358,9 @@ impl MapResponse<VideoPlayer> for response::Player {
|
|||
audio_streams: streams.audio_streams,
|
||||
subtitles,
|
||||
expires_in_seconds: streaming_data.expires_in_seconds,
|
||||
valid_until,
|
||||
hls_manifest_url: streaming_data.hls_manifest_url,
|
||||
dash_manifest_url: streaming_data.dash_manifest_url,
|
||||
preview_frames,
|
||||
drm,
|
||||
client_type: ctx.client_type,
|
||||
visitor_data: self
|
||||
.response_context
|
||||
|
@ -539,9 +372,8 @@ impl MapResponse<VideoPlayer> for response::Player {
|
|||
}
|
||||
}
|
||||
|
||||
struct StreamsMapper<'a> {
|
||||
deobf: Option<Deobfuscator>,
|
||||
session_po_token: Option<&'a str>,
|
||||
struct StreamsMapper {
|
||||
deobf: Deobfuscator,
|
||||
streams: Streams,
|
||||
warnings: Vec<String>,
|
||||
/// First stream mapping error
|
||||
|
@ -559,25 +391,16 @@ struct Streams {
|
|||
audio_streams: Vec<AudioStream>,
|
||||
}
|
||||
|
||||
impl<'a> StreamsMapper<'a> {
|
||||
fn new(
|
||||
deobf_data: Option<&DeobfData>,
|
||||
session_po_token: Option<&'a str>,
|
||||
) -> Result<Self, DeobfError> {
|
||||
let deobf = match deobf_data {
|
||||
Some(deobf_data) => Some(Deobfuscator::new(deobf_data)?),
|
||||
None => None,
|
||||
};
|
||||
|
||||
Ok(Self {
|
||||
impl StreamsMapper {
|
||||
fn new(deobf: Deobfuscator) -> Self {
|
||||
Self {
|
||||
deobf,
|
||||
session_po_token,
|
||||
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>>) {
|
||||
|
@ -637,12 +460,6 @@ impl<'a> StreamsMapper<'a> {
|
|||
})
|
||||
}
|
||||
|
||||
fn deobf(&self) -> Result<&Deobfuscator, DeobfError> {
|
||||
self.deobf
|
||||
.as_ref()
|
||||
.ok_or(DeobfError::Other("no deobfuscator".into()))
|
||||
}
|
||||
|
||||
fn cipher_to_url_params(
|
||||
&self,
|
||||
signature_cipher: &str,
|
||||
|
@ -663,7 +480,7 @@ impl<'a> StreamsMapper<'a> {
|
|||
let (url_base, mut url_params) =
|
||||
util::url_to_params(raw_url).or(Err(DeobfError::Extraction("url params")))?;
|
||||
|
||||
let deobf_sig = self.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))
|
||||
|
@ -674,7 +491,7 @@ impl<'a> StreamsMapper<'a> {
|
|||
let nsig = if n == &self.last_nsig {
|
||||
self.last_nsig_deobf.to_owned()
|
||||
} else {
|
||||
let nsig = self.deobf()?.deobfuscate_nsig(n)?;
|
||||
let nsig = self.deobf.deobfuscate_nsig(n)?;
|
||||
self.last_nsig.clone_from(n);
|
||||
self.last_nsig_deobf.clone_from(&nsig);
|
||||
nsig
|
||||
|
@ -711,10 +528,6 @@ impl<'a> StreamsMapper<'a> {
|
|||
}?;
|
||||
|
||||
self.deobf_nsig(&mut url_params)?;
|
||||
if let Some(pot) = self.session_po_token {
|
||||
url_params.insert("pot".to_owned(), pot.to_owned());
|
||||
}
|
||||
|
||||
let url = Url::parse_with_params(url_base.as_str(), url_params.iter())
|
||||
.map_err(|_| ExtractionError::InvalidData("could not combine URL".into()))?;
|
||||
|
||||
|
@ -761,8 +574,6 @@ impl<'a> StreamsMapper<'a> {
|
|||
format,
|
||||
codec: get_video_codec(codecs),
|
||||
mime: f.mime_type,
|
||||
drm_track_type: f.drm_track_type.map(|t| t.into()),
|
||||
drm_systems: f.drm_families.into_iter().map(|t| t.into()).collect(),
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -786,11 +597,7 @@ impl<'a> StreamsMapper<'a> {
|
|||
itag: f.itag,
|
||||
bitrate: f.bitrate,
|
||||
average_bitrate: f.average_bitrate.unwrap_or(f.bitrate),
|
||||
size: f.content_length.ok_or_else(|| {
|
||||
ExtractionError::InvalidData(
|
||||
format!("no audio content length. itag: {}", f.itag).into(),
|
||||
)
|
||||
})?,
|
||||
size: f.content_length.unwrap(),
|
||||
index_range: f.index_range,
|
||||
init_range: f.init_range,
|
||||
duration_ms: f.approx_duration_ms,
|
||||
|
@ -802,8 +609,6 @@ impl<'a> StreamsMapper<'a> {
|
|||
track: f
|
||||
.audio_track
|
||||
.map(|t| self.map_audio_track(t, map_res.xtags)),
|
||||
drm_track_type: f.drm_track_type.map(|t| t.into()),
|
||||
drm_systems: f.drm_families.into_iter().map(|t| t.into()).collect(),
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -905,52 +710,17 @@ fn get_audio_codec(codecs: Vec<&str>) -> AudioCodec {
|
|||
return AudioCodec::Mp4a;
|
||||
} else if codec.starts_with("opus") {
|
||||
return AudioCodec::Opus;
|
||||
} else if codec.starts_with("ac-3") {
|
||||
return AudioCodec::Ac3;
|
||||
} else if codec.starts_with("ec-3") {
|
||||
return AudioCodec::Ec3;
|
||||
}
|
||||
}
|
||||
AudioCodec::Unknown
|
||||
}
|
||||
|
||||
impl MapResponse<DrmLicense> for response::DrmLicense {
|
||||
fn map_response(self, _ctx: &MapRespCtx<'_>) -> Result<MapResult<DrmLicense>, ExtractionError> {
|
||||
if self.status != "LICENSE_STATUS_OK" {
|
||||
return Err(ExtractionError::InvalidData(self.status.into()));
|
||||
}
|
||||
|
||||
let license = DrmLicense {
|
||||
license: data_encoding::BASE64URL
|
||||
.decode(self.license.as_bytes())
|
||||
.map_err(|_| ExtractionError::InvalidData("license: invalid b64".into()))?,
|
||||
authorized_formats: self
|
||||
.authorized_formats
|
||||
.into_iter()
|
||||
.filter_map(|f| {
|
||||
let key: Option<[u8; 16]> = data_encoding::BASE64URL
|
||||
.decode(f.key_id.as_bytes())
|
||||
.ok()
|
||||
.and_then(|k| k.try_into().ok());
|
||||
key.map(|k| (f.track_type.into(), k))
|
||||
})
|
||||
.collect(),
|
||||
};
|
||||
|
||||
Ok(MapResult {
|
||||
c: license,
|
||||
warnings: Vec::new(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::{fs::File, io::BufReader};
|
||||
|
||||
use path_macro::path;
|
||||
use rstest::rstest;
|
||||
use time::UtcOffset;
|
||||
|
||||
use super::*;
|
||||
use crate::{deobfuscate::DeobfData, param::Language, util::tests::TESTFILES};
|
||||
|
@ -982,13 +752,10 @@ mod tests {
|
|||
.map_response(&MapRespCtx {
|
||||
id: "pPvd8UxmSbQ",
|
||||
lang: Language::En,
|
||||
utc_offset: UtcOffset::UTC,
|
||||
deobf: Some(&DEOBF_DATA),
|
||||
visitor_data: None,
|
||||
client_type,
|
||||
artist: None,
|
||||
authenticated: false,
|
||||
session_po_token: None,
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
|
@ -997,15 +764,24 @@ mod tests {
|
|||
"deserialization/mapping warnings: {:?}",
|
||||
map_res.warnings
|
||||
);
|
||||
let is_desktop = name == "desktop" || name == "desktopmusic";
|
||||
insta::assert_ron_snapshot!(format!("map_player_data_{name}"), map_res.c, {
|
||||
".valid_until" => "[date]"
|
||||
".details.publish_date" => insta::dynamic_redaction(move |value, _path| {
|
||||
if is_desktop {
|
||||
assert!(value.as_str().unwrap().starts_with("2019-05-30T00:00:00"));
|
||||
"2019-05-30T00:00:00"
|
||||
} else {
|
||||
assert_eq!(value, insta::internals::Content::None);
|
||||
"~"
|
||||
}
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
#[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 mapper = StreamsMapper::new(Some(&DEOBF_DATA), None).unwrap();
|
||||
let mut mapper = StreamsMapper::new(Deobfuscator::new(&DEOBF_DATA).unwrap());
|
||||
let url = mapper
|
||||
.map_url(&None, &Some(signature_cipher.to_owned()))
|
||||
.unwrap()
|
||||
|
|
|
@ -203,13 +203,8 @@ impl MapResponse<Playlist> for response::Playlist {
|
|||
.as_deref()
|
||||
.or(last_update_txt2.as_deref())
|
||||
.and_then(|txt| {
|
||||
timeago::parse_textual_date_or_warn(
|
||||
ctx.lang,
|
||||
ctx.utc_offset,
|
||||
txt,
|
||||
&mut mapper.warnings,
|
||||
)
|
||||
.map(OffsetDateTime::date)
|
||||
timeago::parse_textual_date_or_warn(ctx.lang, txt, &mut mapper.warnings)
|
||||
.map(OffsetDateTime::date)
|
||||
});
|
||||
|
||||
Ok(MapResult {
|
||||
|
@ -222,7 +217,6 @@ impl MapResponse<Playlist> for response::Playlist {
|
|||
mapper.ctoken,
|
||||
ctx.visitor_data.map(str::to_owned),
|
||||
ContinuationEndpoint::Browse,
|
||||
ctx.authenticated,
|
||||
),
|
||||
video_count: n_videos,
|
||||
thumbnail: thumbnails.into(),
|
||||
|
@ -257,7 +251,6 @@ mod tests {
|
|||
#[case::nomusic("nomusic", "PL1J-6JOckZtE_P9Xx8D3b2O6w0idhuKBe")]
|
||||
#[case::live("live", "UULVvqRdlKsE5Q8mf8YXbdIJLw")]
|
||||
#[case::pageheader("20241011_pageheader", "PLT2w2oBf1TZKyvY_M6JsASs73m-wjLzH5")]
|
||||
#[case::cmdexecutor("20250316_cmdexecutor", "PLbZIPy20-1pN7mqjckepWF78ndb6ci_qi")]
|
||||
fn map_playlist_data(#[case] name: &str, #[case] id: &str) {
|
||||
let json_path = path!(*TESTFILES / "playlist" / format!("playlist_{name}.json"));
|
||||
let json_file = File::open(json_path).unwrap();
|
||||
|
|
|
@ -2,14 +2,11 @@ use serde::Deserialize;
|
|||
use serde_with::{rust::deserialize_ignore_any, serde_as, DefaultOnError, VecSkipError};
|
||||
|
||||
use super::{
|
||||
video_item::YouTubeListRenderer, Alert, AttachmentRun, AvatarViewModel, ChannelBadge,
|
||||
ContentRenderer, ContentsRenderer, ContinuationActionWrap, ImageView,
|
||||
PageHeaderRendererContent, PhMetadataView, ResponseContext, Thumbnails, TwoColumnBrowseResults,
|
||||
};
|
||||
use crate::{
|
||||
model::Verification,
|
||||
serializer::text::{AttributedText, Text, TextComponent},
|
||||
video_item::YouTubeListRenderer, Alert, ChannelBadge, ContentRenderer, ContentsRenderer,
|
||||
ContinuationActionWrap, ImageView, PageHeaderRendererContent, PhMetadataView, ResponseContext,
|
||||
Thumbnails, TwoColumnBrowseResults,
|
||||
};
|
||||
use crate::serializer::text::{AttributedText, Text, TextComponent};
|
||||
|
||||
#[serde_as]
|
||||
#[derive(Debug, Deserialize)]
|
||||
|
@ -124,7 +121,7 @@ pub(crate) enum CarouselHeaderRendererItem {
|
|||
pub(crate) struct PageHeaderRendererInner {
|
||||
/// Channel title (only used to extract verification badges)
|
||||
#[serde_as(as = "DefaultOnError")]
|
||||
pub title: Option<PhTitleView>,
|
||||
pub title: PhTitleView,
|
||||
/// Channel avatar
|
||||
pub image: PhAvatarView,
|
||||
/// Channel metadata (subscribers, video count)
|
||||
|
@ -133,7 +130,7 @@ pub(crate) struct PageHeaderRendererInner {
|
|||
pub banner: PhBannerView,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[derive(Default, Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub(crate) struct PhTitleView {
|
||||
pub dynamic_text_view_model: PhTitleView2,
|
||||
|
@ -153,6 +150,58 @@ pub(crate) struct PhTitleView3 {
|
|||
pub attachment_runs: Vec<AttachmentRun>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub(crate) struct AttachmentRun {
|
||||
pub element: AttachmentRunElement,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub(crate) struct AttachmentRunElement {
|
||||
#[serde(rename = "type")]
|
||||
pub typ: AttachmentRunElementType,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub(crate) struct AttachmentRunElementType {
|
||||
pub image_type: AttachmentRunElementImageType,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub(crate) struct AttachmentRunElementImageType {
|
||||
pub image: AttachmentRunElementImage,
|
||||
}
|
||||
|
||||
#[serde_as]
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub(crate) struct AttachmentRunElementImage {
|
||||
#[serde_as(as = "VecSkipError<_>")]
|
||||
pub sources: Vec<AttachmentRunElementImageSource>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub(crate) struct AttachmentRunElementImageSource {
|
||||
pub client_resource: ClientResource,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub(crate) struct ClientResource {
|
||||
pub image_name: IconName,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
|
||||
pub(crate) enum IconName {
|
||||
CheckCircleFilled,
|
||||
MusicFilled,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub(crate) struct PhAvatarView {
|
||||
|
@ -162,7 +211,13 @@ pub(crate) struct PhAvatarView {
|
|||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub(crate) struct PhAvatarView2 {
|
||||
pub avatar: AvatarViewModel,
|
||||
pub avatar: PhAvatarView3,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub(crate) struct PhAvatarView3 {
|
||||
pub avatar_view_model: ImageView,
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Deserialize)]
|
||||
|
@ -275,9 +330,15 @@ impl From<PhTitleView> for crate::model::Verification {
|
|||
.dynamic_text_view_model
|
||||
.text
|
||||
.attachment_runs
|
||||
.into_iter()
|
||||
.next()
|
||||
.map(Verification::from)
|
||||
.iter()
|
||||
.find_map(|r| {
|
||||
r.element.typ.image_type.image.sources.first().map(|s| {
|
||||
match s.client_resource.image_name {
|
||||
IconName::CheckCircleFilled => crate::model::Verification::Verified,
|
||||
IconName::MusicFilled => crate::model::Verification::Artist,
|
||||
}
|
||||
})
|
||||
})
|
||||
.unwrap_or_default()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,8 +0,0 @@
|
|||
use serde::Deserialize;
|
||||
|
||||
use super::{video_item::YouTubeListRendererWrap, Tab, TwoColumnBrowseResults};
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub(crate) struct History {
|
||||
pub contents: TwoColumnBrowseResults<Tab<YouTubeListRendererWrap>>,
|
||||
}
|
|
@ -30,7 +30,6 @@ pub(crate) use music_new::MusicNew;
|
|||
pub(crate) use music_playlist::MusicPlaylist;
|
||||
pub(crate) use music_search::MusicSearch;
|
||||
pub(crate) use music_search::MusicSearchSuggestion;
|
||||
pub(crate) use player::DrmLicense;
|
||||
pub(crate) use player::Player;
|
||||
pub(crate) use playlist::Playlist;
|
||||
pub(crate) use search::Search;
|
||||
|
@ -47,15 +46,6 @@ pub(crate) mod channel_rss;
|
|||
#[cfg(feature = "rss")]
|
||||
pub(crate) use channel_rss::ChannelRss;
|
||||
|
||||
#[cfg(feature = "userdata")]
|
||||
pub(crate) mod history;
|
||||
#[cfg(feature = "userdata")]
|
||||
pub(crate) use history::History;
|
||||
#[cfg(feature = "userdata")]
|
||||
pub(crate) mod music_history;
|
||||
#[cfg(feature = "userdata")]
|
||||
pub(crate) use music_history::MusicHistory;
|
||||
|
||||
use std::borrow::Cow;
|
||||
use std::collections::HashMap;
|
||||
use std::marker::PhantomData;
|
||||
|
@ -123,12 +113,6 @@ pub(crate) struct ImageView {
|
|||
pub image: Thumbnails,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub(crate) struct AvatarViewModel {
|
||||
pub avatar_view_model: ImageView,
|
||||
}
|
||||
|
||||
/// List of images in different resolutions.
|
||||
/// Not only used for thumbnails, but also for avatars and banners.
|
||||
#[derive(Default, Debug, Deserialize)]
|
||||
|
@ -152,16 +136,9 @@ pub(crate) struct ContinuationItemRenderer {
|
|||
pub continuation_endpoint: ContinuationEndpoint,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(untagged)]
|
||||
pub(crate) enum ContinuationEndpoint {
|
||||
ContinuationCommand(ContinuationCommandWrap),
|
||||
CommandExecutorCommand(CommandExecutorCommandWrap),
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub(crate) struct ContinuationCommandWrap {
|
||||
pub(crate) struct ContinuationEndpoint {
|
||||
pub continuation_command: ContinuationCommand,
|
||||
}
|
||||
|
||||
|
@ -171,34 +148,7 @@ pub(crate) struct ContinuationCommand {
|
|||
pub token: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub(crate) struct CommandExecutorCommandWrap {
|
||||
pub command_executor_command: CommandExecutorCommand,
|
||||
}
|
||||
|
||||
#[serde_as]
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub(crate) struct CommandExecutorCommand {
|
||||
#[serde_as(as = "VecSkipError<_>")]
|
||||
commands: Vec<ContinuationCommandWrap>,
|
||||
}
|
||||
|
||||
impl ContinuationEndpoint {
|
||||
pub fn into_token(self) -> Option<String> {
|
||||
match self {
|
||||
Self::ContinuationCommand(cmd) => Some(cmd.continuation_command.token),
|
||||
Self::CommandExecutorCommand(cmd) => cmd
|
||||
.command_executor_command
|
||||
.commands
|
||||
.into_iter()
|
||||
.next()
|
||||
.map(|c| c.continuation_command.token),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub(crate) struct Icon {
|
||||
|
@ -238,92 +188,23 @@ pub(crate) enum ChannelBadgeStyle {
|
|||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub(crate) struct Alert {
|
||||
pub alert_renderer: TextBox,
|
||||
pub alert_renderer: AlertRenderer,
|
||||
}
|
||||
|
||||
#[serde_as]
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub(crate) struct TextBox {
|
||||
pub(crate) struct AlertRenderer {
|
||||
#[serde_as(as = "Text")]
|
||||
pub text: String,
|
||||
}
|
||||
|
||||
#[serde_as]
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub(crate) struct SimpleHeaderRenderer {
|
||||
#[serde_as(as = "Text")]
|
||||
pub title: String,
|
||||
}
|
||||
|
||||
#[serde_as]
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub(crate) struct TextComponentBox {
|
||||
#[serde_as(as = "AttributedText")]
|
||||
pub text: TextComponent,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub(crate) struct ResponseContext {
|
||||
pub visitor_data: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub(crate) struct AttachmentRun {
|
||||
pub element: AttachmentRunElement,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub(crate) struct AttachmentRunElement {
|
||||
#[serde(rename = "type")]
|
||||
pub typ: AttachmentRunElementType,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub(crate) struct AttachmentRunElementType {
|
||||
pub image_type: AttachmentRunElementImageType,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub(crate) struct AttachmentRunElementImageType {
|
||||
pub image: AttachmentRunElementImage,
|
||||
}
|
||||
|
||||
#[serde_as]
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub(crate) struct AttachmentRunElementImage {
|
||||
#[serde_as(as = "VecSkipError<_>")]
|
||||
pub sources: Vec<AttachmentRunElementImageSource>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub(crate) struct AttachmentRunElementImageSource {
|
||||
pub client_resource: ClientResource,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub(crate) struct ClientResource {
|
||||
pub image_name: IconName,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
|
||||
pub enum IconName {
|
||||
CheckCircleFilled,
|
||||
#[serde(alias = "AUDIO_BADGE")]
|
||||
MusicFilled,
|
||||
}
|
||||
|
||||
// CONTINUATION
|
||||
|
||||
#[serde_as]
|
||||
|
@ -462,17 +343,6 @@ impl From<Thumbnails> for Vec<crate::model::Thumbnail> {
|
|||
}
|
||||
}
|
||||
|
||||
impl ContentImage {
|
||||
pub(crate) fn into_image(self) -> ImageViewOl {
|
||||
match self {
|
||||
ContentImage::ThumbnailViewModel(image) => image,
|
||||
ContentImage::CollectionThumbnailViewModel { primary_thumbnail } => {
|
||||
primary_thumbnail.thumbnail_view_model
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Vec<ChannelBadge>> for crate::model::Verification {
|
||||
fn from(badges: Vec<ChannelBadge>) -> Self {
|
||||
badges
|
||||
|
@ -496,25 +366,6 @@ impl From<Icon> for crate::model::Verification {
|
|||
}
|
||||
}
|
||||
|
||||
impl From<AttachmentRun> for crate::model::Verification {
|
||||
fn from(value: AttachmentRun) -> Self {
|
||||
match value
|
||||
.element
|
||||
.typ
|
||||
.image_type
|
||||
.image
|
||||
.sources
|
||||
.into_iter()
|
||||
.next()
|
||||
.map(|s| s.client_resource.image_name)
|
||||
{
|
||||
Some(IconName::CheckCircleFilled) => Self::Verified,
|
||||
Some(IconName::MusicFilled) => Self::Artist,
|
||||
None => Self::None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn alerts_to_err(id: &str, alerts: Option<Vec<Alert>>) -> ExtractionError {
|
||||
ExtractionError::NotFound {
|
||||
id: id.to_owned(),
|
||||
|
@ -629,11 +480,9 @@ pub(crate) struct PhMetadataView {
|
|||
pub content_metadata_view_model: PhMetadataView2,
|
||||
}
|
||||
|
||||
#[serde_as]
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub(crate) struct PhMetadataView2 {
|
||||
#[serde_as(as = "VecSkipError<_>")]
|
||||
pub metadata_rows: Vec<PhMetadataRow>,
|
||||
}
|
||||
|
||||
|
@ -649,26 +498,17 @@ pub(crate) struct PhMetadataRow {
|
|||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub(crate) enum MetadataPart {
|
||||
Text(#[serde_as(as = "AttributedText")] TextComponent),
|
||||
Text(#[serde_as(deserialize_as = "AttributedText")] String),
|
||||
#[serde(rename_all = "camelCase")]
|
||||
AvatarStack {
|
||||
avatar_stack_view_model: TextComponentBox,
|
||||
avatar_stack_view_model: AvatarStackViewModel,
|
||||
},
|
||||
}
|
||||
|
||||
impl MetadataPart {
|
||||
pub fn into_text_component(self) -> TextComponent {
|
||||
match self {
|
||||
MetadataPart::Text(text_component) => text_component,
|
||||
MetadataPart::AvatarStack {
|
||||
avatar_stack_view_model,
|
||||
} => avatar_stack_view_model.text,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn as_str(&self) -> &str {
|
||||
match self {
|
||||
MetadataPart::Text(s) => s.as_str(),
|
||||
MetadataPart::Text(s) => s,
|
||||
MetadataPart::AvatarStack {
|
||||
avatar_stack_view_model,
|
||||
} => avatar_stack_view_model.text.as_str(),
|
||||
|
@ -676,50 +516,10 @@ impl MetadataPart {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub(crate) enum ContentImage {
|
||||
ThumbnailViewModel(ImageViewOl),
|
||||
#[serde(rename_all = "camelCase")]
|
||||
CollectionThumbnailViewModel {
|
||||
primary_thumbnail: ThumbnailViewModelWrap,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub(crate) struct ThumbnailViewModelWrap {
|
||||
pub thumbnail_view_model: ImageViewOl,
|
||||
}
|
||||
|
||||
#[serde_as]
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub(crate) struct ImageViewOl {
|
||||
pub image: Thumbnails,
|
||||
#[serde_as(as = "VecSkipError<_>")]
|
||||
pub overlays: Vec<ImageViewOverlay>,
|
||||
pub(crate) struct AvatarStackViewModel {
|
||||
#[serde_as(deserialize_as = "AttributedText")]
|
||||
pub text: TextComponent,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub(crate) struct ImageViewOverlay {
|
||||
pub thumbnail_overlay_badge_view_model: ThumbnailOverlayBadgeViewModel,
|
||||
}
|
||||
|
||||
#[serde_as]
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub(crate) struct ThumbnailOverlayBadgeViewModel {
|
||||
#[serde_as(as = "VecSkipError<_>")]
|
||||
pub thumbnail_badges: Vec<ThumbnailBadges>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub(crate) struct ThumbnailBadges {
|
||||
pub thumbnail_badge_view_model: TextBox,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub(crate) struct Empty {}
|
||||
|
|
|
@ -5,8 +5,7 @@ use crate::serializer::text::Text;
|
|||
|
||||
use super::{
|
||||
music_item::{
|
||||
Button, Grid, ItemSection, MusicMicroformat, MusicThumbnailRenderer, SimpleHeader,
|
||||
SingleColumnBrowseResult,
|
||||
Button, Grid, ItemSection, MusicThumbnailRenderer, SimpleHeader, SingleColumnBrowseResult,
|
||||
},
|
||||
SectionList, Tab,
|
||||
};
|
||||
|
@ -15,10 +14,8 @@ use super::{
|
|||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub(crate) struct MusicArtist {
|
||||
pub contents: Option<SingleColumnBrowseResult<Tab<SectionList<ItemSection>>>>,
|
||||
pub header: Option<Header>,
|
||||
#[serde(default)]
|
||||
pub microformat: MusicMicroformat,
|
||||
pub contents: SingleColumnBrowseResult<Tab<Option<SectionList<ItemSection>>>>,
|
||||
pub header: Header,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
|
|
|
@ -3,8 +3,8 @@ use serde_with::{serde_as, DefaultOnError, VecSkipError};
|
|||
|
||||
use crate::serializer::text::Text;
|
||||
|
||||
use super::AlertRenderer;
|
||||
use super::ContentsRenderer;
|
||||
use super::TextBox;
|
||||
use super::{
|
||||
music_item::{ItemSection, PlaylistPanelRenderer},
|
||||
ContentRenderer,
|
||||
|
@ -115,7 +115,7 @@ pub(crate) struct MusicLyrics {
|
|||
#[serde(rename_all = "camelCase")]
|
||||
pub(crate) enum ListOrMessage<T> {
|
||||
SectionListRenderer(ContentsRenderer<T>),
|
||||
MessageRenderer(TextBox),
|
||||
MessageRenderer(AlertRenderer),
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
|
|
|
@ -1,8 +0,0 @@
|
|||
use serde::Deserialize;
|
||||
|
||||
use super::music_playlist::Contents;
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub(crate) struct MusicHistory {
|
||||
pub contents: Contents,
|
||||
}
|
|
@ -4,7 +4,7 @@ use serde_with::{rust::deserialize_ignore_any, serde_as, DefaultOnError, VecSkip
|
|||
use crate::{
|
||||
model::{
|
||||
self, traits::FromYtItem, AlbumId, AlbumItem, AlbumType, ArtistId, ArtistItem, ChannelId,
|
||||
MusicItem, MusicItemType, MusicPlaylistItem, TrackItem, UserItem,
|
||||
MusicItem, MusicItemType, MusicPlaylistItem, TrackItem,
|
||||
},
|
||||
param::Language,
|
||||
serializer::{
|
||||
|
@ -18,15 +18,10 @@ use super::{
|
|||
url_endpoint::{
|
||||
BrowseEndpointWrap, MusicPage, MusicPageType, MusicVideoType, NavigationEndpoint, PageType,
|
||||
},
|
||||
ContentsRenderer, ContinuationActionWrap, ContinuationEndpoint, MusicContinuationData,
|
||||
SimpleHeaderRenderer, Thumbnails, ThumbnailsWrap,
|
||||
ContentsRenderer, MusicContinuationData, Thumbnails, ThumbnailsWrap,
|
||||
};
|
||||
|
||||
#[cfg(feature = "userdata")]
|
||||
use crate::model::HistoryItem;
|
||||
#[cfg(feature = "userdata")]
|
||||
use time::UtcOffset;
|
||||
|
||||
#[serde_as]
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub(crate) enum ItemSection {
|
||||
|
@ -44,9 +39,6 @@ pub(crate) enum ItemSection {
|
|||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub(crate) struct MusicShelf {
|
||||
#[cfg(feature = "userdata")]
|
||||
#[serde_as(as = "Option<Text>")]
|
||||
pub title: Option<String>,
|
||||
/// Playlist ID (only for playlists)
|
||||
pub playlist_id: Option<String>,
|
||||
pub contents: MapResult<Vec<MusicResponseItem>>,
|
||||
|
@ -93,10 +85,6 @@ pub(crate) enum MusicResponseItem {
|
|||
MusicResponsiveListItemRenderer(ListMusicItem),
|
||||
MusicTwoRowItemRenderer(CoverMusicItem),
|
||||
MessageRenderer(serde::de::IgnoredAny),
|
||||
#[serde(rename_all = "camelCase")]
|
||||
ContinuationItemRenderer {
|
||||
continuation_endpoint: ContinuationEndpoint,
|
||||
},
|
||||
}
|
||||
|
||||
#[serde_as]
|
||||
|
@ -284,7 +272,7 @@ pub(crate) struct QueueMusicItem {
|
|||
#[derive(Default, Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub(crate) struct MusicThumbnailRenderer {
|
||||
#[serde(default, alias = "croppedSquareThumbnailRenderer")]
|
||||
#[serde(alias = "croppedSquareThumbnailRenderer")]
|
||||
pub music_thumbnail_renderer: ThumbnailsWrap,
|
||||
}
|
||||
|
||||
|
@ -333,14 +321,10 @@ impl From<MusicThumbnailRenderer> for Vec<model::Thumbnail> {
|
|||
}
|
||||
|
||||
/// Music list continuation response model
|
||||
#[serde_as]
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub(crate) struct MusicContinuation {
|
||||
pub continuation_contents: Option<ContinuationContents>,
|
||||
#[serde(default)]
|
||||
#[serde_as(as = "VecSkipError<_>")]
|
||||
pub on_response_received_actions: Vec<ContinuationActionWrap<MusicResponseItem>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
|
@ -412,7 +396,15 @@ pub(crate) struct GridRenderer {
|
|||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub(crate) struct GridHeader {
|
||||
pub grid_header_renderer: SimpleHeaderRenderer,
|
||||
pub grid_header_renderer: GridHeaderRenderer,
|
||||
}
|
||||
|
||||
#[serde_as]
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub(crate) struct GridHeaderRenderer {
|
||||
#[serde_as(as = "Text")]
|
||||
pub title: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
|
@ -427,28 +419,20 @@ pub(crate) struct SimpleHeader {
|
|||
pub music_header_renderer: SimpleHeaderRenderer,
|
||||
}
|
||||
|
||||
#[serde_as]
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub(crate) struct SimpleHeaderRenderer {
|
||||
#[serde_as(as = "Text")]
|
||||
pub title: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub(crate) enum TrackBadge {
|
||||
LiveBadgeRenderer {},
|
||||
}
|
||||
|
||||
#[serde_as]
|
||||
#[derive(Default, Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub(crate) struct MusicMicroformat {
|
||||
#[serde_as(as = "DefaultOnError")]
|
||||
pub microformat_data_renderer: MicroformatData,
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub(crate) struct MicroformatData {
|
||||
pub url_canonical: Option<String>,
|
||||
#[serde(default)]
|
||||
pub noindex: bool,
|
||||
}
|
||||
|
||||
/*
|
||||
#MAPPER
|
||||
*/
|
||||
|
@ -459,13 +443,10 @@ pub(crate) struct MusicListMapper {
|
|||
/// Artists list + various artists flag
|
||||
artists: Option<(Vec<ArtistId>, bool)>,
|
||||
album: Option<AlbumId>,
|
||||
/// Default album type in case an album is unlabeled
|
||||
pub album_type: AlbumType,
|
||||
artist_page: bool,
|
||||
search_suggestion: bool,
|
||||
items: Vec<MusicItem>,
|
||||
warnings: Vec<String>,
|
||||
pub ctoken: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
|
@ -482,12 +463,10 @@ impl MusicListMapper {
|
|||
lang,
|
||||
artists: None,
|
||||
album: None,
|
||||
album_type: AlbumType::Single,
|
||||
artist_page: false,
|
||||
search_suggestion: false,
|
||||
items: Vec::new(),
|
||||
warnings: Vec::new(),
|
||||
ctoken: None,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -496,12 +475,10 @@ impl MusicListMapper {
|
|||
lang,
|
||||
artists: None,
|
||||
album: None,
|
||||
album_type: AlbumType::Single,
|
||||
artist_page: false,
|
||||
search_suggestion: true,
|
||||
items: Vec::new(),
|
||||
warnings: Vec::new(),
|
||||
ctoken: None,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -511,12 +488,10 @@ impl MusicListMapper {
|
|||
lang,
|
||||
artists: Some((vec![artist], false)),
|
||||
album: None,
|
||||
album_type: AlbumType::Single,
|
||||
artist_page: true,
|
||||
search_suggestion: false,
|
||||
items: Vec::new(),
|
||||
warnings: Vec::new(),
|
||||
ctoken: None,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -526,12 +501,10 @@ impl MusicListMapper {
|
|||
lang,
|
||||
artists: Some((artists, by_va)),
|
||||
album: Some(album),
|
||||
album_type: AlbumType::Single,
|
||||
artist_page: false,
|
||||
search_suggestion: false,
|
||||
items: Vec::new(),
|
||||
warnings: Vec::new(),
|
||||
ctoken: None,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -543,14 +516,6 @@ impl MusicListMapper {
|
|||
// Tile
|
||||
MusicResponseItem::MusicTwoRowItemRenderer(item) => self.map_tile(item),
|
||||
MusicResponseItem::MessageRenderer(_) => Ok(None),
|
||||
MusicResponseItem::ContinuationItemRenderer {
|
||||
continuation_endpoint,
|
||||
} => {
|
||||
if self.ctoken.is_none() {
|
||||
self.ctoken = continuation_endpoint.into_token();
|
||||
}
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -570,7 +535,7 @@ impl MusicListMapper {
|
|||
etype
|
||||
}
|
||||
|
||||
/// Map a ListMusicItem (album/playlist item, search result)
|
||||
/// Map a ListMusicItem (album/playlist tile)
|
||||
fn map_list_item(&mut self, item: ListMusicItem) -> Result<Option<MusicItemType>, String> {
|
||||
let mut columns = item.flex_columns.into_iter();
|
||||
let c1 = columns.next();
|
||||
|
@ -794,7 +759,7 @@ impl MusicListMapper {
|
|||
artist_id,
|
||||
album,
|
||||
view_count,
|
||||
track_type: vtype.into(),
|
||||
is_video: vtype.is_video(),
|
||||
track_nr,
|
||||
by_va,
|
||||
}));
|
||||
|
@ -802,16 +767,8 @@ impl MusicListMapper {
|
|||
}
|
||||
// Artist / Album / Playlist
|
||||
Some((page_type, id)) => {
|
||||
// Ignore "Shuffle all" button and builtin "Liked music" and "Saved episodes" playlists
|
||||
if page_type == MusicPageType::None
|
||||
|| (page_type == (MusicPageType::Playlist { is_podcast: false })
|
||||
&& matches!(id.as_str(), "MLCT" | "LM" | "SE"))
|
||||
{
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
let mut subtitle_parts = c2
|
||||
.ok_or_else(|| format!("{id}: could not get subtitle"))?
|
||||
.ok_or_else(|| "could not get subtitle".to_owned())?
|
||||
.renderer
|
||||
.text
|
||||
.split(util::DOT_SEPARATOR)
|
||||
|
@ -872,7 +829,7 @@ impl MusicListMapper {
|
|||
}));
|
||||
Ok(Some(MusicItemType::Album))
|
||||
}
|
||||
MusicPageType::Playlist { is_podcast } => {
|
||||
MusicPageType::Playlist => {
|
||||
// Part 1 may be the "Playlist" label
|
||||
let (channel_p, tcount_p) = match subtitle_p3 {
|
||||
Some(_) => (subtitle_p2, subtitle_p3),
|
||||
|
@ -898,23 +855,9 @@ impl MusicListMapper {
|
|||
channel,
|
||||
track_count,
|
||||
from_ytm,
|
||||
is_podcast,
|
||||
}));
|
||||
Ok(Some(MusicItemType::Playlist))
|
||||
}
|
||||
MusicPageType::User => {
|
||||
// Part 1 may be the "Profile" label
|
||||
let handle = map_channel_handle(subtitle_p2.as_ref())
|
||||
.or_else(|| map_channel_handle(subtitle_p1.as_ref()));
|
||||
|
||||
self.items.push(MusicItem::User(UserItem {
|
||||
id,
|
||||
name: title,
|
||||
handle,
|
||||
avatar: item.thumbnail.into(),
|
||||
}));
|
||||
Ok(Some(MusicItemType::User))
|
||||
}
|
||||
MusicPageType::None => {
|
||||
// There may be broken YT channels from the artist search. They can be skipped.
|
||||
Ok(None)
|
||||
|
@ -974,7 +917,7 @@ impl MusicListMapper {
|
|||
artists,
|
||||
album: None,
|
||||
view_count,
|
||||
track_type: vtype.into(),
|
||||
is_video: vtype.is_video(),
|
||||
track_nr: None,
|
||||
by_va,
|
||||
}));
|
||||
|
@ -999,7 +942,7 @@ impl MusicListMapper {
|
|||
}
|
||||
MusicPageType::Album => {
|
||||
let mut year = None;
|
||||
let mut album_type = self.album_type;
|
||||
let mut album_type = AlbumType::Single;
|
||||
|
||||
let (artists, by_va) =
|
||||
match (subtitle_p1, subtitle_p2, &self.artists, self.artist_page) {
|
||||
|
@ -1046,7 +989,7 @@ impl MusicListMapper {
|
|||
}));
|
||||
Ok(Some(MusicItemType::Album))
|
||||
}
|
||||
MusicPageType::Playlist { is_podcast } => {
|
||||
MusicPageType::Playlist => {
|
||||
// When the playlist subtitle has only 1 part, it is a playlist from YT Music
|
||||
// (featured on the startpage or in genres)
|
||||
let from_ytm = subtitle_p2
|
||||
|
@ -1063,11 +1006,10 @@ impl MusicListMapper {
|
|||
channel,
|
||||
track_count: None,
|
||||
from_ytm,
|
||||
is_podcast,
|
||||
}));
|
||||
Ok(Some(MusicItemType::Playlist))
|
||||
}
|
||||
MusicPageType::None | MusicPageType::User => Ok(None),
|
||||
MusicPageType::None => Ok(None),
|
||||
},
|
||||
None => Err("could not determine item type".to_owned()),
|
||||
}
|
||||
|
@ -1138,7 +1080,7 @@ impl MusicListMapper {
|
|||
artists,
|
||||
album: None,
|
||||
view_count: None,
|
||||
track_type: vtype.into(),
|
||||
is_video: vtype.is_video(),
|
||||
track_nr: None,
|
||||
by_va,
|
||||
}));
|
||||
|
@ -1175,14 +1117,14 @@ impl MusicListMapper {
|
|||
artists,
|
||||
album,
|
||||
view_count,
|
||||
track_type: vtype.into(),
|
||||
is_video: vtype.is_video(),
|
||||
track_nr: None,
|
||||
by_va,
|
||||
}));
|
||||
}
|
||||
Some(MusicItemType::Track)
|
||||
}
|
||||
MusicPageType::Playlist { is_podcast } => {
|
||||
MusicPageType::Playlist => {
|
||||
let from_ytm = subtitle_p2
|
||||
.as_ref()
|
||||
.and_then(|p| p.0.first())
|
||||
|
@ -1199,23 +1141,9 @@ impl MusicListMapper {
|
|||
channel,
|
||||
track_count,
|
||||
from_ytm,
|
||||
is_podcast,
|
||||
}));
|
||||
Some(MusicItemType::Playlist)
|
||||
}
|
||||
MusicPageType::User => {
|
||||
// Part 1 may be the "Profile" label
|
||||
let handle = map_channel_handle(subtitle_p2.as_ref())
|
||||
.or_else(|| map_channel_handle(subtitle_p1.as_ref()));
|
||||
|
||||
self.items.push(MusicItem::User(UserItem {
|
||||
id: music_page.id,
|
||||
name: card.title,
|
||||
handle,
|
||||
avatar: card.thumbnail.into(),
|
||||
}));
|
||||
Some(MusicItemType::User)
|
||||
}
|
||||
MusicPageType::None => None,
|
||||
},
|
||||
None => {
|
||||
|
@ -1278,7 +1206,6 @@ impl MusicListMapper {
|
|||
MusicItem::Album(album) => albums.push(album),
|
||||
MusicItem::Artist(artist) => artists.push(artist),
|
||||
MusicItem::Playlist(playlist) => playlists.push(playlist),
|
||||
MusicItem::User(_) => {}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1292,33 +1219,6 @@ impl MusicListMapper {
|
|||
warnings: self.warnings,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "userdata")]
|
||||
pub fn conv_history_items(
|
||||
self,
|
||||
date_txt: Option<String>,
|
||||
utc_offset: UtcOffset,
|
||||
res: &mut MapResult<Vec<HistoryItem<TrackItem>>>,
|
||||
) {
|
||||
res.warnings.extend(self.warnings);
|
||||
res.c.extend(
|
||||
self.items
|
||||
.into_iter()
|
||||
.filter_map(TrackItem::from_ytm_item)
|
||||
.map(|item| HistoryItem {
|
||||
item,
|
||||
playback_date: date_txt.as_deref().and_then(|s| {
|
||||
timeago::parse_textual_date_to_d(
|
||||
self.lang,
|
||||
utc_offset,
|
||||
s,
|
||||
&mut res.warnings,
|
||||
)
|
||||
}),
|
||||
playback_date_txt: date_txt.clone(),
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Map TextComponents containing artist names to a list of artists and a 'Various Artists' flag
|
||||
|
@ -1356,12 +1256,6 @@ fn map_artist_id_fallback(
|
|||
.or_else(|| fallback_artist.and_then(|a| a.id.clone()))
|
||||
}
|
||||
|
||||
fn map_channel_handle(st: Option<&TextComponents>) -> Option<String> {
|
||||
st.map(|t| t.first_str())
|
||||
.filter(|t| t.starts_with('@'))
|
||||
.map(str::to_owned)
|
||||
}
|
||||
|
||||
pub(crate) fn map_artist_id(entries: Vec<MusicItemMenuEntry>) -> Option<String> {
|
||||
entries.into_iter().find_map(|i| {
|
||||
if let NavigationEndpoint::Browse {
|
||||
|
@ -1432,7 +1326,7 @@ pub(crate) fn map_queue_item(item: QueueMusicItem, lang: Language) -> MapResult<
|
|||
artist_id,
|
||||
album,
|
||||
view_count,
|
||||
track_type: MusicVideoType::from_is_video(is_video).into(),
|
||||
is_video,
|
||||
track_nr: None,
|
||||
by_va,
|
||||
},
|
||||
|
@ -1453,18 +1347,13 @@ mod tests {
|
|||
fn map_album_type_samples() {
|
||||
let json_path = path!(*TESTFILES / "dict" / "album_type_samples.json");
|
||||
let json_file = File::open(json_path).unwrap();
|
||||
let atype_samples: BTreeMap<Language, BTreeMap<String, String>> =
|
||||
let atype_samples: BTreeMap<Language, BTreeMap<AlbumType, String>> =
|
||||
serde_json::from_reader(BufReader::new(json_file)).unwrap();
|
||||
|
||||
for (lang, entry) in &atype_samples {
|
||||
for (album_type_str, txt) in entry {
|
||||
let album_type_n = album_type_str.split('_').next().unwrap();
|
||||
let album_type = serde_plain::from_str::<AlbumType>(album_type_n).unwrap();
|
||||
for (album_type, txt) in entry {
|
||||
let res = map_album_type(txt, *lang);
|
||||
assert_eq!(
|
||||
res, album_type,
|
||||
"{album_type_str}: lang: {lang}, txt: {txt}"
|
||||
);
|
||||
assert_eq!(res, *album_type, "lang: {lang}, txt: {txt}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,25 +1,25 @@
|
|||
use serde::Deserialize;
|
||||
use serde_with::{serde_as, DefaultOnError, VecSkipError};
|
||||
|
||||
use crate::serializer::text::{AttributedText, Text, TextComponents};
|
||||
use crate::serializer::text::{Text, TextComponents};
|
||||
|
||||
use super::{
|
||||
music_item::{
|
||||
Button, ItemSection, MusicContentsRenderer, MusicItemMenuEntry, MusicMicroformat,
|
||||
MusicThumbnailRenderer,
|
||||
Button, ItemSection, MusicContentsRenderer, MusicItemMenuEntry, MusicThumbnailRenderer,
|
||||
},
|
||||
url_endpoint::OnTapWrap,
|
||||
ContentsRenderer, SectionList, Tab,
|
||||
};
|
||||
|
||||
/// Response model for YouTube Music playlists and albums
|
||||
#[serde_as]
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub(crate) struct MusicPlaylist {
|
||||
pub contents: Option<Contents>,
|
||||
pub contents: Contents,
|
||||
pub header: Option<Header>,
|
||||
#[serde(default)]
|
||||
pub microformat: MusicMicroformat,
|
||||
#[serde_as(as = "DefaultOnError")]
|
||||
pub microformat: Option<Microformat>,
|
||||
}
|
||||
|
||||
#[serde_as]
|
||||
|
@ -83,10 +83,6 @@ pub(crate) struct HeaderRenderer {
|
|||
#[serde(default)]
|
||||
#[serde_as(as = "Text")]
|
||||
pub second_subtitle: Vec<String>,
|
||||
/// Channel (newer data model)
|
||||
#[serde(default)]
|
||||
#[serde_as(as = "DefaultOnError")]
|
||||
pub facepile: Option<AvatarStackViewModelWrap>,
|
||||
#[serde(default)]
|
||||
#[serde_as(as = "DefaultOnError")]
|
||||
pub menu: Option<HeaderMenu>,
|
||||
|
@ -141,23 +137,12 @@ impl From<Description> for TextComponents {
|
|||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub(crate) struct AvatarStackViewModelWrap {
|
||||
pub avatar_stack_view_model: AvatarStackViewModel,
|
||||
}
|
||||
|
||||
#[serde_as]
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub(crate) struct AvatarStackViewModel {
|
||||
// #[serde(default)]
|
||||
// pub avatars: Vec<AvatarViewModel>,
|
||||
#[serde_as(as = "AttributedText")]
|
||||
pub text: String,
|
||||
pub renderer_context: AvatarStackRendererContext,
|
||||
pub(crate) struct Microformat {
|
||||
pub microformat_data_renderer: MicroformatData,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub(crate) struct AvatarStackRendererContext {
|
||||
pub command_context: Option<OnTapWrap>,
|
||||
pub(crate) struct MicroformatData {
|
||||
pub url_canonical: String,
|
||||
}
|
||||
|
|
|
@ -2,9 +2,9 @@ use std::ops::Range;
|
|||
|
||||
use serde::Deserialize;
|
||||
use serde_with::serde_as;
|
||||
use serde_with::{DefaultOnError, DisplayFromStr, VecSkipError};
|
||||
use serde_with::{DefaultOnError, DisplayFromStr};
|
||||
|
||||
use super::{Empty, ResponseContext, Thumbnails};
|
||||
use super::{ResponseContext, Thumbnails};
|
||||
use crate::serializer::{text::Text, MapResult};
|
||||
|
||||
#[serde_as]
|
||||
|
@ -19,10 +19,6 @@ pub(crate) struct Player {
|
|||
#[serde_as(deserialize_as = "DefaultOnError")]
|
||||
pub storyboards: Option<Storyboards>,
|
||||
pub response_context: ResponseContext,
|
||||
#[serde(default)]
|
||||
pub player_config: PlayerConfig,
|
||||
#[serde(default)]
|
||||
pub heartbeat_params: HeartbeatParams,
|
||||
}
|
||||
|
||||
#[serde_as]
|
||||
|
@ -61,6 +57,9 @@ pub(crate) enum PlayabilityStatus {
|
|||
},
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub(crate) struct Empty {}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub(crate) struct ErrorScreen {
|
||||
|
@ -89,10 +88,6 @@ pub(crate) struct StreamingData {
|
|||
pub dash_manifest_url: Option<String>,
|
||||
/// Only on livestreams
|
||||
pub hls_manifest_url: Option<String>,
|
||||
pub drm_params: Option<String>,
|
||||
#[serde(default)]
|
||||
#[serde_as(deserialize_as = "VecSkipError<_>")]
|
||||
pub initial_authorized_drm_track_types: Vec<DrmTrackType>,
|
||||
}
|
||||
|
||||
#[serde_as]
|
||||
|
@ -141,16 +136,13 @@ pub(crate) struct Format {
|
|||
pub audio_track: Option<AudioTrack>,
|
||||
|
||||
pub signature_cipher: Option<String>,
|
||||
|
||||
#[serde(default)]
|
||||
#[serde_as(deserialize_as = "VecSkipError<_>")]
|
||||
pub drm_families: Vec<DrmFamily>,
|
||||
pub drm_track_type: Option<DrmTrackType>,
|
||||
}
|
||||
|
||||
impl Format {
|
||||
pub fn is_audio(&self) -> bool {
|
||||
self.audio_quality.is_some() && self.audio_sample_rate.is_some()
|
||||
self.content_length.is_some()
|
||||
&& self.audio_quality.is_some()
|
||||
&& self.audio_sample_rate.is_some()
|
||||
}
|
||||
|
||||
pub fn is_video(&self) -> bool {
|
||||
|
@ -162,7 +154,7 @@ impl Format {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Deserialize, PartialEq, Eq, PartialOrd, Ord)]
|
||||
#[derive(Clone, Copy, Debug, Deserialize, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub(crate) enum Quality {
|
||||
Tiny,
|
||||
|
@ -176,7 +168,7 @@ pub(crate) enum Quality {
|
|||
Hd2160,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Deserialize, PartialEq, Eq, PartialOrd, Ord)]
|
||||
#[derive(Clone, Copy, Debug, Deserialize, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub(crate) enum AudioQuality {
|
||||
#[serde(rename = "AUDIO_QUALITY_ULTRALOW")]
|
||||
UltraLow,
|
||||
|
@ -188,7 +180,7 @@ pub(crate) enum AudioQuality {
|
|||
High,
|
||||
}
|
||||
|
||||
#[derive(Default, Clone, Copy, Debug, Deserialize, PartialEq, Eq)]
|
||||
#[derive(Default, Clone, Copy, Debug, Deserialize, PartialEq, Eq, Hash)]
|
||||
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
|
||||
pub(crate) enum FormatType {
|
||||
#[default]
|
||||
|
@ -203,7 +195,7 @@ pub(crate) struct ColorInfo {
|
|||
pub primaries: Primaries,
|
||||
}
|
||||
|
||||
#[derive(Default, Clone, Copy, Debug, Deserialize, PartialEq, Eq)]
|
||||
#[derive(Default, Clone, Copy, Debug, Deserialize, PartialEq, Eq, Hash)]
|
||||
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
|
||||
pub(crate) enum Primaries {
|
||||
#[default]
|
||||
|
@ -211,24 +203,6 @@ pub(crate) enum Primaries {
|
|||
ColorPrimariesBt2020,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Deserialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
|
||||
#[allow(clippy::enum_variant_names)]
|
||||
pub(crate) enum DrmTrackType {
|
||||
DrmTrackTypeAudio,
|
||||
DrmTrackTypeSd,
|
||||
DrmTrackTypeHd,
|
||||
DrmTrackTypeUhd1,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Deserialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
|
||||
pub(crate) enum DrmFamily {
|
||||
Widevine,
|
||||
Playready,
|
||||
Fairplay,
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Deserialize)]
|
||||
#[serde(default, rename_all = "camelCase")]
|
||||
pub(crate) struct AudioTrack {
|
||||
|
@ -290,57 +264,3 @@ pub(crate) struct Storyboards {
|
|||
pub(crate) struct StoryboardRenderer {
|
||||
pub spec: String,
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub(crate) struct PlayerConfig {
|
||||
pub web_drm_config: Option<WebDrmConfig>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub(crate) struct WebDrmConfig {
|
||||
pub widevine_service_cert: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub(crate) struct HeartbeatParams {
|
||||
pub drm_session_id: Option<String>,
|
||||
}
|
||||
|
||||
impl From<DrmTrackType> for crate::model::DrmTrackType {
|
||||
fn from(value: DrmTrackType) -> Self {
|
||||
match value {
|
||||
DrmTrackType::DrmTrackTypeAudio => Self::Audio,
|
||||
DrmTrackType::DrmTrackTypeSd => Self::Sd,
|
||||
DrmTrackType::DrmTrackTypeHd => Self::Hd,
|
||||
DrmTrackType::DrmTrackTypeUhd1 => Self::Uhd1,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<DrmFamily> for crate::model::DrmSystem {
|
||||
fn from(value: DrmFamily) -> Self {
|
||||
match value {
|
||||
DrmFamily::Widevine => Self::Widevine,
|
||||
DrmFamily::Playready => Self::Playready,
|
||||
DrmFamily::Fairplay => Self::Fairplay,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub(crate) struct DrmLicense {
|
||||
pub status: String,
|
||||
pub license: String,
|
||||
pub authorized_formats: Vec<AuthorizedFormat>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub(crate) struct AuthorizedFormat {
|
||||
pub track_type: DrmTrackType,
|
||||
pub key_id: String,
|
||||
}
|
||||
|
|
|
@ -4,9 +4,9 @@ use serde_with::{serde_as, DefaultOnError, VecSkipError};
|
|||
use crate::serializer::text::{AttributedText, Text, TextComponent, TextComponents};
|
||||
|
||||
use super::{
|
||||
url_endpoint::OnTapWrap, video_item::YouTubeListRenderer, Alert, ContentRenderer,
|
||||
url_endpoint::NavigationEndpoint, video_item::YouTubeListRenderer, Alert, ContentRenderer,
|
||||
ContentsRenderer, ImageView, PageHeaderRendererContent, PhMetadataView, ResponseContext,
|
||||
SectionList, Tab, TextBox, ThumbnailsWrap, TwoColumnBrowseResults,
|
||||
SectionList, Tab, ThumbnailsWrap, TwoColumnBrowseResults,
|
||||
};
|
||||
|
||||
#[serde_as]
|
||||
|
@ -70,7 +70,15 @@ pub(crate) struct PlaylistHeaderBanner {
|
|||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub(crate) struct Byline {
|
||||
pub playlist_byline_renderer: TextBox,
|
||||
pub playlist_byline_renderer: BylineRenderer,
|
||||
}
|
||||
|
||||
#[serde_as]
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub(crate) struct BylineRenderer {
|
||||
#[serde_as(as = "Text")]
|
||||
pub text: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
|
@ -173,5 +181,17 @@ pub(crate) struct ActionsRow {
|
|||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub(crate) struct ButtonAction {
|
||||
pub button_view_model: OnTapWrap,
|
||||
pub button_view_model: ButtonViewModel,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub(crate) struct ButtonViewModel {
|
||||
pub on_tap: ActionOnTap,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub(crate) struct ActionOnTap {
|
||||
pub innertube_command: NavigationEndpoint,
|
||||
}
|
||||
|
|
|
@ -1,12 +1,7 @@
|
|||
use serde::Deserialize;
|
||||
use serde_with::{serde_as, DefaultOnError};
|
||||
|
||||
use crate::{
|
||||
model::{TrackType, UrlTarget},
|
||||
util,
|
||||
};
|
||||
|
||||
use super::Empty;
|
||||
use crate::{model::UrlTarget, util};
|
||||
|
||||
/// navigation/resolve_url response model
|
||||
#[derive(Debug, Deserialize)]
|
||||
|
@ -37,9 +32,6 @@ pub(crate) enum NavigationEndpoint {
|
|||
WatchPlaylist {
|
||||
watch_playlist_endpoint: WatchPlaylistEndpoint,
|
||||
},
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[allow(unused)]
|
||||
CreatePlaylist { create_playlist_endpoint: Empty },
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
|
@ -162,18 +154,6 @@ pub(crate) struct WatchEndpointConfig {
|
|||
pub music_video_type: MusicVideoType,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub(crate) struct OnTap {
|
||||
pub innertube_command: NavigationEndpoint,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub(crate) struct OnTapWrap {
|
||||
pub on_tap: OnTap,
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone, Copy, Deserialize, PartialEq, Eq)]
|
||||
pub(crate) enum MusicVideoType {
|
||||
#[default]
|
||||
|
@ -199,16 +179,6 @@ impl MusicVideoType {
|
|||
}
|
||||
}
|
||||
|
||||
impl From<MusicVideoType> for TrackType {
|
||||
fn from(value: MusicVideoType) -> Self {
|
||||
match value {
|
||||
MusicVideoType::Video => Self::Video,
|
||||
MusicVideoType::Track => Self::Track,
|
||||
MusicVideoType::Episode => Self::Episode,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone, Copy, Deserialize, PartialEq, Eq)]
|
||||
pub(crate) enum PageType {
|
||||
#[serde(
|
||||
|
@ -255,9 +225,8 @@ impl PageType {
|
|||
pub(crate) enum MusicPageType {
|
||||
Artist,
|
||||
Album,
|
||||
Playlist { is_podcast: bool },
|
||||
Playlist,
|
||||
Track { vtype: MusicVideoType },
|
||||
User,
|
||||
None,
|
||||
}
|
||||
|
||||
|
@ -266,13 +235,11 @@ impl From<PageType> for MusicPageType {
|
|||
match t {
|
||||
PageType::Artist => MusicPageType::Artist,
|
||||
PageType::Album => MusicPageType::Album,
|
||||
PageType::Playlist => MusicPageType::Playlist { is_podcast: false },
|
||||
PageType::Podcast => MusicPageType::Playlist { is_podcast: true },
|
||||
PageType::Channel => MusicPageType::User,
|
||||
PageType::Playlist | PageType::Podcast => MusicPageType::Playlist,
|
||||
PageType::Channel | PageType::Unknown => MusicPageType::None,
|
||||
PageType::Episode => MusicPageType::Track {
|
||||
vtype: MusicVideoType::Episode,
|
||||
},
|
||||
PageType::Unknown => MusicPageType::None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -341,11 +308,7 @@ impl NavigationEndpoint {
|
|||
watch_playlist_endpoint,
|
||||
} => Some(MusicPage {
|
||||
id: watch_playlist_endpoint.playlist_id,
|
||||
typ: MusicPageType::Playlist { is_podcast: false },
|
||||
}),
|
||||
NavigationEndpoint::CreatePlaylist { .. } => Some(MusicPage {
|
||||
id: String::new(),
|
||||
typ: MusicPageType::None,
|
||||
typ: MusicPageType::Playlist,
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
@ -390,7 +353,6 @@ impl NavigationEndpoint {
|
|||
NavigationEndpoint::WatchPlaylist {
|
||||
watch_playlist_endpoint,
|
||||
} => Some(watch_playlist_endpoint.playlist_id),
|
||||
NavigationEndpoint::CreatePlaylist { .. } => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -530,14 +530,15 @@ pub(crate) enum ContinuationItemVariants {
|
|||
}
|
||||
|
||||
impl ContinuationItemVariants {
|
||||
pub fn into_token(self) -> Option<String> {
|
||||
pub fn token(self) -> String {
|
||||
match self {
|
||||
ContinuationItemVariants::Ep {
|
||||
continuation_endpoint,
|
||||
} => continuation_endpoint,
|
||||
ContinuationItemVariants::Btn { button } => button.button_renderer.command,
|
||||
}
|
||||
.into_token()
|
||||
.continuation_command
|
||||
.token
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -623,7 +624,6 @@ pub(crate) struct CommentViewModelWrap {
|
|||
pub(crate) struct CommentViewModel {
|
||||
pub comment_id: String,
|
||||
pub comment_key: String,
|
||||
pub comment_surface_key: String,
|
||||
pub toolbar_state_key: String,
|
||||
}
|
||||
|
||||
|
@ -695,7 +695,6 @@ pub(crate) struct AuthorCommentBadgeRenderer {
|
|||
#[serde(rename_all = "camelCase")]
|
||||
pub(crate) enum Payload {
|
||||
CommentEntityPayload(CommentEntityPayload),
|
||||
CommentSurfaceEntityPayload(CommentSurfaceEntityPayload),
|
||||
#[serde(rename_all = "camelCase")]
|
||||
EngagementToolbarStateEntityPayload {
|
||||
heart_state: HeartState,
|
||||
|
@ -717,13 +716,6 @@ pub(crate) struct CommentEntityPayload {
|
|||
pub avatar: ImageView,
|
||||
}
|
||||
|
||||
#[serde_as]
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub(crate) struct CommentSurfaceEntityPayload {
|
||||
pub voice_reply_container_view_model: Option<VoiceReplyContainer>,
|
||||
}
|
||||
|
||||
#[serde_as]
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
|
@ -780,17 +772,3 @@ pub(crate) struct ContinuationButton {
|
|||
pub(crate) struct ContinuationButtonRenderer {
|
||||
pub command: ContinuationEndpoint,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub(crate) struct VoiceReplyContainer {
|
||||
pub voice_reply_container_view_model: VoiceReplyContainer2,
|
||||
}
|
||||
|
||||
#[serde_as]
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub(crate) struct VoiceReplyContainer2 {
|
||||
#[serde_as(as = "AttributedText")]
|
||||
pub transcript_text: TextComponents,
|
||||
}
|
||||
|
|
|
@ -4,9 +4,12 @@ use serde_with::{
|
|||
};
|
||||
use time::OffsetDateTime;
|
||||
|
||||
use super::{ChannelBadge, ContentImage, ContinuationItemRenderer, PhMetadataView, Thumbnails};
|
||||
use super::{ChannelBadge, ContinuationEndpoint, Thumbnails};
|
||||
use crate::{
|
||||
model::{Channel, ChannelItem, ChannelTag, PlaylistItem, VideoItem, YouTubeItem},
|
||||
model::{
|
||||
Channel, ChannelId, ChannelItem, ChannelTag, PlaylistItem, Verification, VideoItem,
|
||||
YouTubeItem,
|
||||
},
|
||||
param::Language,
|
||||
serializer::{
|
||||
text::{AttributedText, Text, TextComponent},
|
||||
|
@ -15,11 +18,6 @@ use crate::{
|
|||
util::{self, timeago, TryRemove},
|
||||
};
|
||||
|
||||
#[cfg(feature = "userdata")]
|
||||
use crate::{client::response::SimpleHeaderRenderer, model::HistoryItem};
|
||||
#[cfg(feature = "userdata")]
|
||||
use time::UtcOffset;
|
||||
|
||||
#[serde_as]
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
|
@ -35,11 +33,12 @@ pub(crate) enum YouTubeListItem {
|
|||
|
||||
ChannelRenderer(ChannelRenderer),
|
||||
|
||||
LockupViewModel(LockupViewModel),
|
||||
|
||||
/// Continuation items are located at the end of a list
|
||||
/// Continauation items are located at the end of a list
|
||||
/// and contain the continuation token for progressive loading
|
||||
ContinuationItemRenderer(ContinuationItemRenderer),
|
||||
#[serde(rename_all = "camelCase")]
|
||||
ContinuationItemRenderer {
|
||||
continuation_endpoint: ContinuationEndpoint,
|
||||
},
|
||||
|
||||
/// Corrected search query
|
||||
#[serde(rename_all = "camelCase")]
|
||||
|
@ -65,8 +64,6 @@ pub(crate) enum YouTubeListItem {
|
|||
/// GridRenderer: contains videos on channel page
|
||||
#[serde(alias = "expandedShelfContentsRenderer", alias = "gridRenderer")]
|
||||
ItemSectionRenderer {
|
||||
#[cfg(feature = "userdata")]
|
||||
header: Option<ItemSectionHeader>,
|
||||
#[serde(alias = "items")]
|
||||
contents: MapResult<Vec<YouTubeListItem>>,
|
||||
},
|
||||
|
@ -168,44 +165,6 @@ pub(crate) struct ShortsOverlayMetadata {
|
|||
pub secondary_text: Option<String>,
|
||||
}
|
||||
|
||||
/// Generalized list item, currently only used for channel playlists and YTM items
|
||||
#[serde_as]
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub(crate) struct LockupViewModel {
|
||||
pub content_id: String,
|
||||
#[serde(default)]
|
||||
#[serde_as(deserialize_as = "DefaultOnError")]
|
||||
pub content_type: LockupContentType,
|
||||
pub content_image: ContentImage,
|
||||
pub metadata: LockupViewModelMetadata,
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Deserialize)]
|
||||
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
|
||||
#[allow(clippy::enum_variant_names)]
|
||||
pub(crate) enum LockupContentType {
|
||||
LockupContentTypePlaylist,
|
||||
LockupContentTypeVideo,
|
||||
#[default]
|
||||
Unknown,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub(crate) struct LockupViewModelMetadata {
|
||||
pub lockup_metadata_view_model: LockupViewModelMetadataInner,
|
||||
}
|
||||
|
||||
#[serde_as]
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub(crate) struct LockupViewModelMetadataInner {
|
||||
#[serde_as(as = "AttributedText")]
|
||||
pub title: String,
|
||||
pub metadata: PhMetadataView,
|
||||
}
|
||||
|
||||
/// Video displayed in a playlist
|
||||
#[serde_as]
|
||||
#[derive(Debug, Deserialize)]
|
||||
|
@ -298,13 +257,6 @@ pub(crate) struct YouTubeListRenderer {
|
|||
pub contents: MapResult<Vec<YouTubeListItem>>,
|
||||
}
|
||||
|
||||
#[cfg(feature = "userdata")]
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub(crate) struct ItemSectionHeader {
|
||||
pub item_section_header_renderer: SimpleHeaderRenderer,
|
||||
}
|
||||
|
||||
#[serde_as]
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
|
@ -520,18 +472,19 @@ impl<T> YouTubeListMapper<T> {
|
|||
thumbnail: video.thumbnail.into(),
|
||||
channel: video
|
||||
.channel
|
||||
.and_then(|c| ChannelTag::try_from(c).ok())
|
||||
.map(|mut c| {
|
||||
c.avatar = video
|
||||
.channel_thumbnail_supported_renderers
|
||||
.map(|tn| tn.channel_thumbnail_with_link_renderer.thumbnail)
|
||||
.or(video.channel_thumbnail)
|
||||
.unwrap_or_default()
|
||||
.into();
|
||||
if !c.verification.verified() {
|
||||
c.verification = video.owner_badges.into();
|
||||
}
|
||||
c
|
||||
.and_then(|c| {
|
||||
ChannelId::try_from(c).ok().map(|c| ChannelTag {
|
||||
id: c.id,
|
||||
name: c.name,
|
||||
avatar: video
|
||||
.channel_thumbnail_supported_renderers
|
||||
.map(|tn| tn.channel_thumbnail_with_link_renderer.thumbnail)
|
||||
.or(video.channel_thumbnail)
|
||||
.unwrap_or_default()
|
||||
.into(),
|
||||
verification: video.owner_badges.into(),
|
||||
subscriber_count: None,
|
||||
})
|
||||
})
|
||||
.or_else(|| self.channel.clone()),
|
||||
publish_date: video
|
||||
|
@ -613,7 +566,16 @@ impl<T> YouTubeListMapper<T> {
|
|||
}
|
||||
|
||||
fn map_playlist_video(&mut self, video: PlaylistVideoRenderer) -> VideoItem {
|
||||
let channel = ChannelTag::try_from(video.channel).ok();
|
||||
let channel = ChannelId::try_from(video.channel)
|
||||
.ok()
|
||||
.map(|ch| ChannelTag {
|
||||
id: ch.id,
|
||||
name: ch.name,
|
||||
avatar: Vec::new(),
|
||||
verification: Verification::None,
|
||||
subscriber_count: None,
|
||||
});
|
||||
|
||||
let mut video_info = video.video_info.into_iter();
|
||||
let video_info1 = video_info
|
||||
.next()
|
||||
|
@ -676,12 +638,14 @@ impl<T> YouTubeListMapper<T> {
|
|||
.into(),
|
||||
channel: playlist
|
||||
.channel
|
||||
.and_then(|c| ChannelTag::try_from(c).ok())
|
||||
.map(|mut c| {
|
||||
if !c.verification.verified() {
|
||||
c.verification = playlist.owner_badges.into();
|
||||
}
|
||||
c
|
||||
.and_then(|c| {
|
||||
ChannelId::try_from(c).ok().map(|c| ChannelTag {
|
||||
id: c.id,
|
||||
name: c.name,
|
||||
avatar: Vec::new(),
|
||||
verification: playlist.owner_badges.into(),
|
||||
subscriber_count: None,
|
||||
})
|
||||
})
|
||||
.or_else(|| self.channel.clone()),
|
||||
video_count: playlist.video_count.or_else(|| {
|
||||
|
@ -717,89 +681,6 @@ impl<T> YouTubeListMapper<T> {
|
|||
short_description: channel.description_snippet,
|
||||
}
|
||||
}
|
||||
|
||||
fn map_lockup(&mut self, lockup: LockupViewModel) -> Option<YouTubeItem> {
|
||||
let md = lockup.metadata.lockup_metadata_view_model;
|
||||
let tn = lockup.content_image.into_image();
|
||||
match lockup.content_type {
|
||||
LockupContentType::LockupContentTypePlaylist => {
|
||||
Some(YouTubeItem::Playlist(PlaylistItem {
|
||||
id: lockup.content_id,
|
||||
name: md.title,
|
||||
thumbnail: tn.image.into(),
|
||||
channel: self.channel.clone(),
|
||||
video_count: tn
|
||||
.overlays
|
||||
.first()
|
||||
.and_then(|ol| {
|
||||
ol.thumbnail_overlay_badge_view_model
|
||||
.thumbnail_badges
|
||||
.first()
|
||||
})
|
||||
.and_then(|badge| {
|
||||
util::parse_numeric(&badge.thumbnail_badge_view_model.text).ok()
|
||||
}),
|
||||
}))
|
||||
}
|
||||
LockupContentType::LockupContentTypeVideo => {
|
||||
let mut mdr = md
|
||||
.metadata
|
||||
.content_metadata_view_model
|
||||
.metadata_rows
|
||||
.into_iter();
|
||||
let channel = mdr
|
||||
.next()
|
||||
.and_then(|r| r.metadata_parts.into_iter().next())
|
||||
.and_then(|p| ChannelTag::try_from(p.into_text_component()).ok());
|
||||
let (view_count, publish_date_txt) = mdr
|
||||
.next()
|
||||
.map(|metadata_row| {
|
||||
let mut parts = metadata_row.metadata_parts.into_iter();
|
||||
let p1 = parts.next();
|
||||
let p2 = parts.next();
|
||||
(
|
||||
p1.and_then(|p| {
|
||||
util::parse_large_numstr_or_warn(
|
||||
p.as_str(),
|
||||
self.lang,
|
||||
&mut self.warnings,
|
||||
)
|
||||
}),
|
||||
p2.map(|p2| p2.into_text_component().into_string()),
|
||||
)
|
||||
})
|
||||
.unwrap_or_default();
|
||||
|
||||
Some(YouTubeItem::Video(VideoItem {
|
||||
id: lockup.content_id,
|
||||
name: md.title,
|
||||
duration: tn
|
||||
.overlays
|
||||
.first()
|
||||
.and_then(|ol| {
|
||||
ol.thumbnail_overlay_badge_view_model
|
||||
.thumbnail_badges
|
||||
.first()
|
||||
})
|
||||
.and_then(|badge| {
|
||||
util::parse_video_length(&badge.thumbnail_badge_view_model.text)
|
||||
}),
|
||||
thumbnail: tn.image.into(),
|
||||
channel,
|
||||
publish_date: publish_date_txt.as_deref().and_then(|t| {
|
||||
timeago::parse_timeago_dt_or_warn(self.lang, t, &mut self.warnings)
|
||||
}),
|
||||
publish_date_txt,
|
||||
view_count,
|
||||
is_live: false,
|
||||
is_short: false,
|
||||
is_upcoming: false,
|
||||
short_description: None,
|
||||
}))
|
||||
}
|
||||
LockupContentType::Unknown => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl YouTubeListMapper<YouTubeItem> {
|
||||
|
@ -830,23 +711,16 @@ impl YouTubeListMapper<YouTubeItem> {
|
|||
let mapped = YouTubeItem::Channel(self.map_channel(channel));
|
||||
self.items.push(mapped);
|
||||
}
|
||||
YouTubeListItem::LockupViewModel(lockup) => {
|
||||
if let Some(mapped) = self.map_lockup(lockup) {
|
||||
self.items.push(mapped);
|
||||
}
|
||||
}
|
||||
YouTubeListItem::ContinuationItemRenderer(r) => {
|
||||
if self.ctoken.is_none() {
|
||||
self.ctoken = r.continuation_endpoint.into_token();
|
||||
}
|
||||
}
|
||||
YouTubeListItem::ContinuationItemRenderer {
|
||||
continuation_endpoint,
|
||||
} => self.ctoken = Some(continuation_endpoint.continuation_command.token),
|
||||
YouTubeListItem::ShowingResultsForRenderer { corrected_query } => {
|
||||
self.corrected_query = Some(corrected_query);
|
||||
}
|
||||
YouTubeListItem::RichItemRenderer { content } => {
|
||||
self.map_item(*content);
|
||||
}
|
||||
YouTubeListItem::ItemSectionRenderer { mut contents, .. } => {
|
||||
YouTubeListItem::ItemSectionRenderer { mut contents } => {
|
||||
self.warnings.append(&mut contents.warnings);
|
||||
contents.c.into_iter().for_each(|it| self.map_item(it));
|
||||
}
|
||||
|
@ -880,23 +754,16 @@ impl YouTubeListMapper<VideoItem> {
|
|||
let mapped = self.map_playlist_video(video);
|
||||
self.items.push(mapped);
|
||||
}
|
||||
YouTubeListItem::LockupViewModel(lockup) => {
|
||||
if let Some(YouTubeItem::Video(mapped)) = self.map_lockup(lockup) {
|
||||
self.items.push(mapped);
|
||||
}
|
||||
}
|
||||
YouTubeListItem::ContinuationItemRenderer(r) => {
|
||||
if self.ctoken.is_none() {
|
||||
self.ctoken = r.continuation_endpoint.into_token();
|
||||
}
|
||||
}
|
||||
YouTubeListItem::ContinuationItemRenderer {
|
||||
continuation_endpoint,
|
||||
} => self.ctoken = Some(continuation_endpoint.continuation_command.token),
|
||||
YouTubeListItem::ShowingResultsForRenderer { corrected_query } => {
|
||||
self.corrected_query = Some(corrected_query);
|
||||
}
|
||||
YouTubeListItem::RichItemRenderer { content } => {
|
||||
self.map_item(*content);
|
||||
}
|
||||
YouTubeListItem::ItemSectionRenderer { mut contents, .. } => {
|
||||
YouTubeListItem::ItemSectionRenderer { mut contents } => {
|
||||
self.warnings.append(&mut contents.warnings);
|
||||
contents.c.into_iter().for_each(|it| self.map_item(it));
|
||||
}
|
||||
|
@ -908,23 +775,6 @@ impl YouTubeListMapper<VideoItem> {
|
|||
self.warnings.append(&mut res.warnings);
|
||||
res.c.into_iter().for_each(|item| self.map_item(item));
|
||||
}
|
||||
|
||||
#[cfg(feature = "userdata")]
|
||||
pub(crate) fn conv_history_items(
|
||||
self,
|
||||
date_txt: Option<String>,
|
||||
utc_offset: UtcOffset,
|
||||
res: &mut MapResult<Vec<HistoryItem<VideoItem>>>,
|
||||
) {
|
||||
res.warnings.extend(self.warnings);
|
||||
res.c.extend(self.items.into_iter().map(|item| HistoryItem {
|
||||
item,
|
||||
playback_date: date_txt.as_deref().and_then(|s| {
|
||||
timeago::parse_textual_date_to_d(self.lang, utc_offset, s, &mut res.warnings)
|
||||
}),
|
||||
playback_date_txt: date_txt.clone(),
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
impl YouTubeListMapper<PlaylistItem> {
|
||||
|
@ -934,23 +784,16 @@ impl YouTubeListMapper<PlaylistItem> {
|
|||
let mapped = self.map_playlist(playlist);
|
||||
self.items.push(mapped);
|
||||
}
|
||||
YouTubeListItem::LockupViewModel(lockup) => {
|
||||
if let Some(YouTubeItem::Playlist(mapped)) = self.map_lockup(lockup) {
|
||||
self.items.push(mapped);
|
||||
}
|
||||
}
|
||||
YouTubeListItem::ContinuationItemRenderer(r) => {
|
||||
if self.ctoken.is_none() {
|
||||
self.ctoken = r.continuation_endpoint.into_token();
|
||||
}
|
||||
}
|
||||
YouTubeListItem::ContinuationItemRenderer {
|
||||
continuation_endpoint,
|
||||
} => self.ctoken = Some(continuation_endpoint.continuation_command.token),
|
||||
YouTubeListItem::ShowingResultsForRenderer { corrected_query } => {
|
||||
self.corrected_query = Some(corrected_query);
|
||||
}
|
||||
YouTubeListItem::RichItemRenderer { content } => {
|
||||
self.map_item(*content);
|
||||
}
|
||||
YouTubeListItem::ItemSectionRenderer { mut contents, .. } => {
|
||||
YouTubeListItem::ItemSectionRenderer { mut contents } => {
|
||||
self.warnings.append(&mut contents.warnings);
|
||||
contents.c.into_iter().for_each(|it| self.map_item(it));
|
||||
}
|
||||
|
|
|
@ -120,9 +120,8 @@ impl<T: FromYtItem> MapResponse<SearchResult<T>> for response::Search {
|
|||
.filter_map(T::from_yt_item)
|
||||
.collect(),
|
||||
mapper.ctoken,
|
||||
ctx.visitor_data.map(str::to_owned),
|
||||
None,
|
||||
ContinuationEndpoint::Search,
|
||||
false,
|
||||
),
|
||||
corrected_query: mapper.corrected_query,
|
||||
visitor_data: self
|
||||
|
|
|
@ -25,7 +25,7 @@ Channel(
|
|||
height: 176,
|
||||
),
|
||||
],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
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",
|
||||
tags: [
|
||||
"electronics",
|
||||
|
@ -125,7 +125,7 @@ Channel(
|
|||
id: "UC2DjFE7Xf11URZqWBigcVOQ",
|
||||
name: "EEVblog",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(884000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -166,7 +166,7 @@ Channel(
|
|||
id: "UC2DjFE7Xf11URZqWBigcVOQ",
|
||||
name: "EEVblog",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(884000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -207,7 +207,7 @@ Channel(
|
|||
id: "UC2DjFE7Xf11URZqWBigcVOQ",
|
||||
name: "EEVblog",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(884000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -248,7 +248,7 @@ Channel(
|
|||
id: "UC2DjFE7Xf11URZqWBigcVOQ",
|
||||
name: "EEVblog",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(884000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -289,7 +289,7 @@ Channel(
|
|||
id: "UC2DjFE7Xf11URZqWBigcVOQ",
|
||||
name: "EEVblog",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(884000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -330,7 +330,7 @@ Channel(
|
|||
id: "UC2DjFE7Xf11URZqWBigcVOQ",
|
||||
name: "EEVblog",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(884000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -371,7 +371,7 @@ Channel(
|
|||
id: "UC2DjFE7Xf11URZqWBigcVOQ",
|
||||
name: "EEVblog",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(884000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -412,7 +412,7 @@ Channel(
|
|||
id: "UC2DjFE7Xf11URZqWBigcVOQ",
|
||||
name: "EEVblog",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(884000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -453,7 +453,7 @@ Channel(
|
|||
id: "UC2DjFE7Xf11URZqWBigcVOQ",
|
||||
name: "EEVblog",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(884000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -494,7 +494,7 @@ Channel(
|
|||
id: "UC2DjFE7Xf11URZqWBigcVOQ",
|
||||
name: "EEVblog",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(884000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -535,7 +535,7 @@ Channel(
|
|||
id: "UC2DjFE7Xf11URZqWBigcVOQ",
|
||||
name: "EEVblog",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(884000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -576,7 +576,7 @@ Channel(
|
|||
id: "UC2DjFE7Xf11URZqWBigcVOQ",
|
||||
name: "EEVblog",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(884000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -617,7 +617,7 @@ Channel(
|
|||
id: "UC2DjFE7Xf11URZqWBigcVOQ",
|
||||
name: "EEVblog",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(884000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -658,7 +658,7 @@ Channel(
|
|||
id: "UC2DjFE7Xf11URZqWBigcVOQ",
|
||||
name: "EEVblog",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(884000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -699,7 +699,7 @@ Channel(
|
|||
id: "UC2DjFE7Xf11URZqWBigcVOQ",
|
||||
name: "EEVblog",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(884000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -740,7 +740,7 @@ Channel(
|
|||
id: "UC2DjFE7Xf11URZqWBigcVOQ",
|
||||
name: "EEVblog",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(884000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -781,7 +781,7 @@ Channel(
|
|||
id: "UC2DjFE7Xf11URZqWBigcVOQ",
|
||||
name: "EEVblog",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(884000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -822,7 +822,7 @@ Channel(
|
|||
id: "UC2DjFE7Xf11URZqWBigcVOQ",
|
||||
name: "EEVblog",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(884000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -863,7 +863,7 @@ Channel(
|
|||
id: "UC2DjFE7Xf11URZqWBigcVOQ",
|
||||
name: "EEVblog",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(884000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -904,7 +904,7 @@ Channel(
|
|||
id: "UC2DjFE7Xf11URZqWBigcVOQ",
|
||||
name: "EEVblog",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(884000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -945,7 +945,7 @@ Channel(
|
|||
id: "UC2DjFE7Xf11URZqWBigcVOQ",
|
||||
name: "EEVblog",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(884000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -986,7 +986,7 @@ Channel(
|
|||
id: "UC2DjFE7Xf11URZqWBigcVOQ",
|
||||
name: "EEVblog",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(884000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -1027,7 +1027,7 @@ Channel(
|
|||
id: "UC2DjFE7Xf11URZqWBigcVOQ",
|
||||
name: "EEVblog",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(884000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -1068,7 +1068,7 @@ Channel(
|
|||
id: "UC2DjFE7Xf11URZqWBigcVOQ",
|
||||
name: "EEVblog",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(884000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -1109,7 +1109,7 @@ Channel(
|
|||
id: "UC2DjFE7Xf11URZqWBigcVOQ",
|
||||
name: "EEVblog",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(884000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -1150,7 +1150,7 @@ Channel(
|
|||
id: "UC2DjFE7Xf11URZqWBigcVOQ",
|
||||
name: "EEVblog",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(884000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -1191,7 +1191,7 @@ Channel(
|
|||
id: "UC2DjFE7Xf11URZqWBigcVOQ",
|
||||
name: "EEVblog",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(884000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -1232,7 +1232,7 @@ Channel(
|
|||
id: "UC2DjFE7Xf11URZqWBigcVOQ",
|
||||
name: "EEVblog",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(884000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -1273,7 +1273,7 @@ Channel(
|
|||
id: "UC2DjFE7Xf11URZqWBigcVOQ",
|
||||
name: "EEVblog",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(884000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -1314,7 +1314,7 @@ Channel(
|
|||
id: "UC2DjFE7Xf11URZqWBigcVOQ",
|
||||
name: "EEVblog",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(884000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
|
|
@ -25,7 +25,7 @@ Channel(
|
|||
height: 176,
|
||||
),
|
||||
],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
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",
|
||||
tags: [
|
||||
"electronics",
|
||||
|
@ -109,7 +109,7 @@ Channel(
|
|||
id: "UC2DjFE7Xf11URZqWBigcVOQ",
|
||||
name: "EEVblog",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(881000),
|
||||
)),
|
||||
video_count: Some(2),
|
||||
|
@ -128,7 +128,7 @@ Channel(
|
|||
id: "UC2DjFE7Xf11URZqWBigcVOQ",
|
||||
name: "EEVblog",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(881000),
|
||||
)),
|
||||
video_count: Some(1),
|
||||
|
@ -147,7 +147,7 @@ Channel(
|
|||
id: "UC2DjFE7Xf11URZqWBigcVOQ",
|
||||
name: "EEVblog",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(881000),
|
||||
)),
|
||||
video_count: Some(9),
|
||||
|
@ -166,7 +166,7 @@ Channel(
|
|||
id: "UC2DjFE7Xf11URZqWBigcVOQ",
|
||||
name: "EEVblog",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(881000),
|
||||
)),
|
||||
video_count: Some(2),
|
||||
|
@ -185,7 +185,7 @@ Channel(
|
|||
id: "UC2DjFE7Xf11URZqWBigcVOQ",
|
||||
name: "EEVblog",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(881000),
|
||||
)),
|
||||
video_count: Some(4),
|
||||
|
@ -204,7 +204,7 @@ Channel(
|
|||
id: "UC2DjFE7Xf11URZqWBigcVOQ",
|
||||
name: "EEVblog",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(881000),
|
||||
)),
|
||||
video_count: Some(18),
|
||||
|
@ -223,7 +223,7 @@ Channel(
|
|||
id: "UC2DjFE7Xf11URZqWBigcVOQ",
|
||||
name: "EEVblog",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(881000),
|
||||
)),
|
||||
video_count: Some(3),
|
||||
|
@ -242,7 +242,7 @@ Channel(
|
|||
id: "UC2DjFE7Xf11URZqWBigcVOQ",
|
||||
name: "EEVblog",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(881000),
|
||||
)),
|
||||
video_count: Some(8),
|
||||
|
@ -261,7 +261,7 @@ Channel(
|
|||
id: "UC2DjFE7Xf11URZqWBigcVOQ",
|
||||
name: "EEVblog",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(881000),
|
||||
)),
|
||||
video_count: Some(13),
|
||||
|
@ -280,7 +280,7 @@ Channel(
|
|||
id: "UC2DjFE7Xf11URZqWBigcVOQ",
|
||||
name: "EEVblog",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(881000),
|
||||
)),
|
||||
video_count: Some(9),
|
||||
|
@ -299,7 +299,7 @@ Channel(
|
|||
id: "UC2DjFE7Xf11URZqWBigcVOQ",
|
||||
name: "EEVblog",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(881000),
|
||||
)),
|
||||
video_count: Some(7),
|
||||
|
@ -318,7 +318,7 @@ Channel(
|
|||
id: "UC2DjFE7Xf11URZqWBigcVOQ",
|
||||
name: "EEVblog",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(881000),
|
||||
)),
|
||||
video_count: Some(3),
|
||||
|
@ -337,7 +337,7 @@ Channel(
|
|||
id: "UC2DjFE7Xf11URZqWBigcVOQ",
|
||||
name: "EEVblog",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(881000),
|
||||
)),
|
||||
video_count: Some(8),
|
||||
|
@ -356,7 +356,7 @@ Channel(
|
|||
id: "UC2DjFE7Xf11URZqWBigcVOQ",
|
||||
name: "EEVblog",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(881000),
|
||||
)),
|
||||
video_count: Some(2),
|
||||
|
@ -375,7 +375,7 @@ Channel(
|
|||
id: "UC2DjFE7Xf11URZqWBigcVOQ",
|
||||
name: "EEVblog",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(881000),
|
||||
)),
|
||||
video_count: Some(3),
|
||||
|
@ -394,7 +394,7 @@ Channel(
|
|||
id: "UC2DjFE7Xf11URZqWBigcVOQ",
|
||||
name: "EEVblog",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(881000),
|
||||
)),
|
||||
video_count: Some(10),
|
||||
|
@ -413,7 +413,7 @@ Channel(
|
|||
id: "UC2DjFE7Xf11URZqWBigcVOQ",
|
||||
name: "EEVblog",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(881000),
|
||||
)),
|
||||
video_count: Some(1),
|
||||
|
@ -432,7 +432,7 @@ Channel(
|
|||
id: "UC2DjFE7Xf11URZqWBigcVOQ",
|
||||
name: "EEVblog",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(881000),
|
||||
)),
|
||||
video_count: Some(9),
|
||||
|
@ -451,7 +451,7 @@ Channel(
|
|||
id: "UC2DjFE7Xf11URZqWBigcVOQ",
|
||||
name: "EEVblog",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(881000),
|
||||
)),
|
||||
video_count: Some(16),
|
||||
|
@ -470,7 +470,7 @@ Channel(
|
|||
id: "UC2DjFE7Xf11URZqWBigcVOQ",
|
||||
name: "EEVblog",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(881000),
|
||||
)),
|
||||
video_count: Some(7),
|
||||
|
@ -489,7 +489,7 @@ Channel(
|
|||
id: "UC2DjFE7Xf11URZqWBigcVOQ",
|
||||
name: "EEVblog",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(881000),
|
||||
)),
|
||||
video_count: Some(6),
|
||||
|
@ -508,7 +508,7 @@ Channel(
|
|||
id: "UC2DjFE7Xf11URZqWBigcVOQ",
|
||||
name: "EEVblog",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(881000),
|
||||
)),
|
||||
video_count: Some(12),
|
||||
|
@ -527,7 +527,7 @@ Channel(
|
|||
id: "UC2DjFE7Xf11URZqWBigcVOQ",
|
||||
name: "EEVblog",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(881000),
|
||||
)),
|
||||
video_count: Some(1),
|
||||
|
@ -546,7 +546,7 @@ Channel(
|
|||
id: "UC2DjFE7Xf11URZqWBigcVOQ",
|
||||
name: "EEVblog",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(881000),
|
||||
)),
|
||||
video_count: Some(5),
|
||||
|
@ -565,7 +565,7 @@ Channel(
|
|||
id: "UC2DjFE7Xf11URZqWBigcVOQ",
|
||||
name: "EEVblog",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(881000),
|
||||
)),
|
||||
video_count: Some(2),
|
||||
|
@ -584,7 +584,7 @@ Channel(
|
|||
id: "UC2DjFE7Xf11URZqWBigcVOQ",
|
||||
name: "EEVblog",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(881000),
|
||||
)),
|
||||
video_count: Some(4),
|
||||
|
@ -603,7 +603,7 @@ Channel(
|
|||
id: "UC2DjFE7Xf11URZqWBigcVOQ",
|
||||
name: "EEVblog",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(881000),
|
||||
)),
|
||||
video_count: Some(1),
|
||||
|
@ -622,7 +622,7 @@ Channel(
|
|||
id: "UC2DjFE7Xf11URZqWBigcVOQ",
|
||||
name: "EEVblog",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(881000),
|
||||
)),
|
||||
video_count: Some(2),
|
||||
|
@ -641,7 +641,7 @@ Channel(
|
|||
id: "UC2DjFE7Xf11URZqWBigcVOQ",
|
||||
name: "EEVblog",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(881000),
|
||||
)),
|
||||
video_count: Some(9),
|
||||
|
@ -660,7 +660,7 @@ Channel(
|
|||
id: "UC2DjFE7Xf11URZqWBigcVOQ",
|
||||
name: "EEVblog",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(881000),
|
||||
)),
|
||||
video_count: Some(1),
|
|
@ -1,672 +0,0 @@
|
|||
---
|
||||
source: src/client/channel.rs
|
||||
expression: map_res.c
|
||||
---
|
||||
Channel(
|
||||
id: "UC2DjFE7Xf11URZqWBigcVOQ",
|
||||
name: "EEVblog",
|
||||
handle: Some("@EEVblog"),
|
||||
subscriber_count: Some(952000),
|
||||
video_count: Some(2),
|
||||
avatar: [
|
||||
Thumbnail(
|
||||
url: "https://yt3.googleusercontent.com/ytc/AIdro_l17lYcTcRSydZeQK-RuiSfEeH5eX9m4irSNQj6109v5MQ=s72-c-k-c0x00ffffff-no-rj",
|
||||
width: 72,
|
||||
height: 72,
|
||||
),
|
||||
Thumbnail(
|
||||
url: "https://yt3.googleusercontent.com/ytc/AIdro_l17lYcTcRSydZeQK-RuiSfEeH5eX9m4irSNQj6109v5MQ=s120-c-k-c0x00ffffff-no-rj",
|
||||
width: 120,
|
||||
height: 120,
|
||||
),
|
||||
Thumbnail(
|
||||
url: "https://yt3.googleusercontent.com/ytc/AIdro_l17lYcTcRSydZeQK-RuiSfEeH5eX9m4irSNQj6109v5MQ=s160-c-k-c0x00ffffff-no-rj",
|
||||
width: 160,
|
||||
height: 160,
|
||||
),
|
||||
],
|
||||
verification: verified,
|
||||
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",
|
||||
tags: [
|
||||
"electronics",
|
||||
"engineering",
|
||||
"maker",
|
||||
"hacker",
|
||||
"design",
|
||||
"circuit",
|
||||
"hardware",
|
||||
"pic",
|
||||
"atmel",
|
||||
"oscilloscope",
|
||||
"multimeter",
|
||||
"diy",
|
||||
"hobby",
|
||||
"review",
|
||||
"teardown",
|
||||
"microcontroller",
|
||||
"arduino",
|
||||
"video",
|
||||
"blog",
|
||||
"tutorial",
|
||||
"how-to",
|
||||
"interview",
|
||||
"rant",
|
||||
"industry",
|
||||
"news",
|
||||
"mailbag",
|
||||
"dumpster diving",
|
||||
"debunking",
|
||||
],
|
||||
banner: [
|
||||
Thumbnail(
|
||||
url: "https://yt3.googleusercontent.com/yIJ9ad80n49rK-YUcZLe_8bLmR-aGyg5ybDH_XKIc0GDWrC6s1Wzz8lxnq3_hux_5b6NHPZ9=w1060-fcrop64=1,00005a57ffffa5a8-k-c0xffffffff-no-nd-rj",
|
||||
width: 1060,
|
||||
height: 175,
|
||||
),
|
||||
Thumbnail(
|
||||
url: "https://yt3.googleusercontent.com/yIJ9ad80n49rK-YUcZLe_8bLmR-aGyg5ybDH_XKIc0GDWrC6s1Wzz8lxnq3_hux_5b6NHPZ9=w1138-fcrop64=1,00005a57ffffa5a8-k-c0xffffffff-no-nd-rj",
|
||||
width: 1138,
|
||||
height: 188,
|
||||
),
|
||||
Thumbnail(
|
||||
url: "https://yt3.googleusercontent.com/yIJ9ad80n49rK-YUcZLe_8bLmR-aGyg5ybDH_XKIc0GDWrC6s1Wzz8lxnq3_hux_5b6NHPZ9=w1707-fcrop64=1,00005a57ffffa5a8-k-c0xffffffff-no-nd-rj",
|
||||
width: 1707,
|
||||
height: 283,
|
||||
),
|
||||
Thumbnail(
|
||||
url: "https://yt3.googleusercontent.com/yIJ9ad80n49rK-YUcZLe_8bLmR-aGyg5ybDH_XKIc0GDWrC6s1Wzz8lxnq3_hux_5b6NHPZ9=w2120-fcrop64=1,00005a57ffffa5a8-k-c0xffffffff-no-nd-rj",
|
||||
width: 2120,
|
||||
height: 351,
|
||||
),
|
||||
Thumbnail(
|
||||
url: "https://yt3.googleusercontent.com/yIJ9ad80n49rK-YUcZLe_8bLmR-aGyg5ybDH_XKIc0GDWrC6s1Wzz8lxnq3_hux_5b6NHPZ9=w2276-fcrop64=1,00005a57ffffa5a8-k-c0xffffffff-no-nd-rj",
|
||||
width: 2276,
|
||||
height: 377,
|
||||
),
|
||||
Thumbnail(
|
||||
url: "https://yt3.googleusercontent.com/yIJ9ad80n49rK-YUcZLe_8bLmR-aGyg5ybDH_XKIc0GDWrC6s1Wzz8lxnq3_hux_5b6NHPZ9=w2560-fcrop64=1,00005a57ffffa5a8-k-c0xffffffff-no-nd-rj",
|
||||
width: 2560,
|
||||
height: 424,
|
||||
),
|
||||
],
|
||||
has_shorts: true,
|
||||
has_live: true,
|
||||
visitor_data: None,
|
||||
content: Paginator(
|
||||
count: None,
|
||||
items: [
|
||||
PlaylistItem(
|
||||
id: "PLvOlSehNtuHv268f0mW5m1t_hq_RVGRSA",
|
||||
name: "Jellybean Components Series",
|
||||
thumbnail: [
|
||||
Thumbnail(
|
||||
url: "https://i.ytimg.com/vi/XYdmX8w8xwI/hqdefault.jpg?sqp=-oaymwEXCOADEI4CSFryq4qpAwkIARUAAIhCGAE=&rs=AOn4CLCqmf6TGfDinNXhgU29ZxOkv2u9sQ",
|
||||
width: 480,
|
||||
height: 270,
|
||||
),
|
||||
],
|
||||
channel: Some(ChannelTag(
|
||||
id: "UC2DjFE7Xf11URZqWBigcVOQ",
|
||||
name: "EEVblog",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
subscriber_count: Some(952000),
|
||||
)),
|
||||
video_count: Some(5),
|
||||
),
|
||||
PlaylistItem(
|
||||
id: "PLvOlSehNtuHu46I7nFuUg3LC3PpiWTR4f",
|
||||
name: "Tandy Electronics / Radio Shack & Computers",
|
||||
thumbnail: [
|
||||
Thumbnail(
|
||||
url: "https://i.ytimg.com/vi/uUXxY6gA-7g/hqdefault.jpg?sqp=-oaymwEXCOADEI4CSFryq4qpAwkIARUAAIhCGAE=&rs=AOn4CLAlIVvQ4Axx40Xa_i8F56qmppXEXg",
|
||||
width: 480,
|
||||
height: 270,
|
||||
),
|
||||
],
|
||||
channel: Some(ChannelTag(
|
||||
id: "UC2DjFE7Xf11URZqWBigcVOQ",
|
||||
name: "EEVblog",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
subscriber_count: Some(952000),
|
||||
)),
|
||||
video_count: Some(11),
|
||||
),
|
||||
PlaylistItem(
|
||||
id: "PLvOlSehNtuHuS01_RNCnvpzyk7bycYCmM",
|
||||
name: "Open Source Hardware",
|
||||
thumbnail: [
|
||||
Thumbnail(
|
||||
url: "https://i.ytimg.com/vi/m_8jh_MpWBE/hqdefault.jpg?sqp=-oaymwEXCOADEI4CSFryq4qpAwkIARUAAIhCGAE=&rs=AOn4CLBx6U5iikp5rSO78dIWdy1RQ_BLNQ",
|
||||
width: 480,
|
||||
height: 270,
|
||||
),
|
||||
],
|
||||
channel: Some(ChannelTag(
|
||||
id: "UC2DjFE7Xf11URZqWBigcVOQ",
|
||||
name: "EEVblog",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
subscriber_count: Some(952000),
|
||||
)),
|
||||
video_count: Some(4),
|
||||
),
|
||||
PlaylistItem(
|
||||
id: "PLvOlSehNtuHuwwQ1fpquOJuA5MSfD4iD6",
|
||||
name: "Fluke Multimeters",
|
||||
thumbnail: [
|
||||
Thumbnail(
|
||||
url: "https://i.ytimg.com/vi/ymJc5oxthlw/hqdefault.jpg?sqp=-oaymwEXCOADEI4CSFryq4qpAwkIARUAAIhCGAE=&rs=AOn4CLDAOiw39aJajjAdroLnuj_fh60Ryw",
|
||||
width: 480,
|
||||
height: 270,
|
||||
),
|
||||
],
|
||||
channel: Some(ChannelTag(
|
||||
id: "UC2DjFE7Xf11URZqWBigcVOQ",
|
||||
name: "EEVblog",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
subscriber_count: Some(952000),
|
||||
)),
|
||||
video_count: Some(22),
|
||||
),
|
||||
PlaylistItem(
|
||||
id: "PLvOlSehNtuHs2LwEdDwTp3n7mxb-MyBbo",
|
||||
name: "EEVacademy Digital Design Tutorial Series",
|
||||
thumbnail: [
|
||||
Thumbnail(
|
||||
url: "https://i.ytimg.com/vi/lJ3q9RHIatU/hqdefault.jpg?sqp=-oaymwExCOADEI4CSFryq4qpAyMIARUAAIhCGAHwAQH4Af4JgALQBYoCDAgAEAEYQyBXKGUwDw==&rs=AOn4CLBaaQaTJzi7H-zjwSsTlNJdBsyqvQ",
|
||||
width: 480,
|
||||
height: 270,
|
||||
),
|
||||
],
|
||||
channel: Some(ChannelTag(
|
||||
id: "UC2DjFE7Xf11URZqWBigcVOQ",
|
||||
name: "EEVblog",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
subscriber_count: Some(952000),
|
||||
)),
|
||||
video_count: Some(5),
|
||||
),
|
||||
PlaylistItem(
|
||||
id: "PLvOlSehNtuHu2v8THrRMt8E9ziHtRXPm7",
|
||||
name: "AI / ChatGPT",
|
||||
thumbnail: [
|
||||
Thumbnail(
|
||||
url: "https://i.ytimg.com/vi/g5_Ts9SWbYs/hqdefault.jpg?sqp=-oaymwEXCOADEI4CSFryq4qpAwkIARUAAIhCGAE=&rs=AOn4CLBmZPW6EiAvTCsI86BFg4BxXLj66A",
|
||||
width: 480,
|
||||
height: 270,
|
||||
),
|
||||
],
|
||||
channel: Some(ChannelTag(
|
||||
id: "UC2DjFE7Xf11URZqWBigcVOQ",
|
||||
name: "EEVblog",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
subscriber_count: Some(952000),
|
||||
)),
|
||||
video_count: Some(3),
|
||||
),
|
||||
PlaylistItem(
|
||||
id: "PLvOlSehNtuHvXuXRmoBUys09Dwi1heNii",
|
||||
name: "Shorts",
|
||||
thumbnail: [
|
||||
Thumbnail(
|
||||
url: "https://i.ytimg.com/vi/ndvJtQ8nxV4/hqdefault.jpg?sqp=-oaymwExCOADEI4CSFryq4qpAyMIARUAAIhCGAHwAQH4AbYIgAKAD4oCDAgAEAEYNyBTKH8wDw==&rs=AOn4CLDD0qOLs38KPJtqdG6zCeVLQMf62Q",
|
||||
width: 480,
|
||||
height: 270,
|
||||
),
|
||||
],
|
||||
channel: Some(ChannelTag(
|
||||
id: "UC2DjFE7Xf11URZqWBigcVOQ",
|
||||
name: "EEVblog",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
subscriber_count: Some(952000),
|
||||
)),
|
||||
video_count: Some(1),
|
||||
),
|
||||
PlaylistItem(
|
||||
id: "PLvOlSehNtuHv3gxNg5BGoZJJu9htoAGB6",
|
||||
name: "Microcontrollers",
|
||||
thumbnail: [
|
||||
Thumbnail(
|
||||
url: "https://i.ytimg.com/vi/L9Wrv7nW-S8/hqdefault.jpg?sqp=-oaymwEXCOADEI4CSFryq4qpAwkIARUAAIhCGAE=&rs=AOn4CLDiAT5izyig1ntMSUhvSOVuYSsG1Q",
|
||||
width: 480,
|
||||
height: 270,
|
||||
),
|
||||
],
|
||||
channel: Some(ChannelTag(
|
||||
id: "UC2DjFE7Xf11URZqWBigcVOQ",
|
||||
name: "EEVblog",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
subscriber_count: Some(952000),
|
||||
)),
|
||||
video_count: Some(1),
|
||||
),
|
||||
PlaylistItem(
|
||||
id: "PLvOlSehNtuHvllTQ-vwvY26E3Bvrov93Y",
|
||||
name: "Bypass Capacitors",
|
||||
thumbnail: [
|
||||
Thumbnail(
|
||||
url: "https://i.ytimg.com/vi/1xicZF9glH0/hqdefault.jpg?sqp=-oaymwEXCOADEI4CSFryq4qpAwkIARUAAIhCGAE=&rs=AOn4CLAFb2FcbpdtAG1xLjmdkdIm1hFvgA",
|
||||
width: 480,
|
||||
height: 270,
|
||||
),
|
||||
],
|
||||
channel: Some(ChannelTag(
|
||||
id: "UC2DjFE7Xf11URZqWBigcVOQ",
|
||||
name: "EEVblog",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
subscriber_count: Some(952000),
|
||||
)),
|
||||
video_count: Some(4),
|
||||
),
|
||||
PlaylistItem(
|
||||
id: "PLvOlSehNtuHtOV3AEwhuea4TnviddKfAj",
|
||||
name: "MacGyver Project",
|
||||
thumbnail: [
|
||||
Thumbnail(
|
||||
url: "https://i.ytimg.com/vi/4yosozyeIP4/hqdefault.jpg?sqp=-oaymwEXCOADEI4CSFryq4qpAwkIARUAAIhCGAE=&rs=AOn4CLAkwsCiJjFkWhYxtcg5NgfnQbkZrA",
|
||||
width: 480,
|
||||
height: 270,
|
||||
),
|
||||
],
|
||||
channel: Some(ChannelTag(
|
||||
id: "UC2DjFE7Xf11URZqWBigcVOQ",
|
||||
name: "EEVblog",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
subscriber_count: Some(952000),
|
||||
)),
|
||||
video_count: Some(3),
|
||||
),
|
||||
PlaylistItem(
|
||||
id: "PLvOlSehNtuHuvHE5GQrQJxWXHdmW2l5IF",
|
||||
name: "Calculators",
|
||||
thumbnail: [
|
||||
Thumbnail(
|
||||
url: "https://i.ytimg.com/vi/S3R4r2xvVYQ/hqdefault.jpg?sqp=-oaymwEXCOADEI4CSFryq4qpAwkIARUAAIhCGAE=&rs=AOn4CLB7HH5drG-33c1SyRe9kyZBrXvm3A",
|
||||
width: 480,
|
||||
height: 270,
|
||||
),
|
||||
],
|
||||
channel: Some(ChannelTag(
|
||||
id: "UC2DjFE7Xf11URZqWBigcVOQ",
|
||||
name: "EEVblog",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
subscriber_count: Some(952000),
|
||||
)),
|
||||
video_count: Some(1),
|
||||
),
|
||||
PlaylistItem(
|
||||
id: "PLvOlSehNtuHs6wRwVSaErU0BEnLiHfnKJ",
|
||||
name: "BM235",
|
||||
thumbnail: [
|
||||
Thumbnail(
|
||||
url: "https://i.ytimg.com/vi/WPyEFB4cHkA/hqdefault.jpg?sqp=-oaymwEXCOADEI4CSFryq4qpAwkIARUAAIhCGAE=&rs=AOn4CLAzBuQFV8T9hM8adlPvv58C9TeDug",
|
||||
width: 480,
|
||||
height: 270,
|
||||
),
|
||||
],
|
||||
channel: Some(ChannelTag(
|
||||
id: "UC2DjFE7Xf11URZqWBigcVOQ",
|
||||
name: "EEVblog",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
subscriber_count: Some(952000),
|
||||
)),
|
||||
video_count: Some(9),
|
||||
),
|
||||
PlaylistItem(
|
||||
id: "PLvOlSehNtuHu4k0ZkKFLsysSB5iava6Qu",
|
||||
name: "Vibration Measurement",
|
||||
thumbnail: [
|
||||
Thumbnail(
|
||||
url: "https://i.ytimg.com/vi/uus_cpZiqsU/hqdefault.jpg?sqp=-oaymwEXCOADEI4CSFryq4qpAwkIARUAAIhCGAE=&rs=AOn4CLCqdsjWVFaLOkEcXgbZD2Eca8MnuQ",
|
||||
width: 480,
|
||||
height: 270,
|
||||
),
|
||||
],
|
||||
channel: Some(ChannelTag(
|
||||
id: "UC2DjFE7Xf11URZqWBigcVOQ",
|
||||
name: "EEVblog",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
subscriber_count: Some(952000),
|
||||
)),
|
||||
video_count: Some(3),
|
||||
),
|
||||
PlaylistItem(
|
||||
id: "PLvOlSehNtuHtdQF-m5UFZ5GEjABadI3kI",
|
||||
name: "Component Selection",
|
||||
thumbnail: [
|
||||
Thumbnail(
|
||||
url: "https://i.ytimg.com/vi/uq1DMWtjL2U/hqdefault.jpg?sqp=-oaymwEXCOADEI4CSFryq4qpAwkIARUAAIhCGAE=&rs=AOn4CLAbgb1Jdb5P69JGdZQ-a8asLLyYdA",
|
||||
width: 480,
|
||||
height: 270,
|
||||
),
|
||||
],
|
||||
channel: Some(ChannelTag(
|
||||
id: "UC2DjFE7Xf11URZqWBigcVOQ",
|
||||
name: "EEVblog",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
subscriber_count: Some(952000),
|
||||
)),
|
||||
video_count: Some(6),
|
||||
),
|
||||
PlaylistItem(
|
||||
id: "PLvOlSehNtuHtlndPUSOPgsujUdq1c5Mr9",
|
||||
name: "Solar Roadways",
|
||||
thumbnail: [
|
||||
Thumbnail(
|
||||
url: "https://i.ytimg.com/vi/oIImmlfCyzo/hqdefault.jpg?sqp=-oaymwEXCOADEI4CSFryq4qpAwkIARUAAIhCGAE=&rs=AOn4CLBxApgyGu3dNXRGoqLctVUnESpEIA",
|
||||
width: 480,
|
||||
height: 270,
|
||||
),
|
||||
],
|
||||
channel: Some(ChannelTag(
|
||||
id: "UC2DjFE7Xf11URZqWBigcVOQ",
|
||||
name: "EEVblog",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
subscriber_count: Some(952000),
|
||||
)),
|
||||
video_count: Some(23),
|
||||
),
|
||||
PlaylistItem(
|
||||
id: "PLvOlSehNtuHvD6M_7WeN071OVsZFE0_q-",
|
||||
name: "Electronics Tutorials - AC Circuit Theory Series",
|
||||
thumbnail: [
|
||||
Thumbnail(
|
||||
url: "https://i.ytimg.com/vi/rrPtvYYJ2-g/hqdefault.jpg?sqp=-oaymwEXCOADEI4CSFryq4qpAwkIARUAAIhCGAE=&rs=AOn4CLBEVc71xxSjJ-xlA_dDQaYIjdHyUw",
|
||||
width: 480,
|
||||
height: 270,
|
||||
),
|
||||
],
|
||||
channel: Some(ChannelTag(
|
||||
id: "UC2DjFE7Xf11URZqWBigcVOQ",
|
||||
name: "EEVblog",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
subscriber_count: Some(952000),
|
||||
)),
|
||||
video_count: Some(3),
|
||||
),
|
||||
PlaylistItem(
|
||||
id: "PLvOlSehNtuHtVLq2MDPIz82BWMIZcuwhK",
|
||||
name: "Electronics Tutorial - DC Fundamentals",
|
||||
thumbnail: [
|
||||
Thumbnail(
|
||||
url: "https://i.ytimg.com/vi/xSRe_4TQbuo/hqdefault.jpg?sqp=-oaymwEXCOADEI4CSFryq4qpAwkIARUAAIhCGAE=&rs=AOn4CLDP4V24_MG6vzvUZsHep9WFSCCY6Q",
|
||||
width: 480,
|
||||
height: 270,
|
||||
),
|
||||
],
|
||||
channel: Some(ChannelTag(
|
||||
id: "UC2DjFE7Xf11URZqWBigcVOQ",
|
||||
name: "EEVblog",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
subscriber_count: Some(952000),
|
||||
)),
|
||||
video_count: Some(8),
|
||||
),
|
||||
PlaylistItem(
|
||||
id: "PLvOlSehNtuHvIDfW3x2p4BY6l4RYgfBJE",
|
||||
name: "Oscilloscope Probing",
|
||||
thumbnail: [
|
||||
Thumbnail(
|
||||
url: "https://i.ytimg.com/vi/OiAmER1OJh4/hqdefault.jpg?sqp=-oaymwEXCOADEI4CSFryq4qpAwkIARUAAIhCGAE=&rs=AOn4CLAXeGAvEc8y3pEsPUxWdsNIP9UmPw",
|
||||
width: 480,
|
||||
height: 270,
|
||||
),
|
||||
],
|
||||
channel: Some(ChannelTag(
|
||||
id: "UC2DjFE7Xf11URZqWBigcVOQ",
|
||||
name: "EEVblog",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
subscriber_count: Some(952000),
|
||||
)),
|
||||
video_count: Some(14),
|
||||
),
|
||||
PlaylistItem(
|
||||
id: "PLvOlSehNtuHu6Jjb8U82eKQfvKhJVl0Bu",
|
||||
name: "Thermal Design",
|
||||
thumbnail: [
|
||||
Thumbnail(
|
||||
url: "https://i.ytimg.com/vi/8ruFVmxf0zs/hqdefault.jpg?sqp=-oaymwExCOADEI4CSFryq4qpAyMIARUAAIhCGAHwAQH4Af4JgALQBYoCDAgAEAEYfyA1KDUwDw==&rs=AOn4CLD6PMawyYXKe8KT1-Y6vWjQc2xIDw",
|
||||
width: 480,
|
||||
height: 270,
|
||||
),
|
||||
],
|
||||
channel: Some(ChannelTag(
|
||||
id: "UC2DjFE7Xf11URZqWBigcVOQ",
|
||||
name: "EEVblog",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
subscriber_count: Some(952000),
|
||||
)),
|
||||
video_count: Some(9),
|
||||
),
|
||||
PlaylistItem(
|
||||
id: "PLvOlSehNtuHs-X2Awg33PCBNrP2BGFVhC",
|
||||
name: "Electric Cars",
|
||||
thumbnail: [
|
||||
Thumbnail(
|
||||
url: "https://i.ytimg.com/vi/CPcZm1Tu5VI/hqdefault.jpg?sqp=-oaymwEXCOADEI4CSFryq4qpAwkIARUAAIhCGAE=&rs=AOn4CLCsm8De0QaHPaeCZqxMp_F464fWzg",
|
||||
width: 480,
|
||||
height: 270,
|
||||
),
|
||||
],
|
||||
channel: Some(ChannelTag(
|
||||
id: "UC2DjFE7Xf11URZqWBigcVOQ",
|
||||
name: "EEVblog",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
subscriber_count: Some(952000),
|
||||
)),
|
||||
video_count: Some(9),
|
||||
),
|
||||
PlaylistItem(
|
||||
id: "PLvOlSehNtuHuLODLTeq3PM-OJRP2nzNUa",
|
||||
name: "Designing a better uCurrent",
|
||||
thumbnail: [
|
||||
Thumbnail(
|
||||
url: "https://i.ytimg.com/vi/0AEVilxXAAo/hqdefault.jpg?sqp=-oaymwEXCOADEI4CSFryq4qpAwkIARUAAIhCGAE=&rs=AOn4CLCjotFuRjPPBHd2LWzt3lviPj9HaA",
|
||||
width: 480,
|
||||
height: 270,
|
||||
),
|
||||
],
|
||||
channel: Some(ChannelTag(
|
||||
id: "UC2DjFE7Xf11URZqWBigcVOQ",
|
||||
name: "EEVblog",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
subscriber_count: Some(952000),
|
||||
)),
|
||||
video_count: Some(3),
|
||||
),
|
||||
PlaylistItem(
|
||||
id: "PLvOlSehNtuHtvTKP4RTNW1-08Kmzy1pvA",
|
||||
name: "EMC Compliance & Measurement",
|
||||
thumbnail: [
|
||||
Thumbnail(
|
||||
url: "https://i.ytimg.com/vi/lYmfVMWbIHQ/hqdefault.jpg?sqp=-oaymwEXCOADEI4CSFryq4qpAwkIARUAAIhCGAE=&rs=AOn4CLBtygEqMXx7Lwe5SuBWt2q0CSahYA",
|
||||
width: 480,
|
||||
height: 270,
|
||||
),
|
||||
],
|
||||
channel: Some(ChannelTag(
|
||||
id: "UC2DjFE7Xf11URZqWBigcVOQ",
|
||||
name: "EEVblog",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
subscriber_count: Some(952000),
|
||||
)),
|
||||
video_count: Some(8),
|
||||
),
|
||||
PlaylistItem(
|
||||
id: "PLvOlSehNtuHuUTpCrTVX7BdU68l2aVqMv",
|
||||
name: "Power Counter Display Project",
|
||||
thumbnail: [
|
||||
Thumbnail(
|
||||
url: "https://i.ytimg.com/vi/nTpE1Nw3Yy4/hqdefault.jpg?sqp=-oaymwEXCOADEI4CSFryq4qpAwkIARUAAIhCGAE=&rs=AOn4CLAbPl28_i7isizY6A1t2_c6gV8BAQ",
|
||||
width: 480,
|
||||
height: 270,
|
||||
),
|
||||
],
|
||||
channel: Some(ChannelTag(
|
||||
id: "UC2DjFE7Xf11URZqWBigcVOQ",
|
||||
name: "EEVblog",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
subscriber_count: Some(952000),
|
||||
)),
|
||||
video_count: Some(2),
|
||||
),
|
||||
PlaylistItem(
|
||||
id: "PLvOlSehNtuHvm120Tq40nKrM5SUBlolN3",
|
||||
name: "Live - Ask Dave",
|
||||
thumbnail: [
|
||||
Thumbnail(
|
||||
url: "https://i.ytimg.com/vi/gQ7TTuiDH1M/hqdefault.jpg?sqp=-oaymwEXCOADEI4CSFryq4qpAwkIARUAAIhCGAE=&rs=AOn4CLBMnucUil90WeDSIeFz8mZCOtEv9g",
|
||||
width: 480,
|
||||
height: 270,
|
||||
),
|
||||
],
|
||||
channel: Some(ChannelTag(
|
||||
id: "UC2DjFE7Xf11URZqWBigcVOQ",
|
||||
name: "EEVblog",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
subscriber_count: Some(952000),
|
||||
)),
|
||||
video_count: Some(3),
|
||||
),
|
||||
PlaylistItem(
|
||||
id: "PLvOlSehNtuHsiF93KOLoF1KAHArmIW9lC",
|
||||
name: "Padauk Microcontroller",
|
||||
thumbnail: [
|
||||
Thumbnail(
|
||||
url: "https://i.ytimg.com/vi/r45r4rV5JOI/hqdefault.jpg?sqp=-oaymwEXCOADEI4CSFryq4qpAwkIARUAAIhCGAE=&rs=AOn4CLCn4kGWcjBOhk3vN8QPMDa9L3mkKA",
|
||||
width: 480,
|
||||
height: 270,
|
||||
),
|
||||
],
|
||||
channel: Some(ChannelTag(
|
||||
id: "UC2DjFE7Xf11URZqWBigcVOQ",
|
||||
name: "EEVblog",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
subscriber_count: Some(952000),
|
||||
)),
|
||||
video_count: Some(10),
|
||||
),
|
||||
PlaylistItem(
|
||||
id: "PLvOlSehNtuHvxTzBLwUFw4My4rtrNFzED",
|
||||
name: "Other Debunking Videos",
|
||||
thumbnail: [
|
||||
Thumbnail(
|
||||
url: "https://i.ytimg.com/vi/WopuF9vD7KE/hqdefault.jpg?sqp=-oaymwEXCOADEI4CSFryq4qpAwkIARUAAIhCGAE=&rs=AOn4CLBv5buh3qMs4feQaPj6Fy6bxl_vuA",
|
||||
width: 480,
|
||||
height: 270,
|
||||
),
|
||||
],
|
||||
channel: Some(ChannelTag(
|
||||
id: "UC2DjFE7Xf11URZqWBigcVOQ",
|
||||
name: "EEVblog",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
subscriber_count: Some(952000),
|
||||
)),
|
||||
video_count: Some(1),
|
||||
),
|
||||
PlaylistItem(
|
||||
id: "PLvOlSehNtuHt2pJ7X5tumuM4Wa3r1OC7Q",
|
||||
name: "Audio & Speakers",
|
||||
thumbnail: [
|
||||
Thumbnail(
|
||||
url: "https://i.ytimg.com/vi/qHbkw0Gm7pk/hqdefault.jpg?sqp=-oaymwEXCOADEI4CSFryq4qpAwkIARUAAIhCGAE=&rs=AOn4CLCJBYXTDttGHTm51j3bfwqxOqVFig",
|
||||
width: 480,
|
||||
height: 270,
|
||||
),
|
||||
],
|
||||
channel: Some(ChannelTag(
|
||||
id: "UC2DjFE7Xf11URZqWBigcVOQ",
|
||||
name: "EEVblog",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
subscriber_count: Some(952000),
|
||||
)),
|
||||
video_count: Some(9),
|
||||
),
|
||||
PlaylistItem(
|
||||
id: "PLvOlSehNtuHtX7OearWdmqGzqiu4DHKWi",
|
||||
name: "Cameras",
|
||||
thumbnail: [
|
||||
Thumbnail(
|
||||
url: "https://i.ytimg.com/vi/g9umAQ1-an4/hqdefault.jpg?sqp=-oaymwEXCOADEI4CSFryq4qpAwkIARUAAIhCGAE=&rs=AOn4CLCB5jNm9U-rypnpthK_N321LpYWew",
|
||||
width: 480,
|
||||
height: 270,
|
||||
),
|
||||
],
|
||||
channel: Some(ChannelTag(
|
||||
id: "UC2DjFE7Xf11URZqWBigcVOQ",
|
||||
name: "EEVblog",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
subscriber_count: Some(952000),
|
||||
)),
|
||||
video_count: Some(16),
|
||||
),
|
||||
PlaylistItem(
|
||||
id: "PLvOlSehNtuHu-TaNRp27_PiXjBG5wY9Gv",
|
||||
name: "Cryptocurrency",
|
||||
thumbnail: [
|
||||
Thumbnail(
|
||||
url: "https://i.ytimg.com/vi/ibPgfzd9zd8/hqdefault.jpg?sqp=-oaymwEXCOADEI4CSFryq4qpAwkIARUAAIhCGAE=&rs=AOn4CLDe3IXT88HR3XxnxfqrpAxh6pfYMg",
|
||||
width: 480,
|
||||
height: 270,
|
||||
),
|
||||
],
|
||||
channel: Some(ChannelTag(
|
||||
id: "UC2DjFE7Xf11URZqWBigcVOQ",
|
||||
name: "EEVblog",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
subscriber_count: Some(952000),
|
||||
)),
|
||||
video_count: Some(7),
|
||||
),
|
||||
PlaylistItem(
|
||||
id: "PLvOlSehNtuHvmK-VGcZ33ZuATmcNB8tvH",
|
||||
name: "LCD Tutorial",
|
||||
thumbnail: [
|
||||
Thumbnail(
|
||||
url: "https://i.ytimg.com/vi/ZYvxgl-9tNM/hqdefault.jpg?sqp=-oaymwEXCOADEI4CSFryq4qpAwkIARUAAIhCGAE=&rs=AOn4CLDv2WT4Chl1_H2G43AjfSFpPcKVoA",
|
||||
width: 480,
|
||||
height: 270,
|
||||
),
|
||||
],
|
||||
channel: Some(ChannelTag(
|
||||
id: "UC2DjFE7Xf11URZqWBigcVOQ",
|
||||
name: "EEVblog",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
subscriber_count: Some(952000),
|
||||
)),
|
||||
video_count: Some(6),
|
||||
),
|
||||
],
|
||||
ctoken: Some("4qmFsgLCARIYVUMyRGpGRTdYZjExVVJacVdCaWdjVk9RGnRFZ2x3YkdGNWJHbHpkSE1ZQXlBQk1BRTRBZW9EUEVOblRrUlJhbEZUU2tKSmFWVkZlREpVTW5oVVdsZG9UMlJJVmtsa2JURk1URlphU0ZreGIzcE5NWEF4VVZaU2RGa3dOVU5QU0ZJeVUwTm5PQSUzRCUzRJoCL2Jyb3dzZS1mZWVkVUMyRGpGRTdYZjExVVJacVdCaWdjVk9RcGxheWxpc3RzMTA0"),
|
||||
endpoint: browse,
|
||||
),
|
||||
)
|
|
@ -25,7 +25,7 @@ Channel(
|
|||
height: 176,
|
||||
),
|
||||
],
|
||||
verification: verified,
|
||||
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: [],
|
||||
banner: [
|
||||
|
@ -81,7 +81,7 @@ Channel(
|
|||
id: "UCh8gHdtzO2tXd593_bjErWg",
|
||||
name: "Doobydobap",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(3360000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -107,7 +107,7 @@ Channel(
|
|||
id: "UCh8gHdtzO2tXd593_bjErWg",
|
||||
name: "Doobydobap",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(3360000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -133,7 +133,7 @@ Channel(
|
|||
id: "UCh8gHdtzO2tXd593_bjErWg",
|
||||
name: "Doobydobap",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(3360000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -159,7 +159,7 @@ Channel(
|
|||
id: "UCh8gHdtzO2tXd593_bjErWg",
|
||||
name: "Doobydobap",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(3360000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -185,7 +185,7 @@ Channel(
|
|||
id: "UCh8gHdtzO2tXd593_bjErWg",
|
||||
name: "Doobydobap",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(3360000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -211,7 +211,7 @@ Channel(
|
|||
id: "UCh8gHdtzO2tXd593_bjErWg",
|
||||
name: "Doobydobap",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(3360000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -237,7 +237,7 @@ Channel(
|
|||
id: "UCh8gHdtzO2tXd593_bjErWg",
|
||||
name: "Doobydobap",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(3360000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -263,7 +263,7 @@ Channel(
|
|||
id: "UCh8gHdtzO2tXd593_bjErWg",
|
||||
name: "Doobydobap",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(3360000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -289,7 +289,7 @@ Channel(
|
|||
id: "UCh8gHdtzO2tXd593_bjErWg",
|
||||
name: "Doobydobap",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(3360000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -315,7 +315,7 @@ Channel(
|
|||
id: "UCh8gHdtzO2tXd593_bjErWg",
|
||||
name: "Doobydobap",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(3360000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -341,7 +341,7 @@ Channel(
|
|||
id: "UCh8gHdtzO2tXd593_bjErWg",
|
||||
name: "Doobydobap",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(3360000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -367,7 +367,7 @@ Channel(
|
|||
id: "UCh8gHdtzO2tXd593_bjErWg",
|
||||
name: "Doobydobap",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(3360000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -393,7 +393,7 @@ Channel(
|
|||
id: "UCh8gHdtzO2tXd593_bjErWg",
|
||||
name: "Doobydobap",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(3360000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -419,7 +419,7 @@ Channel(
|
|||
id: "UCh8gHdtzO2tXd593_bjErWg",
|
||||
name: "Doobydobap",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(3360000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -445,7 +445,7 @@ Channel(
|
|||
id: "UCh8gHdtzO2tXd593_bjErWg",
|
||||
name: "Doobydobap",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(3360000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -471,7 +471,7 @@ Channel(
|
|||
id: "UCh8gHdtzO2tXd593_bjErWg",
|
||||
name: "Doobydobap",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(3360000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -497,7 +497,7 @@ Channel(
|
|||
id: "UCh8gHdtzO2tXd593_bjErWg",
|
||||
name: "Doobydobap",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(3360000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -523,7 +523,7 @@ Channel(
|
|||
id: "UCh8gHdtzO2tXd593_bjErWg",
|
||||
name: "Doobydobap",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(3360000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -549,7 +549,7 @@ Channel(
|
|||
id: "UCh8gHdtzO2tXd593_bjErWg",
|
||||
name: "Doobydobap",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(3360000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -575,7 +575,7 @@ Channel(
|
|||
id: "UCh8gHdtzO2tXd593_bjErWg",
|
||||
name: "Doobydobap",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(3360000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -601,7 +601,7 @@ Channel(
|
|||
id: "UCh8gHdtzO2tXd593_bjErWg",
|
||||
name: "Doobydobap",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(3360000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -627,7 +627,7 @@ Channel(
|
|||
id: "UCh8gHdtzO2tXd593_bjErWg",
|
||||
name: "Doobydobap",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(3360000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -653,7 +653,7 @@ Channel(
|
|||
id: "UCh8gHdtzO2tXd593_bjErWg",
|
||||
name: "Doobydobap",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(3360000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -679,7 +679,7 @@ Channel(
|
|||
id: "UCh8gHdtzO2tXd593_bjErWg",
|
||||
name: "Doobydobap",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(3360000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -705,7 +705,7 @@ Channel(
|
|||
id: "UCh8gHdtzO2tXd593_bjErWg",
|
||||
name: "Doobydobap",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(3360000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -731,7 +731,7 @@ Channel(
|
|||
id: "UCh8gHdtzO2tXd593_bjErWg",
|
||||
name: "Doobydobap",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(3360000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -757,7 +757,7 @@ Channel(
|
|||
id: "UCh8gHdtzO2tXd593_bjErWg",
|
||||
name: "Doobydobap",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(3360000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -783,7 +783,7 @@ Channel(
|
|||
id: "UCh8gHdtzO2tXd593_bjErWg",
|
||||
name: "Doobydobap",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(3360000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -809,7 +809,7 @@ Channel(
|
|||
id: "UCh8gHdtzO2tXd593_bjErWg",
|
||||
name: "Doobydobap",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(3360000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -835,7 +835,7 @@ Channel(
|
|||
id: "UCh8gHdtzO2tXd593_bjErWg",
|
||||
name: "Doobydobap",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(3360000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -861,7 +861,7 @@ Channel(
|
|||
id: "UCh8gHdtzO2tXd593_bjErWg",
|
||||
name: "Doobydobap",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(3360000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -887,7 +887,7 @@ Channel(
|
|||
id: "UCh8gHdtzO2tXd593_bjErWg",
|
||||
name: "Doobydobap",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(3360000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -913,7 +913,7 @@ Channel(
|
|||
id: "UCh8gHdtzO2tXd593_bjErWg",
|
||||
name: "Doobydobap",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(3360000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -939,7 +939,7 @@ Channel(
|
|||
id: "UCh8gHdtzO2tXd593_bjErWg",
|
||||
name: "Doobydobap",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(3360000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -965,7 +965,7 @@ Channel(
|
|||
id: "UCh8gHdtzO2tXd593_bjErWg",
|
||||
name: "Doobydobap",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(3360000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -991,7 +991,7 @@ Channel(
|
|||
id: "UCh8gHdtzO2tXd593_bjErWg",
|
||||
name: "Doobydobap",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(3360000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -1017,7 +1017,7 @@ Channel(
|
|||
id: "UCh8gHdtzO2tXd593_bjErWg",
|
||||
name: "Doobydobap",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(3360000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -1043,7 +1043,7 @@ Channel(
|
|||
id: "UCh8gHdtzO2tXd593_bjErWg",
|
||||
name: "Doobydobap",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(3360000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -1069,7 +1069,7 @@ Channel(
|
|||
id: "UCh8gHdtzO2tXd593_bjErWg",
|
||||
name: "Doobydobap",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(3360000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -1095,7 +1095,7 @@ Channel(
|
|||
id: "UCh8gHdtzO2tXd593_bjErWg",
|
||||
name: "Doobydobap",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(3360000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -1121,7 +1121,7 @@ Channel(
|
|||
id: "UCh8gHdtzO2tXd593_bjErWg",
|
||||
name: "Doobydobap",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(3360000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -1147,7 +1147,7 @@ Channel(
|
|||
id: "UCh8gHdtzO2tXd593_bjErWg",
|
||||
name: "Doobydobap",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(3360000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -1173,7 +1173,7 @@ Channel(
|
|||
id: "UCh8gHdtzO2tXd593_bjErWg",
|
||||
name: "Doobydobap",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(3360000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -1199,7 +1199,7 @@ Channel(
|
|||
id: "UCh8gHdtzO2tXd593_bjErWg",
|
||||
name: "Doobydobap",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(3360000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -1225,7 +1225,7 @@ Channel(
|
|||
id: "UCh8gHdtzO2tXd593_bjErWg",
|
||||
name: "Doobydobap",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(3360000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -1251,7 +1251,7 @@ Channel(
|
|||
id: "UCh8gHdtzO2tXd593_bjErWg",
|
||||
name: "Doobydobap",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(3360000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -1277,7 +1277,7 @@ Channel(
|
|||
id: "UCh8gHdtzO2tXd593_bjErWg",
|
||||
name: "Doobydobap",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(3360000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -1303,7 +1303,7 @@ Channel(
|
|||
id: "UCh8gHdtzO2tXd593_bjErWg",
|
||||
name: "Doobydobap",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(3360000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
|
|
@ -25,7 +25,7 @@ Channel(
|
|||
height: 160,
|
||||
),
|
||||
],
|
||||
verification: verified,
|
||||
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: [],
|
||||
banner: [
|
||||
|
@ -81,7 +81,7 @@ Channel(
|
|||
id: "UCh8gHdtzO2tXd593_bjErWg",
|
||||
name: "Doobydobap",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(3740000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -107,7 +107,7 @@ Channel(
|
|||
id: "UCh8gHdtzO2tXd593_bjErWg",
|
||||
name: "Doobydobap",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(3740000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -133,7 +133,7 @@ Channel(
|
|||
id: "UCh8gHdtzO2tXd593_bjErWg",
|
||||
name: "Doobydobap",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(3740000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -159,7 +159,7 @@ Channel(
|
|||
id: "UCh8gHdtzO2tXd593_bjErWg",
|
||||
name: "Doobydobap",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(3740000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -185,7 +185,7 @@ Channel(
|
|||
id: "UCh8gHdtzO2tXd593_bjErWg",
|
||||
name: "Doobydobap",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(3740000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -211,7 +211,7 @@ Channel(
|
|||
id: "UCh8gHdtzO2tXd593_bjErWg",
|
||||
name: "Doobydobap",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(3740000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -237,7 +237,7 @@ Channel(
|
|||
id: "UCh8gHdtzO2tXd593_bjErWg",
|
||||
name: "Doobydobap",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(3740000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -263,7 +263,7 @@ Channel(
|
|||
id: "UCh8gHdtzO2tXd593_bjErWg",
|
||||
name: "Doobydobap",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(3740000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -289,7 +289,7 @@ Channel(
|
|||
id: "UCh8gHdtzO2tXd593_bjErWg",
|
||||
name: "Doobydobap",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(3740000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -315,7 +315,7 @@ Channel(
|
|||
id: "UCh8gHdtzO2tXd593_bjErWg",
|
||||
name: "Doobydobap",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(3740000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -341,7 +341,7 @@ Channel(
|
|||
id: "UCh8gHdtzO2tXd593_bjErWg",
|
||||
name: "Doobydobap",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(3740000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -367,7 +367,7 @@ Channel(
|
|||
id: "UCh8gHdtzO2tXd593_bjErWg",
|
||||
name: "Doobydobap",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(3740000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -393,7 +393,7 @@ Channel(
|
|||
id: "UCh8gHdtzO2tXd593_bjErWg",
|
||||
name: "Doobydobap",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(3740000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -419,7 +419,7 @@ Channel(
|
|||
id: "UCh8gHdtzO2tXd593_bjErWg",
|
||||
name: "Doobydobap",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(3740000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -445,7 +445,7 @@ Channel(
|
|||
id: "UCh8gHdtzO2tXd593_bjErWg",
|
||||
name: "Doobydobap",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(3740000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -471,7 +471,7 @@ Channel(
|
|||
id: "UCh8gHdtzO2tXd593_bjErWg",
|
||||
name: "Doobydobap",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(3740000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -497,7 +497,7 @@ Channel(
|
|||
id: "UCh8gHdtzO2tXd593_bjErWg",
|
||||
name: "Doobydobap",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(3740000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -523,7 +523,7 @@ Channel(
|
|||
id: "UCh8gHdtzO2tXd593_bjErWg",
|
||||
name: "Doobydobap",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(3740000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -549,7 +549,7 @@ Channel(
|
|||
id: "UCh8gHdtzO2tXd593_bjErWg",
|
||||
name: "Doobydobap",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(3740000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -575,7 +575,7 @@ Channel(
|
|||
id: "UCh8gHdtzO2tXd593_bjErWg",
|
||||
name: "Doobydobap",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(3740000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -601,7 +601,7 @@ Channel(
|
|||
id: "UCh8gHdtzO2tXd593_bjErWg",
|
||||
name: "Doobydobap",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(3740000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -627,7 +627,7 @@ Channel(
|
|||
id: "UCh8gHdtzO2tXd593_bjErWg",
|
||||
name: "Doobydobap",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(3740000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -653,7 +653,7 @@ Channel(
|
|||
id: "UCh8gHdtzO2tXd593_bjErWg",
|
||||
name: "Doobydobap",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(3740000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -679,7 +679,7 @@ Channel(
|
|||
id: "UCh8gHdtzO2tXd593_bjErWg",
|
||||
name: "Doobydobap",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(3740000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -705,7 +705,7 @@ Channel(
|
|||
id: "UCh8gHdtzO2tXd593_bjErWg",
|
||||
name: "Doobydobap",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(3740000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -731,7 +731,7 @@ Channel(
|
|||
id: "UCh8gHdtzO2tXd593_bjErWg",
|
||||
name: "Doobydobap",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(3740000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -757,7 +757,7 @@ Channel(
|
|||
id: "UCh8gHdtzO2tXd593_bjErWg",
|
||||
name: "Doobydobap",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(3740000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -783,7 +783,7 @@ Channel(
|
|||
id: "UCh8gHdtzO2tXd593_bjErWg",
|
||||
name: "Doobydobap",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(3740000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -809,7 +809,7 @@ Channel(
|
|||
id: "UCh8gHdtzO2tXd593_bjErWg",
|
||||
name: "Doobydobap",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(3740000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -835,7 +835,7 @@ Channel(
|
|||
id: "UCh8gHdtzO2tXd593_bjErWg",
|
||||
name: "Doobydobap",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(3740000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -861,7 +861,7 @@ Channel(
|
|||
id: "UCh8gHdtzO2tXd593_bjErWg",
|
||||
name: "Doobydobap",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(3740000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -887,7 +887,7 @@ Channel(
|
|||
id: "UCh8gHdtzO2tXd593_bjErWg",
|
||||
name: "Doobydobap",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(3740000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -913,7 +913,7 @@ Channel(
|
|||
id: "UCh8gHdtzO2tXd593_bjErWg",
|
||||
name: "Doobydobap",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(3740000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -939,7 +939,7 @@ Channel(
|
|||
id: "UCh8gHdtzO2tXd593_bjErWg",
|
||||
name: "Doobydobap",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(3740000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -965,7 +965,7 @@ Channel(
|
|||
id: "UCh8gHdtzO2tXd593_bjErWg",
|
||||
name: "Doobydobap",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(3740000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -991,7 +991,7 @@ Channel(
|
|||
id: "UCh8gHdtzO2tXd593_bjErWg",
|
||||
name: "Doobydobap",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(3740000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -1017,7 +1017,7 @@ Channel(
|
|||
id: "UCh8gHdtzO2tXd593_bjErWg",
|
||||
name: "Doobydobap",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(3740000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -1043,7 +1043,7 @@ Channel(
|
|||
id: "UCh8gHdtzO2tXd593_bjErWg",
|
||||
name: "Doobydobap",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(3740000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -1069,7 +1069,7 @@ Channel(
|
|||
id: "UCh8gHdtzO2tXd593_bjErWg",
|
||||
name: "Doobydobap",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(3740000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -1095,7 +1095,7 @@ Channel(
|
|||
id: "UCh8gHdtzO2tXd593_bjErWg",
|
||||
name: "Doobydobap",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(3740000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -1121,7 +1121,7 @@ Channel(
|
|||
id: "UCh8gHdtzO2tXd593_bjErWg",
|
||||
name: "Doobydobap",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(3740000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -1147,7 +1147,7 @@ Channel(
|
|||
id: "UCh8gHdtzO2tXd593_bjErWg",
|
||||
name: "Doobydobap",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(3740000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -1173,7 +1173,7 @@ Channel(
|
|||
id: "UCh8gHdtzO2tXd593_bjErWg",
|
||||
name: "Doobydobap",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(3740000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -1199,7 +1199,7 @@ Channel(
|
|||
id: "UCh8gHdtzO2tXd593_bjErWg",
|
||||
name: "Doobydobap",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(3740000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -1225,7 +1225,7 @@ Channel(
|
|||
id: "UCh8gHdtzO2tXd593_bjErWg",
|
||||
name: "Doobydobap",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(3740000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -1251,7 +1251,7 @@ Channel(
|
|||
id: "UCh8gHdtzO2tXd593_bjErWg",
|
||||
name: "Doobydobap",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(3740000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -1277,7 +1277,7 @@ Channel(
|
|||
id: "UCh8gHdtzO2tXd593_bjErWg",
|
||||
name: "Doobydobap",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(3740000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -1303,7 +1303,7 @@ Channel(
|
|||
id: "UCh8gHdtzO2tXd593_bjErWg",
|
||||
name: "Doobydobap",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(3740000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
|
|
@ -25,7 +25,7 @@ Channel(
|
|||
height: 160,
|
||||
),
|
||||
],
|
||||
verification: verified,
|
||||
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: [],
|
||||
banner: [
|
||||
|
@ -81,7 +81,7 @@ Channel(
|
|||
id: "UCh8gHdtzO2tXd593_bjErWg",
|
||||
name: "Doobydobap",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(3970000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -107,7 +107,7 @@ Channel(
|
|||
id: "UCh8gHdtzO2tXd593_bjErWg",
|
||||
name: "Doobydobap",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(3970000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -133,7 +133,7 @@ Channel(
|
|||
id: "UCh8gHdtzO2tXd593_bjErWg",
|
||||
name: "Doobydobap",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(3970000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -159,7 +159,7 @@ Channel(
|
|||
id: "UCh8gHdtzO2tXd593_bjErWg",
|
||||
name: "Doobydobap",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(3970000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -185,7 +185,7 @@ Channel(
|
|||
id: "UCh8gHdtzO2tXd593_bjErWg",
|
||||
name: "Doobydobap",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(3970000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -211,7 +211,7 @@ Channel(
|
|||
id: "UCh8gHdtzO2tXd593_bjErWg",
|
||||
name: "Doobydobap",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(3970000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -237,7 +237,7 @@ Channel(
|
|||
id: "UCh8gHdtzO2tXd593_bjErWg",
|
||||
name: "Doobydobap",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(3970000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -263,7 +263,7 @@ Channel(
|
|||
id: "UCh8gHdtzO2tXd593_bjErWg",
|
||||
name: "Doobydobap",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(3970000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -289,7 +289,7 @@ Channel(
|
|||
id: "UCh8gHdtzO2tXd593_bjErWg",
|
||||
name: "Doobydobap",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(3970000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -315,7 +315,7 @@ Channel(
|
|||
id: "UCh8gHdtzO2tXd593_bjErWg",
|
||||
name: "Doobydobap",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(3970000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -341,7 +341,7 @@ Channel(
|
|||
id: "UCh8gHdtzO2tXd593_bjErWg",
|
||||
name: "Doobydobap",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(3970000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -367,7 +367,7 @@ Channel(
|
|||
id: "UCh8gHdtzO2tXd593_bjErWg",
|
||||
name: "Doobydobap",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(3970000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -393,7 +393,7 @@ Channel(
|
|||
id: "UCh8gHdtzO2tXd593_bjErWg",
|
||||
name: "Doobydobap",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(3970000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -419,7 +419,7 @@ Channel(
|
|||
id: "UCh8gHdtzO2tXd593_bjErWg",
|
||||
name: "Doobydobap",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(3970000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -445,7 +445,7 @@ Channel(
|
|||
id: "UCh8gHdtzO2tXd593_bjErWg",
|
||||
name: "Doobydobap",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(3970000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -471,7 +471,7 @@ Channel(
|
|||
id: "UCh8gHdtzO2tXd593_bjErWg",
|
||||
name: "Doobydobap",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(3970000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -497,7 +497,7 @@ Channel(
|
|||
id: "UCh8gHdtzO2tXd593_bjErWg",
|
||||
name: "Doobydobap",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(3970000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -523,7 +523,7 @@ Channel(
|
|||
id: "UCh8gHdtzO2tXd593_bjErWg",
|
||||
name: "Doobydobap",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(3970000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -549,7 +549,7 @@ Channel(
|
|||
id: "UCh8gHdtzO2tXd593_bjErWg",
|
||||
name: "Doobydobap",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(3970000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -575,7 +575,7 @@ Channel(
|
|||
id: "UCh8gHdtzO2tXd593_bjErWg",
|
||||
name: "Doobydobap",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(3970000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -601,7 +601,7 @@ Channel(
|
|||
id: "UCh8gHdtzO2tXd593_bjErWg",
|
||||
name: "Doobydobap",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(3970000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -627,7 +627,7 @@ Channel(
|
|||
id: "UCh8gHdtzO2tXd593_bjErWg",
|
||||
name: "Doobydobap",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(3970000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -653,7 +653,7 @@ Channel(
|
|||
id: "UCh8gHdtzO2tXd593_bjErWg",
|
||||
name: "Doobydobap",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(3970000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -679,7 +679,7 @@ Channel(
|
|||
id: "UCh8gHdtzO2tXd593_bjErWg",
|
||||
name: "Doobydobap",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(3970000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -705,7 +705,7 @@ Channel(
|
|||
id: "UCh8gHdtzO2tXd593_bjErWg",
|
||||
name: "Doobydobap",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(3970000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -731,7 +731,7 @@ Channel(
|
|||
id: "UCh8gHdtzO2tXd593_bjErWg",
|
||||
name: "Doobydobap",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(3970000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -757,7 +757,7 @@ Channel(
|
|||
id: "UCh8gHdtzO2tXd593_bjErWg",
|
||||
name: "Doobydobap",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(3970000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -783,7 +783,7 @@ Channel(
|
|||
id: "UCh8gHdtzO2tXd593_bjErWg",
|
||||
name: "Doobydobap",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(3970000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -809,7 +809,7 @@ Channel(
|
|||
id: "UCh8gHdtzO2tXd593_bjErWg",
|
||||
name: "Doobydobap",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(3970000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -835,7 +835,7 @@ Channel(
|
|||
id: "UCh8gHdtzO2tXd593_bjErWg",
|
||||
name: "Doobydobap",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(3970000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -861,7 +861,7 @@ Channel(
|
|||
id: "UCh8gHdtzO2tXd593_bjErWg",
|
||||
name: "Doobydobap",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(3970000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -887,7 +887,7 @@ Channel(
|
|||
id: "UCh8gHdtzO2tXd593_bjErWg",
|
||||
name: "Doobydobap",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(3970000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -913,7 +913,7 @@ Channel(
|
|||
id: "UCh8gHdtzO2tXd593_bjErWg",
|
||||
name: "Doobydobap",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(3970000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -939,7 +939,7 @@ Channel(
|
|||
id: "UCh8gHdtzO2tXd593_bjErWg",
|
||||
name: "Doobydobap",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(3970000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -965,7 +965,7 @@ Channel(
|
|||
id: "UCh8gHdtzO2tXd593_bjErWg",
|
||||
name: "Doobydobap",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(3970000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -991,7 +991,7 @@ Channel(
|
|||
id: "UCh8gHdtzO2tXd593_bjErWg",
|
||||
name: "Doobydobap",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(3970000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -1017,7 +1017,7 @@ Channel(
|
|||
id: "UCh8gHdtzO2tXd593_bjErWg",
|
||||
name: "Doobydobap",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(3970000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -1043,7 +1043,7 @@ Channel(
|
|||
id: "UCh8gHdtzO2tXd593_bjErWg",
|
||||
name: "Doobydobap",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(3970000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -1069,7 +1069,7 @@ Channel(
|
|||
id: "UCh8gHdtzO2tXd593_bjErWg",
|
||||
name: "Doobydobap",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(3970000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -1095,7 +1095,7 @@ Channel(
|
|||
id: "UCh8gHdtzO2tXd593_bjErWg",
|
||||
name: "Doobydobap",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(3970000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -1121,7 +1121,7 @@ Channel(
|
|||
id: "UCh8gHdtzO2tXd593_bjErWg",
|
||||
name: "Doobydobap",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(3970000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -1147,7 +1147,7 @@ Channel(
|
|||
id: "UCh8gHdtzO2tXd593_bjErWg",
|
||||
name: "Doobydobap",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(3970000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -1173,7 +1173,7 @@ Channel(
|
|||
id: "UCh8gHdtzO2tXd593_bjErWg",
|
||||
name: "Doobydobap",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(3970000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -1199,7 +1199,7 @@ Channel(
|
|||
id: "UCh8gHdtzO2tXd593_bjErWg",
|
||||
name: "Doobydobap",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(3970000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -1225,7 +1225,7 @@ Channel(
|
|||
id: "UCh8gHdtzO2tXd593_bjErWg",
|
||||
name: "Doobydobap",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(3970000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -1251,7 +1251,7 @@ Channel(
|
|||
id: "UCh8gHdtzO2tXd593_bjErWg",
|
||||
name: "Doobydobap",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(3970000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -1277,7 +1277,7 @@ Channel(
|
|||
id: "UCh8gHdtzO2tXd593_bjErWg",
|
||||
name: "Doobydobap",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(3970000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -1303,7 +1303,7 @@ Channel(
|
|||
id: "UCh8gHdtzO2tXd593_bjErWg",
|
||||
name: "Doobydobap",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(3970000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
|
|
@ -25,7 +25,7 @@ Channel(
|
|||
height: 176,
|
||||
),
|
||||
],
|
||||
verification: verified,
|
||||
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: [],
|
||||
banner: [
|
||||
|
@ -96,7 +96,7 @@ Channel(
|
|||
id: "UCh8gHdtzO2tXd593_bjErWg",
|
||||
name: "Doobydobap",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(2930000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -137,7 +137,7 @@ Channel(
|
|||
id: "UCh8gHdtzO2tXd593_bjErWg",
|
||||
name: "Doobydobap",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(2930000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -178,7 +178,7 @@ Channel(
|
|||
id: "UCh8gHdtzO2tXd593_bjErWg",
|
||||
name: "Doobydobap",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(2930000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -219,7 +219,7 @@ Channel(
|
|||
id: "UCh8gHdtzO2tXd593_bjErWg",
|
||||
name: "Doobydobap",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(2930000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -260,7 +260,7 @@ Channel(
|
|||
id: "UCh8gHdtzO2tXd593_bjErWg",
|
||||
name: "Doobydobap",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(2930000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -301,7 +301,7 @@ Channel(
|
|||
id: "UCh8gHdtzO2tXd593_bjErWg",
|
||||
name: "Doobydobap",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(2930000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -342,7 +342,7 @@ Channel(
|
|||
id: "UCh8gHdtzO2tXd593_bjErWg",
|
||||
name: "Doobydobap",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(2930000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -383,7 +383,7 @@ Channel(
|
|||
id: "UCh8gHdtzO2tXd593_bjErWg",
|
||||
name: "Doobydobap",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(2930000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -424,7 +424,7 @@ Channel(
|
|||
id: "UCh8gHdtzO2tXd593_bjErWg",
|
||||
name: "Doobydobap",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(2930000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -465,7 +465,7 @@ Channel(
|
|||
id: "UCh8gHdtzO2tXd593_bjErWg",
|
||||
name: "Doobydobap",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(2930000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -506,7 +506,7 @@ Channel(
|
|||
id: "UCh8gHdtzO2tXd593_bjErWg",
|
||||
name: "Doobydobap",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(2930000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -547,7 +547,7 @@ Channel(
|
|||
id: "UCh8gHdtzO2tXd593_bjErWg",
|
||||
name: "Doobydobap",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(2930000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -588,7 +588,7 @@ Channel(
|
|||
id: "UCh8gHdtzO2tXd593_bjErWg",
|
||||
name: "Doobydobap",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(2930000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -629,7 +629,7 @@ Channel(
|
|||
id: "UCh8gHdtzO2tXd593_bjErWg",
|
||||
name: "Doobydobap",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(2930000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -670,7 +670,7 @@ Channel(
|
|||
id: "UCh8gHdtzO2tXd593_bjErWg",
|
||||
name: "Doobydobap",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(2930000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -711,7 +711,7 @@ Channel(
|
|||
id: "UCh8gHdtzO2tXd593_bjErWg",
|
||||
name: "Doobydobap",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(2930000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -752,7 +752,7 @@ Channel(
|
|||
id: "UCh8gHdtzO2tXd593_bjErWg",
|
||||
name: "Doobydobap",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(2930000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -793,7 +793,7 @@ Channel(
|
|||
id: "UCh8gHdtzO2tXd593_bjErWg",
|
||||
name: "Doobydobap",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(2930000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -834,7 +834,7 @@ Channel(
|
|||
id: "UCh8gHdtzO2tXd593_bjErWg",
|
||||
name: "Doobydobap",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(2930000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -875,7 +875,7 @@ Channel(
|
|||
id: "UCh8gHdtzO2tXd593_bjErWg",
|
||||
name: "Doobydobap",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(2930000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -916,7 +916,7 @@ Channel(
|
|||
id: "UCh8gHdtzO2tXd593_bjErWg",
|
||||
name: "Doobydobap",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(2930000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -957,7 +957,7 @@ Channel(
|
|||
id: "UCh8gHdtzO2tXd593_bjErWg",
|
||||
name: "Doobydobap",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(2930000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -998,7 +998,7 @@ Channel(
|
|||
id: "UCh8gHdtzO2tXd593_bjErWg",
|
||||
name: "Doobydobap",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(2930000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -1039,7 +1039,7 @@ Channel(
|
|||
id: "UCh8gHdtzO2tXd593_bjErWg",
|
||||
name: "Doobydobap",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(2930000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -1080,7 +1080,7 @@ Channel(
|
|||
id: "UCh8gHdtzO2tXd593_bjErWg",
|
||||
name: "Doobydobap",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(2930000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -1121,7 +1121,7 @@ Channel(
|
|||
id: "UCh8gHdtzO2tXd593_bjErWg",
|
||||
name: "Doobydobap",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(2930000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -1162,7 +1162,7 @@ Channel(
|
|||
id: "UCh8gHdtzO2tXd593_bjErWg",
|
||||
name: "Doobydobap",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(2930000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -1203,7 +1203,7 @@ Channel(
|
|||
id: "UCh8gHdtzO2tXd593_bjErWg",
|
||||
name: "Doobydobap",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(2930000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -1244,7 +1244,7 @@ Channel(
|
|||
id: "UCh8gHdtzO2tXd593_bjErWg",
|
||||
name: "Doobydobap",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(2930000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -1285,7 +1285,7 @@ Channel(
|
|||
id: "UCh8gHdtzO2tXd593_bjErWg",
|
||||
name: "Doobydobap",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(2930000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
|
|
@ -25,7 +25,7 @@ Channel(
|
|||
height: 176,
|
||||
),
|
||||
],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
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",
|
||||
tags: [
|
||||
"electronics",
|
||||
|
@ -125,7 +125,7 @@ Channel(
|
|||
id: "UC2DjFE7Xf11URZqWBigcVOQ",
|
||||
name: "EEVblog",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(883000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -166,7 +166,7 @@ Channel(
|
|||
id: "UC2DjFE7Xf11URZqWBigcVOQ",
|
||||
name: "EEVblog",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(883000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -207,7 +207,7 @@ Channel(
|
|||
id: "UC2DjFE7Xf11URZqWBigcVOQ",
|
||||
name: "EEVblog",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(883000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -248,7 +248,7 @@ Channel(
|
|||
id: "UC2DjFE7Xf11URZqWBigcVOQ",
|
||||
name: "EEVblog",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(883000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -289,7 +289,7 @@ Channel(
|
|||
id: "UC2DjFE7Xf11URZqWBigcVOQ",
|
||||
name: "EEVblog",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(883000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -330,7 +330,7 @@ Channel(
|
|||
id: "UC2DjFE7Xf11URZqWBigcVOQ",
|
||||
name: "EEVblog",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(883000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -371,7 +371,7 @@ Channel(
|
|||
id: "UC2DjFE7Xf11URZqWBigcVOQ",
|
||||
name: "EEVblog",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(883000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -412,7 +412,7 @@ Channel(
|
|||
id: "UC2DjFE7Xf11URZqWBigcVOQ",
|
||||
name: "EEVblog",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(883000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -453,7 +453,7 @@ Channel(
|
|||
id: "UC2DjFE7Xf11URZqWBigcVOQ",
|
||||
name: "EEVblog",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(883000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -494,7 +494,7 @@ Channel(
|
|||
id: "UC2DjFE7Xf11URZqWBigcVOQ",
|
||||
name: "EEVblog",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(883000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -535,7 +535,7 @@ Channel(
|
|||
id: "UC2DjFE7Xf11URZqWBigcVOQ",
|
||||
name: "EEVblog",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(883000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -576,7 +576,7 @@ Channel(
|
|||
id: "UC2DjFE7Xf11URZqWBigcVOQ",
|
||||
name: "EEVblog",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(883000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -617,7 +617,7 @@ Channel(
|
|||
id: "UC2DjFE7Xf11URZqWBigcVOQ",
|
||||
name: "EEVblog",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(883000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -658,7 +658,7 @@ Channel(
|
|||
id: "UC2DjFE7Xf11URZqWBigcVOQ",
|
||||
name: "EEVblog",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(883000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -699,7 +699,7 @@ Channel(
|
|||
id: "UC2DjFE7Xf11URZqWBigcVOQ",
|
||||
name: "EEVblog",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(883000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -740,7 +740,7 @@ Channel(
|
|||
id: "UC2DjFE7Xf11URZqWBigcVOQ",
|
||||
name: "EEVblog",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(883000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -781,7 +781,7 @@ Channel(
|
|||
id: "UC2DjFE7Xf11URZqWBigcVOQ",
|
||||
name: "EEVblog",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(883000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -822,7 +822,7 @@ Channel(
|
|||
id: "UC2DjFE7Xf11URZqWBigcVOQ",
|
||||
name: "EEVblog",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(883000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -863,7 +863,7 @@ Channel(
|
|||
id: "UC2DjFE7Xf11URZqWBigcVOQ",
|
||||
name: "EEVblog",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(883000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -904,7 +904,7 @@ Channel(
|
|||
id: "UC2DjFE7Xf11URZqWBigcVOQ",
|
||||
name: "EEVblog",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(883000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -945,7 +945,7 @@ Channel(
|
|||
id: "UC2DjFE7Xf11URZqWBigcVOQ",
|
||||
name: "EEVblog",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(883000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -986,7 +986,7 @@ Channel(
|
|||
id: "UC2DjFE7Xf11URZqWBigcVOQ",
|
||||
name: "EEVblog",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(883000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -1027,7 +1027,7 @@ Channel(
|
|||
id: "UC2DjFE7Xf11URZqWBigcVOQ",
|
||||
name: "EEVblog",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(883000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -1068,7 +1068,7 @@ Channel(
|
|||
id: "UC2DjFE7Xf11URZqWBigcVOQ",
|
||||
name: "EEVblog",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(883000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -1109,7 +1109,7 @@ Channel(
|
|||
id: "UC2DjFE7Xf11URZqWBigcVOQ",
|
||||
name: "EEVblog",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(883000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -1150,7 +1150,7 @@ Channel(
|
|||
id: "UC2DjFE7Xf11URZqWBigcVOQ",
|
||||
name: "EEVblog",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(883000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -1191,7 +1191,7 @@ Channel(
|
|||
id: "UC2DjFE7Xf11URZqWBigcVOQ",
|
||||
name: "EEVblog",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(883000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -1232,7 +1232,7 @@ Channel(
|
|||
id: "UC2DjFE7Xf11URZqWBigcVOQ",
|
||||
name: "EEVblog",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(883000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -1273,7 +1273,7 @@ Channel(
|
|||
id: "UC2DjFE7Xf11URZqWBigcVOQ",
|
||||
name: "EEVblog",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(883000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -1314,7 +1314,7 @@ Channel(
|
|||
id: "UC2DjFE7Xf11URZqWBigcVOQ",
|
||||
name: "EEVblog",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(883000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
|
|
@ -20,7 +20,7 @@ Channel(
|
|||
height: 176,
|
||||
),
|
||||
],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
description: "April 14-16 & 21-23, 2023\n",
|
||||
tags: [
|
||||
"coachella",
|
||||
|
@ -70,7 +70,7 @@ Channel(
|
|||
id: "UCHF66aWLOxBW4l6VkSrS3cQ",
|
||||
name: "Coachella",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(2710000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -111,7 +111,7 @@ Channel(
|
|||
id: "UCHF66aWLOxBW4l6VkSrS3cQ",
|
||||
name: "Coachella",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(2710000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -152,7 +152,7 @@ Channel(
|
|||
id: "UCHF66aWLOxBW4l6VkSrS3cQ",
|
||||
name: "Coachella",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(2710000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -193,7 +193,7 @@ Channel(
|
|||
id: "UCHF66aWLOxBW4l6VkSrS3cQ",
|
||||
name: "Coachella",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(2710000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -234,7 +234,7 @@ Channel(
|
|||
id: "UCHF66aWLOxBW4l6VkSrS3cQ",
|
||||
name: "Coachella",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(2710000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -275,7 +275,7 @@ Channel(
|
|||
id: "UCHF66aWLOxBW4l6VkSrS3cQ",
|
||||
name: "Coachella",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(2710000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -316,7 +316,7 @@ Channel(
|
|||
id: "UCHF66aWLOxBW4l6VkSrS3cQ",
|
||||
name: "Coachella",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(2710000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -357,7 +357,7 @@ Channel(
|
|||
id: "UCHF66aWLOxBW4l6VkSrS3cQ",
|
||||
name: "Coachella",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(2710000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -398,7 +398,7 @@ Channel(
|
|||
id: "UCHF66aWLOxBW4l6VkSrS3cQ",
|
||||
name: "Coachella",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(2710000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -439,7 +439,7 @@ Channel(
|
|||
id: "UCHF66aWLOxBW4l6VkSrS3cQ",
|
||||
name: "Coachella",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(2710000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -480,7 +480,7 @@ Channel(
|
|||
id: "UCHF66aWLOxBW4l6VkSrS3cQ",
|
||||
name: "Coachella",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(2710000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -521,7 +521,7 @@ Channel(
|
|||
id: "UCHF66aWLOxBW4l6VkSrS3cQ",
|
||||
name: "Coachella",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(2710000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -562,7 +562,7 @@ Channel(
|
|||
id: "UCHF66aWLOxBW4l6VkSrS3cQ",
|
||||
name: "Coachella",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(2710000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -603,7 +603,7 @@ Channel(
|
|||
id: "UCHF66aWLOxBW4l6VkSrS3cQ",
|
||||
name: "Coachella",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(2710000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -644,7 +644,7 @@ Channel(
|
|||
id: "UCHF66aWLOxBW4l6VkSrS3cQ",
|
||||
name: "Coachella",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(2710000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -685,7 +685,7 @@ Channel(
|
|||
id: "UCHF66aWLOxBW4l6VkSrS3cQ",
|
||||
name: "Coachella",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(2710000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -726,7 +726,7 @@ Channel(
|
|||
id: "UCHF66aWLOxBW4l6VkSrS3cQ",
|
||||
name: "Coachella",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(2710000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -767,7 +767,7 @@ Channel(
|
|||
id: "UCHF66aWLOxBW4l6VkSrS3cQ",
|
||||
name: "Coachella",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(2710000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -808,7 +808,7 @@ Channel(
|
|||
id: "UCHF66aWLOxBW4l6VkSrS3cQ",
|
||||
name: "Coachella",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(2710000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -849,7 +849,7 @@ Channel(
|
|||
id: "UCHF66aWLOxBW4l6VkSrS3cQ",
|
||||
name: "Coachella",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(2710000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -890,7 +890,7 @@ Channel(
|
|||
id: "UCHF66aWLOxBW4l6VkSrS3cQ",
|
||||
name: "Coachella",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(2710000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -931,7 +931,7 @@ Channel(
|
|||
id: "UCHF66aWLOxBW4l6VkSrS3cQ",
|
||||
name: "Coachella",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(2710000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -972,7 +972,7 @@ Channel(
|
|||
id: "UCHF66aWLOxBW4l6VkSrS3cQ",
|
||||
name: "Coachella",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(2710000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -1013,7 +1013,7 @@ Channel(
|
|||
id: "UCHF66aWLOxBW4l6VkSrS3cQ",
|
||||
name: "Coachella",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(2710000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -1054,7 +1054,7 @@ Channel(
|
|||
id: "UCHF66aWLOxBW4l6VkSrS3cQ",
|
||||
name: "Coachella",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(2710000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -1095,7 +1095,7 @@ Channel(
|
|||
id: "UCHF66aWLOxBW4l6VkSrS3cQ",
|
||||
name: "Coachella",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(2710000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -1136,7 +1136,7 @@ Channel(
|
|||
id: "UCHF66aWLOxBW4l6VkSrS3cQ",
|
||||
name: "Coachella",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(2710000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -1177,7 +1177,7 @@ Channel(
|
|||
id: "UCHF66aWLOxBW4l6VkSrS3cQ",
|
||||
name: "Coachella",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(2710000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -1218,7 +1218,7 @@ Channel(
|
|||
id: "UCHF66aWLOxBW4l6VkSrS3cQ",
|
||||
name: "Coachella",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(2710000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -1259,7 +1259,7 @@ Channel(
|
|||
id: "UCHF66aWLOxBW4l6VkSrS3cQ",
|
||||
name: "Coachella",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(2710000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
|
|
@ -25,7 +25,7 @@ Channel(
|
|||
height: 160,
|
||||
),
|
||||
],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
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",
|
||||
tags: [
|
||||
"electronics",
|
||||
|
@ -125,7 +125,7 @@ Channel(
|
|||
id: "UC2DjFE7Xf11URZqWBigcVOQ",
|
||||
name: "EEVblog",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(933000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -166,7 +166,7 @@ Channel(
|
|||
id: "UC2DjFE7Xf11URZqWBigcVOQ",
|
||||
name: "EEVblog",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(933000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -207,7 +207,7 @@ Channel(
|
|||
id: "UC2DjFE7Xf11URZqWBigcVOQ",
|
||||
name: "EEVblog",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(933000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -248,7 +248,7 @@ Channel(
|
|||
id: "UC2DjFE7Xf11URZqWBigcVOQ",
|
||||
name: "EEVblog",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(933000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -289,7 +289,7 @@ Channel(
|
|||
id: "UC2DjFE7Xf11URZqWBigcVOQ",
|
||||
name: "EEVblog",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(933000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -330,7 +330,7 @@ Channel(
|
|||
id: "UC2DjFE7Xf11URZqWBigcVOQ",
|
||||
name: "EEVblog",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(933000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -371,7 +371,7 @@ Channel(
|
|||
id: "UC2DjFE7Xf11URZqWBigcVOQ",
|
||||
name: "EEVblog",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(933000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -412,7 +412,7 @@ Channel(
|
|||
id: "UC2DjFE7Xf11URZqWBigcVOQ",
|
||||
name: "EEVblog",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(933000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -453,7 +453,7 @@ Channel(
|
|||
id: "UC2DjFE7Xf11URZqWBigcVOQ",
|
||||
name: "EEVblog",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(933000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -494,7 +494,7 @@ Channel(
|
|||
id: "UC2DjFE7Xf11URZqWBigcVOQ",
|
||||
name: "EEVblog",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(933000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -535,7 +535,7 @@ Channel(
|
|||
id: "UC2DjFE7Xf11URZqWBigcVOQ",
|
||||
name: "EEVblog",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(933000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -576,7 +576,7 @@ Channel(
|
|||
id: "UC2DjFE7Xf11URZqWBigcVOQ",
|
||||
name: "EEVblog",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(933000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -617,7 +617,7 @@ Channel(
|
|||
id: "UC2DjFE7Xf11URZqWBigcVOQ",
|
||||
name: "EEVblog",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(933000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -658,7 +658,7 @@ Channel(
|
|||
id: "UC2DjFE7Xf11URZqWBigcVOQ",
|
||||
name: "EEVblog",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(933000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -699,7 +699,7 @@ Channel(
|
|||
id: "UC2DjFE7Xf11URZqWBigcVOQ",
|
||||
name: "EEVblog",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(933000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -740,7 +740,7 @@ Channel(
|
|||
id: "UC2DjFE7Xf11URZqWBigcVOQ",
|
||||
name: "EEVblog",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(933000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -781,7 +781,7 @@ Channel(
|
|||
id: "UC2DjFE7Xf11URZqWBigcVOQ",
|
||||
name: "EEVblog",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(933000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -822,7 +822,7 @@ Channel(
|
|||
id: "UC2DjFE7Xf11URZqWBigcVOQ",
|
||||
name: "EEVblog",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(933000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -863,7 +863,7 @@ Channel(
|
|||
id: "UC2DjFE7Xf11URZqWBigcVOQ",
|
||||
name: "EEVblog",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(933000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -904,7 +904,7 @@ Channel(
|
|||
id: "UC2DjFE7Xf11URZqWBigcVOQ",
|
||||
name: "EEVblog",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(933000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -945,7 +945,7 @@ Channel(
|
|||
id: "UC2DjFE7Xf11URZqWBigcVOQ",
|
||||
name: "EEVblog",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(933000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -986,7 +986,7 @@ Channel(
|
|||
id: "UC2DjFE7Xf11URZqWBigcVOQ",
|
||||
name: "EEVblog",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(933000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -1027,7 +1027,7 @@ Channel(
|
|||
id: "UC2DjFE7Xf11URZqWBigcVOQ",
|
||||
name: "EEVblog",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(933000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -1068,7 +1068,7 @@ Channel(
|
|||
id: "UC2DjFE7Xf11URZqWBigcVOQ",
|
||||
name: "EEVblog",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(933000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -1109,7 +1109,7 @@ Channel(
|
|||
id: "UC2DjFE7Xf11URZqWBigcVOQ",
|
||||
name: "EEVblog",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(933000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -1150,7 +1150,7 @@ Channel(
|
|||
id: "UC2DjFE7Xf11URZqWBigcVOQ",
|
||||
name: "EEVblog",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(933000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -1191,7 +1191,7 @@ Channel(
|
|||
id: "UC2DjFE7Xf11URZqWBigcVOQ",
|
||||
name: "EEVblog",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(933000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -1232,7 +1232,7 @@ Channel(
|
|||
id: "UC2DjFE7Xf11URZqWBigcVOQ",
|
||||
name: "EEVblog",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(933000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -1273,7 +1273,7 @@ Channel(
|
|||
id: "UC2DjFE7Xf11URZqWBigcVOQ",
|
||||
name: "EEVblog",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(933000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -1314,7 +1314,7 @@ Channel(
|
|||
id: "UC2DjFE7Xf11URZqWBigcVOQ",
|
||||
name: "EEVblog",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(933000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
|
|
@ -25,7 +25,7 @@ Channel(
|
|||
height: 176,
|
||||
),
|
||||
],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
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",
|
||||
tags: [
|
||||
"electronics",
|
||||
|
@ -125,7 +125,7 @@ Channel(
|
|||
id: "UC2DjFE7Xf11URZqWBigcVOQ",
|
||||
name: "EEVblog",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(880000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -166,7 +166,7 @@ Channel(
|
|||
id: "UC2DjFE7Xf11URZqWBigcVOQ",
|
||||
name: "EEVblog",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(880000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -207,7 +207,7 @@ Channel(
|
|||
id: "UC2DjFE7Xf11URZqWBigcVOQ",
|
||||
name: "EEVblog",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(880000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -248,7 +248,7 @@ Channel(
|
|||
id: "UC2DjFE7Xf11URZqWBigcVOQ",
|
||||
name: "EEVblog",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(880000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -289,7 +289,7 @@ Channel(
|
|||
id: "UC2DjFE7Xf11URZqWBigcVOQ",
|
||||
name: "EEVblog",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(880000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -330,7 +330,7 @@ Channel(
|
|||
id: "UC2DjFE7Xf11URZqWBigcVOQ",
|
||||
name: "EEVblog",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(880000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -371,7 +371,7 @@ Channel(
|
|||
id: "UC2DjFE7Xf11URZqWBigcVOQ",
|
||||
name: "EEVblog",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(880000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -412,7 +412,7 @@ Channel(
|
|||
id: "UC2DjFE7Xf11URZqWBigcVOQ",
|
||||
name: "EEVblog",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(880000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -453,7 +453,7 @@ Channel(
|
|||
id: "UC2DjFE7Xf11URZqWBigcVOQ",
|
||||
name: "EEVblog",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(880000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -494,7 +494,7 @@ Channel(
|
|||
id: "UC2DjFE7Xf11URZqWBigcVOQ",
|
||||
name: "EEVblog",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(880000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -535,7 +535,7 @@ Channel(
|
|||
id: "UC2DjFE7Xf11URZqWBigcVOQ",
|
||||
name: "EEVblog",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(880000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -576,7 +576,7 @@ Channel(
|
|||
id: "UC2DjFE7Xf11URZqWBigcVOQ",
|
||||
name: "EEVblog",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(880000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -617,7 +617,7 @@ Channel(
|
|||
id: "UC2DjFE7Xf11URZqWBigcVOQ",
|
||||
name: "EEVblog",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(880000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -658,7 +658,7 @@ Channel(
|
|||
id: "UC2DjFE7Xf11URZqWBigcVOQ",
|
||||
name: "EEVblog",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(880000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -699,7 +699,7 @@ Channel(
|
|||
id: "UC2DjFE7Xf11URZqWBigcVOQ",
|
||||
name: "EEVblog",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(880000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -740,7 +740,7 @@ Channel(
|
|||
id: "UC2DjFE7Xf11URZqWBigcVOQ",
|
||||
name: "EEVblog",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(880000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -781,7 +781,7 @@ Channel(
|
|||
id: "UC2DjFE7Xf11URZqWBigcVOQ",
|
||||
name: "EEVblog",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(880000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -822,7 +822,7 @@ Channel(
|
|||
id: "UC2DjFE7Xf11URZqWBigcVOQ",
|
||||
name: "EEVblog",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(880000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -863,7 +863,7 @@ Channel(
|
|||
id: "UC2DjFE7Xf11URZqWBigcVOQ",
|
||||
name: "EEVblog",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(880000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -904,7 +904,7 @@ Channel(
|
|||
id: "UC2DjFE7Xf11URZqWBigcVOQ",
|
||||
name: "EEVblog",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(880000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -945,7 +945,7 @@ Channel(
|
|||
id: "UC2DjFE7Xf11URZqWBigcVOQ",
|
||||
name: "EEVblog",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(880000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -986,7 +986,7 @@ Channel(
|
|||
id: "UC2DjFE7Xf11URZqWBigcVOQ",
|
||||
name: "EEVblog",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(880000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -1027,7 +1027,7 @@ Channel(
|
|||
id: "UC2DjFE7Xf11URZqWBigcVOQ",
|
||||
name: "EEVblog",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(880000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -1068,7 +1068,7 @@ Channel(
|
|||
id: "UC2DjFE7Xf11URZqWBigcVOQ",
|
||||
name: "EEVblog",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(880000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -1109,7 +1109,7 @@ Channel(
|
|||
id: "UC2DjFE7Xf11URZqWBigcVOQ",
|
||||
name: "EEVblog",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(880000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -1150,7 +1150,7 @@ Channel(
|
|||
id: "UC2DjFE7Xf11URZqWBigcVOQ",
|
||||
name: "EEVblog",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(880000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -1191,7 +1191,7 @@ Channel(
|
|||
id: "UC2DjFE7Xf11URZqWBigcVOQ",
|
||||
name: "EEVblog",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(880000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -1232,7 +1232,7 @@ Channel(
|
|||
id: "UC2DjFE7Xf11URZqWBigcVOQ",
|
||||
name: "EEVblog",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(880000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -1273,7 +1273,7 @@ Channel(
|
|||
id: "UC2DjFE7Xf11URZqWBigcVOQ",
|
||||
name: "EEVblog",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(880000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -1314,7 +1314,7 @@ Channel(
|
|||
id: "UC2DjFE7Xf11URZqWBigcVOQ",
|
||||
name: "EEVblog",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(880000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
|
|
@ -25,7 +25,7 @@ Channel(
|
|||
height: 176,
|
||||
),
|
||||
],
|
||||
verification: none,
|
||||
verification: None,
|
||||
description: "",
|
||||
tags: [],
|
||||
banner: [],
|
||||
|
|
|
@ -25,7 +25,7 @@ Channel(
|
|||
height: 176,
|
||||
),
|
||||
],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
description: "Welcome to The Good Life by Sensual Musique.\nThe second official channel of Sensual Musique. You can find a lot of music, live streams and some other things on this channel. Stay tuned :)\n\nSubmit your music here: submit.sensualmusiquenetwork@gmail.com",
|
||||
tags: [
|
||||
"live radio",
|
||||
|
@ -109,7 +109,7 @@ Channel(
|
|||
id: "UChs0pSaEoNLV4mevBFGaoKA",
|
||||
name: "The Good Life Radio x Sensual Musique",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(760000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -150,7 +150,7 @@ Channel(
|
|||
id: "UChs0pSaEoNLV4mevBFGaoKA",
|
||||
name: "The Good Life Radio x Sensual Musique",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(760000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -191,7 +191,7 @@ Channel(
|
|||
id: "UChs0pSaEoNLV4mevBFGaoKA",
|
||||
name: "The Good Life Radio x Sensual Musique",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(760000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -232,7 +232,7 @@ Channel(
|
|||
id: "UChs0pSaEoNLV4mevBFGaoKA",
|
||||
name: "The Good Life Radio x Sensual Musique",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(760000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -273,7 +273,7 @@ Channel(
|
|||
id: "UChs0pSaEoNLV4mevBFGaoKA",
|
||||
name: "The Good Life Radio x Sensual Musique",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(760000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -314,7 +314,7 @@ Channel(
|
|||
id: "UChs0pSaEoNLV4mevBFGaoKA",
|
||||
name: "The Good Life Radio x Sensual Musique",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(760000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -355,7 +355,7 @@ Channel(
|
|||
id: "UChs0pSaEoNLV4mevBFGaoKA",
|
||||
name: "The Good Life Radio x Sensual Musique",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(760000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -396,7 +396,7 @@ Channel(
|
|||
id: "UChs0pSaEoNLV4mevBFGaoKA",
|
||||
name: "The Good Life Radio x Sensual Musique",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(760000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -437,7 +437,7 @@ Channel(
|
|||
id: "UChs0pSaEoNLV4mevBFGaoKA",
|
||||
name: "The Good Life Radio x Sensual Musique",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(760000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -478,7 +478,7 @@ Channel(
|
|||
id: "UChs0pSaEoNLV4mevBFGaoKA",
|
||||
name: "The Good Life Radio x Sensual Musique",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(760000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -519,7 +519,7 @@ Channel(
|
|||
id: "UChs0pSaEoNLV4mevBFGaoKA",
|
||||
name: "The Good Life Radio x Sensual Musique",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(760000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -560,7 +560,7 @@ Channel(
|
|||
id: "UChs0pSaEoNLV4mevBFGaoKA",
|
||||
name: "The Good Life Radio x Sensual Musique",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(760000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -601,7 +601,7 @@ Channel(
|
|||
id: "UChs0pSaEoNLV4mevBFGaoKA",
|
||||
name: "The Good Life Radio x Sensual Musique",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(760000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -642,7 +642,7 @@ Channel(
|
|||
id: "UChs0pSaEoNLV4mevBFGaoKA",
|
||||
name: "The Good Life Radio x Sensual Musique",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(760000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -683,7 +683,7 @@ Channel(
|
|||
id: "UChs0pSaEoNLV4mevBFGaoKA",
|
||||
name: "The Good Life Radio x Sensual Musique",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(760000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -724,7 +724,7 @@ Channel(
|
|||
id: "UChs0pSaEoNLV4mevBFGaoKA",
|
||||
name: "The Good Life Radio x Sensual Musique",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(760000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -765,7 +765,7 @@ Channel(
|
|||
id: "UChs0pSaEoNLV4mevBFGaoKA",
|
||||
name: "The Good Life Radio x Sensual Musique",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(760000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -806,7 +806,7 @@ Channel(
|
|||
id: "UChs0pSaEoNLV4mevBFGaoKA",
|
||||
name: "The Good Life Radio x Sensual Musique",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(760000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -847,7 +847,7 @@ Channel(
|
|||
id: "UChs0pSaEoNLV4mevBFGaoKA",
|
||||
name: "The Good Life Radio x Sensual Musique",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(760000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -888,7 +888,7 @@ Channel(
|
|||
id: "UChs0pSaEoNLV4mevBFGaoKA",
|
||||
name: "The Good Life Radio x Sensual Musique",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(760000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -929,7 +929,7 @@ Channel(
|
|||
id: "UChs0pSaEoNLV4mevBFGaoKA",
|
||||
name: "The Good Life Radio x Sensual Musique",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(760000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
|
|
@ -25,7 +25,7 @@ Channel(
|
|||
height: 176,
|
||||
),
|
||||
],
|
||||
verification: none,
|
||||
verification: None,
|
||||
description: "",
|
||||
tags: [],
|
||||
banner: [
|
||||
|
|
|
@ -25,7 +25,7 @@ Channel(
|
|||
height: 176,
|
||||
),
|
||||
],
|
||||
verification: verified,
|
||||
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: [],
|
||||
banner: [
|
||||
|
@ -81,7 +81,7 @@ Channel(
|
|||
id: "UCh8gHdtzO2tXd593_bjErWg",
|
||||
name: "Doobydobap",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(2840000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -107,7 +107,7 @@ Channel(
|
|||
id: "UCh8gHdtzO2tXd593_bjErWg",
|
||||
name: "Doobydobap",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(2840000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -148,7 +148,7 @@ Channel(
|
|||
id: "UCh8gHdtzO2tXd593_bjErWg",
|
||||
name: "Doobydobap",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(2840000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -174,7 +174,7 @@ Channel(
|
|||
id: "UCh8gHdtzO2tXd593_bjErWg",
|
||||
name: "Doobydobap",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(2840000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -200,7 +200,7 @@ Channel(
|
|||
id: "UCh8gHdtzO2tXd593_bjErWg",
|
||||
name: "Doobydobap",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(2840000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -226,7 +226,7 @@ Channel(
|
|||
id: "UCh8gHdtzO2tXd593_bjErWg",
|
||||
name: "Doobydobap",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(2840000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -267,7 +267,7 @@ Channel(
|
|||
id: "UCh8gHdtzO2tXd593_bjErWg",
|
||||
name: "Doobydobap",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(2840000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -293,7 +293,7 @@ Channel(
|
|||
id: "UCh8gHdtzO2tXd593_bjErWg",
|
||||
name: "Doobydobap",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(2840000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -319,7 +319,7 @@ Channel(
|
|||
id: "UCh8gHdtzO2tXd593_bjErWg",
|
||||
name: "Doobydobap",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(2840000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -345,7 +345,7 @@ Channel(
|
|||
id: "UCh8gHdtzO2tXd593_bjErWg",
|
||||
name: "Doobydobap",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(2840000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -386,7 +386,7 @@ Channel(
|
|||
id: "UCh8gHdtzO2tXd593_bjErWg",
|
||||
name: "Doobydobap",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(2840000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -412,7 +412,7 @@ Channel(
|
|||
id: "UCh8gHdtzO2tXd593_bjErWg",
|
||||
name: "Doobydobap",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(2840000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -453,7 +453,7 @@ Channel(
|
|||
id: "UCh8gHdtzO2tXd593_bjErWg",
|
||||
name: "Doobydobap",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(2840000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -479,7 +479,7 @@ Channel(
|
|||
id: "UCh8gHdtzO2tXd593_bjErWg",
|
||||
name: "Doobydobap",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(2840000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -505,7 +505,7 @@ Channel(
|
|||
id: "UCh8gHdtzO2tXd593_bjErWg",
|
||||
name: "Doobydobap",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(2840000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -531,7 +531,7 @@ Channel(
|
|||
id: "UCh8gHdtzO2tXd593_bjErWg",
|
||||
name: "Doobydobap",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(2840000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -557,7 +557,7 @@ Channel(
|
|||
id: "UCh8gHdtzO2tXd593_bjErWg",
|
||||
name: "Doobydobap",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(2840000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -583,7 +583,7 @@ Channel(
|
|||
id: "UCh8gHdtzO2tXd593_bjErWg",
|
||||
name: "Doobydobap",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(2840000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -609,7 +609,7 @@ Channel(
|
|||
id: "UCh8gHdtzO2tXd593_bjErWg",
|
||||
name: "Doobydobap",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(2840000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -635,7 +635,7 @@ Channel(
|
|||
id: "UCh8gHdtzO2tXd593_bjErWg",
|
||||
name: "Doobydobap",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(2840000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -676,7 +676,7 @@ Channel(
|
|||
id: "UCh8gHdtzO2tXd593_bjErWg",
|
||||
name: "Doobydobap",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(2840000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -702,7 +702,7 @@ Channel(
|
|||
id: "UCh8gHdtzO2tXd593_bjErWg",
|
||||
name: "Doobydobap",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(2840000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -728,7 +728,7 @@ Channel(
|
|||
id: "UCh8gHdtzO2tXd593_bjErWg",
|
||||
name: "Doobydobap",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(2840000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -754,7 +754,7 @@ Channel(
|
|||
id: "UCh8gHdtzO2tXd593_bjErWg",
|
||||
name: "Doobydobap",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(2840000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -780,7 +780,7 @@ Channel(
|
|||
id: "UCh8gHdtzO2tXd593_bjErWg",
|
||||
name: "Doobydobap",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(2840000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -806,7 +806,7 @@ Channel(
|
|||
id: "UCh8gHdtzO2tXd593_bjErWg",
|
||||
name: "Doobydobap",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(2840000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -832,7 +832,7 @@ Channel(
|
|||
id: "UCh8gHdtzO2tXd593_bjErWg",
|
||||
name: "Doobydobap",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(2840000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -858,7 +858,7 @@ Channel(
|
|||
id: "UCh8gHdtzO2tXd593_bjErWg",
|
||||
name: "Doobydobap",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(2840000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -899,7 +899,7 @@ Channel(
|
|||
id: "UCh8gHdtzO2tXd593_bjErWg",
|
||||
name: "Doobydobap",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(2840000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -925,7 +925,7 @@ Channel(
|
|||
id: "UCh8gHdtzO2tXd593_bjErWg",
|
||||
name: "Doobydobap",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(2840000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
|
|
@ -25,7 +25,7 @@ Channel(
|
|||
height: 176,
|
||||
),
|
||||
],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
description: "BRAND NEW SECOND CHANNEL: https://youtube.com/channel/UCcsQYra-bISsFxNqnd6Javw\n\nJoin my Discord: https://discord.gg/2YcarWsc8S\n",
|
||||
tags: [
|
||||
"politics",
|
||||
|
@ -113,7 +113,7 @@ Channel(
|
|||
id: "UCcvfHa-GHSOHFAjU0-Ie57A",
|
||||
name: "Adam Something",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(947000),
|
||||
)),
|
||||
publish_date: Some("2022-09-27T16:00:00Z"),
|
||||
|
@ -154,7 +154,7 @@ Channel(
|
|||
id: "UCcvfHa-GHSOHFAjU0-Ie57A",
|
||||
name: "Adam Something",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(947000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -195,7 +195,7 @@ Channel(
|
|||
id: "UCcvfHa-GHSOHFAjU0-Ie57A",
|
||||
name: "Adam Something",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(947000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -236,7 +236,7 @@ Channel(
|
|||
id: "UCcvfHa-GHSOHFAjU0-Ie57A",
|
||||
name: "Adam Something",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(947000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -277,7 +277,7 @@ Channel(
|
|||
id: "UCcvfHa-GHSOHFAjU0-Ie57A",
|
||||
name: "Adam Something",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(947000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -318,7 +318,7 @@ Channel(
|
|||
id: "UCcvfHa-GHSOHFAjU0-Ie57A",
|
||||
name: "Adam Something",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(947000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -359,7 +359,7 @@ Channel(
|
|||
id: "UCcvfHa-GHSOHFAjU0-Ie57A",
|
||||
name: "Adam Something",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(947000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -400,7 +400,7 @@ Channel(
|
|||
id: "UCcvfHa-GHSOHFAjU0-Ie57A",
|
||||
name: "Adam Something",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(947000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -441,7 +441,7 @@ Channel(
|
|||
id: "UCcvfHa-GHSOHFAjU0-Ie57A",
|
||||
name: "Adam Something",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(947000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -482,7 +482,7 @@ Channel(
|
|||
id: "UCcvfHa-GHSOHFAjU0-Ie57A",
|
||||
name: "Adam Something",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(947000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -523,7 +523,7 @@ Channel(
|
|||
id: "UCcvfHa-GHSOHFAjU0-Ie57A",
|
||||
name: "Adam Something",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(947000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -564,7 +564,7 @@ Channel(
|
|||
id: "UCcvfHa-GHSOHFAjU0-Ie57A",
|
||||
name: "Adam Something",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(947000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -605,7 +605,7 @@ Channel(
|
|||
id: "UCcvfHa-GHSOHFAjU0-Ie57A",
|
||||
name: "Adam Something",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(947000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -646,7 +646,7 @@ Channel(
|
|||
id: "UCcvfHa-GHSOHFAjU0-Ie57A",
|
||||
name: "Adam Something",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(947000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -687,7 +687,7 @@ Channel(
|
|||
id: "UCcvfHa-GHSOHFAjU0-Ie57A",
|
||||
name: "Adam Something",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(947000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -728,7 +728,7 @@ Channel(
|
|||
id: "UCcvfHa-GHSOHFAjU0-Ie57A",
|
||||
name: "Adam Something",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(947000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -769,7 +769,7 @@ Channel(
|
|||
id: "UCcvfHa-GHSOHFAjU0-Ie57A",
|
||||
name: "Adam Something",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(947000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -810,7 +810,7 @@ Channel(
|
|||
id: "UCcvfHa-GHSOHFAjU0-Ie57A",
|
||||
name: "Adam Something",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(947000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -851,7 +851,7 @@ Channel(
|
|||
id: "UCcvfHa-GHSOHFAjU0-Ie57A",
|
||||
name: "Adam Something",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(947000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -892,7 +892,7 @@ Channel(
|
|||
id: "UCcvfHa-GHSOHFAjU0-Ie57A",
|
||||
name: "Adam Something",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(947000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -933,7 +933,7 @@ Channel(
|
|||
id: "UCcvfHa-GHSOHFAjU0-Ie57A",
|
||||
name: "Adam Something",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(947000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -974,7 +974,7 @@ Channel(
|
|||
id: "UCcvfHa-GHSOHFAjU0-Ie57A",
|
||||
name: "Adam Something",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(947000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -1015,7 +1015,7 @@ Channel(
|
|||
id: "UCcvfHa-GHSOHFAjU0-Ie57A",
|
||||
name: "Adam Something",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(947000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -1056,7 +1056,7 @@ Channel(
|
|||
id: "UCcvfHa-GHSOHFAjU0-Ie57A",
|
||||
name: "Adam Something",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(947000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -1097,7 +1097,7 @@ Channel(
|
|||
id: "UCcvfHa-GHSOHFAjU0-Ie57A",
|
||||
name: "Adam Something",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(947000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -1138,7 +1138,7 @@ Channel(
|
|||
id: "UCcvfHa-GHSOHFAjU0-Ie57A",
|
||||
name: "Adam Something",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(947000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -1179,7 +1179,7 @@ Channel(
|
|||
id: "UCcvfHa-GHSOHFAjU0-Ie57A",
|
||||
name: "Adam Something",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(947000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -1220,7 +1220,7 @@ Channel(
|
|||
id: "UCcvfHa-GHSOHFAjU0-Ie57A",
|
||||
name: "Adam Something",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(947000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -1261,7 +1261,7 @@ Channel(
|
|||
id: "UCcvfHa-GHSOHFAjU0-Ie57A",
|
||||
name: "Adam Something",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(947000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
@ -1302,7 +1302,7 @@ Channel(
|
|||
id: "UCcvfHa-GHSOHFAjU0-Ie57A",
|
||||
name: "Adam Something",
|
||||
avatar: [],
|
||||
verification: verified,
|
||||
verification: Verified,
|
||||
subscriber_count: Some(947000),
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
|
|
|
@ -1,943 +0,0 @@
|
|||
---
|
||||
source: src/client/music_artist.rs
|
||||
expression: artist
|
||||
---
|
||||
MusicArtist(
|
||||
id: "UCOR4_bSVIXPsGa4BbCSt60Q",
|
||||
name: "Trailerpark",
|
||||
header_image: [
|
||||
Thumbnail(
|
||||
url: "https://lh3.googleusercontent.com/II101BviJo-tGcGg1KKWSU8D3EZjALHQMbQ4v-7-hP4Zfk1pBESaTCLcz8eQb-hggzxq4Z1MuFkBeRE=w540-h225-p-l90-rj",
|
||||
width: 540,
|
||||
height: 225,
|
||||
),
|
||||
Thumbnail(
|
||||
url: "https://lh3.googleusercontent.com/II101BviJo-tGcGg1KKWSU8D3EZjALHQMbQ4v-7-hP4Zfk1pBESaTCLcz8eQb-hggzxq4Z1MuFkBeRE=w721-h300-p-l90-rj",
|
||||
width: 721,
|
||||
height: 300,
|
||||
),
|
||||
],
|
||||
description: None,
|
||||
wikipedia_url: None,
|
||||
subscriber_count: Some(270000),
|
||||
tracks: [
|
||||
TrackItem(
|
||||
id: "YvidasjVLXk",
|
||||
name: "Bleib in der Schule",
|
||||
duration: None,
|
||||
cover: [
|
||||
Thumbnail(
|
||||
url: "https://lh3.googleusercontent.com/V_tvMqbuXgDgoAKuYZ-VFRru3cUb2WQvwO6vVBKY8pdFYAl1dkuIv_W2afjMUNN6uVNxet6r7mHISh0s=w60-h60-l90-rj",
|
||||
width: 60,
|
||||
height: 60,
|
||||
),
|
||||
Thumbnail(
|
||||
url: "https://lh3.googleusercontent.com/V_tvMqbuXgDgoAKuYZ-VFRru3cUb2WQvwO6vVBKY8pdFYAl1dkuIv_W2afjMUNN6uVNxet6r7mHISh0s=w120-h120-l90-rj",
|
||||
width: 120,
|
||||
height: 120,
|
||||
),
|
||||
],
|
||||
artists: [
|
||||
ArtistId(
|
||||
id: Some("UCOR4_bSVIXPsGa4BbCSt60Q"),
|
||||
name: "Trailerpark",
|
||||
),
|
||||
],
|
||||
artist_id: Some("UCOR4_bSVIXPsGa4BbCSt60Q"),
|
||||
album: Some(AlbumId(
|
||||
id: "MPREb_8PsIyll0LFV",
|
||||
name: "Bleib in der Schule",
|
||||
)),
|
||||
view_count: Some(71000000),
|
||||
track_type: track,
|
||||
track_nr: None,
|
||||
by_va: false,
|
||||
),
|
||||
TrackItem(
|
||||
id: "h3T_NXRUUjM",
|
||||
name: "Fledermausland",
|
||||
duration: None,
|
||||
cover: [
|
||||
Thumbnail(
|
||||
url: "https://lh3.googleusercontent.com/1fPBoTszY4e6Nf8egSwBTHWsQT8hotwhDnjArd1SHS8gZc5asCoo_3Z2WhN1IO2KMqyYly0xm7mMZ43d=w60-h60-l90-rj",
|
||||
width: 60,
|
||||
height: 60,
|
||||
),
|
||||
Thumbnail(
|
||||
url: "https://lh3.googleusercontent.com/1fPBoTszY4e6Nf8egSwBTHWsQT8hotwhDnjArd1SHS8gZc5asCoo_3Z2WhN1IO2KMqyYly0xm7mMZ43d=w120-h120-l90-rj",
|
||||
width: 120,
|
||||
height: 120,
|
||||
),
|
||||
],
|
||||
artists: [
|
||||
ArtistId(
|
||||
id: Some("UCOR4_bSVIXPsGa4BbCSt60Q"),
|
||||
name: "Trailerpark",
|
||||
),
|
||||
],
|
||||
artist_id: Some("UCOR4_bSVIXPsGa4BbCSt60Q"),
|
||||
album: Some(AlbumId(
|
||||
id: "MPREb_POeT6m0bw9q",
|
||||
name: "Crackstreet Boys II X Version",
|
||||
)),
|
||||
view_count: Some(30000000),
|
||||
track_type: track,
|
||||
track_nr: None,
|
||||
by_va: false,
|
||||
),
|
||||
TrackItem(
|
||||
id: "XZfoFwWvkGQ",
|
||||
name: "Sterben kannst du überall",
|
||||
duration: None,
|
||||
cover: [
|
||||
Thumbnail(
|
||||
url: "https://lh3.googleusercontent.com/eQCwnR4YLYnizEhQKeSDDE3rulSTo64cTfs8fxR1K-3iWUfC477SHV0ZOOoQa2vJuvr_9i_WDYI-wbo=w60-h60-l90-rj",
|
||||
width: 60,
|
||||
height: 60,
|
||||
),
|
||||
Thumbnail(
|
||||
url: "https://lh3.googleusercontent.com/eQCwnR4YLYnizEhQKeSDDE3rulSTo64cTfs8fxR1K-3iWUfC477SHV0ZOOoQa2vJuvr_9i_WDYI-wbo=w120-h120-l90-rj",
|
||||
width: 120,
|
||||
height: 120,
|
||||
),
|
||||
],
|
||||
artists: [
|
||||
ArtistId(
|
||||
id: Some("UCOR4_bSVIXPsGa4BbCSt60Q"),
|
||||
name: "Trailerpark",
|
||||
),
|
||||
],
|
||||
artist_id: Some("UCOR4_bSVIXPsGa4BbCSt60Q"),
|
||||
album: Some(AlbumId(
|
||||
id: "MPREb_UYdRV1nnK2J",
|
||||
name: "TP4L",
|
||||
)),
|
||||
view_count: Some(40000000),
|
||||
track_type: track,
|
||||
track_nr: None,
|
||||
by_va: false,
|
||||
),
|
||||
TrackItem(
|
||||
id: "LOuVxwVFJhs",
|
||||
name: "Selbstbefriedigung",
|
||||
duration: None,
|
||||
cover: [
|
||||
Thumbnail(
|
||||
url: "https://lh3.googleusercontent.com/1fPBoTszY4e6Nf8egSwBTHWsQT8hotwhDnjArd1SHS8gZc5asCoo_3Z2WhN1IO2KMqyYly0xm7mMZ43d=w60-h60-l90-rj",
|
||||
width: 60,
|
||||
height: 60,
|
||||
),
|
||||
Thumbnail(
|
||||
url: "https://lh3.googleusercontent.com/1fPBoTszY4e6Nf8egSwBTHWsQT8hotwhDnjArd1SHS8gZc5asCoo_3Z2WhN1IO2KMqyYly0xm7mMZ43d=w120-h120-l90-rj",
|
||||
width: 120,
|
||||
height: 120,
|
||||
),
|
||||
],
|
||||
artists: [
|
||||
ArtistId(
|
||||
id: Some("UCOR4_bSVIXPsGa4BbCSt60Q"),
|
||||
name: "Trailerpark",
|
||||
),
|
||||
],
|
||||
artist_id: Some("UCOR4_bSVIXPsGa4BbCSt60Q"),
|
||||
album: Some(AlbumId(
|
||||
id: "MPREb_POeT6m0bw9q",
|
||||
name: "Crackstreet Boys II X Version",
|
||||
)),
|
||||
view_count: Some(13000000),
|
||||
track_type: track,
|
||||
track_nr: None,
|
||||
by_va: false,
|
||||
),
|
||||
TrackItem(
|
||||
id: "GePZUYeIQQQ",
|
||||
name: "Falsche Band",
|
||||
duration: None,
|
||||
cover: [
|
||||
Thumbnail(
|
||||
url: "https://lh3.googleusercontent.com/MIuap-H2LxqP5O7Dry1LdShBFBbg5YTjIPjuXOHWyrKlmnOogsO5cTk6yXH97DhI3WjZg0z3y-jkQxaM=w60-h60-l90-rj",
|
||||
width: 60,
|
||||
height: 60,
|
||||
),
|
||||
Thumbnail(
|
||||
url: "https://lh3.googleusercontent.com/MIuap-H2LxqP5O7Dry1LdShBFBbg5YTjIPjuXOHWyrKlmnOogsO5cTk6yXH97DhI3WjZg0z3y-jkQxaM=w120-h120-l90-rj",
|
||||
width: 120,
|
||||
height: 120,
|
||||
),
|
||||
],
|
||||
artists: [
|
||||
ArtistId(
|
||||
id: Some("UCOR4_bSVIXPsGa4BbCSt60Q"),
|
||||
name: "Trailerpark",
|
||||
),
|
||||
],
|
||||
artist_id: Some("UCOR4_bSVIXPsGa4BbCSt60Q"),
|
||||
album: Some(AlbumId(
|
||||
id: "MPREb_bi34SGT1xlc",
|
||||
name: "Crackstreet Boys 3 (Bonus Tracks Version)",
|
||||
)),
|
||||
view_count: Some(13000000),
|
||||
track_type: track,
|
||||
track_nr: None,
|
||||
by_va: false,
|
||||
),
|
||||
TrackItem(
|
||||
id: "0mcING0Zdis",
|
||||
name: "Trailerpark - TP4L (Live Abschiedskonzert)",
|
||||
duration: None,
|
||||
cover: [
|
||||
Thumbnail(
|
||||
url: "https://i.ytimg.com/vi/0mcING0Zdis/sddefault.jpg?sqp=-oaymwEWCJADEOEBIAQqCghqEJQEGHgg6AJIWg&rs=AMzJL3k5JY0WRBeKNaotfUYrpbObz1mceA",
|
||||
width: 400,
|
||||
height: 225,
|
||||
),
|
||||
Thumbnail(
|
||||
url: "https://i.ytimg.com/vi/0mcING0Zdis/hq720.jpg?sqp=-oaymwEXCKAGEMIDIAQqCwjVARCqCBh4INgESFo&rs=AMzJL3kinVfBJUF-SDFagYKazKmS_ad75w",
|
||||
width: 800,
|
||||
height: 450,
|
||||
),
|
||||
],
|
||||
artists: [
|
||||
ArtistId(
|
||||
id: Some("UCOR4_bSVIXPsGa4BbCSt60Q"),
|
||||
name: "Trailerpark",
|
||||
),
|
||||
],
|
||||
artist_id: Some("UCOR4_bSVIXPsGa4BbCSt60Q"),
|
||||
album: None,
|
||||
view_count: Some(13000),
|
||||
track_type: video,
|
||||
track_nr: None,
|
||||
by_va: false,
|
||||
),
|
||||
TrackItem(
|
||||
id: "EAC-2ttHCyk",
|
||||
name: "Fledermausland (Bonus Track)",
|
||||
duration: None,
|
||||
cover: [
|
||||
Thumbnail(
|
||||
url: "https://i.ytimg.com/vi/EAC-2ttHCyk/sddefault.jpg?sqp=-oaymwEWCJADEOEBIAQqCghqEJQEGHgg6AJIWg&rs=AMzJL3nlrgFTz_pwbBwXFbaASgklpX78vA",
|
||||
width: 400,
|
||||
height: 225,
|
||||
),
|
||||
Thumbnail(
|
||||
url: "https://i.ytimg.com/vi/EAC-2ttHCyk/hq720.jpg?sqp=-oaymwEXCKAGEMIDIAQqCwjVARCqCBh4INgESFo&rs=AMzJL3nHzhiahqhmIkZ0eUXD09BGak2MHQ",
|
||||
width: 800,
|
||||
height: 450,
|
||||
),
|
||||
],
|
||||
artists: [
|
||||
ArtistId(
|
||||
id: Some("UCOR4_bSVIXPsGa4BbCSt60Q"),
|
||||
name: "Trailerpark",
|
||||
),
|
||||
],
|
||||
artist_id: Some("UCOR4_bSVIXPsGa4BbCSt60Q"),
|
||||
album: None,
|
||||
view_count: Some(25000000),
|
||||
track_type: video,
|
||||
track_nr: None,
|
||||
by_va: false,
|
||||
),
|
||||
TrackItem(
|
||||
id: "Bret5VaVzJk",
|
||||
name: "New Kids on the Blech (Bonus Track)",
|
||||
duration: None,
|
||||
cover: [
|
||||
Thumbnail(
|
||||
url: "https://i.ytimg.com/vi/Bret5VaVzJk/sddefault.jpg?sqp=-oaymwEWCJADEOEBIAQqCghqEJQEGHgg6AJIWg&rs=AMzJL3nFa4qUxqJzCtxr-zPdzP15Ixvu-A",
|
||||
width: 400,
|
||||
height: 225,
|
||||
),
|
||||
Thumbnail(
|
||||
url: "https://i.ytimg.com/vi/Bret5VaVzJk/hq720.jpg?sqp=-oaymwEXCKAGEMIDIAQqCwjVARCqCBh4INgESFo&rs=AMzJL3l1hGZVAWUwaJQbZXmbRpcbsMdTeQ",
|
||||
width: 800,
|
||||
height: 450,
|
||||
),
|
||||
],
|
||||
artists: [
|
||||
ArtistId(
|
||||
id: Some("UCOR4_bSVIXPsGa4BbCSt60Q"),
|
||||
name: "Trailerpark",
|
||||
),
|
||||
],
|
||||
artist_id: Some("UCOR4_bSVIXPsGa4BbCSt60Q"),
|
||||
album: None,
|
||||
view_count: Some(6900000),
|
||||
track_type: video,
|
||||
track_nr: None,
|
||||
by_va: false,
|
||||
),
|
||||
TrackItem(
|
||||
id: "EqP1_IcjW-s",
|
||||
name: "Pimpulsiv feat. DNP, Sudden & Dana - Wohnwagensiedlung",
|
||||
duration: None,
|
||||
cover: [
|
||||
Thumbnail(
|
||||
url: "https://i.ytimg.com/vi/EqP1_IcjW-s/sddefault.jpg?sqp=-oaymwEWCJADEOEBIAQqCghqEJQEGHgg6AJIWg&rs=AMzJL3lIeltSLpA_XwwZzdJfHnNZ0vqBzA",
|
||||
width: 400,
|
||||
height: 225,
|
||||
),
|
||||
Thumbnail(
|
||||
url: "https://i.ytimg.com/vi/EqP1_IcjW-s/hq720.jpg?sqp=-oaymwEXCKAGEMIDIAQqCwjVARCqCBh4INgESFo&rs=AMzJL3nfiByY3RfcFYGfg92C5Vlkar0GJA",
|
||||
width: 800,
|
||||
height: 450,
|
||||
),
|
||||
],
|
||||
artists: [
|
||||
ArtistId(
|
||||
id: Some("UCOR4_bSVIXPsGa4BbCSt60Q"),
|
||||
name: "Trailerpark",
|
||||
),
|
||||
],
|
||||
artist_id: Some("UCOR4_bSVIXPsGa4BbCSt60Q"),
|
||||
album: None,
|
||||
view_count: Some(7100000),
|
||||
track_type: video,
|
||||
track_nr: None,
|
||||
by_va: false,
|
||||
),
|
||||
TrackItem(
|
||||
id: "3EoF9Of98e4",
|
||||
name: "Armut treibt Jugendliche in die Popmusik",
|
||||
duration: None,
|
||||
cover: [
|
||||
Thumbnail(
|
||||
url: "https://i.ytimg.com/vi/3EoF9Of98e4/sddefault.jpg?sqp=-oaymwEWCJADEOEBIAQqCghqEJQEGHgg6AJIWg&rs=AMzJL3kvWHX-5mYREKEkf-CM3TLfjrLjlw",
|
||||
width: 400,
|
||||
height: 225,
|
||||
),
|
||||
Thumbnail(
|
||||
url: "https://i.ytimg.com/vi/3EoF9Of98e4/hq720.jpg?sqp=-oaymwEXCKAGEMIDIAQqCwjVARCqCBh4INgESFo&rs=AMzJL3lItzsg6wamh_xSdpoZxTWOHHLS-g",
|
||||
width: 800,
|
||||
height: 450,
|
||||
),
|
||||
],
|
||||
artists: [
|
||||
ArtistId(
|
||||
id: Some("UCOR4_bSVIXPsGa4BbCSt60Q"),
|
||||
name: "Trailerpark",
|
||||
),
|
||||
],
|
||||
artist_id: Some("UCOR4_bSVIXPsGa4BbCSt60Q"),
|
||||
album: None,
|
||||
view_count: Some(5400000),
|
||||
track_type: video,
|
||||
track_nr: None,
|
||||
by_va: false,
|
||||
),
|
||||
TrackItem(
|
||||
id: "qr0eN_uIcTs",
|
||||
name: "Bleib in der Schule (Live in Berlin)",
|
||||
duration: None,
|
||||
cover: [
|
||||
Thumbnail(
|
||||
url: "https://i.ytimg.com/vi/qr0eN_uIcTs/sddefault.jpg?sqp=-oaymwEWCJADEOEBIAQqCghqEJQEGHgg6AJIWg&rs=AMzJL3nspTbohIYzDFOjTg90KEmKecVVvg",
|
||||
width: 400,
|
||||
height: 225,
|
||||
),
|
||||
Thumbnail(
|
||||
url: "https://i.ytimg.com/vi/qr0eN_uIcTs/hq720.jpg?sqp=-oaymwEXCKAGEMIDIAQqCwjVARCqCBh4INgESFo&rs=AMzJL3n0SIeq4dPTPvbGv4STsTWNt24cig",
|
||||
width: 800,
|
||||
height: 450,
|
||||
),
|
||||
],
|
||||
artists: [
|
||||
ArtistId(
|
||||
id: Some("UCOR4_bSVIXPsGa4BbCSt60Q"),
|
||||
name: "Trailerpark",
|
||||
),
|
||||
],
|
||||
artist_id: Some("UCOR4_bSVIXPsGa4BbCSt60Q"),
|
||||
album: None,
|
||||
view_count: Some(56000),
|
||||
track_type: video,
|
||||
track_nr: None,
|
||||
by_va: false,
|
||||
),
|
||||
TrackItem(
|
||||
id: "McgSyiug6XE",
|
||||
name: "We Are Family",
|
||||
duration: None,
|
||||
cover: [
|
||||
Thumbnail(
|
||||
url: "https://i.ytimg.com/vi/McgSyiug6XE/sddefault.jpg?sqp=-oaymwEWCJADEOEBIAQqCghqEJQEGHgg6AJIWg&rs=AMzJL3nxe3Xz99BVFg-VOra20J682me5JQ",
|
||||
width: 400,
|
||||
height: 225,
|
||||
),
|
||||
Thumbnail(
|
||||
url: "https://i.ytimg.com/vi/McgSyiug6XE/hq720.jpg?sqp=-oaymwEXCKAGEMIDIAQqCwjVARCqCBh4INgESFo&rs=AMzJL3lSGwKx_hnqYA-CkoLHapr1PiyX6w",
|
||||
width: 800,
|
||||
height: 450,
|
||||
),
|
||||
],
|
||||
artists: [
|
||||
ArtistId(
|
||||
id: Some("UCOR4_bSVIXPsGa4BbCSt60Q"),
|
||||
name: "Trailerpark",
|
||||
),
|
||||
ArtistId(
|
||||
id: Some("UC5HSrFHr6lMzwAyGjlClm0A"),
|
||||
name: "Timi Hendrix",
|
||||
),
|
||||
],
|
||||
artist_id: Some("UCOR4_bSVIXPsGa4BbCSt60Q"),
|
||||
album: None,
|
||||
view_count: Some(1800000),
|
||||
track_type: video,
|
||||
track_nr: None,
|
||||
by_va: false,
|
||||
),
|
||||
TrackItem(
|
||||
id: "ioZxvVhjFs8",
|
||||
name: "Schlechter Tag",
|
||||
duration: None,
|
||||
cover: [
|
||||
Thumbnail(
|
||||
url: "https://i.ytimg.com/vi/ioZxvVhjFs8/sddefault.jpg?sqp=-oaymwEWCJADEOEBIAQqCghqEJQEGHgg6AJIWg&rs=AMzJL3ltQmZbH1DF9nmho5HLGehqLSGzTw",
|
||||
width: 400,
|
||||
height: 225,
|
||||
),
|
||||
Thumbnail(
|
||||
url: "https://i.ytimg.com/vi/ioZxvVhjFs8/hq720.jpg?sqp=-oaymwEXCKAGEMIDIAQqCwjVARCqCBh4INgESFo&rs=AMzJL3lsluKPeCNxP7QoOCc24tZy4jsn7Q",
|
||||
width: 800,
|
||||
height: 450,
|
||||
),
|
||||
],
|
||||
artists: [
|
||||
ArtistId(
|
||||
id: Some("UCOR4_bSVIXPsGa4BbCSt60Q"),
|
||||
name: "Trailerpark",
|
||||
),
|
||||
],
|
||||
artist_id: Some("UCOR4_bSVIXPsGa4BbCSt60Q"),
|
||||
album: None,
|
||||
view_count: Some(7100000),
|
||||
track_type: video,
|
||||
track_nr: None,
|
||||
by_va: false,
|
||||
),
|
||||
TrackItem(
|
||||
id: "3jyZJEcomkw",
|
||||
name: "Timi Hendrix feat. Alligatoah - Schlaflos in Guantanamo ► prod. by Mantra",
|
||||
duration: None,
|
||||
cover: [
|
||||
Thumbnail(
|
||||
url: "https://i.ytimg.com/vi/3jyZJEcomkw/sddefault.jpg?sqp=-oaymwEWCJADEOEBIAQqCghqEJQEGHgg6AJIWg&rs=AMzJL3k46-OFTCnpEJry_PNst1C11FPA1A",
|
||||
width: 400,
|
||||
height: 225,
|
||||
),
|
||||
Thumbnail(
|
||||
url: "https://i.ytimg.com/vi/3jyZJEcomkw/hq720.jpg?sqp=-oaymwEXCKAGEMIDIAQqCwjVARCqCBh4INgESFo&rs=AMzJL3kN1ryaQSy4M_Y9bQGh9S-tbYGqdg",
|
||||
width: 800,
|
||||
height: 450,
|
||||
),
|
||||
],
|
||||
artists: [
|
||||
ArtistId(
|
||||
id: Some("UCOR4_bSVIXPsGa4BbCSt60Q"),
|
||||
name: "Trailerpark",
|
||||
),
|
||||
],
|
||||
artist_id: Some("UCOR4_bSVIXPsGa4BbCSt60Q"),
|
||||
album: None,
|
||||
view_count: Some(1500000),
|
||||
track_type: video,
|
||||
track_nr: None,
|
||||
by_va: false,
|
||||
),
|
||||
TrackItem(
|
||||
id: "9oM-cflYhGk",
|
||||
name: "Timi Hendrix - Kaiser von China (Official Video) 🐲",
|
||||
duration: None,
|
||||
cover: [
|
||||
Thumbnail(
|
||||
url: "https://i.ytimg.com/vi/9oM-cflYhGk/sddefault.jpg?sqp=-oaymwEWCJADEOEBIAQqCghqEJQEGHgg6AJIWg&rs=AMzJL3m6MksfA1NWyIMv6cTk03J21pA0NQ",
|
||||
width: 400,
|
||||
height: 225,
|
||||
),
|
||||
Thumbnail(
|
||||
url: "https://i.ytimg.com/vi/9oM-cflYhGk/hq720.jpg?sqp=-oaymwEXCKAGEMIDIAQqCwjVARCqCBh4INgESFo&rs=AMzJL3n7oy_XobzQBkUVxEx08iSKNPIB0Q",
|
||||
width: 800,
|
||||
height: 450,
|
||||
),
|
||||
],
|
||||
artists: [
|
||||
ArtistId(
|
||||
id: Some("UCOR4_bSVIXPsGa4BbCSt60Q"),
|
||||
name: "Trailerpark",
|
||||
),
|
||||
],
|
||||
artist_id: Some("UCOR4_bSVIXPsGa4BbCSt60Q"),
|
||||
album: None,
|
||||
view_count: Some(1100000),
|
||||
track_type: video,
|
||||
track_nr: None,
|
||||
by_va: false,
|
||||
),
|
||||
],
|
||||
albums: [
|
||||
AlbumItem(
|
||||
id: "MPREb_UYdRV1nnK2J",
|
||||
name: "TP4L",
|
||||
cover: [
|
||||
Thumbnail(
|
||||
url: "https://lh3.googleusercontent.com/eQCwnR4YLYnizEhQKeSDDE3rulSTo64cTfs8fxR1K-3iWUfC477SHV0ZOOoQa2vJuvr_9i_WDYI-wbo=w226-h226-l90-rj",
|
||||
width: 226,
|
||||
height: 226,
|
||||
),
|
||||
Thumbnail(
|
||||
url: "https://lh3.googleusercontent.com/eQCwnR4YLYnizEhQKeSDDE3rulSTo64cTfs8fxR1K-3iWUfC477SHV0ZOOoQa2vJuvr_9i_WDYI-wbo=w544-h544-l90-rj",
|
||||
width: 544,
|
||||
height: 544,
|
||||
),
|
||||
],
|
||||
artists: [
|
||||
ArtistId(
|
||||
id: Some("UCOR4_bSVIXPsGa4BbCSt60Q"),
|
||||
name: "Trailerpark",
|
||||
),
|
||||
],
|
||||
artist_id: Some("UCOR4_bSVIXPsGa4BbCSt60Q"),
|
||||
album_type: album,
|
||||
year: Some(2017),
|
||||
by_va: false,
|
||||
),
|
||||
AlbumItem(
|
||||
id: "MPREb_bi34SGT1xlc",
|
||||
name: "Crackstreet Boys 3 (Bonus Tracks Version)",
|
||||
cover: [
|
||||
Thumbnail(
|
||||
url: "https://lh3.googleusercontent.com/MIuap-H2LxqP5O7Dry1LdShBFBbg5YTjIPjuXOHWyrKlmnOogsO5cTk6yXH97DhI3WjZg0z3y-jkQxaM=w226-h226-l90-rj",
|
||||
width: 226,
|
||||
height: 226,
|
||||
),
|
||||
Thumbnail(
|
||||
url: "https://lh3.googleusercontent.com/MIuap-H2LxqP5O7Dry1LdShBFBbg5YTjIPjuXOHWyrKlmnOogsO5cTk6yXH97DhI3WjZg0z3y-jkQxaM=w544-h544-l90-rj",
|
||||
width: 544,
|
||||
height: 544,
|
||||
),
|
||||
],
|
||||
artists: [
|
||||
ArtistId(
|
||||
id: Some("UCOR4_bSVIXPsGa4BbCSt60Q"),
|
||||
name: "Trailerpark",
|
||||
),
|
||||
],
|
||||
artist_id: Some("UCOR4_bSVIXPsGa4BbCSt60Q"),
|
||||
album_type: album,
|
||||
year: Some(2014),
|
||||
by_va: false,
|
||||
),
|
||||
AlbumItem(
|
||||
id: "MPREb_5gkbwhqC4AJ",
|
||||
name: "Goldener Schluss (Live in Berlin)",
|
||||
cover: [
|
||||
Thumbnail(
|
||||
url: "https://lh3.googleusercontent.com/ilzR9UxpZFwHZnYOL0L504H6a0Y8k_zPk0AYOhBiBqIjq4TGnX-B1uKcNah56dmjPZoDvp9vGWyfgY8=w226-h226-l90-rj",
|
||||
width: 226,
|
||||
height: 226,
|
||||
),
|
||||
Thumbnail(
|
||||
url: "https://lh3.googleusercontent.com/ilzR9UxpZFwHZnYOL0L504H6a0Y8k_zPk0AYOhBiBqIjq4TGnX-B1uKcNah56dmjPZoDvp9vGWyfgY8=w544-h544-l90-rj",
|
||||
width: 544,
|
||||
height: 544,
|
||||
),
|
||||
],
|
||||
artists: [
|
||||
ArtistId(
|
||||
id: Some("UCOR4_bSVIXPsGa4BbCSt60Q"),
|
||||
name: "Trailerpark",
|
||||
),
|
||||
],
|
||||
artist_id: Some("UCOR4_bSVIXPsGa4BbCSt60Q"),
|
||||
album_type: album,
|
||||
year: Some(2024),
|
||||
by_va: false,
|
||||
),
|
||||
AlbumItem(
|
||||
id: "MPREb_HPXN9BBzFpV",
|
||||
name: "TP4L",
|
||||
cover: [
|
||||
Thumbnail(
|
||||
url: "https://lh3.googleusercontent.com/8Ftr5oIt1q6RbGkdiW7cefw-XGUplUXcjXXN7QntI1Nzh_6oR0euh7Lj2Ner3yXV--U-hVxJewkeq8A=w226-h226-l90-rj",
|
||||
width: 226,
|
||||
height: 226,
|
||||
),
|
||||
Thumbnail(
|
||||
url: "https://lh3.googleusercontent.com/8Ftr5oIt1q6RbGkdiW7cefw-XGUplUXcjXXN7QntI1Nzh_6oR0euh7Lj2Ner3yXV--U-hVxJewkeq8A=w544-h544-l90-rj",
|
||||
width: 544,
|
||||
height: 544,
|
||||
),
|
||||
],
|
||||
artists: [
|
||||
ArtistId(
|
||||
id: Some("UCOR4_bSVIXPsGa4BbCSt60Q"),
|
||||
name: "Trailerpark",
|
||||
),
|
||||
],
|
||||
artist_id: Some("UCOR4_bSVIXPsGa4BbCSt60Q"),
|
||||
album_type: single,
|
||||
year: Some(2017),
|
||||
by_va: false,
|
||||
),
|
||||
AlbumItem(
|
||||
id: "MPREb_hcK0fXETEf9",
|
||||
name: "Endlich normale Leute",
|
||||
cover: [
|
||||
Thumbnail(
|
||||
url: "https://lh3.googleusercontent.com/MW37LppS1rjDQIl5GaG0BxKeWk5fs4xphr6rU0z-KmJiHbvMbA3K5ZzrA9avinP2LjNrDGwB5tSLLsqe=w226-h226-l90-rj",
|
||||
width: 226,
|
||||
height: 226,
|
||||
),
|
||||
Thumbnail(
|
||||
url: "https://lh3.googleusercontent.com/MW37LppS1rjDQIl5GaG0BxKeWk5fs4xphr6rU0z-KmJiHbvMbA3K5ZzrA9avinP2LjNrDGwB5tSLLsqe=w544-h544-l90-rj",
|
||||
width: 544,
|
||||
height: 544,
|
||||
),
|
||||
],
|
||||
artists: [
|
||||
ArtistId(
|
||||
id: Some("UCOR4_bSVIXPsGa4BbCSt60Q"),
|
||||
name: "Trailerpark",
|
||||
),
|
||||
],
|
||||
artist_id: Some("UCOR4_bSVIXPsGa4BbCSt60Q"),
|
||||
album_type: single,
|
||||
year: Some(2017),
|
||||
by_va: false,
|
||||
),
|
||||
AlbumItem(
|
||||
id: "MPREb_R6EV2L1q0oc",
|
||||
name: "Armut treibt Jugendliche in die Popmusik",
|
||||
cover: [
|
||||
Thumbnail(
|
||||
url: "https://lh3.googleusercontent.com/kqKBF4JPQhKY1099AzRpJFGc2P7TFuFa2GeM7z8GGfTJ_DkfAzKTdV8gPtfVkyA5HQ0uZn3XG-VtMVj0=w226-h226-l90-rj",
|
||||
width: 226,
|
||||
height: 226,
|
||||
),
|
||||
Thumbnail(
|
||||
url: "https://lh3.googleusercontent.com/kqKBF4JPQhKY1099AzRpJFGc2P7TFuFa2GeM7z8GGfTJ_DkfAzKTdV8gPtfVkyA5HQ0uZn3XG-VtMVj0=w544-h544-l90-rj",
|
||||
width: 544,
|
||||
height: 544,
|
||||
),
|
||||
],
|
||||
artists: [
|
||||
ArtistId(
|
||||
id: Some("UCOR4_bSVIXPsGa4BbCSt60Q"),
|
||||
name: "Trailerpark",
|
||||
),
|
||||
],
|
||||
artist_id: Some("UCOR4_bSVIXPsGa4BbCSt60Q"),
|
||||
album_type: single,
|
||||
year: Some(2017),
|
||||
by_va: false,
|
||||
),
|
||||
AlbumItem(
|
||||
id: "MPREb_oHieBHkXn3A",
|
||||
name: "Dicks Sucken",
|
||||
cover: [
|
||||
Thumbnail(
|
||||
url: "https://lh3.googleusercontent.com/IVvdOUgbTECe2cVKrwhhCYmhHuipV6p0t5cLqMYWm3E_23zBEABxodGiSuX3H_AxRcEZk2-4V-k3RZw6=w226-h226-l90-rj",
|
||||
width: 226,
|
||||
height: 226,
|
||||
),
|
||||
Thumbnail(
|
||||
url: "https://lh3.googleusercontent.com/IVvdOUgbTECe2cVKrwhhCYmhHuipV6p0t5cLqMYWm3E_23zBEABxodGiSuX3H_AxRcEZk2-4V-k3RZw6=w544-h544-l90-rj",
|
||||
width: 544,
|
||||
height: 544,
|
||||
),
|
||||
],
|
||||
artists: [
|
||||
ArtistId(
|
||||
id: Some("UCOR4_bSVIXPsGa4BbCSt60Q"),
|
||||
name: "Trailerpark",
|
||||
),
|
||||
],
|
||||
artist_id: Some("UCOR4_bSVIXPsGa4BbCSt60Q"),
|
||||
album_type: single,
|
||||
year: Some(2014),
|
||||
by_va: false,
|
||||
),
|
||||
AlbumItem(
|
||||
id: "MPREb_8PsIyll0LFV",
|
||||
name: "Bleib in der Schule",
|
||||
cover: [
|
||||
Thumbnail(
|
||||
url: "https://lh3.googleusercontent.com/V_tvMqbuXgDgoAKuYZ-VFRru3cUb2WQvwO6vVBKY8pdFYAl1dkuIv_W2afjMUNN6uVNxet6r7mHISh0s=w226-h226-l90-rj",
|
||||
width: 226,
|
||||
height: 226,
|
||||
),
|
||||
Thumbnail(
|
||||
url: "https://lh3.googleusercontent.com/V_tvMqbuXgDgoAKuYZ-VFRru3cUb2WQvwO6vVBKY8pdFYAl1dkuIv_W2afjMUNN6uVNxet6r7mHISh0s=w544-h544-l90-rj",
|
||||
width: 544,
|
||||
height: 544,
|
||||
),
|
||||
],
|
||||
artists: [
|
||||
ArtistId(
|
||||
id: Some("UCOR4_bSVIXPsGa4BbCSt60Q"),
|
||||
name: "Trailerpark",
|
||||
),
|
||||
],
|
||||
artist_id: Some("UCOR4_bSVIXPsGa4BbCSt60Q"),
|
||||
album_type: single,
|
||||
year: Some(2014),
|
||||
by_va: false,
|
||||
),
|
||||
AlbumItem(
|
||||
id: "MPREb_POeT6m0bw9q",
|
||||
name: "Crackstreet Boys II X Version",
|
||||
cover: [
|
||||
Thumbnail(
|
||||
url: "https://lh3.googleusercontent.com/1fPBoTszY4e6Nf8egSwBTHWsQT8hotwhDnjArd1SHS8gZc5asCoo_3Z2WhN1IO2KMqyYly0xm7mMZ43d=w226-h226-l90-rj",
|
||||
width: 226,
|
||||
height: 226,
|
||||
),
|
||||
Thumbnail(
|
||||
url: "https://lh3.googleusercontent.com/1fPBoTszY4e6Nf8egSwBTHWsQT8hotwhDnjArd1SHS8gZc5asCoo_3Z2WhN1IO2KMqyYly0xm7mMZ43d=w544-h544-l90-rj",
|
||||
width: 544,
|
||||
height: 544,
|
||||
),
|
||||
],
|
||||
artists: [
|
||||
ArtistId(
|
||||
id: Some("UCOR4_bSVIXPsGa4BbCSt60Q"),
|
||||
name: "Trailerpark",
|
||||
),
|
||||
],
|
||||
artist_id: Some("UCOR4_bSVIXPsGa4BbCSt60Q"),
|
||||
album_type: ep,
|
||||
year: Some(2014),
|
||||
by_va: false,
|
||||
),
|
||||
AlbumItem(
|
||||
id: "MPREb_tdFqP579jQz",
|
||||
name: "Bleib in der Schule (Live in Berlin)",
|
||||
cover: [
|
||||
Thumbnail(
|
||||
url: "https://lh3.googleusercontent.com/VNjspSA1Fm0yFJEKUCuetOziiET6sQG9QXQCiydknEny98Lc_MEmUp8e37FtCbDz1bQ6yvM6AqpsvL0=w226-h226-l90-rj",
|
||||
width: 226,
|
||||
height: 226,
|
||||
),
|
||||
Thumbnail(
|
||||
url: "https://lh3.googleusercontent.com/VNjspSA1Fm0yFJEKUCuetOziiET6sQG9QXQCiydknEny98Lc_MEmUp8e37FtCbDz1bQ6yvM6AqpsvL0=w544-h544-l90-rj",
|
||||
width: 544,
|
||||
height: 544,
|
||||
),
|
||||
],
|
||||
artists: [
|
||||
ArtistId(
|
||||
id: Some("UCOR4_bSVIXPsGa4BbCSt60Q"),
|
||||
name: "Trailerpark",
|
||||
),
|
||||
],
|
||||
artist_id: Some("UCOR4_bSVIXPsGa4BbCSt60Q"),
|
||||
album_type: single,
|
||||
year: Some(2024),
|
||||
by_va: false,
|
||||
),
|
||||
AlbumItem(
|
||||
id: "MPREb_kLvmX2AzYBL",
|
||||
name: "Bleib in der Schule (Live at Wacken 2019)",
|
||||
cover: [
|
||||
Thumbnail(
|
||||
url: "https://lh3.googleusercontent.com/dV3PCeAdRQgLOuSUdIfA4q8jVgNwSoTceeK085ZOCzEe6YBm5c9gNIvO8wGM_K2NKpip-8-PxJtWEPJo=w226-h226-l90-rj",
|
||||
width: 226,
|
||||
height: 226,
|
||||
),
|
||||
Thumbnail(
|
||||
url: "https://lh3.googleusercontent.com/dV3PCeAdRQgLOuSUdIfA4q8jVgNwSoTceeK085ZOCzEe6YBm5c9gNIvO8wGM_K2NKpip-8-PxJtWEPJo=w544-h544-l90-rj",
|
||||
width: 544,
|
||||
height: 544,
|
||||
),
|
||||
],
|
||||
artists: [
|
||||
ArtistId(
|
||||
id: Some("UCOR4_bSVIXPsGa4BbCSt60Q"),
|
||||
name: "Trailerpark",
|
||||
),
|
||||
],
|
||||
artist_id: Some("UCOR4_bSVIXPsGa4BbCSt60Q"),
|
||||
album_type: single,
|
||||
year: Some(2014),
|
||||
by_va: false,
|
||||
),
|
||||
],
|
||||
playlists: [],
|
||||
similar_artists: [
|
||||
ArtistItem(
|
||||
id: "UCVRREKn7V1Cb8qvf43dwZ6w",
|
||||
name: "257ers",
|
||||
avatar: [
|
||||
Thumbnail(
|
||||
url: "https://lh3.googleusercontent.com/yPjiQ4ZVblOXbft1Yo2jd3uJXKJDuSWOP1MCAG6kTIwYqTWsOKRbZBnPhW4gjzvvVll7yVtjbu3e3Q=w226-h226-l90-rj",
|
||||
width: 226,
|
||||
height: 226,
|
||||
),
|
||||
Thumbnail(
|
||||
url: "https://lh3.googleusercontent.com/yPjiQ4ZVblOXbft1Yo2jd3uJXKJDuSWOP1MCAG6kTIwYqTWsOKRbZBnPhW4gjzvvVll7yVtjbu3e3Q=w544-h544-l90-rj",
|
||||
width: 544,
|
||||
height: 544,
|
||||
),
|
||||
],
|
||||
subscriber_count: Some(67300),
|
||||
),
|
||||
ArtistItem(
|
||||
id: "UCuNyvmBfTzQZmWI2rsVX3QQ",
|
||||
name: "Alligatoah",
|
||||
avatar: [
|
||||
Thumbnail(
|
||||
url: "https://lh3.googleusercontent.com/ffIVPiIldrcfp9UEoAbDid6fnAOajn_kgI4OisFoFhK28rk3HVdpYfe2h27T3d_hHfNR943PPSOhHw=w226-h226-p-l90-rj",
|
||||
width: 226,
|
||||
height: 226,
|
||||
),
|
||||
Thumbnail(
|
||||
url: "https://lh3.googleusercontent.com/ffIVPiIldrcfp9UEoAbDid6fnAOajn_kgI4OisFoFhK28rk3HVdpYfe2h27T3d_hHfNR943PPSOhHw=w544-h544-p-l90-rj",
|
||||
width: 544,
|
||||
height: 544,
|
||||
),
|
||||
],
|
||||
subscriber_count: Some(779000),
|
||||
),
|
||||
ArtistItem(
|
||||
id: "UCO04sIqN7F4ff2-1ycVZSgQ",
|
||||
name: "Sudden",
|
||||
avatar: [
|
||||
Thumbnail(
|
||||
url: "https://lh3.googleusercontent.com/TEdMt2cE-UCbnjm6AJtyasWv9-a3LFpdmh2X6w3iBwIMATHUtYIQ_F0cJ30vL5m6uJkqL3qFvNYLpYrN=w226-h226-l90-rj",
|
||||
width: 226,
|
||||
height: 226,
|
||||
),
|
||||
Thumbnail(
|
||||
url: "https://lh3.googleusercontent.com/TEdMt2cE-UCbnjm6AJtyasWv9-a3LFpdmh2X6w3iBwIMATHUtYIQ_F0cJ30vL5m6uJkqL3qFvNYLpYrN=w544-h544-l90-rj",
|
||||
width: 544,
|
||||
height: 544,
|
||||
),
|
||||
],
|
||||
subscriber_count: Some(3660),
|
||||
),
|
||||
ArtistItem(
|
||||
id: "UC5k_3LEPSGchsGEGpqoF6dg",
|
||||
name: "K.I.Z",
|
||||
avatar: [
|
||||
Thumbnail(
|
||||
url: "https://lh3.googleusercontent.com/PVaIRDAgRRyLMuFp7OTS7h3HEMoY9ejKxt7GLgfgi6aFt3bP-Edb1YU5t1IlGN0Z-qcrb86qspETNoI=w226-h226-p-l90-rj",
|
||||
width: 226,
|
||||
height: 226,
|
||||
),
|
||||
Thumbnail(
|
||||
url: "https://lh3.googleusercontent.com/PVaIRDAgRRyLMuFp7OTS7h3HEMoY9ejKxt7GLgfgi6aFt3bP-Edb1YU5t1IlGN0Z-qcrb86qspETNoI=w544-h544-p-l90-rj",
|
||||
width: 544,
|
||||
height: 544,
|
||||
),
|
||||
],
|
||||
subscriber_count: Some(522000),
|
||||
),
|
||||
ArtistItem(
|
||||
id: "UCG8K_22LRSRwqhoJXBWGmbA",
|
||||
name: "FiNCH",
|
||||
avatar: [
|
||||
Thumbnail(
|
||||
url: "https://lh3.googleusercontent.com/cofqKPsHr5dzuLexkKAYQF3vVMkKTT2FuZgIMXs6XIO3J8diK29qqfKQkqrga8NOCmwVl7x-w4z3mQ=w226-h226-p-l90-rj",
|
||||
width: 226,
|
||||
height: 226,
|
||||
),
|
||||
Thumbnail(
|
||||
url: "https://lh3.googleusercontent.com/cofqKPsHr5dzuLexkKAYQF3vVMkKTT2FuZgIMXs6XIO3J8diK29qqfKQkqrga8NOCmwVl7x-w4z3mQ=w544-h544-p-l90-rj",
|
||||
width: 544,
|
||||
height: 544,
|
||||
),
|
||||
],
|
||||
subscriber_count: Some(533000),
|
||||
),
|
||||
ArtistItem(
|
||||
id: "UC5HSrFHr6lMzwAyGjlClm0A",
|
||||
name: "Timi Hendrix",
|
||||
avatar: [
|
||||
Thumbnail(
|
||||
url: "https://lh3.googleusercontent.com/1yi83YgKDBSQ0rgsA2GuZRa0rBABPR2BH41DsuCfGMRmLdF9oR7vv7T6QGLbhNP8FfX6qVHUQci4YM8=w226-h226-p-l90-rj",
|
||||
width: 226,
|
||||
height: 226,
|
||||
),
|
||||
Thumbnail(
|
||||
url: "https://lh3.googleusercontent.com/1yi83YgKDBSQ0rgsA2GuZRa0rBABPR2BH41DsuCfGMRmLdF9oR7vv7T6QGLbhNP8FfX6qVHUQci4YM8=w544-h544-p-l90-rj",
|
||||
width: 544,
|
||||
height: 544,
|
||||
),
|
||||
],
|
||||
subscriber_count: Some(6410),
|
||||
),
|
||||
ArtistItem(
|
||||
id: "UC9izv9vxcTVKA1IibcGTrNA",
|
||||
name: "Pimpulsiv",
|
||||
avatar: [
|
||||
Thumbnail(
|
||||
url: "https://lh3.googleusercontent.com/QXuirXSQsdO1KUZCz-ZX-kRVSorZxIUC4YrxQD0IeSr1mY-42VwvAjf4TTownRVzm-02-U8kLM3VuETf9w=w226-h226-l90-rj",
|
||||
width: 226,
|
||||
height: 226,
|
||||
),
|
||||
Thumbnail(
|
||||
url: "https://lh3.googleusercontent.com/QXuirXSQsdO1KUZCz-ZX-kRVSorZxIUC4YrxQD0IeSr1mY-42VwvAjf4TTownRVzm-02-U8kLM3VuETf9w=w544-h544-l90-rj",
|
||||
width: 544,
|
||||
height: 544,
|
||||
),
|
||||
],
|
||||
subscriber_count: Some(985),
|
||||
),
|
||||
ArtistItem(
|
||||
id: "UCgosMU69MpoCqhuS1JZj6Cw",
|
||||
name: "Sido",
|
||||
avatar: [
|
||||
Thumbnail(
|
||||
url: "https://lh3.googleusercontent.com/HZpnexwxNS5FkIrpz6hdHZuNhBS-GKjs0C9NU8nDSTmHFlPaviqxV-dDLS_ubSEbpEvu0m2P2WT3kaQ=w226-h226-p-l90-rj",
|
||||
width: 226,
|
||||
height: 226,
|
||||
),
|
||||
Thumbnail(
|
||||
url: "https://lh3.googleusercontent.com/HZpnexwxNS5FkIrpz6hdHZuNhBS-GKjs0C9NU8nDSTmHFlPaviqxV-dDLS_ubSEbpEvu0m2P2WT3kaQ=w544-h544-p-l90-rj",
|
||||
width: 544,
|
||||
height: 544,
|
||||
),
|
||||
],
|
||||
subscriber_count: Some(1550000),
|
||||
),
|
||||
ArtistItem(
|
||||
id: "UCAiLb3B6iCjxv7HhPf1S4ag",
|
||||
name: "Marteria",
|
||||
avatar: [
|
||||
Thumbnail(
|
||||
url: "https://lh3.googleusercontent.com/Ms5gYOttabL03qfFYx7SNhRsx-K_Y7hxMN0WXgc7iquYAfLV5cgYZfTBn3nsi0_sN5BaqAaIr1z5iGc=w226-h226-p-l90-rj",
|
||||
width: 226,
|
||||
height: 226,
|
||||
),
|
||||
Thumbnail(
|
||||
url: "https://lh3.googleusercontent.com/Ms5gYOttabL03qfFYx7SNhRsx-K_Y7hxMN0WXgc7iquYAfLV5cgYZfTBn3nsi0_sN5BaqAaIr1z5iGc=w544-h544-p-l90-rj",
|
||||
width: 544,
|
||||
height: 544,
|
||||
),
|
||||
],
|
||||
subscriber_count: Some(422000),
|
||||
),
|
||||
ArtistItem(
|
||||
id: "UCtoec88rzlhABHeo_4d-H8g",
|
||||
name: "Dame",
|
||||
avatar: [
|
||||
Thumbnail(
|
||||
url: "https://lh3.googleusercontent.com/lkbE9cB4qTxtRmzkjAaLEHrpIgeCOzeBXaL4BpBRq6wp4PlCoSIFej3ita3du8lqniIA67NRYfsVwuFj=w226-h226-p-l90-rj",
|
||||
width: 226,
|
||||
height: 226,
|
||||
),
|
||||
Thumbnail(
|
||||
url: "https://lh3.googleusercontent.com/lkbE9cB4qTxtRmzkjAaLEHrpIgeCOzeBXaL4BpBRq6wp4PlCoSIFej3ita3du8lqniIA67NRYfsVwuFj=w544-h544-p-l90-rj",
|
||||
width: 544,
|
||||
height: 544,
|
||||
),
|
||||
],
|
||||
subscriber_count: Some(37700),
|
||||
),
|
||||
],
|
||||
tracks_playlist_id: Some("OLAK5uy_miHesZCUQY5S9EwqfoNP2tZR9nZ0NBAeU"),
|
||||
videos_playlist_id: Some("OLAK5uy_mqbgE6T9uvusUWrAxJGiImf4_P4dM7IvQ"),
|
||||
radio_id: Some("RDEM7AbogW0cCnElSU0WYm1GqA"),
|
||||
)
|
File diff suppressed because it is too large
Load diff
|
@ -59,7 +59,6 @@ MusicArtist(
|
|||
)),
|
||||
track_count: None,
|
||||
from_ytm: false,
|
||||
is_podcast: false,
|
||||
),
|
||||
MusicPlaylistItem(
|
||||
id: "PLwkM1QxaP343YqeP6g5VPGsgJdO1_SV4I",
|
||||
|
@ -82,7 +81,6 @@ MusicArtist(
|
|||
)),
|
||||
track_count: None,
|
||||
from_ytm: false,
|
||||
is_podcast: false,
|
||||
),
|
||||
MusicPlaylistItem(
|
||||
id: "PLwkM1QxaP340xbkARIPpiD1aHuzJVuZUg",
|
||||
|
@ -105,7 +103,6 @@ MusicArtist(
|
|||
)),
|
||||
track_count: None,
|
||||
from_ytm: false,
|
||||
is_podcast: false,
|
||||
),
|
||||
MusicPlaylistItem(
|
||||
id: "PLwkM1QxaP342hjju64dtqG5wKqx2hNgjr",
|
||||
|
@ -128,7 +125,6 @@ MusicArtist(
|
|||
)),
|
||||
track_count: None,
|
||||
from_ytm: false,
|
||||
is_podcast: false,
|
||||
),
|
||||
MusicPlaylistItem(
|
||||
id: "PLwkM1QxaP342v1hhoB3XLiruSQOzmdmBt",
|
||||
|
@ -151,7 +147,6 @@ MusicArtist(
|
|||
)),
|
||||
track_count: None,
|
||||
from_ytm: false,
|
||||
is_podcast: false,
|
||||
),
|
||||
MusicPlaylistItem(
|
||||
id: "PLwkM1QxaP342EBMza0AG10nB3oDD65RPY",
|
||||
|
@ -174,7 +169,6 @@ MusicArtist(
|
|||
)),
|
||||
track_count: None,
|
||||
from_ytm: false,
|
||||
is_podcast: false,
|
||||
),
|
||||
MusicPlaylistItem(
|
||||
id: "PLwkM1QxaP342nVAeBVL6_Q8gbbAD8l4wb",
|
||||
|
@ -197,7 +191,6 @@ MusicArtist(
|
|||
)),
|
||||
track_count: None,
|
||||
from_ytm: false,
|
||||
is_podcast: false,
|
||||
),
|
||||
MusicPlaylistItem(
|
||||
id: "PLwkM1QxaP3438x6ta8VJZlJSlDn43FueA",
|
||||
|
@ -220,7 +213,6 @@ MusicArtist(
|
|||
)),
|
||||
track_count: None,
|
||||
from_ytm: false,
|
||||
is_podcast: false,
|
||||
),
|
||||
],
|
||||
similar_artists: [],
|
||||
|
|
|
@ -64,7 +64,7 @@ MusicArtist(
|
|||
name: "÷ (Deluxe)",
|
||||
)),
|
||||
view_count: Some(5700000000),
|
||||
track_type: track,
|
||||
is_video: false,
|
||||
track_nr: None,
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -96,7 +96,7 @@ MusicArtist(
|
|||
name: "Shape of You",
|
||||
)),
|
||||
view_count: Some(8700000000),
|
||||
track_type: track,
|
||||
is_video: false,
|
||||
track_nr: None,
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -128,7 +128,7 @@ MusicArtist(
|
|||
name: "x (Deluxe Edition)",
|
||||
)),
|
||||
view_count: Some(3300000000),
|
||||
track_type: track,
|
||||
is_video: false,
|
||||
track_nr: None,
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -160,7 +160,7 @@ MusicArtist(
|
|||
name: "x (Deluxe Edition)",
|
||||
)),
|
||||
view_count: Some(4500000000),
|
||||
track_type: track,
|
||||
is_video: false,
|
||||
track_nr: None,
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -192,7 +192,7 @@ MusicArtist(
|
|||
name: "Bad Habits",
|
||||
)),
|
||||
view_count: Some(1100000000),
|
||||
track_type: track,
|
||||
is_video: false,
|
||||
track_nr: None,
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -221,7 +221,7 @@ MusicArtist(
|
|||
artist_id: Some("UClmXPfaYhXOYsNn_QUyheWQ"),
|
||||
album: None,
|
||||
view_count: Some(378000),
|
||||
track_type: video,
|
||||
is_video: true,
|
||||
track_nr: None,
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -250,7 +250,7 @@ MusicArtist(
|
|||
artist_id: Some("UClmXPfaYhXOYsNn_QUyheWQ"),
|
||||
album: None,
|
||||
view_count: Some(250000),
|
||||
track_type: video,
|
||||
is_video: true,
|
||||
track_nr: None,
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -279,7 +279,7 @@ MusicArtist(
|
|||
artist_id: Some("UClmXPfaYhXOYsNn_QUyheWQ"),
|
||||
album: None,
|
||||
view_count: Some(372000),
|
||||
track_type: video,
|
||||
is_video: true,
|
||||
track_nr: None,
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -308,7 +308,7 @@ MusicArtist(
|
|||
artist_id: Some("UClmXPfaYhXOYsNn_QUyheWQ"),
|
||||
album: None,
|
||||
view_count: Some(1000000),
|
||||
track_type: video,
|
||||
is_video: true,
|
||||
track_nr: None,
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -337,7 +337,7 @@ MusicArtist(
|
|||
artist_id: Some("UClmXPfaYhXOYsNn_QUyheWQ"),
|
||||
album: None,
|
||||
view_count: Some(3800000000),
|
||||
track_type: video,
|
||||
is_video: true,
|
||||
track_nr: None,
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -366,7 +366,7 @@ MusicArtist(
|
|||
artist_id: Some("UClmXPfaYhXOYsNn_QUyheWQ"),
|
||||
album: None,
|
||||
view_count: Some(6300000000),
|
||||
track_type: video,
|
||||
is_video: true,
|
||||
track_nr: None,
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -395,7 +395,7 @@ MusicArtist(
|
|||
artist_id: Some("UClmXPfaYhXOYsNn_QUyheWQ"),
|
||||
album: None,
|
||||
view_count: Some(1400000000),
|
||||
track_type: video,
|
||||
is_video: true,
|
||||
track_nr: None,
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -424,7 +424,7 @@ MusicArtist(
|
|||
artist_id: Some("UClmXPfaYhXOYsNn_QUyheWQ"),
|
||||
album: None,
|
||||
view_count: Some(3800000000),
|
||||
track_type: video,
|
||||
is_video: true,
|
||||
track_nr: None,
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -453,7 +453,7 @@ MusicArtist(
|
|||
artist_id: Some("UClmXPfaYhXOYsNn_QUyheWQ"),
|
||||
album: None,
|
||||
view_count: Some(641000000),
|
||||
track_type: video,
|
||||
is_video: true,
|
||||
track_nr: None,
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -482,7 +482,7 @@ MusicArtist(
|
|||
artist_id: Some("UClmXPfaYhXOYsNn_QUyheWQ"),
|
||||
album: None,
|
||||
view_count: Some(364000000),
|
||||
track_type: video,
|
||||
is_video: true,
|
||||
track_nr: None,
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -510,7 +510,7 @@ MusicArtist(
|
|||
),
|
||||
],
|
||||
artist_id: Some("UClmXPfaYhXOYsNn_QUyheWQ"),
|
||||
album_type: album,
|
||||
album_type: Album,
|
||||
year: Some(2024),
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -536,7 +536,7 @@ MusicArtist(
|
|||
),
|
||||
],
|
||||
artist_id: Some("UClmXPfaYhXOYsNn_QUyheWQ"),
|
||||
album_type: album,
|
||||
album_type: Album,
|
||||
year: Some(2023),
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -562,7 +562,7 @@ MusicArtist(
|
|||
),
|
||||
],
|
||||
artist_id: Some("UClmXPfaYhXOYsNn_QUyheWQ"),
|
||||
album_type: album,
|
||||
album_type: Album,
|
||||
year: Some(2023),
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -588,7 +588,7 @@ MusicArtist(
|
|||
),
|
||||
],
|
||||
artist_id: Some("UClmXPfaYhXOYsNn_QUyheWQ"),
|
||||
album_type: album,
|
||||
album_type: Album,
|
||||
year: Some(2023),
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -614,7 +614,7 @@ MusicArtist(
|
|||
),
|
||||
],
|
||||
artist_id: Some("UClmXPfaYhXOYsNn_QUyheWQ"),
|
||||
album_type: album,
|
||||
album_type: Album,
|
||||
year: Some(2021),
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -640,7 +640,7 @@ MusicArtist(
|
|||
),
|
||||
],
|
||||
artist_id: Some("UClmXPfaYhXOYsNn_QUyheWQ"),
|
||||
album_type: album,
|
||||
album_type: Album,
|
||||
year: Some(2019),
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -666,7 +666,7 @@ MusicArtist(
|
|||
),
|
||||
],
|
||||
artist_id: Some("UClmXPfaYhXOYsNn_QUyheWQ"),
|
||||
album_type: album,
|
||||
album_type: Album,
|
||||
year: Some(2017),
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -692,7 +692,7 @@ MusicArtist(
|
|||
),
|
||||
],
|
||||
artist_id: Some("UClmXPfaYhXOYsNn_QUyheWQ"),
|
||||
album_type: ep,
|
||||
album_type: Ep,
|
||||
year: Some(2014),
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -718,7 +718,7 @@ MusicArtist(
|
|||
),
|
||||
],
|
||||
artist_id: Some("UClmXPfaYhXOYsNn_QUyheWQ"),
|
||||
album_type: album,
|
||||
album_type: Album,
|
||||
year: Some(2014),
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -744,7 +744,7 @@ MusicArtist(
|
|||
),
|
||||
],
|
||||
artist_id: Some("UClmXPfaYhXOYsNn_QUyheWQ"),
|
||||
album_type: album,
|
||||
album_type: Album,
|
||||
year: Some(2011),
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -770,7 +770,7 @@ MusicArtist(
|
|||
),
|
||||
],
|
||||
artist_id: Some("UClmXPfaYhXOYsNn_QUyheWQ"),
|
||||
album_type: single,
|
||||
album_type: Single,
|
||||
year: Some(2023),
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -796,7 +796,7 @@ MusicArtist(
|
|||
),
|
||||
],
|
||||
artist_id: Some("UClmXPfaYhXOYsNn_QUyheWQ"),
|
||||
album_type: single,
|
||||
album_type: Single,
|
||||
year: Some(2023),
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -822,7 +822,7 @@ MusicArtist(
|
|||
),
|
||||
],
|
||||
artist_id: Some("UClmXPfaYhXOYsNn_QUyheWQ"),
|
||||
album_type: single,
|
||||
album_type: Single,
|
||||
year: Some(2023),
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -848,7 +848,7 @@ MusicArtist(
|
|||
),
|
||||
],
|
||||
artist_id: Some("UClmXPfaYhXOYsNn_QUyheWQ"),
|
||||
album_type: single,
|
||||
album_type: Single,
|
||||
year: Some(2023),
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -874,7 +874,7 @@ MusicArtist(
|
|||
),
|
||||
],
|
||||
artist_id: Some("UClmXPfaYhXOYsNn_QUyheWQ"),
|
||||
album_type: single,
|
||||
album_type: Single,
|
||||
year: Some(2023),
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -900,7 +900,7 @@ MusicArtist(
|
|||
),
|
||||
],
|
||||
artist_id: Some("UClmXPfaYhXOYsNn_QUyheWQ"),
|
||||
album_type: single,
|
||||
album_type: Single,
|
||||
year: Some(2023),
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -926,7 +926,7 @@ MusicArtist(
|
|||
),
|
||||
],
|
||||
artist_id: Some("UClmXPfaYhXOYsNn_QUyheWQ"),
|
||||
album_type: single,
|
||||
album_type: Single,
|
||||
year: Some(2022),
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -952,7 +952,7 @@ MusicArtist(
|
|||
),
|
||||
],
|
||||
artist_id: Some("UClmXPfaYhXOYsNn_QUyheWQ"),
|
||||
album_type: single,
|
||||
album_type: Single,
|
||||
year: Some(2022),
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -978,7 +978,7 @@ MusicArtist(
|
|||
),
|
||||
],
|
||||
artist_id: Some("UClmXPfaYhXOYsNn_QUyheWQ"),
|
||||
album_type: single,
|
||||
album_type: Single,
|
||||
year: Some(2022),
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -1004,7 +1004,7 @@ MusicArtist(
|
|||
),
|
||||
],
|
||||
artist_id: Some("UClmXPfaYhXOYsNn_QUyheWQ"),
|
||||
album_type: single,
|
||||
album_type: Single,
|
||||
year: Some(2022),
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -1028,7 +1028,6 @@ MusicArtist(
|
|||
channel: None,
|
||||
track_count: None,
|
||||
from_ytm: true,
|
||||
is_podcast: false,
|
||||
),
|
||||
MusicPlaylistItem(
|
||||
id: "RDCLAK5uy_mkPdnadBmgXk28mbGxm_5uGeKvHrec208",
|
||||
|
@ -1048,7 +1047,6 @@ MusicArtist(
|
|||
channel: None,
|
||||
track_count: None,
|
||||
from_ytm: true,
|
||||
is_podcast: false,
|
||||
),
|
||||
MusicPlaylistItem(
|
||||
id: "RDCLAK5uy_m0wlRoNn5iCTTgBedfoOQ19Jq9P3XTLIA",
|
||||
|
@ -1068,7 +1066,6 @@ MusicArtist(
|
|||
channel: None,
|
||||
track_count: None,
|
||||
from_ytm: true,
|
||||
is_podcast: false,
|
||||
),
|
||||
MusicPlaylistItem(
|
||||
id: "RDCLAK5uy_l1oO11DBO4FD8U7bOrqUKK5Y_PkISUMQM",
|
||||
|
@ -1088,7 +1085,6 @@ MusicArtist(
|
|||
channel: None,
|
||||
track_count: None,
|
||||
from_ytm: true,
|
||||
is_podcast: false,
|
||||
),
|
||||
MusicPlaylistItem(
|
||||
id: "RDCLAK5uy_khs3a0YMI9WYs2k1Oqb2ukWX3dA3-lnwI",
|
||||
|
@ -1108,7 +1104,6 @@ MusicArtist(
|
|||
channel: None,
|
||||
track_count: None,
|
||||
from_ytm: true,
|
||||
is_podcast: false,
|
||||
),
|
||||
MusicPlaylistItem(
|
||||
id: "RDCLAK5uy_mfdqvCAl8wodlx2P2_Ai2gNkiRDAufkkI",
|
||||
|
@ -1128,7 +1123,6 @@ MusicArtist(
|
|||
channel: None,
|
||||
track_count: None,
|
||||
from_ytm: true,
|
||||
is_podcast: false,
|
||||
),
|
||||
MusicPlaylistItem(
|
||||
id: "RDCLAK5uy_nxuz8sV0R7aWiLsbDv5W9_Bvp0X9PxFjY",
|
||||
|
@ -1148,7 +1142,6 @@ MusicArtist(
|
|||
channel: None,
|
||||
track_count: None,
|
||||
from_ytm: true,
|
||||
is_podcast: false,
|
||||
),
|
||||
MusicPlaylistItem(
|
||||
id: "RDCLAK5uy_kPqJ_FiGk-lbXtgM4IF42uokskSJZiVTI",
|
||||
|
@ -1168,7 +1161,6 @@ MusicArtist(
|
|||
channel: None,
|
||||
track_count: None,
|
||||
from_ytm: true,
|
||||
is_podcast: false,
|
||||
),
|
||||
MusicPlaylistItem(
|
||||
id: "RDCLAK5uy_nfs_t4FUu00E5ED6lveEBBX1VMYe1mFjk",
|
||||
|
@ -1188,7 +1180,6 @@ MusicArtist(
|
|||
channel: None,
|
||||
track_count: None,
|
||||
from_ytm: true,
|
||||
is_podcast: false,
|
||||
),
|
||||
MusicPlaylistItem(
|
||||
id: "RDCLAK5uy_kEeMXVnyMll_xhEBH1Aza4lEYO58yeQ0M",
|
||||
|
@ -1208,7 +1199,6 @@ MusicArtist(
|
|||
channel: None,
|
||||
track_count: None,
|
||||
from_ytm: true,
|
||||
is_podcast: false,
|
||||
),
|
||||
],
|
||||
similar_artists: [
|
||||
|
|
|
@ -64,7 +64,7 @@ MusicArtist(
|
|||
name: "Evolve",
|
||||
)),
|
||||
view_count: None,
|
||||
track_type: track,
|
||||
is_video: false,
|
||||
track_nr: None,
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -96,7 +96,7 @@ MusicArtist(
|
|||
name: "Mercury : Acts 1 & 2",
|
||||
)),
|
||||
view_count: None,
|
||||
track_type: track,
|
||||
is_video: false,
|
||||
track_nr: None,
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -140,7 +140,7 @@ MusicArtist(
|
|||
name: "Mercury : Acts 1 & 2",
|
||||
)),
|
||||
view_count: None,
|
||||
track_type: track,
|
||||
is_video: false,
|
||||
track_nr: None,
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -172,7 +172,7 @@ MusicArtist(
|
|||
name: "Evolve",
|
||||
)),
|
||||
view_count: None,
|
||||
track_type: track,
|
||||
is_video: false,
|
||||
track_nr: None,
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -204,7 +204,7 @@ MusicArtist(
|
|||
name: "Night Visions",
|
||||
)),
|
||||
view_count: None,
|
||||
track_type: track,
|
||||
is_video: false,
|
||||
track_nr: None,
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -233,7 +233,7 @@ MusicArtist(
|
|||
artist_id: Some("UC0aXrjVxG5pZr99v77wZdPQ"),
|
||||
album: None,
|
||||
view_count: Some(2100000),
|
||||
track_type: video,
|
||||
is_video: true,
|
||||
track_nr: None,
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -262,7 +262,7 @@ MusicArtist(
|
|||
artist_id: Some("UC0aXrjVxG5pZr99v77wZdPQ"),
|
||||
album: None,
|
||||
view_count: Some(2400000000),
|
||||
track_type: video,
|
||||
is_video: true,
|
||||
track_nr: None,
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -291,7 +291,7 @@ MusicArtist(
|
|||
artist_id: Some("UC0aXrjVxG5pZr99v77wZdPQ"),
|
||||
album: None,
|
||||
view_count: Some(207000000),
|
||||
track_type: video,
|
||||
is_video: true,
|
||||
track_nr: None,
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -320,7 +320,7 @@ MusicArtist(
|
|||
artist_id: Some("UC0aXrjVxG5pZr99v77wZdPQ"),
|
||||
album: None,
|
||||
view_count: Some(324000000),
|
||||
track_type: video,
|
||||
is_video: true,
|
||||
track_nr: None,
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -349,7 +349,7 @@ MusicArtist(
|
|||
artist_id: Some("UC0aXrjVxG5pZr99v77wZdPQ"),
|
||||
album: None,
|
||||
view_count: Some(1900000000),
|
||||
track_type: video,
|
||||
is_video: true,
|
||||
track_nr: None,
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -378,7 +378,7 @@ MusicArtist(
|
|||
artist_id: Some("UC0aXrjVxG5pZr99v77wZdPQ"),
|
||||
album: None,
|
||||
view_count: Some(1000000000),
|
||||
track_type: video,
|
||||
is_video: true,
|
||||
track_nr: None,
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -407,7 +407,7 @@ MusicArtist(
|
|||
artist_id: Some("UC0aXrjVxG5pZr99v77wZdPQ"),
|
||||
album: None,
|
||||
view_count: Some(1400000000),
|
||||
track_type: video,
|
||||
is_video: true,
|
||||
track_nr: None,
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -431,7 +431,7 @@ MusicArtist(
|
|||
artist_id: Some("UC0aXrjVxG5pZr99v77wZdPQ"),
|
||||
album: None,
|
||||
view_count: Some(440000000),
|
||||
track_type: video,
|
||||
is_video: true,
|
||||
track_nr: None,
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -460,7 +460,7 @@ MusicArtist(
|
|||
artist_id: Some("UC0aXrjVxG5pZr99v77wZdPQ"),
|
||||
album: None,
|
||||
view_count: Some(557000000),
|
||||
track_type: video,
|
||||
is_video: true,
|
||||
track_nr: None,
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -489,7 +489,7 @@ MusicArtist(
|
|||
artist_id: Some("UC0aXrjVxG5pZr99v77wZdPQ"),
|
||||
album: None,
|
||||
view_count: Some(877000000),
|
||||
track_type: video,
|
||||
is_video: true,
|
||||
track_nr: None,
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -517,7 +517,7 @@ MusicArtist(
|
|||
),
|
||||
],
|
||||
artist_id: Some("UC0aXrjVxG5pZr99v77wZdPQ"),
|
||||
album_type: album,
|
||||
album_type: Album,
|
||||
year: Some(2022),
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -543,7 +543,7 @@ MusicArtist(
|
|||
),
|
||||
],
|
||||
artist_id: Some("UC0aXrjVxG5pZr99v77wZdPQ"),
|
||||
album_type: single,
|
||||
album_type: Single,
|
||||
year: Some(2022),
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -569,7 +569,7 @@ MusicArtist(
|
|||
),
|
||||
],
|
||||
artist_id: Some("UC0aXrjVxG5pZr99v77wZdPQ"),
|
||||
album_type: album,
|
||||
album_type: Album,
|
||||
year: Some(2022),
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -595,7 +595,7 @@ MusicArtist(
|
|||
),
|
||||
],
|
||||
artist_id: Some("UC0aXrjVxG5pZr99v77wZdPQ"),
|
||||
album_type: single,
|
||||
album_type: Single,
|
||||
year: Some(2022),
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -621,7 +621,7 @@ MusicArtist(
|
|||
),
|
||||
],
|
||||
artist_id: Some("UC0aXrjVxG5pZr99v77wZdPQ"),
|
||||
album_type: single,
|
||||
album_type: Single,
|
||||
year: Some(2022),
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -647,7 +647,7 @@ MusicArtist(
|
|||
),
|
||||
],
|
||||
artist_id: Some("UC0aXrjVxG5pZr99v77wZdPQ"),
|
||||
album_type: single,
|
||||
album_type: Single,
|
||||
year: Some(2021),
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -673,7 +673,7 @@ MusicArtist(
|
|||
),
|
||||
],
|
||||
artist_id: Some("UC0aXrjVxG5pZr99v77wZdPQ"),
|
||||
album_type: single,
|
||||
album_type: Single,
|
||||
year: Some(2021),
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -699,7 +699,7 @@ MusicArtist(
|
|||
),
|
||||
],
|
||||
artist_id: Some("UC0aXrjVxG5pZr99v77wZdPQ"),
|
||||
album_type: single,
|
||||
album_type: Single,
|
||||
year: Some(2021),
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -725,7 +725,7 @@ MusicArtist(
|
|||
),
|
||||
],
|
||||
artist_id: Some("UC0aXrjVxG5pZr99v77wZdPQ"),
|
||||
album_type: single,
|
||||
album_type: Single,
|
||||
year: Some(2021),
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -751,7 +751,7 @@ MusicArtist(
|
|||
),
|
||||
],
|
||||
artist_id: Some("UC0aXrjVxG5pZr99v77wZdPQ"),
|
||||
album_type: single,
|
||||
album_type: Single,
|
||||
year: Some(2021),
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -777,7 +777,7 @@ MusicArtist(
|
|||
),
|
||||
],
|
||||
artist_id: Some("UC0aXrjVxG5pZr99v77wZdPQ"),
|
||||
album_type: single,
|
||||
album_type: Single,
|
||||
year: Some(2019),
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -803,7 +803,7 @@ MusicArtist(
|
|||
),
|
||||
],
|
||||
artist_id: Some("UC0aXrjVxG5pZr99v77wZdPQ"),
|
||||
album_type: album,
|
||||
album_type: Album,
|
||||
year: Some(2018),
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -829,7 +829,7 @@ MusicArtist(
|
|||
),
|
||||
],
|
||||
artist_id: Some("UC0aXrjVxG5pZr99v77wZdPQ"),
|
||||
album_type: single,
|
||||
album_type: Single,
|
||||
year: Some(2018),
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -855,7 +855,7 @@ MusicArtist(
|
|||
),
|
||||
],
|
||||
artist_id: Some("UC0aXrjVxG5pZr99v77wZdPQ"),
|
||||
album_type: single,
|
||||
album_type: Single,
|
||||
year: Some(2018),
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -881,7 +881,7 @@ MusicArtist(
|
|||
),
|
||||
],
|
||||
artist_id: Some("UC0aXrjVxG5pZr99v77wZdPQ"),
|
||||
album_type: single,
|
||||
album_type: Single,
|
||||
year: Some(2018),
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -907,7 +907,7 @@ MusicArtist(
|
|||
),
|
||||
],
|
||||
artist_id: Some("UC0aXrjVxG5pZr99v77wZdPQ"),
|
||||
album_type: single,
|
||||
album_type: Single,
|
||||
year: Some(2017),
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -933,7 +933,7 @@ MusicArtist(
|
|||
),
|
||||
],
|
||||
artist_id: Some("UC0aXrjVxG5pZr99v77wZdPQ"),
|
||||
album_type: album,
|
||||
album_type: Album,
|
||||
year: Some(2017),
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -959,7 +959,7 @@ MusicArtist(
|
|||
),
|
||||
],
|
||||
artist_id: Some("UC0aXrjVxG5pZr99v77wZdPQ"),
|
||||
album_type: single,
|
||||
album_type: Single,
|
||||
year: Some(2017),
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -985,7 +985,7 @@ MusicArtist(
|
|||
),
|
||||
],
|
||||
artist_id: Some("UC0aXrjVxG5pZr99v77wZdPQ"),
|
||||
album_type: single,
|
||||
album_type: Single,
|
||||
year: Some(2016),
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -1011,7 +1011,7 @@ MusicArtist(
|
|||
),
|
||||
],
|
||||
artist_id: Some("UC0aXrjVxG5pZr99v77wZdPQ"),
|
||||
album_type: single,
|
||||
album_type: Single,
|
||||
year: Some(2016),
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -1037,7 +1037,7 @@ MusicArtist(
|
|||
),
|
||||
],
|
||||
artist_id: Some("UC0aXrjVxG5pZr99v77wZdPQ"),
|
||||
album_type: single,
|
||||
album_type: Single,
|
||||
year: Some(2015),
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -1063,7 +1063,7 @@ MusicArtist(
|
|||
),
|
||||
],
|
||||
artist_id: Some("UC0aXrjVxG5pZr99v77wZdPQ"),
|
||||
album_type: single,
|
||||
album_type: Single,
|
||||
year: Some(2015),
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -1089,7 +1089,7 @@ MusicArtist(
|
|||
),
|
||||
],
|
||||
artist_id: Some("UC0aXrjVxG5pZr99v77wZdPQ"),
|
||||
album_type: album,
|
||||
album_type: Album,
|
||||
year: Some(2014),
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -1115,7 +1115,7 @@ MusicArtist(
|
|||
),
|
||||
],
|
||||
artist_id: Some("UC0aXrjVxG5pZr99v77wZdPQ"),
|
||||
album_type: album,
|
||||
album_type: Album,
|
||||
year: Some(2011),
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -1141,7 +1141,7 @@ MusicArtist(
|
|||
),
|
||||
],
|
||||
artist_id: Some("UC0aXrjVxG5pZr99v77wZdPQ"),
|
||||
album_type: ep,
|
||||
album_type: Ep,
|
||||
year: Some(2010),
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -1167,7 +1167,7 @@ MusicArtist(
|
|||
),
|
||||
],
|
||||
artist_id: Some("UC0aXrjVxG5pZr99v77wZdPQ"),
|
||||
album_type: ep,
|
||||
album_type: Ep,
|
||||
year: Some(2009),
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -1193,7 +1193,7 @@ MusicArtist(
|
|||
),
|
||||
],
|
||||
artist_id: Some("UC0aXrjVxG5pZr99v77wZdPQ"),
|
||||
album_type: ep,
|
||||
album_type: Ep,
|
||||
year: Some(2017),
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -1219,7 +1219,7 @@ MusicArtist(
|
|||
),
|
||||
],
|
||||
artist_id: Some("UC0aXrjVxG5pZr99v77wZdPQ"),
|
||||
album_type: single,
|
||||
album_type: Single,
|
||||
year: Some(2022),
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -1245,7 +1245,7 @@ MusicArtist(
|
|||
),
|
||||
],
|
||||
artist_id: Some("UC0aXrjVxG5pZr99v77wZdPQ"),
|
||||
album_type: single,
|
||||
album_type: Single,
|
||||
year: Some(2021),
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -1271,7 +1271,7 @@ MusicArtist(
|
|||
),
|
||||
],
|
||||
artist_id: Some("UC0aXrjVxG5pZr99v77wZdPQ"),
|
||||
album_type: single,
|
||||
album_type: Single,
|
||||
year: Some(2017),
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -1297,7 +1297,7 @@ MusicArtist(
|
|||
),
|
||||
],
|
||||
artist_id: Some("UC0aXrjVxG5pZr99v77wZdPQ"),
|
||||
album_type: single,
|
||||
album_type: Single,
|
||||
year: Some(2017),
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -1323,7 +1323,7 @@ MusicArtist(
|
|||
),
|
||||
],
|
||||
artist_id: Some("UC0aXrjVxG5pZr99v77wZdPQ"),
|
||||
album_type: single,
|
||||
album_type: Single,
|
||||
year: Some(2017),
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -1349,7 +1349,7 @@ MusicArtist(
|
|||
),
|
||||
],
|
||||
artist_id: Some("UC0aXrjVxG5pZr99v77wZdPQ"),
|
||||
album_type: single,
|
||||
album_type: Single,
|
||||
year: Some(2015),
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -1375,7 +1375,7 @@ MusicArtist(
|
|||
),
|
||||
],
|
||||
artist_id: Some("UC0aXrjVxG5pZr99v77wZdPQ"),
|
||||
album_type: single,
|
||||
album_type: Single,
|
||||
year: Some(2015),
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -1401,7 +1401,7 @@ MusicArtist(
|
|||
),
|
||||
],
|
||||
artist_id: Some("UC0aXrjVxG5pZr99v77wZdPQ"),
|
||||
album_type: single,
|
||||
album_type: Single,
|
||||
year: Some(2015),
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -1427,7 +1427,7 @@ MusicArtist(
|
|||
),
|
||||
],
|
||||
artist_id: Some("UC0aXrjVxG5pZr99v77wZdPQ"),
|
||||
album_type: single,
|
||||
album_type: Single,
|
||||
year: Some(2014),
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -1453,7 +1453,7 @@ MusicArtist(
|
|||
),
|
||||
],
|
||||
artist_id: Some("UC0aXrjVxG5pZr99v77wZdPQ"),
|
||||
album_type: single,
|
||||
album_type: Single,
|
||||
year: Some(2014),
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -1479,7 +1479,7 @@ MusicArtist(
|
|||
),
|
||||
],
|
||||
artist_id: Some("UC0aXrjVxG5pZr99v77wZdPQ"),
|
||||
album_type: single,
|
||||
album_type: Single,
|
||||
year: Some(2014),
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -1505,7 +1505,7 @@ MusicArtist(
|
|||
),
|
||||
],
|
||||
artist_id: Some("UC0aXrjVxG5pZr99v77wZdPQ"),
|
||||
album_type: single,
|
||||
album_type: Single,
|
||||
year: Some(2013),
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -1529,7 +1529,6 @@ MusicArtist(
|
|||
channel: None,
|
||||
track_count: None,
|
||||
from_ytm: true,
|
||||
is_podcast: false,
|
||||
),
|
||||
MusicPlaylistItem(
|
||||
id: "RDCLAK5uy_mIpIa-YIJFJe0EAcNbcMPgg-3qCdK9qAk",
|
||||
|
@ -1549,7 +1548,6 @@ MusicArtist(
|
|||
channel: None,
|
||||
track_count: None,
|
||||
from_ytm: true,
|
||||
is_podcast: false,
|
||||
),
|
||||
MusicPlaylistItem(
|
||||
id: "RDCLAK5uy_nbzJVrwitbeDjlcHvjM7fgF7khtUOoHgU",
|
||||
|
@ -1569,7 +1567,6 @@ MusicArtist(
|
|||
channel: None,
|
||||
track_count: None,
|
||||
from_ytm: true,
|
||||
is_podcast: false,
|
||||
),
|
||||
MusicPlaylistItem(
|
||||
id: "RDCLAK5uy_nCs5nAmZrJ41ILrSyf36UvOwTBNyx0oEI",
|
||||
|
@ -1589,7 +1586,6 @@ MusicArtist(
|
|||
channel: None,
|
||||
track_count: None,
|
||||
from_ytm: true,
|
||||
is_podcast: false,
|
||||
),
|
||||
MusicPlaylistItem(
|
||||
id: "RDCLAK5uy_nGXEmbtrmoUF9NG7m0WkxpF_qLKYR3YOU",
|
||||
|
@ -1609,7 +1605,6 @@ MusicArtist(
|
|||
channel: None,
|
||||
track_count: None,
|
||||
from_ytm: true,
|
||||
is_podcast: false,
|
||||
),
|
||||
MusicPlaylistItem(
|
||||
id: "RDCLAK5uy_mgHrXs_5F6wPwPFA47S8yrzCfjCi4AXDE",
|
||||
|
@ -1629,7 +1624,6 @@ MusicArtist(
|
|||
channel: None,
|
||||
track_count: None,
|
||||
from_ytm: true,
|
||||
is_podcast: false,
|
||||
),
|
||||
MusicPlaylistItem(
|
||||
id: "RDCLAK5uy_l7u7RCjtiI_I3m5EgnI-V9yWAgx0RNy1E",
|
||||
|
@ -1649,7 +1643,6 @@ MusicArtist(
|
|||
channel: None,
|
||||
track_count: None,
|
||||
from_ytm: true,
|
||||
is_podcast: false,
|
||||
),
|
||||
MusicPlaylistItem(
|
||||
id: "RDCLAK5uy_k7h5535MeHE8xmgHsrZx7HOKH4lb5vAfY",
|
||||
|
@ -1669,7 +1662,6 @@ MusicArtist(
|
|||
channel: None,
|
||||
track_count: None,
|
||||
from_ytm: true,
|
||||
is_podcast: false,
|
||||
),
|
||||
MusicPlaylistItem(
|
||||
id: "RDCLAK5uy_mlCByo5eM1tLBhUdMyn2GphTXICCM_W1w",
|
||||
|
@ -1689,7 +1681,6 @@ MusicArtist(
|
|||
channel: None,
|
||||
track_count: None,
|
||||
from_ytm: true,
|
||||
is_podcast: false,
|
||||
),
|
||||
MusicPlaylistItem(
|
||||
id: "RDCLAK5uy_ke0QH8jvXz6ynXEhn_mbCBy9m7fbnJ9NY",
|
||||
|
@ -1709,7 +1700,6 @@ MusicArtist(
|
|||
channel: None,
|
||||
track_count: None,
|
||||
from_ytm: true,
|
||||
is_podcast: false,
|
||||
),
|
||||
],
|
||||
similar_artists: [
|
||||
|
|
|
@ -64,7 +64,7 @@ MusicArtist(
|
|||
name: "고블린 Goblin",
|
||||
)),
|
||||
view_count: None,
|
||||
track_type: track,
|
||||
is_video: false,
|
||||
track_nr: None,
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -96,7 +96,7 @@ MusicArtist(
|
|||
name: "고블린 Goblin",
|
||||
)),
|
||||
view_count: None,
|
||||
track_type: track,
|
||||
is_video: false,
|
||||
track_nr: None,
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -128,7 +128,7 @@ MusicArtist(
|
|||
name: "고블린 Goblin",
|
||||
)),
|
||||
view_count: None,
|
||||
track_type: track,
|
||||
is_video: false,
|
||||
track_nr: None,
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -157,7 +157,7 @@ MusicArtist(
|
|||
artist_id: Some("UCfwCE5VhPMGxNPFxtVv7lRw"),
|
||||
album: None,
|
||||
view_count: Some(20000000),
|
||||
track_type: video,
|
||||
is_video: true,
|
||||
track_nr: None,
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -186,7 +186,7 @@ MusicArtist(
|
|||
artist_id: Some("UClGBYGUZmpzUaHgeb9gOBww"),
|
||||
album: None,
|
||||
view_count: Some(211000),
|
||||
track_type: video,
|
||||
is_video: true,
|
||||
track_nr: None,
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -215,7 +215,7 @@ MusicArtist(
|
|||
artist_id: Some("UCfaO3pZL5XOr8BvNZkrKeVA"),
|
||||
album: None,
|
||||
view_count: Some(10000),
|
||||
track_type: video,
|
||||
is_video: true,
|
||||
track_nr: None,
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -244,7 +244,7 @@ MusicArtist(
|
|||
artist_id: Some("UCgVWicpO5Jn3VfxqgIU6cpA"),
|
||||
album: None,
|
||||
view_count: Some(15000),
|
||||
track_type: video,
|
||||
is_video: true,
|
||||
track_nr: None,
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -273,7 +273,7 @@ MusicArtist(
|
|||
artist_id: Some("UCe52oeb7Xv_KaJsEzcKXJJg"),
|
||||
album: None,
|
||||
view_count: Some(1200),
|
||||
track_type: video,
|
||||
is_video: true,
|
||||
track_nr: None,
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -302,7 +302,7 @@ MusicArtist(
|
|||
artist_id: Some("UCFFvwAcyQhpeQfuAgBN1XZw"),
|
||||
album: None,
|
||||
view_count: Some(12000),
|
||||
track_type: video,
|
||||
is_video: true,
|
||||
track_nr: None,
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -331,7 +331,7 @@ MusicArtist(
|
|||
artist_id: Some("UC_xEL8cbkItBH00KrGz9fbQ"),
|
||||
album: None,
|
||||
view_count: Some(7400),
|
||||
track_type: video,
|
||||
is_video: true,
|
||||
track_nr: None,
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -360,7 +360,7 @@ MusicArtist(
|
|||
artist_id: Some("UCaFqztcJss3HrXNurzQJyqQ"),
|
||||
album: None,
|
||||
view_count: Some(1400),
|
||||
track_type: video,
|
||||
is_video: true,
|
||||
track_nr: None,
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -389,7 +389,7 @@ MusicArtist(
|
|||
artist_id: Some("UCMPqKiPdiSoi8eCW5Dou1IQ"),
|
||||
album: None,
|
||||
view_count: Some(25000),
|
||||
track_type: video,
|
||||
is_video: true,
|
||||
track_nr: None,
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -418,7 +418,7 @@ MusicArtist(
|
|||
artist_id: Some("UCe52oeb7Xv_KaJsEzcKXJJg"),
|
||||
album: None,
|
||||
view_count: Some(3700),
|
||||
track_type: video,
|
||||
is_video: true,
|
||||
track_nr: None,
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -446,7 +446,7 @@ MusicArtist(
|
|||
),
|
||||
],
|
||||
artist_id: Some("UCfwCE5VhPMGxNPFxtVv7lRw"),
|
||||
album_type: single,
|
||||
album_type: Single,
|
||||
year: Some(2019),
|
||||
by_va: false,
|
||||
),
|
||||
|
|
|
@ -33,7 +33,7 @@ MusicCharts(
|
|||
artist_id: Some("UCiXhCjTprNP0nuQJ9UsLWeg"),
|
||||
album: None,
|
||||
view_count: Some(56000000),
|
||||
track_type: video,
|
||||
is_video: true,
|
||||
track_nr: None,
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -62,7 +62,7 @@ MusicCharts(
|
|||
artist_id: Some("UCybEdRVR5u_WFoV-BLTEBiA"),
|
||||
album: None,
|
||||
view_count: Some(15000000),
|
||||
track_type: video,
|
||||
is_video: true,
|
||||
track_nr: None,
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -91,7 +91,7 @@ MusicCharts(
|
|||
artist_id: Some("UCiY3z8HAGD6BlSNKVn2kSvQ"),
|
||||
album: None,
|
||||
view_count: Some(521000000),
|
||||
track_type: video,
|
||||
is_video: true,
|
||||
track_nr: None,
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -120,7 +120,7 @@ MusicCharts(
|
|||
artist_id: Some("UCWsDFcIhY2DBi3GB5uykGXA"),
|
||||
album: None,
|
||||
view_count: Some(34000000),
|
||||
track_type: video,
|
||||
is_video: true,
|
||||
track_nr: None,
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -149,7 +149,7 @@ MusicCharts(
|
|||
artist_id: Some("UCiY3z8HAGD6BlSNKVn2kSvQ"),
|
||||
album: None,
|
||||
view_count: Some(559000000),
|
||||
track_type: video,
|
||||
is_video: true,
|
||||
track_nr: None,
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -178,7 +178,7 @@ MusicCharts(
|
|||
artist_id: Some("UCMXDyVR2tclKWhbqNforSyA"),
|
||||
album: None,
|
||||
view_count: Some(39000000),
|
||||
track_type: video,
|
||||
is_video: true,
|
||||
track_nr: None,
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -207,7 +207,7 @@ MusicCharts(
|
|||
artist_id: Some("UCJa2FF4TUB13Mm0GurZAqog"),
|
||||
album: None,
|
||||
view_count: Some(139000000),
|
||||
track_type: video,
|
||||
is_video: true,
|
||||
track_nr: None,
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -236,7 +236,7 @@ MusicCharts(
|
|||
artist_id: Some("UCKRnq8aBOCanYlffje7HyvA"),
|
||||
album: None,
|
||||
view_count: Some(311000000),
|
||||
track_type: video,
|
||||
is_video: true,
|
||||
track_nr: None,
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -265,7 +265,7 @@ MusicCharts(
|
|||
artist_id: Some("UCR28YDxjDE3ogQROaNdnRbQ"),
|
||||
album: None,
|
||||
view_count: Some(3800000),
|
||||
track_type: video,
|
||||
is_video: true,
|
||||
track_nr: None,
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -294,7 +294,7 @@ MusicCharts(
|
|||
artist_id: Some("UCpcTrCXblq78GZrTUTLWeBw"),
|
||||
album: None,
|
||||
view_count: Some(46000000),
|
||||
track_type: video,
|
||||
is_video: true,
|
||||
track_nr: None,
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -323,7 +323,7 @@ MusicCharts(
|
|||
artist_id: Some("UCJa2FF4TUB13Mm0GurZAqog"),
|
||||
album: None,
|
||||
view_count: Some(73000000),
|
||||
track_type: video,
|
||||
is_video: true,
|
||||
track_nr: None,
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -356,7 +356,7 @@ MusicCharts(
|
|||
artist_id: Some("UCohgH17dyp4c_V7U9LoBLdA"),
|
||||
album: None,
|
||||
view_count: Some(77000000),
|
||||
track_type: video,
|
||||
is_video: true,
|
||||
track_nr: None,
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -385,7 +385,7 @@ MusicCharts(
|
|||
artist_id: Some("UChWPNW87QHcXAsw2mzlsYNw"),
|
||||
album: None,
|
||||
view_count: Some(2600000),
|
||||
track_type: video,
|
||||
is_video: true,
|
||||
track_nr: None,
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -414,7 +414,7 @@ MusicCharts(
|
|||
artist_id: Some("UC_z9AthnCGSAk_tZf-KqoFA"),
|
||||
album: None,
|
||||
view_count: Some(17000000),
|
||||
track_type: video,
|
||||
is_video: true,
|
||||
track_nr: None,
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -451,7 +451,7 @@ MusicCharts(
|
|||
artist_id: Some("UCdPdi8UM25ZyvzhSJkk1uPw"),
|
||||
album: None,
|
||||
view_count: Some(8600000),
|
||||
track_type: video,
|
||||
is_video: true,
|
||||
track_nr: None,
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -480,7 +480,7 @@ MusicCharts(
|
|||
artist_id: Some("UC_z9AthnCGSAk_tZf-KqoFA"),
|
||||
album: None,
|
||||
view_count: Some(15000000),
|
||||
track_type: video,
|
||||
is_video: true,
|
||||
track_nr: None,
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -509,7 +509,7 @@ MusicCharts(
|
|||
artist_id: Some("UCXT9NWRyDfHJq9Igm1pDQpQ"),
|
||||
album: None,
|
||||
view_count: Some(31000000),
|
||||
track_type: video,
|
||||
is_video: true,
|
||||
track_nr: None,
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -542,7 +542,7 @@ MusicCharts(
|
|||
artist_id: Some("UC7n3gWRN0vQzgiOKc51aZ4w"),
|
||||
album: None,
|
||||
view_count: Some(202000000),
|
||||
track_type: video,
|
||||
is_video: true,
|
||||
track_nr: None,
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -571,7 +571,7 @@ MusicCharts(
|
|||
artist_id: Some("UCGJdT8Qip4XObbQZ98Z1CAA"),
|
||||
album: None,
|
||||
view_count: Some(4900000),
|
||||
track_type: video,
|
||||
is_video: true,
|
||||
track_nr: None,
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -600,7 +600,7 @@ MusicCharts(
|
|||
artist_id: Some("UC7n3gWRN0vQzgiOKc51aZ4w"),
|
||||
album: None,
|
||||
view_count: Some(545000000),
|
||||
track_type: video,
|
||||
is_video: true,
|
||||
track_nr: None,
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -633,7 +633,7 @@ MusicCharts(
|
|||
artist_id: Some("UC5IkSn-EFsUu3XANYklXc8g"),
|
||||
album: None,
|
||||
view_count: Some(20000000),
|
||||
track_type: video,
|
||||
is_video: true,
|
||||
track_nr: None,
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -666,7 +666,7 @@ MusicCharts(
|
|||
artist_id: Some("UC7n3gWRN0vQzgiOKc51aZ4w"),
|
||||
album: None,
|
||||
view_count: Some(36000000),
|
||||
track_type: video,
|
||||
is_video: true,
|
||||
track_nr: None,
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -699,7 +699,7 @@ MusicCharts(
|
|||
artist_id: Some("UCgpBsaDW2n_6ruzht3wvP0A"),
|
||||
album: None,
|
||||
view_count: Some(66000000),
|
||||
track_type: video,
|
||||
is_video: true,
|
||||
track_nr: None,
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -728,7 +728,7 @@ MusicCharts(
|
|||
artist_id: Some("UCPC0L1d253x-KuMNwa05TpA"),
|
||||
album: None,
|
||||
view_count: Some(68000000),
|
||||
track_type: video,
|
||||
is_video: true,
|
||||
track_nr: None,
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -757,7 +757,7 @@ MusicCharts(
|
|||
artist_id: Some("UCju-DqP7JNtCnMWFXhLgPHQ"),
|
||||
album: None,
|
||||
view_count: Some(46000000),
|
||||
track_type: video,
|
||||
is_video: true,
|
||||
track_nr: None,
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -790,7 +790,7 @@ MusicCharts(
|
|||
artist_id: Some("UC5IkSn-EFsUu3XANYklXc8g"),
|
||||
album: None,
|
||||
view_count: Some(43000000),
|
||||
track_type: video,
|
||||
is_video: true,
|
||||
track_nr: None,
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -823,7 +823,7 @@ MusicCharts(
|
|||
artist_id: Some("UCoC_a7lWbj2v7rt4ujp4n2A"),
|
||||
album: None,
|
||||
view_count: Some(7200000),
|
||||
track_type: video,
|
||||
is_video: true,
|
||||
track_nr: None,
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -852,7 +852,7 @@ MusicCharts(
|
|||
artist_id: Some("UCvUZUUxWhwtKLVQ9bVRjLEA"),
|
||||
album: None,
|
||||
view_count: Some(4000000),
|
||||
track_type: video,
|
||||
is_video: true,
|
||||
track_nr: None,
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -881,7 +881,7 @@ MusicCharts(
|
|||
artist_id: Some("UCr_zAwkma5JAyHOWfVXaouA"),
|
||||
album: None,
|
||||
view_count: Some(2900000),
|
||||
track_type: video,
|
||||
is_video: true,
|
||||
track_nr: None,
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -914,7 +914,7 @@ MusicCharts(
|
|||
artist_id: Some("UC_z9AthnCGSAk_tZf-KqoFA"),
|
||||
album: None,
|
||||
view_count: Some(10000000),
|
||||
track_type: video,
|
||||
is_video: true,
|
||||
track_nr: None,
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -943,7 +943,7 @@ MusicCharts(
|
|||
artist_id: Some("UCBabNBocAdKiN5sz8RBjIDg"),
|
||||
album: None,
|
||||
view_count: Some(15000000),
|
||||
track_type: video,
|
||||
is_video: true,
|
||||
track_nr: None,
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -972,7 +972,7 @@ MusicCharts(
|
|||
artist_id: Some("UC5xaQ6_dP7EGDmGLzVGZ1Ow"),
|
||||
album: None,
|
||||
view_count: Some(16000000),
|
||||
track_type: video,
|
||||
is_video: true,
|
||||
track_nr: None,
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -1001,7 +1001,7 @@ MusicCharts(
|
|||
artist_id: Some("UCiXhCjTprNP0nuQJ9UsLWeg"),
|
||||
album: None,
|
||||
view_count: Some(21000000),
|
||||
track_type: video,
|
||||
is_video: true,
|
||||
track_nr: None,
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -1030,7 +1030,7 @@ MusicCharts(
|
|||
artist_id: Some("UC_VCJd8skzwcPktsMLqTz1g"),
|
||||
album: None,
|
||||
view_count: Some(35000000),
|
||||
track_type: video,
|
||||
is_video: true,
|
||||
track_nr: None,
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -1067,7 +1067,7 @@ MusicCharts(
|
|||
artist_id: None,
|
||||
album: None,
|
||||
view_count: Some(30000000),
|
||||
track_type: video,
|
||||
is_video: true,
|
||||
track_nr: None,
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -1096,7 +1096,7 @@ MusicCharts(
|
|||
artist_id: Some("UCq_Fb1zqNikdovyMJgRQjcw"),
|
||||
album: None,
|
||||
view_count: Some(18000000),
|
||||
track_type: video,
|
||||
is_video: true,
|
||||
track_nr: None,
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -1125,7 +1125,7 @@ MusicCharts(
|
|||
artist_id: Some("UChWPNW87QHcXAsw2mzlsYNw"),
|
||||
album: None,
|
||||
view_count: Some(5400000),
|
||||
track_type: video,
|
||||
is_video: true,
|
||||
track_nr: None,
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -1154,7 +1154,7 @@ MusicCharts(
|
|||
artist_id: Some("UCiY3z8HAGD6BlSNKVn2kSvQ"),
|
||||
album: None,
|
||||
view_count: Some(312000000),
|
||||
track_type: video,
|
||||
is_video: true,
|
||||
track_nr: None,
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -1183,7 +1183,7 @@ MusicCharts(
|
|||
artist_id: Some("UC0_1glf30IS53tFQWT8xpxw"),
|
||||
album: None,
|
||||
view_count: Some(28000000),
|
||||
track_type: video,
|
||||
is_video: true,
|
||||
track_nr: None,
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -1212,7 +1212,7 @@ MusicCharts(
|
|||
artist_id: Some("UC_z9AthnCGSAk_tZf-KqoFA"),
|
||||
album: None,
|
||||
view_count: Some(97000000),
|
||||
track_type: video,
|
||||
is_video: true,
|
||||
track_nr: None,
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -1238,7 +1238,7 @@ MusicCharts(
|
|||
artist_id: Some("UCGexNm_Kw4rdQjLxmpb2EKw"),
|
||||
album: None,
|
||||
view_count: Some(6000000),
|
||||
track_type: video,
|
||||
is_video: true,
|
||||
track_nr: None,
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -1262,7 +1262,7 @@ MusicCharts(
|
|||
artist_id: Some("UCybEdRVR5u_WFoV-BLTEBiA"),
|
||||
album: None,
|
||||
view_count: Some(15000000),
|
||||
track_type: video,
|
||||
is_video: true,
|
||||
track_nr: None,
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -1286,7 +1286,7 @@ MusicCharts(
|
|||
artist_id: Some("UCTP45_DE3fMLujU8sZ-MBzw"),
|
||||
album: None,
|
||||
view_count: Some(10000000),
|
||||
track_type: video,
|
||||
is_video: true,
|
||||
track_nr: None,
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -1314,7 +1314,7 @@ MusicCharts(
|
|||
artist_id: Some("UC_duTRnaqtLLTCDIlqjRTcQ"),
|
||||
album: None,
|
||||
view_count: Some(3600000),
|
||||
track_type: video,
|
||||
is_video: true,
|
||||
track_nr: None,
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -1338,7 +1338,7 @@ MusicCharts(
|
|||
artist_id: Some("UCPoQYATXIYvN5WB0c4f6jfQ"),
|
||||
album: None,
|
||||
view_count: Some(524000),
|
||||
track_type: video,
|
||||
is_video: true,
|
||||
track_nr: None,
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -1362,7 +1362,7 @@ MusicCharts(
|
|||
artist_id: Some("UCR28YDxjDE3ogQROaNdnRbQ"),
|
||||
album: None,
|
||||
view_count: Some(3800000),
|
||||
track_type: video,
|
||||
is_video: true,
|
||||
track_nr: None,
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -1386,7 +1386,7 @@ MusicCharts(
|
|||
artist_id: Some("UCpcTrCXblq78GZrTUTLWeBw"),
|
||||
album: None,
|
||||
view_count: Some(46000000),
|
||||
track_type: video,
|
||||
is_video: true,
|
||||
track_nr: None,
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -1410,7 +1410,7 @@ MusicCharts(
|
|||
artist_id: Some("UCEf_Bc-KVd7onSeifS3py9g"),
|
||||
album: None,
|
||||
view_count: Some(8300000),
|
||||
track_type: video,
|
||||
is_video: true,
|
||||
track_nr: None,
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -1434,7 +1434,7 @@ MusicCharts(
|
|||
artist_id: Some("UCVcAt8IIKIeubRSigcYXgtA"),
|
||||
album: None,
|
||||
view_count: Some(13000000),
|
||||
track_type: video,
|
||||
is_video: true,
|
||||
track_nr: None,
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -1458,7 +1458,7 @@ MusicCharts(
|
|||
artist_id: Some("UC0_1glf30IS53tFQWT8xpxw"),
|
||||
album: None,
|
||||
view_count: Some(365000),
|
||||
track_type: video,
|
||||
is_video: true,
|
||||
track_nr: None,
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -1482,7 +1482,7 @@ MusicCharts(
|
|||
artist_id: Some("UC1_liDR4fRFJgH4HoJeV8cw"),
|
||||
album: None,
|
||||
view_count: Some(754000),
|
||||
track_type: video,
|
||||
is_video: true,
|
||||
track_nr: None,
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -1506,7 +1506,7 @@ MusicCharts(
|
|||
artist_id: Some("UCGJdT8Qip4XObbQZ98Z1CAA"),
|
||||
album: None,
|
||||
view_count: Some(4900000),
|
||||
track_type: video,
|
||||
is_video: true,
|
||||
track_nr: None,
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -1530,7 +1530,7 @@ MusicCharts(
|
|||
artist_id: Some("UCr_zAwkma5JAyHOWfVXaouA"),
|
||||
album: None,
|
||||
view_count: Some(2900000),
|
||||
track_type: video,
|
||||
is_video: true,
|
||||
track_nr: None,
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -1554,7 +1554,7 @@ MusicCharts(
|
|||
artist_id: Some("UCvUZUUxWhwtKLVQ9bVRjLEA"),
|
||||
album: None,
|
||||
view_count: Some(4000000),
|
||||
track_type: video,
|
||||
is_video: true,
|
||||
track_nr: None,
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -1582,7 +1582,7 @@ MusicCharts(
|
|||
artist_id: Some("UC7n3gWRN0vQzgiOKc51aZ4w"),
|
||||
album: None,
|
||||
view_count: Some(36000000),
|
||||
track_type: video,
|
||||
is_video: true,
|
||||
track_nr: None,
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -1606,7 +1606,7 @@ MusicCharts(
|
|||
artist_id: Some("UCVcAt8IIKIeubRSigcYXgtA"),
|
||||
album: None,
|
||||
view_count: Some(2000000),
|
||||
track_type: video,
|
||||
is_video: true,
|
||||
track_nr: None,
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -1630,7 +1630,7 @@ MusicCharts(
|
|||
artist_id: Some("UChWPNW87QHcXAsw2mzlsYNw"),
|
||||
album: None,
|
||||
view_count: Some(2600000),
|
||||
track_type: video,
|
||||
is_video: true,
|
||||
track_nr: None,
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -1662,7 +1662,7 @@ MusicCharts(
|
|||
artist_id: Some("UC47k7qXysCBKeaYfc1zmkIA"),
|
||||
album: None,
|
||||
view_count: Some(3500000),
|
||||
track_type: video,
|
||||
is_video: true,
|
||||
track_nr: None,
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -1686,7 +1686,7 @@ MusicCharts(
|
|||
artist_id: Some("UCjfB7ooJY7C43vBAuuCub_A"),
|
||||
album: None,
|
||||
view_count: Some(367000),
|
||||
track_type: video,
|
||||
is_video: true,
|
||||
track_nr: None,
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -1710,7 +1710,7 @@ MusicCharts(
|
|||
artist_id: Some("UC5xaQ6_dP7EGDmGLzVGZ1Ow"),
|
||||
album: None,
|
||||
view_count: Some(1500000),
|
||||
track_type: video,
|
||||
is_video: true,
|
||||
track_nr: None,
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -2416,7 +2416,6 @@ MusicCharts(
|
|||
channel: None,
|
||||
track_count: None,
|
||||
from_ytm: true,
|
||||
is_podcast: false,
|
||||
),
|
||||
MusicPlaylistItem(
|
||||
id: "PL4fGSI1pDJn4fmCoF1vKHLtivI0f9yHiF",
|
||||
|
@ -2436,7 +2435,6 @@ MusicCharts(
|
|||
channel: None,
|
||||
track_count: None,
|
||||
from_ytm: true,
|
||||
is_podcast: false,
|
||||
),
|
||||
MusicPlaylistItem(
|
||||
id: "PL4fGSI1pDJn5O8siDeZuI_4hbk6JWtTX1",
|
||||
|
@ -2456,7 +2454,6 @@ MusicCharts(
|
|||
channel: None,
|
||||
track_count: None,
|
||||
from_ytm: true,
|
||||
is_podcast: false,
|
||||
),
|
||||
MusicPlaylistItem(
|
||||
id: "PL4fGSI1pDJn4EBsWVeFpcSAVOFMfhyipg",
|
||||
|
@ -2476,7 +2473,6 @@ MusicCharts(
|
|||
channel: None,
|
||||
track_count: None,
|
||||
from_ytm: true,
|
||||
is_podcast: false,
|
||||
),
|
||||
MusicPlaylistItem(
|
||||
id: "PL4fGSI1pDJn5LOptOQixqnzXNGjNXAgYY",
|
||||
|
@ -2496,7 +2492,6 @@ MusicCharts(
|
|||
channel: None,
|
||||
track_count: None,
|
||||
from_ytm: true,
|
||||
is_podcast: false,
|
||||
),
|
||||
MusicPlaylistItem(
|
||||
id: "PL4fGSI1pDJn4w4wTTgOmP_S80PoCtbGrL",
|
||||
|
@ -2516,7 +2511,6 @@ MusicCharts(
|
|||
channel: None,
|
||||
track_count: None,
|
||||
from_ytm: true,
|
||||
is_podcast: false,
|
||||
),
|
||||
MusicPlaylistItem(
|
||||
id: "PL4fGSI1pDJn7Wkr6Ll6ds1AhA42rT8uaU",
|
||||
|
@ -2536,7 +2530,6 @@ MusicCharts(
|
|||
channel: None,
|
||||
track_count: None,
|
||||
from_ytm: true,
|
||||
is_podcast: false,
|
||||
),
|
||||
MusicPlaylistItem(
|
||||
id: "PL4fGSI1pDJn4rBU0RHnR6-b1_uE20CzRH",
|
||||
|
@ -2556,7 +2549,6 @@ MusicCharts(
|
|||
channel: None,
|
||||
track_count: None,
|
||||
from_ytm: true,
|
||||
is_podcast: false,
|
||||
),
|
||||
],
|
||||
top_playlist_id: Some("PL4fGSI1pDJn69On1f-8NAvX_CYlx7QyZc"),
|
||||
|
|
|
@ -29,7 +29,7 @@ MusicCharts(
|
|||
artist_id: Some("UCpcTrCXblq78GZrTUTLWeBw"),
|
||||
album: None,
|
||||
view_count: Some(46000000),
|
||||
track_type: video,
|
||||
is_video: true,
|
||||
track_nr: None,
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -62,7 +62,7 @@ MusicCharts(
|
|||
artist_id: Some("UC9vrvNSL3xcWGSkV86REBSg"),
|
||||
album: None,
|
||||
view_count: Some(46000000),
|
||||
track_type: video,
|
||||
is_video: true,
|
||||
track_nr: None,
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -91,7 +91,7 @@ MusicCharts(
|
|||
artist_id: Some("UCo6JijJGA3IvIiPsawDK3Ww"),
|
||||
album: None,
|
||||
view_count: Some(3300000000),
|
||||
track_type: video,
|
||||
is_video: true,
|
||||
track_nr: None,
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -124,7 +124,7 @@ MusicCharts(
|
|||
artist_id: Some("UCONiUl5u7y2bMaVZJcuRDEQ"),
|
||||
album: None,
|
||||
view_count: Some(38000000),
|
||||
track_type: video,
|
||||
is_video: true,
|
||||
track_nr: None,
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -157,7 +157,7 @@ MusicCharts(
|
|||
artist_id: None,
|
||||
album: None,
|
||||
view_count: Some(57000000),
|
||||
track_type: video,
|
||||
is_video: true,
|
||||
track_nr: None,
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -186,7 +186,7 @@ MusicCharts(
|
|||
artist_id: Some("UCiY3z8HAGD6BlSNKVn2kSvQ"),
|
||||
album: None,
|
||||
view_count: Some(521000000),
|
||||
track_type: video,
|
||||
is_video: true,
|
||||
track_nr: None,
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -219,7 +219,7 @@ MusicCharts(
|
|||
artist_id: Some("UC5p07Pr3hlfjXo3YGVCyOgg"),
|
||||
album: None,
|
||||
view_count: Some(76000000),
|
||||
track_type: video,
|
||||
is_video: true,
|
||||
track_nr: None,
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -248,7 +248,7 @@ MusicCharts(
|
|||
artist_id: Some("UCfh2j2Dq-aSeLhzuPOsnhVg"),
|
||||
album: None,
|
||||
view_count: Some(276000000),
|
||||
track_type: video,
|
||||
is_video: true,
|
||||
track_nr: None,
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -281,7 +281,7 @@ MusicCharts(
|
|||
artist_id: Some("UCeBYRgPhy8kcRmIGQWKuqdQ"),
|
||||
album: None,
|
||||
view_count: Some(136000000),
|
||||
track_type: video,
|
||||
is_video: true,
|
||||
track_nr: None,
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -310,7 +310,7 @@ MusicCharts(
|
|||
artist_id: Some("UCiY3z8HAGD6BlSNKVn2kSvQ"),
|
||||
album: None,
|
||||
view_count: Some(559000000),
|
||||
track_type: video,
|
||||
is_video: true,
|
||||
track_nr: None,
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -339,7 +339,7 @@ MusicCharts(
|
|||
artist_id: Some("UCDxKh1gFWeYsqePvgVzmPoQ"),
|
||||
album: None,
|
||||
view_count: Some(331000000),
|
||||
track_type: video,
|
||||
is_video: true,
|
||||
track_nr: None,
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -368,7 +368,7 @@ MusicCharts(
|
|||
artist_id: Some("UCkbbMCA40i18i7UdjayMPAg"),
|
||||
album: None,
|
||||
view_count: Some(257000000),
|
||||
track_type: video,
|
||||
is_video: true,
|
||||
track_nr: None,
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -401,7 +401,7 @@ MusicCharts(
|
|||
artist_id: Some("UC7n3gWRN0vQzgiOKc51aZ4w"),
|
||||
album: None,
|
||||
view_count: Some(36000000),
|
||||
track_type: video,
|
||||
is_video: true,
|
||||
track_nr: None,
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -442,7 +442,7 @@ MusicCharts(
|
|||
artist_id: Some("UCKEFjh4JL-OyMI8z3h5Coaw"),
|
||||
album: None,
|
||||
view_count: Some(50000000),
|
||||
track_type: video,
|
||||
is_video: true,
|
||||
track_nr: None,
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -475,7 +475,7 @@ MusicCharts(
|
|||
artist_id: Some("UC7n3gWRN0vQzgiOKc51aZ4w"),
|
||||
album: None,
|
||||
view_count: Some(202000000),
|
||||
track_type: video,
|
||||
is_video: true,
|
||||
track_nr: None,
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -504,7 +504,7 @@ MusicCharts(
|
|||
artist_id: Some("UCKNGMXJHTiGFdZNSo_zs3fQ"),
|
||||
album: None,
|
||||
view_count: Some(103000000),
|
||||
track_type: video,
|
||||
is_video: true,
|
||||
track_nr: None,
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -533,7 +533,7 @@ MusicCharts(
|
|||
artist_id: Some("UCkbbMCA40i18i7UdjayMPAg"),
|
||||
album: None,
|
||||
view_count: Some(453000000),
|
||||
track_type: video,
|
||||
is_video: true,
|
||||
track_nr: None,
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -562,7 +562,7 @@ MusicCharts(
|
|||
artist_id: Some("UCUamzwxCTrUvpyAvAt4FEdg"),
|
||||
album: None,
|
||||
view_count: Some(44000000),
|
||||
track_type: video,
|
||||
is_video: true,
|
||||
track_nr: None,
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -591,7 +591,7 @@ MusicCharts(
|
|||
artist_id: Some("UCKEFjh4JL-OyMI8z3h5Coaw"),
|
||||
album: None,
|
||||
view_count: Some(81000000),
|
||||
track_type: video,
|
||||
is_video: true,
|
||||
track_nr: None,
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -620,7 +620,7 @@ MusicCharts(
|
|||
artist_id: Some("UCJa2FF4TUB13Mm0GurZAqog"),
|
||||
album: None,
|
||||
view_count: Some(73000000),
|
||||
track_type: video,
|
||||
is_video: true,
|
||||
track_nr: None,
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -649,7 +649,7 @@ MusicCharts(
|
|||
artist_id: Some("UC6uMb9hMAziN9HZoXfTBAlg"),
|
||||
album: None,
|
||||
view_count: Some(45000000),
|
||||
track_type: video,
|
||||
is_video: true,
|
||||
track_nr: None,
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -678,7 +678,7 @@ MusicCharts(
|
|||
artist_id: Some("UC7n3gWRN0vQzgiOKc51aZ4w"),
|
||||
album: None,
|
||||
view_count: Some(545000000),
|
||||
track_type: video,
|
||||
is_video: true,
|
||||
track_nr: None,
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -707,7 +707,7 @@ MusicCharts(
|
|||
artist_id: Some("UCJa2FF4TUB13Mm0GurZAqog"),
|
||||
album: None,
|
||||
view_count: Some(139000000),
|
||||
track_type: video,
|
||||
is_video: true,
|
||||
track_nr: None,
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -736,7 +736,7 @@ MusicCharts(
|
|||
artist_id: Some("UCeYz6rzUGhVwqxRM37FUo8w"),
|
||||
album: None,
|
||||
view_count: Some(197000000),
|
||||
track_type: video,
|
||||
is_video: true,
|
||||
track_nr: None,
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -769,7 +769,7 @@ MusicCharts(
|
|||
artist_id: Some("UCy6qn2oxmoXA4_gBA5Q7zPw"),
|
||||
album: None,
|
||||
view_count: Some(257000000),
|
||||
track_type: video,
|
||||
is_video: true,
|
||||
track_nr: None,
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -798,7 +798,7 @@ MusicCharts(
|
|||
artist_id: Some("UCybEdRVR5u_WFoV-BLTEBiA"),
|
||||
album: None,
|
||||
view_count: Some(15000000),
|
||||
track_type: video,
|
||||
is_video: true,
|
||||
track_nr: None,
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -827,7 +827,7 @@ MusicCharts(
|
|||
artist_id: Some("UCtGHTwNL20Y3fY9bumjHDOw"),
|
||||
album: None,
|
||||
view_count: Some(55000000),
|
||||
track_type: video,
|
||||
is_video: true,
|
||||
track_nr: None,
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -856,7 +856,7 @@ MusicCharts(
|
|||
artist_id: Some("UCWsDFcIhY2DBi3GB5uykGXA"),
|
||||
album: None,
|
||||
view_count: Some(34000000),
|
||||
track_type: video,
|
||||
is_video: true,
|
||||
track_nr: None,
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -889,7 +889,7 @@ MusicCharts(
|
|||
artist_id: Some("UCo6JijJGA3IvIiPsawDK3Ww"),
|
||||
album: None,
|
||||
view_count: Some(123000000),
|
||||
track_type: video,
|
||||
is_video: true,
|
||||
track_nr: None,
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -918,7 +918,7 @@ MusicCharts(
|
|||
artist_id: Some("UCc3e8O2V5_7OA300ursDyFQ"),
|
||||
album: None,
|
||||
view_count: Some(109000000),
|
||||
track_type: video,
|
||||
is_video: true,
|
||||
track_nr: None,
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -947,7 +947,7 @@ MusicCharts(
|
|||
artist_id: Some("UC3QmG1Jn9cE5fTMt14DLuZw"),
|
||||
album: None,
|
||||
view_count: Some(5700000000),
|
||||
track_type: video,
|
||||
is_video: true,
|
||||
track_nr: None,
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -976,7 +976,7 @@ MusicCharts(
|
|||
artist_id: Some("UC03jIQv4WXBSHdr1DlCLYDw"),
|
||||
album: None,
|
||||
view_count: Some(872000000),
|
||||
track_type: video,
|
||||
is_video: true,
|
||||
track_nr: None,
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -1005,7 +1005,7 @@ MusicCharts(
|
|||
artist_id: Some("UCSzWQmDsKG37iKN2vw1G-2Q"),
|
||||
album: None,
|
||||
view_count: Some(7900000),
|
||||
track_type: video,
|
||||
is_video: true,
|
||||
track_nr: None,
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -1034,7 +1034,7 @@ MusicCharts(
|
|||
artist_id: Some("UCo6JijJGA3IvIiPsawDK3Ww"),
|
||||
album: None,
|
||||
view_count: Some(750000000),
|
||||
track_type: video,
|
||||
is_video: true,
|
||||
track_nr: None,
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -1063,7 +1063,7 @@ MusicCharts(
|
|||
artist_id: Some("UCKRnq8aBOCanYlffje7HyvA"),
|
||||
album: None,
|
||||
view_count: Some(311000000),
|
||||
track_type: video,
|
||||
is_video: true,
|
||||
track_nr: None,
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -1100,7 +1100,7 @@ MusicCharts(
|
|||
artist_id: Some("UCQK0swJm0ceapSOtRKIWr0g"),
|
||||
album: None,
|
||||
view_count: Some(37000000),
|
||||
track_type: video,
|
||||
is_video: true,
|
||||
track_nr: None,
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -1133,7 +1133,7 @@ MusicCharts(
|
|||
artist_id: Some("UC7PL9aor5qNRhvhWWVXyOqA"),
|
||||
album: None,
|
||||
view_count: Some(377000000),
|
||||
track_type: video,
|
||||
is_video: true,
|
||||
track_nr: None,
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -1166,7 +1166,7 @@ MusicCharts(
|
|||
artist_id: Some("UC2kPe8FB39lojsUDtyKcqOQ"),
|
||||
album: None,
|
||||
view_count: Some(486000000),
|
||||
track_type: video,
|
||||
is_video: true,
|
||||
track_nr: None,
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -1195,7 +1195,7 @@ MusicCharts(
|
|||
artist_id: Some("UCrP3Rfz32MT-OH9MZh_N9kA"),
|
||||
album: None,
|
||||
view_count: Some(570000000),
|
||||
track_type: video,
|
||||
is_video: true,
|
||||
track_nr: None,
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -1224,7 +1224,7 @@ MusicCharts(
|
|||
artist_id: Some("UC0QVToeCjC9-1u-teWToPsg"),
|
||||
album: None,
|
||||
view_count: Some(28000000),
|
||||
track_type: video,
|
||||
is_video: true,
|
||||
track_nr: None,
|
||||
by_va: false,
|
||||
),
|
||||
|
|
|
@ -33,7 +33,7 @@ TrackDetails(
|
|||
artist_id: Some("UCEdZAdnnKqbaHOlv8nM6OtA"),
|
||||
album: None,
|
||||
view_count: Some(235000000),
|
||||
track_type: video,
|
||||
is_video: true,
|
||||
track_nr: None,
|
||||
by_va: false,
|
||||
),
|
||||
|
|
|
@ -51,7 +51,7 @@ TrackDetails(
|
|||
name: "INVU - The 3rd Album",
|
||||
)),
|
||||
view_count: None,
|
||||
track_type: track,
|
||||
is_video: false,
|
||||
track_nr: None,
|
||||
by_va: false,
|
||||
),
|
||||
|
|
|
@ -35,7 +35,7 @@ Paginator(
|
|||
artist_id: Some("UCEdZAdnnKqbaHOlv8nM6OtA"),
|
||||
album: None,
|
||||
view_count: Some(250000000),
|
||||
track_type: video,
|
||||
is_video: true,
|
||||
track_nr: None,
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -69,7 +69,7 @@ Paginator(
|
|||
artist_id: Some("UC_4Y1QqJr60C5Z7-eQWy-mw"),
|
||||
album: None,
|
||||
view_count: Some(168000000),
|
||||
track_type: video,
|
||||
is_video: true,
|
||||
track_nr: None,
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -103,7 +103,7 @@ Paginator(
|
|||
artist_id: Some("UCAq0pFGa2w9SjxOq0ZxKVIw"),
|
||||
album: None,
|
||||
view_count: Some(464000000),
|
||||
track_type: video,
|
||||
is_video: true,
|
||||
track_nr: None,
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -137,7 +137,7 @@ Paginator(
|
|||
artist_id: Some("UCTP45_DE3fMLujU8sZ-MBzw"),
|
||||
album: None,
|
||||
view_count: Some(230000000),
|
||||
track_type: video,
|
||||
is_video: true,
|
||||
track_nr: None,
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -171,7 +171,7 @@ Paginator(
|
|||
artist_id: Some("UCkbbMCA40i18i7UdjayMPAg"),
|
||||
album: None,
|
||||
view_count: Some(422000000),
|
||||
track_type: video,
|
||||
is_video: true,
|
||||
track_nr: None,
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -205,7 +205,7 @@ Paginator(
|
|||
artist_id: Some("UCHmZYTfdTyVKQEJicLiXEOg"),
|
||||
album: None,
|
||||
view_count: Some(349000000),
|
||||
track_type: video,
|
||||
is_video: true,
|
||||
track_nr: None,
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -239,7 +239,7 @@ Paginator(
|
|||
artist_id: Some("UCuKdaTsJ9Jv94hVV_I9aRxQ"),
|
||||
album: None,
|
||||
view_count: Some(167000000),
|
||||
track_type: video,
|
||||
is_video: true,
|
||||
track_nr: None,
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -273,7 +273,7 @@ Paginator(
|
|||
artist_id: Some("UC-clMkTZa7k-FxmNgMjoCgQ"),
|
||||
album: None,
|
||||
view_count: Some(124000000),
|
||||
track_type: video,
|
||||
is_video: true,
|
||||
track_nr: None,
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -307,7 +307,7 @@ Paginator(
|
|||
artist_id: Some("UCEf_Bc-KVd7onSeifS3py9g"),
|
||||
album: None,
|
||||
view_count: Some(127000000),
|
||||
track_type: video,
|
||||
is_video: true,
|
||||
track_nr: None,
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -336,7 +336,7 @@ Paginator(
|
|||
artist_id: Some("UCqTaQGqjAI6fYkr84KZgZEg"),
|
||||
album: None,
|
||||
view_count: Some(239000000),
|
||||
track_type: video,
|
||||
is_video: true,
|
||||
track_nr: None,
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -370,7 +370,7 @@ Paginator(
|
|||
artist_id: Some("UCAKvDuIX3m1AUdPpDSqV_3w"),
|
||||
album: None,
|
||||
view_count: Some(140000000),
|
||||
track_type: video,
|
||||
is_video: true,
|
||||
track_nr: None,
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -404,7 +404,7 @@ Paginator(
|
|||
artist_id: Some("UC_Cx288SDUD9liYn7CiJLAA"),
|
||||
album: None,
|
||||
view_count: Some(90000000),
|
||||
track_type: video,
|
||||
is_video: true,
|
||||
track_nr: None,
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -438,7 +438,7 @@ Paginator(
|
|||
artist_id: Some("UCDDpqmryjNunitS05bv7-8w"),
|
||||
album: None,
|
||||
view_count: Some(137000000),
|
||||
track_type: video,
|
||||
is_video: true,
|
||||
track_nr: None,
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -472,7 +472,7 @@ Paginator(
|
|||
artist_id: Some("UCEdZAdnnKqbaHOlv8nM6OtA"),
|
||||
album: None,
|
||||
view_count: Some(220000000),
|
||||
track_type: video,
|
||||
is_video: true,
|
||||
track_nr: None,
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -506,7 +506,7 @@ Paginator(
|
|||
artist_id: Some("UCwPKPUAWE8ah0lkOcvNh8_Q"),
|
||||
album: None,
|
||||
view_count: Some(258000000),
|
||||
track_type: video,
|
||||
is_video: true,
|
||||
track_nr: None,
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -540,7 +540,7 @@ Paginator(
|
|||
artist_id: Some("UCWT2ZfW7d8YI-HinHEVhyCA"),
|
||||
album: None,
|
||||
view_count: Some(181000000),
|
||||
track_type: video,
|
||||
is_video: true,
|
||||
track_nr: None,
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -574,7 +574,7 @@ Paginator(
|
|||
artist_id: Some("UCjqYTQjO-JG-8vLlt6-4iyQ"),
|
||||
album: None,
|
||||
view_count: Some(165000000),
|
||||
track_type: video,
|
||||
is_video: true,
|
||||
track_nr: None,
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -608,7 +608,7 @@ Paginator(
|
|||
artist_id: Some("UCEdZAdnnKqbaHOlv8nM6OtA"),
|
||||
album: None,
|
||||
view_count: Some(108000000),
|
||||
track_type: video,
|
||||
is_video: true,
|
||||
track_nr: None,
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -642,7 +642,7 @@ Paginator(
|
|||
artist_id: Some("UCkbbMCA40i18i7UdjayMPAg"),
|
||||
album: None,
|
||||
view_count: Some(222000000),
|
||||
track_type: video,
|
||||
is_video: true,
|
||||
track_nr: None,
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -676,7 +676,7 @@ Paginator(
|
|||
artist_id: Some("UCEUX9tUYqTFfPQdAgVNsKTA"),
|
||||
album: None,
|
||||
view_count: Some(540000000),
|
||||
track_type: video,
|
||||
is_video: true,
|
||||
track_nr: None,
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -710,7 +710,7 @@ Paginator(
|
|||
artist_id: Some("UCG81UKNsFg9Perf0uPQOsQw"),
|
||||
album: None,
|
||||
view_count: Some(90000000),
|
||||
track_type: video,
|
||||
is_video: true,
|
||||
track_nr: None,
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -744,7 +744,7 @@ Paginator(
|
|||
artist_id: Some("UCEdZAdnnKqbaHOlv8nM6OtA"),
|
||||
album: None,
|
||||
view_count: Some(90000000),
|
||||
track_type: video,
|
||||
is_video: true,
|
||||
track_nr: None,
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -778,7 +778,7 @@ Paginator(
|
|||
artist_id: Some("UCDdCbqagfKo_euzzCV9G2EQ"),
|
||||
album: None,
|
||||
view_count: Some(71000000),
|
||||
track_type: video,
|
||||
is_video: true,
|
||||
track_nr: None,
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -807,7 +807,7 @@ Paginator(
|
|||
artist_id: Some("UCTP45_DE3fMLujU8sZ-MBzw"),
|
||||
album: None,
|
||||
view_count: Some(208000000),
|
||||
track_type: video,
|
||||
is_video: true,
|
||||
track_nr: None,
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -841,7 +841,7 @@ Paginator(
|
|||
artist_id: Some("UCDnYJA3OXXhRKYPe3jzLGeQ"),
|
||||
album: None,
|
||||
view_count: Some(140000000),
|
||||
track_type: video,
|
||||
is_video: true,
|
||||
track_nr: None,
|
||||
by_va: false,
|
||||
),
|
||||
|
|
|
@ -53,7 +53,7 @@ Paginator(
|
|||
name: "LOVE DIVE (LOVE DIVE)",
|
||||
)),
|
||||
view_count: None,
|
||||
track_type: track,
|
||||
is_video: false,
|
||||
track_nr: None,
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -105,7 +105,7 @@ Paginator(
|
|||
name: "My Voice - The 1st Album",
|
||||
)),
|
||||
view_count: None,
|
||||
track_type: track,
|
||||
is_video: false,
|
||||
track_nr: None,
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -157,7 +157,7 @@ Paginator(
|
|||
name: "FOREVER 1 - The 7th Album",
|
||||
)),
|
||||
view_count: None,
|
||||
track_type: track,
|
||||
is_video: false,
|
||||
track_nr: None,
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -209,7 +209,7 @@ Paginator(
|
|||
name: "\'The ReVe Festival 2022 - Feel My Rhythm\'",
|
||||
)),
|
||||
view_count: None,
|
||||
track_type: track,
|
||||
is_video: false,
|
||||
track_nr: None,
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -261,7 +261,7 @@ Paginator(
|
|||
name: "NewJeans 1st EP \'New Jeans\'",
|
||||
)),
|
||||
view_count: None,
|
||||
track_type: track,
|
||||
is_video: false,
|
||||
track_nr: None,
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -313,7 +313,7 @@ Paginator(
|
|||
name: "IU 5th Album \'LILAC\' (IU 5th Album \'LILAC\')",
|
||||
)),
|
||||
view_count: None,
|
||||
track_type: track,
|
||||
is_video: false,
|
||||
track_nr: None,
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -365,7 +365,7 @@ Paginator(
|
|||
name: "2021 Winter SMTOWN : SMCU EXPRESS",
|
||||
)),
|
||||
view_count: None,
|
||||
track_type: track,
|
||||
is_video: false,
|
||||
track_nr: None,
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -417,7 +417,7 @@ Paginator(
|
|||
name: "Dear OHMYGIRL",
|
||||
)),
|
||||
view_count: None,
|
||||
track_type: track,
|
||||
is_video: false,
|
||||
track_nr: None,
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -469,7 +469,7 @@ Paginator(
|
|||
name: "I Love",
|
||||
)),
|
||||
view_count: None,
|
||||
track_type: track,
|
||||
is_video: false,
|
||||
track_nr: None,
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -521,7 +521,7 @@ Paginator(
|
|||
name: "BORN PINK",
|
||||
)),
|
||||
view_count: None,
|
||||
track_type: track,
|
||||
is_video: false,
|
||||
track_nr: None,
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -573,7 +573,7 @@ Paginator(
|
|||
name: "YOUNG-LUV.COM (YOUNG-LUV.COM)",
|
||||
)),
|
||||
view_count: None,
|
||||
track_type: track,
|
||||
is_video: false,
|
||||
track_nr: None,
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -625,7 +625,7 @@ Paginator(
|
|||
name: "ELEVEN (ELEVEN)",
|
||||
)),
|
||||
view_count: None,
|
||||
track_type: track,
|
||||
is_video: false,
|
||||
track_nr: None,
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -677,7 +677,7 @@ Paginator(
|
|||
name: "Weekend",
|
||||
)),
|
||||
view_count: None,
|
||||
track_type: track,
|
||||
is_video: false,
|
||||
track_nr: None,
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -729,7 +729,7 @@ Paginator(
|
|||
name: "Offset",
|
||||
)),
|
||||
view_count: None,
|
||||
track_type: track,
|
||||
is_video: false,
|
||||
track_nr: None,
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -785,7 +785,7 @@ Paginator(
|
|||
name: "Scared to Be Lonely",
|
||||
)),
|
||||
view_count: None,
|
||||
track_type: track,
|
||||
is_video: false,
|
||||
track_nr: None,
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -837,7 +837,7 @@ Paginator(
|
|||
name: "After LIKE",
|
||||
)),
|
||||
view_count: None,
|
||||
track_type: track,
|
||||
is_video: false,
|
||||
track_nr: None,
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -889,7 +889,7 @@ Paginator(
|
|||
name: "Purpose - The 2nd Album",
|
||||
)),
|
||||
view_count: None,
|
||||
track_type: track,
|
||||
is_video: false,
|
||||
track_nr: None,
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -941,7 +941,7 @@ Paginator(
|
|||
name: "Heart Burn (열이올라요 (Heart Burn))",
|
||||
)),
|
||||
view_count: None,
|
||||
track_type: track,
|
||||
is_video: false,
|
||||
track_nr: None,
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -993,7 +993,7 @@ Paginator(
|
|||
name: "Rollin\'",
|
||||
)),
|
||||
view_count: None,
|
||||
track_type: track,
|
||||
is_video: false,
|
||||
track_nr: None,
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -1045,7 +1045,7 @@ Paginator(
|
|||
name: "The ReVe Festival: Finale",
|
||||
)),
|
||||
view_count: None,
|
||||
track_type: track,
|
||||
is_video: false,
|
||||
track_nr: None,
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -1097,7 +1097,7 @@ Paginator(
|
|||
name: "Every letter I sent you.",
|
||||
)),
|
||||
view_count: None,
|
||||
track_type: track,
|
||||
is_video: false,
|
||||
track_nr: None,
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -1149,7 +1149,7 @@ Paginator(
|
|||
name: "Stronger (Deluxe Version)",
|
||||
)),
|
||||
view_count: None,
|
||||
track_type: track,
|
||||
is_video: false,
|
||||
track_nr: None,
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -1201,7 +1201,7 @@ Paginator(
|
|||
name: "FATE NUMBER FOR",
|
||||
)),
|
||||
view_count: None,
|
||||
track_type: track,
|
||||
is_video: false,
|
||||
track_nr: None,
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -1253,7 +1253,7 @@ Paginator(
|
|||
name: "ANTIFRAGILE",
|
||||
)),
|
||||
view_count: None,
|
||||
track_type: track,
|
||||
is_video: false,
|
||||
track_nr: None,
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -1305,7 +1305,7 @@ Paginator(
|
|||
name: "소녀시대 Girls\' Generation",
|
||||
)),
|
||||
view_count: None,
|
||||
track_type: track,
|
||||
is_video: false,
|
||||
track_nr: None,
|
||||
by_va: false,
|
||||
),
|
||||
|
|
|
@ -32,7 +32,7 @@ MusicRelated(
|
|||
name: "FOREVER 1 - The 7th Album",
|
||||
)),
|
||||
view_count: None,
|
||||
track_type: track,
|
||||
is_video: false,
|
||||
track_nr: None,
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -64,7 +64,7 @@ MusicRelated(
|
|||
name: "After LIKE",
|
||||
)),
|
||||
view_count: None,
|
||||
track_type: track,
|
||||
is_video: false,
|
||||
track_nr: None,
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -96,7 +96,7 @@ MusicRelated(
|
|||
name: "Windy",
|
||||
)),
|
||||
view_count: None,
|
||||
track_type: track,
|
||||
is_video: false,
|
||||
track_nr: None,
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -128,7 +128,7 @@ MusicRelated(
|
|||
name: "Girls - The 2nd Mini Album",
|
||||
)),
|
||||
view_count: None,
|
||||
track_type: track,
|
||||
is_video: false,
|
||||
track_nr: None,
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -160,7 +160,7 @@ MusicRelated(
|
|||
name: "Weekend",
|
||||
)),
|
||||
view_count: None,
|
||||
track_type: track,
|
||||
is_video: false,
|
||||
track_nr: None,
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -192,7 +192,7 @@ MusicRelated(
|
|||
name: "Hello",
|
||||
)),
|
||||
view_count: None,
|
||||
track_type: track,
|
||||
is_video: false,
|
||||
track_nr: None,
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -224,7 +224,7 @@ MusicRelated(
|
|||
name: "Girls - The 2nd Mini Album",
|
||||
)),
|
||||
view_count: None,
|
||||
track_type: track,
|
||||
is_video: false,
|
||||
track_nr: None,
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -256,7 +256,7 @@ MusicRelated(
|
|||
name: "IT\'z Different",
|
||||
)),
|
||||
view_count: None,
|
||||
track_type: track,
|
||||
is_video: false,
|
||||
track_nr: None,
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -288,7 +288,7 @@ MusicRelated(
|
|||
name: "LOVE DIVE (LOVE DIVE)",
|
||||
)),
|
||||
view_count: None,
|
||||
track_type: track,
|
||||
is_video: false,
|
||||
track_nr: None,
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -320,7 +320,7 @@ MusicRelated(
|
|||
name: "Vanilla",
|
||||
)),
|
||||
view_count: None,
|
||||
track_type: track,
|
||||
is_video: false,
|
||||
track_nr: None,
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -352,7 +352,7 @@ MusicRelated(
|
|||
name: "Savage - The 1st Mini Album",
|
||||
)),
|
||||
view_count: None,
|
||||
track_type: track,
|
||||
is_video: false,
|
||||
track_nr: None,
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -384,7 +384,7 @@ MusicRelated(
|
|||
name: "\'The ReVe Festival 2022 - Feel My Rhythm\'",
|
||||
)),
|
||||
view_count: None,
|
||||
track_type: track,
|
||||
is_video: false,
|
||||
track_nr: None,
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -416,7 +416,7 @@ MusicRelated(
|
|||
name: "I NEVER DIE",
|
||||
)),
|
||||
view_count: None,
|
||||
track_type: track,
|
||||
is_video: false,
|
||||
track_nr: None,
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -448,7 +448,7 @@ MusicRelated(
|
|||
name: "INVU - The 3rd Album",
|
||||
)),
|
||||
view_count: None,
|
||||
track_type: track,
|
||||
is_video: false,
|
||||
track_nr: None,
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -480,7 +480,7 @@ MusicRelated(
|
|||
name: "CHECKMATE",
|
||||
)),
|
||||
view_count: None,
|
||||
track_type: track,
|
||||
is_video: false,
|
||||
track_nr: None,
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -512,7 +512,7 @@ MusicRelated(
|
|||
name: "Girls - The 2nd Mini Album",
|
||||
)),
|
||||
view_count: None,
|
||||
track_type: track,
|
||||
is_video: false,
|
||||
track_nr: None,
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -544,7 +544,7 @@ MusicRelated(
|
|||
name: "Street Dance Girls Fighter (SGF) Special",
|
||||
)),
|
||||
view_count: None,
|
||||
track_type: track,
|
||||
is_video: false,
|
||||
track_nr: None,
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -576,7 +576,7 @@ MusicRelated(
|
|||
name: "Next Level",
|
||||
)),
|
||||
view_count: None,
|
||||
track_type: track,
|
||||
is_video: false,
|
||||
track_nr: None,
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -608,7 +608,7 @@ MusicRelated(
|
|||
name: "IT\'z ICY",
|
||||
)),
|
||||
view_count: None,
|
||||
track_type: track,
|
||||
is_video: false,
|
||||
track_nr: None,
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -640,7 +640,7 @@ MusicRelated(
|
|||
name: "1/6 (6분의1)",
|
||||
)),
|
||||
view_count: None,
|
||||
track_type: track,
|
||||
is_video: false,
|
||||
track_nr: None,
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -666,7 +666,7 @@ MusicRelated(
|
|||
artist_id: Some("UCEdZAdnnKqbaHOlv8nM6OtA"),
|
||||
album: None,
|
||||
view_count: Some(35000000),
|
||||
track_type: video,
|
||||
is_video: true,
|
||||
track_nr: None,
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -690,7 +690,7 @@ MusicRelated(
|
|||
artist_id: Some("UCx5Dw_5guQcKu_lMGCh-IuQ"),
|
||||
album: None,
|
||||
view_count: Some(836000),
|
||||
track_type: video,
|
||||
is_video: true,
|
||||
track_nr: None,
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -714,7 +714,7 @@ MusicRelated(
|
|||
artist_id: Some("UCrGYENbzwtva2X16bAPhTbA"),
|
||||
album: None,
|
||||
view_count: Some(1200000),
|
||||
track_type: video,
|
||||
is_video: true,
|
||||
track_nr: None,
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -738,7 +738,7 @@ MusicRelated(
|
|||
artist_id: Some("UCC3bq4PHj5W5y47jdRjOCPA"),
|
||||
album: None,
|
||||
view_count: Some(987000),
|
||||
track_type: video,
|
||||
is_video: true,
|
||||
track_nr: None,
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -766,7 +766,7 @@ MusicRelated(
|
|||
),
|
||||
],
|
||||
artist_id: Some("UCEdZAdnnKqbaHOlv8nM6OtA"),
|
||||
album_type: album,
|
||||
album_type: Album,
|
||||
year: Some(2022),
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -792,7 +792,7 @@ MusicRelated(
|
|||
),
|
||||
],
|
||||
artist_id: Some("UCEdZAdnnKqbaHOlv8nM6OtA"),
|
||||
album_type: ep,
|
||||
album_type: Ep,
|
||||
year: Some(2021),
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -991,7 +991,6 @@ MusicRelated(
|
|||
)),
|
||||
track_count: None,
|
||||
from_ytm: false,
|
||||
is_podcast: false,
|
||||
),
|
||||
MusicPlaylistItem(
|
||||
id: "PLXE743St3DmXcUceLu--0-1k2FP2EocOk",
|
||||
|
@ -1014,7 +1013,6 @@ MusicRelated(
|
|||
)),
|
||||
track_count: None,
|
||||
from_ytm: false,
|
||||
is_podcast: false,
|
||||
),
|
||||
MusicPlaylistItem(
|
||||
id: "PLrppmyF0pfrfcoUjEygOB3sJpLk7envYZ",
|
||||
|
@ -1037,7 +1035,6 @@ MusicRelated(
|
|||
)),
|
||||
track_count: None,
|
||||
from_ytm: false,
|
||||
is_podcast: false,
|
||||
),
|
||||
MusicPlaylistItem(
|
||||
id: "PLpwgyaUVRzlLwAwXFWUCtIQJgbMS2k5fG",
|
||||
|
@ -1060,7 +1057,6 @@ MusicRelated(
|
|||
)),
|
||||
track_count: None,
|
||||
from_ytm: false,
|
||||
is_podcast: false,
|
||||
),
|
||||
MusicPlaylistItem(
|
||||
id: "PLPhP3bI_bdf1KY5-iN6trq-1XB4AQoZij",
|
||||
|
@ -1083,7 +1079,6 @@ MusicRelated(
|
|||
)),
|
||||
track_count: None,
|
||||
from_ytm: false,
|
||||
is_podcast: false,
|
||||
),
|
||||
MusicPlaylistItem(
|
||||
id: "PLjVRwhW9AxIDrdwuZqGfC_gjmFNfDfXqm",
|
||||
|
@ -1106,7 +1101,6 @@ MusicRelated(
|
|||
)),
|
||||
track_count: None,
|
||||
from_ytm: false,
|
||||
is_podcast: false,
|
||||
),
|
||||
MusicPlaylistItem(
|
||||
id: "PLhBJuM3nUmMEZSJaKFmjA7Y5z-PBzMO0o",
|
||||
|
@ -1129,7 +1123,6 @@ MusicRelated(
|
|||
)),
|
||||
track_count: None,
|
||||
from_ytm: false,
|
||||
is_podcast: false,
|
||||
),
|
||||
MusicPlaylistItem(
|
||||
id: "PL0Ne18oW010y_gRCR_57arzpFiP9gnVEi",
|
||||
|
@ -1152,7 +1145,6 @@ MusicRelated(
|
|||
)),
|
||||
track_count: None,
|
||||
from_ytm: false,
|
||||
is_podcast: false,
|
||||
),
|
||||
MusicPlaylistItem(
|
||||
id: "PLSNAUEM08rvKpvgkWSThc7PP7R9GJ8WdJ",
|
||||
|
@ -1175,7 +1167,6 @@ MusicRelated(
|
|||
)),
|
||||
track_count: None,
|
||||
from_ytm: false,
|
||||
is_podcast: false,
|
||||
),
|
||||
MusicPlaylistItem(
|
||||
id: "PLmOj3ylRt-xido1Feaf3O5HFXSKKeBuRR",
|
||||
|
@ -1198,7 +1189,6 @@ MusicRelated(
|
|||
)),
|
||||
track_count: None,
|
||||
from_ytm: false,
|
||||
is_podcast: false,
|
||||
),
|
||||
],
|
||||
)
|
||||
|
|
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue