mirror of
https://github.com/tapframe/NuvioStreaming.git
synced 2026-04-21 08:41:57 +00:00
added sorting
This commit is contained in:
parent
a7e7c86f18
commit
f8192f53ab
3 changed files with 127 additions and 9 deletions
|
|
@ -42,6 +42,7 @@ 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
|
||||||
|
streamSortMode: 'scraper-then-quality' | 'quality-then-scraper'; // How to sort streams - by scraper first or quality first
|
||||||
// Quality filtering settings
|
// Quality filtering settings
|
||||||
excludedQualities: string[]; // Array of quality strings to exclude (e.g., ['2160p', '4K', '1080p', '720p'])
|
excludedQualities: string[]; // Array of quality strings to exclude (e.g., ['2160p', '4K', '1080p', '720p'])
|
||||||
}
|
}
|
||||||
|
|
@ -68,6 +69,7 @@ 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
|
||||||
|
streamSortMode: 'scraper-then-quality', // Default to current behavior (scraper first, then quality)
|
||||||
// Quality filtering defaults
|
// Quality filtering defaults
|
||||||
excludedQualities: [], // No qualities excluded by default
|
excludedQualities: [], // No qualities excluded by default
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -796,12 +796,34 @@ const PluginsScreen: React.FC = () => {
|
||||||
</View>
|
</View>
|
||||||
<Switch
|
<Switch
|
||||||
value={settings.streamDisplayMode === 'grouped'}
|
value={settings.streamDisplayMode === 'grouped'}
|
||||||
onValueChange={(value) => updateSetting('streamDisplayMode', value ? 'grouped' : 'separate')}
|
onValueChange={(value) => {
|
||||||
|
updateSetting('streamDisplayMode', value ? 'grouped' : 'separate');
|
||||||
|
// Auto-disable quality sorting when grouping is disabled
|
||||||
|
if (!value && settings.streamSortMode === 'quality-then-scraper') {
|
||||||
|
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.settingInfo}>
|
||||||
|
<Text style={[styles.settingTitle, (!settings.enableLocalScrapers || settings.streamDisplayMode !== 'grouped') && styles.disabledText]}>Sort by Quality First</Text>
|
||||||
|
<Text style={[styles.settingDescription, (!settings.enableLocalScrapers || settings.streamDisplayMode !== 'grouped') && styles.disabledText]}>
|
||||||
|
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.
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
<Switch
|
||||||
|
value={settings.streamSortMode === 'quality-then-scraper'}
|
||||||
|
onValueChange={(value) => updateSetting('streamSortMode', value ? 'quality-then-scraper' : 'scraper-then-quality')}
|
||||||
|
trackColor={{ false: colors.elevation3, true: colors.primary }}
|
||||||
|
thumbColor={settings.streamSortMode === 'quality-then-scraper' ? colors.white : '#f4f3f4'}
|
||||||
|
disabled={!settings.enableLocalScrapers || settings.streamDisplayMode !== 'grouped'}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
{/* Quality Filtering */}
|
{/* Quality Filtering */}
|
||||||
|
|
|
||||||
|
|
@ -537,6 +537,84 @@ export const StreamsScreen = () => {
|
||||||
});
|
});
|
||||||
}, [settings.excludedQualities]);
|
}, [settings.excludedQualities]);
|
||||||
|
|
||||||
|
// Helper function to sort streams based on user preference
|
||||||
|
const sortStreams = useCallback((streams: Stream[]) => {
|
||||||
|
const installedAddons = stremioService.getInstalledAddons();
|
||||||
|
|
||||||
|
// Helper function to extract quality as number
|
||||||
|
const getQualityNumeric = (title: string | undefined): number => {
|
||||||
|
if (!title) return 0;
|
||||||
|
|
||||||
|
// Check for 4K first (treat as 2160p)
|
||||||
|
if (/\b4k\b/i.test(title)) {
|
||||||
|
return 2160;
|
||||||
|
}
|
||||||
|
|
||||||
|
const matchWithP = title.match(/(\d+)p/i);
|
||||||
|
if (matchWithP) return parseInt(matchWithP[1], 10);
|
||||||
|
|
||||||
|
const qualityPatterns = [
|
||||||
|
/\b(240|360|480|720|1080|1440|2160|4320|8000)\b/i
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const pattern of qualityPatterns) {
|
||||||
|
const match = title.match(pattern);
|
||||||
|
if (match) {
|
||||||
|
const quality = parseInt(match[1], 10);
|
||||||
|
if (quality >= 240 && quality <= 8000) return quality;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Provider priority (higher number = higher priority)
|
||||||
|
const getProviderPriority = (stream: Stream): number => {
|
||||||
|
const addonId = stream.addonId || stream.addonName || '';
|
||||||
|
const addonIndex = installedAddons.findIndex(addon => addon.id === addonId);
|
||||||
|
|
||||||
|
if (addonIndex !== -1) {
|
||||||
|
// Higher priority for addons installed earlier (reverse index)
|
||||||
|
return 50 - addonIndex;
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0; // Unknown providers get lowest priority
|
||||||
|
};
|
||||||
|
|
||||||
|
return [...streams].sort((a, b) => {
|
||||||
|
const qualityA = getQualityNumeric(a.name || a.title);
|
||||||
|
const qualityB = getQualityNumeric(b.name || b.title);
|
||||||
|
const providerPriorityA = getProviderPriority(a);
|
||||||
|
const providerPriorityB = getProviderPriority(b);
|
||||||
|
const isCachedA = a.behaviorHints?.cached || false;
|
||||||
|
const isCachedB = b.behaviorHints?.cached || false;
|
||||||
|
|
||||||
|
// Always prioritize cached/debrid streams first
|
||||||
|
if (isCachedA !== isCachedB) {
|
||||||
|
return isCachedA ? -1 : 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (settings.streamSortMode === 'quality-then-scraper') {
|
||||||
|
// Sort by quality first, then by provider
|
||||||
|
if (qualityA !== qualityB) {
|
||||||
|
return qualityB - qualityA; // Higher quality first
|
||||||
|
}
|
||||||
|
if (providerPriorityA !== providerPriorityB) {
|
||||||
|
return providerPriorityB - providerPriorityA; // Better provider first
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Default: Sort by provider first, then by quality
|
||||||
|
if (providerPriorityA !== providerPriorityB) {
|
||||||
|
return providerPriorityB - providerPriorityA; // Better provider first
|
||||||
|
}
|
||||||
|
if (qualityA !== qualityB) {
|
||||||
|
return qualityB - qualityA; // Higher quality first
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
});
|
||||||
|
}, [settings.excludedQualities, settings.streamSortMode]);
|
||||||
|
|
||||||
// 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) {
|
||||||
|
|
@ -546,6 +624,12 @@ export const StreamsScreen = () => {
|
||||||
// Helper function to extract quality as number
|
// Helper function to extract quality as number
|
||||||
const getQualityNumeric = (title: string | undefined): number => {
|
const getQualityNumeric = (title: string | undefined): number => {
|
||||||
if (!title) return 0;
|
if (!title) return 0;
|
||||||
|
|
||||||
|
// Check for 4K first (treat as 2160p)
|
||||||
|
if (/\b4k\b/i.test(title)) {
|
||||||
|
return 2160;
|
||||||
|
}
|
||||||
|
|
||||||
const matchWithP = title.match(/(\d+)p/i);
|
const matchWithP = title.match(/(\d+)p/i);
|
||||||
if (matchWithP) return parseInt(matchWithP[1], 10);
|
if (matchWithP) return parseInt(matchWithP[1], 10);
|
||||||
|
|
||||||
|
|
@ -997,16 +1081,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
|
// Apply quality filtering and sorting to streams
|
||||||
const filteredStreams = filterStreamsByQuality(providerStreams);
|
const filteredStreams = filterStreamsByQuality(providerStreams);
|
||||||
|
const sortedStreams = sortStreams(filteredStreams);
|
||||||
|
|
||||||
if (isInstalledAddon) {
|
if (isInstalledAddon) {
|
||||||
addonStreams.push(...filteredStreams);
|
addonStreams.push(...sortedStreams);
|
||||||
if (!addonNames.includes(addonName)) {
|
if (!addonNames.includes(addonName)) {
|
||||||
addonNames.push(addonName);
|
addonNames.push(addonName);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
pluginStreams.push(...filteredStreams);
|
pluginStreams.push(...sortedStreams);
|
||||||
if (!pluginNames.includes(addonName)) {
|
if (!pluginNames.includes(addonName)) {
|
||||||
pluginNames.push(addonName);
|
pluginNames.push(addonName);
|
||||||
}
|
}
|
||||||
|
|
@ -1015,17 +1100,25 @@ export const StreamsScreen = () => {
|
||||||
|
|
||||||
const sections = [];
|
const sections = [];
|
||||||
if (addonStreams.length > 0) {
|
if (addonStreams.length > 0) {
|
||||||
|
// Apply final sorting to the combined addon streams for quality-first mode
|
||||||
|
const finalSortedAddonStreams = settings.streamSortMode === 'quality-then-scraper' ?
|
||||||
|
sortStreams(addonStreams) : addonStreams;
|
||||||
|
|
||||||
sections.push({
|
sections.push({
|
||||||
title: addonNames.join(', '),
|
title: addonNames.join(', '),
|
||||||
addonId: 'grouped-addons',
|
addonId: 'grouped-addons',
|
||||||
data: addonStreams
|
data: finalSortedAddonStreams
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (pluginStreams.length > 0) {
|
if (pluginStreams.length > 0) {
|
||||||
|
// Apply final sorting to the combined plugin streams for quality-first mode
|
||||||
|
const finalSortedPluginStreams = settings.streamSortMode === 'quality-then-scraper' ?
|
||||||
|
sortStreams(pluginStreams) : pluginStreams;
|
||||||
|
|
||||||
sections.push({
|
sections.push({
|
||||||
title: localScraperService.getRepositoryName(),
|
title: localScraperService.getRepositoryName(),
|
||||||
addonId: 'grouped-plugins',
|
addonId: 'grouped-plugins',
|
||||||
data: pluginStreams
|
data: finalSortedPluginStreams
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1033,17 +1126,18 @@ 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
|
// Apply quality filtering and sorting to streams
|
||||||
const filteredStreams = filterStreamsByQuality(providerStreams);
|
const filteredStreams = filterStreamsByQuality(providerStreams);
|
||||||
|
const sortedStreams = sortStreams(filteredStreams);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
title: addonName,
|
title: addonName,
|
||||||
addonId,
|
addonId,
|
||||||
data: filteredStreams
|
data: sortedStreams
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, [selectedProvider, type, episodeStreams, groupedStreams, settings.streamDisplayMode, filterStreamsByQuality]);
|
}, [selectedProvider, type, episodeStreams, groupedStreams, settings.streamDisplayMode, filterStreamsByQuality, sortStreams]);
|
||||||
|
|
||||||
const episodeImage = useMemo(() => {
|
const episodeImage = useMemo(() => {
|
||||||
if (episodeThumbnail) {
|
if (episodeThumbnail) {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue