This commit is contained in:
bart
2026-11-04 19:41:25 +01:00
parent 99cbc00357
commit d77cdfedab
12 changed files with 2181 additions and 605 deletions
+210 -42
View File
@@ -21,17 +21,51 @@ pub enum RemoteNavAction {
Back,
ToggleOSD,
CycleSubtitle,
CycleAudio,
ToggleMute,
TogglePlaybackView,
ActivateSection(usize),
VolumeUp,
VolumeDown,
PlayPause,
Stop,
}
#[derive(Clone)]
pub struct RemoteNavSender {
tx: UnboundedSender<RemoteNavAction>,
ctx: Arc<std::sync::Mutex<Option<eframe::egui::Context>>>,
}
impl RemoteNavSender {
pub fn new(tx: UnboundedSender<RemoteNavAction>, ctx: Arc<std::sync::Mutex<Option<eframe::egui::Context>>>) -> Self {
Self { tx, ctx }
}
pub fn send(&self, action: RemoteNavAction) -> Result<(), tokio::sync::mpsc::error::SendError<RemoteNavAction>> {
let res = self.tx.send(action);
if let Ok(guard) = self.ctx.lock() {
if let Some(ref c) = *guard {
c.request_repaint();
}
}
res
}
}
impl From<UnboundedSender<RemoteNavAction>> for RemoteNavSender {
fn from(tx: UnboundedSender<RemoteNavAction>) -> Self {
Self {
tx,
ctx: Arc::new(std::sync::Mutex::new(None)),
}
}
}
#[derive(Clone)]
pub struct YatseServerState {
pub player: Arc<PlayerController>,
pub nav_tx: UnboundedSender<RemoteNavAction>,
pub nav_tx: RemoteNavSender,
}
#[derive(Debug, Deserialize)]
@@ -142,17 +176,26 @@ async fn dispatch_rpc(state: &YatseServerState, req: JsonRpcRequest) -> Value {
}
"Application.SetVolume" => {
let vol_param = req.params.as_ref()
.and_then(|p| p.get("volume").or_else(|| p.as_array().and_then(|a| a.first())))
.and_then(|v| v.as_f64());
.and_then(|p| p.get("volume").or_else(|| p.as_array().and_then(|a| a.first())));
let new_vol = match vol_param {
Some(v) => {
state.player.set_volume(v).await;
v.clamp(0.0, 100.0) as i64
}
None => {
let new_vol = if let Some(v_str) = vol_param.and_then(|v| v.as_str()) {
if v_str == "increment" {
state.player.volume_up().await;
let _ = state.nav_tx.send(RemoteNavAction::VolumeUp);
state.player.status.lock().await.volume as i64
} else if v_str == "decrement" {
state.player.volume_down().await;
let _ = state.nav_tx.send(RemoteNavAction::VolumeDown);
state.player.status.lock().await.volume as i64
} else {
state.player.status.lock().await.volume as i64
}
} else if let Some(v) = vol_param.and_then(|v| v.as_f64()) {
state.player.set_volume(v).await;
let _ = state.nav_tx.send(RemoteNavAction::VolumeUp);
v.clamp(0.0, 100.0) as i64
} else {
state.player.status.lock().await.volume as i64
};
json!({
"jsonrpc": "2.0",
@@ -163,9 +206,11 @@ async fn dispatch_rpc(state: &YatseServerState, req: JsonRpcRequest) -> Value {
"Player.GetActivePlayers" => {
let st = state.player.status.lock().await;
if st.state != PlaybackState::Stopped {
let pid = if st.media_type == "audio" { 0 } else { 1 };
let ptype = if st.media_type == "audio" { "audio" } else { "video" };
json!({
"jsonrpc": "2.0",
"result": [{"playerid": 1, "playertype": "internal", "type": "video"}],
"result": [{"playerid": pid, "playertype": "internal", "type": ptype}],
"id": id
})
} else {
@@ -181,28 +226,55 @@ async fn dispatch_rpc(state: &YatseServerState, req: JsonRpcRequest) -> Value {
let time = seconds_to_kodi_time(st.time_pos);
let totaltime = seconds_to_kodi_time(st.duration);
let speed = if st.state == PlaybackState::Playing { 1 } else { 0 };
let ptype = if st.media_type == "audio" { "audio" } else { "video" };
json!({
"jsonrpc": "2.0",
"result": {
"canseek": true,
"canchangespeed": true,
"canmove": false,
"canzoom": false,
"canrotate": false,
"speed": speed,
"time": time,
"totaltime": totaltime,
"percentage": if st.duration > 0.0 { (st.time_pos / st.duration) * 100.0 } else { 0.0 }
"percentage": if st.duration > 0.0 { (st.time_pos / st.duration) * 100.0 } else { 0.0 },
"type": ptype,
"live": false,
"subtitleenabled": true,
"subtitles": [
{"index": 0, "name": "Ondertitel 1", "language": ""},
{"index": 1, "name": "Ondertitel 2", "language": ""}
],
"currentsubtitle": {"index": 0, "name": "Ondertitel", "language": ""},
"audiostreams": [
{"index": 0, "name": "Audiostroom 1", "language": ""},
{"index": 1, "name": "Audiostroom 2", "language": ""}
],
"currentaudiostream": {"index": 0, "name": "Audiostroom", "language": ""}
},
"id": id
})
}
"Player.GetItem" => {
let st = state.player.status.lock().await;
let title = if !st.stream_title.is_empty() {
&st.stream_title
} else if !st.title.is_empty() {
&st.title
} else {
"Media"
};
let itype = if st.media_type == "audio" { "song" } else { "movie" };
json!({
"jsonrpc": "2.0",
"result": {
"item": {
"label": st.title,
"title": st.title,
"type": "movie"
"label": title,
"title": title,
"artist": if !st.artist.is_empty() { vec![st.artist.clone()] } else { Vec::new() },
"type": itype
}
},
"id": id
@@ -220,6 +292,7 @@ async fn dispatch_rpc(state: &YatseServerState, req: JsonRpcRequest) -> Value {
}
"Player.Stop" => {
state.player.stop().await;
let _ = state.nav_tx.send(RemoteNavAction::Stop);
json!({
"jsonrpc": "2.0",
"result": "OK",
@@ -227,17 +300,87 @@ async fn dispatch_rpc(state: &YatseServerState, req: JsonRpcRequest) -> Value {
})
}
"Player.Seek" => {
if let Some(params) = req.params {
if let Some(sec) = params.get("value").and_then(|v| v.as_f64()) {
state.player.seek(sec).await;
let mut result_pct = 0.0;
if let Some(ref params) = req.params {
let val = params.get("value").or_else(|| params.as_array().and_then(|a| a.get(1)));
if let Some(v) = val {
if let Some(pct) = v.get("percentage").and_then(|p| p.as_f64()) {
state.player.seek_percent(pct).await;
result_pct = pct;
} else if let Some(time_obj) = v.get("time") {
let h = time_obj.get("hours").and_then(|x| x.as_f64()).unwrap_or(0.0);
let m = time_obj.get("minutes").and_then(|x| x.as_f64()).unwrap_or(0.0);
let s = time_obj.get("seconds").and_then(|x| x.as_f64()).unwrap_or(0.0);
let total_secs = h * 3600.0 + m * 60.0 + s;
state.player.seek_time(total_secs).await;
} else if let Some(step) = v.as_str() {
match step.to_lowercase().as_str() {
"smallforward" => { state.player.seek(10.0).await; }
"smallbackward" => { state.player.seek(-10.0).await; }
"bigforward" => { state.player.seek(60.0).await; }
"bigbackward" => { state.player.seek(-60.0).await; }
_ => {}
}
} else if let Some(n) = v.as_f64() {
if n >= 0.0 && n <= 100.0 {
state.player.seek_percent(n).await;
result_pct = n;
} else {
state.player.seek(n).await;
}
}
}
}
let st = state.player.status.lock().await;
let time = seconds_to_kodi_time(st.time_pos);
let totaltime = seconds_to_kodi_time(st.duration);
let pct = if st.duration > 0.0 { (st.time_pos / st.duration) * 100.0 } else { result_pct };
json!({
"jsonrpc": "2.0",
"result": {"percentage": 50.0},
"result": {
"percentage": pct,
"time": time,
"totaltime": totaltime
},
"id": id
})
}
"Player.SetSubtitle" => {
state.player.cycle_subtitle().await;
let _ = state.nav_tx.send(RemoteNavAction::CycleSubtitle);
json!({"jsonrpc": "2.0", "result": "OK", "id": id})
}
"Player.SetAudioStream" => {
state.player.cycle_audio().await;
json!({"jsonrpc": "2.0", "result": "OK", "id": id})
}
"Player.SetSpeed" => {
if let Some(ref params) = req.params {
let speed_val = params.get("speed").or_else(|| params.as_array().and_then(|a| a.get(1)));
if let Some(s) = speed_val.and_then(|v| v.as_i64()) {
if s == 0 {
let st = state.player.status.lock().await;
if st.state == PlaybackState::Playing {
drop(st);
state.player.play_pause().await;
}
} else if s == 1 {
let st = state.player.status.lock().await;
if st.state == PlaybackState::Paused {
drop(st);
state.player.play_pause().await;
}
}
} else if let Some(s_str) = speed_val.and_then(|v| v.as_str()) {
if s_str == "increment" || s_str == "decrement" {
state.player.play_pause().await;
}
}
}
let st = state.player.status.lock().await;
let speed = if st.state == PlaybackState::Playing { 1 } else { 0 };
json!({"jsonrpc": "2.0", "result": {"speed": speed}, "id": id})
}
"Player.Open" => {
if let Some(params) = req.params {
if let Some(item) = params.get("item") {
@@ -285,50 +428,75 @@ async fn dispatch_rpc(state: &YatseServerState, req: JsonRpcRequest) -> Value {
json!({"jsonrpc": "2.0", "result": "OK", "id": id})
}
"Input.ExecuteAction" => {
let action = req.params.as_ref()
let raw_action = req.params.as_ref()
.and_then(|p| p.get("action").or_else(|| p.as_array().and_then(|a| a.first())))
.and_then(|a| a.as_str())
.unwrap_or("")
.to_lowercase();
.unwrap_or("");
let action = if raw_action.to_lowercase().starts_with("action(") && raw_action.ends_with(')') {
raw_action[7..raw_action.len() - 1].trim().to_lowercase()
} else {
raw_action.to_lowercase()
};
match action.as_str() {
"up" | "cursorup" => { let _ = state.nav_tx.send(RemoteNavAction::Up); }
"down" | "cursordown" => { let _ = state.nav_tx.send(RemoteNavAction::Down); }
"left" | "cursorleft" => { let _ = state.nav_tx.send(RemoteNavAction::Left); }
"right" | "cursorright" => { let _ = state.nav_tx.send(RemoteNavAction::Right); }
"select" | "enter" => { let _ = state.nav_tx.send(RemoteNavAction::Select); }
"back" | "previousmenu" | "parentdir" | "close" => { let _ = state.nav_tx.send(RemoteNavAction::Back); }
"togglefullscreen" => { let _ = state.nav_tx.send(RemoteNavAction::ToggleOSD); }
"osd" | "nextsubtitle" => { let _ = state.nav_tx.send(RemoteNavAction::CycleSubtitle); }
"title" | "menu" => { let _ = state.nav_tx.send(RemoteNavAction::TogglePlaybackView); }
"volumeup" => {
"up" | "cursorup" | "3" => { let _ = state.nav_tx.send(RemoteNavAction::Up); }
"down" | "cursordown" | "4" => { let _ = state.nav_tx.send(RemoteNavAction::Down); }
"left" | "cursorleft" | "1" | "stepback" | "smallstepback" => { let _ = state.nav_tx.send(RemoteNavAction::Left); }
"right" | "cursorright" | "2" | "stepforward" | "smallstepforward" => { let _ = state.nav_tx.send(RemoteNavAction::Right); }
"bigstepforward" => {
let p = state.player.clone();
tokio::spawn(async move { p.seek(60.0).await; });
}
"bigstepback" => {
let p = state.player.clone();
tokio::spawn(async move { p.seek(-60.0).await; });
}
"select" | "enter" | "7" => { let _ = state.nav_tx.send(RemoteNavAction::Select); }
"back" | "previousmenu" | "parentdir" | "close" | "9" | "10" | "escape" => { let _ = state.nav_tx.send(RemoteNavAction::Back); }
"togglefullscreen" | "12" => { let _ = state.nav_tx.send(RemoteNavAction::ToggleOSD); }
"osd" | "nextsubtitle" | "114" | "122" => { let _ = state.nav_tx.send(RemoteNavAction::CycleSubtitle); }
"audionextlanguage" | "nextaudio" => {
let p = state.player.clone();
tokio::spawn(async move { p.cycle_audio().await; });
}
"title" | "menu" | "11" => { let _ = state.nav_tx.send(RemoteNavAction::TogglePlaybackView); }
"volumeup" | "88" => {
let _ = state.nav_tx.send(RemoteNavAction::VolumeUp);
let p = state.player.clone();
tokio::spawn(async move { p.volume_up().await; });
}
"volumedown" => {
"volumedown" | "89" => {
let _ = state.nav_tx.send(RemoteNavAction::VolumeDown);
let p = state.player.clone();
tokio::spawn(async move { p.volume_down().await; });
}
"playpause" | "pause" | "play" => { state.player.play_pause().await; }
"stop" => {
"playpause" | "pause" | "play" | "15" => { state.player.play_pause().await; }
"stop" | "14" => {
let _ = state.nav_tx.send(RemoteNavAction::Stop);
state.player.stop().await;
}
"mute" | "togglemute" | "13" => {
let p = state.player.clone();
tokio::spawn(async move { p.toggle_mute().await; });
}
_ => {}
}
json!({"jsonrpc": "2.0", "result": "OK", "id": id})
}
"GUI.ActivateWindow" => {
if let Some(params) = req.params {
let window = params.get("window").and_then(|w| w.as_str()).unwrap_or("").to_lowercase();
match window.as_str() {
"videos" | "video" | "movies" => { let _ = state.nav_tx.send(RemoteNavAction::ActivateSection(1)); }
"tvshows" => { let _ = state.nav_tx.send(RemoteNavAction::ActivateSection(2)); }
"music" => { let _ = state.nav_tx.send(RemoteNavAction::ActivateSection(3)); }
"pictures" | "photos" => { let _ = state.nav_tx.send(RemoteNavAction::ActivateSection(4)); }
"home" => { let _ = state.nav_tx.send(RemoteNavAction::ActivateSection(0)); }
_ => {}
let p_str = params.to_string().to_lowercase();
if p_str.contains("tvshow") || p_str.contains("series") {
let _ = state.nav_tx.send(RemoteNavAction::ActivateSection(3));
} else if p_str.contains("movietitles") || p_str.contains("movie") || p_str.contains("video") {
let _ = state.nav_tx.send(RemoteNavAction::ActivateSection(2));
} else if p_str.contains("music") || p_str.contains("song") || p_str.contains("artist") || p_str.contains("album") {
let _ = state.nav_tx.send(RemoteNavAction::ActivateSection(4));
} else if p_str.contains("picture") || p_str.contains("photo") {
let _ = state.nav_tx.send(RemoteNavAction::ActivateSection(5));
} else if p_str.contains("livetv") || p_str.contains("channel") || p_str.contains("tv") {
let _ = state.nav_tx.send(RemoteNavAction::ActivateSection(1));
} else if p_str.contains("home") {
let _ = state.nav_tx.send(RemoteNavAction::ActivateSection(0));
}
}
json!({"jsonrpc": "2.0", "result": "OK", "id": id})