From f70c5e2d0bb794aff5548513eae891cfd98d3f4f Mon Sep 17 00:00:00 2001 From: Francisco Solla Date: Thu, 17 Sep 2026 01:07:38 -0300 Subject: [PATCH] =?UTF-8?q?refactor(C179):=20helper=20=C3=BAnico=20da=20UR?= =?UTF-8?q?L=20do=20thumbnail=20do=20YouTube=20(Closes=20#1093)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/changelog/2026-09-17-c179.md | 1 + .../escala-dry-pos-speech-thumbnail-impl.md | 52 +++++++++++++++++++ src/app/(frontend)/corte/[id]/page.tsx | 5 +- .../campaign/speech/SpeechCutPlayer.tsx | 5 +- .../campaign/speech/SpeechCutResultCard.tsx | 3 +- .../campaign/speech/SpeechDetailPlayer.tsx | 4 +- 6 files changed, 61 insertions(+), 9 deletions(-) create mode 100644 docs/changelog/2026-09-17-c179.md create mode 100644 docs/plans/escala-dry-pos-speech-thumbnail-impl.md diff --git a/docs/changelog/2026-09-17-c179.md b/docs/changelog/2026-09-17-c179.md new file mode 100644 index 00000000..ee4afeca --- /dev/null +++ b/docs/changelog/2026-09-17-c179.md @@ -0,0 +1 @@ +**C179 — Helper único da URL do thumbnail do YouTube (2026-09-17):** o literal `https://i.ytimg.com/vi//hqdefault.jpg` estava repetido em 4 módulos (`SpeechDetailPlayer.tsx`, `SpeechCutPlayer.tsx`, `SpeechCutResultCard.tsx` e `src/app/(frontend)/corte/[id]/page.tsx`), o mesmo conhecimento (host + formato + sufixo) copiado 4× com risco de drift silencioso. O plano de intenção manda extrair o helper em `src/lib/speechShare.ts`, mas `src/lib/speechVod.ts` já é o dono do par id→cover: o C175 (#1085, `2026-09-16 12:36`) criou ali `youtubeThumbnailUrl` e o C182 (#1104) compôs `speechCoverUrl(youtubeUrl)` (parse + cover) por cima. Criar uma segunda implementação seria o twin que a própria Issue quer eliminar, então o delivery reusa os donos existentes ("edit the owner, don't twin"): os três consumidores que têm o video id passam a chamar `youtubeThumbnailUrl`, e a página do corte — que só tem a URL crua — passa a chamar `speechCoverUrl`, o mesmo helper do view model da listagem e da rota de poster. Nenhuma mudança de código em `speechVod.ts` (helper e unit do C175 ficam como estão). Sem UI nova, migration, Consent, URL pública ou escrita — a crítica visual do `designer` não se aplica (port mecânico de literal, non-trigger do §Design). Pins intocados: os testes que assertam a URL final (`speechVod.unit.spec.ts`, `speechViewModels.unit.spec.ts`, `speechAcervo.int.spec.ts`, e2e de acervo/corte) passam verbatim. `pnpm gate:fast` verde (334 arquivos / 3476 testes). diff --git a/docs/plans/escala-dry-pos-speech-thumbnail-impl.md b/docs/plans/escala-dry-pos-speech-thumbnail-impl.md new file mode 100644 index 00000000..b2f9867d --- /dev/null +++ b/docs/plans/escala-dry-pos-speech-thumbnail-impl.md @@ -0,0 +1,52 @@ +# Impl: Helper único da URL do thumbnail do YouTube (hqdefault) + +Status: aprovado (modo `--auto`) +Atualizado em: 2026-09-17 +Issue: #1093 +Intenção: docs/plans/escala-dry-pos-speech-thumbnail.md +Appetite restante: herdado (~1 h eng) + +## Leitura da intenção + +- **Outcome:** eliminar a duplicação do literal `https://i.ytimg.com/vi//hqdefault.jpg` nos 4 módulos, consumindo um helper único. +- **O que NÃO negociar:** manter o comportamento atual (host/formato/sufixo); sem mudança visual. +- **O que reavaliar:** o plano de intenção nomeia `src/lib/speechShare.ts` como dono. **Esse pressuposto ficou stale**: o C175 (#1085, 2026-09-16 12:36) já criou `youtubeThumbnailUrl` em `src/lib/speechVod.ts` — o módulo que já expõe `parseYoutubeVideoId`, logo o dono natural do par id→cover. + +## Abordagem recomendada + +- **A (Recomendada, adotada):** reusar o `youtubeThumbnailUrl` existente em `src/lib/speechVod.ts` e consumi-lo nos 4 pontos duplicados. +- **B (Rejeitada):** criar `youtubeThumbnailUrl` também em `src/lib/speechShare.ts` (como escrito no plano de intenção). Rejeitada por AGENTS/engineering-standards — "edit the owner, don't twin": seria uma segunda implementação idêntica do mesmo conhecimento, exatamente o drift que a Issue quer eliminar. +- **C (Rejeitada):** criar um arquivo novo `src/lib/youtube-helpers.ts`. Rejeitada pelo mesmo motivo (twin de `speechVod.ts`). +- **D (Rejeitada):** manter a duplicação e documentar. Rejeitada: risco de drift silencioso (host/CSP). + +### Componentes / mudanças + +- `src/lib/speechVod.ts`: **nenhuma mudança de código** — `youtubeThumbnailUrl(videoId)` (C175) e `speechCoverUrl(youtubeUrl)` (C182, composição id→cover) já existem e são os donos únicos. +- `SpeechDetailPlayer.tsx`, `SpeechCutPlayer.tsx`, `SpeechCutResultCard.tsx` (têm o video id): consumir `youtubeThumbnailUrl`. +- `src/app/(frontend)/corte/[id]/page.tsx` (tem a URL crua): consumir `speechCoverUrl` — o mesmo helper que o view model da listagem e a rota de poster do C182 já usam. +- **Migration:** nenhuma (código puro). +- **Access / Consent:** não aplicável. +- **UI:** Impeccable A — sem superfície visual nova; crítica do `designer` (trigger c) não se aplica (non-trigger: port mecânico do literal). + +## Fases verificáveis + +1. **Consumir** — substituir os 4 literais por `youtubeThumbnailUrl(...)`. +2. **Gates** — `pnpm gate:fast`; entrega via `pnpm push`. + +## Rabbit holes / Não escopo (engenharia) + +- Refatorar o markup duplicado do bloco de saída (`renderYoutubeExit`, 2 ramos) — defer até 3ª variante/superfície. +- Unificar imagem de capa/posters além do helper. +- Migrar o host do thumbnail ou adicionar fallback de imagem. +- Criar um `youtubeThumbnailUrlFromUrl` novo: o C182 já resolveu essa composição com `speechCoverUrl` (2 call sites + o corte/[id]); reusar, não duplicar. + +## Riscos e mitigação + +- **Risco:** conflito de merge com C172. **Status:** C172 já está em `main` (merge 4b3bee7c); risco consumido. +- **Risco:** quebra de pins que assertam a URL. **Mitigação:** verificado — os pins existentes (`speechVod.unit.spec.ts`, `speechViewModels.unit.spec.ts`, `speechAcervo.int.spec.ts`, e2e de acervo/corte) assertam o literal final, que não muda. + +## Aceite de engenharia + +- [x] Aceite de produto da intenção ainda coberto (literal único nos 4 pontos) +- [x] Invariantes AGENTS/engineering-standards (dono único; sem twin) +- [x] Testes de domínio: helper puro já coberto no unit do C175; consumidores cobertos por int/e2e da mesma superfície diff --git a/src/app/(frontend)/corte/[id]/page.tsx b/src/app/(frontend)/corte/[id]/page.tsx index 5358aa52..eca3e9cd 100644 --- a/src/app/(frontend)/corte/[id]/page.tsx +++ b/src/app/(frontend)/corte/[id]/page.tsx @@ -7,7 +7,7 @@ import { } from '@/components/SpeechCutShareActions' import { formatSpeechClock, formatSpeechDate } from '@/lib/speechClock' import { speechCutPublicPath } from '@/lib/speechCut' -import { parseYoutubeVideoId } from '@/lib/speechVod' +import { speechCoverUrl } from '@/lib/speechVod' import type { Media, Speech, SpeechCut } from '@/payload-types' import { getCachedDocumentById } from '@/utilities/documentReads' import { getCachedGlobal } from '@/utilities/globalReads' @@ -41,8 +41,7 @@ const speechOf = (cut: SpeechCut): Speech | null => const youtubeCoverUrl = (cut: SpeechCut): string | null => { const speech = speechOf(cut) - const videoId = parseYoutubeVideoId(speech?.youtubeUrl) - return videoId ? `https://i.ytimg.com/vi/${videoId}/hqdefault.jpg` : null + return speechCoverUrl(speech?.youtubeUrl) } const loadPublishedCut = async (id: string): Promise => { diff --git a/src/components/campaign/speech/SpeechCutPlayer.tsx b/src/components/campaign/speech/SpeechCutPlayer.tsx index 2aa820fe..344dc386 100644 --- a/src/components/campaign/speech/SpeechCutPlayer.tsx +++ b/src/components/campaign/speech/SpeechCutPlayer.tsx @@ -1,4 +1,5 @@ import type { SpeechCutLibraryItemViewModel } from '@/lib/speechCut' +import { youtubeThumbnailUrl } from '@/lib/speechVod' /** * C168 — the cut is a plain MP4 (never the YouTube/VOD source): the same @@ -13,9 +14,7 @@ export const SpeechCutPlayer = ({ cut }: { cut: SpeechCutLibraryItemViewModel }) ) } - const poster = cut.youtubeVideoId - ? `https://i.ytimg.com/vi/${cut.youtubeVideoId}/hqdefault.jpg` - : undefined + const poster = youtubeThumbnailUrl(cut.youtubeVideoId) ?? undefined return (