mirror of
https://github.com/tapframe/NuvioStreaming.git
synced 2026-04-21 00:32:04 +00:00
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-haptics": "~14.0.1",
|
||||||
"expo-image": "~2.0.7",
|
"expo-image": "~2.0.7",
|
||||||
"expo-intent-launcher": "~12.0.2",
|
"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-linear-gradient": "~14.0.2",
|
||||||
"expo-localization": "~16.0.1",
|
"expo-localization": "~16.0.1",
|
||||||
"expo-notifications": "~0.29.14",
|
"expo-notifications": "~0.29.14",
|
||||||
|
|
|
||||||
|
|
@ -48,7 +48,7 @@
|
||||||
"expo-haptics": "~14.0.1",
|
"expo-haptics": "~14.0.1",
|
||||||
"expo-image": "~2.0.7",
|
"expo-image": "~2.0.7",
|
||||||
"expo-intent-launcher": "~12.0.2",
|
"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-linear-gradient": "~14.0.2",
|
||||||
"expo-localization": "~16.0.1",
|
"expo-localization": "~16.0.1",
|
||||||
"expo-notifications": "~0.29.14",
|
"expo-notifications": "~0.29.14",
|
||||||
|
|
|
||||||
|
|
@ -643,21 +643,24 @@ const WatchProgressDisplay = memo(({
|
||||||
{/* Enhanced text container with better typography */}
|
{/* Enhanced text container with better typography */}
|
||||||
<View style={styles.watchProgressTextContainer}>
|
<View style={styles.watchProgressTextContainer}>
|
||||||
<View style={styles.progressInfoMain}>
|
<View style={styles.progressInfoMain}>
|
||||||
<Text style={[isTablet ? styles.tabletWatchProgressMainText : styles.watchProgressMainText, {
|
<Text style={[isTablet ? styles.tabletWatchProgressMainText : styles.watchProgressMainText, {
|
||||||
color: isCompleted ? '#00ff88' : currentTheme.colors.white,
|
color: isCompleted ? '#00ff88' : currentTheme.colors.white,
|
||||||
fontSize: isCompleted ? (isTablet ? 15 : 13) : (isTablet ? 14 : 12),
|
fontSize: isCompleted ? (isTablet ? 15 : 13) : (isTablet ? 14 : 12),
|
||||||
fontWeight: isCompleted ? '700' : '600'
|
fontWeight: isCompleted ? '700' : '600'
|
||||||
}]}>
|
}]}>
|
||||||
{progressData.displayText}
|
{progressData.displayText}
|
||||||
</Text>
|
</Text>
|
||||||
|
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
<Text style={[isTablet ? styles.tabletWatchProgressSubText : styles.watchProgressSubText, {
|
{/* Only show episode info for series */}
|
||||||
color: isCompleted ? 'rgba(0,255,136,0.7)' : currentTheme.colors.textMuted,
|
{progressData.episodeInfo && (
|
||||||
}]}>
|
<Text style={[isTablet ? styles.tabletWatchProgressSubText : styles.watchProgressSubText, {
|
||||||
{progressData.episodeInfo}
|
color: isCompleted ? 'rgba(0,255,136,0.7)' : currentTheme.colors.textMuted,
|
||||||
</Text>
|
}]}>
|
||||||
|
{progressData.episodeInfo}
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Trakt sync status with enhanced styling */}
|
{/* Trakt sync status with enhanced styling */}
|
||||||
{progressData.syncStatus && (
|
{progressData.syncStatus && (
|
||||||
|
|
|
||||||
|
|
@ -134,7 +134,9 @@ export function useTraktAutosync(options: TraktAutosyncOptions) {
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
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 contentData = buildContentData();
|
||||||
|
|
||||||
const success = await startWatching(contentData, progressPercent);
|
const success = await startWatching(contentData, progressPercent);
|
||||||
|
|
@ -164,7 +166,8 @@ export function useTraktAutosync(options: TraktAutosyncOptions) {
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const progressPercent = (currentTime / duration) * 100;
|
const rawProgress = (currentTime / duration) * 100;
|
||||||
|
const progressPercent = Math.min(100, Math.max(0, rawProgress));
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
|
|
||||||
// IMMEDIATE SYNC: Use immediate method for user-triggered actions (force=true)
|
// IMMEDIATE SYNC: Use immediate method for user-triggered actions (force=true)
|
||||||
|
|
@ -280,6 +283,8 @@ export function useTraktAutosync(options: TraktAutosyncOptions) {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
let progressPercent = duration > 0 ? (currentTime / duration) * 100 : 0;
|
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
|
// Initial progress calculation logging removed
|
||||||
|
|
||||||
// For unmount calls, always use the highest available progress
|
// For unmount calls, always use the highest available progress
|
||||||
|
|
@ -301,7 +306,7 @@ export function useTraktAutosync(options: TraktAutosyncOptions) {
|
||||||
);
|
);
|
||||||
|
|
||||||
if (savedProgress && savedProgress.duration > 0) {
|
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) {
|
if (savedProgressPercent > maxProgress) {
|
||||||
maxProgress = savedProgressPercent;
|
maxProgress = savedProgressPercent;
|
||||||
}
|
}
|
||||||
|
|
@ -334,10 +339,11 @@ export function useTraktAutosync(options: TraktAutosyncOptions) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// For natural end events, always set progress to at least 90%
|
// For natural end events, ensure we cross Trakt's 80% scrobble threshold reliably.
|
||||||
if (reason === 'ended' && progressPercent < 90) {
|
// If close to the end, boost to 95% to avoid rounding issues.
|
||||||
logger.log(`[TraktAutosync] Natural end detected but progress is low (${progressPercent.toFixed(1)}%), boosting to 90%`);
|
if (reason === 'ended' && progressPercent < 95) {
|
||||||
progressPercent = 90;
|
logger.log(`[TraktAutosync] Natural end detected at ${progressPercent.toFixed(1)}%, boosting to 95% for scrobble`);
|
||||||
|
progressPercent = 95;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Mark stop attempt and update timestamp
|
// Mark stop attempt and update timestamp
|
||||||
|
|
@ -366,6 +372,25 @@ export function useTraktAutosync(options: TraktAutosyncOptions) {
|
||||||
if (progressPercent >= 80) {
|
if (progressPercent >= 80) {
|
||||||
isSessionComplete.current = true;
|
isSessionComplete.current = true;
|
||||||
logger.log(`[TraktAutosync] Session marked as complete (scrobbled) at ${progressPercent.toFixed(1)}%`);
|
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})`);
|
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(
|
async getScrapersToRerun(
|
||||||
type: string,
|
type: string,
|
||||||
|
|
@ -147,14 +147,26 @@ class HybridCacheService {
|
||||||
const validScraperIds = new Set(validResults.map(r => r.scraperId));
|
const validScraperIds = new Set(validResults.map(r => r.scraperId));
|
||||||
const expiredScraperIds = new Set(expiredScrapers);
|
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
|
const scrapersToRerun = availableScrapers
|
||||||
.filter(scraper =>
|
.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);
|
.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;
|
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(
|
async getScrapersToRerun(
|
||||||
type: string,
|
type: string,
|
||||||
|
|
@ -229,14 +229,26 @@ class LocalScraperCacheService {
|
||||||
const validScraperIds = new Set(validResults.map(r => r.scraperId));
|
const validScraperIds = new Set(validResults.map(r => r.scraperId));
|
||||||
const expiredScraperIds = new Set(expiredScrapers);
|
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
|
const scrapersToRerun = availableScrapers
|
||||||
.filter(scraper =>
|
.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);
|
.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;
|
return scrapersToRerun;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -905,16 +905,26 @@ class LocalScraperService {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Determine which scrapers need to be re-run
|
// Determine which scrapers need to be re-run
|
||||||
const scrapersToRerun = enabledScrapers.filter(scraper =>
|
const scrapersToRerun = enabledScrapers.filter(scraper => {
|
||||||
expiredScrapers.includes(scraper.id) || !validResults.some(r => r.scraperId === scraper.id)
|
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) {
|
if (scrapersToRerun.length === 0) {
|
||||||
logger.log('[LocalScraperService] All scrapers have valid cached results');
|
logger.log('[LocalScraperService] All scrapers have valid cached results');
|
||||||
return;
|
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
|
// Generate a lightweight request id for tracing
|
||||||
const requestId = `rs_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 6)}`;
|
const requestId = `rs_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 6)}`;
|
||||||
|
|
|
||||||
|
|
@ -188,19 +188,56 @@ class StremioService {
|
||||||
this.initializationPromise = this.initialize();
|
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 {
|
public isValidContentId(type: string, id: string | null | undefined): boolean {
|
||||||
const isValidType = type === 'movie' || type === 'series';
|
const isValidType = type === 'movie' || type === 'series';
|
||||||
const lowerId = (id || '').toLowerCase();
|
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 isNullishId = !id || lowerId === 'null' || lowerId === 'undefined';
|
||||||
const providerLikeIds = new Set<string>(['moviebox', 'torbox']);
|
const providerLikeIds = new Set<string>(['moviebox', 'torbox']);
|
||||||
const isProviderSlug = providerLikeIds.has(lowerId);
|
const isProviderSlug = providerLikeIds.has(lowerId);
|
||||||
|
|
||||||
if (!isValidType || isNullishId || isProviderSlug) return false;
|
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 {
|
static getInstance(): StremioService {
|
||||||
|
|
@ -723,13 +760,16 @@ class StremioService {
|
||||||
|
|
||||||
async getMetaDetails(type: string, id: string, preferredAddonId?: string): Promise<MetaDetails | null> {
|
async getMetaDetails(type: string, id: string, preferredAddonId?: string): Promise<MetaDetails | null> {
|
||||||
try {
|
try {
|
||||||
|
// Validate content ID first
|
||||||
|
if (!this.isValidContentId(type, id)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
const addons = this.getInstalledAddons();
|
const addons = this.getInstalledAddons();
|
||||||
|
|
||||||
// If a preferred addon is specified, try it first
|
// If a preferred addon is specified, try it first
|
||||||
if (preferredAddonId) {
|
if (preferredAddonId) {
|
||||||
logger.log(`🔍 [getMetaDetails] Looking for preferred addon: ${preferredAddonId}`);
|
|
||||||
const preferredAddon = addons.find(addon => addon.id === preferredAddonId);
|
const preferredAddon = addons.find(addon => addon.id === preferredAddonId);
|
||||||
logger.log(`🔍 [getMetaDetails] Found preferred addon: ${preferredAddon ? preferredAddon.id : 'null'}`);
|
|
||||||
|
|
||||||
if (preferredAddon && preferredAddon.resources) {
|
if (preferredAddon && preferredAddon.resources) {
|
||||||
// Build URL for metadata request
|
// Build URL for metadata request
|
||||||
|
|
@ -739,6 +779,7 @@ class StremioService {
|
||||||
|
|
||||||
// Check if addon supports meta resource for this type
|
// Check if addon supports meta resource for this type
|
||||||
let hasMetaSupport = false;
|
let hasMetaSupport = false;
|
||||||
|
let supportsIdPrefix = false;
|
||||||
|
|
||||||
for (const resource of preferredAddon.resources) {
|
for (const resource of preferredAddon.resources) {
|
||||||
// Check if the current element is a ResourceObject
|
// Check if the current element is a ResourceObject
|
||||||
|
|
@ -748,6 +789,12 @@ class StremioService {
|
||||||
Array.isArray(typedResource.types) &&
|
Array.isArray(typedResource.types) &&
|
||||||
typedResource.types.includes(type)) {
|
typedResource.types.includes(type)) {
|
||||||
hasMetaSupport = true;
|
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;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -755,17 +802,19 @@ class StremioService {
|
||||||
else if (typeof resource === 'string' && resource === 'meta' && preferredAddon.types) {
|
else if (typeof resource === 'string' && resource === 'meta' && preferredAddon.types) {
|
||||||
if (Array.isArray(preferredAddon.types) && preferredAddon.types.includes(type)) {
|
if (Array.isArray(preferredAddon.types) && preferredAddon.types.includes(type)) {
|
||||||
hasMetaSupport = true;
|
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;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.log(`🔍 Meta support check: ${hasMetaSupport} (addon types: ${JSON.stringify(preferredAddon.types)})`);
|
if (hasMetaSupport && supportsIdPrefix) {
|
||||||
|
|
||||||
if (hasMetaSupport) {
|
|
||||||
try {
|
try {
|
||||||
logger.log(`🔗 [${preferredAddon.name}] Requesting metadata: ${url} (preferred, id=${id}, type=${type})`);
|
|
||||||
|
|
||||||
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 });
|
||||||
});
|
});
|
||||||
|
|
@ -773,7 +822,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) {
|
||||||
// Continue trying other addons
|
// Continue trying other addons
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -785,7 +834,7 @@ class StremioService {
|
||||||
'https://v3-cinemeta.strem.io',
|
'https://v3-cinemeta.strem.io',
|
||||||
'http://v3-cinemeta.strem.io'
|
'http://v3-cinemeta.strem.io'
|
||||||
];
|
];
|
||||||
|
|
||||||
for (const baseUrl of cinemetaUrls) {
|
for (const baseUrl of cinemetaUrls) {
|
||||||
try {
|
try {
|
||||||
const encodedId = encodeURIComponent(id);
|
const encodedId = encodeURIComponent(id);
|
||||||
|
|
@ -804,7 +853,6 @@ class StremioService {
|
||||||
}
|
}
|
||||||
|
|
||||||
// If Cinemeta fails, try other addons (excluding the preferred one already tried)
|
// If Cinemeta fails, try other addons (excluding the preferred one already tried)
|
||||||
|
|
||||||
for (const addon of addons) {
|
for (const addon of addons) {
|
||||||
if (!addon.resources || addon.id === 'com.linvo.cinemeta' || addon.id === preferredAddonId) continue;
|
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
|
// Require both meta support and idPrefix compatibility
|
||||||
if (!(hasMetaSupport && supportsIdPrefix)) continue;
|
if (!(hasMetaSupport && supportsIdPrefix)) continue;
|
||||||
|
|
||||||
|
|
@ -851,7 +900,6 @@ class StremioService {
|
||||||
const encodedId = encodeURIComponent(id);
|
const encodedId = encodeURIComponent(id);
|
||||||
const url = queryParams ? `${baseUrl}/meta/${type}/${encodedId}.json?${queryParams}` : `${baseUrl}/meta/${type}/${encodedId}.json`;
|
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 () => {
|
const response = await this.retryRequest(async () => {
|
||||||
return await axios.get(url, { timeout: 10000 });
|
return await axios.get(url, { timeout: 10000 });
|
||||||
});
|
});
|
||||||
|
|
@ -860,15 +908,10 @@ class StremioService {
|
||||||
return response.data.meta;
|
return response.data.meta;
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.warn(`❌ Failed to fetch meta from ${addon.name} (${addon.id}):`, error);
|
|
||||||
continue; // Try next addon
|
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;
|
return null;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('Error in getMetaDetails:', error);
|
logger.error('Error in getMetaDetails:', error);
|
||||||
|
|
@ -1476,6 +1519,7 @@ class StremioService {
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const stremioService = StremioService.getInstance();
|
export const stremioService = StremioService.getInstance();
|
||||||
|
|
|
||||||
|
|
@ -1547,6 +1547,9 @@ export class TraktService {
|
||||||
*/
|
*/
|
||||||
private async buildScrobblePayload(contentData: TraktContentData, progress: number): Promise<any | null> {
|
private async buildScrobblePayload(contentData: TraktContentData, progress: number): Promise<any | null> {
|
||||||
try {
|
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
|
// Enhanced debug logging for payload building
|
||||||
logger.log('[TraktService] Building scrobble payload:', {
|
logger.log('[TraktService] Building scrobble payload:', {
|
||||||
type: contentData.type,
|
type: contentData.type,
|
||||||
|
|
@ -1558,7 +1561,7 @@ export class TraktService {
|
||||||
showTitle: contentData.showTitle,
|
showTitle: contentData.showTitle,
|
||||||
showYear: contentData.showYear,
|
showYear: contentData.showYear,
|
||||||
showImdbId: contentData.showImdbId,
|
showImdbId: contentData.showImdbId,
|
||||||
progress: progress
|
progress: clampedProgress
|
||||||
});
|
});
|
||||||
|
|
||||||
if (contentData.type === 'movie') {
|
if (contentData.type === 'movie') {
|
||||||
|
|
@ -1583,7 +1586,7 @@ export class TraktService {
|
||||||
imdb: imdbIdWithPrefix
|
imdb: imdbIdWithPrefix
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
progress: Math.round(progress * 100) / 100 // Round to 2 decimal places
|
progress: clampedProgress
|
||||||
};
|
};
|
||||||
|
|
||||||
logger.log('[TraktService] Movie payload built:', payload);
|
logger.log('[TraktService] Movie payload built:', payload);
|
||||||
|
|
@ -1609,7 +1612,7 @@ export class TraktService {
|
||||||
season: contentData.season,
|
season: contentData.season,
|
||||||
number: contentData.episode
|
number: contentData.episode
|
||||||
},
|
},
|
||||||
progress: Math.round(progress * 100) / 100
|
progress: clampedProgress
|
||||||
};
|
};
|
||||||
|
|
||||||
// Add show IMDB ID if available
|
// Add show IMDB ID if available
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue