From 6854ede26117b01c3dbdb80956e96012eeb5cf8f Mon Sep 17 00:00:00 2001 From: Francisco Solla Date: Mon, 14 Sep 2026 06:48:31 -0300 Subject: [PATCH] =?UTF-8?q?refactor(C162):=20dono=20=C3=BAnico=20da=20eleg?= =?UTF-8?q?ibilidade=20VOD,=20start=20inicial=20do=20YouTube=20e=20m=C3=AD?= =?UTF-8?q?dia=20em=20early-returns=20(simplify)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/plans/acervo-vod-sob-demanda-impl.md | 8 +- src/app/(campaign)/campanha/actions/speech.ts | 11 +- .../campaign/speech/SpeechDetailPlayer.tsx | 195 ++++++++++-------- src/lib/speechVod.ts | 68 ++++-- src/utilities/speech/speechViewModels.ts | 9 +- tests/int/speechAcervo.int.spec.ts | 2 +- tests/unit/speechDetailPlayer.unit.spec.tsx | 14 ++ tests/unit/speechVodResolver.unit.spec.ts | 26 ++- 8 files changed, 200 insertions(+), 133 deletions(-) diff --git a/docs/plans/acervo-vod-sob-demanda-impl.md b/docs/plans/acervo-vod-sob-demanda-impl.md index 760cab18..03b40fb9 100644 --- a/docs/plans/acervo-vod-sob-demanda-impl.md +++ b/docs/plans/acervo-vod-sob-demanda-impl.md @@ -1,6 +1,6 @@ # Impl: Acervo de falas: player resolve o trecho sob demanda e YouTube vira default quando existe -Status: em execução +Status: executado Atualizado em: 2026-09-14 Issue: #999 Intenção: docs/plans/acervo-vod-sob-demanda.md @@ -156,9 +156,9 @@ Rejeitadas: B porque ressuscita link efêmero (o problema que o item resolve) e ## Aceite de engenharia -- [ ] Aceite de produto da intenção ainda coberto (quadrantes, `excerptTMs` verbatim, Baixar verificado, estado honesto, crédito CC BY, gate do acervo). -- [ ] Invariantes AGENTS/engineering-standards (dono único, sem twin, sem migration/collection/Consent, sem escrita nova, `overrideAccess: false`, client boundary, zero warnings, dead code). -- [ ] Testes de domínio previstos (unit/resolver/player/int/e2e) onde o access/write path e os contratos de URL mudam. +- [x] Aceite de produto da intenção ainda coberto (quadrantes, `excerptTMs` verbatim, Baixar verificado, estado honesto, crédito CC BY, gate do acervo). +- [x] Invariantes AGENTS/engineering-standards (dono único, sem twin, sem migration/collection/Consent, sem escrita nova, `overrideAccess: false`, client boundary, zero warnings, dead code). +- [x] Testes de domínio previstos (unit/resolver/player/int/e2e) onde o access/write path e os contratos de URL mudam. ## Self-score (decision-quality, gate ≥4) diff --git a/src/app/(campaign)/campanha/actions/speech.ts b/src/app/(campaign)/campanha/actions/speech.ts index 6e04c29e..3019a051 100644 --- a/src/app/(campaign)/campanha/actions/speech.ts +++ b/src/app/(campaign)/campanha/actions/speech.ts @@ -7,7 +7,7 @@ import { SPEECH_VOD_NOT_FOUND_MESSAGE, speechVodRequestSchema, } from '@/lib/schemas/speechVod' -import type { SpeechVodResolution } from '@/lib/speechVod' +import { speechVodCoordinates, type SpeechVodResolution } from '@/lib/speechVod' import { getCampaignActionContext } from '@/utilities/campaignActionContext' import { resolveSpeechVod } from '@/utilities/speech/speechVodResolver' @@ -46,11 +46,8 @@ export const resolveSpeechVodForActor = async (input: { const speech = result.docs[0] if (!speech) throw new Error(SPEECH_VOD_NOT_FOUND_MESSAGE) - const hasStoredVod = Boolean(speech.vodPlaybackUrl || speech.vodDownloadUrl) - const { eventId, audioId, excerptTMs } = speech - if (!hasStoredVod || !eventId || !audioId || !excerptTMs) { - throw new Error(SPEECH_VOD_INELIGIBLE_MESSAGE) - } + const coordinates = speechVodCoordinates(speech) + if (!coordinates) throw new Error(SPEECH_VOD_INELIGIBLE_MESSAGE) - return resolveSpeechVod({ eventId, audioId, excerptTms: excerptTMs }) + return resolveSpeechVod(coordinates) } diff --git a/src/components/campaign/speech/SpeechDetailPlayer.tsx b/src/components/campaign/speech/SpeechDetailPlayer.tsx index 9b226a58..de7d65c7 100644 --- a/src/components/campaign/speech/SpeechDetailPlayer.tsx +++ b/src/components/campaign/speech/SpeechDetailPlayer.tsx @@ -1,7 +1,7 @@ 'use client' import { DownloadIcon, ExternalLinkIcon, FilmIcon, PlayIcon } from 'lucide-react' -import { useEffect, useMemo, useRef, useState, type ReactNode } from 'react' +import { useEffect, useRef, useState, type ReactNode } from 'react' import type { SpeechVodResolveResponse } from '@/app/(campaign)/campanha/(app)/comunicacao/acervo/resolver-vod/types' import { SpeechHighlightParts } from '@/components/campaign/speech/SpeechHighlightParts' @@ -13,6 +13,15 @@ import { cn } from '@/lib/utils' import type { SpeechDetailSegmentViewModel } from '@/utilities/speech/speechViewModels' const RESOLVE_ENDPOINT = '/campanha/comunicacao/acervo/resolver-vod' +const GENERATING_TITLE = 'A Câmara está gerando o trecho deste vídeo.' + +const buildYoutubeSrc = (videoId: string, startSeconds: number | null): string => { + const params = new URLSearchParams({ playsinline: '1', rel: '0' }) + if (startSeconds !== null && startSeconds > 0) { + params.set('start', String(Math.floor(startSeconds))) + } + return `https://www.youtube-nocookie.com/embed/${videoId}?${params.toString()}` +} type ResolutionState = | { kind: 'idle' } @@ -96,8 +105,8 @@ export const SpeechDetailPlayer = ({ const [resolution, setResolution] = useState({ kind: 'idle' }) const [activeStart, setActiveStart] = useState(null) const [youtubeStart, setYoutubeStart] = useState(() => - youtubeVideoId && youtubeOffsetSeconds !== null && initialSeconds !== null - ? youtubeOffsetSeconds + initialSeconds + youtubeVideoId && youtubeOffsetSeconds !== null + ? youtubeOffsetSeconds + (initialSeconds ?? 0) : null, ) @@ -177,95 +186,105 @@ export const SpeechDetailPlayer = ({ setActiveStart(active?.startSeconds ?? null) } - const youtubeSrc = useMemo(() => { - const params = new URLSearchParams({ playsinline: '1', rel: '0' }) - if (youtubeStart !== null && youtubeStart > 0) - params.set('start', String(Math.floor(youtubeStart))) - return `https://www.youtube-nocookie.com/embed/${youtubeVideoId}?${params.toString()}` - }, [youtubeVideoId, youtubeStart]) - const seekable = youtubeVideoId ? youtubeOffsetSeconds !== null : Boolean(playbackUrl) - const downloadAvailable = vodResolvable - - const media = youtubeVideoId ? ( -