|
|
|
@ -153,6 +153,8 @@ export interface MetaDetails extends Meta {
|
|
|
|
released: string;
|
|
|
|
released: string;
|
|
|
|
season?: number;
|
|
|
|
season?: number;
|
|
|
|
episode?: 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> {
|
|
|
|
public async isValidContentId(type: string, id: string | null | undefined): Promise<boolean> {
|
|
|
|
// Ensure addons are initialized before checking types
|
|
|
|
// Ensure addons are initialized before checking types
|
|
|
|
await this.ensureInitialized();
|
|
|
|
await this.ensureInitialized();
|
|
|
|
|
|
|
|
|
|
|
|
// Get all supported types from installed addons
|
|
|
|
// Get all supported types from installed addons
|
|
|
|
const supportedTypes = this.getAllSupportedTypes();
|
|
|
|
const supportedTypes = this.getAllSupportedTypes();
|
|
|
|
const isValidType = supportedTypes.includes(type);
|
|
|
|
const isValidType = supportedTypes.includes(type);
|
|
|
|
|
|
|
|
|
|
|
|
const lowerId = (id || '').toLowerCase();
|
|
|
|
const lowerId = (id || '').toLowerCase();
|
|
|
|
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;
|
|
|
|
|
|
|
|
|
|
|
|
// Get all supported ID prefixes from installed addons
|
|
|
|
// Get all supported ID prefixes from installed addons
|
|
|
|
const supportedPrefixes = this.getAllSupportedIdPrefixes(type);
|
|
|
|
const supportedPrefixes = this.getAllSupportedIdPrefixes(type);
|
|
|
|
|
|
|
|
|
|
|
|
// If no addons declare specific prefixes, allow any non-empty string
|
|
|
|
// If no addons declare specific prefixes, allow any non-empty string
|
|
|
|
if (supportedPrefixes.length === 0) {
|
|
|
|
if (supportedPrefixes.length === 0) {
|
|
|
|
return true;
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Check if the ID matches any supported prefix
|
|
|
|
// Check if the ID matches any supported prefix
|
|
|
|
return supportedPrefixes.some(prefix => lowerId.startsWith(prefix.toLowerCase()));
|
|
|
|
return supportedPrefixes.some(prefix => lowerId.startsWith(prefix.toLowerCase()));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
@ -222,13 +224,13 @@ class StremioService {
|
|
|
|
public getAllSupportedTypes(): string[] {
|
|
|
|
public getAllSupportedTypes(): string[] {
|
|
|
|
const addons = this.getInstalledAddons();
|
|
|
|
const addons = this.getInstalledAddons();
|
|
|
|
const types = new Set<string>();
|
|
|
|
const types = new Set<string>();
|
|
|
|
|
|
|
|
|
|
|
|
for (const addon of addons) {
|
|
|
|
for (const addon of addons) {
|
|
|
|
// Check addon-level types
|
|
|
|
// Check addon-level types
|
|
|
|
if (addon.types && Array.isArray(addon.types)) {
|
|
|
|
if (addon.types && Array.isArray(addon.types)) {
|
|
|
|
addon.types.forEach(type => types.add(type));
|
|
|
|
addon.types.forEach(type => types.add(type));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Check resource-level types
|
|
|
|
// Check resource-level types
|
|
|
|
if (addon.resources && Array.isArray(addon.resources)) {
|
|
|
|
if (addon.resources && Array.isArray(addon.resources)) {
|
|
|
|
for (const resource of addon.resources) {
|
|
|
|
for (const resource of addon.resources) {
|
|
|
|
@ -240,7 +242,7 @@ class StremioService {
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Check catalog-level types
|
|
|
|
// Check catalog-level types
|
|
|
|
if (addon.catalogs && Array.isArray(addon.catalogs)) {
|
|
|
|
if (addon.catalogs && Array.isArray(addon.catalogs)) {
|
|
|
|
for (const catalog of addon.catalogs) {
|
|
|
|
for (const catalog of addon.catalogs) {
|
|
|
|
@ -250,7 +252,7 @@ class StremioService {
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return Array.from(types);
|
|
|
|
return Array.from(types);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@ -258,13 +260,13 @@ class StremioService {
|
|
|
|
public getAllSupportedIdPrefixes(type: string): string[] {
|
|
|
|
public getAllSupportedIdPrefixes(type: string): string[] {
|
|
|
|
const addons = this.getInstalledAddons();
|
|
|
|
const addons = this.getInstalledAddons();
|
|
|
|
const prefixes = new Set<string>();
|
|
|
|
const prefixes = new Set<string>();
|
|
|
|
|
|
|
|
|
|
|
|
for (const addon of addons) {
|
|
|
|
for (const addon of addons) {
|
|
|
|
// Check addon-level idPrefixes
|
|
|
|
// Check addon-level idPrefixes
|
|
|
|
if (addon.idPrefixes && Array.isArray(addon.idPrefixes)) {
|
|
|
|
if (addon.idPrefixes && Array.isArray(addon.idPrefixes)) {
|
|
|
|
addon.idPrefixes.forEach(prefix => prefixes.add(prefix));
|
|
|
|
addon.idPrefixes.forEach(prefix => prefixes.add(prefix));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Check resource-level idPrefixes
|
|
|
|
// Check resource-level idPrefixes
|
|
|
|
if (addon.resources && Array.isArray(addon.resources)) {
|
|
|
|
if (addon.resources && Array.isArray(addon.resources)) {
|
|
|
|
for (const resource of addon.resources) {
|
|
|
|
for (const resource of addon.resources) {
|
|
|
|
@ -280,34 +282,34 @@ class StremioService {
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return Array.from(prefixes);
|
|
|
|
return Array.from(prefixes);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Check if a content ID belongs to a collection addon
|
|
|
|
// Check if a content ID belongs to a collection addon
|
|
|
|
public isCollectionContent(id: string): { isCollection: boolean; addon?: Manifest } {
|
|
|
|
public isCollectionContent(id: string): { isCollection: boolean; addon?: Manifest } {
|
|
|
|
const addons = this.getInstalledAddons();
|
|
|
|
const addons = this.getInstalledAddons();
|
|
|
|
|
|
|
|
|
|
|
|
for (const addon of addons) {
|
|
|
|
for (const addon of addons) {
|
|
|
|
// Check if this addon supports collections
|
|
|
|
// Check if this addon supports collections
|
|
|
|
const supportsCollections = addon.types?.includes('collections') ||
|
|
|
|
const supportsCollections = addon.types?.includes('collections') ||
|
|
|
|
addon.catalogs?.some(catalog => catalog.type === 'collections');
|
|
|
|
addon.catalogs?.some(catalog => catalog.type === 'collections');
|
|
|
|
|
|
|
|
|
|
|
|
if (!supportsCollections) continue;
|
|
|
|
if (!supportsCollections) continue;
|
|
|
|
|
|
|
|
|
|
|
|
// Check if our ID matches this addon's prefixes
|
|
|
|
// Check if our ID matches this addon's prefixes
|
|
|
|
const addonPrefixes = addon.idPrefixes || [];
|
|
|
|
const addonPrefixes = addon.idPrefixes || [];
|
|
|
|
const resourcePrefixes = addon.resources
|
|
|
|
const resourcePrefixes = addon.resources
|
|
|
|
?.filter(resource => typeof resource === 'object' && resource !== null && 'name' in resource)
|
|
|
|
?.filter(resource => typeof resource === 'object' && resource !== null && 'name' in resource)
|
|
|
|
?.filter(resource => (resource as any).name === 'meta' || (resource as any).name === 'catalog')
|
|
|
|
?.filter(resource => (resource as any).name === 'meta' || (resource as any).name === 'catalog')
|
|
|
|
?.flatMap(resource => (resource as any).idPrefixes || []) || [];
|
|
|
|
?.flatMap(resource => (resource as any).idPrefixes || []) || [];
|
|
|
|
|
|
|
|
|
|
|
|
const allPrefixes = [...addonPrefixes, ...resourcePrefixes];
|
|
|
|
const allPrefixes = [...addonPrefixes, ...resourcePrefixes];
|
|
|
|
if (allPrefixes.some(prefix => id.startsWith(prefix))) {
|
|
|
|
if (allPrefixes.some(prefix => id.startsWith(prefix))) {
|
|
|
|
return { isCollection: true, addon };
|
|
|
|
return { isCollection: true, addon };
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return { isCollection: false };
|
|
|
|
return { isCollection: false };
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@ -320,17 +322,17 @@ class StremioService {
|
|
|
|
|
|
|
|
|
|
|
|
private async initialize(): Promise<void> {
|
|
|
|
private async initialize(): Promise<void> {
|
|
|
|
if (this.initialized) return;
|
|
|
|
if (this.initialized) return;
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
try {
|
|
|
|
const scope = (await mmkvStorage.getItem('@user:current')) || 'local';
|
|
|
|
const scope = (await mmkvStorage.getItem('@user:current')) || 'local';
|
|
|
|
// Prefer scoped storage, but fall back to legacy keys to preserve older installs
|
|
|
|
// Prefer scoped storage, but fall back to legacy keys to preserve older installs
|
|
|
|
let storedAddons = await mmkvStorage.getItem(`@user:${scope}:${this.STORAGE_KEY}`);
|
|
|
|
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(this.STORAGE_KEY);
|
|
|
|
if (!storedAddons) storedAddons = await mmkvStorage.getItem(`@user:local:${this.STORAGE_KEY}`);
|
|
|
|
if (!storedAddons) storedAddons = await mmkvStorage.getItem(`@user:local:${this.STORAGE_KEY}`);
|
|
|
|
|
|
|
|
|
|
|
|
if (storedAddons) {
|
|
|
|
if (storedAddons) {
|
|
|
|
const parsed = JSON.parse(storedAddons);
|
|
|
|
const parsed = JSON.parse(storedAddons);
|
|
|
|
|
|
|
|
|
|
|
|
// Convert to Map
|
|
|
|
// Convert to Map
|
|
|
|
this.installedAddons = new Map();
|
|
|
|
this.installedAddons = new Map();
|
|
|
|
for (const addon of parsed) {
|
|
|
|
for (const addon of parsed) {
|
|
|
|
@ -339,11 +341,11 @@ class StremioService {
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Install Cinemeta for new users, but allow existing users to uninstall it
|
|
|
|
// Install Cinemeta for new users, but allow existing users to uninstall it
|
|
|
|
const cinemetaId = 'com.linvo.cinemeta';
|
|
|
|
const cinemetaId = 'com.linvo.cinemeta';
|
|
|
|
const hasUserRemovedCinemeta = await this.hasUserRemovedAddon(cinemetaId);
|
|
|
|
const hasUserRemovedCinemeta = await this.hasUserRemovedAddon(cinemetaId);
|
|
|
|
|
|
|
|
|
|
|
|
if (!this.installedAddons.has(cinemetaId) && !hasUserRemovedCinemeta) {
|
|
|
|
if (!this.installedAddons.has(cinemetaId) && !hasUserRemovedCinemeta) {
|
|
|
|
try {
|
|
|
|
try {
|
|
|
|
const cinemetaManifest = await this.getManifest('https://v3-cinemeta.strem.io/manifest.json');
|
|
|
|
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
|
|
|
|
// Install OpenSubtitles v3 by default unless user has explicitly removed it
|
|
|
|
const opensubsId = 'org.stremio.opensubtitlesv3';
|
|
|
|
const opensubsId = 'org.stremio.opensubtitlesv3';
|
|
|
|
const hasUserRemovedOpenSubtitles = await this.hasUserRemovedAddon(opensubsId);
|
|
|
|
const hasUserRemovedOpenSubtitles = await this.hasUserRemovedAddon(opensubsId);
|
|
|
|
|
|
|
|
|
|
|
|
if (!this.installedAddons.has(opensubsId) && !hasUserRemovedOpenSubtitles) {
|
|
|
|
if (!this.installedAddons.has(opensubsId) && !hasUserRemovedOpenSubtitles) {
|
|
|
|
try {
|
|
|
|
try {
|
|
|
|
const opensubsManifest = await this.getManifest('https://opensubtitles-v3.strem.io/manifest.json');
|
|
|
|
const opensubsManifest = await this.getManifest('https://opensubtitles-v3.strem.io/manifest.json');
|
|
|
|
@ -424,7 +426,7 @@ class StremioService {
|
|
|
|
this.installedAddons.set(opensubsId, fallbackManifest);
|
|
|
|
this.installedAddons.set(opensubsId, fallbackManifest);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Load addon order if exists (scoped first, then legacy, then @user:local for migration safety)
|
|
|
|
// 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}`);
|
|
|
|
let storedOrder = await mmkvStorage.getItem(`@user:${scope}:${this.ADDON_ORDER_KEY}`);
|
|
|
|
if (!storedOrder) storedOrder = await mmkvStorage.getItem(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
|
|
|
|
// Filter out any ids that aren't in installedAddons
|
|
|
|
this.addonOrder = this.addonOrder.filter(id => this.installedAddons.has(id));
|
|
|
|
this.addonOrder = this.addonOrder.filter(id => this.installedAddons.has(id));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Add Cinemeta to order only if user hasn't removed it
|
|
|
|
// Add Cinemeta to order only if user hasn't removed it
|
|
|
|
const hasUserRemovedCinemetaOrder = await this.hasUserRemovedAddon(cinemetaId);
|
|
|
|
const hasUserRemovedCinemetaOrder = await this.hasUserRemovedAddon(cinemetaId);
|
|
|
|
if (!this.addonOrder.includes(cinemetaId) && this.installedAddons.has(cinemetaId) && !hasUserRemovedCinemetaOrder) {
|
|
|
|
if (!this.addonOrder.includes(cinemetaId) && this.installedAddons.has(cinemetaId) && !hasUserRemovedCinemetaOrder) {
|
|
|
|
this.addonOrder.push(cinemetaId);
|
|
|
|
this.addonOrder.push(cinemetaId);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Only add OpenSubtitles to order if user hasn't removed it
|
|
|
|
// Only add OpenSubtitles to order if user hasn't removed it
|
|
|
|
const hasUserRemovedOpenSubtitlesOrder = await this.hasUserRemovedAddon(opensubsId);
|
|
|
|
const hasUserRemovedOpenSubtitlesOrder = await this.hasUserRemovedAddon(opensubsId);
|
|
|
|
if (!this.addonOrder.includes(opensubsId) && this.installedAddons.has(opensubsId) && !hasUserRemovedOpenSubtitlesOrder) {
|
|
|
|
if (!this.addonOrder.includes(opensubsId) && this.installedAddons.has(opensubsId) && !hasUserRemovedOpenSubtitlesOrder) {
|
|
|
|
this.addonOrder.push(opensubsId);
|
|
|
|
this.addonOrder.push(opensubsId);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Add any missing addons to the order
|
|
|
|
// Add any missing addons to the order
|
|
|
|
const installedIds = Array.from(this.installedAddons.keys());
|
|
|
|
const installedIds = Array.from(this.installedAddons.keys());
|
|
|
|
const missingIds = installedIds.filter(id => !this.addonOrder.includes(id));
|
|
|
|
const missingIds = installedIds.filter(id => !this.addonOrder.includes(id));
|
|
|
|
this.addonOrder = [...this.addonOrder, ...missingIds];
|
|
|
|
this.addonOrder = [...this.addonOrder, ...missingIds];
|
|
|
|
|
|
|
|
|
|
|
|
// Ensure order and addons are saved
|
|
|
|
// Ensure order and addons are saved
|
|
|
|
await this.saveAddonOrder();
|
|
|
|
await this.saveAddonOrder();
|
|
|
|
await this.saveInstalledAddons();
|
|
|
|
await this.saveInstalledAddons();
|
|
|
|
|
|
|
|
|
|
|
|
this.initialized = true;
|
|
|
|
this.initialized = true;
|
|
|
|
} catch (error) {
|
|
|
|
} catch (error) {
|
|
|
|
// Initialize with empty state on error
|
|
|
|
// Initialize with empty state on error
|
|
|
|
@ -479,12 +481,12 @@ class StremioService {
|
|
|
|
return await request();
|
|
|
|
return await request();
|
|
|
|
} catch (error: any) {
|
|
|
|
} catch (error: any) {
|
|
|
|
lastError = error;
|
|
|
|
lastError = error;
|
|
|
|
|
|
|
|
|
|
|
|
// Don't retry on 404 errors (content not found) - these are expected for some content
|
|
|
|
// Don't retry on 404 errors (content not found) - these are expected for some content
|
|
|
|
if (error.response?.status === 404) {
|
|
|
|
if (error.response?.status === 404) {
|
|
|
|
throw error;
|
|
|
|
throw error;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Only log warnings for non-404 errors to reduce noise
|
|
|
|
// Only log warnings for non-404 errors to reduce noise
|
|
|
|
if (error.response?.status !== 404) {
|
|
|
|
if (error.response?.status !== 404) {
|
|
|
|
logger.warn(`Request failed (attempt ${attempt + 1}/${retries + 1}):`, {
|
|
|
|
logger.warn(`Request failed (attempt ${attempt + 1}/${retries + 1}):`, {
|
|
|
|
@ -494,7 +496,7 @@ class StremioService {
|
|
|
|
status: error.response?.status,
|
|
|
|
status: error.response?.status,
|
|
|
|
});
|
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (attempt < retries) {
|
|
|
|
if (attempt < retries) {
|
|
|
|
const backoffDelay = delay * Math.pow(2, attempt);
|
|
|
|
const backoffDelay = delay * Math.pow(2, attempt);
|
|
|
|
logger.log(`Retrying in ${backoffDelay}ms...`);
|
|
|
|
logger.log(`Retrying in ${backoffDelay}ms...`);
|
|
|
|
@ -535,25 +537,25 @@ class StremioService {
|
|
|
|
async getManifest(url: string): Promise<Manifest> {
|
|
|
|
async getManifest(url: string): Promise<Manifest> {
|
|
|
|
try {
|
|
|
|
try {
|
|
|
|
// Clean up URL - ensure it ends with manifest.json
|
|
|
|
// Clean up URL - ensure it ends with manifest.json
|
|
|
|
const manifestUrl = url.endsWith('manifest.json')
|
|
|
|
const manifestUrl = url.endsWith('manifest.json')
|
|
|
|
? url
|
|
|
|
? url
|
|
|
|
: `${url.replace(/\/$/, '')}/manifest.json`;
|
|
|
|
: `${url.replace(/\/$/, '')}/manifest.json`;
|
|
|
|
|
|
|
|
|
|
|
|
const response = await this.retryRequest(async () => {
|
|
|
|
const response = await this.retryRequest(async () => {
|
|
|
|
return await axios.get(manifestUrl);
|
|
|
|
return await axios.get(manifestUrl);
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
const manifest = response.data;
|
|
|
|
const manifest = response.data;
|
|
|
|
|
|
|
|
|
|
|
|
// Add some extra fields for internal use
|
|
|
|
// Add some extra fields for internal use
|
|
|
|
manifest.originalUrl = url;
|
|
|
|
manifest.originalUrl = url;
|
|
|
|
manifest.url = url.replace(/manifest\.json$/, '');
|
|
|
|
manifest.url = url.replace(/manifest\.json$/, '');
|
|
|
|
|
|
|
|
|
|
|
|
// Ensure ID exists
|
|
|
|
// Ensure ID exists
|
|
|
|
if (!manifest.id) {
|
|
|
|
if (!manifest.id) {
|
|
|
|
manifest.id = this.formatId(url);
|
|
|
|
manifest.id = this.formatId(url);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return manifest;
|
|
|
|
return manifest;
|
|
|
|
} catch (error) {
|
|
|
|
} catch (error) {
|
|
|
|
logger.error(`Failed to fetch manifest from ${url}:`, error);
|
|
|
|
logger.error(`Failed to fetch manifest from ${url}:`, error);
|
|
|
|
@ -565,16 +567,16 @@ class StremioService {
|
|
|
|
const manifest = await this.getManifest(url);
|
|
|
|
const manifest = await this.getManifest(url);
|
|
|
|
if (manifest && manifest.id) {
|
|
|
|
if (manifest && manifest.id) {
|
|
|
|
this.installedAddons.set(manifest.id, manifest);
|
|
|
|
this.installedAddons.set(manifest.id, manifest);
|
|
|
|
|
|
|
|
|
|
|
|
// If addon was previously removed by user, unmark it on reinstall and clean up
|
|
|
|
// If addon was previously removed by user, unmark it on reinstall and clean up
|
|
|
|
await this.unmarkAddonAsRemovedByUser(manifest.id);
|
|
|
|
await this.unmarkAddonAsRemovedByUser(manifest.id);
|
|
|
|
await this.cleanupRemovedAddonFromStorage(manifest.id);
|
|
|
|
await this.cleanupRemovedAddonFromStorage(manifest.id);
|
|
|
|
|
|
|
|
|
|
|
|
// Add to order if not already present (new addons go to the end)
|
|
|
|
// Add to order if not already present (new addons go to the end)
|
|
|
|
if (!this.addonOrder.includes(manifest.id)) {
|
|
|
|
if (!this.addonOrder.includes(manifest.id)) {
|
|
|
|
this.addonOrder.push(manifest.id);
|
|
|
|
this.addonOrder.push(manifest.id);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
await this.saveInstalledAddons();
|
|
|
|
await this.saveInstalledAddons();
|
|
|
|
await this.saveAddonOrder();
|
|
|
|
await this.saveAddonOrder();
|
|
|
|
// Emit an event that an addon was added
|
|
|
|
// Emit an event that an addon was added
|
|
|
|
@ -641,7 +643,7 @@ class StremioService {
|
|
|
|
const removedAddons = await mmkvStorage.getItem('user_removed_addons');
|
|
|
|
const removedAddons = await mmkvStorage.getItem('user_removed_addons');
|
|
|
|
let removedList = removedAddons ? JSON.parse(removedAddons) : [];
|
|
|
|
let removedList = removedAddons ? JSON.parse(removedAddons) : [];
|
|
|
|
if (!Array.isArray(removedList)) removedList = [];
|
|
|
|
if (!Array.isArray(removedList)) removedList = [];
|
|
|
|
|
|
|
|
|
|
|
|
if (!removedList.includes(addonId)) {
|
|
|
|
if (!removedList.includes(addonId)) {
|
|
|
|
removedList.push(addonId);
|
|
|
|
removedList.push(addonId);
|
|
|
|
await mmkvStorage.setItem('user_removed_addons', JSON.stringify(removedList));
|
|
|
|
await mmkvStorage.setItem('user_removed_addons', JSON.stringify(removedList));
|
|
|
|
@ -656,10 +658,10 @@ class StremioService {
|
|
|
|
try {
|
|
|
|
try {
|
|
|
|
const removedAddons = await mmkvStorage.getItem('user_removed_addons');
|
|
|
|
const removedAddons = await mmkvStorage.getItem('user_removed_addons');
|
|
|
|
if (!removedAddons) return;
|
|
|
|
if (!removedAddons) return;
|
|
|
|
|
|
|
|
|
|
|
|
let removedList = JSON.parse(removedAddons);
|
|
|
|
let removedList = JSON.parse(removedAddons);
|
|
|
|
if (!Array.isArray(removedList)) return;
|
|
|
|
if (!Array.isArray(removedList)) return;
|
|
|
|
|
|
|
|
|
|
|
|
const updatedList = removedList.filter(id => id !== addonId);
|
|
|
|
const updatedList = removedList.filter(id => id !== addonId);
|
|
|
|
await mmkvStorage.setItem('user_removed_addons', JSON.stringify(updatedList));
|
|
|
|
await mmkvStorage.setItem('user_removed_addons', JSON.stringify(updatedList));
|
|
|
|
} catch (error) {
|
|
|
|
} catch (error) {
|
|
|
|
@ -671,14 +673,14 @@ class StremioService {
|
|
|
|
private async cleanupRemovedAddonFromStorage(addonId: string): Promise<void> {
|
|
|
|
private async cleanupRemovedAddonFromStorage(addonId: string): Promise<void> {
|
|
|
|
try {
|
|
|
|
try {
|
|
|
|
const scope = (await mmkvStorage.getItem('@user:current')) || 'local';
|
|
|
|
const scope = (await mmkvStorage.getItem('@user:current')) || 'local';
|
|
|
|
|
|
|
|
|
|
|
|
// Remove from all possible addon order storage keys
|
|
|
|
// Remove from all possible addon order storage keys
|
|
|
|
const keys = [
|
|
|
|
const keys = [
|
|
|
|
`@user:${scope}:${this.ADDON_ORDER_KEY}`,
|
|
|
|
`@user:${scope}:${this.ADDON_ORDER_KEY}`,
|
|
|
|
this.ADDON_ORDER_KEY,
|
|
|
|
this.ADDON_ORDER_KEY,
|
|
|
|
`@user:local:${this.ADDON_ORDER_KEY}`
|
|
|
|
`@user:local:${this.ADDON_ORDER_KEY}`
|
|
|
|
];
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
|
|
for (const key of keys) {
|
|
|
|
for (const key of keys) {
|
|
|
|
const storedOrder = await mmkvStorage.getItem(key);
|
|
|
|
const storedOrder = await mmkvStorage.getItem(key);
|
|
|
|
if (storedOrder) {
|
|
|
|
if (storedOrder) {
|
|
|
|
@ -701,12 +703,12 @@ class StremioService {
|
|
|
|
async getAllCatalogs(): Promise<{ [addonId: string]: Meta[] }> {
|
|
|
|
async getAllCatalogs(): Promise<{ [addonId: string]: Meta[] }> {
|
|
|
|
const result: { [addonId: string]: Meta[] } = {};
|
|
|
|
const result: { [addonId: string]: Meta[] } = {};
|
|
|
|
const addons = this.getInstalledAddons();
|
|
|
|
const addons = this.getInstalledAddons();
|
|
|
|
|
|
|
|
|
|
|
|
const promises = addons.map(async (addon) => {
|
|
|
|
const promises = addons.map(async (addon) => {
|
|
|
|
if (!addon.catalogs || addon.catalogs.length === 0) return;
|
|
|
|
if (!addon.catalogs || addon.catalogs.length === 0) return;
|
|
|
|
|
|
|
|
|
|
|
|
const catalog = addon.catalogs[0]; // Just take the first catalog for now
|
|
|
|
const catalog = addon.catalogs[0]; // Just take the first catalog for now
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
try {
|
|
|
|
const items = await this.getCatalog(addon, catalog.type, catalog.id);
|
|
|
|
const items = await this.getCatalog(addon, catalog.type, catalog.id);
|
|
|
|
if (items.length > 0) {
|
|
|
|
if (items.length > 0) {
|
|
|
|
@ -716,7 +718,7 @@ class StremioService {
|
|
|
|
logger.error(`Failed to fetch catalog from ${addon.name}:`, error);
|
|
|
|
logger.error(`Failed to fetch catalog from ${addon.name}:`, error);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
await Promise.all(promises);
|
|
|
|
await Promise.all(promises);
|
|
|
|
return result;
|
|
|
|
return result;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
@ -724,15 +726,15 @@ class StremioService {
|
|
|
|
private getAddonBaseURL(url: string): { baseUrl: string; queryParams?: string } {
|
|
|
|
private getAddonBaseURL(url: string): { baseUrl: string; queryParams?: string } {
|
|
|
|
// Extract query parameters if they exist
|
|
|
|
// Extract query parameters if they exist
|
|
|
|
const [baseUrl, queryString] = url.split('?');
|
|
|
|
const [baseUrl, queryString] = url.split('?');
|
|
|
|
|
|
|
|
|
|
|
|
// Remove trailing manifest.json and slashes
|
|
|
|
// Remove trailing manifest.json and slashes
|
|
|
|
let cleanBaseUrl = baseUrl.replace(/manifest\.json$/, '').replace(/\/$/, '');
|
|
|
|
let cleanBaseUrl = baseUrl.replace(/manifest\.json$/, '').replace(/\/$/, '');
|
|
|
|
|
|
|
|
|
|
|
|
// Ensure URL has protocol
|
|
|
|
// Ensure URL has protocol
|
|
|
|
if (!cleanBaseUrl.startsWith('http')) {
|
|
|
|
if (!cleanBaseUrl.startsWith('http')) {
|
|
|
|
cleanBaseUrl = `https://${cleanBaseUrl}`;
|
|
|
|
cleanBaseUrl = `https://${cleanBaseUrl}`;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return { baseUrl: cleanBaseUrl, queryParams: queryString };
|
|
|
|
return { baseUrl: cleanBaseUrl, queryParams: queryString };
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@ -744,13 +746,14 @@ class StremioService {
|
|
|
|
.filter(f => f && f.value)
|
|
|
|
.filter(f => f && f.value)
|
|
|
|
.map(f => `&${encodeURIComponent(f.title)}=${encodeURIComponent(f.value!)}`)
|
|
|
|
.map(f => `&${encodeURIComponent(f.title)}=${encodeURIComponent(f.value!)}`)
|
|
|
|
.join('');
|
|
|
|
.join('');
|
|
|
|
|
|
|
|
|
|
|
|
// For all addons
|
|
|
|
// For all addons
|
|
|
|
if (!manifest.url) {
|
|
|
|
if (!manifest.url) {
|
|
|
|
throw new Error('Addon URL is missing');
|
|
|
|
throw new Error('Addon URL is missing');
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
try {
|
|
|
|
|
|
|
|
if (__DEV__) console.log(`🔍 [getCatalog] Manifest URL for ${manifest.name}: ${manifest.url}`);
|
|
|
|
const { baseUrl, queryParams } = this.getAddonBaseURL(manifest.url);
|
|
|
|
const { baseUrl, queryParams } = this.getAddonBaseURL(manifest.url);
|
|
|
|
// Candidate 1: Path-style skip URL: /catalog/{type}/{id}/skip={N}.json
|
|
|
|
// Candidate 1: Path-style skip URL: /catalog/{type}/{id}/skip={N}.json
|
|
|
|
const urlPathStyle = `${baseUrl}/catalog/${type}/${encodedId}/skip=${pageSkip}.json${queryParams ? `?${queryParams}` : ''}`;
|
|
|
|
const urlPathStyle = `${baseUrl}/catalog/${type}/${encodedId}/skip=${pageSkip}.json${queryParams ? `?${queryParams}` : ''}`;
|
|
|
|
@ -762,15 +765,40 @@ class StremioService {
|
|
|
|
if (queryParams) urlQueryStyle += `&${queryParams}`;
|
|
|
|
if (queryParams) urlQueryStyle += `&${queryParams}`;
|
|
|
|
urlQueryStyle += filterQuery;
|
|
|
|
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;
|
|
|
|
let response;
|
|
|
|
try {
|
|
|
|
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) {
|
|
|
|
} catch (e) {
|
|
|
|
try {
|
|
|
|
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) {
|
|
|
|
} 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 {
|
|
|
|
try {
|
|
|
|
const key = `${manifest.id}|${type}|${id}`;
|
|
|
|
const key = `${manifest.id}|${type}|${id}`;
|
|
|
|
if (typeof hasMore === 'boolean') this.catalogHasMore.set(key, hasMore);
|
|
|
|
if (typeof hasMore === 'boolean') this.catalogHasMore.set(key, hasMore);
|
|
|
|
} catch {}
|
|
|
|
} catch { }
|
|
|
|
if (response.data.metas && Array.isArray(response.data.metas)) {
|
|
|
|
if (response.data.metas && Array.isArray(response.data.metas)) {
|
|
|
|
return response.data.metas;
|
|
|
|
return response.data.metas;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
@ -800,13 +828,13 @@ class StremioService {
|
|
|
|
try {
|
|
|
|
try {
|
|
|
|
// Validate content ID first
|
|
|
|
// Validate content ID first
|
|
|
|
const isValidId = await this.isValidContentId(type, id);
|
|
|
|
const isValidId = await this.isValidContentId(type, id);
|
|
|
|
|
|
|
|
|
|
|
|
if (!isValidId) {
|
|
|
|
if (!isValidId) {
|
|
|
|
return null;
|
|
|
|
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) {
|
|
|
|
const preferredAddon = addons.find(addon => addon.id === preferredAddonId);
|
|
|
|
const preferredAddon = addons.find(addon => addon.id === preferredAddonId);
|
|
|
|
@ -820,14 +848,14 @@ 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;
|
|
|
|
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
|
|
|
|
if (typeof resource === 'object' && resource !== null && 'name' in resource) {
|
|
|
|
if (typeof resource === 'object' && resource !== null && 'name' in resource) {
|
|
|
|
const typedResource = resource as ResourceObject;
|
|
|
|
const typedResource = resource as ResourceObject;
|
|
|
|
if (typedResource.name === 'meta' &&
|
|
|
|
if (typedResource.name === 'meta' &&
|
|
|
|
Array.isArray(typedResource.types) &&
|
|
|
|
Array.isArray(typedResource.types) &&
|
|
|
|
typedResource.types.includes(type)) {
|
|
|
|
typedResource.types.includes(type)) {
|
|
|
|
hasMetaSupport = true;
|
|
|
|
hasMetaSupport = true;
|
|
|
|
// Check idPrefix support
|
|
|
|
// Check idPrefix support
|
|
|
|
if (Array.isArray(typedResource.idPrefixes) && typedResource.idPrefixes.length > 0) {
|
|
|
|
if (Array.isArray(typedResource.idPrefixes) && typedResource.idPrefixes.length > 0) {
|
|
|
|
@ -837,7 +865,7 @@ class StremioService {
|
|
|
|
}
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
// Check if the element is the simple string "meta" AND the addon has a top-level types array
|
|
|
|
// 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) {
|
|
|
|
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)) {
|
|
|
|
@ -852,19 +880,19 @@ class StremioService {
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Only require ID prefix compatibility if the addon has declared specific prefixes
|
|
|
|
// Only require ID prefix compatibility if the addon has declared specific prefixes
|
|
|
|
const requiresIdPrefix = preferredAddon.idPrefixes && preferredAddon.idPrefixes.length > 0;
|
|
|
|
const requiresIdPrefix = preferredAddon.idPrefixes && preferredAddon.idPrefixes.length > 0;
|
|
|
|
const isSupported = hasMetaSupport && (!requiresIdPrefix || supportsIdPrefix);
|
|
|
|
const isSupported = hasMetaSupport && (!requiresIdPrefix || supportsIdPrefix);
|
|
|
|
|
|
|
|
|
|
|
|
if (isSupported) {
|
|
|
|
if (isSupported) {
|
|
|
|
try {
|
|
|
|
try {
|
|
|
|
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 });
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (response.data && response.data.meta) {
|
|
|
|
if (response.data && response.data.meta) {
|
|
|
|
return response.data.meta;
|
|
|
|
return response.data.meta;
|
|
|
|
} else {
|
|
|
|
} else {
|
|
|
|
@ -876,25 +904,25 @@ class StremioService {
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Try Cinemeta with different base URLs
|
|
|
|
// Try Cinemeta with different base URLs
|
|
|
|
const cinemetaUrls = [
|
|
|
|
const cinemetaUrls = [
|
|
|
|
'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);
|
|
|
|
const url = `${baseUrl}/meta/${type}/${encodedId}.json`;
|
|
|
|
const url = `${baseUrl}/meta/${type}/${encodedId}.json`;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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 });
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (response.data && response.data.meta) {
|
|
|
|
if (response.data && response.data.meta) {
|
|
|
|
return response.data.meta;
|
|
|
|
return response.data.meta;
|
|
|
|
} else {
|
|
|
|
} else {
|
|
|
|
@ -907,18 +935,18 @@ 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;
|
|
|
|
|
|
|
|
|
|
|
|
// Check if addon supports meta resource for this type AND idPrefix (handles both string and object formats)
|
|
|
|
// Check if addon supports meta resource for this type AND idPrefix (handles both string and object formats)
|
|
|
|
let hasMetaSupport = false;
|
|
|
|
let hasMetaSupport = false;
|
|
|
|
let supportsIdPrefix = false;
|
|
|
|
let supportsIdPrefix = false;
|
|
|
|
|
|
|
|
|
|
|
|
for (const resource of addon.resources) {
|
|
|
|
for (const resource of addon.resources) {
|
|
|
|
// Check if the current element is a ResourceObject
|
|
|
|
// Check if the current element is a ResourceObject
|
|
|
|
if (typeof resource === 'object' && resource !== null && 'name' in resource) {
|
|
|
|
if (typeof resource === 'object' && resource !== null && 'name' in resource) {
|
|
|
|
const typedResource = resource as ResourceObject;
|
|
|
|
const typedResource = resource as ResourceObject;
|
|
|
|
if (typedResource.name === 'meta' &&
|
|
|
|
if (typedResource.name === 'meta' &&
|
|
|
|
Array.isArray(typedResource.types) &&
|
|
|
|
Array.isArray(typedResource.types) &&
|
|
|
|
typedResource.types.includes(type)) {
|
|
|
|
typedResource.types.includes(type)) {
|
|
|
|
hasMetaSupport = true;
|
|
|
|
hasMetaSupport = true;
|
|
|
|
// Match idPrefixes if present; otherwise assume support
|
|
|
|
// Match idPrefixes if present; otherwise assume support
|
|
|
|
if (Array.isArray(typedResource.idPrefixes) && typedResource.idPrefixes.length > 0) {
|
|
|
|
if (Array.isArray(typedResource.idPrefixes) && typedResource.idPrefixes.length > 0) {
|
|
|
|
@ -928,7 +956,7 @@ class StremioService {
|
|
|
|
}
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
// Check if the element is the simple string "meta" AND the addon has a top-level types array
|
|
|
|
// 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) {
|
|
|
|
else if (typeof resource === 'string' && resource === 'meta' && addon.types) {
|
|
|
|
if (Array.isArray(addon.types) && addon.types.includes(type)) {
|
|
|
|
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
|
|
|
|
// 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
|
|
|
|
// Only require ID prefix compatibility if the addon has declared specific prefixes
|
|
|
|
const requiresIdPrefix = addon.idPrefixes && addon.idPrefixes.length > 0;
|
|
|
|
const requiresIdPrefix = addon.idPrefixes && addon.idPrefixes.length > 0;
|
|
|
|
const isSupported = hasMetaSupport && (!requiresIdPrefix || supportsIdPrefix);
|
|
|
|
const isSupported = hasMetaSupport && (!requiresIdPrefix || supportsIdPrefix);
|
|
|
|
|
|
|
|
|
|
|
|
if (!isSupported) {
|
|
|
|
if (!isSupported) {
|
|
|
|
continue;
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
try {
|
|
|
|
const { baseUrl, queryParams } = this.getAddonBaseURL(addon.url || '');
|
|
|
|
const { baseUrl, queryParams } = this.getAddonBaseURL(addon.url || '');
|
|
|
|
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`;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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 });
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (response.data && response.data.meta) {
|
|
|
|
if (response.data && response.data.meta) {
|
|
|
|
return response.data.meta;
|
|
|
|
return response.data.meta;
|
|
|
|
} else {
|
|
|
|
} else {
|
|
|
|
@ -973,7 +1001,7 @@ class StremioService {
|
|
|
|
continue; // Try next addon
|
|
|
|
continue; // Try next addon
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return null;
|
|
|
|
return null;
|
|
|
|
} catch (error) {
|
|
|
|
} catch (error) {
|
|
|
|
logger.error('Error in getMetaDetails:', error);
|
|
|
|
logger.error('Error in getMetaDetails:', error);
|
|
|
|
@ -986,8 +1014,8 @@ class StremioService {
|
|
|
|
* This prevents over-fetching all episode data and reduces memory consumption
|
|
|
|
* This prevents over-fetching all episode data and reduces memory consumption
|
|
|
|
*/
|
|
|
|
*/
|
|
|
|
async getUpcomingEpisodes(
|
|
|
|
async getUpcomingEpisodes(
|
|
|
|
type: string,
|
|
|
|
type: string,
|
|
|
|
id: string,
|
|
|
|
id: string,
|
|
|
|
options: {
|
|
|
|
options: {
|
|
|
|
daysBack?: number;
|
|
|
|
daysBack?: number;
|
|
|
|
daysAhead?: number;
|
|
|
|
daysAhead?: number;
|
|
|
|
@ -996,7 +1024,7 @@ class StremioService {
|
|
|
|
} = {}
|
|
|
|
} = {}
|
|
|
|
): Promise<{ seriesName: string; poster: string; episodes: any[] } | null> {
|
|
|
|
): Promise<{ seriesName: string; poster: string; episodes: any[] } | null> {
|
|
|
|
const { daysBack = 14, daysAhead = 28, maxEpisodes = 50, preferredAddonId } = options;
|
|
|
|
const { daysBack = 14, daysAhead = 28, maxEpisodes = 50, preferredAddonId } = options;
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
try {
|
|
|
|
// Get metadata first (this is lightweight compared to episodes)
|
|
|
|
// Get metadata first (this is lightweight compared to episodes)
|
|
|
|
const metadata = await this.getMetaDetails(type, id, preferredAddonId);
|
|
|
|
const metadata = await this.getMetaDetails(type, id, preferredAddonId);
|
|
|
|
@ -1048,10 +1076,9 @@ class StremioService {
|
|
|
|
// Modify getStreams to use this.getInstalledAddons() instead of getEnabledAddons
|
|
|
|
// Modify getStreams to use this.getInstalledAddons() instead of getEnabledAddons
|
|
|
|
async getStreams(type: string, id: string, callback?: StreamCallback): Promise<void> {
|
|
|
|
async getStreams(type: string, id: string, callback?: StreamCallback): Promise<void> {
|
|
|
|
await this.ensureInitialized();
|
|
|
|
await this.ensureInitialized();
|
|
|
|
|
|
|
|
|
|
|
|
const addons = this.getInstalledAddons();
|
|
|
|
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
|
|
|
|
// Check if local scrapers are enabled and execute them first
|
|
|
|
try {
|
|
|
|
try {
|
|
|
|
// Load settings from AsyncStorage directly (scoped with fallback)
|
|
|
|
// Load settings from AsyncStorage directly (scoped with fallback)
|
|
|
|
@ -1060,25 +1087,25 @@ class StremioService {
|
|
|
|
|| (await mmkvStorage.getItem('app_settings'));
|
|
|
|
|| (await mmkvStorage.getItem('app_settings'));
|
|
|
|
const rawSettings = settingsJson ? JSON.parse(settingsJson) : {};
|
|
|
|
const rawSettings = settingsJson ? JSON.parse(settingsJson) : {};
|
|
|
|
const settings: AppSettings = { ...DEFAULT_SETTINGS, ...rawSettings };
|
|
|
|
const settings: AppSettings = { ...DEFAULT_SETTINGS, ...rawSettings };
|
|
|
|
|
|
|
|
|
|
|
|
if (settings.enableLocalScrapers) {
|
|
|
|
if (settings.enableLocalScrapers) {
|
|
|
|
const hasScrapers = await localScraperService.hasScrapers();
|
|
|
|
const hasScrapers = await localScraperService.hasScrapers();
|
|
|
|
if (hasScrapers) {
|
|
|
|
if (hasScrapers) {
|
|
|
|
logger.log('🔧 [getStreams] Executing local scrapers for', type, id);
|
|
|
|
logger.log('🔧 [getStreams] Executing local scrapers for', type, id);
|
|
|
|
|
|
|
|
|
|
|
|
// Map Stremio types to local scraper types
|
|
|
|
// Map Stremio types to local scraper types
|
|
|
|
const scraperType = type === 'series' ? 'tv' : type;
|
|
|
|
const scraperType = type === 'series' ? 'tv' : type;
|
|
|
|
|
|
|
|
|
|
|
|
// Parse the Stremio ID to extract ID and season/episode info
|
|
|
|
// Parse the Stremio ID to extract ID and season/episode info
|
|
|
|
let tmdbId: string | null = null;
|
|
|
|
let tmdbId: string | null = null;
|
|
|
|
let season: number | undefined = undefined;
|
|
|
|
let season: number | undefined = undefined;
|
|
|
|
let episode: number | undefined = undefined;
|
|
|
|
let episode: number | undefined = undefined;
|
|
|
|
let idType: 'imdb' | 'kitsu' | 'tmdb' = 'imdb';
|
|
|
|
let idType: 'imdb' | 'kitsu' | 'tmdb' = 'imdb';
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
try {
|
|
|
|
const idParts = id.split(':');
|
|
|
|
const idParts = id.split(':');
|
|
|
|
let baseId: string;
|
|
|
|
let baseId: string;
|
|
|
|
|
|
|
|
|
|
|
|
// Handle different episode ID formats
|
|
|
|
// Handle different episode ID formats
|
|
|
|
if (idParts[0] === 'series') {
|
|
|
|
if (idParts[0] === 'series') {
|
|
|
|
// Format: series:imdbId:season:episode or series:kitsu:7442:season:episode
|
|
|
|
// Format: series:imdbId:season:episode or series:kitsu:7442:season:episode
|
|
|
|
@ -1128,7 +1155,7 @@ class StremioService {
|
|
|
|
episode = parseInt(idParts[2], 10);
|
|
|
|
episode = parseInt(idParts[2], 10);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Handle ID conversion for local scrapers (they need TMDB ID)
|
|
|
|
// Handle ID conversion for local scrapers (they need TMDB ID)
|
|
|
|
if (idType === 'imdb') {
|
|
|
|
if (idType === 'imdb') {
|
|
|
|
// Convert IMDb ID to TMDB ID
|
|
|
|
// Convert IMDb ID to TMDB ID
|
|
|
|
@ -1154,7 +1181,7 @@ class StremioService {
|
|
|
|
} catch (error) {
|
|
|
|
} catch (error) {
|
|
|
|
logger.warn('🔧 [getStreams] Skipping local scrapers due to ID parsing error:', error);
|
|
|
|
logger.warn('🔧 [getStreams] Skipping local scrapers due to ID parsing error:', error);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Execute local scrapers asynchronously with TMDB ID (when available)
|
|
|
|
// Execute local scrapers asynchronously with TMDB ID (when available)
|
|
|
|
if (tmdbId) {
|
|
|
|
if (tmdbId) {
|
|
|
|
localScraperService.getStreams(scraperType, tmdbId, season, episode, (streams, scraperId, scraperName, error) => {
|
|
|
|
localScraperService.getStreams(scraperType, tmdbId, season, episode, (streams, scraperId, scraperName, error) => {
|
|
|
|
@ -1191,13 +1218,13 @@ class StremioService {
|
|
|
|
} catch (error) {
|
|
|
|
} catch (error) {
|
|
|
|
// Continue even if local scrapers fail
|
|
|
|
// Continue even if local scrapers fail
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Check specifically for TMDB Embed addon
|
|
|
|
// Check specifically for TMDB Embed addon
|
|
|
|
const tmdbEmbed = addons.find(addon => addon.id === 'org.tmdbembedapi');
|
|
|
|
const tmdbEmbed = addons.find(addon => addon.id === 'org.tmdbembedapi');
|
|
|
|
if (!tmdbEmbed) {
|
|
|
|
if (!tmdbEmbed) {
|
|
|
|
// TMDB Embed addon not found
|
|
|
|
// TMDB Embed addon not found
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Find addons that provide streams and sort them by installation order
|
|
|
|
// Find addons that provide streams and sort them by installation order
|
|
|
|
const streamAddons = addons
|
|
|
|
const streamAddons = addons
|
|
|
|
.filter(addon => {
|
|
|
|
.filter(addon => {
|
|
|
|
@ -1205,35 +1232,30 @@ class StremioService {
|
|
|
|
logger.log(`⚠️ [getStreams] Addon ${addon.id} has no valid resources array`);
|
|
|
|
logger.log(`⚠️ [getStreams] Addon ${addon.id} has no valid resources array`);
|
|
|
|
return false;
|
|
|
|
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 hasStreamResource = false;
|
|
|
|
let supportsIdPrefix = false;
|
|
|
|
let supportsIdPrefix = false;
|
|
|
|
|
|
|
|
|
|
|
|
// Iterate through the resources array, checking each element
|
|
|
|
// Iterate through the resources array, checking each element
|
|
|
|
for (const resource of addon.resources) {
|
|
|
|
for (const resource of addon.resources) {
|
|
|
|
// Check if the current element is a ResourceObject
|
|
|
|
// Check if the current element is a ResourceObject
|
|
|
|
if (typeof resource === 'object' && resource !== null && 'name' in resource) {
|
|
|
|
if (typeof resource === 'object' && resource !== null && 'name' in resource) {
|
|
|
|
const typedResource = resource as ResourceObject;
|
|
|
|
const typedResource = resource as ResourceObject;
|
|
|
|
if (typedResource.name === 'stream' &&
|
|
|
|
if (typedResource.name === 'stream' &&
|
|
|
|
Array.isArray(typedResource.types) &&
|
|
|
|
Array.isArray(typedResource.types) &&
|
|
|
|
typedResource.types.includes(type)) {
|
|
|
|
typedResource.types.includes(type)) {
|
|
|
|
hasStreamResource = true;
|
|
|
|
hasStreamResource = true;
|
|
|
|
|
|
|
|
|
|
|
|
// Check if this addon supports the ID prefix (generic: any prefix that matches start of id)
|
|
|
|
// 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) {
|
|
|
|
if (Array.isArray(typedResource.idPrefixes) && typedResource.idPrefixes.length > 0) {
|
|
|
|
supportsIdPrefix = typedResource.idPrefixes.some(p => id.startsWith(p));
|
|
|
|
supportsIdPrefix = typedResource.idPrefixes.some(p => id.startsWith(p));
|
|
|
|
logger.log(`🔍 [getStreams] Addon ${addon.id} supports prefixes: ${typedResource.idPrefixes.join(', ')} → matches=${supportsIdPrefix}`);
|
|
|
|
|
|
|
|
} else {
|
|
|
|
} else {
|
|
|
|
// If no idPrefixes specified, assume it supports all prefixes
|
|
|
|
// If no idPrefixes specified, assume it supports all prefixes
|
|
|
|
supportsIdPrefix = true;
|
|
|
|
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
|
|
|
|
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
|
|
|
|
// 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) {
|
|
|
|
else if (typeof resource === 'string' && resource === 'stream' && addon.types) {
|
|
|
|
if (Array.isArray(addon.types) && addon.types.includes(type)) {
|
|
|
|
if (Array.isArray(addon.types) && addon.types.includes(type)) {
|
|
|
|
@ -1241,32 +1263,22 @@ class StremioService {
|
|
|
|
// For simple string resources, check addon-level idPrefixes (generic)
|
|
|
|
// For simple string resources, check addon-level idPrefixes (generic)
|
|
|
|
if (addon.idPrefixes && Array.isArray(addon.idPrefixes) && addon.idPrefixes.length > 0) {
|
|
|
|
if (addon.idPrefixes && Array.isArray(addon.idPrefixes) && addon.idPrefixes.length > 0) {
|
|
|
|
supportsIdPrefix = addon.idPrefixes.some(p => id.startsWith(p));
|
|
|
|
supportsIdPrefix = addon.idPrefixes.some(p => id.startsWith(p));
|
|
|
|
logger.log(`🔍 [getStreams] Addon ${addon.id} supports prefixes: ${addon.idPrefixes.join(', ')} → matches=${supportsIdPrefix}`);
|
|
|
|
|
|
|
|
} else {
|
|
|
|
} else {
|
|
|
|
// If no idPrefixes specified, assume it supports all prefixes
|
|
|
|
// If no idPrefixes specified, assume it supports all prefixes
|
|
|
|
supportsIdPrefix = true;
|
|
|
|
supportsIdPrefix = true;
|
|
|
|
logger.log(`🔍 [getStreams] Addon ${addon.id} has no prefix restrictions, assuming support`);
|
|
|
|
|
|
|
|
}
|
|
|
|
}
|
|
|
|
break; // Found the simple stream resource string and type support
|
|
|
|
break; // Found the simple stream resource string and type support
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const canHandleRequest = hasStreamResource && supportsIdPrefix;
|
|
|
|
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;
|
|
|
|
return canHandleRequest;
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
logger.log('📊 [getStreams] Stream capable addons:', streamAddons.map(a => a.id));
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (streamAddons.length === 0) {
|
|
|
|
if (streamAddons.length === 0) {
|
|
|
|
logger.warn('⚠️ [getStreams] No addons found that can provide streams');
|
|
|
|
logger.warn('⚠️ [getStreams] No addons found that can provide streams');
|
|
|
|
// Optionally call callback with an empty result or specific status?
|
|
|
|
// Optionally call callback with an empty result or specific status?
|
|
|
|
@ -1276,7 +1288,7 @@ class StremioService {
|
|
|
|
|
|
|
|
|
|
|
|
// Process each addon and call the callback individually
|
|
|
|
// Process each addon and call the callback individually
|
|
|
|
streamAddons.forEach(addon => {
|
|
|
|
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 () => {
|
|
|
|
(async () => {
|
|
|
|
try {
|
|
|
|
try {
|
|
|
|
if (!addon.url) {
|
|
|
|
if (!addon.url) {
|
|
|
|
@ -1288,9 +1300,9 @@ class StremioService {
|
|
|
|
const { baseUrl, queryParams } = this.getAddonBaseURL(addon.url);
|
|
|
|
const { baseUrl, queryParams } = this.getAddonBaseURL(addon.url);
|
|
|
|
const encodedId = encodeURIComponent(id);
|
|
|
|
const encodedId = encodeURIComponent(id);
|
|
|
|
const url = queryParams ? `${baseUrl}/stream/${type}/${encodedId}.json?${queryParams}` : `${baseUrl}/stream/${type}/${encodedId}.json`;
|
|
|
|
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}`);
|
|
|
|
logger.log(`🔗 [getStreams] Requesting streams from ${addon.name} (${addon.id}): ${url}`);
|
|
|
|
|
|
|
|
|
|
|
|
const response = await this.retryRequest(async () => {
|
|
|
|
const response = await this.retryRequest(async () => {
|
|
|
|
return await axios.get(url);
|
|
|
|
return await axios.get(url);
|
|
|
|
});
|
|
|
|
});
|
|
|
|
@ -1301,7 +1313,7 @@ class StremioService {
|
|
|
|
processedStreams = this.processStreams(response.data.streams, addon);
|
|
|
|
processedStreams = this.processStreams(response.data.streams, addon);
|
|
|
|
logger.log(`✅ [getStreams] Processed ${processedStreams.length} valid streams from ${addon.name} (${addon.id})`);
|
|
|
|
logger.log(`✅ [getStreams] Processed ${processedStreams.length} valid streams from ${addon.name} (${addon.id})`);
|
|
|
|
} else {
|
|
|
|
} 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) {
|
|
|
|
if (callback) {
|
|
|
|
@ -1328,21 +1340,21 @@ class StremioService {
|
|
|
|
logger.warn(`Addon ${addon.id} has no URL defined`);
|
|
|
|
logger.warn(`Addon ${addon.id} has no URL defined`);
|
|
|
|
return null;
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const { baseUrl, queryParams } = this.getAddonBaseURL(addon.url);
|
|
|
|
const { baseUrl, queryParams } = this.getAddonBaseURL(addon.url);
|
|
|
|
const encodedId = encodeURIComponent(id);
|
|
|
|
const encodedId = encodeURIComponent(id);
|
|
|
|
const streamPath = `/stream/${type}/${encodedId}.json`;
|
|
|
|
const streamPath = `/stream/${type}/${encodedId}.json`;
|
|
|
|
const url = queryParams ? `${baseUrl}${streamPath}?${queryParams}` : `${baseUrl}${streamPath}`;
|
|
|
|
const url = queryParams ? `${baseUrl}${streamPath}?${queryParams}` : `${baseUrl}${streamPath}`;
|
|
|
|
|
|
|
|
|
|
|
|
logger.log(`Fetching streams from URL: ${url}`);
|
|
|
|
logger.log(`Fetching streams from URL: ${url}`);
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
try {
|
|
|
|
// Increase timeout for debrid services
|
|
|
|
// Increase timeout for debrid services
|
|
|
|
const timeout = addon.id.toLowerCase().includes('torrentio') ? 60000 : 10000;
|
|
|
|
const timeout = addon.id.toLowerCase().includes('torrentio') ? 60000 : 10000;
|
|
|
|
|
|
|
|
|
|
|
|
const response = await this.retryRequest(async () => {
|
|
|
|
const response = await this.retryRequest(async () => {
|
|
|
|
logger.log(`Making request to ${url} with timeout ${timeout}ms`);
|
|
|
|
logger.log(`Making request to ${url} with timeout ${timeout}ms`);
|
|
|
|
return await axios.get(url, {
|
|
|
|
return await axios.get(url, {
|
|
|
|
timeout,
|
|
|
|
timeout,
|
|
|
|
headers: {
|
|
|
|
headers: {
|
|
|
|
'Accept': 'application/json',
|
|
|
|
'Accept': 'application/json',
|
|
|
|
@ -1350,11 +1362,11 @@ class StremioService {
|
|
|
|
}
|
|
|
|
}
|
|
|
|
});
|
|
|
|
});
|
|
|
|
}, 5); // Increase retries for stream fetching
|
|
|
|
}, 5); // Increase retries for stream fetching
|
|
|
|
|
|
|
|
|
|
|
|
if (response.data && response.data.streams && Array.isArray(response.data.streams)) {
|
|
|
|
if (response.data && response.data.streams && Array.isArray(response.data.streams)) {
|
|
|
|
const streams = this.processStreams(response.data.streams, addon);
|
|
|
|
const streams = this.processStreams(response.data.streams, addon);
|
|
|
|
logger.log(`Successfully processed ${streams.length} streams from ${addon.id}`);
|
|
|
|
logger.log(`Successfully processed ${streams.length} streams from ${addon.id}`);
|
|
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
return {
|
|
|
|
streams,
|
|
|
|
streams,
|
|
|
|
addon: addon.id,
|
|
|
|
addon: addon.id,
|
|
|
|
@ -1377,7 +1389,7 @@ class StremioService {
|
|
|
|
// Re-throw the error with more context
|
|
|
|
// Re-throw the error with more context
|
|
|
|
throw new Error(`Failed to fetch streams from ${addon.name}: ${error.message}`);
|
|
|
|
throw new Error(`Failed to fetch streams from ${addon.name}: ${error.message}`);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return null;
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@ -1495,11 +1507,11 @@ class StremioService {
|
|
|
|
items: Meta[];
|
|
|
|
items: Meta[];
|
|
|
|
}> {
|
|
|
|
}> {
|
|
|
|
const addon = this.getInstalledAddons().find(a => a.id === addonId);
|
|
|
|
const addon = this.getInstalledAddons().find(a => a.id === addonId);
|
|
|
|
|
|
|
|
|
|
|
|
if (!addon) {
|
|
|
|
if (!addon) {
|
|
|
|
throw new Error(`Addon ${addonId} not found`);
|
|
|
|
throw new Error(`Addon ${addonId} not found`);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const items = await this.getCatalog(addon, type, id);
|
|
|
|
const items = await this.getCatalog(addon, type, id);
|
|
|
|
return {
|
|
|
|
return {
|
|
|
|
addon: addonId,
|
|
|
|
addon: addonId,
|
|
|
|
@ -1596,22 +1608,48 @@ class StremioService {
|
|
|
|
return false;
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Check if any installed addons can provide streams
|
|
|
|
// Check if any installed addons can provide streams (including embedded streams in metadata)
|
|
|
|
async hasStreamProviders(): Promise<boolean> {
|
|
|
|
async hasStreamProviders(type?: string): Promise<boolean> {
|
|
|
|
await this.ensureInitialized();
|
|
|
|
await this.ensureInitialized();
|
|
|
|
const addons = Array.from(this.installedAddons.values());
|
|
|
|
const addons = Array.from(this.installedAddons.values());
|
|
|
|
|
|
|
|
|
|
|
|
for (const addon of addons) {
|
|
|
|
for (const addon of addons) {
|
|
|
|
if (addon.resources && Array.isArray(addon.resources)) {
|
|
|
|
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 =>
|
|
|
|
const hasStreamResource = addon.resources.some(resource =>
|
|
|
|
typeof resource === 'string'
|
|
|
|
typeof resource === 'string'
|
|
|
|
? resource === 'stream'
|
|
|
|
? resource === 'stream'
|
|
|
|
: resource.name === 'stream'
|
|
|
|
: (resource as any).name === 'stream'
|
|
|
|
);
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
if (hasStreamResource) {
|
|
|
|
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;
|
|
|
|
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|