UNPKG

@eidoriantan/seratolibraryparser

Version:

Helps parsing serato DJ libraries for node.js based applications.

379 lines (378 loc) 18 kB
"use strict"; var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); }; var __generator = (this && this.__generator) || function (thisArg, body) { var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g; return g = { next: verb(0), "throw": verb(1), "return": verb(2) }, typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; function verb(n) { return function (v) { return step([n, v]); }; } function step(op) { if (f) throw new TypeError("Generator is already executing."); while (_) try { if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; if (y = 0, t) op = [op[0] & 2, t.value]; switch (op[0]) { case 0: case 1: t = op; break; case 4: _.label++; return { value: op[1], done: false }; case 5: _.label++; y = op[1]; op = [0]; continue; case 7: op = _.ops.pop(); _.trys.pop(); continue; default: if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } if (t[2]) _.ops.pop(); _.trys.pop(); continue; } op = body.call(thisArg, _); } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; } }; Object.defineProperty(exports, "__esModule", { value: true }); exports.getDefaultSeratoPath = exports.getSeratoHistory = exports.getSeratoSongs = exports.getSessionSongs = exports.getSessions = exports.getDomTree = exports.getStringFromUInt32 = exports.getUInt32FromString = void 0; var fs = require("fs"); var os = require("os"); var pathLib = require("path"); /** * Datastructure for saving Dom Objects */ var Chunk = /** @class */ (function () { function Chunk(length, tag, data) { this.length = 0; this.length = length; this.tag = tag; this.data = data; } return Chunk; }()); /** * Converts a 4 byte string into a integer * @param {string} s 4 byte string to be converted */ function getUInt32FromString(s) { return ((s.charCodeAt(0) << 24) + (s.charCodeAt(1) << 16) + (s.charCodeAt(2) << 8) + s.charCodeAt(3)); } exports.getUInt32FromString = getUInt32FromString; /** * Converts a 4 byte integer into a string * @param {number} n 4 byte integer */ function getStringFromUInt32(n) { return (String.fromCharCode(Math.floor(n / (1 << 24)) % 256) + String.fromCharCode(Math.floor(n / (1 << 16)) % 256) + String.fromCharCode(Math.floor(n / (1 << 8)) % 256) + String.fromCharCode(Math.floor(n) % 256)); } exports.getStringFromUInt32 = getStringFromUInt32; /** * Returns a single buffer and fills in data tag recursivly * @param {Buffer} buffer A node.js fs buffer to read from * @param {number} index index of first byte * @returns {Promise<{ chunk: Chunk; newIndex: number }>} Promise with object for destructured assignment. New Index is the index of the following chunk */ function parseChunk(buffer, index) { return __awaiter(this, void 0, void 0, function () { var tag, length, data, _a, secondsSince1970, le, i, a, b; return __generator(this, function (_b) { switch (_b.label) { case 0: tag = getStringFromUInt32(buffer.readUInt32BE(index)); length = buffer.readUInt32BE(index + 4); _a = tag; switch (_a) { case "oses": return [3 /*break*/, 1]; case "oent": return [3 /*break*/, 1]; case "otrk": return [3 /*break*/, 1]; case "adat": return [3 /*break*/, 1]; case "\u0000\u0000\u0000\u0001": return [3 /*break*/, 3]; case "\u0000\u0000\u0000\u000f": return [3 /*break*/, 3]; case "\u0000\u0000\u00005": return [3 /*break*/, 4]; } return [3 /*break*/, 5]; case 1: return [4 /*yield*/, parseChunkArray(buffer, index + 8, index + 8 + length)]; case 2: data = _b.sent(); return [3 /*break*/, 6]; case 3: data = buffer.readUInt32BE(index + 8); return [3 /*break*/, 6]; case 4: secondsSince1970 = buffer.readUInt32BE(index + 8); data = new Date(0); data.setUTCSeconds(secondsSince1970); return [3 /*break*/, 6]; case 5: le = buffer.subarray(index + 8, index + 8 + length); for (i = 0; i < le.byteLength; i += 2) { a = le[i]; b = le[i + 1]; le[i] = b; le[i + 1] = a; } data = le.toString("utf-16le"); return [3 /*break*/, 6]; case 6: return [2 /*return*/, { chunk: new Chunk(length, tag, data), newIndex: index + length + 8 }]; } }); }); } /** * Reads in a ongoing list of serato chunks till the maximum length is reached * @param {Buffer} buffer A node.js fs buffer to read from * @param {number} start Index of the first byte of the chunk * @param {number} end Maximum length of the array data * @returns {Promise<Chunk[]>} Array of chunks read in */ function parseChunkArray(buffer, start, end) { return __awaiter(this, void 0, void 0, function () { var chunks, cursor, _a, chunk, newIndex; return __generator(this, function (_b) { switch (_b.label) { case 0: chunks = []; cursor = start; _b.label = 1; case 1: if (!(cursor < end)) return [3 /*break*/, 3]; return [4 /*yield*/, parseChunk(buffer, cursor)]; case 2: _a = _b.sent(), chunk = _a.chunk, newIndex = _a.newIndex; cursor = newIndex; chunks.push(chunk); return [3 /*break*/, 1]; case 3: return [2 /*return*/, chunks]; } }); }); } /** * Returns the raw domtree of a serato file * @param {string} path The path to the file that shoud be parsed * @returns {Promise<Chunk[]>} Nested object dom */ function getDomTree(path) { return __awaiter(this, void 0, void 0, function () { var buffer, chunks; return __generator(this, function (_a) { switch (_a.label) { case 0: return [4 /*yield*/, fs.promises.readFile(path)]; case 1: buffer = _a.sent(); return [4 /*yield*/, parseChunkArray(buffer, 0, buffer.length)]; case 2: chunks = _a.sent(); return [2 /*return*/, chunks]; } }); }); } exports.getDomTree = getDomTree; /** * Reads in a history.databases file * @param {string} path Path to the history.database file * @returns {Promise<{ [Key: string]: number }>} A dictonary with the number of the session file for every date */ function getSessions(path) { return __awaiter(this, void 0, void 0, function () { var buffer, chunks, sessions; return __generator(this, function (_a) { switch (_a.label) { case 0: return [4 /*yield*/, fs.promises.readFile(path)]; case 1: buffer = _a.sent(); return [4 /*yield*/, parseChunkArray(buffer, 0, buffer.length)]; case 2: chunks = _a.sent(); sessions = {}; chunks.forEach(function (chunk) { if (chunk.tag === "oses") { if (Array.isArray(chunk.data)) { if (chunk.data[0].tag === "adat") { if (Array.isArray(chunk.data[0].data)) { var date_1 = ""; var index_1 = -1; chunk.data[0].data.forEach(function (subChunk) { if (subChunk.tag === "\u0000\u0000\u0000\u0001") { index_1 = subChunk.data; } if (subChunk.tag === "\u0000\u0000\u0000)") { date_1 = subChunk.data; } }); sessions[date_1] = index_1; } } } } }); return [2 /*return*/, sessions]; } }); }); } exports.getSessions = getSessions; /** * Reads in a serato session file. * @param {string} path Path to *.session file * @returns {Promise<SessionSong[]>} An array containing title and artist for every song played */ function getSessionSongs(path) { return __awaiter(this, void 0, void 0, function () { var buffer, chunks, songs; return __generator(this, function (_a) { switch (_a.label) { case 0: return [4 /*yield*/, fs.promises.readFile(path)]; case 1: buffer = _a.sent(); return [4 /*yield*/, parseChunkArray(buffer, 0, buffer.length)]; case 2: chunks = _a.sent(); songs = []; chunks.forEach(function (chunk) { if (chunk.tag === "oent") { if (Array.isArray(chunk.data)) { if (chunk.data[0].tag === "adat") { if (Array.isArray(chunk.data[0].data)) { var title_1 = ""; var artist_1 = ""; var bpm_1; var filePath_1 = ""; var timePlayed_1 = new Date(); chunk.data[0].data.forEach(function (subChunk) { if (subChunk.tag === "\u0000\u0000\u0000\u0006") { title_1 = subChunk.data; } if (subChunk.tag === "\u0000\u0000\u0000\u0007") { artist_1 = subChunk.data; } if (subChunk.tag === "\u0000\u0000\u0000\u000f") { bpm_1 = subChunk.data; } if (subChunk.tag === "pfil") { filePath_1 = subChunk.data; } if (subChunk.tag === "\u0000\u0000\u00005") { timePlayed_1 = subChunk.data; } }); // console.log(chunk.data[0].data); // For Development songs.push({ title: title_1, artist: artist_1, bpm: bpm_1, filePath: filePath_1, timePlayed: timePlayed_1 }); } } } } }); return [2 /*return*/, songs]; } }); }); } exports.getSessionSongs = getSessionSongs; /** * Gets all songs of the database v2 serato file * @param {string} path path to database v2 serato file */ function getSeratoSongs(path) { return __awaiter(this, void 0, void 0, function () { var buffer, chunks, songs; return __generator(this, function (_a) { switch (_a.label) { case 0: return [4 /*yield*/, fs.promises.readFile(path)]; case 1: buffer = _a.sent(); return [4 /*yield*/, parseChunkArray(buffer, 0, buffer.length)]; case 2: chunks = _a.sent(); songs = []; chunks.forEach(function (chunk) { if (chunk.tag === "otrk") { if (Array.isArray(chunk.data)) { var title_2 = ""; var artist_2 = ""; var bpm_2; var filePath_2 = ""; chunk.data.forEach(function (subChunk) { if (subChunk.tag === "tsng") { title_2 = subChunk.data; } if (subChunk.tag === "tart") { artist_2 = subChunk.data; } if (subChunk.tag === "tbpm") { bpm_2 = subChunk.data; } if (subChunk.tag === "pfil") { filePath_2 = subChunk.data; } }); songs.push({ title: title_2, artist: artist_2, bpm: bpm_2, filePath: filePath_2 }); } } }); return [2 /*return*/, songs]; } }); }); } exports.getSeratoSongs = getSeratoSongs; /** * Reads all sessions and played songs from the _Serato_ folder * @param {string} seratoPath path to _Serato_ folder (including _Serato_) * @returns {Promise<Session[]>} list of sessions including songs */ function getSeratoHistory(seratoPath) { return __awaiter(this, void 0, void 0, function () { var sessions, result, _a, _b, _i, key, session, songlist; return __generator(this, function (_c) { switch (_c.label) { case 0: return [4 /*yield*/, getSessions(pathLib.join(seratoPath, 'History/history.database'))]; case 1: sessions = _c.sent(); result = []; _a = []; for (_b in sessions) _a.push(_b); _i = 0; _c.label = 2; case 2: if (!(_i < _a.length)) return [3 /*break*/, 5]; key = _a[_i]; if (!sessions.hasOwnProperty(key)) return [3 /*break*/, 4]; session = sessions[key]; return [4 /*yield*/, getSessionSongs(pathLib.join(seratoPath, 'History/Sessions/', session + '.session'))]; case 3: songlist = _c.sent(); result.push({ date: key, songs: songlist }); _c.label = 4; case 4: _i++; return [3 /*break*/, 2]; case 5: return [2 /*return*/, result]; } }); }); } exports.getSeratoHistory = getSeratoHistory; /** * Returns the default path to the _serato_ folder of the user * @returns {string} path to _serato_ folder */ function getDefaultSeratoPath() { return pathLib.join(os.homedir(), 'Music/_Serato_/'); } exports.getDefaultSeratoPath = getDefaultSeratoPath; // getSessionSongs('/Users/tobiasjacob/Music/_Serato_/History/Sessions/12.session'); // for testing // getSessions('/Users/tobiasjacob/Music/_Serato_/History/history.database'); // for testing