mirror of
https://github.com/tapframe/NuvioStreaming.git
synced 2026-04-21 00:32:04 +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
|
// Use a ref to track if a background refresh is in progress to avoid state updates
|
||||||
const isRefreshingRef = useRef(false);
|
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
|
// Cache for metadata to avoid redundant API calls
|
||||||
const metadataCache = useRef<Record<string, { metadata: any; basicContent: StreamingContent | null; timestamp: number }>>({});
|
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 result;
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
} catch (error) {
|
} catch (error: any) {
|
||||||
logger.error(`Failed to fetch metadata for ${type}:${id}:`, error);
|
// Skip logging 404 errors to reduce noise
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// Modified loadContinueWatching to render incrementally
|
// Modified loadContinueWatching to render incrementally
|
||||||
const loadContinueWatching = useCallback(async (isBackgroundRefresh = false) => {
|
const loadContinueWatching = useCallback(async (isBackgroundRefresh = false) => {
|
||||||
if (isRefreshingRef.current) return;
|
if (isRefreshingRef.current) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (!isBackgroundRefresh) {
|
if (!isBackgroundRefresh) {
|
||||||
setLoading(true);
|
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)
|
// Helper to merge a batch of items into state (dedupe by type:id, keep newest)
|
||||||
const mergeBatchIntoState = (batch: ContinueWatchingItem[]) => {
|
const mergeBatchIntoState = (batch: ContinueWatchingItem[]) => {
|
||||||
if (!batch || batch.length === 0) return;
|
if (!batch || batch.length === 0) return;
|
||||||
|
|
||||||
setContinueWatchingItems((prev) => {
|
setContinueWatchingItems((prev) => {
|
||||||
const map = new Map<string, ContinueWatchingItem>();
|
const map = new Map<string, ContinueWatchingItem>();
|
||||||
for (const it of prev) {
|
for (const it of prev) {
|
||||||
map.set(`${it.type}:${it.id}`, it);
|
map.set(`${it.type}:${it.id}`, it);
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const it of batch) {
|
for (const it of batch) {
|
||||||
const key = `${it.type}:${it.id}`;
|
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);
|
const existing = map.get(key);
|
||||||
if (!existing || (it.lastUpdated ?? 0) > (existing.lastUpdated ?? 0)) {
|
if (!existing || (it.lastUpdated ?? 0) > (existing.lastUpdated ?? 0)) {
|
||||||
map.set(key, it);
|
map.set(key, it);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const merged = Array.from(map.values());
|
const merged = Array.from(map.values());
|
||||||
merged.sort((a, b) => (b.lastUpdated ?? 0) - (a.lastUpdated ?? 0));
|
merged.sort((a, b) => (b.lastUpdated ?? 0) - (a.lastUpdated ?? 0));
|
||||||
|
|
||||||
return merged;
|
return merged;
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
@ -274,7 +290,7 @@ const ContinueWatchingSection = React.forwardRef<ContinueWatchingRef>((props, re
|
||||||
|
|
||||||
if (batch.length > 0) mergeBatchIntoState(batch);
|
if (batch.length > 0) mergeBatchIntoState(batch);
|
||||||
} catch (error) {
|
} 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]) => {
|
const perShowPromises = Object.entries(latestWatchedByShow).map(async ([showId, info]) => {
|
||||||
try {
|
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 nextEpisode = info.episode + 1;
|
||||||
const cachedData = await getCachedMetadata('series', showId);
|
const cachedData = await getCachedMetadata('series', showId);
|
||||||
if (!cachedData?.basicContent) return;
|
if (!cachedData?.basicContent) return;
|
||||||
|
|
@ -313,6 +336,7 @@ const ContinueWatchingSection = React.forwardRef<ContinueWatchingRef>((props, re
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (nextEpisodeVideo && isEpisodeReleased(nextEpisodeVideo)) {
|
if (nextEpisodeVideo && isEpisodeReleased(nextEpisodeVideo)) {
|
||||||
|
logger.log(`➕ [TraktSync] Adding next episode for ${showId}: S${info.season}E${nextEpisode}`);
|
||||||
mergeBatchIntoState([
|
mergeBatchIntoState([
|
||||||
{
|
{
|
||||||
...basicContent,
|
...basicContent,
|
||||||
|
|
@ -327,38 +351,43 @@ const ContinueWatchingSection = React.forwardRef<ContinueWatchingRef>((props, re
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Persist "watched" progress for the episode that Trakt reported
|
// Persist "watched" progress for the episode that Trakt reported (only if not recently removed)
|
||||||
const watchedEpisodeId = `${showId}:${info.season}:${info.episode}`;
|
if (!recentlyRemovedRef.current.has(showKey)) {
|
||||||
const existingProgress = allProgress[`series:${showId}:${watchedEpisodeId}`];
|
const watchedEpisodeId = `${showId}:${info.season}:${info.episode}`;
|
||||||
const existingPercent = existingProgress ? (existingProgress.currentTime / existingProgress.duration) * 100 : 0;
|
const existingProgress = allProgress[`series:${showId}:${watchedEpisodeId}`];
|
||||||
if (!existingProgress || existingPercent < 85) {
|
const existingPercent = existingProgress ? (existingProgress.currentTime / existingProgress.duration) * 100 : 0;
|
||||||
await storageService.setWatchProgress(
|
if (!existingProgress || existingPercent < 85) {
|
||||||
showId,
|
logger.log(`💾 [TraktSync] Adding local progress for ${showId}: S${info.season}E${info.episode}`);
|
||||||
'series',
|
await storageService.setWatchProgress(
|
||||||
{
|
showId,
|
||||||
currentTime: 1,
|
'series',
|
||||||
duration: 1,
|
{
|
||||||
lastUpdated: info.watchedAt,
|
currentTime: 1,
|
||||||
traktSynced: true,
|
duration: 1,
|
||||||
traktProgress: 100,
|
lastUpdated: info.watchedAt,
|
||||||
} as any,
|
traktSynced: true,
|
||||||
`${info.season}:${info.episode}`
|
traktProgress: 100,
|
||||||
);
|
} as any,
|
||||||
|
`${info.season}:${info.episode}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
logger.log(`🚫 [TraktSync] Skipping local progress for recently removed show: ${showKey}`);
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
logger.error('Failed to build placeholder from history:', err);
|
// Continue with other shows even if one fails
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
await Promise.allSettled(perShowPromises);
|
await Promise.allSettled(perShowPromises);
|
||||||
} catch (err) {
|
} 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
|
// Wait for all groups and trakt merge to settle, then finalize loading state
|
||||||
await Promise.allSettled([...groupPromises, traktMergePromise]);
|
await Promise.allSettled([...groupPromises, traktMergePromise]);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('Failed to load continue watching items:', error);
|
// Continue even if loading fails
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
isRefreshingRef.current = false;
|
isRefreshingRef.current = false;
|
||||||
|
|
@ -474,26 +503,47 @@ const ContinueWatchingSection = React.forwardRef<ContinueWatchingRef>((props, re
|
||||||
try {
|
try {
|
||||||
// Trigger haptic feedback for confirmation
|
// Trigger haptic feedback for confirmation
|
||||||
Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success);
|
Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success);
|
||||||
|
|
||||||
// Remove all watch progress for this content (all episodes if series)
|
// Remove all watch progress for this content (all episodes if series)
|
||||||
await storageService.removeAllWatchProgressForContent(item.id, item.type, { addBaseTombstone: true });
|
await storageService.removeAllWatchProgressForContent(item.id, item.type, { addBaseTombstone: true });
|
||||||
|
|
||||||
// Also remove from Trakt playback queue if authenticated
|
// Also remove from Trakt playback queue if authenticated
|
||||||
const traktService = TraktService.getInstance();
|
const traktService = TraktService.getInstance();
|
||||||
const isAuthed = await traktService.isAuthenticated();
|
const isAuthed = await traktService.isAuthenticated();
|
||||||
|
logger.log(`🔍 [ContinueWatching] Trakt authentication status: ${isAuthed}`);
|
||||||
|
|
||||||
if (isAuthed) {
|
if (isAuthed) {
|
||||||
await traktService.deletePlaybackForContent(
|
logger.log(`🗑️ [ContinueWatching] Removing Trakt history for ${item.id}`);
|
||||||
item.id,
|
let traktResult = false;
|
||||||
item.type as 'movie' | 'series',
|
|
||||||
undefined,
|
if (item.type === 'movie') {
|
||||||
undefined
|
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
|
// 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) {
|
} catch (error) {
|
||||||
logger.error('Failed to remove watch progress:', error);
|
// Continue even if removal fails
|
||||||
} finally {
|
} finally {
|
||||||
setDeletingItemId(null);
|
setDeletingItemId(null);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -602,7 +602,6 @@ class NotificationService {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!metadata) {
|
if (!metadata) {
|
||||||
logger.warn(`[NotificationService] No metadata found for series: ${seriesId}`);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -415,22 +415,35 @@ class StorageService {
|
||||||
options?: { addBaseTombstone?: boolean }
|
options?: { addBaseTombstone?: boolean }
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
try {
|
try {
|
||||||
|
logger.log(`🗑️ [StorageService] removeAllWatchProgressForContent called for ${type}:${id}`);
|
||||||
|
|
||||||
const all = await this.getAllWatchProgress();
|
const all = await this.getAllWatchProgress();
|
||||||
const prefix = `${type}:${id}`;
|
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>> = [];
|
const removals: Array<Promise<void>> = [];
|
||||||
for (const key of Object.keys(all)) {
|
for (const key of matchingKeys) {
|
||||||
if (key === prefix || key.startsWith(`${prefix}:`)) {
|
// Compute episodeId if present
|
||||||
// Compute episodeId if present
|
const episodeId = key.length > prefix.length + 1 ? key.slice(prefix.length + 1) : undefined;
|
||||||
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));
|
removals.push(this.removeWatchProgress(id, type, episodeId));
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
await Promise.allSettled(removals);
|
await Promise.allSettled(removals);
|
||||||
|
logger.log(`✅ [StorageService] All watch progress removals completed`);
|
||||||
|
|
||||||
if (options?.addBaseTombstone) {
|
if (options?.addBaseTombstone) {
|
||||||
|
logger.log(`🪦 [StorageService] Adding tombstone for ${type}:${id}`);
|
||||||
await this.addWatchProgressTombstone(id, type);
|
await this.addWatchProgressTombstone(id, type);
|
||||||
|
logger.log(`✅ [StorageService] Tombstone added successfully`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
logger.log(`✅ [StorageService] removeAllWatchProgressForContent completed for ${type}:${id}`);
|
||||||
} catch (error) {
|
} 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 {
|
try {
|
||||||
const cinemetaManifest = await this.getManifest('https://v3-cinemeta.strem.io/manifest.json');
|
const cinemetaManifest = await this.getManifest('https://v3-cinemeta.strem.io/manifest.json');
|
||||||
this.installedAddons.set(cinemetaId, cinemetaManifest);
|
this.installedAddons.set(cinemetaId, cinemetaManifest);
|
||||||
logger.log('✅ Cinemeta pre-installed as default addon with full manifest');
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('Failed to fetch Cinemeta manifest, using fallback:', error);
|
|
||||||
// Fallback to minimal manifest if fetch fails
|
// Fallback to minimal manifest if fetch fails
|
||||||
const fallbackManifest: Manifest = {
|
const fallbackManifest: Manifest = {
|
||||||
id: cinemetaId,
|
id: cinemetaId,
|
||||||
|
|
@ -263,7 +261,6 @@ class StremioService {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
this.installedAddons.set(cinemetaId, fallbackManifest);
|
this.installedAddons.set(cinemetaId, fallbackManifest);
|
||||||
logger.log('✅ Cinemeta pre-installed with fallback manifest');
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -273,9 +270,7 @@ class StremioService {
|
||||||
try {
|
try {
|
||||||
const opensubsManifest = await this.getManifest('https://opensubtitles-v3.strem.io/manifest.json');
|
const opensubsManifest = await this.getManifest('https://opensubtitles-v3.strem.io/manifest.json');
|
||||||
this.installedAddons.set(opensubsId, opensubsManifest);
|
this.installedAddons.set(opensubsId, opensubsManifest);
|
||||||
logger.log('✅ OpenSubtitles v3 pre-installed as default subtitles addon');
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('Failed to fetch OpenSubtitles manifest, using fallback:', error);
|
|
||||||
const fallbackManifest: Manifest = {
|
const fallbackManifest: Manifest = {
|
||||||
id: opensubsId,
|
id: opensubsId,
|
||||||
name: 'OpenSubtitles v3',
|
name: 'OpenSubtitles v3',
|
||||||
|
|
@ -297,7 +292,6 @@ class StremioService {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
this.installedAddons.set(opensubsId, fallbackManifest);
|
this.installedAddons.set(opensubsId, fallbackManifest);
|
||||||
logger.log('✅ OpenSubtitles v3 pre-installed with fallback manifest');
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -330,7 +324,6 @@ class StremioService {
|
||||||
|
|
||||||
this.initialized = true;
|
this.initialized = true;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('Failed to initialize addons:', error);
|
|
||||||
// Initialize with empty state on error
|
// Initialize with empty state on error
|
||||||
this.installedAddons = new Map();
|
this.installedAddons = new Map();
|
||||||
this.addonOrder = [];
|
this.addonOrder = [];
|
||||||
|
|
@ -352,12 +345,21 @@ class StremioService {
|
||||||
return await request();
|
return await request();
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
lastError = error;
|
lastError = error;
|
||||||
logger.warn(`Request failed (attempt ${attempt + 1}/${retries + 1}):`, {
|
|
||||||
message: error.message,
|
// Don't retry on 404 errors (content not found) - these are expected for some content
|
||||||
code: error.code,
|
if (error.response?.status === 404) {
|
||||||
isAxiosError: error.isAxiosError,
|
throw error;
|
||||||
status: error.response?.status,
|
}
|
||||||
});
|
|
||||||
|
// 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) {
|
if (attempt < retries) {
|
||||||
const backoffDelay = delay * Math.pow(2, attempt);
|
const backoffDelay = delay * Math.pow(2, attempt);
|
||||||
|
|
@ -375,7 +377,7 @@ class StremioService {
|
||||||
const scope = (await AsyncStorage.getItem('@user:current')) || 'local';
|
const scope = (await AsyncStorage.getItem('@user:current')) || 'local';
|
||||||
await AsyncStorage.setItem(`@user:${scope}:${this.STORAGE_KEY}`, JSON.stringify(addonsArray));
|
await AsyncStorage.setItem(`@user:${scope}:${this.STORAGE_KEY}`, JSON.stringify(addonsArray));
|
||||||
} catch (error) {
|
} 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)),
|
AsyncStorage.setItem(this.ADDON_ORDER_KEY, JSON.stringify(this.addonOrder)),
|
||||||
]);
|
]);
|
||||||
} catch (error) {
|
} 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 {
|
removeAddon(id: string): void {
|
||||||
// Prevent removal of Cinemeta as it's a pre-installed addon
|
// Prevent removal of Cinemeta as it's a pre-installed addon
|
||||||
if (id === 'com.linvo.cinemeta') {
|
if (id === 'com.linvo.cinemeta') {
|
||||||
logger.warn('❌ Cannot remove Cinemeta - it is a pre-installed addon');
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -548,10 +549,8 @@ class StremioService {
|
||||||
|
|
||||||
// Add filters
|
// Add filters
|
||||||
if (filters.length > 0) {
|
if (filters.length > 0) {
|
||||||
logger.log(`Adding ${filters.length} filters to Cinemeta request`);
|
|
||||||
filters.forEach(filter => {
|
filters.forEach(filter => {
|
||||||
if (filter.value) {
|
if (filter.value) {
|
||||||
logger.log(`Adding filter ${filter.title}=${filter.value}`);
|
|
||||||
url += `&${encodeURIComponent(filter.title)}=${encodeURIComponent(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 () => {
|
const response = await this.retryRequest(async () => {
|
||||||
return await axios.get(url);
|
return await axios.get(url);
|
||||||
});
|
});
|
||||||
|
|
@ -612,18 +613,13 @@ class StremioService {
|
||||||
|
|
||||||
// If a preferred addon is specified, try it first
|
// If a preferred addon is specified, try it first
|
||||||
if (preferredAddonId) {
|
if (preferredAddonId) {
|
||||||
logger.log(`🎯 Trying preferred addon first: ${preferredAddonId}`);
|
|
||||||
const preferredAddon = addons.find(addon => addon.id === preferredAddonId);
|
const preferredAddon = addons.find(addon => addon.id === preferredAddonId);
|
||||||
|
|
||||||
if (preferredAddon && preferredAddon.resources) {
|
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 { baseUrl, queryParams } = this.getAddonBaseURL(preferredAddon.url || '');
|
||||||
const wouldBeUrl = queryParams ? `${baseUrl}/meta/${type}/${id}.json?${queryParams}` : `${baseUrl}/meta/${type}/${id}.json`;
|
const url = 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));
|
|
||||||
|
|
||||||
// Check if addon supports meta resource for this type
|
// Check if addon supports meta resource for this type
|
||||||
let hasMetaSupport = false;
|
let hasMetaSupport = false;
|
||||||
|
|
||||||
|
|
@ -653,20 +649,16 @@ class StremioService {
|
||||||
try {
|
try {
|
||||||
|
|
||||||
const response = await this.retryRequest(async () => {
|
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) {
|
if (response.data && response.data.meta) {
|
||||||
return response.data.meta;
|
return response.data.meta;
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} 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) {
|
for (const baseUrl of cinemetaUrls) {
|
||||||
try {
|
try {
|
||||||
const url = `${baseUrl}/meta/${type}/${id}.json`;
|
const url = `${baseUrl}/meta/${type}/${id}.json`;
|
||||||
|
|
||||||
const response = await this.retryRequest(async () => {
|
const response = await this.retryRequest(async () => {
|
||||||
return await axios.get(url, { timeout: 10000 });
|
return await axios.get(url, { timeout: 10000 });
|
||||||
});
|
});
|
||||||
|
|
@ -687,8 +679,7 @@ class StremioService {
|
||||||
if (response.data && response.data.meta) {
|
if (response.data && response.data.meta) {
|
||||||
return response.data.meta;
|
return response.data.meta;
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error: any) {
|
||||||
logger.warn(`❌ Failed to fetch meta from ${baseUrl}:`, error);
|
|
||||||
continue; // Try next URL
|
continue; // Try next URL
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -727,7 +718,7 @@ class StremioService {
|
||||||
const { baseUrl, queryParams } = this.getAddonBaseURL(addon.url || '');
|
const { baseUrl, queryParams } = this.getAddonBaseURL(addon.url || '');
|
||||||
const url = queryParams ? `${baseUrl}/meta/${type}/${id}.json?${queryParams}` : `${baseUrl}/meta/${type}/${id}.json`;
|
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 () => {
|
const response = await this.retryRequest(async () => {
|
||||||
return await axios.get(url, { timeout: 10000 });
|
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;
|
return null;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('Error in getMetaDetails:', 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())
|
.sort((a, b) => new Date(a.released).getTime() - new Date(b.released).getTime())
|
||||||
.slice(0, maxEpisodes); // Limit number of episodes to prevent memory overflow
|
.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 {
|
return {
|
||||||
seriesName: metadata.name,
|
seriesName: metadata.name,
|
||||||
poster: metadata.poster || '',
|
poster: metadata.poster || '',
|
||||||
|
|
@ -872,25 +864,20 @@ class StremioService {
|
||||||
|
|
||||||
if (tmdbIdNumber) {
|
if (tmdbIdNumber) {
|
||||||
tmdbId = tmdbIdNumber.toString();
|
tmdbId = tmdbIdNumber.toString();
|
||||||
logger.log(`🔄 [getStreams] Converted IMDb ID ${baseImdbId} to TMDB ID ${tmdbId}${scraperType === 'tv' ? ` (S${season}E${episode})` : ''}`);
|
|
||||||
} else {
|
} else {
|
||||||
logger.warn(`⚠️ [getStreams] Could not convert IMDb ID ${baseImdbId} to TMDB ID`);
|
|
||||||
return; // Skip local scrapers if we can't convert the ID
|
return; // Skip local scrapers if we can't convert the ID
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error(`❌ [getStreams] Failed to parse Stremio ID or convert to TMDB ID:`, error);
|
|
||||||
return; // Skip local scrapers if ID parsing fails
|
return; // Skip local scrapers if ID parsing fails
|
||||||
}
|
}
|
||||||
|
|
||||||
// Execute local scrapers asynchronously with TMDB ID
|
// Execute local scrapers asynchronously with TMDB ID
|
||||||
localScraperService.getStreams(scraperType, tmdbId, season, episode, (streams, scraperId, scraperName, error) => {
|
localScraperService.getStreams(scraperType, tmdbId, season, episode, (streams, scraperId, scraperName, error) => {
|
||||||
if (error) {
|
if (error) {
|
||||||
logger.error(`❌ [getStreams] Local scraper ${scraperName} failed:`, error);
|
|
||||||
if (callback) {
|
if (callback) {
|
||||||
callback(null, scraperId, scraperName, error);
|
callback(null, scraperId, scraperName, error);
|
||||||
}
|
}
|
||||||
} else if (streams && streams.length > 0) {
|
} else if (streams && streams.length > 0) {
|
||||||
logger.log(`✅ [getStreams] Local scraper ${scraperName} returned ${streams.length} streams`);
|
|
||||||
if (callback) {
|
if (callback) {
|
||||||
callback(streams, scraperId, scraperName, null);
|
callback(streams, scraperId, scraperName, null);
|
||||||
}
|
}
|
||||||
|
|
@ -899,21 +886,13 @@ class StremioService {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('❌ [getStreams] Failed to execute local scrapers:', error);
|
// Continue even if local scrapers fail
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check specifically for TMDB Embed addon
|
// Check specifically for TMDB Embed addon
|
||||||
const tmdbEmbed = addons.find(addon => addon.id === 'org.tmdbembedapi');
|
const tmdbEmbed = addons.find(addon => addon.id === 'org.tmdbembedapi');
|
||||||
if (tmdbEmbed) {
|
if (!tmdbEmbed) {
|
||||||
logger.log('🔍 [getStreams] Found TMDB Embed Streams addon:', {
|
// TMDB Embed addon not found
|
||||||
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');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Find addons that provide streams and sort them by installation order
|
// Find addons that provide streams and sort them by installation order
|
||||||
|
|
@ -1002,7 +981,6 @@ class StremioService {
|
||||||
callback(processedStreams, addon.id, addon.name, null);
|
callback(processedStreams, addon.id, addon.name, null);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error(`❌ [getStreams] Failed to get streams from ${addon.name} (${addon.id}):`, error);
|
|
||||||
if (callback) {
|
if (callback) {
|
||||||
// Call callback with error
|
// Call callback with error
|
||||||
callback(null, addon.id, addon.name, error as Error);
|
callback(null, addon.id, addon.name, error as Error);
|
||||||
|
|
@ -1067,8 +1045,6 @@ class StremioService {
|
||||||
status: error.response?.status,
|
status: error.response?.status,
|
||||||
responseData: error.response?.data
|
responseData: error.response?.data
|
||||||
};
|
};
|
||||||
logger.error('Failed to fetch streams from addon:', errorDetails);
|
|
||||||
|
|
||||||
// Re-throw the error with more context
|
// Re-throw the error with more context
|
||||||
throw new Error(`Failed to fetch streams from ${addon.name}: ${error.message}`);
|
throw new Error(`Failed to fetch streams from ${addon.name}: ${error.message}`);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -252,6 +252,133 @@ export interface TraktContentData {
|
||||||
showImdbId?: string;
|
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 {
|
export class TraktService {
|
||||||
private static instance: TraktService;
|
private static instance: TraktService;
|
||||||
private accessToken: string | null = null;
|
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> {
|
public async deletePlaybackForContent(imdbId: string, type: 'movie' | 'series', season?: number, episode?: number): Promise<boolean> {
|
||||||
try {
|
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();
|
const progressItems = await this.getPlaybackProgress();
|
||||||
|
logger.log(`📊 [TraktService] Found ${progressItems.length} playback items`);
|
||||||
|
|
||||||
const target = progressItems.find(item => {
|
const target = progressItems.find(item => {
|
||||||
if (type === 'movie' && item.type === 'movie' && item.movie?.ids.imdb === imdbId) {
|
if (type === 'movie' && item.type === 'movie' && item.movie?.ids.imdb === imdbId) {
|
||||||
|
logger.log(`🎯 [TraktService] Found matching movie: ${item.movie?.title}`);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if (type === 'series' && item.type === 'episode' && item.show?.ids.imdb === imdbId) {
|
if (type === 'series' && item.type === 'episode' && item.show?.ids.imdb === imdbId) {
|
||||||
if (season !== undefined && episode !== undefined) {
|
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 true; // match any episode of the show if specific not provided
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
});
|
});
|
||||||
|
|
||||||
if (target) {
|
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) {
|
} catch (error) {
|
||||||
logger.error('[TraktService] Error deleting playback for content:', error);
|
logger.error(`❌ [TraktService] Error deleting playback for content ${type}:${imdbId}:`, error);
|
||||||
return false;
|
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
|
* Handle app state changes to reduce memory pressure
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
|
|
@ -28,7 +28,6 @@ export class MemoryManager {
|
||||||
// Request garbage collection if available (development builds)
|
// Request garbage collection if available (development builds)
|
||||||
if (global && typeof global.gc === 'function') {
|
if (global && typeof global.gc === 'function') {
|
||||||
global.gc();
|
global.gc();
|
||||||
logger.log('[MemoryManager] Forced garbage collection');
|
|
||||||
} else if (__DEV__) {
|
} else if (__DEV__) {
|
||||||
// In development, we can try to trigger GC by creating and releasing large objects
|
// In development, we can try to trigger GC by creating and releasing large objects
|
||||||
this.triggerGCInDev();
|
this.triggerGCInDev();
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue