232 lines
11 KiB
Markdown
232 lines
11 KiB
Markdown
# Progresso de Implementação
|
||
|
||
**Data:** 28/02/2026
|
||
**Status:** Fases 1–6 concluídas + exportação de faixa + exibição/remoção de faixas adicionadas + layout responsivo + progresso em tempo real + cancelamento de geração — compilando, 33 testes passando, 14 warnings de `unused`/`dead_code` pendentes, aplicação executável
|
||
**Referência:** DEVELOPMENT_PLAN.md v1.1
|
||
|
||
---
|
||
|
||
## Resumo Executivo
|
||
|
||
Todas as 6 fases do plano de desenvolvimento foram implementadas. O projeto compila sem erros,
|
||
33 testes unitários passam e a aplicação pode ser executada com `cargo run`.
|
||
|
||
Ainda há 14 warnings de `unused`/`dead_code` (estruturas e métodos não chamados diretamente pela UI) que não causam falhas mas devem ser tratados antes de uma liberação final.
|
||
|
||
Após a conclusão das fases, foram implementadas funcionalidades adicionais:
|
||
- Exportação de faixa de áudio ou legenda diretamente da lista de faixas existentes
|
||
- Exibição das faixas externas adicionadas com opção de remoção individual
|
||
- Layout responsivo com painéis fixos (cabeçalho, rodapé, barra lateral) e área central com scroll
|
||
- Progresso em tempo real via `run_ffmpeg_async` (Opção A: `std::sync::mpsc` em toda a cadeia)
|
||
- Cancelamento de geração em andamento via botão "Cancelar" — encerra o processo FFmpeg filho imediatamente
|
||
|
||
---
|
||
|
||
## Status por Fase
|
||
|
||
| Fase | Descrição | Status |
|
||
|------|----------------------------------|-------------|
|
||
| 1 | Setup do projeto | ✅ Concluída |
|
||
| 2 | Domain (núcleo puro) | ✅ Concluída |
|
||
| 3 | Application (use cases + ports) | ✅ Concluída |
|
||
| 4 | Adapters (FFmpeg + filesystem) | ✅ Concluída |
|
||
| 5 | Infrastructure (processo async) | ✅ Concluída |
|
||
| 6 | UI (eframe/egui) | ✅ Concluída |
|
||
|
||
---
|
||
|
||
## Estrutura de Arquivos Criados
|
||
|
||
```
|
||
src/
|
||
├── main.rs — entrada, configuração da janela eframe
|
||
│
|
||
├── domain/
|
||
│ ├── mod.rs
|
||
│ ├── value_objects/
|
||
│ │ ├── mod.rs
|
||
│ │ ├── file_path.rs — FilePath (newtype sobre PathBuf)
|
||
│ │ ├── track_id.rs — TrackId (newtype opaco sobre u32)
|
||
│ │ ├── sync_offset.rs — SyncOffset (i64 ms, nunca f64)
|
||
│ │ └── track_language.rs — TrackLanguage (ISO 639-2, validado)
|
||
│ └── entities/
|
||
│ ├── mod.rs
|
||
│ ├── project.rs — Project (entidade raiz / fonte de verdade)
|
||
│ ├── video_file.rs — VideoFile
|
||
│ ├── audio_track.rs — AudioTrack
|
||
│ ├── subtitle_track.rs — SubtitleTrack
|
||
│ ├── media_track_info.rs — MediaTrackInfo + TrackKind
|
||
│ ├── mkv_output.rs — MkvOutput
|
||
│ └── track.rs — Track (enum: Audio | Subtitle)
|
||
│
|
||
├── application/
|
||
│ ├── mod.rs
|
||
│ ├── ports/
|
||
│ │ └── mod.rs — MediaInfoPort, MediaProcessorPort, FileSystemPort
|
||
│ └── use_cases/
|
||
│ ├── mod.rs
|
||
│ ├── load_media_info.rs — popula Project::existing_tracks via ffprobe
|
||
│ ├── add_audio_track.rs — adiciona AudioTrack ao Project
|
||
│ ├── add_subtitle.rs — adiciona SubtitleTrack ao Project
|
||
│ ├── adjust_sync.rs — altera SyncOffset de faixa externa
|
||
│ ├── edit_existing_track_sync.rs — altera SyncOffset de faixa existente
|
||
│ ├── export_track.rs — exporta faixa de áudio ou legenda para arquivo
|
||
│ ├── remove_track.rs — remove faixa externa do projeto pelo TrackId
|
||
│ ├── set_track_language.rs — altera idioma de faixa externa
|
||
│ └── generate_output.rs — constrói comando e delega ao MediaProcessorPort
|
||
│
|
||
├── adapters/
|
||
│ ├── mod.rs
|
||
│ ├── ffmpeg/
|
||
│ │ ├── mod.rs
|
||
│ │ ├── command_builder.rs — FfmpegCommandBuilder: Project → Vec<String>; build_export()
|
||
│ │ ├── ffprobe_gateway.rs — FfprobeGateway impl MediaInfoPort
|
||
│ │ └── ffmpeg_gateway.rs — FfmpegGateway impl MediaProcessorPort
|
||
│ └── filesystem/
|
||
│ ├── mod.rs — RealFileSystem impl FileSystemPort
|
||
│ └── file_picker.rs — FilePickerAdapter; save_audio(codec), save_subtitle(codec)
|
||
│
|
||
├── infrastructure/
|
||
│ ├── mod.rs
|
||
│ └── process/
|
||
└── mod.rs — run_ffmpeg_async (tokio, cancel via oneshot), validate_dependencies()
|
||
│
|
||
└── ui/
|
||
├── mod.rs
|
||
├── app.rs — eframe::App; Project como única fonte de verdade; cancel_tx para interromper FFmpeg
|
||
└── components/
|
||
├── mod.rs
|
||
├── video_selector.rs — seleção do vídeo base
|
||
├── output_selector.rs — seleção do arquivo de saída (.mkv)
|
||
├── existing_track_list.rs — lista faixas detectadas + edição de offset│ ├── added_track_list.rs — lista faixas externas adicionadas + botão remover ├── add_audio_track_form.rs — formulário: áudio externo
|
||
├── add_subtitle_form.rs — formulário: legenda externa
|
||
├── sync_offset_field.rs — campo de atraso em segundos
|
||
├── language_field.rs — campo de idioma ISO 639-2
|
||
└── execution_panel.rs — botão gerar, botão cancelar, progresso, estados: Idle/Running/Success/Cancelled/Error
|
||
```
|
||
|
||
---
|
||
|
||
## Testes Unitários (33/33 passando)
|
||
|
||
### Domain — Value Objects
|
||
| Teste | Resultado |
|
||
|-------|-----------|
|
||
| `sync_offset::from_seconds_str_positivo` — `"1.2"` → `SyncOffset(1200)` | ✅ |
|
||
| `sync_offset::from_seconds_str_negativo` — `"-0.5"` → `SyncOffset(-500)` | ✅ |
|
||
| `sync_offset::from_seconds_str_com_sufixo_s` | ✅ |
|
||
| `sync_offset::from_seconds_str_invalido` | ✅ |
|
||
| `sync_offset::from_seconds_str_zero` | ✅ |
|
||
| `track_language::idioma_valido` — `"por"`, `"eng"` | ✅ |
|
||
| `track_language::idioma_invalido_curto` — `"pt"` | ✅ |
|
||
| `track_language::idioma_invalido_maiusculo` — `"POR"` | ✅ |
|
||
| `track_language::idioma_invalido_longo` — `"port"` | ✅ |
|
||
|
||
### Domain — Entities
|
||
| Teste | Resultado |
|
||
|-------|-----------|
|
||
| `project::projeto_valido` | ✅ |
|
||
| `project::projeto_invalido_mesmo_caminho` — output == source retorna erro | ✅ |
|
||
| `project::track_id_opaco` — TrackId não expõe indexação interna | ✅ |
|
||
|
||
### Application — Use Cases (todos com mocks, sem I/O real)
|
||
| Teste | Resultado |
|
||
|-------|-----------|
|
||
| `load_media_info::carrega_faixas_no_projeto` | ✅ |
|
||
| `add_audio_track::adiciona_audio_no_projeto` | ✅ |
|
||
| `add_subtitle::adiciona_legenda_no_projeto` | ✅ |
|
||
| `adjust_sync::ajusta_offset_existente` | ✅ |
|
||
| `adjust_sync::erro_se_id_inexistente` | ✅ |
|
||
| `edit_existing_track_sync::edita_offset_de_faixa_existente` | ✅ |
|
||
| `edit_existing_track_sync::erro_se_faixa_existente_nao_encontrada` | ✅ |
|
||
| `set_track_language::altera_idioma_da_faixa` | ✅ |
|
||
| `generate_output::sempre_inclui_c_copy` | ✅ |
|
||
|
||
### Adapters — FfmpegCommandBuilder
|
||
| Teste | Resultado |
|
||
|-------|-----------|
|
||
| `sempre_contem_c_copy` — invariante RNF-01 | ✅ |
|
||
| `output_e_o_ultimo_argumento` | ✅ |
|
||
| `itsoffset_formato_correto` — `1200ms` → `"1.200"` | ✅ |
|
||
| `mapa_faixa_externa_de_audio` — `map 1:a` | ✅ |
|
||
| `metadata_idioma_audio` — `-metadata:s:a:0 language=por` | ✅ |
|
||
|
||
### Application — ExportTrack
|
||
| Teste | Resultado |
|
||
|-------|-----------|
|
||
| `export_track::exporta_audio_com_c_a_copy` — `-c:a copy` presente, sem `-c copy` genérico | ✅ |
|
||
| `export_track::exporta_audio_mapeia_stream_correto` — `0:stream_index` correto | ✅ |
|
||
| `export_track::exporta_legenda_sem_c_copy` — nenhuma forma de `-c` para legendas | ✅ |
|
||
| `export_track::exporta_legenda_output_e_ultimo_argumento` | ✅ |
|
||
| `export_track::rejeita_faixa_de_video` — retorna erro para `TrackKind::Video` | ✅ |
|
||
|
||
### Application — RemoveTrack
|
||
| Teste | Resultado |
|
||
|-------|-----------|
|
||
| `remove_track::remove_faixa_existente` — faixa removida do Vec | ✅ |
|
||
| `remove_track::erro_se_id_inexistente` — retorna erro se id não existe | ✅ |
|
||
|
||
---
|
||
|
||
## Dependências (Cargo.toml)
|
||
|
||
```toml
|
||
eframe = "0.27"
|
||
anyhow = "1"
|
||
serde = { version = "1", features = ["derive"] }
|
||
serde_json = "1"
|
||
tokio = { version = "1", features = ["process", "rt-multi-thread", "macros", "io-util", "sync"] }
|
||
rfd = "0.14"
|
||
```
|
||
|
||
---
|
||
|
||
## Invariantes Garantidas
|
||
|
||
- **`-c copy` sempre presente** — `FfmpegCommandBuilder::build()` emite incondicionalmente (RNF-01)
|
||
- **`SyncOffset` nunca usa `f64`** — armazenado como `i64` ms; conversão para string FFmpeg feita exclusivamente no `FfmpegCommandBuilder`
|
||
- **`TrackId` é opaco** — não expõe índice interno; mapeamento para `-map N:tipo` feito apenas no adapter
|
||
- **`Project` rejeita `output == source`** — validado no construtor
|
||
- **Termos FFmpeg nunca aparecem na UI** — a interface usa linguagem do usuário final
|
||
- **Exceção documentada ao RNF-01** — `build_export()` omite `-c copy` exclusivamente para legendas (conversão de container de texto, sem processamento de mídia); comentário inline explica a exceção
|
||
|
||
---
|
||
|
||
## O Que Falta (Backlog de Refinamento)
|
||
|
||
### Funcional
|
||
- [x] Exportar faixa de áudio ou legenda existente para arquivo separado
|
||
- [x] Exibição de faixas externas adicionadas com opção de remover
|
||
- [x] Progresso em tempo real via `run_ffmpeg_async` — `run_ffmpeg_async` usa `std::sync::mpsc::Sender<String>`; `start_generation` cria tokio Runtime + thread encaminhadora; cada linha de stderr do FFmpeg aparece no log antes do término
|
||
- [x] Cancelamento de geração — botão "Cancelar" visível durante `Running`; sinal via `tokio::sync::oneshot`; `child.kill().await` no `tokio::select!`; estado `ExecutionState::Cancelled` exibido em amarelo
|
||
|
||
### Qualidade
|
||
- [ ] Corrigir 14 warnings de `unused`/`dead_code` identificados por `cargo build`:
|
||
- `next_input_idx` atribuído mas não lido após o loop de offset-inputs (`command_builder.rs`)
|
||
- `RealFileSystem` nunca construído; `FileSystemPort` nunca usado na UI
|
||
- `AdjustSync`, `GenerateOutput`, `SetTrackLanguage` nunca instanciados diretamente (UI usa chamadas diretas nos métodos)
|
||
- Métodos `find_track_mut`, `language`, `set_offset`, `set_language`, `from_ms` declarados mas não chamados
|
||
- [ ] Testes de integração com FFmpeg real (requer ffmpeg instalado no CI)
|
||
- [ ] Testes de snapshot para o `FfmpegCommandBuilder` com projetos mais complexos
|
||
|
||
### UI
|
||
- [x] Layout responsivo mais refinado
|
||
- [ ] Exibição do comando FFmpeg gerado em modo debug (ex: painel colapsável na `ExecutionPanel`)
|
||
- [ ] Persistência do estado do projeto em disco (`serde` já disponível)
|
||
- [ ] Mensagens de erro mais amigáveis na UI
|
||
|
||
---
|
||
|
||
## Como Executar
|
||
|
||
```bash
|
||
# Verificar compilação
|
||
cargo check
|
||
|
||
# Rodar testes
|
||
cargo test
|
||
|
||
# Executar a aplicação (requer ffmpeg e ffprobe no PATH)
|
||
cargo run
|
||
```
|