some addon id prefix detection improvemets
This commit is contained in:
parent
106461b2b2
commit
563208689b
9 changed files with 163 additions and 54 deletions
2
package-lock.json
generated
2
package-lock.json
generated
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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 && (
|
||||
|
|
|
|||
|
|
@ -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})`);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)}`;
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in a new issue