no local cache
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
//! API REST handlers spécifiques au cache audio
|
||||
|
||||
use crate::cache;
|
||||
use crate::metadata_ext::AudioTrackMetadataExt;
|
||||
use crate::Cache;
|
||||
use axum::{
|
||||
@@ -8,7 +9,7 @@ use axum::{
|
||||
response::IntoResponse,
|
||||
Json,
|
||||
};
|
||||
use pmometadata::TrackMetadata;
|
||||
use pmocache::api::{AddItemRequest, AddItemResponse, ErrorResponse};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::sync::Arc;
|
||||
|
||||
@@ -96,3 +97,79 @@ pub async fn get_cover_url(
|
||||
)
|
||||
.into_response()
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
enum AddSource<'a> {
|
||||
Url(&'a str),
|
||||
Local(&'a str),
|
||||
}
|
||||
|
||||
/// Handler spécialisé pour l'ajout d'éléments dans le cache audio.
|
||||
pub async fn add_audio_item(
|
||||
State(cache): State<Arc<Cache>>,
|
||||
Json(req): Json<AddItemRequest>,
|
||||
) -> impl IntoResponse {
|
||||
let mode = match (req.url.as_deref(), req.path.as_deref()) {
|
||||
(Some(url), None) if !url.is_empty() => AddSource::Url(url),
|
||||
(None, Some(path)) if !path.is_empty() => AddSource::Local(path),
|
||||
(Some(_), Some(_)) => {
|
||||
return (
|
||||
StatusCode::BAD_REQUEST,
|
||||
Json(ErrorResponse {
|
||||
error: "INVALID_REQUEST".to_string(),
|
||||
message: "Provide either 'url' or 'path', not both".to_string(),
|
||||
}),
|
||||
)
|
||||
.into_response()
|
||||
}
|
||||
_ => {
|
||||
return (
|
||||
StatusCode::BAD_REQUEST,
|
||||
Json(ErrorResponse {
|
||||
error: "INVALID_REQUEST".to_string(),
|
||||
message: "Either 'url' or 'path' must be provided".to_string(),
|
||||
}),
|
||||
)
|
||||
.into_response()
|
||||
}
|
||||
};
|
||||
|
||||
let collection = req.collection.as_deref();
|
||||
let add_result = match mode {
|
||||
AddSource::Url(url) => cache.add_from_url(url, collection).await,
|
||||
AddSource::Local(path) => cache::add_local_file(&cache, path, collection).await,
|
||||
};
|
||||
|
||||
match add_result {
|
||||
Ok(pk) => {
|
||||
let origin =
|
||||
cache
|
||||
.db
|
||||
.get_origin_url(&pk)
|
||||
.ok()
|
||||
.flatten()
|
||||
.unwrap_or_else(|| match mode {
|
||||
AddSource::Url(url) => url.to_string(),
|
||||
AddSource::Local(path) => format!("file://{}", path),
|
||||
});
|
||||
|
||||
(
|
||||
StatusCode::CREATED,
|
||||
Json(AddItemResponse {
|
||||
pk,
|
||||
url: origin,
|
||||
message: "Item added successfully".to_string(),
|
||||
}),
|
||||
)
|
||||
.into_response()
|
||||
}
|
||||
Err(e) => (
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
Json(ErrorResponse {
|
||||
error: "PROCESSING_ERROR".to_string(),
|
||||
message: format!("Cannot add item: {}", e),
|
||||
}),
|
||||
)
|
||||
.into_response(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,10 +5,13 @@
|
||||
//! des métadonnées en JSON dans la base de données.
|
||||
|
||||
use crate::metadata_ext::AudioTrackMetadataExt;
|
||||
use anyhow::Result;
|
||||
use pmocache::download::TransformMetadata;
|
||||
use pmocache::CacheConfig;
|
||||
use anyhow::{anyhow, Result};
|
||||
use pmocache::download::{read_exact_or_eof, TransformMetadata};
|
||||
use pmocache::{pk_from_content_header, CacheConfig};
|
||||
use pmoflac::is_flac_magic_header;
|
||||
use serde_json::json;
|
||||
use std::sync::Arc;
|
||||
use tokio::io::AsyncSeekExt;
|
||||
|
||||
/// Configuration pour le cache audio
|
||||
pub struct AudioConfig;
|
||||
@@ -122,6 +125,76 @@ pub async fn new_cache_with_consolidation(dir: &str, limit: usize) -> Result<Arc
|
||||
Ok(cache)
|
||||
}
|
||||
|
||||
/// Ajoute un fichier audio local. Les FLAC sont référencés sans copie, les autres formats
|
||||
/// sont convertis via le pipeline classique.
|
||||
pub async fn add_local_file(cache: &Cache, path: &str, collection: Option<&str>) -> Result<String> {
|
||||
let canonical_path = std::fs::canonicalize(path)?;
|
||||
let file_url = format!("file://{}", canonical_path.display());
|
||||
let length = tokio::fs::metadata(&canonical_path)
|
||||
.await
|
||||
.ok()
|
||||
.map(|m| m.len());
|
||||
let mut reader = tokio::fs::File::open(&canonical_path).await?;
|
||||
|
||||
let header = read_exact_or_eof(&mut reader, 1024)
|
||||
.await
|
||||
.map_err(|e| anyhow!("Failed to read header bytes: {}", e))?;
|
||||
|
||||
let pk_bytes = if header.len() >= 1024 {
|
||||
&header[512..]
|
||||
} else {
|
||||
&header[..]
|
||||
};
|
||||
let pk = pk_from_content_header(pk_bytes);
|
||||
|
||||
if cache.db.get(&pk, false).is_ok() {
|
||||
cache.db.update_hit(&pk)?;
|
||||
return Ok(pk);
|
||||
}
|
||||
|
||||
if let Some(download) = cache.get_download(&pk).await {
|
||||
if download.finished().await {
|
||||
cache.db.update_hit(&pk)?;
|
||||
}
|
||||
return Ok(pk);
|
||||
}
|
||||
|
||||
let is_flac = is_flac_magic_header(&header);
|
||||
if !is_flac {
|
||||
reader
|
||||
.rewind()
|
||||
.await
|
||||
.map_err(|e| anyhow!("Failed to rewind local file: {}", e))?;
|
||||
|
||||
return cache
|
||||
.add_from_reader_with_pk(Some(&file_url), reader, length, collection, Some(pk))
|
||||
.await;
|
||||
}
|
||||
|
||||
let mut metadata = vec![
|
||||
("local_passthrough".to_string(), json!(true)),
|
||||
(
|
||||
"local_source_path".to_string(),
|
||||
json!(canonical_path.to_string_lossy().to_string()),
|
||||
),
|
||||
];
|
||||
if let Some(len) = length {
|
||||
metadata.push(("source_size".to_string(), json!(len)));
|
||||
}
|
||||
|
||||
cache
|
||||
.register_local_file_reference(
|
||||
&pk,
|
||||
&canonical_path,
|
||||
collection,
|
||||
Some(&file_url),
|
||||
Some(&metadata),
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(pk)
|
||||
}
|
||||
|
||||
/// Ajoute une piste audio depuis une URL avec extraction et stockage des métadonnées
|
||||
///
|
||||
/// Cette fonction étend `add_from_url` du cache en ajoutant :
|
||||
|
||||
@@ -11,6 +11,11 @@
|
||||
//! - stockage des métadonnées dans la table `metadata` de `pmocache::DB` ;
|
||||
//! - helpers pour renseigner les collections à partir des tags ;
|
||||
//! - intégration optionnelle avec `pmoserver` (routes REST + diffusion de fichiers).
|
||||
//! - référence de fichiers FLAC locaux : [`cache::add_local_file`] détecte les fichiers déjà au
|
||||
//! bon format et enregistre une entrée du cache sans recopier les octets tout en laissant les
|
||||
//! autres formats passer par la conversion standard.
|
||||
//! - support complet des lazy PK hérités de [`pmocache`], permettant de publier des playlists
|
||||
//! avec des entrées différées et de déclencher le téléchargement lors de la première lecture.
|
||||
//!
|
||||
//! ## Exemple rapide
|
||||
//!
|
||||
@@ -198,7 +203,7 @@ pub trait AudioCacheExt {
|
||||
}
|
||||
|
||||
#[cfg(feature = "pmoserver")]
|
||||
use pmocache::pmoserver_ext::{create_api_router, create_file_router};
|
||||
use pmocache::pmoserver_ext::create_file_router;
|
||||
#[cfg(feature = "pmoserver")]
|
||||
use utoipa::OpenApi;
|
||||
|
||||
@@ -219,9 +224,28 @@ impl AudioCacheExt for pmoserver::Server {
|
||||
);
|
||||
self.add_router("/", file_router).await;
|
||||
|
||||
// API REST générique (pmocache)
|
||||
// Routes: GET/POST/DELETE /api/audio, etc.
|
||||
let mut api_router = create_api_router(cache.clone());
|
||||
// API REST (handlers génériques + POST spécialisé audio)
|
||||
let mut api_router = axum::Router::new()
|
||||
.route(
|
||||
"/",
|
||||
axum::routing::get(pmocache::api::list_items::<AudioConfig>)
|
||||
.post(crate::api::add_audio_item)
|
||||
.delete(pmocache::api::purge_cache::<AudioConfig>),
|
||||
)
|
||||
.route(
|
||||
"/{pk}",
|
||||
axum::routing::get(pmocache::api::get_item_info::<AudioConfig>)
|
||||
.delete(pmocache::api::delete_item::<AudioConfig>),
|
||||
)
|
||||
.route(
|
||||
"/{pk}/status",
|
||||
axum::routing::get(pmocache::api::get_download_status::<AudioConfig>),
|
||||
)
|
||||
.route(
|
||||
"/consolidate",
|
||||
axum::routing::post(pmocache::api::consolidate_cache::<AudioConfig>),
|
||||
)
|
||||
.with_state(cache.clone());
|
||||
|
||||
// Ajouter les endpoints audio spécifiques
|
||||
// Route: GET /api/audio/{pk}/cover-url
|
||||
|
||||
@@ -94,3 +94,47 @@ async fn test_cache_limit() {
|
||||
let count = cache.db.count().unwrap();
|
||||
assert_eq!(count, 2);
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
#[tokio::test]
|
||||
async fn test_local_flac_passthrough_symlink() {
|
||||
let (_temp_dir, cache) = create_test_cache();
|
||||
|
||||
let flac_file = tempfile::NamedTempFile::with_suffix(".flac").unwrap();
|
||||
let mut data = vec![0u8; 2048];
|
||||
data[..4].copy_from_slice(b"fLaC");
|
||||
for (idx, byte) in data.iter_mut().enumerate().skip(4) {
|
||||
*byte = (idx % 251) as u8;
|
||||
}
|
||||
std::fs::write(flac_file.path(), &data).unwrap();
|
||||
|
||||
let pk = cache::add_local_file(
|
||||
&cache,
|
||||
flac_file.path().to_str().unwrap(),
|
||||
Some("album:test"),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let cached_path = cache.get(&pk).await.unwrap();
|
||||
let metadata = std::fs::symlink_metadata(&cached_path).unwrap();
|
||||
assert!(metadata.file_type().is_symlink());
|
||||
|
||||
let canonical_source = std::fs::canonicalize(flac_file.path()).unwrap();
|
||||
let link_target = std::fs::read_link(&cached_path).unwrap();
|
||||
assert_eq!(link_target, canonical_source);
|
||||
|
||||
let stored_metadata = cache.db.get_metadata(&pk).unwrap().unwrap();
|
||||
assert_eq!(
|
||||
stored_metadata
|
||||
.get("local_passthrough")
|
||||
.and_then(|v| v.as_bool()),
|
||||
Some(true)
|
||||
);
|
||||
assert_eq!(
|
||||
stored_metadata
|
||||
.get("local_source_path")
|
||||
.and_then(|v| v.as_str()),
|
||||
Some(canonical_source.to_str().unwrap())
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user