feat: stream currently playing + volume + toggle pause + seek position

This commit is contained in:
2024-11-24 23:36:24 +01:00
parent 51a57ebd40
commit de9e430828
14 changed files with 314 additions and 62 deletions

1
Cargo.lock generated
View File

@@ -741,6 +741,7 @@ dependencies = [
"sha2", "sha2",
"symphonia", "symphonia",
"tokio", "tokio",
"tokio-stream",
"tonic", "tonic",
"tonic-build", "tonic-build",
"tonic-web", "tonic-web",

View File

@@ -18,6 +18,7 @@ rusqlite = { version = "0.32.1", features = ["bundled"] }
sha2 = "0.10.8" sha2 = "0.10.8"
symphonia = { version = "0.5.4", features = ["mp3"] } symphonia = { version = "0.5.4", features = ["mp3"] }
tokio = { version = "1.41.1", features = ["full"] } tokio = { version = "1.41.1", features = ["full"] }
tokio-stream = "0.1.16"
tonic = "0.12.3" tonic = "0.12.3"
tonic-web = "0.12.3" tonic-web = "0.12.3"
tower-http = { version = "0.6.2", features = ["cors", "fs"] } tower-http = { version = "0.6.2", features = ["cors", "fs"] }

View File

@@ -17,4 +17,5 @@ message Track {
string name = 2; string name = 2;
string artist_name = 3; string artist_name = 3;
uint64 artist_id = 4; uint64 artist_id = 4;
uint64 duration = 5;
} }

View File

