Files
simple-multimidia-track-aud…/DEVELOPMENT_PLAN.md
T

265 lines
9.6 KiB
Markdown

# Plano de Desenvolvimento — Simple Multimedia Track Audio Editor
**Versão:** 1.0
**Data:** 28/02/2026
**Referência:** PRD v1.1
---
## Estratégia Geral
O desenvolvimento segue a **Clean Architecture** de dentro para fora: as camadas mais internas (domínio) são implementadas e testadas primeiro, sem qualquer dependência de I/O, frameworks ou processos externos. A UI é sempre a última camada a ser construída.
```
Fase 1: Setup
└── Fase 2: Domain (testável, sem I/O)
└── Fase 3: Application (testável com mocks)
└── Fase 4: Adapters (integração com FFmpeg)
└── Fase 5: Infrastructure (processo real)
└── Fase 6: UI (montagem final)
```
Cada fase deve estar **compilando e com testes passando** antes de avançar para a próxima.
---
## Fase 1 — Setup do Projeto
**Objetivo:** Preparar o ambiente antes de escrever qualquer lógica de negócio.
### Tarefas
- [ ] Atualizar `Cargo.toml` com as dependências:
```toml
eframe = "0.27"
anyhow = "1"
serde = { version = "1", features = ["derive"] }
tokio = { version = "1", features = ["process", "rt-multi-thread", "macros"] }
serde_json = "1"
```
- [ ] Criar a estrutura de pastas:
```
src/
├── domain/
│ ├── entities/
│ └── value_objects/
├── application/
│ ├── use_cases/
│ └── ports/
├── adapters/
│ ├── ffmpeg/
│ └── filesystem/
├── infrastructure/
│ └── process/
└── ui/
├── components/
└── app.rs
```
- [ ] Declarar os módulos em `src/main.rs`
- [ ] Verificar que o projeto compila (`cargo check`)
---
## Fase 2 — Domain (núcleo puro)
**Objetivo:** Modelar o problema de negócio sem nenhum acoplamento a frameworks, I/O ou processos externos.
> Regra: nenhum `use std::process`, nenhum `use eframe`, nenhuma chamada de rede ou filesystem nessa camada.
### Value Objects (`src/domain/value_objects/`)
| Tipo | Implementação |
|---|---|
| `FilePath` | Newtype sobre `PathBuf`; derivar `Clone`, `Debug`, `Serialize`, `Deserialize` |
| `TrackId` | Newtype opaco sobre `u32`; derivar `Clone`, `Copy`, `Debug`, `PartialEq`, `Eq`, `Hash` |
| `SyncOffset` | Newtype sobre `i64` (milissegundos, **nunca `f64`**); implementar método `from_seconds_str` e `as_ms` |
| `TrackLanguage` | Newtype sobre `String` (ex: `"por"`, `"eng"`); validar formato ISO 639-2 |
### Entities (`src/domain/entities/`)
| Tipo | Campos principais |
|---|---|
| `VideoFile` | `path: FilePath` |
| `AudioTrack` | `id: TrackId`, `path: FilePath`, `offset: SyncOffset`, `language: TrackLanguage` |
| `SubtitleTrack` | `id: TrackId`, `path: FilePath`, `offset: SyncOffset`, `language: TrackLanguage` |
| `MediaTrackInfo` | `id: TrackId`, `kind: TrackKind` (enum: Video/Audio/Subtitle), `codec: String`, `language: Option<TrackLanguage>` |
| `MkvOutput` | `path: FilePath` |
| `Project` | Entidade raiz — ver abaixo |
#### Estrutura de `Project`
```rust
pub struct Project {
pub source: VideoFile,
pub tracks: Vec<Track>, // faixas externas adicionadas pelo usuário
pub existing_tracks: Vec<MediaTrackInfo>, // faixas lidas do arquivo via ffprobe
pub output: MkvOutput,
}
```
### Testes obrigatórios
- [ ] `SyncOffset::from_seconds_str("1.2")` → `SyncOffset(1200)`
- [ ] `SyncOffset::from_seconds_str("-0.5")` → `SyncOffset(-500)`
- [ ] `Project` não aceita `output.path` igual a `source.path`
- [ ] `TrackId` é opaco (não expõe indexação interna)
---
## Fase 3 — Application (casos de uso e ports)
**Objetivo:** Definir o que o sistema faz sem saber como. Ports são traits; use cases orquestram entidades.
### Ports (`src/application/ports/`)
```rust
// MediaInfoPort: inspeciona faixas de um arquivo de mídia
pub trait MediaInfoPort {
fn probe(&self, path: &FilePath) -> Result<Vec<MediaTrackInfo>>;
}
// MediaProcessorPort: executa o processamento final
pub trait MediaProcessorPort {
fn execute(&self, args: Vec<String>) -> Result<()>;
}
// FileSystemPort: abstrai acesso ao sistema de arquivos
pub trait FileSystemPort {
fn exists(&self, path: &FilePath) -> bool;
}
```
### Use Cases (`src/application/use_cases/`)
Implementar nesta ordem (dependência crescente):
| # | Use Case | Descrição |
|---|---|---|
| 1 | `LoadMediaInfo` | Usa `MediaInfoPort` para popular `Project::existing_tracks` |
| 2 | `AddAudioTrack` | Adiciona `AudioTrack` externo ao `Project::tracks` |
| 3 | `AddSubtitle` | Adiciona `SubtitleTrack` externo ao `Project::tracks` |
| 4 | `AdjustSync` | Altera `SyncOffset` de uma faixa existente pelo `TrackId` |
| 5 | `EditExistingTrackSync` | Ajusta offset de faixa já presente no arquivo original |
| 6 | `SetTrackLanguage` | Altera idioma de uma faixa pelo `TrackId` |
| 7 | `GenerateOutput` | Constrói o comando final e delega ao `MediaProcessorPort` |
### Testes obrigatórios
- [ ] Usar mocks dos ports (sem chamar nenhum processo externo)
- [ ] `GenerateOutput` sempre produz comando com `-c copy` (verificado via mock)
- [ ] `AdjustSync` com `TrackId` inexistente retorna erro
- [ ] `AddAudioTrack` em `Project` sem `source` retorna erro
---
## Fase 4 — Adapters (implementações concretas)
**Objetivo:** Conectar o domínio ao FFmpeg e ao filesystem real.
### FFmpeg (`src/adapters/ffmpeg/`)
#### `FfmpegCommandBuilder`
Responsabilidade única: converter `Project` em `Vec<String>` de argumentos para o FFmpeg.
Regras invariantes:
- `-c copy` **sempre presente** (RNF-01 — nunca opcional, nunca configurável)
- `SyncOffset(i64 ms)` → `-itsoffset 1.200` (conversão feita **somente aqui**)
- `TrackId` → `-map 0:a:N` (mapeamento de índice feito **somente aqui**)
Exemplo de saída esperada:
```
ffmpeg -i input.mkv -itsoffset 1.200 -i audio_pt.aac -map 0:v -map 0:a -map 1:a -c copy -metadata:s:a:1 language=por output.mkv
```
- [ ] Implementar `FfmpegCommandBuilder::build(project: &Project) -> Vec<String>`
- [ ] Testes: verificar presença de `-c copy`, ordem dos `-map`, formato de `-itsoffset`
#### `FfprobeGateway`
Implementa `MediaInfoPort`. Executa:
```
ffprobe -v quiet -print_format json -show_streams <path>
```
e mapeia a saída JSON para `Vec<MediaTrackInfo>`.
- [ ] Implementar parsing de JSON via `serde_json`
- [ ] Mapear `codec_type` para `TrackKind`
- [ ] Mapear `tags.language` para `Option<TrackLanguage>`
#### `FfmpegGateway`
Implementa `MediaProcessorPort`. Executa o processo real do FFmpeg e captura stderr.
- [ ] Capturar stderr para exibição de erros (RF-07)
- [ ] Retornar erro com mensagem legível em caso de código de saída não-zero
### Filesystem (`src/adapters/filesystem/`)
- [ ] `FilePickerAdapter`: abstrai seleção de arquivo via diálogo nativo (usar crate `rfd`)
---
## Fase 5 — Infrastructure
**Objetivo:** Execução real e não-bloqueante de processos externos.
### Módulo `src/infrastructure/process/`
- [ ] Implementar execução via `tokio::process::Command` (assíncrono, RNF-05)
- [ ] Capturar stdout e stderr em stream (para exibir progresso em tempo real — RF-07)
### Validação na inicialização
- [ ] Verificar se `ffmpeg` está disponível no `PATH` (DT-06)
- [ ] Verificar se `ffprobe` está disponível no `PATH`
- [ ] Exibir mensagem clara ao usuário se algum dos dois estiver ausente
---
## Fase 6 — UI (camada mais externa)
**Objetivo:** Interface gráfica que conecta o usuário ao `Project` via use cases.
> Todo estado da aplicação vive no `Project`. A UI apenas lê e dispara use cases.
> Termos técnicos do FFmpeg **nunca aparecem na interface** (ver PRD seção 13).
### Componentes (`src/ui/components/`), em ordem de construção
| # | Componente | Descrição |
|---|---|---|
| 1 | `VideoSelector` | Seletor de arquivo de vídeo; dispara `LoadMediaInfo` ao confirmar |
| 2 | `ExistingTrackList` | Lista faixas detectadas (áudio/legenda); permite editar offset de cada uma |
| 3 | `AddAudioTrackForm` | Formulário para adicionar faixa de áudio externa (arquivo, idioma, offset) |
| 4 | `AddSubtitleForm` | Formulário para adicionar legenda externa (arquivo, idioma, offset) |
| 5 | `SyncOffsetField` | Campo de offset em segundos (ex: `-1.2s`); converte para `SyncOffset(ms)` internamente |
| 6 | `LanguageField` | Seletor/input de idioma (ex: `por`, `eng`) |
| 7 | `OutputSelector` | Campo de caminho de saída + extensão `.mkv` forçada |
| 8 | `ExecutionPanel` | Botão "Gerar MKV", exibição de progresso e erros legíveis |
### App (`src/ui/app.rs`)
- [ ] Implementar `eframe::App` para `App`
- [ ] `App` contém `project: Project` como única fonte de verdade do estado
- [ ] Despachar eventos de UI para os use cases correspondentes
- [ ] Integrar execução assíncrona (tokio) com o loop de UI do eframe
---
## Critérios de Conclusão (v1.0)
Alinhados com o PRD seção 12:
- [ ] É possível selecionar um arquivo de vídeo base
- [ ] Ao selecionar o vídeo, as faixas existentes são listadas automaticamente (via `ffprobe`)
- [ ] É possível adicionar uma ou mais faixas de áudio externas
- [ ] É possível adicionar uma ou mais faixas de legenda
- [ ] É possível definir offset de sincronização por faixa ao adicionar uma nova faixa
- [ ] É possível editar o offset de sincronização de uma faixa já existente no arquivo
- [ ] É possível atribuir idioma a cada faixa
- [ ] O arquivo MKV é gerado corretamente ao confirmar
- [ ] Erros do FFmpeg são exibidos de forma legível
- [ ] A interface não trava durante o processamento
- [ ] Nenhum reencoding ocorre (verificável via `ffprobe` no arquivo de saída)
- [ ] O flag `-c copy` está sempre presente no comando gerado (verificável em modo debug)