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"
|
||||
}
|
||||
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/passport": "^1.0.12",
|
||||
"bcryptjs": "^2.4.3",
|
||||
"canvas": "^2.11.2",
|
||||
"class-transformer": "^0.5.1",
|
||||
"class-validator": "^0.13.2",
|
||||
"json-logger-service": "^9.0.1",
|
||||
"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",
|
||||
"nodemailer": "^6.9.5",
|
||||
"opensheetmusicdisplay": "^1.8.4",
|
||||
"passport-google-oauth20": "^2.0.0",
|
||||
"passport-jwt": "^4.0.1",
|
||||
"passport-local": "^1.0.0",
|
||||
@@ -81,7 +85,8 @@
|
||||
"moduleFileExtensions": [
|
||||
"js",
|
||||
"json",
|
||||
"ts"
|
||||
"ts",
|
||||
"mjs"
|
||||
],
|
||||
"rootDir": "src",
|
||||
"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,
|
||||
StreamableFile,
|
||||
UseGuards,
|
||||
} from '@nestjs/common';
|
||||
import { ApiOkResponsePlaginated, Plage } from 'src/models/plage';
|
||||
import { CreateSongDto } from './dto/create-song.dto';
|
||||
import { SongService } from './song.service';
|
||||
import { Request } from 'express';
|
||||
import { Prisma, Song } from '@prisma/client';
|
||||
import { createReadStream, existsSync } from 'fs';
|
||||
Header,
|
||||
} from "@nestjs/common";
|
||||
import { ApiOkResponsePlaginated, Plage } from "src/models/plage";
|
||||
import { CreateSongDto } from "./dto/create-song.dto";
|
||||
import { SongService } from "./song.service";
|
||||
import { Request } from "express";
|
||||
import { Prisma, Song } from "@prisma/client";
|
||||
import { createReadStream, existsSync, readFileSync } from "fs";
|
||||
import {
|
||||
ApiNotFoundResponse,
|
||||
ApiOkResponse,
|
||||
@@ -29,14 +30,13 @@ import {
|
||||
ApiProperty,
|
||||
ApiTags,
|
||||
ApiUnauthorizedResponse,
|
||||
} from '@nestjs/swagger';
|
||||
import { HistoryService } from 'src/history/history.service';
|
||||
import { JwtAuthGuard } from 'src/auth/jwt-auth.guard';
|
||||
import { FilterQuery } from 'src/utils/filter.pipe';
|
||||
import { Song as _Song } from 'src/_gen/prisma-class/song';
|
||||
import { SongHistory } from 'src/_gen/prisma-class/song_history';
|
||||
import { IncludeMap, mapInclude } from 'src/utils/include';
|
||||
import { Public } from 'src/auth/public';
|
||||
} from "@nestjs/swagger";
|
||||
import { HistoryService } from "src/history/history.service";
|
||||
import { JwtAuthGuard } from "src/auth/jwt-auth.guard";
|
||||
import { FilterQuery } from "src/utils/filter.pipe";
|
||||
import { Song as _Song } from "src/_gen/prisma-class/song";
|
||||
import { SongHistory } from "src/_gen/prisma-class/song_history";
|
||||
import { IncludeMap, mapInclude } from "src/utils/include";
|
||||
|
||||
class SongHistoryResult {
|
||||
@ApiProperty()
|
||||
@@ -45,16 +45,16 @@ class SongHistoryResult {
|
||||
history: SongHistory[];
|
||||
}
|
||||
|
||||
@Controller('song')
|
||||
@ApiTags('song')
|
||||
@Controller("song")
|
||||
@ApiTags("song")
|
||||
@UseGuards(JwtAuthGuard)
|
||||
export class SongController {
|
||||
static filterableFields: string[] = [
|
||||
'+id',
|
||||
'name',
|
||||
'+artistId',
|
||||
'+albumId',
|
||||
'+genreId',
|
||||
"+id",
|
||||
"name",
|
||||
"+artistId",
|
||||
"+albumId",
|
||||
"+genreId",
|
||||
];
|
||||
static includableFields: IncludeMap<Prisma.SongInclude> = {
|
||||
artist: true,
|
||||
@@ -69,36 +69,35 @@ export class SongController {
|
||||
private readonly historyService: HistoryService,
|
||||
) {}
|
||||
|
||||
@Get(':id/midi')
|
||||
@ApiOperation({ description: 'Streams the midi file of the requested song' })
|
||||
@ApiNotFoundResponse({ description: 'Song not found' })
|
||||
@ApiOkResponse({ description: 'Returns the midi file succesfully' })
|
||||
async getMidi(@Param('id', ParseIntPipe) id: number) {
|
||||
@Get(":id/midi")
|
||||
@ApiOperation({ description: "Streams the midi file of the requested song" })
|
||||
@ApiNotFoundResponse({ description: "Song not found" })
|
||||
@ApiOkResponse({ description: "Returns the midi file succesfully" })
|
||||
async getMidi(@Param("id", ParseIntPipe) id: number) {
|
||||
const song = await this.songService.song({ id });
|
||||
if (!song) throw new NotFoundException('Song not found');
|
||||
if (!song) throw new NotFoundException("Song not found");
|
||||
|
||||
try {
|
||||
const file = createReadStream(song.midiPath);
|
||||
return new StreamableFile(file, { type: 'audio/midi' });
|
||||
return new StreamableFile(file, { type: "audio/midi" });
|
||||
} catch {
|
||||
throw new InternalServerErrorException();
|
||||
}
|
||||
}
|
||||
|
||||
@Get(':id/illustration')
|
||||
@Get(":id/illustration")
|
||||
@ApiOperation({
|
||||
description: 'Streams the illustration of the requested song',
|
||||
description: "Streams the illustration of the requested song",
|
||||
})
|
||||
@ApiNotFoundResponse({ description: 'Song not found' })
|
||||
@ApiOkResponse({ description: 'Returns the illustration succesfully' })
|
||||
@Public()
|
||||
async getIllustration(@Param('id', ParseIntPipe) id: number) {
|
||||
@ApiNotFoundResponse({ description: "Song not found" })
|
||||
@ApiOkResponse({ description: "Returns the illustration succesfully" })
|
||||
async getIllustration(@Param("id", ParseIntPipe) id: number) {
|
||||
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 (!existsSync(song.illustrationPath))
|
||||
throw new NotFoundException('Illustration not found');
|
||||
throw new NotFoundException("Illustration not found");
|
||||
|
||||
try {
|
||||
const file = createReadStream(song.illustrationPath);
|
||||
@@ -108,24 +107,76 @@ export class SongController {
|
||||
}
|
||||
}
|
||||
|
||||
@Get(':id/musicXml')
|
||||
@Get(":id/musicXml")
|
||||
@ApiOperation({
|
||||
description: 'Streams the musicXML file of the requested song',
|
||||
description: "Streams the musicXML file of the requested song",
|
||||
})
|
||||
@ApiNotFoundResponse({ description: 'Song not found' })
|
||||
@ApiOkResponse({ description: 'Returns the musicXML file succesfully' })
|
||||
async getMusicXml(@Param('id', ParseIntPipe) id: number) {
|
||||
@ApiNotFoundResponse({ description: "Song not found" })
|
||||
@ApiOkResponse({ description: "Returns the musicXML file succesfully" })
|
||||
async getMusicXml(@Param("id", ParseIntPipe) id: number) {
|
||||
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);
|
||||
}
|
||||
|
||||
@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()
|
||||
@ApiOperation({
|
||||
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) {
|
||||
try {
|
||||
@@ -148,13 +199,13 @@ export class SongController {
|
||||
}
|
||||
}
|
||||
|
||||
@Delete(':id')
|
||||
@ApiOperation({ description: 'delete a song by id' })
|
||||
async remove(@Param('id', ParseIntPipe) id: number) {
|
||||
@Delete(":id")
|
||||
@ApiOperation({ description: "delete a song by id" })
|
||||
async remove(@Param("id", ParseIntPipe) id: number) {
|
||||
try {
|
||||
return await this.songService.deleteSong({ id });
|
||||
} catch {
|
||||
throw new NotFoundException('Invalid ID');
|
||||
throw new NotFoundException("Invalid ID");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -163,9 +214,9 @@ export class SongController {
|
||||
async findAll(
|
||||
@Req() req: Request,
|
||||
@FilterQuery(SongController.filterableFields) where: Prisma.SongWhereInput,
|
||||
@Query('include') include: string,
|
||||
@Query('skip', new DefaultValuePipe(0), ParseIntPipe) skip: number,
|
||||
@Query('take', new DefaultValuePipe(20), ParseIntPipe) take: number,
|
||||
@Query("include") include: string,
|
||||
@Query("skip", new DefaultValuePipe(0), ParseIntPipe) skip: number,
|
||||
@Query("take", new DefaultValuePipe(20), ParseIntPipe) take: number,
|
||||
): Promise<Plage<Song>> {
|
||||
const ret = await this.songService.songs({
|
||||
skip,
|
||||
@@ -176,14 +227,14 @@ export class SongController {
|
||||
return new Plage(ret, req);
|
||||
}
|
||||
|
||||
@Get(':id')
|
||||
@ApiOperation({ description: 'Get a specific song data' })
|
||||
@ApiNotFoundResponse({ description: 'Song not found' })
|
||||
@ApiOkResponse({ type: _Song, description: 'Requested song' })
|
||||
@Get(":id")
|
||||
@ApiOperation({ description: "Get a specific song data" })
|
||||
@ApiNotFoundResponse({ description: "Song not found" })
|
||||
@ApiOkResponse({ type: _Song, description: "Requested song" })
|
||||
async findOne(
|
||||
@Req() req: Request,
|
||||
@Param('id', ParseIntPipe) id: number,
|
||||
@Query('include') include: string,
|
||||
@Param("id", ParseIntPipe) id: number,
|
||||
@Query("include") include: string,
|
||||
) {
|
||||
const res = await this.songService.song(
|
||||
{
|
||||
@@ -192,21 +243,22 @@ export class SongController {
|
||||
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;
|
||||
}
|
||||
|
||||
@Get(':id/history')
|
||||
@Get(":id/history")
|
||||
@HttpCode(200)
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@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({
|
||||
type: SongHistoryResult,
|
||||
description: 'Records of previous games of the user',
|
||||
description: "Records of previous games of the user",
|
||||
})
|
||||
@ApiUnauthorizedResponse({ description: 'Invalid token' })
|
||||
async getHistory(@Req() req: any, @Param('id', ParseIntPipe) id: number) {
|
||||
@ApiUnauthorizedResponse({ description: "Invalid token" })
|
||||
async getHistory(@Req() req: any, @Param("id", ParseIntPipe) id: number) {
|
||||
return this.historyService.getForSong({
|
||||
playerId: req.user.id,
|
||||
songId: id,
|
||||
|
||||
@@ -1,11 +1,18 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { Prisma, Song } from '@prisma/client';
|
||||
import { PrismaService } from 'src/prisma/prisma.service';
|
||||
import { Injectable } from "@nestjs/common";
|
||||
import { Prisma, Song } from "@prisma/client";
|
||||
import { PrismaService } from "src/prisma/prisma.service";
|
||||
import { generateSongAssets } from "src/assetsgenerator/generateImages_browserless";
|
||||
|
||||
@Injectable()
|
||||
export class SongService {
|
||||
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[]> {
|
||||
return this.prisma.song.findMany({
|
||||
where: {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"module": "commonjs",
|
||||
"module": "NodeNext",
|
||||
"declaration": true,
|
||||
"removeComments": true,
|
||||
"emitDecoratorMetadata": true,
|
||||
@@ -16,6 +16,9 @@
|
||||
"noImplicitAny": false,
|
||||
"strictBindCallApply": false,
|
||||
"forceConsistentCasingInFileNames": false,
|
||||
"noFallthroughCasesInSwitch": true
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"moduleResolution": "NodeNext",
|
||||
"esModuleInterop": true,
|
||||
"allowJs": true,
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user