Skip to content

Commit 0f41c71

Browse files
Initial commit
0 parents  commit 0f41c71

19 files changed

+671
-0
lines changed

.gitignore

+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
.idea
2+
node_modules

README.md

+86
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
# parse-torrent-title
2+
3+
This package helps you extract information from a torrent name such as language, resolution and codec.
4+
5+
## Installation
6+
7+
You can install it using npm:
8+
```bash
9+
npm install parse-torrent-title
10+
```
11+
You should use Node 4.0 or higher to use this package.
12+
13+
## Usage
14+
15+
A simple usage is as follow:
16+
```javascript
17+
const ptt = require("parse-torrent-title");
18+
const information = ptt.parse("Game.of.Thrones.S01E01.720p.HDTV.x264-CTU");
19+
20+
console.log(information.title); // Game of Thrones
21+
console.log(information.season); // 1
22+
console.log(information.episode); // 1
23+
console.log(information.resolution); // 720p
24+
console.log(information.codec); // x264
25+
console.log(information.source); // HDTV
26+
console.log(information.group); // CTU
27+
```
28+
29+
## Advanced usage
30+
31+
This module is configurable and extendable using handlers and regular expressions.
32+
33+
### Regular expressions
34+
35+
If you want an extra field to be populated, you can use a regular expression as follow:
36+
37+
```javascript
38+
const ptt = require("parse-torrent-title");
39+
ptt.addHandler("part", /Part[. ]([0-9])/i, { type: "integer" });
40+
const information = ptt.parse("Silent.Witness.S18E03.Falling.Angels.Part.1.720p.HDTV.x264-FTP");
41+
42+
console.log(information.part); // 1
43+
```
44+
45+
If you want to keep only a part of the matched regular expression, you should use capturing groups
46+
[explained here](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp).
47+
48+
### Handlers
49+
50+
A handler is a function with the title and the resulting information as parameters.
51+
It can modify the result in any wanted way.
52+
If the matched string is not part of the title, it should return the beginning of it.
53+
54+
```javascript
55+
const ptt = require("parse-torrent-title");
56+
const information = ptt.parse("[REQ] Harry Potter And The Prisoner Of Azkaban 2004 1080p BluRay DTS x264-hV");
57+
console.log(information.isHarryPotterRelated); // undefined
58+
59+
ptt.addHandler(({ title, result }) => {
60+
const match = title.match(/harry.potter/i);
61+
if (match) {
62+
result.isHarryPotterRelated = true;
63+
}
64+
});
65+
66+
const information2 = ptt.parse("[REQ] Harry Potter And The Prisoner Of Azkaban 2004 1080p BluRay DTS x264-hV");
67+
console.log(information2.isHarryPotterRelated); // true
68+
```
69+
70+
## Multiple parsers
71+
72+
You may want several different parsers within the same project.
73+
To do that, you can create new parsers simply:
74+
```javascript
75+
const Parser = require("parse-torrent-title").Parser;
76+
const parser = new Parser();
77+
const anotherParser = new Parser();
78+
```
79+
80+
If you want to add default handlers to a parser, you can do so using the specific method:
81+
```javascript
82+
const ptt = require("parse-torrent-title");
83+
const parser = new ptt.Parser();
84+
ptt.addDefaults(parser); // parser is now ready
85+
```
86+

index.js

+11
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
const Parser = require("./src/parser").Parser;
2+
const handlers = require("./src/handlers");
3+
4+
const defaultParser = new Parser();
5+
6+
handlers.addDefaults(defaultParser);
7+
8+
exports.addDefaults = handlers.addDefaults;
9+
exports.addHandler = (handlerName, handler, options) => defaultParser.addHandler(handlerName, handler, options);
10+
exports.parse = (title) => defaultParser.parse(title);
11+
exports.Parser = Parser;

package.json

