mirror of
https://github.com/tapframe/NuvioStreaming.git
synced 2026-01-11 20:10:25 +00:00
refactor for tf
This commit is contained in:
parent
8a34bf6678
commit
9cc8b2ea67
6 changed files with 431 additions and 602 deletions
|
|
@ -155,7 +155,7 @@ export type RootStackParamList = {
|
|||
TraktSettings: undefined;
|
||||
PlayerSettings: undefined;
|
||||
ThemeSettings: undefined;
|
||||
ScraperSettings: undefined;
|
||||
PluginSettings: undefined;
|
||||
CastMovies: {
|
||||
castMember: {
|
||||
id: number;
|
||||
|
|
@ -1463,7 +1463,7 @@ const InnerNavigator = ({ initialRouteName }: { initialRouteName?: keyof RootSta
|
|||
}}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name="ScraperSettings"
|
||||
name="PluginSettings"
|
||||
component={PluginsScreen}
|
||||
options={{
|
||||
animation: Platform.OS === 'android' ? 'slide_from_right' : 'fade',
|
||||
|
|
|
|||
|
|
@ -46,7 +46,7 @@ if (Platform.OS === 'ios') {
|
|||
}
|
||||
}
|
||||
// Removed community blur and expo-constants for Android overlay
|
||||
import axios from 'axios';
|
||||
|
||||
import { useTheme } from '../contexts/ThemeContext';
|
||||
|
||||
// Extend Manifest type to include logo only (remove disabled status)
|
||||
|
|
@ -60,11 +60,7 @@ interface ExtendedManifest extends Manifest {
|
|||
};
|
||||
}
|
||||
|
||||
// Interface for Community Addon structure from the JSON URL
|
||||
interface CommunityAddon {
|
||||
transportUrl: string;
|
||||
manifest: ExtendedManifest;
|
||||
}
|
||||
|
||||
|
||||
const { width } = Dimensions.get('window');
|
||||
|
||||
|
|
@ -623,10 +619,7 @@ const AddonsScreen = () => {
|
|||
const colors = currentTheme.colors;
|
||||
const styles = createStyles(colors);
|
||||
|
||||
// State for community addons
|
||||
const [communityAddons, setCommunityAddons] = useState<CommunityAddon[]>([]);
|
||||
const [communityLoading, setCommunityLoading] = useState(true);
|
||||
const [communityError, setCommunityError] = useState<string | null>(null);
|
||||
|
||||
|
||||
// Promotional addon: Nuvio Streams
|
||||
const PROMO_ADDON_URL = 'https://nuviostreams.hayd.uk/manifest.json';
|
||||
|
|
@ -652,7 +645,6 @@ const AddonsScreen = () => {
|
|||
|
||||
useEffect(() => {
|
||||
loadAddons();
|
||||
loadCommunityAddons();
|
||||
}, []);
|
||||
|
||||
const loadAddons = async () => {
|
||||
|
|
@ -702,27 +694,7 @@ const AddonsScreen = () => {
|
|||
}
|
||||
};
|
||||
|
||||
// Function to load community addons
|
||||
const loadCommunityAddons = async () => {
|
||||
setCommunityLoading(true);
|
||||
setCommunityError(null);
|
||||
try {
|
||||
const response = await axios.get<CommunityAddon[]>('https://stremio-addons.com/catalog.json');
|
||||
// Filter out addons without a manifest or transportUrl (basic validation)
|
||||
let validAddons = response.data.filter(addon => addon.manifest && addon.transportUrl);
|
||||
|
||||
// Filter out Cinemeta since it's now pre-installed
|
||||
validAddons = validAddons.filter(addon => addon.manifest.id !== 'com.linvo.cinemeta');
|
||||
|
||||
setCommunityAddons(validAddons);
|
||||
} catch (error) {
|
||||
logger.error('Failed to load community addons:', error);
|
||||
setCommunityError('Failed to load community addons. Please try again later.');
|
||||
setCommunityAddons([]);
|
||||
} finally {
|
||||
setCommunityLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleAddAddon = async (url?: string) => {
|
||||
let urlToInstall = url || addonUrl;
|
||||
|
|
@ -783,7 +755,6 @@ const AddonsScreen = () => {
|
|||
|
||||
const refreshAddons = async () => {
|
||||
loadAddons();
|
||||
loadCommunityAddons();
|
||||
};
|
||||
|
||||
const moveAddonUp = (addon: ExtendedManifest) => {
|
||||
|
|
@ -835,7 +806,7 @@ const AddonsScreen = () => {
|
|||
configUrl = addon.behaviorHints.configurationURL;
|
||||
logger.info(`Using configurationURL from behaviorHints: ${configUrl}`);
|
||||
}
|
||||
// If a transport URL was provided directly (for community addons)
|
||||
// If a transport URL was provided directly
|
||||
else if (transportUrl) {
|
||||
// Remove any trailing filename like manifest.json
|
||||
const baseUrl = transportUrl.replace(/\/[^\/]+\.json$/, '/');
|
||||
|
|
@ -1057,65 +1028,7 @@ const AddonsScreen = () => {
|
|||
);
|
||||
};
|
||||
|
||||
// Function to render community addon items
|
||||
const renderCommunityAddonItem = ({ item }: { item: CommunityAddon }) => {
|
||||
const { manifest, transportUrl } = item;
|
||||
const types = manifest.types || [];
|
||||
const description = manifest.description || 'No description provided.';
|
||||
// @ts-ignore - logo might exist
|
||||
const logo = manifest.logo || null;
|
||||
const categoryText = types.length > 0
|
||||
? types.map(t => t.charAt(0).toUpperCase() + t.slice(1)).join(' • ')
|
||||
: 'General';
|
||||
// Check if addon is configurable
|
||||
const isConfigurable = manifest.behaviorHints?.configurable === true;
|
||||
|
||||
return (
|
||||
<View style={styles.communityAddonItem}>
|
||||
{logo ? (
|
||||
<FastImage
|
||||
source={{ uri: logo }}
|
||||
style={styles.communityAddonIcon}
|
||||
resizeMode={FastImage.resizeMode.contain}
|
||||
/>
|
||||
) : (
|
||||
<View style={styles.communityAddonIconPlaceholder}>
|
||||
<MaterialIcons name="extension" size={22} color={colors.darkGray} />
|
||||
</View>
|
||||
)}
|
||||
<View style={styles.communityAddonDetails}>
|
||||
<Text style={styles.communityAddonName}>{manifest.name}</Text>
|
||||
<Text style={styles.communityAddonDesc} numberOfLines={2}>{description}</Text>
|
||||
<View style={styles.communityAddonMetaContainer}>
|
||||
<Text style={styles.communityAddonVersion}>v{manifest.version || 'N/A'}</Text>
|
||||
<Text style={styles.communityAddonDot}>•</Text>
|
||||
<Text style={styles.communityAddonCategory}>{categoryText}</Text>
|
||||
</View>
|
||||
</View>
|
||||
<View style={styles.addonActionButtons}>
|
||||
{isConfigurable && (
|
||||
<TouchableOpacity
|
||||
style={styles.configButton}
|
||||
onPress={() => handleConfigureAddon(manifest, transportUrl)}
|
||||
>
|
||||
<MaterialIcons name="settings" size={20} color={colors.primary} />
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
<TouchableOpacity
|
||||
style={[styles.installButton, installing && { opacity: 0.6 }]}
|
||||
onPress={() => handleAddAddon(transportUrl)}
|
||||
disabled={installing}
|
||||
>
|
||||
{installing ? (
|
||||
<ActivityIndicator size="small" color={colors.white} />
|
||||
) : (
|
||||
<MaterialIcons name="add" size={20} color={colors.white} />
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
const StatsCard = ({ value, label }: { value: number; label: string }) => (
|
||||
<View style={styles.statsCard}>
|
||||
|
|
@ -1316,91 +1229,7 @@ const AddonsScreen = () => {
|
|||
</View>
|
||||
)}
|
||||
|
||||
{/* Community Addons Section */}
|
||||
<View style={styles.section}>
|
||||
<Text style={styles.sectionTitle}>COMMUNITY ADDONS</Text>
|
||||
<View style={styles.addonList}>
|
||||
{communityLoading ? (
|
||||
<View style={styles.loadingContainer}>
|
||||
<ActivityIndicator size="large" color={colors.primary} />
|
||||
</View>
|
||||
) : communityError ? (
|
||||
<View style={styles.emptyContainer}>
|
||||
<MaterialIcons name="error-outline" size={32} color={colors.error} />
|
||||
<Text style={styles.emptyText}>{communityError}</Text>
|
||||
</View>
|
||||
) : communityAddons.length === 0 ? (
|
||||
<View style={styles.emptyContainer}>
|
||||
<MaterialIcons name="extension-off" size={32} color={colors.mediumGray} />
|
||||
<Text style={styles.emptyText}>No community addons available</Text>
|
||||
</View>
|
||||
) : (
|
||||
communityAddons.map((item, index) => (
|
||||
<View
|
||||
key={item.transportUrl}
|
||||
style={{ marginBottom: index === communityAddons.length - 1 ? 32 : 16 }}
|
||||
>
|
||||
<View style={styles.addonItem}>
|
||||
<View style={styles.addonHeader}>
|
||||
{item.manifest.logo ? (
|
||||
<FastImage
|
||||
source={{ uri: item.manifest.logo }}
|
||||
style={styles.addonIcon}
|
||||
resizeMode={FastImage.resizeMode.contain}
|
||||
/>
|
||||
) : (
|
||||
<View style={styles.addonIconPlaceholder}>
|
||||
<MaterialIcons name="extension" size={22} color={colors.mediumGray} />
|
||||
</View>
|
||||
)}
|
||||
<View style={styles.addonTitleContainer}>
|
||||
<Text style={styles.addonName}>{item.manifest.name}</Text>
|
||||
<View style={styles.addonMetaContainer}>
|
||||
<Text style={styles.addonVersion}>v{item.manifest.version || 'N/A'}</Text>
|
||||
<Text style={styles.addonDot}>•</Text>
|
||||
<Text style={styles.addonCategory}>
|
||||
{item.manifest.types && item.manifest.types.length > 0
|
||||
? item.manifest.types.map(t => t.charAt(0).toUpperCase() + t.slice(1)).join(' • ')
|
||||
: 'General'}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
<View style={styles.addonActions}>
|
||||
{item.manifest.behaviorHints?.configurable && (
|
||||
<TouchableOpacity
|
||||
style={styles.configButton}
|
||||
onPress={() => handleConfigureAddon(item.manifest, item.transportUrl)}
|
||||
>
|
||||
<MaterialIcons name="settings" size={20} color={colors.primary} />
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
<TouchableOpacity
|
||||
style={[styles.installButton, installing && { opacity: 0.6 }]}
|
||||
onPress={() => handleAddAddon(item.transportUrl)}
|
||||
disabled={installing}
|
||||
>
|
||||
{installing ? (
|
||||
<ActivityIndicator size="small" color={colors.white} />
|
||||
) : (
|
||||
<MaterialIcons name="add" size={20} color={colors.white} />
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<Text style={styles.addonDescription}>
|
||||
{item.manifest.description
|
||||
? (item.manifest.description.length > 100
|
||||
? item.manifest.description.substring(0, 100) + '...'
|
||||
: item.manifest.description)
|
||||
: 'No description provided.'}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
))
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
</ScrollView>
|
||||
)}
|
||||
|
||||
|
|
|
|||
|
|
@ -78,17 +78,17 @@ const createStyles = (colors: any) => StyleSheet.create({
|
|||
padding: 16,
|
||||
},
|
||||
sectionTitle: {
|
||||
fontSize: 20,
|
||||
fontWeight: '600',
|
||||
color: colors.white,
|
||||
marginBottom: 8,
|
||||
},
|
||||
fontSize: 20,
|
||||
fontWeight: '600',
|
||||
color: colors.white,
|
||||
marginBottom: 8,
|
||||
},
|
||||
sectionHeader: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
marginBottom: 16,
|
||||
},
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
marginBottom: 16,
|
||||
},
|
||||
sectionDescription: {
|
||||
fontSize: 14,
|
||||
color: colors.mediumGray,
|
||||
|
|
@ -283,59 +283,59 @@ const createStyles = (colors: any) => StyleSheet.create({
|
|||
marginTop: 8,
|
||||
},
|
||||
infoText: {
|
||||
fontSize: 14,
|
||||
color: colors.mediumEmphasis,
|
||||
lineHeight: 20,
|
||||
},
|
||||
content: {
|
||||
flex: 1,
|
||||
},
|
||||
emptyState: {
|
||||
alignItems: 'center',
|
||||
paddingVertical: 32,
|
||||
},
|
||||
emptyStateTitle: {
|
||||
fontSize: 18,
|
||||
fontWeight: '600',
|
||||
color: colors.white,
|
||||
marginTop: 16,
|
||||
marginBottom: 8,
|
||||
},
|
||||
emptyStateDescription: {
|
||||
fontSize: 14,
|
||||
color: colors.mediumGray,
|
||||
textAlign: 'center',
|
||||
lineHeight: 20,
|
||||
},
|
||||
scrapersList: {
|
||||
gap: 12,
|
||||
},
|
||||
scrapersContainer: {
|
||||
marginBottom: 24,
|
||||
},
|
||||
inputContainer: {
|
||||
marginBottom: 16,
|
||||
},
|
||||
lastSection: {
|
||||
borderBottomWidth: 0,
|
||||
},
|
||||
disabledSection: {
|
||||
opacity: 0.5,
|
||||
},
|
||||
disabledText: {
|
||||
color: colors.elevation3,
|
||||
},
|
||||
disabledContainer: {
|
||||
opacity: 0.5,
|
||||
},
|
||||
disabledInput: {
|
||||
backgroundColor: colors.elevation1,
|
||||
opacity: 0.5,
|
||||
},
|
||||
disabledButton: {
|
||||
opacity: 0.5,
|
||||
},
|
||||
disabledImage: {
|
||||
fontSize: 14,
|
||||
color: colors.mediumEmphasis,
|
||||
lineHeight: 20,
|
||||
},
|
||||
content: {
|
||||
flex: 1,
|
||||
},
|
||||
emptyState: {
|
||||
alignItems: 'center',
|
||||
paddingVertical: 32,
|
||||
},
|
||||
emptyStateTitle: {
|
||||
fontSize: 18,
|
||||
fontWeight: '600',
|
||||
color: colors.white,
|
||||
marginTop: 16,
|
||||
marginBottom: 8,
|
||||
},
|
||||
emptyStateDescription: {
|
||||
fontSize: 14,
|
||||
color: colors.mediumGray,
|
||||
textAlign: 'center',
|
||||
lineHeight: 20,
|
||||
},
|
||||
scrapersList: {
|
||||
gap: 12,
|
||||
},
|
||||
scrapersContainer: {
|
||||
marginBottom: 24,
|
||||
},
|
||||
inputContainer: {
|
||||
marginBottom: 16,
|
||||
},
|
||||
lastSection: {
|
||||
borderBottomWidth: 0,
|
||||
},
|
||||
disabledSection: {
|
||||
opacity: 0.5,
|
||||
},
|
||||
disabledText: {
|
||||
color: colors.elevation3,
|
||||
},
|
||||
disabledContainer: {
|
||||
opacity: 0.5,
|
||||
},
|
||||
disabledInput: {
|
||||
backgroundColor: colors.elevation1,
|
||||
opacity: 0.5,
|
||||
},
|
||||
disabledButton: {
|
||||
opacity: 0.5,
|
||||
},
|
||||
disabledImage: {
|
||||
opacity: 0.3,
|
||||
},
|
||||
availableIndicator: {
|
||||
|
|
@ -761,10 +761,10 @@ const CollapsibleSection: React.FC<{
|
|||
<View style={styles.collapsibleSection}>
|
||||
<TouchableOpacity style={styles.collapsibleHeader} onPress={onToggle}>
|
||||
<Text style={styles.collapsibleTitle}>{title}</Text>
|
||||
<Ionicons
|
||||
name={isExpanded ? "chevron-up" : "chevron-down"}
|
||||
size={20}
|
||||
color={colors.mediumGray}
|
||||
<Ionicons
|
||||
name={isExpanded ? "chevron-up" : "chevron-down"}
|
||||
size={20}
|
||||
color={colors.mediumGray}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
{isExpanded && <View style={styles.collapsibleContent}>{children}</View>}
|
||||
|
|
@ -803,7 +803,7 @@ const StatusBadge: React.FC<{
|
|||
};
|
||||
|
||||
const config = getStatusConfig();
|
||||
|
||||
|
||||
return (
|
||||
<View style={{
|
||||
flexDirection: 'row',
|
||||
|
|
@ -828,7 +828,7 @@ const PluginsScreen: React.FC = () => {
|
|||
const { currentTheme } = useTheme();
|
||||
const colors = currentTheme.colors;
|
||||
const styles = createStyles(colors);
|
||||
|
||||
|
||||
// CustomAlert state
|
||||
const [alertVisible, setAlertVisible] = useState(false);
|
||||
const [alertTitle, setAlertTitle] = useState('');
|
||||
|
|
@ -842,7 +842,7 @@ const PluginsScreen: React.FC = () => {
|
|||
) => {
|
||||
setAlertTitle(title);
|
||||
setAlertMessage(message);
|
||||
setAlertActions(actions && actions.length > 0 ? actions : [{ label: 'OK', onPress: () => {} }]);
|
||||
setAlertActions(actions && actions.length > 0 ? actions : [{ label: 'OK', onPress: () => { } }]);
|
||||
setAlertVisible(true);
|
||||
};
|
||||
|
||||
|
|
@ -856,14 +856,14 @@ const PluginsScreen: React.FC = () => {
|
|||
const [showboxSavedToken, setShowboxSavedToken] = useState<string>('');
|
||||
const [showboxScraperId, setShowboxScraperId] = useState<string | null>(null);
|
||||
const [showboxTokenVisible, setShowboxTokenVisible] = useState<boolean>(false);
|
||||
|
||||
|
||||
// Multiple repositories state
|
||||
const [repositories, setRepositories] = useState<RepositoryInfo[]>([]);
|
||||
const [currentRepositoryId, setCurrentRepositoryId] = useState<string>('');
|
||||
const [showAddRepositoryModal, setShowAddRepositoryModal] = useState(false);
|
||||
const [newRepositoryUrl, setNewRepositoryUrl] = useState('');
|
||||
const [switchingRepository, setSwitchingRepository] = useState<string | null>(null);
|
||||
|
||||
|
||||
// New UX state
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
const [selectedFilter, setSelectedFilter] = useState<'all' | 'movie' | 'tv'>('all');
|
||||
|
|
@ -897,7 +897,7 @@ const PluginsScreen: React.FC = () => {
|
|||
// Filter by search query
|
||||
if (searchQuery.trim()) {
|
||||
const query = searchQuery.toLowerCase();
|
||||
filtered = filtered.filter(scraper =>
|
||||
filtered = filtered.filter(scraper =>
|
||||
scraper.name.toLowerCase().includes(query) ||
|
||||
scraper.description.toLowerCase().includes(query) ||
|
||||
scraper.id.toLowerCase().includes(query)
|
||||
|
|
@ -906,7 +906,7 @@ const PluginsScreen: React.FC = () => {
|
|||
|
||||
// Filter by type
|
||||
if (selectedFilter !== 'all') {
|
||||
filtered = filtered.filter(scraper =>
|
||||
filtered = filtered.filter(scraper =>
|
||||
scraper.supportedTypes?.includes(selectedFilter as 'movie' | 'tv')
|
||||
);
|
||||
}
|
||||
|
|
@ -933,14 +933,14 @@ const PluginsScreen: React.FC = () => {
|
|||
const handleBulkToggle = async (enabled: boolean) => {
|
||||
try {
|
||||
setIsRefreshing(true);
|
||||
const promises = filteredScrapers.map(scraper =>
|
||||
const promises = filteredScrapers.map(scraper =>
|
||||
pluginService.setScraperEnabled(scraper.id, enabled)
|
||||
);
|
||||
await Promise.all(promises);
|
||||
await loadScrapers();
|
||||
openAlert('Success', `${enabled ? 'Enabled' : 'Disabled'} ${filteredScrapers.length} scrapers`);
|
||||
} catch (error) {
|
||||
logger.error('[ScraperSettings] Failed to bulk toggle:', error);
|
||||
logger.error('[PluginsScreen] Failed to bulk toggle:', error);
|
||||
openAlert('Error', 'Failed to update scrapers');
|
||||
} finally {
|
||||
setIsRefreshing(false);
|
||||
|
|
@ -994,14 +994,14 @@ const PluginsScreen: React.FC = () => {
|
|||
description: '',
|
||||
enabled: true
|
||||
});
|
||||
|
||||
|
||||
await loadRepositories();
|
||||
|
||||
|
||||
// Switch to the new repository and refresh it
|
||||
await pluginService.setCurrentRepository(repoId);
|
||||
await loadRepositories();
|
||||
await loadScrapers();
|
||||
|
||||
|
||||
setNewRepositoryUrl('');
|
||||
setShowAddRepositoryModal(false);
|
||||
openAlert('Success', 'Repository added and refreshed successfully');
|
||||
|
|
@ -1021,7 +1021,7 @@ const PluginsScreen: React.FC = () => {
|
|||
await loadScrapers();
|
||||
openAlert('Success', 'Repository switched successfully');
|
||||
} catch (error) {
|
||||
logger.error('[ScraperSettings] Failed to switch repository:', error);
|
||||
logger.error('[PluginsScreen] Failed to switch repository:', error);
|
||||
openAlert('Error', 'Failed to switch repository');
|
||||
} finally {
|
||||
setSwitchingRepository(null);
|
||||
|
|
@ -1034,9 +1034,9 @@ const PluginsScreen: React.FC = () => {
|
|||
|
||||
// Special handling for the last repository
|
||||
const isLastRepository = repositories.length === 1;
|
||||
|
||||
|
||||
const alertTitle = isLastRepository ? 'Remove Last Repository' : 'Remove Repository';
|
||||
const alertMessage = isLastRepository
|
||||
const alertMessage = isLastRepository
|
||||
? `Are you sure you want to remove "${repo.name}"? This is your only repository, so you'll have no scrapers available until you add a new repository.`
|
||||
: `Are you sure you want to remove "${repo.name}"? This will also remove all scrapers from this repository.`;
|
||||
|
||||
|
|
@ -1044,7 +1044,7 @@ const PluginsScreen: React.FC = () => {
|
|||
alertTitle,
|
||||
alertMessage,
|
||||
[
|
||||
{ label: 'Cancel', onPress: () => {} },
|
||||
{ label: 'Cancel', onPress: () => { } },
|
||||
{
|
||||
label: 'Remove',
|
||||
onPress: async () => {
|
||||
|
|
@ -1052,12 +1052,12 @@ const PluginsScreen: React.FC = () => {
|
|||
await pluginService.removeRepository(repoId);
|
||||
await loadRepositories();
|
||||
await loadScrapers();
|
||||
const successMessage = isLastRepository
|
||||
const successMessage = isLastRepository
|
||||
? 'Repository removed successfully. You can add a new repository using the "Add Repository" button.'
|
||||
: 'Repository removed successfully';
|
||||
openAlert('Success', successMessage);
|
||||
} catch (error) {
|
||||
logger.error('[ScraperSettings] Failed to remove repository:', error);
|
||||
logger.error('[PluginsScreen] Failed to remove repository:', error);
|
||||
openAlert('Error', error instanceof Error ? error.message : 'Failed to remove repository');
|
||||
}
|
||||
},
|
||||
|
|
@ -1097,7 +1097,7 @@ const PluginsScreen: React.FC = () => {
|
|||
setShowboxTokenVisible(false);
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('[ScraperSettings] Failed to load scrapers:', error);
|
||||
logger.error('[PluginsScreen] Failed to load scrapers:', error);
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -1105,20 +1105,20 @@ const PluginsScreen: React.FC = () => {
|
|||
try {
|
||||
// First refresh repository names from manifests for existing repositories
|
||||
await pluginService.refreshRepositoryNamesFromManifests();
|
||||
|
||||
|
||||
const repos = await pluginService.getRepositories();
|
||||
setRepositories(repos);
|
||||
setHasRepository(repos.length > 0);
|
||||
|
||||
|
||||
const currentRepoId = pluginService.getCurrentRepositoryId();
|
||||
setCurrentRepositoryId(currentRepoId);
|
||||
|
||||
|
||||
const currentRepo = repos.find(r => r.id === currentRepoId);
|
||||
if (currentRepo) {
|
||||
setRepositoryUrl(currentRepo.url);
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('[ScraperSettings] Failed to load repositories:', error);
|
||||
logger.error('[PluginsScreen] Failed to load repositories:', error);
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -1130,7 +1130,7 @@ const PluginsScreen: React.FC = () => {
|
|||
setRepositoryUrl(repoUrl);
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('[ScraperSettings] Failed to check repository:', error);
|
||||
logger.error('[PluginsScreen] Failed to check repository:', error);
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -1144,7 +1144,7 @@ const PluginsScreen: React.FC = () => {
|
|||
const url = repositoryUrl.trim();
|
||||
if (!url.startsWith('https://raw.githubusercontent.com/') && !url.startsWith('http://')) {
|
||||
openAlert(
|
||||
'Invalid URL Format',
|
||||
'Invalid URL Format',
|
||||
'Please use a valid GitHub raw URL format:\n\nhttps://raw.githubusercontent.com/username/repo/refs/heads/branch\n\nExample:\nhttps://raw.githubusercontent.com/tapframe/nuvio-providers/refs/heads/master'
|
||||
);
|
||||
return;
|
||||
|
|
@ -1157,7 +1157,7 @@ const PluginsScreen: React.FC = () => {
|
|||
setHasRepository(true);
|
||||
openAlert('Success', 'Repository URL saved successfully');
|
||||
} catch (error) {
|
||||
logger.error('[ScraperSettings] Failed to save repository:', error);
|
||||
logger.error('[PluginsScreen] Failed to save repository:', error);
|
||||
openAlert('Error', 'Failed to save repository URL');
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
|
|
@ -1199,7 +1199,7 @@ const PluginsScreen: React.FC = () => {
|
|||
// If enabling a scraper, ensure it's installed first
|
||||
const installedScrapers = await pluginService.getInstalledScrapers();
|
||||
const isInstalled = installedScrapers.some(scraper => scraper.id === scraperId);
|
||||
|
||||
|
||||
if (!isInstalled) {
|
||||
// Need to install the scraper first
|
||||
setIsRefreshing(true);
|
||||
|
|
@ -1207,32 +1207,32 @@ const PluginsScreen: React.FC = () => {
|
|||
setIsRefreshing(false);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
await pluginService.setScraperEnabled(scraperId, enabled);
|
||||
await loadScrapers();
|
||||
} catch (error) {
|
||||
logger.error('[ScraperSettings] Failed to toggle scraper:', error);
|
||||
openAlert('Error', 'Failed to update scraper status');
|
||||
logger.error('[PluginsScreen] Failed to toggle plugin:', error);
|
||||
openAlert('Error', 'Failed to update plugin status');
|
||||
setIsRefreshing(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleClearScrapers = () => {
|
||||
openAlert(
|
||||
'Clear All Scrapers',
|
||||
'Are you sure you want to remove all installed scrapers? This action cannot be undone.',
|
||||
'Clear All Plugins',
|
||||
'Are you sure you want to remove all installed plugins? This action cannot be undone.',
|
||||
[
|
||||
{ label: 'Cancel', onPress: () => {} },
|
||||
{ label: 'Cancel', onPress: () => { } },
|
||||
{
|
||||
label: 'Clear',
|
||||
onPress: async () => {
|
||||
try {
|
||||
await pluginService.clearScrapers();
|
||||
await loadScrapers();
|
||||
openAlert('Success', 'All scrapers have been removed');
|
||||
openAlert('Success', 'All plugins have been removed');
|
||||
} catch (error) {
|
||||
logger.error('[ScraperSettings] Failed to clear scrapers:', error);
|
||||
openAlert('Error', 'Failed to clear scrapers');
|
||||
logger.error('[PluginsScreen] Failed to clear plugins:', error);
|
||||
openAlert('Error', 'Failed to clear plugins');
|
||||
}
|
||||
},
|
||||
},
|
||||
|
|
@ -1243,9 +1243,9 @@ const PluginsScreen: React.FC = () => {
|
|||
const handleClearCache = () => {
|
||||
openAlert(
|
||||
'Clear Repository Cache',
|
||||
'This will remove the saved repository URL and clear all cached scraper data. You will need to re-enter your repository URL.',
|
||||
'This will remove the saved repository URL and clear all cached plugin data. You will need to re-enter your repository URL.',
|
||||
[
|
||||
{ label: 'Cancel', onPress: () => {} },
|
||||
{ label: 'Cancel', onPress: () => { } },
|
||||
{
|
||||
label: 'Clear Cache',
|
||||
onPress: async () => {
|
||||
|
|
@ -1258,7 +1258,7 @@ const PluginsScreen: React.FC = () => {
|
|||
await loadScrapers();
|
||||
openAlert('Success', 'Repository cache cleared successfully');
|
||||
} catch (error) {
|
||||
logger.error('[ScraperSettings] Failed to clear cache:', error);
|
||||
logger.error('[PluginsScreen] Failed to clear cache:', error);
|
||||
openAlert('Error', 'Failed to clear repository cache');
|
||||
}
|
||||
},
|
||||
|
|
@ -1274,19 +1274,19 @@ const PluginsScreen: React.FC = () => {
|
|||
|
||||
const handleToggleLocalScrapers = async (enabled: boolean) => {
|
||||
await updateSetting('enableLocalScrapers', enabled);
|
||||
|
||||
|
||||
// If enabling plugins, refresh repository and reload plugins
|
||||
if (enabled) {
|
||||
try {
|
||||
setIsRefreshing(true);
|
||||
logger.log('[PluginsScreen] Enabling plugins - refreshing repository...');
|
||||
|
||||
|
||||
// Refresh repository to ensure plugins are available
|
||||
await pluginService.refreshRepository();
|
||||
|
||||
|
||||
// Reload plugins to get the latest state
|
||||
await loadScrapers();
|
||||
|
||||
|
||||
logger.log('[PluginsScreen] Plugins enabled and repository refreshed');
|
||||
} catch (error) {
|
||||
logger.error('[PluginsScreen] Failed to refresh repository when enabling plugins:', error);
|
||||
|
|
@ -1304,7 +1304,7 @@ const PluginsScreen: React.FC = () => {
|
|||
const handleToggleQualityExclusion = async (quality: string) => {
|
||||
const currentExcluded = settings.excludedQualities || [];
|
||||
const isExcluded = currentExcluded.includes(quality);
|
||||
|
||||
|
||||
let newExcluded: string[];
|
||||
if (isExcluded) {
|
||||
// Remove from excluded list
|
||||
|
|
@ -1313,14 +1313,14 @@ const PluginsScreen: React.FC = () => {
|
|||
// Add to excluded list
|
||||
newExcluded = [...currentExcluded, quality];
|
||||
}
|
||||
|
||||
|
||||
await updateSetting('excludedQualities', newExcluded);
|
||||
};
|
||||
|
||||
const handleToggleLanguageExclusion = async (language: string) => {
|
||||
const currentExcluded = settings.excludedLanguages || [];
|
||||
const isExcluded = currentExcluded.includes(language);
|
||||
|
||||
|
||||
let newExcluded: string[];
|
||||
if (isExcluded) {
|
||||
// Remove from excluded list
|
||||
|
|
@ -1329,13 +1329,13 @@ const PluginsScreen: React.FC = () => {
|
|||
// Add to excluded list
|
||||
newExcluded = [...currentExcluded, language];
|
||||
}
|
||||
|
||||
|
||||
await updateSetting('excludedLanguages', newExcluded);
|
||||
};
|
||||
|
||||
// Define available quality options
|
||||
const qualityOptions = ['Auto', 'Adaptive', '2160p', '4K', '1080p', '720p', '360p', 'DV', 'HDR', 'REMUX', '480p', 'CAM', 'TS'];
|
||||
|
||||
|
||||
// Define available language options
|
||||
const languageOptions = ['Original', 'English', 'Spanish', 'Latin', 'French', 'German', 'Italian', 'Portuguese', 'Russian', 'Japanese', 'Korean', 'Chinese', 'Arabic', 'Hindi', 'Turkish', 'Dutch', 'Polish'];
|
||||
|
||||
|
|
@ -1344,7 +1344,7 @@ const PluginsScreen: React.FC = () => {
|
|||
return (
|
||||
<SafeAreaView style={styles.container}>
|
||||
<StatusBar barStyle="light-content" />
|
||||
|
||||
|
||||
{/* Header */}
|
||||
<View style={styles.header}>
|
||||
<TouchableOpacity
|
||||
|
|
@ -1354,7 +1354,7 @@ const PluginsScreen: React.FC = () => {
|
|||
<Ionicons name="arrow-back" size={24} color={colors.primary} />
|
||||
<Text style={styles.backText}>Settings</Text>
|
||||
</TouchableOpacity>
|
||||
|
||||
|
||||
<View style={styles.headerActions}>
|
||||
{/* Help Button */}
|
||||
<TouchableOpacity
|
||||
|
|
@ -1365,7 +1365,7 @@ const PluginsScreen: React.FC = () => {
|
|||
</TouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
|
||||
<Text style={styles.headerTitle}>Plugins</Text>
|
||||
|
||||
<ScrollView
|
||||
|
|
@ -1427,9 +1427,9 @@ const PluginsScreen: React.FC = () => {
|
|||
styles={styles}
|
||||
>
|
||||
<Text style={styles.sectionDescription}>
|
||||
Manage multiple scraper repositories. Switch between repositories to access different sets of scrapers.
|
||||
Manage multiple plugin repositories. Switch between repositories to access different sets of plugins.
|
||||
</Text>
|
||||
|
||||
|
||||
{/* Current Repository */}
|
||||
{currentRepositoryId && (
|
||||
<View style={styles.currentRepoContainer}>
|
||||
|
|
@ -1438,7 +1438,7 @@ const PluginsScreen: React.FC = () => {
|
|||
<Text style={[styles.currentRepoUrl, { fontSize: 12, opacity: 0.7, marginTop: 4 }]}>{repositoryUrl}</Text>
|
||||
</View>
|
||||
)}
|
||||
|
||||
|
||||
{/* Repository List */}
|
||||
{repositories.length > 0 && (
|
||||
<View style={styles.repositoriesList}>
|
||||
|
|
@ -1466,9 +1466,9 @@ const PluginsScreen: React.FC = () => {
|
|||
)}
|
||||
<Text style={styles.repositoryUrl}>{repo.url}</Text>
|
||||
<Text style={styles.repositoryMeta}>
|
||||
{repo.scraperCount || 0} scrapers • Last updated: {repo.lastUpdated ? new Date(repo.lastUpdated).toLocaleDateString() : 'Never'}
|
||||
</Text>
|
||||
</View>
|
||||
{repo.scraperCount || 0} plugins • Last updated: {repo.lastUpdated ? new Date(repo.lastUpdated).toLocaleDateString() : 'Never'}
|
||||
</Text>
|
||||
</View>
|
||||
<View style={styles.repositoryActions}>
|
||||
{repo.id !== currentRepositoryId && (
|
||||
<TouchableOpacity
|
||||
|
|
@ -1502,7 +1502,7 @@ const PluginsScreen: React.FC = () => {
|
|||
<Text style={styles.repositoryActionButtonText}>Remove</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
)}
|
||||
|
|
@ -1535,15 +1535,15 @@ const PluginsScreen: React.FC = () => {
|
|||
style={styles.searchInput}
|
||||
value={searchQuery}
|
||||
onChangeText={setSearchQuery}
|
||||
placeholder="Search scrapers..."
|
||||
placeholder="Search plugins..."
|
||||
placeholderTextColor={colors.mediumGray}
|
||||
/>
|
||||
{searchQuery.length > 0 && (
|
||||
<TouchableOpacity onPress={() => setSearchQuery('')}>
|
||||
<Ionicons name="close-circle" size={20} color={colors.mediumGray} />
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
</View>
|
||||
|
||||
{/* Filter Chips */}
|
||||
<View style={styles.filterContainer}>
|
||||
|
|
@ -1561,7 +1561,7 @@ const PluginsScreen: React.FC = () => {
|
|||
selectedFilter === filter && styles.filterChipTextSelected
|
||||
]}>
|
||||
{filter === 'all' ? 'All' : filter === 'movie' ? 'Movies' : 'TV Shows'}
|
||||
</Text>
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
))}
|
||||
</View>
|
||||
|
|
@ -1590,19 +1590,19 @@ const PluginsScreen: React.FC = () => {
|
|||
|
||||
{filteredScrapers.length === 0 ? (
|
||||
<View style={styles.emptyStateContainer}>
|
||||
<Ionicons
|
||||
name={searchQuery ? "search" : "download-outline"}
|
||||
size={48}
|
||||
<Ionicons
|
||||
name={searchQuery ? "search" : "download-outline"}
|
||||
size={48}
|
||||
color={colors.mediumGray}
|
||||
style={styles.emptyStateIcon}
|
||||
/>
|
||||
<Text style={styles.emptyStateTitle}>
|
||||
{searchQuery ? 'No Scrapers Found' : 'No Scrapers Available'}
|
||||
</Text>
|
||||
{searchQuery ? 'No Plugins Found' : 'No Plugins Available'}
|
||||
</Text>
|
||||
<Text style={styles.emptyStateDescription}>
|
||||
{searchQuery
|
||||
? `No scrapers match "${searchQuery}". Try a different search term.`
|
||||
: 'Configure a repository above to view available scrapers.'
|
||||
{searchQuery
|
||||
? `No plugins match "${searchQuery}". Try a different search term.`
|
||||
: 'Configure a repository above to view available plugins.'
|
||||
}
|
||||
</Text>
|
||||
{searchQuery && (
|
||||
|
|
@ -1613,45 +1613,45 @@ const PluginsScreen: React.FC = () => {
|
|||
<Text style={styles.secondaryButtonText}>Clear Search</Text>
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
</View>
|
||||
) : (
|
||||
<View style={styles.scrapersContainer}>
|
||||
</View>
|
||||
) : (
|
||||
<View style={styles.scrapersContainer}>
|
||||
{filteredScrapers.map((scraper) => (
|
||||
<View key={scraper.id} style={styles.scraperCard}>
|
||||
<View style={styles.scraperCardHeader}>
|
||||
{scraper.logo ? (
|
||||
(scraper.logo.toLowerCase().endsWith('.svg') || scraper.logo.toLowerCase().includes('.svg?')) ? (
|
||||
<Image
|
||||
source={{ uri: scraper.logo }}
|
||||
style={styles.scraperLogo}
|
||||
resizeMode="contain"
|
||||
/>
|
||||
) : (
|
||||
<FastImage
|
||||
source={{ uri: scraper.logo }}
|
||||
style={styles.scraperLogo}
|
||||
resizeMode={FastImage.resizeMode.contain}
|
||||
/>
|
||||
)
|
||||
) : (
|
||||
{scraper.logo ? (
|
||||
(scraper.logo.toLowerCase().endsWith('.svg') || scraper.logo.toLowerCase().includes('.svg?')) ? (
|
||||
<Image
|
||||
source={{ uri: scraper.logo }}
|
||||
style={styles.scraperLogo}
|
||||
resizeMode="contain"
|
||||
/>
|
||||
) : (
|
||||
<FastImage
|
||||
source={{ uri: scraper.logo }}
|
||||
style={styles.scraperLogo}
|
||||
resizeMode={FastImage.resizeMode.contain}
|
||||
/>
|
||||
)
|
||||
) : (
|
||||
<View style={styles.scraperLogo} />
|
||||
)}
|
||||
<View style={styles.scraperCardInfo}>
|
||||
<View style={{ flexDirection: 'row', alignItems: 'center', marginBottom: 4, gap: 8 }}>
|
||||
<Text style={styles.scraperName}>{scraper.name}</Text>
|
||||
<StatusBadge status={getScraperStatus(scraper)} colors={colors} />
|
||||
</View>
|
||||
<Text style={styles.scraperDescription}>{scraper.description}</Text>
|
||||
</View>
|
||||
<Switch
|
||||
value={scraper.enabled && settings.enableLocalScrapers}
|
||||
onValueChange={(enabled) => handleToggleScraper(scraper.id, enabled)}
|
||||
trackColor={{ false: colors.elevation3, true: colors.primary }}
|
||||
thumbColor={scraper.enabled && settings.enableLocalScrapers ? colors.white : '#f4f3f4'}
|
||||
disabled={!settings.enableLocalScrapers || scraper.manifestEnabled === false || (scraper.disabledPlatforms && scraper.disabledPlatforms.includes(Platform.OS as 'ios' | 'android'))}
|
||||
/>
|
||||
</View>
|
||||
|
||||
<Text style={styles.scraperDescription}>{scraper.description}</Text>
|
||||
</View>
|
||||
<Switch
|
||||
value={scraper.enabled && settings.enableLocalScrapers}
|
||||
onValueChange={(enabled) => handleToggleScraper(scraper.id, enabled)}
|
||||
trackColor={{ false: colors.elevation3, true: colors.primary }}
|
||||
thumbColor={scraper.enabled && settings.enableLocalScrapers ? colors.white : '#f4f3f4'}
|
||||
disabled={!settings.enableLocalScrapers || scraper.manifestEnabled === false || (scraper.disabledPlatforms && scraper.disabledPlatforms.includes(Platform.OS as 'ios' | 'android'))}
|
||||
/>
|
||||
</View>
|
||||
|
||||
<View style={styles.scraperCardMeta}>
|
||||
<View style={styles.scraperCardMetaItem}>
|
||||
<Ionicons name="information-circle" size={12} color={colors.mediumGray} />
|
||||
|
|
@ -1682,62 +1682,62 @@ const PluginsScreen: React.FC = () => {
|
|||
</View>
|
||||
|
||||
{/* ShowBox Settings - only visible when ShowBox scraper is available */}
|
||||
{showboxScraperId && scraper.id === showboxScraperId && settings.enableLocalScrapers && (
|
||||
{showboxScraperId && scraper.id === showboxScraperId && settings.enableLocalScrapers && (
|
||||
<View style={{ marginTop: 16, paddingTop: 16, borderTopWidth: 1, borderTopColor: colors.elevation3 }}>
|
||||
<Text style={[styles.settingTitle, { marginBottom: 8 }]}>ShowBox UI Token</Text>
|
||||
<View style={{ flexDirection: 'row', alignItems: 'center', marginBottom: 12 }}>
|
||||
<TextInput
|
||||
style={[styles.textInput, { flex: 1, marginBottom: 0 }]}
|
||||
value={showboxUiToken}
|
||||
onChangeText={setShowboxUiToken}
|
||||
placeholder="Paste your ShowBox UI token"
|
||||
placeholderTextColor={colors.mediumGray}
|
||||
autoCapitalize="none"
|
||||
autoCorrect={false}
|
||||
secureTextEntry={showboxSavedToken.length > 0 && !showboxTokenVisible}
|
||||
multiline={false}
|
||||
numberOfLines={1}
|
||||
/>
|
||||
{showboxSavedToken.length > 0 && (
|
||||
<TouchableOpacity onPress={() => setShowboxTokenVisible(v => !v)} accessibilityRole="button" accessibilityLabel={showboxTokenVisible ? 'Hide token' : 'Show token'} style={{ marginLeft: 10 }}>
|
||||
<Ionicons name={showboxTokenVisible ? 'eye-off' : 'eye'} size={18} color={colors.primary} />
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
</View>
|
||||
<View style={styles.buttonRow}>
|
||||
{showboxUiToken !== showboxSavedToken && (
|
||||
<TouchableOpacity
|
||||
style={[styles.button, styles.primaryButton]}
|
||||
onPress={async () => {
|
||||
if (showboxScraperId) {
|
||||
await pluginService.setScraperSettings(showboxScraperId, { uiToken: showboxUiToken });
|
||||
}
|
||||
setShowboxSavedToken(showboxUiToken);
|
||||
openAlert('Saved', 'ShowBox settings updated');
|
||||
}}
|
||||
>
|
||||
<Text style={styles.buttonText}>Save</Text>
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
<TouchableOpacity
|
||||
style={[styles.button, styles.secondaryButton]}
|
||||
onPress={async () => {
|
||||
setShowboxUiToken('');
|
||||
setShowboxSavedToken('');
|
||||
if (showboxScraperId) {
|
||||
await pluginService.setScraperSettings(showboxScraperId, {});
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Text style={styles.secondaryButtonText}>Clear</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
<Text style={[styles.settingTitle, { marginBottom: 8 }]}>ShowBox UI Token</Text>
|
||||
<View style={{ flexDirection: 'row', alignItems: 'center', marginBottom: 12 }}>
|
||||
<TextInput
|
||||
style={[styles.textInput, { flex: 1, marginBottom: 0 }]}
|
||||
value={showboxUiToken}
|
||||
onChangeText={setShowboxUiToken}
|
||||
placeholder="Paste your ShowBox UI token"
|
||||
placeholderTextColor={colors.mediumGray}
|
||||
autoCapitalize="none"
|
||||
autoCorrect={false}
|
||||
secureTextEntry={showboxSavedToken.length > 0 && !showboxTokenVisible}
|
||||
multiline={false}
|
||||
numberOfLines={1}
|
||||
/>
|
||||
{showboxSavedToken.length > 0 && (
|
||||
<TouchableOpacity onPress={() => setShowboxTokenVisible(v => !v)} accessibilityRole="button" accessibilityLabel={showboxTokenVisible ? 'Hide token' : 'Show token'} style={{ marginLeft: 10 }}>
|
||||
<Ionicons name={showboxTokenVisible ? 'eye-off' : 'eye'} size={18} color={colors.primary} />
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
</View>
|
||||
<View style={styles.buttonRow}>
|
||||
{showboxUiToken !== showboxSavedToken && (
|
||||
<TouchableOpacity
|
||||
style={[styles.button, styles.primaryButton]}
|
||||
onPress={async () => {
|
||||
if (showboxScraperId) {
|
||||
await pluginService.setScraperSettings(showboxScraperId, { uiToken: showboxUiToken });
|
||||
}
|
||||
setShowboxSavedToken(showboxUiToken);
|
||||
openAlert('Saved', 'ShowBox settings updated');
|
||||
}}
|
||||
>
|
||||
<Text style={styles.buttonText}>Save</Text>
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
<TouchableOpacity
|
||||
style={[styles.button, styles.secondaryButton]}
|
||||
onPress={async () => {
|
||||
setShowboxUiToken('');
|
||||
setShowboxSavedToken('');
|
||||
if (showboxScraperId) {
|
||||
await pluginService.setScraperSettings(showboxScraperId, {});
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Text style={styles.secondaryButtonText}>Clear</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
)}
|
||||
</CollapsibleSection>
|
||||
|
||||
{/* Additional Settings */}
|
||||
|
|
@ -1763,7 +1763,7 @@ const PluginsScreen: React.FC = () => {
|
|||
disabled={!settings.enableLocalScrapers}
|
||||
/>
|
||||
</View>
|
||||
|
||||
|
||||
<View style={styles.settingRow}>
|
||||
<View style={styles.settingInfo}>
|
||||
<Text style={styles.settingTitle}>Group Plugin Streams</Text>
|
||||
|
|
@ -1772,50 +1772,50 @@ const PluginsScreen: React.FC = () => {
|
|||
</Text>
|
||||
</View>
|
||||
<Switch
|
||||
value={settings.streamDisplayMode === 'grouped'}
|
||||
onValueChange={(value) => {
|
||||
updateSetting('streamDisplayMode', value ? 'grouped' : 'separate');
|
||||
// Auto-disable quality sorting when grouping is disabled
|
||||
if (!value && settings.streamSortMode === 'quality-then-scraper') {
|
||||
updateSetting('streamSortMode', 'scraper-then-quality');
|
||||
}
|
||||
}}
|
||||
trackColor={{ false: colors.elevation3, true: colors.primary }}
|
||||
thumbColor={settings.streamDisplayMode === 'grouped' ? colors.white : '#f4f3f4'}
|
||||
disabled={!settings.enableLocalScrapers}
|
||||
/>
|
||||
value={settings.streamDisplayMode === 'grouped'}
|
||||
onValueChange={(value) => {
|
||||
updateSetting('streamDisplayMode', value ? 'grouped' : 'separate');
|
||||
// Auto-disable quality sorting when grouping is disabled
|
||||
if (!value && settings.streamSortMode === 'quality-then-scraper') {
|
||||
updateSetting('streamSortMode', 'scraper-then-quality');
|
||||
}
|
||||
}}
|
||||
trackColor={{ false: colors.elevation3, true: colors.primary }}
|
||||
thumbColor={settings.streamDisplayMode === 'grouped' ? colors.white : '#f4f3f4'}
|
||||
disabled={!settings.enableLocalScrapers}
|
||||
/>
|
||||
</View>
|
||||
|
||||
|
||||
<View style={styles.settingRow}>
|
||||
<View style={styles.settingInfo}>
|
||||
<Text style={styles.settingTitle}>Sort by Quality First</Text>
|
||||
<Text style={styles.settingDescription}>
|
||||
When enabled, streams are sorted by quality first, then by scraper. When disabled, streams are sorted by scraper first, then by quality. Only available when grouping is enabled.
|
||||
When enabled, streams are sorted by quality first, then by plugin. When disabled, streams are sorted by plugin first, then by quality. Only available when grouping is enabled.
|
||||
</Text>
|
||||
</View>
|
||||
<Switch
|
||||
value={settings.streamSortMode === 'quality-then-scraper'}
|
||||
onValueChange={(value) => updateSetting('streamSortMode', value ? 'quality-then-scraper' : 'scraper-then-quality')}
|
||||
trackColor={{ false: colors.elevation3, true: colors.primary }}
|
||||
thumbColor={settings.streamSortMode === 'quality-then-scraper' ? colors.white : '#f4f3f4'}
|
||||
disabled={!settings.enableLocalScrapers || settings.streamDisplayMode !== 'grouped'}
|
||||
/>
|
||||
value={settings.streamSortMode === 'quality-then-scraper'}
|
||||
onValueChange={(value) => updateSetting('streamSortMode', value ? 'quality-then-scraper' : 'scraper-then-quality')}
|
||||
trackColor={{ false: colors.elevation3, true: colors.primary }}
|
||||
thumbColor={settings.streamSortMode === 'quality-then-scraper' ? colors.white : '#f4f3f4'}
|
||||
disabled={!settings.enableLocalScrapers || settings.streamDisplayMode !== 'grouped'}
|
||||
/>
|
||||
</View>
|
||||
|
||||
|
||||
<View style={styles.settingRow}>
|
||||
<View style={styles.settingInfo}>
|
||||
<Text style={styles.settingTitle}>Show Scraper Logos</Text>
|
||||
<Text style={styles.settingTitle}>Show Plugin Logos</Text>
|
||||
<Text style={styles.settingDescription}>
|
||||
Display scraper logos next to streaming links on the streams screen.
|
||||
Display plugin logos next to streaming links on the streams screen.
|
||||
</Text>
|
||||
</View>
|
||||
<Switch
|
||||
value={settings.showScraperLogos && settings.enableLocalScrapers}
|
||||
onValueChange={(value) => updateSetting('showScraperLogos', value)}
|
||||
trackColor={{ false: colors.elevation3, true: colors.primary }}
|
||||
thumbColor={settings.showScraperLogos && settings.enableLocalScrapers ? colors.white : '#f4f3f4'}
|
||||
disabled={!settings.enableLocalScrapers}
|
||||
/>
|
||||
value={settings.showScraperLogos && settings.enableLocalScrapers}
|
||||
onValueChange={(value) => updateSetting('showScraperLogos', value)}
|
||||
trackColor={{ false: colors.elevation3, true: colors.primary }}
|
||||
thumbColor={settings.showScraperLogos && settings.enableLocalScrapers ? colors.white : '#f4f3f4'}
|
||||
disabled={!settings.enableLocalScrapers}
|
||||
/>
|
||||
</View>
|
||||
</CollapsibleSection>
|
||||
|
||||
|
|
@ -1830,7 +1830,7 @@ const PluginsScreen: React.FC = () => {
|
|||
<Text style={styles.sectionDescription}>
|
||||
Exclude specific video qualities from search results. Tap on a quality to exclude it from plugin results.
|
||||
</Text>
|
||||
|
||||
|
||||
<View style={styles.qualityChipsContainer}>
|
||||
{qualityOptions.map((quality) => {
|
||||
const isExcluded = (settings.excludedQualities || []).includes(quality);
|
||||
|
|
@ -1856,7 +1856,7 @@ const PluginsScreen: React.FC = () => {
|
|||
);
|
||||
})}
|
||||
</View>
|
||||
|
||||
|
||||
{(settings.excludedQualities || []).length > 0 && (
|
||||
<Text style={[styles.infoText, { marginTop: 12 }, !settings.enableLocalScrapers && styles.disabledText]}>
|
||||
Excluded qualities: {(settings.excludedQualities || []).join(', ')}
|
||||
|
|
@ -1875,11 +1875,11 @@ const PluginsScreen: React.FC = () => {
|
|||
<Text style={styles.sectionDescription}>
|
||||
Exclude specific languages from search results. Tap on a language to exclude it from plugin results.
|
||||
</Text>
|
||||
|
||||
|
||||
<Text style={[styles.infoText, { marginTop: 8, fontSize: 13, color: colors.mediumEmphasis }]}>
|
||||
<Text style={{ fontWeight: '600' }}>Note:</Text> This filter only applies to providers that include language information in their stream names. It does not affect other providers.
|
||||
</Text>
|
||||
|
||||
|
||||
<View style={styles.qualityChipsContainer}>
|
||||
{languageOptions.map((language) => {
|
||||
const isExcluded = (settings.excludedLanguages || []).includes(language);
|
||||
|
|
@ -1905,7 +1905,7 @@ const PluginsScreen: React.FC = () => {
|
|||
);
|
||||
})}
|
||||
</View>
|
||||
|
||||
|
||||
{(settings.excludedLanguages || []).length > 0 && (
|
||||
<Text style={[styles.infoText, { marginTop: 12 }, !settings.enableLocalScrapers && styles.disabledText]}>
|
||||
Excluded languages: {(settings.excludedLanguages || []).join(', ')}
|
||||
|
|
@ -1944,10 +1944,10 @@ const PluginsScreen: React.FC = () => {
|
|||
2. <Text style={{ fontWeight: '600' }}>Add Repository</Text> - Add a GitHub raw URL or use the default repository
|
||||
</Text>
|
||||
<Text style={styles.modalText}>
|
||||
3. <Text style={{ fontWeight: '600' }}>Refresh Repository</Text> - Download available scrapers from the repository
|
||||
3. <Text style={{ fontWeight: '600' }}>Refresh Repository</Text> - Download available plugins from the repository
|
||||
</Text>
|
||||
<Text style={styles.modalText}>
|
||||
4. <Text style={{ fontWeight: '600' }}>Enable Scrapers</Text> - Turn on the scrapers you want to use for streaming
|
||||
4. <Text style={{ fontWeight: '600' }}>Enable Plugins</Text> - Turn on the plugins you want to use for streaming
|
||||
</Text>
|
||||
<TouchableOpacity
|
||||
style={styles.modalButton}
|
||||
|
|
@ -1988,36 +1988,36 @@ const PluginsScreen: React.FC = () => {
|
|||
/>
|
||||
|
||||
|
||||
{/* Format Hint */}
|
||||
<Text style={styles.formatHint}>
|
||||
Format: https://raw.githubusercontent.com/username/repo/refs/heads/branch
|
||||
</Text>
|
||||
{/* Format Hint */}
|
||||
<Text style={styles.formatHint}>
|
||||
Format: https://raw.githubusercontent.com/username/repo/refs/heads/branch
|
||||
</Text>
|
||||
|
||||
{/* Action Buttons */}
|
||||
<View style={styles.compactActions}>
|
||||
<TouchableOpacity
|
||||
style={[styles.compactButton, styles.cancelButton]}
|
||||
onPress={() => {
|
||||
setShowAddRepositoryModal(false);
|
||||
setNewRepositoryUrl('');
|
||||
}}
|
||||
>
|
||||
<Text style={styles.cancelButtonText}>Cancel</Text>
|
||||
</TouchableOpacity>
|
||||
{/* Action Buttons */}
|
||||
<View style={styles.compactActions}>
|
||||
<TouchableOpacity
|
||||
style={[styles.compactButton, styles.cancelButton]}
|
||||
onPress={() => {
|
||||
setShowAddRepositoryModal(false);
|
||||
setNewRepositoryUrl('');
|
||||
}}
|
||||
>
|
||||
<Text style={styles.cancelButtonText}>Cancel</Text>
|
||||
</TouchableOpacity>
|
||||
|
||||
<TouchableOpacity
|
||||
style={[styles.compactButton, styles.addButton, (!newRepositoryUrl.trim() || isLoading) && styles.disabledButton]}
|
||||
onPress={handleAddRepository}
|
||||
disabled={!newRepositoryUrl.trim() || isLoading}
|
||||
>
|
||||
{isLoading ? (
|
||||
<ActivityIndicator size="small" color={colors.white} />
|
||||
) : (
|
||||
<Text style={styles.addButtonText}>Add</Text>
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</ScrollView>
|
||||
<TouchableOpacity
|
||||
style={[styles.compactButton, styles.addButton, (!newRepositoryUrl.trim() || isLoading) && styles.disabledButton]}
|
||||
onPress={handleAddRepository}
|
||||
disabled={!newRepositoryUrl.trim() || isLoading}
|
||||
>
|
||||
{isLoading ? (
|
||||
<ActivityIndicator size="small" color={colors.white} />
|
||||
) : (
|
||||
<Text style={styles.addButtonText}>Add</Text>
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</ScrollView>
|
||||
</View>
|
||||
</View>
|
||||
</Modal>
|
||||
|
|
|
|||
|
|
@ -561,7 +561,7 @@ const SettingsScreen: React.FC = () => {
|
|||
description="Manage plugins and repositories"
|
||||
customIcon={<PluginIcon size={isTablet ? 24 : 20} color={currentTheme.colors.primary} />}
|
||||
renderControl={ChevronRight}
|
||||
onPress={() => navigation.navigate('ScraperSettings')}
|
||||
onPress={() => navigation.navigate('PluginSettings')}
|
||||
isTablet={isTablet}
|
||||
/>
|
||||
<SettingItem
|
||||
|
|
|
|||
|
|
@ -913,13 +913,13 @@ export const StreamsScreen = () => {
|
|||
const handleStreamPress = useCallback(async (stream: Stream) => {
|
||||
try {
|
||||
if (stream.url) {
|
||||
// Block magnet links - not supported yet
|
||||
|
||||
// Block magnet links with sanitized message
|
||||
if (typeof stream.url === 'string' && stream.url.startsWith('magnet:')) {
|
||||
try {
|
||||
openAlert('Not supported', 'Torrent streaming is not supported yet.');
|
||||
} catch (_e) { }
|
||||
openAlert('Stream Not Supported', 'This stream format is not supported.');
|
||||
return;
|
||||
}
|
||||
|
||||
// If stream is actually MKV format, force the in-app VLC-based player on iOS
|
||||
try {
|
||||
if (Platform.OS === 'ios' && settings.preferredPlayer === 'internal') {
|
||||
|
|
@ -1078,7 +1078,7 @@ export const StreamsScreen = () => {
|
|||
const isMagnet = typeof stream.url === 'string' && stream.url.startsWith('magnet:');
|
||||
|
||||
if (isMagnet) {
|
||||
// For magnet links, open directly which will trigger the torrent app chooser
|
||||
// For magnet links, open directly
|
||||
if (__DEV__) console.log('Opening magnet link directly');
|
||||
Linking.openURL(stream.url)
|
||||
.then(() => { if (__DEV__) console.log('Successfully opened magnet link'); })
|
||||
|
|
|
|||
|
|
@ -113,7 +113,7 @@ class LocalScraperService {
|
|||
id: 'default',
|
||||
name: this.extractRepositoryName(storedRepoUrl),
|
||||
url: storedRepoUrl,
|
||||
description: 'Default repository',
|
||||
description: 'Default Plugins Repository',
|
||||
isDefault: true,
|
||||
enabled: true,
|
||||
lastUpdated: Date.now()
|
||||
|
|
@ -146,21 +146,21 @@ class LocalScraperService {
|
|||
if (storedScrapers) {
|
||||
const scrapers: ScraperInfo[] = JSON.parse(storedScrapers);
|
||||
const validScrapers: ScraperInfo[] = [];
|
||||
|
||||
|
||||
scrapers.forEach(scraper => {
|
||||
// Skip scrapers with missing essential fields
|
||||
if (!scraper.id || !scraper.name || !scraper.version) {
|
||||
logger.warn('[LocalScraperService] Skipping invalid scraper with missing essential fields:', scraper);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
// Ensure contentLanguage is an array (migration for older scrapers)
|
||||
if (!scraper.contentLanguage) {
|
||||
scraper.contentLanguage = ['en']; // Default to English
|
||||
} else if (typeof scraper.contentLanguage === 'string') {
|
||||
scraper.contentLanguage = [scraper.contentLanguage]; // Convert string to array
|
||||
}
|
||||
|
||||
|
||||
// Ensure supportedTypes is an array (migration for older scrapers)
|
||||
if (!scraper.supportedTypes || !Array.isArray(scraper.supportedTypes)) {
|
||||
scraper.supportedTypes = ['movie', 'tv']; // Default to both types
|
||||
|
|
@ -175,7 +175,7 @@ class LocalScraperService {
|
|||
if (!scraper.supportedFormats && scraper.formats) {
|
||||
scraper.supportedFormats = scraper.formats;
|
||||
}
|
||||
|
||||
|
||||
// Ensure other required fields have defaults
|
||||
if (!scraper.description) {
|
||||
scraper.description = 'No description available';
|
||||
|
|
@ -186,16 +186,16 @@ class LocalScraperService {
|
|||
if (scraper.enabled === undefined) {
|
||||
scraper.enabled = true;
|
||||
}
|
||||
|
||||
|
||||
this.installedScrapers.set(scraper.id, scraper);
|
||||
validScrapers.push(scraper);
|
||||
});
|
||||
|
||||
|
||||
// Save cleaned scrapers back to storage if any were filtered out
|
||||
if (validScrapers.length !== scrapers.length) {
|
||||
logger.log('[LocalScraperService] Cleaned up invalid scrapers, saving valid ones');
|
||||
await mmkvStorage.setItem(this.STORAGE_KEY, JSON.stringify(validScrapers));
|
||||
|
||||
|
||||
// Clean up cached code for removed scrapers
|
||||
const validScraperIds = new Set(validScrapers.map(s => s.id));
|
||||
const removedScrapers = scrapers.filter(s => s.id && !validScraperIds.has(s.id));
|
||||
|
|
@ -212,7 +212,7 @@ class LocalScraperService {
|
|||
|
||||
// Load scraper code from cache
|
||||
await this.loadScraperCode();
|
||||
|
||||
|
||||
// Auto-refresh repository on app startup if URL is configured (only once)
|
||||
if (this.repositoryUrl && !this.autoRefreshCompleted) {
|
||||
try {
|
||||
|
|
@ -225,7 +225,7 @@ class LocalScraperService {
|
|||
this.autoRefreshCompleted = true; // Mark as completed even on error to prevent retries
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
this.initialized = true;
|
||||
logger.log('[LocalScraperService] Initialized with', this.installedScrapers.size, 'scrapers');
|
||||
} catch (error) {
|
||||
|
|
@ -268,7 +268,7 @@ class LocalScraperService {
|
|||
async addRepository(repo: Omit<RepositoryInfo, 'id' | 'lastUpdated' | 'scraperCount'>): Promise<string> {
|
||||
await this.ensureInitialized();
|
||||
const id = `repo_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
||||
|
||||
|
||||
// Try to fetch the repository name from manifest if not provided
|
||||
let repositoryName = repo.name;
|
||||
if (!repositoryName || repositoryName.trim() === '') {
|
||||
|
|
@ -279,7 +279,7 @@ class LocalScraperService {
|
|||
repositoryName = this.extractRepositoryName(repo.url);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
const newRepo: RepositoryInfo = {
|
||||
...repo,
|
||||
name: repositoryName,
|
||||
|
|
@ -302,7 +302,7 @@ class LocalScraperService {
|
|||
const updatedRepo = { ...repo, ...updates };
|
||||
this.repositories.set(id, updatedRepo);
|
||||
await this.saveRepositories();
|
||||
|
||||
|
||||
// If this is the current repository, update current values
|
||||
if (id === this.currentRepositoryId) {
|
||||
this.repositoryUrl = updatedRepo.url;
|
||||
|
|
@ -316,10 +316,10 @@ class LocalScraperService {
|
|||
if (!this.repositories.has(id)) {
|
||||
throw new Error(`Repository with id ${id} not found`);
|
||||
}
|
||||
|
||||
|
||||
// Allow removing the last repository - users can add new ones
|
||||
// The app will work without repositories (no scrapers available)
|
||||
|
||||
|
||||
// If removing current repository, switch to another one or clear current
|
||||
if (id === this.currentRepositoryId) {
|
||||
const remainingRepos = Array.from(this.repositories.values()).filter(r => r.id !== id);
|
||||
|
|
@ -331,18 +331,18 @@ class LocalScraperService {
|
|||
await mmkvStorage.removeItem('current-repository-id');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Remove scrapers from this repository
|
||||
const scrapersToRemove = Array.from(this.installedScrapers.values())
|
||||
.filter(s => s.repositoryId === id)
|
||||
.map(s => s.id);
|
||||
|
||||
|
||||
for (const scraperId of scrapersToRemove) {
|
||||
this.installedScrapers.delete(scraperId);
|
||||
this.scraperCode.delete(scraperId);
|
||||
await mmkvStorage.removeItem(`scraper-code-${scraperId}`);
|
||||
}
|
||||
|
||||
|
||||
this.repositories.delete(id);
|
||||
await this.saveRepositories();
|
||||
await this.saveInstalledScrapers();
|
||||
|
|
@ -355,13 +355,13 @@ class LocalScraperService {
|
|||
if (!repo) {
|
||||
throw new Error(`Repository with id ${id} not found`);
|
||||
}
|
||||
|
||||
|
||||
this.currentRepositoryId = id;
|
||||
this.repositoryUrl = repo.url;
|
||||
this.repositoryName = repo.name;
|
||||
|
||||
|
||||
await mmkvStorage.setItem('current-repository-id', id);
|
||||
|
||||
|
||||
// Refresh the repository to get its scrapers
|
||||
try {
|
||||
logger.log('[LocalScraperService] Refreshing repository after switch:', repo.name);
|
||||
|
|
@ -370,7 +370,7 @@ class LocalScraperService {
|
|||
logger.error('[LocalScraperService] Failed to refresh repository after switch:', error);
|
||||
// Don't throw error, just log it - the switch should still succeed
|
||||
}
|
||||
|
||||
|
||||
logger.log('[LocalScraperService] Switched to repository:', repo.name);
|
||||
}
|
||||
|
||||
|
|
@ -396,21 +396,21 @@ class LocalScraperService {
|
|||
async fetchRepositoryNameFromManifest(repositoryUrl: string): Promise<string> {
|
||||
try {
|
||||
logger.log('[LocalScraperService] Fetching repository name from manifest:', repositoryUrl);
|
||||
|
||||
|
||||
// Construct manifest URL
|
||||
const baseManifestUrl = repositoryUrl.endsWith('/')
|
||||
const baseManifestUrl = repositoryUrl.endsWith('/')
|
||||
? `${repositoryUrl}manifest.json`
|
||||
: `${repositoryUrl}/manifest.json`;
|
||||
const manifestUrl = `${baseManifestUrl}?t=${Date.now()}`;
|
||||
|
||||
const response = await axios.get(manifestUrl, {
|
||||
|
||||
const response = await axios.get(manifestUrl, {
|
||||
timeout: 10000,
|
||||
headers: {
|
||||
'Cache-Control': 'no-cache',
|
||||
'Pragma': 'no-cache'
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
if (response.data && response.data.name) {
|
||||
logger.log('[LocalScraperService] Found repository name in manifest:', response.data.name);
|
||||
return response.data.name;
|
||||
|
|
@ -427,14 +427,14 @@ class LocalScraperService {
|
|||
// Update repository name from manifest for existing repositories
|
||||
async refreshRepositoryNamesFromManifests(): Promise<void> {
|
||||
await this.ensureInitialized();
|
||||
|
||||
|
||||
for (const [id, repo] of this.repositories) {
|
||||
try {
|
||||
const manifestName = await this.fetchRepositoryNameFromManifest(repo.url);
|
||||
if (manifestName !== repo.name) {
|
||||
logger.log('[LocalScraperService] Updating repository name:', repo.name, '->', manifestName);
|
||||
repo.name = manifestName;
|
||||
|
||||
|
||||
// If this is the current repository, update the current name
|
||||
if (id === this.currentRepositoryId) {
|
||||
this.repositoryName = manifestName;
|
||||
|
|
@ -444,7 +444,7 @@ class LocalScraperService {
|
|||
logger.warn('[LocalScraperService] Failed to refresh name for repository:', repo.name, error);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
await this.saveRepositories();
|
||||
}
|
||||
|
||||
|
|
@ -457,19 +457,19 @@ class LocalScraperService {
|
|||
// Check if a scraper is compatible with the current platform
|
||||
private isPlatformCompatible(scraper: ScraperInfo): boolean {
|
||||
const currentPlatform = Platform.OS as 'ios' | 'android';
|
||||
|
||||
|
||||
// If disabledPlatforms is specified and includes current platform, scraper is not compatible
|
||||
if (scraper.disabledPlatforms && scraper.disabledPlatforms.includes(currentPlatform)) {
|
||||
logger.log(`[LocalScraperService] Scraper ${scraper.name} is disabled on ${currentPlatform}`);
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
// If supportedPlatforms is specified and doesn't include current platform, scraper is not compatible
|
||||
if (scraper.supportedPlatforms && !scraper.supportedPlatforms.includes(currentPlatform)) {
|
||||
logger.log(`[LocalScraperService] Scraper ${scraper.name} is not supported on ${currentPlatform}`);
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
// If neither supportedPlatforms nor disabledPlatforms is specified, or current platform is supported
|
||||
return true;
|
||||
}
|
||||
|
|
@ -516,33 +516,33 @@ class LocalScraperService {
|
|||
const manifestUrl = `${baseManifestUrl}?t=${Date.now()}&v=${Math.random()}`;
|
||||
|
||||
const response = await axios.get(manifestUrl, {
|
||||
timeout: 10000,
|
||||
headers: {
|
||||
'Cache-Control': 'no-cache',
|
||||
'Pragma': 'no-cache',
|
||||
'Expires': '0'
|
||||
}
|
||||
});
|
||||
const manifest: ScraperManifest = response.data;
|
||||
|
||||
// Store repository name from manifest
|
||||
if (manifest.name) {
|
||||
this.repositoryName = manifest.name;
|
||||
}
|
||||
|
||||
logger.log('[LocalScraperService] getAvailableScrapers - Raw manifest data:', JSON.stringify(manifest, null, 2));
|
||||
logger.log('[LocalScraperService] getAvailableScrapers - Manifest scrapers count:', manifest.scrapers?.length || 0);
|
||||
|
||||
// Log each scraper's enabled status from manifest
|
||||
manifest.scrapers?.forEach(scraper => {
|
||||
logger.log(`[LocalScraperService] getAvailableScrapers - Scraper ${scraper.name}: enabled=${scraper.enabled}`);
|
||||
});
|
||||
|
||||
timeout: 10000,
|
||||
headers: {
|
||||
'Cache-Control': 'no-cache',
|
||||
'Pragma': 'no-cache',
|
||||
'Expires': '0'
|
||||
}
|
||||
});
|
||||
const manifest: ScraperManifest = response.data;
|
||||
|
||||
// Store repository name from manifest
|
||||
if (manifest.name) {
|
||||
this.repositoryName = manifest.name;
|
||||
}
|
||||
|
||||
logger.log('[LocalScraperService] getAvailableScrapers - Raw manifest data:', JSON.stringify(manifest, null, 2));
|
||||
logger.log('[LocalScraperService] getAvailableScrapers - Manifest scrapers count:', manifest.scrapers?.length || 0);
|
||||
|
||||
// Log each scraper's enabled status from manifest
|
||||
manifest.scrapers?.forEach(scraper => {
|
||||
logger.log(`[LocalScraperService] getAvailableScrapers - Scraper ${scraper.name}: enabled=${scraper.enabled}`);
|
||||
});
|
||||
|
||||
logger.log('[LocalScraperService] Found', manifest.scrapers.length, 'scrapers in repository');
|
||||
|
||||
|
||||
// Get current manifest scraper IDs
|
||||
const manifestScraperIds = new Set(manifest.scrapers.map(s => s.id));
|
||||
|
||||
|
||||
// Remove scrapers that are no longer in the manifest
|
||||
const currentScraperIds = Array.from(this.installedScrapers.keys());
|
||||
for (const scraperId of currentScraperIds) {
|
||||
|
|
@ -554,11 +554,11 @@ class LocalScraperService {
|
|||
await mmkvStorage.removeItem(`scraper-code-${scraperId}`);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Download and install each scraper from manifest
|
||||
for (const scraperInfo of manifest.scrapers) {
|
||||
const isPlatformCompatible = this.isPlatformCompatible(scraperInfo);
|
||||
|
||||
|
||||
if (isPlatformCompatible) {
|
||||
// Add repository ID to scraper info
|
||||
const scraperWithRepo = { ...scraperInfo, repositoryId: this.currentRepositoryId };
|
||||
|
|
@ -575,9 +575,9 @@ class LocalScraperService {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
await this.saveInstalledScrapers();
|
||||
|
||||
|
||||
// Update repository info
|
||||
const currentRepo = this.repositories.get(this.currentRepositoryId);
|
||||
if (currentRepo) {
|
||||
|
|
@ -588,9 +588,9 @@ class LocalScraperService {
|
|||
scraperCount
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
logger.log('[LocalScraperService] Repository refresh completed');
|
||||
|
||||
|
||||
} catch (error) {
|
||||
logger.error('[LocalScraperService] Failed to refresh repository:', error);
|
||||
throw error;
|
||||
|
|
@ -602,12 +602,12 @@ class LocalScraperService {
|
|||
// Download individual scraper
|
||||
private async downloadScraper(scraperInfo: ScraperInfo): Promise<void> {
|
||||
try {
|
||||
const scraperUrl = this.repositoryUrl.endsWith('/')
|
||||
const scraperUrl = this.repositoryUrl.endsWith('/')
|
||||
? `${this.repositoryUrl}${scraperInfo.filename}`
|
||||
: `${this.repositoryUrl}/${scraperInfo.filename}`;
|
||||
|
||||
|
||||
logger.log('[LocalScraperService] Downloading scraper:', scraperInfo.name);
|
||||
|
||||
|
||||
// Add cache-busting parameters to force fresh download
|
||||
const scraperUrlWithCacheBust = `${scraperUrl}?t=${Date.now()}&v=${Math.random()}`;
|
||||
|
||||
|
|
@ -620,11 +620,11 @@ class LocalScraperService {
|
|||
}
|
||||
});
|
||||
const scraperCode = response.data;
|
||||
|
||||
|
||||
// Store scraper info and code
|
||||
const existingScraper = this.installedScrapers.get(scraperInfo.id);
|
||||
const isPlatformCompatible = this.isPlatformCompatible(scraperInfo);
|
||||
|
||||
|
||||
const updatedScraperInfo = {
|
||||
...scraperInfo,
|
||||
// Store the manifest's enabled state separately
|
||||
|
|
@ -635,14 +635,14 @@ class LocalScraperService {
|
|||
// Otherwise, preserve user's enabled state or default to true for new installations
|
||||
enabled: scraperInfo.enabled && isPlatformCompatible ? (existingScraper?.enabled ?? true) : false
|
||||
};
|
||||
|
||||
|
||||
// Ensure contentLanguage is an array (migration for older scrapers)
|
||||
if (!updatedScraperInfo.contentLanguage) {
|
||||
updatedScraperInfo.contentLanguage = ['en']; // Default to English
|
||||
} else if (typeof updatedScraperInfo.contentLanguage === 'string') {
|
||||
updatedScraperInfo.contentLanguage = [updatedScraperInfo.contentLanguage]; // Convert string to array
|
||||
}
|
||||
|
||||
|
||||
// Ensure supportedTypes is an array (migration for older scrapers)
|
||||
if (!updatedScraperInfo.supportedTypes || !Array.isArray(updatedScraperInfo.supportedTypes)) {
|
||||
updatedScraperInfo.supportedTypes = ['movie', 'tv']; // Default to both types
|
||||
|
|
@ -657,16 +657,16 @@ class LocalScraperService {
|
|||
if (!updatedScraperInfo.supportedFormats && updatedScraperInfo.formats) {
|
||||
updatedScraperInfo.supportedFormats = updatedScraperInfo.formats;
|
||||
}
|
||||
|
||||
|
||||
this.installedScrapers.set(scraperInfo.id, updatedScraperInfo);
|
||||
|
||||
|
||||
this.scraperCode.set(scraperInfo.id, scraperCode);
|
||||
|
||||
|
||||
// Cache the scraper code
|
||||
await this.cacheScraperCode(scraperInfo.id, scraperCode);
|
||||
|
||||
|
||||
logger.log('[LocalScraperService] Successfully downloaded:', scraperInfo.name);
|
||||
|
||||
|
||||
} catch (error) {
|
||||
logger.error('[LocalScraperService] Failed to download scraper', scraperInfo.name, ':', error);
|
||||
}
|
||||
|
|
@ -752,7 +752,7 @@ class LocalScraperService {
|
|||
|
||||
try {
|
||||
logger.log('[LocalScraperService] Fetching available scrapers from manifest');
|
||||
|
||||
|
||||
// Fetch manifest with cache busting
|
||||
const baseManifestUrl = this.repositoryUrl.endsWith('/')
|
||||
? `${this.repositoryUrl}manifest.json`
|
||||
|
|
@ -760,26 +760,26 @@ class LocalScraperService {
|
|||
const manifestUrl = `${baseManifestUrl}?t=${Date.now()}&v=${Math.random()}`;
|
||||
|
||||
const response = await axios.get(manifestUrl, {
|
||||
timeout: 10000,
|
||||
headers: {
|
||||
'Cache-Control': 'no-cache',
|
||||
'Pragma': 'no-cache',
|
||||
'Expires': '0'
|
||||
}
|
||||
});
|
||||
timeout: 10000,
|
||||
headers: {
|
||||
'Cache-Control': 'no-cache',
|
||||
'Pragma': 'no-cache',
|
||||
'Expires': '0'
|
||||
}
|
||||
});
|
||||
const manifest: ScraperManifest = response.data;
|
||||
|
||||
|
||||
// Store repository name from manifest
|
||||
if (manifest.name) {
|
||||
this.repositoryName = manifest.name;
|
||||
}
|
||||
|
||||
|
||||
// Return scrapers from manifest, respecting manifest's enabled field and platform compatibility
|
||||
const availableScrapers = manifest.scrapers
|
||||
.filter(scraperInfo => this.isPlatformCompatible(scraperInfo))
|
||||
.map(scraperInfo => {
|
||||
const installedScraper = this.installedScrapers.get(scraperInfo.id);
|
||||
|
||||
|
||||
// Create a copy with manifest data
|
||||
const scraperWithManifestData = {
|
||||
...scraperInfo,
|
||||
|
|
@ -802,19 +802,19 @@ class LocalScraperService {
|
|||
if (!anyScraper.supportedFormats && anyScraper.formats) {
|
||||
anyScraper.supportedFormats = anyScraper.formats;
|
||||
}
|
||||
|
||||
|
||||
return scraperWithManifestData;
|
||||
});
|
||||
|
||||
|
||||
logger.log('[LocalScraperService] Found', availableScrapers.length, 'available scrapers in repository');
|
||||
|
||||
// Log final scraper states being returned to UI
|
||||
availableScrapers.forEach(scraper => {
|
||||
logger.log(`[LocalScraperService] Final scraper ${scraper.name}: manifestEnabled=${scraper.manifestEnabled}, enabled=${scraper.enabled}`);
|
||||
});
|
||||
|
||||
return availableScrapers;
|
||||
|
||||
|
||||
// Log final scraper states being returned to UI
|
||||
availableScrapers.forEach(scraper => {
|
||||
logger.log(`[LocalScraperService] Final scraper ${scraper.name}: manifestEnabled=${scraper.manifestEnabled}, enabled=${scraper.enabled}`);
|
||||
});
|
||||
|
||||
return availableScrapers;
|
||||
|
||||
} catch (error) {
|
||||
logger.error('[LocalScraperService] Failed to fetch available scrapers from manifest:', error);
|
||||
// Fallback to installed scrapers if manifest fetch fails
|
||||
|
|
@ -844,7 +844,7 @@ class LocalScraperService {
|
|||
// Enable/disable scraper
|
||||
async setScraperEnabled(scraperId: string, enabled: boolean): Promise<void> {
|
||||
await this.ensureInitialized();
|
||||
|
||||
|
||||
const scraper = this.installedScrapers.get(scraperId);
|
||||
if (scraper) {
|
||||
// Prevent enabling if manifest has disabled it or if platform-incompatible
|
||||
|
|
@ -852,7 +852,7 @@ class LocalScraperService {
|
|||
logger.log('[LocalScraperService] Cannot enable scraper', scraperId, '- disabled in manifest or platform-incompatible');
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
scraper.enabled = enabled;
|
||||
this.installedScrapers.set(scraperId, scraper);
|
||||
await this.saveInstalledScrapers();
|
||||
|
|
@ -920,7 +920,7 @@ class LocalScraperService {
|
|||
if (enabledScrapers.length > 0) {
|
||||
try {
|
||||
logger.log('[LocalScraperService] Enabled scrapers:', enabledScrapers.map(s => s.name).join(', '));
|
||||
} catch {}
|
||||
} catch { }
|
||||
}
|
||||
|
||||
if (enabledScrapers.length === 0) {
|
||||
|
|
@ -983,7 +983,7 @@ class LocalScraperService {
|
|||
promise.finally(() => {
|
||||
const current = this.inFlightByKey.get(flightKey);
|
||||
if (current === promise) this.inFlightByKey.delete(flightKey);
|
||||
}).catch(() => {});
|
||||
}).catch(() => { });
|
||||
}
|
||||
|
||||
const results = await promise;
|
||||
|
|
@ -1013,16 +1013,16 @@ class LocalScraperService {
|
|||
const settingsData = await mmkvStorage.getItem('app_settings');
|
||||
const settings = settingsData ? JSON.parse(settingsData) : {};
|
||||
const urlValidationEnabled = settings.enableScraperUrlValidation ?? true;
|
||||
|
||||
|
||||
// Load per-scraper settings for this run
|
||||
const allScraperSettingsRaw = await mmkvStorage.getItem(this.SCRAPER_SETTINGS_KEY);
|
||||
const allScraperSettings = allScraperSettingsRaw ? JSON.parse(allScraperSettingsRaw) : {};
|
||||
const perScraperSettings = (params && params.scraperId && allScraperSettings[params.scraperId]) ? allScraperSettings[params.scraperId] : (params?.settings || {});
|
||||
|
||||
|
||||
// Create a limited global context
|
||||
const moduleExports = {};
|
||||
const moduleObj = { exports: moduleExports };
|
||||
|
||||
|
||||
// Try to load cheerio-without-node-native
|
||||
let cheerio = null;
|
||||
try {
|
||||
|
|
@ -1034,14 +1034,14 @@ class LocalScraperService {
|
|||
// Cheerio not available, scrapers will fall back to regex
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// MovieBox constants - read from Expo public envs so they bundle in builds
|
||||
const MOVIEBOX_PRIMARY_KEY = process.env.EXPO_PUBLIC_MOVIEBOX_PRIMARY_KEY;
|
||||
const MOVIEBOX_TMDB_API_KEY = process.env.EXPO_PUBLIC_MOVIEBOX_TMDB_API_KEY || '439c478a771f35c05022f9feabcca01c';
|
||||
if (!MOVIEBOX_PRIMARY_KEY) {
|
||||
throw new Error('Missing EXPO_PUBLIC_MOVIEBOX_PRIMARY_KEY');
|
||||
}
|
||||
|
||||
|
||||
const sandbox = {
|
||||
console: {
|
||||
log: (...args: any[]) => logger.log('[Scraper]', ...args),
|
||||
|
|
@ -1076,7 +1076,7 @@ class LocalScraperService {
|
|||
// Add fetch for HTTP requests (using native fetch for MovieBox, axios for others)
|
||||
fetch: async (url: string, options: any = {}) => {
|
||||
const isMovieBoxRequest = url.includes('api.inmoviebox.com') || url.includes('themoviedb.org');
|
||||
|
||||
|
||||
if (isMovieBoxRequest) {
|
||||
// Always use native fetch for MovieBox requests
|
||||
try {
|
||||
|
|
@ -1084,7 +1084,7 @@ class LocalScraperService {
|
|||
method: options.method || 'GET',
|
||||
hasBody: !!options.body
|
||||
});
|
||||
|
||||
|
||||
const nativeResponse = await fetch(url, {
|
||||
method: options.method || 'GET',
|
||||
headers: {
|
||||
|
|
@ -1094,13 +1094,13 @@ class LocalScraperService {
|
|||
},
|
||||
body: options.body
|
||||
});
|
||||
|
||||
|
||||
const responseData = await nativeResponse.text();
|
||||
logger.log(`[Sandbox] Native fetch successful for MovieBox:`, {
|
||||
status: nativeResponse.status,
|
||||
ok: nativeResponse.ok
|
||||
});
|
||||
|
||||
|
||||
return {
|
||||
ok: nativeResponse.ok,
|
||||
status: nativeResponse.status,
|
||||
|
|
@ -1134,7 +1134,7 @@ class LocalScraperService {
|
|||
timeout: 120000, // Increased to 2 minutes for complex scrapers
|
||||
validateStatus: () => true // Don't throw on HTTP error status codes
|
||||
};
|
||||
|
||||
|
||||
try {
|
||||
logger.log(`[Sandbox] Using axios for request: ${url}`, {
|
||||
method: axiosConfig.method,
|
||||
|
|
@ -1142,7 +1142,7 @@ class LocalScraperService {
|
|||
hasBody: !!axiosConfig.data
|
||||
});
|
||||
const response = await axios(axiosConfig);
|
||||
|
||||
|
||||
return {
|
||||
ok: response.status >= 200 && response.status < 300,
|
||||
status: response.status,
|
||||
|
|
@ -1181,7 +1181,7 @@ class LocalScraperService {
|
|||
SCRAPER_SETTINGS: perScraperSettings,
|
||||
SCRAPER_ID: params?.scraperId
|
||||
};
|
||||
|
||||
|
||||
// Execute the scraper code with 1 minute timeout
|
||||
const SCRAPER_EXECUTION_TIMEOUT_MS = 60000; // 1 minute
|
||||
|
||||
|
|
@ -1237,7 +1237,7 @@ class LocalScraperService {
|
|||
setTimeout(() => reject(new Error(`Scraper execution timed out after ${SCRAPER_EXECUTION_TIMEOUT_MS}ms`)), SCRAPER_EXECUTION_TIMEOUT_MS)
|
||||
)
|
||||
]);
|
||||
|
||||
|
||||
} catch (error) {
|
||||
logger.error('[LocalScraperService] Sandbox execution failed:', error);
|
||||
throw error;
|
||||
|
|
@ -1250,22 +1250,22 @@ class LocalScraperService {
|
|||
logger.warn('[LocalScraperService] Scraper returned non-array result');
|
||||
return [];
|
||||
}
|
||||
|
||||
|
||||
return results.map((result, index) => {
|
||||
// Build title with quality information for UI compatibility
|
||||
let title = result.title || result.name || `${scraper.name} Stream ${index + 1}`;
|
||||
|
||||
|
||||
// Add quality to title if available and not already present
|
||||
if (result.quality && !title.includes(result.quality)) {
|
||||
title = `${title} ${result.quality}`;
|
||||
}
|
||||
|
||||
|
||||
// Build name with quality information
|
||||
let streamName = result.name || `${scraper.name}`;
|
||||
if (result.quality && !streamName.includes(result.quality)) {
|
||||
streamName = `${streamName} - ${result.quality}`;
|
||||
}
|
||||
|
||||
|
||||
const stream: Stream = {
|
||||
// Include quality in name field for proper display
|
||||
name: streamName,
|
||||
|
|
@ -1280,22 +1280,22 @@ class LocalScraperService {
|
|||
bingeGroup: `local-scraper-${scraper.id}`
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
// Add additional properties if available
|
||||
if (result.infoHash) {
|
||||
stream.infoHash = result.infoHash;
|
||||
}
|
||||
|
||||
|
||||
// Preserve any additional fields from the scraper result
|
||||
if (result.quality && !stream.quality) {
|
||||
stream.quality = result.quality;
|
||||
}
|
||||
|
||||
|
||||
// Pass headers from scraper result if available
|
||||
if (result.headers) {
|
||||
stream.headers = result.headers;
|
||||
}
|
||||
|
||||
|
||||
return stream;
|
||||
}).filter(stream => stream.url); // Filter out streams without URLs
|
||||
}
|
||||
|
|
@ -1303,13 +1303,13 @@ class LocalScraperService {
|
|||
// Parse size string to bytes
|
||||
private parseSize(sizeStr: string): number {
|
||||
if (!sizeStr) return 0;
|
||||
|
||||
|
||||
const match = sizeStr.match(/([0-9.]+)\s*(GB|MB|KB|TB)/i);
|
||||
if (!match) return 0;
|
||||
|
||||
|
||||
const value = parseFloat(match[1]);
|
||||
const unit = match[2].toUpperCase();
|
||||
|
||||
|
||||
switch (unit) {
|
||||
case 'TB': return value * 1024 * 1024 * 1024 * 1024;
|
||||
case 'GB': return value * 1024 * 1024 * 1024;
|
||||
|
|
@ -1323,22 +1323,22 @@ class LocalScraperService {
|
|||
async clearScrapers(): Promise<void> {
|
||||
this.installedScrapers.clear();
|
||||
this.scraperCode.clear();
|
||||
|
||||
|
||||
// Clear from storage
|
||||
await mmkvStorage.removeItem(this.STORAGE_KEY);
|
||||
|
||||
|
||||
// Clear cached code
|
||||
const keys = await mmkvStorage.getAllKeys();
|
||||
const scraperCodeKeys = keys.filter(key => key.startsWith('scraper-code-'));
|
||||
await mmkvStorage.multiRemove(scraperCodeKeys);
|
||||
|
||||
|
||||
logger.log('[LocalScraperService] All scrapers cleared');
|
||||
}
|
||||
|
||||
// Check if local scrapers are available
|
||||
async hasScrapers(): Promise<boolean> {
|
||||
await this.ensureInitialized();
|
||||
|
||||
|
||||
// Get user settings to check if local scrapers are enabled
|
||||
const userSettings = await this.getUserScraperSettings();
|
||||
logger.log('[LocalScraperService.hasScrapers] enableLocalScrapers:', userSettings.enableLocalScrapers);
|
||||
|
|
@ -1346,13 +1346,13 @@ class LocalScraperService {
|
|||
logger.log('[LocalScraperService.hasScrapers] Returning false: local scrapers disabled');
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
// If no repository is configured, return false
|
||||
if (!this.repositoryUrl) {
|
||||
logger.log('[LocalScraperService.hasScrapers] Returning false: no repository URL configured');
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
// If no scrapers are installed, try to refresh repository
|
||||
if (this.installedScrapers.size === 0) {
|
||||
logger.log('[LocalScraperService.hasScrapers] No scrapers installed, attempting to refresh repository');
|
||||
|
|
@ -1363,16 +1363,16 @@ class LocalScraperService {
|
|||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
logger.log('[LocalScraperService.hasScrapers] installedScrapers.size:', this.installedScrapers.size);
|
||||
logger.log('[LocalScraperService.hasScrapers] enabledScrapers set size:', userSettings.enabledScrapers?.size);
|
||||
|
||||
|
||||
// Check if there are any enabled scrapers based on user settings
|
||||
if (userSettings.enabledScrapers && userSettings.enabledScrapers.size > 0) {
|
||||
logger.log('[LocalScraperService.hasScrapers] Returning true: enabledScrapers set has items');
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
// Fallback: check if any scrapers are enabled in the internal state
|
||||
const hasEnabledScrapers = Array.from(this.installedScrapers.values()).some(scraper => scraper.enabled);
|
||||
logger.log('[LocalScraperService.hasScrapers] Fallback check - hasEnabledScrapers:', hasEnabledScrapers);
|
||||
|
|
|
|||
Loading…
Reference in a new issue