Compare commits
6 commits
renovate/t
...
main
Author | SHA1 | Date | |
---|---|---|---|
5d248bd110 | |||
5da3887932 | |||
8e0e66ffec | |||
ac8fbc3e67 | |||
870ff79ee0 | |||
|
e1e1687605 |
17 changed files with 19722 additions and 169 deletions
20
CHANGELOG.md
20
CHANGELOG.md
|
@ -3,6 +3,26 @@
|
||||||
All notable changes to this project will be documented in this file.
|
All notable changes to this project will be documented in this file.
|
||||||
|
|
||||||
|
|
||||||
|
## [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
|
## [v0.6.0](https://codeberg.org/ThetaDev/rustypipe/compare/rustypipe/v0.5.0..rustypipe/v0.6.0) - 2024-10-28
|
||||||
|
|
||||||
### 🚀 Features
|
### 🚀 Features
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
[package]
|
[package]
|
||||||
name = "rustypipe"
|
name = "rustypipe"
|
||||||
version = "0.6.0"
|
version = "0.7.0"
|
||||||
rust-version = "1.67.1"
|
rust-version = "1.67.1"
|
||||||
edition.workspace = true
|
edition.workspace = true
|
||||||
authors.workspace = true
|
authors.workspace = true
|
||||||
|
@ -73,7 +73,7 @@ path_macro = "1.0.0"
|
||||||
tracing-test = "0.2.5"
|
tracing-test = "0.2.5"
|
||||||
|
|
||||||
# Included crates
|
# Included crates
|
||||||
rustypipe = { path = ".", version = "0.6.0", default-features = false }
|
rustypipe = { path = ".", version = "0.7.0", default-features = false }
|
||||||
rustypipe-downloader = { path = "./downloader", version = "0.2.1", default-features = false, features = [
|
rustypipe-downloader = { path = "./downloader", version = "0.2.1", default-features = false, features = [
|
||||||
"indicatif",
|
"indicatif",
|
||||||
"audiotag",
|
"audiotag",
|
||||||
|
|
10
Justfile
10
Justfile
|
@ -1,15 +1,15 @@
|
||||||
test:
|
test:
|
||||||
# cargo test --features=rss
|
# cargo test --features=rss
|
||||||
cargo nextest run --workspace --features=rss --no-fail-fast --failure-output final --retries 1
|
cargo nextest run --workspace --features=rss --no-fail-fast --retries 1
|
||||||
|
|
||||||
unittest:
|
unittest:
|
||||||
cargo nextest run --features=rss --no-fail-fast --failure-output final --lib
|
cargo nextest run --features=rss --no-fail-fast --lib
|
||||||
|
|
||||||
testyt:
|
testyt:
|
||||||
cargo nextest run --features=rss --no-fail-fast --failure-output final --retries 1 --test youtube
|
cargo nextest run --features=rss --no-fail-fast --retries 1 --test youtube
|
||||||
|
|
||||||
testyt-localized:
|
testyt-localized:
|
||||||
YT_LANG=th cargo nextest run --features=rss --no-fail-fast --failure-output final --retries 1 --test youtube
|
YT_LANG=th cargo nextest run --features=rss --no-fail-fast ---retries 1 --test youtube
|
||||||
|
|
||||||
testintl:
|
testintl:
|
||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
|
@ -28,7 +28,7 @@ testintl:
|
||||||
for YT_LANG in "${LANGUAGES[@]}"; do
|
for YT_LANG in "${LANGUAGES[@]}"; do
|
||||||
echo "---TESTS FOR $YT_LANG ---"
|
echo "---TESTS FOR $YT_LANG ---"
|
||||||
|
|
||||||
if YT_LANG="$YT_LANG" cargo nextest run --no-fail-fast --failure-output final --retries 1 --test-threads 4 --test youtube -E 'not test(/^resolve/)'; then
|
if YT_LANG="$YT_LANG" cargo nextest run --no-fail-fast ---retries 1 --test-threads 4 --test youtube -E 'not test(/^resolve/)'; then
|
||||||
echo "--- $YT_LANG COMPLETED ---"
|
echo "--- $YT_LANG COMPLETED ---"
|
||||||
else
|
else
|
||||||
echo "--- $YT_LANG FAILED ---"
|
echo "--- $YT_LANG FAILED ---"
|
||||||
|
|
|
@ -3,6 +3,18 @@
|
||||||
All notable changes to this project will be documented in this file.
|
All notable changes to this project will be documented in this file.
|
||||||
|
|
||||||
|
|
||||||
|
## [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
|
## [v0.3.0](https://codeberg.org/ThetaDev/rustypipe/compare/rustypipe-cli/v0.2.2..rustypipe-cli/v0.3.0) - 2024-10-28
|
||||||
|
|
||||||
### 🚀 Features
|
### 🚀 Features
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
[package]
|
[package]
|
||||||
name = "rustypipe-cli"
|
name = "rustypipe-cli"
|
||||||
version = "0.3.0"
|
version = "0.4.0"
|
||||||
rust-version = "1.70.0"
|
rust-version = "1.70.0"
|
||||||
edition.workspace = true
|
edition.workspace = true
|
||||||
authors.workspace = true
|
authors.workspace = true
|
||||||
|
|
|
@ -3,12 +3,24 @@
|
||||||
All notable changes to this project will be documented in this file.
|
All notable changes to this project will be documented in this file.
|
||||||
|
|
||||||
|
|
||||||
|
## [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
|
## [v0.2.3](https://codeberg.org/ThetaDev/rustypipe/compare/rustypipe-downloader/v0.2.2..rustypipe-downloader/v0.2.3) - 2024-10-28
|
||||||
|
|
||||||
### 🐛 Bug Fixes
|
### 🐛 Bug Fixes
|
||||||
|
|
||||||
- Remove unnecessary image.rs dependencies - ([1b08166](https://codeberg.org/ThetaDev/rustypipe/commit/1b08166399cccb8394d2fdd82d54162c1a9e01be))
|
- 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
|
## [v0.2.2](https://codeberg.org/ThetaDev/rustypipe/compare/rustypipe-downloader/v0.2.1..rustypipe-downloader/v0.2.2) - 2024-10-13
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
[package]
|
[package]
|
||||||
name = "rustypipe-downloader"
|
name = "rustypipe-downloader"
|
||||||
version = "0.2.3"
|
version = "0.2.4"
|
||||||
rust-version = "1.67.1"
|
rust-version = "1.67.1"
|
||||||
edition.workspace = true
|
edition.workspace = true
|
||||||
authors.workspace = true
|
authors.workspace = true
|
||||||
|
|
|
@ -9,7 +9,7 @@ use crate::{
|
||||||
error::{Error, ExtractionError},
|
error::{Error, ExtractionError},
|
||||||
model::{
|
model::{
|
||||||
paginator::{ContinuationEndpoint, Paginator},
|
paginator::{ContinuationEndpoint, Paginator},
|
||||||
Channel, ChannelInfo, PlaylistItem, VideoItem,
|
Channel, ChannelInfo, PlaylistItem, Verification, VideoItem,
|
||||||
},
|
},
|
||||||
param::{ChannelOrder, ChannelVideoTab, Language},
|
param::{ChannelOrder, ChannelVideoTab, Language},
|
||||||
serializer::{text::TextComponent, MapResult},
|
serializer::{text::TextComponent, MapResult},
|
||||||
|
@ -489,7 +489,7 @@ fn map_channel(
|
||||||
.avatar_view_model
|
.avatar_view_model
|
||||||
.image
|
.image
|
||||||
.into(),
|
.into(),
|
||||||
verification: hdata.title.into(),
|
verification: hdata.title.map(Verification::from).unwrap_or_default(),
|
||||||
description: metadata.description,
|
description: metadata.description,
|
||||||
tags: microformat.microformat_data_renderer.tags,
|
tags: microformat.microformat_data_renderer.tags,
|
||||||
banner: hdata.banner.image_banner_view_model.image.into(),
|
banner: hdata.banner.image_banner_view_model.image.into(),
|
||||||
|
|
|
@ -2,11 +2,14 @@ use serde::Deserialize;
|
||||||
use serde_with::{rust::deserialize_ignore_any, serde_as, DefaultOnError, VecSkipError};
|
use serde_with::{rust::deserialize_ignore_any, serde_as, DefaultOnError, VecSkipError};
|
||||||
|
|
||||||
use super::{
|
use super::{
|
||||||
video_item::YouTubeListRenderer, Alert, ChannelBadge, ContentRenderer, ContentsRenderer,
|
video_item::YouTubeListRenderer, Alert, AttachmentRun, ChannelBadge, ContentRenderer,
|
||||||
ContinuationActionWrap, ImageView, PageHeaderRendererContent, PhMetadataView, ResponseContext,
|
ContentsRenderer, ContinuationActionWrap, ImageView, PageHeaderRendererContent, PhMetadataView,
|
||||||
Thumbnails, TwoColumnBrowseResults,
|
ResponseContext, Thumbnails, TwoColumnBrowseResults,
|
||||||
|
};
|
||||||
|
use crate::{
|
||||||
|
model::Verification,
|
||||||
|
serializer::text::{AttributedText, Text, TextComponent},
|
||||||
};
|
};
|
||||||
use crate::serializer::text::{AttributedText, Text, TextComponent};
|
|
||||||
|
|
||||||
#[serde_as]
|
#[serde_as]
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Debug, Deserialize)]
|
||||||
|
@ -121,7 +124,7 @@ pub(crate) enum CarouselHeaderRendererItem {
|
||||||
pub(crate) struct PageHeaderRendererInner {
|
pub(crate) struct PageHeaderRendererInner {
|
||||||
/// Channel title (only used to extract verification badges)
|
/// Channel title (only used to extract verification badges)
|
||||||
#[serde_as(as = "DefaultOnError")]
|
#[serde_as(as = "DefaultOnError")]
|
||||||
pub title: PhTitleView,
|
pub title: Option<PhTitleView>,
|
||||||
/// Channel avatar
|
/// Channel avatar
|
||||||
pub image: PhAvatarView,
|
pub image: PhAvatarView,
|
||||||
/// Channel metadata (subscribers, video count)
|
/// Channel metadata (subscribers, video count)
|
||||||
|
@ -130,7 +133,7 @@ pub(crate) struct PageHeaderRendererInner {
|
||||||
pub banner: PhBannerView,
|
pub banner: PhBannerView,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Default, Debug, Deserialize)]
|
#[derive(Debug, Deserialize)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub(crate) struct PhTitleView {
|
pub(crate) struct PhTitleView {
|
||||||
pub dynamic_text_view_model: PhTitleView2,
|
pub dynamic_text_view_model: PhTitleView2,
|
||||||
|
@ -150,58 +153,6 @@ pub(crate) struct PhTitleView3 {
|
||||||
pub attachment_runs: Vec<AttachmentRun>,
|
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)]
|
#[derive(Debug, Deserialize)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub(crate) struct PhAvatarView {
|
pub(crate) struct PhAvatarView {
|
||||||
|
@ -330,15 +281,9 @@ impl From<PhTitleView> for crate::model::Verification {
|
||||||
.dynamic_text_view_model
|
.dynamic_text_view_model
|
||||||
.text
|
.text
|
||||||
.attachment_runs
|
.attachment_runs
|
||||||
.iter()
|
.into_iter()
|
||||||
.find_map(|r| {
|
.next()
|
||||||
r.element.typ.image_type.image.sources.first().map(|s| {
|
.map(Verification::from)
|
||||||
match s.client_resource.image_name {
|
|
||||||
IconName::CheckCircleFilled => crate::model::Verification::Verified,
|
|
||||||
IconName::MusicFilled => crate::model::Verification::Artist,
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
.unwrap_or_default()
|
.unwrap_or_default()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -199,12 +199,73 @@ pub(crate) struct TextBox {
|
||||||
pub text: String,
|
pub text: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[serde_as]
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub(crate) struct TextComponentBox {
|
||||||
|
#[serde_as(deserialize_as = "AttributedText")]
|
||||||
|
pub text: TextComponent,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Debug, Deserialize)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub(crate) struct ResponseContext {
|
pub(crate) struct ResponseContext {
|
||||||
pub visitor_data: Option<String>,
|
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
|
// CONTINUATION
|
||||||
|
|
||||||
#[serde_as]
|
#[serde_as]
|
||||||
|
@ -343,6 +404,17 @@ 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 {
|
impl From<Vec<ChannelBadge>> for crate::model::Verification {
|
||||||
fn from(badges: Vec<ChannelBadge>) -> Self {
|
fn from(badges: Vec<ChannelBadge>) -> Self {
|
||||||
badges
|
badges
|
||||||
|
@ -366,6 +438,25 @@ 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 {
|
pub(crate) fn alerts_to_err(id: &str, alerts: Option<Vec<Alert>>) -> ExtractionError {
|
||||||
ExtractionError::NotFound {
|
ExtractionError::NotFound {
|
||||||
id: id.to_owned(),
|
id: id.to_owned(),
|
||||||
|
@ -480,9 +571,11 @@ pub(crate) struct PhMetadataView {
|
||||||
pub content_metadata_view_model: PhMetadataView2,
|
pub content_metadata_view_model: PhMetadataView2,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[serde_as]
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Debug, Deserialize)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub(crate) struct PhMetadataView2 {
|
pub(crate) struct PhMetadataView2 {
|
||||||
|
#[serde_as(as = "VecSkipError<_>")]
|
||||||
pub metadata_rows: Vec<PhMetadataRow>,
|
pub metadata_rows: Vec<PhMetadataRow>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -498,17 +591,26 @@ pub(crate) struct PhMetadataRow {
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Debug, Deserialize)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub(crate) enum MetadataPart {
|
pub(crate) enum MetadataPart {
|
||||||
Text(#[serde_as(deserialize_as = "AttributedText")] String),
|
Text(#[serde_as(deserialize_as = "AttributedText")] TextComponent),
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
AvatarStack {
|
AvatarStack {
|
||||||
avatar_stack_view_model: AvatarStackViewModel,
|
avatar_stack_view_model: TextComponentBox,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
impl MetadataPart {
|
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 {
|
pub fn as_str(&self) -> &str {
|
||||||
match self {
|
match self {
|
||||||
MetadataPart::Text(s) => s,
|
MetadataPart::Text(s) => s.as_str(),
|
||||||
MetadataPart::AvatarStack {
|
MetadataPart::AvatarStack {
|
||||||
avatar_stack_view_model,
|
avatar_stack_view_model,
|
||||||
} => avatar_stack_view_model.text.as_str(),
|
} => avatar_stack_view_model.text.as_str(),
|
||||||
|
@ -516,24 +618,14 @@ impl MetadataPart {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[serde_as]
|
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Debug, Deserialize)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub(crate) struct AvatarStackViewModel {
|
pub(crate) enum ContentImage {
|
||||||
#[serde_as(deserialize_as = "AttributedText")]
|
ThumbnailViewModel(ImageViewOl),
|
||||||
pub text: TextComponent,
|
#[serde(rename_all = "camelCase")]
|
||||||
}
|
CollectionThumbnailViewModel {
|
||||||
|
primary_thumbnail: ThumbnailViewModelWrap,
|
||||||
#[derive(Debug, Deserialize)]
|
},
|
||||||
#[serde(rename_all = "camelCase")]
|
|
||||||
pub(crate) struct ContentImage {
|
|
||||||
pub collection_thumbnail_view_model: CollectionThumbnailViewModel,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
|
||||||
#[serde(rename_all = "camelCase")]
|
|
||||||
pub(crate) struct CollectionThumbnailViewModel {
|
|
||||||
pub primary_thumbnail: ThumbnailViewModelWrap,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Debug, Deserialize)]
|
||||||
|
|
|
@ -4,12 +4,9 @@ use serde_with::{
|
||||||
};
|
};
|
||||||
use time::OffsetDateTime;
|
use time::OffsetDateTime;
|
||||||
|
|
||||||
use super::{ChannelBadge, ContentImage, ContinuationEndpoint, Thumbnails};
|
use super::{ChannelBadge, ContentImage, ContinuationEndpoint, PhMetadataView, Thumbnails};
|
||||||
use crate::{
|
use crate::{
|
||||||
model::{
|
model::{Channel, ChannelItem, ChannelTag, PlaylistItem, VideoItem, YouTubeItem},
|
||||||
Channel, ChannelId, ChannelItem, ChannelTag, PlaylistItem, Verification, VideoItem,
|
|
||||||
YouTubeItem,
|
|
||||||
},
|
|
||||||
param::Language,
|
param::Language,
|
||||||
serializer::{
|
serializer::{
|
||||||
text::{AttributedText, Text, TextComponent},
|
text::{AttributedText, Text, TextComponent},
|
||||||
|
@ -167,23 +164,25 @@ pub(crate) struct ShortsOverlayMetadata {
|
||||||
pub secondary_text: Option<String>,
|
pub secondary_text: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Generalized list item, currently only used for playlists
|
/// Generalized list item, currently only used for channel playlists and YTM items
|
||||||
#[serde_as]
|
#[serde_as]
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Debug, Deserialize)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub(crate) struct LockupViewModel {
|
pub(crate) struct LockupViewModel {
|
||||||
pub content_image: ContentImage,
|
|
||||||
pub metadata: LockupViewModelMetadata,
|
|
||||||
pub content_id: String,
|
pub content_id: String,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
#[serde_as(deserialize_as = "DefaultOnError")]
|
#[serde_as(deserialize_as = "DefaultOnError")]
|
||||||
pub content_type: LockupContentType,
|
pub content_type: LockupContentType,
|
||||||
|
pub content_image: ContentImage,
|
||||||
|
pub metadata: LockupViewModelMetadata,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Default, Debug, Deserialize)]
|
#[derive(Default, Debug, Deserialize)]
|
||||||
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
|
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
|
||||||
|
#[allow(clippy::enum_variant_names)]
|
||||||
pub(crate) enum LockupContentType {
|
pub(crate) enum LockupContentType {
|
||||||
LockupContentTypePlaylist,
|
LockupContentTypePlaylist,
|
||||||
|
LockupContentTypeVideo,
|
||||||
#[default]
|
#[default]
|
||||||
Unknown,
|
Unknown,
|
||||||
}
|
}
|
||||||
|
@ -200,6 +199,7 @@ pub(crate) struct LockupViewModelMetadata {
|
||||||
pub(crate) struct LockupViewModelMetadataInner {
|
pub(crate) struct LockupViewModelMetadataInner {
|
||||||
#[serde_as(as = "AttributedText")]
|
#[serde_as(as = "AttributedText")]
|
||||||
pub title: String,
|
pub title: String,
|
||||||
|
pub metadata: PhMetadataView,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Video displayed in a playlist
|
/// Video displayed in a playlist
|
||||||
|
@ -509,19 +509,18 @@ impl<T> YouTubeListMapper<T> {
|
||||||
thumbnail: video.thumbnail.into(),
|
thumbnail: video.thumbnail.into(),
|
||||||
channel: video
|
channel: video
|
||||||
.channel
|
.channel
|
||||||
.and_then(|c| {
|
.and_then(|c| ChannelTag::try_from(c).ok())
|
||||||
ChannelId::try_from(c).ok().map(|c| ChannelTag {
|
.map(|mut c| {
|
||||||
id: c.id,
|
c.avatar = video
|
||||||
name: c.name,
|
|
||||||
avatar: video
|
|
||||||
.channel_thumbnail_supported_renderers
|
.channel_thumbnail_supported_renderers
|
||||||
.map(|tn| tn.channel_thumbnail_with_link_renderer.thumbnail)
|
.map(|tn| tn.channel_thumbnail_with_link_renderer.thumbnail)
|
||||||
.or(video.channel_thumbnail)
|
.or(video.channel_thumbnail)
|
||||||
.unwrap_or_default()
|
.unwrap_or_default()
|
||||||
.into(),
|
.into();
|
||||||
verification: video.owner_badges.into(),
|
if !c.verification.verified() {
|
||||||
subscriber_count: None,
|
c.verification = video.owner_badges.into();
|
||||||
})
|
}
|
||||||
|
c
|
||||||
})
|
})
|
||||||
.or_else(|| self.channel.clone()),
|
.or_else(|| self.channel.clone()),
|
||||||
publish_date: video
|
publish_date: video
|
||||||
|
@ -603,16 +602,7 @@ impl<T> YouTubeListMapper<T> {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn map_playlist_video(&mut self, video: PlaylistVideoRenderer) -> VideoItem {
|
fn map_playlist_video(&mut self, video: PlaylistVideoRenderer) -> VideoItem {
|
||||||
let channel = ChannelId::try_from(video.channel)
|
let channel = ChannelTag::try_from(video.channel).ok();
|
||||||
.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 mut video_info = video.video_info.into_iter();
|
||||||
let video_info1 = video_info
|
let video_info1 = video_info
|
||||||
.next()
|
.next()
|
||||||
|
@ -675,14 +665,12 @@ impl<T> YouTubeListMapper<T> {
|
||||||
.into(),
|
.into(),
|
||||||
channel: playlist
|
channel: playlist
|
||||||
.channel
|
.channel
|
||||||
.and_then(|c| {
|
.and_then(|c| ChannelTag::try_from(c).ok())
|
||||||
ChannelId::try_from(c).ok().map(|c| ChannelTag {
|
.map(|mut c| {
|
||||||
id: c.id,
|
if !c.verification.verified() {
|
||||||
name: c.name,
|
c.verification = playlist.owner_badges.into();
|
||||||
avatar: Vec::new(),
|
}
|
||||||
verification: playlist.owner_badges.into(),
|
c
|
||||||
subscriber_count: None,
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
.or_else(|| self.channel.clone()),
|
.or_else(|| self.channel.clone()),
|
||||||
video_count: playlist.video_count.or_else(|| {
|
video_count: playlist.video_count.or_else(|| {
|
||||||
|
@ -719,15 +707,12 @@ impl<T> YouTubeListMapper<T> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn map_lockup(&mut self, lockup: LockupViewModel) -> Option<PlaylistItem> {
|
fn map_lockup(&mut self, lockup: LockupViewModel) -> Option<YouTubeItem> {
|
||||||
let md = lockup.metadata.lockup_metadata_view_model;
|
let md = lockup.metadata.lockup_metadata_view_model;
|
||||||
let tn = lockup
|
let tn = lockup.content_image.into_image();
|
||||||
.content_image
|
|
||||||
.collection_thumbnail_view_model
|
|
||||||
.primary_thumbnail
|
|
||||||
.thumbnail_view_model;
|
|
||||||
match lockup.content_type {
|
match lockup.content_type {
|
||||||
LockupContentType::LockupContentTypePlaylist => Some(PlaylistItem {
|
LockupContentType::LockupContentTypePlaylist => {
|
||||||
|
Some(YouTubeItem::Playlist(PlaylistItem {
|
||||||
id: lockup.content_id,
|
id: lockup.content_id,
|
||||||
name: md.title,
|
name: md.title,
|
||||||
thumbnail: tn.image.into(),
|
thumbnail: tn.image.into(),
|
||||||
|
@ -741,12 +726,66 @@ impl<T> YouTubeListMapper<T> {
|
||||||
.first()
|
.first()
|
||||||
})
|
})
|
||||||
.and_then(|badge| {
|
.and_then(|badge| {
|
||||||
util::parse_numeric_or_warn(
|
util::parse_numeric(&badge.thumbnail_badge_view_model.text).ok()
|
||||||
&badge.thumbnail_badge_view_model.text,
|
}),
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
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,
|
&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_textual_date_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,
|
LockupContentType::Unknown => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -782,7 +821,7 @@ impl YouTubeListMapper<YouTubeItem> {
|
||||||
}
|
}
|
||||||
YouTubeListItem::LockupViewModel(lockup) => {
|
YouTubeListItem::LockupViewModel(lockup) => {
|
||||||
if let Some(mapped) = self.map_lockup(lockup) {
|
if let Some(mapped) = self.map_lockup(lockup) {
|
||||||
self.items.push(YouTubeItem::Playlist(mapped));
|
self.items.push(mapped);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
YouTubeListItem::ContinuationItemRenderer {
|
YouTubeListItem::ContinuationItemRenderer {
|
||||||
|
@ -828,6 +867,11 @@ impl YouTubeListMapper<VideoItem> {
|
||||||
let mapped = self.map_playlist_video(video);
|
let mapped = self.map_playlist_video(video);
|
||||||
self.items.push(mapped);
|
self.items.push(mapped);
|
||||||
}
|
}
|
||||||
|
YouTubeListItem::LockupViewModel(lockup) => {
|
||||||
|
if let Some(YouTubeItem::Video(mapped)) = self.map_lockup(lockup) {
|
||||||
|
self.items.push(mapped);
|
||||||
|
}
|
||||||
|
}
|
||||||
YouTubeListItem::ContinuationItemRenderer {
|
YouTubeListItem::ContinuationItemRenderer {
|
||||||
continuation_endpoint,
|
continuation_endpoint,
|
||||||
} => self.ctoken = Some(continuation_endpoint.continuation_command.token),
|
} => self.ctoken = Some(continuation_endpoint.continuation_command.token),
|
||||||
|
@ -859,7 +903,7 @@ impl YouTubeListMapper<PlaylistItem> {
|
||||||
self.items.push(mapped);
|
self.items.push(mapped);
|
||||||
}
|
}
|
||||||
YouTubeListItem::LockupViewModel(lockup) => {
|
YouTubeListItem::LockupViewModel(lockup) => {
|
||||||
if let Some(mapped) = self.map_lockup(lockup) {
|
if let Some(YouTubeItem::Playlist(mapped)) = self.map_lockup(lockup) {
|
||||||
self.items.push(mapped);
|
self.items.push(mapped);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,732 @@
|
||||||
|
---
|
||||||
|
source: src/client/video_details.rs
|
||||||
|
expression: map_res.c
|
||||||
|
---
|
||||||
|
VideoDetails(
|
||||||
|
id: "XuM2onMGvTI",
|
||||||
|
name: "Gäa",
|
||||||
|
description: RichText([
|
||||||
|
Text(
|
||||||
|
text: "Provided to YouTube by Universal Music Group\n\nGäa · Oonagh\n\nBest Of\n\n℗ An Airforce1 Records / We Love Music recording; ℗ 2014 Universal Music GmbH\n\nReleased on: 2020-08-07\n\nProducer, Associated Performer, Background Vocalist: Hardy Krech\nProducer: Mark Nissen\nAssociated Performer, Background Vocalist: Andreas Fahnert\nAssociated Performer, Background Vocalist: Velile Mchunu\nAssociated Performer, Background Vocalist: Billy King\nAssociated Performer, Background Vocalist: Alex Prince\nAssociated Performer, Flute: Sandro Friedrich\nProgrammer: Hartmut Krech\nEditor: Severin Zahler\nComposer Lyricist: Hartmut Krech\nComposer Lyricist: Mark Nissen\nAuthor: Lukas Hainer\nAuthor: Michael Boden\n\nAuto-generated by YouTube.",
|
||||||
|
),
|
||||||
|
]),
|
||||||
|
channel: ChannelTag(
|
||||||
|
id: "UCVGvnqB-5znqPSbMGlhF4Pw",
|
||||||
|
name: "Sentamusic",
|
||||||
|
avatar: [
|
||||||
|
Thumbnail(
|
||||||
|
url: "https://yt3.ggpht.com/VPr2HRKMco6qVkF1mr4KI_g_autDEE0KKEt3ZBfQdnETGAV0QWROheWVzExnPva4yJAz1unz=s48-c-k-c0x00ffffff-no-rj",
|
||||||
|
width: 48,
|
||||||
|
height: 48,
|
||||||
|
),
|
||||||
|
Thumbnail(
|
||||||
|
url: "https://yt3.ggpht.com/VPr2HRKMco6qVkF1mr4KI_g_autDEE0KKEt3ZBfQdnETGAV0QWROheWVzExnPva4yJAz1unz=s88-c-k-c0x00ffffff-no-rj",
|
||||||
|
width: 88,
|
||||||
|
height: 88,
|
||||||
|
),
|
||||||
|
Thumbnail(
|
||||||
|
url: "https://yt3.ggpht.com/VPr2HRKMco6qVkF1mr4KI_g_autDEE0KKEt3ZBfQdnETGAV0QWROheWVzExnPva4yJAz1unz=s176-c-k-c0x00ffffff-no-rj",
|
||||||
|
width: 176,
|
||||||
|
height: 176,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
verification: artist,
|
||||||
|
subscriber_count: Some(38100),
|
||||||
|
),
|
||||||
|
view_count: 28898,
|
||||||
|
like_count: Some(213),
|
||||||
|
publish_date: "[date]",
|
||||||
|
publish_date_txt: Some("Aug 6, 2020"),
|
||||||
|
is_live: false,
|
||||||
|
is_ccommons: false,
|
||||||
|
chapters: [],
|
||||||
|
recommended: Paginator(
|
||||||
|
count: None,
|
||||||
|
items: [
|
||||||
|
VideoItem(
|
||||||
|
id: "E8XaMMeUX7M",
|
||||||
|
name: "Sie singt für die, die sie nicht hören",
|
||||||
|
duration: Some(245),
|
||||||
|
thumbnail: [
|
||||||
|
Thumbnail(
|
||||||
|
url: "https://i.ytimg.com/vi/E8XaMMeUX7M/hqdefault.jpg?sqp=-oaymwEbCKgBEF5IVfKriqkDDggBFQAAiEIYAXABwAEG&rs=AOn4CLDpslNXCsGByXyek5rqUTlPduA6PQ",
|
||||||
|
width: 168,
|
||||||
|
height: 94,
|
||||||
|
),
|
||||||
|
Thumbnail(
|
||||||
|
url: "https://i.ytimg.com/vi/E8XaMMeUX7M/hqdefault.jpg?sqp=-oaymwEcCNACELwBSFXyq4qpAw4IARUAAIhCGAFwAcABBg==&rs=AOn4CLCiR5P4H39iF_JpaRsWj33p6xILZQ",
|
||||||
|
width: 336,
|
||||||
|
height: 188,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
channel: Some(ChannelTag(
|
||||||
|
id: "UCVGvnqB-5znqPSbMGlhF4Pw",
|
||||||
|
name: "Sentamusic",
|
||||||
|
avatar: [
|
||||||
|
Thumbnail(
|
||||||
|
url: "https://yt3.ggpht.com/VPr2HRKMco6qVkF1mr4KI_g_autDEE0KKEt3ZBfQdnETGAV0QWROheWVzExnPva4yJAz1unz=s88-c-k-c0x00ffffff-no-rj",
|
||||||
|
width: 68,
|
||||||
|
height: 68,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
verification: artist,
|
||||||
|
subscriber_count: None,
|
||||||
|
)),
|
||||||
|
publish_date: "[date]",
|
||||||
|
publish_date_txt: Some("6 years ago"),
|
||||||
|
view_count: Some(127967),
|
||||||
|
is_live: false,
|
||||||
|
is_short: false,
|
||||||
|
is_upcoming: false,
|
||||||
|
short_description: None,
|
||||||
|
),
|
||||||
|
VideoItem(
|
||||||
|
id: "gJapS0meSlc",
|
||||||
|
name: "Kingdom of Heaven: Burning the Past Extended (20 minutes version)",
|
||||||
|
duration: Some(1204),
|
||||||
|
thumbnail: [
|
||||||
|
Thumbnail(
|
||||||
|
url: "https://i.ytimg.com/vi/gJapS0meSlc/hqdefault.jpg?sqp=-oaymwEbCKgBEF5IVfKriqkDDggBFQAAiEIYAXABwAEG&rs=AOn4CLAfLAy2Om8QBrXljaToKGg4EAmy6w",
|
||||||
|
width: 168,
|
||||||
|
height: 94,
|
||||||
|
),
|
||||||
|
Thumbnail(
|
||||||
|
url: "https://i.ytimg.com/vi/gJapS0meSlc/hqdefault.jpg?sqp=-oaymwEcCNACELwBSFXyq4qpAw4IARUAAIhCGAFwAcABBg==&rs=AOn4CLBI4RQ40zY_KOh8qZG4991VueDo3w",
|
||||||
|
width: 336,
|
||||||
|
height: 188,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
channel: Some(ChannelTag(
|
||||||
|
id: "UC8N3VCj1kY6VB3kiDh38bxw",
|
||||||
|
name: "Encosen",
|
||||||
|
avatar: [
|
||||||
|
Thumbnail(
|
||||||
|
url: "https://yt3.ggpht.com/ytc/AIdro_knaJfooIGRk_MXCNQLk5Zmx79JCKWsLMOl1LdlTZdnsg=s68-c-k-c0x00ffffff-no-rj",
|
||||||
|
width: 68,
|
||||||
|
height: 68,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
verification: none,
|
||||||
|
subscriber_count: None,
|
||||||
|
)),
|
||||||
|
publish_date: "[date]",
|
||||||
|
publish_date_txt: Some("4 years ago"),
|
||||||
|
view_count: Some(243357),
|
||||||
|
is_live: false,
|
||||||
|
is_short: false,
|
||||||
|
is_upcoming: false,
|
||||||
|
short_description: None,
|
||||||
|
),
|
||||||
|
VideoItem(
|
||||||
|
id: "u2XCC1rKxV0",
|
||||||
|
name: "Faolan",
|
||||||
|
duration: Some(256),
|
||||||
|
thumbnail: [
|
||||||
|
Thumbnail(
|
||||||
|
url: "https://i.ytimg.com/vi/u2XCC1rKxV0/hqdefault.jpg?sqp=-oaymwEbCKgBEF5IVfKriqkDDggBFQAAiEIYAXABwAEG&rs=AOn4CLDd4Q0v9znAMTV6GLYt4Jq40MWX0w",
|
||||||
|
width: 168,
|
||||||
|
height: 94,
|
||||||
|
),
|
||||||
|
Thumbnail(
|
||||||
|
url: "https://i.ytimg.com/vi/u2XCC1rKxV0/hqdefault.jpg?sqp=-oaymwEcCNACELwBSFXyq4qpAw4IARUAAIhCGAFwAcABBg==&rs=AOn4CLBpNh48nrIpf9g319CFxdew6geBNg",
|
||||||
|
width: 336,
|
||||||
|
height: 188,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
channel: Some(ChannelTag(
|
||||||
|
id: "UCVGvnqB-5znqPSbMGlhF4Pw",
|
||||||
|
name: "Sentamusic",
|
||||||
|
avatar: [
|
||||||
|
Thumbnail(
|
||||||
|
url: "https://yt3.ggpht.com/VPr2HRKMco6qVkF1mr4KI_g_autDEE0KKEt3ZBfQdnETGAV0QWROheWVzExnPva4yJAz1unz=s88-c-k-c0x00ffffff-no-rj",
|
||||||
|
width: 68,
|
||||||
|
height: 68,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
verification: artist,
|
||||||
|
subscriber_count: None,
|
||||||
|
)),
|
||||||
|
publish_date: "[date]",
|
||||||
|
publish_date_txt: Some("4 years ago"),
|
||||||
|
view_count: Some(25802),
|
||||||
|
is_live: false,
|
||||||
|
is_short: false,
|
||||||
|
is_upcoming: false,
|
||||||
|
short_description: None,
|
||||||
|
),
|
||||||
|
VideoItem(
|
||||||
|
id: "C_pGRMlCM3U",
|
||||||
|
name: "Oonagh - Gäa [Offizielles Musikvideo]",
|
||||||
|
duration: Some(247),
|
||||||
|
thumbnail: [
|
||||||
|
Thumbnail(
|
||||||
|
url: "https://i.ytimg.com/vi/C_pGRMlCM3U/hqdefault.jpg?sqp=-oaymwE1CKgBEF5IVfKriqkDKAgBFQAAiEIYAXABwAEG8AEB-AH-CYAC0AWKAgwIABABGEMgZSguMA8=&rs=AOn4CLB_vkbQhRUgH75tZQVVJLFPs7K8sg",
|
||||||
|
width: 168,
|
||||||
|
height: 94,
|
||||||
|
),
|
||||||
|
Thumbnail(
|
||||||
|
url: "https://i.ytimg.com/vi/C_pGRMlCM3U/hqdefault.jpg?sqp=-oaymwE2CNACELwBSFXyq4qpAygIARUAAIhCGAFwAcABBvABAfgB_gmAAtAFigIMCAAQARhDIGUoLjAP&rs=AOn4CLBJX6P3v_qpy--7IRxCBdygxb9ZoA",
|
||||||
|
width: 336,
|
||||||
|
height: 188,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
channel: Some(ChannelTag(
|
||||||
|
id: "UCD8hHNW3x7CA4M0Z1pi5NRQ",
|
||||||
|
name: "AIRFORCE1.TV",
|
||||||
|
avatar: [
|
||||||
|
Thumbnail(
|
||||||
|
url: "https://yt3.ggpht.com/ytc/AIdro_lVsvv0Zc8K2igVxGy6UQYJqmFHouOR3ux9zH74iv30eQ=s68-c-k-c0x00ffffff-no-rj",
|
||||||
|
width: 68,
|
||||||
|
height: 68,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
verification: none,
|
||||||
|
subscriber_count: None,
|
||||||
|
)),
|
||||||
|
publish_date: "[date]",
|
||||||
|
publish_date_txt: Some("10 years ago"),
|
||||||
|
view_count: Some(14449259),
|
||||||
|
is_live: false,
|
||||||
|
is_short: false,
|
||||||
|
is_upcoming: false,
|
||||||
|
short_description: None,
|
||||||
|
),
|
||||||
|
VideoItem(
|
||||||
|
id: "UtP9J88Jzg0",
|
||||||
|
name: "Ruinen im Sand",
|
||||||
|
duration: Some(195),
|
||||||
|
thumbnail: [
|
||||||
|
Thumbnail(
|
||||||
|
url: "https://i.ytimg.com/vi/UtP9J88Jzg0/hqdefault.jpg?sqp=-oaymwEbCKgBEF5IVfKriqkDDggBFQAAiEIYAXABwAEG&rs=AOn4CLCmkebFxTlBZCOUHBFkMHv0DodLFQ",
|
||||||
|
width: 168,
|
||||||
|
height: 94,
|
||||||
|
),
|
||||||
|
Thumbnail(
|
||||||
|
url: "https://i.ytimg.com/vi/UtP9J88Jzg0/hqdefault.jpg?sqp=-oaymwEcCNACELwBSFXyq4qpAw4IARUAAIhCGAFwAcABBg==&rs=AOn4CLAOdwJVkok-U7P1YrZEchZZqY_HlQ",
|
||||||
|
width: 336,
|
||||||
|
height: 188,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
channel: Some(ChannelTag(
|
||||||
|
id: "UCVGvnqB-5znqPSbMGlhF4Pw",
|
||||||
|
name: "Sentamusic",
|
||||||
|
avatar: [
|
||||||
|
Thumbnail(
|
||||||
|
url: "https://yt3.ggpht.com/VPr2HRKMco6qVkF1mr4KI_g_autDEE0KKEt3ZBfQdnETGAV0QWROheWVzExnPva4yJAz1unz=s88-c-k-c0x00ffffff-no-rj",
|
||||||
|
width: 68,
|
||||||
|
height: 68,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
verification: artist,
|
||||||
|
subscriber_count: None,
|
||||||
|
)),
|
||||||
|
publish_date: "[date]",
|
||||||
|
publish_date_txt: Some("4 years ago"),
|
||||||
|
view_count: Some(66406),
|
||||||
|
is_live: false,
|
||||||
|
is_short: false,
|
||||||
|
is_upcoming: false,
|
||||||
|
short_description: None,
|
||||||
|
),
|
||||||
|
VideoItem(
|
||||||
|
id: "Vu-6Er21_bM",
|
||||||
|
name: "Mutter Erde",
|
||||||
|
duration: Some(193),
|
||||||
|
thumbnail: [
|
||||||
|
Thumbnail(
|
||||||
|
url: "https://i.ytimg.com/vi/Vu-6Er21_bM/hqdefault.jpg?sqp=-oaymwEbCKgBEF5IVfKriqkDDggBFQAAiEIYAXABwAEG&rs=AOn4CLDvKXWNaaAND_YpZMz6MohvZaHcBw",
|
||||||
|
width: 168,
|
||||||
|
height: 94,
|
||||||
|
),
|
||||||
|
Thumbnail(
|
||||||
|
url: "https://i.ytimg.com/vi/Vu-6Er21_bM/hqdefault.jpg?sqp=-oaymwEcCNACELwBSFXyq4qpAw4IARUAAIhCGAFwAcABBg==&rs=AOn4CLDY7VmNj1HMKx4iaRKrgT2s5AJpqw",
|
||||||
|
width: 336,
|
||||||
|
height: 188,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
channel: Some(ChannelTag(
|
||||||
|
id: "UCkYFRZTBmE1IzaKNuF6B3Uw",
|
||||||
|
name: "Story Of Dakota - Topic",
|
||||||
|
avatar: [
|
||||||
|
Thumbnail(
|
||||||
|
url: "https://yt3.ggpht.com/G1umXDgUfbA4XATwAC9Hb3RvQuFsNgE1k-WbtgSQUjMtSXU3SZdp5Se25A2H2xJcjgKWZxWxPQ=s68-c-k-c0x00ffffff-no-rj",
|
||||||
|
width: 68,
|
||||||
|
height: 68,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
verification: none,
|
||||||
|
subscriber_count: None,
|
||||||
|
)),
|
||||||
|
publish_date: "[date]",
|
||||||
|
publish_date_txt: Some("6 years ago"),
|
||||||
|
view_count: Some(34662),
|
||||||
|
is_live: false,
|
||||||
|
is_short: false,
|
||||||
|
is_upcoming: false,
|
||||||
|
short_description: None,
|
||||||
|
),
|
||||||
|
VideoItem(
|
||||||
|
id: "nLmNchGTh20",
|
||||||
|
name: "Oonagh - Kuliko Jana - Eine neue Zeit",
|
||||||
|
duration: Some(214),
|
||||||
|
thumbnail: [
|
||||||
|
Thumbnail(
|
||||||
|
url: "https://i.ytimg.com/vi/nLmNchGTh20/hqdefault.jpg?sqp=-oaymwEbCKgBEF5IVfKriqkDDggBFQAAiEIYAXABwAEG&rs=AOn4CLCKWMorJR33a_BMctF8siBEPYPvSQ",
|
||||||
|
width: 168,
|
||||||
|
height: 94,
|
||||||
|
),
|
||||||
|
Thumbnail(
|
||||||
|
url: "https://i.ytimg.com/vi/nLmNchGTh20/hqdefault.jpg?sqp=-oaymwEcCNACELwBSFXyq4qpAw4IARUAAIhCGAFwAcABBg==&rs=AOn4CLBb_bEgnelYdP-J1piDHKQK4D0lBA",
|
||||||
|
width: 336,
|
||||||
|
height: 188,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
channel: Some(ChannelTag(
|
||||||
|
id: "UCUJHYmJ3_1Kwfs3lqYc_Rxg",
|
||||||
|
name: "ICH FIND SCHLAGER TOLL",
|
||||||
|
avatar: [
|
||||||
|
Thumbnail(
|
||||||
|
url: "https://yt3.ggpht.com/An23BS5d6Fbk1nRfF5PZQaOijfmnVWgI7XCjhFl0pwWAthZ1Ayw-4ZG6_zwkxCaBKfesXBmO=s68-c-k-c0x00ffffff-no-rj",
|
||||||
|
width: 68,
|
||||||
|
height: 68,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
verification: verified,
|
||||||
|
subscriber_count: None,
|
||||||
|
)),
|
||||||
|
publish_date: "[date]",
|
||||||
|
publish_date_txt: Some("5 years ago"),
|
||||||
|
view_count: Some(1841784),
|
||||||
|
is_live: false,
|
||||||
|
is_short: false,
|
||||||
|
is_upcoming: false,
|
||||||
|
short_description: None,
|
||||||
|
),
|
||||||
|
VideoItem(
|
||||||
|
id: "Ctpe9kafn78",
|
||||||
|
name: "So still mein Herz",
|
||||||
|
duration: Some(259),
|
||||||
|
thumbnail: [
|
||||||
|
Thumbnail(
|
||||||
|
url: "https://i.ytimg.com/vi/Ctpe9kafn78/hqdefault.jpg?sqp=-oaymwEbCKgBEF5IVfKriqkDDggBFQAAiEIYAXABwAEG&rs=AOn4CLDJqIxVeJPDgMFXTavr1aaYBuaY6w",
|
||||||
|
width: 168,
|
||||||
|
height: 94,
|
||||||
|
),
|
||||||
|
Thumbnail(
|
||||||
|
url: "https://i.ytimg.com/vi/Ctpe9kafn78/hqdefault.jpg?sqp=-oaymwEcCNACELwBSFXyq4qpAw4IARUAAIhCGAFwAcABBg==&rs=AOn4CLBxlTlqj7JCTsvoQdWyMkB_JZJ1dA",
|
||||||
|
width: 336,
|
||||||
|
height: 188,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
channel: Some(ChannelTag(
|
||||||
|
id: "UCVGvnqB-5znqPSbMGlhF4Pw",
|
||||||
|
name: "Sentamusic",
|
||||||
|
avatar: [
|
||||||
|
Thumbnail(
|
||||||
|
url: "https://yt3.ggpht.com/VPr2HRKMco6qVkF1mr4KI_g_autDEE0KKEt3ZBfQdnETGAV0QWROheWVzExnPva4yJAz1unz=s88-c-k-c0x00ffffff-no-rj",
|
||||||
|
width: 68,
|
||||||
|
height: 68,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
verification: artist,
|
||||||
|
subscriber_count: None,
|
||||||
|
)),
|
||||||
|
publish_date: "[date]",
|
||||||
|
publish_date_txt: Some("4 years ago"),
|
||||||
|
view_count: Some(48241),
|
||||||
|
is_live: false,
|
||||||
|
is_short: false,
|
||||||
|
is_upcoming: false,
|
||||||
|
short_description: None,
|
||||||
|
),
|
||||||
|
VideoItem(
|
||||||
|
id: "sg6j-zfUF_A",
|
||||||
|
name: "Eldamar",
|
||||||
|
duration: Some(223),
|
||||||
|
thumbnail: [
|
||||||
|
Thumbnail(
|
||||||
|
url: "https://i.ytimg.com/vi/sg6j-zfUF_A/hqdefault.jpg?sqp=-oaymwEbCKgBEF5IVfKriqkDDggBFQAAiEIYAXABwAEG&rs=AOn4CLDk_V_5woDmnABodJnokWXNeyUulg",
|
||||||
|
width: 168,
|
||||||
|
height: 94,
|
||||||
|
),
|
||||||
|
Thumbnail(
|
||||||
|
url: "https://i.ytimg.com/vi/sg6j-zfUF_A/hqdefault.jpg?sqp=-oaymwEcCNACELwBSFXyq4qpAw4IARUAAIhCGAFwAcABBg==&rs=AOn4CLCFtDJv4zRfP6XgwasjaN_nvdfG7Q",
|
||||||
|
width: 336,
|
||||||
|
height: 188,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
channel: Some(ChannelTag(
|
||||||
|
id: "UCVGvnqB-5znqPSbMGlhF4Pw",
|
||||||
|
name: "Sentamusic",
|
||||||
|
avatar: [
|
||||||
|
Thumbnail(
|
||||||
|
url: "https://yt3.ggpht.com/VPr2HRKMco6qVkF1mr4KI_g_autDEE0KKEt3ZBfQdnETGAV0QWROheWVzExnPva4yJAz1unz=s88-c-k-c0x00ffffff-no-rj",
|
||||||
|
width: 68,
|
||||||
|
height: 68,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
verification: artist,
|
||||||
|
subscriber_count: None,
|
||||||
|
)),
|
||||||
|
publish_date: "[date]",
|
||||||
|
publish_date_txt: Some("4 years ago"),
|
||||||
|
view_count: Some(11079),
|
||||||
|
is_live: false,
|
||||||
|
is_short: false,
|
||||||
|
is_upcoming: false,
|
||||||
|
short_description: None,
|
||||||
|
),
|
||||||
|
VideoItem(
|
||||||
|
id: "mabaKE-xNUo",
|
||||||
|
name: "Celtic Woman - Tír na nÓg (feat Oonagh) [Official Music Video]",
|
||||||
|
duration: Some(196),
|
||||||
|
thumbnail: [
|
||||||
|
Thumbnail(
|
||||||
|
url: "https://i.ytimg.com/vi/mabaKE-xNUo/hqdefault.jpg?sqp=-oaymwEbCKgBEF5IVfKriqkDDggBFQAAiEIYAXABwAEG&rs=AOn4CLBZn8xM0oEJ6MDxWRRZ7jqqDvCrAw",
|
||||||
|
width: 168,
|
||||||
|
height: 94,
|
||||||
|
),
|
||||||
|
Thumbnail(
|
||||||
|
url: "https://i.ytimg.com/vi/mabaKE-xNUo/hqdefault.jpg?sqp=-oaymwEcCNACELwBSFXyq4qpAw4IARUAAIhCGAFwAcABBg==&rs=AOn4CLDfLAhbREVerzlQTKI3c6b7-xJYmw",
|
||||||
|
width: 336,
|
||||||
|
height: 188,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
channel: Some(ChannelTag(
|
||||||
|
id: "UCUt83CEM-kE3OQx0GqLdZtw",
|
||||||
|
name: "Universal Music Deutschland",
|
||||||
|
avatar: [
|
||||||
|
Thumbnail(
|
||||||
|
url: "https://yt3.ggpht.com/ytc/AIdro_neU_ZtLnU0iv2J6Qhh-m-SdyrGBH5EY6OA_eLP35vUqA=s68-c-k-c0x00ffffff-no-rj",
|
||||||
|
width: 68,
|
||||||
|
height: 68,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
verification: verified,
|
||||||
|
subscriber_count: None,
|
||||||
|
)),
|
||||||
|
publish_date: "[date]",
|
||||||
|
publish_date_txt: Some("9 years ago"),
|
||||||
|
view_count: Some(10097065),
|
||||||
|
is_live: false,
|
||||||
|
is_short: false,
|
||||||
|
is_upcoming: false,
|
||||||
|
short_description: None,
|
||||||
|
),
|
||||||
|
VideoItem(
|
||||||
|
id: "BQIbe3nNrLs",
|
||||||
|
name: "Senta - Egal wie Weit (Offizielles Video)",
|
||||||
|
duration: Some(166),
|
||||||
|
thumbnail: [
|
||||||
|
Thumbnail(
|
||||||
|
url: "https://i.ytimg.com/vi/BQIbe3nNrLs/hqdefault.jpg?sqp=-oaymwEbCKgBEF5IVfKriqkDDggBFQAAiEIYAXABwAEG&rs=AOn4CLCozZphHkPbL1-MzdpLOTSONm-xPQ",
|
||||||
|
width: 168,
|
||||||
|
height: 94,
|
||||||
|
),
|
||||||
|
Thumbnail(
|
||||||
|
url: "https://i.ytimg.com/vi/BQIbe3nNrLs/hqdefault.jpg?sqp=-oaymwEcCNACELwBSFXyq4qpAw4IARUAAIhCGAFwAcABBg==&rs=AOn4CLC2zkNbvfvCy95_KE9yP0CH2a_Wgw",
|
||||||
|
width: 336,
|
||||||
|
height: 188,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
channel: Some(ChannelTag(
|
||||||
|
id: "UCVGvnqB-5znqPSbMGlhF4Pw",
|
||||||
|
name: "Sentamusic",
|
||||||
|
avatar: [
|
||||||
|
Thumbnail(
|
||||||
|
url: "https://yt3.ggpht.com/VPr2HRKMco6qVkF1mr4KI_g_autDEE0KKEt3ZBfQdnETGAV0QWROheWVzExnPva4yJAz1unz=s88-c-k-c0x00ffffff-no-rj",
|
||||||
|
width: 68,
|
||||||
|
height: 68,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
verification: artist,
|
||||||
|
subscriber_count: None,
|
||||||
|
)),
|
||||||
|
publish_date: "[date]",
|
||||||
|
publish_date_txt: Some("1 year ago"),
|
||||||
|
view_count: Some(199272),
|
||||||
|
is_live: false,
|
||||||
|
is_short: false,
|
||||||
|
is_upcoming: false,
|
||||||
|
short_description: None,
|
||||||
|
),
|
||||||
|
VideoItem(
|
||||||
|
id: "6jkNLisOu6g",
|
||||||
|
name: "01 Aulë und Yavanna",
|
||||||
|
duration: Some(218),
|
||||||
|
thumbnail: [
|
||||||
|
Thumbnail(
|
||||||
|
url: "https://i.ytimg.com/vi/6jkNLisOu6g/hqdefault.jpg?sqp=-oaymwE1CKgBEF5IVfKriqkDKAgBFQAAiEIYAXABwAEG8AEB-AHeA4AC4AOKAgwIABABGEkgYChlMA8=&rs=AOn4CLDl2aKLj-BQchX2vOtKn59s5iYt9A",
|
||||||
|
width: 168,
|
||||||
|
height: 94,
|
||||||
|
),
|
||||||
|
Thumbnail(
|
||||||
|
url: "https://i.ytimg.com/vi/6jkNLisOu6g/hqdefault.jpg?sqp=-oaymwE2CNACELwBSFXyq4qpAygIARUAAIhCGAFwAcABBvABAfgB3gOAAuADigIMCAAQARhJIGAoZTAP&rs=AOn4CLAsr456CW0aDfmJbwNcl31xgr_6sw",
|
||||||
|
width: 336,
|
||||||
|
height: 188,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
channel: Some(ChannelTag(
|
||||||
|
id: "UCawJ5ocVllDHdba04OoiNgw",
|
||||||
|
name: "T\'Owd Cheshire Tup",
|
||||||
|
avatar: [
|
||||||
|
Thumbnail(
|
||||||
|
url: "https://yt3.ggpht.com/ytc/AIdro_nzZfr0zDdl4lpjqQZYVgr-tTjAwPzHUKsYCp9vRsE=s68-c-k-c0x00ffffff-no-rj",
|
||||||
|
width: 68,
|
||||||
|
height: 68,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
verification: none,
|
||||||
|
subscriber_count: None,
|
||||||
|
)),
|
||||||
|
publish_date: "[date]",
|
||||||
|
publish_date_txt: Some("5 years ago"),
|
||||||
|
view_count: Some(9826),
|
||||||
|
is_live: false,
|
||||||
|
is_short: false,
|
||||||
|
is_upcoming: false,
|
||||||
|
short_description: None,
|
||||||
|
),
|
||||||
|
VideoItem(
|
||||||
|
id: "vnjva6Q2NMw",
|
||||||
|
name: "#Oonagh #OonaghLive | Oonagh | Show completo • Full concert (2017) / 480p",
|
||||||
|
duration: Some(2407),
|
||||||
|
thumbnail: [
|
||||||
|
Thumbnail(
|
||||||
|
url: "https://i.ytimg.com/vi/vnjva6Q2NMw/hqdefault.jpg?sqp=-oaymwEbCKgBEF5IVfKriqkDDggBFQAAiEIYAXABwAEG&rs=AOn4CLCo-TVqOy7D1phoX7z3TnoGSe2wQQ",
|
||||||
|
width: 168,
|
||||||
|
height: 94,
|
||||||
|
),
|
||||||
|
Thumbnail(
|
||||||
|
url: "https://i.ytimg.com/vi/vnjva6Q2NMw/hqdefault.jpg?sqp=-oaymwEcCNACELwBSFXyq4qpAw4IARUAAIhCGAFwAcABBg==&rs=AOn4CLApZ1cuBTMKRWYowCDgEOJ-cEXBdg",
|
||||||
|
width: 336,
|
||||||
|
height: 188,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
channel: Some(ChannelTag(
|
||||||
|
id: "UCXAUp0fwOEZ4Q4ijtco-j-Q",
|
||||||
|
name: "Oonagh Brazil",
|
||||||
|
avatar: [
|
||||||
|
Thumbnail(
|
||||||
|
url: "https://yt3.ggpht.com/ytc/AIdro_lQEvqMeM7OnUcyoPo9r0sfONO3EgcS31imZyqbm16vKA=s68-c-k-c0x00ffffff-no-rj",
|
||||||
|
width: 68,
|
||||||
|
height: 68,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
verification: none,
|
||||||
|
subscriber_count: None,
|
||||||
|
)),
|
||||||
|
publish_date: "[date]",
|
||||||
|
publish_date_txt: Some("4 years ago"),
|
||||||
|
view_count: Some(15623),
|
||||||
|
is_live: false,
|
||||||
|
is_short: false,
|
||||||
|
is_upcoming: false,
|
||||||
|
short_description: None,
|
||||||
|
),
|
||||||
|
VideoItem(
|
||||||
|
id: "kz34YyjG-Oo",
|
||||||
|
name: "CRAZY UNEXPECTED BATTLE combinations on The Voice",
|
||||||
|
duration: Some(2187),
|
||||||
|
thumbnail: [
|
||||||
|
Thumbnail(
|
||||||
|
url: "https://i.ytimg.com/vi/kz34YyjG-Oo/hqdefault.jpg?sqp=-oaymwEbCKgBEF5IVfKriqkDDggBFQAAiEIYAXABwAEG&rs=AOn4CLAXEkH_BO5UX-d12GbXXEk_pAhFhQ",
|
||||||
|
width: 168,
|
||||||
|
height: 94,
|
||||||
|
),
|
||||||
|
Thumbnail(
|
||||||
|
url: "https://i.ytimg.com/vi/kz34YyjG-Oo/hqdefault.jpg?sqp=-oaymwEcCNACELwBSFXyq4qpAw4IARUAAIhCGAFwAcABBg==&rs=AOn4CLAGfSNohj4XUlcEncnhvehyehgXZA",
|
||||||
|
width: 336,
|
||||||
|
height: 188,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
channel: Some(ChannelTag(
|
||||||
|
id: "UCJYtYkiGldqX6Ne938j-k2g",
|
||||||
|
name: "The Voice Global",
|
||||||
|
avatar: [
|
||||||
|
Thumbnail(
|
||||||
|
url: "https://yt3.ggpht.com/0Q3BwKKmO0pXApaqRiQr7dTKz-ftpS1TxUtBZ0FGgrMEZhlMjUrwWXQypH7pu3Dg7A7FsbtHQg=s68-c-k-c0x00ffffff-no-rj",
|
||||||
|
width: 68,
|
||||||
|
height: 68,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
verification: verified,
|
||||||
|
subscriber_count: None,
|
||||||
|
)),
|
||||||
|
publish_date: "[date]",
|
||||||
|
publish_date_txt: Some("4 months ago"),
|
||||||
|
view_count: Some(3150637),
|
||||||
|
is_live: false,
|
||||||
|
is_short: false,
|
||||||
|
is_upcoming: false,
|
||||||
|
short_description: None,
|
||||||
|
),
|
||||||
|
VideoItem(
|
||||||
|
id: "2cKs7BcgIfQ",
|
||||||
|
name: "Minne Duett",
|
||||||
|
duration: Some(220),
|
||||||
|
thumbnail: [
|
||||||
|
Thumbnail(
|
||||||
|
url: "https://i.ytimg.com/vi/2cKs7BcgIfQ/hqdefault.jpg?sqp=-oaymwEbCKgBEF5IVfKriqkDDggBFQAAiEIYAXABwAEG&rs=AOn4CLAAvtdLsnu2ea26UIk2lN9tqOr_VA",
|
||||||
|
width: 168,
|
||||||
|
height: 94,
|
||||||
|
),
|
||||||
|
Thumbnail(
|
||||||
|
url: "https://i.ytimg.com/vi/2cKs7BcgIfQ/hqdefault.jpg?sqp=-oaymwEcCNACELwBSFXyq4qpAw4IARUAAIhCGAFwAcABBg==&rs=AOn4CLAYhd8ZhicK5DIND-EZFJ5H2qCgww",
|
||||||
|
width: 336,
|
||||||
|
height: 188,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
channel: Some(ChannelTag(
|
||||||
|
id: "UCxWwz-uZkTNwEM_duLUrWkQ",
|
||||||
|
name: "fauntube",
|
||||||
|
avatar: [
|
||||||
|
Thumbnail(
|
||||||
|
url: "https://yt3.ggpht.com/ytc/AIdro_mP0XxeJzJhZPOtjS8H8UE0v5mt80H91MX6a_kfG2eJgjM=s88-c-k-c0x00ffffff-no-rj",
|
||||||
|
width: 68,
|
||||||
|
height: 68,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
verification: artist,
|
||||||
|
subscriber_count: None,
|
||||||
|
)),
|
||||||
|
publish_date: "[date]",
|
||||||
|
publish_date_txt: Some("6 years ago"),
|
||||||
|
view_count: Some(294858),
|
||||||
|
is_live: false,
|
||||||
|
is_short: false,
|
||||||
|
is_upcoming: false,
|
||||||
|
short_description: None,
|
||||||
|
),
|
||||||
|
VideoItem(
|
||||||
|
id: "ow8xLlqkMuU",
|
||||||
|
name: "Oonagh - Faolan",
|
||||||
|
duration: Some(321),
|
||||||
|
thumbnail: [
|
||||||
|
Thumbnail(
|
||||||
|
url: "https://i.ytimg.com/vi/ow8xLlqkMuU/hqdefault.jpg?sqp=-oaymwEbCKgBEF5IVfKriqkDDggBFQAAiEIYAXABwAEG&rs=AOn4CLCLrrwu8ZDxOLwcLJn8C75tlhyT4A",
|
||||||
|
width: 168,
|
||||||
|
height: 94,
|
||||||
|
),
|
||||||
|
Thumbnail(
|
||||||
|
url: "https://i.ytimg.com/vi/ow8xLlqkMuU/hqdefault.jpg?sqp=-oaymwEcCNACELwBSFXyq4qpAw4IARUAAIhCGAFwAcABBg==&rs=AOn4CLB2IoX8D-ILbTtwSZLHDvL6wfqnVg",
|
||||||
|
width: 336,
|
||||||
|
height: 188,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
channel: Some(ChannelTag(
|
||||||
|
id: "UCopVUcd6jgIxM9YTegO7Hww",
|
||||||
|
name: "Martchen13",
|
||||||
|
avatar: [
|
||||||
|
Thumbnail(
|
||||||
|
url: "https://yt3.ggpht.com/B6tbK2h24H4EBxB8vFikRBd5vI319leVh6FDsxtmx4HLRGPBbMlnrbyB4-vw9q4xehn3yM4i43w=s68-c-k-c0x00ffffff-no-rj",
|
||||||
|
width: 68,
|
||||||
|
height: 68,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
verification: none,
|
||||||
|
subscriber_count: None,
|
||||||
|
)),
|
||||||
|
publish_date: "[date]",
|
||||||
|
publish_date_txt: Some("9 years ago"),
|
||||||
|
view_count: Some(1634446),
|
||||||
|
is_live: false,
|
||||||
|
is_short: false,
|
||||||
|
is_upcoming: false,
|
||||||
|
short_description: None,
|
||||||
|
),
|
||||||
|
VideoItem(
|
||||||
|
id: "IqiTJK_uzUY",
|
||||||
|
name: "Hans Zimmer | ULTIMATE Soundtrack Compilation Mix",
|
||||||
|
duration: Some(3493),
|
||||||
|
thumbnail: [
|
||||||
|
Thumbnail(
|
||||||
|
url: "https://i.ytimg.com/vi/IqiTJK_uzUY/hqdefault.jpg?sqp=-oaymwEbCKgBEF5IVfKriqkDDggBFQAAiEIYAXABwAEG&rs=AOn4CLBYDY-oXJIpwhOqMPF35qtcItpXTw",
|
||||||
|
width: 168,
|
||||||
|
height: 94,
|
||||||
|
),
|
||||||
|
Thumbnail(
|
||||||
|
url: "https://i.ytimg.com/vi/IqiTJK_uzUY/hqdefault.jpg?sqp=-oaymwEcCNACELwBSFXyq4qpAw4IARUAAIhCGAFwAcABBg==&rs=AOn4CLDdEWAy_yZzt80AsnDXMbYJ7SEw-Q",
|
||||||
|
width: 336,
|
||||||
|
height: 188,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
channel: Some(ChannelTag(
|
||||||
|
id: "UCEQKgGFD-oJAl3L0CfTb_1g",
|
||||||
|
name: "Straals",
|
||||||
|
avatar: [
|
||||||
|
Thumbnail(
|
||||||
|
url: "https://yt3.ggpht.com/ytc/AIdro_lPuejfIc_Kf0WuQthwDSelUCruf8WNXWyLXg6yc3qIRA=s68-c-k-c0x00ffffff-no-rj",
|
||||||
|
width: 68,
|
||||||
|
height: 68,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
verification: none,
|
||||||
|
subscriber_count: None,
|
||||||
|
)),
|
||||||
|
publish_date: "[date]",
|
||||||
|
publish_date_txt: Some("5 years ago"),
|
||||||
|
view_count: Some(19574820),
|
||||||
|
is_live: false,
|
||||||
|
is_short: false,
|
||||||
|
is_upcoming: false,
|
||||||
|
short_description: None,
|
||||||
|
),
|
||||||
|
VideoItem(
|
||||||
|
id: "44G9JvrauLg",
|
||||||
|
name: "Tri Martolod",
|
||||||
|
duration: Some(199),
|
||||||
|
thumbnail: [
|
||||||
|
Thumbnail(
|
||||||
|
url: "https://i.ytimg.com/vi/44G9JvrauLg/hqdefault.jpg?sqp=-oaymwEbCKgBEF5IVfKriqkDDggBFQAAiEIYAXABwAEG&rs=AOn4CLAQ3prhoiPh2Be5MN0tkX1gDGWs1w",
|
||||||
|
width: 168,
|
||||||
|
height: 94,
|
||||||
|
),
|
||||||
|
Thumbnail(
|
||||||
|
url: "https://i.ytimg.com/vi/44G9JvrauLg/hqdefault.jpg?sqp=-oaymwEcCNACELwBSFXyq4qpAw4IARUAAIhCGAFwAcABBg==&rs=AOn4CLB9Ow4teYaOtOQkV_AcUhT2-qJ9hw",
|
||||||
|
width: 336,
|
||||||
|
height: 188,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
channel: Some(ChannelTag(
|
||||||
|
id: "UC9qKvxWJEiRxsQnFN0tenyw",
|
||||||
|
name: "Santiano",
|
||||||
|
avatar: [
|
||||||
|
Thumbnail(
|
||||||
|
url: "https://yt3.ggpht.com/daePPPIXMLk1HBFMiESnwZtkEijLne7yeTkM92kupm-ruG4acyPoAuY80mGZLKUUPoBTW0MO=s88-c-k-c0x00ffffff-no-rj",
|
||||||
|
width: 68,
|
||||||
|
height: 68,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
verification: artist,
|
||||||
|
subscriber_count: None,
|
||||||
|
)),
|
||||||
|
publish_date: "[date]",
|
||||||
|
publish_date_txt: Some("6 years ago"),
|
||||||
|
view_count: Some(932727),
|
||||||
|
is_live: false,
|
||||||
|
is_short: false,
|
||||||
|
is_upcoming: false,
|
||||||
|
short_description: None,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
ctoken: Some("CBQSExILWHVNMm9uTUd2VEnAAQHIAQEYACqeDDJzNkw2d3lQQ1FxTUNRb0Q4ajRBQ2c3Q1Bnc0lsNU83LV9URHJMR3lBUW9Pd2o0TENPX085YWFVc0xtMGtnRUtEY0ktQ2dpNG41dXo3N1c2NG5rS0RjSS1DZ2p2dHVQWWl0T0VtSFFLRGNJLUNnal9fYWpTbC0ySTZBWUtEc0ktQ3dqTXlQeUE1b3l0MjljQkNnM0NQZ29JMWVQVzZKV1J3NEJTQ2czQ1Bnb0lpTjJYbHJHODc4WVFDZzdDUGdzSXlyMkw2NkhLdm9ya0FRb093ajRMQ0steDVKNjF0LWI0bndFS0RjSS1DZ2k2aWNfQWdJN2h6RG9LRHNJLUN3akc1T1RibnZDUzl0NEJDZzdDUGdzSXd2cnZ6LWZMN3NYUUFRb053ajRLQ091S2w5SHozTFBwT2dvT3dqNExDSzZUajduSXhiS0lvd0VLRGNJLUNnajExWkhpcHViYjhSTUtEY0ktQ2dqeDc0bWtnYlNQb1I0S0RjSS1DZ2kwLVlXSXl1aU43endLRHNJLUN3ajhpSmlkM1lhVWdwRUJDZzdDUGdzSTFwdWE4Y0NTaU9iakFRb093ajRMQ1B5SHNwakI5dHFxekFFS0RjSS1DZ2pQZ3NuSzZMZWF5aTBLRGNJLUNnaXI0OGZKcmZPYXpHUUtEc0ktQ3dqaHI5X210YlRsb2JVQkNnN0NQZ3NJdl83dHU3dXUxb3VBQVFvRDhqNEFDZzNDUGdvSXM3X1J2SXpHOXVJVENnUHlQZ0FLRHNJLUN3alhsUG5NdEttcXk0QUJDZ1B5UGdBS0o5SS1KQW9pVUV4aGFGSktXRWczV0ZCSWJWZE9TemMwVm5wM1NVWXljWFZYY1dSR1NWZDJjZ29EOGo0QUNnN0NQZ3NJM1lxcjFyWEI4TEs3QVFvRDhqNEFDZzNDUGdvSTllYUl5c3pJa2YwTENnUHlQZ0FLSDlJLUhBb2FVa1JGVFdRNFVGcEpkamxEVUhOMmRrVkVZbTlmY0ZWRU5IY0tBX0ktQUFvTndqNEtDSTJjcF9qOHBQX3BVZ29EOGo0QUNnM0NQZ29Jc192WDdhdkM3dmRXQ2dQeVBnQUtEc0ktQ3dqdGpzNk1vYTdqM0p3QkNnUHlQZ0FLRGNJLUNnaV92XzYwNU42WDdRb0tBX0ktQUFvT3dqNExDUEN2MEw2el82aUhzZ0VLQV9JLUFBb093ajRMQ01ycXhQMkV4YmJUbVFFS0FfSS1BQW9Od2o0S0NMdlp0czYzNzRhQkJRb0Q4ajRBQ2c3Q1Bnc0lxUGU2Mk9LbHc1enFBUW9EOGo0QUNnN0NQZ3NJek9uWW9icnR1N3ktQVFvRDhqNEFDZzdDUGdzSTZ2R2J4cktNX3A2VEFRb0Q4ajRBQ2c3Q1Bnc0k5TU9BdWNHZHEtSFpBUW9EOGo0QUNnN0NQZ3NJNWVXUTFlV2x6SWVqQVFvRDhqNEFDZzNDUGdvSXhwcTdfOHJrcE5RaUNnUHlQZ0FLRHNJLUN3aTQ4ZXJXNzZUdndPTUJFaFVBR2h3ZUlDSWtKaWdxTEM0d01qUTJPRG84UGtBYUJBZ0FFQUVhQkFnQUVBSWFCQWdBRUFNYUJBZ0FFQVFhQkFnQUVBVWFCQWdBRUFZYUJBZ0FFQWNhQkFnQUVBZ2FCQWdBRUFrYUJBZ0FFQW9hQkFnQUVBc2FCQWdBRUF3YUJBZ0FFQTBhQkFnQUVBNGFCQWdBRUE4YUJBZ0FFQkFhQkFnQUVCRWFCQWdBRUJJYUJBZ0FFQk1hQkFnQUVCUWFCQWdBRUJVYUJBZ0FFQllhQkFnQUVCY2FCQWdBRUJnYUJBZ0FFQmthQkFnYUVCc2FCQWdjRUIwYUJBZ2VFQjhhQkFnZ0VDRWFCQWdpRUNNYUJBZ2tFQ1VhQkFnbUVDY2FCQWdvRUNrYUJBZ3FFQ3NhQkFnc0VDMGFCQWd1RUM4YUJBZ3dFREVhQkFneUVETWFCQWcwRURVYUJBZzJFRGNhQkFnNEVEa2FCQWc2RURzYUJBZzhFRDBhQkFnLUVEOGFCQWhBRUVFcUZRQWFIQjRnSWlRbUtDb3NMakF5TkRZNE9qdy1RQWoPd2F0Y2gtbmV4dC1mZWVk"),
|
||||||
|
visitor_data: Some("CgthU0ZEaGhJbEgwVyiJp8C5BjIKCgJVUxIEEgAgEQ%3D%3D"),
|
||||||
|
endpoint: next,
|
||||||
|
),
|
||||||
|
top_comments: Paginator(
|
||||||
|
count: Some(2),
|
||||||
|
items: [],
|
||||||
|
ctoken: Some("Eg0SC1h1TTJvbk1HdlRJGAYyJSIRIgtYdU0yb25NR3ZUSTAAeAJCEGNvbW1lbnRzLXNlY3Rpb24%3D"),
|
||||||
|
visitor_data: Some("CgthU0ZEaGhJbEgwVyiJp8C5BjIKCgJVUxIEEgAgEQ%3D%3D"),
|
||||||
|
endpoint: next,
|
||||||
|
),
|
||||||
|
latest_comments: Paginator(
|
||||||
|
count: Some(2),
|
||||||
|
items: [],
|
||||||
|
ctoken: Some("Eg0SC1h1TTJvbk1HdlRJGAYyOCIRIgtYdU0yb25NR3ZUSTABeAIwAUIhZW5nYWdlbWVudC1wYW5lbC1jb21tZW50cy1zZWN0aW9u"),
|
||||||
|
visitor_data: Some("CgthU0ZEaGhJbEgwVyiJp8C5BjIKCgJVUxIEEgAgEQ%3D%3D"),
|
||||||
|
endpoint: next,
|
||||||
|
),
|
||||||
|
visitor_data: Some("CgthU0ZEaGhJbEgwVyiJp8C5BjIKCgJVUxIEEgAgEQ%3D%3D"),
|
||||||
|
)
|
|
@ -252,6 +252,7 @@ impl MapResponse<VideoDetails> for response::VideoDetails {
|
||||||
text,
|
text,
|
||||||
page_type,
|
page_type,
|
||||||
browse_id,
|
browse_id,
|
||||||
|
..
|
||||||
} => match page_type {
|
} => match page_type {
|
||||||
response::url_endpoint::PageType::Channel => (browse_id, text),
|
response::url_endpoint::PageType::Channel => (browse_id, text),
|
||||||
_ => {
|
_ => {
|
||||||
|
@ -657,6 +658,7 @@ mod tests {
|
||||||
#[case::ab_new_cont("20221011_new_continuation", "ZeerrnuLi5E")]
|
#[case::ab_new_cont("20221011_new_continuation", "ZeerrnuLi5E")]
|
||||||
#[case::ab_no_recommends("20221011_rec_isr", "nFDBxBUfE74")]
|
#[case::ab_no_recommends("20221011_rec_isr", "nFDBxBUfE74")]
|
||||||
#[case::ab_new_likes("20231103_likes", "ZeerrnuLi5E")]
|
#[case::ab_new_likes("20231103_likes", "ZeerrnuLi5E")]
|
||||||
|
#[case::mix("20241109_mix", "XuM2onMGvTI")]
|
||||||
fn map_video_details(#[case] name: &str, #[case] id: &str) {
|
fn map_video_details(#[case] name: &str, #[case] id: &str) {
|
||||||
let json_path = path!(*TESTFILES / "video_details" / format!("video_details_{name}.json"));
|
let json_path = path!(*TESTFILES / "video_details" / format!("video_details_{name}.json"));
|
||||||
let json_file = File::open(json_path).unwrap();
|
let json_file = File::open(json_path).unwrap();
|
||||||
|
|
|
@ -139,6 +139,7 @@ SAttributed {
|
||||||
text: "#aespa",
|
text: "#aespa",
|
||||||
page_type: Unknown,
|
page_type: Unknown,
|
||||||
browse_id: "FEhashtag",
|
browse_id: "FEhashtag",
|
||||||
|
verification: None,
|
||||||
},
|
},
|
||||||
Text {
|
Text {
|
||||||
text: " ",
|
text: " ",
|
||||||
|
@ -152,6 +153,7 @@ SAttributed {
|
||||||
text: "#æspa",
|
text: "#æspa",
|
||||||
page_type: Unknown,
|
page_type: Unknown,
|
||||||
browse_id: "FEhashtag",
|
browse_id: "FEhashtag",
|
||||||
|
verification: None,
|
||||||
},
|
},
|
||||||
Text {
|
Text {
|
||||||
text: " ",
|
text: " ",
|
||||||
|
@ -165,6 +167,7 @@ SAttributed {
|
||||||
text: "#BlackMamba",
|
text: "#BlackMamba",
|
||||||
page_type: Unknown,
|
page_type: Unknown,
|
||||||
browse_id: "FEhashtag",
|
browse_id: "FEhashtag",
|
||||||
|
verification: None,
|
||||||
},
|
},
|
||||||
Text {
|
Text {
|
||||||
text: " ",
|
text: " ",
|
||||||
|
@ -178,6 +181,7 @@ SAttributed {
|
||||||
text: "#블랙맘바",
|
text: "#블랙맘바",
|
||||||
page_type: Unknown,
|
page_type: Unknown,
|
||||||
browse_id: "FEhashtag",
|
browse_id: "FEhashtag",
|
||||||
|
verification: None,
|
||||||
},
|
},
|
||||||
Text {
|
Text {
|
||||||
text: " ",
|
text: " ",
|
||||||
|
@ -191,6 +195,7 @@ SAttributed {
|
||||||
text: "#에스파",
|
text: "#에스파",
|
||||||
page_type: Unknown,
|
page_type: Unknown,
|
||||||
browse_id: "FEhashtag",
|
browse_id: "FEhashtag",
|
||||||
|
verification: None,
|
||||||
},
|
},
|
||||||
Text {
|
Text {
|
||||||
text: "\naespa 에스파 'Black Mamba' MV ℗ SM Entertainment",
|
text: "\naespa 에스파 'Black Mamba' MV ℗ SM Entertainment",
|
||||||
|
|
|
@ -4,10 +4,13 @@ use serde::{Deserialize, Deserializer};
|
||||||
use serde_with::{serde_as, DefaultOnError, DeserializeAs, VecSkipError};
|
use serde_with::{serde_as, DefaultOnError, DeserializeAs, VecSkipError};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
client::response::url_endpoint::{
|
client::response::{
|
||||||
|
url_endpoint::{
|
||||||
MusicPage, MusicPageType, MusicVideoType, NavigationEndpoint, OnTap, PageType,
|
MusicPage, MusicPageType, MusicVideoType, NavigationEndpoint, OnTap, PageType,
|
||||||
},
|
},
|
||||||
model::{richtext::Style, UrlTarget},
|
AttachmentRun,
|
||||||
|
},
|
||||||
|
model::{richtext::Style, UrlTarget, Verification},
|
||||||
util,
|
util,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -101,6 +104,7 @@ pub(crate) enum TextComponent {
|
||||||
text: String,
|
text: String,
|
||||||
page_type: PageType,
|
page_type: PageType,
|
||||||
browse_id: String,
|
browse_id: String,
|
||||||
|
verification: Verification,
|
||||||
},
|
},
|
||||||
Web {
|
Web {
|
||||||
text: String,
|
text: String,
|
||||||
|
@ -151,6 +155,9 @@ pub(crate) struct AttributedText {
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
#[serde_as(as = "VecSkipError<_>")]
|
#[serde_as(as = "VecSkipError<_>")]
|
||||||
style_runs: Vec<StyleRun>,
|
style_runs: Vec<StyleRun>,
|
||||||
|
#[serde(default)]
|
||||||
|
#[serde_as(as = "VecSkipError<_>")]
|
||||||
|
attachment_runs: Vec<AttachmentRun>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[serde_as]
|
#[serde_as]
|
||||||
|
@ -229,6 +236,7 @@ impl From<RichTextRun> for TextComponent {
|
||||||
strikethrough: run.strikethrough,
|
strikethrough: run.strikethrough,
|
||||||
},
|
},
|
||||||
run.navigation_endpoint,
|
run.navigation_endpoint,
|
||||||
|
Verification::None,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -272,6 +280,7 @@ fn map_text_component(
|
||||||
text: String,
|
text: String,
|
||||||
style: Style,
|
style: Style,
|
||||||
nav: Option<NavigationEndpoint>,
|
nav: Option<NavigationEndpoint>,
|
||||||
|
verification: Verification,
|
||||||
) -> TextComponent {
|
) -> TextComponent {
|
||||||
match nav {
|
match nav {
|
||||||
Some(NavigationEndpoint::Watch { watch_endpoint }) => TextComponent::Video {
|
Some(NavigationEndpoint::Watch { watch_endpoint }) => TextComponent::Video {
|
||||||
|
@ -296,6 +305,7 @@ fn map_text_component(
|
||||||
},
|
},
|
||||||
text,
|
text,
|
||||||
browse_id: browse_endpoint.browse_id,
|
browse_id: browse_endpoint.browse_id,
|
||||||
|
verification,
|
||||||
},
|
},
|
||||||
Some(NavigationEndpoint::Url { url_endpoint }) => TextComponent::Web {
|
Some(NavigationEndpoint::Url { url_endpoint }) => TextComponent::Web {
|
||||||
text,
|
text,
|
||||||
|
@ -307,6 +317,7 @@ fn map_text_component(
|
||||||
text,
|
text,
|
||||||
page_type: PageType::Playlist,
|
page_type: PageType::Playlist,
|
||||||
browse_id: watch_playlist_endpoint.playlist_id,
|
browse_id: watch_playlist_endpoint.playlist_id,
|
||||||
|
verification,
|
||||||
},
|
},
|
||||||
None => TextComponent::Text { text, style },
|
None => TextComponent::Text { text, style },
|
||||||
}
|
}
|
||||||
|
@ -385,6 +396,13 @@ impl<'de> DeserializeAs<'de, TextComponents> for AttributedText {
|
||||||
);
|
);
|
||||||
runs.sort_by_key(|run| run.start_index);
|
runs.sort_by_key(|run| run.start_index);
|
||||||
|
|
||||||
|
let verification = text
|
||||||
|
.attachment_runs
|
||||||
|
.into_iter()
|
||||||
|
.next()
|
||||||
|
.map(Verification::from)
|
||||||
|
.unwrap_or_default();
|
||||||
|
|
||||||
let mut components = Vec::with_capacity(runs.len() + 1);
|
let mut components = Vec::with_capacity(runs.len() + 1);
|
||||||
for run in runs {
|
for run in runs {
|
||||||
let txt_before = take_chars(run.start_index);
|
let txt_before = take_chars(run.start_index);
|
||||||
|
@ -415,12 +433,14 @@ impl<'de> DeserializeAs<'de, TextComponents> for AttributedText {
|
||||||
format!("{first_word}: {txt_link}"),
|
format!("{first_word}: {txt_link}"),
|
||||||
Style::default(),
|
Style::default(),
|
||||||
Some(link),
|
Some(link),
|
||||||
|
verification,
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
map_text_component(
|
map_text_component(
|
||||||
txt_link.to_owned(),
|
txt_link.to_owned(),
|
||||||
Style::default(),
|
Style::default(),
|
||||||
Some(link),
|
Some(link),
|
||||||
|
verification,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -428,14 +448,15 @@ impl<'de> DeserializeAs<'de, TextComponents> for AttributedText {
|
||||||
txt_link.to_owned(),
|
txt_link.to_owned(),
|
||||||
Style::default(),
|
Style::default(),
|
||||||
Some(link),
|
Some(link),
|
||||||
|
verification,
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
map_text_component(txt_link, Style::default(), Some(link))
|
map_text_component(txt_link, Style::default(), Some(link), verification)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
AttributedTextRunContent::Style(style) => {
|
AttributedTextRunContent::Style(style) => {
|
||||||
map_text_component(txt_run.to_string(), style, None)
|
map_text_component(txt_run.to_string(), style, None, verification)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -485,6 +506,7 @@ impl TryFrom<TextComponent> for crate::model::ChannelId {
|
||||||
text,
|
text,
|
||||||
page_type: PageType::Channel | PageType::Artist,
|
page_type: PageType::Channel | PageType::Artist,
|
||||||
browse_id,
|
browse_id,
|
||||||
|
..
|
||||||
} => Ok(crate::model::ChannelId {
|
} => Ok(crate::model::ChannelId {
|
||||||
id: browse_id,
|
id: browse_id,
|
||||||
name: text,
|
name: text,
|
||||||
|
@ -494,6 +516,28 @@ impl TryFrom<TextComponent> for crate::model::ChannelId {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl TryFrom<TextComponent> for crate::model::ChannelTag {
|
||||||
|
type Error = ();
|
||||||
|
|
||||||
|
fn try_from(value: TextComponent) -> Result<Self, Self::Error> {
|
||||||
|
match value {
|
||||||
|
TextComponent::Browse {
|
||||||
|
text,
|
||||||
|
page_type: PageType::Channel | PageType::Artist,
|
||||||
|
browse_id,
|
||||||
|
verification,
|
||||||
|
} => Ok(crate::model::ChannelTag {
|
||||||
|
id: browse_id,
|
||||||
|
name: text,
|
||||||
|
avatar: Vec::new(),
|
||||||
|
verification,
|
||||||
|
subscriber_count: None,
|
||||||
|
}),
|
||||||
|
_ => Err(()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl TryFrom<TextComponent> for crate::model::AlbumId {
|
impl TryFrom<TextComponent> for crate::model::AlbumId {
|
||||||
type Error = ();
|
type Error = ();
|
||||||
|
|
||||||
|
@ -503,6 +547,7 @@ impl TryFrom<TextComponent> for crate::model::AlbumId {
|
||||||
text,
|
text,
|
||||||
page_type: PageType::Album,
|
page_type: PageType::Album,
|
||||||
browse_id,
|
browse_id,
|
||||||
|
..
|
||||||
} => Ok(Self {
|
} => Ok(Self {
|
||||||
id: browse_id,
|
id: browse_id,
|
||||||
name: text,
|
name: text,
|
||||||
|
@ -519,6 +564,7 @@ impl From<TextComponent> for crate::model::ArtistId {
|
||||||
text,
|
text,
|
||||||
page_type,
|
page_type,
|
||||||
browse_id,
|
browse_id,
|
||||||
|
..
|
||||||
} => match page_type {
|
} => match page_type {
|
||||||
PageType::Channel | PageType::Artist => Self {
|
PageType::Channel | PageType::Artist => Self {
|
||||||
id: Some(browse_id),
|
id: Some(browse_id),
|
||||||
|
@ -558,6 +604,7 @@ impl From<TextComponent> for crate::model::richtext::TextComponent {
|
||||||
text,
|
text,
|
||||||
page_type,
|
page_type,
|
||||||
browse_id,
|
browse_id,
|
||||||
|
..
|
||||||
} => match page_type.to_url_target(browse_id) {
|
} => match page_type.to_url_target(browse_id) {
|
||||||
Some(target) => Self::YouTube { text, target },
|
Some(target) => Self::YouTube { text, target },
|
||||||
None => Self::Text {
|
None => Self::Text {
|
||||||
|
@ -597,6 +644,15 @@ impl TextComponent {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn into_string(self) -> String {
|
||||||
|
match self {
|
||||||
|
TextComponent::Video { text, .. }
|
||||||
|
| TextComponent::Browse { text, .. }
|
||||||
|
| TextComponent::Web { text, .. }
|
||||||
|
| TextComponent::Text { text, .. } => text,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn music_page(self) -> Option<MusicPage> {
|
pub fn music_page(self) -> Option<MusicPage> {
|
||||||
match self {
|
match self {
|
||||||
TextComponent::Video {
|
TextComponent::Video {
|
||||||
|
@ -844,6 +900,7 @@ mod tests {
|
||||||
text: "DEEP - The 1st Mini Album",
|
text: "DEEP - The 1st Mini Album",
|
||||||
page_type: Album,
|
page_type: Album,
|
||||||
browse_id: "MPREb_TKV2ccxsj5i",
|
browse_id: "MPREb_TKV2ccxsj5i",
|
||||||
|
verification: None,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
"###);
|
"###);
|
||||||
|
@ -878,6 +935,7 @@ mod tests {
|
||||||
text: "laserluca",
|
text: "laserluca",
|
||||||
page_type: Channel,
|
page_type: Channel,
|
||||||
browse_id: "UCmxc6kXbU1J-0pR2F3wIx9A",
|
browse_id: "UCmxc6kXbU1J-0pR2F3wIx9A",
|
||||||
|
verification: None,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
"###);
|
"###);
|
||||||
|
@ -993,6 +1051,7 @@ mod tests {
|
||||||
text: "Roland Kaiser",
|
text: "Roland Kaiser",
|
||||||
page_type: Artist,
|
page_type: Artist,
|
||||||
browse_id: "UCtqi0viP-suK-okUQfaw8Ew",
|
browse_id: "UCtqi0viP-suK-okUQfaw8Ew",
|
||||||
|
verification: None,
|
||||||
},
|
},
|
||||||
Text {
|
Text {
|
||||||
text: " & ",
|
text: " & ",
|
||||||
|
@ -1006,6 +1065,7 @@ mod tests {
|
||||||
text: "Maite Kelly",
|
text: "Maite Kelly",
|
||||||
page_type: Artist,
|
page_type: Artist,
|
||||||
browse_id: "UCY06CayCwdaOd1CnDgjy6uw",
|
browse_id: "UCY06CayCwdaOd1CnDgjy6uw",
|
||||||
|
verification: None,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|
18625
testfiles/video_details/video_details_20241109_mix.json
Normal file
18625
testfiles/video_details/video_details_20241109_mix.json
Normal file
File diff suppressed because it is too large
Load diff
|
@ -999,7 +999,7 @@ async fn channel_search(rp: RustyPipe) {
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
assert_channel_eevblog(&channel);
|
assert_channel_eevblog(&channel);
|
||||||
assert_next(channel.content, rp.query(), 20, 2, true).await;
|
assert_next(channel.content, rp.query(), 18, 2, true).await;
|
||||||
}
|
}
|
||||||
|
|
||||||
fn assert_channel_eevblog<T>(channel: &Channel<T>) {
|
fn assert_channel_eevblog<T>(channel: &Channel<T>) {
|
||||||
|
@ -1526,13 +1526,17 @@ async fn music_playlist(
|
||||||
assert_eq!(playlist.description.map(|d| d.to_plaintext()), description);
|
assert_eq!(playlist.description.map(|d| d.to_plaintext()), description);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
playlist.from_ytm, from_ytm,
|
||||||
|
"got channel: {:?}",
|
||||||
|
playlist.channel
|
||||||
|
);
|
||||||
if let Some(expect) = channel {
|
if let Some(expect) = channel {
|
||||||
let c = playlist.channel.expect("channel");
|
let c = playlist.channel.expect("channel");
|
||||||
assert_eq!(c.id, expect.0);
|
assert_eq!(c.id, expect.0);
|
||||||
assert_eq!(c.name, expect.1);
|
assert_eq!(c.name, expect.1);
|
||||||
}
|
}
|
||||||
assert!(!playlist.thumbnail.is_empty());
|
assert!(!playlist.thumbnail.is_empty());
|
||||||
assert_eq!(playlist.from_ytm, from_ytm);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[rstest]
|
#[rstest]
|
||||||
|
|
Loading…
Reference in a new issue