From fd6e29a8ececcd4d1f8921db2a3896c2d7c0ffd4 Mon Sep 17 00:00:00 2001 From: tapframe Date: Mon, 29 Dec 2025 19:48:26 +0530 Subject: [PATCH] Added granular control for TMDB Enrichment --- ios/KSPlayerView.swift | 24 +- ios/Nuvio.xcodeproj/project.pbxproj | 120 +++++----- .../player/modals/SubtitleModals.tsx | 219 ++++++++++-------- src/hooks/useMetadata.ts | 196 +++++++++------- src/hooks/useMetadataAssets.ts | 86 +++---- src/hooks/useSettings.ts | 26 ++- src/screens/SettingsScreen.tsx | 2 +- src/screens/TMDBSettingsScreen.tsx | 195 ++++++++++++++++ 8 files changed, 570 insertions(+), 298 deletions(-) diff --git a/ios/KSPlayerView.swift b/ios/KSPlayerView.swift index dea31c2c..4863323c 100644 --- a/ios/KSPlayerView.swift +++ b/ios/KSPlayerView.swift @@ -757,16 +757,26 @@ extension KSPlayerView: KSPlayerLayerDelegate { print("KSPlayerView: [READY TO PLAY] Found subtitle part: start=\(firstPart.start), end=\(firstPart.end), text='\(firstPart.text?.string ?? "nil")'") } - // Auto-select first enabled subtitle if none selected - if self.playerView.srtControl.selectedSubtitleInfo == nil { - self.playerView.srtControl.selectedSubtitleInfo = self.playerView.srtControl.subtitleInfos.first { $0.isEnabled } - if let selected = self.playerView.srtControl.selectedSubtitleInfo { - print("KSPlayerView: [READY TO PLAY] Auto-selected subtitle: \(selected.name)") + // Only auto-select first enabled subtitle if textTrack prop is NOT set to -1 (disabled) + // If React Native explicitly set textTrack=-1, user wants subtitles off + if self.textTrack.intValue != -1 { + // Auto-select first enabled subtitle if none selected + if self.playerView.srtControl.selectedSubtitleInfo == nil { + self.playerView.srtControl.selectedSubtitleInfo = self.playerView.srtControl.subtitleInfos.first { $0.isEnabled } + if let selected = self.playerView.srtControl.selectedSubtitleInfo { + print("KSPlayerView: [READY TO PLAY] Auto-selected subtitle: \(selected.name)") + } else { + print("KSPlayerView: [READY TO PLAY] No enabled subtitle found for auto-selection") + } } else { - print("KSPlayerView: [READY TO PLAY] No enabled subtitle found for auto-selection") + print("KSPlayerView: [READY TO PLAY] Subtitle already selected: \(self.playerView.srtControl.selectedSubtitleInfo?.name ?? "unknown")") } } else { - print("KSPlayerView: [READY TO PLAY] Subtitle already selected: \(self.playerView.srtControl.selectedSubtitleInfo?.name ?? "unknown")") + print("KSPlayerView: [READY TO PLAY] textTrack=-1 (disabled), skipping auto-selection") + // Ensure subtitles are disabled + self.playerView.srtControl.selectedSubtitleInfo = nil + self.playerView.subtitleLabel.isHidden = true + self.playerView.subtitleBackView.isHidden = true } } } else { diff --git a/ios/Nuvio.xcodeproj/project.pbxproj b/ios/Nuvio.xcodeproj/project.pbxproj index 3273eb7b..96f27ddc 100644 --- a/ios/Nuvio.xcodeproj/project.pbxproj +++ b/ios/Nuvio.xcodeproj/project.pbxproj @@ -11,7 +11,7 @@ 13B07FBF1A68108700A75B9A /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 13B07FB51A68108700A75B9A /* Images.xcassets */; }; 2AA769395C1242F225F875AF /* ExpoModulesProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E007C0BAC8C453623E81663 /* ExpoModulesProvider.swift */; }; 3E461D99554A48A4959DE609 /* SplashScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = AA286B85B6C04FC6940260E9 /* SplashScreen.storyboard */; }; - 564F8559E25775FFA08707DA /* libPods-Nuvio.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 18260C6F06D8D6DAF4C1D17E /* libPods-Nuvio.a */; }; + 72D4090694139E0DAD9B066E /* libPods-Nuvio.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 406CD0CEF46F8EAEEAD92BF7 /* libPods-Nuvio.a */; }; 9FBA88F42E86ECD700892850 /* KSPlayerViewManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FBA88F32E86ECD700892850 /* KSPlayerViewManager.swift */; }; 9FBA88F52E86ECD700892850 /* KSPlayerModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FBA88F12E86ECD700892850 /* KSPlayerModule.swift */; }; 9FBA88F62E86ECD700892850 /* KSPlayerManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 9FBA88F02E86ECD700892850 /* KSPlayerManager.m */; }; @@ -24,12 +24,12 @@ 13B07F961A680F5B00A75B9A /* Nuvio.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Nuvio.app; sourceTree = BUILT_PRODUCTS_DIR; }; 13B07FB51A68108700A75B9A /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Images.xcassets; path = Nuvio/Images.xcassets; sourceTree = ""; }; 13B07FB61A68108700A75B9A /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = Info.plist; path = Nuvio/Info.plist; sourceTree = ""; }; - 18260C6F06D8D6DAF4C1D17E /* libPods-Nuvio.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-Nuvio.a"; sourceTree = BUILT_PRODUCTS_DIR; }; - 2118C3C63E4B7D66EAC534DE /* Pods-Nuvio.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Nuvio.debug.xcconfig"; path = "Target Support Files/Pods-Nuvio/Pods-Nuvio.debug.xcconfig"; sourceTree = ""; }; + 15864A7148A4384BAA9F0B37 /* Pods-Nuvio.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Nuvio.release.xcconfig"; path = "Target Support Files/Pods-Nuvio/Pods-Nuvio.release.xcconfig"; sourceTree = ""; }; + 406CD0CEF46F8EAEEAD92BF7 /* libPods-Nuvio.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-Nuvio.a"; sourceTree = BUILT_PRODUCTS_DIR; }; 49055D6E250FAFA21141FE49 /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xml; name = PrivacyInfo.xcprivacy; path = Nuvio/PrivacyInfo.xcprivacy; sourceTree = ""; }; 6E007C0BAC8C453623E81663 /* ExpoModulesProvider.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ExpoModulesProvider.swift; path = "Pods/Target Support Files/Pods-Nuvio/ExpoModulesProvider.swift"; sourceTree = ""; }; 73BB213C2E9EEAC700EC03F8 /* NuvioRelease.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; name = NuvioRelease.entitlements; path = Nuvio/NuvioRelease.entitlements; sourceTree = ""; }; - 7F2FA62198C389C99926AA47 /* Pods-Nuvio.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Nuvio.release.xcconfig"; path = "Target Support Files/Pods-Nuvio/Pods-Nuvio.release.xcconfig"; sourceTree = ""; }; + 819F6DCD44DFE0C72440FDCF /* Pods-Nuvio.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Nuvio.debug.xcconfig"; path = "Target Support Files/Pods-Nuvio/Pods-Nuvio.debug.xcconfig"; sourceTree = ""; }; 9FBA88F02E86ECD700892850 /* KSPlayerManager.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KSPlayerManager.m; sourceTree = ""; }; 9FBA88F12E86ECD700892850 /* KSPlayerModule.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KSPlayerModule.swift; sourceTree = ""; }; 9FBA88F22E86ECD700892850 /* KSPlayerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KSPlayerView.swift; sourceTree = ""; }; @@ -46,7 +46,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - 564F8559E25775FFA08707DA /* libPods-Nuvio.a in Frameworks */, + 72D4090694139E0DAD9B066E /* libPods-Nuvio.a in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -76,7 +76,7 @@ isa = PBXGroup; children = ( ED297162215061F000B7C4FE /* JavaScriptCore.framework */, - 18260C6F06D8D6DAF4C1D17E /* libPods-Nuvio.a */, + 406CD0CEF46F8EAEEAD92BF7 /* libPods-Nuvio.a */, ); name = Frameworks; sourceTree = ""; @@ -131,8 +131,8 @@ D90A3959C97EE9926C513293 /* Pods */ = { isa = PBXGroup; children = ( - 2118C3C63E4B7D66EAC534DE /* Pods-Nuvio.debug.xcconfig */, - 7F2FA62198C389C99926AA47 /* Pods-Nuvio.release.xcconfig */, + 819F6DCD44DFE0C72440FDCF /* Pods-Nuvio.debug.xcconfig */, + 15864A7148A4384BAA9F0B37 /* Pods-Nuvio.release.xcconfig */, ); path = Pods; sourceTree = ""; @@ -152,15 +152,15 @@ isa = PBXNativeTarget; buildConfigurationList = 13B07F931A680F5B00A75B9A /* Build configuration list for PBXNativeTarget "Nuvio" */; buildPhases = ( - 4A10611824FCBAA4C1793637 /* [CP] Check Pods Manifest.lock */, + E060D0359630A7C0F4793812 /* [CP] Check Pods Manifest.lock */, 99A79B70155E84EE1FB7F466 /* [Expo] Configure project */, 13B07F871A680F5B00A75B9A /* Sources */, 13B07F8C1A680F5B00A75B9A /* Frameworks */, 13B07F8E1A680F5B00A75B9A /* Resources */, 00DD1BFF1BD5951E006B06BC /* Bundle React Native code and images */, 9B977D89FE30470F8C59964C /* Upload Debug Symbols to Sentry */, - EE80421364369BBCA82253B9 /* [CP] Embed Pods Frameworks */, - 778B42B39FEE5454E4D24252 /* [CP] Copy Pods Resources */, + B84E40F81DF927F34032D68B /* [CP] Embed Pods Frameworks */, + AA47AE6072D35F0490B53926 /* [CP] Copy Pods Resources */, ); buildRules = ( ); @@ -234,29 +234,45 @@ shellPath = /bin/sh; shellScript = "if [[ -f \"$PODS_ROOT/../.xcode.env\" ]]; then\n source \"$PODS_ROOT/../.xcode.env\"\nfi\nif [[ -f \"$PODS_ROOT/../.xcode.env.local\" ]]; then\n source \"$PODS_ROOT/../.xcode.env.local\"\nfi\n\n# The project root by default is one level up from the ios directory\nexport PROJECT_ROOT=\"$PROJECT_DIR\"/..\n\nif [[ \"$CONFIGURATION\" = *Debug* ]]; then\n export SKIP_BUNDLING=1\nfi\nif [[ -z \"$ENTRY_FILE\" ]]; then\n # Set the entry JS file using the bundler's entry resolution.\n export ENTRY_FILE=\"$(\"$NODE_BINARY\" -e \"require('expo/scripts/resolveAppEntry')\" \"$PROJECT_ROOT\" ios absolute | tail -n 1)\"\nfi\n\nif [[ -z \"$CLI_PATH\" ]]; then\n # Use Expo CLI\n export CLI_PATH=\"$(\"$NODE_BINARY\" --print \"require.resolve('@expo/cli', { paths: [require.resolve('expo/package.json')] })\")\"\nfi\nif [[ -z \"$BUNDLE_COMMAND\" ]]; then\n # Default Expo CLI command for bundling\n export BUNDLE_COMMAND=\"export:embed\"\nfi\n\n# Source .xcode.env.updates if it exists to allow\n# SKIP_BUNDLING to be unset if needed\nif [[ -f \"$PODS_ROOT/../.xcode.env.updates\" ]]; then\n source \"$PODS_ROOT/../.xcode.env.updates\"\nfi\n# Source local changes to allow overrides\n# if needed\nif [[ -f \"$PODS_ROOT/../.xcode.env.local\" ]]; then\n source \"$PODS_ROOT/../.xcode.env.local\"\nfi\n\n/bin/sh `\"$NODE_BINARY\" --print \"require('path').dirname(require.resolve('@sentry/react-native/package.json')) + '/scripts/sentry-xcode.sh'\"` `\"$NODE_BINARY\" --print \"require('path').dirname(require.resolve('react-native/package.json')) + '/scripts/react-native-xcode.sh'\"`\n\n"; }; - 4A10611824FCBAA4C1793637 /* [CP] Check Pods Manifest.lock */ = { + 99A79B70155E84EE1FB7F466 /* [Expo] Configure project */ = { isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; buildActionMask = 2147483647; files = ( ); inputFileListPaths = ( ); inputPaths = ( - "${PODS_PODFILE_DIR_PATH}/Podfile.lock", - "${PODS_ROOT}/Manifest.lock", + "$(SRCROOT)/.xcode.env", + "$(SRCROOT)/.xcode.env.local", + "$(SRCROOT)/Nuvio/Nuvio.entitlements", + "$(SRCROOT)/Pods/Target Support Files/Pods-Nuvio/expo-configure-project.sh", ); - name = "[CP] Check Pods Manifest.lock"; + name = "[Expo] Configure project"; outputFileListPaths = ( ); outputPaths = ( - "$(DERIVED_FILE_DIR)/Pods-Nuvio-checkManifestLockResult.txt", + "$(SRCROOT)/Pods/Target Support Files/Pods-Nuvio/ExpoModulesProvider.swift", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; - showEnvVarsInLog = 0; + shellScript = "# This script configures Expo modules and generates the modules provider file.\nbash -l -c \"./Pods/Target\\ Support\\ Files/Pods-Nuvio/expo-configure-project.sh\"\n"; }; - 778B42B39FEE5454E4D24252 /* [CP] Copy Pods Resources */ = { + 9B977D89FE30470F8C59964C /* Upload Debug Symbols to Sentry */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "Upload Debug Symbols to Sentry"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "/bin/sh `${NODE_BINARY:-node} --print \"require('path').dirname(require.resolve('@sentry/react-native/package.json')) + '/scripts/sentry-xcode-debug-files.sh'\"`"; + }; + AA47AE6072D35F0490B53926 /* [CP] Copy Pods Resources */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( @@ -368,45 +384,7 @@ shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Nuvio/Pods-Nuvio-resources.sh\"\n"; showEnvVarsInLog = 0; }; - 99A79B70155E84EE1FB7F466 /* [Expo] Configure project */ = { - isa = PBXShellScriptBuildPhase; - alwaysOutOfDate = 1; - buildActionMask = 2147483647; - files = ( - ); - inputFileListPaths = ( - ); - inputPaths = ( - "$(SRCROOT)/.xcode.env", - "$(SRCROOT)/.xcode.env.local", - "$(SRCROOT)/Nuvio/Nuvio.entitlements", - "$(SRCROOT)/Pods/Target Support Files/Pods-Nuvio/expo-configure-project.sh", - ); - name = "[Expo] Configure project"; - outputFileListPaths = ( - ); - outputPaths = ( - "$(SRCROOT)/Pods/Target Support Files/Pods-Nuvio/ExpoModulesProvider.swift", - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "# This script configures Expo modules and generates the modules provider file.\nbash -l -c \"./Pods/Target\\ Support\\ Files/Pods-Nuvio/expo-configure-project.sh\"\n"; - }; - 9B977D89FE30470F8C59964C /* Upload Debug Symbols to Sentry */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputPaths = ( - ); - name = "Upload Debug Symbols to Sentry"; - outputPaths = ( - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "/bin/sh `${NODE_BINARY:-node} --print \"require('path').dirname(require.resolve('@sentry/react-native/package.json')) + '/scripts/sentry-xcode-debug-files.sh'\"`"; - }; - EE80421364369BBCA82253B9 /* [CP] Embed Pods Frameworks */ = { + B84E40F81DF927F34032D68B /* [CP] Embed Pods Frameworks */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( @@ -428,6 +406,28 @@ shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Nuvio/Pods-Nuvio-frameworks.sh\"\n"; showEnvVarsInLog = 0; }; + E060D0359630A7C0F4793812 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-Nuvio-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; /* End PBXShellScriptBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ @@ -449,7 +449,7 @@ /* Begin XCBuildConfiguration section */ 13B07F941A680F5B00A75B9A /* Debug */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 2118C3C63E4B7D66EAC534DE /* Pods-Nuvio.debug.xcconfig */; + baseConfigurationReference = 819F6DCD44DFE0C72440FDCF /* Pods-Nuvio.debug.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; @@ -487,7 +487,7 @@ }; 13B07F951A680F5B00A75B9A /* Release */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 7F2FA62198C389C99926AA47 /* Pods-Nuvio.release.xcconfig */; + baseConfigurationReference = 15864A7148A4384BAA9F0B37 /* Pods-Nuvio.release.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; diff --git a/src/components/player/modals/SubtitleModals.tsx b/src/components/player/modals/SubtitleModals.tsx index 0655693a..f7dc4d78 100644 --- a/src/components/player/modals/SubtitleModals.tsx +++ b/src/components/player/modals/SubtitleModals.tsx @@ -33,6 +33,7 @@ interface SubtitleModalsProps { loadWyzieSubtitle: (subtitle: WyzieSubtitle) => void; selectTextTrack: (trackId: number) => void; disableCustomSubtitles: () => void; + setSubtitlesAutoSelect?: (autoSelect: boolean) => void; increaseSubtitleSize: () => void; decreaseSubtitleSize: () => void; toggleSubtitleBackground: () => void; @@ -89,6 +90,7 @@ export const SubtitleModals: React.FC = ({ subtitleOutlineWidth, setSubtitleOutlineWidth, subtitleAlign, setSubtitleAlign, subtitleBottomOffset, setSubtitleBottomOffset, subtitleLetterSpacing, setSubtitleLetterSpacing, subtitleLineHeightMultiplier, setSubtitleLineHeightMultiplier, subtitleOffsetSec, setSubtitleOffsetSec, + setSubtitlesAutoSelect, }) => { const { width, height } = useWindowDimensions(); const isIos = Platform.OS === 'ios'; @@ -157,7 +159,12 @@ export const SubtitleModals: React.FC = ({ {activeTab === 'built-in' && ( { selectTextTrack(-1); setSelectedOnlineSubtitleId(null); }} + onPress={() => { + selectTextTrack(-1); + setSelectedOnlineSubtitleId(null); + // Disable auto-select for future playback sessions + setSubtitlesAutoSelect?.(false); + }} style={{ padding: 10, borderRadius: 12, backgroundColor: selectedTextTrack === -1 ? 'white' : 'rgba(242, 184, 181)' }} > None @@ -165,7 +172,12 @@ export const SubtitleModals: React.FC = ({ {ksTextTracks.map((track) => ( { selectTextTrack(track.id); setSelectedOnlineSubtitleId(null); }} + onPress={() => { + selectTextTrack(track.id); + setSelectedOnlineSubtitleId(null); + // Enable auto-select for future playback sessions when user selects a subtitle + setSubtitlesAutoSelect?.(true); + }} style={{ padding: 10, borderRadius: 12, backgroundColor: selectedTextTrack === track.id ? 'white' : 'rgba(255,255,255,0.05)', flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center' }} > {getTrackDisplayName(track)} @@ -186,7 +198,12 @@ export const SubtitleModals: React.FC = ({ availableSubtitles.map((sub) => ( { setSelectedOnlineSubtitleId(sub.id); loadWyzieSubtitle(sub); }} + onPress={() => { + setSelectedOnlineSubtitleId(sub.id); + loadWyzieSubtitle(sub); + // Enable auto-select for future playback sessions when user selects a subtitle + setSubtitlesAutoSelect?.(true); + }} style={{ padding: 5, paddingLeft: 8, paddingRight: 10, borderRadius: 12, backgroundColor: selectedOnlineSubtitleId === sub.id ? 'white' : 'rgba(255,255,255,0.05)', flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center' }} > @@ -235,49 +252,49 @@ export const SubtitleModals: React.FC = ({ {/* Quick Presets - Hidden for ExoPlayer internal subtitles */} {!isExoPlayerInternal && ( - - - - Quick Presets + + + + Quick Presets + + + { + setSubtitleTextColor('#FFFFFF'); setSubtitleBgOpacity(0.7); setSubtitleTextShadow(true); + setSubtitleOutline(true); setSubtitleOutlineColor('#000000'); setSubtitleOutlineWidth(4); + setSubtitleAlign('center'); setSubtitleBottomOffset(10); setSubtitleLetterSpacing(0); + setSubtitleLineHeightMultiplier(1.2); + }} + style={{ paddingHorizontal: chipPadH, paddingVertical: chipPadV, borderRadius: 20, backgroundColor: 'rgba(255,255,255,0.08)', borderWidth: 1, borderColor: 'rgba(255,255,255,0.15)' }} + > + Default + + { + setSubtitleTextColor('#FFD700'); setSubtitleOutline(true); setSubtitleOutlineColor('#000000'); setSubtitleOutlineWidth(4); setSubtitleBgOpacity(0.3); setSubtitleTextShadow(false); + }} + style={{ paddingHorizontal: chipPadH, paddingVertical: chipPadV, borderRadius: 20, backgroundColor: 'rgba(255,215,0,0.12)', borderWidth: 1, borderColor: 'rgba(255,215,0,0.35)' }} + > + Yellow + + { + setSubtitleTextColor('#FFFFFF'); setSubtitleOutline(true); setSubtitleOutlineColor('#000000'); setSubtitleOutlineWidth(3); setSubtitleBgOpacity(0.0); setSubtitleTextShadow(false); setSubtitleLetterSpacing(0.5); + }} + style={{ paddingHorizontal: chipPadH, paddingVertical: chipPadV, borderRadius: 20, backgroundColor: 'rgba(34,197,94,0.12)', borderWidth: 1, borderColor: 'rgba(34,197,94,0.35)' }} + > + High Contrast + + { + setSubtitleTextColor('#FFFFFF'); setSubtitleBgOpacity(0.6); setSubtitleTextShadow(true); setSubtitleOutline(true); setSubtitleAlign('center'); setSubtitleLineHeightMultiplier(1.3); + }} + style={{ paddingHorizontal: chipPadH, paddingVertical: chipPadV, borderRadius: 20, backgroundColor: 'rgba(59,130,246,0.12)', borderWidth: 1, borderColor: 'rgba(59,130,246,0.35)' }} + > + Large + + - - { - setSubtitleTextColor('#FFFFFF'); setSubtitleBgOpacity(0.7); setSubtitleTextShadow(true); - setSubtitleOutline(true); setSubtitleOutlineColor('#000000'); setSubtitleOutlineWidth(4); - setSubtitleAlign('center'); setSubtitleBottomOffset(10); setSubtitleLetterSpacing(0); - setSubtitleLineHeightMultiplier(1.2); - }} - style={{ paddingHorizontal: chipPadH, paddingVertical: chipPadV, borderRadius: 20, backgroundColor: 'rgba(255,255,255,0.08)', borderWidth: 1, borderColor: 'rgba(255,255,255,0.15)' }} - > - Default - - { - setSubtitleTextColor('#FFD700'); setSubtitleOutline(true); setSubtitleOutlineColor('#000000'); setSubtitleOutlineWidth(4); setSubtitleBgOpacity(0.3); setSubtitleTextShadow(false); - }} - style={{ paddingHorizontal: chipPadH, paddingVertical: chipPadV, borderRadius: 20, backgroundColor: 'rgba(255,215,0,0.12)', borderWidth: 1, borderColor: 'rgba(255,215,0,0.35)' }} - > - Yellow - - { - setSubtitleTextColor('#FFFFFF'); setSubtitleOutline(true); setSubtitleOutlineColor('#000000'); setSubtitleOutlineWidth(3); setSubtitleBgOpacity(0.0); setSubtitleTextShadow(false); setSubtitleLetterSpacing(0.5); - }} - style={{ paddingHorizontal: chipPadH, paddingVertical: chipPadV, borderRadius: 20, backgroundColor: 'rgba(34,197,94,0.12)', borderWidth: 1, borderColor: 'rgba(34,197,94,0.35)' }} - > - High Contrast - - { - setSubtitleTextColor('#FFFFFF'); setSubtitleBgOpacity(0.6); setSubtitleTextShadow(true); setSubtitleOutline(true); setSubtitleAlign('center'); setSubtitleLineHeightMultiplier(1.3); - }} - style={{ paddingHorizontal: chipPadH, paddingVertical: chipPadV, borderRadius: 20, backgroundColor: 'rgba(59,130,246,0.12)', borderWidth: 1, borderColor: 'rgba(59,130,246,0.35)' }} - > - Large - - - )} {/* Core controls */} @@ -305,18 +322,18 @@ export const SubtitleModals: React.FC = ({ {/* Show Background - Not supported on ExoPlayer internal subtitles */} {!isExoPlayerInternal && ( - - - - Show Background + + + + Show Background + + + + - - - - )} @@ -328,30 +345,30 @@ export const SubtitleModals: React.FC = ({ {/* Text Color - Not supported on ExoPlayer internal subtitles */} {!isExoPlayerInternal && ( - - - - Text Color + + + + Text Color + + + {['#FFFFFF', '#FFD700', '#00E5FF', '#FF5C5C', '#00FF88', '#9b59b6', '#f97316'].map(c => ( + setSubtitleTextColor(c)} style={{ width: 22, height: 22, borderRadius: 11, backgroundColor: c, borderWidth: 2, borderColor: subtitleTextColor === c ? '#fff' : 'rgba(255,255,255,0.3)' }} /> + ))} + - - {['#FFFFFF', '#FFD700', '#00E5FF', '#FF5C5C', '#00FF88', '#9b59b6', '#f97316'].map(c => ( - setSubtitleTextColor(c)} style={{ width: 22, height: 22, borderRadius: 11, backgroundColor: c, borderWidth: 2, borderColor: subtitleTextColor === c ? '#fff' : 'rgba(255,255,255,0.3)' }} /> - ))} - - )} {/* Align - Not supported on ExoPlayer internal subtitles */} {!isExoPlayerInternal && ( - - Align - - {([{ key: 'left', icon: 'format-align-left' }, { key: 'center', icon: 'format-align-center' }, { key: 'right', icon: 'format-align-right' }] as const).map(a => ( - setSubtitleAlign(a.key)} style={{ paddingHorizontal: isCompact ? 8 : 10, paddingVertical: isCompact ? 4 : 6, borderRadius: 8, backgroundColor: subtitleAlign === a.key ? 'rgba(255,255,255,0.18)' : 'rgba(255,255,255,0.08)', borderWidth: 1, borderColor: 'rgba(255,255,255,0.15)' }}> - - - ))} + + Align + + {([{ key: 'left', icon: 'format-align-left' }, { key: 'center', icon: 'format-align-center' }, { key: 'right', icon: 'format-align-right' }] as const).map(a => ( + setSubtitleAlign(a.key)} style={{ paddingHorizontal: isCompact ? 8 : 10, paddingVertical: isCompact ? 4 : 6, borderRadius: 8, backgroundColor: subtitleAlign === a.key ? 'rgba(255,255,255,0.18)' : 'rgba(255,255,255,0.08)', borderWidth: 1, borderColor: 'rgba(255,255,255,0.15)' }}> + + + ))} + - )} Bottom Offset @@ -369,20 +386,20 @@ export const SubtitleModals: React.FC = ({ {/* Background Opacity - Not supported on ExoPlayer internal subtitles */} {!isExoPlayerInternal && ( - - Background Opacity - - setSubtitleBgOpacity(Math.max(0, +(subtitleBgOpacity - 0.1).toFixed(1)))} style={{ width: controlBtn.size, height: controlBtn.size, borderRadius: controlBtn.radius, backgroundColor: 'rgba(255,255,255,0.18)', alignItems: 'center', justifyContent: 'center' }}> - - - - {subtitleBgOpacity.toFixed(1)} + + Background Opacity + + setSubtitleBgOpacity(Math.max(0, +(subtitleBgOpacity - 0.1).toFixed(1)))} style={{ width: controlBtn.size, height: controlBtn.size, borderRadius: controlBtn.radius, backgroundColor: 'rgba(255,255,255,0.18)', alignItems: 'center', justifyContent: 'center' }}> + + + + {subtitleBgOpacity.toFixed(1)} + + setSubtitleBgOpacity(Math.min(1, +(subtitleBgOpacity + 0.1).toFixed(1)))} style={{ width: controlBtn.size, height: controlBtn.size, borderRadius: controlBtn.radius, backgroundColor: 'rgba(255,255,255,0.18)', alignItems: 'center', justifyContent: 'center' }}> + + - setSubtitleBgOpacity(Math.min(1, +(subtitleBgOpacity + 0.1).toFixed(1)))} style={{ width: controlBtn.size, height: controlBtn.size, borderRadius: controlBtn.radius, backgroundColor: 'rgba(255,255,255,0.18)', alignItems: 'center', justifyContent: 'center' }}> - - - )} {!isUsingInternalSubtitle && ( @@ -452,23 +469,23 @@ export const SubtitleModals: React.FC = ({ )} {/* Timing Offset - Not supported on ExoPlayer internal subtitles */} {!isExoPlayerInternal && ( - - - Timing Offset (s) - - setSubtitleOffsetSec(+(subtitleOffsetSec - 0.1).toFixed(1))} style={{ width: controlBtn.size, height: controlBtn.size, borderRadius: controlBtn.radius, backgroundColor: 'rgba(255,255,255,0.18)', alignItems: 'center', justifyContent: 'center' }}> - - - - {subtitleOffsetSec.toFixed(1)} + + + Timing Offset (s) + + setSubtitleOffsetSec(+(subtitleOffsetSec - 0.1).toFixed(1))} style={{ width: controlBtn.size, height: controlBtn.size, borderRadius: controlBtn.radius, backgroundColor: 'rgba(255,255,255,0.18)', alignItems: 'center', justifyContent: 'center' }}> + + + + {subtitleOffsetSec.toFixed(1)} + + setSubtitleOffsetSec(+(subtitleOffsetSec + 0.1).toFixed(1))} style={{ width: controlBtn.size, height: controlBtn.size, borderRadius: controlBtn.radius, backgroundColor: 'rgba(255,255,255,0.18)', alignItems: 'center', justifyContent: 'center' }}> + + - setSubtitleOffsetSec(+(subtitleOffsetSec + 0.1).toFixed(1))} style={{ width: controlBtn.size, height: controlBtn.size, borderRadius: controlBtn.radius, backgroundColor: 'rgba(255,255,255,0.18)', alignItems: 'center', justifyContent: 'center' }}> - - + Nudge subtitles earlier (-) or later (+) to sync if needed. - Nudge subtitles earlier (-) or later (+) to sync if needed. - )} 0) { @@ -908,8 +909,9 @@ export const useMetadata = ({ id, type, addonId }: UseMetadataProps): UseMetadat // Centralized logo fetching logic try { - if (settings.enrichMetadataWithTMDB) { - // Only use TMDB logos when enrichment is ON + // Check both master switch AND granular logos setting + if (settings.enrichMetadataWithTMDB && settings.tmdbEnrichLogos) { + // Only use TMDB logos when both enrichment AND logos option are ON const tmdbService = TMDBService.getInstance(); const preferredLanguage = settings.tmdbLanguagePreference || 'en'; const contentType = type === 'series' ? 'tv' : 'movie'; @@ -940,12 +942,13 @@ export const useMetadata = ({ id, type, addonId }: UseMetadataProps): UseMetadat if (__DEV__) console.log('[useMetadata] No TMDB ID found for logo, will show text title'); } } else { - // When enrichment is OFF, keep addon logo or undefined + // When enrichment or logos is OFF, keep addon logo or undefined finalMetadata.logo = finalMetadata.logo || undefined; if (__DEV__) { - console.log('[useMetadata] TMDB enrichment disabled, using addon logo:', { + console.log('[useMetadata] TMDB logo enrichment disabled, using addon logo:', { hasAddonLogo: !!finalMetadata.logo, - enrichmentEnabled: false + enrichmentEnabled: settings.enrichMetadataWithTMDB, + logosEnabled: settings.tmdbEnrichLogos }); } } @@ -961,8 +964,8 @@ export const useMetadata = ({ id, type, addonId }: UseMetadataProps): UseMetadat (finalMetadata as any).addonLogo = addonLogo; } - // Clear banner field if TMDB enrichment is enabled to prevent flash - if (settings.enrichMetadataWithTMDB) { + // Clear banner field if TMDB banner enrichment is enabled to prevent flash + if (settings.enrichMetadataWithTMDB && settings.tmdbEnrichBanners) { finalMetadata = { ...finalMetadata, banner: undefined, // Let useMetadataAssets handle banner via TMDB @@ -1114,8 +1117,8 @@ export const useMetadata = ({ id, type, addonId }: UseMetadataProps): UseMetadat if (__DEV__) logger.log(`📺 Processed addon episodes into ${Object.keys(groupedAddonEpisodes).length} seasons`); - // Fetch season posters from TMDB only if enrichment is enabled; otherwise skip quietly - if (settings.enrichMetadataWithTMDB) { + // Fetch season posters from TMDB only if enrichment AND season posters are enabled + if (settings.enrichMetadataWithTMDB && settings.tmdbEnrichSeasonPosters) { try { const tmdbIdToUse = tmdbId || (id.startsWith('tt') ? await tmdbService.findTMDBIdByIMDB(id) : null); if (tmdbIdToUse) { @@ -1140,11 +1143,11 @@ export const useMetadata = ({ id, type, addonId }: UseMetadataProps): UseMetadat logger.error('Failed to fetch TMDB season posters for addon episodes:', error); } } else { - if (__DEV__) logger.log('[loadSeriesData] TMDB enrichment disabled; skipping season poster fetch'); + if (__DEV__) logger.log('[loadSeriesData] TMDB season poster enrichment disabled; skipping season poster fetch'); } - // If localized TMDB text is enabled, merge episode names/overviews per language - if (settings.enrichMetadataWithTMDB && settings.useTmdbLocalizedMetadata) { + // If localized TMDB text is enabled AND episode enrichment is enabled, merge episode names/overviews per language + if (settings.enrichMetadataWithTMDB && settings.tmdbEnrichEpisodes && settings.useTmdbLocalizedMetadata) { try { const tmdbIdToUse = tmdbId || (id.startsWith('tt') ? await tmdbService.findTMDBIdByIMDB(id) : null); if (tmdbIdToUse) { @@ -1241,8 +1244,9 @@ export const useMetadata = ({ id, type, addonId }: UseMetadataProps): UseMetadat } // Try to get TMDB ID for additional metadata (cast, etc.) but don't override episodes - if (!settings.enrichMetadataWithTMDB) { - if (__DEV__) logger.log('[loadSeriesData] TMDB enrichment disabled; skipping TMDB episode fallback (preserving current episodes)'); + // Skip TMDB episode fallback if enrichment or episode enrichment is disabled + if (!settings.enrichMetadataWithTMDB || !settings.tmdbEnrichEpisodes) { + if (__DEV__) logger.log('[loadSeriesData] TMDB episode enrichment disabled; skipping TMDB episode fallback (preserving current episodes)'); return; } const tmdbIdResult = await tmdbService.findTMDBIdByIMDB(id); @@ -2053,7 +2057,7 @@ export const useMetadata = ({ id, type, addonId }: UseMetadataProps): UseMetadat useEffect(() => { const fetchTmdbIdAndRecommendations = async () => { if (!settings.enrichMetadataWithTMDB) { - if (__DEV__) console.log('[useMetadata] enrichment disabled; skip TMDB id extraction and certification (extract path)'); + if (__DEV__) console.log('[useMetadata] enrichment disabled; skip TMDB id extraction (extract path)'); return; } if (metadata && !tmdbId) { @@ -2063,17 +2067,22 @@ export const useMetadata = ({ id, type, addonId }: UseMetadataProps): UseMetadat if (fetchedTmdbId) { if (__DEV__) console.log('[useMetadata] extracted TMDB id from content id', { id, fetchedTmdbId }); setTmdbId(fetchedTmdbId); - // Fetch certification - const certification = await tmdbService.getCertification(type, fetchedTmdbId); - if (certification) { - if (__DEV__) console.log('[useMetadata] fetched certification via TMDB id (extract path)', { type, fetchedTmdbId, certification }); - setMetadata(prev => prev ? { - ...prev, - tmdbId: fetchedTmdbId, - certification - } : null); + // Fetch certification only if granular setting is enabled + if (settings.tmdbEnrichCertification) { + const certification = await tmdbService.getCertification(type, fetchedTmdbId); + if (certification) { + if (__DEV__) console.log('[useMetadata] fetched certification via TMDB id (extract path)', { type, fetchedTmdbId, certification }); + setMetadata(prev => prev ? { + ...prev, + tmdbId: fetchedTmdbId, + certification + } : null); + } else { + if (__DEV__) console.warn('[useMetadata] certification not returned from TMDB (extract path)', { type, fetchedTmdbId }); + } } else { - if (__DEV__) console.warn('[useMetadata] certification not returned from TMDB (extract path)', { type, fetchedTmdbId }); + // Just set the TMDB ID without certification + setMetadata(prev => prev ? { ...prev, tmdbId: fetchedTmdbId } : null); } } else { if (__DEV__) console.warn('[useMetadata] Could not determine TMDB ID for recommendations / certification', { id }); @@ -2089,8 +2098,9 @@ export const useMetadata = ({ id, type, addonId }: UseMetadataProps): UseMetadat useEffect(() => { if (tmdbId) { - if (settings.enrichMetadataWithTMDB) { - if (__DEV__) console.log('[useMetadata] tmdbId available; loading recommendations and enabling certification checks', { tmdbId }); + // Check both master switch AND granular recommendations setting + if (settings.enrichMetadataWithTMDB && settings.tmdbEnrichRecommendations) { + if (__DEV__) console.log('[useMetadata] tmdbId available; loading recommendations', { tmdbId }); loadRecommendations(); } // Reset recommendations when tmdbId changes @@ -2099,21 +2109,23 @@ export const useMetadata = ({ id, type, addonId }: UseMetadataProps): UseMetadat setLoadingRecommendations(true); }; } - }, [tmdbId, loadRecommendations, settings.enrichMetadataWithTMDB]); + }, [tmdbId, loadRecommendations, settings.enrichMetadataWithTMDB, settings.tmdbEnrichRecommendations]); - // Load addon cast data when metadata is available and TMDB enrichment is disabled + // Load addon cast data when metadata is available and TMDB cast enrichment is disabled useEffect(() => { - if (!settings.enrichMetadataWithTMDB && metadata?.addonCast && metadata.addonCast.length > 0) { + // Load addon cast if master switch is off OR if cast enrichment specifically is off + if ((!settings.enrichMetadataWithTMDB || !settings.tmdbEnrichCast) && metadata?.addonCast && metadata.addonCast.length > 0) { if (__DEV__) logger.log('[useMetadata] Loading addon cast data after metadata loaded'); loadCast(); } - }, [metadata, settings.enrichMetadataWithTMDB]); + }, [metadata, settings.enrichMetadataWithTMDB, settings.tmdbEnrichCast]); // Ensure certification is attached whenever a TMDB id is known and metadata lacks it useEffect(() => { const maybeAttachCertification = async () => { - if (!settings.enrichMetadataWithTMDB) { - if (__DEV__) console.log('[useMetadata] enrichment disabled; skip certification (attach path)'); + // Check both master switch AND granular certification setting + if (!settings.enrichMetadataWithTMDB || !settings.tmdbEnrichCertification) { + if (__DEV__) console.log('[useMetadata] certification enrichment disabled; skip (attach path)'); return; } try { @@ -2142,12 +2154,18 @@ export const useMetadata = ({ id, type, addonId }: UseMetadataProps): UseMetadat } }; maybeAttachCertification(); - }, [tmdbId, metadata, type, settings.enrichMetadataWithTMDB]); + }, [tmdbId, metadata, type, settings.enrichMetadataWithTMDB, settings.tmdbEnrichCertification]); // Fetch TMDB networks/production companies when TMDB ID is available and enrichment is enabled const productionInfoFetchedRef = useRef(null); useEffect(() => { - if (!tmdbId || !settings.enrichMetadataWithTMDB || !metadata) { + // Check if any of the relevant granular settings are enabled + const anyProductionEnrichmentEnabled = settings.tmdbEnrichProductionInfo || + settings.tmdbEnrichTvDetails || + settings.tmdbEnrichMovieDetails || + settings.tmdbEnrichCollections; + + if (!tmdbId || !settings.enrichMetadataWithTMDB || !metadata || !anyProductionEnrichmentEnabled) { return; } @@ -2175,7 +2193,11 @@ export const useMetadata = ({ id, type, addonId }: UseMetadataProps): UseMetadat tmdbId, useLocalized: settings.useTmdbLocalizedMetadata, lang: settings.useTmdbLocalizedMetadata ? (settings.tmdbLanguagePreference || 'en') : 'en', - hasExistingNetworks: !!(metadata as any).networks + hasExistingNetworks: !!(metadata as any).networks, + productionInfoEnabled: settings.tmdbEnrichProductionInfo, + tvDetailsEnabled: settings.tmdbEnrichTvDetails, + movieDetailsEnabled: settings.tmdbEnrichMovieDetails, + collectionsEnabled: settings.tmdbEnrichCollections }); if (type === 'series') { @@ -2187,8 +2209,8 @@ export const useMetadata = ({ id, type, addonId }: UseMetadataProps): UseMetadat hasNetworks: !!showDetails.networks, networksCount: showDetails.networks?.length || 0 }); - // Fetch networks - if (showDetails.networks) { + // Fetch networks only if production info is enabled + if (settings.tmdbEnrichProductionInfo && showDetails.networks) { productionInfo = Array.isArray(showDetails.networks) ? showDetails.networks .map((n: any) => ({ @@ -2200,30 +2222,32 @@ export const useMetadata = ({ id, type, addonId }: UseMetadataProps): UseMetadat : []; } - // Fetch additional TV details - const tvDetails = { - status: showDetails.status, - firstAirDate: showDetails.first_air_date, - lastAirDate: showDetails.last_air_date, - numberOfSeasons: showDetails.number_of_seasons, - numberOfEpisodes: showDetails.number_of_episodes, - episodeRunTime: showDetails.episode_run_time, - type: showDetails.type, - originCountry: showDetails.origin_country, - originalLanguage: showDetails.original_language, - createdBy: showDetails.created_by?.map(creator => ({ - id: creator.id, - name: creator.name, - profile_path: creator.profile_path || undefined - })), - }; + // Fetch additional TV details only if TV details is enabled + if (settings.tmdbEnrichTvDetails) { + const tvDetails = { + status: showDetails.status, + firstAirDate: showDetails.first_air_date, + lastAirDate: showDetails.last_air_date, + numberOfSeasons: showDetails.number_of_seasons, + numberOfEpisodes: showDetails.number_of_episodes, + episodeRunTime: showDetails.episode_run_time, + type: showDetails.type, + originCountry: showDetails.origin_country, + originalLanguage: showDetails.original_language, + createdBy: showDetails.created_by?.map(creator => ({ + id: creator.id, + name: creator.name, + profile_path: creator.profile_path || undefined + })), + }; - // Update metadata with TV details - setMetadata((prev: any) => ({ - ...prev, - tmdbId, - tvDetails - })); + // Update metadata with TV details + setMetadata((prev: any) => ({ + ...prev, + tmdbId, + tvDetails + })); + } } } else if (type === 'movie') { // Fetch production companies and additional details for movies @@ -2234,8 +2258,8 @@ export const useMetadata = ({ id, type, addonId }: UseMetadataProps): UseMetadat hasProductionCompanies: !!movieDetails.production_companies, productionCompaniesCount: movieDetails.production_companies?.length || 0 }); - // Fetch production companies - if (movieDetails.production_companies) { + // Fetch production companies only if production info is enabled + if (settings.tmdbEnrichProductionInfo && movieDetails.production_companies) { productionInfo = Array.isArray(movieDetails.production_companies) ? movieDetails.production_companies .map((c: any) => ({ @@ -2247,27 +2271,29 @@ export const useMetadata = ({ id, type, addonId }: UseMetadataProps): UseMetadat : []; } - // Fetch additional movie details - const movieDetailsObj = { - status: movieDetails.status, - releaseDate: movieDetails.release_date, - runtime: movieDetails.runtime, - budget: movieDetails.budget, - revenue: movieDetails.revenue, - originalLanguage: movieDetails.original_language, - originCountry: movieDetails.production_countries?.map((c: any) => c.iso_3166_1), - tagline: movieDetails.tagline, - }; + // Fetch additional movie details only if movie details is enabled + if (settings.tmdbEnrichMovieDetails) { + const movieDetailsObj = { + status: movieDetails.status, + releaseDate: movieDetails.release_date, + runtime: movieDetails.runtime, + budget: movieDetails.budget, + revenue: movieDetails.revenue, + originalLanguage: movieDetails.original_language, + originCountry: movieDetails.production_countries?.map((c: any) => c.iso_3166_1), + tagline: movieDetails.tagline, + }; - // Update metadata with movie details - setMetadata((prev: any) => ({ - ...prev, - tmdbId, - movieDetails: movieDetailsObj - })); + // Update metadata with movie details + setMetadata((prev: any) => ({ + ...prev, + tmdbId, + movieDetails: movieDetailsObj + })); + } - // Fetch collection data if movie belongs to a collection - if (movieDetails.belongs_to_collection) { + // Fetch collection data if movie belongs to a collection AND collections is enabled + if (settings.tmdbEnrichCollections && movieDetails.belongs_to_collection) { setLoadingCollection(true); try { const collectionDetails = await tmdbService.getCollectionDetails( @@ -2365,7 +2391,7 @@ export const useMetadata = ({ id, type, addonId }: UseMetadataProps): UseMetadat }; fetchProductionInfo(); - }, [tmdbId, settings.enrichMetadataWithTMDB, metadata, type]); + }, [tmdbId, settings.enrichMetadataWithTMDB, metadata, type, settings.tmdbEnrichProductionInfo, settings.tmdbEnrichTvDetails, settings.tmdbEnrichMovieDetails, settings.tmdbEnrichCollections]); // Reset tmdbId when id changes useEffect(() => { diff --git a/src/hooks/useMetadataAssets.ts b/src/hooks/useMetadataAssets.ts index 694ddefa..8d80f1d9 100644 --- a/src/hooks/useMetadataAssets.ts +++ b/src/hooks/useMetadataAssets.ts @@ -14,7 +14,7 @@ const checkImageAvailability = async (url: string): Promise => { if (imageAvailabilityCache[url] !== undefined) { return imageAvailabilityCache[url]; } - + // Check AsyncStorage cache try { const cachedResult = await mmkvStorage.getItem(`image_available:${url}`); @@ -31,7 +31,7 @@ const checkImageAvailability = async (url: string): Promise => { try { const response = await fetch(url, { method: 'HEAD' }); const isAvailable = response.ok; - + // Update caches imageAvailabilityCache[url] = isAvailable; try { @@ -39,7 +39,7 @@ const checkImageAvailability = async (url: string): Promise => { } catch (error) { // Ignore AsyncStorage errors } - + return isAvailable; } catch (error) { return false; @@ -47,9 +47,9 @@ const checkImageAvailability = async (url: string): Promise => { }; export const useMetadataAssets = ( - metadata: any, - id: string, - type: string, + metadata: any, + id: string, + type: string, imdbId: string | null, settings: any, setMetadata: (metadata: any) => void @@ -58,22 +58,22 @@ export const useMetadataAssets = ( const [bannerImage, setBannerImage] = useState(null); const [loadingBanner, setLoadingBanner] = useState(false); const forcedBannerRefreshDone = useRef(false); - + // Add source tracking to prevent mixing sources const [bannerSource, setBannerSource] = useState<'tmdb' | 'metahub' | 'default' | null>(null); - + // For TMDB ID tracking const [foundTmdbId, setFoundTmdbId] = useState(null); - - + + const isMountedRef = useRef(true); - + // CRITICAL: AbortController to cancel in-flight requests when component unmounts const abortControllerRef = useRef(new AbortController()); - + // Track pending requests to prevent duplicate concurrent API calls const pendingFetchRef = useRef | null>(null); - + // Cleanup on unmount useEffect(() => { return () => { @@ -82,12 +82,12 @@ export const useMetadataAssets = ( abortControllerRef.current.abort(); }; }, []); - - + + useEffect(() => { abortControllerRef.current = new AbortController(); }, [id, type]); - + // Force reset when preference changes useEffect(() => { // Reset all cached data when preference changes @@ -101,7 +101,7 @@ export const useMetadataAssets = ( // Optimized banner fetching with race condition fixes const fetchBanner = useCallback(async () => { if (!metadata || !isMountedRef.current) return; - + // Prevent concurrent fetch requests for the same metadata if (pendingFetchRef.current) { try { @@ -110,18 +110,18 @@ export const useMetadataAssets = ( // Previous request failed, allow new attempt } } - + // Create a promise to track this fetch operation const fetchPromise = (async () => { try { if (!isMountedRef.current) return; - + if (isMountedRef.current) { setLoadingBanner(true); } - - // If enrichment is disabled, use addon banner and don't fetch from external sources - if (!settings.enrichMetadataWithTMDB) { + + // If enrichment or banner enrichment is disabled, use addon banner and don't fetch from external sources + if (!settings.enrichMetadataWithTMDB || !settings.tmdbEnrichBanners) { const addonBanner = metadata?.banner || null; if (isMountedRef.current && addonBanner && addonBanner !== bannerImage) { setBannerImage(addonBanner); @@ -132,15 +132,15 @@ export const useMetadataAssets = ( } return; } - + try { const currentPreference = settings.logoSourcePreference || 'tmdb'; const contentType = type === 'series' ? 'tv' : 'movie'; - + // Collect final state before updating to prevent intermediate null states let finalBanner: string | null = bannerImage; // Start with current to prevent flicker let bannerSourceType: 'tmdb' | 'default' = (bannerSource === 'tmdb' || bannerSource === 'default') ? bannerSource : 'default'; - + // TMDB path only if (currentPreference === 'tmdb') { let tmdbId = null; @@ -163,24 +163,24 @@ export const useMetadataAssets = ( logger.debug('[useMetadataAssets] TMDB ID lookup failed:', error); } } - + if (tmdbId && isMountedRef.current) { try { const tmdbService = TMDBService.getInstance(); const endpoint = contentType === 'tv' ? 'tv' : 'movie'; - + // Fetch details (AbortSignal will be used for future implementations) - const details = endpoint === 'movie' - ? await tmdbService.getMovieDetails(tmdbId) + const details = endpoint === 'movie' + ? await tmdbService.getMovieDetails(tmdbId) : await tmdbService.getTVShowDetails(Number(tmdbId)); - + // Only update if request wasn't aborted and component is still mounted if (!isMountedRef.current) return; - + if (details?.backdrop_path) { finalBanner = tmdbService.getImageUrl(details.backdrop_path); bannerSourceType = 'tmdb'; - + // Preload the image if (finalBanner) { FastImage.preload([{ uri: finalBanner }]); @@ -196,10 +196,10 @@ export const useMetadataAssets = ( // Request was cancelled, don't update state return; } - + // Only update state if still mounted after error if (!isMountedRef.current) return; - + logger.debug('[useMetadataAssets] TMDB details fetch failed:', error); // Keep current banner on error instead of setting to null finalBanner = bannerImage || metadata?.banner || null; @@ -207,27 +207,27 @@ export const useMetadataAssets = ( } } } - + // Final fallback to metadata banner only if (!finalBanner) { finalBanner = metadata?.banner || null; bannerSourceType = 'default'; } - + // CRITICAL: Batch all state updates into a single call to prevent race conditions // This ensures the native view hierarchy doesn't receive conflicting unmount/remount signals if (isMountedRef.current && (finalBanner !== bannerImage || bannerSourceType !== bannerSource)) { setBannerImage(finalBanner); setBannerSource(bannerSourceType); } - + if (isMountedRef.current) { forcedBannerRefreshDone.current = true; } } catch (error) { // Outer catch for any unexpected errors if (!isMountedRef.current) return; - + logger.error('[useMetadataAssets] Unexpected error in banner fetch:', error); // Use current banner on error, don't set to null const defaultBanner = bannerImage || metadata?.banner || null; @@ -244,17 +244,17 @@ export const useMetadataAssets = ( pendingFetchRef.current = null; } })(); - + pendingFetchRef.current = fetchPromise; return fetchPromise; - }, [metadata, id, type, imdbId, settings.logoSourcePreference, settings.tmdbLanguagePreference, settings.enrichMetadataWithTMDB, foundTmdbId, bannerImage, bannerSource]); + }, [metadata, id, type, imdbId, settings.logoSourcePreference, settings.tmdbLanguagePreference, settings.enrichMetadataWithTMDB, settings.tmdbEnrichBanners, foundTmdbId, bannerImage, bannerSource]); // Fetch banner when needed useEffect(() => { if (!isMountedRef.current) return; - + const currentPreference = settings.logoSourcePreference || 'tmdb'; - + if (bannerSource !== currentPreference && !forcedBannerRefreshDone.current) { fetchBanner(); } @@ -267,6 +267,6 @@ export const useMetadataAssets = ( setBannerImage, bannerSource, logoLoadError: false, - setLogoLoadError: () => {}, + setLogoLoadError: () => { }, }; }; \ No newline at end of file diff --git a/src/hooks/useSettings.ts b/src/hooks/useSettings.ts index bfc1af7c..0b89113f 100644 --- a/src/hooks/useSettings.ts +++ b/src/hooks/useSettings.ts @@ -78,8 +78,20 @@ export interface AppSettings { // AI aiChatEnabled: boolean; // Enable/disable Ask AI and AI features // Metadata enrichment - enrichMetadataWithTMDB: boolean; // Use TMDB to enrich metadata (cast, certification, posters, fallbacks) + enrichMetadataWithTMDB: boolean; // Master switch - use TMDB to enrich metadata useTmdbLocalizedMetadata: boolean; // Use TMDB localized metadata (titles, overviews) per tmdbLanguagePreference + // Granular TMDB enrichment controls (only apply when enrichMetadataWithTMDB is true) + tmdbEnrichCast: boolean; // Use TMDB cast data (actors, directors, crew) + tmdbEnrichLogos: boolean; // Use TMDB title logos + tmdbEnrichBanners: boolean; // Use TMDB backdrop/banner images + tmdbEnrichCertification: boolean; // Show TMDB content certification (PG-13, R, etc.) + tmdbEnrichRecommendations: boolean; // Show TMDB recommendations + tmdbEnrichEpisodes: boolean; // Use TMDB episode data (thumbnails, info, fallbacks) + tmdbEnrichSeasonPosters: boolean; // Use TMDB season posters + tmdbEnrichProductionInfo: boolean; // Show networks/production companies with logos + tmdbEnrichMovieDetails: boolean; // Show movie details (budget, revenue, tagline, etc.) + tmdbEnrichTvDetails: boolean; // Show TV details (status, seasons count, networks, etc.) + tmdbEnrichCollections: boolean; // Show movie collections/franchises // Trakt integration showTraktComments: boolean; // Show Trakt comments in metadata screens // Continue Watching behavior @@ -147,6 +159,18 @@ export const DEFAULT_SETTINGS: AppSettings = { // Metadata enrichment enrichMetadataWithTMDB: true, useTmdbLocalizedMetadata: false, + // Granular TMDB enrichment controls (all enabled by default for backward compatibility) + tmdbEnrichCast: true, + tmdbEnrichLogos: true, + tmdbEnrichBanners: true, + tmdbEnrichCertification: true, + tmdbEnrichRecommendations: true, + tmdbEnrichEpisodes: true, + tmdbEnrichSeasonPosters: true, + tmdbEnrichProductionInfo: true, + tmdbEnrichMovieDetails: true, + tmdbEnrichTvDetails: true, + tmdbEnrichCollections: true, // Trakt integration showTraktComments: true, // Show Trakt comments by default when authenticated // Continue Watching behavior diff --git a/src/screens/SettingsScreen.tsx b/src/screens/SettingsScreen.tsx index afbfe55f..b54612f0 100644 --- a/src/screens/SettingsScreen.tsx +++ b/src/screens/SettingsScreen.tsx @@ -1310,7 +1310,7 @@ const styles = StyleSheet.create({ flexGrow: 1, width: '100%', paddingTop: 8, - paddingBottom: 100, + paddingBottom: 32, }, // Tablet-specific styles diff --git a/src/screens/TMDBSettingsScreen.tsx b/src/screens/TMDBSettingsScreen.tsx index bef2ad13..62c21c15 100644 --- a/src/screens/TMDBSettingsScreen.tsx +++ b/src/screens/TMDBSettingsScreen.tsx @@ -651,6 +651,201 @@ const TMDBSettingsScreen = () => { )} + + {/* Granular Enrichment Options */} + + + Enrichment Options + + Control which data is fetched from TMDb. Disabled options will use addon data if available. + + + {/* Cast & Crew */} + + + Cast & Crew + + Actors, directors, writers with profile photos + + + updateSetting('tmdbEnrichCast', v)} + trackColor={{ false: 'rgba(255,255,255,0.1)', true: currentTheme.colors.primary }} + thumbColor={Platform.OS === 'android' ? currentTheme.colors.white : ''} + ios_backgroundColor={'rgba(255,255,255,0.1)'} + /> + + + {/* Title Logos */} + + + Title Logos + + High-quality title treatment images + + + updateSetting('tmdbEnrichLogos', v)} + trackColor={{ false: 'rgba(255,255,255,0.1)', true: currentTheme.colors.primary }} + thumbColor={Platform.OS === 'android' ? currentTheme.colors.white : ''} + ios_backgroundColor={'rgba(255,255,255,0.1)'} + /> + + + {/* Banners/Backdrops */} + + + Banners & Backdrops + + High-resolution backdrop images + + + updateSetting('tmdbEnrichBanners', v)} + trackColor={{ false: 'rgba(255,255,255,0.1)', true: currentTheme.colors.primary }} + thumbColor={Platform.OS === 'android' ? currentTheme.colors.white : ''} + ios_backgroundColor={'rgba(255,255,255,0.1)'} + /> + + + {/* Certification */} + + + Content Certification + + Age ratings (PG-13, R, TV-MA, etc.) + + + updateSetting('tmdbEnrichCertification', v)} + trackColor={{ false: 'rgba(255,255,255,0.1)', true: currentTheme.colors.primary }} + thumbColor={Platform.OS === 'android' ? currentTheme.colors.white : ''} + ios_backgroundColor={'rgba(255,255,255,0.1)'} + /> + + + {/* Recommendations */} + + + Recommendations + + Similar content suggestions + + + updateSetting('tmdbEnrichRecommendations', v)} + trackColor={{ false: 'rgba(255,255,255,0.1)', true: currentTheme.colors.primary }} + thumbColor={Platform.OS === 'android' ? currentTheme.colors.white : ''} + ios_backgroundColor={'rgba(255,255,255,0.1)'} + /> + + + {/* Episode Data */} + + + Episode Data + + Episode thumbnails, info & fallbacks for TV shows + + + updateSetting('tmdbEnrichEpisodes', v)} + trackColor={{ false: 'rgba(255,255,255,0.1)', true: currentTheme.colors.primary }} + thumbColor={Platform.OS === 'android' ? currentTheme.colors.white : ''} + ios_backgroundColor={'rgba(255,255,255,0.1)'} + /> + + + {/* Season Posters */} + + + Season Posters + + Season-specific poster images + + + updateSetting('tmdbEnrichSeasonPosters', v)} + trackColor={{ false: 'rgba(255,255,255,0.1)', true: currentTheme.colors.primary }} + thumbColor={Platform.OS === 'android' ? currentTheme.colors.white : ''} + ios_backgroundColor={'rgba(255,255,255,0.1)'} + /> + + + {/* Production Info */} + + + Production Info + + Networks & production companies with logos + + + updateSetting('tmdbEnrichProductionInfo', v)} + trackColor={{ false: 'rgba(255,255,255,0.1)', true: currentTheme.colors.primary }} + thumbColor={Platform.OS === 'android' ? currentTheme.colors.white : ''} + ios_backgroundColor={'rgba(255,255,255,0.1)'} + /> + + + {/* Movie Details */} + + + Movie Details + + Budget, revenue, runtime, tagline + + + updateSetting('tmdbEnrichMovieDetails', v)} + trackColor={{ false: 'rgba(255,255,255,0.1)', true: currentTheme.colors.primary }} + thumbColor={Platform.OS === 'android' ? currentTheme.colors.white : ''} + ios_backgroundColor={'rgba(255,255,255,0.1)'} + /> + + + {/* TV Details */} + + + TV Show Details + + Status, seasons count, networks, creators + + + updateSetting('tmdbEnrichTvDetails', v)} + trackColor={{ false: 'rgba(255,255,255,0.1)', true: currentTheme.colors.primary }} + thumbColor={Platform.OS === 'android' ? currentTheme.colors.white : ''} + ios_backgroundColor={'rgba(255,255,255,0.1)'} + /> + + + {/* Collections */} + + + Movie Collections + + Franchise movies (Marvel, Star Wars, etc.) + + + updateSetting('tmdbEnrichCollections', v)} + trackColor={{ false: 'rgba(255,255,255,0.1)', true: currentTheme.colors.primary }} + thumbColor={Platform.OS === 'android' ? currentTheme.colors.white : ''} + ios_backgroundColor={'rgba(255,255,255,0.1)'} + /> + )}