diff --git a/Server/.example.env b/Server/.example.env index 0863aef..74527e9 100644 --- a/Server/.example.env +++ b/Server/.example.env @@ -3,11 +3,12 @@ BODY_SIZE_LIMIT=50mb PROJECT_NAME=Partypack Service SERVER_URL=localhost DASHBOARD_URL=localhost:5173 # set this to wherever you're hosting the Partypack dashboard +USE_HTTPS=true # set this to false if you're debugging on your computer ENVIRONMENT=dev COOKIE_SIGN_KEY= ADMIN_KEY= +JWT_KEY= BOT_TOKEN= -USE_HTTPS=true # set this to false if you're debugging on your computer MAX_AMOUNT_OF_DRAFTS_AT_ONCE=30 DISCORD_SERVER_ID= diff --git a/Server/Source/Handlers/Database.ts b/Server/Source/Handlers/Database.ts index 9e7e5de..beedbf3 100644 --- a/Server/Source/Handlers/Database.ts +++ b/Server/Source/Handlers/Database.ts @@ -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}`); + }) })(); /* diff --git a/Server/Source/Handlers/Server.ts b/Server/Source/Handlers/Server.ts index 26c2120..b68f4d7 100644 --- a/Server/Source/Handlers/Server.ts +++ b/Server/Source/Handlers/Server.ts @@ -68,7 +68,7 @@ async function Initialize() { Initialize(); // ! FESTIVAL-SPECIFIC STUFF -import axios from "axios"; +//import axios from "axios"; -axios.defaults.validateStatus = () => true; -axios.defaults.headers.common["X-Do-Not-Redirect"] = "true"; \ No newline at end of file +//axios.defaults.validateStatus = () => true; +//axios.defaults.headers.common["X-Do-Not-Redirect"] = "true"; \ No newline at end of file diff --git a/Server/Source/Modules/FNUtil.ts b/Server/Source/Modules/FNUtil.ts index a971fac..206dd52 100644 --- a/Server/Source/Modules/FNUtil.ts +++ b/Server/Source/Modules/FNUtil.ts @@ -1,3 +1,4 @@ +import axios from "axios"; import { Err } from "./Logger"; import { red } from "colorette"; import { FULL_SERVER_ROOT } from "./Constants"; @@ -11,31 +12,15 @@ let LastContentDownloadDate: Date = new Date(0); // set it to 1970 as default cu GenerateFortnitePages(null); export async function GenerateFortnitePages(ForUser: User | null): Promise<{ Success: boolean, FNPages: { [key: string]: unknown } | null }> { - let status; - let data; - if (FullFortnitePages === null || Date.now() > LastContentDownloadDate.getTime() + 30 * 60 * 1000) { - const response = await fetch("https://fortnitecontent-website-prod07.ol.epicgames.com/content/api/pages/fortnite-game"); - status = response.status; - data = await response.json(); - } else { - status = 200; - data = FullFortnitePages; - } - /*const { status, data } = // check if 30 minutes have passed since last content update. if so, get a new copy of pages, if not, fuck off + const { status, data } = // check if 30 minutes have passed since last content update. if so, get a new copy of pages, if not, fuck off FullFortnitePages === null || Date.now() > LastContentDownloadDate.getTime() + 30 * 60 * 1000 ? - await fetch("https://fortnitecontent-website-prod07.ol.epicgames.com/content/api/pages/fortnite-game") : - FullFortnitePages;*/ + await axios.get("https://fortnitecontent-website-prod07.ol.epicgames.com/content/api/pages/fortnite-game") : + { status: 200, data: FullFortnitePages }; - let OGSparks; - - if (OriginalSparks === null || Date.now() > LastContentDownloadDate.getTime() + 30 * 60 * 1000) { - const response = await fetch("https://fortnitecontent-website-prod07.ol.epicgames.com/content/api/pages/fortnite-game/spark-tracks"); - OGSparks = {status: 200, data: await response.json()} - } else { - OGSparks = OriginalSparks - } - - + const OGSparks = + OriginalSparks === null || Date.now() > LastContentDownloadDate.getTime() + 30 * 60 * 1000 ? + await axios.get("https://fortnitecontent-website-prod07.ol.epicgames.com/content/api/pages/fortnite-game/spark-tracks") : + { status: 200, data: OriginalSparks }; FullFortnitePages = { ...data, @@ -111,7 +96,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: [ { diff --git a/Server/Source/Modules/Middleware.ts b/Server/Source/Modules/Middleware.ts index dcb5fd3..89a3b5d 100644 --- a/Server/Source/Modules/Middleware.ts +++ b/Server/Source/Modules/Middleware.ts @@ -16,7 +16,7 @@ declare global { export function RequireAuthentication(Relations?: object) { return async (req: Request, res: Response, next: NextFunction) => { if (!req.header("X-Partypack-Token") && !req.cookies["Token"] && !req.header("Authorization")) - return res.status(401).send("This endpoint requires authorization."); + return res.status(401).send("You must be logged in to perform this action."); let JWT: JwtPayload; try { @@ -55,4 +55,4 @@ export function ValidateQuery(Schema: j.Schema) { res.status(400).json(err) } } -} \ No newline at end of file +} diff --git a/Server/Source/Routes/Authentication.ts b/Server/Source/Routes/Authentication.ts index a018bd5..6a411ec 100644 --- a/Server/Source/Routes/Authentication.ts +++ b/Server/Source/Routes/Authentication.ts @@ -1,3 +1,4 @@ +import axios from "axios"; import jwt from "jsonwebtoken"; import qs from "querystring"; import j from "joi"; @@ -15,15 +16,11 @@ const App = Router(); // ? hacky, if you want, make it less hacky async function QuickRevokeToken(res: Response, Token: string) { - - await fetch("https://discord.com/api/oauth2/token/revoke", { - method: "POST", + await axios.post("https://discord.com/api/oauth2/token/revoke", qs.stringify({ token: Token, token_type_hint: "access_token" }), { headers: { "Content-Type": "application/x-www-form-urlencoded", "Authorization": `Basic ${Buffer.from(`${DISCORD_CLIENT_ID}:${DISCORD_CLIENT_SECRET}`).toString("base64")}` - }, - body: qs.stringify({ token: Token, token_type_hint: "access_token" }) - + } }) return res; } @@ -39,36 +36,33 @@ App.get("/discord", //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! } }); - const Discord = await fetch( + 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` }), { - method: "POST", headers: { "Content-Type": "application/x-www-form-urlencoded", "Authorization": `Basic ${Buffer.from(`${DISCORD_CLIENT_ID}:${DISCORD_CLIENT_SECRET}`).toString("base64")}` - }, - body: qs.stringify({ grant_type: "authorization_code", code: req.query.code as string, redirect_uri: `${FULL_SERVER_ROOT}/api/discord` }) + } } ) if (Discord.status !== 200) return res.status(500).send("Failed to request OAuth token from Discord's services."); - const DiscordData = await Discord.json() as any; // :waaaaa: + const DiscordData = Discord.data; // :waaaaa: if (!DiscordData.scope.includes("identify")) return (await QuickRevokeToken(res, DiscordData.access_token)).status(400).send("Missing identify scope. Please check if your OAuth link is correctly set up!"); - const UserData = await fetch("https://discord.com/api/v10/users/@me", { - method: "GET", + const UserData = await axios.get("https://discord.com/api/v10/users/@me", { headers: { "Authorization": `${DiscordData.token_type} ${DiscordData.access_token}` } - }) - const UserDataBody = await UserData.json() as any; + const UserDataBody = UserData.data; if (UserData.status !== 200) return (await QuickRevokeToken(res, DiscordData.access_token)).status(500).send("Failed to request user data from Discord's services."); diff --git a/Server/Source/Routes/Downloads.ts b/Server/Source/Routes/Downloads.ts index fbaf81f..e7699b5 100644 --- a/Server/Source/Routes/Downloads.ts +++ b/Server/Source/Routes/Downloads.ts @@ -5,6 +5,7 @@ import { CreateBlurl } from "../Modules/BLURL"; import { Song } from "../Schemas/Song"; import { RequireAuthentication } from "../Modules/Middleware"; import { UserPermissions } from "../Schemas/User"; +import path from "path"; const App = Router(); @@ -14,9 +15,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."); @@ -31,8 +35,8 @@ async (req, res) => { { type: "main", language: "en", - url: `${SongData.Directory}/Manifest.mpd`, - data: readFileSync(`${SongData.Directory}/Manifest.mpd`).toString().replaceAll("{BASEURL}", BaseURL) + url: `${BaseURL}master.blurl`, + data: readFileSync(ManifestPath).toString().replaceAll("{BASEURL}", BaseURL) } ], type: "vod", @@ -42,7 +46,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 +69,37 @@ async (req, res) => { if (!req.params.File.endsWith(".m4s") && !req.params.File.endsWith(".webm")) return res.sendStatus(403).send("Unsupported File Format."); - if (!existsSync(`${SongData.Directory}/Chunks/${req.params.File}`)) - return res.sendStatus(404).send("Chunk not found."); + const ChunkPath = `${SongData.Directory}/${IsPreview ? `PreviewChunks` : `Chunks`}/${req.params.File}` + if (!existsSync(ChunkPath)) + return res.status(404).send("Chunk not found."); res.set("content-type", "video/mp4") - res.send(readFileSync(`${SongData.Directory}/Chunks/${req.params.File}`)); + res.send(readFileSync(ChunkPath)); }); App.get("/:InternalID", +(req, res, next) => { + // send back index.html when the internal id is not a uuid - causes issues with reloading on sub-pages otherwise + if (!/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/.test(req.params.InternalID)) + return res.sendFile(path.join(process.cwd(), "dist", "index.html")); + + next(); +}, RequireAuthentication(), -async (req, res, next) => { - const SongData = await Song.findOne({ where: { ID: req.params.InternalID }, relations: { Author: true } }); +async (req, res) => { + const SongData = await Song.findOne({ where: [ { ID: req.params.InternalID }, { PID: req.params.InternalID } ], relations: { Author: true } }); if (!SongData) - return next(); // trust me bro + return res.status(404).send("Track not found."); + + 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: "", diff --git a/Server/Source/Routes/Drafting.ts b/Server/Source/Routes/Drafting.ts index fc97666..00a3fe4 100644 --- a/Server/Source/Routes/Drafting.ts +++ b/Server/Source/Routes/Drafting.ts @@ -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,115 @@ 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(/[\w\d\r\n\t]*<\/ProgramInformation>/i, "{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((resolve, reject) => { + ffmpeg() + .input(`${AudioPath}/Audio.${ext}`) + .audioCodec("libopus") + .audioFilter(FilterLeft + FilterRight) + .outputOptions([ + "-use_timeline 1", + "-f dash", + "-ac 2", // downmix + "-t 30" // max of 30 seconds (requested by mc) + ]) + .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(/[\w\d\r\n\t]*<\/ProgramInformation>/i, "{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(/[\w\d\r\n\t]*<\/ProgramInformation>/i, "{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; diff --git a/Server/Source/Routes/Pages.ts b/Server/Source/Routes/Pages.ts index 8379846..7f09cb8 100644 --- a/Server/Source/Routes/Pages.ts +++ b/Server/Source/Routes/Pages.ts @@ -2,6 +2,7 @@ import { Router } from "express"; import { FullFortnitePages, GenerateFortnitePages } from "../Modules/FNUtil"; import { IS_DEBUG } from "../Modules/Constants"; import { RequireAuthentication } from "../Modules/Middleware"; +import axios from "axios"; const App = Router(); @@ -41,11 +42,11 @@ App.get("/content/api/pages/fortnite-game/:Section", RequireAuthentication(), as if (!CachedSection) return res.status(404).send("funny section not found haha kill me"); - const ContentFromServer = await fetch(`https://fortnitecontent-website-prod07.ol.epicgames.com/content/api/pages/fortnite-game/${CachedSection._title}`) + const ContentFromServer = await axios.get(`https://fortnitecontent-website-prod07.ol.epicgames.com/content/api/pages/fortnite-game/${CachedSection._title}`) if (ContentFromServer.status !== 200) - return res.status(404).json({ error: IS_DEBUG ? await ContentFromServer.text() : "Fortnite server returned an error." }); + return res.status(404).json({ error: IS_DEBUG ? ContentFromServer.data : "Fortnite server returned an error." }); - res.json(await ContentFromServer.json()); + res.json(ContentFromServer.data); }) export default { diff --git a/Server/Source/Schemas/Song.ts b/Server/Source/Schemas/Song.ts index 8e0f34b..092bf25 100644 --- a/Server/Source/Schemas/Song.ts +++ b/Server/Source/Schemas/Song.ts @@ -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 }); diff --git a/Server/package-lock.json b/Server/package-lock.json index f063e0d..3fdf44e 100644 --- a/Server/package-lock.json +++ b/Server/package-lock.json @@ -10,6 +10,7 @@ "license": "ISC", "dependencies": { "@types/node-cron": "^3.0.11", + "axios": "^1.6.7", "better-sqlite3": "^9.3.0", "colorette": "^2.0.20", "cookie-parser": "^1.4.6", @@ -23,6 +24,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 +42,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 +188,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 +277,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 +475,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 +532,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", @@ -468,6 +547,21 @@ "resolved": "https://registry.npmjs.org/async/-/async-3.2.5.tgz", "integrity": "sha512-baNZyqaaLhyLVKm/DlvdW051MSgO6b8eVfIezl9E5PqWxFgzLm/wQntEW4zOytVburDEr0JlALEpdOFwvErLsg==" }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" + }, + "node_modules/axios": { + "version": "1.6.7", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.6.7.tgz", + "integrity": "sha512-/hDJGff6/c7u0hDkvkGxR/oy6CbCs8ziCsC7SqmhjfozqiJGc8Z11wrv9z9lYfY4K8l+H9TpjcMDX0xOZmx+RA==", + "dependencies": { + "follow-redirects": "^1.15.4", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } + }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", @@ -837,6 +931,17 @@ "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==" }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/content-disposition": { "version": "0.5.4", "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", @@ -901,6 +1006,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", @@ -949,6 +1059,14 @@ "node": ">=4.0.0" } }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/depd": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", @@ -974,6 +1092,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", @@ -1189,6 +1315,25 @@ "which": "bin/which" } }, + "node_modules/follow-redirects": { + "version": "1.15.5", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.5.tgz", + "integrity": "sha512-vSFWUON1B+yAw1VN4xMfxgn5fTUiaOzAJCKBwIIgT/+7CuGy9+r+5gITvP62j3RmaD5Ph65UaERdOSRGUzZtgw==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, "node_modules/foreground-child": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.1.1.tgz", @@ -1204,6 +1349,19 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/form-data": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", + "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/forwarded": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", @@ -1549,6 +1707,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", @@ -1864,6 +2027,11 @@ "node": ">= 0.10" } }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" + }, "node_modules/pump": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", @@ -2368,6 +2536,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 +2736,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 +2798,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 +3005,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" + } } } } diff --git a/Server/package.json b/Server/package.json index d92f4bd..a0609c6 100644 --- a/Server/package.json +++ b/Server/package.json @@ -42,6 +42,7 @@ }, "dependencies": { "@types/node-cron": "^3.0.11", + "axios": "^1.6.7", "better-sqlite3": "^9.3.0", "colorette": "^2.0.20", "cookie-parser": "^1.4.6", @@ -55,6 +56,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" diff --git a/src/routes/AdminHome.tsx b/src/routes/AdminHome.tsx index 1ccbe14..fa1c1e7 100644 --- a/src/routes/AdminHome.tsx +++ b/src/routes/AdminHome.tsx @@ -12,9 +12,9 @@ export function AdminHome() { Partypack Admin Management Panel - TEMP + Welcome! Please select a management page you'd like to visit: - + diff --git a/src/routes/Home.tsx b/src/routes/Home.tsx index 4847a5f..cff96e8 100644 --- a/src/routes/Home.tsx +++ b/src/routes/Home.tsx @@ -14,7 +14,7 @@ export function Home() { Partypack was created by everyone listed here. If you're interested in hosting your own instance of Partypack, be sure to check the GitHub repo. Quickstart Guide - Consider watching the easier to understand, visual guide available here.
+ Consider watching the easier to understand, visual guide available in the Discord server.
1. Join this instance's Discord server
2. Click on the icon in the top right
3. Log in using your Discord account
diff --git a/src/routes/Profile.tsx b/src/routes/Profile.tsx index a2ac633..e8c061e 100644 --- a/src/routes/Profile.tsx +++ b/src/routes/Profile.tsx @@ -270,7 +270,7 @@ export function Profile() { }}>Delete draft ; }) - : You have no bookmarked songs. + : You have no drafts. } : diff --git a/src/routes/TrackSubmission.tsx b/src/routes/TrackSubmission.tsx index 4c77c77..b72252c 100644 --- a/src/routes/TrackSubmission.tsx +++ b/src/routes/TrackSubmission.tsx @@ -91,7 +91,7 @@ export function TrackSubmission() { You can use the #tools-and-resources channel to find useful resources on how to create MIDIs. - Audio File (.m4a, .mp3, .wav) + Audio File (.m4a, .mp3, .wav, .ogg) This will play in the background of your song. Make sure it was exported from REAPER.