mirror of
https://github.com/advplyr/audiobookshelf.git
synced 2024-12-20 19:06:06 +01:00
609 lines
17 KiB
JavaScript
609 lines
17 KiB
JavaScript
"use strict";
|
|
|
|
const {
|
|
convertSize,
|
|
max,
|
|
min
|
|
} = require("./utils");
|
|
|
|
const Randomizer = (data, replacement) => {
|
|
var mutable = [...data];
|
|
if (replacement === undefined || typeof replacement !== "boolean") replacement = true;
|
|
|
|
function _next() {
|
|
var selection;
|
|
const index = Math.floor(Math.random() * mutable.length);
|
|
|
|
if (replacement) {
|
|
selection = mutable.slice(index, index + 1)[0];
|
|
} else {
|
|
selection = mutable.splice(index, 1)[0];
|
|
if (mutable.length === 0) mutable = [...data];
|
|
}
|
|
|
|
return selection;
|
|
}
|
|
|
|
return {
|
|
next: _next
|
|
};
|
|
};
|
|
|
|
const Result = (type) => {
|
|
var _result;
|
|
|
|
switch (type) {
|
|
case "stats":
|
|
_result = {
|
|
size: 0,
|
|
lines: 0,
|
|
records: 0,
|
|
errors: [],
|
|
blanks: 0,
|
|
created: undefined,
|
|
modified: undefined,
|
|
start: Date.now(),
|
|
end: undefined,
|
|
elapsed: 0
|
|
};
|
|
break;
|
|
case "distribute":
|
|
_result = {
|
|
stores: undefined,
|
|
records: 0,
|
|
errors: [],
|
|
start: Date.now(),
|
|
end: undefined,
|
|
elapsed: undefined
|
|
};
|
|
break;
|
|
case "insert":
|
|
_result = {
|
|
inserted: 0,
|
|
start: Date.now(),
|
|
end: undefined,
|
|
elapsed: 0
|
|
};
|
|
break;
|
|
case "insertFile":
|
|
_result = {
|
|
lines: 0,
|
|
inserted: 0,
|
|
errors: [],
|
|
blanks: 0,
|
|
start: Date.now(),
|
|
end: undefined
|
|
};
|
|
break;
|
|
case "select":
|
|
_result = {
|
|
lines: 0,
|
|
selected: 0,
|
|
ignored: 0,
|
|
errors: [],
|
|
blanks: 0,
|
|
start: Date.now(),
|
|
end: undefined,
|
|
elapsed: 0,
|
|
data: [],
|
|
};
|
|
break;
|
|
case "update":
|
|
_result = {
|
|
lines: 0,
|
|
selected: 0,
|
|
updated: 0,
|
|
unchanged: 0,
|
|
errors: [],
|
|
blanks: 0,
|
|
start: Date.now(),
|
|
end: undefined,
|
|
elapsed: 0,
|
|
data: [],
|
|
records: []
|
|
};
|
|
break;
|
|
case "delete":
|
|
_result = {
|
|
lines: 0,
|
|
deleted: 0,
|
|
retained: 0,
|
|
errors: [],
|
|
blanks: 0,
|
|
start: Date.now(),
|
|
end: undefined,
|
|
elapsed: 0,
|
|
data: [],
|
|
records: []
|
|
};
|
|
break;
|
|
case "aggregate":
|
|
_result = {
|
|
lines: 0,
|
|
aggregates: {},
|
|
indexed: 0,
|
|
unindexed: 0,
|
|
errors: [],
|
|
blanks: 0,
|
|
start: Date.now(),
|
|
end: undefined,
|
|
elapsed: 0
|
|
};
|
|
break;
|
|
}
|
|
|
|
return _result;
|
|
}
|
|
|
|
const Reduce = (type) => {
|
|
var _reduce;
|
|
|
|
switch (type) {
|
|
case "stats":
|
|
_reduce = Object.assign(Result("stats"), {
|
|
stores: 0,
|
|
min: undefined,
|
|
max: undefined,
|
|
mean: undefined,
|
|
var: undefined,
|
|
std: undefined,
|
|
m2: 0
|
|
});
|
|
break;
|
|
case "drop":
|
|
_reduce = {
|
|
dropped: false,
|
|
start: Date.now(),
|
|
end: 0,
|
|
elapsed: 0
|
|
};
|
|
break;
|
|
case "aggregate":
|
|
_reduce = Object.assign(Result("aggregate"), {
|
|
data: []
|
|
});
|
|
break;
|
|
default:
|
|
_reduce = Result(type);
|
|
break;
|
|
}
|
|
|
|
_reduce.details = undefined;
|
|
|
|
return _reduce;
|
|
};
|
|
|
|
const Handler = (type, ...functions) => {
|
|
var _results = Result(type);
|
|
|
|
const _next = (record, writer) => {
|
|
record = new Record(record);
|
|
_results.lines++;
|
|
|
|
if (record.length === 0) {
|
|
_results.blanks++;
|
|
} else {
|
|
if (record.data) {
|
|
switch (type) {
|
|
case "stats":
|
|
statsHandler(record, _results);
|
|
break;
|
|
case "select":
|
|
selectHandler(record, functions[0], functions[1], _results);
|
|
break;
|
|
case "update":
|
|
updateHandler(record, functions[0], functions[1], writer, _results);
|
|
break;
|
|
case "delete":
|
|
deleteHandler(record, functions[0], writer, _results);
|
|
break;
|
|
case "aggregate":
|
|
aggregateHandler(record, functions[0], functions[1], functions[2], _results);
|
|
break;
|
|
}
|
|
} else {
|
|
_results.errors.push({ error: record.error, line: _results.lines, data: record.source });
|
|
|
|
if (type === "update" || type === "delete") {
|
|
if (writer) {
|
|
writer.write(record.source + "\n");
|
|
} else {
|
|
_results.data.push(record.source);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
const _return = () => {
|
|
_results.end = Date.now();
|
|
_results.elapsed = _results.end - _results.start;
|
|
return _results;
|
|
}
|
|
|
|
return {
|
|
next: _next,
|
|
return: _return
|
|
};
|
|
};
|
|
|
|
const statsHandler = (record, results) => {
|
|
results.records++;
|
|
return results;
|
|
};
|
|
|
|
const selectHandler = (record, selecter, projecter, results) => {
|
|
if (record.select(selecter)) {
|
|
if (projecter) {
|
|
results.data.push(record.project(projecter));
|
|
} else {
|
|
results.data.push(record.data);
|
|
}
|
|
results.selected++;
|
|
} else {
|
|
results.ignored++;
|
|
}
|
|
};
|
|
|
|
const updateHandler = (record, selecter, updater, writer, results) => {
|
|
if (record.select(selecter)) {
|
|
results.selected++;
|
|
if (record.update(updater)) {
|
|
results.updated++;
|
|
results.records.push(record.data);
|
|
} else {
|
|
results.unchanged++;
|
|
}
|
|
} else {
|
|
results.unchanged++;
|
|
}
|
|
|
|
if (writer) {
|
|
writer.write(JSON.stringify(record.data) + "\n");
|
|
} else {
|
|
results.data.push(JSON.stringify(record.data));
|
|
}
|
|
};
|
|
|
|
const deleteHandler = (record, selecter, writer, results) => {
|
|
if (record.select(selecter)) {
|
|
results.deleted++;
|
|
results.records.push(record.data);
|
|
} else {
|
|
results.retained++;
|
|
|
|
if (writer) {
|
|
writer.write(JSON.stringify(record.data) + "\n");
|
|
} else {
|
|
results.data.push(JSON.stringify(record.data));
|
|
}
|
|
}
|
|
};
|
|
|
|
const aggregateHandler = (record, selecter, indexer, projecter, results) => {
|
|
if (record.select(selecter)) {
|
|
const index = record.index(indexer);
|
|
|
|
if (!index) {
|
|
results.unindexed++;
|
|
} else {
|
|
var projection;
|
|
var fields;
|
|
|
|
if (results.aggregates[index]) {
|
|
results.aggregates[index].count++;
|
|
} else {
|
|
results.aggregates[index] = {
|
|
count: 1,
|
|
aggregates: {}
|
|
};
|
|
}
|
|
|
|
if (projecter) {
|
|
projection = record.project(projecter);
|
|
fields = Object.keys(projection);
|
|
} else {
|
|
projection = record.data;
|
|
fields = Object.keys(record.data);
|
|
}
|
|
|
|
for (const field of fields) {
|
|
if (projection[field] !== undefined) {
|
|
if (results.aggregates[index].aggregates[field]) {
|
|
accumulateAggregate(results.aggregates[index].aggregates[field], projection[field]);
|
|
} else {
|
|
results.aggregates[index].aggregates[field] = {
|
|
min: projection[field],
|
|
max: projection[field],
|
|
count: 1
|
|
};
|
|
if (typeof projection[field] === "number") {
|
|
results.aggregates[index].aggregates[field]["sum"] = projection[field];
|
|
results.aggregates[index].aggregates[field]["mean"] = projection[field];
|
|
results.aggregates[index].aggregates[field]["m2"] = 0;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
results.indexed++;
|
|
}
|
|
}
|
|
}
|
|
|
|
const accumulateAggregate = (index, projection) => {
|
|
index["min"] = min(index["min"], projection);
|
|
index["max"] = max(index["max"], projection);
|
|
index["count"]++;
|
|
|
|
// Welford's algorithm
|
|
if (typeof projection === "number") {
|
|
const delta1 = projection - index["mean"];
|
|
index["sum"] += projection;
|
|
index["mean"] += delta1 / index["count"];
|
|
const delta2 = projection - index["mean"];
|
|
index["m2"] += delta1 * delta2;
|
|
}
|
|
|
|
return index;
|
|
};
|
|
|
|
class Record {
|
|
constructor(record) {
|
|
this.source = record.trim();
|
|
this.length = this.source.length
|
|
this.data = {};
|
|
this.error = "";
|
|
|
|
try {
|
|
this.data = JSON.parse(this.source)
|
|
} catch (e) {
|
|
this.data = undefined;
|
|
this.error = e.message;
|
|
}
|
|
}
|
|
}
|
|
|
|
Record.prototype.select = function (selecter) {
|
|
var result;
|
|
|
|
try {
|
|
result = selecter(this.data);
|
|
} catch {
|
|
return false;
|
|
}
|
|
|
|
if (typeof result !== "boolean") {
|
|
throw new TypeError("Selecter must return a boolean");
|
|
} else {
|
|
return result;
|
|
}
|
|
};
|
|
|
|
Record.prototype.update = function (updater) {
|
|
var result;
|
|
|
|
try {
|
|
result = updater(this.data);
|
|
} catch {
|
|
return false;
|
|
}
|
|
|
|
if (typeof result !== "object") {
|
|
throw new TypeError("Updater must return an object");
|
|
} else {
|
|
this.data = result;
|
|
return true;
|
|
}
|
|
}
|
|
|
|
Record.prototype.project = function (projecter) {
|
|
var result;
|
|
|
|
try {
|
|
result = projecter(this.data);
|
|
} catch {
|
|
return undefined;
|
|
}
|
|
|
|
if (Array.isArray(result) || typeof result !== "object") {
|
|
throw new TypeError("Projecter must return an object");
|
|
} else {
|
|
return result;
|
|
}
|
|
};
|
|
|
|
Record.prototype.index = function (indexer) {
|
|
try {
|
|
return indexer(this.data);
|
|
} catch {
|
|
return undefined;
|
|
}
|
|
};
|
|
|
|
const Reducer = (type, results) => {
|
|
var _reduce = Reduce(type);
|
|
|
|
var i = 0;
|
|
var aggregates = {};
|
|
|
|
for (const result of results) {
|
|
switch (type) {
|
|
case "stats":
|
|
statsReducer(_reduce, result, i);
|
|
break;
|
|
case "insert":
|
|
insertReducer(_reduce, result);
|
|
break;
|
|
case "select":
|
|
selectReducer(_reduce, result);
|
|
break;
|
|
case "update":
|
|
updateReducer(_reduce, result);
|
|
break;
|
|
case "delete":
|
|
deleteReducer(_reduce, result);
|
|
break;
|
|
case "aggregate":
|
|
aggregateReducer(_reduce, result, aggregates);
|
|
break
|
|
}
|
|
|
|
if (type === "stats") {
|
|
_reduce.stores++;
|
|
i++;
|
|
}
|
|
|
|
if (type === "drop") {
|
|
_reduce.dropped = true;
|
|
} else if (type !== "insert") {
|
|
_reduce.lines += result.lines;
|
|
_reduce.errors = _reduce.errors.concat(result.errors);
|
|
_reduce.blanks += result.blanks;
|
|
}
|
|
|
|
_reduce.start = min(_reduce.start, result.start);
|
|
_reduce.end = max(_reduce.end, result.end);
|
|
}
|
|
|
|
if (type === "stats") {
|
|
_reduce.size = convertSize(_reduce.size);
|
|
_reduce.var = _reduce.m2 / (results.length);
|
|
_reduce.std = Math.sqrt(_reduce.m2 / (results.length));
|
|
delete _reduce.m2;
|
|
} else if (type === "aggregate") {
|
|
for (const index of Object.keys(aggregates)) {
|
|
var aggregate = {
|
|
index: index,
|
|
count: aggregates[index].count,
|
|
aggregates: []
|
|
};
|
|
for (const field of Object.keys(aggregates[index].aggregates)) {
|
|
delete aggregates[index].aggregates[field].m2;
|
|
aggregate.aggregates.push({ field: field, data: aggregates[index].aggregates[field] });
|
|
}
|
|
_reduce.data.push(aggregate);
|
|
}
|
|
delete _reduce.aggregates;
|
|
}
|
|
|
|
_reduce.elapsed = _reduce.end - _reduce.start;
|
|
_reduce.details = results;
|
|
|
|
return _reduce;
|
|
};
|
|
|
|
const statsReducer = (reduce, result, i) => {
|
|
reduce.size += result.size;
|
|
reduce.records += result.records;
|
|
reduce.min = min(reduce.min, result.records);
|
|
reduce.max = max(reduce.max, result.records);
|
|
if (reduce.mean === undefined) reduce.mean = result.records;
|
|
const delta1 = result.records - reduce.mean;
|
|
reduce.mean += delta1 / (i + 2);
|
|
const delta2 = result.records - reduce.mean;
|
|
reduce.m2 += delta1 * delta2;
|
|
reduce.created = min(reduce.created, result.created);
|
|
reduce.modified = max(reduce.modified, result.modified);
|
|
};
|
|
|
|
const insertReducer = (reduce, result) => {
|
|
reduce.inserted += result.inserted;
|
|
};
|
|
|
|
const selectReducer = (reduce, result) => {
|
|
reduce.selected += result.selected;
|
|
reduce.ignored += result.ignored;
|
|
reduce.data = reduce.data.concat(result.data);
|
|
delete result.data;
|
|
};
|
|
|
|
const updateReducer = (reduce, result) => {
|
|
reduce.selected += result.selected;
|
|
reduce.updated += result.updated;
|
|
reduce.unchanged += result.unchanged;
|
|
};
|
|
|
|
const deleteReducer = (reduce, result) => {
|
|
reduce.deleted += result.deleted;
|
|
reduce.retained += result.retained;
|
|
};
|
|
|
|
const aggregateReducer = (reduce, result, aggregates) => {
|
|
reduce.indexed += result.indexed;
|
|
reduce.unindexed += result.unindexed;
|
|
|
|
const indexes = Object.keys(result.aggregates);
|
|
|
|
for (const index of indexes) {
|
|
if (aggregates[index]) {
|
|
aggregates[index].count += result.aggregates[index].count;
|
|
} else {
|
|
aggregates[index] = {
|
|
count: result.aggregates[index].count,
|
|
aggregates: {}
|
|
};
|
|
}
|
|
|
|
const fields = Object.keys(result.aggregates[index].aggregates);
|
|
|
|
for (const field of fields) {
|
|
const aggregateObject = aggregates[index].aggregates[field];
|
|
const resultObject = result.aggregates[index].aggregates[field];
|
|
|
|
if (aggregateObject) {
|
|
reduceAggregate(aggregateObject, resultObject);
|
|
} else {
|
|
aggregates[index].aggregates[field] = {
|
|
min: resultObject["min"],
|
|
max: resultObject["max"],
|
|
count: resultObject["count"]
|
|
};
|
|
|
|
if (resultObject["m2"] !== undefined) {
|
|
aggregates[index].aggregates[field]["sum"] = resultObject["sum"];
|
|
aggregates[index].aggregates[field]["mean"] = resultObject["mean"];
|
|
aggregates[index].aggregates[field]["varp"] = resultObject["m2"] / resultObject["count"];
|
|
aggregates[index].aggregates[field]["vars"] = resultObject["m2"] / (resultObject["count"] - 1);
|
|
aggregates[index].aggregates[field]["stdp"] = Math.sqrt(resultObject["m2"] / resultObject["count"]);
|
|
aggregates[index].aggregates[field]["stds"] = Math.sqrt(resultObject["m2"] / (resultObject["count"] - 1));
|
|
aggregates[index].aggregates[field]["m2"] = resultObject["m2"];
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
delete result.aggregates;
|
|
};
|
|
|
|
const reduceAggregate = (aggregate, result) => {
|
|
const n = aggregate["count"] + result["count"];
|
|
|
|
aggregate["min"] = min(aggregate["min"], result["min"]);
|
|
aggregate["max"] = max(aggregate["max"], result["max"]);
|
|
|
|
// Parallel version of Welford's algorithm
|
|
if (result["m2"] !== undefined) {
|
|
const delta = result["mean"] - aggregate["mean"];
|
|
const m2 = aggregate["m2"] + result["m2"] + (Math.pow(delta, 2) * ((aggregate["count"] * result["count"]) / n));
|
|
aggregate["m2"] = m2;
|
|
aggregate["varp"] = m2 / n;
|
|
aggregate["vars"] = m2 / (n - 1);
|
|
aggregate["stdp"] = Math.sqrt(m2 / n);
|
|
aggregate["stds"] = Math.sqrt(m2 / (n - 1));
|
|
}
|
|
|
|
if (result["sum"] !== undefined) {
|
|
aggregate["mean"] = (aggregate["sum"] + result["sum"]) / n;
|
|
aggregate["sum"] += result["sum"];
|
|
}
|
|
|
|
aggregate["count"] = n;
|
|
};
|
|
|
|
exports.Randomizer = Randomizer;
|
|
exports.Result = Result;
|
|
exports.Reduce = Reduce;
|
|
exports.Handler = Handler;
|
|
exports.Reducer = Reducer;
|