Compare commits

...

2 Commits

Author SHA1 Message Date
Connor Johnstone
1f983bbecf Minimal subsonic functionality 2026-03-20 20:04:35 -04:00
Connor Johnstone
4fda9071c7 Added the playlist editor 2026-03-20 18:36:59 -04:00
5 changed files with 126 additions and 0 deletions

View File

@@ -19,6 +19,9 @@ pub struct Model {
pub username: String, pub username: String,
#[serde(skip_serializing)] #[serde(skip_serializing)]
pub password_hash: String, pub password_hash: String,
#[serde(skip_serializing)]
#[sea_orm(nullable)]
pub subsonic_password: Option<String>,
pub role: UserRole, pub role: UserRole,
pub created_at: chrono::NaiveDateTime, pub created_at: chrono::NaiveDateTime,
pub updated_at: chrono::NaiveDateTime, pub updated_at: chrono::NaiveDateTime,

View File

@@ -0,0 +1,35 @@
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
.alter_table(
Table::alter()
.table(Users::Table)
.add_column(ColumnDef::new(Users::SubsonicPassword).text())
.to_owned(),
)
.await
}
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager
.alter_table(
Table::alter()
.table(Users::Table)
.drop_column(Users::SubsonicPassword)
.to_owned(),
)
.await
}
}
#[derive(DeriveIden)]
enum Users {
Table,
SubsonicPassword,
}

View File

@@ -13,6 +13,7 @@ mod m20260319_000011_create_users;
mod m20260319_000012_add_user_id_to_wanted_items; mod m20260319_000012_add_user_id_to_wanted_items;
mod m20260320_000013_add_artist_monitoring; mod m20260320_000013_add_artist_monitoring;
mod m20260320_000014_create_playlists; mod m20260320_000014_create_playlists;
mod m20260320_000015_add_subsonic_password;
pub struct Migrator; pub struct Migrator;
@@ -33,6 +34,7 @@ impl MigratorTrait for Migrator {
Box::new(m20260319_000012_add_user_id_to_wanted_items::Migration), Box::new(m20260319_000012_add_user_id_to_wanted_items::Migration),
Box::new(m20260320_000013_add_artist_monitoring::Migration), Box::new(m20260320_000013_add_artist_monitoring::Migration),
Box::new(m20260320_000014_create_playlists::Migration), Box::new(m20260320_000014_create_playlists::Migration),
Box::new(m20260320_000015_add_subsonic_password::Migration),
] ]
} }
} }

View File

@@ -103,3 +103,76 @@ pub async fn delete(db: &DatabaseConnection, id: i32) -> DbResult<()> {
Playlists::delete_by_id(id).exec(db).await?; Playlists::delete_by_id(id).exec(db).await?;
Ok(()) Ok(())
} }
pub async fn add_track(db: &DatabaseConnection, playlist_id: i32, track_id: i32) -> DbResult<()> {
// Get max position for this playlist
let max_pos = PlaylistTracks::find()
.filter(playlist_track::Column::PlaylistId.eq(playlist_id))
.order_by_desc(playlist_track::Column::Position)
.one(db)
.await?
.map(|pt| pt.position)
.unwrap_or(-1);
let pt = TrackActiveModel {
playlist_id: Set(playlist_id),
track_id: Set(track_id),
position: Set(max_pos + 1),
..Default::default()
};
pt.insert(db).await?;
Ok(())
}
pub async fn remove_track(
db: &DatabaseConnection,
playlist_id: i32,
track_id: i32,
) -> DbResult<()> {
// Delete the playlist_track row
PlaylistTracks::delete_many()
.filter(playlist_track::Column::PlaylistId.eq(playlist_id))
.filter(playlist_track::Column::TrackId.eq(track_id))
.exec(db)
.await?;
// Renumber remaining positions
let remaining = PlaylistTracks::find()
.filter(playlist_track::Column::PlaylistId.eq(playlist_id))
.order_by_asc(playlist_track::Column::Position)
.all(db)
.await?;
for (i, pt) in remaining.into_iter().enumerate() {
let mut active: TrackActiveModel = pt.into();
active.position = Set(i as i32);
active.update(db).await?;
}
Ok(())
}
pub async fn reorder_tracks(
db: &DatabaseConnection,
playlist_id: i32,
track_ids: Vec<i32>,
) -> DbResult<()> {
// Delete all playlist_tracks for this playlist
PlaylistTracks::delete_many()
.filter(playlist_track::Column::PlaylistId.eq(playlist_id))
.exec(db)
.await?;
// Re-insert with positions matching the vec order
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(())
}

View File

@@ -52,6 +52,19 @@ pub async fn count(db: &DatabaseConnection) -> DbResult<u64> {
Ok(Users::find().count(db).await?) Ok(Users::find().count(db).await?)
} }
/// Set the Subsonic password for a user. Stored as plaintext per the Subsonic protocol.
pub async fn set_subsonic_password(
db: &DatabaseConnection,
user_id: i32,
password: &str,
) -> DbResult<User> {
let existing = get_by_id(db, user_id).await?;
let mut active: ActiveModel = existing.into();
active.subsonic_password = Set(Some(password.to_string()));
active.updated_at = Set(Utc::now().naive_utc());
Ok(active.update(db).await?)
}
/// Assign all orphaned wanted items (user_id IS NULL) to the given user. /// Assign all orphaned wanted items (user_id IS NULL) to the given user.
pub async fn adopt_orphaned_wanted_items(db: &DatabaseConnection, user_id: i32) -> DbResult<u64> { pub async fn adopt_orphaned_wanted_items(db: &DatabaseConnection, user_id: i32) -> DbResult<u64> {
use crate::entities::wanted_item; use crate::entities::wanted_item;