update la webapp

This commit is contained in:
2025-11-25 08:24:08 +01:00
parent 3957e17f64
commit aeddcc9c64
17 changed files with 1550 additions and 78 deletions

View File

@@ -11,6 +11,7 @@
//! - **Error handling**: Distinguishes between transient errors (NotImplemented, ReadOnly)
//! and backend errors that should be propagated
//! - **Metadata copying**: Helper function to copy metadata between implementations
//! - **Fallback cover**: Provides a default cover image when none is available
//!
//! # Examples
//!
@@ -40,6 +41,60 @@ use std::{
};
use tokio::sync::RwLock;
/// Image SVG par défaut pour les covers manquantes.
///
/// Cette constante contient un SVG élégant représentant une note de musique,
/// utilisé comme fallback quand aucune cover n'est disponible.
///
/// # Utilisation
///
/// ```rust
/// use pmometadata::DEFAULT_COVER_SVG;
///
/// // Utiliser comme data URL
/// let data_url = format!("data:image/svg+xml;utf8,{}", DEFAULT_COVER_SVG);
/// ```
pub const DEFAULT_COVER_SVG: &str = r#"<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 400 400">
<defs>
<linearGradient id="bgGrad" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" style="stop-color:#667eea;stop-opacity:1" />
<stop offset="100%" style="stop-color:#764ba2;stop-opacity:1" />
</linearGradient>
</defs>
<rect width="400" height="400" fill="url(#bgGrad)"/>
<g transform="translate(200, 200)">
<circle cx="0" cy="20" r="35" fill="white" opacity="0.9"/>
<ellipse cx="0" cy="20" rx="35" ry="10" fill="white" opacity="0.3"/>
<rect x="-6" y="-120" width="12" height="140" rx="6" fill="white" opacity="0.9"/>
<path d="M 6,-115 Q 45,-105 45,-70 Q 45,-40 20,-35"
fill="none" stroke="white" stroke-width="12"
stroke-linecap="round" opacity="0.9"/>
</g>
<text x="200" y="360" text-anchor="middle"
font-family="system-ui, -apple-system, sans-serif"
font-size="20" fill="white" opacity="0.6">
No Cover Available
</text>
</svg>"#;
/// Retourne l'URL de la cover par défaut comme data URL.
///
/// Cette fonction peut être étendue à l'avenir pour accepter des paramètres
/// de personnalisation (taille, couleur, etc.).
///
/// # Exemples
///
/// ```rust
/// use pmometadata::get_default_cover_url;
///
/// let url = get_default_cover_url();
/// // url commence par "data:image/svg+xml;utf8,<svg..."
/// assert!(url.starts_with("data:image/svg+xml"));
/// ```
pub fn get_default_cover_url() -> String {
format!("data:image/svg+xml;utf8,{}", DEFAULT_COVER_SVG)
}
/// Helper macro for copying a single metadata field.
macro_rules! copy_a_metadata {
($src:ident, $dest:ident, $key:ident) => {
@@ -322,6 +377,105 @@ pub trait TrackMetadata: Send + Sync {
Err(MetadataError::NotImplemented)
}
/// Retourne l'URL de la cover avec logique de fallback.
///
/// Cette méthode implémente la logique de priorité suivante :
/// 1. Si `cover_pk` est défini, retourne `Some(cover_pk)` (pour utilisation avec le cover cache)
/// 2. Sinon, si `cover_url` est défini, retourne `Some(cover_url)` (URL externe)
/// 3. Sinon, retourne `None`
///
/// Si vous voulez toujours obtenir une URL (avec image par défaut),
/// utilisez [`get_cover_url_or_default`] à la place.
///
/// Les implémentations peuvent overrider cette méthode si elles veulent un comportement
/// différent, mais l'implémentation par défaut devrait convenir à la plupart des cas.
///
/// # Exemples
///
/// ```rust
/// use pmometadata::{TrackMetadata, MemoryTrackMetadata};
///
/// # tokio_test::block_on(async {
/// let mut metadata = MemoryTrackMetadata::new();
///
/// // Cas 1: cover_pk défini (prioritaire)
/// metadata.set_cover_pk(Some("abc123".to_string())).await.unwrap();
/// metadata.set_cover_url(Some("https://example.com/cover.jpg".to_string())).await.unwrap();
/// assert_eq!(
/// metadata.get_cover_url_with_fallback().await.unwrap(),
/// Some("abc123".to_string())
/// );
///
/// // Cas 2: seulement cover_url
/// let mut metadata2 = MemoryTrackMetadata::new();
/// metadata2.set_cover_url(Some("https://example.com/cover.jpg".to_string())).await.unwrap();
/// assert_eq!(
/// metadata2.get_cover_url_with_fallback().await.unwrap(),
/// Some("https://example.com/cover.jpg".to_string())
/// );
///
/// // Cas 3: aucune cover
/// let metadata3 = MemoryTrackMetadata::new();
/// assert_eq!(metadata3.get_cover_url_with_fallback().await.unwrap(), None);
/// # });
/// ```
async fn get_cover_url_with_fallback(&self) -> MetadataResult<String> {
// Priorité 1: cover_pk (cache local)
if let Ok(Some(pk)) = self.get_cover_pk().await {
if !pk.is_empty() {
return Ok(Some(pk));
}
}
// Priorité 2: cover_url (URL externe)
if let Ok(Some(url)) = self.get_cover_url().await {
if !url.is_empty() {
return Ok(Some(url));
}
}
// Aucune cover disponible
Ok(None)
}
/// Retourne l'URL de la cover ou l'image par défaut.
///
/// Contrairement à [`get_cover_url_with_fallback`], cette méthode retourne **toujours**
/// une URL utilisable, en fournissant une image SVG par défaut si aucune cover n'est disponible.
///
/// La logique de priorité est :
/// 1. `cover_pk` (cache local)
/// 2. `cover_url` (URL externe)
/// 3. Image SVG par défaut (data URL)
///
/// # Exemples
///
/// ```rust
/// use pmometadata::{TrackMetadata, MemoryTrackMetadata};
///
/// # tokio_test::block_on(async {
/// // Sans cover : retourne l'image par défaut
/// let metadata = MemoryTrackMetadata::new();
/// let url = metadata.get_cover_url_or_default().await.unwrap();
/// assert!(url.starts_with("data:image/svg+xml"));
///
/// // Avec cover : retourne la cover
/// let mut metadata2 = MemoryTrackMetadata::new();
/// metadata2.set_cover_pk(Some("abc123".to_string())).await.unwrap();
/// assert_eq!(
/// metadata2.get_cover_url_or_default().await.unwrap(),
/// "abc123"
/// );
/// # });
/// ```
async fn get_cover_url_or_default(&self) -> Result<String, MetadataError> {
match self.get_cover_url_with_fallback().await {
Ok(Some(url)) => Ok(url),
Ok(None) => Ok(get_default_cover_url()),
Err(e) => Err(e),
}
}
async fn get_extra(&self) -> MetadataResult<HashMap<String, String>> {
Err(MetadataError::NotImplemented)
}
@@ -1057,4 +1211,107 @@ mod tests {
Some("Artist".to_string())
);
}
#[tokio::test]
async fn test_get_cover_url_with_fallback_cover_pk_priority() {
let mut metadata = MemoryTrackMetadata::new();
metadata
.set_cover_pk(Some("abc123".to_string()))
.await
.unwrap();
metadata
.set_cover_url(Some("https://example.com/cover.jpg".to_string()))
.await
.unwrap();
// cover_pk should have priority
assert_eq!(
metadata.get_cover_url_with_fallback().await.unwrap(),
Some("abc123".to_string())
);
}
#[tokio::test]
async fn test_get_cover_url_with_fallback_cover_url_only() {
let mut metadata = MemoryTrackMetadata::new();
metadata
.set_cover_url(Some("https://example.com/cover.jpg".to_string()))
.await
.unwrap();
assert_eq!(
metadata.get_cover_url_with_fallback().await.unwrap(),
Some("https://example.com/cover.jpg".to_string())
);
}
#[tokio::test]
async fn test_get_cover_url_with_fallback_none() {
let metadata = MemoryTrackMetadata::new();
assert_eq!(
metadata.get_cover_url_with_fallback().await.unwrap(),
None
);
}
#[tokio::test]
async fn test_get_cover_url_with_fallback_empty_strings() {
let mut metadata = MemoryTrackMetadata::new();
metadata.set_cover_pk(Some("".to_string())).await.unwrap();
metadata
.set_cover_url(Some("https://example.com/cover.jpg".to_string()))
.await
.unwrap();
// Empty cover_pk should fallback to cover_url
assert_eq!(
metadata.get_cover_url_with_fallback().await.unwrap(),
Some("https://example.com/cover.jpg".to_string())
);
}
#[tokio::test]
async fn test_get_cover_url_or_default_with_cover_pk() {
let mut metadata = MemoryTrackMetadata::new();
metadata
.set_cover_pk(Some("abc123".to_string()))
.await
.unwrap();
assert_eq!(
metadata.get_cover_url_or_default().await.unwrap(),
"abc123"
);
}
#[tokio::test]
async fn test_get_cover_url_or_default_with_cover_url() {
let mut metadata = MemoryTrackMetadata::new();
metadata
.set_cover_url(Some("https://example.com/cover.jpg".to_string()))
.await
.unwrap();
assert_eq!(
metadata.get_cover_url_or_default().await.unwrap(),
"https://example.com/cover.jpg"
);
}
#[tokio::test]
async fn test_get_cover_url_or_default_no_cover() {
let metadata = MemoryTrackMetadata::new();
let url = metadata.get_cover_url_or_default().await.unwrap();
// Should return the default SVG data URL
assert!(url.starts_with("data:image/svg+xml"));
assert!(url.contains("<svg"));
}
#[tokio::test]
async fn test_default_cover_url() {
let url = get_default_cover_url();
assert!(url.starts_with("data:image/svg+xml;utf8,<svg"));
assert!(url.contains("No Cover Available"));
}
}