revert: drop theintrodb.org integration

Revert the theintrodb.org skip-intro provider and return to the existing introdb.app behavior.
This commit is contained in:
tapframe 2026-01-29 11:47:46 +05:30
parent ec7525668b
commit f73e418b36
7 changed files with 33 additions and 331 deletions

View file

@ -44,7 +44,6 @@ import ParentalGuideOverlay from './overlays/ParentalGuideOverlay';
import SkipIntroButton from './overlays/SkipIntroButton';
import UpNextButton from './common/UpNextButton';
import { CustomAlert } from '../CustomAlert';
import { CreditsInfo } from '../../services/introService';
// Android-specific components
@ -145,9 +144,6 @@ const AndroidVideoPlayer: React.FC = () => {
// Subtitle sync modal state
const [showSyncModal, setShowSyncModal] = useState(false);
// Credits timing state from API
const [creditsInfo, setCreditsInfo] = useState<CreditsInfo | null>(null);
// Track auto-selection ref to prevent duplicate selections
const hasAutoSelectedTracks = useRef(false);
@ -171,7 +167,7 @@ const AndroidVideoPlayer: React.FC = () => {
}, [uri, episodeId]);
const metadataResult = useMetadata({ id: id || 'placeholder', type: (type as any) });
const { metadata, cast, tmdbId } = Boolean(id && type) ? (metadataResult as any) : { metadata: null, cast: [], tmdbId: null };
const { metadata, cast } = Boolean(id && type) ? (metadataResult as any) : { metadata: null, cast: [] };
const hasLogo = metadata && metadata.logo;
const openingAnimation = useOpeningAnimation(backdrop, metadata);
@ -976,10 +972,8 @@ const AndroidVideoPlayer: React.FC = () => {
episode={episode}
malId={(metadata as any)?.mal_id || (metadata as any)?.external_ids?.mal_id}
kitsuId={id?.startsWith('kitsu:') ? id.split(':')[1] : undefined}
tmdbId={tmdbId || undefined}
currentTime={playerState.currentTime}
onSkip={(endTime) => controlsHook.seekToTime(endTime)}
onCreditsInfo={setCreditsInfo}
controlsVisible={playerState.showControls}
controlsFixedOffset={100}
/>
@ -1005,7 +999,6 @@ const AndroidVideoPlayer: React.FC = () => {
metadata={metadataResult?.metadata ? { poster: metadataResult.metadata.poster, id: metadataResult.metadata.id } : undefined}
controlsVisible={playerState.showControls}
controlsFixedOffset={100}
creditsInfo={creditsInfo}
/>
</View>

View file

@ -21,7 +21,6 @@ import ResumeOverlay from './modals/ResumeOverlay';
import ParentalGuideOverlay from './overlays/ParentalGuideOverlay';
import SkipIntroButton from './overlays/SkipIntroButton';
import { SpeedActivatedOverlay, PauseOverlay, GestureControls } from './components';
import { CreditsInfo } from '../../services/introService';
// Platform-specific components
import { KSPlayerSurface } from './ios/components/KSPlayerSurface';
@ -155,7 +154,7 @@ const KSPlayerCore: React.FC = () => {
const speedControl = useSpeedControl(1.0);
// Metadata Hook
const { metadata, groupedEpisodes, cast, tmdbId } = useMetadata({ id, type: type as 'movie' | 'series' });
const { metadata, groupedEpisodes, cast } = useMetadata({ id, type: type as 'movie' | 'series' });
// Trakt Autosync
const traktAutosync = useTraktAutosync({
@ -178,9 +177,6 @@ const KSPlayerCore: React.FC = () => {
// Subtitle sync modal state
const [showSyncModal, setShowSyncModal] = useState(false);
// Credits timing state from API
const [creditsInfo, setCreditsInfo] = useState<CreditsInfo | null>(null);
// Track auto-selection refs to prevent duplicate selections
const hasAutoSelectedTracks = useRef(false);
@ -946,10 +942,8 @@ const KSPlayerCore: React.FC = () => {
episode={episode}
malId={(metadata as any)?.mal_id || (metadata as any)?.external_ids?.mal_id}
kitsuId={id?.startsWith('kitsu:') ? id.split(':')[1] : undefined}
tmdbId={tmdbId || undefined}
currentTime={currentTime}
onSkip={(endTime) => controls.seekToTime(endTime)}
onCreditsInfo={setCreditsInfo}
controlsVisible={showControls}
controlsFixedOffset={126}
/>
@ -975,7 +969,6 @@ const KSPlayerCore: React.FC = () => {
metadata={metadata ? { poster: metadata.poster, id: metadata.id } : undefined}
controlsVisible={showControls}
controlsFixedOffset={126}
creditsInfo={creditsInfo}
/>
{/* Modals */}

View file

@ -4,7 +4,6 @@ import { Animated } from 'react-native';
import { MaterialIcons } from '@expo/vector-icons';
import { logger } from '../../../utils/logger';
import { LinearGradient } from 'expo-linear-gradient';
import { CreditsInfo } from '../../../services/introService';
export interface Insets {
top: number;
@ -34,7 +33,6 @@ interface UpNextButtonProps {
metadata?: { poster?: string; id?: string }; // Added metadata prop
controlsVisible?: boolean;
controlsFixedOffset?: number;
creditsInfo?: CreditsInfo | null; // Add credits info from API
}
const UpNextButton: React.FC<UpNextButtonProps> = ({
@ -51,7 +49,6 @@ const UpNextButton: React.FC<UpNextButtonProps> = ({
metadata,
controlsVisible = false,
controlsFixedOffset = 100,
creditsInfo,
}) => {
const [visible, setVisible] = useState(false);
const opacity = useRef(new Animated.Value(0)).current;
@ -79,19 +76,10 @@ const UpNextButton: React.FC<UpNextButtonProps> = ({
const shouldShow = useMemo(() => {
if (!nextEpisode || duration <= 0) return false;
// If we have credits timing from API, use that as primary source
if (creditsInfo?.startTime !== null && creditsInfo?.startTime !== undefined) {
// Show button when we reach credits start time and stay visible until 10s before end
const timeRemaining = duration - currentTime;
const isInCredits = currentTime >= creditsInfo.startTime;
return isInCredits && timeRemaining > 10;
}
// Fallback: Use fixed timing (show when under ~1 minute and above 10s)
const timeRemaining = duration - currentTime;
// Be tolerant to timer jitter: show when under ~1 minute and above 10s
return timeRemaining < 61 && timeRemaining > 10;
}, [nextEpisode, duration, currentTime, creditsInfo]);
}, [nextEpisode, duration, currentTime]);
// Debug logging removed to reduce console noise
// The state is computed in shouldShow useMemo above

View file

@ -10,7 +10,7 @@ import Animated, {
import { useSafeAreaInsets } from 'react-native-safe-area-context';
import { MaterialIcons } from '@expo/vector-icons';
import { BlurView } from 'expo-blur';
import { introService, SkipInterval, SkipType, CreditsInfo } from '../../../services/introService';
import { introService, SkipInterval, SkipType } from '../../../services/introService';
import { useTheme } from '../../../contexts/ThemeContext';
import { logger } from '../../../utils/logger';
import { useSettings } from '../../../hooks/useSettings';
@ -22,10 +22,8 @@ interface SkipIntroButtonProps {
episode?: number;
malId?: string;
kitsuId?: string;
tmdbId?: number;
currentTime: number;
onSkip: (endTime: number) => void;
onCreditsInfo?: (credits: CreditsInfo | null) => void;
controlsVisible?: boolean;
controlsFixedOffset?: number;
}
@ -37,10 +35,8 @@ export const SkipIntroButton: React.FC<SkipIntroButtonProps> = ({
episode,
malId,
kitsuId,
tmdbId,
currentTime,
onSkip,
onCreditsInfo,
controlsVisible = false,
controlsFixedOffset = 100,
}) => {
@ -69,22 +65,20 @@ export const SkipIntroButton: React.FC<SkipIntroButtonProps> = ({
// Fetch skip data when episode changes
useEffect(() => {
const episodeKey = `${imdbId}-${season}-${episode}-${malId}-${kitsuId}-${tmdbId}`;
const episodeKey = `${imdbId}-${season}-${episode}-${malId}-${kitsuId}`;
if (!skipIntroEnabled) {
setSkipIntervals([]);
setCurrentInterval(null);
setIsVisible(false);
fetchedRef.current = false;
if (onCreditsInfo) onCreditsInfo(null);
return;
}
// Skip if not a series or missing required data (though MAL/Kitsu ID might be enough for some cases, usually need season/ep)
if (type !== 'series' || (!imdbId && !malId && !kitsuId && !tmdbId) || !season || !episode) {
if (type !== 'series' || (!imdbId && !malId && !kitsuId) || !season || !episode) {
setSkipIntervals([]);
fetchedRef.current = false;
if (onCreditsInfo) onCreditsInfo(null);
return;
}
@ -100,35 +94,24 @@ export const SkipIntroButton: React.FC<SkipIntroButtonProps> = ({
setSkipIntervals([]);
const fetchSkipData = async () => {
logger.log(`[SkipIntroButton] Fetching skip data for S${season}E${episode} (TMDB: ${tmdbId}, IMDB: ${imdbId}, MAL: ${malId}, Kitsu: ${kitsuId})...`);
logger.log(`[SkipIntroButton] Fetching skip data for S${season}E${episode} (IMDB: ${imdbId}, MAL: ${malId}, Kitsu: ${kitsuId})...`);
try {
const mediaType = type === 'series' ? 'tv' : type === 'movie' ? 'movie' : 'tv';
const result = await introService.getSkipTimes(imdbId, season, episode, malId, kitsuId, tmdbId, mediaType);
setSkipIntervals(result.intervals);
const intervals = await introService.getSkipTimes(imdbId, season, episode, malId, kitsuId);
setSkipIntervals(intervals);
// Pass credits info to parent via callback
if (onCreditsInfo) {
onCreditsInfo(result.credits);
}
if (result.intervals.length > 0) {
logger.log(`[SkipIntroButton] ✓ Found ${result.intervals.length} skip intervals:`, result.intervals);
if (intervals.length > 0) {
logger.log(`[SkipIntroButton] ✓ Found ${intervals.length} skip intervals:`, intervals);
} else {
logger.log(`[SkipIntroButton] ✗ No skip data available for this episode`);
}
if (result.credits) {
logger.log(`[SkipIntroButton] ✓ Found credits timing:`, result.credits);
}
} catch (error) {
logger.error('[SkipIntroButton] Error fetching skip data:', error);
setSkipIntervals([]);
if (onCreditsInfo) onCreditsInfo(null);
}
};
fetchSkipData();
}, [imdbId, type, season, episode, malId, kitsuId, tmdbId, skipIntroEnabled, onCreditsInfo]);
}, [imdbId, type, season, episode, malId, kitsuId, skipIntroEnabled]);
// Determine active interval based on current playback position
useEffect(() => {

View file

@ -59,7 +59,6 @@ export interface AppSettings {
// Playback behavior
alwaysResume: boolean; // If true, resume automatically without prompt when progress < 85%
skipIntroEnabled: boolean; // Enable/disable Skip Intro overlay (IntroDB)
introDbSource: 'theintrodb' | 'introdb'; // Preferred IntroDB source: TheIntroDB (new) or IntroDB (legacy)
// Downloads
enableDownloads: boolean; // Show Downloads tab and enable saving streams
// Theme settings
@ -148,7 +147,6 @@ export const DEFAULT_SETTINGS: AppSettings = {
// Playback behavior defaults
alwaysResume: true,
skipIntroEnabled: true,
introDbSource: 'theintrodb', // Default to TheIntroDB (new API)
// Downloads
enableDownloads: false,
useExternalPlayerForDownloads: false,

View file

@ -1,5 +1,5 @@
import React, { useState, useCallback, useMemo, useRef, useEffect } from 'react';
import { View, StyleSheet, ScrollView, StatusBar, Platform, Text, TouchableOpacity, Dimensions, Image } from 'react-native';
import { View, StyleSheet, ScrollView, StatusBar, Platform, Text, TouchableOpacity, Dimensions } from 'react-native';
import { useNavigation, useFocusEffect } from '@react-navigation/native';
import { NavigationProp } from '@react-navigation/native';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
@ -17,7 +17,6 @@ import { SvgXml } from 'react-native-svg';
const { width } = Dimensions.get('window');
const INTRODB_LOGO_URI = 'https://introdb.app/images/logo-vector.svg';
const THEINTRODB_FAVICON_URI = 'https://theintrodb.org/favicon.ico';
// Available languages for audio/subtitle selection
const AVAILABLE_LANGUAGES = [
@ -78,7 +77,6 @@ export const PlaybackSettingsContent: React.FC<PlaybackSettingsContentProps> = (
const config = useRealtimeConfig();
const [introDbLogoXml, setIntroDbLogoXml] = useState<string | null>(null);
const [theIntroDbLoaded, setTheIntroDbLoaded] = useState(false);
useEffect(() => {
let cancelled = false;
@ -105,57 +103,20 @@ export const PlaybackSettingsContent: React.FC<PlaybackSettingsContentProps> = (
};
}, []);
// Preload TheIntroDB favicon
useEffect(() => {
let cancelled = false;
const load = async () => {
try {
await fetch(THEINTRODB_FAVICON_URI);
if (!cancelled) setTheIntroDbLoaded(true);
} catch {
if (!cancelled) setTheIntroDbLoaded(false);
}
};
load();
return () => {
cancelled = true;
};
}, []);
const introDbLogoIcon = useMemo(() => {
const selectedSource = settings?.introDbSource || 'theintrodb';
if (selectedSource === 'theintrodb') {
// Show TheIntroDB favicon
return theIntroDbLoaded ? (
<Image
source={{ uri: THEINTRODB_FAVICON_URI }}
style={{ width: 20, height: 20 }}
resizeMode="contain"
/>
) : (
<MaterialIcons name="skip-next" size={18} color={currentTheme.colors.primary} />
);
} else {
// Show IntroDB logo (legacy)
return introDbLogoXml ? (
<SvgXml xml={introDbLogoXml} width={28} height={18} />
) : (
<MaterialIcons name="skip-next" size={18} color={currentTheme.colors.primary} />
);
}
}, [settings?.introDbSource, introDbLogoXml, theIntroDbLoaded, currentTheme.colors.primary]);
const introDbLogoIcon = introDbLogoXml ? (
<SvgXml xml={introDbLogoXml} width={28} height={18} />
) : (
<MaterialIcons name="skip-next" size={18} color={currentTheme.colors.primary} />
);
// Bottom sheet refs
const audioLanguageSheetRef = useRef<BottomSheetModal>(null);
const subtitleLanguageSheetRef = useRef<BottomSheetModal>(null);
const subtitleSourceSheetRef = useRef<BottomSheetModal>(null);
const introSourceSheetRef = useRef<BottomSheetModal>(null);
// Snap points
const languageSnapPoints = useMemo(() => ['70%'], []);
const sourceSnapPoints = useMemo(() => ['45%'], []);
const introSourceSnapPoints = useMemo(() => ['35%'], []);
// Handlers to present sheets - ensure only one is open at a time
const openAudioLanguageSheet = useCallback(() => {
@ -176,13 +137,6 @@ export const PlaybackSettingsContent: React.FC<PlaybackSettingsContentProps> = (
setTimeout(() => subtitleSourceSheetRef.current?.present(), 100);
}, []);
const openIntroSourceSheet = useCallback(() => {
audioLanguageSheetRef.current?.dismiss();
subtitleLanguageSheetRef.current?.dismiss();
subtitleSourceSheetRef.current?.dismiss();
setTimeout(() => introSourceSheetRef.current?.present(), 100);
}, []);
const isItemVisible = (itemId: string) => {
if (!config?.items) return true;
const item = config.items[itemId];
@ -234,17 +188,6 @@ export const PlaybackSettingsContent: React.FC<PlaybackSettingsContentProps> = (
subtitleSourceSheetRef.current?.dismiss();
};
const handleSelectIntroSource = (value: 'theintrodb' | 'introdb') => {
updateSetting('introDbSource', value);
introSourceSheetRef.current?.dismiss();
};
const getIntroSourceLabel = (value: string) => {
if (value === 'theintrodb') return 'TheIntroDB';
if (value === 'introdb') return 'IntroDB';
return 'TheIntroDB';
};
return (
<>
{hasVisibleItems(['video_player']) && (
@ -269,7 +212,7 @@ export const PlaybackSettingsContent: React.FC<PlaybackSettingsContentProps> = (
<SettingsCard title={t('player.section_playback', { defaultValue: 'Playback' })} isTablet={isTablet}>
<SettingItem
title={t('player.skip_intro_settings_title', { defaultValue: 'Skip Intro' })}
description={getIntroSourceLabel(settings?.introDbSource || 'theintrodb')}
description={t('player.powered_by_introdb', { defaultValue: 'Powered by IntroDB' })}
customIcon={introDbLogoIcon}
renderControl={() => (
<CustomSwitch
@ -277,19 +220,9 @@ export const PlaybackSettingsContent: React.FC<PlaybackSettingsContentProps> = (
onValueChange={(value) => updateSetting('skipIntroEnabled', value)}
/>
)}
isLast
isTablet={isTablet}
/>
{settings?.skipIntroEnabled && (
<SettingItem
title="Intro Source"
description={`Using ${getIntroSourceLabel(settings?.introDbSource || 'theintrodb')}`}
icon="database"
renderControl={() => <ChevronRight />}
onPress={openIntroSourceSheet}
isLast
isTablet={isTablet}
/>
)}
</SettingsCard>
{/* Audio & Subtitle Preferences */}
@ -509,67 +442,6 @@ export const PlaybackSettingsContent: React.FC<PlaybackSettingsContentProps> = (
})}
</BottomSheetScrollView>
</BottomSheetModal>
{/* Intro Source Bottom Sheet */}
<BottomSheetModal
ref={introSourceSheetRef}
index={0}
snapPoints={introSourceSnapPoints}
enableDynamicSizing={false}
enablePanDownToClose={true}
backdropComponent={renderBackdrop}
backgroundStyle={{ backgroundColor: '#1a1a1a' }}
handleIndicatorStyle={{ backgroundColor: 'rgba(255,255,255,0.3)' }}
>
<View style={styles.sheetHeader}>
<Text style={styles.sheetTitle}>Skip Intro Source</Text>
</View>
<BottomSheetScrollView contentContainerStyle={styles.sheetContent}>
{[
{ value: 'theintrodb', label: 'TheIntroDB', description: 'theintrodb.org - Supports skip recap and end credits if available', logo: THEINTRODB_FAVICON_URI },
{ value: 'introdb', label: 'IntroDB', description: 'Skip Intro database by introdb.app', logo: INTRODB_LOGO_URI }
].map((option) => {
const isSelected = option.value === (settings?.introDbSource || 'theintrodb');
return (
<TouchableOpacity
key={option.value}
style={[
styles.sourceItem,
isSelected && { backgroundColor: currentTheme.colors.primary + '20', borderColor: currentTheme.colors.primary }
]}
onPress={() => handleSelectIntroSource(option.value as 'theintrodb' | 'introdb')}
>
<View style={styles.sourceItemContent}>
<View style={{ flexDirection: 'row', alignItems: 'center', gap: 8 }}>
{option.value === 'theintrodb' ? (
<Image
source={{ uri: option.logo }}
style={{ width: 20, height: 20 }}
resizeMode="contain"
/>
) : (
introDbLogoXml ? (
<SvgXml xml={introDbLogoXml} width={28} height={18} />
) : (
<MaterialIcons name="skip-next" size={18} color={currentTheme.colors.primary} />
)
)}
<Text style={[styles.sourceLabel, { color: isSelected ? currentTheme.colors.primary : '#fff' }]}>
{option.label}
</Text>
</View>
<Text style={styles.sourceDescription}>
{option.description}
</Text>
</View>
{isSelected && (
<MaterialIcons name="check" size={20} color={currentTheme.colors.primary} />
)}
</TouchableOpacity>
);
})}
</BottomSheetScrollView>
</BottomSheetModal>
</>
);
};

View file

@ -1,7 +1,6 @@
import axios from 'axios';
import { logger } from '../utils/logger';
import { tmdbService } from './tmdbService';
import { mmkvStorage } from './mmkvStorage';
/**
* IntroDB API service for fetching TV show intro timestamps
@ -9,7 +8,6 @@ import { mmkvStorage } from './mmkvStorage';
*/
const INTRODB_API_URL = process.env.EXPO_PUBLIC_INTRODB_API_URL;
const THEINTRODB_API_URL = 'https://api.theintrodb.org/v1';
const ANISKIP_API_URL = 'https://api.aniskip.com/v2';
const KITSU_API_URL = 'https://kitsu.io/api/edge';
const ARM_IMDB_URL = 'https://arm.haglund.dev/api/v2/imdb';
@ -20,31 +18,10 @@ export interface SkipInterval {
startTime: number;
endTime: number;
type: SkipType;
provider: 'introdb' | 'aniskip' | 'theintrodb';
provider: 'introdb' | 'aniskip';
skipId?: string;
}
export interface CreditsInfo {
startTime: number | null;
endTime: number | null;
confidence: number;
}
export interface TheIntroDBTimestamp {
start_ms: number | null;
end_ms: number | null;
confidence: number;
submission_count: number;
}
export interface TheIntroDBResponse {
tmdb_id: number;
type: 'movie' | 'tv';
intro?: TheIntroDBTimestamp;
recap?: TheIntroDBTimestamp;
credits?: TheIntroDBTimestamp;
}
export interface IntroTimestamps {
imdb_id: string;
season: number;
@ -175,75 +152,6 @@ async function fetchFromAniSkip(malId: string, episode: number): Promise<SkipInt
return [];
}
async function fetchFromTheIntroDb(
tmdbId: number,
type: 'movie' | 'tv',
season?: number,
episode?: number
): Promise<{ intervals: SkipInterval[], credits: CreditsInfo | null }> {
try {
const params: any = { tmdb_id: tmdbId };
if (type === 'tv' && season !== undefined && episode !== undefined) {
params.season = season;
params.episode = episode;
}
const response = await axios.get<TheIntroDBResponse>(`${THEINTRODB_API_URL}/media`, {
params,
timeout: 5000,
});
const intervals: SkipInterval[] = [];
let credits: CreditsInfo | null = null;
// Add intro skip interval if available
if (response.data.intro && response.data.intro.end_ms !== null) {
intervals.push({
startTime: response.data.intro.start_ms !== null ? response.data.intro.start_ms / 1000 : 0,
endTime: response.data.intro.end_ms / 1000,
type: 'intro',
provider: 'theintrodb'
});
}
// Add recap skip interval if available
if (response.data.recap && response.data.recap.start_ms !== null && response.data.recap.end_ms !== null) {
intervals.push({
startTime: response.data.recap.start_ms / 1000,
endTime: response.data.recap.end_ms / 1000,
type: 'recap',
provider: 'theintrodb'
});
}
// Store credits info for next episode button timing
if (response.data.credits && response.data.credits.start_ms !== null) {
credits = {
startTime: response.data.credits.start_ms / 1000,
endTime: response.data.credits.end_ms !== null ? response.data.credits.end_ms / 1000 : null,
confidence: response.data.credits.confidence
};
}
if (intervals.length > 0 || credits) {
logger.log(`[IntroService] TheIntroDB found data for TMDB ${tmdbId}:`, {
intervals: intervals.length,
hasCredits: !!credits
});
}
return { intervals, credits };
} catch (error: any) {
if (axios.isAxiosError(error) && error.response?.status === 404) {
logger.log(`[IntroService] No TheIntroDB data for TMDB ${tmdbId}`);
return { intervals: [], credits: null };
}
logger.error('[IntroService] Error fetching from TheIntroDB:', error?.message || error);
return { intervals: [], credits: null };
}
}
async function fetchFromIntroDb(imdbId: string, season: number, episode: number): Promise<SkipInterval[]> {
try {
const response = await axios.get<IntroTimestamps>(`${INTRODB_API_URL}/intro`, {
@ -287,52 +195,19 @@ export async function getSkipTimes(
season: number,
episode: number,
malId?: string,
kitsuId?: string,
tmdbId?: number,
type?: 'movie' | 'tv'
): Promise<{ intervals: SkipInterval[], credits: CreditsInfo | null }> {
// Get user preference for intro source
const introDbSource = mmkvStorage.getString('introDbSource') || 'theintrodb';
if (introDbSource === 'theintrodb') {
// User prefers TheIntroDB (new API)
// 1. Try TheIntroDB (Primary) - Supports both movies and TV shows
if (tmdbId && type) {
const theIntroDbResult = await fetchFromTheIntroDb(tmdbId, type, season, episode);
if (theIntroDbResult.intervals.length > 0 || theIntroDbResult.credits) {
return theIntroDbResult;
}
}
// 2. Try old IntroDB (Fallback for TV Shows)
if (imdbId) {
const introDbIntervals = await fetchFromIntroDb(imdbId, season, episode);
if (introDbIntervals.length > 0) {
return { intervals: introDbIntervals, credits: null };
}
}
} else {
// User prefers IntroDB (legacy)
// 1. Try old IntroDB first
if (imdbId) {
const introDbIntervals = await fetchFromIntroDb(imdbId, season, episode);
if (introDbIntervals.length > 0) {
return { intervals: introDbIntervals, credits: null };
}
}
// 2. Try TheIntroDB as fallback
if (tmdbId && type) {
const theIntroDbResult = await fetchFromTheIntroDb(tmdbId, type, season, episode);
if (theIntroDbResult.intervals.length > 0 || theIntroDbResult.credits) {
return theIntroDbResult;
}
kitsuId?: string
): Promise<SkipInterval[]> {
// 1. Try IntroDB (TV Shows) first
if (imdbId) {
const introDbIntervals = await fetchFromIntroDb(imdbId, season, episode);
if (introDbIntervals.length > 0) {
return introDbIntervals;
}
}
// 3. Try AniSkip (Anime) if we have MAL ID or Kitsu ID
// 2. Try AniSkip (Anime) if we have MAL ID or Kitsu ID
let finalMalId = malId;
// If we have Kitsu ID but no MAL ID, try to resolve it
if (!finalMalId && kitsuId) {
logger.log(`[IntroService] Resolving MAL ID from Kitsu ID: ${kitsuId}`);
@ -357,11 +232,11 @@ export async function getSkipTimes(
const aniSkipIntervals = await fetchFromAniSkip(finalMalId, episode);
if (aniSkipIntervals.length > 0) {
logger.log(`[IntroService] Found ${aniSkipIntervals.length} skip intervals from AniSkip`);
return { intervals: aniSkipIntervals, credits: null };
return aniSkipIntervals;
}
}
return { intervals: [], credits: null };
return [];
}
/**