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() { } /> } /> } /> + } /> } /> } /> diff --git a/src/components/SiteHeader.tsx b/src/components/SiteHeader.tsx index e99cbd3..27fde00 100644 --- a/src/components/SiteHeader.tsx +++ b/src/components/SiteHeader.tsx @@ -33,9 +33,8 @@ export function SiteHeader() { Partypack - navigate("/rotation")}>Daily Rotation - navigate("/creators")}>Top Creators navigate("/tracks")}>Tracks + navigate("/submissions")}>Submissions navigate("/tutorials")}>Tutorials navigate("/faq")}>FAQ window.open("https://discord.gg/KaxknAbqDS")}>Discord diff --git a/src/routes/AdminCreateTrack.tsx b/src/routes/AdminCreateTrack.tsx index dedff9a..89d0ba5 100644 --- a/src/routes/AdminCreateTrack.tsx +++ b/src/routes/AdminCreateTrack.tsx @@ -14,7 +14,7 @@ export function AdminCreateTrack() { return ( <> - Create a New Track + [ADMIN] Create a New Track
Song Name diff --git a/src/routes/AdminTrackList.tsx b/src/routes/AdminTrackList.tsx index b425b3c..3f053d8 100644 --- a/src/routes/AdminTrackList.tsx +++ b/src/routes/AdminTrackList.tsx @@ -21,7 +21,7 @@ export function AdminTrackList() { return ( <> - All tracks (admin) + [ADMIN] All tracks { tracks.map(x => { diff --git a/src/routes/TrackSubmission.tsx b/src/routes/TrackSubmission.tsx new file mode 100644 index 0000000..3d7fb9c --- /dev/null +++ b/src/routes/TrackSubmission.tsx @@ -0,0 +1,181 @@ +import { ActionList, ActionMenu, FormControl, TextInput, Heading, Button, Text, Box } from "@primer/react"; +import { useRef, useState } from "react"; +import { toast } from "react-toastify"; +import { Buffer } from "buffer/"; +import axios from "axios"; + +const formControlStyle = { paddingTop: 3 }; + +export function TrackSubmission() { + const formRef = useRef(null); + const [Key, setKey] = useState("Select a key..."); + const [Scale, setScale] = useState("Select a scale..."); + const [GuitarStarterType, setGuitarStarterType] = useState("Select the starter type..."); + + return ( + <> + {/* + + + */} + Create a New Draft + Drafts are private versions of Tracks, only available to you. If you want to publish that track, click the "Submit for Review" button on the management page. + + + Song Name + + + + Artist + If there are multiple artists, separate them with a comma. + + + + Album + + + + Release Year + + + + Length (in seconds) + + + + Key + + {Key} + + { + ["A", "Ab", "B", "Bb", "C", "Cb", "D", "Db", "E", "Eb", "F", "Fb", "G", "Gb"].map(x => { + return ( + setKey(x)}> + {x} + + ) + }) + } + + + + + Scale + + {Scale} + + setScale("Minor")}>Minor + setScale("Major")}>Major + + + + + Lead Type + This is defining what lead instrument the song is going to start with. You can change the instrument mid-game with [keytar] and [guitar] text events. + + {GuitarStarterType} + + setGuitarStarterType("Guitar")}>Guitar + setGuitarStarterType("Keytar")}>Keytar + + + + + Tempo + + + + MIDI File (.mid) + + You can use the #tools-and-resources channel to find useful resources on how to create MIDIs. + + + Audio File (.m4a, .mp3, .wav) + + This will play in the background of your song. Make sure it was exported from REAPER. + + + Cover Image (.jpg, .jpeg, .webp, .png) + + Must be a 1:1 ratio. Max: 2048x2048, min: 512x512 + + + Lead Difficulty + + Ranges from 0-6 + + + Drums Difficulty + + Ranges from 0-6 + + + Vocals Difficulty + + Ranges from 0-6 + + + Bass Difficulty + + Ranges from 0-6 + + + + + + + ) +} \ No newline at end of file