Compare commits

...

2 commits

Author SHA1 Message Date
bf80db8a9a fix!: parse full video info from playlist items, remove PlaylistVideo model
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2023-05-13 23:10:05 +02:00
54f42bcb54 fix: add more markdown escape chars, change RichText enum 2023-05-13 15:59:41 +02:00
33 changed files with 106413 additions and 42432 deletions

View file

@ -181,6 +181,7 @@ async fn playlist() {
("short", "RDCLAK5uy_kFQXdnqMaQCVx2wpUM4ZfbsGCDibZtkJk"),
("long", "PL5dDx681T4bR7ZF1IuWzOv1omlRbE7PiJ"),
("nomusic", "PL1J-6JOckZtE_P9Xx8D3b2O6w0idhuKBe"),
("live", "UULVvqRdlKsE5Q8mf8YXbdIJLw"),
] {
let json_path = path!(*TESTFILES_DIR / "playlist" / format!("playlist_{name}.json"));
if json_path.exists() {

View file

@ -2,7 +2,7 @@ use crate::error::{Error, ExtractionError};
use crate::model::{
paginator::{ContinuationEndpoint, Paginator},
traits::FromYtItem,
Comment, MusicItem, PlaylistVideo, YouTubeItem,
Comment, MusicItem, YouTubeItem,
};
use crate::serializer::MapResult;
@ -265,16 +265,6 @@ impl Paginator<Comment> {
}
}
impl Paginator<PlaylistVideo> {
/// Get the next page from the paginator (or `None` if the paginator is exhausted)
pub async fn next<Q: AsRef<RustyPipeQuery>>(&self, query: Q) -> Result<Option<Self>, Error> {
Ok(match &self.ctoken {
Some(ctoken) => Some(query.as_ref().playlist_continuation(ctoken).await?),
None => None,
})
}
}
macro_rules! paginator {
($entity_type:ty) => {
impl Paginator<$entity_type> {
@ -337,7 +327,6 @@ macro_rules! paginator {
}
paginator!(Comment);
paginator!(PlaylistVideo);
#[cfg(test)]
mod tests {
@ -348,15 +337,15 @@ mod tests {
use super::*;
use crate::{
model::{MusicPlaylistItem, PlaylistItem, TrackItem},
model::{MusicPlaylistItem, PlaylistItem, TrackItem, VideoItem},
param::Language,
util::tests::TESTFILES,
};
#[rstest]
#[case("search", path!("search" / "cont.json"))]
#[case("startpage", path!("trends" / "startpage_cont.json"))]
#[case("recommendations", path!("video_details" / "recommendations.json"))]
#[case::search("search", path!("search" / "cont.json"))]
#[case::startpage("startpage", path!("trends" / "startpage_cont.json"))]
#[case::recommendations("recommendations", path!("video_details" / "recommendations.json"))]
fn map_continuation_items(#[case] name: &str, #[case] path: PathBuf) {
let json_path = path!(*TESTFILES / path);
let json_file = File::open(json_path).unwrap();
@ -377,7 +366,31 @@ mod tests {
}
#[rstest]
#[case("channel_playlists", path!("channel" / "channel_playlists_cont.json"))]
#[case::channel_videos("channel_videos", path!("channel" / "channel_videos_cont.json"))]
#[case::playlist("playlist", path!("playlist" / "playlist_cont.json"))]
fn map_continuation_videos(#[case] name: &str, #[case] path: PathBuf) {
let json_path = path!(*TESTFILES / path);
let json_file = File::open(json_path).unwrap();
let items: response::Continuation =
serde_json::from_reader(BufReader::new(json_file)).unwrap();
let map_res: MapResult<Paginator<YouTubeItem>> =
items.map_response("", Language::En, None).unwrap();
let paginator: Paginator<VideoItem> =
map_yt_paginator(map_res.c, None, ContinuationEndpoint::Browse);
assert!(
map_res.warnings.is_empty(),
"deserialization/mapping warnings: {:?}",
map_res.warnings
);
insta::assert_ron_snapshot!(format!("map_{name}"), paginator, {
".items[].publish_date" => "[date]",
});
}
#[rstest]
#[case::channel_playlists("channel_playlists", path!("channel" / "channel_playlists_cont.json"))]
fn map_continuation_playlists(#[case] name: &str, #[case] path: PathBuf) {
let json_path = path!(*TESTFILES / path);
let json_file = File::open(json_path).unwrap();
@ -398,9 +411,9 @@ mod tests {
}
#[rstest]
#[case("playlist_tracks", path!("music_playlist" / "playlist_cont.json"))]
#[case("search_tracks", path!("music_search" / "tracks_cont.json"))]
#[case("radio_tracks", path!("music_details" / "radio_cont.json"))]
#[case::playlist_tracks("playlist_tracks", path!("music_playlist" / "playlist_cont.json"))]
#[case::search_tracks("search_tracks", path!("music_search" / "tracks_cont.json"))]
#[case::radio_tracks("radio_tracks", path!("music_details" / "radio_cont.json"))]
fn map_continuation_tracks(#[case] name: &str, #[case] path: PathBuf) {
let json_path = path!(*TESTFILES / path);
let json_file = File::open(json_path).unwrap();
@ -421,7 +434,7 @@ mod tests {
}
#[rstest]
#[case("playlist_related", path!("music_playlist" / "playlist_related.json"))]
#[case::playlist_related("playlist_related", path!("music_playlist" / "playlist_related.json"))]
fn map_continuation_music_playlists(#[case] name: &str, #[case] path: PathBuf) {
let json_path = path!(*TESTFILES / path);
let json_file = File::open(json_path).unwrap();

View file

@ -4,11 +4,11 @@ use time::OffsetDateTime;
use crate::{
error::{Error, ExtractionError},
model::{paginator::Paginator, ChannelId, Playlist, PlaylistVideo},
model::{paginator::Paginator, ChannelId, Playlist, VideoItem},
util::{self, timeago, TryRemove},
};
use super::{response, ClientType, MapResponse, MapResult, QBrowse, QContinuation, RustyPipeQuery};
use super::{response, ClientType, MapResponse, MapResult, QBrowse, RustyPipeQuery};
impl RustyPipeQuery {
/// Get a YouTube playlist
@ -29,28 +29,6 @@ impl RustyPipeQuery {
)
.await
}
/// Get more playlist items using the given continuation token
pub async fn playlist_continuation<S: AsRef<str>>(
&self,
ctoken: S,
) -> Result<Paginator<PlaylistVideo>, Error> {
let ctoken = ctoken.as_ref();
let context = self.get_context(ClientType::Desktop, true, None).await;
let request_body = QContinuation {
context,
continuation: ctoken,
};
self.execute_request::<response::PlaylistCont, _, _>(
ClientType::Desktop,
"playlist_continuation",
ctoken,
"browse",
&request_body,
)
.await
}
}
impl MapResponse<Playlist> for response::Playlist {
@ -91,7 +69,8 @@ impl MapResponse<Playlist> for response::Playlist {
.playlist_video_list_renderer
.contents;
let (videos, ctoken) = map_playlist_items(video_items.c);
let mut mapper = response::YouTubeListMapper::<VideoItem>::new(lang);
mapper.map_response(video_items);
let (thumbnails, last_update_txt) = match self.sidebar {
Some(sidebar) => {
@ -136,10 +115,11 @@ impl MapResponse<Playlist> for response::Playlist {
}
};
let n_videos = match ctoken {
Some(_) => util::parse_numeric(&header.playlist_header_renderer.num_videos_text)
.map_err(|_| ExtractionError::InvalidData(Cow::Borrowed("no video count")))?,
None => videos.len() as u64,
let n_videos = if mapper.ctoken.is_some() {
util::parse_numeric(&header.playlist_header_renderer.num_videos_text)
.map_err(|_| ExtractionError::InvalidData(Cow::Borrowed("no video count")))?
} else {
mapper.items.len() as u64
};
let playlist_id = header.playlist_header_renderer.playlist_id;
@ -156,16 +136,16 @@ impl MapResponse<Playlist> for response::Playlist {
.owner_text
.and_then(|link| ChannelId::try_from(link).ok());
let mut warnings = video_items.warnings;
let last_update = last_update_txt.as_ref().and_then(|txt| {
timeago::parse_textual_date_or_warn(lang, txt, &mut warnings).map(OffsetDateTime::date)
timeago::parse_textual_date_or_warn(lang, txt, &mut mapper.warnings)
.map(OffsetDateTime::date)
});
Ok(MapResult {
c: Playlist {
id: playlist_id,
name,
videos: Paginator::new(Some(n_videos), videos, ctoken),
videos: Paginator::new(Some(n_videos), mapper.items, mapper.ctoken),
video_count: n_videos,
thumbnail: thumbnails.into(),
description,
@ -174,63 +154,11 @@ impl MapResponse<Playlist> for response::Playlist {
last_update_txt,
visitor_data: self.response_context.visitor_data,
},
warnings,
warnings: mapper.warnings,
})
}
}
impl MapResponse<Paginator<PlaylistVideo>> for response::PlaylistCont {
fn map_response(
self,
_id: &str,
_lang: crate::param::Language,
_deobf: Option<&crate::deobfuscate::DeobfData>,
) -> Result<MapResult<Paginator<PlaylistVideo>>, ExtractionError> {
let action = self.on_response_received_actions.into_iter().next();
let ((items, ctoken), warnings) = action
.map(|action| {
(
map_playlist_items(
action.append_continuation_items_action.continuation_items.c,
),
action
.append_continuation_items_action
.continuation_items
.warnings,
)
})
.unwrap_or_default();
Ok(MapResult {
c: Paginator::new(None, items, ctoken),
warnings,
})
}
}
fn map_playlist_items(
items: Vec<response::playlist::PlaylistItem>,
) -> (Vec<PlaylistVideo>, Option<String>) {
let mut ctoken: Option<String> = None;
let videos = items
.into_iter()
.filter_map(|it| match it {
response::playlist::PlaylistItem::PlaylistVideoRenderer(video) => {
PlaylistVideo::try_from(video).ok()
}
response::playlist::PlaylistItem::ContinuationItemRenderer {
continuation_endpoint,
} => {
ctoken = Some(continuation_endpoint.continuation_command.token);
None
}
response::playlist::PlaylistItem::None => None,
})
.collect::<Vec<_>>();
(videos, ctoken)
}
#[cfg(test)]
mod tests {
use std::{fs::File, io::BufReader};
@ -246,6 +174,7 @@ mod tests {
#[case::short("short", "RDCLAK5uy_kFQXdnqMaQCVx2wpUM4ZfbsGCDibZtkJk")]
#[case::long("long", "PL5dDx681T4bR7ZF1IuWzOv1omlRbE7PiJ")]
#[case::nomusic("nomusic", "PL1J-6JOckZtE_P9Xx8D3b2O6w0idhuKBe")]
#[case::live("live", "UULVvqRdlKsE5Q8mf8YXbdIJLw")]
fn map_playlist_data(#[case] name: &str, #[case] id: &str) {
let json_path = path!(*TESTFILES / "playlist" / format!("playlist_{name}.json"));
let json_file = File::open(json_path).unwrap();
@ -260,24 +189,8 @@ mod tests {
map_res.warnings
);
insta::assert_ron_snapshot!(format!("map_playlist_data_{name}"), map_res.c, {
".last_update" => "[date]"
".last_update" => "[date]",
".videos.items[].publish_date" => "[date]",
});
}
#[test]
fn map_playlist_cont() {
let json_path = path!(*TESTFILES / "playlist" / "playlist_cont.json");
let json_file = File::open(json_path).unwrap();
let playlist: response::PlaylistCont =
serde_json::from_reader(BufReader::new(json_file)).unwrap();
let map_res = playlist.map_response("", Language::En, None).unwrap();
assert!(
map_res.warnings.is_empty(),
"deserialization/mapping warnings: {:?}",
map_res.warnings
);
insta::assert_ron_snapshot!("map_playlist_cont", map_res.c);
}
}

View file

@ -31,7 +31,6 @@ pub(crate) use music_search::MusicSearch;
pub(crate) use music_search::MusicSearchSuggestion;
pub(crate) use player::Player;
pub(crate) use playlist::Playlist;
pub(crate) use playlist::PlaylistCont;
pub(crate) use search::Search;
pub(crate) use search::SearchSuggestion;
pub(crate) use trends::Startpage;

View file

@ -1,16 +1,10 @@
use serde::Deserialize;
use serde_with::{
json::JsonString, rust::deserialize_ignore_any, serde_as, DefaultOnError, VecSkipError,
};
use serde_with::{serde_as, DefaultOnError};
use crate::serializer::{
text::{Text, TextComponent},
MapResult,
};
use crate::util::MappingError;
use crate::serializer::text::{Text, TextComponent};
use super::{
Alert, ContentsRenderer, ContinuationEndpoint, ResponseContext, SectionList, Tab, Thumbnails,
video_item::YouTubeListRenderer, Alert, ContentsRenderer, ResponseContext, SectionList, Tab,
ThumbnailsWrap, TwoColumnBrowseResults,
};
@ -26,15 +20,6 @@ pub(crate) struct Playlist {
pub response_context: ResponseContext,
}
#[serde_as]
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub(crate) struct PlaylistCont {
#[serde(default)]
#[serde_as(as = "VecSkipError<_>")]
pub on_response_received_actions: Vec<OnResponseReceivedAction>,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub(crate) struct ItemSection {
@ -44,13 +29,7 @@ pub(crate) struct ItemSection {
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub(crate) struct PlaylistVideoListRenderer {
pub playlist_video_list_renderer: PlaylistVideoList,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub(crate) struct PlaylistVideoList {
pub contents: MapResult<Vec<PlaylistItem>>,
pub playlist_video_list_renderer: YouTubeListRenderer,
}
#[derive(Debug, Deserialize)]
@ -130,63 +109,3 @@ pub(crate) struct PlaylistThumbnailRenderer {
#[serde(alias = "playlistCustomThumbnailRenderer")]
pub playlist_video_thumbnail_renderer: ThumbnailsWrap,
}
#[serde_as]
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub(crate) enum PlaylistItem {
/// Video in playlist
PlaylistVideoRenderer(PlaylistVideoRenderer),
/// Continauation items are located at the end of a list
/// and contain the continuation token for progressive loading
#[serde(rename_all = "camelCase")]
ContinuationItemRenderer {
continuation_endpoint: ContinuationEndpoint,
},
/// No video list item (e.g. ad) or unimplemented item
#[serde(other, deserialize_with = "deserialize_ignore_any")]
None,
}
/// Video displayed in a playlist
#[serde_as]
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub(crate) struct PlaylistVideoRenderer {
pub video_id: String,
pub thumbnail: Thumbnails,
#[serde_as(as = "Text")]
pub title: String,
#[serde(rename = "shortBylineText")]
pub channel: TextComponent,
#[serde_as(as = "JsonString")]
pub length_seconds: u32,
}
impl TryFrom<PlaylistVideoRenderer> for crate::model::PlaylistVideo {
type Error = MappingError;
fn try_from(video: PlaylistVideoRenderer) -> Result<Self, Self::Error> {
Ok(Self {
id: video.video_id,
name: video.title,
length: video.length_seconds,
thumbnail: video.thumbnail.into(),
channel: crate::model::ChannelId::try_from(video.channel)?,
})
}
}
// Continuation
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub(crate) struct OnResponseReceivedAction {
pub append_continuation_items_action: AppendAction,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub(crate) struct AppendAction {
pub continuation_items: MapResult<Vec<PlaylistItem>>,
}

View file

@ -9,8 +9,8 @@ use time::OffsetDateTime;
use super::{url_endpoint::NavigationEndpoint, ChannelBadge, ContinuationEndpoint, Thumbnails};
use crate::{
model::{
Channel, ChannelId, ChannelInfo, ChannelItem, ChannelTag, PlaylistItem, VideoItem,
YouTubeItem,
Channel, ChannelId, ChannelInfo, ChannelItem, ChannelTag, PlaylistItem, Verification,
VideoItem, YouTubeItem,
},
param::Language,
serializer::{
@ -27,6 +27,7 @@ pub(crate) enum YouTubeListItem {
#[serde(alias = "gridVideoRenderer", alias = "compactVideoRenderer")]
VideoRenderer(VideoRenderer),
ReelItemRenderer(ReelItemRenderer),
PlaylistVideoRenderer(PlaylistVideoRenderer),
#[serde(alias = "gridPlaylistRenderer")]
PlaylistRenderer(PlaylistRenderer),
@ -145,6 +146,33 @@ pub(crate) struct ReelItemRenderer {
pub navigation_endpoint: Option<ReelNavigationEndpoint>,
}
/// Video displayed in a playlist
#[serde_as]
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub(crate) struct PlaylistVideoRenderer {
pub video_id: String,
pub thumbnail: Thumbnails,
#[serde_as(as = "Text")]
pub title: String,
#[serde(rename = "shortBylineText")]
pub channel: TextComponent,
#[serde_as(as = "Option<JsonString>")]
pub length_seconds: Option<u32>,
/// Regular video: `["29K views", " • ", "13 years ago"]`
/// Livestream: `["66K", " watching"]`
/// Upcoming: `["8", " waiting"]`
#[serde(default)]
#[serde_as(as = "Text")]
pub video_info: Vec<String>,
/// Contains Short/Live tag
#[serde(default)]
#[serde_as(as = "VecSkipError<_>")]
pub thumbnail_overlays: Vec<TimeOverlay>,
/// Release date for upcoming videos
pub upcoming_event_data: Option<UpcomingEventData>,
}
/// Playlist displayed in search results
#[serde_as]
#[derive(Debug, Deserialize)]
@ -492,7 +520,7 @@ impl<T> YouTubeListMapper<T> {
}
}
fn map_short_video(&mut self, video: ReelItemRenderer, lang: Language) -> VideoItem {
fn map_short_video(&mut self, video: ReelItemRenderer) -> VideoItem {
let pub_date_txt = video.navigation_endpoint.map(|n| {
n.reel_watch_endpoint
.overlay
@ -505,7 +533,7 @@ impl<T> YouTubeListMapper<T> {
let length = video.accessibility.and_then(|acc| {
let parts = ACCESSIBILITY_SEP_REGEX.split(&acc).collect::<Vec<_>>();
if parts.len() > 2 {
let i = match lang {
let i = match self.lang {
Language::Ru => 1,
_ => 2,
};
@ -531,9 +559,9 @@ impl<T> YouTubeListMapper<T> {
timeago::parse_timeago_dt_or_warn(self.lang, txt, &mut self.warnings)
}),
publish_date_txt: pub_date_txt,
view_count: video
.view_count_text
.and_then(|txt| util::parse_large_numstr_or_warn(&txt, lang, &mut self.warnings)),
view_count: video.view_count_text.and_then(|txt| {
util::parse_large_numstr_or_warn(&txt, self.lang, &mut self.warnings)
}),
is_live: false,
is_short: true,
is_upcoming: false,
@ -541,6 +569,68 @@ impl<T> YouTubeListMapper<T> {
}
}
fn map_playlist_video(&mut self, video: PlaylistVideoRenderer) -> VideoItem {
let channel = ChannelId::try_from(video.channel)
.ok()
.map(|ch| ChannelTag {
id: ch.id,
name: ch.name,
avatar: Vec::new(),
verification: Verification::None,
subscriber_count: None,
});
let mut video_info = video.video_info.into_iter();
let video_info1 = video_info
.next()
.map(|s| match video_info.next().as_deref() {
None | Some(util::DOT_SEPARATOR) => s,
Some(s2) => s + s2,
});
let video_info2 = video_info.next();
// RU: "7 лет назад" " • " "210 млн просмотров" (order flipped)
let (view_count_txt, publish_date_txt) =
if self.lang == Language::Ru && video_info2.is_some() {
(video_info2, video_info1)
} else {
(video_info1, video_info2)
};
let is_live = video.thumbnail_overlays.is_live();
let publish_date = video
.upcoming_event_data
.as_ref()
.and_then(|upc| OffsetDateTime::from_unix_timestamp(upc.start_time).ok())
.or_else(|| {
if is_live {
None
} else {
publish_date_txt.as_ref().and_then(|txt| {
timeago::parse_timeago_dt_or_warn(self.lang, txt, &mut self.warnings)
})
}
});
VideoItem {
id: video.video_id,
name: video.title,
length: video.length_seconds,
thumbnail: video.thumbnail.into(),
channel,
publish_date,
publish_date_txt,
view_count: view_count_txt.and_then(|txt| {
util::parse_large_numstr_or_warn(&txt, self.lang, &mut self.warnings)
}),
is_live,
is_short: video.thumbnail_overlays.is_short(),
is_upcoming: video.upcoming_event_data.is_some(),
short_description: None,
}
}
fn map_playlist(&self, playlist: PlaylistRenderer) -> PlaylistItem {
PlaylistItem {
id: playlist.playlist_id,
@ -607,7 +697,11 @@ impl YouTubeListMapper<YouTubeItem> {
self.items.push(mapped);
}
YouTubeListItem::ReelItemRenderer(video) => {
let mapped = self.map_short_video(video, self.lang);
let mapped = self.map_short_video(video);
self.items.push(YouTubeItem::Video(mapped));
}
YouTubeListItem::PlaylistVideoRenderer(video) => {
let mapped = self.map_playlist_video(video);
self.items.push(YouTubeItem::Video(mapped));
}
YouTubeListItem::PlaylistRenderer(playlist) => {
@ -671,7 +765,11 @@ impl YouTubeListMapper<VideoItem> {
self.items.push(mapped);
}
YouTubeListItem::ReelItemRenderer(video) => {
let mapped = self.map_short_video(video, self.lang);
let mapped = self.map_short_video(video);
self.items.push(mapped);
}
YouTubeListItem::PlaylistVideoRenderer(video) => {
let mapped = self.map_playlist_video(video);
self.items.push(mapped);
}
YouTubeListItem::ContinuationItemRenderer {

View file

@ -1,13 +1,13 @@
---
source: src/client/channel.rs
expression: map_res.c
source: src/client/pagination.rs
expression: paginator
---
Paginator(
count: None,
items: [
ChannelVideo(
VideoItem(
id: "R2fw2g6WFbg",
title: "EEVblog 1477 - TEARDOWN! - NEW Tektronix 2 Series Oscilloscope",
name: "EEVblog 1477 - TEARDOWN! - NEW Tektronix 2 Series Oscilloscope",
length: Some(2718),
thumbnail: [
Thumbnail(
@ -31,16 +31,18 @@ Paginator(
height: 188,
),
],
channel: None,
publish_date: "[date]",
publish_date_txt: Some("3 months ago"),
view_count: 80296,
view_count: Some(80296),
is_live: false,
is_short: false,
is_upcoming: false,
short_description: None,
),
ChannelVideo(
VideoItem(
id: "JDXKrXJloSw",
title: "EEVblog 1476 - Keithley 515A Wheatstone Bridge TEARDOWN & TUTORIAL",
name: "EEVblog 1476 - Keithley 515A Wheatstone Bridge TEARDOWN & TUTORIAL",
length: Some(1721),
thumbnail: [
Thumbnail(
@ -64,16 +66,18 @@ Paginator(
height: 188,
),
],
channel: None,
publish_date: "[date]",
publish_date_txt: Some("3 months ago"),
view_count: 36294,
view_count: Some(36294),
is_live: false,
is_short: false,
is_upcoming: false,
short_description: None,
),
ChannelVideo(
VideoItem(
id: "H8ot_YPi6QU",
title: "eevBLAB 98 - The Pressure Youtubers Are Under",
name: "eevBLAB 98 - The Pressure Youtubers Are Under",
length: Some(431),
thumbnail: [
Thumbnail(
@ -97,16 +101,18 @@ Paginator(
height: 188,
),
],
channel: None,
publish_date: "[date]",
publish_date_txt: Some("3 months ago"),
view_count: 34736,
view_count: Some(34736),
is_live: false,
is_short: false,
is_upcoming: false,
short_description: None,
),
ChannelVideo(
VideoItem(
id: "i1Ad5jfk_v4",
title: "EEVblog 1475 - What\'s This SMD Part?",
name: "EEVblog 1475 - What\'s This SMD Part?",
length: Some(1785),
thumbnail: [
Thumbnail(
@ -130,16 +136,18 @@ Paginator(
height: 188,
),
],
channel: None,
publish_date: "[date]",
publish_date_txt: Some("3 months ago"),
view_count: 73544,
view_count: Some(73544),
is_live: false,
is_short: false,
is_upcoming: false,
short_description: None,
),
ChannelVideo(
VideoItem(
id: "GHbo4v8pahc",
title: "eevBLAB 97 - Is Apple Serious About Right To Repair? (The Verge)",
name: "eevBLAB 97 - Is Apple Serious About Right To Repair? (The Verge)",
length: Some(1186),
thumbnail: [
Thumbnail(
@ -163,16 +171,18 @@ Paginator(
height: 188,
),
],
channel: None,
publish_date: "[date]",
publish_date_txt: Some("4 months ago"),
view_count: 67231,
view_count: Some(67231),
is_live: false,
is_short: false,
is_upcoming: false,
short_description: None,
),
ChannelVideo(
VideoItem(
id: "Uds-wLoaZmA",
title: "EEVblog 1474 - Can You Measure Capacitors IN Circuit?",
name: "EEVblog 1474 - Can You Measure Capacitors IN Circuit?",
length: Some(1407),
thumbnail: [
Thumbnail(
@ -196,16 +206,18 @@ Paginator(
height: 188,
),
],
channel: None,
publish_date: "[date]",
publish_date_txt: Some("4 months ago"),
view_count: 44946,
view_count: Some(44946),
is_live: false,
is_short: false,
is_upcoming: false,
short_description: None,
),
ChannelVideo(
VideoItem(
id: "D9J-AmCcf4U",
title: "EEVblog 1473 - How Your LCR Meter Works",
name: "EEVblog 1473 - How Your LCR Meter Works",
length: Some(1183),
thumbnail: [
Thumbnail(
@ -229,16 +241,18 @@ Paginator(
height: 188,
),
],
channel: None,
publish_date: "[date]",
publish_date_txt: Some("4 months ago"),
view_count: 43264,
view_count: Some(43264),
is_live: false,
is_short: false,
is_upcoming: false,
short_description: None,
),
ChannelVideo(
VideoItem(
id: "Eoh-JKVQZwg",
title: "EEVblog 1472 - Resistor Cube Problem SOLVED",
name: "EEVblog 1472 - Resistor Cube Problem SOLVED",
length: Some(1196),
thumbnail: [
Thumbnail(
@ -262,16 +276,18 @@ Paginator(
height: 188,
),
],
channel: None,
publish_date: "[date]",
publish_date_txt: Some("4 months ago"),
view_count: 98175,
view_count: Some(98175),
is_live: false,
is_short: false,
is_upcoming: false,
short_description: None,
),
ChannelVideo(
VideoItem(
id: "U81glZBDpIg",
title: "EEVblog 1471 - Mailbag",
name: "EEVblog 1471 - Mailbag",
length: Some(2252),
thumbnail: [
Thumbnail(
@ -295,16 +311,18 @@ Paginator(
height: 188,
),
],
channel: None,
publish_date: "[date]",
publish_date_txt: Some("4 months ago"),
view_count: 59376,
view_count: Some(59376),
is_live: false,
is_short: false,
is_upcoming: false,
short_description: None,
),
ChannelVideo(
VideoItem(
id: "gLfxznVJ2q0",
title: "Petition - Australian Standards Should be FREE",
name: "Petition - Australian Standards Should be FREE",
length: Some(585),
thumbnail: [
Thumbnail(
@ -328,16 +346,18 @@ Paginator(
height: 188,
),
],
channel: None,
publish_date: "[date]",
publish_date_txt: Some("5 months ago"),
view_count: 25496,
view_count: Some(25496),
is_live: false,
is_short: false,
is_upcoming: false,
short_description: None,
),
ChannelVideo(
VideoItem(
id: "GfihUkWPCQQ",
title: "EEVblog 1470 - AC Basics Tutorial Part 3 - Complex Numbers are EASY!",
name: "EEVblog 1470 - AC Basics Tutorial Part 3 - Complex Numbers are EASY!",
length: Some(1468),
thumbnail: [
Thumbnail(
@ -361,16 +381,18 @@ Paginator(
height: 188,
),
],
channel: None,
publish_date: "[date]",
publish_date_txt: Some("5 months ago"),
view_count: 22982,
view_count: Some(22982),
is_live: false,
is_short: false,
is_upcoming: false,
short_description: None,
),
ChannelVideo(
VideoItem(
id: "yEG6pKUdIlg",
title: "EEVblog 1469 - AC Basics Tutorial - Part 2 - Phasors",
name: "EEVblog 1469 - AC Basics Tutorial - Part 2 - Phasors",
length: Some(1147),
thumbnail: [
Thumbnail(
@ -394,16 +416,18 @@ Paginator(
height: 188,
),
],
channel: None,
publish_date: "[date]",
publish_date_txt: Some("5 months ago"),
view_count: 38804,
view_count: Some(38804),
is_live: false,
is_short: false,
is_upcoming: false,
short_description: None,
),
ChannelVideo(
VideoItem(
id: "wPzzPGzxD00",
title: "EEVblog 1468 - Electronex Show Tour 2022",
name: "EEVblog 1468 - Electronex Show Tour 2022",
length: Some(2850),
thumbnail: [
Thumbnail(
@ -427,16 +451,18 @@ Paginator(
height: 188,
),
],
channel: None,
publish_date: "[date]",
publish_date_txt: Some("5 months ago"),
view_count: 25505,
view_count: Some(25505),
is_live: false,
is_short: false,
is_upcoming: false,
short_description: None,
),
ChannelVideo(
VideoItem(
id: "Tdge8vEODeY",
title: "EEVblog 1467 - Stanford Solar Power at Nightime! BUSTED",
name: "EEVblog 1467 - Stanford Solar Power at Nightime! BUSTED",
length: Some(836),
thumbnail: [
Thumbnail(
@ -460,16 +486,18 @@ Paginator(
height: 188,
),
],
channel: None,
publish_date: "[date]",
publish_date_txt: Some("5 months ago"),
view_count: 98432,
view_count: Some(98432),
is_live: false,
is_short: false,
is_upcoming: false,
short_description: None,
),
ChannelVideo(
VideoItem(
id: "ebQ2Im5zfT0",
title: "EEVblog 1466 - Dumpster Dive Xeon Server",
name: "EEVblog 1466 - Dumpster Dive Xeon Server",
length: Some(1138),
thumbnail: [
Thumbnail(
@ -493,16 +521,18 @@ Paginator(
height: 188,
),
],
channel: None,
publish_date: "[date]",
publish_date_txt: Some("5 months ago"),
view_count: 53410,
view_count: Some(53410),
is_live: false,
is_short: false,
is_upcoming: false,
short_description: None,
),
ChannelVideo(
VideoItem(
id: "UrS5ezesA9s",
title: "EEVblog 1465 - Your Multimeter Can Measure Inductors",
name: "EEVblog 1465 - Your Multimeter Can Measure Inductors",
length: Some(596),
thumbnail: [
Thumbnail(
@ -526,16 +556,18 @@ Paginator(
height: 188,
),
],
channel: None,
publish_date: "[date]",
publish_date_txt: Some("5 months ago"),
view_count: 54771,
view_count: Some(54771),
is_live: false,
is_short: false,
is_upcoming: false,
short_description: None,
),
ChannelVideo(
VideoItem(
id: "c5M8P6oe9xY",
title: "EEVblog 1464 - TOP 5 Jellybean Comparators",
name: "EEVblog 1464 - TOP 5 Jellybean Comparators",
length: Some(2399),
thumbnail: [
Thumbnail(
@ -559,16 +591,18 @@ Paginator(
height: 188,
),
],
channel: None,
publish_date: "[date]",
publish_date_txt: Some("6 months ago"),
view_count: 39823,
view_count: Some(39823),
is_live: false,
is_short: false,
is_upcoming: false,
short_description: None,
),
ChannelVideo(
VideoItem(
id: "9TDKP9RLlPs",
title: "EEVblog 1463 - Mailbag",
name: "EEVblog 1463 - Mailbag",
length: Some(2664),
thumbnail: [
Thumbnail(
@ -592,16 +626,18 @@ Paginator(
height: 188,
),
],
channel: None,
publish_date: "[date]",
publish_date_txt: Some("6 months ago"),
view_count: 51596,
view_count: Some(51596),
is_live: false,
is_short: false,
is_upcoming: false,
short_description: None,
),
ChannelVideo(
VideoItem(
id: "hwggIw2HQuQ",
title: "eevBLAB 96 - BUSTED! - Dymo Gets WORSE!",
name: "eevBLAB 96 - BUSTED! - Dymo Gets WORSE!",
length: Some(347),
thumbnail: [
Thumbnail(
@ -625,16 +661,18 @@ Paginator(
height: 188,
),
],
channel: None,
publish_date: "[date]",
publish_date_txt: Some("6 months ago"),
view_count: 125391,
view_count: Some(125391),
is_live: false,
is_short: false,
is_upcoming: false,
short_description: None,
),
ChannelVideo(
VideoItem(
id: "xzSDJRC0F6c",
title: "EEVblog 1462 - Why Dymo Label Printers SUCK!",
name: "EEVblog 1462 - Why Dymo Label Printers SUCK!",
length: Some(1353),
thumbnail: [
Thumbnail(
@ -658,16 +696,18 @@ Paginator(
height: 188,
),
],
channel: None,
publish_date: "[date]",
publish_date_txt: Some("6 months ago"),
view_count: 120457,
view_count: Some(120457),
is_live: false,
is_short: false,
is_upcoming: false,
short_description: None,
),
ChannelVideo(
VideoItem(
id: "9wuyPZjjR9k",
title: "EEVblog 1461 - The MOSFET Search CHALLENGE",
name: "EEVblog 1461 - The MOSFET Search CHALLENGE",
length: Some(3505),
thumbnail: [
Thumbnail(
@ -691,16 +731,18 @@ Paginator(
height: 188,
),
],
channel: None,
publish_date: "[date]",
publish_date_txt: Some("6 months ago"),
view_count: 49062,
view_count: Some(49062),
is_live: false,
is_short: false,
is_upcoming: false,
short_description: None,
),
ChannelVideo(
VideoItem(
id: "vyJuMGEFbjQ",
title: "EEVblog1460 - REPAIRING a LED Studio Light with a DUMPSTER LAPTOP!",
name: "EEVblog1460 - REPAIRING a LED Studio Light with a DUMPSTER LAPTOP!",
length: Some(1798),
thumbnail: [
Thumbnail(
@ -724,16 +766,18 @@ Paginator(
height: 188,
),
],
channel: None,
publish_date: "[date]",
publish_date_txt: Some("6 months ago"),
view_count: 49032,
view_count: Some(49032),
is_live: false,
is_short: false,
is_upcoming: false,
short_description: None,
),
ChannelVideo(
VideoItem(
id: "_pETMto-9iE",
title: "EEVblog 1459 - Is it worth PARTS SALVAGING an Inkjet Printer/Scanner?",
name: "EEVblog 1459 - Is it worth PARTS SALVAGING an Inkjet Printer/Scanner?",
length: Some(1588),
thumbnail: [
Thumbnail(
@ -757,16 +801,18 @@ Paginator(
height: 188,
),
],
channel: None,
publish_date: "[date]",
publish_date_txt: Some("7 months ago"),
view_count: 64108,
view_count: Some(64108),
is_live: false,
is_short: false,
is_upcoming: false,
short_description: None,
),
ChannelVideo(
VideoItem(
id: "MvFf9RSJUhk",
title: "EEVblog 1458 - Microscope Polarising MAGIC!",
name: "EEVblog 1458 - Microscope Polarising MAGIC!",
length: Some(942),
thumbnail: [
Thumbnail(
@ -790,16 +836,18 @@ Paginator(
height: 188,
),
],
channel: None,
publish_date: "[date]",
publish_date_txt: Some("7 months ago"),
view_count: 76831,
view_count: Some(76831),
is_live: false,
is_short: false,
is_upcoming: false,
short_description: None,
),
ChannelVideo(
VideoItem(
id: "E6obq3T71vI",
title: "EEVblog1457 - Old School Mailbag - ESC Burnout",
name: "EEVblog1457 - Old School Mailbag - ESC Burnout",
length: Some(1552),
thumbnail: [
Thumbnail(
@ -823,16 +871,18 @@ Paginator(
height: 188,
),
],
channel: None,
publish_date: "[date]",
publish_date_txt: Some("7 months ago"),
view_count: 49961,
view_count: Some(49961),
is_live: false,
is_short: false,
is_upcoming: false,
short_description: None,
),
ChannelVideo(
VideoItem(
id: "ZTwVQmUm6NY",
title: "eevBLAB 95 - Why Are Youtube Playlists So BAD?",
name: "eevBLAB 95 - Why Are Youtube Playlists So BAD?",
length: Some(865),
thumbnail: [
Thumbnail(
@ -856,16 +906,18 @@ Paginator(
height: 188,
),
],
channel: None,
publish_date: "[date]",
publish_date_txt: Some("7 months ago"),
view_count: 17393,
view_count: Some(17393),
is_live: false,
is_short: false,
is_upcoming: false,
short_description: None,
),
ChannelVideo(
VideoItem(
id: "prQinQ4MWmU",
title: "EEVblog 1456 - Sega Toys Homestar Planetarium REPAIR",
name: "EEVblog 1456 - Sega Toys Homestar Planetarium REPAIR",
length: Some(899),
thumbnail: [
Thumbnail(
@ -889,16 +941,18 @@ Paginator(
height: 188,
),
],
channel: None,
publish_date: "[date]",
publish_date_txt: Some("7 months ago"),
view_count: 38281,
view_count: Some(38281),
is_live: false,
is_short: false,
is_upcoming: false,
short_description: None,
),
ChannelVideo(
VideoItem(
id: "yMIzsFAztv4",
title: "EEVblog 1455 - Capacitors Produce Current During Reflow Soldering! WTF!",
name: "EEVblog 1455 - Capacitors Produce Current During Reflow Soldering! WTF!",
length: Some(894),
thumbnail: [
Thumbnail(
@ -922,16 +976,18 @@ Paginator(
height: 188,
),
],
channel: None,
publish_date: "[date]",
publish_date_txt: Some("7 months ago"),
view_count: 70004,
view_count: Some(70004),
is_live: false,
is_short: false,
is_upcoming: false,
short_description: None,
),
ChannelVideo(
VideoItem(
id: "QtqljdMwRyk",
title: "EEVblog 1454 - Water from Air AGAIN! - The Kara Pure",
name: "EEVblog 1454 - Water from Air AGAIN! - The Kara Pure",
length: Some(1198),
thumbnail: [
Thumbnail(
@ -955,16 +1011,18 @@ Paginator(
height: 188,
),
],
channel: None,
publish_date: "[date]",
publish_date_txt: Some("7 months ago"),
view_count: 93700,
view_count: Some(93700),
is_live: false,
is_short: false,
is_upcoming: false,
short_description: None,
),
ChannelVideo(
VideoItem(
id: "kcWwAweWjQg",
title: "EEVblog 1453 - Elgato Key Light TEARDOWN",
name: "EEVblog 1453 - Elgato Key Light TEARDOWN",
length: Some(1048),
thumbnail: [
Thumbnail(
@ -988,13 +1046,16 @@ Paginator(
height: 188,
),
],
channel: None,
publish_date: "[date]",
publish_date_txt: Some("7 months ago"),
view_count: 37515,
view_count: Some(37515),
is_live: false,
is_short: false,
is_upcoming: false,
short_description: None,
),
],
ctoken: Some("4qmFsgKxARIYVUMyRGpGRTdYZjExVVJacVdCaWdjVk9RGmZFZ1oyYVdSbGIzTVlBeUFBTUFFNEFlb0ROME5uVGtSU1JXdFRRM2RwU1cxMGNUaHpTVVJ6TkhCRlFrdEVTWGRCYW1jNFVXZHpTUzFRUkVodFVWbFJhVzloY0VWclowTlZSRWslM0SaAixicm93c2UtZmVlZFVDMkRqRkU3WGYxMVVSWnFXQmlnY1ZPUXZpZGVvczEwMg%3D%3D"),
endpoint: browse,
)

File diff suppressed because it is too large Load diff

View file

@ -8,11 +8,21 @@ Paginator(
Comment(
id: "UgyrLHPb5Q8yx7XzfKl4AaABAg",
text: RichText([
Text("200.000.000 "),
Text("🤘🏻"),
Text("🫶"),
Text("🏻"),
Text("💗"),
Text(
text: "200.000.000 ",
),
Text(
text: "🤘🏻",
),
Text(
text: "🫶",
),
Text(
text: "🏻",
),
Text(
text: "💗",
),
]),
author: Some(ChannelTag(
id: "UCKv2ITDFlBf8sCaM4bGAq9w",
@ -54,7 +64,9 @@ Paginator(
Comment(
id: "UgwgAG_gp2OsyCJhxh54AaABAg",
text: RichText([
Text("Mys, spotify wraps comes in a few months..so start str3ming Aespa all songs on spotify."),
Text(
text: "Mys, spotify wraps comes in a few months..so start str3ming Aespa all songs on spotify.",
),
]),
author: Some(ChannelTag(
id: "UCpO0iV02gvz1zY0xaEip-Lw",
@ -96,8 +108,12 @@ Paginator(
Comment(
id: "UgyZRJd_8n_wWXjWk0Z4AaABAg",
text: RichText([
Text("Cerita black mamba kan udah tutup buku. Kira2 nanti konsep comebacknya apa yaa? "),
Text("😍"),
Text(
text: "Cerita black mamba kan udah tutup buku. Kira2 nanti konsep comebacknya apa yaa? ",
),
Text(
text: "😍",
),
]),
author: Some(ChannelTag(
id: "UC8Uu90kmT1uBXMuylOe5F2g",
@ -139,11 +155,21 @@ Paginator(
Comment(
id: "UgzHO_WuV1ASUFXXV5Z4AaABAg",
text: RichText([
Text("What do u guys think. Amazing debut song."),
Text("\n"),
Text("Like if u think so"),
Text("\n"),
Text("👇"),
Text(
text: "What do u guys think. Amazing debut song.",
),
Text(
text: "\n",
),
Text(
text: "Like if u think so",
),
Text(
text: "\n",
),
Text(
text: "👇",
),
]),
author: Some(ChannelTag(
id: "UCKBVxnCV35xZ9Oe1U6DLENQ",
@ -185,7 +211,9 @@ Paginator(
Comment(
id: "UgyUnq05V9-nzf-LOTV4AaABAg",
text: RichText([
Text("Love this song so much"),
Text(
text: "Love this song so much",
),
]),
author: Some(ChannelTag(
id: "UCsJS5T1NM3Ra7HLmiPjxeGg",
@ -227,7 +255,9 @@ Paginator(
Comment(
id: "UgwNsbK2-lmMt0B8LrJ4AaABAg",
text: RichText([
Text("They are the background of our karaoke"),
Text(
text: "They are the background of our karaoke",
),
]),
author: Some(ChannelTag(
id: "UCh6Qqw1NFPNi6l7kfU1b4PA",
@ -269,7 +299,9 @@ Paginator(
Comment(
id: "UgyGXKLpUGtLHkg-8n54AaABAg",
text: RichText([
Text("AESPA"),
Text(
text: "AESPA",
),
]),
author: Some(ChannelTag(
id: "UCh6Qqw1NFPNi6l7kfU1b4PA",
@ -311,7 +343,9 @@ Paginator(
Comment(
id: "Ugx5WMDdp479KsHnzFx4AaABAg",
text: RichText([
Text("AVATAR EXPERIENCE ASPECT"),
Text(
text: "AVATAR EXPERIENCE ASPECT",
),
]),
author: Some(ChannelTag(
id: "UCh6Qqw1NFPNi6l7kfU1b4PA",
@ -353,7 +387,9 @@ Paginator(
Comment(
id: "UgyYed-oE-wKjQriOfp4AaABAg",
text: RichText([
Text("HEIHH!!"),
Text(
text: "HEIHH!!",
),
]),
author: Some(ChannelTag(
id: "UCgTOQ5KTAurWso9Hw2IGVow",
@ -395,7 +431,9 @@ Paginator(
Comment(
id: "UgxeiRIMXSk9_z4JhrZ4AaABAg",
text: RichText([
Text("Road to 234M!"),
Text(
text: "Road to 234M!",
),
]),
author: Some(ChannelTag(
id: "UCuTNUUpUzXVljMSEy1YooZw",
@ -437,7 +475,9 @@ Paginator(
Comment(
id: "UgyAfVKtB-4GsArpyFt4AaABAg",
text: RichText([
Text("Happy 4m likes!"),
Text(
text: "Happy 4m likes!",
),
]),
author: Some(ChannelTag(
id: "UCbfj_QOoelszMm1JlXpY7nw",
@ -479,7 +519,9 @@ Paginator(
Comment(
id: "UgzA83tcCkEQKawjyY54AaABAg",
text: RichText([
Text("MY\'s and aespa go fighting!"),
Text(
text: "MY\'s and aespa go fighting!",
),
]),
author: Some(ChannelTag(
id: "UCh6Qqw1NFPNi6l7kfU1b4PA",
@ -521,7 +563,9 @@ Paginator(
Comment(
id: "UgyCWVh43JdpxgevI-Z4AaABAg",
text: RichText([
Text("I hope they bring back this colorful set for the next comeback"),
Text(
text: "I hope they bring back this colorful set for the next comeback",
),
]),
author: Some(ChannelTag(
id: "UCh6Qqw1NFPNi6l7kfU1b4PA",
@ -563,7 +607,9 @@ Paginator(
Comment(
id: "Ugz8T4zrMSdovk8PwJl4AaABAg",
text: RichText([
Text("Keep str3aming MY\'s"),
Text(
text: "Keep str3aming MY\'s",
),
]),
author: Some(ChannelTag(
id: "UCh6Qqw1NFPNi6l7kfU1b4PA",
@ -605,7 +651,9 @@ Paginator(
Comment(
id: "UgyX8EhFG3R2mzdS57t4AaABAg",
text: RichText([
Text("Ji-min-jeong Cross!"),
Text(
text: "Ji-min-jeong Cross!",
),
]),
author: Some(ChannelTag(
id: "UCh6Qqw1NFPNi6l7kfU1b4PA",
@ -647,7 +695,9 @@ Paginator(
Comment(
id: "UgwCYEQKVU2LLcfUaeB4AaABAg",
text: RichText([
Text("BEST DEBUT EVER !!!"),
Text(
text: "BEST DEBUT EVER !!!",
),
]),
author: Some(ChannelTag(
id: "UC-wGUUnJO1gTAw011kCGwfQ",
@ -689,7 +739,9 @@ Paginator(
Comment(
id: "UgwV_-ac6tBgXSoIhSt4AaABAg",
text: RichText([
Text("เพลงโครตน\u{e48}าเบ\u{e37}\u{e48}อ ด\u{e35}แค\u{e48} cg"),
Text(
text: "เพลงโครตน\u{e48}าเบ\u{e37}\u{e48}อ ด\u{e35}แค\u{e48} cg",
),
]),
author: Some(ChannelTag(
id: "UCE7UmaNfXFkZ8iO8SZnx1_g",
@ -731,7 +783,9 @@ Paginator(
Comment(
id: "Ugy0_tjx6M2D6MYACA94AaABAg",
text: RichText([
Text("233230"),
Text(
text: "233230",
),
]),
author: Some(ChannelTag(
id: "UCTV1SSSkB8Rr9Y5pD1tN8eA",

View file

@ -8,10 +8,18 @@ Paginator(
Comment(
id: "UgwcU73OBV8OmSBOKrt4AaABAg",
text: RichText([
Text("Who\'s excited for the special choreography video?? Let\'s go 100M!!!"),
Text("\n"),
Text("\n"),
Text("👇"),
Text(
text: "Who\'s excited for the special choreography video?? Let\'s go 100M!!!",
),
Text(
text: "\n",
),
Text(
text: "\n",
),
Text(
text: "👇",
),
]),
author: Some(ChannelTag(
id: "UC--QwZHg12dkw8UKw5yB1MQ",
@ -53,7 +61,9 @@ Paginator(
Comment(
id: "Ugyrg-RcAMjT9yefZtJ4AaABAg",
text: RichText([
Text("Chicas vamos por los 300M, démosle a todas esas haters chorrillo masivo fulminante de la envidia. Vamos, Mys puede a llegar a ser poderoso si nos unimos. Vamos por los 300M solo nos falta poco para llegar!!!!!!! Vamos Mys, alcemos nuestro teléfonos y brindemos por lo que puede hacer las Mys por Aespa"),
Text(
text: "Chicas vamos por los 300M, démosle a todas esas haters chorrillo masivo fulminante de la envidia. Vamos, Mys puede a llegar a ser poderoso si nos unimos. Vamos por los 300M solo nos falta poco para llegar!!!!!!! Vamos Mys, alcemos nuestro teléfonos y brindemos por lo que puede hacer las Mys por Aespa",
),
]),
author: Some(ChannelTag(
id: "UCqT9iACjb69o254SFlUjcDQ",
@ -95,7 +105,9 @@ Paginator(
Comment(
id: "UgzP00PlfmotA1LxkS94AaABAg",
text: RichText([
Text("The best 4th gen group debut for me. Camerawork, artistic value, choreography, concept, make up and styling, this one nailed ALL."),
Text(
text: "The best 4th gen group debut for me. Camerawork, artistic value, choreography, concept, make up and styling, this one nailed ALL.",
),
]),
author: Some(ChannelTag(
id: "UC00F5JjCjDDum6MlP3eloxw",
@ -137,7 +149,9 @@ Paginator(
Comment(
id: "UgzQJw-ncvJM2tmMstp4AaABAg",
text: RichText([
Text("Black Manba o MV mais icônico do aespa amooooi"),
Text(
text: "Black Manba o MV mais icônico do aespa amooooi",
),
]),
author: Some(ChannelTag(
id: "UCbfGo61YSYOlfyF1cWejhng",
@ -179,7 +193,9 @@ Paginator(
Comment(
id: "Ugz3auWqfNwtNT1UCd54AaABAg",
text: RichText([
Text("MYs if we want to make Black Mamba the most debut MV for 4th gen- let us keep on strêaming. Consistency is the key."),
Text(
text: "MYs if we want to make Black Mamba the most debut MV for 4th gen- let us keep on strêaming. Consistency is the key.",
),
]),
author: Some(ChannelTag(
id: "UCITMz9SiXQrV-cltI-9Auuw",
@ -221,12 +237,24 @@ Paginator(
Comment(
id: "Ugz9k0J4D8v1l101-zN4AaABAg",
text: RichText([
Text("Usually this type of songs don\'t really click with me, but damn this is a masterpiece"),
Text("\n"),
Text("Also why i just find out that this is their debut song after listening it many times..."),
Text("\n"),
Text("\n"),
Text("(ps. Winter is my love <3)"),
Text(
text: "Usually this type of songs don\'t really click with me, but damn this is a masterpiece",
),
Text(
text: "\n",
),
Text(
text: "Also why i just find out that this is their debut song after listening it many times...",
),
Text(
text: "\n",
),
Text(
text: "\n",
),
Text(
text: "(ps. Winter is my love <3)",
),
]),
author: Some(ChannelTag(
id: "UCDaOL1lsanMlpbzaQunz47Q",
@ -268,13 +296,27 @@ Paginator(
Comment(
id: "Ugznd4KqU5k8A7axB7V4AaABAg",
text: RichText([
Text("Keep streaming mys"),
Text("\n"),
Text("Black Mamba and Next Level --> 200 M"),
Text("\n"),
Text("Savage --> 160 M"),
Text("\n"),
Text("Forever, Dreams come true --> 50 M"),
Text(
text: "Keep streaming mys",
),
Text(
text: "\n",
),
Text(
text: "Black Mamba and Next Level --> 200 M",
),
Text(
text: "\n",
),
Text(
text: "Savage --> 160 M",
),
Text(
text: "\n",
),
Text(
text: "Forever, Dreams come true --> 50 M",
),
]),
author: Some(ChannelTag(
id: "UC2P4Zx1o4D3VQIjA_MIToQQ",
@ -316,7 +358,9 @@ Paginator(
Comment(
id: "UgzHSudD7eVIrU37HBV4AaABAg",
text: RichText([
Text("Logremos más de 250M Antes de que se termine el año ♡"),
Text(
text: "Logremos más de 250M Antes de que se termine el año ♡",
),
]),
author: Some(ChannelTag(
id: "UC5cxR3XtdfVR_XT3sfIKdNg",
@ -358,7 +402,9 @@ Paginator(
Comment(
id: "UgxmAIlOcpyAMGUZOlh4AaABAg",
text: RichText([
Text("Llevemos está Masterpiece a los 300M!! ಥ‿ಥ♡ Don\'t stop My, 230M soon"),
Text(
text: "Llevemos está Masterpiece a los 300M!! ಥ‿ಥ♡ Don\'t stop My, 230M soon",
),
]),
author: Some(ChannelTag(
id: "UC5cxR3XtdfVR_XT3sfIKdNg",
@ -400,7 +446,9 @@ Paginator(
Comment(
id: "UgycqtDEEEP5fSwm8tt4AaABAg",
text: RichText([
Text("Keep streaming my\'s let\'s reach 300 million views before aespa\'s full album comeback."),
Text(
text: "Keep streaming my\'s let\'s reach 300 million views before aespa\'s full album comeback.",
),
]),
author: Some(ChannelTag(
id: "UCfQOeizIiPTG3hgKa-6MOeA",
@ -442,9 +490,15 @@ Paginator(
Comment(
id: "UgxgsPeAR4eQ_BjEXs94AaABAg",
text: RichText([
Text("Hay que seguir reproduciendo para que llegue a 300 millones pronto antes de que cumpla dos años en noviembre "),
Text("\n"),
Text("Vamos que si se puede"),
Text(
text: "Hay que seguir reproduciendo para que llegue a 300 millones pronto antes de que cumpla dos años en noviembre ",
),
Text(
text: "\n",
),
Text(
text: "Vamos que si se puede",
),
]),
author: Some(ChannelTag(
id: "UCgRWbRJqUHCvWv6xOuB4few",
@ -486,7 +540,9 @@ Paginator(
Comment(
id: "UgyTlZ_kkX6rrt5wKEp4AaABAg",
text: RichText([
Text("DENLE LIKE AL MV Q FALTA POCO PARA LOS 4M DE LIKES ¡!¡!¡"),
Text(
text: "DENLE LIKE AL MV Q FALTA POCO PARA LOS 4M DE LIKES ¡!¡!¡",
),
]),
author: Some(ChannelTag(
id: "UCBqR9kJhtiHLE6dP-Oo_DMQ",
@ -528,7 +584,9 @@ Paginator(
Comment(
id: "Ugyg3D9dUIErJ4y_L2l4AaABAg",
text: RichText([
Text("146 millones vamos por 147 millones si se puede vamos a por mas GO"),
Text(
text: "146 millones vamos por 147 millones si se puede vamos a por mas GO",
),
]),
author: Some(ChannelTag(
id: "UCjbUNOs6PAAAAo3_feDwLQA",
@ -570,7 +628,9 @@ Paginator(
Comment(
id: "UgymMTcc-41D0p8oXL54AaABAg",
text: RichText([
Text("Best MV debut for this gen!"),
Text(
text: "Best MV debut for this gen!",
),
]),
author: Some(ChannelTag(
id: "UCjXq-4-e-OKn1S3SB52wHRQ",
@ -612,7 +672,9 @@ Paginator(
Comment(
id: "UgznkUYwXNa5x-m6FNF4AaABAg",
text: RichText([
Text("Ya es 2022 y aún no superó este M/V, stream mys para los 300M!"),
Text(
text: "Ya es 2022 y aún no superó este M/V, stream mys para los 300M!",
),
]),
author: Some(ChannelTag(
id: "UCwan9APGANDUfYdYBbU6OeQ",
@ -654,7 +716,9 @@ Paginator(
Comment(
id: "UgxYqYkFAgWb3JaTHYt4AaABAg",
text: RichText([
Text("I get goosebump every single time it comes to NingNings high note. Baby youre amazingggg"),
Text(
text: "I get goosebump every single time it comes to NingNings high note. Baby youre amazingggg",
),
]),
author: Some(ChannelTag(
id: "UC3IEDx0AQiUOP8FtC_MlyFQ",
@ -696,7 +760,9 @@ Paginator(
Comment(
id: "Ugy4IPsS4P7c9YzMhT54AaABAg",
text: RichText([
Text("This is still the best K-pop debut <3 MYS LETS STREAM HARD FOR GIRLS"),
Text(
text: "This is still the best K-pop debut <3 MYS LETS STREAM HARD FOR GIRLS",
),
]),
author: Some(ChannelTag(
id: "UC2vbH7mtFRTiPGLRt9l_OSw",
@ -738,7 +804,9 @@ Paginator(
Comment(
id: "UgwsT7Wpll7wOJQUcWd4AaABAg",
text: RichText([
Text("Don\'t be shy SM Entertainment! Give the girls another legendary comeback!"),
Text(
text: "Don\'t be shy SM Entertainment! Give the girls another legendary comeback!",
),
]),
author: Some(ChannelTag(
id: "UCWgL2cewcYnsIqwN1tMikJg",
@ -780,7 +848,9 @@ Paginator(
Comment(
id: "UgzoZf9nG7VBNhcDLRF4AaABAg",
text: RichText([
Text("aespa\'s debut song is really powerful. I\'ve been listening to this song many times since its release. But, the energy the song is sending me is just as powerful"),
Text(
text: "aespa\'s debut song is really powerful. I\'ve been listening to this song many times since its release. But, the energy the song is sending me is just as powerful",
),
]),
author: Some(ChannelTag(
id: "UCN4tkZuSBXlZ5b9E8CETBUw",
@ -822,9 +892,15 @@ Paginator(
Comment(
id: "UgyMeq1WDhSYAIJ3JQh4AaABAg",
text: RichText([
Text("219,2M still one of my fav debut song! "),
Text("🔥"),
Text("💎"),
Text(
text: "219,2M still one of my fav debut song! ",
),
Text(
text: "🔥",
),
Text(
text: "💎",
),
]),
author: Some(ChannelTag(
id: "UCoU9ZmBOV-CB2Mjl_JM3-ZA",

View file

@ -6,12 +6,16 @@ VideoDetails(
id: "ZeerrnuLi5E",
name: "aespa 에스파 \'Black Mamba\' MV",
description: RichText([
Text("🎧Listen and download aespa\'s debut single \"Black Mamba\": "),
Text(
text: "🎧Listen and download aespa\'s debut single \"Black Mamba\": ",
),
Web(
text: "https://smarturl.it/aespa_BlackMamba",
url: "https://smarturl.it/aespa_BlackMamba",
),
Text("\n🐍The Debut Stage "),
Text(
text: "\n🐍The Debut Stage ",
),
YouTube(
text: "aespa 에스파 \'Black ...",
target: Video(
@ -19,57 +23,95 @@ VideoDetails(
start_time: 0,
),
),
Text("\n\n🎟\u{fe0f} aespa Showcase SYNK in LA! Tickets now on sale: "),
Text(
text: "\n\n🎟\u{fe0f} aespa Showcase SYNK in LA! Tickets now on sale: ",
),
Web(
text: "https://www.ticketmaster.com/event/0A...",
url: "https://www.ticketmaster.com/event/0A005CCD9E871F6E",
),
Text("\n\nSubscribe to aespa Official YouTube Channel!\n"),
Text(
text: "\n\nSubscribe to aespa Official YouTube Channel!\n",
),
Web(
text: "https://www.youtube.com/aespa?sub_con...",
url: "https://www.youtube.com/aespa?sub_confirmation=1",
),
Text("\n\naespa official\n"),
Text(
text: "\n\naespa official\n",
),
Web(
text: "aespa",
url: "https://www.youtube.com/c/aespa",
),
Text("\n"),
Text(
text: "\n",
),
Web(
text: "https://www.instagram.com/aespa_official",
url: "https://www.instagram.com/aespa_official",
),
Text("\n"),
Text(
text: "\n",
),
Web(
text: "https://www.tiktok.com/@aespa_official",
url: "https://www.tiktok.com/@aespa_official",
),
Text("\n"),
Text(
text: "\n",
),
Web(
text: "https://twitter.com/aespa_Official",
url: "https://twitter.com/aespa_Official",
),
Text("\n"),
Text(
text: "\n",
),
Web(
text: "https://www.facebook.com/aespa.official",
url: "https://www.facebook.com/aespa.official",
),
Text("\n"),
Text(
text: "\n",
),
Web(
text: "https://weibo.com/aespa",
url: "https://weibo.com/aespa",
),
Text("\n\n"),
Text("#aespa"),
Text(" "),
Text("#æspa"),
Text(" "),
Text("#BlackMamba"),
Text(" "),
Text("#블랙맘바"),
Text(" "),
Text("#에스파"),
Text("\naespa 에스파 \'Black Mamba\' MV ℗ SM Entertainment"),
Text(
text: "\n\n",
),
Text(
text: "#aespa",
),
Text(
text: " ",
),
Text(
text: "#æspa",
),
Text(
text: " ",
),
Text(
text: "#BlackMamba",
),
Text(
text: " ",
),
Text(
text: "#블랙맘바",
),
Text(
text: " ",
),
Text(
text: "#에스파",
),
Text(
text: "\naespa 에스파 \'Black Mamba\' MV ℗ SM Entertainment",
),
]),
channel: ChannelTag(
id: "UCEf_Bc-KVd7onSeifS3py9g",

View file

@ -6,12 +6,16 @@ VideoDetails(
id: "ZeerrnuLi5E",
name: "aespa 에스파 \'Black Mamba\' MV",
description: RichText([
Text("🎧Listen and download aespa\'s debut single \"Black Mamba\": "),
Text(
text: "🎧Listen and download aespa\'s debut single \"Black Mamba\": ",
),
Web(
text: "https://smarturl.it/aespa_BlackMamba",
url: "https://smarturl.it/aespa_BlackMamba",
),
Text("\n🐍The Debut Stage "),
Text(
text: "\n🐍The Debut Stage ",
),
YouTube(
text: "https://youtu.be/Ky5RT5oGg0w",
target: Video(
@ -19,57 +23,95 @@ VideoDetails(
start_time: 0,
),
),
Text("\n\n🎟\u{fe0f} aespa Showcase SYNK in LA! Tickets now on sale: "),
Text(
text: "\n\n🎟\u{fe0f} aespa Showcase SYNK in LA! Tickets now on sale: ",
),
Web(
text: "https://www.ticketmaster.com/event/0A...",
url: "https://www.ticketmaster.com/event/0A005CCD9E871F6E",
),
Text("\n\nSubscribe to aespa Official YouTube Channel!\n"),
Text(
text: "\n\nSubscribe to aespa Official YouTube Channel!\n",
),
Web(
text: "https://www.youtube.com/aespa?sub_con...",
url: "https://www.youtube.com/aespa?sub_confirmation=1",
),
Text("\n\naespa official\n"),
Text(
text: "\n\naespa official\n",
),
Web(
text: "https://www.youtube.com/c/aespa",
url: "https://www.youtube.com/c/aespa",
),
Text("\n"),
Text(
text: "\n",
),
Web(
text: "https://www.instagram.com/aespa_official",
url: "https://www.instagram.com/aespa_official",
),
Text("\n"),
Text(
text: "\n",
),
Web(
text: "https://www.tiktok.com/@aespa_official",
url: "https://www.tiktok.com/@aespa_official",
),
Text("\n"),
Text(
text: "\n",
),
Web(
text: "https://twitter.com/aespa_Official",
url: "https://twitter.com/aespa_Official",
),
Text("\n"),
Text(
text: "\n",
),
Web(
text: "https://www.facebook.com/aespa.official",
url: "https://www.facebook.com/aespa.official",
),
Text("\n"),
Text(
text: "\n",
),
Web(
text: "https://weibo.com/aespa",
url: "https://weibo.com/aespa",
),
Text("\n\n"),
Text("#aespa"),
Text(" "),
Text("#æspa"),
Text(" "),
Text("#BlackMamba"),
Text(" "),
Text("#블랙맘바"),
Text(" "),
Text("#에스파"),
Text("\naespa 에스파 \'Black Mamba\' MV ℗ SM Entertainment"),
Text(
text: "\n\n",
),
Text(
text: "#aespa",
),
Text(
text: " ",
),
Text(
text: "#æspa",
),
Text(
text: " ",
),
Text(
text: "#BlackMamba",
),
Text(
text: " ",
),
Text(
text: "#블랙맘바",
),
Text(
text: " ",
),
Text(
text: "#에스파",
),
Text(
text: "\naespa 에스파 \'Black Mamba\' MV ℗ SM Entertainment",
),
]),
channel: ChannelTag(
id: "UCEf_Bc-KVd7onSeifS3py9g",

View file

@ -6,96 +6,140 @@ VideoDetails(
id: "nFDBxBUfE74",
name: "The Prepper PC",
description: RichText([
Text("Thanks to Jackery for sponsoring today\'s video! Check out Jackery\'s Solar Generator 2000 Pro and get 10% off with code LinusTechTips at "),
Text(
text: "Thanks to Jackery for sponsoring today\'s video! Check out Jackery\'s Solar Generator 2000 Pro and get 10% off with code LinusTechTips at ",
),
Web(
text: "https://lmg.gg/SG2000PROLTT",
url: "https://lmg.gg/SG2000PROLTT",
),
Text("\n\nThese days, you can game almost anywhere on the planet, anytime. But what if that planet was in the middle of an apocalypse? After youve stashed years of food, water, and toilet paper away, how will you pass the time? With this PC, you can be prepared to game until the whole mess to sort itself out. \n\nDiscuss on the forum: "),
Text(
text: "\n\nThese days, you can game almost anywhere on the planet, anytime. But what if that planet was in the middle of an apocalypse? After youve stashed years of food, water, and toilet paper away, how will you pass the time? With this PC, you can be prepared to game until the whole mess to sort itself out. \n\nDiscuss on the forum: ",
),
Web(
text: "https://linustechtips.com/topic/14554...",
url: "https://linustechtips.com/topic/1455447-the-prepper-pc-sponsored/",
),
Text("\n\nBuy a Jackery Solar Generator 2000 Pro: "),
Text(
text: "\n\nBuy a Jackery Solar Generator 2000 Pro: ",
),
Web(
text: "https://geni.us/034L",
url: "https://geni.us/034L",
),
Text("\n\nBuy a Jackery Explorer 2000 Pro: "),
Text(
text: "\n\nBuy a Jackery Explorer 2000 Pro: ",
),
Web(
text: "https://lmg.gg/1dyF4",
url: "https://lmg.gg/1dyF4",
),
Text("\n\nBuy a Seasonic Fanless TX: "),
Text(
text: "\n\nBuy a Seasonic Fanless TX: ",
),
Web(
text: "https://geni.us/S0Wt76G",
url: "https://geni.us/S0Wt76G",
),
Text("\n\nBuy an Intel Core i3 (12th Gen) i3-12100: "),
Text(
text: "\n\nBuy an Intel Core i3 (12th Gen) i3-12100: ",
),
Web(
text: "https://geni.us/hLZvxa",
url: "https://geni.us/hLZvxa",
),
Text("\n\nBuy an RTX 3050: "),
Text(
text: "\n\nBuy an RTX 3050: ",
),
Web(
text: "https://geni.us/6A6hl",
url: "https://geni.us/6A6hl",
),
Text("\n\nBuy an RX 6500XT: "),
Text(
text: "\n\nBuy an RX 6500XT: ",
),
Web(
text: "https://geni.us/fUF1p",
url: "https://geni.us/fUF1p",
),
Text("\n\nPurchases made through some store links may provide some compensation to Linus Media Group.\n\n► GET MERCH: "),
Text(
text: "\n\nPurchases made through some store links may provide some compensation to Linus Media Group.\n\n► GET MERCH: ",
),
Web(
text: "https://lttstore.com",
url: "https://lttstore.com/",
),
Text("\n► SUPPORT US ON FLOATPLANE: "),
Text(
text: "\n► SUPPORT US ON FLOATPLANE: ",
),
Web(
text: "https://www.floatplane.com/ltt",
url: "https://www.floatplane.com/ltt",
),
Text("\n► AFFILIATES, SPONSORS & REFERRALS: "),
Text(
text: "\n► AFFILIATES, SPONSORS & REFERRALS: ",
),
Web(
text: "https://lmg.gg/sponsors",
url: "https://lmg.gg/sponsors",
),
Text("\n► PODCAST GEAR: "),
Text(
text: "\n► PODCAST GEAR: ",
),
Web(
text: "https://lmg.gg/podcastgear",
url: "https://lmg.gg/podcastgear",
),
Text("\n\n\nFOLLOW US \n"),
Text("-------------------------------------------------"),
Text(" \nTwitter: "),
Text(
text: "\n\n\nFOLLOW US \n",
),
Text(
text: "-------------------------------------------------",
),
Text(
text: " \nTwitter: ",
),
Web(
text: "https://twitter.com/linustech",
url: "https://twitter.com/linustech",
),
Text("\nFacebook: "),
Text(
text: "\nFacebook: ",
),
Web(
text: "http://www.facebook.com/LinusTech",
url: "http://www.facebook.com/LinusTech",
),
Text("\nInstagram: "),
Text(
text: "\nInstagram: ",
),
Web(
text: "https://www.instagram.com/linustech",
url: "https://www.instagram.com/linustech",
),
Text("\nTikTok: "),
Text(
text: "\nTikTok: ",
),
Web(
text: "https://www.tiktok.com/@linustech",
url: "https://www.tiktok.com/@linustech",
),
Text("\nTwitch: "),
Text(
text: "\nTwitch: ",
),
Web(
text: "https://www.twitch.tv/linustech",
url: "https://www.twitch.tv/linustech",
),
Text("\n\nMUSIC CREDIT\n"),
Text("-------------------------------------------------"),
Text("\nIntro: Laszlo - Supernova\nVideo Link: "),
Text(
text: "\n\nMUSIC CREDIT\n",
),
Text(
text: "-------------------------------------------------",
),
Text(
text: "\nIntro: Laszlo - Supernova\nVideo Link: ",
),
YouTube(
text: "https://www.youtube.com/watch?v=PKfxm...",
target: Video(
@ -103,17 +147,23 @@ VideoDetails(
start_time: 0,
),
),
Text("\niTunes Download Link: "),
Text(
text: "\niTunes Download Link: ",
),
Web(
text: "https://itunes.apple.com/us/album/sup...",
url: "https://itunes.apple.com/us/album/supernova/id936805712",
),
Text("\nArtist Link: "),
Text(
text: "\nArtist Link: ",
),
Web(
text: "https://soundcloud.com/laszlomusic",
url: "https://soundcloud.com/laszlomusic",
),
Text("\n\nOutro: Approaching Nirvana - Sugar High\nVideo Link: "),
Text(
text: "\n\nOutro: Approaching Nirvana - Sugar High\nVideo Link: ",
),
YouTube(
text: "https://www.youtube.com/watch?v=ngsGB...",
target: Video(
@ -121,39 +171,57 @@ VideoDetails(
start_time: 0,
),
),
Text("\nListen on Spotify: "),
Text(
text: "\nListen on Spotify: ",
),
Web(
text: "http://spoti.fi/UxWkUw",
url: "http://spoti.fi/UxWkUw",
),
Text("\nArtist Link: "),
Text(
text: "\nArtist Link: ",
),
Web(
text: "http://www.youtube.com/approachingnir...",
url: "http://www.youtube.com/approachingnirvana",
),
Text("\n\nIntro animation by MBarek Abdelwassaa "),
Text(
text: "\n\nIntro animation by MBarek Abdelwassaa ",
),
Web(
text: "https://www.instagram.com/mbarek_abdel/",
url: "https://www.instagram.com/mbarek_abdel/",
),
Text("\nMonitor And Keyboard by vadimmihalkevich / CC BY 4.0 "),
Text(
text: "\nMonitor And Keyboard by vadimmihalkevich / CC BY 4.0 ",
),
Web(
text: "https://geni.us/PgGWp",
url: "https://geni.us/PgGWp",
),
Text("\nMechanical RGB Keyboard by BigBrotherECE / CC BY 4.0 "),
Text(
text: "\nMechanical RGB Keyboard by BigBrotherECE / CC BY 4.0 ",
),
Web(
text: "https://geni.us/mj6pHk4",
url: "https://geni.us/mj6pHk4",
),
Text("\nMouse Gamer free Model By Oscar Creativo / CC BY 4.0 "),
Text(
text: "\nMouse Gamer free Model By Oscar Creativo / CC BY 4.0 ",
),
Web(
text: "https://geni.us/Ps3XfE",
url: "https://geni.us/Ps3XfE",
),
Text("\n\nCHAPTERS\n"),
Text("-------------------------------------------------"),
Text("\n"),
Text(
text: "\n\nCHAPTERS\n",
),
Text(
text: "-------------------------------------------------",
),
Text(
text: "\n",
),
YouTube(
text: "0:00",
target: Video(
@ -161,7 +229,9 @@ VideoDetails(
start_time: 0,
),
),
Text(" Intro\n"),
Text(
text: " Intro\n",
),
YouTube(
text: "0:42",
target: Video(
@ -169,7 +239,9 @@ VideoDetails(
start_time: 42,
),
),
Text(" The PC Built for Super Efficiency\n"),
Text(
text: " The PC Built for Super Efficiency\n",
),
YouTube(
text: "2:41",
target: Video(
@ -177,7 +249,9 @@ VideoDetails(
start_time: 161,
),
),
Text(" Our BURIAL ENCLOSURE?!\n"),
Text(
text: " Our BURIAL ENCLOSURE?!\n",
),
YouTube(
text: "3:31",
target: Video(
@ -185,7 +259,9 @@ VideoDetails(
start_time: 211,
),
),
Text(" Our Power Solution (Thanks Jackery!)\n"),
Text(
text: " Our Power Solution (Thanks Jackery!)\n",
),
YouTube(
text: "4:47",
target: Video(
@ -193,7 +269,9 @@ VideoDetails(
start_time: 287,
),
),
Text(" Diggin\' Holes\n"),
Text(
text: " Diggin\' Holes\n",
),
YouTube(
text: "5:30",
target: Video(
@ -201,7 +279,9 @@ VideoDetails(
start_time: 330,
),
),
Text(" Colonoscopy?\n"),
Text(
text: " Colonoscopy?\n",
),
YouTube(
text: "7:04",
target: Video(
@ -209,7 +289,9 @@ VideoDetails(
start_time: 424,
),
),
Text(" Diggin\' like a man\n"),
Text(
text: " Diggin\' like a man\n",
),
YouTube(
text: "8:29",
target: Video(
@ -217,7 +299,9 @@ VideoDetails(
start_time: 509,
),
),
Text(" The world\'s worst woodsman\n"),
Text(
text: " The world\'s worst woodsman\n",
),
YouTube(
text: "9:03",
target: Video(
@ -225,7 +309,9 @@ VideoDetails(
start_time: 543,
),
),
Text(" Backyard cable management\n"),
Text(
text: " Backyard cable management\n",
),
YouTube(
text: "10:02",
target: Video(
@ -233,7 +319,9 @@ VideoDetails(
start_time: 602,
),
),
Text(" Time to bury this boy\n"),
Text(
text: " Time to bury this boy\n",
),
YouTube(
text: "10:46",
target: Video(
@ -241,7 +329,9 @@ VideoDetails(
start_time: 646,
),
),
Text(" Solar Power Generation\n"),
Text(
text: " Solar Power Generation\n",
),
YouTube(
text: "11:37",
target: Video(
@ -249,7 +339,9 @@ VideoDetails(
start_time: 697,
),
),
Text(" Issues\n"),
Text(
text: " Issues\n",
),
YouTube(
text: "12:08",
target: Video(
@ -257,7 +349,9 @@ VideoDetails(
start_time: 728,
),
),
Text(" First Play Test\n"),
Text(
text: " First Play Test\n",
),
YouTube(
text: "13:20",
target: Video(
@ -265,7 +359,9 @@ VideoDetails(
start_time: 800,
),
),
Text(" Conclusion"),
Text(
text: " Conclusion",
),
]),
channel: ChannelTag(
id: "UCXuqSBlHAE6Xw-yeJA0Tunw",

View file

@ -10,13 +10,19 @@ VideoDetails(
text: "https://media.ccc.de/v/36c3-10652-bah...",
url: "https://media.ccc.de/v/36c3-10652-bahnmining_-_punktlichkeit_ist_eine_zier",
),
Text("\n\n\n\nSeit Anfang 2019 hat David jeden einzelnen Halt jeder einzelnen Zugfahrt auf jedem einzelnen Fernbahnhof in ganz Deutschland systematisch gespeichert. Inklusive Verspätungen und allem drum und dran. Und die werden wir in einem bunten Vortrag erforschen und endlich mal wieder ein bisschen Spaß mit Daten haben.\n\nRechtlicher Hinweis: Es liegt eine schriftliche Genehmigung der Bahn vor, von ihr abgerufene Rohdaten aggregieren und für Vorträge nutzen zu dürfen. Inhaltliche Absprachen oder gar Auflagen existieren nicht.\n\nDie Bahn gibt ihre Verspätungen in \"Prozent pünktlicher Züge pro Monat\" an. Das ist so radikal zusammengefasst, dass man daraus natürlich nichts interessantes lesen kann. Jetzt stellt euch mal vor, man könnte da mal ein bisschen genauer reingucken.\n\nStellt sich raus: Das geht! Davids Datensatz umfasst knapp 25 Millionen Halte - mehr als 50.000 pro Tag. Wir haben die Rohdaten und sind in unserer Betrachtung völlig frei. \n\nDer Vortrag hat wieder mehrere rote Fäden.\n\n 1) Wir vermessen ein fast komplettes Fernverkehrsjahr der deutschen Bahn. Hier etwas Erwartungsmanagement: Sinn ist keinesfalls Bahn-Bashing oder Sensationsheischerei - wer einen Hassvortrag gegen die Bahn erwartet, ist in dieser Veranstaltung falsch. Wir werden die Daten aber nutzen, um die Bahn einmal ein bisschen kennenzulernen. Die Bahn ist eine riesige Maschine mit Millionen beweglicher Teile. Wie viele Zugfahrten gibt es überhaupt? Was sind die größten Bahnhöfe? Wir werden natürlich auch die unerfreulichen Themen ansprechen, für die sich im Moment viele interessieren: Ist das Problem mit den Zugverspätungen wirklich so schlimm, wie alle sagen? Gibt es Orte und Zeiten, an denen es besonders hapert? Und wo fallen Züge einfach aus?\n\n 2) Es gibt wieder mehrere Blicke über den Tellerrand, wie bei Davids vorherigen Vorträgen auch. Ihr werdet wieder ganz automatisch und nebenher einen allgemeinverständlichen Einblick in die heutige Datenauswerterei bekommen. (Eine verbreitete Verschwörungstheorie sagt, euch zur Auswertung öffentlicher Daten zu inspirieren, wäre sogar der Hauptzweck von Davids Vorträgen. :-) )Die Welt braucht Leute mit Ratio, die Analyse wichtiger als Kreischerei finden. Und darum beschreibt David auch, wie man so ein durchaus aufwändiges Hobbyprojekt technisch angeht, Anfängerfehler vermeidet, und verantwortungsvoll handelt.\n\nDavid Kriesel\n\n"),
Text(
text: "\n\n\n\nSeit Anfang 2019 hat David jeden einzelnen Halt jeder einzelnen Zugfahrt auf jedem einzelnen Fernbahnhof in ganz Deutschland systematisch gespeichert. Inklusive Verspätungen und allem drum und dran. Und die werden wir in einem bunten Vortrag erforschen und endlich mal wieder ein bisschen Spaß mit Daten haben.\n\nRechtlicher Hinweis: Es liegt eine schriftliche Genehmigung der Bahn vor, von ihr abgerufene Rohdaten aggregieren und für Vorträge nutzen zu dürfen. Inhaltliche Absprachen oder gar Auflagen existieren nicht.\n\nDie Bahn gibt ihre Verspätungen in \"Prozent pünktlicher Züge pro Monat\" an. Das ist so radikal zusammengefasst, dass man daraus natürlich nichts interessantes lesen kann. Jetzt stellt euch mal vor, man könnte da mal ein bisschen genauer reingucken.\n\nStellt sich raus: Das geht! Davids Datensatz umfasst knapp 25 Millionen Halte - mehr als 50.000 pro Tag. Wir haben die Rohdaten und sind in unserer Betrachtung völlig frei. \n\nDer Vortrag hat wieder mehrere rote Fäden.\n\n 1) Wir vermessen ein fast komplettes Fernverkehrsjahr der deutschen Bahn. Hier etwas Erwartungsmanagement: Sinn ist keinesfalls Bahn-Bashing oder Sensationsheischerei - wer einen Hassvortrag gegen die Bahn erwartet, ist in dieser Veranstaltung falsch. Wir werden die Daten aber nutzen, um die Bahn einmal ein bisschen kennenzulernen. Die Bahn ist eine riesige Maschine mit Millionen beweglicher Teile. Wie viele Zugfahrten gibt es überhaupt? Was sind die größten Bahnhöfe? Wir werden natürlich auch die unerfreulichen Themen ansprechen, für die sich im Moment viele interessieren: Ist das Problem mit den Zugverspätungen wirklich so schlimm, wie alle sagen? Gibt es Orte und Zeiten, an denen es besonders hapert? Und wo fallen Züge einfach aus?\n\n 2) Es gibt wieder mehrere Blicke über den Tellerrand, wie bei Davids vorherigen Vorträgen auch. Ihr werdet wieder ganz automatisch und nebenher einen allgemeinverständlichen Einblick in die heutige Datenauswerterei bekommen. (Eine verbreitete Verschwörungstheorie sagt, euch zur Auswertung öffentlicher Daten zu inspirieren, wäre sogar der Hauptzweck von Davids Vorträgen. :-) )Die Welt braucht Leute mit Ratio, die Analyse wichtiger als Kreischerei finden. Und darum beschreibt David auch, wie man so ein durchaus aufwändiges Hobbyprojekt technisch angeht, Anfängerfehler vermeidet, und verantwortungsvoll handelt.\n\nDavid Kriesel\n\n",
),
Web(
text: "https://fahrplan.events.ccc.de/congre...",
url: "https://fahrplan.events.ccc.de/congress/2019/Fahrplan/events/10652.html",
),
Text("\n\n"),
Text("#36C3"),
Text(
text: "\n\n",
),
Text(
text: "#36C3",
),
]),
channel: ChannelTag(
id: "UC2TXq_t06Hjdr2g_KdKpHQg",

View file

@ -6,92 +6,128 @@ VideoDetails(
id: "nFDBxBUfE74",
name: "The Prepper PC",
description: RichText([
Text("Thanks to Jackery for sponsoring today\'s video! Check out Jackery\'s Solar Generator 2000 Pro and get 10% off with code LinusTechTips at "),
Text(
text: "Thanks to Jackery for sponsoring today\'s video! Check out Jackery\'s Solar Generator 2000 Pro and get 10% off with code LinusTechTips at ",
),
Web(
text: "https://lmg.gg/SG2000PROLTT",
url: "https://lmg.gg/SG2000PROLTT",
),
Text("\n\nThese days, you can game almost anywhere on the planet, anytime. But what if that planet was in the middle of an apocalypse? After youve stashed years of food, water, and toilet paper away, how will you pass the time? With this PC, you can be prepared to game until the whole mess to sort itself out. \n\nDiscuss on the forum: "),
Text(
text: "\n\nThese days, you can game almost anywhere on the planet, anytime. But what if that planet was in the middle of an apocalypse? After youve stashed years of food, water, and toilet paper away, how will you pass the time? With this PC, you can be prepared to game until the whole mess to sort itself out. \n\nDiscuss on the forum: ",
),
Web(
text: "https://linustechtips.com/topic/14554...",
url: "https://linustechtips.com/topic/1455447-the-prepper-pc-sponsored/",
),
Text("\n\nBuy a Jackery Solar Generator 2000 Pro: "),
Text(
text: "\n\nBuy a Jackery Solar Generator 2000 Pro: ",
),
Web(
text: "https://geni.us/034L",
url: "https://geni.us/034L",
),
Text("\n\nBuy a Jackery Explorer 2000 Pro: "),
Text(
text: "\n\nBuy a Jackery Explorer 2000 Pro: ",
),
Web(
text: "https://lmg.gg/1dyF4",
url: "https://lmg.gg/1dyF4",
),
Text("\n\nBuy a Seasonic Fanless TX: "),
Text(
text: "\n\nBuy a Seasonic Fanless TX: ",
),
Web(
text: "https://geni.us/S0Wt76G",
url: "https://geni.us/S0Wt76G",
),
Text("\n\nBuy an Intel Core i3 (12th Gen) i3-12100: "),
Text(
text: "\n\nBuy an Intel Core i3 (12th Gen) i3-12100: ",
),
Web(
text: "https://geni.us/hLZvxa",
url: "https://geni.us/hLZvxa",
),
Text("\n\nBuy an RTX 3050: "),
Text(
text: "\n\nBuy an RTX 3050: ",
),
Web(
text: "https://geni.us/6A6hl",
url: "https://geni.us/6A6hl",
),
Text("\n\nBuy an RX 6500XT: "),
Text(
text: "\n\nBuy an RX 6500XT: ",
),
Web(
text: "https://geni.us/fUF1p",
url: "https://geni.us/fUF1p",
),
Text("\n\nPurchases made through some store links may provide some compensation to Linus Media Group.\n\n► GET MERCH: "),
Text(
text: "\n\nPurchases made through some store links may provide some compensation to Linus Media Group.\n\n► GET MERCH: ",
),
Web(
text: "https://lttstore.com",
url: "https://lttstore.com/",
),
Text("\n► SUPPORT US ON FLOATPLANE: "),
Text(
text: "\n► SUPPORT US ON FLOATPLANE: ",
),
Web(
text: "https://www.floatplane.com/ltt",
url: "https://www.floatplane.com/ltt",
),
Text("\n► AFFILIATES, SPONSORS & REFERRALS: "),
Text(
text: "\n► AFFILIATES, SPONSORS & REFERRALS: ",
),
Web(
text: "https://lmg.gg/sponsors",
url: "https://lmg.gg/sponsors",
),
Text("\n► PODCAST GEAR: "),
Text(
text: "\n► PODCAST GEAR: ",
),
Web(
text: "https://lmg.gg/podcastgear",
url: "https://lmg.gg/podcastgear",
),
Text("\n\n\nFOLLOW US \n--------------------------------------------------- \nTwitter: "),
Text(
text: "\n\n\nFOLLOW US \n--------------------------------------------------- \nTwitter: ",
),
Web(
text: "https://twitter.com/linustech",
url: "https://twitter.com/linustech",
),
Text("\nFacebook: "),
Text(
text: "\nFacebook: ",
),
Web(
text: "http://www.facebook.com/LinusTech",
url: "http://www.facebook.com/LinusTech",
),
Text("\nInstagram: "),
Text(
text: "\nInstagram: ",
),
Web(
text: "https://www.instagram.com/linustech",
url: "https://www.instagram.com/linustech",
),
Text("\nTikTok: "),
Text(
text: "\nTikTok: ",
),
Web(
text: "https://www.tiktok.com/@linustech",
url: "https://www.tiktok.com/@linustech",
),
Text("\nTwitch: "),
Text(
text: "\nTwitch: ",
),
Web(
text: "https://www.twitch.tv/linustech",
url: "https://www.twitch.tv/linustech",
),
Text("\n\nMUSIC CREDIT\n---------------------------------------------------\nIntro: Laszlo - Supernova\nVideo Link: "),
Text(
text: "\n\nMUSIC CREDIT\n---------------------------------------------------\nIntro: Laszlo - Supernova\nVideo Link: ",
),
YouTube(
text: "https://www.youtube.com/watch?v=PKfxm...",
target: Video(
@ -99,17 +135,23 @@ VideoDetails(
start_time: 0,
),
),
Text("\niTunes Download Link: "),
Text(
text: "\niTunes Download Link: ",
),
Web(
text: "https://itunes.apple.com/us/album/sup...",
url: "https://itunes.apple.com/us/album/supernova/id936805712",
),
Text("\nArtist Link: "),
Text(
text: "\nArtist Link: ",
),
Web(
text: "https://soundcloud.com/laszlomusic",
url: "https://soundcloud.com/laszlomusic",
),
Text("\n\nOutro: Approaching Nirvana - Sugar High\nVideo Link: "),
Text(
text: "\n\nOutro: Approaching Nirvana - Sugar High\nVideo Link: ",
),
YouTube(
text: "https://www.youtube.com/watch?v=ngsGB...",
target: Video(
@ -117,37 +159,51 @@ VideoDetails(
start_time: 0,
),
),
Text("\nListen on Spotify: "),
Text(
text: "\nListen on Spotify: ",
),
Web(
text: "http://spoti.fi/UxWkUw",
url: "http://spoti.fi/UxWkUw",
),
Text("\nArtist Link: "),
Text(
text: "\nArtist Link: ",
),
Web(
text: "http://www.youtube.com/approachingnir...",
url: "http://www.youtube.com/approachingnirvana",
),
Text("\n\nIntro animation by MBarek Abdelwassaa "),
Text(
text: "\n\nIntro animation by MBarek Abdelwassaa ",
),
Web(
text: "https://www.instagram.com/mbarek_abdel/",
url: "https://www.instagram.com/mbarek_abdel/",
),
Text("\nMonitor And Keyboard by vadimmihalkevich / CC BY 4.0 "),
Text(
text: "\nMonitor And Keyboard by vadimmihalkevich / CC BY 4.0 ",
),
Web(
text: "https://geni.us/PgGWp",
url: "https://geni.us/PgGWp",
),
Text("\nMechanical RGB Keyboard by BigBrotherECE / CC BY 4.0 "),
Text(
text: "\nMechanical RGB Keyboard by BigBrotherECE / CC BY 4.0 ",
),
Web(
text: "https://geni.us/mj6pHk4",
url: "https://geni.us/mj6pHk4",
),
Text("\nMouse Gamer free Model By Oscar Creativo / CC BY 4.0 "),
Text(
text: "\nMouse Gamer free Model By Oscar Creativo / CC BY 4.0 ",
),
Web(
text: "https://geni.us/Ps3XfE",
url: "https://geni.us/Ps3XfE",
),
Text("\n\nCHAPTERS\n---------------------------------------------------\n"),
Text(
text: "\n\nCHAPTERS\n---------------------------------------------------\n",
),
YouTube(
text: "0:00",
target: Video(
@ -155,7 +211,9 @@ VideoDetails(
start_time: 0,
),
),
Text(" Intro\n"),
Text(
text: " Intro\n",
),
YouTube(
text: "0:42",
target: Video(
@ -163,7 +221,9 @@ VideoDetails(
start_time: 42,
),
),
Text(" The PC Built for Super Efficiency\n"),
Text(
text: " The PC Built for Super Efficiency\n",
),
YouTube(
text: "2:41",
target: Video(
@ -171,7 +231,9 @@ VideoDetails(
start_time: 161,
),
),
Text(" Our BURIAL ENCLOSURE?!\n"),
Text(
text: " Our BURIAL ENCLOSURE?!\n",
),
YouTube(
text: "3:31",
target: Video(
@ -179,7 +241,9 @@ VideoDetails(
start_time: 211,
),
),
Text(" Our Power Solution (Thanks Jackery!)\n"),
Text(
text: " Our Power Solution (Thanks Jackery!)\n",
),
YouTube(
text: "4:47",
target: Video(
@ -187,7 +251,9 @@ VideoDetails(
start_time: 287,
),
),
Text(" Diggin\' Holes\n"),
Text(
text: " Diggin\' Holes\n",
),
YouTube(
text: "5:30",
target: Video(
@ -195,7 +261,9 @@ VideoDetails(
start_time: 330,
),
),
Text(" Colonoscopy?\n"),
Text(
text: " Colonoscopy?\n",
),
YouTube(
text: "7:04",
target: Video(
@ -203,7 +271,9 @@ VideoDetails(
start_time: 424,
),
),
Text(" Diggin\' like a man\n"),
Text(
text: " Diggin\' like a man\n",
),
YouTube(
text: "8:29",
target: Video(
@ -211,7 +281,9 @@ VideoDetails(
start_time: 509,
),
),
Text(" The world\'s worst woodsman\n"),
Text(
text: " The world\'s worst woodsman\n",
),
YouTube(
text: "9:03",
target: Video(
@ -219,7 +291,9 @@ VideoDetails(
start_time: 543,
),
),
Text(" Backyard cable management\n"),
Text(
text: " Backyard cable management\n",
),
YouTube(
text: "10:02",
target: Video(
@ -227,7 +301,9 @@ VideoDetails(
start_time: 602,
),
),
Text(" Time to bury this boy\n"),
Text(
text: " Time to bury this boy\n",
),
YouTube(
text: "10:46",
target: Video(
@ -235,7 +311,9 @@ VideoDetails(
start_time: 646,
),
),
Text(" Solar Power Generation\n"),
Text(
text: " Solar Power Generation\n",
),
YouTube(
text: "11:37",
target: Video(
@ -243,7 +321,9 @@ VideoDetails(
start_time: 697,
),
),
Text(" Issues\n"),
Text(
text: " Issues\n",
),
YouTube(
text: "12:08",
target: Video(
@ -251,7 +331,9 @@ VideoDetails(
start_time: 728,
),
),
Text(" First Play Test\n"),
Text(
text: " First Play Test\n",
),
YouTube(
text: "13:20",
target: Video(
@ -259,7 +341,9 @@ VideoDetails(
start_time: 800,
),
),
Text(" Conclusion"),
Text(
text: " Conclusion",
),
]),
channel: ChannelTag(
id: "UCXuqSBlHAE6Xw-yeJA0Tunw",

View file

@ -6,7 +6,9 @@ VideoDetails(
id: "86YLFOog4GM",
name: "🌎 Nasa Live Stream - Earth From Space : Live Views from the ISS",
description: RichText([
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"),
Text(
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",
),
YouTube(
text: "https://www.youtube.com/watch?v=SEzK4...",
target: Video(
@ -14,27 +16,53 @@ VideoDetails(
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("#nasalive"),
Text(" "),
Text("#isslive"),
Text(" "),
Text("#spacelive"),
Text(" "),
Text("#earthlive"),
Text(" "),
Text("#earthfromspace"),
Text("\n\nThe Expedition 67 Crew are: \n Sergey Korsakov\nOleg Artemyev\nDenis Matveev\nKjell Lindgren\nRobert Hines\nJessica Watkins\nSamantha Cristoforetti\n\nYulia Peresild\nKlim Shipenko - onboard as part of a film.\n\nTHIS WILL SHOW LIVE and PRE-RECORDED FOOTAGE - depending on signal from the station or if the ISS is on the night side of Earth.\n\nWhen the feed is live the words LIVE NOW will appear in the top left hand corner of the screen.\nAs the Space Station passes into a period of night every 45 mins video is unavailable - during this time, and other breaks in transmission, recorded footage is shown .\nWhen back in daylight the live stream of earth will recommence\n\nIf you are here to talk about a flat earth then please don\'t bother. You can stay and watch our beautiful globe earth as it spins in space , but please don\'t share your nonsense beliefs in our chat.\n\nGot a question about this feed? Read our FAQ\'s\n"),
Text(
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: "#nasalive",
),
Text(
text: " ",
),
Text(
text: "#isslive",
),
Text(
text: " ",
),
Text(
text: "#spacelive",
),
Text(
text: " ",
),
Text(
text: "#earthlive",
),
Text(
text: " ",
),
Text(
text: "#earthfromspace",
),
Text(
text: "\n\nThe Expedition 67 Crew are: \n Sergey Korsakov\nOleg Artemyev\nDenis Matveev\nKjell Lindgren\nRobert Hines\nJessica Watkins\nSamantha Cristoforetti\n\nYulia Peresild\nKlim Shipenko - onboard as part of a film.\n\nTHIS WILL SHOW LIVE and PRE-RECORDED FOOTAGE - depending on signal from the station or if the ISS is on the night side of Earth.\n\nWhen the feed is live the words LIVE NOW will appear in the top left hand corner of the screen.\nAs the Space Station passes into a period of night every 45 mins video is unavailable - during this time, and other breaks in transmission, recorded footage is shown .\nWhen back in daylight the live stream of earth will recommence\n\nIf you are here to talk about a flat earth then please don\'t bother. You can stay and watch our beautiful globe earth as it spins in space , but please don\'t share your nonsense beliefs in our chat.\n\nGot a question about this feed? Read our FAQ\'s\n",
),
Web(
text: "https://spacevideosfaq.tumblr.com/",
url: "https://spacevideosfaq.tumblr.com/",
),
Text("\n\nWatch the earth roll by courtesy of the NASA Live cameras\nInternational Space Station Live Feed: Thanks to NASA for this\n"),
Text(
text: "\n\nWatch the earth roll by courtesy of the NASA Live cameras\nInternational Space Station Live Feed: Thanks to NASA for this\n",
),
Web(
text: "http://www.nasa.gov",
url: "http://www.nasa.gov/",
),
Text(" The ISS passes into the dark side of the earth for roughly half of each of its 90 minute orbits. During this time no video is available.\n\nMusic by Kevin Macleod \n"),
Text(
text: " The ISS passes into the dark side of the earth for roughly half of each of its 90 minute orbits. During this time no video is available.\n\nMusic by Kevin Macleod \n",
),
Web(
text: "http://incompetech.com/music/royalty-...",
url: "http://incompetech.com/music/royalty-free/",

View file

@ -6,7 +6,9 @@ VideoDetails(
id: "XuM2onMGvTI",
name: "Gäa",
description: RichText([
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."),
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",

View file

@ -6,12 +6,16 @@ VideoDetails(
id: "ZeerrnuLi5E",
name: "aespa 에스파 \'Black Mamba\' MV",
description: RichText([
Text("🎧Listen and download aespa\'s debut single \"Black Mamba\": "),
Text(
text: "🎧Listen and download aespa\'s debut single \"Black Mamba\": ",
),
Web(
text: "https://smarturl.it/aespa_BlackMamba",
url: "https://smarturl.it/aespa_BlackMamba",
),
Text("\n🐍The Debut Stage "),
Text(
text: "\n🐍The Debut Stage ",
),
YouTube(
text: "https://youtu.be/Ky5RT5oGg0w",
target: Video(
@ -19,57 +23,95 @@ VideoDetails(
start_time: 0,
),
),
Text("\n\n🎟\u{fe0f} aespa Showcase SYNK in LA! Tickets now on sale: "),
Text(
text: "\n\n🎟\u{fe0f} aespa Showcase SYNK in LA! Tickets now on sale: ",
),
Web(
text: "https://www.ticketmaster.com/event/0A...",
url: "https://www.ticketmaster.com/event/0A005CCD9E871F6E",
),
Text("\n\nSubscribe to aespa Official YouTube Channel!\n"),
Text(
text: "\n\nSubscribe to aespa Official YouTube Channel!\n",
),
Web(
text: "https://www.youtube.com/aespa?sub_con...",
url: "https://www.youtube.com/aespa?sub_confirmation=1",
),
Text("\n\naespa official\n"),
Text(
text: "\n\naespa official\n",
),
Web(
text: "https://www.youtube.com/c/aespa",
url: "https://www.youtube.com/c/aespa",
),
Text("\n"),
Text(
text: "\n",
),
Web(
text: "https://www.instagram.com/aespa_official",
url: "https://www.instagram.com/aespa_official",
),
Text("\n"),
Text(
text: "\n",
),
Web(
text: "https://www.tiktok.com/@aespa_official",
url: "https://www.tiktok.com/@aespa_official",
),
Text("\n"),
Text(
text: "\n",
),
Web(
text: "https://twitter.com/aespa_Official",
url: "https://twitter.com/aespa_Official",
),
Text("\n"),
Text(
text: "\n",
),
Web(
text: "https://www.facebook.com/aespa.official",
url: "https://www.facebook.com/aespa.official",
),
Text("\n"),
Text(
text: "\n",
),
Web(
text: "https://weibo.com/aespa",
url: "https://weibo.com/aespa",
),
Text("\n\n"),
Text("#aespa"),
Text(" "),
Text("#æspa"),
Text(" "),
Text("#BlackMamba"),
Text(" "),
Text("#블랙맘바"),
Text(" "),
Text("#에스파"),
Text("\naespa 에스파 \'Black Mamba\' MV ℗ SM Entertainment"),
Text(
text: "\n\n",
),
Text(
text: "#aespa",
),
Text(
text: " ",
),
Text(
text: "#æspa",
),
Text(
text: " ",
),
Text(
text: "#BlackMamba",
),
Text(
text: " ",
),
Text(
text: "#블랙맘바",
),
Text(
text: " ",
),
Text(
text: "#에스파",
),
Text(
text: "\naespa 에스파 \'Black Mamba\' MV ℗ SM Entertainment",
),
]),
channel: ChannelTag(
id: "UCEf_Bc-KVd7onSeifS3py9g",

View file

@ -1,7 +1,7 @@
use super::{
AlbumItem, ArtistId, ArtistItem, Channel, ChannelId, ChannelItem, ChannelRssVideo, ChannelTag,
MusicArtist, MusicItem, MusicPlaylistItem, PlaylistItem, PlaylistVideo, TrackItem, VideoId,
VideoItem, YouTubeItem,
MusicArtist, MusicItem, MusicPlaylistItem, PlaylistItem, TrackItem, VideoId, VideoItem,
YouTubeItem,
};
/// Trait for casting generic YouTube/YouTube music items to a specific kind.
@ -159,15 +159,6 @@ impl From<VideoItem> for VideoId {
}
}
impl From<PlaylistVideo> for VideoId {
fn from(video: PlaylistVideo) -> Self {
Self {
id: video.id,
name: video.name,
}
}
}
impl From<ChannelRssVideo> for VideoId {
fn from(video: ChannelRssVideo) -> Self {
Self {

View file

@ -510,7 +510,7 @@ pub struct Playlist {
/// Playlist name
pub name: String,
/// Playlist videos
pub videos: Paginator<PlaylistVideo>,
pub videos: Paginator<VideoItem>,
/// Number of videos in the playlist
pub video_count: u64,
/// Playlist thumbnail
@ -528,22 +528,6 @@ pub struct Playlist {
pub visitor_data: Option<String>,
}
/// YouTube video extracted from a playlist
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[non_exhaustive]
pub struct PlaylistVideo {
/// Unique YouTube video ID
pub id: String,
/// Video title
pub name: String,
/// Video length in seconds
pub length: u32,
/// Video thumbnail
pub thumbnail: Vec<Thumbnail>,
/// Channel of the video
pub channel: ChannelId,
}
/// Channel identifier
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[non_exhaustive]

View file

@ -16,7 +16,10 @@ pub struct RichText(pub Vec<TextComponent>);
#[non_exhaustive]
pub enum TextComponent {
/// Plain text
Text(String),
Text {
/// Plain text
text: String,
},
/// Web link
Web {
/// Link text
@ -73,7 +76,7 @@ impl TextComponent {
/// Get the text from the component
pub fn get_text(&self) -> &str {
match self {
TextComponent::Text(text)
TextComponent::Text { text }
| TextComponent::Web { text, .. }
| TextComponent::YouTube { text, .. } => text,
}
@ -84,7 +87,7 @@ impl TextComponent {
/// Returns an empty string if the component is not a link.
pub fn get_url(&self, yt_host: &str) -> String {
match self {
TextComponent::Text(_) => String::new(),
TextComponent::Text { .. } => String::new(),
TextComponent::Web { url, .. } => url.clone(),
TextComponent::YouTube { target, .. } => target.to_url_yt_host(yt_host),
}
@ -94,7 +97,7 @@ impl TextComponent {
impl ToPlaintext for TextComponent {
fn to_plaintext_yt_host(&self, yt_host: &str) -> String {
match self {
TextComponent::Text(text) => text.clone(),
TextComponent::Text { text } => text.clone(),
_ => self.get_url(yt_host),
}
}
@ -103,7 +106,7 @@ impl ToPlaintext for TextComponent {
impl ToHtml for TextComponent {
fn to_html_yt_host(&self, yt_host: &str) -> String {
match self {
TextComponent::Text(text) => util::escape_html(text),
TextComponent::Text { text } => util::escape_html(text),
TextComponent::Web { text, .. } => {
format!(
r#"<a href="{}" target="_blank" rel="noreferrer">{}</a>"#,
@ -125,7 +128,7 @@ impl ToHtml for TextComponent {
impl ToMarkdown for TextComponent {
fn to_markdown_yt_host(&self, yt_host: &str) -> String {
match self {
TextComponent::Text(text) => util::escape_markdown(text),
TextComponent::Text { text } => util::escape_markdown(text),
TextComponent::Web { text, .. } | TextComponent::YouTube { text, .. } => {
format!(
"[{}]({})",
@ -246,26 +249,10 @@ aespa 에스파 'Black Mamba' MV ℗ SM Entertainment"#
fn to_markdown() {
let richtext = RichText::from(TEXT_SOURCE.clone());
let markdown = richtext.to_markdown_yt_host("https://piped.kavin.rocks");
println!("{}", markdown);
assert_eq!(
markdown,
r#"🎧Listen and download aespa's debut single "Black Mamba": [https://smarturl.it/aespa\_BlackMamba](https://smarturl.it/aespa_BlackMamba)<br>
🐍The Debut Stage [https://youtu.be/Ky5RT5oGg0w](https://piped.kavin.rocks/watch?v=Ky5RT5oGg0w)
🎟 aespa Showcase SYNK in LA! Tickets now on sale: [https://www.ticketmaster.com/event/0A...](https://www.ticketmaster.com/event/0A005CCD9E871F6E)
Subscribe to aespa Official YouTube Channel!<br>
[https://www.youtube.com/aespa?sub\_con...](https://www.youtube.com/aespa?sub_confirmation=1)
aespa official<br>
[https://www.youtube.com/c/aespa](https://www.youtube.com/c/aespa)<br>
[https://www.instagram.com/aespa\_official](https://www.instagram.com/aespa_official)<br>
[https://www.tiktok.com/@aespa\_official](https://www.tiktok.com/@aespa_official)<br>
[https://twitter.com/aespa\_Official](https://twitter.com/aespa_Official)<br>
[https://www.facebook.com/aespa.official](https://www.facebook.com/aespa.official)<br>
[https://weibo.com/aespa](https://weibo.com/aespa)
\#aespa \#æspa \#BlackMamba \# \#<br>
aespa 'Black Mamba' MV SM Entertainment"#
r#"🎧Listen and download aespa's debut single "Black Mamba"\: [https\://smarturl.it/aespa\_BlackMamba](https://smarturl.it/aespa_BlackMamba)<br>🐍The Debut Stage [https\://youtu.be/Ky5RT5oGg0w](https://piped.kavin.rocks/watch?v=Ky5RT5oGg0w)<br><br>🎟️ aespa Showcase SYNK in LA! Tickets now on sale\: [https\://www.ticketmaster.com/event/0A...](https://www.ticketmaster.com/event/0A005CCD9E871F6E)<br><br>Subscribe to aespa Official YouTube Channel!<br>[https\://www.youtube.com/aespa?sub\_con...](https://www.youtube.com/aespa?sub_confirmation=1)<br><br>aespa official<br>[https\://www.youtube.com/c/aespa](https://www.youtube.com/c/aespa)<br>[https\://www.instagram.com/aespa\_official](https://www.instagram.com/aespa_official)<br>[https\://www.tiktok.com/@aespa\_official](https://www.tiktok.com/@aespa_official)<br>[https\://twitter.com/aespa\_Official](https://twitter.com/aespa_Official)<br>[https\://www.facebook.com/aespa.official](https://www.facebook.com/aespa.official)<br>[https\://weibo.com/aespa](https://weibo.com/aespa)<br><br>\#aespa \#æspa \#BlackMamba \#블랙맘바 \#에스파<br>aespa 에스파 'Black Mamba' MV ℗ SM Entertainment"#
)
}
}

View file

@ -380,13 +380,13 @@ impl From<TextComponent> for crate::model::richtext::TextComponent {
browse_id,
} => match page_type.to_url_target(browse_id) {
Some(target) => Self::YouTube { text, target },
None => Self::Text(text),
None => Self::Text { text },
},
TextComponent::Web { text, url } => Self::Web {
text,
url: util::sanitize_yt_url(&url),
},
TextComponent::Text { text } => Self::Text(text),
TextComponent::Text { text } => Self::Text { text },
}
}
}

View file

@ -329,7 +329,7 @@ where
if after_point {
exp -= 1;
}
} else if c == decimal_point {
} else if c == decimal_point && !digits.is_empty() {
after_point = true;
} else if !matches!(
c,
@ -394,21 +394,14 @@ pub fn escape_html(input: &str) -> String {
/// inserting into Markdown.
pub fn escape_markdown(input: &str) -> String {
let mut buf = String::with_capacity(input.len());
let mut chars = input.chars().peekable();
while let Some(c) = chars.next() {
for c in input.chars() {
match c {
'<' => buf.push_str("&lt;"),
'>' => buf.push_str("&gt;"),
'\n' => {
if chars.peek() == Some(&'\n') {
chars.next();
buf.push_str("\n\n");
} else {
buf.push_str("<br>\n");
}
}
'*' | '#' | '(' | ')' | '[' | ']' | '_' | '`' => {
'\n' => buf.push_str("<br>"),
'*' | '#' | '(' | ')' | '[' | ']' | '_' | '`' | '~' | '$' | '^' | '=' | ':' | '+'
| '\\' => {
buf.push('\\');
buf.push(c);
}
@ -588,6 +581,7 @@ pub(crate) mod tests {
3_360_000
)]
#[case(Language::As, "১ জন গ্ৰাহক", 1)]
#[case(Language::Ru, "Зрителей, ожидающих начала трансляции: 6", 6)]
fn t_parse_large_numstr(#[case] lang: Language, #[case] string: &str, #[case] expect: u64) {
let res = parse_large_numstr::<u64>(string, lang).unwrap();
assert_eq!(res, expect);

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -340,6 +340,13 @@ fn get_player_error(#[case] id: &str, #[case] expect: UnavailabilityReason, rp:
Some("SHINE - Survival Hardcore in New Environment: Auf einem Server machen sich tapfere Spieler auf, mystische Welten zu erkunden, magische Technologien zu erforschen und vorallem zu überleben...".to_owned()),
Some(("UCQM0bS4_04-Y4JuYrgmnpZQ", "Chaosflo44")),
)]
#[case::live(
"UULVvqRdlKsE5Q8mf8YXbdIJLw",
"Live streams",
true,
None,
Some(("UCvqRdlKsE5Q8mf8YXbdIJLw", "LoL Esports"))
)]
fn get_playlist(
#[case] id: &str,
#[case] name: &str,