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 ADDED
File without changes
@@ -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":"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"}
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
+ }