feat: implement project management and media track handling

- Added domain entities for audio, subtitle, and video tracks.
- Created a Project entity to manage media editing sessions.
- Implemented value objects for file paths, sync offsets, track IDs, and languages.
- Developed infrastructure for asynchronous FFmpeg process execution.
- Built a user interface for selecting video files, adding audio and subtitle tracks, and managing output settings.
- Integrated error handling and logging for media processing tasks.
This commit is contained in:
2026-02-28 13:42:23 -03:00
parent eff779a1df
commit ce236eca5d
47 changed files with 6818 additions and 2 deletions
+42
View File
@@ -0,0 +1,42 @@
use std::path::PathBuf;
use serde::{Deserialize, Serialize};
/// Newtype sobre PathBuf representando um caminho de arquivo validado.
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct FilePath(PathBuf);
impl FilePath {
pub fn new(path: impl Into<PathBuf>) -> Self {
FilePath(path.into())
}
pub fn as_path(&self) -> &std::path::Path {
self.0.as_path()
}
pub fn to_str(&self) -> Option<&str> {
self.0.to_str()
}
pub fn to_string_lossy(&self) -> std::borrow::Cow<str> {
self.0.to_string_lossy()
}
}
impl std::fmt::Display for FilePath {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.0.display())
}
}
impl From<PathBuf> for FilePath {
fn from(p: PathBuf) -> Self {
FilePath(p)
}
}
impl From<&str> for FilePath {
fn from(s: &str) -> Self {
FilePath(PathBuf::from(s))
}
}
+9
View File
@@ -0,0 +1,9 @@
pub mod file_path;
pub mod sync_offset;
pub mod track_id;
pub mod track_language;
pub use file_path::FilePath;
pub use sync_offset::SyncOffset;
pub use track_id::TrackId;
pub use track_language::TrackLanguage;
+75
View File
@@ -0,0 +1,75 @@
use anyhow::{anyhow, Result};
use serde::{Deserialize, Serialize};
/// Offset de sincronização armazenado em milissegundos como inteiro.
/// Nunca usa f64 — a conversão de/para segundos com vírgula é feita aqui.
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize, Default)]
pub struct SyncOffset(i64);
impl SyncOffset {
/// Cria um SyncOffset a partir de milissegundos.
pub fn from_ms(ms: i64) -> Self {
SyncOffset(ms)
}
/// Cria um SyncOffset a partir de uma string de segundos (ex: "1.2", "-0.5").
pub fn from_seconds_str(s: &str) -> Result<Self> {
let trimmed = s.trim().trim_end_matches('s');
let seconds: f64 = trimmed
.parse()
.map_err(|_| anyhow!("Offset inválido: '{}'", s))?;
let ms = (seconds * 1000.0).round() as i64;
Ok(SyncOffset(ms))
}
/// Retorna o offset em milissegundos.
pub fn as_ms(&self) -> i64 {
self.0
}
/// Retorna true se o offset é zero (sem deslocamento).
pub fn is_zero(&self) -> bool {
self.0 == 0
}
}
impl std::fmt::Display for SyncOffset {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let seconds = self.0 as f64 / 1000.0;
write!(f, "{:.3}s", seconds)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn from_seconds_str_positivo() {
let offset = SyncOffset::from_seconds_str("1.2").unwrap();
assert_eq!(offset.as_ms(), 1200);
}
#[test]
fn from_seconds_str_negativo() {
let offset = SyncOffset::from_seconds_str("-0.5").unwrap();
assert_eq!(offset.as_ms(), -500);
}
#[test]
fn from_seconds_str_com_sufixo_s() {
let offset = SyncOffset::from_seconds_str("2.0s").unwrap();
assert_eq!(offset.as_ms(), 2000);
}
#[test]
fn from_seconds_str_invalido() {
assert!(SyncOffset::from_seconds_str("abc").is_err());
}
#[test]
fn from_seconds_str_zero() {
let offset = SyncOffset::from_seconds_str("0").unwrap();
assert!(offset.is_zero());
}
}
+25
View File
@@ -0,0 +1,25 @@
use serde::{Deserialize, Serialize};
/// Identificador opaco de uma faixa de mídia.
/// Não expõe indexação interna do FFmpeg — isso é responsabilidade do adapter.
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct TrackId(u32);
impl TrackId {
pub fn new(id: u32) -> Self {
TrackId(id)
}
}
impl TrackId {
/// Retorna o valor interno (uso restrito a crates internas).
pub(crate) fn val(self) -> u32 {
self.0
}
}
impl std::fmt::Display for TrackId {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "TrackId({})", self.0)
}
}
@@ -0,0 +1,57 @@
use anyhow::{anyhow, Result};
use serde::{Deserialize, Serialize};
/// Código de idioma ISO 639-2 (3 letras minúsculas), ex: "por", "eng".
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct TrackLanguage(String);
impl TrackLanguage {
/// Cria um TrackLanguage validando o formato ISO 639-2 (3 letras minúsculas).
pub fn new(code: impl Into<String>) -> Result<Self> {
let code = code.into();
if code.len() == 3 && code.chars().all(|c| c.is_ascii_lowercase()) {
Ok(TrackLanguage(code))
} else {
Err(anyhow!(
"Código de idioma inválido: '{}'. Esperado formato ISO 639-2 (3 letras minúsculas, ex: 'por', 'eng')",
code
))
}
}
pub fn as_str(&self) -> &str {
&self.0
}
}
impl std::fmt::Display for TrackLanguage {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.0)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn idioma_valido() {
assert!(TrackLanguage::new("por").is_ok());
assert!(TrackLanguage::new("eng").is_ok());
}
#[test]
fn idioma_invalido_curto() {
assert!(TrackLanguage::new("pt").is_err());
}
#[test]
fn idioma_invalido_maiusculo() {
assert!(TrackLanguage::new("POR").is_err());
}
#[test]
fn idioma_invalido_longo() {
assert!(TrackLanguage::new("port").is_err());
}
}