Ignore bootleg release groups #247

Merged
wojtek merged 10 commits from 239---ignore-bootleg-release-groups into main 2025-01-03 21:00:22 +01:00
5 changed files with 128 additions and 50 deletions

View File

@ -138,16 +138,16 @@ pub enum AlbumSecondaryType {
} }
/// The album's ownership status. /// The album's ownership status.
pub enum AlbumStatus { pub enum AlbumOwnership {
None, None,
Owned(TrackFormat), Owned(TrackFormat),
} }
impl AlbumStatus { impl AlbumOwnership {
pub fn from_tracks(tracks: &[Track]) -> AlbumStatus { pub fn from_tracks(tracks: &[Track]) -> AlbumOwnership {
match tracks.iter().map(|t| t.quality.format).min() { match tracks.iter().map(|t| t.quality.format).min() {
Some(format) => AlbumStatus::Owned(format), Some(format) => AlbumOwnership::Owned(format),
None => AlbumStatus::None, None => AlbumOwnership::None,
} }
} }
} }
@ -165,8 +165,8 @@ impl Album {
self self
} }
pub fn get_status(&self) -> AlbumStatus { pub fn get_ownership(&self) -> AlbumOwnership {
AlbumStatus::from_tracks(&self.tracks) AlbumOwnership::from_tracks(&self.tracks)
} }
} }

View File

@ -33,17 +33,28 @@ impl BrowseReleaseGroupPage {
pub type SerdeBrowseReleaseGroupPage = BrowseReleaseGroupPage; pub type SerdeBrowseReleaseGroupPage = BrowseReleaseGroupPage;
impl<Http: IMusicBrainzHttp> MusicBrainzClient<Http> { impl<Http: IMusicBrainzHttp> MusicBrainzClient<Http> {
fn browse_release_group_url(
request: &BrowseReleaseGroupRequest,
paging: &PageSettings,
) -> String {
let entity = &request.entity;
let mbid = request.mbid.uuid().as_hyphenated();
let status = request
.release_group_status
.as_ref()
.map(|s| format!("&release-group-status={s}"))
.unwrap_or_default();
let page = ApiDisplay::format_page_settings(paging);
format!("{MB_BASE_URL}/release-group?{entity}={mbid}{status}{page}")
}
pub fn browse_release_group( pub fn browse_release_group(
&mut self, &mut self,
request: &BrowseReleaseGroupRequest, request: &BrowseReleaseGroupRequest,
paging: &PageSettings, paging: &PageSettings,
) -> Result<BrowseReleaseGroupResponse, Error> { ) -> Result<BrowseReleaseGroupResponse, Error> {
let entity = &request.entity; let url = Self::browse_release_group_url(request, paging);
let mbid = request.mbid.uuid().as_hyphenated();
let page = ApiDisplay::format_page_settings(paging);
let url = format!("{MB_BASE_URL}/release-group?{entity}={mbid}{page}");
let response: DeserializeBrowseReleaseGroupResponse = self.http.get(&url)?; let response: DeserializeBrowseReleaseGroupResponse = self.http.get(&url)?;
Ok(response.into()) Ok(response.into())
} }
@ -52,6 +63,7 @@ impl<Http: IMusicBrainzHttp> MusicBrainzClient<Http> {
pub struct BrowseReleaseGroupRequest<'a> { pub struct BrowseReleaseGroupRequest<'a> {
entity: BrowseReleaseGroupRequestEntity, entity: BrowseReleaseGroupRequestEntity,
mbid: &'a Mbid, mbid: &'a Mbid,
release_group_status: Option<BrowseReleaseGroupRequestReleaseGroupStatus>,
} }
enum BrowseReleaseGroupRequestEntity { enum BrowseReleaseGroupRequestEntity {
@ -61,7 +73,21 @@ enum BrowseReleaseGroupRequestEntity {
impl fmt::Display for BrowseReleaseGroupRequestEntity { impl fmt::Display for BrowseReleaseGroupRequestEntity {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self { match self {
BrowseReleaseGroupRequestEntity::Artist => write!(f, "artist"), Self::Artist => write!(f, "artist"),
}
}
}
enum BrowseReleaseGroupRequestReleaseGroupStatus {
WebsiteDefault,
All,
}
impl fmt::Display for BrowseReleaseGroupRequestReleaseGroupStatus {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::WebsiteDefault => write!(f, "website-default"),
Self::All => write!(f, "all"),
} }
} }
} }
@ -71,8 +97,20 @@ impl<'a> BrowseReleaseGroupRequest<'a> {
BrowseReleaseGroupRequest { BrowseReleaseGroupRequest {
entity: BrowseReleaseGroupRequestEntity::Artist, entity: BrowseReleaseGroupRequestEntity::Artist,
mbid, mbid,
release_group_status: None,
} }
} }
pub fn filter_status_website_default(mut self) -> Self {
self.release_group_status =
Some(BrowseReleaseGroupRequestReleaseGroupStatus::WebsiteDefault);
self
}
pub fn filter_status_all(mut self) -> Self {
self.release_group_status = Some(BrowseReleaseGroupRequestReleaseGroupStatus::All);
self
}
} }
#[derive(Debug, PartialEq, Eq)] #[derive(Debug, PartialEq, Eq)]
@ -175,4 +213,38 @@ mod tests {
let result = client.browse_release_group(&request, &paging).unwrap(); let result = client.browse_release_group(&request, &paging).unwrap();
assert_eq!(result, response); assert_eq!(result, response);
} }
#[test]
fn browse_release_group_filter_status() {
type Client = MusicBrainzClient<MockIMusicBrainzHttp>;
let mbid_str = "00000000-0000-0000-0000-000000000000";
let mbid: Mbid = mbid_str.try_into().unwrap();
let paging = PageSettings::with_max_limit();
let request = BrowseReleaseGroupRequest::artist(&mbid);
let url = format!(
"https://musicbrainz.org/ws/2/release-group\
?artist={mbid_str}\
&limit={MB_MAX_PAGE_LIMIT}",
);
assert_eq!(Client::browse_release_group_url(&request, &paging), url);
let request = BrowseReleaseGroupRequest::artist(&mbid).filter_status_website_default();
let url = format!(
"https://musicbrainz.org/ws/2/release-group\
?artist={mbid_str}\
&release-group-status=website-default\
&limit={MB_MAX_PAGE_LIMIT}",
);
assert_eq!(Client::browse_release_group_url(&request, &paging), url);
let request = BrowseReleaseGroupRequest::artist(&mbid).filter_status_all();
let url = format!(
"https://musicbrainz.org/ws/2/release-group\
?artist={mbid_str}\
&release-group-status=all\
&limit={MB_MAX_PAGE_LIMIT}",
);
assert_eq!(Client::browse_release_group_url(&request, &paging), url);
}
} }

