trakt list sorting order fix

This commit is contained in:
tapframe 2025-10-04 23:58:24 +05:30
parent ff1b406c48
commit 71487fce59
9 changed files with 253 additions and 141 deletions

View file

@ -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";

View file

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

View file

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

View file

@ -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

View file

@ -653,10 +653,10 @@ const WatchProgressDisplay = memo(({
</View>
<Text style={[isTablet ? styles.tabletWatchProgressSubText : styles.watchProgressSubText, {
<Text style={[isTablet ? styles.tabletWatchProgressSubText : styles.watchProgressSubText, {
color: isCompleted ? 'rgba(0,255,136,0.7)' : currentTheme.colors.textMuted,
}]}>
{progressData.episodeInfo} Last watched {progressData.formattedTime}
{progressData.episodeInfo} {progressData.formattedTime}
</Text>
{/* Trakt sync status with enhanced styling */}

View file

@ -181,13 +181,15 @@ export const PlayerControls: React.FC<PlayerControlsProps> = ({
</Text>
</TouchableOpacity>
{/* Playback Speed Button */}
<TouchableOpacity style={styles.bottomButton} onPress={cyclePlaybackSpeed}>
<Ionicons name="speedometer" size={20} color="white" />
<Text style={styles.bottomButtonText}>
Speed {currentPlaybackSpeed}x
</Text>
</TouchableOpacity>
{/* Playback Speed Button - Hidden on iOS */}
{Platform.OS !== 'ios' && (
<TouchableOpacity style={styles.bottomButton} onPress={cyclePlaybackSpeed}>
<Ionicons name="speedometer" size={20} color="white" />
<Text style={styles.bottomButtonText}>
Speed {currentPlaybackSpeed}x
</Text>
</TouchableOpacity>
)}
{/* Audio Button - Updated to use ksAudioTracks */}
<TouchableOpacity

View file

@ -531,7 +531,7 @@ const LibraryScreen = () => {
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;

View file

@ -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');
}

View file

@ -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<TraktScrobbleResponse | null> {
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<TraktScrobbleResponse | null> {
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<TraktScrobbleResponse | null> {
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<any | null> {
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;
}