Fixed cache misimplementation and reinstalled canvas package with correct node version (17) works on prod docker compose but not on dev so :)
This commit is contained in:
@@ -1,4 +1,4 @@
|
|||||||
{
|
{
|
||||||
"singleQuote": true,
|
"singleQuote": false,
|
||||||
"trailingComma": "all"
|
"trailingComma": "all"
|
||||||
}
|
}
|
||||||
11830
back/package-lock.json
generated
11830
back/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -35,12 +35,16 @@
|
|||||||
"@types/bcryptjs": "^2.4.2",
|
"@types/bcryptjs": "^2.4.2",
|
||||||
"@types/passport": "^1.0.12",
|
"@types/passport": "^1.0.12",
|
||||||
"bcryptjs": "^2.4.3",
|
"bcryptjs": "^2.4.3",
|
||||||
|
"canvas": "^2.11.2",
|
||||||
"class-transformer": "^0.5.1",
|
"class-transformer": "^0.5.1",
|
||||||
"class-validator": "^0.13.2",
|
|
||||||
"json-logger-service": "^9.0.1",
|
|
||||||
"class-validator": "^0.14.0",
|
"class-validator": "^0.14.0",
|
||||||
|
"cross-blob": "^3.0.2",
|
||||||
|
"fs": "^0.0.1-security",
|
||||||
|
"jsdom": "^22.1.0",
|
||||||
|
"json-logger-service": "^9.0.1",
|
||||||
"node-fetch": "^2.6.12",
|
"node-fetch": "^2.6.12",
|
||||||
"nodemailer": "^6.9.5",
|
"nodemailer": "^6.9.5",
|
||||||
|
"opensheetmusicdisplay": "^1.8.4",
|
||||||
"passport-google-oauth20": "^2.0.0",
|
"passport-google-oauth20": "^2.0.0",
|
||||||
"passport-jwt": "^4.0.1",
|
"passport-jwt": "^4.0.1",
|
||||||
"passport-local": "^1.0.0",
|
"passport-local": "^1.0.0",
|
||||||
@@ -81,7 +85,8 @@
|
|||||||
"moduleFileExtensions": [
|
"moduleFileExtensions": [
|
||||||
"js",
|
"js",
|
||||||
"json",
|
"json",
|
||||||
"ts"
|
"ts",
|
||||||
|
"mjs"
|
||||||
],
|
],
|
||||||
"rootDir": "src",
|
"rootDir": "src",
|
||||||
"testRegex": ".*\\.spec\\.ts$",
|
"testRegex": ".*\\.spec\\.ts$",
|
||||||
|
|||||||
793
back/src/assetsgenerator/generateImages_browserless.js
Normal file
793
back/src/assetsgenerator/generateImages_browserless.js
Normal file
@@ -0,0 +1,793 @@
|
|||||||
|
// import Blob from "cross-blob";
|
||||||
|
import FS from "fs";
|
||||||
|
import jsdom from "jsdom";
|
||||||
|
//import headless_gl from "gl"; // this is now imported dynamically in a try catch, in case gl install fails, see #1160
|
||||||
|
import * as OSMD from "opensheetmusicdisplay"; // window needs to be available before we can require OSMD
|
||||||
|
|
||||||
|
|
||||||
|
let Blob;
|
||||||
|
|
||||||
|
/*
|
||||||
|
Render each OSMD sample, grab the generated images, andg
|
||||||
|
dump them into a local directory as PNG or SVG files.
|
||||||
|
|
||||||
|
inspired by Vexflow's generate_png_images and vexflow-tests.js
|
||||||
|
|
||||||
|
This can be used to generate PNGs or SVGs from OSMD without a browser.
|
||||||
|
It's also used with the visual regression test system (using PNGs) in
|
||||||
|
`tools/visual_regression.sh`
|
||||||
|
(see package.json, used with npm run generate:blessed and generate:current, then test:visual).
|
||||||
|
|
||||||
|
Note: this script needs to "fake" quite a few browser elements, like window, document,
|
||||||
|
and a Canvas HTMLElement (for PNG) or the DOM (for SVG) ,
|
||||||
|
which otherwise are missing in pure nodejs, causing errors in OSMD.
|
||||||
|
For PNG it needs the canvas package installed.
|
||||||
|
There are also some hacks needed to set the container size (offsetWidth) correctly.
|
||||||
|
|
||||||
|
Otherwise you'd need to run a headless browser, which is way slower,
|
||||||
|
see the semi-obsolete generateDiffImagesPuppeteerLocalhost.js
|
||||||
|
*/
|
||||||
|
|
||||||
|
function sleep(ms) {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
setTimeout(resolve, ms);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const timestampToMs = (timestamp, wholeNoteLength) => {
|
||||||
|
return timestamp.RealValue * wholeNoteLength;
|
||||||
|
};
|
||||||
|
const getActualNoteLength = (note, wholeNoteLength) => {
|
||||||
|
let duration = timestampToMs(note.Length, wholeNoteLength);
|
||||||
|
if (note.NoteTie) {
|
||||||
|
const firstNote = note.NoteTie.Notes.at(1);
|
||||||
|
if (Object.is(note.NoteTie.StartNote, note) && firstNote) {
|
||||||
|
duration += timestampToMs(firstNote.Length, wholeNoteLength);
|
||||||
|
} else {
|
||||||
|
duration = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return duration;
|
||||||
|
};
|
||||||
|
|
||||||
|
function getCursorPositions(osmd, filename) {
|
||||||
|
osmd.cursor.show();
|
||||||
|
const bpm = osmd.Sheet.HasBPMInfo
|
||||||
|
? osmd.Sheet.getExpressionsStartTempoInBPM()
|
||||||
|
: 60;
|
||||||
|
const wholeNoteLength = Math.round((60 / bpm) * 4000);
|
||||||
|
const curPos = [];
|
||||||
|
while (!osmd.cursor.iterator.EndReached) {
|
||||||
|
const notesToPlay = osmd.cursor
|
||||||
|
.NotesUnderCursor()
|
||||||
|
.filter((note) => {
|
||||||
|
return note.isRest() == false && note.Pitch;
|
||||||
|
})
|
||||||
|
.map((note) => {
|
||||||
|
const fixedKey =
|
||||||
|
note.ParentVoiceEntry.ParentVoice.Parent.SubInstruments.at(0)
|
||||||
|
?.fixedKey ?? 0;
|
||||||
|
const midiNumber = note.halfTone - fixedKey * 12;
|
||||||
|
const gain = note.ParentVoiceEntry.ParentVoice.Volume;
|
||||||
|
return {
|
||||||
|
note: midiNumber,
|
||||||
|
gain: gain,
|
||||||
|
duration: getActualNoteLength(note, wholeNoteLength),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
const shortestNotes = osmd.cursor
|
||||||
|
.NotesUnderCursor()
|
||||||
|
.sort((n1, n2) => n1.Length.CompareTo(n2.Length))
|
||||||
|
.at(0);
|
||||||
|
const ts = timestampToMs(
|
||||||
|
shortestNotes?.getAbsoluteTimestamp() ?? new OSMD.Fraction(-1),
|
||||||
|
wholeNoteLength,
|
||||||
|
);
|
||||||
|
const sNL = timestampToMs(
|
||||||
|
shortestNotes?.Length ?? new OSMD.Fraction(-1),
|
||||||
|
wholeNoteLength,
|
||||||
|
);
|
||||||
|
curPos.push({
|
||||||
|
x: parseFloat(osmd.cursor.cursorElement.style.left),
|
||||||
|
y: parseFloat(osmd.cursor.cursorElement.style.top),
|
||||||
|
width: osmd.cursor.cursorElement.width,
|
||||||
|
height: osmd.cursor.cursorElement.height,
|
||||||
|
notes: notesToPlay,
|
||||||
|
timestamp: ts,
|
||||||
|
timing: sNL,
|
||||||
|
});
|
||||||
|
osmd.cursor.next();
|
||||||
|
}
|
||||||
|
osmd.cursor.reset();
|
||||||
|
osmd.cursor.hide();
|
||||||
|
|
||||||
|
const cursorsFilename = `${imageDir}/${filename}.json`;
|
||||||
|
FS.writeFileSync(
|
||||||
|
cursorsFilename,
|
||||||
|
JSON.stringify(
|
||||||
|
{
|
||||||
|
pageWidth: osmd.Sheet.pageWidth,
|
||||||
|
cursors: curPos,
|
||||||
|
},
|
||||||
|
null,
|
||||||
|
2,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
console.log(`Saved cursor positions to ${cursorsFilename}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// global variables
|
||||||
|
// (without these being global, we'd have to pass many of these values to the generateSampleImage function)
|
||||||
|
// eslint-disable-next-line prefer-const
|
||||||
|
let assetName;
|
||||||
|
let sampleDir;
|
||||||
|
let imageDir;
|
||||||
|
let imageFormat;
|
||||||
|
let pageWidth;
|
||||||
|
let pageHeight;
|
||||||
|
let filterRegex;
|
||||||
|
let mode;
|
||||||
|
let debugSleepTimeString;
|
||||||
|
let skyBottomLinePreference;
|
||||||
|
let pageFormat;
|
||||||
|
|
||||||
|
export async function generateSongAssets(
|
||||||
|
assetName_,
|
||||||
|
sampleDir_,
|
||||||
|
imageDir_,
|
||||||
|
imageFormat_,
|
||||||
|
pageWidth_,
|
||||||
|
pageHeight_,
|
||||||
|
filterRegex_,
|
||||||
|
mode_,
|
||||||
|
debugSleepTimeString_,
|
||||||
|
skyBottomLinePreference_,
|
||||||
|
) {
|
||||||
|
assetName = assetName_;
|
||||||
|
sampleDir = sampleDir_;
|
||||||
|
imageDir = imageDir_;
|
||||||
|
imageFormat = imageFormat_;
|
||||||
|
pageWidth = pageWidth_;
|
||||||
|
pageHeight = pageHeight_;
|
||||||
|
filterRegex = filterRegex_;
|
||||||
|
mode = mode_;
|
||||||
|
debugSleepTimeString = debugSleepTimeString_;
|
||||||
|
skyBottomLinePreference = skyBottomLinePreference_;
|
||||||
|
imageFormat = imageFormat?.toLowerCase();
|
||||||
|
eval(`import("cross-blob")`).then((module) => {
|
||||||
|
Blob = module.default;
|
||||||
|
});
|
||||||
|
try {
|
||||||
|
let e = require("canvas")
|
||||||
|
console.log(e, "pass");
|
||||||
|
} catch (e) {
|
||||||
|
console.log(e);
|
||||||
|
}
|
||||||
|
debug("" + sampleDir + " " + imageDir + " " + imageFormat);
|
||||||
|
|
||||||
|
if (!mode) {
|
||||||
|
mode = "";
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
!assetName ||
|
||||||
|
!sampleDir ||
|
||||||
|
!imageDir ||
|
||||||
|
(imageFormat !== "png" && imageFormat !== "svg")
|
||||||
|
) {
|
||||||
|
console.log(
|
||||||
|
"usage: " +
|
||||||
|
// eslint-disable-next-line max-len
|
||||||
|
"node test/Util/generateImages_browserless.mjs osmdBuildDir sampleDirectory imageDirectory svg|png [width|0] [height|0] [filterRegex|all|allSmall] [--debug|--osmdtesting] [debugSleepTime]",
|
||||||
|
);
|
||||||
|
console.log(
|
||||||
|
" (use pageWidth and pageHeight 0 to not divide the rendering into pages (endless page))",
|
||||||
|
);
|
||||||
|
console.log(
|
||||||
|
' (use "all" to skip filterRegex parameter. "allSmall" with --osmdtesting skips two huge OSMD samples that take forever to render)',
|
||||||
|
);
|
||||||
|
console.log(
|
||||||
|
"example: node test/Util/generateImages_browserless.mjs ../../build ./test/data/ ./export png",
|
||||||
|
);
|
||||||
|
console.log(
|
||||||
|
"Error: need osmdBuildDir, sampleDir, imageDir and svg|png arguments. Exiting.",
|
||||||
|
);
|
||||||
|
Promise.reject(
|
||||||
|
"Error: need osmdBuildDir, sampleDir, imageDir and svg|png arguments. Exiting.",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
await init();
|
||||||
|
}
|
||||||
|
|
||||||
|
// let OSMD; // can only be required once window was simulated
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||||
|
|
||||||
|
async function init() {
|
||||||
|
debug("init");
|
||||||
|
|
||||||
|
const osmdTestingMode = mode.includes("osmdtesting"); // can also be --debugosmdtesting
|
||||||
|
const osmdTestingSingleMode = mode.includes("osmdtestingsingle");
|
||||||
|
const DEBUG = mode.startsWith("--debug");
|
||||||
|
// const debugSleepTime = Number.parseInt(process.env.GENERATE_DEBUG_SLEEP_TIME) || 0; // 5000 works for me [sschmidTU]
|
||||||
|
if (DEBUG) {
|
||||||
|
// debug(' (note that --debug slows down the script by about 0.3s per file, through logging)')
|
||||||
|
const debugSleepTimeMs = Number.parseInt(debugSleepTimeString, 10);
|
||||||
|
if (debugSleepTimeMs > 0) {
|
||||||
|
debug("debug sleep time: " + debugSleepTimeString);
|
||||||
|
await sleep(Number.parseInt(debugSleepTimeMs, 10));
|
||||||
|
// [VSCode] apparently this is necessary for the debugger to attach itself in time before the program closes.
|
||||||
|
// sometimes this is not enough, so you may have to try multiple times or increase the sleep timer. Unfortunately debugging nodejs isn't easy.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
debug("sampleDir: " + sampleDir, DEBUG);
|
||||||
|
debug("imageDir: " + imageDir, DEBUG);
|
||||||
|
debug("imageFormat: " + imageFormat, DEBUG);
|
||||||
|
|
||||||
|
pageFormat = "Endless";
|
||||||
|
pageWidth = Number.parseInt(pageWidth, 10);
|
||||||
|
pageHeight = Number.parseInt(pageHeight, 10);
|
||||||
|
const endlessPage = !(pageHeight > 0 && pageWidth > 0);
|
||||||
|
if (!endlessPage) {
|
||||||
|
pageFormat = `${pageWidth}x${pageHeight}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- hacks to fake Browser elements OSMD and Vexflow need, like window, document, and a canvas HTMLElement ----
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||||
|
const dom = new jsdom.JSDOM("<!DOCTYPE html></html>");
|
||||||
|
// eslint-disable-next-line no-global-assign
|
||||||
|
// window = dom.window;
|
||||||
|
// eslint-disable-next-line no-global-assign
|
||||||
|
// document = dom.window.document;
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-global-assign
|
||||||
|
global.window = dom.window;
|
||||||
|
// eslint-disable-next-line no-global-assign
|
||||||
|
global.document = window.document;
|
||||||
|
//window.console = console; // probably does nothing
|
||||||
|
global.HTMLElement = window.HTMLElement;
|
||||||
|
global.HTMLAnchorElement = window.HTMLAnchorElement;
|
||||||
|
global.XMLHttpRequest = window.XMLHttpRequest;
|
||||||
|
global.DOMParser = window.DOMParser;
|
||||||
|
global.Node = window.Node;
|
||||||
|
if (imageFormat === "png") {
|
||||||
|
global.Canvas = window.Canvas;
|
||||||
|
}
|
||||||
|
|
||||||
|
// For WebGLSkyBottomLineCalculatorBackend: Try to import gl dynamically
|
||||||
|
// this is so that the script doesn't fail if gl could not be installed,
|
||||||
|
// which can happen in some linux setups where gcc-11 is installed, see #1160
|
||||||
|
try {
|
||||||
|
const { default: headless_gl } = await import("gl");
|
||||||
|
const oldCreateElement = document.createElement.bind(document);
|
||||||
|
document.createElement = function (tagName, options) {
|
||||||
|
if (tagName.toLowerCase() === "canvas") {
|
||||||
|
const canvas = oldCreateElement(tagName, options);
|
||||||
|
const oldGetContext = canvas.getContext.bind(canvas);
|
||||||
|
canvas.getContext = function (contextType, contextAttributes) {
|
||||||
|
if (
|
||||||
|
contextType.toLowerCase() === "webgl" ||
|
||||||
|
contextType.toLowerCase() === "experimental-webgl"
|
||||||
|
) {
|
||||||
|
const gl = headless_gl(
|
||||||
|
canvas.width,
|
||||||
|
canvas.height,
|
||||||
|
contextAttributes,
|
||||||
|
);
|
||||||
|
gl.canvas = canvas;
|
||||||
|
return gl;
|
||||||
|
} else {
|
||||||
|
return oldGetContext(contextType, contextAttributes);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
return canvas;
|
||||||
|
} else {
|
||||||
|
return oldCreateElement(tagName, options);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
} catch {
|
||||||
|
if (skyBottomLinePreference === "--webgl") {
|
||||||
|
debug(
|
||||||
|
"WebGL image generation was requested but gl is not installed; using non-WebGL generation.",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// fix Blob not found (to support external modules like is-blob)
|
||||||
|
global.Blob = Blob;
|
||||||
|
|
||||||
|
const div = document.createElement("div");
|
||||||
|
div.id = "browserlessDiv";
|
||||||
|
document.body.appendChild(div);
|
||||||
|
// const canvas = document.createElement('canvas')
|
||||||
|
// div.canvas = document.createElement('canvas')
|
||||||
|
|
||||||
|
const zoom = 1.0;
|
||||||
|
// width of the div / PNG generated
|
||||||
|
let width = pageWidth * zoom;
|
||||||
|
// TODO sometimes the width is way too small for the score, may need to adjust zoom.
|
||||||
|
if (endlessPage) {
|
||||||
|
width = 1440;
|
||||||
|
}
|
||||||
|
let height = pageHeight;
|
||||||
|
if (endlessPage) {
|
||||||
|
height = 32767;
|
||||||
|
}
|
||||||
|
div.width = width;
|
||||||
|
div.height = height;
|
||||||
|
// div.offsetWidth = width; // doesn't work, offsetWidth is always 0 from this. see below
|
||||||
|
// div.clientWidth = width;
|
||||||
|
// div.clientHeight = height;
|
||||||
|
// div.scrollHeight = height;
|
||||||
|
// div.scrollWidth = width;
|
||||||
|
div.setAttribute("width", width);
|
||||||
|
div.setAttribute("height", height);
|
||||||
|
div.setAttribute("offsetWidth", width);
|
||||||
|
// debug('div.offsetWidth: ' + div.offsetWidth, DEBUG) // 0 here, set correctly later
|
||||||
|
// debug('div.height: ' + div.height, DEBUG)
|
||||||
|
|
||||||
|
// hack: set offsetWidth reliably
|
||||||
|
Object.defineProperties(window.HTMLElement.prototype, {
|
||||||
|
offsetLeft: {
|
||||||
|
get: function () {
|
||||||
|
return parseFloat(window.getComputedStyle(this).marginTop) || 0;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
offsetTop: {
|
||||||
|
get: function () {
|
||||||
|
return parseFloat(window.getComputedStyle(this).marginTop) || 0;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
offsetHeight: {
|
||||||
|
get: function () {
|
||||||
|
return height;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
offsetWidth: {
|
||||||
|
get: function () {
|
||||||
|
return width;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
debug("div.offsetWidth: " + div.offsetWidth, DEBUG);
|
||||||
|
debug("div.height: " + div.height, DEBUG);
|
||||||
|
// ---- end browser hacks (hopefully) ----
|
||||||
|
|
||||||
|
// load globally
|
||||||
|
|
||||||
|
// Create the image directory if it doesn't exist.
|
||||||
|
FS.mkdirSync(imageDir, { recursive: true });
|
||||||
|
|
||||||
|
// const sampleDirFilenames = FS.readdirSync(sampleDir);
|
||||||
|
let samplesToProcess = []; // samples we want to process/generate pngs of, excluding the filtered out files/filenames
|
||||||
|
|
||||||
|
// sampleDir is the direct path to a single file but is then only keept as a the directory containing the file
|
||||||
|
if (sampleDir.match("^.*(.xml)|(.musicxml)|(.mxl)$")) {
|
||||||
|
let pathParts = sampleDir.split("/");
|
||||||
|
let filename = pathParts[pathParts.length - 1];
|
||||||
|
sampleDir = pathParts.slice(0, pathParts.length - 1).join("/");
|
||||||
|
samplesToProcess.push(filename);
|
||||||
|
} else {
|
||||||
|
debug("not a correct extension sampleDir: " + sampleDir, DEBUG);
|
||||||
|
}
|
||||||
|
// for (const sampleFilename of sampleDirFilenames) {
|
||||||
|
// if (osmdTestingMode && filterRegex === "allSmall") {
|
||||||
|
// if (sampleFilename.match("^(Actor)|(Gounod)")) {
|
||||||
|
// // TODO maybe filter by file size instead
|
||||||
|
// debug("filtering big file: " + sampleFilename, DEBUG);
|
||||||
|
// continue;
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// // eslint-disable-next-line no-useless-escape
|
||||||
|
// if (sampleFilename.match("^.*(.xml)|(.musicxml)|(.mxl)$")) {
|
||||||
|
// // debug('found musicxml/mxl: ' + sampleFilename)
|
||||||
|
// samplesToProcess.push(sampleFilename);
|
||||||
|
// } else {
|
||||||
|
// debug("discarded file/directory: " + sampleFilename, DEBUG);
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
// filter samples to process by regex if given
|
||||||
|
if (
|
||||||
|
filterRegex &&
|
||||||
|
filterRegex !== "" &&
|
||||||
|
filterRegex !== "all" &&
|
||||||
|
!(osmdTestingMode && filterRegex === "allSmall")
|
||||||
|
) {
|
||||||
|
debug("filtering samples for regex: " + filterRegex, DEBUG);
|
||||||
|
samplesToProcess = samplesToProcess.filter((filename) =>
|
||||||
|
filename.match(filterRegex),
|
||||||
|
);
|
||||||
|
debug(`found ${samplesToProcess.length} matches: `, DEBUG);
|
||||||
|
for (let i = 0; i < samplesToProcess.length; i++) {
|
||||||
|
debug(samplesToProcess[i], DEBUG);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const backend = imageFormat === "png" ? "canvas" : "svg";
|
||||||
|
const osmdInstance = new OSMD.OpenSheetMusicDisplay(div, {
|
||||||
|
autoResize: false,
|
||||||
|
backend: backend,
|
||||||
|
pageBackgroundColor: "#FFFFFF",
|
||||||
|
pageFormat: pageFormat,
|
||||||
|
// defaultFontFamily: 'Arial',
|
||||||
|
drawTitle: false,
|
||||||
|
renderSingleHorizontalStaffline: true,
|
||||||
|
drawComposer: false,
|
||||||
|
drawCredits: false,
|
||||||
|
drawLyrics: false,
|
||||||
|
drawPartNames: false,
|
||||||
|
followCursor: false,
|
||||||
|
cursorsOptions: [{ type: 0, color: "green", alpha: 0.5, follow: false }],
|
||||||
|
});
|
||||||
|
// for more options check OSMDOptions.ts
|
||||||
|
|
||||||
|
// you can set finer-grained rendering/engraving settings in EngravingRules:
|
||||||
|
// osmdInstance.EngravingRules.TitleTopDistance = 5.0 // 5.0 is default
|
||||||
|
// (unless in osmdTestingMode, these will be reset with drawingParameters default)
|
||||||
|
// osmdInstance.EngravingRules.PageTopMargin = 5.0 // 5 is default
|
||||||
|
// osmdInstance.EngravingRules.PageBottomMargin = 5.0 // 5 is default. <5 can cut off scores that extend in the last staffline
|
||||||
|
// note that for now the png and canvas will still have the height given in the script argument,
|
||||||
|
// so even with a margin of 0 the image will be filled to the full height.
|
||||||
|
// osmdInstance.EngravingRules.PageLeftMargin = 5.0 // 5 is default
|
||||||
|
// osmdInstance.EngravingRules.PageRightMargin = 5.0 // 5 is default
|
||||||
|
// osmdInstance.EngravingRules.MetronomeMarkXShift = -8; // -6 is default
|
||||||
|
// osmdInstance.EngravingRules.DistanceBetweenVerticalSystemLines = 0.15; // 0.35 is default
|
||||||
|
// for more options check EngravingRules.ts (though not all of these are meant and fully supported to be changed at will)
|
||||||
|
|
||||||
|
if (DEBUG) {
|
||||||
|
osmdInstance.setLogLevel("debug");
|
||||||
|
// debug(`osmd PageFormat: ${osmdInstance.EngravingRules.PageFormat.width}x${osmdInstance.EngravingRules.PageFormat.height}`)
|
||||||
|
debug(
|
||||||
|
`osmd PageFormat idString: ${osmdInstance.EngravingRules.PageFormat.idString}`,
|
||||||
|
);
|
||||||
|
debug("PageHeight: " + osmdInstance.EngravingRules.PageHeight);
|
||||||
|
} else {
|
||||||
|
osmdInstance.setLogLevel("info"); // doesn't seem to work, log.debug still logs
|
||||||
|
}
|
||||||
|
|
||||||
|
debug(
|
||||||
|
"[OSMD.generateImages] starting loop over samples, saving images to " +
|
||||||
|
imageDir,
|
||||||
|
DEBUG,
|
||||||
|
);
|
||||||
|
for (let i = 0; i < samplesToProcess.length; i++) {
|
||||||
|
const sampleFilename = samplesToProcess[i];
|
||||||
|
debug("sampleFilename: " + sampleFilename, DEBUG);
|
||||||
|
|
||||||
|
await generateSampleImage(
|
||||||
|
sampleFilename,
|
||||||
|
sampleDir,
|
||||||
|
osmdInstance,
|
||||||
|
osmdTestingMode,
|
||||||
|
{},
|
||||||
|
DEBUG,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (
|
||||||
|
osmdTestingMode &&
|
||||||
|
!osmdTestingSingleMode &&
|
||||||
|
sampleFilename.startsWith("Beethoven") &&
|
||||||
|
sampleFilename.includes("Geliebte")
|
||||||
|
) {
|
||||||
|
// generate one more testing image with skyline and bottomline. (startsWith 'Beethoven' don't catch the function test)
|
||||||
|
await generateSampleImage(
|
||||||
|
sampleFilename,
|
||||||
|
sampleDir,
|
||||||
|
osmdInstance,
|
||||||
|
osmdTestingMode,
|
||||||
|
{ skyBottomLine: true },
|
||||||
|
DEBUG,
|
||||||
|
);
|
||||||
|
// generate one more testing image with GraphicalNote positions
|
||||||
|
await generateSampleImage(
|
||||||
|
sampleFilename,
|
||||||
|
sampleDir,
|
||||||
|
osmdInstance,
|
||||||
|
osmdTestingMode,
|
||||||
|
{ boundingBoxes: "VexFlowGraphicalNote" },
|
||||||
|
DEBUG,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
debug("done, exiting.");
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line
|
||||||
|
// let maxRss = 0, maxRssFilename = '' // to log memory usage (debug)
|
||||||
|
async function generateSampleImage(
|
||||||
|
sampleFilename,
|
||||||
|
directory,
|
||||||
|
osmdInstance,
|
||||||
|
osmdTestingMode,
|
||||||
|
options = {},
|
||||||
|
DEBUG = false,
|
||||||
|
) {
|
||||||
|
function makeSkyBottomLineOptions() {
|
||||||
|
const preference = skyBottomLinePreference ?? "";
|
||||||
|
if (preference === "--batch") {
|
||||||
|
return {
|
||||||
|
preferredSkyBottomLineBatchCalculatorBackend: 0, // plain
|
||||||
|
skyBottomLineBatchCriteria: 0, // use batch algorithm only
|
||||||
|
};
|
||||||
|
} else if (preference === "--webgl") {
|
||||||
|
return {
|
||||||
|
preferredSkyBottomLineBatchCalculatorBackend: 1, // webgl
|
||||||
|
skyBottomLineBatchCriteria: 0, // use batch algorithm only
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
return {
|
||||||
|
preferredSkyBottomLineBatchCalculatorBackend: 0, // plain
|
||||||
|
skyBottomLineBatchCriteria: Infinity, // use non-batch algorithm only
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const samplePath = directory + "/" + sampleFilename;
|
||||||
|
let loadParameter = FS.readFileSync(samplePath);
|
||||||
|
|
||||||
|
if (sampleFilename.endsWith(".mxl")) {
|
||||||
|
loadParameter = await OSMD.MXLHelper.MXLtoXMLstring(loadParameter);
|
||||||
|
} else {
|
||||||
|
loadParameter = loadParameter.toString();
|
||||||
|
}
|
||||||
|
// debug('loadParameter: ' + loadParameter)
|
||||||
|
// debug('typeof loadParameter: ' + typeof loadParameter)
|
||||||
|
|
||||||
|
// set sample-specific options for OSMD visual regression testing
|
||||||
|
let includeSkyBottomLine = false;
|
||||||
|
let drawBoundingBoxString;
|
||||||
|
let isTestOctaveShiftInvisibleInstrument;
|
||||||
|
let isTestInvisibleMeasureNotAffectingLayout;
|
||||||
|
if (osmdTestingMode) {
|
||||||
|
const isFunctionTestAutobeam = sampleFilename.startsWith(
|
||||||
|
"OSMD_function_test_autobeam",
|
||||||
|
);
|
||||||
|
const isFunctionTestAutoColoring = sampleFilename.startsWith(
|
||||||
|
"OSMD_function_test_auto-custom-coloring",
|
||||||
|
);
|
||||||
|
const isFunctionTestSystemAndPageBreaks = sampleFilename.startsWith(
|
||||||
|
"OSMD_Function_Test_System_and_Page_Breaks",
|
||||||
|
);
|
||||||
|
const isFunctionTestDrawingRange = sampleFilename.startsWith(
|
||||||
|
"OSMD_function_test_measuresToDraw_",
|
||||||
|
);
|
||||||
|
const defaultOrCompactTightMode = sampleFilename.startsWith(
|
||||||
|
"OSMD_Function_Test_Container_height",
|
||||||
|
)
|
||||||
|
? "compacttight"
|
||||||
|
: "default";
|
||||||
|
const isTestFlatBeams = sampleFilename.startsWith("test_drum_tuplet_beams");
|
||||||
|
const isTestEndClefStaffEntryBboxes = sampleFilename.startsWith(
|
||||||
|
"test_end_measure_clefs_staffentry_bbox",
|
||||||
|
);
|
||||||
|
const isTestPageBreakImpliesSystemBreak = sampleFilename.startsWith(
|
||||||
|
"test_pagebreak_implies_systembreak",
|
||||||
|
);
|
||||||
|
const isTestPageBottomMargin0 =
|
||||||
|
sampleFilename.includes("PageBottomMargin0");
|
||||||
|
const isTestTupletBracketTupletNumber = sampleFilename.includes(
|
||||||
|
"test_tuplet_bracket_tuplet_number",
|
||||||
|
);
|
||||||
|
const isTestCajon2NoteSystem = sampleFilename.includes(
|
||||||
|
"test_cajon_2-note-system",
|
||||||
|
);
|
||||||
|
isTestOctaveShiftInvisibleInstrument = sampleFilename.includes(
|
||||||
|
"test_octaveshift_first_instrument_invisible",
|
||||||
|
);
|
||||||
|
const isTextOctaveShiftExtraGraphicalMeasure = sampleFilename.includes(
|
||||||
|
"test_octaveshift_extragraphicalmeasure",
|
||||||
|
);
|
||||||
|
isTestInvisibleMeasureNotAffectingLayout = sampleFilename.includes(
|
||||||
|
"test_invisible_measure_not_affecting_layout",
|
||||||
|
);
|
||||||
|
const isTestWedgeMultilineCrescendo = sampleFilename.includes(
|
||||||
|
"test_wedge_multiline_crescendo",
|
||||||
|
);
|
||||||
|
const isTestWedgeMultilineDecrescendo = sampleFilename.includes(
|
||||||
|
"test_wedge_multiline_decrescendo",
|
||||||
|
);
|
||||||
|
osmdInstance.EngravingRules.loadDefaultValues(); // note this may also be executed in setOptions below via drawingParameters default
|
||||||
|
if (isTestEndClefStaffEntryBboxes) {
|
||||||
|
drawBoundingBoxString = "VexFlowStaffEntry";
|
||||||
|
} else {
|
||||||
|
drawBoundingBoxString = options.boundingBoxes; // undefined is also a valid value: no bboxes
|
||||||
|
}
|
||||||
|
osmdInstance.setOptions({
|
||||||
|
autoBeam: isFunctionTestAutobeam, // only set to true for function test autobeam
|
||||||
|
coloringMode: isFunctionTestAutoColoring ? 2 : 0,
|
||||||
|
// eslint-disable-next-line max-len
|
||||||
|
coloringSetCustom: isFunctionTestAutoColoring
|
||||||
|
? [
|
||||||
|
"#d82c6b",
|
||||||
|
"#F89D15",
|
||||||
|
"#FFE21A",
|
||||||
|
"#4dbd5c",
|
||||||
|
"#009D96",
|
||||||
|
"#43469d",
|
||||||
|
"#76429c",
|
||||||
|
"#ff0000",
|
||||||
|
]
|
||||||
|
: undefined,
|
||||||
|
colorStemsLikeNoteheads: isFunctionTestAutoColoring,
|
||||||
|
drawingParameters: defaultOrCompactTightMode, // note: default resets all EngravingRules. could be solved differently
|
||||||
|
drawFromMeasureNumber: isFunctionTestDrawingRange ? 9 : 1,
|
||||||
|
drawUpToMeasureNumber: isFunctionTestDrawingRange
|
||||||
|
? 12
|
||||||
|
: Number.MAX_SAFE_INTEGER,
|
||||||
|
newSystemFromXML: isFunctionTestSystemAndPageBreaks,
|
||||||
|
newSystemFromNewPageInXML: isTestPageBreakImpliesSystemBreak,
|
||||||
|
newPageFromXML: isFunctionTestSystemAndPageBreaks,
|
||||||
|
pageBackgroundColor: "#FFFFFF", // reset by drawingparameters default
|
||||||
|
pageFormat: pageFormat, // reset by drawingparameters default,
|
||||||
|
...makeSkyBottomLineOptions(),
|
||||||
|
});
|
||||||
|
// note that loadDefaultValues() may be executed in setOptions with drawingParameters default
|
||||||
|
//osmdInstance.EngravingRules.RenderSingleHorizontalStaffline = true; // to use this option here, place it after setOptions(), see above
|
||||||
|
osmdInstance.EngravingRules.AlwaysSetPreferredSkyBottomLineBackendAutomatically = false; // this would override the command line options (--plain etc)
|
||||||
|
includeSkyBottomLine = options.skyBottomLine
|
||||||
|
? options.skyBottomLine
|
||||||
|
: false; // apparently es6 doesn't have ?? operator
|
||||||
|
osmdInstance.drawSkyLine = includeSkyBottomLine; // if includeSkyBottomLine, draw skyline and bottomline, else not
|
||||||
|
osmdInstance.drawBottomLine = includeSkyBottomLine;
|
||||||
|
osmdInstance.setDrawBoundingBox(drawBoundingBoxString, false); // false: don't render (now). also (re-)set if undefined!
|
||||||
|
if (isTestFlatBeams) {
|
||||||
|
osmdInstance.EngravingRules.FlatBeams = true;
|
||||||
|
// osmdInstance.EngravingRules.FlatBeamOffset = 30;
|
||||||
|
osmdInstance.EngravingRules.FlatBeamOffset = 10;
|
||||||
|
osmdInstance.EngravingRules.FlatBeamOffsetPerBeam = 10;
|
||||||
|
} else {
|
||||||
|
osmdInstance.EngravingRules.FlatBeams = false;
|
||||||
|
}
|
||||||
|
if (isTestPageBottomMargin0) {
|
||||||
|
osmdInstance.EngravingRules.PageBottomMargin = 0;
|
||||||
|
}
|
||||||
|
if (isTestTupletBracketTupletNumber) {
|
||||||
|
osmdInstance.EngravingRules.TupletNumberLimitConsecutiveRepetitions = true;
|
||||||
|
osmdInstance.EngravingRules.TupletNumberMaxConsecutiveRepetitions = 2;
|
||||||
|
osmdInstance.EngravingRules.TupletNumberAlwaysDisableAfterFirstMax = true; // necessary to trigger bug
|
||||||
|
}
|
||||||
|
if (isTestCajon2NoteSystem) {
|
||||||
|
osmdInstance.EngravingRules.PercussionUseCajon2NoteSystem = true;
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
isTextOctaveShiftExtraGraphicalMeasure ||
|
||||||
|
isTestOctaveShiftInvisibleInstrument ||
|
||||||
|
isTestWedgeMultilineCrescendo ||
|
||||||
|
isTestWedgeMultilineDecrescendo
|
||||||
|
) {
|
||||||
|
osmdInstance.EngravingRules.NewSystemAtXMLNewSystemAttribute = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
debug("loading sample " + sampleFilename, DEBUG);
|
||||||
|
await osmdInstance.load(loadParameter, sampleFilename); // if using load.then() without await, memory will not be freed up between renders
|
||||||
|
if (isTestOctaveShiftInvisibleInstrument) {
|
||||||
|
osmdInstance.Sheet.Instruments[0].Visible = false;
|
||||||
|
}
|
||||||
|
if (isTestInvisibleMeasureNotAffectingLayout) {
|
||||||
|
if (osmdInstance.Sheet.Instruments[1]) {
|
||||||
|
// some systems can't handle ?. in this script (just a safety check anyways)
|
||||||
|
osmdInstance.Sheet.Instruments[1].Visible = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (ex) {
|
||||||
|
debug(
|
||||||
|
"couldn't load sample " + sampleFilename + ", skipping. Error: \n" + ex,
|
||||||
|
);
|
||||||
|
return Promise.reject(ex);
|
||||||
|
}
|
||||||
|
debug("xml loaded", DEBUG);
|
||||||
|
try {
|
||||||
|
osmdInstance.render();
|
||||||
|
getCursorPositions(osmdInstance, assetName);
|
||||||
|
// there were reports that await could help here, but render isn't a synchronous function, and it seems to work. see #932
|
||||||
|
} catch (ex) {
|
||||||
|
debug("renderError: " + ex);
|
||||||
|
}
|
||||||
|
debug("rendered", DEBUG);
|
||||||
|
|
||||||
|
const markupStrings = []; // svg
|
||||||
|
const dataUrls = []; // png
|
||||||
|
let canvasImage;
|
||||||
|
|
||||||
|
for (
|
||||||
|
let pageNumber = 1;
|
||||||
|
pageNumber < Number.POSITIVE_INFINITY;
|
||||||
|
pageNumber++
|
||||||
|
) {
|
||||||
|
if (imageFormat === "png") {
|
||||||
|
canvasImage = document.getElementById(
|
||||||
|
"osmdCanvasVexFlowBackendCanvas" + pageNumber,
|
||||||
|
);
|
||||||
|
if (!canvasImage) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (!canvasImage.toDataURL) {
|
||||||
|
debug(
|
||||||
|
`error: could not get canvas image for page ${pageNumber} for file: ${sampleFilename}`,
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
dataUrls.push(canvasImage.toDataURL());
|
||||||
|
} else if (imageFormat === "svg") {
|
||||||
|
const svgElement = document.getElementById("osmdSvgPage" + pageNumber);
|
||||||
|
if (!svgElement) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
// The important xmlns attribute is not serialized unless we set it here
|
||||||
|
svgElement.setAttribute("xmlns", "http://www.w3.org/2000/svg");
|
||||||
|
markupStrings.push(svgElement.outerHTML);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (
|
||||||
|
let pageIndex = 0;
|
||||||
|
pageIndex < Math.max(dataUrls.length, markupStrings.length);
|
||||||
|
pageIndex++
|
||||||
|
) {
|
||||||
|
const pageNumberingString = `${pageIndex + 1}`;
|
||||||
|
const skybottomlineString = includeSkyBottomLine ? "skybottomline_" : "";
|
||||||
|
const graphicalNoteBboxesString = drawBoundingBoxString
|
||||||
|
? "bbox" + drawBoundingBoxString + "_"
|
||||||
|
: "";
|
||||||
|
// pageNumberingString = dataUrls.length > 0 ? pageNumberingString : '' // don't put '_1' at the end if only one page. though that may cause more work
|
||||||
|
const pageFilename = `${imageDir}/${assetName}.${imageFormat}`;
|
||||||
|
|
||||||
|
if (imageFormat === "png") {
|
||||||
|
const dataUrl = dataUrls[pageIndex];
|
||||||
|
if (!dataUrl || !dataUrl.split) {
|
||||||
|
debug(
|
||||||
|
`error: could not get dataUrl (imageData) for page ${
|
||||||
|
pageIndex + 1
|
||||||
|
} of sample: ${sampleFilename}`,
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const imageData = dataUrl.split(";base64,").pop();
|
||||||
|
const imageBuffer = Buffer.from(imageData, "base64");
|
||||||
|
|
||||||
|
debug("got image data, saving to: " + pageFilename, DEBUG);
|
||||||
|
FS.writeFileSync(pageFilename, imageBuffer, { encoding: "base64" });
|
||||||
|
} else if (imageFormat === "svg") {
|
||||||
|
const markup = markupStrings[pageIndex];
|
||||||
|
if (!markup) {
|
||||||
|
debug(
|
||||||
|
`error: could not get markup (SVG data) for page ${
|
||||||
|
pageIndex + 1
|
||||||
|
} of sample: ${sampleFilename}`,
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
debug("got svg markup data, saving to: " + pageFilename, DEBUG);
|
||||||
|
FS.writeFileSync(pageFilename, markup, { encoding: "utf-8" });
|
||||||
|
}
|
||||||
|
|
||||||
|
// debug: log memory usage
|
||||||
|
// const usage = process.memoryUsage()
|
||||||
|
// for (const entry of Object.entries(usage)) {
|
||||||
|
// if (entry[0] === 'rss') {
|
||||||
|
// if (entry[1] > maxRss) {
|
||||||
|
// maxRss = entry[1]
|
||||||
|
// maxRssFilename = pageFilename
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// debug(entry[0] + ': ' + entry[1] / (1024 * 1024) + 'mb')
|
||||||
|
// }
|
||||||
|
// debug('maxRss: ' + (maxRss / 1024 / 1024) + 'mb' + ' for ' + maxRssFilename)
|
||||||
|
}
|
||||||
|
// debug('maxRss total: ' + (maxRss / 1024 / 1024) + 'mb' + ' for ' + maxRssFilename)
|
||||||
|
|
||||||
|
// await sleep(5000)
|
||||||
|
// }) // end read file
|
||||||
|
}
|
||||||
|
|
||||||
|
function debug(msg, debugEnabled = true) {
|
||||||
|
if (debugEnabled) {
|
||||||
|
console.log("[generateImages] " + msg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// init();
|
||||||
@@ -15,13 +15,14 @@ import {
|
|||||||
Req,
|
Req,
|
||||||
StreamableFile,
|
StreamableFile,
|
||||||
UseGuards,
|
UseGuards,
|
||||||
} from '@nestjs/common';
|
Header,
|
||||||
import { ApiOkResponsePlaginated, Plage } from 'src/models/plage';
|
} from "@nestjs/common";
|
||||||
import { CreateSongDto } from './dto/create-song.dto';
|
import { ApiOkResponsePlaginated, Plage } from "src/models/plage";
|
||||||
import { SongService } from './song.service';
|
import { CreateSongDto } from "./dto/create-song.dto";
|
||||||
import { Request } from 'express';
|
import { SongService } from "./song.service";
|
||||||
import { Prisma, Song } from '@prisma/client';
|
import { Request } from "express";
|
||||||
import { createReadStream, existsSync } from 'fs';
|
import { Prisma, Song } from "@prisma/client";
|
||||||
|
import { createReadStream, existsSync, readFileSync } from "fs";
|
||||||
import {
|
import {
|
||||||
ApiNotFoundResponse,
|
ApiNotFoundResponse,
|
||||||
ApiOkResponse,
|
ApiOkResponse,
|
||||||
@@ -29,14 +30,13 @@ import {
|
|||||||
ApiProperty,
|
ApiProperty,
|
||||||
ApiTags,
|
ApiTags,
|
||||||
ApiUnauthorizedResponse,
|
ApiUnauthorizedResponse,
|
||||||
} from '@nestjs/swagger';
|
} from "@nestjs/swagger";
|
||||||
import { HistoryService } from 'src/history/history.service';
|
import { HistoryService } from "src/history/history.service";
|
||||||
import { JwtAuthGuard } from 'src/auth/jwt-auth.guard';
|
import { JwtAuthGuard } from "src/auth/jwt-auth.guard";
|
||||||
import { FilterQuery } from 'src/utils/filter.pipe';
|
import { FilterQuery } from "src/utils/filter.pipe";
|
||||||
import { Song as _Song } from 'src/_gen/prisma-class/song';
|
import { Song as _Song } from "src/_gen/prisma-class/song";
|
||||||
import { SongHistory } from 'src/_gen/prisma-class/song_history';
|
import { SongHistory } from "src/_gen/prisma-class/song_history";
|
||||||
import { IncludeMap, mapInclude } from 'src/utils/include';
|
import { IncludeMap, mapInclude } from "src/utils/include";
|
||||||
import { Public } from 'src/auth/public';
|
|
||||||
|
|
||||||
class SongHistoryResult {
|
class SongHistoryResult {
|
||||||
@ApiProperty()
|
@ApiProperty()
|
||||||
@@ -45,16 +45,16 @@ class SongHistoryResult {
|
|||||||
history: SongHistory[];
|
history: SongHistory[];
|
||||||
}
|
}
|
||||||
|
|
||||||
@Controller('song')
|
@Controller("song")
|
||||||
@ApiTags('song')
|
@ApiTags("song")
|
||||||
@UseGuards(JwtAuthGuard)
|
@UseGuards(JwtAuthGuard)
|
||||||
export class SongController {
|
export class SongController {
|
||||||
static filterableFields: string[] = [
|
static filterableFields: string[] = [
|
||||||
'+id',
|
"+id",
|
||||||
'name',
|
"name",
|
||||||
'+artistId',
|
"+artistId",
|
||||||
'+albumId',
|
"+albumId",
|
||||||
'+genreId',
|
"+genreId",
|
||||||
];
|
];
|
||||||
static includableFields: IncludeMap<Prisma.SongInclude> = {
|
static includableFields: IncludeMap<Prisma.SongInclude> = {
|
||||||
artist: true,
|
artist: true,
|
||||||
@@ -69,36 +69,35 @@ export class SongController {
|
|||||||
private readonly historyService: HistoryService,
|
private readonly historyService: HistoryService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
@Get(':id/midi')
|
@Get(":id/midi")
|
||||||
@ApiOperation({ description: 'Streams the midi file of the requested song' })
|
@ApiOperation({ description: "Streams the midi file of the requested song" })
|
||||||
@ApiNotFoundResponse({ description: 'Song not found' })
|
@ApiNotFoundResponse({ description: "Song not found" })
|
||||||
@ApiOkResponse({ description: 'Returns the midi file succesfully' })
|
@ApiOkResponse({ description: "Returns the midi file succesfully" })
|
||||||
async getMidi(@Param('id', ParseIntPipe) id: number) {
|
async getMidi(@Param("id", ParseIntPipe) id: number) {
|
||||||
const song = await this.songService.song({ id });
|
const song = await this.songService.song({ id });
|
||||||
if (!song) throw new NotFoundException('Song not found');
|
if (!song) throw new NotFoundException("Song not found");
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const file = createReadStream(song.midiPath);
|
const file = createReadStream(song.midiPath);
|
||||||
return new StreamableFile(file, { type: 'audio/midi' });
|
return new StreamableFile(file, { type: "audio/midi" });
|
||||||
} catch {
|
} catch {
|
||||||
throw new InternalServerErrorException();
|
throw new InternalServerErrorException();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Get(':id/illustration')
|
@Get(":id/illustration")
|
||||||
@ApiOperation({
|
@ApiOperation({
|
||||||
description: 'Streams the illustration of the requested song',
|
description: "Streams the illustration of the requested song",
|
||||||
})
|
})
|
||||||
@ApiNotFoundResponse({ description: 'Song not found' })
|
@ApiNotFoundResponse({ description: "Song not found" })
|
||||||
@ApiOkResponse({ description: 'Returns the illustration succesfully' })
|
@ApiOkResponse({ description: "Returns the illustration succesfully" })
|
||||||
@Public()
|
async getIllustration(@Param("id", ParseIntPipe) id: number) {
|
||||||
async getIllustration(@Param('id', ParseIntPipe) id: number) {
|
|
||||||
const song = await this.songService.song({ id });
|
const song = await this.songService.song({ id });
|
||||||
if (!song) throw new NotFoundException('Song not found');
|
if (!song) throw new NotFoundException("Song not found");
|
||||||
|
|
||||||
if (song.illustrationPath === null) throw new NotFoundException();
|
if (song.illustrationPath === null) throw new NotFoundException();
|
||||||
if (!existsSync(song.illustrationPath))
|
if (!existsSync(song.illustrationPath))
|
||||||
throw new NotFoundException('Illustration not found');
|
throw new NotFoundException("Illustration not found");
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const file = createReadStream(song.illustrationPath);
|
const file = createReadStream(song.illustrationPath);
|
||||||
@@ -108,24 +107,76 @@ export class SongController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Get(':id/musicXml')
|
@Get(":id/musicXml")
|
||||||
@ApiOperation({
|
@ApiOperation({
|
||||||
description: 'Streams the musicXML file of the requested song',
|
description: "Streams the musicXML file of the requested song",
|
||||||
})
|
})
|
||||||
@ApiNotFoundResponse({ description: 'Song not found' })
|
@ApiNotFoundResponse({ description: "Song not found" })
|
||||||
@ApiOkResponse({ description: 'Returns the musicXML file succesfully' })
|
@ApiOkResponse({ description: "Returns the musicXML file succesfully" })
|
||||||
async getMusicXml(@Param('id', ParseIntPipe) id: number) {
|
async getMusicXml(@Param("id", ParseIntPipe) id: number) {
|
||||||
const song = await this.songService.song({ id });
|
const song = await this.songService.song({ id });
|
||||||
if (!song) throw new NotFoundException('Song not found');
|
if (!song) throw new NotFoundException("Song not found");
|
||||||
|
|
||||||
const file = createReadStream(song.musicXmlPath, { encoding: 'binary' });
|
const file = createReadStream(song.musicXmlPath, { encoding: "binary" });
|
||||||
return new StreamableFile(file);
|
return new StreamableFile(file);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Get(":id/assets/partition")
|
||||||
|
@ApiOperation({
|
||||||
|
description: "Streams the svg partition of the requested song",
|
||||||
|
})
|
||||||
|
@ApiNotFoundResponse({ description: "Song not found" })
|
||||||
|
@ApiOkResponse({ description: "Returns the svg partition succesfully" })
|
||||||
|
@Header("Cache-Control", "max-age=86400")
|
||||||
|
@Header("Content-Type", "image/svg+xml")
|
||||||
|
async getPartition(@Param("id", ParseIntPipe) id: number) {
|
||||||
|
const song = await this.songService.song({ id });
|
||||||
|
if (!song) throw new NotFoundException("Song not found");
|
||||||
|
|
||||||
|
// check if /data/cache/songs/id exists
|
||||||
|
if (!existsSync("/data/cache/songs/" + id + ".svg")) {
|
||||||
|
// if not, generate assets
|
||||||
|
await this.songService.createAssets(song.musicXmlPath, id);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const file = readFileSync("/data/cache/songs/" + id + ".svg");
|
||||||
|
return file.toString();
|
||||||
|
} catch {
|
||||||
|
throw new InternalServerErrorException();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get(":id/assets/cursors")
|
||||||
|
@ApiOperation({
|
||||||
|
description: "Streams the partition cursors of the requested song",
|
||||||
|
})
|
||||||
|
@ApiNotFoundResponse({ description: "Song not found" })
|
||||||
|
@ApiOkResponse({ description: "Returns the partition cursors succesfully" })
|
||||||
|
@Header("Cache-Control", "max-age=86400")
|
||||||
|
@Header("Content-Type", "application/json")
|
||||||
|
async getCursors(@Param("id", ParseIntPipe) id: number) {
|
||||||
|
const song = await this.songService.song({ id });
|
||||||
|
if (!song) throw new NotFoundException("Song not found");
|
||||||
|
|
||||||
|
// check if /data/cache/songs/id exists
|
||||||
|
if (!existsSync("/data/cache/songs/" + id + ".json")) {
|
||||||
|
// if not, generate assets
|
||||||
|
await this.songService.createAssets(song.musicXmlPath, id);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const file = readFileSync("/data/cache/songs/" + id + ".json");
|
||||||
|
return JSON.parse(file.toString());
|
||||||
|
} catch {
|
||||||
|
throw new InternalServerErrorException();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Post()
|
@Post()
|
||||||
@ApiOperation({
|
@ApiOperation({
|
||||||
description:
|
description:
|
||||||
'register a new song in the database, should not be used by the frontend',
|
"register a new song in the database, should not be used by the frontend",
|
||||||
})
|
})
|
||||||
async create(@Body() createSongDto: CreateSongDto) {
|
async create(@Body() createSongDto: CreateSongDto) {
|
||||||
try {
|
try {
|
||||||
@@ -148,13 +199,13 @@ export class SongController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Delete(':id')
|
@Delete(":id")
|
||||||
@ApiOperation({ description: 'delete a song by id' })
|
@ApiOperation({ description: "delete a song by id" })
|
||||||
async remove(@Param('id', ParseIntPipe) id: number) {
|
async remove(@Param("id", ParseIntPipe) id: number) {
|
||||||
try {
|
try {
|
||||||
return await this.songService.deleteSong({ id });
|
return await this.songService.deleteSong({ id });
|
||||||
} catch {
|
} catch {
|
||||||
throw new NotFoundException('Invalid ID');
|
throw new NotFoundException("Invalid ID");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -163,9 +214,9 @@ export class SongController {
|
|||||||
async findAll(
|
async findAll(
|
||||||
@Req() req: Request,
|
@Req() req: Request,
|
||||||
@FilterQuery(SongController.filterableFields) where: Prisma.SongWhereInput,
|
@FilterQuery(SongController.filterableFields) where: Prisma.SongWhereInput,
|
||||||
@Query('include') include: string,
|
@Query("include") include: string,
|
||||||
@Query('skip', new DefaultValuePipe(0), ParseIntPipe) skip: number,
|
@Query("skip", new DefaultValuePipe(0), ParseIntPipe) skip: number,
|
||||||
@Query('take', new DefaultValuePipe(20), ParseIntPipe) take: number,
|
@Query("take", new DefaultValuePipe(20), ParseIntPipe) take: number,
|
||||||
): Promise<Plage<Song>> {
|
): Promise<Plage<Song>> {
|
||||||
const ret = await this.songService.songs({
|
const ret = await this.songService.songs({
|
||||||
skip,
|
skip,
|
||||||
@@ -176,14 +227,14 @@ export class SongController {
|
|||||||
return new Plage(ret, req);
|
return new Plage(ret, req);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Get(':id')
|
@Get(":id")
|
||||||
@ApiOperation({ description: 'Get a specific song data' })
|
@ApiOperation({ description: "Get a specific song data" })
|
||||||
@ApiNotFoundResponse({ description: 'Song not found' })
|
@ApiNotFoundResponse({ description: "Song not found" })
|
||||||
@ApiOkResponse({ type: _Song, description: 'Requested song' })
|
@ApiOkResponse({ type: _Song, description: "Requested song" })
|
||||||
async findOne(
|
async findOne(
|
||||||
@Req() req: Request,
|
@Req() req: Request,
|
||||||
@Param('id', ParseIntPipe) id: number,
|
@Param("id", ParseIntPipe) id: number,
|
||||||
@Query('include') include: string,
|
@Query("include") include: string,
|
||||||
) {
|
) {
|
||||||
const res = await this.songService.song(
|
const res = await this.songService.song(
|
||||||
{
|
{
|
||||||
@@ -192,21 +243,22 @@ export class SongController {
|
|||||||
mapInclude(include, req, SongController.includableFields),
|
mapInclude(include, req, SongController.includableFields),
|
||||||
);
|
);
|
||||||
|
|
||||||
if (res === null) throw new NotFoundException('Song not found');
|
if (res === null) throw new NotFoundException("Song not found");
|
||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Get(':id/history')
|
@Get(":id/history")
|
||||||
@HttpCode(200)
|
@HttpCode(200)
|
||||||
|
@UseGuards(JwtAuthGuard)
|
||||||
@ApiOperation({
|
@ApiOperation({
|
||||||
description: 'get the history of the connected user on a specific song',
|
description: "get the history of the connected user on a specific song",
|
||||||
})
|
})
|
||||||
@ApiOkResponse({
|
@ApiOkResponse({
|
||||||
type: SongHistoryResult,
|
type: SongHistoryResult,
|
||||||
description: 'Records of previous games of the user',
|
description: "Records of previous games of the user",
|
||||||
})
|
})
|
||||||
@ApiUnauthorizedResponse({ description: 'Invalid token' })
|
@ApiUnauthorizedResponse({ description: "Invalid token" })
|
||||||
async getHistory(@Req() req: any, @Param('id', ParseIntPipe) id: number) {
|
async getHistory(@Req() req: any, @Param("id", ParseIntPipe) id: number) {
|
||||||
return this.historyService.getForSong({
|
return this.historyService.getForSong({
|
||||||
playerId: req.user.id,
|
playerId: req.user.id,
|
||||||
songId: id,
|
songId: id,
|
||||||
|
|||||||
@@ -1,11 +1,18 @@
|
|||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from "@nestjs/common";
|
||||||
import { Prisma, Song } from '@prisma/client';
|
import { Prisma, Song } from "@prisma/client";
|
||||||
import { PrismaService } from 'src/prisma/prisma.service';
|
import { PrismaService } from "src/prisma/prisma.service";
|
||||||
|
import { generateSongAssets } from "src/assetsgenerator/generateImages_browserless";
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class SongService {
|
export class SongService {
|
||||||
constructor(private prisma: PrismaService) {}
|
constructor(private prisma: PrismaService) {}
|
||||||
|
|
||||||
|
async createAssets(mxlPath: string, songId: number): Promise<void> {
|
||||||
|
// mxlPath can the path to an archive to an xml file or the path to the xml file directly
|
||||||
|
// const generateSongAssets = (await import("src/assetsgenerator/generateImages_browserless.mjs")).default;
|
||||||
|
return generateSongAssets(songId, mxlPath, "/data/cache/songs", "svg");
|
||||||
|
}
|
||||||
|
|
||||||
async songByArtist(data: number): Promise<Song[]> {
|
async songByArtist(data: number): Promise<Song[]> {
|
||||||
return this.prisma.song.findMany({
|
return this.prisma.song.findMany({
|
||||||
where: {
|
where: {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"module": "commonjs",
|
"module": "NodeNext",
|
||||||
"declaration": true,
|
"declaration": true,
|
||||||
"removeComments": true,
|
"removeComments": true,
|
||||||
"emitDecoratorMetadata": true,
|
"emitDecoratorMetadata": true,
|
||||||
@@ -16,6 +16,9 @@
|
|||||||
"noImplicitAny": false,
|
"noImplicitAny": false,
|
||||||
"strictBindCallApply": false,
|
"strictBindCallApply": false,
|
||||||
"forceConsistentCasingInFileNames": false,
|
"forceConsistentCasingInFileNames": false,
|
||||||
"noFallthroughCasesInSwitch": true
|
"noFallthroughCasesInSwitch": true,
|
||||||
|
"moduleResolution": "NodeNext",
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"allowJs": true,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user