Music Player & Playlist Manager
This commit is contained in:
@@ -6,6 +6,7 @@ pub mod device_code;
|
||||
pub mod entity_errors;
|
||||
pub mod file;
|
||||
pub mod folder;
|
||||
pub mod playlist;
|
||||
pub mod session;
|
||||
pub mod share;
|
||||
pub mod trashed_item;
|
||||
|
||||
@@ -0,0 +1,470 @@
|
||||
use chrono::{DateTime, Utc};
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::common::errors::{DomainError, ErrorKind, Result};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Playlist {
|
||||
id: Uuid,
|
||||
name: String,
|
||||
description: Option<String>,
|
||||
owner_id: Uuid,
|
||||
is_public: bool,
|
||||
cover_file_id: Option<Uuid>,
|
||||
created_at: DateTime<Utc>,
|
||||
updated_at: DateTime<Utc>,
|
||||
}
|
||||
|
||||
impl Playlist {
|
||||
pub fn new(name: String, owner_id: Uuid, description: Option<String>) -> Result<Self> {
|
||||
if name.is_empty() {
|
||||
return Err(DomainError::new(
|
||||
ErrorKind::InvalidInput,
|
||||
"Playlist",
|
||||
"Playlist name cannot be empty",
|
||||
));
|
||||
}
|
||||
|
||||
let now = Utc::now();
|
||||
|
||||
Ok(Self {
|
||||
id: Uuid::new_v4(),
|
||||
name,
|
||||
description,
|
||||
owner_id,
|
||||
is_public: false,
|
||||
cover_file_id: None,
|
||||
created_at: now,
|
||||
updated_at: now,
|
||||
})
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn with_id(
|
||||
id: Uuid,
|
||||
name: String,
|
||||
description: Option<String>,
|
||||
owner_id: Uuid,
|
||||
is_public: bool,
|
||||
cover_file_id: Option<Uuid>,
|
||||
created_at: DateTime<Utc>,
|
||||
updated_at: DateTime<Utc>,
|
||||
) -> Result<Self> {
|
||||
if name.is_empty() {
|
||||
return Err(DomainError::new(
|
||||
ErrorKind::InvalidInput,
|
||||
"Playlist",
|
||||
"Playlist name cannot be empty",
|
||||
));
|
||||
}
|
||||
|
||||
Ok(Self {
|
||||
id,
|
||||
name,
|
||||
description,
|
||||
owner_id,
|
||||
is_public,
|
||||
cover_file_id,
|
||||
created_at,
|
||||
updated_at,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn id(&self) -> &Uuid {
|
||||
&self.id
|
||||
}
|
||||
|
||||
pub fn name(&self) -> &str {
|
||||
&self.name
|
||||
}
|
||||
|
||||
pub fn description(&self) -> Option<&str> {
|
||||
self.description.as_deref()
|
||||
}
|
||||
|
||||
pub fn owner_id(&self) -> &Uuid {
|
||||
&self.owner_id
|
||||
}
|
||||
|
||||
pub fn is_public(&self) -> bool {
|
||||
self.is_public
|
||||
}
|
||||
|
||||
pub fn cover_file_id(&self) -> Option<&Uuid> {
|
||||
self.cover_file_id.as_ref()
|
||||
}
|
||||
|
||||
pub fn created_at(&self) -> &DateTime<Utc> {
|
||||
&self.created_at
|
||||
}
|
||||
|
||||
pub fn updated_at(&self) -> &DateTime<Utc> {
|
||||
&self.updated_at
|
||||
}
|
||||
|
||||
pub fn update_name(&mut self, name: String) -> Result<()> {
|
||||
if name.is_empty() {
|
||||
return Err(DomainError::new(
|
||||
ErrorKind::InvalidInput,
|
||||
"Playlist",
|
||||
"Playlist name cannot be empty",
|
||||
));
|
||||
}
|
||||
self.name = name;
|
||||
self.updated_at = Utc::now();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn update_description(&mut self, description: Option<String>) {
|
||||
self.description = description;
|
||||
self.updated_at = Utc::now();
|
||||
}
|
||||
|
||||
pub fn set_public(&mut self, is_public: bool) {
|
||||
self.is_public = is_public;
|
||||
self.updated_at = Utc::now();
|
||||
}
|
||||
|
||||
pub fn set_cover(&mut self, cover_file_id: Option<Uuid>) {
|
||||
self.cover_file_id = cover_file_id;
|
||||
self.updated_at = Utc::now();
|
||||
}
|
||||
|
||||
pub fn belongs_to(&self, user_id: &Uuid) -> bool {
|
||||
self.owner_id == *user_id
|
||||
}
|
||||
|
||||
pub fn touch(&mut self) {
|
||||
self.updated_at = Utc::now();
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct PlaylistItem {
|
||||
id: Uuid,
|
||||
playlist_id: Uuid,
|
||||
file_id: Uuid,
|
||||
position: i32,
|
||||
added_at: DateTime<Utc>,
|
||||
}
|
||||
|
||||
impl PlaylistItem {
|
||||
pub fn new(playlist_id: Uuid, file_id: Uuid, position: i32) -> Result<Self> {
|
||||
if position < 0 {
|
||||
return Err(DomainError::new(
|
||||
ErrorKind::InvalidInput,
|
||||
"PlaylistItem",
|
||||
"Position cannot be negative",
|
||||
));
|
||||
}
|
||||
|
||||
Ok(Self {
|
||||
id: Uuid::new_v4(),
|
||||
playlist_id,
|
||||
file_id,
|
||||
position,
|
||||
added_at: Utc::now(),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn with_id(
|
||||
id: Uuid,
|
||||
playlist_id: Uuid,
|
||||
file_id: Uuid,
|
||||
position: i32,
|
||||
added_at: DateTime<Utc>,
|
||||
) -> Result<Self> {
|
||||
if position < 0 {
|
||||
return Err(DomainError::new(
|
||||
ErrorKind::InvalidInput,
|
||||
"PlaylistItem",
|
||||
"Position cannot be negative",
|
||||
));
|
||||
}
|
||||
|
||||
Ok(Self {
|
||||
id,
|
||||
playlist_id,
|
||||
file_id,
|
||||
position,
|
||||
added_at,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn id(&self) -> &Uuid {
|
||||
&self.id
|
||||
}
|
||||
|
||||
pub fn playlist_id(&self) -> &Uuid {
|
||||
&self.playlist_id
|
||||
}
|
||||
|
||||
pub fn file_id(&self) -> &Uuid {
|
||||
&self.file_id
|
||||
}
|
||||
|
||||
pub fn position(&self) -> i32 {
|
||||
self.position
|
||||
}
|
||||
|
||||
pub fn added_at(&self) -> &DateTime<Utc> {
|
||||
&self.added_at
|
||||
}
|
||||
|
||||
pub fn set_position(&mut self, position: i32) -> Result<()> {
|
||||
if position < 0 {
|
||||
return Err(DomainError::new(
|
||||
ErrorKind::InvalidInput,
|
||||
"PlaylistItem",
|
||||
"Position cannot be negative",
|
||||
));
|
||||
}
|
||||
self.position = position;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct AudioFileMetadata {
|
||||
file_id: Uuid,
|
||||
title: Option<String>,
|
||||
artist: Option<String>,
|
||||
album: Option<String>,
|
||||
album_artist: Option<String>,
|
||||
genre: Option<String>,
|
||||
track_number: Option<i32>,
|
||||
disc_number: Option<i32>,
|
||||
year: Option<i32>,
|
||||
duration_secs: i32,
|
||||
bitrate: Option<i32>,
|
||||
sample_rate: Option<i32>,
|
||||
channels: Option<i16>,
|
||||
format: Option<String>,
|
||||
created_at: DateTime<Utc>,
|
||||
updated_at: DateTime<Utc>,
|
||||
}
|
||||
|
||||
impl AudioFileMetadata {
|
||||
pub fn new(file_id: Uuid, duration_secs: i32) -> Self {
|
||||
let now = Utc::now();
|
||||
Self {
|
||||
file_id,
|
||||
title: None,
|
||||
artist: None,
|
||||
album: None,
|
||||
album_artist: None,
|
||||
genre: None,
|
||||
track_number: None,
|
||||
disc_number: None,
|
||||
year: None,
|
||||
duration_secs,
|
||||
bitrate: None,
|
||||
sample_rate: None,
|
||||
channels: None,
|
||||
format: None,
|
||||
created_at: now,
|
||||
updated_at: now,
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn with_all_fields(
|
||||
file_id: Uuid,
|
||||
title: Option<String>,
|
||||
artist: Option<String>,
|
||||
album: Option<String>,
|
||||
album_artist: Option<String>,
|
||||
genre: Option<String>,
|
||||
track_number: Option<i32>,
|
||||
disc_number: Option<i32>,
|
||||
year: Option<i32>,
|
||||
duration_secs: i32,
|
||||
bitrate: Option<i32>,
|
||||
sample_rate: Option<i32>,
|
||||
channels: Option<i16>,
|
||||
format: Option<String>,
|
||||
created_at: DateTime<Utc>,
|
||||
updated_at: DateTime<Utc>,
|
||||
) -> Self {
|
||||
Self {
|
||||
file_id,
|
||||
title,
|
||||
artist,
|
||||
album,
|
||||
album_artist,
|
||||
genre,
|
||||
track_number,
|
||||
disc_number,
|
||||
year,
|
||||
duration_secs,
|
||||
bitrate,
|
||||
sample_rate,
|
||||
channels,
|
||||
format,
|
||||
created_at,
|
||||
updated_at,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn file_id(&self) -> &Uuid {
|
||||
&self.file_id
|
||||
}
|
||||
|
||||
pub fn title(&self) -> Option<&str> {
|
||||
self.title.as_deref()
|
||||
}
|
||||
|
||||
pub fn artist(&self) -> Option<&str> {
|
||||
self.artist.as_deref()
|
||||
}
|
||||
|
||||
pub fn album(&self) -> Option<&str> {
|
||||
self.album.as_deref()
|
||||
}
|
||||
|
||||
pub fn album_artist(&self) -> Option<&str> {
|
||||
self.album_artist.as_deref()
|
||||
}
|
||||
|
||||
pub fn genre(&self) -> Option<&str> {
|
||||
self.genre.as_deref()
|
||||
}
|
||||
|
||||
pub fn track_number(&self) -> Option<i32> {
|
||||
self.track_number
|
||||
}
|
||||
|
||||
pub fn disc_number(&self) -> Option<i32> {
|
||||
self.disc_number
|
||||
}
|
||||
|
||||
pub fn year(&self) -> Option<i32> {
|
||||
self.year
|
||||
}
|
||||
|
||||
pub fn duration_secs(&self) -> i32 {
|
||||
self.duration_secs
|
||||
}
|
||||
|
||||
pub fn bitrate(&self) -> Option<i32> {
|
||||
self.bitrate
|
||||
}
|
||||
|
||||
pub fn sample_rate(&self) -> Option<i32> {
|
||||
self.sample_rate
|
||||
}
|
||||
|
||||
pub fn channels(&self) -> Option<i16> {
|
||||
self.channels
|
||||
}
|
||||
|
||||
pub fn format(&self) -> Option<&str> {
|
||||
self.format.as_deref()
|
||||
}
|
||||
|
||||
pub fn created_at(&self) -> &DateTime<Utc> {
|
||||
&self.created_at
|
||||
}
|
||||
|
||||
pub fn updated_at(&self) -> &DateTime<Utc> {
|
||||
&self.updated_at
|
||||
}
|
||||
|
||||
pub fn set_title(&mut self, title: Option<String>) {
|
||||
self.title = title;
|
||||
self.updated_at = Utc::now();
|
||||
}
|
||||
|
||||
pub fn set_artist(&mut self, artist: Option<String>) {
|
||||
self.artist = artist;
|
||||
self.updated_at = Utc::now();
|
||||
}
|
||||
|
||||
pub fn set_album(&mut self, album: Option<String>) {
|
||||
self.album = album;
|
||||
self.updated_at = Utc::now();
|
||||
}
|
||||
|
||||
pub fn set_album_artist(&mut self, album_artist: Option<String>) {
|
||||
self.album_artist = album_artist;
|
||||
self.updated_at = Utc::now();
|
||||
}
|
||||
|
||||
pub fn set_genre(&mut self, genre: Option<String>) {
|
||||
self.genre = genre;
|
||||
self.updated_at = Utc::now();
|
||||
}
|
||||
|
||||
pub fn set_track_number(&mut self, track_number: Option<i32>) {
|
||||
self.track_number = track_number;
|
||||
self.updated_at = Utc::now();
|
||||
}
|
||||
|
||||
pub fn set_disc_number(&mut self, disc_number: Option<i32>) {
|
||||
self.disc_number = disc_number;
|
||||
self.updated_at = Utc::now();
|
||||
}
|
||||
|
||||
pub fn set_year(&mut self, year: Option<i32>) {
|
||||
self.year = year;
|
||||
self.updated_at = Utc::now();
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_create_playlist() {
|
||||
let owner_id = Uuid::new_v4();
|
||||
let playlist = Playlist::new("My Playlist".to_string(), owner_id, None);
|
||||
assert!(playlist.is_ok());
|
||||
let playlist = playlist.unwrap();
|
||||
assert_eq!(playlist.name(), "My Playlist");
|
||||
assert!(!playlist.is_public());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_create_playlist_empty_name() {
|
||||
let owner_id = Uuid::new_v4();
|
||||
let playlist = Playlist::new("".to_string(), owner_id, None);
|
||||
assert!(playlist.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_playlist_update() {
|
||||
let owner_id = Uuid::new_v4();
|
||||
let mut playlist = Playlist::new("My Playlist".to_string(), owner_id, None).unwrap();
|
||||
playlist
|
||||
.update_name("Updated Playlist".to_string())
|
||||
.unwrap();
|
||||
assert_eq!(playlist.name(), "Updated Playlist");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_create_playlist_item() {
|
||||
let playlist_id = Uuid::new_v4();
|
||||
let file_id = Uuid::new_v4();
|
||||
let item = PlaylistItem::new(playlist_id, file_id, 0);
|
||||
assert!(item.is_ok());
|
||||
let item = item.unwrap();
|
||||
assert_eq!(item.position(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_create_playlist_item_negative_position() {
|
||||
let playlist_id = Uuid::new_v4();
|
||||
let file_id = Uuid::new_v4();
|
||||
let item = PlaylistItem::new(playlist_id, file_id, -1);
|
||||
assert!(item.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_audio_metadata() {
|
||||
let file_id = Uuid::new_v4();
|
||||
let metadata = AudioFileMetadata::new(file_id, 180);
|
||||
assert_eq!(metadata.duration_secs(), 180);
|
||||
assert!(metadata.title().is_none());
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@ pub mod calendar_repository;
|
||||
pub mod contact_repository;
|
||||
pub mod file_repository;
|
||||
pub mod folder_repository;
|
||||
pub mod playlist_repository;
|
||||
pub mod session_repository;
|
||||
pub mod settings_repository;
|
||||
pub mod share_repository;
|
||||
|
||||
@@ -0,0 +1,115 @@
|
||||
use crate::common::errors::DomainError;
|
||||
use crate::domain::entities::playlist::{AudioFileMetadata, Playlist, PlaylistItem};
|
||||
use uuid::Uuid;
|
||||
|
||||
pub type PlaylistRepositoryResult<T> = Result<T, DomainError>;
|
||||
|
||||
pub trait PlaylistRepository: Send + Sync + 'static {
|
||||
async fn create_playlist(&self, playlist: Playlist) -> PlaylistRepositoryResult<Playlist>;
|
||||
|
||||
async fn update_playlist(&self, playlist: Playlist) -> PlaylistRepositoryResult<Playlist>;
|
||||
|
||||
async fn delete_playlist(&self, id: &Uuid) -> PlaylistRepositoryResult<()>;
|
||||
|
||||
async fn find_playlist_by_id(&self, id: &Uuid) -> PlaylistRepositoryResult<Playlist>;
|
||||
|
||||
async fn list_playlists_by_owner(
|
||||
&self,
|
||||
owner_id: Uuid,
|
||||
) -> PlaylistRepositoryResult<Vec<Playlist>>;
|
||||
|
||||
async fn list_public_playlists(
|
||||
&self,
|
||||
limit: i64,
|
||||
offset: i64,
|
||||
) -> PlaylistRepositoryResult<Vec<Playlist>>;
|
||||
|
||||
async fn list_shared_with_user(&self, user_id: Uuid)
|
||||
-> PlaylistRepositoryResult<Vec<Playlist>>;
|
||||
|
||||
async fn user_has_access(
|
||||
&self,
|
||||
playlist_id: &Uuid,
|
||||
user_id: Uuid,
|
||||
) -> PlaylistRepositoryResult<bool>;
|
||||
|
||||
async fn share_playlist(
|
||||
&self,
|
||||
playlist_id: &Uuid,
|
||||
user_id: Uuid,
|
||||
can_write: bool,
|
||||
) -> PlaylistRepositoryResult<()>;
|
||||
|
||||
async fn remove_share(&self, playlist_id: &Uuid, user_id: Uuid)
|
||||
-> PlaylistRepositoryResult<()>;
|
||||
|
||||
async fn get_shares(&self, playlist_id: &Uuid) -> PlaylistRepositoryResult<Vec<(Uuid, bool)>>;
|
||||
}
|
||||
|
||||
pub type PlaylistItemRepositoryResult<T> = Result<T, DomainError>;
|
||||
|
||||
pub trait PlaylistItemRepository: Send + Sync + 'static {
|
||||
async fn add_item(&self, item: PlaylistItem) -> PlaylistItemRepositoryResult<PlaylistItem>;
|
||||
|
||||
async fn remove_item(&self, id: &Uuid) -> PlaylistItemRepositoryResult<()>;
|
||||
|
||||
async fn remove_item_by_playlist_and_file(
|
||||
&self,
|
||||
playlist_id: &Uuid,
|
||||
file_id: &Uuid,
|
||||
) -> PlaylistItemRepositoryResult<()>;
|
||||
|
||||
async fn find_item_by_id(&self, id: &Uuid) -> PlaylistItemRepositoryResult<PlaylistItem>;
|
||||
|
||||
async fn list_items_in_playlist(
|
||||
&self,
|
||||
playlist_id: &Uuid,
|
||||
) -> PlaylistItemRepositoryResult<Vec<PlaylistItem>>;
|
||||
|
||||
async fn update_position(
|
||||
&self,
|
||||
id: &Uuid,
|
||||
new_position: i32,
|
||||
) -> PlaylistItemRepositoryResult<()>;
|
||||
|
||||
async fn reorder_items(
|
||||
&self,
|
||||
playlist_id: &Uuid,
|
||||
item_ids: &[Uuid],
|
||||
) -> PlaylistItemRepositoryResult<()>;
|
||||
|
||||
async fn get_item_count(&self, playlist_id: &Uuid) -> PlaylistItemRepositoryResult<i64>;
|
||||
|
||||
async fn get_max_position(&self, playlist_id: &Uuid) -> PlaylistItemRepositoryResult<i32>;
|
||||
}
|
||||
|
||||
pub type AudioMetadataRepositoryResult<T> = Result<T, DomainError>;
|
||||
|
||||
pub trait AudioMetadataRepository: Send + Sync + 'static {
|
||||
async fn create_or_update(
|
||||
&self,
|
||||
metadata: AudioFileMetadata,
|
||||
) -> AudioMetadataRepositoryResult<AudioFileMetadata>;
|
||||
|
||||
async fn find_by_file_id(
|
||||
&self,
|
||||
file_id: &Uuid,
|
||||
) -> AudioMetadataRepositoryResult<Option<AudioFileMetadata>>;
|
||||
|
||||
async fn delete(&self, file_id: &Uuid) -> AudioMetadataRepositoryResult<()>;
|
||||
|
||||
async fn list_by_artist(
|
||||
&self,
|
||||
artist: &str,
|
||||
) -> AudioMetadataRepositoryResult<Vec<AudioFileMetadata>>;
|
||||
|
||||
async fn list_by_album(
|
||||
&self,
|
||||
album: &str,
|
||||
) -> AudioMetadataRepositoryResult<Vec<AudioFileMetadata>>;
|
||||
|
||||
async fn list_by_genre(
|
||||
&self,
|
||||
genre: &str,
|
||||
) -> AudioMetadataRepositoryResult<Vec<AudioFileMetadata>>;
|
||||
}
|
||||
Reference in New Issue
Block a user