feat: update IntroDB integration to support recap and outro segments

This commit is contained in:
paregi12 2026-02-07 11:51:12 +05:30
parent dbb5337204
commit b857256916
2 changed files with 144 additions and 54 deletions

View file

@ -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<SubmitIntroModalProps> = ({
const [startTimeStr, setStartTimeStr] = useState('00:00');
const [endTimeStr, setEndTimeStr] = useState(formatSecondsToMMSS(currentTime));
const [segmentType, setSegmentType] = useState<SkipType>('intro');
const [isSubmitting, setIsSubmitting] = useState(false);
useEffect(() => {
@ -107,14 +108,15 @@ export const SubmitIntroModal: React.FC<SubmitIntroModalProps> = ({
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<SubmitIntroModalProps> = ({
}
};
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 (
<View style={[StyleSheet.absoluteFill, { zIndex: 10000 }]}>
@ -144,13 +148,42 @@ export const SubmitIntroModal: React.FC<SubmitIntroModalProps> = ({
style={[localStyles.modalContainer, { width: Math.min(width * 0.85, 380) }]}
>
<View style={localStyles.header}>
<Text style={localStyles.title}>Submit Intro Timestamp</Text>
<Text style={localStyles.title}>Submit Timestamps</Text>
<TouchableOpacity onPress={onClose} style={localStyles.closeButton}>
<Ionicons name="close" size={24} color="rgba(255,255,255,0.5)" />
</TouchableOpacity>
</View>
<View style={localStyles.content}>
<ScrollView showsVerticalScrollIndicator={false} contentContainerStyle={localStyles.content}>
{/* Segment Type Selector */}
<View>
<Text style={localStyles.label}>Segment Type</Text>
<View style={localStyles.typeRow}>
{segmentTypes.map((type) => (
<TouchableOpacity
key={type.value}
onPress={() => setSegmentType(type.value)}
style={[
localStyles.typeButton,
segmentType === type.value && localStyles.typeButtonActive
]}
>
<MaterialIcons
name={type.icon}
size={18}
color={segmentType === type.value ? 'black' : 'rgba(255,255,255,0.6)'}
/>
<Text style={[
localStyles.typeButtonText,
segmentType === type.value && localStyles.typeButtonTextActive
]}>
{type.label}
</Text>
</TouchableOpacity>
))}
</View>
</View>
{/* Start Time Input */}
<View style={localStyles.inputRow}>
<View style={{ flex: 1 }}>
@ -214,7 +247,7 @@ export const SubmitIntroModal: React.FC<SubmitIntroModalProps> = ({
)}
</TouchableOpacity>
</View>
</View>
</ScrollView>
</Animated.View>
</View>
</View>
@ -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',
},
});

View file

@ -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<string | null> {
@ -154,7 +164,7 @@ async function fetchFromAniSkip(malId: string, episode: number): Promise<SkipInt
async function fetchFromIntroDb(imdbId: string, season: number, episode: number): Promise<SkipInterval[]> {
try {
const response = await axios.get<IntroTimestamps>(`${INTRODB_API_URL}/intro`, {
const response = await axios.get<IntroTimestamps>(`${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<boolean> {
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<IntroTimestamps | null> {
): Promise<any | null> {
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;