tserato 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.
Files changed (41) hide show
  1. package/README.md +113 -0
  2. package/dist/builder.d.ts +15 -0
  3. package/dist/builder.d.ts.map +1 -0
  4. package/dist/builder.js +187 -0
  5. package/dist/encoders/baseEncoder.d.ts +8 -0
  6. package/dist/encoders/baseEncoder.d.ts.map +1 -0
  7. package/dist/encoders/baseEncoder.js +6 -0
  8. package/dist/encoders/serato_tags.d.ts +5 -0
  9. package/dist/encoders/serato_tags.d.ts.map +1 -0
  10. package/dist/encoders/serato_tags.js +7 -0
  11. package/dist/encoders/utils.d.ts +3 -0
  12. package/dist/encoders/utils.d.ts.map +1 -0
  13. package/dist/encoders/utils.js +24 -0
  14. package/dist/encoders/v2/v2Mp3Encoder.d.ts +21 -0
  15. package/dist/encoders/v2/v2Mp3Encoder.d.ts.map +1 -0
  16. package/dist/encoders/v2/v2Mp3Encoder.js +197 -0
  17. package/dist/index.d.ts +7 -0
  18. package/dist/index.d.ts.map +1 -0
  19. package/dist/index.js +15 -0
  20. package/dist/model/crate.d.ts +15 -0
  21. package/dist/model/crate.d.ts.map +1 -0
  22. package/dist/model/crate.js +76 -0
  23. package/dist/model/hotCue.d.ts +28 -0
  24. package/dist/model/hotCue.d.ts.map +1 -0
  25. package/dist/model/hotCue.js +166 -0
  26. package/dist/model/hotCueType.d.ts +7 -0
  27. package/dist/model/hotCueType.d.ts.map +1 -0
  28. package/dist/model/hotCueType.js +10 -0
  29. package/dist/model/seratoColor.d.ts +21 -0
  30. package/dist/model/seratoColor.d.ts.map +1 -0
  31. package/dist/model/seratoColor.js +24 -0
  32. package/dist/model/tempo.d.ts +5 -0
  33. package/dist/model/tempo.d.ts.map +1 -0
  34. package/dist/model/tempo.js +2 -0
  35. package/dist/model/track.d.ts +29 -0
  36. package/dist/model/track.d.ts.map +1 -0
  37. package/dist/model/track.js +75 -0
  38. package/dist/util.d.ts +11 -0
  39. package/dist/util.d.ts.map +1 -0
  40. package/dist/util.js +93 -0
  41. package/package.json +24 -0
