diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index b2f377fa8..9ade9816b 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -9,7 +9,9 @@ dependencies = [ "chrono", "fix-path-env", "flate2", + "libc", "log", + "once_cell", "quick-xml 0.31.0", "reqwest 0.11.27", "serde", diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index c68f1ae40..251b3547c 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -37,6 +37,8 @@ reqwest = { version = "0.11", features = ["json", "gzip"] } flate2 = "1.0" quick-xml = { version = "0.31", features = ["serialize"] } tauri-plugin-sql = { version = "2", features = ["sqlite"] } +once_cell = "1.19.0" +libc = "0.2" [target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies] tauri-plugin-updater = "2" diff --git a/src-tauri/src/commands/media.rs b/src-tauri/src/commands/media.rs index 442301859..6574fcc22 100644 --- a/src-tauri/src/commands/media.rs +++ b/src-tauri/src/commands/media.rs @@ -1,9 +1,36 @@ use log::info; +use once_cell::sync::Lazy; +use serde::Serialize; +use std::collections::HashMap; use std::path::Path; use std::process::Command; +use std::sync::Mutex; +use std::thread; +use std::time::{SystemTime, UNIX_EPOCH}; +use tauri::Emitter; +use tauri::Runtime; + +#[derive(Debug, Serialize, Clone)] +pub struct MpvProcess { + id: u32, + url: String, + start_time: u64, + last_known_time: Option, + title: String, + thumbnail: Option, +} + +static MPV_PROCESSES: Lazy>> = + Lazy::new(|| Mutex::new(HashMap::new())); #[tauri::command] -pub async fn open_in_mpv(url: String, path: String) -> Result<(), String> { +pub async fn open_in_mpv( + url: String, + path: String, + title: String, + thumbnail: Option, + app_handle: tauri::AppHandle, +) -> Result { info!("Custom MVP path: {}", path); let mpv_paths = if cfg!(target_os = "windows") { vec![ @@ -32,16 +59,108 @@ pub async fn open_in_mpv(url: String, path: String) -> Result<(), String> { info!("Using the following MPV player path: {}", mpv_path); - Command::new(mpv_path) + // Add --input-ipc-server for IPC communication + let ipc_socket = format!( + "/tmp/mpv-socket-{}", + SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_secs() + ); + + let child = Command::new(mpv_path) + .arg(format!("--input-ipc-server={}", ipc_socket)) .arg(&url) .spawn() .map_err(|e| e.to_string())?; - Ok(()) + let process_id = child.id(); + + // Clone app_handle for the monitoring thread + let app_handle_clone = app_handle.clone(); + + // Spawn a thread to monitor the process + thread::spawn(move || { + let _ = child.wait_with_output(); // Wait for the process to exit + + // Process has exited, remove it from our map and notify frontend + if let Some(process) = MPV_PROCESSES.lock().unwrap().remove(&process_id) { + let _ = app_handle_clone.emit("mpv-process-removed", process); + } + }); + + // Store process information + let mpv_process = MpvProcess { + id: process_id, + url: url.clone(), + start_time: SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_secs(), + last_known_time: None, + title, + thumbnail, + }; + + MPV_PROCESSES + .lock() + .unwrap() + .insert(process_id, mpv_process.clone()); + + // Updated event emission + app_handle + .emit("mpv-process-added", mpv_process) + .map_err(|e| e.to_string())?; + + Ok(process_id) } #[tauri::command] -pub async fn open_in_vlc(url: String, path: String) -> Result<(), String> { +pub async fn get_active_mpv_processes() -> Vec { + MPV_PROCESSES.lock().unwrap().values().cloned().collect() +} + +#[tauri::command] +pub async fn close_mpv_process( + process_id: u32, + app_handle: tauri::AppHandle, +) -> Result<(), String> { + if let Some(process) = MPV_PROCESSES.lock().unwrap().remove(&process_id) { + #[cfg(unix)] + { + unsafe { + libc::kill(process_id as i32, libc::SIGTERM); + } + } + + #[cfg(windows)] + { + unsafe { + let handle = OpenProcess(PROCESS_TERMINATE, 0, process_id); + if !handle.is_null() { + TerminateProcess(handle, 1); + CloseHandle(handle); + } + } + } + + // Updated event emission + app_handle + .emit("mpv-process-removed", process) + .map_err(|e| e.to_string())?; + + Ok(()) + } else { + Err("Process not found".to_string()) + } +} + +#[tauri::command] +pub async fn open_in_vlc( + url: String, + path: String, + _app_handle: tauri::AppHandle, +) -> Result<(), String> { info!("Custom MVP path: {}", path); let vlc_paths = if cfg!(target_os = "windows") { vec![ diff --git a/src-tauri/src/database.rs b/src-tauri/src/database.rs index b94c18c4d..dde0db0ad 100644 --- a/src-tauri/src/database.rs +++ b/src-tauri/src/database.rs @@ -3,38 +3,72 @@ use tauri_plugin_sql::{Migration, MigrationKind}; pub fn get_migrations() -> Vec { vec![Migration { version: 1, - description: "Create initial tables for Xtream API data", + description: "Create initial tables for playlist data", kind: MigrationKind::Up, sql: " - CREATE TABLE IF NOT EXISTS playlists ( - id TEXT PRIMARY KEY, - name TEXT NOT NULL, - api_url TEXT NOT NULL, - username TEXT NOT NULL, - password TEXT NOT NULL, - last_updated DATETIME - ); - - CREATE TABLE IF NOT EXISTS categories ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - playlist_id TEXT NOT NULL, - name TEXT NOT NULL, - type TEXT NOT NULL CHECK (type IN ('live', 'movies', 'series')), - xtream_id INTEGER NOT NULL, - FOREIGN KEY (playlist_id) REFERENCES playlists (id) ON DELETE CASCADE - ); - - CREATE TABLE IF NOT EXISTS content ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - category_id INTEGER NOT NULL, - title TEXT NOT NULL, - rating TEXT, - added TEXT, - poster_url TEXT, - xtream_id INTEGER NOT NULL, - type TEXT NOT NULL CHECK (type IN ('live', 'movie', 'series')), - FOREIGN KEY (category_id) REFERENCES categories (id) ON DELETE CASCADE - ); - ", + CREATE TABLE IF NOT EXISTS playlists ( + id TEXT PRIMARY KEY, + name TEXT NOT NULL, + serverUrl TEXT, + username TEXT, + password TEXT, + date_created DATETIME DEFAULT CURRENT_TIMESTAMP, + last_updated DATETIME, + type TEXT NOT NULL CHECK (type IN ('xtream', 'stalker', 'm3u-file', 'm3u-text', 'm3u-url')), + userAgent TEXT, + filePath TEXT, + autoRefresh BOOLEAN DEFAULT 0, + macAddress TEXT, + url TEXT, + last_usage DATETIME + ); + + CREATE TABLE IF NOT EXISTS categories ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + playlist_id TEXT NOT NULL, + name TEXT NOT NULL, + type TEXT NOT NULL CHECK (type IN ('live', 'movies', 'series')), + xtream_id INTEGER NOT NULL, + FOREIGN KEY (playlist_id) REFERENCES playlists (id) ON DELETE CASCADE + ); + + CREATE TABLE IF NOT EXISTS content ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + category_id INTEGER NOT NULL, + title TEXT NOT NULL, + rating TEXT, + added TEXT, + poster_url TEXT, + xtream_id INTEGER NOT NULL, + type TEXT NOT NULL CHECK (type IN ('live', 'movie', 'series')), + FOREIGN KEY (category_id) REFERENCES categories (id) ON DELETE CASCADE + ); + + CREATE TABLE IF NOT EXISTS recently_viewed ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + content_id INTEGER NOT NULL, + playlist_id TEXT NOT NULL, + viewed_at DATETIME DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (content_id) REFERENCES content (id) ON DELETE CASCADE, + FOREIGN KEY (playlist_id) REFERENCES playlists (id) ON DELETE CASCADE + ); + + CREATE TABLE IF NOT EXISTS favorites ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + content_id INTEGER NOT NULL, + playlist_id TEXT NOT NULL, + added_at DATETIME DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (content_id) REFERENCES content(id) ON DELETE CASCADE, + FOREIGN KEY (playlist_id) REFERENCES playlists(id) ON DELETE CASCADE + ); + + -- Create indexes for better query performance + CREATE INDEX IF NOT EXISTS idx_content_type ON content(type); + CREATE INDEX IF NOT EXISTS idx_content_category ON content(category_id); + CREATE INDEX IF NOT EXISTS idx_categories_playlist ON categories(playlist_id); + CREATE INDEX IF NOT EXISTS idx_content_title ON content(title); + CREATE INDEX IF NOT EXISTS idx_content_xtream ON content(xtream_id); + CREATE INDEX IF NOT EXISTS idx_categories_type ON categories(type); + ", }] } diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index cae549bdb..65650f11a 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -27,7 +27,9 @@ pub fn run() { commands::epg::get_channel_programs, commands::epg::get_epg_by_range, commands::media::open_in_mpv, - commands::media::open_in_vlc + commands::media::open_in_vlc, + commands::media::get_active_mpv_processes, + commands::media::close_mpv_process ]) .setup(|app| { if cfg!(debug_assertions) {