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