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
AudioeMeditation— ambos compartilhamMediaMetadataMixin/EngagementMetricsMixine expõemprogress_model_label/progress_field_name, usados para resolver o model de progresso genericamente _sync_progress_durationsusa.update()(queryset) de propósito:AudioProgress.save()/MeditationProgress.save()chamamsuper().save()dentro doif total_duration vazio, então um.save()comtotal_durationjá 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;positionecompleted_atsã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 osave()do model tem branches que impedem a persistência do campo em questão