Improved admin UI
This commit is contained in:
@@ -528,6 +528,20 @@ pub async fn update_album_full(
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct SetGenreBody { pub genre: String }
|
||||
|
||||
pub async fn set_album_tracks_genre(
|
||||
State(state): State<S>,
|
||||
Path(id): Path<i64>,
|
||||
Json(body): Json<SetGenreBody>,
|
||||
) -> impl IntoResponse {
|
||||
match db::set_album_tracks_genre(&state.pool, id, &body.genre).await {
|
||||
Ok(()) => StatusCode::NO_CONTENT.into_response(),
|
||||
Err(e) => error_response(StatusCode::INTERNAL_SERVER_ERROR, &e.to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct ReorderBody {
|
||||
pub orders: Vec<(i64, i32)>,
|
||||
@@ -544,19 +558,82 @@ pub async fn reorder_album_tracks(
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn album_cover(State(state): State<S>, Path(id): Path<i64>) -> impl IntoResponse {
|
||||
let cover = match db::get_album_cover(&state.pool, id).await {
|
||||
Ok(Some(c)) => c,
|
||||
Ok(None) => return StatusCode::NOT_FOUND.into_response(),
|
||||
Err(e) => return error_response(StatusCode::INTERNAL_SERVER_ERROR, &e.to_string()),
|
||||
/// Cover by artist+album name — used for queue items that may not have an album_id yet.
|
||||
#[derive(Deserialize)]
|
||||
pub struct CoverByNameQuery {
|
||||
#[serde(default)] pub artist: String,
|
||||
#[serde(default)] pub name: String,
|
||||
}
|
||||
pub async fn album_cover_by_name(State(state): State<S>, Query(q): Query<CoverByNameQuery>) -> impl IntoResponse {
|
||||
let album_id = match db::find_album_id(&state.pool, &q.artist, &q.name).await {
|
||||
Ok(Some(id)) => id,
|
||||
_ => return StatusCode::NOT_FOUND.into_response(),
|
||||
};
|
||||
match tokio::fs::read(&cover.0).await {
|
||||
Ok(bytes) => (
|
||||
[(axum::http::header::CONTENT_TYPE, cover.1)],
|
||||
bytes,
|
||||
).into_response(),
|
||||
Err(_) => StatusCode::NOT_FOUND.into_response(),
|
||||
album_cover_by_id(&state, album_id).await
|
||||
}
|
||||
|
||||
pub async fn album_cover(State(state): State<S>, Path(id): Path<i64>) -> impl IntoResponse {
|
||||
album_cover_by_id(&state, id).await
|
||||
}
|
||||
|
||||
async fn album_cover_by_id(state: &super::AppState, id: i64) -> axum::response::Response {
|
||||
// 1. Try album_images table
|
||||
if let Ok(Some((file_path, mime_type))) = db::get_album_cover(&state.pool, id).await {
|
||||
if let Ok(bytes) = tokio::fs::read(&file_path).await {
|
||||
return ([(axum::http::header::CONTENT_TYPE, mime_type)], bytes).into_response();
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Fallback: extract embedded cover from first track in album
|
||||
if let Ok(Some(track_path)) = db::get_album_first_track_path(&state.pool, id).await {
|
||||
let path = std::path::PathBuf::from(track_path);
|
||||
if path.exists() {
|
||||
let result = tokio::task::spawn_blocking(move || extract_embedded_cover(&path)).await;
|
||||
if let Ok(Some((bytes, mime))) = result {
|
||||
return ([(axum::http::header::CONTENT_TYPE, mime)], bytes).into_response();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
StatusCode::NOT_FOUND.into_response()
|
||||
}
|
||||
|
||||
fn extract_embedded_cover(path: &std::path::Path) -> Option<(Vec<u8>, String)> {
|
||||
use symphonia::core::{
|
||||
formats::FormatOptions,
|
||||
io::MediaSourceStream,
|
||||
meta::MetadataOptions,
|
||||
probe::Hint,
|
||||
};
|
||||
|
||||
let file = std::fs::File::open(path).ok()?;
|
||||
let mss = MediaSourceStream::new(Box::new(file), Default::default());
|
||||
|
||||
let mut hint = Hint::new();
|
||||
if let Some(ext) = path.extension().and_then(|e| e.to_str()) {
|
||||
hint.with_extension(ext);
|
||||
}
|
||||
|
||||
let mut probed = symphonia::default::get_probe()
|
||||
.format(
|
||||
&hint,
|
||||
mss,
|
||||
&FormatOptions { enable_gapless: false, ..Default::default() },
|
||||
&MetadataOptions::default(),
|
||||
)
|
||||
.ok()?;
|
||||
|
||||
if let Some(rev) = probed.metadata.get().as_ref().and_then(|m| m.current()) {
|
||||
if let Some(v) = rev.visuals().first() {
|
||||
return Some((v.data.to_vec(), v.media_type.clone()));
|
||||
}
|
||||
}
|
||||
if let Some(rev) = probed.format.metadata().current() {
|
||||
if let Some(v) = rev.visuals().first() {
|
||||
return Some((v.data.to_vec(), v.media_type.clone()));
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
|
||||
Reference in New Issue
Block a user