From b857256916f7cfce54b5d54d25e12c1d04bfe138 Mon Sep 17 00:00:00 2001 From: paregi12 Date: Sat, 7 Feb 2026 11:51:12 +0530 Subject: [PATCH] feat: update IntroDB integration to support recap and outro segments --- .../player/modals/SubmitIntroModal.tsx | 101 +++++++++++++----- src/services/introService.ts | 97 ++++++++++++----- 2 files changed, 144 insertions(+), 54 deletions(-) diff --git a/src/components/player/modals/SubmitIntroModal.tsx b/src/components/player/modals/SubmitIntroModal.tsx index 42873ba0..11afaa7c 100644 --- a/src/components/player/modals/SubmitIntroModal.tsx +++ b/src/components/player/modals/SubmitIntroModal.tsx @@ -1,5 +1,5 @@ import React, { useState, useEffect } from 'react'; -import { View, Text, TouchableOpacity, useWindowDimensions, StyleSheet, TextInput, ActivityIndicator } from 'react-native'; +import { View, Text, TouchableOpacity, useWindowDimensions, StyleSheet, TextInput, ActivityIndicator, ScrollView } from 'react-native'; import { Ionicons, MaterialIcons } from '@expo/vector-icons'; import { useTranslation } from 'react-i18next'; import Animated, { @@ -9,7 +9,7 @@ import Animated, { SlideOutDown, } from 'react-native-reanimated'; import { useSettings } from '../../../hooks/useSettings'; -import { introService } from '../../../services/introService'; +import { introService, SkipType } from '../../../services/introService'; import { toastService } from '../../../services/toastService'; interface SubmitIntroModalProps { @@ -67,6 +67,7 @@ export const SubmitIntroModal: React.FC = ({ const [startTimeStr, setStartTimeStr] = useState('00:00'); const [endTimeStr, setEndTimeStr] = useState(formatSecondsToMMSS(currentTime)); + const [segmentType, setSegmentType] = useState('intro'); const [isSubmitting, setIsSubmitting] = useState(false); useEffect(() => { @@ -107,14 +108,15 @@ export const SubmitIntroModal: React.FC = ({ season, episode, startSec, - endSec + endSec, + segmentType ); if (success) { - toastService.success(t('player_ui.intro_submitted', { defaultValue: 'Intro submitted successfully' })); + toastService.success(t('player_ui.intro_submitted', { defaultValue: 'Segment submitted successfully' })); onClose(); } else { - toastService.error(t('player_ui.intro_submit_failed', { defaultValue: 'Failed to submit intro' })); + toastService.error(t('player_ui.intro_submit_failed', { defaultValue: 'Failed to submit segment' })); } } catch (error) { toastService.error('Error', 'An unexpected error occurred'); @@ -123,9 +125,11 @@ export const SubmitIntroModal: React.FC = ({ } }; - const startVal = parseTimeToSeconds(startTimeStr); - const endVal = parseTimeToSeconds(endTimeStr); - const durationSec = (startVal !== null && endVal !== null) ? endVal - startVal : 0; + const segmentTypes: { label: string; value: SkipType; icon: any }[] = [ + { label: 'Intro', value: 'intro', icon: 'play-circle-outline' }, + { label: 'Recap', value: 'recap', icon: 'replay' }, + { label: 'Outro', value: 'outro', icon: 'stop-circle' }, + ]; return ( @@ -144,13 +148,42 @@ export const SubmitIntroModal: React.FC = ({ style={[localStyles.modalContainer, { width: Math.min(width * 0.85, 380) }]} > - Submit Intro Timestamp + Submit Timestamps - + + {/* Segment Type Selector */} + + Segment Type + + {segmentTypes.map((type) => ( + setSegmentType(type.value)} + style={[ + localStyles.typeButton, + segmentType === type.value && localStyles.typeButtonActive + ]} + > + + + {type.label} + + + ))} + + + {/* Start Time Input */} @@ -214,7 +247,7 @@ export const SubmitIntroModal: React.FC = ({ )} - + @@ -239,6 +272,7 @@ const localStyles = StyleSheet.create({ shadowOpacity: 0.5, shadowRadius: 15, elevation: 20, + maxHeight: '80%', }, header: { flexDirection: 'row', @@ -257,6 +291,34 @@ const localStyles = StyleSheet.create({ content: { gap: 20, }, + typeRow: { + flexDirection: 'row', + gap: 8, + }, + typeButton: { + flex: 1, + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'center', + gap: 6, + backgroundColor: 'rgba(255,255,255,0.05)', + borderRadius: 12, + paddingVertical: 10, + borderWidth: 1, + borderColor: 'rgba(255,255,255,0.1)', + }, + typeButtonActive: { + backgroundColor: 'white', + borderColor: 'white', + }, + typeButtonText: { + color: 'rgba(255,255,255,0.6)', + fontSize: 13, + fontWeight: '600', + }, + typeButtonTextActive: { + color: 'black', + }, inputRow: { flexDirection: 'row', alignItems: 'flex-end', @@ -295,22 +357,6 @@ const localStyles = StyleSheet.create({ fontSize: 13, fontWeight: '600', }, - summaryBox: { - backgroundColor: 'rgba(255,255,255,0.03)', - borderRadius: 16, - padding: 16, - marginTop: 8, - }, - summaryText: { - color: 'rgba(255,255,255,0.5)', - fontSize: 14, - marginBottom: 4, - }, - hintText: { - color: 'rgba(255,255,255,0.3)', - fontSize: 12, - fontStyle: 'italic', - }, buttonRow: { flexDirection: 'row', gap: 12, @@ -345,3 +391,4 @@ const localStyles = StyleSheet.create({ fontWeight: '700', }, }); + diff --git a/src/services/introService.ts b/src/services/introService.ts index de9a1c78..9d96b43c 100644 --- a/src/services/introService.ts +++ b/src/services/introService.ts @@ -26,11 +26,21 @@ export interface IntroTimestamps { imdb_id: string; season: number; episode: number; - start_sec: number; - end_sec: number; - start_ms: number; - end_ms: number; - confidence: number; + intro?: { + start_sec: number; + end_sec: number; + confidence: number; + }; + recap?: { + start_sec: number; + end_sec: number; + confidence: number; + }; + outro?: { + start_sec: number; + end_sec: number; + confidence: number; + }; } async function getMalIdFromArm(imdbId: string): Promise { @@ -154,7 +164,7 @@ async function fetchFromAniSkip(malId: string, episode: number): Promise { try { - const response = await axios.get(`${INTRODB_API_URL}/intro`, { + const response = await axios.get(`${INTRODB_API_URL}/segments`, { params: { imdb_id: imdbId, season, @@ -163,26 +173,48 @@ async function fetchFromIntroDb(imdbId: string, season: number, episode: number) timeout: 5000, }); - logger.log(`[IntroService] Found intro for ${imdbId} S${season}E${episode}:`, { - start: response.data.start_sec, - end: response.data.end_sec, - confidence: response.data.confidence, - }); + const intervals: SkipInterval[] = []; - return [{ - startTime: response.data.start_sec, - endTime: response.data.end_sec, - type: 'intro', - provider: 'introdb' - }]; + if (response.data.intro) { + intervals.push({ + startTime: response.data.intro.start_sec, + endTime: response.data.intro.end_sec, + type: 'intro', + provider: 'introdb' + }); + } + + if (response.data.recap) { + intervals.push({ + startTime: response.data.recap.start_sec, + endTime: response.data.recap.end_sec, + type: 'recap', + provider: 'introdb' + }); + } + + if (response.data.outro) { + intervals.push({ + startTime: response.data.outro.start_sec, + endTime: response.data.outro.end_sec, + type: 'outro', + provider: 'introdb' + }); + } + + if (intervals.length > 0) { + logger.log(`[IntroService] Found ${intervals.length} segments for ${imdbId} S${season}E${episode}`); + } + + return intervals; } catch (error: any) { if (axios.isAxiosError(error) && error.response?.status === 404) { // No intro data available for this episode - this is expected - logger.log(`[IntroService] No intro data for ${imdbId} S${season}E${episode}`); + logger.log(`[IntroService] No segment data for ${imdbId} S${season}E${episode}`); return []; } - logger.error('[IntroService] Error fetching intro timestamps:', error?.message || error); + logger.error('[IntroService] Error fetching segments from IntroDB:', error?.message || error); return []; } } @@ -230,7 +262,8 @@ export async function submitIntro( season: number, episode: number, startTime: number, // in seconds - endTime: number // in seconds + endTime: number, // in seconds + segmentType: SkipType = 'intro' ): Promise { try { if (!apiKey) { @@ -240,8 +273,12 @@ export async function submitIntro( const response = await axios.post(`${INTRODB_API_URL}/submit`, { imdb_id: imdbId, + segment_type: segmentType === 'op' ? 'intro' : (segmentType === 'ed' ? 'outro' : segmentType), season, episode, + start_sec: startTime, + end_sec: endTime, + // Keep start_ms/end_ms for backward compatibility if the server still expects it start_ms: Math.round(startTime * 1000), end_ms: Math.round(endTime * 1000), }, { @@ -319,18 +356,24 @@ export async function getIntroTimestamps( imdbId: string, season: number, episode: number -): Promise { +): Promise { const intervals = await fetchFromIntroDb(imdbId, season, episode); - if (intervals.length > 0) { + const intro = intervals.find(i => i.type === 'intro'); + if (intro) { return { imdb_id: imdbId, season, episode, - start_sec: intervals[0].startTime, - end_sec: intervals[0].endTime, - start_ms: intervals[0].startTime * 1000, - end_ms: intervals[0].endTime * 1000, - confidence: 1.0 + start_sec: intro.startTime, + end_sec: intro.endTime, + start_ms: intro.startTime * 1000, + end_ms: intro.endTime * 1000, + confidence: 1.0, + intro: { + start_sec: intro.startTime, + end_sec: intro.endTime, + confidence: 1.0 + } }; } return null;