@thebeastlt/parse-torrent-title 0.0.0-stage → 2.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +313 -2
- package/index.d.ts +184 -0
- package/index.js +148 -0
- package/native.js +839 -0
- package/package.json +62 -4
package/README.md
CHANGED
|
@@ -1,3 +1,314 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @thebeastlt/parse-torrent-title
|
|
2
2
|
|
|
3
|
-
This
|
|
3
|
+
This package extracts information from a torrent or file name: title, year, season and episode numbers,
|
|
4
|
+
resolution, source, codec, audio, HDR, languages, release group and more.
|
|
5
|
+
|
|
6
|
+
It is written in [Rust](https://github.com/TheBeastLT/parse-torrent-title) and runs as a native addon, with prebuilt
|
|
7
|
+
binaries for Linux (glibc and musl) and macOS on x64 and arm64, and for Windows on x64.
|
|
8
|
+
|
|
9
|
+
## Installation
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
npm install @thebeastlt/parse-torrent-title
|
|
13
|
+
```
|
|
14
|
+
Node 16 or higher is required.
|
|
15
|
+
|
|
16
|
+
## Usage
|
|
17
|
+
|
|
18
|
+
```javascript
|
|
19
|
+
const ptt = require("@thebeastlt/parse-torrent-title");
|
|
20
|
+
const information = ptt.parse("Game.of.Thrones.S01E01.720p.HDTV.x264-CTU");
|
|
21
|
+
|
|
22
|
+
console.log(information.title); // Game of Thrones
|
|
23
|
+
console.log(information.seasons); // [1]
|
|
24
|
+
console.log(information.season); // 1
|
|
25
|
+
console.log(information.episodes); // [1]
|
|
26
|
+
console.log(information.episode); // 1
|
|
27
|
+
console.log(information.resolution); // 720p
|
|
28
|
+
console.log(information.source); // HDTV
|
|
29
|
+
console.log(information.codec); // x264
|
|
30
|
+
console.log(information.group); // CTU
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
A few more examples:
|
|
34
|
+
|
|
35
|
+
```javascript
|
|
36
|
+
ptt.parse("The.Mandalorian.S02E05.2160p.DSNP.WEB-DL.DDP5.1.Atmos.DV.HDR10.HEVC-FLUX.mkv");
|
|
37
|
+
// {
|
|
38
|
+
// title: "The Mandalorian", seasons: [2], season: 2, episodes: [5], episode: 5,
|
|
39
|
+
// resolution: "4k", source: "WEB-DL", hdr: ["DV", "HDR"], bitDepth: "10bit", codec: "hevc",
|
|
40
|
+
// audio: "atmos", group: "FLUX", container: "mkv", extension: "mkv"
|
|
41
|
+
// }
|
|
42
|
+
|
|
43
|
+
ptt.parse("[SubsPlease] Jujutsu Kaisen - 12.5 (1080p) [8A3F2C1D].mkv");
|
|
44
|
+
// {
|
|
45
|
+
// title: "Jujutsu Kaisen", episodes: [12], episode: 12, halfEpisode: true, resolution: "1080p",
|
|
46
|
+
// episodeCode: "8A3F2C1D", group: "SubsPlease", container: "mkv", extension: "mkv"
|
|
47
|
+
// }
|
|
48
|
+
|
|
49
|
+
ptt.parse("Interstellar (2014) [1080p] [Dual Audio] [Hindi + English] [x264]");
|
|
50
|
+
// {
|
|
51
|
+
// title: "Interstellar", year: 2014, resolution: "1080p", codec: "x264",
|
|
52
|
+
// languages: ["dual audio", "english", "hindi"], dubbed: true
|
|
53
|
+
// }
|
|
54
|
+
|
|
55
|
+
ptt.parse("The Daily Show 2024.03.12 720p WEB h264-BAE");
|
|
56
|
+
// { title: "The Daily Show", date: "2024-03-12", resolution: "720p", source: "WEB", codec: "h264", group: "BAE" }
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
### Parsed fields
|
|
60
|
+
|
|
61
|
+
Only fields that were found are present in the result, except for `title`, which is always set.
|
|
62
|
+
|
|
63
|
+
| Field | Type | Examples |
|
|
64
|
+
|-----------------|------------|-------------------------------------------------------------------------------------------------|
|
|
65
|
+
| `title` | `string` | `"Game of Thrones"` |
|
|
66
|
+
| `year` | `number` or `string` | `2014`, or `"2010-2015"` for a year range |
|
|
67
|
+
| `date` | `string` | `"2024-03-12"` (always formatted as `YYYY-MM-DD`) |
|
|
68
|
+
| `seasons` | `number[]` | `[1]`, `[1, 2, 3, 4, 5]` |
|
|
69
|
+
| `season` | `number` | set only when `seasons` holds a single season |
|
|
70
|
+
| `episodes` | `number[]` | `[5]`, `[1, 2, 3]` |
|
|
71
|
+
| `episode` | `number` | set only when `episodes` holds a single episode |
|
|
72
|
+
| `halfEpisode` | `boolean` | `true` for `12.5`-style recap/special episodes (`episode` is then `12`) |
|
|
73
|
+
| `episodeCode` | `string` | `"8A3F2C1D"` (anime CRC32 hash) |
|
|
74
|
+
| `volumes` | `number[]` | `[1, 2, 3]` |
|
|
75
|
+
| `complete` | `boolean` | complete series, collections, box sets, trilogies, batches |
|
|
76
|
+
| `extras` | `boolean` | packs that also bundle OVAs, specials, movies, OSTs and similar |
|
|
77
|
+
| `resolution` | `string` | `"4k"`, `"1080p"`, `"720p"`, `"576p"`, `"480p"` |
|
|
78
|
+
| `source` | `string` | `"BluRay REMUX"`, `"BluRay"`, `"BDRip"`, `"BRRip"`, `"UHDRip"`, `"WEB-DL"`, `"WEBRip"`, `"WEB"`, `"HDTV"`, `"HDRip"`, `"DVDRip"`, `"DVD"`, `"TVRip"`, `"SATRip"`, `"PPVRip"`, `"SCR"`, `"R5"`, `"TeleCine"`, `"TeleSync"`, `"CAM"` |
|
|
79
|
+
| `codec` | `string` | `"x264"`, `"x265"`, `"h264"`, `"h265"`, `"hevc"`, `"avc"`, `"av1"`, `"xvid"`, `"divx"`, `"mpeg2"` |
|
|
80
|
+
| `bitDepth` | `string` | `"8bit"`, `"10bit"`, `"12bit"` |
|
|
81
|
+
| `hdr` | `string[]` | `["DV"]`, `["HDR10+"]`, `["DV", "HDR"]` |
|
|
82
|
+
| `threeD` | `string` | `"3D"`, `"3D SBS"`, `"3D HSBS"`, `"3D OU"`, `"3D HOU"` |
|
|
83
|
+
| `audio` | `string` | `"aac"`, `"ac3"`, `"eac3"`, `"dd5.1"`, `"dts"`, `"dts-hd"`, `"truehd"`, `"atmos"`, `"7.1 Atmos"`, `"flac"`, `"mp3"`, `"2.0"` |
|
|
84
|
+
| `audioChannels` | `string` | `"2.0"`, `"5.1"`, `"7.1"` |
|
|
85
|
+
| `languages` | `string[]` | `["english", "hindi"]`, `["multi audio"]`, `["dual audio"]`, `["multi subs"]`, `["latino"]` |
|
|
86
|
+
| `dubbed` | `boolean` | `true` when dubbed, or when `languages` contains `multi audio` or `dual audio` |
|
|
87
|
+
| `group` | `string` | `"FLUX"`, `"SubsPlease"` |
|
|
88
|
+
| `site` | `string` | `"www.Torrenting.com"` from a `www.Site.com - ` prefix |
|
|
89
|
+
| `container` | `string` | `"mkv"`, `"avi"`, `"mp4"`, `"wmv"`, `"mpg"`, `"mpeg"` |
|
|
90
|
+
| `extension` | `string` | `"mkv"`, `"mp4"`, `"srt"`, `"nfo"` (video, subtitle and some other file extensions) |
|
|
91
|
+
| `region` | `string` | `"R5"` |
|
|
92
|
+
| `extended` | `boolean` | |
|
|
93
|
+
| `unrated` | `boolean` | also set for uncensored releases |
|
|
94
|
+
| `uncut` | `boolean` | only when uncut ends the title before its year, resolution or season |
|
|
95
|
+
| `remastered` | `boolean` | |
|
|
96
|
+
| `proper` | `boolean` | |
|
|
97
|
+
| `repack` | `boolean` | also set for rerips |
|
|
98
|
+
| `retail` | `boolean` | |
|
|
99
|
+
| `convert` | `boolean` | |
|
|
100
|
+
| `hardcoded` | `boolean` | hardcoded subtitles |
|
|
101
|
+
|
|
102
|
+
## Advanced usage
|
|
103
|
+
|
|
104
|
+
The parser is a list of handlers that run in order. Each handler belongs to a category, the result field it fills,
|
|
105
|
+
such as `resolution` or `languages`. A category usually has several handlers, tried one after another.
|
|
106
|
+
The title is the part of the name before the earliest match, cleaned of dots, underscores, brackets and
|
|
107
|
+
alternative-language titles.
|
|
108
|
+
|
|
109
|
+
To customize the parser, create your own copy of the default one:
|
|
110
|
+
|
|
111
|
+
```javascript
|
|
112
|
+
const ptt = require("@thebeastlt/parse-torrent-title");
|
|
113
|
+
|
|
114
|
+
const parser = ptt.createParser(); // a copy of the default parser
|
|
115
|
+
parser.addHandler("part", /Part[. ]([0-9])/i, ptt.Transformers.integer);
|
|
116
|
+
|
|
117
|
+
console.log(parser.parse("Silent.Witness.S18E03.Falling.Angels.Part.1.720p.HDTV.x264-FTP").part); // 1
|
|
118
|
+
console.log(ptt.parse("Silent.Witness.S18E03.Falling.Angels.Part.1.720p.HDTV.x264-FTP").part); // undefined
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
The module level `ptt.addHandler` and `ptt.removeHandlers` change the shared default parser used by `ptt.parse`,
|
|
122
|
+
and with it every other user of `ptt.parse` in your process.
|
|
123
|
+
|
|
124
|
+
### Regular expression handlers
|
|
125
|
+
|
|
126
|
+
The signature is `addHandler(name, regExp, [transformer], [options])`.
|
|
127
|
+
If the regular expression has a [capturing group](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_expressions/Groups_and_backreferences),
|
|
128
|
+
only the first group is passed to the transformer. Otherwise the whole match is used.
|
|
129
|
+
|
|
130
|
+
#### Transformers
|
|
131
|
+
|
|
132
|
+
A transformer converts the matched string into the stored value. If it returns a falsy value, the match is ignored.
|
|
133
|
+
Built-in transformers are exported as `Transformers` and run natively; any other function works too, as long as it
|
|
134
|
+
returns a boolean, a number, a string or an array of numbers or strings:
|
|
135
|
+
|
|
136
|
+
| Transformer | Result |
|
|
137
|
+
|-----------------------|-------------------------------------------------------------------------------------------------|
|
|
138
|
+
| `none` (default) | the matched string as is |
|
|
139
|
+
| `value(v)` | a fixed value `v`. A `$1` in a string value is replaced by the match, e.g. `value("$1p")` |
|
|
140
|
+
| `integer` | `parseInt` of the match |
|
|
141
|
+
| `boolean` | `true` |
|
|
142
|
+
| `lowercase` | the match in lower case |
|
|
143
|
+
| `uppercase` | the match in upper case |
|
|
144
|
+
| `date(format)` | a `YYYY-MM-DD` string, parsed with a [moment](https://momentjs.com/docs/#/parsing/string-format/) style format made of the `YYYY`, `YY`, `MMMM`, `MMM`, `MM`, `M`, `Do`, `DD` and `D` tokens, such as `"DD MM YYYY"`. An invalid date does not match |
|
|
145
|
+
| `range` | an array of consecutive numbers: `"1-3"` gives `[1, 2, 3]`, `"S01 S02"` gives `[1, 2]`. Returns `null` for numbers that are not in ascending sequence |
|
|
146
|
+
| `rangeUpTo` | `[1..n]` from a single number |
|
|
147
|
+
| `list` | a sorted array of the unique numbers in the match |
|
|
148
|
+
| `yearRange` | a year, or a `"2010-2015"` string for a year range |
|
|
149
|
+
| `array(chain)` | the value from `chain` wrapped in an array, e.g. `array(integer)` |
|
|
150
|
+
| `uniqConcat(chain)` | the value from `chain` appended to the existing array without duplicates, e.g. `uniqConcat(value("english"))` |
|
|
151
|
+
|
|
152
|
+
#### Options
|
|
153
|
+
|
|
154
|
+
| Option | Default | Description |
|
|
155
|
+
|----------------------|---------|---------------------------------------------------------------------------------------------------|
|
|
156
|
+
| `skipIfAlreadyFound` | `true` | Skip this handler if the field was already set by an earlier handler. |
|
|
157
|
+
| `remove` | `false` | Remove the matched text from the name, so later handlers do not see it. |
|
|
158
|
+
| `skipFromTitle` | `false` | Do not let this match end the title. Use it for markers that can appear before or inside the title. |
|
|
159
|
+
| `skipIfFirst` | `false` | Skip the match if it comes before every other match found so far, i.e. it is probably part of the title. |
|
|
160
|
+
| `skipIfBefore` | `[]` | Skip the match if it comes before a match of any of the listed fields. |
|
|
161
|
+
| `skipIfAfter` | `[]` | Skip the match if it comes after a match of any of the listed fields. |
|
|
162
|
+
| `value` | | Store this value instead of the transformer result. |
|
|
163
|
+
| `before` / `after` | | Where to insert the handler, see [Positioning handlers](#positioning-handlers). |
|
|
164
|
+
|
|
165
|
+
The options object can be passed as the third argument when no transformer is needed:
|
|
166
|
+
|
|
167
|
+
```javascript
|
|
168
|
+
parser.addHandler("network", /\b(AMZN|NF|DSNP)\b/, { remove: true });
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
### Function handlers
|
|
172
|
+
|
|
173
|
+
A handler can also be a function, added with `addHandler(name, fn, [options])` or `addHandler(fn, [options])`.
|
|
174
|
+
Only the `before` and `after` options apply to function handlers.
|
|
175
|
+
It receives a context object with:
|
|
176
|
+
|
|
177
|
+
* `title`: the name being parsed, with the matches of earlier `remove` handlers taken out
|
|
178
|
+
* `result`: the result object, which the handler can change
|
|
179
|
+
* `matched`: the matches found so far, as `{ [field]: { rawMatch, matchIndex } }`
|
|
180
|
+
|
|
181
|
+
```javascript
|
|
182
|
+
const parser = ptt.createParser();
|
|
183
|
+
|
|
184
|
+
parser.addHandler("isHarryPotterRelated", ({ title, result }) => {
|
|
185
|
+
if (/harry.potter/i.test(title)) {
|
|
186
|
+
result.isHarryPotterRelated = true;
|
|
187
|
+
}
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
const information = parser.parse("[REQ] Harry Potter And The Prisoner Of Azkaban 2004 1080p BluRay DTS x264-hV");
|
|
191
|
+
console.log(information.isHarryPotterRelated); // true
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
If the matched text should end the title, return `{ matchIndex }` from the handler.
|
|
195
|
+
Return `{ rawMatch, matchIndex, remove: true }` to also remove the match from the name.
|
|
196
|
+
|
|
197
|
+
#### Values
|
|
198
|
+
|
|
199
|
+
The parser runs natively, so the values handlers and transformers store are passed to it as JSON. Store JSON values,
|
|
200
|
+
such as booleans, numbers, strings, arrays and plain objects. A `Date` is stored as its ISO string, and each handler
|
|
201
|
+
sees a copy of the result: an object stored by one handler is equal to, but not the same object as, the one the next
|
|
202
|
+
handler receives. `uniqConcat` therefore compares values by equality rather than identity.
|
|
203
|
+
|
|
204
|
+
### Customizing handlers
|
|
205
|
+
|
|
206
|
+
#### Positioning handlers
|
|
207
|
+
|
|
208
|
+
By default a new handler is added at the end, so it runs after all default handlers.
|
|
209
|
+
Use the `before` or `after` option to run it earlier:
|
|
210
|
+
|
|
211
|
+
```javascript
|
|
212
|
+
const parser = ptt.createParser();
|
|
213
|
+
|
|
214
|
+
// before the first resolution handler
|
|
215
|
+
parser.addHandler("resolution", /\b(8k|4320p)\b/i, ptt.Transformers.value("8k"), { before: "resolution", remove: true });
|
|
216
|
+
|
|
217
|
+
// after the last resolution handler
|
|
218
|
+
parser.addHandler("network", /\b(AMZN|NF|DSNP)\b/, { after: "resolution", remove: true });
|
|
219
|
+
|
|
220
|
+
// after the handler with index 3 of the seasons category (0-based, negative counts from the end)
|
|
221
|
+
parser.addHandler("myStep", ({ result }) => { /* ... */ }, { after: { name: "seasons", index: 3 } });
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
The position is resolved when the handler is added. `addHandler` throws if the target does not exist,
|
|
225
|
+
or if both `before` and `after` are given.
|
|
226
|
+
|
|
227
|
+
#### Removing handlers
|
|
228
|
+
|
|
229
|
+
`removeHandlers` takes a category name, an array of names or a filter function, and returns how many handlers it removed:
|
|
230
|
+
|
|
231
|
+
```javascript
|
|
232
|
+
parser.removeHandlers("site");
|
|
233
|
+
parser.removeHandlers(["languages", "dubbed"]);
|
|
234
|
+
parser.removeHandlers(handler => handler.name === "resolution" && handler.index > 5);
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
Some categories are derived from others: `season` from `seasons`, `episode` from `episodes`,
|
|
238
|
+
and `dubbed` partly from `languages`. Removing a category does not remove the categories derived from it,
|
|
239
|
+
they just find nothing.
|
|
240
|
+
|
|
241
|
+
To leave categories out when adding the default handlers to a parser, use `exclude`:
|
|
242
|
+
|
|
243
|
+
```javascript
|
|
244
|
+
const { Parser, addDefaults } = require("@thebeastlt/parse-torrent-title");
|
|
245
|
+
|
|
246
|
+
const parser = new Parser(); // has no handlers
|
|
247
|
+
addDefaults(parser, { exclude: ["site", "languages"] });
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
#### Listing handlers
|
|
251
|
+
|
|
252
|
+
`getHandlers` returns a description of each handler, which helps to find the index to target:
|
|
253
|
+
|
|
254
|
+
```javascript
|
|
255
|
+
parser.getHandlers().filter(handler => handler.name === "resolution");
|
|
256
|
+
// [
|
|
257
|
+
// { name: "resolution", index: 0, position: 3, regExp: /(?<=\b(?:4k|2160p)\b.*)\b(480|720|1080)p\b/i, options: { ... } },
|
|
258
|
+
// { name: "resolution", index: 1, position: 4, regExp: /\b[([]?4k[)\]]?\b/i, options: { ... } },
|
|
259
|
+
// ...
|
|
260
|
+
// ]
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
`index` is the position within the category and `position` the position within all handlers.
|
|
264
|
+
`regExp` and `options` are only present for regular expression handlers.
|
|
265
|
+
|
|
266
|
+
#### Copying parsers
|
|
267
|
+
|
|
268
|
+
`parser.clone()` returns a copy of a parser, which can be changed without affecting the original.
|
|
269
|
+
`ptt.createParser()` is the same as cloning the default parser.
|
|
270
|
+
|
|
271
|
+
### Usage with TypeScript
|
|
272
|
+
|
|
273
|
+
If you add new fields with `addHandler` in TypeScript, the `DefaultParserResult` type does not include them,
|
|
274
|
+
and you get a TS2339 error.
|
|
275
|
+
|
|
276
|
+
To fix this, create an interface that extends `DefaultParserResult` with the extra fields and use it as the
|
|
277
|
+
`Parser` type parameter:
|
|
278
|
+
|
|
279
|
+
```ts
|
|
280
|
+
import { Parser, DefaultParserResult, addDefaults } from "@thebeastlt/parse-torrent-title";
|
|
281
|
+
|
|
282
|
+
interface ParserResult extends DefaultParserResult {
|
|
283
|
+
part?: number;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
const parser = new Parser<ParserResult>();
|
|
287
|
+
addDefaults(parser);
|
|
288
|
+
parser.addHandler("part", /(?:Part|CD)[. ]?([0-9])/i, input => parseInt(input, 10));
|
|
289
|
+
|
|
290
|
+
const result = parser.parse("Watergate.2018.Part1.DOC.SUBFRENCH.1080p.HDTV.H264-ELEARNiNG");
|
|
291
|
+
console.log(result.year); // 2018
|
|
292
|
+
console.log(result.part); // 1
|
|
293
|
+
```
|
|
294
|
+
|
|
295
|
+
### Differences from earlier javascript versions
|
|
296
|
+
|
|
297
|
+
The parser now runs on Rust, which changes a few edge cases:
|
|
298
|
+
|
|
299
|
+
* An invalid date is left out of the result, instead of being `"Invalid date"`.
|
|
300
|
+
* `Transformers.date` takes a single format, and rejects tokens other than the ones listed above.
|
|
301
|
+
* The sticky `y` flag of regular expressions is not supported.
|
|
302
|
+
* Characters outside the basic multilingual plane, such as emoji, count as one character in the default patterns,
|
|
303
|
+
where javascript counts the two halves of their utf-16 encoding.
|
|
304
|
+
* Built-in transformers return strings for non-string input, e.g. `Transformers.none(5)` is `"5"`.
|
|
305
|
+
|
|
306
|
+
## Development
|
|
307
|
+
|
|
308
|
+
The parser is implemented in [Rust](https://github.com/TheBeastLT/parse-torrent-title); this package wraps it.
|
|
309
|
+
|
|
310
|
+
```bash
|
|
311
|
+
npm install
|
|
312
|
+
npm run build # builds the native addon, which needs a Rust toolchain
|
|
313
|
+
npm test # mocha test suite in test/
|
|
314
|
+
```
|
package/index.d.ts
ADDED
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
declare namespace ParseTorrentTitle {
|
|
2
|
+
|
|
3
|
+
/** A category name, or a specific handler of a category by its 0-based index (negative counts from the end) */
|
|
4
|
+
type HandlerTarget = string | { name: string, index?: number };
|
|
5
|
+
|
|
6
|
+
interface PositionOptions {
|
|
7
|
+
/** Insert before this handler, or before the first handler of this category */
|
|
8
|
+
before?: HandlerTarget;
|
|
9
|
+
/** Insert after this handler, or after the last handler of this category */
|
|
10
|
+
after?: HandlerTarget;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
interface ParserOptions extends PositionOptions {
|
|
14
|
+
/** Whether to skip a matcher if another matcher from this group was already found */
|
|
15
|
+
skipIfAlreadyFound?: boolean;
|
|
16
|
+
/** Whether to exclude found match from the end result title */
|
|
17
|
+
skipFromTitle?: boolean;
|
|
18
|
+
/** Whether to skip this matcher if there are no other groups matched before it's matchIndex */
|
|
19
|
+
skipIfFirst?: boolean;
|
|
20
|
+
/** Whether to skip this matcher if it appears before specified matcher group in the name */
|
|
21
|
+
skipIfBefore?: Array<string>;
|
|
22
|
+
/** Whether to skip this matcher if it appears after specified matcher group in the name */
|
|
23
|
+
skipIfAfter?: Array<string>;
|
|
24
|
+
/** Whether to remove the found match from further matchers */
|
|
25
|
+
remove?: boolean;
|
|
26
|
+
/** Value to set instead of the transformed match */
|
|
27
|
+
value?: any;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
interface DefaultParserResult {
|
|
31
|
+
title: string;
|
|
32
|
+
date?: string;
|
|
33
|
+
year?: number | string;
|
|
34
|
+
resolution?: string;
|
|
35
|
+
extended?: boolean;
|
|
36
|
+
unrated?: boolean;
|
|
37
|
+
uncut?: boolean;
|
|
38
|
+
proper?: boolean;
|
|
39
|
+
repack?: boolean;
|
|
40
|
+
convert?: boolean;
|
|
41
|
+
hardcoded?: boolean;
|
|
42
|
+
retail?: boolean;
|
|
43
|
+
remastered?: boolean;
|
|
44
|
+
complete?: boolean;
|
|
45
|
+
extras?: boolean;
|
|
46
|
+
region?: string;
|
|
47
|
+
container?: string;
|
|
48
|
+
extension?: string;
|
|
49
|
+
source?: string;
|
|
50
|
+
codec?: string;
|
|
51
|
+
bitDepth?: string;
|
|
52
|
+
hdr?: Array<string>;
|
|
53
|
+
threeD?: string;
|
|
54
|
+
audio?: string;
|
|
55
|
+
audioChannels?: string;
|
|
56
|
+
group?: string;
|
|
57
|
+
site?: string;
|
|
58
|
+
volumes?: Array<number>;
|
|
59
|
+
seasons?: Array<number>;
|
|
60
|
+
season?: number;
|
|
61
|
+
episodes?: Array<number>;
|
|
62
|
+
episode?: number;
|
|
63
|
+
halfEpisode?: boolean;
|
|
64
|
+
episodeCode?: string;
|
|
65
|
+
languages?: Array<string>;
|
|
66
|
+
dubbed?: boolean;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
interface Match {
|
|
70
|
+
rawMatch?: string;
|
|
71
|
+
matchIndex: number;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
interface HandlerContext<ParserResult = DefaultParserResult> {
|
|
75
|
+
/** The name being parsed, without the matches removed by previous handlers */
|
|
76
|
+
title: string;
|
|
77
|
+
result: ParserResult;
|
|
78
|
+
/** Matches found so far, keyed by the field name */
|
|
79
|
+
matched: { [field: string]: Match };
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
type HandlerResult = Match & {
|
|
83
|
+
/** Whether to exclude the match from the end result title */
|
|
84
|
+
skipFromTitle?: boolean;
|
|
85
|
+
} & (
|
|
86
|
+
{ remove?: false } |
|
|
87
|
+
/** Removing the match from the name for further handlers needs its text */
|
|
88
|
+
{ remove: true, rawMatch: string }
|
|
89
|
+
);
|
|
90
|
+
|
|
91
|
+
interface Handler<ParserResult = DefaultParserResult> {
|
|
92
|
+
(context: HandlerContext<ParserResult>): HandlerResult | null | void;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
interface ParseFunction<ParserResult = DefaultParserResult> {
|
|
96
|
+
(title: string): ParserResult;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
interface Transformer {
|
|
100
|
+
(input: string, existingValue?: any): any;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
interface Transformers {
|
|
104
|
+
none: Transformer;
|
|
105
|
+
value: (value: any) => Transformer;
|
|
106
|
+
integer: Transformer;
|
|
107
|
+
cjkInteger: Transformer;
|
|
108
|
+
boolean: Transformer;
|
|
109
|
+
lowercase: Transformer;
|
|
110
|
+
uppercase: Transformer;
|
|
111
|
+
/** A moment.js style format made of the YYYY, YY, MMMM, MMM, MM, M, Do, DD and D tokens */
|
|
112
|
+
date: (dateFormat: string) => Transformer;
|
|
113
|
+
range: Transformer;
|
|
114
|
+
list: Transformer;
|
|
115
|
+
rangeUpTo: Transformer;
|
|
116
|
+
yearRange: Transformer;
|
|
117
|
+
array: (chain?: Transformer) => Transformer;
|
|
118
|
+
uniqConcat: (chain: Transformer) => Transformer;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
interface AddHandlerFunction<ParserResult = DefaultParserResult> {
|
|
122
|
+
(handlerName: string, handler: RegExp, transformer?: Transformer, options?: ParserOptions): void;
|
|
123
|
+
(handlerName: string, handler: RegExp, options?: ParserOptions): void;
|
|
124
|
+
(handlerName: string, handler: Handler<ParserResult>, options?: PositionOptions): void;
|
|
125
|
+
(handler: Handler<ParserResult>, options?: PositionOptions): void;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
interface HandlerInfo {
|
|
129
|
+
name: string;
|
|
130
|
+
/** Index of the handler within its category */
|
|
131
|
+
index: number;
|
|
132
|
+
/** Index of the handler within all handlers of the parser */
|
|
133
|
+
position: number;
|
|
134
|
+
/** Only present for regular expression handlers */
|
|
135
|
+
regExp?: RegExp;
|
|
136
|
+
/** Only present for regular expression handlers, with all defaults filled in */
|
|
137
|
+
options?: Required<Omit<ParserOptions, "before" | "after" | "value">> & Pick<ParserOptions, "value">;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
type RemoveHandlersTarget = string | Array<string> | ((handler: HandlerInfo) => boolean);
|
|
141
|
+
|
|
142
|
+
interface AddDefaultsOptions {
|
|
143
|
+
/** Categories to leave out */
|
|
144
|
+
exclude?: Array<string>;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
interface AddDefaultsFunction {
|
|
148
|
+
(parser: Parser<any>, options?: AddDefaultsOptions): void;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
class Parser<ParserResult = DefaultParserResult> {
|
|
152
|
+
|
|
153
|
+
constructor();
|
|
154
|
+
|
|
155
|
+
addHandler: AddHandlerFunction<ParserResult>;
|
|
156
|
+
parse: ParseFunction<ParserResult>;
|
|
157
|
+
/** Removes handlers and returns the number of removed handlers */
|
|
158
|
+
removeHandlers(target: RemoveHandlersTarget): number;
|
|
159
|
+
getHandlers(): Array<HandlerInfo>;
|
|
160
|
+
clone(): Parser<ParserResult>;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
declare module "@thebeastlt/parse-torrent-title" {
|
|
165
|
+
|
|
166
|
+
export interface DefaultParserResult extends ParseTorrentTitle.DefaultParserResult { }
|
|
167
|
+
export type ParserOptions = ParseTorrentTitle.ParserOptions;
|
|
168
|
+
export type PositionOptions = ParseTorrentTitle.PositionOptions;
|
|
169
|
+
export type HandlerTarget = ParseTorrentTitle.HandlerTarget;
|
|
170
|
+
export type HandlerInfo = ParseTorrentTitle.HandlerInfo;
|
|
171
|
+
export type AddDefaultsOptions = ParseTorrentTitle.AddDefaultsOptions;
|
|
172
|
+
export type Handler<ParserResult = DefaultParserResult> = ParseTorrentTitle.Handler<ParserResult>;
|
|
173
|
+
export type HandlerContext<ParserResult = DefaultParserResult> = ParseTorrentTitle.HandlerContext<ParserResult>;
|
|
174
|
+
export type HandlerResult = ParseTorrentTitle.HandlerResult;
|
|
175
|
+
export type RemoveHandlersTarget = ParseTorrentTitle.RemoveHandlersTarget;
|
|
176
|
+
export type Transformer = ParseTorrentTitle.Transformer;
|
|
177
|
+
export class Parser<ParserResult = DefaultParserResult> extends ParseTorrentTitle.Parser<ParserResult> { }
|
|
178
|
+
export const parse: ParseTorrentTitle.ParseFunction;
|
|
179
|
+
export const addHandler: ParseTorrentTitle.AddHandlerFunction;
|
|
180
|
+
export const removeHandlers: (target: ParseTorrentTitle.RemoveHandlersTarget) => number;
|
|
181
|
+
export const createParser: () => Parser;
|
|
182
|
+
export const addDefaults: ParseTorrentTitle.AddDefaultsFunction;
|
|
183
|
+
export const Transformers: ParseTorrentTitle.Transformers;
|
|
184
|
+
}
|
package/index.js
ADDED
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
// The api of the js library on top of the rust parser of `native.js`, built by `npm run build`.
|
|
2
|
+
// Values cross into rust as json; handlers and transformers written in js are wrapped to take and return json,
|
|
3
|
+
// so they see copies of the values: a Date becomes its iso string, and an object is equal to but not the same as
|
|
4
|
+
// the one a previous handler stored.
|
|
5
|
+
const { NativeParser, applyTransformer } = require("./native");
|
|
6
|
+
|
|
7
|
+
// built in transformers are described for rust to run them, and stay callable from js
|
|
8
|
+
function builtin(spec) {
|
|
9
|
+
const transformer = (input, current) => {
|
|
10
|
+
const json = current === undefined ? null : JSON.stringify(current);
|
|
11
|
+
const result = applyTransformer(JSON.stringify(spec), String(input), json);
|
|
12
|
+
return result === null ? null : JSON.parse(result);
|
|
13
|
+
};
|
|
14
|
+
transformer.spec = spec;
|
|
15
|
+
return transformer;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// the moment.js tokens of a date format as the chrono specifiers the rust transformer reads
|
|
19
|
+
const MOMENT_TOKENS = { YYYY: "%Y", YY: "%y", MMMM: "%b", MMM: "%b", MM: "%m", M: "%m", Do: "%d", DD: "%d", D: "%d" };
|
|
20
|
+
|
|
21
|
+
function chronoFormat(format) {
|
|
22
|
+
if (typeof format !== "string") {
|
|
23
|
+
throw new Error("Transformers.date takes a single moment.js format");
|
|
24
|
+
}
|
|
25
|
+
const converted = format.replace(/YYYY|YY|MMMM|MMM|MM|Do|DD|M|D/g, token => MOMENT_TOKENS[token]);
|
|
26
|
+
if (/[a-z]/i.test(converted.replace(/%[a-z]/gi, ""))) {
|
|
27
|
+
throw new Error(`Unsupported token in date format ${JSON.stringify(format)}, use YYYY, YY, MMMM, MMM, MM, M, Do, DD or D`);
|
|
28
|
+
}
|
|
29
|
+
return converted;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const Transformers = {
|
|
33
|
+
none: builtin({ kind: "none" }),
|
|
34
|
+
value: value => builtin({ kind: "value", value }),
|
|
35
|
+
integer: builtin({ kind: "integer" }),
|
|
36
|
+
cjkInteger: builtin({ kind: "cjkInteger" }),
|
|
37
|
+
boolean: builtin({ kind: "boolean" }),
|
|
38
|
+
lowercase: builtin({ kind: "lowercase" }),
|
|
39
|
+
uppercase: builtin({ kind: "uppercase" }),
|
|
40
|
+
date: format => builtin({ kind: "date", format: chronoFormat(format) }),
|
|
41
|
+
range: builtin({ kind: "range" }),
|
|
42
|
+
list: builtin({ kind: "list" }),
|
|
43
|
+
rangeUpTo: builtin({ kind: "rangeUpTo" }),
|
|
44
|
+
yearRange: builtin({ kind: "yearRange" }),
|
|
45
|
+
array: chain => (!chain || chain.spec ? builtin({ kind: "array", chain: chain && chain.spec }) : input => [chain(input)]),
|
|
46
|
+
uniqConcat: chain => (chain.spec ? builtin({ kind: "uniqConcat", chain: chain.spec }) : (input, current) => {
|
|
47
|
+
const values = current || [];
|
|
48
|
+
const value = chain(input);
|
|
49
|
+
// values are json, copied on each call, so an equal value is the same value
|
|
50
|
+
const json = JSON.stringify(value);
|
|
51
|
+
const found = !Array.isArray(value) && values.some(existing => JSON.stringify(existing) === json);
|
|
52
|
+
return found ? values : values.concat(value);
|
|
53
|
+
})
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
// a transformer written in js, called by rust with `[input, current]` and returning its truthy value as json
|
|
57
|
+
function jsonTransformer(transformer) {
|
|
58
|
+
return json => {
|
|
59
|
+
const [input, current] = JSON.parse(json);
|
|
60
|
+
const value = transformer(input, current === null ? undefined : current);
|
|
61
|
+
return value ? JSON.stringify(value) : null;
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// a handler written in js, called by rust with its context and returning the changed context with its result
|
|
66
|
+
function jsonHandler(handler) {
|
|
67
|
+
return json => {
|
|
68
|
+
const context = JSON.parse(json);
|
|
69
|
+
const returned = handler(context);
|
|
70
|
+
return JSON.stringify({ result: context.result, matched: context.matched, returned: returned || null });
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function toPosition({ before, after }) {
|
|
75
|
+
return JSON.stringify({ before: before || null, after: after || null });
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
class Parser {
|
|
79
|
+
|
|
80
|
+
constructor(native) {
|
|
81
|
+
this.native = native || new NativeParser();
|
|
82
|
+
this.parse = this.parse.bind(this);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
addHandler(handlerName, handler, transformer, options) {
|
|
86
|
+
if (typeof handlerName === "function") {
|
|
87
|
+
|
|
88
|
+
// If no name is provided and a function handler is directly given
|
|
89
|
+
[handlerName, handler, transformer] = ["unknown", handlerName, handler];
|
|
90
|
+
}
|
|
91
|
+
if (transformer && typeof transformer === "object") {
|
|
92
|
+
|
|
93
|
+
// If options are given in place of the transformer
|
|
94
|
+
[transformer, options] = [Transformers.none, transformer];
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const { before, after, ...matchOptions } = options || {};
|
|
98
|
+
const position = toPosition({ before, after });
|
|
99
|
+
|
|
100
|
+
if (handler instanceof RegExp) {
|
|
101
|
+
const spec = transformer ? transformer.spec : Transformers.none.spec;
|
|
102
|
+
const custom = transformer && !transformer.spec ? jsonTransformer(transformer) : undefined;
|
|
103
|
+
this.native.addRegexHandler(handlerName, handler.source, handler.flags, spec && JSON.stringify(spec), custom, JSON.stringify(matchOptions), position);
|
|
104
|
+
} else if (typeof handler === "function") {
|
|
105
|
+
this.native.addFunctionHandler(handlerName, jsonHandler(handler), position);
|
|
106
|
+
} else {
|
|
107
|
+
throw new Error(`Handler for ${handlerName} should be a RegExp or a function. Got: ${typeof handler}`);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
removeHandlers(target) {
|
|
112
|
+
const shouldRemove = typeof target === "function" ? target : handler => [].concat(target).includes(handler.name);
|
|
113
|
+
const positions = this.getHandlers().filter(handler => shouldRemove(handler)).map(handler => handler.position);
|
|
114
|
+
|
|
115
|
+
return this.native.removeAt(positions);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
getHandlers() {
|
|
119
|
+
return JSON.parse(this.native.getHandlers()).map(({ name, index, position, source, flags, options }) => ({
|
|
120
|
+
name,
|
|
121
|
+
index,
|
|
122
|
+
position,
|
|
123
|
+
...source !== undefined && { regExp: new RegExp(source, flags), options }
|
|
124
|
+
}));
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
clone() {
|
|
128
|
+
return new this.constructor(this.native.clone());
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
parse(title) {
|
|
132
|
+
return JSON.parse(this.native.parse(title));
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function addDefaults(parser, { exclude } = {}) {
|
|
137
|
+
parser.native.addDefaults([].concat(exclude || []));
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const defaultParser = new Parser(NativeParser.withDefaults());
|
|
141
|
+
|
|
142
|
+
exports.addDefaults = addDefaults;
|
|
143
|
+
exports.addHandler = (...args) => defaultParser.addHandler(...args);
|
|
144
|
+
exports.removeHandlers = target => defaultParser.removeHandlers(target);
|
|
145
|
+
exports.createParser = () => defaultParser.clone();
|
|
146
|
+
exports.parse = title => defaultParser.parse(title);
|
|
147
|
+
exports.Parser = Parser;
|
|
148
|
+
exports.Transformers = Transformers;
|