ts-flp 1.0.2 → 1.0.4
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/dist/index.cjs +1102 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.js +1038 -40
- package/dist/index.js.map +1 -1
- package/dist/src/api/ProjectApi.d.ts.map +1 -1
- package/dist/src/io/BinaryReader.d.ts +1 -0
- package/dist/src/io/BinaryReader.d.ts.map +1 -1
- package/dist/src/parser/FlpParser.d.ts.map +1 -1
- package/package.json +6 -3
- package/dist/src/api/ProjectApi.js +0 -326
- package/dist/src/api/ProjectApi.js.map +0 -1
- package/dist/src/generated/events.generated.js +0 -257
- package/dist/src/generated/events.generated.js.map +0 -1
- package/dist/src/io/BinaryReader.js +0 -159
- package/dist/src/io/BinaryReader.js.map +0 -1
- package/dist/src/io/BinaryWriter.js +0 -124
- package/dist/src/io/BinaryWriter.js.map +0 -1
- package/dist/src/parser/FlpParser.js +0 -313
- package/dist/src/parser/FlpParser.js.map +0 -1
- package/dist/test/flparser.test.js +0 -345
- package/dist/test/flparser.test.js.map +0 -1
- package/dist/vitest.config.js +0 -10
- package/dist/vitest.config.js.map +0 -1
|
@@ -1,313 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* FL Studio .flp file parser and serializer
|
|
3
|
-
* Conservative patch approach: preserves all unknown data byte-for-byte
|
|
4
|
-
*/
|
|
5
|
-
import { DWORD, EVENT_ID, TEXT, WORD, getEventKind } from "../generated/events.generated.js";
|
|
6
|
-
import { BinaryReader } from "../io/BinaryReader.js";
|
|
7
|
-
import { BinaryWriter } from "../io/BinaryWriter.js";
|
|
8
|
-
/**
|
|
9
|
-
* Header structure constants
|
|
10
|
-
*/
|
|
11
|
-
const FLP_HEADER_MAGIC = "FLhd";
|
|
12
|
-
const FLP_DATA_MAGIC = "FLdt";
|
|
13
|
-
const FLP_HEADER_SIZE = 6; // Expected header chunk size
|
|
14
|
-
/**
|
|
15
|
-
* Parses an FL Studio project file
|
|
16
|
-
*
|
|
17
|
-
* @param buffer - Raw file data
|
|
18
|
-
* @returns Parsed FLP structure
|
|
19
|
-
* @throws Error if file is corrupted or invalid
|
|
20
|
-
*/
|
|
21
|
-
export function parseFlp(buffer) {
|
|
22
|
-
const reader = new BinaryReader(buffer);
|
|
23
|
-
// Read FLhd header chunk
|
|
24
|
-
const headerMagic = reader.readBytes(4).toString("ascii");
|
|
25
|
-
if (headerMagic !== FLP_HEADER_MAGIC) {
|
|
26
|
-
throw new Error(`Invalid header magic: expected 'FLhd', got '${headerMagic}'`);
|
|
27
|
-
}
|
|
28
|
-
const headerSize = reader.readU32LE();
|
|
29
|
-
if (headerSize !== FLP_HEADER_SIZE) {
|
|
30
|
-
throw new Error(`Invalid header size: expected ${FLP_HEADER_SIZE}, got ${headerSize}`);
|
|
31
|
-
}
|
|
32
|
-
const format = reader.readI16LE();
|
|
33
|
-
const channelCount = reader.readU16LE();
|
|
34
|
-
const ppq = reader.readU16LE();
|
|
35
|
-
// Validate format (0 = normal project)
|
|
36
|
-
if (format < -1 || format > 0x50) {
|
|
37
|
-
throw new Error(`Invalid file format: ${format}`);
|
|
38
|
-
}
|
|
39
|
-
// Store complete header chunk (14 bytes)
|
|
40
|
-
const headerChunkBytes = buffer.subarray(0, 14);
|
|
41
|
-
// Read FLdt data chunk header
|
|
42
|
-
const dataMagic = reader.readBytes(4).toString("ascii");
|
|
43
|
-
if (dataMagic !== FLP_DATA_MAGIC) {
|
|
44
|
-
throw new Error(`Invalid data magic: expected 'FLdt', got '${dataMagic}'`);
|
|
45
|
-
}
|
|
46
|
-
const eventsSize = reader.readU32LE();
|
|
47
|
-
// Validate events size
|
|
48
|
-
const expectedFileSize = 22 + eventsSize; // 14 (FLhd) + 8 (FLdt header) + events
|
|
49
|
-
if (buffer.length !== expectedFileSize) {
|
|
50
|
-
throw new Error(`Data chunk size mismatch: expected file size ${expectedFileSize}, got ${buffer.length}`);
|
|
51
|
-
}
|
|
52
|
-
// Store FLdt header (8 bytes)
|
|
53
|
-
const fldtHeaderBytes = buffer.subarray(14, 22);
|
|
54
|
-
// Parse events
|
|
55
|
-
const events = [];
|
|
56
|
-
let flVersion = "0.0.0";
|
|
57
|
-
let useUnicode = false;
|
|
58
|
-
const eventsStart = reader.tell();
|
|
59
|
-
const eventsEnd = eventsStart + eventsSize;
|
|
60
|
-
while (reader.tell() < eventsEnd) {
|
|
61
|
-
const eventStart = reader.tell();
|
|
62
|
-
const eventId = reader.readU8();
|
|
63
|
-
let payloadSize;
|
|
64
|
-
let headerSize;
|
|
65
|
-
// Determine payload size based on event ID range
|
|
66
|
-
if (eventId < WORD) {
|
|
67
|
-
// BYTE range: 1 byte payload
|
|
68
|
-
payloadSize = 1;
|
|
69
|
-
headerSize = 1; // Just the ID
|
|
70
|
-
}
|
|
71
|
-
else if (eventId < DWORD) {
|
|
72
|
-
// WORD range: 2 bytes payload
|
|
73
|
-
payloadSize = 2;
|
|
74
|
-
headerSize = 1;
|
|
75
|
-
}
|
|
76
|
-
else if (eventId < TEXT) {
|
|
77
|
-
// DWORD range: 4 bytes payload
|
|
78
|
-
payloadSize = 4;
|
|
79
|
-
headerSize = 1;
|
|
80
|
-
}
|
|
81
|
-
else {
|
|
82
|
-
// TEXT/DATA range: VarInt-encoded size
|
|
83
|
-
const sizeStart = reader.tell();
|
|
84
|
-
payloadSize = reader.readVarInt();
|
|
85
|
-
headerSize = 1 + (reader.tell() - sizeStart); // ID + VarInt bytes
|
|
86
|
-
}
|
|
87
|
-
// Read payload
|
|
88
|
-
const payload = reader.readBytes(payloadSize);
|
|
89
|
-
// Extract header from original buffer for byte-perfect round-trip
|
|
90
|
-
const header = buffer.subarray(eventStart, eventStart + headerSize);
|
|
91
|
-
// Determine event kind
|
|
92
|
-
const kind = getEventKind(eventId);
|
|
93
|
-
const event = {
|
|
94
|
-
id: eventId,
|
|
95
|
-
kind,
|
|
96
|
-
header: Buffer.from(header),
|
|
97
|
-
payload: Buffer.from(payload),
|
|
98
|
-
};
|
|
99
|
-
events.push(event);
|
|
100
|
-
// Detect FL version from FLVersion event
|
|
101
|
-
if (eventId === EVENT_ID.PROJECT_FL_VERSION) {
|
|
102
|
-
flVersion = payload.toString("ascii").replace(/\0/g, "");
|
|
103
|
-
const parts = flVersion.split(".").map((p) => parseInt(p, 10));
|
|
104
|
-
const major = parts[0] ?? 0;
|
|
105
|
-
const minor = parts[1] ?? 0;
|
|
106
|
-
useUnicode = parts.length >= 2 && (major > 11 || (major === 11 && minor >= 5));
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
// Check for trailing bytes
|
|
110
|
-
let trailingBytes;
|
|
111
|
-
if (reader.remaining() > 0) {
|
|
112
|
-
trailingBytes = reader.readBytes(reader.remaining());
|
|
113
|
-
}
|
|
114
|
-
return {
|
|
115
|
-
headerChunkBytes: Buffer.from(headerChunkBytes),
|
|
116
|
-
fldtHeaderBytes: Buffer.from(fldtHeaderBytes),
|
|
117
|
-
events,
|
|
118
|
-
trailingBytes,
|
|
119
|
-
flVersion,
|
|
120
|
-
useUnicode,
|
|
121
|
-
};
|
|
122
|
-
}
|
|
123
|
-
/**
|
|
124
|
-
* Serializes a parsed FLP back to a buffer
|
|
125
|
-
* Recalculates FLdt chunk size if events have changed
|
|
126
|
-
*
|
|
127
|
-
* @param parsed - Parsed FLP structure
|
|
128
|
-
* @returns Serialized buffer
|
|
129
|
-
*/
|
|
130
|
-
export function serializeFlp(parsed) {
|
|
131
|
-
const writer = new BinaryWriter();
|
|
132
|
-
// Write FLhd header chunk (preserved byte-for-byte)
|
|
133
|
-
writer.writeBytes(parsed.headerChunkBytes);
|
|
134
|
-
// Calculate total events size
|
|
135
|
-
let eventsSize = 0;
|
|
136
|
-
for (const event of parsed.events) {
|
|
137
|
-
eventsSize += serializeEvent(event).length;
|
|
138
|
-
}
|
|
139
|
-
// Add trailing bytes if present
|
|
140
|
-
if (parsed.trailingBytes) {
|
|
141
|
-
eventsSize += parsed.trailingBytes.length;
|
|
142
|
-
}
|
|
143
|
-
// Write FLdt header with updated size
|
|
144
|
-
writer.writeBytes(Buffer.from(FLP_DATA_MAGIC, "ascii"));
|
|
145
|
-
writer.writeU32LE(eventsSize);
|
|
146
|
-
// Write all events
|
|
147
|
-
for (const event of parsed.events) {
|
|
148
|
-
writer.writeBytes(serializeEvent(event));
|
|
149
|
-
}
|
|
150
|
-
// Write trailing bytes if present
|
|
151
|
-
if (parsed.trailingBytes) {
|
|
152
|
-
writer.writeBytes(parsed.trailingBytes);
|
|
153
|
-
}
|
|
154
|
-
return writer.toBuffer();
|
|
155
|
-
}
|
|
156
|
-
/**
|
|
157
|
-
* Serializes a single event to bytes
|
|
158
|
-
* Uses original header for unchanged events, recalculates for modified ones
|
|
159
|
-
*/
|
|
160
|
-
function serializeEvent(event) {
|
|
161
|
-
const writer = new BinaryWriter();
|
|
162
|
-
// Write event ID
|
|
163
|
-
writer.writeU8(event.id);
|
|
164
|
-
// For TEXT/DATA events, we need to write the VarInt size
|
|
165
|
-
if (event.id >= TEXT) {
|
|
166
|
-
writer.writeVarInt(event.payload.length);
|
|
167
|
-
}
|
|
168
|
-
// Write payload
|
|
169
|
-
writer.writeBytes(event.payload);
|
|
170
|
-
return writer.toBuffer();
|
|
171
|
-
}
|
|
172
|
-
/**
|
|
173
|
-
* Creates a new event with the given ID and payload
|
|
174
|
-
*/
|
|
175
|
-
export function createEvent(id, payload) {
|
|
176
|
-
const kind = getEventKind(id);
|
|
177
|
-
// Build header
|
|
178
|
-
const headerWriter = new BinaryWriter();
|
|
179
|
-
headerWriter.writeU8(id);
|
|
180
|
-
if (id >= TEXT) {
|
|
181
|
-
headerWriter.writeVarInt(payload.length);
|
|
182
|
-
}
|
|
183
|
-
return {
|
|
184
|
-
id,
|
|
185
|
-
kind,
|
|
186
|
-
header: headerWriter.toBuffer(),
|
|
187
|
-
payload: Buffer.from(payload),
|
|
188
|
-
};
|
|
189
|
-
}
|
|
190
|
-
/**
|
|
191
|
-
* Applies a patcher function to all events
|
|
192
|
-
* Returns a new ParsedFlp with patched events
|
|
193
|
-
*
|
|
194
|
-
* @param parsed - Original parsed FLP
|
|
195
|
-
* @param patcher - Function that transforms each event
|
|
196
|
-
* @returns New ParsedFlp with patched events
|
|
197
|
-
*/
|
|
198
|
-
export function patchEvents(parsed, patcher) {
|
|
199
|
-
const patchedEvents = parsed.events.map((event, index) => {
|
|
200
|
-
const patched = patcher(event, index);
|
|
201
|
-
// If payload changed, recalculate header for TEXT/DATA events
|
|
202
|
-
if (patched.payload !== event.payload && patched.id >= TEXT) {
|
|
203
|
-
const headerWriter = new BinaryWriter();
|
|
204
|
-
headerWriter.writeU8(patched.id);
|
|
205
|
-
headerWriter.writeVarInt(patched.payload.length);
|
|
206
|
-
return {
|
|
207
|
-
...patched,
|
|
208
|
-
header: headerWriter.toBuffer(),
|
|
209
|
-
};
|
|
210
|
-
}
|
|
211
|
-
return patched;
|
|
212
|
-
});
|
|
213
|
-
return {
|
|
214
|
-
...parsed,
|
|
215
|
-
events: patchedEvents,
|
|
216
|
-
};
|
|
217
|
-
}
|
|
218
|
-
/**
|
|
219
|
-
* Finds all events with a specific ID
|
|
220
|
-
*/
|
|
221
|
-
export function findEvents(parsed, eventId) {
|
|
222
|
-
return parsed.events.filter((e) => e.id === eventId);
|
|
223
|
-
}
|
|
224
|
-
/**
|
|
225
|
-
* Finds the first event with a specific ID
|
|
226
|
-
*/
|
|
227
|
-
export function findFirstEvent(parsed, eventId) {
|
|
228
|
-
return parsed.events.find((e) => e.id === eventId);
|
|
229
|
-
}
|
|
230
|
-
/**
|
|
231
|
-
* Gets event payload as a string (handles encoding)
|
|
232
|
-
*/
|
|
233
|
-
export function getEventString(event, useUnicode) {
|
|
234
|
-
if (event.kind !== "text") {
|
|
235
|
-
throw new Error(`Event ${event.id} is not a text event`);
|
|
236
|
-
}
|
|
237
|
-
if (useUnicode) {
|
|
238
|
-
const str = event.payload.toString("utf16le");
|
|
239
|
-
const nullPos = str.indexOf("\0");
|
|
240
|
-
return nullPos === -1 ? str : str.substring(0, nullPos);
|
|
241
|
-
}
|
|
242
|
-
else {
|
|
243
|
-
const str = event.payload.toString("ascii");
|
|
244
|
-
const nullPos = str.indexOf("\0");
|
|
245
|
-
return nullPos === -1 ? str : str.substring(0, nullPos);
|
|
246
|
-
}
|
|
247
|
-
}
|
|
248
|
-
/**
|
|
249
|
-
* Creates a text payload with proper encoding
|
|
250
|
-
*/
|
|
251
|
-
export function createTextPayload(text, useUnicode) {
|
|
252
|
-
if (useUnicode) {
|
|
253
|
-
return Buffer.from(text + "\0", "utf16le");
|
|
254
|
-
}
|
|
255
|
-
else {
|
|
256
|
-
return Buffer.from(text + "\0", "ascii");
|
|
257
|
-
}
|
|
258
|
-
}
|
|
259
|
-
/**
|
|
260
|
-
* Gets event payload as a number
|
|
261
|
-
*/
|
|
262
|
-
export function getEventNumber(event) {
|
|
263
|
-
const reader = new BinaryReader(event.payload);
|
|
264
|
-
switch (event.kind) {
|
|
265
|
-
case "u8":
|
|
266
|
-
return reader.readU8();
|
|
267
|
-
case "i8":
|
|
268
|
-
return reader.readI8();
|
|
269
|
-
case "u16":
|
|
270
|
-
return reader.readU16LE();
|
|
271
|
-
case "i16":
|
|
272
|
-
return reader.readI16LE();
|
|
273
|
-
case "u32":
|
|
274
|
-
return reader.readU32LE();
|
|
275
|
-
case "i32":
|
|
276
|
-
return reader.readI32LE();
|
|
277
|
-
case "f32":
|
|
278
|
-
return reader.readF32LE();
|
|
279
|
-
default:
|
|
280
|
-
throw new Error(`Cannot get number from ${event.kind} event`);
|
|
281
|
-
}
|
|
282
|
-
}
|
|
283
|
-
/**
|
|
284
|
-
* Creates a numeric payload
|
|
285
|
-
*/
|
|
286
|
-
export function createNumberPayload(value, kind) {
|
|
287
|
-
const writer = new BinaryWriter();
|
|
288
|
-
switch (kind) {
|
|
289
|
-
case "u8":
|
|
290
|
-
writer.writeU8(value);
|
|
291
|
-
break;
|
|
292
|
-
case "i8":
|
|
293
|
-
writer.writeI8(value);
|
|
294
|
-
break;
|
|
295
|
-
case "u16":
|
|
296
|
-
writer.writeU16LE(value);
|
|
297
|
-
break;
|
|
298
|
-
case "i16":
|
|
299
|
-
writer.writeI16LE(value);
|
|
300
|
-
break;
|
|
301
|
-
case "u32":
|
|
302
|
-
writer.writeU32LE(value);
|
|
303
|
-
break;
|
|
304
|
-
case "i32":
|
|
305
|
-
writer.writeI32LE(value);
|
|
306
|
-
break;
|
|
307
|
-
case "f32":
|
|
308
|
-
writer.writeF32LE(value);
|
|
309
|
-
break;
|
|
310
|
-
}
|
|
311
|
-
return writer.toBuffer();
|
|
312
|
-
}
|
|
313
|
-
//# sourceMappingURL=FlpParser.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"FlpParser.js","sourceRoot":"","sources":["../../../src/parser/FlpParser.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,345 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* FLP Parser unit tests
|
|
3
|
-
* Converted from flparser_unitest.py
|
|
4
|
-
*/
|
|
5
|
-
import * as fs from "fs";
|
|
6
|
-
import * as path from "path";
|
|
7
|
-
import { beforeAll, describe, expect, it } from "vitest";
|
|
8
|
-
import { getFlVersion, listPlugins, listSamples, parseFlp, readProjectMeta, readProjectTimeInfo, rewriteSamplePaths, serializeFlp, writeProjectMeta, writeProjectTimeInfo } from "../index.js";
|
|
9
|
-
// Test configuration
|
|
10
|
-
const TEST_PROJS_DIR = path.join(import.meta.dirname, "..", "test_projs");
|
|
11
|
-
const EXPECTED_FL_VERSION = "24"; // Update this to match your FL Studio version
|
|
12
|
-
/**
|
|
13
|
-
* Helper function to get test FLP files
|
|
14
|
-
*/
|
|
15
|
-
function getTestableProjects() {
|
|
16
|
-
if (!fs.existsSync(TEST_PROJS_DIR)) {
|
|
17
|
-
return [];
|
|
18
|
-
}
|
|
19
|
-
return fs.readdirSync(TEST_PROJS_DIR).filter((f) => f.endsWith(".flp"));
|
|
20
|
-
}
|
|
21
|
-
/**
|
|
22
|
-
* Helper function to load and parse a test FLP file
|
|
23
|
-
*/
|
|
24
|
-
function loadTestProject(filename) {
|
|
25
|
-
const filepath = path.join(TEST_PROJS_DIR, filename);
|
|
26
|
-
const buffer = fs.readFileSync(filepath);
|
|
27
|
-
return parseFlp(buffer);
|
|
28
|
-
}
|
|
29
|
-
function randomString(length) {
|
|
30
|
-
let result = "";
|
|
31
|
-
while (result.length < length) {
|
|
32
|
-
result += Math.random().toString(36).substring(2);
|
|
33
|
-
}
|
|
34
|
-
return result.substring(0, length);
|
|
35
|
-
}
|
|
36
|
-
function randomBPM() {
|
|
37
|
-
// return a random BPM between 50 and 190
|
|
38
|
-
return Math.floor(Math.random() * 140) + 50;
|
|
39
|
-
}
|
|
40
|
-
function randomDecimalBPM() {
|
|
41
|
-
// Random BPM with decimals between 60 and 200
|
|
42
|
-
return Math.floor(Math.random() * 140000 + 60000) / 1000;
|
|
43
|
-
}
|
|
44
|
-
function randomTimestamp() {
|
|
45
|
-
// Random date between 2020 and 2025
|
|
46
|
-
const start = new Date(2020, 0, 1).getTime();
|
|
47
|
-
const end = new Date(2025, 11, 31).getTime();
|
|
48
|
-
return new Date(start + Math.random() * (end - start));
|
|
49
|
-
}
|
|
50
|
-
function randomWorkTime() {
|
|
51
|
-
// Random work time between 0 and 1000000 seconds (~277 hours)
|
|
52
|
-
return Math.floor(Math.random() * 1000000);
|
|
53
|
-
}
|
|
54
|
-
function randomEmoji() {
|
|
55
|
-
const emojis = ["🎵", "🎹", "🎸", "🥁", "🎤", "🔊", "🎧", "🎼", "🤓", "👨🍳", "🔥", "💯", "✨", "🚀"];
|
|
56
|
-
return emojis[Math.floor(Math.random() * emojis.length)];
|
|
57
|
-
}
|
|
58
|
-
describe("FLP Parser", () => {
|
|
59
|
-
const testProjects = getTestableProjects();
|
|
60
|
-
beforeAll(() => {
|
|
61
|
-
if (testProjects.length === 0) {
|
|
62
|
-
console.warn(`No test projects found in ${TEST_PROJS_DIR}`);
|
|
63
|
-
console.warn("Create a 'test_projs' folder with .flp files to run tests");
|
|
64
|
-
}
|
|
65
|
-
});
|
|
66
|
-
describe("Basic Parsing", () => {
|
|
67
|
-
it.skipIf(testProjects.length === 0)("should parse FLP file without errors", () => {
|
|
68
|
-
const project = loadTestProject(testProjects[0]);
|
|
69
|
-
expect(project).toBeDefined();
|
|
70
|
-
expect(project.events).toBeDefined();
|
|
71
|
-
expect(project.events.length).toBeGreaterThan(0);
|
|
72
|
-
});
|
|
73
|
-
it.skipIf(testProjects.length === 0)("should detect FL version", () => {
|
|
74
|
-
const project = loadTestProject(testProjects[0]);
|
|
75
|
-
const version = getFlVersion(project);
|
|
76
|
-
expect(version).toBeDefined();
|
|
77
|
-
expect(version.length).toBeGreaterThan(0);
|
|
78
|
-
console.log(`Detected FL version: ${version}`);
|
|
79
|
-
});
|
|
80
|
-
it.skipIf(testProjects.length === 0)(`should be FL Studio version ${EXPECTED_FL_VERSION}.x`, () => {
|
|
81
|
-
const project = loadTestProject(testProjects[0]);
|
|
82
|
-
const version = getFlVersion(project);
|
|
83
|
-
expect(version.startsWith(EXPECTED_FL_VERSION)).toBe(true);
|
|
84
|
-
});
|
|
85
|
-
});
|
|
86
|
-
describe("Round-trip Stability", () => {
|
|
87
|
-
it.skipIf(testProjects.length === 0)("should produce identical output when parsing and re-serializing without changes", () => {
|
|
88
|
-
const filepath = path.join(TEST_PROJS_DIR, testProjects[0]);
|
|
89
|
-
const originalBuffer = fs.readFileSync(filepath);
|
|
90
|
-
const parsed = parseFlp(originalBuffer);
|
|
91
|
-
const reserialized = serializeFlp(parsed);
|
|
92
|
-
expect(reserialized.length).toBe(originalBuffer.length);
|
|
93
|
-
expect(reserialized.equals(originalBuffer)).toBe(true);
|
|
94
|
-
});
|
|
95
|
-
});
|
|
96
|
-
describe("Case 1: Basic metadata (title, artist, description, genre)", () => {
|
|
97
|
-
it.skipIf(testProjects.length === 0)("should read and write basic metadata", () => {
|
|
98
|
-
const project = loadTestProject(testProjects[0]);
|
|
99
|
-
const name = randomString(10);
|
|
100
|
-
const artist = randomString(10);
|
|
101
|
-
const description = randomString(100);
|
|
102
|
-
const genre = randomString(15);
|
|
103
|
-
const modified = writeProjectMeta(project, {
|
|
104
|
-
name: name,
|
|
105
|
-
artist: artist,
|
|
106
|
-
description: description,
|
|
107
|
-
genre: genre,
|
|
108
|
-
});
|
|
109
|
-
const meta = readProjectMeta(modified);
|
|
110
|
-
expect(meta.name).toBe(name);
|
|
111
|
-
expect(meta.artist).toBe(artist);
|
|
112
|
-
expect(meta.description).toContain(description);
|
|
113
|
-
expect(meta.genre).toBe(genre);
|
|
114
|
-
});
|
|
115
|
-
});
|
|
116
|
-
describe("Case 2: Metadata with genre and tempo", () => {
|
|
117
|
-
it.skipIf(testProjects.length === 0)("should read and write metadata with genre and tempo", () => {
|
|
118
|
-
const project = loadTestProject(testProjects[0]);
|
|
119
|
-
const name = randomString(10);
|
|
120
|
-
const artist = randomString(10);
|
|
121
|
-
const description = randomString(100);
|
|
122
|
-
const genre = randomString(15);
|
|
123
|
-
const bpm = randomBPM();
|
|
124
|
-
const modified = writeProjectMeta(project, {
|
|
125
|
-
name: name,
|
|
126
|
-
artist: artist,
|
|
127
|
-
description: description,
|
|
128
|
-
genre: genre,
|
|
129
|
-
bpm: bpm,
|
|
130
|
-
});
|
|
131
|
-
const meta = readProjectMeta(modified);
|
|
132
|
-
expect(meta.name).toBe(name);
|
|
133
|
-
expect(meta.artist).toBe(artist);
|
|
134
|
-
expect(meta.description).toContain(description);
|
|
135
|
-
expect(meta.genre).toBe(genre);
|
|
136
|
-
expect(meta.bpm).toBe(bpm);
|
|
137
|
-
});
|
|
138
|
-
});
|
|
139
|
-
describe("Case 3: Decimal tempo", () => {
|
|
140
|
-
it.skipIf(testProjects.length === 0)("should handle decimal tempo values", () => {
|
|
141
|
-
const project = loadTestProject(testProjects[0]);
|
|
142
|
-
const name = randomString(10);
|
|
143
|
-
const bpm = randomBPM();
|
|
144
|
-
const modified = writeProjectMeta(project, {
|
|
145
|
-
name: name,
|
|
146
|
-
bpm: bpm,
|
|
147
|
-
});
|
|
148
|
-
const meta = readProjectMeta(modified);
|
|
149
|
-
// BPM is stored as integer * 1000, so we get 3 decimal places precision
|
|
150
|
-
expect(meta.bpm).toBeCloseTo(bpm, 3);
|
|
151
|
-
});
|
|
152
|
-
});
|
|
153
|
-
describe("Case 4: Type coercion", () => {
|
|
154
|
-
it.skipIf(testProjects.length === 0)("should handle numeric values as strings", () => {
|
|
155
|
-
const project = loadTestProject(testProjects[0]);
|
|
156
|
-
const bpm = randomBPM();
|
|
157
|
-
// In TypeScript we enforce types, but we test that numeric strings work
|
|
158
|
-
const modified = writeProjectMeta(project, {
|
|
159
|
-
name: String(89465),
|
|
160
|
-
description: String(10231658784241),
|
|
161
|
-
bpm: bpm,
|
|
162
|
-
});
|
|
163
|
-
const meta = readProjectMeta(modified);
|
|
164
|
-
expect(meta.name).toBe("89465");
|
|
165
|
-
expect(meta.description).toBe("10231658784241");
|
|
166
|
-
expect(meta.bpm).toBeCloseTo(bpm, 3);
|
|
167
|
-
});
|
|
168
|
-
});
|
|
169
|
-
describe("Case 5: Samples", () => {
|
|
170
|
-
it.skipIf(testProjects.length === 0)("should list samples", () => {
|
|
171
|
-
const project = loadTestProject(testProjects[0]);
|
|
172
|
-
const samples = listSamples(project);
|
|
173
|
-
// Just verify we can list samples without error
|
|
174
|
-
expect(Array.isArray(samples)).toBe(true);
|
|
175
|
-
console.log(`Found ${samples.length} samples`);
|
|
176
|
-
if (samples.length > 0) {
|
|
177
|
-
expect(samples[0]).toHaveProperty("path");
|
|
178
|
-
expect(samples[0]).toHaveProperty("eventIndex");
|
|
179
|
-
}
|
|
180
|
-
});
|
|
181
|
-
it.skipIf(testProjects.length === 0)("should rewrite sample paths with random prefix", () => {
|
|
182
|
-
const project = loadTestProject(testProjects[0]);
|
|
183
|
-
const originalSamples = listSamples(project);
|
|
184
|
-
if (originalSamples.length > 0) {
|
|
185
|
-
const randomPrefix = randomString(8);
|
|
186
|
-
const modified = rewriteSamplePaths(project, (oldPath) => oldPath.replace("C:\\", `${randomPrefix}:\\`));
|
|
187
|
-
const newSamples = listSamples(modified);
|
|
188
|
-
expect(newSamples.length).toBe(originalSamples.length);
|
|
189
|
-
// Verify at least one path was modified
|
|
190
|
-
const modifiedPaths = newSamples.filter((s) => s.path.includes(randomPrefix));
|
|
191
|
-
if (originalSamples.some((s) => s.path.includes("C:\\"))) {
|
|
192
|
-
expect(modifiedPaths.length).toBeGreaterThan(0);
|
|
193
|
-
}
|
|
194
|
-
}
|
|
195
|
-
});
|
|
196
|
-
});
|
|
197
|
-
describe("Case 6: Unicode/Emoji support", () => {
|
|
198
|
-
it.skipIf(testProjects.length === 0)("should handle emojis in metadata", () => {
|
|
199
|
-
const project = loadTestProject(testProjects[0]);
|
|
200
|
-
const emoji1 = randomEmoji();
|
|
201
|
-
const emoji2 = randomEmoji();
|
|
202
|
-
const baseName = randomString(10);
|
|
203
|
-
const artist = randomString(10);
|
|
204
|
-
const description = randomString(50);
|
|
205
|
-
const nameWithEmoji = `${baseName} ${emoji1}${emoji2}`;
|
|
206
|
-
const descWithEmoji = `${description} ${emoji1}`;
|
|
207
|
-
const modified = writeProjectMeta(project, {
|
|
208
|
-
name: nameWithEmoji,
|
|
209
|
-
artist: artist,
|
|
210
|
-
description: descWithEmoji,
|
|
211
|
-
});
|
|
212
|
-
const meta = readProjectMeta(modified);
|
|
213
|
-
expect(meta.name).toBe(nameWithEmoji);
|
|
214
|
-
expect(meta.artist).toBe(artist);
|
|
215
|
-
expect(meta.description).toBe(descWithEmoji);
|
|
216
|
-
});
|
|
217
|
-
});
|
|
218
|
-
describe("Case 7: createdAt timestamp", () => {
|
|
219
|
-
it.skipIf(testProjects.length === 0)("should read and write creation date", () => {
|
|
220
|
-
const project = loadTestProject(testProjects[0]);
|
|
221
|
-
const testDate = randomTimestamp();
|
|
222
|
-
const modified = writeProjectTimeInfo(project, {
|
|
223
|
-
creationDate: testDate,
|
|
224
|
-
});
|
|
225
|
-
const timeInfo = readProjectTimeInfo(modified);
|
|
226
|
-
expect(timeInfo.creationDate).toBeDefined();
|
|
227
|
-
if (timeInfo.creationDate) {
|
|
228
|
-
// Allow 1 second tolerance due to float precision
|
|
229
|
-
const diff = Math.abs(timeInfo.creationDate.getTime() - testDate.getTime());
|
|
230
|
-
expect(diff).toBeLessThan(1000);
|
|
231
|
-
}
|
|
232
|
-
});
|
|
233
|
-
});
|
|
234
|
-
describe("Case 8: workTime = 0", () => {
|
|
235
|
-
it.skipIf(testProjects.length === 0)("should handle zero work time", () => {
|
|
236
|
-
const project = loadTestProject(testProjects[0]);
|
|
237
|
-
const modified = writeProjectTimeInfo(project, {
|
|
238
|
-
workTimeSeconds: 0,
|
|
239
|
-
});
|
|
240
|
-
const timeInfo = readProjectTimeInfo(modified);
|
|
241
|
-
expect(timeInfo.workTimeSeconds).toBe(0);
|
|
242
|
-
});
|
|
243
|
-
});
|
|
244
|
-
describe("Case 9: workTime with value", () => {
|
|
245
|
-
it.skipIf(testProjects.length === 0)("should read and write work time", () => {
|
|
246
|
-
const project = loadTestProject(testProjects[0]);
|
|
247
|
-
const workTime = randomWorkTime();
|
|
248
|
-
const modified = writeProjectTimeInfo(project, {
|
|
249
|
-
workTimeSeconds: workTime,
|
|
250
|
-
});
|
|
251
|
-
const timeInfo = readProjectTimeInfo(modified);
|
|
252
|
-
expect(timeInfo.workTimeSeconds).toBeCloseTo(workTime, 1);
|
|
253
|
-
});
|
|
254
|
-
});
|
|
255
|
-
describe("Plugins", () => {
|
|
256
|
-
it.skipIf(testProjects.length === 0)("should list plugins", () => {
|
|
257
|
-
const project = loadTestProject(testProjects[0]);
|
|
258
|
-
const plugins = listPlugins(project);
|
|
259
|
-
expect(Array.isArray(plugins)).toBe(true);
|
|
260
|
-
console.log(`Found ${plugins.length} plugins`);
|
|
261
|
-
for (const plugin of plugins) {
|
|
262
|
-
console.log(` - ${plugin.name} (${plugin.vendor ?? "N/A"})`);
|
|
263
|
-
}
|
|
264
|
-
});
|
|
265
|
-
});
|
|
266
|
-
describe("Combined metadata and time info", () => {
|
|
267
|
-
it.skipIf(testProjects.length === 0)("should handle all modifications together", () => {
|
|
268
|
-
const project = loadTestProject(testProjects[0]);
|
|
269
|
-
// Generate random test data
|
|
270
|
-
const emoji = randomEmoji();
|
|
271
|
-
const name = `${randomString(10)} ${emoji}`;
|
|
272
|
-
const artist = randomString(10);
|
|
273
|
-
const description = randomString(100);
|
|
274
|
-
const genre = randomString(15);
|
|
275
|
-
const bpm = randomDecimalBPM();
|
|
276
|
-
const testDate = randomTimestamp();
|
|
277
|
-
const workTime = randomWorkTime();
|
|
278
|
-
// Apply metadata changes
|
|
279
|
-
let modified = writeProjectMeta(project, {
|
|
280
|
-
name: name,
|
|
281
|
-
artist: artist,
|
|
282
|
-
description: description,
|
|
283
|
-
genre: genre,
|
|
284
|
-
bpm: bpm,
|
|
285
|
-
});
|
|
286
|
-
// Apply time info changes
|
|
287
|
-
modified = writeProjectTimeInfo(modified, {
|
|
288
|
-
creationDate: testDate,
|
|
289
|
-
workTimeSeconds: workTime,
|
|
290
|
-
});
|
|
291
|
-
// Verify all changes
|
|
292
|
-
const meta = readProjectMeta(modified);
|
|
293
|
-
expect(meta.name).toBe(name);
|
|
294
|
-
expect(meta.artist).toBe(artist);
|
|
295
|
-
expect(meta.description).toBe(description);
|
|
296
|
-
expect(meta.genre).toBe(genre);
|
|
297
|
-
expect(meta.bpm).toBeCloseTo(bpm, 3);
|
|
298
|
-
const timeInfo = readProjectTimeInfo(modified);
|
|
299
|
-
expect(timeInfo.workTimeSeconds).toBeCloseTo(workTime, 1);
|
|
300
|
-
// Verify round-trip works
|
|
301
|
-
const serialized = serializeFlp(modified);
|
|
302
|
-
const reparsed = parseFlp(serialized);
|
|
303
|
-
const reMeta = readProjectMeta(reparsed);
|
|
304
|
-
expect(reMeta.name).toBe(name);
|
|
305
|
-
expect(reMeta.artist).toBe(artist);
|
|
306
|
-
expect(reMeta.genre).toBe(genre);
|
|
307
|
-
expect(reMeta.bpm).toBeCloseTo(bpm, 3);
|
|
308
|
-
// Save the modified FLP to demo folder
|
|
309
|
-
const demoDir = path.join(TEST_PROJS_DIR, "demo");
|
|
310
|
-
if (!fs.existsSync(demoDir)) {
|
|
311
|
-
fs.mkdirSync(demoDir, { recursive: true });
|
|
312
|
-
}
|
|
313
|
-
const outputPath = path.join(demoDir, `modified_${testProjects[0]}`);
|
|
314
|
-
fs.writeFileSync(outputPath, serialized);
|
|
315
|
-
console.log(`Saved modified FLP to: ${outputPath}`);
|
|
316
|
-
});
|
|
317
|
-
});
|
|
318
|
-
});
|
|
319
|
-
// Run all tests on each available test project
|
|
320
|
-
describe.skipIf(getTestableProjects().length === 0)("All test projects", () => {
|
|
321
|
-
const projects = getTestableProjects();
|
|
322
|
-
for (const projectFile of projects) {
|
|
323
|
-
describe(`Project: ${projectFile}`, () => {
|
|
324
|
-
it("should parse without errors", () => {
|
|
325
|
-
const project = loadTestProject(projectFile);
|
|
326
|
-
expect(project).toBeDefined();
|
|
327
|
-
expect(project.events.length).toBeGreaterThan(0);
|
|
328
|
-
});
|
|
329
|
-
it("should round-trip without data loss", () => {
|
|
330
|
-
const filepath = path.join(TEST_PROJS_DIR, projectFile);
|
|
331
|
-
const original = fs.readFileSync(filepath);
|
|
332
|
-
const parsed = parseFlp(original);
|
|
333
|
-
const reserialized = serializeFlp(parsed);
|
|
334
|
-
expect(reserialized.equals(original)).toBe(true);
|
|
335
|
-
});
|
|
336
|
-
it("should read metadata", () => {
|
|
337
|
-
const project = loadTestProject(projectFile);
|
|
338
|
-
const meta = readProjectMeta(project);
|
|
339
|
-
expect(meta).toBeDefined();
|
|
340
|
-
console.log(` ${projectFile}: "${meta.name}" @ ${meta.bpm} BPM`);
|
|
341
|
-
});
|
|
342
|
-
});
|
|
343
|
-
}
|
|
344
|
-
});
|
|
345
|
-
//# sourceMappingURL=flparser.test.js.map
|