i dont remember what i did here

This commit is contained in:
McMistrzYT 2024-01-28 16:54:32 +01:00
parent 0588183828
commit b97048d3fb
16 changed files with 499 additions and 28 deletions

View File

@ -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

View File

@ -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.

View File

@ -45,3 +45,14 @@ export function ValidateBody(Schema: j.Schema) {
}
}
}
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 })
}
}
}

View File

@ -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.");

View File

@ -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" });

View File

@ -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"
}

View File

@ -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,

View File

@ -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[];

View File

@ -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",

View File

@ -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",

View File

@ -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",

View File

@ -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() {
<Route path="/" element={<Home />} />
<Route path="/download" element={<Download />} />
<Route path="/tracks" element={<Tracks />} />
<Route path="/submissions" element={<TrackSubmission />} />
<Route path="/profile" element={<Profile />} />
<Route path="*" element={<NotFound />} />

View File

@ -33,9 +33,8 @@ export function SiteHeader() {
<img src={Favicon} style={{ width: 32, height: "auto", paddingRight: 5 }} />
<b>Partypack</b>
</Header.Item>
<Header.Item full sx={{ cursor: "pointer" }} onClick={() => navigate("/rotation")}>Daily Rotation</Header.Item>
<Header.Item full sx={{ cursor: "pointer" }} onClick={() => navigate("/creators")}>Top Creators</Header.Item>
<Header.Item full sx={{ cursor: "pointer" }} onClick={() => navigate("/tracks")}>Tracks</Header.Item>
<Header.Item full sx={{ cursor: "pointer" }} onClick={() => navigate("/submissions")}>Submissions</Header.Item>
<Header.Item full sx={{ cursor: "pointer" }} onClick={() => navigate("/tutorials")}>Tutorials</Header.Item>
<Header.Item full sx={{ cursor: "pointer" }} onClick={() => navigate("/faq")}>FAQ</Header.Item>
<Header.Item full sx={{ cursor: "pointer" }} onClick={() => window.open("https://discord.gg/KaxknAbqDS")}>Discord</Header.Item>

View File

@ -14,7 +14,7 @@ export function AdminCreateTrack() {
return (
<>
<Heading>Create a New Track</Heading>
<Heading>[ADMIN] Create a New Track</Heading>
<form method="GET" action="" ref={formRef}>
<FormControl required={true} sx={formControlStyle}>
<FormControl.Label>Song Name</FormControl.Label>

View File

@ -21,7 +21,7 @@ export function AdminTrackList() {
return (
<>
<Heading>All tracks (admin) <Button sx={{ marginBottom: 2 }} onClick={() => navigate("/admin/tracks/create")}>Create</Button></Heading>
<Heading>[ADMIN] All tracks <Button sx={{ marginBottom: 2 }} onClick={() => navigate("/admin/tracks/create")}>Create</Button></Heading>
<Box className="songCategory">
{
tracks.map(x => {

View File

@ -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<HTMLFormElement>(null);
const [Key, setKey] = useState<string>("Select a key...");
const [Scale, setScale] = useState<string>("Select a scale...");
const [GuitarStarterType, setGuitarStarterType] = useState<string>("Select the starter type...");
return (
<>
{/*<Box m={150} sx={{ float: "right" }}>
<img src="https://upload.wikimedia.org/wikipedia/en/9/9a/Trollface_non-free.png" style={{ width: "256px", height: "auto", borderRadius: 10 }} />
<Text></Text>
</Box>*/}
<Heading>Create a New Draft</Heading>
<Text>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.</Text>
<form method="GET" action="" ref={formRef}>
<FormControl required={true} sx={formControlStyle}>
<FormControl.Label>Song Name</FormControl.Label>
<TextInput />
</FormControl>
<FormControl required={true} sx={formControlStyle}>
<FormControl.Label>Artist</FormControl.Label>
<FormControl.Caption>If there are multiple artists, separate them with a comma.</FormControl.Caption>
<TextInput />
</FormControl>
<FormControl required={true} sx={formControlStyle}>
<FormControl.Label>Album</FormControl.Label>
<TextInput />
</FormControl>
<FormControl required={true} sx={formControlStyle}>
<FormControl.Label>Release Year</FormControl.Label>
<TextInput type="number" />
</FormControl>
<FormControl required={true} sx={formControlStyle}>
<FormControl.Label>Length (in seconds)</FormControl.Label>
<TextInput type="number" />
</FormControl>
<FormControl required={true} sx={formControlStyle}>
<FormControl.Label>Key</FormControl.Label>
<ActionMenu>
<ActionMenu.Button>{Key}</ActionMenu.Button>
<ActionMenu.Overlay width="medium">
{
["A", "Ab", "B", "Bb", "C", "Cb", "D", "Db", "E", "Eb", "F", "Fb", "G", "Gb"].map(x => {
return (
<ActionList.Item onSelect={() => setKey(x)}>
{x}
</ActionList.Item>
)
})
}
</ActionMenu.Overlay>
</ActionMenu>
</FormControl>
<FormControl required={true} sx={formControlStyle}>
<FormControl.Label>Scale</FormControl.Label>
<ActionMenu>
<ActionMenu.Button>{Scale}</ActionMenu.Button>
<ActionMenu.Overlay width="medium">
<ActionList.Item onSelect={() => setScale("Minor")}>Minor</ActionList.Item>
<ActionList.Item onSelect={() => setScale("Major")}>Major</ActionList.Item>
</ActionMenu.Overlay>
</ActionMenu>
</FormControl>
<FormControl required={true} sx={formControlStyle}>
<FormControl.Label>Lead Type</FormControl.Label>
<FormControl.Caption>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.</FormControl.Caption>
<ActionMenu>
<ActionMenu.Button>{GuitarStarterType}</ActionMenu.Button>
<ActionMenu.Overlay width="medium">
<ActionList.Item onSelect={() => setGuitarStarterType("Guitar")}>Guitar</ActionList.Item>
<ActionList.Item onSelect={() => setGuitarStarterType("Keytar")}>Keytar</ActionList.Item>
</ActionMenu.Overlay>
</ActionMenu>
</FormControl>
<FormControl required={true} sx={formControlStyle}>
<FormControl.Label>Tempo</FormControl.Label>
<TextInput type="number" />
</FormControl>
<FormControl required={true} sx={formControlStyle}>
<FormControl.Label>MIDI File (.mid)</FormControl.Label>
<TextInput type="file" />
<FormControl.Caption>You can use the #tools-and-resources channel to find useful resources on how to create MIDIs.</FormControl.Caption>
</FormControl>
<FormControl required={true} sx={formControlStyle}>
<FormControl.Label>Audio File (.m4a, .mp3, .wav)</FormControl.Label>
<TextInput type="file" />
<FormControl.Caption>This will play in the background of your song. Make sure it was exported from REAPER.</FormControl.Caption>
</FormControl>
<FormControl required={true} sx={formControlStyle}>
<FormControl.Label>Cover Image (.jpg, .jpeg, .webp, .png)</FormControl.Label>
<TextInput type="file" />
<FormControl.Caption>Must be a 1:1 ratio. Max: 2048x2048, min: 512x512</FormControl.Caption>
</FormControl>
<FormControl required={true} sx={formControlStyle}>
<FormControl.Label>Lead Difficulty</FormControl.Label>
<TextInput type="number" />
<FormControl.Caption>Ranges from 0-6</FormControl.Caption>
</FormControl>
<FormControl required={true} sx={formControlStyle}>
<FormControl.Label>Drums Difficulty</FormControl.Label>
<TextInput type="number" />
<FormControl.Caption>Ranges from 0-6</FormControl.Caption>
</FormControl>
<FormControl required={true} sx={formControlStyle}>
<FormControl.Label>Vocals Difficulty</FormControl.Label>
<TextInput type="number" />
<FormControl.Caption>Ranges from 0-6</FormControl.Caption>
</FormControl>
<FormControl required={true} sx={formControlStyle}>
<FormControl.Label>Bass Difficulty</FormControl.Label>
<TextInput type="number" />
<FormControl.Caption>Ranges from 0-6</FormControl.Caption>
</FormControl>
<Button sx={{ marginTop: 2 }} type="submit" onClick={async e => {
e.preventDefault();
console.log(formRef);
if (formRef.current == null)
return;
const Name = (formRef.current[0] as HTMLInputElement).value;
const ArtistName = (formRef.current[1] as HTMLInputElement).value;
const Album = (formRef.current[2] as HTMLInputElement).value;
const Year = (formRef.current[3] as HTMLInputElement).value;
const Length = (formRef.current[4] as HTMLInputElement).valueAsNumber;
//const Key = (formRef.current[5] as HTMLInputElement).value;
//const Scale = (formRef.current[6] as HTMLInputElement).value;
//const GuitarStarterType = (formRef.current[7] as HTMLInputElement).value;
const Tempo = (formRef.current[8] as HTMLInputElement).valueAsNumber;
const Midi = (formRef.current[9] as HTMLInputElement).files![0];
const Music = (formRef.current[10] as HTMLInputElement).files![0];
const Cover = (formRef.current[11] as HTMLInputElement).files![0];
const GuitarDifficulty = (formRef.current[12] as HTMLInputElement).valueAsNumber;
const DrumsDifficulty = (formRef.current[13] as HTMLInputElement).valueAsNumber;
const VocalsDifficulty = (formRef.current[14] as HTMLInputElement).valueAsNumber;
const BassDifficulty = (formRef.current[15] as HTMLInputElement).valueAsNumber;
const SongData = await axios.post("/api/drafts/create", {
Name,
ArtistName,
Album,
Year,
Length,
Key,
Scale,
GuitarStarterType,
Tempo,
GuitarDifficulty,
DrumsDifficulty,
VocalsDifficulty,
BassDifficulty
});
toast(SongData.data, { type: SongData.status === 200 ? "success" : "error" });
if (SongData.status !== 200)
return;
const MidiRes = await axios.post("/api/drafts/upload/midi", { Data: Buffer.from(await Midi.arrayBuffer()).toString("hex"), TargetSong: SongData.data.ID });
toast(MidiRes.status === 200 ? "Uploaded MIDI chart successfully." : MidiRes.data.errorMessage, { type: MidiRes.status === 200 ? "success" : "error" });
const AudioRes = await axios.post("/api/drafts/upload/audio", { Data: Buffer.from(await Music.arrayBuffer()).toString("hex"), TargetSong: SongData.data.ID });
toast(AudioRes.status === 200 ? "Uploaded audio for processing successfully." : AudioRes.data.errorMessage, { type: AudioRes.status === 200 ? "success" : "error" });
const CoverRes = await axios.post("/api/drafts/upload/cover", { Data: Buffer.from(await Cover.arrayBuffer()).toString("hex"), TargetSong: SongData.data.ID });
toast(MidiRes.status === 200 ? "Uploaded cover image successfully." : MidiRes.data.errorMessage, { type: CoverRes.status === 200 ? "success" : "error" });
}}>Create</Button>
</form>
</>
)
}