From 3de2fb480906f03181fc6d2a17e9dcf2aa369232 Mon Sep 17 00:00:00 2001 From: paregi12 Date: Sun, 4 Jan 2026 11:45:05 +0530 Subject: [PATCH 1/4] feat: implement AniSkip support in video player --- package-lock.json | 2 +- package.json | 2 +- src/components/player/AndroidVideoPlayer.tsx | 2 + .../player/overlays/SkipIntroButton.tsx | 149 ++++++++++++------ src/services/introService.ts | 144 +++++++++++++++-- 5 files changed, 232 insertions(+), 67 deletions(-) diff --git a/package-lock.json b/package-lock.json index e690a85..9b8ddc5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -100,7 +100,7 @@ "babel-plugin-transform-remove-console": "^6.9.4", "patch-package": "^8.0.1", "react-native-svg-transformer": "^1.5.0", - "typescript": "^5.3.3", + "typescript": "^5.9.3", "xcode": "^3.0.1" } }, diff --git a/package.json b/package.json index f6c33bd..07aa652 100644 --- a/package.json +++ b/package.json @@ -100,7 +100,7 @@ "babel-plugin-transform-remove-console": "^6.9.4", "patch-package": "^8.0.1", "react-native-svg-transformer": "^1.5.0", - "typescript": "^5.3.3", + "typescript": "^5.9.3", "xcode": "^3.0.1" }, "private": true diff --git a/src/components/player/AndroidVideoPlayer.tsx b/src/components/player/AndroidVideoPlayer.tsx index 3ee2f38..d6a43e1 100644 --- a/src/components/player/AndroidVideoPlayer.tsx +++ b/src/components/player/AndroidVideoPlayer.tsx @@ -943,6 +943,8 @@ const AndroidVideoPlayer: React.FC = () => { type={type || 'movie'} season={season} episode={episode} + malId={(metadata as any)?.mal_id || (metadata as any)?.external_ids?.mal_id} + kitsuId={id?.startsWith('kitsu:') ? id.split(':')[1] : undefined} currentTime={playerState.currentTime} onSkip={(endTime) => controlsHook.seekToTime(endTime)} controlsVisible={playerState.showControls} diff --git a/src/components/player/overlays/SkipIntroButton.tsx b/src/components/player/overlays/SkipIntroButton.tsx index 9b2df86..580e9d2 100644 --- a/src/components/player/overlays/SkipIntroButton.tsx +++ b/src/components/player/overlays/SkipIntroButton.tsx @@ -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, IntroTimestamps } from '../../../services/introService'; +import { introService, SkipInterval, SkipType } from '../../../services/introService'; import { useTheme } from '../../../contexts/ThemeContext'; import { logger } from '../../../utils/logger'; @@ -19,6 +19,8 @@ interface SkipIntroButtonProps { type: 'movie' | 'series' | string; season?: number; episode?: number; + malId?: string; + kitsuId?: string; currentTime: number; onSkip: (endTime: number) => void; controlsVisible?: boolean; @@ -30,6 +32,8 @@ export const SkipIntroButton: React.FC = ({ type, season, episode, + malId, + kitsuId, currentTime, onSkip, controlsVisible = false, @@ -37,10 +41,15 @@ export const SkipIntroButton: React.FC = ({ }) => { const { currentTheme } = useTheme(); const insets = useSafeAreaInsets(); - const [introData, setIntroData] = useState(null); + + // State + const [skipIntervals, setSkipIntervals] = useState([]); + const [currentInterval, setCurrentInterval] = useState(null); const [isVisible, setIsVisible] = useState(false); - const [hasSkipped, setHasSkipped] = useState(false); + const [hasSkippedCurrent, setHasSkippedCurrent] = useState(false); const [autoHidden, setAutoHidden] = useState(false); + + // Refs const fetchedRef = useRef(false); const lastEpisodeRef = useRef(''); const autoHideTimerRef = useRef(null); @@ -50,14 +59,13 @@ export const SkipIntroButton: React.FC = ({ const scale = useSharedValue(0.8); const translateY = useSharedValue(0); - // Fetch intro data when episode changes + // Fetch skip data when episode changes useEffect(() => { - const episodeKey = `${imdbId}-${season}-${episode}`; + const episodeKey = `${imdbId}-${season}-${episode}-${malId}-${kitsuId}`; - // Skip if not a series or missing required data - if (type !== 'series' || !imdbId || !season || !episode) { - logger.log(`[SkipIntroButton] Skipping fetch - type: ${type}, imdbId: ${imdbId}, season: ${season}, episode: ${episode}`); - setIntroData(null); + // 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) || !season || !episode) { + setSkipIntervals([]); fetchedRef.current = false; return; } @@ -69,45 +77,76 @@ export const SkipIntroButton: React.FC = ({ lastEpisodeRef.current = episodeKey; fetchedRef.current = true; - setHasSkipped(false); + setHasSkippedCurrent(false); setAutoHidden(false); + setSkipIntervals([]); - const fetchIntroData = async () => { - logger.log(`[SkipIntroButton] Fetching intro data for ${imdbId} S${season}E${episode}...`); + const fetchSkipData = async () => { + logger.log(`[SkipIntroButton] Fetching skip data for S${season}E${episode} (IMDB: ${imdbId}, MAL: ${malId}, Kitsu: ${kitsuId})...`); try { - const data = await introService.getIntroTimestamps(imdbId, season, episode); - setIntroData(data); + const intervals = await introService.getSkipTimes(imdbId, season, episode, malId, kitsuId); + setSkipIntervals(intervals); - if (data) { - logger.log(`[SkipIntroButton] ✓ Found intro: ${data.start_sec}s - ${data.end_sec}s (confidence: ${data.confidence})`); + if (intervals.length > 0) { + logger.log(`[SkipIntroButton] ✓ Found ${intervals.length} skip intervals:`, intervals); } else { - logger.log(`[SkipIntroButton] ✗ No intro data available for this episode`); + logger.log(`[SkipIntroButton] ✗ No skip data available for this episode`); } } catch (error) { - logger.error('[SkipIntroButton] Error fetching intro data:', error); - setIntroData(null); + logger.error('[SkipIntroButton] Error fetching skip data:', error); + setSkipIntervals([]); } }; - fetchIntroData(); - }, [imdbId, type, season, episode]); + fetchSkipData(); + }, [imdbId, type, season, episode, malId, kitsuId]); - // Determine if button should show based on current playback position + // Determine active interval based on current playback position + useEffect(() => { + if (skipIntervals.length === 0) { + setCurrentInterval(null); + return; + } + + // Find an interval that contains the current time + const active = skipIntervals.find( + interval => currentTime >= interval.startTime && currentTime < (interval.endTime - 0.5) + ); + + if (active) { + // If we found a new active interval that is different from the previous one + if (!currentInterval || + active.startTime !== currentInterval.startTime || + active.type !== currentInterval.type) { + logger.log(`[SkipIntroButton] Entering interval: ${active.type} (${active.startTime}-${active.endTime})`); + setCurrentInterval(active); + setHasSkippedCurrent(false); // Reset skipped state for new interval + setAutoHidden(false); // Reset auto-hide for new interval + } + } else { + // No active interval + if (currentInterval) { + logger.log('[SkipIntroButton] Exiting interval'); + setCurrentInterval(null); + } + } + }, [currentTime, skipIntervals]); + + // Determine if button should show const shouldShowButton = useCallback(() => { - if (!introData || hasSkipped) return false; - // Show when within intro range, with a small buffer at the end - const inIntroRange = currentTime >= introData.start_sec && currentTime < (introData.end_sec - 0.5); + if (!currentInterval || hasSkippedCurrent) return false; + // If auto-hidden, only show when controls are visible if (autoHidden && !controlsVisible) return false; - return inIntroRange; - }, [introData, currentTime, hasSkipped, autoHidden, controlsVisible]); + + return true; + }, [currentInterval, hasSkippedCurrent, autoHidden, controlsVisible]); // Handle visibility animations useEffect(() => { const shouldShow = shouldShowButton(); if (shouldShow && !isVisible) { - logger.log(`[SkipIntroButton] Showing button - currentTime: ${currentTime.toFixed(1)}s, intro: ${introData?.start_sec}s - ${introData?.end_sec}s`); setIsVisible(true); opacity.value = withTiming(1, { duration: 300, easing: Easing.out(Easing.cubic) }); scale.value = withSpring(1, { damping: 15, stiffness: 150 }); @@ -115,8 +154,7 @@ export const SkipIntroButton: React.FC = ({ // Start 15-second auto-hide timer if (autoHideTimerRef.current) clearTimeout(autoHideTimerRef.current); autoHideTimerRef.current = setTimeout(() => { - if (!hasSkipped) { - logger.log('[SkipIntroButton] Auto-hiding after 15 seconds'); + if (!hasSkippedCurrent) { setAutoHidden(true); opacity.value = withTiming(0, { duration: 200 }); scale.value = withTiming(0.8, { duration: 200 }); @@ -124,25 +162,20 @@ export const SkipIntroButton: React.FC = ({ } }, 15000); } else if (!shouldShow && isVisible) { - logger.log(`[SkipIntroButton] Hiding button - currentTime: ${currentTime.toFixed(1)}s, hasSkipped: ${hasSkipped}`); if (autoHideTimerRef.current) clearTimeout(autoHideTimerRef.current); opacity.value = withTiming(0, { duration: 200 }); scale.value = withTiming(0.8, { duration: 200 }); // Delay hiding to allow animation to complete setTimeout(() => setIsVisible(false), 250); } - }, [shouldShowButton, isVisible]); + }, [shouldShowButton, isVisible, hasSkippedCurrent]); - // Re-show when controls become visible (if still in intro range and was auto-hidden) + // Re-show when controls become visible (if still in interval and was auto-hidden) useEffect(() => { - if (controlsVisible && autoHidden && introData && !hasSkipped) { - const inIntroRange = currentTime >= introData.start_sec && currentTime < (introData.end_sec - 0.5); - if (inIntroRange) { - logger.log('[SkipIntroButton] Re-showing button because controls became visible'); - setAutoHidden(false); - } + if (controlsVisible && autoHidden && currentInterval && !hasSkippedCurrent) { + setAutoHidden(false); } - }, [controlsVisible, autoHidden, introData, hasSkipped, currentTime]); + }, [controlsVisible, autoHidden, currentInterval, hasSkippedCurrent]); // Cleanup timer on unmount useEffect(() => { @@ -162,12 +195,32 @@ export const SkipIntroButton: React.FC = ({ // Handle skip action const handleSkip = useCallback(() => { - if (!introData) return; + if (!currentInterval) return; - logger.log(`[SkipIntroButton] User pressed Skip Intro - seeking to ${introData.end_sec}s (from ${currentTime.toFixed(1)}s)`); - setHasSkipped(true); - onSkip(introData.end_sec); - }, [introData, onSkip, currentTime]); + logger.log(`[SkipIntroButton] User pressed Skip - seeking to ${currentInterval.endTime}s`); + setHasSkippedCurrent(true); + onSkip(currentInterval.endTime); + }, [currentInterval, onSkip]); + + // Get display text based on skip type + const getButtonText = () => { + if (!currentInterval) return 'Skip'; + + switch (currentInterval.type) { + case 'op': + case 'mixed-op': + case 'intro': + return 'Skip Intro'; + case 'ed': + case 'mixed-ed': + case 'outro': + return 'Skip Ending'; + case 'recap': + return 'Skip Recap'; + default: + return 'Skip'; + } + }; // Animated styles const containerStyle = useAnimatedStyle(() => ({ @@ -175,8 +228,8 @@ export const SkipIntroButton: React.FC = ({ transform: [{ scale: scale.value }, { translateY: translateY.value }], })); - // Don't render if not visible or no intro data - if (!isVisible || !introData) { + // Don't render if not visible (and animation complete) + if (!isVisible && opacity.value === 0) { return null; } @@ -208,7 +261,7 @@ export const SkipIntroButton: React.FC = ({ color="#FFFFFF" style={styles.icon} /> - Skip Intro + {getButtonText()} { +async function getMalIdFromKitsu(kitsuId: string): Promise { try { - const response = await axios.get(`${API_BASE_URL}/intro`, { + const response = await axios.get(`${KITSU_API_URL}/anime/${kitsuId}/mappings`); + const data = response.data; + if (data && data.data) { + const malMapping = data.data.find((m: any) => m.attributes.externalSite === 'myanimelist/anime'); + if (malMapping) { + return malMapping.attributes.externalId; + } + } + } catch (error) { + logger.warn('[IntroService] Failed to fetch MAL ID from Kitsu:', error); + } + return null; +} + +async function fetchFromAniSkip(malId: string, episode: number): Promise { + try { + // Fetch OP, ED, and Recap + const url = `${ANISKIP_API_URL}/skip-times/${malId}/${episode}?types[]=op&types[]=ed&types[]=recap&types[]=mixed-op&types[]=mixed-ed`; + const response = await axios.get(url); + + if (response.data.found && response.data.results) { + return response.data.results.map((res: any) => ({ + startTime: res.interval.startTime, + endTime: res.interval.endTime, + type: res.skipType, + provider: 'aniskip', + skipId: res.skipId + })); + } + } catch (error) { + if (axios.isAxiosError(error) && error.response?.status !== 404) { + logger.error('[IntroService] Error fetching AniSkip:', error); + } + } + return []; +} + +async function fetchFromIntroDb(imdbId: string, season: number, episode: number): Promise { + try { + const response = await axios.get(`${INTRODB_API_URL}/intro`, { params: { imdb_id: imdbId, season, @@ -47,21 +87,91 @@ export async function getIntroTimestamps( confidence: response.data.confidence, }); - return response.data; + return [{ + startTime: response.data.start_sec, + endTime: response.data.end_sec, + type: 'intro', + provider: 'introdb' + }]; } 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}`); - return null; + return []; } logger.error('[IntroService] Error fetching intro timestamps:', error?.message || error); - return null; + return []; } } +/** + * Fetches skip intervals (intro, outro, recap) from available providers + */ +export async function getSkipTimes( + imdbId: string | undefined, + season: number, + episode: number, + malId?: string, + kitsuId?: string +): Promise { + // 1. 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}`); + finalMalId = await getMalIdFromKitsu(kitsuId) || undefined; + } + + if (finalMalId) { + logger.log(`[IntroService] Fetching AniSkip for MAL ID: ${finalMalId} Ep: ${episode}`); + const aniSkipIntervals = await fetchFromAniSkip(finalMalId, episode); + if (aniSkipIntervals.length > 0) { + logger.log(`[IntroService] Found ${aniSkipIntervals.length} skip intervals from AniSkip`); + return aniSkipIntervals; + } + } + + // 2. Try IntroDB (TV Shows) as fallback or for non-anime + if (imdbId) { + const introDbIntervals = await fetchFromIntroDb(imdbId, season, episode); + if (introDbIntervals.length > 0) { + return introDbIntervals; + } + } + + return []; +} + +/** + * Legacy function for backward compatibility + * Fetches intro timestamps for a TV show episode + */ +export async function getIntroTimestamps( + imdbId: string, + season: number, + episode: number +): Promise { + const intervals = await fetchFromIntroDb(imdbId, season, episode); + if (intervals.length > 0) { + 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 + }; + } + return null; +} + export const introService = { getIntroTimestamps, + getSkipTimes }; export default introService; From 0919a40c7505059b6f625ba8b7d5a07cce7df3b2 Mon Sep 17 00:00:00 2001 From: paregi12 Date: Sun, 4 Jan 2026 11:58:54 +0530 Subject: [PATCH 2/4] fix: correct AniSkip API query parameters --- src/services/introService.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/services/introService.ts b/src/services/introService.ts index 3b1541b..e7337f9 100644 --- a/src/services/introService.ts +++ b/src/services/introService.ts @@ -50,7 +50,12 @@ async function getMalIdFromKitsu(kitsuId: string): Promise { async function fetchFromAniSkip(malId: string, episode: number): Promise { try { // Fetch OP, ED, and Recap - const url = `${ANISKIP_API_URL}/skip-times/${malId}/${episode}?types[]=op&types[]=ed&types[]=recap&types[]=mixed-op&types[]=mixed-ed`; + // AniSkip expects repeated 'types' parameters without brackets: ?types=op&types=ed... + // episodeLength=0 is required for validation + const types = ['op', 'ed', 'recap', 'mixed-op', 'mixed-ed']; + const queryParams = types.map(t => `types=${t}`).join('&'); + const url = `${ANISKIP_API_URL}/skip-times/${malId}/${episode}?${queryParams}&episodeLength=0`; + const response = await axios.get(url); if (response.data.found && response.data.results) { From 6a7d6a1458c6254cd56052f20a333e4dd7b3920f Mon Sep 17 00:00:00 2001 From: paregi12 Date: Sun, 4 Jan 2026 19:23:53 +0530 Subject: [PATCH 3/4] feat: implement robust IMDb to MAL resolution for AniSkip support --- src/services/introService.ts | 57 ++++++++++++++++++++++++++++++++++++ src/services/tmdbService.ts | 4 +-- 2 files changed, 59 insertions(+), 2 deletions(-) diff --git a/src/services/introService.ts b/src/services/introService.ts index e7337f9..e8cdac4 100644 --- a/src/services/introService.ts +++ b/src/services/introService.ts @@ -1,5 +1,6 @@ import axios from 'axios'; import { logger } from '../utils/logger'; +import { tmdbService } from './tmdbService'; /** * IntroDB API service for fetching TV show intro timestamps @@ -47,6 +48,56 @@ async function getMalIdFromKitsu(kitsuId: string): Promise { return null; } +async function getMalIdFromImdb(imdbId: string): Promise { + try { + // 1. Try direct Kitsu mapping (IMDb -> Kitsu) + const kitsuDirectResponse = await axios.get(`${KITSU_API_URL}/mappings`, { + params: { + 'filter[external_site]': 'imdb', + 'filter[external_id]': imdbId, + 'include': 'item' + } + }); + + if (kitsuDirectResponse.data?.data?.length > 0) { + const kitsuId = kitsuDirectResponse.data.data[0].relationships?.item?.data?.id; + if (kitsuId) { + return await getMalIdFromKitsu(kitsuId); + } + } + + // 2. Try TMDB -> TVDB -> Kitsu path (Robust for Cinemeta users) + const tmdbId = await tmdbService.findTMDBIdByIMDB(imdbId); + + if (tmdbId) { + const extIds = await tmdbService.getShowExternalIds(tmdbId); + const tvdbId = extIds?.tvdb_id; + + if (tvdbId) { + // Search Kitsu for TVDB mapping + const kitsuTvdbResponse = await axios.get(`${KITSU_API_URL}/mappings`, { + params: { + 'filter[external_site]': 'thetvdb/series', + 'filter[external_id]': tvdbId.toString(), + 'include': 'item' + } + }); + + if (kitsuTvdbResponse.data?.data?.length > 0) { + const kitsuId = kitsuTvdbResponse.data.data[0].relationships?.item?.data?.id; + if (kitsuId) { + logger.log(`[IntroService] Resolved Kitsu ID ${kitsuId} from TVDB ID ${tvdbId} (via IMDb ${imdbId})`); + return await getMalIdFromKitsu(kitsuId); + } + } + } + } + } catch (error) { + // Silent fail - it might just not be an anime or API limit reached + } + return null; +} + async function fetchFromAniSkip(malId: string, episode: number): Promise { try { // Fetch OP, ED, and Recap @@ -129,6 +180,12 @@ export async function getSkipTimes( finalMalId = await getMalIdFromKitsu(kitsuId) || undefined; } + // If we still don't have MAL ID but have IMDb ID (e.g. Cinemeta), try to resolve it + if (!finalMalId && imdbId) { + logger.log(`[IntroService] Attempting to resolve MAL ID from IMDb ID: ${imdbId}`); + finalMalId = await getMalIdFromImdb(imdbId) || undefined; + } + if (finalMalId) { logger.log(`[IntroService] Fetching AniSkip for MAL ID: ${finalMalId} Ep: ${episode}`); const aniSkipIntervals = await fetchFromAniSkip(finalMalId, episode); diff --git a/src/services/tmdbService.ts b/src/services/tmdbService.ts index 99ba703..18e8029 100644 --- a/src/services/tmdbService.ts +++ b/src/services/tmdbService.ts @@ -850,9 +850,9 @@ export class TMDBService { } /** - * Get external IDs for a TV show (including IMDb ID) + * Get external IDs for a TV show (including IMDb ID and TVDB ID) */ - async getShowExternalIds(tmdbId: number): Promise<{ imdb_id: string | null } | null> { + async getShowExternalIds(tmdbId: number): Promise<{ imdb_id: string | null, tvdb_id?: number | null, [key: string]: any } | null> { const cacheKey = this.generateCacheKey(`tv_${tmdbId}_external_ids`); // Check cache (local or remote) From 1e60af1ffb817ab3c98e918698fa5b69c4957e29 Mon Sep 17 00:00:00 2001 From: paregi12 Date: Mon, 5 Jan 2026 00:33:33 +0530 Subject: [PATCH 4/4] feat: prioritize IntroDB and implement ARM API for faster MAL ID resolution --- src/services/introService.ts | 55 ++++++++++++++++++++++++++++-------- 1 file changed, 44 insertions(+), 11 deletions(-) diff --git a/src/services/introService.ts b/src/services/introService.ts index e8cdac4..9d22590 100644 --- a/src/services/introService.ts +++ b/src/services/introService.ts @@ -10,6 +10,7 @@ import { tmdbService } from './tmdbService'; const INTRODB_API_URL = process.env.EXPO_PUBLIC_INTRODB_API_URL; 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'; export type SkipType = 'op' | 'ed' | 'recap' | 'intro' | 'outro' | 'mixed-op' | 'mixed-ed'; @@ -32,6 +33,31 @@ export interface IntroTimestamps { confidence: number; } +async function getMalIdFromArm(imdbId: string): Promise { + try { + const response = await axios.get(ARM_IMDB_URL, { + params: { + id: imdbId, + include: 'myanimelist' + } + }); + + // ARM returns an array of matches (e.g. for different seasons) + // We typically take the first one or try to match logic if possible + if (Array.isArray(response.data) && response.data.length > 0) { + const result = response.data[0]; + if (result && result.myanimelist) { + logger.log(`[IntroService] Found MAL ID via ARM: ${result.myanimelist}`); + return result.myanimelist.toString(); + } + } + } catch (error) { + // Silent fail as this is just one of the resolution methods + // logger.warn('[IntroService] Failed to fetch MAL ID from ARM', error); + } + return null; +} + async function getMalIdFromKitsu(kitsuId: string): Promise { try { const response = await axios.get(`${KITSU_API_URL}/anime/${kitsuId}/mappings`); @@ -171,7 +197,15 @@ export async function getSkipTimes( malId?: string, kitsuId?: string ): Promise { - // 1. Try AniSkip (Anime) if we have MAL ID or Kitsu ID + // 1. Try IntroDB (TV Shows) first + if (imdbId) { + const introDbIntervals = await fetchFromIntroDb(imdbId, season, episode); + if (introDbIntervals.length > 0) { + return introDbIntervals; + } + } + + // 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 @@ -182,8 +216,15 @@ export async function getSkipTimes( // If we still don't have MAL ID but have IMDb ID (e.g. Cinemeta), try to resolve it if (!finalMalId && imdbId) { - logger.log(`[IntroService] Attempting to resolve MAL ID from IMDb ID: ${imdbId}`); - finalMalId = await getMalIdFromImdb(imdbId) || undefined; + // Priority 1: ARM API (Fastest) + logger.log(`[IntroService] Attempting to resolve MAL ID via ARM for: ${imdbId}`); + finalMalId = await getMalIdFromArm(imdbId) || undefined; + + // Priority 2: Kitsu/TMDB Chain (Fallback) + if (!finalMalId) { + logger.log(`[IntroService] ARM failed, falling back to Kitsu/TMDB chain for: ${imdbId}`); + finalMalId = await getMalIdFromImdb(imdbId) || undefined; + } } if (finalMalId) { @@ -195,14 +236,6 @@ export async function getSkipTimes( } } - // 2. Try IntroDB (TV Shows) as fallback or for non-anime - if (imdbId) { - const introDbIntervals = await fetchFromIntroDb(imdbId, season, episode); - if (introDbIntervals.length > 0) { - return introDbIntervals; - } - } - return []; }