feat: Implement helper for selecting best m3u8 URL

This commit is contained in:
Peifan Li
2026-01-04 13:08:38 -05:00
parent 98ec0b342f
commit f76acfdcf1
3 changed files with 168 additions and 40 deletions

View File

@@ -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');
});
});
});

View File

@@ -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<Array<{ language: string; filename: string; path: string }>> {
return bilibiliSubtitle.downloadSubtitles(videoUrl, baseFilename);
return bilibiliSubtitle.downloadSubtitles(
videoUrl,
baseFilename,
SUBTITLES_DIR,
"/subtitles"
);
}
}

View File

@@ -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];
}
}