@eidoriantan/seratolibraryparser
Version:
Helps parsing serato DJ libraries for node.js based applications.
379 lines (378 loc) • 18 kB
JavaScript
"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