Custom TTL for Stream cache

This commit is contained in:
tapframe 2025-10-21 23:34:35 +05:30
parent f0271cd395
commit ce7f92b540
7 changed files with 321 additions and 146 deletions

View file

@ -461,7 +461,7 @@
); );
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_DEBUG"; OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_DEBUG";
PRODUCT_BUNDLE_IDENTIFIER = com.nuvio.app; PRODUCT_BUNDLE_IDENTIFIER = com.nuvio.app;
PRODUCT_NAME = Nuvio; PRODUCT_NAME = "Nuvio";
SWIFT_OBJC_BRIDGING_HEADER = "Nuvio/Nuvio-Bridging-Header.h"; SWIFT_OBJC_BRIDGING_HEADER = "Nuvio/Nuvio-Bridging-Header.h";
SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 5.0; SWIFT_VERSION = 5.0;
@ -492,8 +492,8 @@
"-lc++", "-lc++",
); );
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE"; OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE";
PRODUCT_BUNDLE_IDENTIFIER = com.nuviohub.app; PRODUCT_BUNDLE_IDENTIFIER = "com.nuvio.app";
PRODUCT_NAME = Nuvio; PRODUCT_NAME = "Nuvio";
SWIFT_OBJC_BRIDGING_HEADER = "Nuvio/Nuvio-Bridging-Header.h"; SWIFT_OBJC_BRIDGING_HEADER = "Nuvio/Nuvio-Bridging-Header.h";
SWIFT_VERSION = 5.0; SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2"; TARGETED_DEVICE_FAMILY = "1,2";

View file

