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:
@@ -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))
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user