Rewrite error handling and display
Some checks failed
Push Workflows / rustfmt (push) Successful in 11s
Push Workflows / mdbook (push) Successful in 16s
Push Workflows / mdbook-server (push) Successful in 4m20s
Push Workflows / docs (push) Successful in 5m44s
Push Workflows / clippy (push) Successful in 7m48s
Push Workflows / test (push) Successful in 11m14s
Push Workflows / leptos-test (push) Failing after 11m33s
Push Workflows / build (push) Successful in 12m53s
Push Workflows / nix-build (push) Successful in 16m54s
Push Workflows / docker-build (push) Successful in 17m40s
Some checks failed
Push Workflows / rustfmt (push) Successful in 11s
Push Workflows / mdbook (push) Successful in 16s
Push Workflows / mdbook-server (push) Successful in 4m20s
Push Workflows / docs (push) Successful in 5m44s
Push Workflows / clippy (push) Successful in 7m48s
Push Workflows / test (push) Successful in 11m14s
Push Workflows / leptos-test (push) Failing after 11m33s
Push Workflows / build (push) Successful in 12m53s
Push Workflows / nix-build (push) Successful in 16m54s
Push Workflows / docker-build (push) Successful in 17m40s
This commit is contained in:
@ -1,4 +1,5 @@
|
||||
use crate::models::frontend;
|
||||
use crate::util::error::*;
|
||||
use crate::util::serverfn_client::Client;
|
||||
use leptos::prelude::*;
|
||||
|
||||
@ -7,14 +8,13 @@ use cfg_if::cfg_if;
|
||||
cfg_if! {
|
||||
if #[cfg(feature = "ssr")] {
|
||||
use diesel::prelude::*;
|
||||
use leptos::server_fn::error::NoCustomError;
|
||||
use crate::util::database::get_db_conn;
|
||||
use crate::models::backend;
|
||||
}
|
||||
}
|
||||
|
||||
#[server(endpoint = "album/get", client = Client)]
|
||||
pub async fn get_album(id: i32) -> Result<Option<frontend::Album>, ServerFnError> {
|
||||
pub async fn get_album(id: i32) -> BackendResult<Option<frontend::Album>> {
|
||||
use crate::models::backend::Album;
|
||||
use crate::schema::*;
|
||||
|
||||
@ -24,9 +24,7 @@ pub async fn get_album(id: i32) -> Result<Option<frontend::Album>, ServerFnError
|
||||
.find(id)
|
||||
.first::<Album>(db_con)
|
||||
.optional()
|
||||
.map_err(|e| {
|
||||
ServerFnError::<NoCustomError>::ServerError(format!("Error getting album: {e}"))
|
||||
})?;
|
||||
.context("Error loading album from database")?;
|
||||
|
||||
let Some(album) = album else { return Ok(None) };
|
||||
|
||||
@ -34,7 +32,8 @@ pub async fn get_album(id: i32) -> Result<Option<frontend::Album>, ServerFnError
|
||||
.filter(album_artists::album_id.eq(id))
|
||||
.inner_join(artists::table.on(album_artists::artist_id.eq(artists::id)))
|
||||
.select(artists::all_columns)
|
||||
.load(db_con)?;
|
||||
.load(db_con)
|
||||
.context("Error loading album artists from database")?;
|
||||
|
||||
let img = album
|
||||
.image_path
|
||||
@ -52,12 +51,14 @@ pub async fn get_album(id: i32) -> Result<Option<frontend::Album>, ServerFnError
|
||||
}
|
||||
|
||||
#[server(endpoint = "album/get_songs", client = Client)]
|
||||
pub async fn get_songs(id: i32) -> Result<Vec<frontend::Song>, ServerFnError> {
|
||||
pub async fn get_songs(id: i32) -> BackendResult<Vec<frontend::Song>> {
|
||||
use crate::api::auth::get_logged_in_user;
|
||||
use crate::schema::*;
|
||||
use std::collections::HashMap;
|
||||
|
||||
let user = get_logged_in_user().await?;
|
||||
let user = get_logged_in_user()
|
||||
.await
|
||||
.context("Error getting logged-in user")?;
|
||||
|
||||
let db_con = &mut get_db_conn();
|
||||
|
||||
@ -94,7 +95,8 @@ pub async fn get_songs(id: i32) -> Result<Vec<frontend::Song>, ServerFnError> {
|
||||
song_dislikes::all_columns.nullable(),
|
||||
))
|
||||
.order(songs::track.asc())
|
||||
.load(db_con)?;
|
||||
.load(db_con)
|
||||
.context("Error loading album songs from database")?;
|
||||
song_list
|
||||
} else {
|
||||
let song_list: Vec<(
|
||||
@ -115,7 +117,8 @@ pub async fn get_songs(id: i32) -> Result<Vec<frontend::Song>, ServerFnError> {
|
||||
artists::all_columns.nullable(),
|
||||
))
|
||||
.order(songs::track.asc())
|
||||
.load(db_con)?;
|
||||
.load(db_con)
|
||||
.context("Error loading album songs from database")?;
|
||||
|
||||
let song_list: Vec<(
|
||||
backend::Album,
|
||||
|
@ -1,7 +1,7 @@
|
||||
use crate::util::error::*;
|
||||
use crate::util::serverfn_client::Client;
|
||||
use leptos::prelude::*;
|
||||
|
||||
use cfg_if::cfg_if;
|
||||
use leptos::prelude::*;
|
||||
|
||||
cfg_if! {
|
||||
if #[cfg(feature = "ssr")] {
|
||||
@ -27,18 +27,17 @@ pub async fn add_album(
|
||||
album_title: String,
|
||||
release_date: Option<String>,
|
||||
image_path: Option<String>,
|
||||
) -> Result<(), ServerFnError> {
|
||||
) -> BackendResult<()> {
|
||||
use crate::models::backend::NewAlbum;
|
||||
use crate::schema::albums::{self};
|
||||
use leptos::server_fn::error::NoCustomError;
|
||||
|
||||
let parsed_release_date = match release_date {
|
||||
Some(date) => match NaiveDate::parse_from_str(date.trim(), "%Y-%m-%d") {
|
||||
Ok(parsed_date) => Some(parsed_date),
|
||||
Err(_e) => {
|
||||
return Err(ServerFnError::<NoCustomError>::ServerError(
|
||||
"Invalid release date".to_string(),
|
||||
))
|
||||
Err(e) => {
|
||||
return Err(
|
||||
InputError::InvalidInput(format!("Error parsing release date: {e}")).into(),
|
||||
);
|
||||
}
|
||||
},
|
||||
None => None,
|
||||
@ -56,9 +55,7 @@ pub async fn add_album(
|
||||
diesel::insert_into(albums::table)
|
||||
.values(&new_album)
|
||||
.execute(db)
|
||||
.map_err(|e| {
|
||||
ServerFnError::<NoCustomError>::ServerError(format!("Error adding album: {e}"))
|
||||
})?;
|
||||
.context("Error inserting new album into database")?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
@ -4,6 +4,7 @@ use cfg_if::cfg_if;
|
||||
|
||||
use crate::models::backend::Artist;
|
||||
use crate::models::frontend;
|
||||
use crate::util::error::*;
|
||||
use crate::util::serverfn_client::Client;
|
||||
|
||||
cfg_if! {
|
||||
@ -26,9 +27,8 @@ cfg_if! {
|
||||
/// * `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) -> Result<(), ServerFnError> {
|
||||
pub async fn add_artist(artist_name: String) -> BackendResult<()> {
|
||||
use crate::schema::artists::dsl::*;
|
||||
use leptos::server_fn::error::NoCustomError;
|
||||
|
||||
let new_artist = NewArtist { name: artist_name };
|
||||
|
||||
@ -36,26 +36,21 @@ pub async fn add_artist(artist_name: String) -> Result<(), ServerFnError> {
|
||||
diesel::insert_into(artists)
|
||||
.values(&new_artist)
|
||||
.execute(db)
|
||||
.map_err(|e| {
|
||||
ServerFnError::<NoCustomError>::ServerError(format!("Error adding artist: {e}"))
|
||||
})?;
|
||||
.context("Error inserting new artist into database")?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[server(endpoint = "artists/get", client = Client)]
|
||||
pub async fn get_artist_by_id(artist_id: i32) -> Result<Option<Artist>, ServerFnError> {
|
||||
pub async fn get_artist_by_id(artist_id: i32) -> BackendResult<Option<Artist>> {
|
||||
use crate::schema::artists::dsl::*;
|
||||
use leptos::server_fn::error::NoCustomError;
|
||||
|
||||
let db = &mut get_db_conn();
|
||||
let artist = artists
|
||||
.filter(id.eq(artist_id))
|
||||
.first::<Artist>(db)
|
||||
.optional()
|
||||
.map_err(|e| {
|
||||
ServerFnError::<NoCustomError>::ServerError(format!("Error getting artist: {e}"))
|
||||
})?;
|
||||
.context("Error loading artist from database")?;
|
||||
|
||||
Ok(artist)
|
||||
}
|
||||
@ -64,18 +59,12 @@ pub async fn get_artist_by_id(artist_id: i32) -> Result<Option<Artist>, ServerFn
|
||||
pub async fn top_songs_by_artist(
|
||||
artist_id: i32,
|
||||
limit: Option<i64>,
|
||||
) -> Result<Vec<(frontend::Song, i64)>, ServerFnError> {
|
||||
) -> BackendResult<Vec<(frontend::Song, i64)>> {
|
||||
use crate::api::auth::get_user;
|
||||
use crate::models::backend::Song;
|
||||
use crate::schema::*;
|
||||
use leptos::server_fn::error::NoCustomError;
|
||||
|
||||
let user_id = get_user()
|
||||
.await
|
||||
.map_err(|e| {
|
||||
ServerFnError::ServerError::<NoCustomError>(format!("Error getting user: {e}"))
|
||||
})?
|
||||
.id;
|
||||
let user_id = get_user().await.context("Error getting logged-in user")?.id;
|
||||
|
||||
let db = &mut get_db_conn();
|
||||
let song_play_counts: Vec<(i32, i64)> = if let Some(limit) = limit {
|
||||
@ -175,8 +164,8 @@ pub async fn top_songs_by_artist(
|
||||
|
||||
let plays = song_play_counts
|
||||
.get(&song.id)
|
||||
.ok_or(ServerFnError::ServerError::<NoCustomError>(
|
||||
"Song id not found in history counts".to_string(),
|
||||
.ok_or(BackendError::InternalError(
|
||||
"Song id not found in history counts",
|
||||
))?;
|
||||
|
||||
top_songs_map.insert(song.id, (songdata, *plays));
|
||||
@ -192,7 +181,7 @@ pub async fn top_songs_by_artist(
|
||||
pub async fn albums_by_artist(
|
||||
artist_id: i32,
|
||||
limit: Option<i64>,
|
||||
) -> Result<Vec<frontend::Album>, ServerFnError> {
|
||||
) -> BackendResult<Vec<frontend::Album>> {
|
||||
use crate::schema::*;
|
||||
|
||||
let db = &mut get_db_conn();
|
||||
@ -219,7 +208,8 @@ pub async fn albums_by_artist(
|
||||
.on(albums::id.eq(album_artists::album_id)),
|
||||
)
|
||||
.select((albums::all_columns, artists::all_columns))
|
||||
.load(db)?;
|
||||
.load(db)
|
||||
.context("Error loading album artists from database")?;
|
||||
|
||||
for (album, artist) in album_artists {
|
||||
if let Some(stored_album) = albums_map.get_mut(&album.id) {
|
||||
|
136
src/api/auth.rs
136
src/api/auth.rs
@ -4,7 +4,6 @@ use cfg_if::cfg_if;
|
||||
|
||||
cfg_if! {
|
||||
if #[cfg(feature = "ssr")] {
|
||||
use leptos::server_fn::error::NoCustomError;
|
||||
use leptos_axum::extract;
|
||||
use axum_login::AuthSession;
|
||||
use crate::util::auth_backend::AuthBackend;
|
||||
@ -13,18 +12,17 @@ cfg_if! {
|
||||
|
||||
use crate::api::users::UserCredentials;
|
||||
use crate::models::backend::{NewUser, User};
|
||||
use crate::util::error::*;
|
||||
use crate::util::serverfn_client::Client;
|
||||
|
||||
/// Create a new user and log them in
|
||||
/// Takes in a NewUser struct, with the password in plaintext
|
||||
/// Returns a Result with the error message if the user could not be created
|
||||
#[server(endpoint = "signup", client = Client)]
|
||||
pub async fn signup(new_user: NewUser) -> Result<(), ServerFnError> {
|
||||
pub async fn signup(new_user: NewUser) -> BackendResult<()> {
|
||||
// Check LIBRETUNES_DISABLE_SIGNUP env var
|
||||
if std::env::var("LIBRETUNES_DISABLE_SIGNUP").is_ok_and(|v| v == "true") {
|
||||
return Err(ServerFnError::<NoCustomError>::ServerError(
|
||||
"Signup is disabled".to_string(),
|
||||
));
|
||||
return Err(AuthError::SignupDisabled.into());
|
||||
}
|
||||
|
||||
use crate::api::users::create_user;
|
||||
@ -35,13 +33,13 @@ pub async fn signup(new_user: NewUser) -> Result<(), ServerFnError> {
|
||||
..new_user
|
||||
};
|
||||
|
||||
create_user(&new_user).await.map_err(|e| {
|
||||
ServerFnError::<NoCustomError>::ServerError(format!("Error creating user: {e}"))
|
||||
})?;
|
||||
create_user(&new_user)
|
||||
.await
|
||||
.context("Error creating user")?;
|
||||
|
||||
let mut auth_session = extract::<AuthSession<AuthBackend>>().await.map_err(|e| {
|
||||
ServerFnError::<NoCustomError>::ServerError(format!("Error getting auth session: {e}"))
|
||||
})?;
|
||||
let mut auth_session = extract::<AuthSession<AuthBackend>>()
|
||||
.await
|
||||
.context("Error extracting auth session")?;
|
||||
|
||||
let credentials = UserCredentials {
|
||||
username_or_email: new_user.username.clone(),
|
||||
@ -49,15 +47,12 @@ pub async fn signup(new_user: NewUser) -> Result<(), ServerFnError> {
|
||||
};
|
||||
|
||||
match auth_session.authenticate(credentials).await {
|
||||
Ok(Some(user)) => auth_session.login(&user).await.map_err(|e| {
|
||||
ServerFnError::<NoCustomError>::ServerError(format!("Error logging in user: {e}"))
|
||||
}),
|
||||
Ok(None) => Err(ServerFnError::<NoCustomError>::ServerError(
|
||||
"Error authenticating user: User not found".to_string(),
|
||||
)),
|
||||
Err(e) => Err(ServerFnError::<NoCustomError>::ServerError(format!(
|
||||
"Error authenticating user: {e}"
|
||||
))),
|
||||
Ok(Some(user)) => auth_session
|
||||
.login(&user)
|
||||
.await
|
||||
.map_err(|e| AuthError::AuthError(format!("Error logging in user: {e}")).into()),
|
||||
Ok(None) => Err(AuthError::InvalidCredentials.into()),
|
||||
Err(e) => Err(AuthError::AuthError(format!("Error authenticating user: {e}")).into()),
|
||||
}
|
||||
}
|
||||
|
||||
@ -65,21 +60,22 @@ pub async fn signup(new_user: NewUser) -> Result<(), ServerFnError> {
|
||||
/// Takes in a username or email and a password in plaintext
|
||||
/// Returns a Result with a boolean indicating if the login was successful
|
||||
#[server(endpoint = "login", client = Client)]
|
||||
pub async fn login(credentials: UserCredentials) -> Result<Option<User>, ServerFnError> {
|
||||
pub async fn login(credentials: UserCredentials) -> BackendResult<Option<User>> {
|
||||
use crate::api::users::validate_user;
|
||||
|
||||
let mut auth_session = extract::<AuthSession<AuthBackend>>().await.map_err(|e| {
|
||||
ServerFnError::<NoCustomError>::ServerError(format!("Error getting auth session: {e}"))
|
||||
})?;
|
||||
let mut auth_session = extract::<AuthSession<AuthBackend>>()
|
||||
.await
|
||||
.context("Error extracting auth session")?;
|
||||
|
||||
let user = validate_user(credentials).await.map_err(|e| {
|
||||
ServerFnError::<NoCustomError>::ServerError(format!("Error validating user: {e}"))
|
||||
})?;
|
||||
let user = validate_user(credentials)
|
||||
.await
|
||||
.context("Error validating user credentials")?;
|
||||
|
||||
if let Some(mut user) = user {
|
||||
auth_session.login(&user).await.map_err(|e| {
|
||||
ServerFnError::<NoCustomError>::ServerError(format!("Error logging in user: {e}"))
|
||||
})?;
|
||||
auth_session
|
||||
.login(&user)
|
||||
.await
|
||||
.map_err(|e| AuthError::AuthError(format!("Error logging in user: {e}")))?;
|
||||
|
||||
user.password = None;
|
||||
Ok(Some(user))
|
||||
@ -91,14 +87,15 @@ pub async fn login(credentials: UserCredentials) -> Result<Option<User>, ServerF
|
||||
/// Log a user out
|
||||
/// Returns a Result with the error message if the user could not be logged out
|
||||
#[server(endpoint = "logout", client = Client)]
|
||||
pub async fn logout() -> Result<(), ServerFnError> {
|
||||
let mut auth_session = extract::<AuthSession<AuthBackend>>().await.map_err(|e| {
|
||||
ServerFnError::<NoCustomError>::ServerError(format!("Error getting auth session: {e}"))
|
||||
})?;
|
||||
pub async fn logout() -> BackendResult<()> {
|
||||
let mut auth_session = extract::<AuthSession<AuthBackend>>()
|
||||
.await
|
||||
.context("Error extracting auth session")?;
|
||||
|
||||
auth_session.logout().await.map_err(|e| {
|
||||
ServerFnError::<NoCustomError>::ServerError(format!("Error getting auth session: {e}"))
|
||||
})?;
|
||||
auth_session
|
||||
.logout()
|
||||
.await
|
||||
.map_err(|e| AuthError::AuthError(format!("Error logging out user: {e}")))?;
|
||||
|
||||
leptos_axum::redirect("/login");
|
||||
Ok(())
|
||||
@ -107,10 +104,10 @@ pub async fn logout() -> Result<(), ServerFnError> {
|
||||
/// Check if a user is logged in
|
||||
/// Returns a Result with a boolean indicating if the user is logged in
|
||||
#[server(endpoint = "check_auth", client = Client)]
|
||||
pub async fn check_auth() -> Result<bool, ServerFnError> {
|
||||
let auth_session = extract::<AuthSession<AuthBackend>>().await.map_err(|e| {
|
||||
ServerFnError::<NoCustomError>::ServerError(format!("Error getting auth session: {e}"))
|
||||
})?;
|
||||
pub async fn check_auth() -> BackendResult<bool> {
|
||||
let auth_session = extract::<AuthSession<AuthBackend>>()
|
||||
.await
|
||||
.context("Error extracting auth session")?;
|
||||
|
||||
Ok(auth_session.user.is_some())
|
||||
}
|
||||
@ -129,16 +126,17 @@ pub async fn check_auth() -> Result<bool, ServerFnError> {
|
||||
/// }
|
||||
/// ```
|
||||
#[cfg(feature = "ssr")]
|
||||
pub async fn require_auth() -> Result<(), ServerFnError> {
|
||||
check_auth().await.and_then(|logged_in| {
|
||||
if logged_in {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(ServerFnError::<NoCustomError>::ServerError(
|
||||
"Unauthorized".to_string(),
|
||||
))
|
||||
}
|
||||
})
|
||||
pub async fn require_auth() -> BackendResult<()> {
|
||||
check_auth()
|
||||
.await
|
||||
.context("Error checking authentication")
|
||||
.and_then(|logged_in| {
|
||||
if logged_in {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(AuthError::Unauthorized.into())
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// Get the current logged-in user
|
||||
@ -157,23 +155,19 @@ pub async fn require_auth() -> Result<(), ServerFnError> {
|
||||
/// }
|
||||
/// ```
|
||||
#[cfg(feature = "ssr")]
|
||||
pub async fn get_user() -> Result<User, ServerFnError> {
|
||||
let auth_session = extract::<AuthSession<AuthBackend>>().await.map_err(|e| {
|
||||
ServerFnError::<NoCustomError>::ServerError(format!("Error getting auth session: {e}"))
|
||||
})?;
|
||||
pub async fn get_user() -> BackendResult<User> {
|
||||
let auth_session = extract::<AuthSession<AuthBackend>>()
|
||||
.await
|
||||
.context("Error extracting auth session")?;
|
||||
|
||||
auth_session
|
||||
.user
|
||||
.ok_or(ServerFnError::<NoCustomError>::ServerError(
|
||||
"User not logged in".to_string(),
|
||||
))
|
||||
auth_session.user.ok_or(AuthError::Unauthorized.into())
|
||||
}
|
||||
|
||||
#[server(endpoint = "get_logged_in_user", client = Client)]
|
||||
pub async fn get_logged_in_user() -> Result<Option<User>, ServerFnError> {
|
||||
let auth_session = extract::<AuthSession<AuthBackend>>().await.map_err(|e| {
|
||||
ServerFnError::<NoCustomError>::ServerError(format!("Error getting auth session: {e}"))
|
||||
})?;
|
||||
pub async fn get_logged_in_user() -> BackendResult<Option<User>> {
|
||||
let auth_session = extract::<AuthSession<AuthBackend>>()
|
||||
.await
|
||||
.context("Error extracting auth session")?;
|
||||
|
||||
let user = auth_session.user.map(|mut user| {
|
||||
user.password = None;
|
||||
@ -186,10 +180,10 @@ pub async fn get_logged_in_user() -> Result<Option<User>, ServerFnError> {
|
||||
/// Check if a user is an admin
|
||||
/// Returns a Result with a boolean indicating if the user is logged in and an admin
|
||||
#[server(endpoint = "check_admin", client = Client)]
|
||||
pub async fn check_admin() -> Result<bool, ServerFnError> {
|
||||
let auth_session = extract::<AuthSession<AuthBackend>>().await.map_err(|e| {
|
||||
ServerFnError::<NoCustomError>::ServerError(format!("Error getting auth session: {e}"))
|
||||
})?;
|
||||
pub async fn check_admin() -> BackendResult<bool> {
|
||||
let auth_session = extract::<AuthSession<AuthBackend>>()
|
||||
.await
|
||||
.context("Error extracting auth session")?;
|
||||
|
||||
Ok(auth_session.user.as_ref().map(|u| u.admin).unwrap_or(false))
|
||||
}
|
||||
@ -208,14 +202,12 @@ pub async fn check_admin() -> Result<bool, ServerFnError> {
|
||||
/// }
|
||||
/// ```
|
||||
#[cfg(feature = "ssr")]
|
||||
pub async fn require_admin() -> Result<(), ServerFnError> {
|
||||
pub async fn require_admin() -> BackendResult<()> {
|
||||
check_admin().await.and_then(|is_admin| {
|
||||
if is_admin {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(ServerFnError::<NoCustomError>::ServerError(
|
||||
"Unauthorized".to_string(),
|
||||
))
|
||||
Err(AuthError::AdminRequired.into())
|
||||
}
|
||||
})
|
||||
}
|
||||
|
@ -1,23 +1,23 @@
|
||||
use crate::util::error::*;
|
||||
use crate::util::serverfn_client::Client;
|
||||
use leptos::prelude::*;
|
||||
|
||||
use crate::util::serverfn_client::Client;
|
||||
|
||||
#[server(endpoint = "health", client = Client)]
|
||||
pub async fn health() -> Result<String, ServerFnError> {
|
||||
pub async fn health() -> BackendResult<String> {
|
||||
use crate::util::database::get_db_conn;
|
||||
use crate::util::redis::get_redis_conn;
|
||||
use diesel::connection::SimpleConnection;
|
||||
use server_fn::error::NoCustomError;
|
||||
use tower_sessions_redis_store::fred::interfaces::ClientLike;
|
||||
|
||||
get_db_conn()
|
||||
.batch_execute("SELECT 1")
|
||||
.map_err(|e| ServerFnError::<NoCustomError>::ServerError(format!("Database error: {e}")))?;
|
||||
.context("Failed to execute database health check query")?;
|
||||
|
||||
get_redis_conn()
|
||||
.ping::<()>(None)
|
||||
.await
|
||||
.map_err(|e| ServerFnError::<NoCustomError>::ServerError(format!("Redis error: {e}")))?;
|
||||
.map_err(|e| BackendError::InternalError(format!("{e}")))
|
||||
.context("Failed to execute Redis health check ping")?;
|
||||
|
||||
Ok("ok".to_string())
|
||||
}
|
||||
|
@ -1,5 +1,6 @@
|
||||
use crate::models::backend::HistoryEntry;
|
||||
use crate::models::backend::Song;
|
||||
use crate::util::error::*;
|
||||
use crate::util::serverfn_client::Client;
|
||||
use chrono::NaiveDateTime;
|
||||
use leptos::prelude::*;
|
||||
@ -8,7 +9,6 @@ use cfg_if::cfg_if;
|
||||
|
||||
cfg_if! {
|
||||
if #[cfg(feature = "ssr")] {
|
||||
use leptos::server_fn::error::NoCustomError;
|
||||
use crate::util::database::get_db_conn;
|
||||
use crate::api::auth::get_user;
|
||||
}
|
||||
@ -16,35 +16,39 @@ cfg_if! {
|
||||
|
||||
/// Get the history of the current user.
|
||||
#[server(endpoint = "history/get", client = Client)]
|
||||
pub async fn get_history(limit: Option<i64>) -> Result<Vec<HistoryEntry>, ServerFnError> {
|
||||
let user = get_user().await?;
|
||||
pub async fn get_history(limit: Option<i64>) -> BackendResult<Vec<HistoryEntry>> {
|
||||
let user = get_user().await.context("Error getting logged-in user")?;
|
||||
|
||||
let db_con = &mut get_db_conn();
|
||||
let history = user.get_history(limit, db_con).map_err(|e| {
|
||||
ServerFnError::<NoCustomError>::ServerError(format!("Error getting history: {e}"))
|
||||
})?;
|
||||
let history = user
|
||||
.get_history(limit, db_con)
|
||||
.context("Error getting history")?;
|
||||
|
||||
Ok(history)
|
||||
}
|
||||
|
||||
/// Get the listen dates and songs of the current user.
|
||||
#[server(endpoint = "history/get_songs", client = Client)]
|
||||
pub async fn get_history_songs(
|
||||
limit: Option<i64>,
|
||||
) -> Result<Vec<(NaiveDateTime, Song)>, ServerFnError> {
|
||||
let user = get_user().await?;
|
||||
pub async fn get_history_songs(limit: Option<i64>) -> BackendResult<Vec<(NaiveDateTime, Song)>> {
|
||||
let user = get_user().await.context("Error getting logged-in user")?;
|
||||
|
||||
let db_con = &mut get_db_conn();
|
||||
let songs = user.get_history_songs(limit, db_con).map_err(|e| {
|
||||
ServerFnError::<NoCustomError>::ServerError(format!("Error getting history songs: {e}"))
|
||||
})?;
|
||||
|
||||
let songs = user
|
||||
.get_history_songs(limit, db_con)
|
||||
.context("Error getting history songs")?;
|
||||
|
||||
Ok(songs)
|
||||
}
|
||||
|
||||
/// Add a song to the history of the current user.
|
||||
#[server(endpoint = "history/add", client = Client)]
|
||||
pub async fn add_history(song_id: i32) -> Result<(), ServerFnError> {
|
||||
let user = get_user().await?;
|
||||
pub async fn add_history(song_id: i32) -> BackendResult<()> {
|
||||
let user = get_user().await.context("Error getting logged-in user")?;
|
||||
|
||||
let db_con = &mut get_db_conn();
|
||||
user.add_history(song_id, db_con).map_err(|e| {
|
||||
ServerFnError::<NoCustomError>::ServerError(format!("Error adding history: {e}"))
|
||||
})?;
|
||||
user.add_history(song_id, db_con)
|
||||
.context("Error adding song to history")?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
@ -1,4 +1,5 @@
|
||||
use crate::models::{backend, frontend};
|
||||
use crate::util::error::*;
|
||||
use crate::util::serverfn_client::Client;
|
||||
use cfg_if::cfg_if;
|
||||
use leptos::prelude::*;
|
||||
@ -11,14 +12,13 @@ cfg_if! {
|
||||
use crate::util::database::get_db_conn;
|
||||
use crate::util::extract_field::extract_field;
|
||||
use std::collections::HashMap;
|
||||
use server_fn::error::NoCustomError;
|
||||
use log::*;
|
||||
use crate::schema::*;
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "ssr")]
|
||||
async fn user_owns_playlist(user_id: i32, playlist_id: i32) -> Result<bool, ServerFnError> {
|
||||
async fn user_owns_playlist(user_id: i32, playlist_id: i32) -> BackendResult<bool> {
|
||||
let mut db_conn = get_db_conn();
|
||||
|
||||
let exists = playlists::table
|
||||
@ -27,22 +27,15 @@ async fn user_owns_playlist(user_id: i32, playlist_id: i32) -> Result<bool, Serv
|
||||
.select(playlists::id)
|
||||
.first::<i32>(&mut db_conn)
|
||||
.optional()
|
||||
.map_err(|e| {
|
||||
ServerFnError::<NoCustomError>::ServerError(format!("Error checking playlist: {e}"))
|
||||
})?
|
||||
.context("Error loading playlist from database")?
|
||||
.is_some();
|
||||
|
||||
Ok(exists)
|
||||
}
|
||||
|
||||
#[server(endpoint = "playlists/get_all", client = Client)]
|
||||
pub async fn get_playlists() -> Result<Vec<backend::Playlist>, ServerFnError> {
|
||||
let user_id = get_user()
|
||||
.await
|
||||
.map_err(|e| {
|
||||
ServerFnError::<NoCustomError>::ServerError(format!("Error getting user: {e}"))
|
||||
})?
|
||||
.id;
|
||||
pub async fn get_playlists() -> BackendResult<Vec<backend::Playlist>> {
|
||||
let user_id = get_user().await.context("Error getting logged-in user")?.id;
|
||||
|
||||
let mut db_conn = get_db_conn();
|
||||
|
||||
@ -50,21 +43,14 @@ pub async fn get_playlists() -> Result<Vec<backend::Playlist>, ServerFnError> {
|
||||
.filter(playlists::owner_id.eq(user_id))
|
||||
.select(playlists::all_columns)
|
||||
.load::<backend::Playlist>(&mut db_conn)
|
||||
.map_err(|e| {
|
||||
ServerFnError::<NoCustomError>::ServerError(format!("Error getting playlists: {e}"))
|
||||
})?;
|
||||
.context("Error loading playlists from database")?;
|
||||
|
||||
Ok(playlists)
|
||||
}
|
||||
|
||||
#[server(endpoint = "playlists/get", client = Client)]
|
||||
pub async fn get_playlist(playlist_id: i32) -> Result<backend::Playlist, ServerFnError> {
|
||||
let user_id = get_user()
|
||||
.await
|
||||
.map_err(|e| {
|
||||
ServerFnError::<NoCustomError>::ServerError(format!("Error getting user: {e}"))
|
||||
})?
|
||||
.id;
|
||||
pub async fn get_playlist(playlist_id: i32) -> BackendResult<backend::Playlist> {
|
||||
let user_id = get_user().await.context("Error getting logged-in user")?.id;
|
||||
|
||||
let mut db_conn = get_db_conn();
|
||||
|
||||
@ -73,33 +59,23 @@ pub async fn get_playlist(playlist_id: i32) -> Result<backend::Playlist, ServerF
|
||||
.filter(playlists::owner_id.eq(user_id))
|
||||
.select(playlists::all_columns)
|
||||
.first(&mut db_conn)
|
||||
.map_err(|e| {
|
||||
ServerFnError::<NoCustomError>::ServerError(format!("Error getting playlist: {e}"))
|
||||
})?;
|
||||
.context("Error loading playlist from database")?;
|
||||
|
||||
Ok(playlist)
|
||||
}
|
||||
|
||||
#[server(endpoint = "playlists/get_songs", client = Client)]
|
||||
pub async fn get_playlist_songs(playlist_id: i32) -> Result<Vec<frontend::Song>, ServerFnError> {
|
||||
let user_id = get_user()
|
||||
.await
|
||||
.map_err(|e| {
|
||||
ServerFnError::<NoCustomError>::ServerError(format!("Error getting user: {e}"))
|
||||
})?
|
||||
.id;
|
||||
pub async fn get_playlist_songs(playlist_id: i32) -> BackendResult<Vec<frontend::Song>> {
|
||||
let user_id = get_user().await.context("Error getting logged-in user")?.id;
|
||||
|
||||
// Check if the playlist exists and belongs to the user
|
||||
let valid_playlist = user_owns_playlist(user_id, playlist_id)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
ServerFnError::<NoCustomError>::ServerError(format!("Error checking playlist: {e}"))
|
||||
})?;
|
||||
.context("Error checking if playlist exists and is owned by user")?;
|
||||
|
||||
if !valid_playlist {
|
||||
return Err(ServerFnError::<NoCustomError>::ServerError(
|
||||
"Playlist does not exist or does not belong to the user".to_string(),
|
||||
));
|
||||
return Err(AccessError::NotFoundOrUnauthorized
|
||||
.context("Playlist does not exist or does not belong to the user"));
|
||||
}
|
||||
|
||||
let mut db_conn = get_db_conn();
|
||||
@ -139,7 +115,8 @@ pub async fn get_playlist_songs(playlist_id: i32) -> Result<Vec<frontend::Song>,
|
||||
song_likes::all_columns.nullable(),
|
||||
song_dislikes::all_columns.nullable(),
|
||||
))
|
||||
.load(&mut db_conn)?;
|
||||
.load(&mut db_conn)
|
||||
.context("Error loading playlist songs from database")?;
|
||||
|
||||
let mut playlist_songs: HashMap<i32, frontend::Song> = HashMap::new();
|
||||
|
||||
@ -185,24 +162,19 @@ pub async fn get_playlist_songs(playlist_id: i32) -> Result<Vec<frontend::Song>,
|
||||
}
|
||||
|
||||
#[server(endpoint = "playlists/add_song", client = Client)]
|
||||
pub async fn add_song_to_playlist(playlist_id: i32, song_id: i32) -> Result<(), ServerFnError> {
|
||||
pub async fn add_song_to_playlist(playlist_id: i32, song_id: i32) -> BackendResult<()> {
|
||||
use crate::schema::*;
|
||||
|
||||
let user = get_user().await.map_err(|e| {
|
||||
ServerFnError::<NoCustomError>::ServerError(format!("Error getting user: {e}"))
|
||||
})?;
|
||||
let user = get_user().await.context("Error getting logged-in user")?;
|
||||
|
||||
// Check if the playlist exists and belongs to the user
|
||||
let valid_playlist = user_owns_playlist(user.id, playlist_id)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
ServerFnError::<NoCustomError>::ServerError(format!("Error checking playlist: {e}"))
|
||||
})?;
|
||||
.context("Error checking if playlist exists and is owned by user")?;
|
||||
|
||||
if !valid_playlist {
|
||||
return Err(ServerFnError::<NoCustomError>::ServerError(
|
||||
"Playlist does not exist or does not belong to the user".to_string(),
|
||||
));
|
||||
return Err(AccessError::NotFoundOrUnauthorized
|
||||
.context("Playlist does not exist or does not belong to the user"));
|
||||
}
|
||||
|
||||
let mut db_conn = get_db_conn();
|
||||
@ -213,23 +185,17 @@ pub async fn add_song_to_playlist(playlist_id: i32, song_id: i32) -> Result<(),
|
||||
playlist_songs::song_id.eq(song_id),
|
||||
))
|
||||
.execute(&mut db_conn)
|
||||
.map_err(|e| {
|
||||
ServerFnError::<NoCustomError>::ServerError(format!(
|
||||
"Error adding song to playlist: {e}"
|
||||
))
|
||||
})?;
|
||||
.context("Error adding song to playlist in database")?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[server(input = MultipartFormData, endpoint = "playlists/create")]
|
||||
pub async fn create_playlist(data: MultipartData) -> Result<(), ServerFnError> {
|
||||
pub async fn create_playlist(data: MultipartData) -> BackendResult<()> {
|
||||
use crate::models::backend::NewPlaylist;
|
||||
use image_convert::{to_webp, ImageResource, WEBPConfig};
|
||||
|
||||
let user = get_user().await.map_err(|e| {
|
||||
ServerFnError::<NoCustomError>::ServerError(format!("Error getting user: {e}"))
|
||||
})?;
|
||||
let user = get_user().await.context("Error getting logged-in user")?;
|
||||
|
||||
// Safe to unwrap - "On the server side, this always returns Some(_). On the client side, always returns None."
|
||||
let mut data = data.into_inner().unwrap();
|
||||
@ -246,20 +212,17 @@ pub async fn create_playlist(data: MultipartData) -> Result<(), ServerFnError> {
|
||||
}
|
||||
"picture" => {
|
||||
// Read the image
|
||||
let bytes = field.bytes().await.map_err(|e| {
|
||||
ServerFnError::<NoCustomError>::ServerError(format!(
|
||||
"Error getting field bytes: {e}"
|
||||
))
|
||||
})?;
|
||||
let bytes = field
|
||||
.bytes()
|
||||
.await
|
||||
.map_err(|e| InputError::FieldReadError(format!("{e}")))
|
||||
.context("Error reading bytes of the picture field")?;
|
||||
|
||||
// Check if the image is empty
|
||||
if !bytes.is_empty() {
|
||||
let reader = std::io::Cursor::new(bytes);
|
||||
let image_source = ImageResource::from_reader(reader).map_err(|e| {
|
||||
ServerFnError::<NoCustomError>::ServerError(format!(
|
||||
"Error creating image resource: {e}"
|
||||
))
|
||||
})?;
|
||||
let image_source = ImageResource::from_reader(reader)
|
||||
.context("Error creating image resource from reader")?;
|
||||
|
||||
picture_data = Some(image_source);
|
||||
}
|
||||
@ -272,7 +235,7 @@ pub async fn create_playlist(data: MultipartData) -> Result<(), ServerFnError> {
|
||||
|
||||
// Unwrap mandatory fields
|
||||
let name = playlist_name.ok_or_else(|| {
|
||||
ServerFnError::<NoCustomError>::ServerError("Missing playlist name".to_string())
|
||||
InputError::MissingField("name".to_string()).context("Missing playlist name")
|
||||
})?;
|
||||
|
||||
let new_playlist = NewPlaylist {
|
||||
@ -288,33 +251,27 @@ pub async fn create_playlist(data: MultipartData) -> Result<(), ServerFnError> {
|
||||
let playlist = diesel::insert_into(playlists::table)
|
||||
.values(&new_playlist)
|
||||
.get_result::<backend::Playlist>(db_conn)
|
||||
.map_err(|e| {
|
||||
ServerFnError::<NoCustomError>::ServerError(format!("Error creating playlist: {e}"))
|
||||
})?;
|
||||
.context("Error creating playlist in database")?;
|
||||
|
||||
// If a picture was provided, save it to the database
|
||||
if let Some(image_source) = picture_data {
|
||||
let image_path = format!("assets/images/playlist/{}.webp", playlist.id);
|
||||
|
||||
let mut image_target = ImageResource::from_path(&image_path);
|
||||
to_webp(&mut image_target, &image_source, &WEBPConfig::new()).map_err(|e| {
|
||||
ServerFnError::<NoCustomError>::ServerError(format!(
|
||||
"Error converting image to webp: {e}"
|
||||
))
|
||||
})?;
|
||||
to_webp(&mut image_target, &image_source, &WEBPConfig::new())
|
||||
.map_err(|e| InputError::InvalidInput(format!("{e}")))
|
||||
.context("Error converting image to webp")?;
|
||||
}
|
||||
|
||||
Ok::<(), ServerFnError>(())
|
||||
Ok::<(), BackendError>(())
|
||||
})
|
||||
}
|
||||
|
||||
#[server(input = MultipartFormData, endpoint = "playlists/edit_image")]
|
||||
pub async fn edit_playlist_image(data: MultipartData) -> Result<(), ServerFnError> {
|
||||
pub async fn edit_playlist_image(data: MultipartData) -> BackendResult<()> {
|
||||
use image_convert::{to_webp, ImageResource, WEBPConfig};
|
||||
|
||||
let user = get_user().await.map_err(|e| {
|
||||
ServerFnError::<NoCustomError>::ServerError(format!("Error getting user: {e}"))
|
||||
})?;
|
||||
let user = get_user().await.context("Error getting logged-in user")?;
|
||||
|
||||
// Safe to unwrap - "On the server side, this always returns Some(_). On the client side, always returns None."
|
||||
let mut data = data.into_inner().unwrap();
|
||||
@ -331,20 +288,17 @@ pub async fn edit_playlist_image(data: MultipartData) -> Result<(), ServerFnErro
|
||||
}
|
||||
"picture" => {
|
||||
// Read the image
|
||||
let bytes = field.bytes().await.map_err(|e| {
|
||||
ServerFnError::<NoCustomError>::ServerError(format!(
|
||||
"Error getting field bytes: {e}"
|
||||
))
|
||||
})?;
|
||||
let bytes = field
|
||||
.bytes()
|
||||
.await
|
||||
.map_err(|e| InputError::FieldReadError(format!("{e}")))
|
||||
.context("Error reading bytes of the picture field")?;
|
||||
|
||||
// Check if the image is empty
|
||||
if !bytes.is_empty() {
|
||||
let reader = std::io::Cursor::new(bytes);
|
||||
let image_source = ImageResource::from_reader(reader).map_err(|e| {
|
||||
ServerFnError::<NoCustomError>::ServerError(format!(
|
||||
"Error creating image resource: {e}"
|
||||
))
|
||||
})?;
|
||||
let image_source = ImageResource::from_reader(reader)
|
||||
.context("Error creating image resource from reader")?;
|
||||
|
||||
picture_data = Some(image_source);
|
||||
}
|
||||
@ -356,25 +310,22 @@ pub async fn edit_playlist_image(data: MultipartData) -> Result<(), ServerFnErro
|
||||
}
|
||||
|
||||
// Unwrap mandatory fields
|
||||
let playlist_id = playlist_id.ok_or_else(|| {
|
||||
ServerFnError::<NoCustomError>::ServerError("Missing playlist name".to_string())
|
||||
})?;
|
||||
let playlist_id = playlist_id
|
||||
.ok_or_else(|| InputError::MissingField("id".to_string()).context("Missing playlist ID"))?;
|
||||
|
||||
let playlist_id: i32 = playlist_id.parse().map_err(|e| {
|
||||
ServerFnError::<NoCustomError>::ServerError(format!("Invalid playlist ID: {e}"))
|
||||
})?;
|
||||
let playlist_id: i32 = playlist_id
|
||||
.parse()
|
||||
.map_err(|e| InputError::InvalidInput(format!("Invalid playlist ID: {e}")))
|
||||
.context("Error parsing playlist ID from string")?;
|
||||
|
||||
// Make sure the playlist exists and belongs to the user
|
||||
let valid_playlist = user_owns_playlist(user.id, playlist_id)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
ServerFnError::<NoCustomError>::ServerError(format!("Error checking playlist: {e}"))
|
||||
})?;
|
||||
.context("Error checking if playlist exists and is owned by user")?;
|
||||
|
||||
if !valid_playlist {
|
||||
return Err(ServerFnError::<NoCustomError>::ServerError(
|
||||
"Playlist does not exist or does not belong to the user".to_string(),
|
||||
));
|
||||
return Err(AccessError::NotFoundOrUnauthorized
|
||||
.context("Playlist does not exist or does not belong to the user"));
|
||||
}
|
||||
|
||||
// If a picture was provided, save it to the database
|
||||
@ -382,65 +333,52 @@ pub async fn edit_playlist_image(data: MultipartData) -> Result<(), ServerFnErro
|
||||
let image_path = format!("assets/images/playlist/{playlist_id}.webp");
|
||||
|
||||
let mut image_target = ImageResource::from_path(&image_path);
|
||||
to_webp(&mut image_target, &image_source, &WEBPConfig::new()).map_err(|e| {
|
||||
ServerFnError::<NoCustomError>::ServerError(format!(
|
||||
"Error converting image to webp: {e}"
|
||||
))
|
||||
})?;
|
||||
to_webp(&mut image_target, &image_source, &WEBPConfig::new())
|
||||
.map_err(|e| InputError::InvalidInput(format!("{e}")))
|
||||
.context("Error converting image to webp")?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[server(endpoint = "playlists/delete", client = Client)]
|
||||
pub async fn delete_playlist(playlist_id: i32) -> Result<(), ServerFnError> {
|
||||
pub async fn delete_playlist(playlist_id: i32) -> BackendResult<()> {
|
||||
use crate::schema::*;
|
||||
|
||||
let user = get_user().await.map_err(|e| {
|
||||
ServerFnError::<NoCustomError>::ServerError(format!("Error getting user: {e}"))
|
||||
})?;
|
||||
let user = get_user().await.context("Error getting logged-in user")?;
|
||||
|
||||
// Check if the playlist exists and belongs to the user
|
||||
let valid_playlist = user_owns_playlist(user.id, playlist_id)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
ServerFnError::<NoCustomError>::ServerError(format!("Error checking playlist: {e}"))
|
||||
})?;
|
||||
.context("Error checking if playlist exists and is owned by user")?;
|
||||
|
||||
if !valid_playlist {
|
||||
return Err(ServerFnError::<NoCustomError>::ServerError(
|
||||
"Playlist does not exist or does not belong to the user".to_string(),
|
||||
));
|
||||
return Err(AccessError::NotFoundOrUnauthorized
|
||||
.context("Playlist does not exist or does not belong to the user"));
|
||||
}
|
||||
|
||||
let mut db_conn = get_db_conn();
|
||||
|
||||
diesel::delete(playlists::table.find(playlist_id))
|
||||
.execute(&mut db_conn)
|
||||
.map_err(|e| {
|
||||
ServerFnError::<NoCustomError>::ServerError(format!("Error deleting playlist: {e}"))
|
||||
})?;
|
||||
.context("Error deleting playlist from database")?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[server(endpoint = "playlists/rename", client = Client)]
|
||||
pub async fn rename_playlist(id: i32, new_name: String) -> Result<(), ServerFnError> {
|
||||
pub async fn rename_playlist(id: i32, new_name: String) -> BackendResult<()> {
|
||||
use crate::schema::*;
|
||||
|
||||
let user = get_user().await.map_err(|e| {
|
||||
ServerFnError::<NoCustomError>::ServerError(format!("Error getting user: {e}"))
|
||||
})?;
|
||||
let user = get_user().await.context("Error getting logged-in user")?;
|
||||
|
||||
// Check if the playlist exists and belongs to the user
|
||||
let valid_playlist = user_owns_playlist(user.id, id).await.map_err(|e| {
|
||||
ServerFnError::<NoCustomError>::ServerError(format!("Error checking playlist: {e}"))
|
||||
})?;
|
||||
let valid_playlist = user_owns_playlist(user.id, id)
|
||||
.await
|
||||
.context("Error checking if playlist exists and is owned by user")?;
|
||||
|
||||
if !valid_playlist {
|
||||
return Err(ServerFnError::<NoCustomError>::ServerError(
|
||||
"Playlist does not exist or does not belong to the user".to_string(),
|
||||
));
|
||||
return Err(AccessError::NotFoundOrUnauthorized.into());
|
||||
}
|
||||
|
||||
let mut db_conn = get_db_conn();
|
||||
@ -448,9 +386,7 @@ pub async fn rename_playlist(id: i32, new_name: String) -> Result<(), ServerFnEr
|
||||
diesel::update(playlists::table.find(id))
|
||||
.set(playlists::name.eq(new_name))
|
||||
.execute(&mut db_conn)
|
||||
.map_err(|e| {
|
||||
ServerFnError::<NoCustomError>::ServerError(format!("Error renaming playlist: {e}"))
|
||||
})?;
|
||||
.context("Error renaming playlist in database")?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
@ -1,8 +1,8 @@
|
||||
use crate::util::error::*;
|
||||
use cfg_if::cfg_if;
|
||||
use leptos::prelude::*;
|
||||
use server_fn::codec::{MultipartData, MultipartFormData};
|
||||
|
||||
use cfg_if::cfg_if;
|
||||
|
||||
use crate::models::frontend;
|
||||
use crate::util::serverfn_client::Client;
|
||||
use chrono::NaiveDateTime;
|
||||
@ -10,7 +10,6 @@ use chrono::NaiveDateTime;
|
||||
cfg_if! {
|
||||
if #[cfg(feature = "ssr")] {
|
||||
use crate::api::auth::get_user;
|
||||
use server_fn::error::NoCustomError;
|
||||
|
||||
use crate::util::database::get_db_conn;
|
||||
use diesel::prelude::*;
|
||||
@ -25,46 +24,48 @@ cfg_if! {
|
||||
|
||||
/// Handle a user uploading a profile picture. Converts the image to webp and saves it to the server.
|
||||
#[server(input = MultipartFormData, endpoint = "/profile/upload_picture")]
|
||||
pub async fn upload_picture(data: MultipartData) -> Result<(), ServerFnError> {
|
||||
pub async fn upload_picture(data: MultipartData) -> BackendResult<()> {
|
||||
// Safe to unwrap - "On the server side, this always returns Some(_). On the client side, always returns None."
|
||||
let mut data = data.into_inner().unwrap();
|
||||
|
||||
let field = data
|
||||
.next_field()
|
||||
.await
|
||||
.map_err(|e| {
|
||||
ServerFnError::<NoCustomError>::ServerError(format!("Error getting field: {e}"))
|
||||
})?
|
||||
.ok_or_else(|| ServerFnError::<NoCustomError>::ServerError("No field found".to_string()))?;
|
||||
.map_err(|e| InputError::InvalidInput(format!("Error reading multipart data: {e}")))
|
||||
.context("Error getting next field from multipart data")?
|
||||
.ok_or_else(|| {
|
||||
InputError::InvalidInput("Expected a field in the multipart data".to_string())
|
||||
})?;
|
||||
|
||||
if field.name() != Some("picture") {
|
||||
return Err(ServerFnError::ServerError(
|
||||
"Field name is not 'picture'".to_string(),
|
||||
));
|
||||
return Err(InputError::InvalidInput(format!(
|
||||
"Expected field 'picture', got '{:?}'",
|
||||
field.name()
|
||||
))
|
||||
.into());
|
||||
}
|
||||
|
||||
// Get user id from session
|
||||
let user = get_user().await.map_err(|e| {
|
||||
ServerFnError::<NoCustomError>::ServerError(format!("Error getting user: {e}"))
|
||||
})?;
|
||||
let user = get_user().await.context("Error getting logged-in user")?;
|
||||
|
||||
// Read the image, and convert it to webp
|
||||
use image_convert::{to_webp, ImageResource, WEBPConfig};
|
||||
|
||||
let bytes = field.bytes().await.map_err(|e| {
|
||||
ServerFnError::<NoCustomError>::ServerError(format!("Error getting field bytes: {e}"))
|
||||
})?;
|
||||
let bytes = field
|
||||
.bytes()
|
||||
.await
|
||||
.map_err(|e| InputError::InvalidInput(format!("Error reading bytes from field: {e}")))
|
||||
.context("Error reading bytes of the picture field")?;
|
||||
|
||||
let reader = std::io::Cursor::new(bytes);
|
||||
let image_source = ImageResource::from_reader(reader).map_err(|e| {
|
||||
ServerFnError::<NoCustomError>::ServerError(format!("Error creating image resource: {e}"))
|
||||
})?;
|
||||
let image_source = ImageResource::from_reader(reader)
|
||||
.map_err(|e| InputError::InvalidInput(format!("Error creating image resource: {e}")))
|
||||
.context("Error creating image resource from reader")?;
|
||||
|
||||
let profile_picture_path = format!("assets/images/profile/{}.webp", user.id);
|
||||
let mut image_target = ImageResource::from_path(&profile_picture_path);
|
||||
to_webp(&mut image_target, &image_source, &WEBPConfig::new()).map_err(|e| {
|
||||
ServerFnError::<NoCustomError>::ServerError(format!("Error converting image to webp: {e}"))
|
||||
})?;
|
||||
to_webp(&mut image_target, &image_source, &WEBPConfig::new())
|
||||
.map_err(|e| InputError::InvalidInput(format!("Error converting image to webp: {e}")))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@ -78,13 +79,8 @@ pub async fn upload_picture(data: MultipartData) -> Result<(), ServerFnError> {
|
||||
pub async fn recent_songs(
|
||||
for_user_id: i32,
|
||||
limit: Option<i64>,
|
||||
) -> Result<Vec<(NaiveDateTime, frontend::Song)>, ServerFnError> {
|
||||
let viewing_user_id = get_user()
|
||||
.await
|
||||
.map_err(|e| {
|
||||
ServerFnError::<NoCustomError>::ServerError(format!("Error getting user: {e}"))
|
||||
})?
|
||||
.id;
|
||||
) -> BackendResult<Vec<(NaiveDateTime, frontend::Song)>> {
|
||||
let viewing_user_id = get_user().await.context("Error getting logged-in user")?.id;
|
||||
|
||||
let mut db_con = get_db_conn();
|
||||
|
||||
@ -138,7 +134,8 @@ pub async fn recent_songs(
|
||||
song_likes::all_columns.nullable(),
|
||||
song_dislikes::all_columns.nullable(),
|
||||
))
|
||||
.load(&mut db_con)?;
|
||||
.load(&mut db_con)
|
||||
.context("Error loading recent songs from database")?;
|
||||
|
||||
// Process the history data into a map of song ids to song data
|
||||
let mut history_songs: HashMap<i32, (NaiveDateTime, frontend::Song)> = HashMap::new();
|
||||
@ -201,13 +198,8 @@ pub async fn top_songs(
|
||||
start_date: NaiveDateTime,
|
||||
end_date: NaiveDateTime,
|
||||
limit: Option<i64>,
|
||||
) -> Result<Vec<(i64, frontend::Song)>, ServerFnError> {
|
||||
let viewing_user_id = get_user()
|
||||
.await
|
||||
.map_err(|e| {
|
||||
ServerFnError::<NoCustomError>::ServerError(format!("Error getting user: {e}"))
|
||||
})?
|
||||
.id;
|
||||
) -> BackendResult<Vec<(i64, frontend::Song)>> {
|
||||
let viewing_user_id = get_user().await.context("Error getting logged-in user")?.id;
|
||||
|
||||
let mut db_con = get_db_conn();
|
||||
|
||||
@ -220,14 +212,16 @@ pub async fn top_songs(
|
||||
.select((song_history::song_id, count(song_history::song_id)))
|
||||
.order(count(song_history::song_id).desc())
|
||||
.limit(limit)
|
||||
.load(&mut db_con)?
|
||||
.load(&mut db_con)
|
||||
.context("Error loading top song ids and counts from database")?
|
||||
} else {
|
||||
song_history::table
|
||||
.filter(song_history::date.between(start_date, end_date))
|
||||
.filter(song_history::user_id.eq(for_user_id))
|
||||
.group_by(song_history::song_id)
|
||||
.select((song_history::song_id, count(song_history::song_id)))
|
||||
.load(&mut db_con)?
|
||||
.load(&mut db_con)
|
||||
.context("Error loading top song ids and counts from database")?
|
||||
};
|
||||
|
||||
let history_counts: HashMap<i32, i64> = history_counts.into_iter().collect();
|
||||
@ -265,7 +259,8 @@ pub async fn top_songs(
|
||||
song_likes::all_columns.nullable(),
|
||||
song_dislikes::all_columns.nullable(),
|
||||
))
|
||||
.load(&mut db_con)?;
|
||||
.load(&mut db_con)
|
||||
.context("Error loading top songs from database")?;
|
||||
|
||||
// Process the history data into a map of song ids to song data
|
||||
let mut history_songs_map: HashMap<i32, (i64, frontend::Song)> =
|
||||
@ -308,8 +303,8 @@ pub async fn top_songs(
|
||||
|
||||
let plays = history_counts
|
||||
.get(&song.id)
|
||||
.ok_or(ServerFnError::ServerError::<NoCustomError>(
|
||||
"Song id not found in history counts".to_string(),
|
||||
.ok_or(BackendError::InternalError(
|
||||
"Song id not found in history counts",
|
||||
))?;
|
||||
|
||||
history_songs_map.insert(song.id, (*plays, songdata));
|
||||
@ -332,7 +327,7 @@ pub async fn top_artists(
|
||||
start_date: NaiveDateTime,
|
||||
end_date: NaiveDateTime,
|
||||
limit: Option<i64>,
|
||||
) -> Result<Vec<(i64, frontend::Artist)>, ServerFnError> {
|
||||
) -> BackendResult<Vec<(i64, frontend::Artist)>> {
|
||||
let mut db_con = get_db_conn();
|
||||
|
||||
let artist_counts: Vec<(i64, Artist)> = if let Some(limit) = limit {
|
||||
@ -345,7 +340,8 @@ pub async fn top_artists(
|
||||
.select((count(artists::id), artists::all_columns))
|
||||
.order(count(artists::id).desc())
|
||||
.limit(limit)
|
||||
.load(&mut db_con)?
|
||||
.load(&mut db_con)
|
||||
.context("Error loading top artists from database")?
|
||||
} else {
|
||||
song_history::table
|
||||
.filter(song_history::date.between(start_date, end_date))
|
||||
@ -355,7 +351,8 @@ pub async fn top_artists(
|
||||
.group_by(artists::id)
|
||||
.select((count(artists::id), artists::all_columns))
|
||||
.order(count(artists::id).desc())
|
||||
.load(&mut db_con)?
|
||||
.load(&mut db_con)
|
||||
.context("Error loading top artists from database")?
|
||||
};
|
||||
|
||||
let artist_data: Vec<(i64, frontend::Artist)> = artist_counts
|
||||
@ -376,13 +373,8 @@ pub async fn top_artists(
|
||||
}
|
||||
|
||||
#[server(endpoint = "/profile/liked_songs", client = Client)]
|
||||
pub async fn get_liked_songs() -> Result<Vec<frontend::Song>, ServerFnError> {
|
||||
let user_id = get_user()
|
||||
.await
|
||||
.map_err(|e| {
|
||||
ServerFnError::<NoCustomError>::ServerError(format!("Error getting user: {e}"))
|
||||
})?
|
||||
.id;
|
||||
pub async fn get_liked_songs() -> BackendResult<Vec<frontend::Song>> {
|
||||
let user_id = get_user().await.context("Error getting logged-in user")?.id;
|
||||
|
||||
let mut db_conn = get_db_conn();
|
||||
|
||||
@ -407,7 +399,8 @@ pub async fn get_liked_songs() -> Result<Vec<frontend::Song>, ServerFnError> {
|
||||
albums::all_columns.nullable(),
|
||||
artists::all_columns.nullable(),
|
||||
))
|
||||
.load(&mut db_conn)?;
|
||||
.load(&mut db_conn)
|
||||
.context("Error loading liked songs from database")?;
|
||||
|
||||
let mut liked_songs: HashMap<i32, frontend::Song> = HashMap::new();
|
||||
|
||||
|
@ -1,4 +1,5 @@
|
||||
use crate::models::frontend;
|
||||
use crate::util::error::*;
|
||||
use crate::util::serverfn_client::Client;
|
||||
use leptos::prelude::*;
|
||||
|
||||
@ -49,7 +50,7 @@ pub type SearchResults<T> = Vec<(T, f32)>;
|
||||
pub async fn search_albums(
|
||||
query: String,
|
||||
limit: i64,
|
||||
) -> Result<SearchResults<frontend::Album>, ServerFnError> {
|
||||
) -> BackendResult<SearchResults<frontend::Album>> {
|
||||
use crate::schema::*;
|
||||
|
||||
let mut db_conn = get_db_conn();
|
||||
@ -59,7 +60,8 @@ pub async fn search_albums(
|
||||
.order_by(trgm_distance(albums::title, query.clone()).desc())
|
||||
.limit(limit)
|
||||
.select(albums::id)
|
||||
.load::<i32>(&mut db_conn)?;
|
||||
.load::<i32>(&mut db_conn)
|
||||
.context("Error loading album ids from database")?;
|
||||
|
||||
let mut albums_map: HashMap<i32, (frontend::Album, f32)> = HashMap::new();
|
||||
|
||||
@ -75,7 +77,8 @@ pub async fn search_albums(
|
||||
artists::all_columns,
|
||||
trgm_distance(albums::title, query.clone()),
|
||||
))
|
||||
.load(&mut db_conn)?;
|
||||
.load(&mut db_conn)
|
||||
.context("Error loading album artists from database")?;
|
||||
|
||||
for (album, artist, score) in album_artists {
|
||||
if let Some((stored_album, _score)) = albums_map.get_mut(&album.id) {
|
||||
@ -113,7 +116,7 @@ pub async fn search_albums(
|
||||
pub async fn search_artists(
|
||||
query: String,
|
||||
limit: i64,
|
||||
) -> Result<SearchResults<frontend::Artist>, ServerFnError> {
|
||||
) -> BackendResult<SearchResults<frontend::Artist>> {
|
||||
use crate::schema::*;
|
||||
|
||||
let mut db_conn = get_db_conn();
|
||||
@ -123,7 +126,8 @@ pub async fn search_artists(
|
||||
.order_by(trgm_distance(artists::name, query.clone()).desc())
|
||||
.limit(limit)
|
||||
.select((artists::all_columns, trgm_distance(artists::name, query)))
|
||||
.load::<(backend::Artist, f32)>(&mut db_conn)?;
|
||||
.load::<(backend::Artist, f32)>(&mut db_conn)
|
||||
.context("Error loading artists from database")?;
|
||||
|
||||
let artist_data = artist_list
|
||||
.into_iter()
|
||||
@ -154,11 +158,13 @@ pub async fn search_artists(
|
||||
pub async fn search_songs(
|
||||
query: String,
|
||||
limit: i64,
|
||||
) -> Result<SearchResults<frontend::Song>, ServerFnError> {
|
||||
) -> BackendResult<SearchResults<frontend::Song>> {
|
||||
use crate::api::auth::get_logged_in_user;
|
||||
use crate::schema::*;
|
||||
|
||||
let user = get_logged_in_user().await?;
|
||||
let user = get_logged_in_user()
|
||||
.await
|
||||
.context("Error getting logged-in user")?;
|
||||
|
||||
let mut db_conn = get_db_conn();
|
||||
|
||||
@ -198,7 +204,8 @@ pub async fn search_songs(
|
||||
song_dislikes::all_columns.nullable(),
|
||||
trgm_distance(songs::title, query.clone()),
|
||||
))
|
||||
.load(&mut db_conn)?;
|
||||
.load(&mut db_conn)
|
||||
.context("Error loading songs from database")?;
|
||||
|
||||
song_list
|
||||
} else {
|
||||
@ -223,7 +230,8 @@ pub async fn search_songs(
|
||||
artists::all_columns.nullable(),
|
||||
trgm_distance(songs::title, query.clone()),
|
||||
))
|
||||
.load(&mut db_conn)?;
|
||||
.load(&mut db_conn)
|
||||
.context("Error loading songs from database")?;
|
||||
|
||||
song_list
|
||||
.into_iter()
|
||||
@ -293,14 +301,11 @@ pub async fn search_songs(
|
||||
pub async fn search(
|
||||
query: String,
|
||||
limit: i64,
|
||||
) -> Result<
|
||||
(
|
||||
SearchResults<frontend::Album>,
|
||||
SearchResults<frontend::Artist>,
|
||||
SearchResults<frontend::Song>,
|
||||
),
|
||||
ServerFnError,
|
||||
> {
|
||||
) -> BackendResult<(
|
||||
SearchResults<frontend::Album>,
|
||||
SearchResults<frontend::Artist>,
|
||||
SearchResults<frontend::Song>,
|
||||
)> {
|
||||
let albums = search_albums(query.clone(), limit);
|
||||
let artists = search_artists(query.clone(), limit);
|
||||
let songs = search_songs(query, limit);
|
||||
@ -308,5 +313,10 @@ pub async fn search(
|
||||
use tokio::join;
|
||||
|
||||
let (albums, artists, songs) = join!(albums, artists, songs);
|
||||
Ok((albums?, artists?, songs?))
|
||||
|
||||
let albums = albums.context("Error searching for albums")?;
|
||||
let artists = artists.context("Error searching for artists")?;
|
||||
let songs = songs.context("Error searching for songs")?;
|
||||
|
||||
Ok((albums, artists, songs))
|
||||
}
|
||||
|
@ -3,11 +3,11 @@ 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 leptos::server_fn::error::NoCustomError;
|
||||
use crate::util::database::get_db_conn;
|
||||
use crate::api::auth::get_user;
|
||||
use crate::models::backend::{Song, Album, Artist};
|
||||
@ -17,66 +17,56 @@ cfg_if! {
|
||||
|
||||
/// Like or unlike a song
|
||||
#[server(endpoint = "songs/set_like", client = Client)]
|
||||
pub async fn set_like_song(song_id: i32, like: bool) -> Result<(), ServerFnError> {
|
||||
let user = get_user().await.map_err(|e| {
|
||||
ServerFnError::<NoCustomError>::ServerError(format!("Error getting user: {e}"))
|
||||
})?;
|
||||
pub async fn set_like_song(song_id: i32, like: bool) -> BackendResult<()> {
|
||||
let user = get_user().await.context("Error getting logged-in user")?;
|
||||
|
||||
let db_con = &mut get_db_conn();
|
||||
|
||||
user.set_like_song(song_id, like, db_con)
|
||||
.await
|
||||
.map_err(|e| ServerFnError::<NoCustomError>::ServerError(format!("Error liking song: {e}")))
|
||||
.context("Error setting like status for song")
|
||||
}
|
||||
|
||||
/// Dislike or remove dislike from a song
|
||||
#[server(endpoint = "songs/set_dislike", client = Client)]
|
||||
pub async fn set_dislike_song(song_id: i32, dislike: bool) -> Result<(), ServerFnError> {
|
||||
let user = get_user().await.map_err(|e| {
|
||||
ServerFnError::<NoCustomError>::ServerError(format!("Error getting user: {e}"))
|
||||
})?;
|
||||
pub async fn set_dislike_song(song_id: i32, dislike: bool) -> BackendResult<()> {
|
||||
let user = get_user().await.context("Error getting logged-in user")?;
|
||||
|
||||
let db_con = &mut get_db_conn();
|
||||
|
||||
user.set_dislike_song(song_id, dislike, db_con)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
ServerFnError::<NoCustomError>::ServerError(format!("Error disliking song: {e}"))
|
||||
})
|
||||
.context("Error setting dislike status for song")
|
||||
}
|
||||
|
||||
/// Get the like and dislike status of a song
|
||||
#[server(endpoint = "songs/get_like_dislike", client = Client)]
|
||||
pub async fn get_like_dislike_song(song_id: i32) -> Result<(bool, bool), ServerFnError> {
|
||||
let user = get_user().await.map_err(|e| {
|
||||
ServerFnError::<NoCustomError>::ServerError(format!("Error getting user: {e}"))
|
||||
})?;
|
||||
pub async fn get_like_dislike_song(song_id: i32) -> BackendResult<(bool, bool)> {
|
||||
let user = get_user().await.context("Error getting logged-in user")?;
|
||||
|
||||
let db_con = &mut get_db_conn();
|
||||
|
||||
// TODO this could probably be done more efficiently with a tokio::try_join, but
|
||||
// doing so is much more complicated than it would initially seem
|
||||
|
||||
let like = user.get_like_song(song_id, db_con).await.map_err(|e| {
|
||||
ServerFnError::<NoCustomError>::ServerError(format!("Error getting song liked: {e}"))
|
||||
})?;
|
||||
let dislike = user.get_dislike_song(song_id, db_con).await.map_err(|e| {
|
||||
ServerFnError::<NoCustomError>::ServerError(format!("Error getting song disliked: {e}"))
|
||||
})?;
|
||||
let like = user
|
||||
.get_like_song(song_id, db_con)
|
||||
.await
|
||||
.context("Error getting song like status")?;
|
||||
|
||||
let dislike = user
|
||||
.get_dislike_song(song_id, db_con)
|
||||
.await
|
||||
.context("Error getting song dislike status")?;
|
||||
|
||||
Ok((like, dislike))
|
||||
}
|
||||
|
||||
#[server(endpoint = "songs/get", client = Client)]
|
||||
pub async fn get_song_by_id(song_id: i32) -> Result<Option<frontend::Song>, ServerFnError> {
|
||||
pub async fn get_song_by_id(song_id: i32) -> BackendResult<Option<frontend::Song>> {
|
||||
use crate::schema::*;
|
||||
|
||||
let user_id: i32 = get_user()
|
||||
.await
|
||||
.map_err(|e| {
|
||||
ServerFnError::<NoCustomError>::ServerError(format!("Error getting user: {e}"))
|
||||
})?
|
||||
.id;
|
||||
let user_id: i32 = get_user().await.context("Error getting logged-in user")?.id;
|
||||
|
||||
let db_con = &mut get_db_conn();
|
||||
|
||||
@ -111,7 +101,8 @@ pub async fn get_song_by_id(song_id: i32) -> Result<Option<frontend::Song>, Serv
|
||||
song_likes::all_columns.nullable(),
|
||||
song_dislikes::all_columns.nullable(),
|
||||
))
|
||||
.load(db_con)?;
|
||||
.load(db_con)
|
||||
.context("Error loading song from database")?;
|
||||
|
||||
let song = song_parts.first().cloned();
|
||||
let artists = song_parts
|
||||
@ -148,7 +139,7 @@ pub async fn get_song_by_id(song_id: i32) -> Result<Option<frontend::Song>, Serv
|
||||
}
|
||||
|
||||
#[server(endpoint = "songs/plays", client = Client)]
|
||||
pub async fn get_song_plays(song_id: i32) -> Result<i64, ServerFnError> {
|
||||
pub async fn get_song_plays(song_id: i32) -> BackendResult<i64> {
|
||||
use crate::schema::*;
|
||||
|
||||
let db_con = &mut get_db_conn();
|
||||
@ -157,23 +148,16 @@ pub async fn get_song_plays(song_id: i32) -> Result<i64, ServerFnError> {
|
||||
.filter(song_history::song_id.eq(song_id))
|
||||
.count()
|
||||
.get_result::<i64>(db_con)
|
||||
.map_err(|e| {
|
||||
ServerFnError::<NoCustomError>::ServerError(format!("Error getting song plays: {e}"))
|
||||
})?;
|
||||
.context("Error getting song plays")?;
|
||||
|
||||
Ok(plays)
|
||||
}
|
||||
|
||||
#[server(endpoint = "songs/my-plays", client = Client)]
|
||||
pub async fn get_my_song_plays(song_id: i32) -> Result<i64, ServerFnError> {
|
||||
pub async fn get_my_song_plays(song_id: i32) -> BackendResult<i64> {
|
||||
use crate::schema::*;
|
||||
|
||||
let user_id: i32 = get_user()
|
||||
.await
|
||||
.map_err(|e| {
|
||||
ServerFnError::<NoCustomError>::ServerError(format!("Error getting user: {e}"))
|
||||
})?
|
||||
.id;
|
||||
let user_id: i32 = get_user().await.context("Error getting logged-in user")?.id;
|
||||
|
||||
let db_con = &mut get_db_conn();
|
||||
|
||||
@ -185,9 +169,7 @@ pub async fn get_my_song_plays(song_id: i32) -> Result<i64, ServerFnError> {
|
||||
)
|
||||
.count()
|
||||
.get_result::<i64>(db_con)
|
||||
.map_err(|e| {
|
||||
ServerFnError::<NoCustomError>::ServerError(format!("Error getting song plays: {e}"))
|
||||
})?;
|
||||
.context("Error getting song plays for user")?;
|
||||
|
||||
Ok(plays)
|
||||
}
|
||||
|
@ -1,3 +1,4 @@
|
||||
use crate::util::error::*;
|
||||
use leptos::prelude::*;
|
||||
use server_fn::codec::{MultipartData, MultipartFormData};
|
||||
|
||||
@ -10,7 +11,6 @@ cfg_if! {
|
||||
use crate::util::extract_field::extract_field;
|
||||
use diesel::prelude::*;
|
||||
use log::*;
|
||||
use server_fn::error::NoCustomError;
|
||||
use chrono::NaiveDate;
|
||||
}
|
||||
}
|
||||
@ -18,7 +18,7 @@ cfg_if! {
|
||||
/// Validate the artist ids in a multipart field
|
||||
/// Expects a field with a comma-separated list of artist ids, and ensures each is a valid artist id in the database
|
||||
#[cfg(feature = "ssr")]
|
||||
async fn validate_artist_ids(artist_ids: Field<'static>) -> Result<Vec<i32>, ServerFnError> {
|
||||
async fn validate_artist_ids(artist_ids: Field<'static>) -> BackendResult<Vec<i32>> {
|
||||
use crate::models::backend::Artist;
|
||||
use diesel::result::Error::NotFound;
|
||||
|
||||
@ -40,32 +40,24 @@ async fn validate_artist_ids(artist_ids: Field<'static>) -> Result<Vec<i32>, Ser
|
||||
|
||||
match artist {
|
||||
Ok(_) => Ok(artist_id),
|
||||
Err(NotFound) => Err(ServerFnError::<NoCustomError>::ServerError(
|
||||
"Artist does not exist".to_string(),
|
||||
)),
|
||||
Err(e) => Err(ServerFnError::<NoCustomError>::ServerError(format!(
|
||||
"Error finding artist id: {e}"
|
||||
))),
|
||||
Err(NotFound) => Err(AccessError::NotFound.context("Artist not found")),
|
||||
Err(e) => Err(e.context("Error finding artist id")),
|
||||
}
|
||||
} else {
|
||||
Err(ServerFnError::<NoCustomError>::ServerError(
|
||||
"Error parsing artist id".to_string(),
|
||||
))
|
||||
Err(InputError::InvalidInput("Error parsing artist id".to_string()).into())
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
Err(e) => Err(ServerFnError::<NoCustomError>::ServerError(format!(
|
||||
"Error reading artist id: {e}"
|
||||
))),
|
||||
Err(e) => Err(InputError::FieldReadError(format!("Error reading artist ids: {e}")).into()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Validate the album id in a multipart field
|
||||
/// Expects a field with an album id, and ensures it is a valid album id in the database
|
||||
#[cfg(feature = "ssr")]
|
||||
async fn validate_album_id(album_id: Field<'static>) -> Result<Option<i32>, ServerFnError> {
|
||||
async fn validate_album_id(album_id: Field<'static>) -> BackendResult<Option<i32>> {
|
||||
use crate::models::backend::Album;
|
||||
use diesel::result::Error::NotFound;
|
||||
|
||||
@ -86,29 +78,21 @@ async fn validate_album_id(album_id: Field<'static>) -> Result<Option<i32>, Serv
|
||||
|
||||
match album {
|
||||
Ok(_) => Ok(Some(album_id)),
|
||||
Err(NotFound) => Err(ServerFnError::<NoCustomError>::ServerError(
|
||||
"Album does not exist".to_string(),
|
||||
)),
|
||||
Err(e) => Err(ServerFnError::<NoCustomError>::ServerError(format!(
|
||||
"Error finding album id: {e}"
|
||||
))),
|
||||
Err(NotFound) => Err(AccessError::NotFound.context("Album not found")),
|
||||
Err(e) => Err(e.context("Error finding album id")),
|
||||
}
|
||||
} else {
|
||||
Err(ServerFnError::<NoCustomError>::ServerError(
|
||||
"Error parsing album id".to_string(),
|
||||
))
|
||||
Err(InputError::InvalidInput("Error parsing album id".to_string()).into())
|
||||
}
|
||||
}
|
||||
Err(e) => Err(ServerFnError::<NoCustomError>::ServerError(format!(
|
||||
"Error reading album id: {e}"
|
||||
))),
|
||||
Err(e) => Err(InputError::FieldReadError(format!("Error reading album id: {e}")).into()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Validate the track number in a multipart field
|
||||
/// Expects a field with a track number, and ensures it is a valid track number (non-negative integer)
|
||||
#[cfg(feature = "ssr")]
|
||||
async fn validate_track_number(track_number: Field<'static>) -> Result<Option<i32>, ServerFnError> {
|
||||
async fn validate_track_number(track_number: Field<'static>) -> BackendResult<Option<i32>> {
|
||||
match track_number.text().await {
|
||||
Ok(track_number) => {
|
||||
if track_number.is_empty() {
|
||||
@ -117,30 +101,27 @@ async fn validate_track_number(track_number: Field<'static>) -> Result<Option<i3
|
||||
|
||||
if let Ok(track_number) = track_number.parse::<i32>() {
|
||||
if track_number < 0 {
|
||||
Err(ServerFnError::<NoCustomError>::ServerError(
|
||||
"Track number must be positive or 0".to_string(),
|
||||
))
|
||||
Err(
|
||||
InputError::InvalidInput("Track number must be positive or 0".to_string())
|
||||
.into(),
|
||||
)
|
||||
} else {
|
||||
Ok(Some(track_number))
|
||||
}
|
||||
} else {
|
||||
Err(ServerFnError::<NoCustomError>::ServerError(
|
||||
"Error parsing track number".to_string(),
|
||||
))
|
||||
Err(InputError::InvalidInput("Error parsing track number".to_string()).into())
|
||||
}
|
||||
}
|
||||
Err(e) => Err(ServerFnError::<NoCustomError>::ServerError(format!(
|
||||
"Error reading track number: {e}"
|
||||
)))?,
|
||||
Err(e) => {
|
||||
Err(InputError::FieldReadError(format!("Error reading track number: {e}")).into())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Validate the release date in a multipart field
|
||||
/// Expects a field with a release date, and ensures it is a valid date in the format [year]-[month]-[day]
|
||||
#[cfg(feature = "ssr")]
|
||||
async fn validate_release_date(
|
||||
release_date: Field<'static>,
|
||||
) -> Result<Option<NaiveDate>, ServerFnError> {
|
||||
async fn validate_release_date(release_date: Field<'static>) -> BackendResult<Option<NaiveDate>> {
|
||||
match release_date.text().await {
|
||||
Ok(release_date) => {
|
||||
if release_date.trim().is_empty() {
|
||||
@ -151,20 +132,19 @@ async fn validate_release_date(
|
||||
|
||||
match release_date {
|
||||
Ok(release_date) => Ok(Some(release_date)),
|
||||
Err(_) => Err(ServerFnError::<NoCustomError>::ServerError(
|
||||
"Invalid release date".to_string(),
|
||||
)),
|
||||
Err(_) => Err(InputError::InvalidInput(
|
||||
"Invalid release date format, expected YYYY-MM-DD".to_string(),
|
||||
)
|
||||
.into()),
|
||||
}
|
||||
}
|
||||
Err(e) => Err(ServerFnError::<NoCustomError>::ServerError(format!(
|
||||
"Error reading release date: {e}"
|
||||
))),
|
||||
Err(e) => Err(InputError::InvalidInput(format!("Error reading release date: {e}")).into()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Handle the file upload form
|
||||
#[server(input = MultipartFormData, endpoint = "/upload")]
|
||||
pub async fn upload(data: MultipartData) -> Result<(), ServerFnError> {
|
||||
pub async fn upload(data: MultipartData) -> BackendResult<()> {
|
||||
// Safe to unwrap - "On the server side, this always returns Some(_). On the client side, always returns None."
|
||||
let mut data = data.into_inner().unwrap();
|
||||
|
||||
@ -182,19 +162,39 @@ pub async fn upload(data: MultipartData) -> Result<(), ServerFnError> {
|
||||
|
||||
match name.as_str() {
|
||||
"title" => {
|
||||
title = Some(extract_field(field).await?);
|
||||
title = Some(
|
||||
extract_field(field)
|
||||
.await
|
||||
.context("Error extracting title field")?,
|
||||
);
|
||||
}
|
||||
"artist_ids" => {
|
||||
artist_ids = Some(validate_artist_ids(field).await?);
|
||||
artist_ids = Some(
|
||||
validate_artist_ids(field)
|
||||
.await
|
||||
.context("Error validating artist ids")?,
|
||||
);
|
||||
}
|
||||
"album_id" => {
|
||||
album_id = Some(validate_album_id(field).await?);
|
||||
album_id = Some(
|
||||
validate_album_id(field)
|
||||
.await
|
||||
.context("Error validating album id")?,
|
||||
);
|
||||
}
|
||||
"track_number" => {
|
||||
track = Some(validate_track_number(field).await?);
|
||||
track = Some(
|
||||
validate_track_number(field)
|
||||
.await
|
||||
.context("Error validating track number")?,
|
||||
);
|
||||
}
|
||||
"release_date" => {
|
||||
release_date = Some(validate_release_date(field).await?);
|
||||
release_date = Some(
|
||||
validate_release_date(field)
|
||||
.await
|
||||
.context("Error validating release date")?,
|
||||
);
|
||||
}
|
||||
"file" => {
|
||||
use crate::util::audio::extract_metadata;
|
||||
@ -206,11 +206,9 @@ pub async fn upload(data: MultipartData) -> Result<(), ServerFnError> {
|
||||
// or behavior that we may wish to change in the future
|
||||
|
||||
// Create file name
|
||||
let title = title
|
||||
.clone()
|
||||
.ok_or(ServerFnError::<NoCustomError>::ServerError(
|
||||
"Title field required and must precede file field".to_string(),
|
||||
))?;
|
||||
let title = title.clone().ok_or(InputError::InvalidInput(
|
||||
"Title field must be present and must precede file field".to_string(),
|
||||
))?;
|
||||
|
||||
let clean_title = title.replace(" ", "_").replace("/", "_");
|
||||
let date_str = chrono::Utc::now().format("%Y-%m-%d_%H:%M:%S").to_string();
|
||||
@ -226,28 +224,30 @@ pub async fn upload(data: MultipartData) -> Result<(), ServerFnError> {
|
||||
.write(true)
|
||||
.create(true)
|
||||
.truncate(true)
|
||||
.open(upload_path.clone())?;
|
||||
.open(upload_path.clone())
|
||||
.context("Error opening file for upload")?;
|
||||
|
||||
while let Some(chunk) = field.chunk().await? {
|
||||
file.write_all(&chunk)?;
|
||||
while let Some(chunk) = field.chunk().await.map_err(|e| {
|
||||
InputError::FieldReadError(format!("Error reading file chunk: {e}"))
|
||||
})? {
|
||||
file.write_all(&chunk)
|
||||
.context("Error writing field chunk to file")?;
|
||||
}
|
||||
|
||||
file.flush()?;
|
||||
file.flush().context("Error flusing file")?;
|
||||
|
||||
// Rewind the file so the duration can be measured
|
||||
file.rewind()?;
|
||||
file.rewind().context("Error rewinding file")?;
|
||||
|
||||
// Get the codec and duration of the file
|
||||
let (file_codec, file_duration) = extract_metadata(file).map_err(|e| {
|
||||
let msg = format!("Error measuring duration of audio file {upload_path}: {e}");
|
||||
warn!("{}", msg);
|
||||
ServerFnError::<NoCustomError>::ServerError(msg)
|
||||
})?;
|
||||
let (file_codec, file_duration) = extract_metadata(file)
|
||||
.context("Error extracting metadata from uploaded file")?;
|
||||
|
||||
if file_codec != CODEC_TYPE_MP3 {
|
||||
let msg = format!("Invalid uploaded audio file codec: {file_codec}");
|
||||
warn!("{}", msg);
|
||||
return Err(ServerFnError::<NoCustomError>::ServerError(msg));
|
||||
return Err(InputError::InvalidInput(format!(
|
||||
"Invalid uploaded audio file codec: {file_codec}"
|
||||
))
|
||||
.into());
|
||||
}
|
||||
|
||||
duration = Some(file_duration);
|
||||
@ -259,30 +259,23 @@ pub async fn upload(data: MultipartData) -> Result<(), ServerFnError> {
|
||||
}
|
||||
|
||||
// Unwrap mandatory fields
|
||||
let title = title.ok_or(ServerFnError::<NoCustomError>::ServerError(
|
||||
"Missing title".to_string(),
|
||||
))?;
|
||||
let title = title.ok_or(InputError::MissingField("title".to_string()))?;
|
||||
let artist_ids = artist_ids.unwrap_or(vec![]);
|
||||
let file_name = file_name.ok_or(ServerFnError::<NoCustomError>::ServerError(
|
||||
"Missing file".to_string(),
|
||||
))?;
|
||||
let duration = duration.ok_or(ServerFnError::<NoCustomError>::ServerError(
|
||||
"Missing duration".to_string(),
|
||||
))?;
|
||||
let duration = i32::try_from(duration).map_err(|e| {
|
||||
ServerFnError::<NoCustomError>::ServerError(format!(
|
||||
"Error converting duration to i32: {e}"
|
||||
))
|
||||
})?;
|
||||
let file_name = file_name.ok_or(InputError::MissingField("file".to_string()))?;
|
||||
let duration = duration.ok_or(InputError::MissingField("duration".to_string()))?;
|
||||
let duration = i32::try_from(duration)
|
||||
.map_err(|e| InputError::InvalidInput(format!("Error parsing duration: {e}")))
|
||||
.context("Error converting duration to i32")?;
|
||||
|
||||
let album_id = album_id.unwrap_or(None);
|
||||
let track = track.unwrap_or(None);
|
||||
let release_date = release_date.unwrap_or(None);
|
||||
|
||||
if album_id.is_some() != track.is_some() {
|
||||
return Err(ServerFnError::<NoCustomError>::ServerError(
|
||||
return Err(InputError::InvalidInput(
|
||||
"Album id and track number must both be present or both be absent".to_string(),
|
||||
));
|
||||
)
|
||||
.into());
|
||||
}
|
||||
|
||||
// Create the song
|
||||
@ -302,11 +295,7 @@ pub async fn upload(data: MultipartData) -> Result<(), ServerFnError> {
|
||||
let song = song
|
||||
.insert_into(crate::schema::songs::table)
|
||||
.get_result::<Song>(db_con)
|
||||
.map_err(|e| {
|
||||
let msg = format!("Error saving song to database: {e}");
|
||||
warn!("{}", msg);
|
||||
ServerFnError::<NoCustomError>::ServerError(msg)
|
||||
})?;
|
||||
.context("Error adding song to database")?;
|
||||
|
||||
// Save the song's artists to the database
|
||||
use crate::schema::song_artists;
|
||||
@ -325,11 +314,7 @@ pub async fn upload(data: MultipartData) -> Result<(), ServerFnError> {
|
||||
diesel::insert_into(crate::schema::song_artists::table)
|
||||
.values(&artist_ids)
|
||||
.execute(db_con)
|
||||
.map_err(|e| {
|
||||
let msg = format!("Error saving song artists to database: {e}");
|
||||
warn!("{}", msg);
|
||||
ServerFnError::<NoCustomError>::ServerError(msg)
|
||||
})?;
|
||||
.context("Error saving song artists to database")?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
@ -16,6 +16,7 @@ cfg_if::cfg_if! {
|
||||
}
|
||||
|
||||
use crate::models::backend::User;
|
||||
use crate::util::error::*;
|
||||
use crate::util::serverfn_client::Client;
|
||||
use leptos::prelude::*;
|
||||
use serde::{Deserialize, Serialize};
|
||||
@ -29,9 +30,8 @@ pub struct UserCredentials {
|
||||
/// Get a user from the database by username or email
|
||||
/// Returns a Result with the user if found, None if not found, or an error if there was a problem
|
||||
#[cfg(feature = "ssr")]
|
||||
pub async fn find_user(username_or_email: String) -> Result<Option<User>, ServerFnError> {
|
||||
pub async fn find_user(username_or_email: String) -> BackendResult<Option<User>> {
|
||||
use crate::schema::users::dsl::*;
|
||||
use leptos::server_fn::error::NoCustomError;
|
||||
|
||||
// Look for either a username or email that matches the input, and return an option with None if no user is found
|
||||
let db_con = &mut get_db_conn();
|
||||
@ -40,11 +40,7 @@ pub async fn find_user(username_or_email: String) -> Result<Option<User>, Server
|
||||
.or_filter(email.eq(username_or_email))
|
||||
.first::<User>(db_con)
|
||||
.optional()
|
||||
.map_err(|e| {
|
||||
ServerFnError::<NoCustomError>::ServerError(format!(
|
||||
"Error getting user from database: {e}"
|
||||
))
|
||||
})?;
|
||||
.context("Error loading user from database")?;
|
||||
|
||||
Ok(user)
|
||||
}
|
||||
@ -52,20 +48,15 @@ pub async fn find_user(username_or_email: String) -> Result<Option<User>, Server
|
||||
/// Get a user from the database by ID
|
||||
/// Returns a Result with the user if found, None if not found, or an error if there was a problem
|
||||
#[cfg(feature = "ssr")]
|
||||
pub async fn find_user_by_id(user_id: i32) -> Result<Option<User>, ServerFnError> {
|
||||
pub async fn find_user_by_id(user_id: i32) -> BackendResult<Option<User>> {
|
||||
use crate::schema::users::dsl::*;
|
||||
use leptos::server_fn::error::NoCustomError;
|
||||
|
||||
let db_con = &mut get_db_conn();
|
||||
let user = users
|
||||
.filter(id.eq(user_id))
|
||||
.first::<User>(db_con)
|
||||
.optional()
|
||||
.map_err(|e| {
|
||||
ServerFnError::<NoCustomError>::ServerError(format!(
|
||||
"Error getting user from database: {e}"
|
||||
))
|
||||
})?;
|
||||
.context("Error loading user from database")?;
|
||||
|
||||
Ok(user)
|
||||
}
|
||||
@ -73,25 +64,21 @@ pub async fn find_user_by_id(user_id: i32) -> Result<Option<User>, ServerFnError
|
||||
/// Create a new user in the database
|
||||
/// Returns an empty Result if successful, or an error if there was a problem
|
||||
#[cfg(feature = "ssr")]
|
||||
pub async fn create_user(new_user: &NewUser) -> Result<(), ServerFnError> {
|
||||
pub async fn create_user(new_user: &NewUser) -> BackendResult<()> {
|
||||
use crate::schema::users::dsl::*;
|
||||
use leptos::server_fn::error::NoCustomError;
|
||||
|
||||
let new_password =
|
||||
new_user
|
||||
.password
|
||||
.clone()
|
||||
.ok_or(ServerFnError::<NoCustomError>::ServerError(format!(
|
||||
"No password provided for user {}",
|
||||
new_user.username
|
||||
.ok_or(BackendError::InputError(InputError::MissingField(
|
||||
"password".to_string(),
|
||||
)))?;
|
||||
|
||||
let salt = SaltString::generate(&mut OsRng);
|
||||
let password_hash = Pbkdf2
|
||||
.hash_password(new_password.as_bytes(), &salt)
|
||||
.map_err(|_| {
|
||||
ServerFnError::<NoCustomError>::ServerError("Error hashing password".to_string())
|
||||
})?
|
||||
.map_err(|e| AuthError::AuthError(format!("Error hashing password: {e}")))?
|
||||
.to_string();
|
||||
|
||||
let new_user = NewUser {
|
||||
@ -104,9 +91,7 @@ pub async fn create_user(new_user: &NewUser) -> Result<(), ServerFnError> {
|
||||
diesel::insert_into(users)
|
||||
.values(&new_user)
|
||||
.execute(db_con)
|
||||
.map_err(|e| {
|
||||
ServerFnError::<NoCustomError>::ServerError(format!("Error creating user: {e}"))
|
||||
})?;
|
||||
.context("Error inserting new user into database")?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@ -114,16 +99,10 @@ pub async fn create_user(new_user: &NewUser) -> Result<(), ServerFnError> {
|
||||
/// Validate a user's credentials
|
||||
/// Returns a Result with the user if the credentials are valid, None if not valid, or an error if there was a problem
|
||||
#[cfg(feature = "ssr")]
|
||||
pub async fn validate_user(credentials: UserCredentials) -> Result<Option<User>, ServerFnError> {
|
||||
use leptos::server_fn::error::NoCustomError;
|
||||
|
||||
pub async fn validate_user(credentials: UserCredentials) -> BackendResult<Option<User>> {
|
||||
let db_user = find_user(credentials.username_or_email.clone())
|
||||
.await
|
||||
.map_err(|e| {
|
||||
ServerFnError::<NoCustomError>::ServerError(format!(
|
||||
"Error getting user from database: {e}"
|
||||
))
|
||||
})?;
|
||||
.context("Error finding user in database")?;
|
||||
|
||||
// If the user is not found, return None
|
||||
let db_user = match db_user {
|
||||
@ -131,18 +110,13 @@ pub async fn validate_user(credentials: UserCredentials) -> Result<Option<User>,
|
||||
None => return Ok(None),
|
||||
};
|
||||
|
||||
let db_password =
|
||||
db_user
|
||||
.password
|
||||
.clone()
|
||||
.ok_or(ServerFnError::<NoCustomError>::ServerError(format!(
|
||||
"No password found for user {}",
|
||||
db_user.username
|
||||
)))?;
|
||||
let db_password = db_user.password.clone().ok_or(AuthError::AuthError(
|
||||
"No password stored for user".to_string(),
|
||||
))?;
|
||||
|
||||
let password_hash = PasswordHash::new(&db_password).map_err(|e| {
|
||||
ServerFnError::<NoCustomError>::ServerError(format!("Error hashing supplied password: {e}"))
|
||||
})?;
|
||||
let password_hash = PasswordHash::new(&db_password)
|
||||
.map_err(|e| AuthError::AuthError(format!("{e}")))
|
||||
.context("Error parsing password hash from database")?;
|
||||
|
||||
match Pbkdf2.verify_password(credentials.password.as_bytes(), &password_hash) {
|
||||
Ok(()) => {}
|
||||
@ -150,9 +124,9 @@ pub async fn validate_user(credentials: UserCredentials) -> Result<Option<User>,
|
||||
return Ok(None);
|
||||
}
|
||||
Err(e) => {
|
||||
return Err(ServerFnError::<NoCustomError>::ServerError(format!(
|
||||
"Error verifying password: {e}"
|
||||
)));
|
||||
return Err(
|
||||
AuthError::AuthError(format!("{e}")).context("Error verifying password hash")
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -162,8 +136,10 @@ pub async fn validate_user(credentials: UserCredentials) -> Result<Option<User>,
|
||||
/// Get a user from the database by username or email
|
||||
/// Returns a Result with the user if found, None if not found, or an error if there was a problem
|
||||
#[server(endpoint = "find_user", client = Client)]
|
||||
pub async fn get_user(username_or_email: String) -> Result<Option<User>, ServerFnError> {
|
||||
let mut user = find_user(username_or_email).await?;
|
||||
pub async fn get_user(username_or_email: String) -> BackendResult<Option<User>> {
|
||||
let mut user = find_user(username_or_email)
|
||||
.await
|
||||
.context("Error finding user by username or email")?;
|
||||
|
||||
// Remove the password hash before returning the user
|
||||
if let Some(user) = user.as_mut() {
|
||||
@ -174,8 +150,10 @@ pub async fn get_user(username_or_email: String) -> Result<Option<User>, ServerF
|
||||
}
|
||||
|
||||
#[server(endpoint = "get_user_by_id", client = Client)]
|
||||
pub async fn get_user_by_id(user_id: i32) -> Result<Option<User>, ServerFnError> {
|
||||
let mut user = find_user_by_id(user_id).await?;
|
||||
pub async fn get_user_by_id(user_id: i32) -> BackendResult<Option<User>> {
|
||||
let mut user = find_user_by_id(user_id)
|
||||
.await
|
||||
.context("Error finding user by ID")?;
|
||||
|
||||
// Remove the password hash before returning the user
|
||||
if let Some(user) = user.as_mut() {
|
||||
|
@ -2,6 +2,7 @@ use crate::components::error::Error;
|
||||
use crate::components::loading::*;
|
||||
use crate::components::menu::*;
|
||||
use crate::util::state::GlobalState;
|
||||
use leptos::either::*;
|
||||
use leptos::html::Div;
|
||||
use leptos::prelude::*;
|
||||
use leptos_icons::*;
|
||||
@ -121,49 +122,38 @@ pub fn Playlists() -> impl IntoView {
|
||||
<Transition
|
||||
fallback=move || view! { <Loading /> }
|
||||
>
|
||||
<ErrorBoundary
|
||||
fallback=|errors| {
|
||||
errors.get().into_iter().map(|(_id, error)| {
|
||||
view! {
|
||||
<Error<String>
|
||||
message=error.to_string()
|
||||
/>
|
||||
}
|
||||
}).collect::<Vec<_>>()
|
||||
|
||||
<A href={"/liked".to_string()} {..}
|
||||
style={move || if liked_songs_active() {"background-color: var(--color-neutral-700);"} else {""}}
|
||||
class="flex items-center hover:bg-neutral-700 rounded-md my-1"
|
||||
>
|
||||
<img class="w-15 h-15 rounded-xl p-2"
|
||||
src="/assets/images/placeholders/MusicPlaceholder.svg" />
|
||||
<h2 class="pr-3 my-2">"Liked Songs"</h2>
|
||||
</A>
|
||||
{move || GlobalState::playlists().get().map(|playlists| {
|
||||
match playlists {
|
||||
Ok(playlists) => Either::Left(view! {
|
||||
{playlists.into_iter().map(|playlist| {
|
||||
let active = Signal::derive(move || {
|
||||
location.pathname.get().ends_with(&format!("/playlist/{}", playlist.id))
|
||||
});
|
||||
|
||||
view! {
|
||||
<A href={format!("/playlist/{}", playlist.id)} {..}
|
||||
style={move || if active() {"background-color: var(--color-neutral-700);"} else {""}}
|
||||
class="flex items-center hover:bg-neutral-700 rounded-md my-1" >
|
||||
<img class="w-15 h-15 rounded-xl p-2 object-cover"
|
||||
src={format!("/assets/images/playlist/{}.webp", playlist.id)}
|
||||
onerror={crate::util::img_fallback::MUSIC_IMG_FALLBACK} />
|
||||
<h2 class="pr-3 my-2">{playlist.name}</h2>
|
||||
</A>
|
||||
}
|
||||
}).collect::<Vec<_>>()}
|
||||
}),
|
||||
Err(error) => Either::Right(error.to_component()),
|
||||
}
|
||||
>
|
||||
<A href={"/liked".to_string()} {..}
|
||||
style={move || if liked_songs_active() {"background-color: var(--color-neutral-700);"} else {""}}
|
||||
class="flex items-center hover:bg-neutral-700 rounded-md my-1"
|
||||
>
|
||||
<img class="w-15 h-15 rounded-xl p-2"
|
||||
src="/assets/images/placeholders/MusicPlaceholder.svg" />
|
||||
<h2 class="pr-3 my-2">"Liked Songs"</h2>
|
||||
</A>
|
||||
{move || GlobalState::playlists().get().map(|playlists| {
|
||||
playlists.map(|playlists| {
|
||||
|
||||
view! {
|
||||
{playlists.into_iter().map(|playlist| {
|
||||
let active = Signal::derive(move || {
|
||||
location.pathname.get().ends_with(&format!("/playlist/{}", playlist.id))
|
||||
});
|
||||
|
||||
view! {
|
||||
<A href={format!("/playlist/{}", playlist.id)} {..}
|
||||
style={move || if active() {"background-color: var(--color-neutral-700);"} else {""}}
|
||||
class="flex items-center hover:bg-neutral-700 rounded-md my-1" >
|
||||
<img class="w-15 h-15 rounded-xl p-2 object-cover"
|
||||
src={format!("/assets/images/playlist/{}.webp", playlist.id)}
|
||||
onerror={crate::util::img_fallback::MUSIC_IMG_FALLBACK} />
|
||||
<h2 class="pr-3 my-2">{playlist.name}</h2>
|
||||
</A>
|
||||
}
|
||||
}).collect::<Vec<_>>()}
|
||||
}
|
||||
})
|
||||
})}
|
||||
</ErrorBoundary>
|
||||
})}
|
||||
</Transition>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -8,8 +8,8 @@ cfg_if! {
|
||||
if #[cfg(feature = "ssr")] {
|
||||
use diesel::prelude::*;
|
||||
use crate::util::database::*;
|
||||
use std::error::Error;
|
||||
use crate::models::backend::{Song, HistoryEntry};
|
||||
use crate::util::error::*;
|
||||
}
|
||||
}
|
||||
|
||||
@ -59,7 +59,7 @@ impl User {
|
||||
&self,
|
||||
limit: Option<i64>,
|
||||
conn: &mut PgPooledConn,
|
||||
) -> Result<Vec<HistoryEntry>, Box<dyn Error>> {
|
||||
) -> BackendResult<Vec<HistoryEntry>> {
|
||||
use crate::schema::song_history::dsl::*;
|
||||
|
||||
let my_history = if let Some(limit) = limit {
|
||||
@ -67,7 +67,8 @@ impl User {
|
||||
.filter(user_id.eq(self.id))
|
||||
.order(date.desc())
|
||||
.limit(limit)
|
||||
.load(conn)?
|
||||
.load(conn)
|
||||
.context("Error getting user history")?
|
||||
} else {
|
||||
song_history.filter(user_id.eq(self.id)).load(conn)?
|
||||
};
|
||||
@ -96,7 +97,7 @@ impl User {
|
||||
&self,
|
||||
limit: Option<i64>,
|
||||
conn: &mut PgPooledConn,
|
||||
) -> Result<Vec<(NaiveDateTime, Song)>, Box<dyn Error>> {
|
||||
) -> BackendResult<Vec<(NaiveDateTime, Song)>> {
|
||||
use crate::schema::song_history::dsl::*;
|
||||
use crate::schema::songs::dsl::*;
|
||||
|
||||
@ -107,14 +108,16 @@ impl User {
|
||||
.order(date.desc())
|
||||
.limit(limit)
|
||||
.select((date, songs::all_columns()))
|
||||
.load(conn)?
|
||||
.load(conn)
|
||||
.context("Error getting user history songs")?
|
||||
} else {
|
||||
song_history
|
||||
.inner_join(songs)
|
||||
.filter(user_id.eq(self.id))
|
||||
.order(date.desc())
|
||||
.select((date, songs::all_columns()))
|
||||
.load(conn)?
|
||||
.load(conn)
|
||||
.context("Error getting user history songs")?
|
||||
};
|
||||
|
||||
Ok(my_history)
|
||||
@ -135,7 +138,7 @@ impl User {
|
||||
/// * `Result<(), Box<dyn Error>>` - A result indicating success with an empty value, or an error
|
||||
///
|
||||
#[cfg(feature = "ssr")]
|
||||
pub fn add_history(&self, song_id: i32, conn: &mut PgPooledConn) -> Result<(), Box<dyn Error>> {
|
||||
pub fn add_history(&self, song_id: i32, conn: &mut PgPooledConn) -> BackendResult<()> {
|
||||
use crate::schema::song_history;
|
||||
|
||||
diesel::insert_into(song_history::table)
|
||||
@ -143,7 +146,8 @@ impl User {
|
||||
song_history::user_id.eq(self.id),
|
||||
song_history::song_id.eq(song_id),
|
||||
))
|
||||
.execute(conn)?;
|
||||
.execute(conn)
|
||||
.context("Error adding song to history")?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@ -156,7 +160,7 @@ impl User {
|
||||
song_id: i32,
|
||||
like: bool,
|
||||
conn: &mut PgPooledConn,
|
||||
) -> Result<(), Box<dyn Error>> {
|
||||
) -> BackendResult<()> {
|
||||
use log::*;
|
||||
debug!("Setting like for song {} to {}", song_id, like);
|
||||
|
||||
@ -169,7 +173,8 @@ impl User {
|
||||
song_likes::song_id.eq(song_id),
|
||||
song_likes::user_id.eq(self.id),
|
||||
))
|
||||
.execute(conn)?;
|
||||
.execute(conn)
|
||||
.context("Error liking song")?;
|
||||
|
||||
// Remove dislike if it exists
|
||||
diesel::delete(
|
||||
@ -179,7 +184,8 @@ impl User {
|
||||
.and(song_dislikes::user_id.eq(self.id)),
|
||||
),
|
||||
)
|
||||
.execute(conn)?;
|
||||
.execute(conn)
|
||||
.context("Error removing dislike for song")?;
|
||||
} else {
|
||||
diesel::delete(
|
||||
song_likes::table.filter(
|
||||
@ -188,7 +194,8 @@ impl User {
|
||||
.and(song_likes::user_id.eq(self.id)),
|
||||
),
|
||||
)
|
||||
.execute(conn)?;
|
||||
.execute(conn)
|
||||
.context("Error removing like for song")?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
@ -200,7 +207,7 @@ impl User {
|
||||
&self,
|
||||
song_id: i32,
|
||||
conn: &mut PgPooledConn,
|
||||
) -> Result<bool, Box<dyn Error>> {
|
||||
) -> BackendResult<bool> {
|
||||
use crate::schema::song_likes;
|
||||
|
||||
let like = song_likes::table
|
||||
@ -210,7 +217,8 @@ impl User {
|
||||
.and(song_likes::user_id.eq(self.id)),
|
||||
)
|
||||
.first::<(i32, i32)>(conn)
|
||||
.optional()?
|
||||
.optional()
|
||||
.context("Error checking if song is liked")?
|
||||
.is_some();
|
||||
|
||||
Ok(like)
|
||||
@ -224,7 +232,7 @@ impl User {
|
||||
song_id: i32,
|
||||
dislike: bool,
|
||||
conn: &mut PgPooledConn,
|
||||
) -> Result<(), Box<dyn Error>> {
|
||||
) -> BackendResult<()> {
|
||||
use log::*;
|
||||
debug!("Setting dislike for song {} to {}", song_id, dislike);
|
||||
|
||||
@ -237,7 +245,8 @@ impl User {
|
||||
song_dislikes::song_id.eq(song_id),
|
||||
song_dislikes::user_id.eq(self.id),
|
||||
))
|
||||
.execute(conn)?;
|
||||
.execute(conn)
|
||||
.context("Error disliking song")?;
|
||||
|
||||
// Remove like if it exists
|
||||
diesel::delete(
|
||||
@ -247,7 +256,8 @@ impl User {
|
||||
.and(song_likes::user_id.eq(self.id)),
|
||||
),
|
||||
)
|
||||
.execute(conn)?;
|
||||
.execute(conn)
|
||||
.context("Error removing like for song")?;
|
||||
} else {
|
||||
diesel::delete(
|
||||
song_dislikes::table.filter(
|
||||
@ -256,7 +266,8 @@ impl User {
|
||||
.and(song_dislikes::user_id.eq(self.id)),
|
||||
),
|
||||
)
|
||||
.execute(conn)?;
|
||||
.execute(conn)
|
||||
.context("Error removing dislike for song")?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
@ -268,7 +279,7 @@ impl User {
|
||||
&self,
|
||||
song_id: i32,
|
||||
conn: &mut PgPooledConn,
|
||||
) -> Result<bool, Box<dyn Error>> {
|
||||
) -> BackendResult<bool> {
|
||||
use crate::schema::song_dislikes;
|
||||
|
||||
let dislike = song_dislikes::table
|
||||
@ -278,7 +289,8 @@ impl User {
|
||||
.and(song_dislikes::user_id.eq(self.id)),
|
||||
)
|
||||
.first::<(i32, i32)>(conn)
|
||||
.optional()?
|
||||
.optional()
|
||||
.context("Error checking if song is disliked")?
|
||||
.is_some();
|
||||
|
||||
Ok(dislike)
|
||||
|
@ -6,7 +6,6 @@ use crate::models::frontend;
|
||||
use leptos::either::*;
|
||||
use leptos::prelude::*;
|
||||
use leptos_router::hooks::use_params_map;
|
||||
use server_fn::error::NoCustomError;
|
||||
|
||||
#[component]
|
||||
pub fn AlbumPage() -> impl IntoView {
|
||||
@ -61,12 +60,7 @@ fn AlbumIdPage(#[prop(into)] id: Signal<i32>) -> impl IntoView {
|
||||
message=format!("Album with ID {} not found", id.get())
|
||||
/>
|
||||
}),
|
||||
Err(error) => EitherOf3::C(view! {
|
||||
<ServerError<NoCustomError>
|
||||
title="Error Getting Album"
|
||||
error
|
||||
/>
|
||||
}),
|
||||
Err(error) => EitherOf3::C(error.to_component()),
|
||||
}
|
||||
})}
|
||||
</Transition>
|
||||
@ -98,21 +92,14 @@ fn AlbumSongs(#[prop(into)] id: Signal<i32>) -> impl IntoView {
|
||||
<Transition
|
||||
fallback= move || view! { <Loading /> }
|
||||
>
|
||||
<ErrorBoundary
|
||||
fallback=|errors| view! {
|
||||
{move || errors.get()
|
||||
.into_iter()
|
||||
.map(|(_, e)| view! { <p>{e.to_string()}</p> })
|
||||
.collect_view()
|
||||
}
|
||||
}
|
||||
>
|
||||
{move || songs.get().map(|songs| {
|
||||
songs.map(|songs| {
|
||||
{move || songs.get().map(|songs| {
|
||||
match songs {
|
||||
Ok(songs) => Either::Left(
|
||||
view! { <SongList songs=songs /> }
|
||||
})
|
||||
})}
|
||||
</ErrorBoundary>
|
||||
),
|
||||
Err(error) => Either::Right(error.to_component()),
|
||||
}
|
||||
})}
|
||||
</Transition>
|
||||
}
|
||||
}
|
||||
|
@ -2,7 +2,6 @@ use leptos::either::*;
|
||||
use leptos::prelude::*;
|
||||
use leptos_icons::*;
|
||||
use leptos_router::hooks::use_params_map;
|
||||
use server_fn::error::NoCustomError;
|
||||
|
||||
use crate::models::backend::Artist;
|
||||
|
||||
@ -65,12 +64,7 @@ fn ArtistIdProfile(#[prop(into)] id: Signal<i32>) -> impl IntoView {
|
||||
message=format!("Artist with ID {} not found", id.get())
|
||||
/>
|
||||
}),
|
||||
Err(error) => EitherOf3::C(view! {
|
||||
<ServerError<NoCustomError>
|
||||
title="Error Getting Artist"
|
||||
error
|
||||
/>
|
||||
}),
|
||||
Err(error) => EitherOf3::C(error.to_component())
|
||||
}
|
||||
})}
|
||||
</Transition>
|
||||
@ -126,21 +120,16 @@ fn TopSongsByArtist(#[prop(into)] artist_id: Signal<i32>) -> impl IntoView {
|
||||
<Transition
|
||||
fallback=move || view! { <Loading /> }
|
||||
>
|
||||
<ErrorBoundary
|
||||
fallback=|errors| view! {
|
||||
{move || errors.get()
|
||||
.into_iter()
|
||||
.map(|(_, e)| view! { <p>{e.to_string()}</p>})
|
||||
.collect_view()
|
||||
}
|
||||
{move || top_songs.get().map(|top_songs| {
|
||||
match top_songs {
|
||||
Ok(top_songs) => {
|
||||
Either::Left(view! {
|
||||
<SongListExtra songs=top_songs />
|
||||
})
|
||||
},
|
||||
Err(error) => Either::Right(error.to_component()),
|
||||
}
|
||||
>
|
||||
{move || top_songs.get().map(|top_songs| {
|
||||
top_songs.map(|top_songs| {
|
||||
view! { <SongListExtra songs=top_songs /> }
|
||||
})
|
||||
})}
|
||||
</ErrorBoundary>
|
||||
})}
|
||||
</Transition>
|
||||
}
|
||||
}
|
||||
@ -162,27 +151,20 @@ fn AlbumsByArtist(#[prop(into)] artist_id: Signal<i32>) -> impl IntoView {
|
||||
<Transition
|
||||
fallback=move || view! { <Loading /> }
|
||||
>
|
||||
<ErrorBoundary
|
||||
fallback=|errors| view! {
|
||||
{move || errors.get()
|
||||
.into_iter()
|
||||
.map(|(_, e)| view! { <p>{e.to_string()}</p>})
|
||||
.collect_view()
|
||||
}
|
||||
}
|
||||
>
|
||||
{move || albums.get().map(|albums| {
|
||||
albums.map(|albums| {
|
||||
{move || albums.get().map(|albums| {
|
||||
match albums {
|
||||
Ok(albums) => Either::Left({
|
||||
let tiles = albums.into_iter().map(|album| {
|
||||
album.into()
|
||||
album.into()
|
||||
}).collect::<Vec<_>>();
|
||||
|
||||
view! {
|
||||
<DashboardRow title="Albums" tiles />
|
||||
}
|
||||
})
|
||||
})}
|
||||
</ErrorBoundary>
|
||||
}),
|
||||
Err(error) => Either::Right(error.to_component()),
|
||||
}
|
||||
})}
|
||||
</Transition>
|
||||
}
|
||||
}
|
||||
|
@ -2,7 +2,6 @@ use leptos::either::*;
|
||||
use leptos::prelude::*;
|
||||
use leptos_icons::*;
|
||||
use leptos_router::hooks::use_params_map;
|
||||
use server_fn::error::NoCustomError;
|
||||
|
||||
use crate::components::dashboard_row::DashboardRow;
|
||||
use crate::components::error::*;
|
||||
@ -118,13 +117,7 @@ fn UserIdProfile(#[prop(into)] id: Signal<i32>) -> impl IntoView {
|
||||
},
|
||||
Err(error) => {
|
||||
show_details.set(false);
|
||||
|
||||
EitherOf3::C(view! {
|
||||
<ServerError<NoCustomError>
|
||||
title="Error Getting User"
|
||||
error
|
||||
/>
|
||||
})
|
||||
EitherOf3::C(error.to_component())
|
||||
}
|
||||
}
|
||||
})}
|
||||
@ -203,25 +196,18 @@ fn TopSongs(#[prop(into)] user_id: Signal<i32>) -> impl IntoView {
|
||||
<Transition
|
||||
fallback=move || view! { <Loading /> }
|
||||
>
|
||||
<ErrorBoundary
|
||||
fallback=|errors| view! {
|
||||
{move || errors.get()
|
||||
.into_iter()
|
||||
.map(|(_, e)| view! { <p>{e.to_string()}</p>})
|
||||
.collect_view()
|
||||
}
|
||||
}
|
||||
>
|
||||
{move ||
|
||||
top_songs.get().map(|top_songs| {
|
||||
top_songs.map(|top_songs| {
|
||||
{move ||
|
||||
top_songs.get().map(|top_songs| {
|
||||
match top_songs {
|
||||
Ok(top_songs) => Either::Left({
|
||||
view! {
|
||||
<SongListExtra songs=top_songs />
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
</ErrorBoundary>
|
||||
}),
|
||||
Err(err) => Either::Right(err.to_component()),
|
||||
}
|
||||
})
|
||||
}
|
||||
</Transition>
|
||||
}
|
||||
}
|
||||
@ -248,25 +234,16 @@ fn RecentSongs(#[prop(into)] user_id: Signal<i32>) -> impl IntoView {
|
||||
<Transition
|
||||
fallback=move || view! { <Loading /> }
|
||||
>
|
||||
<ErrorBoundary
|
||||
fallback=|errors| view! {
|
||||
{move || errors.get()
|
||||
.into_iter()
|
||||
.map(|(_, e)| view! { <p>{e.to_string()}</p>})
|
||||
.collect_view()
|
||||
{move ||
|
||||
recent_songs.get().map(|recent_songs| {
|
||||
match recent_songs {
|
||||
Ok(recent_songs) => Either::Left(view! {
|
||||
<SongList songs=recent_songs />
|
||||
}),
|
||||
Err(err) => Either::Right(err.to_component()),
|
||||
}
|
||||
}
|
||||
>
|
||||
{move ||
|
||||
recent_songs.get().map(|recent_songs| {
|
||||
recent_songs.map(|recent_songs| {
|
||||
view! {
|
||||
<SongList songs=recent_songs />
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
</ErrorBoundary>
|
||||
})
|
||||
}
|
||||
</Transition>
|
||||
}
|
||||
}
|
||||
@ -305,19 +282,10 @@ fn TopArtists(#[prop(into)] user_id: Signal<i32>) -> impl IntoView {
|
||||
<Loading />
|
||||
}
|
||||
>
|
||||
<ErrorBoundary
|
||||
fallback=|errors| view! {
|
||||
<h2 class="text-xl font-bold">{format!("Top Artists {HISTORY_MESSAGE}")}</h2>
|
||||
{move || errors.get()
|
||||
.into_iter()
|
||||
.map(|(_, e)| view! { <p>{e.to_string()}</p>})
|
||||
.collect_view()
|
||||
}
|
||||
}
|
||||
>
|
||||
{move ||
|
||||
top_artists.get().map(|top_artists| {
|
||||
top_artists.map(|top_artists| {
|
||||
{move ||
|
||||
top_artists.get().map(|top_artists| {
|
||||
match top_artists {
|
||||
Ok(top_artists) => Either::Left({
|
||||
let tiles = top_artists.into_iter().map(|artist| {
|
||||
artist.into()
|
||||
}).collect::<Vec<_>>();
|
||||
@ -325,10 +293,16 @@ fn TopArtists(#[prop(into)] user_id: Signal<i32>) -> impl IntoView {
|
||||
view! {
|
||||
<DashboardRow title=format!("Top Artists {}", HISTORY_MESSAGE) tiles />
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
</ErrorBoundary>
|
||||
}),
|
||||
Err(err) => Either::Right(
|
||||
view! {
|
||||
<h2 class="text-xl font-bold">{format!("Top Artists {HISTORY_MESSAGE}")}</h2>
|
||||
{err.to_component()}
|
||||
}
|
||||
)
|
||||
}
|
||||
})
|
||||
}
|
||||
</Transition>
|
||||
}
|
||||
}
|
||||
|
@ -1,8 +1,8 @@
|
||||
use crate::api::search::search;
|
||||
use crate::components::dashboard_row::*;
|
||||
use crate::components::error::*;
|
||||
use crate::components::loading::*;
|
||||
use crate::components::song_list::*;
|
||||
use leptos::either::*;
|
||||
use leptos::html::Input;
|
||||
use leptos::prelude::*;
|
||||
use leptos_router::hooks::query_signal;
|
||||
@ -35,20 +35,10 @@ pub fn Search() -> impl IntoView {
|
||||
<Suspense
|
||||
fallback=|| view! { <Loading /> }
|
||||
>
|
||||
<ErrorBoundary
|
||||
fallback=|errors| {
|
||||
errors.get().into_iter().map(|(_id, error)| {
|
||||
view! {
|
||||
<Error<String>
|
||||
message=error.to_string()
|
||||
/>
|
||||
}
|
||||
}).collect::<Vec<_>>()
|
||||
}
|
||||
>
|
||||
{move || {
|
||||
search.get().map(|results| {
|
||||
results.map(|(albums, artists, songs)| {
|
||||
{move || {
|
||||
search.get().map(|results| {
|
||||
match results {
|
||||
Ok((albums, artists, songs)) => Either::Right(
|
||||
view! {
|
||||
{
|
||||
(albums.is_empty() && artists.is_empty() && songs.is_empty()).then(|| {
|
||||
@ -86,10 +76,11 @@ pub fn Search() -> impl IntoView {
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
}}
|
||||
</ErrorBoundary>
|
||||
),
|
||||
Err(err) => Either::Left(err.to_component()),
|
||||
}
|
||||
})
|
||||
}}
|
||||
</Suspense>
|
||||
}
|
||||
}
|
||||
|
@ -2,7 +2,6 @@ use leptos::either::*;
|
||||
use leptos::prelude::*;
|
||||
use leptos_icons::*;
|
||||
use leptos_router::hooks::use_params_map;
|
||||
use server_fn::error::NoCustomError;
|
||||
|
||||
use crate::api::songs;
|
||||
use crate::api::songs::*;
|
||||
@ -67,14 +66,7 @@ fn SongDetails(#[prop(into)] id: Signal<i32>) -> impl IntoView {
|
||||
/>
|
||||
})
|
||||
},
|
||||
Err(error) => {
|
||||
EitherOf3::C(view! {
|
||||
<ServerError<NoCustomError>
|
||||
title="Error Fetching Song"
|
||||
error
|
||||
/>
|
||||
})
|
||||
}
|
||||
Err(error) => EitherOf3::C(error.to_component()),
|
||||
}
|
||||
})}
|
||||
</Transition>
|
||||
@ -153,14 +145,7 @@ fn SongPlays(#[prop(into)] id: Signal<i32>) -> impl IntoView {
|
||||
<p>{format!("Plays: {plays}")}</p>
|
||||
})
|
||||
},
|
||||
Err(error) => {
|
||||
Either::Right(view! {
|
||||
<ServerError<NoCustomError>
|
||||
title="Error fetching song plays"
|
||||
error
|
||||
/>
|
||||
})
|
||||
}
|
||||
Err(error) => Either::Right(error.to_component())
|
||||
}
|
||||
})}
|
||||
</Transition>
|
||||
@ -183,12 +168,7 @@ fn MySongPlays(#[prop(into)] id: Signal<i32>) -> impl IntoView {
|
||||
})
|
||||
},
|
||||
Err(error) => {
|
||||
Either::Right(view! {
|
||||
<ServerError<NoCustomError>
|
||||
title="Error fetching my song plays"
|
||||
error
|
||||
/>
|
||||
})
|
||||
Either::Right(error.to_component())
|
||||
}
|
||||
}
|
||||
})}
|
||||
|
@ -1,3 +1,4 @@
|
||||
use crate::util::error::*;
|
||||
use std::fs::File;
|
||||
use symphonia::core::codecs::CodecType;
|
||||
use symphonia::core::formats::FormatOptions;
|
||||
@ -7,38 +8,44 @@ use symphonia::core::probe::Hint;
|
||||
|
||||
/// Extract the codec and duration of an audio file
|
||||
/// This is combined into one function because the file object will be consumed
|
||||
pub fn extract_metadata(file: File) -> Result<(CodecType, u64), Box<dyn std::error::Error>> {
|
||||
pub fn extract_metadata(file: File) -> BackendResult<(CodecType, u64)> {
|
||||
let source_stream = MediaSourceStream::new(Box::new(file), Default::default());
|
||||
|
||||
let hint = Hint::new();
|
||||
let format_opts = FormatOptions::default();
|
||||
let metadata_opts = MetadataOptions::default();
|
||||
|
||||
let probe = symphonia::default::get_probe().format(
|
||||
&hint,
|
||||
source_stream,
|
||||
&format_opts,
|
||||
&metadata_opts,
|
||||
)?;
|
||||
let probe = symphonia::default::get_probe()
|
||||
.format(&hint, source_stream, &format_opts, &metadata_opts)
|
||||
.map_err(|e| InputError::InvalidInput(format!("{e}")))?;
|
||||
let reader = probe.format;
|
||||
|
||||
if reader.tracks().len() != 1 {
|
||||
return Err(format!("Expected 1 track, found {}", reader.tracks().len()).into());
|
||||
return Err(InputError::InvalidInput(format!(
|
||||
"Expected 1 track, found {}",
|
||||
reader.tracks().len()
|
||||
))
|
||||
.into());
|
||||
}
|
||||
|
||||
let track = &reader.tracks()[0];
|
||||
|
||||
let time_base = track.codec_params.time_base.ok_or("Missing time base")?;
|
||||
let time_base = track
|
||||
.codec_params
|
||||
.time_base
|
||||
.ok_or(InputError::InvalidInput("Missing time base".into()))?;
|
||||
let duration = track
|
||||
.codec_params
|
||||
.n_frames
|
||||
.map(|frames| track.codec_params.start_ts + frames)
|
||||
.ok_or("Missing number of frames")?;
|
||||
.ok_or(InputError::InvalidInput("Missing frame count".to_string()))?;
|
||||
|
||||
let duration = duration
|
||||
.checked_mul(time_base.numer as u64)
|
||||
.and_then(|v| v.checked_div(time_base.denom as u64))
|
||||
.ok_or("Overflow while computing duration")?;
|
||||
.ok_or(BackendError::InternalError(
|
||||
"Duration calculation overflowed",
|
||||
))?;
|
||||
|
||||
Ok((track.codec_params.codec, duration))
|
||||
}
|
||||
|
@ -1,14 +1,10 @@
|
||||
use crate::util::error::*;
|
||||
use multer::Field;
|
||||
use server_fn::{error::NoCustomError, ServerFnError};
|
||||
|
||||
/// Extract the text from a multipart field
|
||||
pub async fn extract_field(field: Field<'static>) -> Result<String, ServerFnError> {
|
||||
let field = match field.text().await {
|
||||
Ok(field) => field,
|
||||
Err(e) => Err(ServerFnError::<NoCustomError>::ServerError(format!(
|
||||
"Error reading field: {e}"
|
||||
)))?,
|
||||
};
|
||||
|
||||
Ok(field)
|
||||
pub async fn extract_field(field: Field<'static>) -> BackendResult<String> {
|
||||
field
|
||||
.text()
|
||||
.await
|
||||
.map_err(|e| InputError::FieldReadError(format!("{e}")).into())
|
||||
}
|
||||
|
@ -6,6 +6,7 @@ use crate::api::playlists::get_playlists;
|
||||
use crate::models::backend;
|
||||
use crate::models::backend::User;
|
||||
use crate::models::frontend::PlayStatus;
|
||||
use crate::util::error::*;
|
||||
|
||||
/// Global front-end state
|
||||
/// Contains anything frequently needed across multiple components
|
||||
@ -22,7 +23,7 @@ pub struct GlobalState {
|
||||
pub play_status: RwSignal<PlayStatus>,
|
||||
|
||||
/// A resource that fetches the playlists
|
||||
pub playlists: Resource<Result<Vec<backend::Playlist>, ServerFnError>>,
|
||||
pub playlists: Resource<BackendResult<Vec<backend::Playlist>>>,
|
||||
}
|
||||
|
||||
impl GlobalState {
|
||||
@ -60,7 +61,7 @@ impl GlobalState {
|
||||
expect_context::<Self>().play_status
|
||||
}
|
||||
|
||||
pub fn playlists() -> Resource<Result<Vec<backend::Playlist>, ServerFnError>> {
|
||||
pub fn playlists() -> Resource<BackendResult<Vec<backend::Playlist>>> {
|
||||
expect_context::<Self>().playlists
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user