Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/constants/locales.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,6 @@ export const LOCALES = [
{ code: 'es', key: 'spanish' },
{ code: 'hr', key: 'croatian' },
{ code: 'zh-CN', key: 'chinese' },
{ code: 'hi', key: 'hindi' }
{ code: 'hi', key: 'hindi' },
{ code: 'sr', key: 'serbian' }
];
42 changes: 37 additions & 5 deletions src/screens/settings/PlaybackSettingsScreen.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import React, { useState, useCallback, useMemo, useRef, useEffect } from 'react';
import { View, StyleSheet, ScrollView, StatusBar, Platform, Text, TouchableOpacity, Dimensions, TextInput } from 'react-native';
import { View, StyleSheet, ScrollView, StatusBar, Platform, Text, TouchableOpacity, Dimensions, TextInput, ActivityIndicator } from 'react-native';
import { useNavigation, useFocusEffect } from '@react-navigation/native';
import { NavigationProp } from '@react-navigation/native';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
Expand All @@ -14,6 +14,7 @@ import { BottomSheetModal, BottomSheetScrollView, BottomSheetBackdrop } from '@g
import { useTranslation } from 'react-i18next';
import { SvgXml } from 'react-native-svg';
import { toastService } from '../../services/toastService';
import { introService } from '../../services/introService';

const { width } = Dimensions.get('window');

Expand Down Expand Up @@ -79,14 +80,40 @@ export const PlaybackSettingsContent: React.FC<PlaybackSettingsContentProps> = (

const [introDbLogoXml, setIntroDbLogoXml] = useState<string | null>(null);
const [apiKeyInput, setApiKeyInput] = useState(settings?.introDbApiKey || '');
const [isVerifyingKey, setIsVerifyingKey] = useState(false);

const isMounted = useRef(true);

useEffect(() => {
isMounted.current = true;
return () => {
isMounted.current = false;
};
}, []);

useEffect(() => {
setApiKeyInput(settings?.introDbApiKey || '');
}, [settings?.introDbApiKey]);

const handleApiKeySubmit = () => {
updateSetting('introDbApiKey', apiKeyInput);
toastService.success(t('settings.items.api_key_saved', { defaultValue: 'API Key Saved' }));
const handleApiKeySubmit = async () => {
if (!apiKeyInput.trim()) {
updateSetting('introDbApiKey', '');
toastService.success(t('settings.items.api_key_cleared', { defaultValue: 'API Key Cleared' }));
return;
}

setIsVerifyingKey(true);
const isValid = await introService.verifyApiKey(apiKeyInput);

if (!isMounted.current) return;
setIsVerifyingKey(false);

if (isValid) {
updateSetting('introDbApiKey', apiKeyInput);
toastService.success(t('settings.items.api_key_saved', { defaultValue: 'API Key Saved' }));
} else {
toastService.error(t('settings.items.api_key_invalid', { defaultValue: 'Invalid API Key' }));
}
};

useEffect(() => {
Expand Down Expand Up @@ -271,8 +298,13 @@ export const PlaybackSettingsContent: React.FC<PlaybackSettingsContentProps> = (
<TouchableOpacity
style={styles.confirmButton}
onPress={handleApiKeySubmit}
disabled={isVerifyingKey}
>
<MaterialIcons name="check" size={24} color="black" />
{isVerifyingKey ? (
<ActivityIndicator size="small" color="black" />
) : (
<MaterialIcons name="check" size={24} color="black" />
)}
</TouchableOpacity>
</View>
</View>
Expand Down
37 changes: 36 additions & 1 deletion src/services/introService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,40 @@ async function fetchFromIntroDb(imdbId: string, season: number, episode: number)
}
}

/**
* Verifies an IntroDB API key
*/
export async function verifyApiKey(apiKey: string): Promise<boolean> {
try {
if (!apiKey) return false;

const response = await axios.post(`${INTRODB_API_URL}/submit`, {}, {
headers: {
'Authorization': `Bearer ${apiKey}`,
'Content-Type': 'application/json'
},
timeout: 5000,
validateStatus: (status) => true // Handle status codes manually
});

// 400 means Auth passed but payload was empty/invalid -> Key is Valid
if (response.status === 400) return true;

// 200/201 would also mean valid (though unexpected with empty body)
if (response.status === 200 || response.status === 201) return true;

// Explicitly handle auth failures
if (response.status === 401 || response.status === 403) return false;

// Log warning for unexpected states (500, 429, etc.) but fail safe
logger.warn(`[IntroService] Verification received unexpected status: ${response.status}`);
return false;
} catch (error: any) {
logger.log('[IntroService] API Key verification failed:', error.message);
return false;
}
}

/**
* Submits an intro timestamp to IntroDB
*/
Expand Down Expand Up @@ -305,7 +339,8 @@ export async function getIntroTimestamps(
export const introService = {
getIntroTimestamps,
getSkipTimes,
submitIntro
submitIntro,
verifyApiKey
};

export default introService;