diff --git a/lib/player/resolution-probe-utils.ts b/lib/player/resolution-probe-utils.ts index f409fe3..bc35ca1 100644 --- a/lib/player/resolution-probe-utils.ts +++ b/lib/player/resolution-probe-utils.ts @@ -1,4 +1,4 @@ -import { extractPlaybackQualityLabel } from '@/lib/utils/video'; +import { extractNumericResolutionLabel } from '@/lib/utils/video'; export interface ResolutionProbeLabel { label: string; @@ -8,27 +8,22 @@ export interface ResolutionProbeLabel { } const QUALITY_RANK: Record = { + '8K': 780, '4K': 700, '2K': 620, '1080P': 540, - '蓝光': 520, - 'HDR': 500, - '超清': 480, '720P': 420, - 'WEB-DL': 380, - 'HDTV': 360, - '高清': 340, '540P': 300, - 'DVD': 280, '480P': 260, '360P': 220, - 'TS': 120, - 'SD': 100, + '240P': 180, + '144P': 160, }; const DIMENSION_PATTERN = /(\d{3,4})\s*[xX]\s*(\d{3,4})/g; const HLS_RESOLUTION_PATTERN = /RESOLUTION=(\d+)x(\d+)/gi; const TEXT_QUALITY_PATTERNS: Array<{ pattern: RegExp; width?: number; height?: number; label: string; color: string }> = [ + { pattern: /(?:^|[^\d])(4320p?|8k)(?:[^\d]|$)/i, width: 7680, height: 4320, label: '8K', color: 'bg-rose-500' }, { pattern: /(?:^|[^\d])(2160p?|4k|uhd)(?:[^\d]|$)/i, width: 3840, height: 2160, label: '4K', color: 'bg-amber-500' }, { pattern: /(?:^|[^\d])(1440p?|2k|qhd)(?:[^\d]|$)/i, width: 2560, height: 1440, label: '2K', color: 'bg-emerald-500' }, { pattern: /(?:^|[^\d])(1080p?|1080i|fhd|fullhd|full-hd)(?:[^\d]|$)/i, width: 1920, height: 1080, label: '1080P', color: 'bg-green-500' }, @@ -36,12 +31,15 @@ const TEXT_QUALITY_PATTERNS: Array<{ pattern: RegExp; width?: number; height?: n { pattern: /(?:^|[^\d])540p?(?:[^\d]|$)/i, width: 960, height: 540, label: '540P', color: 'bg-cyan-500' }, { pattern: /(?:^|[^\d])480p?(?:[^\d]|$)/i, width: 854, height: 480, label: '480P', color: 'bg-sky-500' }, { pattern: /(?:^|[^\d])360p?(?:[^\d]|$)/i, width: 640, height: 360, label: '360P', color: 'bg-gray-500' }, + { pattern: /(?:^|[^\d])240p?(?:[^\d]|$)/i, width: 426, height: 240, label: '240P', color: 'bg-gray-500' }, + { pattern: /(?:^|[^\d])144p?(?:[^\d]|$)/i, width: 256, height: 144, label: '144P', color: 'bg-gray-500' }, ]; export function getResolutionLabel(width: number, height: number): ResolutionProbeLabel { const normalizedWidth = Math.max(width, height); const normalizedHeight = Math.min(width, height); + if (normalizedHeight >= 4320) return { width: normalizedWidth, height: normalizedHeight, label: '8K', color: 'bg-rose-500' }; if (normalizedHeight >= 2160) return { width: normalizedWidth, height: normalizedHeight, label: '4K', color: 'bg-amber-500' }; if (normalizedHeight >= 1440) return { width: normalizedWidth, height: normalizedHeight, label: '2K', color: 'bg-emerald-500' }; if (normalizedHeight >= 1080) return { width: normalizedWidth, height: normalizedHeight, label: '1080P', color: 'bg-green-500' }; @@ -49,6 +47,8 @@ export function getResolutionLabel(width: number, height: number): ResolutionPro if (normalizedHeight >= 540) return { width: normalizedWidth, height: normalizedHeight, label: '540P', color: 'bg-cyan-500' }; if (normalizedHeight >= 480) return { width: normalizedWidth, height: normalizedHeight, label: '480P', color: 'bg-sky-500' }; if (normalizedHeight >= 360) return { width: normalizedWidth, height: normalizedHeight, label: '360P', color: 'bg-gray-500' }; + if (normalizedHeight >= 240) return { width: normalizedWidth, height: normalizedHeight, label: '240P', color: 'bg-gray-500' }; + if (normalizedHeight >= 144) return { width: normalizedWidth, height: normalizedHeight, label: '144P', color: 'bg-gray-500' }; return { width: normalizedWidth, height: normalizedHeight, label: `${normalizedHeight}P`, color: 'bg-gray-500' }; } @@ -95,7 +95,7 @@ export function extractResolutionHint(...values: Array): Res }); } - best = chooseHigherQuality(best, extractPlaybackQualityLabel(value) || null); + best = chooseHigherQuality(best, extractNumericResolutionLabel(value) || null); } return best; diff --git a/lib/player/source-list-utils.ts b/lib/player/source-list-utils.ts index 388b2e2..aa34528 100644 --- a/lib/player/source-list-utils.ts +++ b/lib/player/source-list-utils.ts @@ -1,4 +1,4 @@ -import { extractPlaybackQualityLabel } from '@/lib/utils/video'; +import { extractNumericResolutionLabel } from '@/lib/utils/video'; export interface ResolutionBadge { label: string; @@ -42,5 +42,5 @@ export function getSourceResolutionBadge(options: { return { label: cachedResolution.label, color: cachedResolution.color }; } - return extractPlaybackQualityLabel(remarks) || null; + return extractNumericResolutionLabel(remarks) || null; } diff --git a/lib/utils/video.ts b/lib/utils/video.ts index 6494387..8312ade 100644 --- a/lib/utils/video.ts +++ b/lib/utils/video.ts @@ -27,39 +27,35 @@ export function parseVideoTitle(title: string): { cleanTitle: string, quality?: } /** - * Quality keywords and their display labels, ordered by priority (highest first). + * Numeric resolution keywords only. + * Qualitative labels such as "蓝光" or "高清" are intentionally excluded because + * they are not real numeric resolutions. */ -const PLAYBACK_QUALITY_PATTERNS: { pattern: RegExp; label: string; color: string }[] = [ - { pattern: /4k|2160p|uhd/i, label: '4K', color: 'bg-amber-500' }, - { pattern: /2k|1440p|qhd/i, label: '2K', color: 'bg-emerald-500' }, - { pattern: /蓝光|藍光|bluray|blu-ray|remux/i, label: '蓝光', color: 'bg-blue-500' }, - { pattern: /\bhdr\b|hdr10\+?/i, label: 'HDR', color: 'bg-violet-500' }, - { pattern: /1080p|1080i|full\s*hd|fhd/i, label: '1080P', color: 'bg-green-500' }, - { pattern: /超清|超高清/i, label: '超清', color: 'bg-green-500' }, - { pattern: /540p/i, label: '540P', color: 'bg-cyan-500' }, - { pattern: /720p|hd720/i, label: '720P', color: 'bg-teal-500' }, - { pattern: /480p/i, label: '480P', color: 'bg-sky-500' }, - { pattern: /360p/i, label: '360P', color: 'bg-gray-500' }, - { pattern: /高清|流畅/i, label: '高清', color: 'bg-sky-500' }, - { pattern: /web-?dl|webrip/i, label: 'WEB-DL', color: 'bg-indigo-500' }, - { pattern: /hdtv/i, label: 'HDTV', color: 'bg-teal-500' }, - { pattern: /dvd|dvdrip/i, label: 'DVD', color: 'bg-purple-500' }, - { pattern: /抢先|枪版|ts版|(?:^|[\s([【_-])(ts|cam|hdts)(?:$|[\s)\]】_-])|预告/i, label: 'TS', color: 'bg-orange-500' }, - { pattern: /标清|(?:^|[\s([【_-])sd(?:$|[\s)\]】_-])/i, label: 'SD', color: 'bg-gray-500' }, +const NUMERIC_RESOLUTION_PATTERNS: { pattern: RegExp; label: string; color: string }[] = [ + { pattern: /(?:^|[^\d])(4320p?|8k)(?:[^\d]|$)/i, label: '8K', color: 'bg-rose-500' }, + { pattern: /(?:^|[^\d])(2160p?|4k|uhd)(?:[^\d]|$)/i, label: '4K', color: 'bg-amber-500' }, + { pattern: /(?:^|[^\d])(1440p?|2k|qhd)(?:[^\d]|$)/i, label: '2K', color: 'bg-emerald-500' }, + { pattern: /(?:^|[^\d])(1080p?|1080i|full\s*hd|fhd)(?:[^\d]|$)/i, label: '1080P', color: 'bg-green-500' }, + { pattern: /(?:^|[^\d])720p?(?:[^\d]|$)|hd720/i, label: '720P', color: 'bg-teal-500' }, + { pattern: /(?:^|[^\d])540p?(?:[^\d]|$)/i, label: '540P', color: 'bg-cyan-500' }, + { pattern: /(?:^|[^\d])480p?(?:[^\d]|$)/i, label: '480P', color: 'bg-sky-500' }, + { pattern: /(?:^|[^\d])360p?(?:[^\d]|$)/i, label: '360P', color: 'bg-gray-500' }, + { pattern: /(?:^|[^\d])240p?(?:[^\d]|$)/i, label: '240P', color: 'bg-gray-500' }, + { pattern: /(?:^|[^\d])144p?(?:[^\d]|$)/i, label: '144P', color: 'bg-gray-500' }, ]; /** - * Extracts a playback quality label from video remarks or title. - * This intentionally excludes language, subtitle, and audio labels. + * Extracts a numeric playback resolution from video remarks or title. + * Non-numeric labels are intentionally ignored. */ -export function extractPlaybackQualityLabel( +export function extractNumericResolutionLabel( remarks?: string, quality?: string ): { label: string; color: string } | null { const text = `${remarks || ''} ${quality || ''}`; if (!text.trim()) return null; - for (const { pattern, label, color } of PLAYBACK_QUALITY_PATTERNS) { + for (const { pattern, label, color } of NUMERIC_RESOLUTION_PATTERNS) { if (pattern.test(text)) { return { label, color }; } @@ -68,6 +64,13 @@ export function extractPlaybackQualityLabel( return null; } +export function extractPlaybackQualityLabel( + remarks?: string, + quality?: string +): { label: string; color: string } | null { + return extractNumericResolutionLabel(remarks, quality); +} + export function extractQualityLabel(remarks?: string, quality?: string): { label: string; color: string } | null { - return extractPlaybackQualityLabel(remarks, quality); + return extractNumericResolutionLabel(remarks, quality); } diff --git a/tests/player-source-list.test.ts b/tests/player-source-list.test.ts index 28d76ab..07ba346 100644 --- a/tests/player-source-list.test.ts +++ b/tests/player-source-list.test.ts @@ -5,7 +5,7 @@ import { shouldExpandForCurrentSource, } from '@/lib/player/source-list-utils'; import { shouldReuseCachedResolution } from '@/lib/player/resolution-cache'; -import { extractPlaybackQualityLabel } from '@/lib/utils/video'; +import { extractNumericResolutionLabel } from '@/lib/utils/video'; test('shouldExpandForCurrentSource detects hidden active sources', () => { const sources = [ @@ -27,7 +27,7 @@ test('getSourceResolutionBadge prefers current actual resolution, then probed, t currentResolution: { label: '1080P', color: 'bg-green-500' }, probedResolution: { label: '720P', color: 'bg-teal-500' }, cachedResolution: { label: '4K', color: 'bg-amber-500' }, - remarks: '蓝光', + remarks: '1080p', }); assert.deepEqual(current, { label: '1080P', color: 'bg-green-500' }); @@ -35,32 +35,37 @@ test('getSourceResolutionBadge prefers current actual resolution, then probed, t isCurrent: false, probedResolution: { label: '720P', color: 'bg-teal-500' }, cachedResolution: { label: '4K', color: 'bg-amber-500' }, - remarks: '蓝光', + remarks: '1080p', }); assert.deepEqual(probed, { label: '720P', color: 'bg-teal-500' }); const cached = getSourceResolutionBadge({ isCurrent: false, cachedResolution: { label: '4K', color: 'bg-amber-500' }, - remarks: '蓝光', + remarks: '1080p', }); assert.deepEqual(cached, { label: '4K', color: 'bg-amber-500' }); const remark = getSourceResolutionBadge({ isCurrent: false, - remarks: '蓝光原盘', + remarks: '2160p remux', }); - assert.deepEqual(remark, { label: '蓝光', color: 'bg-blue-500' }); + assert.deepEqual(remark, { label: '4K', color: 'bg-amber-500' }); }); -test('getSourceResolutionBadge does not treat language markers as playback quality', () => { +test('getSourceResolutionBadge only accepts numeric resolution hints', () => { const remark = getSourceResolutionBadge({ isCurrent: false, - remarks: '国语', + remarks: '国语 蓝光原盘', }); assert.equal(remark, null); - assert.equal(extractPlaybackQualityLabel('中字'), null); - assert.equal(extractPlaybackQualityLabel('segment.ts'), null); + assert.equal(extractNumericResolutionLabel('中字'), null); + assert.equal(extractNumericResolutionLabel('segment.ts'), null); + assert.equal(extractNumericResolutionLabel('HDR remux'), null); + assert.deepEqual(extractNumericResolutionLabel('folder/2160/index.m3u8'), { + label: '4K', + color: 'bg-amber-500', + }); }); test('shouldReuseCachedResolution keeps played results across episode changes but re-probes stale probed data', () => { @@ -92,8 +97,8 @@ test('shouldReuseCachedResolution keeps played results across episode changes bu }, 5), false); assert.equal(shouldReuseCachedResolution({ - label: '蓝光', - color: 'bg-blue-500', + label: '1080P', + color: 'bg-green-500', origin: 'hint', episodeIndex: 2, }, 2), false); diff --git a/tests/resolution-probe-utils.test.ts b/tests/resolution-probe-utils.test.ts index a1a028a..e31287e 100644 --- a/tests/resolution-probe-utils.test.ts +++ b/tests/resolution-probe-utils.test.ts @@ -13,9 +13,11 @@ test('extractResolutionHint recognizes resolution hints from URLs and remarks', ); assert.deepEqual( - extractResolutionHint('蓝光原盘'), - { label: '蓝光', color: 'bg-blue-500' } + extractResolutionHint('片源 1280x720'), + { label: '720P', color: 'bg-teal-500', width: 1280, height: 720 } ); + + assert.equal(extractResolutionHint('蓝光原盘'), null); }); test('parseResolutionFromManifest prefers the highest explicit resolution', () => {