package/README.md ADDED
@@ -0,0 +1,113 @@
1
+ # pyserato-ts
2
+
3
+ TypeScript port of pyserato (https://github.com/laker-93/pyserato/).
4
+
5
+ ## Write Crates
6
+
7
+ ```
8
+ // example/testEncoder.ts
9
+ import { Builder, Crate, Track, V2Mp3Encoder, HotCue, HotCueType } from "tserato"
10
+
11
+ async function main() {
12
+ // create encoder + builder
13
+ const mp3Encoder = new V2Mp3Encoder();
14
+ const builder = new Builder(mp3Encoder);
15
+
16
+ // create crate
17
+ const crate = new Crate("foojs");
18
+
19
+ // add track
20
+ const track = Track.fromPath("/Users/lukepurnell/test music/Russian Circles - Gnosis/Russian Circles - Gnosis - 06 Betrayal.mp3");
21
+ crate.addTrack(track);
22
+
23
+ // add cues
24
+ track.addHotCue(
25
+ new HotCue({
26
+ name: "mycue1",
27
+ type: HotCueType.CUE,
28
+ start: 500,
29
+ index: 1,
30
+ })
31
+ );
32
+ track.addHotCue(
33
+ new HotCue({
34
+ name: "mycue2",
35
+ type: HotCueType.CUE,
36
+ start: 1000,
37
+ index: 2,
38
+ })
39
+ );
40
+
41
+ track.addHotCue(
42
+ new HotCue({
43
+ name: "myloop1",
44
+ type: HotCueType.LOOP,
45
+ start: 2000,
46
+ end: 3000,
47
+ index: 3,
48
+ })
49
+ );
50
+
51
+ // save crate (writes .crate file + tags)
52
+ await builder.save(crate, undefined, true);
53
+
54
+ console.log("Crate saved successfully!");
55
+ }
56
+
57
+ main().catch(console.error);
58
+
59
+ ```
60
+
61
+ ## Write Cues
62
+
63
+ ```
64
+ import { Builder, Crate, Track, V2Mp3Encoder, HotCue, HotCueType } from "tserato"
65
+
66
+ async function main() {
67
+ // create encoder + builder
68
+ const mp3Encoder = new V2Mp3Encoder();
69
+ const builder = new Builder(mp3Encoder);
70
+
71
+ // create crate
72
+ const crate = new Crate("foojs");
73
+
74
+ // add track
75
+ const track = Track.fromPath("/Users/lukepurnell/test music/Russian Circles - Gnosis/Russian Circles - Gnosis - 06 Betrayal.mp3");
76
+ crate.addTrack(track);
77
+
78
+ // add cues
79
+ track.addHotCue(
80
+ new HotCue({
81
+ name: "mycue1",
82
+ type: HotCueType.CUE,
83
+ start: 500,
84
+ index: 1,
85
+ })
86
+ );
87
+ track.addHotCue(
88
+ new HotCue({
89
+ name: "mycue2",
90
+ type: HotCueType.CUE,
91
+ start: 1000,
92
+ index: 2,
93
+ })
94
+ );
95
+
96
+ track.addHotCue(
97
+ new HotCue({
98
+ name: "myloop1",
99
+ type: HotCueType.LOOP,
100
+ start: 2000,
101
+ end: 3000,
102
+ index: 3,
103
+ })
104
+ );
105
+
106
+ // save crate (writes .crate file + tags)
107
+ await builder.save(crate, undefined, true);
108
+
109
+ console.log("Crate saved successfully!");
110
+ }
111
+
112
+ main().catch(console.error);
113
+ ```
@@ -0,0 +1,15 @@
1
+ import { BaseEncoder } from './encoders/baseEncoder';
2
+ import { Crate } from './model/crate';
3
+ export declare class Builder {
4
+ private _encoder?;
5
+ constructor(encoder?: BaseEncoder);
6
+ private static _resolvePath;
7
+ private static _parseCrateNames;
8
+ private _buildCrateFilepath;
9
+ parseCratesFromRootPath(subcratePath: string): Record<string, Crate>;
10
+ private _buildCratesFromFilepath;
11
+ private _parseCrateTracks;
12
+ private _construct;
13
+ save(root: Crate, savePath?: string, overwrite?: boolean): void;
14
+ }
15
+ //# sourceMappingURL=builder.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"builder.d.ts","sourceRoot":"","sources":["../src/builder.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,WAAW,EAAE,MAAM,wBAAwB,CAAC;AACrD,OAAO,EAAE,KAAK,EAAE,MAAM,eAAe,CAAC;AAMtC,qBAAa,OAAO;IAClB,OAAO,CAAC,QAAQ,CAAC,CAAc;gBACnB,OAAO,CAAC,EAAE,WAAW;IAEjC,OAAO,CAAC,MAAM,CAAE,YAAY;IAc5B,OAAO,CAAC,MAAM,CAAE,gBAAgB;IAKhC,OAAO,CAAE,mBAAmB;IAQ5B,uBAAuB,CAAC,YAAY,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,KAAK,CAAC;IAiBpE,OAAO,CAAC,wBAAwB;IAqBhC,OAAO,CAAE,iBAAiB;IAkB1B,OAAO,CAAC,UAAU;IA2DlB,IAAI,CAAC,IAAI,EAAE,KAAK,EAAE,QAAQ,GAAE,MAA8B,EAAE,SAAS,UAAQ;CAO9E"}
@@ -0,0 +1,187 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.Builder = void 0;
37
+ const fs = __importStar(require("fs"));
38
+ const path = __importStar(require("path"));
39
+ const crate_1 = require("./model/crate");
40
+ const track_1 = require("./model/track");
41
+ const util_1 = require("./util");
42
+ const DEFAULT_SERATO_FOLDER = path.join(process.env.HOME || process.cwd(), 'Music', '_Serato_');
43
+ class Builder {
44
+ constructor(encoder) { this._encoder = encoder; }
45
+ static *_resolvePath(root) {
46
+ let pathStr = '';
47
+ const stack = [[root, pathStr]];
48
+ while (stack.length) {
49
+ const [crate, p] = stack.pop();
50
+ let newPath = p + `${crate.name}%%`;
51
+ const children = crate.children;
52
+ if (children && children.length) {
53
+ for (const child of children)
54
+ stack.push([child, newPath]);
55
+ }
56
+ yield [crate, newPath.replace(/%%$/, '') + '.crate'];
57
+ }
58
+ }
59
+ static *_parseCrateNames(filepath) {
60
+ const parts = path.basename(filepath).split('%%');
61
+ for (const p of parts)
62
+ yield p.replace('.crate', '');
63
+ }
64
+ *_buildCrateFilepath(crate, seratoFolder) {
65
+ const subcrateFolder = path.join(seratoFolder, 'SubCrates');
66
+ if (!fs.existsSync(subcrateFolder))
67
+ fs.mkdirSync(subcrateFolder, { recursive: true });
68
+ for (const [c, p] of Builder._resolvePath(crate)) {
69
+ yield [c, path.join(subcrateFolder, p)];
70
+ }
71
+ }
72
+ parseCratesFromRootPath(subcratePath) {
73
+ const result = {};
74
+ const entries = fs.readdirSync(subcratePath);
75
+ for (const name of entries) {
76
+ if (!name.endsWith('crate'))
77
+ continue;
78
+ const full = path.join(subcratePath, name);
79
+ const crate = this._buildCratesFromFilepath(full);
80
+ if (result[crate.name]) {
81
+ const merged_crate = crate.plus(result[crate.name]);
82
+ result[crate.name] = merged_crate;
83
+ }
84
+ else {
85
+ result[crate.name] = crate;
86
+ }
87
+ }
88
+ return result;
89
+ }
90
+ _buildCratesFromFilepath(filepath) {
91
+ const crateNames = Array.from(Builder._parseCrateNames(filepath));
92
+ let childCrate = null;
93
+ let crate = null;
94
+ for (const crateName of crateNames.reverse()) {
95
+ if (childCrate == null) {
96
+ crate = new crate_1.Crate(crateName);
97
+ for (const filePath of this._parseCrateTracks(filepath)) {
98
+ const t = track_1.Track.fromPath(filePath);
99
+ crate.addTrack(t);
100
+ }
101
+ childCrate = crate;
102
+ }
103
+ else {
104
+ crate = new crate_1.Crate(crateName, [childCrate]);
105
+ childCrate = crate;
106
+ }
107
+ }
108
+ if (!crate)
109
+ throw new Error(`no crates parsed from ${filepath}`);
110
+ return crate;
111
+ }
112
+ *_parseCrateTracks(filepath) {
113
+ let buffer = fs.readFileSync(filepath);
114
+ buffer = Buffer.from(buffer);
115
+ while (buffer.length > 0) {
116
+ const otrkIdx = buffer.indexOf(Buffer.from('otrk'));
117
+ if (otrkIdx < 0)
118
+ break;
119
+ const ptrkIdx = buffer.indexOf(Buffer.from('ptrk'), otrkIdx);
120
+ if (ptrkIdx < 0)
121
+ break;
122
+ const ptrkSection = buffer.slice(ptrkIdx);
123
+ const trackNameLength = ptrkSection.readUInt32BE(4);
124
+ const trackNameEncoded = ptrkSection.slice(8, 8 + trackNameLength);
125
+ let filePath = (0, util_1.seratoDecode)(trackNameEncoded);
126
+ if (!filePath.startsWith('/'))
127
+ filePath = '/' + filePath;
128
+ yield filePath;
129
+ buffer = ptrkSection.slice(8 + trackNameLength);
130
+ }
131
+ }
132
+ _construct(crate) {
133
+ // ----- HEADER -----
134
+ let header = (0, util_1.concatBytes)([
135
+ (0, util_1.latin1Encode)("vrsn"),
136
+ (0, util_1.intToBytes)(0, 1),
137
+ (0, util_1.intToBytes)(0, 1),
138
+ (0, util_1.seratoEncode)("81.0"),
139
+ (0, util_1.seratoEncode)("/Serato ScratchLive Crate"),
140
+ ]);
141
+ // ----- DEFAULT COLUMNS -----
142
+ const DEFAULT_COLUMNS = ["track", "artist", "album", "length"];
143
+ const parts = [];
144
+ for (const column of DEFAULT_COLUMNS) {
145
+ parts.push((0, util_1.latin1Encode)("ovct"));
146
+ parts.push((0, util_1.intToBytes)(column.length * 2 + 18, 4));
147
+ parts.push((0, util_1.latin1Encode)("tvcn"));
148
+ parts.push((0, util_1.intToBytes)(column.length * 2, 4));
149
+ parts.push((0, util_1.seratoEncode)(column));
150
+ parts.push((0, util_1.latin1Encode)("tvcw"));
151
+ parts.push((0, util_1.intToBytes)(2, 4));
152
+ parts.push((0, util_1.latin1Encode)("0"));
153
+ parts.push((0, util_1.latin1Encode)("0"));
154
+ }
155
+ const columnSection = (0, util_1.concatBytes)(parts);
156
+ // ----- PLAYLIST SECTION -----
157
+ const playlistParts = [];
158
+ if (crate.tracks) {
159
+ for (const track of crate.tracks) {
160
+ if (this._encoder) {
161
+ this._encoder.write(track);
162
+ }
163
+ const absoluteTrackPath = path.resolve(track.path);
164
+ const otrkSize = (0, util_1.intToBytes)(absoluteTrackPath.length * 2 + 8, 4);
165
+ const ptrkSize = (0, util_1.intToBytes)(absoluteTrackPath.length * 2, 4);
166
+ playlistParts.push((0, util_1.latin1Encode)("otrk"));
167
+ playlistParts.push(otrkSize);
168
+ playlistParts.push((0, util_1.latin1Encode)("ptrk"));
169
+ playlistParts.push(ptrkSize);
170
+ playlistParts.push((0, util_1.seratoEncode)(absoluteTrackPath));
171
+ }
172
+ }
173
+ const playlistSection = (0, util_1.concatBytes)(playlistParts);
174
+ // ----- FINAL CONTENTS -----
175
+ const contents = (0, util_1.concatBytes)([header, columnSection, playlistSection]);
176
+ return contents;
177
+ }
178
+ save(root, savePath = DEFAULT_SERATO_FOLDER, overwrite = false) {
179
+ for (const [crate, filepath] of this._buildCrateFilepath(root, savePath)) {
180
+ if (fs.existsSync(filepath) && !overwrite)
181
+ continue;
182
+ const buffer = this._construct(crate);
183
+ fs.writeFileSync(filepath, buffer);
184
+ }
185
+ }
186
+ }
187
+ exports.Builder = Builder;
@@ -0,0 +1,8 @@
1
+ import { Track } from '../model/track';
2
+ export declare abstract class BaseEncoder {
3
+ abstract get tagName(): string;
4
+ abstract get tagVersion(): Buffer;
5
+ abstract get markersName(): string;
6
+ abstract write(track: Track): void;
7
+ }
8
+ //# sourceMappingURL=baseEncoder.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"baseEncoder.d.ts","sourceRoot":"","sources":["../../src/encoders/baseEncoder.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,MAAM,gBAAgB,CAAC;AAEvC,8BAAsB,WAAW;IAC/B,QAAQ,KAAK,OAAO,IAAI,MAAM,CAAC;IAC/B,QAAQ,KAAK,UAAU,IAAI,MAAM,CAAC;IAClC,QAAQ,KAAK,WAAW,IAAI,MAAM,CAAC;IACnC,QAAQ,CAAC,KAAK,CAAC,KAAK,EAAE,KAAK,GAAG,IAAI;CACnC"}
@@ -0,0 +1,6 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.BaseEncoder = void 0;
4
+ class BaseEncoder {
5
+ }
6
+ exports.BaseEncoder = BaseEncoder;
@@ -0,0 +1,5 @@
1
+ export declare const SERATO_MARKERS_V2 = "Serato Markers2";
2
+ export declare const SERATO_OVERVIEW = "Serato Overview";
3
+ export declare const SERATO_MARKERS_V1 = "Serato Markers_";
4
+ export declare const SERATO_ANALYSIS = "Serato Analysis";
5
+ //# sourceMappingURL=serato_tags.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"serato_tags.d.ts","sourceRoot":"","sources":["../../src/encoders/serato_tags.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,iBAAiB,oBAAoB,CAAC;AACnD,eAAO,MAAM,eAAe,oBAAoB,CAAC;AACjD,eAAO,MAAM,iBAAiB,oBAAoB,CAAC;AACnD,eAAO,MAAM,eAAe,oBAAoB,CAAC"}
@@ -0,0 +1,7 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.SERATO_ANALYSIS = exports.SERATO_MARKERS_V1 = exports.SERATO_OVERVIEW = exports.SERATO_MARKERS_V2 = void 0;
4
+ exports.SERATO_MARKERS_V2 = 'Serato Markers2';
5
+ exports.SERATO_OVERVIEW = 'Serato Overview';
6
+ exports.SERATO_MARKERS_V1 = 'Serato Markers_';
7
+ exports.SERATO_ANALYSIS = 'Serato Analysis';
@@ -0,0 +1,3 @@
1
+ export declare function encode(data: Buffer): Buffer;
2
+ export declare function decode(data: Buffer): Buffer;
3
+ //# sourceMappingURL=utils.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../../src/encoders/utils.ts"],"names":[],"mappings":"AAAA,wBAAgB,MAAM,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAS3C;AAED,wBAAgB,MAAM,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAS3C"}
@@ -0,0 +1,24 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.encode = encode;
4
+ exports.decode = decode;
5
+ function encode(data) {
6
+ const a = data.readUInt8(0);
7
+ const b = data.readUInt8(1);
8
+ const c = data.readUInt8(2);
9
+ const z = c & 0x7F;
10
+ const y = ((c >> 7) | (b << 1)) & 0x7F;
11
+ const x = ((b >> 6) | (a << 2)) & 0x7F;
12
+ const w = a >> 5;
13
+ return Buffer.from([w, x, y, z]);
14
+ }
15
+ function decode(data) {
16
+ const w = data.readUInt8(0);
17
+ const x = data.readUInt8(1);
18
+ const y = data.readUInt8(2);
19
+ const z = data.readUInt8(3);
20
+ const c = (z & 0x7F) | ((y & 0x01) << 7);
21
+ const b = ((y & 0x7F) >> 1) | ((x & 0x03) << 6);
22
+ const a = ((x & 0x7F) >> 2) | ((w & 0x07) << 5);
23
+ return Buffer.from([a, b, c]);
24
+ }
@@ -0,0 +1,21 @@
1
+ import { HotCue } from '../../../src/model/hotCue';
2
+ import { Track } from '../../model/track';
3
+ import { BaseEncoder } from '../baseEncoder';
4
+ export declare class V2Mp3Encoder extends BaseEncoder {
5
+ get tagName(): string;
6
+ get tagVersion(): Buffer;
7
+ get markersName(): string;
8
+ write(track: Track): void;
9
+ readCues(track: Track): HotCue[];
10
+ private _decode;
11
+ private _removeNullPadding;
12
+ private _getEntryName;
13
+ private _padEncodedData;
14
+ private _write;
15
+ private _encode;
16
+ private _pad;
17
+ private _removeEncodedDataPad;
18
+ private _padPayload;
19
+ private _enrichPayload;
20
+ }
21
+ //# sourceMappingURL=v2Mp3Encoder.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"v2Mp3Encoder.d.ts","sourceRoot":"","sources":["../../../src/encoders/v2/v2Mp3Encoder.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,2BAA2B,CAAC;AAEnD,OAAO,EAAE,KAAK,EAAE,MAAM,mBAAmB,CAAC;AAC1C,OAAO,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAC;AAiC7C,qBAAa,YAAa,SAAQ,WAAW;IAE3C,IAAI,OAAO,IAAI,MAAM,CAEpB;IAED,IAAI,UAAU,IAAI,MAAM,CAEvB;IAED,IAAI,WAAW,IAAI,MAAM,CAExB;IAED,KAAK,CAAC,KAAK,EAAE,KAAK,GAAG,IAAI;IAKzB,QAAQ,CAAC,KAAK,EAAE,KAAK,GAAG,MAAM,EAAE;IAoBhC,OAAO,CAAE,OAAO;IAmDhB,OAAO,CAAC,kBAAkB;IAK1B,OAAO,CAAC,aAAa;IAUrB,OAAO,CAAC,eAAe;IAWvB,OAAO,CAAC,MAAM;IAkCd,OAAO,CAAC,OAAO;IAWf,OAAO,CAAC,IAAI;IAWZ,OAAO,CAAC,qBAAqB;IAI7B,OAAO,CAAC,WAAW;IAQnB,OAAO,CAAC,cAAc;CASvB"}
@@ -0,0 +1,197 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.V2Mp3Encoder = void 0;
7
+ const hotCue_1 = require("../../../src/model/hotCue");
8
+ const hotCueType_1 = require("../../model/hotCueType");
9
+ const baseEncoder_1 = require("../baseEncoder");
10
+ const serato_tags_1 = require("../serato_tags");
11
+ const mp3tag_js_1 = __importDefault(require("mp3tag.js"));
12
+ const util_1 = require("../../util");
13
+ const fs = require('fs');
14
+ /**
15
+ * Replacement for Python's BytesIO
16
+ */
17
+ class BufferReader {
18
+ constructor(buffer) {
19
+ this.buffer = buffer;
20
+ this.offset = 0;
21
+ }
22
+ read(n) {
23
+ const chunk = this.buffer.slice(this.offset, this.offset + n);
24
+ this.offset += n;
25
+ return chunk;
26
+ }
27
+ readUInt8() {
28
+ const value = this.buffer.readUInt8(this.offset);
29
+ this.offset += 1;
30
+ return value;
31
+ }
32
+ readUInt32BE() {
33
+ const value = this.buffer.readUInt32BE(this.offset);
34
+ this.offset += 4;
35
+ return value;
36
+ }
37
+ }
38
+ class V2Mp3Encoder extends baseEncoder_1.BaseEncoder {
39
+ get tagName() {
40
+ return serato_tags_1.SERATO_MARKERS_V2;
41
+ }
42
+ get tagVersion() {
43
+ return Buffer.from([0x01, 0x01]);
44
+ }
45
+ get markersName() {
46
+ return "Serato Markers2";
47
+ }
48
+ write(track) {
49
+ const payload = this._encode(track);
50
+ this._write(track, payload);
51
+ }
52
+ readCues(track) {
53
+ // Read the buffer of an audio file
54
+ const buffer = fs.readFileSync(track.path.toString());
55
+ // Now, pass it to MP3Tag
56
+ const mp3tag = new mp3tag_js_1.default(buffer, true);
57
+ mp3tag.read();
58
+ const geob = mp3tag.tags.v2?.GEOB;
59
+ if (!geob || !geob) {
60
+ return [];
61
+ }
62
+ const data = Buffer.from(geob[0].object);
63
+ return Array.from(this._decode(data));
64
+ }
65
+ *_decode(data) {
66
+ const fp = new BufferReader(data);
67
+ // Expect version 0x01, 0x01
68
+ const a = fp.readUInt8();
69
+ const b = fp.readUInt8();
70
+ if (a !== 0x01 || b !== 0x01) {
71
+ throw new Error("Invalid Serato markers header");
72
+ }
73
+ const payload = fp.read(data.length - 2);
74
+ let processed = this._removeNullPadding(payload);
75
+ processed = this._padEncodedData(processed);
76
+ const decoded = Buffer.from(processed.toString(), "base64");
77
+ const fp2 = new BufferReader(decoded);
78
+ const a2 = fp2.readUInt8();
79
+ const b2 = fp2.readUInt8();
80
+ if (a2 !== 0x01 || b2 !== 0x01) {
81
+ throw new Error("Invalid decoded header");
82
+ }
83
+ while (true) {
84
+ const entryName = this._getEntryName(fp2);
85
+ if (entryName.length === 0) {
86
+ break;
87
+ }
88
+ const structLength = fp2.readUInt32BE();
89
+ if (structLength <= 0)
90
+ throw new Error("Invalid struct length");
91
+ const entryData = fp2.read(structLength);
92
+ switch (entryName) {
93
+ case "COLOR":
94
+ // not yet implemented
95
+ continue;
96
+ case "CUE":
97
+ yield hotCue_1.HotCue.fromBytes(entryData, hotCueType_1.HotCueType.CUE);
98
+ continue;
99
+ case "LOOP":
100
+ yield hotCue_1.HotCue.fromBytes(entryData, hotCueType_1.HotCueType.LOOP);
101
+ continue;
102
+ case "BPMLOCK":
103
+ // not yet implemented
104
+ continue;
105
+ }
106
+ }
107
+ }
108
+ _removeNullPadding(payload) {
109
+ const nullIndex = payload.indexOf(0x00);
110
+ return nullIndex >= 0 ? payload.slice(0, nullIndex) : payload;
111
+ }
112
+ _getEntryName(fp) {
113
+ const bytes = [];
114
+ while (true) {
115
+ const byte = fp.read(1)[0];
116
+ if (byte === 0x00)
117
+ break;
118
+ bytes.push(byte);
119
+ }
120
+ return Buffer.from(bytes).toString("utf-8");
121
+ }
122
+ _padEncodedData(data) {
123
+ const len = data.length;
124
+ let padding;
125
+ if (len % 4 === 1) {
126
+ padding = Buffer.from("A==");
127
+ }
128
+ else {
129
+ padding = Buffer.from("=".repeat((-len % 4 + 4) % 4));
130
+ }
131
+ return Buffer.concat([data, padding]);
132
+ }
133
+ _write(track, payload) {
134
+ // Read the buffer of an audio file
135
+ const buffer = fs.readFileSync(track.path.toString());
136
+ const mp3tag = new mp3tag_js_1.default(buffer, true);
137
+ mp3tag.read();
138
+ // Write the ID3v2 tags.
139
+ // See https://mp3tag.js.org/docs/frames.html for the list of supported ID3v2 frames
140
+ const object = Array.from(payload);
141
+ mp3tag.tags.v2.GEOB = [
142
+ {
143
+ format: 'application/octet-stream',
144
+ filename: "",
145
+ object: object,
146
+ description: this.markersName,
147
+ }
148
+ ];
149
+ // Save the tags
150
+ mp3tag.save({ id3v2: { encoding: "latin1" } });
151
+ // Handle error if there's any
152
+ if (mp3tag.error !== '')
153
+ throw new Error(mp3tag.error);
154
+ // Read the new buffer again
155
+ mp3tag.read();
156
+ // Write the new buffer to file
157
+ fs.writeFileSync(track.path.toString(), mp3tag.buffer);
158
+ }
159
+ _encode(track) {
160
+ let payload = Buffer.alloc(0);
161
+ for (const cue of track.hotCues) {
162
+ payload = Buffer.concat([payload, cue.toV2Bytes()]);
163
+ }
164
+ for (const cue of track.cueLoops) {
165
+ payload = Buffer.concat([payload, cue.toV2Bytes()]);
166
+ }
167
+ return this._pad(payload);
168
+ }
169
+ _pad(payload, entriesCount) {
170
+ // prepend tag version
171
+ payload = Buffer.concat([this.tagVersion, payload]);
172
+ payload = this._removeEncodedDataPad(Buffer.from(payload.toString("base64")));
173
+ payload = this._padPayload((0, util_1.splitString)(payload));
174
+ payload = this._enrichPayload(payload, entriesCount);
175
+ return payload;
176
+ }
177
+ _removeEncodedDataPad(data) {
178
+ return Buffer.from(data.toString().replace(/=/g, "A"));
179
+ }
180
+ _padPayload(payload) {
181
+ const length = payload.length;
182
+ if (length < 468) {
183
+ return Buffer.concat([payload, Buffer.alloc(468 - length)]);
184
+ }
185
+ return Buffer.concat([payload, Buffer.alloc(982 - length), Buffer.from([0x00])]);
186
+ }
187
+ _enrichPayload(payload, entriesCount) {
188
+ let header = this.tagVersion;
189
+ if (entriesCount !== undefined) {
190
+ const buf = Buffer.alloc(4);
191
+ buf.writeUInt32BE(entriesCount, 0);
192
+ header = Buffer.concat([header, buf]);
193
+ }
194
+ return Buffer.concat([header, payload]);
195
+ }
196
+ }
197
+ exports.V2Mp3Encoder = V2Mp3Encoder;
@@ -0,0 +1,7 @@
1
+ export { Builder } from './builder';
2
+ export { V2Mp3Encoder } from './encoders/v2/v2Mp3Encoder';
3
+ export { Crate } from './model/crate';
4
+ export { Track } from './model/track';
5
+ export { HotCue } from './model/hotCue';
6
+ export { HotCueType } from './model/hotCueType';
7
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACpC,OAAO,EAAE,YAAY,EAAE,MAAM,4BAA4B,CAAC;AAC1D,OAAO,EAAE,KAAK,EAAE,MAAM,eAAe,CAAC;AACtC,OAAO,EAAE,KAAK,EAAE,MAAM,eAAe,CAAC;AACtC,OAAO,EAAE,MAAM,EAAE,MAAM,gBAAgB,CAAC;AACxC,OAAO,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,15 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.HotCueType = exports.HotCue = exports.Track = exports.Crate = exports.V2Mp3Encoder = exports.Builder = void 0;
4
+ var builder_1 = require("./builder");
5
+ Object.defineProperty(exports, "Builder", { enumerable: true, get: function () { return builder_1.Builder; } });
6
+ var v2Mp3Encoder_1 = require("./encoders/v2/v2Mp3Encoder");
7
+ Object.defineProperty(exports, "V2Mp3Encoder", { enumerable: true, get: function () { return v2Mp3Encoder_1.V2Mp3Encoder; } });
8
+ var crate_1 = require("./model/crate");
9
+ Object.defineProperty(exports, "Crate", { enumerable: true, get: function () { return crate_1.Crate; } });
10
+ var track_1 = require("./model/track");
11
+ Object.defineProperty(exports, "Track", { enumerable: true, get: function () { return track_1.Track; } });
12
+ var hotCue_1 = require("./model/hotCue");
13
+ Object.defineProperty(exports, "HotCue", { enumerable: true, get: function () { return hotCue_1.HotCue; } });
14
+ var hotCueType_1 = require("./model/hotCueType");
15
+ Object.defineProperty(exports, "HotCueType", { enumerable: true, get: function () { return hotCueType_1.HotCueType; } });
@@ -0,0 +1,15 @@
1
+ import { Track } from './track';
2
+ export declare class Crate {
3
+ private _children;
4
+ readonly name: string;
5
+ private _tracks;
6
+ constructor(name: string, children?: Crate[]);
7
+ get children(): Crate[];
8
+ get tracks(): Set<Track>;
9
+ addTrack(track: Track): void;
10
+ toString(): string;
11
+ plus(other: Crate): Crate;
12
+ deepCopy(memodict?: Map<any, any>): Crate;
13
+ equals(other: Crate): boolean;
14
+ }
15
+ //# sourceMappingURL=crate.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"crate.d.ts","sourceRoot":"","sources":["../../src/model/crate.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,MAAM,SAAS,CAAC;AAGhC,qBAAa,KAAK;IAChB,OAAO,CAAC,SAAS,CAAU;IAC3B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,OAAO,CAAC,OAAO,CAAa;gBAEhB,IAAI,EAAE,MAAM,EAAE,QAAQ,GAAE,KAAK,EAAO;IAMhD,IAAI,QAAQ,IAAI,KAAK,EAAE,CAEtB;IAED,IAAI,MAAM,IAAI,GAAG,CAAC,KAAK,CAAC,CAEvB;IAED,QAAQ,CAAC,KAAK,EAAE,KAAK,GAAG,IAAI;IAO5B,QAAQ,IAAI,MAAM;IAQlB,IAAI,CAAC,KAAK,EAAE,KAAK,GAAG,KAAK;IAazB,QAAQ,CAAC,QAAQ,gBAAsB,GAAG,KAAK;IAa/C,MAAM,CAAC,KAAK,EAAE,KAAK,GAAG,OAAO;CAqB9B"}
@@ -0,0 +1,76 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.Crate = void 0;
4
+ const util_1 = require("../util");
5
+ class Crate {
6
+ constructor(name, children = []) {
7
+ this._children = [...children];
8
+ this.name = (0, util_1.sanitizeFilename)(name);
9
+ this._tracks = new Set();
10
+ }
11
+ get children() {
12
+ return this._children;
13
+ }
14
+ get tracks() {
15
+ return this._tracks;
16
+ }
17
+ addTrack(track) {
18
+ if (this._tracks.has(track)) {
19
+ throw new util_1.DuplicateTrackError(`track ${track} is already in the crate ${this.name}`);
20
+ }
21
+ this._tracks.add(track);
22
+ }
23
+ toString() {
24
+ return `Crate<${this.name}>`;
25
+ }
26
+ [Symbol.for("nodejs.util.inspect.custom")]() {
27
+ return this.toString();
28
+ }
29
+ plus(other) {
30
+ if (this.name !== other.name) {
31
+ throw new Error("Cannot merge crates with different names");
32
+ }
33
+ const childrenCopy = [...this._children, ...other._children].map((c) => c.deepCopy());
34
+ const merged = new Crate(this.name, childrenCopy);
35
+ const allTracks = new Set([...this._tracks, ...other._tracks]);
36
+ for (const track of allTracks) {
37
+ merged.addTrack(track);
38
+ }
39
+ return merged;
40
+ }
41
+ deepCopy(memodict = new Map()) {
42
+ if (memodict.has(this)) {
43
+ return memodict.get(this);
44
+ }
45
+ const childrenCopy = this._children.map((c) => c.deepCopy(memodict));
46
+ const copy = new Crate(this.name, childrenCopy);
47
+ memodict.set(this, copy);
48
+ for (const track of this._tracks) {
49
+ copy.addTrack(track);
50
+ }
51
+ return copy;
52
+ }
53
+ equals(other) {
54
+ if (this.name !== other.name)
55
+ return false;
56
+ if (this._tracks.size !== other._tracks.size)
57
+ return false;
58
+ // compare tracks by reference equality (like Python set)
59
+ for (const track of this._tracks) {
60
+ if (!other._tracks.has(track))
61
+ return false;
62
+ }
63
+ if (this._children.length || other._children.length) {
64
+ const sortedChildren = [...this._children].sort((a, b) => a._tracks.size - b._tracks.size);
65
+ const sortedOtherChildren = [...other._children].sort((a, b) => a._tracks.size - b._tracks.size);
66
+ if (sortedChildren.length !== sortedOtherChildren.length)
67
+ return false;
68
+ for (let i = 0; i < sortedChildren.length; i++) {
69
+ if (!sortedChildren[i].equals(sortedOtherChildren[i]))
70
+ return false;
71
+ }
72
+ }
73
+ return true;
74
+ }
75
+ }
76
+ exports.Crate = Crate;
@@ -0,0 +1,28 @@
1
+ import { HotCueType } from './hotCueType';
2
+ import { SeratoColor } from './seratoColor';
3
+ import { Buffer } from 'buffer';
4
+ export interface HotCueData {
5
+ name: string;
6
+ type: HotCueType;
7
+ start: number;
8
+ index: number;
9
+ end?: number | null;
10
+ isLocked?: boolean;
11
+ color?: SeratoColor;
12
+ }
13
+ export declare class HotCue implements HotCueData {
14
+ name: string;
15
+ type: HotCueType;
16
+ start: number;
17
+ index: number;
18
+ end?: number | null;
19
+ isLocked?: boolean;
20
+ color: SeratoColor;
21
+ constructor(data: HotCueData);
22
+ toString(): string;
23
+ toV2Bytes(): Buffer;
24
+ private loopToV2Bytes;
25
+ private cueToV2Bytes;
26
+ static fromBytes(data: Buffer, hotcueType: HotCueType): HotCue;
27
+ }
28
+ //# sourceMappingURL=hotCue.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"hotCue.d.ts","sourceRoot":"","sources":["../../src/model/hotCue.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAC1C,OAAO,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAC5C,OAAO,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AA6BhC,MAAM,WAAW,UAAU;IACzB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,UAAU,CAAC;IACjB,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;IACd,GAAG,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACpB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,KAAK,CAAC,EAAE,WAAW,CAAC;CACrB;AAED,qBAAa,MAAO,YAAW,UAAU;IACvC,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,UAAU,CAAC;IACjB,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;IACd,GAAG,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACpB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,KAAK,EAAE,WAAW,CAAC;gBAEP,IAAI,EAAE,UAAU;IAU5B,QAAQ,IAAI,MAAM;IAMlB,SAAS,IAAI,MAAM;IAMnB,OAAO,CAAC,aAAa;IAmBrB,OAAO,CAAC,YAAY;IAiDpB,MAAM,CAAC,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,UAAU,EAAE,UAAU,GAAG,MAAM;CAiE/D"}
@@ -0,0 +1,166 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.HotCue = void 0;
4
+ const hotCueType_1 = require("./hotCueType");
5
+ const seratoColor_1 = require("./seratoColor");
6
+ const buffer_1 = require("buffer");
7
+ function writeNullTerminatedString(str) {
8
+ const encoder = new TextEncoder();
9
+ const strBytes = encoder.encode(str);
10
+ const result = new Uint8Array(strBytes.length + 1); // +1 for null terminator
11
+ result.set(strBytes, 0);
12
+ result[strBytes.length] = 0;
13
+ return result;
14
+ }
15
+ function concatUint8Arrays(arrays) {
16
+ let totalLength = arrays.reduce((sum, arr) => sum + arr.length, 0);
17
+ let result = new Uint8Array(totalLength);
18
+ let offset = 0;
19
+ arrays.forEach(arr => {
20
+ result.set(arr, offset);
21
+ offset += arr.length;
22
+ });
23
+ return result;
24
+ }
25
+ function encodeElement(name, data) {
26
+ const nameBytes = writeNullTerminatedString(name);
27
+ const lengthBytes = new Uint8Array(4);
28
+ new DataView(lengthBytes.buffer).setUint32(0, data.length, false); // big endian
29
+ return concatUint8Arrays([nameBytes, lengthBytes, data]);
30
+ }
31
+ class HotCue {
32
+ constructor(data) {
33
+ this.name = data.name;
34
+ this.type = data.type;
35
+ this.start = data.start;
36
+ this.index = data.index;
37
+ this.end = data.end ?? null;
38
+ this.isLocked = data.isLocked ?? false;
39
+ this.color = data.color ?? seratoColor_1.SeratoColor.RED;
40
+ }
41
+ toString() {
42
+ const startStr = `${this.start}ms`;
43
+ const endStr = this.end != null ? ` | End: ${this.end}ms` : '';
44
+ return `Start: ${startStr}${endStr} | Index: ${String(this.index).padStart(2)} | Name: ${this.name} | Color: ${this.color}`;
45
+ }
46
+ toV2Bytes() {
47
+ if (this.type === hotCueType_1.HotCueType.CUE)
48
+ return this.cueToV2Bytes();
49
+ if (this.type === hotCueType_1.HotCueType.LOOP)
50
+ return this.loopToV2Bytes();
51
+ throw new Error(`unsupported hotcue type ${this.type}`);
52
+ }
53
+ loopToV2Bytes() {
54
+ let nameBytes = writeNullTerminatedString(this.name);
55
+ let buf = new Uint8Array(0x14 + nameBytes.length);
56
+ let dv = new DataView(buf.buffer);
57
+ buf[0x0] = 0x00; // flags? (unused in parse)
58
+ buf[0x1] = this.index;
59
+ dv.setUint32(0x02, this.start, false);
60
+ dv.setUint32(0x06, this.end, false);
61
+ buf[0x0e] = 0;
62
+ buf[0x0f] = 0;
63
+ buf[0x10] = 255;
64
+ buf[0x11] = 255;
65
+ buf[0x13] = 1;
66
+ buf.set(nameBytes, 0x14);
67
+ return buffer_1.Buffer.from(encodeElement("LOOP", buf));
68
+ }
69
+ cueToV2Bytes() {
70
+ const parts = [];
71
+ // >B (unsigned char, big-endian)
72
+ parts.push(buffer_1.Buffer.from([0]));
73
+ // >B self.index
74
+ parts.push(buffer_1.Buffer.from([this.index]));
75
+ // >I self.start (4-byte big-endian unsigned int)
76
+ const startBuf = buffer_1.Buffer.alloc(4);
77
+ startBuf.writeUInt32BE(this.start, 0);
78
+ parts.push(startBuf);
79
+ // >B 0
80
+ parts.push(buffer_1.Buffer.from([0]));
81
+ // >3s color (3 raw bytes from hex string)
82
+ const colorBuf = buffer_1.Buffer.from(this.color, "hex");
83
+ if (colorBuf.length !== 3) {
84
+ throw new Error(`Color must be 3 bytes (e.g. "ff0000"), got ${this.color}`);
85
+ }
86
+ parts.push(colorBuf);
87
+ // >B 0
88
+ parts.push(buffer_1.Buffer.from([0]));
89
+ parts.push(buffer_1.Buffer.from([1]));
90
+ parts.push(buffer_1.Buffer.from(this.name, "utf8"));
91
+ // >B 0 (null terminator after name)
92
+ parts.push(buffer_1.Buffer.from([0]));
93
+ const data = buffer_1.Buffer.concat(parts);
94
+ const lenBuf = buffer_1.Buffer.alloc(4);
95
+ lenBuf.writeUInt32BE(data.length, 0);
96
+ const payload = buffer_1.Buffer.concat([
97
+ buffer_1.Buffer.from("CUE", "ascii"),
98
+ buffer_1.Buffer.from([0]),
99
+ lenBuf,
100
+ data,
101
+ ]);
102
+ return payload;
103
+ }
104
+ static fromBytes(data, hotcueType) {
105
+ let offset = 1; // skip first null
106
+ if (hotcueType === hotCueType_1.HotCueType.CUE) {
107
+ const index = data.readUInt8(offset);
108
+ offset += 1;
109
+ const start = data.readUInt32BE(offset);
110
+ offset += 4;
111
+ offset += 1; // skip null / position end
112
+ offset += 0; // skip placeholder
113
+ const colorBytes = data.slice(offset, offset + 3);
114
+ offset += 3;
115
+ offset += 1; // skip null
116
+ const locked = data.readUInt8(offset) !== 0;
117
+ offset += 1;
118
+ const nameBuf = data.slice(offset).subarray(0, data.slice(offset).indexOf(0));
119
+ const name = nameBuf.toString("utf-8");
120
+ const colorHex = colorBytes.toString("hex").toUpperCase();
121
+ const color = (Object.values(seratoColor_1.SeratoColor).includes(colorHex)
122
+ ? colorHex
123
+ : seratoColor_1.SeratoColor.RED);
124
+ return new HotCue({
125
+ name,
126
+ type: hotCueType_1.HotCueType.CUE,
127
+ index,
128
+ start,
129
+ color,
130
+ isLocked: locked,
131
+ });
132
+ }
133
+ else if (hotcueType === hotCueType_1.HotCueType.LOOP) {
134
+ const index = data.readUInt8(offset);
135
+ offset += 1;
136
+ const start = data.readUInt32BE(offset);
137
+ offset += 4;
138
+ const end = data.readUInt32BE(offset);
139
+ offset += 4;
140
+ // Skip to offset 0x0E
141
+ offset = 0x0E;
142
+ // skip color placeholder bytes (0x0E–0x11)
143
+ offset += 2; // 0x0E, 0x0F
144
+ offset += 2; // 0x10, 0x11
145
+ offset += 1; // 0x12 (padding)
146
+ const isLocked = Boolean(data.readUInt8(offset));
147
+ offset += 1;
148
+ // read null-terminated name
149
+ const nameEnd = data.indexOf(0x00, offset);
150
+ const name = data.toString("utf8", offset, nameEnd === -1 ? data.length : nameEnd);
151
+ return new HotCue({
152
+ name,
153
+ type: hotCueType_1.HotCueType.CUE,
154
+ index,
155
+ start,
156
+ end: end,
157
+ color: seratoColor_1.SeratoColor.RED,
158
+ isLocked: isLocked,
159
+ });
160
+ }
161
+ else {
162
+ throw new Error(`Unknown hotcue type: ${hotcueType}`);
163
+ }
164
+ }
165
+ }
166
+ exports.HotCue = HotCue;
@@ -0,0 +1,7 @@
1
+ export declare enum HotCueType {
2
+ CUE = 0,
3
+ LOOP = 1,
4
+ INVALID = 99,
5
+ PASSTHROUGH = 100
6
+ }
7
+ //# sourceMappingURL=hotCueType.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"hotCueType.d.ts","sourceRoot":"","sources":["../../src/model/hotCueType.ts"],"names":[],"mappings":"AAAA,oBAAY,UAAU;IACpB,GAAG,IAAI;IACP,IAAI,IAAI;IACR,OAAO,KAAK;IACZ,WAAW,MAAM;CAClB"}
@@ -0,0 +1,10 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.HotCueType = void 0;
4
+ var HotCueType;
5
+ (function (HotCueType) {
6
+ HotCueType[HotCueType["CUE"] = 0] = "CUE";
7
+ HotCueType[HotCueType["LOOP"] = 1] = "LOOP";
8
+ HotCueType[HotCueType["INVALID"] = 99] = "INVALID";
9
+ HotCueType[HotCueType["PASSTHROUGH"] = 100] = "PASSTHROUGH";
10
+ })(HotCueType || (exports.HotCueType = HotCueType = {}));
@@ -0,0 +1,21 @@
1
+ export declare enum SeratoColor {
2
+ RED = "CC0000",
3
+ ORANGE = "CC4400",
4
+ AMBER = "CC8800",
5
+ YELLOW = "CCCC00",
6
+ LIME_GREEN = "88CC00",
7
+ GREEN_YELLOW = "44CC00",
8
+ GREEN = "00CC00",
9
+ MINT_GREEN = "00CC44",
10
+ TEAL_GREEN = "00CC88",
11
+ TEAL = "00CCCC",
12
+ SKY_BLUE = "0088CC",
13
+ BLUE = "0044CC",
14
+ DARK_BLUE = "0000CC",
15
+ PURPLE = "4400CC",
16
+ VIOLET = "8800CC",
17
+ MAGENTA = "CC00CC",
18
+ PINK = "CC0088",
19
+ RED_PINK = "CC0044"
20
+ }
21
+ //# sourceMappingURL=seratoColor.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"seratoColor.d.ts","sourceRoot":"","sources":["../../src/model/seratoColor.ts"],"names":[],"mappings":"AAAA,oBAAY,WAAW;IACrB,GAAG,WAAW;IACd,MAAM,WAAW;IACjB,KAAK,WAAW;IAChB,MAAM,WAAW;IACjB,UAAU,WAAW;IACrB,YAAY,WAAW;IACvB,KAAK,WAAW;IAChB,UAAU,WAAW;IACrB,UAAU,WAAW;IACrB,IAAI,WAAW;IACf,QAAQ,WAAW;IACnB,IAAI,WAAW;IACf,SAAS,WAAW;IACpB,MAAM,WAAW;IACjB,MAAM,WAAW;IACjB,OAAO,WAAW;IAClB,IAAI,WAAW;IACf,QAAQ,WAAW;CACpB"}
@@ -0,0 +1,24 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.SeratoColor = void 0;
4
+ var SeratoColor;
5
+ (function (SeratoColor) {
6
+ SeratoColor["RED"] = "CC0000";
7
+ SeratoColor["ORANGE"] = "CC4400";
8
+ SeratoColor["AMBER"] = "CC8800";
9
+ SeratoColor["YELLOW"] = "CCCC00";
10
+ SeratoColor["LIME_GREEN"] = "88CC00";
11
+ SeratoColor["GREEN_YELLOW"] = "44CC00";
12
+ SeratoColor["GREEN"] = "00CC00";
13
+ SeratoColor["MINT_GREEN"] = "00CC44";
14
+ SeratoColor["TEAL_GREEN"] = "00CC88";
15
+ SeratoColor["TEAL"] = "00CCCC";
16
+ SeratoColor["SKY_BLUE"] = "0088CC";
17
+ SeratoColor["BLUE"] = "0044CC";
18
+ SeratoColor["DARK_BLUE"] = "0000CC";
19
+ SeratoColor["PURPLE"] = "4400CC";
20
+ SeratoColor["VIOLET"] = "8800CC";
21
+ SeratoColor["MAGENTA"] = "CC00CC";
22
+ SeratoColor["PINK"] = "CC0088";
23
+ SeratoColor["RED_PINK"] = "CC0044";
24
+ })(SeratoColor || (exports.SeratoColor = SeratoColor = {}));
@@ -0,0 +1,5 @@
1
+ export interface Tempo {
2
+ position?: number | null;
3
+ bpm?: number | null;
4
+ }
5
+ //# sourceMappingURL=tempo.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tempo.d.ts","sourceRoot":"","sources":["../../src/model/tempo.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,KAAK;IACpB,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,GAAG,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CACrB"}
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -0,0 +1,29 @@
1
+ import { HotCue } from './hotCue';
2
+ import { Tempo } from './tempo';
3
+ export declare class Track {
4
+ path: string;
5
+ trackId: string;
6
+ averageBpm: number;
7
+ dateAdded: string;
8
+ playCount: string;
9
+ tonality: string;
10
+ totalTime: number;
11
+ beatgrid: Tempo[];
12
+ hotCues: HotCue[];
13
+ cueLoops: HotCue[];
14
+ constructor(trackPath: string, params?: {
15
+ trackId?: string;
16
+ averageBpm?: number;
17
+ dateAdded?: string;
18
+ playCount?: string;
19
+ tonality?: string;
20
+ totalTime?: number;
21
+ beatgrid?: Tempo[];
22
+ hotCues?: HotCue[];
23
+ cueLoops?: HotCue[];
24
+ });
25
+ static fromPath(trackPath: string, userRoot?: string): Track;
26
+ addBeatgridMarker(tempo: Tempo): void;
27
+ addHotCue(hotCue: HotCue): void;
28
+ }
29
+ //# sourceMappingURL=track.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"track.d.ts","sourceRoot":"","sources":["../../src/model/track.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,MAAM,EAAE,MAAM,UAAU,CAAC;AAElC,OAAO,EAAE,KAAK,EAAE,MAAM,SAAS,CAAC;AAEhC,qBAAa,KAAK;IAChB,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;IAElB,QAAQ,EAAE,KAAK,EAAE,CAAC;IAClB,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,QAAQ,EAAE,MAAM,EAAE,CAAC;gBAGjB,SAAS,EAAE,MAAM,EACjB,MAAM,GAAE;QACN,OAAO,CAAC,EAAE,MAAM,CAAC;QACjB,UAAU,CAAC,EAAE,MAAM,CAAC;QACpB,SAAS,CAAC,EAAE,MAAM,CAAC;QACnB,SAAS,CAAC,EAAE,MAAM,CAAC;QACnB,QAAQ,CAAC,EAAE,MAAM,CAAC;QAClB,SAAS,CAAC,EAAE,MAAM,CAAC;QACnB,QAAQ,CAAC,EAAE,KAAK,EAAE,CAAC;QACnB,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;QACnB,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAC;KAChB;IAeR,MAAM,CAAC,QAAQ,CAAC,SAAS,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,MAAM,GAAG,KAAK;IAK5D,iBAAiB,CAAC,KAAK,EAAE,KAAK,GAAG,IAAI;IAIrC,SAAS,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI;CAehC"}
@@ -0,0 +1,75 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.Track = void 0;
37
+ const path = __importStar(require("path"));
38
+ const hotCueType_1 = require("./hotCueType");
39
+ class Track {
40
+ constructor(trackPath, params = {}) {
41
+ this.path = trackPath;
42
+ this.trackId = params.trackId ?? "";
43
+ this.averageBpm = params.averageBpm ?? 0.0;
44
+ this.dateAdded = params.dateAdded ?? "";
45
+ this.playCount = params.playCount ?? "";
46
+ this.tonality = params.tonality ?? "";
47
+ this.totalTime = params.totalTime ?? 0.0;
48
+ this.beatgrid = params.beatgrid ?? [];
49
+ this.hotCues = params.hotCues ?? [];
50
+ this.cueLoops = params.cueLoops ?? [];
51
+ }
52
+ static fromPath(trackPath, userRoot) {
53
+ const resolved = userRoot ? path.resolve(userRoot, trackPath) : path.resolve(trackPath);
54
+ return new Track(resolved);
55
+ }
56
+ addBeatgridMarker(tempo) {
57
+ this.beatgrid.push(tempo);
58
+ }
59
+ addHotCue(hotCue) {
60
+ if (this.hotCues.length >= 8) {
61
+ throw new Error("cannot have more than 8 hot cues on a track");
62
+ }
63
+ if (this.cueLoops.length >= 4) {
64
+ throw new Error("cannot have more than 4 loops on a track");
65
+ }
66
+ const atIndex = hotCue.index;
67
+ if (hotCue.type === hotCueType_1.HotCueType.LOOP) {
68
+ this.cueLoops.splice(atIndex, 0, hotCue);
69
+ }
70
+ else {
71
+ this.hotCues.splice(atIndex, 0, hotCue);
72
+ }
73
+ }
74
+ }
75
+ exports.Track = Track;
package/dist/util.d.ts ADDED
@@ -0,0 +1,11 @@
1
+ import { Buffer } from 'buffer';
2
+ export declare function splitString(input: Buffer, after?: number, delimiter?: Buffer): Buffer;
3
+ export declare function seratoDecode(s: Buffer): string;
4
+ export declare function concatBytes(arrays: Uint8Array[]): Uint8Array;
5
+ export declare function latin1Encode(str: string): Uint8Array;
6
+ export declare function intToBytes(value: number, length: number): Uint8Array;
7
+ export declare function seratoEncode(s: string): Uint8Array;
8
+ export declare function sanitizeFilename(filename: string): string;
9
+ export declare class DuplicateTrackError extends Error {
10
+ }
11
+ //# sourceMappingURL=util.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"util.d.ts","sourceRoot":"","sources":["../src/util.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAIhC,wBAAgB,WAAW,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,GAAE,MAAW,EAAE,SAAS,GAAE,MAA0B,GAAG,MAAM,CAY5G;AAED,wBAAgB,YAAY,CAAC,CAAC,EAAE,MAAM,GAAG,MAAM,CA2B9C;AAED,wBAAgB,WAAW,CAAC,MAAM,EAAE,UAAU,EAAE,GAAG,UAAU,CAS5D;AAED,wBAAgB,YAAY,CAAC,GAAG,EAAE,MAAM,GAAG,UAAU,CAEpD;AAED,wBAAgB,UAAU,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,UAAU,CAOpE;AAED,wBAAgB,YAAY,CAAC,CAAC,EAAE,MAAM,GAAG,UAAU,CAWlD;AAED,wBAAgB,gBAAgB,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAEzD;AAED,qBAAa,mBAAoB,SAAQ,KAAK;CAAG"}
package/dist/util.js ADDED
@@ -0,0 +1,93 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.DuplicateTrackError = void 0;
4
+ exports.splitString = splitString;
5
+ exports.seratoDecode = seratoDecode;
6
+ exports.concatBytes = concatBytes;
7
+ exports.latin1Encode = latin1Encode;
8
+ exports.intToBytes = intToBytes;
9
+ exports.seratoEncode = seratoEncode;
10
+ exports.sanitizeFilename = sanitizeFilename;
11
+ const buffer_1 = require("buffer");
12
+ const INVALID_CHARACTERS_REGEX = /[^A-Za-z0-9_ ]/i;
13
+ function splitString(input, after = 72, delimiter = buffer_1.Buffer.from('\n')) {
14
+ const pieces = [];
15
+ let buf = buffer_1.Buffer.from(input);
16
+ while (buf.length > 0) {
17
+ pieces.push(buf.slice(0, after));
18
+ buf = buf.slice(after);
19
+ }
20
+ return buffer_1.Buffer.concat(pieces.reduce((acc, p, i) => {
21
+ if (i)
22
+ acc.push(delimiter);
23
+ acc.push(p);
24
+ return acc;
25
+ }, []));
26
+ }
27
+ function seratoDecode(s) {
28
+ // Decodes Serato 4-byte blocks into UTF-16-encoded string
29
+ const out = [];
30
+ for (let i = 0; i < s.length; i += 4) {
31
+ const block = s.slice(i, i + 4);
32
+ if (block.length < 4)
33
+ break;
34
+ const w = block.readUInt8(0);
35
+ const x = block.readUInt8(1);
36
+ const y = block.readUInt8(2);
37
+ const z = block.readUInt8(3);
38
+ const c = (z & 0x7F) | ((y & 0x01) << 7);
39
+ const b = ((y & 0x7F) >> 1) | ((x & 0x03) << 6);
40
+ const a = ((x & 0x7F) >> 2) | ((w & 0x07) << 5);
41
+ out.push(a, b, c);
42
+ }
43
+ // Interpret as UTF-16-BE pairs
44
+ // Convert bytes to string by grouping into 2-byte code units
45
+ const bytes = buffer_1.Buffer.from(out);
46
+ let str = '';
47
+ for (let i = 0; i < bytes.length; i += 2) {
48
+ const hi = bytes[i];
49
+ const lo = (i + 1) < bytes.length ? bytes[i + 1] : 0;
50
+ const code = (hi << 8) | lo;
51
+ if (code === 0)
52
+ break;
53
+ str += String.fromCharCode(code);
54
+ }
55
+ return str;
56
+ }
57
+ function concatBytes(arrays) {
58
+ const totalLength = arrays.reduce((sum, a) => sum + a.length, 0);
59
+ const result = new Uint8Array(totalLength);
60
+ let offset = 0;
61
+ for (const a of arrays) {
62
+ result.set(a, offset);
63
+ offset += a.length;
64
+ }
65
+ return result;
66
+ }
67
+ function latin1Encode(str) {
68
+ return Uint8Array.from(buffer_1.Buffer.from(str, "latin1"));
69
+ }
70
+ function intToBytes(value, length) {
71
+ const bytes = new Uint8Array(length);
72
+ for (let i = 0; i < length; i++) {
73
+ // big-endian
74
+ bytes[length - 1 - i] = (value >> (8 * i)) & 0xff;
75
+ }
76
+ return bytes;
77
+ }
78
+ function seratoEncode(s) {
79
+ const result = [];
80
+ for (const c of s) {
81
+ const codePoint = c.charCodeAt(0); // UTF-16 code unit
82
+ // Big-endian: high byte first, then low byte
83
+ result.push((codePoint >> 8) & 0xff);
84
+ result.push(codePoint & 0xff);
85
+ }
86
+ return Uint8Array.from(result);
87
+ }
88
+ function sanitizeFilename(filename) {
89
+ return filename.replace(INVALID_CHARACTERS_REGEX, '-');
90
+ }
91
+ class DuplicateTrackError extends Error {
92
+ }
93
+ exports.DuplicateTrackError = DuplicateTrackError;
package/package.json ADDED
@@ -0,0 +1,24 @@
1
+ {
2
+ "name": "tserato",
3
+ "version": "0.1.0",
4
+ "main": "dist/index.js",
5
+ "types": "dist/index.d.ts",
6
+ "type": "commonjs",
7
+ "scripts": {
8
+ "build": "tsc"
9
+ },
10
+ "repository": {
11
+ "type": "git",
12
+ "url": "https://github.com/laker-93/tserato.git"
13
+ },
14
+ "devDependencies": {
15
+ "@types/node": "^24.5.1",
16
+ "typescript": "^5.3.3",
17
+ "vitest": "^0.34.6"
18
+ },
19
+ "dependencies": {
20
+ "base-64": "^1.0.0",
21
+ "mp3tag.js": "^3.14.1",
22
+ "node-id3": "^0.2.6"
23
+ }
24
+ }