diff --git a/ios/Nuvio.xcodeproj/project.pbxproj b/ios/Nuvio.xcodeproj/project.pbxproj index a248d5aa..0f31caf4 100644 --- a/ios/Nuvio.xcodeproj/project.pbxproj +++ b/ios/Nuvio.xcodeproj/project.pbxproj @@ -467,7 +467,7 @@ ); OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_DEBUG"; PRODUCT_BUNDLE_IDENTIFIER = com.nuvio.app; - PRODUCT_NAME = "Nuvio"; + PRODUCT_NAME = Nuvio; SWIFT_OBJC_BRIDGING_HEADER = "Nuvio/Nuvio-Bridging-Header.h"; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 5.0; @@ -501,7 +501,7 @@ ); OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE"; PRODUCT_BUNDLE_IDENTIFIER = com.nuvio.app; - PRODUCT_NAME = "Nuvio"; + PRODUCT_NAME = Nuvio; SWIFT_OBJC_BRIDGING_HEADER = "Nuvio/Nuvio-Bridging-Header.h"; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; diff --git a/ios/Nuvio/Info.plist b/ios/Nuvio/Info.plist index 9e74c907..b8a4ac16 100644 --- a/ios/Nuvio/Info.plist +++ b/ios/Nuvio/Info.plist @@ -1,99 +1,95 @@ - - CADisableMinimumFrameDurationOnPhone - - CFBundleDevelopmentRegion - $(DEVELOPMENT_LANGUAGE) - CFBundleDisplayName - Nuvio - CFBundleExecutable - $(EXECUTABLE_NAME) - CFBundleIdentifier - $(PRODUCT_BUNDLE_IDENTIFIER) - CFBundleInfoDictionaryVersion - 6.0 - CFBundleName - $(PRODUCT_NAME) - CFBundlePackageType - $(PRODUCT_BUNDLE_PACKAGE_TYPE) - CFBundleShortVersionString - 1.2.3 - CFBundleSignature - ???? - CFBundleURLTypes - - - CFBundleURLSchemes - - nuvio - com.nuvio.app - - - - CFBundleURLSchemes - - exp+nuvio - - - - CFBundleVersion - 18 - LSMinimumSystemVersion - 12.0 - LSRequiresIPhoneOS - - LSSupportsOpeningDocumentsInPlace - - NSAppTransportSecurity - - NSAllowsArbitraryLoads - - - NSBonjourServices - - _http._tcp - - NSLocalNetworkUsageDescription - Allow $(PRODUCT_NAME) to access your local network - NSMicrophoneUsageDescription - This app does not require microphone access. - RCTRootViewBackgroundColor - 4278322180 - UIBackgroundModes - - audio - - UIFileSharingEnabled - - UILaunchStoryboardName - SplashScreen - UIRequiredDeviceCapabilities - - arm64 - - UIRequiresFullScreen - - UIStatusBarStyle - UIStatusBarStyleDefault - UISupportedInterfaceOrientations - - UIInterfaceOrientationPortrait - UIInterfaceOrientationPortraitUpsideDown - UIInterfaceOrientationLandscapeLeft - UIInterfaceOrientationLandscapeRight - - UISupportedInterfaceOrientations~ipad - - UIInterfaceOrientationPortrait - UIInterfaceOrientationPortraitUpsideDown - UIInterfaceOrientationLandscapeLeft - UIInterfaceOrientationLandscapeRight - - UIUserInterfaceStyle - Dark - UIViewControllerBasedStatusBarAppearance - - - \ No newline at end of file + + CADisableMinimumFrameDurationOnPhone + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleDisplayName + Nuvio + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + $(PRODUCT_BUNDLE_PACKAGE_TYPE) + CFBundleShortVersionString + 1.2.3 + CFBundleSignature + ???? + CFBundleURLTypes + + + CFBundleURLSchemes + + nuvio + com.nuvio.app + + + + CFBundleURLSchemes + + exp+nuvio + + + + CFBundleVersion + 18 + LSMinimumSystemVersion + 12.0 + LSRequiresIPhoneOS + + LSSupportsOpeningDocumentsInPlace + + NSAppTransportSecurity + + NSAllowsArbitraryLoads + + + NSBonjourServices + + _http._tcp + + NSLocalNetworkUsageDescription + Allow $(PRODUCT_NAME) to access your local network + NSMicrophoneUsageDescription + This app does not require microphone access. + RCTRootViewBackgroundColor + 4278322180 + UIFileSharingEnabled + + UILaunchStoryboardName + SplashScreen + UIRequiredDeviceCapabilities + + arm64 + + UIRequiresFullScreen + + UIStatusBarStyle + UIStatusBarStyleDefault + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UIUserInterfaceStyle + Dark + UIViewControllerBasedStatusBarAppearance + + + diff --git a/ios/Nuvio/NuvioRelease.entitlements b/ios/Nuvio/NuvioRelease.entitlements index a0bc443f..0c67376e 100644 --- a/ios/Nuvio/NuvioRelease.entitlements +++ b/ios/Nuvio/NuvioRelease.entitlements @@ -1,10 +1,5 @@ - - aps-environment - development - com.apple.developer.associated-domains - - - \ No newline at end of file + + diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 875ca833..1bc56a12 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -232,7 +232,7 @@ PODS: - ExpoModulesCore - ExpoDevice (7.0.3): - ExpoModulesCore - - ExpoDocumentPicker (14.0.7): + - ExpoDocumentPicker (13.0.3): - ExpoModulesCore - ExpoFileSystem (18.0.12): - ExpoModulesCore @@ -248,7 +248,7 @@ PODS: - SDWebImageSVGCoder (~> 1.7.0) - ExpoKeepAwake (14.0.3): - ExpoModulesCore - - ExpoLibVlcPlayer (2.1.7): + - ExpoLibVlcPlayer (2.2.1): - ExpoModulesCore - MobileVLCKit (= 3.6.1b1) - ExpoLinearGradient (14.0.2): @@ -304,7 +304,7 @@ PODS: - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - Yoga - - ExpoSharing (14.0.7): + - ExpoSharing (13.0.1): - ExpoModulesCore - ExpoSystemUI (4.0.9): - ExpoModulesCore @@ -2908,20 +2908,20 @@ SPEC CHECKSUMS: ExpoBrightness: c0011699a3225c869666e266326774a6fb6a9075 ExpoCrypto: e97e864c8d7b9ce4a000bca45dddb93544a1b2b4 ExpoDevice: d36ab4186b6799a28fd449bb9a1c77455f23fd1a - ExpoDocumentPicker: 2200eefc2817f19315fa18f0147e0b80ece86926 + ExpoDocumentPicker: 6d3d499cf15b692688a804f42927d0f35de5ebaa ExpoFileSystem: 42d363d3b96f9afab980dcef60d5657a4443c655 ExpoFont: f354e926f8feae5e831ec8087f36652b44a0b188 ExpoHaptics: 8d199b2f33245ea85289ff6c954c7ee7c00a5b5d ExpoImage: d840b256050f4428d2942bc2b6e9251f9e0d7021 ExpoKeepAwake: b0171a73665bfcefcfcc311742a72a956e6aa680 - ExpoLibVlcPlayer: 027c16c178364a133f6ee10fc7a7d8636f92dbe8 + ExpoLibVlcPlayer: dce3d0b5847838cd5f8c5f3c3aa1bc55c92e911d ExpoLinearGradient: 35ebd83b16f80b3add053a2fd68cc328ed927f60 ExpoLinking: 8d12bee174ba0cdf31239706578e29e74a417402 ExpoLocalization: 7776ea3bdb112125390745bbaf919b734b2ad1c7 ExpoModulesCore: c25d77625038b1968ea1afefc719862c0d8dd993 ExpoRandom: d1444df65007bdd4070009efd5dab18e20bf0f00 ExpoScreenOrientation: af8b31d3164239a4ef3ea0b32bd63fb65df70d58 - ExpoSharing: 032c01bb034319e2374badf082ae935be866d2e9 + ExpoSharing: 849a5ce9985c22598c16ec027e32969be8062e8e ExpoSystemUI: b82a45cf0f6a4fa18d07c46deba8725dd27688b4 ExpoWebBrowser: a212e6b480d8857d3e441fba51e0c968333803b3 EXStructuredHeaders: 09c70347b282e3d2507e25fb4c747b1b885f87f6 diff --git a/src/components/metadata/HeroSection.tsx b/src/components/metadata/HeroSection.tsx index 051d6426..796c482d 100644 --- a/src/components/metadata/HeroSection.tsx +++ b/src/components/metadata/HeroSection.tsx @@ -653,10 +653,10 @@ const WatchProgressDisplay = memo(({ - - {progressData.episodeInfo} • Last watched {progressData.formattedTime} + {progressData.episodeInfo} • {progressData.formattedTime} {/* Trakt sync status with enhanced styling */} diff --git a/src/components/player/controls/PlayerControls.tsx b/src/components/player/controls/PlayerControls.tsx index 0b7c2ac4..dbc2ec88 100644 --- a/src/components/player/controls/PlayerControls.tsx +++ b/src/components/player/controls/PlayerControls.tsx @@ -181,13 +181,15 @@ export const PlayerControls: React.FC = ({ - {/* Playback Speed Button */} - - - - Speed {currentPlaybackSpeed}x - - + {/* Playback Speed Button - Hidden on iOS */} + {Platform.OS !== 'ios' && ( + + + + Speed {currentPlaybackSpeed}x + + + )} {/* Audio Button - Updated to use ksAudioTracks */} { type: 'movie', poster: 'placeholder', year: movie.year, - lastWatched: new Date(watchedMovie.last_watched_at).toLocaleDateString(), + lastWatched: watchedMovie.last_watched_at, // Store raw timestamp for sorting plays: watchedMovie.plays, imdbId: movie.ids.imdb, traktId: movie.ids.trakt, @@ -551,7 +551,7 @@ const LibraryScreen = () => { type: 'series', poster: 'placeholder', year: show.year, - lastWatched: new Date(watchedShow.last_watched_at).toLocaleDateString(), + lastWatched: watchedShow.last_watched_at, // Store raw timestamp for sorting plays: watchedShow.plays, imdbId: show.ids.imdb, traktId: show.ids.trakt, @@ -573,7 +573,7 @@ const LibraryScreen = () => { type: 'movie', poster: 'placeholder', year: item.movie.year, - lastWatched: new Date(item.paused_at).toLocaleDateString(), + lastWatched: item.paused_at, // Store raw timestamp for sorting imdbId: item.movie.ids.imdb, traktId: item.movie.ids.trakt, images: item.movie.images, @@ -585,7 +585,7 @@ const LibraryScreen = () => { type: 'series', poster: 'placeholder', year: item.show.year, - lastWatched: new Date(item.paused_at).toLocaleDateString(), + lastWatched: item.paused_at, // Store raw timestamp for sorting imdbId: item.show.ids.imdb, traktId: item.show.ids.trakt, images: item.show.images, @@ -607,7 +607,7 @@ const LibraryScreen = () => { type: 'movie', poster: 'placeholder', year: movie.year, - lastWatched: new Date(watchlistMovie.listed_at).toLocaleDateString(), + lastWatched: watchlistMovie.listed_at, // Store raw timestamp for sorting imdbId: movie.ids.imdb, traktId: movie.ids.trakt, images: movie.images, @@ -626,7 +626,7 @@ const LibraryScreen = () => { type: 'series', poster: 'placeholder', year: show.year, - lastWatched: new Date(watchlistShow.listed_at).toLocaleDateString(), + lastWatched: watchlistShow.listed_at, // Store raw timestamp for sorting imdbId: show.ids.imdb, traktId: show.ids.trakt, images: show.images, @@ -648,7 +648,7 @@ const LibraryScreen = () => { type: 'movie', poster: 'placeholder', year: movie.year, - lastWatched: new Date(collectionMovie.collected_at).toLocaleDateString(), + lastWatched: collectionMovie.collected_at, // Store raw timestamp for sorting imdbId: movie.ids.imdb, traktId: movie.ids.trakt, images: movie.images, @@ -667,7 +667,7 @@ const LibraryScreen = () => { type: 'series', poster: 'placeholder', year: show.year, - lastWatched: new Date(collectionShow.collected_at).toLocaleDateString(), + lastWatched: collectionShow.collected_at, // Store raw timestamp for sorting imdbId: show.ids.imdb, traktId: show.ids.trakt, images: show.images, @@ -689,7 +689,7 @@ const LibraryScreen = () => { type: 'movie', poster: 'placeholder', year: movie.year, - lastWatched: new Date(ratedItem.rated_at).toLocaleDateString(), + lastWatched: ratedItem.rated_at, // Store raw timestamp for sorting rating: ratedItem.rating, imdbId: movie.ids.imdb, traktId: movie.ids.trakt, @@ -703,7 +703,7 @@ const LibraryScreen = () => { type: 'series', poster: 'placeholder', year: show.year, - lastWatched: new Date(ratedItem.rated_at).toLocaleDateString(), + lastWatched: ratedItem.rated_at, // Store raw timestamp for sorting rating: ratedItem.rating, imdbId: show.ids.imdb, traktId: show.ids.trakt, @@ -715,7 +715,7 @@ const LibraryScreen = () => { break; } - // Sort by last watched/added date (most recent first) + // Sort by last watched/added date (most recent first) using raw timestamps return items.sort((a, b) => { const dateA = a.lastWatched ? new Date(a.lastWatched).getTime() : 0; const dateB = b.lastWatched ? new Date(b.lastWatched).getTime() : 0; diff --git a/src/screens/StreamsScreen.tsx b/src/screens/StreamsScreen.tsx index 77730d7d..bcca2147 100644 --- a/src/screens/StreamsScreen.tsx +++ b/src/screens/StreamsScreen.tsx @@ -705,25 +705,32 @@ export const StreamsScreen = () => { }, [loadingStreams, loadingEpisodeStreams, groupedStreams, episodeStreams, type]); + // Reset autoplay state when episode changes (but preserve fromPlayer logic) + useEffect(() => { + // Reset autoplay triggered state when episode changes + // This allows autoplay to work for each episode individually + setAutoplayTriggered(false); + }, [selectedEpisode]); + // Reset the selected provider to 'all' if the current selection is no longer available // But preserve special filter values like 'grouped-plugins' and 'all' useEffect(() => { // Don't reset if it's a special filter value const isSpecialFilter = selectedProvider === 'all' || selectedProvider === 'grouped-plugins'; - + if (isSpecialFilter) { return; // Always preserve special filters } - + // Check if provider exists in current streams data const currentStreamsData = metadata?.videos && metadata.videos.length > 1 && selectedEpisode ? episodeStreams : groupedStreams; - const hasStreamsForProvider = currentStreamsData[selectedProvider] && - currentStreamsData[selectedProvider].streams && + const hasStreamsForProvider = currentStreamsData[selectedProvider] && + currentStreamsData[selectedProvider].streams && currentStreamsData[selectedProvider].streams.length > 0; - + // Only reset if the provider doesn't exist in available providers AND doesn't have streams const isAvailableProvider = availableProviders.has(selectedProvider); - + if (!isAvailableProvider && !hasStreamsForProvider) { setSelectedProvider('all'); } diff --git a/src/services/traktService.ts b/src/services/traktService.ts index 3feea65a..5972014d 100644 --- a/src/services/traktService.ts +++ b/src/services/traktService.ts @@ -797,7 +797,32 @@ export class TraktService { if (!response.ok) { const errorText = await response.text(); - logger.error(`[TraktService] API Error ${response.status} for ${endpoint}:`, errorText); + + // Enhanced error logging for debugging + logger.error(`[TraktService] API Error ${response.status} for ${endpoint}:`, { + status: response.status, + statusText: response.statusText, + errorText: errorText, + requestBody: body ? JSON.stringify(body, null, 2) : 'No body', + headers: Object.fromEntries(response.headers.entries()) + }); + + // Handle 404 errors more gracefully - they might indicate content not found in Trakt + if (response.status === 404) { + logger.warn(`[TraktService] Content not found in Trakt database (404) for ${endpoint}. This might indicate:`); + logger.warn(`[TraktService] 1. Invalid IMDb ID: ${body?.movie?.ids?.imdb || body?.show?.ids?.imdb || 'N/A'}`); + logger.warn(`[TraktService] 2. Content not in Trakt database: ${body?.movie?.title || body?.show?.title || 'N/A'}`); + logger.warn(`[TraktService] 3. Authentication issues with token`); + + // Return a graceful response for 404s instead of throwing + return { + id: 0, + action: 'not_found', + progress: body?.progress || 0, + error: 'Content not found in Trakt database' + } as any; + } + throw new Error(`API request failed: ${response.status}`); } @@ -1199,6 +1224,13 @@ export class TraktService { */ public async startWatching(contentData: TraktContentData, progress: number): Promise { try { + // Validate content data before making API call + const validation = this.validateContentData(contentData); + if (!validation.isValid) { + logger.error('[TraktService] Invalid content data for start watching:', validation.errors); + return null; + } + const payload = await this.buildScrobblePayload(contentData, progress); if (!payload) { return null; @@ -1216,6 +1248,13 @@ export class TraktService { */ public async pauseWatching(contentData: TraktContentData, progress: number): Promise { try { + // Validate content data before making API call + const validation = this.validateContentData(contentData); + if (!validation.isValid) { + logger.error('[TraktService] Invalid content data for pause watching:', validation.errors); + return null; + } + const payload = await this.buildScrobblePayload(contentData, progress); if (!payload) { return null; @@ -1233,6 +1272,13 @@ export class TraktService { */ public async stopWatching(contentData: TraktContentData, progress: number): Promise { try { + // Validate content data before making API call + const validation = this.validateContentData(contentData); + if (!validation.isValid) { + logger.error('[TraktService] Invalid content data for stop watching:', validation.errors); + return null; + } + const payload = await this.buildScrobblePayload(contentData, progress); if (!payload) { return null; @@ -1254,12 +1300,73 @@ export class TraktService { return this.stopWatching(contentData, progress); } + /** + * Validate content data before making API calls + */ + private validateContentData(contentData: TraktContentData): { isValid: boolean; errors: string[] } { + const errors: string[] = []; + + if (!contentData.type || !['movie', 'episode'].includes(contentData.type)) { + errors.push('Invalid content type'); + } + + if (!contentData.title || contentData.title.trim() === '') { + errors.push('Missing or empty title'); + } + + if (!contentData.imdbId || contentData.imdbId.trim() === '') { + errors.push('Missing or empty IMDb ID'); + } + + if (contentData.type === 'episode') { + if (!contentData.season || contentData.season < 1) { + errors.push('Invalid season number'); + } + if (!contentData.episode || contentData.episode < 1) { + errors.push('Invalid episode number'); + } + if (!contentData.showTitle || contentData.showTitle.trim() === '') { + errors.push('Missing or empty show title'); + } + if (!contentData.showYear || contentData.showYear < 1900) { + errors.push('Invalid show year'); + } + } + + return { + isValid: errors.length === 0, + errors + }; + } + /** * Build scrobble payload for API requests */ private async buildScrobblePayload(contentData: TraktContentData, progress: number): Promise { try { + // Enhanced debug logging for payload building + logger.log('[TraktService] Building scrobble payload:', { + type: contentData.type, + title: contentData.title, + imdbId: contentData.imdbId, + year: contentData.year, + season: contentData.season, + episode: contentData.episode, + showTitle: contentData.showTitle, + showYear: contentData.showYear, + showImdbId: contentData.showImdbId, + progress: progress + }); + if (contentData.type === 'movie') { + if (!contentData.imdbId || !contentData.title) { + logger.error('[TraktService] Missing movie data for scrobbling:', { + imdbId: contentData.imdbId, + title: contentData.title + }); + return null; + } + // Clean IMDB ID - some APIs want it without 'tt' prefix const cleanImdbId = contentData.imdbId.startsWith('tt') ? contentData.imdbId.substring(2) @@ -1276,11 +1383,16 @@ export class TraktService { progress: Math.round(progress * 100) / 100 // Round to 2 decimal places }; - // Movie payload logging removed + logger.log('[TraktService] Movie payload built:', payload); return payload; } else if (contentData.type === 'episode') { if (!contentData.season || !contentData.episode || !contentData.showTitle || !contentData.showYear) { - logger.error('[TraktService] Missing episode data for scrobbling'); + logger.error('[TraktService] Missing episode data for scrobbling:', { + season: contentData.season, + episode: contentData.episode, + showTitle: contentData.showTitle, + showYear: contentData.showYear + }); return null; } @@ -1318,7 +1430,7 @@ export class TraktService { payload.episode.ids.imdb = cleanEpisodeImdbId; } - // Episode payload logging removed + logger.log('[TraktService] Episode payload built:', payload); return payload; }