From 83bb91e1d15addc664b9bd158dcfb327b449cd0c Mon Sep 17 00:00:00 2001 From: tapframe <85391825+tapframe@users.noreply.github.com> Date: Sat, 24 Jan 2026 10:25:27 +0530 Subject: [PATCH] added support for multiple addon installation of the same addon.id --- ios/Podfile.lock | 24 +++--- src/components/player/AndroidVideoPlayer.tsx | 2 - .../player/modals/LoadingOverlay.tsx | 25 +++--- src/screens/AddonsScreen.tsx | 10 +-- src/screens/CatalogSettingsScreen.tsx | 21 +++-- src/screens/streams/useStreamsScreen.ts | 61 ++++++++++---- src/services/stremioService.ts | 79 +++++++++++++------ 7 files changed, 143 insertions(+), 79 deletions(-) diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 6e35d658..84bbf230 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -2439,7 +2439,7 @@ PODS: - SDWebImageSVGCoder (~> 1.7.0) - SDWebImageWebPCoder (~> 0.14) - Yoga - - RNGestureHandler (2.29.1): + - RNGestureHandler (2.30.0): - hermes-engine - RCTRequired - RCTTypeSafety @@ -2461,7 +2461,7 @@ PODS: - ReactCommon/turbomodule/core - ReactNativeDependencies - Yoga - - RNReanimated (4.2.0): + - RNReanimated (4.2.1): - hermes-engine - RCTRequired - RCTTypeSafety @@ -2483,10 +2483,10 @@ PODS: - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - ReactNativeDependencies - - RNReanimated/reanimated (= 4.2.0) + - RNReanimated/reanimated (= 4.2.1) - RNWorklets - Yoga - - RNReanimated/reanimated (4.2.0): + - RNReanimated/reanimated (4.2.1): - hermes-engine - RCTRequired - RCTTypeSafety @@ -2508,10 +2508,10 @@ PODS: - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - ReactNativeDependencies - - RNReanimated/reanimated/apple (= 4.2.0) + - RNReanimated/reanimated/apple (= 4.2.1) - RNWorklets - Yoga - - RNReanimated/reanimated/apple (4.2.0): + - RNReanimated/reanimated/apple (4.2.1): - hermes-engine - RCTRequired - RCTTypeSafety @@ -2535,7 +2535,7 @@ PODS: - ReactNativeDependencies - RNWorklets - Yoga - - RNScreens (4.18.0): + - RNScreens (4.20.0): - hermes-engine - RCTRequired - RCTTypeSafety @@ -2557,9 +2557,9 @@ PODS: - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - ReactNativeDependencies - - RNScreens/common (= 4.18.0) + - RNScreens/common (= 4.20.0) - Yoga - - RNScreens/common (4.18.0): + - RNScreens/common (4.20.0): - hermes-engine - RCTRequired - RCTTypeSafety @@ -3309,9 +3309,9 @@ SPEC CHECKSUMS: ReactNativeDependencies: ed6d1e64802b150399f04f1d5728ec16b437251e RNCPicker: c8a3584b74133464ee926224463fcc54dfdaebca RNFastImage: 2d36f4cfed9b2342f94f8591c8be69dd047ac67c - RNGestureHandler: 723f29dac55e25f109d263ed65cecc4b9c4bd46a - RNReanimated: e1c71e6e693a66b203ae98773347b625d3cc85ee - RNScreens: 61c18865ab074f4d995ac8d7cf5060522a649d05 + RNGestureHandler: e0d0bce5599f6120b7adf90c38d2805e2935795f + RNReanimated: 8a7182314bb7afc01041a529e409a9112c007a50 + RNScreens: a00979e0d17609f1c9b0f97881c34550fc4565bf RNSentry: 1d7b9fdae7a01ad8f9053335b5d44e75c39a955e RNSVG: cf9ae78f2edf2988242c71a6392d15ff7dd62522 RNVectorIcons: 4351544f100d4f12cac156a7c13399e60bab3e26 diff --git a/src/components/player/AndroidVideoPlayer.tsx b/src/components/player/AndroidVideoPlayer.tsx index b53e8682..c757b8d7 100644 --- a/src/components/player/AndroidVideoPlayer.tsx +++ b/src/components/player/AndroidVideoPlayer.tsx @@ -729,8 +729,6 @@ const AndroidVideoPlayer: React.FC = () => { return ( = ({ // Reset logoOpacity.value = 0; logoScale.value = 1; - + // Start animations after 1 second delay logoOpacity.value = withDelay( 1000, @@ -53,7 +53,7 @@ const LoadingOverlay: React.FC = ({ easing: Easing.out(Easing.cubic), }) ); - + logoScale.value = withDelay( 1000, withRepeat( @@ -82,24 +82,23 @@ const LoadingOverlay: React.FC = ({ if (!visible) return null; return ( - {backdrop && ( + StyleSheet.absoluteFill, + { + opacity: backdropImageOpacityAnim + } + ]}> = ({ locations={[0, 0.3, 0.7, 1]} style={StyleSheet.absoluteFill} /> - - - + {hasLogo && logo ? ( { }; const moveAddonUp = (addon: ExtendedManifest) => { - if (stremioService.moveAddonUp(addon.id)) { + if (stremioService.moveAddonUp(addon.installationId || addon.id)) { // Refresh the list to reflect the new order loadAddons(); } }; const moveAddonDown = (addon: ExtendedManifest) => { - if (stremioService.moveAddonDown(addon.id)) { + if (stremioService.moveAddonDown(addon.installationId || addon.id)) { // Refresh the list to reflect the new order loadAddons(); } @@ -690,8 +690,8 @@ const AddonsScreen = () => { { label: t('addons.uninstall_button'), onPress: async () => { - await stremioService.removeAddon(addon.id); - setAddons(prev => prev.filter(a => a.id !== addon.id)); + 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(); }, @@ -1066,7 +1066,7 @@ const AddonsScreen = () => { ) : ( addons.map((addon, index) => ( {renderAddonItem({ item: addon, index })} diff --git a/src/screens/CatalogSettingsScreen.tsx b/src/screens/CatalogSettingsScreen.tsx index 320d7252..d3f084f3 100644 --- a/src/screens/CatalogSettingsScreen.tsx +++ b/src/screens/CatalogSettingsScreen.tsx @@ -308,9 +308,12 @@ const CatalogSettingsScreen = () => { addons.forEach(addon => { if (addon.catalogs && addon.catalogs.length > 0) { const uniqueCatalogs = new Map(); + // Use installationId if available, otherwise fallback to id + const uniqueAddonId = addon.installationId || addon.id; addon.catalogs.forEach(catalog => { - const settingKey = `${addon.id}:${catalog.type}:${catalog.id}`; + // Generate a truly unique key using installationId + const settingKey = `${uniqueAddonId}:${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); @@ -335,7 +338,7 @@ const CatalogSettingsScreen = () => { } uniqueCatalogs.set(settingKey, { - addonId: addon.id, + addonId: uniqueAddonId, // Store unique ID here catalogId: catalog.id, type: catalog.type, name: displayName, @@ -351,11 +354,15 @@ const CatalogSettingsScreen = () => { // Group settings by addon name const grouped: GroupedCatalogs = {}; availableCatalogs.forEach(setting => { - const addon = addons.find(a => a.id === setting.addonId); + // Find addon by matching either installationId or id + const addon = addons.find(a => (a.installationId || a.id) === setting.addonId); if (!addon) return; - if (!grouped[setting.addonId]) { - grouped[setting.addonId] = { + // Use the unique addon ID (installationId) as the group key + const groupKey = setting.addonId; + + if (!grouped[groupKey]) { + grouped[groupKey] = { name: addon.name, catalogs: [], expanded: true, @@ -363,9 +370,9 @@ const CatalogSettingsScreen = () => { }; } - grouped[setting.addonId].catalogs.push(setting); + grouped[groupKey].catalogs.push(setting); if (setting.enabled) { - grouped[setting.addonId].enabledCount++; + grouped[groupKey].enabledCount++; } }); diff --git a/src/screens/streams/useStreamsScreen.ts b/src/screens/streams/useStreamsScreen.ts index 4a88f57b..ca9c043e 100644 --- a/src/screens/streams/useStreamsScreen.ts +++ b/src/screens/streams/useStreamsScreen.ts @@ -224,7 +224,10 @@ export const useStreamsScreen = () => { const getProviderPriority = (addonId: string): number => { const installedAddons = stremioService.getInstalledAddons(); - const addonIndex = installedAddons.findIndex(addon => addon.id === addonId); + // Check installationId first, then id for backward compatibility + const addonIndex = installedAddons.findIndex(addon => + (addon.installationId && addon.installationId === addonId) || addon.id === addonId + ); if (addonIndex !== -1) { return 50 - addonIndex; } @@ -759,7 +762,9 @@ export const useStreamsScreen = () => { const pluginProviders: string[] = []; Array.from(allProviders).forEach(provider => { - const isInstalledAddon = installedAddons.some(addon => addon.id === provider); + const isInstalledAddon = installedAddons.some(addon => + (addon.installationId && addon.installationId === provider) || addon.id === provider + ); if (isInstalledAddon) { addonProviders.push(provider); } else { @@ -771,12 +776,18 @@ export const useStreamsScreen = () => { addonProviders .sort((a, b) => { - const indexA = installedAddons.findIndex(addon => addon.id === a); - const indexB = installedAddons.findIndex(addon => addon.id === 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 + ); return indexA - indexB; }) .forEach(provider => { - const installedAddon = installedAddons.find(addon => addon.id === provider); + const installedAddon = installedAddons.find(addon => + (addon.installationId && addon.installationId === provider) || addon.id === provider + ); filterChips.push({ id: provider, name: installedAddon?.name || provider }); }); @@ -806,8 +817,12 @@ export const useStreamsScreen = () => { { id: 'all', name: 'All Providers' }, ...Array.from(allProviders) .sort((a, b) => { - const indexA = installedAddons.findIndex(addon => addon.id === a); - const indexB = installedAddons.findIndex(addon => addon.id === 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 + ); if (indexA !== -1 && indexB !== -1) return indexA - indexB; if (indexA !== -1) return -1; if (indexB !== -1) return 1; @@ -815,7 +830,9 @@ export const useStreamsScreen = () => { }) .map(provider => { const addonInfo = streams[provider]; - const installedAddon = installedAddons.find(addon => addon.id === provider); + const installedAddon = installedAddons.find(addon => + (addon.installationId && addon.installationId === provider) || addon.id === provider + ); let displayName = provider; if (installedAddon) displayName = installedAddon.name; else if (addonInfo?.addonName) displayName = addonInfo.addonName; @@ -847,7 +864,9 @@ export const useStreamsScreen = () => { // Legacy: handle old grouped-plugins filter (fallback) if (settings.streamDisplayMode === 'grouped' && selectedProvider === 'grouped-plugins') { - const isInstalledAddon = installedAddons.some(addon => addon.id === addonId); + const isInstalledAddon = installedAddons.some(addon => + (addon.installationId && addon.installationId === addonId) || addon.id === addonId + ); return !isInstalledAddon; } @@ -856,8 +875,12 @@ export const useStreamsScreen = () => { // Sort entries: installed addons first (in their installation order), then plugins const sortedEntries = filteredEntries.sort(([addonIdA], [addonIdB]) => { - const isAddonA = installedAddons.some(addon => addon.id === addonIdA); - const isAddonB = installedAddons.some(addon => addon.id === 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 + ); // Addons always come before plugins if (isAddonA && !isAddonB) return -1; @@ -865,8 +888,12 @@ export const useStreamsScreen = () => { // Both are addons - sort by installation order if (isAddonA && isAddonB) { - const indexA = installedAddons.findIndex(addon => addon.id === addonIdA); - const indexB = installedAddons.findIndex(addon => addon.id === addonIdB); + 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 + ); return indexA - indexB; } @@ -884,7 +911,9 @@ export const useStreamsScreen = () => { const pluginStreams: Stream[] = []; sortedEntries.forEach(([addonId, { streams: providerStreams }]) => { - const isInstalledAddon = installedAddons.some(addon => addon.id === addonId); + const isInstalledAddon = installedAddons.some(addon => + (addon.installationId && addon.installationId === addonId) || addon.id === addonId + ); if (isInstalledAddon) { addonStreams.push(...providerStreams); @@ -931,7 +960,9 @@ export const useStreamsScreen = () => { return sortedEntries .map(([addonId, { addonName, streams: providerStreams }]) => { - const isInstalledAddon = installedAddons.some(addon => addon.id === addonId); + const isInstalledAddon = installedAddons.some(addon => + (addon.installationId && addon.installationId === addonId) || addon.id === addonId + ); let filteredStreams = providerStreams; if (!isInstalledAddon) { diff --git a/src/services/stremioService.ts b/src/services/stremioService.ts index 4a9a562e..a8a4d802 100644 --- a/src/services/stremioService.ts +++ b/src/services/stremioService.ts @@ -208,6 +208,7 @@ 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 @@ -422,7 +423,11 @@ class StremioService { this.installedAddons = new Map(); for (const addon of parsed) { if (addon && addon.id) { - this.installedAddons.set(addon.id, addon); + // Migration: Ensure installationId exists + if (!addon.installationId) { + addon.installationId = addon.id; + } + this.installedAddons.set(addon.installationId, addon); } } } @@ -431,14 +436,21 @@ class StremioService { const cinemetaId = 'com.linvo.cinemeta'; const hasUserRemovedCinemeta = await this.hasUserRemovedAddon(cinemetaId); - if (!this.installedAddons.has(cinemetaId) && !hasUserRemovedCinemeta) { + // Check if any installed addon has this ID (using valid values iteration) + const isCinemetaInstalled = Array.from(this.installedAddons.values()).some(a => a.id === cinemetaId); + + if (!isCinemetaInstalled && !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); } catch (error) { // Fallback to minimal manifest if fetch fails const fallbackManifest: Manifest = { id: cinemetaId, + installationId: cinemetaId, name: 'Cinemeta', version: '3.0.13', description: 'Provides metadata for movies and series from TheTVDB, TheMovieDB, etc.', @@ -483,13 +495,17 @@ class StremioService { const opensubsId = 'org.stremio.opensubtitlesv3'; const hasUserRemovedOpenSubtitles = await this.hasUserRemovedAddon(opensubsId); - if (!this.installedAddons.has(opensubsId) && !hasUserRemovedOpenSubtitles) { + const isOpenSubsInstalled = Array.from(this.installedAddons.values()).some(a => a.id === opensubsId); + + if (!isOpenSubsInstalled && !hasUserRemovedOpenSubtitles) { try { const opensubsManifest = await this.getManifest('https://opensubtitles-v3.strem.io/manifest.json'); + opensubsManifest.installationId = opensubsId; this.installedAddons.set(opensubsId, opensubsManifest); } catch (error) { const fallbackManifest: Manifest = { id: opensubsId, + installationId: opensubsId, name: 'OpenSubtitles v3', version: '1.0.0', description: 'OpenSubtitles v3 Addon for Stremio', @@ -522,8 +538,9 @@ class StremioService { 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 and it's installed 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); } @@ -651,43 +668,54 @@ class StremioService { async installAddon(url: string): Promise { const manifest = await this.getManifest(url); if (manifest && manifest.id) { - this.installedAddons.set(manifest.id, manifest); + // Generate a unique installation ID + const installationId = `${manifest.id}-${Date.now()}-${Math.floor(Math.random() * 10000)}`; + manifest.installationId = installationId; + + this.installedAddons.set(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); - // Add to order if not already present (new addons go to the end) - if (!this.addonOrder.includes(manifest.id)) { - this.addonOrder.push(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); await this.saveInstalledAddons(); await this.saveAddonOrder(); // Emit an event that an addon was added - addonEmitter.emit(ADDON_EVENTS.ADDON_ADDED, manifest.id); + addonEmitter.emit(ADDON_EVENTS.ADDON_ADDED, installationId); } else { throw new Error('Invalid addon manifest'); } } - async removeAddon(id: string): Promise { - // Allow removal of any addon, including pre-installed ones like Cinemeta - if (this.installedAddons.has(id)) { - this.installedAddons.delete(id); + async removeAddon(installationId: string): Promise { + // Allow removal of any addon + if (this.installedAddons.has(installationId)) { + const addon = this.installedAddons.get(installationId); + this.installedAddons.delete(installationId); // Remove from order - this.addonOrder = this.addonOrder.filter(addonId => addonId !== id); + this.addonOrder = this.addonOrder.filter(id => id !== installationId); - // Track user explicit removal for any addon (tombstone) - await this.markAddonAsRemovedByUser(id); - // Proactively clean up any persisted orders/legacy keys for this addon - await this.cleanupRemovedAddonFromStorage(id); + // Track user explicit removal for the addon ID (tombstone to prevent auto-reinstall of defaults) + if (addon && addon.id) { + await this.markAddonAsRemovedByUser(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(); // Emit an event that an addon was removed - addonEmitter.emit(ADDON_EVENTS.ADDON_REMOVED, id); + addonEmitter.emit(ADDON_EVENTS.ADDON_REMOVED, installationId); } } @@ -1402,7 +1430,7 @@ class StremioService { try { if (!addon.url) { logger.warn(`⚠️ [getStreams] Addon ${addon.id} has no URL`); - if (callback) callback(null, addon.id, addon.name, new Error('Addon has no URL')); + if (callback) callback(null, addon.installationId || addon.id, addon.name, new Error('Addon has no URL')); return; } @@ -1427,12 +1455,13 @@ class StremioService { if (callback) { // Call callback with processed streams (can be empty array) - callback(processedStreams, addon.id, addon.name, null); + // Use installationId if available to distinct between multiple installations of same addon + callback(processedStreams, addon.installationId || addon.id, addon.name, null); } } catch (error) { if (callback) { // Call callback with error - callback(null, addon.id, addon.name, error as Error); + callback(null, addon.installationId || addon.id, addon.name, error as Error); } } })(); // Immediately invoke the async function @@ -1613,7 +1642,7 @@ class StremioService { name: name, title: displayTitle, addonName: addon.name, - addonId: addon.id, + addonId: addon.installationId || addon.id, // Include description as-is to preserve full details description: stream.description,