From 7a6780240539b2e2305278908d6c1b4ed1da3bbe Mon Sep 17 00:00:00 2001 From: Pierre Date: Sat, 6 Jun 2026 15:19:09 +0200 Subject: [PATCH] detection improvement --- package-lock.json | 5 - src/lib/FileNameVideoMetaExtractor.ts | 18 +++ src/lib/index.ts | 1 + src/lib/tools/video/ReleaseTagExtractor.ts | 148 +++++++++++++++++++++ src/tests/ReleaseTagExtractor.test.ts | 56 ++++++++ 5 files changed, 223 insertions(+), 5 deletions(-) create mode 100644 src/lib/tools/video/ReleaseTagExtractor.ts create mode 100644 src/tests/ReleaseTagExtractor.test.ts diff --git a/package-lock.json b/package-lock.json index 127a345..f888ba5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -961,7 +961,6 @@ "integrity": "sha512-N2clP5pJhB2YnZJ3PIHFk5RkygRX5WO/5f0WC08tp0wd+sv0rsJk3MqWn3CbNmT2J505a5336jaQj4ph1AdMug==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "undici-types": "~6.21.0" } @@ -1327,7 +1326,6 @@ "dev": true, "hasInstallScript": true, "license": "MIT", - "peer": true, "bin": { "esbuild": "bin/esbuild" }, @@ -1902,7 +1900,6 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -1952,7 +1949,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", @@ -2475,7 +2471,6 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" diff --git a/src/lib/FileNameVideoMetaExtractor.ts b/src/lib/FileNameVideoMetaExtractor.ts index 5a7c4b2..d81fa08 100644 --- a/src/lib/FileNameVideoMetaExtractor.ts +++ b/src/lib/FileNameVideoMetaExtractor.ts @@ -1,6 +1,7 @@ import {FileNameCleaner} from "./tools/FileNameCleaner.js"; import {StringArrayOperation} from "./tools/StringArrayOperation.js"; import {SerieFilesNameAnalysisTool} from "./tools/video/SerieFilesNamaAnalysisTool.js"; +import {extractQuality, extractCodec, extractLanguages} from "./tools/video/ReleaseTagExtractor.js"; import path from 'path'; /** @@ -14,6 +15,12 @@ export interface VideoFileMetadata { movieYear?: string; videoType?: 'tvshow' | 'movie'; extra?: string; + /** Title-derived resolution tier (`"1080p"`, …). See ReleaseTagExtractor. */ + quality?: string; + /** Title-derived normalized video codec (`"hevc"`/`"h264"`/…). */ + codec?: string; + /** Title-derived audio/subtitle languages, ISO 639-2/B (`["fre","mul"]`). */ + languages?: string[]; } const MIN_TITLE_LENGTH = 3; @@ -310,6 +317,17 @@ export class FileNameVideoMetaExtractor { result.extra = "true"; } + // Title-derived release tags (quality / codec / languages). Matched over + // the whole path so a tag in the parent folder ("Show.1080p/ep01.mkv") + // or the filename both count. Best-effort — a real-stream probe should + // take precedence when one is available (see ReleaseTagExtractor). + const quality = extractQuality(filePath); + if (quality) result.quality = quality; + const codec = extractCodec(filePath); + if (codec) result.codec = codec; + const languages = extractLanguages(filePath); + if (languages.length > 0) result.languages = languages; + // Compute video type const season = result.season ? parseInt(result.season) : -1; const episode = result.episode ? parseInt(result.episode) : -1; diff --git a/src/lib/index.ts b/src/lib/index.ts index 61ffeda..bdf9f59 100644 --- a/src/lib/index.ts +++ b/src/lib/index.ts @@ -6,6 +6,7 @@ export * from './FileType.js'; export * from './tools/folder/FolderTool.js'; export * from './tools/lang/LanguageTool.js'; export * from './tools/video/SerieFilesNamaAnalysisTool.js'; +export * from './tools/video/ReleaseTagExtractor.js'; export * from './tools/FileNameCleaner.js'; export * from './tools/FileNameStringOperation.js'; export * from './tools/FileTypeConfigurable.js'; diff --git a/src/lib/tools/video/ReleaseTagExtractor.ts b/src/lib/tools/video/ReleaseTagExtractor.ts new file mode 100644 index 0000000..da26433 --- /dev/null +++ b/src/lib/tools/video/ReleaseTagExtractor.ts @@ -0,0 +1,148 @@ +/** + * ReleaseTagExtractor — title-derived video release tags: resolution `quality`, + * `codec`, and audio/subtitle `languages`. + * + * These are a faithful TypeScript port of the gateway feeders' Rust + * `filename_meta.rs` (`extract_quality` / `extract_codec` / `extract_languages`). + * Keeping the two in lockstep means a record enriched by the meta-sort + * filename-parser plugin and one stamped by a gateway feeder agree on the same + * normalized vocabulary (METADATA_KEYS: `quality` §3, `codec` §12, `languages` + * §1 in the ISO 639-2/B `fre`/`ger`/`chi` space meta-share's language chips use). + * + * All three are **title-string guesses** — for a locally-ingested file the + * authoritative values come from probing the real stream (ffmpeg / language + * plugin), which a consumer should prefer (`real-probe > title-guess`). + */ + +/** Resolution tier regex (progressive/interlaced collapse to the `p` tier). */ +const QUALITY_TIER = /\b(2160p|4k|1080p|1080i|720p|720i|576p|480p)\b/i; +/** `WxH` token — mapped to a tier by height. */ +const QUALITY_WXH = /\b\d{3,4}\s*x\s*(\d{3,4})\b/i; + +/** + * Normalized resolution tier (`"2160p"`/`"1080p"`/…) or `undefined`. `4k`→2160p, + * `1080i`→1080p, `720i`→720p; a `1920x1080` token maps by height. + */ +export function extractQuality(title: string): string | undefined { + const t = title.replace(/[._]/g, " "); + const tier = t.match(QUALITY_TIER); + if (tier) { + const tok = tier[1].toLowerCase(); + switch (tok) { + case "4k": + return "2160p"; + case "1080i": + return "1080p"; + case "720i": + return "720p"; + default: + return tok; + } + } + const wxh = t.match(QUALITY_WXH); + if (wxh) { + const h = parseInt(wxh[1], 10); + if (h >= 2000) return "2160p"; + if (h >= 1000) return "1080p"; + if (h >= 700) return "720p"; + if (h >= 500) return "576p"; + return "480p"; + } + return undefined; +} + +/** Codec regex → ffmpeg-style normalized token. */ +const CODEC_RE = /\b(x265|h\s?265|hevc|x264|h\s?264|avc|av1|xvid|divx|vp9)\b/i; + +/** + * Normalized video codec (`hevc`/`h264`/`av1`/`xvid`/`divx`/`vp9`) or + * `undefined`. `x265`/`h265`→`hevc`, `x264`/`avc`→`h264`. + */ +export function extractCodec(title: string): string | undefined { + const t = title.replace(/[._]/g, " "); + const m = t.match(CODEC_RE); + if (!m) return undefined; + const cleaned = m[1].toLowerCase().replace(/\s/g, ""); + switch (cleaned) { + case "x265": + case "h265": + case "hevc": + return "hevc"; + case "x264": + case "h264": + case "avc": + return "h264"; + default: + return cleaned; // av1, xvid, divx, vp9 + } +} + +/** `www.` noise, stripped while the dots are still intact. */ +const URL_STRIP = /\bwww\.\S+/gi; +/** + * Guard phrases removed before language matching because each contains a + * substring that would false-positive a tag (reproduces lookbehind guards + * without lookaround): `shang chi`→`chi`, `tel aviv`→`tel`, `web dl`/`webdl`→`dl`. + */ +const LANG_GUARD = /\b(?:shang\s+chi|tel\s+aviv|web\s+dl|webdl)\b/gi; + +/** + * Release-title language tags → ISO 639-2/B (alpha-3). Codes match meta-share's + * UI vocabulary (`fre`/`ger`/`chi`, not 639-3 `fra`/`deu`/`zho`). `mul` is the + * honest marker for `MULTi`/dual-audio. Every matching pattern contributes its + * code, so the result is a set (a `MULTi VOSTFR` release → `{fre, mul}`). + */ +const LANGUAGE_TAGS: ReadonlyArray = [ + [/\bmulti(?:lang)?\b|\bmultisubs?\b|\bdual\s?audio\b|\bdual\b|\bdl\b/i, "mul"], + [ + /\btruefrench\b|\bvostfr\b|\bsubfrench\b|\bfrench\b|\bvf[fqi2]?\b|\bstfr\b|\bfra\b|\bfre\b/i, + "fre", + ], + [/\benglish\b|\beng\s?sub\b|\beng\b/i, "eng"], + [/\bjapanese\b|\bjpn\b|\bjap\b/i, "jpn"], + [/\bitalian\b|\bita\b/i, "ita"], + [/\bgerman\b|\bger\b|\bdeu\b/i, "ger"], + [/\bspanish\b|\bcastellano\b|\bespa[nñ]ol\b|\blatino\b|\besp\b|\bspa\b/i, "spa"], + [/\bportuguese\b|\bdublado\b|\blegendado\b|\bpor\b/i, "por"], + [/\bkorean\b|\bkor\b/i, "kor"], + [/\bchinese\b|\bmandarin\b|\bcantonese\b|\bchs\b|\bcht\b|\bzho\b|\bchi\b/i, "chi"], + [/\brussian\b|\brus\b/i, "rus"], + [/\bdutch\b|\bflemish\b|\bnld\b|\bdut\b/i, "dut"], + [/\bpolish\b|\bpl\s?dub\b|\bdub\s?pl\b|\blektor\b|\bpol\b/i, "pol"], + [/\bczech\b|\bcze\b|\bces\b/i, "cze"], + [/\bslovak\b|\bslo\b|\bslk\b/i, "slo"], + [/\bhungarian\b|\bhundub\b|\bhun\b/i, "hun"], + [/\bromanian\b|\brosub(?:bed)?\b|\brodubbed\b|\brum\b|\bron\b/i, "rum"], + [/\bgreek\b|\bgre\b|\bell\b/i, "gre"], + [/\bukrainian\b|\bukr\b/i, "ukr"], + [/\bbulgarian\b|\bbgaudio\b|\bbul\b/i, "bul"], + [/\bswedish\b|\bswesub\b|\bswe\b/i, "swe"], + [/\bnorwegian\b|\bnorsk\b/i, "nor"], + [/\bdanish\b|\bdansk\b/i, "dan"], + [/\bfinnish\b|\bsuomi\b/i, "fin"], + [/\bturkish\b|\btur\b/i, "tur"], + [/\barabic\b|\bara\b/i, "ara"], + [/\bhindi\b|\bhin\b/i, "hin"], + [/\btamil\b|\btam\b/i, "tam"], + [/\btelugu\b|\btel\b/i, "tel"], + [/\bmalayalam\b|\bmal\b/i, "mal"], + [/\bvietnamese\b|\bvie\b/i, "vie"], + [/\bthai\b|\btha\b/i, "tha"], +]; + +/** + * The set of ISO 639-2/B language codes detected from a release title, sorted + * (deterministic). Empty when no tag matches. + */ +export function extractLanguages(title: string): string[] { + const stripped = title.replace(URL_STRIP, " "); + // Normalize `.`/`_`/`-` to spaces so `\b`-anchored tags + guards match + // regardless of the original separator. + const norm = stripped.replace(/[._-]/g, " "); + const guarded = norm.replace(LANG_GUARD, " "); + const out = new Set(); + for (const [re, code] of LANGUAGE_TAGS) { + if (re.test(guarded)) out.add(code); + } + return Array.from(out).sort(); +} diff --git a/src/tests/ReleaseTagExtractor.test.ts b/src/tests/ReleaseTagExtractor.test.ts new file mode 100644 index 0000000..c4134d5 --- /dev/null +++ b/src/tests/ReleaseTagExtractor.test.ts @@ -0,0 +1,56 @@ +import {describe, it, expect} from 'vitest'; +import {extractQuality, extractCodec, extractLanguages} from '../lib/tools/video/ReleaseTagExtractor.js'; + +describe('extractQuality', () => { + it('reads resolution tiers across separators', () => { + expect(extractQuality('Show.S01E01.1080p.WEB.h264')).toBe('1080p'); + expect(extractQuality('Movie_2008_720p')).toBe('720p'); + expect(extractQuality('Doc 2160p HDR')).toBe('2160p'); + }); + it('collapses 4k and interlaced to the progressive tier', () => { + expect(extractQuality('Film 4K')).toBe('2160p'); + expect(extractQuality('Broadcast 1080i')).toBe('1080p'); + expect(extractQuality('Old 720i')).toBe('720p'); + }); + it('maps a WxH token by height', () => { + expect(extractQuality('BDRip 1920x1080 x264')).toBe('1080p'); + expect(extractQuality('rip 1280x720')).toBe('720p'); + }); + it('returns undefined when there is no resolution', () => { + expect(extractQuality('Big Buck Bunny')).toBeUndefined(); + }); +}); + +describe('extractCodec', () => { + it('normalizes encoder spellings to ffmpeg tokens', () => { + expect(extractCodec('Show.x265.WEB')).toBe('hevc'); + expect(extractCodec('Show H.265')).toBe('hevc'); + expect(extractCodec('Movie x264')).toBe('h264'); + expect(extractCodec('Movie AVC')).toBe('h264'); + expect(extractCodec('Clip AV1')).toBe('av1'); + expect(extractCodec('Old XviD rip')).toBe('xvid'); + }); + it('returns undefined when no codec tag', () => { + expect(extractCodec('Big Buck Bunny 1080p')).toBeUndefined(); + }); +}); + +describe('extractLanguages', () => { + it('detects a single language tag', () => { + expect(extractLanguages('Movie.2008.FRENCH.1080p')).toEqual(['fre']); + expect(extractLanguages('Show.S01.German.WEB')).toEqual(['ger']); + }); + it('returns a sorted set for multi-tag releases', () => { + // MULTi VOSTFR → both mul and fre (sorted). + expect(extractLanguages('Movie.MULTi.VOSTFR.1080p')).toEqual(['fre', 'mul']); + }); + it('honors guard phrases (no false positives)', () => { + // "web dl" must not trigger the German `dl` → mul marker by itself. + expect(extractLanguages('Movie.2021.WEB-DL.1080p.x264')).toEqual([]); + // "Shang-Chi" must not trigger Chinese via `chi`. + expect(extractLanguages('Shang-Chi.2021.1080p')).toEqual([]); + }); + it('returns empty when no language tag', () => { + expect(extractLanguages('Big Buck Bunny 2008 1080p')).toEqual([]); + }); +});