diff --git a/src/components/player/AndroidVideoPlayer.tsx b/src/components/player/AndroidVideoPlayer.tsx index 8312fb74..e22c2847 100644 --- a/src/components/player/AndroidVideoPlayer.tsx +++ b/src/components/player/AndroidVideoPlayer.tsx @@ -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(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} /> diff --git a/src/components/player/KSPlayerCore.tsx b/src/components/player/KSPlayerCore.tsx index a607a1e9..79574b9e 100644 --- a/src/components/player/KSPlayerCore.tsx +++ b/src/components/player/KSPlayerCore.tsx @@ -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(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 */} diff --git a/src/components/player/common/UpNextButton.tsx b/src/components/player/common/UpNextButton.tsx index d8c5de0b..9958e0c6 100644 --- a/src/components/player/common/UpNextButton.tsx +++ b/src/components/player/common/UpNextButton.tsx @@ -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 = ({ @@ -51,7 +49,6 @@ const UpNextButton: React.FC = ({ 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 = ({ 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 diff --git a/src/components/player/overlays/SkipIntroButton.tsx b/src/components/player/overlays/SkipIntroButton.tsx index 2061aab7..b5329c6a 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, 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 = ({ episode, malId, kitsuId, - tmdbId, currentTime, onSkip, - onCreditsInfo, controlsVisible = false, controlsFixedOffset = 100, }) => { @@ -69,22 +65,20 @@ export const SkipIntroButton: React.FC = ({ // 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 = ({ 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(() => { diff --git a/src/hooks/useSettings.ts b/src/hooks/useSettings.ts index 838afb94..c285a1de 100644 --- a/src/hooks/useSettings.ts +++ b/src/hooks/useSettings.ts @@ -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, diff --git a/src/screens/settings/PlaybackSettingsScreen.tsx b/src/screens/settings/PlaybackSettingsScreen.tsx index 00b3f998..8cd69710 100644 --- a/src/screens/settings/PlaybackSettingsScreen.tsx +++ b/src/screens/settings/PlaybackSettingsScreen.tsx @@ -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 = ( const config = useRealtimeConfig(); const [introDbLogoXml, setIntroDbLogoXml] = useState(null); - const [theIntroDbLoaded, setTheIntroDbLoaded] = useState(false); useEffect(() => { let cancelled = false; @@ -105,57 +103,20 @@ export const PlaybackSettingsContent: React.FC = ( }; }, []); - // 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 ? ( - - ) : ( - - ); - } else { - // Show IntroDB logo (legacy) - return introDbLogoXml ? ( - - ) : ( - - ); - } - }, [settings?.introDbSource, introDbLogoXml, theIntroDbLoaded, currentTheme.colors.primary]); + const introDbLogoIcon = introDbLogoXml ? ( + + ) : ( + + ); // Bottom sheet refs const audioLanguageSheetRef = useRef(null); const subtitleLanguageSheetRef = useRef(null); const subtitleSourceSheetRef = useRef(null); - const introSourceSheetRef = useRef(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 = ( 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 = ( 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 = ( ( = ( onValueChange={(value) => updateSetting('skipIntroEnabled', value)} /> )} + isLast isTablet={isTablet} /> - {settings?.skipIntroEnabled && ( - } - onPress={openIntroSourceSheet} - isLast - isTablet={isTablet} - /> - )} {/* Audio & Subtitle Preferences */} @@ -509,67 +442,6 @@ export const PlaybackSettingsContent: React.FC = ( })} - - {/* Intro Source Bottom Sheet */} - - - Skip Intro Source - - - {[ - { 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 ( - handleSelectIntroSource(option.value as 'theintrodb' | 'introdb')} - > - - - {option.value === 'theintrodb' ? ( - - ) : ( - introDbLogoXml ? ( - - ) : ( - - ) - )} - - {option.label} - - - - {option.description} - - - {isSelected && ( - - )} - - ); - })} - - ); }; diff --git a/src/services/introService.ts b/src/services/introService.ts index 1cce4ee1..9d225903 100644 --- a/src/services/introService.ts +++ b/src/services/introService.ts @@ -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 { - 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(`${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 { try { const response = await axios.get(`${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 { + // 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 []; } /**