+24
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
{
2+
"name": "parse-torrent-title",
3+
"version": "0.1.0",
4+
"description": "Parse torrent title to extract useful information",
5+
"main": "index.js",
6+
"scripts": {
7+
"lint": "eslint",
8+
"test": "mocha"
9+
},
10+
"repository": {
11+
"type": "git",
12+
"url": "git+ssh://git@github.com/clement-escolano/parse-torrent-title.git"
13+
},
14+
"homepage": "https://github.com/clement-escolano/parse-torrent-title#readme",
15+
"author": "Clément Escolano <clement.escolano@icloud.com>",
16+
"license": "MIT",
17+
"engines": {
18+
"node": ">=4"
19+
},
20+
"devDependencies": {
21+
"chai": "^3.5.0",
22+
"mocha": "^3.3.0"
23+
}
24+
}

src/handlers.js

+23
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
exports.addDefaults = (parser) => {
2+
// Year
3+
parser.addHandler("year", /(?!^)[(\[]?((?:19[0-9]|20[012])[0-9])[)\]]?/, { type: "integer" });
4+
// Resolution
5+
parser.addHandler("resolution", /([0-9]{3,4}[pi])/i, { type: "lowercase" });
6+
parser.addHandler("resolution", /(4k)/i, { type: "lowercase" });
7+
// Extended
8+
parser.addHandler("extended", /EXTENDED/, { type: "boolean" });
9+
// Convert
10+
parser.addHandler("convert", /CONVERT/, { type: "boolean" });
11+
// Hardcoded
12+
parser.addHandler("hardcoded", /HC|HARDCODED/, { type: "boolean" });
13+
// Proper
14+
parser.addHandler("proper", /(?:REAL.)?PROPER/, { type: "boolean" });
15+
// Repack
16+
parser.addHandler("repack", /REPACK/, { type: "boolean" });
17+
// Region
18+
parser.addHandler("region", /R[0-9]/);
19+
// Container
20+
parser.addHandler("container", /MKV|AVI|MP4/i, { type: "lowercase" });
21+
// Source
22+
parser.addHandler("source", /hdtv|bluray|(?:b[dr]|dvd|hd|tv)rip|web-?(?:dl|rip)|dvd|ppv/i, { type: "lowercase" });
23+
};

src/parser.js

