From ecc8380645ef1b1918cc7b176b9dcc95bf14395e Mon Sep 17 00:00:00 2001 From: Ryan Walters Date: Wed, 20 Aug 2025 02:20:24 -0500 Subject: [PATCH] refactor: reorganize backend tauri code --- src-tauri/src/ff.rs | 47 ++++++++ src-tauri/src/lib.rs | 248 ++-------------------------------------- src-tauri/src/media.rs | 141 +++++++++++++++++++++++ src-tauri/src/models.rs | 56 +++++++++ 4 files changed, 252 insertions(+), 240 deletions(-) create mode 100644 src-tauri/src/ff.rs create mode 100644 src-tauri/src/media.rs create mode 100644 src-tauri/src/models.rs diff --git a/src-tauri/src/ff.rs b/src-tauri/src/ff.rs new file mode 100644 index 0000000..6249bf7 --- /dev/null +++ b/src-tauri/src/ff.rs @@ -0,0 +1,47 @@ +use crate::models::StreamDetail; + +pub fn extract_streams(info: &ffprobe::FfProbe) -> Vec { + let mut streams = Vec::new(); + + for stream in &info.streams { + match stream.codec_type.as_deref() { + Some("video") => { + streams.push(StreamDetail::Video { + codec: stream + .codec_name + .clone() + .unwrap_or_else(|| "unknown".to_string()), + width: stream.width.map(|w| w as u32), + height: stream.height.map(|h| h as u32), + bit_rate: stream.bit_rate.as_ref().map(|b| b.to_string()), + frame_rate: Some(stream.r_frame_rate.clone()), + }); + } + Some("audio") => { + streams.push(StreamDetail::Audio { + codec: stream + .codec_name + .clone() + .unwrap_or_else(|| "unknown".to_string()), + sample_rate: stream.sample_rate.clone(), + channels: stream.channels.map(|c| c as u32), + bit_rate: stream.bit_rate.as_ref().map(|b| b.to_string()), + }); + } + Some("subtitle") => { + streams.push(StreamDetail::Subtitle { + codec: stream + .codec_name + .clone() + .unwrap_or_else(|| "unknown".to_string()), + language: stream.tags.as_ref().and_then(|tags| tags.language.clone()), + }); + } + _ => {} + } + } + + streams +} + + diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index bdeaaa7..e2c768f 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -1,245 +1,13 @@ -use serde::{Deserialize, Serialize}; -use std::fs::File; -use std::io::Read; +mod ff; +mod media; +mod models; + use std::path::Path; -use ts_rs::TS; +use models::{MediaType, StreamDetail, StreamResult, StreamResultError}; +use media::{detect_media_type, is_media_file}; +use ff::extract_streams; -#[derive(Serialize, Deserialize, Debug, Clone, TS)] -#[ts(export)] -enum MediaType { - Audio, - Video, - Image, - Document, - Executable, - Archive, - Library, - Unknown, -} - -#[derive(Serialize, Deserialize, Debug, Clone, TS)] -#[ts(export)] -struct StreamResult { - path: String, - filename: String, - media_type: MediaType, - duration: Option, - size: u64, - streams: Vec, -} - -#[derive(Serialize, Deserialize, Debug, Clone, TS)] -#[ts(export)] -enum StreamDetail { - Video { - codec: String, - width: Option, - height: Option, - bit_rate: Option, - frame_rate: Option, - }, - Audio { - codec: String, - sample_rate: Option, - channels: Option, - bit_rate: Option, - }, - Subtitle { - codec: String, - language: Option, - }, -} - -#[derive(Serialize, Deserialize, Debug, Clone, TS)] -#[ts(export)] -struct StreamResultError { - filename: Option, - reason: String, - error_type: String, -} - -fn detect_media_type(path: &Path) -> MediaType { - // First try to detect using infer crate (magic number detection) - if let Ok(mut file) = File::open(path) { - let mut buffer = [0; 512]; // Read first 512 bytes for magic number detection - if let Ok(bytes_read) = file.read(&mut buffer) { - if let Some(kind) = infer::get(&buffer[..bytes_read]) { - return match kind.mime_type() { - // Audio types - "audio/mpeg" | "audio/mp3" | "audio/m4a" | "audio/ogg" | "audio/x-flac" - | "audio/x-wav" | "audio/amr" | "audio/aac" | "audio/x-aiff" - | "audio/x-dsf" | "audio/x-ape" | "audio/midi" => MediaType::Audio, - - // Video types - "video/mp4" | "video/x-m4v" | "video/x-matroska" | "video/webm" - | "video/quicktime" | "video/x-msvideo" | "video/x-ms-wmv" | "video/mpeg" - | "video/x-flv" => MediaType::Video, - - // Image types - "image/jpeg" - | "image/png" - | "image/gif" - | "image/webp" - | "image/x-canon-cr2" - | "image/tiff" - | "image/bmp" - | "image/heif" - | "image/avif" - | "image/vnd.ms-photo" - | "image/vnd.adobe.photoshop" - | "image/vnd.microsoft.icon" - | "image/openraster" - | "image/vnd.djvu" => MediaType::Image, - - // Document types - "application/pdf" - | "application/rtf" - | "application/msword" - | "application/vnd.openxmlformats-officedocument.wordprocessingml.document" - | "application/vnd.ms-excel" - | "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" - | "application/vnd.ms-powerpoint" - | "application/vnd.openxmlformats-officedocument.presentationml.presentation" - | "application/vnd.oasis.opendocument.text" - | "application/vnd.oasis.opendocument.spreadsheet" - | "application/vnd.oasis.opendocument.presentation" => MediaType::Document, - - // Archive types - "application/zip" - | "application/x-tar" - | "application/vnd.rar" - | "application/gzip" - | "application/x-bzip2" - | "application/vnd.bzip3" - | "application/x-7z-compressed" - | "application/x-xz" - | "application/x-shockwave-flash" - | "application/octet-stream" - | "application/postscript" - | "application/vnd.sqlite3" - | "application/x-nintendo-nes-rom" - | "application/x-google-chrome-extension" - | "application/vnd.ms-cab-compressed" - | "application/vnd.debian.binary-package" - | "application/x-unix-archive" - | "application/x-compress" - | "application/x-lzip" - | "application/x-rpm" - | "application/dicom" - | "application/zstd" - | "application/x-lz4" - | "application/x-ole-storage" - | "application/x-cpio" - | "application/x-par2" - | "application/epub+zip" - | "application/x-mobipocket-ebook" => MediaType::Archive, - - // Executable types - "application/vnd.microsoft.portable-executable" - | "application/x-executable" - | "application/llvm" - | "application/x-mach-binary" - | "application/java" - | "application/vnd.android.dex" - | "application/vnd.android.dey" - | "application/x-x509-ca-cert" => MediaType::Executable, - - // Library types (covered by executable types above, but keeping for clarity) - _ => MediaType::Unknown, - }; - } - } - } - - // Fallback to extension-based detection - if let Some(extension) = path.extension() { - match extension.to_str().unwrap_or("").to_lowercase().as_str() { - // Audio extensions - "mp3" | "wav" | "flac" | "ogg" | "m4a" | "aac" | "wma" | "mid" | "amr" | "aiff" - | "dsf" | "ape" => MediaType::Audio, - - // Video extensions - "mp4" | "mkv" | "webm" | "mov" | "avi" | "wmv" | "mpg" | "flv" | "m4v" => { - MediaType::Video - } - - // Image extensions - "gif" | "png" | "jpg" | "jpeg" | "bmp" | "tiff" | "webp" | "cr2" | "heif" | "avif" - | "jxr" | "psd" | "ico" | "ora" | "djvu" => MediaType::Image, - - // Document extensions - "txt" | "md" | "pdf" | "doc" | "docx" | "xls" | "xlsx" | "ppt" | "pptx" | "odt" - | "ods" | "odp" | "rtf" => MediaType::Document, - - // Archive extensions - "zip" | "rar" | "7z" | "tar" | "gz" | "bz2" | "bz3" | "xz" | "swf" | "sqlite" - | "nes" | "crx" | "cab" | "deb" | "ar" | "Z" | "lz" | "rpm" | "dcm" | "zst" | "lz4" - | "msi" | "cpio" | "par2" | "epub" | "mobi" => MediaType::Archive, - - // Executable extensions - "exe" | "dll" | "msi" | "dmg" | "pkg" | "deb" | "rpm" | "app" | "elf" | "bc" - | "mach" | "class" | "dex" | "dey" | "der" | "obj" => MediaType::Executable, - - // Library extensions - "so" | "dylib" => MediaType::Library, - - _ => MediaType::Unknown, - } - } else { - MediaType::Unknown - } -} - -fn is_media_file(media_type: &MediaType) -> bool { - matches!( - media_type, - MediaType::Audio | MediaType::Video | MediaType::Image - ) -} - -fn extract_streams(info: &ffprobe::FfProbe) -> Vec { - let mut streams = Vec::new(); - - for stream in &info.streams { - match stream.codec_type.as_deref() { - Some("video") => { - streams.push(StreamDetail::Video { - codec: stream - .codec_name - .clone() - .unwrap_or_else(|| "unknown".to_string()), - width: stream.width.map(|w| w as u32), - height: stream.height.map(|h| h as u32), - bit_rate: stream.bit_rate.as_ref().map(|b| b.to_string()), - frame_rate: Some(stream.r_frame_rate.clone()), - }); - } - Some("audio") => { - streams.push(StreamDetail::Audio { - codec: stream - .codec_name - .clone() - .unwrap_or_else(|| "unknown".to_string()), - sample_rate: stream.sample_rate.clone(), - channels: stream.channels.map(|c| c as u32), - bit_rate: stream.bit_rate.as_ref().map(|b| b.to_string()), - }); - } - Some("subtitle") => { - streams.push(StreamDetail::Subtitle { - codec: stream - .codec_name - .clone() - .unwrap_or_else(|| "unknown".to_string()), - language: stream.tags.as_ref().and_then(|tags| tags.language.clone()), - }); - } - _ => {} - } - } - - streams -} +// detection, helpers moved to modules above #[tauri::command] fn has_streams(paths: Vec) -> Result, StreamResultError> { diff --git a/src-tauri/src/media.rs b/src-tauri/src/media.rs new file mode 100644 index 0000000..87141b2 --- /dev/null +++ b/src-tauri/src/media.rs @@ -0,0 +1,141 @@ +use crate::models::MediaType; +use std::{fs::File, io::Read, path::Path}; + +pub fn detect_media_type(path: &Path) -> MediaType { + // First try to detect using infer crate (magic number detection) + if let Ok(mut file) = File::open(path) { + let mut buffer = [0; 512]; + if let Ok(bytes_read) = file.read(&mut buffer) { + if let Some(kind) = infer::get(&buffer[..bytes_read]) { + return match kind.mime_type() { + // Audio types + "audio/mpeg" | "audio/mp3" | "audio/m4a" | "audio/ogg" | "audio/x-flac" + | "audio/x-wav" | "audio/amr" | "audio/aac" | "audio/x-aiff" + | "audio/x-dsf" | "audio/x-ape" | "audio/midi" => MediaType::Audio, + + // Video types + "video/mp4" | "video/x-m4v" | "video/x-matroska" | "video/webm" + | "video/quicktime" | "video/x-msvideo" | "video/x-ms-wmv" | "video/mpeg" + | "video/x-flv" => MediaType::Video, + + // Image types + "image/jpeg" + | "image/png" + | "image/gif" + | "image/webp" + | "image/x-canon-cr2" + | "image/tiff" + | "image/bmp" + | "image/heif" + | "image/avif" + | "image/vnd.ms-photo" + | "image/vnd.adobe.photoshop" + | "image/vnd.microsoft.icon" + | "image/openraster" + | "image/vnd.djvu" => MediaType::Image, + + // Document types + "application/pdf" + | "application/rtf" + | "application/msword" + | "application/vnd.openxmlformats-officedocument.wordprocessingml.document" + | "application/vnd.ms-excel" + | "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" + | "application/vnd.ms-powerpoint" + | "application/vnd.openxmlformats-officedocument.presentationml.presentation" + | "application/vnd.oasis.opendocument.text" + | "application/vnd.oasis.opendocument.spreadsheet" + | "application/vnd.oasis.opendocument.presentation" => MediaType::Document, + + // Archive types + "application/zip" + | "application/x-tar" + | "application/vnd.rar" + | "application/gzip" + | "application/x-bzip2" + | "application/vnd.bzip3" + | "application/x-7z-compressed" + | "application/x-xz" + | "application/x-shockwave-flash" + | "application/octet-stream" + | "application/postscript" + | "application/vnd.sqlite3" + | "application/x-nintendo-nes-rom" + | "application/x-google-chrome-extension" + | "application/vnd.ms-cab-compressed" + | "application/vnd.debian.binary-package" + | "application/x-unix-archive" + | "application/x-compress" + | "application/x-lzip" + | "application/x-rpm" + | "application/dicom" + | "application/zstd" + | "application/x-lz4" + | "application/x-ole-storage" + | "application/x-cpio" + | "application/x-par2" + | "application/epub+zip" + | "application/x-mobipocket-ebook" => MediaType::Archive, + + // Executable types + "application/vnd.microsoft.portable-executable" + | "application/x-executable" + | "application/llvm" + | "application/x-mach-binary" + | "application/java" + | "application/vnd.android.dex" + | "application/vnd.android.dey" + | "application/x-x509-ca-cert" => MediaType::Executable, + + // Library types (covered by executable types above, but keeping for clarity) + _ => MediaType::Unknown, + }; + } + } + } + + // Fallback to extension-based detection + if let Some(extension) = path.extension() { + match extension.to_str().unwrap_or("").to_lowercase().as_str() { + // Audio extensions + "mp3" | "wav" | "flac" | "ogg" | "m4a" | "aac" | "wma" | "mid" | "amr" | "aiff" + | "dsf" | "ape" => MediaType::Audio, + + // Video extensions + "mp4" | "mkv" | "webm" | "mov" | "avi" | "wmv" | "mpg" | "flv" | "m4v" => { + MediaType::Video + } + + // Image extensions + "gif" | "png" | "jpg" | "jpeg" | "bmp" | "tiff" | "webp" | "cr2" | "heif" | "avif" + | "jxr" | "psd" | "ico" | "ora" | "djvu" => MediaType::Image, + + // Document extensions + "txt" | "md" | "pdf" | "doc" | "docx" | "xls" | "xlsx" | "ppt" | "pptx" | "odt" + | "ods" | "odp" | "rtf" => MediaType::Document, + + // Archive extensions + "zip" | "rar" | "7z" | "tar" | "gz" | "bz2" | "bz3" | "xz" | "swf" | "sqlite" + | "nes" | "crx" | "cab" | "deb" | "ar" | "Z" | "lz" | "rpm" | "dcm" | "zst" | "lz4" + | "msi" | "cpio" | "par2" | "epub" | "mobi" => MediaType::Archive, + + // Executable extensions + "exe" | "dll" | "msi" | "dmg" | "pkg" | "deb" | "rpm" | "app" | "elf" | "bc" + | "mach" | "class" | "dex" | "dey" | "der" | "obj" => MediaType::Executable, + + // Library extensions + "so" | "dylib" => MediaType::Library, + + _ => MediaType::Unknown, + } + } else { + MediaType::Unknown + } +} + +pub fn is_media_file(media_type: &MediaType) -> bool { + matches!( + media_type, + MediaType::Audio | MediaType::Video | MediaType::Image + ) +} diff --git a/src-tauri/src/models.rs b/src-tauri/src/models.rs new file mode 100644 index 0000000..204ba20 --- /dev/null +++ b/src-tauri/src/models.rs @@ -0,0 +1,56 @@ +use serde::{Deserialize, Serialize}; +use ts_rs::TS; + +#[derive(Serialize, Deserialize, Debug, Clone, TS)] +#[ts(export)] +pub enum MediaType { + Audio, + Video, + Image, + Document, + Executable, + Archive, + Library, + Unknown, +} + +#[derive(Serialize, Deserialize, Debug, Clone, TS)] +#[ts(export)] +pub struct StreamResult { + pub path: String, + pub filename: String, + pub media_type: MediaType, + pub duration: Option, + pub size: u64, + pub streams: Vec, +} + +#[derive(Serialize, Deserialize, Debug, Clone, TS)] +#[ts(export)] +pub enum StreamDetail { + Video { + codec: String, + width: Option, + height: Option, + bit_rate: Option, + frame_rate: Option, + }, + Audio { + codec: String, + sample_rate: Option, + channels: Option, + bit_rate: Option, + }, + Subtitle { + codec: String, + language: Option, + }, +} + +#[derive(Serialize, Deserialize, Debug, Clone, TS)] +#[ts(export)] +pub struct StreamResultError { + pub filename: Option, + pub reason: String, + pub error_type: String, +}