import crypto from "node:crypto"; import fs from "node:fs"; import path from "node:path"; import process from "node:process"; import pg from "pg"; import config from "../built/config/index.js"; const { Client } = pg; const marker = "karaoke-service-rights-safe-test-data"; const createdAt = new Date(); function genId(date = new Date()) { const time2000 = 946684800000; const timestamp = Math.max(date.getTime() - time2000, 0).toString(36).padStart(8, "0"); const random = crypto.randomBytes(8).toString("base64url").replace(/[^a-z0-9]/gi, "").toLowerCase().slice(0, 8).padEnd(8, "0"); return `${timestamp}${random}`; } function md5(buffer) { return crypto.createHash("md5").update(buffer).digest("hex"); } function saveInternalFile(name, buffer) { const accessKey = crypto.randomUUID(); fs.mkdirSync(config.mediaDir, { recursive: true }); fs.writeFileSync(path.join(config.mediaDir, accessKey), buffer); return { accessKey, url: `${config.url}/files/${accessKey}`, name, size: buffer.byteLength, md5: md5(buffer), }; } function createWav() { const sampleRate = 44100; const seconds = 16; const samples = sampleRate * seconds; const data = Buffer.alloc(samples * 2); const notes = [261.63, 293.66, 329.63, 392.0, 349.23, 329.63, 293.66, 261.63]; for (let i = 0; i < samples; i++) { const t = i / sampleRate; const beat = Math.floor(t / 2) % notes.length; const root = notes[beat] / 2; const value = Math.sin(2 * Math.PI * root * t) * 0.22 + Math.sin(2 * Math.PI * root * 1.5 * t) * 0.12 + Math.sin(2 * Math.PI * root * 2 * t) * 0.08; data.writeInt16LE(Math.round(Math.max(-1, Math.min(1, value)) * 0x7fff), i * 2); } const header = Buffer.alloc(44); header.write("RIFF", 0); header.writeUInt32LE(36 + data.length, 4); header.write("WAVE", 8); header.write("fmt ", 12); header.writeUInt32LE(16, 16); header.writeUInt16LE(1, 20); header.writeUInt16LE(1, 22); header.writeUInt32LE(sampleRate, 24); header.writeUInt32LE(sampleRate * 2, 28); header.writeUInt16LE(2, 32); header.writeUInt16LE(16, 34); header.write("data", 36); header.writeUInt32LE(data.length, 40); return Buffer.concat([header, data]); } function varLen(value) { let buffer = value & 0x7f; while ((value >>= 7) > 0) { buffer <<= 8; buffer |= (value & 0x7f) | 0x80; } const bytes = []; for (;;) { bytes.push(buffer & 0xff); if (buffer & 0x80) buffer >>= 8; else break; } return Buffer.from(bytes); } function createMidi() { const ticksPerQuarter = 480; const events = []; const push = (...bytes) => events.push(Buffer.from(bytes)); const pushDelta = (delta) => events.push(varLen(delta)); pushDelta(0); push(0xff, 0x51, 0x03, 0x07, 0xa1, 0x20); // 120 BPM pushDelta(0); push(0xc0, 0x00); const melody = [60, 62, 64, 67, 65, 64, 62, 60]; for (const note of melody) { pushDelta(0); push(0x90, note, 0x64); pushDelta(ticksPerQuarter * 2); push(0x80, note, 0x40); } pushDelta(0); push(0xff, 0x2f, 0x00); const track = Buffer.concat(events); const header = Buffer.alloc(14); header.write("MThd", 0); header.writeUInt32BE(6, 4); header.writeUInt16BE(0, 8); header.writeUInt16BE(1, 10); header.writeUInt16BE(ticksPerQuarter, 12); const trackHeader = Buffer.alloc(8); trackHeader.write("MTrk", 0); trackHeader.writeUInt32BE(track.length, 4); return Buffer.concat([header, trackHeader, track]); } function textBuffer(text) { return Buffer.from(text.replace(/\n/g, "\r\n"), "utf8"); } async function main() { const client = new Client({ host: config.db.host, port: config.db.port, user: config.db.user, password: config.db.pass, database: config.db.db, ...config.db.extra, }); await client.connect(); try { const admin = await client.query( `SELECT "id", "username", "host" FROM "user" WHERE "usernameLower" = 'admin' AND "host" IS NULL LIMIT 1`, ); if (admin.rowCount === 0) { throw new Error("Local @admin user was not found. Create @admin first, then rerun this script."); } const adminUser = admin.rows[0]; const existing = await client.query( `SELECT "id", "fileIds" FROM "note" WHERE "userId" = $1 AND "text" ILIKE $2 LIMIT 1`, [adminUser.id, `%${marker}%`], ); const assets = [ { name: "karaoke-test-accompaniment.wav", type: "audio/wav", comment: "KaraokeService test accompaniment. Original generated tone progression.", buffer: createWav(), }, { name: "karaoke-test-lyrics.lrc", type: "text/plain", comment: "KaraokeService timed lyrics.", buffer: textBuffer(`[00:00.000]Original karaoke test [00:02.000]Sing the first clear tone [00:04.000]Move to the next note [00:06.000]Hold the bright third [00:08.000]Reach the open fifth [00:10.000]Return with steady timing [00:12.000]Finish on the home note [00:14.000]This data is rights-safe`), }, { name: "karaoke-test-pitch.mid", type: "audio/midi", comment: "KaraokeService MIDI pitch and tempo data.", buffer: createMidi(), }, { name: "karaoke-test-score.json", type: "application/json", comment: "KaraokeService scoring metadata.", buffer: textBuffer(JSON.stringify({ version: 1, scoring: "midi-pitch", pitchFile: "karaoke-test-pitch.mid", toleranceCents: 50, partialToleranceCents: 100, extraSingingPenalty: true, }, null, 2)), }, { name: "karaoke-test-song-info.txt", type: "text/plain", comment: "KaraokeService song metadata.", buffer: textBuffer(`Title: Original Karaoke Test Artist: FrozenFriendsYume test data License: Public-domain equivalent test fixture generated for this repository BPM: 120 Key: C major Notes: All melody, lyrics, accompaniment, and metadata were generated locally for testing.`), }, ]; const files = assets.map((asset) => ({ id: genId(createdAt), createdAt, userId: adminUser.id, userHost: null, ...saveInternalFile(asset.name, asset.buffer), type: asset.type, comment: asset.comment, })); await client.query("BEGIN"); for (const file of files) { await client.query( `INSERT INTO "drive_file" ( "id", "createdAt", "userId", "userHost", "md5", "name", "type", "size", "comment", "blurhash", "properties", "storedInternal", "url", "thumbnailUrl", "webpublicUrl", "webpublicType", "accessKey", "thumbnailAccessKey", "webpublicAccessKey", "uri", "src", "folderId", "isSensitive", "allowDownload", "isLink", "requestHeaders", "requestIp" ) VALUES ( $1, $2, $3, $4, $5, $6, $7, $8, $9, NULL, '{}', TRUE, $10, NULL, NULL, NULL, $11, NULL, NULL, NULL, NULL, NULL, FALSE, FALSE, FALSE, NULL, NULL )`, [ file.id, file.createdAt, file.userId, file.userHost, file.md5, file.name, file.type, file.size, file.comment, file.url, file.accessKey, ], ); } const noteText = `Original Karaoke Test Rights-safe generated karaoke test data. Marker: ${marker} #KaraokeService`; if (existing.rowCount > 0) { const noteId = existing.rows[0].id; const oldFileIds = existing.rows[0].fileIds ?? []; const oldFiles = oldFileIds.length > 0 ? await client.query(`SELECT "id", "accessKey" FROM "drive_file" WHERE "id" = ANY($1)`, [oldFileIds]) : { rows: [] }; await client.query( `UPDATE "note" SET "text" = $2, "fileIds" = $3, "attachedFileTypes" = $4, "updatedAt" = $5 WHERE "id" = $1`, [noteId, noteText, files.map((file) => file.id), files.map((file) => file.type), new Date()], ); if (oldFileIds.length > 0) { await client.query(`DELETE FROM "drive_file" WHERE "id" = ANY($1)`, [oldFileIds]); for (const file of oldFiles.rows) { if (!file.accessKey) continue; fs.rmSync(path.join(config.mediaDir, file.accessKey), { force: true }); } } await client.query("COMMIT"); console.log(`Recreated KaraokeService test media for existing @admin note: ${noteId}`); return; } const noteId = genId(createdAt); await client.query( `INSERT INTO "note" ( "id", "createdAt", "replyId", "renoteId", "threadId", "text", "name", "cw", "userId", "groupId", "localOnly", "renoteCount", "repliesCount", "viewCount", "reactions", "visibility", "uri", "url", "score", "fileIds", "attachedFileTypes", "visibleUserIds", "mentions", "mentionedRemoteUsers", "emojis", "tags", "hasPoll", "channelId", "quoteAuthorization", "canQuote", "userHost", "replyUserId", "replyUserHost", "renoteUserId", "renoteUserHost", "updatedAt" ) VALUES ( $1, $2, NULL, NULL, NULL, $3, NULL, NULL, $4, NULL, FALSE, 0, 0, 0, '{}', 'public', NULL, NULL, 0, $5, $6, '{}', '{}', '[]', '{}', $7, FALSE, NULL, NULL, TRUE, NULL, NULL, NULL, NULL, NULL, NULL )`, [ noteId, createdAt, noteText, adminUser.id, files.map((file) => file.id), files.map((file) => file.type), ["karaokeservice"], ], ); await client.query(`UPDATE "user" SET "notesCount" = "notesCount" + 1 WHERE "id" = $1`, [adminUser.id]); await client.query("COMMIT"); console.log(`Created KaraokeService test note as @admin: ${noteId}`); } catch (err) { await client.query("ROLLBACK").catch(() => undefined); throw err; } finally { await client.end(); } } main().catch((err) => { console.error(err); process.exit(1); });