diff --git a/PRD.md b/PRD.md index e14e9e1..305b804 100644 --- a/PRD.md +++ b/PRD.md @@ -82,6 +82,20 @@ As ferramentas existentes são complexas (ex: interface direta do FFmpeg via CLI - O usuário pode atribuir um código de idioma a cada faixa (ex: `por`, `eng`) - Implementado via `ffmpeg -metadata:s` +### RF-10 — Definir nome da faixa + +- O usuário pode preencher um título livre para cada faixa externa adicionada (ex: `Português`, `Comentários`) +- O campo aceita string vazia, que instrui o `FfmpegCommandBuilder` a emitir `title=` limpando qualquer título herdado do arquivo fonte +- Objetivo principal: sobrescrever títulos gerados automaticamente por ferramentas externas (ex: `"ISO Media file produced by Google Inc."` em arquivos M4A do Google) que confundem players como Jellyfin mobile +- Implementado via `ffmpeg -metadata:s:a:{idx} title=` ou `-metadata:s:s:{idx} title=`; sempre emitido, mesmo quando vazio + +### RF-11 — Definir faixa padrão + +- O usuário pode marcar uma faixa externa como “faixa padrão” do container MKV +- Quando marcada, o `FfmpegCommandBuilder` emite `-disposition:a/s:{idx} default` para a faixa selecionada e `-disposition:a/s:{orig_idx} 0` para todas as faixas existentes do mesmo tipo, removendo o flag `default=1` que o arquivo fonte carrega +- Objetivo: garantir que players como Jellyfin selecionem automaticamente a faixa preferida sem depender de configuração manual no side do player +- Implementado via `ffmpeg -disposition:a:{idx} default` + ### RF-05 — Selecionar arquivo de vídeo base - O usuário seleciona o arquivo de vídeo de entrada @@ -319,6 +333,7 @@ src/ | DT-04 | Compatibilidade de codecs | Alguns codecs podem não ser aceitos | Usar MKV como container padrão | | DT-05 | Performance de processo externo | Pequeno overhead | Aceitável — mux é rápido | | DT-06 | ffprobe como dependência adicional | Parsing de JSON da saída do ffprobe | Validar presença de ffprobe na inicialização; exibir mensagem clara se ausente | +| DT-07 | Compatibilidade com Jellyfin mobile | Faixas não selecionadas automaticamente | Investigando; as RFC RF-10 e RF-11 mitigam parcialmente — `title` e `disposition` são emitidos corretamente, porém o Jellyfin mobile pode ter comportamento diferente do web; requer testes com arquivo gerado | --- diff --git a/PROGRESS.md b/PROGRESS.md index 7015360..c4a25b7 100644 --- a/PROGRESS.md +++ b/PROGRESS.md @@ -112,6 +112,39 @@ src/ --- +## Inventário de Mudanças — Compatibilidade com Jellyfin (01/03/2026) + +### Contexto + +Arquivos M4A produzidos pelo Google (ex: downloads do YouTube Music ou Google Drive) carregam +um metadado `title` na stream de áudio com o valor `"ISO Media file produced by Google Inc."`. Quando +o FFmpeg faz o mux sem sobrescrever esse metadado, ele é copiado literalmente para a faixa do MKV. + +O Jellyfin **web** é tolerante e usa a tag `language` para selecionar a faixa preferida. Já o +Jellyfin **mobile** usa o campo `title` da stream para exibir e selecionar faixas — quando não +reconhece o título como idioma, não realiza seleção automática e mantém a faixa original. + +Algo semelhante ocorre com `disposition:default`: a faixa original do fonte fica com `default=1`; +a nova faixa fica com `default=0`; o Jellyfin mobile respeitam esse flag e tocam a original. + +> **Status:** mudanças implementadas e compilando; investigação em andamento — o problema no +> Jellyfin mobile ainda não foi completamente resolvido. + +### Mudanças por camada + +| Arquivo | Mudança | +| --- | --- | +| `domain/entities/audio_track.rs` | Adicionado `is_default: bool` e `title: String` a `AudioTrack` | +| `domain/entities/subtitle_track.rs` | Adicionado `is_default: bool` e `title: String` a `SubtitleTrack` | +| `application/use_cases/add_audio_track.rs` | Parâmetros `is_default` e `title` adicionados ao `execute()` | +| `application/use_cases/add_subtitle.rs` | Parâmetros `is_default` e `title` adicionados ao `execute()` | +| `adapters/ffmpeg/command_builder.rs` | Emite `-disposition:a/s:{idx} 0` nas faixas existentes quando uma externa é marcada como padrão; emite `-disposition:a/s:{idx} default` na faixa marcada; emite `-metadata:s:a/s:{idx} title={valor}` para toda faixa externa (string vazia apaga o título herdado) | +| `ui/components/add_audio_track_form.rs` | Campo “Nome da faixa” (`TextEdit`) e checkbox “Definir como faixa padrão” | +| `ui/components/add_subtitle_form.rs` | Campo “Nome da faixa” (`TextEdit`) e checkbox “Definir como faixa padrão” | +| `ui/components/batch_panel.rs` | `PendingAudio` e `PendingSubtitle` propagam `is_default` e `title` | + +--- + ## Testes Unitários (33/33 passando) ### Domain — Value Objects @@ -200,6 +233,7 @@ rfd = "0.14" - **`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 +- **`title=` sempre emitido** — `FfmpegCommandBuilder` emite `-metadata:s:a/s:{idx} title=` para toda faixa externa, garantindo que títulos herdados do arquivo fonte sejam sobrescritos; string vazia limpa o campo no MKV --- diff --git a/src/adapters/ffmpeg/command_builder.rs b/src/adapters/ffmpeg/command_builder.rs index 48e252f..46afa6d 100644 --- a/src/adapters/ffmpeg/command_builder.rs +++ b/src/adapters/ffmpeg/command_builder.rs @@ -91,6 +91,18 @@ impl FfmpegCommandBuilder { ext_idx += 1; } + // ── Opções de muxing ───────────────────────────────────────────────────── + // -max_interleave_delta 0: remove o limite do buffer de intercalação (padrão: 10s). + // Sem isso, quando o áudio externo fica à frente do vídeo, o FFmpeg descarta + // pacotes silenciosamente, gerando trechos mudos no output. Com 0, nenhum + // pacote é descartado por excesso de delta — apenas memória RAM é consumida. + // -avoid_negative_ts make_zero: normaliza timestamps negativos comuns em M4A/AAC + // de serviços de streaming (DASH), que causam dessincronização no muxer MKV. + args.push("-max_interleave_delta".to_string()); + args.push("0".to_string()); + args.push("-avoid_negative_ts".to_string()); + args.push("make_zero".to_string()); + // ── -c copy (SEMPRE — invariante central do produto) ──────────────────── args.push("-c".to_string()); args.push("copy".to_string()); @@ -115,16 +127,96 @@ impl FfmpegCommandBuilder { Track::Audio(t) => { args.push(format!("-metadata:s:a:{}", ext_audio_idx)); args.push(format!("language={}", t.language)); + // Sempre emite title (mesmo vazio) para sobrescrever qualquer título + // herdado do arquivo fonte (ex: "ISO Media file produced by Google Inc."). + // Players como Jellyfin mobile usam o title para identificar a faixa. + args.push(format!("-metadata:s:a:{}", ext_audio_idx)); + args.push(format!("title={}", t.title)); ext_audio_idx += 1; } Track::Subtitle(t) => { args.push(format!("-metadata:s:s:{}", ext_sub_idx)); args.push(format!("language={}", t.language)); + args.push(format!("-metadata:s:s:{}", ext_sub_idx)); + args.push(format!("title={}", t.title)); ext_sub_idx += 1; } } } + // ── Disposições (faixa padrão) ──────────────────────────────────────── + // Quando o usuário marca uma faixa externa como "padrão", é necessário: + // 1. Remover o flag `default` de todas as faixas existentes do mesmo tipo + // (o arquivo fonte geralmente já tem `disposition:default=1` na sua + // primeira faixa de áudio, e Jellyfin/Kodi respeitam esse flag). + // 2. Emitir `-disposition:a/s:{idx} default` para a faixa externa marcada. + // + // Índices de disposição seguem a ordem de streams do tipo no output: + // - Faixas existentes (existing_tracks) vêm primeiro, na ordem do map. + // - Faixas externas (tracks) vêm depois, na ordem de adição. + + let existing_audio_count_disp = project + .existing_tracks + .iter() + .filter(|t| matches!(t.kind, TrackKind::Audio)) + .count(); + let existing_sub_count_disp = project + .existing_tracks + .iter() + .filter(|t| matches!(t.kind, TrackKind::Subtitle)) + .count(); + + let any_ext_audio_default = project + .tracks + .iter() + .any(|t| matches!(t, Track::Audio(a) if a.is_default)); + let any_ext_sub_default = project + .tracks + .iter() + .any(|t| matches!(t, Track::Subtitle(s) if s.is_default)); + + // Remove default das faixas existentes quando uma faixa externa é padrão + if any_ext_audio_default { + for i in 0..existing_audio_count_disp { + args.push(format!("-disposition:a:{}", i)); + args.push("0".to_string()); + } + } + if any_ext_sub_default { + for i in 0..existing_sub_count_disp { + args.push(format!("-disposition:s:{}", i)); + args.push("0".to_string()); + } + } + + // Emite `default` para as faixas externas marcadas + let mut disp_audio_idx = 0usize; + let mut disp_sub_idx = 0usize; + for track in &project.tracks { + match track { + Track::Audio(a) => { + if a.is_default { + args.push(format!( + "-disposition:a:{}", + existing_audio_count_disp + disp_audio_idx + )); + args.push("default".to_string()); + } + disp_audio_idx += 1; + } + Track::Subtitle(s) => { + if s.is_default { + args.push(format!( + "-disposition:s:{}", + existing_sub_count_disp + disp_sub_idx + )); + args.push("default".to_string()); + } + disp_sub_idx += 1; + } + } + } + // ── Output ────────────────────────────────────────────────────────────── args.push(project.output.path.to_string_lossy().to_string()); @@ -216,6 +308,8 @@ mod tests { FilePath::from("audio.aac"), SyncOffset::from_ms(1200), TrackLanguage::new("por").unwrap(), + false, + String::new(), ) .unwrap(); @@ -233,6 +327,8 @@ mod tests { FilePath::from("audio.aac"), SyncOffset::default(), TrackLanguage::new("por").unwrap(), + false, + String::new(), ) .unwrap(); @@ -252,6 +348,8 @@ mod tests { FilePath::from("audio.aac"), SyncOffset::default(), TrackLanguage::new("por").unwrap(), + false, + String::new(), ) .unwrap(); diff --git a/src/application/use_cases/add_audio_track.rs b/src/application/use_cases/add_audio_track.rs index cce0ab2..6113c90 100644 --- a/src/application/use_cases/add_audio_track.rs +++ b/src/application/use_cases/add_audio_track.rs @@ -1,6 +1,6 @@ -use anyhow::Result; use crate::domain::entities::{AudioTrack, Project, Track}; use crate::domain::value_objects::{FilePath, SyncOffset, TrackLanguage}; +use anyhow::Result; /// Adiciona uma faixa de áudio externa ao projeto. pub struct AddAudioTrack; @@ -11,9 +11,11 @@ impl AddAudioTrack { path: FilePath, offset: SyncOffset, language: TrackLanguage, + is_default: bool, + title: String, ) -> Result<()> { let id = project.next_track_id(); - let track = AudioTrack::new(id, path, offset, language); + let track = AudioTrack::new(id, path, offset, language, is_default, title); project.tracks.push(Track::Audio(track)); Ok(()) } @@ -39,6 +41,8 @@ mod tests { FilePath::from("audio_pt.aac"), SyncOffset::default(), lang, + false, + String::new(), ) .unwrap(); diff --git a/src/application/use_cases/add_subtitle.rs b/src/application/use_cases/add_subtitle.rs index 1c88e37..f209c0e 100644 --- a/src/application/use_cases/add_subtitle.rs +++ b/src/application/use_cases/add_subtitle.rs @@ -1,6 +1,6 @@ -use anyhow::Result; use crate::domain::entities::{Project, SubtitleTrack, Track}; use crate::domain::value_objects::{FilePath, SyncOffset, TrackLanguage}; +use anyhow::Result; /// Adiciona uma faixa de legenda externa ao projeto. pub struct AddSubtitle; @@ -11,9 +11,11 @@ impl AddSubtitle { path: FilePath, offset: SyncOffset, language: TrackLanguage, + is_default: bool, + title: String, ) -> Result<()> { let id = project.next_track_id(); - let track = SubtitleTrack::new(id, path, offset, language); + let track = SubtitleTrack::new(id, path, offset, language, is_default, title); project.tracks.push(Track::Subtitle(track)); Ok(()) } @@ -38,6 +40,8 @@ mod tests { FilePath::from("sub_en.srt"), SyncOffset::default(), lang, + false, + String::new(), ) .unwrap(); diff --git a/src/application/use_cases/adjust_sync.rs b/src/application/use_cases/adjust_sync.rs index 869c85f..61aed55 100644 --- a/src/application/use_cases/adjust_sync.rs +++ b/src/application/use_cases/adjust_sync.rs @@ -37,6 +37,8 @@ mod tests { FilePath::from("audio.aac"), SyncOffset::default(), lang, + false, + String::new(), ) .unwrap(); let id = project.tracks[0].id(); diff --git a/src/application/use_cases/generate_output.rs b/src/application/use_cases/generate_output.rs index 36b2155..cb767fd 100644 --- a/src/application/use_cases/generate_output.rs +++ b/src/application/use_cases/generate_output.rs @@ -49,6 +49,8 @@ mod tests { FilePath::from("audio_pt.aac"), SyncOffset::from_ms(1200), TrackLanguage::new("por").unwrap(), + false, + String::new(), ) .unwrap(); diff --git a/src/application/use_cases/remove_track.rs b/src/application/use_cases/remove_track.rs index dc96d9d..308dc97 100644 --- a/src/application/use_cases/remove_track.rs +++ b/src/application/use_cases/remove_track.rs @@ -38,6 +38,8 @@ mod tests { FilePath::from("audio.aac"), SyncOffset::from_ms(0), TrackLanguage::new("por").unwrap(), + false, + String::new(), ))); assert_eq!(project.tracks.len(), 1); diff --git a/src/application/use_cases/set_track_language.rs b/src/application/use_cases/set_track_language.rs index 15a489c..33cd3f6 100644 --- a/src/application/use_cases/set_track_language.rs +++ b/src/application/use_cases/set_track_language.rs @@ -36,6 +36,8 @@ mod tests { FilePath::from("audio.aac"), SyncOffset::default(), TrackLanguage::new("por").unwrap(), + false, + String::new(), ) .unwrap(); let id = project.tracks[0].id(); diff --git a/src/domain/entities/audio_track.rs b/src/domain/entities/audio_track.rs index ee28d77..b1bf693 100644 --- a/src/domain/entities/audio_track.rs +++ b/src/domain/entities/audio_track.rs @@ -1,5 +1,5 @@ -use serde::{Deserialize, Serialize}; use crate::domain::value_objects::{FilePath, SyncOffset, TrackId, TrackLanguage}; +use serde::{Deserialize, Serialize}; /// Faixa de áudio externa adicionada pelo usuário. #[derive(Clone, Debug, Serialize, Deserialize)] @@ -8,10 +8,31 @@ pub struct AudioTrack { pub path: FilePath, pub offset: SyncOffset, pub language: TrackLanguage, + /// Quando verdadeiro, emite `-disposition:a:{idx} default` no FFmpeg, + /// tornando esta faixa a padrão para players como Jellyfin. + pub is_default: bool, + /// Título exibido na lista de faixas do player (ex: "Português", "Comentários"). + /// String vazia emite `title=` para apagar o título herdado do arquivo fonte + /// (ex: "ISO Media file produced by Google Inc."). + pub title: String, } impl AudioTrack { - pub fn new(id: TrackId, path: FilePath, offset: SyncOffset, language: TrackLanguage) -> Self { - AudioTrack { id, path, offset, language } + pub fn new( + id: TrackId, + path: FilePath, + offset: SyncOffset, + language: TrackLanguage, + is_default: bool, + title: String, + ) -> Self { + AudioTrack { + id, + path, + offset, + language, + is_default, + title, + } } } diff --git a/src/domain/entities/subtitle_track.rs b/src/domain/entities/subtitle_track.rs index f6a8160..4bc0eac 100644 --- a/src/domain/entities/subtitle_track.rs +++ b/src/domain/entities/subtitle_track.rs @@ -1,5 +1,5 @@ -use serde::{Deserialize, Serialize}; use crate::domain::value_objects::{FilePath, SyncOffset, TrackId, TrackLanguage}; +use serde::{Deserialize, Serialize}; /// Faixa de legenda externa adicionada pelo usuário. #[derive(Clone, Debug, Serialize, Deserialize)] @@ -8,10 +8,29 @@ pub struct SubtitleTrack { pub path: FilePath, pub offset: SyncOffset, pub language: TrackLanguage, + /// Quando verdadeiro, emite `-disposition:s:{idx} default` no FFmpeg. + pub is_default: bool, + /// Título exibido na lista de faixas do player (ex: "Português", "Forçada"). + /// String vazia emite `title=` para apagar o título herdado do arquivo fonte. + pub title: String, } impl SubtitleTrack { - pub fn new(id: TrackId, path: FilePath, offset: SyncOffset, language: TrackLanguage) -> Self { - SubtitleTrack { id, path, offset, language } + pub fn new( + id: TrackId, + path: FilePath, + offset: SyncOffset, + language: TrackLanguage, + is_default: bool, + title: String, + ) -> Self { + SubtitleTrack { + id, + path, + offset, + language, + is_default, + title, + } } } diff --git a/src/ui/app.rs b/src/ui/app.rs index 9f38f44..8e8b43e 100644 --- a/src/ui/app.rs +++ b/src/ui/app.rs @@ -719,11 +719,11 @@ impl eframe::App for App { if let Some(req) = audio_req { let _ = - AddAudioTrack::execute(project, req.path, req.offset, req.language); + AddAudioTrack::execute(project, req.path, req.offset, req.language, req.is_default, req.title); } if let Some(req) = subtitle_req { let _ = - AddSubtitle::execute(project, req.path, req.offset, req.language); + AddSubtitle::execute(project, req.path, req.offset, req.language, req.is_default, req.title); } ui.add_space(8.0); diff --git a/src/ui/components/add_audio_track_form.rs b/src/ui/components/add_audio_track_form.rs index d36f63e..75ebc5c 100644 --- a/src/ui/components/add_audio_track_form.rs +++ b/src/ui/components/add_audio_track_form.rs @@ -1,13 +1,15 @@ -use eframe::egui; use crate::adapters::filesystem::FilePickerAdapter; use crate::domain::value_objects::{FilePath, SyncOffset, TrackLanguage}; use crate::ui::components::{language_field::LanguageField, sync_offset_field::SyncOffsetField}; +use eframe::egui; /// Evento emitido ao confirmar adição de faixa de áudio. pub struct AddAudioTrackRequest { pub path: FilePath, pub offset: SyncOffset, pub language: TrackLanguage, + pub is_default: bool, + pub title: String, } /// Formulário para adicionar uma faixa de áudio externa. @@ -15,6 +17,8 @@ pub struct AddAudioTrackForm { selected_path: Option, offset_field: SyncOffsetField, language_field: LanguageField, + is_default: bool, + title_input: String, } impl AddAudioTrackForm { @@ -23,6 +27,8 @@ impl AddAudioTrackForm { selected_path: None, offset_field: SyncOffsetField::new(SyncOffset::default()), language_field: LanguageField::new(None), + is_default: false, + title_input: String::new(), } } @@ -48,6 +54,22 @@ impl AddAudioTrackForm { self.offset_field.ui(ui, "Atraso:"); self.language_field.ui(ui); + ui.horizontal(|ui| { + ui.label("Nome da faixa:"); + ui.text_edit_singleline(&mut self.title_input) + .on_hover_text( + "Nome exibido no player (ex: \"Português\", \"Comentários\").\n\ + Deixe vazio para remover o nome original do arquivo fonte.\n\ + Recomendado para evitar nomes como \"ISO Media file produced by Google Inc.\"", + ); + }); + + ui.checkbox(&mut self.is_default, "Definir como faixa padrão") + .on_hover_text( + "Marca esta faixa como padrão no container MKV.\n\ + Recomendado para que players como Jellyfin selecionem este áudio automaticamente.", + ); + ui.add_space(4.0); let can_add = self.selected_path.is_some(); @@ -60,11 +82,19 @@ impl AddAudioTrackForm { self.offset_field.parse(), self.language_field.parse(), ) { - result = Some(AddAudioTrackRequest { path, offset, language }); + result = Some(AddAudioTrackRequest { + path, + offset, + language, + is_default: self.is_default, + title: self.title_input.trim().to_string(), + }); // Reset form self.selected_path = None; self.offset_field = SyncOffsetField::new(SyncOffset::default()); self.language_field = LanguageField::new(None); + self.is_default = false; + self.title_input = String::new(); } } }); diff --git a/src/ui/components/add_subtitle_form.rs b/src/ui/components/add_subtitle_form.rs index 382f712..fbc64e0 100644 --- a/src/ui/components/add_subtitle_form.rs +++ b/src/ui/components/add_subtitle_form.rs @@ -1,13 +1,15 @@ -use eframe::egui; use crate::adapters::filesystem::FilePickerAdapter; use crate::domain::value_objects::{FilePath, SyncOffset, TrackLanguage}; use crate::ui::components::{language_field::LanguageField, sync_offset_field::SyncOffsetField}; +use eframe::egui; /// Evento emitido ao confirmar adição de legenda. pub struct AddSubtitleRequest { pub path: FilePath, pub offset: SyncOffset, pub language: TrackLanguage, + pub is_default: bool, + pub title: String, } /// Formulário para adicionar uma faixa de legenda externa. @@ -15,6 +17,8 @@ pub struct AddSubtitleForm { selected_path: Option, offset_field: SyncOffsetField, language_field: LanguageField, + is_default: bool, + title_input: String, } impl AddSubtitleForm { @@ -23,6 +27,8 @@ impl AddSubtitleForm { selected_path: None, offset_field: SyncOffsetField::new(SyncOffset::default()), language_field: LanguageField::new(None), + is_default: false, + title_input: String::new(), } } @@ -48,6 +54,21 @@ impl AddSubtitleForm { self.offset_field.ui(ui, "Atraso:"); self.language_field.ui(ui); + ui.horizontal(|ui| { + ui.label("Nome da faixa:"); + ui.text_edit_singleline(&mut self.title_input) + .on_hover_text( + "Nome exibido no player (ex: \"Português\", \"Forçada\").\n\ + Deixe vazio para remover o nome original do arquivo fonte.", + ); + }); + + ui.checkbox(&mut self.is_default, "Definir como faixa padrão") + .on_hover_text( + "Marca esta faixa como padrão no container MKV.\n\ + Recomendado para que players como Jellyfin selecionem esta legenda automaticamente.", + ); + ui.add_space(4.0); let can_add = self.selected_path.is_some(); @@ -60,10 +81,18 @@ impl AddSubtitleForm { self.offset_field.parse(), self.language_field.parse(), ) { - result = Some(AddSubtitleRequest { path, offset, language }); + result = Some(AddSubtitleRequest { + path, + offset, + language, + is_default: self.is_default, + title: self.title_input.trim().to_string(), + }); self.selected_path = None; self.offset_field = SyncOffsetField::new(SyncOffset::default()); self.language_field = LanguageField::new(None); + self.is_default = false; + self.title_input = String::new(); } } }); diff --git a/src/ui/components/batch_panel.rs b/src/ui/components/batch_panel.rs index 4bd4f58..f594e5a 100644 --- a/src/ui/components/batch_panel.rs +++ b/src/ui/components/batch_panel.rs @@ -49,12 +49,16 @@ struct PendingAudio { path: FilePath, offset: SyncOffset, language: TrackLanguage, + is_default: bool, + title: String, } struct PendingSubtitle { path: FilePath, offset: SyncOffset, language: TrackLanguage, + is_default: bool, + title: String, } // ── BatchPanel ───────────────────────────────────────────────────────────────── @@ -117,11 +121,15 @@ impl BatchPanel { path: t.path.clone(), offset: t.offset, language: t.language.clone(), + is_default: t.is_default, + title: t.title.clone(), }), Track::Subtitle(t) => self.form_pending_subtitles.push(PendingSubtitle { path: t.path.clone(), offset: t.offset, language: t.language.clone(), + is_default: t.is_default, + title: t.title.clone(), }), } } @@ -459,6 +467,8 @@ impl BatchPanel { path: req.path, offset: req.offset, language: req.language, + is_default: req.is_default, + title: req.title, }); } if let Some(req) = subtitle_req { @@ -466,6 +476,8 @@ impl BatchPanel { path: req.path, offset: req.offset, language: req.language, + is_default: req.is_default, + title: req.title, }); } @@ -507,6 +519,8 @@ impl BatchPanel { t.path, t.offset, t.language, + t.is_default, + t.title, ); } for s in subs { @@ -515,6 +529,8 @@ impl BatchPanel { s.path, s.offset, s.language, + s.is_default, + s.title, ); } if let Some(idx) = self.editing_index {