diff --git a/Cargo.lock b/Cargo.lock index 85dbf9a..1662a5e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1280,7 +1280,7 @@ dependencies = [ [[package]] name = "flex" -version = "0.16.0" +version = "0.16.1" dependencies = [ "anyhow", "chrono", @@ -1304,7 +1304,7 @@ dependencies = [ [[package]] name = "flex-anilist" -version = "0.16.0" +version = "0.16.1" dependencies = [ "flex-plugin", "ureq", @@ -1312,7 +1312,7 @@ dependencies = [ [[package]] name = "flex-core" -version = "0.16.0" +version = "0.16.1" dependencies = [ "anyhow", "directories", @@ -1337,7 +1337,7 @@ dependencies = [ [[package]] name = "flex-lotus-lamp" -version = "0.16.0" +version = "0.16.1" dependencies = [ "flex-plugin", "lotus-lamp", @@ -1346,7 +1346,7 @@ dependencies = [ [[package]] name = "flex-plugin" -version = "0.16.0" +version = "0.16.1" dependencies = [ "serde", "serde_json", @@ -1354,7 +1354,7 @@ dependencies = [ [[package]] name = "flex-trakt" -version = "0.16.0" +version = "0.16.1" dependencies = [ "flex-plugin", "ureq", @@ -1362,7 +1362,7 @@ dependencies = [ [[package]] name = "flex-web" -version = "0.16.0" +version = "0.16.1" dependencies = [ "anyhow", "axum", diff --git a/Cargo.toml b/Cargo.toml index a56dff2..3ad935a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,7 +6,7 @@ default-members = ["crates/*"] [workspace.package] edition = "2024" -version = "0.16.0" +version = "0.16.1" license = "MIT" [profile.release] diff --git a/crates/flex-app/Cargo.toml b/crates/flex-app/Cargo.toml index a11d96d..cf827f5 100644 --- a/crates/flex-app/Cargo.toml +++ b/crates/flex-app/Cargo.toml @@ -9,9 +9,9 @@ anyhow = "1.0.104" chrono = { version = "0.4.45", default-features = false, features = ["clock"] } eframe = { version = "0.36", default-features = false, features = ["glow", "default_fonts"] } env_logger = "0.11.11" -flex-core = { version = "0.16.0", path = "../flex-core" } -flex-plugin = { version = "0.16.0", path = "../flex-plugin" } -flex-web = { version = "0.16.0", path = "../flex-web" } +flex-core = { version = "0.16.1", path = "../flex-core" } +flex-plugin = { version = "0.16.1", path = "../flex-plugin" } +flex-web = { version = "0.16.1", path = "../flex-web" } image = { version = "0.25", default-features = false, features = ["jpeg", "png", "webp"] } libloading = "0.9" log = "0.4.34" @@ -21,7 +21,7 @@ ureq = "3" winit = { version = "0.30", default-features = false } [target."cfg(windows)".dependencies] -windows-sys = { version = "0.61.2", features = ["Win32_UI_WindowsAndMessaging", "Win32_UI_Input_KeyboardAndMouse", "Win32_Foundation", "Win32_Storage_FileSystem", "Win32_UI_Input", "Win32_System_Diagnostics_Debug", "Win32_System_Kernel", "Win32_System_LibraryLoader", "Win32_System_Com", "Win32_System_Registry", "Win32_System_Threading"] } +windows-sys = { version = "0.61.2", features = ["Win32_UI_WindowsAndMessaging", "Win32_UI_Input_KeyboardAndMouse", "Win32_Foundation", "Win32_Storage_FileSystem", "Win32_UI_Input", "Win32_System_Diagnostics_Debug", "Win32_System_Kernel", "Win32_System_LibraryLoader", "Win32_System_Com", "Win32_System_Registry", "Win32_System_Threading", "Win32_System_Power"] } [target."cfg(target_os = \"linux\")".dependencies] # Remote buttons egui doesn't report (media keys), read from /dev/input. diff --git a/crates/flex-app/src/awake.rs b/crates/flex-app/src/awake.rs new file mode 100644 index 0000000..f8f576f --- /dev/null +++ b/crates/flex-app/src/awake.rs @@ -0,0 +1,107 @@ +//! Keeps the computer from sleeping while something plays. mpv would do this for its own +//! window, but Flex draws mpv's picture itself, so it has to ask for itself. + +/// What playback needs kept on. +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +pub enum Need { + /// A video: the screen and the computer. + Screen, + /// Music: only the computer; the screen may go off. + System, +} + +/// Holds the request while playback needs it. Dropping it lets the computer sleep again. +#[derive(Default)] +pub struct KeepAwake { + held: Option<(Need, imp::Hold)>, + failed: bool, +} + +impl KeepAwake { + /// Asks for `need` (or lets go with `None`); only does anything when it changes. + pub fn set(&mut self, need: Option) { + if self.held.as_ref().map(|(n, _)| *n) == need { + return; + } + // Let go of the old one first: on Windows they're the same switch. + self.held = None; + let Some(need) = need else { return }; + match imp::Hold::new(need) { + Ok(hold) => self.held = Some((need, hold)), + // Once is enough in the log; this runs at every play and pause. + Err(e) if !self.failed => { + self.failed = true; + log::warn!("couldn't keep the computer awake while playing: {e}"); + } + Err(_) => {} + } + } +} + +#[cfg(windows)] +mod imp { + use super::Need; + use windows_sys::Win32::System::Power::{ES_CONTINUOUS, ES_DISPLAY_REQUIRED, ES_SYSTEM_REQUIRED, SetThreadExecutionState}; + + /// Windows' own setting for media players. It belongs to the thread that set it, which + /// is always Flex's UI thread. + pub struct Hold; + + impl Hold { + pub fn new(need: Need) -> Result { + let display = if need == Need::Screen { ES_DISPLAY_REQUIRED } else { 0 }; + let previous = unsafe { SetThreadExecutionState(ES_CONTINUOUS | ES_SYSTEM_REQUIRED | display) }; + if previous == 0 { Err("Windows refused".into()) } else { Ok(Hold) } + } + } + + impl Drop for Hold { + fn drop(&mut self) { + unsafe { SetThreadExecutionState(ES_CONTINUOUS) }; + } + } +} + +#[cfg(unix)] +mod imp { + use super::Need; + use std::process::{Child, Command, Stdio}; + + /// A helper that holds the request for as long as it runs: macOS's `caffeinate`, or + /// systemd's `systemd-inhibit` elsewhere. + pub struct Hold(Child); + + impl Hold { + pub fn new(need: Need) -> Result { + let mut cmd = command(need); + // If Flex dies without stopping it, the helper goes too: `caffeinate -w` watches + // Flex, and `cat` ends when Flex's end of its stdin closes. + cmd.stdin(Stdio::piped()).stdout(Stdio::null()).stderr(Stdio::null()); + let name = cmd.get_program().to_string_lossy().into_owned(); + cmd.spawn().map(Hold).map_err(|e| format!("{name}: {e}")) + } + } + + #[cfg(target_os = "macos")] + fn command(need: Need) -> Command { + let mut cmd = Command::new("caffeinate"); + cmd.arg(if need == Need::Screen { "-di" } else { "-i" }).args(["-w", &std::process::id().to_string()]); + cmd + } + + #[cfg(not(target_os = "macos"))] + fn command(need: Need) -> Command { + // "idle" also stops logind blanking the screen; music only needs "sleep". + let what = if need == Need::Screen { "idle:sleep" } else { "sleep" }; + let mut cmd = Command::new("systemd-inhibit"); + cmd.args([&format!("--what={what}"), "--who=Flex", "--why=Playing", "--mode=block", "cat"]); + cmd + } + + impl Drop for Hold { + fn drop(&mut self) { + let _ = self.0.kill(); + let _ = self.0.wait(); + } + } +} diff --git a/crates/flex-app/src/main.rs b/crates/flex-app/src/main.rs index 68731fd..8199004 100644 --- a/crates/flex-app/src/main.rs +++ b/crates/flex-app/src/main.rs @@ -3,6 +3,7 @@ mod ambient; mod app; +mod awake; #[cfg(windows)] mod autostart; mod images; diff --git a/crates/flex-app/src/shared.rs b/crates/flex-app/src/shared.rs index 9cd5794..d0cefc6 100644 --- a/crates/flex-app/src/shared.rs +++ b/crates/flex-app/src/shared.rs @@ -341,6 +341,8 @@ pub struct Shared { playback_sent: Option, /// The latest playback seen (with its library file), for where a stop happened. playback_last: Option<(Playback, Option<(i64, i64)>)>, + /// Stops the computer sleeping while something plays. + awake: crate::awake::KeepAwake, pub youtube: crate::youtube::YouTube, /// Bumped when something is removed from Continue Watching, so Home refreshes. pub continue_version: u64, @@ -390,6 +392,7 @@ impl Shared { ambient, playback_sent: None, playback_last: None, + awake: Default::default(), youtube: crate::youtube::YouTube::new(ctx), continue_version: 0, sync: None, @@ -1828,7 +1831,8 @@ impl Shared { } } - /// Handles plugin messages, tells plugins about playback and updates lights. + /// Handles plugin messages, tells plugins about playback, keeps the computer awake + /// while something plays and updates lights. /// `playing`: what the top screen is playing, if anything, and the library file. pub fn poll_plugins(&mut self, playing: Option<(Playback, Option<(i64, i64)>)>) { if let Some(web) = &self.web { @@ -1842,6 +1846,10 @@ impl Shared { let youtube = self.youtube.tools.is_some() && self.youtube_enabled(); web.set_status(flex_web::remote::Status { playing: now_playing, youtube }); } + let need = playing.as_ref().filter(|(p, _)| p.state == PlaybackState::Playing).map(|(p, _)| { + if p.kind == "video" { crate::awake::Need::Screen } else { crate::awake::Need::System } + }); + self.awake.set(need); let mut wait = self.plugins.poll(&self.db); let plugin_toasts = self.plugins.take_toasts(); self.toasts.extend(plugin_toasts); diff --git a/crates/flex-core/Cargo.toml b/crates/flex-core/Cargo.toml index c7244b8..83b33fb 100644 --- a/crates/flex-core/Cargo.toml +++ b/crates/flex-core/Cargo.toml @@ -7,7 +7,7 @@ publish = false [dependencies] anyhow = "1.0.104" directories = "6.0.0" -flex-plugin = { version = "0.16.0", path = "../flex-plugin" } +flex-plugin = { version = "0.16.1", path = "../flex-plugin" } hunch = "2" lofty = "0.25.4" log = "0.4.34" diff --git a/crates/flex-web/Cargo.toml b/crates/flex-web/Cargo.toml index 75d2d9b..7e05202 100644 --- a/crates/flex-web/Cargo.toml +++ b/crates/flex-web/Cargo.toml @@ -7,7 +7,7 @@ publish = false [dependencies] anyhow = "1.0.104" axum = "0.8.9" -flex-core = { version = "0.16.0", path = "../flex-core" } +flex-core = { version = "0.16.1", path = "../flex-core" } log = "0.4.34" serde = { version = "1.0.229", features = ["derive", "rc"] } serde_json = "1.0.151" diff --git a/plugins/anilist/Cargo.toml b/plugins/anilist/Cargo.toml index 292ae11..fedb288 100644 --- a/plugins/anilist/Cargo.toml +++ b/plugins/anilist/Cargo.toml @@ -7,5 +7,5 @@ license.workspace = true publish = false [dependencies] -flex-plugin = { version = "0.16.0", path = "../../crates/flex-plugin" } +flex-plugin = { version = "0.16.1", path = "../../crates/flex-plugin" } ureq = { version = "3", features = ["json"] } diff --git a/plugins/anilist/plugin.toml b/plugins/anilist/plugin.toml index 002e9c2..0502cc4 100644 --- a/plugins/anilist/plugin.toml +++ b/plugins/anilist/plugin.toml @@ -1,6 +1,6 @@ name = "AniList" description = "Updates your AniList progress as you watch anime in Flex" -version = "0.16.0" +version = "0.16.1" command = "flex-anilist" subscribe = ["playback"] diff --git a/plugins/lotus-lamp/Cargo.toml b/plugins/lotus-lamp/Cargo.toml index 0ce99cf..f6da222 100644 --- a/plugins/lotus-lamp/Cargo.toml +++ b/plugins/lotus-lamp/Cargo.toml @@ -7,6 +7,6 @@ license.workspace = true publish = false [dependencies] -flex-plugin = { version = "0.16.0", path = "../../crates/flex-plugin" } +flex-plugin = { version = "0.16.1", path = "../../crates/flex-plugin" } lotus-lamp = { git = "https://github.com/bombpersons/lotus-lamp-rs", rev = "59cf5ccaa0007f006f1812fd02cc5991aff11e72" } tokio = { version = "1.53.2", features = ["rt", "time", "sync", "macros"] } diff --git a/plugins/lotus-lamp/plugin.toml b/plugins/lotus-lamp/plugin.toml index e712c4f..0d4d655 100644 --- a/plugins/lotus-lamp/plugin.toml +++ b/plugins/lotus-lamp/plugin.toml @@ -1,6 +1,6 @@ name = "LED strip (Lotus Lamp)" description = "Lights a Bluetooth LED strip behind the TV with the colours on screen" -version = "0.16.0" +version = "0.16.1" command = "flex-lotus-lamp" subscribe = ["light"] diff --git a/plugins/trakt/Cargo.toml b/plugins/trakt/Cargo.toml index 27235c4..dc0cbd1 100644 --- a/plugins/trakt/Cargo.toml +++ b/plugins/trakt/Cargo.toml @@ -7,5 +7,5 @@ license.workspace = true publish = false [dependencies] -flex-plugin = { version = "0.16.0", path = "../../crates/flex-plugin" } +flex-plugin = { version = "0.16.1", path = "../../crates/flex-plugin" } ureq = { version = "3", features = ["json"] } diff --git a/plugins/trakt/plugin.toml b/plugins/trakt/plugin.toml index 9e5fcba..93e9c1d 100644 --- a/plugins/trakt/plugin.toml +++ b/plugins/trakt/plugin.toml @@ -1,6 +1,6 @@ name = "Trakt" description = "Keeps your Trakt history up to date with what you watch in Flex" -version = "0.16.0" +version = "0.16.1" command = "flex-trakt" subscribe = ["playback"]