* [meta] Add ignore paths to vscode settings * [feature] Add few more fields to song information Fields include lyricist, composer, genre, category and label.
247 lines
5.5 KiB
Rust
247 lines
5.5 KiB
Rust
use anyhow::Result;
|
|
use core::clone::Clone;
|
|
use diesel;
|
|
use diesel::prelude::*;
|
|
use diesel::sql_types;
|
|
use diesel::BelongingToDsl;
|
|
use std::path::Path;
|
|
|
|
use super::*;
|
|
use crate::app::index::Song;
|
|
use crate::app::vfs;
|
|
use crate::db::{playlist_songs, playlists, users, DB};
|
|
|
|
#[derive(Clone)]
|
|
pub struct Manager {
|
|
db: DB,
|
|
vfs_manager: vfs::Manager,
|
|
}
|
|
|
|
impl Manager {
|
|
pub fn new(db: DB, vfs_manager: vfs::Manager) -> Self {
|
|
Self { db, vfs_manager }
|
|
}
|
|
|
|
pub fn list_playlists(&self, owner: &str) -> Result<Vec<String>, Error> {
|
|
let connection = self.db.connect()?;
|
|
|
|
let user: User = {
|
|
use self::users::dsl::*;
|
|
users
|
|
.filter(name.eq(owner))
|
|
.select((id,))
|
|
.first(&connection)
|
|
.optional()
|
|
.map_err(anyhow::Error::new)?
|
|
.ok_or(Error::UserNotFound)?
|
|
};
|
|
|
|
{
|
|
use self::playlists::dsl::*;
|
|
let found_playlists: Vec<String> = Playlist::belonging_to(&user)
|
|
.select(name)
|
|
.load(&connection)
|
|
.map_err(anyhow::Error::new)?;
|
|
Ok(found_playlists)
|
|
}
|
|
}
|
|
|
|
pub fn save_playlist(
|
|
&self,
|
|
playlist_name: &str,
|
|
owner: &str,
|
|
content: &[String],
|
|
) -> Result<(), Error> {
|
|
let new_playlist: NewPlaylist;
|
|
let playlist: Playlist;
|
|
let vfs = self.vfs_manager.get_vfs()?;
|
|
|
|
{
|
|
let connection = self.db.connect()?;
|
|
|
|
// Find owner
|
|
let user: User = {
|
|
use self::users::dsl::*;
|
|
users
|
|
.filter(name.eq(owner))
|
|
.select((id,))
|
|
.first(&connection)
|
|
.optional()
|
|
.map_err(anyhow::Error::new)?
|
|
.ok_or(Error::UserNotFound)?
|
|
};
|
|
|
|
// Create playlist
|
|
new_playlist = NewPlaylist {
|
|
name: playlist_name.into(),
|
|
owner: user.id,
|
|
};
|
|
|
|
diesel::insert_into(playlists::table)
|
|
.values(&new_playlist)
|
|
.execute(&connection)
|
|
.map_err(anyhow::Error::new)?;
|
|
|
|
playlist = {
|
|
use self::playlists::dsl::*;
|
|
playlists
|
|
.select((id, owner))
|
|
.filter(name.eq(playlist_name).and(owner.eq(user.id)))
|
|
.get_result(&connection)
|
|
.map_err(anyhow::Error::new)?
|
|
}
|
|
}
|
|
|
|
let mut new_songs: Vec<NewPlaylistSong> = Vec::new();
|
|
new_songs.reserve(content.len());
|
|
|
|
for (i, path) in content.iter().enumerate() {
|
|
let virtual_path = Path::new(&path);
|
|
if let Some(real_path) = vfs
|
|
.virtual_to_real(virtual_path)
|
|
.ok()
|
|
.and_then(|p| p.to_str().map(|s| s.to_owned()))
|
|
{
|
|
new_songs.push(NewPlaylistSong {
|
|
playlist: playlist.id,
|
|
path: real_path,
|
|
ordering: i as i32,
|
|
});
|
|
}
|
|
}
|
|
|
|
{
|
|
let connection = self.db.connect()?;
|
|
connection
|
|
.transaction::<_, diesel::result::Error, _>(|| {
|
|
// Delete old content (if any)
|
|
let old_songs = PlaylistSong::belonging_to(&playlist);
|
|
diesel::delete(old_songs).execute(&connection)?;
|
|
|
|
// Insert content
|
|
diesel::insert_into(playlist_songs::table)
|
|
.values(&new_songs)
|
|
.execute(&*connection)?; // TODO https://github.com/diesel-rs/diesel/issues/1822
|
|
Ok(())
|
|
})
|
|
.map_err(anyhow::Error::new)?;
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
pub fn read_playlist(&self, playlist_name: &str, owner: &str) -> Result<Vec<Song>, Error> {
|
|
let vfs = self.vfs_manager.get_vfs()?;
|
|
let songs: Vec<Song>;
|
|
|
|
{
|
|
let connection = self.db.connect()?;
|
|
|
|
// Find owner
|
|
let user: User = {
|
|
use self::users::dsl::*;
|
|
users
|
|
.filter(name.eq(owner))
|
|
.select((id,))
|
|
.first(&connection)
|
|
.optional()
|
|
.map_err(anyhow::Error::new)?
|
|
.ok_or(Error::UserNotFound)?
|
|
};
|
|
|
|
// Find playlist
|
|
let playlist: Playlist = {
|
|
use self::playlists::dsl::*;
|
|
playlists
|
|
.select((id, owner))
|
|
.filter(name.eq(playlist_name).and(owner.eq(user.id)))
|
|
.get_result(&connection)
|
|
.optional()
|
|
.map_err(anyhow::Error::new)?
|
|
.ok_or(Error::PlaylistNotFound)?
|
|
};
|
|
|
|
// Select songs. Not using Diesel because we need to LEFT JOIN using a custom column
|
|
let query = diesel::sql_query(
|
|
r#"
|
|
SELECT s.id, s.path, s.parent, s.track_number, s.disc_number, s.title, s.artist, s.album_artist, s.year, s.album, s.artwork, s.duration, s.lyricist, s.composer, s.genre, s.label
|
|
FROM playlist_songs ps
|
|
LEFT JOIN songs s ON ps.path = s.path
|
|
WHERE ps.playlist = ?
|
|
ORDER BY ps.ordering
|
|
"#,
|
|
);
|
|
let query = query.clone().bind::<sql_types::Integer, _>(playlist.id);
|
|
songs = query.get_results(&connection).map_err(anyhow::Error::new)?;
|
|
}
|
|
|
|
// Map real path to virtual paths
|
|
let virtual_songs = songs
|
|
.into_iter()
|
|
.filter_map(|s| s.virtualize(&vfs))
|
|
.collect();
|
|
|
|
Ok(virtual_songs)
|
|
}
|
|
|
|
pub fn delete_playlist(&self, playlist_name: &str, owner: &str) -> Result<(), Error> {
|
|
let connection = self.db.connect()?;
|
|
|
|
let user: User = {
|
|
use self::users::dsl::*;
|
|
users
|
|
.filter(name.eq(owner))
|
|
.select((id,))
|
|
.first(&connection)
|
|
.optional()
|
|
.map_err(anyhow::Error::new)?
|
|
.ok_or(Error::UserNotFound)?
|
|
};
|
|
|
|
{
|
|
use self::playlists::dsl::*;
|
|
let q = Playlist::belonging_to(&user).filter(name.eq(playlist_name));
|
|
match diesel::delete(q)
|
|
.execute(&connection)
|
|
.map_err(anyhow::Error::new)?
|
|
{
|
|
0 => Err(Error::PlaylistNotFound),
|
|
_ => Ok(()),
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Identifiable, Queryable, Associations)]
|
|
#[belongs_to(User, foreign_key = "owner")]
|
|
struct Playlist {
|
|
id: i32,
|
|
owner: i32,
|
|
}
|
|
|
|
#[derive(Identifiable, Queryable, Associations)]
|
|
#[belongs_to(Playlist, foreign_key = "playlist")]
|
|
struct PlaylistSong {
|
|
id: i32,
|
|
playlist: i32,
|
|
}
|
|
|
|
#[derive(Insertable)]
|
|
#[table_name = "playlists"]
|
|
struct NewPlaylist {
|
|
name: String,
|
|
owner: i32,
|
|
}
|
|
|
|
#[derive(Insertable)]
|
|
#[table_name = "playlist_songs"]
|
|
struct NewPlaylistSong {
|
|
playlist: i32,
|
|
path: String,
|
|
ordering: i32,
|
|
}
|
|
|
|
#[derive(Identifiable, Queryable)]
|
|
struct User {
|
|
id: i32,
|
|
}
|