@viren070/parse-torrent-title 0.1.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.
@@ -0,0 +1,2744 @@
1
+ /**
2
+ * Handler definitions matching handlers.go
3
+ *
4
+ * IMPORTANT: The order of handlers matters! They must match the exact order in handlers.go
5
+ * to ensure 1:1 parsing behavior.
6
+ */
7
+ import { ValueSet } from './types.js';
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';
10
+ /**
11
+ * All handlers in the exact order as handlers.go
12
+ *
13
+ * Start porting from line 284 of handlers.go
14
+ */
15
+ export const handlers = [
16
+ // Title handlers (lines 285-292 in handlers.go)
17
+ {
18
+ field: 'title',
19
+ pattern: /360.Degrees.of.Vision.The.Byakugan'?s.Blind.Spot/i,
20
+ remove: true
21
+ },
22
+ {
23
+ field: 'title',
24
+ pattern: /\b(?:INTERNAL|HFR)\b/i,
25
+ remove: true
26
+ },
27
+ // PPV handlers (lines 294-300 in handlers.go)
28
+ {
29
+ field: 'ppv',
30
+ pattern: /\bPPV\b/i,
31
+ remove: true,
32
+ skipFromTitle: true
33
+ },
34
+ {
35
+ field: 'ppv',
36
+ pattern: /\b\W?Fight.?Nights?\W?\b/i,
37
+ skipFromTitle: true
38
+ },
39
+ // Site handlers (lines 302-317 in handlers.go)
40
+ {
41
+ field: 'site',
42
+ pattern: /^(www?[., ][\w-]+[. ][\w-]+(?:[. ][\w-]+)?)\s+-\s*/i,
43
+ keepMatching: true,
44
+ skipFromTitle: true,
45
+ remove: true
46
+ },
47
+ {
48
+ field: 'site',
49
+ pattern: /^((?:www?[\.,])?[\w-]+\.[\w-]+(?:\.[\w-]+)*?)\s+-\s*/i,
50
+ keepMatching: true
51
+ },
52
+ {
53
+ field: 'site',
54
+ pattern: /\bwww[., ][\w-]+[., ](?:rodeo|hair)\b/i,
55
+ remove: true,
56
+ skipFromTitle: true
57
+ },
58
+ // Episode Code handlers (lines 319-328 in handlers.go)
59
+ {
60
+ field: 'episodeCode',
61
+ pattern: /([\[(]([a-z0-9]{8}|[A-Z0-9]{8})[\])])(?:\.[a-zA-Z0-9]{1,5}$|$)/,
62
+ transform: toUppercase(),
63
+ remove: true,
64
+ matchGroup: 1,
65
+ valueGroup: 2
66
+ },
67
+ {
68
+ field: 'episodeCode',
69
+ pattern: /\[([A-Z0-9]{8})]/,
70
+ validateMatch: validateMatch(/(?:[A-Z]+\d|\d+[A-Z])/),
71
+ transform: toUppercase(),
72
+ remove: true
73
+ },
74
+ // Resolution handlers (lines 330-378 in handlers.go)
75
+ {
76
+ field: 'resolution',
77
+ pattern: /\b(?:4k|2160p|1080p|720p|480p)\b.+\b(4k|2160p|1080p|720p|480p)\b/i,
78
+ transform: toLowercase(),
79
+ remove: true,
80
+ matchGroup: 1
81
+ },
82
+ {
83
+ field: 'resolution',
84
+ pattern: /\b[(\[]?4k[)\]]?\b/i,
85
+ transform: toValue('4k'),
86
+ remove: true
87
+ },
88
+ {
89
+ field: 'resolution',
90
+ pattern: /21600?[pi]/i,
91
+ transform: toValue('4k'),
92
+ remove: true,
93
+ keepMatching: true
94
+ },
95
+ {
96
+ field: 'resolution',
97
+ pattern: /[(\[]?3840x\d{4}[)\]]?/i,
98
+ transform: toValue('4k'),
99
+ remove: true
100
+ },
101
+ {
102
+ field: 'resolution',
103
+ pattern: /[(\[]?1920x\d{3,4}[)\]]?/i,
104
+ transform: toValue('1080p'),
105
+ remove: true
106
+ },
107
+ {
108
+ field: 'resolution',
109
+ pattern: /[(\[]?1280x\d{3}[)\]]?/i,
110
+ transform: toValue('720p'),
111
+ remove: true
112
+ },
113
+ {
114
+ field: 'resolution',
115
+ pattern: /[(\[]?\d{3,4}x(\d{3,4})[)\]]?/i,
116
+ transform: toWithSuffix('p'),
117
+ remove: true
118
+ },
119
+ {
120
+ field: 'resolution',
121
+ pattern: /(480|720|1080)0[pi]/i,
122
+ transform: toWithSuffix('p'),
123
+ remove: true
124
+ },
125
+ {
126
+ field: 'resolution',
127
+ pattern: /(?:BD|HD|M)(720|1080|2160)/i,
128
+ transform: toWithSuffix('p'),
129
+ remove: true
130
+ },
131
+ {
132
+ field: 'resolution',
133
+ pattern: /(480|576|720|1080|2160)[pi]/i,
134
+ transform: toWithSuffix('p'),
135
+ remove: true
136
+ },
137
+ {
138
+ field: 'resolution',
139
+ pattern: /(?:^|\D)(\d{3,4})[pi]/i,
140
+ transform: toWithSuffix('p'),
141
+ remove: true
142
+ },
143
+ // Date handlers (lines 380-451 in handlers.go)
144
+ {
145
+ field: 'date',
146
+ pattern: /(?:\W|^)([(\[]?((?:19[6-9]|20[012])[0-9]([. \-/\\])(?:0[1-9]|1[012])([. \-/\\])(?:0[1-9]|[12][0-9]|3[01]))[)\]]?)(?:\W|$)/,
147
+ validateMatch: validateMatchedGroupsAreSame(3, 4),
148
+ transform: toDate('2006 01 02'),
149
+ remove: true,
150
+ valueGroup: 2,
151
+ matchGroup: 1
152
+ },
153
+ {
154
+ field: 'date',
155
+ pattern: /(?:\W|^)[(\[]?((?:0[1-9]|[12][0-9]|3[01])([. \-/\\])(?:0[1-9]|1[012])([. \-/\\])(?:19[6-9]|20[012])[0-9])[)\]]?(?:\W|$)/,
156
+ validateMatch: validateMatchedGroupsAreSame(2, 3),
157
+ transform: toDate('02 01 2006'),
158
+ remove: true
159
+ },
160
+ {
161
+ field: 'date',
162
+ pattern: /(?:\W)[(\[]?((?:0[1-9]|1[012])([. \-/\\])(?:0[1-9]|[12][0-9]|3[01])([. \-/\\])(?:19[6-9]|20[012])[0-9])[)\]]?(?:\W|$)/,
163
+ validateMatch: validateMatchedGroupsAreSame(2, 3),
164
+ transform: toDate('01 02 2006'),
165
+ remove: true
166
+ },
167
+ {
168
+ field: 'date',
169
+ pattern: /(?:\W)[(\[]?((?:0[1-9]|1[012])([. \-/\\])(?:0[1-9]|[12][0-9]|3[01])([. \-/\\])(?:[0][1-9]|[0126789][0-9]))[)\]]?(?:\W|$)/,
170
+ validateMatch: validateMatchedGroupsAreSame(2, 3),
171
+ transform: toDate('01 02 06'),
172
+ remove: true
173
+ },
174
+ {
175
+ field: 'date',
176
+ pattern: /(?:\W)[(\[]?((?:0[1-9]|[12][0-9]|3[01])([. \-/\\])(?:0[1-9]|1[012])([. \-/\\])(?:[0][1-9]|[0126789][0-9]))[)\]]?(?:\W|$)/,
177
+ validateMatch: validateMatchedGroupsAreSame(2, 3),
178
+ transform: toDate('02 01 06'),
179
+ matchGroup: 1,
180
+ remove: true
181
+ },
182
+ {
183
+ field: 'date',
184
+ pattern: /(?:\W|^)[(\[]?((?:0?[1-9]|[12][0-9]|3[01])[. ]?(?:st|nd|rd|th)?([. \-/\\])(?:feb(?:ruary)?|jan(?:uary)?|mar(?:ch)?|apr(?:il)?|may|june?|july?|aug(?:ust)?|sept?(?:ember)?|oct(?:ober)?|nov(?:ember)?|dec(?:ember)?)([. \-/\\])(?:19[7-9]|20[012])[0-9])[)\]]?(?:\W|$)/i,
185
+ validateMatch: validateMatchedGroupsAreSame(2, 3),
186
+ transform: (title, m, result) => {
187
+ toCleanDate()(title, m, result);
188
+ toCleanMonth()(title, m, result);
189
+ toDate('_2 Jan 2006')(title, m, result);
190
+ },
191
+ remove: true
192
+ },
193
+ {
194
+ field: 'date',
195
+ pattern: /(?:\W|^)[(\[]?((?:0?[1-9]|[12][0-9]|3[01])[. ]?(?:st|nd|rd|th)?([. \-/\\])(?:feb(?:ruary)?|jan(?:uary)?|mar(?:ch)?|apr(?:il)?|may|june?|july?|aug(?:ust)?|sept?(?:ember)?|oct(?:ober)?|nov(?:ember)?|dec(?:ember)?)([. \-/\\])(?:0[1-9]|[0126789][0-9]))[)\]]?(?:\W|$)/i,
196
+ validateMatch: validateMatchedGroupsAreSame(2, 3),
197
+ transform: (title, m, result) => {
198
+ toCleanDate()(title, m, result);
199
+ toCleanMonth()(title, m, result);
200
+ toDate('_2 Jan 06')(title, m, result);
201
+ },
202
+ remove: true
203
+ },
204
+ {
205
+ field: 'date',
206
+ pattern: /(?:\W|^)[(\[]?(20[012][0-9](?:0[1-9]|1[012])(?:0[1-9]|[12][0-9]|3[01]))[)\]]?(?:\W|$)/,
207
+ transform: toDate('20060102'),
208
+ remove: true
209
+ },
210
+ // Year handlers (lines 456-542 in handlers.go)
211
+ {
212
+ field: 'year',
213
+ pattern: /[ .]?([(\[*]?((?:19\d|20[012])\d[ .]?-[ .]?(?:19\d|20[012])\d)[*)\]]?)[ .]?/,
214
+ transform: (title, m, result) => {
215
+ toYear()(title, m, result);
216
+ if (!result.has('complete') && typeof m.value === 'string' && m.value.includes('-')) {
217
+ result.set('complete', {
218
+ mIndex: m.mIndex,
219
+ mValue: m.mValue,
220
+ value: true,
221
+ remove: false,
222
+ processed: false
223
+ });
224
+ }
225
+ },
226
+ matchGroup: 1,
227
+ valueGroup: 2,
228
+ remove: true
229
+ },
230
+ {
231
+ field: 'year',
232
+ pattern: /[(\[*][ .]?((?:19\d|20[012])\d[ .]?-[ .]?\d{2})(?:\s?[*)\]])?/,
233
+ transform: (title, m, result) => {
234
+ toYear()(title, m, result);
235
+ if (!result.has('complete') && typeof m.value === 'string' && m.value.includes('-')) {
236
+ result.set('complete', {
237
+ mIndex: m.mIndex,
238
+ mValue: m.mValue,
239
+ value: true,
240
+ remove: false,
241
+ processed: false
242
+ });
243
+ }
244
+ },
245
+ remove: true
246
+ },
247
+ {
248
+ field: 'year',
249
+ pattern: /[(\[*]?\b(20[0-9]{2}|2100)[*\])]?/,
250
+ validateMatch: (input, match) => {
251
+ const re = /(?:\D*\d{4}\b)/;
252
+ return !re.test(input.substring(match[3]));
253
+ },
254
+ transform: toYear(),
255
+ remove: true
256
+ },
257
+ {
258
+ field: 'year',
259
+ pattern: /(?:[(\[*]|.)((?:\d|Cap[. ]?)?(?:19\d|20[012])\d(?:\d|kbps)?)[*)\]]?/,
260
+ validateMatch: (input, match) => {
261
+ if (match[0] < 2) {
262
+ return false;
263
+ }
264
+ return input.substring(match[2], match[3]).length === 4;
265
+ },
266
+ transform: toYear(),
267
+ remove: true,
268
+ matchGroup: 1
269
+ },
270
+ {
271
+ field: 'year',
272
+ pattern: /^[(\[]?((?:19\d|20[012])\d)(?:\d|kbps)?[)\]]?/,
273
+ validateMatch: (input, match) => {
274
+ const mValue = input.substring(match[0], match[1]);
275
+ if (mValue.length === 4) {
276
+ return match[0] !== 0;
277
+ }
278
+ return mValue.replace(/[()[\]]/g, '').length === 4;
279
+ },
280
+ transform: toYear(),
281
+ remove: true
282
+ },
283
+ // Extended handlers (lines 544-549 in handlers.go)
284
+ {
285
+ field: 'extended',
286
+ pattern: /EXTENDED/,
287
+ transform: toBoolean()
288
+ },
289
+ {
290
+ field: 'extended',
291
+ pattern: /- Extended/i,
292
+ transform: toBoolean()
293
+ },
294
+ // Edition handlers (lines 551-606 in handlers.go)
295
+ {
296
+ field: 'edition',
297
+ pattern: /\b\d{2,3}(?:th)?[\.\s\-\+_\/(),]Anniversary[\.\s\-\+_\/(),](?:Edition|Ed)?\b/i,
298
+ transform: toValue('Anniversary Edition'),
299
+ remove: true,
300
+ },
301
+ {
302
+ field: 'edition',
303
+ pattern: /\bUltimate[\.\s\-\+_\/(),]Edition\b/i,
304
+ transform: toValue('Ultimate Edition'),
305
+ remove: true
306
+ },
307
+ {
308
+ field: 'edition',
309
+ pattern: /\bExtended[\.\s\-\+_\/(),]Director'?s\b/i,
310
+ transform: toValue("Director's Cut"),
311
+ remove: true
312
+ },
313
+ {
314
+ field: 'edition',
315
+ pattern: /\b(?:custom.?)?Extended\b/i,
316
+ transform: toValue('Extended Edition'),
317
+ remove: true
318
+ },
319
+ {
320
+ field: 'edition',
321
+ pattern: /\bDirector'?s.?Cut\b/i,
322
+ transform: toValue("Director's Cut"),
323
+ remove: true
324
+ },
325
+ {
326
+ field: 'edition',
327
+ pattern: /\bCollector'?s\b/i,
328
+ transform: toValue("Collector's Edition"),
329
+ remove: true
330
+ },
331
+ {
332
+ field: 'edition',
333
+ pattern: /\bTheatrical\b/i,
334
+ transform: toValue('Theatrical'),
335
+ remove: true
336
+ },
337
+ {
338
+ field: 'edition',
339
+ pattern: /\buncut(?:.gems)?\b/i,
340
+ validateMatch: validateNotMatch(/(?:.gems)/i),
341
+ transform: toValue('Uncut'),
342
+ remove: true
343
+ },
344
+ {
345
+ field: 'edition',
346
+ pattern: /\bIMAX\b/i,
347
+ transform: toValue('IMAX'),
348
+ remove: true,
349
+ skipFromTitle: true
350
+ },
351
+ {
352
+ field: 'edition',
353
+ pattern: /\b\.Diamond\.\b/i,
354
+ transform: toValue('Diamond Edition'),
355
+ remove: true
356
+ },
357
+ {
358
+ field: 'edition',
359
+ pattern: /\bRemaster(?:ed)?\b|\b[\[(]?REKONSTRUKCJA[\])]?\b/i,
360
+ transform: toValue('Remastered'),
361
+ keepMatching: true,
362
+ remove: true
363
+ },
364
+ {
365
+ field: 'edition',
366
+ process: (title, m, result) => {
367
+ if (m.value === 'Remastered') {
368
+ if (!result.has('remastered')) {
369
+ result.set('remastered', {
370
+ mIndex: m.mIndex,
371
+ mValue: m.mValue,
372
+ value: true,
373
+ remove: false,
374
+ processed: false
375
+ });
376
+ }
377
+ }
378
+ return m;
379
+ }
380
+ },
381
+ // Release Types handlers (lines 608-623 in handlers.go)
382
+ {
383
+ field: 'releaseTypes',
384
+ pattern: /\b((?:OAD|OAV|ODA|ONA|OVA)\b(?:[+&]\b(?:OAD|OAV|ODA|ONA|OVA)\b)?)/i,
385
+ transform: toValueSetMultiWithTransform((v) => {
386
+ const values = [];
387
+ for (const part of v.split(nonAlphasRegex)) {
388
+ if (part) {
389
+ values.push(part.toUpperCase());
390
+ }
391
+ }
392
+ return values;
393
+ }),
394
+ remove: true,
395
+ matchGroup: 1
396
+ },
397
+ {
398
+ field: 'releaseTypes',
399
+ pattern: /\b(OAD|OAV|ODA|ONA|OVA)(?:[ .-]*\d{1,3})?(?:v\d)?/i,
400
+ transform: toValueSetWithTransform((v) => v.toUpperCase()),
401
+ remove: true,
402
+ matchGroup: 1
403
+ },
404
+ // Upscaled handlers (lines 625-636 in handlers.go)
405
+ {
406
+ field: 'upscaled',
407
+ pattern: /\b(?:AI.?)?(Upscal(ed?|ing)|Enhanced?)\b/i,
408
+ transform: toBoolean()
409
+ },
410
+ {
411
+ field: 'upscaled',
412
+ pattern: /\b(?:iris2|regrade|ups(?:uhd|fhd|hd|4k)?)\b/i,
413
+ transform: toBoolean()
414
+ },
415
+ {
416
+ field: 'upscaled',
417
+ pattern: /\b\.AI\.\b/i,
418
+ transform: toBoolean()
419
+ },
420
+ // Convert handler (lines 638-643 in handlers.go)
421
+ {
422
+ field: 'convert',
423
+ pattern: /\bCONVERT\b/,
424
+ transform: toBoolean(),
425
+ remove: true
426
+ },
427
+ // Hardcoded handler (lines 645-650 in handlers.go)
428
+ {
429
+ field: 'hardcoded',
430
+ pattern: /\bHC|HARDCODED\b/,
431
+ transform: toBoolean(),
432
+ remove: true
433
+ },
434
+ // Proper handler (lines 652-657 in handlers.go)
435
+ {
436
+ field: 'proper',
437
+ pattern: /\b(?:REAL.)?PROPER\b/i,
438
+ transform: toBoolean(),
439
+ remove: true
440
+ },
441
+ // Repack handler (lines 659-664 in handlers.go)
442
+ {
443
+ field: 'repack',
444
+ pattern: /\bREPACK|RERIP\b/i,
445
+ transform: toBoolean(),
446
+ remove: true
447
+ },
448
+ // Retail handler (lines 666-670 in handlers.go)
449
+ {
450
+ field: 'retail',
451
+ pattern: /\bRetail\b/i,
452
+ transform: toBoolean()
453
+ },
454
+ // Documentary handler (lines 672-677 in handlers.go)
455
+ {
456
+ field: 'documentary',
457
+ pattern: /\bDOCU(?:menta?ry)?\b/i,
458
+ transform: toBoolean(),
459
+ skipFromTitle: true
460
+ },
461
+ // Unrated handler (lines 679-684 in handlers.go)
462
+ {
463
+ field: 'unrated',
464
+ pattern: /\bunrated\b/i,
465
+ transform: toBoolean(),
466
+ remove: true
467
+ },
468
+ // Uncensored handler (lines 686-691 in handlers.go)
469
+ {
470
+ field: 'uncensored',
471
+ pattern: /\buncensored\b/i,
472
+ transform: toBoolean(),
473
+ remove: true
474
+ },
475
+ // Commentary handler (lines 693-698 in handlers.go)
476
+ {
477
+ field: 'commentary',
478
+ pattern: /\bcommentary\b/i,
479
+ transform: toBoolean(),
480
+ remove: true
481
+ },
482
+ // Region handlers (lines 700-710 in handlers.go)
483
+ {
484
+ field: 'region',
485
+ pattern: /R\dJ?\b/,
486
+ remove: true,
487
+ skipIfFirst: true
488
+ },
489
+ {
490
+ field: 'region',
491
+ pattern: /\b(PAL|NTSC|SECAM)\b/,
492
+ transform: toUppercase(),
493
+ remove: true
494
+ },
495
+ // Quality/Source handlers (lines 712-1054 in handlers.go)
496
+ {
497
+ field: 'quality',
498
+ pattern: /\b(?:H[DQ][ .-]*)?CAM(?:H[DQ])?(?:[ .-]*Rip)?\b/i,
499
+ transform: toValue('CAM'),
500
+ remove: true
501
+ },
502
+ {
503
+ field: 'quality',
504
+ pattern: /\b(?:H[DQ][ .-]*)?S[ .-]+print/i,
505
+ transform: toValue('CAM'),
506
+ remove: true
507
+ },
508
+ {
509
+ field: 'quality',
510
+ pattern: /\b(?:HD[ .-]*)?T(?:ELE)?S(?:YNC)?(?:Rip)?\b/i,
511
+ transform: toValue('TeleSync'),
512
+ remove: true
513
+ },
514
+ {
515
+ field: 'quality',
516
+ pattern: /\b(?:HD[ .-]*)?T(?:ELE)?C(?:INE)?(?:Rip)?\b/,
517
+ transform: toValue('TeleCine'),
518
+ remove: true
519
+ },
520
+ {
521
+ field: 'quality',
522
+ pattern: /\b(?:DVD?|BD|BR|HD)?[ .-]*Scr(?:eener)?\b/i,
523
+ transform: toValue('SCR'),
524
+ remove: true
525
+ },
526
+ {
527
+ field: 'quality',
528
+ pattern: /\bP(?:RE)?-?(HD|DVD)(?:Rip)?\b/i,
529
+ transform: toValue('SCR'),
530
+ remove: true
531
+ },
532
+ {
533
+ field: 'quality',
534
+ pattern: /\b(Blu[ .-]*Ray)\b(?:.*remux)/i,
535
+ transform: toValue('BluRay REMUX'),
536
+ remove: true,
537
+ matchGroup: 1
538
+ },
539
+ {
540
+ field: 'quality',
541
+ pattern: /(?:BD|BR|UHD)[- ]?remux/i,
542
+ transform: toValue('BluRay REMUX'),
543
+ remove: true
544
+ },
545
+ {
546
+ field: 'quality',
547
+ pattern: /(?:remux.*)\bBlu[ .-]*Ray\b/i,
548
+ transform: toValue('BluRay REMUX'),
549
+ remove: true
550
+ },
551
+ {
552
+ field: 'quality',
553
+ pattern: /\bremux\b/i,
554
+ transform: toValue('REMUX'),
555
+ remove: true
556
+ },
557
+ {
558
+ field: 'quality',
559
+ pattern: /\bBlu[ .-]*Ray\b(?:[ .-]*Rip)?/i,
560
+ validateMatch: (input, match) => {
561
+ return !input.substring(match[0], match[1]).toLowerCase().endsWith('rip');
562
+ },
563
+ transform: toValue('BluRay'),
564
+ remove: true
565
+ },
566
+ {
567
+ field: 'quality',
568
+ pattern: /\bUHD[ .-]*Rip\b/i,
569
+ transform: toValue('UHDRip'),
570
+ remove: true
571
+ },
572
+ {
573
+ field: 'quality',
574
+ pattern: /\bHD[ .-]*Rip\b/i,
575
+ transform: toValue('HDRip'),
576
+ remove: true
577
+ },
578
+ {
579
+ field: 'quality',
580
+ pattern: /\bMicro[ .-]*HD\b/i,
581
+ transform: toValue('HDRip'),
582
+ remove: true
583
+ },
584
+ {
585
+ field: 'quality',
586
+ pattern: /\b(?:BR|Blu[ .-]*Ray)[ .-]*Rip\b/i,
587
+ transform: toValue('BRRip'),
588
+ remove: true
589
+ },
590
+ {
591
+ field: 'quality',
592
+ pattern: /\bBD[ .-]*Rip\b|\bBDR\b|\bBD-RM\b|[\[(]BD[\]) .,-]/i,
593
+ transform: toValue('BDRip'),
594
+ remove: true
595
+ },
596
+ {
597
+ field: 'quality',
598
+ pattern: /\bVOD[ .-]*Rip\b/i,
599
+ transform: toValue('VODR'),
600
+ remove: true
601
+ },
602
+ {
603
+ field: 'quality',
604
+ pattern: /\b(?:HD[ .-]*)?DVD[ .-]*Rip\b/i,
605
+ transform: toValue('DVDRip'),
606
+ remove: true
607
+ },
608
+ {
609
+ field: 'quality',
610
+ pattern: /\bVHS[ .-]*Rip\b/i,
611
+ transform: toValue('VHSRip'),
612
+ remove: true
613
+ },
614
+ {
615
+ field: 'quality',
616
+ pattern: /\bDVD(?:R\d?)?\b/i,
617
+ transform: toValue('DVD'),
618
+ remove: true
619
+ },
620
+ {
621
+ field: 'quality',
622
+ pattern: /\bVHS\b/i,
623
+ transform: toValue('DVD'),
624
+ remove: true,
625
+ skipIfFirst: true
626
+ },
627
+ {
628
+ field: 'quality',
629
+ pattern: /\bPPV[ .-]*HD\b/i,
630
+ transform: toValue('PPV'),
631
+ remove: true
632
+ },
633
+ {
634
+ field: 'quality',
635
+ pattern: /\bPPVRip\b/i,
636
+ transform: toValue('PPVRip'),
637
+ remove: true
638
+ },
639
+ {
640
+ field: 'quality',
641
+ pattern: /\bHD.?TV.?Rip\b/i,
642
+ transform: toValue('HDTVRip'),
643
+ remove: true
644
+ },
645
+ {
646
+ field: 'quality',
647
+ pattern: /\bDVB[ .-]*(?:Rip)?\b/i,
648
+ transform: toValue('HDTV'),
649
+ remove: true
650
+ },
651
+ {
652
+ field: 'quality',
653
+ pattern: /\bSAT[ .-]*Rips?\b/i,
654
+ transform: toValue('SATRip'),
655
+ remove: true
656
+ },
657
+ {
658
+ field: 'quality',
659
+ pattern: /\bTVRips?\b/i,
660
+ transform: toValue('TVRip'),
661
+ remove: true
662
+ },
663
+ {
664
+ field: 'quality',
665
+ pattern: /\bR5\b/i,
666
+ transform: toValue('R5'),
667
+ remove: true
668
+ },
669
+ {
670
+ field: 'quality',
671
+ pattern: /\bWEB[ .-]*Rip\b/i,
672
+ transform: toValue('WEBRip'),
673
+ remove: true
674
+ },
675
+ {
676
+ field: 'quality',
677
+ pattern: /\bWEB[ .-]?DL[ .-]?Rip\b/i,
678
+ transform: toValue('WEB-DLRip'),
679
+ remove: true
680
+ },
681
+ {
682
+ field: 'quality',
683
+ pattern: /\bWEB[ .-]*(DL|.BDrip|.DLRIP)\b/i,
684
+ transform: toValue('WEB-DL'),
685
+ remove: true
686
+ },
687
+ {
688
+ field: 'quality',
689
+ pattern: /\b(?:DL|WEB|BD|BR)MUX\b/i,
690
+ remove: true
691
+ },
692
+ {
693
+ field: 'quality',
694
+ pattern: /\b(DivX|XviD)\b/
695
+ },
696
+ {
697
+ field: 'quality',
698
+ pattern: /\b(?:\w.)?WEB\b|\bWEB(?:(?:[ \.\-\(\],]+\d))?\b/i,
699
+ validateMatch: validateNotMatch(/\b(?:\w.)WEB\b|\bWEB(?:(?:[ \.\-\(\],]+\d))\b/i),
700
+ transform: toValue('WEB'),
701
+ remove: true,
702
+ skipFromTitle: true
703
+ },
704
+ {
705
+ field: 'quality',
706
+ pattern: /\bPDTV\b/i,
707
+ transform: toValue('PDTV'),
708
+ remove: true
709
+ },
710
+ {
711
+ field: 'quality',
712
+ pattern: /\bHD(?:.?TV)?\b/i,
713
+ transform: toValue('HDTV'),
714
+ remove: true
715
+ },
716
+ // Bit Depth handlers (lines 1056-1077 in handlers.go)
717
+ {
718
+ field: 'bitDepth',
719
+ pattern: /(?:8|10|12)[-.]?bit\b/i,
720
+ transform: toLowercase(),
721
+ remove: true
722
+ },
723
+ {
724
+ field: 'bitDepth',
725
+ pattern: /\bhevc\s?10\b/i,
726
+ transform: toValue('10bit')
727
+ },
728
+ {
729
+ field: 'bitDepth',
730
+ pattern: /\bhdr10(?:\+|plus)?\b/i,
731
+ transform: toValue('10bit')
732
+ },
733
+ {
734
+ field: 'bitDepth',
735
+ pattern: /\bhi10\b/i,
736
+ transform: toValue('10bit')
737
+ },
738
+ {
739
+ field: 'bitDepth',
740
+ process: removeFromValue(/[ -]/)
741
+ },
742
+ // HDR handlers (lines 1079-1101 in handlers.go)
743
+ {
744
+ field: 'hdr',
745
+ pattern: /\bDV\b|dolby.?vision|\bDoVi\b/i,
746
+ transform: toValueSet('DV'),
747
+ remove: true,
748
+ keepMatching: true
749
+ },
750
+ {
751
+ field: 'hdr',
752
+ pattern: /HDR10(?:\+|plus)/i,
753
+ transform: toValueSet('HDR10+'),
754
+ remove: true,
755
+ keepMatching: true
756
+ },
757
+ {
758
+ field: 'hdr',
759
+ pattern: /\bHDR(?:10)?\b/i,
760
+ transform: toValueSet('HDR'),
761
+ remove: true,
762
+ keepMatching: true
763
+ },
764
+ {
765
+ field: 'hdr',
766
+ pattern: /\bSDR\b/i,
767
+ transform: toValueSet('SDR'),
768
+ remove: true,
769
+ keepMatching: true
770
+ },
771
+ // 3D handlers (lines 1103-1138 in handlers.go)
772
+ {
773
+ field: 'threeD',
774
+ pattern: /\b(3D)\b.*\b(Half-?SBS|H[-\\/]?SBS)\b/i,
775
+ transform: toValue('3D HSBS')
776
+ },
777
+ {
778
+ field: 'threeD',
779
+ pattern: /\bHalf.Side.?By.?Side\b/i,
780
+ transform: toValue('3D HSBS')
781
+ },
782
+ {
783
+ field: 'threeD',
784
+ pattern: /\b(3D)\b.*\b(Full-?SBS|SBS)\b/i,
785
+ transform: toValue('3D SBS')
786
+ },
787
+ {
788
+ field: 'threeD',
789
+ pattern: /\bSide.?By.?Side\b/i,
790
+ transform: toValue('3D SBS')
791
+ },
792
+ {
793
+ field: 'threeD',
794
+ pattern: /\b(3D)\b.*\b(Half-?OU|H[-\\/]?OU)\b/i,
795
+ transform: toValue('3D HOU')
796
+ },
797
+ {
798
+ field: 'threeD',
799
+ pattern: /\bHalf.?Over.?Under\b/i,
800
+ transform: toValue('3D HOU')
801
+ },
802
+ {
803
+ field: 'threeD',
804
+ pattern: /\b(3D)\b.*\b(OU)\b/i,
805
+ transform: toValue('3D OU')
806
+ },
807
+ {
808
+ field: 'threeD',
809
+ pattern: /\bOver.?Under\b/i,
810
+ transform: toValue('3D OU')
811
+ },
812
+ {
813
+ field: 'threeD',
814
+ pattern: /\b((?:BD)?3D)\b/i,
815
+ transform: toValue('3D'),
816
+ skipIfFirst: true
817
+ },
818
+ // Codec handlers (lines 1140-1167 in handlers.go)
819
+ {
820
+ field: 'codec',
821
+ pattern: /\b[xh][-. ]?26[45]/i,
822
+ transform: toLowercase(),
823
+ remove: true
824
+ },
825
+ {
826
+ field: 'codec',
827
+ pattern: /\bhevc(?:\s?10)?\b/i,
828
+ transform: toValue('hevc'),
829
+ remove: true,
830
+ keepMatching: true
831
+ },
832
+ {
833
+ field: 'codec',
834
+ pattern: /\b(?:dvix|mpeg2|divx|xvid|avc)\b/i,
835
+ transform: toLowercase(),
836
+ remove: true,
837
+ keepMatching: true
838
+ },
839
+ {
840
+ field: 'codec',
841
+ process: removeFromValue(/[ .-]/)
842
+ },
843
+ // Channels handlers (lines 1169-1199 in handlers.go)
844
+ {
845
+ field: 'channels',
846
+ pattern: /5[.\s]1(?:ch|-S\d+)?\b/i,
847
+ transform: toValueSet('5.1'),
848
+ keepMatching: true,
849
+ remove: true
850
+ },
851
+ {
852
+ field: 'channels',
853
+ pattern: /\b(?:x[2-4]|5[\W]1(?:x[2-4])?)\b/i,
854
+ transform: toValueSet('5.1'),
855
+ keepMatching: true,
856
+ remove: true
857
+ },
858
+ {
859
+ field: 'channels',
860
+ pattern: /\b7[.\- ]1(?:.?ch(?:annel)?)?\b/i,
861
+ transform: toValueSet('7.1'),
862
+ keepMatching: true,
863
+ remove: true
864
+ },
865
+ {
866
+ field: 'channels',
867
+ pattern: /(?:\b|AAC|DDP)\+?(2[.\s]0)(?:x[2-4])?\b/i,
868
+ transform: toValueSet('2.0'),
869
+ keepMatching: true,
870
+ remove: true,
871
+ matchGroup: 1
872
+ },
873
+ {
874
+ field: 'channels',
875
+ pattern: /\b2\.0\b/i,
876
+ transform: toValueSet('2.0'),
877
+ keepMatching: true,
878
+ remove: true
879
+ },
880
+ {
881
+ field: 'channels',
882
+ pattern: /\bstereo\b/i,
883
+ transform: toValueSet('stereo'),
884
+ keepMatching: true
885
+ },
886
+ {
887
+ field: 'channels',
888
+ pattern: /\bmono\b/i,
889
+ transform: toValueSet('mono'),
890
+ keepMatching: true
891
+ },
892
+ // Audio handlers (lines 1201-1251 in handlers.go)
893
+ {
894
+ field: 'audio',
895
+ pattern: /\b(?:.+HR)?(?:DTS.?HD.?Ma(?:ster)?|DTS.?X)\b/i,
896
+ validateMatch: validateNotMatch(/(?:.+HR)/i),
897
+ transform: toValueSet('DTS Lossless'),
898
+ remove: true,
899
+ keepMatching: true
900
+ },
901
+ {
902
+ field: 'audio',
903
+ pattern: /\bDTS(?:(?:.?HD.?Ma(?:ster)?|.X))?.?(?:HD.?HR|HD)?\b/i,
904
+ validateMatch: validateNotMatch(/DTS(?:.?HD.?Ma(?:ster)?|.X)/i),
905
+ transform: toValueSet('DTS Lossy'),
906
+ remove: true,
907
+ keepMatching: true
908
+ },
909
+ {
910
+ field: 'audio',
911
+ pattern: /\b(?:Dolby.?)?Atmos\b/i,
912
+ transform: toValueSet('Atmos'),
913
+ remove: true,
914
+ keepMatching: true
915
+ },
916
+ {
917
+ field: 'audio',
918
+ pattern: /\b(?:True[ .-]?HD|\.True\.)\b/i,
919
+ transform: toValueSet('TrueHD'),
920
+ keepMatching: true,
921
+ remove: true,
922
+ skipFromTitle: true
923
+ },
924
+ {
925
+ field: 'audio',
926
+ pattern: /\bTRUE\b/,
927
+ transform: toValueSet('TrueHD'),
928
+ keepMatching: true,
929
+ remove: true,
930
+ skipFromTitle: true
931
+ },
932
+ // More Audio handlers (lines 1253-1521 in handlers.go)
933
+ {
934
+ field: 'audio',
935
+ pattern: /\bFLAC(?:\d\.\d)?(?:x\d+)?\b/i,
936
+ transform: toValueSet('FLAC'),
937
+ keepMatching: true,
938
+ remove: true
939
+ },
940
+ {
941
+ field: 'audio',
942
+ pattern: /DD2?[+p]|DD Plus|Dolby Digital Plus|DDP5[ ._]1/i,
943
+ transform: toValueSet('DDP'),
944
+ keepMatching: true,
945
+ remove: true
946
+ },
947
+ {
948
+ field: 'audio',
949
+ pattern: /E-?AC-?3(?:-S\d+)?/i,
950
+ transform: toValueSet('EAC3'),
951
+ keepMatching: true,
952
+ remove: true
953
+ },
954
+ {
955
+ field: 'audio',
956
+ pattern: /\b(DD|Dolby.?Digital|DolbyD)\b/i,
957
+ transform: toValueSet('DD'),
958
+ keepMatching: true,
959
+ remove: true
960
+ },
961
+ {
962
+ field: 'audio',
963
+ pattern: /\b(AC-?3(?:x2)?(?:-S\d+)?)\b/i,
964
+ transform: toValueSet('AC3'),
965
+ keepMatching: true,
966
+ remove: true
967
+ },
968
+ {
969
+ field: 'audio',
970
+ pattern: /\bQ?AAC(?:[. ]?2[. ]0|x2)?\b/,
971
+ transform: toValueSet('AAC'),
972
+ keepMatching: true,
973
+ remove: true
974
+ },
975
+ {
976
+ field: 'audio',
977
+ pattern: /\bL?PCM\b/i,
978
+ transform: toValueSet('PCM'),
979
+ keepMatching: true,
980
+ remove: true
981
+ },
982
+ {
983
+ field: 'audio',
984
+ pattern: /\bOPUS(?:\b|\d)(?:.*[ ._-](?:\d{3,4}p))?/i,
985
+ validateMatch: validateNotMatch(/OPUS(?:\b|\d)(?:.*[ ._-](?:\d{3,4}p))/i),
986
+ transform: toValueSet('OPUS'),
987
+ keepMatching: true,
988
+ remove: true
989
+ },
990
+ {
991
+ field: 'audio',
992
+ pattern: /\b(?:H[DQ])?.?(?:Clean.?Aud(?:io)?)\b/i,
993
+ transform: toValueSet('HQ'),
994
+ remove: true,
995
+ keepMatching: true
996
+ },
997
+ {
998
+ field: 'channels',
999
+ pattern: /\[([257][.-][01])]/,
1000
+ transform: toValueSetWithTransform((v) => v.toLowerCase()),
1001
+ remove: true,
1002
+ keepMatching: true
1003
+ },
1004
+ // Group handler (lines 1523-1528 in handlers.go)
1005
+ {
1006
+ field: 'group',
1007
+ pattern: /(- ?([^\-. \[]+[^\-. \[)\]\d][^\-. \[)\]]*))(?:\[[\w.-]+])?(?:\.\w{2,4}$|$)/i,
1008
+ validateMatch: validateNotMatch(/- ?(?:\d+$|S\d+|\d+x|ep?\d+|[^\[]+]$)/i),
1009
+ matchGroup: 1,
1010
+ valueGroup: 2
1011
+ },
1012
+ // Container handler (lines 1530-1534 in handlers.go)
1013
+ {
1014
+ field: 'container',
1015
+ pattern: /\.?[\[(]?\b(MKV|AVI|MP4|WMV|MPG|MPEG)\b[\])]?/i,
1016
+ transform: toLowercase()
1017
+ },
1018
+ // Batch 6: Volumes, Languages, Complete handlers (lines 1536-1749 in handlers.go)
1019
+ // Volumes handlers (lines 1548-1591 in handlers.go)
1020
+ {
1021
+ field: 'volumes',
1022
+ pattern: /\bvol(?:s|umes?)?[. -]*(?:\d{1,3}[., +/\\&-]+)+\d{1,3}\b/i,
1023
+ transform: toIntRange(),
1024
+ remove: true
1025
+ },
1026
+ {
1027
+ field: 'volumes',
1028
+ process: (title, m, result) => {
1029
+ const re = /\bvol(?:ume)?[. -]*(\d{1,3})/i;
1030
+ let startIndex = 0;
1031
+ if (result.has('year')) {
1032
+ const yr = result.get('year');
1033
+ startIndex = Math.min(yr.mIndex, title.length);
1034
+ }
1035
+ const match = title.substring(startIndex).match(re);
1036
+ if (match && match[1]) {
1037
+ const num = parseInt(match[1], 10);
1038
+ if (!isNaN(num)) {
1039
+ m.mIndex = startIndex + match.index;
1040
+ m.mValue = match[0];
1041
+ m.value = [num];
1042
+ m.remove = true;
1043
+ }
1044
+ }
1045
+ return m;
1046
+ }
1047
+ },
1048
+ // Country handler (lines 1593-1597 in handlers.go)
1049
+ {
1050
+ field: 'country',
1051
+ pattern: /\b(US|UK|AU|NZ)\b/
1052
+ },
1053
+ // Languages handlers (lines 1599-1612 in handlers.go)
1054
+ {
1055
+ field: 'languages',
1056
+ pattern: /\b(temporadas?|completa)\b/i,
1057
+ transform: toValueSet('es'),
1058
+ keepMatching: true
1059
+ },
1060
+ {
1061
+ field: 'languages',
1062
+ pattern: /\b(?:INT[EÉ]GRALE?)\b/i,
1063
+ transform: toValueSet('fr'),
1064
+ keepMatching: true
1065
+ },
1066
+ {
1067
+ field: 'languages',
1068
+ pattern: /\b(?:Saison)\b/i,
1069
+ transform: toValueSet('fr'),
1070
+ keepMatching: true
1071
+ },
1072
+ // Complete handlers (lines 1614-1725 in handlers.go)
1073
+ {
1074
+ field: 'complete',
1075
+ pattern: /\b(?:INTEGRALE?|INTÉGRALE?)\b/i,
1076
+ transform: toBoolean(),
1077
+ keepMatching: true,
1078
+ remove: true
1079
+ },
1080
+ {
1081
+ field: 'complete',
1082
+ pattern: /(?:\bthe\W)?(?:\bcomplete|collection|dvd)?\b[ .]?\bbox[ .-]?set\b/i,
1083
+ transform: toBoolean()
1084
+ },
1085
+ {
1086
+ field: 'complete',
1087
+ pattern: /(?:\bthe\W)?(?:\bcomplete|collection|dvd)?\b[ .]?\bmini[ .-]?series\b/i,
1088
+ transform: toBoolean()
1089
+ },
1090
+ {
1091
+ field: 'complete',
1092
+ pattern: /(?:\bthe\W)?(?:\bcomplete|full|all)\b.*\b(?:series|seasons|collection|episodes|set|pack|movies)\b/i,
1093
+ transform: toBoolean()
1094
+ },
1095
+ {
1096
+ field: 'complete',
1097
+ pattern: /\b(?:series|seasons|movies?)\b.*\b(?:complete|collection)\b/i,
1098
+ transform: toBoolean()
1099
+ },
1100
+ {
1101
+ field: 'complete',
1102
+ pattern: /(?:\bthe\W)?\bultimate\b[ .]\bcollection\b/i,
1103
+ transform: toBoolean(),
1104
+ keepMatching: true
1105
+ },
1106
+ {
1107
+ field: 'complete',
1108
+ pattern: /\bcollection\b.*\b(?:set|pack|movies)\b/i,
1109
+ transform: toBoolean()
1110
+ },
1111
+ {
1112
+ field: 'complete',
1113
+ pattern: /\bcollection(?:(\s\[|\s\())/i,
1114
+ transform: toBoolean(),
1115
+ remove: true
1116
+ },
1117
+ {
1118
+ field: 'complete',
1119
+ pattern: /\bkolekcja\b(?:\Wfilm(?:y|ów|ow)?)?/i,
1120
+ transform: toBoolean(),
1121
+ remove: true
1122
+ },
1123
+ {
1124
+ field: 'complete',
1125
+ pattern: /duology|trilogy|quadr[oi]logy|tetralogy|pentalogy|hexalogy|heptalogy|anthology/i,
1126
+ transform: toBoolean(),
1127
+ keepMatching: true
1128
+ },
1129
+ {
1130
+ field: 'complete',
1131
+ pattern: /\bcompleta\b/i,
1132
+ transform: toBoolean(),
1133
+ remove: true
1134
+ },
1135
+ {
1136
+ field: 'complete',
1137
+ pattern: /\bsaga\b/i,
1138
+ transform: toBoolean(),
1139
+ keepMatching: true,
1140
+ skipFromTitle: true
1141
+ },
1142
+ {
1143
+ field: 'complete',
1144
+ pattern: /\b\[Complete\]\b/i,
1145
+ transform: toBoolean(),
1146
+ remove: true
1147
+ },
1148
+ {
1149
+ field: 'complete',
1150
+ pattern: /(?:A.?|The.?)?\bComplete\b/i,
1151
+ validateMatch: validateNotMatch(/(?:A.?|The.?)\bComplete/i),
1152
+ transform: toBoolean(),
1153
+ remove: true
1154
+ },
1155
+ {
1156
+ field: 'complete',
1157
+ pattern: /\bCOMPLETE\b/,
1158
+ transform: toBoolean(),
1159
+ remove: true
1160
+ },
1161
+ // Batch 7: Seasons handlers (lines 1727-1868 in handlers.go)
1162
+ {
1163
+ field: 'seasons',
1164
+ pattern: /(?:complete\W|seasons?\W|\W|^)((?:s\d{1,2}[., +/\\&-]+)+s\d{1,2}\b)/i,
1165
+ transform: toIntRange(),
1166
+ remove: true
1167
+ },
1168
+ {
1169
+ field: 'seasons',
1170
+ pattern: /(?:complete\W|seasons?\W|\W|^)[(\[]?(s\d{2,}-\d{2,}\b)[)\]]?/i,
1171
+ transform: toIntRange(),
1172
+ remove: true
1173
+ },
1174
+ {
1175
+ field: 'seasons',
1176
+ pattern: /(?:complete\W|seasons?\W|\W|^)[(\[]?(s[1-9]-[2-9]\b)[)\]]?/i,
1177
+ transform: toIntRange(),
1178
+ remove: true
1179
+ },
1180
+ {
1181
+ field: 'seasons',
1182
+ pattern: /\d+ª(?:.+)?(?:a.?)?\d+ª(?:(?:.+)?(?:temporadas?))/i,
1183
+ transform: toIntRange(),
1184
+ remove: true
1185
+ },
1186
+ {
1187
+ field: 'seasons',
1188
+ pattern: /(?:(?:\bthe\W)?\bcomplete\W)?(?:seasons?|[Сс]езони?|sezon|temporadas?|stagioni)[. ]?[-:]?[. ]?[(\[]?((?:\d{1,2} ?(?:[,/\\&]+ ?)+)+\d{1,2}\b)[)\]]?/i,
1189
+ transform: toIntRange()
1190
+ },
1191
+ {
1192
+ field: 'seasons',
1193
+ pattern: /(?:(?:\bthe\W)?\bcomplete\W)?(?:seasons|[Сс]езони?|sezon|temporadas?|stagioni)[. ]?[-:]?[. ]?[(\[]?((?:\d{1,2}[. -]+)+0?[1-9]\d?\b)[)\]]?/i,
1194
+ transform: toIntRange(),
1195
+ remove: true
1196
+ },
1197
+ {
1198
+ field: 'seasons',
1199
+ pattern: /(?:(?:\bthe\W)?\bcomplete\W)?season[. ]?[(\[]?((?:\d{1,2}[. -]+)+[1-9]\d?\b)[)\]]?(?:.*\.\w{2,4}$)?/i,
1200
+ validateMatch: validateNotMatch(/(?:.*\.\w{2,4}$)/i),
1201
+ transform: toIntRange(),
1202
+ remove: true
1203
+ },
1204
+ {
1205
+ field: 'seasons',
1206
+ pattern: /(?:(?:\bthe\W)?\bcomplete\W)?\bseasons?\b[. -]?(\d{1,2}[. -]?(?:to|thru|and|\+|:)[. -]?\d{1,2})\b/i,
1207
+ transform: toIntRange(),
1208
+ remove: true
1209
+ },
1210
+ {
1211
+ field: 'seasons',
1212
+ pattern: /\bseason\b[ .-]?(\d{1,2}[ .-]?(?:to|thru|and|\+)[ .-]?\bseason\b[ .-]?\d{1,2})/i,
1213
+ transform: toIntRange()
1214
+ },
1215
+ {
1216
+ field: 'seasons',
1217
+ pattern: /(\d{1,2})(?:-?й)?[. _]?(?:[Сс]езон|sez(?:on)?)(?:\W?\D|$)/i,
1218
+ transform: toIntArray(),
1219
+ remove: true
1220
+ },
1221
+ {
1222
+ field: 'seasons',
1223
+ pattern: /(?:(?:\bthe\W)?\bcomplete\W)?(?:saison|seizoen|sezon(?:SO?)?|stagione|season|series|temp(?:orada)?):?[. ]?(\d{1,2})/i,
1224
+ transform: toIntArray()
1225
+ },
1226
+ {
1227
+ field: 'seasons',
1228
+ pattern: /[Сс]езон:?[. _]?№?(\d{1,2})(?:\d)?/i,
1229
+ validateMatch: validateNotMatch(/\d{3}/i),
1230
+ transform: toIntArray(),
1231
+ remove: true
1232
+ },
1233
+ {
1234
+ field: 'seasons',
1235
+ pattern: /(?:\D|^)(\d{1,2})Â?[°ºªa]?[. ]*temporada/i,
1236
+ transform: toIntArray(),
1237
+ remove: true
1238
+ },
1239
+ {
1240
+ field: 'seasons',
1241
+ pattern: /t(\d{1,3})(?:[ex]+|$)/i,
1242
+ transform: toIntArray(),
1243
+ remove: true
1244
+ },
1245
+ {
1246
+ field: 'seasons',
1247
+ pattern: /(?:(?:\bthe\W)?\bcomplete)?(?:\W|^)so?([01]?[0-5]?[1-9])(?:[\Wex]|\d{2}\b)/i,
1248
+ transform: toIntArray(),
1249
+ keepMatching: true
1250
+ },
1251
+ {
1252
+ field: 'seasons',
1253
+ pattern: /(?:so?|t)(\d{1,2})[. ]?[xх-]?[. ]?(?:e|x|х|ep|-|\.)[. ]?\d{1,4}(?:[abc]|v0?[1-4]|\D|$)/i,
1254
+ transform: toIntArray()
1255
+ },
1256
+ {
1257
+ field: 'seasons',
1258
+ pattern: /(?:(?:\bthe\W)?\bcomplete\W)?(?:\W|^)(\d{1,2})[. ]?(?:st|nd|rd|th)[. ]*season/i,
1259
+ transform: toIntArray()
1260
+ },
1261
+ {
1262
+ field: 'seasons',
1263
+ pattern: /(?:\D|^)(\d{1,2})[Xxх]\d{1,3}(?:\D|$)/,
1264
+ transform: toIntArray()
1265
+ },
1266
+ {
1267
+ field: 'seasons',
1268
+ pattern: /\bSn([1-9])(?:\D|$)/,
1269
+ transform: toIntArray()
1270
+ },
1271
+ {
1272
+ field: 'seasons',
1273
+ pattern: /[\[(](\d{1,2})\.\d{1,3}[)\]]/,
1274
+ transform: toIntArray()
1275
+ },
1276
+ {
1277
+ field: 'seasons',
1278
+ pattern: /-\s?(\d{1,2})\.\d{2,3}\s?-/,
1279
+ transform: toIntArray()
1280
+ },
1281
+ {
1282
+ field: 'seasons',
1283
+ pattern: /^(\d{1,2})\.\d{2,3} - /,
1284
+ transform: toIntArray(),
1285
+ skipIfBefore: ['year', 'source', 'resolution']
1286
+ },
1287
+ {
1288
+ field: 'seasons',
1289
+ pattern: /(?:^|\/)(?:20-20)?(\d{1,2})-\d{2}\b(?:-\d)?/,
1290
+ validateMatch: validateNotMatch(/^(?:20-20)|(\d{1,2})-\d{2}\b(?:-\d)/),
1291
+ transform: toIntArray()
1292
+ },
1293
+ {
1294
+ field: 'seasons',
1295
+ pattern: /[^\w-](\d{1,2})-\d{2}(?:\.\w{2,4}$)/,
1296
+ transform: toIntArray()
1297
+ },
1298
+ {
1299
+ field: 'seasons',
1300
+ pattern: /(?:\bEp?(?:isode)? ?\d+\b.*)?\b(\d{2})[ ._]\d{2}(?:.F)?\.\w{2,4}$/,
1301
+ validateMatch: validateNotMatch(/(?:\bEp?(?:isode)? ?\d+\b.*)/),
1302
+ transform: toIntArray()
1303
+ },
1304
+ {
1305
+ field: 'seasons',
1306
+ pattern: /\bEp(?:isode)?\W+(\d{1,2})\.\d{1,3}\b/i,
1307
+ transform: toIntArray()
1308
+ },
1309
+ {
1310
+ field: 'seasons',
1311
+ pattern: /(?:(?:\bthe\W)?\bcomplete)?(?:[a-z])?\bs(\d{1,3})(?:[\Wex]|\d{2}\b|$)/i,
1312
+ validateMatch: validateNotMatch(/(?:[a-z])\bs\d{1,3}/i),
1313
+ transform: toIntArray(),
1314
+ keepMatching: true
1315
+ },
1316
+ {
1317
+ field: 'seasons',
1318
+ pattern: /\bSeasons?\b.*\b(\d{1,2}-\d{1,2})\b/i,
1319
+ transform: toIntRange()
1320
+ },
1321
+ {
1322
+ field: 'seasons',
1323
+ pattern: /(?:\W|^)(\d{1,2})(?:e|ep)\d{1,3}(?:\W|$)/i,
1324
+ transform: toIntArray()
1325
+ },
1326
+ // Batch 8: Episodes handlers (lines 1870-2125 in handlers.go)
1327
+ {
1328
+ field: 'episodes',
1329
+ pattern: /(?:[\W\d]|^)e[ .]?[(\[]?(\d{1,3}(?:[à .-]*(?:[&+]|e){1,2}[ .]?\d{1,3})+)(?:\W|$)/i,
1330
+ transform: toIntRange()
1331
+ },
1332
+ {
1333
+ field: 'episodes',
1334
+ pattern: /(?:[\W\d]|^)ep[ .]?[(\[]?(\d{1,3}(?:[ .-]*(?:[&+]|ep){1,2}[ .]?\d{1,3})+)(?:\W|$)/i,
1335
+ transform: toIntRange()
1336
+ },
1337
+ {
1338
+ field: 'episodes',
1339
+ pattern: /(?:[\W\d]|^)\d+[xх][ .]?[(\[]?(\d{1,3}(?:[ .]?[xх][ .]?\d{1,3})+)(?:\W|$)/i,
1340
+ transform: toIntRange()
1341
+ },
1342
+ {
1343
+ field: 'episodes',
1344
+ pattern: /(?:[\W\d]|^)(?:episodes?|[Сс]ерии:?)[ .]?[(\[]?(\d{1,3}(?:[ .+]*[&+][ .]?\d{1,3})+)(?:\W|$)/i,
1345
+ transform: toIntRange()
1346
+ },
1347
+ {
1348
+ field: 'episodes',
1349
+ pattern: /[(\[]?(?:\D|^)(\d{1,3}[ .]?ao[ .]?\d{1,3})[)\]]?(?:\W|$)/i,
1350
+ transform: toIntRange()
1351
+ },
1352
+ {
1353
+ field: 'episodes',
1354
+ pattern: /(?:[\W\d]|^)(?:e|eps?|episodes?|[Сс]ерии:?|\d+[xх])[ .]*[(\[]?(\d{1,3}(?:-\d{1,3})+)(?:\W|$)/i,
1355
+ transform: toIntRange()
1356
+ },
1357
+ {
1358
+ field: 'episodes',
1359
+ pattern: /\bs\d{1,2}[ .]*-[ .]*\b(\d{1,3}(?:[ .]*~[ .]*\d{1,3})+)\b/i,
1360
+ transform: toIntRange()
1361
+ },
1362
+ {
1363
+ field: 'episodes',
1364
+ pattern: /(?:so?|t)\d{1,3}[. ]?[xх-]?[. ]?(?:e|x|х|ep)[. ]?(\d{1,4})(?:[abc]|v0?[1-4]|\D|$)/i,
1365
+ remove: true,
1366
+ transform: toIntArray()
1367
+ },
1368
+ {
1369
+ field: 'episodes',
1370
+ pattern: /(?:so?|t)\d{1,2}\s?[-.]\s?(\d{1,4})(?:[abc]|v0?[1-4]|\D|$)/i,
1371
+ transform: toIntArray()
1372
+ },
1373
+ {
1374
+ field: 'episodes',
1375
+ pattern: /\b(?:so?|t)\d{2}(\d{2})\b/i,
1376
+ transform: toIntArray()
1377
+ },
1378
+ {
1379
+ field: 'episodes',
1380
+ pattern: /(?:\W|^)(\d{1,3}(?:[ .]*~[ .]*\d{1,3})+)(?:\W|$)/i,
1381
+ transform: toIntRange()
1382
+ },
1383
+ {
1384
+ field: 'episodes',
1385
+ pattern: /-\s(\d{1,3}[ .]*-[ .]*\d{1,3})(?:-\d*)?(?:\W|$)/i,
1386
+ validateMatch: validateNotMatch(/-\s(\d{1,3}[ .]*-[ .]*\d{1,3})(?:-\d*)/i),
1387
+ transform: toIntRange()
1388
+ },
1389
+ {
1390
+ field: 'episodes',
1391
+ pattern: /s\d{1,2}\s?\((\d{1,3}[ .]*-[ .]*\d{1,3})\)/i,
1392
+ transform: toIntRange()
1393
+ },
1394
+ {
1395
+ field: 'episodes',
1396
+ pattern: /(?:^|\/)(?:20-20)?\d{1,2}-(\d{2})\b(?:-\d)?/,
1397
+ validateMatch: validateNotMatch(/^(?:20-20)|\d{1,2}-(\d{2})\b(?:-\d)/),
1398
+ transform: toIntArray()
1399
+ },
1400
+ {
1401
+ field: 'episodes',
1402
+ pattern: /(?:\d-)?\b\d{1,2}-(\d{2})(?:\.\w{2,4}$)/,
1403
+ validateMatch: validateNotMatch(/(?:\d-)\b\d{1,2}-(\d{2})/),
1404
+ transform: toIntArray()
1405
+ },
1406
+ {
1407
+ field: 'episodes',
1408
+ pattern: /(?:^\[.+].+)([. ]+-[. ]*(\d{1,4})[. ]+)(?:\W)/i,
1409
+ transform: toIntArray(),
1410
+ valueGroup: 2,
1411
+ matchGroup: 1
1412
+ },
1413
+ {
1414
+ field: 'episodes',
1415
+ pattern: /(?:(?:seasons?|[Сс]езони?)\W*)?(?:[ .(\[-]|^)(\d{1,3}(?:[ .]?[,&+~][ .]?\d{1,3})+)(?:[ .)\]-]|$)/i,
1416
+ validateMatch: validateNotMatch(/(?:(?:seasons?|[Сс]езони?)\W*)/i),
1417
+ transform: toIntRange()
1418
+ },
1419
+ {
1420
+ field: 'episodes',
1421
+ pattern: /(?:(?:seasons?|[Сс]езони?)\W*)?(?:20-20)?(?:[ .(\[-]|^)(\d{1,4}(?:-\d{1,4})+)(?:[ .)(\]]|[+-]\D|$)/i,
1422
+ validateMatch: validateNotMatch(/(?:(?:seasons?|[Сс]езони?)\W*|^)(?:20-20)/i),
1423
+ transform: toIntRange()
1424
+ },
1425
+ {
1426
+ field: 'episodes',
1427
+ pattern: /\bEp(?:isode)?\W+\d{1,2}\.(\d{1,3})\b/i,
1428
+ transform: toIntArray()
1429
+ },
1430
+ {
1431
+ field: 'episodes',
1432
+ pattern: /Ep.\d+.-.\d+/i,
1433
+ transform: toIntRange(),
1434
+ remove: true
1435
+ },
1436
+ {
1437
+ field: 'episodes',
1438
+ pattern: /(?:\b[ée]p?(?:isode)?|[Ээ]пизод|[Сс]ер(?:ии|ия|\.)?|caa?p(?:itulo)?|epis[oó]dio)[. ]?[-:#№]?[. ]?(\d{1,4})(?:[abc]|v0?[1-4]|\W|$)/i,
1439
+ transform: toIntArray()
1440
+ },
1441
+ {
1442
+ field: 'episodes',
1443
+ pattern: /\b(\d{1,3})(?:-?я)?[ ._-]*(?:ser(?:i?[iyj]a|\b)|[Сс]ер(?:ии|ия|\.)?)/i,
1444
+ transform: toIntArray()
1445
+ },
1446
+ {
1447
+ field: 'episodes',
1448
+ pattern: /(?:\D|^)\d{1,2}[. ]?[Xxх][. ]?(\d{1,3})(?:[abc]|v0?[1-4]|\D|$)/i,
1449
+ transform: toIntArray()
1450
+ },
1451
+ {
1452
+ field: 'episodes',
1453
+ pattern: /[\[(]\d{1,2}\.(\d{1,3})[)\]]/i,
1454
+ transform: toIntArray()
1455
+ },
1456
+ {
1457
+ field: 'episodes',
1458
+ pattern: /\b[Ss](?:eason\W?)?\d{1,2}[ .](\d{1,2})\b/,
1459
+ transform: toIntArray()
1460
+ },
1461
+ {
1462
+ field: 'episodes',
1463
+ pattern: /-\s?\d{1,2}\.(\d{2,3})\s?-/i,
1464
+ transform: toIntArray()
1465
+ },
1466
+ {
1467
+ field: 'episodes',
1468
+ pattern: /^\d{1,2}\.(\d{2,3}) - /,
1469
+ skipIfBefore: ['year', 'source', 'resolution'],
1470
+ transform: toIntArray()
1471
+ },
1472
+ {
1473
+ field: 'episodes',
1474
+ pattern: /(?:\D|^)(\d{1,3})[. ]?(?:of|из|iz)[. ]?\d{1,3}(?:\D|$)/i,
1475
+ transform: toIntArray()
1476
+ },
1477
+ {
1478
+ field: 'episodes',
1479
+ pattern: /\b\d{2}[ ._-](\d{2})(?:.F)?\.\w{2,4}$/i,
1480
+ transform: toIntArray()
1481
+ },
1482
+ {
1483
+ field: 'episodes',
1484
+ pattern: /(?:^)?\[(\d{2,3})](?:(?:\.\w{2,4})?$)?/i,
1485
+ validateMatch: validateAnd(validateNotAtStart(), validateNotAtEnd(), validateNotMatch(/(?:720|1080)|\[(\d{2,3})](?:(?:\.\w{2,4})$)/i)),
1486
+ transform: toIntArray()
1487
+ },
1488
+ {
1489
+ field: 'episodes',
1490
+ pattern: /\bodc[. ]+(\d{1,3})\b/i,
1491
+ transform: toIntArray()
1492
+ },
1493
+ {
1494
+ field: 'episodes',
1495
+ pattern: /\b264\b|\b265\b/i,
1496
+ validateMatch: (input, match) => {
1497
+ const re = /\b[xh]\b/i;
1498
+ return !re.test(input.substring(0, match[0]));
1499
+ },
1500
+ transform: toIntArray(),
1501
+ remove: true
1502
+ },
1503
+ {
1504
+ field: 'episodes',
1505
+ pattern: /(?:\W|^)(?:\d+)?(?:e|ep)(\d{1,3})(?:\W|$)/i,
1506
+ transform: toIntArray(),
1507
+ remove: true
1508
+ },
1509
+ {
1510
+ field: 'episodes',
1511
+ pattern: /\d+.-.\d+TV/i,
1512
+ transform: toIntRange(),
1513
+ remove: true
1514
+ },
1515
+ {
1516
+ field: 'episodes',
1517
+ pattern: /season\s*\d{1,2}\s*(\d{1,4}\s*-\s*\d{1,4})/i,
1518
+ transform: toIntRange()
1519
+ },
1520
+ {
1521
+ field: 'episodes',
1522
+ process: (title, m, result) => {
1523
+ if (m.value !== null && m.value !== undefined) {
1524
+ return m;
1525
+ }
1526
+ const btRe = /(?:movie\W*|film\W*|^)?(?:[ .]+-[ .]+|[(\[][ .]*)(\d{1,4})(?:a|b|v\d|\.\d)?(?:\W|$)(?:movie|film|\d+)?/i;
1527
+ const btReNegBefore = /(?:movie\W*|film\W*)(?:[ .]+-[ .]+|[(\[][ .]*)(\d{1,4})/i;
1528
+ const btReNegAfter = /(?:movie|film)|(\d{1,4})(?:a|b|v\d|\.\d)(?:\W)(?:\d+)/i;
1529
+ const mtRe = /^(?:[(\[-][ .]?)?(\d{1,4})(?:a|b|v\d)?(?:\Wmovie|\Wfilm|-\d)?(?:\W|$)/i;
1530
+ const mtReNegAfter = /(\d{1,4})(?:a|b|v\d)?(?:\Wmovie|\Wfilm|-\d)/i;
1531
+ const commonResolutionNeg = /\[(?:480|720|1080)\]/;
1532
+ const commonFPSNeg = /\d+(?:fps|帧率?)/i;
1533
+ let startIndex = 0;
1534
+ for (const component of ['year', 'seasons']) {
1535
+ if (result.has(component)) {
1536
+ const cm = result.get(component);
1537
+ if (cm.mIndex > 0 && (startIndex === 0 || cm.mIndex < startIndex)) {
1538
+ startIndex = cm.mIndex;
1539
+ }
1540
+ }
1541
+ }
1542
+ let endIndex = title.length;
1543
+ for (const component of ['resolution', 'quality', 'codec', 'audio']) {
1544
+ if (result.has(component)) {
1545
+ const cm = result.get(component);
1546
+ if (cm.mIndex > 0 && cm.mIndex < endIndex) {
1547
+ endIndex = cm.mIndex;
1548
+ }
1549
+ }
1550
+ }
1551
+ const beginningTitle = title.substring(0, endIndex);
1552
+ startIndex = Math.min(startIndex, title.length);
1553
+ const middleTitle = title.substring(startIndex, Math.max(endIndex, startIndex));
1554
+ let match = beginningTitle.match(btRe);
1555
+ let mStr = '';
1556
+ if (match && match.index !== undefined) {
1557
+ mStr = match[0];
1558
+ if (match.index === 0 || btReNegBefore.test(mStr) || btReNegAfter.test(mStr) ||
1559
+ commonResolutionNeg.test(mStr) || commonFPSNeg.test(mStr)) {
1560
+ match = null;
1561
+ mStr = '';
1562
+ }
1563
+ else if (match[1]) {
1564
+ mStr = match[1];
1565
+ }
1566
+ }
1567
+ if (!mStr) {
1568
+ match = middleTitle.match(mtRe);
1569
+ if (match && match.index !== undefined) {
1570
+ const afterMatch = middleTitle.substring(match.index + match[0].length);
1571
+ if (mtReNegAfter.test(afterMatch) || commonResolutionNeg.test(mStr)) {
1572
+ match = null;
1573
+ mStr = '';
1574
+ }
1575
+ else if (match[1]) {
1576
+ mStr = match[1];
1577
+ }
1578
+ }
1579
+ }
1580
+ if (mStr) {
1581
+ mStr = mStr.replace(/\D/g, '');
1582
+ const ep = parseInt(mStr, 10);
1583
+ if (!isNaN(ep)) {
1584
+ m.mIndex = title.indexOf(mStr);
1585
+ m.mValue = mStr;
1586
+ m.value = [ep];
1587
+ }
1588
+ }
1589
+ return m;
1590
+ }
1591
+ },
1592
+ // Batch 9: Subbed, Dubbed, Multi-language detection (lines 2259-2290 in handlers.go)
1593
+ {
1594
+ field: 'subbed',
1595
+ pattern: /\bSUB(?:FRENCH)\b|\b(?:DAN|E|FIN|PL|SLO|SWE)SUBS?\b/i,
1596
+ transform: toBoolean()
1597
+ },
1598
+ {
1599
+ field: 'languages',
1600
+ pattern: /\bmulti(?:ple)?[ .-]*(?:su?$|sub\w*|dub\w*)\b|msub/i,
1601
+ transform: toValueSet('multi subs'),
1602
+ keepMatching: true,
1603
+ remove: true
1604
+ },
1605
+ {
1606
+ field: 'languages',
1607
+ pattern: /\bmulti(?:ple)?[ .-]*(?:lang(?:uages?)?|audio|VF2)?\b/i,
1608
+ transform: toValueSet('multi audio'),
1609
+ keepMatching: true
1610
+ },
1611
+ {
1612
+ field: 'languages',
1613
+ pattern: /\btri(?:ple)?[ .-]*(?:audio|dub\w*)\b/i,
1614
+ transform: toValueSet('multi audio'),
1615
+ keepMatching: true
1616
+ },
1617
+ {
1618
+ field: 'languages',
1619
+ pattern: /\bdual[ .-]*(?:au?$|[aá]udio|line)\b/i,
1620
+ transform: toValueSet('dual audio'),
1621
+ keepMatching: true
1622
+ },
1623
+ {
1624
+ field: 'languages',
1625
+ pattern: /\bdual\b(?:[ .-]*sub)?/i,
1626
+ validateMatch: validateNotMatch(/(?:[ .-]*sub)/i),
1627
+ transform: toValueSet('dual audio'),
1628
+ keepMatching: true
1629
+ },
1630
+ // Batch 10: Detailed language handlers, subbed/dubbed, network, size, group, extension (lines 2291-3592)
1631
+ // English language handlers
1632
+ {
1633
+ field: 'languages',
1634
+ pattern: /\bengl?(?:sub[A-Z]*)?\b/i,
1635
+ transform: toValueSet('en'),
1636
+ keepMatching: true
1637
+ },
1638
+ {
1639
+ field: 'languages',
1640
+ pattern: /\beng?sub[A-Z]*\b/i,
1641
+ transform: toValueSet('en'),
1642
+ keepMatching: true
1643
+ },
1644
+ {
1645
+ field: 'languages',
1646
+ pattern: /\bing(?:l[eéê]s)?\b/i,
1647
+ transform: toValueSet('en'),
1648
+ keepMatching: true
1649
+ },
1650
+ {
1651
+ field: 'languages',
1652
+ pattern: /\besub\b/i,
1653
+ transform: toValueSet('en'),
1654
+ keepMatching: true,
1655
+ remove: true
1656
+ },
1657
+ {
1658
+ field: 'languages',
1659
+ pattern: /\benglish\W+(?:subs?|sdh|hi)\b/i,
1660
+ transform: toValueSet('en'),
1661
+ keepMatching: true
1662
+ },
1663
+ {
1664
+ field: 'languages',
1665
+ pattern: /\bEN\b/i,
1666
+ transform: toValueSet('en'),
1667
+ keepMatching: true,
1668
+ skipFromTitle: true
1669
+ },
1670
+ {
1671
+ field: 'languages',
1672
+ pattern: /\benglish?\b/i,
1673
+ transform: toValueSet('en'),
1674
+ keepMatching: true,
1675
+ skipIfFirst: true
1676
+ },
1677
+ // Japanese language handlers
1678
+ {
1679
+ field: 'languages',
1680
+ pattern: /\b(?:JP|JAP|JPN)\b/i,
1681
+ transform: toValueSet('ja'),
1682
+ keepMatching: true
1683
+ },
1684
+ {
1685
+ field: 'languages',
1686
+ pattern: /(japanese|japon[eê]s)\b/i,
1687
+ transform: toValueSet('ja'),
1688
+ keepMatching: true,
1689
+ skipIfFirst: true
1690
+ },
1691
+ // Korean language handlers
1692
+ {
1693
+ field: 'languages',
1694
+ pattern: /\b(?:KOR|kor[ .-]?sub)\b/i,
1695
+ transform: toValueSet('ko'),
1696
+ keepMatching: true
1697
+ },
1698
+ {
1699
+ field: 'languages',
1700
+ pattern: /(korean|coreano)\b/i,
1701
+ transform: toValueSet('ko'),
1702
+ keepMatching: true,
1703
+ skipIfFirst: true
1704
+ },
1705
+ // Chinese language handlers
1706
+ {
1707
+ field: 'languages',
1708
+ pattern: /\b(?:traditional\W*chinese|chinese\W*traditional)(?:\Wchi)?\b/i,
1709
+ transform: toValueSet('zh-tw'),
1710
+ keepMatching: true,
1711
+ remove: true
1712
+ },
1713
+ {
1714
+ field: 'languages',
1715
+ pattern: /\bzh-hant\b/i,
1716
+ transform: toValueSet('zh-tw'),
1717
+ keepMatching: true
1718
+ },
1719
+ {
1720
+ field: 'languages',
1721
+ pattern: /\b(?:mand[ae]rin|ch[sn])\b/i,
1722
+ transform: toValueSet('zh'),
1723
+ keepMatching: true
1724
+ },
1725
+ {
1726
+ field: 'languages',
1727
+ pattern: /(?:shang-?)?\bCH(?:I|T)\b/i,
1728
+ validateMatch: validateNotMatch(/shang-?/i),
1729
+ transform: toValueSet('zh'),
1730
+ keepMatching: true,
1731
+ skipFromTitle: true
1732
+ },
1733
+ {
1734
+ field: 'languages',
1735
+ pattern: /(chinese|chin[eê]s)\b/i,
1736
+ transform: toValueSet('zh'),
1737
+ keepMatching: true,
1738
+ skipIfFirst: true
1739
+ },
1740
+ {
1741
+ field: 'languages',
1742
+ pattern: /\bzh-hans\b/i,
1743
+ transform: toValueSet('zh'),
1744
+ keepMatching: true
1745
+ },
1746
+ // French language handlers
1747
+ {
1748
+ field: 'languages',
1749
+ pattern: /\bFR(?:a|e|anc[eê]s|VF[FQIB2]?)?\b/i,
1750
+ transform: toValueSet('fr'),
1751
+ keepMatching: true,
1752
+ skipFromTitle: true
1753
+ },
1754
+ {
1755
+ field: 'languages',
1756
+ pattern: /\b(?:TRUE|SUB).?FRENCH\b|\bFRENCH\b/,
1757
+ transform: toValueSet('fr'),
1758
+ keepMatching: true
1759
+ },
1760
+ {
1761
+ field: 'languages',
1762
+ pattern: /\b\[?(?:VF[FQRIB2]?\]?\b|(?:VOST)?FR2?)\b/,
1763
+ transform: toValueSet('fr'),
1764
+ keepMatching: true
1765
+ },
1766
+ {
1767
+ field: 'languages',
1768
+ pattern: /\bVOST(?:FR?|A)?\b/i,
1769
+ transform: toValueSet('fr'),
1770
+ keepMatching: true
1771
+ },
1772
+ // Spanish/Latino language handlers
1773
+ {
1774
+ field: 'languages',
1775
+ pattern: /\bspanish\W?latin|american\W*(?:spa|esp?)/i,
1776
+ transform: toValueSet('es-419'),
1777
+ keepMatching: true,
1778
+ remove: true,
1779
+ skipFromTitle: true
1780
+ },
1781
+ {
1782
+ field: 'languages',
1783
+ pattern: /\b(?:audio.)?lat(?:in?|ino)?\b/i,
1784
+ transform: toValueSet('es-419'),
1785
+ keepMatching: true
1786
+ },
1787
+ {
1788
+ field: 'languages',
1789
+ pattern: /\b(?:audio.)?(?:ESP|spa|(?:en[ .]+)?espa[nñ]ola?|castellano)\b/i,
1790
+ transform: toValueSet('es'),
1791
+ keepMatching: true,
1792
+ remove: true
1793
+ },
1794
+ {
1795
+ field: 'languages',
1796
+ pattern: /\b(?:[ .,/-]+(?:[A-Z]{2}[ .,/-]+){2,})es\b/i,
1797
+ transform: toValueSet('es'),
1798
+ keepMatching: true,
1799
+ skipFromTitle: true
1800
+ },
1801
+ {
1802
+ field: 'languages',
1803
+ pattern: /\b(?:[ .,/-]*[A-Z]{2}[ .,/-]+)es(?:[ .,/-]+[A-Z]{2}[ .,/-]+)\b/i,
1804
+ transform: toValueSet('es'),
1805
+ keepMatching: true,
1806
+ skipFromTitle: true
1807
+ },
1808
+ {
1809
+ field: 'languages',
1810
+ pattern: /\bes(?:\.(?:ass|ssa|srt|sub|idx)$)/i,
1811
+ transform: toValueSet('es'),
1812
+ keepMatching: true,
1813
+ skipFromTitle: true
1814
+ },
1815
+ {
1816
+ field: 'languages',
1817
+ pattern: /\bspanish\W+subs?\b/i,
1818
+ transform: toValueSet('es'),
1819
+ keepMatching: true
1820
+ },
1821
+ {
1822
+ field: 'languages',
1823
+ pattern: /\b(spanish|espanhol)\b/i,
1824
+ transform: toValueSet('es'),
1825
+ keepMatching: true,
1826
+ skipIfFirst: true
1827
+ },
1828
+ // Portuguese language handlers
1829
+ {
1830
+ field: 'languages',
1831
+ pattern: /\b(?:p[rt]|en|port)[. (\\/-]*BR\b/i,
1832
+ transform: toValueSet('pt'),
1833
+ keepMatching: true,
1834
+ remove: true
1835
+ },
1836
+ {
1837
+ field: 'languages',
1838
+ pattern: /\bbr(?:a|azil|azilian)\W+(?:pt|por)\b/i,
1839
+ transform: toValueSet('pt'),
1840
+ keepMatching: true,
1841
+ remove: true
1842
+ },
1843
+ {
1844
+ field: 'languages',
1845
+ pattern: /\b(?:leg(?:endado|endas?)?|dub(?:lado)?|portugu[eèê]se?)[. -]*BR\b/i,
1846
+ transform: toValueSet('pt'),
1847
+ keepMatching: true
1848
+ },
1849
+ {
1850
+ field: 'languages',
1851
+ pattern: /\bleg(?:endado|endas?)\b/i,
1852
+ transform: toValueSet('pt'),
1853
+ keepMatching: true
1854
+ },
1855
+ {
1856
+ field: 'languages',
1857
+ pattern: /\bportugu[eèê]s[ea]?\b/i,
1858
+ transform: toValueSet('pt'),
1859
+ keepMatching: true
1860
+ },
1861
+ {
1862
+ field: 'languages',
1863
+ pattern: /\bPT[. -]*(?:PT|ENG?|sub(?:s|titles?))\b/i,
1864
+ transform: toValueSet('pt'),
1865
+ keepMatching: true
1866
+ },
1867
+ {
1868
+ field: 'languages',
1869
+ pattern: /\bpt(?:\.(?:ass|ssa|srt|sub|idx)$)/i,
1870
+ transform: toValueSet('pt'),
1871
+ keepMatching: true,
1872
+ skipFromTitle: true
1873
+ },
1874
+ {
1875
+ field: 'languages',
1876
+ pattern: /\bpor\b/i,
1877
+ transform: toValueSet('pt'),
1878
+ keepMatching: true,
1879
+ skipFromTitle: true
1880
+ },
1881
+ // Italian language handlers
1882
+ {
1883
+ field: 'languages',
1884
+ pattern: /\bITA\b/i,
1885
+ transform: toValueSet('it'),
1886
+ keepMatching: true
1887
+ },
1888
+ {
1889
+ field: 'languages',
1890
+ pattern: /\b(?:w{3}\.\w+\.)?IT(?:[ .,/-]+(?:[a-zA-Z]{2}[ .,/-]+){2,})\b/,
1891
+ validateMatch: validateNotMatch(/(?:w{3}\.\w+\.)IT/),
1892
+ transform: toValueSet('it'),
1893
+ keepMatching: true,
1894
+ skipFromTitle: true
1895
+ },
1896
+ {
1897
+ field: 'languages',
1898
+ pattern: /\bit(?:\.(?:ass|ssa|srt|sub|idx)$)/i,
1899
+ transform: toValueSet('it'),
1900
+ keepMatching: true,
1901
+ skipFromTitle: true
1902
+ },
1903
+ {
1904
+ field: 'languages',
1905
+ pattern: /\bitaliano?\b/i,
1906
+ transform: toValueSet('it'),
1907
+ keepMatching: true,
1908
+ skipIfFirst: true
1909
+ },
1910
+ // Greek language handlers
1911
+ {
1912
+ field: 'languages',
1913
+ pattern: /\bgreek[ .-]*(?:audio|lang(?:uage)?|subs?(?:titles?)?)?\b/i,
1914
+ transform: toValueSet('el'),
1915
+ keepMatching: true,
1916
+ skipIfFirst: true
1917
+ },
1918
+ // German language handlers
1919
+ {
1920
+ field: 'languages',
1921
+ pattern: /\b(?:GER|DEU)\b/i,
1922
+ transform: toValueSet('de'),
1923
+ keepMatching: true,
1924
+ skipFromTitle: true
1925
+ },
1926
+ {
1927
+ field: 'languages',
1928
+ pattern: /\bde(?:[ .,/-]+(?:[A-Z]{2}[ .,/-]+){2,})\b/i,
1929
+ transform: toValueSet('de'),
1930
+ keepMatching: true,
1931
+ skipFromTitle: true
1932
+ },
1933
+ {
1934
+ field: 'languages',
1935
+ pattern: /\b(?:[ .,/-]+(?:[A-Z]{2}[ .,/-]+){2,})de\b/i,
1936
+ transform: toValueSet('de'),
1937
+ keepMatching: true,
1938
+ skipFromTitle: true
1939
+ },
1940
+ {
1941
+ field: 'languages',
1942
+ pattern: /\bde(?:\.(?:ass|ssa|srt|sub|idx)$)/i,
1943
+ transform: toValueSet('de'),
1944
+ keepMatching: true,
1945
+ skipFromTitle: true
1946
+ },
1947
+ {
1948
+ field: 'languages',
1949
+ pattern: /\b(german|alem[aã]o)\b/i,
1950
+ transform: toValueSet('de'),
1951
+ keepMatching: true,
1952
+ skipIfFirst: true
1953
+ },
1954
+ // Russian language handlers
1955
+ {
1956
+ field: 'languages',
1957
+ pattern: /\bRUS?\b/i,
1958
+ transform: toValueSet('ru'),
1959
+ keepMatching: true
1960
+ },
1961
+ {
1962
+ field: 'languages',
1963
+ pattern: /(russian|russo)\b/i,
1964
+ transform: toValueSet('ru'),
1965
+ keepMatching: true,
1966
+ skipIfFirst: true
1967
+ },
1968
+ // Ukrainian language handlers
1969
+ {
1970
+ field: 'languages',
1971
+ pattern: /\bUKR\b/i,
1972
+ transform: toValueSet('uk'),
1973
+ keepMatching: true
1974
+ },
1975
+ {
1976
+ field: 'languages',
1977
+ pattern: /\bukrainian\b/i,
1978
+ transform: toValueSet('uk'),
1979
+ keepMatching: true,
1980
+ skipIfFirst: true
1981
+ },
1982
+ // Indian language handlers
1983
+ {
1984
+ field: 'languages',
1985
+ pattern: /\bhin(?:di)?\b/i,
1986
+ transform: toValueSet('hi'),
1987
+ keepMatching: true
1988
+ },
1989
+ {
1990
+ field: 'languages',
1991
+ pattern: /\b(?:(?:w{3}\.\w+\.)?tel(?:\W*aviv)?|telugu)\b/i,
1992
+ validateMatch: validateNotMatch(/(?:(?:w{3}\.\w+\.)tel)|(?:tel(?:\W*aviv))/i),
1993
+ transform: toValueSet('te'),
1994
+ keepMatching: true
1995
+ },
1996
+ {
1997
+ field: 'languages',
1998
+ pattern: /\bt[aâ]m(?:il)?\b/i,
1999
+ transform: toValueSet('ta'),
2000
+ keepMatching: true
2001
+ },
2002
+ {
2003
+ field: 'languages',
2004
+ pattern: /\b(?:(?:w{3}\.\w+\.)?MAL(?:ay)?|malayalam)\b/i,
2005
+ validateMatch: validateNotMatch(/\b(?:(?:w{3}\.\w+\.)MAL)\b/i),
2006
+ transform: toValueSet('ml'),
2007
+ keepMatching: true,
2008
+ remove: true,
2009
+ skipIfFirst: true
2010
+ },
2011
+ {
2012
+ field: 'languages',
2013
+ pattern: /\b(?:(?:w{3}\.\w+\.)?KAN(?:nada)?|kannada)\b/i,
2014
+ validateMatch: validateNotMatch(/\b(?:(?:w{3}\.\w+\.)KAN)\b/i),
2015
+ transform: toValueSet('kn'),
2016
+ keepMatching: true,
2017
+ remove: true
2018
+ },
2019
+ {
2020
+ field: 'languages',
2021
+ pattern: /\b(?:(?:w{3}\.\w+\.)?MAR(?:a(?:thi)?)?|marathi)\b/i,
2022
+ validateMatch: validateNotMatch(/\b(?:(?:w{3}\.\w+\.)MAR)\b/i),
2023
+ transform: toValueSet('mr'),
2024
+ keepMatching: true
2025
+ },
2026
+ {
2027
+ field: 'languages',
2028
+ pattern: /\b(?:(?:w{3}\.\w+\.)?GUJ(?:arati)?|gujarati)\b/i,
2029
+ validateMatch: validateNotMatch(/\b(?:(?:w{3}\.\w+\.)GUJ)\b/i),
2030
+ transform: toValueSet('gu'),
2031
+ keepMatching: true
2032
+ },
2033
+ {
2034
+ field: 'languages',
2035
+ pattern: /\b(?:(?:w{3}\.\w+\.)?PUN(?:jabi)?|punjabi)\b/i,
2036
+ validateMatch: validateNotMatch(/\b(?:(?:w{3}\.\w+\.)PUN)\b/i),
2037
+ transform: toValueSet('pa'),
2038
+ keepMatching: true
2039
+ },
2040
+ {
2041
+ field: 'languages',
2042
+ pattern: /\b(?:(?:w{3}\.\w+\.)?BEN(?:.\bThe|and|of\b)?(?:gali)?|bengali)\b/i,
2043
+ validateMatch: validateNotMatch(/\b(?:(?:w{3}\.\w+\.)BEN)|(?:BEN)(?:.\bThe|and|of\b)\b/i),
2044
+ transform: toValueSet('bn'),
2045
+ keepMatching: true,
2046
+ skipIfFirst: true
2047
+ },
2048
+ // Baltic language handlers
2049
+ {
2050
+ field: 'languages',
2051
+ pattern: /\b(?:YTS\.)?LT\b/i,
2052
+ validateMatch: validateNotMatch(/(?:YTS\.)/i),
2053
+ transform: toValueSet('lt'),
2054
+ keepMatching: true,
2055
+ skipFromTitle: true
2056
+ },
2057
+ {
2058
+ field: 'languages',
2059
+ pattern: /\blithuanian\b/i,
2060
+ transform: toValueSet('lt'),
2061
+ keepMatching: true,
2062
+ skipIfFirst: true
2063
+ },
2064
+ {
2065
+ field: 'languages',
2066
+ pattern: /\blatvian\b/i,
2067
+ transform: toValueSet('lv'),
2068
+ keepMatching: true,
2069
+ skipIfFirst: true
2070
+ },
2071
+ {
2072
+ field: 'languages',
2073
+ pattern: /\bestonian\b/i,
2074
+ transform: toValueSet('et'),
2075
+ keepMatching: true,
2076
+ skipIfFirst: true
2077
+ },
2078
+ // Polish language handlers
2079
+ {
2080
+ field: 'languages',
2081
+ pattern: /\b(?:PLDUB|Dub(?:bing.?)?PL|Lek(?:tor.?)?PL|Film.Polski)\b/i,
2082
+ transform: toValueSet('pl'),
2083
+ keepMatching: true,
2084
+ remove: true
2085
+ },
2086
+ {
2087
+ field: 'languages',
2088
+ pattern: /\b(?:Napisy.PL|PLSUB(?:BED)?)\b/i,
2089
+ transform: toValueSet('pl'),
2090
+ keepMatching: true,
2091
+ remove: true
2092
+ },
2093
+ {
2094
+ field: 'languages',
2095
+ pattern: /\b(?:(?:w{3}\.\w+\.)?PL|pol)\b/i,
2096
+ validateMatch: validateNotMatch(/(?:w{3}\.\w+\.)/i),
2097
+ transform: toValueSet('pl'),
2098
+ keepMatching: true
2099
+ },
2100
+ {
2101
+ field: 'languages',
2102
+ pattern: /\b(polish|polon[eê]s|polaco)\b/i,
2103
+ transform: toValueSet('pl'),
2104
+ keepMatching: true,
2105
+ skipIfFirst: true
2106
+ },
2107
+ // Czech/Slovak language handlers
2108
+ {
2109
+ field: 'languages',
2110
+ pattern: /\bCZ[EH]?\b/i,
2111
+ transform: toValueSet('cs'),
2112
+ keepMatching: true,
2113
+ skipIfFirst: true
2114
+ },
2115
+ {
2116
+ field: 'languages',
2117
+ pattern: /\bczech\b/i,
2118
+ transform: toValueSet('cs'),
2119
+ keepMatching: true,
2120
+ skipIfFirst: true
2121
+ },
2122
+ {
2123
+ field: 'languages',
2124
+ pattern: /\bslo(?:vak|vakian|subs|[\]_)]?\.\w{2,4}$)\b/i,
2125
+ transform: toValueSet('sk'),
2126
+ keepMatching: true,
2127
+ skipFromTitle: true
2128
+ },
2129
+ // Hungarian language handlers
2130
+ {
2131
+ field: 'languages',
2132
+ pattern: /\bHU\b/i,
2133
+ transform: toValueSet('hu'),
2134
+ keepMatching: true,
2135
+ skipFromTitle: true
2136
+ },
2137
+ {
2138
+ field: 'languages',
2139
+ pattern: /\bHUN(?:garian)?\b/i,
2140
+ transform: toValueSet('hu'),
2141
+ keepMatching: true,
2142
+ skipFromTitle: true
2143
+ },
2144
+ // Romanian language handlers
2145
+ {
2146
+ field: 'languages',
2147
+ pattern: /\bROM(?:anian)?\b/i,
2148
+ transform: toValueSet('ro'),
2149
+ keepMatching: true,
2150
+ skipFromTitle: true
2151
+ },
2152
+ {
2153
+ field: 'languages',
2154
+ pattern: /\bRO(?:[ .,/-]*(?:[A-Z]{2}[ .,/-]+)*sub)/i,
2155
+ transform: toValueSet('ro'),
2156
+ keepMatching: true
2157
+ },
2158
+ // Bulgarian language handlers
2159
+ {
2160
+ field: 'languages',
2161
+ pattern: /\bbul(?:garian)?\b/i,
2162
+ transform: toValueSet('bg'),
2163
+ keepMatching: true,
2164
+ skipFromTitle: true
2165
+ },
2166
+ // Serbian/Croatian/Slovenian language handlers
2167
+ {
2168
+ field: 'languages',
2169
+ pattern: /\b(?:srp|serbian)\b/i,
2170
+ transform: toValueSet('sr'),
2171
+ keepMatching: true
2172
+ },
2173
+ {
2174
+ field: 'languages',
2175
+ pattern: /\b(?:HRV|croatian)\b/i,
2176
+ transform: toValueSet('hr'),
2177
+ keepMatching: true
2178
+ },
2179
+ {
2180
+ field: 'languages',
2181
+ pattern: /\bHR(?:[ .,/-]*(?:[A-Z]{2}[ .,/-]+)*sub\w*)\b/i,
2182
+ transform: toValueSet('hr'),
2183
+ keepMatching: true
2184
+ },
2185
+ {
2186
+ field: 'languages',
2187
+ pattern: /\bslovenian\b/i,
2188
+ transform: toValueSet('sl'),
2189
+ keepMatching: true,
2190
+ skipFromTitle: true
2191
+ },
2192
+ // Dutch language handlers
2193
+ {
2194
+ field: 'languages',
2195
+ pattern: /\b(?:(?:w{3}\.\w+\.)?NL|dut|holand[eê]s)\b/i,
2196
+ validateMatch: validateNotMatch(/(?:w{3}\.\w+\.)NL/i),
2197
+ transform: toValueSet('nl'),
2198
+ keepMatching: true
2199
+ },
2200
+ {
2201
+ field: 'languages',
2202
+ pattern: /\bdutch\b/i,
2203
+ transform: toValueSet('nl'),
2204
+ keepMatching: true,
2205
+ skipFromTitle: true
2206
+ },
2207
+ {
2208
+ field: 'languages',
2209
+ pattern: /\bflemish\b/i,
2210
+ transform: toValueSet('nl'),
2211
+ keepMatching: true
2212
+ },
2213
+ // Danish language handlers
2214
+ {
2215
+ field: 'languages',
2216
+ pattern: /\b(?:DK|danska|dansub|nordic)\b/i,
2217
+ transform: toValueSet('da'),
2218
+ keepMatching: true
2219
+ },
2220
+ {
2221
+ field: 'languages',
2222
+ pattern: /\b(danish|dinamarqu[eê]s)\b/i,
2223
+ transform: toValueSet('da'),
2224
+ keepMatching: true,
2225
+ skipFromTitle: true
2226
+ },
2227
+ {
2228
+ field: 'languages',
2229
+ pattern: /\bdan\b(?:.*\.(?:srt|vtt|ssa|ass|sub|idx)$)/i,
2230
+ transform: toValueSet('da'),
2231
+ keepMatching: true,
2232
+ skipFromTitle: true
2233
+ },
2234
+ // Finnish language handlers
2235
+ {
2236
+ field: 'languages',
2237
+ pattern: /\b(?:(?:w{3}\.\w+\.|Sci-)?FI|finsk|finsub|nordic)\b/i,
2238
+ validateMatch: validateNotMatch(/(?:w{3}\.\w+\.|Sci-)FI/i),
2239
+ transform: toValueSet('fi'),
2240
+ keepMatching: true
2241
+ },
2242
+ {
2243
+ field: 'languages',
2244
+ pattern: /\bfinnish\b/i,
2245
+ transform: toValueSet('fi'),
2246
+ keepMatching: true,
2247
+ skipFromTitle: true
2248
+ },
2249
+ // Swedish language handlers
2250
+ {
2251
+ field: 'languages',
2252
+ pattern: /\b(?:(?:w{3}\.\w+\.)?SE|swe|swesubs?|sv(?:ensk)?|nordic)\b/i,
2253
+ validateMatch: validateNotMatch(/(?:w{3}\.\w+\.)SE/i),
2254
+ transform: toValueSet('sv'),
2255
+ keepMatching: true
2256
+ },
2257
+ {
2258
+ field: 'languages',
2259
+ pattern: /\b(swedish|sueco)\b/i,
2260
+ transform: toValueSet('sv'),
2261
+ keepMatching: true,
2262
+ skipFromTitle: true
2263
+ },
2264
+ // Norwegian language handlers
2265
+ {
2266
+ field: 'languages',
2267
+ pattern: /\b(?:NOR|norsk|norsub|nordic)\b/i,
2268
+ transform: toValueSet('no'),
2269
+ keepMatching: true
2270
+ },
2271
+ {
2272
+ field: 'languages',
2273
+ pattern: /\b(norwegian|noruegu[eê]s|bokm[aå]l|nob|nor(?:[\]_)]?\.\w{2,4}$))\b/i,
2274
+ transform: toValueSet('no'),
2275
+ keepMatching: true,
2276
+ skipFromTitle: true
2277
+ },
2278
+ // Arabic language handlers
2279
+ {
2280
+ field: 'languages',
2281
+ pattern: /\b(?:arabic|[aá]rabe|ara)\b/i,
2282
+ transform: toValueSet('ar'),
2283
+ keepMatching: true,
2284
+ skipIfFirst: true
2285
+ },
2286
+ {
2287
+ field: 'languages',
2288
+ pattern: /\barab.*(?:audio|lang(?:uage)?|sub(?:s|titles?)?)\b/i,
2289
+ transform: toValueSet('ar'),
2290
+ keepMatching: true,
2291
+ skipFromTitle: true
2292
+ },
2293
+ {
2294
+ field: 'languages',
2295
+ pattern: /\bar(?:\.(?:ass|ssa|srt|sub|idx)$)/i,
2296
+ transform: toValueSet('ar'),
2297
+ keepMatching: true,
2298
+ skipFromTitle: true
2299
+ },
2300
+ // Turkish language handlers
2301
+ {
2302
+ field: 'languages',
2303
+ pattern: /\b(?:turkish|tur(?:co)?)\b/i,
2304
+ transform: toValueSet('tr'),
2305
+ keepMatching: true,
2306
+ skipFromTitle: true
2307
+ },
2308
+ {
2309
+ field: 'languages',
2310
+ pattern: /\b(TİVİBU|tivibu|bitturk(?:\.net)?|turktorrent)\b/i,
2311
+ transform: toValueSet('tr'),
2312
+ keepMatching: true,
2313
+ skipFromTitle: true
2314
+ },
2315
+ // Vietnamese language handlers
2316
+ {
2317
+ field: 'languages',
2318
+ pattern: /\bvietnamese\b|\bvie(?:[\]_)]?\.\w{2,4}$)/i,
2319
+ transform: toValueSet('vi'),
2320
+ keepMatching: true,
2321
+ skipFromTitle: true
2322
+ },
2323
+ // Indonesian language handlers
2324
+ {
2325
+ field: 'languages',
2326
+ pattern: /\bind(?:onesian)?\b/i,
2327
+ transform: toValueSet('id'),
2328
+ keepMatching: true,
2329
+ skipFromTitle: true
2330
+ },
2331
+ // Thai language handlers
2332
+ {
2333
+ field: 'languages',
2334
+ pattern: /\b(thai|tailand[eê]s)\b/i,
2335
+ transform: toValueSet('th'),
2336
+ keepMatching: true,
2337
+ skipIfFirst: true
2338
+ },
2339
+ {
2340
+ field: 'languages',
2341
+ pattern: /\b(THA|tha)\b/,
2342
+ transform: toValueSet('th'),
2343
+ keepMatching: true,
2344
+ skipFromTitle: true
2345
+ },
2346
+ // Malay language handlers
2347
+ {
2348
+ field: 'languages',
2349
+ pattern: /\b(?:malay|may(?:[\]_)]?\.\w{2,4}$)|(?:subs?\([a-z,]+)may)\b/i,
2350
+ transform: toValueSet('ms'),
2351
+ keepMatching: true
2352
+ },
2353
+ // Hebrew language handlers
2354
+ {
2355
+ field: 'languages',
2356
+ pattern: /\bheb(?:rew|raico)?\b/i,
2357
+ transform: toValueSet('he'),
2358
+ keepMatching: true,
2359
+ skipFromTitle: true
2360
+ },
2361
+ // Persian language handlers
2362
+ {
2363
+ field: 'languages',
2364
+ pattern: /\b(persian|persa)\b/i,
2365
+ transform: toValueSet('fa'),
2366
+ keepMatching: true,
2367
+ skipFromTitle: true
2368
+ },
2369
+ // Unicode script detection for languages
2370
+ {
2371
+ field: 'languages',
2372
+ pattern: /[\u3040-\u30ff]+/i,
2373
+ transform: toValueSet('ja'),
2374
+ keepMatching: true,
2375
+ skipFromTitle: true
2376
+ },
2377
+ {
2378
+ field: 'languages',
2379
+ pattern: /[\u3400-\u4dbf]+/i,
2380
+ transform: toValueSet('zh'),
2381
+ keepMatching: true,
2382
+ skipFromTitle: true
2383
+ },
2384
+ {
2385
+ field: 'languages',
2386
+ pattern: /[\u4e00-\u9fff]+/i,
2387
+ transform: toValueSet('zh'),
2388
+ keepMatching: true,
2389
+ skipFromTitle: true
2390
+ },
2391
+ {
2392
+ field: 'languages',
2393
+ pattern: /[\uf900-\ufaff]+/i,
2394
+ transform: toValueSet('zh'),
2395
+ keepMatching: true,
2396
+ skipFromTitle: true
2397
+ },
2398
+ {
2399
+ field: 'languages',
2400
+ pattern: /[\uff66-\uff9f]+/i,
2401
+ transform: toValueSet('ja'),
2402
+ keepMatching: true,
2403
+ skipFromTitle: true
2404
+ },
2405
+ {
2406
+ field: 'languages',
2407
+ pattern: /[\u0400-\u04ff]+/i,
2408
+ transform: toValueSet('ru'),
2409
+ keepMatching: true,
2410
+ skipFromTitle: true
2411
+ },
2412
+ {
2413
+ field: 'languages',
2414
+ pattern: /[\u0600-\u06ff]+/i,
2415
+ transform: toValueSet('ar'),
2416
+ keepMatching: true,
2417
+ skipFromTitle: true
2418
+ },
2419
+ {
2420
+ field: 'languages',
2421
+ pattern: /[\u0750-\u077f]+/i,
2422
+ transform: toValueSet('ar'),
2423
+ keepMatching: true,
2424
+ skipFromTitle: true
2425
+ },
2426
+ {
2427
+ field: 'languages',
2428
+ pattern: /[\u0c80-\u0cff]+/i,
2429
+ transform: toValueSet('kn'),
2430
+ keepMatching: true,
2431
+ skipFromTitle: true
2432
+ },
2433
+ {
2434
+ field: 'languages',
2435
+ pattern: /[\u0d00-\u0d7f]+/i,
2436
+ transform: toValueSet('ml'),
2437
+ keepMatching: true,
2438
+ skipFromTitle: true
2439
+ },
2440
+ {
2441
+ field: 'languages',
2442
+ pattern: /[\u0e00-\u0e7f]+/i,
2443
+ transform: toValueSet('th'),
2444
+ keepMatching: true,
2445
+ skipFromTitle: true
2446
+ },
2447
+ {
2448
+ field: 'languages',
2449
+ pattern: /[\u0900-\u097f]+/i,
2450
+ transform: toValueSet('hi'),
2451
+ keepMatching: true,
2452
+ skipFromTitle: true
2453
+ },
2454
+ {
2455
+ field: 'languages',
2456
+ pattern: /[\u0980-\u09ff]+/i,
2457
+ transform: toValueSet('bn'),
2458
+ keepMatching: true,
2459
+ skipFromTitle: true
2460
+ },
2461
+ {
2462
+ field: 'languages',
2463
+ pattern: /[\u0a00-\u0a7f]+/i,
2464
+ transform: toValueSet('gu'),
2465
+ keepMatching: true,
2466
+ skipFromTitle: true
2467
+ },
2468
+ // Portuguese/Spanish episode detection
2469
+ {
2470
+ field: 'languages',
2471
+ process: (title, m, result) => {
2472
+ const ere = /capitulo|ao/i;
2473
+ const tre = /dublado/i;
2474
+ m.mIndex = 0;
2475
+ m.mValue = '';
2476
+ const vs = m.value;
2477
+ if (vs && vs.exists && (vs.exists('pt') || vs.exists('es'))) {
2478
+ return m;
2479
+ }
2480
+ const em = result.get('episodes');
2481
+ if ((em && em.mValue && ere.test(em.mValue)) || tre.test(title)) {
2482
+ if (!vs || !vs.append) {
2483
+ const newVs = new ValueSet();
2484
+ m.value = newVs.append('pt');
2485
+ }
2486
+ else {
2487
+ m.value = vs.append('pt');
2488
+ }
2489
+ }
2490
+ return m;
2491
+ }
2492
+ },
2493
+ // Subbed handlers
2494
+ {
2495
+ field: 'subbed',
2496
+ pattern: /\b(?:Official.*?|Dual-?)?sub(?:s|bed)?\b/i,
2497
+ transform: toBoolean(),
2498
+ remove: true
2499
+ },
2500
+ {
2501
+ field: 'subbed',
2502
+ process: (title, m, result) => {
2503
+ const lm = result.get('languages');
2504
+ if (!lm) {
2505
+ return m;
2506
+ }
2507
+ const s = lm.value;
2508
+ if (s && s.exists && s.exists('multi subs')) {
2509
+ m.value = true;
2510
+ }
2511
+ return m;
2512
+ }
2513
+ },
2514
+ // Dubbed handlers
2515
+ {
2516
+ field: 'dubbed',
2517
+ pattern: /\b(?:fan\s?dub)\b/i,
2518
+ transform: toBoolean(),
2519
+ remove: true,
2520
+ skipFromTitle: true
2521
+ },
2522
+ {
2523
+ field: 'dubbed',
2524
+ pattern: /\b(?:Fan.*)?(?:DUBBED|dublado|dubbing|DUBS?)\b/i,
2525
+ transform: toBoolean(),
2526
+ remove: true
2527
+ },
2528
+ {
2529
+ field: 'dubbed',
2530
+ pattern: /\b(?:.*\bsub(?:s|bed)?\b)?(?:[ _\-\[(\.])?(dual|multi)(?:[ _\-\[(\.])?(?:audio)\b/i,
2531
+ validateMatch: validateNotMatch(/\b(?:.*\bsub(s|bed)?\b)/i),
2532
+ transform: toBoolean(),
2533
+ remove: true
2534
+ },
2535
+ {
2536
+ field: 'dubbed',
2537
+ pattern: /\b(?:DUBBED|dublado|dubbing|DUBS?)\b/i,
2538
+ transform: toBoolean()
2539
+ },
2540
+ {
2541
+ field: 'dubbed',
2542
+ process: (title, m, result) => {
2543
+ const lm = result.get('languages');
2544
+ if (!lm) {
2545
+ return m;
2546
+ }
2547
+ const s = lm.value;
2548
+ if (s && s.exists && (s.exists('multi audio') || s.exists('dual audio'))) {
2549
+ m.value = true;
2550
+ }
2551
+ return m;
2552
+ }
2553
+ },
2554
+ // Size handler
2555
+ {
2556
+ field: 'size',
2557
+ pattern: /\b(\d+(\.\d+)?\s?(MB|GB|TB))\b/i,
2558
+ remove: true
2559
+ },
2560
+ // Site handlers
2561
+ {
2562
+ field: 'site',
2563
+ pattern: /\[([^\[\].]+\.[^\].]+)\](?:\.\w{2,4}$|\s)/i,
2564
+ transform: toTrimmed(),
2565
+ remove: true,
2566
+ skipFromTitle: true,
2567
+ matchGroup: 1
2568
+ },
2569
+ {
2570
+ field: 'site',
2571
+ pattern: /[\[{(](www.\w*.\w+)[)}\]]/i,
2572
+ remove: true,
2573
+ skipFromTitle: true
2574
+ },
2575
+ {
2576
+ field: 'site',
2577
+ pattern: /\b(?:www?.?)?(?:\w+\-)?\w+[\.\s](?:com|org|net|ms|tv|mx|co|party|vip|nu|pics)\b/i,
2578
+ remove: true,
2579
+ skipFromTitle: true
2580
+ },
2581
+ // Network handlers
2582
+ {
2583
+ field: 'network',
2584
+ pattern: /\bATVP?\b/i,
2585
+ transform: toValue('Apple TV'),
2586
+ remove: true
2587
+ },
2588
+ {
2589
+ field: 'network',
2590
+ pattern: /\bAMZN\b/i,
2591
+ transform: toValue('Amazon'),
2592
+ remove: true
2593
+ },
2594
+ {
2595
+ field: 'network',
2596
+ pattern: /\bNF|Netflix\b/i,
2597
+ transform: toValue('Netflix'),
2598
+ remove: true
2599
+ },
2600
+ {
2601
+ field: 'network',
2602
+ pattern: /\bNICK(?:elodeon)?\b/i,
2603
+ transform: toValue('Nickelodeon'),
2604
+ remove: true
2605
+ },
2606
+ {
2607
+ field: 'network',
2608
+ pattern: /\bDSNY?P?\b/i,
2609
+ transform: toValue('Disney'),
2610
+ remove: true
2611
+ },
2612
+ {
2613
+ field: 'network',
2614
+ pattern: /\bH(MAX|BO)\b/i,
2615
+ transform: toValue('HBO'),
2616
+ remove: true
2617
+ },
2618
+ {
2619
+ field: 'network',
2620
+ pattern: /\bHULU\b/i,
2621
+ transform: toValue('Hulu'),
2622
+ remove: true
2623
+ },
2624
+ {
2625
+ field: 'network',
2626
+ pattern: /\bCBS\b/i,
2627
+ transform: toValue('CBS'),
2628
+ remove: true
2629
+ },
2630
+ {
2631
+ field: 'network',
2632
+ pattern: /\bNBC\b/i,
2633
+ transform: toValue('NBC'),
2634
+ remove: true
2635
+ },
2636
+ {
2637
+ field: 'network',
2638
+ pattern: /\bAMC\b/i,
2639
+ transform: toValue('AMC'),
2640
+ remove: true
2641
+ },
2642
+ {
2643
+ field: 'network',
2644
+ pattern: /\bPBS\b/i,
2645
+ transform: toValue('PBS'),
2646
+ remove: true
2647
+ },
2648
+ {
2649
+ field: 'network',
2650
+ pattern: /\b(Crunchyroll|[. -]CR[. -])\b/i,
2651
+ transform: toValue('Crunchyroll'),
2652
+ remove: true
2653
+ },
2654
+ {
2655
+ field: 'network',
2656
+ pattern: /\bVICE\b/,
2657
+ transform: toValue('VICE'),
2658
+ remove: true
2659
+ },
2660
+ {
2661
+ field: 'network',
2662
+ pattern: /\bSony\b/i,
2663
+ transform: toValue('Sony'),
2664
+ remove: true
2665
+ },
2666
+ {
2667
+ field: 'network',
2668
+ pattern: /\bHallmark\b/i,
2669
+ transform: toValue('Hallmark'),
2670
+ remove: true
2671
+ },
2672
+ {
2673
+ field: 'network',
2674
+ pattern: /\bAdult.?Swim\b/i,
2675
+ transform: toValue('Adult Swim'),
2676
+ remove: true
2677
+ },
2678
+ {
2679
+ field: 'network',
2680
+ pattern: /\bAnimal.?Planet|ANPL\b/i,
2681
+ transform: toValue('Animal Planet'),
2682
+ remove: true
2683
+ },
2684
+ {
2685
+ field: 'network',
2686
+ pattern: /\bCartoon.?Network(?:.TOONAMI.BROADCAST)?\b/i,
2687
+ transform: toValue('Cartoon Network'),
2688
+ remove: true
2689
+ },
2690
+ // Group handlers (final)
2691
+ {
2692
+ field: 'group',
2693
+ pattern: /\b(INFLATE|DEFLATE)\b/,
2694
+ remove: true
2695
+ },
2696
+ {
2697
+ field: 'group',
2698
+ pattern: /\b(?:Erai-raws|Erai-raws\.com)\b/i,
2699
+ transform: toValue('Erai-raws'),
2700
+ remove: true
2701
+ },
2702
+ {
2703
+ field: 'group',
2704
+ pattern: /^\[([^\[\]]+)]/
2705
+ },
2706
+ {
2707
+ field: 'group',
2708
+ pattern: /\(([\w-]+)\)(?:$|\.\w{2,4}$)/
2709
+ },
2710
+ {
2711
+ field: 'group',
2712
+ process: (title, m, result) => {
2713
+ const re = /^\[.+]$/;
2714
+ if (m.mValue && re.test(m.mValue)) {
2715
+ const endIndex = m.mIndex + m.mValue.length;
2716
+ // remove anime group match if some other parameter is contained in it, since it's a false positive.
2717
+ for (const [key, km] of result.entries()) {
2718
+ if (km.mIndex > 0 && km.mIndex < endIndex) {
2719
+ m.value = null;
2720
+ return m;
2721
+ }
2722
+ }
2723
+ }
2724
+ m.mIndex = 0;
2725
+ m.mValue = '';
2726
+ return m;
2727
+ }
2728
+ },
2729
+ // Extension handler
2730
+ {
2731
+ field: 'extension',
2732
+ pattern: /\.(3g2|3gp|avi|flv|mkv|mk3d|mov|mp2|mp4|m4v|mpe|mpeg|mpg|mpv|webm|wmv|ogm|divx|ts|m2ts|iso|vob|sub|idx|ttxt|txt|smi|srt|ssa|ass|vtt|nfo|html)$/i,
2733
+ transform: toLowercase()
2734
+ },
2735
+ // Final MP3 audio handler
2736
+ {
2737
+ field: 'audio',
2738
+ pattern: /\bMP3\b/i,
2739
+ transform: toValueSet('MP3'),
2740
+ remove: true,
2741
+ keepMatching: true
2742
+ }
2743
+ ];
2744
+ //# sourceMappingURL=handlers.js.map