diff --git a/src/hooks/useSettings.ts b/src/hooks/useSettings.ts
index c3d3b00..b809812 100644
--- a/src/hooks/useSettings.ts
+++ b/src/hooks/useSettings.ts
@@ -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';
diff --git a/src/screens/PluginsScreen.tsx b/src/screens/PluginsScreen.tsx
index 53dca90..d4a4f33 100644
--- a/src/screens/PluginsScreen.tsx
+++ b/src/screens/PluginsScreen.tsx
@@ -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 = () => {
+ {/* Quality Filtering */}
+
+ Quality Filtering
+
+ Exclude specific video qualities from search results. Tap on a quality to exclude it from plugin results.
+
+
+
+ {qualityOptions.map((quality) => {
+ const isExcluded = (settings.excludedQualities || []).includes(quality);
+ return (
+ handleToggleQualityExclusion(quality)}
+ disabled={!settings.enableLocalScrapers}
+ >
+
+ {isExcluded ? '✕ ' : ''}{quality}
+
+
+ );
+ })}
+
+
+ {(settings.excludedQualities || []).length > 0 && (
+
+ 💡 Excluded qualities: {(settings.excludedQualities || []).join(', ')}
+
+ )}
+
{/* About */}
diff --git a/src/screens/StreamsScreen.tsx b/src/screens/StreamsScreen.tsx
index e412a21..7b77c13 100644
--- a/src/screens/StreamsScreen.tsx
+++ b/src/screens/StreamsScreen.tsx
@@ -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) {