@@ -1,13 +1,18 @@
syntax = "proto3"; syntax = "proto3";
import 'google/protobuf/empty.proto'; import 'google/protobuf/empty.proto';
import 'library.proto';
package player; package player;
service Player { service Player {
rpc PlayTrack(PlayTrackRequest) returns (PlayTrackResponse); rpc PlayTrack(PlayTrackRequest) returns (PlayTrackResponse);
rpc ResumeTrack(google.protobuf.Empty) returns (google.protobuf.Empty); rpc ResumeTrack(google.protobuf.Empty) returns (PauseState);
rpc PauseTrack(google.protobuf.Empty) returns (google.protobuf.Empty); rpc PauseTrack(google.protobuf.Empty) returns (PauseState);
rpc TogglePause(google.protobuf.Empty) returns (PauseState);
rpc GetStatus(google.protobuf.Empty) returns (stream PlayerStatus);
rpc SeekPosition(SeekPositionRequest) returns (SeekPositionResponse);
rpc SetVolume(SetVolumeRequest) returns (SetVolumeResponse);
} }
message PlayTrackRequest { message PlayTrackRequest {
@@ -15,4 +20,33 @@ message PlayTrackRequest {
} }
message PlayTrackResponse { message PlayTrackResponse {
library.Track track = 1;
uint64 position = 2;
}
message PlayerStatus {
optional library.Track currently_playing = 1;
bool is_paused = 2;
float volume = 3;
uint64 progress = 4;
}
message PauseState {
bool is_paused = 1;
}
message SeekPositionRequest {
uint64 position = 1;
}
message SeekPositionResponse {
uint64 position = 1;
}
message SetVolumeRequest {
float volume = 1;
}
message SetVolumeResponse {
float volume = 1;
} }

View File

@@ -1,4 +1,4 @@
use sha2::{Digest, Sha256, self}; use sha2::{self, Digest, Sha256};
pub fn generate_hash(content: impl AsRef<[u8]>) -> String { pub fn generate_hash(content: impl AsRef<[u8]>) -> String {
let mut hasher = Sha256::new(); let mut hasher = Sha256::new();

View File

@@ -21,7 +21,6 @@ pub fn get_all_cover_hashes() -> Vec<String> {
let _ = fs::create_dir_all(base_path); let _ = fs::create_dir_all(base_path);
} }
let walkdir = walkdir::WalkDir::new(path).min_depth(1).max_depth(1); let walkdir = walkdir::WalkDir::new(path).min_depth(1).max_depth(1);
let hashes: Vec<String> = walkdir let hashes: Vec<String> = walkdir

View File

@@ -1,6 +1,6 @@
pub mod artists;
pub mod paths; pub mod paths;
pub mod tracks; pub mod tracks;
pub mod artists;
use r2d2::{Pool, PooledConnection}; use r2d2::{Pool, PooledConnection};
use r2d2_sqlite::SqliteConnectionManager; use r2d2_sqlite::SqliteConnectionManager;
@@ -37,8 +37,7 @@ pub fn initialize_database(
[], [],
)?; )?;
connection connection.execute(
.execute(
" "
CREATE TABLE IF NOT EXISTS tracks ( CREATE TABLE IF NOT EXISTS tracks (
hash TEXT PRIMARY KEY NOT NULL, hash TEXT PRIMARY KEY NOT NULL,
@@ -46,6 +45,7 @@ pub fn initialize_database(
name TEXT NOT NULL, name TEXT NOT NULL,
artist_id INTEGER NOT NULL, artist_id INTEGER NOT NULL,
path TEXT NOT NULL, path TEXT NOT NULL,
duration INTEGER NOT NULL,
FOREIGN KEY (library_path_id) REFERENCES library_paths (id) ON DELETE CASCADE, FOREIGN KEY (library_path_id) REFERENCES library_paths (id) ON DELETE CASCADE,
FOREIGN KEY (artist_id) REFERENCES artists (id) ON DELETE CASCADE FOREIGN KEY (artist_id) REFERENCES artists (id) ON DELETE CASCADE
); );

View File

@@ -11,31 +11,58 @@ use rusqlite::{params, Row};
use crate::{ use crate::{
covers::{get_all_cover_hashes, write_cover}, covers::{get_all_cover_hashes, write_cover},
music::metadata::TrackMetadata, music::metadata::TrackMetadata,
proto,
}; };
use super::artists::get_artists; use super::artists::get_artists;
#[derive(Debug)] #[derive(Debug, Clone, PartialEq)]
pub struct Track { pub struct Track {
pub hash: String, pub hash: String,
pub name: String, pub name: String,
pub artist_name: String, pub artist_name: String,
pub artist_id: u64, pub artist_id: u64,
pub duration: u64,
}
impl Into<proto::library::Track> for Track {
fn into(self) -> proto::library::Track {
proto::library::Track {
hash: self.hash,
name: self.name,
artist_name: self.artist_name,
artist_id: self.artist_id,
duration: self.duration,
}
}
}
impl From<proto::library::Track> for Track {
fn from(value: proto::library::Track) -> Self {
Track {
hash: value.hash,
name: value.name,
artist_name: value.artist_name,
artist_id: value.artist_id,
duration: value.duration,
}
}
} }
fn map_track(row: &Row) -> Result<Track, rusqlite::Error> { fn map_track(row: &Row) -> Result<Track, rusqlite::Error> {
Ok(Track { Ok(Track {
hash: row.get(0)?, hash: row.get(0)?,
name: row.get(1)?, name: row.get(1)?,
artist_id: row.get(2)?, duration: row.get(2)?,
artist_name: row.get(3)?, artist_id: row.get(3)?,
artist_name: row.get(4)?,
}) })
} }
pub fn get_tracks( pub fn get_tracks(
connection: &PooledConnection<SqliteConnectionManager>, connection: &PooledConnection<SqliteConnectionManager>,
) -> Result<Vec<Track>, rusqlite::Error> { ) -> Result<Vec<Track>, rusqlite::Error> {
let mut statement = connection.prepare("SELECT t.hash, t.name, t.artist_id, a.name AS artist_name FROM tracks t INNER JOIN artists a ON a.id = t.artist_id")?; 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")?;
let rows = statement.query_map([], map_track)?; let rows = statement.query_map([], map_track)?;
let mut tracks: Vec<Track> = Vec::new(); let mut tracks: Vec<Track> = Vec::new();
@@ -53,7 +80,7 @@ pub fn get_track(
connection: &PooledConnection<SqliteConnectionManager>, connection: &PooledConnection<SqliteConnectionManager>,
hash: &str, hash: &str,
) -> Result<Track, rusqlite::Error> { ) -> Result<Track, rusqlite::Error> {
connection.query_row("SELECT t.hash, t.name, 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 fn get_track_full_path( pub fn get_track_full_path(
@@ -122,7 +149,7 @@ pub fn insert_tracks(
{ {
let mut statement = let mut statement =
tx.prepare("INSERT OR REPLACE INTO tracks (hash, library_path_id, name, artist_id, path) VALUES (?1, ?2, ?3, ?4, ?5)")?; tx.prepare("INSERT OR REPLACE INTO tracks (hash, library_path_id, name, artist_id, path, duration) VALUES (?1, ?2, ?3, ?4, ?5, ?6)")?;
for (hash, meta) in tracks { for (hash, meta) in tracks {
statement.execute(params![ statement.execute(params![
@@ -131,6 +158,7 @@ pub fn insert_tracks(
meta.name, meta.name,
artist_names_to_id[&meta.artist_name], artist_names_to_id[&meta.artist_name],
meta.path, meta.path,
meta.total_seconds * 1000
])?; ])?;
if let Some(cover) = meta.cover { if let Some(cover) = meta.cover {

View File

@@ -9,7 +9,7 @@ use crate::{
checksum::generate_hash, checksum::generate_hash,
database::tracks::{get_tracks, insert_tracks}, database::tracks::{get_tracks, insert_tracks},
music::metadata::{extract_track_data, TrackMetadata}, music::metadata::{extract_track_data, TrackMetadata},
proto::{self, library_server::Library}, proto::library::{library_server::Library, Track, TrackList},
state::GrooveState, state::GrooveState,
}; };
@@ -30,7 +30,7 @@ impl Library for LibraryService {
async fn list_tracks( async fn list_tracks(
&self, &self,
_request: tonic::Request<()>, _request: tonic::Request<()>,
) -> Result<tonic::Response<proto::TrackList>, tonic::Status> { ) -> Result<tonic::Response<TrackList>, tonic::Status> {
let Ok(db) = self.pool.get() else { let Ok(db) = self.pool.get() else {
return Err(tonic::Status::internal("")); return Err(tonic::Status::internal(""));
}; };
@@ -39,16 +39,17 @@ impl Library for LibraryService {
return Err(tonic::Status::internal("")); return Err(tonic::Status::internal(""));
}; };
let response = proto::TrackList { let response = TrackList {
tracks: tracks tracks: tracks
.iter() .iter()
.map(|t| proto::Track { .map(|t| Track {
hash: t.hash.clone(), hash: t.hash.clone(),
name: t.name.clone(), name: t.name.clone(),
artist_name: t.artist_name.clone(), artist_name: t.artist_name.clone(),
artist_id: t.artist_id, artist_id: t.artist_id,
duration: t.duration,
}) })
.collect::<Vec<proto::Track>>(), .collect::<Vec<Track>>(),
}; };
Ok(tonic::Response::new(response)) Ok(tonic::Response::new(response))

View File

@@ -3,12 +3,12 @@ use database::{establish_connection, initialize_database};
use library::LibraryService; use library::LibraryService;
use music::player::AudioPlayer; use music::player::AudioPlayer;
use player::PlayerService; use player::PlayerService;
use proto::library_server::LibraryServer; use proto::library::library_server::LibraryServer;
use proto::player_server::PlayerServer; use proto::player::player_server::PlayerServer;
use proto::settings_server::SettingsServer; use proto::settings::settings_server::SettingsServer;
use rodio::{OutputStream, Sink}; use rodio::{OutputStream, Sink};
use state::{GrooveState, GrooveStateData}; use state::{GrooveState, GrooveStateData};
use tokio::sync::RwLock; use tokio::sync::Mutex;
use tonic::transport::Server; use tonic::transport::Server;
pub mod checksum; pub mod checksum;
@@ -23,9 +23,15 @@ pub mod state;
use settings::SettingsService; use settings::SettingsService;
pub mod proto { pub mod proto {
pub mod settings {
tonic::include_proto!("settings"); tonic::include_proto!("settings");
}
pub mod library {
tonic::include_proto!("library"); tonic::include_proto!("library");
}
pub mod player {
tonic::include_proto!("player"); tonic::include_proto!("player");
}
} }
#[tokio::main] #[tokio::main]
@@ -44,7 +50,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
let player = AudioPlayer::new(sink); let player = AudioPlayer::new(sink);
let state = GrooveState::new(RwLock::new(GrooveStateData::new(player))); let state = GrooveState::new(Mutex::new(GrooveStateData::new(player)));
let settings = SettingsService::new(state.clone(), pool.clone()); let settings = SettingsService::new(state.clone(), pool.clone());
let library = LibraryService::new(state.clone(), pool.clone()); let library = LibraryService::new(state.clone(), pool.clone());

View File

@@ -1,28 +1,40 @@
use std::{ use std::{
fs, fs,
io::{BufReader, Cursor}, path::Path, io::{BufReader, Cursor},
path::Path,
time::Duration,
}; };
use rodio::{Decoder, Sink, Source}; use rodio::{Decoder, Sink};
use crate::{database::tracks::Track, proto::player::PlayerStatus};
pub struct AudioPlayer { pub struct AudioPlayer {
pub sink: Sink, sink: Sink,
currently_playing: Option<Track>,
} }
impl AudioPlayer { impl AudioPlayer {
pub fn new(sink: Sink) -> Self { pub fn new(sink: Sink) -> Self {
sink.set_volume(0.5);
Self { Self {
sink sink,
currently_playing: None,
} }
} }
pub fn play_song<P>(&mut self, path: P) -> Result<(), Box<dyn std::error::Error>> where P: AsRef<Path> { pub fn play_track<P>(&mut self, track: Track, path: P) -> Result<(), Box<dyn std::error::Error>>
where
P: AsRef<Path>,
{
self.sink.clear(); self.sink.clear();
let file = BufReader::new(Cursor::new(fs::read(path)?)); let file = BufReader::new(Cursor::new(fs::read(path)?));
let source = Decoder::new(file)?.amplify(0.2); let source = Decoder::new(file)?;
self.currently_playing = Some(track);
self.sink.append(source); self.sink.append(source);
self.sink.play(); self.sink.play();
@@ -30,11 +42,74 @@ impl AudioPlayer {
Ok(()) Ok(())
} }
pub fn resume(&mut self) { pub fn resume(&self) {
self.sink.play(); self.sink.play();
} }
pub fn pause(&mut self) { pub fn pause(&self) {
self.sink.pause(); self.sink.pause();
} }
/// Toggles the player's pause state and returns the new value
pub fn toggle_pause(&self) -> bool {
if self.is_paused() {
self.resume();
false
} else {
self.pause();
true
}
}
pub fn volume(&self) -> f32 {
self.sink.volume()
}
pub fn set_volume(&self, value: f32) {
self.sink.set_volume(value);
}
pub fn position(&self) -> u128 {
self.sink.get_pos().as_millis()
}
pub fn set_position(&self, position: u64) {
let _ = self.sink.try_seek(Duration::from_millis(position));
}
pub fn is_paused(&self) -> bool {
self.sink.is_paused()
}
pub fn currently_playing(&self) -> Option<Track> {
self.currently_playing.clone()
}
pub fn get_snapshot(&self) -> StatusSnapshot {
StatusSnapshot {
volume: self.volume(),
position: self.position(),
is_paused: self.is_paused(),
currently_playing: self.currently_playing(),
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct StatusSnapshot {
pub volume: f32,
pub position: u128,
pub is_paused: bool,
pub currently_playing: Option<Track>,
}
impl Into<PlayerStatus> for StatusSnapshot {
fn into(self) -> PlayerStatus {
PlayerStatus {
volume: self.volume,
is_paused: self.is_paused,
progress: self.position as u64,
currently_playing: self.currently_playing.clone().map(|t| t.into()),
}
}
} }

View File

@@ -1,8 +1,19 @@
use r2d2::Pool; use r2d2::Pool;
use r2d2_sqlite::SqliteConnectionManager; use r2d2_sqlite::SqliteConnectionManager;
use std::{pin::Pin, time::Duration};
use tokio::sync::mpsc;
use tokio_stream::{wrappers::ReceiverStream, Stream};
use crate::{ use crate::{
database::tracks::get_track_full_path, proto::{player_server::Player, PlayTrackRequest, PlayTrackResponse}, state::GrooveState database::tracks::{get_track, get_track_full_path},
proto::{
self,
player::{
player_server::Player, PauseState, PlayTrackRequest, PlayTrackResponse, PlayerStatus,
SeekPositionRequest, SeekPositionResponse, SetVolumeRequest, SetVolumeResponse,
},
},
state::GrooveState,
}; };
pub struct PlayerService { pub struct PlayerService {
@@ -18,6 +29,8 @@ impl PlayerService {
#[tonic::async_trait] #[tonic::async_trait]
impl Player for PlayerService { impl Player for PlayerService {
type GetStatusStream = Pin<Box<dyn Stream<Item = Result<PlayerStatus, tonic::Status>> + Send>>;
async fn play_track( async fn play_track(
&self, &self,
request: tonic::Request<PlayTrackRequest>, request: tonic::Request<PlayTrackRequest>,
@@ -28,15 +41,21 @@ impl Player for PlayerService {
let input = request.get_ref(); let input = request.get_ref();
let Ok(track) = get_track(&db, input.hash.as_str()) else {
return Err(tonic::Status::not_found(""));
};
let Ok(track_path) = get_track_full_path(&db, input.hash.as_str()) else { let Ok(track_path) = get_track_full_path(&db, input.hash.as_str()) else {
return Err(tonic::Status::not_found("")); return Err(tonic::Status::not_found(""));
}; };
let mut state = self.state.write().await; let mut state = self.state.lock().await;
let _ = state.player.play_song(track_path); let _ = state.player.play_track(track.clone(), track_path);
let response = PlayTrackResponse { let response = PlayTrackResponse {
track: Some(proto::library::Track::from(track.into())),
position: 0,
}; };
Ok(tonic::Response::new(response)) Ok(tonic::Response::new(response))
@@ -45,22 +64,105 @@ impl Player for PlayerService {
async fn resume_track( async fn resume_track(
&self, &self,
_request: tonic::Request<()>, _request: tonic::Request<()>,
) -> Result<tonic::Response<()>, tonic::Status> { ) -> Result<tonic::Response<PauseState>, tonic::Status> {
let mut state = self.state.write().await; let state = self.state.lock().await;
state.player.resume(); state.player.resume();
Ok(tonic::Response::new(())) let response = PauseState { is_paused: false };
Ok(tonic::Response::new(response))
} }
async fn pause_track( async fn pause_track(
&self, &self,
_request: tonic::Request<()>, _request: tonic::Request<()>,
) -> Result<tonic::Response<()>, tonic::Status> { ) -> Result<tonic::Response<PauseState>, tonic::Status> {
let mut state = self.state.write().await; let state = self.state.lock().await;
state.player.pause(); state.player.pause();
Ok(tonic::Response::new(())) let response = PauseState { is_paused: true };
Ok(tonic::Response::new(response))
}
async fn toggle_pause(
&self,
_request: tonic::Request<()>,
) -> Result<tonic::Response<PauseState>, tonic::Status> {
let state = self.state.lock().await;
let is_paused = state.player.toggle_pause();
let response = PauseState { is_paused };
Ok(tonic::Response::new(response))
}
async fn seek_position(
&self,
request: tonic::Request<SeekPositionRequest>,
) -> Result<tonic::Response<SeekPositionResponse>, tonic::Status> {
let input = request.get_ref();
let state = self.state.lock().await;
state.player.set_position(input.position);
let response = SeekPositionResponse {
position: input.position,
};
Ok(tonic::Response::new(response))
}
async fn set_volume(
&self,
request: tonic::Request<SetVolumeRequest>,
) -> Result<tonic::Response<SetVolumeResponse>, tonic::Status> {
let input = request.get_ref();
let state = self.state.lock().await;
state.player.set_volume(input.volume);
let response = SetVolumeResponse {
volume: input.volume,
};
Ok(tonic::Response::new(response))
}
async fn get_status(
&self,
_request: tonic::Request<()>,
) -> Result<tonic::Response<Self::GetStatusStream>, tonic::Status> {
let state = self.state.clone();
let (tx, rx) = mpsc::channel::<Result<PlayerStatus, tonic::Status>>(128);
let sleep_duration = Duration::from_millis(1000);
tokio::spawn(async move {
println!("get_status stream opened");
while !tx.is_closed() {
if let Err(_) = tx
.send(Ok(state.lock().await.player.get_snapshot().into()))
.await
{
break;
}
tokio::time::sleep(sleep_duration).await;
}
println!("get_status stream closed");
});
let output_stream = ReceiverStream::new(rx);
let response = Box::pin(output_stream) as Self::GetStatusStream;
Ok(tonic::Response::new(response))
} }
} }

View File

@@ -3,9 +3,13 @@ use crate::database::paths::{
}; };
use crate::library::index_path; use crate::library::index_path;
use crate::proto; use crate::proto;
use crate::proto::settings::{
AddPathRequest, AddPathResponse, DeletePathRequest, DeletePathResponse, RefreshPathRequest,
RefreshPathResponse,
};
use crate::state::GrooveState; use crate::state::GrooveState;
use proto::settings_server::Settings; use proto::settings::{settings_server::Settings, LibraryPath, SettingsData};
use r2d2::Pool; use r2d2::Pool;
use r2d2_sqlite::SqliteConnectionManager; use r2d2_sqlite::SqliteConnectionManager;
@@ -26,7 +30,7 @@ impl Settings for SettingsService {
async fn list_paths( async fn list_paths(
&self, &self,
_request: tonic::Request<()>, _request: tonic::Request<()>,
) -> Result<tonic::Response<proto::SettingsData>, tonic::Status> { ) -> Result<tonic::Response<SettingsData>, tonic::Status> {
let Ok(db) = self.pool.get() else { let Ok(db) = self.pool.get() else {
return Err(tonic::Status::internal("")); return Err(tonic::Status::internal(""));
}; };
@@ -35,10 +39,10 @@ impl Settings for SettingsService {
return Err(tonic::Status::internal("")); return Err(tonic::Status::internal(""));
}; };
let response = proto::SettingsData { let response = SettingsData {
library_paths: library_paths library_paths: library_paths
.iter() .iter()
.map(|p| proto::LibraryPath { .map(|p| LibraryPath {
id: p.id, id: p.id,
path: p.path.clone(), path: p.path.clone(),
}) })
@@ -50,8 +54,8 @@ impl Settings for SettingsService {
async fn add_path( async fn add_path(
&self, &self,
request: tonic::Request<proto::AddPathRequest>, request: tonic::Request<AddPathRequest>,
) -> Result<tonic::Response<proto::AddPathResponse>, tonic::Status> { ) -> Result<tonic::Response<AddPathResponse>, tonic::Status> {
let input = request.into_inner(); let input = request.into_inner();
let Ok(db) = self.pool.get() else { let Ok(db) = self.pool.get() else {
@@ -66,15 +70,15 @@ impl Settings for SettingsService {
return Err(tonic::Status::internal("")); return Err(tonic::Status::internal(""));
} }
let response = proto::AddPathResponse { id: 0 }; let response = AddPathResponse { id: 0 };
Ok(tonic::Response::new(response)) Ok(tonic::Response::new(response))
} }
async fn delete_path( async fn delete_path(
&self, &self,
request: tonic::Request<proto::DeletePathRequest>, request: tonic::Request<DeletePathRequest>,
) -> Result<tonic::Response<proto::DeletePathResponse>, tonic::Status> { ) -> Result<tonic::Response<DeletePathResponse>, tonic::Status> {
let input = request.into_inner(); let input = request.into_inner();
let Ok(db) = self.pool.get() else { let Ok(db) = self.pool.get() else {
@@ -89,15 +93,15 @@ impl Settings for SettingsService {
return Err(tonic::Status::not_found("")); return Err(tonic::Status::not_found(""));
} }
let response = proto::DeletePathResponse {}; let response = DeletePathResponse {};
Ok(tonic::Response::new(response)) Ok(tonic::Response::new(response))
} }
async fn refresh_path( async fn refresh_path(
&self, &self,
request: tonic::Request<proto::RefreshPathRequest>, request: tonic::Request<RefreshPathRequest>,
) -> Result<tonic::Response<proto::RefreshPathResponse>, tonic::Status> { ) -> Result<tonic::Response<RefreshPathResponse>, tonic::Status> {
let input = request.into_inner(); let input = request.into_inner();
let Ok(db) = self.pool.get() else { let Ok(db) = self.pool.get() else {
@@ -110,7 +114,7 @@ impl Settings for SettingsService {
let _ = index_path(library_path.path.into(), db, library_path.id); let _ = index_path(library_path.path.into(), db, library_path.id);
let response = proto::RefreshPathResponse {}; let response = RefreshPathResponse {};
Ok(tonic::Response::new(response)) Ok(tonic::Response::new(response))
} }

View File

@@ -1,10 +1,10 @@
use std::sync::Arc; use std::sync::Arc;
use tokio::sync::RwLock; use tokio::sync::Mutex;
use crate::music::player::AudioPlayer; use crate::music::player::AudioPlayer;
pub type GrooveState = Arc<RwLock<GrooveStateData>>; pub type GrooveState = Arc<Mutex<GrooveStateData>>;
pub struct GrooveStateData { pub struct GrooveStateData {
pub player: AudioPlayer, pub player: AudioPlayer,