mirror of
https://github.com/Xevion/byte-me.git
synced 2026-01-31 06:23:50 -06:00
Compare commits
5 Commits
46876d5d9d
...
cd8feeabd2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cd8feeabd2 | ||
|
|
f83fc24d13 | ||
|
|
18ee2c8342 | ||
|
|
f34a67b949 | ||
|
|
c28fe92f1c |
+3
-2
@@ -16,7 +16,7 @@
|
||||
"@tailwindcss/vite": "^4.1.11",
|
||||
"@tauri-apps/api": "^2",
|
||||
"@tauri-apps/plugin-opener": "^2",
|
||||
"lucide-react": "^0.525.0",
|
||||
"lucide-react": "^0.540.0",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"tailwindcss": "^4.1.11",
|
||||
@@ -24,12 +24,13 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tauri-apps/cli": "^2",
|
||||
"@tsconfig/vite-react": "^7.0.0",
|
||||
"@types/react": "^18.3.1",
|
||||
"@types/react-dom": "^18.3.1",
|
||||
"@vitejs/plugin-react": "^4.3.4",
|
||||
"prettier": "^3.6.2",
|
||||
"tsx": "^4.19.2",
|
||||
"typescript": "~5.6.2",
|
||||
"typescript": "~5.9.2",
|
||||
"vite": "^6.0.3",
|
||||
"vitest": "^3.2.4"
|
||||
}
|
||||
|
||||
Generated
+18
-10
@@ -24,8 +24,8 @@ importers:
|
||||
specifier: ^2
|
||||
version: 2.4.0
|
||||
lucide-react:
|
||||
specifier: ^0.525.0
|
||||
version: 0.525.0(react@18.3.1)
|
||||
specifier: ^0.540.0
|
||||
version: 0.540.0(react@18.3.1)
|
||||
react:
|
||||
specifier: ^18.3.1
|
||||
version: 18.3.1
|
||||
@@ -42,6 +42,9 @@ importers:
|
||||
'@tauri-apps/cli':
|
||||
specifier: ^2
|
||||
version: 2.6.2
|
||||
'@tsconfig/vite-react':
|
||||
specifier: ^7.0.0
|
||||
version: 7.0.0
|
||||
'@types/react':
|
||||
specifier: ^18.3.1
|
||||
version: 18.3.23
|
||||
@@ -58,8 +61,8 @@ importers:
|
||||
specifier: ^4.19.2
|
||||
version: 4.20.4
|
||||
typescript:
|
||||
specifier: ~5.6.2
|
||||
version: 5.6.3
|
||||
specifier: ~5.9.2
|
||||
version: 5.9.2
|
||||
vite:
|
||||
specifier: ^6.0.3
|
||||
version: 6.3.5(jiti@2.4.2)(lightningcss@1.30.1)(tsx@4.20.4)
|
||||
@@ -679,6 +682,9 @@ packages:
|
||||
'@tauri-apps/plugin-opener@2.4.0':
|
||||
resolution: {integrity: sha512-43VyN8JJtvKWJY72WI/KNZszTpDpzHULFxQs0CJBIYUdCRowQ6Q1feWTDb979N7nldqSuDOaBupZ6wz2nvuWwQ==}
|
||||
|
||||
'@tsconfig/vite-react@7.0.0':
|
||||
resolution: {integrity: sha512-fiuTviENxttMlo8BHuVWgPe/DRwcuU722oVvQ/HLfI3pxXfX4uBjvj9tHm1fbj5+iYbcPmdGENXOUks6yKF2Ug==}
|
||||
|
||||
'@types/babel__core@7.20.5':
|
||||
resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==}
|
||||
|
||||
@@ -1040,8 +1046,8 @@ packages:
|
||||
lru-cache@5.1.1:
|
||||
resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==}
|
||||
|
||||
lucide-react@0.525.0:
|
||||
resolution: {integrity: sha512-Tm1txJ2OkymCGkvwoHt33Y2JpN5xucVq1slHcgE6Lk0WjDfjgKWor5CdVER8U6DvcfMwh4M8XxmpTiyzfmfDYQ==}
|
||||
lucide-react@0.540.0:
|
||||
resolution: {integrity: sha512-armkCAqQvO62EIX4Hq7hqX/q11WSZu0Jd23cnnqx0/49yIxGXyL/zyZfBxNN9YDx0ensPTb4L+DjTh3yQXUxtQ==}
|
||||
peerDependencies:
|
||||
react: ^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0
|
||||
|
||||
@@ -1189,8 +1195,8 @@ packages:
|
||||
engines: {node: '>=18.0.0'}
|
||||
hasBin: true
|
||||
|
||||
typescript@5.6.3:
|
||||
resolution: {integrity: sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw==}
|
||||
typescript@5.9.2:
|
||||
resolution: {integrity: sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==}
|
||||
engines: {node: '>=14.17'}
|
||||
hasBin: true
|
||||
|
||||
@@ -1863,6 +1869,8 @@ snapshots:
|
||||
dependencies:
|
||||
'@tauri-apps/api': 2.6.0
|
||||
|
||||
'@tsconfig/vite-react@7.0.0': {}
|
||||
|
||||
'@types/babel__core@7.20.5':
|
||||
dependencies:
|
||||
'@babel/parser': 7.28.0
|
||||
@@ -2213,7 +2221,7 @@ snapshots:
|
||||
dependencies:
|
||||
yallist: 3.1.1
|
||||
|
||||
lucide-react@0.525.0(react@18.3.1):
|
||||
lucide-react@0.540.0(react@18.3.1):
|
||||
dependencies:
|
||||
react: 18.3.1
|
||||
|
||||
@@ -2353,7 +2361,7 @@ snapshots:
|
||||
optionalDependencies:
|
||||
fsevents: 2.3.3
|
||||
|
||||
typescript@5.6.3: {}
|
||||
typescript@5.9.2: {}
|
||||
|
||||
update-browserslist-db@1.1.3(browserslist@4.25.1):
|
||||
dependencies:
|
||||
|
||||
Generated
+99
-3
@@ -401,6 +401,8 @@ dependencies = [
|
||||
"tauri",
|
||||
"tauri-build",
|
||||
"tauri-plugin-opener",
|
||||
"tracing",
|
||||
"tracing-subscriber",
|
||||
"ts-rs",
|
||||
]
|
||||
|
||||
@@ -2201,6 +2203,15 @@ dependencies = [
|
||||
"syn 2.0.104",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "matchers"
|
||||
version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558"
|
||||
dependencies = [
|
||||
"regex-automata 0.1.10",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "matches"
|
||||
version = "0.1.10"
|
||||
@@ -2325,6 +2336,16 @@ version = "0.1.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "72ef4a56884ca558e5ddb05a1d1e7e1bfd9a68d9ed024c21704cc98872dae1bb"
|
||||
|
||||
[[package]]
|
||||
name = "nu-ansi-term"
|
||||
version = "0.46.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84"
|
||||
dependencies = [
|
||||
"overload",
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-bigint"
|
||||
version = "0.4.6"
|
||||
@@ -2662,6 +2683,12 @@ dependencies = [
|
||||
"pin-project-lite",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "overload"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39"
|
||||
|
||||
[[package]]
|
||||
name = "pango"
|
||||
version = "0.18.3"
|
||||
@@ -3197,8 +3224,17 @@ checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"memchr",
|
||||
"regex-automata",
|
||||
"regex-syntax",
|
||||
"regex-automata 0.4.9",
|
||||
"regex-syntax 0.8.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "regex-automata"
|
||||
version = "0.1.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132"
|
||||
dependencies = [
|
||||
"regex-syntax 0.6.29",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3209,9 +3245,15 @@ checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"memchr",
|
||||
"regex-syntax",
|
||||
"regex-syntax 0.8.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "regex-syntax"
|
||||
version = "0.6.29"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1"
|
||||
|
||||
[[package]]
|
||||
name = "regex-syntax"
|
||||
version = "0.8.5"
|
||||
@@ -3568,6 +3610,15 @@ dependencies = [
|
||||
"digest",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sharded-slab"
|
||||
version = "0.1.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6"
|
||||
dependencies = [
|
||||
"lazy_static",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "shlex"
|
||||
version = "1.3.0"
|
||||
@@ -4332,6 +4383,15 @@ dependencies = [
|
||||
"syn 2.0.104",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thread_local"
|
||||
version = "1.1.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "time"
|
||||
version = "0.3.41"
|
||||
@@ -4579,6 +4639,36 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b9d12581f227e93f094d3af2ae690a574abb8a2b9b7a96e7cfe9647b2b617678"
|
||||
dependencies = [
|
||||
"once_cell",
|
||||
"valuable",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tracing-log"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3"
|
||||
dependencies = [
|
||||
"log",
|
||||
"once_cell",
|
||||
"tracing-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tracing-subscriber"
|
||||
version = "0.3.19"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e8189decb5ac0fa7bc8b96b7cb9b2701d60d48805aca84a238004d665fcc4008"
|
||||
dependencies = [
|
||||
"matchers",
|
||||
"nu-ansi-term",
|
||||
"once_cell",
|
||||
"regex",
|
||||
"sharded-slab",
|
||||
"smallvec",
|
||||
"thread_local",
|
||||
"tracing",
|
||||
"tracing-core",
|
||||
"tracing-log",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -4784,6 +4874,12 @@ dependencies = [
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "valuable"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65"
|
||||
|
||||
[[package]]
|
||||
name = "version-compare"
|
||||
version = "0.2.0"
|
||||
|
||||
@@ -24,3 +24,5 @@ serde = { version = "1", features = ["derive"] }
|
||||
ffprobe = "0.4.0"
|
||||
ts-rs = { version = "11.0", features = ["format"] }
|
||||
infer = "0.19.0"
|
||||
tracing = "0.1"
|
||||
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
|
||||
|
||||
+86
-22
@@ -1,45 +1,109 @@
|
||||
use crate::models::StreamDetail;
|
||||
use tracing::{debug, info, instrument};
|
||||
|
||||
#[instrument(skip(info), fields(stream_count = info.streams.len()))]
|
||||
pub fn extract_streams(info: &ffprobe::FfProbe) -> Vec<StreamDetail> {
|
||||
let mut streams = Vec::new();
|
||||
let mut video_count = 0;
|
||||
let mut audio_count = 0;
|
||||
let mut subtitle_count = 0;
|
||||
|
||||
for stream in &info.streams {
|
||||
info!(total_streams = info.streams.len(), "Extracting streams from media file");
|
||||
|
||||
for (index, stream) in info.streams.iter().enumerate() {
|
||||
match stream.codec_type.as_deref() {
|
||||
Some("video") => {
|
||||
video_count += 1;
|
||||
let codec = stream
|
||||
.codec_name
|
||||
.clone()
|
||||
.unwrap_or_else(|| "unknown".to_string());
|
||||
let width = stream.width.map(|w| w as u32);
|
||||
let height = stream.height.map(|h| h as u32);
|
||||
let bit_rate = stream.bit_rate.as_ref().map(|b| b.to_string());
|
||||
let frame_rate = Some(stream.r_frame_rate.clone());
|
||||
|
||||
debug!(
|
||||
stream_index = index,
|
||||
codec = %codec,
|
||||
width = ?width,
|
||||
height = ?height,
|
||||
bit_rate = ?bit_rate,
|
||||
frame_rate = ?frame_rate,
|
||||
"Extracted video stream"
|
||||
);
|
||||
|
||||
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()),
|
||||
codec,
|
||||
width,
|
||||
height,
|
||||
bit_rate,
|
||||
frame_rate,
|
||||
});
|
||||
}
|
||||
Some("audio") => {
|
||||
audio_count += 1;
|
||||
let codec = stream
|
||||
.codec_name
|
||||
.clone()
|
||||
.unwrap_or_else(|| "unknown".to_string());
|
||||
let sample_rate = stream.sample_rate.clone();
|
||||
let channels = stream.channels.map(|c| c as u32);
|
||||
let bit_rate = stream.bit_rate.as_ref().map(|b| b.to_string());
|
||||
|
||||
debug!(
|
||||
stream_index = index,
|
||||
codec = %codec,
|
||||
sample_rate = ?sample_rate,
|
||||
channels = ?channels,
|
||||
bit_rate = ?bit_rate,
|
||||
"Extracted audio stream"
|
||||
);
|
||||
|
||||
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()),
|
||||
codec,
|
||||
sample_rate,
|
||||
channels,
|
||||
bit_rate,
|
||||
});
|
||||
}
|
||||
Some("subtitle") => {
|
||||
subtitle_count += 1;
|
||||
let codec = stream
|
||||
.codec_name
|
||||
.clone()
|
||||
.unwrap_or_else(|| "unknown".to_string());
|
||||
let language = stream.tags.as_ref().and_then(|tags| tags.language.clone());
|
||||
|
||||
debug!(
|
||||
stream_index = index,
|
||||
codec = %codec,
|
||||
language = ?language,
|
||||
"Extracted subtitle stream"
|
||||
);
|
||||
|
||||
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()),
|
||||
codec,
|
||||
language,
|
||||
});
|
||||
}
|
||||
_ => {}
|
||||
other => {
|
||||
debug!(
|
||||
stream_index = index,
|
||||
codec_type = ?other,
|
||||
"Skipping unknown stream type"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
info!(
|
||||
video_streams = video_count,
|
||||
audio_streams = audio_count,
|
||||
subtitle_streams = subtitle_count,
|
||||
total_extracted = streams.len(),
|
||||
"Stream extraction completed"
|
||||
);
|
||||
|
||||
streams
|
||||
}
|
||||
|
||||
+53
-8
@@ -1,28 +1,45 @@
|
||||
mod ff;
|
||||
mod media;
|
||||
mod models;
|
||||
pub mod ff;
|
||||
pub mod media;
|
||||
pub mod models;
|
||||
pub mod strings;
|
||||
|
||||
use ff::extract_streams;
|
||||
use media::{detect_media_type, is_media_file};
|
||||
use models::{StreamResult, StreamResultError};
|
||||
use strings::transform_filename;
|
||||
use std::path::Path;
|
||||
use tracing::{debug, error, info, instrument, warn};
|
||||
|
||||
// detection, helpers moved to modules above
|
||||
|
||||
#[tauri::command]
|
||||
#[instrument(skip(paths), fields(file_count = paths.len()))]
|
||||
fn has_streams(paths: Vec<String>) -> Result<Vec<StreamResult>, StreamResultError> {
|
||||
paths
|
||||
info!(file_count = paths.len(), "Processing files for stream analysis");
|
||||
|
||||
let results = paths
|
||||
.into_iter()
|
||||
.map(|path_str| {
|
||||
.enumerate()
|
||||
.map(|(index, path_str)| {
|
||||
let path = Path::new(&path_str);
|
||||
let filename = path
|
||||
.file_name()
|
||||
.and_then(|name| name.to_str())
|
||||
.unwrap_or("unknown")
|
||||
.to_string();
|
||||
|
||||
// Log full path only on first occurrence, then use truncated filename
|
||||
if index == 0 {
|
||||
debug!(full_path = %path_str, filename = %filename, "Processing first file");
|
||||
} else {
|
||||
let truncated_name = transform_filename(&filename, 15);
|
||||
debug!(filename = %truncated_name, "Processing file");
|
||||
}
|
||||
|
||||
// Check if file exists
|
||||
if !path.exists() {
|
||||
let truncated_name = transform_filename(&filename, 15);
|
||||
warn!(filename = %truncated_name, "File does not exist");
|
||||
return Err(StreamResultError {
|
||||
filename: Some(filename),
|
||||
reason: "File does not exist".to_string(),
|
||||
@@ -32,6 +49,8 @@ fn has_streams(paths: Vec<String>) -> Result<Vec<StreamResult>, StreamResultErro
|
||||
|
||||
// Check if it's a file (not directory)
|
||||
if !path.is_file() {
|
||||
let truncated_name = transform_filename(&filename, 15);
|
||||
warn!(filename = %truncated_name, "Path is not a file");
|
||||
return Err(StreamResultError {
|
||||
filename: Some(filename),
|
||||
reason: "Not a file (directory or other)".to_string(),
|
||||
@@ -44,11 +63,17 @@ fn has_streams(paths: Vec<String>) -> Result<Vec<StreamResult>, StreamResultErro
|
||||
.map(|metadata| metadata.len())
|
||||
.unwrap_or(0);
|
||||
|
||||
let truncated_name = transform_filename(&filename, 15);
|
||||
debug!(filename = %truncated_name, size = size, "File metadata retrieved");
|
||||
|
||||
// Detect media type using magic numbers and fallback to extensions
|
||||
let media_type = detect_media_type(path);
|
||||
debug!(filename = %truncated_name, media_type = ?media_type, "Media type detected");
|
||||
|
||||
// Only try to analyze media files with ffprobe
|
||||
if is_media_file(&media_type) {
|
||||
info!(filename = %truncated_name, media_type = ?media_type, "Analyzing media file with ffprobe");
|
||||
|
||||
// Analyze with ffprobe
|
||||
match ffprobe::ffprobe(&path_str) {
|
||||
Ok(info) => {
|
||||
@@ -58,6 +83,13 @@ fn has_streams(paths: Vec<String>) -> Result<Vec<StreamResult>, StreamResultErro
|
||||
.duration
|
||||
.and_then(|dur_str| dur_str.parse::<f64>().ok());
|
||||
|
||||
info!(
|
||||
filename = %truncated_name,
|
||||
stream_count = streams.len(),
|
||||
duration = ?duration,
|
||||
"Successfully analyzed media file"
|
||||
);
|
||||
|
||||
Ok(StreamResult {
|
||||
filename,
|
||||
path: path_str,
|
||||
@@ -68,7 +100,7 @@ fn has_streams(paths: Vec<String>) -> Result<Vec<StreamResult>, StreamResultErro
|
||||
})
|
||||
}
|
||||
Err(err) => {
|
||||
eprintln!("Could not analyze media file with ffprobe: {err:?}");
|
||||
error!(filename = %truncated_name, error = %err, "Failed to analyze media file with ffprobe");
|
||||
Err(StreamResultError {
|
||||
filename: Some(filename),
|
||||
reason: format!("Could not analyze media file: {err}"),
|
||||
@@ -77,6 +109,7 @@ fn has_streams(paths: Vec<String>) -> Result<Vec<StreamResult>, StreamResultErro
|
||||
}
|
||||
}
|
||||
} else {
|
||||
debug!(filename = %truncated_name, media_type = ?media_type, "Skipping non-media file");
|
||||
// For non-media files, return an error indicating it's not a media file
|
||||
Err(StreamResultError {
|
||||
filename: Some(filename),
|
||||
@@ -85,14 +118,26 @@ fn has_streams(paths: Vec<String>) -> Result<Vec<StreamResult>, StreamResultErro
|
||||
})
|
||||
}
|
||||
})
|
||||
.collect::<Result<Vec<_>, _>>()
|
||||
.collect::<Result<Vec<_>, _>>();
|
||||
|
||||
match &results {
|
||||
Ok(streams) => {
|
||||
info!(successful_files = streams.len(), "Successfully processed all files");
|
||||
}
|
||||
Err(_) => {
|
||||
warn!("Some files failed to process");
|
||||
}
|
||||
}
|
||||
|
||||
results
|
||||
}
|
||||
|
||||
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
||||
pub fn run() {
|
||||
info!("Initializing Tauri application");
|
||||
tauri::Builder::default()
|
||||
.plugin(tauri_plugin_opener::init())
|
||||
.invoke_handler(tauri::generate_handler![has_streams])
|
||||
.run(tauri::generate_context!())
|
||||
.expect("error while running tauri application");
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,19 @@
|
||||
// Prevents additional console window on Windows in release, DO NOT REMOVE!!
|
||||
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
|
||||
|
||||
use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt, EnvFilter};
|
||||
|
||||
fn main() {
|
||||
// Initialize tracing with env-filter
|
||||
tracing_subscriber::registry()
|
||||
.with(
|
||||
EnvFilter::from_default_env()
|
||||
.add_directive("byte_me=debug".parse().unwrap())
|
||||
.add_directive("tauri=info".parse().unwrap()),
|
||||
)
|
||||
.with(tracing_subscriber::fmt::layer())
|
||||
.init();
|
||||
|
||||
tracing::info!("Starting byte-me application");
|
||||
byte_me_lib::run()
|
||||
}
|
||||
|
||||
+34
-5
@@ -1,13 +1,22 @@
|
||||
use crate::models::MediaType;
|
||||
use std::{fs::File, io::Read, path::Path};
|
||||
use tracing::{debug, instrument, trace, warn};
|
||||
|
||||
#[instrument(skip(path), fields(path = %path.display()))]
|
||||
pub fn detect_media_type(path: &Path) -> MediaType {
|
||||
debug!("Starting media type detection");
|
||||
|
||||
// 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) {
|
||||
trace!(bytes_read = bytes_read, "Read file header for magic number detection");
|
||||
|
||||
if let Some(kind) = infer::get(&buffer[..bytes_read]) {
|
||||
return match kind.mime_type() {
|
||||
let mime_type = kind.mime_type();
|
||||
debug!(mime_type = %mime_type, "Detected MIME type from magic numbers");
|
||||
|
||||
let media_type = match 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"
|
||||
@@ -90,13 +99,25 @@ pub fn detect_media_type(path: &Path) -> MediaType {
|
||||
// Library types (covered by executable types above, but keeping for clarity)
|
||||
_ => MediaType::Unknown,
|
||||
};
|
||||
|
||||
debug!(media_type = ?media_type, "Detected media type from magic numbers");
|
||||
return media_type;
|
||||
} else {
|
||||
debug!("Magic number detection failed, falling back to extension-based detection");
|
||||
}
|
||||
} else {
|
||||
warn!("Failed to read file for magic number detection");
|
||||
}
|
||||
} else {
|
||||
warn!("Failed to open file for magic number detection");
|
||||
}
|
||||
|
||||
// Fallback to extension-based detection
|
||||
if let Some(extension) = path.extension() {
|
||||
match extension.to_str().unwrap_or("").to_lowercase().as_str() {
|
||||
let ext_str = extension.to_str().unwrap_or("").to_lowercase();
|
||||
debug!(extension = %ext_str, "Detecting media type from file extension");
|
||||
|
||||
let media_type = match ext_str.as_str() {
|
||||
// Audio extensions
|
||||
"mp3" | "wav" | "flac" | "ogg" | "m4a" | "aac" | "wma" | "mid" | "amr" | "aiff"
|
||||
| "dsf" | "ape" => MediaType::Audio,
|
||||
@@ -127,15 +148,23 @@ pub fn detect_media_type(path: &Path) -> MediaType {
|
||||
"so" | "dylib" => MediaType::Library,
|
||||
|
||||
_ => MediaType::Unknown,
|
||||
}
|
||||
};
|
||||
|
||||
debug!(media_type = ?media_type, "Detected media type from extension");
|
||||
media_type
|
||||
} else {
|
||||
debug!("No file extension found, returning Unknown");
|
||||
MediaType::Unknown
|
||||
}
|
||||
}
|
||||
|
||||
#[instrument(skip(media_type))]
|
||||
pub fn is_media_file(media_type: &MediaType) -> bool {
|
||||
matches!(
|
||||
let is_media = matches!(
|
||||
media_type,
|
||||
MediaType::Audio | MediaType::Video | MediaType::Image
|
||||
)
|
||||
);
|
||||
|
||||
debug!(media_type = ?media_type, is_media = is_media, "Checking if file is media type");
|
||||
is_media
|
||||
}
|
||||
|
||||
@@ -0,0 +1,122 @@
|
||||
/// Transforms a filename to fit within a character limit while preserving the most useful context
|
||||
///
|
||||
/// This function prioritizes preserving:
|
||||
/// 1. File extension (if reasonable length ≤ 5 chars including dot)
|
||||
/// 2. Beginning of filename (for identification)
|
||||
/// 3. End of filename before extension (often contains important info like numbers)
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `filename` - The filename to transform
|
||||
/// * `limit` - Maximum number of characters
|
||||
///
|
||||
/// # Returns
|
||||
/// * Transformed filename that fits within the limit, using ellipsis (...) to indicate truncation
|
||||
///
|
||||
/// # Examples
|
||||
/// ```
|
||||
/// use byte_me_lib::strings::transform_filename;
|
||||
///
|
||||
/// // Short filenames remain unchanged
|
||||
/// assert_eq!(transform_filename("test.mp4", 20), "test.mp4");
|
||||
///
|
||||
/// // Long filename with extension - preserve extension and context
|
||||
/// assert_eq!(transform_filename("very_long_video_file_name.mp4", 18), "ver...ile_name.mp4");
|
||||
///
|
||||
/// // Numeric sequences - preserve start and end numbers
|
||||
/// assert_eq!(transform_filename("43509374693.TS.mp4", 15), "435...93.TS.mp4");
|
||||
///
|
||||
/// // No extension - preserve start and end of name
|
||||
/// assert_eq!(transform_filename("very_long_document_name", 15), "ver...ment_name");
|
||||
///
|
||||
/// // Long extension treated as part of name
|
||||
/// assert_eq!(transform_filename("file.verylongextension", 15), "fil...extension");
|
||||
/// ```
|
||||
pub fn transform_filename(filename: &str, limit: usize) -> String {
|
||||
// Handle edge cases
|
||||
if limit == 0 || filename.is_empty() {
|
||||
return String::new();
|
||||
}
|
||||
|
||||
if filename.len() <= limit {
|
||||
return filename.to_string();
|
||||
}
|
||||
|
||||
// Find potential extension (last dot, not at start or end)
|
||||
let extension_start = filename
|
||||
.rfind('.')
|
||||
.filter(|&pos| pos > 0 && pos < filename.len() - 1);
|
||||
|
||||
let (name_part, extension_part) = if let Some(ext_pos) = extension_start {
|
||||
let ext = &filename[ext_pos..];
|
||||
// Only treat as extension if it's reasonable length (≤ 5 chars including dot)
|
||||
// and doesn't contain additional dots (compound extensions like .TS.mp4)
|
||||
if ext.len() <= 5 && !ext[1..].contains('.') {
|
||||
(&filename[..ext_pos], ext)
|
||||
} else {
|
||||
(filename, "")
|
||||
}
|
||||
} else {
|
||||
(filename, "")
|
||||
};
|
||||
|
||||
// If even just the extension is too long, truncate the whole thing
|
||||
if extension_part.len() >= limit {
|
||||
return truncate_string(filename, limit);
|
||||
}
|
||||
|
||||
// Calculate space available for the name part
|
||||
let name_limit = limit - extension_part.len();
|
||||
|
||||
// If name fits in available space, no truncation needed
|
||||
if name_part.len() <= name_limit {
|
||||
return filename.to_string();
|
||||
}
|
||||
|
||||
// Need to truncate the name part
|
||||
let truncated_name = truncate_string(name_part, name_limit);
|
||||
format!("{}{}", truncated_name, extension_part)
|
||||
}
|
||||
|
||||
/// Helper function to truncate a string with ellipsis, preserving start and end context
|
||||
pub fn truncate_string(s: &str, limit: usize) -> String {
|
||||
if s.len() <= limit {
|
||||
return s.to_string();
|
||||
}
|
||||
|
||||
// For very small limits, just truncate without ellipsis
|
||||
if limit < 5 {
|
||||
return s.chars().take(limit).collect();
|
||||
}
|
||||
|
||||
// For limits 5 and above, use start + "..." + end pattern
|
||||
// Strategy: Use 3 chars for ellipsis, split remaining between start and end
|
||||
// But ensure we get meaningful chunks from both ends
|
||||
|
||||
let available_for_content = limit - 3; // Reserve 3 for "..."
|
||||
|
||||
// Determine start and end characters based on available space
|
||||
let (start_chars, end_chars) = if available_for_content <= 4 {
|
||||
// Very limited space: minimal start, rest for end
|
||||
(1, available_for_content - 1)
|
||||
} else if available_for_content <= 6 {
|
||||
// Medium space: balanced approach
|
||||
let start = available_for_content / 2;
|
||||
(start, available_for_content - start)
|
||||
} else {
|
||||
// Plenty of space: cap start at 3, use more for end to preserve context
|
||||
let start = 3;
|
||||
(start, available_for_content - start)
|
||||
};
|
||||
|
||||
let start: String = s.chars().take(start_chars).collect();
|
||||
let end: String = s
|
||||
.chars()
|
||||
.rev()
|
||||
.take(end_chars)
|
||||
.collect::<String>()
|
||||
.chars()
|
||||
.rev()
|
||||
.collect();
|
||||
|
||||
format!("{}...{}", start, end)
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
use byte_me_lib::strings::{transform_filename, truncate_string};
|
||||
|
||||
#[test]
|
||||
fn test_transform_filename() {
|
||||
// Test cases focusing on practical, readable outputs
|
||||
|
||||
// 1. Short filenames should remain unchanged
|
||||
assert_eq!(transform_filename("test.mp4", 20), "test.mp4");
|
||||
assert_eq!(transform_filename("short.txt", 15), "short.txt");
|
||||
assert_eq!(transform_filename("a.b", 10), "a.b");
|
||||
|
||||
// 2. No extension cases - preserve meaningful start and end
|
||||
assert_eq!(transform_filename("short_name", 15), "short_name");
|
||||
assert_eq!(
|
||||
transform_filename("very_long_document_name", 15),
|
||||
"ver...ment_name"
|
||||
);
|
||||
assert_eq!(
|
||||
transform_filename("medium_length_name", 13),
|
||||
"med...th_name"
|
||||
);
|
||||
|
||||
// 3. Normal extension cases (preserving extension)
|
||||
assert_eq!(
|
||||
transform_filename("very_long_video_file_name.mp4", 18),
|
||||
"ver...ile_name.mp4"
|
||||
);
|
||||
assert_eq!(
|
||||
transform_filename("document_with_long_name.pdf", 15),
|
||||
"doc..._name.pdf"
|
||||
);
|
||||
assert_eq!(
|
||||
transform_filename("image_file_name.jpeg", 15),
|
||||
"ima...name.jpeg"
|
||||
);
|
||||
|
||||
// 4. Numeric sequences (like user's example) - preserve start and end numbers
|
||||
assert_eq!(
|
||||
transform_filename("43509374693.TS.mp4", 15),
|
||||
"435...93.TS.mp4"
|
||||
);
|
||||
assert_eq!(
|
||||
transform_filename("20231201_video.mp4", 15),
|
||||
"202...video.mp4"
|
||||
);
|
||||
assert_eq!(transform_filename("file_v2.1.3.tar", 12), "fi...1.3.tar");
|
||||
|
||||
// 5. Long extensions (treated as part of filename)
|
||||
assert_eq!(
|
||||
transform_filename("file.verylongextension", 15),
|
||||
"fil...extension"
|
||||
);
|
||||
assert_eq!(
|
||||
transform_filename("document.backup_old", 15),
|
||||
"doc...ackup_old"
|
||||
);
|
||||
|
||||
// 6. Edge cases
|
||||
assert_eq!(transform_filename("", 10), "");
|
||||
assert_eq!(transform_filename("a", 0), "");
|
||||
assert_eq!(transform_filename("test", 4), "test");
|
||||
assert_eq!(transform_filename("test", 3), "tes");
|
||||
assert_eq!(transform_filename("ab", 2), "ab");
|
||||
|
||||
// 7. Very short limits - graceful degradation
|
||||
assert_eq!(transform_filename("test.mp4", 8), "test.mp4");
|
||||
assert_eq!(transform_filename("verylongname", 8), "ve...ame");
|
||||
assert_eq!(transform_filename("test.mp4", 7), "tes.mp4");
|
||||
assert_eq!(transform_filename("hello.txt", 9), "hello.txt");
|
||||
|
||||
// 8. Extension edge cases
|
||||
assert_eq!(transform_filename("file.", 10), "file.");
|
||||
assert_eq!(transform_filename(".hidden", 10), ".hidden");
|
||||
assert_eq!(transform_filename("test.a", 10), "test.a");
|
||||
|
||||
// 9. Real-world examples
|
||||
assert_eq!(
|
||||
transform_filename("IMG_20231201_143022.jpg", 15),
|
||||
"IMG...43022.jpg"
|
||||
);
|
||||
assert_eq!(
|
||||
transform_filename("meeting_recording_final_v2.mp4", 20),
|
||||
"mee...g_final_v2.mp4"
|
||||
);
|
||||
assert_eq!(
|
||||
transform_filename("my document (copy).docx", 15),
|
||||
"my ...opy).docx"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_truncate_string() {
|
||||
// Test the helper function directly
|
||||
assert_eq!(truncate_string("hello", 10), "hello");
|
||||
assert_eq!(truncate_string("hello", 5), "hello");
|
||||
assert_eq!(truncate_string("hello_world", 8), "he...rld");
|
||||
assert_eq!(truncate_string("test", 4), "test");
|
||||
assert_eq!(truncate_string("test", 3), "tes");
|
||||
assert_eq!(truncate_string("ab", 2), "ab");
|
||||
assert_eq!(truncate_string("a", 1), "a");
|
||||
assert_eq!(truncate_string("hello", 1), "h");
|
||||
assert_eq!(truncate_string("hello", 0), "");
|
||||
assert_eq!(truncate_string("very_long_name", 10), "ver...name");
|
||||
assert_eq!(truncate_string("document_name", 9), "doc...ame");
|
||||
}
|
||||
+4
-4
@@ -1,7 +1,7 @@
|
||||
import { useDragDropPaths } from "./hooks/useDragDropPaths.js";
|
||||
import Graph from "./features/graph/graph.js";
|
||||
import DropOverlay from "./features/drop/drop-overlay.js";
|
||||
import type { Frame } from "./types/graph.js";
|
||||
import { useDragDropPaths } from "@/hooks/useDragDropPaths";
|
||||
import Graph from "@/components/graph";
|
||||
import DropOverlay from "@/components/drop-overlay";
|
||||
import type { Frame } from "@/types/graph";
|
||||
|
||||
function App() {
|
||||
const data: Frame[] = [];
|
||||
|
||||
+4
-4
@@ -1,8 +1,8 @@
|
||||
// Import generated TypeScript types from ts-rs
|
||||
import type { StreamResult } from "./bindings/StreamResult";
|
||||
import type { StreamDetail } from "./bindings/StreamDetail";
|
||||
import type { StreamResultError } from "./bindings/StreamResultError";
|
||||
import type { MediaType } from "./bindings/MediaType";
|
||||
import type { StreamResult } from "@/bindings/StreamResult";
|
||||
import type { StreamDetail } from "@/bindings/StreamDetail";
|
||||
import type { StreamResultError } from "@/bindings/StreamResultError";
|
||||
import type { MediaType } from "@/bindings/MediaType";
|
||||
export type { StreamResult, StreamDetail, StreamResultError, MediaType };
|
||||
|
||||
// Tauri invoke wrapper
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ReactNode, useEffect, useRef, useState } from "react";
|
||||
import { type ReactNode, useEffect, useRef, useState } from "react";
|
||||
import { match, P } from "ts-pattern";
|
||||
import {
|
||||
CheckCircle,
|
||||
@@ -10,8 +10,8 @@ import {
|
||||
Music,
|
||||
XCircle,
|
||||
} from "lucide-react";
|
||||
import { commands } from "../../bindings";
|
||||
import type { MediaType, StreamDetail } from "../../bindings";
|
||||
import { commands } from "@/bindings";
|
||||
import type { MediaType, StreamDetail } from "@/bindings";
|
||||
|
||||
type DropOverlayProps = {
|
||||
paths: string[];
|
||||
@@ -1,6 +1,6 @@
|
||||
import { ResponsiveLine } from "@nivo/line";
|
||||
import { formatBytes } from "../../lib/format.js";
|
||||
import type { Frame } from "../../types/graph.js";
|
||||
import { formatBytes } from "@/lib/format";
|
||||
import type { Frame } from "@/types/graph";
|
||||
|
||||
type GraphProps = {
|
||||
data: Frame[];
|
||||
@@ -1,4 +1,4 @@
|
||||
import { formatBytes } from "./format.js";
|
||||
import { formatBytes } from "@/lib/format";
|
||||
import { test, expect } from "vitest";
|
||||
|
||||
test("formats bytes less than 1024", () => {
|
||||
|
||||
+2
-2
@@ -1,7 +1,7 @@
|
||||
import React from "react";
|
||||
import ReactDOM from "react-dom/client";
|
||||
import App from "./App";
|
||||
import "./global.css";
|
||||
import App from "@/App";
|
||||
import "@/global.css";
|
||||
|
||||
ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(
|
||||
<React.StrictMode>
|
||||
|
||||
+6
-21
@@ -1,25 +1,10 @@
|
||||
{
|
||||
"extends": "@tsconfig/vite-react/tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"target": "ES2020",
|
||||
"useDefineForClassFields": true,
|
||||
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
||||
"module": "ESNext",
|
||||
"skipLibCheck": true,
|
||||
|
||||
/* Bundler mode */
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"noEmit": true,
|
||||
"jsx": "react-jsx",
|
||||
|
||||
/* Linting */
|
||||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"noFallthroughCasesInSwitch": true
|
||||
/* Paths */
|
||||
"paths": {
|
||||
"@/*": ["./src/*"]
|
||||
}
|
||||
},
|
||||
"include": ["src"],
|
||||
"references": [{ "path": "./tsconfig.node.json" }]
|
||||
"include": ["src"]
|
||||
}
|
||||
|
||||
+6
-1
@@ -1,13 +1,18 @@
|
||||
import { defineConfig } from "vite";
|
||||
import react from "@vitejs/plugin-react";
|
||||
import tailwindcss from "@tailwindcss/vite";
|
||||
import path from "path";
|
||||
|
||||
// @ts-expect-error process is a nodejs global
|
||||
const host = process.env.TAURI_DEV_HOST;
|
||||
|
||||
// https://vite.dev/config/
|
||||
export default defineConfig(async () => ({
|
||||
plugins: [react(), tailwindcss()],
|
||||
resolve: {
|
||||
alias: {
|
||||
"@": path.resolve(__dirname, "src"),
|
||||
},
|
||||
},
|
||||
|
||||
// Vite options tailored for Tauri development and only applied in `tauri dev` or `tauri build`
|
||||
//
|
||||
|
||||
Reference in New Issue
Block a user