diff --git a/Cargo.lock b/Cargo.lock index 59882b5..78c2533 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -32,6 +32,21 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4aa90d7ce82d4be67b64039a3d588d38dbcc6736577de4a847025ce5b0c468d1" +[[package]] +name = "android-tzdata" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + [[package]] name = "anyhow" version = "1.0.90" @@ -196,6 +211,20 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "chrono" +version = "0.4.38" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a21f936df1771bf62b77f047b726c4625ff2e8aa607c01ec06e5a05bd8463401" +dependencies = [ + "android-tzdata", + "iana-time-zone", + "js-sys", + "num-traits", + "wasm-bindgen", + "windows-targets 0.52.6", +] + [[package]] name = "color_quant" version = "1.1.0" @@ -604,6 +633,29 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" +[[package]] +name = "iana-time-zone" +version = "0.1.61" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "235e081f3925a06703c2d0117ea8b91f042756fd6e7a6e5d901e8ca1a996b220" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "wasm-bindgen", + "windows-core 0.52.0", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + [[package]] name = "icrate" version = "0.1.2" @@ -736,6 +788,15 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f5d4a7da358eff58addd2877a45865158f0d78c911d43a5784ceb7bbf52833b0" +[[package]] +name = "js-sys" +version = "0.3.72" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a88f1bda2bd75b0452a14784937d796722fdebfe50df998aeb3f0b7603019a9" +dependencies = [ + "wasm-bindgen", +] + [[package]] name = "lazy_static" version = "1.5.0" @@ -1700,6 +1761,7 @@ checksum = "65fc09f10666a9f147042251e0dda9c18f166ff7de300607007e96bdebc1068d" name = "webfishing-midi" version = "1.2.0" dependencies = [ + "chrono", "device_query", "dialoguer", "enigo", @@ -1771,6 +1833,15 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "windows-core" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" +dependencies = [ + "windows-targets 0.52.6", +] + [[package]] name = "windows-core" version = "0.56.0" diff --git a/Cargo.toml b/Cargo.toml index adb5db8..64db488 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,6 +15,7 @@ indicatif = "0.17.8" indicatif-log-bridge = "0.2.3" tabled = "0.16.0" rusqlite = { version = "0.32.1", features = ["bundled"] } +chrono = "0.4.38" [features] default = [] diff --git a/src/main.rs b/src/main.rs index ff885b1..02ddc73 100644 --- a/src/main.rs +++ b/src/main.rs @@ -13,6 +13,7 @@ use std::{fs, io::stdin, path::Path, path::PathBuf, process::exit}; use tabled::{builder::Builder, settings::Style}; use webfishing_player::{PlayerSettings, WebfishingPlayer}; use xcap::Window; +use chrono::{Local, NaiveTime, Timelike}; const MIDI_DIR: &str = "./midi"; const WINDOW_NAMES: [&str; 3] = ["steam_app_3146520", "Fish! (On the WEB!)", "Godot_Engine"]; @@ -83,7 +84,7 @@ fn main() -> Result<(), Box> { } }; - let (should_sing, loop_midi, add_another_song, playback_speed) = get_user_options(&theme)?; + let (should_sing, loop_midi, add_another_song, playback_speed, start_time) = get_user_options(&theme)?; let mut sing_above: u8 = 60; if should_sing { @@ -95,7 +96,7 @@ fn main() -> Result<(), Box> { } // Add the selected song to the queue - let mut settings = match PlayerSettings::new(midi_data, loop_midi, should_sing, sing_above, playback_speed) { + let mut settings = match PlayerSettings::new(midi_data, loop_midi, should_sing, sing_above, playback_speed, start_time) { Ok(settings) => settings, Err(e) => { error!("Failed to parse MIDI data: {}", e); @@ -116,7 +117,7 @@ fn main() -> Result<(), Box> { // Play all songs in the queue for (index, settings) in song_queue.into_iter().enumerate() { - let is_first_song = index == 0; + let is_first_song = index == 0 && !settings.start_time.is_some(); let mut player = match WebfishingPlayer::new( settings, @@ -148,12 +149,13 @@ fn main() -> Result<(), Box> { Ok(()) } -fn get_user_options(theme: &ColorfulTheme) -> Result<(bool, bool, bool, f64), dialoguer::Error> { +fn get_user_options(theme: &ColorfulTheme) -> Result<(bool, bool, bool, f64, Option), dialoguer::Error> { let options = vec![ "Sing along", "Loop the song", "Queue another song", "Set playback speed", + "Set start time", ]; let selected_options = MultiSelect::with_theme(theme) @@ -165,7 +167,9 @@ fn get_user_options(theme: &ColorfulTheme) -> Result<(bool, bool, bool, f64), di let loop_midi = selected_options.contains(&1); let add_another_song = selected_options.contains(&2); let mut playback_speed = 1.0; + let mut start_time: Option = None; + // Playback speed if selected_options.contains(&3) { let speed_input = Input::with_theme(theme) .with_prompt("Enter playback speed:") @@ -175,6 +179,39 @@ fn get_user_options(theme: &ColorfulTheme) -> Result<(bool, bool, bool, f64), di playback_speed = speed_input.trim().parse().unwrap_or(1.0); } + // Start time + if selected_options.contains(&4) { + // Get the next whole minute to use as default + let now = Local::now(); + let next_minute = now + chrono::Duration::seconds(60 - now.second() as i64); + let default_time = next_minute.format("%H:%M:%S").to_string(); + let time_input = Input::with_theme(theme) + .with_prompt("Enter start time (HH:MM:SS):") + .default(default_time) // Set the default to the next whole minute + .interact_text()?; + + if let Ok(naive_time) = NaiveTime::parse_from_str(&time_input, "%H:%M:%S") { + let current_date = Local::now().date(); + let start_datetime = current_date.and_time(naive_time); + start_time = Some(start_datetime.expect("idk what to put here but otherwise it doesn't work").timestamp() as u64 * 1000); + + let delay_input = Input::with_theme(theme) + .with_prompt("Enter delay in ms to account for latency (Ping to host):") + .default("0".to_string()) + .interact_text()?; + + if let Ok(delay) = delay_input.trim().parse::() { + if let Some(existing_start_time) = start_time { + start_time = Some(existing_start_time + delay); + } + } else { + println!("Invalid delay input. No delay will be added."); + } + } else { + println!("Invalid time format. Please use HH:MM:SS."); + } + } + // Check for conflicting options if loop_midi && add_another_song { let confirm = dialoguer::Confirm::with_theme(theme) @@ -183,13 +220,13 @@ fn get_user_options(theme: &ColorfulTheme) -> Result<(bool, bool, bool, f64), di .interact()?; if confirm { - return Ok((should_sing, true, false, playback_speed)) + return Ok((should_sing, true, false, playback_speed, start_time)) } else { return get_user_options(theme); } } - Ok((should_sing, loop_midi, add_another_song, playback_speed)) + Ok((should_sing, loop_midi, add_another_song, playback_speed, start_time)) } fn get_window(name: &str) -> Option { diff --git a/src/webfishing_player.rs b/src/webfishing_player.rs index ccead60..477536f 100644 --- a/src/webfishing_player.rs +++ b/src/webfishing_player.rs @@ -27,7 +27,7 @@ use std::{ Arc, }, thread::sleep, - time::{Duration, Instant}, + time::{Duration, Instant, SystemTime}, }; use xcap::Window; @@ -92,10 +92,11 @@ pub struct PlayerSettings<'a> { pub sing_above: u8, pub tracks: Option>, pub playback_speed: f64, + pub start_time: Option, } impl<'a> PlayerSettings<'a> { - pub fn new(midi_data: Vec, loop_midi: bool, should_sing: bool, sing_above: u8, playback_speed: f64) -> Result { + pub fn new(midi_data: Vec, loop_midi: bool, should_sing: bool, sing_above: u8, playback_speed: f64, start_time: Option) -> Result { let smf = Smf::parse(&midi_data)?; // This is safe because we keep midi_data & smf alive in the struct let smf = unsafe { std::mem::transmute::, Smf<'a>>(smf) }; @@ -108,6 +109,7 @@ impl<'a> PlayerSettings<'a> { sing_above, tracks: None, playback_speed, + start_time, }) } } @@ -129,6 +131,7 @@ pub struct WebfishingPlayer<'a> { sing_above: u8, tracks: Vec, playback_speed: f64, + start_time: Option, multi: &'a MultiProgress, paused: Arc, song_elapsed_micros: Arc, @@ -187,6 +190,7 @@ impl<'a> WebfishingPlayer<'a> { sing_above: settings.sing_above, tracks: settings.tracks.unwrap_or(Vec::new()), playback_speed: settings.playback_speed, + start_time: settings.start_time, multi, paused: Arc::new(AtomicBool::new(false)), song_elapsed_micros: Arc::new(AtomicU64::new(0)), @@ -325,6 +329,10 @@ impl<'a> WebfishingPlayer<'a> { if self.wait_for_user { // Attempt to press space in-case the user's OS requires a permission pop-up for input self.enigo.key(Key::Space, Click).unwrap(); + + #[cfg(feature = "silent_input")] + println!("Press backspace to start playing"); + #[cfg(not(feature = "silent_input"))] println!("Tab over to the game and press backspace to start playing"); loop { if device_state.get_keys().contains(&Keycode::Backspace) { @@ -332,6 +340,27 @@ impl<'a> WebfishingPlayer<'a> { } } } + else { + // Wait to start at a certain timestamp if provided + if let Some(start_time) = self.start_time { + let current_time = SystemTime::now() + .duration_since(SystemTime::UNIX_EPOCH) + .expect("Time went backwards") + .as_millis(); + + if u128::from(start_time) > current_time { + let wait_duration = Duration::from_millis((start_time as u128 - current_time) as u64); + let wait_seconds = wait_duration.as_secs(); + + #[cfg(feature = "silent_input")] + println!("Starting playback in {} seconds...", wait_seconds); + #[cfg(not(feature = "silent_input"))] + println!("Tab over to the game, starting playback in {} seconds...", wait_seconds); + + std::thread::sleep(wait_duration); + } + } + } // Reset the guitar to all open string self.set_fret(6, 0);