@ -1,99 +1,101 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0"> <plist version="1.0">
<dict> <dict>
<key>CADisableMinimumFrameDurationOnPhone</key> <key>CADisableMinimumFrameDurationOnPhone</key>
<true/> <true/>
<key>CFBundleDevelopmentRegion</key> <key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string> <string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleDisplayName</key> <key>CFBundleDisplayName</key>
<string>Nuvio</string> <string>Nuvio</string>
<key>CFBundleExecutable</key> <key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string> <string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key> <key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string> <string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key> <key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string> <string>6.0</string>
<key>CFBundleName</key> <key>CFBundleName</key>
<string>$(PRODUCT_NAME)</string> <string>$(PRODUCT_NAME)</string>
<key>CFBundlePackageType</key> <key>CFBundlePackageType</key>
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string> <string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
<key>CFBundleShortVersionString</key> <key>CFBundleShortVersionString</key>
<string>1.2.6</string> <string>1.2.6</string>
<key>CFBundleSignature</key> <key>CFBundleSignature</key>
<string>????</string> <string>????</string>
<key>CFBundleURLTypes</key> <key>CFBundleURLTypes</key>
<array> <array>
<dict> <dict>
<key>CFBundleURLSchemes</key> <key>CFBundleURLSchemes</key>
<array> <array>
<string>nuvio</string> <string>nuvio</string>
<string>com.nuvio.app</string> <string>com.nuvio.app</string>
</array> </array>
</dict> </dict>
<dict> <dict>
<key>CFBundleURLSchemes</key> <key>CFBundleURLSchemes</key>
<array> <array>
<string>exp+nuvio</string> <string>exp+nuvio</string>
</array> </array>
</dict> </dict>
</array> </array>
<key>CFBundleVersion</key> <key>CFBundleVersion</key>
<string>21</string> <string>21</string>
<key>LSMinimumSystemVersion</key> <key>LSMinimumSystemVersion</key>
<string>12.0</string> <string>12.0</string>
<key>LSRequiresIPhoneOS</key> <key>LSRequiresIPhoneOS</key>
<true/> <true/>
<key>LSSupportsOpeningDocumentsInPlace</key> <key>LSSupportsOpeningDocumentsInPlace</key>
<true/> <true/>
<key>NSAppTransportSecurity</key> <key>NSAppTransportSecurity</key>
<dict> <dict>
<key>NSAllowsArbitraryLoads</key> <key>NSAllowsArbitraryLoads</key>
<true/> <true/>
</dict> </dict>
<key>NSBonjourServices</key> <key>NSBonjourServices</key>
<array> <array>
<string>_http._tcp</string> <string>_http._tcp</string>
</array> </array>
<key>NSLocalNetworkUsageDescription</key> <key>NSLocalNetworkUsageDescription</key>
<string>Allow $(PRODUCT_NAME) to access your local network</string> <string>Allow $(PRODUCT_NAME) to access your local network</string>
<key>RCTNewArchEnabled</key> <key>NSMicrophoneUsageDescription</key>
<true/> <string>This app does not require microphone access.</string>
<key>RCTRootViewBackgroundColor</key> <key>RCTNewArchEnabled</key>
<integer>4278322180</integer> <true/>
<key>UIBackgroundModes</key> <key>RCTRootViewBackgroundColor</key>
<array> <integer>4278322180</integer>
<string>audio</string> <key>UIBackgroundModes</key>
</array> <array>
<key>UIFileSharingEnabled</key> <string>audio</string>
<true/> </array>
<key>UILaunchStoryboardName</key> <key>UIFileSharingEnabled</key>
<string>SplashScreen</string> <true/>
<key>UIRequiredDeviceCapabilities</key> <key>UILaunchStoryboardName</key>
<array> <string>SplashScreen</string>
<string>arm64</string> <key>UIRequiredDeviceCapabilities</key>
</array> <array>
<key>UIRequiresFullScreen</key> <string>arm64</string>
<false/> </array>
<key>UIStatusBarStyle</key> <key>UIRequiresFullScreen</key>
<string>UIStatusBarStyleDefault</string> <false/>
<key>UISupportedInterfaceOrientations</key> <key>UIStatusBarStyle</key>
<array> <string>UIStatusBarStyleDefault</string>
<string>UIInterfaceOrientationPortrait</string> <key>UISupportedInterfaceOrientations</key>
<string>UIInterfaceOrientationPortraitUpsideDown</string> <array>
<string>UIInterfaceOrientationLandscapeLeft</string> <string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationLandscapeRight</string> <string>UIInterfaceOrientationPortraitUpsideDown</string>
</array> <string>UIInterfaceOrientationLandscapeLeft</string>
<key>UISupportedInterfaceOrientations~ipad</key> <string>UIInterfaceOrientationLandscapeRight</string>
<array> </array>
<string>UIInterfaceOrientationPortrait</string> <key>UISupportedInterfaceOrientations~ipad</key>
<string>UIInterfaceOrientationPortraitUpsideDown</string> <array>
<string>UIInterfaceOrientationLandscapeLeft</string> <string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationLandscapeRight</string> <string>UIInterfaceOrientationPortraitUpsideDown</string>
</array> <string>UIInterfaceOrientationLandscapeLeft</string>
<key>UIUserInterfaceStyle</key> <string>UIInterfaceOrientationLandscapeRight</string>
<string>Dark</string> </array>
<key>UIViewControllerBasedStatusBarAppearance</key> <key>UIUserInterfaceStyle</key>
<false/> <string>Dark</string>
</dict> <key>UIViewControllerBasedStatusBarAppearance</key>
</plist> <false/>
</dict>
</plist>

View file

@ -1,5 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0"> <plist version="1.0">
<dict/> <dict>
</plist> <key>aps-environment</key>
<string>development</string>
<key>com.apple.developer.associated-domains</key>
<array/>
</dict>
</plist>

View file

