detection improvement

This commit is contained in:
Pierre
2026-06-06 15:19:09 +02:00
parent e6a21cb36e
commit 7a67802405
5 changed files with 223 additions and 5 deletions
-5
View File
@@ -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"
+18
View File
@@ -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;
+1
View File
@@ -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';
+148
View File
@@ -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.<domain>` 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<readonly [RegExp, string]> = [
[/\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<string>();
for (const [re, code] of LANGUAGE_TAGS) {
if (re.test(guarded)) out.add(code);
}
return Array.from(out).sort();
}
+56
View File
@@ -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([]);
});
});