added option to exclude qualities

This commit is contained in:
tapframe 2025-07-29 01:26:57 +05:30
parent ebcbced142
commit 84689a9ba7
3 changed files with 126 additions and 6 deletions

View file

@ -42,6 +42,8 @@ export interface AppSettings {
scraperTimeout: number; // Timeout for scraper execution in seconds
enableScraperUrlValidation: boolean; // Enable/disable URL validation for scrapers
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 = {
@ -66,6 +68,8 @@ export const DEFAULT_SETTINGS: AppSettings = {
scraperTimeout: 60, // 60 seconds timeout
enableScraperUrlValidation: true, // Enable URL validation by default
streamDisplayMode: 'separate', // Default to separate display by provider
// Quality filtering defaults
excludedQualities: [], // No qualities excluded by default
};
const SETTINGS_STORAGE_KEY = 'app_settings';

View file

@ -328,6 +328,33 @@ const createStyles = (colors: any) => StyleSheet.create({
fontSize: 10,
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 = () => {
@ -510,6 +537,25 @@ const PluginsScreen: React.FC = () => {
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 (
@ -757,6 +803,45 @@ const PluginsScreen: React.FC = () => {
</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 */}
<View style={[styles.section, styles.lastSection]}>

View file

@ -516,6 +516,27 @@ export const StreamsScreen = () => {
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
const getBestStream = useCallback((streamsData: typeof groupedStreams): Stream | null => {
if (!streamsData || Object.keys(streamsData).length === 0) {
@ -566,7 +587,10 @@ export const StreamsScreen = () => {
}> = [];
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 providerPriority = getProviderPriority(addonId);
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})`);
return allStreams[0].stream;
}, []);
}, [filterStreamsByQuality]);
const currentEpisode = useMemo(() => {
if (!selectedEpisode) return null;
@ -977,13 +1001,17 @@ export const StreamsScreen = () => {
filteredEntries.forEach(([addonId, { addonName, streams: providerStreams }]) => {
const isInstalledAddon = installedAddons.some(addon => addon.id === addonId);
// Apply quality filtering to streams
const filteredStreams = filterStreamsByQuality(providerStreams);
if (isInstalledAddon) {
addonStreams.push(...providerStreams);
addonStreams.push(...filteredStreams);
if (!addonNames.includes(addonName)) {
addonNames.push(addonName);
}
} else {
pluginStreams.push(...providerStreams);
pluginStreams.push(...filteredStreams);
if (!pluginNames.includes(addonName)) {
pluginNames.push(addonName);
}
@ -1010,14 +1038,17 @@ export const StreamsScreen = () => {
} else {
// Use separate sections for each provider (current behavior)
return filteredEntries.map(([addonId, { addonName, streams: providerStreams }]) => {
// Apply quality filtering to streams
const filteredStreams = filterStreamsByQuality(providerStreams);
return {
title: addonName,
addonId,
data: providerStreams
data: filteredStreams
};
});
}
}, [selectedProvider, type, episodeStreams, groupedStreams, settings.streamDisplayMode]);
}, [selectedProvider, type, episodeStreams, groupedStreams, settings.streamDisplayMode, filterStreamsByQuality]);
const episodeImage = useMemo(() => {
if (episodeThumbnail) {