Compare commits
1 commit
main
...
feat/http3
Author | SHA1 | Date | |
---|---|---|---|
9743b3f9dc |
293 changed files with 59036 additions and 442896 deletions
|
@ -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"}}'
|
|
@ -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/*
|
|
@ -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 }}"
|
|
@ -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
19
.gitea/workflows/ci.yaml
Normal 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
3
.gitignore
vendored
|
@ -4,5 +4,4 @@
|
|||
*.snap.new
|
||||
|
||||
rustypipe_reports
|
||||
rustypipe_cache*.json
|
||||
bg_snapshot.bin
|
||||
rustypipe_cache.json
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
repos:
|
||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||
rev: v5.0.0
|
||||
rev: v4.3.0
|
||||
hooks:
|
||||
- id: end-of-file-fixer
|
||||
- id: check-json
|
||||
|
@ -10,8 +10,4 @@ repos:
|
|||
hooks:
|
||||
- id: cargo-fmt
|
||||
- id: cargo-clippy
|
||||
name: cargo-clippy rustypipe
|
||||
args: ["--package=rustypipe", "--tests", "--", "-D", "warnings"]
|
||||
- id: cargo-clippy
|
||||
name: cargo-clippy workspace
|
||||
args: ["--all", "--tests", "--features=rss,userdata,indicatif,audiotag", "--", "-D", "warnings"]
|
||||
args: ["--all", "--tests", "--features=rss", "--", "-D", "warnings"]
|
||||
|
|
10
.woodpecker.yml
Normal file
10
.woodpecker.yml
Normal file
|
@ -0,0 +1,10 @@
|
|||
steps:
|
||||
test:
|
||||
image: rust:latest
|
||||
environment:
|
||||
- CARGO_REGISTRIES_CRATES_IO_PROTOCOL=sparse
|
||||
commands:
|
||||
- rustup component add rustfmt clippy
|
||||
- cargo fmt --all --check
|
||||
- cargo clippy --all --features=rss -- -D warnings
|
||||
- cargo test --features=rss --workspace
|
396
CHANGELOG.md
396
CHANGELOG.md
|
@ -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 -->
|
161
Cargo.toml
161
Cargo.toml
|
@ -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"
|
||||
|
|
|
@ -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`)
|
57
Justfile
57
Justfile
|
@ -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
149
README.md
|
@ -1,12 +1,9 @@
|
|||
# 
|
||||
# RustyPipe
|
||||
|
||||
[](https://crates.io/crates/rustypipe)
|
||||
[](https://opensource.org/licenses/GPL-3.0)
|
||||
[](https://docs.rs/rustypipe)
|
||||
[](https://codeberg.org/ThetaDev/rustypipe/actions/?workflow=ci.yaml)
|
||||
[](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`)
|
||||
|
|
207
cli/CHANGELOG.md
207
cli/CHANGELOG.md
|
@ -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 -->
|
|
@ -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"
|
||||
|
|
174
cli/README.md
174
cli/README.md
|
@ -1,174 +0,0 @@
|
|||
#  CLI
|
||||
|
||||
[](https://crates.io/crates/rustypipe-cli)
|
||||
[](https://opensource.org/licenses/GPL-3.0)
|
||||
[](https://codeberg.org/ThetaDev/rustypipe/actions/?workflow=ci.yaml)
|
||||
|
||||
The RustyPipe CLI is a powerful YouTube client for the command line. It allows you to
|
||||
access most of the features of the RustyPipe crate: getting data from YouTube and
|
||||
downloading videos.
|
||||
|
||||
## Installation
|
||||
|
||||
You can download a compiled version of RustyPipe here:
|
||||
<https://codeberg.org/ThetaDev/rustypipe/releases>
|
||||
|
||||
Alternatively, you can compile it yourself by installing [Rust](https://rustup.rs/) and
|
||||
running `cargo install rustypipe-cli`.
|
||||
|
||||
To be able to access streams from web-based clients (Desktop, Mobile) you need to
|
||||
download [rustypipe-botguard](https://codeberg.org/ThetaDev/rustypipe-botguard/releases)
|
||||
and place the binary either in the PATH or the current working directory.
|
||||
|
||||
For downloading videos you also need to have ffmpeg installed.
|
||||
|
||||
## `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.
|
1742
cli/src/main.rs
1742
cli/src/main.rs
File diff suppressed because it is too large
Load diff
100
cliff.toml
100
cliff.toml
|
@ -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
|
|
@ -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"] }
|
||||
|
|
|
@ -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\""))
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
|
|
|
@ -1,130 +0,0 @@
|
|||
use std::{collections::BTreeMap, fs::File, io::BufReader};
|
||||
|
||||
use path_macro::path;
|
||||
use rustypipe::{
|
||||
client::{ClientType, RustyPipe},
|
||||
param::{Language, LANGUAGES},
|
||||
};
|
||||
use serde::Deserialize;
|
||||
use serde_with::rust::deserialize_ignore_any;
|
||||
|
||||
use crate::{
|
||||
model::{QBrowse, SectionList, TextRuns},
|
||||
util::{self, DICT_DIR},
|
||||
};
|
||||
|
||||
pub async fn collect_album_versions_titles() {
|
||||
let json_path = path!(*DICT_DIR / "other_versions_titles.json");
|
||||
let mut res = BTreeMap::new();
|
||||
|
||||
let rp = RustyPipe::new();
|
||||
|
||||
for lang in LANGUAGES {
|
||||
let query = QBrowse {
|
||||
browse_id: "MPREb_nlBWQROfvjo",
|
||||
params: None,
|
||||
};
|
||||
let raw_resp = rp
|
||||
.query()
|
||||
.lang(lang)
|
||||
.raw(ClientType::DesktopMusic, "browse", &query)
|
||||
.await
|
||||
.unwrap();
|
||||
let data = serde_json::from_str::<AlbumData>(&raw_resp).unwrap();
|
||||
let title = data
|
||||
.contents
|
||||
.two_column_browse_results_renderer
|
||||
.secondary_contents
|
||||
.section_list_renderer
|
||||
.contents
|
||||
.into_iter()
|
||||
.find_map(|x| match x {
|
||||
ItemSection::MusicCarouselShelfRenderer(music_carousel_shelf) => {
|
||||
Some(music_carousel_shelf)
|
||||
}
|
||||
ItemSection::None => None,
|
||||
})
|
||||
.expect("other versions")
|
||||
.header
|
||||
.expect("header")
|
||||
.music_carousel_shelf_basic_header_renderer
|
||||
.title
|
||||
.runs
|
||||
.into_iter()
|
||||
.next()
|
||||
.unwrap()
|
||||
.text;
|
||||
println!("{lang}: {title}");
|
||||
res.insert(lang, title);
|
||||
}
|
||||
|
||||
let file = File::create(json_path).unwrap();
|
||||
serde_json::to_writer_pretty(file, &res).unwrap();
|
||||
}
|
||||
|
||||
pub fn write_samples_to_dict() {
|
||||
let json_path = path!(*DICT_DIR / "other_versions_titles.json");
|
||||
let json_file = File::open(json_path).unwrap();
|
||||
let collected: BTreeMap<Language, String> =
|
||||
serde_json::from_reader(BufReader::new(json_file)).unwrap();
|
||||
let mut dict = util::read_dict();
|
||||
let langs = dict.keys().copied().collect::<Vec<_>>();
|
||||
|
||||
for lang in langs {
|
||||
let dict_entry = dict.entry(lang).or_default();
|
||||
|
||||
let e = collected.get(&lang).unwrap();
|
||||
assert_eq!(e, e.trim());
|
||||
dict_entry.album_versions_title = e.to_owned();
|
||||
|
||||
for lang in &dict_entry.equivalent {
|
||||
let ee = collected.get(lang).unwrap();
|
||||
if ee != e {
|
||||
panic!("equivalent lang conflict, lang: {lang}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
util::write_dict(dict);
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct AlbumData {
|
||||
contents: AlbumDataContents,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct AlbumDataContents {
|
||||
two_column_browse_results_renderer: X1,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct X1 {
|
||||
secondary_contents: SectionList<ItemSection>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
enum ItemSection {
|
||||
MusicCarouselShelfRenderer(MusicCarouselShelf),
|
||||
#[serde(other, deserialize_with = "deserialize_ignore_any")]
|
||||
None,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct MusicCarouselShelf {
|
||||
header: Option<MusicCarouselShelfHeader>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct MusicCarouselShelfHeader {
|
||||
music_carousel_shelf_basic_header_renderer: MusicCarouselShelfHeaderRenderer,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct MusicCarouselShelfHeaderRenderer {
|
||||
title: TextRuns,
|
||||
}
|
|
@ -1,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);
|
||||
}
|
|
@ -1,110 +0,0 @@
|
|||
use std::{collections::BTreeMap, fs::File, io::BufReader};
|
||||
|
||||
use path_macro::path;
|
||||
use rustypipe::{
|
||||
client::RustyPipe,
|
||||
param::{Language, LANGUAGES},
|
||||
};
|
||||
|
||||
use crate::util::{self, DICT_DIR};
|
||||
|
||||
type CollectedDates = BTreeMap<Language, BTreeMap<String, String>>;
|
||||
|
||||
const THIS_WEEK: &str = "this_week";
|
||||
const LAST_WEEK: &str = "last_week";
|
||||
|
||||
pub async fn collect_dates_music() {
|
||||
let json_path = path!(*DICT_DIR / "history_date_samples.json");
|
||||
let rp = RustyPipe::builder()
|
||||
.storage_dir(path!(env!("CARGO_MANIFEST_DIR") / ".."))
|
||||
.build()
|
||||
.unwrap();
|
||||
|
||||
let mut res: CollectedDates = {
|
||||
let json_file = File::open(&json_path).unwrap();
|
||||
serde_json::from_reader(BufReader::new(json_file)).unwrap()
|
||||
};
|
||||
|
||||
for lang in LANGUAGES {
|
||||
println!("{lang}");
|
||||
let history = rp.query().lang(lang).music_history().await.unwrap();
|
||||
if history.items.len() < 3 {
|
||||
panic!("{lang} empty history")
|
||||
}
|
||||
|
||||
// The indexes have to be adapted before running
|
||||
let entry = res.entry(lang).or_default();
|
||||
entry.insert(
|
||||
THIS_WEEK.to_owned(),
|
||||
history.items[0].playback_date_txt.clone().unwrap(),
|
||||
);
|
||||
entry.insert(
|
||||
LAST_WEEK.to_owned(),
|
||||
history.items[18].playback_date_txt.clone().unwrap(),
|
||||
);
|
||||
}
|
||||
|
||||
let file = File::create(&json_path).unwrap();
|
||||
serde_json::to_writer_pretty(file, &res).unwrap();
|
||||
}
|
||||
|
||||
pub async fn collect_dates() {
|
||||
let json_path = path!(*DICT_DIR / "history_date_samples.json");
|
||||
let rp = RustyPipe::builder()
|
||||
.storage_dir(path!(env!("CARGO_MANIFEST_DIR") / ".."))
|
||||
.build()
|
||||
.unwrap();
|
||||
|
||||
let mut res: CollectedDates = {
|
||||
let json_file = File::open(&json_path).unwrap();
|
||||
serde_json::from_reader(BufReader::new(json_file)).unwrap()
|
||||
};
|
||||
|
||||
for lang in LANGUAGES {
|
||||
println!("{lang}");
|
||||
let history = rp.query().lang(lang).history().await.unwrap();
|
||||
if history.items.len() < 3 {
|
||||
panic!("{lang} empty history")
|
||||
}
|
||||
|
||||
let entry = res.entry(lang).or_default();
|
||||
entry.insert(
|
||||
"tuesday".to_owned(),
|
||||
history.items[0].playback_date_txt.clone().unwrap(),
|
||||
);
|
||||
entry.insert(
|
||||
"0000-01-06".to_owned(),
|
||||
history.items[1].playback_date_txt.clone().unwrap(),
|
||||
);
|
||||
entry.insert(
|
||||
"2024-12-28".to_owned(),
|
||||
history.items[15].playback_date_txt.clone().unwrap(),
|
||||
);
|
||||
}
|
||||
|
||||
let file = File::create(&json_path).unwrap();
|
||||
serde_json::to_writer_pretty(file, &res).unwrap();
|
||||
}
|
||||
|
||||
pub fn write_samples_to_dict() {
|
||||
let json_path = path!(*DICT_DIR / "history_date_samples.json");
|
||||
|
||||
let json_file = File::open(json_path).unwrap();
|
||||
let collected_dates: CollectedDates =
|
||||
serde_json::from_reader(BufReader::new(json_file)).unwrap();
|
||||
let mut dict = util::read_dict();
|
||||
let langs = dict.keys().copied().collect::<Vec<_>>();
|
||||
|
||||
for lang in langs {
|
||||
let dict_entry = dict.entry(lang).or_default();
|
||||
let cd = &collected_dates[&lang];
|
||||
dict_entry
|
||||
.timeago_nd_tokens
|
||||
.insert(util::filter_datestr(&cd[THIS_WEEK]), "0Wl".to_owned());
|
||||
dict_entry
|
||||
.timeago_nd_tokens
|
||||
.insert(util::filter_datestr(&cd[LAST_WEEK]), "1Wl".to_owned());
|
||||
}
|
||||
|
||||
util::write_dict(dict);
|
||||
}
|
|
@ -6,7 +6,7 @@ use std::{
|
|||
};
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
use futures_util::{stream, StreamExt};
|
||||
use futures::{stream, StreamExt};
|
||||
use once_cell::sync::Lazy;
|
||||
use path_macro::path;
|
||||
use regex::Regex;
|
||||
|
@ -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,
|
||||
},
|
||||
|
|
|
@ -5,7 +5,7 @@ use std::{
|
|||
io::BufReader,
|
||||
};
|
||||
|
||||
use futures_util::{stream, StreamExt};
|
||||
use futures::{stream, StreamExt};
|
||||
use ordered_hash_map::OrderedHashMap;
|
||||
use path_macro::path;
|
||||
use rustypipe::{
|
||||
|
|
|
@ -3,7 +3,7 @@ use std::{
|
|||
fs::File,
|
||||
};
|
||||
|
||||
use futures_util::{stream, StreamExt};
|
||||
use futures::{stream, StreamExt};
|
||||
use path_macro::path;
|
||||
use rustypipe::{
|
||||
client::{RustyPipe, RustyPipeQuery},
|
||||
|
|
|
@ -5,7 +5,7 @@ use std::{
|
|||
};
|
||||
|
||||
use anyhow::Result;
|
||||
use futures_util::{stream, StreamExt};
|
||||
use futures::{stream, StreamExt};
|
||||
use path_macro::path;
|
||||
use rustypipe::{
|
||||
client::{ClientType, RustyPipe, RustyPipeQuery},
|
||||
|
@ -204,6 +204,8 @@ pub fn parse_video_durations() {
|
|||
parse(&mut words, lang, dict_entry.by_char, txt, *d);
|
||||
}
|
||||
|
||||
// dbg!(&words);
|
||||
|
||||
for (k, v) in words {
|
||||
if let Some(v) = v {
|
||||
dict_entry.timeago_tokens.insert(k, v.to_string());
|
||||
|
@ -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"),
|
||||
},
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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 } => {
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
|
||||
|
|
|
@ -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(' ')
|
||||
|
|
|
@ -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 -->
|
|
@ -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"] }
|
||||
|
|
|
@ -1,47 +0,0 @@
|
|||
#  Downloader
|
||||
|
||||
[](https://crates.io/crates/rustypipe-downloader)
|
||||
[](https://opensource.org/licenses/GPL-3.0)
|
||||
[](https://docs.rs/rustypipe-downloader)
|
||||
[](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;
|
||||
```
|
|
@ -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
|
@ -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
|
||||
///
|
||||
|
|
|
@ -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());
|
||||
}
|
|
@ -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%)
|
||||
|
||||

|
||||
|
||||
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%)
|
||||
|
||||

|
||||
|
||||
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 |
BIN
notes/logo.png
BIN
notes/logo.png
Binary file not shown.
Before Width: | Height: | Size: 3.5 KiB |
110
notes/logo.svg
110
notes/logo.svg
|
@ -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 |
|
@ -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
|
|
@ -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
|
||||
}
|
21
src/cache.rs
21
src/cache.rs
|
@ -16,8 +16,7 @@
|
|||
//! the cache as a JSON file.
|
||||
|
||||
use std::{
|
||||
fs::File,
|
||||
io::Write,
|
||||
fs,
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
|
||||
|
@ -69,21 +68,7 @@ impl Default for FileStorage {
|
|||
|
||||
impl CacheStorage for FileStorage {
|
||||
fn write(&self, data: &str) {
|
||||
fn _write(path: &Path, data: &str) -> Result<(), std::io::Error> {
|
||||
let mut f = File::create(path)?;
|
||||
// Set cache file permissions to 0600 on Unix-based systems
|
||||
#[cfg(target_family = "unix")]
|
||||
{
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
let metadata = f.metadata()?;
|
||||
let mut permissions = metadata.permissions();
|
||||
permissions.set_mode(0o600);
|
||||
std::fs::set_permissions(path, permissions)?;
|
||||
}
|
||||
f.write_all(data.as_bytes())
|
||||
}
|
||||
|
||||
_write(&self.path, data).unwrap_or_else(|e| {
|
||||
fs::write(&self.path, data).unwrap_or_else(|e| {
|
||||
error!(
|
||||
"Could not write cache to file `{}`. Error: {}",
|
||||
self.path.to_string_lossy(),
|
||||
|
@ -97,7 +82,7 @@ impl CacheStorage for FileStorage {
|
|||
return None;
|
||||
}
|
||||
|
||||
match std::fs::read_to_string(&self.path) {
|
||||
match fs::read_to_string(&self.path) {
|
||||
Ok(data) => Some(data),
|
||||
Err(e) => {
|
||||
error!(
|
||||
|
|
|
@ -9,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]
|
||||
|
|
|
@ -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()),
|
||||
|
|
1996
src/client/mod.rs
1996
src/client/mod.rs
File diff suppressed because it is too large
Load diff
|
@ -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 {
|
||||
|
|
|
@ -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(),
|
||||
|
|
|
@ -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(),
|
||||
|
|
|
@ -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(),
|
||||
|
|
|
@ -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(),
|
||||
|
|
|
@ -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(),
|
||||
|
|
|
@ -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(),
|
||||
|
|
|
@ -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]",
|
||||
});
|
||||
}
|
||||
}
|
|
@ -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(),
|
||||
|
|
1065
src/client/player.rs
1065
src/client/player.rs
File diff suppressed because it is too large
Load diff
|
@ -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(),
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,8 +0,0 @@
|
|||
use serde::Deserialize;
|
||||
|
||||
use super::{video_item::YouTubeListRendererWrap, Tab, TwoColumnBrowseResults};
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub(crate) struct History {
|
||||
pub contents: TwoColumnBrowseResults<Tab<YouTubeListRendererWrap>>,
|
||||
}
|
|
@ -30,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 {}
|
||||
|
|
|
@ -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)]
|
||||
|
|
|
@ -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>,
|
||||
}
|
||||
|
|
|
@ -1,8 +0,0 @@
|
|||
use serde::Deserialize;
|
||||
|
||||
use super::music_playlist::Contents;
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub(crate) struct MusicHistory {
|
||||
pub contents: Contents,
|
||||
}
|
|
@ -4,7 +4,7 @@ use serde_with::{rust::deserialize_ignore_any, serde_as, DefaultOnError, VecSkip
|
|||
use crate::{
|
||||
model::{
|
||||
self, traits::FromYtItem, AlbumId, AlbumItem, AlbumType, ArtistId, ArtistItem, ChannelId,
|
||||
MusicItem, MusicItemType, MusicPlaylistItem, TrackItem, UserItem,
|
||||
MusicItem, MusicItemType, MusicPlaylistItem, TrackItem,
|
||||
},
|
||||
param::Language,
|
||||
serializer::{
|
||||
|
@ -18,15 +18,10 @@ use super::{
|
|||
url_endpoint::{
|
||||
BrowseEndpointWrap, MusicPage, MusicPageType, MusicVideoType, NavigationEndpoint, PageType,
|
||||
},
|
||||
ContentsRenderer, ContinuationActionWrap, ContinuationEndpoint, MusicContinuationData,
|
||||
SimpleHeaderRenderer, Thumbnails, ThumbnailsWrap,
|
||||
ContentsRenderer, MusicContinuationData, Thumbnails, ThumbnailsWrap,
|
||||
};
|
||||
|
||||
#[cfg(feature = "userdata")]
|
||||
use crate::model::HistoryItem;
|
||||
#[cfg(feature = "userdata")]
|
||||
use time::UtcOffset;
|
||||
|
||||
#[serde_as]
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub(crate) enum ItemSection {
|
||||
|
@ -44,9 +39,6 @@ pub(crate) enum ItemSection {
|
|||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub(crate) struct MusicShelf {
|
||||
#[cfg(feature = "userdata")]
|
||||
#[serde_as(as = "Option<Text>")]
|
||||
pub title: Option<String>,
|
||||
/// Playlist ID (only for playlists)
|
||||
pub playlist_id: Option<String>,
|
||||
pub contents: MapResult<Vec<MusicResponseItem>>,
|
||||
|
@ -93,10 +85,6 @@ pub(crate) enum MusicResponseItem {
|
|||
MusicResponsiveListItemRenderer(ListMusicItem),
|
||||
MusicTwoRowItemRenderer(CoverMusicItem),
|
||||
MessageRenderer(serde::de::IgnoredAny),
|
||||
#[serde(rename_all = "camelCase")]
|
||||
ContinuationItemRenderer {
|
||||
continuation_endpoint: ContinuationEndpoint,
|
||||
},
|
||||
}
|
||||
|
||||
#[serde_as]
|
||||
|
@ -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}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,25 +1,25 @@
|
|||
use serde::Deserialize;
|
||||
use serde_with::{serde_as, DefaultOnError, VecSkipError};
|
||||
|
||||
use crate::serializer::text::{AttributedText, Text, TextComponents};
|
||||
use crate::serializer::text::{Text, TextComponents};
|
||||
|
||||
use super::{
|
||||
music_item::{
|
||||
Button, ItemSection, MusicContentsRenderer, MusicItemMenuEntry, MusicMicroformat,
|
||||
MusicThumbnailRenderer,
|
||||
Button, ItemSection, MusicContentsRenderer, MusicItemMenuEntry, MusicThumbnailRenderer,
|
||||
},
|
||||
url_endpoint::OnTapWrap,
|
||||
ContentsRenderer, SectionList, Tab,
|
||||
};
|
||||
|
||||
/// Response model for YouTube Music playlists and albums
|
||||
#[serde_as]
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub(crate) struct MusicPlaylist {
|
||||
pub contents: Option<Contents>,
|
||||
pub contents: Contents,
|
||||
pub header: Option<Header>,
|
||||
#[serde(default)]
|
||||
pub microformat: MusicMicroformat,
|
||||
#[serde_as(as = "DefaultOnError")]
|
||||
pub microformat: Option<Microformat>,
|
||||
}
|
||||
|
||||
#[serde_as]
|
||||
|
@ -83,10 +83,6 @@ pub(crate) struct HeaderRenderer {
|
|||
#[serde(default)]
|
||||
#[serde_as(as = "Text")]
|
||||
pub second_subtitle: Vec<String>,
|
||||
/// Channel (newer data model)
|
||||
#[serde(default)]
|
||||
#[serde_as(as = "DefaultOnError")]
|
||||
pub facepile: Option<AvatarStackViewModelWrap>,
|
||||
#[serde(default)]
|
||||
#[serde_as(as = "DefaultOnError")]
|
||||
pub menu: Option<HeaderMenu>,
|
||||
|
@ -141,23 +137,12 @@ impl From<Description> for TextComponents {
|
|||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub(crate) struct AvatarStackViewModelWrap {
|
||||
pub avatar_stack_view_model: AvatarStackViewModel,
|
||||
}
|
||||
|
||||
#[serde_as]
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub(crate) struct AvatarStackViewModel {
|
||||
// #[serde(default)]
|
||||
// pub avatars: Vec<AvatarViewModel>,
|
||||
#[serde_as(as = "AttributedText")]
|
||||
pub text: String,
|
||||
pub renderer_context: AvatarStackRendererContext,
|
||||
pub(crate) struct Microformat {
|
||||
pub microformat_data_renderer: MicroformatData,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub(crate) struct AvatarStackRendererContext {
|
||||
pub command_context: Option<OnTapWrap>,
|
||||
pub(crate) struct MicroformatData {
|
||||
pub url_canonical: String,
|
||||
}
|
||||
|
|
|
@ -2,9 +2,9 @@ use std::ops::Range;
|
|||
|
||||
use serde::Deserialize;
|
||||
use serde_with::serde_as;
|
||||
use serde_with::{DefaultOnError, DisplayFromStr, VecSkipError};
|
||||
use serde_with::{DefaultOnError, DisplayFromStr};
|
||||
|
||||
use super::{Empty, ResponseContext, Thumbnails};
|
||||
use super::{ResponseContext, Thumbnails};
|
||||
use crate::serializer::{text::Text, MapResult};
|
||||
|
||||
#[serde_as]
|
||||
|
@ -19,10 +19,6 @@ pub(crate) struct Player {
|
|||
#[serde_as(deserialize_as = "DefaultOnError")]
|
||||
pub storyboards: Option<Storyboards>,
|
||||
pub response_context: ResponseContext,
|
||||
#[serde(default)]
|
||||
pub player_config: PlayerConfig,
|
||||
#[serde(default)]
|
||||
pub heartbeat_params: HeartbeatParams,
|
||||
}
|
||||
|
||||
#[serde_as]
|
||||
|
@ -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,
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
|
|
|
@ -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")]
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
|
|
|
@ -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(),
|
||||
|
|
|
@ -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]",
|
||||
|
|
|
@ -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),
|
|
@ -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,
|
||||
),
|
||||
)
|
|
@ -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, I’m Tina, aka Doobydobap!\n\nFood is the medium I use to tell stories and connect with people who share the same passion as I do. Whether it’s because you’re hungry at midnight or trying to learn how to cook, I hope you enjoy watching my content and recipes. Don\'t yuck my yum!\n\nwww.doobydobap.com\n",
|
||||
tags: [],
|
||||
vanity_url: Some("https://www.youtube.com/@Doobydobap"),
|
||||
banner: [
|
||||
Thumbnail(
|
||||
url: "https://yt3.googleusercontent.com/BvnAqgiursrXpmS9AgDLtkOSTQfOG_Dqn0KzY5hcwO9XrHTEQTVgaflI913f9KRp7d0U2qBp=w1060-fcrop64=1,00005a57ffffa5a8-k-c0xffffffff-no-nd-rj",
|
||||
|
@ -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]",
|
||||
|
|
|
@ -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, I’m Tina, aka Doobydobap!\n\nFood is the medium I use to tell stories and connect with people who share the same passion as I do. Whether it’s because you’re hungry at midnight or trying to learn how to cook, I hope you enjoy watching my content and recipes. Don\'t yuck my yum!\n\nwww.doobydobap.com\n",
|
||||
tags: [],
|
||||
vanity_url: Some("https://www.youtube.com/@Doobydobap"),
|
||||
banner: [
|
||||
Thumbnail(
|
||||
url: "https://yt3.googleusercontent.com/BvnAqgiursrXpmS9AgDLtkOSTQfOG_Dqn0KzY5hcwO9XrHTEQTVgaflI913f9KRp7d0U2qBp=w1060-fcrop64=1,00005a57ffffa5a8-k-c0xffffffff-no-nd-rj",
|
||||
|
@ -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]",
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -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, I’m Tina, aka Doobydobap!\n\nFood is the medium I use to tell stories and connect with people who share the same passion as I do. Whether it’s because you’re hungry at midnight or trying to learn how to cook, I hope you enjoy watching my content and recipes. Don\'t yuck my yum!\n\nwww.doobydobap.com\n",
|
||||
tags: [],
|
||||
vanity_url: Some("https://www.youtube.com/c/Doobydobap"),
|
||||
banner: [
|
||||
Thumbnail(
|
||||
url: "https://yt3.ggpht.com/BvnAqgiursrXpmS9AgDLtkOSTQfOG_Dqn0KzY5hcwO9XrHTEQTVgaflI913f9KRp7d0U2qBp=w1060-fcrop64=1,00005a57ffffa5a8-k-c0xffffffff-no-nd-rj",
|
||||
|
@ -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]",
|
||||
|
|
|
@ -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]",
|
||||
|
|
|
@ -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]",
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -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]",
|
||||
|
|
|
@ -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"),
|
||||
|
|
|
@ -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]",
|
||||
|
|
|
@ -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"),
|
||||
|
|
|
@ -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, I’m Tina, aka Doobydobap!\n\nFood is the medium I use to tell stories and connect with people who share the same passion as I do. Whether it’s because you’re hungry at midnight or trying to learn how to cook, I hope you enjoy watching my content and recipes. Don\'t yuck my yum!\n\nwww.doobydobap.com\n",
|
||||
tags: [],
|
||||
vanity_url: Some("https://www.youtube.com/c/Doobydobap"),
|
||||
banner: [
|
||||
Thumbnail(
|
||||
url: "https://yt3.ggpht.com/BvnAqgiursrXpmS9AgDLtkOSTQfOG_Dqn0KzY5hcwO9XrHTEQTVgaflI913f9KRp7d0U2qBp=w1060-fcrop64=1,00005a57ffffa5a8-k-c0xffffffff-no-nd-rj",
|
||||
|
@ -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]",
|
||||
|
|
|
@ -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]",
|
||||
|
|
|
@ -1,943 +0,0 @@
|
|||
---
|
||||
source: src/client/music_artist.rs
|
||||
expression: artist
|
||||
---
|
||||
MusicArtist(
|
||||
id: "UCOR4_bSVIXPsGa4BbCSt60Q",
|
||||
name: "Trailerpark",
|
||||
header_image: [
|
||||
Thumbnail(
|
||||
url: "https://lh3.googleusercontent.com/II101BviJo-tGcGg1KKWSU8D3EZjALHQMbQ4v-7-hP4Zfk1pBESaTCLcz8eQb-hggzxq4Z1MuFkBeRE=w540-h225-p-l90-rj",
|
||||
width: 540,
|
||||
height: 225,
|
||||
),
|
||||
Thumbnail(
|
||||
url: "https://lh3.googleusercontent.com/II101BviJo-tGcGg1KKWSU8D3EZjALHQMbQ4v-7-hP4Zfk1pBESaTCLcz8eQb-hggzxq4Z1MuFkBeRE=w721-h300-p-l90-rj",
|
||||
width: 721,
|
||||
height: 300,
|
||||
),
|
||||
],
|
||||
description: None,
|
||||
wikipedia_url: None,
|
||||
subscriber_count: Some(270000),
|
||||
tracks: [
|
||||
TrackItem(
|
||||
id: "YvidasjVLXk",
|
||||
name: "Bleib in der Schule",
|
||||
duration: None,
|
||||
cover: [
|
||||
Thumbnail(
|
||||
url: "https://lh3.googleusercontent.com/V_tvMqbuXgDgoAKuYZ-VFRru3cUb2WQvwO6vVBKY8pdFYAl1dkuIv_W2afjMUNN6uVNxet6r7mHISh0s=w60-h60-l90-rj",
|
||||
width: 60,
|
||||
height: 60,
|
||||
),
|
||||
Thumbnail(
|
||||
url: "https://lh3.googleusercontent.com/V_tvMqbuXgDgoAKuYZ-VFRru3cUb2WQvwO6vVBKY8pdFYAl1dkuIv_W2afjMUNN6uVNxet6r7mHISh0s=w120-h120-l90-rj",
|
||||
width: 120,
|
||||
height: 120,
|
||||
),
|
||||
],
|
||||
artists: [
|
||||
ArtistId(
|
||||
id: Some("UCOR4_bSVIXPsGa4BbCSt60Q"),
|
||||
name: "Trailerpark",
|
||||
),
|
||||
],
|
||||
artist_id: Some("UCOR4_bSVIXPsGa4BbCSt60Q"),
|
||||
album: Some(AlbumId(
|
||||
id: "MPREb_8PsIyll0LFV",
|
||||
name: "Bleib in der Schule",
|
||||
)),
|
||||
view_count: Some(71000000),
|
||||
track_type: track,
|
||||
track_nr: None,
|
||||
by_va: false,
|
||||
),
|
||||
TrackItem(
|
||||
id: "h3T_NXRUUjM",
|
||||
name: "Fledermausland",
|
||||
duration: None,
|
||||
cover: [
|
||||
Thumbnail(
|
||||
url: "https://lh3.googleusercontent.com/1fPBoTszY4e6Nf8egSwBTHWsQT8hotwhDnjArd1SHS8gZc5asCoo_3Z2WhN1IO2KMqyYly0xm7mMZ43d=w60-h60-l90-rj",
|
||||
width: 60,
|
||||
height: 60,
|
||||
),
|
||||
Thumbnail(
|
||||
url: "https://lh3.googleusercontent.com/1fPBoTszY4e6Nf8egSwBTHWsQT8hotwhDnjArd1SHS8gZc5asCoo_3Z2WhN1IO2KMqyYly0xm7mMZ43d=w120-h120-l90-rj",
|
||||
width: 120,
|
||||
height: 120,
|
||||
),
|
||||
],
|
||||
artists: [
|
||||
ArtistId(
|
||||
id: Some("UCOR4_bSVIXPsGa4BbCSt60Q"),
|
||||
name: "Trailerpark",
|
||||
),
|
||||
],
|
||||
artist_id: Some("UCOR4_bSVIXPsGa4BbCSt60Q"),
|
||||
album: Some(AlbumId(
|
||||
id: "MPREb_POeT6m0bw9q",
|
||||
name: "Crackstreet Boys II X Version",
|
||||
)),
|
||||
view_count: Some(30000000),
|
||||
track_type: track,
|
||||
track_nr: None,
|
||||
by_va: false,
|
||||
),
|
||||
TrackItem(
|
||||
id: "XZfoFwWvkGQ",
|
||||
name: "Sterben kannst du überall",
|
||||
duration: None,
|
||||
cover: [
|
||||
Thumbnail(
|
||||
url: "https://lh3.googleusercontent.com/eQCwnR4YLYnizEhQKeSDDE3rulSTo64cTfs8fxR1K-3iWUfC477SHV0ZOOoQa2vJuvr_9i_WDYI-wbo=w60-h60-l90-rj",
|
||||
width: 60,
|
||||
height: 60,
|
||||
),
|
||||
Thumbnail(
|
||||
url: "https://lh3.googleusercontent.com/eQCwnR4YLYnizEhQKeSDDE3rulSTo64cTfs8fxR1K-3iWUfC477SHV0ZOOoQa2vJuvr_9i_WDYI-wbo=w120-h120-l90-rj",
|
||||
width: 120,
|
||||
height: 120,
|
||||
),
|
||||
],
|
||||
artists: [
|
||||
ArtistId(
|
||||
id: Some("UCOR4_bSVIXPsGa4BbCSt60Q"),
|
||||
name: "Trailerpark",
|
||||
),
|
||||
],
|
||||
artist_id: Some("UCOR4_bSVIXPsGa4BbCSt60Q"),
|
||||
album: Some(AlbumId(
|
||||
id: "MPREb_UYdRV1nnK2J",
|
||||
name: "TP4L",
|
||||
)),
|
||||
view_count: Some(40000000),
|
||||
track_type: track,
|
||||
track_nr: None,
|
||||
by_va: false,
|
||||
),
|
||||
TrackItem(
|
||||
id: "LOuVxwVFJhs",
|
||||
name: "Selbstbefriedigung",
|
||||
duration: None,
|
||||
cover: [
|
||||
Thumbnail(
|
||||
url: "https://lh3.googleusercontent.com/1fPBoTszY4e6Nf8egSwBTHWsQT8hotwhDnjArd1SHS8gZc5asCoo_3Z2WhN1IO2KMqyYly0xm7mMZ43d=w60-h60-l90-rj",
|
||||
width: 60,
|
||||
height: 60,
|
||||
),
|
||||
Thumbnail(
|
||||
url: "https://lh3.googleusercontent.com/1fPBoTszY4e6Nf8egSwBTHWsQT8hotwhDnjArd1SHS8gZc5asCoo_3Z2WhN1IO2KMqyYly0xm7mMZ43d=w120-h120-l90-rj",
|
||||
width: 120,
|
||||
height: 120,
|
||||
),
|
||||
],
|
||||
artists: [
|
||||
ArtistId(
|
||||
id: Some("UCOR4_bSVIXPsGa4BbCSt60Q"),
|
||||
name: "Trailerpark",
|
||||
),
|
||||
],
|
||||
artist_id: Some("UCOR4_bSVIXPsGa4BbCSt60Q"),
|
||||
album: Some(AlbumId(
|
||||
id: "MPREb_POeT6m0bw9q",
|
||||
name: "Crackstreet Boys II X Version",
|
||||
)),
|
||||
view_count: Some(13000000),
|
||||
track_type: track,
|
||||
track_nr: None,
|
||||
by_va: false,
|
||||
),
|
||||
TrackItem(
|
||||
id: "GePZUYeIQQQ",
|
||||
name: "Falsche Band",
|
||||
duration: None,
|
||||
cover: [
|
||||
Thumbnail(
|
||||
url: "https://lh3.googleusercontent.com/MIuap-H2LxqP5O7Dry1LdShBFBbg5YTjIPjuXOHWyrKlmnOogsO5cTk6yXH97DhI3WjZg0z3y-jkQxaM=w60-h60-l90-rj",
|
||||
width: 60,
|
||||
height: 60,
|
||||
),
|
||||
Thumbnail(
|
||||
url: "https://lh3.googleusercontent.com/MIuap-H2LxqP5O7Dry1LdShBFBbg5YTjIPjuXOHWyrKlmnOogsO5cTk6yXH97DhI3WjZg0z3y-jkQxaM=w120-h120-l90-rj",
|
||||
width: 120,
|
||||
height: 120,
|
||||
),
|
||||
],
|
||||
artists: [
|
||||
ArtistId(
|
||||
id: Some("UCOR4_bSVIXPsGa4BbCSt60Q"),
|
||||
name: "Trailerpark",
|
||||
),
|
||||
],
|
||||
artist_id: Some("UCOR4_bSVIXPsGa4BbCSt60Q"),
|
||||
album: Some(AlbumId(
|
||||
id: "MPREb_bi34SGT1xlc",
|
||||
name: "Crackstreet Boys 3 (Bonus Tracks Version)",
|
||||
)),
|
||||
view_count: Some(13000000),
|
||||
track_type: track,
|
||||
track_nr: None,
|
||||
by_va: false,
|
||||
),
|
||||
TrackItem(
|
||||
id: "0mcING0Zdis",
|
||||
name: "Trailerpark - TP4L (Live Abschiedskonzert)",
|
||||
duration: None,
|
||||
cover: [
|
||||
Thumbnail(
|
||||
url: "https://i.ytimg.com/vi/0mcING0Zdis/sddefault.jpg?sqp=-oaymwEWCJADEOEBIAQqCghqEJQEGHgg6AJIWg&rs=AMzJL3k5JY0WRBeKNaotfUYrpbObz1mceA",
|
||||
width: 400,
|
||||
height: 225,
|
||||
),
|
||||
Thumbnail(
|
||||
url: "https://i.ytimg.com/vi/0mcING0Zdis/hq720.jpg?sqp=-oaymwEXCKAGEMIDIAQqCwjVARCqCBh4INgESFo&rs=AMzJL3kinVfBJUF-SDFagYKazKmS_ad75w",
|
||||
width: 800,
|
||||
height: 450,
|
||||
),
|
||||
],
|
||||
artists: [
|
||||
ArtistId(
|
||||
id: Some("UCOR4_bSVIXPsGa4BbCSt60Q"),
|
||||
name: "Trailerpark",
|
||||
),
|
||||
],
|
||||
artist_id: Some("UCOR4_bSVIXPsGa4BbCSt60Q"),
|
||||
album: None,
|
||||
view_count: Some(13000),
|
||||
track_type: video,
|
||||
track_nr: None,
|
||||
by_va: false,
|
||||
),
|
||||
TrackItem(
|
||||
id: "EAC-2ttHCyk",
|
||||
name: "Fledermausland (Bonus Track)",
|
||||
duration: None,
|
||||
cover: [
|
||||
Thumbnail(
|
||||
url: "https://i.ytimg.com/vi/EAC-2ttHCyk/sddefault.jpg?sqp=-oaymwEWCJADEOEBIAQqCghqEJQEGHgg6AJIWg&rs=AMzJL3nlrgFTz_pwbBwXFbaASgklpX78vA",
|
||||
width: 400,
|
||||
height: 225,
|
||||
),
|
||||
Thumbnail(
|
||||
url: "https://i.ytimg.com/vi/EAC-2ttHCyk/hq720.jpg?sqp=-oaymwEXCKAGEMIDIAQqCwjVARCqCBh4INgESFo&rs=AMzJL3nHzhiahqhmIkZ0eUXD09BGak2MHQ",
|
||||
width: 800,
|
||||
height: 450,
|
||||
),
|
||||
],
|
||||
artists: [
|
||||
ArtistId(
|
||||
id: Some("UCOR4_bSVIXPsGa4BbCSt60Q"),
|
||||
name: "Trailerpark",
|
||||
),
|
||||
],
|
||||
artist_id: Some("UCOR4_bSVIXPsGa4BbCSt60Q"),
|
||||
album: None,
|
||||
view_count: Some(25000000),
|
||||
track_type: video,
|
||||
track_nr: None,
|
||||
by_va: false,
|
||||
),
|
||||
TrackItem(
|
||||
id: "Bret5VaVzJk",
|
||||
name: "New Kids on the Blech (Bonus Track)",
|
||||
duration: None,
|
||||
cover: [
|
||||
Thumbnail(
|
||||
url: "https://i.ytimg.com/vi/Bret5VaVzJk/sddefault.jpg?sqp=-oaymwEWCJADEOEBIAQqCghqEJQEGHgg6AJIWg&rs=AMzJL3nFa4qUxqJzCtxr-zPdzP15Ixvu-A",
|
||||
width: 400,
|
||||
height: 225,
|
||||
),
|
||||
Thumbnail(
|
||||
url: "https://i.ytimg.com/vi/Bret5VaVzJk/hq720.jpg?sqp=-oaymwEXCKAGEMIDIAQqCwjVARCqCBh4INgESFo&rs=AMzJL3l1hGZVAWUwaJQbZXmbRpcbsMdTeQ",
|
||||
width: 800,
|
||||
height: 450,
|
||||
),
|
||||
],
|
||||
artists: [
|
||||
ArtistId(
|
||||
id: Some("UCOR4_bSVIXPsGa4BbCSt60Q"),
|
||||
name: "Trailerpark",
|
||||
),
|
||||
],
|
||||
artist_id: Some("UCOR4_bSVIXPsGa4BbCSt60Q"),
|
||||
album: None,
|
||||
view_count: Some(6900000),
|
||||
track_type: video,
|
||||
track_nr: None,
|
||||
by_va: false,
|
||||
),
|
||||
TrackItem(
|
||||
id: "EqP1_IcjW-s",
|
||||
name: "Pimpulsiv feat. DNP, Sudden & Dana - Wohnwagensiedlung",
|
||||
duration: None,
|
||||
cover: [
|
||||
Thumbnail(
|
||||
url: "https://i.ytimg.com/vi/EqP1_IcjW-s/sddefault.jpg?sqp=-oaymwEWCJADEOEBIAQqCghqEJQEGHgg6AJIWg&rs=AMzJL3lIeltSLpA_XwwZzdJfHnNZ0vqBzA",
|
||||
width: 400,
|
||||
height: 225,
|
||||
),
|
||||
Thumbnail(
|
||||
url: "https://i.ytimg.com/vi/EqP1_IcjW-s/hq720.jpg?sqp=-oaymwEXCKAGEMIDIAQqCwjVARCqCBh4INgESFo&rs=AMzJL3nfiByY3RfcFYGfg92C5Vlkar0GJA",
|
||||
width: 800,
|
||||
height: 450,
|
||||
),
|
||||
],
|
||||
artists: [
|
||||
ArtistId(
|
||||
id: Some("UCOR4_bSVIXPsGa4BbCSt60Q"),
|
||||
name: "Trailerpark",
|
||||
),
|
||||
],
|
||||
artist_id: Some("UCOR4_bSVIXPsGa4BbCSt60Q"),
|
||||
album: None,
|
||||
view_count: Some(7100000),
|
||||
track_type: video,
|
||||
track_nr: None,
|
||||
by_va: false,
|
||||
),
|
||||
TrackItem(
|
||||
id: "3EoF9Of98e4",
|
||||
name: "Armut treibt Jugendliche in die Popmusik",
|
||||
duration: None,
|
||||
cover: [
|
||||
Thumbnail(
|
||||
url: "https://i.ytimg.com/vi/3EoF9Of98e4/sddefault.jpg?sqp=-oaymwEWCJADEOEBIAQqCghqEJQEGHgg6AJIWg&rs=AMzJL3kvWHX-5mYREKEkf-CM3TLfjrLjlw",
|
||||
width: 400,
|
||||
height: 225,
|
||||
),
|
||||
Thumbnail(
|
||||
url: "https://i.ytimg.com/vi/3EoF9Of98e4/hq720.jpg?sqp=-oaymwEXCKAGEMIDIAQqCwjVARCqCBh4INgESFo&rs=AMzJL3lItzsg6wamh_xSdpoZxTWOHHLS-g",
|
||||
width: 800,
|
||||
height: 450,
|
||||
),
|
||||
],
|
||||
artists: [
|
||||
ArtistId(
|
||||
id: Some("UCOR4_bSVIXPsGa4BbCSt60Q"),
|
||||
name: "Trailerpark",
|
||||
),
|
||||
],
|
||||
artist_id: Some("UCOR4_bSVIXPsGa4BbCSt60Q"),
|
||||
album: None,
|
||||
view_count: Some(5400000),
|
||||
track_type: video,
|
||||
track_nr: None,
|
||||
by_va: false,
|
||||
),
|
||||
TrackItem(
|
||||
id: "qr0eN_uIcTs",
|
||||
name: "Bleib in der Schule (Live in Berlin)",
|
||||
duration: None,
|
||||
cover: [
|
||||
Thumbnail(
|
||||
url: "https://i.ytimg.com/vi/qr0eN_uIcTs/sddefault.jpg?sqp=-oaymwEWCJADEOEBIAQqCghqEJQEGHgg6AJIWg&rs=AMzJL3nspTbohIYzDFOjTg90KEmKecVVvg",
|
||||
width: 400,
|
||||
height: 225,
|
||||
),
|
||||
Thumbnail(
|
||||
url: "https://i.ytimg.com/vi/qr0eN_uIcTs/hq720.jpg?sqp=-oaymwEXCKAGEMIDIAQqCwjVARCqCBh4INgESFo&rs=AMzJL3n0SIeq4dPTPvbGv4STsTWNt24cig",
|
||||
width: 800,
|
||||
height: 450,
|
||||
),
|
||||
],
|
||||
artists: [
|
||||
ArtistId(
|
||||
id: Some("UCOR4_bSVIXPsGa4BbCSt60Q"),
|
||||
name: "Trailerpark",
|
||||
),
|
||||
],
|
||||
artist_id: Some("UCOR4_bSVIXPsGa4BbCSt60Q"),
|
||||
album: None,
|
||||
view_count: Some(56000),
|
||||
track_type: video,
|
||||
track_nr: None,
|
||||
by_va: false,
|
||||
),
|
||||
TrackItem(
|
||||
id: "McgSyiug6XE",
|
||||
name: "We Are Family",
|
||||
duration: None,
|
||||
cover: [
|
||||
Thumbnail(
|
||||
url: "https://i.ytimg.com/vi/McgSyiug6XE/sddefault.jpg?sqp=-oaymwEWCJADEOEBIAQqCghqEJQEGHgg6AJIWg&rs=AMzJL3nxe3Xz99BVFg-VOra20J682me5JQ",
|
||||
width: 400,
|
||||
height: 225,
|
||||
),
|
||||
Thumbnail(
|
||||
url: "https://i.ytimg.com/vi/McgSyiug6XE/hq720.jpg?sqp=-oaymwEXCKAGEMIDIAQqCwjVARCqCBh4INgESFo&rs=AMzJL3lSGwKx_hnqYA-CkoLHapr1PiyX6w",
|
||||
width: 800,
|
||||
height: 450,
|
||||
),
|
||||
],
|
||||
artists: [
|
||||
ArtistId(
|
||||
id: Some("UCOR4_bSVIXPsGa4BbCSt60Q"),
|
||||
name: "Trailerpark",
|
||||
),
|
||||
ArtistId(
|
||||
id: Some("UC5HSrFHr6lMzwAyGjlClm0A"),
|
||||
name: "Timi Hendrix",
|
||||
),
|
||||
],
|
||||
artist_id: Some("UCOR4_bSVIXPsGa4BbCSt60Q"),
|
||||
album: None,
|
||||
view_count: Some(1800000),
|
||||
track_type: video,
|
||||
track_nr: None,
|
||||
by_va: false,
|
||||
),
|
||||
TrackItem(
|
||||
id: "ioZxvVhjFs8",
|
||||
name: "Schlechter Tag",
|
||||
duration: None,
|
||||
cover: [
|
||||
Thumbnail(
|
||||
url: "https://i.ytimg.com/vi/ioZxvVhjFs8/sddefault.jpg?sqp=-oaymwEWCJADEOEBIAQqCghqEJQEGHgg6AJIWg&rs=AMzJL3ltQmZbH1DF9nmho5HLGehqLSGzTw",
|
||||
width: 400,
|
||||
height: 225,
|
||||
),
|
||||
Thumbnail(
|
||||
url: "https://i.ytimg.com/vi/ioZxvVhjFs8/hq720.jpg?sqp=-oaymwEXCKAGEMIDIAQqCwjVARCqCBh4INgESFo&rs=AMzJL3lsluKPeCNxP7QoOCc24tZy4jsn7Q",
|
||||
width: 800,
|
||||
height: 450,
|
||||
),
|
||||
],
|
||||
artists: [
|
||||
ArtistId(
|
||||
id: Some("UCOR4_bSVIXPsGa4BbCSt60Q"),
|
||||
name: "Trailerpark",
|
||||
),
|
||||
],
|
||||
artist_id: Some("UCOR4_bSVIXPsGa4BbCSt60Q"),
|
||||
album: None,
|
||||
view_count: Some(7100000),
|
||||
track_type: video,
|
||||
track_nr: None,
|
||||
by_va: false,
|
||||
),
|
||||
TrackItem(
|
||||
id: "3jyZJEcomkw",
|
||||
name: "Timi Hendrix feat. Alligatoah - Schlaflos in Guantanamo ► prod. by Mantra",
|
||||
duration: None,
|
||||
cover: [
|
||||
Thumbnail(
|
||||
url: "https://i.ytimg.com/vi/3jyZJEcomkw/sddefault.jpg?sqp=-oaymwEWCJADEOEBIAQqCghqEJQEGHgg6AJIWg&rs=AMzJL3k46-OFTCnpEJry_PNst1C11FPA1A",
|
||||
width: 400,
|
||||
height: 225,
|
||||
),
|
||||
Thumbnail(
|
||||
url: "https://i.ytimg.com/vi/3jyZJEcomkw/hq720.jpg?sqp=-oaymwEXCKAGEMIDIAQqCwjVARCqCBh4INgESFo&rs=AMzJL3kN1ryaQSy4M_Y9bQGh9S-tbYGqdg",
|
||||
width: 800,
|
||||
height: 450,
|
||||
),
|
||||
],
|
||||
artists: [
|
||||
ArtistId(
|
||||
id: Some("UCOR4_bSVIXPsGa4BbCSt60Q"),
|
||||
name: "Trailerpark",
|
||||
),
|
||||
],
|
||||
artist_id: Some("UCOR4_bSVIXPsGa4BbCSt60Q"),
|
||||
album: None,
|
||||
view_count: Some(1500000),
|
||||
track_type: video,
|
||||
track_nr: None,
|
||||
by_va: false,
|
||||
),
|
||||
TrackItem(
|
||||
id: "9oM-cflYhGk",
|
||||
name: "Timi Hendrix - Kaiser von China (Official Video) 🐲",
|
||||
duration: None,
|
||||
cover: [
|
||||
Thumbnail(
|
||||
url: "https://i.ytimg.com/vi/9oM-cflYhGk/sddefault.jpg?sqp=-oaymwEWCJADEOEBIAQqCghqEJQEGHgg6AJIWg&rs=AMzJL3m6MksfA1NWyIMv6cTk03J21pA0NQ",
|
||||
width: 400,
|
||||
height: 225,
|
||||
),
|
||||
Thumbnail(
|
||||
url: "https://i.ytimg.com/vi/9oM-cflYhGk/hq720.jpg?sqp=-oaymwEXCKAGEMIDIAQqCwjVARCqCBh4INgESFo&rs=AMzJL3n7oy_XobzQBkUVxEx08iSKNPIB0Q",
|
||||
width: 800,
|
||||
height: 450,
|
||||
),
|
||||
],
|
||||
artists: [
|
||||
ArtistId(
|
||||
id: Some("UCOR4_bSVIXPsGa4BbCSt60Q"),
|
||||
name: "Trailerpark",
|
||||
),
|
||||
],
|
||||
artist_id: Some("UCOR4_bSVIXPsGa4BbCSt60Q"),
|
||||
album: None,
|
||||
view_count: Some(1100000),
|
||||
track_type: video,
|
||||
track_nr: None,
|
||||
by_va: false,
|
||||
),
|
||||
],
|
||||
albums: [
|
||||
AlbumItem(
|
||||
id: "MPREb_UYdRV1nnK2J",
|
||||
name: "TP4L",
|
||||
cover: [
|
||||
Thumbnail(
|
||||
url: "https://lh3.googleusercontent.com/eQCwnR4YLYnizEhQKeSDDE3rulSTo64cTfs8fxR1K-3iWUfC477SHV0ZOOoQa2vJuvr_9i_WDYI-wbo=w226-h226-l90-rj",
|
||||
width: 226,
|
||||
height: 226,
|
||||
),
|
||||
Thumbnail(
|
||||
url: "https://lh3.googleusercontent.com/eQCwnR4YLYnizEhQKeSDDE3rulSTo64cTfs8fxR1K-3iWUfC477SHV0ZOOoQa2vJuvr_9i_WDYI-wbo=w544-h544-l90-rj",
|
||||
width: 544,
|
||||
height: 544,
|
||||
),
|
||||
],
|
||||
artists: [
|
||||
ArtistId(
|
||||
id: Some("UCOR4_bSVIXPsGa4BbCSt60Q"),
|
||||
name: "Trailerpark",
|
||||
),
|
||||
],
|
||||
artist_id: Some("UCOR4_bSVIXPsGa4BbCSt60Q"),
|
||||
album_type: album,
|
||||
year: Some(2017),
|
||||
by_va: false,
|
||||
),
|
||||
AlbumItem(
|
||||
id: "MPREb_bi34SGT1xlc",
|
||||
name: "Crackstreet Boys 3 (Bonus Tracks Version)",
|
||||
cover: [
|
||||
Thumbnail(
|
||||
url: "https://lh3.googleusercontent.com/MIuap-H2LxqP5O7Dry1LdShBFBbg5YTjIPjuXOHWyrKlmnOogsO5cTk6yXH97DhI3WjZg0z3y-jkQxaM=w226-h226-l90-rj",
|
||||
width: 226,
|
||||
height: 226,
|
||||
),
|
||||
Thumbnail(
|
||||
url: "https://lh3.googleusercontent.com/MIuap-H2LxqP5O7Dry1LdShBFBbg5YTjIPjuXOHWyrKlmnOogsO5cTk6yXH97DhI3WjZg0z3y-jkQxaM=w544-h544-l90-rj",
|
||||
width: 544,
|
||||
height: 544,
|
||||
),
|
||||
],
|
||||
artists: [
|
||||
ArtistId(
|
||||
id: Some("UCOR4_bSVIXPsGa4BbCSt60Q"),
|
||||
name: "Trailerpark",
|
||||
),
|
||||
],
|
||||
artist_id: Some("UCOR4_bSVIXPsGa4BbCSt60Q"),
|
||||
album_type: album,
|
||||
year: Some(2014),
|
||||
by_va: false,
|
||||
),
|
||||
AlbumItem(
|
||||
id: "MPREb_5gkbwhqC4AJ",
|
||||
name: "Goldener Schluss (Live in Berlin)",
|
||||
cover: [
|
||||
Thumbnail(
|
||||
url: "https://lh3.googleusercontent.com/ilzR9UxpZFwHZnYOL0L504H6a0Y8k_zPk0AYOhBiBqIjq4TGnX-B1uKcNah56dmjPZoDvp9vGWyfgY8=w226-h226-l90-rj",
|
||||
width: 226,
|
||||
height: 226,
|
||||
),
|
||||
Thumbnail(
|
||||
url: "https://lh3.googleusercontent.com/ilzR9UxpZFwHZnYOL0L504H6a0Y8k_zPk0AYOhBiBqIjq4TGnX-B1uKcNah56dmjPZoDvp9vGWyfgY8=w544-h544-l90-rj",
|
||||
width: 544,
|
||||
height: 544,
|
||||
),
|
||||
],
|
||||
artists: [
|
||||
ArtistId(
|
||||
id: Some("UCOR4_bSVIXPsGa4BbCSt60Q"),
|
||||
name: "Trailerpark",
|
||||
),
|
||||
],
|
||||
artist_id: Some("UCOR4_bSVIXPsGa4BbCSt60Q"),
|
||||
album_type: album,
|
||||
year: Some(2024),
|
||||
by_va: false,
|
||||
),
|
||||
AlbumItem(
|
||||
id: "MPREb_HPXN9BBzFpV",
|
||||
name: "TP4L",
|
||||
cover: [
|
||||
Thumbnail(
|
||||
url: "https://lh3.googleusercontent.com/8Ftr5oIt1q6RbGkdiW7cefw-XGUplUXcjXXN7QntI1Nzh_6oR0euh7Lj2Ner3yXV--U-hVxJewkeq8A=w226-h226-l90-rj",
|
||||
width: 226,
|
||||
height: 226,
|
||||
),
|
||||
Thumbnail(
|
||||
url: "https://lh3.googleusercontent.com/8Ftr5oIt1q6RbGkdiW7cefw-XGUplUXcjXXN7QntI1Nzh_6oR0euh7Lj2Ner3yXV--U-hVxJewkeq8A=w544-h544-l90-rj",
|
||||
width: 544,
|
||||
height: 544,
|
||||
),
|
||||
],
|
||||
artists: [
|
||||
ArtistId(
|
||||
id: Some("UCOR4_bSVIXPsGa4BbCSt60Q"),
|
||||
name: "Trailerpark",
|
||||
),
|
||||
],
|
||||
artist_id: Some("UCOR4_bSVIXPsGa4BbCSt60Q"),
|
||||
album_type: single,
|
||||
year: Some(2017),
|
||||
by_va: false,
|
||||
),
|
||||
AlbumItem(
|
||||
id: "MPREb_hcK0fXETEf9",
|
||||
name: "Endlich normale Leute",
|
||||
cover: [
|
||||
Thumbnail(
|
||||
url: "https://lh3.googleusercontent.com/MW37LppS1rjDQIl5GaG0BxKeWk5fs4xphr6rU0z-KmJiHbvMbA3K5ZzrA9avinP2LjNrDGwB5tSLLsqe=w226-h226-l90-rj",
|
||||
width: 226,
|
||||
height: 226,
|
||||
),
|
||||
Thumbnail(
|
||||
url: "https://lh3.googleusercontent.com/MW37LppS1rjDQIl5GaG0BxKeWk5fs4xphr6rU0z-KmJiHbvMbA3K5ZzrA9avinP2LjNrDGwB5tSLLsqe=w544-h544-l90-rj",
|
||||
width: 544,
|
||||
height: 544,
|
||||
),
|
||||
],
|
||||
artists: [
|
||||
ArtistId(
|
||||
id: Some("UCOR4_bSVIXPsGa4BbCSt60Q"),
|
||||
name: "Trailerpark",
|
||||
),
|
||||
],
|
||||
artist_id: Some("UCOR4_bSVIXPsGa4BbCSt60Q"),
|
||||
album_type: single,
|
||||
year: Some(2017),
|
||||
by_va: false,
|
||||
),
|
||||
AlbumItem(
|
||||
id: "MPREb_R6EV2L1q0oc",
|
||||
name: "Armut treibt Jugendliche in die Popmusik",
|
||||
cover: [
|
||||
Thumbnail(
|
||||
url: "https://lh3.googleusercontent.com/kqKBF4JPQhKY1099AzRpJFGc2P7TFuFa2GeM7z8GGfTJ_DkfAzKTdV8gPtfVkyA5HQ0uZn3XG-VtMVj0=w226-h226-l90-rj",
|
||||
width: 226,
|
||||
height: 226,
|
||||
),
|
||||
Thumbnail(
|
||||
url: "https://lh3.googleusercontent.com/kqKBF4JPQhKY1099AzRpJFGc2P7TFuFa2GeM7z8GGfTJ_DkfAzKTdV8gPtfVkyA5HQ0uZn3XG-VtMVj0=w544-h544-l90-rj",
|
||||
width: 544,
|
||||
height: 544,
|
||||
),
|
||||
],
|
||||
artists: [
|
||||
ArtistId(
|
||||
id: Some("UCOR4_bSVIXPsGa4BbCSt60Q"),
|
||||
name: "Trailerpark",
|
||||
),
|
||||
],
|
||||
artist_id: Some("UCOR4_bSVIXPsGa4BbCSt60Q"),
|
||||
album_type: single,
|
||||
year: Some(2017),
|
||||
by_va: false,
|
||||
),
|
||||
AlbumItem(
|
||||
id: "MPREb_oHieBHkXn3A",
|
||||
name: "Dicks Sucken",
|
||||
cover: [
|
||||
Thumbnail(
|
||||
url: "https://lh3.googleusercontent.com/IVvdOUgbTECe2cVKrwhhCYmhHuipV6p0t5cLqMYWm3E_23zBEABxodGiSuX3H_AxRcEZk2-4V-k3RZw6=w226-h226-l90-rj",
|
||||
width: 226,
|
||||
height: 226,
|
||||
),
|
||||
Thumbnail(
|
||||
url: "https://lh3.googleusercontent.com/IVvdOUgbTECe2cVKrwhhCYmhHuipV6p0t5cLqMYWm3E_23zBEABxodGiSuX3H_AxRcEZk2-4V-k3RZw6=w544-h544-l90-rj",
|
||||
width: 544,
|
||||
height: 544,
|
||||
),
|
||||
],
|
||||
artists: [
|
||||
ArtistId(
|
||||
id: Some("UCOR4_bSVIXPsGa4BbCSt60Q"),
|
||||
name: "Trailerpark",
|
||||
),
|
||||
],
|
||||
artist_id: Some("UCOR4_bSVIXPsGa4BbCSt60Q"),
|
||||
album_type: single,
|
||||
year: Some(2014),
|
||||
by_va: false,
|
||||
),
|
||||
AlbumItem(
|
||||
id: "MPREb_8PsIyll0LFV",
|
||||
name: "Bleib in der Schule",
|
||||
cover: [
|
||||
Thumbnail(
|
||||
url: "https://lh3.googleusercontent.com/V_tvMqbuXgDgoAKuYZ-VFRru3cUb2WQvwO6vVBKY8pdFYAl1dkuIv_W2afjMUNN6uVNxet6r7mHISh0s=w226-h226-l90-rj",
|
||||
width: 226,
|
||||
height: 226,
|
||||
),
|
||||
Thumbnail(
|
||||
url: "https://lh3.googleusercontent.com/V_tvMqbuXgDgoAKuYZ-VFRru3cUb2WQvwO6vVBKY8pdFYAl1dkuIv_W2afjMUNN6uVNxet6r7mHISh0s=w544-h544-l90-rj",
|
||||
width: 544,
|
||||
height: 544,
|
||||
),
|
||||
],
|
||||
artists: [
|
||||
ArtistId(
|
||||
id: Some("UCOR4_bSVIXPsGa4BbCSt60Q"),
|
||||
name: "Trailerpark",
|
||||
),
|
||||
],
|
||||
artist_id: Some("UCOR4_bSVIXPsGa4BbCSt60Q"),
|
||||
album_type: single,
|
||||
year: Some(2014),
|
||||
by_va: false,
|
||||
),
|
||||
AlbumItem(
|
||||
id: "MPREb_POeT6m0bw9q",
|
||||
name: "Crackstreet Boys II X Version",
|
||||
cover: [
|
||||
Thumbnail(
|
||||
url: "https://lh3.googleusercontent.com/1fPBoTszY4e6Nf8egSwBTHWsQT8hotwhDnjArd1SHS8gZc5asCoo_3Z2WhN1IO2KMqyYly0xm7mMZ43d=w226-h226-l90-rj",
|
||||
width: 226,
|
||||
height: 226,
|
||||
),
|
||||
Thumbnail(
|
||||
url: "https://lh3.googleusercontent.com/1fPBoTszY4e6Nf8egSwBTHWsQT8hotwhDnjArd1SHS8gZc5asCoo_3Z2WhN1IO2KMqyYly0xm7mMZ43d=w544-h544-l90-rj",
|
||||
width: 544,
|
||||
height: 544,
|
||||
),
|
||||
],
|
||||
artists: [
|
||||
ArtistId(
|
||||
id: Some("UCOR4_bSVIXPsGa4BbCSt60Q"),
|
||||
name: "Trailerpark",
|
||||
),
|
||||
],
|
||||
artist_id: Some("UCOR4_bSVIXPsGa4BbCSt60Q"),
|
||||
album_type: ep,
|
||||
year: Some(2014),
|
||||
by_va: false,
|
||||
),
|
||||
AlbumItem(
|
||||
id: "MPREb_tdFqP579jQz",
|
||||
name: "Bleib in der Schule (Live in Berlin)",
|
||||
cover: [
|
||||
Thumbnail(
|
||||
url: "https://lh3.googleusercontent.com/VNjspSA1Fm0yFJEKUCuetOziiET6sQG9QXQCiydknEny98Lc_MEmUp8e37FtCbDz1bQ6yvM6AqpsvL0=w226-h226-l90-rj",
|
||||
width: 226,
|
||||
height: 226,
|
||||
),
|
||||
Thumbnail(
|
||||
url: "https://lh3.googleusercontent.com/VNjspSA1Fm0yFJEKUCuetOziiET6sQG9QXQCiydknEny98Lc_MEmUp8e37FtCbDz1bQ6yvM6AqpsvL0=w544-h544-l90-rj",
|
||||
width: 544,
|
||||
height: 544,
|
||||
),
|
||||
],
|
||||
artists: [
|
||||
ArtistId(
|
||||
id: Some("UCOR4_bSVIXPsGa4BbCSt60Q"),
|
||||
name: "Trailerpark",
|
||||
),
|
||||
],
|
||||
artist_id: Some("UCOR4_bSVIXPsGa4BbCSt60Q"),
|
||||
album_type: single,
|
||||
year: Some(2024),
|
||||
by_va: false,
|
||||
),
|
||||
AlbumItem(
|
||||
id: "MPREb_kLvmX2AzYBL",
|
||||
name: "Bleib in der Schule (Live at Wacken 2019)",
|
||||
cover: [
|
||||
Thumbnail(
|
||||
url: "https://lh3.googleusercontent.com/dV3PCeAdRQgLOuSUdIfA4q8jVgNwSoTceeK085ZOCzEe6YBm5c9gNIvO8wGM_K2NKpip-8-PxJtWEPJo=w226-h226-l90-rj",
|
||||
width: 226,
|
||||
height: 226,
|
||||
),
|
||||
Thumbnail(
|
||||
url: "https://lh3.googleusercontent.com/dV3PCeAdRQgLOuSUdIfA4q8jVgNwSoTceeK085ZOCzEe6YBm5c9gNIvO8wGM_K2NKpip-8-PxJtWEPJo=w544-h544-l90-rj",
|
||||
width: 544,
|
||||
height: 544,
|
||||
),
|
||||
],
|
||||
artists: [
|
||||
ArtistId(
|
||||
id: Some("UCOR4_bSVIXPsGa4BbCSt60Q"),
|
||||
name: "Trailerpark",
|
||||
),
|
||||
],
|
||||
artist_id: Some("UCOR4_bSVIXPsGa4BbCSt60Q"),
|
||||
album_type: single,
|
||||
year: Some(2014),
|
||||
by_va: false,
|
||||
),
|
||||
],
|
||||
playlists: [],
|
||||
similar_artists: [
|
||||
ArtistItem(
|
||||
id: "UCVRREKn7V1Cb8qvf43dwZ6w",
|
||||
name: "257ers",
|
||||
avatar: [
|
||||
Thumbnail(
|
||||
url: "https://lh3.googleusercontent.com/yPjiQ4ZVblOXbft1Yo2jd3uJXKJDuSWOP1MCAG6kTIwYqTWsOKRbZBnPhW4gjzvvVll7yVtjbu3e3Q=w226-h226-l90-rj",
|
||||
width: 226,
|
||||
height: 226,
|
||||
),
|
||||
Thumbnail(
|
||||
url: "https://lh3.googleusercontent.com/yPjiQ4ZVblOXbft1Yo2jd3uJXKJDuSWOP1MCAG6kTIwYqTWsOKRbZBnPhW4gjzvvVll7yVtjbu3e3Q=w544-h544-l90-rj",
|
||||
width: 544,
|
||||
height: 544,
|
||||
),
|
||||
],
|
||||
subscriber_count: Some(67300),
|
||||
),
|
||||
ArtistItem(
|
||||
id: "UCuNyvmBfTzQZmWI2rsVX3QQ",
|
||||
name: "Alligatoah",
|
||||
avatar: [
|
||||
Thumbnail(
|
||||
url: "https://lh3.googleusercontent.com/ffIVPiIldrcfp9UEoAbDid6fnAOajn_kgI4OisFoFhK28rk3HVdpYfe2h27T3d_hHfNR943PPSOhHw=w226-h226-p-l90-rj",
|
||||
width: 226,
|
||||
height: 226,
|
||||
),
|
||||
Thumbnail(
|
||||
url: "https://lh3.googleusercontent.com/ffIVPiIldrcfp9UEoAbDid6fnAOajn_kgI4OisFoFhK28rk3HVdpYfe2h27T3d_hHfNR943PPSOhHw=w544-h544-p-l90-rj",
|
||||
width: 544,
|
||||
height: 544,
|
||||
),
|
||||
],
|
||||
subscriber_count: Some(779000),
|
||||
),
|
||||
ArtistItem(
|
||||
id: "UCO04sIqN7F4ff2-1ycVZSgQ",
|
||||
name: "Sudden",
|
||||
avatar: [
|
||||
Thumbnail(
|
||||
url: "https://lh3.googleusercontent.com/TEdMt2cE-UCbnjm6AJtyasWv9-a3LFpdmh2X6w3iBwIMATHUtYIQ_F0cJ30vL5m6uJkqL3qFvNYLpYrN=w226-h226-l90-rj",
|
||||
width: 226,
|
||||
height: 226,
|
||||
),
|
||||
Thumbnail(
|
||||
url: "https://lh3.googleusercontent.com/TEdMt2cE-UCbnjm6AJtyasWv9-a3LFpdmh2X6w3iBwIMATHUtYIQ_F0cJ30vL5m6uJkqL3qFvNYLpYrN=w544-h544-l90-rj",
|
||||
width: 544,
|
||||
height: 544,
|
||||
),
|
||||
],
|
||||
subscriber_count: Some(3660),
|
||||
),
|
||||
ArtistItem(
|
||||
id: "UC5k_3LEPSGchsGEGpqoF6dg",
|
||||
name: "K.I.Z",
|
||||
avatar: [
|
||||
Thumbnail(
|
||||
url: "https://lh3.googleusercontent.com/PVaIRDAgRRyLMuFp7OTS7h3HEMoY9ejKxt7GLgfgi6aFt3bP-Edb1YU5t1IlGN0Z-qcrb86qspETNoI=w226-h226-p-l90-rj",
|
||||
width: 226,
|
||||
height: 226,
|
||||
),
|
||||
Thumbnail(
|
||||
url: "https://lh3.googleusercontent.com/PVaIRDAgRRyLMuFp7OTS7h3HEMoY9ejKxt7GLgfgi6aFt3bP-Edb1YU5t1IlGN0Z-qcrb86qspETNoI=w544-h544-p-l90-rj",
|
||||
width: 544,
|
||||
height: 544,
|
||||
),
|
||||
],
|
||||
subscriber_count: Some(522000),
|
||||
),
|
||||
ArtistItem(
|
||||
id: "UCG8K_22LRSRwqhoJXBWGmbA",
|
||||
name: "FiNCH",
|
||||
avatar: [
|
||||
Thumbnail(
|
||||
url: "https://lh3.googleusercontent.com/cofqKPsHr5dzuLexkKAYQF3vVMkKTT2FuZgIMXs6XIO3J8diK29qqfKQkqrga8NOCmwVl7x-w4z3mQ=w226-h226-p-l90-rj",
|
||||
width: 226,
|
||||
height: 226,
|
||||
),
|
||||
Thumbnail(
|
||||
url: "https://lh3.googleusercontent.com/cofqKPsHr5dzuLexkKAYQF3vVMkKTT2FuZgIMXs6XIO3J8diK29qqfKQkqrga8NOCmwVl7x-w4z3mQ=w544-h544-p-l90-rj",
|
||||
width: 544,
|
||||
height: 544,
|
||||
),
|
||||
],
|
||||
subscriber_count: Some(533000),
|
||||
),
|
||||
ArtistItem(
|
||||
id: "UC5HSrFHr6lMzwAyGjlClm0A",
|
||||
name: "Timi Hendrix",
|
||||
avatar: [
|
||||
Thumbnail(
|
||||
url: "https://lh3.googleusercontent.com/1yi83YgKDBSQ0rgsA2GuZRa0rBABPR2BH41DsuCfGMRmLdF9oR7vv7T6QGLbhNP8FfX6qVHUQci4YM8=w226-h226-p-l90-rj",
|
||||
width: 226,
|
||||
height: 226,
|
||||
),
|
||||
Thumbnail(
|
||||
url: "https://lh3.googleusercontent.com/1yi83YgKDBSQ0rgsA2GuZRa0rBABPR2BH41DsuCfGMRmLdF9oR7vv7T6QGLbhNP8FfX6qVHUQci4YM8=w544-h544-p-l90-rj",
|
||||
width: 544,
|
||||
height: 544,
|
||||
),
|
||||
],
|
||||
subscriber_count: Some(6410),
|
||||
),
|
||||
ArtistItem(
|
||||
id: "UC9izv9vxcTVKA1IibcGTrNA",
|
||||
name: "Pimpulsiv",
|
||||
avatar: [
|
||||
Thumbnail(
|
||||
url: "https://lh3.googleusercontent.com/QXuirXSQsdO1KUZCz-ZX-kRVSorZxIUC4YrxQD0IeSr1mY-42VwvAjf4TTownRVzm-02-U8kLM3VuETf9w=w226-h226-l90-rj",
|
||||
width: 226,
|
||||
height: 226,
|
||||
),
|
||||
Thumbnail(
|
||||
url: "https://lh3.googleusercontent.com/QXuirXSQsdO1KUZCz-ZX-kRVSorZxIUC4YrxQD0IeSr1mY-42VwvAjf4TTownRVzm-02-U8kLM3VuETf9w=w544-h544-l90-rj",
|
||||
width: 544,
|
||||
height: 544,
|
||||
),
|
||||
],
|
||||
subscriber_count: Some(985),
|
||||
),
|
||||
ArtistItem(
|
||||
id: "UCgosMU69MpoCqhuS1JZj6Cw",
|
||||
name: "Sido",
|
||||
avatar: [
|
||||
Thumbnail(
|
||||
url: "https://lh3.googleusercontent.com/HZpnexwxNS5FkIrpz6hdHZuNhBS-GKjs0C9NU8nDSTmHFlPaviqxV-dDLS_ubSEbpEvu0m2P2WT3kaQ=w226-h226-p-l90-rj",
|
||||
width: 226,
|
||||
height: 226,
|
||||
),
|
||||
Thumbnail(
|
||||
url: "https://lh3.googleusercontent.com/HZpnexwxNS5FkIrpz6hdHZuNhBS-GKjs0C9NU8nDSTmHFlPaviqxV-dDLS_ubSEbpEvu0m2P2WT3kaQ=w544-h544-p-l90-rj",
|
||||
width: 544,
|
||||
height: 544,
|
||||
),
|
||||
],
|
||||
subscriber_count: Some(1550000),
|
||||
),
|
||||
ArtistItem(
|
||||
id: "UCAiLb3B6iCjxv7HhPf1S4ag",
|
||||
name: "Marteria",
|
||||
avatar: [
|
||||
Thumbnail(
|
||||
url: "https://lh3.googleusercontent.com/Ms5gYOttabL03qfFYx7SNhRsx-K_Y7hxMN0WXgc7iquYAfLV5cgYZfTBn3nsi0_sN5BaqAaIr1z5iGc=w226-h226-p-l90-rj",
|
||||
width: 226,
|
||||
height: 226,
|
||||
),
|
||||
Thumbnail(
|
||||
url: "https://lh3.googleusercontent.com/Ms5gYOttabL03qfFYx7SNhRsx-K_Y7hxMN0WXgc7iquYAfLV5cgYZfTBn3nsi0_sN5BaqAaIr1z5iGc=w544-h544-p-l90-rj",
|
||||
width: 544,
|
||||
height: 544,
|
||||
),
|
||||
],
|
||||
subscriber_count: Some(422000),
|
||||
),
|
||||
ArtistItem(
|
||||
id: "UCtoec88rzlhABHeo_4d-H8g",
|
||||
name: "Dame",
|
||||
avatar: [
|
||||
Thumbnail(
|
||||
url: "https://lh3.googleusercontent.com/lkbE9cB4qTxtRmzkjAaLEHrpIgeCOzeBXaL4BpBRq6wp4PlCoSIFej3ita3du8lqniIA67NRYfsVwuFj=w226-h226-p-l90-rj",
|
||||
width: 226,
|
||||
height: 226,
|
||||
),
|
||||
Thumbnail(
|
||||
url: "https://lh3.googleusercontent.com/lkbE9cB4qTxtRmzkjAaLEHrpIgeCOzeBXaL4BpBRq6wp4PlCoSIFej3ita3du8lqniIA67NRYfsVwuFj=w544-h544-p-l90-rj",
|
||||
width: 544,
|
||||
height: 544,
|
||||
),
|
||||
],
|
||||
subscriber_count: Some(37700),
|
||||
),
|
||||
],
|
||||
tracks_playlist_id: Some("OLAK5uy_miHesZCUQY5S9EwqfoNP2tZR9nZ0NBAeU"),
|
||||
videos_playlist_id: Some("OLAK5uy_mqbgE6T9uvusUWrAxJGiImf4_P4dM7IvQ"),
|
||||
radio_id: Some("RDEM7AbogW0cCnElSU0WYm1GqA"),
|
||||
)
|
File diff suppressed because it is too large
Load diff
|
@ -59,7 +59,6 @@ MusicArtist(
|
|||
)),
|
||||
track_count: None,
|
||||
from_ytm: false,
|
||||
is_podcast: false,
|
||||
),
|
||||
MusicPlaylistItem(
|
||||
id: "PLwkM1QxaP343YqeP6g5VPGsgJdO1_SV4I",
|
||||
|
@ -82,7 +81,6 @@ MusicArtist(
|
|||
)),
|
||||
track_count: None,
|
||||
from_ytm: false,
|
||||
is_podcast: false,
|
||||
),
|
||||
MusicPlaylistItem(
|
||||
id: "PLwkM1QxaP340xbkARIPpiD1aHuzJVuZUg",
|
||||
|
@ -105,7 +103,6 @@ MusicArtist(
|
|||
)),
|
||||
track_count: None,
|
||||
from_ytm: false,
|
||||
is_podcast: false,
|
||||
),
|
||||
MusicPlaylistItem(
|
||||
id: "PLwkM1QxaP342hjju64dtqG5wKqx2hNgjr",
|
||||
|
@ -128,7 +125,6 @@ MusicArtist(
|
|||
)),
|
||||
track_count: None,
|
||||
from_ytm: false,
|
||||
is_podcast: false,
|
||||
),
|
||||
MusicPlaylistItem(
|
||||
id: "PLwkM1QxaP342v1hhoB3XLiruSQOzmdmBt",
|
||||
|
@ -151,7 +147,6 @@ MusicArtist(
|
|||
)),
|
||||
track_count: None,
|
||||
from_ytm: false,
|
||||
is_podcast: false,
|
||||
),
|
||||
MusicPlaylistItem(
|
||||
id: "PLwkM1QxaP342EBMza0AG10nB3oDD65RPY",
|
||||
|
@ -174,7 +169,6 @@ MusicArtist(
|
|||
)),
|
||||
track_count: None,
|
||||
from_ytm: false,
|
||||
is_podcast: false,
|
||||
),
|
||||
MusicPlaylistItem(
|
||||
id: "PLwkM1QxaP342nVAeBVL6_Q8gbbAD8l4wb",
|
||||
|
@ -197,7 +191,6 @@ MusicArtist(
|
|||
)),
|
||||
track_count: None,
|
||||
from_ytm: false,
|
||||
is_podcast: false,
|
||||
),
|
||||
MusicPlaylistItem(
|
||||
id: "PLwkM1QxaP3438x6ta8VJZlJSlDn43FueA",
|
||||
|
@ -220,7 +213,6 @@ MusicArtist(
|
|||
)),
|
||||
track_count: None,
|
||||
from_ytm: false,
|
||||
is_podcast: false,
|
||||
),
|
||||
],
|
||||
similar_artists: [],
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -64,7 +64,7 @@ MusicArtist(
|
|||
name: "Evolve",
|
||||
)),
|
||||
view_count: None,
|
||||
track_type: track,
|
||||
is_video: false,
|
||||
track_nr: None,
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -96,7 +96,7 @@ MusicArtist(
|
|||
name: "Mercury : Acts 1 & 2",
|
||||
)),
|
||||
view_count: None,
|
||||
track_type: track,
|
||||
is_video: false,
|
||||
track_nr: None,
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -140,7 +140,7 @@ MusicArtist(
|
|||
name: "Mercury : Acts 1 & 2",
|
||||
)),
|
||||
view_count: None,
|
||||
track_type: track,
|
||||
is_video: false,
|
||||
track_nr: None,
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -172,7 +172,7 @@ MusicArtist(
|
|||
name: "Evolve",
|
||||
)),
|
||||
view_count: None,
|
||||
track_type: track,
|
||||
is_video: false,
|
||||
track_nr: None,
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -204,7 +204,7 @@ MusicArtist(
|
|||
name: "Night Visions",
|
||||
)),
|
||||
view_count: None,
|
||||
track_type: track,
|
||||
is_video: false,
|
||||
track_nr: None,
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -233,7 +233,7 @@ MusicArtist(
|
|||
artist_id: Some("UC0aXrjVxG5pZr99v77wZdPQ"),
|
||||
album: None,
|
||||
view_count: Some(2100000),
|
||||
track_type: video,
|
||||
is_video: true,
|
||||
track_nr: None,
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -262,7 +262,7 @@ MusicArtist(
|
|||
artist_id: Some("UC0aXrjVxG5pZr99v77wZdPQ"),
|
||||
album: None,
|
||||
view_count: Some(2400000000),
|
||||
track_type: video,
|
||||
is_video: true,
|
||||
track_nr: None,
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -291,7 +291,7 @@ MusicArtist(
|
|||
artist_id: Some("UC0aXrjVxG5pZr99v77wZdPQ"),
|
||||
album: None,
|
||||
view_count: Some(207000000),
|
||||
track_type: video,
|
||||
is_video: true,
|
||||
track_nr: None,
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -320,7 +320,7 @@ MusicArtist(
|
|||
artist_id: Some("UC0aXrjVxG5pZr99v77wZdPQ"),
|
||||
album: None,
|
||||
view_count: Some(324000000),
|
||||
track_type: video,
|
||||
is_video: true,
|
||||
track_nr: None,
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -349,7 +349,7 @@ MusicArtist(
|
|||
artist_id: Some("UC0aXrjVxG5pZr99v77wZdPQ"),
|
||||
album: None,
|
||||
view_count: Some(1900000000),
|
||||
track_type: video,
|
||||
is_video: true,
|
||||
track_nr: None,
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -378,7 +378,7 @@ MusicArtist(
|
|||
artist_id: Some("UC0aXrjVxG5pZr99v77wZdPQ"),
|
||||
album: None,
|
||||
view_count: Some(1000000000),
|
||||
track_type: video,
|
||||
is_video: true,
|
||||
track_nr: None,
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -407,7 +407,7 @@ MusicArtist(
|
|||
artist_id: Some("UC0aXrjVxG5pZr99v77wZdPQ"),
|
||||
album: None,
|
||||
view_count: Some(1400000000),
|
||||
track_type: video,
|
||||
is_video: true,
|
||||
track_nr: None,
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -431,7 +431,7 @@ MusicArtist(
|
|||
artist_id: Some("UC0aXrjVxG5pZr99v77wZdPQ"),
|
||||
album: None,
|
||||
view_count: Some(440000000),
|
||||
track_type: video,
|
||||
is_video: true,
|
||||
track_nr: None,
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -460,7 +460,7 @@ MusicArtist(
|
|||
artist_id: Some("UC0aXrjVxG5pZr99v77wZdPQ"),
|
||||
album: None,
|
||||
view_count: Some(557000000),
|
||||
track_type: video,
|
||||
is_video: true,
|
||||
track_nr: None,
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -489,7 +489,7 @@ MusicArtist(
|
|||
artist_id: Some("UC0aXrjVxG5pZr99v77wZdPQ"),
|
||||
album: None,
|
||||
view_count: Some(877000000),
|
||||
track_type: video,
|
||||
is_video: true,
|
||||
track_nr: None,
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -517,7 +517,7 @@ MusicArtist(
|
|||
),
|
||||
],
|
||||
artist_id: Some("UC0aXrjVxG5pZr99v77wZdPQ"),
|
||||
album_type: album,
|
||||
album_type: Album,
|
||||
year: Some(2022),
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -543,7 +543,7 @@ MusicArtist(
|
|||
),
|
||||
],
|
||||
artist_id: Some("UC0aXrjVxG5pZr99v77wZdPQ"),
|
||||
album_type: single,
|
||||
album_type: Single,
|
||||
year: Some(2022),
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -569,7 +569,7 @@ MusicArtist(
|
|||
),
|
||||
],
|
||||
artist_id: Some("UC0aXrjVxG5pZr99v77wZdPQ"),
|
||||
album_type: album,
|
||||
album_type: Album,
|
||||
year: Some(2022),
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -595,7 +595,7 @@ MusicArtist(
|
|||
),
|
||||
],
|
||||
artist_id: Some("UC0aXrjVxG5pZr99v77wZdPQ"),
|
||||
album_type: single,
|
||||
album_type: Single,
|
||||
year: Some(2022),
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -621,7 +621,7 @@ MusicArtist(
|
|||
),
|
||||
],
|
||||
artist_id: Some("UC0aXrjVxG5pZr99v77wZdPQ"),
|
||||
album_type: single,
|
||||
album_type: Single,
|
||||
year: Some(2022),
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -647,7 +647,7 @@ MusicArtist(
|
|||
),
|
||||
],
|
||||
artist_id: Some("UC0aXrjVxG5pZr99v77wZdPQ"),
|
||||
album_type: single,
|
||||
album_type: Single,
|
||||
year: Some(2021),
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -673,7 +673,7 @@ MusicArtist(
|
|||
),
|
||||
],
|
||||
artist_id: Some("UC0aXrjVxG5pZr99v77wZdPQ"),
|
||||
album_type: single,
|
||||
album_type: Single,
|
||||
year: Some(2021),
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -699,7 +699,7 @@ MusicArtist(
|
|||
),
|
||||
],
|
||||
artist_id: Some("UC0aXrjVxG5pZr99v77wZdPQ"),
|
||||
album_type: single,
|
||||
album_type: Single,
|
||||
year: Some(2021),
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -725,7 +725,7 @@ MusicArtist(
|
|||
),
|
||||
],
|
||||
artist_id: Some("UC0aXrjVxG5pZr99v77wZdPQ"),
|
||||
album_type: single,
|
||||
album_type: Single,
|
||||
year: Some(2021),
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -751,7 +751,7 @@ MusicArtist(
|
|||
),
|
||||
],
|
||||
artist_id: Some("UC0aXrjVxG5pZr99v77wZdPQ"),
|
||||
album_type: single,
|
||||
album_type: Single,
|
||||
year: Some(2021),
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -777,7 +777,7 @@ MusicArtist(
|
|||
),
|
||||
],
|
||||
artist_id: Some("UC0aXrjVxG5pZr99v77wZdPQ"),
|
||||
album_type: single,
|
||||
album_type: Single,
|
||||
year: Some(2019),
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -803,7 +803,7 @@ MusicArtist(
|
|||
),
|
||||
],
|
||||
artist_id: Some("UC0aXrjVxG5pZr99v77wZdPQ"),
|
||||
album_type: album,
|
||||
album_type: Album,
|
||||
year: Some(2018),
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -829,7 +829,7 @@ MusicArtist(
|
|||
),
|
||||
],
|
||||
artist_id: Some("UC0aXrjVxG5pZr99v77wZdPQ"),
|
||||
album_type: single,
|
||||
album_type: Single,
|
||||
year: Some(2018),
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -855,7 +855,7 @@ MusicArtist(
|
|||
),
|
||||
],
|
||||
artist_id: Some("UC0aXrjVxG5pZr99v77wZdPQ"),
|
||||
album_type: single,
|
||||
album_type: Single,
|
||||
year: Some(2018),
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -881,7 +881,7 @@ MusicArtist(
|
|||
),
|
||||
],
|
||||
artist_id: Some("UC0aXrjVxG5pZr99v77wZdPQ"),
|
||||
album_type: single,
|
||||
album_type: Single,
|
||||
year: Some(2018),
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -907,7 +907,7 @@ MusicArtist(
|
|||
),
|
||||
],
|
||||
artist_id: Some("UC0aXrjVxG5pZr99v77wZdPQ"),
|
||||
album_type: single,
|
||||
album_type: Single,
|
||||
year: Some(2017),
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -933,7 +933,7 @@ MusicArtist(
|
|||
),
|
||||
],
|
||||
artist_id: Some("UC0aXrjVxG5pZr99v77wZdPQ"),
|
||||
album_type: album,
|
||||
album_type: Album,
|
||||
year: Some(2017),
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -959,7 +959,7 @@ MusicArtist(
|
|||
),
|
||||
],
|
||||
artist_id: Some("UC0aXrjVxG5pZr99v77wZdPQ"),
|
||||
album_type: single,
|
||||
album_type: Single,
|
||||
year: Some(2017),
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -985,7 +985,7 @@ MusicArtist(
|
|||
),
|
||||
],
|
||||
artist_id: Some("UC0aXrjVxG5pZr99v77wZdPQ"),
|
||||
album_type: single,
|
||||
album_type: Single,
|
||||
year: Some(2016),
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -1011,7 +1011,7 @@ MusicArtist(
|
|||
),
|
||||
],
|
||||
artist_id: Some("UC0aXrjVxG5pZr99v77wZdPQ"),
|
||||
album_type: single,
|
||||
album_type: Single,
|
||||
year: Some(2016),
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -1037,7 +1037,7 @@ MusicArtist(
|
|||
),
|
||||
],
|
||||
artist_id: Some("UC0aXrjVxG5pZr99v77wZdPQ"),
|
||||
album_type: single,
|
||||
album_type: Single,
|
||||
year: Some(2015),
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -1063,7 +1063,7 @@ MusicArtist(
|
|||
),
|
||||
],
|
||||
artist_id: Some("UC0aXrjVxG5pZr99v77wZdPQ"),
|
||||
album_type: single,
|
||||
album_type: Single,
|
||||
year: Some(2015),
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -1089,7 +1089,7 @@ MusicArtist(
|
|||
),
|
||||
],
|
||||
artist_id: Some("UC0aXrjVxG5pZr99v77wZdPQ"),
|
||||
album_type: album,
|
||||
album_type: Album,
|
||||
year: Some(2014),
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -1115,7 +1115,7 @@ MusicArtist(
|
|||
),
|
||||
],
|
||||
artist_id: Some("UC0aXrjVxG5pZr99v77wZdPQ"),
|
||||
album_type: album,
|
||||
album_type: Album,
|
||||
year: Some(2011),
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -1141,7 +1141,7 @@ MusicArtist(
|
|||
),
|
||||
],
|
||||
artist_id: Some("UC0aXrjVxG5pZr99v77wZdPQ"),
|
||||
album_type: ep,
|
||||
album_type: Ep,
|
||||
year: Some(2010),
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -1167,7 +1167,7 @@ MusicArtist(
|
|||
),
|
||||
],
|
||||
artist_id: Some("UC0aXrjVxG5pZr99v77wZdPQ"),
|
||||
album_type: ep,
|
||||
album_type: Ep,
|
||||
year: Some(2009),
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -1193,7 +1193,7 @@ MusicArtist(
|
|||
),
|
||||
],
|
||||
artist_id: Some("UC0aXrjVxG5pZr99v77wZdPQ"),
|
||||
album_type: ep,
|
||||
album_type: Ep,
|
||||
year: Some(2017),
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -1219,7 +1219,7 @@ MusicArtist(
|
|||
),
|
||||
],
|
||||
artist_id: Some("UC0aXrjVxG5pZr99v77wZdPQ"),
|
||||
album_type: single,
|
||||
album_type: Single,
|
||||
year: Some(2022),
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -1245,7 +1245,7 @@ MusicArtist(
|
|||
),
|
||||
],
|
||||
artist_id: Some("UC0aXrjVxG5pZr99v77wZdPQ"),
|
||||
album_type: single,
|
||||
album_type: Single,
|
||||
year: Some(2021),
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -1271,7 +1271,7 @@ MusicArtist(
|
|||
),
|
||||
],
|
||||
artist_id: Some("UC0aXrjVxG5pZr99v77wZdPQ"),
|
||||
album_type: single,
|
||||
album_type: Single,
|
||||
year: Some(2017),
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -1297,7 +1297,7 @@ MusicArtist(
|
|||
),
|
||||
],
|
||||
artist_id: Some("UC0aXrjVxG5pZr99v77wZdPQ"),
|
||||
album_type: single,
|
||||
album_type: Single,
|
||||
year: Some(2017),
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -1323,7 +1323,7 @@ MusicArtist(
|
|||
),
|
||||
],
|
||||
artist_id: Some("UC0aXrjVxG5pZr99v77wZdPQ"),
|
||||
album_type: single,
|
||||
album_type: Single,
|
||||
year: Some(2017),
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -1349,7 +1349,7 @@ MusicArtist(
|
|||
),
|
||||
],
|
||||
artist_id: Some("UC0aXrjVxG5pZr99v77wZdPQ"),
|
||||
album_type: single,
|
||||
album_type: Single,
|
||||
year: Some(2015),
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -1375,7 +1375,7 @@ MusicArtist(
|
|||
),
|
||||
],
|
||||
artist_id: Some("UC0aXrjVxG5pZr99v77wZdPQ"),
|
||||
album_type: single,
|
||||
album_type: Single,
|
||||
year: Some(2015),
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -1401,7 +1401,7 @@ MusicArtist(
|
|||
),
|
||||
],
|
||||
artist_id: Some("UC0aXrjVxG5pZr99v77wZdPQ"),
|
||||
album_type: single,
|
||||
album_type: Single,
|
||||
year: Some(2015),
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -1427,7 +1427,7 @@ MusicArtist(
|
|||
),
|
||||
],
|
||||
artist_id: Some("UC0aXrjVxG5pZr99v77wZdPQ"),
|
||||
album_type: single,
|
||||
album_type: Single,
|
||||
year: Some(2014),
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -1453,7 +1453,7 @@ MusicArtist(
|
|||
),
|
||||
],
|
||||
artist_id: Some("UC0aXrjVxG5pZr99v77wZdPQ"),
|
||||
album_type: single,
|
||||
album_type: Single,
|
||||
year: Some(2014),
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -1479,7 +1479,7 @@ MusicArtist(
|
|||
),
|
||||
],
|
||||
artist_id: Some("UC0aXrjVxG5pZr99v77wZdPQ"),
|
||||
album_type: single,
|
||||
album_type: Single,
|
||||
year: Some(2014),
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -1505,7 +1505,7 @@ MusicArtist(
|
|||
),
|
||||
],
|
||||
artist_id: Some("UC0aXrjVxG5pZr99v77wZdPQ"),
|
||||
album_type: single,
|
||||
album_type: Single,
|
||||
year: Some(2013),
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -1529,7 +1529,6 @@ MusicArtist(
|
|||
channel: None,
|
||||
track_count: None,
|
||||
from_ytm: true,
|
||||
is_podcast: false,
|
||||
),
|
||||
MusicPlaylistItem(
|
||||
id: "RDCLAK5uy_mIpIa-YIJFJe0EAcNbcMPgg-3qCdK9qAk",
|
||||
|
@ -1549,7 +1548,6 @@ MusicArtist(
|
|||
channel: None,
|
||||
track_count: None,
|
||||
from_ytm: true,
|
||||
is_podcast: false,
|
||||
),
|
||||
MusicPlaylistItem(
|
||||
id: "RDCLAK5uy_nbzJVrwitbeDjlcHvjM7fgF7khtUOoHgU",
|
||||
|
@ -1569,7 +1567,6 @@ MusicArtist(
|
|||
channel: None,
|
||||
track_count: None,
|
||||
from_ytm: true,
|
||||
is_podcast: false,
|
||||
),
|
||||
MusicPlaylistItem(
|
||||
id: "RDCLAK5uy_nCs5nAmZrJ41ILrSyf36UvOwTBNyx0oEI",
|
||||
|
@ -1589,7 +1586,6 @@ MusicArtist(
|
|||
channel: None,
|
||||
track_count: None,
|
||||
from_ytm: true,
|
||||
is_podcast: false,
|
||||
),
|
||||
MusicPlaylistItem(
|
||||
id: "RDCLAK5uy_nGXEmbtrmoUF9NG7m0WkxpF_qLKYR3YOU",
|
||||
|
@ -1609,7 +1605,6 @@ MusicArtist(
|
|||
channel: None,
|
||||
track_count: None,
|
||||
from_ytm: true,
|
||||
is_podcast: false,
|
||||
),
|
||||
MusicPlaylistItem(
|
||||
id: "RDCLAK5uy_mgHrXs_5F6wPwPFA47S8yrzCfjCi4AXDE",
|
||||
|
@ -1629,7 +1624,6 @@ MusicArtist(
|
|||
channel: None,
|
||||
track_count: None,
|
||||
from_ytm: true,
|
||||
is_podcast: false,
|
||||
),
|
||||
MusicPlaylistItem(
|
||||
id: "RDCLAK5uy_l7u7RCjtiI_I3m5EgnI-V9yWAgx0RNy1E",
|
||||
|
@ -1649,7 +1643,6 @@ MusicArtist(
|
|||
channel: None,
|
||||
track_count: None,
|
||||
from_ytm: true,
|
||||
is_podcast: false,
|
||||
),
|
||||
MusicPlaylistItem(
|
||||
id: "RDCLAK5uy_k7h5535MeHE8xmgHsrZx7HOKH4lb5vAfY",
|
||||
|
@ -1669,7 +1662,6 @@ MusicArtist(
|
|||
channel: None,
|
||||
track_count: None,
|
||||
from_ytm: true,
|
||||
is_podcast: false,
|
||||
),
|
||||
MusicPlaylistItem(
|
||||
id: "RDCLAK5uy_mlCByo5eM1tLBhUdMyn2GphTXICCM_W1w",
|
||||
|
@ -1689,7 +1681,6 @@ MusicArtist(
|
|||
channel: None,
|
||||
track_count: None,
|
||||
from_ytm: true,
|
||||
is_podcast: false,
|
||||
),
|
||||
MusicPlaylistItem(
|
||||
id: "RDCLAK5uy_ke0QH8jvXz6ynXEhn_mbCBy9m7fbnJ9NY",
|
||||
|
@ -1709,7 +1700,6 @@ MusicArtist(
|
|||
channel: None,
|
||||
track_count: None,
|
||||
from_ytm: true,
|
||||
is_podcast: false,
|
||||
),
|
||||
],
|
||||
similar_artists: [
|
||||
|
|
|
@ -64,7 +64,7 @@ MusicArtist(
|
|||
name: "고블린 Goblin",
|
||||
)),
|
||||
view_count: None,
|
||||
track_type: track,
|
||||
is_video: false,
|
||||
track_nr: None,
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -96,7 +96,7 @@ MusicArtist(
|
|||
name: "고블린 Goblin",
|
||||
)),
|
||||
view_count: None,
|
||||
track_type: track,
|
||||
is_video: false,
|
||||
track_nr: None,
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -128,7 +128,7 @@ MusicArtist(
|
|||
name: "고블린 Goblin",
|
||||
)),
|
||||
view_count: None,
|
||||
track_type: track,
|
||||
is_video: false,
|
||||
track_nr: None,
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -157,7 +157,7 @@ MusicArtist(
|
|||
artist_id: Some("UCfwCE5VhPMGxNPFxtVv7lRw"),
|
||||
album: None,
|
||||
view_count: Some(20000000),
|
||||
track_type: video,
|
||||
is_video: true,
|
||||
track_nr: None,
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -186,7 +186,7 @@ MusicArtist(
|
|||
artist_id: Some("UClGBYGUZmpzUaHgeb9gOBww"),
|
||||
album: None,
|
||||
view_count: Some(211000),
|
||||
track_type: video,
|
||||
is_video: true,
|
||||
track_nr: None,
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -215,7 +215,7 @@ MusicArtist(
|
|||
artist_id: Some("UCfaO3pZL5XOr8BvNZkrKeVA"),
|
||||
album: None,
|
||||
view_count: Some(10000),
|
||||
track_type: video,
|
||||
is_video: true,
|
||||
track_nr: None,
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -244,7 +244,7 @@ MusicArtist(
|
|||
artist_id: Some("UCgVWicpO5Jn3VfxqgIU6cpA"),
|
||||
album: None,
|
||||
view_count: Some(15000),
|
||||
track_type: video,
|
||||
is_video: true,
|
||||
track_nr: None,
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -273,7 +273,7 @@ MusicArtist(
|
|||
artist_id: Some("UCe52oeb7Xv_KaJsEzcKXJJg"),
|
||||
album: None,
|
||||
view_count: Some(1200),
|
||||
track_type: video,
|
||||
is_video: true,
|
||||
track_nr: None,
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -302,7 +302,7 @@ MusicArtist(
|
|||
artist_id: Some("UCFFvwAcyQhpeQfuAgBN1XZw"),
|
||||
album: None,
|
||||
view_count: Some(12000),
|
||||
track_type: video,
|
||||
is_video: true,
|
||||
track_nr: None,
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -331,7 +331,7 @@ MusicArtist(
|
|||
artist_id: Some("UC_xEL8cbkItBH00KrGz9fbQ"),
|
||||
album: None,
|
||||
view_count: Some(7400),
|
||||
track_type: video,
|
||||
is_video: true,
|
||||
track_nr: None,
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -360,7 +360,7 @@ MusicArtist(
|
|||
artist_id: Some("UCaFqztcJss3HrXNurzQJyqQ"),
|
||||
album: None,
|
||||
view_count: Some(1400),
|
||||
track_type: video,
|
||||
is_video: true,
|
||||
track_nr: None,
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -389,7 +389,7 @@ MusicArtist(
|
|||
artist_id: Some("UCMPqKiPdiSoi8eCW5Dou1IQ"),
|
||||
album: None,
|
||||
view_count: Some(25000),
|
||||
track_type: video,
|
||||
is_video: true,
|
||||
track_nr: None,
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -418,7 +418,7 @@ MusicArtist(
|
|||
artist_id: Some("UCe52oeb7Xv_KaJsEzcKXJJg"),
|
||||
album: None,
|
||||
view_count: Some(3700),
|
||||
track_type: video,
|
||||
is_video: true,
|
||||
track_nr: None,
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -446,7 +446,7 @@ MusicArtist(
|
|||
),
|
||||
],
|
||||
artist_id: Some("UCfwCE5VhPMGxNPFxtVv7lRw"),
|
||||
album_type: single,
|
||||
album_type: Single,
|
||||
year: Some(2019),
|
||||
by_va: false,
|
||||
),
|
||||
|
|
|
@ -33,7 +33,7 @@ MusicCharts(
|
|||
artist_id: Some("UCiXhCjTprNP0nuQJ9UsLWeg"),
|
||||
album: None,
|
||||
view_count: Some(56000000),
|
||||
track_type: video,
|
||||
is_video: true,
|
||||
track_nr: None,
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -62,7 +62,7 @@ MusicCharts(
|
|||
artist_id: Some("UCybEdRVR5u_WFoV-BLTEBiA"),
|
||||
album: None,
|
||||
view_count: Some(15000000),
|
||||
track_type: video,
|
||||
is_video: true,
|
||||
track_nr: None,
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -91,7 +91,7 @@ MusicCharts(
|
|||
artist_id: Some("UCiY3z8HAGD6BlSNKVn2kSvQ"),
|
||||
album: None,
|
||||
view_count: Some(521000000),
|
||||
track_type: video,
|
||||
is_video: true,
|
||||
track_nr: None,
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -120,7 +120,7 @@ MusicCharts(
|
|||
artist_id: Some("UCWsDFcIhY2DBi3GB5uykGXA"),
|
||||
album: None,
|
||||
view_count: Some(34000000),
|
||||
track_type: video,
|
||||
is_video: true,
|
||||
track_nr: None,
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -149,7 +149,7 @@ MusicCharts(
|
|||
artist_id: Some("UCiY3z8HAGD6BlSNKVn2kSvQ"),
|
||||
album: None,
|
||||
view_count: Some(559000000),
|
||||
track_type: video,
|
||||
is_video: true,
|
||||
track_nr: None,
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -178,7 +178,7 @@ MusicCharts(
|
|||
artist_id: Some("UCMXDyVR2tclKWhbqNforSyA"),
|
||||
album: None,
|
||||
view_count: Some(39000000),
|
||||
track_type: video,
|
||||
is_video: true,
|
||||
track_nr: None,
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -207,7 +207,7 @@ MusicCharts(
|
|||
artist_id: Some("UCJa2FF4TUB13Mm0GurZAqog"),
|
||||
album: None,
|
||||
view_count: Some(139000000),
|
||||
track_type: video,
|
||||
is_video: true,
|
||||
track_nr: None,
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -236,7 +236,7 @@ MusicCharts(
|
|||
artist_id: Some("UCKRnq8aBOCanYlffje7HyvA"),
|
||||
album: None,
|
||||
view_count: Some(311000000),
|
||||
track_type: video,
|
||||
is_video: true,
|
||||
track_nr: None,
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -265,7 +265,7 @@ MusicCharts(
|
|||
artist_id: Some("UCR28YDxjDE3ogQROaNdnRbQ"),
|
||||
album: None,
|
||||
view_count: Some(3800000),
|
||||
track_type: video,
|
||||
is_video: true,
|
||||
track_nr: None,
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -294,7 +294,7 @@ MusicCharts(
|
|||
artist_id: Some("UCpcTrCXblq78GZrTUTLWeBw"),
|
||||
album: None,
|
||||
view_count: Some(46000000),
|
||||
track_type: video,
|
||||
is_video: true,
|
||||
track_nr: None,
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -323,7 +323,7 @@ MusicCharts(
|
|||
artist_id: Some("UCJa2FF4TUB13Mm0GurZAqog"),
|
||||
album: None,
|
||||
view_count: Some(73000000),
|
||||
track_type: video,
|
||||
is_video: true,
|
||||
track_nr: None,
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -356,7 +356,7 @@ MusicCharts(
|
|||
artist_id: Some("UCohgH17dyp4c_V7U9LoBLdA"),
|
||||
album: None,
|
||||
view_count: Some(77000000),
|
||||
track_type: video,
|
||||
is_video: true,
|
||||
track_nr: None,
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -385,7 +385,7 @@ MusicCharts(
|
|||
artist_id: Some("UChWPNW87QHcXAsw2mzlsYNw"),
|
||||
album: None,
|
||||
view_count: Some(2600000),
|
||||
track_type: video,
|
||||
is_video: true,
|
||||
track_nr: None,
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -414,7 +414,7 @@ MusicCharts(
|
|||
artist_id: Some("UC_z9AthnCGSAk_tZf-KqoFA"),
|
||||
album: None,
|
||||
view_count: Some(17000000),
|
||||
track_type: video,
|
||||
is_video: true,
|
||||
track_nr: None,
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -451,7 +451,7 @@ MusicCharts(
|
|||
artist_id: Some("UCdPdi8UM25ZyvzhSJkk1uPw"),
|
||||
album: None,
|
||||
view_count: Some(8600000),
|
||||
track_type: video,
|
||||
is_video: true,
|
||||
track_nr: None,
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -480,7 +480,7 @@ MusicCharts(
|
|||
artist_id: Some("UC_z9AthnCGSAk_tZf-KqoFA"),
|
||||
album: None,
|
||||
view_count: Some(15000000),
|
||||
track_type: video,
|
||||
is_video: true,
|
||||
track_nr: None,
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -509,7 +509,7 @@ MusicCharts(
|
|||
artist_id: Some("UCXT9NWRyDfHJq9Igm1pDQpQ"),
|
||||
album: None,
|
||||
view_count: Some(31000000),
|
||||
track_type: video,
|
||||
is_video: true,
|
||||
track_nr: None,
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -542,7 +542,7 @@ MusicCharts(
|
|||
artist_id: Some("UC7n3gWRN0vQzgiOKc51aZ4w"),
|
||||
album: None,
|
||||
view_count: Some(202000000),
|
||||
track_type: video,
|
||||
is_video: true,
|
||||
track_nr: None,
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -571,7 +571,7 @@ MusicCharts(
|
|||
artist_id: Some("UCGJdT8Qip4XObbQZ98Z1CAA"),
|
||||
album: None,
|
||||
view_count: Some(4900000),
|
||||
track_type: video,
|
||||
is_video: true,
|
||||
track_nr: None,
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -600,7 +600,7 @@ MusicCharts(
|
|||
artist_id: Some("UC7n3gWRN0vQzgiOKc51aZ4w"),
|
||||
album: None,
|
||||
view_count: Some(545000000),
|
||||
track_type: video,
|
||||
is_video: true,
|
||||
track_nr: None,
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -633,7 +633,7 @@ MusicCharts(
|
|||
artist_id: Some("UC5IkSn-EFsUu3XANYklXc8g"),
|
||||
album: None,
|
||||
view_count: Some(20000000),
|
||||
track_type: video,
|
||||
is_video: true,
|
||||
track_nr: None,
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -666,7 +666,7 @@ MusicCharts(
|
|||
artist_id: Some("UC7n3gWRN0vQzgiOKc51aZ4w"),
|
||||
album: None,
|
||||
view_count: Some(36000000),
|
||||
track_type: video,
|
||||
is_video: true,
|
||||
track_nr: None,
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -699,7 +699,7 @@ MusicCharts(
|
|||
artist_id: Some("UCgpBsaDW2n_6ruzht3wvP0A"),
|
||||
album: None,
|
||||
view_count: Some(66000000),
|
||||
track_type: video,
|
||||
is_video: true,
|
||||
track_nr: None,
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -728,7 +728,7 @@ MusicCharts(
|
|||
artist_id: Some("UCPC0L1d253x-KuMNwa05TpA"),
|
||||
album: None,
|
||||
view_count: Some(68000000),
|
||||
track_type: video,
|
||||
is_video: true,
|
||||
track_nr: None,
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -757,7 +757,7 @@ MusicCharts(
|
|||
artist_id: Some("UCju-DqP7JNtCnMWFXhLgPHQ"),
|
||||
album: None,
|
||||
view_count: Some(46000000),
|
||||
track_type: video,
|
||||
is_video: true,
|
||||
track_nr: None,
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -790,7 +790,7 @@ MusicCharts(
|
|||
artist_id: Some("UC5IkSn-EFsUu3XANYklXc8g"),
|
||||
album: None,
|
||||
view_count: Some(43000000),
|
||||
track_type: video,
|
||||
is_video: true,
|
||||
track_nr: None,
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -823,7 +823,7 @@ MusicCharts(
|
|||
artist_id: Some("UCoC_a7lWbj2v7rt4ujp4n2A"),
|
||||
album: None,
|
||||
view_count: Some(7200000),
|
||||
track_type: video,
|
||||
is_video: true,
|
||||
track_nr: None,
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -852,7 +852,7 @@ MusicCharts(
|
|||
artist_id: Some("UCvUZUUxWhwtKLVQ9bVRjLEA"),
|
||||
album: None,
|
||||
view_count: Some(4000000),
|
||||
track_type: video,
|
||||
is_video: true,
|
||||
track_nr: None,
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -881,7 +881,7 @@ MusicCharts(
|
|||
artist_id: Some("UCr_zAwkma5JAyHOWfVXaouA"),
|
||||
album: None,
|
||||
view_count: Some(2900000),
|
||||
track_type: video,
|
||||
is_video: true,
|
||||
track_nr: None,
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -914,7 +914,7 @@ MusicCharts(
|
|||
artist_id: Some("UC_z9AthnCGSAk_tZf-KqoFA"),
|
||||
album: None,
|
||||
view_count: Some(10000000),
|
||||
track_type: video,
|
||||
is_video: true,
|
||||
track_nr: None,
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -943,7 +943,7 @@ MusicCharts(
|
|||
artist_id: Some("UCBabNBocAdKiN5sz8RBjIDg"),
|
||||
album: None,
|
||||
view_count: Some(15000000),
|
||||
track_type: video,
|
||||
is_video: true,
|
||||
track_nr: None,
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -972,7 +972,7 @@ MusicCharts(
|
|||
artist_id: Some("UC5xaQ6_dP7EGDmGLzVGZ1Ow"),
|
||||
album: None,
|
||||
view_count: Some(16000000),
|
||||
track_type: video,
|
||||
is_video: true,
|
||||
track_nr: None,
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -1001,7 +1001,7 @@ MusicCharts(
|
|||
artist_id: Some("UCiXhCjTprNP0nuQJ9UsLWeg"),
|
||||
album: None,
|
||||
view_count: Some(21000000),
|
||||
track_type: video,
|
||||
is_video: true,
|
||||
track_nr: None,
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -1030,7 +1030,7 @@ MusicCharts(
|
|||
artist_id: Some("UC_VCJd8skzwcPktsMLqTz1g"),
|
||||
album: None,
|
||||
view_count: Some(35000000),
|
||||
track_type: video,
|
||||
is_video: true,
|
||||
track_nr: None,
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -1067,7 +1067,7 @@ MusicCharts(
|
|||
artist_id: None,
|
||||
album: None,
|
||||
view_count: Some(30000000),
|
||||
track_type: video,
|
||||
is_video: true,
|
||||
track_nr: None,
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -1096,7 +1096,7 @@ MusicCharts(
|
|||
artist_id: Some("UCq_Fb1zqNikdovyMJgRQjcw"),
|
||||
album: None,
|
||||
view_count: Some(18000000),
|
||||
track_type: video,
|
||||
is_video: true,
|
||||
track_nr: None,
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -1125,7 +1125,7 @@ MusicCharts(
|
|||
artist_id: Some("UChWPNW87QHcXAsw2mzlsYNw"),
|
||||
album: None,
|
||||
view_count: Some(5400000),
|
||||
track_type: video,
|
||||
is_video: true,
|
||||
track_nr: None,
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -1154,7 +1154,7 @@ MusicCharts(
|
|||
artist_id: Some("UCiY3z8HAGD6BlSNKVn2kSvQ"),
|
||||
album: None,
|
||||
view_count: Some(312000000),
|
||||
track_type: video,
|
||||
is_video: true,
|
||||
track_nr: None,
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -1183,7 +1183,7 @@ MusicCharts(
|
|||
artist_id: Some("UC0_1glf30IS53tFQWT8xpxw"),
|
||||
album: None,
|
||||
view_count: Some(28000000),
|
||||
track_type: video,
|
||||
is_video: true,
|
||||
track_nr: None,
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -1212,7 +1212,7 @@ MusicCharts(
|
|||
artist_id: Some("UC_z9AthnCGSAk_tZf-KqoFA"),
|
||||
album: None,
|
||||
view_count: Some(97000000),
|
||||
track_type: video,
|
||||
is_video: true,
|
||||
track_nr: None,
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -1238,7 +1238,7 @@ MusicCharts(
|
|||
artist_id: Some("UCGexNm_Kw4rdQjLxmpb2EKw"),
|
||||
album: None,
|
||||
view_count: Some(6000000),
|
||||
track_type: video,
|
||||
is_video: true,
|
||||
track_nr: None,
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -1262,7 +1262,7 @@ MusicCharts(
|
|||
artist_id: Some("UCybEdRVR5u_WFoV-BLTEBiA"),
|
||||
album: None,
|
||||
view_count: Some(15000000),
|
||||
track_type: video,
|
||||
is_video: true,
|
||||
track_nr: None,
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -1286,7 +1286,7 @@ MusicCharts(
|
|||
artist_id: Some("UCTP45_DE3fMLujU8sZ-MBzw"),
|
||||
album: None,
|
||||
view_count: Some(10000000),
|
||||
track_type: video,
|
||||
is_video: true,
|
||||
track_nr: None,
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -1314,7 +1314,7 @@ MusicCharts(
|
|||
artist_id: Some("UC_duTRnaqtLLTCDIlqjRTcQ"),
|
||||
album: None,
|
||||
view_count: Some(3600000),
|
||||
track_type: video,
|
||||
is_video: true,
|
||||
track_nr: None,
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -1338,7 +1338,7 @@ MusicCharts(
|
|||
artist_id: Some("UCPoQYATXIYvN5WB0c4f6jfQ"),
|
||||
album: None,
|
||||
view_count: Some(524000),
|
||||
track_type: video,
|
||||
is_video: true,
|
||||
track_nr: None,
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -1362,7 +1362,7 @@ MusicCharts(
|
|||
artist_id: Some("UCR28YDxjDE3ogQROaNdnRbQ"),
|
||||
album: None,
|
||||
view_count: Some(3800000),
|
||||
track_type: video,
|
||||
is_video: true,
|
||||
track_nr: None,
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -1386,7 +1386,7 @@ MusicCharts(
|
|||
artist_id: Some("UCpcTrCXblq78GZrTUTLWeBw"),
|
||||
album: None,
|
||||
view_count: Some(46000000),
|
||||
track_type: video,
|
||||
is_video: true,
|
||||
track_nr: None,
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -1410,7 +1410,7 @@ MusicCharts(
|
|||
artist_id: Some("UCEf_Bc-KVd7onSeifS3py9g"),
|
||||
album: None,
|
||||
view_count: Some(8300000),
|
||||
track_type: video,
|
||||
is_video: true,
|
||||
track_nr: None,
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -1434,7 +1434,7 @@ MusicCharts(
|
|||
artist_id: Some("UCVcAt8IIKIeubRSigcYXgtA"),
|
||||
album: None,
|
||||
view_count: Some(13000000),
|
||||
track_type: video,
|
||||
is_video: true,
|
||||
track_nr: None,
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -1458,7 +1458,7 @@ MusicCharts(
|
|||
artist_id: Some("UC0_1glf30IS53tFQWT8xpxw"),
|
||||
album: None,
|
||||
view_count: Some(365000),
|
||||
track_type: video,
|
||||
is_video: true,
|
||||
track_nr: None,
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -1482,7 +1482,7 @@ MusicCharts(
|
|||
artist_id: Some("UC1_liDR4fRFJgH4HoJeV8cw"),
|
||||
album: None,
|
||||
view_count: Some(754000),
|
||||
track_type: video,
|
||||
is_video: true,
|
||||
track_nr: None,
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -1506,7 +1506,7 @@ MusicCharts(
|
|||
artist_id: Some("UCGJdT8Qip4XObbQZ98Z1CAA"),
|
||||
album: None,
|
||||
view_count: Some(4900000),
|
||||
track_type: video,
|
||||
is_video: true,
|
||||
track_nr: None,
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -1530,7 +1530,7 @@ MusicCharts(
|
|||
artist_id: Some("UCr_zAwkma5JAyHOWfVXaouA"),
|
||||
album: None,
|
||||
view_count: Some(2900000),
|
||||
track_type: video,
|
||||
is_video: true,
|
||||
track_nr: None,
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -1554,7 +1554,7 @@ MusicCharts(
|
|||
artist_id: Some("UCvUZUUxWhwtKLVQ9bVRjLEA"),
|
||||
album: None,
|
||||
view_count: Some(4000000),
|
||||
track_type: video,
|
||||
is_video: true,
|
||||
track_nr: None,
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -1582,7 +1582,7 @@ MusicCharts(
|
|||
artist_id: Some("UC7n3gWRN0vQzgiOKc51aZ4w"),
|
||||
album: None,
|
||||
view_count: Some(36000000),
|
||||
track_type: video,
|
||||
is_video: true,
|
||||
track_nr: None,
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -1606,7 +1606,7 @@ MusicCharts(
|
|||
artist_id: Some("UCVcAt8IIKIeubRSigcYXgtA"),
|
||||
album: None,
|
||||
view_count: Some(2000000),
|
||||
track_type: video,
|
||||
is_video: true,
|
||||
track_nr: None,
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -1630,7 +1630,7 @@ MusicCharts(
|
|||
artist_id: Some("UChWPNW87QHcXAsw2mzlsYNw"),
|
||||
album: None,
|
||||
view_count: Some(2600000),
|
||||
track_type: video,
|
||||
is_video: true,
|
||||
track_nr: None,
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -1662,7 +1662,7 @@ MusicCharts(
|
|||
artist_id: Some("UC47k7qXysCBKeaYfc1zmkIA"),
|
||||
album: None,
|
||||
view_count: Some(3500000),
|
||||
track_type: video,
|
||||
is_video: true,
|
||||
track_nr: None,
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -1686,7 +1686,7 @@ MusicCharts(
|
|||
artist_id: Some("UCjfB7ooJY7C43vBAuuCub_A"),
|
||||
album: None,
|
||||
view_count: Some(367000),
|
||||
track_type: video,
|
||||
is_video: true,
|
||||
track_nr: None,
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -1710,7 +1710,7 @@ MusicCharts(
|
|||
artist_id: Some("UC5xaQ6_dP7EGDmGLzVGZ1Ow"),
|
||||
album: None,
|
||||
view_count: Some(1500000),
|
||||
track_type: video,
|
||||
is_video: true,
|
||||
track_nr: None,
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -2416,7 +2416,6 @@ MusicCharts(
|
|||
channel: None,
|
||||
track_count: None,
|
||||
from_ytm: true,
|
||||
is_podcast: false,
|
||||
),
|
||||
MusicPlaylistItem(
|
||||
id: "PL4fGSI1pDJn4fmCoF1vKHLtivI0f9yHiF",
|
||||
|
@ -2436,7 +2435,6 @@ MusicCharts(
|
|||
channel: None,
|
||||
track_count: None,
|
||||
from_ytm: true,
|
||||
is_podcast: false,
|
||||
),
|
||||
MusicPlaylistItem(
|
||||
id: "PL4fGSI1pDJn5O8siDeZuI_4hbk6JWtTX1",
|
||||
|
@ -2456,7 +2454,6 @@ MusicCharts(
|
|||
channel: None,
|
||||
track_count: None,
|
||||
from_ytm: true,
|
||||
is_podcast: false,
|
||||
),
|
||||
MusicPlaylistItem(
|
||||
id: "PL4fGSI1pDJn4EBsWVeFpcSAVOFMfhyipg",
|
||||
|
@ -2476,7 +2473,6 @@ MusicCharts(
|
|||
channel: None,
|
||||
track_count: None,
|
||||
from_ytm: true,
|
||||
is_podcast: false,
|
||||
),
|
||||
MusicPlaylistItem(
|
||||
id: "PL4fGSI1pDJn5LOptOQixqnzXNGjNXAgYY",
|
||||
|
@ -2496,7 +2492,6 @@ MusicCharts(
|
|||
channel: None,
|
||||
track_count: None,
|
||||
from_ytm: true,
|
||||
is_podcast: false,
|
||||
),
|
||||
MusicPlaylistItem(
|
||||
id: "PL4fGSI1pDJn4w4wTTgOmP_S80PoCtbGrL",
|
||||
|
@ -2516,7 +2511,6 @@ MusicCharts(
|
|||
channel: None,
|
||||
track_count: None,
|
||||
from_ytm: true,
|
||||
is_podcast: false,
|
||||
),
|
||||
MusicPlaylistItem(
|
||||
id: "PL4fGSI1pDJn7Wkr6Ll6ds1AhA42rT8uaU",
|
||||
|
@ -2536,7 +2530,6 @@ MusicCharts(
|
|||
channel: None,
|
||||
track_count: None,
|
||||
from_ytm: true,
|
||||
is_podcast: false,
|
||||
),
|
||||
MusicPlaylistItem(
|
||||
id: "PL4fGSI1pDJn4rBU0RHnR6-b1_uE20CzRH",
|
||||
|
@ -2556,7 +2549,6 @@ MusicCharts(
|
|||
channel: None,
|
||||
track_count: None,
|
||||
from_ytm: true,
|
||||
is_podcast: false,
|
||||
),
|
||||
],
|
||||
top_playlist_id: Some("PL4fGSI1pDJn69On1f-8NAvX_CYlx7QyZc"),
|
||||
|
|
|
@ -29,7 +29,7 @@ MusicCharts(
|
|||
artist_id: Some("UCpcTrCXblq78GZrTUTLWeBw"),
|
||||
album: None,
|
||||
view_count: Some(46000000),
|
||||
track_type: video,
|
||||
is_video: true,
|
||||
track_nr: None,
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -62,7 +62,7 @@ MusicCharts(
|
|||
artist_id: Some("UC9vrvNSL3xcWGSkV86REBSg"),
|
||||
album: None,
|
||||
view_count: Some(46000000),
|
||||
track_type: video,
|
||||
is_video: true,
|
||||
track_nr: None,
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -91,7 +91,7 @@ MusicCharts(
|
|||
artist_id: Some("UCo6JijJGA3IvIiPsawDK3Ww"),
|
||||
album: None,
|
||||
view_count: Some(3300000000),
|
||||
track_type: video,
|
||||
is_video: true,
|
||||
track_nr: None,
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -124,7 +124,7 @@ MusicCharts(
|
|||
artist_id: Some("UCONiUl5u7y2bMaVZJcuRDEQ"),
|
||||
album: None,
|
||||
view_count: Some(38000000),
|
||||
track_type: video,
|
||||
is_video: true,
|
||||
track_nr: None,
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -157,7 +157,7 @@ MusicCharts(
|
|||
artist_id: None,
|
||||
album: None,
|
||||
view_count: Some(57000000),
|
||||
track_type: video,
|
||||
is_video: true,
|
||||
track_nr: None,
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -186,7 +186,7 @@ MusicCharts(
|
|||
artist_id: Some("UCiY3z8HAGD6BlSNKVn2kSvQ"),
|
||||
album: None,
|
||||
view_count: Some(521000000),
|
||||
track_type: video,
|
||||
is_video: true,
|
||||
track_nr: None,
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -219,7 +219,7 @@ MusicCharts(
|
|||
artist_id: Some("UC5p07Pr3hlfjXo3YGVCyOgg"),
|
||||
album: None,
|
||||
view_count: Some(76000000),
|
||||
track_type: video,
|
||||
is_video: true,
|
||||
track_nr: None,
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -248,7 +248,7 @@ MusicCharts(
|
|||
artist_id: Some("UCfh2j2Dq-aSeLhzuPOsnhVg"),
|
||||
album: None,
|
||||
view_count: Some(276000000),
|
||||
track_type: video,
|
||||
is_video: true,
|
||||
track_nr: None,
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -281,7 +281,7 @@ MusicCharts(
|
|||
artist_id: Some("UCeBYRgPhy8kcRmIGQWKuqdQ"),
|
||||
album: None,
|
||||
view_count: Some(136000000),
|
||||
track_type: video,
|
||||
is_video: true,
|
||||
track_nr: None,
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -310,7 +310,7 @@ MusicCharts(
|
|||
artist_id: Some("UCiY3z8HAGD6BlSNKVn2kSvQ"),
|
||||
album: None,
|
||||
view_count: Some(559000000),
|
||||
track_type: video,
|
||||
is_video: true,
|
||||
track_nr: None,
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -339,7 +339,7 @@ MusicCharts(
|
|||
artist_id: Some("UCDxKh1gFWeYsqePvgVzmPoQ"),
|
||||
album: None,
|
||||
view_count: Some(331000000),
|
||||
track_type: video,
|
||||
is_video: true,
|
||||
track_nr: None,
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -368,7 +368,7 @@ MusicCharts(
|
|||
artist_id: Some("UCkbbMCA40i18i7UdjayMPAg"),
|
||||
album: None,
|
||||
view_count: Some(257000000),
|
||||
track_type: video,
|
||||
is_video: true,
|
||||
track_nr: None,
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -401,7 +401,7 @@ MusicCharts(
|
|||
artist_id: Some("UC7n3gWRN0vQzgiOKc51aZ4w"),
|
||||
album: None,
|
||||
view_count: Some(36000000),
|
||||
track_type: video,
|
||||
is_video: true,
|
||||
track_nr: None,
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -442,7 +442,7 @@ MusicCharts(
|
|||
artist_id: Some("UCKEFjh4JL-OyMI8z3h5Coaw"),
|
||||
album: None,
|
||||
view_count: Some(50000000),
|
||||
track_type: video,
|
||||
is_video: true,
|
||||
track_nr: None,
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -475,7 +475,7 @@ MusicCharts(
|
|||
artist_id: Some("UC7n3gWRN0vQzgiOKc51aZ4w"),
|
||||
album: None,
|
||||
view_count: Some(202000000),
|
||||
track_type: video,
|
||||
is_video: true,
|
||||
track_nr: None,
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -504,7 +504,7 @@ MusicCharts(
|
|||
artist_id: Some("UCKNGMXJHTiGFdZNSo_zs3fQ"),
|
||||
album: None,
|
||||
view_count: Some(103000000),
|
||||
track_type: video,
|
||||
is_video: true,
|
||||
track_nr: None,
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -533,7 +533,7 @@ MusicCharts(
|
|||
artist_id: Some("UCkbbMCA40i18i7UdjayMPAg"),
|
||||
album: None,
|
||||
view_count: Some(453000000),
|
||||
track_type: video,
|
||||
is_video: true,
|
||||
track_nr: None,
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -562,7 +562,7 @@ MusicCharts(
|
|||
artist_id: Some("UCUamzwxCTrUvpyAvAt4FEdg"),
|
||||
album: None,
|
||||
view_count: Some(44000000),
|
||||
track_type: video,
|
||||
is_video: true,
|
||||
track_nr: None,
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -591,7 +591,7 @@ MusicCharts(
|
|||
artist_id: Some("UCKEFjh4JL-OyMI8z3h5Coaw"),
|
||||
album: None,
|
||||
view_count: Some(81000000),
|
||||
track_type: video,
|
||||
is_video: true,
|
||||
track_nr: None,
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -620,7 +620,7 @@ MusicCharts(
|
|||
artist_id: Some("UCJa2FF4TUB13Mm0GurZAqog"),
|
||||
album: None,
|
||||
view_count: Some(73000000),
|
||||
track_type: video,
|
||||
is_video: true,
|
||||
track_nr: None,
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -649,7 +649,7 @@ MusicCharts(
|
|||
artist_id: Some("UC6uMb9hMAziN9HZoXfTBAlg"),
|
||||
album: None,
|
||||
view_count: Some(45000000),
|
||||
track_type: video,
|
||||
is_video: true,
|
||||
track_nr: None,
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -678,7 +678,7 @@ MusicCharts(
|
|||
artist_id: Some("UC7n3gWRN0vQzgiOKc51aZ4w"),
|
||||
album: None,
|
||||
view_count: Some(545000000),
|
||||
track_type: video,
|
||||
is_video: true,
|
||||
track_nr: None,
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -707,7 +707,7 @@ MusicCharts(
|
|||
artist_id: Some("UCJa2FF4TUB13Mm0GurZAqog"),
|
||||
album: None,
|
||||
view_count: Some(139000000),
|
||||
track_type: video,
|
||||
is_video: true,
|
||||
track_nr: None,
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -736,7 +736,7 @@ MusicCharts(
|
|||
artist_id: Some("UCeYz6rzUGhVwqxRM37FUo8w"),
|
||||
album: None,
|
||||
view_count: Some(197000000),
|
||||
track_type: video,
|
||||
is_video: true,
|
||||
track_nr: None,
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -769,7 +769,7 @@ MusicCharts(
|
|||
artist_id: Some("UCy6qn2oxmoXA4_gBA5Q7zPw"),
|
||||
album: None,
|
||||
view_count: Some(257000000),
|
||||
track_type: video,
|
||||
is_video: true,
|
||||
track_nr: None,
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -798,7 +798,7 @@ MusicCharts(
|
|||
artist_id: Some("UCybEdRVR5u_WFoV-BLTEBiA"),
|
||||
album: None,
|
||||
view_count: Some(15000000),
|
||||
track_type: video,
|
||||
is_video: true,
|
||||
track_nr: None,
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -827,7 +827,7 @@ MusicCharts(
|
|||
artist_id: Some("UCtGHTwNL20Y3fY9bumjHDOw"),
|
||||
album: None,
|
||||
view_count: Some(55000000),
|
||||
track_type: video,
|
||||
is_video: true,
|
||||
track_nr: None,
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -856,7 +856,7 @@ MusicCharts(
|
|||
artist_id: Some("UCWsDFcIhY2DBi3GB5uykGXA"),
|
||||
album: None,
|
||||
view_count: Some(34000000),
|
||||
track_type: video,
|
||||
is_video: true,
|
||||
track_nr: None,
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -889,7 +889,7 @@ MusicCharts(
|
|||
artist_id: Some("UCo6JijJGA3IvIiPsawDK3Ww"),
|
||||
album: None,
|
||||
view_count: Some(123000000),
|
||||
track_type: video,
|
||||
is_video: true,
|
||||
track_nr: None,
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -918,7 +918,7 @@ MusicCharts(
|
|||
artist_id: Some("UCc3e8O2V5_7OA300ursDyFQ"),
|
||||
album: None,
|
||||
view_count: Some(109000000),
|
||||
track_type: video,
|
||||
is_video: true,
|
||||
track_nr: None,
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -947,7 +947,7 @@ MusicCharts(
|
|||
artist_id: Some("UC3QmG1Jn9cE5fTMt14DLuZw"),
|
||||
album: None,
|
||||
view_count: Some(5700000000),
|
||||
track_type: video,
|
||||
is_video: true,
|
||||
track_nr: None,
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -976,7 +976,7 @@ MusicCharts(
|
|||
artist_id: Some("UC03jIQv4WXBSHdr1DlCLYDw"),
|
||||
album: None,
|
||||
view_count: Some(872000000),
|
||||
track_type: video,
|
||||
is_video: true,
|
||||
track_nr: None,
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -1005,7 +1005,7 @@ MusicCharts(
|
|||
artist_id: Some("UCSzWQmDsKG37iKN2vw1G-2Q"),
|
||||
album: None,
|
||||
view_count: Some(7900000),
|
||||
track_type: video,
|
||||
is_video: true,
|
||||
track_nr: None,
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -1034,7 +1034,7 @@ MusicCharts(
|
|||
artist_id: Some("UCo6JijJGA3IvIiPsawDK3Ww"),
|
||||
album: None,
|
||||
view_count: Some(750000000),
|
||||
track_type: video,
|
||||
is_video: true,
|
||||
track_nr: None,
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -1063,7 +1063,7 @@ MusicCharts(
|
|||
artist_id: Some("UCKRnq8aBOCanYlffje7HyvA"),
|
||||
album: None,
|
||||
view_count: Some(311000000),
|
||||
track_type: video,
|
||||
is_video: true,
|
||||
track_nr: None,
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -1100,7 +1100,7 @@ MusicCharts(
|
|||
artist_id: Some("UCQK0swJm0ceapSOtRKIWr0g"),
|
||||
album: None,
|
||||
view_count: Some(37000000),
|
||||
track_type: video,
|
||||
is_video: true,
|
||||
track_nr: None,
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -1133,7 +1133,7 @@ MusicCharts(
|
|||
artist_id: Some("UC7PL9aor5qNRhvhWWVXyOqA"),
|
||||
album: None,
|
||||
view_count: Some(377000000),
|
||||
track_type: video,
|
||||
is_video: true,
|
||||
track_nr: None,
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -1166,7 +1166,7 @@ MusicCharts(
|
|||
artist_id: Some("UC2kPe8FB39lojsUDtyKcqOQ"),
|
||||
album: None,
|
||||
view_count: Some(486000000),
|
||||
track_type: video,
|
||||
is_video: true,
|
||||
track_nr: None,
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -1195,7 +1195,7 @@ MusicCharts(
|
|||
artist_id: Some("UCrP3Rfz32MT-OH9MZh_N9kA"),
|
||||
album: None,
|
||||
view_count: Some(570000000),
|
||||
track_type: video,
|
||||
is_video: true,
|
||||
track_nr: None,
|
||||
by_va: false,
|
||||
),
|
||||
|
@ -1224,7 +1224,7 @@ MusicCharts(
|
|||
artist_id: Some("UC0QVToeCjC9-1u-teWToPsg"),
|
||||
album: None,
|
||||
view_count: Some(28000000),
|
||||
track_type: video,
|
||||
is_video: true,
|
||||
track_nr: None,
|
||||
by_va: false,
|
||||
),
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue