From 84689a9ba7f4c319a0f67ab62b3befa26712ddb9 Mon Sep 17 00:00:00 2001 From: tapframe Date: Tue, 29 Jul 2025 01:26:57 +0530 Subject: [PATCH] added option to exclude qualities --- src/hooks/useSettings.ts | 4 ++ src/screens/PluginsScreen.tsx | 85 +++++++++++++++++++++++++++++++++++ src/screens/StreamsScreen.tsx | 43 +++++++++++++++--- 3 files changed, 126 insertions(+), 6 deletions(-) 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) {