+96
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
function extendOptions(options) {
2+
options = options || {};
3+
4+
const defaultOptions = {
5+
skipIfAlreadyFound: true,
6+
type: "string",
7+
};
8+
9+
options.skipIfAlreadyFound = options.skipIfAlreadyFound || defaultOptions.skipIfAlreadyFound;
10+
options.multipleParts = options.multipleParts || defaultOptions.multipleParts;
11+
12+
return options;
13+
}
14+
15+
function createHandlerFromRegExp(name, regExp, options) {
16+
let transformer;
17+
18+
if (!options.type) {
19+
transformer = input => input;
20+
} else if (options.type.toLowerCase() === "lowercase") {
21+
transformer = input => input.toLowerCase();
22+
} else if (options.type.toLowerCase().substr(0, 4) === "bool") {
23+
transformer = input => true;
24+
} else if (options.type.toLowerCase().substr(0, 3) === "int") {
25+
transformer = input => parseInt(input, 10);
26+
} else {
27+
transformer = input => input;
28+
}
29+
30+
return ({ title, result }) => {
31+
if (result[name] && options.skipIfAlreadyFound) {
32+
return;
33+
}
34+
35+
const match = title.match(regExp);
36+
const [rawMatch, cleanMatch] = match || [];
37+
38+
if (rawMatch) {
39+
result[name] = transformer(cleanMatch || rawMatch);
40+
return match.index;
41+
}
42+
}
43+
}
44+
45+
function cleanTitle(rawTitle) {
46+
if (rawTitle.indexOf(" ") === -1 && rawTitle.indexOf(".") !== -1) {
47+
rawTitle = rawTitle.replace(/\./g, " ");
48+
}
49+
50+
rawTitle = rawTitle.replace(/_/g, " ");
51+
rawTitle = rawTitle.replace(/([(_]|- )$/, "").trim();
52+
53+
return rawTitle;
54+
}
55+
56+
class Parser {
57+
58+
constructor() {
59+
this.handlers = [];
60+
}
61+
62+
addHandler(handlerName, handler, options) {
63+
if (typeof handler === "undefined" && typeof handlerName === "function") {
64+
// If no name is provided and a function handler is directly given
65+
handler = handlerName;
66+
handlerName = "Unknown handler";
67+
} else if (typeof handlerName === "string" && handler instanceof RegExp) {
68+
options = extendOptions(options);
69+
// If the handler provided is a regular expression
70+
handler = createHandlerFromRegExp(handlerName, handler, options);
71+
} else if (typeof handler !== "function") {
72+
// If the handler is neither a function or a regular expression, throw an error
73+
throw new Error(`Handler for ${handlerName} should be a RegExp or a function. Got: ${typeof handler}`);
74+
}
75+
76+
this.handlers.push(handler);
77+
}
78+
79+
parse(title) {
80+
const result = {};
81+
let endOfTitle = title.length;
82+
83+
for (const handler of this.handlers) {
84+
const matchIndex = handler({ title: title, result });
85+
if (matchIndex && matchIndex < endOfTitle) {
86+
endOfTitle = matchIndex;
87+
}
88+
}
89+
90+
result.title = cleanTitle(title.substr(0, endOfTitle));
91+
92+
return result;
93+
}
94+
}
95+
96+
exports.Parser = Parser;

test/container.js

+19
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
const { expect } = require("chai");
2+
const parse = require("../index").parse;
3+
4+
describe("Parsing container", () => {
5+
it("should detect if the mkv release has the correct container", () => {
6+
const releaseName = "Kevin Hart What Now (2016) 1080p BluRay x265 6ch -Dtech mkv";
7+
expect(parse(releaseName)).to.deep.include({ container: "mkv" });
8+
});
9+
10+
it("should detect if the mp4 release has the correct container", () => {
11+
const releaseName = "The Gorburger Show S01E05 AAC MP4-Mobile";
12+
expect(parse(releaseName)).to.deep.include({ container: "mp4" });
13+
});
14+
15+
it("should detect if the avi release has the correct container", () => {
16+
const releaseName = "[req]Night of the Lepus (1972) DVDRip XviD avi";
17+
expect(parse(releaseName)).to.deep.include({ container: "avi" });
18+
});
19+
});

test/convert.js

+14
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
const { expect } = require("chai");
2+
const parse = require("../index").parse;
3+
4+
describe("Parsing convert", () => {
5+
it("should detect if the release is convert", () => {
6+
const releaseName = "Better.Call.Saul.S03E04.CONVERT.720p.WEB.h264-TBS";
7+
expect(parse(releaseName)).to.deep.include({ convert: true });
8+
});
9+
10+
it("should not detect convert when the release is not flagged as such", () => {
11+
const releaseName = "Have I Got News For You S53E02 EXTENDED 720p HDTV x264-QPEL";
12+
expect(parse(releaseName)).to.not.have.property("convert");
13+
});
14+
});

test/extended.js

+14
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
const { expect } = require("chai");
2+
const parse = require("../index").parse;
3+
4+
describe("Parsing extended", () => {
5+
it("should detect if the release is extended", () => {
6+
const releaseName = "Have I Got News For You S53E02 EXTENDED 720p HDTV x264-QPEL";
7+
expect(parse(releaseName)).to.deep.include({ extended: true });
8+
});
9+
10+
it("should not detect extended when the release is not flagged as such", () => {
11+
const releaseName = "Better.Call.Saul.S03E04.CONVERT.720p.WEB.h264-TBS";
12+
expect(parse(releaseName)).to.not.have.property("extended");
13+
});
14+
});

test/hardcoded.js

+14
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
const { expect } = require("chai");
2+
const parse = require("../index").parse;
3+
4+
describe("Parsing hardcoded", () => {
5+
it("should detect if the release is hardcoded", () => {
6+
const releaseName = "Ghost In The Shell 2017 1080p HC HDRip X264 AC3-EVO";
7+
expect(parse(releaseName)).to.deep.include({ hardcoded: true });
8+
});
9+
10+
it("should not detect hardcoded when the release is not flagged as such", () => {
11+
const releaseName = "Have I Got News For You S53E02 EXTENDED 720p HDTV x264-QPEL";
12+
expect(parse(releaseName)).to.not.have.property("hardcoded");
13+
});
14+
});

test/main.js

+9
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
const { expect } = require("chai");
2+
const parse = require("../index").parse;
3+
4+
xdescribe("Real-world releases", () => {
5+
it("sons.of.anarchy.s05e10.480p.BluRay.x264-GAnGSteR", () => {
6+
const releaseName = "sons.of.anarchy.s05e10.480p.BluRay.x264-GAnGSteR";
7+
expect(parse(releaseName)).to.deep.equal({ title: "sons of anarchy", resolution: "480p" });
8+
});
9+
});

test/proper.js

+19
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
const { expect } = require("chai");
2+
const parse = require("../index").parse;
3+
4+
describe("Parsing proper", () => {
5+
it("should detect if the release is proper", () => {
6+
const releaseName = "Into the Badlands S02E07 PROPER 720p HDTV x264-W4F";
7+
expect(parse(releaseName)).to.deep.include({ proper: true });
8+
});
9+
10+
it("should detect if the release is real proper", () => {
11+
const releaseName = "Bossi-Reality-REAL PROPER-CDM-FLAC-1999-MAHOU";
12+
expect(parse(releaseName)).to.deep.include({ proper: true });
13+
});
14+
15+
it("should not detect proper when the release is not flagged as such", () => {
16+
const releaseName = "Have I Got News For You S53E02 EXTENDED 720p HDTV x264-QPEL";
17+
expect(parse(releaseName)).to.not.have.property("proper");
18+
});
19+
});

test/region.js

+10
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
const { expect } = require("chai");
2+
const parse = require("../index").parse;
3+
4+
describe("Parsing region", () => {
5+
it("should detect the R5 region correctly", () => {
6+
const releaseName = "Welcome to New York 2014 R5 XviD AC3-SUPERFAST";
7+
expect(parse(releaseName)).to.deep.include({ region: "R5" });
8+
});
9+
});
10+

test/repack.js

+14
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
const { expect } = require("chai");
2+
const parse = require("../index").parse;
3+
4+
describe("Parsing repack", () => {
5+
it("should detect if the release is repack", () => {
6+
const releaseName = "Silicon Valley S04E03 REPACK HDTV x264-SVA";
7+
expect(parse(releaseName)).to.deep.include({ repack: true });
8+
});
9+
10+
it("should not detect repack when the release is not flagged as such", () => {
11+
const releaseName = "Have I Got News For You S53E02 EXTENDED 720p HDTV x264-QPEL";
12+
expect(parse(releaseName)).to.not.have.property("repack");
13+
});
14+
});

test/resolution.js

+25
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
const { expect } = require("chai");
2+
const parse = require("../index").parse;
3+
4+
describe("Parsing resolution", () => {
5+
it("should detect the 1080p resolution correctly", () => {
6+
const releaseName = "Annabelle.2014.1080p.PROPER.HC.WEBRip.x264.AAC.2.0-RARBG";
7+
expect(parse(releaseName)).to.deep.include({ resolution: "1080p" });
8+
});
9+
10+
it("should detect the 720p resolution correctly", () => {
11+
const releaseName = "doctor_who_2005.8x12.death_in_heaven.720p_hdtv_x264-fov";
12+
expect(parse(releaseName)).to.deep.include({ resolution: "720p" });
13+
});
14+
15+
it("should detect the 720p resolution with uppercase correctly", () => {
16+
const releaseName = "UFC 187 PPV 720P HDTV X264-KYR";
17+
expect(parse(releaseName)).to.deep.include({ resolution: "720p" });
18+
});
19+
20+
it("should detect the 4k resolution correctly", () => {
21+
const releaseName = "The Smurfs 2 2013 COMPLETE FULL BLURAY UHD (4K) - IPT EXCLUSIVE";
22+
expect(parse(releaseName)).to.deep.include({ resolution: "4k" });
23+
});
24+
});
25+

0 commit comments

Comments
 (0)