From adbdd19e661fec3fcd23ee27edec9f43f18676e4 Mon Sep 17 00:00:00 2001 From: Pas <74743263+Pasithea0@users.noreply.github.com> Date: Wed, 4 Feb 2026 17:44:24 -0700 Subject: [PATCH] update TIDB integration --- src/assets/locales/en.json | 39 +- src/components/overlays/Modal.tsx | 11 +- src/components/player/TIDBSubmissionForm.tsx | 398 ++++++++++-------- .../player/atoms/SkipSegmentButton.tsx | 4 +- src/components/player/hooks/useSkipTime.ts | 82 +--- src/utils/tidb.ts | 2 +- 6 files changed, 281 insertions(+), 255 deletions(-) diff --git a/src/assets/locales/en.json b/src/assets/locales/en.json index 86c6d2ea..32845ede 100644 --- a/src/assets/locales/en.json +++ b/src/assets/locales/en.json @@ -1003,7 +1003,8 @@ "types": { "intro": "Intro", "recap": "Recap", - "credits": "Credits" + "credits": "Credits", + "preview": "Preview" }, "startTimeLabel": "Start (s)", "endTimeLabel": "End (s)", @@ -1011,12 +1012,14 @@ "start": { "intro": "2:30 or 150 (leave empty for start at beginning)", "recap": "2:30 or 150 (leave empty for start at beginning)", - "credits": "2:30 or 150 (required)" + "credits": "2:30 or 150 (required)", + "preview": "2:30 or 150 (required)" }, "end": { "intro": "3:30 or 210 (required)", "recap": "3:30 or 210 (required)", - "credits": "3:30 or 210 (leave empty for end of media)" + "credits": "3:30 or 210 (leave empty for end of media)", + "preview": "3:30 or 210 (leave empty for end of media)" } }, "whenToTitle": "Timestamps guide:", @@ -1029,7 +1032,35 @@ "durationLabel": "Duration:", "durationDesc": "Min 5s if end provided", "excludeLabel": "Exclude:", - "excludeDesc": "Post-credits scenes" + "excludeDesc": "Post-credits scenes", + "intro": { + "whenToTitle": "Intro timestamps guide:", + "startDesc": "Optional - leave empty if the intro starts at the beginning of the episode.", + "endDesc": "Required - when the intro ends (usually when the title card or main episode begins).", + "durationDesc": "Most intros are 30–90 seconds. Minimum 5s if both start and end are set.", + "excludeDesc": "Do not include cold opens or pre-intro scenes; only the main title sequence." + }, + "recap": { + "whenToTitle": "Recap timestamps guide:", + "startDesc": "Optional - leave empty if the recap starts at the beginning of the episode.", + "endDesc": "Required - when the recap ends and the new content begins.", + "durationDesc": "Recaps vary in length. Minimum 5s if both start and end are set.", + "excludeDesc": "Only the \"previously on\" or recap segment; do not include the intro or new scenes." + }, + "credits": { + "whenToTitle": "Credits timestamps guide:", + "startDesc": "Required - when the closing credits begin (e.g. when the first credit text appears).", + "endDesc": "Optional - leave empty if credits run to the end of the file.", + "durationDesc": "Minimum 5s if end is provided. Can extend to end of media.", + "excludeDesc": "Exclude mid-credits or post-credits scenes; only the main credit roll." + }, + "preview": { + "whenToTitle": "Preview timestamps guide:", + "startDesc": "Required - when the preview starts (e.g. when the preview text appears).", + "endDesc": "Optional - leave empty if the preview ends at the end of the media.", + "durationDesc": "Preview varies in length. Minimum 5s.", + "excludeDesc": "Preview is usually 15-90 seconds at the end of the episode previewing the next episode." + } }, "cancel": "Cancel", "submit": "Submit", diff --git a/src/components/overlays/Modal.tsx b/src/components/overlays/Modal.tsx index fea9fe41..b8abfc12 100644 --- a/src/components/overlays/Modal.tsx +++ b/src/components/overlays/Modal.tsx @@ -26,8 +26,13 @@ export function ModalCard(props: { className?: ReactNode; }) { return ( -
-
+
+
{props.children}
@@ -50,7 +55,7 @@ export function Modal(props: { id: string; children?: ReactNode }) { -
+
{props.children}
diff --git a/src/components/player/TIDBSubmissionForm.tsx b/src/components/player/TIDBSubmissionForm.tsx index 02e6f629..a1a19b40 100644 --- a/src/components/player/TIDBSubmissionForm.tsx +++ b/src/components/player/TIDBSubmissionForm.tsx @@ -1,8 +1,8 @@ -import { useEffect, useState } from "react"; +import { useEffect, useRef, useState } from "react"; import { useTranslation } from "react-i18next"; import { Button } from "@/components/buttons/Button"; -import { Dropdown } from "@/components/form/Dropdown"; +import { Icon, Icons } from "@/components/Icon"; import { Modal, useModal } from "@/components/overlays/Modal"; import { SegmentData } from "@/components/player/hooks/useSkipTime"; import { AuthInputBox } from "@/components/text-inputs/AuthInputBox"; @@ -11,7 +11,7 @@ import { usePlayerStore } from "@/stores/player/store"; import { usePreferencesStore } from "@/stores/preferences"; import { submitIntro } from "@/utils/tidb"; -type SegmentType = "intro" | "recap" | "credits"; +type SegmentType = "intro" | "recap" | "credits" | "preview"; // Helper function to parse time format (hh:mm:ss, mm:ss, or seconds) // Returns null if empty string, NaN if invalid, or number if valid @@ -19,46 +19,43 @@ function parseTimeToSeconds(timeStr: string): number | null { if (!timeStr.trim()) return null; // Check if it's in hh:mm:ss format - const hhmmssMatch = timeStr.match(/^(\d{1,2}):([0-5]?\d):([0-5]?\d)$/); + const hhmmssMatch = timeStr.match(/^($\d{1,2}$):($[0-5]?\d$):($[0-5]?\d$)$/); if (hhmmssMatch) { const hours = parseInt(hhmmssMatch[1], 10); const minutes = parseInt(hhmmssMatch[2], 10); const seconds = parseInt(hhmmssMatch[3], 10); - // Validate reasonable bounds (max 99 hours, minutes/seconds 0-59) if (hours > 99 || minutes > 59 || seconds > 59) { - return NaN; // Invalid format + return NaN; } return hours * 3600 + minutes * 60 + seconds; } // Check if it's in mm:ss format - const mmssMatch = timeStr.match(/^(\d{1,3}):([0-5]?\d)$/); + const mmssMatch = timeStr.match(/^($\d{1,3}$):($[0-5]?\d$)$/); if (mmssMatch) { const minutes = parseInt(mmssMatch[1], 10); const seconds = parseInt(mmssMatch[2], 10); - // Validate reasonable bounds (max 999 minutes, seconds 0-59) if (minutes > 999 || seconds > 59) { - return NaN; // Invalid format + return NaN; } return minutes * 60 + seconds; } - // Otherwise, treat as plain seconds (but only if no colons in input) if (timeStr.includes(":")) { - return NaN; // Invalid time format - has colons but didn't match time patterns + return NaN; } const parsed = parseFloat(timeStr); if ( Number.isNaN(parsed) || !Number.isFinite(parsed) || parsed < 0 || - parsed > 20000000 + parsed > 21600000 ) { - return NaN; // Invalid input + return NaN; } return parsed; @@ -79,6 +76,7 @@ export function TIDBSubmissionForm({ const meta = usePlayerStore((s) => s.meta); const tidbKey = usePreferencesStore((s) => s.tidbKey); const submissionModal = useModal("tidb-submission"); + const formRef = useRef(null); const [isSubmitting, setIsSubmitting] = useState(false); const [formData, setFormData] = useState<{ segment: SegmentType; @@ -109,7 +107,6 @@ export function TIDBSubmissionForm({ const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); - // Check if form is valid if (!formData.segment) { // eslint-disable-next-line no-alert alert(t("player.skipTime.feedback.modal.error.segment")); @@ -131,7 +128,7 @@ export function TIDBSubmissionForm({ const startSeconds = parseTimeToSeconds(formData.start); const endSeconds = parseTimeToSeconds(formData.end); - // Basic validation + // Validate required fields based on segment type if (formData.segment === "intro" || formData.segment === "recap") { if (endSeconds === null || Number.isNaN(endSeconds)) { // eslint-disable-next-line no-alert @@ -139,7 +136,10 @@ export function TIDBSubmissionForm({ setIsSubmitting(false); return; } - } else if (formData.segment === "credits") { + } else if ( + formData.segment === "credits" || + formData.segment === "preview" + ) { if (startSeconds === null || Number.isNaN(startSeconds)) { // eslint-disable-next-line no-alert alert(t("player.skipTime.feedback.modal.error.startTime")); @@ -148,31 +148,38 @@ export function TIDBSubmissionForm({ } } - // Prepare submission data - const submissionData: any = { + const submissionData: { + tmdb_id: number; + type: "movie" | "tv"; + segment: SegmentType; + season?: number; + episode?: number; + start_sec?: number | null; + end_sec?: number | null; + } = { tmdb_id: parseInt(meta.tmdbId.toString(), 10), type: meta.type === "show" ? "tv" : "movie", segment: formData.segment, }; - // Add season/episode for TV shows if (meta.type === "show" && meta.season && meta.episode) { submissionData.season = meta.season.number; submissionData.episode = meta.episode.number; } - // Set start_sec and end_sec based on segment type if (formData.segment === "intro" || formData.segment === "recap") { submissionData.start_sec = startSeconds !== null ? startSeconds : null; submissionData.end_sec = endSeconds!; - } else if (formData.segment === "credits") { + } else if ( + formData.segment === "credits" || + formData.segment === "preview" + ) { submissionData.start_sec = startSeconds!; submissionData.end_sec = endSeconds !== null ? endSeconds : null; } await submitIntro(submissionData, tidbKey); - // Success submissionModal.hide(); if (onSuccess) onSuccess(); } catch (error) { @@ -186,175 +193,200 @@ export function TIDBSubmissionForm({ } }; + const handleClose = () => { + submissionModal.hide(); + if (onCancel) onCancel(); + }; + return ( -
-
- - {t("player.skipTime.feedback.modal.title")} - - +
+
+
+ + + {t("player.skipTime.feedback.modal.title")} + +
+ {t("player.skipTime.feedback.modal.description")} -
- {/* Section: Segment timestamps */} -
-
diff --git a/src/components/player/atoms/SkipSegmentButton.tsx b/src/components/player/atoms/SkipSegmentButton.tsx index f5440e4e..4eb8d217 100644 --- a/src/components/player/atoms/SkipSegmentButton.tsx +++ b/src/components/player/atoms/SkipSegmentButton.tsx @@ -11,7 +11,7 @@ import { PlayerMeta } from "@/stores/player/slices/source"; import { usePlayerStore } from "@/stores/player/store"; function getSegmentText( - type: "intro" | "recap" | "credits", + type: "intro" | "recap" | "credits" | "preview", t: (key: string) => string, ): string { switch (type) { @@ -21,6 +21,8 @@ function getSegmentText( return t("player.skipTime.recap"); case "credits": return t("player.skipTime.credits"); + case "preview": + return t("player.skipTime.preview"); default: return t("player.skipTime.intro"); } diff --git a/src/components/player/hooks/useSkipTime.ts b/src/components/player/hooks/useSkipTime.ts index 3d6375b4..be34290a 100644 --- a/src/components/player/hooks/useSkipTime.ts +++ b/src/components/player/hooks/useSkipTime.ts @@ -36,7 +36,7 @@ export function useSkipTimeSource(): typeof currentSkipTimeSource { } export interface SegmentData { - type: "intro" | "recap" | "credits"; + type: "intro" | "recap" | "credits" | "preview"; start_ms: number | null; end_ms: number | null; confidence: number | null; @@ -58,49 +58,6 @@ export function useSkipTime() { // Another fetch for this key is already in progress (e.g. two components mounted) if (fetchingForCacheKey === cacheKey) return; fetchingForCacheKey = cacheKey; - // Validate segment data according to rules - // eslint-disable-next-line camelcase - const validateSegment = ( - type: "intro" | "recap" | "credits", - // eslint-disable-next-line camelcase - start_ms: number | null, - // eslint-disable-next-line camelcase - end_ms: number | null, - ): boolean => { - // eslint-disable-next-line camelcase - const start = start_ms ?? 0; - // eslint-disable-next-line camelcase - const end = end_ms; - - if (type === "intro") { - // Intro: end_ms is required, duration must be 0 or 5-200 seconds - if (end === null) return false; - const duration = (end - start) / 1000; - if (duration === 0) return true; // No intro is valid - return duration >= 5 && duration <= 200; - } - - if (type === "recap") { - // Recap: end_ms is required, duration must be 0 or 5-1200 seconds - if (end === null) return false; - const duration = (end - start) / 1000; - if (duration === 0) return true; // No recap is valid - return duration >= 5 && duration <= 1200; - } - - if (type === "credits") { - // Credits: start_ms is required - // If end_ms is provided, duration must be at least 5 seconds - // If end_ms is null, credits extend to end of video (valid) - // eslint-disable-next-line camelcase - if (start_ms === null) return false; - if (end === null) return true; // Credits to end of video is valid - const duration = (end - start) / 1000; - return duration >= 5; - } - - return false; - }; const fetchTheIntroDBSegments = async (): Promise<{ segments: SegmentData[]; @@ -122,12 +79,8 @@ export function useSkipTime() { const fetchedSegments: SegmentData[] = []; - // Add intro segment if it has valid data - if ( - data?.intro && - data.intro.submission_count > 0 && - validateSegment("intro", data.intro.start_ms, data.intro.end_ms) - ) { + // Add intro segment if it has data + if (data?.intro && data.intro.submission_count > 0) { fetchedSegments.push({ type: "intro", start_ms: data.intro.start_ms, @@ -137,12 +90,8 @@ export function useSkipTime() { }); } - // Add recap segment if it has valid data - if ( - data?.recap && - data.recap.submission_count > 0 && - validateSegment("recap", data.recap.start_ms, data.recap.end_ms) - ) { + // Add recap segment if it has data + if (data?.recap && data.recap.submission_count > 0) { fetchedSegments.push({ type: "recap", start_ms: data.recap.start_ms, @@ -152,12 +101,8 @@ export function useSkipTime() { }); } - // Add credits segment if it has valid data - if ( - data?.credits && - data.credits.submission_count > 0 && - validateSegment("credits", data.credits.start_ms, data.credits.end_ms) - ) { + // Add credits segment if it has data + if (data?.credits && data.credits.submission_count > 0) { fetchedSegments.push({ type: "credits", start_ms: data.credits.start_ms, @@ -167,6 +112,17 @@ export function useSkipTime() { }); } + // Add preview segment if it has data + if (data?.preview && data.preview.submission_count > 0) { + fetchedSegments.push({ + type: "preview", + start_ms: data.preview.start_ms, + end_ms: data.preview.end_ms, + confidence: data.preview.confidence, + submission_count: data.preview.submission_count, + }); + } + // TIDB returned 200 – we have segment data for this media (even if no intro) return { segments: fetchedSegments, tidbNotFound: false }; } catch (error: unknown) { @@ -213,7 +169,7 @@ export function useSkipTime() { const parseSkipTime = (timeStr: string | undefined): number | null => { if (!timeStr || typeof timeStr !== "string") return null; - const match = timeStr.match(/^(\d+)s$/); + const match = timeStr.match(/^($\d+$)s$/); if (!match) return null; return parseInt(match[1], 10); }; diff --git a/src/utils/tidb.ts b/src/utils/tidb.ts index d483f469..623de25a 100644 --- a/src/utils/tidb.ts +++ b/src/utils/tidb.ts @@ -1,4 +1,4 @@ -export type SegmentType = "intro" | "recap" | "credits"; +export type SegmentType = "intro" | "recap" | "credits" | "preview"; export interface SubmissionRequest { tmdb_id: number;