fix: handling new podcast links
This commit is contained in:
parent
ba06e2c8c8
commit
452f765ffd
11 changed files with 147 additions and 116 deletions
|
|
@ -235,7 +235,6 @@ fn map_artist_page(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
mapper.check_unknown()?;
|
|
||||||
let mut mapped = mapper.group_items();
|
let mut mapped = mapper.group_items();
|
||||||
|
|
||||||
static WIKIPEDIA_REGEX: Lazy<Regex> =
|
static WIKIPEDIA_REGEX: Lazy<Regex> =
|
||||||
|
|
@ -332,7 +331,6 @@ impl MapResponse<Vec<AlbumItem>> for response::MusicArtistAlbums {
|
||||||
mapper.map_response(grid.grid_renderer.items);
|
mapper.map_response(grid.grid_renderer.items);
|
||||||
}
|
}
|
||||||
|
|
||||||
mapper.check_unknown()?;
|
|
||||||
let mapped = mapper.group_items();
|
let mapped = mapper.group_items();
|
||||||
|
|
||||||
Ok(MapResult {
|
Ok(MapResult {
|
||||||
|
|
|
||||||
|
|
@ -98,6 +98,7 @@ impl MapResponse<MusicCharts> for response::MusicCharts {
|
||||||
h.music_carousel_shelf_basic_header_renderer
|
h.music_carousel_shelf_basic_header_renderer
|
||||||
.more_content_button
|
.more_content_button
|
||||||
.and_then(|btn| btn.button_renderer.navigation_endpoint.music_page())
|
.and_then(|btn| btn.button_renderer.navigation_endpoint.music_page())
|
||||||
|
.map(|mp| (mp.typ, mp.id))
|
||||||
}) {
|
}) {
|
||||||
Some((MusicPageType::Playlist, id)) => {
|
Some((MusicPageType::Playlist, id)) => {
|
||||||
// Top music videos (first shelf with associated playlist)
|
// Top music videos (first shelf with associated playlist)
|
||||||
|
|
@ -120,10 +121,6 @@ impl MapResponse<MusicCharts> for response::MusicCharts {
|
||||||
response::music_charts::ItemSection::None => {}
|
response::music_charts::ItemSection::None => {}
|
||||||
});
|
});
|
||||||
|
|
||||||
mapper_top.check_unknown()?;
|
|
||||||
mapper_trending.check_unknown()?;
|
|
||||||
mapper_other.check_unknown()?;
|
|
||||||
|
|
||||||
let mapped_top = mapper_top.conv_items::<TrackItem>();
|
let mapped_top = mapper_top.conv_items::<TrackItem>();
|
||||||
let mut mapped_trending = mapper_trending.conv_items::<TrackItem>();
|
let mut mapped_trending = mapper_trending.conv_items::<TrackItem>();
|
||||||
let mut mapped_other = mapper_other.group_items();
|
let mut mapped_other = mapper_other.group_items();
|
||||||
|
|
|
||||||
|
|
@ -387,9 +387,6 @@ impl MapResponse<MusicRelated> for response::MusicRelated {
|
||||||
_ => {}
|
_ => {}
|
||||||
});
|
});
|
||||||
|
|
||||||
mapper.check_unknown()?;
|
|
||||||
mapper_tracks.check_unknown()?;
|
|
||||||
|
|
||||||
let mapped_tracks = mapper_tracks.conv_items();
|
let mapped_tracks = mapper_tracks.conv_items();
|
||||||
let mut mapped = mapper.group_items();
|
let mut mapped = mapper.group_items();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -75,7 +75,6 @@ impl<T: FromYtItem> MapResponse<Vec<T>> for response::MusicNew {
|
||||||
|
|
||||||
let mut mapper = MusicListMapper::new(lang);
|
let mut mapper = MusicListMapper::new(lang);
|
||||||
mapper.map_response(items);
|
mapper.map_response(items);
|
||||||
mapper.check_unknown()?;
|
|
||||||
|
|
||||||
Ok(mapper.conv_items())
|
Ok(mapper.conv_items())
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -174,7 +174,6 @@ impl MapResponse<MusicPlaylist> for response::MusicPlaylist {
|
||||||
|
|
||||||
let mut mapper = MusicListMapper::new(lang);
|
let mut mapper = MusicListMapper::new(lang);
|
||||||
mapper.map_response(shelf.contents);
|
mapper.map_response(shelf.contents);
|
||||||
mapper.check_unknown()?;
|
|
||||||
let map_res = mapper.conv_items();
|
let map_res = mapper.conv_items();
|
||||||
|
|
||||||
let ctoken = shelf
|
let ctoken = shelf
|
||||||
|
|
|
||||||
|
|
@ -266,7 +266,6 @@ impl MapResponse<MusicSearchResult> for response::MusicSearch {
|
||||||
response::music_search::ItemSection::None => {}
|
response::music_search::ItemSection::None => {}
|
||||||
});
|
});
|
||||||
|
|
||||||
mapper.check_unknown()?;
|
|
||||||
let map_res = mapper.group_items();
|
let map_res = mapper.group_items();
|
||||||
|
|
||||||
Ok(MapResult {
|
Ok(MapResult {
|
||||||
|
|
@ -325,7 +324,6 @@ impl<T: FromYtItem> MapResponse<MusicSearchFiltered<T>> for response::MusicSearc
|
||||||
response::music_search::ItemSection::None => {}
|
response::music_search::ItemSection::None => {}
|
||||||
});
|
});
|
||||||
|
|
||||||
mapper.check_unknown()?;
|
|
||||||
let map_res = mapper.conv_items();
|
let map_res = mapper.conv_items();
|
||||||
|
|
||||||
Ok(MapResult {
|
Ok(MapResult {
|
||||||
|
|
@ -371,7 +369,6 @@ impl MapResponse<MusicSearchSuggestion> for response::MusicSearchSuggestion {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
mapper.check_unknown()?;
|
|
||||||
let map_res = mapper.conv_items();
|
let map_res = mapper.conv_items();
|
||||||
|
|
||||||
Ok(MapResult {
|
Ok(MapResult {
|
||||||
|
|
|
||||||
|
|
@ -2,14 +2,13 @@ use serde::Deserialize;
|
||||||
use serde_with::{rust::deserialize_ignore_any, serde_as, DefaultOnError, VecSkipError};
|
use serde_with::{rust::deserialize_ignore_any, serde_as, DefaultOnError, VecSkipError};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
error::ExtractionError,
|
|
||||||
model::{
|
model::{
|
||||||
self, traits::FromYtItem, AlbumId, AlbumItem, AlbumType, ArtistId, ArtistItem, ChannelId,
|
self, traits::FromYtItem, AlbumId, AlbumItem, AlbumType, ArtistId, ArtistItem, ChannelId,
|
||||||
MusicItem, MusicItemType, MusicPlaylistItem, TrackItem,
|
MusicItem, MusicItemType, MusicPlaylistItem, TrackItem,
|
||||||
},
|
},
|
||||||
param::Language,
|
param::Language,
|
||||||
serializer::{
|
serializer::{
|
||||||
text::{Text, TextComponents},
|
text::{Text, TextComponent, TextComponents},
|
||||||
MapResult,
|
MapResult,
|
||||||
},
|
},
|
||||||
util::{self, dictionary},
|
util::{self, dictionary},
|
||||||
|
|
@ -17,7 +16,7 @@ use crate::{
|
||||||
|
|
||||||
use super::{
|
use super::{
|
||||||
url_endpoint::{
|
url_endpoint::{
|
||||||
BrowseEndpointWrap, MusicPageType, MusicVideoType, NavigationEndpoint, PageType,
|
BrowseEndpointWrap, MusicPage, MusicPageType, MusicVideoType, NavigationEndpoint, PageType,
|
||||||
},
|
},
|
||||||
ContentsRenderer, MusicContinuationData, Thumbnails, ThumbnailsWrap,
|
ContentsRenderer, MusicContinuationData, Thumbnails, ThumbnailsWrap,
|
||||||
};
|
};
|
||||||
|
|
@ -434,8 +433,6 @@ pub(crate) struct MusicListMapper {
|
||||||
search_suggestion: bool,
|
search_suggestion: bool,
|
||||||
items: Vec<MusicItem>,
|
items: Vec<MusicItem>,
|
||||||
warnings: Vec<String>,
|
warnings: Vec<String>,
|
||||||
/// True if unknown items were mapped
|
|
||||||
has_unknown: bool,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
|
|
@ -456,7 +453,6 @@ impl MusicListMapper {
|
||||||
search_suggestion: false,
|
search_suggestion: false,
|
||||||
items: Vec::new(),
|
items: Vec::new(),
|
||||||
warnings: Vec::new(),
|
warnings: Vec::new(),
|
||||||
has_unknown: false,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -469,7 +465,6 @@ impl MusicListMapper {
|
||||||
search_suggestion: true,
|
search_suggestion: true,
|
||||||
items: Vec::new(),
|
items: Vec::new(),
|
||||||
warnings: Vec::new(),
|
warnings: Vec::new(),
|
||||||
has_unknown: false,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -483,7 +478,6 @@ impl MusicListMapper {
|
||||||
search_suggestion: false,
|
search_suggestion: false,
|
||||||
items: Vec::new(),
|
items: Vec::new(),
|
||||||
warnings: Vec::new(),
|
warnings: Vec::new(),
|
||||||
has_unknown: false,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -497,7 +491,6 @@ impl MusicListMapper {
|
||||||
search_suggestion: false,
|
search_suggestion: false,
|
||||||
items: Vec::new(),
|
items: Vec::new(),
|
||||||
warnings: Vec::new(),
|
warnings: Vec::new(),
|
||||||
has_unknown: false,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -545,55 +538,44 @@ impl MusicListMapper {
|
||||||
.thumbnails
|
.thumbnails
|
||||||
.first();
|
.first();
|
||||||
|
|
||||||
let pt_id = item
|
let music_page = item
|
||||||
.navigation_endpoint
|
.navigation_endpoint
|
||||||
.and_then(NavigationEndpoint::music_page)
|
.and_then(NavigationEndpoint::music_page)
|
||||||
.or_else(|| {
|
.or_else(|| {
|
||||||
c1.and_then(|c1| {
|
c1.and_then(|c1| {
|
||||||
c1.renderer.text.0.into_iter().next().and_then(|t| match t {
|
c1.renderer
|
||||||
crate::serializer::text::TextComponent::Video {
|
.text
|
||||||
video_id, vtype, ..
|
.0
|
||||||
} => Some((MusicPageType::Track { vtype }, video_id)),
|
.into_iter()
|
||||||
crate::serializer::text::TextComponent::Browse {
|
.next()
|
||||||
page_type,
|
.and_then(TextComponent::music_page)
|
||||||
browse_id,
|
|
||||||
..
|
|
||||||
} => Some((page_type.into(), browse_id)),
|
|
||||||
_ => None,
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
.or_else(|| {
|
.or_else(|| {
|
||||||
item.playlist_item_data.map(|d| {
|
item.playlist_item_data.map(|d| MusicPage {
|
||||||
(
|
id: d.video_id,
|
||||||
MusicPageType::Track {
|
typ: MusicPageType::Track {
|
||||||
vtype: MusicVideoType::from_is_video(
|
vtype: MusicVideoType::from_is_video(
|
||||||
self.album.is_none()
|
self.album.is_none()
|
||||||
&& !first_tn
|
&& !first_tn.map(|tn| tn.height == tn.width).unwrap_or_default(),
|
||||||
.map(|tn| tn.height == tn.width)
|
),
|
||||||
.unwrap_or_default(),
|
},
|
||||||
),
|
|
||||||
},
|
|
||||||
d.video_id,
|
|
||||||
)
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
.or_else(|| {
|
.or_else(|| {
|
||||||
first_tn.and_then(|tn| {
|
first_tn.and_then(|tn| {
|
||||||
util::video_id_from_thumbnail_url(&tn.url).map(|id| {
|
util::video_id_from_thumbnail_url(&tn.url).map(|id| MusicPage {
|
||||||
(
|
id,
|
||||||
MusicPageType::Track {
|
typ: MusicPageType::Track {
|
||||||
vtype: MusicVideoType::from_is_video(
|
vtype: MusicVideoType::from_is_video(
|
||||||
self.album.is_none() && tn.width != tn.height,
|
self.album.is_none() && tn.width != tn.height,
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
id,
|
|
||||||
)
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
|
|
||||||
match pt_id {
|
match music_page.map(|mp| (mp.typ, mp.id)) {
|
||||||
// Track
|
// Track
|
||||||
Some((MusicPageType::Track { vtype }, id)) => {
|
Some((MusicPageType::Track { vtype }, id)) => {
|
||||||
let title = title.ok_or_else(|| format!("track {id}: could not get title"))?;
|
let title = title.ok_or_else(|| format!("track {id}: could not get title"))?;
|
||||||
|
|
@ -852,10 +834,6 @@ impl MusicListMapper {
|
||||||
}
|
}
|
||||||
// Tracks were already handled above
|
// Tracks were already handled above
|
||||||
MusicPageType::Track { .. } => unreachable!(),
|
MusicPageType::Track { .. } => unreachable!(),
|
||||||
MusicPageType::Unknown => {
|
|
||||||
self.has_unknown = true;
|
|
||||||
Ok(None)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
None => {
|
None => {
|
||||||
|
|
@ -875,12 +853,12 @@ impl MusicListMapper {
|
||||||
let subtitle_p2 = subtitle_parts.next();
|
let subtitle_p2 = subtitle_parts.next();
|
||||||
|
|
||||||
match item.navigation_endpoint.music_page() {
|
match item.navigation_endpoint.music_page() {
|
||||||
Some((page_type, id)) => match page_type {
|
Some(music_page) => match music_page.typ {
|
||||||
MusicPageType::Track { vtype } => {
|
MusicPageType::Track { vtype } => {
|
||||||
let (artists, by_va) = map_artists(subtitle_p1);
|
let (artists, by_va) = map_artists(subtitle_p1);
|
||||||
|
|
||||||
self.items.push(MusicItem::Track(TrackItem {
|
self.items.push(MusicItem::Track(TrackItem {
|
||||||
id,
|
id: music_page.id,
|
||||||
name: item.title,
|
name: item.title,
|
||||||
duration: None,
|
duration: None,
|
||||||
cover: item.thumbnail_renderer.into(),
|
cover: item.thumbnail_renderer.into(),
|
||||||
|
|
@ -910,7 +888,7 @@ impl MusicListMapper {
|
||||||
});
|
});
|
||||||
|
|
||||||
self.items.push(MusicItem::Artist(ArtistItem {
|
self.items.push(MusicItem::Artist(ArtistItem {
|
||||||
id,
|
id: music_page.id,
|
||||||
name: item.title,
|
name: item.title,
|
||||||
avatar: item.thumbnail_renderer.into(),
|
avatar: item.thumbnail_renderer.into(),
|
||||||
subscriber_count,
|
subscriber_count,
|
||||||
|
|
@ -947,12 +925,15 @@ impl MusicListMapper {
|
||||||
(Vec::new(), true)
|
(Vec::new(), true)
|
||||||
}
|
}
|
||||||
_ => {
|
_ => {
|
||||||
return Err(format!("could not parse subtitle of album {id}"));
|
return Err(format!(
|
||||||
|
"could not parse subtitle of album {}",
|
||||||
|
music_page.id
|
||||||
|
));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
self.items.push(MusicItem::Album(AlbumItem {
|
self.items.push(MusicItem::Album(AlbumItem {
|
||||||
id,
|
id: music_page.id,
|
||||||
name: item.title,
|
name: item.title,
|
||||||
cover: item.thumbnail_renderer.into(),
|
cover: item.thumbnail_renderer.into(),
|
||||||
artist_id: artists.first().and_then(|a| a.id.clone()),
|
artist_id: artists.first().and_then(|a| a.id.clone()),
|
||||||
|
|
@ -974,7 +955,7 @@ impl MusicListMapper {
|
||||||
.and_then(|p| p.0.into_iter().find_map(|c| ChannelId::try_from(c).ok()));
|
.and_then(|p| p.0.into_iter().find_map(|c| ChannelId::try_from(c).ok()));
|
||||||
|
|
||||||
self.items.push(MusicItem::Playlist(MusicPlaylistItem {
|
self.items.push(MusicItem::Playlist(MusicPlaylistItem {
|
||||||
id,
|
id: music_page.id,
|
||||||
name: item.title,
|
name: item.title,
|
||||||
thumbnail: item.thumbnail_renderer.into(),
|
thumbnail: item.thumbnail_renderer.into(),
|
||||||
channel,
|
channel,
|
||||||
|
|
@ -984,10 +965,6 @@ impl MusicListMapper {
|
||||||
Ok(Some(MusicItemType::Playlist))
|
Ok(Some(MusicItemType::Playlist))
|
||||||
}
|
}
|
||||||
MusicPageType::None => Ok(None),
|
MusicPageType::None => Ok(None),
|
||||||
MusicPageType::Unknown => {
|
|
||||||
self.has_unknown = true;
|
|
||||||
Ok(None)
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
None => Err("could not determine item type".to_owned()),
|
None => Err("could not determine item type".to_owned()),
|
||||||
}
|
}
|
||||||
|
|
@ -1009,7 +986,7 @@ impl MusicListMapper {
|
||||||
let subtitle_p4 = subtitle_parts.next();
|
let subtitle_p4 = subtitle_parts.next();
|
||||||
|
|
||||||
let item_type = match card.on_tap.music_page() {
|
let item_type = match card.on_tap.music_page() {
|
||||||
Some((page_type, id)) => match page_type {
|
Some(music_page) => match music_page.typ {
|
||||||
MusicPageType::Artist => {
|
MusicPageType::Artist => {
|
||||||
let subscriber_count = subtitle_p2.and_then(|p| {
|
let subscriber_count = subtitle_p2.and_then(|p| {
|
||||||
util::parse_large_numstr_or_warn(
|
util::parse_large_numstr_or_warn(
|
||||||
|
|
@ -1020,7 +997,7 @@ impl MusicListMapper {
|
||||||
});
|
});
|
||||||
|
|
||||||
self.items.push(MusicItem::Artist(ArtistItem {
|
self.items.push(MusicItem::Artist(ArtistItem {
|
||||||
id,
|
id: music_page.id,
|
||||||
name: card.title,
|
name: card.title,
|
||||||
avatar: card.thumbnail.into(),
|
avatar: card.thumbnail.into(),
|
||||||
subscriber_count,
|
subscriber_count,
|
||||||
|
|
@ -1034,7 +1011,7 @@ impl MusicListMapper {
|
||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
|
|
||||||
self.items.push(MusicItem::Album(AlbumItem {
|
self.items.push(MusicItem::Album(AlbumItem {
|
||||||
id,
|
id: music_page.id,
|
||||||
name: card.title,
|
name: card.title,
|
||||||
cover: card.thumbnail.into(),
|
cover: card.thumbnail.into(),
|
||||||
artist_id: artists.first().and_then(|a| a.id.clone()),
|
artist_id: artists.first().and_then(|a| a.id.clone()),
|
||||||
|
|
@ -1050,7 +1027,7 @@ impl MusicListMapper {
|
||||||
let (artists, by_va) = map_artists(subtitle_p3);
|
let (artists, by_va) = map_artists(subtitle_p3);
|
||||||
|
|
||||||
self.items.push(MusicItem::Track(TrackItem {
|
self.items.push(MusicItem::Track(TrackItem {
|
||||||
id,
|
id: music_page.id,
|
||||||
name: card.title,
|
name: card.title,
|
||||||
duration: None,
|
duration: None,
|
||||||
cover: card.thumbnail.into(),
|
cover: card.thumbnail.into(),
|
||||||
|
|
@ -1087,7 +1064,7 @@ impl MusicListMapper {
|
||||||
};
|
};
|
||||||
|
|
||||||
self.items.push(MusicItem::Track(TrackItem {
|
self.items.push(MusicItem::Track(TrackItem {
|
||||||
id,
|
id: music_page.id,
|
||||||
name: card.title,
|
name: card.title,
|
||||||
duration,
|
duration,
|
||||||
cover: card.thumbnail.into(),
|
cover: card.thumbnail.into(),
|
||||||
|
|
@ -1113,7 +1090,7 @@ impl MusicListMapper {
|
||||||
subtitle_p3.and_then(|p| util::parse_numeric(p.first_str()).ok());
|
subtitle_p3.and_then(|p| util::parse_numeric(p.first_str()).ok());
|
||||||
|
|
||||||
self.items.push(MusicItem::Playlist(MusicPlaylistItem {
|
self.items.push(MusicItem::Playlist(MusicPlaylistItem {
|
||||||
id,
|
id: music_page.id,
|
||||||
name: card.title,
|
name: card.title,
|
||||||
thumbnail: card.thumbnail.into(),
|
thumbnail: card.thumbnail.into(),
|
||||||
channel,
|
channel,
|
||||||
|
|
@ -1123,10 +1100,6 @@ impl MusicListMapper {
|
||||||
Some(MusicItemType::Playlist)
|
Some(MusicItemType::Playlist)
|
||||||
}
|
}
|
||||||
MusicPageType::None => None,
|
MusicPageType::None => None,
|
||||||
MusicPageType::Unknown => {
|
|
||||||
self.has_unknown = true;
|
|
||||||
None
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
None => {
|
None => {
|
||||||
self.warnings
|
self.warnings
|
||||||
|
|
@ -1201,20 +1174,6 @@ impl MusicListMapper {
|
||||||
warnings: self.warnings,
|
warnings: self.warnings,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Sometimes the YT Music API returns responses containing unknown items.
|
|
||||||
///
|
|
||||||
/// In this case, the response data is likely missing some fields, which leads to
|
|
||||||
/// parsing errors and wrong data being extracted.
|
|
||||||
///
|
|
||||||
/// Therefore it is safest to discard such responses and retry the request.
|
|
||||||
pub fn check_unknown(&self) -> Result<(), ExtractionError> {
|
|
||||||
if self.has_unknown {
|
|
||||||
Err(ExtractionError::InvalidData("unknown YTM items".into()))
|
|
||||||
} else {
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Map TextComponents containing artist names to a list of artists and a 'Various Artists' flag
|
/// Map TextComponents containing artist names to a list of artists and a 'Various Artists' flag
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use serde_with::{serde_as, DefaultOnError};
|
use serde_with::{serde_as, DefaultOnError};
|
||||||
|
|
||||||
use crate::model::UrlTarget;
|
use crate::{model::UrlTarget, util};
|
||||||
|
|
||||||
/// navigation/resolve_url response model
|
/// navigation/resolve_url response model
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Debug, Deserialize)]
|
||||||
|
|
@ -185,6 +185,10 @@ pub(crate) enum PageType {
|
||||||
Channel,
|
Channel,
|
||||||
#[serde(rename = "MUSIC_PAGE_TYPE_PLAYLIST", alias = "WEB_PAGE_TYPE_PLAYLIST")]
|
#[serde(rename = "MUSIC_PAGE_TYPE_PLAYLIST", alias = "WEB_PAGE_TYPE_PLAYLIST")]
|
||||||
Playlist,
|
Playlist,
|
||||||
|
#[serde(rename = "MUSIC_PAGE_TYPE_PODCAST_SHOW_DETAIL_PAGE")]
|
||||||
|
Podcast,
|
||||||
|
#[serde(rename = "MUSIC_PAGE_TYPE_NON_MUSIC_AUDIO_TRACK_PAGE")]
|
||||||
|
Episode,
|
||||||
#[default]
|
#[default]
|
||||||
Unknown,
|
Unknown,
|
||||||
}
|
}
|
||||||
|
|
@ -195,6 +199,13 @@ impl PageType {
|
||||||
PageType::Artist | PageType::Channel => Some(UrlTarget::Channel { id }),
|
PageType::Artist | PageType::Channel => Some(UrlTarget::Channel { id }),
|
||||||
PageType::Album => Some(UrlTarget::Album { id }),
|
PageType::Album => Some(UrlTarget::Album { id }),
|
||||||
PageType::Playlist => Some(UrlTarget::Playlist { id }),
|
PageType::Playlist => Some(UrlTarget::Playlist { id }),
|
||||||
|
PageType::Podcast => Some(UrlTarget::Playlist {
|
||||||
|
id: util::strip_prefix(&id, util::PODCAST_PLAYLIST_PREFIX),
|
||||||
|
}),
|
||||||
|
PageType::Episode => Some(UrlTarget::Video {
|
||||||
|
id: util::strip_prefix(&id, util::PODCAST_EPISODE_PREFIX),
|
||||||
|
start_time: 0,
|
||||||
|
}),
|
||||||
PageType::Unknown => None,
|
PageType::Unknown => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -206,7 +217,6 @@ pub(crate) enum MusicPageType {
|
||||||
Album,
|
Album,
|
||||||
Playlist,
|
Playlist,
|
||||||
Track { vtype: MusicVideoType },
|
Track { vtype: MusicVideoType },
|
||||||
Unknown,
|
|
||||||
None,
|
None,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -215,16 +225,40 @@ impl From<PageType> for MusicPageType {
|
||||||
match t {
|
match t {
|
||||||
PageType::Artist => MusicPageType::Artist,
|
PageType::Artist => MusicPageType::Artist,
|
||||||
PageType::Album => MusicPageType::Album,
|
PageType::Album => MusicPageType::Album,
|
||||||
PageType::Playlist => MusicPageType::Playlist,
|
PageType::Playlist | PageType::Podcast => MusicPageType::Playlist,
|
||||||
PageType::Channel => MusicPageType::None,
|
PageType::Channel | PageType::Unknown => MusicPageType::None,
|
||||||
PageType::Unknown => MusicPageType::Unknown,
|
PageType::Episode => MusicPageType::Track {
|
||||||
|
vtype: MusicVideoType::Episode,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) struct MusicPage {
|
||||||
|
pub id: String,
|
||||||
|
pub typ: MusicPageType,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MusicPage {
|
||||||
|
/// Create a new MusicPage object, applying the required ID fixes when
|
||||||
|
/// mapping a browse link
|
||||||
|
pub fn from_browse(mut id: String, typ: PageType) -> Self {
|
||||||
|
if typ == PageType::Podcast {
|
||||||
|
id = util::strip_prefix(&id, util::PODCAST_PLAYLIST_PREFIX);
|
||||||
|
} else if typ == PageType::Episode && id.len() == 15 {
|
||||||
|
id = util::strip_prefix(&id, util::PODCAST_EPISODE_PREFIX);
|
||||||
|
}
|
||||||
|
|
||||||
|
Self {
|
||||||
|
id,
|
||||||
|
typ: typ.into(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl NavigationEndpoint {
|
impl NavigationEndpoint {
|
||||||
/// Get the YouTube Music page and id from a browse/watch endpoint
|
/// Get the YouTube Music page and id from a browse/watch endpoint
|
||||||
pub(crate) fn music_page(self) -> Option<(MusicPageType, String)> {
|
pub(crate) fn music_page(self) -> Option<MusicPage> {
|
||||||
match self {
|
match self {
|
||||||
NavigationEndpoint::Watch { watch_endpoint } => {
|
NavigationEndpoint::Watch { watch_endpoint } => {
|
||||||
if watch_endpoint
|
if watch_endpoint
|
||||||
|
|
@ -233,17 +267,20 @@ impl NavigationEndpoint {
|
||||||
.unwrap_or_default()
|
.unwrap_or_default()
|
||||||
{
|
{
|
||||||
// Genre radios (e.g. "pop radio") will be skipped
|
// Genre radios (e.g. "pop radio") will be skipped
|
||||||
Some((MusicPageType::None, watch_endpoint.video_id))
|
Some(MusicPage {
|
||||||
|
id: watch_endpoint.video_id,
|
||||||
|
typ: MusicPageType::None,
|
||||||
|
})
|
||||||
} else {
|
} else {
|
||||||
Some((
|
Some(MusicPage {
|
||||||
MusicPageType::Track {
|
id: watch_endpoint.video_id,
|
||||||
|
typ: MusicPageType::Track {
|
||||||
vtype: watch_endpoint
|
vtype: watch_endpoint
|
||||||
.watch_endpoint_music_supported_configs
|
.watch_endpoint_music_supported_configs
|
||||||
.watch_endpoint_music_config
|
.watch_endpoint_music_config
|
||||||
.music_video_type,
|
.music_video_type,
|
||||||
},
|
},
|
||||||
watch_endpoint.video_id,
|
})
|
||||||
))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
NavigationEndpoint::Browse {
|
NavigationEndpoint::Browse {
|
||||||
|
|
@ -251,9 +288,9 @@ impl NavigationEndpoint {
|
||||||
} => browse_endpoint
|
} => browse_endpoint
|
||||||
.browse_endpoint_context_supported_configs
|
.browse_endpoint_context_supported_configs
|
||||||
.map(|config| {
|
.map(|config| {
|
||||||
(
|
MusicPage::from_browse(
|
||||||
config.browse_endpoint_context_music_config.page_type.into(),
|
|
||||||
browse_endpoint.browse_id,
|
browse_endpoint.browse_id,
|
||||||
|
config.browse_endpoint_context_music_config.page_type,
|
||||||
)
|
)
|
||||||
}),
|
}),
|
||||||
NavigationEndpoint::Url { .. } => None,
|
NavigationEndpoint::Url { .. } => None,
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,9 @@ use serde::{Deserialize, Deserializer};
|
||||||
use serde_with::{serde_as, DeserializeAs, VecSkipError};
|
use serde_with::{serde_as, DeserializeAs, VecSkipError};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
client::response::url_endpoint::{MusicVideoType, NavigationEndpoint, PageType},
|
client::response::url_endpoint::{
|
||||||
|
MusicPage, MusicPageType, MusicVideoType, NavigationEndpoint, PageType,
|
||||||
|
},
|
||||||
model::UrlTarget,
|
model::UrlTarget,
|
||||||
util,
|
util,
|
||||||
};
|
};
|
||||||
|
|
@ -419,6 +421,23 @@ impl TextComponent {
|
||||||
| TextComponent::Text { text } => text,
|
| TextComponent::Text { text } => text,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn music_page(self) -> Option<MusicPage> {
|
||||||
|
match self {
|
||||||
|
TextComponent::Video {
|
||||||
|
video_id, vtype, ..
|
||||||
|
} => Some(MusicPage {
|
||||||
|
id: video_id,
|
||||||
|
typ: MusicPageType::Track { vtype },
|
||||||
|
}),
|
||||||
|
TextComponent::Browse {
|
||||||
|
page_type,
|
||||||
|
browse_id,
|
||||||
|
..
|
||||||
|
} => Some(MusicPage::from_browse(browse_id, page_type)),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<TextComponent> for String {
|
impl From<TextComponent> for String {
|
||||||
|
|
|
||||||
|
|
@ -41,6 +41,8 @@ pub const DOT_SEPARATOR: &str = " • ";
|
||||||
pub const VARIOUS_ARTISTS: &str = "Various Artists";
|
pub const VARIOUS_ARTISTS: &str = "Various Artists";
|
||||||
pub const PLAYLIST_ID_ALBUM_PREFIX: &str = "OLAK";
|
pub const PLAYLIST_ID_ALBUM_PREFIX: &str = "OLAK";
|
||||||
pub const ARTIST_DISCOGRAPHY_PREFIX: &str = "MPAD";
|
pub const ARTIST_DISCOGRAPHY_PREFIX: &str = "MPAD";
|
||||||
|
pub const PODCAST_PLAYLIST_PREFIX: &str = "MPSP";
|
||||||
|
pub const PODCAST_EPISODE_PREFIX: &str = "MPED";
|
||||||
|
|
||||||
const CONTENT_PLAYBACK_NONCE_ALPHABET: &[u8; 64] =
|
const CONTENT_PLAYBACK_NONCE_ALPHABET: &[u8; 64] =
|
||||||
b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
|
b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
|
||||||
|
|
@ -474,6 +476,11 @@ pub fn country_from_name(name: &str) -> Option<Country> {
|
||||||
.map(|i| COUNTRIES[i])
|
.map(|i| COUNTRIES[i])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Strip prefix from string if presend
|
||||||
|
pub fn strip_prefix(s: &str, prefix: &str) -> String {
|
||||||
|
s.strip_prefix(prefix).unwrap_or(s).to_string()
|
||||||
|
}
|
||||||
|
|
||||||
/// An iterator over the chars in a string (in str format)
|
/// An iterator over the chars in a string (in str format)
|
||||||
pub struct SplitChar<'a> {
|
pub struct SplitChar<'a> {
|
||||||
txt: &'a str,
|
txt: &'a str,
|
||||||
|
|
|
||||||
|
|
@ -1664,7 +1664,9 @@ fn music_search_tracks(rp: RustyPipe, unlocalized: bool) {
|
||||||
.items
|
.items
|
||||||
.iter()
|
.iter()
|
||||||
.find(|a| a.id == "BL-aIpCLWnU")
|
.find(|a| a.id == "BL-aIpCLWnU")
|
||||||
.unwrap();
|
.unwrap_or_else(|| {
|
||||||
|
panic!("could not find track, got {:#?}", &res.items.items);
|
||||||
|
});
|
||||||
|
|
||||||
assert_eq!(track.name, "Black Mamba");
|
assert_eq!(track.name, "Black Mamba");
|
||||||
assert!(!track.cover.is_empty(), "got no cover");
|
assert!(!track.cover.is_empty(), "got no cover");
|
||||||
|
|
@ -1699,7 +1701,9 @@ fn music_search_videos(rp: RustyPipe, unlocalized: bool) {
|
||||||
.items
|
.items
|
||||||
.iter()
|
.iter()
|
||||||
.find(|a| a.id == "ZeerrnuLi5E")
|
.find(|a| a.id == "ZeerrnuLi5E")
|
||||||
.unwrap();
|
.unwrap_or_else(|| {
|
||||||
|
panic!("could not find video, got {:#?}", &res.items.items);
|
||||||
|
});
|
||||||
|
|
||||||
assert_eq!(track.name, "Black Mamba");
|
assert_eq!(track.name, "Black Mamba");
|
||||||
assert!(!track.cover.is_empty(), "got no cover");
|
assert!(!track.cover.is_empty(), "got no cover");
|
||||||
|
|
@ -1739,7 +1743,12 @@ fn music_search_episode(rp: RustyPipe, #[case] videos: bool) {
|
||||||
.tracks
|
.tracks
|
||||||
};
|
};
|
||||||
|
|
||||||
let track = &tracks.iter().find(|a| a.id == "Zq_-LDy7AgE").unwrap();
|
let track = &tracks
|
||||||
|
.iter()
|
||||||
|
.find(|a| a.id == "Zq_-LDy7AgE")
|
||||||
|
.unwrap_or_else(|| {
|
||||||
|
panic!("could not find episode, got {:#?}", &tracks);
|
||||||
|
});
|
||||||
|
|
||||||
assert_eq!(track.artists.len(), 1);
|
assert_eq!(track.artists.len(), 1);
|
||||||
let track_artist = &track.artists[0];
|
let track_artist = &track.artists[0];
|
||||||
|
|
@ -1805,7 +1814,14 @@ fn music_search_albums(
|
||||||
) {
|
) {
|
||||||
let res = tokio_test::block_on(rp.query().music_search_albums(query)).unwrap();
|
let res = tokio_test::block_on(rp.query().music_search_albums(query)).unwrap();
|
||||||
|
|
||||||
let album = &res.items.items.iter().find(|a| a.id == id).unwrap();
|
let album = &res
|
||||||
|
.items
|
||||||
|
.items
|
||||||
|
.iter()
|
||||||
|
.find(|a| a.id == id)
|
||||||
|
.unwrap_or_else(|| {
|
||||||
|
panic!("could not find album, got {:#?}", &res.items.items);
|
||||||
|
});
|
||||||
assert_eq!(album.name, name);
|
assert_eq!(album.name, name);
|
||||||
|
|
||||||
assert_eq!(album.artists.len(), 1);
|
assert_eq!(album.artists.len(), 1);
|
||||||
|
|
@ -1836,7 +1852,9 @@ fn music_search_artists(rp: RustyPipe, unlocalized: bool) {
|
||||||
.items
|
.items
|
||||||
.iter()
|
.iter()
|
||||||
.find(|a| a.id == "UCIh4j8fXWf2U0ro0qnGU8Mg")
|
.find(|a| a.id == "UCIh4j8fXWf2U0ro0qnGU8Mg")
|
||||||
.unwrap();
|
.unwrap_or_else(|| {
|
||||||
|
panic!("could not find artist, got {:#?}", &res.items.items);
|
||||||
|
});
|
||||||
if unlocalized {
|
if unlocalized {
|
||||||
assert_eq!(artist.name, "Namika");
|
assert_eq!(artist.name, "Namika");
|
||||||
}
|
}
|
||||||
|
|
@ -1871,7 +1889,9 @@ fn music_search_playlists(rp: RustyPipe, unlocalized: bool) {
|
||||||
.items
|
.items
|
||||||
.iter()
|
.iter()
|
||||||
.find(|p| p.id == "RDCLAK5uy_nLtxizvEMkzYQUrA-bFf6MnBeR4bGYWUQ")
|
.find(|p| p.id == "RDCLAK5uy_nLtxizvEMkzYQUrA-bFf6MnBeR4bGYWUQ")
|
||||||
.expect("no playlist");
|
.unwrap_or_else(|| {
|
||||||
|
panic!("could not find playlist, got {:#?}", &res.items.items);
|
||||||
|
});
|
||||||
|
|
||||||
if unlocalized {
|
if unlocalized {
|
||||||
assert_eq!(playlist.name, "Today's Rock Hits");
|
assert_eq!(playlist.name, "Today's Rock Hits");
|
||||||
|
|
@ -1901,7 +1921,9 @@ fn music_search_playlists_community(rp: RustyPipe) {
|
||||||
.items
|
.items
|
||||||
.iter()
|
.iter()
|
||||||
.find(|p| p.id == "PLMC9KNkIncKtGvr2kFRuXBVmBev6cAJ2u")
|
.find(|p| p.id == "PLMC9KNkIncKtGvr2kFRuXBVmBev6cAJ2u")
|
||||||
.expect("no playlist");
|
.unwrap_or_else(|| {
|
||||||
|
panic!("could not find playlist, got {:#?}", &res.items.items);
|
||||||
|
});
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
playlist.name,
|
playlist.name,
|
||||||
|
|
|
||||||
Reference in a new issue