feat(musicbrainz): add smart playlist generation backend (Stage 4)
- Add PlaylistRule enum with BandMembers, ArtistGroups, CollaboratorNetwork variants - Add GeneratedPlaylist struct for playlist preview results - Add PlaylistGenerationConfig for customizing track limits - Implement smart_playlist_preview command to generate playlists from MusicBrainz relationships - Implement smart_playlist_resolve_artist helper to find artist MBID by name - Implement smart_playlist_get_available_types to check what playlist types are available - Search Qobuz for member/collaborator tracks and return matching track IDs
This commit is contained in:
1 parent
c97d62393d
commit
04197b98f0
5 files changed
+534
No files matched your search
@@ -20,6 +20,7 @@ pub mod queue;
|
|||||||
pub mod radio;
|
pub mod radio;
|
||||||
pub mod search;
|
pub mod search;
|
||||||
pub mod share;
|
pub mod share;
|
||||||
|
pub mod smart_playlists;
|
||||||
|
|
||||||
pub use audio_backends::*;
|
pub use audio_backends::*;
|
||||||
pub use audio_diagnostics::*;
|
pub use audio_diagnostics::*;
|
||||||
@@ -39,3 +40,4 @@ pub use queue::*;
|
|||||||
pub use radio::*;
|
pub use radio::*;
|
||||||
pub use search::*;
|
pub use search::*;
|
||||||
pub use share::*;
|
pub use share::*;
|
||||||
|
pub use smart_playlists::*;
|
||||||
@@ -0,0 +1,448 @@
|
|||||||
|
//! Smart Playlist Tauri commands
|
||||||
|
//!
|
||||||
|
//! Generates playlists based on MusicBrainz artist relationships
|
||||||
|
|
||||||
|
use tauri::State;
|
||||||
|
|
||||||
|
use crate::api::QobuzClient;
|
||||||
|
use crate::musicbrainz::smart_playlists::{
|
||||||
|
GeneratedPlaylist, PlaylistGenerationConfig, PlaylistRule,
|
||||||
|
};
|
||||||
|
use crate::musicbrainz::MusicBrainzSharedState;
|
||||||
|
use crate::AppState;
|
||||||
|
|
||||||
|
/// Preview a smart playlist (get track list without creating)
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn smart_playlist_preview(
|
||||||
|
rule: PlaylistRule,
|
||||||
|
config: Option<PlaylistGenerationConfig>,
|
||||||
|
mb_state: State<'_, MusicBrainzSharedState>,
|
||||||
|
app_state: State<'_, AppState>,
|
||||||
|
) -> Result<GeneratedPlaylist, String> {
|
||||||
|
let config = config.unwrap_or_default();
|
||||||
|
|
||||||
|
// Check if MusicBrainz is enabled
|
||||||
|
if !mb_state.client.is_enabled().await {
|
||||||
|
return Err("MusicBrainz integration is disabled".to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
match &rule {
|
||||||
|
PlaylistRule::BandMembers {
|
||||||
|
artist_mbid,
|
||||||
|
include_past_members,
|
||||||
|
} => {
|
||||||
|
generate_band_members_playlist(
|
||||||
|
artist_mbid,
|
||||||
|
*include_past_members,
|
||||||
|
&config,
|
||||||
|
&mb_state,
|
||||||
|
&app_state,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
PlaylistRule::ArtistGroups { artist_mbid } => {
|
||||||
|
generate_artist_groups_playlist(artist_mbid, &config, &mb_state, &app_state).await
|
||||||
|
}
|
||||||
|
PlaylistRule::CollaboratorNetwork { artist_mbid, depth } => {
|
||||||
|
generate_collaborator_playlist(artist_mbid, *depth, &config, &mb_state, &app_state)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Generate playlist from band members' solo work
|
||||||
|
async fn generate_band_members_playlist(
|
||||||
|
artist_mbid: &str,
|
||||||
|
include_past_members: bool,
|
||||||
|
config: &PlaylistGenerationConfig,
|
||||||
|
mb_state: &State<'_, MusicBrainzSharedState>,
|
||||||
|
app_state: &State<'_, AppState>,
|
||||||
|
) -> Result<GeneratedPlaylist, String> {
|
||||||
|
// Get relationships
|
||||||
|
let relationships = mb_state
|
||||||
|
.client
|
||||||
|
.get_artist_with_relations(artist_mbid)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
// Get the band name for the playlist title
|
||||||
|
let band_name = relationships.name.clone();
|
||||||
|
|
||||||
|
// Collect member names
|
||||||
|
let mut member_names: Vec<String> = Vec::new();
|
||||||
|
|
||||||
|
if let Some(relations) = &relationships.relations {
|
||||||
|
for relation in relations {
|
||||||
|
if relation.relation_type == "member of band" {
|
||||||
|
if let Some(ref artist) = relation.artist {
|
||||||
|
// Check direction - we want members of this band
|
||||||
|
if relation.direction.as_deref() != Some("backward") {
|
||||||
|
// This is a member
|
||||||
|
let is_past = relation.ended == Some(true);
|
||||||
|
if !is_past || include_past_members {
|
||||||
|
member_names.push(artist.name.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if member_names.is_empty() {
|
||||||
|
return Ok(GeneratedPlaylist {
|
||||||
|
suggested_name: format!("{} Members", band_name),
|
||||||
|
description: "No band members found in MusicBrainz".to_string(),
|
||||||
|
rule: PlaylistRule::BandMembers {
|
||||||
|
artist_mbid: artist_mbid.to_string(),
|
||||||
|
include_past_members,
|
||||||
|
},
|
||||||
|
included_artists: vec![],
|
||||||
|
track_ids: vec![],
|
||||||
|
total_tracks_found: 0,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Search Qobuz for each member and get their tracks
|
||||||
|
let mut all_track_ids: Vec<u64> = Vec::new();
|
||||||
|
let mut found_artists: Vec<String> = Vec::new();
|
||||||
|
|
||||||
|
let client = app_state.client.lock().await;
|
||||||
|
|
||||||
|
for member_name in &member_names {
|
||||||
|
match search_artist_tracks(&client, member_name, config.max_tracks_per_artist).await {
|
||||||
|
Ok(tracks) => {
|
||||||
|
if !tracks.is_empty() {
|
||||||
|
found_artists.push(member_name.clone());
|
||||||
|
all_track_ids.extend(tracks);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
log::warn!("Failed to search tracks for {}: {}", member_name, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if we've hit the limit
|
||||||
|
if all_track_ids.len() >= config.max_total_tracks {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let total_found = all_track_ids.len();
|
||||||
|
all_track_ids.truncate(config.max_total_tracks);
|
||||||
|
|
||||||
|
Ok(GeneratedPlaylist {
|
||||||
|
suggested_name: format!("{} Members", band_name),
|
||||||
|
description: format!(
|
||||||
|
"Solo work by members of {}{}",
|
||||||
|
band_name,
|
||||||
|
if include_past_members {
|
||||||
|
" (including past members)"
|
||||||
|
} else {
|
||||||
|
""
|
||||||
|
}
|
||||||
|
),
|
||||||
|
rule: PlaylistRule::BandMembers {
|
||||||
|
artist_mbid: artist_mbid.to_string(),
|
||||||
|
include_past_members,
|
||||||
|
},
|
||||||
|
included_artists: found_artists,
|
||||||
|
track_ids: all_track_ids,
|
||||||
|
total_tracks_found: total_found,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Generate playlist from groups an artist is/was a member of
|
||||||
|
async fn generate_artist_groups_playlist(
|
||||||
|
artist_mbid: &str,
|
||||||
|
config: &PlaylistGenerationConfig,
|
||||||
|
mb_state: &State<'_, MusicBrainzSharedState>,
|
||||||
|
app_state: &State<'_, AppState>,
|
||||||
|
) -> Result<GeneratedPlaylist, String> {
|
||||||
|
// Get relationships
|
||||||
|
let relationships = mb_state
|
||||||
|
.client
|
||||||
|
.get_artist_with_relations(artist_mbid)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let artist_name = relationships.name.clone();
|
||||||
|
|
||||||
|
// Collect group names
|
||||||
|
let mut group_names: Vec<String> = Vec::new();
|
||||||
|
|
||||||
|
if let Some(relations) = &relationships.relations {
|
||||||
|
for relation in relations {
|
||||||
|
if relation.relation_type == "member of band" {
|
||||||
|
if let Some(ref artist) = relation.artist {
|
||||||
|
// Check direction - "backward" means this artist is a member OF that group
|
||||||
|
if relation.direction.as_deref() == Some("backward") {
|
||||||
|
group_names.push(artist.name.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if group_names.is_empty() {
|
||||||
|
return Ok(GeneratedPlaylist {
|
||||||
|
suggested_name: format!("{}'s Groups", artist_name),
|
||||||
|
description: "No groups found in MusicBrainz".to_string(),
|
||||||
|
rule: PlaylistRule::ArtistGroups {
|
||||||
|
artist_mbid: artist_mbid.to_string(),
|
||||||
|
},
|
||||||
|
included_artists: vec![],
|
||||||
|
track_ids: vec![],
|
||||||
|
total_tracks_found: 0,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Search Qobuz for each group and get their tracks
|
||||||
|
let mut all_track_ids: Vec<u64> = Vec::new();
|
||||||
|
let mut found_artists: Vec<String> = Vec::new();
|
||||||
|
|
||||||
|
let client = app_state.client.lock().await;
|
||||||
|
|
||||||
|
for group_name in &group_names {
|
||||||
|
match search_artist_tracks(&client, group_name, config.max_tracks_per_artist).await {
|
||||||
|
Ok(tracks) => {
|
||||||
|
if !tracks.is_empty() {
|
||||||
|
found_artists.push(group_name.clone());
|
||||||
|
all_track_ids.extend(tracks);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
log::warn!("Failed to search tracks for {}: {}", group_name, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if all_track_ids.len() >= config.max_total_tracks {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let total_found = all_track_ids.len();
|
||||||
|
all_track_ids.truncate(config.max_total_tracks);
|
||||||
|
|
||||||
|
Ok(GeneratedPlaylist {
|
||||||
|
suggested_name: format!("{}'s Groups", artist_name),
|
||||||
|
description: format!("Tracks from groups {} is/was a member of", artist_name),
|
||||||
|
rule: PlaylistRule::ArtistGroups {
|
||||||
|
artist_mbid: artist_mbid.to_string(),
|
||||||
|
},
|
||||||
|
included_artists: found_artists,
|
||||||
|
track_ids: all_track_ids,
|
||||||
|
total_tracks_found: total_found,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Generate playlist from collaborators
|
||||||
|
async fn generate_collaborator_playlist(
|
||||||
|
artist_mbid: &str,
|
||||||
|
depth: u8,
|
||||||
|
config: &PlaylistGenerationConfig,
|
||||||
|
mb_state: &State<'_, MusicBrainzSharedState>,
|
||||||
|
app_state: &State<'_, AppState>,
|
||||||
|
) -> Result<GeneratedPlaylist, String> {
|
||||||
|
// For now, only support depth=1 (direct collaborators)
|
||||||
|
if depth > 1 {
|
||||||
|
return Err("Collaborator depth > 1 not yet supported".to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get relationships
|
||||||
|
let relationships = mb_state
|
||||||
|
.client
|
||||||
|
.get_artist_with_relations(artist_mbid)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let artist_name = relationships.name.clone();
|
||||||
|
|
||||||
|
// Collect collaborator names
|
||||||
|
let mut collaborator_names: Vec<String> = Vec::new();
|
||||||
|
|
||||||
|
if let Some(relations) = &relationships.relations {
|
||||||
|
for relation in relations {
|
||||||
|
if relation.relation_type == "collaboration" {
|
||||||
|
if let Some(ref artist) = relation.artist {
|
||||||
|
collaborator_names.push(artist.name.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if collaborator_names.is_empty() {
|
||||||
|
return Ok(GeneratedPlaylist {
|
||||||
|
suggested_name: format!("{} Collaborations", artist_name),
|
||||||
|
description: "No collaborators found in MusicBrainz".to_string(),
|
||||||
|
rule: PlaylistRule::CollaboratorNetwork {
|
||||||
|
artist_mbid: artist_mbid.to_string(),
|
||||||
|
depth,
|
||||||
|
},
|
||||||
|
included_artists: vec![],
|
||||||
|
track_ids: vec![],
|
||||||
|
total_tracks_found: 0,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Search Qobuz for each collaborator
|
||||||
|
let mut all_track_ids: Vec<u64> = Vec::new();
|
||||||
|
let mut found_artists: Vec<String> = Vec::new();
|
||||||
|
|
||||||
|
let client = app_state.client.lock().await;
|
||||||
|
|
||||||
|
for collab_name in &collaborator_names {
|
||||||
|
match search_artist_tracks(&client, collab_name, config.max_tracks_per_artist).await {
|
||||||
|
Ok(tracks) => {
|
||||||
|
if !tracks.is_empty() {
|
||||||
|
found_artists.push(collab_name.clone());
|
||||||
|
all_track_ids.extend(tracks);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
log::warn!("Failed to search tracks for {}: {}", collab_name, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if all_track_ids.len() >= config.max_total_tracks {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let total_found = all_track_ids.len();
|
||||||
|
all_track_ids.truncate(config.max_total_tracks);
|
||||||
|
|
||||||
|
Ok(GeneratedPlaylist {
|
||||||
|
suggested_name: format!("{} Collaborations", artist_name),
|
||||||
|
description: format!("Tracks from artists who have collaborated with {}", artist_name),
|
||||||
|
rule: PlaylistRule::CollaboratorNetwork {
|
||||||
|
artist_mbid: artist_mbid.to_string(),
|
||||||
|
depth,
|
||||||
|
},
|
||||||
|
included_artists: found_artists,
|
||||||
|
track_ids: all_track_ids,
|
||||||
|
total_tracks_found: total_found,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Search for an artist on Qobuz and get their top tracks
|
||||||
|
async fn search_artist_tracks(
|
||||||
|
client: &QobuzClient,
|
||||||
|
artist_name: &str,
|
||||||
|
max_tracks: usize,
|
||||||
|
) -> Result<Vec<u64>, String> {
|
||||||
|
// Search for the artist
|
||||||
|
let search_results = client
|
||||||
|
.search_artists(artist_name, 5, 0)
|
||||||
|
.await
|
||||||
|
.map_err(|e| format!("Search failed: {}", e))?;
|
||||||
|
|
||||||
|
if search_results.items.is_empty() {
|
||||||
|
return Ok(vec![]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find exact or close match
|
||||||
|
let artist = search_results
|
||||||
|
.items
|
||||||
|
.iter()
|
||||||
|
.find(|a| a.name.to_lowercase() == artist_name.to_lowercase())
|
||||||
|
.or_else(|| search_results.items.first());
|
||||||
|
|
||||||
|
let Some(artist) = artist else {
|
||||||
|
return Ok(vec![]);
|
||||||
|
};
|
||||||
|
|
||||||
|
let artist_id = artist.id;
|
||||||
|
|
||||||
|
// Get the artist's top tracks by searching with artist name
|
||||||
|
let track_results = client
|
||||||
|
.search_tracks(artist_name, max_tracks as u32, 0)
|
||||||
|
.await
|
||||||
|
.map_err(|e| format!("Track search failed: {}", e))?;
|
||||||
|
|
||||||
|
// Filter tracks by this specific artist and extract IDs
|
||||||
|
let track_ids: Vec<u64> = track_results
|
||||||
|
.items
|
||||||
|
.iter()
|
||||||
|
.filter(|t| {
|
||||||
|
// Check if performer matches
|
||||||
|
t.performer
|
||||||
|
.as_ref()
|
||||||
|
.map(|p| p.id == artist_id)
|
||||||
|
.unwrap_or(false)
|
||||||
|
})
|
||||||
|
.map(|t| t.id)
|
||||||
|
.take(max_tracks)
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
Ok(track_ids)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the artist MBID for a given artist name (helper for frontend)
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn smart_playlist_resolve_artist(
|
||||||
|
artist_name: String,
|
||||||
|
mb_state: State<'_, MusicBrainzSharedState>,
|
||||||
|
) -> Result<Option<String>, String> {
|
||||||
|
if !mb_state.client.is_enabled().await {
|
||||||
|
return Err("MusicBrainz integration is disabled".to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
let result = mb_state.client.search_artist(&artist_name).await?;
|
||||||
|
|
||||||
|
// Find best match with high confidence
|
||||||
|
let best_match = result
|
||||||
|
.artists
|
||||||
|
.iter()
|
||||||
|
.find(|a| a.score.unwrap_or(0) >= 90);
|
||||||
|
|
||||||
|
Ok(best_match.map(|a| a.id.clone()))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get available playlist types for an artist
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn smart_playlist_get_available_types(
|
||||||
|
artist_mbid: String,
|
||||||
|
mb_state: State<'_, MusicBrainzSharedState>,
|
||||||
|
) -> Result<Vec<String>, String> {
|
||||||
|
if !mb_state.client.is_enabled().await {
|
||||||
|
return Err("MusicBrainz integration is disabled".to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
let relationships = mb_state
|
||||||
|
.client
|
||||||
|
.get_artist_with_relations(&artist_mbid)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let mut available: Vec<String> = Vec::new();
|
||||||
|
|
||||||
|
if let Some(relations) = &relationships.relations {
|
||||||
|
let mut has_members = false;
|
||||||
|
let mut has_groups = false;
|
||||||
|
let mut has_collaborators = false;
|
||||||
|
|
||||||
|
for relation in relations {
|
||||||
|
match relation.relation_type.as_str() {
|
||||||
|
"member of band" => {
|
||||||
|
if relation.direction.as_deref() == Some("backward") {
|
||||||
|
has_groups = true;
|
||||||
|
} else {
|
||||||
|
has_members = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"collaboration" => {
|
||||||
|
has_collaborators = true;
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if has_members {
|
||||||
|
available.push("band_members".to_string());
|
||||||
|
}
|
||||||
|
if has_groups {
|
||||||
|
available.push("artist_groups".to_string());
|
||||||
|
}
|
||||||
|
if has_collaborators {
|
||||||
|
available.push("collaborator_network".to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(available)
|
||||||
|
}
|
||||||
@@ -775,6 +775,10 @@ pub fn run() {
|
|||||||
commands::listenbrainz_flush_queue,
|
commands::listenbrainz_flush_queue,
|
||||||
commands::listenbrainz_clear_queue,
|
commands::listenbrainz_clear_queue,
|
||||||
commands::listenbrainz_cleanup_queue,
|
commands::listenbrainz_cleanup_queue,
|
||||||
|
// Smart playlist generation commands
|
||||||
|
commands::smart_playlist_preview,
|
||||||
|
commands::smart_playlist_resolve_artist,
|
||||||
|
commands::smart_playlist_get_available_types,
|
||||||
])
|
])
|
||||||
.run(tauri::generate_context!())
|
.run(tauri::generate_context!())
|
||||||
.expect("error while running tauri application");
|
.expect("error while running tauri application");
|
||||||
|
|||||||
@@ -17,6 +17,7 @@
|
|||||||
pub mod cache;
|
pub mod cache;
|
||||||
pub mod client;
|
pub mod client;
|
||||||
pub mod models;
|
pub mod models;
|
||||||
|
pub mod smart_playlists;
|
||||||
|
|
||||||
pub use cache::{CacheStats, MusicBrainzCache, MusicBrainzCacheState};
|
pub use cache::{CacheStats, MusicBrainzCache, MusicBrainzCacheState};
|
||||||
pub use client::{MusicBrainzClient, MusicBrainzConfig};
|
pub use client::{MusicBrainzClient, MusicBrainzConfig};
|
||||||
|
|||||||
@@ -0,0 +1,79 @@
|
|||||||
|
//! Smart Playlist Generation
|
||||||
|
//!
|
||||||
|
//! Generates playlists based on MusicBrainz artist relationships.
|
||||||
|
//! Uses Stage 1 infrastructure to resolve artists and Stage 3 relationships.
|
||||||
|
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
/// Types of smart playlists that can be generated
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
#[serde(rename_all = "snake_case")]
|
||||||
|
pub enum PlaylistRule {
|
||||||
|
/// All solo work by members of a band
|
||||||
|
BandMembers {
|
||||||
|
/// The band's MusicBrainz ID
|
||||||
|
artist_mbid: String,
|
||||||
|
/// Include past members
|
||||||
|
include_past_members: bool,
|
||||||
|
},
|
||||||
|
/// Groups that a solo artist is/was a member of
|
||||||
|
ArtistGroups {
|
||||||
|
/// The artist's MusicBrainz ID
|
||||||
|
artist_mbid: String,
|
||||||
|
},
|
||||||
|
/// Artists who have collaborated with the seed artist
|
||||||
|
CollaboratorNetwork {
|
||||||
|
/// Seed artist's MusicBrainz ID
|
||||||
|
artist_mbid: String,
|
||||||
|
/// How many degrees of separation (1 = direct collaborators only)
|
||||||
|
depth: u8,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A generated smart playlist
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct GeneratedPlaylist {
|
||||||
|
/// Suggested name for the playlist
|
||||||
|
pub suggested_name: String,
|
||||||
|
/// Description of how it was generated
|
||||||
|
pub description: String,
|
||||||
|
/// The rule used to generate this playlist
|
||||||
|
pub rule: PlaylistRule,
|
||||||
|
/// Artist names included in this playlist
|
||||||
|
pub included_artists: Vec<String>,
|
||||||
|
/// Qobuz track IDs found
|
||||||
|
pub track_ids: Vec<u64>,
|
||||||
|
/// Total tracks found (before any limits)
|
||||||
|
pub total_tracks_found: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Result of searching for an artist's tracks on Qobuz
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct ArtistTracksResult {
|
||||||
|
pub artist_name: String,
|
||||||
|
pub qobuz_artist_id: Option<u64>,
|
||||||
|
pub track_ids: Vec<u64>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Configuration for playlist generation
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct PlaylistGenerationConfig {
|
||||||
|
/// Maximum tracks per related artist
|
||||||
|
pub max_tracks_per_artist: usize,
|
||||||
|
/// Maximum total tracks in playlist
|
||||||
|
pub max_total_tracks: usize,
|
||||||
|
/// Only include tracks available in user's region
|
||||||
|
pub filter_unavailable: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for PlaylistGenerationConfig {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
max_tracks_per_artist: 10,
|
||||||
|
max_total_tracks: 100,
|
||||||
|
filter_unavailable: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in new issue
Block a user