veza/veza-stream-server/src/playlist_manager.rs
2025-12-03 20:36:56 +01:00

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);
}
}