7 Commits

Author SHA1 Message Date
vhaudiquet 953a920a23 bump version (0.3.0 start)
record-daemon / Build, check and test (push) Successful in 2m21s
2026-06-16 22:05:22 +02:00
vhaudiquet 146d7a5fb9 fix: fix default.json permissions 2026-06-16 21:55:49 +02:00
vhaudiquet 45aac067f8 feat: add option to launch app at Windows startup
record-daemon / Build, check and test (push) Successful in 2m23s
- Add tauri-plugin-autostart dependency to Cargo.toml
- Add autostart permissions to capabilities/default.json
- Initialize autostart plugin in lib.rs with LaunchAgent config
- Add "Launch at Startup" toggle in Settings.vue Daemon tab with state management and async enable/disable functions
2026-06-15 20:10:49 +02:00
vhaudiquet ba4ed63f4c fmt
record-daemon / Build, check and test (push) Successful in 2m38s
2026-06-15 19:52:38 +02:00
vhaudiquet 384ccda515 tryfix: reconnect to league client
record-daemon / Build, check and test (push) Failing after 7s
2026-06-05 21:17:15 +02:00
vhaudiquet 1c1b9c4d1a tryfix: reconnect to client on connection error
record-daemon / Build, check and test (push) Failing after 10s
2026-06-04 13:47:48 +02:00
vhaudiquet 642ad3e13a fix: enable assists by default on highlights
record-daemon / Build, check and test (push) Failing after 13m50s
2026-06-02 21:54:11 +02:00
13 changed files with 221 additions and 34 deletions
+1 -1
View File
@@ -2387,7 +2387,7 @@ dependencies = [
[[package]] [[package]]
name = "record-daemon" name = "record-daemon"
version = "0.1.0" version = "0.3.0"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"async-trait", "async-trait",
+1 -1
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "record-daemon" name = "record-daemon"
version = "0.1.0" version = "0.3.0"
edition = "2021" edition = "2021"
description = "High-performance League of Legends recording daemon using libobs" description = "High-performance League of Legends recording daemon using libobs"
authors = ["LeagueRecorder"] authors = ["LeagueRecorder"]
+30 -8
View File
@@ -31,6 +31,8 @@ pub struct LqpClient {
shutdown: Arc<RwLock<bool>>, shutdown: Arc<RwLock<bool>>,
/// Last emitted game ID for deduplication of GameStart events. /// Last emitted game ID for deduplication of GameStart events.
last_emitted_game_id: Arc<RwLock<Option<u64>>>, last_emitted_game_id: Arc<RwLock<Option<u64>>>,
/// WebSocket connection state (true if WebSocket is connected).
ws_connected: Arc<RwLock<bool>>,
} }
impl LqpClient { impl LqpClient {
@@ -50,6 +52,7 @@ impl LqpClient {
http_client, http_client,
shutdown: Arc::new(RwLock::new(false)), shutdown: Arc::new(RwLock::new(false)),
last_emitted_game_id: Arc::new(RwLock::new(None)), last_emitted_game_id: Arc::new(RwLock::new(None)),
ws_connected: Arc::new(RwLock::new(false)),
} }
} }
@@ -63,12 +66,20 @@ impl LqpClient {
self.state.read().await.clone() self.state.read().await.clone()
} }
/// Check if connected to League Client. /// Check if connected to League Client (has valid credentials).
pub async fn is_connected(&self) -> bool { pub async fn is_connected(&self) -> bool {
self.credentials.read().await.is_some() self.credentials.read().await.is_some()
} }
/// Check if WebSocket is connected.
pub async fn is_ws_connected(&self) -> bool {
*self.ws_connected.read().await
}
/// Connect to the League Client with the given credentials. /// Connect to the League Client with the given credentials.
///
/// This only stores credentials and verifies basic connectivity.
/// The actual WebSocket connection is established in start_event_listener().
pub async fn connect(&self, creds: LockfileCredentials) -> Result<()> { pub async fn connect(&self, creds: LockfileCredentials) -> Result<()> {
info!("Connecting to League Client at port {}", creds.port); info!("Connecting to League Client at port {}", creds.port);
@@ -85,12 +96,13 @@ impl LqpClient {
info!("Connected to League Client, current phase: {:?}", phase); info!("Connected to League Client, current phase: {:?}", phase);
} }
Err(e) => { Err(e) => {
warn!("Failed to verify connection: {}", e); warn!("Failed to verify connection via REST API: {}", e);
// Still consider connected, WebSocket might work // REST API might not be ready yet, but WebSocket could work
// Don't fail here - let start_event_listener() try the WebSocket
} }
} }
// Fetch local player's puuid for champion extraction // Fetch local player's puuid for champion extraction (best effort)
if let Ok(summoner) = self.get_summoner().await { if let Ok(summoner) = self.get_summoner().await {
if let Some(puuid) = summoner.get("puuid").and_then(|p| p.as_str()) { if let Some(puuid) = summoner.get("puuid").and_then(|p| p.as_str()) {
self.state.write().await.local_puuid = Some(puuid.to_string()); self.state.write().await.local_puuid = Some(puuid.to_string());
@@ -104,6 +116,7 @@ impl LqpClient {
/// Disconnect from the League Client. /// Disconnect from the League Client.
pub async fn disconnect(&self) { pub async fn disconnect(&self) {
*self.shutdown.write().await = true; *self.shutdown.write().await = true;
*self.ws_connected.write().await = false;
*self.credentials.write().await = None; *self.credentials.write().await = None;
*self.state.write().await = ClientState::default(); *self.state.write().await = ClientState::default();
*self.last_emitted_game_id.write().await = None; *self.last_emitted_game_id.write().await = None;
@@ -167,12 +180,16 @@ impl LqpClient {
} }
info!("All subscriptions sent"); info!("All subscriptions sent");
// Mark WebSocket as connected
*self.ws_connected.write().await = true;
// Clone references for the async block // Clone references for the async block
let event_sender = self.event_sender.clone(); let event_sender = self.event_sender.clone();
let state = self.state.clone(); let state = self.state.clone();
let shutdown = self.shutdown.clone(); let shutdown = self.shutdown.clone();
let credentials = self.credentials.clone(); let credentials = self.credentials.clone();
let last_emitted_game_id = self.last_emitted_game_id.clone(); let last_emitted_game_id = self.last_emitted_game_id.clone();
let ws_connected = self.ws_connected.clone();
// Spawn the message handler // Spawn the message handler
tokio::spawn(async move { tokio::spawn(async move {
@@ -296,9 +313,10 @@ impl LqpClient {
} }
} }
// Clear credentials on disconnect // Clear connection state on disconnect
*ws_connected.write().await = false;
*credentials.write().await = None; *credentials.write().await = None;
debug!("WebSocket listener ended"); info!("WebSocket listener ended");
}); });
Ok(()) Ok(())
@@ -503,12 +521,16 @@ impl LqpClient {
info!("Starting live client event poller"); info!("Starting live client event poller");
tokio::spawn(async move { tokio::spawn(async move {
// Small delay to ensure connection is stable
tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;
let mut last_event_id: Option<u64> = None; let mut last_event_id: Option<u64> = None;
let mut poll_count = 0u32; let mut poll_count = 0u32;
loop { loop {
if *shutdown.read().await { let is_shutdown = *shutdown.read().await;
info!("Live client event poller shutting down"); if is_shutdown {
info!("Live client event poller shutting down (shutdown flag is true)");
break; break;
} }
+50 -8
View File
@@ -206,16 +206,31 @@ impl Daemon {
match watcher.check()? { match watcher.check()? {
Some(true) => { Some(true) => {
// Client started // Client started - try to connect
info!("League Client detected"); info!("League Client detected");
self.state_machine
.transition(StateTransition::ClientStarted);
if let Some(creds) = watcher.credentials() { if let Some(creds) = watcher.credentials() {
self.lqp_client.connect(creds.clone()).await?; match self.lqp_client.connect(creds.clone()).await {
self.lqp_client.start_event_listener().await?; Ok(()) => {
// Start polling for live client events (kills, deaths, objectives) // Only transition to Monitoring after successful connection
self.lqp_client.start_live_client_event_poller().await; self.state_machine
.transition(StateTransition::ClientStarted);
if let Err(e) = self.lqp_client.start_event_listener().await {
warn!("Failed to start event listener: {}", e);
// Still stay in Monitoring, will retry on next check
} else {
// Start polling for live client events (kills, deaths, objectives)
self.lqp_client.start_live_client_event_poller().await;
}
}
Err(e) => {
warn!("Failed to connect to League Client: {}", e);
// Don't transition to Monitoring - will retry on next check
// The lockfile watcher will return None on next check since
// credentials are already set, so we need to handle reconnection
}
}
} }
} }
Some(false) => { Some(false) => {
@@ -225,7 +240,34 @@ impl Daemon {
.transition(StateTransition::ClientStopped); .transition(StateTransition::ClientStopped);
self.lqp_client.disconnect().await; self.lqp_client.disconnect().await;
} }
None => {} None => {
// No change in lockfile status
// Check if we need to reconnect (lockfile exists but WebSocket not connected)
if watcher.credentials().is_some() && !self.lqp_client.is_ws_connected().await {
info!("Lockfile exists but WebSocket not connected, attempting reconnect...");
if let Some(creds) = watcher.credentials() {
match self.lqp_client.connect(creds.clone()).await {
Ok(()) => {
// Transition to Monitoring if not already
if !self.state_machine.is_monitoring() {
self.state_machine
.transition(StateTransition::ClientStarted);
}
if let Err(e) = self.lqp_client.start_event_listener().await {
warn!("Failed to start event listener on reconnect: {}", e);
} else {
self.lqp_client.start_live_client_event_poller().await;
}
}
Err(e) => {
info!("Reconnect attempt failed: {}", e);
// Will retry on next check
}
}
}
}
}
} }
tokio::time::sleep(poll_interval).await; tokio::time::sleep(poll_interval).await;
+2 -2
View File
@@ -1,12 +1,12 @@
{ {
"name": "tauri-app", "name": "tauri-app",
"version": "0.1.0", "version": "0.3.0",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "tauri-app", "name": "tauri-app",
"version": "0.1.0", "version": "0.3.0",
"dependencies": { "dependencies": {
"@tauri-apps/api": "^2", "@tauri-apps/api": "^2",
"@tauri-apps/plugin-opener": "^2", "@tauri-apps/plugin-opener": "^2",
+1 -1
View File
@@ -1,7 +1,7 @@
{ {
"name": "tauri-app", "name": "tauri-app",
"private": true, "private": true,
"version": "0.1.0", "version": "0.3.0",
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "vite", "dev": "vite",
+75 -9
View File
@@ -216,6 +216,17 @@ version = "1.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0"
[[package]]
name = "auto-launch"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1f012b8cc0c850f34117ec8252a44418f2e34a2cf501de89e29b241ae5f79471"
dependencies = [
"dirs 4.0.0",
"thiserror 1.0.69",
"winreg 0.10.1",
]
[[package]] [[package]]
name = "autocfg" name = "autocfg"
version = "1.5.0" version = "1.5.0"
@@ -740,7 +751,16 @@ version = "6.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "16f5094c54661b38d03bd7e50df373292118db60b585c08a411c6d840017fe7d" checksum = "16f5094c54661b38d03bd7e50df373292118db60b585c08a411c6d840017fe7d"
dependencies = [ dependencies = [
"dirs-sys", "dirs-sys 0.5.0",
]
[[package]]
name = "dirs"
version = "4.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ca3aa72a6f96ea37bbc5aa912f6788242832f75369bdfdadcb0e38423f100059"
dependencies = [
"dirs-sys 0.3.7",
] ]
[[package]] [[package]]
@@ -749,7 +769,18 @@ version = "6.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c3e8aa94d75141228480295a7d0e7feb620b1a5ad9f12bc40be62411e38cce4e" checksum = "c3e8aa94d75141228480295a7d0e7feb620b1a5ad9f12bc40be62411e38cce4e"
dependencies = [ dependencies = [
"dirs-sys", "dirs-sys 0.5.0",
]
[[package]]
name = "dirs-sys"
version = "0.3.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1b1d1d91c932ef41c0f2663aa8b0ca0342d444d842c06914aa0a7e352d0bada6"
dependencies = [
"libc",
"redox_users 0.4.6",
"winapi",
] ]
[[package]] [[package]]
@@ -760,7 +791,7 @@ checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab"
dependencies = [ dependencies = [
"libc", "libc",
"option-ext", "option-ext",
"redox_users", "redox_users 0.5.2",
"windows-sys 0.61.2", "windows-sys 0.61.2",
] ]
@@ -872,7 +903,7 @@ dependencies = [
"rustc_version", "rustc_version",
"toml 0.9.12+spec-1.1.0", "toml 0.9.12+spec-1.1.0",
"vswhom", "vswhom",
"winreg", "winreg 0.55.0",
] ]
[[package]] [[package]]
@@ -1962,7 +1993,7 @@ dependencies = [
[[package]] [[package]]
name = "leaguerecorder" name = "leaguerecorder"
version = "0.1.0" version = "0.3.0"
dependencies = [ dependencies = [
"chrono", "chrono",
"directories", "directories",
@@ -1971,6 +2002,7 @@ dependencies = [
"serde_json", "serde_json",
"tauri", "tauri",
"tauri-build", "tauri-build",
"tauri-plugin-autostart",
"tauri-plugin-opener", "tauri-plugin-opener",
"tokio", "tokio",
"uuid", "uuid",
@@ -2990,6 +3022,17 @@ dependencies = [
"bitflags 2.11.0", "bitflags 2.11.0",
] ]
[[package]]
name = "redox_users"
version = "0.4.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43"
dependencies = [
"getrandom 0.2.17",
"libredox",
"thiserror 1.0.69",
]
[[package]] [[package]]
name = "redox_users" name = "redox_users"
version = "0.5.2" version = "0.5.2"
@@ -3770,7 +3813,7 @@ dependencies = [
"anyhow", "anyhow",
"bytes", "bytes",
"cookie", "cookie",
"dirs", "dirs 6.0.0",
"dunce", "dunce",
"embed_plist", "embed_plist",
"getrandom 0.3.4", "getrandom 0.3.4",
@@ -3821,7 +3864,7 @@ checksum = "4bbc990d1dbf57a8e1c7fa2327f2a614d8b757805603c1b9ba5c81bade09fd4d"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"cargo_toml", "cargo_toml",
"dirs", "dirs 6.0.0",
"glob", "glob",
"heck 0.5.0", "heck 0.5.0",
"json-patch", "json-patch",
@@ -3893,6 +3936,20 @@ dependencies = [
"walkdir", "walkdir",
] ]
[[package]]
name = "tauri-plugin-autostart"
version = "2.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "459383cebc193cdd03d1ba4acc40f2c408a7abce419d64bdcd2d745bc2886f70"
dependencies = [
"auto-launch",
"serde",
"serde_json",
"tauri",
"tauri-plugin",
"thiserror 2.0.18",
]
[[package]] [[package]]
name = "tauri-plugin-opener" name = "tauri-plugin-opener"
version = "2.5.3" version = "2.5.3"
@@ -4345,7 +4402,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a5e85aa143ceb072062fc4d6356c1b520a51d636e7bc8e77ec94be3608e5e80c" checksum = "a5e85aa143ceb072062fc4d6356c1b520a51d636e7bc8e77ec94be3608e5e80c"
dependencies = [ dependencies = [
"crossbeam-channel", "crossbeam-channel",
"dirs", "dirs 6.0.0",
"libappindicator", "libappindicator",
"muda", "muda",
"objc2", "objc2",
@@ -5297,6 +5354,15 @@ dependencies = [
"memchr", "memchr",
] ]
[[package]]
name = "winreg"
version = "0.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "80d0f4e272c85def139476380b12f9ac60926689dd2e01d4923222f40580869d"
dependencies = [
"winapi",
]
[[package]] [[package]]
name = "winreg" name = "winreg"
version = "0.55.0" version = "0.55.0"
@@ -5411,7 +5477,7 @@ dependencies = [
"block2", "block2",
"cookie", "cookie",
"crossbeam-channel", "crossbeam-channel",
"dirs", "dirs 6.0.0",
"dom_query", "dom_query",
"dpi", "dpi",
"dunce", "dunce",
+2 -1
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "leaguerecorder" name = "leaguerecorder"
version = "0.1.0" version = "0.3.0"
description = "A Tauri App" description = "A Tauri App"
authors = ["you"] authors = ["you"]
edition = "2021" edition = "2021"
@@ -20,6 +20,7 @@ tauri-build = { version = "2", features = [] }
[dependencies] [dependencies]
tauri = { version = "2", features = ["protocol-asset"] } tauri = { version = "2", features = ["protocol-asset"] }
tauri-plugin-opener = "2" tauri-plugin-opener = "2"
tauri-plugin-autostart = "2"
serde = { version = "1", features = ["derive"] } serde = { version = "1", features = ["derive"] }
serde_json = "1" serde_json = "1"
chrono = { version = "0.4", features = ["serde"] } chrono = { version = "0.4", features = ["serde"] }
@@ -5,6 +5,9 @@
"windows": ["main"], "windows": ["main"],
"permissions": [ "permissions": [
"core:default", "core:default",
"opener:default" "opener:default",
"autostart:allow-enable",
"autostart:allow-disable",
"autostart:allow-is-enabled"
] ]
} }
+4
View File
@@ -395,6 +395,10 @@ fn get_video_metadata(video_path: String) -> Result<Value, String> {
pub fn run() { pub fn run() {
tauri::Builder::default() tauri::Builder::default()
.plugin(tauri_plugin_opener::init()) .plugin(tauri_plugin_opener::init())
.plugin(tauri_plugin_autostart::init(
tauri_plugin_autostart::MacosLauncher::LaunchAgent,
Some(vec!["--hidden"]),
))
.setup(|app| { .setup(|app| {
// Auto-start the record-daemon on app launch. // Auto-start the record-daemon on app launch.
// We spawn a background task so the UI isn't blocked while the // We spawn a background task so the UI isn't blocked while the
+1 -1
View File
@@ -1,7 +1,7 @@
{ {
"$schema": "https://schema.tauri.app/config/2", "$schema": "https://schema.tauri.app/config/2",
"productName": "leaguerecorder", "productName": "leaguerecorder",
"version": "0.2.0", "version": "0.3.0",
"identifier": "v.leaguerecorder", "identifier": "v.leaguerecorder",
"build": { "build": {
"beforeDevCommand": "npm run dev", "beforeDevCommand": "npm run dev",
+49
View File
@@ -37,6 +37,8 @@ const daemonStatus = ref<DaemonStatusResponse | null>(null);
const encoders = ref<EncodersResponse | null>(null); const encoders = ref<EncodersResponse | null>(null);
const daemonRunning = ref(false); const daemonRunning = ref(false);
const statusPolling = ref<ReturnType<typeof setInterval> | null>(null); const statusPolling = ref<ReturnType<typeof setInterval> | null>(null);
const autostartEnabled = ref(false);
const autostartLoading = ref(false);
// Active settings tab // Active settings tab
type SettingsTab = "video" | "output" | "audio" | "daemon"; type SettingsTab = "video" | "output" | "audio" | "daemon";
@@ -97,6 +99,35 @@ async function refreshStatus() {
} }
} }
// ---------------------------------------------------------------------------
// Autostart management
// ---------------------------------------------------------------------------
async function checkAutostart() {
try {
autostartEnabled.value = await invoke<boolean>("plugin:autostart|is_enabled");
} catch {
autostartEnabled.value = false;
}
}
async function toggleAutostart() {
autostartLoading.value = true;
try {
if (autostartEnabled.value) {
await invoke("plugin:autostart|disable");
autostartEnabled.value = false;
} else {
await invoke("plugin:autostart|enable");
autostartEnabled.value = true;
}
} catch (e) {
error.value = `Failed to ${autostartEnabled.value ? 'disable' : 'enable'} autostart: ${e}`;
} finally {
autostartLoading.value = false;
}
}
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// Save settings // Save settings
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
@@ -240,6 +271,7 @@ const statusColor = computed(() => {
onMounted(async () => { onMounted(async () => {
await loadSettings(); await loadSettings();
await checkAutostart();
// Poll daemon status every 3 seconds // Poll daemon status every 3 seconds
statusPolling.value = setInterval(refreshStatus, 3000); statusPolling.value = setInterval(refreshStatus, 3000);
}); });
@@ -651,6 +683,23 @@ onUnmounted(() => {
<!-- Daemon tab --> <!-- Daemon tab -->
<div v-if="activeTab === 'daemon'" class="tab-content"> <div v-if="activeTab === 'daemon'" class="tab-content">
<section class="settings-section">
<h3>Startup</h3>
<div class="form-group">
<label class="form-label">Launch at Startup</label>
<label class="toggle">
<input
type="checkbox"
:checked="autostartEnabled"
:disabled="autostartLoading"
@change="toggleAutostart"
/>
<span class="toggle-slider"></span>
</label>
<span class="form-hint">Automatically launch League Recorder when you log in to Windows</span>
</div>
</section>
<section class="settings-section"> <section class="settings-section">
<h3>Daemon Behavior</h3> <h3>Daemon Behavior</h3>
<div class="form-group"> <div class="form-group">
+1 -1
View File
@@ -755,7 +755,7 @@ export const DEFAULT_HIGHLIGHT_SETTINGS: HighlightSettings = {
buffer_before: 10, buffer_before: 10,
buffer_after: 3, buffer_after: 3,
min_duration: 5, min_duration: 5,
included_types: ["kill", "death", "multi_kill", "trade_kill"], included_types: ["kill", "death", "multi_kill", "trade_kill", "assist"],
merge_overlapping: true, merge_overlapping: true,
merge_gap_secs: 5, merge_gap_secs: 5,
}; };