-
Notifications
You must be signed in to change notification settings - Fork 11
Expand file tree
/
Copy pathindex.js
More file actions
183 lines (162 loc) · 5.18 KB
/
index.js
File metadata and controls
183 lines (162 loc) · 5.18 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
const fs = require("fs");
const util = require("util");
const os = require("os");
const path = require("path");
const {
parse,
toSeratoString,
intToHexbin,
sanitizeFilename,
removeDriveRoot,
selectExternalRoot,
isFromExternalDrive,
} = require("./util");
// Singleton for Serato Folder Path (I doubt it'll change during runtime)
const PLATFORM_DEFAULT_SERATO_FOLDER = path.join(
os.homedir(),
"Music",
"_Serato_"
);
function getSubcratesFolder(seratoFolder) {
return path.join(seratoFolder, "SubCrates");
}
/**
* For each Serato Folder location, collect crates and returns a list
* of all of these.
*/
function listCratesSync(seratoFolders = [PLATFORM_DEFAULT_SERATO_FOLDER]) {
const allCrates = [];
seratoFolders.forEach((seratoFolder) => {
const subcratesFolder = getSubcratesFolder(seratoFolder);
const crates = fs.readdirSync(subcratesFolder).map((x) => {
const name = path.basename(x, ".crate");
return new Crate(name, seratoFolder);
});
allCrates.push(...crates);
});
return allCrates;
}
async function listCrates(seratoFolders = [PLATFORM_DEFAULT_SERATO_FOLDER]) {
const allCrates = [];
for (const seratoFolder of seratoFolders) {
const subcratesFolder = getSubcratesFolder(seratoFolder);
const files = await util.promisify(fs.readdir)(subcratesFolder);
const crates = files.map((x) => {
const name = path.basename(x, ".crate");
return new Crate(name, seratoFolder);
});
allCrates.push(...crates);
}
return allCrates;
}
class Crate {
/**
* Serato saves crates in all the drives from which songs
* in the crate come from. When you create a seratojs.Crate
* it assumes we are dealing with a Music-folder-main-drive crate.
*
* You can "fix" this crate to represent a particular crate in
* one particular Serato folder; in which case saving will use
* that location only. You are responsible for adding songs
* compatible with that drive. This is what we call 'location-aware'
* crates.
*/
constructor(name, seratoFolder) {
// TODO: Make private
this.name = sanitizeFilename(name);
this.filename = this.name + ".crate";
this.songPaths = [];
this.seratoFolder = seratoFolder; // To override for testing...
}
/**
* Returns the Serato directories where this will be saved.
*/
getSaveLocations() {
if (this.seratoFolder) {
return [this.seratoFolder]; // if specified at construction use this only.
}
if (this.songPaths.length === 0) {
return [PLATFORM_DEFAULT_SERATO_FOLDER];
}
const roots = new Set();
this.songPaths.forEach((songPath) => {
if (isFromExternalDrive(songPath)) {
const externalRoot = selectExternalRoot(songPath);
roots.add(path.join(externalRoot, "_Serato_"));
} else {
roots.add(PLATFORM_DEFAULT_SERATO_FOLDER);
}
});
return Array.from(roots);
}
// TODO: When reading, where should it read from?
async getSongPaths() {
const filepath = this._buildCrateFilepath(
this.seratoFolder || PLATFORM_DEFAULT_SERATO_FOLDER
);
const contents = await util.promisify(fs.readFile)(filepath, "ascii");
return parse(contents);
}
getSongPathsSync() {
const filepath = this._buildCrateFilepath(
this.seratoFolder || PLATFORM_DEFAULT_SERATO_FOLDER
);
const contents = fs.readFileSync(filepath, "ascii");
return parse(contents);
}
addSong(songPath) {
if (this.songPaths === null) {
this.songPaths = [];
}
const resolved = path.resolve(songPath);
this.songPaths.push(resolved);
}
_buildCrateFilepath(seratoFolder) {
const subcrateFolder = getSubcratesFolder(seratoFolder);
const filepath = path.join(subcrateFolder, this.filename);
return filepath;
}
_buildSaveBuffer() {
const header =
"vrsn 8 1 . 0 / S e r a t o S c r a t c h L i v e C r a t e".replace(
/ /g,
"\0"
);
let playlistSection = "";
if (this.songPaths) {
this.songPaths.forEach((songPath) => {
const absoluteSongPath = path.resolve(songPath);
const songPathWithoutDrive = removeDriveRoot(absoluteSongPath);
const data = toSeratoString(songPathWithoutDrive);
let ptrkSize = intToHexbin(data.length);
let otrkSize = intToHexbin(data.length + 8); // fixing the +8 (4 for 'ptrk', 4 for ptrkSize)
playlistSection += "otrk" + otrkSize + "ptrk" + ptrkSize + data;
});
}
const contents = header + playlistSection;
return Buffer.from(contents, "ascii");
}
async save() {
for (const seratoFolder of this.getSaveLocations()) {
const filepath = this._buildCrateFilepath(seratoFolder);
const buffer = this._buildSaveBuffer();
return util.promisify(fs.writeFile)(filepath, buffer, {
encoding: null,
});
}
}
saveSync() {
for (const seratoFolder of this.getSaveLocations()) {
const filepath = this._buildCrateFilepath(seratoFolder);
const buffer = this._buildSaveBuffer();
// Ensure folder exists
fs.writeFileSync(filepath, buffer, { encoding: null });
}
}
}
const seratojs = {
Crate: Crate,
listCratesSync: listCratesSync,
listCrates: listCrates,
};
module.exports = seratojs;