Music Player & Playlist Manager
This commit is contained in:
@@ -10,7 +10,9 @@
|
||||
pub mod calendar_storage_adapter;
|
||||
pub mod contact_storage_adapter;
|
||||
pub mod error_adapters;
|
||||
pub mod music_storage_adapter;
|
||||
|
||||
pub use calendar_storage_adapter::CalendarStorageAdapter;
|
||||
pub use contact_storage_adapter::ContactStorageAdapter;
|
||||
pub use error_adapters::IntoDomainError;
|
||||
pub use music_storage_adapter::MusicStorageAdapter;
|
||||
|
||||
@@ -0,0 +1,296 @@
|
||||
use std::sync::Arc;
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::application::dtos::playlist_dto::{
|
||||
AudioMetadataDto, CreatePlaylistDto, PlaylistDto, PlaylistItemDto, UpdatePlaylistDto,
|
||||
};
|
||||
use crate::application::ports::music_ports::MusicStoragePort;
|
||||
use crate::common::errors::{DomainError, ErrorKind};
|
||||
use crate::domain::entities::playlist::Playlist;
|
||||
use crate::domain::repositories::playlist_repository::{
|
||||
AudioMetadataRepository, PlaylistItemRepository, PlaylistRepository,
|
||||
};
|
||||
use crate::infrastructure::repositories::pg::{
|
||||
AudioMetadataPgRepository, PlaylistItemPgRepository, PlaylistPgRepository,
|
||||
};
|
||||
|
||||
pub struct MusicStorageAdapter {
|
||||
playlist_repository: Arc<PlaylistPgRepository>,
|
||||
item_repository: Arc<PlaylistItemPgRepository>,
|
||||
audio_metadata_repository: Arc<AudioMetadataPgRepository>,
|
||||
}
|
||||
|
||||
impl MusicStorageAdapter {
|
||||
pub fn new(
|
||||
playlist_repository: Arc<PlaylistPgRepository>,
|
||||
item_repository: Arc<PlaylistItemPgRepository>,
|
||||
audio_metadata_repository: Arc<AudioMetadataPgRepository>,
|
||||
) -> Self {
|
||||
Self {
|
||||
playlist_repository,
|
||||
item_repository,
|
||||
audio_metadata_repository,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl MusicStoragePort for MusicStorageAdapter {
|
||||
async fn create_playlist(
|
||||
&self,
|
||||
dto: CreatePlaylistDto,
|
||||
user_id: Uuid,
|
||||
) -> Result<PlaylistDto, DomainError> {
|
||||
let playlist = Playlist::new(dto.name, user_id, dto.description)?;
|
||||
let created = self.playlist_repository.create_playlist(playlist).await?;
|
||||
Ok(PlaylistDto::from(created))
|
||||
}
|
||||
|
||||
async fn update_playlist(
|
||||
&self,
|
||||
playlist_id: &str,
|
||||
dto: UpdatePlaylistDto,
|
||||
) -> Result<PlaylistDto, DomainError> {
|
||||
let uuid = Uuid::parse_str(playlist_id).map_err(|_| {
|
||||
DomainError::new(ErrorKind::InvalidInput, "Playlist", "Invalid playlist ID")
|
||||
})?;
|
||||
|
||||
let mut playlist = self.playlist_repository.find_playlist_by_id(&uuid).await?;
|
||||
|
||||
if let Some(name) = dto.name {
|
||||
playlist.update_name(name)?;
|
||||
}
|
||||
if let Some(description) = dto.description {
|
||||
playlist.update_description(Some(description));
|
||||
}
|
||||
if let Some(is_public) = dto.is_public {
|
||||
playlist.set_public(is_public);
|
||||
}
|
||||
if let Some(cover_file_id) = dto.cover_file_id {
|
||||
let cover_uuid = Uuid::parse_str(&cover_file_id).map_err(|_| {
|
||||
DomainError::new(ErrorKind::InvalidInput, "Playlist", "Invalid cover file ID")
|
||||
})?;
|
||||
playlist.set_cover(Some(cover_uuid));
|
||||
}
|
||||
|
||||
let updated = self.playlist_repository.update_playlist(playlist).await?;
|
||||
Ok(PlaylistDto::from(updated))
|
||||
}
|
||||
|
||||
async fn delete_playlist(&self, playlist_id: &str) -> Result<(), DomainError> {
|
||||
let uuid = Uuid::parse_str(playlist_id).map_err(|_| {
|
||||
DomainError::new(ErrorKind::InvalidInput, "Playlist", "Invalid playlist ID")
|
||||
})?;
|
||||
self.playlist_repository.delete_playlist(&uuid).await
|
||||
}
|
||||
|
||||
async fn get_playlist(&self, playlist_id: &str) -> Result<Option<PlaylistDto>, DomainError> {
|
||||
let uuid = Uuid::parse_str(playlist_id).map_err(|_| {
|
||||
DomainError::new(ErrorKind::InvalidInput, "Playlist", "Invalid playlist ID")
|
||||
})?;
|
||||
|
||||
match self.playlist_repository.find_playlist_by_id(&uuid).await {
|
||||
Ok(playlist) => Ok(Some(PlaylistDto::from(playlist))),
|
||||
Err(e) if e.kind == ErrorKind::NotFound => Ok(None),
|
||||
Err(e) => Err(e),
|
||||
}
|
||||
}
|
||||
|
||||
async fn list_playlists_by_owner(
|
||||
&self,
|
||||
owner_id: Uuid,
|
||||
) -> Result<Vec<PlaylistDto>, DomainError> {
|
||||
let playlists = self
|
||||
.playlist_repository
|
||||
.list_playlists_by_owner(owner_id)
|
||||
.await?;
|
||||
let mut result = Vec::new();
|
||||
for playlist in playlists {
|
||||
let dto = PlaylistDto::from(playlist);
|
||||
let track_count = self
|
||||
.get_track_count(&uuid::Uuid::parse_str(&dto.id).unwrap())
|
||||
.await?;
|
||||
result.push(dto.with_track_info(track_count, 0));
|
||||
}
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
async fn list_shared_with_user(&self, user_id: Uuid) -> Result<Vec<PlaylistDto>, DomainError> {
|
||||
let playlists = self
|
||||
.playlist_repository
|
||||
.list_shared_with_user(user_id)
|
||||
.await?;
|
||||
let mut result = Vec::new();
|
||||
for playlist in playlists {
|
||||
let dto = PlaylistDto::from(playlist);
|
||||
let track_count = self
|
||||
.get_track_count(&uuid::Uuid::parse_str(&dto.id).unwrap())
|
||||
.await?;
|
||||
result.push(dto.with_track_info(track_count, 0));
|
||||
}
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
async fn list_public_playlists(
|
||||
&self,
|
||||
limit: i64,
|
||||
offset: i64,
|
||||
) -> Result<Vec<PlaylistDto>, DomainError> {
|
||||
let playlists = self
|
||||
.playlist_repository
|
||||
.list_public_playlists(limit, offset)
|
||||
.await?;
|
||||
let mut result = Vec::new();
|
||||
for playlist in playlists {
|
||||
let dto = PlaylistDto::from(playlist);
|
||||
let track_count = self
|
||||
.get_track_count(&uuid::Uuid::parse_str(&dto.id).unwrap())
|
||||
.await?;
|
||||
result.push(dto.with_track_info(track_count, 0));
|
||||
}
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
async fn user_has_access(&self, playlist_id: &str, user_id: Uuid) -> Result<bool, DomainError> {
|
||||
let uuid = Uuid::parse_str(playlist_id).map_err(|_| {
|
||||
DomainError::new(ErrorKind::InvalidInput, "Playlist", "Invalid playlist ID")
|
||||
})?;
|
||||
self.playlist_repository
|
||||
.user_has_access(&uuid, user_id)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn user_can_write(&self, playlist_id: &str, user_id: Uuid) -> Result<bool, DomainError> {
|
||||
let uuid = Uuid::parse_str(playlist_id).map_err(|_| {
|
||||
DomainError::new(ErrorKind::InvalidInput, "Playlist", "Invalid playlist ID")
|
||||
})?;
|
||||
|
||||
let playlist = self.playlist_repository.find_playlist_by_id(&uuid).await?;
|
||||
if playlist.owner_id() == &user_id {
|
||||
return Ok(true);
|
||||
}
|
||||
|
||||
let shares = self.playlist_repository.get_shares(&uuid).await?;
|
||||
Ok(shares
|
||||
.iter()
|
||||
.any(|(uid, can_write)| uid == &user_id && *can_write))
|
||||
}
|
||||
|
||||
async fn add_tracks(
|
||||
&self,
|
||||
playlist_id: &Uuid,
|
||||
file_ids: &[Uuid],
|
||||
) -> Result<Vec<PlaylistItemDto>, DomainError> {
|
||||
let mut max_position = self.item_repository.get_max_position(playlist_id).await?;
|
||||
let mut items = Vec::new();
|
||||
|
||||
for file_id in file_ids {
|
||||
max_position += 1;
|
||||
let item = crate::domain::entities::playlist::PlaylistItem::new(
|
||||
*playlist_id,
|
||||
*file_id,
|
||||
max_position,
|
||||
)?;
|
||||
let created = self.item_repository.add_item(item).await?;
|
||||
items.push(PlaylistItemDto::from(created));
|
||||
}
|
||||
|
||||
Ok(items)
|
||||
}
|
||||
|
||||
async fn remove_track(&self, playlist_id: &Uuid, file_id: &Uuid) -> Result<(), DomainError> {
|
||||
self.item_repository
|
||||
.remove_item_by_playlist_and_file(playlist_id, file_id)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn reorder_tracks(
|
||||
&self,
|
||||
playlist_id: &Uuid,
|
||||
item_ids: &[Uuid],
|
||||
) -> Result<(), DomainError> {
|
||||
self.item_repository
|
||||
.reorder_items(playlist_id, item_ids)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn list_playlist_tracks(
|
||||
&self,
|
||||
playlist_id: &Uuid,
|
||||
) -> Result<Vec<PlaylistItemDto>, DomainError> {
|
||||
let enriched_items = self
|
||||
.item_repository
|
||||
.list_items_in_playlist_enriched(playlist_id)
|
||||
.await?;
|
||||
|
||||
let items: Vec<PlaylistItemDto> = enriched_items
|
||||
.into_iter()
|
||||
.map(
|
||||
|row| crate::application::dtos::playlist_dto::PlaylistItemDto {
|
||||
id: row.id.to_string(),
|
||||
playlist_id: row.playlist_id.to_string(),
|
||||
file_id: row.file_id.to_string(),
|
||||
position: row.position,
|
||||
added_at: row.added_at,
|
||||
file_name: row.file_name,
|
||||
file_size: row.file_size,
|
||||
mime_type: row.mime_type,
|
||||
title: row.title,
|
||||
artist: row.artist,
|
||||
album: row.album,
|
||||
duration_secs: row.duration_secs,
|
||||
},
|
||||
)
|
||||
.collect();
|
||||
Ok(items)
|
||||
}
|
||||
|
||||
async fn share_playlist(
|
||||
&self,
|
||||
playlist_id: &Uuid,
|
||||
user_id: Uuid,
|
||||
can_write: bool,
|
||||
) -> Result<(), DomainError> {
|
||||
self.playlist_repository
|
||||
.share_playlist(playlist_id, user_id, can_write)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn remove_share(&self, playlist_id: &Uuid, user_id: Uuid) -> Result<(), DomainError> {
|
||||
self.playlist_repository
|
||||
.remove_share(playlist_id, user_id)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn get_shares(&self, playlist_id: &Uuid) -> Result<Vec<(Uuid, bool)>, DomainError> {
|
||||
self.playlist_repository.get_shares(playlist_id).await
|
||||
}
|
||||
|
||||
async fn get_audio_metadata(
|
||||
&self,
|
||||
file_id: &Uuid,
|
||||
) -> Result<Option<AudioMetadataDto>, DomainError> {
|
||||
match self
|
||||
.audio_metadata_repository
|
||||
.find_by_file_id(file_id)
|
||||
.await
|
||||
{
|
||||
Ok(Some(metadata)) => Ok(Some(AudioMetadataDto::from(metadata))),
|
||||
Ok(None) => Ok(None),
|
||||
Err(e) => Err(e),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl MusicStorageAdapter {
|
||||
async fn get_track_count(&self, playlist_id: &Uuid) -> Result<i64, DomainError> {
|
||||
let count: (i64,) =
|
||||
sqlx::query_as("SELECT COUNT(*) FROM audio.playlist_items WHERE playlist_id = $1")
|
||||
.bind(playlist_id)
|
||||
.fetch_one(self.playlist_repository.pool())
|
||||
.await
|
||||
.map_err(|e| {
|
||||
DomainError::database_error(format!("Failed to get track count: {}", e))
|
||||
})?;
|
||||
Ok(count.0)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user