Files
LibreTunes/src/api/artists.rs
Ethan Girouard 856f66c918
Some checks failed
Push Workflows / rustfmt (push) Successful in 9s
Push Workflows / mdbook (push) Successful in 22s
Push Workflows / mdbook-server (push) Successful in 4m20s
Push Workflows / docs (push) Successful in 6m25s
Push Workflows / clippy (push) Successful in 8m6s
Push Workflows / test (push) Successful in 11m5s
Push Workflows / docker-build (push) Failing after 12m5s
Push Workflows / leptos-test (push) Successful in 12m20s
Push Workflows / nix-build (push) Successful in 16m42s
Push Workflows / build (push) Successful in 13m12s
Paths refactoring
Update API and components to use LocalPath/WebPath
Remove img_fallback
Exchange some frontend and backend types
Other path-related changes
Update image/song uploading to use random paths
2025-10-11 19:04:55 -04:00

256 lines
8.4 KiB
Rust

use leptos::prelude::*;
use cfg_if::cfg_if;
use crate::models::frontend;
use crate::util::error::*;
use crate::util::serverfn_client::Client;
cfg_if! {
if #[cfg(feature = "ssr")] {
use diesel::prelude::*;
use std::collections::HashMap;
use crate::models::backend::Artist;
use crate::models::backend::Album;
use crate::models::backend::NewArtist;
use crate::util::backend_state::BackendState;
use crate::util::paths::*;
}
}
/// Add an artist to the database
///
/// # Arguments
///
/// * `artist_name` - The name of the artist to add
///
/// # Returns
/// * `Result<(), Box<dyn Error>>` - A empty result if successful, or an error
///
#[server(endpoint = "artists/add-artist", client = Client)]
pub async fn add_artist(artist_name: String) -> BackendResult<()> {
use crate::schema::artists::dsl::*;
let new_artist = NewArtist {
name: artist_name,
image_path: None,
};
let mut db_conn = BackendState::get().await?.get_db_conn()?;
diesel::insert_into(artists)
.values(&new_artist)
.execute(&mut db_conn)
.context("Error inserting new artist into database")?;
Ok(())
}
#[server(endpoint = "artists/get", client = Client)]
pub async fn get_artist_by_id(artist_id: i32) -> BackendResult<Option<frontend::Artist>> {
use crate::schema::artists::dsl::*;
let mut db_conn = BackendState::get().await?.get_db_conn()?;
let artist = artists
.filter(id.eq(artist_id))
.first::<Artist>(&mut db_conn)
.optional()
.context("Error loading artist from database")?;
let Some(artist) = artist else {
return Ok(None);
};
let artist = frontend::Artist {
id: artist.id,
name: artist.name,
image_path: LocalPath::to_web_path_or_placeholder(artist.image_path),
};
Ok(Some(artist))
}
#[server(endpoint = "artists/top_songs", client = Client)]
pub async fn top_songs_by_artist(
artist_id: i32,
limit: Option<i64>,
) -> BackendResult<Vec<(frontend::Song, i64)>> {
use crate::api::auth::get_user;
use crate::models::backend::Song;
use crate::schema::*;
let user_id = get_user().await.context("Error getting logged-in user")?.id;
let mut db_conn = BackendState::get().await?.get_db_conn()?;
let song_play_counts: Vec<(i32, i64)> = if let Some(limit) = limit {
song_history::table
.group_by(song_history::song_id)
.select((song_history::song_id, diesel::dsl::count(song_history::id)))
.left_join(song_artists::table.on(song_artists::song_id.eq(song_history::song_id)))
.filter(song_artists::artist_id.eq(artist_id))
.order_by(diesel::dsl::count(song_history::id).desc())
.left_join(songs::table.on(songs::id.eq(song_history::song_id)))
.limit(limit)
.load(&mut db_conn)?
} else {
song_history::table
.group_by(song_history::song_id)
.select((song_history::song_id, diesel::dsl::count(song_history::id)))
.left_join(song_artists::table.on(song_artists::song_id.eq(song_history::song_id)))
.filter(song_artists::artist_id.eq(artist_id))
.order_by(diesel::dsl::count(song_history::id).desc())
.left_join(songs::table.on(songs::id.eq(song_history::song_id)))
.load(&mut db_conn)?
};
let song_play_counts: HashMap<i32, i64> = song_play_counts.into_iter().collect();
let top_song_ids: Vec<i32> = song_play_counts.keys().copied().collect();
let top_songs: Vec<(
Song,
Option<Album>,
Option<Artist>,
Option<(i32, i32)>,
Option<(i32, i32)>,
)> = songs::table
.filter(songs::id.eq_any(top_song_ids))
.left_join(albums::table.on(songs::album_id.eq(albums::id.nullable())))
.left_join(
song_artists::table
.inner_join(artists::table)
.on(songs::id.eq(song_artists::song_id)),
)
.left_join(
song_likes::table.on(songs::id
.eq(song_likes::song_id)
.and(song_likes::user_id.eq(user_id))),
)
.left_join(
song_dislikes::table.on(songs::id
.eq(song_dislikes::song_id)
.and(song_dislikes::user_id.eq(user_id))),
)
.select((
songs::all_columns,
albums::all_columns.nullable(),
artists::all_columns.nullable(),
song_likes::all_columns.nullable(),
song_dislikes::all_columns.nullable(),
))
.load(&mut db_conn)?;
let mut top_songs_map: HashMap<i32, (frontend::Song, i64)> =
HashMap::with_capacity(top_songs.len());
for (song, album, artist, like, dislike) in top_songs {
if let Some((stored_songdata, _)) = top_songs_map.get_mut(&song.id) {
// If the song is already in the map, update the artists
if let Some(artist) = artist {
stored_songdata.artists.push(artist);
}
} else {
let like_dislike = match (like, dislike) {
(Some(_), Some(_)) => Some((true, true)),
(Some(_), None) => Some((true, false)),
(None, Some(_)) => Some((false, true)),
_ => None,
};
let image_path = song.image_web_path_or_placeholder(album.as_ref());
let song_path = song
.storage_path
.to_web_path(AssetType::Audio)
.context(format!(
"Error converting audio path to web path for song {} (id: {})",
song.title.clone(),
song.id
))?;
let songdata = frontend::Song {
id: song.id,
title: song.title,
artists: artist.map(|artist| vec![artist]).unwrap_or_default(),
album,
track: song.track,
duration: song.duration,
release_date: song.release_date,
song_path,
image_path,
like_dislike,
added_date: song.added_date,
};
let plays = song_play_counts
.get(&song.id)
.ok_or(BackendError::InternalError(
"Song id not found in history counts",
))?;
top_songs_map.insert(song.id, (songdata, *plays));
}
}
let mut top_songs: Vec<(frontend::Song, i64)> = top_songs_map.into_values().collect();
top_songs.sort_by(|(_, plays1), (_, plays2)| plays2.cmp(plays1));
Ok(top_songs)
}
#[server(endpoint = "artists/albums", client = Client)]
pub async fn albums_by_artist(
artist_id: i32,
limit: Option<i64>,
) -> BackendResult<Vec<frontend::Album>> {
use crate::schema::*;
let mut db_conn = BackendState::get().await?.get_db_conn()?;
let album_ids = albums::table
.left_join(album_artists::table)
.filter(album_artists::artist_id.eq(artist_id))
.order_by(albums::release_date.desc())
.select(albums::id);
let album_ids = if let Some(limit) = limit {
album_ids.limit(limit).into_boxed()
} else {
album_ids.into_boxed()
};
let mut albums_map: HashMap<i32, frontend::Album> = HashMap::new();
let album_artists: Vec<(Album, Artist)> = albums::table
.filter(albums::id.eq_any(album_ids))
.inner_join(
album_artists::table
.inner_join(artists::table)
.on(albums::id.eq(album_artists::album_id)),
)
.select((albums::all_columns, artists::all_columns))
.load(&mut db_conn)
.context("Error loading album artists from database")?;
for (album, artist) in album_artists {
if let Some(stored_album) = albums_map.get_mut(&album.id) {
stored_album.artists.push(artist);
} else {
let albumdata = frontend::Album {
id: album.id,
title: album.title,
artists: vec![artist],
release_date: album.release_date,
image_path: LocalPath::to_web_path_or_placeholder(album.image_path),
};
albums_map.insert(album.id, albumdata);
}
}
let mut albums: Vec<frontend::Album> = albums_map.into_values().collect();
albums.sort_by(|a1, a2| a2.release_date.cmp(&a1.release_date));
Ok(albums)
}