272 lines
7.6 KiB
Rust
272 lines
7.6 KiB
Rust
use serde::{Deserialize, Serialize};
|
|
use sqlx::types::chrono::{DateTime, Utc};
|
|
use sqlx::{Postgres, Pool};
|
|
use std::collections::HashMap;
|
|
// Note: Use tracing::info! macro directly instead of importing
|
|
|
|
/// Représente un mode de lecture pour une playlist
|
|
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
|
pub enum PlaybackMode {
|
|
/// Lecture séquentielle normale
|
|
Normal,
|
|
/// Répéter la playlist
|
|
Repeat,
|
|
/// Répéter un seul track
|
|
RepeatOne,
|
|
/// Lecture aléatoire
|
|
Shuffle,
|
|
}
|
|
|
|
/// Représente une playlist
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct Playlist {
|
|
pub id: i64,
|
|
pub user_id: i64,
|
|
pub name: String,
|
|
pub description: Option<String>,
|
|
pub is_public: bool,
|
|
pub playback_mode: PlaybackMode,
|
|
pub created_at: DateTime<Utc>,
|
|
pub updated_at: DateTime<Utc>,
|
|
}
|
|
|
|
/// Représente un track dans une playlist avec son ordre
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct PlaylistTrack {
|
|
pub playlist_id: i64,
|
|
pub track_id: i64,
|
|
pub position: i32,
|
|
pub added_at: DateTime<Utc>,
|
|
}
|
|
|
|
/// Manager pour gérer les playlists
|
|
pub struct PlaylistManager {
|
|
pool: Pool<Postgres>,
|
|
}
|
|
|
|
impl PlaylistManager {
|
|
pub fn new(pool: Pool<Postgres>) -> Self {
|
|
Self { pool }
|
|
}
|
|
|
|
/// Créer une nouvelle playlist
|
|
#[instrument(skip(self))]
|
|
pub async fn create_playlist(
|
|
&self,
|
|
user_id: i64,
|
|
name: &str,
|
|
description: Option<&str>,
|
|
is_public: bool,
|
|
) -> Result<Playlist, sqlx::Error> {
|
|
let playlist = sqlx::query_as::<_, (i64, String, Option<String>, bool, DateTime<Utc>, DateTime<Utc>)>(
|
|
"INSERT INTO playlists (user_id, name, description, is_public, created_at, updated_at)
|
|
VALUES ($1, $2, $3, $4, NOW(), NOW())
|
|
RETURNING id, name, description, is_public, created_at, updated_at"
|
|
)
|
|
.bind(user_id)
|
|
.bind(name)
|
|
.bind(description)
|
|
.bind(is_public)
|
|
.fetch_one(&self.pool)
|
|
.await?;
|
|
|
|
let (id, name, description, is_public, created_at, updated_at) = playlist;
|
|
|
|
tracing::info!(
|
|
playlist_id = id,
|
|
user_id = user_id,
|
|
"Playlist created"
|
|
);
|
|
|
|
Ok(Playlist {
|
|
id,
|
|
user_id,
|
|
name,
|
|
description,
|
|
is_public,
|
|
playback_mode: PlaybackMode::Normal,
|
|
created_at,
|
|
updated_at,
|
|
})
|
|
}
|
|
|
|
/// Ajouter un track à une playlist
|
|
#[instrument(skip(self))]
|
|
pub async fn add_track_to_playlist(
|
|
&self,
|
|
playlist_id: i64,
|
|
track_id: i64,
|
|
) -> Result<(), sqlx::Error> {
|
|
// Obtenir la position maximale actuelle
|
|
let max_position: Option<i32> = sqlx::query_scalar(
|
|
"SELECT MAX(position) FROM playlist_tracks WHERE playlist_id = $1"
|
|
)
|
|
.bind(playlist_id)
|
|
.fetch_optional(&self.pool)
|
|
.await?;
|
|
|
|
let position = max_position.unwrap_or(0) + 1;
|
|
|
|
sqlx::query(
|
|
"INSERT INTO playlist_tracks (playlist_id, track_id, position, added_at)
|
|
VALUES ($1, $2, $3, NOW())
|
|
ON CONFLICT (playlist_id, track_id) DO NOTHING"
|
|
)
|
|
.bind(playlist_id)
|
|
.bind(track_id)
|
|
.bind(position)
|
|
.execute(&self.pool)
|
|
.await?;
|
|
|
|
tracing::info!(
|
|
playlist_id = playlist_id,
|
|
track_id = track_id,
|
|
position = position,
|
|
"Track added to playlist"
|
|
);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Retirer un track d'une playlist
|
|
#[instrument(skip(self))]
|
|
pub async fn remove_track_from_playlist(
|
|
&self,
|
|
playlist_id: i64,
|
|
track_id: i64,
|
|
) -> Result<(), sqlx::Error> {
|
|
sqlx::query(
|
|
"DELETE FROM playlist_tracks WHERE playlist_id = $1 AND track_id = $2"
|
|
)
|
|
.bind(playlist_id)
|
|
.bind(track_id)
|
|
.execute(&self.pool)
|
|
.await?;
|
|
|
|
tracing::info!(
|
|
playlist_id = playlist_id,
|
|
track_id = track_id,
|
|
"Track removed from playlist"
|
|
);
|
|
|
|
// Réorganiser les positions pour qu'elles soient séquentielles
|
|
self.reorder_positions(playlist_id).await?;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Réorganiser les positions des tracks dans une playlist
|
|
async fn reorder_positions(&self, playlist_id: i64) -> Result<(), sqlx::Error> {
|
|
sqlx::query(
|
|
"UPDATE playlist_tracks
|
|
SET position = subquery.row_num
|
|
FROM (
|
|
SELECT track_id, ROW_NUMBER() OVER (ORDER BY position) AS row_num
|
|
FROM playlist_tracks
|
|
WHERE playlist_id = $1
|
|
) AS subquery
|
|
WHERE playlist_tracks.track_id = subquery.track_id
|
|
AND playlist_tracks.playlist_id = $1"
|
|
)
|
|
.bind(playlist_id)
|
|
.execute(&self.pool)
|
|
.await?;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Obtenir les tracks d'une playlist avec leur ordre
|
|
#[instrument(skip(self))]
|
|
pub async fn get_playlist_tracks(
|
|
&self,
|
|
playlist_id: i64,
|
|
) -> Result<Vec<PlaylistTrack>, sqlx::Error> {
|
|
let tracks = sqlx::query_as::<_, (i64, i64, i32, DateTime<Utc>)>(
|
|
"SELECT playlist_id, track_id, position, added_at
|
|
FROM playlist_tracks
|
|
WHERE playlist_id = $1
|
|
ORDER BY position"
|
|
)
|
|
.bind(playlist_id)
|
|
.fetch_all(&self.pool)
|
|
.await?;
|
|
|
|
let result = tracks.into_iter().map(|(playlist_id, track_id, position, added_at)| {
|
|
PlaylistTrack {
|
|
playlist_id,
|
|
track_id,
|
|
position,
|
|
added_at,
|
|
}
|
|
}).collect();
|
|
|
|
Ok(result)
|
|
}
|
|
|
|
/// Mettre à jour le mode de lecture
|
|
#[instrument(skip(self))]
|
|
pub async fn set_playback_mode(
|
|
&self,
|
|
playlist_id: i64,
|
|
mode: PlaybackMode,
|
|
) -> Result<(), sqlx::Error> {
|
|
let mode_str = match mode {
|
|
PlaybackMode::Normal => "normal",
|
|
PlaybackMode::Repeat => "repeat",
|
|
PlaybackMode::RepeatOne => "repeat_one",
|
|
PlaybackMode::Shuffle => "shuffle",
|
|
};
|
|
|
|
sqlx::query(
|
|
"UPDATE playlists SET playback_mode = $1, updated_at = NOW() WHERE id = $2"
|
|
)
|
|
.bind(mode_str)
|
|
.bind(playlist_id)
|
|
.execute(&self.pool)
|
|
.await?;
|
|
|
|
tracing::info!(playlist_id = playlist_id, mode = ?mode, "Playback mode updated");
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Obtenir les playlists d'un user
|
|
#[instrument(skip(self))]
|
|
pub async fn get_user_playlists(&self, user_id: i64) -> Result<Vec<Playlist>, sqlx::Error> {
|
|
let playlists = sqlx::query_as::<_, (i64, i64, String, Option<String>, bool, DateTime<Utc>, DateTime<Utc>)>(
|
|
"SELECT id, user_id, name, description, is_public, created_at, updated_at
|
|
FROM playlists
|
|
WHERE user_id = $1
|
|
ORDER BY updated_at DESC"
|
|
)
|
|
.bind(user_id)
|
|
.fetch_all(&self.pool)
|
|
.await?;
|
|
|
|
let result = playlists.into_iter().map(|(id, user_id, name, description, is_public, created_at, updated_at)| {
|
|
Playlist {
|
|
id,
|
|
user_id,
|
|
name,
|
|
description,
|
|
is_public,
|
|
playback_mode: PlaybackMode::Normal,
|
|
created_at,
|
|
updated_at,
|
|
}
|
|
}).collect();
|
|
|
|
Ok(result)
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[tokio::test]
|
|
async fn test_playlist_manager() {
|
|
// Note: Ces tests nécessitent une base de données de test
|
|
assert!(true);
|
|
}
|
|
}
|