View File

@ -99,7 +99,7 @@ impl<Http: IMusicBrainzHttp> IMusicBrainz for MusicBrainz<Http> {
artist: &Mbid, artist: &Mbid,
paging: &mut Option<PageSettings>, paging: &mut Option<PageSettings>,
) -> Result<Vec<Entity<AlbumMeta>>, Error> { ) -> Result<Vec<Entity<AlbumMeta>>, Error> {
let request = BrowseReleaseGroupRequest::artist(artist); let request = BrowseReleaseGroupRequest::artist(artist).filter_status_website_default();
let page = paging.take().unwrap_or_default(); let page = paging.take().unwrap_or_default();
let mb_response = self.client.browse_release_group(&request, &page)?; let mb_response = self.client.browse_release_group(&request, &page)?;

View File

@ -1,5 +1,5 @@
use musichoard::collection::{ use musichoard::collection::{
album::{Album, AlbumStatus}, album::{Album, AlbumOwnership},
artist::Artist, artist::Artist,
track::{Track, TrackFormat}, track::{Track, TrackFormat},
}; };
@ -163,19 +163,19 @@ impl<'a, 'b> AlbumState<'a, 'b> {
"Title: {}\n\ "Title: {}\n\
Date: {}\n\ Date: {}\n\
Type: {}\n\ Type: {}\n\
Status: {}", Ownership: {}",
album.map(|a| a.meta.id.title.as_str()).unwrap_or(""), album.map(|a| a.meta.id.title.as_str()).unwrap_or(""),
album album
.map(|a| UiDisplay::display_date(&a.meta.date, &a.meta.seq)) .map(|a| UiDisplay::display_date(&a.meta.date, &a.meta.seq))
.unwrap_or_default(), .unwrap_or_default(),
album album
.map(|a| UiDisplay::display_type( .map(|a| UiDisplay::display_album_type(
&a.meta.info.primary_type, &a.meta.info.primary_type,
&a.meta.info.secondary_types &a.meta.info.secondary_types
)) ))
.unwrap_or_default(), .unwrap_or_default(),
album album
.map(|a| UiDisplay::display_album_status(&a.get_status())) .map(|a| UiDisplay::display_album_ownership(&a.get_ownership()))
.unwrap_or("") .unwrap_or("")
)); ));
@ -188,9 +188,9 @@ impl<'a, 'b> AlbumState<'a, 'b> {
} }
fn to_list_item(album: &Album) -> ListItem { fn to_list_item(album: &Album) -> ListItem {
let line = match album.get_status() { let line = match album.get_ownership() {
AlbumStatus::None => Line::raw(album.meta.id.title.as_str()), AlbumOwnership::None => Line::raw(album.meta.id.title.as_str()),
AlbumStatus::Owned(format) => match format { AlbumOwnership::Owned(format) => match format {
TrackFormat::Mp3 => Line::styled(album.meta.id.title.as_str(), UiColor::FG_WARN), TrackFormat::Mp3 => Line::styled(album.meta.id.title.as_str(), UiColor::FG_WARN),
TrackFormat::Flac => Line::styled(album.meta.id.title.as_str(), UiColor::FG_GOOD), TrackFormat::Flac => Line::styled(album.meta.id.title.as_str(), UiColor::FG_GOOD),
}, },

View File

@ -1,7 +1,7 @@
use musichoard::collection::{ use musichoard::collection::{
album::{ album::{
AlbumDate, AlbumId, AlbumLibId, AlbumMeta, AlbumPrimaryType, AlbumSecondaryType, AlbumSeq, AlbumDate, AlbumId, AlbumLibId, AlbumMeta, AlbumOwnership, AlbumPrimaryType,
AlbumStatus, AlbumSecondaryType, AlbumSeq,
}, },
artist::ArtistMeta, artist::ArtistMeta,
musicbrainz::{IMusicBrainzRef, MbRefOption}, musicbrainz::{IMusicBrainzRef, MbRefOption},
@ -50,19 +50,19 @@ impl UiDisplay {
} }
} }
pub fn display_type( pub fn display_album_type(
primary: &Option<AlbumPrimaryType>, primary: &Option<AlbumPrimaryType>,
secondary: &Vec<AlbumSecondaryType>, secondary: &Vec<AlbumSecondaryType>,
) -> String { ) -> String {
match primary { match primary {
Some(ref primary) => { Some(ref primary) => {
if secondary.is_empty() { if secondary.is_empty() {
Self::display_primary_type(primary).to_string() Self::display_album_primary_type(primary).to_string()
} else { } else {
format!( format!(
"{} ({})", "{} ({})",
Self::display_primary_type(primary), Self::display_album_primary_type(primary),
Self::display_secondary_types(secondary) Self::display_album_secondary_types(secondary)
) )
} }
} }
@ -70,7 +70,7 @@ impl UiDisplay {
} }
} }
pub fn display_primary_type(value: &AlbumPrimaryType) -> &'static str { pub fn display_album_primary_type(value: &AlbumPrimaryType) -> &'static str {
match value { match value {
AlbumPrimaryType::Album => "Album", AlbumPrimaryType::Album => "Album",
AlbumPrimaryType::Single => "Single", AlbumPrimaryType::Single => "Single",
@ -80,7 +80,7 @@ impl UiDisplay {
} }
} }
pub fn display_secondary_types(values: &Vec<AlbumSecondaryType>) -> String { pub fn display_album_secondary_types(values: &Vec<AlbumSecondaryType>) -> String {
let mut types: Vec<&'static str> = vec![]; let mut types: Vec<&'static str> = vec![];
for value in values { for value in values {
match value { match value {
@ -101,10 +101,10 @@ impl UiDisplay {
types.join(", ") types.join(", ")
} }
pub fn display_album_status(status: &AlbumStatus) -> &'static str { pub fn display_album_ownership(status: &AlbumOwnership) -> &'static str {
match status { match status {
AlbumStatus::None => "None", AlbumOwnership::None => "None",
AlbumStatus::Owned(format) => match format { AlbumOwnership::Owned(format) => match format {
TrackFormat::Mp3 => "MP3", TrackFormat::Mp3 => "MP3",
TrackFormat::Flac => "FLAC", TrackFormat::Flac => "FLAC",
}, },
@ -173,7 +173,7 @@ impl UiDisplay {
"{:010} | {} [{}]", "{:010} | {} [{}]",
UiDisplay::display_album_date(&album.date), UiDisplay::display_album_date(&album.date),
album.id.title, album.id.title,
UiDisplay::display_type(&album.info.primary_type, &album.info.secondary_types), UiDisplay::display_album_type(&album.info.primary_type, &album.info.secondary_types),
) )
} }
@ -224,30 +224,33 @@ mod tests {
} }
#[test] #[test]
fn display_primary_type() { fn display_album_primary_type() {
assert_eq!( assert_eq!(
UiDisplay::display_primary_type(&AlbumPrimaryType::Album), UiDisplay::display_album_primary_type(&AlbumPrimaryType::Album),
"Album" "Album"
); );
assert_eq!( assert_eq!(
UiDisplay::display_primary_type(&AlbumPrimaryType::Single), UiDisplay::display_album_primary_type(&AlbumPrimaryType::Single),
"Single" "Single"
); );
assert_eq!(UiDisplay::display_primary_type(&AlbumPrimaryType::Ep), "EP");
assert_eq!( assert_eq!(
UiDisplay::display_primary_type(&AlbumPrimaryType::Broadcast), UiDisplay::display_album_primary_type(&AlbumPrimaryType::Ep),
"EP"
);
assert_eq!(
UiDisplay::display_album_primary_type(&AlbumPrimaryType::Broadcast),
"Broadcast" "Broadcast"
); );
assert_eq!( assert_eq!(
UiDisplay::display_primary_type(&AlbumPrimaryType::Other), UiDisplay::display_album_primary_type(&AlbumPrimaryType::Other),
"Other" "Other"
); );
} }
#[test] #[test]
fn display_secondary_types() { fn display_album_secondary_types() {
assert_eq!( assert_eq!(
UiDisplay::display_secondary_types(&vec![ UiDisplay::display_album_secondary_types(&vec![
AlbumSecondaryType::Compilation, AlbumSecondaryType::Compilation,
AlbumSecondaryType::Soundtrack, AlbumSecondaryType::Soundtrack,
AlbumSecondaryType::Spokenword, AlbumSecondaryType::Spokenword,
@ -267,14 +270,14 @@ mod tests {
} }
#[test] #[test]
fn display_type() { fn display_album_type() {
assert_eq!(UiDisplay::display_type(&None, &vec![]), ""); assert_eq!(UiDisplay::display_album_type(&None, &vec![]), "");
assert_eq!( assert_eq!(
UiDisplay::display_type(&Some(AlbumPrimaryType::Album), &vec![]), UiDisplay::display_album_type(&Some(AlbumPrimaryType::Album), &vec![]),
"Album" "Album"
); );
assert_eq!( assert_eq!(
UiDisplay::display_type( UiDisplay::display_album_type(
&Some(AlbumPrimaryType::Album), &Some(AlbumPrimaryType::Album),
&vec![AlbumSecondaryType::Live, AlbumSecondaryType::Compilation] &vec![AlbumSecondaryType::Live, AlbumSecondaryType::Compilation]
), ),
@ -283,14 +286,17 @@ mod tests {
} }
#[test] #[test]
fn display_album_status() { fn display_album_ownership() {
assert_eq!(UiDisplay::display_album_status(&AlbumStatus::None), "None");
assert_eq!( assert_eq!(
UiDisplay::display_album_status(&AlbumStatus::Owned(TrackFormat::Mp3)), UiDisplay::display_album_ownership(&AlbumOwnership::None),
"None"
);
assert_eq!(
UiDisplay::display_album_ownership(&AlbumOwnership::Owned(TrackFormat::Mp3)),
"MP3" "MP3"
); );
assert_eq!( assert_eq!(
UiDisplay::display_album_status(&AlbumStatus::Owned(TrackFormat::Flac)), UiDisplay::display_album_ownership(&AlbumOwnership::Owned(TrackFormat::Flac)),
"FLAC" "FLAC"
); );
} }