Compare commits
3 commits
8629454b5b
...
169b70ff06
Author | SHA1 | Date | |
---|---|---|---|
169b70ff06 | |||
e4b10fcc83 | |||
ed522e622d |
6 changed files with 210 additions and 70 deletions
7
.pre-commit-config.yaml
Normal file
7
.pre-commit-config.yaml
Normal file
|
@ -0,0 +1,7 @@
|
|||
repos:
|
||||
- repo: https://github.com/cathiele/pre-commit-rust
|
||||
rev: v0.1.0
|
||||
hooks:
|
||||
- id: cargo-fmt
|
||||
- id: cargo-check
|
||||
- id: cargo-clippy
|
|
@ -13,8 +13,8 @@ use crate::serializer::{
|
|||
};
|
||||
|
||||
use super::{
|
||||
ChannelBadge, ContentsRenderer, ContinuationEndpoint, ContinuationItemRenderer, Icon,
|
||||
Thumbnails, VideoBadge, VideoListItem, VideoOwner,
|
||||
ChannelBadge, ContinuationEndpoint, ContinuationItemRenderer, Icon, Thumbnails, VideoBadge,
|
||||
VideoListItem, VideoOwner,
|
||||
};
|
||||
|
||||
/*
|
||||
|
@ -348,7 +348,16 @@ pub enum EngagementPanelRenderer {
|
|||
#[derive(Clone, Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ChapterMarkersContent {
|
||||
pub macro_markers_list_renderer: ContentsRenderer<MacroMarkersListItem>,
|
||||
pub macro_markers_list_renderer: MacroMarkersListRenderer,
|
||||
}
|
||||
|
||||
/// Chapter markers
|
||||
#[serde_as]
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct MacroMarkersListRenderer {
|
||||
#[serde_as(as = "VecLogError<_>")]
|
||||
pub contents: MapResult<Vec<MacroMarkersListItem>>,
|
||||
}
|
||||
|
||||
/// Chapter marker
|
||||
|
@ -366,9 +375,6 @@ pub struct MacroMarkersListItemRenderer {
|
|||
/// Contains chapter start time in seconds
|
||||
pub on_tap: MacroMarkersListItemOnTap,
|
||||
pub thumbnail: Thumbnails,
|
||||
/// Textual time (`1:42`)
|
||||
#[serde_as(as = "Text")]
|
||||
pub time_description: String,
|
||||
/// Chapter title
|
||||
#[serde_as(as = "Text")]
|
||||
pub title: String,
|
||||
|
|
|
@ -5,7 +5,9 @@ use reqwest::Method;
|
|||
use serde::Serialize;
|
||||
|
||||
use crate::{
|
||||
model::{Channel, ChannelId, Comment, Language, Paginator, RecommendedVideo, VideoDetails},
|
||||
model::{
|
||||
Channel, ChannelId, Chapter, Comment, Language, Paginator, RecommendedVideo, VideoDetails,
|
||||
},
|
||||
serializer::MapResult,
|
||||
timeago,
|
||||
util::{self, TryRemove},
|
||||
|
@ -258,6 +260,26 @@ impl MapResponse<VideoDetails> for response::VideoDetails {
|
|||
response::video_details::EngagementPanelRenderer::None => {},
|
||||
});
|
||||
|
||||
let chapters = chapter_panel
|
||||
.map(|chapters| {
|
||||
let mut content = chapters.macro_markers_list_renderer.contents;
|
||||
warnings.append(&mut content.warnings);
|
||||
content
|
||||
.c
|
||||
.into_iter()
|
||||
.map(|item| Chapter {
|
||||
title: item.macro_markers_list_item_renderer.title,
|
||||
position: item
|
||||
.macro_markers_list_item_renderer
|
||||
.on_tap
|
||||
.watch_endpoint
|
||||
.start_time_seconds,
|
||||
thumbnail: item.macro_markers_list_item_renderer.thumbnail.into(),
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
})
|
||||
.unwrap_or_default();
|
||||
|
||||
let latest_comments_ctoken = comment_panel.and_then(|comments| {
|
||||
let mut items = comments
|
||||
.engagement_panel_title_header_renderer
|
||||
|
@ -288,6 +310,7 @@ impl MapResponse<VideoDetails> for response::VideoDetails {
|
|||
publish_date_txt,
|
||||
is_live,
|
||||
is_ccommons,
|
||||
chapters,
|
||||
recommended,
|
||||
top_comments: Paginator::new(None, Vec::new(), comment_ctoken),
|
||||
latest_comments: Paginator::new(None, Vec::new(), latest_comments_ctoken),
|
||||
|
@ -550,8 +573,9 @@ mod tests {
|
|||
url: "https://smarturl.it/aespa_BlackMamba"
|
||||
- Text: "\n🐍The Debut Stage "
|
||||
- Video:
|
||||
title: "https://youtu.be/Ky5RT5oGg0w"
|
||||
text: "https://youtu.be/Ky5RT5oGg0w"
|
||||
id: Ky5RT5oGg0w
|
||||
start_time: 0
|
||||
- Text: "\n\n🎟️ aespa Showcase SYNK in LA! Tickets now on sale: "
|
||||
- Web:
|
||||
text: "https://www.ticketmaster.com/event/0A..."
|
||||
|
@ -820,8 +844,9 @@ mod tests {
|
|||
url: "https://www.twitch.tv/linustech"
|
||||
- Text: "\n\nMUSIC CREDIT\n---------------------------------------------------\nIntro: Laszlo - Supernova\nVideo Link: "
|
||||
- Video:
|
||||
title: "https://www.youtube.com/watch?v=PKfxm..."
|
||||
text: "https://www.youtube.com/watch?v=PKfxm..."
|
||||
id: PKfxmFU3lWY
|
||||
start_time: 0
|
||||
- Text: "\niTunes Download Link: "
|
||||
- Web:
|
||||
text: "https://itunes.apple.com/us/album/sup..."
|
||||
|
@ -832,8 +857,9 @@ mod tests {
|
|||
url: "https://soundcloud.com/laszlomusic"
|
||||
- Text: "\n\nOutro: Approaching Nirvana - Sugar High\nVideo Link: "
|
||||
- Video:
|
||||
title: "https://www.youtube.com/watch?v=ngsGB..."
|
||||
text: "https://www.youtube.com/watch?v=ngsGB..."
|
||||
id: ngsGBSCDwcI
|
||||
start_time: 0
|
||||
- Text: "\nListen on Spotify: "
|
||||
- Web:
|
||||
text: "http://spoti.fi/UxWkUw"
|
||||
|
@ -860,60 +886,74 @@ mod tests {
|
|||
url: "https://geni.us/Ps3XfE"
|
||||
- Text: "\n\nCHAPTERS\n---------------------------------------------------\n"
|
||||
- Video:
|
||||
title: "0:00"
|
||||
text: "0:00"
|
||||
id: nFDBxBUfE74
|
||||
start_time: 0
|
||||
- Text: " Intro\n"
|
||||
- Video:
|
||||
title: "0:42"
|
||||
text: "0:42"
|
||||
id: nFDBxBUfE74
|
||||
start_time: 42
|
||||
- Text: " The PC Built for Super Efficiency\n"
|
||||
- Video:
|
||||
title: "2:41"
|
||||
text: "2:41"
|
||||
id: nFDBxBUfE74
|
||||
start_time: 161
|
||||
- Text: " Our BURIAL ENCLOSURE?!\n"
|
||||
- Video:
|
||||
title: "3:31"
|
||||
text: "3:31"
|
||||
id: nFDBxBUfE74
|
||||
start_time: 211
|
||||
- Text: " Our Power Solution (Thanks Jackery!)\n"
|
||||
- Video:
|
||||
title: "4:47"
|
||||
text: "4:47"
|
||||
id: nFDBxBUfE74
|
||||
start_time: 287
|
||||
- Text: " Diggin' Holes\n"
|
||||
- Video:
|
||||
title: "5:30"
|
||||
text: "5:30"
|
||||
id: nFDBxBUfE74
|
||||
start_time: 330
|
||||
- Text: " Colonoscopy?\n"
|
||||
- Video:
|
||||
title: "7:04"
|
||||
text: "7:04"
|
||||
id: nFDBxBUfE74
|
||||
start_time: 424
|
||||
- Text: " Diggin' like a man\n"
|
||||
- Video:
|
||||
title: "8:29"
|
||||
text: "8:29"
|
||||
id: nFDBxBUfE74
|
||||
start_time: 509
|
||||
- Text: " The world's worst woodsman\n"
|
||||
- Video:
|
||||
title: "9:03"
|
||||
text: "9:03"
|
||||
id: nFDBxBUfE74
|
||||
start_time: 543
|
||||
- Text: " Backyard cable management\n"
|
||||
- Video:
|
||||
title: "10:02"
|
||||
text: "10:02"
|
||||
id: nFDBxBUfE74
|
||||
start_time: 602
|
||||
- Text: " Time to bury this boy\n"
|
||||
- Video:
|
||||
title: "10:46"
|
||||
text: "10:46"
|
||||
id: nFDBxBUfE74
|
||||
start_time: 646
|
||||
- Text: " Solar Power Generation\n"
|
||||
- Video:
|
||||
title: "11:37"
|
||||
text: "11:37"
|
||||
id: nFDBxBUfE74
|
||||
start_time: 697
|
||||
- Text: " Issues\n"
|
||||
- Video:
|
||||
title: "12:08"
|
||||
text: "12:08"
|
||||
id: nFDBxBUfE74
|
||||
start_time: 728
|
||||
- Text: " First Play Test\n"
|
||||
- Video:
|
||||
title: "13:20"
|
||||
text: "13:20"
|
||||
id: nFDBxBUfE74
|
||||
start_time: 800
|
||||
- Text: " Conclusion"
|
||||
"###);
|
||||
|
||||
|
@ -942,6 +982,57 @@ mod tests {
|
|||
assert!(!details.is_live);
|
||||
assert!(!details.is_ccommons);
|
||||
|
||||
insta::assert_yaml_snapshot!(details.chapters, {
|
||||
"[].thumbnail" => insta::dynamic_redaction(move |value, _path| {
|
||||
assert!(!value.as_slice().unwrap().is_empty());
|
||||
"[ok]"
|
||||
}),
|
||||
}, @r###"
|
||||
---
|
||||
- title: Intro
|
||||
position: 0
|
||||
thumbnail: "[ok]"
|
||||
- title: The PC Built for Super Efficiency
|
||||
position: 42
|
||||
thumbnail: "[ok]"
|
||||
- title: Our BURIAL ENCLOSURE?!
|
||||
position: 161
|
||||
thumbnail: "[ok]"
|
||||
- title: Our Power Solution (Thanks Jackery!)
|
||||
position: 211
|
||||
thumbnail: "[ok]"
|
||||
- title: "Diggin' Holes"
|
||||
position: 287
|
||||
thumbnail: "[ok]"
|
||||
- title: Colonoscopy?
|
||||
position: 330
|
||||
thumbnail: "[ok]"
|
||||
- title: "Diggin' like a man"
|
||||
position: 424
|
||||
thumbnail: "[ok]"
|
||||
- title: "The world's worst woodsman"
|
||||
position: 509
|
||||
thumbnail: "[ok]"
|
||||
- title: Backyard cable management
|
||||
position: 543
|
||||
thumbnail: "[ok]"
|
||||
- title: Time to bury this boy
|
||||
position: 602
|
||||
thumbnail: "[ok]"
|
||||
- title: Solar Power Generation
|
||||
position: 646
|
||||
thumbnail: "[ok]"
|
||||
- title: Issues
|
||||
position: 697
|
||||
thumbnail: "[ok]"
|
||||
- title: First Play Test
|
||||
position: 728
|
||||
thumbnail: "[ok]"
|
||||
- title: Conclusion
|
||||
position: 800
|
||||
thumbnail: "[ok]"
|
||||
"###);
|
||||
|
||||
assert!(!details.recommended.items.is_empty());
|
||||
assert!(!details.recommended.is_exhausted());
|
||||
|
||||
|
@ -971,8 +1062,9 @@ mod tests {
|
|||
---
|
||||
- Text: "Live NASA - Views Of Earth from Space\nLive video feed of Earth from the International Space Station (ISS) Cameras\n-----------------------------------------------------------------------------------------------------\nWatch our latest video - The Sun - 4K Video / Solar Flares\n"
|
||||
- Video:
|
||||
title: "https://www.youtube.com/watch?v=SEzK4..."
|
||||
text: "https://www.youtube.com/watch?v=SEzK4..."
|
||||
id: SEzK4ZfMvUQ
|
||||
start_time: 0
|
||||
- Text: "\n-----------------------------------------------------------------------------------------------------\nNasa ISS live stream from aboard the International Space Station as it circles the earth at 240 miles above the planet, on the edge of space in low earth orbit. \n\nThe station is crewed by NASA astronauts as well as Russian Cosmonauts and a mixture of Japanese, Canadian and European astronauts as well.\n\n"
|
||||
- Text: " "
|
||||
- Text: " "
|
||||
|
@ -1032,7 +1124,7 @@ mod tests {
|
|||
let rp = RustyPipe::builder().strict().build();
|
||||
let details = rp.query().video_details("HRKu0cvrr_o").await.unwrap();
|
||||
|
||||
dbg!(&details);
|
||||
// dbg!(&details);
|
||||
|
||||
assert_eq!(details.id, "HRKu0cvrr_o");
|
||||
assert_eq!(
|
||||
|
|
|
@ -245,6 +245,8 @@ pub struct VideoDetails {
|
|||
///
|
||||
/// https://creativecommons.org/licenses/by/3.0/
|
||||
pub is_ccommons: bool,
|
||||
/// Chapters of the video
|
||||
pub chapters: Vec<Chapter>,
|
||||
/// Recommended videos
|
||||
///
|
||||
/// Note: Recommendations are not available for age-restricted videos
|
||||
|
@ -255,6 +257,18 @@ pub struct VideoDetails {
|
|||
pub latest_comments: Paginator<Comment>,
|
||||
}
|
||||
|
||||
/// Videos can consist of different chapters, which YouTube shows
|
||||
/// on the seek bar and below the description text.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
pub struct Chapter {
|
||||
/// Chapter title
|
||||
pub title: String,
|
||||
/// Chapter position in seconds
|
||||
pub position: u32,
|
||||
/// Chapter thumbnail
|
||||
pub thumbnail: Vec<Thumbnail>,
|
||||
}
|
||||
|
||||
/*
|
||||
@RECOMMENDATIONS
|
||||
*/
|
||||
|
|
|
@ -10,15 +10,19 @@ pub enum TextComponent {
|
|||
/// Web link
|
||||
Web { text: String, url: String },
|
||||
/// Link to a YouTube video
|
||||
Video { title: String, id: String },
|
||||
Video {
|
||||
text: String,
|
||||
id: String,
|
||||
start_time: u32,
|
||||
},
|
||||
/// Link to a YouTube channel
|
||||
Channel { name: String, id: String },
|
||||
Channel { text: String, id: String },
|
||||
/// Link to a YouTube playlist
|
||||
Playlist { name: String, id: String },
|
||||
Playlist { text: String, id: String },
|
||||
/// Link to a YouTube Music artist
|
||||
Artist { name: String, id: String },
|
||||
Artist { text: String, id: String },
|
||||
/// Link to a YouTube Music album
|
||||
Album { name: String, id: String },
|
||||
Album { text: String, id: String },
|
||||
}
|
||||
|
||||
/// Trait for converting rich text to plain text.
|
||||
|
@ -46,12 +50,27 @@ pub trait ToHtml {
|
|||
fn to_html_yt_host(&self, yt_host: &str) -> String;
|
||||
}
|
||||
|
||||
impl ToPlaintext for TextComponent {
|
||||
fn to_plaintext_yt_host(&self, yt_host: &str) -> String {
|
||||
impl TextComponent {
|
||||
pub fn get_text<'a>(&'a self) -> &'a str {
|
||||
match self {
|
||||
TextComponent::Text(text) => text.to_owned(),
|
||||
TextComponent::Text(text) => text,
|
||||
TextComponent::Web { text, .. } => text,
|
||||
TextComponent::Video { text, .. } => text,
|
||||
TextComponent::Channel { text, .. } => text,
|
||||
TextComponent::Playlist { text, .. } => text,
|
||||
TextComponent::Artist { text, .. } => text,
|
||||
TextComponent::Album { text, .. } => text,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_url(&self, yt_host: &str) -> String {
|
||||
match self {
|
||||
TextComponent::Text(_) => "".to_owned(),
|
||||
TextComponent::Web { url, .. } => url.to_owned(),
|
||||
TextComponent::Video { id, .. } => format!("{}/watch?v={}", yt_host, id),
|
||||
TextComponent::Video { id, start_time, .. } => match start_time {
|
||||
0 => format!("{}/watch?v={}", yt_host, id),
|
||||
n => format!("{}/watch?v={}&t={}s", yt_host, id, n),
|
||||
},
|
||||
TextComponent::Channel { id, .. } | TextComponent::Artist { id, .. } => {
|
||||
format!("{}/channel/{}", yt_host, id)
|
||||
}
|
||||
|
@ -62,6 +81,15 @@ impl ToPlaintext for TextComponent {
|
|||
}
|
||||
}
|
||||
|
||||
impl ToPlaintext for TextComponent {
|
||||
fn to_plaintext_yt_host(&self, yt_host: &str) -> String {
|
||||
match self {
|
||||
TextComponent::Text(text) => text.to_owned(),
|
||||
_ => self.get_url(yt_host),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "html")]
|
||||
impl ToHtml for TextComponent {
|
||||
fn to_html_yt_host(&self, yt_host: &str) -> String {
|
||||
|
@ -69,35 +97,18 @@ impl ToHtml for TextComponent {
|
|||
TextComponent::Text(text) => askama_escape::escape(&text, askama_escape::Html)
|
||||
.to_string()
|
||||
.replace("\n", "<br>"),
|
||||
TextComponent::Web { text, url } => {
|
||||
TextComponent::Web { text, .. } => {
|
||||
format!(
|
||||
r#"<a href="{}" target="_blank" rel="noreferrer">{}</a>"#,
|
||||
url,
|
||||
askama_escape::escape(&text, askama_escape::Html)
|
||||
self.get_url(yt_host),
|
||||
askama_escape::escape(text, askama_escape::Html)
|
||||
)
|
||||
}
|
||||
TextComponent::Video { title, id } => {
|
||||
_ => {
|
||||
format!(
|
||||
r#"<a href="{}/watch?v={}" rel="noreferrer">{}</a>"#,
|
||||
yt_host,
|
||||
id,
|
||||
askama_escape::escape(&title, askama_escape::Html)
|
||||
)
|
||||
}
|
||||
TextComponent::Channel { name, id } | TextComponent::Artist { name, id } => {
|
||||
format!(
|
||||
r#"<a href="{}/channel/{}" rel="noreferrer">{}</a>"#,
|
||||
yt_host,
|
||||
id,
|
||||
askama_escape::escape(&name, askama_escape::Html)
|
||||
)
|
||||
}
|
||||
TextComponent::Playlist { name, id } | TextComponent::Album { name, id } => {
|
||||
format!(
|
||||
r#"<a href="{}/playlist?list={}" rel="noreferrer">{}</a>"#,
|
||||
yt_host,
|
||||
id,
|
||||
askama_escape::escape(&name, askama_escape::Html)
|
||||
r#"<a href="{}">{}</a>"#,
|
||||
self.get_url(yt_host),
|
||||
askama_escape::escape(self.get_text(), askama_escape::Html)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -133,7 +144,7 @@ mod tests {
|
|||
text::TextComponent::Text { text: "🎧Listen and download aespa's debut single \"Black Mamba\": ".to_owned() },
|
||||
text::TextComponent::Web { text: "https://smarturl.it/aespa_BlackMamba".to_owned(), url: "https://www.youtube.com/redirect?event=video_description&redir_token=QUFFLUhqbFY1QmpQamJPSms0Z1FnVTlQUS00ZFhBZnBJZ3xBQ3Jtc0tuRGJBanludGoyRnphb2dZWVd3cUNnS3dEd0FnNHFOZEY1NHBJaHFmLXpaWUJwX3ZucDZxVnpGeHNGX1FpMzFkZW9jQkI2Mi1wNGJ1UVFNN3h1MnN3R3JLMzdxU01nZ01POHBGcmxHU2puSUk1WHRzQQ&q=https%3A%2F%2Fsmarturl.it%2Faespa_BlackMamba&v=ZeerrnuLi5E".to_owned() },
|
||||
text::TextComponent::Text { text: "\n🐍The Debut Stage ".to_owned() },
|
||||
text::TextComponent::Video { title: "https://youtu.be/Ky5RT5oGg0w".to_owned(), video_id: "Ky5RT5oGg0w".to_owned() },
|
||||
text::TextComponent::Video { text: "https://youtu.be/Ky5RT5oGg0w".to_owned(), video_id: "Ky5RT5oGg0w".to_owned(), start_time: 0 },
|
||||
text::TextComponent::Text { text: "\n\n🎟️ aespa Showcase SYNK in LA! Tickets now on sale: ".to_owned() },
|
||||
text::TextComponent::Web { text: "https://www.ticketmaster.com/event/0A...".to_owned(), url: "https://www.youtube.com/redirect?event=video_description&redir_token=QUFFLUhqbFpUMEZiaXJWWkszaVZXaEM0emxWU1JQV3NoQXxBQ3Jtc0tuU2g4VWNPNE5UY3hoSWYtamFzX0h4bUVQLVJiRy1ubDZrTnh3MUpGdDNSaUo0ZlMyT3lUM28ycUVBdHJLMndGcDhla3BkOFpxSVFfOS1QdVJPVHBUTEV1LXpOV0J2QXdhV05lV210cEJtZUJMeHdaTQ&q=https%3A%2F%2Fwww.ticketmaster.com%2Fevent%2F0A005CCD9E871F6E&v=ZeerrnuLi5E".to_owned() },
|
||||
text::TextComponent::Text { text: "\n\nSubscribe to aespa Official YouTube Channel!\n".to_owned() },
|
||||
|
@ -198,7 +209,7 @@ aespa 에스파 'Black Mamba' MV ℗ SM Entertainment"#
|
|||
let html = richtext.to_html_yt_host("https://piped.kavin.rocks");
|
||||
assert_eq!(
|
||||
html,
|
||||
"🎧Listen and download aespa's debut single "Black Mamba": <a href=\"https://smarturl.it/aespa_BlackMamba\" target=\"_blank\" rel=\"noreferrer\">https://smarturl.it/aespa_BlackMamba</a><br>🐍The Debut Stage <a href=\"https://piped.kavin.rocks/watch?v=Ky5RT5oGg0w\" rel=\"noreferrer\">https://youtu.be/Ky5RT5oGg0w</a><br><br>🎟\u{fe0f} aespa Showcase SYNK in LA! Tickets now on sale: <a href=\"https://www.ticketmaster.com/event/0A005CCD9E871F6E\" target=\"_blank\" rel=\"noreferrer\">https://www.ticketmaster.com/event/0A...</a><br><br>Subscribe to aespa Official YouTube Channel!<br><a href=\"https://www.youtube.com/aespa?sub_confirmation=1\" target=\"_blank\" rel=\"noreferrer\">https://www.youtube.com/aespa?sub_con...</a><br><br>aespa official<br><a href=\"https://www.youtube.com/c/aespa\" target=\"_blank\" rel=\"noreferrer\">https://www.youtube.com/c/aespa</a><br><a href=\"https://www.instagram.com/aespa_official\" target=\"_blank\" rel=\"noreferrer\">https://www.instagram.com/aespa_official</a><br><a href=\"https://www.tiktok.com/@aespa_official\" target=\"_blank\" rel=\"noreferrer\">https://www.tiktok.com/@aespa_official</a><br><a href=\"https://twitter.com/aespa_Official\" target=\"_blank\" rel=\"noreferrer\">https://twitter.com/aespa_Official</a><br><a href=\"https://www.facebook.com/aespa.official\" target=\"_blank\" rel=\"noreferrer\">https://www.facebook.com/aespa.official</a><br><a href=\"https://weibo.com/aespa\" target=\"_blank\" rel=\"noreferrer\">https://weibo.com/aespa</a><br><br>#aespa #æspa #BlackMamba #블랙맘바 #에스파<br>aespa 에스파 'Black Mamba' MV ℗ SM Entertainment"
|
||||
"🎧Listen and download aespa's debut single "Black Mamba": <a href=\"https://smarturl.it/aespa_BlackMamba\" target=\"_blank\" rel=\"noreferrer\">https://smarturl.it/aespa_BlackMamba</a><br>🐍The Debut Stage <a href=\"https://piped.kavin.rocks/watch?v=Ky5RT5oGg0w\">https://youtu.be/Ky5RT5oGg0w</a><br><br>🎟\u{fe0f} aespa Showcase SYNK in LA! Tickets now on sale: <a href=\"https://www.ticketmaster.com/event/0A005CCD9E871F6E\" target=\"_blank\" rel=\"noreferrer\">https://www.ticketmaster.com/event/0A...</a><br><br>Subscribe to aespa Official YouTube Channel!<br><a href=\"https://www.youtube.com/aespa?sub_confirmation=1\" target=\"_blank\" rel=\"noreferrer\">https://www.youtube.com/aespa?sub_con...</a><br><br>aespa official<br><a href=\"https://www.youtube.com/c/aespa\" target=\"_blank\" rel=\"noreferrer\">https://www.youtube.com/c/aespa</a><br><a href=\"https://www.instagram.com/aespa_official\" target=\"_blank\" rel=\"noreferrer\">https://www.instagram.com/aespa_official</a><br><a href=\"https://www.tiktok.com/@aespa_official\" target=\"_blank\" rel=\"noreferrer\">https://www.tiktok.com/@aespa_official</a><br><a href=\"https://twitter.com/aespa_Official\" target=\"_blank\" rel=\"noreferrer\">https://twitter.com/aespa_Official</a><br><a href=\"https://www.facebook.com/aespa.official\" target=\"_blank\" rel=\"noreferrer\">https://www.facebook.com/aespa.official</a><br><a href=\"https://weibo.com/aespa\" target=\"_blank\" rel=\"noreferrer\">https://weibo.com/aespa</a><br><br>#aespa #æspa #BlackMamba #블랙맘바 #에스파<br>aespa 에스파 'Black Mamba' MV ℗ SM Entertainment"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -86,8 +86,9 @@ pub struct TextComponents(pub Vec<TextComponent>);
|
|||
#[derive(Debug, Clone)]
|
||||
pub enum TextComponent {
|
||||
Video {
|
||||
title: String,
|
||||
text: String,
|
||||
video_id: String,
|
||||
start_time: u32,
|
||||
},
|
||||
Browse {
|
||||
text: String,
|
||||
|
@ -141,6 +142,8 @@ struct NavigationEndpoint {
|
|||
#[serde(rename_all = "camelCase")]
|
||||
struct WatchEndpoint {
|
||||
video_id: String,
|
||||
#[serde(default)]
|
||||
start_time_seconds: u32,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
|
@ -202,8 +205,9 @@ fn map_richtext_run(lr: &RichTextRun) -> Option<TextComponent> {
|
|||
|
||||
Some(match &nav.watch_endpoint {
|
||||
Some(w) => TextComponent::Video {
|
||||
title: text,
|
||||
text,
|
||||
video_id: w.video_id.to_owned(),
|
||||
start_time: w.start_time_seconds,
|
||||
},
|
||||
None => match &nav.browse_endpoint {
|
||||
Some(b) => TextComponent::Browse {
|
||||
|
@ -284,9 +288,14 @@ impl TryFrom<TextComponent> for crate::model::ChannelId {
|
|||
impl From<TextComponent> for crate::model::richtext::TextComponent {
|
||||
fn from(component: TextComponent) -> Self {
|
||||
match component {
|
||||
TextComponent::Video { title, video_id } => Self::Video {
|
||||
title,
|
||||
TextComponent::Video {
|
||||
text,
|
||||
video_id,
|
||||
start_time,
|
||||
} => Self::Video {
|
||||
text,
|
||||
id: video_id,
|
||||
start_time,
|
||||
},
|
||||
TextComponent::Browse {
|
||||
text,
|
||||
|
@ -294,19 +303,19 @@ impl From<TextComponent> for crate::model::richtext::TextComponent {
|
|||
browse_id,
|
||||
} => match page_type {
|
||||
PageType::Artist => Self::Artist {
|
||||
name: text,
|
||||
text,
|
||||
id: browse_id,
|
||||
},
|
||||
PageType::Album => Self::Album {
|
||||
name: text,
|
||||
text,
|
||||
id: browse_id,
|
||||
},
|
||||
PageType::Channel => Self::Channel {
|
||||
name: text,
|
||||
text,
|
||||
id: browse_id,
|
||||
},
|
||||
PageType::Playlist => Self::Playlist {
|
||||
name: text,
|
||||
text,
|
||||
id: browse_id,
|
||||
},
|
||||
},
|
||||
|
@ -443,8 +452,9 @@ mod tests {
|
|||
insta::assert_debug_snapshot!(res, @r###"
|
||||
SLink {
|
||||
ln: Video {
|
||||
title: "DEEP",
|
||||
text: "DEEP",
|
||||
video_id: "wZIoIgz5mbs",
|
||||
start_time: 0,
|
||||
},
|
||||
}
|
||||
"###);
|
||||
|
|
Loading…
Add table
Reference in a new issue