feat: allow searching for YTM users

This commit is contained in:
ThetaDev 2024-11-09 00:36:42 +01:00
parent 577370b06d
commit 50010b7b08
No known key found for this signature in database
GPG key ID: E319D3C5148D65B6
12 changed files with 8224 additions and 3336 deletions

View file

@ -269,6 +269,7 @@ enum MusicSearchCategory {
Albums,
PlaylistsYtm,
PlaylistsCommunity,
Users,
}
#[derive(Copy, Clone, PartialEq, Eq, ValueEnum)]
@ -348,8 +349,13 @@ fn print_data<T: Serialize>(data: &T, format: Format, pretty: bool) {
};
}
fn print_entities(items: &[impl YtEntity]) {
fn print_entities(items: &[impl YtEntity], with_type: bool) {
for e in items {
if with_type {
if let Some(t) = e.music_item_type() {
anstream::print!("{: >8} ", format!("{t:?}").dimmed());
}
}
anstream::print!("[{}] {}", e.id(), e.name().bold());
if let Some(n) = e.channel_name() {
anstream::print!(" - {}", n.cyan());
@ -399,6 +405,7 @@ fn print_music_search<T: Serialize + YtEntity>(
data: &MusicSearchResult<T>,
format: Option<Format>,
pretty: bool,
with_type: bool,
) {
match format {
Some(format) => print_data(data, format, pretty),
@ -406,7 +413,7 @@ fn print_music_search<T: Serialize + YtEntity>(
if let Some(corr) = &data.corrected_query {
anstream::println!("Did you mean `{}`?", corr.magenta());
}
print_entities(&data.items.items);
print_entities(&data.items.items, with_type);
}
}
}
@ -788,7 +795,7 @@ async fn run() -> anyhow::Result<()> {
print_description(Some(details.description.to_plaintext()));
if !details.recommended.is_empty() {
print_h2("Recommended");
print_entities(&details.recommended.items);
print_entities(&details.recommended.items, false);
}
let comment_list = comments.map(|c| match c {
CommentsOrder::Top => &details.top_comments.items,
@ -872,11 +879,11 @@ async fn run() -> anyhow::Result<()> {
}
if !artist.playlists.is_empty() {
print_h2("Playlists");
print_entities(&artist.playlists);
print_entities(&artist.playlists, false);
}
if !artist.similar_artists.is_empty() {
print_h2("Similar artists");
print_entities(&artist.similar_artists);
print_entities(&artist.similar_artists, false);
}
}
}
@ -903,7 +910,7 @@ async fn run() -> anyhow::Result<()> {
);
}
println!();
print_entities(&rss.videos);
print_entities(&rss.videos, false);
}
}
} else {
@ -944,7 +951,7 @@ async fn run() -> anyhow::Result<()> {
}
print_description(Some(channel.description));
println!();
print_entities(&channel.content.items);
print_entities(&channel.content.items, false);
}
}
}
@ -973,7 +980,7 @@ async fn run() -> anyhow::Result<()> {
anstream::println!("{} {}", "Videos:".blue(), vids);
}
println!();
print_entities(&channel.content.items);
print_entities(&channel.content.items, false);
}
}
}
@ -1077,7 +1084,7 @@ async fn run() -> anyhow::Result<()> {
}
print_description(playlist.description.map(|d| d.to_plaintext()));
println!();
print_entities(&playlist.videos.items);
print_entities(&playlist.videos.items, false);
}
}
}
@ -1145,7 +1152,7 @@ async fn run() -> anyhow::Result<()> {
}
print_description(Some(channel.description));
println!();
print_entities(&channel.content.items);
print_entities(&channel.content.items, false);
}
}
}
@ -1167,34 +1174,34 @@ async fn run() -> anyhow::Result<()> {
if let Some(corr) = res.corrected_query {
anstream::println!("Did you mean `{}`?", corr.magenta());
}
print_entities(&res.items.items);
print_entities(&res.items.items, false);
}
}
}
},
Some(MusicSearchCategory::All) => {
let res = rp.query().music_search_main(&query).await?;
print_music_search(&res, format, pretty);
print_music_search(&res, format, pretty, true);
}
Some(MusicSearchCategory::Tracks) => {
let mut res = rp.query().music_search_tracks(&query).await?;
res.items.extend_limit(rp.query(), limit).await?;
print_music_search(&res, format, pretty);
print_music_search(&res, format, pretty, false);
}
Some(MusicSearchCategory::Videos) => {
let mut res = rp.query().music_search_videos(&query).await?;
res.items.extend_limit(rp.query(), limit).await?;
print_music_search(&res, format, pretty);
print_music_search(&res, format, pretty, false);
}
Some(MusicSearchCategory::Artists) => {
let mut res = rp.query().music_search_artists(&query).await?;
res.items.extend_limit(rp.query(), limit).await?;
print_music_search(&res, format, pretty);
print_music_search(&res, format, pretty, false);
}
Some(MusicSearchCategory::Albums) => {
let mut res = rp.query().music_search_albums(&query).await?;
res.items.extend_limit(rp.query(), limit).await?;
print_music_search(&res, format, pretty);
print_music_search(&res, format, pretty, false);
}
Some(MusicSearchCategory::PlaylistsYtm | MusicSearchCategory::PlaylistsCommunity) => {
let mut res = rp
@ -1205,7 +1212,12 @@ async fn run() -> anyhow::Result<()> {
)
.await?;
res.items.extend_limit(rp.query(), limit).await?;
print_music_search(&res, format, pretty);
print_music_search(&res, format, pretty, false);
}
Some(MusicSearchCategory::Users) => {
let mut res = rp.query().music_search_users(&query).await?;
res.items.extend_limit(rp.query(), limit).await?;
print_music_search(&res, format, pretty, false);
}
},
Commands::Vdata => {

View file

@ -9,7 +9,7 @@ use crate::{
paginator::{ContinuationEndpoint, Paginator},
traits::FromYtItem,
AlbumItem, ArtistItem, MusicItem, MusicPlaylistItem, MusicSearchResult,
MusicSearchSuggestion, TrackItem,
MusicSearchSuggestion, TrackItem, UserItem,
},
param::search_filter::MusicSearchFilter,
serializer::MapResult,
@ -121,6 +121,15 @@ impl RustyPipeQuery {
.await
}
/// Search YouTube Music users
pub async fn music_search_users<S: AsRef<str>>(
&self,
query: S,
) -> Result<MusicSearchResult<UserItem>, Error> {
self.music_search(query, Some(MusicSearchFilter::Users))
.await
}
/// Get YouTube Music search suggestions
#[tracing::instrument(skip(self), level = "error")]
pub async fn music_search_suggestion<S: AsRef<str> + Debug>(

View file

@ -4,7 +4,7 @@ use serde_with::{rust::deserialize_ignore_any, serde_as, DefaultOnError, VecSkip
use crate::{
model::{
self, traits::FromYtItem, AlbumId, AlbumItem, AlbumType, ArtistId, ArtistItem, ChannelId,
MusicItem, MusicItemType, MusicPlaylistItem, TrackItem,
MusicItem, MusicItemType, MusicPlaylistItem, TrackItem, UserItem,
},
param::Language,
serializer::{
@ -535,7 +535,7 @@ impl MusicListMapper {
etype
}
/// Map a ListMusicItem (album/playlist tile)
/// Map a ListMusicItem (album/playlist item, search result)
fn map_list_item(&mut self, item: ListMusicItem) -> Result<Option<MusicItemType>, String> {
let mut columns = item.flex_columns.into_iter();
let c1 = columns.next();
@ -858,6 +858,19 @@ impl MusicListMapper {
}));
Ok(Some(MusicItemType::Playlist))
}
MusicPageType::User => {
// Part 1 may be the "Profile" label
let handle = map_channel_handle(subtitle_p2.as_ref())
.or_else(|| map_channel_handle(subtitle_p1.as_ref()));
self.items.push(MusicItem::User(UserItem {
id,
name: title,
handle,
avatar: item.thumbnail.into(),
}));
Ok(Some(MusicItemType::User))
}
MusicPageType::None => {
// There may be broken YT channels from the artist search. They can be skipped.
Ok(None)
@ -1009,7 +1022,7 @@ impl MusicListMapper {
}));
Ok(Some(MusicItemType::Playlist))
}
MusicPageType::None => Ok(None),
MusicPageType::None | MusicPageType::User => Ok(None),
},
None => Err("could not determine item type".to_owned()),
}
@ -1144,6 +1157,19 @@ impl MusicListMapper {
}));
Some(MusicItemType::Playlist)
}
MusicPageType::User => {
// Part 1 may be the "Profile" label
let handle = map_channel_handle(subtitle_p2.as_ref())
.or_else(|| map_channel_handle(subtitle_p1.as_ref()));
self.items.push(MusicItem::User(UserItem {
id: music_page.id,
name: card.title,
handle,
avatar: card.thumbnail.into(),
}));
Some(MusicItemType::User)
}
MusicPageType::None => None,
},
None => {
@ -1206,6 +1232,7 @@ impl MusicListMapper {
MusicItem::Album(album) => albums.push(album),
MusicItem::Artist(artist) => artists.push(artist),
MusicItem::Playlist(playlist) => playlists.push(playlist),
MusicItem::User(_) => {}
}
}
@ -1256,6 +1283,12 @@ fn map_artist_id_fallback(
.or_else(|| fallback_artist.and_then(|a| a.id.clone()))
}
fn map_channel_handle(st: Option<&TextComponents>) -> Option<String> {
st.map(|t| t.first_str())
.filter(|t| t.starts_with('@'))
.map(str::to_owned)
}
pub(crate) fn map_artist_id(entries: Vec<MusicItemMenuEntry>) -> Option<String> {
entries.into_iter().find_map(|i| {
if let NavigationEndpoint::Browse {

View file

@ -227,6 +227,7 @@ pub(crate) enum MusicPageType {
Album,
Playlist,
Track { vtype: MusicVideoType },
User,
None,
}
@ -236,10 +237,11 @@ impl From<PageType> for MusicPageType {
PageType::Artist => MusicPageType::Artist,
PageType::Album => MusicPageType::Album,
PageType::Playlist | PageType::Podcast => MusicPageType::Playlist,
PageType::Channel | PageType::Unknown => MusicPageType::None,
PageType::Channel => MusicPageType::User,
PageType::Episode => MusicPageType::Track {
vtype: MusicVideoType::Episode,
},
PageType::Unknown => MusicPageType::None,
}
}
}

View file

@ -4,7 +4,7 @@ expression: map_res.c
---
MusicSearchResult(
items: Paginator(
count: Some(16),
count: Some(28),
items: [
Track(TrackItem(
id: "ZeerrnuLi5E",
@ -25,7 +25,79 @@ MusicSearchResult(
],
artist_id: Some("UCEdZAdnnKqbaHOlv8nM6OtA"),
album: None,
view_count: Some(235000000),
view_count: Some(273000000),
is_video: true,
track_nr: None,
by_va: false,
)),
Track(TrackItem(
id: "NU611fxGyPU",
name: "Black Mamba",
duration: Some(175),
cover: [
Thumbnail(
url: "https://i.ytimg.com/vi/NU611fxGyPU/sddefault.jpg?sqp=-oaymwEWCJADEOEBIAQqCghqEJQEGHgg6AJIWg&rs=AMzJL3md93InOvanuHclIZe1FpSmEVWGKw",
width: 400,
height: 225,
),
],
artists: [
ArtistId(
id: Some("UCEdZAdnnKqbaHOlv8nM6OtA"),
name: "aespa",
),
],
artist_id: Some("UCEdZAdnnKqbaHOlv8nM6OtA"),
album: None,
view_count: Some(43000000),
is_video: true,
track_nr: None,
by_va: false,
)),
Track(TrackItem(
id: "Yi2nsnpw5h0",
name: "aespa - Black Mamba (Official Instrumental)",
duration: Some(175),
cover: [
Thumbnail(
url: "https://i.ytimg.com/vi/Yi2nsnpw5h0/sddefault.jpg?sqp=-oaymwEWCJADEOEBIAQqCghqEJQEGHgg6AJIWg&rs=AMzJL3meMnbqX2Gi5z5lD0G6PeDxcp-zpA",
width: 400,
height: 225,
),
],
artists: [
ArtistId(
id: Some("UCx5Dw_5guQcKu_lMGCh-IuQ"),
name: "aesthetic inst.",
),
],
artist_id: Some("UCx5Dw_5guQcKu_lMGCh-IuQ"),
album: None,
view_count: Some(1500000),
is_video: true,
track_nr: None,
by_va: false,
)),
Track(TrackItem(
id: "2Qefh0W_H88",
name: "aespa - black mamba (𝒔𝒍𝒐𝒘𝒆𝒅 𝒏 𝒓𝒆𝒗𝒆𝒓𝒃)",
duration: Some(209),
cover: [
Thumbnail(
url: "https://i.ytimg.com/vi/2Qefh0W_H88/sddefault.jpg?sqp=-oaymwEWCJADEOEBIAQqCghqEJQEGHgg6AJIWg&rs=AMzJL3knLw9_f0ukxeV-S6vS5_JOTXnaWQ",
width: 400,
height: 225,
),
],
artists: [
ArtistId(
id: Some("UCrGYENbzwtva2X16bAPhTbA"),
name: "i n s o m n i o",
),
],
artist_id: Some("UCrGYENbzwtva2X16bAPhTbA"),
album: None,
view_count: Some(1500000),
is_video: true,
track_nr: None,
by_va: false,
@ -57,110 +129,82 @@ MusicSearchResult(
id: "MPREb_OpHWHwyNOuY",
name: "Black Mamba",
)),
view_count: None,
view_count: Some(544000000),
is_video: false,
track_nr: None,
by_va: false,
)),
Track(TrackItem(
id: "cATe8Toht70",
name: "Black Mamba",
duration: Some(74),
id: "PpKu3UsHYrk",
name: "Ghetto Millionnaire",
duration: Some(263),
cover: [
Thumbnail(
url: "https://lh3.googleusercontent.com/ZesxRmV1_bDW89z70eojCd6DofYPbzbgGaXSIRP3UjmE4nIAkOuWc8pXaozR4AwrzPQublDCKrg6vcxHOg=w60-h60-l90-rj",
url: "https://lh3.googleusercontent.com/p6AWfbIdksK7FGWMlutdCV0t449Nd_odfNnT9G80KDajqmXklX4H-nymvTADsn1JhEnRDaPSfbw_hmlKdg=w60-h60-l90-rj",
width: 60,
height: 60,
),
Thumbnail(
url: "https://lh3.googleusercontent.com/ZesxRmV1_bDW89z70eojCd6DofYPbzbgGaXSIRP3UjmE4nIAkOuWc8pXaozR4AwrzPQublDCKrg6vcxHOg=w120-h120-l90-rj",
url: "https://lh3.googleusercontent.com/p6AWfbIdksK7FGWMlutdCV0t449Nd_odfNnT9G80KDajqmXklX4H-nymvTADsn1JhEnRDaPSfbw_hmlKdg=w120-h120-l90-rj",
width: 120,
height: 120,
),
],
artists: [
ArtistId(
id: Some("UCfCNL5oajlQBAlyjWv1ChVw"),
name: "Hans Zimmer",
),
ArtistId(
id: Some("UCvTXGTZf9EvuCAwZOkoR2iQ"),
name: "Lorne Balfe",
id: Some("UCxX9tNcQgCBuU56ezupriqg"),
name: "Black Mamba",
),
],
artist_id: Some("UCfCNL5oajlQBAlyjWv1ChVw"),
artist_id: Some("UCxX9tNcQgCBuU56ezupriqg"),
album: Some(AlbumId(
id: "MPREb_UmDOhLpDsc0",
name: "Megamind (Music from the Motion Picture)",
id: "MPREb_miyMs44ZpHc",
name: "Ghetto Millionnaire",
)),
view_count: None,
view_count: Some(1200000),
is_video: false,
track_nr: None,
by_va: false,
)),
Track(TrackItem(
id: "WwNKyoizf8k",
name: "BLACK MAMBA",
duration: Some(182),
id: "jynOfK8JB0E",
name: "It Ain\'t You",
duration: Some(268),
cover: [
Thumbnail(
url: "https://lh3.googleusercontent.com/gall0XXuwoV_SYR3S6EgtOGaBC3YOR5wOpQxCyqgxC3Xht3Jc95Y-sFg-sGAcQl946MfurGY_xSv0YBT=w60-h60-l90-rj",
url: "https://lh3.googleusercontent.com/ByZsPc5CHoZwtn-cl7e_nbhiVkWxoFJ2RHkNUvLTiowT8228-aVd6r2XT08Z8a32Qa7d-0-Go44sxkdf=w60-h60-l90-rj",
width: 60,
height: 60,
),
Thumbnail(
url: "https://lh3.googleusercontent.com/gall0XXuwoV_SYR3S6EgtOGaBC3YOR5wOpQxCyqgxC3Xht3Jc95Y-sFg-sGAcQl946MfurGY_xSv0YBT=w120-h120-l90-rj",
url: "https://lh3.googleusercontent.com/ByZsPc5CHoZwtn-cl7e_nbhiVkWxoFJ2RHkNUvLTiowT8228-aVd6r2XT08Z8a32Qa7d-0-Go44sxkdf=w120-h120-l90-rj",
width: 120,
height: 120,
),
],
artists: [
ArtistId(
id: Some("UCz6yr3CgFGrrrPDa2asbWMQ"),
name: "Bayamon PR Tribe",
id: Some("UCaDT20-B3U8h-tPg_VMvntw"),
name: "The Black Mamba",
),
],
artist_id: Some("UCz6yr3CgFGrrrPDa2asbWMQ"),
artist_id: Some("UCaDT20-B3U8h-tPg_VMvntw"),
album: Some(AlbumId(
id: "MPREb_RV0PGHyGfkp",
name: "LISTEN ME",
id: "MPREb_hXasyBrDJm7",
name: "The Black Mamba",
)),
view_count: None,
view_count: Some(1300000),
is_video: false,
track_nr: None,
by_va: false,
)),
Track(TrackItem(
id: "yQUU29NwNF4",
name: "aespa(에스파) - Black Mamba @인기가요 inkigayo 20201122",
duration: Some(213),
id: "pgjQkcYD-rQ",
name: "Black Mamba (Techwear ver. Dance Practice)",
duration: Some(198),
cover: [
Thumbnail(
url: "https://i.ytimg.com/vi/yQUU29NwNF4/sddefault.jpg?sqp=-oaymwEWCJADEOEBIAQqCghqEJQEGHgg6AJIWg&rs=AMzJL3k0HD8CTPlz4YU0hvy1GqKSf2HKUQ",
width: 400,
height: 225,
),
],
artists: [
ArtistId(
id: Some("UCS_hnpJLQTvBkqALgapi_4g"),
name: "스브스케이팝 X INKIGAYO",
),
],
artist_id: Some("UCS_hnpJLQTvBkqALgapi_4g"),
album: None,
view_count: Some(10000000),
is_video: true,
track_nr: None,
by_va: false,
)),
Track(TrackItem(
id: "Ky5RT5oGg0w",
name: "Black Mamba",
duration: Some(287),
cover: [
Thumbnail(
url: "https://i.ytimg.com/vi/Ky5RT5oGg0w/sddefault.jpg?sqp=-oaymwEWCJADEOEBIAQqCghqEJQEGHgg6AJIWg&rs=AMzJL3mB-DDgCruC-dhPM0v66ckiZJQnJg",
url: "https://i.ytimg.com/vi/pgjQkcYD-rQ/sddefault.jpg?sqp=-oaymwEWCJADEOEBIAQqCghqEJQEGHgg6AJIWg&rs=AMzJL3k8ndHhyzqiuzAXoLwsrk-I7IKt5Q",
width: 400,
height: 225,
),
@ -173,18 +217,18 @@ MusicSearchResult(
],
artist_id: Some("UCEdZAdnnKqbaHOlv8nM6OtA"),
album: None,
view_count: Some(18000000),
view_count: Some(9600000),
is_video: true,
track_nr: None,
by_va: false,
)),
Track(TrackItem(
id: "dz9bieeSVRw",
name: "aespa - Black Mamba (Music Bank) | KBS WORLD TV 201127",
duration: Some(192),
id: "w2GXdb-pHo8",
name: "(Hot Debut) aespa - Black Mamba (Music Bank) | KBS WORLD TV 201120",
duration: None,
cover: [
Thumbnail(
url: "https://i.ytimg.com/vi/dz9bieeSVRw/sddefault.jpg?sqp=-oaymwEWCJADEOEBIAQqCghqEJQEGHgg6AJIWg&rs=AMzJL3lsJGKTqJhnt-ckrJtBLlvSp46Y5g",
url: "https://i.ytimg.com/vi/w2GXdb-pHo8/sddefault.jpg?sqp=-oaymwEWCJADEOEBIAQqCghqEJQEGHgg6AJIWg&rs=AMzJL3l5kwjfr-rdoZvgzcLk38ilMou95g",
width: 400,
height: 225,
),
@ -197,135 +241,223 @@ MusicSearchResult(
],
artist_id: Some("UC5BMQOsAB8hKUyHu9KI6yig"),
album: None,
view_count: Some(3200000),
view_count: None,
is_video: true,
track_nr: None,
by_va: false,
)),
Track(TrackItem(
id: "JepNreB58TA",
name: "aespa (에스파) - Black Mamba | Sydney - SYNK: Parallel Line | 4K60 직캠 Fancam Front Row",
duration: Some(170),
cover: [
Thumbnail(
url: "https://i.ytimg.com/vi/JepNreB58TA/sddefault.jpg?sqp=-oaymwEWCJADEOEBIAQqCghqEJQEGHgg6AJIWg&rs=AMzJL3lhDLwngEone0tYZ0omfA6rs6Nj2w",
width: 400,
height: 225,
),
],
artists: [
ArtistId(
id: Some("UCAOoElZAQnI0zN91qvzezCw"),
name: "yentaxi",
),
],
artist_id: Some("UCAOoElZAQnI0zN91qvzezCw"),
album: None,
view_count: Some(213000),
is_video: true,
track_nr: None,
by_va: false,
)),
Album(AlbumItem(
id: "MPREb_OpHWHwyNOuY",
id: "MPREb_rR0VQ4fTxPM",
name: "black mamba",
cover: [
Thumbnail(
url: "https://lh3.googleusercontent.com/9H5D-h9AQdUQsPlq7emEOm4R6atXeOVsQl9CNFfKAXocK9UWVemlewjCc665YE_CJFJPQzm4euGmHDvl=w60-h60-l90-rj",
width: 60,
height: 60,
),
Thumbnail(
url: "https://lh3.googleusercontent.com/9H5D-h9AQdUQsPlq7emEOm4R6atXeOVsQl9CNFfKAXocK9UWVemlewjCc665YE_CJFJPQzm4euGmHDvl=w120-h120-l90-rj",
width: 120,
height: 120,
),
Thumbnail(
url: "https://lh3.googleusercontent.com/9H5D-h9AQdUQsPlq7emEOm4R6atXeOVsQl9CNFfKAXocK9UWVemlewjCc665YE_CJFJPQzm4euGmHDvl=w226-h226-l90-rj",
width: 226,
height: 226,
),
Thumbnail(
url: "https://lh3.googleusercontent.com/9H5D-h9AQdUQsPlq7emEOm4R6atXeOVsQl9CNFfKAXocK9UWVemlewjCc665YE_CJFJPQzm4euGmHDvl=w544-h544-l90-rj",
width: 544,
height: 544,
),
],
artists: [
ArtistId(
id: Some("UClSZ2io808U-NOICSbjvwEg"),
name: "ff phonk",
),
],
artist_id: Some("UClSZ2io808U-NOICSbjvwEg"),
album_type: Single,
year: Some(2024),
by_va: false,
)),
Album(AlbumItem(
id: "MPREb_ZXbDKPXnct4",
name: "Mi Back",
cover: [
Thumbnail(
url: "https://lh3.googleusercontent.com/N1vPbX8Qwykpsx_bCguQKz4D6REvCvHSvgqpGKwN8Z1GIuZHblZalXlKZn-4IMe5Gxv3uSmNDRiagss3XA=w60-h60-l90-rj",
width: 60,
height: 60,
),
Thumbnail(
url: "https://lh3.googleusercontent.com/N1vPbX8Qwykpsx_bCguQKz4D6REvCvHSvgqpGKwN8Z1GIuZHblZalXlKZn-4IMe5Gxv3uSmNDRiagss3XA=w120-h120-l90-rj",
width: 120,
height: 120,
),
Thumbnail(
url: "https://lh3.googleusercontent.com/N1vPbX8Qwykpsx_bCguQKz4D6REvCvHSvgqpGKwN8Z1GIuZHblZalXlKZn-4IMe5Gxv3uSmNDRiagss3XA=w226-h226-l90-rj",
width: 226,
height: 226,
),
Thumbnail(
url: "https://lh3.googleusercontent.com/N1vPbX8Qwykpsx_bCguQKz4D6REvCvHSvgqpGKwN8Z1GIuZHblZalXlKZn-4IMe5Gxv3uSmNDRiagss3XA=w544-h544-l90-rj",
width: 544,
height: 544,
),
],
artists: [
ArtistId(
id: Some("UCzKrKM1QQQyw8uZ_NcNldGQ"),
name: "Black Mamba & eLgozzy",
),
],
artist_id: Some("UCzKrKM1QQQyw8uZ_NcNldGQ"),
album_type: Single,
year: Some(2024),
by_va: false,
)),
Album(AlbumItem(
id: "MPREb_LGXKt36T0rM",
name: "Black Mamba",
cover: [
Thumbnail(
url: "https://lh3.googleusercontent.com/MOL4_Ula9hocErkX2xK_7mISFiWvQz51vReT14KCHF9wsqCEH6sO8iilFFelWMn7JOYIk2WFa-gMmw2uvw=w60-h60-l90-rj",
url: "https://lh3.googleusercontent.com/eusGkrg73YceOgTNl2na4Ywi2pKSdeIVCNYuebdd5nJ20Yw_L4wBTKKR0_Qj4W0-in32dKal-GYKNUGB=w60-h60-l90-rj",
width: 60,
height: 60,
),
Thumbnail(
url: "https://lh3.googleusercontent.com/MOL4_Ula9hocErkX2xK_7mISFiWvQz51vReT14KCHF9wsqCEH6sO8iilFFelWMn7JOYIk2WFa-gMmw2uvw=w120-h120-l90-rj",
url: "https://lh3.googleusercontent.com/eusGkrg73YceOgTNl2na4Ywi2pKSdeIVCNYuebdd5nJ20Yw_L4wBTKKR0_Qj4W0-in32dKal-GYKNUGB=w120-h120-l90-rj",
width: 120,
height: 120,
),
Thumbnail(
url: "https://lh3.googleusercontent.com/MOL4_Ula9hocErkX2xK_7mISFiWvQz51vReT14KCHF9wsqCEH6sO8iilFFelWMn7JOYIk2WFa-gMmw2uvw=w226-h226-l90-rj",
url: "https://lh3.googleusercontent.com/eusGkrg73YceOgTNl2na4Ywi2pKSdeIVCNYuebdd5nJ20Yw_L4wBTKKR0_Qj4W0-in32dKal-GYKNUGB=w226-h226-l90-rj",
width: 226,
height: 226,
),
Thumbnail(
url: "https://lh3.googleusercontent.com/MOL4_Ula9hocErkX2xK_7mISFiWvQz51vReT14KCHF9wsqCEH6sO8iilFFelWMn7JOYIk2WFa-gMmw2uvw=w544-h544-l90-rj",
url: "https://lh3.googleusercontent.com/eusGkrg73YceOgTNl2na4Ywi2pKSdeIVCNYuebdd5nJ20Yw_L4wBTKKR0_Qj4W0-in32dKal-GYKNUGB=w544-h544-l90-rj",
width: 544,
height: 544,
),
],
artists: [
ArtistId(
id: Some("UCEdZAdnnKqbaHOlv8nM6OtA"),
name: "aespa",
id: Some("UC7R_cJYLxanyOw9KsfOzu7Q"),
name: "MOGI (IL)",
),
],
artist_id: Some("UCEdZAdnnKqbaHOlv8nM6OtA"),
artist_id: Some("UC7R_cJYLxanyOw9KsfOzu7Q"),
album_type: Single,
year: Some(2020),
year: Some(2024),
by_va: false,
)),
Album(AlbumItem(
id: "MPREb_pvdHyqvGjbI",
name: "Girls - The 2nd Mini Album",
cover: [
Playlist(MusicPlaylistItem(
id: "PLnAcDMIXVUFqVONj6hrtTb5jfumRjA6NF",
name: "Black Mamba Mixtape",
thumbnail: [
Thumbnail(
url: "https://lh3.googleusercontent.com/JYOTl7neLJLMUEVjdg_qIqz7XjUZB2AQAx_sRDlNVd5jSYiv1xA0v68ZN8Kn0KKf1fSfQnTaeakGeQgI=w60-h60-l90-rj",
width: 60,
height: 60,
url: "https://i.ytimg.com/vi/S3xisVb4Nt0/sddefault.jpg?sqp=-oaymwEWCJADEOEBIAQqCghqEJQEGHgg6AJIWg&rs=AMzJL3kHkhtnq5pAgdX7sVqd7699sdwzPw",
width: 400,
height: 225,
),
Thumbnail(
url: "https://lh3.googleusercontent.com/JYOTl7neLJLMUEVjdg_qIqz7XjUZB2AQAx_sRDlNVd5jSYiv1xA0v68ZN8Kn0KKf1fSfQnTaeakGeQgI=w120-h120-l90-rj",
width: 120,
height: 120,
url: "https://i.ytimg.com/vi/S3xisVb4Nt0/hq720.jpg?sqp=-oaymwEXCKAGEMIDIAQqCwjVARCqCBh4INgESFo&rs=AMzJL3kgdAd7dmEUsPEwH_QG1yjtHLfxNA",
width: 800,
height: 450,
),
Thumbnail(
url: "https://lh3.googleusercontent.com/JYOTl7neLJLMUEVjdg_qIqz7XjUZB2AQAx_sRDlNVd5jSYiv1xA0v68ZN8Kn0KKf1fSfQnTaeakGeQgI=w226-h226-l90-rj",
width: 226,
height: 226,
),
Thumbnail(
url: "https://lh3.googleusercontent.com/JYOTl7neLJLMUEVjdg_qIqz7XjUZB2AQAx_sRDlNVd5jSYiv1xA0v68ZN8Kn0KKf1fSfQnTaeakGeQgI=w544-h544-l90-rj",
width: 544,
height: 544,
url: "https://i.ytimg.com/vi/S3xisVb4Nt0/hq720.jpg?sqp=-oaymwEXCNUGEOADIAQqCwjVARCqCBh4INgESFo&rs=AMzJL3mLtxWlynBlNSCHsBbxPLxGUhcijQ",
width: 853,
height: 480,
),
],
artists: [
ArtistId(
id: Some("UCEdZAdnnKqbaHOlv8nM6OtA"),
name: "aespa",
),
],
artist_id: Some("UCEdZAdnnKqbaHOlv8nM6OtA"),
album_type: Album,
year: Some(2022),
by_va: false,
channel: Some(ChannelId(
id: "UCulZuGBZLHEu_9natGq9Q7g",
name: "Jay South Music",
)),
Album(AlbumItem(
id: "MPREb_CznUTKnATw6",
name: "Black Mamba (feat. Foolio)",
cover: [
Thumbnail(
url: "https://lh3.googleusercontent.com/3ut0tvS5LYcfHjLwrYPSYNbraALbFb9ov28b2GXHB8ABaMGWILUko_BJa1jpsSVrELE_B8so3NtYMVfb1g=w60-h60-l90-rj",
width: 60,
height: 60,
),
Thumbnail(
url: "https://lh3.googleusercontent.com/3ut0tvS5LYcfHjLwrYPSYNbraALbFb9ov28b2GXHB8ABaMGWILUko_BJa1jpsSVrELE_B8so3NtYMVfb1g=w120-h120-l90-rj",
width: 120,
height: 120,
),
Thumbnail(
url: "https://lh3.googleusercontent.com/3ut0tvS5LYcfHjLwrYPSYNbraALbFb9ov28b2GXHB8ABaMGWILUko_BJa1jpsSVrELE_B8so3NtYMVfb1g=w226-h226-l90-rj",
width: 226,
height: 226,
),
Thumbnail(
url: "https://lh3.googleusercontent.com/3ut0tvS5LYcfHjLwrYPSYNbraALbFb9ov28b2GXHB8ABaMGWILUko_BJa1jpsSVrELE_B8so3NtYMVfb1g=w544-h544-l90-rj",
width: 544,
height: 544,
),
],
artists: [
ArtistId(
id: Some("UCZK5n7V2-iPHfUXLV2tDvzw"),
name: "Cojack",
),
],
artist_id: Some("UCZK5n7V2-iPHfUXLV2tDvzw"),
album_type: Single,
year: Some(2020),
by_va: false,
track_count: None,
from_ytm: false,
)),
Artist(ArtistItem(
id: "UCEdZAdnnKqbaHOlv8nM6OtA",
name: "aespa",
avatar: [
Playlist(MusicPlaylistItem(
id: "PL38uS170Dxaatridfyyj-fqjSOlwg7h5R",
name: "Black Mamba Man",
thumbnail: [
Thumbnail(
url: "https://lh3.googleusercontent.com/gV8Sbt3iKraNm_H9ZaH3oh6ERRdN0Dj6qHmTLPiQQ4WS8uGNN09HlpujMJOWwei_z5yC9Th1cZXyOQ=w60-h60-p-l90-rj",
width: 60,
height: 60,
url: "https://i.ytimg.com/vi/1jbpmnC_ox0/sddefault.jpg?sqp=-oaymwEWCJADEOEBIAQqCghqEJQEGHgg6AJIWg&rs=AMzJL3kZHx-HBttEizSkMOftu5xGb7CYYQ",
width: 400,
height: 225,
),
Thumbnail(
url: "https://lh3.googleusercontent.com/gV8Sbt3iKraNm_H9ZaH3oh6ERRdN0Dj6qHmTLPiQQ4WS8uGNN09HlpujMJOWwei_z5yC9Th1cZXyOQ=w120-h120-p-l90-rj",
width: 120,
height: 120,
url: "https://i.ytimg.com/vi/1jbpmnC_ox0/hq720.jpg?sqp=-oaymwEXCKAGEMIDIAQqCwjVARCqCBh4INgESFo&rs=AMzJL3kYUMfl3cLxq6_oUdli4dyGLPrJRA",
width: 800,
height: 450,
),
Thumbnail(
url: "https://i.ytimg.com/vi/1jbpmnC_ox0/hq720.jpg?sqp=-oaymwEXCNUGEOADIAQqCwjVARCqCBh4INgESFo&rs=AMzJL3n_rKN8FnNasX2FNzN8_OtVyGmvBQ",
width: 853,
height: 480,
),
],
subscriber_count: Some(4120000),
channel: Some(ChannelId(
id: "UCNgek3KQIVQsT_2O2puCWRA",
name: "one day",
)),
track_count: None,
from_ytm: false,
)),
Playlist(MusicPlaylistItem(
id: "PLEl8NhnoNOpbZ0HkkChu3xEhKShx_vtya",
name: "The Black Mamba",
thumbnail: [
Thumbnail(
url: "https://yt3.ggpht.com/NtKO3BdJFpImVJNOTFMS2f1F6rK6ivWHVP3jGsYoERpvvBr7oXb7eWSwhIZBYRCEtW_Qvyib9KH2=s192",
width: 192,
height: 192,
),
Thumbnail(
url: "https://yt3.ggpht.com/NtKO3BdJFpImVJNOTFMS2f1F6rK6ivWHVP3jGsYoERpvvBr7oXb7eWSwhIZBYRCEtW_Qvyib9KH2=s576",
width: 576,
height: 576,
),
Thumbnail(
url: "https://yt3.ggpht.com/NtKO3BdJFpImVJNOTFMS2f1F6rK6ivWHVP3jGsYoERpvvBr7oXb7eWSwhIZBYRCEtW_Qvyib9KH2=s1200",
width: 1200,
height: 1200,
),
],
channel: Some(ChannelId(
id: "UCCE3DsIpCrNWQMICx-zMNew",
name: "MoveAMente",
)),
track_count: None,
from_ytm: false,
)),
Artist(ArtistItem(
id: "UCaDT20-B3U8h-tPg_VMvntw",
@ -342,19 +474,36 @@ MusicSearchResult(
height: 120,
),
],
subscriber_count: Some(2640),
subscriber_count: Some(2890),
)),
Artist(ArtistItem(
id: "UCRpi1gBlax4sK3dNNxIxxFg",
name: "Black Mamba Official",
avatar: [
Thumbnail(
url: "https://lh3.googleusercontent.com/DBaVgQwyirgH4_Rg6w7jQBTP1fyHl5dNMK91dLZD5q2lFLoijOK3Or53rVnNgYfTQwDIofcLrG2QtxKQoQ=w60-h60-l90-rj",
width: 60,
height: 60,
),
Thumbnail(
url: "https://lh3.googleusercontent.com/DBaVgQwyirgH4_Rg6w7jQBTP1fyHl5dNMK91dLZD5q2lFLoijOK3Or53rVnNgYfTQwDIofcLrG2QtxKQoQ=w120-h120-l90-rj",
width: 120,
height: 120,
),
],
subscriber_count: Some(140),
)),
Artist(ArtistItem(
id: "UCLcwLJIGBDDvbfq8JERV6Ag",
name: "Black Mamba",
avatar: [
Thumbnail(
url: "https://lh3.googleusercontent.com/qPN6oDatmKgTxytO4b8ScN1qGGMBpsF2_vH9OG1sSDn8Hew28J8vy9y4WNWOJYvSCyHbghIs_B5aGgkJ=w60-h60-l90-rj",
url: "https://lh3.googleusercontent.com/qorCs0oXX4VRdkGM6T6pG9IEugjWfeA9hWoGSzkH427PkRcMi5cJR6Vy4m_FTw-Bhmnj-sAHH54i7PI2=w60-h60-l90-rj",
width: 60,
height: 60,
),
Thumbnail(
url: "https://lh3.googleusercontent.com/qPN6oDatmKgTxytO4b8ScN1qGGMBpsF2_vH9OG1sSDn8Hew28J8vy9y4WNWOJYvSCyHbghIs_B5aGgkJ=w120-h120-l90-rj",
url: "https://lh3.googleusercontent.com/qorCs0oXX4VRdkGM6T6pG9IEugjWfeA9hWoGSzkH427PkRcMi5cJR6Vy4m_FTw-Bhmnj-sAHH54i7PI2=w120-h120-l90-rj",
width: 120,
height: 120,
),
@ -362,86 +511,205 @@ MusicSearchResult(
subscriber_count: Some(9),
)),
Playlist(MusicPlaylistItem(
id: "PLk76iSbFqNJsu_Gozn9SkEXxQ7t-bpXid",
name: "IRMA MIRTILLA Black Mamba",
id: "PLF1nPSf9c6AdlQDKMe5gW0ztSNR4GPrcl",
name: "The Mamba + Maddy Show",
thumbnail: [
Thumbnail(
url: "https://i.ytimg.com/vi/md19pon3B9o/sddefault.jpg?sqp=-oaymwEWCJADEOEBIAQqCghqEJQEGHgg6AJIWg&rs=AMzJL3kR84wE4E_UufGzATfZhAsFWEieaA",
width: 400,
height: 225,
url: "https://i.ytimg.com/pl_c/PLF1nPSf9c6AdlQDKMe5gW0ztSNR4GPrcl/studio_square_thumbnail.jpg?sqp=CM3xtbkG-oaymwEICDwQPCAASFqi85f_AwYI3OWhtAY&rs=AMzJL3m9ePMyYiazdHhl0bve79YoANXNHA",
width: 60,
height: 60,
),
Thumbnail(
url: "https://i.ytimg.com/vi/md19pon3B9o/hq720.jpg?sqp=-oaymwEXCKAGEMIDIAQqCwjVARCqCBh4INgESFo&rs=AMzJL3nxumiGKYWYiiTokZB8M6rwtK5mRw",
width: 800,
height: 450,
url: "https://i.ytimg.com/pl_c/PLF1nPSf9c6AdlQDKMe5gW0ztSNR4GPrcl/studio_square_thumbnail.jpg?sqp=CM3xtbkG-oaymwEICHgQeCAASFqi85f_AwYI3OWhtAY&rs=AMzJL3kBrm54WfDV1202bhc_7NnFtuR2QA",
width: 120,
height: 120,
),
Thumbnail(
url: "https://i.ytimg.com/vi/md19pon3B9o/hq720.jpg?sqp=-oaymwEXCNUGEOADIAQqCwjVARCqCBh4INgESFo&rs=AMzJL3mEU1yvpIHQXYgVnCyXx8Rlzilg6Q",
width: 853,
height: 480,
url: "https://i.ytimg.com/pl_c/PLF1nPSf9c6AdlQDKMe5gW0ztSNR4GPrcl/studio_square_thumbnail.jpg?sqp=CM3xtbkG-oaymwEKCOIBEOIBIABIWqLzl_8DBgjc5aG0Bg&rs=AMzJL3moPZlY6pebVzsucNM0hFJg6E1iOA",
width: 226,
height: 226,
),
Thumbnail(
url: "https://i.ytimg.com/pl_c/PLF1nPSf9c6AdlQDKMe5gW0ztSNR4GPrcl/studio_square_thumbnail.jpg?sqp=CM3xtbkG-oaymwEKCKAEEKAEIABIWqLzl_8DBgjc5aG0Bg&rs=AMzJL3kK8BcSgB8AnNLvrIlo25u5ldoy9A",
width: 544,
height: 544,
),
],
channel: Some(ChannelId(
id: "UCtZaFx5MXZHIh7VTItJK1lQ",
name: "Lajos Fülöp",
)),
channel: None,
track_count: None,
from_ytm: false,
)),
Playlist(MusicPlaylistItem(
id: "PLIL9Q2jz6euDEJZKHd4QaG4iic944_vKY",
name: "Black Mamba",
id: "PL4OEJAvKcBHAe32md9b1c9kEUNi6Ifbqu",
name: "Tooth & Claw Podcast",
thumbnail: [
Thumbnail(
url: "https://yt3.ggpht.com/jsvBK6isPIQ0ERSc1xV6PoaYxbYZqCzqr90lHZNEfUcQL2lP0oNzrdimX8KIBchE6X8myc58zwyS=s192",
width: 192,
height: 192,
url: "https://i.ytimg.com/vi/s7sLjdWb-D4/hqdefault.jpg?sqp=-oaymwExCI4CEI4CIAQqCggAEOADGC0guwJIWvKriqkDFZoCEgg0EDgYFyABLQAAoEE1zcxMPw&rs=AMzJL3m2chIFdDJTYKB7dW_xtWdZcsZg3A",
width: 270,
height: 270,
),
Thumbnail(
url: "https://yt3.ggpht.com/jsvBK6isPIQ0ERSc1xV6PoaYxbYZqCzqr90lHZNEfUcQL2lP0oNzrdimX8KIBchE6X8myc58zwyS=s576",
width: 576,
height: 576,
),
Thumbnail(
url: "https://yt3.ggpht.com/jsvBK6isPIQ0ERSc1xV6PoaYxbYZqCzqr90lHZNEfUcQL2lP0oNzrdimX8KIBchE6X8myc58zwyS=s1200",
width: 1200,
height: 1200,
url: "https://i.ytimg.com/vi/s7sLjdWb-D4/hq720.jpg?sqp=-oaymwElCNAFENAFIAZIWvKriqkDFZoCEgg0EDgYFyABLQAAoEE1zcxMPw&rs=AMzJL3klUEptMBmqrEkpdCBn4cnZ_dnaXw",
width: 720,
height: 720,
),
],
channel: Some(ChannelId(
id: "UCwFT0vvkbtbohtzVbwx7WjQ",
name: "Toshihiko KOMINAMI",
)),
channel: None,
track_count: None,
from_ytm: false,
)),
Playlist(MusicPlaylistItem(
id: "PLinm7-cvTdN7RqadpfNrncUGqkdyKNpn6",
name: "Black Mamba",
id: "PLQ0daRB_QJ_ZkIhLnp_nPeURDM4vQM5yg",
name: "You Should Know Podcast",
thumbnail: [
Thumbnail(
url: "https://yt3.ggpht.com/hj6EywHSUD3UEnRQPHaEjHPC1VRi9UcsrkW8zGiOaXhRGlyNikLw6Iv0VnHTSuo2MlVBiQaskqo=s192",
width: 192,
height: 192,
url: "https://yt3.googleusercontent.com/n4-LrAD8Piik4s3N4OKu3gmotbTnjlJ30twT8IrUVMoNvSHBrCiFppALvovh52qVhvWifoR7jA=w60-c-h60-k-c0x00ffffff-no-l90-rj",
width: 60,
height: 60,
),
Thumbnail(
url: "https://yt3.ggpht.com/hj6EywHSUD3UEnRQPHaEjHPC1VRi9UcsrkW8zGiOaXhRGlyNikLw6Iv0VnHTSuo2MlVBiQaskqo=s576",
width: 576,
height: 576,
url: "https://yt3.googleusercontent.com/n4-LrAD8Piik4s3N4OKu3gmotbTnjlJ30twT8IrUVMoNvSHBrCiFppALvovh52qVhvWifoR7jA=w120-c-h120-k-c0x00ffffff-no-l90-rj",
width: 120,
height: 120,
),
Thumbnail(
url: "https://yt3.ggpht.com/hj6EywHSUD3UEnRQPHaEjHPC1VRi9UcsrkW8zGiOaXhRGlyNikLw6Iv0VnHTSuo2MlVBiQaskqo=s1200",
width: 1200,
height: 1200,
url: "https://yt3.googleusercontent.com/n4-LrAD8Piik4s3N4OKu3gmotbTnjlJ30twT8IrUVMoNvSHBrCiFppALvovh52qVhvWifoR7jA=w226-c-h226-k-c0x00ffffff-no-l90-rj",
width: 226,
height: 226,
),
Thumbnail(
url: "https://yt3.googleusercontent.com/n4-LrAD8Piik4s3N4OKu3gmotbTnjlJ30twT8IrUVMoNvSHBrCiFppALvovh52qVhvWifoR7jA=w544-c-h544-k-c0x00ffffff-no-l90-rj",
width: 544,
height: 544,
),
],
channel: Some(ChannelId(
id: "UCEdZAdnnKqbaHOlv8nM6OtA",
name: "aespa",
)),
channel: None,
track_count: None,
from_ytm: false,
)),
Track(TrackItem(
id: "xd-9D3GzUpo",
name: "MAMBA MENTALITY - Kobe Bryant Motivational Speech",
duration: None,
cover: [
Thumbnail(
url: "https://i.ytimg.com/vi/xd-9D3GzUpo/hqdefault.jpg?sqp=-oaymwEWCOADEI4CIAQqCggAEOADGC0guwJIWg&rs=AMzJL3k3TxrniLSRkQR1LMtpKFsrd-x-Vg",
width: 480,
height: 270,
),
],
artists: [
ArtistId(
id: None,
name: "Discipline Motivation - Best Motivational Speeches By Motiversity",
),
],
artist_id: None,
album: None,
view_count: None,
is_video: true,
track_nr: None,
by_va: false,
)),
Track(TrackItem(
id: "GE0UAdxPTc0",
name: "THE MAMBA MENTALITY - Kobe Bryant Motivational Speech Compilation",
duration: None,
cover: [
Thumbnail(
url: "https://i.ytimg.com/vi/GE0UAdxPTc0/hqdefault.jpg?sqp=-oaymwEWCOADEI4CIAQqCggAEOADGC0guwJIWg&rs=AMzJL3mI3Lbo29pKfU9Qpv3lLY04Fi0yLg",
width: 480,
height: 270,
),
],
artists: [
ArtistId(
id: None,
name: "Motivation Daily by Motiversity",
),
],
artist_id: None,
album: None,
view_count: None,
is_video: true,
track_nr: None,
by_va: false,
)),
Track(TrackItem(
id: "4gi9y3sTrXE",
name: "Mamba Mentality - Kobe Bryant (Motivational Video)",
duration: None,
cover: [
Thumbnail(
url: "https://i.ytimg.com/vi/4gi9y3sTrXE/hqdefault.jpg?sqp=-oaymwEWCOADEI4CIAQqCggAEOADGC0guwJIWg&rs=AMzJL3mi8Id0rBphHeAUx35-u2iDbL2liQ",
width: 480,
height: 270,
),
],
artists: [
ArtistId(
id: None,
name: "Powerful Motivational Speech (Chispa Motivation)",
),
],
artist_id: None,
album: None,
view_count: None,
is_video: true,
track_nr: None,
by_va: false,
)),
User(UserItem(
id: "UCOeTBeQwhOSvNcaZhxM1PUg",
name: "Black Mamba",
handle: Some("@blackmambagyn"),
avatar: [
Thumbnail(
url: "https://yt3.googleusercontent.com/WR8-SnEMVJ-FRQxo0M_nsOO5ceDql9vWSZ8Os4pyrPd6gLE_cLm3K68F6Ozh38gKoYke9FSL1g=w60-c-h60-k-c0x00ffffff-no-l90-rj",
width: 60,
height: 60,
),
Thumbnail(
url: "https://yt3.googleusercontent.com/WR8-SnEMVJ-FRQxo0M_nsOO5ceDql9vWSZ8Os4pyrPd6gLE_cLm3K68F6Ozh38gKoYke9FSL1g=w120-c-h120-k-c0x00ffffff-no-l90-rj",
width: 120,
height: 120,
),
],
)),
User(UserItem(
id: "UCpxiesQUPBb1H-rCNR9vU-w",
name: "BLACK MAMBA",
handle: Some("@BLACKMAMBA-lv4xw"),
avatar: [
Thumbnail(
url: "https://yt3.googleusercontent.com/TunC2xLBq7LNTJHQYyRKFqyUp6QNUl0ZNDo3axPBiDIWMDAsaOvHPi6cHqzdONhLXcFOPCU2FA=w60-c-h60-k-c0x00ffffff-no-l90-rj",
width: 60,
height: 60,
),
Thumbnail(
url: "https://yt3.googleusercontent.com/TunC2xLBq7LNTJHQYyRKFqyUp6QNUl0ZNDo3axPBiDIWMDAsaOvHPi6cHqzdONhLXcFOPCU2FA=w120-c-h120-k-c0x00ffffff-no-l90-rj",
width: 120,
height: 120,
),
],
)),
User(UserItem(
id: "UC04-OP5K9gQ_0x39dERUBQw",
name: "Black Mamba",
handle: Some("@blackmamba1294"),
avatar: [
Thumbnail(
url: "https://yt3.googleusercontent.com/ytc/AIdro_n4zT7h4GQ1HWesFb706jhqzxvNRZlV3oAmF4ug-OU=w60-c-h60-k-c0x00ffffff-no-l90-rj",
width: 60,
height: 60,
),
Thumbnail(
url: "https://yt3.googleusercontent.com/ytc/AIdro_n4zT7h4GQ1HWesFb706jhqzxvNRZlV3oAmF4ug-OU=w120-c-h120-k-c0x00ffffff-no-l90-rj",
width: 120,
height: 120,
),
],
)),
],
ctoken: None,
endpoint: music_search,

View file

@ -4,7 +4,7 @@ expression: map_res.c
---
MusicSearchResult(
items: Paginator(
count: Some(24),
count: Some(27),
items: [
Playlist(MusicPlaylistItem(
id: "RDATficG9wIHJhZGlv",
@ -650,6 +650,57 @@ MusicSearchResult(
track_nr: None,
by_va: false,
)),
User(UserItem(
id: "UCdQrWgvHD9f-caMSn3SS-WQ",
name: "Pop Hist Radio",
handle: Some("@PopHistRadio"),
avatar: [
Thumbnail(
url: "https://yt3.googleusercontent.com/2Py1-HUmfdmgxE3PEL-EdCE4lgGktuklnIOPdBy0N1_51Ne65bW5gJnxcc-eJB9vxhbU4-JQCg=w60-c-h60-k-c0x00ffffff-no-l90-rj",
width: 60,
height: 60,
),
Thumbnail(
url: "https://yt3.googleusercontent.com/2Py1-HUmfdmgxE3PEL-EdCE4lgGktuklnIOPdBy0N1_51Ne65bW5gJnxcc-eJB9vxhbU4-JQCg=w120-c-h120-k-c0x00ffffff-no-l90-rj",
width: 120,
height: 120,
),
],
)),
User(UserItem(
id: "UC-j25R4eGB_pjsaRxF5WOoQ",
name: "HMD RADIO POP",
handle: Some("@hmdradiopop5581"),
avatar: [
Thumbnail(
url: "https://yt3.googleusercontent.com/ytc/AIdro_kuTfeNrt95jHhzzMfQoqFSGWo2cCfUsZ_OHH5zpeM=w60-c-h60-k-c0x00ffffff-no-l90-rj",
width: 60,
height: 60,
),
Thumbnail(
url: "https://yt3.googleusercontent.com/ytc/AIdro_kuTfeNrt95jHhzzMfQoqFSGWo2cCfUsZ_OHH5zpeM=w120-c-h120-k-c0x00ffffff-no-l90-rj",
width: 120,
height: 120,
),
],
)),
User(UserItem(
id: "UCRkEipin-M9fQ12UWxc9UGQ",
name: "MUSIC RADIO",
handle: Some("@musicradio8514"),
avatar: [
Thumbnail(
url: "https://yt3.googleusercontent.com/evLHWABaoAFyXkfnSH95NZ7Fj96AmPW4iHp7pQckNn48PBmKeIvrkgnMtSdwQCOuqLx3tpQIuA=w60-c-h60-k-c0x00ffffff-no-l90-rj",
width: 60,
height: 60,
),
Thumbnail(
url: "https://yt3.googleusercontent.com/evLHWABaoAFyXkfnSH95NZ7Fj96AmPW4iHp7pQckNn48PBmKeIvrkgnMtSdwQCOuqLx3tpQIuA=w120-c-h120-k-c0x00ffffff-no-l90-rj",
width: 120,
height: 120,
),
],
)),
],
ctoken: None,
endpoint: music_search,

View file

@ -1,7 +1,7 @@
use super::{
AlbumItem, ArtistId, ArtistItem, Channel, ChannelId, ChannelItem, ChannelRssVideo, ChannelTag,
MusicArtist, MusicItem, MusicPlaylistItem, PlaylistItem, TrackItem, VideoId, VideoItem,
YouTubeItem,
MusicArtist, MusicItem, MusicPlaylistItem, PlaylistItem, TrackItem, UserItem, VideoId,
VideoItem, YouTubeItem,
};
/// Trait for casting generic YouTube/YouTube music items to a specific kind.
@ -139,6 +139,21 @@ impl From<MusicPlaylistItem> for MusicItem {
}
}
impl FromYtItem for UserItem {
fn from_ytm_item(item: MusicItem) -> Option<Self> {
match item {
MusicItem::User(user) => Some(user),
_ => None,
}
}
}
impl From<UserItem> for MusicItem {
fn from(value: UserItem) -> Self {
Self::User(value)
}
}
impl<T> From<Channel<T>> for ChannelTag {
fn from(channel: Channel<T>) -> Self {
Self {

View file

@ -964,6 +964,19 @@ pub struct ArtistItem {
pub subscriber_count: Option<u64>,
}
/// YouTube Music user item
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct UserItem {
/// Unique YouTube user ID
pub id: String,
/// User name
pub name: String,
/// YouTube channel handle (e.g. `@EEVblog`)
pub handle: Option<String>,
/// User avatar/profile picture
pub avatar: Vec<Thumbnail>,
}
/// YouTube Music artist identifier
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[non_exhaustive]
@ -1140,6 +1153,7 @@ pub enum MusicItem {
Album(AlbumItem),
Artist(ArtistItem),
Playlist(MusicPlaylistItem),
User(UserItem),
}
/// YouTube Music item type
@ -1150,6 +1164,7 @@ pub enum MusicItemType {
Album,
Artist,
Playlist,
User,
}
/// YouTube Music search result

View file

@ -143,10 +143,12 @@ pub trait YtEntity {
///
/// `None` if the entity does not belong to a channel
fn channel_name(&self) -> Option<&str>;
/// YTM item type
fn music_item_type(&self) -> Option<MusicItemType>;
}
macro_rules! yt_entity {
($entity_type:ty) => {
($entity_type:ty, $music_item_type:expr) => {
impl YtEntity for $entity_type {
fn id(&self) -> &str {
&self.id
@ -163,12 +165,16 @@ macro_rules! yt_entity {
fn channel_name(&self) -> Option<&str> {
None
}
fn music_item_type(&self) -> Option<MusicItemType> {
$music_item_type
}
}
};
}
macro_rules! yt_entity_owner {
($entity_type:ty) => {
($entity_type:ty, $music_item_type:expr) => {
impl YtEntity for $entity_type {
fn id(&self) -> &str {
&self.id
@ -185,12 +191,16 @@ macro_rules! yt_entity_owner {
fn channel_name(&self) -> Option<&str> {
Some(&self.channel.name)
}
fn music_item_type(&self) -> Option<MusicItemType> {
Some($music_item_type)
}
}
};
}
macro_rules! yt_entity_owner_opt {
($entity_type:ty) => {
($entity_type:ty, $music_item_type:expr) => {
impl YtEntity for $entity_type {
fn id(&self) -> &str {
&self.id
@ -207,12 +217,16 @@ macro_rules! yt_entity_owner_opt {
fn channel_name(&self) -> Option<&str> {
self.channel.as_ref().map(|c| c.name.as_str())
}
fn music_item_type(&self) -> Option<MusicItemType> {
Some($music_item_type)
}
}
};
}
macro_rules! yt_entity_owner_music {
($entity_type:ty) => {
($entity_type:ty, $music_item_type:expr) => {
impl YtEntity for $entity_type {
fn id(&self) -> &str {
&self.id
@ -233,6 +247,10 @@ macro_rules! yt_entity_owner_music {
self.artists.first().map(|a| a.name.as_str())
}
}
fn music_item_type(&self) -> Option<MusicItemType> {
Some($music_item_type)
}
}
};
}
@ -253,6 +271,10 @@ impl<T> YtEntity for Channel<T> {
fn channel_name(&self) -> Option<&str> {
None
}
fn music_item_type(&self) -> Option<MusicItemType> {
Some(MusicItemType::User)
}
}
impl YtEntity for YouTubeItem {
@ -287,6 +309,14 @@ impl YtEntity for YouTubeItem {
YouTubeItem::Channel(_) => None,
}
}
fn music_item_type(&self) -> Option<MusicItemType> {
Some(match self {
YouTubeItem::Video(_) => MusicItemType::Track,
YouTubeItem::Playlist(_) => MusicItemType::Playlist,
YouTubeItem::Channel(_) => MusicItemType::User,
})
}
}
impl YtEntity for MusicItem {
@ -296,6 +326,7 @@ impl YtEntity for MusicItem {
MusicItem::Album(b) => &b.id,
MusicItem::Artist(a) => &a.id,
MusicItem::Playlist(p) => &p.id,
MusicItem::User(u) => &u.id,
}
}
@ -305,6 +336,7 @@ impl YtEntity for MusicItem {
MusicItem::Album(b) => &b.name,
MusicItem::Artist(a) => &a.name,
MusicItem::Playlist(p) => &p.name,
MusicItem::User(u) => &u.name,
}
}
@ -312,7 +344,7 @@ impl YtEntity for MusicItem {
match self {
MusicItem::Track(t) => t.channel_id(),
MusicItem::Album(b) => b.channel_id(),
MusicItem::Artist(_) => None,
MusicItem::Artist(_) | MusicItem::User(_) => None,
MusicItem::Playlist(p) => p.channel_id(),
}
}
@ -321,29 +353,40 @@ impl YtEntity for MusicItem {
match self {
MusicItem::Track(t) => t.channel_name(),
MusicItem::Album(b) => b.channel_name(),
MusicItem::Artist(_) => None,
MusicItem::Playlist(p) => p.channel_id(),
}
MusicItem::Artist(_) | MusicItem::User(_) => None,
MusicItem::Playlist(p) => p.channel_name(),
}
}
yt_entity_owner_opt! {Playlist}
yt_entity! {ChannelId}
yt_entity_owner! {VideoDetails}
yt_entity! {ChannelTag}
yt_entity! {ChannelRss}
yt_entity! {ChannelRssVideo}
yt_entity_owner_opt! {VideoItem}
yt_entity! {ChannelItem}
yt_entity_owner_opt! {PlaylistItem}
yt_entity! {VideoId}
yt_entity_owner_music! {TrackItem}
yt_entity! {ArtistItem}
yt_entity_owner_music! {AlbumItem}
yt_entity_owner_opt! {MusicPlaylistItem}
yt_entity! {AlbumId}
yt_entity_owner_opt! {MusicPlaylist}
yt_entity_owner_music! {MusicAlbum}
yt_entity! {MusicArtist}
yt_entity! {MusicGenreItem}
yt_entity! {MusicGenre}
fn music_item_type(&self) -> Option<MusicItemType> {
Some(match self {
MusicItem::Track(_) => MusicItemType::Track,
MusicItem::Album(_) => MusicItemType::Album,
MusicItem::Artist(_) => MusicItemType::Artist,
MusicItem::Playlist(_) => MusicItemType::Playlist,
MusicItem::User(_) => MusicItemType::User,
})
}
}
yt_entity_owner_opt! {Playlist, MusicItemType::Playlist}
yt_entity! {ChannelId, Some(MusicItemType::User)}
yt_entity_owner! {VideoDetails, MusicItemType::Track}
yt_entity! {ChannelTag, Some(MusicItemType::User)}
yt_entity! {ChannelRss, Some(MusicItemType::User)}
yt_entity! {ChannelRssVideo, Some(MusicItemType::Track)}
yt_entity_owner_opt! {VideoItem, MusicItemType::Track}
yt_entity! {ChannelItem, Some(MusicItemType::User)}
yt_entity_owner_opt! {PlaylistItem, MusicItemType::Playlist}
yt_entity! {VideoId, Some(MusicItemType::Track)}
yt_entity_owner_music! {TrackItem, MusicItemType::Track}
yt_entity! {ArtistItem, Some(MusicItemType::Artist)}
yt_entity_owner_music! {AlbumItem, MusicItemType::Album}
yt_entity_owner_opt! {MusicPlaylistItem, MusicItemType::Playlist}
yt_entity! {AlbumId, Some(MusicItemType::Album)}
yt_entity_owner_opt! {MusicPlaylist, MusicItemType::Playlist}
yt_entity_owner_music! {MusicAlbum, MusicItemType::Album}
yt_entity! {MusicArtist, Some(MusicItemType::Artist)}
yt_entity! {UserItem, Some(MusicItemType::User)}
yt_entity! {MusicGenreItem, None}
yt_entity! {MusicGenre, None}

View file

@ -236,17 +236,20 @@ pub enum MusicSearchFilter {
YtmPlaylists,
/// Playlists created by YouTube users
CommunityPlaylists,
/// Users
Users,
}
impl MusicSearchFilter {
pub(crate) fn params(self) -> &'static str {
match self {
MusicSearchFilter::Tracks => "EgWKAQIIAWoMEAMQBBAJEA4QChAF",
MusicSearchFilter::Videos => "EgWKAQIQAWoMEAMQBBAJEA4QChAF",
MusicSearchFilter::Albums => "EgWKAQIYAWoMEAMQBBAJEA4QChAF",
MusicSearchFilter::Artists => "EgWKAQIgAWoMEAMQBBAJEA4QChAF",
MusicSearchFilter::YtmPlaylists => "EgeKAQQoADgBagwQAxAEEAkQDhAKEAU%3D",
MusicSearchFilter::CommunityPlaylists => "EgeKAQQoAEABagwQAxAEEAkQDhAKEAU%3D",
MusicSearchFilter::Tracks => "EgWKAQIIAWoQEAMQBBAJEAoQBRAREBAQFQ%3D%3D",
MusicSearchFilter::Videos => "EgWKAQIQAWoQEAMQBBAJEAoQBRAREBAQFQ%3D%3D",
MusicSearchFilter::Albums => "EgWKAQIYAWoQEAMQBBAJEAoQBRAREBAQFQ%3D%3D",
MusicSearchFilter::Artists => "EgWKAQIgAWoQEAMQBBAJEAoQBRAREBAQFQ%3D%3D",
MusicSearchFilter::YtmPlaylists => "EgeKAQQoADgBahIQAxAEEAkQDhAKEAUQERAQEBU%3D",
MusicSearchFilter::CommunityPlaylists => "EgeKAQQoAEABahAQAxAEEAkQChAFEBEQEBAV",
MusicSearchFilter::Users => "EgWKAQJYAWoQEAMQBBAJEAoQBRAREBAQFQ%3D%3D",
}
}
}

File diff suppressed because it is too large Load diff

View file

@ -1902,6 +1902,7 @@ fn check_search_result(items: &[MusicItem]) {
let mut has_albums = false;
let mut has_artists = false;
let mut has_playlists = false;
let mut has_users = false;
for itm in items {
match itm {
@ -1915,6 +1916,7 @@ fn check_search_result(items: &[MusicItem]) {
MusicItem::Album(_) => has_albums = true,
MusicItem::Artist(_) => has_artists = true,
MusicItem::Playlist(_) => has_playlists = true,
MusicItem::User(_) => has_users = true,
}
}
@ -1923,6 +1925,7 @@ fn check_search_result(items: &[MusicItem]) {
assert!(has_albums, "no albums");
assert!(has_artists, "no artists");
assert!(has_playlists, "no playlists");
assert!(has_users, "no users");
}
#[rstest]
@ -2204,6 +2207,30 @@ async fn music_search_playlists_community(rp: RustyPipe) {
assert!(!playlist.from_ytm);
}
#[rstest]
#[tokio::test]
async fn music_search_users(rp: RustyPipe) {
let res = rp
.query()
.music_search_users("amyprincesspink")
.await
.unwrap();
assert_eq!(res.corrected_query, None);
let user = res
.items
.items
.iter()
.find(|u| u.id == "UC-CeCRHc8D47hh8P_9MR5Vg")
.unwrap_or_else(|| {
panic!("could not find user, got {:#?}", &res.items.items);
});
assert_eq!(user.name, "amyprincesspink");
assert_eq!(user.handle.as_deref().unwrap(), "@amyprincesspink");
assert!(!user.avatar.is_empty(), "got no avatar");
}
/// The YouTube Music search sometimes shows genre radio items. They should be skipped.
#[rstest]
#[tokio::test]