mirror of
https://github.com/tapframe/NuvioStreaming.git
synced 2026-03-11 17:45:38 +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";
|
||||
|
|
|
|||
|
|
@ -57,6 +57,10 @@
|
|||
<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>
|
||||
|
|
|
|||
|
|
@ -4,5 +4,7 @@
|
|||
<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)
|
||||
}[];
|
||||
}
|
||||
|
||||
|
|
@ -751,6 +753,7 @@ class StremioService {
|
|||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1050,7 +1078,6 @@ class StremioService {
|
|||
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 {
|
||||
|
|
@ -1206,9 +1233,6 @@ class StremioService {
|
|||
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;
|
||||
|
||||
|
|
@ -1225,11 +1249,9 @@ class StremioService {
|
|||
// 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
|
||||
}
|
||||
|
|
@ -1241,11 +1263,9 @@ 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
|
||||
}
|
||||
|
|
@ -1254,18 +1274,10 @@ class StremioService {
|
|||
|
||||
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');
|
||||
|
|
@ -1596,24 +1608,50 @@ 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
|
||||
// Check for explicit 'stream' resource
|
||||
const hasStreamResource = addon.resources.some(resource =>
|
||||
typeof resource === 'string'
|
||||
? resource === 'stream'
|
||||
: resource.name === 'stream'
|
||||
: (resource as any).name === 'stream'
|
||||
);
|
||||
|
||||
if (hasStreamResource) {
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
|
|
|
|||
Loading…
Reference in a new issue