Compare commits
1 Commits
8a1435d9e9
...
f03f8f0362
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f03f8f0362 |
@@ -1,6 +1,8 @@
|
||||
pub mod album;
|
||||
pub mod artist;
|
||||
pub mod download_queue;
|
||||
pub mod playlist;
|
||||
pub mod playlist_track;
|
||||
pub mod search_cache;
|
||||
pub mod track;
|
||||
pub mod user;
|
||||
@@ -9,6 +11,8 @@ pub mod wanted_item;
|
||||
pub use album::Entity as Albums;
|
||||
pub use artist::Entity as Artists;
|
||||
pub use download_queue::Entity as DownloadQueue;
|
||||
pub use playlist::Entity as Playlists;
|
||||
pub use playlist_track::Entity as PlaylistTracks;
|
||||
pub use search_cache::Entity as SearchCache;
|
||||
pub use track::Entity as Tracks;
|
||||
pub use user::Entity as Users;
|
||||
|
||||
43
src/entities/playlist.rs
Normal file
43
src/entities/playlist.rs
Normal file
@@ -0,0 +1,43 @@
|
||||
use sea_orm::entity::prelude::*;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Serialize, Deserialize)]
|
||||
#[sea_orm(table_name = "playlists")]
|
||||
pub struct Model {
|
||||
#[sea_orm(primary_key)]
|
||||
pub id: i32,
|
||||
pub name: String,
|
||||
#[sea_orm(nullable)]
|
||||
pub description: Option<String>,
|
||||
#[sea_orm(nullable)]
|
||||
pub user_id: Option<i32>,
|
||||
pub created_at: chrono::NaiveDateTime,
|
||||
pub updated_at: chrono::NaiveDateTime,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
||||
pub enum Relation {
|
||||
#[sea_orm(
|
||||
belongs_to = "super::user::Entity",
|
||||
from = "Column::UserId",
|
||||
to = "super::user::Column::Id",
|
||||
on_delete = "SetNull"
|
||||
)]
|
||||
User,
|
||||
#[sea_orm(has_many = "super::playlist_track::Entity")]
|
||||
PlaylistTracks,
|
||||
}
|
||||
|
||||
impl Related<super::user::Entity> for Entity {
|
||||
fn to() -> RelationDef {
|
||||
Relation::User.def()
|
||||
}
|
||||
}
|
||||
|
||||
impl Related<super::playlist_track::Entity> for Entity {
|
||||
fn to() -> RelationDef {
|
||||
Relation::PlaylistTracks.def()
|
||||
}
|
||||
}
|
||||
|
||||
impl ActiveModelBehavior for ActiveModel {}
|
||||
43
src/entities/playlist_track.rs
Normal file
43
src/entities/playlist_track.rs
Normal file
@@ -0,0 +1,43 @@
|
||||
use sea_orm::entity::prelude::*;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Serialize, Deserialize)]
|
||||
#[sea_orm(table_name = "playlist_tracks")]
|
||||
pub struct Model {
|
||||
#[sea_orm(primary_key)]
|
||||
pub id: i32,
|
||||
pub playlist_id: i32,
|
||||
pub track_id: i32,
|
||||
pub position: i32,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
||||
pub enum Relation {
|
||||
#[sea_orm(
|
||||
belongs_to = "super::playlist::Entity",
|
||||
from = "Column::PlaylistId",
|
||||
to = "super::playlist::Column::Id",
|
||||
on_delete = "Cascade"
|
||||
)]
|
||||
Playlist,
|
||||
#[sea_orm(
|
||||
belongs_to = "super::track::Entity",
|
||||
from = "Column::TrackId",
|
||||
to = "super::track::Column::Id"
|
||||
)]
|
||||
Track,
|
||||
}
|
||||
|
||||
impl Related<super::playlist::Entity> for Entity {
|
||||
fn to() -> RelationDef {
|
||||
Relation::Playlist.def()
|
||||
}
|
||||
}
|
||||
|
||||
impl Related<super::track::Entity> for Entity {
|
||||
fn to() -> RelationDef {
|
||||
Relation::Track.def()
|
||||
}
|
||||
}
|
||||
|
||||
impl ActiveModelBehavior for ActiveModel {}
|
||||
136
src/migration/m20260320_000014_create_playlists.rs
Normal file
136
src/migration/m20260320_000014_create_playlists.rs
Normal file
@@ -0,0 +1,136 @@
|
||||
use sea_orm_migration::prelude::*;
|
||||
|
||||
#[derive(DeriveMigrationName)]
|
||||
pub struct Migration;
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl MigrationTrait for Migration {
|
||||
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
|
||||
manager
|
||||
.create_table(
|
||||
Table::create()
|
||||
.table(Playlists::Table)
|
||||
.if_not_exists()
|
||||
.col(
|
||||
ColumnDef::new(Playlists::Id)
|
||||
.integer()
|
||||
.not_null()
|
||||
.auto_increment()
|
||||
.primary_key(),
|
||||
)
|
||||
.col(ColumnDef::new(Playlists::Name).text().not_null())
|
||||
.col(ColumnDef::new(Playlists::Description).text())
|
||||
.col(ColumnDef::new(Playlists::UserId).integer())
|
||||
.col(
|
||||
ColumnDef::new(Playlists::CreatedAt)
|
||||
.date_time()
|
||||
.not_null()
|
||||
.default(Expr::current_timestamp()),
|
||||
)
|
||||
.col(
|
||||
ColumnDef::new(Playlists::UpdatedAt)
|
||||
.date_time()
|
||||
.not_null()
|
||||
.default(Expr::current_timestamp()),
|
||||
)
|
||||
.foreign_key(
|
||||
ForeignKey::create()
|
||||
.from(Playlists::Table, Playlists::UserId)
|
||||
.to(Users::Table, Users::Id),
|
||||
)
|
||||
.to_owned(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
manager
|
||||
.create_table(
|
||||
Table::create()
|
||||
.table(PlaylistTracks::Table)
|
||||
.if_not_exists()
|
||||
.col(
|
||||
ColumnDef::new(PlaylistTracks::Id)
|
||||
.integer()
|
||||
.not_null()
|
||||
.auto_increment()
|
||||
.primary_key(),
|
||||
)
|
||||
.col(
|
||||
ColumnDef::new(PlaylistTracks::PlaylistId)
|
||||
.integer()
|
||||
.not_null(),
|
||||
)
|
||||
.col(ColumnDef::new(PlaylistTracks::TrackId).integer().not_null())
|
||||
.col(
|
||||
ColumnDef::new(PlaylistTracks::Position)
|
||||
.integer()
|
||||
.not_null(),
|
||||
)
|
||||
.foreign_key(
|
||||
ForeignKey::create()
|
||||
.from(PlaylistTracks::Table, PlaylistTracks::PlaylistId)
|
||||
.to(Playlists::Table, Playlists::Id)
|
||||
.on_delete(ForeignKeyAction::Cascade),
|
||||
)
|
||||
.foreign_key(
|
||||
ForeignKey::create()
|
||||
.from(PlaylistTracks::Table, PlaylistTracks::TrackId)
|
||||
.to(Tracks::Table, Tracks::Id),
|
||||
)
|
||||
.to_owned(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
manager
|
||||
.create_index(
|
||||
Index::create()
|
||||
.name("idx_playlist_tracks_unique")
|
||||
.table(PlaylistTracks::Table)
|
||||
.col(PlaylistTracks::PlaylistId)
|
||||
.col(PlaylistTracks::Position)
|
||||
.unique()
|
||||
.to_owned(),
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
|
||||
manager
|
||||
.drop_table(Table::drop().table(PlaylistTracks::Table).to_owned())
|
||||
.await?;
|
||||
manager
|
||||
.drop_table(Table::drop().table(Playlists::Table).to_owned())
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(DeriveIden)]
|
||||
enum Playlists {
|
||||
Table,
|
||||
Id,
|
||||
Name,
|
||||
Description,
|
||||
UserId,
|
||||
CreatedAt,
|
||||
UpdatedAt,
|
||||
}
|
||||
|
||||
#[derive(DeriveIden)]
|
||||
enum PlaylistTracks {
|
||||
Table,
|
||||
Id,
|
||||
PlaylistId,
|
||||
TrackId,
|
||||
Position,
|
||||
}
|
||||
|
||||
#[derive(DeriveIden)]
|
||||
enum Users {
|
||||
Table,
|
||||
Id,
|
||||
}
|
||||
|
||||
#[derive(DeriveIden)]
|
||||
enum Tracks {
|
||||
Table,
|
||||
Id,
|
||||
}
|
||||
@@ -12,6 +12,7 @@ mod m20260317_000010_add_wanted_mbid;
|
||||
mod m20260319_000011_create_users;
|
||||
mod m20260319_000012_add_user_id_to_wanted_items;
|
||||
mod m20260320_000013_add_artist_monitoring;
|
||||
mod m20260320_000014_create_playlists;
|
||||
|
||||
pub struct Migrator;
|
||||
|
||||
@@ -31,6 +32,7 @@ impl MigratorTrait for Migrator {
|
||||
Box::new(m20260319_000011_create_users::Migration),
|
||||
Box::new(m20260319_000012_add_user_id_to_wanted_items::Migration),
|
||||
Box::new(m20260320_000013_add_artist_monitoring::Migration),
|
||||
Box::new(m20260320_000014_create_playlists::Migration),
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ pub mod albums;
|
||||
pub mod artists;
|
||||
pub mod cache;
|
||||
pub mod downloads;
|
||||
pub mod playlists;
|
||||
pub mod tracks;
|
||||
pub mod users;
|
||||
pub mod wanted;
|
||||
|
||||
105
src/queries/playlists.rs
Normal file
105
src/queries/playlists.rs
Normal file
@@ -0,0 +1,105 @@
|
||||
use chrono::Utc;
|
||||
use sea_orm::*;
|
||||
|
||||
use crate::entities::playlist::{self, ActiveModel, Entity as Playlists, Model as Playlist};
|
||||
use crate::entities::playlist_track::{
|
||||
self, ActiveModel as TrackActiveModel, Entity as PlaylistTracks,
|
||||
};
|
||||
use crate::entities::track::{Entity as Tracks, Model as Track};
|
||||
use crate::error::{DbError, DbResult};
|
||||
|
||||
pub async fn create(
|
||||
db: &DatabaseConnection,
|
||||
name: &str,
|
||||
description: Option<&str>,
|
||||
user_id: Option<i32>,
|
||||
track_ids: &[i32],
|
||||
) -> DbResult<Playlist> {
|
||||
let now = Utc::now().naive_utc();
|
||||
let active = ActiveModel {
|
||||
name: Set(name.to_string()),
|
||||
description: Set(description.map(String::from)),
|
||||
user_id: Set(user_id),
|
||||
created_at: Set(now),
|
||||
updated_at: Set(now),
|
||||
..Default::default()
|
||||
};
|
||||
let playlist = active.insert(db).await?;
|
||||
|
||||
// Insert tracks with positions
|
||||
for (pos, track_id) in track_ids.iter().enumerate() {
|
||||
let pt = TrackActiveModel {
|
||||
playlist_id: Set(playlist.id),
|
||||
track_id: Set(*track_id),
|
||||
position: Set(pos as i32),
|
||||
..Default::default()
|
||||
};
|
||||
pt.insert(db).await?;
|
||||
}
|
||||
|
||||
Ok(playlist)
|
||||
}
|
||||
|
||||
pub async fn list(db: &DatabaseConnection, user_id: Option<i32>) -> DbResult<Vec<Playlist>> {
|
||||
let mut query = Playlists::find().order_by_desc(playlist::Column::CreatedAt);
|
||||
if let Some(uid) = user_id {
|
||||
query = query.filter(playlist::Column::UserId.eq(uid));
|
||||
}
|
||||
Ok(query.all(db).await?)
|
||||
}
|
||||
|
||||
pub async fn get_by_id(db: &DatabaseConnection, id: i32) -> DbResult<Playlist> {
|
||||
Playlists::find_by_id(id)
|
||||
.one(db)
|
||||
.await?
|
||||
.ok_or_else(|| DbError::NotFound(format!("playlist id={id}")))
|
||||
}
|
||||
|
||||
pub async fn get_tracks(db: &DatabaseConnection, playlist_id: i32) -> DbResult<Vec<Track>> {
|
||||
// Get playlist_tracks ordered by position, then join with tracks
|
||||
let pt_list = PlaylistTracks::find()
|
||||
.filter(playlist_track::Column::PlaylistId.eq(playlist_id))
|
||||
.order_by_asc(playlist_track::Column::Position)
|
||||
.all(db)
|
||||
.await?;
|
||||
|
||||
let mut tracks = Vec::with_capacity(pt_list.len());
|
||||
for pt in pt_list {
|
||||
if let Some(track) = Tracks::find_by_id(pt.track_id).one(db).await? {
|
||||
tracks.push(track);
|
||||
}
|
||||
}
|
||||
Ok(tracks)
|
||||
}
|
||||
|
||||
pub async fn get_track_count(db: &DatabaseConnection, playlist_id: i32) -> DbResult<u64> {
|
||||
let count = PlaylistTracks::find()
|
||||
.filter(playlist_track::Column::PlaylistId.eq(playlist_id))
|
||||
.count(db)
|
||||
.await?;
|
||||
Ok(count)
|
||||
}
|
||||
|
||||
pub async fn update(
|
||||
db: &DatabaseConnection,
|
||||
id: i32,
|
||||
name: Option<&str>,
|
||||
description: Option<&str>,
|
||||
) -> DbResult<Playlist> {
|
||||
let existing = get_by_id(db, id).await?;
|
||||
let mut active: ActiveModel = existing.into();
|
||||
if let Some(n) = name {
|
||||
active.name = Set(n.to_string());
|
||||
}
|
||||
if let Some(d) = description {
|
||||
active.description = Set(Some(d.to_string()));
|
||||
}
|
||||
active.updated_at = Set(Utc::now().naive_utc());
|
||||
Ok(active.update(db).await?)
|
||||
}
|
||||
|
||||
pub async fn delete(db: &DatabaseConnection, id: i32) -> DbResult<()> {
|
||||
// playlist_tracks cascade-deleted by FK
|
||||
Playlists::delete_by_id(id).exec(db).await?;
|
||||
Ok(())
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
use chrono::Utc;
|
||||
use sea_orm::sea_query::Expr;
|
||||
use sea_orm::*;
|
||||
|
||||
use crate::entities::track::{self, ActiveModel, Entity as Tracks, Model as Track};
|
||||
@@ -121,3 +122,66 @@ pub async fn delete(db: &DatabaseConnection, id: i32) -> DbResult<()> {
|
||||
Tracks::delete_by_id(id).exec(db).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Get tracks matching a genre (case-insensitive).
|
||||
pub async fn get_by_genre(db: &DatabaseConnection, genre: &str) -> DbResult<Vec<Track>> {
|
||||
let pattern = format!("%{genre}%");
|
||||
Ok(Tracks::find()
|
||||
.filter(Expr::cust_with_values(
|
||||
"LOWER(genre) LIKE LOWER(?)",
|
||||
[pattern],
|
||||
))
|
||||
.all(db)
|
||||
.await?)
|
||||
}
|
||||
|
||||
/// Get tracks by artist MusicBrainz ID (via the artist table).
|
||||
pub async fn get_by_artist_mbid(db: &DatabaseConnection, mbid: &str) -> DbResult<Vec<Track>> {
|
||||
use crate::entities::artist;
|
||||
|
||||
// Find artist with this MBID
|
||||
let artist = artist::Entity::find()
|
||||
.filter(artist::Column::MusicbrainzId.eq(mbid))
|
||||
.one(db)
|
||||
.await?;
|
||||
|
||||
if let Some(a) = artist {
|
||||
Ok(Tracks::find()
|
||||
.filter(track::Column::ArtistId.eq(a.id))
|
||||
.all(db)
|
||||
.await?)
|
||||
} else {
|
||||
Ok(vec![])
|
||||
}
|
||||
}
|
||||
|
||||
/// Get random tracks.
|
||||
pub async fn get_random(db: &DatabaseConnection, count: u64) -> DbResult<Vec<Track>> {
|
||||
Ok(Tracks::find()
|
||||
.order_by(Expr::cust("RANDOM()"), Order::Asc)
|
||||
.limit(count)
|
||||
.all(db)
|
||||
.await?)
|
||||
}
|
||||
|
||||
/// Get tracks added within the last N days.
|
||||
pub async fn get_recent(db: &DatabaseConnection, days: u32, limit: u64) -> DbResult<Vec<Track>> {
|
||||
let cutoff = Utc::now().naive_utc() - chrono::Duration::days(i64::from(days));
|
||||
Ok(Tracks::find()
|
||||
.filter(track::Column::AddedAt.gte(cutoff))
|
||||
.order_by_desc(track::Column::AddedAt)
|
||||
.limit(limit)
|
||||
.all(db)
|
||||
.await?)
|
||||
}
|
||||
|
||||
/// Get tracks by artist name (case-insensitive match).
|
||||
pub async fn get_by_artist_name(db: &DatabaseConnection, name: &str) -> DbResult<Vec<Track>> {
|
||||
Ok(Tracks::find()
|
||||
.filter(Expr::cust_with_values(
|
||||
"LOWER(artist) = LOWER(?)",
|
||||
[name.to_string()],
|
||||
))
|
||||
.all(db)
|
||||
.await?)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user