@ -85,6 +85,7 @@ export interface AppSettings {
// Continue Watching behavior // Continue Watching behavior
useCachedStreams: boolean; // Enable/disable direct player navigation from Continue Watching cache useCachedStreams: boolean; // Enable/disable direct player navigation from Continue Watching cache
openMetadataScreenWhenCacheDisabled: boolean; // When cache disabled, open MetadataScreen instead of StreamsScreen openMetadataScreenWhenCacheDisabled: boolean; // When cache disabled, open MetadataScreen instead of StreamsScreen
streamCacheTTL: number; // Stream cache duration in milliseconds (default: 1 hour)
} }
export const DEFAULT_SETTINGS: AppSettings = { export const DEFAULT_SETTINGS: AppSettings = {
@ -143,6 +144,7 @@ export const DEFAULT_SETTINGS: AppSettings = {
// Continue Watching behavior // Continue Watching behavior
useCachedStreams: false, // Enable by default useCachedStreams: false, // Enable by default
openMetadataScreenWhenCacheDisabled: true, // Default to StreamsScreen when cache disabled openMetadataScreenWhenCacheDisabled: true, // Default to StreamsScreen when cache disabled
streamCacheTTL: 60 * 60 * 1000, // Default: 1 hour in milliseconds
}; };
const SETTINGS_STORAGE_KEY = 'app_settings'; const SETTINGS_STORAGE_KEY = 'app_settings';

View file

@ -18,11 +18,29 @@ import { useTheme } from '../contexts/ThemeContext';
import { useSettings } from '../hooks/useSettings'; import { useSettings } from '../hooks/useSettings';
import { RootStackParamList } from '../navigation/AppNavigator'; import { RootStackParamList } from '../navigation/AppNavigator';
// TTL options in milliseconds - organized in rows
const TTL_OPTIONS = [
[
{ label: '15 min', value: 15 * 60 * 1000 },
{ label: '30 min', value: 30 * 60 * 1000 },
{ label: '1 hour', value: 60 * 60 * 1000 },
],
[
{ label: '2 hours', value: 2 * 60 * 60 * 1000 },
{ label: '6 hours', value: 6 * 60 * 60 * 1000 },
{ label: '12 hours', value: 12 * 60 * 60 * 1000 },
],
[
{ label: '24 hours', value: 24 * 60 * 60 * 1000 },
],
];
const ContinueWatchingSettingsScreen: React.FC = () => { const ContinueWatchingSettingsScreen: React.FC = () => {
const navigation = useNavigation<NavigationProp<RootStackParamList>>(); const navigation = useNavigation<NavigationProp<RootStackParamList>>();
const { settings, updateSetting } = useSettings(); const { settings, updateSetting } = useSettings();
const { currentTheme } = useTheme(); const { currentTheme } = useTheme();
const colors = currentTheme.colors; const colors = currentTheme.colors;
const styles = createStyles(colors);
const [showSavedIndicator, setShowSavedIndicator] = useState(false); const [showSavedIndicator, setShowSavedIndicator] = useState(false);
const fadeAnim = React.useRef(new Animated.Value(0)).current; const fadeAnim = React.useRef(new Animated.Value(0)).current;
@ -96,7 +114,6 @@ const ContinueWatchingSettingsScreen: React.FC = () => {
styles.settingItem, styles.settingItem,
{ {
borderBottomColor: isLast ? 'transparent' : colors.border, borderBottomColor: isLast ? 'transparent' : colors.border,
backgroundColor: colors.elevation1
} }
]}> ]}>
<View style={styles.settingContent}> <View style={styles.settingContent}>
@ -111,31 +128,52 @@ const ContinueWatchingSettingsScreen: React.FC = () => {
</View> </View>
); );
const SectionHeader = ({ title }: { title: string }) => (
<View style={[styles.sectionHeader, { backgroundColor: colors.darkBackground }]}> const TTLPickerItem = ({ option }: { option: { label: string; value: number } }) => {
<Text style={[styles.sectionTitle, { color: colors.highEmphasis }]}> const isSelected = settings.streamCacheTTL === option.value;
{title} return (
</Text> <TouchableOpacity
</View> style={[
); styles.ttlOption,
{
backgroundColor: isSelected ? colors.primary : colors.elevation1,
borderColor: isSelected ? colors.primary : colors.border,
}
]}
onPress={() => handleUpdateSetting('streamCacheTTL', option.value)}
activeOpacity={0.7}
>
<Text style={[
styles.ttlOptionText,
{ color: isSelected ? colors.white : colors.highEmphasis }
]}>
{option.label}
</Text>
{isSelected && (
<MaterialIcons name="check" size={20} color={colors.white} />
)}
</TouchableOpacity>
);
};
return ( return (
<SafeAreaView style={[styles.container, { backgroundColor: colors.darkBackground }]}> <SafeAreaView style={[styles.container, { backgroundColor: colors.darkBackground }]}>
<StatusBar barStyle="light-content" /> <StatusBar barStyle="light-content" />
{/* Header */} {/* Header */}
<View style={[styles.header, { backgroundColor: colors.darkBackground }]}> <View style={styles.header}>
<TouchableOpacity <TouchableOpacity
style={styles.backButton} style={styles.backButton}
onPress={handleBack} onPress={handleBack}
> >
<MaterialIcons name="chevron-left" size={28} color={colors.primary} /> <MaterialIcons name="chevron-left" size={28} color={colors.white} />
<Text style={[styles.backText, { color: colors.primary }]}>Settings</Text> <Text style={styles.backText}>Settings</Text>
</TouchableOpacity> </TouchableOpacity>
<Text style={[styles.headerTitle, { color: colors.highEmphasis }]}>
Continue Watching
</Text>
</View> </View>
<Text style={styles.headerTitle}>
Continue Watching
</Text>
{/* Content */} {/* Content */}
<ScrollView <ScrollView
@ -143,9 +181,9 @@ const ContinueWatchingSettingsScreen: React.FC = () => {
showsVerticalScrollIndicator={false} showsVerticalScrollIndicator={false}
contentContainerStyle={styles.contentContainer} contentContainerStyle={styles.contentContainer}
> >
<SectionHeader title="PLAYBACK BEHAVIOR" /> <View style={styles.section}>
<Text style={styles.sectionTitle}>PLAYBACK BEHAVIOR</Text>
<View style={[styles.settingsCard, { backgroundColor: colors.elevation1 }]}> <View style={styles.settingsCard}>
<SettingItem <SettingItem
title="Use Cached Streams" title="Use Cached Streams"
description="When enabled, clicking Continue Watching items will open the player directly using previously played streams. When disabled, opens a content screen instead." description="When enabled, clicking Continue Watching items will open the player directly using previously played streams. When disabled, opens a content screen instead."
@ -162,9 +200,52 @@ const ContinueWatchingSettingsScreen: React.FC = () => {
isLast={true} isLast={true}
/> />
)} )}
</View>
</View> </View>
<View style={[styles.infoCard, { backgroundColor: colors.elevation1 }]}> {settings.useCachedStreams && (
<View style={styles.section}>
<Text style={styles.sectionTitle}>CACHE SETTINGS</Text>
<View style={styles.settingsCard}>
<View style={[styles.settingItem, { borderBottomWidth: 0, flexDirection: 'column', alignItems: 'flex-start' }]}>
<Text style={[styles.settingTitle, { color: colors.highEmphasis, marginBottom: 8 }]}>
Stream Cache Duration
</Text>
<Text style={[styles.settingDescription, { color: colors.mediumEmphasis, marginBottom: 16 }]}>
How long to keep cached stream links before they expire
</Text>
<View style={styles.ttlOptionsContainer}>
{TTL_OPTIONS.map((row, rowIndex) => (
<View key={rowIndex} style={styles.ttlRow}>
{row.map((option) => (
<TTLPickerItem key={option.value} option={option} />
))}
</View>
))}
</View>
</View>
</View>
</View>
)}
{settings.useCachedStreams && (
<View style={styles.section}>
<View style={[styles.warningCard, { borderColor: colors.warning }]}>
<View style={styles.warningHeader}>
<MaterialIcons name="warning" size={20} color={colors.warning} />
<Text style={[styles.warningTitle, { color: colors.warning }]}>
Important Note
</Text>
</View>
<Text style={[styles.warningText, { color: colors.mediumEmphasis }]}>
Not all stream links may remain active for the full cache duration. Longer cache times may result in expired links. If a cached link fails, the app will fall back to fetching fresh streams.
</Text>
</View>
</View>
)}
<View style={styles.section}>
<View style={styles.infoCard}>
<View style={styles.infoHeader}> <View style={styles.infoHeader}>
<MaterialIcons name="info" size={20} color={colors.primary} /> <MaterialIcons name="info" size={20} color={colors.primary} />
<Text style={[styles.infoTitle, { color: colors.highEmphasis }]}> <Text style={[styles.infoTitle, { color: colors.highEmphasis }]}>
@ -172,12 +253,24 @@ const ContinueWatchingSettingsScreen: React.FC = () => {
</Text> </Text>
</View> </View>
<Text style={[styles.infoText, { color: colors.mediumEmphasis }]}> <Text style={[styles.infoText, { color: colors.mediumEmphasis }]}>
Streams are cached for 1 hour after playing{'\n'} {settings.useCachedStreams ? (
Cached streams are validated before use{'\n'} <>
If cache is invalid or expired, falls back to content screen{'\n'} Streams are cached for your selected duration after playing{'\n'}
"Use Cached Streams" controls direct player vs screen navigation{'\n'} Cached streams are validated before use{'\n'}
"Open Metadata Screen" appears only when cached streams are disabled If cache is invalid or expired, falls back to content screen{'\n'}
"Use Cached Streams" controls direct player vs screen navigation{'\n'}
"Open Metadata Screen" appears only when cached streams are disabled
</>
) : (
<>
When cached streams are disabled, clicking Continue Watching items opens content screens{'\n'}
"Open Metadata Screen" option controls which screen to open{'\n'}
Metadata screen shows content details and allows manual stream selection{'\n'}
Streams screen shows available streams for immediate playback
</>
)}
</Text> </Text>
</View>
</View> </View>
</ScrollView> </ScrollView>
@ -198,31 +291,35 @@ const ContinueWatchingSettingsScreen: React.FC = () => {
); );
}; };
const styles = StyleSheet.create({ // Create a styles creator function that accepts the theme colors
const createStyles = (colors: any) => StyleSheet.create({
container: { container: {
flex: 1, flex: 1,
}, },
header: { header: {
flexDirection: 'row', flexDirection: 'row',
alignItems: 'center', alignItems: 'center',
justifyContent: 'space-between',
paddingHorizontal: 16, paddingHorizontal: 16,
paddingVertical: 12, paddingTop: Platform.OS === 'android' ? (StatusBar.currentHeight || 0) + 8 : 8,
paddingTop: Platform.OS === 'ios' ? 0 : 12,
}, },
backButton: { backButton: {
flexDirection: 'row', flexDirection: 'row',
alignItems: 'center', alignItems: 'center',
marginRight: 16, padding: 8,
}, },
backText: { backText: {
fontSize: 16, fontSize: 17,
fontWeight: '600', fontWeight: '400',
marginLeft: 4, color: colors.primary,
}, },
headerTitle: { headerTitle: {
fontSize: 20, fontSize: 34,
fontWeight: '700', fontWeight: '700',
flex: 1, color: colors.white,
paddingHorizontal: 16,
paddingBottom: 16,
paddingTop: 8,
}, },
content: { content: {
flex: 1, flex: 1,
@ -230,26 +327,33 @@ const styles = StyleSheet.create({
contentContainer: { contentContainer: {
paddingBottom: 100, paddingBottom: 100,
}, },
sectionHeader: { section: {
paddingHorizontal: 16, marginBottom: 24,
paddingVertical: 12,
paddingTop: 24,
}, },
sectionTitle: { sectionTitle: {
fontSize: 13, fontSize: 13,
fontWeight: '700', fontWeight: '600',
color: colors.mediumGray,
marginHorizontal: 16,
marginBottom: 8,
letterSpacing: 0.5, letterSpacing: 0.5,
textTransform: 'uppercase', textTransform: 'uppercase',
}, },
settingsCard: { settingsCard: {
marginHorizontal: 16, marginHorizontal: 16,
backgroundColor: colors.elevation2,
borderRadius: 12, borderRadius: 12,
padding: 16,
shadowColor: '#000',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.1,
shadowRadius: 4,
elevation: 2,
overflow: 'hidden', overflow: 'hidden',
}, },
settingItem: { settingItem: {
flexDirection: 'row', flexDirection: 'row',
alignItems: 'center', alignItems: 'center',
paddingHorizontal: 16,
paddingVertical: 16, paddingVertical: 16,
borderBottomWidth: 1, borderBottomWidth: 1,
}, },
@ -269,8 +373,14 @@ const styles = StyleSheet.create({
infoCard: { infoCard: {
marginHorizontal: 16, marginHorizontal: 16,
marginTop: 16, marginTop: 16,
padding: 16, backgroundColor: colors.elevation2,
borderRadius: 12, borderRadius: 12,
padding: 16,
shadowColor: '#000',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.1,
shadowRadius: 4,
elevation: 2,
}, },
infoHeader: { infoHeader: {
flexDirection: 'row', flexDirection: 'row',
@ -304,6 +414,58 @@ const styles = StyleSheet.create({
fontWeight: '600', fontWeight: '600',
marginLeft: 8, marginLeft: 8,
}, },
ttlOptionsContainer: {
width: '100%',
gap: 8,
},
ttlRow: {
flexDirection: 'row',
justifyContent: 'space-between',
width: '100%',
gap: 8,
},
ttlOption: {
flexDirection: 'row',
alignItems: 'center',
paddingHorizontal: 12,
paddingVertical: 10,
borderRadius: 8,
borderWidth: 1,
flex: 1,
justifyContent: 'center',
gap: 6,
},
ttlOptionText: {
fontSize: 14,
fontWeight: '600',
},
warningCard: {
marginHorizontal: 16,
marginTop: 16,
backgroundColor: colors.elevation2,
borderRadius: 12,
padding: 16,
borderWidth: 1,
shadowColor: '#000',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.1,
shadowRadius: 4,
elevation: 2,
},
warningHeader: {
flexDirection: 'row',
alignItems: 'center',
marginBottom: 12,
},
warningTitle: {
fontSize: 16,
fontWeight: '600',
marginLeft: 8,
},
warningText: {
fontSize: 14,
lineHeight: 20,
},
}); });
export default ContinueWatchingSettingsScreen; export default ContinueWatchingSettingsScreen;

View file

@ -1159,7 +1159,8 @@ export const StreamsScreen = () => {
season, season,
episode, episode,
episodeTitle, episodeTitle,
imdbId || undefined imdbId || undefined,
settings.streamCacheTTL
); );
} catch (error) { } catch (error) {
logger.warn('[StreamsScreen] Failed to save stream to cache:', error); logger.warn('[StreamsScreen] Failed to save stream to cache:', error);

View file

@ -18,7 +18,7 @@ export interface StreamCacheEntry {
expiresAt: number; // Timestamp when cache expires expiresAt: number; // Timestamp when cache expires
} }
const CACHE_DURATION = 60 * 60 * 1000; // 1 hour in milliseconds const DEFAULT_CACHE_DURATION = 60 * 60 * 1000; // 1 hour in milliseconds (fallback)
const CACHE_KEY_PREFIX = 'stream_cache_'; const CACHE_KEY_PREFIX = 'stream_cache_';
class StreamCacheService { class StreamCacheService {
@ -34,7 +34,8 @@ class StreamCacheService {
season?: number, season?: number,
episode?: number, episode?: number,
episodeTitle?: string, episodeTitle?: string,
imdbId?: string imdbId?: string,
cacheDuration?: number
): Promise<void> { ): Promise<void> {
try { try {
const cacheKey = this.getCacheKey(id, type, episodeId); const cacheKey = this.getCacheKey(id, type, episodeId);
@ -52,16 +53,18 @@ class StreamCacheService {
url: stream.url url: stream.url
}; };
const ttl = cacheDuration || DEFAULT_CACHE_DURATION;
const cacheEntry: StreamCacheEntry = { const cacheEntry: StreamCacheEntry = {
cachedStream, cachedStream,
expiresAt: now + CACHE_DURATION expiresAt: now + ttl
}; };
await AsyncStorage.setItem(cacheKey, JSON.stringify(cacheEntry)); await AsyncStorage.setItem(cacheKey, JSON.stringify(cacheEntry));
logger.log(`💾 [StreamCache] Saved stream cache for ${type}:${id}${episodeId ? `:${episodeId}` : ''}`); logger.log(`💾 [StreamCache] Saved stream cache for ${type}:${id}${episodeId ? `:${episodeId}` : ''}`);
logger.log(`💾 [StreamCache] Cache key: ${cacheKey}`); logger.log(`💾 [StreamCache] Cache key: ${cacheKey}`);
logger.log(`💾 [StreamCache] Stream URL: ${stream.url}`); logger.log(`💾 [StreamCache] Stream URL: ${stream.url}`);
logger.log(`💾 [StreamCache] Expires at: ${new Date(now + CACHE_DURATION).toISOString()}`); logger.log(`💾 [StreamCache] TTL: ${ttl / 1000 / 60} minutes`);
logger.log(`💾 [StreamCache] Expires at: ${new Date(now + ttl).toISOString()}`);
} catch (error) { } catch (error) {
logger.warn('[StreamCache] Failed to save stream to cache:', error); logger.warn('[StreamCache] Failed to save stream to cache:', error);
} }