feat!: playlists
This commit is contained in:
@@ -6,6 +6,11 @@ package library;
|
|||||||
|
|
||||||
service Library {
|
service Library {
|
||||||
rpc ListTracks(google.protobuf.Empty) returns (TrackList);
|
rpc ListTracks(google.protobuf.Empty) returns (TrackList);
|
||||||
|
rpc ListPlaylists(google.protobuf.Empty) returns (ListPlaylistsResponse);
|
||||||
|
rpc CreatePlaylist(CreatePlaylistRequest) returns (CreatePlaylistResponse);
|
||||||
|
rpc DeletePlaylist(DeletePlaylistRequest) returns (google.protobuf.Empty);
|
||||||
|
rpc AddTrackToPlaylist(AddTrackToPlaylistRequest) returns (google.protobuf.Empty);
|
||||||
|
rpc RemoveTrackFromPlaylist(RemoveTrackFromPlaylistRequest) returns (google.protobuf.Empty);
|
||||||
}
|
}
|
||||||
|
|
||||||
message TrackList {
|
message TrackList {
|
||||||
@@ -19,3 +24,35 @@ message Track {
|
|||||||
uint64 artist_id = 4;
|
uint64 artist_id = 4;
|
||||||
uint64 duration = 5;
|
uint64 duration = 5;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
message Playlist {
|
||||||
|
uint32 id = 1;
|
||||||
|
string name = 2;
|
||||||
|
repeated Track tracks = 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
message ListPlaylistsResponse {
|
||||||
|
repeated Playlist playlists = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message CreatePlaylistRequest {
|
||||||
|
string name = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message CreatePlaylistResponse {
|
||||||
|
Playlist playlist = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message DeletePlaylistRequest {
|
||||||
|
uint32 id = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message AddTrackToPlaylistRequest {
|
||||||
|
uint32 playlist_id = 1;
|
||||||
|
string track_hash = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
message RemoveTrackFromPlaylistRequest {
|
||||||
|
uint32 playlist_id = 1;
|
||||||
|
uint32 track_rank = 2;
|
||||||
|
}
|
||||||
|
|||||||
@@ -15,6 +15,8 @@ service Player {
|
|||||||
rpc SetVolume(SetVolumeRequest) returns (SetVolumeResponse);
|
rpc SetVolume(SetVolumeRequest) returns (SetVolumeResponse);
|
||||||
rpc PlayTrackNext(TrackRequest) returns (Queue);
|
rpc PlayTrackNext(TrackRequest) returns (Queue);
|
||||||
rpc AddTrackToQueue(TrackRequest) returns (Queue);
|
rpc AddTrackToQueue(TrackRequest) returns (Queue);
|
||||||
|
rpc AddTracksToQueue(TracksRequest) returns (Queue);
|
||||||
|
rpc PlayPlaylist(PlayPlaylistRequest) returns (PlayerStatus);
|
||||||
rpc SwapQueueIndices(SwapQueueIndicesRequest) returns (Queue);
|
rpc SwapQueueIndices(SwapQueueIndicesRequest) returns (Queue);
|
||||||
rpc SkipTrack(google.protobuf.Empty) returns (PlayerStatus);
|
rpc SkipTrack(google.protobuf.Empty) returns (PlayerStatus);
|
||||||
rpc SkipToQueueIndex(SkipToQueueIndexRequest) returns (PlayerStatus);
|
rpc SkipToQueueIndex(SkipToQueueIndexRequest) returns (PlayerStatus);
|
||||||
@@ -69,3 +71,11 @@ message SwapQueueIndicesRequest {
|
|||||||
uint32 a = 1;
|
uint32 a = 1;
|
||||||
uint32 b = 2;
|
uint32 b = 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
message TracksRequest {
|
||||||
|
repeated string tracks = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message PlayPlaylistRequest {
|
||||||
|
uint32 id = 1;
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
use std::{fs, path::Path};
|
|
||||||
use image::EncodableLayout;
|
|
||||||
use webp::Encoder;
|
|
||||||
use axum::Router;
|
use axum::Router;
|
||||||
|
use image::EncodableLayout;
|
||||||
|
use std::{fs, path::Path};
|
||||||
use tower_http::services::ServeDir;
|
use tower_http::services::ServeDir;
|
||||||
|
use webp::Encoder;
|
||||||
|
|
||||||
use crate::music::metadata::CoverData;
|
use crate::music::metadata::CoverData;
|
||||||
|
|
||||||
@@ -40,7 +40,11 @@ pub fn get_all_cover_hashes() -> Vec<String> {
|
|||||||
hashes
|
hashes
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn write_cover(hash: &str, cover: &CoverData, base_path: &str) -> Result<(), Box<dyn std::error::Error>> {
|
pub fn write_cover(
|
||||||
|
hash: &str,
|
||||||
|
cover: &CoverData,
|
||||||
|
base_path: &str,
|
||||||
|
) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
if cover.mime_type != "image/jpeg"
|
if cover.mime_type != "image/jpeg"
|
||||||
&& cover.mime_type != "image/png"
|
&& cover.mime_type != "image/png"
|
||||||
&& cover.mime_type != "image/webp"
|
&& cover.mime_type != "image/webp"
|
||||||
|
|||||||
@@ -14,9 +14,7 @@ fn map_artist(row: &Row) -> Result<Artist, rusqlite::Error> {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn get_artists(
|
pub async fn get_artists(pool: &Pool) -> Result<Vec<Artist>, rusqlite::Error> {
|
||||||
pool: &Pool,
|
|
||||||
) -> Result<Vec<Artist>, rusqlite::Error> {
|
|
||||||
let manager = pool.get().await.unwrap();
|
let manager = pool.get().await.unwrap();
|
||||||
let connection = manager.lock().unwrap();
|
let connection = manager.lock().unwrap();
|
||||||
|
|
||||||
|
|||||||
@@ -10,14 +10,14 @@ pub fn establish_connection() -> Pool {
|
|||||||
let database_url = std::env::var("DATABASE_URL").expect("DATABASE_URL must be set");
|
let database_url = std::env::var("DATABASE_URL").expect("DATABASE_URL must be set");
|
||||||
|
|
||||||
let cfg = Config::new(database_url);
|
let cfg = Config::new(database_url);
|
||||||
let pool = cfg.create_pool(deadpool::Runtime::Tokio1).expect("Error creating SQLite pool");
|
let pool = cfg
|
||||||
|
.create_pool(deadpool::Runtime::Tokio1)
|
||||||
|
.expect("Error creating SQLite pool");
|
||||||
|
|
||||||
pool
|
pool
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn initialize_database(
|
pub async fn initialize_database(pool: &Pool) -> Result<(), rusqlite::Error> {
|
||||||
pool: &Pool,
|
|
||||||
) -> Result<(), rusqlite::Error> {
|
|
||||||
let manager = pool.get().await.unwrap();
|
let manager = pool.get().await.unwrap();
|
||||||
let conn = manager.lock().unwrap();
|
let conn = manager.lock().unwrap();
|
||||||
|
|
||||||
@@ -65,5 +65,29 @@ pub async fn initialize_database(
|
|||||||
[],
|
[],
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
|
conn.execute(
|
||||||
|
"
|
||||||
|
CREATE TABLE IF NOT EXISTS playlists (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
|
||||||
|
name TEXT NOT NULL
|
||||||
|
);
|
||||||
|
",
|
||||||
|
[],
|
||||||
|
)?;
|
||||||
|
|
||||||
|
conn.execute(
|
||||||
|
"
|
||||||
|
CREATE TABLE IF NOT EXISTS playlist_tracks (
|
||||||
|
playlist_id INTEGER NOT NULL,
|
||||||
|
track_hash TEXT NOT NULL,
|
||||||
|
rank INTEGER NOT NULL,
|
||||||
|
FOREIGN KEY (playlist_id) REFERENCES playlists (id) ON DELETE CASCADE,
|
||||||
|
FOREIGN KEY (track_hash) REFERENCES tracks (hash) ON DELETE CASCADE,
|
||||||
|
UNIQUE (playlist_id, rank) ON CONFLICT FAIL
|
||||||
|
);
|
||||||
|
",
|
||||||
|
[],
|
||||||
|
)?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,9 +16,7 @@ fn map_library_path(row: &Row) -> Result<LibraryPath, rusqlite::Error> {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn get_library_paths(
|
pub async fn get_library_paths(pool: &Pool) -> Result<Vec<LibraryPath>, rusqlite::Error> {
|
||||||
pool: &Pool,
|
|
||||||
) -> Result<Vec<LibraryPath>, rusqlite::Error> {
|
|
||||||
let manager = pool.get().await.unwrap();
|
let manager = pool.get().await.unwrap();
|
||||||
let connection = manager.lock().unwrap();
|
let connection = manager.lock().unwrap();
|
||||||
|
|
||||||
@@ -36,10 +34,7 @@ pub async fn get_library_paths(
|
|||||||
Ok(paths)
|
Ok(paths)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn get_library_path(
|
pub async fn get_library_path(pool: &Pool, id: u64) -> Result<LibraryPath, rusqlite::Error> {
|
||||||
pool: &Pool,
|
|
||||||
id: u64,
|
|
||||||
) -> Result<LibraryPath, rusqlite::Error> {
|
|
||||||
let manager = pool.get().await.unwrap();
|
let manager = pool.get().await.unwrap();
|
||||||
let connection = manager.lock().unwrap();
|
let connection = manager.lock().unwrap();
|
||||||
|
|
||||||
@@ -62,10 +57,7 @@ pub async fn insert_library_path(
|
|||||||
Ok(result > 0)
|
Ok(result > 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn delete_library_path(
|
pub async fn delete_library_path(pool: &Pool, path_id: u64) -> Result<bool, rusqlite::Error> {
|
||||||
pool: &Pool,
|
|
||||||
path_id: u64,
|
|
||||||
) -> Result<bool, rusqlite::Error> {
|
|
||||||
let manager = pool.get().await.unwrap();
|
let manager = pool.get().await.unwrap();
|
||||||
let connection = manager.lock().unwrap();
|
let connection = manager.lock().unwrap();
|
||||||
|
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ use rusqlite::{params, Row};
|
|||||||
use crate::{
|
use crate::{
|
||||||
covers::{get_all_cover_hashes, get_cover_base_path, write_cover},
|
covers::{get_all_cover_hashes, get_cover_base_path, write_cover},
|
||||||
music::metadata::TrackMetadata,
|
music::metadata::TrackMetadata,
|
||||||
proto,
|
proto::{self, library::Playlist},
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::artists::get_artists;
|
use super::artists::get_artists;
|
||||||
@@ -57,9 +57,17 @@ fn map_track(row: &Row) -> Result<Track, rusqlite::Error> {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn get_tracks(
|
/// Creates a playlist from a database row (tracks are empty and need to be filled afterwards)
|
||||||
pool: &Pool,
|
/// * `row`: The database row
|
||||||
) -> Result<Vec<Track>, rusqlite::Error> {
|
fn map_playlist(row: &Row) -> Result<Playlist, rusqlite::Error> {
|
||||||
|
Ok(Playlist {
|
||||||
|
id: row.get(0)?,
|
||||||
|
name: row.get(1)?,
|
||||||
|
tracks: Vec::new(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get_tracks(pool: &Pool) -> Result<Vec<Track>, rusqlite::Error> {
|
||||||
let manager = pool.get().await.unwrap();
|
let manager = pool.get().await.unwrap();
|
||||||
let connection = manager.lock().unwrap();
|
let connection = manager.lock().unwrap();
|
||||||
|
|
||||||
@@ -77,20 +85,31 @@ pub async fn get_tracks(
|
|||||||
Ok(tracks)
|
Ok(tracks)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn get_track(
|
pub async fn get_track(pool: &Pool, hash: &str) -> Result<Track, rusqlite::Error> {
|
||||||
pool: &Pool,
|
|
||||||
hash: &str,
|
|
||||||
) -> Result<Track, rusqlite::Error> {
|
|
||||||
let manager = pool.get().await.unwrap();
|
let manager = pool.get().await.unwrap();
|
||||||
let connection = &manager.lock().unwrap();
|
let connection = &manager.lock().unwrap();
|
||||||
|
|
||||||
connection.query_row("SELECT t.hash, t.name, t.duration, t.artist_id, a.name AS artist_name FROM tracks t INNER JOIN artists a ON a.id = t.artist_id WHERE t.hash = ?1", [hash], map_track)
|
connection.query_row("SELECT t.hash, t.name, t.duration, t.artist_id, a.name AS artist_name FROM tracks t INNER JOIN artists a ON a.id = t.artist_id WHERE t.hash = ?1", [hash], map_track)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn get_track_full_path(
|
pub async fn get_specific_tracks(
|
||||||
pool: &Pool,
|
pool: &Pool,
|
||||||
hash: &str,
|
hashes: &Vec<String>,
|
||||||
) -> Result<PathBuf, rusqlite::Error> {
|
) -> Result<Vec<Track>, rusqlite::Error> {
|
||||||
|
let manager = pool.get().await.unwrap();
|
||||||
|
let connection = &manager.lock().unwrap();
|
||||||
|
|
||||||
|
let mut statement = connection.prepare("SELECT t.hash, t.name, t.duration, t.artist_id, a.name AS artist_name FROM tracks t INNER JOIN artists a ON a.id = t.artist_id WHERE t.hash = ?1")?;
|
||||||
|
let mut tracks: Vec<Track> = Vec::new();
|
||||||
|
|
||||||
|
for hash in hashes {
|
||||||
|
tracks.push(statement.query_row([hash], map_track)?);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(tracks)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get_track_full_path(pool: &Pool, hash: &str) -> Result<PathBuf, rusqlite::Error> {
|
||||||
let manager = pool.get().await.unwrap();
|
let manager = pool.get().await.unwrap();
|
||||||
let connection = &manager.lock().unwrap();
|
let connection = &manager.lock().unwrap();
|
||||||
|
|
||||||
@@ -106,6 +125,35 @@ pub async fn get_track_full_path(
|
|||||||
Ok(path)
|
Ok(path)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn get_specific_tracks_full_path(
|
||||||
|
pool: &Pool,
|
||||||
|
hashes: &Vec<String>,
|
||||||
|
) -> Result<Vec<PathBuf>, rusqlite::Error> {
|
||||||
|
let manager = pool.get().await.unwrap();
|
||||||
|
let connection = &manager.lock().unwrap();
|
||||||
|
|
||||||
|
let mut paths: Vec<PathBuf> = Vec::new();
|
||||||
|
|
||||||
|
let home = home::home_dir().unwrap();
|
||||||
|
let home_dir = home.to_str().unwrap();
|
||||||
|
|
||||||
|
let mut statement = connection.prepare(
|
||||||
|
"SELECT t.path, l.path AS library_root FROM tracks t INNER JOIN library_paths l ON t.library_path_id = l.id WHERE t.hash = ?1",
|
||||||
|
)?;
|
||||||
|
|
||||||
|
for hash in hashes {
|
||||||
|
let (relative_path, library_path): (String, String) =
|
||||||
|
statement.query_row([hash], |r| Ok((r.get(0)?, r.get(1)?)))?;
|
||||||
|
|
||||||
|
let library_root = library_path.replace("~", home_dir);
|
||||||
|
let path = Path::new(&library_root).join(&relative_path);
|
||||||
|
|
||||||
|
paths.push(path);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(paths)
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn insert_tracks(
|
pub async fn insert_tracks(
|
||||||
pool: &Pool,
|
pool: &Pool,
|
||||||
tracks: HashMap<String, TrackMetadata>,
|
tracks: HashMap<String, TrackMetadata>,
|
||||||
@@ -190,3 +238,105 @@ pub async fn insert_tracks(
|
|||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn get_playlists(pool: &Pool) -> Result<Vec<Playlist>, rusqlite::Error> {
|
||||||
|
let manager = pool.get().await.unwrap();
|
||||||
|
let connection = manager.lock().unwrap();
|
||||||
|
|
||||||
|
let mut statement = connection.prepare("SELECT id, name FROM playlists")?;
|
||||||
|
let rows = statement.query_map([], map_playlist)?;
|
||||||
|
|
||||||
|
let mut playlists: Vec<Playlist> = Vec::new();
|
||||||
|
|
||||||
|
let mut tracks_statement = connection.prepare("
|
||||||
|
SELECT t.hash, t.name, t.duration, t.artist_id, a.name FROM playlist_tracks pt INNER JOIN tracks t ON pt.track_hash = t.hash INNER JOIN artists a ON t.artist_id = a.id WHERE pt.playlist_id = ?1
|
||||||
|
")?;
|
||||||
|
|
||||||
|
for row in rows {
|
||||||
|
if let Ok(mut row) = row {
|
||||||
|
let track_rows: Vec<proto::library::Track> = tracks_statement
|
||||||
|
.query_map([row.id], map_track)?
|
||||||
|
.filter_map(|t| t.ok().map(|t| t.into()))
|
||||||
|
.collect();
|
||||||
|
row.tracks = track_rows;
|
||||||
|
|
||||||
|
playlists.push(row);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(playlists)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get_playlist(pool: &Pool, id: u32) -> Result<Playlist, rusqlite::Error> {
|
||||||
|
let manager = pool.get().await.unwrap();
|
||||||
|
let connection = manager.lock().unwrap();
|
||||||
|
|
||||||
|
let mut playlist = connection.query_row(
|
||||||
|
"SELECT id, name FROM playlists WHERE id = ?1",
|
||||||
|
[id],
|
||||||
|
map_playlist,
|
||||||
|
)?;
|
||||||
|
|
||||||
|
let mut tracks_statement = connection.prepare("
|
||||||
|
SELECT t.hash, t.name, t.duration, t.artist_id, a.name FROM playlist_tracks pt INNER JOIN tracks t ON pt.track_hash = t.hash INNER JOIN artists a ON t.artist_id = a.id WHERE pt.playlist_id = ?1
|
||||||
|
")?;
|
||||||
|
|
||||||
|
let track_rows: Vec<proto::library::Track> = tracks_statement
|
||||||
|
.query_map([id], map_track)?
|
||||||
|
.filter_map(|t| t.ok().map(|t| t.into()))
|
||||||
|
.collect();
|
||||||
|
playlist.tracks = track_rows;
|
||||||
|
|
||||||
|
Ok(playlist)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn create_playlist(pool: &Pool, name: &str) -> Result<Playlist, rusqlite::Error> {
|
||||||
|
let manager = pool.get().await.unwrap();
|
||||||
|
let connection = manager.lock().unwrap();
|
||||||
|
|
||||||
|
let mut statement =
|
||||||
|
connection.prepare("INSERT INTO playlists (name) VALUES (?1) RETURNING id, name")?;
|
||||||
|
let playlist = statement.query_row([name], map_playlist)?;
|
||||||
|
|
||||||
|
Ok(playlist)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn delete_playlist(pool: &Pool, id: u32) -> Result<bool, rusqlite::Error> {
|
||||||
|
let manager = pool.get().await.unwrap();
|
||||||
|
let connection = manager.lock().unwrap();
|
||||||
|
|
||||||
|
let mut statement = connection.prepare("DELETE FROM playlists WHERE id = ?1")?;
|
||||||
|
let len = statement.execute([id])?;
|
||||||
|
|
||||||
|
Ok(len == 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn add_track_to_playlist(
|
||||||
|
pool: &Pool,
|
||||||
|
playlist_id: u32,
|
||||||
|
track_hash: &str,
|
||||||
|
) -> Result<bool, rusqlite::Error> {
|
||||||
|
let manager = pool.get().await.unwrap();
|
||||||
|
let connection = manager.lock().unwrap();
|
||||||
|
|
||||||
|
let mut statement = connection
|
||||||
|
.prepare("INSERT INTO playlist_tracks (playlist_id, track_hash, rank) VALUES (?1, ?2, (SELECT COALESCE(MAX(rank) + 1, 0) FROM playlist_tracks WHERE playlist_id = ?1))")?;
|
||||||
|
let len = statement.execute(params![playlist_id, track_hash])?;
|
||||||
|
|
||||||
|
Ok(len == 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn remove_track_from_playlist(
|
||||||
|
pool: &Pool,
|
||||||
|
playlist_id: u32,
|
||||||
|
track_rank: u32,
|
||||||
|
) -> Result<bool, rusqlite::Error> {
|
||||||
|
let manager = pool.get().await.unwrap();
|
||||||
|
let connection = manager.lock().unwrap();
|
||||||
|
|
||||||
|
let mut statement = connection
|
||||||
|
.prepare("DELETE FROM playlist_tracks WHERE playlist_id = ?1 AND track_rank = ?2")?;
|
||||||
|
let len = statement.execute(params![playlist_id, track_rank])?;
|
||||||
|
|
||||||
|
Ok(len == 1)
|
||||||
|
}
|
||||||
|
|||||||
114
src/library.rs
114
src/library.rs
@@ -1,14 +1,22 @@
|
|||||||
use std::{collections::HashMap, path::PathBuf, sync::Arc, time::Instant};
|
|
||||||
use deadpool_sqlite::Pool;
|
use deadpool_sqlite::Pool;
|
||||||
|
use std::{collections::HashMap, path::PathBuf, sync::Arc, time::Instant};
|
||||||
use tokio::{sync::Mutex, task::JoinSet};
|
use tokio::{sync::Mutex, task::JoinSet};
|
||||||
|
|
||||||
|
use tonic::{Request, Response, Status};
|
||||||
use walkdir::{DirEntry, WalkDir};
|
use walkdir::{DirEntry, WalkDir};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
checksum::generate_hash,
|
checksum::generate_hash,
|
||||||
database::tracks::{get_tracks, insert_tracks},
|
database::tracks::{
|
||||||
|
add_track_to_playlist, create_playlist, delete_playlist, get_playlists, get_tracks,
|
||||||
|
insert_tracks, remove_track_from_playlist,
|
||||||
|
},
|
||||||
music::metadata::{extract_track_data, TrackMetadata},
|
music::metadata::{extract_track_data, TrackMetadata},
|
||||||
proto::library::{library_server::Library, Track, TrackList},
|
proto::library::{
|
||||||
|
library_server::Library, AddTrackToPlaylistRequest, CreatePlaylistRequest,
|
||||||
|
CreatePlaylistResponse, DeletePlaylistRequest, ListPlaylistsResponse,
|
||||||
|
RemoveTrackFromPlaylistRequest, Track, TrackList,
|
||||||
|
},
|
||||||
state::GrooveState,
|
state::GrooveState,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -26,12 +34,9 @@ impl LibraryService {
|
|||||||
|
|
||||||
#[tonic::async_trait]
|
#[tonic::async_trait]
|
||||||
impl Library for LibraryService {
|
impl Library for LibraryService {
|
||||||
async fn list_tracks(
|
async fn list_tracks(&self, _request: Request<()>) -> Result<Response<TrackList>, Status> {
|
||||||
&self,
|
|
||||||
_request: tonic::Request<()>,
|
|
||||||
) -> Result<tonic::Response<TrackList>, tonic::Status> {
|
|
||||||
let Ok(tracks) = get_tracks(&self.pool).await else {
|
let Ok(tracks) = get_tracks(&self.pool).await else {
|
||||||
return Err(tonic::Status::internal(""));
|
return Err(Status::internal(""));
|
||||||
};
|
};
|
||||||
|
|
||||||
let response = TrackList {
|
let response = TrackList {
|
||||||
@@ -47,15 +52,96 @@ impl Library for LibraryService {
|
|||||||
.collect::<Vec<Track>>(),
|
.collect::<Vec<Track>>(),
|
||||||
};
|
};
|
||||||
|
|
||||||
Ok(tonic::Response::new(response))
|
Ok(Response::new(response))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn list_playlists(
|
||||||
|
&self,
|
||||||
|
_request: Request<()>,
|
||||||
|
) -> Result<Response<ListPlaylistsResponse>, Status> {
|
||||||
|
let Ok(playlists) = get_playlists(&self.pool).await else {
|
||||||
|
return Err(Status::internal(""));
|
||||||
|
};
|
||||||
|
|
||||||
|
let response = ListPlaylistsResponse { playlists };
|
||||||
|
|
||||||
|
Ok(Response::new(response))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn create_playlist(
|
||||||
|
&self,
|
||||||
|
request: Request<CreatePlaylistRequest>,
|
||||||
|
) -> Result<Response<CreatePlaylistResponse>, Status> {
|
||||||
|
let input = request.get_ref();
|
||||||
|
|
||||||
|
let Ok(playlist) = create_playlist(&self.pool, &input.name).await else {
|
||||||
|
return Err(Status::internal(""));
|
||||||
|
};
|
||||||
|
|
||||||
|
let response = CreatePlaylistResponse {
|
||||||
|
playlist: Some(playlist),
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(Response::new(response))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn delete_playlist(
|
||||||
|
&self,
|
||||||
|
request: Request<DeletePlaylistRequest>,
|
||||||
|
) -> Result<Response<()>, Status> {
|
||||||
|
let input = request.get_ref();
|
||||||
|
|
||||||
|
let Ok(success) = delete_playlist(&self.pool, input.id).await else {
|
||||||
|
return Err(Status::internal(""));
|
||||||
|
};
|
||||||
|
|
||||||
|
if !success {
|
||||||
|
return Err(Status::internal(""));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(Response::new(()))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn add_track_to_playlist(
|
||||||
|
&self,
|
||||||
|
request: Request<AddTrackToPlaylistRequest>,
|
||||||
|
) -> Result<Response<()>, Status> {
|
||||||
|
let input = request.get_ref();
|
||||||
|
|
||||||
|
let Ok(success) =
|
||||||
|
add_track_to_playlist(&self.pool, input.playlist_id, &input.track_hash).await
|
||||||
|
else {
|
||||||
|
return Err(Status::internal(""));
|
||||||
|
};
|
||||||
|
|
||||||
|
if !success {
|
||||||
|
return Err(Status::internal(""));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(Response::new(()))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn remove_track_from_playlist(
|
||||||
|
&self,
|
||||||
|
request: Request<RemoveTrackFromPlaylistRequest>,
|
||||||
|
) -> Result<Response<()>, Status> {
|
||||||
|
let input = request.get_ref();
|
||||||
|
|
||||||
|
let Ok(success) =
|
||||||
|
remove_track_from_playlist(&self.pool, input.playlist_id, input.track_rank).await
|
||||||
|
else {
|
||||||
|
return Err(Status::internal(""));
|
||||||
|
};
|
||||||
|
|
||||||
|
if !success {
|
||||||
|
return Err(Status::internal(""));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(Response::new(()))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn index_path(
|
pub async fn index_path(path: PathBuf, db: &Pool, path_id: u64) -> Result<(), rusqlite::Error> {
|
||||||
path: PathBuf,
|
|
||||||
db: &Pool,
|
|
||||||
path_id: u64,
|
|
||||||
) -> Result<(), rusqlite::Error> {
|
|
||||||
let home = home::home_dir().unwrap();
|
let home = home::home_dir().unwrap();
|
||||||
|
|
||||||
let correct_path = path.to_str().unwrap().replace("~", home.to_str().unwrap());
|
let correct_path = path.to_str().unwrap().replace("~", home.to_str().unwrap());
|
||||||
|
|||||||
@@ -21,8 +21,8 @@ pub mod player;
|
|||||||
pub mod settings;
|
pub mod settings;
|
||||||
pub mod state;
|
pub mod state;
|
||||||
|
|
||||||
use settings::SettingsService;
|
|
||||||
use music::player::start_watching;
|
use music::player::start_watching;
|
||||||
|
use settings::SettingsService;
|
||||||
|
|
||||||
pub mod proto {
|
pub mod proto {
|
||||||
pub mod settings {
|
pub mod settings {
|
||||||
@@ -40,7 +40,9 @@ pub mod proto {
|
|||||||
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
let pool = &mut establish_connection();
|
let pool = &mut establish_connection();
|
||||||
|
|
||||||
initialize_database(&pool).await.expect("Error initializing database");
|
initialize_database(&pool)
|
||||||
|
.await
|
||||||
|
.expect("Error initializing database");
|
||||||
|
|
||||||
let address = "[::1]:39993".parse()?;
|
let address = "[::1]:39993".parse()?;
|
||||||
|
|
||||||
@@ -58,8 +60,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
let mut player = player.lock().await;
|
let mut player = player.lock().await;
|
||||||
match next {
|
match next {
|
||||||
Some(queued_track) => {
|
Some(queued_track) => {
|
||||||
let _ = player
|
let _ = player.play_track(queued_track.track, queued_track.path, false);
|
||||||
.play_track(queued_track.track, queued_track.path, false);
|
|
||||||
}
|
}
|
||||||
None => {
|
None => {
|
||||||
player.clear();
|
player.clear();
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ use std::{
|
|||||||
collections::VecDeque,
|
collections::VecDeque,
|
||||||
fs,
|
fs,
|
||||||
io::{BufReader, Cursor},
|
io::{BufReader, Cursor},
|
||||||
path::Path,
|
path::{Path, PathBuf},
|
||||||
sync::Arc,
|
sync::Arc,
|
||||||
time::Duration,
|
time::Duration,
|
||||||
};
|
};
|
||||||
@@ -97,6 +97,30 @@ impl AudioPlayer {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn add_tracks_to_queue(
|
||||||
|
&mut self,
|
||||||
|
tracks: Vec<(Track, PathBuf)>,
|
||||||
|
clear_queue: bool,
|
||||||
|
) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
if clear_queue {
|
||||||
|
self.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
for (track, path) in tracks {
|
||||||
|
self.queue.push_back(QueuedTrack { track, path });
|
||||||
|
}
|
||||||
|
|
||||||
|
if !self.sink.empty() || self.queue.is_empty() {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(queued_track) = self.queue.pop_front() {
|
||||||
|
self.play_track(queued_track.track, queued_track.path, false)?;
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
pub fn skip_track(&mut self) -> Result<(), Box<dyn std::error::Error>> {
|
pub fn skip_track(&mut self) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
let Some(queued_track) = self.queue.pop_front() else {
|
let Some(queued_track) = self.queue.pop_front() else {
|
||||||
self.clear();
|
self.clear();
|
||||||
|
|||||||
@@ -1,17 +1,21 @@
|
|||||||
use deadpool_sqlite::Pool;
|
use deadpool_sqlite::Pool;
|
||||||
use std::{pin::Pin, time::Duration};
|
use std::{path::PathBuf, pin::Pin, time::Duration};
|
||||||
use tokio::sync::mpsc;
|
use tokio::sync::mpsc;
|
||||||
use tokio_stream::{wrappers::ReceiverStream, Stream};
|
use tokio_stream::{wrappers::ReceiverStream, Stream};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
database::tracks::{get_track, get_track_full_path},
|
database::tracks::{
|
||||||
|
get_playlist, get_specific_tracks, get_specific_tracks_full_path, get_track,
|
||||||
|
get_track_full_path, Track,
|
||||||
|
},
|
||||||
music::queue::queue_to_track_vec,
|
music::queue::queue_to_track_vec,
|
||||||
proto::{
|
proto::{
|
||||||
self,
|
self,
|
||||||
player::{
|
player::{
|
||||||
player_server::Player, PauseState, PlayTrackResponse, PlayerStatus, Queue,
|
player_server::Player, PauseState, PlayPlaylistRequest, PlayTrackResponse,
|
||||||
SeekPositionRequest, SeekPositionResponse, SetVolumeRequest, SetVolumeResponse,
|
PlayerStatus, Queue, SeekPositionRequest, SeekPositionResponse, SetVolumeRequest,
|
||||||
SkipToQueueIndexRequest, SwapQueueIndicesRequest, TrackRequest,
|
SetVolumeResponse, SkipToQueueIndexRequest, SwapQueueIndicesRequest, TrackRequest,
|
||||||
|
TracksRequest,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
state::GrooveState,
|
state::GrooveState,
|
||||||
@@ -198,6 +202,75 @@ impl Player for PlayerService {
|
|||||||
Ok(tonic::Response::new(response))
|
Ok(tonic::Response::new(response))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn add_tracks_to_queue(
|
||||||
|
&self,
|
||||||
|
request: tonic::Request<TracksRequest>,
|
||||||
|
) -> Result<tonic::Response<Queue>, tonic::Status> {
|
||||||
|
let input = request.get_ref();
|
||||||
|
|
||||||
|
let Ok(tracks) = get_specific_tracks(&self.pool, &input.tracks).await else {
|
||||||
|
return Err(tonic::Status::not_found(""));
|
||||||
|
};
|
||||||
|
|
||||||
|
let Ok(track_paths) = get_specific_tracks_full_path(&self.pool, &input.tracks).await else {
|
||||||
|
return Err(tonic::Status::not_found(""));
|
||||||
|
};
|
||||||
|
|
||||||
|
let state = self.state.lock().await;
|
||||||
|
let mut player = state.player.lock().await;
|
||||||
|
|
||||||
|
let tracks_and_paths: Vec<(Track, PathBuf)> =
|
||||||
|
tracks.into_iter().zip(track_paths.into_iter()).collect();
|
||||||
|
|
||||||
|
if let Err(_) = player.add_tracks_to_queue(tracks_and_paths, false).await {
|
||||||
|
return Err(tonic::Status::internal(""));
|
||||||
|
}
|
||||||
|
|
||||||
|
let queue = player.queue();
|
||||||
|
|
||||||
|
let response = Queue {
|
||||||
|
tracks: queue_to_track_vec(queue),
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(tonic::Response::new(response))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn play_playlist(
|
||||||
|
&self,
|
||||||
|
request: tonic::Request<PlayPlaylistRequest>,
|
||||||
|
) -> Result<tonic::Response<PlayerStatus>, tonic::Status> {
|
||||||
|
let input = request.get_ref();
|
||||||
|
|
||||||
|
let Ok(playlist) = get_playlist(&self.pool, input.id).await else {
|
||||||
|
return Err(tonic::Status::not_found(""));
|
||||||
|
};
|
||||||
|
|
||||||
|
let Ok(track_paths) = get_specific_tracks_full_path(
|
||||||
|
&self.pool,
|
||||||
|
&playlist.tracks.iter().map(|t| t.hash.clone()).collect(),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
else {
|
||||||
|
return Err(tonic::Status::not_found(""));
|
||||||
|
};
|
||||||
|
|
||||||
|
let state = self.state.lock().await;
|
||||||
|
let mut player = state.player.lock().await;
|
||||||
|
|
||||||
|
let tracks = playlist.tracks.iter().map(|t| t.clone().into());
|
||||||
|
|
||||||
|
let tracks_and_paths: Vec<(Track, PathBuf)> =
|
||||||
|
tracks.into_iter().zip(track_paths.into_iter()).collect();
|
||||||
|
|
||||||
|
if let Err(_) = player.add_tracks_to_queue(tracks_and_paths, true).await {
|
||||||
|
return Err(tonic::Status::internal(""));
|
||||||
|
}
|
||||||
|
|
||||||
|
let response = player.get_snapshot().into();
|
||||||
|
|
||||||
|
Ok(tonic::Response::new(response))
|
||||||
|
}
|
||||||
|
|
||||||
async fn play_track_next(
|
async fn play_track_next(
|
||||||
&self,
|
&self,
|
||||||
request: tonic::Request<TrackRequest>,
|
request: tonic::Request<TrackRequest>,
|
||||||
|
|||||||
Reference in New Issue
Block a user