Merge pull request #4 from absoluteSpacehead/master

Add preview streams for songs that need it
This commit is contained in:
mc 2024-02-07 07:56:40 +01:00 committed by GitHub
commit 7cb0bfc7a8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 274 additions and 24 deletions

View File

@ -1,10 +1,11 @@
import { DataSource } from "typeorm";
import { DataSource, IsNull } from "typeorm";
import { ENVIRONMENT, SAVED_DATA_PATH } from "../Modules/Constants";
import { Song } from "../Schemas/Song";
import { ForcedCategory } from "../Schemas/ForcedCategory";
import { User } from "../Schemas/User";
import { Rating } from "../Schemas/Rating";
import { DiscordRole } from "../Schemas/DiscordRole";
import { Debug } from "../Modules/Logger";
export const DBSource = new DataSource({
type: "better-sqlite3",
@ -26,6 +27,21 @@ export const DBSource = new DataSource({
(async () => {
await DBSource.initialize();
// Look for songs without a PID here so we can resolve problems before we do anything else
const SongsWithNoPID = await Song.find({ where: { PID: IsNull() } });
Debug(`We have ${SongsWithNoPID.length} song${SongsWithNoPID.length != 1 ? "s" : ""} with no PID`);
SongsWithNoPID.forEach(async (Song) => {
Debug(`Fixing up ${Song.Name} PID`);
// Existing songs that actually need separate PIDs (> 2 channels) will need to have their audio reuploaded entirely
// This is faster than checking to see if they all actually need one though...
Song.PID = Song.ID;
await Song.save();
Debug(`${Song.Name} PID is now ${Song.PID} to match ${Song.ID}`);
})
})();
/*

View File

@ -111,7 +111,7 @@ export async function GenerateFortnitePages(ForUser: User | null): Promise<{ Suc
siv: "Vocals", // siv - Vocals ID to use (only Vocals possible)
qi: JSON.stringify({ // qi - Query Information (frontend related display stuff and language vocals channel related stuff)
sid: Song.ID, // sid - Song UUID
pid: Song.ID, // pid - Playlist Asset ID
pid: Song.PID, // pid - Playlist Asset ID
title: OriginalTrack._title, // title - Song Name - same as _title
tracks: [
{

View File

@ -14,9 +14,12 @@ App.get("/song/download/:InternalID/:File",
RequireAuthentication(),
async (req, res) => {
//const Song = AvailableFestivalSongs.find(x => x.UUID === req.params.SongUUID);
const SongData = await Song.findOne({ where: { ID: req.params.InternalID }, relations: { Author: true } });
const SongData = await Song.findOne({ where: [ { ID: req.params.InternalID}, { PID: req.params.InternalID } ], relations: { Author: true } });
if (!SongData)
return res.status(404).send("Song not found.");
const IsPreview = SongData.ID != SongData.PID && req.params.InternalID == SongData.PID;
const ManifestPath = `${SongData.Directory}/${IsPreview ? `PreviewManifest.mpd` : `Manifest.mpd`}`;
if (SongData.IsDraft && (req.user!.PermissionLevel! < UserPermissions.VerifiedUser && SongData.Author.ID !== req.user!.ID))
return res.status(403).send("You cannot use this track, because it's a draft.");
@ -32,7 +35,7 @@ async (req, res) => {
type: "main",
language: "en",
url: `${BaseURL}master.blurl`,
data: readFileSync(`${SongData.Directory}/Manifest.mpd`).toString().replaceAll("{BASEURL}", BaseURL)
data: readFileSync(ManifestPath).toString().replaceAll("{BASEURL}", BaseURL)
}
],
type: "vod",
@ -42,7 +45,7 @@ async (req, res) => {
case "manifest":
case "manifest.mpd":
return res.set("content-type", "application/dash+xml").send(Buffer.from(readFileSync(`${SongData.Directory}/Manifest.mpd`).toString().replaceAll("{BASEURL}", BaseURL)));
return res.set("content-type", "application/dash+xml").send(Buffer.from(readFileSync(ManifestPath).toString().replaceAll("{BASEURL}", BaseURL)));
case "cover":
case "cover.png":
@ -65,27 +68,30 @@ async (req, res) => {
if (!req.params.File.endsWith(".m4s") && !req.params.File.endsWith(".webm"))
return res.sendStatus(403);
if (!existsSync(`${SongData.Directory}/Chunks/${req.params.File}`))
const ChunkPath = `${SongData.Directory}/${IsPreview ? `PreviewChunks` : `Chunks`}/${req.params.File}`
if (!existsSync(ChunkPath))
return res.sendStatus(404);
res.set("content-type", "video/mp4")
res.send(readFileSync(`${SongData.Directory}/Chunks/${req.params.File}`));
res.send(readFileSync(ChunkPath));
});
App.get("/:InternalID",
RequireAuthentication(),
async (req, res, next) => {
const SongData = await Song.findOne({ where: { ID: req.params.InternalID }, relations: { Author: true } });
const SongData = await Song.findOne({ where: [ { ID: req.params.InternalID }, { PID: req.params.InternalID } ], relations: { Author: true } });
if (!SongData)
return next(); // trust me bro
const IsPreview = SongData.ID != SongData.PID && req.params.InternalID == SongData.PID;
if (SongData.IsDraft && ((req.user ? req.user.PermissionLevel < UserPermissions.VerifiedUser : true) && SongData.Author.ID !== req.user!.ID))
return res.status(403).send("You cannot use this track, because it's a draft.");
const BaseURL = `${FULL_SERVER_ROOT}/song/download/${SongData.ID}/`;
const BaseURL = `${FULL_SERVER_ROOT}/song/download/${IsPreview ? SongData.PID : SongData.ID}/`;
res.set("content-type", "application/json");
res.json({
playlist: Buffer.from(readFileSync(`${SongData.Directory}/Manifest.mpd`).toString().replaceAll("{BASEURL}", BaseURL)).toString("base64"),
playlist: Buffer.from(readFileSync(`${SongData.Directory}/${IsPreview ? `PreviewManifest.mpd` : `Manifest.mpd`}`).toString().replaceAll("{BASEURL}", BaseURL)).toString("base64"),
playlistType: "application/dash+xml",
metadata: {
assetId: "",

View File

@ -8,7 +8,8 @@ import { Song, SongStatus } from "../Schemas/Song";
import { Debug } from "../Modules/Logger";
import { magenta } from "colorette";
import { fromBuffer } from "file-type";
import { rmSync, writeFileSync, renameSync, readFileSync } from "fs";
import { v4 } from "uuid";
import { rmSync, writeFileSync, renameSync, readFileSync, existsSync, mkdirSync } from "fs";
import { FULL_SERVER_ROOT, MAX_AMOUNT_OF_DRAFTS_AT_ONCE, SAVED_DATA_PATH } from "../Modules/Constants";
import { UserPermissions } from "../Schemas/User";
@ -182,45 +183,114 @@ App.post("/upload/audio",
if (req.user!.PermissionLevel! < UserPermissions.Administrator && SongData.Author.ID !== req.user!.ID)
return res.status(403).send("You don't have permission to upload to this song.");
const ChunksPath = `${SAVED_DATA_PATH}/Songs/${req.body.TargetSong}/Chunks`;
if (SongData.HasAudio) {
if (SongData.Status !== SongStatus.BROKEN && SongData.Status !== SongStatus.DEFAULT && SongData.Status !== SongStatus.DENIED && SongData.Status !== SongStatus.PUBLIC)
return res.status(400).send("You cannot update this song at this moment.");
rmSync(`${SAVED_DATA_PATH}/Songs/${req.body.TargetSong}/Chunks`, { recursive: true });
rmSync(ChunksPath, { recursive: true });
SongData.HasAudio = false;
SongData.IsDraft = true;
SongData.Status = SongStatus.PROCESSING;
await SongData.save();
}
await writeFileSync(`${SAVED_DATA_PATH}/Songs/${req.body.TargetSong}/Audio.${ext}`, Decoded);
if (!existsSync(ChunksPath))
mkdirSync(ChunksPath);
const AudioPath = `${SAVED_DATA_PATH}/Songs/${req.body.TargetSong}`;
await writeFileSync(`${AudioPath}/Audio.${ext}`, Decoded);
ffmpeg()
.input(`${SAVED_DATA_PATH}/Songs/${req.body.TargetSong}/Audio.${ext}`)
.input(`${AudioPath}/Audio.${ext}`)
.audioCodec("libopus")
.outputOptions([
"-use_timeline 1",
"-f dash",
"-mapping_family 255"
])
.output(`${SAVED_DATA_PATH}/Songs/${req.body.TargetSong}/Chunks/Manifest.mpd`)
.output(`${AudioPath}/Chunks/Manifest.mpd`)
.on("start", cl => Debug(`ffmpeg running with ${magenta(cl)}`))
.on("end", async () => {
Debug("Ffmpeg finished running");
rmSync(`${SAVED_DATA_PATH}/Songs/${req.body.TargetSong}/Audio.${ext}`);
renameSync(`${SAVED_DATA_PATH}/Songs/${req.body.TargetSong}/Chunks/Manifest.mpd`, `${SAVED_DATA_PATH}/Songs/${req.body.TargetSong}/Manifest.mpd`);
// i love creating thread-safe code that always works! (never gonna error trust me)
writeFileSync(`${SAVED_DATA_PATH}/Songs/${req.body.TargetSong}/Manifest.mpd`, readFileSync(`${SAVED_DATA_PATH}/Songs/${req.body.TargetSong}/Manifest.mpd`).toString().replace(/<ProgramInformation>[\w\d\r\n\t]*<\/ProgramInformation>/i, "<BaseURL>{BASEURL}</BaseURL>"));
// Check channels
ffmpeg.ffprobe(`${AudioPath}/Audio.${ext}`, async (err, metadata) => {
if (err) // FUCK
return console.log(err);
if (metadata.streams[0].codec_type == "audio" && metadata.streams[0].channels! > 2)
{
Debug("Creating preview stream as it's needed...");
// Oh shit!! we need a preview stream!! so let's make one.
await SongData.reload();
SongData.HasAudio = true;
await SongData.save();
// Make a dir for it first
if (!existsSync(`${AudioPath}/PreviewChunks`))
mkdirSync(`${AudioPath}/PreviewChunks`, { recursive: true });
// Then, figure out which channels from the original file to put into each channel on the output file.
// We already ran ffprobe earlier so we can just reuse that lol
// Output with 10 channels is "pan=stereo|c0=c0+c2+c4+c6+c8|c1=c1+c3+c5+c7+c9", ffmpeg uses this to decide how to downmix
var FilterLeft = "pan=stereo|c0=";
var FilterRight = "|c1=";
for (var i = 0; i < metadata.streams[0].channels!; i++)
{
if (i == 0 || i % 2 == 0)
FilterLeft += `${i == 0 ? "" : "+"}c${i}` // out for 0 = "c0", out for 2 = "+c2"
else
FilterRight += `${i == 1 ? "" : "+"}c${i}` // out for 1 = "c1", out for 3 = "+c3"
}
// Need to wait for this before removing the source file, but since it won't ALWAYS get called we don't put the rmSync in here
await new Promise<void>((resolve, reject) => {
ffmpeg()
.input(`${AudioPath}/Audio.${ext}`)
.audioCodec("libopus")
.audioFilter(FilterLeft + FilterRight)
.outputOptions([
"-use_timeline 1",
"-f dash",
"-ac 2", // downmix
])
.output(`${AudioPath}/PreviewChunks/PreviewManifest.mpd`)
.on("start", cl => Debug(`Creating preview stream with ${magenta(cl)}`))
.on("end", async () => {
Debug("Preview stream created");
// Move the mpd out
renameSync(`${AudioPath}/PreviewChunks/PreviewManifest.mpd`, `${AudioPath}/PreviewManifest.mpd`);
writeFileSync(`${AudioPath}/PreviewManifest.mpd`, readFileSync(`${AudioPath}/PreviewManifest.mpd`).toString().replace(/<ProgramInformation>[\w\d\r\n\t]*<\/ProgramInformation>/i, "<BaseURL>{BASEURL}</BaseURL>"));
SongData.PID = v4();
await SongData.save();
resolve();
})
.on("error", async (e, stdout, stderr) => {
console.error(e);
console.log(stdout);
console.error(stderr);
reject(e);
}).run();
});
}
rmSync(`${AudioPath}/Audio.${ext}`);
renameSync(`${AudioPath}/Chunks/Manifest.mpd`, `${AudioPath}/Manifest.mpd`);
// i love creating thread-safe code that always works! (never gonna error trust me)
writeFileSync(`${AudioPath}/Manifest.mpd`, readFileSync(`${AudioPath}/Manifest.mpd`).toString().replace(/<ProgramInformation>[\w\d\r\n\t]*<\/ProgramInformation>/i, "<BaseURL>{BASEURL}</BaseURL>"));
await SongData.reload();
SongData.HasAudio = true;
await SongData.save();
});
})
.on("error", async (e, stdout, stderr) => {
console.error(e);
console.log(stdout);
console.error(stderr);
rmSync(`${SAVED_DATA_PATH}/Songs/${req.body.TargetSong}/Audio.${ext}`);
rmSync(`${AudioPath}/Audio.${ext}`);
await SongData.reload();
SongData.Status = SongStatus.BROKEN;

View File

@ -21,6 +21,9 @@ export class Song extends BaseEntity {
@PrimaryGeneratedColumn("uuid")
ID: string;
@Column("uuid", { nullable: true })
PID?: string;
@ManyToOne(() => User, U => U.CreatedTracks)
Author: User;
@ -102,6 +105,10 @@ export class Song extends BaseEntity {
@BeforeInsert()
Setup() {
this.ID = v4();
if (this.PID === undefined) // im lazy but this will work regardless
this.PID = this.ID; // By default they should be the same to save space, if we *need* a preview stream later we can change this when processing audio
this.Directory = `${SAVED_DATA_PATH}/Songs/${this.ID}`;
if (!existsSync(join(this.Directory, "Chunks")))
mkdirSync(join(this.Directory, "Chunks"), { recursive: true });

152
Server/package-lock.json generated
View File

@ -23,6 +23,7 @@
"joi": "^17.12.0",
"jsonwebtoken": "^9.0.2",
"node-cron": "^3.0.3",
"ts-node": "^10.9.2",
"typeorm": "^0.3.19",
"underscore": "^1.13.6",
"uuid": "^9.0.1"
@ -40,6 +41,17 @@
"typescript": "^5.2.2"
}
},
"node_modules/@cspotcode/source-map-support": {
"version": "0.8.1",
"resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz",
"integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==",
"dependencies": {
"@jridgewell/trace-mapping": "0.3.9"
},
"engines": {
"node": ">=12"
}
},
"node_modules/@discordjs/builders": {
"version": "1.7.0",
"resolved": "https://registry.npmjs.org/@discordjs/builders/-/builders-1.7.0.tgz",
@ -175,6 +187,28 @@
"node": ">=12"
}
},
"node_modules/@jridgewell/resolve-uri": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.1.tgz",
"integrity": "sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA==",
"engines": {
"node": ">=6.0.0"
}
},
"node_modules/@jridgewell/sourcemap-codec": {
"version": "1.4.15",
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz",
"integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg=="
},
"node_modules/@jridgewell/trace-mapping": {
"version": "0.3.9",
"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz",
"integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==",
"dependencies": {
"@jridgewell/resolve-uri": "^3.0.3",
"@jridgewell/sourcemap-codec": "^1.4.10"
}
},
"node_modules/@pkgjs/parseargs": {
"version": "0.11.0",
"resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz",
@ -242,6 +276,26 @@
"resolved": "https://registry.npmjs.org/@tokenizer/token/-/token-0.3.0.tgz",
"integrity": "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A=="
},
"node_modules/@tsconfig/node10": {
"version": "1.0.9",
"resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.9.tgz",
"integrity": "sha512-jNsYVVxU8v5g43Erja32laIDHXeoNvFEpX33OK4d6hljo3jDhCBDhx5dhCCTMWUojscpAagGiRkBKxpdl9fxqA=="
},
"node_modules/@tsconfig/node12": {
"version": "1.0.11",
"resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz",
"integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag=="
},
"node_modules/@tsconfig/node14": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz",
"integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow=="
},
"node_modules/@tsconfig/node16": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz",
"integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA=="
},
"node_modules/@types/body-parser": {
"version": "1.19.3",
"resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.3.tgz",
@ -420,6 +474,25 @@
"node": ">= 0.6"
}
},
"node_modules/acorn": {
"version": "8.11.3",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz",
"integrity": "sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==",
"bin": {
"acorn": "bin/acorn"
},
"engines": {
"node": ">=0.4.0"
}
},
"node_modules/acorn-walk": {
"version": "8.3.2",
"resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.2.tgz",
"integrity": "sha512-cjkyv4OtNCIeqhHrfS81QWXoCBPExR/J62oyEqepVw8WaQeSqpW2uhuLPh1m9eWhDuOo/jUXVTlifvesOWp/4A==",
"engines": {
"node": ">=0.4.0"
}
},
"node_modules/ansi-regex": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz",
@ -458,6 +531,11 @@
"node": ">= 6.0.0"
}
},
"node_modules/arg": {
"version": "4.1.3",
"resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz",
"integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA=="
},
"node_modules/array-flatten": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
@ -901,6 +979,11 @@
"node": ">= 0.10"
}
},
"node_modules/create-require": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz",
"integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ=="
},
"node_modules/cross-spawn": {
"version": "7.0.3",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz",
@ -974,6 +1057,14 @@
"node": ">=8"
}
},
"node_modules/diff": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz",
"integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==",
"engines": {
"node": ">=0.3.1"
}
},
"node_modules/discord-api-types": {
"version": "0.37.61",
"resolved": "https://registry.npmjs.org/discord-api-types/-/discord-api-types-0.37.61.tgz",
@ -1549,6 +1640,11 @@
"resolved": "https://registry.npmjs.org/magic-bytes.js/-/magic-bytes.js-1.8.0.tgz",
"integrity": "sha512-lyWpfvNGVb5lu8YUAbER0+UMBTdR63w2mcSUlhhBTyVbxJvjgqwyAf3AZD6MprgK0uHuBoWXSDAMWLupX83o3Q=="
},
"node_modules/make-error": {
"version": "1.3.6",
"resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz",
"integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw=="
},
"node_modules/media-typer": {
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
@ -2368,6 +2464,48 @@
"resolved": "https://registry.npmjs.org/ts-mixer/-/ts-mixer-6.0.3.tgz",
"integrity": "sha512-k43M7uCG1AkTyxgnmI5MPwKoUvS/bRvLvUb7+Pgpdlmok8AoqmUaZxUUw8zKM5B1lqZrt41GjYgnvAi0fppqgQ=="
},
"node_modules/ts-node": {
"version": "10.9.2",
"resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz",
"integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==",
"dependencies": {
"@cspotcode/source-map-support": "^0.8.0",
"@tsconfig/node10": "^1.0.7",
"@tsconfig/node12": "^1.0.7",
"@tsconfig/node14": "^1.0.0",
"@tsconfig/node16": "^1.0.2",
"acorn": "^8.4.1",
"acorn-walk": "^8.1.1",
"arg": "^4.1.0",
"create-require": "^1.1.0",
"diff": "^4.0.1",
"make-error": "^1.1.1",
"v8-compile-cache-lib": "^3.0.1",
"yn": "3.1.1"
},
"bin": {
"ts-node": "dist/bin.js",
"ts-node-cwd": "dist/bin-cwd.js",
"ts-node-esm": "dist/bin-esm.js",
"ts-node-script": "dist/bin-script.js",
"ts-node-transpile-only": "dist/bin-transpile.js",
"ts-script": "dist/bin-script-deprecated.js"
},
"peerDependencies": {
"@swc/core": ">=1.2.50",
"@swc/wasm": ">=1.2.50",
"@types/node": "*",
"typescript": ">=2.7"
},
"peerDependenciesMeta": {
"@swc/core": {
"optional": true
},
"@swc/wasm": {
"optional": true
}
}
},
"node_modules/tslib": {
"version": "2.6.2",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz",
@ -2526,7 +2664,6 @@
"version": "5.2.2",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.2.2.tgz",
"integrity": "sha512-mI4WrpHsbCIcwT9cF4FZvr80QUeKvsUsUvKDoR+X/7XHQH98xYD8YHZg7ANtz2GtZt/CBq2QJ0thkGJMHfqc1w==",
"dev": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
@ -2589,6 +2726,11 @@
"uuid": "dist/bin/uuid"
}
},
"node_modules/v8-compile-cache-lib": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz",
"integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg=="
},
"node_modules/vary": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
@ -2791,6 +2933,14 @@
"engines": {
"node": ">=8"
}
},
"node_modules/yn": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz",
"integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==",
"engines": {
"node": ">=6"
}
}
}
}

View File

@ -55,6 +55,7 @@
"joi": "^17.12.0",
"jsonwebtoken": "^9.0.2",
"node-cron": "^3.0.3",
"ts-node": "^10.9.2",
"typeorm": "^0.3.19",
"underscore": "^1.13.6",
"uuid": "^9.0.1"