mirror of
https://github.com/tapframe/NuvioStreaming.git
synced 2026-03-11 17:45:38 +00:00
fix for addon renaming and disabling due to addon.id key conflict
This commit is contained in:
parent
c6b45340ba
commit
13c4313703
6 changed files with 250 additions and 173 deletions
|
|
@ -211,12 +211,13 @@ export const useMetadata = ({ id, type, addonId }: UseMetadataProps): UseMetadat
|
|||
|
||||
try {
|
||||
await stremioService.getStreams(type, id,
|
||||
(streams, addonId, addonName, error) => {
|
||||
(streams, addonId, addonName, error, installationId) => {
|
||||
const processTime = Date.now() - sourceStartTime;
|
||||
|
||||
console.log('🔍 [processStremioSource] Callback received:', {
|
||||
addonId,
|
||||
addonName,
|
||||
installationId,
|
||||
streamCount: streams?.length || 0,
|
||||
error: error?.message || null,
|
||||
processTime
|
||||
|
|
@ -275,20 +276,23 @@ export const useMetadata = ({ id, type, addonId }: UseMetadataProps): UseMetadat
|
|||
// Use debounced update to prevent rapid state changes
|
||||
debouncedStreamUpdate(() => {
|
||||
const updateState = (prevState: GroupedStreams): GroupedStreams => {
|
||||
if (__DEV__) logger.log(`🔄 [${logPrefix}:${sourceName}] Updating state for addon ${addonName} (${addonId})`);
|
||||
// Use installationId as key to keep multiple installations separate
|
||||
const key = installationId || addonId || 'unknown';
|
||||
if (__DEV__) logger.log(`🔄 [${logPrefix}:${sourceName}] Updating state for addon ${addonName} (${addonId}) [${installationId}]`);
|
||||
return {
|
||||
...prevState,
|
||||
[addonId]: {
|
||||
[key]: {
|
||||
addonName: addonName,
|
||||
streams: optimizedStreams // Use optimized streams
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
// Track response order for addons
|
||||
// Track response order for addons (use installationId to track each installation separately)
|
||||
setAddonResponseOrder(prevOrder => {
|
||||
if (!prevOrder.includes(addonId)) {
|
||||
return [...prevOrder, addonId];
|
||||
const key = installationId || addonId || 'unknown';
|
||||
if (!prevOrder.includes(key)) {
|
||||
return [...prevOrder, key];
|
||||
}
|
||||
return prevOrder;
|
||||
});
|
||||
|
|
@ -308,20 +312,23 @@ export const useMetadata = ({ id, type, addonId }: UseMetadataProps): UseMetadat
|
|||
|
||||
debouncedStreamUpdate(() => {
|
||||
const updateState = (prevState: GroupedStreams): GroupedStreams => {
|
||||
if (__DEV__) logger.log(`🔄 [${logPrefix}:${sourceName}] Adding empty provider ${addonName} (${addonId}) to state`);
|
||||
// Use installationId as key to keep multiple installations separate
|
||||
const key = installationId || addonId || 'unknown';
|
||||
if (__DEV__) logger.log(`🔄 [${logPrefix}:${sourceName}] Adding empty provider ${addonName} (${addonId}) [${installationId}] to state`);
|
||||
return {
|
||||
...prevState,
|
||||
[addonId]: {
|
||||
[key]: {
|
||||
addonName: addonName,
|
||||
streams: [] // Empty array for providers with no streams
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
// Track response order for addons
|
||||
// Track response order for addons (use installationId to track each installation separately)
|
||||
setAddonResponseOrder(prevOrder => {
|
||||
if (!prevOrder.includes(addonId)) {
|
||||
return [...prevOrder, addonId];
|
||||
const key = installationId || addonId || 'unknown';
|
||||
if (!prevOrder.includes(key)) {
|
||||
return [...prevOrder, key];
|
||||
}
|
||||
return prevOrder;
|
||||
});
|
||||
|
|
|
|||
|
|
@ -624,13 +624,40 @@ const AddonsScreen = () => {
|
|||
try {
|
||||
setInstalling(true);
|
||||
const manifest = await stremioService.getManifest(urlToInstall);
|
||||
|
||||
// Check if this addon is already installed
|
||||
const installedAddons = await stremioService.getInstalledAddonsAsync();
|
||||
const existingInstallations = installedAddons.filter(a => a.id === manifest.id);
|
||||
const isAlreadyInstalled = existingInstallations.length > 0;
|
||||
|
||||
// Check if addon provides streams
|
||||
const providesStreams = manifest.resources?.some(resource => {
|
||||
if (typeof resource === 'string') {
|
||||
return resource === 'stream';
|
||||
} else if (typeof resource === 'object' && resource !== null && 'name' in resource) {
|
||||
return (resource as any).name === 'stream';
|
||||
}
|
||||
return false;
|
||||
}) || false;
|
||||
|
||||
|
||||
if (isAlreadyInstalled && !providesStreams) {
|
||||
setAlertTitle(t('common.error'));
|
||||
setAlertMessage('This addon is already installed. Multiple installations are only allowed for stream providers.');
|
||||
setAlertActions([{ label: t('common.ok'), onPress: () => setAlertVisible(false) }]);
|
||||
setAlertVisible(true);
|
||||
return;
|
||||
}
|
||||
|
||||
setAddonDetails(manifest);
|
||||
setAddonUrl(urlToInstall);
|
||||
setShowConfirmModal(true);
|
||||
} catch (error) {
|
||||
} catch (error: any) {
|
||||
logger.error('Failed to fetch addon details:', error);
|
||||
setAlertTitle(t('common.error'));
|
||||
setAlertMessage(`${t('addons.fetch_error')} ${urlToInstall}`);
|
||||
|
||||
const errorMessage = error?.message || `${t('addons.fetch_error')} ${urlToInstall}`;
|
||||
setAlertMessage(errorMessage);
|
||||
setAlertActions([{ label: t('common.ok'), onPress: () => setAlertVisible(false) }]);
|
||||
setAlertVisible(true);
|
||||
} finally {
|
||||
|
|
@ -652,10 +679,12 @@ const AddonsScreen = () => {
|
|||
setAlertMessage(t('addons.install_success'));
|
||||
setAlertActions([{ label: t('common.ok'), onPress: () => setAlertVisible(false) }]);
|
||||
setAlertVisible(true);
|
||||
} catch (error) {
|
||||
} catch (error: any) {
|
||||
logger.error('Failed to install addon:', error);
|
||||
setAlertTitle(t('common.error'));
|
||||
setAlertMessage(t('addons.install_error'));
|
||||
// Show specific error message if available, otherwise use generic message
|
||||
const errorMessage = error?.message || t('addons.install_error');
|
||||
setAlertMessage(errorMessage);
|
||||
setAlertActions([{ label: t('common.ok'), onPress: () => setAlertVisible(false) }]);
|
||||
setAlertVisible(true);
|
||||
} finally {
|
||||
|
|
@ -669,14 +698,14 @@ const AddonsScreen = () => {
|
|||
};
|
||||
|
||||
const moveAddonUp = (addon: ExtendedManifest) => {
|
||||
if (stremioService.moveAddonUp(addon.installationId || addon.id)) {
|
||||
if (addon.installationId && stremioService.moveAddonUp(addon.installationId)) {
|
||||
// Refresh the list to reflect the new order
|
||||
loadAddons();
|
||||
}
|
||||
};
|
||||
|
||||
const moveAddonDown = (addon: ExtendedManifest) => {
|
||||
if (stremioService.moveAddonDown(addon.installationId || addon.id)) {
|
||||
if (addon.installationId && stremioService.moveAddonDown(addon.installationId)) {
|
||||
// Refresh the list to reflect the new order
|
||||
loadAddons();
|
||||
}
|
||||
|
|
@ -690,10 +719,12 @@ const AddonsScreen = () => {
|
|||
{
|
||||
label: t('addons.uninstall_button'),
|
||||
onPress: async () => {
|
||||
await stremioService.removeAddon(addon.installationId || addon.id);
|
||||
setAddons(prev => prev.filter(a => (a.installationId || a.id) !== (addon.installationId || addon.id)));
|
||||
// Ensure we re-read from storage/order to avoid reappearing on next load
|
||||
await loadAddons();
|
||||
if (addon.installationId) {
|
||||
await stremioService.removeAddon(addon.installationId);
|
||||
setAddons(prev => prev.filter(a => a.installationId !== addon.installationId));
|
||||
// Ensure we re-read from storage/order to avoid reappearing on next load
|
||||
await loadAddons();
|
||||
}
|
||||
},
|
||||
style: { color: colors.error }
|
||||
},
|
||||
|
|
@ -840,6 +871,11 @@ const AddonsScreen = () => {
|
|||
// Check if addon is pre-installed
|
||||
const isPreInstalled = stremioService.isPreInstalledAddon(item.id);
|
||||
|
||||
// Check if there are multiple installations of this addon
|
||||
const sameAddonInstallations = addons.filter(a => a.id === item.id);
|
||||
const hasMultipleInstallations = sameAddonInstallations.length > 1;
|
||||
const installationNumber = sameAddonInstallations.findIndex(a => a.installationId === item.installationId) + 1;
|
||||
|
||||
// Format the types into a simple category text
|
||||
const categoryText = types.length > 0
|
||||
? types.map(t => t.charAt(0).toUpperCase() + t.slice(1)).join(' • ')
|
||||
|
|
@ -890,13 +926,18 @@ const AddonsScreen = () => {
|
|||
</View>
|
||||
)}
|
||||
<View style={styles.addonTitleContainer}>
|
||||
<View style={{ flexDirection: 'row', alignItems: 'center', marginBottom: 2 }}>
|
||||
<View style={{ flexDirection: 'row', alignItems: 'center', marginBottom: 2, flexWrap: 'wrap' }}>
|
||||
<Text style={styles.addonName}>{item.name}</Text>
|
||||
{isPreInstalled && (
|
||||
<View style={[styles.priorityBadge, { marginLeft: 8, backgroundColor: colors.success }]}>
|
||||
<Text style={[styles.priorityText, { fontSize: 10 }]}>{t('addons.pre_installed')}</Text>
|
||||
</View>
|
||||
)}
|
||||
{hasMultipleInstallations && (
|
||||
<View style={[styles.priorityBadge, { marginLeft: 8, backgroundColor: colors.primary }]}>
|
||||
<Text style={[styles.priorityText, { fontSize: 10 }]}>#{installationNumber}</Text>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
<View style={styles.addonMetaContainer}>
|
||||
<Text style={styles.addonVersion}>{t('addons.version', { version: item.version || '1.0.0' })}</Text>
|
||||
|
|
@ -935,6 +976,11 @@ const AddonsScreen = () => {
|
|||
<Text style={styles.addonDescription}>
|
||||
{description.length > 100 ? description.substring(0, 100) + '...' : description}
|
||||
</Text>
|
||||
{hasMultipleInstallations && item.originalUrl && (
|
||||
<Text style={[styles.addonDescription, { fontSize: 11, marginTop: 4, color: colors.mediumGray }]}>
|
||||
{item.originalUrl}
|
||||
</Text>
|
||||
)}
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -308,12 +308,9 @@ const CatalogSettingsScreen = () => {
|
|||
addons.forEach(addon => {
|
||||
if (addon.catalogs && addon.catalogs.length > 0) {
|
||||
const uniqueCatalogs = new Map<string, CatalogSetting>();
|
||||
// Use installationId if available, otherwise fallback to id
|
||||
const uniqueAddonId = addon.installationId || addon.id;
|
||||
|
||||
addon.catalogs.forEach(catalog => {
|
||||
// Generate a truly unique key using installationId
|
||||
const settingKey = `${uniqueAddonId}:${catalog.type}:${catalog.id}`;
|
||||
const settingKey = `${addon.id}:${catalog.type}:${catalog.id}`;
|
||||
let displayName = catalog.name || catalog.id;
|
||||
const catalogType = catalog.type === 'movie' ? 'Movies' : catalog.type === 'series' ? 'TV Shows' : catalog.type.charAt(0).toUpperCase() + catalog.type.slice(1);
|
||||
|
||||
|
|
@ -338,7 +335,7 @@ const CatalogSettingsScreen = () => {
|
|||
}
|
||||
|
||||
uniqueCatalogs.set(settingKey, {
|
||||
addonId: uniqueAddonId, // Store unique ID here
|
||||
addonId: addon.id,
|
||||
catalogId: catalog.id,
|
||||
type: catalog.type,
|
||||
name: displayName,
|
||||
|
|
@ -354,15 +351,11 @@ const CatalogSettingsScreen = () => {
|
|||
// Group settings by addon name
|
||||
const grouped: GroupedCatalogs = {};
|
||||
availableCatalogs.forEach(setting => {
|
||||
// Find addon by matching either installationId or id
|
||||
const addon = addons.find(a => (a.installationId || a.id) === setting.addonId);
|
||||
const addon = addons.find(a => a.id === setting.addonId);
|
||||
if (!addon) return;
|
||||
|
||||
// Use the unique addon ID (installationId) as the group key
|
||||
const groupKey = setting.addonId;
|
||||
|
||||
if (!grouped[groupKey]) {
|
||||
grouped[groupKey] = {
|
||||
if (!grouped[setting.addonId]) {
|
||||
grouped[setting.addonId] = {
|
||||
name: addon.name,
|
||||
catalogs: [],
|
||||
expanded: true,
|
||||
|
|
@ -370,9 +363,9 @@ const CatalogSettingsScreen = () => {
|
|||
};
|
||||
}
|
||||
|
||||
grouped[groupKey].catalogs.push(setting);
|
||||
grouped[setting.addonId].catalogs.push(setting);
|
||||
if (setting.enabled) {
|
||||
grouped[groupKey].enabledCount++;
|
||||
grouped[setting.addonId].enabledCount++;
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -716,10 +716,12 @@ const DebridIntegrationScreen = () => {
|
|||
setAlertMessage(t('debrid.connected_title'));
|
||||
setAlertActions([{ label: t('common.ok'), onPress: () => setAlertVisible(false) }]);
|
||||
setAlertVisible(true);
|
||||
} catch (error) {
|
||||
} catch (error: any) {
|
||||
logger.error('Failed to install Torbox addon:', error);
|
||||
setAlertTitle(t('common.error'));
|
||||
setAlertMessage(t('addons.install_error'));
|
||||
|
||||
const errorMessage = error?.message || t('addons.install_error');
|
||||
setAlertMessage(errorMessage);
|
||||
setAlertActions([{ label: t('common.ok'), onPress: () => setAlertVisible(false) }]);
|
||||
setAlertVisible(true);
|
||||
} finally {
|
||||
|
|
|
|||
|
|
@ -224,10 +224,7 @@ export const useStreamsScreen = () => {
|
|||
|
||||
const getProviderPriority = (addonId: string): number => {
|
||||
const installedAddons = stremioService.getInstalledAddons();
|
||||
// Check installationId first, then id for backward compatibility
|
||||
const addonIndex = installedAddons.findIndex(addon =>
|
||||
(addon.installationId && addon.installationId === addonId) || addon.id === addonId
|
||||
);
|
||||
const addonIndex = installedAddons.findIndex(addon => addon.id === addonId);
|
||||
if (addonIndex !== -1) {
|
||||
return 50 - addonIndex;
|
||||
}
|
||||
|
|
@ -762,9 +759,7 @@ export const useStreamsScreen = () => {
|
|||
const pluginProviders: string[] = [];
|
||||
|
||||
Array.from(allProviders).forEach(provider => {
|
||||
const isInstalledAddon = installedAddons.some(addon =>
|
||||
(addon.installationId && addon.installationId === provider) || addon.id === provider
|
||||
);
|
||||
const isInstalledAddon = installedAddons.some(addon => addon.installationId === provider || addon.id === provider);
|
||||
if (isInstalledAddon) {
|
||||
addonProviders.push(provider);
|
||||
} else {
|
||||
|
|
@ -776,19 +771,20 @@ export const useStreamsScreen = () => {
|
|||
|
||||
addonProviders
|
||||
.sort((a, b) => {
|
||||
const indexA = installedAddons.findIndex(addon =>
|
||||
(addon.installationId && addon.installationId === a) || addon.id === a
|
||||
);
|
||||
const indexB = installedAddons.findIndex(addon =>
|
||||
(addon.installationId && addon.installationId === b) || addon.id === b
|
||||
);
|
||||
const indexA = installedAddons.findIndex(addon => addon.installationId === a || addon.id === a);
|
||||
const indexB = installedAddons.findIndex(addon => addon.installationId === b || addon.id === b);
|
||||
return indexA - indexB;
|
||||
})
|
||||
.forEach(provider => {
|
||||
const installedAddon = installedAddons.find(addon =>
|
||||
(addon.installationId && addon.installationId === provider) || addon.id === provider
|
||||
);
|
||||
filterChips.push({ id: provider, name: installedAddon?.name || provider });
|
||||
const installedAddon = installedAddons.find(addon => addon.installationId === provider || addon.id === provider);
|
||||
// For multiple installations of same addon, show URL to differentiate
|
||||
const sameAddonInstallations = installedAddons.filter(a => installedAddon && a.id === installedAddon.id);
|
||||
const hasMultiple = sameAddonInstallations.length > 1;
|
||||
const installationNumber = hasMultiple ? sameAddonInstallations.findIndex(a => a.installationId === installedAddon?.installationId) + 1 : 0;
|
||||
const displayName = hasMultiple && installationNumber > 0
|
||||
? `${installedAddon?.name} #${installationNumber}`
|
||||
: (installedAddon?.name || provider);
|
||||
filterChips.push({ id: provider, name: displayName });
|
||||
});
|
||||
|
||||
// Group plugins by repository
|
||||
|
|
@ -817,12 +813,8 @@ export const useStreamsScreen = () => {
|
|||
{ id: 'all', name: 'All Providers' },
|
||||
...Array.from(allProviders)
|
||||
.sort((a, b) => {
|
||||
const indexA = installedAddons.findIndex(addon =>
|
||||
(addon.installationId && addon.installationId === a) || addon.id === a
|
||||
);
|
||||
const indexB = installedAddons.findIndex(addon =>
|
||||
(addon.installationId && addon.installationId === b) || addon.id === b
|
||||
);
|
||||
const indexA = installedAddons.findIndex(addon => addon.installationId === a || addon.id === a);
|
||||
const indexB = installedAddons.findIndex(addon => addon.installationId === b || addon.id === b);
|
||||
if (indexA !== -1 && indexB !== -1) return indexA - indexB;
|
||||
if (indexA !== -1) return -1;
|
||||
if (indexB !== -1) return 1;
|
||||
|
|
@ -830,11 +822,17 @@ export const useStreamsScreen = () => {
|
|||
})
|
||||
.map(provider => {
|
||||
const addonInfo = streams[provider];
|
||||
const installedAddon = installedAddons.find(addon =>
|
||||
(addon.installationId && addon.installationId === provider) || addon.id === provider
|
||||
);
|
||||
const installedAddon = installedAddons.find(addon => addon.installationId === provider || addon.id === provider);
|
||||
let displayName = provider;
|
||||
if (installedAddon) displayName = installedAddon.name;
|
||||
if (installedAddon) {
|
||||
// For multiple installations of same addon, show # to differentiate
|
||||
const sameAddonInstallations = installedAddons.filter(a => a.id === installedAddon.id);
|
||||
const hasMultiple = sameAddonInstallations.length > 1;
|
||||
const installationNumber = hasMultiple ? sameAddonInstallations.findIndex(a => a.installationId === installedAddon.installationId) + 1 : 0;
|
||||
displayName = hasMultiple && installationNumber > 0
|
||||
? `${installedAddon.name} #${installationNumber}`
|
||||
: installedAddon.name;
|
||||
}
|
||||
else if (addonInfo?.addonName) displayName = addonInfo.addonName;
|
||||
return { id: provider, name: displayName };
|
||||
}),
|
||||
|
|
@ -846,7 +844,7 @@ export const useStreamsScreen = () => {
|
|||
const streams = selectedEpisode ? episodeStreams : groupedStreams;
|
||||
const installedAddons = stremioService.getInstalledAddons();
|
||||
|
||||
const filteredEntries = Object.entries(streams).filter(([addonId]) => {
|
||||
const filteredEntries = Object.entries(streams).filter(([key]) => {
|
||||
if (selectedProvider === 'all') return true;
|
||||
|
||||
// Handle repository-based filtering (repo-{repoId})
|
||||
|
|
@ -854,33 +852,27 @@ export const useStreamsScreen = () => {
|
|||
const repoId = selectedProvider.replace('repo-', '');
|
||||
if (!repoId) return false;
|
||||
|
||||
const isInstalledAddon = installedAddons.some(addon => addon.id === addonId);
|
||||
const isInstalledAddon = installedAddons.some(addon => addon.installationId === key || addon.id === key);
|
||||
if (isInstalledAddon) return false; // Not a plugin
|
||||
|
||||
// Check if this plugin belongs to the selected repository
|
||||
const repoInfo = localScraperService.getScraperRepository(addonId);
|
||||
const repoInfo = localScraperService.getScraperRepository(key);
|
||||
return !!(repoInfo && (repoInfo.id === repoId || repoInfo.id?.toLowerCase() === repoId?.toLowerCase()));
|
||||
}
|
||||
|
||||
// Legacy: handle old grouped-plugins filter (fallback)
|
||||
if (settings.streamDisplayMode === 'grouped' && selectedProvider === 'grouped-plugins') {
|
||||
const isInstalledAddon = installedAddons.some(addon =>
|
||||
(addon.installationId && addon.installationId === addonId) || addon.id === addonId
|
||||
);
|
||||
const isInstalledAddon = installedAddons.some(addon => addon.installationId === key || addon.id === key);
|
||||
return !isInstalledAddon;
|
||||
}
|
||||
|
||||
return addonId === selectedProvider;
|
||||
return key === selectedProvider;
|
||||
});
|
||||
|
||||
// Sort entries: installed addons first (in their installation order), then plugins
|
||||
const sortedEntries = filteredEntries.sort(([addonIdA], [addonIdB]) => {
|
||||
const isAddonA = installedAddons.some(addon =>
|
||||
(addon.installationId && addon.installationId === addonIdA) || addon.id === addonIdA
|
||||
);
|
||||
const isAddonB = installedAddons.some(addon =>
|
||||
(addon.installationId && addon.installationId === addonIdB) || addon.id === addonIdB
|
||||
);
|
||||
const sortedEntries = filteredEntries.sort(([keyA], [keyB]) => {
|
||||
const isAddonA = installedAddons.some(addon => addon.installationId === keyA || addon.id === keyA);
|
||||
const isAddonB = installedAddons.some(addon => addon.installationId === keyB || addon.id === keyB);
|
||||
|
||||
// Addons always come before plugins
|
||||
if (isAddonA && !isAddonB) return -1;
|
||||
|
|
@ -888,18 +880,14 @@ export const useStreamsScreen = () => {
|
|||
|
||||
// Both are addons - sort by installation order
|
||||
if (isAddonA && isAddonB) {
|
||||
const indexA = installedAddons.findIndex(addon =>
|
||||
(addon.installationId && addon.installationId === addonIdA) || addon.id === addonIdA
|
||||
);
|
||||
const indexB = installedAddons.findIndex(addon =>
|
||||
(addon.installationId && addon.installationId === addonIdB) || addon.id === addonIdB
|
||||
);
|
||||
const indexA = installedAddons.findIndex(addon => addon.installationId === keyA || addon.id === keyA);
|
||||
const indexB = installedAddons.findIndex(addon => addon.installationId === keyB || addon.id === keyB);
|
||||
return indexA - indexB;
|
||||
}
|
||||
|
||||
// Both are plugins - sort by response order
|
||||
const responseIndexA = addonResponseOrder.indexOf(addonIdA);
|
||||
const responseIndexB = addonResponseOrder.indexOf(addonIdB);
|
||||
const responseIndexA = addonResponseOrder.indexOf(keyA);
|
||||
const responseIndexB = addonResponseOrder.indexOf(keyB);
|
||||
if (responseIndexA !== -1 && responseIndexB !== -1) return responseIndexA - responseIndexB;
|
||||
if (responseIndexA !== -1) return -1;
|
||||
if (responseIndexB !== -1) return 1;
|
||||
|
|
@ -910,10 +898,8 @@ export const useStreamsScreen = () => {
|
|||
const addonStreams: Stream[] = [];
|
||||
const pluginStreams: Stream[] = [];
|
||||
|
||||
sortedEntries.forEach(([addonId, { streams: providerStreams }]) => {
|
||||
const isInstalledAddon = installedAddons.some(addon =>
|
||||
(addon.installationId && addon.installationId === addonId) || addon.id === addonId
|
||||
);
|
||||
sortedEntries.forEach(([key, { streams: providerStreams }]) => {
|
||||
const isInstalledAddon = installedAddons.some(addon => addon.installationId === key || addon.id === key);
|
||||
|
||||
if (isInstalledAddon) {
|
||||
addonStreams.push(...providerStreams);
|
||||
|
|
@ -959,10 +945,9 @@ export const useStreamsScreen = () => {
|
|||
}
|
||||
|
||||
return sortedEntries
|
||||
.map(([addonId, { addonName, streams: providerStreams }]) => {
|
||||
const isInstalledAddon = installedAddons.some(addon =>
|
||||
(addon.installationId && addon.installationId === addonId) || addon.id === addonId
|
||||
);
|
||||
.map(([key, { addonName, streams: providerStreams }]) => {
|
||||
const isInstalledAddon = installedAddons.some(addon => addon.installationId === key || addon.id === key);
|
||||
const installedAddon = installedAddons.find(addon => addon.installationId === key || addon.id === key);
|
||||
let filteredStreams = providerStreams;
|
||||
|
||||
if (!isInstalledAddon) {
|
||||
|
|
@ -977,9 +962,20 @@ export const useStreamsScreen = () => {
|
|||
processedStreams = sortStreamsByQuality(filteredStreams);
|
||||
}
|
||||
|
||||
// For multiple installations of same addon, add # to section title
|
||||
let sectionTitle = addonName;
|
||||
if (installedAddon) {
|
||||
const sameAddonInstallations = installedAddons.filter(a => a.id === installedAddon.id);
|
||||
const hasMultiple = sameAddonInstallations.length > 1;
|
||||
const installationNumber = hasMultiple ? sameAddonInstallations.findIndex(a => a.installationId === installedAddon.installationId) + 1 : 0;
|
||||
sectionTitle = hasMultiple && installationNumber > 0
|
||||
? `${addonName} #${installationNumber}`
|
||||
: addonName;
|
||||
}
|
||||
|
||||
return {
|
||||
title: addonName,
|
||||
addonId,
|
||||
title: sectionTitle,
|
||||
addonId: key,
|
||||
data: processedStreams,
|
||||
isEmptyDueToQualityFilter: false,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -148,9 +148,9 @@ export interface SubtitleResponse {
|
|||
addonName: string;
|
||||
}
|
||||
|
||||
// Modify the callback signature to include addon ID
|
||||
// Modify the callback signature to include addon ID and installation ID
|
||||
interface StreamCallback {
|
||||
(streams: Stream[] | null, addonId: string | null, addonName: string | null, error: Error | null): void;
|
||||
(streams: Stream[] | null, addonId: string | null, addonName: string | null, error: Error | null, installationId?: string | null): void;
|
||||
}
|
||||
|
||||
interface CatalogFilter {
|
||||
|
|
@ -186,6 +186,7 @@ interface ResourceObject {
|
|||
|
||||
export interface Manifest {
|
||||
id: string;
|
||||
installationId?: string; // Unique ID for this installation (allows multiple installs of same addon)
|
||||
name: string;
|
||||
version: string;
|
||||
description: string;
|
||||
|
|
@ -208,7 +209,6 @@ export interface Manifest {
|
|||
background?: string; // Background image URL
|
||||
logo?: string; // Logo URL
|
||||
contactEmail?: string; // Contact email
|
||||
installationId?: string; // Unique ID for this specific installation
|
||||
}
|
||||
|
||||
// Config object for addon configuration per protocol
|
||||
|
|
@ -263,8 +263,8 @@ export interface AddonCapabilities {
|
|||
|
||||
class StremioService {
|
||||
private static instance: StremioService;
|
||||
private installedAddons: Map<string, Manifest> = new Map();
|
||||
private addonOrder: string[] = [];
|
||||
private installedAddons: Map<string, Manifest> = new Map(); // Key is installationId
|
||||
private addonOrder: string[] = []; // Array of installationIds
|
||||
private readonly STORAGE_KEY = 'stremio-addons';
|
||||
private readonly ADDON_ORDER_KEY = 'stremio-addon-order';
|
||||
private readonly MAX_CONCURRENT_REQUESTS = 3;
|
||||
|
|
@ -278,6 +278,29 @@ class StremioService {
|
|||
this.initializationPromise = this.initialize();
|
||||
}
|
||||
|
||||
// Generate a unique installation ID for an addon
|
||||
private generateInstallationId(addonId: string): string {
|
||||
const timestamp = Date.now();
|
||||
const random = Math.random().toString(36).substring(2, 9);
|
||||
return `${addonId}-${timestamp}-${random}`;
|
||||
}
|
||||
|
||||
|
||||
private addonProvidesStreams(manifest: Manifest): boolean {
|
||||
if (!manifest.resources || !Array.isArray(manifest.resources)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return manifest.resources.some(resource => {
|
||||
if (typeof resource === 'string') {
|
||||
return resource === 'stream';
|
||||
} else if (typeof resource === 'object' && resource !== null && 'name' in resource) {
|
||||
return (resource as ResourceObject).name === 'stream';
|
||||
}
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
// Dynamic validator for content IDs based on installed addon capabilities
|
||||
public async isValidContentId(type: string, id: string | null | undefined): Promise<boolean> {
|
||||
// Ensure addons are initialized before checking types
|
||||
|
|
@ -419,13 +442,13 @@ class StremioService {
|
|||
if (storedAddons) {
|
||||
const parsed = JSON.parse(storedAddons);
|
||||
|
||||
// Convert to Map
|
||||
// Convert to Map using installationId as key
|
||||
this.installedAddons = new Map();
|
||||
for (const addon of parsed) {
|
||||
if (addon && addon.id) {
|
||||
// Migration: Ensure installationId exists
|
||||
// Generate installationId for existing addons that don't have one (migration)
|
||||
if (!addon.installationId) {
|
||||
addon.installationId = addon.id;
|
||||
addon.installationId = this.generateInstallationId(addon.id);
|
||||
}
|
||||
this.installedAddons.set(addon.installationId, addon);
|
||||
}
|
||||
|
|
@ -436,21 +459,19 @@ class StremioService {
|
|||
const cinemetaId = 'com.linvo.cinemeta';
|
||||
const hasUserRemovedCinemeta = await this.hasUserRemovedAddon(cinemetaId);
|
||||
|
||||
// Check if any installed addon has this ID (using valid values iteration)
|
||||
const isCinemetaInstalled = Array.from(this.installedAddons.values()).some(a => a.id === cinemetaId);
|
||||
// Check if Cinemeta is already installed (by checking addon.id, not installationId)
|
||||
const hasCinemeta = Array.from(this.installedAddons.values()).some(addon => addon.id === cinemetaId);
|
||||
|
||||
if (!isCinemetaInstalled && !hasUserRemovedCinemeta) {
|
||||
if (!hasCinemeta && !hasUserRemovedCinemeta) {
|
||||
try {
|
||||
const cinemetaManifest = await this.getManifest('https://v3-cinemeta.strem.io/manifest.json');
|
||||
// For default addons, we can use the ID as installationId to keep it clean (or generate one)
|
||||
// Using ID ensures only one instance of default addon is auto-installed
|
||||
cinemetaManifest.installationId = cinemetaId;
|
||||
this.installedAddons.set(cinemetaId, cinemetaManifest);
|
||||
cinemetaManifest.installationId = this.generateInstallationId(cinemetaId);
|
||||
this.installedAddons.set(cinemetaManifest.installationId, cinemetaManifest);
|
||||
} catch (error) {
|
||||
// Fallback to minimal manifest if fetch fails
|
||||
const fallbackManifest: Manifest = {
|
||||
id: cinemetaId,
|
||||
installationId: cinemetaId,
|
||||
installationId: this.generateInstallationId(cinemetaId),
|
||||
name: 'Cinemeta',
|
||||
version: '3.0.13',
|
||||
description: 'Provides metadata for movies and series from TheTVDB, TheMovieDB, etc.',
|
||||
|
|
@ -487,7 +508,7 @@ class StremioService {
|
|||
configurable: false
|
||||
}
|
||||
};
|
||||
this.installedAddons.set(cinemetaId, fallbackManifest);
|
||||
this.installedAddons.set(fallbackManifest.installationId!, fallbackManifest);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -495,17 +516,18 @@ class StremioService {
|
|||
const opensubsId = 'org.stremio.opensubtitlesv3';
|
||||
const hasUserRemovedOpenSubtitles = await this.hasUserRemovedAddon(opensubsId);
|
||||
|
||||
const isOpenSubsInstalled = Array.from(this.installedAddons.values()).some(a => a.id === opensubsId);
|
||||
// Check if OpenSubtitles is already installed (by checking addon.id, not installationId)
|
||||
const hasOpenSubs = Array.from(this.installedAddons.values()).some(addon => addon.id === opensubsId);
|
||||
|
||||
if (!isOpenSubsInstalled && !hasUserRemovedOpenSubtitles) {
|
||||
if (!hasOpenSubs && !hasUserRemovedOpenSubtitles) {
|
||||
try {
|
||||
const opensubsManifest = await this.getManifest('https://opensubtitles-v3.strem.io/manifest.json');
|
||||
opensubsManifest.installationId = opensubsId;
|
||||
this.installedAddons.set(opensubsId, opensubsManifest);
|
||||
opensubsManifest.installationId = this.generateInstallationId(opensubsId);
|
||||
this.installedAddons.set(opensubsManifest.installationId, opensubsManifest);
|
||||
} catch (error) {
|
||||
const fallbackManifest: Manifest = {
|
||||
id: opensubsId,
|
||||
installationId: opensubsId,
|
||||
installationId: this.generateInstallationId(opensubsId),
|
||||
name: 'OpenSubtitles v3',
|
||||
version: '1.0.0',
|
||||
description: 'OpenSubtitles v3 Addon for Stremio',
|
||||
|
|
@ -524,7 +546,7 @@ class StremioService {
|
|||
configurable: false
|
||||
}
|
||||
};
|
||||
this.installedAddons.set(opensubsId, fallbackManifest);
|
||||
this.installedAddons.set(fallbackManifest.installationId!, fallbackManifest);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -534,27 +556,30 @@ class StremioService {
|
|||
if (!storedOrder) storedOrder = await mmkvStorage.getItem(`@user:local:${this.ADDON_ORDER_KEY}`);
|
||||
if (storedOrder) {
|
||||
this.addonOrder = JSON.parse(storedOrder);
|
||||
// Filter out any ids that aren't in installedAddons
|
||||
this.addonOrder = this.addonOrder.filter(id => this.installedAddons.has(id));
|
||||
// Filter out any installationIds that aren't in installedAddons
|
||||
this.addonOrder = this.addonOrder.filter(installationId => this.installedAddons.has(installationId));
|
||||
}
|
||||
|
||||
// Add Cinemeta to order only if user hasn't removed it and it's installed
|
||||
// Add Cinemeta to order only if user hasn't removed it
|
||||
const hasUserRemovedCinemetaOrder = await this.hasUserRemovedAddon(cinemetaId);
|
||||
// We check if the installationId (which is cinemetaId for default) is in the map
|
||||
if (!this.addonOrder.includes(cinemetaId) && this.installedAddons.has(cinemetaId) && !hasUserRemovedCinemetaOrder) {
|
||||
this.addonOrder.push(cinemetaId);
|
||||
const cinemetaInstallation = Array.from(this.installedAddons.values()).find(addon => addon.id === cinemetaId);
|
||||
if (cinemetaInstallation && cinemetaInstallation.installationId &&
|
||||
!this.addonOrder.includes(cinemetaInstallation.installationId) && !hasUserRemovedCinemetaOrder) {
|
||||
this.addonOrder.push(cinemetaInstallation.installationId);
|
||||
}
|
||||
|
||||
// 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);
|
||||
const opensubsInstallation = Array.from(this.installedAddons.values()).find(addon => addon.id === opensubsId);
|
||||
if (opensubsInstallation && opensubsInstallation.installationId &&
|
||||
!this.addonOrder.includes(opensubsInstallation.installationId) && !hasUserRemovedOpenSubtitlesOrder) {
|
||||
this.addonOrder.push(opensubsInstallation.installationId);
|
||||
}
|
||||
|
||||
// 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];
|
||||
// Add any missing addons to the order (use installationIds)
|
||||
const installedInstallationIds = Array.from(this.installedAddons.keys());
|
||||
const missingInstallationIds = installedInstallationIds.filter(installationId => !this.addonOrder.includes(installationId));
|
||||
this.addonOrder = [...this.addonOrder, ...missingInstallationIds];
|
||||
|
||||
// Ensure order and addons are saved
|
||||
await this.saveAddonOrder();
|
||||
|
|
@ -668,49 +693,58 @@ class StremioService {
|
|||
async installAddon(url: string): Promise<void> {
|
||||
const manifest = await this.getManifest(url);
|
||||
if (manifest && manifest.id) {
|
||||
// Generate a unique installation ID
|
||||
const installationId = `${manifest.id}-${Date.now()}-${Math.floor(Math.random() * 10000)}`;
|
||||
manifest.installationId = installationId;
|
||||
// Check if this addon is already installed
|
||||
const existingInstallations = Array.from(this.installedAddons.values()).filter(a => a.id === manifest.id);
|
||||
const isAlreadyInstalled = existingInstallations.length > 0;
|
||||
|
||||
this.installedAddons.set(installationId, manifest);
|
||||
// Only allow multiple installations for stream-providing addons
|
||||
if (isAlreadyInstalled && !this.addonProvidesStreams(manifest)) {
|
||||
throw new Error('This addon is already installed. Multiple installations are only allowed for stream providers.');
|
||||
}
|
||||
|
||||
// Generate a unique installation ID for this installation
|
||||
manifest.installationId = this.generateInstallationId(manifest.id);
|
||||
|
||||
// Store using installationId as key (allows multiple installations of same addon)
|
||||
this.installedAddons.set(manifest.installationId, 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);
|
||||
|
||||
// Note: cleanupRemovedAddonFromStorage takes an installationId (from addonOrder context),
|
||||
// but here we are dealing with a new installationId, strictly speaking.
|
||||
// However, we might want to cleanup any stray legacy entries for this addon ID if we wanted strict uniqueness,
|
||||
// but we clearly support duplicates now, so we don't need to clean up other instances.
|
||||
// We ONLY keep the unmarkAddonAsRemovedByUser to allow re-auto-install of defaults if user manually installs them.
|
||||
|
||||
// Add to order (new addons go to the end)
|
||||
this.addonOrder.push(installationId);
|
||||
// Add installationId to order (new addons go to the end)
|
||||
if (!this.addonOrder.includes(manifest.installationId)) {
|
||||
this.addonOrder.push(manifest.installationId);
|
||||
}
|
||||
|
||||
await this.saveInstalledAddons();
|
||||
await this.saveAddonOrder();
|
||||
// Emit an event that an addon was added
|
||||
addonEmitter.emit(ADDON_EVENTS.ADDON_ADDED, installationId);
|
||||
// Emit an event that an addon was added (include both ids for compatibility)
|
||||
addonEmitter.emit(ADDON_EVENTS.ADDON_ADDED, { installationId: manifest.installationId, addonId: manifest.id });
|
||||
} else {
|
||||
throw new Error('Invalid addon manifest');
|
||||
}
|
||||
}
|
||||
|
||||
async removeAddon(installationId: string): Promise<void> {
|
||||
// Allow removal of any addon
|
||||
// Allow removal of any addon installation, including pre-installed ones like Cinemeta
|
||||
if (this.installedAddons.has(installationId)) {
|
||||
const addon = this.installedAddons.get(installationId);
|
||||
this.installedAddons.delete(installationId);
|
||||
// Remove from order
|
||||
// Remove from order using installationId
|
||||
this.addonOrder = this.addonOrder.filter(id => id !== installationId);
|
||||
|
||||
// Track user explicit removal for the addon ID (tombstone to prevent auto-reinstall of defaults)
|
||||
if (addon && addon.id) {
|
||||
await this.markAddonAsRemovedByUser(addon.id);
|
||||
// Track user explicit removal only if this is the last installation of this addon
|
||||
if (addon) {
|
||||
const remainingInstallations = Array.from(this.installedAddons.values()).filter(a => a.id === addon.id);
|
||||
if (remainingInstallations.length === 0) {
|
||||
// This was the last installation, mark addon as removed by user
|
||||
await this.markAddonAsRemovedByUser(addon.id);
|
||||
// Proactively clean up any persisted orders/legacy keys for this addon
|
||||
await this.cleanupRemovedAddonFromStorage(addon.id);
|
||||
}
|
||||
}
|
||||
|
||||
// Clean up this specific installation from storage keys
|
||||
await this.cleanupRemovedAddonFromStorage(installationId);
|
||||
|
||||
// Persist removals before app possibly exits
|
||||
await this.saveInstalledAddons();
|
||||
await this.saveAddonOrder();
|
||||
|
|
@ -720,10 +754,10 @@ class StremioService {
|
|||
}
|
||||
|
||||
getInstalledAddons(): Manifest[] {
|
||||
// Return addons in the specified order
|
||||
// Return addons in the specified order (using installationIds)
|
||||
const result = this.addonOrder
|
||||
.filter(id => this.installedAddons.has(id))
|
||||
.map(id => this.installedAddons.get(id)!);
|
||||
.filter(installationId => this.installedAddons.has(installationId))
|
||||
.map(installationId => this.installedAddons.get(installationId)!);
|
||||
return result;
|
||||
}
|
||||
|
||||
|
|
@ -1430,7 +1464,7 @@ class StremioService {
|
|||
try {
|
||||
if (!addon.url) {
|
||||
logger.warn(`⚠️ [getStreams] Addon ${addon.id} has no URL`);
|
||||
if (callback) callback(null, addon.installationId || addon.id, addon.name, new Error('Addon has no URL'));
|
||||
if (callback) callback(null, addon.id, addon.name, new Error('Addon has no URL'), addon.installationId);
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -1438,7 +1472,7 @@ class StremioService {
|
|||
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}`);
|
||||
logger.log(`🔗 [getStreams] Requesting streams from ${addon.name} (${addon.id}) [${addon.installationId}]: ${url}`);
|
||||
|
||||
const response = await this.retryRequest(async () => {
|
||||
return await axios.get(url, safeAxiosConfig);
|
||||
|
|
@ -1446,22 +1480,21 @@ class StremioService {
|
|||
|
||||
let processedStreams: Stream[] = [];
|
||||
if (response.data && response.data.streams) {
|
||||
logger.log(`✅ [getStreams] Got ${response.data.streams.length} streams from ${addon.name} (${addon.id})`);
|
||||
logger.log(`✅ [getStreams] Got ${response.data.streams.length} streams from ${addon.name} (${addon.id}) [${addon.installationId}]`);
|
||||
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}) [${addon.installationId}]`);
|
||||
} 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}) [${addon.installationId}]`);
|
||||
}
|
||||
|
||||
if (callback) {
|
||||
// Call callback with processed streams (can be empty array)
|
||||
// Use installationId if available to distinct between multiple installations of same addon
|
||||
callback(processedStreams, addon.installationId || addon.id, addon.name, null);
|
||||
// Call callback with processed streams (can be empty array), include installationId
|
||||
callback(processedStreams, addon.id, addon.name, null, addon.installationId);
|
||||
}
|
||||
} catch (error) {
|
||||
if (callback) {
|
||||
// Call callback with error
|
||||
callback(null, addon.installationId || addon.id, addon.name, error as Error);
|
||||
// Call callback with error, include installationId
|
||||
callback(null, addon.id, addon.name, error as Error, addon.installationId);
|
||||
}
|
||||
}
|
||||
})(); // Immediately invoke the async function
|
||||
|
|
@ -1642,7 +1675,7 @@ class StremioService {
|
|||
name: name,
|
||||
title: displayTitle,
|
||||
addonName: addon.name,
|
||||
addonId: addon.installationId || addon.id,
|
||||
addonId: addon.id,
|
||||
|
||||
// Include description as-is to preserve full details
|
||||
description: stream.description,
|
||||
|
|
@ -1833,9 +1866,9 @@ class StremioService {
|
|||
return deduped;
|
||||
}
|
||||
|
||||
// Add methods to move addons in the order
|
||||
moveAddonUp(id: string): boolean {
|
||||
const index = this.addonOrder.indexOf(id);
|
||||
// Add methods to move addons in the order (using installationIds)
|
||||
moveAddonUp(installationId: string): boolean {
|
||||
const index = this.addonOrder.indexOf(installationId);
|
||||
if (index > 0) {
|
||||
// Swap with the previous item
|
||||
[this.addonOrder[index - 1], this.addonOrder[index]] =
|
||||
|
|
@ -1848,8 +1881,8 @@ class StremioService {
|
|||
return false;
|
||||
}
|
||||
|
||||
moveAddonDown(id: string): boolean {
|
||||
const index = this.addonOrder.indexOf(id);
|
||||
moveAddonDown(installationId: string): boolean {
|
||||
const index = this.addonOrder.indexOf(installationId);
|
||||
if (index >= 0 && index < this.addonOrder.length - 1) {
|
||||
// Swap with the next item
|
||||
[this.addonOrder[index], this.addonOrder[index + 1]] =
|
||||
|
|
|
|||
Loading…
Reference in a new issue