This commit is contained in:
McMistrzYT 2024-01-28 23:36:28 +01:00
parent 017b619766
commit 2bd4203827
7 changed files with 88 additions and 22 deletions

View File

@ -53,9 +53,13 @@ App.post("/upload/midi",
if ((await fromBuffer(Decoded))?.ext !== "mid") if ((await fromBuffer(Decoded))?.ext !== "mid")
return res.status(400).send("Uploaded MIDI file is not a valid MIDI."); return res.status(400).send("Uploaded MIDI file is not a valid MIDI.");
if (!await Song.exists({ where: { ID: req.body.TargetSong } })) const SongData = await Song.findOne({ where: { ID: req.body.TargetSong }, relations: { Author: true } })
if (!SongData)
return res.status(404).send("The song you're trying to upload a MIDI for does not exist."); return res.status(404).send("The song you're trying to upload a MIDI for does not exist.");
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.");
writeFileSync(`./Saved/Songs/${req.body.TargetSong}/Data.mid`, Decoded); writeFileSync(`./Saved/Songs/${req.body.TargetSong}/Data.mid`, Decoded);
res.send(`${FULL_SERVER_ROOT}/song/download/${req.body.TargetSong}/midi.mid`); res.send(`${FULL_SERVER_ROOT}/song/download/${req.body.TargetSong}/midi.mid`);
}); });
@ -112,9 +116,13 @@ App.post("/upload/audio",
if (!["mp3", "m4a", "ogg", "wav"].includes(ext)) if (!["mp3", "m4a", "ogg", "wav"].includes(ext))
return res.status(404).send("Invalid audio file. (supported: mp3, m4a, ogg, wav)"); return res.status(404).send("Invalid audio file. (supported: mp3, m4a, ogg, wav)");
if (!await Song.exists({ where: { ID: req.body.TargetSong } })) const SongData = await Song.findOne({ where: { ID: req.body.TargetSong }, relations: { Author: true } })
if (!SongData)
return res.status(404).send("The song you're trying to upload audio for does not exist."); return res.status(404).send("The song you're trying to upload audio for does not exist.");
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.");
await writeFileSync(`./Saved/Songs/${req.body.TargetSong}/Audio.${ext}`, Decoded); await writeFileSync(`./Saved/Songs/${req.body.TargetSong}/Audio.${ext}`, Decoded);
ffmpeg() ffmpeg()
.input(`./Saved/Songs/${req.body.TargetSong}/Audio.${ext}`) .input(`./Saved/Songs/${req.body.TargetSong}/Audio.${ext}`)
@ -142,9 +150,28 @@ App.post("/upload/audio",
App.post("/submit", App.post("/submit",
RequireAuthentication(), RequireAuthentication(),
ValidateBody(j.object({})), ValidateBody(j.object({
TargetSong: j.string().uuid().required()
})),
async (req, res) => { async (req, res) => {
const SongData = await Song.findOne({ where: { ID: req.body.TargetSong }, relations: { Author: true } })
if (!SongData)
return res.status(404).send("The song you're trying to submit for review does not exist.");
if (SongData.Author.ID !== req.user!.ID)
return res.status(403).send("You don't have permission to submit this song for approval.");
if (!SongData.IsDraft)
return res.status(400).send("This song has already been approved and published.");
if (SongData.DraftAwaitingReview)
return res.status(400).send("You already submitted this song for review.");
SongData.DraftReviewSubmittedAt = new Date();
SongData.DraftAwaitingReview = true;
await SongData.save();
return res.send("Song has been submitted for approval by admins.");
}); });
export default { export default {

View File

@ -3,6 +3,7 @@ import { RequireAuthentication, ValidateBody } from "../Modules/Middleware";
import { Song } from "../Schemas/Song"; import { Song } from "../Schemas/Song";
import { OriginalSparks } from "../Modules/FNUtil"; import { OriginalSparks } from "../Modules/FNUtil";
import j from "joi"; import j from "joi";
import { UserPermissions } from "../Schemas/User";
const App = Router(); const App = Router();
@ -27,9 +28,13 @@ async (req, res) => {
if (req.user!.Library.findIndex(x => x.SongID.toLowerCase() === req.body.SongID.toLowerCase() || x.Overriding.toLowerCase() === req.body.ToOverride.toLowerCase()) !== -1) if (req.user!.Library.findIndex(x => x.SongID.toLowerCase() === req.body.SongID.toLowerCase() || x.Overriding.toLowerCase() === req.body.ToOverride.toLowerCase()) !== -1)
return res.status(400).json({ errorMessage: "This song is already activated." }); return res.status(400).json({ errorMessage: "This song is already activated." });
if (!await Song.exists({ where: { ID: req.body.SongID } })) const SongData = await Song.findOne({ where: { ID: req.body.SongID }, relations: { Author: true } });
if (!SongData)
return res.status(404).json({ errorMessage: "Provided song doesn't exist." }); return res.status(404).json({ errorMessage: "Provided song doesn't exist." });
if (SongData.IsDraft && (req.user!.PermissionLevel < UserPermissions.Administrator && SongData.Author.ID !== req.user!.ID))
return res.status(403).json({ errorMessage: "You cannot subscribe to this track, because it's a draft." });
req.user!.Library.push({ SongID: req.body.SongID.toLowerCase(), Overriding: req.body.ToOverride.toLowerCase() }); req.user!.Library.push({ SongID: req.body.SongID.toLowerCase(), Overriding: req.body.ToOverride.toLowerCase() });
req.user!.save(); req.user!.save();
@ -65,7 +70,7 @@ async (req, res) => {
if (!SongData) if (!SongData)
return res.status(404).json({ errorMessage: "Provided song doesn't exist." }); return res.status(404).json({ errorMessage: "Provided song doesn't exist." });
if (SongData.IsDraft && SongData.Author.ID !== req.user.ID) if (SongData.IsDraft && (req.user.PermissionLevel < UserPermissions.Administrator && SongData.Author.ID !== req.user.ID))
return res.status(403).json({ errorMessage: "You cannot subscribe to this track, because it's a draft." }); return res.status(403).json({ errorMessage: "You cannot subscribe to this track, because it's a draft." });
req.user?.BookmarkedSongs.push(SongData); req.user?.BookmarkedSongs.push(SongData);
@ -93,12 +98,12 @@ async (req, res) => {
App.get("/song/data/:InternalID", App.get("/song/data/:InternalID",
RequireAuthentication(), RequireAuthentication(),
async (req, res) => { async (req, res) => {
const SongData = await Song.findOne({ where: { ID: req.params.InternalID }, relations: { Author: true } }); const SongData = await Song.findOne({ where: { ID: req.body.SongID }, relations: { Author: true } });
if (!SongData) if (!SongData)
return res.status(404).json({ errorMessage: "Provided song doesn't exist." }); return res.status(404).json({ errorMessage: "Provided song doesn't exist." });
if (SongData.IsDraft && SongData.Author.ID !== req.user!.ID) if (SongData.IsDraft && (req.user!.PermissionLevel < UserPermissions.Administrator && SongData.Author.ID !== req.user!.ID))
return res.status(403).json({ errorMessage: "You cannot use this track, because it's a draft." }); return res.status(403).json({ errorMessage: "You cannot subscribe to this track, because it's a draft." });
res.json(SongData.Package()); res.json(SongData.Package());
}) })

View File

@ -68,6 +68,9 @@ export class Song extends BaseEntity {
@Column({ default: false }) @Column({ default: false })
DraftAwaitingReview: boolean; DraftAwaitingReview: boolean;
@Column({ nullable: true })
DraftReviewSubmittedAt?: Date;
@Column() @Column()
CreationDate: Date; CreationDate: Date;

View File

@ -9,7 +9,11 @@ export function Song({ data, children }: { data: any, children?: JSX.Element[] |
<Text sx={{ display: "block", textOverflow: "ellipsis", overflow: "hidden", whiteSpace: "nowrap" }}>{data.ArtistName}</Text> <Text sx={{ display: "block", textOverflow: "ellipsis", overflow: "hidden", whiteSpace: "nowrap" }}>{data.ArtistName}</Text>
<Text sx={{ display: "block", textOverflow: "ellipsis", overflow: "hidden", whiteSpace: "nowrap" }}><b>{data.Name}</b></Text> <Text sx={{ display: "block", textOverflow: "ellipsis", overflow: "hidden", whiteSpace: "nowrap" }}><b>{data.Name}</b></Text>
{ {
data.IsDraft ? <Label variant="danger">Draft - not published</Label> : <></> data.IsDraft ?
data.DraftAwaitingReview ?
<Label variant="attention">Draft - awaiting review</Label> :
<Label variant="danger">Draft - not published</Label> :
<></>
} }
{ {
children ? <Divider /> : <></> children ? <Divider /> : <></>

View File

@ -0,0 +1,22 @@
import { useEffect } from "react";
import { toast } from "react-toastify";
import axios from "axios";
export function AdminSubmissions() {
useEffect(() => {
(async () => {
const Data = await axios.get("/api/admin/");
const Overrides = await axios.get("/api/library/available");
if (Data.status !== 200 || Overrides.status !== 200)
return toast("An error has occured while getting the submitted songs!", { type: "error" });
})();
}, []);
return (
<>
</>
)
}

View File

@ -4,7 +4,6 @@ import { PageHeader } from "@primer/react/drafts";
import { useContext, useEffect, useState } from "react"; import { useContext, useEffect, useState } from "react";
import { SiteContext } from "../utils/State"; import { SiteContext } from "../utils/State";
import { useCookies } from "react-cookie"; import { useCookies } from "react-cookie";
import { useNavigate } from "react-router-dom";
import { Song } from "../components/Song"; import { Song } from "../components/Song";
import axios from "axios"; import axios from "axios";
import { toast } from "react-toastify"; import { toast } from "react-toastify";
@ -18,7 +17,6 @@ export function Profile() {
const [draftsSongs, setDraftsSongs] = useState<unknown[]>([]); const [draftsSongs, setDraftsSongs] = useState<unknown[]>([]);
const [availableOverrides, setAvailableOverrides] = useState<{ Name: string, Template: string }[]>([]); const [availableOverrides, setAvailableOverrides] = useState<{ Name: string, Template: string }[]>([]);
const [overriding, setOverriding] = useState<unknown>({}); const [overriding, setOverriding] = useState<unknown>({});
const navigate = useNavigate();
useEffect(() => { useEffect(() => {
(async () => { (async () => {
@ -131,18 +129,27 @@ export function Profile() {
<Box className="songCategory"> <Box className="songCategory">
{ {
draftsSongs.length >= 1 ? draftsSongs.length >= 1 ?
draftsSongs.map(x => { draftsSongs.map((x, i) => {
return <Song data={x}> return <Song data={x}>
<Button sx={{ width: "100%", marginBottom: 1 }} variant="primary" onClick={() => { setIsActivateDialogOpen(true); setOverriding(x) }} disabled={librarySongs.findIndex(y => y.ID === x.ID) !== -1}>Add to Active</Button> <Button sx={{ width: "100%", marginBottom: 1 }} variant="primary" onClick={() => { setIsActivateDialogOpen(true); setOverriding(x) }} disabled={librarySongs.findIndex(y => y.ID === x.ID) !== -1}>Add to Active</Button>
<Button sx={{ width: "100%", marginBottom: 1 }}>Publish</Button> <Button sx={{ width: "100%", marginBottom: 1 }} onClick={async () => {
const Res = await axios.post("/api/drafts/submit", { TargetSong: x.ID });
if (Res.status === 200) {
x.DraftAwaitingReview = true;
draftsSongs[i] = x;
setDraftsSongs([...draftsSongs]);
}
else
toast(Res.data, { type: "error" });
}}>Publish</Button>
<Button sx={{ width: "100%" }} variant="danger" onClick={async () => { <Button sx={{ width: "100%" }} variant="danger" onClick={async () => {
const Res = await axios.post("/api/drafts/delete", { SongID: x.ID }); const Res = await axios.post("/api/drafts/delete", { TargetSong: x.ID });
if (Res.status === 200) { if (Res.status === 200) {
draftsSongs.splice(draftsSongs.findIndex(y => y.ID === x.ID), 1); draftsSongs.splice(draftsSongs.findIndex(y => y.ID === x.ID), 1);
setDraftsSongs([...draftsSongs]); setDraftsSongs([...draftsSongs]);
} }
else else
toast(Res.data.errorMessage, { type: "error" }) toast(Res.data, { type: "error" });
}}>Unsubscribe</Button> }}>Unsubscribe</Button>
</Song>; </Song>;
}) })

View File

@ -19,7 +19,7 @@ export function TrackSubmission() {
<Text></Text> <Text></Text>
</Box>*/} </Box>*/}
<Heading>Create a New Draft</Heading> <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> <Text>Drafts are private versions of Tracks, only available to you. If you want to publish that track, click the "Publish" button on the management page.</Text>
<form method="GET" action="" ref={formRef}> <form method="GET" action="" ref={formRef}>
<FormControl required={true} sx={formControlStyle}> <FormControl required={true} sx={formControlStyle}>
<FormControl.Label>Song Name</FormControl.Label> <FormControl.Label>Song Name</FormControl.Label>
@ -95,7 +95,7 @@ export function TrackSubmission() {
<FormControl.Caption>This will play in the background of your song. Make sure it was exported from REAPER.</FormControl.Caption> <FormControl.Caption>This will play in the background of your song. Make sure it was exported from REAPER.</FormControl.Caption>
</FormControl> </FormControl>
<FormControl required={true} sx={formControlStyle}> <FormControl required={true} sx={formControlStyle}>
<FormControl.Label>Cover Image (.jpg, .jpeg, .webp, .png)</FormControl.Label> <FormControl.Label>Cover Image (.png)</FormControl.Label>
<TextInput type="file" /> <TextInput type="file" />
<FormControl.Caption>Must be a 1:1 ratio. Max: 2048x2048, min: 512x512</FormControl.Caption> <FormControl.Caption>Must be a 1:1 ratio. Max: 2048x2048, min: 512x512</FormControl.Caption>
</FormControl> </FormControl>
@ -165,16 +165,14 @@ export function TrackSubmission() {
return; return;
const MidiRes = await axios.post("/api/drafts/upload/midi", { Data: Buffer.from(await Midi.arrayBuffer()).toString("hex"), TargetSong: SongData.data.ID }); 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" }); toast(MidiRes.status === 200 ? "Uploaded MIDI chart successfully." : MidiRes.data, { 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 }); 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" }); toast(AudioRes.status === 200 ? "Uploaded audio for processing successfully." : AudioRes.data, { 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 }); 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" }); toast(CoverRes.status === 200 ? "Uploaded cover image successfully." : CoverRes.data, { type: CoverRes.status === 200 ? "success" : "error" });
}}>Create</Button> }}>Create</Button>
</form> </form>
</> </>
) )