mirror of
https://github.com/tapframe/NuvioStreaming.git
synced 2026-01-11 20:10:25 +00:00
embedded streams fix
This commit is contained in:
parent
3c35b99759
commit
c01528b309
8 changed files with 375 additions and 274 deletions
|
|
@ -1,4 +1,4 @@
|
|||
defaults.url=https://sentry.io/
|
||||
defaults.org=tapframe
|
||||
defaults.project=react-native
|
||||
auth.token=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
|
||||
# Using SENTRY_AUTH_TOKEN environment variable
|
||||
|
|
@ -477,7 +477,7 @@
|
|||
);
|
||||
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_DEBUG";
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.nuvio.app;
|
||||
PRODUCT_NAME = Nuvio;
|
||||
PRODUCT_NAME = "Nuvio";
|
||||
SWIFT_OBJC_BRIDGING_HEADER = "Nuvio/Nuvio-Bridging-Header.h";
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||
SWIFT_VERSION = 5.0;
|
||||
|
|
@ -508,8 +508,8 @@
|
|||
"-lc++",
|
||||
);
|
||||
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE";
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.nuvio.hub;
|
||||
PRODUCT_NAME = Nuvio;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "com.nuvio.app";
|
||||
PRODUCT_NAME = "Nuvio";
|
||||
SWIFT_OBJC_BRIDGING_HEADER = "Nuvio/Nuvio-Bridging-Header.h";
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
|
|
|
|||
|
|
@ -1,99 +1,103 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CADisableMinimumFrameDurationOnPhone</key>
|
||||
<true/>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
||||
<key>CFBundleDisplayName</key>
|
||||
<string>Nuvio</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>$(EXECUTABLE_NAME)</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>$(PRODUCT_NAME)</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>1.2.10</string>
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
<key>CFBundleURLTypes</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>CFBundleURLSchemes</key>
|
||||
<array>
|
||||
<string>nuvio</string>
|
||||
<string>com.nuvio.app</string>
|
||||
</array>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>CFBundleURLSchemes</key>
|
||||
<array>
|
||||
<string>exp+nuvio</string>
|
||||
</array>
|
||||
</dict>
|
||||
</array>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>25</string>
|
||||
<key>LSMinimumSystemVersion</key>
|
||||
<string>12.0</string>
|
||||
<key>LSRequiresIPhoneOS</key>
|
||||
<true/>
|
||||
<key>LSSupportsOpeningDocumentsInPlace</key>
|
||||
<true/>
|
||||
<key>NSAppTransportSecurity</key>
|
||||
<dict>
|
||||
<key>NSAllowsArbitraryLoads</key>
|
||||
<true/>
|
||||
</dict>
|
||||
<key>NSBonjourServices</key>
|
||||
<array>
|
||||
<string>_http._tcp</string>
|
||||
<string>_googlecast._tcp</string>
|
||||
<string>_CC1AD845._googlecast._tcp</string>
|
||||
</array>
|
||||
<key>RCTNewArchEnabled</key>
|
||||
<true/>
|
||||
<key>RCTRootViewBackgroundColor</key>
|
||||
<integer>4278322180</integer>
|
||||
<key>UIBackgroundModes</key>
|
||||
<array>
|
||||
<string>audio</string>
|
||||
</array>
|
||||
<key>UIFileSharingEnabled</key>
|
||||
<true/>
|
||||
<key>UILaunchStoryboardName</key>
|
||||
<string>SplashScreen</string>
|
||||
<key>UIRequiredDeviceCapabilities</key>
|
||||
<array>
|
||||
<string>arm64</string>
|
||||
</array>
|
||||
<key>UIRequiresFullScreen</key>
|
||||
<true/>
|
||||
<key>UIStatusBarStyle</key>
|
||||
<string>UIStatusBarStyleDefault</string>
|
||||
<key>UISupportedInterfaceOrientations</key>
|
||||
<array>
|
||||
<string>UIInterfaceOrientationPortrait</string>
|
||||
<string>UIInterfaceOrientationPortraitUpsideDown</string>
|
||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||
</array>
|
||||
<key>UISupportedInterfaceOrientations~ipad</key>
|
||||
<array>
|
||||
<string>UIInterfaceOrientationPortrait</string>
|
||||
<string>UIInterfaceOrientationPortraitUpsideDown</string>
|
||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||
</array>
|
||||
<key>UIUserInterfaceStyle</key>
|
||||
<string>Dark</string>
|
||||
<key>UIViewControllerBasedStatusBarAppearance</key>
|
||||
<false/>
|
||||
</dict>
|
||||
</plist>
|
||||
<dict>
|
||||
<key>CADisableMinimumFrameDurationOnPhone</key>
|
||||
<true/>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
||||
<key>CFBundleDisplayName</key>
|
||||
<string>Nuvio</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>$(EXECUTABLE_NAME)</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>$(PRODUCT_NAME)</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>1.2.10</string>
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
<key>CFBundleURLTypes</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>CFBundleURLSchemes</key>
|
||||
<array>
|
||||
<string>nuvio</string>
|
||||
<string>com.nuvio.app</string>
|
||||
</array>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>CFBundleURLSchemes</key>
|
||||
<array>
|
||||
<string>exp+nuvio</string>
|
||||
</array>
|
||||
</dict>
|
||||
</array>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>25</string>
|
||||
<key>LSMinimumSystemVersion</key>
|
||||
<string>12.0</string>
|
||||
<key>LSRequiresIPhoneOS</key>
|
||||
<true/>
|
||||
<key>LSSupportsOpeningDocumentsInPlace</key>
|
||||
<true/>
|
||||
<key>NSAppTransportSecurity</key>
|
||||
<dict>
|
||||
<key>NSAllowsArbitraryLoads</key>
|
||||
<true/>
|
||||
</dict>
|
||||
<key>NSBonjourServices</key>
|
||||
<array>
|
||||
<string>_http._tcp</string>
|
||||
<string>_googlecast._tcp</string>
|
||||
<string>_CC1AD845._googlecast._tcp</string>
|
||||
</array>
|
||||
<key>NSLocalNetworkUsageDescription</key>
|
||||
<string>Allow $(PRODUCT_NAME) to access your local network</string>
|
||||
<key>NSMicrophoneUsageDescription</key>
|
||||
<string>This app does not require microphone access.</string>
|
||||
<key>RCTNewArchEnabled</key>
|
||||
<true/>
|
||||
<key>RCTRootViewBackgroundColor</key>
|
||||
<integer>4278322180</integer>
|
||||
<key>UIBackgroundModes</key>
|
||||
<array>
|
||||
<string>audio</string>
|
||||
</array>
|
||||
<key>UIFileSharingEnabled</key>
|
||||
<true/>
|
||||
<key>UILaunchStoryboardName</key>
|
||||
<string>SplashScreen</string>
|
||||
<key>UIRequiredDeviceCapabilities</key>
|
||||
<array>
|
||||
<string>arm64</string>
|
||||
</array>
|
||||
<key>UIRequiresFullScreen</key>
|
||||
<true/>
|
||||
<key>UIStatusBarStyle</key>
|
||||
<string>UIStatusBarStyleDefault</string>
|
||||
<key>UISupportedInterfaceOrientations</key>
|
||||
<array>
|
||||
<string>UIInterfaceOrientationPortrait</string>
|
||||
<string>UIInterfaceOrientationPortraitUpsideDown</string>
|
||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||
</array>
|
||||
<key>UISupportedInterfaceOrientations~ipad</key>
|
||||
<array>
|
||||
<string>UIInterfaceOrientationPortrait</string>
|
||||
<string>UIInterfaceOrientationPortraitUpsideDown</string>
|
||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||
</array>
|
||||
<key>UIUserInterfaceStyle</key>
|
||||
<string>Dark</string>
|
||||
<key>UIViewControllerBasedStatusBarAppearance</key>
|
||||
<false/>
|
||||
</dict>
|
||||
</plist>
|
||||
|
|
@ -1,8 +1,10 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>aps-environment</key>
|
||||
<string>development</string>
|
||||
</dict>
|
||||
</plist>
|
||||
<dict>
|
||||
<key>aps-environment</key>
|
||||
<string>development</string>
|
||||
<key>com.apple.developer.associated-domains</key>
|
||||
<array/>
|
||||
</dict>
|
||||
</plist>
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
defaults.url=https://sentry.io/
|
||||
defaults.org=tapframe
|
||||
defaults.project=react-native
|
||||
auth.token=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
|
||||
# Using SENTRY_AUTH_TOKEN environment variable
|
||||
|
|
@ -1064,10 +1064,8 @@ export const useMetadata = ({ id, type, addonId }: UseMetadataProps): UseMetadat
|
|||
const groupedAddonEpisodes: GroupedEpisodes = {};
|
||||
|
||||
addonVideos.forEach((video: any) => {
|
||||
const seasonNumber = video.season;
|
||||
if (!seasonNumber || seasonNumber < 1) {
|
||||
return; // Skip season 0, which often contains extras
|
||||
}
|
||||
// Use season 0 for videos without season numbers (PPV-style content, specials, etc.)
|
||||
const seasonNumber = video.season || 0;
|
||||
const episodeNumber = video.episode || video.number || 1;
|
||||
|
||||
if (!groupedAddonEpisodes[seasonNumber]) {
|
||||
|
|
@ -1318,6 +1316,60 @@ export const useMetadata = ({ id, type, addonId }: UseMetadataProps): UseMetadat
|
|||
setError(null);
|
||||
};
|
||||
|
||||
// Extract embedded streams from metadata videos (used by PPV-style addons)
|
||||
const extractEmbeddedStreams = useCallback(() => {
|
||||
if (!metadata?.videos) return;
|
||||
|
||||
// Check if any video has embedded streams
|
||||
const videosWithStreams = (metadata.videos as any[]).filter(
|
||||
(video: any) => video.streams && Array.isArray(video.streams) && video.streams.length > 0
|
||||
);
|
||||
|
||||
if (videosWithStreams.length === 0) return;
|
||||
|
||||
// Get the addon info from metadata if available
|
||||
const addonId = (metadata as any).addonId || 'embedded';
|
||||
const addonName = (metadata as any).addonName || metadata.name || 'Embedded Streams';
|
||||
|
||||
// Extract all streams from videos
|
||||
const embeddedStreams: Stream[] = [];
|
||||
for (const video of videosWithStreams) {
|
||||
for (const stream of video.streams) {
|
||||
embeddedStreams.push({
|
||||
...stream,
|
||||
name: stream.name || stream.title || video.title,
|
||||
title: stream.title || video.title,
|
||||
addonId,
|
||||
addonName,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (embeddedStreams.length > 0) {
|
||||
if (__DEV__) console.log(`✅ [extractEmbeddedStreams] Found ${embeddedStreams.length} embedded streams from ${addonName}`);
|
||||
|
||||
// Add to grouped streams
|
||||
setGroupedStreams(prevStreams => ({
|
||||
...prevStreams,
|
||||
[addonId]: {
|
||||
addonName,
|
||||
streams: embeddedStreams,
|
||||
},
|
||||
}));
|
||||
|
||||
// Track addon response order
|
||||
setAddonResponseOrder(prevOrder => {
|
||||
if (!prevOrder.includes(addonId)) {
|
||||
return [...prevOrder, addonId];
|
||||
}
|
||||
return prevOrder;
|
||||
});
|
||||
|
||||
// Mark loading as complete since we have streams
|
||||
setLoadingStreams(false);
|
||||
}
|
||||
}, [metadata]);
|
||||
|
||||
const loadStreams = async () => {
|
||||
const startTime = Date.now();
|
||||
try {
|
||||
|
|
@ -1478,6 +1530,9 @@ export const useMetadata = ({ id, type, addonId }: UseMetadataProps): UseMetadat
|
|||
if (__DEV__) console.log('🎬 [loadStreams] Using ID for Stremio addons:', stremioId);
|
||||
processStremioSource(type, stremioId, false);
|
||||
|
||||
// Also extract any embedded streams from metadata (PPV-style addons)
|
||||
extractEmbeddedStreams();
|
||||
|
||||
// Monitor scraper completion status instead of using fixed timeout
|
||||
const checkScrapersCompletion = () => {
|
||||
setScraperStatuses(currentStatuses => {
|
||||
|
|
@ -1814,8 +1869,10 @@ export const useMetadata = ({ id, type, addonId }: UseMetadataProps): UseMetadat
|
|||
if (metadata && metadata.videos && metadata.videos.length > 0) {
|
||||
logger.log(`🎬 Metadata updated with ${metadata.videos.length} episodes, reloading series data`);
|
||||
loadSeriesData().catch((error) => { if (__DEV__) console.error(error); });
|
||||
// Also extract embedded streams from metadata videos (PPV-style addons)
|
||||
extractEmbeddedStreams();
|
||||
}
|
||||
}, [metadata?.videos, type]);
|
||||
}, [metadata?.videos, type, extractEmbeddedStreams]);
|
||||
|
||||
const loadRecommendations = useCallback(async () => {
|
||||
if (!settings.enrichMetadataWithTMDB) {
|
||||
|
|
|
|||
|
|
@ -410,9 +410,9 @@ export const StreamsScreen = () => {
|
|||
isLoadingStreamsRef.current = true;
|
||||
|
||||
try {
|
||||
// Check for Stremio addons
|
||||
const hasStremioProviders = await stremioService.hasStreamProviders();
|
||||
if (__DEV__) console.log('[StreamsScreen] hasStremioProviders:', hasStremioProviders);
|
||||
// Check for Stremio addons that support this content type (including embedded streams)
|
||||
const hasStremioProviders = await stremioService.hasStreamProviders(type);
|
||||
if (__DEV__) console.log('[StreamsScreen] hasStremioProviders:', hasStremioProviders, 'for type:', type);
|
||||
|
||||
// Check for local scrapers (only if enabled in settings)
|
||||
const hasLocalScrapers = settings.enableLocalScrapers && await localScraperService.hasScrapers();
|
||||
|
|
|
|||
|
|
@ -153,6 +153,8 @@ export interface MetaDetails extends Meta {
|
|||
released: string;
|
||||
season?: number;
|
||||
episode?: number;
|
||||
thumbnail?: string;
|
||||
streams?: Stream[]; // Embedded streams (used by PPV-style addons)
|
||||
}[];
|
||||
}
|
||||
|
||||
|
|
@ -194,26 +196,26 @@ class StremioService {
|
|||
public async isValidContentId(type: string, id: string | null | undefined): Promise<boolean> {
|
||||
// Ensure addons are initialized before checking types
|
||||
await this.ensureInitialized();
|
||||
|
||||
|
||||
// Get all supported types from installed addons
|
||||
const supportedTypes = this.getAllSupportedTypes();
|
||||
const isValidType = supportedTypes.includes(type);
|
||||
|
||||
|
||||
const lowerId = (id || '').toLowerCase();
|
||||
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;
|
||||
|
||||
|
||||
// Get all supported ID prefixes from installed addons
|
||||
const supportedPrefixes = this.getAllSupportedIdPrefixes(type);
|
||||
|
||||
|
||||
// If no addons declare specific prefixes, allow any non-empty string
|
||||
if (supportedPrefixes.length === 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
// Check if the ID matches any supported prefix
|
||||
return supportedPrefixes.some(prefix => lowerId.startsWith(prefix.toLowerCase()));
|
||||
}
|
||||
|
|
@ -222,13 +224,13 @@ class StremioService {
|
|||
public getAllSupportedTypes(): string[] {
|
||||
const addons = this.getInstalledAddons();
|
||||
const types = new Set<string>();
|
||||
|
||||
|
||||
for (const addon of addons) {
|
||||
// Check addon-level types
|
||||
if (addon.types && Array.isArray(addon.types)) {
|
||||
addon.types.forEach(type => types.add(type));
|
||||
}
|
||||
|
||||
|
||||
// Check resource-level types
|
||||
if (addon.resources && Array.isArray(addon.resources)) {
|
||||
for (const resource of addon.resources) {
|
||||
|
|
@ -240,7 +242,7 @@ class StremioService {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Check catalog-level types
|
||||
if (addon.catalogs && Array.isArray(addon.catalogs)) {
|
||||
for (const catalog of addon.catalogs) {
|
||||
|
|
@ -250,7 +252,7 @@ class StremioService {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return Array.from(types);
|
||||
}
|
||||
|
||||
|
|
@ -258,13 +260,13 @@ class StremioService {
|
|||
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) {
|
||||
|
|
@ -280,34 +282,34 @@ class StremioService {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return Array.from(prefixes);
|
||||
}
|
||||
|
||||
// Check if a content ID belongs to a collection addon
|
||||
public isCollectionContent(id: string): { isCollection: boolean; addon?: Manifest } {
|
||||
const addons = this.getInstalledAddons();
|
||||
|
||||
|
||||
for (const addon of addons) {
|
||||
// Check if this addon supports collections
|
||||
const supportsCollections = addon.types?.includes('collections') ||
|
||||
addon.catalogs?.some(catalog => catalog.type === 'collections');
|
||||
|
||||
const supportsCollections = addon.types?.includes('collections') ||
|
||||
addon.catalogs?.some(catalog => catalog.type === 'collections');
|
||||
|
||||
if (!supportsCollections) continue;
|
||||
|
||||
|
||||
// Check if our ID matches this addon's prefixes
|
||||
const addonPrefixes = addon.idPrefixes || [];
|
||||
const resourcePrefixes = addon.resources
|
||||
?.filter(resource => typeof resource === 'object' && resource !== null && 'name' in resource)
|
||||
?.filter(resource => (resource as any).name === 'meta' || (resource as any).name === 'catalog')
|
||||
?.flatMap(resource => (resource as any).idPrefixes || []) || [];
|
||||
|
||||
|
||||
const allPrefixes = [...addonPrefixes, ...resourcePrefixes];
|
||||
if (allPrefixes.some(prefix => id.startsWith(prefix))) {
|
||||
return { isCollection: true, addon };
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return { isCollection: false };
|
||||
}
|
||||
|
||||
|
|
@ -320,17 +322,17 @@ class StremioService {
|
|||
|
||||
private async initialize(): Promise<void> {
|
||||
if (this.initialized) return;
|
||||
|
||||
|
||||
try {
|
||||
const scope = (await mmkvStorage.getItem('@user:current')) || 'local';
|
||||
// Prefer scoped storage, but fall back to legacy keys to preserve older installs
|
||||
let storedAddons = await mmkvStorage.getItem(`@user:${scope}:${this.STORAGE_KEY}`);
|
||||
if (!storedAddons) storedAddons = await mmkvStorage.getItem(this.STORAGE_KEY);
|
||||
if (!storedAddons) storedAddons = await mmkvStorage.getItem(`@user:local:${this.STORAGE_KEY}`);
|
||||
|
||||
|
||||
if (storedAddons) {
|
||||
const parsed = JSON.parse(storedAddons);
|
||||
|
||||
|
||||
// Convert to Map
|
||||
this.installedAddons = new Map();
|
||||
for (const addon of parsed) {
|
||||
|
|
@ -339,11 +341,11 @@ class StremioService {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Install Cinemeta for new users, but allow existing users to uninstall it
|
||||
const cinemetaId = 'com.linvo.cinemeta';
|
||||
const hasUserRemovedCinemeta = await this.hasUserRemovedAddon(cinemetaId);
|
||||
|
||||
|
||||
if (!this.installedAddons.has(cinemetaId) && !hasUserRemovedCinemeta) {
|
||||
try {
|
||||
const cinemetaManifest = await this.getManifest('https://v3-cinemeta.strem.io/manifest.json');
|
||||
|
|
@ -395,7 +397,7 @@ class StremioService {
|
|||
// Install OpenSubtitles v3 by default unless user has explicitly removed it
|
||||
const opensubsId = 'org.stremio.opensubtitlesv3';
|
||||
const hasUserRemovedOpenSubtitles = await this.hasUserRemovedAddon(opensubsId);
|
||||
|
||||
|
||||
if (!this.installedAddons.has(opensubsId) && !hasUserRemovedOpenSubtitles) {
|
||||
try {
|
||||
const opensubsManifest = await this.getManifest('https://opensubtitles-v3.strem.io/manifest.json');
|
||||
|
|
@ -424,7 +426,7 @@ class StremioService {
|
|||
this.installedAddons.set(opensubsId, fallbackManifest);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Load addon order if exists (scoped first, then legacy, then @user:local for migration safety)
|
||||
let storedOrder = await mmkvStorage.getItem(`@user:${scope}:${this.ADDON_ORDER_KEY}`);
|
||||
if (!storedOrder) storedOrder = await mmkvStorage.getItem(this.ADDON_ORDER_KEY);
|
||||
|
|
@ -434,28 +436,28 @@ class StremioService {
|
|||
// Filter out any ids that aren't in installedAddons
|
||||
this.addonOrder = this.addonOrder.filter(id => this.installedAddons.has(id));
|
||||
}
|
||||
|
||||
|
||||
// Add Cinemeta to order only if user hasn't removed it
|
||||
const hasUserRemovedCinemetaOrder = await this.hasUserRemovedAddon(cinemetaId);
|
||||
if (!this.addonOrder.includes(cinemetaId) && this.installedAddons.has(cinemetaId) && !hasUserRemovedCinemetaOrder) {
|
||||
this.addonOrder.push(cinemetaId);
|
||||
}
|
||||
|
||||
|
||||
// Only add OpenSubtitles to order if user hasn't removed it
|
||||
const hasUserRemovedOpenSubtitlesOrder = await this.hasUserRemovedAddon(opensubsId);
|
||||
if (!this.addonOrder.includes(opensubsId) && this.installedAddons.has(opensubsId) && !hasUserRemovedOpenSubtitlesOrder) {
|
||||
this.addonOrder.push(opensubsId);
|
||||
}
|
||||
|
||||
|
||||
// Add any missing addons to the order
|
||||
const installedIds = Array.from(this.installedAddons.keys());
|
||||
const missingIds = installedIds.filter(id => !this.addonOrder.includes(id));
|
||||
this.addonOrder = [...this.addonOrder, ...missingIds];
|
||||
|
||||
|
||||
// Ensure order and addons are saved
|
||||
await this.saveAddonOrder();
|
||||
await this.saveInstalledAddons();
|
||||
|
||||
|
||||
this.initialized = true;
|
||||
} catch (error) {
|
||||
// Initialize with empty state on error
|
||||
|
|
@ -479,12 +481,12 @@ class StremioService {
|
|||
return await request();
|
||||
} catch (error: any) {
|
||||
lastError = error;
|
||||
|
||||
|
||||
// Don't retry on 404 errors (content not found) - these are expected for some content
|
||||
if (error.response?.status === 404) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
|
||||
// Only log warnings for non-404 errors to reduce noise
|
||||
if (error.response?.status !== 404) {
|
||||
logger.warn(`Request failed (attempt ${attempt + 1}/${retries + 1}):`, {
|
||||
|
|
@ -494,7 +496,7 @@ class StremioService {
|
|||
status: error.response?.status,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
if (attempt < retries) {
|
||||
const backoffDelay = delay * Math.pow(2, attempt);
|
||||
logger.log(`Retrying in ${backoffDelay}ms...`);
|
||||
|
|
@ -535,25 +537,25 @@ class StremioService {
|
|||
async getManifest(url: string): Promise<Manifest> {
|
||||
try {
|
||||
// Clean up URL - ensure it ends with manifest.json
|
||||
const manifestUrl = url.endsWith('manifest.json')
|
||||
? url
|
||||
const manifestUrl = url.endsWith('manifest.json')
|
||||
? url
|
||||
: `${url.replace(/\/$/, '')}/manifest.json`;
|
||||
|
||||
|
||||
const response = await this.retryRequest(async () => {
|
||||
return await axios.get(manifestUrl);
|
||||
});
|
||||
|
||||
|
||||
const manifest = response.data;
|
||||
|
||||
|
||||
// Add some extra fields for internal use
|
||||
manifest.originalUrl = url;
|
||||
manifest.url = url.replace(/manifest\.json$/, '');
|
||||
|
||||
|
||||
// Ensure ID exists
|
||||
if (!manifest.id) {
|
||||
manifest.id = this.formatId(url);
|
||||
}
|
||||
|
||||
|
||||
return manifest;
|
||||
} catch (error) {
|
||||
logger.error(`Failed to fetch manifest from ${url}:`, error);
|
||||
|
|
@ -565,16 +567,16 @@ class StremioService {
|
|||
const manifest = await this.getManifest(url);
|
||||
if (manifest && manifest.id) {
|
||||
this.installedAddons.set(manifest.id, manifest);
|
||||
|
||||
|
||||
// If addon was previously removed by user, unmark it on reinstall and clean up
|
||||
await this.unmarkAddonAsRemovedByUser(manifest.id);
|
||||
await this.cleanupRemovedAddonFromStorage(manifest.id);
|
||||
|
||||
|
||||
// Add to order if not already present (new addons go to the end)
|
||||
if (!this.addonOrder.includes(manifest.id)) {
|
||||
this.addonOrder.push(manifest.id);
|
||||
}
|
||||
|
||||
|
||||
await this.saveInstalledAddons();
|
||||
await this.saveAddonOrder();
|
||||
// Emit an event that an addon was added
|
||||
|
|
@ -641,7 +643,7 @@ class StremioService {
|
|||
const removedAddons = await mmkvStorage.getItem('user_removed_addons');
|
||||
let removedList = removedAddons ? JSON.parse(removedAddons) : [];
|
||||
if (!Array.isArray(removedList)) removedList = [];
|
||||
|
||||
|
||||
if (!removedList.includes(addonId)) {
|
||||
removedList.push(addonId);
|
||||
await mmkvStorage.setItem('user_removed_addons', JSON.stringify(removedList));
|
||||
|
|
@ -656,10 +658,10 @@ class StremioService {
|
|||
try {
|
||||
const removedAddons = await mmkvStorage.getItem('user_removed_addons');
|
||||
if (!removedAddons) return;
|
||||
|
||||
|
||||
let removedList = JSON.parse(removedAddons);
|
||||
if (!Array.isArray(removedList)) return;
|
||||
|
||||
|
||||
const updatedList = removedList.filter(id => id !== addonId);
|
||||
await mmkvStorage.setItem('user_removed_addons', JSON.stringify(updatedList));
|
||||
} catch (error) {
|
||||
|
|
@ -671,14 +673,14 @@ class StremioService {
|
|||
private async cleanupRemovedAddonFromStorage(addonId: string): Promise<void> {
|
||||
try {
|
||||
const scope = (await mmkvStorage.getItem('@user:current')) || 'local';
|
||||
|
||||
|
||||
// Remove from all possible addon order storage keys
|
||||
const keys = [
|
||||
`@user:${scope}:${this.ADDON_ORDER_KEY}`,
|
||||
this.ADDON_ORDER_KEY,
|
||||
`@user:local:${this.ADDON_ORDER_KEY}`
|
||||
];
|
||||
|
||||
|
||||
for (const key of keys) {
|
||||
const storedOrder = await mmkvStorage.getItem(key);
|
||||
if (storedOrder) {
|
||||
|
|
@ -701,12 +703,12 @@ class StremioService {
|
|||
async getAllCatalogs(): Promise<{ [addonId: string]: Meta[] }> {
|
||||
const result: { [addonId: string]: Meta[] } = {};
|
||||
const addons = this.getInstalledAddons();
|
||||
|
||||
|
||||
const promises = addons.map(async (addon) => {
|
||||
if (!addon.catalogs || addon.catalogs.length === 0) return;
|
||||
|
||||
|
||||
const catalog = addon.catalogs[0]; // Just take the first catalog for now
|
||||
|
||||
|
||||
try {
|
||||
const items = await this.getCatalog(addon, catalog.type, catalog.id);
|
||||
if (items.length > 0) {
|
||||
|
|
@ -716,7 +718,7 @@ class StremioService {
|
|||
logger.error(`Failed to fetch catalog from ${addon.name}:`, error);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
await Promise.all(promises);
|
||||
return result;
|
||||
}
|
||||
|
|
@ -724,15 +726,15 @@ class StremioService {
|
|||
private getAddonBaseURL(url: string): { baseUrl: string; queryParams?: string } {
|
||||
// Extract query parameters if they exist
|
||||
const [baseUrl, queryString] = url.split('?');
|
||||
|
||||
|
||||
// Remove trailing manifest.json and slashes
|
||||
let cleanBaseUrl = baseUrl.replace(/manifest\.json$/, '').replace(/\/$/, '');
|
||||
|
||||
|
||||
// Ensure URL has protocol
|
||||
if (!cleanBaseUrl.startsWith('http')) {
|
||||
cleanBaseUrl = `https://${cleanBaseUrl}`;
|
||||
}
|
||||
|
||||
|
||||
return { baseUrl: cleanBaseUrl, queryParams: queryString };
|
||||
}
|
||||
|
||||
|
|
@ -744,13 +746,14 @@ class StremioService {
|
|||
.filter(f => f && f.value)
|
||||
.map(f => `&${encodeURIComponent(f.title)}=${encodeURIComponent(f.value!)}`)
|
||||
.join('');
|
||||
|
||||
|
||||
// For all addons
|
||||
if (!manifest.url) {
|
||||
throw new Error('Addon URL is missing');
|
||||
}
|
||||
|
||||
|
||||
try {
|
||||
if (__DEV__) console.log(`🔍 [getCatalog] Manifest URL for ${manifest.name}: ${manifest.url}`);
|
||||
const { baseUrl, queryParams } = this.getAddonBaseURL(manifest.url);
|
||||
// Candidate 1: Path-style skip URL: /catalog/{type}/{id}/skip={N}.json
|
||||
const urlPathStyle = `${baseUrl}/catalog/${type}/${encodedId}/skip=${pageSkip}.json${queryParams ? `?${queryParams}` : ''}`;
|
||||
|
|
@ -762,15 +765,40 @@ class StremioService {
|
|||
if (queryParams) urlQueryStyle += `&${queryParams}`;
|
||||
urlQueryStyle += filterQuery;
|
||||
|
||||
// Try path-style first, then fallback to query-style
|
||||
// For page 1, also try simple URL without skip (some addons don't support skip)
|
||||
const urlSimple = `${baseUrl}/catalog/${type}/${encodedId}.json${queryParams ? `?${queryParams}` : ''}`;
|
||||
const urlSimpleWithFilters = urlSimple + (urlSimple.includes('?') ? filterQuery : (filterQuery ? `?${filterQuery.slice(1)}` : ''));
|
||||
|
||||
// Try URLs in order of compatibility: simple (page 1 only), path-style, query-style
|
||||
let response;
|
||||
try {
|
||||
response = await this.retryRequest(async () => axios.get(urlPathWithFilters));
|
||||
// For page 1, try simple URL first (best compatibility)
|
||||
if (pageSkip === 0) {
|
||||
if (__DEV__) console.log(`🔍 [getCatalog] Trying simple URL for ${manifest.name}: ${urlSimpleWithFilters}`);
|
||||
response = await this.retryRequest(async () => axios.get(urlSimpleWithFilters));
|
||||
// Check if we got valid metas - if empty, try other styles
|
||||
if (!response?.data?.metas || response.data.metas.length === 0) {
|
||||
throw new Error('Empty response from simple URL');
|
||||
}
|
||||
} else {
|
||||
throw new Error('Not page 1, skip to path-style');
|
||||
}
|
||||
} catch (e) {
|
||||
try {
|
||||
response = await this.retryRequest(async () => axios.get(urlQueryStyle));
|
||||
if (__DEV__) console.log(`🔍 [getCatalog] Trying path-style URL for ${manifest.name}: ${urlPathWithFilters}`);
|
||||
response = await this.retryRequest(async () => axios.get(urlPathWithFilters));
|
||||
// Check if we got valid metas - if empty, try query-style
|
||||
if (!response?.data?.metas || response.data.metas.length === 0) {
|
||||
throw new Error('Empty response from path-style URL');
|
||||
}
|
||||
} catch (e2) {
|
||||
throw e2;
|
||||
try {
|
||||
if (__DEV__) console.log(`🔍 [getCatalog] Trying query-style URL for ${manifest.name}: ${urlQueryStyle}`);
|
||||
response = await this.retryRequest(async () => axios.get(urlQueryStyle));
|
||||
} catch (e3) {
|
||||
if (__DEV__) console.log(`❌ [getCatalog] All URL styles failed for ${manifest.name}`);
|
||||
throw e3;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -779,7 +807,7 @@ class StremioService {
|
|||
try {
|
||||
const key = `${manifest.id}|${type}|${id}`;
|
||||
if (typeof hasMore === 'boolean') this.catalogHasMore.set(key, hasMore);
|
||||
} catch {}
|
||||
} catch { }
|
||||
if (response.data.metas && Array.isArray(response.data.metas)) {
|
||||
return response.data.metas;
|
||||
}
|
||||
|
|
@ -800,13 +828,13 @@ class StremioService {
|
|||
try {
|
||||
// Validate content ID first
|
||||
const isValidId = await this.isValidContentId(type, id);
|
||||
|
||||
|
||||
if (!isValidId) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
const addons = this.getInstalledAddons();
|
||||
|
||||
|
||||
// If a preferred addon is specified, try it first
|
||||
if (preferredAddonId) {
|
||||
const preferredAddon = addons.find(addon => addon.id === preferredAddonId);
|
||||
|
|
@ -820,14 +848,14 @@ 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
|
||||
if (typeof resource === 'object' && resource !== null && 'name' in resource) {
|
||||
const typedResource = resource as ResourceObject;
|
||||
if (typedResource.name === 'meta' &&
|
||||
Array.isArray(typedResource.types) &&
|
||||
typedResource.types.includes(type)) {
|
||||
if (typedResource.name === 'meta' &&
|
||||
Array.isArray(typedResource.types) &&
|
||||
typedResource.types.includes(type)) {
|
||||
hasMetaSupport = true;
|
||||
// Check idPrefix support
|
||||
if (Array.isArray(typedResource.idPrefixes) && typedResource.idPrefixes.length > 0) {
|
||||
|
|
@ -837,7 +865,7 @@ class StremioService {
|
|||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
// Check if the element is the simple string "meta" AND the addon has a top-level types array
|
||||
else if (typeof resource === 'string' && resource === 'meta' && preferredAddon.types) {
|
||||
if (Array.isArray(preferredAddon.types) && preferredAddon.types.includes(type)) {
|
||||
|
|
@ -852,19 +880,19 @@ class StremioService {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
// Only require ID prefix compatibility if the addon has declared specific prefixes
|
||||
const requiresIdPrefix = preferredAddon.idPrefixes && preferredAddon.idPrefixes.length > 0;
|
||||
const isSupported = hasMetaSupport && (!requiresIdPrefix || supportsIdPrefix);
|
||||
|
||||
|
||||
if (isSupported) {
|
||||
try {
|
||||
const response = await this.retryRequest(async () => {
|
||||
return await axios.get(url, { timeout: 10000 });
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
if (response.data && response.data.meta) {
|
||||
return response.data.meta;
|
||||
} else {
|
||||
|
|
@ -876,25 +904,25 @@ class StremioService {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Try Cinemeta with different base URLs
|
||||
const cinemetaUrls = [
|
||||
'https://v3-cinemeta.strem.io',
|
||||
'http://v3-cinemeta.strem.io'
|
||||
];
|
||||
|
||||
|
||||
|
||||
|
||||
for (const baseUrl of cinemetaUrls) {
|
||||
try {
|
||||
const encodedId = encodeURIComponent(id);
|
||||
const url = `${baseUrl}/meta/${type}/${encodedId}.json`;
|
||||
|
||||
|
||||
|
||||
const response = await this.retryRequest(async () => {
|
||||
return await axios.get(url, { timeout: 10000 });
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
if (response.data && response.data.meta) {
|
||||
return response.data.meta;
|
||||
} else {
|
||||
|
|
@ -907,18 +935,18 @@ 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;
|
||||
|
||||
|
||||
// Check if addon supports meta resource for this type AND idPrefix (handles both string and object formats)
|
||||
let hasMetaSupport = false;
|
||||
let supportsIdPrefix = false;
|
||||
|
||||
|
||||
for (const resource of addon.resources) {
|
||||
// Check if the current element is a ResourceObject
|
||||
if (typeof resource === 'object' && resource !== null && 'name' in resource) {
|
||||
const typedResource = resource as ResourceObject;
|
||||
if (typedResource.name === 'meta' &&
|
||||
Array.isArray(typedResource.types) &&
|
||||
typedResource.types.includes(type)) {
|
||||
if (typedResource.name === 'meta' &&
|
||||
Array.isArray(typedResource.types) &&
|
||||
typedResource.types.includes(type)) {
|
||||
hasMetaSupport = true;
|
||||
// Match idPrefixes if present; otherwise assume support
|
||||
if (Array.isArray(typedResource.idPrefixes) && typedResource.idPrefixes.length > 0) {
|
||||
|
|
@ -928,7 +956,7 @@ class StremioService {
|
|||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
// Check if the element is the simple string "meta" AND the addon has a top-level types array
|
||||
else if (typeof resource === 'string' && resource === 'meta' && addon.types) {
|
||||
if (Array.isArray(addon.types) && addon.types.includes(type)) {
|
||||
|
|
@ -943,28 +971,28 @@ class StremioService {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Require meta support, but allow any ID if addon doesn't declare specific prefixes
|
||||
|
||||
|
||||
// Only require ID prefix compatibility if the addon has declared specific prefixes
|
||||
const requiresIdPrefix = addon.idPrefixes && addon.idPrefixes.length > 0;
|
||||
const isSupported = hasMetaSupport && (!requiresIdPrefix || supportsIdPrefix);
|
||||
|
||||
|
||||
if (!isSupported) {
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
try {
|
||||
const { baseUrl, queryParams } = this.getAddonBaseURL(addon.url || '');
|
||||
const encodedId = encodeURIComponent(id);
|
||||
const url = queryParams ? `${baseUrl}/meta/${type}/${encodedId}.json?${queryParams}` : `${baseUrl}/meta/${type}/${encodedId}.json`;
|
||||
|
||||
|
||||
|
||||
const response = await this.retryRequest(async () => {
|
||||
return await axios.get(url, { timeout: 10000 });
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
if (response.data && response.data.meta) {
|
||||
return response.data.meta;
|
||||
} else {
|
||||
|
|
@ -973,7 +1001,7 @@ class StremioService {
|
|||
continue; // Try next addon
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return null;
|
||||
} catch (error) {
|
||||
logger.error('Error in getMetaDetails:', error);
|
||||
|
|
@ -986,8 +1014,8 @@ class StremioService {
|
|||
* This prevents over-fetching all episode data and reduces memory consumption
|
||||
*/
|
||||
async getUpcomingEpisodes(
|
||||
type: string,
|
||||
id: string,
|
||||
type: string,
|
||||
id: string,
|
||||
options: {
|
||||
daysBack?: number;
|
||||
daysAhead?: number;
|
||||
|
|
@ -996,7 +1024,7 @@ class StremioService {
|
|||
} = {}
|
||||
): Promise<{ seriesName: string; poster: string; episodes: any[] } | null> {
|
||||
const { daysBack = 14, daysAhead = 28, maxEpisodes = 50, preferredAddonId } = options;
|
||||
|
||||
|
||||
try {
|
||||
// Get metadata first (this is lightweight compared to episodes)
|
||||
const metadata = await this.getMetaDetails(type, id, preferredAddonId);
|
||||
|
|
@ -1048,10 +1076,9 @@ class StremioService {
|
|||
// Modify getStreams to use this.getInstalledAddons() instead of getEnabledAddons
|
||||
async getStreams(type: string, id: string, callback?: StreamCallback): Promise<void> {
|
||||
await this.ensureInitialized();
|
||||
|
||||
|
||||
const addons = this.getInstalledAddons();
|
||||
logger.log('📌 [getStreams] Installed addons:', addons.map(a => ({ id: a.id, name: a.name, url: a.url })));
|
||||
|
||||
|
||||
// Check if local scrapers are enabled and execute them first
|
||||
try {
|
||||
// Load settings from AsyncStorage directly (scoped with fallback)
|
||||
|
|
@ -1060,25 +1087,25 @@ class StremioService {
|
|||
|| (await mmkvStorage.getItem('app_settings'));
|
||||
const rawSettings = settingsJson ? JSON.parse(settingsJson) : {};
|
||||
const settings: AppSettings = { ...DEFAULT_SETTINGS, ...rawSettings };
|
||||
|
||||
|
||||
if (settings.enableLocalScrapers) {
|
||||
const hasScrapers = await localScraperService.hasScrapers();
|
||||
if (hasScrapers) {
|
||||
logger.log('🔧 [getStreams] Executing local scrapers for', type, id);
|
||||
|
||||
|
||||
// Map Stremio types to local scraper types
|
||||
const scraperType = type === 'series' ? 'tv' : type;
|
||||
|
||||
|
||||
// Parse the Stremio ID to extract ID and season/episode info
|
||||
let tmdbId: string | null = null;
|
||||
let season: number | undefined = undefined;
|
||||
let episode: number | undefined = undefined;
|
||||
let idType: 'imdb' | 'kitsu' | 'tmdb' = 'imdb';
|
||||
|
||||
|
||||
try {
|
||||
const idParts = id.split(':');
|
||||
let baseId: string;
|
||||
|
||||
|
||||
// Handle different episode ID formats
|
||||
if (idParts[0] === 'series') {
|
||||
// Format: series:imdbId:season:episode or series:kitsu:7442:season:episode
|
||||
|
|
@ -1128,7 +1155,7 @@ class StremioService {
|
|||
episode = parseInt(idParts[2], 10);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Handle ID conversion for local scrapers (they need TMDB ID)
|
||||
if (idType === 'imdb') {
|
||||
// Convert IMDb ID to TMDB ID
|
||||
|
|
@ -1154,7 +1181,7 @@ class StremioService {
|
|||
} catch (error) {
|
||||
logger.warn('🔧 [getStreams] Skipping local scrapers due to ID parsing error:', error);
|
||||
}
|
||||
|
||||
|
||||
// Execute local scrapers asynchronously with TMDB ID (when available)
|
||||
if (tmdbId) {
|
||||
localScraperService.getStreams(scraperType, tmdbId, season, episode, (streams, scraperId, scraperName, error) => {
|
||||
|
|
@ -1191,13 +1218,13 @@ class StremioService {
|
|||
} catch (error) {
|
||||
// Continue even if local scrapers fail
|
||||
}
|
||||
|
||||
|
||||
// Check specifically for TMDB Embed addon
|
||||
const tmdbEmbed = addons.find(addon => addon.id === 'org.tmdbembedapi');
|
||||
if (!tmdbEmbed) {
|
||||
// TMDB Embed addon not found
|
||||
}
|
||||
|
||||
|
||||
// Find addons that provide streams and sort them by installation order
|
||||
const streamAddons = addons
|
||||
.filter(addon => {
|
||||
|
|
@ -1205,35 +1232,30 @@ class StremioService {
|
|||
logger.log(`⚠️ [getStreams] Addon ${addon.id} has no valid resources array`);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Log the detailed resources structure for debugging
|
||||
logger.log(`📋 [getStreams] Checking addon ${addon.id} resources:`, JSON.stringify(addon.resources));
|
||||
|
||||
|
||||
let hasStreamResource = false;
|
||||
let supportsIdPrefix = false;
|
||||
|
||||
|
||||
// Iterate through the resources array, checking each element
|
||||
for (const resource of addon.resources) {
|
||||
// Check if the current element is a ResourceObject
|
||||
if (typeof resource === 'object' && resource !== null && 'name' in resource) {
|
||||
const typedResource = resource as ResourceObject;
|
||||
if (typedResource.name === 'stream' &&
|
||||
Array.isArray(typedResource.types) &&
|
||||
typedResource.types.includes(type)) {
|
||||
if (typedResource.name === 'stream' &&
|
||||
Array.isArray(typedResource.types) &&
|
||||
typedResource.types.includes(type)) {
|
||||
hasStreamResource = true;
|
||||
|
||||
|
||||
// Check if this addon supports the ID prefix (generic: any prefix that matches start of id)
|
||||
if (Array.isArray(typedResource.idPrefixes) && typedResource.idPrefixes.length > 0) {
|
||||
supportsIdPrefix = typedResource.idPrefixes.some(p => id.startsWith(p));
|
||||
logger.log(`🔍 [getStreams] Addon ${addon.id} supports prefixes: ${typedResource.idPrefixes.join(', ')} → matches=${supportsIdPrefix}`);
|
||||
} else {
|
||||
// If no idPrefixes specified, assume it supports all prefixes
|
||||
supportsIdPrefix = true;
|
||||
logger.log(`🔍 [getStreams] Addon ${addon.id} has no prefix restrictions, assuming support`);
|
||||
}
|
||||
break; // Found the stream resource object, no need to check further
|
||||
}
|
||||
}
|
||||
}
|
||||
// Check if the element is the simple string "stream" AND the addon has a top-level types array
|
||||
else if (typeof resource === 'string' && resource === 'stream' && addon.types) {
|
||||
if (Array.isArray(addon.types) && addon.types.includes(type)) {
|
||||
|
|
@ -1241,32 +1263,22 @@ class StremioService {
|
|||
// For simple string resources, check addon-level idPrefixes (generic)
|
||||
if (addon.idPrefixes && Array.isArray(addon.idPrefixes) && addon.idPrefixes.length > 0) {
|
||||
supportsIdPrefix = addon.idPrefixes.some(p => id.startsWith(p));
|
||||
logger.log(`🔍 [getStreams] Addon ${addon.id} supports prefixes: ${addon.idPrefixes.join(', ')} → matches=${supportsIdPrefix}`);
|
||||
} else {
|
||||
// If no idPrefixes specified, assume it supports all prefixes
|
||||
supportsIdPrefix = true;
|
||||
logger.log(`🔍 [getStreams] Addon ${addon.id} has no prefix restrictions, assuming support`);
|
||||
}
|
||||
break; // Found the simple stream resource string and type support
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
const canHandleRequest = hasStreamResource && supportsIdPrefix;
|
||||
|
||||
if (!hasStreamResource) {
|
||||
logger.log(`❌ [getStreams] Addon ${addon.id} does not support streaming ${type}`);
|
||||
} else if (!supportsIdPrefix) {
|
||||
logger.log(`❌ [getStreams] Addon ${addon.id} supports ${type} but its idPrefixes did not match id=${id}`);
|
||||
} else {
|
||||
logger.log(`✅ [getStreams] Addon ${addon.id} supports streaming ${type} for id=${id}`);
|
||||
}
|
||||
|
||||
|
||||
return canHandleRequest;
|
||||
});
|
||||
|
||||
logger.log('📊 [getStreams] Stream capable addons:', streamAddons.map(a => a.id));
|
||||
|
||||
|
||||
|
||||
|
||||
if (streamAddons.length === 0) {
|
||||
logger.warn('⚠️ [getStreams] No addons found that can provide streams');
|
||||
// Optionally call callback with an empty result or specific status?
|
||||
|
|
@ -1276,7 +1288,7 @@ class StremioService {
|
|||
|
||||
// Process each addon and call the callback individually
|
||||
streamAddons.forEach(addon => {
|
||||
// Use an IIFE to create scope for async operation inside forEach
|
||||
// Use an IIFE to create scope for async operation inside forEach
|
||||
(async () => {
|
||||
try {
|
||||
if (!addon.url) {
|
||||
|
|
@ -1288,9 +1300,9 @@ class StremioService {
|
|||
const { baseUrl, queryParams } = this.getAddonBaseURL(addon.url);
|
||||
const encodedId = encodeURIComponent(id);
|
||||
const url = queryParams ? `${baseUrl}/stream/${type}/${encodedId}.json?${queryParams}` : `${baseUrl}/stream/${type}/${encodedId}.json`;
|
||||
|
||||
|
||||
logger.log(`🔗 [getStreams] Requesting streams from ${addon.name} (${addon.id}): ${url}`);
|
||||
|
||||
|
||||
const response = await this.retryRequest(async () => {
|
||||
return await axios.get(url);
|
||||
});
|
||||
|
|
@ -1301,7 +1313,7 @@ class StremioService {
|
|||
processedStreams = this.processStreams(response.data.streams, addon);
|
||||
logger.log(`✅ [getStreams] Processed ${processedStreams.length} valid streams from ${addon.name} (${addon.id})`);
|
||||
} else {
|
||||
logger.log(`⚠️ [getStreams] No streams found in response from ${addon.name} (${addon.id})`);
|
||||
logger.log(`⚠️ [getStreams] No streams found in response from ${addon.name} (${addon.id})`);
|
||||
}
|
||||
|
||||
if (callback) {
|
||||
|
|
@ -1328,21 +1340,21 @@ class StremioService {
|
|||
logger.warn(`Addon ${addon.id} has no URL defined`);
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
const { baseUrl, queryParams } = this.getAddonBaseURL(addon.url);
|
||||
const encodedId = encodeURIComponent(id);
|
||||
const streamPath = `/stream/${type}/${encodedId}.json`;
|
||||
const url = queryParams ? `${baseUrl}${streamPath}?${queryParams}` : `${baseUrl}${streamPath}`;
|
||||
|
||||
|
||||
logger.log(`Fetching streams from URL: ${url}`);
|
||||
|
||||
|
||||
try {
|
||||
// Increase timeout for debrid services
|
||||
const timeout = addon.id.toLowerCase().includes('torrentio') ? 60000 : 10000;
|
||||
|
||||
|
||||
const response = await this.retryRequest(async () => {
|
||||
logger.log(`Making request to ${url} with timeout ${timeout}ms`);
|
||||
return await axios.get(url, {
|
||||
return await axios.get(url, {
|
||||
timeout,
|
||||
headers: {
|
||||
'Accept': 'application/json',
|
||||
|
|
@ -1350,11 +1362,11 @@ class StremioService {
|
|||
}
|
||||
});
|
||||
}, 5); // Increase retries for stream fetching
|
||||
|
||||
|
||||
if (response.data && response.data.streams && Array.isArray(response.data.streams)) {
|
||||
const streams = this.processStreams(response.data.streams, addon);
|
||||
logger.log(`Successfully processed ${streams.length} streams from ${addon.id}`);
|
||||
|
||||
|
||||
return {
|
||||
streams,
|
||||
addon: addon.id,
|
||||
|
|
@ -1377,7 +1389,7 @@ class StremioService {
|
|||
// Re-throw the error with more context
|
||||
throw new Error(`Failed to fetch streams from ${addon.name}: ${error.message}`);
|
||||
}
|
||||
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
|
|
@ -1495,11 +1507,11 @@ class StremioService {
|
|||
items: Meta[];
|
||||
}> {
|
||||
const addon = this.getInstalledAddons().find(a => a.id === addonId);
|
||||
|
||||
|
||||
if (!addon) {
|
||||
throw new Error(`Addon ${addonId} not found`);
|
||||
}
|
||||
|
||||
|
||||
const items = await this.getCatalog(addon, type, id);
|
||||
return {
|
||||
addon: addonId,
|
||||
|
|
@ -1596,22 +1608,48 @@ class StremioService {
|
|||
return false;
|
||||
}
|
||||
|
||||
// Check if any installed addons can provide streams
|
||||
async hasStreamProviders(): Promise<boolean> {
|
||||
// Check if any installed addons can provide streams (including embedded streams in metadata)
|
||||
async hasStreamProviders(type?: string): Promise<boolean> {
|
||||
await this.ensureInitialized();
|
||||
const addons = Array.from(this.installedAddons.values());
|
||||
|
||||
for (const addon of addons) {
|
||||
if (addon.resources && Array.isArray(addon.resources)) {
|
||||
// Check for 'stream' resource in the modern format
|
||||
const hasStreamResource = addon.resources.some(resource =>
|
||||
typeof resource === 'string'
|
||||
? resource === 'stream'
|
||||
: resource.name === 'stream'
|
||||
// Check for explicit 'stream' resource
|
||||
const hasStreamResource = addon.resources.some(resource =>
|
||||
typeof resource === 'string'
|
||||
? resource === 'stream'
|
||||
: (resource as any).name === 'stream'
|
||||
);
|
||||
|
||||
if (hasStreamResource) {
|
||||
return true;
|
||||
// If type specified, also check if addon supports this type
|
||||
if (type) {
|
||||
const supportsType = addon.types?.includes(type) ||
|
||||
addon.resources.some(resource =>
|
||||
typeof resource === 'object' &&
|
||||
(resource as any).name === 'stream' &&
|
||||
(resource as any).types?.includes(type)
|
||||
);
|
||||
if (supportsType) return true;
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// Also check for addons with meta resource that support the type
|
||||
// These addons might provide embedded streams within metadata
|
||||
if (type) {
|
||||
const hasMetaResource = addon.resources.some(resource =>
|
||||
typeof resource === 'string'
|
||||
? resource === 'meta'
|
||||
: (resource as any).name === 'meta'
|
||||
);
|
||||
|
||||
if (hasMetaResource && addon.types?.includes(type)) {
|
||||
// This addon provides meta for the type - might have embedded streams
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue