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;
}