zantetsu 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.
- package/.gitkeep +0 -0
- package/dist/index.d.ts +34 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +321 -0
- package/dist/index.js.map +1 -0
- package/jest.config.js +24 -0
- package/package.json +42 -0
- package/src/index.test.ts +164 -0
- package/src/index.ts +376 -0
- package/src/types.d.ts +83 -0
- package/tsconfig.json +23 -0
package/.gitkeep
ADDED
|
File without changes
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Zantetsu - Fast anime metadata parser
|
|
3
|
+
*
|
|
4
|
+
* JavaScript/TypeScript bindings for the heuristic parser engine.
|
|
5
|
+
* Provides both a class-based API and convenience functions.
|
|
6
|
+
* Includes a pure JavaScript fallback when native module is unavailable.
|
|
7
|
+
*/
|
|
8
|
+
import type { HeuristicParserOptions, ParseResult } from './types.js';
|
|
9
|
+
/**
|
|
10
|
+
* HeuristicParser - Fast regex-based anime filename parser
|
|
11
|
+
*
|
|
12
|
+
* Uses optimized regex patterns and scene naming rules for
|
|
13
|
+
* instant parsing with zero ML overhead.
|
|
14
|
+
*/
|
|
15
|
+
export declare class HeuristicParser {
|
|
16
|
+
private parser;
|
|
17
|
+
constructor(_options?: HeuristicParserOptions);
|
|
18
|
+
parse(input: string): ParseResult;
|
|
19
|
+
parseBatch(inputs: string[]): ParseResult[];
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Parse a single filename using the default parser
|
|
23
|
+
*/
|
|
24
|
+
export declare function parse(input: string): ParseResult;
|
|
25
|
+
/**
|
|
26
|
+
* Parse multiple filenames using the default parser
|
|
27
|
+
*/
|
|
28
|
+
export declare function parseBatch(inputs: string[]): ParseResult[];
|
|
29
|
+
/**
|
|
30
|
+
* Check if native module is being used
|
|
31
|
+
*/
|
|
32
|
+
export declare function isUsingNativeModule(): boolean;
|
|
33
|
+
export type { HeuristicParserOptions, ParseResult, EpisodeSpec, Resolution, VideoCodec, AudioCodec, MediaSource, ParseMode } from './types.js';
|
|
34
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAGH,OAAO,KAAK,EACV,sBAAsB,EACtB,WAAW,EAOZ,MAAM,YAAY,CAAC;AAgSpB;;;;;GAKG;AACH,qBAAa,eAAe;IAC1B,OAAO,CAAC,MAAM,CAAwC;gBAE1C,QAAQ,CAAC,EAAE,sBAAsB;IAI7C,KAAK,CAAC,KAAK,EAAE,MAAM,GAAG,WAAW;IAcjC,UAAU,CAAC,MAAM,EAAE,MAAM,EAAE,GAAG,WAAW,EAAE;CAO5C;AAYD;;GAEG;AACH,wBAAgB,KAAK,CAAC,KAAK,EAAE,MAAM,GAAG,WAAW,CAEhD;AAED;;GAEG;AACH,wBAAgB,UAAU,CAAC,MAAM,EAAE,MAAM,EAAE,GAAG,WAAW,EAAE,CAE1D;AAED;;GAEG;AACH,wBAAgB,mBAAmB,IAAI,OAAO,CAG7C;AAGD,YAAY,EAAE,sBAAsB,EAAE,WAAW,EAAE,WAAW,EAAE,UAAU,EAAE,UAAU,EAAE,UAAU,EAAE,WAAW,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Zantetsu - Fast anime metadata parser
|
|
3
|
+
*
|
|
4
|
+
* JavaScript/TypeScript bindings for the heuristic parser engine.
|
|
5
|
+
* Provides both a class-based API and convenience functions.
|
|
6
|
+
* Includes a pure JavaScript fallback when native module is unavailable.
|
|
7
|
+
*/
|
|
8
|
+
// Track if native module is available
|
|
9
|
+
let useNative = false;
|
|
10
|
+
let nativeModule = null;
|
|
11
|
+
let initialized = false;
|
|
12
|
+
/**
|
|
13
|
+
* Initialize the native module (done automatically on first use)
|
|
14
|
+
*/
|
|
15
|
+
function initNative() {
|
|
16
|
+
if (initialized)
|
|
17
|
+
return;
|
|
18
|
+
initialized = true;
|
|
19
|
+
try {
|
|
20
|
+
// Try to load the native addon
|
|
21
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
22
|
+
nativeModule = require('@zan/heuristic-node');
|
|
23
|
+
useNative = true;
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
// Use JavaScript fallback
|
|
27
|
+
nativeModule = null;
|
|
28
|
+
useNative = false;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* JavaScript-only fallback parser using regex patterns
|
|
33
|
+
* Matches the HeuristicParser from the Rust crate
|
|
34
|
+
*/
|
|
35
|
+
class JsHeuristicParser {
|
|
36
|
+
// Simplified regex patterns
|
|
37
|
+
reResolution = /1080p|720p|480p|2160p/i;
|
|
38
|
+
reGroup = /^\[([^\]]+)\]/;
|
|
39
|
+
// Match episode after dash, space, or dot - but not after year
|
|
40
|
+
reEpisode = /(?:[\s\-.])(?:[Ee]p?\.?)?\s*(\d{1,4})(?:\b|v\d|[^\d])/;
|
|
41
|
+
reEpisodeV = /(?:[\s\-.])(?:[Ee]p?\.?)?\s*(\d{1,4})v(\d+)/i;
|
|
42
|
+
reEpisodeRange = /(?:[\s\-.])(?:[Ee]p?\.?)?\s*(\d{1,4})\s*[-~]\s*(\d{1,4})/i;
|
|
43
|
+
reSeason = /(?:^|[\s\-])S(\d+)/i;
|
|
44
|
+
reYear = /\((\d{4})\)/;
|
|
45
|
+
reExtension = /\.(\w+)$/;
|
|
46
|
+
reCrc32 = /\[([A-Fa-f0-9]{8})\]/;
|
|
47
|
+
parse(input) {
|
|
48
|
+
const trimmed = input.trim();
|
|
49
|
+
if (!trimmed) {
|
|
50
|
+
throw new Error('input is empty or whitespace-only');
|
|
51
|
+
}
|
|
52
|
+
const result = {
|
|
53
|
+
input: trimmed,
|
|
54
|
+
title: null,
|
|
55
|
+
group: null,
|
|
56
|
+
episode: null,
|
|
57
|
+
season: null,
|
|
58
|
+
resolution: null,
|
|
59
|
+
video_codec: null,
|
|
60
|
+
audio_codec: null,
|
|
61
|
+
source: null,
|
|
62
|
+
year: null,
|
|
63
|
+
crc32: null,
|
|
64
|
+
extension: null,
|
|
65
|
+
version: null,
|
|
66
|
+
confidence: 0,
|
|
67
|
+
parse_mode: 'Light',
|
|
68
|
+
};
|
|
69
|
+
// Extract basic fields
|
|
70
|
+
result.group = this.extractGroup(trimmed);
|
|
71
|
+
result.extension = this.extractExtension(trimmed);
|
|
72
|
+
result.crc32 = this.extractCrc32(trimmed);
|
|
73
|
+
result.resolution = this.extractResolution(trimmed);
|
|
74
|
+
result.season = this.extractSeason(trimmed);
|
|
75
|
+
result.year = this.extractYear(trimmed);
|
|
76
|
+
result.episode = this.extractEpisode(trimmed);
|
|
77
|
+
// Extract title
|
|
78
|
+
result.title = this.extractTitle(trimmed, result);
|
|
79
|
+
// Compute confidence
|
|
80
|
+
result.confidence = this.computeConfidence(result);
|
|
81
|
+
return result;
|
|
82
|
+
}
|
|
83
|
+
extractGroup(input) {
|
|
84
|
+
const match = this.reGroup.exec(input);
|
|
85
|
+
return match ? match[1].trim() : null;
|
|
86
|
+
}
|
|
87
|
+
extractExtension(input) {
|
|
88
|
+
const match = this.reExtension.exec(input);
|
|
89
|
+
return match ? match[1].toLowerCase() : null;
|
|
90
|
+
}
|
|
91
|
+
extractCrc32(input) {
|
|
92
|
+
const match = this.reCrc32.exec(input);
|
|
93
|
+
return match ? match[1].toUpperCase() : null;
|
|
94
|
+
}
|
|
95
|
+
extractResolution(input) {
|
|
96
|
+
if (input.includes('2160p') || input.includes('2160i'))
|
|
97
|
+
return 'UHD2160';
|
|
98
|
+
if (input.includes('1080p') || input.includes('1080i'))
|
|
99
|
+
return 'FHD1080';
|
|
100
|
+
if (input.includes('720p') || input.includes('720i'))
|
|
101
|
+
return 'HD720';
|
|
102
|
+
if (input.includes('480p') || input.includes('480i'))
|
|
103
|
+
return 'SD480';
|
|
104
|
+
return null;
|
|
105
|
+
}
|
|
106
|
+
extractSeason(input) {
|
|
107
|
+
const match = this.reSeason.exec(input);
|
|
108
|
+
return match ? parseInt(match[1], 10) : null;
|
|
109
|
+
}
|
|
110
|
+
extractYear(input) {
|
|
111
|
+
const match = this.reYear.exec(input);
|
|
112
|
+
return match ? parseInt(match[1], 10) : null;
|
|
113
|
+
}
|
|
114
|
+
extractEpisode(input) {
|
|
115
|
+
// Try versioned episode
|
|
116
|
+
let match = this.reEpisodeV.exec(input);
|
|
117
|
+
if (match) {
|
|
118
|
+
return { type: 'versioned', episode: parseInt(match[1], 10), version: parseInt(match[2], 10) };
|
|
119
|
+
}
|
|
120
|
+
// Try episode range
|
|
121
|
+
match = this.reEpisodeRange.exec(input);
|
|
122
|
+
if (match) {
|
|
123
|
+
const start = parseInt(match[1], 10);
|
|
124
|
+
const end = parseInt(match[2], 10);
|
|
125
|
+
if (start < end) {
|
|
126
|
+
return { type: 'range', start, end };
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
// Try single episode
|
|
130
|
+
match = this.reEpisode.exec(input);
|
|
131
|
+
if (match) {
|
|
132
|
+
return { type: 'single', episode: parseInt(match[1], 10) };
|
|
133
|
+
}
|
|
134
|
+
return null;
|
|
135
|
+
}
|
|
136
|
+
extractTitle(input, result) {
|
|
137
|
+
let work = input;
|
|
138
|
+
// Remove group tag
|
|
139
|
+
if (result.group) {
|
|
140
|
+
const groupIdx = work.indexOf(']');
|
|
141
|
+
if (groupIdx !== -1) {
|
|
142
|
+
work = work.substring(groupIdx + 1);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
// Remove extension
|
|
146
|
+
if (result.extension) {
|
|
147
|
+
const extPos = work.lastIndexOf(`.${result.extension}`);
|
|
148
|
+
if (extPos !== -1) {
|
|
149
|
+
work = work.substring(0, extPos);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
// Remove episode info
|
|
153
|
+
work = work.replace(this.reEpisodeV, ' ');
|
|
154
|
+
work = work.replace(this.reEpisodeRange, ' ');
|
|
155
|
+
work = work.replace(this.reEpisode, ' ');
|
|
156
|
+
work = work.replace(this.reSeason, ' ');
|
|
157
|
+
work = work.replace(this.reYear, ' ');
|
|
158
|
+
// Remove bracketed content
|
|
159
|
+
work = work.replace(/\[[^\]]*\]/g, ' ');
|
|
160
|
+
work = work.replace(/\([^\)]*\)/g, ' ');
|
|
161
|
+
// Clean up
|
|
162
|
+
const cleaned = work
|
|
163
|
+
.replace(/[._]/g, ' ')
|
|
164
|
+
.replace(/-/g, ' ')
|
|
165
|
+
.split(/\s+/)
|
|
166
|
+
.filter(Boolean)
|
|
167
|
+
.join(' ')
|
|
168
|
+
.trim();
|
|
169
|
+
return cleaned || null;
|
|
170
|
+
}
|
|
171
|
+
computeConfidence(result) {
|
|
172
|
+
let fieldsPresent = 0;
|
|
173
|
+
let fieldsTotal = 7;
|
|
174
|
+
if (result.title) {
|
|
175
|
+
fieldsPresent += 2;
|
|
176
|
+
fieldsTotal += 1;
|
|
177
|
+
}
|
|
178
|
+
if (result.group)
|
|
179
|
+
fieldsPresent += 1;
|
|
180
|
+
if (result.episode)
|
|
181
|
+
fieldsPresent += 1;
|
|
182
|
+
if (result.resolution)
|
|
183
|
+
fieldsPresent += 1;
|
|
184
|
+
if (result.video_codec)
|
|
185
|
+
fieldsPresent += 1;
|
|
186
|
+
if (result.audio_codec)
|
|
187
|
+
fieldsPresent += 1;
|
|
188
|
+
if (result.source)
|
|
189
|
+
fieldsPresent += 1;
|
|
190
|
+
return Math.min(fieldsPresent / fieldsTotal, 1.0);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
// Singleton instances
|
|
194
|
+
let jsParser = null;
|
|
195
|
+
let nativeParser = null;
|
|
196
|
+
function getParser() {
|
|
197
|
+
initNative();
|
|
198
|
+
if (useNative) {
|
|
199
|
+
if (!nativeParser) {
|
|
200
|
+
// eslint-disable-next-line new-cap
|
|
201
|
+
nativeParser = new nativeModule.HeuristicParser();
|
|
202
|
+
}
|
|
203
|
+
return nativeParser;
|
|
204
|
+
}
|
|
205
|
+
if (!jsParser) {
|
|
206
|
+
jsParser = new JsHeuristicParser();
|
|
207
|
+
}
|
|
208
|
+
return jsParser;
|
|
209
|
+
}
|
|
210
|
+
/**
|
|
211
|
+
* Convert native episode spec to typed EpisodeSpec
|
|
212
|
+
*/
|
|
213
|
+
function convertEpisodeSpec(native) {
|
|
214
|
+
if (!native)
|
|
215
|
+
return null;
|
|
216
|
+
if (typeof native === 'number') {
|
|
217
|
+
return { type: 'single', episode: native };
|
|
218
|
+
}
|
|
219
|
+
const n = native;
|
|
220
|
+
if (typeof n.type === 'number') {
|
|
221
|
+
switch (n.type) {
|
|
222
|
+
case 0:
|
|
223
|
+
return { type: 'single', episode: n.episode };
|
|
224
|
+
case 1:
|
|
225
|
+
return { type: 'range', start: n.start, end: n.end };
|
|
226
|
+
case 2:
|
|
227
|
+
return { type: 'multi', episodes: n.episodes };
|
|
228
|
+
case 3:
|
|
229
|
+
return { type: 'versioned', episode: n.episode, version: n.version };
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
if (n.episode !== undefined && n.version !== undefined) {
|
|
233
|
+
return { type: 'versioned', episode: n.episode, version: n.version };
|
|
234
|
+
}
|
|
235
|
+
if (n.start !== undefined && n.end !== undefined) {
|
|
236
|
+
return { type: 'range', start: n.start, end: n.end };
|
|
237
|
+
}
|
|
238
|
+
if (n.episodes !== undefined) {
|
|
239
|
+
return { type: 'multi', episodes: n.episodes };
|
|
240
|
+
}
|
|
241
|
+
return null;
|
|
242
|
+
}
|
|
243
|
+
/**
|
|
244
|
+
* Convert native parse result to typed ParseResult
|
|
245
|
+
*/
|
|
246
|
+
function convertResult(native) {
|
|
247
|
+
const n = native;
|
|
248
|
+
return {
|
|
249
|
+
input: n.input,
|
|
250
|
+
title: n.title,
|
|
251
|
+
group: n.group,
|
|
252
|
+
episode: convertEpisodeSpec(n.episode),
|
|
253
|
+
season: n.season,
|
|
254
|
+
resolution: n.resolution,
|
|
255
|
+
video_codec: n.video_codec,
|
|
256
|
+
audio_codec: n.audio_codec,
|
|
257
|
+
source: n.source,
|
|
258
|
+
year: n.year,
|
|
259
|
+
crc32: n.crc32,
|
|
260
|
+
extension: n.extension,
|
|
261
|
+
version: n.version,
|
|
262
|
+
confidence: n.confidence,
|
|
263
|
+
parse_mode: n.parse_mode,
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
/**
|
|
267
|
+
* HeuristicParser - Fast regex-based anime filename parser
|
|
268
|
+
*
|
|
269
|
+
* Uses optimized regex patterns and scene naming rules for
|
|
270
|
+
* instant parsing with zero ML overhead.
|
|
271
|
+
*/
|
|
272
|
+
export class HeuristicParser {
|
|
273
|
+
parser;
|
|
274
|
+
constructor(_options) {
|
|
275
|
+
this.parser = getParser();
|
|
276
|
+
}
|
|
277
|
+
parse(input) {
|
|
278
|
+
if (typeof input !== 'string' || !input.trim()) {
|
|
279
|
+
throw new Error('Input must be a non-empty string');
|
|
280
|
+
}
|
|
281
|
+
const result = this.parser.parse(input);
|
|
282
|
+
if (!useNative) {
|
|
283
|
+
return result;
|
|
284
|
+
}
|
|
285
|
+
return convertResult(result);
|
|
286
|
+
}
|
|
287
|
+
parseBatch(inputs) {
|
|
288
|
+
if (!Array.isArray(inputs)) {
|
|
289
|
+
throw new Error('Input must be an array of strings');
|
|
290
|
+
}
|
|
291
|
+
return inputs.map(input => this.parse(input));
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
// Default parser instance for convenience functions
|
|
295
|
+
let defaultParser = null;
|
|
296
|
+
function getDefaultParser() {
|
|
297
|
+
if (!defaultParser) {
|
|
298
|
+
defaultParser = new HeuristicParser();
|
|
299
|
+
}
|
|
300
|
+
return defaultParser;
|
|
301
|
+
}
|
|
302
|
+
/**
|
|
303
|
+
* Parse a single filename using the default parser
|
|
304
|
+
*/
|
|
305
|
+
export function parse(input) {
|
|
306
|
+
return getDefaultParser().parse(input);
|
|
307
|
+
}
|
|
308
|
+
/**
|
|
309
|
+
* Parse multiple filenames using the default parser
|
|
310
|
+
*/
|
|
311
|
+
export function parseBatch(inputs) {
|
|
312
|
+
return getDefaultParser().parseBatch(inputs);
|
|
313
|
+
}
|
|
314
|
+
/**
|
|
315
|
+
* Check if native module is being used
|
|
316
|
+
*/
|
|
317
|
+
export function isUsingNativeModule() {
|
|
318
|
+
initNative();
|
|
319
|
+
return useNative;
|
|
320
|
+
}
|
|
321
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAcH,sCAAsC;AACtC,IAAI,SAAS,GAAG,KAAK,CAAC;AACtB,IAAI,YAAY,GAAY,IAAI,CAAC;AACjC,IAAI,WAAW,GAAG,KAAK,CAAC;AAExB;;GAEG;AACH,SAAS,UAAU;IACjB,IAAI,WAAW;QAAE,OAAO;IACxB,WAAW,GAAG,IAAI,CAAC;IAEnB,IAAI,CAAC;QACH,+BAA+B;QAC/B,iEAAiE;QACjE,YAAY,GAAG,OAAO,CAAC,qBAAqB,CAAC,CAAC;QAC9C,SAAS,GAAG,IAAI,CAAC;IACnB,CAAC;IAAC,MAAM,CAAC;QACP,0BAA0B;QAC1B,YAAY,GAAG,IAAI,CAAC;QACpB,SAAS,GAAG,KAAK,CAAC;IACpB,CAAC;AACH,CAAC;AAED;;;GAGG;AACH,MAAM,iBAAiB;IACrB,4BAA4B;IACX,YAAY,GAAG,wBAAwB,CAAC;IACxC,OAAO,GAAG,eAAe,CAAC;IAC3C,+DAA+D;IAC9C,SAAS,GAAG,uDAAuD,CAAC;IACpE,UAAU,GAAG,8CAA8C,CAAC;IAC5D,cAAc,GAAG,2DAA2D,CAAC;IAC7E,QAAQ,GAAG,qBAAqB,CAAC;IACjC,MAAM,GAAG,aAAa,CAAC;IACvB,WAAW,GAAG,UAAU,CAAC;IACzB,OAAO,GAAG,sBAAsB,CAAC;IAElD,KAAK,CAAC,KAAa;QACjB,MAAM,OAAO,GAAG,KAAK,CAAC,IAAI,EAAE,CAAC;QAC7B,IAAI,CAAC,OAAO,EAAE,CAAC;YACb,MAAM,IAAI,KAAK,CAAC,mCAAmC,CAAC,CAAC;QACvD,CAAC;QAED,MAAM,MAAM,GAAgB;YAC1B,KAAK,EAAE,OAAO;YACd,KAAK,EAAE,IAAI;YACX,KAAK,EAAE,IAAI;YACX,OAAO,EAAE,IAAI;YACb,MAAM,EAAE,IAAI;YACZ,UAAU,EAAE,IAAI;YAChB,WAAW,EAAE,IAAI;YACjB,WAAW,EAAE,IAAI;YACjB,MAAM,EAAE,IAAI;YACZ,IAAI,EAAE,IAAI;YACV,KAAK,EAAE,IAAI;YACX,SAAS,EAAE,IAAI;YACf,OAAO,EAAE,IAAI;YACb,UAAU,EAAE,CAAC;YACb,UAAU,EAAE,OAAO;SACpB,CAAC;QAEF,uBAAuB;QACvB,MAAM,CAAC,KAAK,GAAG,IAAI,CAAC,YAAY,CAAC,OAAO,CAAC,CAAC;QAC1C,MAAM,CAAC,SAAS,GAAG,IAAI,CAAC,gBAAgB,CAAC,OAAO,CAAC,CAAC;QAClD,MAAM,CAAC,KAAK,GAAG,IAAI,CAAC,YAAY,CAAC,OAAO,CAAC,CAAC;QAC1C,MAAM,CAAC,UAAU,GAAG,IAAI,CAAC,iBAAiB,CAAC,OAAO,CAAC,CAAC;QACpD,MAAM,CAAC,MAAM,GAAG,IAAI,CAAC,aAAa,CAAC,OAAO,CAAC,CAAC;QAC5C,MAAM,CAAC,IAAI,GAAG,IAAI,CAAC,WAAW,CAAC,OAAO,CAAC,CAAC;QACxC,MAAM,CAAC,OAAO,GAAG,IAAI,CAAC,cAAc,CAAC,OAAO,CAAC,CAAC;QAE9C,gBAAgB;QAChB,MAAM,CAAC,KAAK,GAAG,IAAI,CAAC,YAAY,CAAC,OAAO,EAAE,MAAM,CAAC,CAAC;QAElD,qBAAqB;QACrB,MAAM,CAAC,UAAU,GAAG,IAAI,CAAC,iBAAiB,CAAC,MAAM,CAAC,CAAC;QAEnD,OAAO,MAAM,CAAC;IAChB,CAAC;IAEO,YAAY,CAAC,KAAa;QAChC,MAAM,KAAK,GAAG,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QACvC,OAAO,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC;IACxC,CAAC;IAEO,gBAAgB,CAAC,KAAa;QACpC,MAAM,KAAK,GAAG,IAAI,CAAC,WAAW,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QAC3C,OAAO,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,WAAW,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC;IAC/C,CAAC;IAEO,YAAY,CAAC,KAAa;QAChC,MAAM,KAAK,GAAG,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QACvC,OAAO,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,WAAW,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC;IAC/C,CAAC;IAEO,iBAAiB,CAAC,KAAa;QACrC,IAAI,KAAK,CAAC,QAAQ,CAAC,OAAO,CAAC,IAAI,KAAK,CAAC,QAAQ,CAAC,OAAO,CAAC;YAAE,OAAO,SAAS,CAAC;QACzE,IAAI,KAAK,CAAC,QAAQ,CAAC,OAAO,CAAC,IAAI,KAAK,CAAC,QAAQ,CAAC,OAAO,CAAC;YAAE,OAAO,SAAS,CAAC;QACzE,IAAI,KAAK,CAAC,QAAQ,CAAC,MAAM,CAAC,IAAI,KAAK,CAAC,QAAQ,CAAC,MAAM,CAAC;YAAE,OAAO,OAAO,CAAC;QACrE,IAAI,KAAK,CAAC,QAAQ,CAAC,MAAM,CAAC,IAAI,KAAK,CAAC,QAAQ,CAAC,MAAM,CAAC;YAAE,OAAO,OAAO,CAAC;QACrE,OAAO,IAAI,CAAC;IACd,CAAC;IAEO,aAAa,CAAC,KAAa;QACjC,MAAM,KAAK,GAAG,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QACxC,OAAO,KAAK,CAAC,CAAC,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;IAC/C,CAAC;IAEO,WAAW,CAAC,KAAa;QAC/B,MAAM,KAAK,GAAG,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QACtC,OAAO,KAAK,CAAC,CAAC,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;IAC/C,CAAC;IAEO,cAAc,CAAC,KAAa;QAClC,wBAAwB;QACxB,IAAI,KAAK,GAAG,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QACxC,IAAI,KAAK,EAAE,CAAC;YACV,OAAO,EAAE,IAAI,EAAE,WAAW,EAAE,OAAO,EAAE,QAAQ,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,EAAE,OAAO,EAAE,QAAQ,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,EAAE,CAAC;QACjG,CAAC;QAED,oBAAoB;QACpB,KAAK,GAAG,IAAI,CAAC,cAAc,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QACxC,IAAI,KAAK,EAAE,CAAC;YACV,MAAM,KAAK,GAAG,QAAQ,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;YACrC,MAAM,GAAG,GAAG,QAAQ,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;YACnC,IAAI,KAAK,GAAG,GAAG,EAAE,CAAC;gBAChB,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,KAAK,EAAE,GAAG,EAAE,CAAC;YACvC,CAAC;QACH,CAAC;QAED,qBAAqB;QACrB,KAAK,GAAG,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QACnC,IAAI,KAAK,EAAE,CAAC;YACV,OAAO,EAAE,IAAI,EAAE,QAAQ,EAAE,OAAO,EAAE,QAAQ,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,EAAE,CAAC;QAC7D,CAAC;QAED,OAAO,IAAI,CAAC;IACd,CAAC;IAEO,YAAY,CAAC,KAAa,EAAE,MAAmB;QACrD,IAAI,IAAI,GAAG,KAAK,CAAC;QAEjB,mBAAmB;QACnB,IAAI,MAAM,CAAC,KAAK,EAAE,CAAC;YACjB,MAAM,QAAQ,GAAG,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;YACnC,IAAI,QAAQ,KAAK,CAAC,CAAC,EAAE,CAAC;gBACpB,IAAI,GAAG,IAAI,CAAC,SAAS,CAAC,QAAQ,GAAG,CAAC,CAAC,CAAC;YACtC,CAAC;QACH,CAAC;QAED,mBAAmB;QACnB,IAAI,MAAM,CAAC,SAAS,EAAE,CAAC;YACrB,MAAM,MAAM,GAAG,IAAI,CAAC,WAAW,CAAC,IAAI,MAAM,CAAC,SAAS,EAAE,CAAC,CAAC;YACxD,IAAI,MAAM,KAAK,CAAC,CAAC,EAAE,CAAC;gBAClB,IAAI,GAAG,IAAI,CAAC,SAAS,CAAC,CAAC,EAAE,MAAM,CAAC,CAAC;YACnC,CAAC;QACH,CAAC;QAED,sBAAsB;QACtB,IAAI,GAAG,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,UAAU,EAAE,GAAG,CAAC,CAAC;QAC1C,IAAI,GAAG,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,cAAc,EAAE,GAAG,CAAC,CAAC;QAC9C,IAAI,GAAG,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,SAAS,EAAE,GAAG,CAAC,CAAC;QACzC,IAAI,GAAG,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,QAAQ,EAAE,GAAG,CAAC,CAAC;QACxC,IAAI,GAAG,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;QAEtC,2BAA2B;QAC3B,IAAI,GAAG,IAAI,CAAC,OAAO,CAAC,aAAa,EAAE,GAAG,CAAC,CAAC;QACxC,IAAI,GAAG,IAAI,CAAC,OAAO,CAAC,aAAa,EAAE,GAAG,CAAC,CAAC;QAExC,WAAW;QACX,MAAM,OAAO,GAAG,IAAI;aACjB,OAAO,CAAC,OAAO,EAAE,GAAG,CAAC;aACrB,OAAO,CAAC,IAAI,EAAE,GAAG,CAAC;aAClB,KAAK,CAAC,KAAK,CAAC;aACZ,MAAM,CAAC,OAAO,CAAC;aACf,IAAI,CAAC,GAAG,CAAC;aACT,IAAI,EAAE,CAAC;QAEV,OAAO,OAAO,IAAI,IAAI,CAAC;IACzB,CAAC;IAEO,iBAAiB,CAAC,MAAmB;QAC3C,IAAI,aAAa,GAAG,CAAC,CAAC;QACtB,IAAI,WAAW,GAAG,CAAC,CAAC;QAEpB,IAAI,MAAM,CAAC,KAAK,EAAE,CAAC;YACjB,aAAa,IAAI,CAAC,CAAC;YACnB,WAAW,IAAI,CAAC,CAAC;QACnB,CAAC;QACD,IAAI,MAAM,CAAC,KAAK;YAAE,aAAa,IAAI,CAAC,CAAC;QACrC,IAAI,MAAM,CAAC,OAAO;YAAE,aAAa,IAAI,CAAC,CAAC;QACvC,IAAI,MAAM,CAAC,UAAU;YAAE,aAAa,IAAI,CAAC,CAAC;QAC1C,IAAI,MAAM,CAAC,WAAW;YAAE,aAAa,IAAI,CAAC,CAAC;QAC3C,IAAI,MAAM,CAAC,WAAW;YAAE,aAAa,IAAI,CAAC,CAAC;QAC3C,IAAI,MAAM,CAAC,MAAM;YAAE,aAAa,IAAI,CAAC,CAAC;QAEtC,OAAO,IAAI,CAAC,GAAG,CAAC,aAAa,GAAG,WAAW,EAAE,GAAG,CAAC,CAAC;IACpD,CAAC;CACF;AAED,sBAAsB;AACtB,IAAI,QAAQ,GAA6B,IAAI,CAAC;AAC9C,IAAI,YAAY,GAAY,IAAI,CAAC;AAEjC,SAAS,SAAS;IAChB,UAAU,EAAE,CAAC;IAEb,IAAI,SAAS,EAAE,CAAC;QACd,IAAI,CAAC,YAAY,EAAE,CAAC;YAClB,mCAAmC;YACnC,YAAY,GAAG,IAAK,YAAiF,CAAC,eAAe,EAAE,CAAC;QAC1H,CAAC;QACD,OAAO,YAAiD,CAAC;IAC3D,CAAC;IAED,IAAI,CAAC,QAAQ,EAAE,CAAC;QACd,QAAQ,GAAG,IAAI,iBAAiB,EAAE,CAAC;IACrC,CAAC;IACD,OAAO,QAAQ,CAAC;AAClB,CAAC;AAED;;GAEG;AACH,SAAS,kBAAkB,CAAC,MAAe;IACzC,IAAI,CAAC,MAAM;QAAE,OAAO,IAAI,CAAC;IAEzB,IAAI,OAAO,MAAM,KAAK,QAAQ,EAAE,CAAC;QAC/B,OAAO,EAAE,IAAI,EAAE,QAAQ,EAAE,OAAO,EAAE,MAAM,EAAE,CAAC;IAC7C,CAAC;IAED,MAAM,CAAC,GAAG,MAAiC,CAAC;IAE5C,IAAI,OAAO,CAAC,CAAC,IAAI,KAAK,QAAQ,EAAE,CAAC;QAC/B,QAAQ,CAAC,CAAC,IAAI,EAAE,CAAC;YACf,KAAK,CAAC;gBACJ,OAAO,EAAE,IAAI,EAAE,QAAQ,EAAE,OAAO,EAAE,CAAC,CAAC,OAAiB,EAAE,CAAC;YAC1D,KAAK,CAAC;gBACJ,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,KAAK,EAAE,CAAC,CAAC,KAAe,EAAE,GAAG,EAAE,CAAC,CAAC,GAAa,EAAE,CAAC;YAC3E,KAAK,CAAC;gBACJ,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,QAAQ,EAAE,CAAC,CAAC,QAAoB,EAAE,CAAC;YAC7D,KAAK,CAAC;gBACJ,OAAO,EAAE,IAAI,EAAE,WAAW,EAAE,OAAO,EAAE,CAAC,CAAC,OAAiB,EAAE,OAAO,EAAE,CAAC,CAAC,OAAiB,EAAE,CAAC;QAC7F,CAAC;IACH,CAAC;IAED,IAAI,CAAC,CAAC,OAAO,KAAK,SAAS,IAAI,CAAC,CAAC,OAAO,KAAK,SAAS,EAAE,CAAC;QACvD,OAAO,EAAE,IAAI,EAAE,WAAW,EAAE,OAAO,EAAE,CAAC,CAAC,OAAiB,EAAE,OAAO,EAAE,CAAC,CAAC,OAAiB,EAAE,CAAC;IAC3F,CAAC;IACD,IAAI,CAAC,CAAC,KAAK,KAAK,SAAS,IAAI,CAAC,CAAC,GAAG,KAAK,SAAS,EAAE,CAAC;QACjD,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,KAAK,EAAE,CAAC,CAAC,KAAe,EAAE,GAAG,EAAE,CAAC,CAAC,GAAa,EAAE,CAAC;IAC3E,CAAC;IACD,IAAI,CAAC,CAAC,QAAQ,KAAK,SAAS,EAAE,CAAC;QAC7B,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,QAAQ,EAAE,CAAC,CAAC,QAAoB,EAAE,CAAC;IAC7D,CAAC;IAED,OAAO,IAAI,CAAC;AACd,CAAC;AAED;;GAEG;AACH,SAAS,aAAa,CAAC,MAAe;IACpC,MAAM,CAAC,GAAG,MAAiC,CAAC;IAC5C,OAAO;QACL,KAAK,EAAE,CAAC,CAAC,KAAe;QACxB,KAAK,EAAE,CAAC,CAAC,KAAsB;QAC/B,KAAK,EAAE,CAAC,CAAC,KAAsB;QAC/B,OAAO,EAAE,kBAAkB,CAAC,CAAC,CAAC,OAAO,CAAC;QACtC,MAAM,EAAE,CAAC,CAAC,MAAuB;QACjC,UAAU,EAAE,CAAC,CAAC,UAA+B;QAC7C,WAAW,EAAE,CAAC,CAAC,WAAgC;QAC/C,WAAW,EAAE,CAAC,CAAC,WAAgC;QAC/C,MAAM,EAAE,CAAC,CAAC,MAA4B;QACtC,IAAI,EAAE,CAAC,CAAC,IAAqB;QAC7B,KAAK,EAAE,CAAC,CAAC,KAAsB;QAC/B,SAAS,EAAE,CAAC,CAAC,SAA0B;QACvC,OAAO,EAAE,CAAC,CAAC,OAAwB;QACnC,UAAU,EAAE,CAAC,CAAC,UAAoB;QAClC,UAAU,EAAE,CAAC,CAAC,UAAuB;KACtC,CAAC;AACJ,CAAC;AAED;;;;;GAKG;AACH,MAAM,OAAO,eAAe;IAClB,MAAM,CAAwC;IAEtD,YAAY,QAAiC;QAC3C,IAAI,CAAC,MAAM,GAAG,SAAS,EAAE,CAAC;IAC5B,CAAC;IAED,KAAK,CAAC,KAAa;QACjB,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,CAAC,KAAK,CAAC,IAAI,EAAE,EAAE,CAAC;YAC/C,MAAM,IAAI,KAAK,CAAC,kCAAkC,CAAC,CAAC;QACtD,CAAC;QAED,MAAM,MAAM,GAAG,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;QAExC,IAAI,CAAC,SAAS,EAAE,CAAC;YACf,OAAO,MAAqB,CAAC;QAC/B,CAAC;QAED,OAAO,aAAa,CAAC,MAAM,CAAC,CAAC;IAC/B,CAAC;IAED,UAAU,CAAC,MAAgB;QACzB,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,CAAC;YAC3B,MAAM,IAAI,KAAK,CAAC,mCAAmC,CAAC,CAAC;QACvD,CAAC;QAED,OAAO,MAAM,CAAC,GAAG,CAAC,KAAK,CAAC,EAAE,CAAC,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,CAAC;IAChD,CAAC;CACF;AAED,oDAAoD;AACpD,IAAI,aAAa,GAA2B,IAAI,CAAC;AAEjD,SAAS,gBAAgB;IACvB,IAAI,CAAC,aAAa,EAAE,CAAC;QACnB,aAAa,GAAG,IAAI,eAAe,EAAE,CAAC;IACxC,CAAC;IACD,OAAO,aAAa,CAAC;AACvB,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,KAAK,CAAC,KAAa;IACjC,OAAO,gBAAgB,EAAE,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;AACzC,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,UAAU,CAAC,MAAgB;IACzC,OAAO,gBAAgB,EAAE,CAAC,UAAU,CAAC,MAAM,CAAC,CAAC;AAC/C,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,mBAAmB;IACjC,UAAU,EAAE,CAAC;IACb,OAAO,SAAS,CAAC;AACnB,CAAC"}
|
package/jest.config.js
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export default {
|
|
2
|
+
preset: 'ts-jest/presets/default-esm',
|
|
3
|
+
testEnvironment: 'node',
|
|
4
|
+
extensionsToTreatAsEsm: ['.ts'],
|
|
5
|
+
moduleNameMapper: {
|
|
6
|
+
'^(\\.{1,2}/.*)\\.js$': '$1',
|
|
7
|
+
},
|
|
8
|
+
transform: {
|
|
9
|
+
'^.+\\.tsx?$': [
|
|
10
|
+
'ts-jest',
|
|
11
|
+
{
|
|
12
|
+
useESM: true,
|
|
13
|
+
tsconfig: {
|
|
14
|
+
module: 'ES2022',
|
|
15
|
+
moduleResolution: 'bundler',
|
|
16
|
+
},
|
|
17
|
+
},
|
|
18
|
+
],
|
|
19
|
+
},
|
|
20
|
+
testMatch: ['**/*.test.ts'],
|
|
21
|
+
collectCoverageFrom: ['src/**/*.ts', '!src/**/*.test.ts'],
|
|
22
|
+
coverageDirectory: 'coverage',
|
|
23
|
+
testTimeout: 10000,
|
|
24
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "zantetsu",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Fast anime metadata parser - extracts title, episode, resolution, codecs from filenames",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"types": "dist/index.d.ts",
|
|
7
|
+
"type": "module",
|
|
8
|
+
"scripts": {
|
|
9
|
+
"build": "tsc",
|
|
10
|
+
"test": "node --experimental-vm-modules node_modules/jest/bin/jest.js",
|
|
11
|
+
"clean": "rm -rf dist"
|
|
12
|
+
},
|
|
13
|
+
"keywords": [
|
|
14
|
+
"anime",
|
|
15
|
+
"parser",
|
|
16
|
+
"metadata",
|
|
17
|
+
"torrent",
|
|
18
|
+
"filename",
|
|
19
|
+
"heuristic"
|
|
20
|
+
],
|
|
21
|
+
"author": "kokoro",
|
|
22
|
+
"license": "MIT",
|
|
23
|
+
"repository": {
|
|
24
|
+
"type": "git",
|
|
25
|
+
"url": "https://github.com/enrell/zantetsu"
|
|
26
|
+
},
|
|
27
|
+
"devDependencies": {
|
|
28
|
+
"@types/node": "^22.0.0",
|
|
29
|
+
"jest": "^29.7.0",
|
|
30
|
+
"ts-jest": "^29.2.0",
|
|
31
|
+
"typescript": "^5.6.0"
|
|
32
|
+
},
|
|
33
|
+
"engines": {
|
|
34
|
+
"node": ">=18.0.0"
|
|
35
|
+
},
|
|
36
|
+
"exports": {
|
|
37
|
+
".": {
|
|
38
|
+
"types": "./dist/index.d.ts",
|
|
39
|
+
"default": "./dist/index.js"
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Zantetsu - Jest test suite for heuristic parser
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { HeuristicParser, parse, parseBatch } from '../src/index.js';
|
|
6
|
+
|
|
7
|
+
describe('HeuristicParser', () => {
|
|
8
|
+
let parser: HeuristicParser;
|
|
9
|
+
|
|
10
|
+
beforeAll(() => {
|
|
11
|
+
parser = new HeuristicParser();
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
describe('constructor', () => {
|
|
15
|
+
it('should create a parser instance', () => {
|
|
16
|
+
expect(parser).toBeDefined();
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it('should accept options', () => {
|
|
20
|
+
const parserWithOptions = new HeuristicParser({ debug: true });
|
|
21
|
+
expect(parserWithOptions).toBeDefined();
|
|
22
|
+
});
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
describe('parse()', () => {
|
|
26
|
+
it('should parse SubsPlease format correctly', () => {
|
|
27
|
+
const result = parser.parse('[SubsPlease] Jujutsu Kaisen - 24 (1080p) [A1B2C3D4].mkv');
|
|
28
|
+
|
|
29
|
+
expect(result.title).toBe('Jujutsu Kaisen');
|
|
30
|
+
expect(result.group).toBe('SubsPlease');
|
|
31
|
+
expect(result.episode).toEqual({ type: 'single', episode: 24 });
|
|
32
|
+
expect(result.resolution).toBe('FHD1080');
|
|
33
|
+
expect(result.crc32).toBe('A1B2C3D4');
|
|
34
|
+
expect(result.extension).toBe('mkv');
|
|
35
|
+
expect(result.confidence).toBeGreaterThan(0.5);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it('should parse Erai-raws versioned episode format', () => {
|
|
39
|
+
const result = parser.parse(
|
|
40
|
+
'[Erai-raws] Shingeki no Kyojin - The Final Season - 28v2 [1080p][HEVC].mkv'
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
expect(result.group).toBe('Erai-raws');
|
|
44
|
+
expect(result.episode).toEqual({ type: 'versioned', episode: 28, version: 2 });
|
|
45
|
+
expect(result.resolution).toBe('FHD1080');
|
|
46
|
+
expect(result.video_codec).toBe('HEVC');
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('should parse batch episode range', () => {
|
|
50
|
+
const result = parser.parse('[Judas] Golden Kamuy S3 - 01-12 (1080p) [Batch]');
|
|
51
|
+
|
|
52
|
+
expect(result.group).toBe('Judas');
|
|
53
|
+
expect(result.season).toBe(3);
|
|
54
|
+
expect(result.episode).toEqual({ type: 'range', start: 1, end: 12 });
|
|
55
|
+
expect(result.resolution).toBe('FHD1080');
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('should parse dot-separated format', () => {
|
|
59
|
+
const result = parser.parse('One.Piece.1084.VOSTFR.1080p.WEB.x264-AAC.mkv');
|
|
60
|
+
|
|
61
|
+
expect(result.title).toBe('One Piece');
|
|
62
|
+
expect(result.episode).toEqual({ type: 'single', episode: 1084 });
|
|
63
|
+
expect(result.resolution).toBe('FHD1080');
|
|
64
|
+
expect(result.video_codec).toBe('H264');
|
|
65
|
+
expect(result.audio_codec).toBe('AAC');
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it('should handle various resolutions', () => {
|
|
69
|
+
const res480 = parser.parse('[Test] Show - 01 (480p).mkv');
|
|
70
|
+
expect(res480.resolution).toBe('SD480');
|
|
71
|
+
|
|
72
|
+
const res720 = parser.parse('[Test] Show - 01 (720p).mkv');
|
|
73
|
+
expect(res720.resolution).toBe('HD720');
|
|
74
|
+
|
|
75
|
+
const res1080 = parser.parse('[Test] Show - 01 (1080p).mkv');
|
|
76
|
+
expect(res1080.resolution).toBe('FHD1080');
|
|
77
|
+
|
|
78
|
+
const res2160 = parser.parse('[Test] Show - 01 (2160p).mkv');
|
|
79
|
+
expect(res2160.resolution).toBe('UHD2160');
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it('should extract video codecs', () => {
|
|
83
|
+
expect(parser.parse('[G] Title - 01 [x264].mkv').video_codec).toBe('H264');
|
|
84
|
+
expect(parser.parse('[G] Title - 01 [x265].mkv').video_codec).toBe('HEVC');
|
|
85
|
+
expect(parser.parse('[G] Title - 01 [HEVC].mkv').video_codec).toBe('HEVC');
|
|
86
|
+
expect(parser.parse('[G] Title - 01 [AV1].mkv').video_codec).toBe('AV1');
|
|
87
|
+
expect(parser.parse('[G] Title - 01 [VP9].mkv').video_codec).toBe('VP9');
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it('should extract audio codecs', () => {
|
|
91
|
+
expect(parser.parse('[G] Title - 01 [FLAC].mkv').audio_codec).toBe('FLAC');
|
|
92
|
+
expect(parser.parse('[G] Title - 01 [AAC].mkv').audio_codec).toBe('AAC');
|
|
93
|
+
expect(parser.parse('[G] Title - 01 [Opus].mkv').audio_codec).toBe('Opus');
|
|
94
|
+
expect(parser.parse('[G] Title - 01 [AC3].mkv').audio_codec).toBe('AC3');
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it('should extract media sources', () => {
|
|
98
|
+
expect(parser.parse('[G] Title - 01 Blu-ray 1080p.mkv').source).toBe('BluRay');
|
|
99
|
+
expect(parser.parse('[G] Title - 01 WEB-DL 1080p.mkv').source).toBe('WebDL');
|
|
100
|
+
expect(parser.parse('[G] Title - 01 HDTV 720p.mkv').source).toBe('HDTV');
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it('should extract year', () => {
|
|
104
|
+
const result = parser.parse('[Group] Title (2024) - 01 (1080p).mkv');
|
|
105
|
+
expect(result.year).toBe(2024);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it('should compute confidence based on extracted fields', () => {
|
|
109
|
+
// Minimal parse - only title
|
|
110
|
+
const minimal = parser.parse('Some Random Title.mkv');
|
|
111
|
+
expect(minimal.confidence).toBeLessThan(0.5);
|
|
112
|
+
|
|
113
|
+
// Rich parse - many fields
|
|
114
|
+
const rich = parser.parse(
|
|
115
|
+
'[SubsPlease] Jujutsu Kaisen - 24 (1080p) [H264] [AAC] [A1B2C3D4].mkv'
|
|
116
|
+
);
|
|
117
|
+
expect(rich.confidence).toBeGreaterThan(0.7);
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it('should throw on empty input', () => {
|
|
121
|
+
expect(() => parser.parse('')).toThrow();
|
|
122
|
+
expect(() => parser.parse(' ')).toThrow();
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it('should return correct parse mode', () => {
|
|
126
|
+
const result = parser.parse('[Test] Title - 01 (1080p).mkv');
|
|
127
|
+
expect(result.parse_mode).toBe('Light');
|
|
128
|
+
});
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
describe('parseBatch()', () => {
|
|
132
|
+
it('should parse multiple inputs', () => {
|
|
133
|
+
const inputs = [
|
|
134
|
+
'[SubsPlease] Jujutsu Kaisen - 24 (1080p).mkv',
|
|
135
|
+
'[Erai-raws] One Piece - 1000 [1080p].mkv',
|
|
136
|
+
'Anime.Title.01.720p.WEB.x264.mkv',
|
|
137
|
+
];
|
|
138
|
+
|
|
139
|
+
const results = parser.parseBatch(inputs);
|
|
140
|
+
|
|
141
|
+
expect(results).toHaveLength(3);
|
|
142
|
+
expect(results[0].title).toBe('Jujutsu Kaisen');
|
|
143
|
+
expect(results[1].title).toBe('One Piece');
|
|
144
|
+
expect(results[2].title).toBe('Anime Title');
|
|
145
|
+
});
|
|
146
|
+
});
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
describe('Convenience functions', () => {
|
|
150
|
+
describe('parse()', () => {
|
|
151
|
+
it('should work as a convenience function', () => {
|
|
152
|
+
const result = parse('[Test] Anime - 01 (1080p).mkv');
|
|
153
|
+
expect(result.title).toBe('Anime');
|
|
154
|
+
expect(result.episode).toEqual({ type: 'single', episode: 1 });
|
|
155
|
+
});
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
describe('parseBatch()', () => {
|
|
159
|
+
it('should work as a convenience function', () => {
|
|
160
|
+
const results = parseBatch(['[Test] A - 01.mkv', '[Test] B - 02.mkv']);
|
|
161
|
+
expect(results).toHaveLength(2);
|
|
162
|
+
});
|
|
163
|
+
});
|
|
164
|
+
});
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,376 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Zantetsu - Fast anime metadata parser
|
|
3
|
+
*
|
|
4
|
+
* JavaScript/TypeScript bindings for the heuristic parser engine.
|
|
5
|
+
* Provides both a class-based API and convenience functions.
|
|
6
|
+
* Includes a pure JavaScript fallback when native module is unavailable.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
// Import types from the declaration file
|
|
10
|
+
import type {
|
|
11
|
+
HeuristicParserOptions,
|
|
12
|
+
ParseResult,
|
|
13
|
+
EpisodeSpec,
|
|
14
|
+
Resolution,
|
|
15
|
+
VideoCodec,
|
|
16
|
+
AudioCodec,
|
|
17
|
+
MediaSource,
|
|
18
|
+
ParseMode
|
|
19
|
+
} from './types.js';
|
|
20
|
+
|
|
21
|
+
// Track if native module is available
|
|
22
|
+
let useNative = false;
|
|
23
|
+
let nativeModule: unknown = null;
|
|
24
|
+
let initialized = false;
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Initialize the native module (done automatically on first use)
|
|
28
|
+
*/
|
|
29
|
+
function initNative(): void {
|
|
30
|
+
if (initialized) return;
|
|
31
|
+
initialized = true;
|
|
32
|
+
|
|
33
|
+
try {
|
|
34
|
+
// Try to load the native addon
|
|
35
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
36
|
+
nativeModule = require('@zan/heuristic-node');
|
|
37
|
+
useNative = true;
|
|
38
|
+
} catch {
|
|
39
|
+
// Use JavaScript fallback
|
|
40
|
+
nativeModule = null;
|
|
41
|
+
useNative = false;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* JavaScript-only fallback parser using regex patterns
|
|
47
|
+
* Matches the HeuristicParser from the Rust crate
|
|
48
|
+
*/
|
|
49
|
+
class JsHeuristicParser {
|
|
50
|
+
// Simplified regex patterns
|
|
51
|
+
private readonly reResolution = /1080p|720p|480p|2160p/i;
|
|
52
|
+
private readonly reGroup = /^\[([^\]]+)\]/;
|
|
53
|
+
// Match episode after dash, space, or dot - but not after year
|
|
54
|
+
private readonly reEpisode = /(?:[\s\-.])(?:[Ee]p?\.?)?\s*(\d{1,4})(?:\b|v\d|[^\d])/;
|
|
55
|
+
private readonly reEpisodeV = /(?:[\s\-.])(?:[Ee]p?\.?)?\s*(\d{1,4})v(\d+)/i;
|
|
56
|
+
private readonly reEpisodeRange = /(?:[\s\-.])(?:[Ee]p?\.?)?\s*(\d{1,4})\s*[-~]\s*(\d{1,4})/i;
|
|
57
|
+
private readonly reSeason = /(?:^|[\s\-])S(\d+)/i;
|
|
58
|
+
private readonly reYear = /\((\d{4})\)/;
|
|
59
|
+
private readonly reExtension = /\.(\w+)$/;
|
|
60
|
+
private readonly reCrc32 = /\[([A-Fa-f0-9]{8})\]/;
|
|
61
|
+
|
|
62
|
+
parse(input: string): ParseResult {
|
|
63
|
+
const trimmed = input.trim();
|
|
64
|
+
if (!trimmed) {
|
|
65
|
+
throw new Error('input is empty or whitespace-only');
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const result: ParseResult = {
|
|
69
|
+
input: trimmed,
|
|
70
|
+
title: null,
|
|
71
|
+
group: null,
|
|
72
|
+
episode: null,
|
|
73
|
+
season: null,
|
|
74
|
+
resolution: null,
|
|
75
|
+
video_codec: null,
|
|
76
|
+
audio_codec: null,
|
|
77
|
+
source: null,
|
|
78
|
+
year: null,
|
|
79
|
+
crc32: null,
|
|
80
|
+
extension: null,
|
|
81
|
+
version: null,
|
|
82
|
+
confidence: 0,
|
|
83
|
+
parse_mode: 'Light',
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
// Extract basic fields
|
|
87
|
+
result.group = this.extractGroup(trimmed);
|
|
88
|
+
result.extension = this.extractExtension(trimmed);
|
|
89
|
+
result.crc32 = this.extractCrc32(trimmed);
|
|
90
|
+
result.resolution = this.extractResolution(trimmed);
|
|
91
|
+
result.season = this.extractSeason(trimmed);
|
|
92
|
+
result.year = this.extractYear(trimmed);
|
|
93
|
+
result.episode = this.extractEpisode(trimmed);
|
|
94
|
+
|
|
95
|
+
// Extract title
|
|
96
|
+
result.title = this.extractTitle(trimmed, result);
|
|
97
|
+
|
|
98
|
+
// Compute confidence
|
|
99
|
+
result.confidence = this.computeConfidence(result);
|
|
100
|
+
|
|
101
|
+
return result;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
private extractGroup(input: string): string | null {
|
|
105
|
+
const match = this.reGroup.exec(input);
|
|
106
|
+
return match ? match[1].trim() : null;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
private extractExtension(input: string): string | null {
|
|
110
|
+
const match = this.reExtension.exec(input);
|
|
111
|
+
return match ? match[1].toLowerCase() : null;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
private extractCrc32(input: string): string | null {
|
|
115
|
+
const match = this.reCrc32.exec(input);
|
|
116
|
+
return match ? match[1].toUpperCase() : null;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
private extractResolution(input: string): Resolution | null {
|
|
120
|
+
if (input.includes('2160p') || input.includes('2160i')) return 'UHD2160';
|
|
121
|
+
if (input.includes('1080p') || input.includes('1080i')) return 'FHD1080';
|
|
122
|
+
if (input.includes('720p') || input.includes('720i')) return 'HD720';
|
|
123
|
+
if (input.includes('480p') || input.includes('480i')) return 'SD480';
|
|
124
|
+
return null;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
private extractSeason(input: string): number | null {
|
|
128
|
+
const match = this.reSeason.exec(input);
|
|
129
|
+
return match ? parseInt(match[1], 10) : null;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
private extractYear(input: string): number | null {
|
|
133
|
+
const match = this.reYear.exec(input);
|
|
134
|
+
return match ? parseInt(match[1], 10) : null;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
private extractEpisode(input: string): EpisodeSpec | null {
|
|
138
|
+
// Try versioned episode
|
|
139
|
+
let match = this.reEpisodeV.exec(input);
|
|
140
|
+
if (match) {
|
|
141
|
+
return { type: 'versioned', episode: parseInt(match[1], 10), version: parseInt(match[2], 10) };
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Try episode range
|
|
145
|
+
match = this.reEpisodeRange.exec(input);
|
|
146
|
+
if (match) {
|
|
147
|
+
const start = parseInt(match[1], 10);
|
|
148
|
+
const end = parseInt(match[2], 10);
|
|
149
|
+
if (start < end) {
|
|
150
|
+
return { type: 'range', start, end };
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Try single episode
|
|
155
|
+
match = this.reEpisode.exec(input);
|
|
156
|
+
if (match) {
|
|
157
|
+
return { type: 'single', episode: parseInt(match[1], 10) };
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
return null;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
private extractTitle(input: string, result: ParseResult): string | null {
|
|
164
|
+
let work = input;
|
|
165
|
+
|
|
166
|
+
// Remove group tag
|
|
167
|
+
if (result.group) {
|
|
168
|
+
const groupIdx = work.indexOf(']');
|
|
169
|
+
if (groupIdx !== -1) {
|
|
170
|
+
work = work.substring(groupIdx + 1);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// Remove extension
|
|
175
|
+
if (result.extension) {
|
|
176
|
+
const extPos = work.lastIndexOf(`.${result.extension}`);
|
|
177
|
+
if (extPos !== -1) {
|
|
178
|
+
work = work.substring(0, extPos);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Remove episode info
|
|
183
|
+
work = work.replace(this.reEpisodeV, ' ');
|
|
184
|
+
work = work.replace(this.reEpisodeRange, ' ');
|
|
185
|
+
work = work.replace(this.reEpisode, ' ');
|
|
186
|
+
work = work.replace(this.reSeason, ' ');
|
|
187
|
+
work = work.replace(this.reYear, ' ');
|
|
188
|
+
|
|
189
|
+
// Remove bracketed content
|
|
190
|
+
work = work.replace(/\[[^\]]*\]/g, ' ');
|
|
191
|
+
work = work.replace(/\([^\)]*\)/g, ' ');
|
|
192
|
+
|
|
193
|
+
// Clean up
|
|
194
|
+
const cleaned = work
|
|
195
|
+
.replace(/[._]/g, ' ')
|
|
196
|
+
.replace(/-/g, ' ')
|
|
197
|
+
.split(/\s+/)
|
|
198
|
+
.filter(Boolean)
|
|
199
|
+
.join(' ')
|
|
200
|
+
.trim();
|
|
201
|
+
|
|
202
|
+
return cleaned || null;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
private computeConfidence(result: ParseResult): number {
|
|
206
|
+
let fieldsPresent = 0;
|
|
207
|
+
let fieldsTotal = 7;
|
|
208
|
+
|
|
209
|
+
if (result.title) {
|
|
210
|
+
fieldsPresent += 2;
|
|
211
|
+
fieldsTotal += 1;
|
|
212
|
+
}
|
|
213
|
+
if (result.group) fieldsPresent += 1;
|
|
214
|
+
if (result.episode) fieldsPresent += 1;
|
|
215
|
+
if (result.resolution) fieldsPresent += 1;
|
|
216
|
+
if (result.video_codec) fieldsPresent += 1;
|
|
217
|
+
if (result.audio_codec) fieldsPresent += 1;
|
|
218
|
+
if (result.source) fieldsPresent += 1;
|
|
219
|
+
|
|
220
|
+
return Math.min(fieldsPresent / fieldsTotal, 1.0);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// Singleton instances
|
|
225
|
+
let jsParser: JsHeuristicParser | null = null;
|
|
226
|
+
let nativeParser: unknown = null;
|
|
227
|
+
|
|
228
|
+
function getParser(): { parse: (input: string) => unknown } {
|
|
229
|
+
initNative();
|
|
230
|
+
|
|
231
|
+
if (useNative) {
|
|
232
|
+
if (!nativeParser) {
|
|
233
|
+
// eslint-disable-next-line new-cap
|
|
234
|
+
nativeParser = new (nativeModule as { HeuristicParser: new () => { parse: (s: string) => unknown } }).HeuristicParser();
|
|
235
|
+
}
|
|
236
|
+
return nativeParser as { parse: (s: string) => unknown };
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
if (!jsParser) {
|
|
240
|
+
jsParser = new JsHeuristicParser();
|
|
241
|
+
}
|
|
242
|
+
return jsParser;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* Convert native episode spec to typed EpisodeSpec
|
|
247
|
+
*/
|
|
248
|
+
function convertEpisodeSpec(native: unknown): EpisodeSpec | null {
|
|
249
|
+
if (!native) return null;
|
|
250
|
+
|
|
251
|
+
if (typeof native === 'number') {
|
|
252
|
+
return { type: 'single', episode: native };
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
const n = native as Record<string, unknown>;
|
|
256
|
+
|
|
257
|
+
if (typeof n.type === 'number') {
|
|
258
|
+
switch (n.type) {
|
|
259
|
+
case 0:
|
|
260
|
+
return { type: 'single', episode: n.episode as number };
|
|
261
|
+
case 1:
|
|
262
|
+
return { type: 'range', start: n.start as number, end: n.end as number };
|
|
263
|
+
case 2:
|
|
264
|
+
return { type: 'multi', episodes: n.episodes as number[] };
|
|
265
|
+
case 3:
|
|
266
|
+
return { type: 'versioned', episode: n.episode as number, version: n.version as number };
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
if (n.episode !== undefined && n.version !== undefined) {
|
|
271
|
+
return { type: 'versioned', episode: n.episode as number, version: n.version as number };
|
|
272
|
+
}
|
|
273
|
+
if (n.start !== undefined && n.end !== undefined) {
|
|
274
|
+
return { type: 'range', start: n.start as number, end: n.end as number };
|
|
275
|
+
}
|
|
276
|
+
if (n.episodes !== undefined) {
|
|
277
|
+
return { type: 'multi', episodes: n.episodes as number[] };
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
return null;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* Convert native parse result to typed ParseResult
|
|
285
|
+
*/
|
|
286
|
+
function convertResult(native: unknown): ParseResult {
|
|
287
|
+
const n = native as Record<string, unknown>;
|
|
288
|
+
return {
|
|
289
|
+
input: n.input as string,
|
|
290
|
+
title: n.title as string | null,
|
|
291
|
+
group: n.group as string | null,
|
|
292
|
+
episode: convertEpisodeSpec(n.episode),
|
|
293
|
+
season: n.season as number | null,
|
|
294
|
+
resolution: n.resolution as Resolution | null,
|
|
295
|
+
video_codec: n.video_codec as VideoCodec | null,
|
|
296
|
+
audio_codec: n.audio_codec as AudioCodec | null,
|
|
297
|
+
source: n.source as MediaSource | null,
|
|
298
|
+
year: n.year as number | null,
|
|
299
|
+
crc32: n.crc32 as string | null,
|
|
300
|
+
extension: n.extension as string | null,
|
|
301
|
+
version: n.version as number | null,
|
|
302
|
+
confidence: n.confidence as number,
|
|
303
|
+
parse_mode: n.parse_mode as ParseMode,
|
|
304
|
+
};
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/**
|
|
308
|
+
* HeuristicParser - Fast regex-based anime filename parser
|
|
309
|
+
*
|
|
310
|
+
* Uses optimized regex patterns and scene naming rules for
|
|
311
|
+
* instant parsing with zero ML overhead.
|
|
312
|
+
*/
|
|
313
|
+
export class HeuristicParser {
|
|
314
|
+
private parser: { parse: (input: string) => unknown };
|
|
315
|
+
|
|
316
|
+
constructor(_options?: HeuristicParserOptions) {
|
|
317
|
+
this.parser = getParser();
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
parse(input: string): ParseResult {
|
|
321
|
+
if (typeof input !== 'string' || !input.trim()) {
|
|
322
|
+
throw new Error('Input must be a non-empty string');
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
const result = this.parser.parse(input);
|
|
326
|
+
|
|
327
|
+
if (!useNative) {
|
|
328
|
+
return result as ParseResult;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
return convertResult(result);
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
parseBatch(inputs: string[]): ParseResult[] {
|
|
335
|
+
if (!Array.isArray(inputs)) {
|
|
336
|
+
throw new Error('Input must be an array of strings');
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
return inputs.map(input => this.parse(input));
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// Default parser instance for convenience functions
|
|
344
|
+
let defaultParser: HeuristicParser | null = null;
|
|
345
|
+
|
|
346
|
+
function getDefaultParser(): HeuristicParser {
|
|
347
|
+
if (!defaultParser) {
|
|
348
|
+
defaultParser = new HeuristicParser();
|
|
349
|
+
}
|
|
350
|
+
return defaultParser;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
/**
|
|
354
|
+
* Parse a single filename using the default parser
|
|
355
|
+
*/
|
|
356
|
+
export function parse(input: string): ParseResult {
|
|
357
|
+
return getDefaultParser().parse(input);
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
/**
|
|
361
|
+
* Parse multiple filenames using the default parser
|
|
362
|
+
*/
|
|
363
|
+
export function parseBatch(inputs: string[]): ParseResult[] {
|
|
364
|
+
return getDefaultParser().parseBatch(inputs);
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
/**
|
|
368
|
+
* Check if native module is being used
|
|
369
|
+
*/
|
|
370
|
+
export function isUsingNativeModule(): boolean {
|
|
371
|
+
initNative();
|
|
372
|
+
return useNative;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
// Export types
|
|
376
|
+
export type { HeuristicParserOptions, ParseResult, EpisodeSpec, Resolution, VideoCodec, AudioCodec, MediaSource, ParseMode } from './types.js';
|
package/src/types.d.ts
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Zantetsu - Fast anime metadata parser
|
|
3
|
+
*
|
|
4
|
+
* Type definitions for the heuristic parser bindings
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Episode specification types
|
|
9
|
+
*/
|
|
10
|
+
export type EpisodeSpec =
|
|
11
|
+
| { type: 'single'; episode: number }
|
|
12
|
+
| { type: 'range'; start: number; end: number }
|
|
13
|
+
| { type: 'multi'; episodes: number[] }
|
|
14
|
+
| { type: 'versioned'; episode: number; version: number };
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Video resolution
|
|
18
|
+
*/
|
|
19
|
+
export type Resolution = 'SD480' | 'HD720' | 'FHD1080' | 'UHD2160';
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Video codec
|
|
23
|
+
*/
|
|
24
|
+
export type VideoCodec = 'H264' | 'HEVC' | 'AV1' | 'VP9' | 'MPEG4';
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Audio codec
|
|
28
|
+
*/
|
|
29
|
+
export type AudioCodec = 'FLAC' | 'AAC' | 'Opus' | 'AC3' | 'DTS' | 'MP3' | 'Vorbis' | 'TrueHD' | 'EAAC';
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Media source
|
|
33
|
+
*/
|
|
34
|
+
export type MediaSource = 'BluRayRemux' | 'BluRay' | 'WebDL' | 'WebRip' | 'HDTV' | 'DVD' | 'LaserDisc' | 'VHS';
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Parse mode
|
|
38
|
+
*/
|
|
39
|
+
export type ParseMode = 'Full' | 'Light' | 'Auto';
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Options for creating a HeuristicParser
|
|
43
|
+
*/
|
|
44
|
+
export interface HeuristicParserOptions {
|
|
45
|
+
/** Enable debug logging (default: false) */
|
|
46
|
+
debug?: boolean;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Parse result from the heuristic parser
|
|
51
|
+
*/
|
|
52
|
+
export interface ParseResult {
|
|
53
|
+
/** Original input string */
|
|
54
|
+
input: string;
|
|
55
|
+
/** Extracted anime title (normalized) */
|
|
56
|
+
title: string | null;
|
|
57
|
+
/** Release group name (e.g., "SubsPlease", "Erai-raws") */
|
|
58
|
+
group: string | null;
|
|
59
|
+
/** Episode specification */
|
|
60
|
+
episode: EpisodeSpec | null;
|
|
61
|
+
/** Season number */
|
|
62
|
+
season: number | null;
|
|
63
|
+
/** Video resolution */
|
|
64
|
+
resolution: Resolution | null;
|
|
65
|
+
/** Video codec */
|
|
66
|
+
video_codec: VideoCodec | null;
|
|
67
|
+
/** Audio codec */
|
|
68
|
+
audio_codec: AudioCodec | null;
|
|
69
|
+
/** Media source */
|
|
70
|
+
source: MediaSource | null;
|
|
71
|
+
/** Release year */
|
|
72
|
+
year: number | null;
|
|
73
|
+
/** CRC32 checksum (hex string) */
|
|
74
|
+
crc32: string | null;
|
|
75
|
+
/** File extension (without leading dot) */
|
|
76
|
+
extension: string | null;
|
|
77
|
+
/** Release version (e.g., v2 = 2) */
|
|
78
|
+
version: number | null;
|
|
79
|
+
/** Confidence score in [0.0, 1.0] */
|
|
80
|
+
confidence: number;
|
|
81
|
+
/** Parse mode used */
|
|
82
|
+
parse_mode: ParseMode;
|
|
83
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "ES2022",
|
|
5
|
+
"lib": ["ES2022"],
|
|
6
|
+
"moduleResolution": "bundler",
|
|
7
|
+
"outDir": "./dist",
|
|
8
|
+
"rootDir": "./src",
|
|
9
|
+
"declaration": true,
|
|
10
|
+
"declarationMap": true,
|
|
11
|
+
"sourceMap": true,
|
|
12
|
+
"strict": true,
|
|
13
|
+
"esModuleInterop": true,
|
|
14
|
+
"skipLibCheck": true,
|
|
15
|
+
"forceConsistentCasingInFileNames": true,
|
|
16
|
+
"resolveJsonModule": true,
|
|
17
|
+
"allowSyntheticDefaultImports": true,
|
|
18
|
+
"noEmit": false,
|
|
19
|
+
"target": "ES2022"
|
|
20
|
+
},
|
|
21
|
+
"include": ["src/**/*"],
|
|
22
|
+
"exclude": ["node_modules", "dist", "**/*.test.ts"]
|
|
23
|
+
}
|