@nepiraw/parsium
v1.1.0
Published
The most accurate torrent/video filename parser
Maintainers
Readme
Parsium
A highly accurate torrent and video filename parser.
Built and measured against real-world torrents. Handles movies, TV series, anime, multi-language releases and messy international filenames, with a very high title accuracy.
Zero dependencies. Around 0.15ms per parse. Works everywhere JavaScript runs.
Try it live in your browser with our interactive demo and playground.
Highlights
- Very high title accuracy: validated against 10K real production filenames
- Auto-detects content type: movies, series, anime, no hints needed
- 80+ languages: VFF, VOSTFR, MULTi, Latino, DL, DualAudio, and more
- Anime-first-class: fansub conventions, absolute episodes, batch ranges
- Position-independent: handles tracker prefixes, noise, mixed separators
- Explainable: every field reports the evidence it was decided on, and what lost
- Plugin system: add custom proposers without modifying core
- Zero dependencies: pure TypeScript, ESM + CJS, about 57KB gzipped
Install
npm install parsium-media
# or
pnpm add parsium-media
# or
yarn add parsium-mediaThe package is published as
parsium-media. A scoped alias@nepiraw/parsiumpoints at the identical build if you prefer it:npm install @nepiraw/parsium.
Quick Start
import { parse } from 'parsium-media';
const result = parse("The.Boys.S05E02.REPACK.1080p.WEB.H264-TyHD.mkv");
// {
// title: "The Boys",
// contentType: "series",
// seasons: [5],
// episodes: [2],
// resolution: "1080p",
// source: "WEB-DL",
// codec: "H.264",
// releaseGroup: "TyHD",
// isRepack: true,
// ...
// }CLI
# Parse a single filename
npx parsium-media "Movie.2024.2160p.UHD.BluRay.x265-GROUP"
# JSON output (only present fields)
npx parsium-media "filename.mkv" --json
# Full JSON (all fields including false/empty)
npx parsium-media "filename.mkv" --json-full
# Show the step-by-step trace
npx parsium-media "filename.mkv" --explain
# Show what each field was decided on, and what lost
npx parsium-media "filename.mkv" --why
# Add every candidate considered, including the rejected ones
npx parsium-media "filename.mkv" --why full
# Stack the extraction trace onto the same section
npx parsium-media "filename.mkv" --why --explain
# Batch mode from file (one filename per line)
npx parsium-media --file torrents.txt --json --progressAPI
parse(filename, options?)
Parse a single filename into structured metadata.
import { parse } from 'parsium-media';
const result = parse(
"Superman.2025.MULTi.2160p.UHD.BluRay.REMUX.DV.HDR10.HEVC.TrueHD.Atmos.7.1-FRATERNiTY.mkv"
);
result.title // "Superman"
result.year // 2025
result.contentType // "movie"
result.resolution // "2160p"
result.source // "UHD BluRay"
result.codec // "HEVC"
result.hdr // ["DV", "HDR10"]
result.audio // ["TrueHD", "Atmos"]
result.channels // ["7.1"]
result.isMultiLanguage // true
result.isRemux // true
result.releaseGroup // "FRATERNiTY"Options:
| Option | Type | Default | Description |
|--------|------|---------|-------------|
| explain | boolean \| 'summary' \| 'full' | false | Explain the decision. true means 'summary'; 'full' adds every candidate considered. Also includes the step-by-step explanation trace |
// Why did it decide that?
const result = parse("The.Sopranos.S01E02-46.Long.BDRemux.mkv", { explain: 'summary' });
// `explain` is only present when you asked for it, so narrow before reading it.
result.explain?.fields.episodes // { value: [2], ranges: [[13, 19]], why: [{ signal: "vocab-exact", weight: 10 }], rejected: [] }
result.explain?.runnerUp // { seasons: [1], episodeRange: { from: 2, to: 46 } }
// 'full' adds every claim and every rejection the parse ever considered
parse("The.Sopranos.S01E02-46.Long.BDRemux.mkv", { explain: 'full' }).explain;
// The step-by-step trace is still there
parse("Movie.2024.1080p.mkv", { explain: true }).explanation;
// [{ proposer: "resolution", value: "1080p", range: [11, 16], reason: "..." }, ...]parseBatch(filenames, options?)
Parse multiple filenames with optional progress reporting.
import { parseBatch } from 'parsium-media';
const filenames = ['Movie.2024.2160p.BluRay.x265-GROUP.mkv', '[SubsPlease] Frieren - 28 [1080p].mkv'];
const results = parseBatch(filenames, {
onProgress: (done, total) => console.log(`${done}/${total}`),
explain: true,
});createCachedParser(maxSize?)
Create a parser with LRU cache for repeated parsing of the same filenames.
import { createCachedParser } from 'parsium-media';
const parser = createCachedParser(10000); // max 10K entries
parser.parse("filename.mkv"); // parsed
parser.parse("filename.mkv"); // instant cache hit
parser.size; // current cache size
parser.clear(); // clear cachecreateParser({ plugins })
Create a custom parser whose plugins add proposers, arbitrated against the built-ins by the same evidence rules.
import { createParser } from 'parsium-media';
import type { ParsiumPlugin } from 'parsium-media';
const myPlugin: ParsiumPlugin = {
name: 'my-plugin',
// A proposer emits CLAIMS. `priority` is optional: omit it to run after every built-in.
proposers: [{
name: 'my-proposer',
propose(ctx) {
void ctx;
return { claims: [], rejections: [] };
},
}],
};
const parser = createParser({ plugins: [myPlugin] });
const result = parser.parse("filename.mkv");See PLUGINS.md for the full plugin API.
Extracted Fields
Identity
| Field | Type | Description |
|-------|------|-------------|
| title | string | Extracted title (always present) |
| altTitle | string | Alternative title (e.g., original language title) |
| year | number | Release year |
| contentType | 'movie' \| 'series' \| 'unknown' | Auto-detected content type |
| contentSubtype | 'anime' | Subtype when anime signals are detected |
Episodes
| Field | Type | Description |
|-------|------|-------------|
| seasons | number[] | Season numbers (e.g., [5] or [1,2,3] for packs) |
| episodes | number[] | Episode numbers (e.g., [2] or [1,2] for multi-ep) |
| episodeRange | { from, to } | Episode range (e.g., E01-E12) |
| absoluteEpisode | number | Anime absolute episode number |
| absoluteEpisodeRange | { from, to } | Anime batch range (e.g., 01~25) |
| episodeTitle | string | Episode title if present |
| date | string | Date-based episode (YYYY-MM-DD format) |
| isSeasonPack | boolean | Full season pack |
| isCompleteSeries | boolean | Complete series release |
| isBatchRelease | boolean | Batch release (anime) |
Video Quality
| Field | Type | Description |
|-------|------|-------------|
| resolution | string | 2160p, 1080p, 720p, 480p, etc. |
| source | string | BluRay, UHD BluRay, WEB-DL, WEBRip, HDTV, etc. |
| codec | string | x264, x265, H.264, H.265, HEVC, AV1, etc. |
| bitDepth | string | 10-bit, 8-bit, 12-bit |
| frameRate | string | 24fps, 60fps, etc. |
| hdr | string[] | Atomic, ordered: HDR10, DV, HDR10+, HLG, SDR (e.g. ["DV","HDR10"]) |
Audio
| Field | Type | Description |
|-------|------|-------------|
| audio | string[] | Atomic codecs/layers, e.g. ["DD+","Atmos"], ["TrueHD","Atmos"], ["DTS-HD MA"], ["AAC"] |
| channels | string[] | All channel layouts found, e.g. ["7.1"], ["7.1","5.1"], ["2.0"] |
Language
| Field | Type | Description |
|-------|------|-------------|
| languages | Language[] | Detected languages with codes and labels |
| isMultiLanguage | boolean | MULTi / multi-language release |
| isDualAudio | boolean | Dual audio (DualAudio, DUAL, DL/ML markers) |
| subtitleLanguages | Language[] | Detected subtitle languages |
| isMultiSubtitle | boolean | Multiple subtitle tracks (Multi Subs, MSub, M-SUB) |
Release Info
| Field | Type | Description |
|-------|------|-------------|
| releaseGroup | string | Release group name |
| editions | string[] | Edition labels (Director's Cut, Extended, Remastered...) |
| streamingService | string | Service (Netflix, Amazon, Disney+, etc.) |
| isRemux | boolean | REMUX release |
| isRepack | boolean | REPACK release |
| isProper | boolean | PROPER release |
| is3D | boolean | 3D release |
| isHybrid | boolean | Hybrid release |
| isUpscaled | boolean | AI upscale detected |
File Info
| Field | Type | Description |
|-------|------|-------------|
| container | string | File container (mkv, mp4, avi) |
| raw | string | Original input string |
| warnings | string[] | Conditions worth knowing about, as code:detail. See docs/FIELDS.md |
Examples
Movies
parse("Inception.2010.2160p.UHD.BluRay.REMUX.HDR.HEVC.Atmos-GRAIL.mkv");
// title: "Inception", year: 2010, resolution: "2160p", source: "UHD BluRay",
// isRemux: true, hdr: ["HDR"], codec: "HEVC", audio: ["Atmos"]
parse("Le.Comte.de.Monte-Cristo.2024.MULTi.1080p.BluRay.x264-FRATERNiTY");
// title: "Le Comte de Monte-Cristo", year: 2024, isMultiLanguage: trueTV Series
parse("The.Boys.S05E02.REPACK.1080p.WEB.H264-TyHD.mkv");
// title: "The Boys", seasons: [5], episodes: [2], isRepack: true
parse("Malcolm.In.The.Middle.S02E19.1080p.AMZN.WEB-DL.DD+5.1.H.264-ViSiON");
// title: "Malcolm In The Middle", streamingService: "Amazon Prime Video",
// audio: ["DD+"], channels: ["5.1"]Anime
parse("[SubsPlease] Jujutsu Kaisen 2nd Season - 08 (1080p) [ABC12345].mkv");
// title: "Jujutsu Kaisen", contentType: "series", contentSubtype: "anime",
// seasons: [2], episodes: [8], releaseGroup: "SubsPlease"
parse("[Erai-raws] Oshi no Ko - 01~11 [1080p][Multiple Subtitle]");
// title: "Oshi no Ko", contentType: "series", contentSubtype: "anime",
// absoluteEpisodeRange: { from: 1, to: 11 }, isBatchRelease: trueMulti-Language
parse("Film.2024.MULTi.VFF.1080p.WEB-DL.H265-GroupName");
// title: "Film", isMultiLanguage: true, languages: [{ label: "French" }]
parse("Movie.2024.DUAL.1080p.BluRay.x264-GROUP");
// title: "Movie", isDualAudio: trueDate-Based Episodes
parse("Last.Week.Tonight.2025.01.15.1080p.WEB.H264-GROUPNAME");
// title: "Last Week Tonight", date: "2025-01-15", contentType: "series"Architecture
Parsium uses a 5-stage pipeline:
Input → Normalize → Structure → Profile → Propose → Select → Identity → Classify → Repair → Result- Normalize: strips noise (tracker prefixes, URLs), normalizes separators, decodes entities
- Structure and profile: bracket groups, dash positions and one shared structural picture, computed once
- Propose: 14 proposers emit claims, each carrying the evidence for its reading
- Select: overlapping claims are solved together. The better-evidenced reading wins, and the loser is recorded rather than thrown away
- Identity: title, altTitle and episodeTitle, by elimination
- Classify the content type, then apply conservative repairs
A proposer does not consume text, it proposes. A claim commits when its support outweighs the evidence that its span is ordinary text, so a losing reading leaves its text available instead of eating it. The title is what no other field claimed, which is what lets Parsium handle a filename that does not start with its title.
A central field registry (src/field-registry.ts) defines all output fields in one place: their types, categories, display behavior, JSON serialization mode, and default values. The CLI, pipeline assembly, and comparison tools are all driven from this registry, so adding a new field requires only a single entry.
Proposer priority order
Priority is a prior, not ownership: resolution claiming 1080p before season-episode runs is what stops s01e02-1080p being read as a 1080-episode range.
| Priority | Proposer | What it detects | |----------|----------|-----------------| | 1 | Resolution | 2160p, 1080p, 720p, 4K | | 2 | Source | BluRay, WEB-DL, HDTV | | 3 | Codec / Date / FPS | x265, H.264 / 2025.01.15 / 24fps | | 4 | Audio | TrueHD, DTS-HD MA, DD+ | | 5 | HDR | DV, HDR10, HDR10+ | | 6 | Language | 80+ langs, scene markers | | 7 | Year | Position-aware scoring | | 8 | Edition | Director's Cut, Extended | | 9 | Misc | REPACK, PROPER, streaming services | | 10 | Season/Episode | S01E02, packs, ranges | | 11 | Anime Episode | Absolute eps, batches | | 12 | Release Group | Trailing dash, fansub brackets |
There is no title proposer. Identity is a pipeline stage that runs after selection, over whatever text no claim owns.
Requirements
- Node.js 22+ (or Bun/Deno)
- No native dependencies
HTTP API
Parsium also provides an HTTP API for remote parsing. See the API README for endpoints, deployment, and configuration.
Live API demo: https://api.parsium.nepiraw.com
cd api && pnpm install && pnpm dev
# → Parsium API listening on http://localhost:7002curl -X POST http://localhost:7002/v1/parse \
-H "Content-Type: application/json" \
-d '{"filename": "Movie.2024.1080p.BluRay.x264-GROUP.mkv"}'Endpoints: POST /v1/parse, POST /v1/parse/batch, GET /v1/health, GET /v1/version, GET /v1/openapi.json
Options: full, explain (passed via options object in request body)
Deployment: Docker, Vercel, or any Node.js host.
