From b97048d3fb452dce61cfd896f1be4f49002f4dcf Mon Sep 17 00:00:00 2001
From: McMistrzYT <56406996+McMistrzYT@users.noreply.github.com>
Date: Sun, 28 Jan 2024 16:54:32 +0100
Subject: [PATCH] i dont remember what i did here
---
Server/.example.env | 6 +-
Server/Source/Modules/Constants.ts | 1 +
Server/Source/Modules/Middleware.ts | 11 ++
Server/Source/Routes/Admin.ts | 25 +++-
Server/Source/Routes/Authentication.ts | 36 +++--
Server/Source/Routes/Drafting.ts | 143 +++++++++++++++++++
Server/Source/Schemas/Song.ts | 22 ++-
Server/Source/Schemas/User.ts | 3 +
Server/package-lock.json | 81 +++++++++++
Server/package.json | 1 +
package.json | 8 +-
src/App.tsx | 2 +
src/components/SiteHeader.tsx | 3 +-
src/routes/AdminCreateTrack.tsx | 2 +-
src/routes/AdminTrackList.tsx | 2 +-
src/routes/TrackSubmission.tsx | 181 +++++++++++++++++++++++++
16 files changed, 499 insertions(+), 28 deletions(-)
create mode 100644 Server/Source/Routes/Drafting.ts
create mode 100644 src/routes/TrackSubmission.tsx
diff --git a/Server/.example.env b/Server/.example.env
index a2a176c..c6b325d 100644
--- a/Server/.example.env
+++ b/Server/.example.env
@@ -7,6 +7,8 @@ ENVIRONMENT=dev
COOKIE_SIGN_KEY=
ADMIN_KEY=
BOT_TOKEN=
+USE_HTTPS=true # set this to false if you're debugging on your computer
+
+DISCORD_SERVER_ID=
DISCORD_CLIENT_ID=
-DISCORD_CLIENT_SECRET=
-USE_HTTPS=true # set this to false if you're debugging on your computer
\ No newline at end of file
+DISCORD_CLIENT_SECRET=
\ No newline at end of file
diff --git a/Server/Source/Modules/Constants.ts b/Server/Source/Modules/Constants.ts
index ae9c90d..4b4a0f5 100644
--- a/Server/Source/Modules/Constants.ts
+++ b/Server/Source/Modules/Constants.ts
@@ -17,5 +17,6 @@ export const COOKIE_SIGN_KEY = process.env.COOKIE_SIGN_KEY; // Secret that will
export const ADMIN_KEY = process.env.ADMIN_KEY; // Secret that will be required to sign into the Admin Dashboard.
export const JWT_KEY = process.env.JWT_KEY; // Secret that will be required to sign JSON Web Tokens (JWTs).
export const BOT_TOKEN = process.env.BOT_TOKEN; // Used for Discord-related stuff.
+export const DISCORD_SERVER_ID = process.env.DISCORD_SERVER_ID; // Server ID to check roles for permissions in.
export const DISCORD_CLIENT_ID = process.env.DISCORD_CLIENT_ID; // Client ID for authentication and checking what role you have on the Discord server.
export const DISCORD_CLIENT_SECRET = process.env.DISCORD_CLIENT_SECRET; // Client secret for authentication and checking what role you have on the Discord server.
\ No newline at end of file
diff --git a/Server/Source/Modules/Middleware.ts b/Server/Source/Modules/Middleware.ts
index a7a0b17..8936133 100644
--- a/Server/Source/Modules/Middleware.ts
+++ b/Server/Source/Modules/Middleware.ts
@@ -44,4 +44,15 @@ export function ValidateBody(Schema: j.Schema) {
res.status(400).json({ errorMessage: "Body validation failed.", details: err })
}
}
+}
+
+export function ValidateQuery(Schema: j.Schema) {
+ return async (req: Request, res: Response, next: NextFunction) => {
+ try {
+ req.query = await Schema.validateAsync(req.query);
+ next();
+ } catch (err) {
+ res.status(400).json({ errorMessage: "Query validation failed.", details: err })
+ }
+ }
}
\ No newline at end of file
diff --git a/Server/Source/Routes/Admin.ts b/Server/Source/Routes/Admin.ts
index 00c3dc9..49d3217 100644
--- a/Server/Source/Routes/Admin.ts
+++ b/Server/Source/Routes/Admin.ts
@@ -6,6 +6,7 @@ import { Song } from "../Schemas/Song";
import { RequireAuthentication, ValidateBody } from "../Modules/Middleware";
import { writeFileSync } from "fs";
import { ForcedCategory } from "../Schemas/ForcedCategory";
+import { fromBuffer } from "file-type";
import ffmpeg from "fluent-ffmpeg";
import exif from "exif-reader";
import j from "joi";
@@ -52,7 +53,7 @@ ValidateBody(j.object({
VocalsDifficulty: j.number().required().min(0).max(7)
})),
async (req, res) => {
- res.json(await Song.create(req.body).save())
+ res.json(await Song.create({ ...req.body, Draft: false, Author: req.user! }).save())
});
App.post("/upload/midi",
@@ -63,7 +64,7 @@ ValidateBody(j.object({
async (req, res) => {
const Decoded = Buffer.from(req.body.Data, "hex");
- if (!Decoded.toString().startsWith("MThd"))
+ if ((await fromBuffer(Decoded))?.ext !== "mid")
return res.status(400).send("Uploaded MIDI file is not a valid MIDI.");
if (!await Song.exists({ where: { ID: req.body.TargetSong } }))
@@ -80,9 +81,19 @@ ValidateBody(j.object({
})),
async (req, res) => {
const Decoded = Buffer.from(req.body.Data, "hex");
+ const ext = (await fromBuffer(Decoded))!.ext;
+
+ if (!["mp3", "m4a", "ogg", "wav"].includes(ext))
+ return res.status(404).send("Invalid audio file. (supported: mp3, m4a, ogg, wav)");
if (!await Song.exists({ where: { ID: req.body.TargetSong } }))
return res.status(404).send("The song you're trying to upload audio for does not exist.");
+
+ // TODO: implement checks for this
+ writeFileSync(`./Saved/Songs/${req.body.TargetSong}/Audio.${ext}`, Decoded);
+
+ ffmpeg()
+ .input("")
})
App.post("/upload/cover",
@@ -92,12 +103,16 @@ ValidateBody(j.object({
})),
async (req, res) => {
const Decoded = Buffer.from(req.body.Data, "hex");
+ const ext = (await fromBuffer(Decoded))!.ext;
+
+ if (ext !== "png")
+ return res.status(404).send("Invalid image file. (supported: png)");
if (!await Song.exists({ where: { ID: req.body.TargetSong } }))
return res.status(404).send("The song you're trying to upload a cover for does not exist.");
- try { // todo: fix
- /*const ImageMetadata = exif(Decoded);
+ try {
+ const ImageMetadata = exif(Decoded);
if (!ImageMetadata.Image?.ImageWidth || !ImageMetadata.Image?.ImageLength)
throw new Error("Invalid image file.");
@@ -105,7 +120,7 @@ async (req, res) => {
return res.status(400).send("Image must have a 1:1 ratio.");
if (ImageMetadata.Image.ImageWidth < 512 || ImageMetadata.Image.ImageWidth > 2048)
- return res.status(400).send("Image cannot be smaller than 512 pixels and larger than 2048 pixels.");*/
+ return res.status(400).send("Image cannot be smaller than 512 pixels and larger than 2048 pixels.");
} catch (err) {
console.error(err)
return res.status(400).send("Invalid image file.");
diff --git a/Server/Source/Routes/Authentication.ts b/Server/Source/Routes/Authentication.ts
index ea25cfa..3f7c6e0 100644
--- a/Server/Source/Routes/Authentication.ts
+++ b/Server/Source/Routes/Authentication.ts
@@ -1,11 +1,15 @@
import axios from "axios";
import jwt from "jsonwebtoken";
import qs from "querystring";
+import j from "joi";
import { Response, Router } from "express";
-import { DASHBOARD_ROOT, DISCORD_CLIENT_ID, DISCORD_CLIENT_SECRET, FULL_SERVER_ROOT, JWT_KEY } from "../Modules/Constants";
+import { BOT_TOKEN, DASHBOARD_ROOT, DISCORD_CLIENT_ID, DISCORD_CLIENT_SECRET, DISCORD_SERVER_ID, FULL_SERVER_ROOT, JWT_KEY } from "../Modules/Constants";
import { User, UserPermissions } from "../Schemas/User";
+import { ValidateQuery } from "../Modules/Middleware";
+import { Err } from "../Modules/Logger";
const App = Router();
+//let DiscordServerRoleMetadata;
// ? hacky, if you want, make it less hacky
async function QuickRevokeToken(res: Response, Token: string) {
@@ -13,15 +17,25 @@ async function QuickRevokeToken(res: Response, Token: string) {
return res;
}
+async function ReloadRoleData() {
+ const DRMt = await axios.get(`https://discord.com/api/guilds/${DISCORD_SERVER_ID}/roles`, { headers: { Authorization: `Bot ${BOT_TOKEN}` } });
+
+ Err(`Discord roles request failed to execute. Did you set up the .env correctly?`)
+ if (DRMt.status !== 200)
+ process.exit(-1);
+
+ //DiscordServerRoleMetadata = DRMt.data as { id: string, name: string, permissions: number }[];
+}
+
+//ReloadRoleData();
+
App.get("/discord/url", (_ ,res) => res.send(`https://discord.com/api/oauth2/authorize?client_id=${qs.escape(DISCORD_CLIENT_ID!)}&response_type=code&redirect_uri=${qs.escape(`${FULL_SERVER_ROOT}/api/discord`)}&scope=identify`))
-App.get("/discord", async (req, res) => {
- if (!req.query.code)
- return res.status(400).send("Please authorize with Discord first.");
-
- if (!/^(\w|\d)+$/gi.test(req.query.code as string))
- return res.status(400).send("Malformed code.");
-
+App.get("/discord",
+ValidateQuery(j.object({
+ code: j.string().pattern(/^(\w|\d)+$/i).required()
+})),
+async (req, res) => {
const Discord = await axios.post(`https://discord.com/api/oauth2/token`, qs.stringify({ grant_type: "authorization_code", code: req.query.code as string, redirect_uri: `${FULL_SERVER_ROOT}/api/discord` }), { auth: { username: DISCORD_CLIENT_ID!, password: DISCORD_CLIENT_SECRET! } });
if (Discord.status !== 200)
@@ -36,11 +50,15 @@ App.get("/discord", async (req, res) => {
await QuickRevokeToken(res, Discord.data.access_token);
+ // TODO: add discord role thingy
+ let UserPermissionLevel = UserPermissions.User;
+
let DBUser = await User.findOne({ where: { ID: UserData.data.id } });
if (!DBUser)
DBUser = await User.create({
ID: UserData.data.id,
- Library: []
+ Library: [],
+ PermissionLevel: UserPermissionLevel
}).save();
const JWT = jwt.sign({ ID: UserData.data.id }, JWT_KEY!, { algorithm: "HS256" });
diff --git a/Server/Source/Routes/Drafting.ts b/Server/Source/Routes/Drafting.ts
new file mode 100644
index 0000000..fad8f8d
--- /dev/null
+++ b/Server/Source/Routes/Drafting.ts
@@ -0,0 +1,143 @@
+import j from "joi";
+import exif from "exif-reader";
+import ffmpeg from "fluent-ffmpeg";
+import { Router } from "express";
+import { RequireAuthentication, ValidateBody } from "../Modules/Middleware";
+import { Song } from "../Schemas/Song";
+import { Debug } from "../Modules/Logger";
+import { magenta } from "colorette";
+import { fromBuffer } from "file-type";
+import { writeFileSync } from "fs";
+import { FULL_SERVER_ROOT } from "../Modules/Constants";
+
+const App = Router();
+
+App.post("/create",
+ RequireAuthentication(),
+ ValidateBody(j.object({
+ Name: j.string().required().min(3).max(64),
+ Year: j.number().required().min(1).max(2999),
+ ArtistName: j.string().required().min(1).max(64),
+ Length: j.number().required().min(1).max(10000),
+ Scale: j.string().valid("Minor", "Major").required(),
+ Key: j.string().valid("A", "Ab", "B", "Bb", "C", "Cb", "D", "Db", "E", "Eb", "F", "Fb", "G", "Gb").required(),
+ Album: j.string().required(),
+ GuitarStarterType: j.string().valid("Keytar", "Guitar").required(),
+ Tempo: j.number().min(20).max(1250).required(),
+ BassDifficulty: j.number().required().min(0).max(7),
+ GuitarDifficulty: j.number().required().min(0).max(7),
+ DrumsDifficulty: j.number().required().min(0).max(7),
+ VocalsDifficulty: j.number().required().min(0).max(7)
+ })),
+ async (req, res) => {
+ const SongData = await Song.create({
+ ...req.body,
+ IsDraft: true
+ }).save();
+
+ Debug(`New draft created by ${magenta(req.user!.ID!)} as ${magenta(`${SongData.ArtistName} - ${SongData.Name}`)}`)
+ res.json(SongData.Package());
+ });
+
+App.post("/upload/midi",
+ RequireAuthentication(),
+ ValidateBody(j.object({
+ Data: j.string().hex().required(),
+ TargetSong: j.string().uuid().required()
+ })),
+ async (req, res) => {
+ const Decoded = Buffer.from(req.body.Data, "hex");
+
+ if ((await fromBuffer(Decoded))?.ext !== "mid")
+ return res.status(400).send("Uploaded MIDI file is not a valid MIDI.");
+
+ if (!await Song.exists({ where: { ID: req.body.TargetSong } }))
+ return res.status(404).send("The song you're trying to upload a MIDI for does not exist.");
+
+ writeFileSync(`./Saved/Songs/${req.body.TargetSong}/Data.mid`, Decoded);
+ res.send(`${FULL_SERVER_ROOT}/song/download/${req.body.TargetSong}/midi.mid`);
+ });
+
+App.post("/upload/cover",
+ RequireAuthentication(),
+ ValidateBody(j.object({
+ Data: j.string().hex().required(),
+ TargetSong: j.string().uuid().required()
+ })),
+ async (req, res) => {
+ const Decoded = Buffer.from(req.body.Data, "hex");
+ const ext = (await fromBuffer(Decoded))!.ext;
+
+ if (ext !== "png")
+ return res.status(404).send("Invalid image file. (supported: png)");
+
+ if (!await Song.exists({ where: { ID: req.body.TargetSong } }))
+ return res.status(404).send("The song you're trying to upload a cover for does not exist.");
+
+ try {
+ const ImageMetadata = exif(Decoded);
+ if (!ImageMetadata.Image?.ImageWidth || !ImageMetadata.Image?.ImageLength)
+ throw new Error("Invalid image file.");
+
+ if (ImageMetadata.Image.ImageWidth !== ImageMetadata.Image.ImageLength)
+ return res.status(400).send("Image must have a 1:1 ratio.");
+
+ if (ImageMetadata.Image.ImageWidth < 512 || ImageMetadata.Image.ImageWidth > 2048)
+ return res.status(400).send("Image cannot be smaller than 512 pixels and larger than 2048 pixels.");
+ } catch (err) {
+ console.error(err)
+ return res.status(400).send("Invalid image file.");
+ }
+
+ writeFileSync(`./Saved/Songs/${req.body.TargetSong}/Cover.png`, Decoded);
+ res.send(`${FULL_SERVER_ROOT}/song/download/${req.body.TargetSong}/cover.png`);
+ });
+
+App.post("/upload/audio",
+ RequireAuthentication(),
+ ValidateBody(j.object({
+ Data: j.string().hex().required(),
+ TargetSong: j.string().uuid().required()
+ })),
+ async (req, res) => {
+ const Decoded = Buffer.from(req.body.Data, "hex");
+ const ext = (await fromBuffer(Decoded))!.ext;
+
+ if (!["mp3", "m4a", "ogg", "wav"].includes(ext))
+ return res.status(404).send("Invalid audio file. (supported: mp3, m4a, ogg, wav)");
+
+ if (!await Song.exists({ where: { ID: req.body.TargetSong } }))
+ return res.status(404).send("The song you're trying to upload audio for does not exist.");
+
+ await writeFileSync(`./Saved/Songs/${req.body.TargetSong}/Audio.${ext}`, Decoded);
+ ffmpeg()
+ .input(`./Saved/Songs/${req.body.TargetSong}/Audio.${ext}`)
+ .inputOptions(["-re"])
+ .outputOptions([
+ "-map 0",
+ "-c:a aac",
+ "-ar:a:0 48000",
+ "-use_timeline 1",
+ "-adaptation_sets \"id=0,streams=a\"",
+ "-f dash"
+ ])
+ .output(`./Saved/Songs/${req.body.TargetSong}/Chunks/Manifest.mpd`)
+ .on("start", cl => Debug(`ffmpeg running with ${magenta(cl)}`))
+ .on("end", () => Debug("Ffmpeg finished running"))
+ .on("error", (e, stdout, stderr) => { console.error(e); console.log(stdout); console.error(stderr); })
+ .run();
+
+ res.send("ffmpeg now running on song.");
+ });
+
+App.post("/submit",
+ RequireAuthentication(),
+ ValidateBody(j.object({})),
+ async (req, res) => {
+
+ });
+
+export default {
+ App,
+ DefaultAPI: "/api/drafts"
+}
\ No newline at end of file
diff --git a/Server/Source/Schemas/Song.ts b/Server/Source/Schemas/Song.ts
index 978f143..3c64aa2 100644
--- a/Server/Source/Schemas/Song.ts
+++ b/Server/Source/Schemas/Song.ts
@@ -1,14 +1,19 @@
-import { BaseEntity, BeforeInsert, Column, Entity, OneToMany, PrimaryGeneratedColumn } from "typeorm";
+import { BaseEntity, BeforeInsert, BeforeRemove, Column, Entity, ManyToOne, OneToMany, PrimaryGeneratedColumn } from "typeorm";
import { FULL_SERVER_ROOT } from "../Modules/Constants";
import { Rating } from "./Rating";
-import { existsSync, mkdirSync } from "fs";
+import { existsSync, mkdirSync, rmSync } from "fs";
import { v4 } from "uuid";
+import { User } from "./User";
+import { join } from "path";
@Entity()
export class Song extends BaseEntity {
@PrimaryGeneratedColumn("uuid")
ID: string;
+ @ManyToOne(() => User, U => U.CreatedTracks)
+ Author: User;
+
@Column()
Name: string;
@@ -57,6 +62,9 @@ export class Song extends BaseEntity {
@Column()
VocalsDifficulty: number;
+ @Column()
+ IsDraft: boolean;
+
@Column()
CreationDate: Date;
@@ -70,12 +78,18 @@ export class Song extends BaseEntity {
Setup() {
this.ID = v4();
this.Directory = `./Saved/Songs/${this.ID}`;
- if (!existsSync(this.Directory))
- mkdirSync(this.Directory);
+ if (!existsSync(join(this.Directory, "Chunks")))
+ mkdirSync(join(this.Directory, "Chunks"), { recursive: true });
this.CreationDate = new Date();
}
+ @BeforeRemove()
+ Delete() {
+ if (existsSync(this.Directory) && this.Directory.endsWith(this.ID))
+ rmSync(this.Directory, { recursive: true, force: true }); // lets hope this does not cause steam launcher for linux 2.0
+ }
+
public Package() {
return {
...this,
diff --git a/Server/Source/Schemas/User.ts b/Server/Source/Schemas/User.ts
index 5a4ddc1..a581118 100644
--- a/Server/Source/Schemas/User.ts
+++ b/Server/Source/Schemas/User.ts
@@ -23,6 +23,9 @@ export class User extends BaseEntity {
@OneToMany(() => Rating, R => R.Author)
Ratings: Rating[];
+ @OneToMany(() => Song, R => R.Author)
+ CreatedTracks: Song[];
+
@ManyToMany(() => Song, { eager: true })
@JoinTable()
BookmarkedSongs: Song[];
diff --git a/Server/package-lock.json b/Server/package-lock.json
index 2d9604e..f07b39c 100644
--- a/Server/package-lock.json
+++ b/Server/package-lock.json
@@ -17,6 +17,7 @@
"dotenv": "^16.3.1",
"exif-reader": "^2.0.0",
"express": "^4.18.2",
+ "file-type": "^16.5.3",
"fluent-ffmpeg": "^2.1.2",
"joi": "^17.12.0",
"jsonwebtoken": "^9.0.2",
@@ -98,6 +99,11 @@
"resolved": "https://registry.npmjs.org/@sqltools/formatter/-/formatter-1.2.5.tgz",
"integrity": "sha512-Uy0+khmZqUrUGm5dmMqVlnvufZRSK0FbYzVgp0UMstm+F5+W2/jnEEQyc9vo1ZR/E5ZI/B1WjjoTqBqwJL6Krw=="
},
+ "node_modules/@tokenizer/token": {
+ "version": "0.3.0",
+ "resolved": "https://registry.npmjs.org/@tokenizer/token/-/token-0.3.0.tgz",
+ "integrity": "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A=="
+ },
"node_modules/@types/body-parser": {
"version": "1.19.3",
"resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.3.tgz",
@@ -965,6 +971,22 @@
"node": ">= 0.10.0"
}
},
+ "node_modules/file-type": {
+ "version": "16.5.4",
+ "resolved": "https://registry.npmjs.org/file-type/-/file-type-16.5.4.tgz",
+ "integrity": "sha512-/yFHK0aGjFEgDJjEKP0pWCplsPFPhwyfwevf/pVxiN0tmE4L9LmwWxWukdJSHdoCli4VgQLehjJtwQBnqmsKcw==",
+ "dependencies": {
+ "readable-web-to-node-stream": "^3.0.0",
+ "strtok3": "^6.2.4",
+ "token-types": "^4.1.1"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sindresorhus/file-type?sponsor=1"
+ }
+ },
"node_modules/file-uri-to-path": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz",
@@ -1620,6 +1642,18 @@
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz",
"integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ=="
},
+ "node_modules/peek-readable": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/peek-readable/-/peek-readable-4.1.0.tgz",
+ "integrity": "sha512-ZI3LnwUv5nOGbQzD9c2iDG6toheuXSZP5esSHBjopsXH4dg19soufvpUGA3uohi5anFtGb2lhAVdHzH6R/Evvg==",
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/Borewit"
+ }
+ },
"node_modules/prebuild-install": {
"version": "7.1.1",
"resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.1.tgz",
@@ -1734,6 +1768,21 @@
"node": ">= 6"
}
},
+ "node_modules/readable-web-to-node-stream": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/readable-web-to-node-stream/-/readable-web-to-node-stream-3.0.2.tgz",
+ "integrity": "sha512-ePeK6cc1EcKLEhJFt/AebMCLL+GgSKhuygrZ/GLaKZYEecIgIECf4UaUuaByiGtzckwR4ain9VzUh95T1exYGw==",
+ "dependencies": {
+ "readable-stream": "^3.6.0"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/Borewit"
+ }
+ },
"node_modules/reflect-metadata": {
"version": "0.1.14",
"resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.1.14.tgz",
@@ -2042,6 +2091,22 @@
"node": ">=0.10.0"
}
},
+ "node_modules/strtok3": {
+ "version": "6.3.0",
+ "resolved": "https://registry.npmjs.org/strtok3/-/strtok3-6.3.0.tgz",
+ "integrity": "sha512-fZtbhtvI9I48xDSywd/somNqgUHl2L2cstmXCCif0itOf96jeW18MBSyrLuNicYQVkvpOxkZtkzujiTJ9LW5Jw==",
+ "dependencies": {
+ "@tokenizer/token": "^0.3.0",
+ "peek-readable": "^4.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/Borewit"
+ }
+ },
"node_modules/supports-color": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
@@ -2106,6 +2171,22 @@
"node": ">=0.6"
}
},
+ "node_modules/token-types": {
+ "version": "4.2.1",
+ "resolved": "https://registry.npmjs.org/token-types/-/token-types-4.2.1.tgz",
+ "integrity": "sha512-6udB24Q737UD/SDsKAHI9FCRP7Bqc9D/MQUV02ORQg5iskjtLJlZJNdN4kKtcdtwCeWIwIHDGaUsTsCCAa8sFQ==",
+ "dependencies": {
+ "@tokenizer/token": "^0.3.0",
+ "ieee754": "^1.2.1"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/Borewit"
+ }
+ },
"node_modules/tslib": {
"version": "2.6.2",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz",
diff --git a/Server/package.json b/Server/package.json
index 99b3c21..a0847b4 100644
--- a/Server/package.json
+++ b/Server/package.json
@@ -49,6 +49,7 @@
"dotenv": "^16.3.1",
"exif-reader": "^2.0.0",
"express": "^4.18.2",
+ "file-type": "^16.5.3",
"fluent-ffmpeg": "^2.1.2",
"joi": "^17.12.0",
"jsonwebtoken": "^9.0.2",
diff --git a/package.json b/package.json
index a153ef0..8f99ac8 100644
--- a/package.json
+++ b/package.json
@@ -8,11 +8,11 @@
"build:prod": "vite build",
"build:stage": "vite build --mode staging",
- "create:prod": "mkdir ./Out && npm run build:prod && mv ./dist ./Out/dist && cd Server && tsc && cd .. && cp ./Server/.env.prod ./Out/.env && cp ./Server/package.json ./Out && cp ./Server/package-lock.json ./Out",
- "publish:prod": "npm run create:prod && ssh partypack \"cd /home/PartypackProd; rm -rf ./Out\" && scp -r ./Out partypack:/home/PartypackProd && ssh partypack \"cd /home/PartypackProd/Out && npm i && pm2 restart PartypackProd --update-env\" && rm -rf ./Out",
+ "create:prod": "mkdir \"./Out\" && npm run build:prod && move \"./dist\" \"./Out/dist\" && cd \"Server\" && tsc && cd .. && copy \"./Server/.env.prod\" \"./Out/.env\" && copy \"./Server/package.json\" \"./Out/package.json\" && copy \"./Server/package-lock.json\" \"./Out/package-lock.json\"",
+ "publish:prod": "npm run create:prod && ssh partypack \"cd /home/PartypackProd; rm -rf ./Out\" && scp -r \"./Out\" partypack:/home/PartypackProd && ssh partypack \"cd /home/PartypackProd/Out && npm i && pm2 restart PartypackProd --update-env\" && rmdir \"./Out\"",
- "create:stage": "mkdir ./Out && npm run build:stage && mv ./dist ./Out/dist && cd Server && tsc && cd .. && cp ./Server/.env.staging ./Out/.env && cp ./Server/package.json ./Out && cp ./Server/package-lock.json ./Out",
- "publish:stage": "npm run create:stage && ssh partypack \"cd /home/PartypackStage; rm -rf ./Out\" && scp -r ./Out partypack:/home/PartypackStage && ssh partypack \"cd /home/PartypackStage/Out && npm i && pm2 restart PartypackStage --update-env\" && rm -rf ./Out",
+ "create:stage": "mkdir \"./Out\" && npm run build:stage && move \"./dist\" \"./Out/dist\" && cd \"Server\" && tsc && cd .. && copy \"./Server/.env.staging\" \"./Out/.env\" && copy \"./Server/package.json\" \"./Out/package.json\" && copy \"./Server/package-lock.json\" \"./Out/package-lock.json\"",
+ "publish:stage": "npm run create:stage && ssh partypack \"cd /home/PartypackStage; rm -rf ./Out\" && scp -r ./Out partypack:/home/PartypackStage && ssh partypack \"cd /home/PartypackStage/Out && npm i && pm2 restart PartypackStage --update-env\" && rmdir \"./Out\"",
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
"preview": "vite preview",
diff --git a/src/App.tsx b/src/App.tsx
index c27b88f..452ce3c 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -9,6 +9,7 @@ import { CookiesProvider } from "react-cookie";
import { Home } from "./routes/Home";
import { Download } from "./routes/Download";
import { Tracks } from "./routes/Tracks";
+import { TrackSubmission } from "./routes/TrackSubmission";
import { Profile } from "./routes/Profile";
import { NotFound } from "./routes/404";
import { AdminHome } from "./routes/AdminHome";
@@ -41,6 +42,7 @@ function App() {