refactor: reorganize backend tauri code

This commit is contained in:
Ryan Walters
2025-08-20 02:20:24 -05:00
parent 3414880705
commit ecc8380645
4 changed files with 252 additions and 240 deletions

47
src-tauri/src/ff.rs Normal file
View File

@@ -0,0 +1,47 @@
use crate::models::StreamDetail;
pub fn extract_streams(info: &ffprobe::FfProbe) -> Vec<StreamDetail> {
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
}

View File

@@ -1,245 +1,13 @@
use serde::{Deserialize, Serialize}; mod ff;
use std::fs::File; mod media;
use std::io::Read; mod models;
use std::path::Path; 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)] // detection, helpers moved to modules above
#[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<f64>,
size: u64,
streams: Vec<StreamDetail>,
}
#[derive(Serialize, Deserialize, Debug, Clone, TS)]
#[ts(export)]
enum StreamDetail {
Video {
codec: String,
width: Option<u32>,
height: Option<u32>,
bit_rate: Option<String>,
frame_rate: Option<String>,
},
Audio {
codec: String,
sample_rate: Option<String>,
channels: Option<u32>,
bit_rate: Option<String>,
},
Subtitle {
codec: String,
language: Option<String>,
},
}
#[derive(Serialize, Deserialize, Debug, Clone, TS)]
#[ts(export)]
struct StreamResultError {
filename: Option<String>,
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<StreamDetail> {
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
}
#[tauri::command] #[tauri::command]
fn has_streams(paths: Vec<String>) -> Result<Vec<StreamResult>, StreamResultError> { fn has_streams(paths: Vec<String>) -> Result<Vec<StreamResult>, StreamResultError> {

141
src-tauri/src/media.rs Normal file
View File

@@ -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
)
}

56
src-tauri/src/models.rs Normal file
View File

@@ -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<f64>,
pub size: u64,
pub streams: Vec<StreamDetail>,
}
#[derive(Serialize, Deserialize, Debug, Clone, TS)]
#[ts(export)]
pub enum StreamDetail {
Video {
codec: String,
width: Option<u32>,
height: Option<u32>,
bit_rate: Option<String>,
frame_rate: Option<String>,
},
Audio {
codec: String,
sample_rate: Option<String>,
channels: Option<u32>,
bit_rate: Option<String>,
},
Subtitle {
codec: String,
language: Option<String>,
},
}
#[derive(Serialize, Deserialize, Debug, Clone, TS)]
#[ts(export)]
pub struct StreamResultError {
pub filename: Option<String>,
pub reason: String,
pub error_type: String,
}