Files
pmomusic/pmometadata/src/lib.rs

1318 lines
41 KiB
Rust
Raw Normal View History

2025-10-30 07:06:19 +01:00
//! Minimal metadata abstraction shared between PMO crates.
2025-11-01 21:00:26 +01:00
//!
//! This crate provides a lightweight, async-friendly trait [`TrackMetadata`] for managing
//! audio track metadata across different implementations. It supports both in-memory
//! storage and database-backed implementations.
//!
//! # Features
//!
//! - **Async API**: All operations are asynchronous to support database backends
//! - **Optional fields**: Implementations only need to override supported fields
//! - **Error handling**: Distinguishes between transient errors (NotImplemented, ReadOnly)
//! and backend errors that should be propagated
//! - **Metadata copying**: Helper function to copy metadata between implementations
2025-11-25 08:24:08 +01:00
//! - **Fallback cover**: Provides a default cover image when none is available
2025-11-01 21:00:26 +01:00
//!
//! # Examples
//!
//! ```rust
//! use pmometadata::{TrackMetadata, MemoryTrackMetadata};
//! use std::time::Duration;
//!
//! # tokio_test::block_on(async {
//! let mut metadata = MemoryTrackMetadata::new();
//!
//! // Set some metadata
//! metadata.set_title(Some("My Song".to_string())).await.unwrap();
//! metadata.set_artist(Some("Artist Name".to_string())).await.unwrap();
//! metadata.set_duration(Some(Duration::from_secs(180))).await.unwrap();
//!
//! // Retrieve metadata
//! assert_eq!(metadata.get_title().await.unwrap(), Some("My Song".to_string()));
//! # });
//! ```
2025-10-30 07:06:19 +01:00
#![allow(async_fn_in_trait)]
2025-11-01 21:10:57 +01:00
use async_trait::async_trait;
use std::sync::Arc;
2025-10-30 07:06:19 +01:00
use std::{
collections::HashMap,
time::{Duration, SystemTime},
};
use tokio::sync::RwLock;
2025-11-25 08:24:08 +01:00
/// 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)
}
2025-11-01 21:00:26 +01:00
/// Helper macro for copying a single metadata field.
macro_rules! copy_a_metadata {
($src:ident, $dest:ident, $key:ident) => {
::paste::paste! {
match $src.[<get_ $key>]().await {
Ok(Some(value)) => {
match $dest.write().await.[<set_ $key>](Some(value)).await {
Ok(_) => {},
Err(e) if e.is_transient() => {
// Transient error, try setting to None instead
match $dest.write().await.[<set_ $key>](None).await {
Ok(_) => {},
Err(e2) if e2.is_transient() => {},
Err(e2) => return Err(e2),
}
}
Err(e) => return Err(e),
}
},
Ok(None) => {
match $dest.write().await.[<set_ $key>](None).await {
Ok(_) => {},
Err(e) if e.is_transient() => {},
Err(e) => return Err(e),
}
}
Err(e) if e.is_transient() => {
match $dest.write().await.[<set_ $key>](None).await {
Ok(_) => {},
Err(e2) if e2.is_transient() => {},
Err(e2) => return Err(e2),
}
}
Err(e) => {
return Err(e);
}
}
}
};
}
/// Helper macro for copying multiple metadata fields.
macro_rules! copy_metadata {
($src:ident, $dest:ident, $( $key:ident ),*) => {
$(copy_a_metadata!($src, $dest, $key);)*
};
}
2025-10-30 07:06:19 +01:00
/// Convenience alias for metadata operations that can fail or return no value.
2025-11-01 21:00:26 +01:00
///
/// Returns `Ok(Some(T))` when a value is present, `Ok(None)` when the field is empty,
/// or `Err(MetadataError)` when an error occurs.
2025-10-30 07:06:19 +01:00
pub type MetadataResult<T> = Result<Option<T>, MetadataError>;
/// Errors that can occur when manipulating metadata.
#[derive(Debug, thiserror::Error)]
pub enum MetadataError {
2025-11-01 21:00:26 +01:00
/// The metadata field is not implemented by this provider.
///
/// This is a transient error that indicates the implementation doesn't support
/// this particular field. When copying metadata, these errors are ignored.
2025-10-30 07:06:19 +01:00
#[error("metadata field is not implemented")]
NotImplemented,
2025-11-01 21:00:26 +01:00
/// The metadata field is read-only and cannot be modified.
///
/// This is a transient error. When copying metadata, these errors are ignored.
2025-10-30 07:06:19 +01:00
#[error("metadata field is read-only")]
ReadOnly,
2025-11-01 21:00:26 +01:00
/// An error occurred in the backend (e.g., database error).
///
/// These errors are non-transient and should be propagated to the caller.
2025-10-30 07:06:19 +01:00
#[error("backend error: {0}")]
Backend(String),
}
impl MetadataError {
2025-11-01 21:00:26 +01:00
/// Returns `true` if this error is transient and can be safely ignored.
///
/// Transient errors (`NotImplemented`, `ReadOnly`) indicate that the operation
/// is not supported but is not a critical failure. When copying metadata,
/// transient errors result in setting the destination field to `None`.
pub fn is_transient(&self) -> bool {
2025-10-30 07:06:19 +01:00
matches!(
self,
MetadataError::NotImplemented | MetadataError::ReadOnly
)
}
}
/// Trait implemented by metadata providers.
///
2025-11-01 21:00:26 +01:00
/// This trait provides a uniform interface for accessing and modifying audio track metadata.
/// All methods have default implementations that return `NotImplemented`, so implementations
/// only need to override the fields they support.
///
/// # Implementing the trait
///
/// Implementations should:
/// - Override `get_*` methods for readable fields
/// - Override `set_*` methods for writable fields
/// - Call `touch()` in setters to update the modification timestamp
/// - Return `Ok(Some(value))` for successful operations
/// - Return `Ok(None)` when a field is empty
/// - Return `Err(NotImplemented)` for unsupported fields
/// - Return `Err(ReadOnly)` for read-only fields
/// - Return `Err(Backend(msg))` for backend errors
///
/// # Thread safety
///
/// All implementations must be `Send + Sync` to work in async contexts.
2025-10-30 07:06:19 +01:00
#[async_trait]
pub trait TrackMetadata: Send + Sync {
async fn get_title(&self) -> MetadataResult<String> {
Err(MetadataError::NotImplemented)
}
2025-11-01 21:10:57 +01:00
2025-10-30 07:06:19 +01:00
async fn set_title(&mut self, _value: Option<String>) -> MetadataResult<()> {
Err(MetadataError::NotImplemented)
}
async fn get_artist(&self) -> MetadataResult<String> {
Err(MetadataError::NotImplemented)
}
2025-11-01 21:10:57 +01:00
2025-10-30 07:06:19 +01:00
async fn set_artist(&mut self, _value: Option<String>) -> MetadataResult<()> {
Err(MetadataError::NotImplemented)
}
async fn get_album(&self) -> MetadataResult<String> {
Err(MetadataError::NotImplemented)
}
2025-11-01 21:10:57 +01:00
2025-10-30 07:06:19 +01:00
async fn set_album(&mut self, _value: Option<String>) -> MetadataResult<()> {
Err(MetadataError::NotImplemented)
}
async fn get_year(&self) -> MetadataResult<u32> {
Err(MetadataError::NotImplemented)
}
2025-11-01 21:10:57 +01:00
2025-10-30 07:06:19 +01:00
async fn set_year(&mut self, _value: Option<u32>) -> MetadataResult<()> {
Err(MetadataError::NotImplemented)
}
async fn get_genre(&self) -> MetadataResult<String> {
Err(MetadataError::NotImplemented)
}
async fn set_genre(&mut self, _value: Option<String>) -> MetadataResult<()> {
Err(MetadataError::NotImplemented)
}
async fn get_track_number(&self) -> MetadataResult<u32> {
Err(MetadataError::NotImplemented)
}
async fn set_track_number(&mut self, _value: Option<u32>) -> MetadataResult<()> {
Err(MetadataError::NotImplemented)
}
async fn get_track_total(&self) -> MetadataResult<u32> {
Err(MetadataError::NotImplemented)
}
async fn set_track_total(&mut self, _value: Option<u32>) -> MetadataResult<()> {
Err(MetadataError::NotImplemented)
}
async fn get_disc_number(&self) -> MetadataResult<u32> {
Err(MetadataError::NotImplemented)
}
async fn set_disc_number(&mut self, _value: Option<u32>) -> MetadataResult<()> {
Err(MetadataError::NotImplemented)
}
async fn get_disc_total(&self) -> MetadataResult<u32> {
Err(MetadataError::NotImplemented)
}
async fn set_disc_total(&mut self, _value: Option<u32>) -> MetadataResult<()> {
Err(MetadataError::NotImplemented)
}
2025-10-30 07:06:19 +01:00
async fn get_duration(&self) -> MetadataResult<Duration> {
Err(MetadataError::NotImplemented)
}
2025-11-01 21:10:57 +01:00
2025-10-30 07:06:19 +01:00
async fn set_duration(&mut self, _value: Option<Duration>) -> MetadataResult<()> {
Err(MetadataError::NotImplemented)
}
async fn get_sample_rate(&self) -> MetadataResult<u32> {
Err(MetadataError::NotImplemented)
}
async fn set_sample_rate(&mut self, _value: Option<u32>) -> MetadataResult<()> {
Err(MetadataError::NotImplemented)
}
async fn get_total_samples(&self) -> MetadataResult<u64> {
Err(MetadataError::NotImplemented)
}
async fn set_total_samples(&mut self, _value: Option<u64>) -> MetadataResult<()> {
Err(MetadataError::NotImplemented)
}
async fn get_bits_per_sample(&self) -> MetadataResult<u8> {
Err(MetadataError::NotImplemented)
}
async fn set_bits_per_sample(&mut self, _value: Option<u8>) -> MetadataResult<()> {
Err(MetadataError::NotImplemented)
}
async fn get_channels(&self) -> MetadataResult<u8> {
Err(MetadataError::NotImplemented)
}
async fn set_channels(&mut self, _value: Option<u8>) -> MetadataResult<()> {
Err(MetadataError::NotImplemented)
}
async fn get_bitrate(&self) -> MetadataResult<u32> {
Err(MetadataError::NotImplemented)
}
async fn set_bitrate(&mut self, _value: Option<u32>) -> MetadataResult<()> {
Err(MetadataError::NotImplemented)
}
2025-10-30 07:06:19 +01:00
async fn get_track_id(&self) -> MetadataResult<String> {
Err(MetadataError::NotImplemented)
}
2025-11-01 21:10:57 +01:00
2025-10-30 07:06:19 +01:00
async fn set_track_id(&mut self, _value: Option<String>) -> MetadataResult<()> {
Err(MetadataError::NotImplemented)
}
async fn get_channel_id(&self) -> MetadataResult<String> {
Err(MetadataError::NotImplemented)
}
2025-11-01 21:10:57 +01:00
2025-10-30 07:06:19 +01:00
async fn set_channel_id(&mut self, _value: Option<String>) -> MetadataResult<()> {
Err(MetadataError::NotImplemented)
}
async fn get_event(&self) -> MetadataResult<String> {
Err(MetadataError::NotImplemented)
}
2025-11-01 21:10:57 +01:00
2025-10-30 07:06:19 +01:00
async fn set_event(&mut self, _value: Option<String>) -> MetadataResult<()> {
Err(MetadataError::NotImplemented)
}
async fn get_rating(&self) -> MetadataResult<f32> {
Err(MetadataError::NotImplemented)
}
2025-11-01 21:10:57 +01:00
2025-10-30 07:06:19 +01:00
async fn set_rating(&mut self, _value: Option<f32>) -> MetadataResult<()> {
Err(MetadataError::NotImplemented)
}
async fn get_cover_url(&self) -> MetadataResult<String> {
Err(MetadataError::NotImplemented)
}
2025-11-01 21:10:57 +01:00
2025-10-30 07:06:19 +01:00
async fn set_cover_url(&mut self, _value: Option<String>) -> MetadataResult<()> {
Err(MetadataError::NotImplemented)
}
async fn get_cover_pk(&self) -> MetadataResult<String> {
Err(MetadataError::NotImplemented)
}
2025-11-01 21:10:57 +01:00
2025-10-30 07:06:19 +01:00
async fn set_cover_pk(&mut self, _value: Option<String>) -> MetadataResult<()> {
Err(MetadataError::NotImplemented)
}
2025-11-25 08:24:08 +01:00
/// 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),
}
}
2025-10-30 07:06:19 +01:00
async fn get_extra(&self) -> MetadataResult<HashMap<String, String>> {
Err(MetadataError::NotImplemented)
}
2025-11-01 21:10:57 +01:00
2025-10-30 07:06:19 +01:00
async fn set_extra(&mut self, _value: Option<HashMap<String, String>>) -> MetadataResult<()> {
Err(MetadataError::NotImplemented)
}
async fn get_updated_at(&self) -> MetadataResult<SystemTime> {
Err(MetadataError::NotImplemented)
}
2025-11-01 21:10:57 +01:00
2025-10-30 07:06:19 +01:00
async fn touch(&mut self) -> MetadataResult<()> {
Err(MetadataError::NotImplemented)
}
}
2025-11-01 21:00:26 +01:00
/// Copies all available metadata from one implementation to another.
///
/// This function reads all metadata fields from the source and attempts to write them
/// to the destination. It handles errors intelligently:
///
/// - **Transient errors** (`NotImplemented`, `ReadOnly`): The field is set to `None` in the destination
/// - **Backend errors**: The error is propagated to the caller
/// - **Success**: The value is copied from source to destination
///
/// After all fields are copied, the destination's `touch()` method is called to update
/// its modification timestamp.
///
/// # Arguments
///
/// * `src` - Source metadata wrapped in `Arc<RwLock<_>>`
/// * `dest` - Destination metadata wrapped in `Arc<RwLock<_>>`
///
/// # Errors
///
/// Returns an error if:
/// - The destination returns a backend error when setting a field
/// - The destination's `touch()` method returns an error
///
/// # Examples
///
/// ```rust
/// use pmometadata::{TrackMetadata, MemoryTrackMetadata, copy_metadata_into};
/// use std::sync::Arc;
/// use tokio::sync::RwLock;
///
/// # tokio_test::block_on(async {
/// let mut src = MemoryTrackMetadata::new();
/// src.set_title(Some("My Song".to_string())).await.unwrap();
/// src.set_artist(Some("Artist".to_string())).await.unwrap();
///
/// let dest = MemoryTrackMetadata::new();
///
/// let src_lock = Arc::new(RwLock::new(src));
/// let dest_lock = Arc::new(RwLock::new(dest));
///
/// copy_metadata_into(&src_lock, &dest_lock).await.unwrap();
///
/// assert_eq!(
/// dest_lock.read().await.get_title().await.unwrap(),
/// Some("My Song".to_string())
/// );
/// # });
/// ```
2025-10-30 07:06:19 +01:00
pub async fn copy_metadata_into<S, D>(
src: &Arc<RwLock<S>>,
dest: &Arc<RwLock<D>>,
) -> Result<(), MetadataError>
where
S: TrackMetadata + ?Sized,
D: TrackMetadata + ?Sized,
{
let src_guard = src.read().await;
2025-11-01 21:00:26 +01:00
copy_metadata!(
src_guard, dest, title, artist, album, year, duration, sample_rate, total_samples,
bits_per_sample, track_id, channel_id, event, rating, cover_url, cover_pk, extra
2025-11-01 21:00:26 +01:00
);
2025-10-30 07:06:19 +01:00
2025-11-01 21:00:26 +01:00
// Try to update the timestamp, but ignore transient errors
match dest.write().await.touch().await {
Ok(_) => Ok(()),
Err(e) if e.is_transient() => Ok(()),
Err(e) => Err(e),
2025-10-30 07:06:19 +01:00
}
}
/// In-memory metadata implementation with full read/write support.
#[derive(Clone, Debug, Default, PartialEq)]
pub struct MemoryTrackMetadata {
title: Option<String>,
artist: Option<String>,
album: Option<String>,
year: Option<u32>,
genre: Option<String>,
track_number: Option<u32>,
track_total: Option<u32>,
disc_number: Option<u32>,
disc_total: Option<u32>,
2025-10-30 07:06:19 +01:00
duration: Option<Duration>,
sample_rate: Option<u32>,
total_samples: Option<u64>,
bits_per_sample: Option<u8>,
channels: Option<u8>,
bitrate: Option<u32>,
2025-10-30 07:06:19 +01:00
track_id: Option<String>,
channel_id: Option<String>,
event: Option<String>,
rating: Option<f32>,
cover_url: Option<String>,
cover_pk: Option<String>,
updated_at: Option<SystemTime>,
extra: Option<HashMap<String, String>>,
}
impl MemoryTrackMetadata {
pub fn new() -> Self {
Self::default()
}
}
#[async_trait]
impl TrackMetadata for MemoryTrackMetadata {
async fn get_title(&self) -> MetadataResult<String> {
Ok(self.title.clone())
}
2025-11-01 21:10:57 +01:00
2025-10-30 07:06:19 +01:00
async fn set_title(&mut self, value: Option<String>) -> MetadataResult<()> {
self.title = value;
self.touch().await?;
Ok(Some(()))
}
async fn get_artist(&self) -> MetadataResult<String> {
Ok(self.artist.clone())
}
2025-11-01 21:10:57 +01:00
2025-10-30 07:06:19 +01:00
async fn set_artist(&mut self, value: Option<String>) -> MetadataResult<()> {
self.artist = value;
self.touch().await?;
Ok(Some(()))
}
async fn get_album(&self) -> MetadataResult<String> {
Ok(self.album.clone())
}
2025-11-01 21:10:57 +01:00
2025-10-30 07:06:19 +01:00
async fn set_album(&mut self, value: Option<String>) -> MetadataResult<()> {
self.album = value;
self.touch().await?;
Ok(Some(()))
}
async fn get_year(&self) -> MetadataResult<u32> {
Ok(self.year)
}
2025-11-01 21:10:57 +01:00
2025-10-30 07:06:19 +01:00
async fn set_year(&mut self, value: Option<u32>) -> MetadataResult<()> {
self.year = value;
self.touch().await?;
Ok(Some(()))
}
async fn get_genre(&self) -> MetadataResult<String> {
Ok(self.genre.clone())
}
async fn set_genre(&mut self, value: Option<String>) -> MetadataResult<()> {
self.genre = value;
self.touch().await?;
Ok(Some(()))
}
async fn get_track_number(&self) -> MetadataResult<u32> {
Ok(self.track_number)
}
async fn set_track_number(&mut self, value: Option<u32>) -> MetadataResult<()> {
self.track_number = value;
self.touch().await?;
Ok(Some(()))
}
async fn get_track_total(&self) -> MetadataResult<u32> {
Ok(self.track_total)
}
async fn set_track_total(&mut self, value: Option<u32>) -> MetadataResult<()> {
self.track_total = value;
self.touch().await?;
Ok(Some(()))
}
async fn get_disc_number(&self) -> MetadataResult<u32> {
Ok(self.disc_number)
}
async fn set_disc_number(&mut self, value: Option<u32>) -> MetadataResult<()> {
self.disc_number = value;
self.touch().await?;
Ok(Some(()))
}
async fn get_disc_total(&self) -> MetadataResult<u32> {
Ok(self.disc_total)
}
async fn set_disc_total(&mut self, value: Option<u32>) -> MetadataResult<()> {
self.disc_total = value;
self.touch().await?;
Ok(Some(()))
}
2025-10-30 07:06:19 +01:00
async fn get_duration(&self) -> MetadataResult<Duration> {
Ok(self.duration)
}
2025-11-01 21:10:57 +01:00
2025-10-30 07:06:19 +01:00
async fn set_duration(&mut self, value: Option<Duration>) -> MetadataResult<()> {
self.duration = value;
self.touch().await?;
Ok(Some(()))
}
async fn get_sample_rate(&self) -> MetadataResult<u32> {
Ok(self.sample_rate)
}
async fn set_sample_rate(&mut self, value: Option<u32>) -> MetadataResult<()> {
self.sample_rate = value;
self.touch().await?;
Ok(Some(()))
}
async fn get_total_samples(&self) -> MetadataResult<u64> {
Ok(self.total_samples)
}
async fn set_total_samples(&mut self, value: Option<u64>) -> MetadataResult<()> {
self.total_samples = value;
self.touch().await?;
Ok(Some(()))
}
async fn get_bits_per_sample(&self) -> MetadataResult<u8> {
Ok(self.bits_per_sample)
}
async fn set_bits_per_sample(&mut self, value: Option<u8>) -> MetadataResult<()> {
self.bits_per_sample = value;
self.touch().await?;
Ok(Some(()))
}
async fn get_channels(&self) -> MetadataResult<u8> {
Ok(self.channels)
}
async fn set_channels(&mut self, value: Option<u8>) -> MetadataResult<()> {
self.channels = value;
self.touch().await?;
Ok(Some(()))
}
async fn get_bitrate(&self) -> MetadataResult<u32> {
Ok(self.bitrate)
}
async fn set_bitrate(&mut self, value: Option<u32>) -> MetadataResult<()> {
self.bitrate = value;
self.touch().await?;
Ok(Some(()))
}
2025-10-30 07:06:19 +01:00
async fn get_track_id(&self) -> MetadataResult<String> {
Ok(self.track_id.clone())
}
2025-11-01 21:10:57 +01:00
2025-10-30 07:06:19 +01:00
async fn set_track_id(&mut self, value: Option<String>) -> MetadataResult<()> {
self.track_id = value;
self.touch().await?;
Ok(Some(()))
}
async fn get_channel_id(&self) -> MetadataResult<String> {
Ok(self.channel_id.clone())
}
2025-11-01 21:10:57 +01:00
2025-10-30 07:06:19 +01:00
async fn set_channel_id(&mut self, value: Option<String>) -> MetadataResult<()> {
self.channel_id = value;
self.touch().await?;
Ok(Some(()))
}
async fn get_event(&self) -> MetadataResult<String> {
Ok(self.event.clone())
}
2025-11-01 21:10:57 +01:00
2025-10-30 07:06:19 +01:00
async fn set_event(&mut self, value: Option<String>) -> MetadataResult<()> {
self.event = value;
self.touch().await?;
Ok(Some(()))
}
async fn get_rating(&self) -> MetadataResult<f32> {
Ok(self.rating)
}
2025-11-01 21:10:57 +01:00
2025-10-30 07:06:19 +01:00
async fn set_rating(&mut self, value: Option<f32>) -> MetadataResult<()> {
self.rating = value;
self.touch().await?;
Ok(Some(()))
}
async fn get_cover_url(&self) -> MetadataResult<String> {
Ok(self.cover_url.clone())
}
2025-11-01 21:10:57 +01:00
2025-10-30 07:06:19 +01:00
async fn set_cover_url(&mut self, value: Option<String>) -> MetadataResult<()> {
self.cover_url = value;
self.touch().await?;
Ok(Some(()))
}
async fn get_cover_pk(&self) -> MetadataResult<String> {
Ok(self.cover_pk.clone())
}
2025-11-01 21:10:57 +01:00
2025-10-30 07:06:19 +01:00
async fn set_cover_pk(&mut self, value: Option<String>) -> MetadataResult<()> {
self.cover_pk = value;
self.touch().await?;
Ok(Some(()))
}
async fn get_extra(&self) -> MetadataResult<HashMap<String, String>> {
Ok(self.extra.clone())
}
2025-11-01 21:10:57 +01:00
2025-10-30 07:06:19 +01:00
async fn set_extra(&mut self, value: Option<HashMap<String, String>>) -> MetadataResult<()> {
self.extra = value;
self.touch().await?;
Ok(Some(()))
}
async fn get_updated_at(&self) -> MetadataResult<SystemTime> {
Ok(self.updated_at)
}
2025-11-01 21:10:57 +01:00
2025-10-30 07:06:19 +01:00
async fn touch(&mut self) -> MetadataResult<()> {
self.updated_at = Some(SystemTime::now());
Ok(Some(()))
}
2025-10-30 07:06:19 +01:00
}
2025-11-01 21:00:26 +01:00
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn test_memory_metadata_new() {
let metadata = MemoryTrackMetadata::new();
assert_eq!(metadata.get_title().await.unwrap(), None);
assert_eq!(metadata.get_artist().await.unwrap(), None);
assert_eq!(metadata.get_album().await.unwrap(), None);
assert_eq!(metadata.get_year().await.unwrap(), None);
assert_eq!(metadata.get_duration().await.unwrap(), None);
assert_eq!(metadata.get_sample_rate().await.unwrap(), None);
assert_eq!(metadata.get_total_samples().await.unwrap(), None);
assert_eq!(metadata.get_bits_per_sample().await.unwrap(), None);
2025-11-01 21:00:26 +01:00
assert_eq!(metadata.get_updated_at().await.unwrap(), None);
}
#[tokio::test]
async fn test_memory_metadata_set_get_title() {
let mut metadata = MemoryTrackMetadata::new();
let result = metadata.set_title(Some("Test Title".to_string())).await;
assert!(result.is_ok());
assert_eq!(result.unwrap(), Some(()));
let title = metadata.get_title().await.unwrap();
assert_eq!(title, Some("Test Title".to_string()));
}
#[tokio::test]
async fn test_memory_metadata_set_get_artist() {
let mut metadata = MemoryTrackMetadata::new();
2025-11-01 21:10:57 +01:00
metadata
.set_artist(Some("Artist Name".to_string()))
.await
.unwrap();
2025-11-01 21:00:26 +01:00
assert_eq!(
metadata.get_artist().await.unwrap(),
Some("Artist Name".to_string())
);
}
#[tokio::test]
async fn test_memory_metadata_set_get_album() {
let mut metadata = MemoryTrackMetadata::new();
2025-11-01 21:10:57 +01:00
metadata
.set_album(Some("Album Name".to_string()))
.await
.unwrap();
2025-11-01 21:00:26 +01:00
assert_eq!(
metadata.get_album().await.unwrap(),
Some("Album Name".to_string())
);
}
#[tokio::test]
async fn test_memory_metadata_set_get_year() {
let mut metadata = MemoryTrackMetadata::new();
metadata.set_year(Some(2024)).await.unwrap();
assert_eq!(metadata.get_year().await.unwrap(), Some(2024));
}
#[tokio::test]
async fn test_memory_metadata_set_get_duration() {
let mut metadata = MemoryTrackMetadata::new();
let duration = Duration::from_secs(180);
metadata.set_duration(Some(duration)).await.unwrap();
assert_eq!(metadata.get_duration().await.unwrap(), Some(duration));
}
#[tokio::test]
async fn test_memory_metadata_set_get_rating() {
let mut metadata = MemoryTrackMetadata::new();
metadata.set_rating(Some(4.5)).await.unwrap();
assert_eq!(metadata.get_rating().await.unwrap(), Some(4.5));
}
#[tokio::test]
async fn test_memory_metadata_set_get_track_id() {
let mut metadata = MemoryTrackMetadata::new();
2025-11-01 21:10:57 +01:00
metadata
.set_track_id(Some("track123".to_string()))
.await
.unwrap();
2025-11-01 21:00:26 +01:00
assert_eq!(
metadata.get_track_id().await.unwrap(),
Some("track123".to_string())
);
}
#[tokio::test]
async fn test_memory_metadata_set_get_channel_id() {
let mut metadata = MemoryTrackMetadata::new();
2025-11-01 21:10:57 +01:00
metadata
.set_channel_id(Some("channel456".to_string()))
.await
.unwrap();
2025-11-01 21:00:26 +01:00
assert_eq!(
metadata.get_channel_id().await.unwrap(),
Some("channel456".to_string())
);
}
#[tokio::test]
async fn test_memory_metadata_set_get_event() {
let mut metadata = MemoryTrackMetadata::new();
2025-11-01 21:10:57 +01:00
metadata
.set_event(Some("event789".to_string()))
.await
.unwrap();
2025-11-01 21:00:26 +01:00
assert_eq!(
metadata.get_event().await.unwrap(),
Some("event789".to_string())
);
}
#[tokio::test]
async fn test_memory_metadata_set_get_cover_url() {
let mut metadata = MemoryTrackMetadata::new();
2025-11-01 21:10:57 +01:00
metadata
.set_cover_url(Some("https://example.com/cover.jpg".to_string()))
.await
.unwrap();
2025-11-01 21:00:26 +01:00
assert_eq!(
metadata.get_cover_url().await.unwrap(),
Some("https://example.com/cover.jpg".to_string())
);
}
#[tokio::test]
async fn test_memory_metadata_set_get_cover_pk() {
let mut metadata = MemoryTrackMetadata::new();
2025-11-01 21:10:57 +01:00
metadata
.set_cover_pk(Some("pk123".to_string()))
.await
.unwrap();
2025-11-01 21:00:26 +01:00
assert_eq!(
metadata.get_cover_pk().await.unwrap(),
Some("pk123".to_string())
);
}
#[tokio::test]
async fn test_memory_metadata_set_get_extra() {
let mut metadata = MemoryTrackMetadata::new();
let mut extra = HashMap::new();
extra.insert("key1".to_string(), "value1".to_string());
extra.insert("key2".to_string(), "value2".to_string());
metadata.set_extra(Some(extra.clone())).await.unwrap();
assert_eq!(metadata.get_extra().await.unwrap(), Some(extra));
}
#[tokio::test]
async fn test_memory_metadata_set_none() {
let mut metadata = MemoryTrackMetadata::new();
metadata.set_title(Some("Title".to_string())).await.unwrap();
2025-11-01 21:10:57 +01:00
assert_eq!(
metadata.get_title().await.unwrap(),
Some("Title".to_string())
);
2025-11-01 21:00:26 +01:00
metadata.set_title(None).await.unwrap();
assert_eq!(metadata.get_title().await.unwrap(), None);
}
#[tokio::test]
async fn test_memory_metadata_touch() {
let mut metadata = MemoryTrackMetadata::new();
assert_eq!(metadata.get_updated_at().await.unwrap(), None);
metadata.touch().await.unwrap();
let updated_at = metadata.get_updated_at().await.unwrap();
assert!(updated_at.is_some());
}
#[tokio::test]
async fn test_memory_metadata_touch_on_set() {
let mut metadata = MemoryTrackMetadata::new();
assert_eq!(metadata.get_updated_at().await.unwrap(), None);
metadata.set_title(Some("Title".to_string())).await.unwrap();
let updated_at = metadata.get_updated_at().await.unwrap();
assert!(updated_at.is_some());
}
#[tokio::test]
async fn test_metadata_error_is_transient() {
assert!(MetadataError::NotImplemented.is_transient());
assert!(MetadataError::ReadOnly.is_transient());
assert!(!MetadataError::Backend("error".to_string()).is_transient());
}
#[tokio::test]
async fn test_copy_metadata_into_basic() {
let mut src = MemoryTrackMetadata::new();
2025-11-01 21:10:57 +01:00
src.set_title(Some("Source Title".to_string()))
.await
.unwrap();
src.set_artist(Some("Source Artist".to_string()))
.await
.unwrap();
2025-11-01 21:00:26 +01:00
src.set_year(Some(2024)).await.unwrap();
let dest = MemoryTrackMetadata::new();
let src_lock = Arc::new(RwLock::new(src));
let dest_lock = Arc::new(RwLock::new(dest));
copy_metadata_into(&src_lock, &dest_lock).await.unwrap();
let dest_guard = dest_lock.read().await;
2025-11-01 21:10:57 +01:00
assert_eq!(
dest_guard.get_title().await.unwrap(),
Some("Source Title".to_string())
);
assert_eq!(
dest_guard.get_artist().await.unwrap(),
Some("Source Artist".to_string())
);
2025-11-01 21:00:26 +01:00
assert_eq!(dest_guard.get_year().await.unwrap(), Some(2024));
}
#[tokio::test]
async fn test_copy_metadata_into_partial() {
let mut src = MemoryTrackMetadata::new();
src.set_title(Some("Title".to_string())).await.unwrap();
// artist is None
let dest = MemoryTrackMetadata::new();
let src_lock = Arc::new(RwLock::new(src));
let dest_lock = Arc::new(RwLock::new(dest));
copy_metadata_into(&src_lock, &dest_lock).await.unwrap();
let dest_guard = dest_lock.read().await;
2025-11-01 21:10:57 +01:00
assert_eq!(
dest_guard.get_title().await.unwrap(),
Some("Title".to_string())
);
2025-11-01 21:00:26 +01:00
assert_eq!(dest_guard.get_artist().await.unwrap(), None);
}
#[tokio::test]
async fn test_copy_metadata_into_updates_timestamp() {
let src = MemoryTrackMetadata::new();
let dest = MemoryTrackMetadata::new();
let src_lock = Arc::new(RwLock::new(src));
let dest_lock = Arc::new(RwLock::new(dest));
let before = dest_lock.read().await.get_updated_at().await.unwrap();
assert_eq!(before, None);
copy_metadata_into(&src_lock, &dest_lock).await.unwrap();
let after = dest_lock.read().await.get_updated_at().await.unwrap();
assert!(after.is_some());
}
#[tokio::test]
async fn test_copy_metadata_into_all_fields() {
let mut src = MemoryTrackMetadata::new();
src.set_title(Some("Title".to_string())).await.unwrap();
src.set_artist(Some("Artist".to_string())).await.unwrap();
src.set_album(Some("Album".to_string())).await.unwrap();
src.set_year(Some(2024)).await.unwrap();
2025-11-01 21:10:57 +01:00
src.set_duration(Some(Duration::from_secs(180)))
.await
.unwrap();
src.set_track_id(Some("track123".to_string()))
.await
.unwrap();
src.set_channel_id(Some("channel456".to_string()))
.await
.unwrap();
2025-11-01 21:00:26 +01:00
src.set_event(Some("event789".to_string())).await.unwrap();
src.set_rating(Some(4.5)).await.unwrap();
2025-11-01 21:10:57 +01:00
src.set_cover_url(Some("https://example.com/cover.jpg".to_string()))
.await
.unwrap();
2025-11-01 21:00:26 +01:00
src.set_cover_pk(Some("pk123".to_string())).await.unwrap();
let mut extra = HashMap::new();
extra.insert("key".to_string(), "value".to_string());
src.set_extra(Some(extra.clone())).await.unwrap();
let dest = MemoryTrackMetadata::new();
let src_lock = Arc::new(RwLock::new(src));
let dest_lock = Arc::new(RwLock::new(dest));
copy_metadata_into(&src_lock, &dest_lock).await.unwrap();
let dest_guard = dest_lock.read().await;
2025-11-01 21:10:57 +01:00
assert_eq!(
dest_guard.get_title().await.unwrap(),
Some("Title".to_string())
);
assert_eq!(
dest_guard.get_artist().await.unwrap(),
Some("Artist".to_string())
);
assert_eq!(
dest_guard.get_album().await.unwrap(),
Some("Album".to_string())
);
2025-11-01 21:00:26 +01:00
assert_eq!(dest_guard.get_year().await.unwrap(), Some(2024));
2025-11-01 21:10:57 +01:00
assert_eq!(
dest_guard.get_duration().await.unwrap(),
Some(Duration::from_secs(180))
);
assert_eq!(
dest_guard.get_track_id().await.unwrap(),
Some("track123".to_string())
);
assert_eq!(
dest_guard.get_channel_id().await.unwrap(),
Some("channel456".to_string())
);
assert_eq!(
dest_guard.get_event().await.unwrap(),
Some("event789".to_string())
);
2025-11-01 21:00:26 +01:00
assert_eq!(dest_guard.get_rating().await.unwrap(), Some(4.5));
2025-11-01 21:10:57 +01:00
assert_eq!(
dest_guard.get_cover_url().await.unwrap(),
Some("https://example.com/cover.jpg".to_string())
);
assert_eq!(
dest_guard.get_cover_pk().await.unwrap(),
Some("pk123".to_string())
);
2025-11-01 21:00:26 +01:00
assert_eq!(dest_guard.get_extra().await.unwrap(), Some(extra));
}
#[tokio::test]
async fn test_copy_metadata_handles_errors() {
// Test that copy works even when some fields can't be set
let mut src = MemoryTrackMetadata::new();
src.set_title(Some("Title".to_string())).await.unwrap();
src.set_artist(Some("Artist".to_string())).await.unwrap();
let dest = MemoryTrackMetadata::new();
let src_lock = Arc::new(RwLock::new(src));
let dest_lock = Arc::new(RwLock::new(dest));
// Should succeed
let result = copy_metadata_into(&src_lock, &dest_lock).await;
assert!(result.is_ok());
// Verify the data was copied
let dest_guard = dest_lock.read().await;
2025-11-01 21:10:57 +01:00
assert_eq!(
dest_guard.get_title().await.unwrap(),
Some("Title".to_string())
);
assert_eq!(
dest_guard.get_artist().await.unwrap(),
Some("Artist".to_string())
);
2025-11-01 21:00:26 +01:00
}
2025-11-25 08:24:08 +01:00
#[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"));
}
2025-11-01 21:00:26 +01:00
}