Compare commits

..

1 commit

Author SHA1 Message Date
9743b3f9dc
feat: add http3 support
All checks were successful
CI / Test (push) Successful in 4m40s
2024-03-16 19:59:58 +01:00
293 changed files with 59036 additions and 442896 deletions

View file

@ -1,68 +0,0 @@
name: CI
on:
push:
branches: ["main"]
pull_request:
jobs:
Test:
runs-on: cimaster-latest
services:
warpproxy:
image: thetadev256/warpproxy
env:
WARP_DEVICE_ID: ${{ secrets.WARP_DEVICE_ID }}
WARP_ACCESS_TOKEN: ${{ secrets.WARP_ACCESS_TOKEN }}
WARP_LICENSE_KEY: ${{ secrets.WARP_LICENSE_KEY }}
WARP_PRIVATE_KEY: ${{ secrets.WARP_PRIVATE_KEY }}
steps:
- name: 📦 Checkout repository
uses: actions/checkout@v4
- name: 🦀 Setup Rust cache
uses: https://github.com/Swatinem/rust-cache@v2
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
- name: 🧪 Test
run: cargo nextest run --config-file ~/.config/nextest.toml --profile ci --retries 2 --features rss,userdata --workspace -- --skip 'user_data::'
env:
ALL_PROXY: "http://warpproxy:8124"
- name: Move test report
if: always()
run: mv target/nextest/ci/junit.xml junit.xml || true
- name: 💌 Upload test report
if: always()
uses: https://code.forgejo.org/forgejo/upload-artifact@v4
with:
name: test
path: |
junit.xml
rustypipe_reports
- name: 🔗 Artifactview PR comment
if: ${{ always() && github.event_name == 'pull_request' }}
run: |
if [[ "$GITEA_ACTIONS" == "true" ]]; then RUN_NUMBER="$GITHUB_RUN_NUMBER"; else RUN_NUMBER="$GITHUB_RUN_ID"; fi
curl -SsL --fail-with-body -w "\n" -X POST https://av.thetadev.de/.well-known/api/prComment -H "Content-Type: application/json" \
--data '{"url": "'"$GITHUB_SERVER_URL/$GITHUB_REPOSITORY/actions/runs/$RUN_NUMBER"'", "pr": ${{ github.event.number }}, "artifact_titles": {"test":"🧪 Test report"}, "artifact_paths": {"test":"/junit.xml?viewer=1"}}'

View file

@ -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/*

View file

@ -1,34 +0,0 @@
name: Release
on:
push:
tags:
- "*/v*.*.*"
jobs:
Release:
runs-on: cimaster-latest
steps:
- name: 📦 Checkout repository
uses: actions/checkout@v4
- name: Get variables
run: |
CRATE=$(echo '${{ github.ref_name }}' | awk 'BEGIN{RS="/"} NR==1{print}')
echo "CRATE=$CRATE" >> "$GITHUB_ENV"
echo "CRATE_VERSION=$(echo '${{ github.ref_name }}' | awk 'BEGIN{RS="/"} NR==2{print}')" >> "$GITHUB_ENV"
CL_PATH="CHANGELOG.md"
if [[ "$CRATE" != "rustypipe" ]]; then pfx="rustypipe-"; CL_PATH="${CRATE#"$pfx"}/$CL_PATH"; fi
{
echo 'CHANGELOG<<END_OF_FILE'
awk 'BEGIN{RS="(^|\n)## [^\n]+\n*"} NR==2 { print }' "$CL_PATH"
echo END_OF_FILE
} >> "$GITHUB_ENV"
- name: 📤 Publish crate on crates.io
run: cargo publish --token ${{ secrets.CARGO_TOKEN }} --package "${{ env.CRATE }}"
- name: 🎉 Publish release
uses: https://gitea.com/actions/release-action@main
with:
title: "${{ env.CRATE }} ${{ env.CRATE_VERSION }}"
body: "${{ env.CHANGELOG }}"

View file

@ -1,63 +0,0 @@
name: renovate
on:
push:
branches: ["main"]
paths:
- ".forgejo/workflows/renovate.yaml"
- "renovate.json"
schedule:
- cron: "0 0 * * *"
workflow_dispatch:
env:
RENOVATE_REPOSITORIES: ${{ github.repository }}
jobs:
renovate:
runs-on: docker
container:
image: renovate/renovate:39
steps:
- name: Load renovate repo cache
uses: actions/cache/restore@v4
with:
path: |
.tmp/cache/renovate/repository
.tmp/cache/renovate/renovate-cache-sqlite
.tmp/osv
key: repo-cache-${{ github.run_id }}
restore-keys: |
repo-cache-
- name: Run renovate
run: renovate
env:
LOG_LEVEL: debug
RENOVATE_BASE_DIR: ${{ github.workspace }}/.tmp
RENOVATE_ENDPOINT: ${{ github.server_url }}
RENOVATE_PLATFORM: gitea
RENOVATE_REPOSITORY_CACHE: 'enabled'
RENOVATE_TOKEN: ${{ secrets.FORGEJO_CI_BOT_TOKEN }}
GITHUB_COM_TOKEN: ${{ secrets.GH_PUBLIC_TOKEN }}
RENOVATE_GIT_AUTHOR: 'Renovate Bot <forgejo-renovate-action@forgejo.org>'
RENOVATE_X_SQLITE_PACKAGE_CACHE: true
GIT_AUTHOR_NAME: 'Renovate Bot'
GIT_AUTHOR_EMAIL: 'forgejo-renovate-action@forgejo.org'
GIT_COMMITTER_NAME: 'Renovate Bot'
GIT_COMMITTER_EMAIL: 'forgejo-renovate-action@forgejo.org'
OSV_OFFLINE_ROOT_DIR: ${{ github.workspace }}/.tmp/osv
- name: Save renovate repo cache
if: always() && env.RENOVATE_DRY_RUN != 'full'
uses: actions/cache/save@v4
with:
path: |
.tmp/cache/renovate/repository
.tmp/cache/renovate/renovate-cache-sqlite
.tmp/osv
key: repo-cache-${{ github.run_id }}

19
.gitea/workflows/ci.yaml Normal file
View file

@ -0,0 +1,19 @@
name: CI
on: [push, pull_request]
jobs:
Test:
runs-on: cimaster-latest
steps:
- name: 📦 Checkout repository
uses: actions/checkout@v3
- name: 🦀 Setup Rust cache
uses: https://github.com/Swatinem/rust-cache@v2
with:
cache-on-failure: "true"
- name: 📎 Clippy
run: cargo clippy --all --features=rss -- -D warnings
- name: 🧪 Test
run: cargo test --features=rss --workspace

3
.gitignore vendored
View file

@ -4,5 +4,4 @@
*.snap.new
rustypipe_reports
rustypipe_cache*.json
bg_snapshot.bin
rustypipe_cache.json

View file

@ -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", "--", "-D", "warnings"]

10
.woodpecker.yml Normal file
View 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

View file

@ -1,396 +0,0 @@
# Changelog
All notable changes to this project will be documented in this file.
## [v0.11.4](https://codeberg.org/ThetaDev/rustypipe/compare/rustypipe/v0.11.3..rustypipe/v0.11.4) - 2025-04-23
### 🚀 Features
- Player: handle VPN ban and captcha required error messages - ([be6da5e](https://codeberg.org/ThetaDev/rustypipe/commit/be6da5e7e3558ef39773bf45bcb8afbf006bacec))
### 🐛 Bug Fixes
- Deobfuscator: handle 1-char long global variables, find nsig fn (player 6450230e) - ([d675987](https://codeberg.org/ThetaDev/rustypipe/commit/d675987654972c6aa4cc2b291d25bc49fa60173e))
## [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
- Add mobile client - ([71d3ec6](https://codeberg.org/ThetaDev/rustypipe/commit/71d3ec65ddafa966ef6b41cf4eb71687ba4b594c))
### 🐛 Bug Fixes
- Prioritize visitor_data argument before opts - ([ace0fae](https://codeberg.org/ThetaDev/rustypipe/commit/ace0fae1005217cd396000176e7c01682eae026f))
- Ignore live tracks in YTM searches - ([f3f2e1d](https://codeberg.org/ThetaDev/rustypipe/commit/f3f2e1d3ca1e9c838c682356bb5a7ded6951c8e5))
- A/B test 16 (pageHeaderRenderer on playlist pages) - ([e65f145](https://codeberg.org/ThetaDev/rustypipe/commit/e65f14556f3003fa59fee3f9f1410fb5ddf63219))
### ⚙️ Miscellaneous Tasks
- *(deps)* Update rust crate rstest to 0.23.0 (#12) - ([96776e9](https://codeberg.org/ThetaDev/rustypipe/commit/96776e98d76fa1d31d5f84dbceafbe8f9dfd9085))
## [v0.4.0](https://codeberg.org/ThetaDev/rustypipe/compare/rustypipe/v0.3.0..rustypipe/v0.4.0) - 2024-09-10
### 🚀 Features
- Add RustyPipe version constant - ([7a019f5](https://codeberg.org/ThetaDev/rustypipe/commit/7a019f5706e19f7fe9f2e16e3b94d7b98cc8aca9))
### 🐛 Bug Fixes
- Show docs.rs feature flags - ([67a231d](https://codeberg.org/ThetaDev/rustypipe/commit/67a231d6d1b6427f500667729a59032f2b28cc65))
- A/B test 15 (parsing channel shortsLockupViewModel) - ([7972df0](https://codeberg.org/ThetaDev/rustypipe/commit/7972df0df498edd7801e25037b9b2456367f9204))
### 📚 Documentation
- Fix license badge URL - ([4a253e1](https://codeberg.org/ThetaDev/rustypipe/commit/4a253e1a47317e9999e6ad31ac5c411956a0986a))
### ⚙️ Miscellaneous Tasks
- *(deps)* Update rust crate tokio to 1.20.4 [security] (#10) - ([a445e51](https://codeberg.org/ThetaDev/rustypipe/commit/a445e51b54a9afc44cd9657260a0b3d2abddbfa6))
## [v0.3.0](https://codeberg.org/ThetaDev/rustypipe/compare/rustypipe/v0.2.1..rustypipe/v0.3.0) - 2024-08-18
### 🚀 Features
- Add client_type to VideoPlayer, simplify MapResponse trait - ([90540c6](https://codeberg.org/ThetaDev/rustypipe/commit/90540c6aaad658d4ce24ed41450d8509bac711bd))
- Add http_client method to RustyPipe and user_agent method to RustyPipeQuery - ([3d6de53](https://codeberg.org/ThetaDev/rustypipe/commit/3d6de5354599ea691351e0ca161154e53f2e0b41))
- Add channel_id and channel_name getters to YtEntity trait - ([bbbe9b4](https://codeberg.org/ThetaDev/rustypipe/commit/bbbe9b4b322c6b5b30764772e282c6823aeea524))
- [**breaking**] Make StreamFilter use Vec internally, remove lifetime - ([821984b](https://codeberg.org/ThetaDev/rustypipe/commit/821984bbd51d65cf96b1d14087417ef968eaf9b2))
- Overhauled downloader - ([11a0038](https://codeberg.org/ThetaDev/rustypipe/commit/11a00383502917cd98245c3da349107289ba3aa9))
- Add player_from_clients function to specify client order - ([72b5dfe](https://codeberg.org/ThetaDev/rustypipe/commit/72b5dfec69ec25445b94cb0976662416a5df56ef))
- [**breaking**] Add TV client - ([e608811](https://codeberg.org/ThetaDev/rustypipe/commit/e608811e5f5615416241e67561671330097092cb))
- Downloader: add audio tagging - ([1e1315a](https://codeberg.org/ThetaDev/rustypipe/commit/1e1315a8378bd0ad25b5f1614e83dabc4a0b40d5))
- Add audiotag+indicatif features to downloader - ([97fb057](https://codeberg.org/ThetaDev/rustypipe/commit/97fb0578b5c4954a596d8dee0c4b6e1d773a9300))
- Add YtEntity trait to YouTubeItem and MusicItem - ([114a86a](https://codeberg.org/ThetaDev/rustypipe/commit/114a86a3823a175875aa2aeb31a61a6799ef13bc))
- Change default player client order - ([97904d7](https://codeberg.org/ThetaDev/rustypipe/commit/97904d77374c2c937a49dc7905759c2d8e8ef9ae))
- [**breaking**] Update channel model, addd handle + video_count, remove tv/mobile banner - ([e671570](https://codeberg.org/ThetaDev/rustypipe/commit/e6715700d950912031d5fbc1263f8770b6ffc49c))
- [**breaking**] Add handle to ChannelItem, remove video_count - ([1cffb27](https://codeberg.org/ThetaDev/rustypipe/commit/1cffb27cc0b64929f9627f5839df2d73b81988a4))
- [**breaking**] Remove startpage - ([3599aca](https://codeberg.org/ThetaDev/rustypipe/commit/3599acafef1a21fa6f8dea97902eb4a3fb048c14))
### 🐛 Bug Fixes
- [**breaking**] Extracting nsig function, remove field `throttled` from Video/Audio stream model - ([dd0565b](https://codeberg.org/ThetaDev/rustypipe/commit/dd0565ba98acb3289ed220fd2a3aaf86bb8b0788))
- Make nsig_fn regex more generic - ([fb7af3b](https://codeberg.org/ThetaDev/rustypipe/commit/fb7af3b96698b452b6b24d1e094ba13a245cb83c))
- Improve deobfuscator (support multiple nsig name matches, error if mapping all streams fails) - ([8152ce6](https://codeberg.org/ThetaDev/rustypipe/commit/8152ce6b088b57be9b8419b754aca93805e5f34d))
- Nsig fn extraction - ([3c83e11](https://codeberg.org/ThetaDev/rustypipe/commit/3c83e11e753f8eb6efea5d453a7c819c487b3464))
- Add var to deobf fn assignment - ([c6bd03f](https://codeberg.org/ThetaDev/rustypipe/commit/c6bd03fb70871ae1b764be18f88e86e71818fc56))
- Make Verification enum exhaustive - ([d053ac3](https://codeberg.org/ThetaDev/rustypipe/commit/d053ac3eba810a7241df91f2f50bcbe1fd968c86))
- Extraction error message - ([d36ba59](https://codeberg.org/ThetaDev/rustypipe/commit/d36ba595dab0bbaef1012ebfa8930fc0e6bf8167))
- Set tracing instrumentation level to Error - ([9da3b25](https://codeberg.org/ThetaDev/rustypipe/commit/9da3b25be2b2577f7bd0282c09d10d368ac8b73f))
- Detect ip-ban error message - ([da39c64](https://codeberg.org/ThetaDev/rustypipe/commit/da39c64f302bc2edc4214bbe25a0a9eb54063b09))
- Player_from_clients: fall back to TvHtml5Embed client - ([d0ae796](https://codeberg.org/ThetaDev/rustypipe/commit/d0ae7961ba91d56c8b9a8d1c545875e869b818f5))
- Parsing channels without banner - ([5a6b2c3](https://codeberg.org/ThetaDev/rustypipe/commit/5a6b2c3a621f6b20c1324ea8b9c03426e3d8018b))
- Get TV client version - ([ee3ae40](https://codeberg.org/ThetaDev/rustypipe/commit/ee3ae40395263c5989784c7e00038ff13bc1151a))
### ⚙️ Miscellaneous Tasks
- Renovate: disable approveMajorUpdates - ([4743f9d](https://codeberg.org/ThetaDev/rustypipe/commit/4743f9d8e101b58ad6a43548495da9f4f381b9f4))
- Renovate: disable scheduleDaily - ([015bd6f](https://codeberg.org/ThetaDev/rustypipe/commit/015bd6fcbf04163565fcb190b163ecfdb5664e11))
- Renovate: enable automerge - ([882abc5](https://codeberg.org/ThetaDev/rustypipe/commit/882abc53ca894229ee78ec0edaa723d9ea61bbcb))
- *(deps)* Update rust crate quick-xml to 0.36.0 (#8) - ([b6bc05c](https://codeberg.org/ThetaDev/rustypipe/commit/b6bc05c1f39da9a846b2e3d1d24bcbccb031203b))
- *(deps)* Update rust crate rstest to 0.22.0 (#9) - ([abb7832](https://codeberg.org/ThetaDev/rustypipe/commit/abb783219aba4b492c1dff03c2148acf1f51a55d))
- Change repo URL to Codeberg - ([1793331](https://codeberg.org/ThetaDev/rustypipe/commit/17933315d947f76d5fe1aa52abf7ea24c3ce6381))
- Adjust dependency versions - ([70c6f8c](https://codeberg.org/ThetaDev/rustypipe/commit/70c6f8c3b97baefd316fff90cc727524516657af))
### Todo
- Update metadata - ([8692ca8](https://codeberg.org/ThetaDev/rustypipe/commit/8692ca81d972d0d2acf6fb4da79b9e0f5ebf4daf))
## [v0.2.1](https://codeberg.org/ThetaDev/rustypipe/compare/rustypipe/v0.2.0..rustypipe/v0.2.1) - 2024-07-01
### 🐛 Bug Fixes
- *(deps)* Update quick-xml to v0.35.0 - ([298e4de](https://codeberg.org/ThetaDev/rustypipe/commit/298e4def93d1595fba91be103f014aa645a08937))
## [v0.2.0](https://codeberg.org/ThetaDev/rustypipe/compare/rustypipe/v0.1.3..rustypipe/v0.2.0) - 2024-06-27
### 🚀 Features
- Add text formatting (bold/italic/strikethrough) - ([b8825f9](https://codeberg.org/ThetaDev/rustypipe/commit/b8825f9199365c873a4f0edd98a435e986b8daa2))
- Prefix chip-style web links (social media) with the service name - ([6c41ef2](https://codeberg.org/ThetaDev/rustypipe/commit/6c41ef2fb2531e10a12c271e2d48504510a3b0bf))
- Make get_visitor_data() public - ([da1d1bd](https://codeberg.org/ThetaDev/rustypipe/commit/da1d1bd2a0b214da10436ae221c90a0f88697b9a))
- Add UnavailabilityReason: IpBan - ([401d4e8](https://codeberg.org/ThetaDev/rustypipe/commit/401d4e8255b1e86444319fed6d114dfbd0f80bbd))
- Add YtEntity trait - ([792e3b3](https://codeberg.org/ThetaDev/rustypipe/commit/792e3b31e0101087a167935baad39a2e3b4296d0))
### 🐛 Bug Fixes
- Remove Innertube API keys, update android player params - ([a8fb337](https://codeberg.org/ThetaDev/rustypipe/commit/a8fb337fae9cb0112e0152f9a0a19ebae49c2a4d))
- Parsing error when no `music_related` content available - ([8fbd6b9](https://codeberg.org/ThetaDev/rustypipe/commit/8fbd6b95b6f01108b46f53fe60a56b0c561e40c1))
- Parsing audiobook type in European Portuguese - ([041ce2d](https://codeberg.org/ThetaDev/rustypipe/commit/041ce2d08f6021c88e8890034f551f7e01b2f012))
- Renovate ci token - ([e0759eb](https://codeberg.org/ThetaDev/rustypipe/commit/e0759ebce32a5520245bb2c0cb920734b04ee7dc))
### 🚜 Refactor
- [**breaking**] Rename VideoItem/VideoPlayerDetails.length to duration for consistency - ([94e8d24](https://codeberg.org/ThetaDev/rustypipe/commit/94e8d24c6848b8bfca70dd03a7d89547ba9d6051))
### 📚 Documentation
- Add logo - ([6646078](https://codeberg.org/ThetaDev/rustypipe/commit/66460789449be0d5984cbdb6ec372e69323b7a88))
### ⚙️ Miscellaneous Tasks
- Changelog: fix incorrect version URLs - ([97b6f07](https://codeberg.org/ThetaDev/rustypipe/commit/97b6f07399e80e00a6c015d013e744568be125dd))
- Update rstest to v0.19.0 - ([50fd1f0](https://codeberg.org/ThetaDev/rustypipe/commit/50fd1f08caf39c1298654e06059cc393543e925b))
- Introduce MSRV - ([5dbb288](https://codeberg.org/ThetaDev/rustypipe/commit/5dbb288a496d53a299effa2026f5258af7b1f176))
- Fix clippy lints - ([45b9f2a](https://codeberg.org/ThetaDev/rustypipe/commit/45b9f2a627b4e7075ba0b1c5f16efcc19aef7922))
- Vscode: enable rss feature by default - ([e75ffbb](https://codeberg.org/ThetaDev/rustypipe/commit/e75ffbb5da6198086385ea96383ab9d0791592a5))
- Configure Renovate (#3) - ([44c2deb](https://codeberg.org/ThetaDev/rustypipe/commit/44c2debea61f70c24ad6d827987e85e2132ed3d1))
- *(deps)* Update rust crate tokio to 1.20.4 [security] (#4) - ([ce3ec34](https://codeberg.org/ThetaDev/rustypipe/commit/ce3ec34337b8acac41410ea39264aab7423d5801))
- *(deps)* Update rust crate quick-xml to 0.34.0 (#5) - ([1e8a1af](https://codeberg.org/ThetaDev/rustypipe/commit/1e8a1af08c873cee7feadf63c2eff62753a78f64))
- *(deps)* Update rust crate rstest to 0.21.0 (#7) - ([c3af918](https://codeberg.org/ThetaDev/rustypipe/commit/c3af918ba53c6230c0e4aef822a0cb2cf120bf3f))
## [v0.1.3](https://codeberg.org/ThetaDev/rustypipe/compare/rustypipe/v0.1.2..rustypipe/v0.1.3) - 2024-04-01
### 🐛 Bug Fixes
- Parse new comment model (A/B#14 frameworkUpdates) - ([b0331f7](https://codeberg.org/ThetaDev/rustypipe/commit/b0331f7250f5d7d61a45209150739d2cb08b4280))
### ◀️ Revert
- "fix: improve VecLogErr messages" (leads to infinite loop) - ([348c852](https://codeberg.org/ThetaDev/rustypipe/commit/348c8523fe847f2f6ce98317375a7ab65e778ed2))
## [v0.1.2](https://codeberg.org/ThetaDev/rustypipe/compare/rustypipe/v0.1.1..rustypipe/v0.1.2) - 2024-03-26
### 🐛 Bug Fixes
- Correctly parse subscriber count with new channel header - ([180dd98](https://codeberg.org/ThetaDev/rustypipe/commit/180dd9891a14b4da9f130a73d73aecc3822fce2f))
## [v0.1.1](https://codeberg.org/ThetaDev/rustypipe/compare/rustypipe/v0.1.0..rustypipe/v0.1.1) - 2024-03-26
### 🐛 Bug Fixes
- Specify internal dependency versions - ([6598a23](https://codeberg.org/ThetaDev/rustypipe/commit/6598a23d0699e6fe298275a67e0146a19c422c88))
- Move package attributes to workspace - ([e4b204e](https://codeberg.org/ThetaDev/rustypipe/commit/e4b204eae65f450471be0890b0198d2f30714b3b))
- Parsing music details with video description tab - ([a81c3e8](https://codeberg.org/ThetaDev/rustypipe/commit/a81c3e83366fdf72d01dd3ee00fb2e831f7aaa26))
### ⚙️ Miscellaneous Tasks
- Changes to release command - ([0bcced1](https://codeberg.org/ThetaDev/rustypipe/commit/0bcced1db377198a54c9c7d03b8d038125a2bfe4))
- Update user agent (FF 115.0) - ([be314d5](https://codeberg.org/ThetaDev/rustypipe/commit/be314d57ea1d99bfdc80649351ee3e7845541238))
- Fix release script (unquoted include paths) - ([78ba9cb](https://codeberg.org/ThetaDev/rustypipe/commit/78ba9cb34c6bba3aba177583b242d3f76ea9847d))
## [v0.1.0](https://codeberg.org/ThetaDev/rustypipe/commits/tag/rustypipe/v0.1.0) - 2024-03-22
Initial release
<!-- generated by git-cliff -->

View file

@ -1,90 +1,23 @@
[package]
name = "rustypipe"
version = "0.11.4"
rust-version = "1.67.1"
edition.workspace = true
authors.workspace = true
license.workspace = true
repository.workspace = true
keywords.workspace = true
categories.workspace = true
version = "0.1.0"
edition = "2021"
authors = ["ThetaDev <thetadev@magenta.de>"]
license = "GPL-3.0"
description = "Client for the public YouTube / YouTube Music API (Innertube), inspired by NewPipe"
repository = "https://code.thetadev.de/ThetaDev/rustypipe"
keywords = ["youtube", "video", "music"]
categories = ["api-bindings", "multimedia"]
include = ["/src", "README.md", "CHANGELOG.md", "LICENSE", "!snapshots"]
include = ["/src", "README.md", "LICENSE", "!snapshots"]
[workspace]
members = [".", "codegen", "downloader", "cli"]
[workspace.package]
edition = "2021"
authors = ["ThetaDev <thetadev@magenta.de>"]
license = "GPL-3.0"
repository = "https://codeberg.org/ThetaDev/rustypipe"
keywords = ["youtube", "video", "music"]
categories = ["api-bindings", "multimedia"]
[workspace.dependencies]
rquickjs = "0.9.0"
once_cell = "1.12.0"
regex = "1.6.0"
fancy-regex = "0.14.0"
thiserror = "2.0.0"
url = "2.2.0"
reqwest = { version = "0.12.0", default-features = false }
tokio = "1.20.4"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0.82"
serde_with = { version = "3.0.0", default-features = false, features = [
"alloc",
"macros",
] }
serde_plain = "1.0.0"
sha1 = "0.10.0"
rand = "0.9.0"
time = { version = "0.3.37", features = [
"macros",
"serde-human-readable",
"serde-well-known",
"local-offset",
] }
futures-util = "0.3.31"
ress = "0.11.0"
phf = "0.11.0"
phf_codegen = "0.11.0"
data-encoding = "2.0.0"
urlencoding = "2.1.0"
quick-xml = { version = "0.37.0", features = ["serialize"] }
tracing = { version = "0.1.0", features = ["log"] }
localzone = "0.3.1"
# CLI
indicatif = "0.17.0"
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"
filenamify = "0.1.0"
# Testing
rstest = "0.25.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.4", default-features = false }
rustypipe-downloader = { path = "./downloader", version = "0.3.1", default-features = false, features = [
"indicatif",
"audiotag",
] }
[features]
default = ["default-tls"]
default = ["rustls-tls-native-roots"]
rss = ["dep:quick-xml"]
userdata = []
rss = ["quick-xml"]
# Reqwest TLS options
default-tls = ["reqwest/default-tls"]
@ -94,39 +27,47 @@ native-tls-vendored = ["reqwest/native-tls-vendored"]
rustls-tls-webpki-roots = ["reqwest/rustls-tls-webpki-roots"]
rustls-tls-native-roots = ["reqwest/rustls-tls-native-roots"]
# Requires RUSTFLAGS='--cfg reqwest_unstable'
http3 = ["reqwest/http3"]
[dependencies]
rquickjs.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"] }
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
urlencoding.workspace = true
tracing.workspace = true
localzone.workspace = true
quick-xml = { workspace = true, optional = true }
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.13.0"
thiserror = "1.0.36"
url = "2.2.2"
reqwest = { version = "0.11.11", default-features = false, features = [
"json",
"gzip",
"brotli",
] }
tokio = { version = "1.20.0", features = ["macros", "time"] }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0.82"
serde_with = { version = "3.0.0", default-features = false, features = [
"alloc",
"macros",
] }
serde_plain = "1.0.1"
rand = "0.8.5"
time = { version = "0.3.15", features = [
"macros",
"serde-human-readable",
"serde-well-known",
] }
futures = "0.3.21"
ress = "0.11.4"
phf = "0.11.1"
base64 = "0.22.0"
urlencoding = "2.1.2"
quick-xml = { version = "0.31.0", features = ["serialize"], optional = true }
tracing = { version = "0.1.37", features = ["log"] }
[dev-dependencies]
rstest.workspace = true
tokio-test.workspace = true
insta.workspace = true
path_macro.workspace = true
tracing-test.workspace = true
[package.metadata.docs.rs]
# To build locally:
# RUSTDOCFLAGS="--cfg docsrs" cargo +nightly doc --features rss,userdata --no-deps --open
features = ["rss", "userdata"]
rustdoc-args = ["--cfg", "docsrs"]
rstest = "0.18.1"
tokio-test = "0.4.2"
insta = { version = "1.17.1", features = ["ron", "redactions"] }
path_macro = "1.0.0"

View file

@ -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`)

View file

@ -1,19 +1,14 @@
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
unittest:
cargo nextest run --features=rss,userdata --no-fail-fast --lib
cargo test --features=rss --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 test --features=rss --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 test --features=rss --test youtube
testintl:
#!/usr/bin/env bash
@ -32,8 +27,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 test --test youtube -- --test-threads 4 --skip resolve; then
echo "--- $YT_LANG COMPLETED ---"
else
echo "--- $YT_LANG FAILED ---"
@ -49,44 +43,3 @@ testfiles:
report2yaml:
mkdir -p rustypipe_reports/conv
for f in rustypipe_reports/*.json; do yq '.http_request.resp_body' $f | yq -o json -P > rustypipe_reports/conv/`basename $f .json`_body.json; yq e -Pi "del(.http_request.resp_body)" $f; mv $f rustypipe_reports/conv/`basename $f .json`.yaml; done;
release crate="rustypipe":
#!/usr/bin/env bash
set -e
CRATE="{{crate}}"
CHANGELOG="CHANGELOG.md"
if [ "$CRATE" = "rustypipe" ]; then
INCLUDES="--exclude-path 'notes/**' --exclude-path 'cli/**' --exclude-path 'downloader/**'"
else
if [ ! -d "$CRATE" ]; then
echo "$CRATE does not exist."; exit 1
fi
INCLUDES="--include-path README.md --include-path LICENSE --include-path Cargo.toml --include-path '$CRATE/**'"
CHANGELOG="$CRATE/$CHANGELOG"
CRATE="rustypipe-$CRATE" # Add crate name prefix
fi
VERSION=$(cargo pkgid --package "$CRATE" | tr '#@' '\n' | tail -n 1)
TAG="${CRATE}/v${VERSION}"
echo "Releasing $TAG:"
if git rev-parse "$TAG" >/dev/null 2>&1; then echo "version tag $TAG already exists"; exit 1; fi
CLIFF_ARGS="--tag '${TAG}' --tag-pattern '${CRATE}/v*' --unreleased $INCLUDES"
echo "git-cliff $CLIFF_ARGS"
if [ -f "$CHANGELOG" ]; then
eval "git-cliff $CLIFF_ARGS --prepend '$CHANGELOG'"
else
eval "git-cliff $CLIFF_ARGS --output '$CHANGELOG'"
fi
editor "$CHANGELOG"
git add .
git commit -m "chore(release): release $CRATE v$VERSION"
awk 'BEGIN{RS="(^|\n)## [^\n]+\n*"} NR==2 { print }' "$CHANGELOG" | git tag -as -F - --cleanup whitespace "$TAG"
echo "🚀 Run 'git push origin $TAG' to publish"

149
README.md
View file

@ -1,12 +1,9 @@
# ![RustyPipe](https://codeberg.org/ThetaDev/rustypipe/raw/branch/main/notes/logo.svg)
# RustyPipe
[![Current crates.io version](https://img.shields.io/crates/v/rustypipe.svg)](https://crates.io/crates/rustypipe)
[![License](https://img.shields.io/badge/License-GPL--3-blue.svg?style=flat)](https://opensource.org/licenses/GPL-3.0)
[![Docs](https://img.shields.io/docsrs/rustypipe/latest?style=flat)](https://docs.rs/rustypipe)
[![CI status](https://codeberg.org/ThetaDev/rustypipe/actions/workflows/ci.yaml/badge.svg?style=flat&label=CI)](https://codeberg.org/ThetaDev/rustypipe/actions/?workflow=ci.yaml)
[![CI status](https://ci.thetadev.de/api/badges/ThetaDev/rustypipe/status.svg)](https://ci.thetadev.de/ThetaDev/rustypipe)
RustyPipe is a fully featured Rust client for the public YouTube / YouTube Music API
(Innertube), inspired by [NewPipe](https://github.com/TeamNewPipe/NewPipeExtractor).
Client for the public YouTube / YouTube Music API (Innertube), inspired by
[NewPipe](https://github.com/TeamNewPipe/NewPipeExtractor).
## Features
@ -21,8 +18,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,35 +31,14 @@ 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
[dependencies]
rustypipe = "0.1.3"
rustypipe = "0.1.0"
tokio = { version = "1.20.0", features = ["macros", "rt-multi-thread"] }
```
@ -181,105 +155,28 @@ 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
- [`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`)

View file

@ -1,207 +0,0 @@
# Changelog
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
- Add mobile client - ([71d3ec6](https://codeberg.org/ThetaDev/rustypipe/commit/71d3ec65ddafa966ef6b41cf4eb71687ba4b594c))
### ⚙️ Miscellaneous Tasks
- *(deps)* Update rust crate rstest to 0.23.0 (#12) - ([96776e9](https://codeberg.org/ThetaDev/rustypipe/commit/96776e98d76fa1d31d5f84dbceafbe8f9dfd9085))
- *(deps)* Update rustypipe to 0.5.0
## [v0.2.1](https://codeberg.org/ThetaDev/rustypipe/compare/rustypipe-cli/v0.2.0..rustypipe-cli/v0.2.1) - 2024-09-10
### 🚀 Features
- Add RustyPipe version constant - ([7a019f5](https://codeberg.org/ThetaDev/rustypipe/commit/7a019f5706e19f7fe9f2e16e3b94d7b98cc8aca9))
### 📚 Documentation
- Fix license badge URL - ([4a253e1](https://codeberg.org/ThetaDev/rustypipe/commit/4a253e1a47317e9999e6ad31ac5c411956a0986a))
### ⚙️ Miscellaneous Tasks
- *(deps)* Update rust crate tokio to 1.20.4 [security] (#10) - ([a445e51](https://codeberg.org/ThetaDev/rustypipe/commit/a445e51b54a9afc44cd9657260a0b3d2abddbfa6))
## [v0.2.0](https://codeberg.org/ThetaDev/rustypipe/compare/rustypipe-cli/v0.1.1..rustypipe-cli/v0.2.0) - 2024-08-18
### 🚀 Features
- Overhauled downloader - ([11a0038](https://codeberg.org/ThetaDev/rustypipe/commit/11a00383502917cd98245c3da349107289ba3aa9))
- [**breaking**] Add TV client - ([e608811](https://codeberg.org/ThetaDev/rustypipe/commit/e608811e5f5615416241e67561671330097092cb))
- Downloader: add audio tagging - ([1e1315a](https://codeberg.org/ThetaDev/rustypipe/commit/1e1315a8378bd0ad25b5f1614e83dabc4a0b40d5))
- Downloader: add download_track fn, improve path templates - ([e1e4fb2](https://codeberg.org/ThetaDev/rustypipe/commit/e1e4fb29c190fec07f17c59ec88bef4f1c2a76a1))
- Add audiotag+indicatif features to downloader - ([97fb057](https://codeberg.org/ThetaDev/rustypipe/commit/97fb0578b5c4954a596d8dee0c4b6e1d773a9300))
- Add plaintext output to CLI - ([91b020e](https://codeberg.org/ThetaDev/rustypipe/commit/91b020efd498eff6e0f354a1de39439e252a79dd))
- Add potoken option to downloader - ([904f821](https://codeberg.org/ThetaDev/rustypipe/commit/904f8215d84c810b04e4d2134718e786a4803ad2))
- Print error message - ([8f16e5b](https://codeberg.org/ThetaDev/rustypipe/commit/8f16e5ba6eec3fd6aba1bb6a19571c65fb69ce0e))
- Add list of clients to downloader - ([5e646af](https://codeberg.org/ThetaDev/rustypipe/commit/5e646afd1edc6c0101501311527ea56d3bad5fd2))
- Retry with different client after 403 error - ([d875b54](https://codeberg.org/ThetaDev/rustypipe/commit/d875b5442de9822ba7ddc6f05789f56a8962808c))
- Add option to fetch RSS feed - ([03c4d3c](https://codeberg.org/ThetaDev/rustypipe/commit/03c4d3c392386e06f2673f0e0783e22d10087989))
- [**breaking**] Update channel model, addd handle + video_count, remove tv/mobile banner - ([e671570](https://codeberg.org/ThetaDev/rustypipe/commit/e6715700d950912031d5fbc1263f8770b6ffc49c))
### 🐛 Bug Fixes
- *(deps)* Update quick-xml to v0.35.0 - ([298e4de](https://codeberg.org/ThetaDev/rustypipe/commit/298e4def93d1595fba91be103f014aa645a08937))
- Improve deobfuscator (support multiple nsig name matches, error if mapping all streams fails) - ([8152ce6](https://codeberg.org/ThetaDev/rustypipe/commit/8152ce6b088b57be9b8419b754aca93805e5f34d))
- Cli: print video ID when logging errors - ([2c7a3fb](https://codeberg.org/ThetaDev/rustypipe/commit/2c7a3fb5cc153ff0b8b5e79234ae497d916e471c))
- Use anstream + owo-color for colorful CLI output - ([e8324cf](https://codeberg.org/ThetaDev/rustypipe/commit/e8324cf3b065cb977adbc9529b1ef5ee18c3dd47))
- Use native tls by default for CLI - ([f37432a](https://codeberg.org/ThetaDev/rustypipe/commit/f37432a48c1f93cab5f7942f791daf7b27cb1565))
- Detect ip-ban error message - ([da39c64](https://codeberg.org/ThetaDev/rustypipe/commit/da39c64f302bc2edc4214bbe25a0a9eb54063b09))
- Dont store cache in current dir with --report option - ([6009de7](https://codeberg.org/ThetaDev/rustypipe/commit/6009de7bddc6031f2af17005c473c17934327c02))
- Show docs.rs feature flags - ([67a231d](https://codeberg.org/ThetaDev/rustypipe/commit/67a231d6d1b6427f500667729a59032f2b28cc65))
### ⚙️ Miscellaneous Tasks
- *(deps)* Update rust crate quick-xml to 0.36.0 (#8) - ([b6bc05c](https://codeberg.org/ThetaDev/rustypipe/commit/b6bc05c1f39da9a846b2e3d1d24bcbccb031203b))
- *(deps)* Update rust crate rstest to 0.22.0 (#9) - ([abb7832](https://codeberg.org/ThetaDev/rustypipe/commit/abb783219aba4b492c1dff03c2148acf1f51a55d))
- Change repo URL to Codeberg - ([1793331](https://codeberg.org/ThetaDev/rustypipe/commit/17933315d947f76d5fe1aa52abf7ea24c3ce6381))
- Adjust dependency versions - ([70c6f8c](https://codeberg.org/ThetaDev/rustypipe/commit/70c6f8c3b97baefd316fff90cc727524516657af))
### Todo
- Update metadata - ([8692ca8](https://codeberg.org/ThetaDev/rustypipe/commit/8692ca81d972d0d2acf6fb4da79b9e0f5ebf4daf))
## [v0.1.1](https://codeberg.org/ThetaDev/rustypipe/compare/rustypipe-cli/v0.1.0..rustypipe-cli/v0.1.1) - 2024-06-27
### 🚀 Features
- CLI: setting player type - ([16e0e28](https://codeberg.org/ThetaDev/rustypipe/commit/16e0e28c4866bb69d8e4c06eef94176f329a1c27))
### 🐛 Bug Fixes
- Clippy warning - ([8420c2f](https://codeberg.org/ThetaDev/rustypipe/commit/8420c2f8dbd2791b524ceca2e19fb68e5b918bfa))
### 📚 Documentation
- Add logo - ([6646078](https://codeberg.org/ThetaDev/rustypipe/commit/66460789449be0d5984cbdb6ec372e69323b7a88))
### ⚙️ Miscellaneous Tasks
- Changelog: fix incorrect version URLs - ([97b6f07](https://codeberg.org/ThetaDev/rustypipe/commit/97b6f07399e80e00a6c015d013e744568be125dd))
- Update rstest to v0.19.0 - ([50fd1f0](https://codeberg.org/ThetaDev/rustypipe/commit/50fd1f08caf39c1298654e06059cc393543e925b))
- Introduce MSRV - ([5dbb288](https://codeberg.org/ThetaDev/rustypipe/commit/5dbb288a496d53a299effa2026f5258af7b1f176))
- Fix clippy lints - ([45b9f2a](https://codeberg.org/ThetaDev/rustypipe/commit/45b9f2a627b4e7075ba0b1c5f16efcc19aef7922))
- *(deps)* Update rust crate tokio to 1.20.4 [security] (#4) - ([ce3ec34](https://codeberg.org/ThetaDev/rustypipe/commit/ce3ec34337b8acac41410ea39264aab7423d5801))
- *(deps)* Update rust crate quick-xml to 0.34.0 (#5) - ([1e8a1af](https://codeberg.org/ThetaDev/rustypipe/commit/1e8a1af08c873cee7feadf63c2eff62753a78f64))
- *(deps)* Update rust crate rstest to 0.21.0 (#7) - ([c3af918](https://codeberg.org/ThetaDev/rustypipe/commit/c3af918ba53c6230c0e4aef822a0cb2cf120bf3f))
- Update rustypipe to 0.2.0
## [v0.1.0](https://codeberg.org/ThetaDev/rustypipe/commits/tag/rustypipe-cli/v0.1.0) - 2024-03-22
Initial release
<!-- generated by git-cliff -->

View file

@ -1,18 +1,15 @@
[package]
name = "rustypipe-cli"
version = "0.7.2"
rust-version = "1.70.0"
edition.workspace = true
authors.workspace = true
license.workspace = true
repository.workspace = true
keywords.workspace = true
categories.workspace = true
version = "0.1.0"
edition = "2021"
authors = ["ThetaDev <t.testboy@gmail.com>"]
license = "GPL-3.0"
description = "CLI for RustyPipe - download videos and extract data from YouTube / YouTube Music"
keywords = ["youtube", "video", "music"]
categories = ["multimedia"]
[features]
default = ["native-tls"]
timezone = ["dep:time", "dep:time-tz"]
default = ["rustls-tls-native-roots"]
# Reqwest TLS options
native-tls = [
@ -42,29 +39,16 @@ rustls-tls-native-roots = [
]
[dependencies]
rustypipe = { workspace = true, features = ["rss", "userdata"] }
rustypipe-downloader.workspace = true
reqwest.workspace = true
tokio = { workspace = true, features = ["macros", "rt-multi-thread"] }
futures-util.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
clap.workspace = true
tracing.workspace = true
tracing-subscriber.workspace = true
serde_yaml.workspace = true
dirs.workspace = true
anstream = "0.6.15"
owo-colors = "4.0.0"
const_format = "0.2.33"
[[bin]]
name = "rustypipe"
path = "src/main.rs"
rustypipe = { path = "../", default-features = false }
rustypipe-downloader = { path = "../downloader", default-features = false }
reqwest = { version = "0.11.11", default_features = false }
tokio = { version = "1.20.0", features = ["macros", "rt-multi-thread"] }
indicatif = "0.17.0"
futures = "0.3.21"
anyhow = "1.0"
clap = { version = "4.0.29", features = ["derive"] }
tracing-subscriber = "0.3.17"
serde = "1.0"
serde_json = "1.0.82"
serde_yaml = "0.9.19"
dirs = "5.0.0"

View file

@ -1,174 +0,0 @@
# ![RustyPipe](https://codeberg.org/ThetaDev/rustypipe/raw/branch/main/notes/logo.svg) CLI
[![Current crates.io version](https://img.shields.io/crates/v/rustypipe-cli.svg)](https://crates.io/crates/rustypipe-cli)
[![License](https://img.shields.io/badge/License-GPL--3-blue.svg?style=flat)](https://opensource.org/licenses/GPL-3.0)
[![CI status](https://codeberg.org/ThetaDev/rustypipe/actions/workflows/ci.yaml/badge.svg?style=flat&label=CI)](https://codeberg.org/ThetaDev/rustypipe/actions/?workflow=ci.yaml)
The RustyPipe CLI is a powerful YouTube client for the command line. It allows you to
access most of the features of the RustyPipe crate: getting data from YouTube and
downloading videos.
## 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.
## `get`: Fetch information
You can call the get command with any YouTube entity ID or URL and RustyPipe will fetch
the associated metadata. It can fetch channels, playlists, albums and videos.
**Usage:** `rustypipe get UC2TXq_t06Hjdr2g_KdKpHQg`
- `-l`, `--limit` Limit the number of list items to fetch
- `-t`, `--tab` Channel tab (options: **videos**, shorts, live, playlists, info)
- `-m, --music` Use the YouTube Music API
- `--rss`Fetch the RSS feed of a channel
- `--comments` Get comments (options: top, latest)
- `--lyrics` Get the lyrics for YTM tracks
- `--player` Get the player data instead of the video details when fetching videos
- `-c`, `--client-type` YT clients used to fetch player data (options: desktop, tv,
tv-embed, android, ios; if multiple clients are specified, they are attempted in
order)
## `search`: Search YouTube
With the search command you can search the entire YouTube platform or individual
channels. YouTube Music search is also supported.
Note that search filters are only supported when searching YouTube. They have no effect
when searching YTM or individual channels.
**Usage:** `rustypipe search "query"`
### Options
- `-l`, `--limit` Limit the number of list items to fetch
- `--item-type` Filter results by item type
- `--length` Filter results by video length
- `--date` Filter results by upload date (options: hour, day, week, month, year)
- `--order` Sort search results (options: rating, date, views)
- `--channel` Channel ID for searching channel videos
- `-m`, `--music` Search YouTube Music in the given category (options: all, tracks,
videos, artists, albums, playlists-ytm, playlists-community)
## `dl`: Download videos
The downloader can download individual videos, playlists, albums and channels. Multiple
videos can be downloaded in parallel for improved performance.
**Usage:** `rustypipe dl eRsGyueVLvQ`
### Options
- `-o`, `--output` Download to the given directory
- `--output-file` Download to the given file
- `--template` Download to a path determined by a template
- `-r`, `--resolution` Video resolution (e.g. 720, 1080). Set to 0 for audio-only
- `-a`, `--audio` Download only the audio track and write track metadata + album cover
- `-p`, `--parallel` Number of videos downloaded in parallel (default: 8)
- `-m`, `--music` Use YouTube Music for downloading playlists
- `-l`, `--limit` Limit the number of videos to download (default: 1000)
- `-c`, `--client-type` YT clients used to fetch player data (options: desktop, tv,
tv-embed, android, ios; if multiple clients are specified, they are attempted in
order)
## `vdata`: Get visitor data
You can use the vdata command to get a new visitor data ID. 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
### Output format
By default, the CLI outputs YouTube data in a human-readable text format. If you want to
store the data or process it with a script, you should choose a machine readable output
format. You can choose both JSON and YAML with the `-f, --format` flag.

File diff suppressed because it is too large Load diff

View file

@ -1,100 +0,0 @@
# git-cliff ~ default configuration file
# https://git-cliff.org/docs/configuration
#
# Lines starting with "#" are comments.
# Configuration options are organized into tables and keys.
# See documentation for more information on available options.
[changelog]
# changelog header
header = """
# Changelog\n
All notable changes to this project will be documented in this file.\n
"""
# template for the changelog body
# https://keats.github.io/tera/docs/#introduction
body = """
{% set repo_url = "https://codeberg.org/ThetaDev/rustypipe" %}\
{% if version %}\
{%set vname = version | split(pat="/") | last %}
{%if previous.version %}\
## [{{ vname }}]({{ repo_url }}/compare/{{ previous.version }}..{{ version }})\
{% else %}\
## [{{ vname }}]({{ repo_url }}/commits/tag/{{ version }})\
{% endif %} - {{ timestamp | date(format="%Y-%m-%d") }}
{% else %}\
## [unreleased]
{% endif %}\
{% if previous.version %}\
{% for group, commits in commits | group_by(attribute="group") %}
### {{ group | striptags | trim | upper_first }}
{% for commit in commits %}
- {% if commit.scope %}*({{ commit.scope }})* {% endif %}\
{% if commit.breaking %}[**breaking**] {% endif %}\
{{ commit.message | upper_first }} - \
([{{ commit.id | truncate(length=7, end="") }}]({{ repo_url }}/commit/{{ commit.id }}))\
{% endfor %}
{% endfor %}\
{% else %}
Initial release
{% endif %}\n
"""
# template for the changelog footer
footer = """
<!-- generated by git-cliff -->
"""
# remove the leading and trailing s
trim = true
# postprocessors
postprocessors = [
# { pattern = '<REPO>', replace = "https://github.com/orhun/git-cliff" }, # replace repository URL
]
[git]
# parse the commits based on https://www.conventionalcommits.org
conventional_commits = true
# filter out the commits that are not conventional
filter_unconventional = true
# process each line of a commit as an individual commit
split_commits = false
# regex for preprocessing the commit messages
commit_preprocessors = [
# Replace issue numbers
#{ pattern = '\((\w+\s)?#([0-9]+)\)', replace = "([#${2}](<REPO>/issues/${2}))"},
# Check spelling of the commit with https://github.com/crate-ci/typos
# If the spelling is incorrect, it will be automatically fixed.
#{ pattern = '.*', replace_command = 'typos --write-changes -' },
]
# regex for parsing and grouping commits
commit_parsers = [
{ message = "^feat", group = "<!-- 0 -->🚀 Features" },
{ message = "^fix", group = "<!-- 1 -->🐛 Bug Fixes" },
{ message = "^doc", group = "<!-- 3 -->📚 Documentation" },
{ message = "^perf", group = "<!-- 4 -->⚡ Performance" },
{ message = "^refactor", group = "<!-- 2 -->🚜 Refactor" },
{ message = "^style", group = "<!-- 5 -->🎨 Styling" },
{ message = "^test", skip = true },
{ message = "^chore\\(release\\)", skip = true },
{ message = "^chore\\(pr\\)", skip = true },
{ message = "^chore\\(pull\\)", skip = true },
{ message = "^chore", group = "<!-- 7 -->⚙️ Miscellaneous Tasks" },
{ message = "^ci", skip = true },
{ body = ".*security", group = "<!-- 8 -->🛡️ Security" },
{ message = "^revert", group = "<!-- 9 -->◀️ Revert" },
]
# protect breaking changes from being skipped due to matching a skipping commit_parser
protect_breaking_commits = false
# filter out the commits that are not matched by commit parsers
filter_commits = false
# regex for matching git tags
# tag_pattern = "v[0-9].*"
# regex for skipping tags
# skip_tags = ""
# regex for ignoring tags
# ignore_tags = ""
# sort the tags topologically
topo_order = false
# sort the commits inside sections by oldest/newest order
sort_commits = "oldest"
# limit the number of commits included in the changelog.
# limit_commits = 42

View file

@ -1,33 +1,29 @@
[package]
name = "rustypipe-codegen"
version = "0.1.0"
rust-version = "1.74.0"
edition.workspace = true
authors.workspace = true
license.workspace = true
repository.workspace = true
edition = "2021"
publish = false
[dependencies]
rustypipe = { path = "../", features = ["userdata"] }
reqwest.workspace = true
tokio = { workspace = true, features = ["rt-multi-thread"] }
futures-util.workspace = true
serde.workspace = true
serde_json.workspace = true
serde_plain.workspace = true
serde_with.workspace = true
once_cell.workspace = true
regex.workspace = true
path_macro.workspace = true
anyhow.workspace = true
tracing.workspace = true
tracing-subscriber.workspace = true
clap.workspace = true
phf_codegen.workspace = true
indicatif.workspace = true
num_enum = "0.7.2"
rustypipe = { path = "../" }
reqwest = "0.11.11"
tokio = { version = "1.20.0", features = ["macros", "rt-multi-thread"] }
futures = "0.3.21"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0.82"
serde_with = { version = "3.0.0", default-features = false, features = [
"macros",
] }
anyhow = "1.0"
log = "0.4.17"
env_logger = "0.10.0"
clap = { version = "4.0.29", features = ["derive"] }
phf_codegen = "0.11.1"
once_cell = "1.12.0"
regex = "1.7.1"
indicatif = "0.17.0"
num_enum = "0.6.1"
path_macro = "1.0.0"
intl_pluralrules = "7.0.2"
unic-langid = "0.9.1"
ordered_hash_map = { version = "0.4.0", features = ["serde"] }
ordered_hash_map = { version = "0.2.0", features = ["serde"] }

View file

@ -1,20 +1,18 @@
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;
use regex::Regex;
use rustypipe::client::{ClientType, RustyPipe, RustyPipeQuery};
use rustypipe::client::{ClientType, RustyPipe, RustyPipeQuery, YTContext};
use rustypipe::model::{MusicItem, YouTubeItem};
use rustypipe::param::search_filter::{ItemType, SearchFilter};
use rustypipe::param::ChannelVideoTab;
use serde::de::IgnoredAny;
use serde::{Deserialize, Serialize};
use crate::model::QCont;
#[derive(
Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, TryFromPrimitive, Serialize, Deserialize,
)]
@ -33,23 +31,10 @@ pub enum ABTest {
LikeButtonViewmodel = 11,
ChannelPageHeader = 12,
MusicPlaylistTwoColumn = 13,
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; 2] = [ABTest::ChannelPageHeader, ABTest::MusicPlaylistTwoColumn];
#[derive(Debug, Serialize, Deserialize)]
pub struct ABTestRes {
@ -63,6 +48,7 @@ pub struct ABTestRes {
#[derive(Debug, Serialize)]
struct QVideo<'a> {
context: YTContext<'a>,
video_id: &'a str,
content_check_ok: bool,
racy_check_ok: bool,
@ -71,6 +57,7 @@ struct QVideo<'a> {
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
struct QBrowse<'a> {
context: YTContext<'a>,
browse_id: &'a str,
#[serde(skip_serializing_if = "Option::is_none")]
params: Option<&'a str>,
@ -85,6 +72,7 @@ pub async fn run_test(
let rp = RustyPipe::new();
let pb = ProgressBar::new(n as u64);
let http = reqwest::Client::default();
pb.set_style(
ProgressStyle::with_template(
"{msg} [{elapsed_precise}] [{wide_bar:.cyan/blue}] {pos}/{len}",
@ -96,8 +84,9 @@ pub async fn run_test(
.map(|_| {
let rp = rp.clone();
let pb = pb.clone();
let http = http.clone();
async move {
let visitor_data = rp.query().get_visitor_data(true).await.unwrap();
let visitor_data = get_visitor_data(&http).await;
let query = rp.query().visitor_data(&visitor_data);
let is_present = match ab {
ABTest::AttributedTextDescription => attributed_text_description(&query).await,
@ -115,17 +104,6 @@ pub async fn run_test(
ABTest::LikeButtonViewmodel => like_button_viewmodel(&query).await,
ABTest::ChannelPageHeader => channel_page_header(&query).await,
ABTest::MusicPlaylistTwoColumn => music_playlist_two_column(&query).await,
ABTest::CommentsFrameworkUpdate => comments_framework_update(&query).await,
ABTest::ChannelShortsLockup => channel_shorts_lockup(&query).await,
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);
@ -147,14 +125,30 @@ pub async fn run_test(
(count, vd_present, vd_absent)
}
async fn get_visitor_data(http: &reqwest::Client) -> String {
let resp = http.get("https://www.youtube.com").send().await.unwrap();
resp.headers()
.get_all(reqwest::header::SET_COOKIE)
.iter()
.find_map(|c| {
if let Ok(cookie) = c.to_str() {
if let Some(after) = cookie.strip_prefix("__Secure-YEC=") {
return after.split_once(';').map(|s| s.0.to_owned());
}
}
None
})
.unwrap()
}
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,
@ -165,12 +159,14 @@ pub async fn run_all_tests(n: usize, concurrency: usize) -> Vec<ABTestRes> {
}
pub async fn attributed_text_description(rp: &RustyPipeQuery) -> Result<bool> {
let context = rp.get_context(ClientType::Desktop, true, None).await;
let q = QVideo {
context,
video_id: "ZeerrnuLi5E",
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 +176,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)
}
@ -193,18 +189,20 @@ pub async fn channel_handles_in_search_results(rp: &RustyPipeQuery) -> Result<bo
Ok(search.items.items.iter().any(|itm| match itm {
YouTubeItem::Channel(channel) => channel
.subscriber_count
.map(|sc| sc > 100 && channel.handle.is_some())
.map(|sc| sc > 100 && channel.video_count.is_none())
.unwrap_or_default(),
_ => false,
}))
}
pub async fn trends_video_tab(rp: &RustyPipeQuery) -> Result<bool> {
let context = rp.get_context(ClientType::Desktop, true, None).await;
let res = rp
.raw(
ClientType::Desktop,
"browse",
&QBrowse {
context,
browse_id: "FEtrending",
params: None,
},
@ -215,11 +213,13 @@ pub async fn trends_video_tab(rp: &RustyPipeQuery) -> Result<bool> {
}
pub async fn trends_page_header_renderer(rp: &RustyPipeQuery) -> Result<bool> {
let context = rp.get_context(ClientType::Desktop, true, None).await;
let res = rp
.raw(
ClientType::Desktop,
"browse",
&QBrowse {
context,
browse_id: "FEtrending",
params: None,
},
@ -243,11 +243,13 @@ pub async fn discography_page(rp: &RustyPipeQuery) -> Result<bool> {
ClientType::DesktopMusic,
"browse",
&QBrowse {
context: rp.get_context(ClientType::DesktopMusic, true, None).await,
browse_id: id,
params: None,
},
)
.await?;
.await
.unwrap();
Ok(res.contains(&format!("\"MPAD{id}\"")))
}
@ -305,11 +307,13 @@ pub async fn channel_about_modal(rp: &RustyPipeQuery) -> Result<bool> {
ClientType::Desktop,
"browse",
&QBrowse {
context: rp.get_context(ClientType::Desktop, true, None).await,
browse_id: id,
params: None,
},
)
.await?;
.await
.unwrap();
Ok(!res.contains("\"EgVhYm91dPIGBAoCEgA%3D\""))
}
@ -319,6 +323,7 @@ pub async fn like_button_viewmodel(rp: &RustyPipeQuery) -> Result<bool> {
ClientType::Desktop,
"next",
&QVideo {
context: rp.get_context(ClientType::Desktop, true, None).await,
video_id: "ZeerrnuLi5E",
content_check_ok: true,
racy_check_ok: true,
@ -332,7 +337,7 @@ pub async fn channel_page_header(rp: &RustyPipeQuery) -> Result<bool> {
let channel = rp
.channel_videos_tab("UCh8gHdtzO2tXd593_bjErWg", ChannelVideoTab::Shorts)
.await?;
Ok(channel.video_count.is_some())
Ok(channel.mobile_banner.is_empty() && channel.tv_banner.is_empty())
}
pub async fn music_playlist_two_column(rp: &RustyPipeQuery) -> Result<bool> {
@ -342,139 +347,12 @@ pub async fn music_playlist_two_column(rp: &RustyPipeQuery) -> Result<bool> {
ClientType::DesktopMusic,
"browse",
&QBrowse {
context: rp.get_context(ClientType::DesktopMusic, true, None).await,
browse_id: id,
params: None,
},
)
.await?;
.await
.unwrap();
Ok(res.contains("\"musicResponsiveHeaderRenderer\""))
}
pub async fn comments_framework_update(rp: &RustyPipeQuery) -> Result<bool> {
let continuation =
"Eg0SC3dMZHBSN2d1S3k4GAYyJSIRIgt3TGRwUjdndUt5ODAAeAJCEGNvbW1lbnRzLXNlY3Rpb24%3D";
let res = rp
.raw(ClientType::Desktop, "next", &QCont { continuation })
.await?;
Ok(res.contains("\"frameworkUpdates\""))
}
pub async fn channel_shorts_lockup(rp: &RustyPipeQuery) -> Result<bool> {
let id = "UCh8gHdtzO2tXd593_bjErWg";
let res = rp
.raw(
ClientType::Desktop,
"browse",
&QBrowse {
browse_id: id,
params: Some("EgZzaG9ydHPyBgUKA5oBAA%3D%3D"),
},
)
.await?;
Ok(res.contains("\"shortsLockupViewModel\""))
}
pub async fn playlist_page_header_renderer(rp: &RustyPipeQuery) -> Result<bool> {
let id = "VLPLZN_exA7d4RVmCQrG5VlWIjMOkMFZVVOc";
let res = rp
.raw(
ClientType::Desktop,
"browse",
&QBrowse {
browse_id: id,
params: None,
},
)
.await?;
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\""))
}

View file

@ -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)]
@ -132,7 +95,11 @@ struct HeaderRenderer {
}
async fn get_album_type(query: &RustyPipeQuery, id: &str) -> String {
let context = query
.get_context(ClientType::DesktopMusic, true, None)
.await;
let body = QBrowse {
context,
browse_id: id,
params: None,
};
@ -143,20 +110,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 +119,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,
}

View file

@ -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,
}

View file

@ -1,75 +0,0 @@
use std::{collections::BTreeMap, fs::File, io::BufReader};
use path_macro::path;
use rustypipe::{
client::RustyPipe,
param::{Language, LANGUAGES},
};
use serde::{Deserialize, Serialize};
use crate::util::{self, DICT_DIR};
#[derive(Debug, Serialize, Deserialize)]
struct Entry {
prefix: String,
suffix: String,
}
pub async fn collect_chan_prefixes() {
let cname = "kiernanchrisman";
let json_path = path!(*DICT_DIR / "chan_prefixes.json");
let mut res = BTreeMap::new();
let rp = RustyPipe::new();
for lang in LANGUAGES {
let playlist = rp
.query()
.lang(lang)
.playlist("PLZN_exA7d4RVmCQrG5VlWIjMOkMFZVVOc")
.await
.unwrap();
let n = playlist.channel.unwrap().name;
let offset = n.find(cname).unwrap();
let prefix = &n[..offset];
let suffix = &n[(offset + cname.len())..];
res.insert(
lang,
Entry {
prefix: prefix.to_owned(),
suffix: suffix.to_owned(),
},
);
}
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 / "chan_prefixes.json");
let json_file = File::open(json_path).unwrap();
let collected: BTreeMap<Language, Entry> =
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();
dict_entry.chan_prefix = e.prefix.trim().to_owned();
dict_entry.chan_suffix = e.suffix.trim().to_owned();
for lang in &dict_entry.equivalent {
let ee = collected.get(lang).unwrap();
if ee.prefix != e.prefix || ee.suffix != e.suffix {
panic!("equivalent lang conflict, lang: {lang}");
}
}
}
util::write_dict(dict);
}

View file

@ -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);
}

View file

@ -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;
@ -350,6 +350,7 @@ async fn get_channel(query: &RustyPipeQuery, channel_id: &str) -> Result<Channel
ClientType::Desktop,
"browse",
&QBrowse {
context: query.get_context(ClientType::Desktop, true, None).await,
browse_id: channel_id,
params: Some("EgZ2aWRlb3MYASAAMAE"),
},
@ -391,6 +392,7 @@ async fn get_channel(query: &RustyPipeQuery, channel_id: &str) -> Result<Channel
ClientType::Desktop,
"browse",
&QCont {
context: query.get_context(ClientType::Desktop, true, None).await,
continuation: &popular_token,
},
)
@ -429,6 +431,9 @@ async fn music_channel_subscribers(query: &RustyPipeQuery, channel_id: &str) ->
ClientType::DesktopMusic,
"browse",
&QBrowse {
context: query
.get_context(ClientType::DesktopMusic, true, None)
.await,
browse_id: channel_id,
params: None,
},

View file

@ -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::{

View file

@ -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},

View file

@ -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());
@ -268,6 +270,7 @@ async fn get_channel_vlengths(
ClientType::Desktop,
"browse",
&QBrowse {
context: query.get_context(ClientType::Desktop, true, None).await,
browse_id: channel_id,
params: Some("EgZ2aWRlb3MYASAAMAE"),
},

View file

@ -38,6 +38,8 @@ pub async fn download_testfiles() {
search_cont().await;
search_playlists().await;
search_empty().await;
startpage().await;
startpage_cont().await;
trending().await;
music_playlist().await;
@ -62,23 +64,12 @@ 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] = [
ClientType::Desktop,
ClientType::DesktopMusic,
ClientType::Tv,
ClientType::TvHtml5Embed,
ClientType::Android,
ClientType::Ios,
];
@ -456,6 +447,29 @@ async fn search_empty() {
.unwrap();
}
async fn startpage() {
let json_path = path!(*TESTFILES_DIR / "trends" / "startpage.json");
if json_path.exists() {
return;
}
let rp = rp_testfile(&json_path);
rp.query().startpage().await.unwrap();
}
async fn startpage_cont() {
let json_path = path!(*TESTFILES_DIR / "trends" / "startpage_cont.json");
if json_path.exists() {
return;
}
let rp = RustyPipe::new();
let startpage = rp.query().startpage().await.unwrap();
let rp = rp_testfile(&json_path);
startpage.next(rp.query()).await.unwrap();
}
async fn trending() {
let json_path = path!(*TESTFILES_DIR / "trends" / "trending_videos.json");
if json_path.exists() {
@ -466,36 +480,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 +801,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();
}

View file

@ -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)
@ -86,12 +85,6 @@ pub(crate) struct Entry {
///
/// Format: Parsed text -> Album type
pub album_types: phf::Map<&'static str, AlbumType>,
/// Channel name prefix on playlist pages (e.g. `by`)
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 +133,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 +180,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 }},\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).unwrap();
}
code_timeago_tokens = code_timeago_tokens.trim_end().to_owned() + "\n }\n}\n";

View file

@ -2,9 +2,6 @@
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 +29,10 @@ enum Commands {
CollectAlbumTypes,
CollectVideoDurations,
CollectVideoDates,
CollectHistoryDates,
CollectMusicHistoryDates,
CollectChanPrefixes,
CollectAlbumVersionsTitles,
ParsePlaylistDates,
ParseHistoryDates,
ParseLargeNumbers,
ParseAlbumTypes,
ParseVideoDurations,
ParseChanPrefixes,
ParseAlbumVersionsTitles,
GenLocales,
GenDict,
DownloadTestfiles,
@ -56,41 +46,32 @@ enum Commands {
#[tokio::main]
async fn main() {
tracing_subscriber::fmt::init();
env_logger::init();
let cli = Cli::parse();
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
}
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
collect_video_dates::collect_video_dates(cli.concurrency).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 } => {

View file

@ -1,7 +1,7 @@
use std::collections::BTreeMap;
use ordered_hash_map::OrderedHashMap;
use rustypipe::{model::AlbumType, param::Language};
use rustypipe::{client::YTContext, model::AlbumType, param::Language};
use serde::{Deserialize, Serialize};
use serde_with::{serde_as, DefaultOnError, VecSkipError};
@ -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
@ -57,12 +50,10 @@ pub struct DictEntry {
///
/// Format: Parsed text -> Album type
pub album_types: BTreeMap<String, AlbumType>,
/// Channel name prefix on playlist pages (e.g. `by`)
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,
/// Names of item types (Song, Video, Artist, Playlist)
///
/// Format: Parsed text -> Item type
pub item_types: BTreeMap<String, ExtItemType>,
}
/// Parsed TimeAgo string, contains amount and time unit.
@ -76,12 +67,12 @@ pub struct TimeAgo {
pub unit: TimeUnit,
}
impl std::fmt::Display for TimeAgo {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
impl ToString for TimeAgo {
fn to_string(&self) -> String {
if self.n > 1 {
write!(f, "{}{}", self.n, self.unit.as_str())
format!("{}{}", self.n, self.unit.as_str())
} else {
f.write_str(self.unit.as_str())
self.unit.as_str().to_owned()
}
}
}
@ -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",
}
}
}
@ -129,6 +116,7 @@ pub enum ExtItemType {
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct QBrowse<'a> {
pub context: YTContext<'a>,
pub browse_id: &'a str,
#[serde(skip_serializing_if = "Option::is_none")]
pub params: Option<&'a str>,
@ -137,6 +125,7 @@ pub struct QBrowse<'a> {
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
pub struct QCont<'a> {
pub context: YTContext<'a>,
pub continuation: &'a str,
}
@ -154,7 +143,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 +161,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 +170,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,
}

View file

@ -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(' ')

View file

@ -1,175 +0,0 @@
# Changelog
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
- *(deps)* Update rust crate rstest to 0.23.0 (#12) - ([96776e9](https://codeberg.org/ThetaDev/rustypipe/commit/96776e98d76fa1d31d5f84dbceafbe8f9dfd9085))
- *(deps)* Update rustypipe to 0.5.0
## [v0.2.1](https://codeberg.org/ThetaDev/rustypipe/compare/rustypipe-downloader/v0.2.0..rustypipe-downloader/v0.2.1) - 2024-09-10
### 📚 Documentation
- Fix license badge URL - ([4a253e1](https://codeberg.org/ThetaDev/rustypipe/commit/4a253e1a47317e9999e6ad31ac5c411956a0986a))
### ⚙️ Miscellaneous Tasks
- *(deps)* Update rust crate tokio to 1.20.4 [security] (#10) - ([a445e51](https://codeberg.org/ThetaDev/rustypipe/commit/a445e51b54a9afc44cd9657260a0b3d2abddbfa6))
## [v0.2.0](https://codeberg.org/ThetaDev/rustypipe/compare/rustypipe-downloader/v0.1.1..rustypipe-downloader/v0.2.0) - 2024-08-18
### 🚀 Features
- Overhauled downloader - ([11a0038](https://codeberg.org/ThetaDev/rustypipe/commit/11a00383502917cd98245c3da349107289ba3aa9))
- [**breaking**] Add TV client - ([e608811](https://codeberg.org/ThetaDev/rustypipe/commit/e608811e5f5615416241e67561671330097092cb))
- Downloader: add audio tagging - ([1e1315a](https://codeberg.org/ThetaDev/rustypipe/commit/1e1315a8378bd0ad25b5f1614e83dabc4a0b40d5))
- Downloader: add download_track fn, improve path templates - ([e1e4fb2](https://codeberg.org/ThetaDev/rustypipe/commit/e1e4fb29c190fec07f17c59ec88bef4f1c2a76a1))
- Add audiotag+indicatif features to downloader - ([97fb057](https://codeberg.org/ThetaDev/rustypipe/commit/97fb0578b5c4954a596d8dee0c4b6e1d773a9300))
- Add plaintext output to CLI - ([91b020e](https://codeberg.org/ThetaDev/rustypipe/commit/91b020efd498eff6e0f354a1de39439e252a79dd))
- Add potoken option to downloader - ([904f821](https://codeberg.org/ThetaDev/rustypipe/commit/904f8215d84c810b04e4d2134718e786a4803ad2))
- Add list of clients to downloader - ([5e646af](https://codeberg.org/ThetaDev/rustypipe/commit/5e646afd1edc6c0101501311527ea56d3bad5fd2))
- Retry with different client after 403 error - ([d875b54](https://codeberg.org/ThetaDev/rustypipe/commit/d875b5442de9822ba7ddc6f05789f56a8962808c))
- [**breaking**] Update channel model, addd handle + video_count, remove tv/mobile banner - ([e671570](https://codeberg.org/ThetaDev/rustypipe/commit/e6715700d950912031d5fbc1263f8770b6ffc49c))
### 🐛 Bug Fixes
- *(deps)* Update quick-xml to v0.35.0 - ([298e4de](https://codeberg.org/ThetaDev/rustypipe/commit/298e4def93d1595fba91be103f014aa645a08937))
- Improve deobfuscator (support multiple nsig name matches, error if mapping all streams fails) - ([8152ce6](https://codeberg.org/ThetaDev/rustypipe/commit/8152ce6b088b57be9b8419b754aca93805e5f34d))
- Set tracing instrumentation level to Error - ([9da3b25](https://codeberg.org/ThetaDev/rustypipe/commit/9da3b25be2b2577f7bd0282c09d10d368ac8b73f))
- Add docs.rs feature attributes - ([ec13cbb](https://codeberg.org/ThetaDev/rustypipe/commit/ec13cbb1f35081118dda0f7f35e3ef90f7ca79a8))
- Show docs.rs feature flags - ([67a231d](https://codeberg.org/ThetaDev/rustypipe/commit/67a231d6d1b6427f500667729a59032f2b28cc65))
### ⚙️ Miscellaneous Tasks
- *(deps)* Update rust crate quick-xml to 0.36.0 (#8) - ([b6bc05c](https://codeberg.org/ThetaDev/rustypipe/commit/b6bc05c1f39da9a846b2e3d1d24bcbccb031203b))
- *(deps)* Update rust crate rstest to 0.22.0 (#9) - ([abb7832](https://codeberg.org/ThetaDev/rustypipe/commit/abb783219aba4b492c1dff03c2148acf1f51a55d))
- Change repo URL to Codeberg - ([1793331](https://codeberg.org/ThetaDev/rustypipe/commit/17933315d947f76d5fe1aa52abf7ea24c3ce6381))
- Adjust dependency versions - ([70c6f8c](https://codeberg.org/ThetaDev/rustypipe/commit/70c6f8c3b97baefd316fff90cc727524516657af))
### Todo
- Update metadata - ([8692ca8](https://codeberg.org/ThetaDev/rustypipe/commit/8692ca81d972d0d2acf6fb4da79b9e0f5ebf4daf))
## [v0.1.1](https://codeberg.org/ThetaDev/rustypipe/compare/rustypipe-downloader/v0.1.0..rustypipe-downloader/v0.1.1) - 2024-06-27
### 📚 Documentation
- Add logo - ([6646078](https://codeberg.org/ThetaDev/rustypipe/commit/66460789449be0d5984cbdb6ec372e69323b7a88))
### ⚙️ Miscellaneous Tasks
- Changelog: fix incorrect version URLs - ([97b6f07](https://codeberg.org/ThetaDev/rustypipe/commit/97b6f07399e80e00a6c015d013e744568be125dd))
- Update rstest to v0.19.0 - ([50fd1f0](https://codeberg.org/ThetaDev/rustypipe/commit/50fd1f08caf39c1298654e06059cc393543e925b))
- Introduce MSRV - ([5dbb288](https://codeberg.org/ThetaDev/rustypipe/commit/5dbb288a496d53a299effa2026f5258af7b1f176))
- Fix clippy lints - ([45b9f2a](https://codeberg.org/ThetaDev/rustypipe/commit/45b9f2a627b4e7075ba0b1c5f16efcc19aef7922))
- *(deps)* Update rust crate tokio to 1.20.4 [security] (#4) - ([ce3ec34](https://codeberg.org/ThetaDev/rustypipe/commit/ce3ec34337b8acac41410ea39264aab7423d5801))
- *(deps)* Update rust crate quick-xml to 0.34.0 (#5) - ([1e8a1af](https://codeberg.org/ThetaDev/rustypipe/commit/1e8a1af08c873cee7feadf63c2eff62753a78f64))
- *(deps)* Update rust crate rstest to 0.21.0 (#7) - ([c3af918](https://codeberg.org/ThetaDev/rustypipe/commit/c3af918ba53c6230c0e4aef822a0cb2cf120bf3f))
- Update rustypipe to 0.2.0
## [v0.1.0](https://codeberg.org/ThetaDev/rustypipe/commits/tag/rustypipe-downloader/v0.1.0) - 2024-03-22
Initial release
<!-- generated by git-cliff -->

View file

@ -1,14 +1,12 @@
[package]
name = "rustypipe-downloader"
version = "0.3.1"
rust-version = "1.67.1"
edition.workspace = true
authors.workspace = true
license.workspace = true
repository.workspace = true
keywords.workspace = true
categories.workspace = true
version = "0.1.0"
edition = "2021"
authors = ["ThetaDev <t.testboy@gmail.com>"]
license = "GPL-3.0"
description = "Downloader extension for RustyPipe"
keywords = ["youtube", "video", "music"]
categories = ["multimedia"]
[features]
default = ["default-tls"]
@ -30,37 +28,17 @@ rustls-tls-native-roots = [
"rustypipe/rustls-tls-native-roots",
]
audiotag = ["dep:lofty", "dep:image", "dep:smartcrop2"]
[dependencies]
rustypipe.workspace = true
once_cell.workspace = true
regex.workspace = true
thiserror.workspace = true
futures-util.workspace = true
reqwest = { workspace = true, features = ["stream"] }
rand.workspace = true
tokio = { workspace = true, features = ["macros", "fs", "process"] }
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",
rustypipe = { path = "..", default-features = false }
once_cell = "1.12.0"
regex = "1.6.0"
thiserror = "1.0.36"
futures = "0.3.21"
indicatif = "0.17.0"
filenamify = "0.1.0"
log = "0.4.17"
reqwest = { version = "0.11.11", default-features = false, features = [
"stream",
] }
smartcrop2 = { version = "0.4.0", optional = true }
[dev-dependencies]
path_macro.workspace = true
rstest.workspace = true
serde_json.workspace = true
temp_testdir = "0.2.3"
[package.metadata.docs.rs]
# To build locally:
# RUSTDOCFLAGS="--cfg docsrs" cargo +nightly doc --features indicatif,audiotag --no-deps --open
features = ["indicatif", "audiotag"]
rustdoc-args = ["--cfg", "docsrs"]
rand = "0.8.5"
tokio = { version = "1.20.0", features = ["macros", "fs", "process"] }

View file

@ -1,47 +0,0 @@
# ![RustyPipe](https://codeberg.org/ThetaDev/rustypipe/raw/branch/main/notes/logo.svg) Downloader
[![Current crates.io version](https://img.shields.io/crates/v/rustypipe-downloader.svg)](https://crates.io/crates/rustypipe-downloader)
[![License](https://img.shields.io/badge/License-GPL--3-blue.svg?style=flat)](https://opensource.org/licenses/GPL-3.0)
[![Docs](https://img.shields.io/docsrs/rustypipe-downloader/latest?style=flat)](https://docs.rs/rustypipe-downloader)
[![CI status](https://codeberg.org/ThetaDev/rustypipe/actions/workflows/ci.yaml/badge.svg?style=flat&label=CI)](https://codeberg.org/ThetaDev/rustypipe/actions/?workflow=ci.yaml)
The downloader is a companion crate for RustyPipe that allows for easy and fast
downloading of video and audio files.
## Features
- Fast download of streams, bypassing YouTube's throttling
- Join video and audio streams using ffmpeg
- [Indicatif](https://crates.io/crates/indicatif) support to show download progress bars
(enable `indicatif` feature to use)
- Tag audio files with title, album, artist, date, description and album cover (enable
`audiotag` feature to use)
- Album covers are automatically cropped using smartcrop to ensure they are square
## How to use
For the downloader to work, you need to have ffmpeg installed on your system. If your
ffmpeg binary is located at a non-standard path, you can configure the location using
[`DownloaderBuilder::ffmpeg`].
At first you have to instantiate and configure the downloader using either
[`Downloader::new`] or the [`DownloaderBuilder`].
Then you can build a new download query with a video ID, stream filter and destination
path and finally download the video.
```rust ignore
use rustypipe::param::StreamFilter;
use rustypipe_downloader::DownloaderBuilder;
let dl = DownloaderBuilder::new()
.audio_tag()
.crop_cover()
.build();
let filter_audio = StreamFilter::new().no_video();
dl.id("eRsGyueVLvQ").stream_filter(filter_audio).to_file("audio.opus").download().await;
let filter_video = StreamFilter::new().video_max_res(720);
dl.id("eRsGyueVLvQ").stream_filter(filter_video).to_file("video.mp4").download().await;
```

View file

@ -1,59 +0,0 @@
use std::{borrow::Cow, path::PathBuf};
use rustypipe::client::ClientType;
/// Error from the video downloader
#[derive(thiserror::Error, Debug)]
#[non_exhaustive]
pub enum DownloadError {
/// RustyPipe error
#[error("{0}")]
RustyPipe(#[from] rustypipe::error::Error),
/// Error from the HTTP client
#[error("http error: {0}")]
Http(#[from] reqwest::Error),
/// 403 error trying to download video
#[error("YouTube returned 403 error; 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>,
},
/// File IO error
#[error(transparent)]
Io(#[from] std::io::Error),
/// FFmpeg returned an error
#[error("FFmpeg error: {0}")]
Ffmpeg(Cow<'static, str>),
/// Error parsing ranges for progressive download
#[error("Progressive download error: {0}")]
Progressive(Cow<'static, str>),
/// Video could not be downloaded because of invalid player data
#[error("source error: {0}")]
Source(Cow<'static, str>),
/// Download target already exists
#[error("file {0} already exists")]
Exists(PathBuf),
#[cfg(feature = "audiotag")]
/// Audio tagging error
#[error("Audio tag error: {0}")]
AudioTag(Cow<'static, str>),
/// Other error
#[error("error: {0}")]
Other(Cow<'static, str>),
}
#[cfg(feature = "audiotag")]
impl From<lofty::error::LoftyError> for DownloadError {
fn from(value: lofty::error::LoftyError) -> Self {
Self::AudioTag(value.to_string().into())
}
}
#[cfg(feature = "audiotag")]
impl From<image::ImageError> for DownloadError {
fn from(value: image::ImageError) -> Self {
Self::AudioTag(value.to_string().into())
}
}

File diff suppressed because it is too large Load diff

View file

@ -1,8 +1,26 @@
use std::collections::BTreeMap;
use std::{borrow::Cow, collections::BTreeMap};
use reqwest::Url;
use crate::DownloadError;
/// Error from the video downloader
#[derive(thiserror::Error, Debug)]
#[non_exhaustive]
pub enum DownloadError {
/// Error from the HTTP client
#[error("http error: {0}")]
Http(#[from] reqwest::Error),
/// File IO error
#[error(transparent)]
Io(#[from] std::io::Error),
#[error("FFmpeg error: {0}")]
Ffmpeg(Cow<'static, str>),
#[error("Progressive download error: {0}")]
Progressive(Cow<'static, str>),
#[error("input error: {0}")]
Input(Cow<'static, str>),
#[error("error: {0}")]
Other(Cow<'static, str>),
}
/// Split an URL into its base string and parameter map
///

View file

@ -1,127 +0,0 @@
use std::{fs, os::unix::fs::MetadataExt, path::Path, process::Command};
use path_macro::path;
use rstest::{fixture, rstest};
use rustypipe::{client::RustyPipe, model::AudioCodec, param::StreamFilter};
use rustypipe_downloader::Downloader;
use temp_testdir::TempDir;
/// Get a new RusttyPipe instance
#[fixture]
fn rp() -> RustyPipe {
let vdata = std::env::var("YT_VDATA").ok();
RustyPipe::builder()
.strict()
.storage_dir(path!(env!("CARGO_MANIFEST_DIR") / ".."))
.visitor_data_opt(vdata)
.build()
.unwrap()
}
#[rstest]
#[tokio::test]
async fn download_video(rp: RustyPipe) {
let td = TempDir::default();
let td_path = td.to_path_buf();
let dl = Downloader::builder().rustypipe(&rp).build();
let res = dl
.id("UXqq0ZvbOnk")
.to_dir(&td_path)
.stream_filter(StreamFilter::new().video_max_res(480))
.download()
.await
.unwrap();
assert_eq!(
res.dest,
path!(td_path / "CHARGE - Blender Open Movie [UXqq0ZvbOnk].mp4")
);
assert_eq!(res.player_data.details.id, "UXqq0ZvbOnk");
}
#[rstest]
#[tokio::test]
async fn download_music(rp: RustyPipe) {
let td = TempDir::default();
let td_path = td.to_path_buf();
#[allow(unused_mut)]
let mut dl = Downloader::builder().rustypipe(&rp);
#[cfg(feature = "audiotag")]
{
dl = dl.audio_tag().crop_cover();
}
let dl = dl.build();
let res = dl
.id("bVtv3st8bgc")
.to_dir(&td_path)
.stream_filter(
StreamFilter::new()
.no_video()
.audio_codecs([AudioCodec::Opus]),
)
.download()
.await
.unwrap();
assert_eq!(
res.dest,
path!(td_path / "Lord of the Riffs [bVtv3st8bgc].opus")
);
assert_eq!(res.player_data.details.id, "bVtv3st8bgc");
let fm = fs::metadata(&res.dest).unwrap();
assert_gte(fm.size(), 6_000_000, "file size");
assert_audio_meta(
&res.dest,
"Lord of the Riffs",
"Alexander Nakarada",
"Lord of the Riffs",
"2022-02-05",
);
}
/// Assert that number A is greater than or equal to number B
#[track_caller]
fn assert_gte<T: PartialOrd + std::fmt::Display>(a: T, b: T, msg: &str) {
assert!(a >= b, "expected >= {b} {msg}, got {a}");
}
#[track_caller]
fn assert_audio_meta(p: &Path, title: &str, artist: &str, album: &str, date: &str) {
let res = Command::new("ffprobe")
.args([
"-loglevel",
"error",
"-show_entries",
"stream_tags",
"-of",
"json",
])
.arg(p)
.output()
.unwrap();
if !res.status.success() {
panic!("ffprobe error\n{}", String::from_utf8_lossy(&res.stderr))
}
let res_json = serde_json::from_slice::<serde_json::Value>(&res.stdout).unwrap();
let tags = &res_json["streams"][0]["tags"];
assert_eq!(tags["TITLE"].as_str(), Some(title));
assert_eq!(tags["ARTIST"].as_str(), Some(artist));
assert_eq!(tags["ALBUM"].as_str(), Some(album));
assert_eq!(tags["DATE"].as_str(), Some(date));
}
/// 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());
}

View file

@ -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.
@ -26,7 +26,6 @@ to the new feature.
**Status:**
- Discontinued (0%)
- Experimental (<3%)
- Common (>3%)
- Frequent (>40%)
@ -312,7 +311,7 @@ The data model for the video shelves did not change.
- **Encountered on:** 1.05.2023
- **Impact:** 🟢 Low
- **Endpoint:** browse (trending videos)
- **Status:** Stabilized
- **Status:** Frequent (99%)
YouTube changed the header renderer type on the trending page to a `pageHeaderRenderer`.
@ -381,7 +380,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**
@ -489,7 +488,7 @@ looks needlessly complex but contains the same parsing-relevant data as the old
- **Encountered on:** 29.01.2024
- **Impact:** 🟢 Low
- **Endpoint:** browse
- **Status:** Stabilized
- **Status:** Experimental (<1%)
YouTube introduced a new data model for channel headers, based on a
`"pageHeaderRenderer"`. The new model comes with more needless complexity that needs to
@ -593,511 +592,15 @@ be accomodated. There are also no mobile/TV header images available any more.
}
```
## [13] Music album/playlist 2-column layout
- **Encountered on:** 29.02.2024
- **Impact:** 🟢 Low
- **Endpoint:** browse
- **Status:** Stabilized
- **Status:** Common (6%)
![A/B test 13 screenshot](./_img/ab_13.png)
YouTube Music updated the layout of album and playlist pages. The new layout shows the
cover on the left side of the playlist content.
## [14] Comments Framework update
- **Encountered on:** 31.01.2024
- **Impact:** 🟢 Low
- **Endpoint:** next
- **Status:** Stabilized
YouTube changed the data model for YouTube comments, now putting the content into a
seperate framework update object
```json
{
"frameworkUpdates": {
"onResponseReceivedEndpoints": [
{
"clickTrackingParams": "CAAQg2ciEwi64q3dmKGFAxWvy0IFHc14BKM=",
"reloadContinuationItemsCommand": {
"targetId": "comments-section",
"continuationItems": [
{
"commentThreadRenderer": {
"replies": {
"commentRepliesRenderer": {
"contents": [
{
"continuationItemRenderer": {
"trigger": "CONTINUATION_TRIGGER_ON_ITEM_SHOWN",
"continuationEndpoint": {
"clickTrackingParams": "CHgQvnUiEwi64q3dmKGFAxWvy0IFHc14BKM=",
"commandMetadata": {
"webCommandMetadata": {
"sendPost": true,
"apiUrl": "/youtubei/v1/next"
}
},
"continuationCommand": {
"token": "Eg0SC1FpcDFWa1R1TTcwGAYygwEaUBIaVWd5TlRUOHV4REVqZ1lxeWJJRjRBYUFCQWciAggAKhhVQ3lhZmx6ek9IMEdDNjgzRGxRLWZ6d2cyC1FpcDFWa1R1TTcwQAFICoIBAggBQi9jb21tZW50LXJlcGxpZXMtaXRlbS1VZ3lOVFQ4dXhERWpnWXF5YklGNEFhQUJBZw%3D%3D",
"request": "CONTINUATION_REQUEST_TYPE_WATCH_NEXT"
}
}
}
}
],
"trackingParams": "CHgQvnUiEwi64q3dmKGFAxWvy0IFHc14BKM=",
"viewReplies": {
"buttonRenderer": {
"text": { "runs": [{ "text": "220 replies" }] },
"icon": { "iconType": "ARROW_DROP_DOWN" },
"trackingParams": "CHoQosAEIhMIuuKt3ZihhQMVr8tCBR3NeASj",
"iconPosition": "BUTTON_ICON_POSITION_TYPE_LEFT_OF_TEXT"
}
},
"hideReplies": {
"buttonRenderer": {
"text": { "runs": [{ "text": "220 replies" }] },
"icon": { "iconType": "ARROW_DROP_UP" },
"trackingParams": "CHkQ280EIhMIuuKt3ZihhQMVr8tCBR3NeASj",
"iconPosition": "BUTTON_ICON_POSITION_TYPE_LEFT_OF_TEXT"
}
},
"targetId": "comment-replies-item-UgyNTT8uxDEjgYqybIF4AaABAg"
}
},
"trackingParams": "CHYQwnUYywEiEwi64q3dmKGFAxWvy0IFHc14BKM=",
"renderingPriority": "RENDERING_PRIORITY_PINNED_COMMENT",
"isModeratedElqComment": false,
"commentViewModel": {
"commentViewModel": {
"commentId": "UgyNTT8uxDEjgYqybIF4AaABAg"
}
}
}
}
]
}
}
],
"entityBatchUpdate": {
"mutations": [
{
"entityKey": "EhpVZ3lOVFQ4dXhERWpnWXF5YklGNEFhQUJBZyAoKAE%3D",
"type": "ENTITY_MUTATION_TYPE_REPLACE",
"payload": {
"commentEntityPayload": {
"key": "EhpVZ3lOVFQ4dXhERWpnWXF5YklGNEFhQUJBZyAoKAE%3D",
"properties": {
"commentId": "UgyNTT8uxDEjgYqybIF4AaABAg",
"content": {
"content": "⚠️ Important notice: if you put any symbol immediately after markup, it will not work: *here is the comma*, without space.\n\nYou should leave space before and after , to make it work.\n\nSame for _underscore_, and -hyphen-.\n\nLeave space before opening and after closing underscore and hyphen. Put all dots and commas inside markup.",
"styleRuns": [
{
"startIndex": 135,
"length": 28,
"weightLabel": "FONT_WEIGHT_MEDIUM"
},
{
"startIndex": 267,
"length": 10,
"weightLabel": "FONT_WEIGHT_NORMAL",
"italic": true
},
{
"startIndex": 282,
"length": 7,
"weightLabel": "FONT_WEIGHT_NORMAL",
"strikethrough": "LINE_STYLE_SINGLE"
}
]
},
"publishedTime": "2 years ago (edited)",
"replyLevel": 0,
"authorButtonA11y": "@kibizoid",
"toolbarStateKey": "EhpVZ3lOVFQ4dXhERWpnWXF5YklGNEFhQUJBZyAsKAE%3D",
"translateButtonEntityKey": "EhpVZ3lOVFQ4dXhERWpnWXF5YklGNEFhQUJBZyD_ASgB"
},
"author": {
"channelId": "UCUJfyiofeHQTmxKwZ6cCwIg",
"displayName": "@kibizoid",
"avatarThumbnailUrl": "https://yt3.ggpht.com/ytc/AIdro_nY2PkIyojDqs9Bk5RY6J90-U7wePswTYl799DNJQ=s88-c-k-c0x00ffffff-no-rj",
"isVerified": false,
"isCurrentUser": false,
"isCreator": false,
"isArtist": false
},
"avatar": {
"image": {
"sources": [
{
"url": "https://yt3.ggpht.com/ytc/AIdro_nY2PkIyojDqs9Bk5RY6J90-U7wePswTYl799DNJQ=s88-c-k-c0x00ffffff-no-rj",
"width": 88,
"height": 88
}
]
}
}
}
}
}
]
}
}
}
```
## [15] Channel shorts: shortsLockupViewModel
- **Encountered on:** 10.09.2024
- **Impact:** 🟢 Low
- **Endpoint:** browse
- **Status:** Stabilized
YouTube changed the data model for the channel shorts tab
```json
{
"richItemRenderer": {
"content": {
"shortsLockupViewModel": {
"entityId": "shorts-shelf-item-ovaHmfy3O6U",
"accessibilityText": "hangover food, 17 million views - play Short",
"thumbnail": {
"sources": [
{
"url": "https://i.ytimg.com/vi/ovaHmfy3O6U/oar2.jpg?sqp=-oaymwEdCJUDENAFSFWQAgHyq4qpAwwIARUAAIhCcAHAAQY=&rs=AOn4CLBg-kG4rAi-BQ8Xkp2hOtOu-oXDLQ",
"width": 405,
"height": 720
}
]
},
"overlayMetadata": {
"primaryText": {
"content": "hangover food"
},
"secondaryText": {
"content": "17M views"
}
}
}
}
}
}
```
## [16] New playlist header renderer
- **Encountered on:** 11.10.2024
- **Impact:** 🟢 Low
- **Endpoint:** browse
- **Status:** Stabilized
```json
{
"pageHeaderRenderer": {
"pageTitle": "LilyPichu",
"content": {
"pageHeaderViewModel": {
"title": {
"dynamicTextViewModel": {
"text": {
"content": "LilyPichu"
}
}
},
"metadata": {
"contentMetadataViewModel": {
"metadataRows": [
{
"metadataParts": [
{
"avatarStack": {
"avatarStackViewModel": {
"avatars": [
{
"avatarViewModel": {
"image": {
"sources": [
{
"url": "https://yt3.ggpht.com/ytc/AIdro_kcjhSY2e8WlYjQABOB65Za8n3QYycNHP9zXwxjKpBfOg=s48-c-k-c0x00ffffff-no-rj",
"width": 48,
"height": 48
}
]
}
}
}
],
"text": {
"content": "by Kevin Ramirez",
"commandRuns": [
{
"startIndex": 0,
"length": 16,
"onTap": {
"innertubeCommand": {
"browseEndpoint": {
"browseId": "UCai7BcI5lrXC2vdc3ySku8A",
"canonicalBaseUrl": "/@XxthekevinramirezxX"
}
}
}
}
]
}
}
}
}
]
},
{
"metadataParts": [
{
"text": {
"content": "Playlist"
}
},
{
"text": {
"content": "10 videos"
}
},
{
"text": {
"content": "856 views"
}
}
]
}
]
}
},
"actions": {},
"description": {
"descriptionPreviewViewModel": {
"description": { "content": "Hello World" }
}
},
"heroImage": {
"contentPreviewImageViewModel": {
"image": {
"sources": [
{
"url": "https://i.ytimg.com/vi/DXuNJ267Vss/hqdefault.jpg?sqp=-oaymwEWCKgBEF5IWvKriqkDCQgBFQAAiEIYAQ==&rs=AOn4CLAHp6V96b70x4SWm9Pe6WEHnQhP6A",
"width": 168,
"height": 94
}
]
}
}
}
}
}
}
}
```
## [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%)
![A/B test 21 screenshot](./_img/ab_21.png)
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"
}
}
]
}
}
}
}
```
YouTube Music updated the layout of album and playlist pages. The new layout shows
the cover on the left side of the playlist content.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 290 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.5 KiB

View file

@ -1,110 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="530"
height="80"
viewBox="0 0 140.22916 21.166667"
version="1.1"
id="svg5"
inkscape:version="1.2.2 (b0a8486541, 2022-12-01)"
sodipodi:docname="logo.svg"
xml:space="preserve"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"><sodipodi:namedview
id="namedview7"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:showpageshadow="2"
inkscape:pageopacity="0"
inkscape:pagecheckerboard="false"
inkscape:deskcolor="#d1d1d1"
inkscape:document-units="px"
showgrid="false"
inkscape:zoom="1.329974"
inkscape:cx="206.77097"
inkscape:cy="117.29553"
inkscape:window-width="2516"
inkscape:window-height="1051"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:current-layer="svg5" /><defs
id="defs2" /><g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"><g
aria-label="RUSTYPIPE"
id="text236"
style="font-size:21.1667px;line-height:1.25;display:inline;stroke-width:0.264583"
transform="translate(-22.622596,-15.875)"><path
style="font-family:'Saira Stencil One';-inkscape-font-specification:'Saira Stencil One';fill:#ff2000;fill-opacity:1"
d="m 51.720162,28.78667 h -0.846668 v -3.238506 h 0.719667 q 0.656168,0 0.994835,-0.04233 0.338668,-0.0635 0.529168,-0.211667 0.169333,-0.148167 0.232834,-0.444501 0.0635,-0.296334 0.0635,-0.867834 0,-0.571501 -0.0635,-0.867835 -0.0635,-0.317501 -0.232834,-0.465668 -0.169334,-0.148166 -0.508001,-0.1905 -0.3175,-0.04233 -1.016002,-0.04233 h -0.719667 v -3.238505 h 2.18017 q 1.502835,0 2.43417,0.296333 0.931335,0.296334 1.439336,0.910169 0.465667,0.5715 0.613834,1.418168 0.169334,0.846668 0.169334,2.180171 0,1.714502 -0.317501,2.645837 -0.4445,1.185335 -1.566335,1.672169 l 2.201336,5.439842 h -4.445007 z"
id="path2732" /><path
style="font-family:'Saira Stencil One';-inkscape-font-specification:'Saira Stencil One';fill:#8c441a;fill-opacity:1"
d="m 45.751152,19.176988 h 4.23334 v 14.562689 h -4.23334 z"
id="path2711" /><path
style="font-family:'Saira Stencil One';-inkscape-font-specification:'Saira Stencil One';fill:#ff2000;fill-opacity:1"
d="m 67.701016,19.176988 h 4.23334 v 7.916346 q 0,2.074336 -0.08467,3.132671 -0.08467,1.058335 -0.465667,1.778003 -0.423334,0.825501 -1.291169,1.270002 -0.867834,0.444501 -2.391837,0.571501 z"
id="path2736" /><path
style="font-family:'Saira Stencil One';-inkscape-font-specification:'Saira Stencil One';fill:#8c441a;fill-opacity:1"
d="m 65.94418,33.909011 q -2.222504,0 -3.429006,-0.359834 -1.206501,-0.359834 -1.778002,-1.164168 -0.529168,-0.740835 -0.656168,-1.883837 -0.127,-1.143002 -0.127,-3.407838 v -7.916346 h 4.23334 v 8.763014 q 0,0.783167 0.04233,1.502835 0.04233,0.571501 0.190501,0.825502 0.148166,0.254 0.508,0.3175 0.317501,0.08467 1.016002,0.08467 h 0.486834 q 0.169334,0 0.381001,-0.04233 v 3.259672 q -0.148167,0.02117 -0.423334,0.02117 z"
id="path2713" /><path
style="font-family:'Saira Stencil One';-inkscape-font-specification:'Saira Stencil One';fill:#ff2000;fill-opacity:1"
d="m 80.041215,33.909011 q -2.11667,0 -3.937006,-0.1905 -1.079502,-0.105834 -1.629836,-0.211667 v -3.090339 q 1.248835,0.105834 2.857504,0.211667 1.016002,0.04233 1.439336,0.04233 1.143002,0 1.502836,-0.0635 v 3.302005 z"
id="path2742" /><path
style="font-family:'Saira Stencil One';-inkscape-font-specification:'Saira Stencil One';fill:#8c441a;fill-opacity:1"
d="m 81.16305,29.442837 q 0,-0.4445 -0.0635,-0.635001 -0.0635,-0.211667 -0.211667,-0.275167 -0.148167,-0.08467 -0.508001,-0.127 l -2.603504,-0.317501 q -2.30717,-0.254 -3.111505,-1.502835 -0.359834,-0.529168 -0.486834,-1.291169 -0.127,-0.762001 -0.127,-1.86267 0,-2.349503 1.206502,-3.386672 0.973668,-0.846668 3.048004,-0.994835 v 4.254507 q 0,0.275167 0.02117,0.465668 0.02117,0.1905 0.08467,0.296333 0.0635,0.127001 0.211667,0.190501 0.148167,0.04233 0.444501,0.0635 l 2.921004,0.359834 q 0.910168,0.127 1.481669,0.3175 0.571501,0.1905 0.973669,0.592668 0.952501,0.994835 0.952501,3.534839 0,2.688171 -1.185335,3.767672 -0.529168,0.486835 -1.291169,0.719668 -0.740834,0.211667 -1.756836,0.275167 z"
id="path2740" /><path
style="font-family:'Saira Stencil One';-inkscape-font-specification:'Saira Stencil One';fill:#ff2000;fill-opacity:1"
d="m 84.655556,22.521326 q -0.698502,-0.08467 -2.455338,-0.232833 -0.973668,-0.04233 -1.566335,-0.04233 -0.762002,0 -1.439336,0.04233 v -3.280839 h 0.529167 q 1.73567,0 3.598339,0.232834 0.592668,0.08467 1.333503,0.232833 z"
id="path2715" /><path
style="font-family:'Saira Stencil One';-inkscape-font-specification:'Saira Stencil One';fill:#8c441a;fill-opacity:1"
d="m 90.222387,23.410328 h 4.23334 v 10.329349 h -4.23334 z"
id="path2746" /><path
style="font-family:'Saira Stencil One';-inkscape-font-specification:'Saira Stencil One';fill:#ff2000;fill-opacity:1"
d="M 86.391214,19.176988 H 98.308067 V 22.56366 H 86.391214 Z"
id="path2717" /><path
style="font-family:'Saira Stencil One';-inkscape-font-specification:'Saira Stencil One';fill:#ff2000;fill-opacity:1"
d="m 106.33024,23.685495 1.9685,-4.508507 h 4.23334 l -1.651,3.429005 -0.6985,1.439336 -1.33351,2.772837 -0.52916,1.100669 z"
id="path2750" /><path
style="font-family:'Saira Stencil One';-inkscape-font-specification:'Saira Stencil One';fill:#8c441a;fill-opacity:1"
d="m 103.59973,28.934836 -0.1905,-0.423334 -0.55033,-1.079501 -0.52917,-1.121835 q -0.0847,-0.148167 -0.21167,-0.444501 l -0.86783,-1.79917 -2.328342,-4.889507 h 4.318012 l 4.59317,9.779015 v 4.783674 h -4.23334 z"
id="path2719" /><path
style="font-family:'Saira Stencil One';-inkscape-font-specification:'Saira Stencil One';fill:#ff2000;fill-opacity:1"
d="m 118.92441,25.971498 h 0.508 q 0.67733,0 0.99483,-0.04233 0.33867,-0.0635 0.52917,-0.254 0.1905,-0.169334 0.23283,-0.486835 0.0635,-0.338667 0.0635,-0.994834 0,-0.656168 -0.0635,-0.973669 -0.0423,-0.338667 -0.23283,-0.529167 -0.1905,-0.169334 -0.52917,-0.211667 -0.33866,-0.0635 -0.99483,-0.0635 h -0.508 v -3.238505 h 1.75683 q 1.56634,0 2.54001,0.3175 0.99483,0.296334 1.50283,0.931335 0.48684,0.592668 0.65617,1.502836 0.16933,0.889001 0.16933,2.264837 0,1.312335 -0.16933,2.18017 -0.14817,0.867834 -0.61383,1.460502 -0.52917,0.677334 -1.52401,1.037168 -0.97366,0.338668 -2.56117,0.338668 h -1.75683 z"
id="path2754" /><path
style="font-family:'Saira Stencil One';-inkscape-font-specification:'Saira Stencil One';fill:#8c441a;fill-opacity:1"
d="m 113.80207,19.176988 h 4.23334 v 14.562689 h -4.23334 z"
id="path2721" /><path
style="font-family:'Saira Stencil One';-inkscape-font-specification:'Saira Stencil One';fill:#8c441a;fill-opacity:1"
d="m 127.4546,19.176988 h 4.23334 v 14.562689 h -4.23334 z"
id="path2723" /><path
style="font-family:'Saira Stencil One';-inkscape-font-specification:'Saira Stencil One';fill:#ff2000;fill-opacity:1"
d="m 139.56195,25.971498 h 0.508 q 0.67734,0 0.99484,-0.04233 0.33866,-0.0635 0.52916,-0.254 0.1905,-0.169334 0.23284,-0.486835 0.0635,-0.338667 0.0635,-0.994834 0,-0.656168 -0.0635,-0.973669 -0.0423,-0.338667 -0.23284,-0.529167 -0.1905,-0.169334 -0.52916,-0.211667 -0.33867,-0.0635 -0.99484,-0.0635 h -0.508 v -3.238505 h 1.75684 q 1.56633,0 2.54,0.3175 0.99484,0.296334 1.50284,0.931335 0.48683,0.592668 0.65616,1.502836 0.16934,0.889001 0.16934,2.264837 0,1.312335 -0.16934,2.18017 -0.14816,0.867834 -0.61383,1.460502 -0.52917,0.677334 -1.524,1.037168 -0.97367,0.338668 -2.56117,0.338668 h -1.75684 z"
id="path2760" /><path
style="font-family:'Saira Stencil One';-inkscape-font-specification:'Saira Stencil One';fill:#8c441a;fill-opacity:1"
d="m 134.43961,19.176988 h 4.23334 v 14.562689 h -4.23334 z"
id="path2725" /><path
style="font-family:'Saira Stencil One';-inkscape-font-specification:'Saira Stencil One';fill:#ff2000;fill-opacity:1"
d="m 153.21448,30.501172 h 5.37635 v 3.238505 h -5.37635 z"
id="path2768" /><path
style="font-family:'Saira Stencil One';-inkscape-font-specification:'Saira Stencil One';fill:#8c441a;fill-opacity:1"
d="m 153.21448,24.764996 h 4.38151 v 3.238506 h -4.38151 z"
id="path2766" /><path
style="font-family:'Saira Stencil One';-inkscape-font-specification:'Saira Stencil One';fill:#ff2000;fill-opacity:1"
d="m 153.21448,19.176988 h 5.37635 v 3.238505 h -5.37635 z"
id="path2764" /><path
style="font-family:'Saira Stencil One';-inkscape-font-specification:'Saira Stencil One';fill:#8c441a;fill-opacity:1"
d="m 148.09214,19.176988 h 4.23334 v 14.562689 h -4.23334 z"
id="path2727" /></g><path
d="m 17.157261,11.267722 c 0.02821,-0.225786 0.04939,-0.451553 0.04939,-0.684389 0,-0.232826 -0.02107,-0.465666 -0.04939,-0.7055542 l 1.488721,-1.150055 c 0.134053,-0.105841 0.169334,-0.296333 0.08466,-0.451555 l -1.411108,-2.441223 c -0.08466,-0.155226 -0.275166,-0.218719 -0.43039,-0.155226 l -1.75683,0.705555 c -0.366888,-0.275166 -0.747887,-0.515056 -1.192389,-0.691443 l -0.261066,-1.869722 c -0.02822,-0.169333 -0.1764,-0.296332 -0.352775,-0.296332 h -2.822222 c -0.176401,0 -0.324554,0.127013 -0.352776,0.296332 l -0.2610673,1.869722 c -0.444501,0.176373 -0.825501,0.416277 -1.192388,0.691443 l -1.756835,-0.705555 c -0.155226,-0.06349 -0.345719,0 -0.430385,0.155226 l -1.411112,2.441223 c -0.09173,0.155226 -0.04939,0.34572 0.08467,0.451555 l 1.488722,1.150055 c -0.02822,0.2398932 -0.04938,0.4727232 -0.04938,0.7055542 0,0.232826 0.02107,0.458611 0.04938,0.684389 l -1.488722,1.171221 c -0.134053,0.10584 -0.1764,0.296333 -0.08467,0.451556 l 1.411112,2.44122 c 0.08466,0.155227 0.275165,0.211654 0.430385,0.155227 l 1.756835,-0.712611 c 0.366887,0.282222 0.747887,0.522112 1.192388,0.698501 l 0.2610673,1.86972 c 0.02821,0.169333 0.1764,0.296333 0.352776,0.296333 h 2.822222 c 0.176399,0 0.324554,-0.126987 0.352775,-0.296333 l 0.261066,-1.86972 c 0.444502,-0.183439 0.825501,-0.416279 1.192389,-0.698501 l 1.75683,0.712611 c 0.155227,0.05646 0.345723,0 0.43039,-0.155227 l 1.41111,-2.44122 c 0.08466,-0.155227 0.04939,-0.345721 -0.08466,-0.451556 z"
id="path458"
style="fill:none;fill-opacity:1;stroke:#8c441a;stroke-width:1.5875;stroke-dasharray:none;stroke-opacity:1"
sodipodi:nodetypes="csccccccccssccccccccsccccccccsscccccccc" /><path
style="fill:#ff2000;fill-opacity:1;stroke-width:0.829285"
d="M 10.29091,13.0712 14.594918,10.583335 10.29091,8.0954668 V 13.0712"
id="path1225" /></g></svg>

Before

Width:  |  Height:  |  Size: 11 KiB

View file

@ -1,30 +0,0 @@
# About the new `pot` token
YouTube has implemented a new method to prevent downloaders and alternative clients from accessing
their videos. Now requests to YouTube's video servers require a `pot` URL parameter.
It is currently only required in the web player. The YTM and embedded player sends the token, too, but does not require it (this may change in the future).
The TV player does not use the token at all and is currently the best workaround. The only downside
is that the TV player does not return any video metadata like title and description text.
The first part of a video file (range: 0-1007959 bytes) can be downloaded without the token.
Requesting more of the file requires the pot token to be set, otherwise YouTube responds with a 403
error.
The pot token is base64-formatted and usually starts with a M
`MnToZ2brHmyo0ehfKtK_EWUq60dPYDXksNX_UsaniM_Uj6zbtiIZujCHY02hr7opxB_n3XHetJQCBV9cnNHovuhvDqrjfxsKR-sjn-eIxqv3qOZKphvyDpQzlYBnT2AXK41R-ti6iPonrvlvKIASNmYX2lhsEg==`
The token is generated from YouTubes Botguard script. The token is bound to the visitor data ID
used to fetch the player data.
This feature has been A/B-tested for a few weeks. During that time, refetching the player in case
of a 403 download error often made things work again. As of 08.08.2024 this new feature seems to be
stabilized and retrying requests does not work any more.
## Getting a `pot` token
You need a real browser environment to run YouTube's botguard and obtain a pot token. The Invidious project has created a script to
<https://github.com/iv-org/youtube-trusted-session-generator/tree/master>.
The script opens YouTube's embedded video player, starts playback and extracts the visitor data

View file

@ -1,11 +0,0 @@
{
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
"extends": ["config:best-practices", ":preserveSemverRanges"],
"semanticCommits": "enabled",
"automerge": true,
"automergeStrategy": "squash",
"osvVulnerabilityAlerts": true,
"labels": ["dependency-upgrade"],
"enabledManagers": ["cargo"],
"prHourlyLimit": 5
}

View file

@ -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!(

View file

@ -9,20 +9,19 @@ 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, MapResponse, QContinuation, RustyPipeQuery, YTContext};
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
struct QChannel<'a> {
context: YTContext<'a>,
browse_id: &'a str,
params: ChannelTab,
#[serde(skip_serializing_if = "Option::is_none")]
@ -62,7 +61,9 @@ impl RustyPipeQuery {
operation: &str,
) -> Result<Channel<Paginator<VideoItem>>, Error> {
let channel_id = channel_id.as_ref();
let context = self.get_context(ClientType::Desktop, true, None).await;
let request_body = QChannel {
context,
browse_id: channel_id,
params,
query,
@ -79,7 +80,7 @@ impl RustyPipeQuery {
}
/// Get the videos from a YouTube channel
#[tracing::instrument(skip(self), level = "error")]
#[tracing::instrument(skip(self))]
pub async fn channel_videos<S: AsRef<str> + Debug>(
&self,
channel_id: S,
@ -91,7 +92,7 @@ impl RustyPipeQuery {
/// Get a ordered list of videos from a YouTube channel
///
/// This function does not return channel metadata.
#[tracing::instrument(skip(self), level = "error")]
#[tracing::instrument(skip(self))]
pub async fn channel_videos_order<S: AsRef<str> + Debug>(
&self,
channel_id: S,
@ -102,7 +103,7 @@ impl RustyPipeQuery {
}
/// Get the videos of the given tab (Shorts, Livestreams) from a YouTube channel
#[tracing::instrument(skip(self), level = "error")]
#[tracing::instrument(skip(self))]
pub async fn channel_videos_tab<S: AsRef<str> + Debug>(
&self,
channel_id: S,
@ -115,23 +116,25 @@ impl RustyPipeQuery {
/// Get a ordered list of videos from the given tab (Shorts, Livestreams) of a YouTube channel
///
/// This function does not return channel metadata.
#[tracing::instrument(skip(self), level = "error")]
#[tracing::instrument(skip(self))]
pub async fn channel_videos_tab_order<S: AsRef<str> + Debug>(
&self,
channel_id: S,
tab: ChannelVideoTab,
order: ChannelOrder,
) -> Result<Paginator<VideoItem>, Error> {
let visitor_data = Some(self.get_visitor_data().await?);
self.continuation(
order_ctoken(channel_id.as_ref(), tab, order, &random_target()),
ContinuationEndpoint::Browse,
None,
visitor_data.as_deref(),
)
.await
}
/// Search the videos of a channel
#[tracing::instrument(skip(self), level = "error")]
#[tracing::instrument(skip(self))]
pub async fn channel_search<S: AsRef<str> + Debug, S2: AsRef<str> + Debug>(
&self,
channel_id: S,
@ -147,13 +150,15 @@ impl RustyPipeQuery {
}
/// Get the playlists of a channel
#[tracing::instrument(skip(self), level = "error")]
#[tracing::instrument(skip(self))]
pub async fn channel_playlists<S: AsRef<str> + Debug>(
&self,
channel_id: S,
) -> Result<Channel<Paginator<PlaylistItem>>, Error> {
let channel_id = channel_id.as_ref();
let context = self.get_context(ClientType::Desktop, true, None).await;
let request_body = QChannel {
context,
browse_id: channel_id,
params: ChannelTab::Playlists,
query: None,
@ -170,26 +175,24 @@ impl RustyPipeQuery {
}
/// Get additional metadata from the *About* tab of a channel
#[tracing::instrument(skip(self), level = "error")]
#[tracing::instrument(skip(self))]
pub async fn channel_info<S: AsRef<str> + Debug>(
&self,
channel_id: S,
) -> Result<ChannelInfo, Error> {
let channel_id = channel_id.as_ref();
let context = self.get_context(ClientType::Desktop, false, None).await;
let request_body = QContinuation {
context,
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
}
@ -198,13 +201,16 @@ impl RustyPipeQuery {
impl MapResponse<Channel<Paginator<VideoItem>>> for response::Channel {
fn map_response(
self,
ctx: &MapRespCtx<'_>,
id: &str,
lang: Language,
_deobf: Option<&crate::deobfuscate::DeobfData>,
vdata: Option<&str>,
) -> Result<MapResult<Channel<Paginator<VideoItem>>>, ExtractionError> {
let content = map_channel_content(ctx.id, self.contents, self.alerts)?;
let content = map_channel_content(id, self.contents, self.alerts)?;
let visitor_data = self
.response_context
.visitor_data
.or_else(|| ctx.visitor_data.map(str::to_owned));
.or_else(|| vdata.map(str::to_owned));
let channel_data = map_channel(
MapChannelData {
@ -215,11 +221,12 @@ impl MapResponse<Channel<Paginator<VideoItem>>> for response::Channel {
has_shorts: content.has_shorts,
has_live: content.has_live,
},
ctx,
id,
lang,
)?;
let mut mapper = response::YouTubeListMapper::<VideoItem>::with_channel(
ctx.lang,
lang,
&channel_data.c,
channel_data.warnings,
);
@ -230,7 +237,6 @@ impl MapResponse<Channel<Paginator<VideoItem>>> for response::Channel {
mapper.ctoken,
visitor_data,
ContinuationEndpoint::Browse,
false,
);
Ok(MapResult {
@ -243,13 +249,16 @@ impl MapResponse<Channel<Paginator<VideoItem>>> for response::Channel {
impl MapResponse<Channel<Paginator<PlaylistItem>>> for response::Channel {
fn map_response(
self,
ctx: &MapRespCtx<'_>,
id: &str,
lang: Language,
_deobf: Option<&crate::deobfuscate::DeobfData>,
vdata: Option<&str>,
) -> Result<MapResult<Channel<Paginator<PlaylistItem>>>, ExtractionError> {
let content = map_channel_content(ctx.id, self.contents, self.alerts)?;
let content = map_channel_content(id, self.contents, self.alerts)?;
let visitor_data = self
.response_context
.visitor_data
.or_else(|| ctx.visitor_data.map(str::to_owned));
.or_else(|| vdata.map(str::to_owned));
let channel_data = map_channel(
MapChannelData {
@ -260,11 +269,12 @@ impl MapResponse<Channel<Paginator<PlaylistItem>>> for response::Channel {
has_shorts: content.has_shorts,
has_live: content.has_live,
},
ctx,
id,
lang,
)?;
let mut mapper = response::YouTubeListMapper::<PlaylistItem>::with_channel(
ctx.lang,
lang,
&channel_data.c,
channel_data.warnings,
);
@ -279,8 +289,14 @@ 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
fn map_response(
self,
id: &str,
_lang: Language,
_deobf: Option<&crate::deobfuscate::DeobfData>,
_visitor_data: Option<&str>,
) -> Result<MapResult<ChannelInfo>, ExtractionError> {
// Channel info is always fetched in English. There is no localized data there
// and it allows parsing the country name.
let lang = Language::En;
@ -293,7 +309,7 @@ impl MapResponse<ChannelInfo> for response::ChannelAbout {
.ok_or(ExtractionError::InvalidData("no received endpoint".into()))?,
response::ChannelAbout::Content { contents } => {
// Handle errors (e.g. age restriction) when regular channel content was returned
map_channel_content(ctx.id, contents, None)?;
map_channel_content(id, contents, None)?;
return Err(ExtractionError::InvalidData(
"could not extract aboutData".into(),
));
@ -335,7 +351,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
@ -349,6 +365,18 @@ impl MapResponse<ChannelInfo> for response::ChannelAbout {
}
}
fn map_vanity_url(url: &str, id: &str) -> Option<String> {
if url.contains(id) {
return None;
}
Url::parse(url).ok().map(|mut parsed_url| {
// The vanity URL from YouTube is http for some reason
_ = parsed_url.set_scheme("https");
parsed_url.to_string()
})
}
struct MapChannelData {
header: Option<response::channel::Header>,
metadata: Option<response::channel::Metadata>,
@ -360,41 +388,36 @@ struct MapChannelData {
fn map_channel(
d: MapChannelData,
ctx: &MapRespCtx<'_>,
id: &str,
lang: Language,
) -> Result<MapResult<Channel<()>>, ExtractionError> {
let header = d.header.ok_or_else(|| ExtractionError::NotFound {
id: ctx.id.to_owned(),
id: id.to_owned(),
msg: "no header".into(),
})?;
let metadata = d
.metadata
.ok_or_else(|| ExtractionError::NotFound {
id: ctx.id.to_owned(),
id: id.to_owned(),
msg: "no metadata".into(),
})?
.channel_metadata_renderer;
let microformat = d.microformat.ok_or_else(|| ExtractionError::NotFound {
id: ctx.id.to_owned(),
id: id.to_owned(),
msg: "no microformat".into(),
})?;
if metadata.external_id != ctx.id {
if metadata.external_id != id {
return Err(ExtractionError::WrongResult(format!(
"got wrong channel id {}, expected {}",
metadata.external_id, ctx.id
metadata.external_id, id
)));
}
let handle = metadata
let vanity_url = metadata
.vanity_channel_url
.as_ref()
.and_then(|url| Url::parse(url).ok())
.and_then(|url| {
url.path()
.strip_prefix('/')
.filter(|handle| util::CHANNEL_HANDLE_REGEX.is_match(handle))
.map(str::to_owned)
});
.and_then(|url| map_vanity_url(url, id));
let mut warnings = Vec::new();
Ok(MapResult {
@ -402,16 +425,17 @@ fn map_channel(
response::channel::Header::C4TabbedHeaderRenderer(header) => Channel {
id: metadata.external_id,
name: metadata.title,
handle,
subscriber_count: header.subscriber_count_text.and_then(|txt| {
util::parse_large_numstr_or_warn(&txt, ctx.lang, &mut warnings)
}),
video_count: None,
subscriber_count: header
.subscriber_count_text
.and_then(|txt| util::parse_large_numstr_or_warn(&txt, lang, &mut warnings)),
avatar: header.avatar.into(),
verification: header.badges.into(),
description: metadata.description,
tags: microformat.microformat_data_renderer.tags,
vanity_url,
banner: header.banner.into(),
mobile_banner: header.mobile_banner.into(),
tv_banner: header.tv_banner.into(),
has_shorts: d.has_shorts,
has_live: d.has_live,
visitor_data: d.visitor_data,
@ -432,20 +456,21 @@ fn map_channel(
Channel {
id: metadata.external_id,
name: metadata.title,
handle,
subscriber_count: hdata.as_ref().and_then(|hdata| {
hdata.0.as_ref().and_then(|txt| {
util::parse_large_numstr_or_warn(txt, ctx.lang, &mut warnings)
util::parse_large_numstr_or_warn(txt, lang, &mut warnings)
})
}),
video_count: None,
avatar: hdata.map(|hdata| hdata.1.into()).unwrap_or_default(),
// Since the carousel header is only used for YT-internal channels or special events
// (World Cup, Coachella, etc.) we can assume the channel to be verified
verification: crate::model::Verification::Verified,
description: metadata.description,
tags: microformat.microformat_data_renderer.tags,
vanity_url,
banner: Vec::new(),
mobile_banner: Vec::new(),
tv_banner: Vec::new(),
has_shorts: d.has_shorts,
has_live: d.has_live,
visitor_data: d.visitor_data,
@ -455,34 +480,20 @@ fn map_channel(
response::channel::Header::PageHeaderRenderer(header) => {
let hdata = header.content.page_header_view_model;
// channel handle - subscriber count - video count
let md_rows = hdata.metadata.content_metadata_view_model.metadata_rows;
let (sub_part, vc_part) = if md_rows.len() > 1 {
let mp = &md_rows[1].metadata_parts;
(mp.first(), mp.get(1))
} else {
(
md_rows.first().and_then(|md| md.metadata_parts.get(1)),
None,
)
};
let subscriber_count = sub_part.and_then(|t| {
util::parse_large_numstr_or_warn::<u64>(t.as_str(), ctx.lang, &mut warnings)
});
let video_count =
vc_part.and_then(|t| util::parse_numeric_or_warn(t.as_str(), &mut warnings));
let subscriber_count = hdata
.metadata
.content_metadata_view_model
.metadata_rows
.first()
.and_then(|md| {
md.metadata_parts.get(1).and_then(|t| {
util::parse_large_numstr_or_warn::<u64>(&t.text, lang, &mut warnings)
})
});
Channel {
id: metadata.external_id,
name: metadata.title,
handle: handle.or_else(|| {
md_rows
.first()
.and_then(|md| md.metadata_parts.get(1))
.map(|txt| txt.as_str().to_owned())
.filter(|txt| util::CHANNEL_HANDLE_REGEX.is_match(txt))
}),
subscriber_count,
video_count,
avatar: hdata
.image
.decorated_avatar_view_model
@ -490,10 +501,13 @@ 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,
vanity_url,
banner: hdata.banner.image_banner_view_model.image.into(),
mobile_banner: Vec::new(),
tv_banner: Vec::new(),
has_shorts: d.has_shorts,
has_live: d.has_live,
visitor_data: d.visitor_data,
@ -603,14 +617,15 @@ fn combine_channel_data<T>(channel_data: Channel<()>, content: T) -> Channel<T>
Channel {
id: channel_data.id,
name: channel_data.name,
handle: channel_data.handle,
subscriber_count: channel_data.subscriber_count,
video_count: channel_data.video_count,
avatar: channel_data.avatar,
verification: channel_data.verification,
description: channel_data.description,
tags: channel_data.tags,
vanity_url: channel_data.vanity_url,
banner: channel_data.banner,
mobile_banner: channel_data.mobile_banner,
tv_banner: channel_data.tv_banner,
has_shorts: channel_data.has_shorts,
has_live: channel_data.has_live,
visitor_data: channel_data.visitor_data,
@ -627,33 +642,7 @@ fn order_ctoken(
) -> String {
let mut pb_tab = ProtoBuilder::new();
pb_tab.string(2, target_id);
match tab {
ChannelVideoTab::Videos => match order {
ChannelOrder::Latest => {
pb_tab.varint(3, 1);
pb_tab.varint(4, 4);
}
ChannelOrder::Popular => {
pb_tab.varint(3, 2);
pb_tab.varint(4, 2);
}
ChannelOrder::Oldest => {
pb_tab.varint(3, 4);
pb_tab.varint(4, 5);
}
},
ChannelVideoTab::Shorts => match order {
ChannelOrder::Latest => pb_tab.varint(4, 4),
ChannelOrder::Popular => pb_tab.varint(4, 2),
ChannelOrder::Oldest => pb_tab.varint(4, 5),
},
ChannelVideoTab::Live => match order {
ChannelOrder::Latest => pb_tab.varint(5, 12),
ChannelOrder::Popular => pb_tab.varint(5, 14),
ChannelOrder::Oldest => pb_tab.varint(5, 13),
},
}
pb_tab.varint(3, order as u64);
let mut pb_3 = ProtoBuilder::new();
pb_3.embedded(tab.order_ctoken_id(), pb_tab);
@ -708,10 +697,10 @@ mod tests {
use rstest::rstest;
use crate::{
client::{response, MapRespCtx, MapResponse},
client::{response, MapResponse},
error::{ExtractionError, UnavailabilityReason},
model::{paginator::Paginator, Channel, ChannelInfo, PlaylistItem, VideoItem},
param::{ChannelOrder, ChannelVideoTab},
param::{ChannelOrder, ChannelVideoTab, Language},
serializer::MapResult,
util::tests::TESTFILES,
};
@ -731,8 +720,6 @@ mod tests {
#[case::shorts("shorts", "UCh8gHdtzO2tXd593_bjErWg")]
#[case::livestreams("livestreams", "UC2DjFE7Xf11URZqWBigcVOQ")]
#[case::pageheader("shorts_20240129_pageheader", "UCh8gHdtzO2tXd593_bjErWg")]
#[case::pageheader2("videos_20240324_pageheader2", "UC2DjFE7Xf11URZqWBigcVOQ")]
#[case::lockup("shorts_20240910_lockup", "UCh8gHdtzO2tXd593_bjErWg")]
fn map_channel_videos(#[case] name: &str, #[case] id: &str) {
let json_path = path!(*TESTFILES / "channel" / format!("channel_{name}.json"));
let json_file = File::open(json_path).unwrap();
@ -740,7 +727,7 @@ mod tests {
let channel: response::Channel =
serde_json::from_reader(BufReader::new(json_file)).unwrap();
let map_res: MapResult<Channel<Paginator<VideoItem>>> =
channel.map_response(&MapRespCtx::test(id)).unwrap();
channel.map_response(id, Language::En, None, None).unwrap();
assert!(
map_res.warnings.is_empty(),
@ -767,7 +754,7 @@ mod tests {
let channel: response::Channel =
serde_json::from_reader(BufReader::new(json_file)).unwrap();
let res: Result<MapResult<Channel<Paginator<VideoItem>>>, ExtractionError> =
channel.map_response(&MapRespCtx::test("UCbfnHqxXs_K3kvaH-WlNlig"));
channel.map_response("UCbfnHqxXs_K3kvaH-WlNlig", Language::En, None, None);
if let Err(ExtractionError::Unavailable { reason, msg }) = res {
assert_eq!(reason, UnavailabilityReason::AgeRestricted);
assert!(msg.starts_with("Laphroaig Whisky: "));
@ -777,16 +764,14 @@ 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 =
serde_json::from_reader(BufReader::new(json_file)).unwrap();
let map_res: MapResult<Channel<Paginator<PlaylistItem>>> = channel
.map_response(&MapRespCtx::test("UC2DjFE7Xf11URZqWBigcVOQ"))
.map_response("UC2DjFE7Xf11URZqWBigcVOQ", Language::En, None, None)
.unwrap();
assert!(
@ -794,7 +779,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]
@ -805,7 +790,7 @@ mod tests {
let channel: response::ChannelAbout =
serde_json::from_reader(BufReader::new(json_file)).unwrap();
let map_res: MapResult<ChannelInfo> = channel
.map_response(&MapRespCtx::test("UC2DjFE7Xf11U-RZqWBigcVOQ"))
.map_response("UC2DjFE7Xf11U-RZqWBigcVOQ", Language::En, None, None)
.unwrap();
assert!(
@ -826,7 +811,7 @@ mod tests {
ChannelOrder::Popular,
"\n$6461d7c8-0000-2040-87aa-089e0827e420",
);
assert_eq!(videos_popular_token, "4qmFsgJgEhhVQ1h1cVNCbEhBRTZYdy15ZUpBMFR1bncaRDhnWXdHaTU2TEJJbUNpUTJORFl4WkRkak9DMHdNREF3TFRJd05EQXRPRGRoWVMwd09EbGxNRGd5TjJVME1qQVlBaUFD");
assert_eq!(videos_popular_token, "4qmFsgJkEhhVQ1h1cVNCbEhBRTZYdy15ZUpBMFR1bncaSDhnWXVHaXg2S2hJbUNpUTJORFl4WkRkak9DMHdNREF3TFRJd05EQXRPRGRoWVMwd09EbGxNRGd5TjJVME1qQVlBZyUzRCUzRA%3D%3D");
let shorts_popular_token = order_ctoken(
channel_id,
@ -834,7 +819,7 @@ mod tests {
ChannelOrder::Popular,
"\n$64679ffb-0000-26b3-a1bd-582429d2c794",
);
assert_eq!(shorts_popular_token, "4qmFsgJkEhhVQ1h1cVNCbEhBRTZYdy15ZUpBMFR1bncaSDhnWXVHaXhTS2hJbUNpUTJORFkzT1dabVlpMHdNREF3TFRJMllqTXRZVEZpWkMwMU9ESTBNamxrTW1NM09UUWdBZyUzRCUzRA%3D%3D");
assert_eq!(shorts_popular_token, "4qmFsgJkEhhVQ1h1cVNCbEhBRTZYdy15ZUpBMFR1bncaSDhnWXVHaXhTS2hJbUNpUTJORFkzT1dabVlpMHdNREF3TFRJMllqTXRZVEZpWkMwMU9ESTBNamxrTW1NM09UUVlBZyUzRCUzRA%3D%3D");
let live_popular_token = order_ctoken(
channel_id,
@ -842,7 +827,7 @@ mod tests {
ChannelOrder::Popular,
"\n$64693069-0000-2a1e-8c7d-582429bd5ba8",
);
assert_eq!(live_popular_token, "4qmFsgJkEhhVQ1h1cVNCbEhBRTZYdy15ZUpBMFR1bncaSDhnWXVHaXh5S2hJbUNpUTJORFk1TXpBMk9TMHdNREF3TFRKaE1XVXRPR00zWkMwMU9ESTBNamxpWkRWaVlUZ29EZyUzRCUzRA%3D%3D");
assert_eq!(live_popular_token, "4qmFsgJkEhhVQ1h1cVNCbEhBRTZYdy15ZUpBMFR1bncaSDhnWXVHaXh5S2hJbUNpUTJORFk1TXpBMk9TMHdNREF3TFRKaE1XVXRPR00zWkMwMU9ESTBNamxpWkRWaVlUZ1lBZyUzRCUzRA%3D%3D");
}
#[test]

View file

@ -3,7 +3,7 @@ use std::fmt::Debug;
use crate::{
error::{Error, ExtractionError},
model::ChannelRss,
report::Report,
report::{Report, RustyPipeInfo},
util,
};
@ -18,7 +18,7 @@ impl RustyPipeQuery {
/// for checking a lot of channels or implementing a subscription feed.
///
/// The downside of using the RSS feed is that it does not provide video durations.
#[tracing::instrument(skip(self), level = "error")]
#[tracing::instrument(skip(self))]
pub async fn channel_rss<S: AsRef<str> + Debug>(
&self,
channel_id: S,
@ -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()),

File diff suppressed because it is too large Load diff

View file

@ -5,23 +5,16 @@ 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,
error::{Error, ExtractionError},
model::{
paginator::Paginator, traits::FromYtItem, AlbumItem, AlbumType, ArtistId, MusicArtist,
MusicItem,
},
param::{AlbumFilter, AlbumOrder},
model::{AlbumItem, ArtistId, MusicArtist},
serializer::MapResult,
util::{self, ProtoBuilder},
util,
};
use super::{
response::{self, music_item::MusicListMapper, url_endpoint::PageType},
ClientType, MapRespCtx, MapResponse, QBrowse, QBrowseParams, RustyPipeQuery,
ClientType, MapResponse, QBrowse, RustyPipeQuery,
};
impl RustyPipeQuery {
@ -45,7 +38,9 @@ impl RustyPipeQuery {
}
async fn _music_artist(&self, artist_id: &str, all_albums: bool) -> Result<MusicArtist, Error> {
let context = self.get_context(ClientType::DesktopMusic, true, None).await;
let request_body = QBrowse {
context,
browse_id: artist_id,
};
@ -61,9 +56,7 @@ impl RustyPipeQuery {
.await?;
if can_fetch_more {
artist.albums = self
.music_artist_albums(artist_id, None, Some(AlbumOrder::Recency))
.await?;
artist.albums = self.music_artist_albums(artist_id).await?;
}
Ok(artist)
@ -80,59 +73,33 @@ impl RustyPipeQuery {
}
/// Get a list of all albums of a YouTube Music artist
pub async fn music_artist_albums(
&self,
artist_id: &str,
filter: Option<AlbumFilter>,
order: Option<AlbumOrder>,
) -> Result<Vec<AlbumItem>, Error> {
let request_body = QBrowseParams {
pub async fn music_artist_albums(&self, artist_id: &str) -> Result<Vec<AlbumItem>, Error> {
let context = self.get_context(ClientType::DesktopMusic, true, None).await;
let request_body = QBrowse {
context,
browse_id: &format!("{}{}", util::ARTIST_DISCOGRAPHY_PREFIX, artist_id),
params: &albums_param(filter, order),
};
let first_page = self
.execute_request::<response::MusicArtistAlbums, _, _>(
ClientType::DesktopMusic,
"music_artist_albums",
artist_id,
"browse",
&request_body,
)
.await?;
let mut albums = first_page.albums;
let mut ctoken = first_page.ctoken;
while let Some(tkn) = &ctoken {
let request_body = QContinuation { continuation: tkn };
let resp: Paginator<MusicItem> = self
.execute_request_ctx::<response::MusicContinuation, Paginator<MusicItem>, _>(
ClientType::DesktopMusic,
"music_artist_albums_cont",
artist_id,
"browse",
&request_body,
MapRespOptions {
artist: Some(first_page.artist.clone()),
visitor_data: first_page.visitor_data.as_deref(),
..Default::default()
},
)
.await?;
if resp.items.is_empty() {
tracing::warn!("artist albums [{artist_id}] empty continuation");
}
ctoken = resp.ctoken;
albums.extend(resp.items.into_iter().filter_map(AlbumItem::from_ytm_item));
}
Ok(albums)
self.execute_request::<response::MusicArtistAlbums, _, _>(
ClientType::DesktopMusic,
"music_artist_albums",
artist_id,
"browse",
&request_body,
)
.await
}
}
impl MapResponse<MusicArtist> for response::MusicArtist {
fn map_response(self, ctx: &MapRespCtx<'_>) -> Result<MapResult<MusicArtist>, ExtractionError> {
let mapped = map_artist_page(self, ctx, false)?;
fn map_response(
self,
id: &str,
lang: crate::param::Language,
_deobf: Option<&crate::deobfuscate::DeobfData>,
_vdata: Option<&str>,
) -> Result<MapResult<MusicArtist>, ExtractionError> {
let mapped = map_artist_page(self, id, lang, false)?;
Ok(MapResult {
c: mapped.c.0,
warnings: mapped.warnings,
@ -143,35 +110,24 @@ impl MapResponse<MusicArtist> for response::MusicArtist {
impl MapResponse<(MusicArtist, bool)> for response::MusicArtist {
fn map_response(
self,
ctx: &MapRespCtx<'_>,
id: &str,
lang: crate::param::Language,
_deobf: Option<&crate::deobfuscate::DeobfData>,
_vdata: Option<&str>,
) -> Result<MapResult<(MusicArtist, bool)>, ExtractionError> {
map_artist_page(self, ctx, true)
map_artist_page(self, id, lang, true)
}
}
fn map_artist_page(
res: response::MusicArtist,
ctx: &MapRespCtx<'_>,
id: &str,
lang: crate::param::Language,
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;
@ -182,24 +138,26 @@ fn map_artist_page(
.and_then(|pb| util::string_from_pb(pb, 3));
if let Some(share_channel_id) = share_channel_id {
if share_channel_id != ctx.id {
if share_channel_id != id {
return Err(ExtractionError::Redirect(share_channel_id));
}
}
}
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(
ctx.lang,
lang,
ArtistId {
id: Some(ctx.id.to_owned()),
id: Some(id.to_owned()),
name: header.title.clone(),
},
);
@ -224,12 +182,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 +225,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 {
@ -313,7 +264,7 @@ fn map_artist_page(
Ok(MapResult {
c: (
MusicArtist {
id: ctx.id.to_owned(),
id: id.to_owned(),
name: header.title,
header_image: header.thumbnail.into(),
description: header.description,
@ -321,7 +272,7 @@ fn map_artist_page(
subscriber_count: header.subscription_button.and_then(|btn| {
util::parse_large_numstr_or_warn(
&btn.subscribe_button_renderer.subscriber_count_text,
ctx.lang,
lang,
&mut mapped.warnings,
)
}),
@ -339,22 +290,19 @@ fn map_artist_page(
})
}
#[derive(Debug)]
struct FirstAlbumPage {
albums: Vec<AlbumItem>,
ctoken: Option<String>,
artist: ArtistId,
visitor_data: Option<String>,
}
impl MapResponse<FirstAlbumPage> for response::MusicArtistAlbums {
impl MapResponse<Vec<AlbumItem>> for response::MusicArtistAlbums {
fn map_response(
self,
ctx: &MapRespCtx<'_>,
) -> Result<MapResult<FirstAlbumPage>, ExtractionError> {
id: &str,
lang: crate::param::Language,
_deobf: Option<&crate::deobfuscate::DeobfData>,
_vdata: Option<&str>,
) -> Result<MapResult<Vec<AlbumItem>>, ExtractionError> {
// dbg!(&self);
let Some(header) = self.header else {
return Err(ExtractionError::NotFound {
id: ctx.id.into(),
id: id.into(),
msg: "no header".into(),
});
};
@ -371,56 +319,27 @@ impl MapResponse<FirstAlbumPage> for response::MusicArtistAlbums {
.section_list_renderer
.contents;
let artist_id = ArtistId {
id: Some(ctx.id.to_owned()),
name: header.music_header_renderer.title,
};
let mut mapper = MusicListMapper::with_artist(ctx.lang, artist_id.clone());
let mut ctoken = None;
let mut mapper = MusicListMapper::with_artist(
lang,
ArtistId {
id: Some(id.to_owned()),
name: header.music_header_renderer.title,
},
);
for grid in grids {
mapper.map_response(grid.grid_renderer.items);
if ctoken.is_none() {
ctoken = grid
.grid_renderer
.continuations
.into_iter()
.next()
.map(|g| g.next_continuation_data.continuation);
}
}
let mapped = mapper.group_items();
Ok(MapResult {
c: FirstAlbumPage {
albums: mapped.c.albums,
ctoken,
artist: artist_id,
visitor_data: ctx.visitor_data.map(str::to_owned),
},
c: mapped.c.albums,
warnings: mapped.warnings,
})
}
}
fn albums_param(filter: Option<AlbumFilter>, order: Option<AlbumOrder>) -> String {
let mut pb_filter = ProtoBuilder::new();
if let Some(filter) = filter {
pb_filter.varint(1, filter as u64);
}
if let Some(order) = order {
pb_filter.varint(2, order as u64);
}
pb_filter.bytes(3, &[1, 2]);
let mut pb_48 = ProtoBuilder::new();
pb_48.embedded(15, pb_filter);
let mut pb_3 = ProtoBuilder::new();
pb_3.embedded(48, pb_48);
pb_3.to_base64()
}
#[cfg(test)]
mod tests {
use std::{fs::File, io::BufReader};
@ -428,7 +347,7 @@ mod tests {
use path_macro::path;
use rstest::rstest;
use crate::util::tests::TESTFILES;
use crate::{param::Language, util::tests::TESTFILES};
use super::*;
@ -437,7 +356,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();
@ -451,7 +369,7 @@ mod tests {
let resp: response::MusicArtist =
serde_json::from_reader(BufReader::new(json_file)).unwrap();
let map_res: MapResult<(MusicArtist, bool)> =
resp.map_response(&MapRespCtx::test(id)).unwrap();
resp.map_response(id, Language::En, None, None).unwrap();
let (mut artist, can_fetch_more) = map_res.c;
assert!(
@ -461,42 +379,19 @@ mod tests {
);
assert_eq!(can_fetch_more, album_page_path.is_some());
// Album overview
if let Some(album_page_path) = album_page_path {
let json_file = File::open(album_page_path).unwrap();
let resp: response::MusicArtistAlbums =
serde_json::from_reader(BufReader::new(json_file)).unwrap();
let map_res: MapResult<FirstAlbumPage> =
resp.map_response(&MapRespCtx::test(id)).unwrap();
let mut map_res: MapResult<Vec<AlbumItem>> =
resp.map_response(id, Language::En, None, None).unwrap();
assert!(
map_res.warnings.is_empty(),
"deserialization/mapping warnings: {:?}",
map_res.warnings
);
artist.albums = map_res.c.albums;
// Album overview continuation
for i in 2..10 {
let cont_path =
path!(*TESTFILES / "music_artist" / format!("artist_{name}_{i}.json"));
if !cont_path.is_file() {
break;
}
let json_file = File::open(cont_path).unwrap();
let resp: response::MusicContinuation =
serde_json::from_reader(BufReader::new(json_file)).unwrap();
let map_res: MapResult<Paginator<MusicItem>> =
resp.map_response(&MapRespCtx::test(id)).unwrap();
assert!(!map_res.c.items.is_empty());
artist.albums.extend(
map_res
.c
.items
.into_iter()
.filter_map(AlbumItem::from_ytm_item),
);
}
artist.albums.append(&mut map_res.c);
}
insta::assert_ron_snapshot!(format!("map_music_artist_{name}"), artist);
@ -510,7 +405,7 @@ mod tests {
let artist: response::MusicArtist =
serde_json::from_reader(BufReader::new(json_file)).unwrap();
let map_res: MapResult<MusicArtist> = artist
.map_response(&MapRespCtx::test("UClmXPfaYhXOYsNn_QUyheWQ"))
.map_response("UClmXPfaYhXOYsNn_QUyheWQ", Language::En, None, None)
.unwrap();
assert!(
@ -529,7 +424,7 @@ mod tests {
let artist: response::MusicArtist =
serde_json::from_reader(BufReader::new(json_file)).unwrap();
let res: Result<MapResult<MusicArtist>, ExtractionError> =
artist.map_response(&MapRespCtx::test("UCLkAepWjdylmXSltofFvsYQ"));
artist.map_response("UCLkAepWjdylmXSltofFvsYQ", Language::En, None, None);
let e = res.unwrap_err();
match e {

View file

@ -11,12 +11,13 @@ use crate::{
use super::{
response::{self, music_item::MusicListMapper, url_endpoint::MusicPageType},
ClientType, MapRespCtx, MapResponse, RustyPipeQuery,
ClientType, MapResponse, RustyPipeQuery, YTContext,
};
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
struct QCharts<'a> {
context: YTContext<'a>,
browse_id: &'a str,
params: &'a str,
#[serde(skip_serializing_if = "Option::is_none")]
@ -31,9 +32,11 @@ struct FormData {
impl RustyPipeQuery {
/// Get the YouTube Music charts for a given country
#[tracing::instrument(skip(self), level = "error")]
#[tracing::instrument(skip(self))]
pub async fn music_charts(&self, country: Option<Country>) -> Result<MusicCharts, Error> {
let context = self.get_context(ClientType::DesktopMusic, true, None).await;
let request_body = QCharts {
context,
browse_id: "FEmusic_charts",
params: "sgYPRkVtdXNpY19leHBsb3Jl",
form_data: country.map(|c| FormData {
@ -53,7 +56,13 @@ impl RustyPipeQuery {
}
impl MapResponse<MusicCharts> for response::MusicCharts {
fn map_response(self, ctx: &MapRespCtx<'_>) -> Result<MapResult<MusicCharts>, ExtractionError> {
fn map_response(
self,
_id: &str,
lang: crate::param::Language,
_deobf: Option<&crate::deobfuscate::DeobfData>,
_vdata: Option<&str>,
) -> Result<crate::serializer::MapResult<MusicCharts>, crate::error::ExtractionError> {
let countries = self
.framework_updates
.map(|fwu| {
@ -68,9 +77,9 @@ impl MapResponse<MusicCharts> for response::MusicCharts {
let mut top_playlist_id = None;
let mut trending_playlist_id = None;
let mut mapper_top = MusicListMapper::new(ctx.lang);
let mut mapper_trending = MusicListMapper::new(ctx.lang);
let mut mapper_other = MusicListMapper::new(ctx.lang);
let mut mapper_top = MusicListMapper::new(lang);
let mut mapper_trending = MusicListMapper::new(lang);
let mut mapper_other = MusicListMapper::new(lang);
self.contents
.single_column_browse_results_renderer
@ -91,7 +100,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 +122,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 {
@ -142,6 +151,7 @@ mod tests {
use rstest::rstest;
use super::*;
use crate::param::Language;
#[rstest]
#[case::default("global")]
@ -153,7 +163,8 @@ mod tests {
let charts: response::MusicCharts =
serde_json::from_reader(BufReader::new(json_file)).unwrap();
let map_res: MapResult<MusicCharts> = charts.map_response(&MapRespCtx::test("")).unwrap();
let map_res: MapResult<MusicCharts> =
charts.map_response("", Language::En, None, None).unwrap();
assert!(
map_res.warnings.is_empty(),

View file

@ -8,6 +8,7 @@ use crate::{
paginator::{ContinuationEndpoint, Paginator},
ArtistId, Lyrics, MusicRelated, TrackDetails, TrackItem,
},
param::Language,
serializer::MapResult,
};
@ -16,11 +17,12 @@ use super::{
self,
music_item::{map_queue_item, MusicListMapper},
},
ClientType, MapRespCtx, MapResponse, QBrowse, RustyPipeQuery,
ClientType, MapResponse, QBrowse, RustyPipeQuery, YTContext,
};
#[derive(Debug, Serialize)]
struct QMusicDetails<'a> {
context: YTContext<'a>,
video_id: &'a str,
enable_persistent_playlist_panel: bool,
is_audio_only: bool,
@ -29,6 +31,7 @@ struct QMusicDetails<'a> {
#[derive(Debug, Serialize)]
struct QRadio<'a> {
context: YTContext<'a>,
playlist_id: &'a str,
params: &'a str,
enable_persistent_playlist_panel: bool,
@ -37,14 +40,16 @@ struct QRadio<'a> {
}
impl RustyPipeQuery {
/// Get the metadata of a YouTube Music track
#[tracing::instrument(skip(self), level = "error")]
/// Get the metadata of a YouTube music track
#[tracing::instrument(skip(self))]
pub async fn music_details<S: AsRef<str> + Debug>(
&self,
video_id: S,
) -> Result<TrackDetails, Error> {
let video_id = video_id.as_ref();
let context = self.get_context(ClientType::DesktopMusic, true, None).await;
let request_body = QMusicDetails {
context,
video_id,
enable_persistent_playlist_panel: true,
is_audio_only: true,
@ -61,13 +66,15 @@ 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")]
#[tracing::instrument(skip(self))]
pub async fn music_lyrics<S: AsRef<str> + Debug>(&self, lyrics_id: S) -> Result<Lyrics, Error> {
let lyrics_id = lyrics_id.as_ref();
let context = self.get_context(ClientType::DesktopMusic, true, None).await;
let request_body = QBrowse {
context,
browse_id: lyrics_id,
};
@ -84,13 +91,15 @@ impl RustyPipeQuery {
/// Get related items (tracks, playlists, artists) to a YouTube Music track
///
/// The `related_id` has to be obtained using [`RustyPipeQuery::music_details`].
#[tracing::instrument(skip(self), level = "error")]
#[tracing::instrument(skip(self))]
pub async fn music_related<S: AsRef<str> + Debug>(
&self,
related_id: S,
) -> Result<MusicRelated, Error> {
let related_id = related_id.as_ref();
let context = self.get_context(ClientType::DesktopMusic, true, None).await;
let request_body = QBrowse {
context,
browse_id: related_id,
};
@ -107,13 +116,18 @@ impl RustyPipeQuery {
/// Get a YouTube Music radio (a dynamically generated playlist)
///
/// The `radio_id` can be obtained using [`RustyPipeQuery::music_artist`] to get an artist's radio.
#[tracing::instrument(skip(self), level = "error")]
#[tracing::instrument(skip(self))]
pub async fn music_radio<S: AsRef<str> + Debug>(
&self,
radio_id: S,
) -> Result<Paginator<TrackItem>, Error> {
let radio_id = radio_id.as_ref();
let visitor_data = self.get_visitor_data().await?;
let context = self
.get_context(ClientType::DesktopMusic, true, Some(&visitor_data))
.await;
let request_body = QRadio {
context,
playlist_id: radio_id,
params: "wAEB8gECeAE%3D",
enable_persistent_playlist_panel: true,
@ -121,18 +135,19 @@ impl RustyPipeQuery {
tuner_setting_value: "AUTOMIX_SETTING_NORMAL",
};
self.execute_request::<response::MusicDetails, _, _>(
self.execute_request_vdata::<response::MusicDetails, _, _>(
ClientType::DesktopMusic,
"music_radio",
radio_id,
"next",
&request_body,
Some(&visitor_data),
)
.await
}
/// Get a YouTube Music radio (a dynamically generated playlist) for a track
#[tracing::instrument(skip(self), level = "error")]
#[tracing::instrument(skip(self))]
pub async fn music_radio_track<S: AsRef<str> + Debug>(
&self,
video_id: S,
@ -142,7 +157,7 @@ impl RustyPipeQuery {
}
/// Get a YouTube Music radio (a dynamically generated playlist) for a playlist
#[tracing::instrument(skip(self), level = "error")]
#[tracing::instrument(skip(self))]
pub async fn music_radio_playlist<S: AsRef<str> + Debug>(
&self,
playlist_id: S,
@ -155,7 +170,10 @@ impl RustyPipeQuery {
impl MapResponse<TrackDetails> for response::MusicDetails {
fn map_response(
self,
ctx: &MapRespCtx<'_>,
id: &str,
lang: Language,
_deobf: Option<&crate::deobfuscate::DeobfData>,
_vdata: Option<&str>,
) -> Result<MapResult<TrackDetails>, ExtractionError> {
let tabs = self
.contents
@ -193,7 +211,7 @@ impl MapResponse<TrackDetails> for response::MusicDetails {
}
let content = content.ok_or_else(|| ExtractionError::NotFound {
id: ctx.id.to_owned(),
id: id.to_owned(),
msg: "no content".into(),
})?;
let track_item = content
@ -207,7 +225,7 @@ impl MapResponse<TrackDetails> for response::MusicDetails {
response::music_item::PlaylistPanelVideo::None => None,
})
.ok_or(ExtractionError::InvalidData(Cow::Borrowed("no video item")))?;
let mut track = map_queue_item(track_item, ctx.lang);
let mut track = map_queue_item(track_item, lang);
let mut warnings = content.contents.warnings;
warnings.append(&mut track.warnings);
@ -226,7 +244,10 @@ impl MapResponse<TrackDetails> for response::MusicDetails {
impl MapResponse<Paginator<TrackItem>> for response::MusicDetails {
fn map_response(
self,
ctx: &MapRespCtx<'_>,
id: &str,
lang: Language,
_deobf: Option<&crate::deobfuscate::DeobfData>,
_vdata: Option<&str>,
) -> Result<MapResult<Paginator<TrackItem>>, ExtractionError> {
let tabs = self
.contents
@ -239,7 +260,7 @@ impl MapResponse<Paginator<TrackItem>> for response::MusicDetails {
.into_iter()
.find_map(|t| t.tab_renderer.content)
.ok_or_else(|| ExtractionError::NotFound {
id: ctx.id.to_owned(),
id: id.to_owned(),
msg: "no content".into(),
})?
.music_queue_renderer
@ -254,7 +275,7 @@ impl MapResponse<Paginator<TrackItem>> for response::MusicDetails {
.into_iter()
.filter_map(|item| match item {
response::music_item::PlaylistPanelVideo::PlaylistPanelVideoRenderer(item) => {
let mut track = map_queue_item(item, ctx.lang);
let mut track = map_queue_item(item, lang);
warnings.append(&mut track.warnings);
Some(track.c)
}
@ -269,31 +290,35 @@ 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,
})
}
}
impl MapResponse<Lyrics> for response::MusicLyrics {
fn map_response(self, ctx: &MapRespCtx<'_>) -> Result<MapResult<Lyrics>, ExtractionError> {
fn map_response(
self,
id: &str,
_lang: Language,
_deobf: Option<&crate::deobfuscate::DeobfData>,
_vdata: Option<&str>,
) -> Result<MapResult<Lyrics>, ExtractionError> {
let lyrics = self
.contents
.into_res()
.map_err(|msg| ExtractionError::NotFound {
id: ctx.id.to_owned(),
msg: msg.into(),
})?
.into_iter()
.find_map(|item| item.music_description_shelf_renderer)
.ok_or(ExtractionError::InvalidData(Cow::Borrowed("no content")))?;
.section_list_renderer
.and_then(|sl| {
sl.contents
.into_iter()
.find_map(|item| item.music_description_shelf_renderer)
})
.ok_or(match self.contents.message_renderer {
Some(msg) => ExtractionError::NotFound {
id: id.to_owned(),
msg: msg.text.into(),
},
None => ExtractionError::InvalidData(Cow::Borrowed("no content")),
})?;
Ok(MapResult {
c: Lyrics {
@ -308,44 +333,44 @@ impl MapResponse<Lyrics> for response::MusicLyrics {
impl MapResponse<MusicRelated> for response::MusicRelated {
fn map_response(
self,
ctx: &MapRespCtx<'_>,
_id: &str,
lang: Language,
_deobf: Option<&crate::deobfuscate::DeobfData>,
_vdata: Option<&str>,
) -> Result<MapResult<MusicRelated>, ExtractionError> {
let contents = self
.contents
.into_res()
.map_err(|msg| ExtractionError::NotFound {
id: ctx.id.to_owned(),
msg: msg.into(),
})?;
// Find artist
let artist_id = contents.iter().find_map(|section| match section {
response::music_item::ItemSection::MusicCarouselShelfRenderer(shelf) => {
shelf.header.as_ref().and_then(|h| {
h.music_carousel_shelf_basic_header_renderer
.title
.0
.iter()
.find_map(|c| {
let artist = ArtistId::from(c.clone());
if artist.id.is_some() {
Some(artist)
} else {
None
}
})
})
}
_ => None,
});
let artist_id = self
.contents
.section_list_renderer
.contents
.iter()
.find_map(|section| match section {
response::music_item::ItemSection::MusicCarouselShelfRenderer(shelf) => {
shelf.header.as_ref().and_then(|h| {
h.music_carousel_shelf_basic_header_renderer
.title
.0
.iter()
.find_map(|c| {
let artist = ArtistId::from(c.clone());
if artist.id.is_some() {
Some(artist)
} else {
None
}
})
})
}
_ => None,
});
let mut mapper_tracks = MusicListMapper::new(ctx.lang);
let mut mapper_tracks = MusicListMapper::new(lang);
let mut mapper = match artist_id {
Some(artist_id) => MusicListMapper::with_artist(ctx.lang, artist_id),
None => MusicListMapper::new(ctx.lang),
Some(artist_id) => MusicListMapper::with_artist(lang, artist_id),
None => MusicListMapper::new(lang),
};
let mut sections = contents.into_iter();
let mut sections = self.contents.section_list_renderer.contents.into_iter();
if let Some(response::music_item::ItemSection::MusicCarouselShelfRenderer(shelf)) =
sections.next()
{
@ -389,7 +414,7 @@ mod tests {
use rstest::rstest;
use super::*;
use crate::{model, util::tests::TESTFILES};
use crate::{model, param::Language, util::tests::TESTFILES};
#[rstest]
#[case::mv("mv", "ZeerrnuLi5E")]
@ -401,7 +426,7 @@ mod tests {
let details: response::MusicDetails =
serde_json::from_reader(BufReader::new(json_file)).unwrap();
let map_res: MapResult<model::TrackDetails> =
details.map_response(&MapRespCtx::test(id)).unwrap();
details.map_response(id, Language::En, None, None).unwrap();
assert!(
map_res.warnings.is_empty(),
@ -421,7 +446,7 @@ mod tests {
let radio: response::MusicDetails =
serde_json::from_reader(BufReader::new(json_file)).unwrap();
let map_res: MapResult<Paginator<TrackItem>> =
radio.map_response(&MapRespCtx::test(id)).unwrap();
radio.map_response(id, Language::En, None, None).unwrap();
assert!(
map_res.warnings.is_empty(),
@ -438,7 +463,7 @@ mod tests {
let lyrics: response::MusicLyrics =
serde_json::from_reader(BufReader::new(json_file)).unwrap();
let map_res: MapResult<Lyrics> = lyrics.map_response(&MapRespCtx::test("")).unwrap();
let map_res: MapResult<Lyrics> = lyrics.map_response("", Language::En, None, None).unwrap();
assert!(
map_res.warnings.is_empty(),
@ -455,7 +480,8 @@ mod tests {
let lyrics: response::MusicRelated =
serde_json::from_reader(BufReader::new(json_file)).unwrap();
let map_res: MapResult<MusicRelated> = lyrics.map_response(&MapRespCtx::test("")).unwrap();
let map_res: MapResult<MusicRelated> =
lyrics.map_response("", Language::En, None, None).unwrap();
assert!(
map_res.warnings.is_empty(),

View file

@ -8,14 +8,16 @@ use crate::{
use super::{
response::{self, music_item::MusicListMapper, url_endpoint::NavigationEndpoint},
ClientType, MapRespCtx, MapResponse, QBrowse, QBrowseParams, RustyPipeQuery,
ClientType, MapResponse, QBrowse, QBrowseParams, RustyPipeQuery,
};
impl RustyPipeQuery {
/// Get a list of moods and genres from YouTube Music
#[tracing::instrument(skip(self), level = "error")]
#[tracing::instrument(skip(self))]
pub async fn music_genres(&self) -> Result<Vec<MusicGenreItem>, Error> {
let context = self.get_context(ClientType::DesktopMusic, true, None).await;
let request_body = QBrowse {
context,
browse_id: "FEmusic_moods_and_genres",
};
@ -30,13 +32,15 @@ impl RustyPipeQuery {
}
/// Get the playlists from a YouTube Music genre
#[tracing::instrument(skip(self), level = "error")]
#[tracing::instrument(skip(self))]
pub async fn music_genre<S: AsRef<str> + Debug>(
&self,
genre_id: S,
) -> Result<MusicGenre, Error> {
let genre_id = genre_id.as_ref();
let context = self.get_context(ClientType::DesktopMusic, true, None).await;
let request_body = QBrowseParams {
context,
browse_id: "FEmusic_moods_and_genres_category",
params: genre_id,
};
@ -55,8 +59,11 @@ impl RustyPipeQuery {
impl MapResponse<Vec<MusicGenreItem>> for response::MusicGenres {
fn map_response(
self,
_ctx: &MapRespCtx<'_>,
) -> Result<MapResult<Vec<MusicGenreItem>>, ExtractionError> {
_id: &str,
_lang: crate::param::Language,
_deobf: Option<&crate::deobfuscate::DeobfData>,
_vdata: Option<&str>,
) -> Result<crate::serializer::MapResult<Vec<MusicGenreItem>>, ExtractionError> {
let content = self
.contents
.single_column_browse_results_renderer
@ -104,7 +111,15 @@ impl MapResponse<Vec<MusicGenreItem>> for response::MusicGenres {
}
impl MapResponse<MusicGenre> for response::MusicGenre {
fn map_response(self, ctx: &MapRespCtx<'_>) -> Result<MapResult<MusicGenre>, ExtractionError> {
fn map_response(
self,
id: &str,
lang: crate::param::Language,
_deobf: Option<&crate::deobfuscate::DeobfData>,
_vdata: Option<&str>,
) -> Result<crate::serializer::MapResult<MusicGenre>, ExtractionError> {
// dbg!(&self);
let content = self
.contents
.single_column_browse_results_renderer
@ -164,7 +179,7 @@ impl MapResponse<MusicGenre> for response::MusicGenre {
_ => return None,
};
let mut mapper = MusicListMapper::new(ctx.lang);
let mut mapper = MusicListMapper::new(lang);
mapper.map_response(items);
let mut mapped = mapper.conv_items();
warnings.append(&mut mapped.warnings);
@ -179,7 +194,7 @@ impl MapResponse<MusicGenre> for response::MusicGenre {
Ok(MapResult {
c: MusicGenre {
id: ctx.id.to_owned(),
id: id.to_owned(),
name: self.header.music_header_renderer.title,
sections,
},
@ -196,7 +211,7 @@ mod tests {
use rstest::rstest;
use super::*;
use crate::{model, util::tests::TESTFILES};
use crate::{model, param::Language, util::tests::TESTFILES};
#[test]
fn map_music_genres() {
@ -206,7 +221,7 @@ mod tests {
let playlist: response::MusicGenres =
serde_json::from_reader(BufReader::new(json_file)).unwrap();
let map_res: MapResult<Vec<model::MusicGenreItem>> =
playlist.map_response(&MapRespCtx::test("")).unwrap();
playlist.map_response("", Language::En, None, None).unwrap();
assert!(
map_res.warnings.is_empty(),
@ -226,7 +241,7 @@ mod tests {
let playlist: response::MusicGenre =
serde_json::from_reader(BufReader::new(json_file)).unwrap();
let map_res: MapResult<model::MusicGenre> =
playlist.map_response(&MapRespCtx::test(id)).unwrap();
playlist.map_response(id, Language::En, None, None).unwrap();
assert!(
map_res.warnings.is_empty(),

View file

@ -4,16 +4,17 @@ use crate::{
client::response::music_item::MusicListMapper,
error::{Error, ExtractionError},
model::{traits::FromYtItem, AlbumItem, TrackItem},
serializer::MapResult,
};
use super::{response, ClientType, MapRespCtx, MapResponse, QBrowse, RustyPipeQuery};
use super::{response, ClientType, MapResponse, QBrowse, RustyPipeQuery};
impl RustyPipeQuery {
/// Get the new albums that were released on YouTube Music
#[tracing::instrument(skip(self), level = "error")]
#[tracing::instrument(skip(self))]
pub async fn music_new_albums(&self) -> Result<Vec<AlbumItem>, Error> {
let context = self.get_context(ClientType::DesktopMusic, true, None).await;
let request_body = QBrowse {
context,
browse_id: "FEmusic_new_releases_albums",
};
@ -28,9 +29,11 @@ impl RustyPipeQuery {
}
/// Get the new music videos that were released on YouTube Music
#[tracing::instrument(skip(self), level = "error")]
#[tracing::instrument(skip(self))]
pub async fn music_new_videos(&self) -> Result<Vec<TrackItem>, Error> {
let context = self.get_context(ClientType::DesktopMusic, true, None).await;
let request_body = QBrowse {
context,
browse_id: "FEmusic_new_releases_videos",
};
@ -46,7 +49,13 @@ impl RustyPipeQuery {
}
impl<T: FromYtItem> MapResponse<Vec<T>> for response::MusicNew {
fn map_response(self, ctx: &MapRespCtx<'_>) -> Result<MapResult<Vec<T>>, ExtractionError> {
fn map_response(
self,
_id: &str,
lang: crate::param::Language,
_deobf: Option<&crate::deobfuscate::DeobfData>,
_vdata: Option<&str>,
) -> Result<crate::serializer::MapResult<Vec<T>>, ExtractionError> {
let items = self
.contents
.single_column_browse_results_renderer
@ -64,7 +73,7 @@ impl<T: FromYtItem> MapResponse<Vec<T>> for response::MusicNew {
.grid_renderer
.items;
let mut mapper = MusicListMapper::new(ctx.lang);
let mut mapper = MusicListMapper::new(lang);
mapper.map_response(items);
Ok(mapper.conv_items())
@ -79,7 +88,7 @@ mod tests {
use rstest::rstest;
use super::*;
use crate::{serializer::MapResult, util::tests::TESTFILES};
use crate::{param::Language, serializer::MapResult, util::tests::TESTFILES};
#[rstest]
#[case::default("default")]
@ -89,8 +98,9 @@ mod tests {
let new_albums: response::MusicNew =
serde_json::from_reader(BufReader::new(json_file)).unwrap();
let map_res: MapResult<Vec<AlbumItem>> =
new_albums.map_response(&MapRespCtx::test("")).unwrap();
let map_res: MapResult<Vec<AlbumItem>> = new_albums
.map_response("", Language::En, None, None)
.unwrap();
assert!(
map_res.warnings.is_empty(),
@ -109,8 +119,9 @@ mod tests {
let new_videos: response::MusicNew =
serde_json::from_reader(BufReader::new(json_file)).unwrap();
let map_res: MapResult<Vec<TrackItem>> =
new_videos.map_response(&MapRespCtx::test("")).unwrap();
let map_res: MapResult<Vec<TrackItem>> = new_videos
.map_response("", Language::En, None, None)
.unwrap();
assert!(
map_res.warnings.is_empty(),

View file

@ -6,52 +6,63 @@ 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,
music_item::{map_album_type, map_artist_id, map_artists, MusicListMapper},
},
ClientType, MapRespCtx, MapResponse, QBrowse, RustyPipeQuery,
ClientType, MapResponse, QBrowse, RustyPipeQuery,
};
impl RustyPipeQuery {
/// Get a playlist from YouTube Music
#[tracing::instrument(skip(self), level = "error")]
#[tracing::instrument(skip(self))]
pub async fn music_playlist<S: AsRef<str> + Debug>(
&self,
playlist_id: S,
) -> Result<MusicPlaylist, Error> {
let playlist_id = playlist_id.as_ref();
// YTM playlists require visitor data for continuations to work
let visitor_data = if playlist_id.starts_with("RD") {
Some(self.get_visitor_data().await?)
} else {
None
};
let context = self
.get_context(ClientType::DesktopMusic, true, visitor_data.as_deref())
.await;
let request_body = QBrowse {
context,
browse_id: &format!("VL{playlist_id}"),
};
self.execute_request::<response::MusicPlaylist, _, _>(
self.execute_request_vdata::<response::MusicPlaylist, _, _>(
ClientType::DesktopMusic,
"music_playlist",
playlist_id,
"browse",
&request_body,
visitor_data.as_deref(),
)
.await
}
/// Get an album from YouTube Music
#[tracing::instrument(skip(self), level = "error")]
#[tracing::instrument(skip(self))]
pub async fn music_album<S: AsRef<str> + Debug>(
&self,
album_id: S,
) -> Result<MusicAlbum, Error> {
let album_id = album_id.as_ref();
let context = self.get_context(ClientType::DesktopMusic, true, None).await;
let request_body = QBrowse {
context,
browse_id: album_id,
};
@ -87,7 +98,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 +106,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 +126,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;
}
}
}
@ -149,23 +138,14 @@ impl RustyPipeQuery {
impl MapResponse<MusicPlaylist> for response::MusicPlaylist {
fn map_response(
self,
ctx: &MapRespCtx<'_>,
id: &str,
lang: crate::param::Language,
_deobf: Option<&crate::deobfuscate::DeobfData>,
vdata: Option<&str>,
) -> 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
@ -206,26 +186,23 @@ impl MapResponse<MusicPlaylist> for response::MusicPlaylist {
)))?;
if let Some(playlist_id) = shelf.playlist_id {
if playlist_id != ctx.id {
if playlist_id != id {
return Err(ExtractionError::WrongResult(format!(
"got wrong playlist id {}, expected {}",
playlist_id, ctx.id
"got wrong playlist id {playlist_id}, expected {id}"
)));
}
}
let mut mapper = MusicListMapper::new(ctx.lang);
let mut mapper = MusicListMapper::new(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 +228,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,
@ -321,7 +273,7 @@ impl MapResponse<MusicPlaylist> for response::MusicPlaylist {
Ok(MapResult {
c: MusicPlaylist {
id: ctx.id.to_owned(),
id: id.to_owned(),
name,
thumbnail,
channel,
@ -332,17 +284,15 @@ impl MapResponse<MusicPlaylist> for response::MusicPlaylist {
track_count,
map_res.c,
ctoken,
ctx.visitor_data.map(str::to_owned),
vdata.map(str::to_owned),
ContinuationEndpoint::MusicBrowse,
ctx.authenticated,
),
related_playlists: Paginator::new_ext(
None,
Vec::new(),
related_ctoken,
ctx.visitor_data.map(str::to_owned),
vdata.map(str::to_owned),
ContinuationEndpoint::MusicBrowse,
ctx.authenticated,
),
},
warnings: map_res.warnings,
@ -351,22 +301,16 @@ 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()));
}
}
};
fn map_response(
self,
id: &str,
lang: crate::param::Language,
_deobf: Option<&crate::deobfuscate::DeobfData>,
_vdata: Option<&str>,
) -> Result<MapResult<MusicAlbum>, ExtractionError> {
// 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 +350,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);
}
_ => (),
}
@ -468,7 +401,7 @@ impl MapResponse<MusicAlbum> for response::MusicPlaylist {
.map(|part| part.to_string())
.unwrap_or_default();
let album_type = map_album_type(album_type_txt.as_str(), ctx.lang);
let album_type = map_album_type(album_type_txt.as_str(), lang);
let year = year_txt.and_then(|txt| util::parse_numeric(&txt).ok());
fn map_playlist_id(ep: &NavigationEndpoint) -> Option<String> {
@ -482,14 +415,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,20 +447,12 @@ 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,
lang,
artists.clone(),
by_va,
AlbumId {
id: ctx.id.to_owned(),
id: id.to_owned(),
name: header.title.clone(),
},
);
@ -537,7 +460,7 @@ impl MapResponse<MusicAlbum> for response::MusicPlaylist {
let tracks_res = mapper.conv_items();
let mut warnings = tracks_res.warnings;
let mut variants_mapper = MusicListMapper::new(ctx.lang);
let mut variants_mapper = MusicListMapper::new(lang);
if let Some(res) = album_variants {
variants_mapper.map_response(res);
}
@ -546,7 +469,7 @@ impl MapResponse<MusicAlbum> for response::MusicPlaylist {
Ok(MapResult {
c: MusicAlbum {
id: ctx.id.to_owned(),
id: id.to_owned(),
playlist_id,
name: header.title,
cover: header.thumbnail.into(),
@ -558,7 +481,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,
},
@ -575,7 +497,7 @@ mod tests {
use rstest::rstest;
use super::*;
use crate::{model, util::tests::TESTFILES};
use crate::{model, param::Language, util::tests::TESTFILES};
#[rstest]
#[case::short("short", "RDCLAK5uy_kFQXdnqMaQCVx2wpUM4ZfbsGCDibZtkJk")]
@ -583,7 +505,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();
@ -591,7 +512,7 @@ mod tests {
let playlist: response::MusicPlaylist =
serde_json::from_reader(BufReader::new(json_file)).unwrap();
let map_res: MapResult<model::MusicPlaylist> =
playlist.map_response(&MapRespCtx::test(id)).unwrap();
playlist.map_response(id, Language::En, None, None).unwrap();
assert!(
map_res.warnings.is_empty(),
@ -609,8 +530,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();
@ -618,7 +539,7 @@ mod tests {
let playlist: response::MusicPlaylist =
serde_json::from_reader(BufReader::new(json_file)).unwrap();
let map_res: MapResult<model::MusicAlbum> =
playlist.map_response(&MapRespCtx::test(id)).unwrap();
playlist.map_response(id, Language::En, None, None).unwrap();
assert!(
map_res.warnings.is_empty(),

View file

@ -9,17 +9,18 @@ use crate::{
paginator::{ContinuationEndpoint, Paginator},
traits::FromYtItem,
AlbumItem, ArtistItem, MusicItem, MusicPlaylistItem, MusicSearchResult,
MusicSearchSuggestion, TrackItem, UserItem,
MusicSearchSuggestion, TrackItem,
},
param::search_filter::MusicSearchFilter,
serializer::MapResult,
};
use super::{response, ClientType, MapRespCtx, MapResponse, RustyPipeQuery};
use super::{response, ClientType, MapResponse, RustyPipeQuery, YTContext};
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
struct QSearch<'a> {
context: YTContext<'a>,
query: &'a str,
#[serde(skip_serializing_if = "Option::is_none")]
params: Option<&'a str>,
@ -28,6 +29,7 @@ struct QSearch<'a> {
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
struct QSearchSuggestion<'a> {
context: YTContext<'a>,
input: &'a str,
}
@ -42,7 +44,9 @@ impl RustyPipeQuery {
filter: Option<MusicSearchFilter>,
) -> Result<MusicSearchResult<T>, Error> {
let query = query.as_ref();
let context = self.get_context(ClientType::DesktopMusic, true, None).await;
let request_body = QSearch {
context,
query,
params: filter.map(MusicSearchFilter::params),
};
@ -57,7 +61,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,23 +125,18 @@ 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")]
#[tracing::instrument(skip(self))]
pub async fn music_search_suggestion<S: AsRef<str> + Debug>(
&self,
query: S,
) -> Result<MusicSearchSuggestion, Error> {
let query = query.as_ref();
let request_body = QSearchSuggestion { input: query };
let context = self.get_context(ClientType::DesktopMusic, true, None).await;
let request_body = QSearchSuggestion {
context,
input: query,
};
self.execute_request::<response::MusicSearchSuggestion, _, _>(
ClientType::DesktopMusic,
@ -153,8 +152,13 @@ impl RustyPipeQuery {
impl<T: FromYtItem> MapResponse<MusicSearchResult<T>> for response::MusicSearch {
fn map_response(
self,
ctx: &MapRespCtx<'_>,
_id: &str,
lang: crate::param::Language,
_deobf: Option<&crate::deobfuscate::DeobfData>,
vdata: Option<&str>,
) -> Result<MapResult<MusicSearchResult<T>>, ExtractionError> {
// dbg!(&self);
let tabs = self.contents.tabbed_search_results_renderer.contents;
let sections = tabs
.into_iter()
@ -167,7 +171,7 @@ impl<T: FromYtItem> MapResponse<MusicSearchResult<T>> for response::MusicSearch
let mut corrected_query = None;
let mut ctoken = None;
let mut mapper = MusicListMapper::new(ctx.lang);
let mut mapper = MusicListMapper::new(lang);
sections.into_iter().for_each(|section| match section {
response::music_search::ItemSection::MusicShelfRenderer(shelf) => {
@ -187,7 +191,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 {
@ -196,9 +199,8 @@ impl<T: FromYtItem> MapResponse<MusicSearchResult<T>> for response::MusicSearch
None,
map_res.c,
ctoken,
ctx.visitor_data.map(str::to_owned),
vdata.map(str::to_owned),
ContinuationEndpoint::MusicSearch,
false,
),
corrected_query,
},
@ -210,9 +212,12 @@ impl<T: FromYtItem> MapResponse<MusicSearchResult<T>> for response::MusicSearch
impl MapResponse<MusicSearchSuggestion> for response::MusicSearchSuggestion {
fn map_response(
self,
ctx: &MapRespCtx<'_>,
_id: &str,
lang: crate::param::Language,
_deobf: Option<&crate::deobfuscate::DeobfData>,
_vdata: Option<&str>,
) -> Result<MapResult<MusicSearchSuggestion>, ExtractionError> {
let mut mapper = MusicListMapper::new_search_suggest(ctx.lang);
let mut mapper = MusicListMapper::new_search_suggest(lang);
let mut terms = Vec::new();
for section in self.contents {
@ -251,11 +256,12 @@ mod tests {
use rstest::rstest;
use crate::{
client::{response, MapRespCtx, MapResponse},
client::{response, MapResponse},
model::{
AlbumItem, ArtistItem, MusicItem, MusicPlaylistItem, MusicSearchResult,
MusicSearchSuggestion, TrackItem,
},
param::Language,
serializer::MapResult,
util::tests::TESTFILES,
};
@ -265,7 +271,6 @@ mod tests {
#[case::typo("typo")]
#[case::radio("radio")]
#[case::artist("artist")]
#[case::live("live")]
fn map_music_search_main(#[case] name: &str) {
let json_path = path!(*TESTFILES / "music_search" / format!("main_{name}.json"));
let json_file = File::open(json_path).unwrap();
@ -273,7 +278,7 @@ mod tests {
let search: response::MusicSearch =
serde_json::from_reader(BufReader::new(json_file)).unwrap();
let map_res: MapResult<MusicSearchResult<MusicItem>> =
search.map_response(&MapRespCtx::test("")).unwrap();
search.map_response("", Language::En, None, None).unwrap();
assert!(
map_res.warnings.is_empty(),
@ -296,7 +301,7 @@ mod tests {
let search: response::MusicSearch =
serde_json::from_reader(BufReader::new(json_file)).unwrap();
let map_res: MapResult<MusicSearchResult<TrackItem>> =
search.map_response(&MapRespCtx::test("")).unwrap();
search.map_response("", Language::En, None, None).unwrap();
assert!(
map_res.warnings.is_empty(),
@ -315,7 +320,7 @@ mod tests {
let search: response::MusicSearch =
serde_json::from_reader(BufReader::new(json_file)).unwrap();
let map_res: MapResult<MusicSearchResult<AlbumItem>> =
search.map_response(&MapRespCtx::test("")).unwrap();
search.map_response("", Language::En, None, None).unwrap();
assert!(
map_res.warnings.is_empty(),
@ -334,7 +339,7 @@ mod tests {
let search: response::MusicSearch =
serde_json::from_reader(BufReader::new(json_file)).unwrap();
let map_res: MapResult<MusicSearchResult<ArtistItem>> =
search.map_response(&MapRespCtx::test("")).unwrap();
search.map_response("", Language::En, None, None).unwrap();
assert!(
map_res.warnings.is_empty(),
@ -355,7 +360,7 @@ mod tests {
let search: response::MusicSearch =
serde_json::from_reader(BufReader::new(json_file)).unwrap();
let map_res: MapResult<MusicSearchResult<MusicPlaylistItem>> =
search.map_response(&MapRespCtx::test("")).unwrap();
search.map_response("", Language::En, None, None).unwrap();
assert!(
map_res.warnings.is_empty(),
@ -375,8 +380,9 @@ mod tests {
let suggestion: response::MusicSearchSuggestion =
serde_json::from_reader(BufReader::new(json_file)).unwrap();
let map_res: MapResult<MusicSearchSuggestion> =
suggestion.map_response(&MapRespCtx::test("")).unwrap();
let map_res: MapResult<MusicSearchSuggestion> = suggestion
.map_response("", Language::En, None, None)
.unwrap();
assert!(
map_res.warnings.is_empty(),

View file

@ -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]",
});
}
}

View file

@ -1,3 +1,4 @@
use std::borrow::Cow;
use std::fmt::Debug;
use crate::error::{Error, ExtractionError};
@ -8,20 +9,12 @@ 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, ClientType, MapRespCtx, MapRespOptions, MapResponse, QContinuation, RustyPipeQuery,
};
use super::response::music_item::{map_queue_item, MusicListMapper, PlaylistPanelVideo};
use super::{response, ClientType, MapResponse, QContinuation, RustyPipeQuery};
impl RustyPipeQuery {
/// Get more YouTube items from the given continuation token and endpoint
#[tracing::instrument(skip(self), level = "error")]
#[tracing::instrument(skip(self))]
pub async fn continuation<T: FromYtItem, S: AsRef<str> + Debug>(
&self,
ctoken: S,
@ -30,118 +23,114 @@ impl RustyPipeQuery {
) -> Result<Paginator<T>, Error> {
let ctoken = ctoken.as_ref();
if endpoint.is_music() {
// Visitor data is required for YTM continuations
let visitor_data = match visitor_data {
Some(vd) => Cow::Borrowed(vd),
None => Cow::Owned(self.get_visitor_data().await?),
};
let context = self
.get_context(ClientType::DesktopMusic, true, Some(&visitor_data))
.await;
let request_body = QContinuation {
context,
continuation: ctoken,
};
let p = self
.execute_request_ctx::<response::MusicContinuation, Paginator<MusicItem>, _>(
.execute_request_vdata::<response::MusicContinuation, Paginator<MusicItem>, _>(
ClientType::DesktopMusic,
"music_continuation",
ctoken,
endpoint.as_str(),
&request_body,
MapRespOptions {
visitor_data,
..Default::default()
},
Some(&visitor_data),
)
.await?;
Ok(map_ytm_paginator(p, endpoint))
Ok(map_ytm_paginator(p, Some(&visitor_data), endpoint))
} else {
let context = self
.get_context(ClientType::Desktop, true, visitor_data)
.await;
let request_body = QContinuation {
context,
continuation: ctoken,
};
let p = self
.execute_request_ctx::<response::Continuation, Paginator<YouTubeItem>, _>(
.execute_request::<response::Continuation, Paginator<YouTubeItem>, _>(
ClientType::Desktop,
"continuation",
ctoken,
endpoint.as_str(),
&request_body,
MapRespOptions {
visitor_data,
..Default::default()
},
)
.await?;
Ok(map_yt_paginator(p, endpoint))
Ok(map_yt_paginator(p, visitor_data, endpoint))
}
}
}
fn map_yt_paginator<T: FromYtItem>(
p: Paginator<YouTubeItem>,
visitor_data: Option<&str>,
endpoint: ContinuationEndpoint,
) -> Paginator<T> {
Paginator {
count: p.count,
items: p.items.into_iter().filter_map(T::from_yt_item).collect(),
ctoken: p.ctoken,
visitor_data: p.visitor_data,
visitor_data: visitor_data.map(str::to_owned),
endpoint,
authenticated: p.authenticated,
}
}
fn map_ytm_paginator<T: FromYtItem>(
p: Paginator<MusicItem>,
visitor_data: Option<&str>,
endpoint: ContinuationEndpoint,
) -> Paginator<T> {
Paginator {
count: p.count,
items: p.items.into_iter().filter_map(T::from_ytm_item).collect(),
ctoken: p.ctoken,
visitor_data: p.visitor_data,
visitor_data: visitor_data.map(str::to_owned),
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<'_>,
_id: &str,
lang: crate::param::Language,
_deobf: Option<&crate::deobfuscate::DeobfData>,
_vdata: Option<&str>,
) -> 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);
let mut mapper = response::YouTubeListMapper::<YouTubeItem>::new(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,
})
}
@ -150,13 +139,12 @@ impl MapResponse<Paginator<YouTubeItem>> for response::Continuation {
impl MapResponse<Paginator<MusicItem>> for response::MusicContinuation {
fn map_response(
self,
ctx: &MapRespCtx<'_>,
_id: &str,
lang: crate::param::Language,
_deobf: Option<&crate::deobfuscate::DeobfData>,
_vdata: Option<&str>,
) -> Result<MapResult<Paginator<MusicItem>>, ExtractionError> {
let mut mapper = if let Some(artist) = &ctx.artist {
MusicListMapper::with_artist(ctx.lang, artist.clone())
} else {
MusicListMapper::new(ctx.lang)
};
let mut mapper = MusicListMapper::new(lang);
let mut continuations = Vec::new();
match self.continuation_contents {
@ -174,11 +162,7 @@ impl MapResponse<Paginator<MusicItem>> for response::MusicContinuation {
response::music_item::ItemSection::MusicCarouselShelfRenderer(shelf) => {
mapper.map_response(shelf.contents);
}
response::music_item::ItemSection::GridRenderer(mut grid) => {
mapper.map_response(grid.items);
continuations.append(&mut grid.continuations);
}
response::music_item::ItemSection::None => {}
_ => {}
}
}
}
@ -189,133 +173,23 @@ impl MapResponse<Paginator<MusicItem>> for response::MusicContinuation {
mapper.add_warnings(&mut panel.contents.warnings);
panel.contents.c.into_iter().for_each(|item| {
if let PlaylistPanelVideo::PlaylistPanelVideoRenderer(item) = item {
let mut track = map_queue_item(item, ctx.lang);
let mut track = map_queue_item(item, lang);
mapper.add_item(MusicItem::Track(track.c));
mapper.add_warnings(&mut track.warnings);
}
});
}
Some(response::music_item::ContinuationContents::GridContinuation(mut grid)) => {
mapper.map_response(grid.items);
continuations.append(&mut grid.continuations);
}
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 +199,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,
})
}
@ -395,19 +263,6 @@ impl<T: FromYtItem> Paginator<T> {
}
Ok(())
}
/// Extend the items of the paginator until the paginator is exhausted.
pub async fn extend_all<Q: AsRef<RustyPipeQuery>>(&mut self, query: Q) -> Result<(), Error> {
let query = query.as_ref();
loop {
match self.extend(query).await {
Ok(false) => break,
Err(e) => return Err(e),
_ => {}
}
}
Ok(())
}
}
impl Paginator<Comment> {
@ -425,40 +280,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> {
@ -519,33 +340,11 @@ macro_rules! paginator {
}
Ok(())
}
/// Extend the items of the paginator until the paginator is exhausted.
pub async fn extend_all<Q: AsRef<RustyPipeQuery>>(
&mut self,
query: Q,
) -> Result<(), Error> {
let query = query.as_ref();
loop {
match self.extend(query).await {
Ok(false) => break,
Err(e) => return Err(e),
_ => {}
}
}
Ok(())
}
}
};
}
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,15 +355,14 @@ mod tests {
use super::*;
use crate::{
model::{
AlbumItem, ArtistItem, ChannelItem, MusicPlaylistItem, PlaylistItem, TrackItem,
VideoItem,
},
model::{MusicPlaylistItem, PlaylistItem, TrackItem, VideoItem},
param::Language,
util::tests::TESTFILES,
};
#[rstest]
#[case::search("search", path!("search" / "cont.json"))]
#[case::startpage("startpage", path!("trends" / "startpage_cont.json"))]
#[case::recommendations("recommendations", path!("video_details" / "recommendations.json"))]
fn map_continuation_items(#[case] name: &str, #[case] path: PathBuf) {
let json_path = path!(*TESTFILES / path);
@ -573,7 +371,7 @@ mod tests {
let items: response::Continuation =
serde_json::from_reader(BufReader::new(json_file)).unwrap();
let map_res: MapResult<Paginator<YouTubeItem>> =
items.map_response(&MapRespCtx::test("")).unwrap();
items.map_response("", Language::En, None, None).unwrap();
assert!(
map_res.warnings.is_empty(),
@ -595,9 +393,9 @@ mod tests {
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();
items.map_response("", Language::En, None, None).unwrap();
let paginator: Paginator<VideoItem> =
map_yt_paginator(map_res.c, ContinuationEndpoint::Browse);
map_yt_paginator(map_res.c, None, ContinuationEndpoint::Browse);
assert!(
map_res.warnings.is_empty(),
@ -618,30 +416,9 @@ mod tests {
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();
items.map_response("", Language::En, None, None).unwrap();
let paginator: Paginator<PlaylistItem> =
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::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);
map_yt_paginator(map_res.c, None, ContinuationEndpoint::Browse);
assert!(
map_res.warnings.is_empty(),
@ -655,7 +432,6 @@ mod tests {
#[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();
@ -663,51 +439,9 @@ mod tests {
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();
items.map_response("", Language::En, None, None).unwrap();
let paginator: Paginator<TrackItem> =
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_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);
map_ytm_paginator(map_res.c, None, ContinuationEndpoint::MusicBrowse);
assert!(
map_res.warnings.is_empty(),
@ -719,7 +453,6 @@ mod tests {
#[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();
@ -727,9 +460,9 @@ mod tests {
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();
items.map_response("", Language::En, None, None).unwrap();
let paginator: Paginator<MusicPlaylistItem> =
map_ytm_paginator(map_res.c, ContinuationEndpoint::MusicBrowse);
map_ytm_paginator(map_res.c, None, ContinuationEndpoint::MusicBrowse);
assert!(
map_res.warnings.is_empty(),

File diff suppressed because it is too large Load diff

View file

@ -10,35 +10,52 @@ use crate::{
ChannelId, Playlist, VideoItem,
},
serializer::text::{TextComponent, TextComponents},
util::{self, dictionary, timeago, TryRemove},
util::{self, timeago, TryRemove},
};
use super::{response, ClientType, MapRespCtx, MapResponse, MapResult, QBrowse, RustyPipeQuery};
use super::{response, ClientType, MapResponse, MapResult, QBrowse, RustyPipeQuery};
impl RustyPipeQuery {
/// Get a YouTube playlist
#[tracing::instrument(skip(self), level = "error")]
#[tracing::instrument(skip(self))]
pub async fn playlist<S: AsRef<str> + Debug>(&self, playlist_id: S) -> Result<Playlist, Error> {
let playlist_id = playlist_id.as_ref();
// YTM playlists require visitor data for continuations to work
let visitor_data: Option<String> = if playlist_id.starts_with("RD") {
Some(self.get_visitor_data().await?)
} else {
None
};
let context = self
.get_context(ClientType::Desktop, true, visitor_data.as_deref())
.await;
let request_body = QBrowse {
context,
browse_id: &format!("VL{playlist_id}"),
};
self.execute_request::<response::Playlist, _, _>(
self.execute_request_vdata::<response::Playlist, _, _>(
ClientType::Desktop,
"playlist",
playlist_id,
"browse",
&request_body,
visitor_data.as_deref(),
)
.await
}
}
impl MapResponse<Playlist> for response::Playlist {
fn map_response(self, ctx: &MapRespCtx<'_>) -> Result<MapResult<Playlist>, ExtractionError> {
fn map_response(
self,
id: &str,
lang: crate::param::Language,
_deobf: Option<&crate::deobfuscate::DeobfData>,
vdata: Option<&str>,
) -> Result<MapResult<Playlist>, ExtractionError> {
let (Some(contents), Some(header)) = (self.contents, self.header) else {
return Err(response::alerts_to_err(ctx.id, self.alerts));
return Err(response::alerts_to_err(id, self.alerts));
};
let video_items = contents
@ -68,7 +85,7 @@ impl MapResponse<Playlist> for response::Playlist {
.playlist_video_list_renderer
.contents;
let mut mapper = response::YouTubeListMapper::<VideoItem>::new(ctx.lang);
let mut mapper = response::YouTubeListMapper::<VideoItem>::new(lang);
mapper.map_response(video_items);
let (description, thumbnails, last_update_txt) = match self.sidebar {
@ -87,130 +104,70 @@ impl MapResponse<Playlist> for response::Playlist {
.playlist_sidebar_primary_info_renderer
.description
.filter(|d| !d.0.is_empty()),
Some(
primary
.playlist_sidebar_primary_info_renderer
.thumbnail_renderer
.playlist_video_thumbnail_renderer
.thumbnail,
),
primary
.playlist_sidebar_primary_info_renderer
.thumbnail_renderer
.playlist_video_thumbnail_renderer
.thumbnail,
primary
.playlist_sidebar_primary_info_renderer
.stats
.try_swap_remove(2),
)
}
None => (None, None, None),
None => {
let header_banner = header
.playlist_header_renderer
.playlist_header_banner
.ok_or(ExtractionError::InvalidData(Cow::Borrowed(
"no thumbnail found",
)))?;
let mut byline = header.playlist_header_renderer.byline;
let last_update_txt = byline
.try_swap_remove(1)
.map(|b| b.playlist_byline_renderer.text);
(
None,
header_banner.hero_playlist_thumbnail_renderer.thumbnail,
last_update_txt,
)
}
};
let (name, playlist_id, channel, n_videos_txt, description2, thumbnails2, last_update_txt2) =
match header {
response::playlist::Header::PlaylistHeaderRenderer(header_renderer) => {
let mut byline = header_renderer.byline;
let last_update_txt = byline
.try_swap_remove(1)
.map(|b| b.playlist_byline_renderer.text);
(
header_renderer.title,
header_renderer.playlist_id,
header_renderer
.owner_text
.and_then(|link| ChannelId::try_from(link).ok()),
header_renderer.num_videos_text,
header_renderer
.description_text
.map(|text| TextComponents(vec![TextComponent::new(text)])),
header_renderer
.playlist_header_banner
.map(|b| b.hero_playlist_thumbnail_renderer.thumbnail),
last_update_txt,
)
}
response::playlist::Header::PageHeaderRenderer(content_renderer) => {
let h = content_renderer.content.page_header_view_model;
let rows = h.metadata.content_metadata_view_model.metadata_rows;
let n_videos_txt = rows
.get(1)
.and_then(|r| r.metadata_parts.get(1))
.map(|p| p.as_str().to_owned())
.ok_or(ExtractionError::InvalidData("no video count".into()))?;
let mut channel = rows
.into_iter()
.next()
.and_then(|r| r.metadata_parts.into_iter().next())
.and_then(|p| match p {
response::MetadataPart::Text(_) => None,
response::MetadataPart::AvatarStack {
avatar_stack_view_model,
} => ChannelId::try_from(avatar_stack_view_model.text).ok(),
});
// remove "by" prefix
if let Some(c) = channel.as_mut() {
let entry = dictionary::entry(ctx.lang);
let n = c.name.strip_prefix(entry.chan_prefix).unwrap_or(&c.name);
let n = n.strip_suffix(entry.chan_suffix).unwrap_or(n);
c.name = n.trim().to_owned();
}
let playlist_id = h
.actions
.flexible_actions_view_model
.actions_rows
.into_iter()
.next()
.and_then(|r| r.actions.into_iter().next())
.and_then(|a| {
a.button_view_model
.on_tap
.innertube_command
.into_playlist_id()
})
.ok_or(ExtractionError::InvalidData("no playlist id".into()))?;
(
h.title.dynamic_text_view_model.text,
playlist_id,
channel,
n_videos_txt,
h.description.description_preview_view_model.description,
h.hero_image.content_preview_image_view_model.image.into(),
None,
)
}
};
let n_videos = if mapper.ctoken.is_some() {
util::parse_numeric(&n_videos_txt)
.map_err(|_| ExtractionError::InvalidData("no video count".into()))?
util::parse_numeric(&header.playlist_header_renderer.num_videos_text)
.map_err(|_| ExtractionError::InvalidData(Cow::Borrowed("no video count")))?
} else {
mapper.items.len() as u64
};
if playlist_id != ctx.id {
let playlist_id = header.playlist_header_renderer.playlist_id;
if playlist_id != id {
return Err(ExtractionError::WrongResult(format!(
"got wrong playlist id {}, expected {}",
playlist_id, ctx.id
"got wrong playlist id {playlist_id}, expected {id}"
)));
}
let description = description.or(description2).map(RichText::from);
let thumbnails = thumbnails
.or(thumbnails2)
.ok_or(ExtractionError::InvalidData(Cow::Borrowed(
"no thumbnail found",
)))?;
let last_update = last_update_txt
.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,
)
let name = header.playlist_header_renderer.title;
let description = description
.or_else(|| {
header
.playlist_header_renderer
.description_text
.map(|text| TextComponents(vec![TextComponent::Text { text }]))
})
.map(RichText::from);
let channel = header
.playlist_header_renderer
.owner_text
.and_then(|link| ChannelId::try_from(link).ok());
let last_update = last_update_txt.as_ref().and_then(|txt| {
timeago::parse_textual_date_or_warn(lang, txt, &mut mapper.warnings)
.map(OffsetDateTime::date)
});
});
Ok(MapResult {
c: Playlist {
@ -220,9 +177,8 @@ impl MapResponse<Playlist> for response::Playlist {
Some(n_videos),
mapper.items,
mapper.ctoken,
ctx.visitor_data.map(str::to_owned),
vdata.map(str::to_owned),
ContinuationEndpoint::Browse,
ctx.authenticated,
),
video_count: n_videos,
thumbnail: thumbnails.into(),
@ -233,7 +189,7 @@ impl MapResponse<Playlist> for response::Playlist {
visitor_data: self
.response_context
.visitor_data
.or_else(|| ctx.visitor_data.map(str::to_owned)),
.or_else(|| vdata.map(str::to_owned)),
},
warnings: mapper.warnings,
})
@ -247,7 +203,7 @@ mod tests {
use path_macro::path;
use rstest::rstest;
use crate::util::tests::TESTFILES;
use crate::{param::Language, util::tests::TESTFILES};
use super::*;
@ -256,15 +212,13 @@ mod tests {
#[case::long("long", "PL5dDx681T4bR7ZF1IuWzOv1omlRbE7PiJ")]
#[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();
let playlist: response::Playlist =
serde_json::from_reader(BufReader::new(json_file)).unwrap();
let map_res = playlist.map_response(&MapRespCtx::test(id)).unwrap();
let map_res = playlist.map_response(id, Language::En, None, None).unwrap();
assert!(
map_res.warnings.is_empty(),

View file

@ -2,14 +2,10 @@ 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, ResponseContext, Thumbnails, TwoColumnBrowseResults,
};
use crate::serializer::text::{AttributedText, Text, TextComponent};
#[serde_as]
#[derive(Debug, Deserialize)]
@ -80,7 +76,7 @@ pub(crate) enum Header {
C4TabbedHeaderRenderer(HeaderRenderer),
/// Used for special channels like YouTube Music
CarouselHeaderRenderer(ContentsRenderer<CarouselHeaderRendererItem>),
PageHeaderRenderer(ContentRenderer<PageHeaderRendererContent<PageHeaderRendererInner>>),
PageHeaderRenderer(ContentRenderer<PageHeaderRenderer>),
}
#[serde_as]
@ -99,6 +95,11 @@ pub(crate) struct HeaderRenderer {
pub badges: Vec<ChannelBadge>,
#[serde(default)]
pub banner: Thumbnails,
#[serde(default)]
pub mobile_banner: Thumbnails,
/// Fullscreen (16:9) channel banner
#[serde(default)]
pub tv_banner: Thumbnails,
}
#[serde_as]
@ -118,18 +119,18 @@ pub(crate) enum CarouselHeaderRendererItem {
None,
}
#[serde_as]
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub(crate) struct PageHeaderRenderer {
pub page_header_view_model: PageHeaderRendererInner,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub(crate) struct PageHeaderRendererInner {
/// Channel title (only used to extract verification badges)
#[serde_as(as = "DefaultOnError")]
pub title: Option<PhTitleView>,
/// Channel avatar
pub title: PhTitleView,
pub image: PhAvatarView,
/// Channel metadata (subscribers, video count)
pub metadata: PhMetadataView,
#[serde(default)]
pub banner: PhBannerView,
}
@ -139,20 +140,72 @@ pub(crate) struct PhTitleView {
pub dynamic_text_view_model: PhTitleView2,
}
#[derive(Default, Debug, Deserialize)]
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub(crate) struct PhTitleView2 {
pub text: PhTitleView3,
}
#[serde_as]
#[derive(Default, Debug, Deserialize)]
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub(crate) struct PhTitleView3 {
#[serde_as(as = "VecSkipError<_>")]
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,15 +215,53 @@ pub(crate) struct PhAvatarView {
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub(crate) struct PhAvatarView2 {
pub avatar: AvatarViewModel,
pub avatar: PhAvatarView3,
}
#[derive(Default, Debug, Deserialize)]
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub(crate) struct PhAvatarView3 {
pub avatar_view_model: ImageView,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub(crate) struct ImageView {
pub image: Thumbnails,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub(crate) struct PhMetadataView {
pub content_metadata_view_model: PhMetadataView2,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub(crate) struct PhMetadataView2 {
pub metadata_rows: Vec<PhMetadataRow>,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub(crate) struct PhMetadataRow {
pub metadata_parts: Vec<TextWrap>,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub(crate) struct PhBannerView {
pub image_banner_view_model: ImageView,
}
#[serde_as]
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub(crate) struct TextWrap {
#[serde_as(deserialize_as = "Text")]
pub text: String,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub(crate) struct Metadata {
@ -275,9 +366,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()
}
}

View file

@ -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>>,
}

View file

@ -30,11 +30,11 @@ 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;
pub(crate) use search::SearchSuggestion;
pub(crate) use trends::Startpage;
pub(crate) use trends::Trending;
pub(crate) use url_endpoint::ResolvedUrl;
pub(crate) use video_details::VideoComments;
@ -47,17 +47,7 @@ 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;
use serde::{
@ -67,8 +57,7 @@ use serde::{
use serde_with::{serde_as, DisplayFromStr, VecSkipError};
use crate::error::ExtractionError;
use crate::serializer::text::{AttributedText, Text, TextComponent};
use crate::serializer::{MapResult, VecSkipErrorWrap};
use crate::serializer::{text::Text, MapResult, VecSkipErrorWrap};
use self::video_item::YouTubeListRenderer;
@ -117,18 +106,6 @@ pub(crate) struct ThumbnailsWrap {
pub thumbnail: Thumbnails,
}
#[derive(Default, Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
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 +129,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 +141,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 +181,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 +336,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 +359,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(),
@ -530,196 +374,3 @@ pub(crate) fn alerts_to_err(id: &str, alerts: Option<Vec<Alert>>) -> ExtractionE
.unwrap_or_default(),
}
}
// FRAMEWORK UPDATES
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub(crate) struct FrameworkUpdates<T> {
pub entity_batch_update: EntityBatchUpdate<T>,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub(crate) struct EntityBatchUpdate<T> {
pub mutations: FrameworkUpdateMutations<T>,
}
/// List of update mutations that deserializes into a HashMap (entity_key => payload)
#[derive(Debug)]
pub(crate) struct FrameworkUpdateMutations<T> {
pub items: HashMap<String, T>,
pub warnings: Vec<String>,
}
impl<'de, T> Deserialize<'de> for FrameworkUpdateMutations<T>
where
T: Deserialize<'de>,
{
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
struct SeqVisitor<T>(PhantomData<T>);
#[derive(serde::Deserialize)]
#[serde(untagged)]
enum MutationOrError<T> {
#[serde(rename_all = "camelCase")]
Good {
entity_key: String,
payload: T,
},
Error(serde_json::Value),
}
impl<'de, T> Visitor<'de> for SeqVisitor<T>
where
T: Deserialize<'de>,
{
type Value = FrameworkUpdateMutations<T>;
fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
formatter.write_str("sequence of entity mutations")
}
fn visit_seq<A>(self, mut seq: A) -> Result<Self::Value, A::Error>
where
A: serde::de::SeqAccess<'de>,
{
let mut items = HashMap::with_capacity(seq.size_hint().unwrap_or_default());
let mut warnings = Vec::new();
while let Some(value) = seq.next_element::<MutationOrError<T>>()? {
match value {
MutationOrError::Good {
entity_key,
payload,
} => {
items.insert(entity_key, payload);
}
MutationOrError::Error(value) => {
warnings.push(format!(
"error deserializing item: {}",
serde_json::to_string(&value).unwrap_or_default()
));
}
}
}
Ok(FrameworkUpdateMutations { items, warnings })
}
}
deserializer.deserialize_seq(SeqVisitor(PhantomData::<T>))
}
}
// PAGE HEADER
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub(crate) struct PageHeaderRendererContent<T> {
pub page_header_view_model: T,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
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>,
}
#[serde_as]
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub(crate) struct PhMetadataRow {
#[serde_as(as = "VecSkipError<_>")]
pub metadata_parts: Vec<MetadataPart>,
}
#[serde_as]
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub(crate) enum MetadataPart {
Text(#[serde_as(as = "AttributedText")] TextComponent),
#[serde(rename_all = "camelCase")]
AvatarStack {
avatar_stack_view_model: TextComponentBox,
},
}
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::AvatarStack {
avatar_stack_view_model,
} => avatar_stack_view_model.text.as_str(),
}
}
}
#[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>,
}
#[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 {}

View file

@ -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)]

View file

@ -1,13 +1,14 @@
use serde::Deserialize;
use serde_with::{serde_as, DefaultOnError, VecSkipError};
use serde_with::serde_as;
use serde_with::DefaultOnError;
use crate::serializer::text::Text;
use super::AlertRenderer;
use super::ContentsRenderer;
use super::TextBox;
use super::{
music_item::{ItemSection, PlaylistPanelRenderer},
ContentRenderer,
ContentRenderer, SectionList,
};
/// Response model for YouTube Music track details
@ -35,11 +36,9 @@ pub(crate) struct TabbedRenderer {
pub watch_next_tabbed_results_renderer: TabbedRendererInner,
}
#[serde_as]
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub(crate) struct TabbedRendererInner {
#[serde_as(as = "VecSkipError<_>")]
pub tabs: Vec<Tab>,
}
@ -108,14 +107,14 @@ pub(crate) struct PlaylistPanel {
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub(crate) struct MusicLyrics {
pub contents: ListOrMessage<LyricsSection>,
pub contents: LyricsContents,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub(crate) enum ListOrMessage<T> {
SectionListRenderer(ContentsRenderer<T>),
MessageRenderer(TextBox),
pub(crate) struct LyricsContents {
pub message_renderer: Option<AlertRenderer>,
pub section_list_renderer: Option<ContentsRenderer<LyricsSection>>,
}
#[derive(Debug, Deserialize)]
@ -137,14 +136,5 @@ pub(crate) struct LyricsRenderer {
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub(crate) struct MusicRelated {
pub contents: ListOrMessage<ItemSection>,
}
impl<T> ListOrMessage<T> {
pub fn into_res(self) -> Result<Vec<T>, String> {
match self {
ListOrMessage::SectionListRenderer(c) => Ok(c.contents),
ListOrMessage::MessageRenderer(msg) => Err(msg.text),
}
}
pub contents: SectionList<ItemSection>,
}

View file

@ -1,8 +0,0 @@
use serde::Deserialize;
use super::music_playlist::Contents;
#[derive(Debug, Deserialize)]
pub(crate) struct MusicHistory {
pub contents: Contents,
}

View file

@ -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]
@ -181,9 +169,6 @@ pub(crate) struct ListMusicItem {
#[serde_as(as = "Option<Text>")]
pub index: Option<String>,
pub menu: Option<MusicItemMenu>,
#[serde(default)]
#[serde_as(deserialize_as = "VecSkipError<_>")]
pub badges: Vec<TrackBadge>,
}
#[derive(Default, Debug, Copy, Clone, Deserialize)]
@ -284,7 +269,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 +318,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)]
@ -351,7 +332,6 @@ pub(crate) enum ContinuationContents {
MusicShelfContinuation(MusicShelf),
SectionListContinuation(ContentsRenderer<ItemSection>),
PlaylistPanelContinuation(PlaylistPanelRenderer),
GridContinuation(GridRenderer),
}
#[derive(Debug, Deserialize)]
@ -398,21 +378,25 @@ pub(crate) struct Grid {
pub grid_renderer: GridRenderer,
}
#[serde_as]
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub(crate) struct GridRenderer {
pub items: MapResult<Vec<MusicResponseItem>>,
pub header: Option<GridHeader>,
#[serde(default)]
#[serde_as(as = "VecSkipError<_>")]
pub continuations: Vec<MusicContinuationData>,
}
#[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,26 +411,12 @@ pub(crate) struct SimpleHeader {
pub music_header_renderer: SimpleHeaderRenderer,
}
#[serde_as]
#[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,
pub(crate) struct SimpleHeaderRenderer {
#[serde_as(as = "Text")]
pub title: String,
}
/*
@ -459,13 +429,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 +449,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 +461,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 +474,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 +487,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 +502,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 +521,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();
@ -637,15 +588,6 @@ impl MusicListMapper {
view_count: Option<TextComponents>,
}
// Dont map music livestreams
if item
.badges
.iter()
.any(|b| matches!(b, TrackBadge::LiveBadgeRenderer {}))
{
return Ok(None);
}
let p = match item.flex_column_display_style {
// Search result
FlexColumnDisplayStyle::TwoLines => {
@ -779,7 +721,7 @@ impl MusicListMapper {
.unwrap_or_default()
}))
{
artists.clone_from(fb_artists);
artists = fb_artists.clone();
}
}
@ -794,7 +736,7 @@ impl MusicListMapper {
artist_id,
album,
view_count,
track_type: vtype.into(),
is_video: vtype.is_video(),
track_nr,
by_va,
}));
@ -802,16 +744,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)
@ -853,7 +787,7 @@ impl MusicListMapper {
// fall back to menu data
if let Some(a1) = artists.first_mut() {
if a1.id.is_none() {
a1.id.clone_from(&artist_id);
a1.id = artist_id.clone();
}
}
@ -872,7 +806,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 +832,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 +894,7 @@ impl MusicListMapper {
artists,
album: None,
view_count,
track_type: vtype.into(),
is_video: vtype.is_video(),
track_nr: None,
by_va,
}));
@ -999,7 +919,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 +966,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 +983,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 +1057,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 +1094,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 +1118,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 +1183,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 +1196,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 +1233,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 +1303,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 +1324,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}");
}
}
}

View file

@ -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,
}

View file

@ -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]
@ -37,7 +33,8 @@ pub(crate) enum PlayabilityStatus {
#[serde(default)]
reason: String,
#[serde(default)]
error_screen: ErrorScreen,
#[serde_as(deserialize_as = "DefaultOnError")]
error_screen: Option<ErrorScreen>,
},
/// Age limit / Private video
#[serde(rename_all = "camelCase")]
@ -60,18 +57,17 @@ pub(crate) enum PlayabilityStatus {
},
}
#[serde_as]
#[derive(Default, Debug, Deserialize)]
#[derive(Debug, Deserialize)]
pub(crate) struct Empty {}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub(crate) struct ErrorScreen {
#[serde(default)]
#[serde_as(deserialize_as = "DefaultOnError")]
pub player_error_message_renderer: Option<ErrorMessage>,
pub player_captcha_view_model: Option<Empty>,
pub player_error_message_renderer: ErrorMessage,
}
#[serde_as]
#[derive(Default, Debug, Deserialize)]
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub(crate) struct ErrorMessage {
#[serde_as(as = "Text")]
@ -92,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]
@ -144,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 {
@ -165,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,
@ -179,19 +168,17 @@ 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,
#[serde(rename = "AUDIO_QUALITY_LOW")]
#[serde(rename = "AUDIO_QUALITY_LOW", alias = "low")]
Low,
#[serde(rename = "AUDIO_QUALITY_MEDIUM")]
#[serde(rename = "AUDIO_QUALITY_MEDIUM", alias = "medium")]
Medium,
#[serde(rename = "AUDIO_QUALITY_HIGH")]
#[serde(rename = "AUDIO_QUALITY_HIGH", alias = "high")]
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]
@ -206,7 +193,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]
@ -214,24 +201,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 {
@ -267,7 +236,7 @@ pub(crate) struct CaptionTrack {
#[serde(rename_all = "camelCase")]
pub(crate) struct VideoDetails {
pub video_id: String,
pub title: Option<String>,
pub title: String,
#[serde_as(as = "DisplayFromStr")]
pub length_seconds: u32,
#[serde(default)]
@ -276,9 +245,9 @@ pub(crate) struct VideoDetails {
pub short_description: Option<String>,
#[serde(default)]
pub thumbnail: Thumbnails,
#[serde_as(as = "Option<DisplayFromStr>")]
pub view_count: Option<u64>,
pub author: Option<String>,
#[serde_as(as = "DisplayFromStr")]
pub view_count: u64,
pub author: String,
pub is_live_content: bool,
}
@ -293,57 +262,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,
}

View file

@ -1,12 +1,11 @@
use serde::Deserialize;
use serde_with::{serde_as, DefaultOnError, VecSkipError};
use serde_with::{serde_as, DefaultOnError};
use crate::serializer::text::{AttributedText, Text, TextComponent, TextComponents};
use crate::serializer::text::{Text, TextComponent, TextComponents};
use super::{
url_endpoint::OnTapWrap, video_item::YouTubeListRenderer, Alert, ContentRenderer,
ContentsRenderer, ImageView, PageHeaderRendererContent, PhMetadataView, ResponseContext,
SectionList, Tab, TextBox, ThumbnailsWrap, TwoColumnBrowseResults,
video_item::YouTubeListRenderer, Alert, ContentsRenderer, ResponseContext, SectionList, Tab,
ThumbnailsWrap, TwoColumnBrowseResults,
};
#[serde_as]
@ -36,9 +35,8 @@ pub(crate) struct PlaylistVideoListRenderer {
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub(crate) enum Header {
PlaylistHeaderRenderer(HeaderRenderer),
PageHeaderRenderer(ContentRenderer<PageHeaderRendererContent<PageHeaderRendererInner>>),
pub(crate) struct Header {
pub playlist_header_renderer: HeaderRenderer,
}
#[serde_as]
@ -70,7 +68,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)]
@ -105,73 +111,3 @@ pub(crate) struct PlaylistThumbnailRenderer {
#[serde(alias = "playlistCustomThumbnailRenderer")]
pub playlist_video_thumbnail_renderer: ThumbnailsWrap,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub(crate) struct PageHeaderRendererInner {
pub title: PhTitleView,
pub metadata: PhMetadataView,
pub actions: PhActions,
pub description: PhDescription,
pub hero_image: PhHeroImage,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub(crate) struct PhDescription {
pub description_preview_view_model: PhDescription2,
}
#[serde_as]
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub(crate) struct PhDescription2 {
#[serde_as(as = "Option<AttributedText>")]
pub description: Option<TextComponents>,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub(crate) struct PhHeroImage {
pub content_preview_image_view_model: ImageView,
}
#[derive(Default, Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub(crate) struct PhTitleView {
pub dynamic_text_view_model: PhTitleInner,
}
#[serde_as]
#[derive(Default, Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub(crate) struct PhTitleInner {
#[serde_as(as = "AttributedText")]
pub text: String,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub(crate) struct PhActions {
pub flexible_actions_view_model: PhActions2,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub(crate) struct PhActions2 {
pub actions_rows: Vec<ActionsRow>,
}
#[serde_as]
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub(crate) struct ActionsRow {
#[serde_as(as = "VecSkipError<_>")]
pub actions: Vec<ButtonAction>,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub(crate) struct ButtonAction {
pub button_view_model: OnTapWrap,
}

View file

@ -1,6 +1,13 @@
use serde::Deserialize;
use super::{video_item::YouTubeListRendererWrap, Tab, TwoColumnBrowseResults};
use super::{video_item::YouTubeListRendererWrap, ResponseContext, Tab, TwoColumnBrowseResults};
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub(crate) struct Startpage {
pub contents: Contents,
pub response_context: ResponseContext,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]

View file

@ -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,22 +154,10 @@ 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]
#[serde(rename = "MUSIC_VIDEO_TYPE_OMV", alias = "MUSIC_VIDEO_TYPE_UGC")]
#[serde(rename = "MUSIC_VIDEO_TYPE_OMV")]
Video,
#[serde(rename = "MUSIC_VIDEO_TYPE_ATV")]
Track,
@ -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,
}),
}
}
@ -370,27 +333,4 @@ impl NavigationEndpoint {
None
}
}
pub(crate) fn into_playlist_id(self) -> Option<String> {
match self {
NavigationEndpoint::Watch { watch_endpoint } => watch_endpoint.playlist_id,
NavigationEndpoint::Browse {
browse_endpoint,
command_metadata,
} => Some(browse_endpoint.browse_id).filter(|_| {
browse_endpoint
.browse_endpoint_context_supported_configs
.map(|c| c.browse_endpoint_context_music_config.page_type == PageType::Playlist)
.unwrap_or_default()
|| command_metadata
.map(|c| c.web_command_metadata.web_page_type == PageType::Playlist)
.unwrap_or_default()
}),
NavigationEndpoint::Url { .. } => None,
NavigationEndpoint::WatchPlaylist {
watch_playlist_endpoint,
} => Some(watch_playlist_endpoint.playlist_id),
NavigationEndpoint::CreatePlaylist { .. } => None,
}
}
}

View file

@ -3,8 +3,9 @@
use serde::Deserialize;
use serde_with::{rust::deserialize_ignore_any, serde_as, DefaultOnError, VecSkipError};
use crate::serializer::text::TextComponent;
use crate::serializer::{
text::{AccessibilityText, AttributedText, Text, TextComponent, TextComponents},
text::{AccessibilityText, AttributedText, Text, TextComponents},
MapResult,
};
@ -12,10 +13,7 @@ use super::{
url_endpoint::BrowseEndpointWrap, ContinuationEndpoint, ContinuationItemRenderer, Icon,
MusicContinuationData, Thumbnails,
};
use super::{
ChannelBadge, ContentsRendererLogged, FrameworkUpdates, ImageView, ResponseContext,
YouTubeListItem,
};
use super::{ChannelBadge, ContentsRendererLogged, ResponseContext, YouTubeListItem};
/*
#VIDEO DETAILS
@ -478,7 +476,6 @@ pub(crate) struct VideoComments {
/// - n*commentRenderer, continuationItemRenderer:
/// replies + continuation
pub on_response_received_endpoints: MapResult<Vec<CommentsContItem>>,
pub framework_updates: Option<FrameworkUpdates<Payload>>,
}
/// Video comments continuation
@ -501,13 +498,23 @@ pub(crate) struct AppendComments {
#[serde(rename_all = "camelCase")]
pub(crate) enum CommentListItem {
/// Top-level comment
CommentThreadRenderer(CommentThreadRenderer),
#[serde(rename_all = "camelCase")]
CommentThreadRenderer {
comment: Comment,
/// Continuation token to fetch replies
#[serde(default)]
replies: Replies,
#[serde(default)]
#[serde_as(deserialize_as = "DefaultOnError")]
rendering_priority: CommentPriority,
},
/// Reply comment
CommentRenderer(CommentRenderer),
/// Reply comment (A/B #14)
CommentViewModel(CommentViewModel),
/// Continuation token to fetch more comments
ContinuationItemRenderer(ContinuationItemVariants),
#[serde(rename_all = "camelCase")]
ContinuationItemRenderer {
continuation_endpoint: ContinuationEndpoint,
},
/// Header of the comment section (contains number of comments)
#[serde(rename_all = "camelCase")]
CommentsHeaderRenderer {
@ -517,45 +524,6 @@ pub(crate) enum CommentListItem {
},
}
#[derive(Debug, Deserialize)]
#[serde(untagged)]
pub(crate) enum ContinuationItemVariants {
#[serde(rename_all = "camelCase")]
Ep {
continuation_endpoint: ContinuationEndpoint,
},
Btn {
button: ContinuationButton,
},
}
impl ContinuationItemVariants {
pub fn into_token(self) -> Option<String> {
match self {
ContinuationItemVariants::Ep {
continuation_endpoint,
} => continuation_endpoint,
ContinuationItemVariants::Btn { button } => button.button_renderer.command,
}
.into_token()
}
}
#[serde_as]
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub(crate) struct CommentThreadRenderer {
/// Missing on the FrameworkUpdate data model (A/B #14)
pub comment: Option<Comment>,
pub comment_view_model: Option<CommentViewModelWrap>,
/// Continuation token to fetch replies
#[serde(default)]
pub replies: Replies,
#[serde(default)]
#[serde_as(deserialize_as = "DefaultOnError")]
pub rendering_priority: CommentPriority,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub(crate) struct Comment {
@ -596,7 +564,7 @@ pub(crate) struct CommentRenderer {
pub action_buttons: CommentActionButtons,
}
#[derive(Default, Clone, Copy, Debug, Deserialize)]
#[derive(Default, Clone, Copy, Debug, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
pub(crate) enum CommentPriority {
/// Default rendering priority
@ -606,27 +574,6 @@ pub(crate) enum CommentPriority {
RenderingPriorityPinnedComment,
}
impl From<CommentPriority> for bool {
fn from(value: CommentPriority) -> Self {
matches!(value, CommentPriority::RenderingPriorityPinnedComment)
}
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub(crate) struct CommentViewModelWrap {
pub comment_view_model: CommentViewModel,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub(crate) struct CommentViewModel {
pub comment_id: String,
pub comment_key: String,
pub comment_surface_key: String,
pub toolbar_state_key: String,
}
/// Does not contain replies directly but a continuation token
/// for fetching them.
#[derive(Default, Debug, Deserialize)]
@ -690,107 +637,3 @@ pub(crate) struct AuthorCommentBadgeRenderer {
/// Artist: `OFFICIAL_ARTIST_BADGE`
pub icon: Icon,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub(crate) enum Payload {
CommentEntityPayload(CommentEntityPayload),
CommentSurfaceEntityPayload(CommentSurfaceEntityPayload),
#[serde(rename_all = "camelCase")]
EngagementToolbarStateEntityPayload {
heart_state: HeartState,
},
#[serde(other, deserialize_with = "deserialize_ignore_any")]
None,
}
#[serde_as]
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub(crate) struct CommentEntityPayload {
pub properties: CommentProperties,
#[serde(default)]
#[serde_as(as = "DefaultOnError")]
pub author: Option<CommentAuthor>,
pub toolbar: CommentToolbar,
#[serde(default)]
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")]
pub(crate) struct CommentProperties {
#[serde_as(as = "AttributedText")]
pub content: TextComponents,
pub published_time: String,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub(crate) struct CommentAuthor {
pub channel_id: String,
pub display_name: String,
#[serde(default)]
pub is_verified: bool,
#[serde(default)]
pub is_artist: bool,
#[serde(default)]
pub is_creator: bool,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub(crate) struct CommentToolbar {
pub like_count_notliked: String,
pub reply_count: String,
}
#[derive(Debug, Copy, Clone, Deserialize)]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
pub(crate) enum HeartState {
ToolbarHeartStateUnhearted,
ToolbarHeartStateHearted,
}
impl From<HeartState> for bool {
fn from(value: HeartState) -> Self {
match value {
HeartState::ToolbarHeartStateUnhearted => false,
HeartState::ToolbarHeartStateHearted => true,
}
}
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub(crate) struct ContinuationButton {
pub button_renderer: ContinuationButtonRenderer,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
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,
}

View file

@ -4,22 +4,20 @@ 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},
text::{Text, TextComponent},
MapResult,
},
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")]
@ -27,7 +25,6 @@ pub(crate) enum YouTubeListItem {
#[serde(alias = "gridVideoRenderer", alias = "compactVideoRenderer")]
VideoRenderer(VideoRenderer),
ReelItemRenderer(ReelItemRenderer),
ShortsLockupViewModel(ShortsLockupViewModel),
PlaylistVideoRenderer(PlaylistVideoRenderer),
#[serde(alias = "gridPlaylistRenderer")]
@ -35,11 +32,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 +63,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>>,
},
@ -146,66 +142,6 @@ pub(crate) struct ReelItemRenderer {
pub navigation_endpoint: Option<ReelNavigationEndpoint>,
}
// New short video item
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub(crate) struct ShortsLockupViewModel {
/// `shorts-shelf-item-[video_id]`
pub entity_id: String,
pub thumbnail: Thumbnails,
pub overlay_metadata: ShortsOverlayMetadata,
}
#[serde_as]
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub(crate) struct ShortsOverlayMetadata {
/// Title
#[serde_as(as = "AttributedText")]
pub primary_text: String,
/// View count
#[serde_as(as = "Option<AttributedText>")]
pub secondary_text: Option<String>,
}
/// 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 +234,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")]
@ -516,22 +445,23 @@ impl<T> YouTubeListMapper<T> {
VideoItem {
id: video.video_id,
name: video.title,
duration: length_text.and_then(|txt| util::parse_video_length(&txt)),
length: length_text.and_then(|txt| util::parse_video_length(&txt)),
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
@ -570,7 +500,7 @@ impl<T> YouTubeListMapper<T> {
VideoItem {
id: video.video_id,
name: video.headline,
duration: None,
length: None,
thumbnail: video.thumbnail.into(),
channel: self.channel.clone(),
publish_date: pub_date_txt.as_ref().and_then(|txt| {
@ -587,33 +517,17 @@ impl<T> YouTubeListMapper<T> {
}
}
fn map_short_video2(&mut self, video: ShortsLockupViewModel) -> Option<VideoItem> {
if let Some(video_id) = video.entity_id.strip_prefix("shorts-shelf-item-") {
Some(VideoItem {
id: video_id.to_owned(),
name: video.overlay_metadata.primary_text,
duration: None,
thumbnail: video.thumbnail.into(),
channel: self.channel.clone(),
publish_date: None,
publish_date_txt: None,
view_count: video.overlay_metadata.secondary_text.and_then(|txt| {
util::parse_large_numstr_or_warn(&txt, self.lang, &mut self.warnings)
}),
is_live: false,
is_short: true,
is_upcoming: false,
short_description: None,
})
} else {
self.warnings
.push(format!("invalid shorts entityId: {}", video.entity_id));
None
}
}
fn map_playlist_video(&mut self, video: PlaylistVideoRenderer) -> VideoItem {
let channel = 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()
@ -650,7 +564,7 @@ impl<T> YouTubeListMapper<T> {
VideoItem {
id: video.video_id,
name: video.title,
duration: video.length_seconds,
length: video.length_seconds,
thumbnail: video.thumbnail.into(),
channel,
publish_date,
@ -676,12 +590,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(|| {
@ -694,112 +610,31 @@ impl<T> YouTubeListMapper<T> {
fn map_channel(&mut self, channel: ChannelRenderer) -> ChannelItem {
// channel handle instead of subscriber count (A/B test 3)
let (handle, sc_txt) = if channel
let (sc_txt, vc_text) = if channel
.subscriber_count_text
.as_ref()
.map(|txt| txt.starts_with('@'))
.unwrap_or_default()
{
(channel.subscriber_count_text, channel.video_count_text)
(channel.video_count_text, None)
} else {
(None, channel.subscriber_count_text)
(channel.subscriber_count_text, channel.video_count_text)
};
ChannelItem {
id: channel.channel_id,
name: channel.title,
handle,
avatar: channel.thumbnail.into(),
verification: channel.owner_badges.into(),
subscriber_count: sc_txt.and_then(|txt| {
util::parse_large_numstr_or_warn(&txt, self.lang, &mut self.warnings)
}),
video_count: vc_text.and_then(|txt| {
util::parse_large_numstr_or_warn(&txt, self.lang, &mut self.warnings)
}),
short_description: channel.description_snippet,
}
}
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> {
@ -809,11 +644,6 @@ impl YouTubeListMapper<YouTubeItem> {
let mapped = YouTubeItem::Video(self.map_video(video));
self.items.push(mapped);
}
YouTubeListItem::ShortsLockupViewModel(video) => {
if let Some(mapped) = self.map_short_video2(video) {
self.items.push(YouTubeItem::Video(mapped));
}
}
YouTubeListItem::ReelItemRenderer(video) => {
let mapped = self.map_short_video(video);
self.items.push(YouTubeItem::Video(mapped));
@ -830,23 +660,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));
}
@ -871,32 +694,20 @@ impl YouTubeListMapper<VideoItem> {
let mapped = self.map_short_video(video);
self.items.push(mapped);
}
YouTubeListItem::ShortsLockupViewModel(video) => {
if let Some(mapped) = self.map_short_video2(video) {
self.items.push(mapped);
}
}
YouTubeListItem::PlaylistVideoRenderer(video) => {
let mapped = self.map_playlist_video(video);
self.items.push(mapped);
}
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 +719,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 +728,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));
}

View file

@ -12,24 +12,27 @@ use crate::{
param::search_filter::SearchFilter,
};
use super::{response, ClientType, MapRespCtx, MapResponse, MapResult, RustyPipeQuery};
use super::{response, ClientType, MapResponse, MapResult, RustyPipeQuery, YTContext};
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
struct QSearch<'a> {
context: YTContext<'a>,
query: &'a str,
params: &'a str,
}
impl RustyPipeQuery {
/// Search YouTube
#[tracing::instrument(skip(self), level = "error")]
#[tracing::instrument(skip(self))]
pub async fn search<T: FromYtItem, S: AsRef<str> + Debug>(
&self,
query: S,
) -> Result<SearchResult<T>, Error> {
let query = query.as_ref();
let context = self.get_context(ClientType::Desktop, true, None).await;
let request_body = QSearch {
context,
query,
params: "8AEB",
};
@ -45,14 +48,16 @@ impl RustyPipeQuery {
}
/// Search YouTube using the given [`SearchFilter`]
#[tracing::instrument(skip(self), level = "error")]
#[tracing::instrument(skip(self))]
pub async fn search_filter<T: FromYtItem, S: AsRef<str> + Debug>(
&self,
query: S,
filter: &SearchFilter,
) -> Result<SearchResult<T>, Error> {
let query = query.as_ref();
let context = self.get_context(ClientType::Desktop, true, None).await;
let request_body = QSearch {
context,
query,
params: &filter.encode(),
};
@ -68,7 +73,7 @@ impl RustyPipeQuery {
}
/// Get YouTube search suggestions
#[tracing::instrument(skip(self), level = "error")]
#[tracing::instrument(skip(self))]
pub async fn search_suggestion<S: AsRef<str> + Debug>(
&self,
query: S,
@ -98,7 +103,10 @@ impl RustyPipeQuery {
impl<T: FromYtItem> MapResponse<SearchResult<T>> for response::Search {
fn map_response(
self,
ctx: &MapRespCtx<'_>,
_id: &str,
lang: crate::param::Language,
_deobf: Option<&crate::deobfuscate::DeobfData>,
vdata: Option<&str>,
) -> Result<MapResult<SearchResult<T>>, ExtractionError> {
let items = self
.contents
@ -107,7 +115,7 @@ impl<T: FromYtItem> MapResponse<SearchResult<T>> for response::Search {
.section_list_renderer
.contents;
let mut mapper = response::YouTubeListMapper::<YouTubeItem>::new(ctx.lang);
let mut mapper = response::YouTubeListMapper::<YouTubeItem>::new(lang);
mapper.map_response(items);
Ok(MapResult {
@ -120,15 +128,14 @@ 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
.response_context
.visitor_data
.or_else(|| ctx.visitor_data.map(str::to_owned)),
.or_else(|| vdata.map(str::to_owned)),
},
warnings: mapper.warnings,
})
@ -143,8 +150,9 @@ mod tests {
use rstest::rstest;
use crate::{
client::{response, MapRespCtx, MapResponse},
client::{response, MapResponse},
model::{SearchResult, YouTubeItem},
param::Language,
serializer::MapResult,
util::tests::TESTFILES,
};
@ -160,7 +168,7 @@ mod tests {
let search: response::Search = serde_json::from_reader(BufReader::new(json_file)).unwrap();
let map_res: MapResult<SearchResult<YouTubeItem>> =
search.map_response(&MapRespCtx::test("")).unwrap();
search.map_response("", Language::En, None, None).unwrap();
assert!(
map_res.warnings.is_empty(),

View file

@ -5,9 +5,7 @@ expression: map_res.c
Channel(
id: "UC2DjFE7Xf11URZqWBigcVOQ",
name: "EEVblog",
handle: None,
subscriber_count: Some(884000),
video_count: None,
avatar: [
Thumbnail(
url: "https://yt3.ggpht.com/ytc/AMLnZu9eKk4Nd16fX4Rn1TF1G7ReluwOl6M5558FTYAM=s48-c-k-c0x00ffffff-no-rj",
@ -25,7 +23,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",
@ -57,6 +55,7 @@ Channel(
"dumpster diving",
"debunking",
],
vanity_url: Some("https://www.youtube.com/c/EevblogDave"),
banner: [
Thumbnail(
url: "https://yt3.ggpht.com/yIJ9ad80n49rK-YUcZLe_8bLmR-aGyg5ybDH_XKIc0GDWrC6s1Wzz8lxnq3_hux_5b6NHPZ9=w1060-fcrop64=1,00005a57ffffa5a8-k-c0xffffffff-no-nd-rj",
@ -89,6 +88,60 @@ Channel(
height: 424,
),
],
mobile_banner: [
Thumbnail(
url: "https://yt3.ggpht.com/yIJ9ad80n49rK-YUcZLe_8bLmR-aGyg5ybDH_XKIc0GDWrC6s1Wzz8lxnq3_hux_5b6NHPZ9=w320-fcrop64=1,32b75a57cd48a5a8-k-c0xffffffff-no-nd-rj",
width: 320,
height: 88,
),
Thumbnail(
url: "https://yt3.ggpht.com/yIJ9ad80n49rK-YUcZLe_8bLmR-aGyg5ybDH_XKIc0GDWrC6s1Wzz8lxnq3_hux_5b6NHPZ9=w640-fcrop64=1,32b75a57cd48a5a8-k-c0xffffffff-no-nd-rj",
width: 640,
height: 175,
),
Thumbnail(
url: "https://yt3.ggpht.com/yIJ9ad80n49rK-YUcZLe_8bLmR-aGyg5ybDH_XKIc0GDWrC6s1Wzz8lxnq3_hux_5b6NHPZ9=w960-fcrop64=1,32b75a57cd48a5a8-k-c0xffffffff-no-nd-rj",
width: 960,
height: 263,
),
Thumbnail(
url: "https://yt3.ggpht.com/yIJ9ad80n49rK-YUcZLe_8bLmR-aGyg5ybDH_XKIc0GDWrC6s1Wzz8lxnq3_hux_5b6NHPZ9=w1280-fcrop64=1,32b75a57cd48a5a8-k-c0xffffffff-no-nd-rj",
width: 1280,
height: 351,
),
Thumbnail(
url: "https://yt3.ggpht.com/yIJ9ad80n49rK-YUcZLe_8bLmR-aGyg5ybDH_XKIc0GDWrC6s1Wzz8lxnq3_hux_5b6NHPZ9=w1440-fcrop64=1,32b75a57cd48a5a8-k-c0xffffffff-no-nd-rj",
width: 1440,
height: 395,
),
],
tv_banner: [
Thumbnail(
url: "https://yt3.ggpht.com/yIJ9ad80n49rK-YUcZLe_8bLmR-aGyg5ybDH_XKIc0GDWrC6s1Wzz8lxnq3_hux_5b6NHPZ9=w320-fcrop64=1,00000000ffffffff-k-c0xffffffff-no-nd-rj",
width: 320,
height: 180,
),
Thumbnail(
url: "https://yt3.ggpht.com/yIJ9ad80n49rK-YUcZLe_8bLmR-aGyg5ybDH_XKIc0GDWrC6s1Wzz8lxnq3_hux_5b6NHPZ9=w854-fcrop64=1,00000000ffffffff-k-c0xffffffff-no-nd-rj",
width: 854,
height: 480,
),
Thumbnail(
url: "https://yt3.ggpht.com/yIJ9ad80n49rK-YUcZLe_8bLmR-aGyg5ybDH_XKIc0GDWrC6s1Wzz8lxnq3_hux_5b6NHPZ9=w1280-fcrop64=1,00000000ffffffff-k-c0xffffffff-no-nd-rj",
width: 1280,
height: 720,
),
Thumbnail(
url: "https://yt3.ggpht.com/yIJ9ad80n49rK-YUcZLe_8bLmR-aGyg5ybDH_XKIc0GDWrC6s1Wzz8lxnq3_hux_5b6NHPZ9=w1920-fcrop64=1,00000000ffffffff-k-c0xffffffff-no-nd-rj",
width: 1920,
height: 1080,
),
Thumbnail(
url: "https://yt3.ggpht.com/yIJ9ad80n49rK-YUcZLe_8bLmR-aGyg5ybDH_XKIc0GDWrC6s1Wzz8lxnq3_hux_5b6NHPZ9=w2120-fcrop64=1,00000000ffffffff-k-c0xffffffff-no-nd-rj",
width: 2120,
height: 1192,
),
],
has_shorts: false,
has_live: true,
visitor_data: None,
@ -98,7 +151,7 @@ Channel(
VideoItem(
id: "hhs95CI6Dsg",
name: "MARS 2020 Landing LIVE",
duration: Some(6321),
length: Some(6321),
thumbnail: [
Thumbnail(
url: "https://i.ytimg.com/vi/hhs95CI6Dsg/hqdefault.jpg?sqp=-oaymwE1CKgBEF5IVfKriqkDKAgBFQAAiEIYAXABwAEG8AEB-AHUBoAC4AOKAgwIABABGGUgZShlMA8=&rs=AOn4CLAlPp2e1tF8gyf1cJisZGTMleissg",
@ -125,7 +178,7 @@ Channel(
id: "UC2DjFE7Xf11URZqWBigcVOQ",
name: "EEVblog",
avatar: [],
verification: verified,
verification: Verified,
subscriber_count: Some(884000),
)),
publish_date: "[date]",
@ -139,7 +192,7 @@ Channel(
VideoItem(
id: "cpQk2n-wmQ4",
name: "LIVE Soldering",
duration: Some(7046),
length: Some(7046),
thumbnail: [
Thumbnail(
url: "https://i.ytimg.com/vi/cpQk2n-wmQ4/hqdefault.jpg?sqp=-oaymwEbCKgBEF5IVfKriqkDDggBFQAAiEIYAXABwAEG&rs=AOn4CLCoS3qwdY2rDbhkWJOWHisORlMKnA",
@ -166,7 +219,7 @@ Channel(
id: "UC2DjFE7Xf11URZqWBigcVOQ",
name: "EEVblog",
avatar: [],
verification: verified,
verification: Verified,
subscriber_count: Some(884000),
)),
publish_date: "[date]",
@ -180,7 +233,7 @@ Channel(
VideoItem(
id: "kIDV_XN9oA8",
name: "LIVE Soldering",
duration: Some(4353),
length: Some(4353),
thumbnail: [
Thumbnail(
url: "https://i.ytimg.com/vi/kIDV_XN9oA8/hqdefault.jpg?sqp=-oaymwEbCKgBEF5IVfKriqkDDggBFQAAiEIYAXABwAEG&rs=AOn4CLBG3KVoFpBFIYCG2mrox_kEq6Arug",
@ -207,7 +260,7 @@ Channel(
id: "UC2DjFE7Xf11URZqWBigcVOQ",
name: "EEVblog",
avatar: [],
verification: verified,
verification: Verified,
subscriber_count: Some(884000),
)),
publish_date: "[date]",
@ -221,7 +274,7 @@ Channel(
VideoItem(
id: "DWS4Qp3Yn0A",
name: "Apollo 11 Launch LIVE - 50 Years Later",
duration: Some(4560),
length: Some(4560),
thumbnail: [
Thumbnail(
url: "https://i.ytimg.com/vi/DWS4Qp3Yn0A/hqdefault.jpg?sqp=-oaymwEbCKgBEF5IVfKriqkDDggBFQAAiEIYAXABwAEG&rs=AOn4CLAFkIQ4er8qDNMlD9H8lPzfSnE99g",
@ -248,7 +301,7 @@ Channel(
id: "UC2DjFE7Xf11URZqWBigcVOQ",
name: "EEVblog",
avatar: [],
verification: verified,
verification: Verified,
subscriber_count: Some(884000),
)),
publish_date: "[date]",
@ -262,7 +315,7 @@ Channel(
VideoItem(
id: "LwjTe3SiVXg",
name: "EEVblog LIVE Q&A",
duration: Some(3943),
length: Some(3943),
thumbnail: [
Thumbnail(
url: "https://i.ytimg.com/vi/LwjTe3SiVXg/hqdefault.jpg?sqp=-oaymwEbCKgBEF5IVfKriqkDDggBFQAAiEIYAXABwAEG&rs=AOn4CLAzTlnjBJLT3KJVN4teMlX_svuaNA",
@ -289,7 +342,7 @@ Channel(
id: "UC2DjFE7Xf11URZqWBigcVOQ",
name: "EEVblog",
avatar: [],
verification: verified,
verification: Verified,
subscriber_count: Some(884000),
)),
publish_date: "[date]",
@ -303,7 +356,7 @@ Channel(
VideoItem(
id: "skPiz3GrVNs",
name: "LIVE Keysight Scope Draw #2",
duration: Some(2445),
length: Some(2445),
thumbnail: [
Thumbnail(
url: "https://i.ytimg.com/vi/skPiz3GrVNs/hqdefault.jpg?sqp=-oaymwEbCKgBEF5IVfKriqkDDggBFQAAiEIYAXABwAEG&rs=AOn4CLBFiIfUBfoL0Q9CLR9Pc8bXy-zclg",
@ -330,7 +383,7 @@ Channel(
id: "UC2DjFE7Xf11URZqWBigcVOQ",
name: "EEVblog",
avatar: [],
verification: verified,
verification: Verified,
subscriber_count: Some(884000),
)),
publish_date: "[date]",
@ -344,7 +397,7 @@ Channel(
VideoItem(
id: "HZc-Ctvgv5Y",
name: "LIVE Keysight Scope Draw",
duration: Some(6455),
length: Some(6455),
thumbnail: [
Thumbnail(
url: "https://i.ytimg.com/vi/HZc-Ctvgv5Y/hqdefault.jpg?sqp=-oaymwEbCKgBEF5IVfKriqkDDggBFQAAiEIYAXABwAEG&rs=AOn4CLDQM1_QPh6u5_BFonLCdFPz-AcpkQ",
@ -371,7 +424,7 @@ Channel(
id: "UC2DjFE7Xf11URZqWBigcVOQ",
name: "EEVblog",
avatar: [],
verification: verified,
verification: Verified,
subscriber_count: Some(884000),
)),
publish_date: "[date]",
@ -385,7 +438,7 @@ Channel(
VideoItem(
id: "5ilODYy2zGE",
name: "Ask Dave LIVE - March 8th 2019",
duration: Some(10645),
length: Some(10645),
thumbnail: [
Thumbnail(
url: "https://i.ytimg.com/vi/5ilODYy2zGE/hqdefault.jpg?sqp=-oaymwEbCKgBEF5IVfKriqkDDggBFQAAiEIYAXABwAEG&rs=AOn4CLCft4f7Lw3l3_u55bzUibWXr-UHTQ",
@ -412,7 +465,7 @@ Channel(
id: "UC2DjFE7Xf11URZqWBigcVOQ",
name: "EEVblog",
avatar: [],
verification: verified,
verification: Verified,
subscriber_count: Some(884000),
)),
publish_date: "[date]",
@ -426,7 +479,7 @@ Channel(
VideoItem(
id: "gQ7TTuiDH1M",
name: "Ask Dave LIVE - Jan 28th 2019",
duration: Some(17228),
length: Some(17228),
thumbnail: [
Thumbnail(
url: "https://i.ytimg.com/vi/gQ7TTuiDH1M/hqdefault.jpg?sqp=-oaymwEbCKgBEF5IVfKriqkDDggBFQAAiEIYAXABwAEG&rs=AOn4CLAUPZz1xzckl5xzdBRonA_1WNWIyg",
@ -453,7 +506,7 @@ Channel(
id: "UC2DjFE7Xf11URZqWBigcVOQ",
name: "EEVblog",
avatar: [],
verification: verified,
verification: Verified,
subscriber_count: Some(884000),
)),
publish_date: "[date]",
@ -467,7 +520,7 @@ Channel(
VideoItem(
id: "qpw9dKxL2Ho",
name: "LIVE KiCAD 5 PCB Design",
duration: Some(8003),
length: Some(8003),
thumbnail: [
Thumbnail(
url: "https://i.ytimg.com/vi/qpw9dKxL2Ho/hqdefault.jpg?sqp=-oaymwEbCKgBEF5IVfKriqkDDggBFQAAiEIYAXABwAEG&rs=AOn4CLAC-kI2770I7JgVCTYExG0vXoYoxA",
@ -494,7 +547,7 @@ Channel(
id: "UC2DjFE7Xf11URZqWBigcVOQ",
name: "EEVblog",
avatar: [],
verification: verified,
verification: Verified,
subscriber_count: Some(884000),
)),
publish_date: "[date]",
@ -508,7 +561,7 @@ Channel(
VideoItem(
id: "wECZoUNd2GY",
name: "EEVblog LIVE DIY TTL Computer Build",
duration: Some(14599),
length: Some(14599),
thumbnail: [
Thumbnail(
url: "https://i.ytimg.com/vi/wECZoUNd2GY/hqdefault.jpg?sqp=-oaymwEbCKgBEF5IVfKriqkDDggBFQAAiEIYAXABwAEG&rs=AOn4CLDzZwAD6bQQEaYuZEzmQ0sgQKc1yA",
@ -535,7 +588,7 @@ Channel(
id: "UC2DjFE7Xf11URZqWBigcVOQ",
name: "EEVblog",
avatar: [],
verification: verified,
verification: Verified,
subscriber_count: Some(884000),
)),
publish_date: "[date]",
@ -549,7 +602,7 @@ Channel(
VideoItem(
id: "bV99dn-tWDk",
name: "EEVblog LIVE Scope Draw",
duration: Some(2694),
length: Some(2694),
thumbnail: [
Thumbnail(
url: "https://i.ytimg.com/vi/bV99dn-tWDk/hqdefault.jpg?sqp=-oaymwEbCKgBEF5IVfKriqkDDggBFQAAiEIYAXABwAEG&rs=AOn4CLAR4ckJxAituVMFCyWpYhHXozqQRA",
@ -576,7 +629,7 @@ Channel(
id: "UC2DjFE7Xf11URZqWBigcVOQ",
name: "EEVblog",
avatar: [],
verification: verified,
verification: Verified,
subscriber_count: Some(884000),
)),
publish_date: "[date]",
@ -590,7 +643,7 @@ Channel(
VideoItem(
id: "-NGRIFiu_p0",
name: "EEVblog LIVE SHOW - End of 2017",
duration: Some(12238),
length: Some(12238),
thumbnail: [
Thumbnail(
url: "https://i.ytimg.com/vi/-NGRIFiu_p0/hqdefault.jpg?sqp=-oaymwEbCKgBEF5IVfKriqkDDggBFQAAiEIYAXABwAEG&rs=AOn4CLBjMmIdgjiSMBQ2X73h6-NtVUIqSg",
@ -617,7 +670,7 @@ Channel(
id: "UC2DjFE7Xf11URZqWBigcVOQ",
name: "EEVblog",
avatar: [],
verification: verified,
verification: Verified,
subscriber_count: Some(884000),
)),
publish_date: "[date]",
@ -631,7 +684,7 @@ Channel(
VideoItem(
id: "zgE6_x4rM5k",
name: "LIVE Show Giveaway",
duration: Some(5533),
length: Some(5533),
thumbnail: [
Thumbnail(
url: "https://i.ytimg.com/vi/zgE6_x4rM5k/hqdefault.jpg?sqp=-oaymwEbCKgBEF5IVfKriqkDDggBFQAAiEIYAXABwAEG&rs=AOn4CLBjb92wUNqOvTKs9TCLCThvdkdz3A",
@ -658,7 +711,7 @@ Channel(
id: "UC2DjFE7Xf11URZqWBigcVOQ",
name: "EEVblog",
avatar: [],
verification: verified,
verification: Verified,
subscriber_count: Some(884000),
)),
publish_date: "[date]",
@ -672,7 +725,7 @@ Channel(
VideoItem(
id: "9DjABCJN2M8",
name: "LIVE Testing of the Batteriser",
duration: Some(10747),
length: Some(10747),
thumbnail: [
Thumbnail(
url: "https://i.ytimg.com/vi/9DjABCJN2M8/hqdefault.jpg?sqp=-oaymwEbCKgBEF5IVfKriqkDDggBFQAAiEIYAXABwAEG&rs=AOn4CLBXhnnHCuNfSzHZC64KFsfHPPJDNg",
@ -699,7 +752,7 @@ Channel(
id: "UC2DjFE7Xf11URZqWBigcVOQ",
name: "EEVblog",
avatar: [],
verification: verified,
verification: Verified,
subscriber_count: Some(884000),
)),
publish_date: "[date]",
@ -713,7 +766,7 @@ Channel(
VideoItem(
id: "cAsUI2YhqN4",
name: "LIVE Unboxing of the Batteriser! (Batteroo)",
duration: Some(3102),
length: Some(3102),
thumbnail: [
Thumbnail(
url: "https://i.ytimg.com/vi/cAsUI2YhqN4/hqdefault.jpg?sqp=-oaymwEbCKgBEF5IVfKriqkDDggBFQAAiEIYAXABwAEG&rs=AOn4CLCOE1MyG1nFXs9D2qdK78bpN1mc_g",
@ -740,7 +793,7 @@ Channel(
id: "UC2DjFE7Xf11URZqWBigcVOQ",
name: "EEVblog",
avatar: [],
verification: verified,
verification: Verified,
subscriber_count: Some(884000),
)),
publish_date: "[date]",
@ -754,7 +807,7 @@ Channel(
VideoItem(
id: "CLYKwFMW9J0",
name: "Juno Live Again",
duration: Some(811),
length: Some(811),
thumbnail: [
Thumbnail(
url: "https://i.ytimg.com/vi/CLYKwFMW9J0/hqdefault.jpg?sqp=-oaymwEbCKgBEF5IVfKriqkDDggBFQAAiEIYAXABwAEG&rs=AOn4CLC7WO4HX0e7M58ddoJD5dkVjdKHYQ",
@ -781,7 +834,7 @@ Channel(
id: "UC2DjFE7Xf11URZqWBigcVOQ",
name: "EEVblog",
avatar: [],
verification: verified,
verification: Verified,
subscriber_count: Some(884000),
)),
publish_date: "[date]",
@ -795,7 +848,7 @@ Channel(
VideoItem(
id: "nV43vM9VcUA",
name: "Juno Live",
duration: Some(190),
length: Some(190),
thumbnail: [
Thumbnail(
url: "https://i.ytimg.com/vi/nV43vM9VcUA/hqdefault.jpg?sqp=-oaymwEbCKgBEF5IVfKriqkDDggBFQAAiEIYAXABwAEG&rs=AOn4CLCy-zEVPDvomCCi8YoP8Ig_Hrhzfw",
@ -822,7 +875,7 @@ Channel(
id: "UC2DjFE7Xf11URZqWBigcVOQ",
name: "EEVblog",
avatar: [],
verification: verified,
verification: Verified,
subscriber_count: Some(884000),
)),
publish_date: "[date]",
@ -836,7 +889,7 @@ Channel(
VideoItem(
id: "38uFiWzcDnc",
name: "Juno Orbital Insertion Live",
duration: Some(1731),
length: Some(1731),
thumbnail: [
Thumbnail(
url: "https://i.ytimg.com/vi/38uFiWzcDnc/hqdefault.jpg?sqp=-oaymwEbCKgBEF5IVfKriqkDDggBFQAAiEIYAXABwAEG&rs=AOn4CLALhrDygxFH4T2c-4efZqVaJnYY7g",
@ -863,7 +916,7 @@ Channel(
id: "UC2DjFE7Xf11URZqWBigcVOQ",
name: "EEVblog",
avatar: [],
verification: verified,
verification: Verified,
subscriber_count: Some(884000),
)),
publish_date: "[date]",
@ -877,7 +930,7 @@ Channel(
VideoItem(
id: "ib80yjc9VlM",
name: "Juno Jupiter Live",
duration: Some(581),
length: Some(581),
thumbnail: [
Thumbnail(
url: "https://i.ytimg.com/vi/ib80yjc9VlM/hqdefault.jpg?sqp=-oaymwEbCKgBEF5IVfKriqkDDggBFQAAiEIYAXABwAEG&rs=AOn4CLDbJJvzoEmwUc7nAm6GLJpoZJKmgQ",
@ -904,7 +957,7 @@ Channel(
id: "UC2DjFE7Xf11URZqWBigcVOQ",
name: "EEVblog",
avatar: [],
verification: verified,
verification: Verified,
subscriber_count: Some(884000),
)),
publish_date: "[date]",
@ -918,7 +971,7 @@ Channel(
VideoItem(
id: "rQRakYpb8-g",
name: "eevSTREAM: Lab Rearrangement Part 2",
duration: Some(8616),
length: Some(8616),
thumbnail: [
Thumbnail(
url: "https://i.ytimg.com/vi/rQRakYpb8-g/hqdefault.jpg?sqp=-oaymwEbCKgBEF5IVfKriqkDDggBFQAAiEIYAXABwAEG&rs=AOn4CLAdGJH0yhCQ7kmI3d3JXVv_7xzJAQ",
@ -945,7 +998,7 @@ Channel(
id: "UC2DjFE7Xf11URZqWBigcVOQ",
name: "EEVblog",
avatar: [],
verification: verified,
verification: Verified,
subscriber_count: Some(884000),
)),
publish_date: "[date]",
@ -959,7 +1012,7 @@ Channel(
VideoItem(
id: "DwLEFKu2XWg",
name: "eevSTREAM: Lab Rearrangement Part 1",
duration: Some(768),
length: Some(768),
thumbnail: [
Thumbnail(
url: "https://i.ytimg.com/vi/DwLEFKu2XWg/hqdefault.jpg?sqp=-oaymwEbCKgBEF5IVfKriqkDDggBFQAAiEIYAXABwAEG&rs=AOn4CLCXvSePgZ8NIKQTviqWvROVZFRPpA",
@ -986,7 +1039,7 @@ Channel(
id: "UC2DjFE7Xf11URZqWBigcVOQ",
name: "EEVblog",
avatar: [],
verification: verified,
verification: Verified,
subscriber_count: Some(884000),
)),
publish_date: "[date]",
@ -1000,7 +1053,7 @@ Channel(
VideoItem(
id: "VeUDXQR3F2o",
name: "Live Show",
duration: Some(10360),
length: Some(10360),
thumbnail: [
Thumbnail(
url: "https://i.ytimg.com/vi/VeUDXQR3F2o/hqdefault.jpg?sqp=-oaymwEbCKgBEF5IVfKriqkDDggBFQAAiEIYAXABwAEG&rs=AOn4CLDmgrfQXMTaGMahuP8F_UHJAomFbg",
@ -1027,7 +1080,7 @@ Channel(
id: "UC2DjFE7Xf11URZqWBigcVOQ",
name: "EEVblog",
avatar: [],
verification: verified,
verification: Verified,
subscriber_count: Some(884000),
)),
publish_date: "[date]",
@ -1041,7 +1094,7 @@ Channel(
VideoItem(
id: "PgZx25vVwoI",
name: "Live Giveaway",
duration: Some(1808),
length: Some(1808),
thumbnail: [
Thumbnail(
url: "https://i.ytimg.com/vi/PgZx25vVwoI/hqdefault.jpg?sqp=-oaymwEbCKgBEF5IVfKriqkDDggBFQAAiEIYAXABwAEG&rs=AOn4CLDTrMmoCfISxG0YSqC4oEyKGHdK_A",
@ -1068,7 +1121,7 @@ Channel(
id: "UC2DjFE7Xf11URZqWBigcVOQ",
name: "EEVblog",
avatar: [],
verification: verified,
verification: Verified,
subscriber_count: Some(884000),
)),
publish_date: "[date]",
@ -1082,7 +1135,7 @@ Channel(
VideoItem(
id: "jUtzoO-ur34",
name: "Inventables X-Carve LIVE Build Part 4",
duration: Some(10665),
length: Some(10665),
thumbnail: [
Thumbnail(
url: "https://i.ytimg.com/vi/jUtzoO-ur34/hqdefault.jpg?sqp=-oaymwEbCKgBEF5IVfKriqkDDggBFQAAiEIYAXABwAEG&rs=AOn4CLCO35sFP8D_Q08HxMZkNHFO8MmpDg",
@ -1109,7 +1162,7 @@ Channel(
id: "UC2DjFE7Xf11URZqWBigcVOQ",
name: "EEVblog",
avatar: [],
verification: verified,
verification: Verified,
subscriber_count: Some(884000),
)),
publish_date: "[date]",
@ -1123,7 +1176,7 @@ Channel(
VideoItem(
id: "199gtbX1y4M",
name: "Inventables X-Carve LIVE Build Part 3 + Batteriser Rant",
duration: Some(6267),
length: Some(6267),
thumbnail: [
Thumbnail(
url: "https://i.ytimg.com/vi/199gtbX1y4M/hqdefault.jpg?sqp=-oaymwEbCKgBEF5IVfKriqkDDggBFQAAiEIYAXABwAEG&rs=AOn4CLAg3bMS00xpSXmNn1f5hXu_jWWC1w",
@ -1150,7 +1203,7 @@ Channel(
id: "UC2DjFE7Xf11URZqWBigcVOQ",
name: "EEVblog",
avatar: [],
verification: verified,
verification: Verified,
subscriber_count: Some(884000),
)),
publish_date: "[date]",
@ -1164,7 +1217,7 @@ Channel(
VideoItem(
id: "nQH4I_p7-MI",
name: "Inventables X-Carve LIVE Build Part 2",
duration: Some(17643),
length: Some(17643),
thumbnail: [
Thumbnail(
url: "https://i.ytimg.com/vi/nQH4I_p7-MI/hqdefault.jpg?sqp=-oaymwEbCKgBEF5IVfKriqkDDggBFQAAiEIYAXABwAEG&rs=AOn4CLBMIA1YzQefFwGj5UFikXuYS2Nkng",
@ -1191,7 +1244,7 @@ Channel(
id: "UC2DjFE7Xf11URZqWBigcVOQ",
name: "EEVblog",
avatar: [],
verification: verified,
verification: Verified,
subscriber_count: Some(884000),
)),
publish_date: "[date]",
@ -1205,7 +1258,7 @@ Channel(
VideoItem(
id: "XBMNFXGKpaw",
name: "Inventables X-Carve LIVE Build",
duration: Some(5479),
length: Some(5479),
thumbnail: [
Thumbnail(
url: "https://i.ytimg.com/vi/XBMNFXGKpaw/hqdefault.jpg?sqp=-oaymwEbCKgBEF5IVfKriqkDDggBFQAAiEIYAXABwAEG&rs=AOn4CLCV980wWO8tdx0aFDXwPn9aBQ2xlA",
@ -1232,7 +1285,7 @@ Channel(
id: "UC2DjFE7Xf11URZqWBigcVOQ",
name: "EEVblog",
avatar: [],
verification: verified,
verification: Verified,
subscriber_count: Some(884000),
)),
publish_date: "[date]",
@ -1246,7 +1299,7 @@ Channel(
VideoItem(
id: "yl6DGgiE3J8",
name: "Apollo Saturn LVDC Live testing",
duration: Some(1076),
length: Some(1076),
thumbnail: [
Thumbnail(
url: "https://i.ytimg.com/vi/yl6DGgiE3J8/hqdefault.jpg?sqp=-oaymwEbCKgBEF5IVfKriqkDDggBFQAAiEIYAXABwAEG&rs=AOn4CLCugABHuqqPZQjV9cEm0JFh7R5aiA",
@ -1273,7 +1326,7 @@ Channel(
id: "UC2DjFE7Xf11URZqWBigcVOQ",
name: "EEVblog",
avatar: [],
verification: verified,
verification: Verified,
subscriber_count: Some(884000),
)),
publish_date: "[date]",
@ -1287,7 +1340,7 @@ Channel(
VideoItem(
id: "EEMcIZAcKjc",
name: "LIVE EEVblog Mailbag",
duration: Some(7344),
length: Some(7344),
thumbnail: [
Thumbnail(
url: "https://i.ytimg.com/vi/EEMcIZAcKjc/hqdefault.jpg?sqp=-oaymwEbCKgBEF5IVfKriqkDDggBFQAAiEIYAXABwAEG&rs=AOn4CLCg16HpJqC9mNwkYOf8b0cfAuNLOA",
@ -1314,7 +1367,7 @@ Channel(
id: "UC2DjFE7Xf11URZqWBigcVOQ",
name: "EEVblog",
avatar: [],
verification: verified,
verification: Verified,
subscriber_count: Some(884000),
)),
publish_date: "[date]",

View file

@ -5,9 +5,7 @@ expression: map_res.c
Channel(
id: "UC2DjFE7Xf11URZqWBigcVOQ",
name: "EEVblog",
handle: None,
subscriber_count: Some(881000),
video_count: None,
avatar: [
Thumbnail(
url: "https://yt3.ggpht.com/ytc/AMLnZu9eKk4Nd16fX4Rn1TF1G7ReluwOl6M5558FTYAM=s48-c-k-c0x00ffffff-no-rj",
@ -25,7 +23,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",
@ -57,6 +55,7 @@ Channel(
"dumpster diving",
"debunking",
],
vanity_url: Some("https://www.youtube.com/c/EevblogDave"),
banner: [
Thumbnail(
url: "https://yt3.ggpht.com/yIJ9ad80n49rK-YUcZLe_8bLmR-aGyg5ybDH_XKIc0GDWrC6s1Wzz8lxnq3_hux_5b6NHPZ9=w1060-fcrop64=1,00005a57ffffa5a8-k-c0xffffffff-no-nd-rj",
@ -89,6 +88,60 @@ Channel(
height: 424,
),
],
mobile_banner: [
Thumbnail(
url: "https://yt3.ggpht.com/yIJ9ad80n49rK-YUcZLe_8bLmR-aGyg5ybDH_XKIc0GDWrC6s1Wzz8lxnq3_hux_5b6NHPZ9=w320-fcrop64=1,32b75a57cd48a5a8-k-c0xffffffff-no-nd-rj",
width: 320,
height: 88,
),
Thumbnail(
url: "https://yt3.ggpht.com/yIJ9ad80n49rK-YUcZLe_8bLmR-aGyg5ybDH_XKIc0GDWrC6s1Wzz8lxnq3_hux_5b6NHPZ9=w640-fcrop64=1,32b75a57cd48a5a8-k-c0xffffffff-no-nd-rj",
width: 640,
height: 175,
),
Thumbnail(
url: "https://yt3.ggpht.com/yIJ9ad80n49rK-YUcZLe_8bLmR-aGyg5ybDH_XKIc0GDWrC6s1Wzz8lxnq3_hux_5b6NHPZ9=w960-fcrop64=1,32b75a57cd48a5a8-k-c0xffffffff-no-nd-rj",
width: 960,
height: 263,
),
Thumbnail(
url: "https://yt3.ggpht.com/yIJ9ad80n49rK-YUcZLe_8bLmR-aGyg5ybDH_XKIc0GDWrC6s1Wzz8lxnq3_hux_5b6NHPZ9=w1280-fcrop64=1,32b75a57cd48a5a8-k-c0xffffffff-no-nd-rj",
width: 1280,
height: 351,
),
Thumbnail(
url: "https://yt3.ggpht.com/yIJ9ad80n49rK-YUcZLe_8bLmR-aGyg5ybDH_XKIc0GDWrC6s1Wzz8lxnq3_hux_5b6NHPZ9=w1440-fcrop64=1,32b75a57cd48a5a8-k-c0xffffffff-no-nd-rj",
width: 1440,
height: 395,
),
],
tv_banner: [
Thumbnail(
url: "https://yt3.ggpht.com/yIJ9ad80n49rK-YUcZLe_8bLmR-aGyg5ybDH_XKIc0GDWrC6s1Wzz8lxnq3_hux_5b6NHPZ9=w320-fcrop64=1,00000000ffffffff-k-c0xffffffff-no-nd-rj",
width: 320,
height: 180,
),
Thumbnail(
url: "https://yt3.ggpht.com/yIJ9ad80n49rK-YUcZLe_8bLmR-aGyg5ybDH_XKIc0GDWrC6s1Wzz8lxnq3_hux_5b6NHPZ9=w854-fcrop64=1,00000000ffffffff-k-c0xffffffff-no-nd-rj",
width: 854,
height: 480,
),
Thumbnail(
url: "https://yt3.ggpht.com/yIJ9ad80n49rK-YUcZLe_8bLmR-aGyg5ybDH_XKIc0GDWrC6s1Wzz8lxnq3_hux_5b6NHPZ9=w1280-fcrop64=1,00000000ffffffff-k-c0xffffffff-no-nd-rj",
width: 1280,
height: 720,
),
Thumbnail(
url: "https://yt3.ggpht.com/yIJ9ad80n49rK-YUcZLe_8bLmR-aGyg5ybDH_XKIc0GDWrC6s1Wzz8lxnq3_hux_5b6NHPZ9=w1920-fcrop64=1,00000000ffffffff-k-c0xffffffff-no-nd-rj",
width: 1920,
height: 1080,
),
Thumbnail(
url: "https://yt3.ggpht.com/yIJ9ad80n49rK-YUcZLe_8bLmR-aGyg5ybDH_XKIc0GDWrC6s1Wzz8lxnq3_hux_5b6NHPZ9=w2120-fcrop64=1,00000000ffffffff-k-c0xffffffff-no-nd-rj",
width: 2120,
height: 1192,
),
],
has_shorts: false,
has_live: false,
visitor_data: Some("CgttaWpyTVpUN1AyZyioqr2ZBg%3D%3D"),
@ -109,7 +162,7 @@ Channel(
id: "UC2DjFE7Xf11URZqWBigcVOQ",
name: "EEVblog",
avatar: [],
verification: verified,
verification: Verified,
subscriber_count: Some(881000),
)),
video_count: Some(2),
@ -128,7 +181,7 @@ Channel(
id: "UC2DjFE7Xf11URZqWBigcVOQ",
name: "EEVblog",
avatar: [],
verification: verified,
verification: Verified,
subscriber_count: Some(881000),
)),
video_count: Some(1),
@ -147,7 +200,7 @@ Channel(
id: "UC2DjFE7Xf11URZqWBigcVOQ",
name: "EEVblog",
avatar: [],
verification: verified,
verification: Verified,
subscriber_count: Some(881000),
)),
video_count: Some(9),
@ -166,7 +219,7 @@ Channel(
id: "UC2DjFE7Xf11URZqWBigcVOQ",
name: "EEVblog",
avatar: [],
verification: verified,
verification: Verified,
subscriber_count: Some(881000),
)),
video_count: Some(2),
@ -185,7 +238,7 @@ Channel(
id: "UC2DjFE7Xf11URZqWBigcVOQ",
name: "EEVblog",
avatar: [],
verification: verified,
verification: Verified,
subscriber_count: Some(881000),
)),
video_count: Some(4),
@ -204,7 +257,7 @@ Channel(
id: "UC2DjFE7Xf11URZqWBigcVOQ",
name: "EEVblog",
avatar: [],
verification: verified,
verification: Verified,
subscriber_count: Some(881000),
)),
video_count: Some(18),
@ -223,7 +276,7 @@ Channel(
id: "UC2DjFE7Xf11URZqWBigcVOQ",
name: "EEVblog",
avatar: [],
verification: verified,
verification: Verified,
subscriber_count: Some(881000),
)),
video_count: Some(3),
@ -242,7 +295,7 @@ Channel(
id: "UC2DjFE7Xf11URZqWBigcVOQ",
name: "EEVblog",
avatar: [],
verification: verified,
verification: Verified,
subscriber_count: Some(881000),
)),
video_count: Some(8),
@ -261,7 +314,7 @@ Channel(
id: "UC2DjFE7Xf11URZqWBigcVOQ",
name: "EEVblog",
avatar: [],
verification: verified,
verification: Verified,
subscriber_count: Some(881000),
)),
video_count: Some(13),
@ -280,7 +333,7 @@ Channel(
id: "UC2DjFE7Xf11URZqWBigcVOQ",
name: "EEVblog",
avatar: [],
verification: verified,
verification: Verified,
subscriber_count: Some(881000),
)),
video_count: Some(9),
@ -299,7 +352,7 @@ Channel(
id: "UC2DjFE7Xf11URZqWBigcVOQ",
name: "EEVblog",
avatar: [],
verification: verified,
verification: Verified,
subscriber_count: Some(881000),
)),
video_count: Some(7),
@ -318,7 +371,7 @@ Channel(
id: "UC2DjFE7Xf11URZqWBigcVOQ",
name: "EEVblog",
avatar: [],
verification: verified,
verification: Verified,
subscriber_count: Some(881000),
)),
video_count: Some(3),
@ -337,7 +390,7 @@ Channel(
id: "UC2DjFE7Xf11URZqWBigcVOQ",
name: "EEVblog",
avatar: [],
verification: verified,
verification: Verified,
subscriber_count: Some(881000),
)),
video_count: Some(8),
@ -356,7 +409,7 @@ Channel(
id: "UC2DjFE7Xf11URZqWBigcVOQ",
name: "EEVblog",
avatar: [],
verification: verified,
verification: Verified,
subscriber_count: Some(881000),
)),
video_count: Some(2),
@ -375,7 +428,7 @@ Channel(
id: "UC2DjFE7Xf11URZqWBigcVOQ",
name: "EEVblog",
avatar: [],
verification: verified,
verification: Verified,
subscriber_count: Some(881000),
)),
video_count: Some(3),
@ -394,7 +447,7 @@ Channel(
id: "UC2DjFE7Xf11URZqWBigcVOQ",
name: "EEVblog",
avatar: [],
verification: verified,
verification: Verified,
subscriber_count: Some(881000),
)),
video_count: Some(10),
@ -413,7 +466,7 @@ Channel(
id: "UC2DjFE7Xf11URZqWBigcVOQ",
name: "EEVblog",
avatar: [],
verification: verified,
verification: Verified,
subscriber_count: Some(881000),
)),
video_count: Some(1),
@ -432,7 +485,7 @@ Channel(
id: "UC2DjFE7Xf11URZqWBigcVOQ",
name: "EEVblog",
avatar: [],
verification: verified,
verification: Verified,
subscriber_count: Some(881000),
)),
video_count: Some(9),
@ -451,7 +504,7 @@ Channel(
id: "UC2DjFE7Xf11URZqWBigcVOQ",
name: "EEVblog",
avatar: [],
verification: verified,
verification: Verified,
subscriber_count: Some(881000),
)),
video_count: Some(16),
@ -470,7 +523,7 @@ Channel(
id: "UC2DjFE7Xf11URZqWBigcVOQ",
name: "EEVblog",
avatar: [],
verification: verified,
verification: Verified,
subscriber_count: Some(881000),
)),
video_count: Some(7),
@ -489,7 +542,7 @@ Channel(
id: "UC2DjFE7Xf11URZqWBigcVOQ",
name: "EEVblog",
avatar: [],
verification: verified,
verification: Verified,
subscriber_count: Some(881000),
)),
video_count: Some(6),
@ -508,7 +561,7 @@ Channel(
id: "UC2DjFE7Xf11URZqWBigcVOQ",
name: "EEVblog",
avatar: [],
verification: verified,
verification: Verified,
subscriber_count: Some(881000),
)),
video_count: Some(12),
@ -527,7 +580,7 @@ Channel(
id: "UC2DjFE7Xf11URZqWBigcVOQ",
name: "EEVblog",
avatar: [],
verification: verified,
verification: Verified,
subscriber_count: Some(881000),
)),
video_count: Some(1),
@ -546,7 +599,7 @@ Channel(
id: "UC2DjFE7Xf11URZqWBigcVOQ",
name: "EEVblog",
avatar: [],
verification: verified,
verification: Verified,
subscriber_count: Some(881000),
)),
video_count: Some(5),
@ -565,7 +618,7 @@ Channel(
id: "UC2DjFE7Xf11URZqWBigcVOQ",
name: "EEVblog",
avatar: [],
verification: verified,
verification: Verified,
subscriber_count: Some(881000),
)),
video_count: Some(2),
@ -584,7 +637,7 @@ Channel(
id: "UC2DjFE7Xf11URZqWBigcVOQ",
name: "EEVblog",
avatar: [],
verification: verified,
verification: Verified,
subscriber_count: Some(881000),
)),
video_count: Some(4),
@ -603,7 +656,7 @@ Channel(
id: "UC2DjFE7Xf11URZqWBigcVOQ",
name: "EEVblog",
avatar: [],
verification: verified,
verification: Verified,
subscriber_count: Some(881000),
)),
video_count: Some(1),
@ -622,7 +675,7 @@ Channel(
id: "UC2DjFE7Xf11URZqWBigcVOQ",
name: "EEVblog",
avatar: [],
verification: verified,
verification: Verified,
subscriber_count: Some(881000),
)),
video_count: Some(2),
@ -641,7 +694,7 @@ Channel(
id: "UC2DjFE7Xf11URZqWBigcVOQ",
name: "EEVblog",
avatar: [],
verification: verified,
verification: Verified,
subscriber_count: Some(881000),
)),
video_count: Some(9),
@ -660,7 +713,7 @@ Channel(
id: "UC2DjFE7Xf11URZqWBigcVOQ",
name: "EEVblog",
avatar: [],
verification: verified,
verification: Verified,
subscriber_count: Some(881000),
)),
video_count: Some(1),

View file

@ -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,
),
)

View file

@ -5,9 +5,7 @@ expression: map_res.c
Channel(
id: "UCh8gHdtzO2tXd593_bjErWg",
name: "Doobydobap",
handle: Some("@Doobydobap"),
subscriber_count: Some(3360000),
video_count: None,
avatar: [
Thumbnail(
url: "https://yt3.googleusercontent.com/dm5Aq93xvVJz0NoVO88ieBkDXmuShCujGPlZ7qETMEPTrXvPUCFI3-BB6Xs_P-r6Uk3mnBy9zA=s48-c-k-c0x00ffffff-no-rj",
@ -25,9 +23,10 @@ Channel(
height: 176,
),
],
verification: verified,
verification: Verified,
description: "Hi, Im 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 its because youre hungry at midnight or trying to learn how to cook, I hope you enjoy watching my content and recipes. Don\'t yuck my yum!\n\nwww.doobydobap.com\n",
tags: [],
vanity_url: Some("https://www.youtube.com/@Doobydobap"),
banner: [
Thumbnail(
url: "https://yt3.googleusercontent.com/BvnAqgiursrXpmS9AgDLtkOSTQfOG_Dqn0KzY5hcwO9XrHTEQTVgaflI913f9KRp7d0U2qBp=w1060-fcrop64=1,00005a57ffffa5a8-k-c0xffffffff-no-nd-rj",
@ -60,6 +59,60 @@ Channel(
height: 424,
),
],
mobile_banner: [
Thumbnail(
url: "https://yt3.googleusercontent.com/BvnAqgiursrXpmS9AgDLtkOSTQfOG_Dqn0KzY5hcwO9XrHTEQTVgaflI913f9KRp7d0U2qBp=w320-fcrop64=1,32b75a57cd48a5a8-k-c0xffffffff-no-nd-rj",
width: 320,
height: 88,
),
Thumbnail(
url: "https://yt3.googleusercontent.com/BvnAqgiursrXpmS9AgDLtkOSTQfOG_Dqn0KzY5hcwO9XrHTEQTVgaflI913f9KRp7d0U2qBp=w640-fcrop64=1,32b75a57cd48a5a8-k-c0xffffffff-no-nd-rj",
width: 640,
height: 175,
),
Thumbnail(
url: "https://yt3.googleusercontent.com/BvnAqgiursrXpmS9AgDLtkOSTQfOG_Dqn0KzY5hcwO9XrHTEQTVgaflI913f9KRp7d0U2qBp=w960-fcrop64=1,32b75a57cd48a5a8-k-c0xffffffff-no-nd-rj",
width: 960,
height: 263,
),
Thumbnail(
url: "https://yt3.googleusercontent.com/BvnAqgiursrXpmS9AgDLtkOSTQfOG_Dqn0KzY5hcwO9XrHTEQTVgaflI913f9KRp7d0U2qBp=w1280-fcrop64=1,32b75a57cd48a5a8-k-c0xffffffff-no-nd-rj",
width: 1280,
height: 351,
),
Thumbnail(
url: "https://yt3.googleusercontent.com/BvnAqgiursrXpmS9AgDLtkOSTQfOG_Dqn0KzY5hcwO9XrHTEQTVgaflI913f9KRp7d0U2qBp=w1440-fcrop64=1,32b75a57cd48a5a8-k-c0xffffffff-no-nd-rj",
width: 1440,
height: 395,
),
],
tv_banner: [
Thumbnail(
url: "https://yt3.googleusercontent.com/BvnAqgiursrXpmS9AgDLtkOSTQfOG_Dqn0KzY5hcwO9XrHTEQTVgaflI913f9KRp7d0U2qBp=w320-fcrop64=1,00000000ffffffff-k-c0xffffffff-no-nd-rj",
width: 320,
height: 180,
),
Thumbnail(
url: "https://yt3.googleusercontent.com/BvnAqgiursrXpmS9AgDLtkOSTQfOG_Dqn0KzY5hcwO9XrHTEQTVgaflI913f9KRp7d0U2qBp=w854-fcrop64=1,00000000ffffffff-k-c0xffffffff-no-nd-rj",
width: 854,
height: 480,
),
Thumbnail(
url: "https://yt3.googleusercontent.com/BvnAqgiursrXpmS9AgDLtkOSTQfOG_Dqn0KzY5hcwO9XrHTEQTVgaflI913f9KRp7d0U2qBp=w1280-fcrop64=1,00000000ffffffff-k-c0xffffffff-no-nd-rj",
width: 1280,
height: 720,
),
Thumbnail(
url: "https://yt3.googleusercontent.com/BvnAqgiursrXpmS9AgDLtkOSTQfOG_Dqn0KzY5hcwO9XrHTEQTVgaflI913f9KRp7d0U2qBp=w1920-fcrop64=1,00000000ffffffff-k-c0xffffffff-no-nd-rj",
width: 1920,
height: 1080,
),
Thumbnail(
url: "https://yt3.googleusercontent.com/BvnAqgiursrXpmS9AgDLtkOSTQfOG_Dqn0KzY5hcwO9XrHTEQTVgaflI913f9KRp7d0U2qBp=w2120-fcrop64=1,00000000ffffffff-k-c0xffffffff-no-nd-rj",
width: 2120,
height: 1192,
),
],
has_shorts: true,
has_live: false,
visitor_data: Some("CgtHU1dvWkR4cGRfdyjMpt6iBg%3D%3D"),
@ -69,7 +122,7 @@ Channel(
VideoItem(
id: "bGXP83AU3Mc",
name: "do u wanna get swole?",
duration: None,
length: None,
thumbnail: [
Thumbnail(
url: "https://i.ytimg.com/vi/bGXP83AU3Mc/oar2.jpg?sqp=-oaymwEaCJUDENAFSFXyq4qpAwwIARUAAIhCcAHAAQY=&rs=AOn4CLC9bzCBeHDbZFLE84Up3IiBIsxmmA",
@ -81,7 +134,7 @@ Channel(
id: "UCh8gHdtzO2tXd593_bjErWg",
name: "Doobydobap",
avatar: [],
verification: verified,
verification: Verified,
subscriber_count: Some(3360000),
)),
publish_date: "[date]",
@ -95,7 +148,7 @@ Channel(
VideoItem(
id: "E52sSgZlgYs",
name: "the holy trinity of korean street food",
duration: None,
length: None,
thumbnail: [
Thumbnail(
url: "https://i.ytimg.com/vi/E52sSgZlgYs/oar2.jpg?sqp=-oaymwEaCJUDENAFSFXyq4qpAwwIARUAAIhCcAHAAQY=&rs=AOn4CLDBahtFRcfBInHuA8CjXFPWkF2jHg",
@ -107,7 +160,7 @@ Channel(
id: "UCh8gHdtzO2tXd593_bjErWg",
name: "Doobydobap",
avatar: [],
verification: verified,
verification: Verified,
subscriber_count: Some(3360000),
)),
publish_date: "[date]",
@ -121,7 +174,7 @@ Channel(
VideoItem(
id: "ovaHmfy3O6U",
name: "hangover food",
duration: None,
length: None,
thumbnail: [
Thumbnail(
url: "https://i.ytimg.com/vi/ovaHmfy3O6U/oar2.jpg?sqp=-oaymwEaCJUDENAFSFXyq4qpAwwIARUAAIhCcAHAAQY=&rs=AOn4CLCHmvWlG06h-DT6oxfmh69JGQ69KA",
@ -133,7 +186,7 @@ Channel(
id: "UCh8gHdtzO2tXd593_bjErWg",
name: "Doobydobap",
avatar: [],
verification: verified,
verification: Verified,
subscriber_count: Some(3360000),
)),
publish_date: "[date]",
@ -147,7 +200,7 @@ Channel(
VideoItem(
id: "FHTQmKTZnlI",
name: "pig trotter raguuuuuuuuu 💅",
duration: None,
length: None,
thumbnail: [
Thumbnail(
url: "https://i.ytimg.com/vi/FHTQmKTZnlI/oar2.jpg?sqp=-oaymwEaCJUDENAFSFXyq4qpAwwIARUAAIhCcAHAAQY=&rs=AOn4CLD0xhka1osA4nI3VCwhQusn3ND3Hg",
@ -159,7 +212,7 @@ Channel(
id: "UCh8gHdtzO2tXd593_bjErWg",
name: "Doobydobap",
avatar: [],
verification: verified,
verification: Verified,
subscriber_count: Some(3360000),
)),
publish_date: "[date]",
@ -173,7 +226,7 @@ Channel(
VideoItem(
id: "1AXB0l_wKMs",
name: "what i ate in google japan",
duration: None,
length: None,
thumbnail: [
Thumbnail(
url: "https://i.ytimg.com/vi/1AXB0l_wKMs/oar2.jpg?sqp=-oaymwEaCJUDENAFSFXyq4qpAwwIARUAAIhCcAHAAQY=&rs=AOn4CLBsfYJ0KffUNn-9jBzNRTqetyFr8g",
@ -185,7 +238,7 @@ Channel(
id: "UCh8gHdtzO2tXd593_bjErWg",
name: "Doobydobap",
avatar: [],
verification: verified,
verification: Verified,
subscriber_count: Some(3360000),
)),
publish_date: "[date]",
@ -199,7 +252,7 @@ Channel(
VideoItem(
id: "1ARLtk3HiB0",
name: "succumb to your cravings",
duration: None,
length: None,
thumbnail: [
Thumbnail(
url: "https://i.ytimg.com/vi/1ARLtk3HiB0/oar2.jpg?sqp=-oaymwEaCJUDENAFSFXyq4qpAwwIARUAAIhCcAHAAQY=&rs=AOn4CLBY9E40Ehvq862CVItJy0Uj_pS5bg",
@ -211,7 +264,7 @@ Channel(
id: "UCh8gHdtzO2tXd593_bjErWg",
name: "Doobydobap",
avatar: [],
verification: verified,
verification: Verified,
subscriber_count: Some(3360000),
)),
publish_date: "[date]",
@ -225,7 +278,7 @@ Channel(
VideoItem(
id: "0FfDoDHpaN8",
name: "you can\'t let the what ifs rule your life",
duration: None,
length: None,
thumbnail: [
Thumbnail(
url: "https://i.ytimg.com/vi/0FfDoDHpaN8/oar2.jpg?sqp=-oaymwEaCJUDENAFSFXyq4qpAwwIARUAAIhCcAHAAQY=&rs=AOn4CLBiV2TVPO-VbIjoNtwCKmFuxmj6LA",
@ -237,7 +290,7 @@ Channel(
id: "UCh8gHdtzO2tXd593_bjErWg",
name: "Doobydobap",
avatar: [],
verification: verified,
verification: Verified,
subscriber_count: Some(3360000),
)),
publish_date: "[date]",
@ -251,7 +304,7 @@ Channel(
VideoItem(
id: "kuT90_RIdF0",
name: "duck confit lollipop 🦆🍭",
duration: None,
length: None,
thumbnail: [
Thumbnail(
url: "https://i.ytimg.com/vi/kuT90_RIdF0/oar2.jpg?sqp=-oaymwEaCJUDENAFSFXyq4qpAwwIARUAAIhCcAHAAQY=&rs=AOn4CLCUN-DW72m7sAXJMgVkWNxPYpJBcQ",
@ -263,7 +316,7 @@ Channel(
id: "UCh8gHdtzO2tXd593_bjErWg",
name: "Doobydobap",
avatar: [],
verification: verified,
verification: Verified,
subscriber_count: Some(3360000),
)),
publish_date: "[date]",
@ -277,7 +330,7 @@ Channel(
VideoItem(
id: "aPJLhrcM4Yg",
name: "HOUSE TOUR",
duration: None,
length: None,
thumbnail: [
Thumbnail(
url: "https://i.ytimg.com/vi/aPJLhrcM4Yg/oar2.jpg?sqp=-oaymwEaCJUDENAFSFXyq4qpAwwIARUAAIhCcAHAAQY=&rs=AOn4CLD1TbWAIbzyWq8AXLoW0xqaji3ukQ",
@ -289,7 +342,7 @@ Channel(
id: "UCh8gHdtzO2tXd593_bjErWg",
name: "Doobydobap",
avatar: [],
verification: verified,
verification: Verified,
subscriber_count: Some(3360000),
)),
publish_date: "[date]",
@ -303,7 +356,7 @@ Channel(
VideoItem(
id: "DKQrG_hJJX4",
name: "how to meal prep like a korean",
duration: None,
length: None,
thumbnail: [
Thumbnail(
url: "https://i.ytimg.com/vi/DKQrG_hJJX4/oar2.jpg?sqp=-oaymwEaCJUDENAFSFXyq4qpAwwIARUAAIhCcAHAAQY=&rs=AOn4CLBE2DnpLFvtXsZOu1Ta4JQeOToVAw",
@ -315,7 +368,7 @@ Channel(
id: "UCh8gHdtzO2tXd593_bjErWg",
name: "Doobydobap",
avatar: [],
verification: verified,
verification: Verified,
subscriber_count: Some(3360000),
)),
publish_date: "[date]",
@ -329,7 +382,7 @@ Channel(
VideoItem(
id: "lNizW_P_oVw",
name: "Rating Everything I ate at McDonald\'s Japan 🇯🇵",
duration: None,
length: None,
thumbnail: [
Thumbnail(
url: "https://i.ytimg.com/vi/lNizW_P_oVw/oar2.jpg?sqp=-oaymwEaCJUDENAFSFXyq4qpAwwIARUAAIhCcAHAAQY=&rs=AOn4CLBI5XrkQ9Hesbf4lWELy7Uk3yMGMg",
@ -341,7 +394,7 @@ Channel(
id: "UCh8gHdtzO2tXd593_bjErWg",
name: "Doobydobap",
avatar: [],
verification: verified,
verification: Verified,
subscriber_count: Some(3360000),
)),
publish_date: "[date]",
@ -355,7 +408,7 @@ Channel(
VideoItem(
id: "kbWyJjrCjwA",
name: "enemies as fertilizer √(veg)",
duration: None,
length: None,
thumbnail: [
Thumbnail(
url: "https://i.ytimg.com/vi/kbWyJjrCjwA/oar2.jpg?sqp=-oaymwEaCJUDENAFSFXyq4qpAwwIARUAAIhCcAHAAQY=&rs=AOn4CLDlk30Km1M0jze1M3O90fB2LdvoAQ",
@ -367,7 +420,7 @@ Channel(
id: "UCh8gHdtzO2tXd593_bjErWg",
name: "Doobydobap",
avatar: [],
verification: verified,
verification: Verified,
subscriber_count: Some(3360000),
)),
publish_date: "[date]",
@ -381,7 +434,7 @@ Channel(
VideoItem(
id: "xAp910JTDig",
name: "let\'s make some cabbage rolls for lunch",
duration: None,
length: None,
thumbnail: [
Thumbnail(
url: "https://i.ytimg.com/vi/xAp910JTDig/oar2.jpg?sqp=-oaymwEaCJUDENAFSFXyq4qpAwwIARUAAIhCcAHAAQY=&rs=AOn4CLAJtpPGRgffBu9WDXACbtiGa_oRgA",
@ -393,7 +446,7 @@ Channel(
id: "UCh8gHdtzO2tXd593_bjErWg",
name: "Doobydobap",
avatar: [],
verification: verified,
verification: Verified,
subscriber_count: Some(3360000),
)),
publish_date: "[date]",
@ -407,7 +460,7 @@ Channel(
VideoItem(
id: "vSL7dhKatEk",
name: "Rating Everything I ate at IKEA Korea",
duration: None,
length: None,
thumbnail: [
Thumbnail(
url: "https://i.ytimg.com/vi/vSL7dhKatEk/oar2.jpg?sqp=-oaymwEaCJUDENAFSFXyq4qpAwwIARUAAIhCcAHAAQY=&rs=AOn4CLBYpIDYbwwWiCqRNVi6PlfEfjrt4A",
@ -419,7 +472,7 @@ Channel(
id: "UCh8gHdtzO2tXd593_bjErWg",
name: "Doobydobap",
avatar: [],
verification: verified,
verification: Verified,
subscriber_count: Some(3360000),
)),
publish_date: "[date]",
@ -433,7 +486,7 @@ Channel(
VideoItem(
id: "LZzhUpACXSk",
name: "I\'m done being the bigger person",
duration: None,
length: None,
thumbnail: [
Thumbnail(
url: "https://i.ytimg.com/vi/LZzhUpACXSk/oar2.jpg?sqp=-oaymwEaCJUDENAFSFXyq4qpAwwIARUAAIhCcAHAAQY=&rs=AOn4CLAFTvhtVUP7QZ4P7U70-0XH7PzDDg",
@ -445,7 +498,7 @@ Channel(
id: "UCh8gHdtzO2tXd593_bjErWg",
name: "Doobydobap",
avatar: [],
verification: verified,
verification: Verified,
subscriber_count: Some(3360000),
)),
publish_date: "[date]",
@ -459,7 +512,7 @@ Channel(
VideoItem(
id: "5C7nqNDfhis",
name: "we\'re cooking a whole bird today",
duration: None,
length: None,
thumbnail: [
Thumbnail(
url: "https://i.ytimg.com/vi/5C7nqNDfhis/oar2.jpg?sqp=-oaymwEaCJUDENAFSFXyq4qpAwwIARUAAIhCcAHAAQY=&rs=AOn4CLA9I9irDsRjikwd0aqp1FWNFtjAqA",
@ -471,7 +524,7 @@ Channel(
id: "UCh8gHdtzO2tXd593_bjErWg",
name: "Doobydobap",
avatar: [],
verification: verified,
verification: Verified,
subscriber_count: Some(3360000),
)),
publish_date: "[date]",
@ -485,7 +538,7 @@ Channel(
VideoItem(
id: "6mj4Af0kUOQ",
name: "men will disappoint but never potatoes",
duration: None,
length: None,
thumbnail: [
Thumbnail(
url: "https://i.ytimg.com/vi/6mj4Af0kUOQ/oar2.jpg?sqp=-oaymwEaCJUDENAFSFXyq4qpAwwIARUAAIhCcAHAAQY=&rs=AOn4CLAVxl-FPt878AQXPBhbV1VSGeR8sw",
@ -497,7 +550,7 @@ Channel(
id: "UCh8gHdtzO2tXd593_bjErWg",
name: "Doobydobap",
avatar: [],
verification: verified,
verification: Verified,
subscriber_count: Some(3360000),
)),
publish_date: "[date]",
@ -511,7 +564,7 @@ Channel(
VideoItem(
id: "1c3axhSJiaQ",
name: "I used to hate korean food",
duration: None,
length: None,
thumbnail: [
Thumbnail(
url: "https://i.ytimg.com/vi/1c3axhSJiaQ/hq720_2.jpg?sqp=-oaymwEdCJUDENAFSFXyq4qpAw8IARUAAIhCcAHAAQbQAQE=&rs=AOn4CLBucOEbTsWTDjOOCjNa-fAvz1yxyA",
@ -523,7 +576,7 @@ Channel(
id: "UCh8gHdtzO2tXd593_bjErWg",
name: "Doobydobap",
avatar: [],
verification: verified,
verification: Verified,
subscriber_count: Some(3360000),
)),
publish_date: "[date]",
@ -537,7 +590,7 @@ Channel(
VideoItem(
id: "F9Vz0m7DPeU",
name: "Rating everything I got at 7/11 Hawaii ( ft. Mauna Kea )",
duration: None,
length: None,
thumbnail: [
Thumbnail(
url: "https://i.ytimg.com/vi/F9Vz0m7DPeU/oar2.jpg?sqp=-oaymwEaCJUDENAFSFXyq4qpAwwIARUAAIhCcAHAAQY=&rs=AOn4CLDOoCVL6la3ztUeQ6vP4iL1cEBRjQ",
@ -549,7 +602,7 @@ Channel(
id: "UCh8gHdtzO2tXd593_bjErWg",
name: "Doobydobap",
avatar: [],
verification: verified,
verification: Verified,
subscriber_count: Some(3360000),
)),
publish_date: "[date]",
@ -563,7 +616,7 @@ Channel(
VideoItem(
id: "Uey7kl56wks",
name: "Grabbing Snacks from 7/11 Hawaii",
duration: None,
length: None,
thumbnail: [
Thumbnail(
url: "https://i.ytimg.com/vi/Uey7kl56wks/oar2.jpg?sqp=-oaymwEaCJUDENAFSFXyq4qpAwwIARUAAIhCcAHAAQY=&rs=AOn4CLCWmgajinNtIEbiPbqEtDvkC7Ydrg",
@ -575,7 +628,7 @@ Channel(
id: "UCh8gHdtzO2tXd593_bjErWg",
name: "Doobydobap",
avatar: [],
verification: verified,
verification: Verified,
subscriber_count: Some(3360000),
)),
publish_date: "[date]",
@ -589,7 +642,7 @@ Channel(
VideoItem(
id: "3un2eUAr6Dg",
name: "cheesy korean corn balls hit different",
duration: None,
length: None,
thumbnail: [
Thumbnail(
url: "https://i.ytimg.com/vi/3un2eUAr6Dg/oar2.jpg?sqp=-oaymwEaCJUDENAFSFXyq4qpAwwIARUAAIhCcAHAAQY=&rs=AOn4CLD4LziL6GHd1jg8btMJDIM_RhgE_A",
@ -601,7 +654,7 @@ Channel(
id: "UCh8gHdtzO2tXd593_bjErWg",
name: "Doobydobap",
avatar: [],
verification: verified,
verification: Verified,
subscriber_count: Some(3360000),
)),
publish_date: "[date]",
@ -615,7 +668,7 @@ Channel(
VideoItem(
id: "rI5tWrGpDJA",
name: "hawaiian tajin?!?",
duration: None,
length: None,
thumbnail: [
Thumbnail(
url: "https://i.ytimg.com/vi/rI5tWrGpDJA/oar2.jpg?sqp=-oaymwEaCJUDENAFSFXyq4qpAwwIARUAAIhCcAHAAQY=&rs=AOn4CLAjNiKHdFSKGavBrZRDxi9WdR-gJw",
@ -627,7 +680,7 @@ Channel(
id: "UCh8gHdtzO2tXd593_bjErWg",
name: "Doobydobap",
avatar: [],
verification: verified,
verification: Verified,
subscriber_count: Some(3360000),
)),
publish_date: "[date]",
@ -641,7 +694,7 @@ Channel(
VideoItem(
id: "WQiGksTxr5g",
name: "Rating everything I ate at Hawaiian Supermarket 🌺🏰 pt.2",
duration: None,
length: None,
thumbnail: [
Thumbnail(
url: "https://i.ytimg.com/vi/WQiGksTxr5g/oar2.jpg?sqp=-oaymwEaCJUDENAFSFXyq4qpAwwIARUAAIhCcAHAAQY=&rs=AOn4CLCvzC5xVdTEJX8xtiOqzmeKvmouIg",
@ -653,7 +706,7 @@ Channel(
id: "UCh8gHdtzO2tXd593_bjErWg",
name: "Doobydobap",
avatar: [],
verification: verified,
verification: Verified,
subscriber_count: Some(3360000),
)),
publish_date: "[date]",
@ -667,7 +720,7 @@ Channel(
VideoItem(
id: "G7aw-QOsagk",
name: "Grocery Shopping at Hawaiian Supermarket 🌺🏰 pt.1",
duration: None,
length: None,
thumbnail: [
Thumbnail(
url: "https://i.ytimg.com/vi/G7aw-QOsagk/oar2.jpg?sqp=-oaymwEaCJUDENAFSFXyq4qpAwwIARUAAIhCcAHAAQY=&rs=AOn4CLAoKEkj2lqYU07yW_DU35TNHEOq4w",
@ -679,7 +732,7 @@ Channel(
id: "UCh8gHdtzO2tXd593_bjErWg",
name: "Doobydobap",
avatar: [],
verification: verified,
verification: Verified,
subscriber_count: Some(3360000),
)),
publish_date: "[date]",
@ -693,7 +746,7 @@ Channel(
VideoItem(
id: "Y_F1_Yf-DKQ",
name: "Breakfast at Hawaiian McDonald\'s 🌺",
duration: None,
length: None,
thumbnail: [
Thumbnail(
url: "https://i.ytimg.com/vi/Y_F1_Yf-DKQ/hq720_2.jpg?sqp=-oaymwEdCJUDENAFSFXyq4qpAw8IARUAAIhCcAHAAQbQAQE=&rs=AOn4CLDTLFBwRThZUk0eugFSNxc-CKI_HQ",
@ -705,7 +758,7 @@ Channel(
id: "UCh8gHdtzO2tXd593_bjErWg",
name: "Doobydobap",
avatar: [],
verification: verified,
verification: Verified,
subscriber_count: Some(3360000),
)),
publish_date: "[date]",
@ -719,7 +772,7 @@ Channel(
VideoItem(
id: "Q_ZMcP8faw4",
name: "crab rangoon toast 🦀 🍞",
duration: None,
length: None,
thumbnail: [
Thumbnail(
url: "https://i.ytimg.com/vi/Q_ZMcP8faw4/oar2.jpg?sqp=-oaymwEaCJUDENAFSFXyq4qpAwwIARUAAIhCcAHAAQY=&rs=AOn4CLATLiHTNqLoBKsEKbOckkGjXMvoHA",
@ -731,7 +784,7 @@ Channel(
id: "UCh8gHdtzO2tXd593_bjErWg",
name: "Doobydobap",
avatar: [],
verification: verified,
verification: Verified,
subscriber_count: Some(3360000),
)),
publish_date: "[date]",
@ -745,7 +798,7 @@ Channel(
VideoItem(
id: "1aedyP3r3D0",
name: "my secret hot pot sauce 🧙\u{200d}♀\u{fe0f}🍃",
duration: None,
length: None,
thumbnail: [
Thumbnail(
url: "https://i.ytimg.com/vi/1aedyP3r3D0/oar2.jpg?sqp=-oaymwEaCJUDENAFSFXyq4qpAwwIARUAAIhCcAHAAQY=&rs=AOn4CLCh2MpR5k3jCS_wfX-wjtVuIcu7YQ",
@ -757,7 +810,7 @@ Channel(
id: "UCh8gHdtzO2tXd593_bjErWg",
name: "Doobydobap",
avatar: [],
verification: verified,
verification: Verified,
subscriber_count: Some(3360000),
)),
publish_date: "[date]",
@ -771,7 +824,7 @@ Channel(
VideoItem(
id: "fkPkHZ1yyBU",
name: "the good vs the bad",
duration: None,
length: None,
thumbnail: [
Thumbnail(
url: "https://i.ytimg.com/vi/fkPkHZ1yyBU/oar2.jpg?sqp=-oaymwEaCJUDENAFSFXyq4qpAwwIARUAAIhCcAHAAQY=&rs=AOn4CLCMngiRtLrBPppmfPnJwJ-cYMwttA",
@ -783,7 +836,7 @@ Channel(
id: "UCh8gHdtzO2tXd593_bjErWg",
name: "Doobydobap",
avatar: [],
verification: verified,
verification: Verified,
subscriber_count: Some(3360000),
)),
publish_date: "[date]",
@ -797,7 +850,7 @@ Channel(
VideoItem(
id: "NbQcySLMLmA",
name: "cooking with waste?!🗑\u{fe0f}",
duration: None,
length: None,
thumbnail: [
Thumbnail(
url: "https://i.ytimg.com/vi/NbQcySLMLmA/oar2.jpg?sqp=-oaymwEaCJUDENAFSFXyq4qpAwwIARUAAIhCcAHAAQY=&rs=AOn4CLCvxPQo9eqYwjk4cxyBnrHed-tcZg",
@ -809,7 +862,7 @@ Channel(
id: "UCh8gHdtzO2tXd593_bjErWg",
name: "Doobydobap",
avatar: [],
verification: verified,
verification: Verified,
subscriber_count: Some(3360000),
)),
publish_date: "[date]",
@ -823,7 +876,7 @@ Channel(
VideoItem(
id: "3w_5vzM1Pc4",
name: "Shrek burger 🍔🍀👹",
duration: None,
length: None,
thumbnail: [
Thumbnail(
url: "https://i.ytimg.com/vi/3w_5vzM1Pc4/oar2.jpg?sqp=-oaymwEaCJUDENAFSFXyq4qpAwwIARUAAIhCcAHAAQY=&rs=AOn4CLB64zOKgmhOt7bvQseeIbjKBICDAg",
@ -835,7 +888,7 @@ Channel(
id: "UCh8gHdtzO2tXd593_bjErWg",
name: "Doobydobap",
avatar: [],
verification: verified,
verification: Verified,
subscriber_count: Some(3360000),
)),
publish_date: "[date]",
@ -849,7 +902,7 @@ Channel(
VideoItem(
id: "girJP2r_zLg",
name: "$$$ on food",
duration: None,
length: None,
thumbnail: [
Thumbnail(
url: "https://i.ytimg.com/vi/girJP2r_zLg/oar2.jpg?sqp=-oaymwEaCJUDENAFSFXyq4qpAwwIARUAAIhCcAHAAQY=&rs=AOn4CLBg2hmruZvx30aiP4Jb4dhz03qOZA",
@ -861,7 +914,7 @@ Channel(
id: "UCh8gHdtzO2tXd593_bjErWg",
name: "Doobydobap",
avatar: [],
verification: verified,
verification: Verified,
subscriber_count: Some(3360000),
)),
publish_date: "[date]",
@ -875,7 +928,7 @@ Channel(
VideoItem(
id: "zHp7sZ5OONM",
name: "pumpkin spice churro?! 🎃",
duration: None,
length: None,
thumbnail: [
Thumbnail(
url: "https://i.ytimg.com/vi/zHp7sZ5OONM/oar2.jpg?sqp=-oaymwEaCJUDENAFSFXyq4qpAwwIARUAAIhCcAHAAQY=&rs=AOn4CLD8ZrcI6mq91ARKnRb_vg-0Qv2raw",
@ -887,7 +940,7 @@ Channel(
id: "UCh8gHdtzO2tXd593_bjErWg",
name: "Doobydobap",
avatar: [],
verification: verified,
verification: Verified,
subscriber_count: Some(3360000),
)),
publish_date: "[date]",
@ -901,7 +954,7 @@ Channel(
VideoItem(
id: "iqMl3gQEZ0E",
name: "3,000,000",
duration: None,
length: None,
thumbnail: [
Thumbnail(
url: "https://i.ytimg.com/vi/iqMl3gQEZ0E/oar2.jpg?sqp=-oaymwEaCJUDENAFSFXyq4qpAwwIARUAAIhCcAHAAQY=&rs=AOn4CLBUC1sw84NlLiyTJTcfnDWFjVC75w",
@ -913,7 +966,7 @@ Channel(
id: "UCh8gHdtzO2tXd593_bjErWg",
name: "Doobydobap",
avatar: [],
verification: verified,
verification: Verified,
subscriber_count: Some(3360000),
)),
publish_date: "[date]",
@ -927,7 +980,7 @@ Channel(
VideoItem(
id: "glyJWxp7a5g",
name: "being smart was my personality trait",
duration: None,
length: None,
thumbnail: [
Thumbnail(
url: "https://i.ytimg.com/vi/glyJWxp7a5g/oar2.jpg?sqp=-oaymwEaCJUDENAFSFXyq4qpAwwIARUAAIhCcAHAAQY=&rs=AOn4CLBbrWwLndPt5ZV5x4dnqmTC_aAhig",
@ -939,7 +992,7 @@ Channel(
id: "UCh8gHdtzO2tXd593_bjErWg",
name: "Doobydobap",
avatar: [],
verification: verified,
verification: Verified,
subscriber_count: Some(3360000),
)),
publish_date: "[date]",
@ -953,7 +1006,7 @@ Channel(
VideoItem(
id: "dd1EZIkANYs",
name: "the horror maze",
duration: None,
length: None,
thumbnail: [
Thumbnail(
url: "https://i.ytimg.com/vi/dd1EZIkANYs/oar2.jpg?sqp=-oaymwEaCJUDENAFSFXyq4qpAwwIARUAAIhCcAHAAQY=&rs=AOn4CLBlqz2BM3K2VeLlXMPBVwXNXih6vg",
@ -965,7 +1018,7 @@ Channel(
id: "UCh8gHdtzO2tXd593_bjErWg",
name: "Doobydobap",
avatar: [],
verification: verified,
verification: Verified,
subscriber_count: Some(3360000),
)),
publish_date: "[date]",
@ -979,7 +1032,7 @@ Channel(
VideoItem(
id: "enioc_stRww",
name: "furikake bagels with wasabi cream cheese",
duration: None,
length: None,
thumbnail: [
Thumbnail(
url: "https://i.ytimg.com/vi/enioc_stRww/oar2.jpg?sqp=-oaymwEaCJUDENAFSFXyq4qpAwwIARUAAIhCcAHAAQY=&rs=AOn4CLBz9Qo96FWssNsMhQ54DMxdYYwLfQ",
@ -991,7 +1044,7 @@ Channel(
id: "UCh8gHdtzO2tXd593_bjErWg",
name: "Doobydobap",
avatar: [],
verification: verified,
verification: Verified,
subscriber_count: Some(3360000),
)),
publish_date: "[date]",
@ -1005,7 +1058,7 @@ Channel(
VideoItem(
id: "NUM8kCPas5w",
name: "simple is best",
duration: None,
length: None,
thumbnail: [
Thumbnail(
url: "https://i.ytimg.com/vi/NUM8kCPas5w/oar2.jpg?sqp=-oaymwEaCJUDENAFSFXyq4qpAwwIARUAAIhCcAHAAQY=&rs=AOn4CLC8N3YRr9A6-u6L0AtMynct4C_GzQ",
@ -1017,7 +1070,7 @@ Channel(
id: "UCh8gHdtzO2tXd593_bjErWg",
name: "Doobydobap",
avatar: [],
verification: verified,
verification: Verified,
subscriber_count: Some(3360000),
)),
publish_date: "[date]",
@ -1031,7 +1084,7 @@ Channel(
VideoItem(
id: "1djkcsFnlYE",
name: "edible history lesson!",
duration: None,
length: None,
thumbnail: [
Thumbnail(
url: "https://i.ytimg.com/vi/1djkcsFnlYE/oar2.jpg?sqp=-oaymwEaCJUDENAFSFXyq4qpAwwIARUAAIhCcAHAAQY=&rs=AOn4CLBHn_6yOrnRXH_zbxVaAuKzSulcew",
@ -1043,7 +1096,7 @@ Channel(
id: "UCh8gHdtzO2tXd593_bjErWg",
name: "Doobydobap",
avatar: [],
verification: verified,
verification: Verified,
subscriber_count: Some(3360000),
)),
publish_date: "[date]",
@ -1057,7 +1110,7 @@ Channel(
VideoItem(
id: "cIYrJtAoftI",
name: "and I\'m feeling good",
duration: None,
length: None,
thumbnail: [
Thumbnail(
url: "https://i.ytimg.com/vi/cIYrJtAoftI/oar2.jpg?sqp=-oaymwEaCJUDENAFSFXyq4qpAwwIARUAAIhCcAHAAQY=&rs=AOn4CLC4q0VcbBZroejhAztDkdlk7Ww5Og",
@ -1069,7 +1122,7 @@ Channel(
id: "UCh8gHdtzO2tXd593_bjErWg",
name: "Doobydobap",
avatar: [],
verification: verified,
verification: Verified,
subscriber_count: Some(3360000),
)),
publish_date: "[date]",
@ -1083,7 +1136,7 @@ Channel(
VideoItem(
id: "cCrH8Er5tf4",
name: "Rating Korean Convenience Store Milk Flavors 🥛🍼",
duration: None,
length: None,
thumbnail: [
Thumbnail(
url: "https://i.ytimg.com/vi/cCrH8Er5tf4/oar2.jpg?sqp=-oaymwEaCJUDENAFSFXyq4qpAwwIARUAAIhCcAHAAQY=&rs=AOn4CLBwc2ikrGH_gZfcyqTnZDfHjt5LuA",
@ -1095,7 +1148,7 @@ Channel(
id: "UCh8gHdtzO2tXd593_bjErWg",
name: "Doobydobap",
avatar: [],
verification: verified,
verification: Verified,
subscriber_count: Some(3360000),
)),
publish_date: "[date]",
@ -1109,7 +1162,7 @@ Channel(
VideoItem(
id: "tav5wsH7pzU",
name: "online dating?",
duration: None,
length: None,
thumbnail: [
Thumbnail(
url: "https://i.ytimg.com/vi/tav5wsH7pzU/oar2.jpg?sqp=-oaymwEaCJUDENAFSFXyq4qpAwwIARUAAIhCcAHAAQY=&rs=AOn4CLCheup7XAM_O1UAMEO5Iqup4-lGRQ",
@ -1121,7 +1174,7 @@ Channel(
id: "UCh8gHdtzO2tXd593_bjErWg",
name: "Doobydobap",
avatar: [],
verification: verified,
verification: Verified,
subscriber_count: Some(3360000),
)),
publish_date: "[date]",
@ -1135,7 +1188,7 @@ Channel(
VideoItem(
id: "5Vd4_GXjF7o",
name: "Creating thumbnails has never been easier with Adobe Express",
duration: None,
length: None,
thumbnail: [
Thumbnail(
url: "https://i.ytimg.com/vi/5Vd4_GXjF7o/oar2.jpg?sqp=-oaymwEaCJUDENAFSFXyq4qpAwwIARUAAIhCcAHAAQY=&rs=AOn4CLCbYkH7INYGHW0IcO3DKip5iD2PCA",
@ -1147,7 +1200,7 @@ Channel(
id: "UCh8gHdtzO2tXd593_bjErWg",
name: "Doobydobap",
avatar: [],
verification: verified,
verification: Verified,
subscriber_count: Some(3360000),
)),
publish_date: "[date]",
@ -1161,7 +1214,7 @@ Channel(
VideoItem(
id: "-FN1sEI8HkU",
name: "my favorite color is green",
duration: None,
length: None,
thumbnail: [
Thumbnail(
url: "https://i.ytimg.com/vi/-FN1sEI8HkU/oar2.jpg?sqp=-oaymwEaCJUDENAFSFXyq4qpAwwIARUAAIhCcAHAAQY=&rs=AOn4CLCLWKPrR-VCdsXagJ1MIyah7dDdDQ",
@ -1173,7 +1226,7 @@ Channel(
id: "UCh8gHdtzO2tXd593_bjErWg",
name: "Doobydobap",
avatar: [],
verification: verified,
verification: Verified,
subscriber_count: Some(3360000),
)),
publish_date: "[date]",
@ -1187,7 +1240,7 @@ Channel(
VideoItem(
id: "viT-dcl2DGE",
name: "frodo baggins?",
duration: None,
length: None,
thumbnail: [
Thumbnail(
url: "https://i.ytimg.com/vi/viT-dcl2DGE/oar2.jpg?sqp=-oaymwEaCJUDENAFSFXyq4qpAwwIARUAAIhCcAHAAQY=&rs=AOn4CLDb0oYC_3V79CSR0j-4sR4CuNQekQ",
@ -1199,7 +1252,7 @@ Channel(
id: "UCh8gHdtzO2tXd593_bjErWg",
name: "Doobydobap",
avatar: [],
verification: verified,
verification: Verified,
subscriber_count: Some(3360000),
)),
publish_date: "[date]",
@ -1213,7 +1266,7 @@ Channel(
VideoItem(
id: "N5AKQflK1TU",
name: "When you impulse buy...",
duration: None,
length: None,
thumbnail: [
Thumbnail(
url: "https://i.ytimg.com/vi/N5AKQflK1TU/oar2.jpg?sqp=-oaymwEaCJUDENAFSFXyq4qpAwwIARUAAIhCcAHAAQY=&rs=AOn4CLDwfPTcuQHyziYsmTrSkg9xi1jnag",
@ -1225,7 +1278,7 @@ Channel(
id: "UCh8gHdtzO2tXd593_bjErWg",
name: "Doobydobap",
avatar: [],
verification: verified,
verification: Verified,
subscriber_count: Some(3360000),
)),
publish_date: "[date]",
@ -1239,7 +1292,7 @@ Channel(
VideoItem(
id: "OzIFALQ_YtA",
name: "taste testing gam!",
duration: None,
length: None,
thumbnail: [
Thumbnail(
url: "https://i.ytimg.com/vi/OzIFALQ_YtA/oar2.jpg?sqp=-oaymwEaCJUDENAFSFXyq4qpAwwIARUAAIhCcAHAAQY=&rs=AOn4CLBMcyG6Fu4rrXk-JQL5tx0hzSAxlw",
@ -1251,7 +1304,7 @@ Channel(
id: "UCh8gHdtzO2tXd593_bjErWg",
name: "Doobydobap",
avatar: [],
verification: verified,
verification: Verified,
subscriber_count: Some(3360000),
)),
publish_date: "[date]",
@ -1265,7 +1318,7 @@ Channel(
VideoItem(
id: "dAcJILbc_0Q",
name: "How to: Korean rice wine 🍶 (makgeolli)",
duration: None,
length: None,
thumbnail: [
Thumbnail(
url: "https://i.ytimg.com/vi/dAcJILbc_0Q/oar2.jpg?sqp=-oaymwEaCJUDENAFSFXyq4qpAwwIARUAAIhCcAHAAQY=&rs=AOn4CLAXbHym4PFTTO25GCI4n1tjSaQVCw",
@ -1277,7 +1330,7 @@ Channel(
id: "UCh8gHdtzO2tXd593_bjErWg",
name: "Doobydobap",
avatar: [],
verification: verified,
verification: Verified,
subscriber_count: Some(3360000),
)),
publish_date: "[date]",
@ -1291,7 +1344,7 @@ Channel(
VideoItem(
id: "GvutfmW26JQ",
name: "👹stay sour 🍋",
duration: None,
length: None,
thumbnail: [
Thumbnail(
url: "https://i.ytimg.com/vi/GvutfmW26JQ/oar2.jpg?sqp=-oaymwEaCJUDENAFSFXyq4qpAwwIARUAAIhCcAHAAQY=&rs=AOn4CLBgCJ06W3wOend0UgkuBKoHOg0eig",
@ -1303,7 +1356,7 @@ Channel(
id: "UCh8gHdtzO2tXd593_bjErWg",
name: "Doobydobap",
avatar: [],
verification: verified,
verification: Verified,
subscriber_count: Some(3360000),
)),
publish_date: "[date]",

View file

@ -5,9 +5,7 @@ expression: map_res.c
Channel(
id: "UCh8gHdtzO2tXd593_bjErWg",
name: "Doobydobap",
handle: Some("@Doobydobap"),
subscriber_count: Some(3740000),
video_count: None,
avatar: [
Thumbnail(
url: "https://yt3.googleusercontent.com/dm5Aq93xvVJz0NoVO88ieBkDXmuShCujGPlZ7qETMEPTrXvPUCFI3-BB6Xs_P-r6Uk3mnBy9zA=s72-c-k-c0x00ffffff-no-rj",
@ -25,9 +23,10 @@ Channel(
height: 160,
),
],
verification: verified,
verification: Verified,
description: "Hi, Im 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 its because youre hungry at midnight or trying to learn how to cook, I hope you enjoy watching my content and recipes. Don\'t yuck my yum!\n\nwww.doobydobap.com\n",
tags: [],
vanity_url: Some("https://www.youtube.com/@Doobydobap"),
banner: [
Thumbnail(
url: "https://yt3.googleusercontent.com/BvnAqgiursrXpmS9AgDLtkOSTQfOG_Dqn0KzY5hcwO9XrHTEQTVgaflI913f9KRp7d0U2qBp=w1060-fcrop64=1,00005a57ffffa5a8-k-c0xffffffff-no-nd-rj",
@ -60,6 +59,8 @@ Channel(
height: 424,
),
],
mobile_banner: [],
tv_banner: [],
has_shorts: true,
has_live: false,
visitor_data: None,
@ -69,7 +70,7 @@ Channel(
VideoItem(
id: "LJAt2RHBhYA",
name: "Rating Korean Traditional Desserts out of 10!!!",
duration: None,
length: None,
thumbnail: [
Thumbnail(
url: "https://i.ytimg.com/vi/LJAt2RHBhYA/oar2.jpg?sqp=-oaymwEdCJUDENAFSFWQAgHyq4qpAwwIARUAAIhCcAHAAQY=&rs=AOn4CLBs1ulw5vkRkR_SG6pp7Wuy90QK0Q",
@ -81,7 +82,7 @@ Channel(
id: "UCh8gHdtzO2tXd593_bjErWg",
name: "Doobydobap",
avatar: [],
verification: verified,
verification: Verified,
subscriber_count: Some(3740000),
)),
publish_date: "[date]",
@ -95,7 +96,7 @@ Channel(
VideoItem(
id: "apL97zDoAY0",
name: "best bang for your buck",
duration: None,
length: None,
thumbnail: [
Thumbnail(
url: "https://i.ytimg.com/vi/apL97zDoAY0/oar2.jpg?sqp=-oaymwEdCJUDENAFSFWQAgHyq4qpAwwIARUAAIhCcAHAAQY=&rs=AOn4CLDTJ5F-kzUTGBPolJgqloUZWve4GQ",
@ -107,7 +108,7 @@ Channel(
id: "UCh8gHdtzO2tXd593_bjErWg",
name: "Doobydobap",
avatar: [],
verification: verified,
verification: Verified,
subscriber_count: Some(3740000),
)),
publish_date: "[date]",
@ -121,7 +122,7 @@ Channel(
VideoItem(
id: "6au8hFOnUXI",
name: "don\'t judge a book by its cover",
duration: None,
length: None,
thumbnail: [
Thumbnail(
url: "https://i.ytimg.com/vi/6au8hFOnUXI/oar2.jpg?sqp=-oaymwEdCJUDENAFSFWQAgHyq4qpAwwIARUAAIhCcAHAAQY=&rs=AOn4CLCACe2S2wBTr0KVSFWzGda61k8Epw",
@ -133,7 +134,7 @@ Channel(
id: "UCh8gHdtzO2tXd593_bjErWg",
name: "Doobydobap",
avatar: [],
verification: verified,
verification: Verified,
subscriber_count: Some(3740000),
)),
publish_date: "[date]",
@ -147,7 +148,7 @@ Channel(
VideoItem(
id: "4wJAOnnPYsI",
name: "I ❤\u{fe0f} feet",
duration: None,
length: None,
thumbnail: [
Thumbnail(
url: "https://i.ytimg.com/vi/4wJAOnnPYsI/oar2.jpg?sqp=-oaymwEdCJUDENAFSFWQAgHyq4qpAwwIARUAAIhCcAHAAQY=&rs=AOn4CLCwFBUxJXMhuiv1ZevlM6r2x7Wq-Q",
@ -159,7 +160,7 @@ Channel(
id: "UCh8gHdtzO2tXd593_bjErWg",
name: "Doobydobap",
avatar: [],
verification: verified,
verification: Verified,
subscriber_count: Some(3740000),
)),
publish_date: "[date]",
@ -173,7 +174,7 @@ Channel(
VideoItem(
id: "DDsWN4HcoWQ",
name: "Trying North Korean Food 🇰🇵 and Rating it out of 10",
duration: None,
length: None,
thumbnail: [
Thumbnail(
url: "https://i.ytimg.com/vi/DDsWN4HcoWQ/oar2.jpg?sqp=-oaymwEdCJUDENAFSFWQAgHyq4qpAwwIARUAAIhCcAHAAQY=&rs=AOn4CLBIBARX0aoleQ1NFxd_DGhhHT4gDg",
@ -185,7 +186,7 @@ Channel(
id: "UCh8gHdtzO2tXd593_bjErWg",
name: "Doobydobap",
avatar: [],
verification: verified,
verification: Verified,
subscriber_count: Some(3740000),
)),
publish_date: "[date]",
@ -199,7 +200,7 @@ Channel(
VideoItem(
id: "oMIIEp8JMT0",
name: "get that bag 💰",
duration: None,
length: None,
thumbnail: [
Thumbnail(
url: "https://i.ytimg.com/vi/oMIIEp8JMT0/oar2.jpg?sqp=-oaymwEdCJUDENAFSFWQAgHyq4qpAwwIARUAAIhCcAHAAQY=&rs=AOn4CLBKsO5-KQzqU-bJinHsPDWrEQBIzQ",
@ -211,7 +212,7 @@ Channel(
id: "UCh8gHdtzO2tXd593_bjErWg",
name: "Doobydobap",
avatar: [],
verification: verified,
verification: Verified,
subscriber_count: Some(3740000),
)),
publish_date: "[date]",
@ -225,7 +226,7 @@ Channel(
VideoItem(
id: "ElEgDnx3Dfk",
name: "My Mom\'s 10-step Korean Skincare Routine",
duration: None,
length: None,
thumbnail: [
Thumbnail(
url: "https://i.ytimg.com/vi/ElEgDnx3Dfk/oar2.jpg?sqp=-oaymwEdCJUDENAFSFWQAgHyq4qpAwwIARUAAIhCcAHAAQY=&rs=AOn4CLD4uT5fSWvTnjAh_BC6hsW48zoH1w",
@ -237,7 +238,7 @@ Channel(
id: "UCh8gHdtzO2tXd593_bjErWg",
name: "Doobydobap",
avatar: [],
verification: verified,
verification: Verified,
subscriber_count: Some(3740000),
)),
publish_date: "[date]",
@ -251,7 +252,7 @@ Channel(
VideoItem(
id: "kNpFjW0VAUQ",
name: "What Koreans eat on New Year\'s",
duration: None,
length: None,
thumbnail: [
Thumbnail(
url: "https://i.ytimg.com/vi/kNpFjW0VAUQ/oar2.jpg?sqp=-oaymwEdCJUDENAFSFWQAgHyq4qpAwwIARUAAIhCcAHAAQY=&rs=AOn4CLBJTK-xhUMSzq8MBJ3s4kSXR7pFlw",
@ -263,7 +264,7 @@ Channel(
id: "UCh8gHdtzO2tXd593_bjErWg",
name: "Doobydobap",
avatar: [],
verification: verified,
verification: Verified,
subscriber_count: Some(3740000),
)),
publish_date: "[date]",
@ -277,7 +278,7 @@ Channel(
VideoItem(
id: "0_5Y3ZBo5cw",
name: "DOOBYMART",
duration: None,
length: None,
thumbnail: [
Thumbnail(
url: "https://i.ytimg.com/vi/0_5Y3ZBo5cw/oar2.jpg?sqp=-oaymwEdCJUDENAFSFWQAgHyq4qpAwwIARUAAIhCcAHAAQY=&rs=AOn4CLBlcI8i0DqG2vKRFGDzIQXBEsifFQ",
@ -289,7 +290,7 @@ Channel(
id: "UCh8gHdtzO2tXd593_bjErWg",
name: "Doobydobap",
avatar: [],
verification: verified,
verification: Verified,
subscriber_count: Some(3740000),
)),
publish_date: "[date]",
@ -303,7 +304,7 @@ Channel(
VideoItem(
id: "VyVs3GCQlG0",
name: "I love ogres",
duration: None,
length: None,
thumbnail: [
Thumbnail(
url: "https://i.ytimg.com/vi/VyVs3GCQlG0/oar2.jpg?sqp=-oaymwEdCJUDENAFSFWQAgHyq4qpAwwIARUAAIhCcAHAAQY=&rs=AOn4CLBsbPYKlWAAZNphAPobZQReZzk-lA",
@ -315,7 +316,7 @@ Channel(
id: "UCh8gHdtzO2tXd593_bjErWg",
name: "Doobydobap",
avatar: [],
verification: verified,
verification: Verified,
subscriber_count: Some(3740000),
)),
publish_date: "[date]",
@ -329,7 +330,7 @@ Channel(
VideoItem(
id: "_yMEpzXq3yI",
name: "Are you broke?😞📉📉📉",
duration: None,
length: None,
thumbnail: [
Thumbnail(
url: "https://i.ytimg.com/vi/_yMEpzXq3yI/oar2.jpg?sqp=-oaymwEdCJUDENAFSFWQAgHyq4qpAwwIARUAAIhCcAHAAQY=&rs=AOn4CLCRyJDCY-7MsSYbsl2ZZoC8DxPkKw",
@ -341,7 +342,7 @@ Channel(
id: "UCh8gHdtzO2tXd593_bjErWg",
name: "Doobydobap",
avatar: [],
verification: verified,
verification: Verified,
subscriber_count: Some(3740000),
)),
publish_date: "[date]",
@ -355,7 +356,7 @@ Channel(
VideoItem(
id: "sM1jIMnq0M0",
name: "best tteokbokki & dark sketchy alleys",
duration: None,
length: None,
thumbnail: [
Thumbnail(
url: "https://i.ytimg.com/vi/sM1jIMnq0M0/oar2.jpg?sqp=-oaymwEdCJUDENAFSFWQAgHyq4qpAwwIARUAAIhCcAHAAQY=&rs=AOn4CLCIQyuEFF2m676ZX7UpcK3hVHTzzw",
@ -367,7 +368,7 @@ Channel(
id: "UCh8gHdtzO2tXd593_bjErWg",
name: "Doobydobap",
avatar: [],
verification: verified,
verification: Verified,
subscriber_count: Some(3740000),
)),
publish_date: "[date]",
@ -381,7 +382,7 @@ Channel(
VideoItem(
id: "nZg_Qoknu_M",
name: "moist.",
duration: None,
length: None,
thumbnail: [
Thumbnail(
url: "https://i.ytimg.com/vi/nZg_Qoknu_M/oar2.jpg?sqp=-oaymwEdCJUDENAFSFWQAgHyq4qpAwwIARUAAIhCcAHAAQY=&rs=AOn4CLCcXipLuXAs9D8d1nRJbMh-BZkK1Q",
@ -393,7 +394,7 @@ Channel(
id: "UCh8gHdtzO2tXd593_bjErWg",
name: "Doobydobap",
avatar: [],
verification: verified,
verification: Verified,
subscriber_count: Some(3740000),
)),
publish_date: "[date]",
@ -407,7 +408,7 @@ Channel(
VideoItem(
id: "CgS55gL33nY",
name: "Do you have any Korean Friends?",
duration: None,
length: None,
thumbnail: [
Thumbnail(
url: "https://i.ytimg.com/vi/CgS55gL33nY/oar2.jpg?sqp=-oaymwEdCJUDENAFSFWQAgHyq4qpAwwIARUAAIhCcAHAAQY=&rs=AOn4CLCVh8B6HRomEnQX6dyDJVbYhE-RGA",
@ -419,7 +420,7 @@ Channel(
id: "UCh8gHdtzO2tXd593_bjErWg",
name: "Doobydobap",
avatar: [],
verification: verified,
verification: Verified,
subscriber_count: Some(3740000),
)),
publish_date: "[date]",
@ -433,7 +434,7 @@ Channel(
VideoItem(
id: "VOxXkxFNOpM",
name: "take my money",
duration: None,
length: None,
thumbnail: [
Thumbnail(
url: "https://i.ytimg.com/vi/VOxXkxFNOpM/oar2.jpg?sqp=-oaymwEdCJUDENAFSFWQAgHyq4qpAwwIARUAAIhCcAHAAQY=&rs=AOn4CLAkP1n1_Oe8pBGluy_lyi4I3pO4SQ",
@ -445,7 +446,7 @@ Channel(
id: "UCh8gHdtzO2tXd593_bjErWg",
name: "Doobydobap",
avatar: [],
verification: verified,
verification: Verified,
subscriber_count: Some(3740000),
)),
publish_date: "[date]",
@ -459,7 +460,7 @@ Channel(
VideoItem(
id: "aZW65r9uUXA",
name: "i\'m a mother?",
duration: None,
length: None,
thumbnail: [
Thumbnail(
url: "https://i.ytimg.com/vi/aZW65r9uUXA/oar2.jpg?sqp=-oaymwEdCJUDENAFSFWQAgHyq4qpAwwIARUAAIhCcAHAAQY=&rs=AOn4CLDIcJpRgW65hGv0GM8ak-L0PyErUw",
@ -471,7 +472,7 @@ Channel(
id: "UCh8gHdtzO2tXd593_bjErWg",
name: "Doobydobap",
avatar: [],
verification: verified,
verification: Verified,
subscriber_count: Some(3740000),
)),
publish_date: "[date]",
@ -485,7 +486,7 @@ Channel(
VideoItem(
id: "CnI9or-Ings",
name: "moshi moshi (ASMR)",
duration: None,
length: None,
thumbnail: [
Thumbnail(
url: "https://i.ytimg.com/vi/CnI9or-Ings/oar2.jpg?sqp=-oaymwEdCJUDENAFSFWQAgHyq4qpAwwIARUAAIhCcAHAAQY=&rs=AOn4CLDIoImzeIOlO25vkY7j92YwUhAOdw",
@ -497,7 +498,7 @@ Channel(
id: "UCh8gHdtzO2tXd593_bjErWg",
name: "Doobydobap",
avatar: [],
verification: verified,
verification: Verified,
subscriber_count: Some(3740000),
)),
publish_date: "[date]",
@ -511,7 +512,7 @@ Channel(
VideoItem(
id: "oYAp2v4PxdQ",
name: "perfect procrastination",
duration: None,
length: None,
thumbnail: [
Thumbnail(
url: "https://i.ytimg.com/vi/oYAp2v4PxdQ/oar2.jpg?sqp=-oaymwEdCJUDENAFSFWQAgHyq4qpAwwIARUAAIhCcAHAAQY=&rs=AOn4CLA3PsooCO5Cot_z-vLFCF1Kdtshtg",
@ -523,7 +524,7 @@ Channel(
id: "UCh8gHdtzO2tXd593_bjErWg",
name: "Doobydobap",
avatar: [],
verification: verified,
verification: Verified,
subscriber_count: Some(3740000),
)),
publish_date: "[date]",
@ -537,7 +538,7 @@ Channel(
VideoItem(
id: "1ivqsJSGghU",
name: "THREE CHILI MAPO TOFU (Vegan)",
duration: None,
length: None,
thumbnail: [
Thumbnail(
url: "https://i.ytimg.com/vi/1ivqsJSGghU/oar2.jpg?sqp=-oaymwEdCJUDENAFSFWQAgHyq4qpAwwIARUAAIhCcAHAAQY=&rs=AOn4CLCk5IRHy8TmJPzE9yD3M_OGXdww_g",
@ -549,7 +550,7 @@ Channel(
id: "UCh8gHdtzO2tXd593_bjErWg",
name: "Doobydobap",
avatar: [],
verification: verified,
verification: Verified,
subscriber_count: Some(3740000),
)),
publish_date: "[date]",
@ -563,7 +564,7 @@ Channel(
VideoItem(
id: "s56ctQoFi70",
name: "day 1 leaving seoul",
duration: None,
length: None,
thumbnail: [
Thumbnail(
url: "https://i.ytimg.com/vi/s56ctQoFi70/oar2.jpg?sqp=-oaymwEdCJUDENAFSFWQAgHyq4qpAwwIARUAAIhCcAHAAQY=&rs=AOn4CLCfn6oYumOdPd33WlDWaqBUZzaEiw",
@ -575,7 +576,7 @@ Channel(
id: "UCh8gHdtzO2tXd593_bjErWg",
name: "Doobydobap",
avatar: [],
verification: verified,
verification: Verified,
subscriber_count: Some(3740000),
)),
publish_date: "[date]",
@ -589,7 +590,7 @@ Channel(
VideoItem(
id: "8XHcYgsJJjs",
name: "I love Korea but",
duration: None,
length: None,
thumbnail: [
Thumbnail(
url: "https://i.ytimg.com/vi/8XHcYgsJJjs/oar2.jpg?sqp=-oaymwEdCJUDENAFSFWQAgHyq4qpAwwIARUAAIhCcAHAAQY=&rs=AOn4CLAtj-QeOzONDGPzDWC8IdFGigmKYA",
@ -601,7 +602,7 @@ Channel(
id: "UCh8gHdtzO2tXd593_bjErWg",
name: "Doobydobap",
avatar: [],
verification: verified,
verification: Verified,
subscriber_count: Some(3740000),
)),
publish_date: "[date]",
@ -615,7 +616,7 @@ Channel(
VideoItem(
id: "DXYbvkJEYzw",
name: "demogorgon rice",
duration: None,
length: None,
thumbnail: [
Thumbnail(
url: "https://i.ytimg.com/vi/DXYbvkJEYzw/oar2.jpg?sqp=-oaymwEdCJUDENAFSFWQAgHyq4qpAwwIARUAAIhCcAHAAQY=&rs=AOn4CLDd33uW_fC4Dz5JqYlDtGWGdMQn8A",
@ -627,7 +628,7 @@ Channel(
id: "UCh8gHdtzO2tXd593_bjErWg",
name: "Doobydobap",
avatar: [],
verification: verified,
verification: Verified,
subscriber_count: Some(3740000),
)),
publish_date: "[date]",
@ -641,7 +642,7 @@ Channel(
VideoItem(
id: "3YGVw8RrB3U",
name: "Rating Everything I Ate at McDonald\'s Singapore 🇸🇬🤡",
duration: None,
length: None,
thumbnail: [
Thumbnail(
url: "https://i.ytimg.com/vi/3YGVw8RrB3U/oar2.jpg?sqp=-oaymwEdCJUDENAFSFWQAgHyq4qpAwwIARUAAIhCcAHAAQY=&rs=AOn4CLAV2y-_1JXq9RecS8ELjyUsoN52NA",
@ -653,7 +654,7 @@ Channel(
id: "UCh8gHdtzO2tXd593_bjErWg",
name: "Doobydobap",
avatar: [],
verification: verified,
verification: Verified,
subscriber_count: Some(3740000),
)),
publish_date: "[date]",
@ -667,7 +668,7 @@ Channel(
VideoItem(
id: "VWh8hm-GlXw",
name: "the magic number: 25",
duration: None,
length: None,
thumbnail: [
Thumbnail(
url: "https://i.ytimg.com/vi/VWh8hm-GlXw/oar2.jpg?sqp=-oaymwEdCJUDENAFSFWQAgHyq4qpAwwIARUAAIhCcAHAAQY=&rs=AOn4CLD1wp7tz9TU3Kx2Oho7mrVoFys8Jw",
@ -679,7 +680,7 @@ Channel(
id: "UCh8gHdtzO2tXd593_bjErWg",
name: "Doobydobap",
avatar: [],
verification: verified,
verification: Verified,
subscriber_count: Some(3740000),
)),
publish_date: "[date]",
@ -693,7 +694,7 @@ Channel(
VideoItem(
id: "ewpJQHj5jWY",
name: "How we started✨ the garden ✨",
duration: None,
length: None,
thumbnail: [
Thumbnail(
url: "https://i.ytimg.com/vi/ewpJQHj5jWY/oar2.jpg?sqp=-oaymwEdCJUDENAFSFWQAgHyq4qpAwwIARUAAIhCcAHAAQY=&rs=AOn4CLAOw5ntEtOhIgNJy2y1QQCFLUtGBQ",
@ -705,7 +706,7 @@ Channel(
id: "UCh8gHdtzO2tXd593_bjErWg",
name: "Doobydobap",
avatar: [],
verification: verified,
verification: Verified,
subscriber_count: Some(3740000),
)),
publish_date: "[date]",
@ -719,7 +720,7 @@ Channel(
VideoItem(
id: "SRnHMGMJ6mM",
name: "How to Shop at Costco ✨economical milf✨",
duration: None,
length: None,
thumbnail: [
Thumbnail(
url: "https://i.ytimg.com/vi/SRnHMGMJ6mM/oar2.jpg?sqp=-oaymwEdCJUDENAFSFWQAgHyq4qpAwwIARUAAIhCcAHAAQY=&rs=AOn4CLDrwe86eXSemFBKB3xLGnRIDcL_qA",
@ -731,7 +732,7 @@ Channel(
id: "UCh8gHdtzO2tXd593_bjErWg",
name: "Doobydobap",
avatar: [],
verification: verified,
verification: Verified,
subscriber_count: Some(3740000),
)),
publish_date: "[date]",
@ -745,7 +746,7 @@ Channel(
VideoItem(
id: "bGXP83AU3Mc",
name: "do u wanna get swole?",
duration: None,
length: None,
thumbnail: [
Thumbnail(
url: "https://i.ytimg.com/vi/bGXP83AU3Mc/oar2.jpg?sqp=-oaymwEdCJUDENAFSFWQAgHyq4qpAwwIARUAAIhCcAHAAQY=&rs=AOn4CLBLGuAqqpjhQ_Y81P2pxchz-z971g",
@ -757,7 +758,7 @@ Channel(
id: "UCh8gHdtzO2tXd593_bjErWg",
name: "Doobydobap",
avatar: [],
verification: verified,
verification: Verified,
subscriber_count: Some(3740000),
)),
publish_date: "[date]",
@ -771,7 +772,7 @@ Channel(
VideoItem(
id: "E52sSgZlgYs",
name: "the holy trinity of korean street food",
duration: None,
length: None,
thumbnail: [
Thumbnail(
url: "https://i.ytimg.com/vi/E52sSgZlgYs/oar2.jpg?sqp=-oaymwEdCJUDENAFSFWQAgHyq4qpAwwIARUAAIhCcAHAAQY=&rs=AOn4CLAZr5j2o2IdpTLXUoSKT5QK-DFJgw",
@ -783,7 +784,7 @@ Channel(
id: "UCh8gHdtzO2tXd593_bjErWg",
name: "Doobydobap",
avatar: [],
verification: verified,
verification: Verified,
subscriber_count: Some(3740000),
)),
publish_date: "[date]",
@ -797,7 +798,7 @@ Channel(
VideoItem(
id: "ovaHmfy3O6U",
name: "hangover food",
duration: None,
length: None,
thumbnail: [
Thumbnail(
url: "https://i.ytimg.com/vi/ovaHmfy3O6U/oar2.jpg?sqp=-oaymwEdCJUDENAFSFWQAgHyq4qpAwwIARUAAIhCcAHAAQY=&rs=AOn4CLBg-kG4rAi-BQ8Xkp2hOtOu-oXDLQ",
@ -809,7 +810,7 @@ Channel(
id: "UCh8gHdtzO2tXd593_bjErWg",
name: "Doobydobap",
avatar: [],
verification: verified,
verification: Verified,
subscriber_count: Some(3740000),
)),
publish_date: "[date]",
@ -823,7 +824,7 @@ Channel(
VideoItem(
id: "FHTQmKTZnlI",
name: "pig trotter raguuuuuuuuu 💅",
duration: None,
length: None,
thumbnail: [
Thumbnail(
url: "https://i.ytimg.com/vi/FHTQmKTZnlI/oar2.jpg?sqp=-oaymwEdCJUDENAFSFWQAgHyq4qpAwwIARUAAIhCcAHAAQY=&rs=AOn4CLBaPmdp59deKec5AiRRJ_c6oWOpuA",
@ -835,7 +836,7 @@ Channel(
id: "UCh8gHdtzO2tXd593_bjErWg",
name: "Doobydobap",
avatar: [],
verification: verified,
verification: Verified,
subscriber_count: Some(3740000),
)),
publish_date: "[date]",
@ -849,7 +850,7 @@ Channel(
VideoItem(
id: "1AXB0l_wKMs",
name: "what i ate in google japan",
duration: None,
length: None,
thumbnail: [
Thumbnail(
url: "https://i.ytimg.com/vi/1AXB0l_wKMs/oar2.jpg?sqp=-oaymwEdCJUDENAFSFWQAgHyq4qpAwwIARUAAIhCcAHAAQY=&rs=AOn4CLAwymbQoerZLtr77RuARo1iok4_WQ",
@ -861,7 +862,7 @@ Channel(
id: "UCh8gHdtzO2tXd593_bjErWg",
name: "Doobydobap",
avatar: [],
verification: verified,
verification: Verified,
subscriber_count: Some(3740000),
)),
publish_date: "[date]",
@ -875,7 +876,7 @@ Channel(
VideoItem(
id: "1ARLtk3HiB0",
name: "succumb to your cravings",
duration: None,
length: None,
thumbnail: [
Thumbnail(
url: "https://i.ytimg.com/vi/1ARLtk3HiB0/oar2.jpg?sqp=-oaymwEdCJUDENAFSFWQAgHyq4qpAwwIARUAAIhCcAHAAQY=&rs=AOn4CLDSjiCiBhM1i7n68LVRv_G5GW5vRw",
@ -887,7 +888,7 @@ Channel(
id: "UCh8gHdtzO2tXd593_bjErWg",
name: "Doobydobap",
avatar: [],
verification: verified,
verification: Verified,
subscriber_count: Some(3740000),
)),
publish_date: "[date]",
@ -901,7 +902,7 @@ Channel(
VideoItem(
id: "0FfDoDHpaN8",
name: "you can\'t let the what ifs rule your life",
duration: None,
length: None,
thumbnail: [
Thumbnail(
url: "https://i.ytimg.com/vi/0FfDoDHpaN8/oar2.jpg?sqp=-oaymwEdCJUDENAFSFWQAgHyq4qpAwwIARUAAIhCcAHAAQY=&rs=AOn4CLAJK1Av4zHRIWRBMSEfYRXxpwCSlQ",
@ -913,7 +914,7 @@ Channel(
id: "UCh8gHdtzO2tXd593_bjErWg",
name: "Doobydobap",
avatar: [],
verification: verified,
verification: Verified,
subscriber_count: Some(3740000),
)),
publish_date: "[date]",
@ -927,7 +928,7 @@ Channel(
VideoItem(
id: "kuT90_RIdF0",
name: "duck confit lollipop 🦆🍭",
duration: None,
length: None,
thumbnail: [
Thumbnail(
url: "https://i.ytimg.com/vi/kuT90_RIdF0/oar2.jpg?sqp=-oaymwEdCJUDENAFSFWQAgHyq4qpAwwIARUAAIhCcAHAAQY=&rs=AOn4CLCt9L5XUhmlkwuxMuupIt4kCnxctA",
@ -939,7 +940,7 @@ Channel(
id: "UCh8gHdtzO2tXd593_bjErWg",
name: "Doobydobap",
avatar: [],
verification: verified,
verification: Verified,
subscriber_count: Some(3740000),
)),
publish_date: "[date]",
@ -953,7 +954,7 @@ Channel(
VideoItem(
id: "aPJLhrcM4Yg",
name: "HOUSE TOUR",
duration: None,
length: None,
thumbnail: [
Thumbnail(
url: "https://i.ytimg.com/vi/aPJLhrcM4Yg/oar2.jpg?sqp=-oaymwEdCJUDENAFSFWQAgHyq4qpAwwIARUAAIhCcAHAAQY=&rs=AOn4CLBCLF6yI6Ke96PIrfo1s5BhYfeWvg",
@ -965,7 +966,7 @@ Channel(
id: "UCh8gHdtzO2tXd593_bjErWg",
name: "Doobydobap",
avatar: [],
verification: verified,
verification: Verified,
subscriber_count: Some(3740000),
)),
publish_date: "[date]",
@ -979,7 +980,7 @@ Channel(
VideoItem(
id: "DKQrG_hJJX4",
name: "how to meal prep like a korean",
duration: None,
length: None,
thumbnail: [
Thumbnail(
url: "https://i.ytimg.com/vi/DKQrG_hJJX4/oar2.jpg?sqp=-oaymwEdCJUDENAFSFWQAgHyq4qpAwwIARUAAIhCcAHAAQY=&rs=AOn4CLDZTyFRl919wEGdUg95-XrNIH99mg",
@ -991,7 +992,7 @@ Channel(
id: "UCh8gHdtzO2tXd593_bjErWg",
name: "Doobydobap",
avatar: [],
verification: verified,
verification: Verified,
subscriber_count: Some(3740000),
)),
publish_date: "[date]",
@ -1005,7 +1006,7 @@ Channel(
VideoItem(
id: "lNizW_P_oVw",
name: "Rating Everything I ate at McDonald\'s Japan 🇯🇵",
duration: None,
length: None,
thumbnail: [
Thumbnail(
url: "https://i.ytimg.com/vi/lNizW_P_oVw/oar2.jpg?sqp=-oaymwEdCJUDENAFSFWQAgHyq4qpAwwIARUAAIhCcAHAAQY=&rs=AOn4CLAAiwuuJufebYj3P4aTo7wyBjh9Kw",
@ -1017,7 +1018,7 @@ Channel(
id: "UCh8gHdtzO2tXd593_bjErWg",
name: "Doobydobap",
avatar: [],
verification: verified,
verification: Verified,
subscriber_count: Some(3740000),
)),
publish_date: "[date]",
@ -1031,7 +1032,7 @@ Channel(
VideoItem(
id: "kbWyJjrCjwA",
name: "enemies as fertilizer √(veg)",
duration: None,
length: None,
thumbnail: [
Thumbnail(
url: "https://i.ytimg.com/vi/kbWyJjrCjwA/oar2.jpg?sqp=-oaymwEdCJUDENAFSFWQAgHyq4qpAwwIARUAAIhCcAHAAQY=&rs=AOn4CLCItmfFh3UD53WvNCWd9HAItdubgg",
@ -1043,7 +1044,7 @@ Channel(
id: "UCh8gHdtzO2tXd593_bjErWg",
name: "Doobydobap",
avatar: [],
verification: verified,
verification: Verified,
subscriber_count: Some(3740000),
)),
publish_date: "[date]",
@ -1057,7 +1058,7 @@ Channel(
VideoItem(
id: "xAp910JTDig",
name: "let\'s make some cabbage rolls for lunch",
duration: None,
length: None,
thumbnail: [
Thumbnail(
url: "https://i.ytimg.com/vi/xAp910JTDig/oar2.jpg?sqp=-oaymwEdCJUDENAFSFWQAgHyq4qpAwwIARUAAIhCcAHAAQY=&rs=AOn4CLD0jor2XgDSjgk4IdMhCOGNqv0Vag",
@ -1069,7 +1070,7 @@ Channel(
id: "UCh8gHdtzO2tXd593_bjErWg",
name: "Doobydobap",
avatar: [],
verification: verified,
verification: Verified,
subscriber_count: Some(3740000),
)),
publish_date: "[date]",
@ -1083,7 +1084,7 @@ Channel(
VideoItem(
id: "vSL7dhKatEk",
name: "Rating Everything I ate at IKEA Korea",
duration: None,
length: None,
thumbnail: [
Thumbnail(
url: "https://i.ytimg.com/vi/vSL7dhKatEk/oar2.jpg?sqp=-oaymwEdCJUDENAFSFWQAgHyq4qpAwwIARUAAIhCcAHAAQY=&rs=AOn4CLD1jtkvWyabnlTD7ktUQCWYvKctFQ",
@ -1095,7 +1096,7 @@ Channel(
id: "UCh8gHdtzO2tXd593_bjErWg",
name: "Doobydobap",
avatar: [],
verification: verified,
verification: Verified,
subscriber_count: Some(3740000),
)),
publish_date: "[date]",
@ -1109,7 +1110,7 @@ Channel(
VideoItem(
id: "LZzhUpACXSk",
name: "I\'m done being the bigger person",
duration: None,
length: None,
thumbnail: [
Thumbnail(
url: "https://i.ytimg.com/vi/LZzhUpACXSk/oar2.jpg?sqp=-oaymwEdCJUDENAFSFWQAgHyq4qpAwwIARUAAIhCcAHAAQY=&rs=AOn4CLBKpbXJ3fj1mWw8YLEGgqqQJFPapw",
@ -1121,7 +1122,7 @@ Channel(
id: "UCh8gHdtzO2tXd593_bjErWg",
name: "Doobydobap",
avatar: [],
verification: verified,
verification: Verified,
subscriber_count: Some(3740000),
)),
publish_date: "[date]",
@ -1135,7 +1136,7 @@ Channel(
VideoItem(
id: "5C7nqNDfhis",
name: "we\'re cooking a whole bird today",
duration: None,
length: None,
thumbnail: [
Thumbnail(
url: "https://i.ytimg.com/vi/5C7nqNDfhis/oar2.jpg?sqp=-oaymwEdCJUDENAFSFWQAgHyq4qpAwwIARUAAIhCcAHAAQY=&rs=AOn4CLAywBcN0oFzKJrq2jxAcYU8Gz5mQQ",
@ -1147,7 +1148,7 @@ Channel(
id: "UCh8gHdtzO2tXd593_bjErWg",
name: "Doobydobap",
avatar: [],
verification: verified,
verification: Verified,
subscriber_count: Some(3740000),
)),
publish_date: "[date]",
@ -1161,7 +1162,7 @@ Channel(
VideoItem(
id: "6mj4Af0kUOQ",
name: "men will disappoint but never potatoes",
duration: None,
length: None,
thumbnail: [
Thumbnail(
url: "https://i.ytimg.com/vi/6mj4Af0kUOQ/oar2.jpg?sqp=-oaymwEdCJUDENAFSFWQAgHyq4qpAwwIARUAAIhCcAHAAQY=&rs=AOn4CLB3pIz2cfevgV0ccu3bQM4IDaBSKg",
@ -1173,7 +1174,7 @@ Channel(
id: "UCh8gHdtzO2tXd593_bjErWg",
name: "Doobydobap",
avatar: [],
verification: verified,
verification: Verified,
subscriber_count: Some(3740000),
)),
publish_date: "[date]",
@ -1187,7 +1188,7 @@ Channel(
VideoItem(
id: "1c3axhSJiaQ",
name: "I used to hate korean food",
duration: None,
length: None,
thumbnail: [
Thumbnail(
url: "https://i.ytimg.com/vi/1c3axhSJiaQ/hq720_2.jpg?sqp=-oaymwEdCJUDENAFSFXyq4qpAw8IARUAAIhCcAHAAQbQAQE=&rs=AOn4CLBucOEbTsWTDjOOCjNa-fAvz1yxyA",
@ -1199,7 +1200,7 @@ Channel(
id: "UCh8gHdtzO2tXd593_bjErWg",
name: "Doobydobap",
avatar: [],
verification: verified,
verification: Verified,
subscriber_count: Some(3740000),
)),
publish_date: "[date]",
@ -1213,7 +1214,7 @@ Channel(
VideoItem(
id: "F9Vz0m7DPeU",
name: "Rating everything I got at 7/11 Hawaii ( ft. Mauna Kea )",
duration: None,
length: None,
thumbnail: [
Thumbnail(
url: "https://i.ytimg.com/vi/F9Vz0m7DPeU/oar2.jpg?sqp=-oaymwEdCJUDENAFSFWQAgHyq4qpAwwIARUAAIhCcAHAAQY=&rs=AOn4CLD1w_8DhU37Mv_R3tQ9Kb6ouIU_VA",
@ -1225,7 +1226,7 @@ Channel(
id: "UCh8gHdtzO2tXd593_bjErWg",
name: "Doobydobap",
avatar: [],
verification: verified,
verification: Verified,
subscriber_count: Some(3740000),
)),
publish_date: "[date]",
@ -1239,7 +1240,7 @@ Channel(
VideoItem(
id: "Uey7kl56wks",
name: "Grabbing Snacks from 7/11 Hawaii",
duration: None,
length: None,
thumbnail: [
Thumbnail(
url: "https://i.ytimg.com/vi/Uey7kl56wks/oar2.jpg?sqp=-oaymwEdCJUDENAFSFWQAgHyq4qpAwwIARUAAIhCcAHAAQY=&rs=AOn4CLCybGwQAf9s43HoSQJGvWlOmmLPgw",
@ -1251,7 +1252,7 @@ Channel(
id: "UCh8gHdtzO2tXd593_bjErWg",
name: "Doobydobap",
avatar: [],
verification: verified,
verification: Verified,
subscriber_count: Some(3740000),
)),
publish_date: "[date]",
@ -1265,7 +1266,7 @@ Channel(
VideoItem(
id: "3un2eUAr6Dg",
name: "cheesy korean corn balls hit different",
duration: None,
length: None,
thumbnail: [
Thumbnail(
url: "https://i.ytimg.com/vi/3un2eUAr6Dg/oar2.jpg?sqp=-oaymwEdCJUDENAFSFWQAgHyq4qpAwwIARUAAIhCcAHAAQY=&rs=AOn4CLC9I2IQJx0hUPF9mcw4lvs1I6Bj4A",
@ -1277,7 +1278,7 @@ Channel(
id: "UCh8gHdtzO2tXd593_bjErWg",
name: "Doobydobap",
avatar: [],
verification: verified,
verification: Verified,
subscriber_count: Some(3740000),
)),
publish_date: "[date]",
@ -1291,7 +1292,7 @@ Channel(
VideoItem(
id: "rI5tWrGpDJA",
name: "hawaiian tajin?!?",
duration: None,
length: None,
thumbnail: [
Thumbnail(
url: "https://i.ytimg.com/vi/rI5tWrGpDJA/oar2.jpg?sqp=-oaymwEdCJUDENAFSFWQAgHyq4qpAwwIARUAAIhCcAHAAQY=&rs=AOn4CLCkg-RG2ToW-gXsSdYwO57sis0DkA",
@ -1303,7 +1304,7 @@ Channel(
id: "UCh8gHdtzO2tXd593_bjErWg",
name: "Doobydobap",
avatar: [],
verification: verified,
verification: Verified,
subscriber_count: Some(3740000),
)),
publish_date: "[date]",

View file

@ -5,9 +5,7 @@ expression: map_res.c
Channel(
id: "UCh8gHdtzO2tXd593_bjErWg",
name: "Doobydobap",
handle: None,
subscriber_count: Some(2930000),
video_count: None,
avatar: [
Thumbnail(
url: "https://yt3.ggpht.com/dm5Aq93xvVJz0NoVO88ieBkDXmuShCujGPlZ7qETMEPTrXvPUCFI3-BB6Xs_P-r6Uk3mnBy9zA=s48-c-k-c0x00ffffff-no-rj",
@ -25,9 +23,10 @@ Channel(
height: 176,
),
],
verification: verified,
verification: Verified,
description: "Hi, Im 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 its because youre hungry at midnight or trying to learn how to cook, I hope you enjoy watching my content and recipes. Don\'t yuck my yum!\n\nwww.doobydobap.com\n",
tags: [],
vanity_url: Some("https://www.youtube.com/c/Doobydobap"),
banner: [
Thumbnail(
url: "https://yt3.ggpht.com/BvnAqgiursrXpmS9AgDLtkOSTQfOG_Dqn0KzY5hcwO9XrHTEQTVgaflI913f9KRp7d0U2qBp=w1060-fcrop64=1,00005a57ffffa5a8-k-c0xffffffff-no-nd-rj",
@ -60,6 +59,60 @@ Channel(
height: 424,
),
],
mobile_banner: [
Thumbnail(
url: "https://yt3.ggpht.com/BvnAqgiursrXpmS9AgDLtkOSTQfOG_Dqn0KzY5hcwO9XrHTEQTVgaflI913f9KRp7d0U2qBp=w320-fcrop64=1,32b75a57cd48a5a8-k-c0xffffffff-no-nd-rj",
width: 320,
height: 88,
),
Thumbnail(
url: "https://yt3.ggpht.com/BvnAqgiursrXpmS9AgDLtkOSTQfOG_Dqn0KzY5hcwO9XrHTEQTVgaflI913f9KRp7d0U2qBp=w640-fcrop64=1,32b75a57cd48a5a8-k-c0xffffffff-no-nd-rj",
width: 640,
height: 175,
),
Thumbnail(
url: "https://yt3.ggpht.com/BvnAqgiursrXpmS9AgDLtkOSTQfOG_Dqn0KzY5hcwO9XrHTEQTVgaflI913f9KRp7d0U2qBp=w960-fcrop64=1,32b75a57cd48a5a8-k-c0xffffffff-no-nd-rj",
width: 960,
height: 263,
),
Thumbnail(
url: "https://yt3.ggpht.com/BvnAqgiursrXpmS9AgDLtkOSTQfOG_Dqn0KzY5hcwO9XrHTEQTVgaflI913f9KRp7d0U2qBp=w1280-fcrop64=1,32b75a57cd48a5a8-k-c0xffffffff-no-nd-rj",
width: 1280,
height: 351,
),
Thumbnail(
url: "https://yt3.ggpht.com/BvnAqgiursrXpmS9AgDLtkOSTQfOG_Dqn0KzY5hcwO9XrHTEQTVgaflI913f9KRp7d0U2qBp=w1440-fcrop64=1,32b75a57cd48a5a8-k-c0xffffffff-no-nd-rj",
width: 1440,
height: 395,
),
],
tv_banner: [
Thumbnail(
url: "https://yt3.ggpht.com/BvnAqgiursrXpmS9AgDLtkOSTQfOG_Dqn0KzY5hcwO9XrHTEQTVgaflI913f9KRp7d0U2qBp=w320-fcrop64=1,00000000ffffffff-k-c0xffffffff-no-nd-rj",
width: 320,
height: 180,
),
Thumbnail(
url: "https://yt3.ggpht.com/BvnAqgiursrXpmS9AgDLtkOSTQfOG_Dqn0KzY5hcwO9XrHTEQTVgaflI913f9KRp7d0U2qBp=w854-fcrop64=1,00000000ffffffff-k-c0xffffffff-no-nd-rj",
width: 854,
height: 480,
),
Thumbnail(
url: "https://yt3.ggpht.com/BvnAqgiursrXpmS9AgDLtkOSTQfOG_Dqn0KzY5hcwO9XrHTEQTVgaflI913f9KRp7d0U2qBp=w1280-fcrop64=1,00000000ffffffff-k-c0xffffffff-no-nd-rj",
width: 1280,
height: 720,
),
Thumbnail(
url: "https://yt3.ggpht.com/BvnAqgiursrXpmS9AgDLtkOSTQfOG_Dqn0KzY5hcwO9XrHTEQTVgaflI913f9KRp7d0U2qBp=w1920-fcrop64=1,00000000ffffffff-k-c0xffffffff-no-nd-rj",
width: 1920,
height: 1080,
),
Thumbnail(
url: "https://yt3.ggpht.com/BvnAqgiursrXpmS9AgDLtkOSTQfOG_Dqn0KzY5hcwO9XrHTEQTVgaflI913f9KRp7d0U2qBp=w2120-fcrop64=1,00000000ffffffff-k-c0xffffffff-no-nd-rj",
width: 2120,
height: 1192,
),
],
has_shorts: true,
has_live: false,
visitor_data: Some("CgtQdE9zVVR3NVBDbyjz0ZKaBg%3D%3D"),
@ -69,7 +122,7 @@ Channel(
VideoItem(
id: "EIcmfSzeaKk",
name: "our new normal",
duration: Some(1106),
length: Some(1106),
thumbnail: [
Thumbnail(
url: "https://i.ytimg.com/vi/EIcmfSzeaKk/hqdefault.jpg?sqp=-oaymwEbCKgBEF5IVfKriqkDDggBFQAAiEIYAXABwAEG&rs=AOn4CLAsYqYyFrXWHOkwiw0oqls2tGrKQg",
@ -96,7 +149,7 @@ Channel(
id: "UCh8gHdtzO2tXd593_bjErWg",
name: "Doobydobap",
avatar: [],
verification: verified,
verification: Verified,
subscriber_count: Some(2930000),
)),
publish_date: "[date]",
@ -110,7 +163,7 @@ Channel(
VideoItem(
id: "9NuhKCv3crg",
name: "the end.",
duration: Some(982),
length: Some(982),
thumbnail: [
Thumbnail(
url: "https://i.ytimg.com/vi/9NuhKCv3crg/hqdefault.jpg?sqp=-oaymwEbCKgBEF5IVfKriqkDDggBFQAAiEIYAXABwAEG&rs=AOn4CLDB0KHjIok8E-gjwidP56UeDJy7Bg",
@ -137,7 +190,7 @@ Channel(
id: "UCh8gHdtzO2tXd593_bjErWg",
name: "Doobydobap",
avatar: [],
verification: verified,
verification: Verified,
subscriber_count: Some(2930000),
)),
publish_date: "[date]",
@ -151,7 +204,7 @@ Channel(
VideoItem(
id: "38Gd6TdmNVs",
name: "KOREAN BARBECUE l doob gourmand ep.3",
duration: Some(525),
length: Some(525),
thumbnail: [
Thumbnail(
url: "https://i.ytimg.com/vi/38Gd6TdmNVs/hqdefault.jpg?sqp=-oaymwEbCKgBEF5IVfKriqkDDggBFQAAiEIYAXABwAEG&rs=AOn4CLBRo5niO28TGS9JNluTU9wCLCGBQA",
@ -178,7 +231,7 @@ Channel(
id: "UCh8gHdtzO2tXd593_bjErWg",
name: "Doobydobap",
avatar: [],
verification: verified,
verification: Verified,
subscriber_count: Some(2930000),
)),
publish_date: "[date]",
@ -192,7 +245,7 @@ Channel(
VideoItem(
id: "l9TiwunjzgA",
name: "long distance",
duration: Some(1043),
length: Some(1043),
thumbnail: [
Thumbnail(
url: "https://i.ytimg.com/vi/l9TiwunjzgA/hqdefault.jpg?sqp=-oaymwEbCKgBEF5IVfKriqkDDggBFQAAiEIYAXABwAEG&rs=AOn4CLDjM6SZ7ScyfFRr13QdVmIvWEWWrQ",
@ -219,7 +272,7 @@ Channel(
id: "UCh8gHdtzO2tXd593_bjErWg",
name: "Doobydobap",
avatar: [],
verification: verified,
verification: Verified,
subscriber_count: Some(2930000),
)),
publish_date: "[date]",
@ -233,7 +286,7 @@ Channel(
VideoItem(
id: "pRVSdUxdsVw",
name: "Repairing...",
duration: Some(965),
length: Some(965),
thumbnail: [
Thumbnail(
url: "https://i.ytimg.com/vi/pRVSdUxdsVw/hqdefault.jpg?sqp=-oaymwEbCKgBEF5IVfKriqkDDggBFQAAiEIYAXABwAEG&rs=AOn4CLAQWneuYcJcccgooBfa3WI4LdYF3w",
@ -260,7 +313,7 @@ Channel(
id: "UCh8gHdtzO2tXd593_bjErWg",
name: "Doobydobap",
avatar: [],
verification: verified,
verification: Verified,
subscriber_count: Some(2930000),
)),
publish_date: "[date]",
@ -274,7 +327,7 @@ Channel(
VideoItem(
id: "2FJVhdOO0F0",
name: "a health scare",
duration: Some(1238),
length: Some(1238),
thumbnail: [
Thumbnail(
url: "https://i.ytimg.com/vi/2FJVhdOO0F0/hqdefault.jpg?sqp=-oaymwEbCKgBEF5IVfKriqkDDggBFQAAiEIYAXABwAEG&rs=AOn4CLA5ambaz-euRsB9VG5ANaYFUUSEbg",
@ -301,7 +354,7 @@ Channel(
id: "UCh8gHdtzO2tXd593_bjErWg",
name: "Doobydobap",
avatar: [],
verification: verified,
verification: Verified,
subscriber_count: Some(2930000),
)),
publish_date: "[date]",
@ -315,7 +368,7 @@ Channel(
VideoItem(
id: "CutR_1SDDzY",
name: "feels good to be back",
duration: Some(1159),
length: Some(1159),
thumbnail: [
Thumbnail(
url: "https://i.ytimg.com/vi/CutR_1SDDzY/hqdefault.jpg?sqp=-oaymwEbCKgBEF5IVfKriqkDDggBFQAAiEIYAXABwAEG&rs=AOn4CLAt413Uk4xhHjYwpLI5-DXuOsFouA",
@ -342,7 +395,7 @@ Channel(
id: "UCh8gHdtzO2tXd593_bjErWg",
name: "Doobydobap",
avatar: [],
verification: verified,
verification: Verified,
subscriber_count: Some(2930000),
)),
publish_date: "[date]",
@ -356,7 +409,7 @@ Channel(
VideoItem(
id: "KUz7oArksR4",
name: "running away",
duration: Some(1023),
length: Some(1023),
thumbnail: [
Thumbnail(
url: "https://i.ytimg.com/vi/KUz7oArksR4/hqdefault.jpg?sqp=-oaymwEbCKgBEF5IVfKriqkDDggBFQAAiEIYAXABwAEG&rs=AOn4CLD1NwuIgJuJy2oPAiHqMre6rbcuPA",
@ -383,7 +436,7 @@ Channel(
id: "UCh8gHdtzO2tXd593_bjErWg",
name: "Doobydobap",
avatar: [],
verification: verified,
verification: Verified,
subscriber_count: Some(2930000),
)),
publish_date: "[date]",
@ -397,7 +450,7 @@ Channel(
VideoItem(
id: "sPb2gyN-hnE",
name: "worth fighting for",
duration: Some(1232),
length: Some(1232),
thumbnail: [
Thumbnail(
url: "https://i.ytimg.com/vi/sPb2gyN-hnE/hqdefault.jpg?sqp=-oaymwEbCKgBEF5IVfKriqkDDggBFQAAiEIYAXABwAEG&rs=AOn4CLBidXnS47SJMkvOlqt2DgzHxr6wKQ",
@ -424,7 +477,7 @@ Channel(
id: "UCh8gHdtzO2tXd593_bjErWg",
name: "Doobydobap",
avatar: [],
verification: verified,
verification: Verified,
subscriber_count: Some(2930000),
)),
publish_date: "[date]",
@ -438,7 +491,7 @@ Channel(
VideoItem(
id: "PXsK9-CFoH4",
name: "waiting...",
duration: Some(1455),
length: Some(1455),
thumbnail: [
Thumbnail(
url: "https://i.ytimg.com/vi/PXsK9-CFoH4/hqdefault.jpg?sqp=-oaymwEbCKgBEF5IVfKriqkDDggBFQAAiEIYAXABwAEG&rs=AOn4CLBJ-57qZ-dOIsdFy5H8WT9UsS2W9w",
@ -465,7 +518,7 @@ Channel(
id: "UCh8gHdtzO2tXd593_bjErWg",
name: "Doobydobap",
avatar: [],
verification: verified,
verification: Verified,
subscriber_count: Some(2930000),
)),
publish_date: "[date]",
@ -479,7 +532,7 @@ Channel(
VideoItem(
id: "r2ye6zW0nbM",
name: "a wedding",
duration: Some(1207),
length: Some(1207),
thumbnail: [
Thumbnail(
url: "https://i.ytimg.com/vi/r2ye6zW0nbM/hqdefault.jpg?sqp=-oaymwEbCKgBEF5IVfKriqkDDggBFQAAiEIYAXABwAEG&rs=AOn4CLB3L2DVtMtxaPaFjVPcNnjDHE5Wvw",
@ -506,7 +559,7 @@ Channel(
id: "UCh8gHdtzO2tXd593_bjErWg",
name: "Doobydobap",
avatar: [],
verification: verified,
verification: Verified,
subscriber_count: Some(2930000),
)),
publish_date: "[date]",
@ -520,7 +573,7 @@ Channel(
VideoItem(
id: "rriwHj8U664",
name: "my seoul apartment tour",
duration: Some(721),
length: Some(721),
thumbnail: [
Thumbnail(
url: "https://i.ytimg.com/vi/rriwHj8U664/hqdefault.jpg?sqp=-oaymwEbCKgBEF5IVfKriqkDDggBFQAAiEIYAXABwAEG&rs=AOn4CLCy6zauLaf2KLJ6R41q0CPM8298PA",
@ -547,7 +600,7 @@ Channel(
id: "UCh8gHdtzO2tXd593_bjErWg",
name: "Doobydobap",
avatar: [],
verification: verified,
verification: Verified,
subscriber_count: Some(2930000),
)),
publish_date: "[date]",
@ -561,7 +614,7 @@ Channel(
VideoItem(
id: "FKJtrUeol3o",
name: "with quantity comes quality",
duration: Some(1140),
length: Some(1140),
thumbnail: [
Thumbnail(
url: "https://i.ytimg.com/vi/FKJtrUeol3o/hqdefault.jpg?sqp=-oaymwEbCKgBEF5IVfKriqkDDggBFQAAiEIYAXABwAEG&rs=AOn4CLD7U0hZPrEiHZcTVcicymOllR05qw",
@ -588,7 +641,7 @@ Channel(
id: "UCh8gHdtzO2tXd593_bjErWg",
name: "Doobydobap",
avatar: [],
verification: verified,
verification: Verified,
subscriber_count: Some(2930000),
)),
publish_date: "[date]",
@ -602,7 +655,7 @@ Channel(
VideoItem(
id: "zYHB38UlzE0",
name: "Q&A l relationships, burnout, privilege, college advice, living alone, and life after youtube?",
duration: Some(775),
length: Some(775),
thumbnail: [
Thumbnail(
url: "https://i.ytimg.com/vi/zYHB38UlzE0/hqdefault.jpg?sqp=-oaymwEbCKgBEF5IVfKriqkDDggBFQAAiEIYAXABwAEG&rs=AOn4CLDAO5etokCiF7cvyR-7kobN9RhTLA",
@ -629,7 +682,7 @@ Channel(
id: "UCh8gHdtzO2tXd593_bjErWg",
name: "Doobydobap",
avatar: [],
verification: verified,
verification: Verified,
subscriber_count: Some(2930000),
)),
publish_date: "[date]",
@ -643,7 +696,7 @@ Channel(
VideoItem(
id: "hGbQ2WM9nOo",
name: "Why does everything bad for you taste good ㅣ CHILI OIL RAMEN",
duration: Some(428),
length: Some(428),
thumbnail: [
Thumbnail(
url: "https://i.ytimg.com/vi/hGbQ2WM9nOo/hqdefault.jpg?sqp=-oaymwEbCKgBEF5IVfKriqkDDggBFQAAiEIYAXABwAEG&rs=AOn4CLD_bMKoJhW-ifemEiqSBj-6dvEnUg",
@ -670,7 +723,7 @@ Channel(
id: "UCh8gHdtzO2tXd593_bjErWg",
name: "Doobydobap",
avatar: [],
verification: verified,
verification: Verified,
subscriber_count: Some(2930000),
)),
publish_date: "[date]",
@ -684,7 +737,7 @@ Channel(
VideoItem(
id: "PxGmP4v_A38",
name: "Alone and Thriving l late night korean convenience store, muji kitchenware haul, spring cleaning!",
duration: Some(1437),
length: Some(1437),
thumbnail: [
Thumbnail(
url: "https://i.ytimg.com/vi/PxGmP4v_A38/hqdefault.jpg?sqp=-oaymwEbCKgBEF5IVfKriqkDDggBFQAAiEIYAXABwAEG&rs=AOn4CLArZRyFU5e71-vMdGZzuxCCroEkww",
@ -711,7 +764,7 @@ Channel(
id: "UCh8gHdtzO2tXd593_bjErWg",
name: "Doobydobap",
avatar: [],
verification: verified,
verification: Verified,
subscriber_count: Some(2930000),
)),
publish_date: "[date]",
@ -725,7 +778,7 @@ Channel(
VideoItem(
id: "8t-WyYcpEDE",
name: "What I hate most",
duration: Some(61),
length: Some(61),
thumbnail: [
Thumbnail(
url: "https://i.ytimg.com/vi/8t-WyYcpEDE/hqdefault.jpg?sqp=-oaymwEbCKgBEF5IVfKriqkDDggBFQAAiEIYAXABwAEG&rs=AOn4CLDsJHHXMP4fUEFqn-LExXU5yPyZ-Q",
@ -752,7 +805,7 @@ Channel(
id: "UCh8gHdtzO2tXd593_bjErWg",
name: "Doobydobap",
avatar: [],
verification: verified,
verification: Verified,
subscriber_count: Some(2930000),
)),
publish_date: "[date]",
@ -766,7 +819,7 @@ Channel(
VideoItem(
id: "RroYpLxxNjY",
name: "I\'m Back. ㅣ cooking korean food, eating alone, working out, and 2M!",
duration: Some(1313),
length: Some(1313),
thumbnail: [
Thumbnail(
url: "https://i.ytimg.com/vi/RroYpLxxNjY/hqdefault.jpg?sqp=-oaymwEbCKgBEF5IVfKriqkDDggBFQAAiEIYAXABwAEG&rs=AOn4CLBYJ_dcqUt2kR-4jOAUu8O0Ja9SLA",
@ -793,7 +846,7 @@ Channel(
id: "UCh8gHdtzO2tXd593_bjErWg",
name: "Doobydobap",
avatar: [],
verification: verified,
verification: Verified,
subscriber_count: Some(2930000),
)),
publish_date: "[date]",
@ -807,7 +860,7 @@ Channel(
VideoItem(
id: "l47QuudsZ34",
name: "We ate our way through Florence (ft. mamadooby)",
duration: Some(1109),
length: Some(1109),
thumbnail: [
Thumbnail(
url: "https://i.ytimg.com/vi/l47QuudsZ34/hqdefault.jpg?sqp=-oaymwEbCKgBEF5IVfKriqkDDggBFQAAiEIYAXABwAEG&rs=AOn4CLB0Vwc7DhN_hFXSRuDAiivLnGGc2A",
@ -834,7 +887,7 @@ Channel(
id: "UCh8gHdtzO2tXd593_bjErWg",
name: "Doobydobap",
avatar: [],
verification: verified,
verification: Verified,
subscriber_count: Some(2930000),
)),
publish_date: "[date]",
@ -848,7 +901,7 @@ Channel(
VideoItem(
id: "1VW7iXRIrc8",
name: "Alone, in the City of Love",
duration: Some(1875),
length: Some(1875),
thumbnail: [
Thumbnail(
url: "https://i.ytimg.com/vi/1VW7iXRIrc8/hqdefault.jpg?sqp=-oaymwEbCKgBEF5IVfKriqkDDggBFQAAiEIYAXABwAEG&rs=AOn4CLBbNxPLmGzJlvJ-3o5Dz9I5LOGu1A",
@ -875,7 +928,7 @@ Channel(
id: "UCh8gHdtzO2tXd593_bjErWg",
name: "Doobydobap",
avatar: [],
verification: verified,
verification: Verified,
subscriber_count: Some(2930000),
)),
publish_date: "[date]",
@ -889,7 +942,7 @@ Channel(
VideoItem(
id: "6c58-749p6Y",
name: "Old Friends & New",
duration: Some(774),
length: Some(774),
thumbnail: [
Thumbnail(
url: "https://i.ytimg.com/vi/6c58-749p6Y/hqdefault.jpg?sqp=-oaymwEbCKgBEF5IVfKriqkDDggBFQAAiEIYAXABwAEG&rs=AOn4CLClRrTlOF_Q3feHLoM0T5_DFygbIw",
@ -916,7 +969,7 @@ Channel(
id: "UCh8gHdtzO2tXd593_bjErWg",
name: "Doobydobap",
avatar: [],
verification: verified,
verification: Verified,
subscriber_count: Some(2930000),
)),
publish_date: "[date]",
@ -930,7 +983,7 @@ Channel(
VideoItem(
id: "Q2G53LuEUaU",
name: "Where we stand",
duration: Some(858),
length: Some(858),
thumbnail: [
Thumbnail(
url: "https://i.ytimg.com/vi/Q2G53LuEUaU/hqdefault.jpg?sqp=-oaymwEbCKgBEF5IVfKriqkDDggBFQAAiEIYAXABwAEG&rs=AOn4CLC1ppNrqK-xlQ6Sxnn62dp8QXoJBQ",
@ -957,7 +1010,7 @@ Channel(
id: "UCh8gHdtzO2tXd593_bjErWg",
name: "Doobydobap",
avatar: [],
verification: verified,
verification: Verified,
subscriber_count: Some(2930000),
)),
publish_date: "[date]",
@ -971,7 +1024,7 @@ Channel(
VideoItem(
id: "8rAOeowNQrI",
name: "That\'s so last year",
duration: Some(1286),
length: Some(1286),
thumbnail: [
Thumbnail(
url: "https://i.ytimg.com/vi/8rAOeowNQrI/hqdefault.jpg?sqp=-oaymwEbCKgBEF5IVfKriqkDDggBFQAAiEIYAXABwAEG&rs=AOn4CLCSBW_fD0pttfFh4Yc_Kx1UIZHzfg",
@ -998,7 +1051,7 @@ Channel(
id: "UCh8gHdtzO2tXd593_bjErWg",
name: "Doobydobap",
avatar: [],
verification: verified,
verification: Verified,
subscriber_count: Some(2930000),
)),
publish_date: "[date]",
@ -1012,7 +1065,7 @@ Channel(
VideoItem(
id: "0RGIdIKkbSI",
name: "The Muffin Man",
duration: Some(1052),
length: Some(1052),
thumbnail: [
Thumbnail(
url: "https://i.ytimg.com/vi/0RGIdIKkbSI/hqdefault.jpg?sqp=-oaymwEbCKgBEF5IVfKriqkDDggBFQAAiEIYAXABwAEG&rs=AOn4CLDo42DBFMfLKVHtXETG5yuU20FVMw",
@ -1039,7 +1092,7 @@ Channel(
id: "UCh8gHdtzO2tXd593_bjErWg",
name: "Doobydobap",
avatar: [],
verification: verified,
verification: Verified,
subscriber_count: Some(2930000),
)),
publish_date: "[date]",
@ -1053,7 +1106,7 @@ Channel(
VideoItem(
id: "NudTbo2CJMY",
name: "Flying to London",
duration: Some(1078),
length: Some(1078),
thumbnail: [
Thumbnail(
url: "https://i.ytimg.com/vi/NudTbo2CJMY/hqdefault.jpg?sqp=-oaymwEbCKgBEF5IVfKriqkDDggBFQAAiEIYAXABwAEG&rs=AOn4CLDEdvWWmhSaDTTx7b2kJUauMFnQJQ",
@ -1080,7 +1133,7 @@ Channel(
id: "UCh8gHdtzO2tXd593_bjErWg",
name: "Doobydobap",
avatar: [],
verification: verified,
verification: Verified,
subscriber_count: Some(2930000),
)),
publish_date: "[date]",
@ -1094,7 +1147,7 @@ Channel(
VideoItem(
id: "8mJk1ncGZig",
name: "(not so) Teenage Angst",
duration: Some(1376),
length: Some(1376),
thumbnail: [
Thumbnail(
url: "https://i.ytimg.com/vi/8mJk1ncGZig/hqdefault.jpg?sqp=-oaymwEbCKgBEF5IVfKriqkDDggBFQAAiEIYAXABwAEG&rs=AOn4CLB1kTcJ6oRyNfaGJbvl6V5UxRhagg",
@ -1121,7 +1174,7 @@ Channel(
id: "UCh8gHdtzO2tXd593_bjErWg",
name: "Doobydobap",
avatar: [],
verification: verified,
verification: Verified,
subscriber_count: Some(2930000),
)),
publish_date: "[date]",
@ -1135,7 +1188,7 @@ Channel(
VideoItem(
id: "qvgCi2WpbfE",
name: "can\'t smell :s",
duration: Some(875),
length: Some(875),
thumbnail: [
Thumbnail(
url: "https://i.ytimg.com/vi/qvgCi2WpbfE/hqdefault.jpg?sqp=-oaymwEbCKgBEF5IVfKriqkDDggBFQAAiEIYAXABwAEG&rs=AOn4CLBdSLKkLlOTxprZAH9BajRpHiujrw",
@ -1162,7 +1215,7 @@ Channel(
id: "UCh8gHdtzO2tXd593_bjErWg",
name: "Doobydobap",
avatar: [],
verification: verified,
verification: Verified,
subscriber_count: Some(2930000),
)),
publish_date: "[date]",
@ -1176,7 +1229,7 @@ Channel(
VideoItem(
id: "Sm4Yqtqr9f8",
name: "I have covid",
duration: Some(814),
length: Some(814),
thumbnail: [
Thumbnail(
url: "https://i.ytimg.com/vi/Sm4Yqtqr9f8/hqdefault.jpg?sqp=-oaymwEbCKgBEF5IVfKriqkDDggBFQAAiEIYAXABwAEG&rs=AOn4CLDAAWaXioP-Xz_cwkE3APR_5fpkqw",
@ -1203,7 +1256,7 @@ Channel(
id: "UCh8gHdtzO2tXd593_bjErWg",
name: "Doobydobap",
avatar: [],
verification: verified,
verification: Verified,
subscriber_count: Some(2930000),
)),
publish_date: "[date]",
@ -1217,7 +1270,7 @@ Channel(
VideoItem(
id: "ZRtf4ksF3qs",
name: "Everything I ate in Busan & make up tutorial??",
duration: Some(1026),
length: Some(1026),
thumbnail: [
Thumbnail(
url: "https://i.ytimg.com/vi/ZRtf4ksF3qs/hqdefault.jpg?sqp=-oaymwEbCKgBEF5IVfKriqkDDggBFQAAiEIYAXABwAEG&rs=AOn4CLBnRStN9mU3cu7vDQIkUcO3WiyVZw",
@ -1244,7 +1297,7 @@ Channel(
id: "UCh8gHdtzO2tXd593_bjErWg",
name: "Doobydobap",
avatar: [],
verification: verified,
verification: Verified,
subscriber_count: Some(2930000),
)),
publish_date: "[date]",
@ -1258,7 +1311,7 @@ Channel(
VideoItem(
id: "oG4Wth1oVBQ",
name: "On the other side",
duration: Some(1592),
length: Some(1592),
thumbnail: [
Thumbnail(
url: "https://i.ytimg.com/vi/oG4Wth1oVBQ/hqdefault.jpg?sqp=-oaymwEbCKgBEF5IVfKriqkDDggBFQAAiEIYAXABwAEG&rs=AOn4CLDO45Wm2zkuD6ZukxaoxfgGkpuZHg",
@ -1285,7 +1338,7 @@ Channel(
id: "UCh8gHdtzO2tXd593_bjErWg",
name: "Doobydobap",
avatar: [],
verification: verified,
verification: Verified,
subscriber_count: Some(2930000),
)),
publish_date: "[date]",

View file

@ -5,9 +5,7 @@ expression: map_res.c
Channel(
id: "UC2DjFE7Xf11URZqWBigcVOQ",
name: "EEVblog",
handle: None,
subscriber_count: Some(883000),
video_count: None,
avatar: [
Thumbnail(
url: "https://yt3.ggpht.com/ytc/AMLnZu9eKk4Nd16fX4Rn1TF1G7ReluwOl6M5558FTYAM=s48-c-k-c0x00ffffff-no-rj",
@ -25,7 +23,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",
@ -57,6 +55,7 @@ Channel(
"dumpster diving",
"debunking",
],
vanity_url: Some("https://www.youtube.com/c/EevblogDave"),
banner: [
Thumbnail(
url: "https://yt3.ggpht.com/yIJ9ad80n49rK-YUcZLe_8bLmR-aGyg5ybDH_XKIc0GDWrC6s1Wzz8lxnq3_hux_5b6NHPZ9=w1060-fcrop64=1,00005a57ffffa5a8-k-c0xffffffff-no-nd-rj",
@ -89,6 +88,60 @@ Channel(
height: 424,
),
],
mobile_banner: [
Thumbnail(
url: "https://yt3.ggpht.com/yIJ9ad80n49rK-YUcZLe_8bLmR-aGyg5ybDH_XKIc0GDWrC6s1Wzz8lxnq3_hux_5b6NHPZ9=w320-fcrop64=1,32b75a57cd48a5a8-k-c0xffffffff-no-nd-rj",
width: 320,
height: 88,
),
Thumbnail(
url: "https://yt3.ggpht.com/yIJ9ad80n49rK-YUcZLe_8bLmR-aGyg5ybDH_XKIc0GDWrC6s1Wzz8lxnq3_hux_5b6NHPZ9=w640-fcrop64=1,32b75a57cd48a5a8-k-c0xffffffff-no-nd-rj",
width: 640,
height: 175,
),
Thumbnail(
url: "https://yt3.ggpht.com/yIJ9ad80n49rK-YUcZLe_8bLmR-aGyg5ybDH_XKIc0GDWrC6s1Wzz8lxnq3_hux_5b6NHPZ9=w960-fcrop64=1,32b75a57cd48a5a8-k-c0xffffffff-no-nd-rj",
width: 960,
height: 263,
),
Thumbnail(
url: "https://yt3.ggpht.com/yIJ9ad80n49rK-YUcZLe_8bLmR-aGyg5ybDH_XKIc0GDWrC6s1Wzz8lxnq3_hux_5b6NHPZ9=w1280-fcrop64=1,32b75a57cd48a5a8-k-c0xffffffff-no-nd-rj",
width: 1280,
height: 351,
),
Thumbnail(
url: "https://yt3.ggpht.com/yIJ9ad80n49rK-YUcZLe_8bLmR-aGyg5ybDH_XKIc0GDWrC6s1Wzz8lxnq3_hux_5b6NHPZ9=w1440-fcrop64=1,32b75a57cd48a5a8-k-c0xffffffff-no-nd-rj",
width: 1440,
height: 395,
),
],
tv_banner: [
Thumbnail(
url: "https://yt3.ggpht.com/yIJ9ad80n49rK-YUcZLe_8bLmR-aGyg5ybDH_XKIc0GDWrC6s1Wzz8lxnq3_hux_5b6NHPZ9=w320-fcrop64=1,00000000ffffffff-k-c0xffffffff-no-nd-rj",
width: 320,
height: 180,
),
Thumbnail(
url: "https://yt3.ggpht.com/yIJ9ad80n49rK-YUcZLe_8bLmR-aGyg5ybDH_XKIc0GDWrC6s1Wzz8lxnq3_hux_5b6NHPZ9=w854-fcrop64=1,00000000ffffffff-k-c0xffffffff-no-nd-rj",
width: 854,
height: 480,
),
Thumbnail(
url: "https://yt3.ggpht.com/yIJ9ad80n49rK-YUcZLe_8bLmR-aGyg5ybDH_XKIc0GDWrC6s1Wzz8lxnq3_hux_5b6NHPZ9=w1280-fcrop64=1,00000000ffffffff-k-c0xffffffff-no-nd-rj",
width: 1280,
height: 720,
),
Thumbnail(
url: "https://yt3.ggpht.com/yIJ9ad80n49rK-YUcZLe_8bLmR-aGyg5ybDH_XKIc0GDWrC6s1Wzz8lxnq3_hux_5b6NHPZ9=w1920-fcrop64=1,00000000ffffffff-k-c0xffffffff-no-nd-rj",
width: 1920,
height: 1080,
),
Thumbnail(
url: "https://yt3.ggpht.com/yIJ9ad80n49rK-YUcZLe_8bLmR-aGyg5ybDH_XKIc0GDWrC6s1Wzz8lxnq3_hux_5b6NHPZ9=w2120-fcrop64=1,00000000ffffffff-k-c0xffffffff-no-nd-rj",
width: 2120,
height: 1192,
),
],
has_shorts: false,
has_live: true,
visitor_data: Some("Cgs4ZFVmMzVlU1dxbyiBqpeaBg%3D%3D"),
@ -98,7 +151,7 @@ Channel(
VideoItem(
id: "4EcQYK_no5M",
name: "EEVblog 1506 - History of Electricity with Kathy Loves Physics",
duration: Some(6143),
length: Some(6143),
thumbnail: [
Thumbnail(
url: "https://i.ytimg.com/vi/4EcQYK_no5M/hqdefault.jpg?sqp=-oaymwEbCKgBEF5IVfKriqkDDggBFQAAiEIYAXABwAEG&rs=AOn4CLB9dr9RxHmrRUim7aDSz_mPNrfSKA",
@ -125,7 +178,7 @@ Channel(
id: "UC2DjFE7Xf11URZqWBigcVOQ",
name: "EEVblog",
avatar: [],
verification: verified,
verification: Verified,
subscriber_count: Some(883000),
)),
publish_date: "[date]",
@ -139,7 +192,7 @@ Channel(
VideoItem(
id: "zEzjVUzNAFA",
name: "EEVblog 1505 - 120W Home Phantom Power? Audit Time!",
duration: Some(1464),
length: Some(1464),
thumbnail: [
Thumbnail(
url: "https://i.ytimg.com/vi/zEzjVUzNAFA/hqdefault.jpg?sqp=-oaymwEbCKgBEF5IVfKriqkDDggBFQAAiEIYAXABwAEG&rs=AOn4CLDnSdLCdtqGA1HYCFv4_MeTHWdVpw",
@ -166,7 +219,7 @@ Channel(
id: "UC2DjFE7Xf11URZqWBigcVOQ",
name: "EEVblog",
avatar: [],
verification: verified,
verification: Verified,
subscriber_count: Some(883000),
)),
publish_date: "[date]",
@ -180,7 +233,7 @@ Channel(
VideoItem(
id: "YIbQ3nudCA0",
name: "EEVblog 1504 - The COOL thing you MISSED at Tesla AI Day 2022",
duration: Some(1021),
length: Some(1021),
thumbnail: [
Thumbnail(
url: "https://i.ytimg.com/vi/YIbQ3nudCA0/hqdefault.jpg?sqp=-oaymwEbCKgBEF5IVfKriqkDDggBFQAAiEIYAXABwAEG&rs=AOn4CLDesF0deVLo0ouizZ8ZF_lXolOdrw",
@ -207,7 +260,7 @@ Channel(
id: "UC2DjFE7Xf11URZqWBigcVOQ",
name: "EEVblog",
avatar: [],
verification: verified,
verification: Verified,
subscriber_count: Some(883000),
)),
publish_date: "[date]",
@ -221,7 +274,7 @@ Channel(
VideoItem(
id: "W1Jl0rMRGSg",
name: "EEVblog 1503 - Rigol HDO4000 12bit Oscilloscope TEARDOWN",
duration: Some(1798),
length: Some(1798),
thumbnail: [
Thumbnail(
url: "https://i.ytimg.com/vi/W1Jl0rMRGSg/hqdefault.jpg?sqp=-oaymwEbCKgBEF5IVfKriqkDDggBFQAAiEIYAXABwAEG&rs=AOn4CLBKFi3YtWo1ii8h8FdQN6CkYgzX2A",
@ -248,7 +301,7 @@ Channel(
id: "UC2DjFE7Xf11URZqWBigcVOQ",
name: "EEVblog",
avatar: [],
verification: verified,
verification: Verified,
subscriber_count: Some(883000),
)),
publish_date: "[date]",
@ -262,7 +315,7 @@ Channel(
VideoItem(
id: "YFKu_emNzpk",
name: "EEVblog 1502 - Is Home Battery Storage Financially Viable?",
duration: Some(1199),
length: Some(1199),
thumbnail: [
Thumbnail(
url: "https://i.ytimg.com/vi/YFKu_emNzpk/hqdefault.jpg?sqp=-oaymwEbCKgBEF5IVfKriqkDDggBFQAAiEIYAXABwAEG&rs=AOn4CLACI3L7nXsK3ZUFD8yK0VAWd32-Uw",
@ -289,7 +342,7 @@ Channel(
id: "UC2DjFE7Xf11URZqWBigcVOQ",
name: "EEVblog",
avatar: [],
verification: verified,
verification: Verified,
subscriber_count: Some(883000),
)),
publish_date: "[date]",
@ -303,7 +356,7 @@ Channel(
VideoItem(
id: "gremHHvqYTE",
name: "EEVblog 1501 - Rigol HDO4000 Low Noise 12bit Oscilloscope Unboxing & First Impression",
duration: Some(1794),
length: Some(1794),
thumbnail: [
Thumbnail(
url: "https://i.ytimg.com/vi/gremHHvqYTE/hqdefault.jpg?sqp=-oaymwEbCKgBEF5IVfKriqkDDggBFQAAiEIYAXABwAEG&rs=AOn4CLBcwR0YIwLjfFam9HkKdkTkqx_gHw",
@ -330,7 +383,7 @@ Channel(
id: "UC2DjFE7Xf11URZqWBigcVOQ",
name: "EEVblog",
avatar: [],
verification: verified,
verification: Verified,
subscriber_count: Some(883000),
)),
publish_date: "[date]",
@ -344,7 +397,7 @@ Channel(
VideoItem(
id: "WHO8NBfpaO0",
name: "eevBLAB 102 - Last Mile Autonomous Robot Deliveries WILL FAIL",
duration: Some(742),
length: Some(742),
thumbnail: [
Thumbnail(
url: "https://i.ytimg.com/vi/WHO8NBfpaO0/hqdefault.jpg?sqp=-oaymwEbCKgBEF5IVfKriqkDDggBFQAAiEIYAXABwAEG&rs=AOn4CLDQPKMF3Aeo9CydEWz9pQWkn1Lu7Q",
@ -371,7 +424,7 @@ Channel(
id: "UC2DjFE7Xf11URZqWBigcVOQ",
name: "EEVblog",
avatar: [],
verification: verified,
verification: Verified,
subscriber_count: Some(883000),
)),
publish_date: "[date]",
@ -385,7 +438,7 @@ Channel(
VideoItem(
id: "W1Q8CxL95_Y",
name: "EEVblog 1500 - Automatic Transfer Switch REVERSE ENGINEERED",
duration: Some(1770),
length: Some(1770),
thumbnail: [
Thumbnail(
url: "https://i.ytimg.com/vi/W1Q8CxL95_Y/hqdefault.jpg?sqp=-oaymwEbCKgBEF5IVfKriqkDDggBFQAAiEIYAXABwAEG&rs=AOn4CLBIxuct8vahJHOJTLfbOnsMOXnjvw",
@ -412,7 +465,7 @@ Channel(
id: "UC2DjFE7Xf11URZqWBigcVOQ",
name: "EEVblog",
avatar: [],
verification: verified,
verification: Verified,
subscriber_count: Some(883000),
)),
publish_date: "[date]",
@ -426,7 +479,7 @@ Channel(
VideoItem(
id: "lagxSrPeoYg",
name: "EEVblog 1499 - EcoFlow Delta Pro 3.6kWh Portable Battery TEARDOWN!",
duration: Some(2334),
length: Some(2334),
thumbnail: [
Thumbnail(
url: "https://i.ytimg.com/vi/lagxSrPeoYg/hqdefault.jpg?sqp=-oaymwEbCKgBEF5IVfKriqkDDggBFQAAiEIYAXABwAEG&rs=AOn4CLAbAX2gdAF66O7BUCaOVg2vQOsS2Q",
@ -453,7 +506,7 @@ Channel(
id: "UC2DjFE7Xf11URZqWBigcVOQ",
name: "EEVblog",
avatar: [],
verification: verified,
verification: Verified,
subscriber_count: Some(883000),
)),
publish_date: "[date]",
@ -467,7 +520,7 @@ Channel(
VideoItem(
id: "qTctWW9_FmE",
name: "EEVblog 1498 - TransPod Fluxjet Hyperloop $550M Boondoggle!",
duration: Some(2399),
length: Some(2399),
thumbnail: [
Thumbnail(
url: "https://i.ytimg.com/vi/qTctWW9_FmE/hqdefault.jpg?sqp=-oaymwEbCKgBEF5IVfKriqkDDggBFQAAiEIYAXABwAEG&rs=AOn4CLCbnEQaGGI5zD9lCJ8kMmciezX2kA",
@ -494,7 +547,7 @@ Channel(
id: "UC2DjFE7Xf11URZqWBigcVOQ",
name: "EEVblog",
avatar: [],
verification: verified,
verification: Verified,
subscriber_count: Some(883000),
)),
publish_date: "[date]",
@ -508,7 +561,7 @@ Channel(
VideoItem(
id: "3t9G80wk0pk",
name: "eevBLAB 101 - Why Are Tektronix Oscilloscopes So Expensive?",
duration: Some(1423),
length: Some(1423),
thumbnail: [
Thumbnail(
url: "https://i.ytimg.com/vi/3t9G80wk0pk/hqdefault.jpg?sqp=-oaymwEbCKgBEF5IVfKriqkDDggBFQAAiEIYAXABwAEG&rs=AOn4CLDnsVu-VQplpRpc1ZW-yk2byyZjZA",
@ -535,7 +588,7 @@ Channel(
id: "UC2DjFE7Xf11URZqWBigcVOQ",
name: "EEVblog",
avatar: [],
verification: verified,
verification: Verified,
subscriber_count: Some(883000),
)),
publish_date: "[date]",
@ -549,7 +602,7 @@ Channel(
VideoItem(
id: "7dze5CnZnmk",
name: "EEVblog 1497 - RIP Fluke. Thanks Energizer. NOT.",
duration: Some(1168),
length: Some(1168),
thumbnail: [
Thumbnail(
url: "https://i.ytimg.com/vi/7dze5CnZnmk/hqdefault.jpg?sqp=-oaymwEbCKgBEF5IVfKriqkDDggBFQAAiEIYAXABwAEG&rs=AOn4CLAg430MYAmoycM4lbv_57S_d3kZRA",
@ -576,7 +629,7 @@ Channel(
id: "UC2DjFE7Xf11URZqWBigcVOQ",
name: "EEVblog",
avatar: [],
verification: verified,
verification: Verified,
subscriber_count: Some(883000),
)),
publish_date: "[date]",
@ -590,7 +643,7 @@ Channel(
VideoItem(
id: "6XnrZpPYgBg",
name: "EEVblog 1496 - Winning Mailbag",
duration: Some(3139),
length: Some(3139),
thumbnail: [
Thumbnail(
url: "https://i.ytimg.com/vi/6XnrZpPYgBg/hqdefault.jpg?sqp=-oaymwEbCKgBEF5IVfKriqkDDggBFQAAiEIYAXABwAEG&rs=AOn4CLCrBgky13jB1p9xzKbmoUpJ4g0SNQ",
@ -617,7 +670,7 @@ Channel(
id: "UC2DjFE7Xf11URZqWBigcVOQ",
name: "EEVblog",
avatar: [],
verification: verified,
verification: Verified,
subscriber_count: Some(883000),
)),
publish_date: "[date]",
@ -631,7 +684,7 @@ Channel(
VideoItem(
id: "Psp3ltpFvws",
name: "eevBLAB 100 - Reuters Attacks Odysee - LOL",
duration: Some(855),
length: Some(855),
thumbnail: [
Thumbnail(
url: "https://i.ytimg.com/vi/Psp3ltpFvws/hqdefault.jpg?sqp=-oaymwEbCKgBEF5IVfKriqkDDggBFQAAiEIYAXABwAEG&rs=AOn4CLCu8Nu_NmDw5vBHgb7e8JdJR1Dr1Q",
@ -658,7 +711,7 @@ Channel(
id: "UC2DjFE7Xf11URZqWBigcVOQ",
name: "EEVblog",
avatar: [],
verification: verified,
verification: Verified,
subscriber_count: Some(883000),
)),
publish_date: "[date]",
@ -672,7 +725,7 @@ Channel(
VideoItem(
id: "taVYTYz5vLE",
name: "EEVblog 1495 - Quaze Wireless Power (AGAIN!) but for GAMING!",
duration: Some(2592),
length: Some(2592),
thumbnail: [
Thumbnail(
url: "https://i.ytimg.com/vi/taVYTYz5vLE/hqdefault.jpg?sqp=-oaymwEbCKgBEF5IVfKriqkDDggBFQAAiEIYAXABwAEG&rs=AOn4CLAMHngmN8TjWZz327vUD7zjjblYBw",
@ -699,7 +752,7 @@ Channel(
id: "UC2DjFE7Xf11URZqWBigcVOQ",
name: "EEVblog",
avatar: [],
verification: verified,
verification: Verified,
subscriber_count: Some(883000),
)),
publish_date: "[date]",
@ -713,7 +766,7 @@ Channel(
VideoItem(
id: "Y6cZrieFw-k",
name: "EEVblog 1494 - FIVE Ways to Open a CHEAP SAFE!",
duration: Some(1194),
length: Some(1194),
thumbnail: [
Thumbnail(
url: "https://i.ytimg.com/vi/Y6cZrieFw-k/hqdefault.jpg?sqp=-oaymwEbCKgBEF5IVfKriqkDDggBFQAAiEIYAXABwAEG&rs=AOn4CLDsdoJwcvSFZU4e9cwDFbZj3W21Pw",
@ -740,7 +793,7 @@ Channel(
id: "UC2DjFE7Xf11URZqWBigcVOQ",
name: "EEVblog",
avatar: [],
verification: verified,
verification: Verified,
subscriber_count: Some(883000),
)),
publish_date: "[date]",
@ -754,7 +807,7 @@ Channel(
VideoItem(
id: "Kr2XyhpUdUI",
name: "EEVblog 1493 - MacGyver Project - Part 2",
duration: Some(1785),
length: Some(1785),
thumbnail: [
Thumbnail(
url: "https://i.ytimg.com/vi/Kr2XyhpUdUI/hqdefault.jpg?sqp=-oaymwEbCKgBEF5IVfKriqkDDggBFQAAiEIYAXABwAEG&rs=AOn4CLDdL3brjOzbABRuyz-yolawtGRsbw",
@ -781,7 +834,7 @@ Channel(
id: "UC2DjFE7Xf11URZqWBigcVOQ",
name: "EEVblog",
avatar: [],
verification: verified,
verification: Verified,
subscriber_count: Some(883000),
)),
publish_date: "[date]",
@ -795,7 +848,7 @@ Channel(
VideoItem(
id: "rxGafdgkal8",
name: "EEVblog 1492 - $5 Oscilloscope Repaired! + Oz GIVEAWAY",
duration: Some(1163),
length: Some(1163),
thumbnail: [
Thumbnail(
url: "https://i.ytimg.com/vi/rxGafdgkal8/hqdefault.jpg?sqp=-oaymwEbCKgBEF5IVfKriqkDDggBFQAAiEIYAXABwAEG&rs=AOn4CLD-p_t0q_Q2oTGyJuFCQJ5z6VPPMQ",
@ -822,7 +875,7 @@ Channel(
id: "UC2DjFE7Xf11URZqWBigcVOQ",
name: "EEVblog",
avatar: [],
verification: verified,
verification: Verified,
subscriber_count: Some(883000),
)),
publish_date: "[date]",
@ -836,7 +889,7 @@ Channel(
VideoItem(
id: "4yosozyeIP4",
name: "EEVblog 1491 - The MacGyver Project - Part 1",
duration: Some(1706),
length: Some(1706),
thumbnail: [
Thumbnail(
url: "https://i.ytimg.com/vi/4yosozyeIP4/hqdefault.jpg?sqp=-oaymwEbCKgBEF5IVfKriqkDDggBFQAAiEIYAXABwAEG&rs=AOn4CLDRNAWkPQfuQirfiOdowD1iQlWrWg",
@ -863,7 +916,7 @@ Channel(
id: "UC2DjFE7Xf11URZqWBigcVOQ",
name: "EEVblog",
avatar: [],
verification: verified,
verification: Verified,
subscriber_count: Some(883000),
)),
publish_date: "[date]",
@ -877,7 +930,7 @@ Channel(
VideoItem(
id: "06JtC2DC_dQ",
name: "EEVblog 1490 - Insane Jaycar Dumpster Sale! 2022",
duration: Some(1700),
length: Some(1700),
thumbnail: [
Thumbnail(
url: "https://i.ytimg.com/vi/06JtC2DC_dQ/hqdefault.jpg?sqp=-oaymwEbCKgBEF5IVfKriqkDDggBFQAAiEIYAXABwAEG&rs=AOn4CLDVIvEssIKji_8dyBYGYbpIqen7vQ",
@ -904,7 +957,7 @@ Channel(
id: "UC2DjFE7Xf11URZqWBigcVOQ",
name: "EEVblog",
avatar: [],
verification: verified,
verification: Verified,
subscriber_count: Some(883000),
)),
publish_date: "[date]",
@ -918,7 +971,7 @@ Channel(
VideoItem(
id: "piquT76w9TI",
name: "EEVblog 1489 - Mystery Teardown!",
duration: Some(1466),
length: Some(1466),
thumbnail: [
Thumbnail(
url: "https://i.ytimg.com/vi/piquT76w9TI/hqdefault.jpg?sqp=-oaymwEbCKgBEF5IVfKriqkDDggBFQAAiEIYAXABwAEG&rs=AOn4CLCTzIcGeRDwUyINtik50EQCOTxwiA",
@ -945,7 +998,7 @@ Channel(
id: "UC2DjFE7Xf11URZqWBigcVOQ",
name: "EEVblog",
avatar: [],
verification: verified,
verification: Verified,
subscriber_count: Some(883000),
)),
publish_date: "[date]",
@ -959,7 +1012,7 @@ Channel(
VideoItem(
id: "pKuUKT-zU-g",
name: "EEVblog 1488 - Tilt Five Augmented Reality AR Glasses - First Reaction!",
duration: Some(2152),
length: Some(2152),
thumbnail: [
Thumbnail(
url: "https://i.ytimg.com/vi/pKuUKT-zU-g/hqdefault.jpg?sqp=-oaymwEbCKgBEF5IVfKriqkDDggBFQAAiEIYAXABwAEG&rs=AOn4CLCB6Rh4miI20yPy2kJaxul_wA3Now",
@ -986,7 +1039,7 @@ Channel(
id: "UC2DjFE7Xf11URZqWBigcVOQ",
name: "EEVblog",
avatar: [],
verification: verified,
verification: Verified,
subscriber_count: Some(883000),
)),
publish_date: "[date]",
@ -1000,7 +1053,7 @@ Channel(
VideoItem(
id: "_R4wQQNSO6k",
name: "EEVblog 1487 - Do Solar Micro Inverters Take Power at Night?",
duration: Some(2399),
length: Some(2399),
thumbnail: [
Thumbnail(
url: "https://i.ytimg.com/vi/_R4wQQNSO6k/hqdefault.jpg?sqp=-oaymwEbCKgBEF5IVfKriqkDDggBFQAAiEIYAXABwAEG&rs=AOn4CLDEQVZ0yQPLZqwLdQednKWwLWqDmA",
@ -1027,7 +1080,7 @@ Channel(
id: "UC2DjFE7Xf11URZqWBigcVOQ",
name: "EEVblog",
avatar: [],
verification: verified,
verification: Verified,
subscriber_count: Some(883000),
)),
publish_date: "[date]",
@ -1041,7 +1094,7 @@ Channel(
VideoItem(
id: "ikp5BorIo_M",
name: "EEVblog 1486 - What you DIDN\'T KNOW About Film Capacitor FAILURES!",
duration: Some(1792),
length: Some(1792),
thumbnail: [
Thumbnail(
url: "https://i.ytimg.com/vi/ikp5BorIo_M/hqdefault.jpg?sqp=-oaymwEbCKgBEF5IVfKriqkDDggBFQAAiEIYAXABwAEG&rs=AOn4CLBygGB8KC522NC15BhDC1WpuNKsgw",
@ -1068,7 +1121,7 @@ Channel(
id: "UC2DjFE7Xf11URZqWBigcVOQ",
name: "EEVblog",
avatar: [],
verification: verified,
verification: Verified,
subscriber_count: Some(883000),
)),
publish_date: "[date]",
@ -1082,7 +1135,7 @@ Channel(
VideoItem(
id: "7O-QckjCXNo",
name: "eevBLAB 99 - AI SPAM BOT Youtube Space/Science/Tech Channels? - WTF",
duration: Some(592),
length: Some(592),
thumbnail: [
Thumbnail(
url: "https://i.ytimg.com/vi/7O-QckjCXNo/hqdefault.jpg?sqp=-oaymwEbCKgBEF5IVfKriqkDDggBFQAAiEIYAXABwAEG&rs=AOn4CLBY1cRnrWQCbmlAzP5okMmIYjgdsg",
@ -1109,7 +1162,7 @@ Channel(
id: "UC2DjFE7Xf11URZqWBigcVOQ",
name: "EEVblog",
avatar: [],
verification: verified,
verification: Verified,
subscriber_count: Some(883000),
)),
publish_date: "[date]",
@ -1123,7 +1176,7 @@ Channel(
VideoItem(
id: "VutdTxF4E-0",
name: "RIP The Old Garage Lab",
duration: Some(115),
length: Some(115),
thumbnail: [
Thumbnail(
url: "https://i.ytimg.com/vi/VutdTxF4E-0/hqdefault.jpg?sqp=-oaymwEbCKgBEF5IVfKriqkDDggBFQAAiEIYAXABwAEG&rs=AOn4CLDlPpT2-UOGfm2A2djTLjCsygeqSw",
@ -1150,7 +1203,7 @@ Channel(
id: "UC2DjFE7Xf11URZqWBigcVOQ",
name: "EEVblog",
avatar: [],
verification: verified,
verification: Verified,
subscriber_count: Some(883000),
)),
publish_date: "[date]",
@ -1164,7 +1217,7 @@ Channel(
VideoItem(
id: "o7xfGuRaq94",
name: "EEVblog 1485 - PedalCell CadenceX Bike Generator LOL FAIL!",
duration: Some(1026),
length: Some(1026),
thumbnail: [
Thumbnail(
url: "https://i.ytimg.com/vi/o7xfGuRaq94/hqdefault.jpg?sqp=-oaymwEbCKgBEF5IVfKriqkDDggBFQAAiEIYAXABwAEG&rs=AOn4CLBA7RRL2USBwkYXp9ouWTbtU-JHSg",
@ -1191,7 +1244,7 @@ Channel(
id: "UC2DjFE7Xf11URZqWBigcVOQ",
name: "EEVblog",
avatar: [],
verification: verified,
verification: Verified,
subscriber_count: Some(883000),
)),
publish_date: "[date]",
@ -1205,7 +1258,7 @@ Channel(
VideoItem(
id: "3WSIfHOv3fc",
name: "EEVblog 1484 - Kaba Mas X-09 High Security Electronic Lock Teardown",
duration: Some(1106),
length: Some(1106),
thumbnail: [
Thumbnail(
url: "https://i.ytimg.com/vi/3WSIfHOv3fc/hqdefault.jpg?sqp=-oaymwEbCKgBEF5IVfKriqkDDggBFQAAiEIYAXABwAEG&rs=AOn4CLClZroFRo115ZuxYhJ5rcCDO2ZPcQ",
@ -1232,7 +1285,7 @@ Channel(
id: "UC2DjFE7Xf11URZqWBigcVOQ",
name: "EEVblog",
avatar: [],
verification: verified,
verification: Verified,
subscriber_count: Some(883000),
)),
publish_date: "[date]",
@ -1246,7 +1299,7 @@ Channel(
VideoItem(
id: "8yXZJZCKImI",
name: "EEVblog 1483 - Holy Mailbag Bomb Batman!",
duration: Some(3373),
length: Some(3373),
thumbnail: [
Thumbnail(
url: "https://i.ytimg.com/vi/8yXZJZCKImI/hqdefault.jpg?sqp=-oaymwEbCKgBEF5IVfKriqkDDggBFQAAiEIYAXABwAEG&rs=AOn4CLBym7WfmrvKIjs2ClW-FOLtxbENzw",
@ -1273,7 +1326,7 @@ Channel(
id: "UC2DjFE7Xf11URZqWBigcVOQ",
name: "EEVblog",
avatar: [],
verification: verified,
verification: Verified,
subscriber_count: Some(883000),
)),
publish_date: "[date]",
@ -1287,7 +1340,7 @@ Channel(
VideoItem(
id: "vJ4pW6LKJWU",
name: "EEVblog 1482 - Mains Capacitor Zener Regulator Circuit",
duration: Some(1132),
length: Some(1132),
thumbnail: [
Thumbnail(
url: "https://i.ytimg.com/vi/vJ4pW6LKJWU/hqdefault.jpg?sqp=-oaymwEbCKgBEF5IVfKriqkDDggBFQAAiEIYAXABwAEG&rs=AOn4CLDaKgfAJ4NAeqoMIPZDavsTw_JD5w",
@ -1314,7 +1367,7 @@ Channel(
id: "UC2DjFE7Xf11URZqWBigcVOQ",
name: "EEVblog",
avatar: [],
verification: verified,
verification: Verified,
subscriber_count: Some(883000),
)),
publish_date: "[date]",

View file

@ -5,9 +5,7 @@ expression: map_res.c
Channel(
id: "UCHF66aWLOxBW4l6VkSrS3cQ",
name: "Coachella",
handle: Some("@Coachella"),
subscriber_count: Some(2710000),
video_count: None,
avatar: [
Thumbnail(
url: "//yt3.googleusercontent.com/RDZ6VWFjHEMFm_QcmCCf-yG_UiGo9YWXEmVRuiHSC8SvP02dgeBEtAjd4CnEKGLo0V2gGdIRDQ=s88-c-k-c0x00ffffff-no-rj-mo",
@ -20,7 +18,7 @@ Channel(
height: 176,
),
],
verification: verified,
verification: Verified,
description: "April 14-16 & 21-23, 2023\n",
tags: [
"coachella",
@ -33,7 +31,10 @@ Channel(
"indio",
"california",
],
vanity_url: Some("https://www.youtube.com/@Coachella"),
banner: [],
mobile_banner: [],
tv_banner: [],
has_shorts: true,
has_live: true,
visitor_data: Some("CgtjSUhDeVJ6SU5wNCj75uyhBg%3D%3D"),
@ -43,7 +44,7 @@ Channel(
VideoItem(
id: "vFc_pAywtKc",
name: "The Murder Capital - Return My Head - Live at Coachella 2023",
duration: Some(194),
length: Some(194),
thumbnail: [
Thumbnail(
url: "https://i.ytimg.com/vi/vFc_pAywtKc/hqdefault.jpg?sqp=-oaymwEbCKgBEF5IVfKriqkDDggBFQAAiEIYAXABwAEG&rs=AOn4CLAPDC5UHtj76ursSNJqBD-jAiSxHg",
@ -70,7 +71,7 @@ Channel(
id: "UCHF66aWLOxBW4l6VkSrS3cQ",
name: "Coachella",
avatar: [],
verification: verified,
verification: Verified,
subscriber_count: Some(2710000),
)),
publish_date: "[date]",
@ -84,7 +85,7 @@ Channel(
VideoItem(
id: "3JprxZgfcHU",
name: "BENEE - Supaloney - ft. Gus Dapperton - Live at Coachella 2023",
duration: Some(270),
length: Some(270),
thumbnail: [
Thumbnail(
url: "https://i.ytimg.com/vi/3JprxZgfcHU/hqdefault.jpg?sqp=-oaymwEbCKgBEF5IVfKriqkDDggBFQAAiEIYAXABwAEG&rs=AOn4CLCs4cytf-M3ksr1YZB0Iu22b3_Baw",
@ -111,7 +112,7 @@ Channel(
id: "UCHF66aWLOxBW4l6VkSrS3cQ",
name: "Coachella",
avatar: [],
verification: verified,
verification: Verified,
subscriber_count: Some(2710000),
)),
publish_date: "[date]",
@ -125,7 +126,7 @@ Channel(
VideoItem(
id: "a4QufICobaA",
name: "Doechii - What It Is - Live at Coachella 2023",
duration: Some(185),
length: Some(185),
thumbnail: [
Thumbnail(
url: "https://i.ytimg.com/vi/a4QufICobaA/hqdefault.jpg?sqp=-oaymwEbCKgBEF5IVfKriqkDDggBFQAAiEIYAXABwAEG&rs=AOn4CLC1bg4wXk4z0Tcp-PgPodKlRsf8lA",
@ -152,7 +153,7 @@ Channel(
id: "UCHF66aWLOxBW4l6VkSrS3cQ",
name: "Coachella",
avatar: [],
verification: verified,
verification: Verified,
subscriber_count: Some(2710000),
)),
publish_date: "[date]",
@ -166,7 +167,7 @@ Channel(
VideoItem(
id: "QoRm-xhVqYU",
name: "Gabriels - Blame - Live at Coachella 2023",
duration: Some(170),
length: Some(170),
thumbnail: [
Thumbnail(
url: "https://i.ytimg.com/vi/QoRm-xhVqYU/hqdefault.jpg?sqp=-oaymwEbCKgBEF5IVfKriqkDDggBFQAAiEIYAXABwAEG&rs=AOn4CLD9H8nfnmu-G2jIfTelbBNbiAWvqw",
@ -193,7 +194,7 @@ Channel(
id: "UCHF66aWLOxBW4l6VkSrS3cQ",
name: "Coachella",
avatar: [],
verification: verified,
verification: Verified,
subscriber_count: Some(2710000),
)),
publish_date: "[date]",
@ -207,7 +208,7 @@ Channel(
VideoItem(
id: "28DbQYSsn1w",
name: "Kaytranada - Intimidate - ft H.E.R. - Live at Coachella 2023",
duration: Some(252),
length: Some(252),
thumbnail: [
Thumbnail(
url: "https://i.ytimg.com/vi/28DbQYSsn1w/hqdefault.jpg?sqp=-oaymwEbCKgBEF5IVfKriqkDDggBFQAAiEIYAXABwAEG&rs=AOn4CLAF_nO2I3hjct93i3p6V3H1Rmadcg",
@ -234,7 +235,7 @@ Channel(
id: "UCHF66aWLOxBW4l6VkSrS3cQ",
name: "Coachella",
avatar: [],
verification: verified,
verification: Verified,
subscriber_count: Some(2710000),
)),
publish_date: "[date]",
@ -248,7 +249,7 @@ Channel(
VideoItem(
id: "nLFZFp3go3o",
name: "SG Lewis - Impact - ft. Channel Tres - Live at Coachella 2023",
duration: Some(365),
length: Some(365),
thumbnail: [
Thumbnail(
url: "https://i.ytimg.com/vi/nLFZFp3go3o/hqdefault.jpg?sqp=-oaymwEbCKgBEF5IVfKriqkDDggBFQAAiEIYAXABwAEG&rs=AOn4CLBgQvHlztxcmQ3pkFNMKQpgvgMusA",
@ -275,7 +276,7 @@ Channel(
id: "UCHF66aWLOxBW4l6VkSrS3cQ",
name: "Coachella",
avatar: [],
verification: verified,
verification: Verified,
subscriber_count: Some(2710000),
)),
publish_date: "[date]",
@ -289,7 +290,7 @@ Channel(
VideoItem(
id: "RWJMmYcPTR4",
name: "MUNA - Silk Chiffon - Live at Coachella 2023",
duration: Some(220),
length: Some(220),
thumbnail: [
Thumbnail(
url: "https://i.ytimg.com/vi/RWJMmYcPTR4/hqdefault.jpg?sqp=-oaymwEbCKgBEF5IVfKriqkDDggBFQAAiEIYAXABwAEG&rs=AOn4CLBFk7y7WiMC9pZ9zE1YSlh0TA5o5Q",
@ -316,7 +317,7 @@ Channel(
id: "UCHF66aWLOxBW4l6VkSrS3cQ",
name: "Coachella",
avatar: [],
verification: verified,
verification: Verified,
subscriber_count: Some(2710000),
)),
publish_date: "[date]",
@ -330,7 +331,7 @@ Channel(
VideoItem(
id: "gcrW53SoTKs",
name: "Pusha T - Diet Coke - Live at Coachella 2023",
duration: Some(175),
length: Some(175),
thumbnail: [
Thumbnail(
url: "https://i.ytimg.com/vi/gcrW53SoTKs/hqdefault.jpg?sqp=-oaymwEbCKgBEF5IVfKriqkDDggBFQAAiEIYAXABwAEG&rs=AOn4CLAlGMLr4aBbSqb-8HBAPeXGLtkOGg",
@ -357,7 +358,7 @@ Channel(
id: "UCHF66aWLOxBW4l6VkSrS3cQ",
name: "Coachella",
avatar: [],
verification: verified,
verification: Verified,
subscriber_count: Some(2710000),
)),
publish_date: "[date]",
@ -371,7 +372,7 @@ Channel(
VideoItem(
id: "7pYqbVztRtk",
name: "Blink 182 - I Miss You - Live at Coachella 2023",
duration: Some(267),
length: Some(267),
thumbnail: [
Thumbnail(
url: "https://i.ytimg.com/vi/7pYqbVztRtk/hqdefault.jpg?sqp=-oaymwEbCKgBEF5IVfKriqkDDggBFQAAiEIYAXABwAEG&rs=AOn4CLD4rIf8atgIc2nEptj4CjgOPqXVWw",
@ -398,7 +399,7 @@ Channel(
id: "UCHF66aWLOxBW4l6VkSrS3cQ",
name: "Coachella",
avatar: [],
verification: verified,
verification: Verified,
subscriber_count: Some(2710000),
)),
publish_date: "[date]",
@ -412,7 +413,7 @@ Channel(
VideoItem(
id: "yzmSlPiaeRU",
name: "Blink 182 - Whats My Age Again - Live at Coachella 2023",
duration: Some(157),
length: Some(157),
thumbnail: [
Thumbnail(
url: "https://i.ytimg.com/vi/yzmSlPiaeRU/hqdefault.jpg?sqp=-oaymwEbCKgBEF5IVfKriqkDDggBFQAAiEIYAXABwAEG&rs=AOn4CLAzhu2omZ6arr3cGIEM1IGoIp_i3w",
@ -439,7 +440,7 @@ Channel(
id: "UCHF66aWLOxBW4l6VkSrS3cQ",
name: "Coachella",
avatar: [],
verification: verified,
verification: Verified,
subscriber_count: Some(2710000),
)),
publish_date: "[date]",
@ -453,7 +454,7 @@ Channel(
VideoItem(
id: "r3Kpm4lEXmg",
name: "Discover the Mirage, Part 2 - Coachella 2023",
duration: Some(96),
length: Some(96),
thumbnail: [
Thumbnail(
url: "https://i.ytimg.com/vi/r3Kpm4lEXmg/hqdefault.jpg?sqp=-oaymwEbCKgBEF5IVfKriqkDDggBFQAAiEIYAXABwAEG&rs=AOn4CLBLfc6awfa8Mv7I1nTLxfJRY4XUKQ",
@ -480,7 +481,7 @@ Channel(
id: "UCHF66aWLOxBW4l6VkSrS3cQ",
name: "Coachella",
avatar: [],
verification: verified,
verification: Verified,
subscriber_count: Some(2710000),
)),
publish_date: "[date]",
@ -494,7 +495,7 @@ Channel(
VideoItem(
id: "LqrLCWoXR_k",
name: "Coachella on YouTube 2023",
duration: Some(31),
length: Some(31),
thumbnail: [
Thumbnail(
url: "https://i.ytimg.com/vi/LqrLCWoXR_k/hqdefault.jpg?sqp=-oaymwEbCKgBEF5IVfKriqkDDggBFQAAiEIYAXABwAEG&rs=AOn4CLCgX8ylcJLaYZiR3Nvr5WrS_6mw8g",
@ -521,7 +522,7 @@ Channel(
id: "UCHF66aWLOxBW4l6VkSrS3cQ",
name: "Coachella",
avatar: [],
verification: verified,
verification: Verified,
subscriber_count: Some(2710000),
)),
publish_date: "[date]",
@ -535,7 +536,7 @@ Channel(
VideoItem(
id: "J1cmYPtABo0",
name: "Discover the Mirage, Part 1 - Coachella 2023",
duration: Some(91),
length: Some(91),
thumbnail: [
Thumbnail(
url: "https://i.ytimg.com/vi/J1cmYPtABo0/hqdefault.jpg?sqp=-oaymwEbCKgBEF5IVfKriqkDDggBFQAAiEIYAXABwAEG&rs=AOn4CLAEDuvdZhNVkmvG-usGm9tmgJt7QQ",
@ -562,7 +563,7 @@ Channel(
id: "UCHF66aWLOxBW4l6VkSrS3cQ",
name: "Coachella",
avatar: [],
verification: verified,
verification: Verified,
subscriber_count: Some(2710000),
)),
publish_date: "[date]",
@ -576,7 +577,7 @@ Channel(
VideoItem(
id: "a0BuUhI3f20",
name: "Coachella 2023 featuring Bad Bunny, BLACKPINK, Frank Ocean and more 🌵",
duration: Some(31),
length: Some(31),
thumbnail: [
Thumbnail(
url: "https://i.ytimg.com/vi/a0BuUhI3f20/hqdefault.jpg?sqp=-oaymwE1CKgBEF5IVfKriqkDKAgBFQAAiEIYAXABwAEG8AEB-AH-CYAC0AWKAgwIABABGGUgUCg-MA8=&rs=AOn4CLBDwxWN_SrIR8rCSQVokx1wfe1iqQ",
@ -603,7 +604,7 @@ Channel(
id: "UCHF66aWLOxBW4l6VkSrS3cQ",
name: "Coachella",
avatar: [],
verification: verified,
verification: Verified,
subscriber_count: Some(2710000),
)),
publish_date: "[date]",
@ -617,7 +618,7 @@ Channel(
VideoItem(
id: "G2p-YqRGh80",
name: "MEUTE Interview Coachella Curated 2022",
duration: Some(224),
length: Some(224),
thumbnail: [
Thumbnail(
url: "https://i.ytimg.com/vi/G2p-YqRGh80/hqdefault.jpg?sqp=-oaymwE1CKgBEF5IVfKriqkDKAgBFQAAiEIYAXABwAEG8AEB-AH-CYAC0AWKAgwIABABGGUgSSg-MA8=&rs=AOn4CLBWAFod2tomSeOXcy3y5EOIjimn9g",
@ -644,7 +645,7 @@ Channel(
id: "UCHF66aWLOxBW4l6VkSrS3cQ",
name: "Coachella",
avatar: [],
verification: verified,
verification: Verified,
subscriber_count: Some(2710000),
)),
publish_date: "[date]",
@ -658,7 +659,7 @@ Channel(
VideoItem(
id: "eLZq4l37G7k",
name: "Belly - Interview - Coachella 2022",
duration: Some(302),
length: Some(302),
thumbnail: [
Thumbnail(
url: "https://i.ytimg.com/vi/eLZq4l37G7k/hqdefault.jpg?sqp=-oaymwE1CKgBEF5IVfKriqkDKAgBFQAAiEIYAXABwAEG8AEB-AH-CYAC0AWKAgwIABABGHIgPyg6MA8=&rs=AOn4CLBtjJQRABeVsxDVsYK2RwoTETjE8A",
@ -685,7 +686,7 @@ Channel(
id: "UCHF66aWLOxBW4l6VkSrS3cQ",
name: "Coachella",
avatar: [],
verification: verified,
verification: Verified,
subscriber_count: Some(2710000),
)),
publish_date: "[date]",
@ -699,7 +700,7 @@ Channel(
VideoItem(
id: "ViPAf8JpMXY",
name: "Still Woozy - Interview - Coachella 2022",
duration: Some(304),
length: Some(304),
thumbnail: [
Thumbnail(
url: "https://i.ytimg.com/vi/ViPAf8JpMXY/hqdefault.jpg?sqp=-oaymwE1CKgBEF5IVfKriqkDKAgBFQAAiEIYAXABwAEG8AEB-AH-CYAC0AWKAgwIABABGGUgUyhEMA8=&rs=AOn4CLBFMadm51TmtXHYl-3B3s1DS1NLoQ",
@ -726,7 +727,7 @@ Channel(
id: "UCHF66aWLOxBW4l6VkSrS3cQ",
name: "Coachella",
avatar: [],
verification: verified,
verification: Verified,
subscriber_count: Some(2710000),
)),
publish_date: "[date]",
@ -740,7 +741,7 @@ Channel(
VideoItem(
id: "4PKCIRUOZRE",
name: "Slander - Interview - Coachella 2022",
duration: Some(259),
length: Some(259),
thumbnail: [
Thumbnail(
url: "https://i.ytimg.com/vi/4PKCIRUOZRE/hqdefault.jpg?sqp=-oaymwE1CKgBEF5IVfKriqkDKAgBFQAAiEIYAXABwAEG8AEB-AH-CYAC0AWKAgwIABABGGUgUChCMA8=&rs=AOn4CLD6iAmhCyMAwfcKJl18WeC_BrjyFQ",
@ -767,7 +768,7 @@ Channel(
id: "UCHF66aWLOxBW4l6VkSrS3cQ",
name: "Coachella",
avatar: [],
verification: verified,
verification: Verified,
subscriber_count: Some(2710000),
)),
publish_date: "[date]",
@ -781,7 +782,7 @@ Channel(
VideoItem(
id: "0O7abvoOxro",
name: "Run The Jewels - Interview Coachella",
duration: Some(408),
length: Some(408),
thumbnail: [
Thumbnail(
url: "https://i.ytimg.com/vi/0O7abvoOxro/hqdefault.jpg?sqp=-oaymwE1CKgBEF5IVfKriqkDKAgBFQAAiEIYAXABwAEG8AEB-AH-CYAC0AWKAgwIABABGGUgXihNMA8=&rs=AOn4CLCYxlSf_-9OXuvGCVfY8caFGVaGeQ",
@ -808,7 +809,7 @@ Channel(
id: "UCHF66aWLOxBW4l6VkSrS3cQ",
name: "Coachella",
avatar: [],
verification: verified,
verification: Verified,
subscriber_count: Some(2710000),
)),
publish_date: "[date]",
@ -822,7 +823,7 @@ Channel(
VideoItem(
id: "z1Q7ahNLU9o",
name: "Rina Sawayama - Interview - Coachella 2022",
duration: Some(297),
length: Some(297),
thumbnail: [
Thumbnail(
url: "https://i.ytimg.com/vi/z1Q7ahNLU9o/hqdefault.jpg?sqp=-oaymwE1CKgBEF5IVfKriqkDKAgBFQAAiEIYAXABwAEG8AEB-AH-CYAC0AWKAgwIABABGGUgWCg_MA8=&rs=AOn4CLAFBsN92p3Xd5jd75JOkVQmFpRaOQ",
@ -849,7 +850,7 @@ Channel(
id: "UCHF66aWLOxBW4l6VkSrS3cQ",
name: "Coachella",
avatar: [],
verification: verified,
verification: Verified,
subscriber_count: Some(2710000),
)),
publish_date: "[date]",
@ -863,7 +864,7 @@ Channel(
VideoItem(
id: "VB71WJvcdsM",
name: "Rich Brian - Interview - Coachella 2022",
duration: Some(371),
length: Some(371),
thumbnail: [
Thumbnail(
url: "https://i.ytimg.com/vi/VB71WJvcdsM/hqdefault.jpg?sqp=-oaymwE1CKgBEF5IVfKriqkDKAgBFQAAiEIYAXABwAEG8AEB-AH-CYAC0AWKAgwIABABGGUgVyhAMA8=&rs=AOn4CLAnNIA4THR0-WH60GnpECd_KRhUEQ",
@ -890,7 +891,7 @@ Channel(
id: "UCHF66aWLOxBW4l6VkSrS3cQ",
name: "Coachella",
avatar: [],
verification: verified,
verification: Verified,
subscriber_count: Some(2710000),
)),
publish_date: "[date]",
@ -904,7 +905,7 @@ Channel(
VideoItem(
id: "FYr3OasngBI",
name: "Masego - Interview - Coachella 2022",
duration: Some(323),
length: Some(323),
thumbnail: [
Thumbnail(
url: "https://i.ytimg.com/vi/FYr3OasngBI/hqdefault.jpg?sqp=-oaymwE1CKgBEF5IVfKriqkDKAgBFQAAiEIYAXABwAEG8AEB-AH-CYAC0AWKAgwIABABGHIgXyg6MA8=&rs=AOn4CLAAT-2gUtrDLaKVDQmsUkKmkE__Lg",
@ -931,7 +932,7 @@ Channel(
id: "UCHF66aWLOxBW4l6VkSrS3cQ",
name: "Coachella",
avatar: [],
verification: verified,
verification: Verified,
subscriber_count: Some(2710000),
)),
publish_date: "[date]",
@ -945,7 +946,7 @@ Channel(
VideoItem(
id: "BwDnV5sbFeU",
name: "Louis The Child - Interview - Coachella 2022",
duration: Some(360),
length: Some(360),
thumbnail: [
Thumbnail(
url: "https://i.ytimg.com/vi/BwDnV5sbFeU/hqdefault.jpg?sqp=-oaymwE1CKgBEF5IVfKriqkDKAgBFQAAiEIYAXABwAEG8AEB-AH-CYAC0AWKAgwIABABGGUgWig2MA8=&rs=AOn4CLAXRG17JkByDUun5WIfMdVARqYwtg",
@ -972,7 +973,7 @@ Channel(
id: "UCHF66aWLOxBW4l6VkSrS3cQ",
name: "Coachella",
avatar: [],
verification: verified,
verification: Verified,
subscriber_count: Some(2710000),
)),
publish_date: "[date]",
@ -986,7 +987,7 @@ Channel(
VideoItem(
id: "iH8KFwkMurQ",
name: "Kim Petras - Interview - Coachella 2022",
duration: Some(294),
length: Some(294),
thumbnail: [
Thumbnail(
url: "https://i.ytimg.com/vi/iH8KFwkMurQ/hqdefault.jpg?sqp=-oaymwE1CKgBEF5IVfKriqkDKAgBFQAAiEIYAXABwAEG8AEB-AH-CYAC0AWKAgwIABABGGUgVChIMA8=&rs=AOn4CLBzJcKvWEWZmdtorJ8P7tfMT1306A",
@ -1013,7 +1014,7 @@ Channel(
id: "UCHF66aWLOxBW4l6VkSrS3cQ",
name: "Coachella",
avatar: [],
verification: verified,
verification: Verified,
subscriber_count: Some(2710000),
)),
publish_date: "[date]",
@ -1027,7 +1028,7 @@ Channel(
VideoItem(
id: "NK96m-YTUaE",
name: "Joe Kay - Interview - Coachella 2022",
duration: Some(189),
length: Some(189),
thumbnail: [
Thumbnail(
url: "https://i.ytimg.com/vi/NK96m-YTUaE/hqdefault.jpg?sqp=-oaymwE1CKgBEF5IVfKriqkDKAgBFQAAiEIYAXABwAEG8AEB-AH-CYAC0AWKAgwIABABGGUgWyhJMA8=&rs=AOn4CLD6ptJ2_2cwyY2pkGieoYscFjlVpQ",
@ -1054,7 +1055,7 @@ Channel(
id: "UCHF66aWLOxBW4l6VkSrS3cQ",
name: "Coachella",
avatar: [],
verification: verified,
verification: Verified,
subscriber_count: Some(2710000),
)),
publish_date: "[date]",
@ -1068,7 +1069,7 @@ Channel(
VideoItem(
id: "jnG1qLK0SiI",
name: "Japanese Breakfast - Interview - Coachella 2022",
duration: Some(312),
length: Some(312),
thumbnail: [
Thumbnail(
url: "https://i.ytimg.com/vi/jnG1qLK0SiI/hqdefault.jpg?sqp=-oaymwE1CKgBEF5IVfKriqkDKAgBFQAAiEIYAXABwAEG8AEB-AH-CYAC0AWKAgwIABABGHIgWCguMA8=&rs=AOn4CLBTEvxp-kJ7uYZwIaiylaohW_7wGQ",
@ -1095,7 +1096,7 @@ Channel(
id: "UCHF66aWLOxBW4l6VkSrS3cQ",
name: "Coachella",
avatar: [],
verification: verified,
verification: Verified,
subscriber_count: Some(2710000),
)),
publish_date: "[date]",
@ -1109,7 +1110,7 @@ Channel(
VideoItem(
id: "NdKnb1e9_qA",
name: "Idles - Interview - Coachella 2022",
duration: Some(395),
length: Some(395),
thumbnail: [
Thumbnail(
url: "https://i.ytimg.com/vi/NdKnb1e9_qA/hqdefault.jpg?sqp=-oaymwE1CKgBEF5IVfKriqkDKAgBFQAAiEIYAXABwAEG8AEB-AH-CYAC0AWKAgwIABABGGUgWihTMA8=&rs=AOn4CLBEHQoRUkshAo-28mmB520wlFwlxA",
@ -1136,7 +1137,7 @@ Channel(
id: "UCHF66aWLOxBW4l6VkSrS3cQ",
name: "Coachella",
avatar: [],
verification: verified,
verification: Verified,
subscriber_count: Some(2710000),
)),
publish_date: "[date]",
@ -1150,7 +1151,7 @@ Channel(
VideoItem(
id: "o8LEidp-Dq8",
name: "Freddie Gibbs - Interview - Coachella 2022",
duration: Some(207),
length: Some(207),
thumbnail: [
Thumbnail(
url: "https://i.ytimg.com/vi/o8LEidp-Dq8/hqdefault.jpg?sqp=-oaymwE1CKgBEF5IVfKriqkDKAgBFQAAiEIYAXABwAEG8AEB-AH-CYAC0AWKAgwIABABGHIgNyhKMA8=&rs=AOn4CLBqrWHD5sKYIrl_Fj6dTSixhqFAbw",
@ -1177,7 +1178,7 @@ Channel(
id: "UCHF66aWLOxBW4l6VkSrS3cQ",
name: "Coachella",
avatar: [],
verification: verified,
verification: Verified,
subscriber_count: Some(2710000),
)),
publish_date: "[date]",
@ -1191,7 +1192,7 @@ Channel(
VideoItem(
id: "4-sEy0jxh-U",
name: "Epik High - Interview - Coachella 2022",
duration: Some(386),
length: Some(386),
thumbnail: [
Thumbnail(
url: "https://i.ytimg.com/vi/4-sEy0jxh-U/hqdefault.jpg?sqp=-oaymwE1CKgBEF5IVfKriqkDKAgBFQAAiEIYAXABwAEG8AEB-AH-CYAC0AWKAgwIABABGGUgUCg9MA8=&rs=AOn4CLBqn7VHNUlbgYidF-k2x8b_W-_xWQ",
@ -1218,7 +1219,7 @@ Channel(
id: "UCHF66aWLOxBW4l6VkSrS3cQ",
name: "Coachella",
avatar: [],
verification: verified,
verification: Verified,
subscriber_count: Some(2710000),
)),
publish_date: "[date]",
@ -1232,7 +1233,7 @@ Channel(
VideoItem(
id: "YN5CjIFmx88",
name: "Duke Dumont - Interview - Coachella 2022",
duration: Some(443),
length: Some(443),
thumbnail: [
Thumbnail(
url: "https://i.ytimg.com/vi/YN5CjIFmx88/hqdefault.jpg?sqp=-oaymwE1CKgBEF5IVfKriqkDKAgBFQAAiEIYAXABwAEG8AEB-AH-CYAC0AWKAgwIABABGHIgUyhCMA8=&rs=AOn4CLAPYvywgTRHRSLHZaQXLC1-pdsIIg",
@ -1259,7 +1260,7 @@ Channel(
id: "UCHF66aWLOxBW4l6VkSrS3cQ",
name: "Coachella",
avatar: [],
verification: verified,
verification: Verified,
subscriber_count: Some(2710000),
)),
publish_date: "[date]",

View file

@ -5,9 +5,7 @@ expression: map_res.c
Channel(
id: "UC2DjFE7Xf11URZqWBigcVOQ",
name: "EEVblog",
handle: None,
subscriber_count: Some(880000),
video_count: None,
avatar: [
Thumbnail(
url: "https://yt3.ggpht.com/ytc/AMLnZu9eKk4Nd16fX4Rn1TF1G7ReluwOl6M5558FTYAM=s48-c-k-c0x00ffffff-no-rj",
@ -25,7 +23,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",
@ -57,6 +55,7 @@ Channel(
"dumpster diving",
"debunking",
],
vanity_url: Some("https://www.youtube.com/c/EevblogDave"),
banner: [
Thumbnail(
url: "https://yt3.ggpht.com/yIJ9ad80n49rK-YUcZLe_8bLmR-aGyg5ybDH_XKIc0GDWrC6s1Wzz8lxnq3_hux_5b6NHPZ9=w1060-fcrop64=1,00005a57ffffa5a8-k-c0xffffffff-no-nd-rj",
@ -89,6 +88,60 @@ Channel(
height: 424,
),
],
mobile_banner: [
Thumbnail(
url: "https://yt3.ggpht.com/yIJ9ad80n49rK-YUcZLe_8bLmR-aGyg5ybDH_XKIc0GDWrC6s1Wzz8lxnq3_hux_5b6NHPZ9=w320-fcrop64=1,32b75a57cd48a5a8-k-c0xffffffff-no-nd-rj",
width: 320,
height: 88,
),
Thumbnail(
url: "https://yt3.ggpht.com/yIJ9ad80n49rK-YUcZLe_8bLmR-aGyg5ybDH_XKIc0GDWrC6s1Wzz8lxnq3_hux_5b6NHPZ9=w640-fcrop64=1,32b75a57cd48a5a8-k-c0xffffffff-no-nd-rj",
width: 640,
height: 175,
),
Thumbnail(
url: "https://yt3.ggpht.com/yIJ9ad80n49rK-YUcZLe_8bLmR-aGyg5ybDH_XKIc0GDWrC6s1Wzz8lxnq3_hux_5b6NHPZ9=w960-fcrop64=1,32b75a57cd48a5a8-k-c0xffffffff-no-nd-rj",
width: 960,
height: 263,
),
Thumbnail(
url: "https://yt3.ggpht.com/yIJ9ad80n49rK-YUcZLe_8bLmR-aGyg5ybDH_XKIc0GDWrC6s1Wzz8lxnq3_hux_5b6NHPZ9=w1280-fcrop64=1,32b75a57cd48a5a8-k-c0xffffffff-no-nd-rj",
width: 1280,
height: 351,
),
Thumbnail(
url: "https://yt3.ggpht.com/yIJ9ad80n49rK-YUcZLe_8bLmR-aGyg5ybDH_XKIc0GDWrC6s1Wzz8lxnq3_hux_5b6NHPZ9=w1440-fcrop64=1,32b75a57cd48a5a8-k-c0xffffffff-no-nd-rj",
width: 1440,
height: 395,
),
],
tv_banner: [
Thumbnail(
url: "https://yt3.ggpht.com/yIJ9ad80n49rK-YUcZLe_8bLmR-aGyg5ybDH_XKIc0GDWrC6s1Wzz8lxnq3_hux_5b6NHPZ9=w320-fcrop64=1,00000000ffffffff-k-c0xffffffff-no-nd-rj",
width: 320,
height: 180,
),
Thumbnail(
url: "https://yt3.ggpht.com/yIJ9ad80n49rK-YUcZLe_8bLmR-aGyg5ybDH_XKIc0GDWrC6s1Wzz8lxnq3_hux_5b6NHPZ9=w854-fcrop64=1,00000000ffffffff-k-c0xffffffff-no-nd-rj",
width: 854,
height: 480,
),
Thumbnail(
url: "https://yt3.ggpht.com/yIJ9ad80n49rK-YUcZLe_8bLmR-aGyg5ybDH_XKIc0GDWrC6s1Wzz8lxnq3_hux_5b6NHPZ9=w1280-fcrop64=1,00000000ffffffff-k-c0xffffffff-no-nd-rj",
width: 1280,
height: 720,
),
Thumbnail(
url: "https://yt3.ggpht.com/yIJ9ad80n49rK-YUcZLe_8bLmR-aGyg5ybDH_XKIc0GDWrC6s1Wzz8lxnq3_hux_5b6NHPZ9=w1920-fcrop64=1,00000000ffffffff-k-c0xffffffff-no-nd-rj",
width: 1920,
height: 1080,
),
Thumbnail(
url: "https://yt3.ggpht.com/yIJ9ad80n49rK-YUcZLe_8bLmR-aGyg5ybDH_XKIc0GDWrC6s1Wzz8lxnq3_hux_5b6NHPZ9=w2120-fcrop64=1,00000000ffffffff-k-c0xffffffff-no-nd-rj",
width: 2120,
height: 1192,
),
],
has_shorts: false,
has_live: false,
visitor_data: Some("CgszNU5rbDVZS2hMcyim4K2ZBg%3D%3D"),
@ -98,7 +151,7 @@ Channel(
VideoItem(
id: "gremHHvqYTE",
name: "EEVblog 1501 - Rigol HDO4000 Low Noise 12bit Oscilloscope Unboxing & First Impression",
duration: Some(1794),
length: Some(1794),
thumbnail: [
Thumbnail(
url: "https://i.ytimg.com/vi/gremHHvqYTE/hqdefault.jpg?sqp=-oaymwEbCKgBEF5IVfKriqkDDggBFQAAiEIYAXABwAEG&rs=AOn4CLBcwR0YIwLjfFam9HkKdkTkqx_gHw",
@ -125,7 +178,7 @@ Channel(
id: "UC2DjFE7Xf11URZqWBigcVOQ",
name: "EEVblog",
avatar: [],
verification: verified,
verification: Verified,
subscriber_count: Some(880000),
)),
publish_date: "[date]",
@ -139,7 +192,7 @@ Channel(
VideoItem(
id: "WHO8NBfpaO0",
name: "eevBLAB 102 - Last Mile Autonomous Robot Deliveries WILL FAIL",
duration: Some(742),
length: Some(742),
thumbnail: [
Thumbnail(
url: "https://i.ytimg.com/vi/WHO8NBfpaO0/hqdefault.jpg?sqp=-oaymwEbCKgBEF5IVfKriqkDDggBFQAAiEIYAXABwAEG&rs=AOn4CLDQPKMF3Aeo9CydEWz9pQWkn1Lu7Q",
@ -166,7 +219,7 @@ Channel(
id: "UC2DjFE7Xf11URZqWBigcVOQ",
name: "EEVblog",
avatar: [],
verification: verified,
verification: Verified,
subscriber_count: Some(880000),
)),
publish_date: "[date]",
@ -180,7 +233,7 @@ Channel(
VideoItem(
id: "W1Q8CxL95_Y",
name: "EEVblog 1500 - Automatic Transfer Switch REVERSE ENGINEERED",
duration: Some(1770),
length: Some(1770),
thumbnail: [
Thumbnail(
url: "https://i.ytimg.com/vi/W1Q8CxL95_Y/hqdefault.jpg?sqp=-oaymwEbCKgBEF5IVfKriqkDDggBFQAAiEIYAXABwAEG&rs=AOn4CLBIxuct8vahJHOJTLfbOnsMOXnjvw",
@ -207,7 +260,7 @@ Channel(
id: "UC2DjFE7Xf11URZqWBigcVOQ",
name: "EEVblog",
avatar: [],
verification: verified,
verification: Verified,
subscriber_count: Some(880000),
)),
publish_date: "[date]",
@ -221,7 +274,7 @@ Channel(
VideoItem(
id: "lagxSrPeoYg",
name: "EEVblog 1499 - EcoFlow Delta Pro 3.6kWh Portable Battery TEARDOWN!",
duration: Some(2334),
length: Some(2334),
thumbnail: [
Thumbnail(
url: "https://i.ytimg.com/vi/lagxSrPeoYg/hqdefault.jpg?sqp=-oaymwEbCKgBEF5IVfKriqkDDggBFQAAiEIYAXABwAEG&rs=AOn4CLAbAX2gdAF66O7BUCaOVg2vQOsS2Q",
@ -248,7 +301,7 @@ Channel(
id: "UC2DjFE7Xf11URZqWBigcVOQ",
name: "EEVblog",
avatar: [],
verification: verified,
verification: Verified,
subscriber_count: Some(880000),
)),
publish_date: "[date]",
@ -262,7 +315,7 @@ Channel(
VideoItem(
id: "qTctWW9_FmE",
name: "EEVblog 1498 - TransPod Fluxjet Hyperloop $550M Boondoggle!",
duration: Some(2399),
length: Some(2399),
thumbnail: [
Thumbnail(
url: "https://i.ytimg.com/vi/qTctWW9_FmE/hqdefault.jpg?sqp=-oaymwEbCKgBEF5IVfKriqkDDggBFQAAiEIYAXABwAEG&rs=AOn4CLCbnEQaGGI5zD9lCJ8kMmciezX2kA",
@ -289,7 +342,7 @@ Channel(
id: "UC2DjFE7Xf11URZqWBigcVOQ",
name: "EEVblog",
avatar: [],
verification: verified,
verification: Verified,
subscriber_count: Some(880000),
)),
publish_date: "[date]",
@ -303,7 +356,7 @@ Channel(
VideoItem(
id: "3t9G80wk0pk",
name: "eevBLAB 101 - Why Are Tektronix Oscilloscopes So Expensive?",
duration: Some(1423),
length: Some(1423),
thumbnail: [
Thumbnail(
url: "https://i.ytimg.com/vi/3t9G80wk0pk/hqdefault.jpg?sqp=-oaymwEbCKgBEF5IVfKriqkDDggBFQAAiEIYAXABwAEG&rs=AOn4CLDnsVu-VQplpRpc1ZW-yk2byyZjZA",
@ -330,7 +383,7 @@ Channel(
id: "UC2DjFE7Xf11URZqWBigcVOQ",
name: "EEVblog",
avatar: [],
verification: verified,
verification: Verified,
subscriber_count: Some(880000),
)),
publish_date: "[date]",
@ -344,7 +397,7 @@ Channel(
VideoItem(
id: "7dze5CnZnmk",
name: "EEVblog 1497 - RIP Fluke. Thanks Energizer. NOT.",
duration: Some(1168),
length: Some(1168),
thumbnail: [
Thumbnail(
url: "https://i.ytimg.com/vi/7dze5CnZnmk/hqdefault.jpg?sqp=-oaymwEbCKgBEF5IVfKriqkDDggBFQAAiEIYAXABwAEG&rs=AOn4CLAg430MYAmoycM4lbv_57S_d3kZRA",
@ -371,7 +424,7 @@ Channel(
id: "UC2DjFE7Xf11URZqWBigcVOQ",
name: "EEVblog",
avatar: [],
verification: verified,
verification: Verified,
subscriber_count: Some(880000),
)),
publish_date: "[date]",
@ -385,7 +438,7 @@ Channel(
VideoItem(
id: "6XnrZpPYgBg",
name: "EEVblog 1496 - Winning Mailbag",
duration: Some(3139),
length: Some(3139),
thumbnail: [
Thumbnail(
url: "https://i.ytimg.com/vi/6XnrZpPYgBg/hqdefault.jpg?sqp=-oaymwEbCKgBEF5IVfKriqkDDggBFQAAiEIYAXABwAEG&rs=AOn4CLCrBgky13jB1p9xzKbmoUpJ4g0SNQ",
@ -412,7 +465,7 @@ Channel(
id: "UC2DjFE7Xf11URZqWBigcVOQ",
name: "EEVblog",
avatar: [],
verification: verified,
verification: Verified,
subscriber_count: Some(880000),
)),
publish_date: "[date]",
@ -426,7 +479,7 @@ Channel(
VideoItem(
id: "Psp3ltpFvws",
name: "eevBLAB 100 - Reuters Attacks Odysee - LOL",
duration: Some(855),
length: Some(855),
thumbnail: [
Thumbnail(
url: "https://i.ytimg.com/vi/Psp3ltpFvws/hqdefault.jpg?sqp=-oaymwEbCKgBEF5IVfKriqkDDggBFQAAiEIYAXABwAEG&rs=AOn4CLCu8Nu_NmDw5vBHgb7e8JdJR1Dr1Q",
@ -453,7 +506,7 @@ Channel(
id: "UC2DjFE7Xf11URZqWBigcVOQ",
name: "EEVblog",
avatar: [],
verification: verified,
verification: Verified,
subscriber_count: Some(880000),
)),
publish_date: "[date]",
@ -467,7 +520,7 @@ Channel(
VideoItem(
id: "taVYTYz5vLE",
name: "EEVblog 1495 - Quaze Wireless Power (AGAIN!) but for GAMING!",
duration: Some(2592),
length: Some(2592),
thumbnail: [
Thumbnail(
url: "https://i.ytimg.com/vi/taVYTYz5vLE/hqdefault.jpg?sqp=-oaymwEbCKgBEF5IVfKriqkDDggBFQAAiEIYAXABwAEG&rs=AOn4CLAMHngmN8TjWZz327vUD7zjjblYBw",
@ -494,7 +547,7 @@ Channel(
id: "UC2DjFE7Xf11URZqWBigcVOQ",
name: "EEVblog",
avatar: [],
verification: verified,
verification: Verified,
subscriber_count: Some(880000),
)),
publish_date: "[date]",
@ -508,7 +561,7 @@ Channel(
VideoItem(
id: "Y6cZrieFw-k",
name: "EEVblog 1494 - FIVE Ways to Open a CHEAP SAFE!",
duration: Some(1194),
length: Some(1194),
thumbnail: [
Thumbnail(
url: "https://i.ytimg.com/vi/Y6cZrieFw-k/hqdefault.jpg?sqp=-oaymwEbCKgBEF5IVfKriqkDDggBFQAAiEIYAXABwAEG&rs=AOn4CLDsdoJwcvSFZU4e9cwDFbZj3W21Pw",
@ -535,7 +588,7 @@ Channel(
id: "UC2DjFE7Xf11URZqWBigcVOQ",
name: "EEVblog",
avatar: [],
verification: verified,
verification: Verified,
subscriber_count: Some(880000),
)),
publish_date: "[date]",
@ -549,7 +602,7 @@ Channel(
VideoItem(
id: "Kr2XyhpUdUI",
name: "EEVblog 1493 - MacGyver Project - Part 2",
duration: Some(1785),
length: Some(1785),
thumbnail: [
Thumbnail(
url: "https://i.ytimg.com/vi/Kr2XyhpUdUI/hqdefault.jpg?sqp=-oaymwEbCKgBEF5IVfKriqkDDggBFQAAiEIYAXABwAEG&rs=AOn4CLDdL3brjOzbABRuyz-yolawtGRsbw",
@ -576,7 +629,7 @@ Channel(
id: "UC2DjFE7Xf11URZqWBigcVOQ",
name: "EEVblog",
avatar: [],
verification: verified,
verification: Verified,
subscriber_count: Some(880000),
)),
publish_date: "[date]",
@ -590,7 +643,7 @@ Channel(
VideoItem(
id: "rxGafdgkal8",
name: "EEVblog 1492 - $5 Oscilloscope Repaired! + Oz GIVEAWAY",
duration: Some(1163),
length: Some(1163),
thumbnail: [
Thumbnail(
url: "https://i.ytimg.com/vi/rxGafdgkal8/hqdefault.jpg?sqp=-oaymwEbCKgBEF5IVfKriqkDDggBFQAAiEIYAXABwAEG&rs=AOn4CLD-p_t0q_Q2oTGyJuFCQJ5z6VPPMQ",
@ -617,7 +670,7 @@ Channel(
id: "UC2DjFE7Xf11URZqWBigcVOQ",
name: "EEVblog",
avatar: [],
verification: verified,
verification: Verified,
subscriber_count: Some(880000),
)),
publish_date: "[date]",
@ -631,7 +684,7 @@ Channel(
VideoItem(
id: "4yosozyeIP4",
name: "EEVblog 1491 - The MacGyver Project - Part 1",
duration: Some(1706),
length: Some(1706),
thumbnail: [
Thumbnail(
url: "https://i.ytimg.com/vi/4yosozyeIP4/hqdefault.jpg?sqp=-oaymwEbCKgBEF5IVfKriqkDDggBFQAAiEIYAXABwAEG&rs=AOn4CLDRNAWkPQfuQirfiOdowD1iQlWrWg",
@ -658,7 +711,7 @@ Channel(
id: "UC2DjFE7Xf11URZqWBigcVOQ",
name: "EEVblog",
avatar: [],
verification: verified,
verification: Verified,
subscriber_count: Some(880000),
)),
publish_date: "[date]",
@ -672,7 +725,7 @@ Channel(
VideoItem(
id: "06JtC2DC_dQ",
name: "EEVblog 1490 - Insane Jaycar Dumpster Sale! 2022",
duration: Some(1700),
length: Some(1700),
thumbnail: [
Thumbnail(
url: "https://i.ytimg.com/vi/06JtC2DC_dQ/hqdefault.jpg?sqp=-oaymwEbCKgBEF5IVfKriqkDDggBFQAAiEIYAXABwAEG&rs=AOn4CLDVIvEssIKji_8dyBYGYbpIqen7vQ",
@ -699,7 +752,7 @@ Channel(
id: "UC2DjFE7Xf11URZqWBigcVOQ",
name: "EEVblog",
avatar: [],
verification: verified,
verification: Verified,
subscriber_count: Some(880000),
)),
publish_date: "[date]",
@ -713,7 +766,7 @@ Channel(
VideoItem(
id: "piquT76w9TI",
name: "EEVblog 1489 - Mystery Teardown!",
duration: Some(1466),
length: Some(1466),
thumbnail: [
Thumbnail(
url: "https://i.ytimg.com/vi/piquT76w9TI/hqdefault.jpg?sqp=-oaymwEbCKgBEF5IVfKriqkDDggBFQAAiEIYAXABwAEG&rs=AOn4CLCTzIcGeRDwUyINtik50EQCOTxwiA",
@ -740,7 +793,7 @@ Channel(
id: "UC2DjFE7Xf11URZqWBigcVOQ",
name: "EEVblog",
avatar: [],
verification: verified,
verification: Verified,
subscriber_count: Some(880000),
)),
publish_date: "[date]",
@ -754,7 +807,7 @@ Channel(
VideoItem(
id: "pKuUKT-zU-g",
name: "EEVblog 1488 - Tilt Five Augmented Reality AR Glasses - First Reaction!",
duration: Some(2152),
length: Some(2152),
thumbnail: [
Thumbnail(
url: "https://i.ytimg.com/vi/pKuUKT-zU-g/hqdefault.jpg?sqp=-oaymwEbCKgBEF5IVfKriqkDDggBFQAAiEIYAXABwAEG&rs=AOn4CLCB6Rh4miI20yPy2kJaxul_wA3Now",
@ -781,7 +834,7 @@ Channel(
id: "UC2DjFE7Xf11URZqWBigcVOQ",
name: "EEVblog",
avatar: [],
verification: verified,
verification: Verified,
subscriber_count: Some(880000),
)),
publish_date: "[date]",
@ -795,7 +848,7 @@ Channel(
VideoItem(
id: "_R4wQQNSO6k",
name: "EEVblog 1487 - Do Solar Micro Inverters Take Power at Night?",
duration: Some(2399),
length: Some(2399),
thumbnail: [
Thumbnail(
url: "https://i.ytimg.com/vi/_R4wQQNSO6k/hqdefault.jpg?sqp=-oaymwEbCKgBEF5IVfKriqkDDggBFQAAiEIYAXABwAEG&rs=AOn4CLDEQVZ0yQPLZqwLdQednKWwLWqDmA",
@ -822,7 +875,7 @@ Channel(
id: "UC2DjFE7Xf11URZqWBigcVOQ",
name: "EEVblog",
avatar: [],
verification: verified,
verification: Verified,
subscriber_count: Some(880000),
)),
publish_date: "[date]",
@ -836,7 +889,7 @@ Channel(
VideoItem(
id: "ikp5BorIo_M",
name: "EEVblog 1486 - What you DIDN\'T KNOW About Film Capacitor FAILURES!",
duration: Some(1792),
length: Some(1792),
thumbnail: [
Thumbnail(
url: "https://i.ytimg.com/vi/ikp5BorIo_M/hqdefault.jpg?sqp=-oaymwEbCKgBEF5IVfKriqkDDggBFQAAiEIYAXABwAEG&rs=AOn4CLBygGB8KC522NC15BhDC1WpuNKsgw",
@ -863,7 +916,7 @@ Channel(
id: "UC2DjFE7Xf11URZqWBigcVOQ",
name: "EEVblog",
avatar: [],
verification: verified,
verification: Verified,
subscriber_count: Some(880000),
)),
publish_date: "[date]",
@ -877,7 +930,7 @@ Channel(
VideoItem(
id: "7O-QckjCXNo",
name: "eevBLAB 99 - AI SPAM BOT Youtube Space/Science/Tech Channels? - WTF",
duration: Some(592),
length: Some(592),
thumbnail: [
Thumbnail(
url: "https://i.ytimg.com/vi/7O-QckjCXNo/hqdefault.jpg?sqp=-oaymwEbCKgBEF5IVfKriqkDDggBFQAAiEIYAXABwAEG&rs=AOn4CLBY1cRnrWQCbmlAzP5okMmIYjgdsg",
@ -904,7 +957,7 @@ Channel(
id: "UC2DjFE7Xf11URZqWBigcVOQ",
name: "EEVblog",
avatar: [],
verification: verified,
verification: Verified,
subscriber_count: Some(880000),
)),
publish_date: "[date]",
@ -918,7 +971,7 @@ Channel(
VideoItem(
id: "VutdTxF4E-0",
name: "RIP The Old Garage Lab",
duration: Some(115),
length: Some(115),
thumbnail: [
Thumbnail(
url: "https://i.ytimg.com/vi/VutdTxF4E-0/hqdefault.jpg?sqp=-oaymwEbCKgBEF5IVfKriqkDDggBFQAAiEIYAXABwAEG&rs=AOn4CLDlPpT2-UOGfm2A2djTLjCsygeqSw",
@ -945,7 +998,7 @@ Channel(
id: "UC2DjFE7Xf11URZqWBigcVOQ",
name: "EEVblog",
avatar: [],
verification: verified,
verification: Verified,
subscriber_count: Some(880000),
)),
publish_date: "[date]",
@ -959,7 +1012,7 @@ Channel(
VideoItem(
id: "o7xfGuRaq94",
name: "EEVblog 1485 - PedalCell CadenceX Bike Generator LOL FAIL!",
duration: Some(1026),
length: Some(1026),
thumbnail: [
Thumbnail(
url: "https://i.ytimg.com/vi/o7xfGuRaq94/hqdefault.jpg?sqp=-oaymwEbCKgBEF5IVfKriqkDDggBFQAAiEIYAXABwAEG&rs=AOn4CLBA7RRL2USBwkYXp9ouWTbtU-JHSg",
@ -986,7 +1039,7 @@ Channel(
id: "UC2DjFE7Xf11URZqWBigcVOQ",
name: "EEVblog",
avatar: [],
verification: verified,
verification: Verified,
subscriber_count: Some(880000),
)),
publish_date: "[date]",
@ -1000,7 +1053,7 @@ Channel(
VideoItem(
id: "3WSIfHOv3fc",
name: "EEVblog 1484 - Kaba Mas X-09 High Security Electronic Lock Teardown",
duration: Some(1106),
length: Some(1106),
thumbnail: [
Thumbnail(
url: "https://i.ytimg.com/vi/3WSIfHOv3fc/hqdefault.jpg?sqp=-oaymwEbCKgBEF5IVfKriqkDDggBFQAAiEIYAXABwAEG&rs=AOn4CLClZroFRo115ZuxYhJ5rcCDO2ZPcQ",
@ -1027,7 +1080,7 @@ Channel(
id: "UC2DjFE7Xf11URZqWBigcVOQ",
name: "EEVblog",
avatar: [],
verification: verified,
verification: Verified,
subscriber_count: Some(880000),
)),
publish_date: "[date]",
@ -1041,7 +1094,7 @@ Channel(
VideoItem(
id: "8yXZJZCKImI",
name: "EEVblog 1483 - Holy Mailbag Bomb Batman!",
duration: Some(3373),
length: Some(3373),
thumbnail: [
Thumbnail(
url: "https://i.ytimg.com/vi/8yXZJZCKImI/hqdefault.jpg?sqp=-oaymwEbCKgBEF5IVfKriqkDDggBFQAAiEIYAXABwAEG&rs=AOn4CLBym7WfmrvKIjs2ClW-FOLtxbENzw",
@ -1068,7 +1121,7 @@ Channel(
id: "UC2DjFE7Xf11URZqWBigcVOQ",
name: "EEVblog",
avatar: [],
verification: verified,
verification: Verified,
subscriber_count: Some(880000),
)),
publish_date: "[date]",
@ -1082,7 +1135,7 @@ Channel(
VideoItem(
id: "vJ4pW6LKJWU",
name: "EEVblog 1482 - Mains Capacitor Zener Regulator Circuit",
duration: Some(1132),
length: Some(1132),
thumbnail: [
Thumbnail(
url: "https://i.ytimg.com/vi/vJ4pW6LKJWU/hqdefault.jpg?sqp=-oaymwEbCKgBEF5IVfKriqkDDggBFQAAiEIYAXABwAEG&rs=AOn4CLDaKgfAJ4NAeqoMIPZDavsTw_JD5w",
@ -1109,7 +1162,7 @@ Channel(
id: "UC2DjFE7Xf11URZqWBigcVOQ",
name: "EEVblog",
avatar: [],
verification: verified,
verification: Verified,
subscriber_count: Some(880000),
)),
publish_date: "[date]",
@ -1123,7 +1176,7 @@ Channel(
VideoItem(
id: "myqiqUE00fo",
name: "EEVblog 1481 - Dodgy Dangerous Heater REPAIR",
duration: Some(1622),
length: Some(1622),
thumbnail: [
Thumbnail(
url: "https://i.ytimg.com/vi/myqiqUE00fo/hqdefault.jpg?sqp=-oaymwEbCKgBEF5IVfKriqkDDggBFQAAiEIYAXABwAEG&rs=AOn4CLB3nqRnunVeYPk1_vdXP7IEv1E1Rg",
@ -1150,7 +1203,7 @@ Channel(
id: "UC2DjFE7Xf11URZqWBigcVOQ",
name: "EEVblog",
avatar: [],
verification: verified,
verification: Verified,
subscriber_count: Some(880000),
)),
publish_date: "[date]",
@ -1164,7 +1217,7 @@ Channel(
VideoItem(
id: "xIokNnjuam8",
name: "EEVblog 1480 - Lightyear Zero Solar Powered Electric Car",
duration: Some(1196),
length: Some(1196),
thumbnail: [
Thumbnail(
url: "https://i.ytimg.com/vi/xIokNnjuam8/hqdefault.jpg?sqp=-oaymwEbCKgBEF5IVfKriqkDDggBFQAAiEIYAXABwAEG&rs=AOn4CLBRxCpyCftz0LJooMtxBcIWwaF6hw",
@ -1191,7 +1244,7 @@ Channel(
id: "UC2DjFE7Xf11URZqWBigcVOQ",
name: "EEVblog",
avatar: [],
verification: verified,
verification: Verified,
subscriber_count: Some(880000),
)),
publish_date: "[date]",
@ -1205,7 +1258,7 @@ Channel(
VideoItem(
id: "S3R4r2xvVYQ",
name: "EEVblog 1479 - Is Your Calculator WRONG?",
duration: Some(1066),
length: Some(1066),
thumbnail: [
Thumbnail(
url: "https://i.ytimg.com/vi/S3R4r2xvVYQ/hqdefault.jpg?sqp=-oaymwEbCKgBEF5IVfKriqkDDggBFQAAiEIYAXABwAEG&rs=AOn4CLC2ZW-UUXJGrtHphT2E53pFafr-1g",
@ -1232,7 +1285,7 @@ Channel(
id: "UC2DjFE7Xf11URZqWBigcVOQ",
name: "EEVblog",
avatar: [],
verification: verified,
verification: Verified,
subscriber_count: Some(880000),
)),
publish_date: "[date]",
@ -1246,7 +1299,7 @@ Channel(
VideoItem(
id: "RlwcdUnRw6w",
name: "EEVblog 1478 - Waveform Update Rate Shootout - Tek 2 Series vs Others",
duration: Some(1348),
length: Some(1348),
thumbnail: [
Thumbnail(
url: "https://i.ytimg.com/vi/RlwcdUnRw6w/hqdefault.jpg?sqp=-oaymwEbCKgBEF5IVfKriqkDDggBFQAAiEIYAXABwAEG&rs=AOn4CLBYaH7c8-BP8807GgNGML2WUNK8pg",
@ -1273,7 +1326,7 @@ Channel(
id: "UC2DjFE7Xf11URZqWBigcVOQ",
name: "EEVblog",
avatar: [],
verification: verified,
verification: Verified,
subscriber_count: Some(880000),
)),
publish_date: "[date]",
@ -1287,7 +1340,7 @@ Channel(
VideoItem(
id: "R2fw2g6WFbg",
name: "EEVblog 1477 - TEARDOWN! - NEW Tektronix 2 Series Oscilloscope",
duration: Some(2718),
length: Some(2718),
thumbnail: [
Thumbnail(
url: "https://i.ytimg.com/vi/R2fw2g6WFbg/hqdefault.jpg?sqp=-oaymwEbCKgBEF5IVfKriqkDDggBFQAAiEIYAXABwAEG&rs=AOn4CLBwd6wqvFI0HcPpOkDW_XDzWSPH_w",
@ -1314,7 +1367,7 @@ Channel(
id: "UC2DjFE7Xf11URZqWBigcVOQ",
name: "EEVblog",
avatar: [],
verification: verified,
verification: Verified,
subscriber_count: Some(880000),
)),
publish_date: "[date]",

View file

@ -5,9 +5,7 @@ expression: map_res.c
Channel(
id: "UCxBa895m48H5idw5li7h-0g",
name: "Sebastian Figurroa",
handle: None,
subscriber_count: None,
video_count: None,
avatar: [
Thumbnail(
url: "https://yt3.ggpht.com/ytc/AMLnZu_hsZ1XlUXHzXsGNHJw0np79WhWZcC4j8eFdy-tiUCDBKAjJyJOzE5kXFRiqL2S=s48-c-k-c0x00ffffff-no-rj",
@ -25,10 +23,13 @@ Channel(
height: 176,
),
],
verification: none,
verification: None,
description: "",
tags: [],
vanity_url: None,
banner: [],
mobile_banner: [],
tv_banner: [],
has_shorts: false,
has_live: false,
visitor_data: Some("Cgtvc2s4UllvTGl6byigxseZBg%3D%3D"),

View file

@ -5,9 +5,7 @@ expression: map_res.c
Channel(
id: "UChs0pSaEoNLV4mevBFGaoKA",
name: "The Good Life Radio x Sensual Musique",
handle: None,
subscriber_count: Some(760000),
video_count: None,
avatar: [
Thumbnail(
url: "https://yt3.ggpht.com/ytc/AMLnZu_V9mOdHaorjNFqGXCecFeOBZhDWB8tVYG_I8gJwA=s48-c-k-c0x00ffffff-no-rj",
@ -25,7 +23,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",
@ -41,6 +39,7 @@ Channel(
"tropical house",
"house music",
],
vanity_url: Some("https://www.youtube.com/c/TheGoodLiferadio"),
banner: [
Thumbnail(
url: "https://yt3.ggpht.com/fL4x31Q80O_BvnhVIMI9YlV3apsiFvBENwGiSA-Hw9An6djAGw92RSOFax6z2r_rJNbRWPMA=w1060-fcrop64=1,00005a57ffffa5a8-k-c0xffffffff-no-nd-rj",
@ -73,6 +72,60 @@ Channel(
height: 424,
),
],
mobile_banner: [
Thumbnail(
url: "https://yt3.ggpht.com/fL4x31Q80O_BvnhVIMI9YlV3apsiFvBENwGiSA-Hw9An6djAGw92RSOFax6z2r_rJNbRWPMA=w320-fcrop64=1,32b75a57cd48a5a8-k-c0xffffffff-no-nd-rj",
width: 320,
height: 88,
),
Thumbnail(
url: "https://yt3.ggpht.com/fL4x31Q80O_BvnhVIMI9YlV3apsiFvBENwGiSA-Hw9An6djAGw92RSOFax6z2r_rJNbRWPMA=w640-fcrop64=1,32b75a57cd48a5a8-k-c0xffffffff-no-nd-rj",
width: 640,
height: 175,
),
Thumbnail(
url: "https://yt3.ggpht.com/fL4x31Q80O_BvnhVIMI9YlV3apsiFvBENwGiSA-Hw9An6djAGw92RSOFax6z2r_rJNbRWPMA=w960-fcrop64=1,32b75a57cd48a5a8-k-c0xffffffff-no-nd-rj",
width: 960,
height: 263,
),
Thumbnail(
url: "https://yt3.ggpht.com/fL4x31Q80O_BvnhVIMI9YlV3apsiFvBENwGiSA-Hw9An6djAGw92RSOFax6z2r_rJNbRWPMA=w1280-fcrop64=1,32b75a57cd48a5a8-k-c0xffffffff-no-nd-rj",
width: 1280,
height: 351,
),
Thumbnail(
url: "https://yt3.ggpht.com/fL4x31Q80O_BvnhVIMI9YlV3apsiFvBENwGiSA-Hw9An6djAGw92RSOFax6z2r_rJNbRWPMA=w1440-fcrop64=1,32b75a57cd48a5a8-k-c0xffffffff-no-nd-rj",
width: 1440,
height: 395,
),
],
tv_banner: [
Thumbnail(
url: "https://yt3.ggpht.com/fL4x31Q80O_BvnhVIMI9YlV3apsiFvBENwGiSA-Hw9An6djAGw92RSOFax6z2r_rJNbRWPMA=w320-fcrop64=1,00000000ffffffff-k-c0xffffffff-no-nd-rj",
width: 320,
height: 180,
),
Thumbnail(
url: "https://yt3.ggpht.com/fL4x31Q80O_BvnhVIMI9YlV3apsiFvBENwGiSA-Hw9An6djAGw92RSOFax6z2r_rJNbRWPMA=w854-fcrop64=1,00000000ffffffff-k-c0xffffffff-no-nd-rj",
width: 854,
height: 480,
),
Thumbnail(
url: "https://yt3.ggpht.com/fL4x31Q80O_BvnhVIMI9YlV3apsiFvBENwGiSA-Hw9An6djAGw92RSOFax6z2r_rJNbRWPMA=w1280-fcrop64=1,00000000ffffffff-k-c0xffffffff-no-nd-rj",
width: 1280,
height: 720,
),
Thumbnail(
url: "https://yt3.ggpht.com/fL4x31Q80O_BvnhVIMI9YlV3apsiFvBENwGiSA-Hw9An6djAGw92RSOFax6z2r_rJNbRWPMA=w1920-fcrop64=1,00000000ffffffff-k-c0xffffffff-no-nd-rj",
width: 1920,
height: 1080,
),
Thumbnail(
url: "https://yt3.ggpht.com/fL4x31Q80O_BvnhVIMI9YlV3apsiFvBENwGiSA-Hw9An6djAGw92RSOFax6z2r_rJNbRWPMA=w2120-fcrop64=1,00000000ffffffff-k-c0xffffffff-no-nd-rj",
width: 2120,
height: 1192,
),
],
has_shorts: false,
has_live: false,
visitor_data: Some("CgtkYXJITElwYmd4OCj85a2ZBg%3D%3D"),
@ -82,7 +135,7 @@ Channel(
VideoItem(
id: "csP93FGy0bs",
name: "Chill Out Music Mix • 24/7 Live Radio | Relaxing Deep House, Chillout Lounge, Vocal & Instrumental",
duration: None,
length: None,
thumbnail: [
Thumbnail(
url: "https://i.ytimg.com/vi/csP93FGy0bs/hqdefault_live.jpg?sqp=-oaymwEbCKgBEF5IVfKriqkDDggBFQAAiEIYAXABwAEG&rs=AOn4CLDq5TEpXIGH_OHZhn2_Jx7lp2kMUQ",
@ -109,7 +162,7 @@ Channel(
id: "UChs0pSaEoNLV4mevBFGaoKA",
name: "The Good Life Radio x Sensual Musique",
avatar: [],
verification: verified,
verification: Verified,
subscriber_count: Some(760000),
)),
publish_date: "[date]",
@ -123,7 +176,7 @@ Channel(
VideoItem(
id: "19hKXI1ENrY",
name: "Deep House Radio | Relaxing & Chill House, Best Summer Mix 2022, Gym & Workout Music",
duration: None,
length: None,
thumbnail: [
Thumbnail(
url: "https://i.ytimg.com/vi/19hKXI1ENrY/hqdefault_live.jpg?sqp=-oaymwEbCKgBEF5IVfKriqkDDggBFQAAiEIYAXABwAEG&rs=AOn4CLAmi9jgARxMYdZpIOLw5RhQkRx0Dg",
@ -150,7 +203,7 @@ Channel(
id: "UChs0pSaEoNLV4mevBFGaoKA",
name: "The Good Life Radio x Sensual Musique",
avatar: [],
verification: verified,
verification: Verified,
subscriber_count: Some(760000),
)),
publish_date: "[date]",
@ -164,7 +217,7 @@ Channel(
VideoItem(
id: "CqMUC5eXX7c",
name: "Back To School / Work 📚 Deep Focus Chillout Mix | The Good Life Radio #4",
duration: Some(4667),
length: Some(4667),
thumbnail: [
Thumbnail(
url: "https://i.ytimg.com/vi/CqMUC5eXX7c/hqdefault.jpg?sqp=-oaymwEbCKgBEF5IVfKriqkDDggBFQAAiEIYAXABwAEG&rs=AOn4CLDJglNaF89w0KFxzGn4Y3UAwu9ydg",
@ -191,7 +244,7 @@ Channel(
id: "UChs0pSaEoNLV4mevBFGaoKA",
name: "The Good Life Radio x Sensual Musique",
avatar: [],
verification: verified,
verification: Verified,
subscriber_count: Some(760000),
)),
publish_date: "[date]",
@ -205,7 +258,7 @@ Channel(
VideoItem(
id: "A77SYlXKQEM",
name: "Chillout Lounge 🏖\u{fe0f} Calm & Relaxing Background Music | The Good Life Radio #3",
duration: Some(1861),
length: Some(1861),
thumbnail: [
Thumbnail(
url: "https://i.ytimg.com/vi/A77SYlXKQEM/hqdefault.jpg?sqp=-oaymwEbCKgBEF5IVfKriqkDDggBFQAAiEIYAXABwAEG&rs=AOn4CLA6c0iWB5IjXrbncP1JT2gvljTwyw",
@ -232,7 +285,7 @@ Channel(
id: "UChs0pSaEoNLV4mevBFGaoKA",
name: "The Good Life Radio x Sensual Musique",
avatar: [],
verification: verified,
verification: Verified,
subscriber_count: Some(760000),
)),
publish_date: "[date]",
@ -246,7 +299,7 @@ Channel(
VideoItem(
id: "72vkRHQfjbk",
name: "Summer Lovers 💖 A Beautiful & Relaxing Chillout Deep House Mix | The Good Life Radio #2",
duration: Some(1832),
length: Some(1832),
thumbnail: [
Thumbnail(
url: "https://i.ytimg.com/vi/72vkRHQfjbk/hqdefault.jpg?sqp=-oaymwEbCKgBEF5IVfKriqkDDggBFQAAiEIYAXABwAEG&rs=AOn4CLBBMAUBpqHTq2IalWplaJugEhf4eQ",
@ -273,7 +326,7 @@ Channel(
id: "UChs0pSaEoNLV4mevBFGaoKA",
name: "The Good Life Radio x Sensual Musique",
avatar: [],
verification: verified,
verification: Verified,
subscriber_count: Some(760000),
)),
publish_date: "[date]",
@ -287,7 +340,7 @@ Channel(
VideoItem(
id: "AMWMDhibROw",
name: "Relaxing & Chill House 🌴 Summer \'21 Chill-Out Mix | The Good Life Radio #1",
duration: Some(1949),
length: Some(1949),
thumbnail: [
Thumbnail(
url: "https://i.ytimg.com/vi/AMWMDhibROw/hqdefault.jpg?sqp=-oaymwEbCKgBEF5IVfKriqkDDggBFQAAiEIYAXABwAEG&rs=AOn4CLCDO-i7ZMHpgILmTxjIvtFEDl3fTQ",
@ -314,7 +367,7 @@ Channel(
id: "UChs0pSaEoNLV4mevBFGaoKA",
name: "The Good Life Radio x Sensual Musique",
avatar: [],
verification: verified,
verification: Verified,
subscriber_count: Some(760000),
)),
publish_date: "[date]",
@ -328,7 +381,7 @@ Channel(
VideoItem(
id: "9UMxZofMNbA",
name: "Chillout Lounge - Calm & Relaxing Background Music | Study, Work, Sleep, Meditation, Chill",
duration: None,
length: None,
thumbnail: [
Thumbnail(
url: "https://i.ytimg.com/vi/9UMxZofMNbA/hqdefault_live.jpg?sqp=-oaymwEbCKgBEF5IVfKriqkDDggBFQAAiEIYAXABwAEG&rs=AOn4CLDc3KEjaAI_syibPmnpLN04x1Wv7g",
@ -355,7 +408,7 @@ Channel(
id: "UChs0pSaEoNLV4mevBFGaoKA",
name: "The Good Life Radio x Sensual Musique",
avatar: [],
verification: verified,
verification: Verified,
subscriber_count: Some(760000),
)),
publish_date: "[date]",
@ -369,7 +422,7 @@ Channel(
VideoItem(
id: "a2sEYVwBvX4",
name: "Paratone - Heaven Is A Place On Earth (feat. kaii)",
duration: Some(161),
length: Some(161),
thumbnail: [
Thumbnail(
url: "https://i.ytimg.com/vi/a2sEYVwBvX4/hqdefault.jpg?sqp=-oaymwEbCKgBEF5IVfKriqkDDggBFQAAiEIYAXABwAEG&rs=AOn4CLBwBX3CEEc3ZK1SsP8iUbebtp5hUw",
@ -396,7 +449,7 @@ Channel(
id: "UChs0pSaEoNLV4mevBFGaoKA",
name: "The Good Life Radio x Sensual Musique",
avatar: [],
verification: verified,
verification: Verified,
subscriber_count: Some(760000),
)),
publish_date: "[date]",
@ -410,7 +463,7 @@ Channel(
VideoItem(
id: "JAY-prtJnGY",
name: "Joseph Feinstein - Where I Belong",
duration: Some(126),
length: Some(126),
thumbnail: [
Thumbnail(
url: "https://i.ytimg.com/vi/JAY-prtJnGY/hqdefault.jpg?sqp=-oaymwEbCKgBEF5IVfKriqkDDggBFQAAiEIYAXABwAEG&rs=AOn4CLC79uFNaKWCm0lQ8_uxV0s2G0jJ-Q",
@ -437,7 +490,7 @@ Channel(
id: "UChs0pSaEoNLV4mevBFGaoKA",
name: "The Good Life Radio x Sensual Musique",
avatar: [],
verification: verified,
verification: Verified,
subscriber_count: Some(760000),
)),
publish_date: "[date]",
@ -451,7 +504,7 @@ Channel(
VideoItem(
id: "DySa8OrQDi4",
name: "LA Vision & Gigi D\'Agostino - Hollywood",
duration: Some(200),
length: Some(200),
thumbnail: [
Thumbnail(
url: "https://i.ytimg.com/vi/DySa8OrQDi4/hqdefault.jpg?sqp=-oaymwEbCKgBEF5IVfKriqkDDggBFQAAiEIYAXABwAEG&rs=AOn4CLAzPj5ZqrnaLQELc8EDtgLlUhDdRQ",
@ -478,7 +531,7 @@ Channel(
id: "UChs0pSaEoNLV4mevBFGaoKA",
name: "The Good Life Radio x Sensual Musique",
avatar: [],
verification: verified,
verification: Verified,
subscriber_count: Some(760000),
)),
publish_date: "[date]",
@ -492,7 +545,7 @@ Channel(
VideoItem(
id: "NqzXULaB8MA",
name: "LO - Home",
duration: Some(163),
length: Some(163),
thumbnail: [
Thumbnail(
url: "https://i.ytimg.com/vi/NqzXULaB8MA/hqdefault.jpg?sqp=-oaymwEbCKgBEF5IVfKriqkDDggBFQAAiEIYAXABwAEG&rs=AOn4CLDFvB5JbSQIUtb-pldtNWWHb2Y3SQ",
@ -519,7 +572,7 @@ Channel(
id: "UChs0pSaEoNLV4mevBFGaoKA",
name: "The Good Life Radio x Sensual Musique",
avatar: [],
verification: verified,
verification: Verified,
subscriber_count: Some(760000),
)),
publish_date: "[date]",
@ -533,7 +586,7 @@ Channel(
VideoItem(
id: "UGzy6uhZkmw",
name: "Luca - Sunset",
duration: Some(153),
length: Some(153),
thumbnail: [
Thumbnail(
url: "https://i.ytimg.com/vi/UGzy6uhZkmw/hqdefault.jpg?sqp=-oaymwEbCKgBEF5IVfKriqkDDggBFQAAiEIYAXABwAEG&rs=AOn4CLD93d5foF1_yGd6ej5_8t-PM7ZCDw",
@ -560,7 +613,7 @@ Channel(
id: "UChs0pSaEoNLV4mevBFGaoKA",
name: "The Good Life Radio x Sensual Musique",
avatar: [],
verification: verified,
verification: Verified,
subscriber_count: Some(760000),
)),
publish_date: "[date]",
@ -574,7 +627,7 @@ Channel(
VideoItem(
id: "iuvapHKpW8A",
name: "nourii - Better Off (feat. BCS)",
duration: Some(126),
length: Some(126),
thumbnail: [
Thumbnail(
url: "https://i.ytimg.com/vi/iuvapHKpW8A/hqdefault.jpg?sqp=-oaymwEbCKgBEF5IVfKriqkDDggBFQAAiEIYAXABwAEG&rs=AOn4CLCsDj4nWrDpmF-BTY_9REtx8xiHjA",
@ -601,7 +654,7 @@ Channel(
id: "UChs0pSaEoNLV4mevBFGaoKA",
name: "The Good Life Radio x Sensual Musique",
avatar: [],
verification: verified,
verification: Verified,
subscriber_count: Some(760000),
)),
publish_date: "[date]",
@ -615,7 +668,7 @@ Channel(
VideoItem(
id: "n_1Nwht-Gh4",
name: "Deep House Covers & Remixes of Popular Songs 2020 🌴 Deep House, G-House, Chill-Out Music Playlist",
duration: Some(2940),
length: Some(2940),
thumbnail: [
Thumbnail(
url: "https://i.ytimg.com/vi/n_1Nwht-Gh4/hqdefault.jpg?sqp=-oaymwEbCKgBEF5IVfKriqkDDggBFQAAiEIYAXABwAEG&rs=AOn4CLAwRnMWNt4fNmmGR4THSsTh-9MiCw",
@ -642,7 +695,7 @@ Channel(
id: "UChs0pSaEoNLV4mevBFGaoKA",
name: "The Good Life Radio x Sensual Musique",
avatar: [],
verification: verified,
verification: Verified,
subscriber_count: Some(760000),
)),
publish_date: "[date]",
@ -656,7 +709,7 @@ Channel(
VideoItem(
id: "6TptI5BtP5U",
name: "The Good Life Radio Mix #2 | Summer Memories ☀\u{fe0f} (Chill Music Playlist 2020)",
duration: Some(3448),
length: Some(3448),
thumbnail: [
Thumbnail(
url: "https://i.ytimg.com/vi/6TptI5BtP5U/hqdefault.jpg?sqp=-oaymwEbCKgBEF5IVfKriqkDDggBFQAAiEIYAXABwAEG&rs=AOn4CLBGvxAmGVff9uk5AOxBij56uB6azw",
@ -683,7 +736,7 @@ Channel(
id: "UChs0pSaEoNLV4mevBFGaoKA",
name: "The Good Life Radio x Sensual Musique",
avatar: [],
verification: verified,
verification: Verified,
subscriber_count: Some(760000),
)),
publish_date: "[date]",
@ -697,7 +750,7 @@ Channel(
VideoItem(
id: "36YnV9STBqc",
name: "The Good Life Radio\u{a0}•\u{a0}24/7 Live Radio | Best Relax House, Chillout, Study, Running, Gym, Happy Music",
duration: None,
length: None,
thumbnail: [
Thumbnail(
url: "https://i.ytimg.com/vi/36YnV9STBqc/hqdefault_live.jpg?sqp=-oaymwEbCKgBEF5IVfKriqkDDggBFQAAiEIYAXABwAEG&rs=AOn4CLCe7OwcMt2h8bSNHbTTULV9-SST1Q",
@ -724,7 +777,7 @@ Channel(
id: "UChs0pSaEoNLV4mevBFGaoKA",
name: "The Good Life Radio x Sensual Musique",
avatar: [],
verification: verified,
verification: Verified,
subscriber_count: Some(760000),
)),
publish_date: "[date]",
@ -738,7 +791,7 @@ Channel(
VideoItem(
id: "7x6ii2TcsPE",
name: "The Good Life Radio Mix #1 | Relaxing & Chill House Music Playlist 2020",
duration: Some(2726),
length: Some(2726),
thumbnail: [
Thumbnail(
url: "https://i.ytimg.com/vi/7x6ii2TcsPE/hqdefault.jpg?sqp=-oaymwEbCKgBEF5IVfKriqkDDggBFQAAiEIYAXABwAEG&rs=AOn4CLC-CNpKCSMnLIscrYKNPX7DRZ0buA",
@ -765,7 +818,7 @@ Channel(
id: "UChs0pSaEoNLV4mevBFGaoKA",
name: "The Good Life Radio x Sensual Musique",
avatar: [],
verification: verified,
verification: Verified,
subscriber_count: Some(760000),
)),
publish_date: "[date]",
@ -779,7 +832,7 @@ Channel(
VideoItem(
id: "mxV5MBZYYDE",
name: "Christmas Music with Vocals 🎅 Best Relaxing Christmas Songs 2020",
duration: Some(5863),
length: Some(5863),
thumbnail: [
Thumbnail(
url: "https://i.ytimg.com/vi/mxV5MBZYYDE/hqdefault.jpg?sqp=-oaymwEbCKgBEF5IVfKriqkDDggBFQAAiEIYAXABwAEG&rs=AOn4CLCVUbM3MtN0zZcE_8lY4eyo-Ly5Kw",
@ -806,7 +859,7 @@ Channel(
id: "UChs0pSaEoNLV4mevBFGaoKA",
name: "The Good Life Radio x Sensual Musique",
avatar: [],
verification: verified,
verification: Verified,
subscriber_count: Some(760000),
)),
publish_date: "[date]",
@ -820,7 +873,7 @@ Channel(
VideoItem(
id: "hh2AOoPoAIo",
name: "The Good Life Radio Mix 2019 🎅 Winter & Christmas Relax House Playlist [Best of Part 1]",
duration: Some(2530),
length: Some(2530),
thumbnail: [
Thumbnail(
url: "https://i.ytimg.com/vi/hh2AOoPoAIo/hqdefault.jpg?sqp=-oaymwEbCKgBEF5IVfKriqkDDggBFQAAiEIYAXABwAEG&rs=AOn4CLAMmrbiYHz-7STgazeW2PAuGCkCcg",
@ -847,7 +900,7 @@ Channel(
id: "UChs0pSaEoNLV4mevBFGaoKA",
name: "The Good Life Radio x Sensual Musique",
avatar: [],
verification: verified,
verification: Verified,
subscriber_count: Some(760000),
)),
publish_date: "[date]",
@ -861,7 +914,7 @@ Channel(
VideoItem(
id: "aFlvhtWsJ0g",
name: "Chillout Playlist | Relaxing Summer Music Mix 2019 [Deep & Tropical House]",
duration: Some(2483),
length: Some(2483),
thumbnail: [
Thumbnail(
url: "https://i.ytimg.com/vi/aFlvhtWsJ0g/hqdefault.jpg?sqp=-oaymwEbCKgBEF5IVfKriqkDDggBFQAAiEIYAXABwAEG&rs=AOn4CLAvMC2I82wG7eQPDQmnyC3RbUGFWg",
@ -888,7 +941,7 @@ Channel(
id: "UChs0pSaEoNLV4mevBFGaoKA",
name: "The Good Life Radio x Sensual Musique",
avatar: [],
verification: verified,
verification: Verified,
subscriber_count: Some(760000),
)),
publish_date: "[date]",
@ -902,7 +955,7 @@ Channel(
VideoItem(
id: "cD-d7u6fnEI",
name: "Chill House Playlist | Relaxing Summer Music 2019",
duration: Some(3165),
length: Some(3165),
thumbnail: [
Thumbnail(
url: "https://i.ytimg.com/vi/cD-d7u6fnEI/hqdefault.jpg?sqp=-oaymwEbCKgBEF5IVfKriqkDDggBFQAAiEIYAXABwAEG&rs=AOn4CLBU_f1nTElkLg9ic2eKjM6luGgVcw",
@ -929,7 +982,7 @@ Channel(
id: "UChs0pSaEoNLV4mevBFGaoKA",
name: "The Good Life Radio x Sensual Musique",
avatar: [],
verification: verified,
verification: Verified,
subscriber_count: Some(760000),
)),
publish_date: "[date]",

View file

@ -5,9 +5,7 @@ expression: map_res.c
Channel(
id: "UC_vmjW5e1xEHhYjY2a0kK1A",
name: "Oonagh - Topic",
handle: None,
subscriber_count: None,
video_count: None,
avatar: [
Thumbnail(
url: "https://yt3.ggpht.com/pqKv4iqSjmMKPxsMCeyklTbpROSyInGNR4XvD1DqKD0AlROlsHzvoAlTvtMTO1g1x2WxaQ2Enxw=s48-c-k-c0x00ffffff-no-rj",
@ -25,9 +23,10 @@ Channel(
height: 176,
),
],
verification: none,
verification: None,
description: "",
tags: [],
vanity_url: None,
banner: [
Thumbnail(
url: "https://yt3.ggpht.com/EDatBjgcL94-qSfQa5Twr8l88hYcAXQJksDrwARWbotrWzJhG03gRyZLKV1mk1a1tMI_LSg4qg=w1060-fcrop64=1,00005a57ffffa5a8-k-c0xffffffff-no-nd-rj",
@ -60,6 +59,60 @@ Channel(
height: 424,
),
],
mobile_banner: [
Thumbnail(
url: "https://yt3.ggpht.com/EDatBjgcL94-qSfQa5Twr8l88hYcAXQJksDrwARWbotrWzJhG03gRyZLKV1mk1a1tMI_LSg4qg=w320-fcrop64=1,32b75a57cd48a5a8-k-c0xffffffff-no-nd-rj",
width: 320,
height: 88,
),
Thumbnail(
url: "https://yt3.ggpht.com/EDatBjgcL94-qSfQa5Twr8l88hYcAXQJksDrwARWbotrWzJhG03gRyZLKV1mk1a1tMI_LSg4qg=w640-fcrop64=1,32b75a57cd48a5a8-k-c0xffffffff-no-nd-rj",
width: 640,
height: 175,
),
Thumbnail(
url: "https://yt3.ggpht.com/EDatBjgcL94-qSfQa5Twr8l88hYcAXQJksDrwARWbotrWzJhG03gRyZLKV1mk1a1tMI_LSg4qg=w960-fcrop64=1,32b75a57cd48a5a8-k-c0xffffffff-no-nd-rj",
width: 960,
height: 263,
),
Thumbnail(
url: "https://yt3.ggpht.com/EDatBjgcL94-qSfQa5Twr8l88hYcAXQJksDrwARWbotrWzJhG03gRyZLKV1mk1a1tMI_LSg4qg=w1280-fcrop64=1,32b75a57cd48a5a8-k-c0xffffffff-no-nd-rj",
width: 1280,
height: 351,
),
Thumbnail(
url: "https://yt3.ggpht.com/EDatBjgcL94-qSfQa5Twr8l88hYcAXQJksDrwARWbotrWzJhG03gRyZLKV1mk1a1tMI_LSg4qg=w1440-fcrop64=1,32b75a57cd48a5a8-k-c0xffffffff-no-nd-rj",
width: 1440,
height: 395,
),
],
tv_banner: [
Thumbnail(
url: "https://yt3.ggpht.com/EDatBjgcL94-qSfQa5Twr8l88hYcAXQJksDrwARWbotrWzJhG03gRyZLKV1mk1a1tMI_LSg4qg=w320-fcrop64=1,00000000ffffffff-k-c0xffffffff-no-nd-rj",
width: 320,
height: 180,
),
Thumbnail(
url: "https://yt3.ggpht.com/EDatBjgcL94-qSfQa5Twr8l88hYcAXQJksDrwARWbotrWzJhG03gRyZLKV1mk1a1tMI_LSg4qg=w854-fcrop64=1,00000000ffffffff-k-c0xffffffff-no-nd-rj",
width: 854,
height: 480,
),
Thumbnail(
url: "https://yt3.ggpht.com/EDatBjgcL94-qSfQa5Twr8l88hYcAXQJksDrwARWbotrWzJhG03gRyZLKV1mk1a1tMI_LSg4qg=w1280-fcrop64=1,00000000ffffffff-k-c0xffffffff-no-nd-rj",
width: 1280,
height: 720,
),
Thumbnail(
url: "https://yt3.ggpht.com/EDatBjgcL94-qSfQa5Twr8l88hYcAXQJksDrwARWbotrWzJhG03gRyZLKV1mk1a1tMI_LSg4qg=w1920-fcrop64=1,00000000ffffffff-k-c0xffffffff-no-nd-rj",
width: 1920,
height: 1080,
),
Thumbnail(
url: "https://yt3.ggpht.com/EDatBjgcL94-qSfQa5Twr8l88hYcAXQJksDrwARWbotrWzJhG03gRyZLKV1mk1a1tMI_LSg4qg=w2120-fcrop64=1,00000000ffffffff-k-c0xffffffff-no-nd-rj",
width: 2120,
height: 1192,
),
],
has_shorts: false,
has_live: false,
visitor_data: Some("CgtCV1l2R2Rzb2ZSZyiu4a2ZBg%3D%3D"),

View file

@ -5,9 +5,7 @@ expression: map_res.c
Channel(
id: "UCh8gHdtzO2tXd593_bjErWg",
name: "Doobydobap",
handle: None,
subscriber_count: Some(2840000),
video_count: None,
avatar: [
Thumbnail(
url: "https://yt3.ggpht.com/dm5Aq93xvVJz0NoVO88ieBkDXmuShCujGPlZ7qETMEPTrXvPUCFI3-BB6Xs_P-r6Uk3mnBy9zA=s48-c-k-c0x00ffffff-no-rj",
@ -25,9 +23,10 @@ Channel(
height: 176,
),
],
verification: verified,
verification: Verified,
description: "Hi, Im 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 its because youre hungry at midnight or trying to learn how to cook, I hope you enjoy watching my content and recipes. Don\'t yuck my yum!\n\nwww.doobydobap.com\n",
tags: [],
vanity_url: Some("https://www.youtube.com/c/Doobydobap"),
banner: [
Thumbnail(
url: "https://yt3.ggpht.com/BvnAqgiursrXpmS9AgDLtkOSTQfOG_Dqn0KzY5hcwO9XrHTEQTVgaflI913f9KRp7d0U2qBp=w1060-fcrop64=1,00005a57ffffa5a8-k-c0xffffffff-no-nd-rj",
@ -60,6 +59,60 @@ Channel(
height: 424,
),
],
mobile_banner: [
Thumbnail(
url: "https://yt3.ggpht.com/BvnAqgiursrXpmS9AgDLtkOSTQfOG_Dqn0KzY5hcwO9XrHTEQTVgaflI913f9KRp7d0U2qBp=w320-fcrop64=1,32b75a57cd48a5a8-k-c0xffffffff-no-nd-rj",
width: 320,
height: 88,
),
Thumbnail(
url: "https://yt3.ggpht.com/BvnAqgiursrXpmS9AgDLtkOSTQfOG_Dqn0KzY5hcwO9XrHTEQTVgaflI913f9KRp7d0U2qBp=w640-fcrop64=1,32b75a57cd48a5a8-k-c0xffffffff-no-nd-rj",
width: 640,
height: 175,
),
Thumbnail(
url: "https://yt3.ggpht.com/BvnAqgiursrXpmS9AgDLtkOSTQfOG_Dqn0KzY5hcwO9XrHTEQTVgaflI913f9KRp7d0U2qBp=w960-fcrop64=1,32b75a57cd48a5a8-k-c0xffffffff-no-nd-rj",
width: 960,
height: 263,
),
Thumbnail(
url: "https://yt3.ggpht.com/BvnAqgiursrXpmS9AgDLtkOSTQfOG_Dqn0KzY5hcwO9XrHTEQTVgaflI913f9KRp7d0U2qBp=w1280-fcrop64=1,32b75a57cd48a5a8-k-c0xffffffff-no-nd-rj",
width: 1280,
height: 351,
),
Thumbnail(
url: "https://yt3.ggpht.com/BvnAqgiursrXpmS9AgDLtkOSTQfOG_Dqn0KzY5hcwO9XrHTEQTVgaflI913f9KRp7d0U2qBp=w1440-fcrop64=1,32b75a57cd48a5a8-k-c0xffffffff-no-nd-rj",
width: 1440,
height: 395,
),
],
tv_banner: [
Thumbnail(
url: "https://yt3.ggpht.com/BvnAqgiursrXpmS9AgDLtkOSTQfOG_Dqn0KzY5hcwO9XrHTEQTVgaflI913f9KRp7d0U2qBp=w320-fcrop64=1,00000000ffffffff-k-c0xffffffff-no-nd-rj",
width: 320,
height: 180,
),
Thumbnail(
url: "https://yt3.ggpht.com/BvnAqgiursrXpmS9AgDLtkOSTQfOG_Dqn0KzY5hcwO9XrHTEQTVgaflI913f9KRp7d0U2qBp=w854-fcrop64=1,00000000ffffffff-k-c0xffffffff-no-nd-rj",
width: 854,
height: 480,
),
Thumbnail(
url: "https://yt3.ggpht.com/BvnAqgiursrXpmS9AgDLtkOSTQfOG_Dqn0KzY5hcwO9XrHTEQTVgaflI913f9KRp7d0U2qBp=w1280-fcrop64=1,00000000ffffffff-k-c0xffffffff-no-nd-rj",
width: 1280,
height: 720,
),
Thumbnail(
url: "https://yt3.ggpht.com/BvnAqgiursrXpmS9AgDLtkOSTQfOG_Dqn0KzY5hcwO9XrHTEQTVgaflI913f9KRp7d0U2qBp=w1920-fcrop64=1,00000000ffffffff-k-c0xffffffff-no-nd-rj",
width: 1920,
height: 1080,
),
Thumbnail(
url: "https://yt3.ggpht.com/BvnAqgiursrXpmS9AgDLtkOSTQfOG_Dqn0KzY5hcwO9XrHTEQTVgaflI913f9KRp7d0U2qBp=w2120-fcrop64=1,00000000ffffffff-k-c0xffffffff-no-nd-rj",
width: 2120,
height: 1192,
),
],
has_shorts: false,
has_live: false,
visitor_data: Some("CgtneXVRbGtSMWtlYyj75a2ZBg%3D%3D"),
@ -69,7 +122,7 @@ Channel(
VideoItem(
id: "JBUZE0mIlg8",
name: "small but sure joy",
duration: None,
length: None,
thumbnail: [
Thumbnail(
url: "https://i.ytimg.com/vi/JBUZE0mIlg8/hq720_2.jpg?sqp=-oaymwEdCJUDENAFSFXyq4qpAw8IARUAAIhCcAHAAQbQAQE=&rs=AOn4CLCRBlyIUBUm_aypWz4tGkrDNJxIZw",
@ -81,7 +134,7 @@ Channel(
id: "UCh8gHdtzO2tXd593_bjErWg",
name: "Doobydobap",
avatar: [],
verification: verified,
verification: Verified,
subscriber_count: Some(2840000),
)),
publish_date: "[date]",
@ -95,7 +148,7 @@ Channel(
VideoItem(
id: "SRrvxFc2b2c",
name: "i don\'t believe in long distance relationships",
duration: None,
length: None,
thumbnail: [
Thumbnail(
url: "https://i.ytimg.com/vi/SRrvxFc2b2c/hq720_2.jpg?sqp=-oaymwEdCJUDENAFSFXyq4qpAw8IARUAAIhCcAHAAQbQAQE=&rs=AOn4CLA0hJdOfUp-zMI-vW43sYnKgufocA",
@ -107,7 +160,7 @@ Channel(
id: "UCh8gHdtzO2tXd593_bjErWg",
name: "Doobydobap",
avatar: [],
verification: verified,
verification: Verified,
subscriber_count: Some(2840000),
)),
publish_date: "[date]",
@ -121,7 +174,7 @@ Channel(
VideoItem(
id: "l9TiwunjzgA",
name: "long distance",
duration: Some(1043),
length: Some(1043),
thumbnail: [
Thumbnail(
url: "https://i.ytimg.com/vi/l9TiwunjzgA/hqdefault.jpg?sqp=-oaymwEbCKgBEF5IVfKriqkDDggBFQAAiEIYAXABwAEG&rs=AOn4CLDjM6SZ7ScyfFRr13QdVmIvWEWWrQ",
@ -148,7 +201,7 @@ Channel(
id: "UCh8gHdtzO2tXd593_bjErWg",
name: "Doobydobap",
avatar: [],
verification: verified,
verification: Verified,
subscriber_count: Some(2840000),
)),
publish_date: "[date]",
@ -162,7 +215,7 @@ Channel(
VideoItem(
id: "cNx0ql9gnf4",
name: "come over :)",
duration: None,
length: None,
thumbnail: [
Thumbnail(
url: "https://i.ytimg.com/vi/cNx0ql9gnf4/hq720_2.jpg?sqp=-oaymwEdCJUDENAFSFXyq4qpAw8IARUAAIhCcAHAAQbQAQE=&rs=AOn4CLBvAKRZE2LyKIo6_6prX9pzfiWoVw",
@ -174,7 +227,7 @@ Channel(
id: "UCh8gHdtzO2tXd593_bjErWg",
name: "Doobydobap",
avatar: [],
verification: verified,
verification: Verified,
subscriber_count: Some(2840000),
)),
publish_date: "[date]",
@ -188,7 +241,7 @@ Channel(
VideoItem(
id: "fGQUWI4o__A",
name: "Baskin Robbins in South Korea",
duration: None,
length: None,
thumbnail: [
Thumbnail(
url: "https://i.ytimg.com/vi/fGQUWI4o__A/hq720_2.jpg?sqp=-oaymwEdCJUDENAFSFXyq4qpAw8IARUAAIhCcAHAAQbQAQE=&rs=AOn4CLDyPuI762qzLAZM0QikxjFKVpoF9w",
@ -200,7 +253,7 @@ Channel(
id: "UCh8gHdtzO2tXd593_bjErWg",
name: "Doobydobap",
avatar: [],
verification: verified,
verification: Verified,
subscriber_count: Some(2840000),
)),
publish_date: "[date]",
@ -214,7 +267,7 @@ Channel(
VideoItem(
id: "Q73VTjdqVA8",
name: "dry hot pot",
duration: None,
length: None,
thumbnail: [
Thumbnail(
url: "https://i.ytimg.com/vi/Q73VTjdqVA8/hq720_2.jpg?sqp=-oaymwEdCJUDENAFSFXyq4qpAw8IARUAAIhCcAHAAQbQAQE=&rs=AOn4CLBfJXtFWfAnyMOvaJfvpYJ5WrhbSA",
@ -226,7 +279,7 @@ Channel(
id: "UCh8gHdtzO2tXd593_bjErWg",
name: "Doobydobap",
avatar: [],
verification: verified,
verification: Verified,
subscriber_count: Some(2840000),
)),
publish_date: "[date]",
@ -240,7 +293,7 @@ Channel(
VideoItem(
id: "pRVSdUxdsVw",
name: "Repairing...",
duration: Some(965),
length: Some(965),
thumbnail: [
Thumbnail(
url: "https://i.ytimg.com/vi/pRVSdUxdsVw/hqdefault.jpg?sqp=-oaymwEbCKgBEF5IVfKriqkDDggBFQAAiEIYAXABwAEG&rs=AOn4CLAQWneuYcJcccgooBfa3WI4LdYF3w",
@ -267,7 +320,7 @@ Channel(
id: "UCh8gHdtzO2tXd593_bjErWg",
name: "Doobydobap",
avatar: [],
verification: verified,
verification: Verified,
subscriber_count: Some(2840000),
)),
publish_date: "[date]",
@ -281,7 +334,7 @@ Channel(
VideoItem(
id: "gTG2WDbiYGo",
name: "time machine",
duration: None,
length: None,
thumbnail: [
Thumbnail(
url: "https://i.ytimg.com/vi/gTG2WDbiYGo/hq720_2.jpg?sqp=-oaymwEdCJUDENAFSFXyq4qpAw8IARUAAIhCcAHAAQbQAQE=&rs=AOn4CLDw5Lw19mNLJnoIF3aCGkMbxvgILQ",
@ -293,7 +346,7 @@ Channel(
id: "UCh8gHdtzO2tXd593_bjErWg",
name: "Doobydobap",
avatar: [],
verification: verified,
verification: Verified,
subscriber_count: Some(2840000),
)),
publish_date: "[date]",
@ -307,7 +360,7 @@ Channel(
VideoItem(
id: "y5JK5YFp92g",
name: "tiramissu",
duration: None,
length: None,
thumbnail: [
Thumbnail(
url: "https://i.ytimg.com/vi/y5JK5YFp92g/hq720_2.jpg?sqp=-oaymwEdCJUDENAFSFXyq4qpAw8IARUAAIhCcAHAAQbQAQE=&rs=AOn4CLCR66ytQIBWWw_ajvgyaUdUawHVIg",
@ -319,7 +372,7 @@ Channel(
id: "UCh8gHdtzO2tXd593_bjErWg",
name: "Doobydobap",
avatar: [],
verification: verified,
verification: Verified,
subscriber_count: Some(2840000),
)),
publish_date: "[date]",
@ -333,7 +386,7 @@ Channel(
VideoItem(
id: "pvSWHm4wlxY",
name: "having kids",
duration: None,
length: None,
thumbnail: [
Thumbnail(
url: "https://i.ytimg.com/vi/pvSWHm4wlxY/hq720_2.jpg?sqp=-oaymwEdCJUDENAFSFXyq4qpAw8IARUAAIhCcAHAAQbQAQE=&rs=AOn4CLDt7ZAwQoObfa5A7gC_hJnU1WH4Ug",
@ -345,7 +398,7 @@ Channel(
id: "UCh8gHdtzO2tXd593_bjErWg",
name: "Doobydobap",
avatar: [],
verification: verified,
verification: Verified,
subscriber_count: Some(2840000),
)),
publish_date: "[date]",
@ -359,7 +412,7 @@ Channel(
VideoItem(
id: "2FJVhdOO0F0",
name: "a health scare",
duration: Some(1238),
length: Some(1238),
thumbnail: [
Thumbnail(
url: "https://i.ytimg.com/vi/2FJVhdOO0F0/hqdefault.jpg?sqp=-oaymwEbCKgBEF5IVfKriqkDDggBFQAAiEIYAXABwAEG&rs=AOn4CLA5ambaz-euRsB9VG5ANaYFUUSEbg",
@ -386,7 +439,7 @@ Channel(
id: "UCh8gHdtzO2tXd593_bjErWg",
name: "Doobydobap",
avatar: [],
verification: verified,
verification: Verified,
subscriber_count: Some(2840000),
)),
publish_date: "[date]",
@ -400,7 +453,7 @@ Channel(
VideoItem(
id: "CqFGACRrWJE",
name: "just do it",
duration: None,
length: None,
thumbnail: [
Thumbnail(
url: "https://i.ytimg.com/vi/CqFGACRrWJE/hq720_2.jpg?sqp=-oaymwEdCJUDENAFSFXyq4qpAw8IARUAAIhCcAHAAQbQAQE=&rs=AOn4CLDyAIF4S_foRXsyvq16YCPJWNKewQ",
@ -412,7 +465,7 @@ Channel(
id: "UCh8gHdtzO2tXd593_bjErWg",
name: "Doobydobap",
avatar: [],
verification: verified,
verification: Verified,
subscriber_count: Some(2840000),
)),
publish_date: "[date]",
@ -426,7 +479,7 @@ Channel(
VideoItem(
id: "CutR_1SDDzY",
name: "feels good to be back",
duration: Some(1159),
length: Some(1159),
thumbnail: [
Thumbnail(
url: "https://i.ytimg.com/vi/CutR_1SDDzY/hqdefault.jpg?sqp=-oaymwEbCKgBEF5IVfKriqkDDggBFQAAiEIYAXABwAEG&rs=AOn4CLAt413Uk4xhHjYwpLI5-DXuOsFouA",
@ -453,7 +506,7 @@ Channel(
id: "UCh8gHdtzO2tXd593_bjErWg",
name: "Doobydobap",
avatar: [],
verification: verified,
verification: Verified,
subscriber_count: Some(2840000),
)),
publish_date: "[date]",
@ -467,7 +520,7 @@ Channel(
VideoItem(
id: "DdGr6t2NqKc",
name: "coming soon",
duration: None,
length: None,
thumbnail: [
Thumbnail(
url: "https://i.ytimg.com/vi/DdGr6t2NqKc/hq720_2.jpg?sqp=-oaymwEdCJUDENAFSFXyq4qpAw8IARUAAIhCcAHAAQbQAQE=&rs=AOn4CLDRYfxh25EjK3zuOJORNNahxeBanA",
@ -479,7 +532,7 @@ Channel(
id: "UCh8gHdtzO2tXd593_bjErWg",
name: "Doobydobap",
avatar: [],
verification: verified,
verification: Verified,
subscriber_count: Some(2840000),
)),
publish_date: "[date]",
@ -493,7 +546,7 @@ Channel(
VideoItem(
id: "jKS44NMWuXw",
name: "adult money",
duration: None,
length: None,
thumbnail: [
Thumbnail(
url: "https://i.ytimg.com/vi/jKS44NMWuXw/hq720_2.jpg?sqp=-oaymwEdCJUDENAFSFXyq4qpAw8IARUAAIhCcAHAAQbQAQE=&rs=AOn4CLAIexckdN7FXJUgkeJvITHyzXw1TQ",
@ -505,7 +558,7 @@ Channel(
id: "UCh8gHdtzO2tXd593_bjErWg",
name: "Doobydobap",
avatar: [],
verification: verified,
verification: Verified,
subscriber_count: Some(2840000),
)),
publish_date: "[date]",
@ -519,7 +572,7 @@ Channel(
VideoItem(
id: "kx1YtJM_vbI",
name: "a fig\'s journey",
duration: None,
length: None,
thumbnail: [
Thumbnail(
url: "https://i.ytimg.com/vi/kx1YtJM_vbI/hq720_2.jpg?sqp=-oaymwEdCJUDENAFSFXyq4qpAw8IARUAAIhCcAHAAQbQAQE=&rs=AOn4CLAi03nhSbt84LL7PFD2ij8GmaDlLQ",
@ -531,7 +584,7 @@ Channel(
id: "UCh8gHdtzO2tXd593_bjErWg",
name: "Doobydobap",
avatar: [],
verification: verified,
verification: Verified,
subscriber_count: Some(2840000),
)),
publish_date: "[date]",
@ -545,7 +598,7 @@ Channel(
VideoItem(
id: "Sdbzs-1WWH0",
name: "How to.. Mozzarella 🧀",
duration: None,
length: None,
thumbnail: [
Thumbnail(
url: "https://i.ytimg.com/vi/Sdbzs-1WWH0/hq720_2.jpg?sqp=-oaymwEdCJUDENAFSFXyq4qpAw8IARUAAIhCcAHAAQbQAQE=&rs=AOn4CLC8IkwAif4wXhBGxHiosiILbPCSBw",
@ -557,7 +610,7 @@ Channel(
id: "UCh8gHdtzO2tXd593_bjErWg",
name: "Doobydobap",
avatar: [],
verification: verified,
verification: Verified,
subscriber_count: Some(2840000),
)),
publish_date: "[date]",
@ -571,7 +624,7 @@ Channel(
VideoItem(
id: "9qBHyJIDous",
name: "how to drink like a real korean",
duration: None,
length: None,
thumbnail: [
Thumbnail(
url: "https://i.ytimg.com/vi/9qBHyJIDous/hq720_2.jpg?sqp=-oaymwEdCJUDENAFSFXyq4qpAw8IARUAAIhCcAHAAQbQAQE=&rs=AOn4CLB9Ib_E0siDiRMZ_GVHVxBfMd0Dkw",
@ -583,7 +636,7 @@ Channel(
id: "UCh8gHdtzO2tXd593_bjErWg",
name: "Doobydobap",
avatar: [],
verification: verified,
verification: Verified,
subscriber_count: Some(2840000),
)),
publish_date: "[date]",
@ -597,7 +650,7 @@ Channel(
VideoItem(
id: "mBeFDb4gp8s",
name: "mr. krabs soup",
duration: None,
length: None,
thumbnail: [
Thumbnail(
url: "https://i.ytimg.com/vi/mBeFDb4gp8s/hq720_2.jpg?sqp=-oaymwEdCJUDENAFSFXyq4qpAw8IARUAAIhCcAHAAQbQAQE=&rs=AOn4CLCzAPzv16WTJLr4ma-sAz6fNkFL0g",
@ -609,7 +662,7 @@ Channel(
id: "UCh8gHdtzO2tXd593_bjErWg",
name: "Doobydobap",
avatar: [],
verification: verified,
verification: Verified,
subscriber_count: Some(2840000),
)),
publish_date: "[date]",
@ -623,7 +676,7 @@ Channel(
VideoItem(
id: "b38r1UYqoBQ",
name: "in five years",
duration: None,
length: None,
thumbnail: [
Thumbnail(
url: "https://i.ytimg.com/vi/b38r1UYqoBQ/hq720_2.jpg?sqp=-oaymwEdCJUDENAFSFXyq4qpAw8IARUAAIhCcAHAAQbQAQE=&rs=AOn4CLCGB9IpC2Enx5iZ-YCl0vEpMGpo9A",
@ -635,7 +688,7 @@ Channel(
id: "UCh8gHdtzO2tXd593_bjErWg",
name: "Doobydobap",
avatar: [],
verification: verified,
verification: Verified,
subscriber_count: Some(2840000),
)),
publish_date: "[date]",
@ -649,7 +702,7 @@ Channel(
VideoItem(
id: "KUz7oArksR4",
name: "running away",
duration: Some(1023),
length: Some(1023),
thumbnail: [
Thumbnail(
url: "https://i.ytimg.com/vi/KUz7oArksR4/hqdefault.jpg?sqp=-oaymwEbCKgBEF5IVfKriqkDDggBFQAAiEIYAXABwAEG&rs=AOn4CLD1NwuIgJuJy2oPAiHqMre6rbcuPA",
@ -676,7 +729,7 @@ Channel(
id: "UCh8gHdtzO2tXd593_bjErWg",
name: "Doobydobap",
avatar: [],
verification: verified,
verification: Verified,
subscriber_count: Some(2840000),
)),
publish_date: "[date]",
@ -690,7 +743,7 @@ Channel(
VideoItem(
id: "RdFk4WaifEo",
name: "a weeknight dinner",
duration: None,
length: None,
thumbnail: [
Thumbnail(
url: "https://i.ytimg.com/vi/RdFk4WaifEo/hq720_2.jpg?sqp=-oaymwEdCJUDENAFSFXyq4qpAw8IARUAAIhCcAHAAQbQAQE=&rs=AOn4CLBlKLBjBagaTQj24nYb-HkCQQcWHA",
@ -702,7 +755,7 @@ Channel(
id: "UCh8gHdtzO2tXd593_bjErWg",
name: "Doobydobap",
avatar: [],
verification: verified,
verification: Verified,
subscriber_count: Some(2840000),
)),
publish_date: "[date]",
@ -716,7 +769,7 @@ Channel(
VideoItem(
id: "GuyGyzZcumI",
name: "McDonald\'s Michelin Burger",
duration: None,
length: None,
thumbnail: [
Thumbnail(
url: "https://i.ytimg.com/vi/GuyGyzZcumI/hq720_2.jpg?sqp=-oaymwEdCJUDENAFSFXyq4qpAw8IARUAAIhCcAHAAQbQAQE=&rs=AOn4CLDtmyilZAgMw8VWNy518etIKi4phA",
@ -728,7 +781,7 @@ Channel(
id: "UCh8gHdtzO2tXd593_bjErWg",
name: "Doobydobap",
avatar: [],
verification: verified,
verification: Verified,
subscriber_count: Some(2840000),
)),
publish_date: "[date]",
@ -742,7 +795,7 @@ Channel(
VideoItem(
id: "07Zipsb3-qU",
name: "cwispy potato pancake",
duration: None,
length: None,
thumbnail: [
Thumbnail(
url: "https://i.ytimg.com/vi/07Zipsb3-qU/hq720_2.jpg?sqp=-oaymwEdCJYDENAFSFXyq4qpAw8IARUAAIhCcAHAAQbQAQE=&rs=AOn4CLARXBTZlNStCVemXSkHfAWksRogng",
@ -754,7 +807,7 @@ Channel(
id: "UCh8gHdtzO2tXd593_bjErWg",
name: "Doobydobap",
avatar: [],
verification: verified,
verification: Verified,
subscriber_count: Some(2840000),
)),
publish_date: "[date]",
@ -768,7 +821,7 @@ Channel(
VideoItem(
id: "3kaePnU6Clo",
name: "authenticity is overrated",
duration: None,
length: None,
thumbnail: [
Thumbnail(
url: "https://i.ytimg.com/vi/3kaePnU6Clo/hq720_2.jpg?sqp=-oaymwEdCJYDENAFSFXyq4qpAw8IARUAAIhCcAHAAQbQAQE=&rs=AOn4CLDq0MY9dsMvr9Y6yaJ7069fgtdpGA",
@ -780,7 +833,7 @@ Channel(
id: "UCh8gHdtzO2tXd593_bjErWg",
name: "Doobydobap",
avatar: [],
verification: verified,
verification: Verified,
subscriber_count: Some(2840000),
)),
publish_date: "[date]",
@ -794,7 +847,7 @@ Channel(
VideoItem(
id: "rt4rXMftnpg",
name: "you can kimchi anything (T&C applies)",
duration: None,
length: None,
thumbnail: [
Thumbnail(
url: "https://i.ytimg.com/vi/rt4rXMftnpg/hq720_2.jpg?sqp=-oaymwEdCJUDENAFSFXyq4qpAw8IARUAAIhCcAHAAQbQAQE=&rs=AOn4CLC7WfSTGHkH2FEmn9gQ5E4AqpRtug",
@ -806,7 +859,7 @@ Channel(
id: "UCh8gHdtzO2tXd593_bjErWg",
name: "Doobydobap",
avatar: [],
verification: verified,
verification: Verified,
subscriber_count: Some(2840000),
)),
publish_date: "[date]",
@ -820,7 +873,7 @@ Channel(
VideoItem(
id: "DTyLUvbf128",
name: "egg, soy, and perfect pot rice",
duration: None,
length: None,
thumbnail: [
Thumbnail(
url: "https://i.ytimg.com/vi/DTyLUvbf128/hq720_2.jpg?sqp=-oaymwEdCJYDENAFSFXyq4qpAw8IARUAAIhCcAHAAQbQAQE=&rs=AOn4CLAN1AtPya1D1NyiO0XYKOjIZIyhhQ",
@ -832,7 +885,7 @@ Channel(
id: "UCh8gHdtzO2tXd593_bjErWg",
name: "Doobydobap",
avatar: [],
verification: verified,
verification: Verified,
subscriber_count: Some(2840000),
)),
publish_date: "[date]",
@ -846,7 +899,7 @@ Channel(
VideoItem(
id: "DzjLBgIe_aI",
name: "love language",
duration: None,
length: None,
thumbnail: [
Thumbnail(
url: "https://i.ytimg.com/vi/DzjLBgIe_aI/hq720_2.jpg?sqp=-oaymwEdCJYDENAFSFXyq4qpAw8IARUAAIhCcAHAAQbQAQE=&rs=AOn4CLDWVkrYrt64LvvxrMRfs29g_lGrNw",
@ -858,7 +911,7 @@ Channel(
id: "UCh8gHdtzO2tXd593_bjErWg",
name: "Doobydobap",
avatar: [],
verification: verified,
verification: Verified,
subscriber_count: Some(2840000),
)),
publish_date: "[date]",
@ -872,7 +925,7 @@ Channel(
VideoItem(
id: "sPb2gyN-hnE",
name: "worth fighting for",
duration: Some(1232),
length: Some(1232),
thumbnail: [
Thumbnail(
url: "https://i.ytimg.com/vi/sPb2gyN-hnE/hqdefault.jpg?sqp=-oaymwEbCKgBEF5IVfKriqkDDggBFQAAiEIYAXABwAEG&rs=AOn4CLBidXnS47SJMkvOlqt2DgzHxr6wKQ",
@ -899,7 +952,7 @@ Channel(
id: "UCh8gHdtzO2tXd593_bjErWg",
name: "Doobydobap",
avatar: [],
verification: verified,
verification: Verified,
subscriber_count: Some(2840000),
)),
publish_date: "[date]",
@ -913,7 +966,7 @@ Channel(
VideoItem(
id: "9JboRKeJ2m4",
name: "Rating Italian McDonald\'s",
duration: None,
length: None,
thumbnail: [
Thumbnail(
url: "https://i.ytimg.com/vi/9JboRKeJ2m4/hq720_2.jpg?sqp=-oaymwEdCJYDENAFSFXyq4qpAw8IARUAAIhCcAHAAQbQAQE=&rs=AOn4CLC7xktrbnAqJq2nHH9aDggULsb3Cg",
@ -925,7 +978,7 @@ Channel(
id: "UCh8gHdtzO2tXd593_bjErWg",
name: "Doobydobap",
avatar: [],
verification: verified,
verification: Verified,
subscriber_count: Some(2840000),
)),
publish_date: "[date]",

View file

@ -5,9 +5,7 @@ expression: map_res.c
Channel(
id: "UCcvfHa-GHSOHFAjU0-Ie57A",
name: "Adam Something",
handle: None,
subscriber_count: Some(947000),
video_count: None,
avatar: [
Thumbnail(
url: "https://yt3.ggpht.com/FzV47fzr2nc8_KOeUO2FSIH-daaxCZaPDGqrgC1_Qp0_zEn0DnKmi7PiMwcssTG4IEDL1XfdTIk=s48-c-k-c0x00ffffff-no-rj",
@ -25,7 +23,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",
@ -45,6 +43,7 @@ Channel(
"budapest",
"eu",
],
vanity_url: Some("https://www.youtube.com/c/AdamSomething"),
banner: [
Thumbnail(
url: "https://yt3.ggpht.com/Bk54VHh5FsxlwAAEltJp6rgx3VzBgxbi8naNngh5C4zQni1ijUhgTmVmDrE_I9M95SxtXTnd=w1060-fcrop64=1,00005a57ffffa5a8-k-c0xffffffff-no-nd-rj",
@ -77,6 +76,60 @@ Channel(
height: 424,
),
],
mobile_banner: [
Thumbnail(
url: "https://yt3.ggpht.com/Bk54VHh5FsxlwAAEltJp6rgx3VzBgxbi8naNngh5C4zQni1ijUhgTmVmDrE_I9M95SxtXTnd=w320-fcrop64=1,32b75a57cd48a5a8-k-c0xffffffff-no-nd-rj",
width: 320,
height: 88,
),
Thumbnail(
url: "https://yt3.ggpht.com/Bk54VHh5FsxlwAAEltJp6rgx3VzBgxbi8naNngh5C4zQni1ijUhgTmVmDrE_I9M95SxtXTnd=w640-fcrop64=1,32b75a57cd48a5a8-k-c0xffffffff-no-nd-rj",
width: 640,
height: 175,
),
Thumbnail(
url: "https://yt3.ggpht.com/Bk54VHh5FsxlwAAEltJp6rgx3VzBgxbi8naNngh5C4zQni1ijUhgTmVmDrE_I9M95SxtXTnd=w960-fcrop64=1,32b75a57cd48a5a8-k-c0xffffffff-no-nd-rj",
width: 960,
height: 263,
),
Thumbnail(
url: "https://yt3.ggpht.com/Bk54VHh5FsxlwAAEltJp6rgx3VzBgxbi8naNngh5C4zQni1ijUhgTmVmDrE_I9M95SxtXTnd=w1280-fcrop64=1,32b75a57cd48a5a8-k-c0xffffffff-no-nd-rj",
width: 1280,
height: 351,
),
Thumbnail(
url: "https://yt3.ggpht.com/Bk54VHh5FsxlwAAEltJp6rgx3VzBgxbi8naNngh5C4zQni1ijUhgTmVmDrE_I9M95SxtXTnd=w1440-fcrop64=1,32b75a57cd48a5a8-k-c0xffffffff-no-nd-rj",
width: 1440,
height: 395,
),
],
tv_banner: [
Thumbnail(
url: "https://yt3.ggpht.com/Bk54VHh5FsxlwAAEltJp6rgx3VzBgxbi8naNngh5C4zQni1ijUhgTmVmDrE_I9M95SxtXTnd=w320-fcrop64=1,00000000ffffffff-k-c0xffffffff-no-nd-rj",
width: 320,
height: 180,
),
Thumbnail(
url: "https://yt3.ggpht.com/Bk54VHh5FsxlwAAEltJp6rgx3VzBgxbi8naNngh5C4zQni1ijUhgTmVmDrE_I9M95SxtXTnd=w854-fcrop64=1,00000000ffffffff-k-c0xffffffff-no-nd-rj",
width: 854,
height: 480,
),
Thumbnail(
url: "https://yt3.ggpht.com/Bk54VHh5FsxlwAAEltJp6rgx3VzBgxbi8naNngh5C4zQni1ijUhgTmVmDrE_I9M95SxtXTnd=w1280-fcrop64=1,00000000ffffffff-k-c0xffffffff-no-nd-rj",
width: 1280,
height: 720,
),
Thumbnail(
url: "https://yt3.ggpht.com/Bk54VHh5FsxlwAAEltJp6rgx3VzBgxbi8naNngh5C4zQni1ijUhgTmVmDrE_I9M95SxtXTnd=w1920-fcrop64=1,00000000ffffffff-k-c0xffffffff-no-nd-rj",
width: 1920,
height: 1080,
),
Thumbnail(
url: "https://yt3.ggpht.com/Bk54VHh5FsxlwAAEltJp6rgx3VzBgxbi8naNngh5C4zQni1ijUhgTmVmDrE_I9M95SxtXTnd=w2120-fcrop64=1,00000000ffffffff-k-c0xffffffff-no-nd-rj",
width: 2120,
height: 1192,
),
],
has_shorts: false,
has_live: false,
visitor_data: Some("Cgs4Ri1tLW1KNWozNCjGk8yZBg%3D%3D"),
@ -86,7 +139,7 @@ Channel(
VideoItem(
id: "B-KjpyR4n5Q",
name: "The Online Manosphere",
duration: None,
length: None,
thumbnail: [
Thumbnail(
url: "https://i.ytimg.com/vi/B-KjpyR4n5Q/hqdefault.jpg?sqp=-oaymwEbCKgBEF5IVfKriqkDDggBFQAAiEIYAXABwAEG&rs=AOn4CLC_J9WgOnkXvtw1uUhZASVDLPlrZg",
@ -113,7 +166,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"),
@ -127,7 +180,7 @@ Channel(
VideoItem(
id: "umDsCyZ67J0",
name: "Ukraine - The Beginning of the End",
duration: Some(614),
length: Some(614),
thumbnail: [
Thumbnail(
url: "https://i.ytimg.com/vi/umDsCyZ67J0/hqdefault.jpg?sqp=-oaymwEbCKgBEF5IVfKriqkDDggBFQAAiEIYAXABwAEG&rs=AOn4CLBih3bLoQ9xphjCDt3lqXTLKtE52g",
@ -154,7 +207,7 @@ Channel(
id: "UCcvfHa-GHSOHFAjU0-Ie57A",
name: "Adam Something",
avatar: [],
verification: verified,
verification: Verified,
subscriber_count: Some(947000),
)),
publish_date: "[date]",
@ -168,7 +221,7 @@ Channel(
VideoItem(
id: "dNgKGL8lQck",
name: "Honest Russian Military Recruitment Video",
duration: Some(62),
length: Some(62),
thumbnail: [
Thumbnail(
url: "https://i.ytimg.com/vi/dNgKGL8lQck/hqdefault.jpg?sqp=-oaymwEbCKgBEF5IVfKriqkDDggBFQAAiEIYAXABwAEG&rs=AOn4CLDrbxxCEBDfZP2wA0bIJpzbtmyARw",
@ -195,7 +248,7 @@ Channel(
id: "UCcvfHa-GHSOHFAjU0-Ie57A",
name: "Adam Something",
avatar: [],
verification: verified,
verification: Verified,
subscriber_count: Some(947000),
)),
publish_date: "[date]",
@ -209,7 +262,7 @@ Channel(
VideoItem(
id: "UVWciFJeFNA",
name: "Self-Driving Cars Will Only Make Traffic Worse",
duration: Some(458),
length: Some(458),
thumbnail: [
Thumbnail(
url: "https://i.ytimg.com/vi/UVWciFJeFNA/hqdefault.jpg?sqp=-oaymwEbCKgBEF5IVfKriqkDDggBFQAAiEIYAXABwAEG&rs=AOn4CLDJhcm03VJaYQU5xAIg2w5h0SOaUA",
@ -236,7 +289,7 @@ Channel(
id: "UCcvfHa-GHSOHFAjU0-Ie57A",
name: "Adam Something",
avatar: [],
verification: verified,
verification: Verified,
subscriber_count: Some(947000),
)),
publish_date: "[date]",
@ -250,7 +303,7 @@ Channel(
VideoItem(
id: "vyWaax07_ks",
name: "NEOM Is The Parody Of The Future",
duration: Some(636),
length: Some(636),
thumbnail: [
Thumbnail(
url: "https://i.ytimg.com/vi/vyWaax07_ks/hqdefault.jpg?sqp=-oaymwEbCKgBEF5IVfKriqkDDggBFQAAiEIYAXABwAEG&rs=AOn4CLD-sXnmtClcL6lcjjAR_05F1IpndA",
@ -277,7 +330,7 @@ Channel(
id: "UCcvfHa-GHSOHFAjU0-Ie57A",
name: "Adam Something",
avatar: [],
verification: verified,
verification: Verified,
subscriber_count: Some(947000),
)),
publish_date: "[date]",
@ -291,7 +344,7 @@ Channel(
VideoItem(
id: "onQ0ICkLEJw",
name: "I Got An Email From \"The Dubai Sheikh\'s Personal Friend\"",
duration: Some(211),
length: Some(211),
thumbnail: [
Thumbnail(
url: "https://i.ytimg.com/vi/onQ0ICkLEJw/hqdefault.jpg?sqp=-oaymwEbCKgBEF5IVfKriqkDDggBFQAAiEIYAXABwAEG&rs=AOn4CLAmwCI9t6a_pXPPteQ835LNPgcYbw",
@ -318,7 +371,7 @@ Channel(
id: "UCcvfHa-GHSOHFAjU0-Ie57A",
name: "Adam Something",
avatar: [],
verification: verified,
verification: Verified,
subscriber_count: Some(947000),
)),
publish_date: "[date]",
@ -332,7 +385,7 @@ Channel(
VideoItem(
id: "yDEL1pTYOhs",
name: "The \"Meritocracy\" Isn\'t Real",
duration: Some(385),
length: Some(385),
thumbnail: [
Thumbnail(
url: "https://i.ytimg.com/vi/yDEL1pTYOhs/hqdefault.jpg?sqp=-oaymwEbCKgBEF5IVfKriqkDDggBFQAAiEIYAXABwAEG&rs=AOn4CLDmKg0HtcuQfJUsjVj_3WIUtOkZDg",
@ -359,7 +412,7 @@ Channel(
id: "UCcvfHa-GHSOHFAjU0-Ie57A",
name: "Adam Something",
avatar: [],
verification: verified,
verification: Verified,
subscriber_count: Some(947000),
)),
publish_date: "[date]",
@ -373,7 +426,7 @@ Channel(
VideoItem(
id: "EnVvlhhqWtw",
name: "City Review - Prague: Beautiful and Disappointing",
duration: Some(834),
length: Some(834),
thumbnail: [
Thumbnail(
url: "https://i.ytimg.com/vi/EnVvlhhqWtw/hqdefault.jpg?sqp=-oaymwEbCKgBEF5IVfKriqkDDggBFQAAiEIYAXABwAEG&rs=AOn4CLDFUovL8XJ7tUzOh_sdB1ymKJS4Qg",
@ -400,7 +453,7 @@ Channel(
id: "UCcvfHa-GHSOHFAjU0-Ie57A",
name: "Adam Something",
avatar: [],
verification: verified,
verification: Verified,
subscriber_count: Some(947000),
)),
publish_date: "[date]",
@ -414,7 +467,7 @@ Channel(
VideoItem(
id: "Oxz4oY0T85Y",
name: "European International Rail SUCKS, Here\'s Why",
duration: Some(810),
length: Some(810),
thumbnail: [
Thumbnail(
url: "https://i.ytimg.com/vi/Oxz4oY0T85Y/hqdefault.jpg?sqp=-oaymwEbCKgBEF5IVfKriqkDDggBFQAAiEIYAXABwAEG&rs=AOn4CLAdlPQSSzVGixQsH_uXsd1VVsfMcQ",
@ -441,7 +494,7 @@ Channel(
id: "UCcvfHa-GHSOHFAjU0-Ie57A",
name: "Adam Something",
avatar: [],
verification: verified,
verification: Verified,
subscriber_count: Some(947000),
)),
publish_date: "[date]",
@ -455,7 +508,7 @@ Channel(
VideoItem(
id: "lxUEuOkblws",
name: "Why the Straddling Bus Failed",
duration: Some(614),
length: Some(614),
thumbnail: [
Thumbnail(
url: "https://i.ytimg.com/vi/lxUEuOkblws/hqdefault.jpg?sqp=-oaymwEbCKgBEF5IVfKriqkDDggBFQAAiEIYAXABwAEG&rs=AOn4CLAaUchMwc5d_yNfH9BM0VlexxjPtQ",
@ -482,7 +535,7 @@ Channel(
id: "UCcvfHa-GHSOHFAjU0-Ie57A",
name: "Adam Something",
avatar: [],
verification: verified,
verification: Verified,
subscriber_count: Some(947000),
)),
publish_date: "[date]",
@ -496,7 +549,7 @@ Channel(
VideoItem(
id: "UG8jiKOtedk",
name: "How Canadian Ukrainian Volunteer Got Exposed",
duration: Some(538),
length: Some(538),
thumbnail: [
Thumbnail(
url: "https://i.ytimg.com/vi/UG8jiKOtedk/hqdefault.jpg?sqp=-oaymwEbCKgBEF5IVfKriqkDDggBFQAAiEIYAXABwAEG&rs=AOn4CLB7OMkifWaF0tbm5qu0IZmxuK7AtA",
@ -523,7 +576,7 @@ Channel(
id: "UCcvfHa-GHSOHFAjU0-Ie57A",
name: "Adam Something",
avatar: [],
verification: verified,
verification: Verified,
subscriber_count: Some(947000),
)),
publish_date: "[date]",
@ -537,7 +590,7 @@ Channel(
VideoItem(
id: "bQld7iJJSyk",
name: "Why Roads ALWAYS Fill Up, No Matter How Much We Widen Them",
duration: Some(159),
length: Some(159),
thumbnail: [
Thumbnail(
url: "https://i.ytimg.com/vi/bQld7iJJSyk/hqdefault.jpg?sqp=-oaymwEbCKgBEF5IVfKriqkDDggBFQAAiEIYAXABwAEG&rs=AOn4CLA-9aUtPGBCwAO2wl2JG5JnwWh-iA",
@ -564,7 +617,7 @@ Channel(
id: "UCcvfHa-GHSOHFAjU0-Ie57A",
name: "Adam Something",
avatar: [],
verification: verified,
verification: Verified,
subscriber_count: Some(947000),
)),
publish_date: "[date]",
@ -578,7 +631,7 @@ Channel(
VideoItem(
id: "WUK0K5mdQ_s",
name: "Egypt\'s New Capital is an Ozymandian Nightmare",
duration: Some(870),
length: Some(870),
thumbnail: [
Thumbnail(
url: "https://i.ytimg.com/vi/WUK0K5mdQ_s/hqdefault.jpg?sqp=-oaymwEbCKgBEF5IVfKriqkDDggBFQAAiEIYAXABwAEG&rs=AOn4CLCViMWL1-G3s7PBgMgo1mVdFSx9Rw",
@ -605,7 +658,7 @@ Channel(
id: "UCcvfHa-GHSOHFAjU0-Ie57A",
name: "Adam Something",
avatar: [],
verification: verified,
verification: Verified,
subscriber_count: Some(947000),
)),
publish_date: "[date]",
@ -619,7 +672,7 @@ Channel(
VideoItem(
id: "LB-vsT1Sl68",
name: "Why Car-Centric Cities are a GREAT Idea",
duration: Some(369),
length: Some(369),
thumbnail: [
Thumbnail(
url: "https://i.ytimg.com/vi/LB-vsT1Sl68/hqdefault.jpg?sqp=-oaymwEbCKgBEF5IVfKriqkDDggBFQAAiEIYAXABwAEG&rs=AOn4CLDfLhC6VYMirWX_lL0eXhZCpAYabA",
@ -646,7 +699,7 @@ Channel(
id: "UCcvfHa-GHSOHFAjU0-Ie57A",
name: "Adam Something",
avatar: [],
verification: verified,
verification: Verified,
subscriber_count: Some(947000),
)),
publish_date: "[date]",
@ -660,7 +713,7 @@ Channel(
VideoItem(
id: "p8NiM_p8n5A",
name: "HE FIXED TRAFFIC",
duration: Some(157),
length: Some(157),
thumbnail: [
Thumbnail(
url: "https://i.ytimg.com/vi/p8NiM_p8n5A/hqdefault.jpg?sqp=-oaymwEbCKgBEF5IVfKriqkDDggBFQAAiEIYAXABwAEG&rs=AOn4CLDTl0EgGk8_v6gPMJY0IF5tUvDGAg",
@ -687,7 +740,7 @@ Channel(
id: "UCcvfHa-GHSOHFAjU0-Ie57A",
name: "Adam Something",
avatar: [],
verification: verified,
verification: Verified,
subscriber_count: Some(947000),
)),
publish_date: "[date]",
@ -701,7 +754,7 @@ Channel(
VideoItem(
id: "U9YdnzOf4NQ",
name: "Why a Mars Colony is a Stupid and Dangerous Idea",
duration: Some(1000),
length: Some(1000),
thumbnail: [
Thumbnail(
url: "https://i.ytimg.com/vi/U9YdnzOf4NQ/hqdefault.jpg?sqp=-oaymwEbCKgBEF5IVfKriqkDDggBFQAAiEIYAXABwAEG&rs=AOn4CLARNVQBbhiSGasL6fMQUU1ITgHuDQ",
@ -728,7 +781,7 @@ Channel(
id: "UCcvfHa-GHSOHFAjU0-Ie57A",
name: "Adam Something",
avatar: [],
verification: verified,
verification: Verified,
subscriber_count: Some(947000),
)),
publish_date: "[date]",
@ -742,7 +795,7 @@ Channel(
VideoItem(
id: "CH55WpJxF1s",
name: "What #Elongate Is Really About",
duration: Some(122),
length: Some(122),
thumbnail: [
Thumbnail(
url: "https://i.ytimg.com/vi/CH55WpJxF1s/hqdefault.jpg?sqp=-oaymwEbCKgBEF5IVfKriqkDDggBFQAAiEIYAXABwAEG&rs=AOn4CLACITT6pFDi4KlXA0E6lLB5FHVxbQ",
@ -769,7 +822,7 @@ Channel(
id: "UCcvfHa-GHSOHFAjU0-Ie57A",
name: "Adam Something",
avatar: [],
verification: verified,
verification: Verified,
subscriber_count: Some(947000),
)),
publish_date: "[date]",
@ -783,7 +836,7 @@ Channel(
VideoItem(
id: "PPcsZwUv350",
name: "Vladimir Putin\'s Three Choices",
duration: Some(505),
length: Some(505),
thumbnail: [
Thumbnail(
url: "https://i.ytimg.com/vi/PPcsZwUv350/hqdefault.jpg?sqp=-oaymwEbCKgBEF5IVfKriqkDDggBFQAAiEIYAXABwAEG&rs=AOn4CLBnYUqduIFS2zR6BQwWIdWH0eyIng",
@ -810,7 +863,7 @@ Channel(
id: "UCcvfHa-GHSOHFAjU0-Ie57A",
name: "Adam Something",
avatar: [],
verification: verified,
verification: Verified,
subscriber_count: Some(947000),
)),
publish_date: "[date]",
@ -824,7 +877,7 @@ Channel(
VideoItem(
id: "B78-FgNqdc8",
name: "Was I WRONG About Electric Buses?",
duration: Some(1536),
length: Some(1536),
thumbnail: [
Thumbnail(
url: "https://i.ytimg.com/vi/B78-FgNqdc8/hqdefault.jpg?sqp=-oaymwEbCKgBEF5IVfKriqkDDggBFQAAiEIYAXABwAEG&rs=AOn4CLD4r2bdbKCxbgvGoKGauCaZDBp3mw",
@ -851,7 +904,7 @@ Channel(
id: "UCcvfHa-GHSOHFAjU0-Ie57A",
name: "Adam Something",
avatar: [],
verification: verified,
verification: Verified,
subscriber_count: Some(947000),
)),
publish_date: "[date]",
@ -865,7 +918,7 @@ Channel(
VideoItem(
id: "JCXLwOMSDxk",
name: "If We Treated Afghanistan Like Ukraine",
duration: Some(92),
length: Some(92),
thumbnail: [
Thumbnail(
url: "https://i.ytimg.com/vi/JCXLwOMSDxk/hqdefault.jpg?sqp=-oaymwEbCKgBEF5IVfKriqkDDggBFQAAiEIYAXABwAEG&rs=AOn4CLBhu3_HO7U63sl-DBLhRbDllmFoRA",
@ -892,7 +945,7 @@ Channel(
id: "UCcvfHa-GHSOHFAjU0-Ie57A",
name: "Adam Something",
avatar: [],
verification: verified,
verification: Verified,
subscriber_count: Some(947000),
)),
publish_date: "[date]",
@ -906,7 +959,7 @@ Channel(
VideoItem(
id: "IpIWswLYAbA",
name: "Who\'s Winning the War for Ukraine?",
duration: Some(646),
length: Some(646),
thumbnail: [
Thumbnail(
url: "https://i.ytimg.com/vi/IpIWswLYAbA/hqdefault.jpg?sqp=-oaymwEbCKgBEF5IVfKriqkDDggBFQAAiEIYAXABwAEG&rs=AOn4CLDpoUqVQJOt4bR1niy4QTCpbNo8cg",
@ -933,7 +986,7 @@ Channel(
id: "UCcvfHa-GHSOHFAjU0-Ie57A",
name: "Adam Something",
avatar: [],
verification: verified,
verification: Verified,
subscriber_count: Some(947000),
)),
publish_date: "[date]",
@ -947,7 +1000,7 @@ Channel(
VideoItem(
id: "NIItoD1Ebh0",
name: "Old Habits Die Hard",
duration: Some(107),
length: Some(107),
thumbnail: [
Thumbnail(
url: "https://i.ytimg.com/vi/NIItoD1Ebh0/hqdefault.jpg?sqp=-oaymwEbCKgBEF5IVfKriqkDDggBFQAAiEIYAXABwAEG&rs=AOn4CLBHqsrLzPqmebGr4w1j40V31wgRcQ",
@ -974,7 +1027,7 @@ Channel(
id: "UCcvfHa-GHSOHFAjU0-Ie57A",
name: "Adam Something",
avatar: [],
verification: verified,
verification: Verified,
subscriber_count: Some(947000),
)),
publish_date: "[date]",
@ -988,7 +1041,7 @@ Channel(
VideoItem(
id: "pENUV9DLa2g",
name: "Anarcho-Capitalism In Practice III - The Final Attempt",
duration: Some(600),
length: Some(600),
thumbnail: [
Thumbnail(
url: "https://i.ytimg.com/vi/pENUV9DLa2g/hqdefault.jpg?sqp=-oaymwEbCKgBEF5IVfKriqkDDggBFQAAiEIYAXABwAEG&rs=AOn4CLCmoujwlLKjddw_4xZGN0iY0-uO_g",
@ -1015,7 +1068,7 @@ Channel(
id: "UCcvfHa-GHSOHFAjU0-Ie57A",
name: "Adam Something",
avatar: [],
verification: verified,
verification: Verified,
subscriber_count: Some(947000),
)),
publish_date: "[date]",
@ -1029,7 +1082,7 @@ Channel(
VideoItem(
id: "gFGQI8P9BMg",
name: "How The Gravel Institute Lies To You About Ukraine",
duration: Some(2472),
length: Some(2472),
thumbnail: [
Thumbnail(
url: "https://i.ytimg.com/vi/gFGQI8P9BMg/hqdefault.jpg?sqp=-oaymwEbCKgBEF5IVfKriqkDDggBFQAAiEIYAXABwAEG&rs=AOn4CLBpJDqK5bFk3L2AuDsyN8SrCv4fKA",
@ -1056,7 +1109,7 @@ Channel(
id: "UCcvfHa-GHSOHFAjU0-Ie57A",
name: "Adam Something",
avatar: [],
verification: verified,
verification: Verified,
subscriber_count: Some(947000),
)),
publish_date: "[date]",
@ -1070,7 +1123,7 @@ Channel(
VideoItem(
id: "AVLevneWvaE",
name: "Why Russia Can\'t Achieve Air Supremacy In Ukraine",
duration: Some(188),
length: Some(188),
thumbnail: [
Thumbnail(
url: "https://i.ytimg.com/vi/AVLevneWvaE/hqdefault.jpg?sqp=-oaymwEbCKgBEF5IVfKriqkDDggBFQAAiEIYAXABwAEG&rs=AOn4CLAjh4bMN4kEdZqI94bBJlK60-6WWA",
@ -1097,7 +1150,7 @@ Channel(
id: "UCcvfHa-GHSOHFAjU0-Ie57A",
name: "Adam Something",
avatar: [],
verification: verified,
verification: Verified,
subscriber_count: Some(947000),
)),
publish_date: "[date]",
@ -1111,7 +1164,7 @@ Channel(
VideoItem(
id: "MfRcY90OccY",
name: "Can Ukraine Actually WIN This?",
duration: Some(606),
length: Some(606),
thumbnail: [
Thumbnail(
url: "https://i.ytimg.com/vi/MfRcY90OccY/hqdefault.jpg?sqp=-oaymwEbCKgBEF5IVfKriqkDDggBFQAAiEIYAXABwAEG&rs=AOn4CLCxpbaMlVdngsFBMi1pYqCTkhnk4g",
@ -1138,7 +1191,7 @@ Channel(
id: "UCcvfHa-GHSOHFAjU0-Ie57A",
name: "Adam Something",
avatar: [],
verification: verified,
verification: Verified,
subscriber_count: Some(947000),
)),
publish_date: "[date]",
@ -1152,7 +1205,7 @@ Channel(
VideoItem(
id: "dQXwreYzJ40",
name: "Here\'s What Will Happen To Ukraine [Update: yep, called it]",
duration: Some(397),
length: Some(397),
thumbnail: [
Thumbnail(
url: "https://i.ytimg.com/vi/dQXwreYzJ40/hqdefault.jpg?sqp=-oaymwEbCKgBEF5IVfKriqkDDggBFQAAiEIYAXABwAEG&rs=AOn4CLBCyh95CRe_cTECmF4XY9oq3jtFjw",
@ -1179,7 +1232,7 @@ Channel(
id: "UCcvfHa-GHSOHFAjU0-Ie57A",
name: "Adam Something",
avatar: [],
verification: verified,
verification: Verified,
subscriber_count: Some(947000),
)),
publish_date: "[date]",
@ -1193,7 +1246,7 @@ Channel(
VideoItem(
id: "-OO3RiNMDB8",
name: "Assessing The Russian Invasion Threat",
duration: Some(655),
length: Some(655),
thumbnail: [
Thumbnail(
url: "https://i.ytimg.com/vi/-OO3RiNMDB8/hqdefault.jpg?sqp=-oaymwEbCKgBEF5IVfKriqkDDggBFQAAiEIYAXABwAEG&rs=AOn4CLAWbC-UhcsGON1ERqF0BToXXwNXdA",
@ -1220,7 +1273,7 @@ Channel(
id: "UCcvfHa-GHSOHFAjU0-Ie57A",
name: "Adam Something",
avatar: [],
verification: verified,
verification: Verified,
subscriber_count: Some(947000),
)),
publish_date: "[date]",
@ -1234,7 +1287,7 @@ Channel(
VideoItem(
id: "obMTYs30E9A",
name: "Ukraine - The Country That Defied Vladimir Putin",
duration: Some(2498),
length: Some(2498),
thumbnail: [
Thumbnail(
url: "https://i.ytimg.com/vi/obMTYs30E9A/hqdefault.jpg?sqp=-oaymwEbCKgBEF5IVfKriqkDDggBFQAAiEIYAXABwAEG&rs=AOn4CLDsLbeOIYyrq_a3srmaShg1BXt6IA",
@ -1261,7 +1314,7 @@ Channel(
id: "UCcvfHa-GHSOHFAjU0-Ie57A",
name: "Adam Something",
avatar: [],
verification: verified,
verification: Verified,
subscriber_count: Some(947000),
)),
publish_date: "[date]",
@ -1275,7 +1328,7 @@ Channel(
VideoItem(
id: "4-2bR1iFlhk",
name: "\"Wait, Russia isn\'t in NATO?!\" Insane Debate on Ukraine, US Politics, and more!",
duration: Some(12151),
length: Some(12151),
thumbnail: [
Thumbnail(
url: "https://i.ytimg.com/vi/4-2bR1iFlhk/hqdefault.jpg?sqp=-oaymwEbCKgBEF5IVfKriqkDDggBFQAAiEIYAXABwAEG&rs=AOn4CLDi8mzVinfZpO33L7lijfeQJdsVuA",
@ -1302,7 +1355,7 @@ Channel(
id: "UCcvfHa-GHSOHFAjU0-Ie57A",
name: "Adam Something",
avatar: [],
verification: verified,
verification: Verified,
subscriber_count: Some(947000),
)),
publish_date: "[date]",

View file

@ -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"),
)

View file

@ -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: [],

View file

@ -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: [

View file

@ -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,
),

View file

@ -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"),

View file

@ -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,
),

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