Skip to content

Repository files navigation

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-media

The package is published as parsium-media. A scoped alias @nepiraw/parsium points 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 --progress

API

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 cache

createParser({ 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: true

TV 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: true

Multi-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: true

Date-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
  1. Normalize: strips noise (tracker prefixes, URLs), normalizes separators, decodes entities
  2. Structure and profile: bracket groups, dash positions and one shared structural picture, computed once
  3. Propose: 14 proposers emit claims, each carrying the evidence for its reading
  4. Select: overlapping claims are solved together. The better-evidenced reading wins, and the loser is recorded rather than thrown away
  5. Identity: title, altTitle and episodeTitle, by elimination
  6. 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:7002
curl -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.