mirror of
https://github.com/tapframe/NuvioStreaming.git
synced 2026-03-11 17:45:38 +00:00
trakt continue watching removal fix
This commit is contained in:
parent
a5d2756854
commit
263da30f17
6 changed files with 525 additions and 108 deletions
|
|
@ -98,6 +98,10 @@ const ContinueWatchingSection = React.forwardRef<ContinueWatchingRef>((props, re
|
|||
|
||||
// Use a ref to track if a background refresh is in progress to avoid state updates
|
||||
const isRefreshingRef = useRef(false);
|
||||
|
||||
// Track recently removed items to prevent immediate re-addition
|
||||
const recentlyRemovedRef = useRef<Set<string>>(new Set());
|
||||
const REMOVAL_IGNORE_DURATION = 10000; // 10 seconds
|
||||
|
||||
// Cache for metadata to avoid redundant API calls
|
||||
const metadataCache = useRef<Record<string, { metadata: any; basicContent: StreamingContent | null; timestamp: number }>>({});
|
||||
|
|
@ -125,15 +129,17 @@ const ContinueWatchingSection = React.forwardRef<ContinueWatchingRef>((props, re
|
|||
return result;
|
||||
}
|
||||
return null;
|
||||
} catch (error) {
|
||||
logger.error(`Failed to fetch metadata for ${type}:${id}:`, error);
|
||||
} catch (error: any) {
|
||||
// Skip logging 404 errors to reduce noise
|
||||
return null;
|
||||
}
|
||||
}, []);
|
||||
|
||||
// Modified loadContinueWatching to render incrementally
|
||||
const loadContinueWatching = useCallback(async (isBackgroundRefresh = false) => {
|
||||
if (isRefreshingRef.current) return;
|
||||
if (isRefreshingRef.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isBackgroundRefresh) {
|
||||
setLoading(true);
|
||||
|
|
@ -143,20 +149,30 @@ const ContinueWatchingSection = React.forwardRef<ContinueWatchingRef>((props, re
|
|||
// Helper to merge a batch of items into state (dedupe by type:id, keep newest)
|
||||
const mergeBatchIntoState = (batch: ContinueWatchingItem[]) => {
|
||||
if (!batch || batch.length === 0) return;
|
||||
|
||||
setContinueWatchingItems((prev) => {
|
||||
const map = new Map<string, ContinueWatchingItem>();
|
||||
for (const it of prev) {
|
||||
map.set(`${it.type}:${it.id}`, it);
|
||||
}
|
||||
|
||||
for (const it of batch) {
|
||||
const key = `${it.type}:${it.id}`;
|
||||
|
||||
// Skip recently removed items to prevent immediate re-addition
|
||||
if (recentlyRemovedRef.current.has(key)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const existing = map.get(key);
|
||||
if (!existing || (it.lastUpdated ?? 0) > (existing.lastUpdated ?? 0)) {
|
||||
map.set(key, it);
|
||||
}
|
||||
}
|
||||
|
||||
const merged = Array.from(map.values());
|
||||
merged.sort((a, b) => (b.lastUpdated ?? 0) - (a.lastUpdated ?? 0));
|
||||
|
||||
return merged;
|
||||
});
|
||||
};
|
||||
|
|
@ -274,7 +290,7 @@ const ContinueWatchingSection = React.forwardRef<ContinueWatchingRef>((props, re
|
|||
|
||||
if (batch.length > 0) mergeBatchIntoState(batch);
|
||||
} catch (error) {
|
||||
logger.error(`Failed to process content group ${group.type}:${group.id}:`, error);
|
||||
// Continue processing other groups even if one fails
|
||||
}
|
||||
});
|
||||
|
||||
|
|
@ -302,6 +318,13 @@ const ContinueWatchingSection = React.forwardRef<ContinueWatchingRef>((props, re
|
|||
|
||||
const perShowPromises = Object.entries(latestWatchedByShow).map(async ([showId, info]) => {
|
||||
try {
|
||||
// Check if this show was recently removed by the user
|
||||
const showKey = `series:${showId}`;
|
||||
if (recentlyRemovedRef.current.has(showKey)) {
|
||||
logger.log(`🚫 [TraktSync] Skipping recently removed show: ${showKey}`);
|
||||
return;
|
||||
}
|
||||
|
||||
const nextEpisode = info.episode + 1;
|
||||
const cachedData = await getCachedMetadata('series', showId);
|
||||
if (!cachedData?.basicContent) return;
|
||||
|
|
@ -313,6 +336,7 @@ const ContinueWatchingSection = React.forwardRef<ContinueWatchingRef>((props, re
|
|||
);
|
||||
}
|
||||
if (nextEpisodeVideo && isEpisodeReleased(nextEpisodeVideo)) {
|
||||
logger.log(`➕ [TraktSync] Adding next episode for ${showId}: S${info.season}E${nextEpisode}`);
|
||||
mergeBatchIntoState([
|
||||
{
|
||||
...basicContent,
|
||||
|
|
@ -327,38 +351,43 @@ const ContinueWatchingSection = React.forwardRef<ContinueWatchingRef>((props, re
|
|||
]);
|
||||
}
|
||||
|
||||
// Persist "watched" progress for the episode that Trakt reported
|
||||
const watchedEpisodeId = `${showId}:${info.season}:${info.episode}`;
|
||||
const existingProgress = allProgress[`series:${showId}:${watchedEpisodeId}`];
|
||||
const existingPercent = existingProgress ? (existingProgress.currentTime / existingProgress.duration) * 100 : 0;
|
||||
if (!existingProgress || existingPercent < 85) {
|
||||
await storageService.setWatchProgress(
|
||||
showId,
|
||||
'series',
|
||||
{
|
||||
currentTime: 1,
|
||||
duration: 1,
|
||||
lastUpdated: info.watchedAt,
|
||||
traktSynced: true,
|
||||
traktProgress: 100,
|
||||
} as any,
|
||||
`${info.season}:${info.episode}`
|
||||
);
|
||||
// Persist "watched" progress for the episode that Trakt reported (only if not recently removed)
|
||||
if (!recentlyRemovedRef.current.has(showKey)) {
|
||||
const watchedEpisodeId = `${showId}:${info.season}:${info.episode}`;
|
||||
const existingProgress = allProgress[`series:${showId}:${watchedEpisodeId}`];
|
||||
const existingPercent = existingProgress ? (existingProgress.currentTime / existingProgress.duration) * 100 : 0;
|
||||
if (!existingProgress || existingPercent < 85) {
|
||||
logger.log(`💾 [TraktSync] Adding local progress for ${showId}: S${info.season}E${info.episode}`);
|
||||
await storageService.setWatchProgress(
|
||||
showId,
|
||||
'series',
|
||||
{
|
||||
currentTime: 1,
|
||||
duration: 1,
|
||||
lastUpdated: info.watchedAt,
|
||||
traktSynced: true,
|
||||
traktProgress: 100,
|
||||
} as any,
|
||||
`${info.season}:${info.episode}`
|
||||
);
|
||||
}
|
||||
} else {
|
||||
logger.log(`🚫 [TraktSync] Skipping local progress for recently removed show: ${showKey}`);
|
||||
}
|
||||
} catch (err) {
|
||||
logger.error('Failed to build placeholder from history:', err);
|
||||
// Continue with other shows even if one fails
|
||||
}
|
||||
});
|
||||
await Promise.allSettled(perShowPromises);
|
||||
} catch (err) {
|
||||
logger.error('Error merging Trakt history:', err);
|
||||
// Continue even if Trakt history merge fails
|
||||
}
|
||||
})();
|
||||
|
||||
// Wait for all groups and trakt merge to settle, then finalize loading state
|
||||
await Promise.allSettled([...groupPromises, traktMergePromise]);
|
||||
} catch (error) {
|
||||
logger.error('Failed to load continue watching items:', error);
|
||||
// Continue even if loading fails
|
||||
} finally {
|
||||
setLoading(false);
|
||||
isRefreshingRef.current = false;
|
||||
|
|
@ -474,26 +503,47 @@ const ContinueWatchingSection = React.forwardRef<ContinueWatchingRef>((props, re
|
|||
try {
|
||||
// Trigger haptic feedback for confirmation
|
||||
Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success);
|
||||
|
||||
|
||||
// Remove all watch progress for this content (all episodes if series)
|
||||
await storageService.removeAllWatchProgressForContent(item.id, item.type, { addBaseTombstone: true });
|
||||
|
||||
|
||||
// Also remove from Trakt playback queue if authenticated
|
||||
const traktService = TraktService.getInstance();
|
||||
const isAuthed = await traktService.isAuthenticated();
|
||||
logger.log(`🔍 [ContinueWatching] Trakt authentication status: ${isAuthed}`);
|
||||
|
||||
if (isAuthed) {
|
||||
await traktService.deletePlaybackForContent(
|
||||
item.id,
|
||||
item.type as 'movie' | 'series',
|
||||
undefined,
|
||||
undefined
|
||||
);
|
||||
logger.log(`🗑️ [ContinueWatching] Removing Trakt history for ${item.id}`);
|
||||
let traktResult = false;
|
||||
|
||||
if (item.type === 'movie') {
|
||||
traktResult = await traktService.removeMovieFromHistory(item.id);
|
||||
} else {
|
||||
traktResult = await traktService.removeShowFromHistory(item.id);
|
||||
}
|
||||
|
||||
logger.log(`✅ [ContinueWatching] Trakt removal result: ${traktResult}`);
|
||||
} else {
|
||||
logger.log(`ℹ️ [ContinueWatching] Skipping Trakt removal - not authenticated`);
|
||||
}
|
||||
|
||||
|
||||
// Track this item as recently removed to prevent immediate re-addition
|
||||
const itemKey = `${item.type}:${item.id}`;
|
||||
recentlyRemovedRef.current.add(itemKey);
|
||||
|
||||
// Clear from recently removed after the ignore duration
|
||||
setTimeout(() => {
|
||||
recentlyRemovedRef.current.delete(itemKey);
|
||||
}, REMOVAL_IGNORE_DURATION);
|
||||
|
||||
// Update the list by filtering out the deleted item
|
||||
setContinueWatchingItems(prev => prev.filter(i => i.id !== item.id));
|
||||
setContinueWatchingItems(prev => {
|
||||
const newList = prev.filter(i => i.id !== item.id);
|
||||
return newList;
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
logger.error('Failed to remove watch progress:', error);
|
||||
// Continue even if removal fails
|
||||
} finally {
|
||||
setDeletingItemId(null);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -602,7 +602,6 @@ class NotificationService {
|
|||
}
|
||||
|
||||
if (!metadata) {
|
||||
logger.warn(`[NotificationService] No metadata found for series: ${seriesId}`);
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -415,22 +415,35 @@ class StorageService {
|
|||
options?: { addBaseTombstone?: boolean }
|
||||
): Promise<void> {
|
||||
try {
|
||||
logger.log(`🗑️ [StorageService] removeAllWatchProgressForContent called for ${type}:${id}`);
|
||||
|
||||
const all = await this.getAllWatchProgress();
|
||||
const prefix = `${type}:${id}`;
|
||||
logger.log(`🔍 [StorageService] Looking for keys with prefix: ${prefix}`);
|
||||
|
||||
const matchingKeys = Object.keys(all).filter(key => key === prefix || key.startsWith(`${prefix}:`));
|
||||
logger.log(`📊 [StorageService] Found ${matchingKeys.length} matching keys:`, matchingKeys);
|
||||
|
||||
const removals: Array<Promise<void>> = [];
|
||||
for (const key of Object.keys(all)) {
|
||||
if (key === prefix || key.startsWith(`${prefix}:`)) {
|
||||
// Compute episodeId if present
|
||||
const episodeId = key.length > prefix.length + 1 ? key.slice(prefix.length + 1) : undefined;
|
||||
removals.push(this.removeWatchProgress(id, type, episodeId));
|
||||
}
|
||||
for (const key of matchingKeys) {
|
||||
// Compute episodeId if present
|
||||
const episodeId = key.length > prefix.length + 1 ? key.slice(prefix.length + 1) : undefined;
|
||||
logger.log(`🗑️ [StorageService] Removing progress for key: ${key} (episodeId: ${episodeId})`);
|
||||
removals.push(this.removeWatchProgress(id, type, episodeId));
|
||||
}
|
||||
|
||||
await Promise.allSettled(removals);
|
||||
logger.log(`✅ [StorageService] All watch progress removals completed`);
|
||||
|
||||
if (options?.addBaseTombstone) {
|
||||
logger.log(`🪦 [StorageService] Adding tombstone for ${type}:${id}`);
|
||||
await this.addWatchProgressTombstone(id, type);
|
||||
logger.log(`✅ [StorageService] Tombstone added successfully`);
|
||||
}
|
||||
|
||||
logger.log(`✅ [StorageService] removeAllWatchProgressForContent completed for ${type}:${id}`);
|
||||
} catch (error) {
|
||||
logger.error('Error removing all watch progress for content:', error);
|
||||
logger.error(`❌ [StorageService] Error removing all watch progress for content ${type}:${id}:`, error);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -220,9 +220,7 @@ class StremioService {
|
|||
try {
|
||||
const cinemetaManifest = await this.getManifest('https://v3-cinemeta.strem.io/manifest.json');
|
||||
this.installedAddons.set(cinemetaId, cinemetaManifest);
|
||||
logger.log('✅ Cinemeta pre-installed as default addon with full manifest');
|
||||
} catch (error) {
|
||||
logger.error('Failed to fetch Cinemeta manifest, using fallback:', error);
|
||||
// Fallback to minimal manifest if fetch fails
|
||||
const fallbackManifest: Manifest = {
|
||||
id: cinemetaId,
|
||||
|
|
@ -263,7 +261,6 @@ class StremioService {
|
|||
}
|
||||
};
|
||||
this.installedAddons.set(cinemetaId, fallbackManifest);
|
||||
logger.log('✅ Cinemeta pre-installed with fallback manifest');
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -273,9 +270,7 @@ class StremioService {
|
|||
try {
|
||||
const opensubsManifest = await this.getManifest('https://opensubtitles-v3.strem.io/manifest.json');
|
||||
this.installedAddons.set(opensubsId, opensubsManifest);
|
||||
logger.log('✅ OpenSubtitles v3 pre-installed as default subtitles addon');
|
||||
} catch (error) {
|
||||
logger.error('Failed to fetch OpenSubtitles manifest, using fallback:', error);
|
||||
const fallbackManifest: Manifest = {
|
||||
id: opensubsId,
|
||||
name: 'OpenSubtitles v3',
|
||||
|
|
@ -297,7 +292,6 @@ class StremioService {
|
|||
}
|
||||
};
|
||||
this.installedAddons.set(opensubsId, fallbackManifest);
|
||||
logger.log('✅ OpenSubtitles v3 pre-installed with fallback manifest');
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -330,7 +324,6 @@ class StremioService {
|
|||
|
||||
this.initialized = true;
|
||||
} catch (error) {
|
||||
logger.error('Failed to initialize addons:', error);
|
||||
// Initialize with empty state on error
|
||||
this.installedAddons = new Map();
|
||||
this.addonOrder = [];
|
||||
|
|
@ -352,12 +345,21 @@ class StremioService {
|
|||
return await request();
|
||||
} catch (error: any) {
|
||||
lastError = error;
|
||||
logger.warn(`Request failed (attempt ${attempt + 1}/${retries + 1}):`, {
|
||||
message: error.message,
|
||||
code: error.code,
|
||||
isAxiosError: error.isAxiosError,
|
||||
status: error.response?.status,
|
||||
});
|
||||
|
||||
// Don't retry on 404 errors (content not found) - these are expected for some content
|
||||
if (error.response?.status === 404) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
// Only log warnings for non-404 errors to reduce noise
|
||||
if (error.response?.status !== 404) {
|
||||
logger.warn(`Request failed (attempt ${attempt + 1}/${retries + 1}):`, {
|
||||
message: error.message,
|
||||
code: error.code,
|
||||
isAxiosError: error.isAxiosError,
|
||||
status: error.response?.status,
|
||||
});
|
||||
}
|
||||
|
||||
if (attempt < retries) {
|
||||
const backoffDelay = delay * Math.pow(2, attempt);
|
||||
|
|
@ -375,7 +377,7 @@ class StremioService {
|
|||
const scope = (await AsyncStorage.getItem('@user:current')) || 'local';
|
||||
await AsyncStorage.setItem(`@user:${scope}:${this.STORAGE_KEY}`, JSON.stringify(addonsArray));
|
||||
} catch (error) {
|
||||
logger.error('Failed to save addons:', error);
|
||||
// Continue even if save fails
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -388,7 +390,7 @@ class StremioService {
|
|||
AsyncStorage.setItem(this.ADDON_ORDER_KEY, JSON.stringify(this.addonOrder)),
|
||||
]);
|
||||
} catch (error) {
|
||||
logger.error('Failed to save addon order:', error);
|
||||
// Continue even if save fails
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -444,7 +446,6 @@ class StremioService {
|
|||
removeAddon(id: string): void {
|
||||
// Prevent removal of Cinemeta as it's a pre-installed addon
|
||||
if (id === 'com.linvo.cinemeta') {
|
||||
logger.warn('❌ Cannot remove Cinemeta - it is a pre-installed addon');
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -548,10 +549,8 @@ class StremioService {
|
|||
|
||||
// Add filters
|
||||
if (filters.length > 0) {
|
||||
logger.log(`Adding ${filters.length} filters to Cinemeta request`);
|
||||
filters.forEach(filter => {
|
||||
if (filter.value) {
|
||||
logger.log(`Adding filter ${filter.title}=${filter.value}`);
|
||||
url += `&${encodeURIComponent(filter.title)}=${encodeURIComponent(filter.value)}`;
|
||||
}
|
||||
});
|
||||
|
|
@ -592,6 +591,8 @@ class StremioService {
|
|||
});
|
||||
}
|
||||
|
||||
logger.log(`🔗 [${manifest.name}] Requesting catalog: ${url}`);
|
||||
|
||||
const response = await this.retryRequest(async () => {
|
||||
return await axios.get(url);
|
||||
});
|
||||
|
|
@ -612,18 +613,13 @@ class StremioService {
|
|||
|
||||
// If a preferred addon is specified, try it first
|
||||
if (preferredAddonId) {
|
||||
logger.log(`🎯 Trying preferred addon first: ${preferredAddonId}`);
|
||||
const preferredAddon = addons.find(addon => addon.id === preferredAddonId);
|
||||
|
||||
|
||||
if (preferredAddon && preferredAddon.resources) {
|
||||
// Log what URL would be used for debugging
|
||||
// Build URL for metadata request
|
||||
const { baseUrl, queryParams } = this.getAddonBaseURL(preferredAddon.url || '');
|
||||
const wouldBeUrl = queryParams ? `${baseUrl}/meta/${type}/${id}.json?${queryParams}` : `${baseUrl}/meta/${type}/${id}.json`;
|
||||
logger.log(`🔍 Would check URL: ${wouldBeUrl} (addon: ${preferredAddon.name})`);
|
||||
|
||||
// Log addon resources for debugging
|
||||
logger.log(`🔍 Addon resources:`, JSON.stringify(preferredAddon.resources, null, 2));
|
||||
|
||||
const url = queryParams ? `${baseUrl}/meta/${type}/${id}.json?${queryParams}` : `${baseUrl}/meta/${type}/${id}.json`;
|
||||
|
||||
// Check if addon supports meta resource for this type
|
||||
let hasMetaSupport = false;
|
||||
|
||||
|
|
@ -653,20 +649,16 @@ class StremioService {
|
|||
try {
|
||||
|
||||
const response = await this.retryRequest(async () => {
|
||||
return await axios.get(wouldBeUrl, { timeout: 10000 });
|
||||
return await axios.get(url, { timeout: 10000 });
|
||||
});
|
||||
|
||||
if (response.data && response.data.meta) {
|
||||
return response.data.meta;
|
||||
}
|
||||
} catch (error) {
|
||||
logger.warn(`❌ Failed to fetch meta from preferred addon ${preferredAddon.name}:`, error);
|
||||
// Continue trying other addons
|
||||
}
|
||||
} else {
|
||||
logger.warn(`⚠️ Preferred addon ${preferredAddonId} does not support meta for type ${type}`);
|
||||
}
|
||||
} else {
|
||||
logger.warn(`⚠️ Preferred addon ${preferredAddonId} not found or has no resources`);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -679,7 +671,7 @@ class StremioService {
|
|||
for (const baseUrl of cinemetaUrls) {
|
||||
try {
|
||||
const url = `${baseUrl}/meta/${type}/${id}.json`;
|
||||
|
||||
|
||||
const response = await this.retryRequest(async () => {
|
||||
return await axios.get(url, { timeout: 10000 });
|
||||
});
|
||||
|
|
@ -687,8 +679,7 @@ class StremioService {
|
|||
if (response.data && response.data.meta) {
|
||||
return response.data.meta;
|
||||
}
|
||||
} catch (error) {
|
||||
logger.warn(`❌ Failed to fetch meta from ${baseUrl}:`, error);
|
||||
} catch (error: any) {
|
||||
continue; // Try next URL
|
||||
}
|
||||
}
|
||||
|
|
@ -727,7 +718,7 @@ class StremioService {
|
|||
const { baseUrl, queryParams } = this.getAddonBaseURL(addon.url || '');
|
||||
const url = queryParams ? `${baseUrl}/meta/${type}/${id}.json?${queryParams}` : `${baseUrl}/meta/${type}/${id}.json`;
|
||||
|
||||
logger.log(`HTTP GET: ${url}`);
|
||||
logger.log(`🔗 [${addon.name}] Requesting metadata: ${url}`);
|
||||
const response = await this.retryRequest(async () => {
|
||||
return await axios.get(url, { timeout: 10000 });
|
||||
});
|
||||
|
|
@ -741,7 +732,10 @@ class StremioService {
|
|||
}
|
||||
}
|
||||
|
||||
logger.warn('No metadata found from any addon');
|
||||
// Only log this warning in debug mode to reduce noise
|
||||
if (__DEV__) {
|
||||
logger.warn('No metadata found from any addon');
|
||||
}
|
||||
return null;
|
||||
} catch (error) {
|
||||
logger.error('Error in getMetaDetails:', error);
|
||||
|
|
@ -796,8 +790,6 @@ class StremioService {
|
|||
.sort((a, b) => new Date(a.released).getTime() - new Date(b.released).getTime())
|
||||
.slice(0, maxEpisodes); // Limit number of episodes to prevent memory overflow
|
||||
|
||||
logger.log(`[StremioService] Filtered ${metadata.videos.length} episodes down to ${filteredEpisodes.length} upcoming episodes for ${metadata.name}`);
|
||||
|
||||
return {
|
||||
seriesName: metadata.name,
|
||||
poster: metadata.poster || '',
|
||||
|
|
@ -872,25 +864,20 @@ class StremioService {
|
|||
|
||||
if (tmdbIdNumber) {
|
||||
tmdbId = tmdbIdNumber.toString();
|
||||
logger.log(`🔄 [getStreams] Converted IMDb ID ${baseImdbId} to TMDB ID ${tmdbId}${scraperType === 'tv' ? ` (S${season}E${episode})` : ''}`);
|
||||
} else {
|
||||
logger.warn(`⚠️ [getStreams] Could not convert IMDb ID ${baseImdbId} to TMDB ID`);
|
||||
return; // Skip local scrapers if we can't convert the ID
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error(`❌ [getStreams] Failed to parse Stremio ID or convert to TMDB ID:`, error);
|
||||
return; // Skip local scrapers if ID parsing fails
|
||||
}
|
||||
|
||||
// Execute local scrapers asynchronously with TMDB ID
|
||||
localScraperService.getStreams(scraperType, tmdbId, season, episode, (streams, scraperId, scraperName, error) => {
|
||||
if (error) {
|
||||
logger.error(`❌ [getStreams] Local scraper ${scraperName} failed:`, error);
|
||||
if (callback) {
|
||||
callback(null, scraperId, scraperName, error);
|
||||
}
|
||||
} else if (streams && streams.length > 0) {
|
||||
logger.log(`✅ [getStreams] Local scraper ${scraperName} returned ${streams.length} streams`);
|
||||
if (callback) {
|
||||
callback(streams, scraperId, scraperName, null);
|
||||
}
|
||||
|
|
@ -899,21 +886,13 @@ class StremioService {
|
|||
}
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('❌ [getStreams] Failed to execute local scrapers:', error);
|
||||
// Continue even if local scrapers fail
|
||||
}
|
||||
|
||||
// Check specifically for TMDB Embed addon
|
||||
const tmdbEmbed = addons.find(addon => addon.id === 'org.tmdbembedapi');
|
||||
if (tmdbEmbed) {
|
||||
logger.log('🔍 [getStreams] Found TMDB Embed Streams addon:', {
|
||||
id: tmdbEmbed.id,
|
||||
name: tmdbEmbed.name,
|
||||
url: tmdbEmbed.url,
|
||||
resources: tmdbEmbed.resources,
|
||||
types: tmdbEmbed.types
|
||||
});
|
||||
} else {
|
||||
logger.log('⚠️ [getStreams] TMDB Embed Streams addon not found among installed addons');
|
||||
if (!tmdbEmbed) {
|
||||
// TMDB Embed addon not found
|
||||
}
|
||||
|
||||
// Find addons that provide streams and sort them by installation order
|
||||
|
|
@ -1002,7 +981,6 @@ class StremioService {
|
|||
callback(processedStreams, addon.id, addon.name, null);
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error(`❌ [getStreams] Failed to get streams from ${addon.name} (${addon.id}):`, error);
|
||||
if (callback) {
|
||||
// Call callback with error
|
||||
callback(null, addon.id, addon.name, error as Error);
|
||||
|
|
@ -1067,8 +1045,6 @@ class StremioService {
|
|||
status: error.response?.status,
|
||||
responseData: error.response?.data
|
||||
};
|
||||
logger.error('Failed to fetch streams from addon:', errorDetails);
|
||||
|
||||
// Re-throw the error with more context
|
||||
throw new Error(`Failed to fetch streams from ${addon.name}: ${error.message}`);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -252,6 +252,133 @@ export interface TraktContentData {
|
|||
showImdbId?: string;
|
||||
}
|
||||
|
||||
export interface TraktHistoryItem {
|
||||
id: number;
|
||||
watched_at: string;
|
||||
action: 'scrobble' | 'checkin' | 'watch';
|
||||
type: 'movie' | 'episode';
|
||||
movie?: {
|
||||
title: string;
|
||||
year: number;
|
||||
ids: {
|
||||
trakt: number;
|
||||
slug: string;
|
||||
imdb: string;
|
||||
tmdb: number;
|
||||
};
|
||||
};
|
||||
episode?: {
|
||||
season: number;
|
||||
number: number;
|
||||
title: string;
|
||||
ids: {
|
||||
trakt: number;
|
||||
tvdb?: number;
|
||||
imdb?: string;
|
||||
tmdb?: number;
|
||||
};
|
||||
};
|
||||
show?: {
|
||||
title: string;
|
||||
year: number;
|
||||
ids: {
|
||||
trakt: number;
|
||||
slug: string;
|
||||
tvdb?: number;
|
||||
imdb: string;
|
||||
tmdb: number;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
export interface TraktHistoryRemovePayload {
|
||||
movies?: Array<{
|
||||
title?: string;
|
||||
year?: number;
|
||||
ids: {
|
||||
trakt?: number;
|
||||
slug?: string;
|
||||
imdb?: string;
|
||||
tmdb?: number;
|
||||
};
|
||||
}>;
|
||||
shows?: Array<{
|
||||
title?: string;
|
||||
year?: number;
|
||||
ids: {
|
||||
trakt?: number;
|
||||
slug?: string;
|
||||
tvdb?: number;
|
||||
imdb?: string;
|
||||
tmdb?: number;
|
||||
};
|
||||
seasons?: Array<{
|
||||
number: number;
|
||||
episodes?: Array<{
|
||||
number: number;
|
||||
}>;
|
||||
}>;
|
||||
}>;
|
||||
seasons?: Array<{
|
||||
ids: {
|
||||
trakt?: number;
|
||||
tvdb?: number;
|
||||
tmdb?: number;
|
||||
};
|
||||
}>;
|
||||
episodes?: Array<{
|
||||
ids: {
|
||||
trakt?: number;
|
||||
tvdb?: number;
|
||||
imdb?: string;
|
||||
tmdb?: number;
|
||||
};
|
||||
}>;
|
||||
ids?: number[];
|
||||
}
|
||||
|
||||
export interface TraktHistoryRemoveResponse {
|
||||
deleted: {
|
||||
movies: number;
|
||||
episodes: number;
|
||||
shows?: number;
|
||||
seasons?: number;
|
||||
};
|
||||
not_found: {
|
||||
movies: Array<{
|
||||
ids: {
|
||||
imdb?: string;
|
||||
trakt?: number;
|
||||
tmdb?: number;
|
||||
};
|
||||
}>;
|
||||
shows: Array<{
|
||||
ids: {
|
||||
imdb?: string;
|
||||
trakt?: number;
|
||||
tvdb?: number;
|
||||
tmdb?: number;
|
||||
};
|
||||
}>;
|
||||
seasons: Array<{
|
||||
ids: {
|
||||
trakt?: number;
|
||||
tvdb?: number;
|
||||
tmdb?: number;
|
||||
};
|
||||
}>;
|
||||
episodes: Array<{
|
||||
ids: {
|
||||
trakt?: number;
|
||||
tvdb?: number;
|
||||
imdb?: string;
|
||||
tmdb?: number;
|
||||
};
|
||||
}>;
|
||||
ids: number[];
|
||||
};
|
||||
}
|
||||
|
||||
export class TraktService {
|
||||
private static instance: TraktService;
|
||||
private accessToken: string | null = null;
|
||||
|
|
@ -1524,26 +1651,47 @@ export class TraktService {
|
|||
*/
|
||||
public async deletePlaybackForContent(imdbId: string, type: 'movie' | 'series', season?: number, episode?: number): Promise<boolean> {
|
||||
try {
|
||||
if (!this.accessToken) return false;
|
||||
logger.log(`🔍 [TraktService] deletePlaybackForContent called for ${type}:${imdbId} (season:${season}, episode:${episode})`);
|
||||
|
||||
if (!this.accessToken) {
|
||||
logger.log(`❌ [TraktService] No access token - cannot delete playback`);
|
||||
return false;
|
||||
}
|
||||
|
||||
logger.log(`🔍 [TraktService] Fetching current playback progress...`);
|
||||
const progressItems = await this.getPlaybackProgress();
|
||||
logger.log(`📊 [TraktService] Found ${progressItems.length} playback items`);
|
||||
|
||||
const target = progressItems.find(item => {
|
||||
if (type === 'movie' && item.type === 'movie' && item.movie?.ids.imdb === imdbId) {
|
||||
logger.log(`🎯 [TraktService] Found matching movie: ${item.movie?.title}`);
|
||||
return true;
|
||||
}
|
||||
if (type === 'series' && item.type === 'episode' && item.show?.ids.imdb === imdbId) {
|
||||
if (season !== undefined && episode !== undefined) {
|
||||
return item.episode?.season === season && item.episode?.number === episode;
|
||||
const matches = item.episode?.season === season && item.episode?.number === episode;
|
||||
if (matches) {
|
||||
logger.log(`🎯 [TraktService] Found matching episode: ${item.show?.title} S${season}E${episode}`);
|
||||
}
|
||||
return matches;
|
||||
}
|
||||
logger.log(`🎯 [TraktService] Found matching series episode: ${item.show?.title} S${item.episode?.season}E${item.episode?.number}`);
|
||||
return true; // match any episode of the show if specific not provided
|
||||
}
|
||||
return false;
|
||||
});
|
||||
|
||||
if (target) {
|
||||
return await this.deletePlaybackItem(target.id);
|
||||
logger.log(`🗑️ [TraktService] Deleting playback item with ID: ${target.id}`);
|
||||
const result = await this.deletePlaybackItem(target.id);
|
||||
logger.log(`✅ [TraktService] Delete result: ${result}`);
|
||||
return result;
|
||||
} else {
|
||||
logger.log(`ℹ️ [TraktService] No matching playback item found for ${type}:${imdbId}`);
|
||||
return false;
|
||||
}
|
||||
return false;
|
||||
} catch (error) {
|
||||
logger.error('[TraktService] Error deleting playback for content:', error);
|
||||
logger.error(`❌ [TraktService] Error deleting playback for content ${type}:${imdbId}:`, error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
|
@ -1571,6 +1719,238 @@ export class TraktService {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove items from user's watched history
|
||||
*/
|
||||
public async removeFromHistory(payload: TraktHistoryRemovePayload): Promise<TraktHistoryRemoveResponse | null> {
|
||||
try {
|
||||
logger.log(`🔍 [TraktService] removeFromHistory called with payload:`, JSON.stringify(payload, null, 2));
|
||||
|
||||
if (!await this.isAuthenticated()) {
|
||||
logger.log(`❌ [TraktService] Not authenticated for removeFromHistory`);
|
||||
return null;
|
||||
}
|
||||
|
||||
const result = await this.apiRequest<TraktHistoryRemoveResponse>('/sync/history/remove', 'POST', payload);
|
||||
|
||||
logger.log(`📥 [TraktService] removeFromHistory API response:`, JSON.stringify(result, null, 2));
|
||||
|
||||
return result;
|
||||
} catch (error) {
|
||||
logger.error('[TraktService] Failed to remove from history:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get user's watch history with optional filtering
|
||||
*/
|
||||
public async getHistory(
|
||||
type?: 'movies' | 'shows' | 'episodes',
|
||||
id?: number,
|
||||
startAt?: Date,
|
||||
endAt?: Date,
|
||||
page: number = 1,
|
||||
limit: number = 100
|
||||
): Promise<TraktHistoryItem[]> {
|
||||
try {
|
||||
if (!await this.isAuthenticated()) {
|
||||
return [];
|
||||
}
|
||||
|
||||
let endpoint = '/sync/history';
|
||||
if (type) {
|
||||
endpoint += `/${type}`;
|
||||
if (id) {
|
||||
endpoint += `/${id}`;
|
||||
}
|
||||
}
|
||||
|
||||
const params = new URLSearchParams();
|
||||
params.append('page', page.toString());
|
||||
params.append('limit', limit.toString());
|
||||
|
||||
if (startAt) {
|
||||
params.append('start_at', startAt.toISOString());
|
||||
}
|
||||
|
||||
if (endAt) {
|
||||
params.append('end_at', endAt.toISOString());
|
||||
}
|
||||
|
||||
const queryString = params.toString();
|
||||
if (queryString) {
|
||||
endpoint += `?${queryString}`;
|
||||
}
|
||||
|
||||
return this.apiRequest<TraktHistoryItem[]>(endpoint, 'GET');
|
||||
} catch (error) {
|
||||
logger.error('[TraktService] Failed to get history:', error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get user's movie watch history
|
||||
*/
|
||||
public async getHistoryMovies(
|
||||
startAt?: Date,
|
||||
endAt?: Date,
|
||||
page: number = 1,
|
||||
limit: number = 100
|
||||
): Promise<TraktHistoryItem[]> {
|
||||
return this.getHistory('movies', undefined, startAt, endAt, page, limit);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get user's episode watch history
|
||||
*/
|
||||
public async getHistoryEpisodes(
|
||||
startAt?: Date,
|
||||
endAt?: Date,
|
||||
page: number = 1,
|
||||
limit: number = 100
|
||||
): Promise<TraktHistoryItem[]> {
|
||||
return this.getHistory('episodes', undefined, startAt, endAt, page, limit);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get user's show watch history
|
||||
*/
|
||||
public async getHistoryShows(
|
||||
startAt?: Date,
|
||||
endAt?: Date,
|
||||
page: number = 1,
|
||||
limit: number = 100
|
||||
): Promise<TraktHistoryItem[]> {
|
||||
return this.getHistory('shows', undefined, startAt, endAt, page, limit);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a movie from watched history by IMDB ID
|
||||
*/
|
||||
public async removeMovieFromHistory(imdbId: string): Promise<boolean> {
|
||||
try {
|
||||
const payload: TraktHistoryRemovePayload = {
|
||||
movies: [
|
||||
{
|
||||
ids: {
|
||||
imdb: imdbId.startsWith('tt') ? imdbId : `tt${imdbId}`
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
const result = await this.removeFromHistory(payload);
|
||||
return result !== null && result.deleted.movies > 0;
|
||||
} catch (error) {
|
||||
logger.error('[TraktService] Failed to remove movie from history:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove an episode from watched history by IMDB IDs
|
||||
*/
|
||||
public async removeEpisodeFromHistory(showImdbId: string, season: number, episode: number): Promise<boolean> {
|
||||
try {
|
||||
const payload: TraktHistoryRemovePayload = {
|
||||
shows: [
|
||||
{
|
||||
ids: {
|
||||
imdb: showImdbId.startsWith('tt') ? showImdbId : `tt${showImdbId}`
|
||||
},
|
||||
seasons: [
|
||||
{
|
||||
number: season,
|
||||
episodes: [
|
||||
{
|
||||
number: episode
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
const result = await this.removeFromHistory(payload);
|
||||
return result !== null && result.deleted.episodes > 0;
|
||||
} catch (error) {
|
||||
logger.error('[TraktService] Failed to remove episode from history:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove entire show from watched history by IMDB ID
|
||||
*/
|
||||
public async removeShowFromHistory(imdbId: string): Promise<boolean> {
|
||||
try {
|
||||
logger.log(`🔍 [TraktService] removeShowFromHistory called for ${imdbId}`);
|
||||
|
||||
// First, let's check if this show exists in history
|
||||
logger.log(`🔍 [TraktService] Checking if ${imdbId} exists in watch history...`);
|
||||
const history = await this.getHistoryEpisodes(undefined, undefined, 1, 200); // Get recent episode history
|
||||
const fullImdbId = imdbId.startsWith('tt') ? imdbId : `tt${imdbId}`;
|
||||
const showInHistory = history.some(item =>
|
||||
item.show?.ids?.imdb === fullImdbId
|
||||
);
|
||||
|
||||
logger.log(`📊 [TraktService] Show ${imdbId} found in history: ${showInHistory}`);
|
||||
|
||||
if (!showInHistory) {
|
||||
logger.log(`ℹ️ [TraktService] Show ${imdbId} not found in watch history - nothing to remove`);
|
||||
return true; // Consider this a success since there's nothing to remove
|
||||
}
|
||||
|
||||
const payload: TraktHistoryRemovePayload = {
|
||||
shows: [
|
||||
{
|
||||
ids: {
|
||||
imdb: imdbId.startsWith('tt') ? imdbId : `tt${imdbId}`
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
logger.log(`📤 [TraktService] Sending removeFromHistory payload:`, JSON.stringify(payload, null, 2));
|
||||
|
||||
const result = await this.removeFromHistory(payload);
|
||||
|
||||
logger.log(`📥 [TraktService] removeFromHistory response:`, JSON.stringify(result, null, 2));
|
||||
|
||||
if (result) {
|
||||
const success = result.deleted.episodes > 0;
|
||||
logger.log(`✅ [TraktService] Show removal success: ${success} (${result.deleted.episodes} episodes deleted)`);
|
||||
return success;
|
||||
}
|
||||
|
||||
logger.log(`❌ [TraktService] No response from removeFromHistory API`);
|
||||
return false;
|
||||
} catch (error) {
|
||||
logger.error('[TraktService] Failed to remove show from history:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove items from history by history IDs
|
||||
*/
|
||||
public async removeHistoryByIds(historyIds: number[]): Promise<boolean> {
|
||||
try {
|
||||
const payload: TraktHistoryRemovePayload = {
|
||||
ids: historyIds
|
||||
};
|
||||
|
||||
const result = await this.removeFromHistory(payload);
|
||||
return result !== null && (result.deleted.movies > 0 || result.deleted.episodes > 0);
|
||||
} catch (error) {
|
||||
logger.error('[TraktService] Failed to remove history by IDs:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle app state changes to reduce memory pressure
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -28,7 +28,6 @@ export class MemoryManager {
|
|||
// Request garbage collection if available (development builds)
|
||||
if (global && typeof global.gc === 'function') {
|
||||
global.gc();
|
||||
logger.log('[MemoryManager] Forced garbage collection');
|
||||
} else if (__DEV__) {
|
||||
// In development, we can try to trigger GC by creating and releasing large objects
|
||||
this.triggerGCInDev();
|
||||
|
|
|
|||
Loading…
Reference in a new issue