@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 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)
@@ -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;AA2B/C;;;;GAIG;AACH,eAAO,MAAM,QAAQ,EAAE,OAAO,EAuxF7B,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 { validateAnd, validateNotAtStart, validateNotAtEnd, validateNotMatch, validateMatch, validateMatchedGroupsAreSame, toValue, toLowercase, toUppercase, toTrimmed, toCleanDate, toCleanMonth, toDate, toYear, toIntRange, toWithSuffix, toBoolean, toValueSet, toValueSetWithTransform, toValueSetMultiWithTransform, toIntArray, removeFromValue } from './transforms.js';
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\b/i,
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') && typeof m.value === 'string' && m.value.includes('-')) {
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') && typeof m.value === 'string' && m.value.includes('-')) {
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(DivX|XviD)\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 || btReNegBefore.test(mStr) || btReNegAfter.test(mStr) ||
1572
- commonResolutionNeg.test(mStr) || commonFPSNeg.test(mStr)) {
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) || commonResolutionNeg.test(mStr)) {
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]?)?\b/i,
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|spa|(?:en[ .]+)?espa[nñ]ola?|castellano)\b/i,
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(?:w{3}\.\w+\.)?IT(?:[ .,/-]+(?:[a-zA-Z]{2}[ .,/-]+){2,})\b/,
1906
- validateMatch: validateNotMatch(/(?:w{3}\.\w+\.)IT/),
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(?:\.(?:ass|ssa|srt|sub|idx)$)/i,
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(?:[ .,/-]+(?:[A-Z]{2}[ .,/-]+){2,})\b/i,
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: /\b(?:[ .,/-]+(?:[A-Z]{2}[ .,/-]+){2,})de\b/i,
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 && s.exists && (s.exists('multi audio') || s.exists('dual audio'))) {
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: /\[([^\[\].]+\.[^\].]+)\](?:\.\w{2,4}$|\s)/i,
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: /[\[{(](www.\w*.\w+)[)}\]]/i,
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',