Music Player & Playlist Manager

This commit is contained in:
Andrey Tkachenko
2026-04-08 15:14:03 +03:00
parent 2e8e0ef6c5
commit da066f47fa
53 changed files with 6043 additions and 23 deletions
+2
View File
@@ -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)
}
}