mirror of
https://github.com/tapframe/NuvioStreaming.git
synced 2026-03-11 17:45:38 +00:00
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:
parent
ec7525668b
commit
f73e418b36
7 changed files with 33 additions and 331 deletions
|
|
@ -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>
|
||||
|
||||
|
|
|
|||
|
|
@ -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 */}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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(() => {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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 [];
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
Loading…
Reference in a new issue