mirror of
https://github.com/tapframe/NuvioStreaming.git
synced 2026-04-21 00:32:04 +00:00
added option to exclude qualities
This commit is contained in:
parent
ebcbced142
commit
84689a9ba7
3 changed files with 126 additions and 6 deletions
|
|
@ -42,6 +42,8 @@ export interface AppSettings {
|
||||||
scraperTimeout: number; // Timeout for scraper execution in seconds
|
scraperTimeout: number; // Timeout for scraper execution in seconds
|
||||||
enableScraperUrlValidation: boolean; // Enable/disable URL validation for scrapers
|
enableScraperUrlValidation: boolean; // Enable/disable URL validation for scrapers
|
||||||
streamDisplayMode: 'separate' | 'grouped'; // How to display streaming links - separately by provider or grouped under one name
|
streamDisplayMode: 'separate' | 'grouped'; // How to display streaming links - separately by provider or grouped under one name
|
||||||
|
// Quality filtering settings
|
||||||
|
excludedQualities: string[]; // Array of quality strings to exclude (e.g., ['2160p', '4K', '1080p', '720p'])
|
||||||
}
|
}
|
||||||
|
|
||||||
export const DEFAULT_SETTINGS: AppSettings = {
|
export const DEFAULT_SETTINGS: AppSettings = {
|
||||||
|
|
@ -66,6 +68,8 @@ export const DEFAULT_SETTINGS: AppSettings = {
|
||||||
scraperTimeout: 60, // 60 seconds timeout
|
scraperTimeout: 60, // 60 seconds timeout
|
||||||
enableScraperUrlValidation: true, // Enable URL validation by default
|
enableScraperUrlValidation: true, // Enable URL validation by default
|
||||||
streamDisplayMode: 'separate', // Default to separate display by provider
|
streamDisplayMode: 'separate', // Default to separate display by provider
|
||||||
|
// Quality filtering defaults
|
||||||
|
excludedQualities: [], // No qualities excluded by default
|
||||||
};
|
};
|
||||||
|
|
||||||
const SETTINGS_STORAGE_KEY = 'app_settings';
|
const SETTINGS_STORAGE_KEY = 'app_settings';
|
||||||
|
|
|
||||||
|
|
@ -328,6 +328,33 @@ const createStyles = (colors: any) => StyleSheet.create({
|
||||||
fontSize: 10,
|
fontSize: 10,
|
||||||
fontWeight: '600',
|
fontWeight: '600',
|
||||||
},
|
},
|
||||||
|
qualityChipsContainer: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
flexWrap: 'wrap',
|
||||||
|
gap: 8,
|
||||||
|
marginTop: 8,
|
||||||
|
},
|
||||||
|
qualityChip: {
|
||||||
|
backgroundColor: colors.elevation2,
|
||||||
|
paddingHorizontal: 12,
|
||||||
|
paddingVertical: 6,
|
||||||
|
borderRadius: 16,
|
||||||
|
borderWidth: 1,
|
||||||
|
borderColor: colors.elevation3,
|
||||||
|
},
|
||||||
|
qualityChipSelected: {
|
||||||
|
backgroundColor: '#ff3b30',
|
||||||
|
borderColor: '#ff3b30',
|
||||||
|
},
|
||||||
|
qualityChipText: {
|
||||||
|
color: colors.white,
|
||||||
|
fontSize: 13,
|
||||||
|
fontWeight: '500',
|
||||||
|
},
|
||||||
|
qualityChipTextSelected: {
|
||||||
|
color: colors.white,
|
||||||
|
fontWeight: '600',
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const PluginsScreen: React.FC = () => {
|
const PluginsScreen: React.FC = () => {
|
||||||
|
|
@ -510,6 +537,25 @@ const PluginsScreen: React.FC = () => {
|
||||||
await updateSetting('enableScraperUrlValidation', enabled);
|
await updateSetting('enableScraperUrlValidation', enabled);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleToggleQualityExclusion = async (quality: string) => {
|
||||||
|
const currentExcluded = settings.excludedQualities || [];
|
||||||
|
const isExcluded = currentExcluded.includes(quality);
|
||||||
|
|
||||||
|
let newExcluded: string[];
|
||||||
|
if (isExcluded) {
|
||||||
|
// Remove from excluded list
|
||||||
|
newExcluded = currentExcluded.filter(q => q !== quality);
|
||||||
|
} else {
|
||||||
|
// Add to excluded list
|
||||||
|
newExcluded = [...currentExcluded, quality];
|
||||||
|
}
|
||||||
|
|
||||||
|
await updateSetting('excludedQualities', newExcluded);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Define available quality options
|
||||||
|
const qualityOptions = ['2160p', '4K', '1080p', '720p', '360p', 'DV', 'HDR', 'REMUX', '480p', 'CAM', 'TS'];
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
@ -757,6 +803,45 @@ const PluginsScreen: React.FC = () => {
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
|
{/* Quality Filtering */}
|
||||||
|
<View style={[styles.section, !settings.enableLocalScrapers && styles.disabledSection]}>
|
||||||
|
<Text style={[styles.sectionTitle, !settings.enableLocalScrapers && styles.disabledText]}>Quality Filtering</Text>
|
||||||
|
<Text style={[styles.sectionDescription, !settings.enableLocalScrapers && styles.disabledText]}>
|
||||||
|
Exclude specific video qualities from search results. Tap on a quality to exclude it from plugin results.
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
<View style={styles.qualityChipsContainer}>
|
||||||
|
{qualityOptions.map((quality) => {
|
||||||
|
const isExcluded = (settings.excludedQualities || []).includes(quality);
|
||||||
|
return (
|
||||||
|
<TouchableOpacity
|
||||||
|
key={quality}
|
||||||
|
style={[
|
||||||
|
styles.qualityChip,
|
||||||
|
isExcluded && styles.qualityChipSelected,
|
||||||
|
!settings.enableLocalScrapers && styles.disabledButton
|
||||||
|
]}
|
||||||
|
onPress={() => handleToggleQualityExclusion(quality)}
|
||||||
|
disabled={!settings.enableLocalScrapers}
|
||||||
|
>
|
||||||
|
<Text style={[
|
||||||
|
styles.qualityChipText,
|
||||||
|
isExcluded && styles.qualityChipTextSelected,
|
||||||
|
!settings.enableLocalScrapers && styles.disabledText
|
||||||
|
]}>
|
||||||
|
{isExcluded ? '✕ ' : ''}{quality}
|
||||||
|
</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{(settings.excludedQualities || []).length > 0 && (
|
||||||
|
<Text style={[styles.infoText, { marginTop: 12 }, !settings.enableLocalScrapers && styles.disabledText]}>
|
||||||
|
💡 Excluded qualities: {(settings.excludedQualities || []).join(', ')}
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
|
||||||
{/* About */}
|
{/* About */}
|
||||||
<View style={[styles.section, styles.lastSection]}>
|
<View style={[styles.section, styles.lastSection]}>
|
||||||
|
|
|
||||||
|
|
@ -516,6 +516,27 @@ export const StreamsScreen = () => {
|
||||||
setSelectedProvider(provider);
|
setSelectedProvider(provider);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
// Helper function to filter streams by quality exclusions
|
||||||
|
const filterStreamsByQuality = useCallback((streams: Stream[]) => {
|
||||||
|
if (!settings.excludedQualities || settings.excludedQualities.length === 0) {
|
||||||
|
return streams;
|
||||||
|
}
|
||||||
|
|
||||||
|
return streams.filter(stream => {
|
||||||
|
const streamTitle = stream.title || stream.name || '';
|
||||||
|
|
||||||
|
// Check if any excluded quality is found in the stream title
|
||||||
|
const hasExcludedQuality = settings.excludedQualities.some(excludedQuality => {
|
||||||
|
// Create a case-insensitive regex pattern for the quality
|
||||||
|
const pattern = new RegExp(excludedQuality.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'i');
|
||||||
|
return pattern.test(streamTitle);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Return true to keep the stream (if it doesn't have excluded quality)
|
||||||
|
return !hasExcludedQuality;
|
||||||
|
});
|
||||||
|
}, [settings.excludedQualities]);
|
||||||
|
|
||||||
// Function to determine the best stream based on quality, provider priority, and other factors
|
// Function to determine the best stream based on quality, provider priority, and other factors
|
||||||
const getBestStream = useCallback((streamsData: typeof groupedStreams): Stream | null => {
|
const getBestStream = useCallback((streamsData: typeof groupedStreams): Stream | null => {
|
||||||
if (!streamsData || Object.keys(streamsData).length === 0) {
|
if (!streamsData || Object.keys(streamsData).length === 0) {
|
||||||
|
|
@ -566,7 +587,10 @@ export const StreamsScreen = () => {
|
||||||
}> = [];
|
}> = [];
|
||||||
|
|
||||||
Object.entries(streamsData).forEach(([addonId, { streams }]) => {
|
Object.entries(streamsData).forEach(([addonId, { streams }]) => {
|
||||||
streams.forEach(stream => {
|
// Apply quality filtering to streams before processing
|
||||||
|
const filteredStreams = filterStreamsByQuality(streams);
|
||||||
|
|
||||||
|
filteredStreams.forEach(stream => {
|
||||||
const quality = getQualityNumeric(stream.name || stream.title);
|
const quality = getQualityNumeric(stream.name || stream.title);
|
||||||
const providerPriority = getProviderPriority(addonId);
|
const providerPriority = getProviderPriority(addonId);
|
||||||
const isDebrid = stream.behaviorHints?.cached || false;
|
const isDebrid = stream.behaviorHints?.cached || false;
|
||||||
|
|
@ -607,7 +631,7 @@ export const StreamsScreen = () => {
|
||||||
logger.log(`🎯 Best stream selected: ${allStreams[0].stream.name || allStreams[0].stream.title} (Quality: ${allStreams[0].quality}p, Provider Priority: ${allStreams[0].providerPriority}, Cached: ${allStreams[0].isCached})`);
|
logger.log(`🎯 Best stream selected: ${allStreams[0].stream.name || allStreams[0].stream.title} (Quality: ${allStreams[0].quality}p, Provider Priority: ${allStreams[0].providerPriority}, Cached: ${allStreams[0].isCached})`);
|
||||||
|
|
||||||
return allStreams[0].stream;
|
return allStreams[0].stream;
|
||||||
}, []);
|
}, [filterStreamsByQuality]);
|
||||||
|
|
||||||
const currentEpisode = useMemo(() => {
|
const currentEpisode = useMemo(() => {
|
||||||
if (!selectedEpisode) return null;
|
if (!selectedEpisode) return null;
|
||||||
|
|
@ -977,13 +1001,17 @@ export const StreamsScreen = () => {
|
||||||
|
|
||||||
filteredEntries.forEach(([addonId, { addonName, streams: providerStreams }]) => {
|
filteredEntries.forEach(([addonId, { addonName, streams: providerStreams }]) => {
|
||||||
const isInstalledAddon = installedAddons.some(addon => addon.id === addonId);
|
const isInstalledAddon = installedAddons.some(addon => addon.id === addonId);
|
||||||
|
|
||||||
|
// Apply quality filtering to streams
|
||||||
|
const filteredStreams = filterStreamsByQuality(providerStreams);
|
||||||
|
|
||||||
if (isInstalledAddon) {
|
if (isInstalledAddon) {
|
||||||
addonStreams.push(...providerStreams);
|
addonStreams.push(...filteredStreams);
|
||||||
if (!addonNames.includes(addonName)) {
|
if (!addonNames.includes(addonName)) {
|
||||||
addonNames.push(addonName);
|
addonNames.push(addonName);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
pluginStreams.push(...providerStreams);
|
pluginStreams.push(...filteredStreams);
|
||||||
if (!pluginNames.includes(addonName)) {
|
if (!pluginNames.includes(addonName)) {
|
||||||
pluginNames.push(addonName);
|
pluginNames.push(addonName);
|
||||||
}
|
}
|
||||||
|
|
@ -1010,14 +1038,17 @@ export const StreamsScreen = () => {
|
||||||
} else {
|
} else {
|
||||||
// Use separate sections for each provider (current behavior)
|
// Use separate sections for each provider (current behavior)
|
||||||
return filteredEntries.map(([addonId, { addonName, streams: providerStreams }]) => {
|
return filteredEntries.map(([addonId, { addonName, streams: providerStreams }]) => {
|
||||||
|
// Apply quality filtering to streams
|
||||||
|
const filteredStreams = filterStreamsByQuality(providerStreams);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
title: addonName,
|
title: addonName,
|
||||||
addonId,
|
addonId,
|
||||||
data: providerStreams
|
data: filteredStreams
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, [selectedProvider, type, episodeStreams, groupedStreams, settings.streamDisplayMode]);
|
}, [selectedProvider, type, episodeStreams, groupedStreams, settings.streamDisplayMode, filterStreamsByQuality]);
|
||||||
|
|
||||||
const episodeImage = useMemo(() => {
|
const episodeImage = useMemo(() => {
|
||||||
if (episodeThumbnail) {
|
if (episodeThumbnail) {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue