This commit is contained in:
McMistrzYT 2024-01-28 22:02:29 +01:00
parent b97048d3fb
commit 017b619766
9 changed files with 124 additions and 47 deletions

View File

@ -7,8 +7,9 @@ import { RequireAuthentication, ValidateBody } from "../Modules/Middleware";
import { writeFileSync } from "fs";
import { ForcedCategory } from "../Schemas/ForcedCategory";
import { fromBuffer } from "file-type";
import { Debug } from "../Modules/Logger";
import { magenta } from "colorette";
import ffmpeg from "fluent-ffmpeg";
import exif from "exif-reader";
import j from "joi";
const App = Router();
@ -75,6 +76,7 @@ async (req, res) => {
});
App.post("/upload/audio",
RequireAuthentication(),
ValidateBody(j.object({
Data: j.string().hex().required(),
TargetSong: j.string().uuid().required()
@ -89,12 +91,22 @@ async (req, res) => {
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);
await writeFileSync(`./Saved/Songs/${req.body.TargetSong}/Audio.${ext}`, Decoded);
ffmpeg()
.input("")
})
.input(`./Saved/Songs/${req.body.TargetSong}/Audio.${ext}`)
.outputOptions([
"-map 0",
"-use_timeline 1",
"-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("/upload/cover",
ValidateBody(j.object({
@ -106,13 +118,13 @@ async (req, res) => {
const ext = (await fromBuffer(Decoded))!.ext;
if (ext !== "png")
return res.status(404).send("Invalid image file. (supported: png)");
return res.status(400).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);
/*const ImageMetadata = exif(Decoded);
if (!ImageMetadata.Image?.ImageWidth || !ImageMetadata.Image?.ImageLength)
throw new Error("Invalid image file.");
@ -120,7 +132,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

@ -9,7 +9,7 @@ App.get("/", async (req, res) => {
const New = {
ID: "new",
Header: "Recently added",
Songs: (await Song.find({ take: 10, order: { CreationDate: "DESC" } })).map(x => x.Package()),
Songs: (await Song.find({ where: { IsDraft: false }, take: 10, order: { CreationDate: "DESC" } })).map(x => x.Package()),
Priority: 100,
Custom: false
}

View File

@ -3,15 +3,23 @@ import { existsSync, readFileSync } from "fs";
import { FULL_SERVER_ROOT } from "../Modules/Constants";
import { CreateBlurl } from "../Modules/BLURL";
import { Song } from "../Schemas/Song";
import { RequireAuthentication } from "../Modules/Middleware";
const App = Router();
App.get("/song/download/:InternalID/:File", async (req, res) => {
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 } });
const SongData = await Song.findOne({ where: { ID: req.params.InternalID }, relations: { Author: true } });
if (!SongData)
return res.status(404).json({ errorMessage: "Song not found." });
console.log(SongData);
if (SongData.IsDraft && SongData.Author.ID !== req.user!.ID)
return res.status(403).json({ errorMessage: "You cannot use this track, because it's a draft." });
const BaseURL = `${FULL_SERVER_ROOT}/song/download/${SongData.ID}/`;
switch (req.params.File.toLowerCase()) {
case "master.blurl":
@ -64,10 +72,13 @@ App.get("/song/download/:InternalID/:File", async (req, res) => {
});
App.get("/:InternalID", async (req, res, next) => {
const SongData = await Song.findOne({ where: { ID: req.params.InternalID } });
const SongData = await Song.findOne({ where: { ID: req.params.InternalID }, relations: { Author: true } });
if (!SongData)
return next(); // trust me bro
if (SongData.IsDraft && SongData.Author.ID !== req.user!.ID)
return res.status(403).json({ errorMessage: "You cannot use this track, because it's a draft." });
const BaseURL = `${FULL_SERVER_ROOT}/song/download/${SongData.ID}/`;
res.set("content-type", "application/json");
res.json({

View File

@ -7,8 +7,9 @@ import { Song } from "../Schemas/Song";
import { Debug } from "../Modules/Logger";
import { magenta } from "colorette";
import { fromBuffer } from "file-type";
import { writeFileSync } from "fs";
import { rmSync, writeFileSync } from "fs";
import { FULL_SERVER_ROOT } from "../Modules/Constants";
import { UserPermissions } from "../Schemas/User";
const App = Router();
@ -32,7 +33,8 @@ App.post("/create",
async (req, res) => {
const SongData = await Song.create({
...req.body,
IsDraft: true
IsDraft: true,
Author: req.user!
}).save();
Debug(`New draft created by ${magenta(req.user!.ID!)} as ${magenta(`${SongData.ArtistName} - ${SongData.Name}`)}`)
@ -71,11 +73,15 @@ App.post("/upload/cover",
if (ext !== "png")
return res.status(404).send("Invalid image file. (supported: png)");
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 cover 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.");
try {
const ImageMetadata = exif(Decoded);
/*const ImageMetadata = exif(Decoded);
if (!ImageMetadata.Image?.ImageWidth || !ImageMetadata.Image?.ImageLength)
throw new Error("Invalid image file.");
@ -83,7 +89,7 @@ App.post("/upload/cover",
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.");
@ -112,19 +118,23 @@ App.post("/upload/audio",
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); })
.on("end", () => {
Debug("Ffmpeg finished running");
rmSync(`./Saved/Songs/${req.body.TargetSong}/Audio.${ext}`);
})
.on("error", (e, stdout, stderr) => {
console.error(e);
console.log(stdout);
console.error(stderr);
rmSync(`./Saved/Songs/${req.body.TargetSong}/Audio.${ext}`);
})
.run();
res.send("ffmpeg now running on song.");

View File

@ -6,9 +6,10 @@ import j from "joi";
const App = Router();
App.get("/me", RequireAuthentication({ BookmarkedSongs: true }), (req, res) => {
App.get("/me", RequireAuthentication({ BookmarkedSongs: true, CreatedTracks: true }), (req, res) => {
res.json({
Bookmarks: req.user?.BookmarkedSongs.map(x => x.Package()),
Created: req.user?.CreatedTracks.map(x => x.Package()),
Library: req.user?.Library
})
})
@ -60,10 +61,13 @@ async (req, res) => {
if (req.user?.BookmarkedSongs.findIndex(x => x.ID.toLowerCase() === req.body.SongID.toLowerCase()) !== -1)
return res.status(400).json({ errorMessage: "You're already subscribed to this song." });
const SongData = await Song.findOne({ 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." });
if (SongData.IsDraft && 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?.BookmarkedSongs.push(SongData);
req.user?.save();
@ -86,10 +90,15 @@ async (req, res) => {
res.json(req.user?.BookmarkedSongs.map(x => x.Package()));
})
App.get("/song/data/:InternalID", async (req, res) => {
const SongData = await Song.findOne({ where: { ID: req.params.InternalID } });
App.get("/song/data/:InternalID",
RequireAuthentication(),
async (req, res) => {
const SongData = await Song.findOne({ where: { ID: req.params.InternalID }, relations: { Author: true } });
if (!SongData)
return res.status(404).json({ errorMessage: "Song not found." });
return res.status(404).json({ errorMessage: "Provided song doesn't exist." });
if (SongData.IsDraft && SongData.Author.ID !== req.user!.ID)
return res.status(403).json({ errorMessage: "You cannot use this track, because it's a draft." });
res.json(SongData.Package());
})

View File

@ -65,6 +65,9 @@ export class Song extends BaseEntity {
@Column()
IsDraft: boolean;
@Column({ default: false })
DraftAwaitingReview: boolean;
@Column()
CreationDate: Date;

View File

@ -8,11 +8,17 @@
"build:prod": "vite build",
"build:stage": "vite build --mode staging",
"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\"",
"win: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\"",
"win: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 && 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\"",
"win: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\"",
"win: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\"",
"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: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",
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
"preview": "vite preview",

View File

@ -1,4 +1,4 @@
import { Box, Text } from "@primer/react";
import { Box, Label, Text } from "@primer/react";
import { Divider } from "@primer/react/lib-esm/ActionList/Divider";
export function Song({ data, children }: { data: any, children?: JSX.Element[] | JSX.Element | string }) {
@ -8,6 +8,9 @@ export function Song({ data, children }: { data: any, children?: JSX.Element[] |
<center>
<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>
{
data.IsDraft ? <Label variant="danger">Draft - not published</Label> : <></>
}
{
children ? <Divider /> : <></>
}

View File

@ -15,6 +15,7 @@ export function Profile() {
const [isActivateDialogOpen, setIsActivateDialogOpen] = useState<boolean>(false);
const [librarySongs, setLibrarySongs] = useState<unknown[]>([]);
const [bookmarkedSongs, setBookmarkedSongs] = useState<unknown[]>([]);
const [draftsSongs, setDraftsSongs] = useState<unknown[]>([]);
const [availableOverrides, setAvailableOverrides] = useState<{ Name: string, Template: string }[]>([]);
const [overriding, setOverriding] = useState<unknown>({});
const navigate = useNavigate();
@ -28,9 +29,9 @@ export function Profile() {
return toast("An error has occured while getting your library!", { type: "error" });
const LibSongs = (await Promise.all(Data.data.Library.map((x: { SongID: string; }) => axios.get(`/api/library/song/data/${x.SongID}`)))).map(x => { return { ...x.data, Override: Data.data.Library.find((y: { SongID: string; }) => y.SongID === x.data.ID).Overriding } });
const BookSongs = (await Promise.all(Data.data.Bookmarks.map((x: { ID: string; }) => axios.get(`/api/library/song/data/${x.ID}`)))).map(x => x.data);
setLibrarySongs(LibSongs);
setBookmarkedSongs(BookSongs);
setBookmarkedSongs(Data.data.Bookmarks);
setDraftsSongs(Data.data.Created);
setAvailableOverrides(Overrides.data);
})();
}, []);
@ -126,6 +127,28 @@ export function Profile() {
: <Text>You have no bookmarked songs.</Text>
}
</Box>
<Heading sx={{ marginTop: 2, marginBottom: 2 }}>My Drafts & Published Songs</Heading>
<Box className="songCategory">
{
draftsSongs.length >= 1 ?
draftsSongs.map(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 }}>Publish</Button>
<Button sx={{ width: "100%" }} variant="danger" onClick={async () => {
const Res = await axios.post("/api/drafts/delete", { SongID: x.ID });
if (Res.status === 200) {
draftsSongs.splice(draftsSongs.findIndex(y => y.ID === x.ID), 1);
setDraftsSongs([...draftsSongs]);
}
else
toast(Res.data.errorMessage, { type: "error" })
}}>Unsubscribe</Button>
</Song>;
})
: <Text>You have no bookmarked songs.</Text>
}
</Box>
</Box> :
<>
<Text>You are not logged in.<br />Log in using the button in the top right.</Text>