Recálculo de duração de áudio depende da troca de arquivo

O que aconteceu

Ao editar um áudio diário no admin e trocar o arquivo, a duração não era recalculada. Os progressos passavam a usar uma duração que não correspondia ao áudio novo, gerando barras de progresso e percentuais incoerentes (inclusive position > total_duration).

Causa raiz

MediaMetadataMixin (apps/audios/models/media_base.py) calcula duration/file_size no save(). Calcular apenas quando not self.file_size significa que trocar o media_file de um registro existente (ex.: no admin) não recalcula a duração — o arquivo muda mas a duração antiga permanece.

Além disso, duration é um valor por conteúdo, mas AudioProgress.total_duration / MeditationProgress.total_duration são snapshots pegos na criação do progresso. Quando a duração do conteúdo muda, esses snapshots ficam defasados: o front calcula a barra como position / total_duration e as métricas do admin usam total_duration, então ambos passam a mostrar progresso incoerente.

Correção

A detecção de troca compara o media_file persistido no banco com o atual (_media_file_changed); registro novo só calcula no primeiro upload. Quando a duração muda, o recálculo dispara _sync_progress_durations(), que atualiza em lote o total_duration de todos os progressos daquele conteúdo via .update().

Contexto da implementação:

  • Vale para Audio e Meditation — ambos compartilham MediaMetadataMixin/EngagementMetricsMixin e expõem progress_model_label/progress_field_name, usados para resolver o model de progresso genericamente
  • _sync_progress_durations usa .update() (queryset) de propósito: AudioProgress.save()/MeditationProgress.save() chamam super().save() dentro do if total_duration vazio, então um .save() com total_duration já preenchido não persistiria. Bug latente conhecido, fora do escopo desta mudança
  • Decisão de negócio: ao mudar a duração, só total_duration é sincronizado; position e completed_at são preservados. Um progresso já concluído contra a duração antiga continua concluído

Como evitar

  • Ao calcular metadados derivados de um arquivo no save(), condicionar a mudança do arquivo, não à ausência do metadado
  • Todo campo que é snapshot de um valor de outro model precisa de um caminho explícito de sincronização quando a fonte muda
  • Usar .update() de queryset quando o save() do model tem branches que impedem a persistência do campo em questão

Referências