mirror of
https://github.com/tapframe/NuvioStreaming.git
synced 2026-03-11 17:45:38 +00:00
todo:fix moviesmod
This commit is contained in:
parent
3e5a547bd8
commit
335012c792
5 changed files with 404 additions and 72 deletions
|
|
@ -39,6 +39,7 @@ import LogoSourceSettings from '../screens/LogoSourceSettings';
|
|||
import ThemeScreen from '../screens/ThemeScreen';
|
||||
import ProfilesScreen from '../screens/ProfilesScreen';
|
||||
import OnboardingScreen from '../screens/OnboardingScreen';
|
||||
import PluginsScreen from '../screens/PluginsScreen';
|
||||
|
||||
// Stack navigator types
|
||||
export type RootStackParamList = {
|
||||
|
|
@ -104,6 +105,7 @@ export type RootStackParamList = {
|
|||
LogoSourceSettings: undefined;
|
||||
ThemeSettings: undefined;
|
||||
ProfilesSettings: undefined;
|
||||
Plugins: undefined;
|
||||
};
|
||||
|
||||
export type RootStackNavigationProp = NativeStackNavigationProp<RootStackParamList>;
|
||||
|
|
@ -1013,6 +1015,21 @@ const AppNavigator = ({ initialRouteName }: { initialRouteName?: keyof RootStack
|
|||
},
|
||||
}}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name="Plugins"
|
||||
component={PluginsScreen}
|
||||
options={{
|
||||
animation: Platform.OS === 'android' ? 'slide_from_right' : 'fade',
|
||||
animationDuration: Platform.OS === 'android' ? 250 : 200,
|
||||
presentation: 'card',
|
||||
gestureEnabled: true,
|
||||
gestureDirection: 'horizontal',
|
||||
headerShown: false,
|
||||
contentStyle: {
|
||||
backgroundColor: currentTheme.colors.darkBackground,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</Stack.Navigator>
|
||||
</View>
|
||||
</PaperProvider>
|
||||
|
|
|
|||
253
src/screens/PluginsScreen.tsx
Normal file
253
src/screens/PluginsScreen.tsx
Normal file
|
|
@ -0,0 +1,253 @@
|
|||
import React, { useCallback, useState, useEffect } from 'react';
|
||||
import {
|
||||
View,
|
||||
Text,
|
||||
StyleSheet,
|
||||
TouchableOpacity,
|
||||
ScrollView,
|
||||
SafeAreaView,
|
||||
StatusBar,
|
||||
Alert,
|
||||
Platform,
|
||||
Dimensions,
|
||||
TextInput,
|
||||
ActivityIndicator,
|
||||
} from 'react-native';
|
||||
import { useNavigation } from '@react-navigation/native';
|
||||
import MaterialIcons from 'react-native-vector-icons/MaterialIcons';
|
||||
import { useTheme } from '../contexts/ThemeContext';
|
||||
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
||||
import { pluginManager } from '../services/PluginManager';
|
||||
import { SettingItem, SettingsCard } from './SettingsScreen'; // Assuming these are exported
|
||||
|
||||
const { width } = Dimensions.get('window');
|
||||
|
||||
interface LoadedPlugin {
|
||||
name: string;
|
||||
version: string;
|
||||
sourceUrl?: string;
|
||||
}
|
||||
|
||||
const PluginsScreen: React.FC = () => {
|
||||
const navigation = useNavigation();
|
||||
const { currentTheme } = useTheme();
|
||||
const insets = useSafeAreaInsets();
|
||||
|
||||
const [pluginUrl, setPluginUrl] = useState('');
|
||||
const [loadedPlugins, setLoadedPlugins] = useState<LoadedPlugin[]>([]);
|
||||
const [isLoadingPlugin, setIsLoadingPlugin] = useState(false);
|
||||
|
||||
const refreshPluginsList = useCallback(() => {
|
||||
const plugins = pluginManager.getScraperPlugins();
|
||||
setLoadedPlugins(
|
||||
plugins.map(p => ({
|
||||
name: p.name,
|
||||
version: p.version,
|
||||
sourceUrl: p.sourceUrl,
|
||||
}))
|
||||
);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
refreshPluginsList();
|
||||
}, [refreshPluginsList]);
|
||||
|
||||
const handleLoadPlugin = useCallback(async () => {
|
||||
if (!pluginUrl.trim() || !pluginUrl.startsWith('http')) {
|
||||
Alert.alert('Invalid URL', 'Please enter a valid plugin URL.');
|
||||
return;
|
||||
}
|
||||
setIsLoadingPlugin(true);
|
||||
const success = await pluginManager.loadPluginFromUrl(pluginUrl.trim());
|
||||
setIsLoadingPlugin(false);
|
||||
if (success) {
|
||||
Alert.alert('Success', 'Plugin loaded successfully.');
|
||||
setPluginUrl('');
|
||||
refreshPluginsList();
|
||||
} else {
|
||||
Alert.alert(
|
||||
'Error',
|
||||
'Failed to load the plugin. Check the URL and console for errors.'
|
||||
);
|
||||
}
|
||||
}, [pluginUrl, refreshPluginsList]);
|
||||
|
||||
const handleRemovePlugin = useCallback(
|
||||
(sourceUrl: string) => {
|
||||
if (!sourceUrl) return;
|
||||
Alert.alert(
|
||||
'Remove Plugin',
|
||||
'Are you sure you want to remove this plugin?',
|
||||
[
|
||||
{ text: 'Cancel', style: 'cancel' },
|
||||
{
|
||||
text: 'Remove',
|
||||
style: 'destructive',
|
||||
onPress: () => {
|
||||
pluginManager.removePlugin(sourceUrl);
|
||||
refreshPluginsList();
|
||||
},
|
||||
},
|
||||
]
|
||||
);
|
||||
},
|
||||
[refreshPluginsList]
|
||||
);
|
||||
|
||||
const headerBaseHeight = Platform.OS === 'android' ? 80 : 60;
|
||||
const topSpacing = Platform.OS === 'android' ? StatusBar.currentHeight || 0 : insets.top;
|
||||
const headerHeight = headerBaseHeight + topSpacing;
|
||||
|
||||
return (
|
||||
<View style={[styles.container, { backgroundColor: currentTheme.colors.darkBackground }]}>
|
||||
<StatusBar barStyle={'light-content'} />
|
||||
<View style={{ flex: 1 }}>
|
||||
<View style={[styles.header, { height: headerHeight, paddingTop: topSpacing }]}>
|
||||
<TouchableOpacity onPress={() => navigation.goBack()} style={styles.backButton}>
|
||||
<MaterialIcons name="arrow-back" size={24} color={currentTheme.colors.text} />
|
||||
</TouchableOpacity>
|
||||
<Text style={[styles.headerTitle, { color: currentTheme.colors.text }]}>
|
||||
Plugins
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
<View style={styles.contentContainer}>
|
||||
<ScrollView
|
||||
style={styles.scrollView}
|
||||
showsVerticalScrollIndicator={false}
|
||||
contentContainerStyle={styles.scrollContent}
|
||||
>
|
||||
<SettingsCard title="Add Plugin">
|
||||
<View style={[styles.pluginInputContainer, { borderBottomColor: currentTheme.colors.elevation2 }]}>
|
||||
<TextInput
|
||||
style={[styles.pluginInput, { color: currentTheme.colors.highEmphasis }]}
|
||||
placeholder="Enter plugin URL..."
|
||||
placeholderTextColor={currentTheme.colors.mediumEmphasis}
|
||||
value={pluginUrl}
|
||||
onChangeText={setPluginUrl}
|
||||
autoCapitalize="none"
|
||||
autoCorrect={false}
|
||||
keyboardType="url"
|
||||
returnKeyType="done"
|
||||
onSubmitEditing={handleLoadPlugin}
|
||||
/>
|
||||
<TouchableOpacity
|
||||
style={[styles.loadButton, { backgroundColor: isLoadingPlugin ? currentTheme.colors.mediumGray : currentTheme.colors.primary }]}
|
||||
onPress={handleLoadPlugin}
|
||||
disabled={isLoadingPlugin}
|
||||
>
|
||||
{isLoadingPlugin ? (
|
||||
<ActivityIndicator size="small" color={currentTheme.colors.white} />
|
||||
) : (
|
||||
<Text style={[styles.loadButtonText, { color: currentTheme.colors.white }]}>Load</Text>
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</SettingsCard>
|
||||
|
||||
<SettingsCard title="Loaded Scrapers">
|
||||
{loadedPlugins.length > 0 ? (
|
||||
loadedPlugins.map((plugin, index) => (
|
||||
<SettingItem
|
||||
key={plugin.sourceUrl || index}
|
||||
icon="extension"
|
||||
title={`${plugin.name} v${plugin.version}`}
|
||||
description={plugin.sourceUrl ? 'External' : 'Built-in'}
|
||||
isLast={index === loadedPlugins.length - 1}
|
||||
renderControl={() =>
|
||||
plugin.sourceUrl ? (
|
||||
<TouchableOpacity onPress={() => handleRemovePlugin(plugin.sourceUrl!)} style={styles.removeButton}>
|
||||
<MaterialIcons name="close" size={20} color={currentTheme.colors.warning} />
|
||||
</TouchableOpacity>
|
||||
) : null
|
||||
}
|
||||
/>
|
||||
))
|
||||
) : (
|
||||
<SettingItem
|
||||
icon="extension"
|
||||
title="No Custom Scrapers"
|
||||
description="Add a plugin URL above to get started"
|
||||
isLast={true}
|
||||
/>
|
||||
)}
|
||||
</SettingsCard>
|
||||
</ScrollView>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
},
|
||||
header: {
|
||||
paddingHorizontal: Math.max(1, width * 0.05),
|
||||
flexDirection: 'row',
|
||||
alignItems: 'flex-end',
|
||||
paddingBottom: 8,
|
||||
backgroundColor: 'transparent',
|
||||
zIndex: 2,
|
||||
},
|
||||
backButton: {
|
||||
position: 'absolute',
|
||||
left: Math.max(1, width * 0.05),
|
||||
bottom: 8,
|
||||
zIndex: 10,
|
||||
},
|
||||
headerTitle: {
|
||||
flex: 1,
|
||||
textAlign: 'center',
|
||||
fontSize: Math.min(24, width * 0.06),
|
||||
fontWeight: '700',
|
||||
letterSpacing: 0.3,
|
||||
},
|
||||
contentContainer: {
|
||||
flex: 1,
|
||||
zIndex: 1,
|
||||
width: '100%',
|
||||
},
|
||||
scrollView: {
|
||||
flex: 1,
|
||||
width: '100%',
|
||||
},
|
||||
scrollContent: {
|
||||
flexGrow: 1,
|
||||
width: '100%',
|
||||
paddingBottom: 100,
|
||||
},
|
||||
pluginInputContainer: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
paddingHorizontal: 16,
|
||||
paddingVertical: 8,
|
||||
borderBottomWidth: 1,
|
||||
},
|
||||
pluginInput: {
|
||||
flex: 1,
|
||||
fontSize: 16,
|
||||
paddingVertical: 10,
|
||||
},
|
||||
loadButton: {
|
||||
marginLeft: 12,
|
||||
paddingHorizontal: 16,
|
||||
height: 40,
|
||||
borderRadius: 20,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
minWidth: 80,
|
||||
},
|
||||
loadButtonText: {
|
||||
fontSize: 15,
|
||||
fontWeight: '600',
|
||||
},
|
||||
removeButton: {
|
||||
padding: 8,
|
||||
borderRadius: 20,
|
||||
marginLeft: 8,
|
||||
},
|
||||
});
|
||||
|
||||
export default PluginsScreen;
|
||||
|
|
@ -38,12 +38,12 @@ const { width } = Dimensions.get('window');
|
|||
const ANDROID_STATUSBAR_HEIGHT = StatusBar.currentHeight || 0;
|
||||
|
||||
// Card component with minimalistic style
|
||||
interface SettingsCardProps {
|
||||
export interface SettingsCardProps {
|
||||
children: React.ReactNode;
|
||||
title?: string;
|
||||
}
|
||||
|
||||
const SettingsCard: React.FC<SettingsCardProps> = ({ children, title }) => {
|
||||
export const SettingsCard: React.FC<SettingsCardProps> = ({ children, title }) => {
|
||||
const { currentTheme } = useTheme();
|
||||
|
||||
return (
|
||||
|
|
@ -68,7 +68,7 @@ const SettingsCard: React.FC<SettingsCardProps> = ({ children, title }) => {
|
|||
);
|
||||
};
|
||||
|
||||
interface SettingItemProps {
|
||||
export interface SettingItemProps {
|
||||
title: string;
|
||||
description?: string;
|
||||
icon: string;
|
||||
|
|
@ -78,7 +78,7 @@ interface SettingItemProps {
|
|||
badge?: string | number;
|
||||
}
|
||||
|
||||
const SettingItem: React.FC<SettingItemProps> = ({
|
||||
export const SettingItem: React.FC<SettingItemProps> = ({
|
||||
title,
|
||||
description,
|
||||
icon,
|
||||
|
|
@ -131,6 +131,12 @@ const SettingItem: React.FC<SettingItemProps> = ({
|
|||
);
|
||||
};
|
||||
|
||||
interface LoadedPlugin {
|
||||
name: string;
|
||||
version: string;
|
||||
sourceUrl?: string;
|
||||
}
|
||||
|
||||
const SettingsScreen: React.FC = () => {
|
||||
const { settings, updateSetting } = useSettings();
|
||||
const navigation = useNavigation<NavigationProp<RootStackParamList>>();
|
||||
|
|
@ -160,32 +166,14 @@ const SettingsScreen: React.FC = () => {
|
|||
const [addonCount, setAddonCount] = useState<number>(0);
|
||||
const [catalogCount, setCatalogCount] = useState<number>(0);
|
||||
const [mdblistKeySet, setMdblistKeySet] = useState<boolean>(false);
|
||||
const [pluginUrl, setPluginUrl] = useState('');
|
||||
|
||||
const [loadedPlugins, setLoadedPlugins] = useState<string[]>([]);
|
||||
const [isLoadingPlugin, setIsLoadingPlugin] = useState(false);
|
||||
|
||||
const refreshPluginsList = useCallback(() => {
|
||||
const plugins = pluginManager.getScraperPlugins();
|
||||
setLoadedPlugins(plugins.map(p => `${p.name} v${p.version}`));
|
||||
}, []);
|
||||
|
||||
const handleLoadPlugin = useCallback(async () => {
|
||||
if (!pluginUrl.trim() || !pluginUrl.startsWith('http')) {
|
||||
Alert.alert('Invalid URL', 'Please enter a valid plugin URL.');
|
||||
return;
|
||||
}
|
||||
setIsLoadingPlugin(true);
|
||||
const success = await pluginManager.loadPluginFromUrl(pluginUrl.trim());
|
||||
setIsLoadingPlugin(false);
|
||||
if (success) {
|
||||
Alert.alert('Success', 'Plugin loaded successfully.');
|
||||
setPluginUrl('');
|
||||
refreshPluginsList();
|
||||
} else {
|
||||
Alert.alert('Error', 'Failed to load the plugin. Check the URL and console for errors.');
|
||||
}
|
||||
}, [pluginUrl, refreshPluginsList]);
|
||||
|
||||
const loadData = useCallback(async () => {
|
||||
try {
|
||||
// Load addon count and get their catalogs
|
||||
|
|
@ -418,6 +406,17 @@ const SettingsScreen: React.FC = () => {
|
|||
isLast={true}
|
||||
/>
|
||||
</SettingsCard>
|
||||
|
||||
<SettingsCard title="Plugins">
|
||||
<SettingItem
|
||||
title="Manage Plugins"
|
||||
description="Add or remove external plugins"
|
||||
icon="extension"
|
||||
onPress={() => navigation.navigate('Plugins')}
|
||||
renderControl={ChevronRight}
|
||||
isLast={true}
|
||||
/>
|
||||
</SettingsCard>
|
||||
|
||||
{/* Playback & Experience */}
|
||||
<SettingsCard title="PLAYBACK">
|
||||
|
|
@ -526,40 +525,6 @@ const SettingsScreen: React.FC = () => {
|
|||
</SettingsCard>
|
||||
)}
|
||||
|
||||
<SettingsCard title="Plugins">
|
||||
<View style={[styles.pluginInputContainer, { borderBottomColor: currentTheme.colors.elevation2 }]}>
|
||||
<TextInput
|
||||
style={[styles.pluginInput, { color: currentTheme.colors.highEmphasis }]}
|
||||
placeholder="Enter plugin URL..."
|
||||
placeholderTextColor={currentTheme.colors.mediumEmphasis}
|
||||
value={pluginUrl}
|
||||
onChangeText={setPluginUrl}
|
||||
autoCapitalize="none"
|
||||
autoCorrect={false}
|
||||
keyboardType="url"
|
||||
returnKeyType="done"
|
||||
onSubmitEditing={handleLoadPlugin}
|
||||
/>
|
||||
<TouchableOpacity
|
||||
style={[styles.loadButton, { backgroundColor: isLoadingPlugin ? currentTheme.colors.mediumGray : currentTheme.colors.primary }]}
|
||||
onPress={handleLoadPlugin}
|
||||
disabled={isLoadingPlugin}
|
||||
>
|
||||
{isLoadingPlugin ? (
|
||||
<ActivityIndicator size="small" color={currentTheme.colors.white} />
|
||||
) : (
|
||||
<Text style={[styles.loadButtonText, { color: currentTheme.colors.white }]}>Load</Text>
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
<SettingItem
|
||||
icon="extension"
|
||||
title="Loaded Scrapers"
|
||||
description={loadedPlugins.length > 0 ? loadedPlugins.join(', ') : 'No custom plugins loaded'}
|
||||
isLast={true}
|
||||
/>
|
||||
</SettingsCard>
|
||||
|
||||
<View style={styles.footer}>
|
||||
<Text style={[styles.footerText, { color: currentTheme.colors.mediumEmphasis }]}>
|
||||
Made with ❤️ by the Nuvio team
|
||||
|
|
@ -741,6 +706,11 @@ const styles = StyleSheet.create({
|
|||
fontSize: 15,
|
||||
fontWeight: '600',
|
||||
},
|
||||
removeButton: {
|
||||
padding: 8,
|
||||
borderRadius: 20,
|
||||
marginLeft: 8,
|
||||
},
|
||||
});
|
||||
|
||||
export default SettingsScreen;
|
||||
|
|
@ -1,13 +1,11 @@
|
|||
// Export all screens from a single file
|
||||
export { default as HomeScreen } from './HomeScreen';
|
||||
export { default as PlayerSettingsScreen } from './PlayerSettingsScreen';
|
||||
export { default as ProfilesScreen } from './ProfilesScreen';
|
||||
export { default as SearchScreen } from './SearchScreen';
|
||||
export { default as AddonsScreen } from './AddonsScreen';
|
||||
export { default as SettingsScreen } from './SettingsScreen';
|
||||
export { default as MetadataScreen } from './MetadataScreen';
|
||||
export { default as CatalogScreen } from './CatalogScreen';
|
||||
export { default as DiscoverScreen } from './DiscoverScreen';
|
||||
export { default as LibraryScreen } from './LibraryScreen';
|
||||
export { default as ShowRatingsScreen } from './ShowRatingsScreen';
|
||||
export { default as CatalogSettingsScreen } from './CatalogSettingsScreen';
|
||||
export { default as StreamsScreen } from './StreamsScreen';
|
||||
export { default as OnboardingScreen } from './OnboardingScreen';
|
||||
export { default as ThemeScreen } from './ThemeScreen';
|
||||
export { default as TMDBSettingsScreen } from './TMDBSettingsScreen';
|
||||
export { default as TraktSettingsScreen } from './TraktSettingsScreen';
|
||||
export { default as PluginsScreen } from './PluginsScreen';
|
||||
|
|
@ -1,9 +1,12 @@
|
|||
import { logger } from '../utils/logger';
|
||||
import * as cheerio from 'cheerio';
|
||||
import AsyncStorage from '@react-native-async-storage/async-storage';
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore – no types for Babel standalone
|
||||
const Babel = require('@babel/standalone');
|
||||
|
||||
const PLUGIN_URLS_STORAGE_KEY = '@plugin_urls';
|
||||
|
||||
// --- Type Definitions ---
|
||||
|
||||
interface Plugin {
|
||||
|
|
@ -13,6 +16,7 @@ interface Plugin {
|
|||
description: string;
|
||||
type: 'scraper' | 'other';
|
||||
getStreams: (options: GetStreamsOptions) => Promise<Stream[]>;
|
||||
sourceUrl?: string; // To track the origin of the plugin
|
||||
}
|
||||
|
||||
interface Stream {
|
||||
|
|
@ -104,6 +108,9 @@ class PluginManager {
|
|||
|
||||
private constructor() {
|
||||
this.loadBuiltInPlugins();
|
||||
this.loadPersistedPlugins().catch(err => {
|
||||
logger.error('[PluginManager] Error during async initialization', err);
|
||||
});
|
||||
}
|
||||
|
||||
public static getInstance(): PluginManager {
|
||||
|
|
@ -113,20 +120,89 @@ class PluginManager {
|
|||
return PluginManager.instance;
|
||||
}
|
||||
|
||||
public async loadPluginFromUrl(url: string): Promise<boolean> {
|
||||
private async loadPersistedPlugins() {
|
||||
try {
|
||||
const storedUrlsJson = await AsyncStorage.getItem(PLUGIN_URLS_STORAGE_KEY);
|
||||
if (storedUrlsJson) {
|
||||
const urls = JSON.parse(storedUrlsJson);
|
||||
if (Array.isArray(urls)) {
|
||||
logger.log('[PluginManager] Loading persisted plugins...', urls);
|
||||
for (const url of urls) {
|
||||
await this.loadPluginFromUrl(url, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('[PluginManager] Failed to load persisted plugins:', error);
|
||||
}
|
||||
}
|
||||
|
||||
private async persistPluginUrl(url: string) {
|
||||
try {
|
||||
const storedUrlsJson = await AsyncStorage.getItem(PLUGIN_URLS_STORAGE_KEY);
|
||||
let urls: string[] = [];
|
||||
if (storedUrlsJson) {
|
||||
urls = JSON.parse(storedUrlsJson);
|
||||
}
|
||||
if (!urls.includes(url)) {
|
||||
urls.push(url);
|
||||
await AsyncStorage.setItem(PLUGIN_URLS_STORAGE_KEY, JSON.stringify(urls));
|
||||
logger.log(`[PluginManager] Persisted plugin URL: ${url}`);
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('[PluginManager] Failed to persist plugin URL:', error);
|
||||
}
|
||||
}
|
||||
|
||||
private async removePersistedPluginUrl(url: string) {
|
||||
try {
|
||||
const storedUrlsJson = await AsyncStorage.getItem(PLUGIN_URLS_STORAGE_KEY);
|
||||
if (storedUrlsJson) {
|
||||
let urls: string[] = JSON.parse(storedUrlsJson);
|
||||
const index = urls.indexOf(url);
|
||||
if (index > -1) {
|
||||
urls.splice(index, 1);
|
||||
await AsyncStorage.setItem(PLUGIN_URLS_STORAGE_KEY, JSON.stringify(urls));
|
||||
logger.log(`[PluginManager] Removed persisted plugin URL: ${url}`);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('[PluginManager] Failed to remove persisted plugin URL:', error);
|
||||
}
|
||||
}
|
||||
|
||||
public async loadPluginFromUrl(url: string, persist = true): Promise<boolean> {
|
||||
logger.log(`[PluginManager] Attempting to load plugin from URL: ${url}`);
|
||||
|
||||
if (this.plugins.some(p => p.sourceUrl === url)) {
|
||||
logger.log(`[PluginManager] Plugin from URL ${url} is already loaded.`);
|
||||
return true;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(url);
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to fetch plugin from URL: ${response.statusText}`);
|
||||
}
|
||||
const pluginCode = await response.text();
|
||||
this.runPlugin(pluginCode);
|
||||
// Assuming runPlugin is synchronous for registration purposes.
|
||||
// A more robust system might have runPlugin return success/failure.
|
||||
return true;
|
||||
const newPlugin = this.runPlugin(pluginCode, url);
|
||||
|
||||
if (newPlugin) {
|
||||
if (persist) {
|
||||
await this.persistPluginUrl(url);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
logger.error(`[PluginManager] Plugin from ${url} executed but failed to register.`);
|
||||
return false;
|
||||
|
||||
} catch (error) {
|
||||
logger.error(`[PluginManager] Failed to load plugin from URL ${url}:`, error);
|
||||
if (persist === false) {
|
||||
logger.log(`[PluginManager] Removing failed persisted plugin URL: ${url}`);
|
||||
await this.removePersistedPluginUrl(url);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
|
@ -144,7 +220,7 @@ class PluginManager {
|
|||
};
|
||||
|
||||
// Require and execute the built-in MoviesMod plugin module (IIFE)
|
||||
require('./plugins/moviesmod.plugin.js');
|
||||
//require('./plugins/moviesmod.plugin.js');
|
||||
|
||||
delete (global as any).registerPlugin;
|
||||
} catch (error) {
|
||||
|
|
@ -152,7 +228,22 @@ class PluginManager {
|
|||
}
|
||||
}
|
||||
|
||||
private runPlugin(pluginCode: string) {
|
||||
public removePlugin(sourceUrl: string) {
|
||||
const pluginIndex = this.plugins.findIndex(p => p.sourceUrl === sourceUrl);
|
||||
if (pluginIndex > -1) {
|
||||
const plugin = this.plugins[pluginIndex];
|
||||
this.plugins.splice(pluginIndex, 1);
|
||||
logger.log(`[PluginManager] Removed plugin: ${plugin.name}`);
|
||||
if (plugin.sourceUrl) {
|
||||
this.removePersistedPluginUrl(plugin.sourceUrl).catch(err => {
|
||||
logger.error(`[PluginManager] Failed to remove persisted URL: ${plugin.sourceUrl}`, err);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private runPlugin(pluginCode: string, sourceUrl?: string): Plugin | null {
|
||||
let registeredPlugin: Plugin | null = null;
|
||||
const pluginsBefore = this.plugins.length;
|
||||
|
||||
// Attempt to strip the JSDoc-style header comment which may cause parsing issues in some JS engines.
|
||||
|
|
@ -164,7 +255,9 @@ class PluginManager {
|
|||
// This is simpler and more reliable than using `with` or the Function constructor's scope.
|
||||
(global as any).registerPlugin = (plugin: Plugin) => {
|
||||
if (plugin && typeof plugin.getStreams === 'function') {
|
||||
if (sourceUrl) plugin.sourceUrl = sourceUrl;
|
||||
this.plugins.push(plugin);
|
||||
registeredPlugin = plugin;
|
||||
logger.log(`[PluginManager] Successfully registered plugin: ${plugin.name} v${plugin.version}`);
|
||||
} else {
|
||||
logger.error('[PluginManager] An invalid plugin was passed to registerPlugin.');
|
||||
|
|
@ -192,6 +285,7 @@ class PluginManager {
|
|||
// Clean up the global scope to prevent pollution
|
||||
delete (global as any).registerPlugin;
|
||||
}
|
||||
return registeredPlugin;
|
||||
}
|
||||
|
||||
public getScraperPlugins(): Plugin[] {
|
||||
|
|
|
|||
Loading…
Reference in a new issue