@viren070/parse-torrent-title 0.1.3 → 0.3.0
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 +346 -1
- package/dist/handlers.d.ts.map +1 -1
- package/dist/handlers.js +118 -40
- package/dist/handlers.js.map +1 -1
- package/dist/index.d.ts +48 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +79 -5
- package/dist/index.js.map +1 -1
- package/dist/parser.d.ts.map +1 -1
- package/dist/parser.js +20 -3
- package/dist/parser.js.map +1 -1
- package/dist/processors.d.ts +6 -0
- package/dist/processors.d.ts.map +1 -0
- package/dist/processors.js +12 -0
- package/dist/processors.js.map +1 -0
- package/dist/transforms.d.ts +1 -14
- package/dist/transforms.d.ts.map +1 -1
- package/dist/transforms.js +21 -58
- package/dist/transforms.js.map +1 -1
- package/dist/types.d.ts +1 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/dist/utils.d.ts.map +1 -1
- package/dist/utils.js +5 -2
- package/dist/utils.js.map +1 -1
- package/dist/validators.d.ts +14 -0
- package/dist/validators.d.ts.map +1 -0
- package/dist/validators.js +71 -0
- package/dist/validators.js.map +1 -0
- package/package.json +4 -2
package/README.md
CHANGED
|
@@ -1,3 +1,348 @@
|
|
|
1
1
|
# parse-torrent-title
|
|
2
2
|
|
|
3
|
-
A TypeScript library for parsing torrent titles. This is mostly a port of [go-ptt](https://github.com/MunifTanjim/go-ptt).
|
|
3
|
+
A TypeScript library for parsing torrent titles. This is mostly a port of [go-ptt](https://github.com/MunifTanjim/go-ptt), which in turn is inspired by [TheBeastLT/parse-torrent-title](https://github.com/TheBeastLT/parse-torrent-title) and [dreulavelle/PTT](https://github.com/dreulavelle/PTT).
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
**npm:**
|
|
8
|
+
```bash
|
|
9
|
+
npm install @viren070/parse-torrent-title
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
**pnpm:**
|
|
13
|
+
```bash
|
|
14
|
+
pnpm add @viren070/parse-torrent-title
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
**yarn:**
|
|
18
|
+
```bash
|
|
19
|
+
yarn add @viren070/parse-torrent-title
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Demo
|
|
23
|
+
|
|
24
|
+
Try out the parser in your browser with our [demo](https://viren070.github.io/parse-torrent-title/)! Enter any torrent filename and see the parsed results in real-time.
|
|
25
|
+
|
|
26
|
+
## Quick Start
|
|
27
|
+
|
|
28
|
+
```typescript
|
|
29
|
+
import { parseTorrentTitle } from '@viren070/parse-torrent-title';
|
|
30
|
+
|
|
31
|
+
const result = parseTorrentTitle('The.Matrix.1999.1080p.BluRay.x264.DTS-HD.MA.5.1');
|
|
32
|
+
|
|
33
|
+
console.log(result);
|
|
34
|
+
// {
|
|
35
|
+
// "resolution": "1080p",
|
|
36
|
+
// "year": "1999",
|
|
37
|
+
// "quality": "BluRay",
|
|
38
|
+
// "codec": "x264",
|
|
39
|
+
// "channels": [
|
|
40
|
+
// "5.1"
|
|
41
|
+
// ],
|
|
42
|
+
// "audio": [
|
|
43
|
+
// "DTS Lossless"
|
|
44
|
+
// ],
|
|
45
|
+
// "title": "The Matrix"
|
|
46
|
+
// }
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## Output Types
|
|
50
|
+
|
|
51
|
+
The parser returns a `ParsedResult` object with the following optional fields:
|
|
52
|
+
|
|
53
|
+
```typescript
|
|
54
|
+
interface ParsedResult {
|
|
55
|
+
// Basic Information
|
|
56
|
+
title?: string; // Movie or series title
|
|
57
|
+
year?: string; // Release year
|
|
58
|
+
date?: string; // Release date
|
|
59
|
+
|
|
60
|
+
// Video Quality
|
|
61
|
+
resolution?: string; // e.g., '1080p', '720p', '4K', '2160p'
|
|
62
|
+
quality?: string; // e.g., 'BluRay', 'WEB-DL', 'HDTV', 'DVDRip'
|
|
63
|
+
codec?: string; // e.g., 'x264', 'x265', 'hevc', 'xvid'
|
|
64
|
+
bitDepth?: string; // e.g., '10bit', '8bit'
|
|
65
|
+
hdr?: string[]; // e.g., ['HDR10', 'HDR', 'Dolby Vision']
|
|
66
|
+
threeD?: string; // e.g., '3D SBS', '3D HOU'
|
|
67
|
+
|
|
68
|
+
// Audio
|
|
69
|
+
audio?: string[]; // e.g., ['DTS', 'AC3', 'AAC', 'MP3']
|
|
70
|
+
channels?: string[]; // e.g., ['5.1', '7.1', '2.0']
|
|
71
|
+
|
|
72
|
+
// Episode/Season Information
|
|
73
|
+
seasons?: number[]; // Season numbers, e.g., [1, 2, 3]
|
|
74
|
+
episodes?: number[]; // Episode numbers, e.g., [1, 2, 3]
|
|
75
|
+
episodeCode?: string; // e.g., '5E46AC39'
|
|
76
|
+
complete?: boolean; // Complete season/series indicator
|
|
77
|
+
volumes?: number[]; // Volume numbers
|
|
78
|
+
|
|
79
|
+
// Languages and Subtitles
|
|
80
|
+
languages?: string[]; // e.g., ['en', 'jp', 'multi subs', 'multi audio', 'dual audio', 'es-419']
|
|
81
|
+
dubbed?: boolean; // Whether content is dubbed
|
|
82
|
+
subbed?: boolean; // Whether subtitles are included
|
|
83
|
+
hardcoded?: boolean; // Hardcoded subtitles
|
|
84
|
+
|
|
85
|
+
// Release Information
|
|
86
|
+
group?: string; // Release group name
|
|
87
|
+
site?: string; // Source site
|
|
88
|
+
network?: string; // Broadcasting network (e.g., 'Netflix', 'HBO', 'Amazon')
|
|
89
|
+
edition?: string; // e.g., 'Extended', 'Theatrical', "Director's Cut"
|
|
90
|
+
releaseTypes?: string[]; // Release type tags
|
|
91
|
+
|
|
92
|
+
// Release Flags
|
|
93
|
+
repack?: boolean; // Repack indicator
|
|
94
|
+
proper?: boolean; // Proper release indicator
|
|
95
|
+
retail?: boolean; // Retail release
|
|
96
|
+
remastered?: boolean; // Remastered release
|
|
97
|
+
unrated?: boolean; // Unrated version
|
|
98
|
+
uncensored?: boolean; // Uncensored version
|
|
99
|
+
extended?: boolean; // Extended version
|
|
100
|
+
convert?: boolean; // Converted release
|
|
101
|
+
documentary?: boolean; // Documentary flag
|
|
102
|
+
commentary?: boolean; // Commentary track included
|
|
103
|
+
upscaled?: boolean; // Upscaled content indicator
|
|
104
|
+
|
|
105
|
+
// Technical Details
|
|
106
|
+
container?: string; // File container, e.g., 'mkv', 'mp4', 'avi'
|
|
107
|
+
extension?: string; // File extension
|
|
108
|
+
region?: string; // Regional encoding
|
|
109
|
+
size?: string; // File size
|
|
110
|
+
}
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
## Advanced Usage
|
|
114
|
+
|
|
115
|
+
### Using the Parser Class
|
|
116
|
+
|
|
117
|
+
For more control, you can use the `Parser` class:
|
|
118
|
+
|
|
119
|
+
```typescript
|
|
120
|
+
import { Parser } from '@viren070/parse-torrent-title';
|
|
121
|
+
|
|
122
|
+
// Create a parser with only specific fields
|
|
123
|
+
const parser = new Parser()
|
|
124
|
+
.addDefaultHandlers('title', 'year', 'resolution', 'quality');
|
|
125
|
+
|
|
126
|
+
const result = parser.parse('The.Matrix.1999.1080p.BluRay.x264');
|
|
127
|
+
// Only extracts: title, year, resolution, quality
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
## Writing Custom Handlers
|
|
131
|
+
|
|
132
|
+
You can extend the parser's capabilities by writing your own handlers. A handler is an object that defines how to find and process a piece of metadata in the torrent title.
|
|
133
|
+
|
|
134
|
+
### Handler Interface
|
|
135
|
+
|
|
136
|
+
Here is the structure of a `Handler` object:
|
|
137
|
+
|
|
138
|
+
```typescript
|
|
139
|
+
interface Handler {
|
|
140
|
+
field: string;
|
|
141
|
+
pattern?: RegExp;
|
|
142
|
+
validateMatch?: (input: string, idxs: number[]) => boolean;
|
|
143
|
+
transform?: (title: string, m: ParseMeta, result: Map<string, ParseMeta>) => void;
|
|
144
|
+
process?: (title: string, m: ParseMeta, result: Map<string, ParseMeta>) => ParseMeta;
|
|
145
|
+
remove?: boolean;
|
|
146
|
+
keepMatching?: boolean;
|
|
147
|
+
skipIfFirst?: boolean;
|
|
148
|
+
skipIfBefore?: string[];
|
|
149
|
+
skipFromTitle?: boolean;
|
|
150
|
+
matchGroup?: number;
|
|
151
|
+
valueGroup?: number;
|
|
152
|
+
}
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
### Handler Properties
|
|
156
|
+
|
|
157
|
+
| Property | Type | Description |
|
|
158
|
+
| :--- | :--- | :--- |
|
|
159
|
+
| `field` | `string` | **Required.** The key this handler will populate in the result object (e.g., `'resolution'`, `'quality'`). |
|
|
160
|
+
| `pattern` | `RegExp` | A regular expression used to find a value in the title. |
|
|
161
|
+
| `transform` | `function` | A function to process the matched value. See the [Transformers](#transformers) section for available utilities. |
|
|
162
|
+
| `validateMatch` | `function` | A function that returns `true` or `false` to determine if a match is valid. Use this to prevent a handler from running on a bad match. See [Validators](#validators) for available utilities to aid in writing validation logic. |
|
|
163
|
+
| `process` | `function` | For complex logic that can't be handled by `pattern` and `transform`. It can create or modify the entire metadata object for a field. |
|
|
164
|
+
| `remove` | `boolean` | If `true`, the matched text is removed from the title string. Defaults to `false`. |
|
|
165
|
+
| `keepMatching` | `boolean` | If `true`, the parser will keep searching for this field even if a value has already been found. Useful for fields that can have multiple values (e.g., `languages`). Defaults to `false`. |
|
|
166
|
+
| `skipFromTitle` | `boolean` | If `true`, this match will not be used to determine the end of the main `title` property. Useful for metadata at the end of the string (e.g., release group). Defaults to `false`. |
|
|
167
|
+
| `skipIfFirst` | `boolean` | If `true`, the handler will be skipped if its match is the very first piece of metadata found in the title. |
|
|
168
|
+
| `skipIfBefore` | `string[]` | An array of field names. The handler will be skipped if its match appears before any of those fields have been matched. |
|
|
169
|
+
| `matchGroup` | `number` | The regex capture group to use as the "full match" text. This is the part that gets removed from the title if `remove` is `true`. |
|
|
170
|
+
| `valueGroup` | `number` | The regex capture group to use as the "value" for the field. If not set, it defaults to capture group 1 if it exists. |
|
|
171
|
+
|
|
172
|
+
### Custom Handler Example
|
|
173
|
+
|
|
174
|
+
Here is an example of a custom handler that finds a fictional "Source" tag (e.g., `[SRC-XYZ]`) and adds it to a custom field:
|
|
175
|
+
|
|
176
|
+
```javascript
|
|
177
|
+
import { Parser, toTrimmed } from '@viren070/parse-torrent-title';
|
|
178
|
+
|
|
179
|
+
const parser = new Parser();
|
|
180
|
+
|
|
181
|
+
// Add a custom handler
|
|
182
|
+
parser.addHandler({
|
|
183
|
+
field: 'sourceId',
|
|
184
|
+
pattern: /\[SRC-([\w]+)\]/i,
|
|
185
|
+
transform: toTrimmed(), // Trim whitespace from the matched value
|
|
186
|
+
remove: true, // Remove "[SRC-XYZ]" from the title
|
|
187
|
+
valueGroup: 1, // Use the first capture group "XYZ" as the value
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
parser.addDefaultHandlers();
|
|
191
|
+
|
|
192
|
+
const result = parser.parse('My.Movie.2023.1080p.[SRC-WIKIPEDIA].mkv');
|
|
193
|
+
|
|
194
|
+
console.log(result.sourceId); // "WIKIPEDIA"
|
|
195
|
+
console.log(result.title); // "My Movie"
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
### Type-Safe Custom Fields
|
|
199
|
+
|
|
200
|
+
You can use TypeScript generics to get full type safety for your custom fields:
|
|
201
|
+
|
|
202
|
+
```typescript
|
|
203
|
+
import { Parser, Handler, transforms } from '@viren070/parse-torrent-title';
|
|
204
|
+
|
|
205
|
+
const customHandler: Handler = {
|
|
206
|
+
field: 'customId',
|
|
207
|
+
pattern: /custom-(\d+)/i,
|
|
208
|
+
transform: transforms.toIntArray()
|
|
209
|
+
};
|
|
210
|
+
|
|
211
|
+
const parser = new Parser()
|
|
212
|
+
.addDefaultHandlers() // Add all default handlers
|
|
213
|
+
.addHandler(customHandler); // Add your custom handler
|
|
214
|
+
|
|
215
|
+
type CustomFields = { customId: number[] };
|
|
216
|
+
const typedResult = parser.parse<CustomFields>('Movie.2024.custom-123.1080p');
|
|
217
|
+
console.log(typedResult.customId); // [123] - fully type-safe!
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
### Transformers
|
|
221
|
+
|
|
222
|
+
Transformers modify the captured value before it's stored in the result. All transformers are functions that return a `HandlerTransformer`.
|
|
223
|
+
|
|
224
|
+
**Value Transformers:**
|
|
225
|
+
- `toValue(value: string)` - Set a fixed value instead of the captured text
|
|
226
|
+
- `toBoolean()` - Set the value to `true` (useful for flags)
|
|
227
|
+
- `toIntArray()` - Convert captured text to an array with a single integer
|
|
228
|
+
- `toIntRange()` - Convert range like "1-8" to `[1,2,3,4,5,6,7,8]`
|
|
229
|
+
|
|
230
|
+
**String Transformers:**
|
|
231
|
+
- `toLowercase()` - Convert to lowercase
|
|
232
|
+
- `toUppercase()` - Convert to uppercase
|
|
233
|
+
- `toTrimmed()` - Trim whitespace from the value
|
|
234
|
+
- `toWithSuffix(suffix: string)` - Append a suffix to the value
|
|
235
|
+
|
|
236
|
+
**Date/Time Transformers:**
|
|
237
|
+
- `toDate(format: string)` - Parse and format date strings (supports formats like "2006 01 02", "20060102")
|
|
238
|
+
- `toYear()` - Extract and format year (handles year ranges like "2020-2024")
|
|
239
|
+
- `toCleanDate()` - Remove ordinal suffixes (1st → 1, 2nd → 2, etc.)
|
|
240
|
+
- `toCleanMonth()` - Normalize month names to 3-letter abbreviations
|
|
241
|
+
|
|
242
|
+
**Advanced Transformers:**
|
|
243
|
+
- `toValueSet(value: any)` - Add value to a ValueSet (for multi-value fields)
|
|
244
|
+
- `toValueSetWithTransform(fn: (v: string) => any)` - Transform and add to ValueSet
|
|
245
|
+
- `toValueSetMultiWithTransform(fn: (v: string) => any[])` - Transform to multiple values and add to ValueSet
|
|
246
|
+
|
|
247
|
+
**Processor:**
|
|
248
|
+
- `removeFromValue(regex: RegExp)` - Remove matching patterns from the captured value
|
|
249
|
+
|
|
250
|
+
### Validators
|
|
251
|
+
|
|
252
|
+
Validators determine whether a match should be accepted. They're used with the `validateMatch` property.
|
|
253
|
+
|
|
254
|
+
- `validateMatch(regex: RegExp)` - Accept only if the captured text matches the regex
|
|
255
|
+
- `validateNotMatch(regex: RegExp)` - Reject if the captured text matches the regex
|
|
256
|
+
- `validateNotAtStart()` - Reject matches at the start of the title
|
|
257
|
+
- `validateNotAtEnd()` - Reject matches at the end of the title
|
|
258
|
+
- `validateMatchedGroupsAreSame(...indices: number[])` - Accept only if specified capture groups have the same value
|
|
259
|
+
- `validateAnd(...validators: HandlerMatchValidator[])` - Combine multiple validators (all must pass)
|
|
260
|
+
|
|
261
|
+
Example with validators:
|
|
262
|
+
|
|
263
|
+
```typescript
|
|
264
|
+
import { Parser, Handler, validators, transforms } from '@viren070/parse-torrent-title';
|
|
265
|
+
|
|
266
|
+
const handler: Handler = {
|
|
267
|
+
field: 'episodes',
|
|
268
|
+
pattern: /episode[.\s]*(\d+)/i,
|
|
269
|
+
validateMatch: validators.validateNotMatch(/season/i), // Don't match if "season" is present
|
|
270
|
+
transform: transforms.toIntArray()
|
|
271
|
+
};
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
## Examples
|
|
275
|
+
|
|
276
|
+
### TV Series
|
|
277
|
+
|
|
278
|
+
```typescript
|
|
279
|
+
parseTorrentTitle('[Erai-raws] Attack on Titan - S04E01 [1080p][Multiple Subtitle].mkv');
|
|
280
|
+
// {
|
|
281
|
+
// "resolution": "1080p",
|
|
282
|
+
// "container": "mkv",
|
|
283
|
+
// "seasons": [4],
|
|
284
|
+
// "episodes": [1],
|
|
285
|
+
// "languages": ["multi subs"],
|
|
286
|
+
// "subbed": true,
|
|
287
|
+
// "group": "Erai-raws",
|
|
288
|
+
// "extension": "mkv",
|
|
289
|
+
// "title": "Attack on Titan"
|
|
290
|
+
// }
|
|
291
|
+
```
|
|
292
|
+
|
|
293
|
+
### Season Packs
|
|
294
|
+
|
|
295
|
+
```typescript
|
|
296
|
+
parseTorrentTitle('Breaking.Bad.Season.1-5.Complete.1080p.BluRay.x265.HEVC.10bit');
|
|
297
|
+
// {
|
|
298
|
+
// "resolution": "1080p",
|
|
299
|
+
// "quality": "BluRay",
|
|
300
|
+
// "bitDepth": "10bit",
|
|
301
|
+
// "codec": "hevc",
|
|
302
|
+
// "complete": true,
|
|
303
|
+
// "seasons": [1, 2, 3, 4, 5],
|
|
304
|
+
// "title": "Breaking Bad"
|
|
305
|
+
// }
|
|
306
|
+
```
|
|
307
|
+
|
|
308
|
+
### Movies with HDR
|
|
309
|
+
|
|
310
|
+
```typescript
|
|
311
|
+
parseTorrentTitle('Dune.2021.2160p.UHD.BluRay.HDR10.DV.x265.DTS-HD.MA.5.1');
|
|
312
|
+
// {
|
|
313
|
+
// "resolution": "4k",
|
|
314
|
+
// "year": "2021",
|
|
315
|
+
// "quality": "BluRay",
|
|
316
|
+
// "bitDepth": "10bit",
|
|
317
|
+
// "hdr": [
|
|
318
|
+
// "DV",
|
|
319
|
+
// "HDR"
|
|
320
|
+
// ],
|
|
321
|
+
// "codec": "x265",
|
|
322
|
+
// "channels": [
|
|
323
|
+
// "5.1"
|
|
324
|
+
// ],
|
|
325
|
+
// "audio": [
|
|
326
|
+
// "DTS Lossless"
|
|
327
|
+
// ],
|
|
328
|
+
// "title": "Dune"
|
|
329
|
+
// }
|
|
330
|
+
```
|
|
331
|
+
|
|
332
|
+
## TypeScript Support
|
|
333
|
+
|
|
334
|
+
This library is written in TypeScript and includes full type definitions. All types are exported for your use:
|
|
335
|
+
|
|
336
|
+
```typescript
|
|
337
|
+
import { ParsedResult, Handler } from '@viren070/parse-torrent-title';
|
|
338
|
+
```
|
|
339
|
+
|
|
340
|
+
## License
|
|
341
|
+
|
|
342
|
+
MIT
|
|
343
|
+
|
|
344
|
+
## Credits
|
|
345
|
+
|
|
346
|
+
- [MunifTanjim/go-ptt](https://github.com/MunifTanjim/go-ptt)
|
|
347
|
+
- [TheBeastLT/parse-torrent-title](https://github.com/TheBeastLT/parse-torrent-title)
|
|
348
|
+
- [dreulavelle/PTT](https://github.com/dreulavelle/PTT)
|
package/dist/handlers.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"handlers.d.ts","sourceRoot":"","sources":["../src/handlers.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EAAE,OAAO,EAAY,MAAM,YAAY,CAAC;
|
|
1
|
+
{"version":3,"file":"handlers.d.ts","sourceRoot":"","sources":["../src/handlers.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EAAE,OAAO,EAAY,MAAM,YAAY,CAAC;AAgC/C;;;;GAIG;AACH,eAAO,MAAM,QAAQ,EAAE,OAAO,EA+7F7B,CAAC"}
|
package/dist/handlers.js
CHANGED
|
@@ -6,7 +6,9 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import { ValueSet } from './types.js';
|
|
8
8
|
import { nonAlphasRegex } from './utils.js';
|
|
9
|
-
import {
|
|
9
|
+
import { toValue, toLowercase, toUppercase, toTrimmed, toCleanDate, toCleanMonth, toDate, toYear, toIntRange, toWithSuffix, toBoolean, toValueSet, toValueSetWithTransform, toValueSetMultiWithTransform, toIntArray } from './transforms.js';
|
|
10
|
+
import { validateAnd, validateNotAtStart, validateNotAtEnd, validateNotMatch, validateMatch, validateMatchedGroupsAreSame, validateLookbehind, validateOr, validateLookahead } from './validators.js';
|
|
11
|
+
import { removeFromValue } from './processors.js';
|
|
10
12
|
/**
|
|
11
13
|
* All handlers in the exact order as handlers.go
|
|
12
14
|
*
|
|
@@ -27,13 +29,15 @@ export const handlers = [
|
|
|
27
29
|
// PPV handlers (lines 294-300 in handlers.go)
|
|
28
30
|
{
|
|
29
31
|
field: 'ppv',
|
|
30
|
-
pattern: /\bPPV
|
|
32
|
+
pattern: /\bPPV(?:HD)?\b/i,
|
|
33
|
+
transform: toBoolean(),
|
|
31
34
|
remove: true,
|
|
32
35
|
skipFromTitle: true
|
|
33
36
|
},
|
|
34
37
|
{
|
|
35
38
|
field: 'ppv',
|
|
36
39
|
pattern: /\b\W?Fight.?Nights?\W?\b/i,
|
|
40
|
+
transform: toBoolean(),
|
|
37
41
|
skipFromTitle: true
|
|
38
42
|
},
|
|
39
43
|
// Site handlers (lines 302-317 in handlers.go)
|
|
@@ -213,7 +217,9 @@ export const handlers = [
|
|
|
213
217
|
pattern: /[ .]?([(\[*]?((?:19\d|20[012])\d[ .]?-[ .]?(?:19\d|20[012])\d)[*)\]]?)[ .]?/,
|
|
214
218
|
transform: (title, m, result) => {
|
|
215
219
|
toYear()(title, m, result);
|
|
216
|
-
if (!result.has('complete') &&
|
|
220
|
+
if (!result.has('complete') &&
|
|
221
|
+
typeof m.value === 'string' &&
|
|
222
|
+
m.value.includes('-')) {
|
|
217
223
|
result.set('complete', {
|
|
218
224
|
mIndex: m.mIndex,
|
|
219
225
|
mValue: m.mValue,
|
|
@@ -232,7 +238,9 @@ export const handlers = [
|
|
|
232
238
|
pattern: /[(\[*][ .]?((?:19\d|20[012])\d[ .]?-[ .]?\d{2})(?:\s?[*)\]])?/,
|
|
233
239
|
transform: (title, m, result) => {
|
|
234
240
|
toYear()(title, m, result);
|
|
235
|
-
if (!result.has('complete') &&
|
|
241
|
+
if (!result.has('complete') &&
|
|
242
|
+
typeof m.value === 'string' &&
|
|
243
|
+
m.value.includes('-')) {
|
|
236
244
|
result.set('complete', {
|
|
237
245
|
mIndex: m.mIndex,
|
|
238
246
|
mValue: m.mValue,
|
|
@@ -296,7 +304,7 @@ export const handlers = [
|
|
|
296
304
|
field: 'edition',
|
|
297
305
|
pattern: /\b\d{2,3}(?:th)?[\.\s\-\+_\/(),]Anniversary[\.\s\-\+_\/(),](?:Edition|Ed)?\b/i,
|
|
298
306
|
transform: toValue('Anniversary Edition'),
|
|
299
|
-
remove: true
|
|
307
|
+
remove: true
|
|
300
308
|
},
|
|
301
309
|
{
|
|
302
310
|
field: 'edition',
|
|
@@ -672,6 +680,12 @@ export const handlers = [
|
|
|
672
680
|
transform: toValue('WEBRip'),
|
|
673
681
|
remove: true
|
|
674
682
|
},
|
|
683
|
+
{
|
|
684
|
+
field: 'quality',
|
|
685
|
+
pattern: /\bWEB[ .-]?Cap\b/i,
|
|
686
|
+
transform: toValue('WEBCap'),
|
|
687
|
+
remove: true
|
|
688
|
+
},
|
|
675
689
|
{
|
|
676
690
|
field: 'quality',
|
|
677
691
|
pattern: /\bWEB[ .-]?DL[ .-]?Rip\b/i,
|
|
@@ -691,7 +705,9 @@ export const handlers = [
|
|
|
691
705
|
},
|
|
692
706
|
{
|
|
693
707
|
field: 'quality',
|
|
694
|
-
pattern: /\b(
|
|
708
|
+
pattern: /\b(W(?:ORK)P(?:RINT))\b/,
|
|
709
|
+
transform: toValue('WORKPRINT'),
|
|
710
|
+
remove: true
|
|
695
711
|
},
|
|
696
712
|
{
|
|
697
713
|
field: 'quality',
|
|
@@ -709,10 +725,16 @@ export const handlers = [
|
|
|
709
725
|
},
|
|
710
726
|
{
|
|
711
727
|
field: 'quality',
|
|
712
|
-
pattern: /\bHD(?:.?TV)?\b/i,
|
|
728
|
+
pattern: /\bHD(?:.?TV)?\b(?!-ELITE\.NET)/i,
|
|
713
729
|
transform: toValue('HDTV'),
|
|
714
730
|
remove: true
|
|
715
731
|
},
|
|
732
|
+
{
|
|
733
|
+
field: 'quality',
|
|
734
|
+
pattern: /\bSD(?:.?TV)?\b/i,
|
|
735
|
+
transform: toValue('SDTV'),
|
|
736
|
+
remove: true
|
|
737
|
+
},
|
|
716
738
|
// Bit Depth handlers (lines 1056-1077 in handlers.go)
|
|
717
739
|
{
|
|
718
740
|
field: 'bitDepth',
|
|
@@ -1568,8 +1590,11 @@ export const handlers = [
|
|
|
1568
1590
|
let mStr = '';
|
|
1569
1591
|
if (match && match.index !== undefined) {
|
|
1570
1592
|
mStr = match[0];
|
|
1571
|
-
if (match.index === 0 ||
|
|
1572
|
-
|
|
1593
|
+
if (match.index === 0 ||
|
|
1594
|
+
btReNegBefore.test(mStr) ||
|
|
1595
|
+
btReNegAfter.test(mStr) ||
|
|
1596
|
+
commonResolutionNeg.test(mStr) ||
|
|
1597
|
+
commonFPSNeg.test(mStr)) {
|
|
1573
1598
|
match = null;
|
|
1574
1599
|
mStr = '';
|
|
1575
1600
|
}
|
|
@@ -1583,7 +1608,8 @@ export const handlers = [
|
|
|
1583
1608
|
// Check from the start of capture group 1 (the number) to the end
|
|
1584
1609
|
const captureGroupIndex = match[0].indexOf(match[1]);
|
|
1585
1610
|
const fromCaptureGroup = middleTitle.substring(match.index + captureGroupIndex);
|
|
1586
|
-
if (mtReNegAfter.test(fromCaptureGroup) ||
|
|
1611
|
+
if (mtReNegAfter.test(fromCaptureGroup) ||
|
|
1612
|
+
commonResolutionNeg.test(mStr)) {
|
|
1587
1613
|
match = null;
|
|
1588
1614
|
mStr = '';
|
|
1589
1615
|
}
|
|
@@ -1761,22 +1787,24 @@ export const handlers = [
|
|
|
1761
1787
|
// French language handlers
|
|
1762
1788
|
{
|
|
1763
1789
|
field: 'languages',
|
|
1764
|
-
pattern: /\bFR(?:a|e|anc[eê]s|VF[FQIB2]?)
|
|
1790
|
+
pattern: /\bFR(?:a|e|anc[eê]s|VF[FQIB2]?)\b/i,
|
|
1765
1791
|
transform: toValueSet('fr'),
|
|
1766
1792
|
keepMatching: true,
|
|
1767
1793
|
skipFromTitle: true
|
|
1768
1794
|
},
|
|
1769
1795
|
{
|
|
1770
1796
|
field: 'languages',
|
|
1771
|
-
pattern: /\b(?:TRUE|SUB).?FRENCH\b|\bFRENCH\b/,
|
|
1797
|
+
pattern: /\b(?:TRUE|SUB).?FRENCH\b|\bFRENCH\b|\bFre?\b/,
|
|
1772
1798
|
transform: toValueSet('fr'),
|
|
1773
|
-
keepMatching: true
|
|
1799
|
+
keepMatching: true,
|
|
1800
|
+
remove: true
|
|
1774
1801
|
},
|
|
1775
1802
|
{
|
|
1776
1803
|
field: 'languages',
|
|
1777
1804
|
pattern: /\b\[?(?:VF[FQRIB2]?\]?\b|(?:VOST)?FR2?)\b/,
|
|
1778
1805
|
transform: toValueSet('fr'),
|
|
1779
|
-
keepMatching: true
|
|
1806
|
+
keepMatching: true,
|
|
1807
|
+
remove: true
|
|
1780
1808
|
},
|
|
1781
1809
|
{
|
|
1782
1810
|
field: 'languages',
|
|
@@ -1801,24 +1829,9 @@ export const handlers = [
|
|
|
1801
1829
|
},
|
|
1802
1830
|
{
|
|
1803
1831
|
field: 'languages',
|
|
1804
|
-
pattern: /\b(?:audio.)?(?:ESP
|
|
1832
|
+
pattern: /\b(?:audio.)?(?:ESP?|spa|(?:en[ .]+)?espa[nñ]ola?|castellano)\b/i,
|
|
1805
1833
|
transform: toValueSet('es'),
|
|
1806
|
-
keepMatching: true
|
|
1807
|
-
remove: true
|
|
1808
|
-
},
|
|
1809
|
-
{
|
|
1810
|
-
field: 'languages',
|
|
1811
|
-
pattern: /\b(?:[ .,/-]+(?:[A-Z]{2}[ .,/-]+){2,})es\b/i,
|
|
1812
|
-
transform: toValueSet('es'),
|
|
1813
|
-
keepMatching: true,
|
|
1814
|
-
skipFromTitle: true
|
|
1815
|
-
},
|
|
1816
|
-
{
|
|
1817
|
-
field: 'languages',
|
|
1818
|
-
pattern: /\b(?:[ .,/-]*[A-Z]{2}[ .,/-]+)es(?:[ .,/-]+[A-Z]{2}[ .,/-]+)\b/i,
|
|
1819
|
-
transform: toValueSet('es'),
|
|
1820
|
-
keepMatching: true,
|
|
1821
|
-
skipFromTitle: true
|
|
1834
|
+
keepMatching: true
|
|
1822
1835
|
},
|
|
1823
1836
|
{
|
|
1824
1837
|
field: 'languages',
|
|
@@ -1840,6 +1853,13 @@ export const handlers = [
|
|
|
1840
1853
|
keepMatching: true,
|
|
1841
1854
|
skipIfFirst: true
|
|
1842
1855
|
},
|
|
1856
|
+
{
|
|
1857
|
+
field: 'languages',
|
|
1858
|
+
pattern: /\b[\.\s\[]?Sp[\.\s\]]?\b/i,
|
|
1859
|
+
transform: toValueSet('es'),
|
|
1860
|
+
keepMatching: true,
|
|
1861
|
+
remove: true
|
|
1862
|
+
},
|
|
1843
1863
|
// Portuguese language handlers
|
|
1844
1864
|
{
|
|
1845
1865
|
field: 'languages',
|
|
@@ -1886,6 +1906,13 @@ export const handlers = [
|
|
|
1886
1906
|
keepMatching: true,
|
|
1887
1907
|
skipFromTitle: true
|
|
1888
1908
|
},
|
|
1909
|
+
{
|
|
1910
|
+
field: 'languages',
|
|
1911
|
+
pattern: /\bpt\b/i,
|
|
1912
|
+
transform: toValueSet('pt'),
|
|
1913
|
+
keepMatching: true,
|
|
1914
|
+
remove: true
|
|
1915
|
+
},
|
|
1889
1916
|
{
|
|
1890
1917
|
field: 'languages',
|
|
1891
1918
|
pattern: /\bpor\b/i,
|
|
@@ -1902,15 +1929,16 @@ export const handlers = [
|
|
|
1902
1929
|
},
|
|
1903
1930
|
{
|
|
1904
1931
|
field: 'languages',
|
|
1905
|
-
pattern: /\b
|
|
1906
|
-
validateMatch:
|
|
1932
|
+
pattern: /\bIT\b/i,
|
|
1933
|
+
validateMatch: validateAnd(validateLookbehind('(?:w{3}\\.\\w+\\.)', 'i', false), validateOr(validateLookahead('(?:[ .,/-]+(?:[A-Z]{2}[ .,/-]+){2,})', 'i', true), validateLookbehind('(?:(?:[ .,/-]*[A-Z]{2}){2,}[ .,/-]+)', 'i', true))),
|
|
1907
1934
|
transform: toValueSet('it'),
|
|
1908
1935
|
keepMatching: true,
|
|
1909
1936
|
skipFromTitle: true
|
|
1910
1937
|
},
|
|
1911
1938
|
{
|
|
1912
1939
|
field: 'languages',
|
|
1913
|
-
pattern: /\bit
|
|
1940
|
+
pattern: /\bit/i,
|
|
1941
|
+
validateMatch: validateLookahead('(?:\\.(?:ass|ssa|srt|sub|idx)$)', 'i', true),
|
|
1914
1942
|
transform: toValueSet('it'),
|
|
1915
1943
|
keepMatching: true,
|
|
1916
1944
|
skipFromTitle: true
|
|
@@ -1940,15 +1968,25 @@ export const handlers = [
|
|
|
1940
1968
|
},
|
|
1941
1969
|
{
|
|
1942
1970
|
field: 'languages',
|
|
1943
|
-
pattern: /\bde
|
|
1971
|
+
pattern: /\bde\b/i,
|
|
1972
|
+
validateMatch: validateLookahead('(?:[ .,/-]+(?:[A-Z]{2}[ .,/-]+){2,})', 'i', true),
|
|
1944
1973
|
transform: toValueSet('de'),
|
|
1945
1974
|
keepMatching: true,
|
|
1946
1975
|
skipFromTitle: true
|
|
1947
1976
|
},
|
|
1948
1977
|
{
|
|
1949
1978
|
field: 'languages',
|
|
1950
|
-
pattern: /\
|
|
1979
|
+
pattern: /\bde\b/i,
|
|
1951
1980
|
transform: toValueSet('de'),
|
|
1981
|
+
validateMatch: validateLookbehind('(?:[ .,/-]+(?:[A-Z]{2}[ .,/-]+){2,})', 'i', true),
|
|
1982
|
+
keepMatching: true,
|
|
1983
|
+
skipFromTitle: true
|
|
1984
|
+
},
|
|
1985
|
+
{
|
|
1986
|
+
field: 'languages',
|
|
1987
|
+
pattern: /\bde\b/i,
|
|
1988
|
+
transform: toValueSet('de'),
|
|
1989
|
+
validateMatch: validateAnd(validateLookbehind('(?:[ .,/-]+[A-Z]{2}[ .,/-]+)', 'i', true), validateLookahead('(?:[ .,/-]+[A-Z]{2}[ .,/-]+)', 'i', true)),
|
|
1952
1990
|
keepMatching: true,
|
|
1953
1991
|
skipFromTitle: true
|
|
1954
1992
|
},
|
|
@@ -2534,6 +2572,12 @@ export const handlers = [
|
|
|
2534
2572
|
remove: true,
|
|
2535
2573
|
skipFromTitle: true
|
|
2536
2574
|
},
|
|
2575
|
+
{
|
|
2576
|
+
field: 'dubbed',
|
|
2577
|
+
pattern: /\bMULTi\b/i,
|
|
2578
|
+
transform: toBoolean(),
|
|
2579
|
+
remove: true
|
|
2580
|
+
},
|
|
2537
2581
|
{
|
|
2538
2582
|
field: 'dubbed',
|
|
2539
2583
|
pattern: /\b(?:Fan.*)?(?:DUBBED|dublado|dubbing|DUBS?)\b/i,
|
|
@@ -2560,7 +2604,9 @@ export const handlers = [
|
|
|
2560
2604
|
return m;
|
|
2561
2605
|
}
|
|
2562
2606
|
const s = lm.value;
|
|
2563
|
-
if (s &&
|
|
2607
|
+
if (s &&
|
|
2608
|
+
s.exists &&
|
|
2609
|
+
(s.exists('multi audio') || s.exists('dual audio'))) {
|
|
2564
2610
|
m.value = true;
|
|
2565
2611
|
}
|
|
2566
2612
|
return m;
|
|
@@ -2575,7 +2621,21 @@ export const handlers = [
|
|
|
2575
2621
|
// Site handlers
|
|
2576
2622
|
{
|
|
2577
2623
|
field: 'site',
|
|
2578
|
-
pattern: /\[
|
|
2624
|
+
pattern: /\[eztv\]/i,
|
|
2625
|
+
transform: toValue('eztv.re'),
|
|
2626
|
+
remove: true,
|
|
2627
|
+
skipFromTitle: true
|
|
2628
|
+
},
|
|
2629
|
+
{
|
|
2630
|
+
field: 'site',
|
|
2631
|
+
pattern: /\beztv\b/i,
|
|
2632
|
+
transform: toValue('eztv.re'),
|
|
2633
|
+
remove: true,
|
|
2634
|
+
skipFromTitle: true
|
|
2635
|
+
},
|
|
2636
|
+
{
|
|
2637
|
+
field: 'site',
|
|
2638
|
+
pattern: /^\[([^\[\]]+\.[^\[\]]+)\]/i,
|
|
2579
2639
|
transform: toTrimmed(),
|
|
2580
2640
|
remove: true,
|
|
2581
2641
|
skipFromTitle: true,
|
|
@@ -2583,9 +2643,27 @@ export const handlers = [
|
|
|
2583
2643
|
},
|
|
2584
2644
|
{
|
|
2585
2645
|
field: 'site',
|
|
2586
|
-
pattern:
|
|
2646
|
+
pattern: /-(www\.[\w-]+\.[\w-]+(?:\.[\w-]+)*)\.(\w{2,4})$/i,
|
|
2647
|
+
transform: toTrimmed(),
|
|
2587
2648
|
remove: true,
|
|
2588
|
-
skipFromTitle: true
|
|
2649
|
+
skipFromTitle: true,
|
|
2650
|
+
matchGroup: 1
|
|
2651
|
+
},
|
|
2652
|
+
{
|
|
2653
|
+
field: 'site',
|
|
2654
|
+
pattern: /\[([^\[\].]+\.[^\].]+)\](?:\.\w{2,4})?(?:$|\s)/i,
|
|
2655
|
+
transform: toTrimmed(),
|
|
2656
|
+
remove: true,
|
|
2657
|
+
skipFromTitle: true,
|
|
2658
|
+
matchGroup: 1
|
|
2659
|
+
},
|
|
2660
|
+
{
|
|
2661
|
+
field: 'site',
|
|
2662
|
+
pattern: /[\[{(](www\.[\w-]+\.[\w-]+(?:\.[\w-]+)*)[)}\]]/i,
|
|
2663
|
+
transform: toTrimmed(),
|
|
2664
|
+
remove: true,
|
|
2665
|
+
skipFromTitle: true,
|
|
2666
|
+
matchGroup: 1
|
|
2589
2667
|
},
|
|
2590
2668
|
{
|
|
2591
2669
|
field: 'site',
|