some addon id prefix detection improvemets

This commit is contained in:
tapframe 2025-10-08 11:39:40 +05:30
parent 106461b2b2
commit 563208689b
9 changed files with 163 additions and 54 deletions

2
package-lock.json generated
View file

@ -48,7 +48,7 @@
"expo-haptics": "~14.0.1",
"expo-image": "~2.0.7",
"expo-intent-launcher": "~12.0.2",
"expo-libvlc-player": "^2.1.7",
"expo-libvlc-player": "^2.2.1",
"expo-linear-gradient": "~14.0.2",
"expo-localization": "~16.0.1",
"expo-notifications": "~0.29.14",

View file

@ -48,7 +48,7 @@
"expo-haptics": "~14.0.1",
"expo-image": "~2.0.7",
"expo-intent-launcher": "~12.0.2",
"expo-libvlc-player": "^2.1.7",
"expo-libvlc-player": "^2.2.1",
"expo-linear-gradient": "~14.0.2",
"expo-localization": "~16.0.1",
"expo-notifications": "~0.29.14",

View file

@ -643,21 +643,24 @@ const WatchProgressDisplay = memo(({
{/* Enhanced text container with better typography */}
<View style={styles.watchProgressTextContainer}>
<View style={styles.progressInfoMain}>
<Text style={[isTablet ? styles.tabletWatchProgressMainText : styles.watchProgressMainText, {
<Text style={[isTablet ? styles.tabletWatchProgressMainText : styles.watchProgressMainText, {
color: isCompleted ? '#00ff88' : currentTheme.colors.white,
fontSize: isCompleted ? (isTablet ? 15 : 13) : (isTablet ? 14 : 12),
fontWeight: isCompleted ? '700' : '600'
}]}>
{progressData.displayText}
</Text>
</View>
<Text style={[isTablet ? styles.tabletWatchProgressSubText : styles.watchProgressSubText, {
color: isCompleted ? 'rgba(0,255,136,0.7)' : currentTheme.colors.textMuted,
}]}>
{progressData.episodeInfo}
</Text>
</View>
{/* Only show episode info for series */}
{progressData.episodeInfo && (
<Text style={[isTablet ? styles.tabletWatchProgressSubText : styles.watchProgressSubText, {
color: isCompleted ? 'rgba(0,255,136,0.7)' : currentTheme.colors.textMuted,
}]}>
{progressData.episodeInfo}
</Text>
)}
{/* Trakt sync status with enhanced styling */}
{progressData.syncStatus && (

View file

@ -134,7 +134,9 @@ export function useTraktAutosync(options: TraktAutosyncOptions) {
}
try {
const progressPercent = (currentTime / duration) * 100;
// Clamp progress between 0 and 100
const rawProgress = (currentTime / duration) * 100;
const progressPercent = Math.min(100, Math.max(0, rawProgress));
const contentData = buildContentData();
const success = await startWatching(contentData, progressPercent);
@ -164,7 +166,8 @@ export function useTraktAutosync(options: TraktAutosyncOptions) {
}
try {
const progressPercent = (currentTime / duration) * 100;
const rawProgress = (currentTime / duration) * 100;
const progressPercent = Math.min(100, Math.max(0, rawProgress));
const now = Date.now();
// IMMEDIATE SYNC: Use immediate method for user-triggered actions (force=true)
@ -280,6 +283,8 @@ export function useTraktAutosync(options: TraktAutosyncOptions) {
try {
let progressPercent = duration > 0 ? (currentTime / duration) * 100 : 0;
// Clamp progress between 0 and 100
progressPercent = Math.min(100, Math.max(0, progressPercent));
// Initial progress calculation logging removed
// For unmount calls, always use the highest available progress
@ -301,7 +306,7 @@ export function useTraktAutosync(options: TraktAutosyncOptions) {
);
if (savedProgress && savedProgress.duration > 0) {
const savedProgressPercent = (savedProgress.currentTime / savedProgress.duration) * 100;
const savedProgressPercent = Math.min(100, Math.max(0, (savedProgress.currentTime / savedProgress.duration) * 100));
if (savedProgressPercent > maxProgress) {
maxProgress = savedProgressPercent;
}
@ -334,10 +339,11 @@ export function useTraktAutosync(options: TraktAutosyncOptions) {
return;
}
// For natural end events, always set progress to at least 90%
if (reason === 'ended' && progressPercent < 90) {
logger.log(`[TraktAutosync] Natural end detected but progress is low (${progressPercent.toFixed(1)}%), boosting to 90%`);
progressPercent = 90;
// For natural end events, ensure we cross Trakt's 80% scrobble threshold reliably.
// If close to the end, boost to 95% to avoid rounding issues.
if (reason === 'ended' && progressPercent < 95) {
logger.log(`[TraktAutosync] Natural end detected at ${progressPercent.toFixed(1)}%, boosting to 95% for scrobble`);
progressPercent = 95;
}
// Mark stop attempt and update timestamp
@ -366,6 +372,25 @@ export function useTraktAutosync(options: TraktAutosyncOptions) {
if (progressPercent >= 80) {
isSessionComplete.current = true;
logger.log(`[TraktAutosync] Session marked as complete (scrobbled) at ${progressPercent.toFixed(1)}%`);
// Ensure local watch progress reflects completion so UI shows as watched
try {
if (duration > 0) {
await storageService.setWatchProgress(
options.id,
options.type,
{
currentTime: duration,
duration,
lastUpdated: Date.now(),
traktSynced: true,
traktProgress: Math.max(progressPercent, 100),
} as any,
options.episodeId,
{ forceNotify: true }
);
}
} catch {}
}
logger.log(`[TraktAutosync] ${useImmediate ? 'IMMEDIATE: ' : ''}Successfully stopped watching: ${contentData.title} (${progressPercent.toFixed(1)}% - ${reason})`);

View file

@ -132,7 +132,7 @@ class HybridCacheService {
}
/**
* Get list of scrapers that need to be re-run
* Get list of scrapers that need to be re-run (expired, failed, or not cached)
*/
async getScrapersToRerun(
type: string,
@ -147,14 +147,26 @@ class HybridCacheService {
const validScraperIds = new Set(validResults.map(r => r.scraperId));
const expiredScraperIds = new Set(expiredScrapers);
// Return scrapers that are either expired or not cached
// Get scrapers that previously failed (returned no streams)
const failedScraperIds = new Set(
validResults
.filter(r => !r.success || r.streams.length === 0)
.map(r => r.scraperId)
);
// Return scrapers that are:
// 1. Not cached at all
// 2. Expired
// 3. Previously failed (regardless of cache status)
const scrapersToRerun = availableScrapers
.filter(scraper =>
!validScraperIds.has(scraper.id) || expiredScraperIds.has(scraper.id)
!validScraperIds.has(scraper.id) ||
expiredScraperIds.has(scraper.id) ||
failedScraperIds.has(scraper.id)
)
.map(scraper => scraper.id);
logger.log(`[HybridCache] Scrapers to re-run: ${scrapersToRerun.join(', ')}`);
logger.log(`[HybridCache] Scrapers to re-run: ${scrapersToRerun.join(', ')} (not cached: ${availableScrapers.filter(s => !validScraperIds.has(s.id)).length}, expired: ${expiredScrapers.length}, failed: ${failedScraperIds.size})`);
return scrapersToRerun;
}

View file

@ -215,7 +215,7 @@ class LocalScraperCacheService {
}
/**
* Get list of scrapers that need to be re-run (expired or failed)
* Get list of scrapers that need to be re-run (expired, failed, or not cached)
*/
async getScrapersToRerun(
type: string,
@ -229,14 +229,26 @@ class LocalScraperCacheService {
const validScraperIds = new Set(validResults.map(r => r.scraperId));
const expiredScraperIds = new Set(expiredScrapers);
// Return scrapers that are either expired or not cached at all
// Get scrapers that previously failed (returned no streams)
const failedScraperIds = new Set(
validResults
.filter(r => !r.success || r.streams.length === 0)
.map(r => r.scraperId)
);
// Return scrapers that are:
// 1. Not cached at all
// 2. Expired
// 3. Previously failed (regardless of cache status)
const scrapersToRerun = availableScrapers
.filter(scraper =>
!validScraperIds.has(scraper.id) || expiredScraperIds.has(scraper.id)
!validScraperIds.has(scraper.id) ||
expiredScraperIds.has(scraper.id) ||
failedScraperIds.has(scraper.id)
)
.map(scraper => scraper.id);
logger.log(`[LocalScraperCache] Scrapers to re-run: ${scrapersToRerun.join(', ')}`);
logger.log(`[LocalScraperCache] Scrapers to re-run: ${scrapersToRerun.join(', ')} (not cached: ${availableScrapers.filter(s => !validScraperIds.has(s.id)).length}, expired: ${expiredScrapers.length}, failed: ${failedScraperIds.size})`);
return scrapersToRerun;
}

View file

@ -905,16 +905,26 @@ class LocalScraperService {
}
// Determine which scrapers need to be re-run
const scrapersToRerun = enabledScrapers.filter(scraper =>
expiredScrapers.includes(scraper.id) || !validResults.some(r => r.scraperId === scraper.id)
);
const scrapersToRerun = enabledScrapers.filter(scraper => {
const hasValidResult = validResults.some(r => r.scraperId === scraper.id);
const isExpired = expiredScrapers.includes(scraper.id);
const hasFailedResult = validResults.some(r => r.scraperId === scraper.id && (!r.success || r.streams.length === 0));
return !hasValidResult || isExpired || hasFailedResult;
});
if (scrapersToRerun.length === 0) {
logger.log('[LocalScraperService] All scrapers have valid cached results');
return;
}
logger.log(`[LocalScraperService] Re-running ${scrapersToRerun.length} scrapers (${expiredScrapers.length} expired, ${scrapersToRerun.length - expiredScrapers.length} not cached) for ${type}:${tmdbId}`);
logger.log(`[LocalScraperService] Re-running ${scrapersToRerun.length} scrapers for ${type}:${tmdbId}`, {
totalEnabled: enabledScrapers.length,
expired: expiredScrapers.length,
failed: validResults.filter(r => !r.success || r.streams.length === 0).length,
notCached: enabledScrapers.length - validResults.length,
scrapersToRerun: scrapersToRerun.map(s => s.name)
});
// Generate a lightweight request id for tracing
const requestId = `rs_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 6)}`;

View file

@ -188,19 +188,56 @@ class StremioService {
this.initializationPromise = this.initialize();
}
// Shared validator for content IDs eligible for metadata requests
// Dynamic validator for content IDs based on installed addon capabilities
public isValidContentId(type: string, id: string | null | undefined): boolean {
const isValidType = type === 'movie' || type === 'series';
const lowerId = (id || '').toLowerCase();
const looksLikeImdb = /^tt\d+/.test(lowerId);
const looksLikeKitsu = lowerId.startsWith('kitsu:') || lowerId === 'kitsu';
const looksLikeSeriesId = lowerId.startsWith('series:');
const isNullishId = !id || lowerId === 'null' || lowerId === 'undefined';
const providerLikeIds = new Set<string>(['moviebox', 'torbox']);
const isProviderSlug = providerLikeIds.has(lowerId);
if (!isValidType || isNullishId || isProviderSlug) return false;
return looksLikeImdb || looksLikeKitsu || looksLikeSeriesId;
// Get all supported ID prefixes from installed addons
const supportedPrefixes = this.getAllSupportedIdPrefixes(type);
// Check if the ID matches any supported prefix
return supportedPrefixes.some(prefix => lowerId.startsWith(prefix.toLowerCase()));
}
// Get all ID prefixes supported by installed addons for a given content type
public getAllSupportedIdPrefixes(type: string): string[] {
const addons = this.getInstalledAddons();
const prefixes = new Set<string>();
for (const addon of addons) {
// Check addon-level idPrefixes
if (addon.idPrefixes && Array.isArray(addon.idPrefixes)) {
addon.idPrefixes.forEach(prefix => prefixes.add(prefix));
}
// Check resource-level idPrefixes
if (addon.resources && Array.isArray(addon.resources)) {
for (const resource of addon.resources) {
if (typeof resource === 'object' && resource !== null && 'name' in resource) {
const typedResource = resource as ResourceObject;
// Only include prefixes for resources that support the content type
if (Array.isArray(typedResource.types) && typedResource.types.includes(type)) {
if (Array.isArray(typedResource.idPrefixes)) {
typedResource.idPrefixes.forEach(prefix => prefixes.add(prefix));
}
}
}
}
}
}
// Always include common prefixes as fallback
prefixes.add('tt'); // IMDb
prefixes.add('kitsu:'); // Kitsu
prefixes.add('series:'); // Series
return Array.from(prefixes);
}
static getInstance(): StremioService {
@ -723,13 +760,16 @@ class StremioService {
async getMetaDetails(type: string, id: string, preferredAddonId?: string): Promise<MetaDetails | null> {
try {
// Validate content ID first
if (!this.isValidContentId(type, id)) {
return null;
}
const addons = this.getInstalledAddons();
// If a preferred addon is specified, try it first
if (preferredAddonId) {
logger.log(`🔍 [getMetaDetails] Looking for preferred addon: ${preferredAddonId}`);
const preferredAddon = addons.find(addon => addon.id === preferredAddonId);
logger.log(`🔍 [getMetaDetails] Found preferred addon: ${preferredAddon ? preferredAddon.id : 'null'}`);
if (preferredAddon && preferredAddon.resources) {
// Build URL for metadata request
@ -739,6 +779,7 @@ class StremioService {
// Check if addon supports meta resource for this type
let hasMetaSupport = false;
let supportsIdPrefix = false;
for (const resource of preferredAddon.resources) {
// Check if the current element is a ResourceObject
@ -748,6 +789,12 @@ class StremioService {
Array.isArray(typedResource.types) &&
typedResource.types.includes(type)) {
hasMetaSupport = true;
// Check idPrefix support
if (Array.isArray(typedResource.idPrefixes) && typedResource.idPrefixes.length > 0) {
supportsIdPrefix = typedResource.idPrefixes.some(p => id.startsWith(p));
} else {
supportsIdPrefix = true;
}
break;
}
}
@ -755,17 +802,19 @@ class StremioService {
else if (typeof resource === 'string' && resource === 'meta' && preferredAddon.types) {
if (Array.isArray(preferredAddon.types) && preferredAddon.types.includes(type)) {
hasMetaSupport = true;
// Check addon-level idPrefixes
if (preferredAddon.idPrefixes && Array.isArray(preferredAddon.idPrefixes) && preferredAddon.idPrefixes.length > 0) {
supportsIdPrefix = preferredAddon.idPrefixes.some(p => id.startsWith(p));
} else {
supportsIdPrefix = true;
}
break;
}
}
}
logger.log(`🔍 Meta support check: ${hasMetaSupport} (addon types: ${JSON.stringify(preferredAddon.types)})`);
if (hasMetaSupport) {
if (hasMetaSupport && supportsIdPrefix) {
try {
logger.log(`🔗 [${preferredAddon.name}] Requesting metadata: ${url} (preferred, id=${id}, type=${type})`);
const response = await this.retryRequest(async () => {
return await axios.get(url, { timeout: 10000 });
});
@ -773,7 +822,7 @@ class StremioService {
if (response.data && response.data.meta) {
return response.data.meta;
}
} catch (error) {
} catch (error: any) {
// Continue trying other addons
}
}
@ -785,7 +834,7 @@ class StremioService {
'https://v3-cinemeta.strem.io',
'http://v3-cinemeta.strem.io'
];
for (const baseUrl of cinemetaUrls) {
try {
const encodedId = encodeURIComponent(id);
@ -804,7 +853,6 @@ class StremioService {
}
// If Cinemeta fails, try other addons (excluding the preferred one already tried)
for (const addon of addons) {
if (!addon.resources || addon.id === 'com.linvo.cinemeta' || addon.id === preferredAddonId) continue;
@ -843,6 +891,7 @@ class StremioService {
}
}
}
// Require both meta support and idPrefix compatibility
if (!(hasMetaSupport && supportsIdPrefix)) continue;
@ -851,7 +900,6 @@ class StremioService {
const encodedId = encodeURIComponent(id);
const url = queryParams ? `${baseUrl}/meta/${type}/${encodedId}.json?${queryParams}` : `${baseUrl}/meta/${type}/${encodedId}.json`;
logger.log(`🔗 [${addon.name}] Requesting metadata: ${url} (id=${id}, type=${type})`);
const response = await this.retryRequest(async () => {
return await axios.get(url, { timeout: 10000 });
});
@ -860,15 +908,10 @@ class StremioService {
return response.data.meta;
}
} catch (error) {
logger.warn(`❌ Failed to fetch meta from ${addon.name} (${addon.id}):`, error);
continue; // Try next 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);
@ -1476,6 +1519,7 @@ class StremioService {
return false;
}
}
export const stremioService = StremioService.getInstance();

View file

@ -1547,6 +1547,9 @@ export class TraktService {
*/
private async buildScrobblePayload(contentData: TraktContentData, progress: number): Promise<any | null> {
try {
// Clamp progress between 0 and 100 and round to 2 decimals for API
const clampedProgress = Math.min(100, Math.max(0, Math.round(progress * 100) / 100));
// Enhanced debug logging for payload building
logger.log('[TraktService] Building scrobble payload:', {
type: contentData.type,
@ -1558,7 +1561,7 @@ export class TraktService {
showTitle: contentData.showTitle,
showYear: contentData.showYear,
showImdbId: contentData.showImdbId,
progress: progress
progress: clampedProgress
});
if (contentData.type === 'movie') {
@ -1583,7 +1586,7 @@ export class TraktService {
imdb: imdbIdWithPrefix
}
},
progress: Math.round(progress * 100) / 100 // Round to 2 decimal places
progress: clampedProgress
};
logger.log('[TraktService] Movie payload built:', payload);
@ -1609,7 +1612,7 @@ export class TraktService {
season: contentData.season,
number: contentData.episode
},
progress: Math.round(progress * 100) / 100
progress: clampedProgress
};
// Add show IMDB ID if available