feat!: playlists
This commit is contained in:
@@ -6,6 +6,11 @@ package library;
|
||||
|
||||
service Library {
|
||||
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 {
|
||||
@@ -19,3 +24,35 @@ message Track {
|
||||
uint64 artist_id = 4;
|
||||
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 PlayTrackNext(TrackRequest) returns (Queue);
|
||||
rpc AddTrackToQueue(TrackRequest) returns (Queue);
|
||||
rpc AddTracksToQueue(TracksRequest) returns (Queue);
|
||||
rpc PlayPlaylist(PlayPlaylistRequest) returns (PlayerStatus);
|
||||
rpc SwapQueueIndices(SwapQueueIndicesRequest) returns (Queue);
|
||||
rpc SkipTrack(google.protobuf.Empty) returns (PlayerStatus);
|
||||
rpc SkipToQueueIndex(SkipToQueueIndexRequest) returns (PlayerStatus);
|
||||
@@ -69,3 +71,11 @@ message SwapQueueIndicesRequest {
|
||||
uint32 a = 1;
|
||||
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 image::EncodableLayout;
|
||||
use std::{fs, path::Path};
|
||||
use tower_http::services::ServeDir;
|
||||
use webp::Encoder;
|
||||
|
||||
use crate::music::metadata::CoverData;
|
||||
|
||||
@@ -40,7 +40,11 @@ pub fn get_all_cover_hashes() -> Vec<String> {
|
||||
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"
|
||||
&& cover.mime_type != "image/png"
|
||||
&& cover.mime_type != "image/webp"
|
||||
|
||||
@@ -14,9 +14,7 @@ fn map_artist(row: &Row) -> Result<Artist, rusqlite::Error> {
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn get_artists(
|
||||
pool: &Pool,
|
||||
) -> Result<Vec<Artist>, rusqlite::Error> {
|
||||
pub async fn get_artists(pool: &Pool) -> Result<Vec<Artist>, rusqlite::Error> {
|
||||
let manager = pool.get().await.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 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
|
||||
}
|
||||
|
||||
pub async fn initialize_database(
|
||||
pool: &Pool,
|
||||
) -> Result<(), rusqlite::Error> {
|
||||
pub async fn initialize_database(pool: &Pool) -> Result<(), rusqlite::Error> {
|
||||
let manager = pool.get().await.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(())
|
||||
}
|
||||
|
||||
@@ -16,9 +16,7 @@ fn map_library_path(row: &Row) -> Result<LibraryPath, rusqlite::Error> {
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn get_library_paths(
|
||||
pool: &Pool,
|
||||
) -> Result<Vec<LibraryPath>, rusqlite::Error> {
|
||||
pub async fn get_library_paths(pool: &Pool) -> Result<Vec<LibraryPath>, rusqlite::Error> {
|
||||
let manager = pool.get().await.unwrap();
|
||||
let connection = manager.lock().unwrap();
|
||||
|
||||
@@ -36,10 +34,7 @@ pub async fn get_library_paths(
|
||||
Ok(paths)
|
||||
}
|
||||
|
||||
pub async fn get_library_path(
|
||||
pool: &Pool,
|
||||
id: u64,
|
||||
) -> Result<LibraryPath, rusqlite::Error> {
|
||||
pub async fn get_library_path(pool: &Pool, id: u64) -> Result<LibraryPath, rusqlite::Error> {
|
||||
let manager = pool.get().await.unwrap();
|
||||
let connection = manager.lock().unwrap();
|
||||
|
||||
@@ -62,10 +57,7 @@ pub async fn insert_library_path(
|
||||
Ok(result > 0)
|
||||
}
|
||||
|
||||
pub async fn delete_library_path(
|
||||
pool: &Pool,
|
||||
path_id: u64,
|
||||
) -> Result<bool, rusqlite::Error> {
|
||||
pub async fn delete_library_path(pool: &Pool, path_id: u64) -> Result<bool, rusqlite::Error> {
|
||||
let manager = pool.get().await.unwrap();
|
||||
let connection = manager.lock().unwrap();
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ use rusqlite::{params, Row};
|
||||
use crate::{
|
||||
covers::{get_all_cover_hashes, get_cover_base_path, write_cover},
|
||||
music::metadata::TrackMetadata,
|
||||
proto,
|
||||
proto::{self, library::Playlist},
|
||||
};
|
||||
|
||||
use super::artists::get_artists;
|
||||
@@ -57,9 +57,17 @@ fn map_track(row: &Row) -> Result<Track, rusqlite::Error> {
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn get_tracks(
|
||||
pool: &Pool,
|
||||
) -> Result<Vec<Track>, rusqlite::Error> {
|
||||
/// Creates a playlist from a database row (tracks are empty and need to be filled afterwards)
|
||||
/// * `row`: The database row
|
||||
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 connection = manager.lock().unwrap();
|
||||
|
||||
@@ -77,20 +85,31 @@ pub async fn get_tracks(
|
||||
Ok(tracks)
|
||||
}
|
||||
|
||||
pub async fn get_track(
|
||||
pool: &Pool,
|
||||
hash: &str,
|
||||
) -> Result<Track, rusqlite::Error> {
|
||||
pub async fn get_track(pool: &Pool, hash: &str) -> Result<Track, rusqlite::Error> {
|
||||
let manager = pool.get().await.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)
|
||||
}
|
||||
|
||||
pub async fn get_track_full_path(
|
||||
pub async fn get_specific_tracks(
|
||||
pool: &Pool,
|
||||
hash: &str,
|
||||
) -> Result<PathBuf, rusqlite::Error> {
|
||||
hashes: &Vec<String>,
|
||||
) -> 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 connection = &manager.lock().unwrap();
|
||||
|
||||
@@ -106,6 +125,35 @@ pub async fn get_track_full_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(
|
||||
pool: &Pool,
|
||||
tracks: HashMap<String, TrackMetadata>,
|
||||
@@ -190,3 +238,105 @@ pub async fn insert_tracks(
|
||||
|
||||
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 std::{collections::HashMap, path::PathBuf, sync::Arc, time::Instant};
|
||||
use tokio::{sync::Mutex, task::JoinSet};
|
||||
|
||||
use tonic::{Request, Response, Status};
|
||||
use walkdir::{DirEntry, WalkDir};
|
||||
|
||||
use crate::{
|
||||
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},
|
||||
proto::library::{library_server::Library, Track, TrackList},
|
||||
proto::library::{
|
||||
library_server::Library, AddTrackToPlaylistRequest, CreatePlaylistRequest,
|
||||
CreatePlaylistResponse, DeletePlaylistRequest, ListPlaylistsResponse,
|
||||
RemoveTrackFromPlaylistRequest, Track, TrackList,
|
||||
},
|
||||
state::GrooveState,
|
||||
};
|
||||
|
||||
@@ -26,12 +34,9 @@ impl LibraryService {
|
||||
|
||||
#[tonic::async_trait]
|
||||
impl Library for LibraryService {
|
||||
async fn list_tracks(
|
||||
&self,
|
||||
_request: tonic::Request<()>,
|
||||
) -> Result<tonic::Response<TrackList>, tonic::Status> {
|
||||
async fn list_tracks(&self, _request: Request<()>) -> Result<Response<TrackList>, Status> {
|
||||
let Ok(tracks) = get_tracks(&self.pool).await else {
|
||||
return Err(tonic::Status::internal(""));
|
||||
return Err(Status::internal(""));
|
||||
};
|
||||
|
||||
let response = TrackList {
|
||||
@@ -47,15 +52,96 @@ impl Library for LibraryService {
|
||||
.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(
|
||||
path: PathBuf,
|
||||
db: &Pool,
|
||||
path_id: u64,
|
||||
) -> Result<(), rusqlite::Error> {
|
||||
pub async fn index_path(path: PathBuf, db: &Pool, path_id: u64) -> Result<(), rusqlite::Error> {
|
||||
let home = home::home_dir().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 state;
|
||||
|
||||
use settings::SettingsService;
|
||||
use music::player::start_watching;
|
||||
use settings::SettingsService;
|
||||
|
||||
pub mod proto {
|
||||
pub mod settings {
|
||||
@@ -40,7 +40,9 @@ pub mod proto {
|
||||
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
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()?;
|
||||
|
||||
@@ -58,8 +60,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let mut player = player.lock().await;
|
||||
match next {
|
||||
Some(queued_track) => {
|
||||
let _ = player
|
||||
.play_track(queued_track.track, queued_track.path, false);
|
||||
let _ = player.play_track(queued_track.track, queued_track.path, false);
|
||||
}
|
||||
None => {
|
||||
player.clear();
|
||||
|
||||
@@ -2,7 +2,7 @@ use std::{
|
||||
collections::VecDeque,
|
||||
fs,
|
||||
io::{BufReader, Cursor},
|
||||
path::Path,
|
||||
path::{Path, PathBuf},
|
||||
sync::Arc,
|
||||
time::Duration,
|
||||
};
|
||||
@@ -97,6 +97,30 @@ impl AudioPlayer {
|
||||
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>> {
|
||||
let Some(queued_track) = self.queue.pop_front() else {
|
||||
self.clear();
|
||||
|
||||
@@ -1,17 +1,21 @@
|
||||
use deadpool_sqlite::Pool;
|
||||
use std::{pin::Pin, time::Duration};
|
||||
use std::{path::PathBuf, pin::Pin, time::Duration};
|
||||
use tokio::sync::mpsc;
|
||||
use tokio_stream::{wrappers::ReceiverStream, Stream};
|
||||
|
||||
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,
|
||||
proto::{
|
||||
self,
|
||||
player::{
|
||||
player_server::Player, PauseState, PlayTrackResponse, PlayerStatus, Queue,
|
||||
SeekPositionRequest, SeekPositionResponse, SetVolumeRequest, SetVolumeResponse,
|
||||
SkipToQueueIndexRequest, SwapQueueIndicesRequest, TrackRequest,
|
||||
player_server::Player, PauseState, PlayPlaylistRequest, PlayTrackResponse,
|
||||
PlayerStatus, Queue, SeekPositionRequest, SeekPositionResponse, SetVolumeRequest,
|
||||
SetVolumeResponse, SkipToQueueIndexRequest, SwapQueueIndicesRequest, TrackRequest,
|
||||
TracksRequest,
|
||||
},
|
||||
},
|
||||
state::GrooveState,
|
||||
@@ -198,6 +202,75 @@ impl Player for PlayerService {
|
||||
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(
|
||||
&self,
|
||||
request: tonic::Request<TrackRequest>,
|
||||
|
||||
Reference in New Issue
Block a user