From f76acfdcf13524fd716c0636842f7f41fe10c8f8 Mon Sep 17 00:00:00 2001 From: Peifan Li Date: Sun, 4 Jan 2026 13:08:38 -0500 Subject: [PATCH] feat: Implement helper for selecting best m3u8 URL --- .../MissAVDownloader.urlSelection.test.ts | 74 +++++++++++ .../downloaders/BilibiliDownloader.ts | 10 +- .../services/downloaders/MissAVDownloader.ts | 124 ++++++++++++------ 3 files changed, 168 insertions(+), 40 deletions(-) create mode 100644 backend/src/__tests__/services/downloaders/MissAVDownloader.urlSelection.test.ts diff --git a/backend/src/__tests__/services/downloaders/MissAVDownloader.urlSelection.test.ts b/backend/src/__tests__/services/downloaders/MissAVDownloader.urlSelection.test.ts new file mode 100644 index 0000000..d505b46 --- /dev/null +++ b/backend/src/__tests__/services/downloaders/MissAVDownloader.urlSelection.test.ts @@ -0,0 +1,74 @@ +import { MissAVDownloader } from '../../../services/downloaders/MissAVDownloader'; + +describe('MissAVDownloader URL Selection', () => { + describe('selectBestM3u8Url', () => { + it('should prioritize surrit.com master playlist over other specific quality playlists', () => { + const urls = [ + 'https://surrit.com/9183fb8b-9d17-43c7-b429-4c28b7813e2c/playlist.m3u8', + 'https://surrit.com/9183fb8b-9d17-43c7-b429-4c28b7813e2c/480p/video.m3u8', + 'https://edge-hls.growcdnssedge.com/hls/121964773/master/121964773_240p.m3u8', + 'https://media-hls.growcdnssedge.com/b-hls-18/121964773/121964773_240p.m3u8' + ]; + + // Default behavior (no format sort) + const selected = MissAVDownloader.selectBestM3u8Url(urls, false); + expect(selected).toBe('https://surrit.com/9183fb8b-9d17-43c7-b429-4c28b7813e2c/playlist.m3u8'); + }); + + it('should prioritize higher resolution when multiple surrit URLs exist', () => { + const urls = [ + 'https://surrit.com/uuid/playlist.m3u8', // Master + 'https://surrit.com/uuid/720p/video.m3u8', + 'https://surrit.com/uuid/480p/video.m3u8' + ]; + + const selected = MissAVDownloader.selectBestM3u8Url(urls, false); + // If we have specific qualities, we usually prefer the highest specific one if no format sort is used, + // OR we might prefer the master if we trust yt-dlp to pick best. + // Based on typical behavior without format sort: existing logic preferred specific resolutions. + // But for MissAV, playlist.m3u8 is usually more reliable/complete. + // Let's assume we want to stick with Master if available for surrit. + expect(selected).toContain('playlist.m3u8'); + // OR if we keep logic "prefer specific quality", then 720p. + // The requirement is "Prioritize surrit.com URLs... prefer playlist.m3u8 (generic master) over specific resolution masters if the specific resolution is low/suspicious" + // In this case 720p is good. + // However, usually playlist.m3u8 contains all variants. + }); + + it('should fallback to resolution comparison if no surrit URLs', () => { + const urls = [ + 'https://other.com/video_240p.m3u8', + 'https://other.com/video_720p.m3u8', + 'https://other.com/video_480p.m3u8' + ]; + + const selected = MissAVDownloader.selectBestM3u8Url(urls, false); + expect(selected).toBe('https://other.com/video_720p.m3u8'); + }); + + it('should handle real world scenario from logs', () => { + // From user log + const urls = [ + 'https://surrit.com/9183fb8b-9d17-43c7-b429-4c28b7813e2c/playlist.m3u8', + 'https://surrit.com/9183fb8b-9d17-43c7-b429-4c28b7813e2c/480p/video.m3u8', + 'https://media-hls.growcdnssedge.com/b-hls-18/121964773/121964773_240p.m3u8', + 'https://edge-hls.growcdnssedge.com/hls/121964773/master/121964773_240p.m3u8' + ]; + + const selected = MissAVDownloader.selectBestM3u8Url(urls, false); + // The bug was it picked the last one (edge-hls...240p.m3u8) or similar. + // We want the surrit playlist. + expect(selected).toBe('https://surrit.com/9183fb8b-9d17-43c7-b429-4c28b7813e2c/playlist.m3u8'); + }); + + it('should respect format sort when enabled', () => { + const urls = [ + 'https://surrit.com/uuid/playlist.m3u8', + 'https://surrit.com/uuid/480p/video.m3u8' + ]; + // With format sort, we DEFINITELY want the master playlist so yt-dlp can do the sorting + const selected = MissAVDownloader.selectBestM3u8Url(urls, true); + expect(selected).toBe('https://surrit.com/uuid/playlist.m3u8'); + }); + }); +}); diff --git a/backend/src/services/downloaders/BilibiliDownloader.ts b/backend/src/services/downloaders/BilibiliDownloader.ts index 0704aed..39e959a 100644 --- a/backend/src/services/downloaders/BilibiliDownloader.ts +++ b/backend/src/services/downloaders/BilibiliDownloader.ts @@ -1,3 +1,4 @@ +import { SUBTITLES_DIR } from "../../config/paths"; import { extractBilibiliVideoId } from "../../utils/helpers"; import { Video } from "../storageService"; import { BaseDownloader, DownloadOptions, VideoInfo } from "./BaseDownloader"; @@ -22,7 +23,7 @@ export type { BilibiliVideoInfo, BilibiliVideosResult, CollectionDownloadResult, - DownloadResult, + DownloadResult }; export class BilibiliDownloader extends BaseDownloader { @@ -183,6 +184,11 @@ export class BilibiliDownloader extends BaseDownloader { videoUrl: string, baseFilename: string ): Promise> { - return bilibiliSubtitle.downloadSubtitles(videoUrl, baseFilename); + return bilibiliSubtitle.downloadSubtitles( + videoUrl, + baseFilename, + SUBTITLES_DIR, + "/subtitles" + ); } } diff --git a/backend/src/services/downloaders/MissAVDownloader.ts b/backend/src/services/downloaders/MissAVDownloader.ts index 5e6a3b3..ffe267c 100644 --- a/backend/src/services/downloaders/MissAVDownloader.ts +++ b/backend/src/services/downloaders/MissAVDownloader.ts @@ -162,48 +162,16 @@ export class MissAVDownloader extends BaseDownloader { const hasFormatSort = !!(userConfig.S || userConfig.formatSort); // 4. Select the best m3u8 URL from collected URLs - // If user specified format sort, prefer master playlists so yt-dlp can choose resolution - // Otherwise, prefer specific quality playlists - let m3u8Url: string | null = null; - if (m3u8Urls.length > 0) { - // Sort URLs based on whether user wants format sort - const sortedUrls = m3u8Urls.sort((a, b) => { - const aIsMaster = - a.includes("/playlist.m3u8") || a.includes("/master/"); - const bIsMaster = - b.includes("/playlist.m3u8") || b.includes("/master/"); - - if (hasFormatSort) { - // When format sort is specified, prefer master playlists - // so yt-dlp can apply format sort to choose the right resolution - if (aIsMaster && !bIsMaster) return -1; // Master playlist first - if (!aIsMaster && bIsMaster) return 1; - // Among master playlists or non-master playlists, prefer higher quality - const aQuality = a.match(/(\d+p)/)?.[1] || "0p"; - const bQuality = b.match(/(\d+p)/)?.[1] || "0p"; - const aQualityNum = parseInt(aQuality) || 0; - const bQualityNum = parseInt(bQuality) || 0; - return bQualityNum - aQualityNum; // Higher quality first - } else { - // Default behavior: prefer specific quality playlists over master playlists - if (aIsMaster && !bIsMaster) return 1; - if (!aIsMaster && bIsMaster) return -1; - // Among non-master playlists, prefer higher quality (480p > 240p) - const aQuality = a.match(/(\d+p)/)?.[1] || "0p"; - const bQuality = b.match(/(\d+p)/)?.[1] || "0p"; - const aQualityNum = parseInt(aQuality) || 0; - const bQualityNum = parseInt(bQuality) || 0; - return bQualityNum - aQualityNum; // Higher quality first - } - }); - - m3u8Url = sortedUrls[0]; + let m3u8Url = MissAVDownloader.selectBestM3u8Url(m3u8Urls, hasFormatSort); + + if (m3u8Url) { logger.info( `Selected m3u8 URL from ${m3u8Urls.length} candidates (format sort: ${hasFormatSort}):`, m3u8Url ); - if (sortedUrls.length > 1) { - logger.info("Alternative URLs:", sortedUrls.slice(1)); + const alternatives = m3u8Urls.filter(u => u !== m3u8Url); + if (alternatives.length > 0) { + logger.info("Alternative URLs:", alternatives); } } @@ -539,4 +507,84 @@ export class MissAVDownloader extends BaseDownloader { throw error; } } + + // Helper to select best m3u8 URL + static selectBestM3u8Url(urls: string[], hasFormatSort: boolean): string | null { + if (urls.length === 0) return null; + + const sortedUrls = [...urls].sort((a, b) => { + // 1. Priority: surrit.com + const aIsSurrit = a.includes("surrit.com"); + const bIsSurrit = b.includes("surrit.com"); + if (aIsSurrit && !bIsSurrit) return -1; + if (!aIsSurrit && bIsSurrit) return 1; + + // 2. Priority: Master playlist (playlist.m3u8 specifically for surrit, or general master) + // We generally prefer master playlists because they contain all variants, allowing yt-dlp to pick the best. + // The previous logic penalized master playlists without explicit resolution, which caused issues. + const aIsMaster = a.includes("/playlist.m3u8") || a.includes("/master/"); + const bIsMaster = b.includes("/playlist.m3u8") || b.includes("/master/"); + + // If we are strictly comparing surrit URLs (both are surrit), we prefer the master playlist + // because it's the "cleanest" source. + if (aIsSurrit && bIsSurrit) { + const aIsPlaylistM3u8 = a.includes("playlist.m3u8"); + const bIsPlaylistM3u8 = b.includes("playlist.m3u8"); + if (aIsPlaylistM3u8 && !bIsPlaylistM3u8) return -1; + if (!aIsPlaylistM3u8 && bIsPlaylistM3u8) return 1; + } + + // If format sort is enabled, we almost always want the master playlist + if (hasFormatSort) { + if (aIsMaster && !bIsMaster) return -1; + if (!aIsMaster && bIsMaster) return 1; + } else { + // If NO format sort, previously we preferred specific resolution. + // BUT, given the bug report where a 240p stream was picked over a master, + // we should probably trust the master playlist more particularly if the alternative is low quality. + // However, if we have a high quality specific stream (e.g. 720p/1080p explicit), that might be fine. + + // Let's refine: If one is surrit master, pick it. (Handled by step 1 & surrit sub-logic) + // If neither is surrit, and one is master... + + // If both are master or both are not master, compare resolution. + } + + // 3. Priority: Resolution (detected from URL) + const aQuality = a.match(/(\d+p)/)?.[1] || "0p"; + const bQuality = b.match(/(\d+p)/)?.[1] || "0p"; + const aQualityNum = parseInt(aQuality) || 0; + const bQualityNum = parseInt(bQuality) || 0; + + // If we have a significant resolution difference, we might prefer the higher one + // UNLESS one is a master playlist and the other is a low res specific one. + // If one is master (0p detected) and other is 240p, 0p (master) should win if it's likely to contain better streams. + + // Updated Strategy: + // If both have resolution, compare them. + if (aQualityNum > 0 && bQualityNum > 0) { + return bQualityNum - aQualityNum; // Higher quality first + } + + // If one is master (assumed 0p from URL) and other is specific resolution: + // If we are prioritizing master playlists (e.g. because of surrit or format sort), master wins. + // If we are NOT specifically prioritizing master, we still might want to prefer it over very low res (e.g. < 480p). + if (aIsMaster && bQualityNum > 0 && bQualityNum < 480) return -1; // Master wins over < 480p + if (bIsMaster && aQualityNum > 0 && aQualityNum < 480) return 1; // Master wins over < 480p + + // Fallback: Default to higher number (so 720p wins over 0p/master if we didn't catch it above) + // This preserves 'best attempt' for specific high quality URLs if they exist not on surrit. + if (aQualityNum !== bQualityNum) { + return bQualityNum - aQualityNum; + } + + // Final tie-breaker: prefer master if all else equal + if (aIsMaster && !bIsMaster) return -1; + if (!aIsMaster && bIsMaster) return 1; + + return 0; + }); + + return sortedUrls[0]; + } }