Compare commits
2 commits
220e335314
...
9b6c952fd3
Author | SHA1 | Date | |
---|---|---|---|
9b6c952fd3 | |||
ea16a3e74f |
4 changed files with 57 additions and 22 deletions
|
@ -345,8 +345,9 @@ impl MusicListMapper {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn map_item(&mut self, item: MusicResponseItem) -> Result<MusicEntityType, String> {
|
fn map_item(&mut self, item: MusicResponseItem) -> Result<Option<MusicEntityType>, String> {
|
||||||
match item {
|
match item {
|
||||||
|
// List item
|
||||||
MusicResponseItem::MusicResponsiveListItemRenderer(item) => {
|
MusicResponseItem::MusicResponsiveListItemRenderer(item) => {
|
||||||
let mut columns = item.flex_columns.into_iter();
|
let mut columns = item.flex_columns.into_iter();
|
||||||
let title = columns.next().map(|col| col.renderer.text.to_string());
|
let title = columns.next().map(|col| col.renderer.text.to_string());
|
||||||
|
@ -356,19 +357,27 @@ impl MusicListMapper {
|
||||||
match item.navigation_endpoint {
|
match item.navigation_endpoint {
|
||||||
// Artist / Album / Playlist
|
// Artist / Album / Playlist
|
||||||
Some(ne) => {
|
Some(ne) => {
|
||||||
let (page_type, id) = ne
|
|
||||||
.music_page()
|
|
||||||
.ok_or_else(|| "could not get navigation endpoint".to_owned())?;
|
|
||||||
|
|
||||||
let title =
|
|
||||||
title.ok_or_else(|| format!("track {}: could not get title", id))?;
|
|
||||||
|
|
||||||
let mut subtitle_parts = c2
|
let mut subtitle_parts = c2
|
||||||
.ok_or_else(|| format!("item {}: could not get subtitle", id))?
|
.ok_or_else(|| "could not get subtitle".to_owned())?
|
||||||
.renderer
|
.renderer
|
||||||
.text
|
.text
|
||||||
.split(util::DOT_SEPARATOR)
|
.split(util::DOT_SEPARATOR)
|
||||||
.into_iter();
|
.into_iter();
|
||||||
|
|
||||||
|
let (page_type, id) = match ne.music_page() {
|
||||||
|
Some(music_page) => music_page,
|
||||||
|
None => {
|
||||||
|
// Ignore radio items
|
||||||
|
if subtitle_parts.len() == 1 {
|
||||||
|
return Ok(None);
|
||||||
|
}
|
||||||
|
return Err("invalid navigation endpoint".to_string());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let title =
|
||||||
|
title.ok_or_else(|| format!("track {}: could not get title", id))?;
|
||||||
|
|
||||||
let subtitle_p1 = subtitle_parts.next();
|
let subtitle_p1 = subtitle_parts.next();
|
||||||
let subtitle_p2 = subtitle_parts.next();
|
let subtitle_p2 = subtitle_parts.next();
|
||||||
let subtitle_p3 = subtitle_parts.next();
|
let subtitle_p3 = subtitle_parts.next();
|
||||||
|
@ -385,7 +394,7 @@ impl MusicListMapper {
|
||||||
avatar: item.thumbnail.into(),
|
avatar: item.thumbnail.into(),
|
||||||
subscriber_count,
|
subscriber_count,
|
||||||
}));
|
}));
|
||||||
Ok(MusicEntityType::Artist)
|
Ok(Some(MusicEntityType::Artist))
|
||||||
}
|
}
|
||||||
PageType::Album => {
|
PageType::Album => {
|
||||||
let album_type = subtitle_p1
|
let album_type = subtitle_p1
|
||||||
|
@ -406,7 +415,7 @@ impl MusicListMapper {
|
||||||
year,
|
year,
|
||||||
by_va,
|
by_va,
|
||||||
}));
|
}));
|
||||||
Ok(MusicEntityType::Album)
|
Ok(Some(MusicEntityType::Album))
|
||||||
}
|
}
|
||||||
PageType::Playlist => {
|
PageType::Playlist => {
|
||||||
// Part 1 may be the "Playlist" label
|
// Part 1 may be the "Playlist" label
|
||||||
|
@ -433,11 +442,11 @@ impl MusicListMapper {
|
||||||
track_count,
|
track_count,
|
||||||
from_ytm,
|
from_ytm,
|
||||||
}));
|
}));
|
||||||
Ok(MusicEntityType::Playlist)
|
Ok(Some(MusicEntityType::Playlist))
|
||||||
}
|
}
|
||||||
PageType::Channel => {
|
PageType::Channel => {
|
||||||
// There may be broken YT channels from the artist search. They can be skipped.
|
// There may be broken YT channels from the artist search. They can be skipped.
|
||||||
Ok(MusicEntityType::Artist)
|
Ok(None)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -553,10 +562,11 @@ impl MusicListMapper {
|
||||||
is_video,
|
is_video,
|
||||||
track_nr,
|
track_nr,
|
||||||
}));
|
}));
|
||||||
Ok(MusicEntityType::Track)
|
Ok(Some(MusicEntityType::Track))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// Tile
|
||||||
MusicResponseItem::MusicTwoRowItemRenderer(item) => {
|
MusicResponseItem::MusicTwoRowItemRenderer(item) => {
|
||||||
let mut subtitle_parts = item.subtitle.split(util::DOT_SEPARATOR).into_iter();
|
let mut subtitle_parts = item.subtitle.split(util::DOT_SEPARATOR).into_iter();
|
||||||
let subtitle_p1 = subtitle_parts.next();
|
let subtitle_p1 = subtitle_parts.next();
|
||||||
|
@ -581,7 +591,7 @@ impl MusicListMapper {
|
||||||
is_video: true,
|
is_video: true,
|
||||||
track_nr: None,
|
track_nr: None,
|
||||||
}));
|
}));
|
||||||
Ok(MusicEntityType::Track)
|
Ok(Some(MusicEntityType::Track))
|
||||||
}
|
}
|
||||||
// Artist / Album / Playlist
|
// Artist / Album / Playlist
|
||||||
None => {
|
None => {
|
||||||
|
@ -636,7 +646,7 @@ impl MusicListMapper {
|
||||||
year,
|
year,
|
||||||
by_va,
|
by_va,
|
||||||
}));
|
}));
|
||||||
Ok(MusicEntityType::Album)
|
Ok(Some(MusicEntityType::Album))
|
||||||
}
|
}
|
||||||
PageType::Playlist => {
|
PageType::Playlist => {
|
||||||
let from_ytm = subtitle_p2
|
let from_ytm = subtitle_p2
|
||||||
|
@ -657,7 +667,7 @@ impl MusicListMapper {
|
||||||
track_count,
|
track_count,
|
||||||
from_ytm,
|
from_ytm,
|
||||||
}));
|
}));
|
||||||
Ok(MusicEntityType::Playlist)
|
Ok(Some(MusicEntityType::Playlist))
|
||||||
}
|
}
|
||||||
PageType::Artist => {
|
PageType::Artist => {
|
||||||
let subscriber_count = subtitle_p1.and_then(|p| {
|
let subscriber_count = subtitle_p1.and_then(|p| {
|
||||||
|
@ -670,7 +680,7 @@ impl MusicListMapper {
|
||||||
avatar: item.thumbnail_renderer.into(),
|
avatar: item.thumbnail_renderer.into(),
|
||||||
subscriber_count,
|
subscriber_count,
|
||||||
}));
|
}));
|
||||||
Ok(MusicEntityType::Artist)
|
Ok(Some(MusicEntityType::Artist))
|
||||||
}
|
}
|
||||||
PageType::Channel => {
|
PageType::Channel => {
|
||||||
Err(format!("channel items unsupported. id: {}", id))
|
Err(format!("channel items unsupported. id: {}", id))
|
||||||
|
@ -691,7 +701,12 @@ impl MusicListMapper {
|
||||||
res.c
|
res.c
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.for_each(|item| match self.map_item(item) {
|
.for_each(|item| match self.map_item(item) {
|
||||||
Ok(t) => etype = Some(t),
|
Ok(Some(et)) => {
|
||||||
|
if etype.is_none() {
|
||||||
|
etype = Some(et);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(None) => {}
|
||||||
Err(e) => self.warnings.push(e),
|
Err(e) => self.warnings.push(e),
|
||||||
});
|
});
|
||||||
etype
|
etype
|
||||||
|
|
|
@ -72,6 +72,19 @@ impl RustyPipeQuery {
|
||||||
Ok(UrlTarget::Playlist { id })
|
Ok(UrlTarget::Playlist { id })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// Album or channel
|
||||||
|
Some("browse") => match path_split.next() {
|
||||||
|
Some(id) => {
|
||||||
|
if util::CHANNEL_ID_REGEX.is_match(id).unwrap_or_default() {
|
||||||
|
Ok(UrlTarget::Channel { id: id.to_owned() })
|
||||||
|
} else if util::ALBUM_ID_REGEX.is_match(id).unwrap_or_default() {
|
||||||
|
Ok(UrlTarget::Album { id: id.to_owned() })
|
||||||
|
} else {
|
||||||
|
Err(Error::Other("invalid url: no browse id".into()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None => Err(Error::Other("invalid url: invalid browse id".into())),
|
||||||
|
},
|
||||||
// Channel vanity URL or youtu.be shortlink
|
// Channel vanity URL or youtu.be shortlink
|
||||||
Some(mut id) => {
|
Some(mut id) => {
|
||||||
if id == "c" || id == "user" {
|
if id == "c" || id == "user" {
|
||||||
|
|
|
@ -68,9 +68,7 @@ impl UrlTarget {
|
||||||
format!("{}/playlist?list={}", yt_host, id)
|
format!("{}/playlist?list={}", yt_host, id)
|
||||||
}
|
}
|
||||||
UrlTarget::Album { id } => {
|
UrlTarget::Album { id } => {
|
||||||
// The official album URLs use the playlist ID
|
format!("https://music.youtube.com/browse/{}", id)
|
||||||
// This looks weird, but it works
|
|
||||||
format!("{}/channel/{}", yt_host, id)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1189,6 +1189,8 @@ async fn search_suggestion_empty() {
|
||||||
// Both a video ID and a channel name + video time param => returns video
|
// Both a video ID and a channel name + video time param => returns video
|
||||||
#[case("https://piped.mha.fi/dQw4w9WgXcQ?t=0", UrlTarget::Video {id: "dQw4w9WgXcQ".to_owned(), start_time: 0})]
|
#[case("https://piped.mha.fi/dQw4w9WgXcQ?t=0", UrlTarget::Video {id: "dQw4w9WgXcQ".to_owned(), start_time: 0})]
|
||||||
#[case("https://music.youtube.com/playlist?list=OLAK5uy_k0yFrZlFRgCf3rLPza-lkRmCrtLPbK9pE", UrlTarget::Album {id: "MPREb_GyH43gCvdM5".to_owned()})]
|
#[case("https://music.youtube.com/playlist?list=OLAK5uy_k0yFrZlFRgCf3rLPza-lkRmCrtLPbK9pE", UrlTarget::Album {id: "MPREb_GyH43gCvdM5".to_owned()})]
|
||||||
|
#[case("https://music.youtube.com/browse/MPREb_GyH43gCvdM5", UrlTarget::Album {id: "MPREb_GyH43gCvdM5".to_owned()})]
|
||||||
|
#[case("https://music.youtube.com/browse/UC5I2hjZYiW9gZPVkvzM8_Cw", UrlTarget::Channel {id: "UC5I2hjZYiW9gZPVkvzM8_Cw".to_owned()})]
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn resolve_url(#[case] url: &str, #[case] expect: UrlTarget) {
|
async fn resolve_url(#[case] url: &str, #[case] expect: UrlTarget) {
|
||||||
let rp = RustyPipe::builder().strict().build();
|
let rp = RustyPipe::builder().strict().build();
|
||||||
|
@ -1746,6 +1748,13 @@ async fn music_search_playlists_community() {
|
||||||
assert!(!playlist.from_ytm);
|
assert!(!playlist.from_ytm);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The YouTube Music search sometimes shows genre radio items. They should be skipped.
|
||||||
|
#[tokio::test]
|
||||||
|
async fn music_search_genre_radio() {
|
||||||
|
let rp = RustyPipe::builder().strict().build();
|
||||||
|
rp.query().music_search("pop radio").await.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
//#TESTUTIL
|
//#TESTUTIL
|
||||||
|
|
||||||
/// Assert equality within 10% margin
|
/// Assert equality within 10% margin
|
||||||
|
|
Loading…
Reference in a new issue