mirror of
https://github.com/R0m1k3/xtremflow.git
synced 2026-10-18 20:59:59 +02:00
Le serveur Dart (bin/) est remplacé par un serveur Rust (server/) : tokio + axum, reqwest/rustls, SQLite embarqué via rusqlite. Même API, mêmes routes et mêmes réponses : le front Flutter ne change pas. La base de production est reprise telle quelle : schéma, migrations et formats de dates (DateTime.toIso8601String de Dart) sont reproduits à l'identique, les sessions et hachages existants restent valides. Comportement conservé : cascade de routage, anti-SSRF, logs masqués, rate limits, transcodage HLS/turbo.ts avec repli GPU, quotas fournisseur, enregistrements (reprise, relances, fusion), season passes, EPG, logos de repli, arrêt gracieux sur SIGTERM. Tests Dart portés (132 tests Rust) ; la CI exécute fmt, test et clippy -D warnings, l'image Docker compile le serveur dans un étage rust:1.98.1. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
114 lines
3.3 KiB
Rust
114 lines
3.3 KiB
Rust
//! Inspection des fichiers média locaux (enregistrements) via ffprobe.
|
|
//!
|
|
//! Les résultats sont mémorisés tant que le fichier ne change pas : une
|
|
//! liste d'enregistrements est rafraîchie toutes les 5 s pendant une capture,
|
|
//! et la playlist de lecture est rechargée en continu — sans cache, ffprobe
|
|
//! tournerait en boucle.
|
|
|
|
use std::collections::HashMap;
|
|
use std::sync::{LazyLock, Mutex};
|
|
use std::time::UNIX_EPOCH;
|
|
|
|
struct CacheEntry {
|
|
size: u64,
|
|
mtime_ms: u128,
|
|
value: String,
|
|
}
|
|
|
|
static CACHE: LazyLock<Mutex<HashMap<String, CacheEntry>>> =
|
|
LazyLock::new(|| Mutex::new(HashMap::new()));
|
|
|
|
/// Binaire ffprobe : l'image Docker l'installe dans `/usr/local/bin`
|
|
/// à côté de FFmpeg ; `FFPROBE_PATH` permet de le surcharger.
|
|
pub fn ffprobe_path() -> String {
|
|
if let Ok(path) = std::env::var("FFPROBE_PATH")
|
|
&& !path.is_empty()
|
|
{
|
|
return path;
|
|
}
|
|
if cfg!(target_os = "linux") && std::path::Path::new("/usr/local/bin/ffprobe").exists() {
|
|
return "/usr/local/bin/ffprobe".to_string();
|
|
}
|
|
"ffprobe".to_string()
|
|
}
|
|
|
|
/// Durée du média en secondes, ou `None` si elle n'est pas mesurable.
|
|
pub async fn duration(path: &str) -> Option<f64> {
|
|
let value = probe(path, "duration", &["-show_entries", "format=duration"]).await?;
|
|
let seconds: f64 = value.parse().ok()?;
|
|
(seconds.is_finite() && seconds > 0.0).then_some(seconds)
|
|
}
|
|
|
|
/// Codec de la première piste vidéo (`h264`, `hevc`, `mpeg2video`…).
|
|
pub async fn video_codec(path: &str) -> Option<String> {
|
|
let value = probe(
|
|
path,
|
|
"videoCodec",
|
|
&[
|
|
"-select_streams",
|
|
"v:0",
|
|
"-show_entries",
|
|
"stream=codec_name",
|
|
],
|
|
)
|
|
.await?;
|
|
(!value.is_empty()).then(|| value.to_lowercase())
|
|
}
|
|
|
|
fn stat(path: &str) -> Option<(u64, u128)> {
|
|
let meta = std::fs::metadata(path).ok()?;
|
|
let mtime = meta
|
|
.modified()
|
|
.ok()?
|
|
.duration_since(UNIX_EPOCH)
|
|
.ok()?
|
|
.as_millis();
|
|
Some((meta.len(), mtime))
|
|
}
|
|
|
|
/// Exécute ffprobe une fois par (fichier, champ) et mémorise le résultat
|
|
/// tant que taille et date de modification sont inchangées.
|
|
async fn probe(path: &str, field: &str, args: &[&str]) -> Option<String> {
|
|
let (size, mtime_ms) = stat(path)?;
|
|
let key = format!("{field}@{path}");
|
|
{
|
|
let cache = CACHE.lock().unwrap();
|
|
if let Some(entry) = cache.get(&key)
|
|
&& entry.size == size
|
|
&& entry.mtime_ms == mtime_ms
|
|
{
|
|
return Some(entry.value.clone());
|
|
}
|
|
}
|
|
|
|
let output = tokio::process::Command::new(ffprobe_path())
|
|
.args(["-v", "error"])
|
|
.args(args)
|
|
.args(["-of", "default=noprint_wrappers=1:nokey=1", path])
|
|
.stdin(std::process::Stdio::null())
|
|
.output()
|
|
.await
|
|
.ok()?;
|
|
// ffprobe imprime une ligne par flux : seule la première nous intéresse.
|
|
let stdout = String::from_utf8_lossy(&output.stdout);
|
|
let first = stdout
|
|
.trim()
|
|
.split('\n')
|
|
.next()
|
|
.unwrap_or("")
|
|
.trim()
|
|
.to_string();
|
|
if first.is_empty() || first == "N/A" {
|
|
return None;
|
|
}
|
|
CACHE.lock().unwrap().insert(
|
|
key,
|
|
CacheEntry {
|
|
size,
|
|
mtime_ms,
|
|
value: first.clone(),
|
|
},
|
|
);
|
|
Some(first)
|
|
}
|