diff --git a/Server/Source/Modules/Extensions.ts b/Server/Source/Modules/Extensions.ts new file mode 100644 index 0000000..e82e23b --- /dev/null +++ b/Server/Source/Modules/Extensions.ts @@ -0,0 +1,4 @@ +// https://advancedweb.hu/how-to-use-async-functions-with-array-filter-in-javascript/ +export async function AsyncFilter(arr: unknown[], predicate: (value: unknown, index: number, array: unknown[]) => Promise) { + return Promise.all(arr.map(predicate)).then((results) => arr.filter((_v, index) => results[index])); +} \ No newline at end of file diff --git a/Server/Source/Routes/Debug.ts b/Server/Source/Routes/Debug.ts index 824763b..fe371e6 100644 --- a/Server/Source/Routes/Debug.ts +++ b/Server/Source/Routes/Debug.ts @@ -1,8 +1,9 @@ import { Router } from "express"; import { ENVIRONMENT } from "../Modules/Constants"; import { RequireAuthentication, ValidateBody } from "../Modules/Middleware"; -import { UserPermissions } from "../Schemas/User"; +import { User, UserPermissions } from "../Schemas/User"; import j from "joi"; +import { Song } from "../Schemas/Song"; const App = Router(); @@ -38,6 +39,12 @@ async (req, res) => { res.json(req.user); }) +App.get("/raw/song/:SongID", +async (req, res) => res.json(await Song.findOne({ where: { ID: req.params.SongID } }))); + +App.get("/raw/user/:UserID", +async (req, res) => res.json(await User.findOne({ where: { ID: req.params.UserID } }))); + export default { App, DefaultAPI: "/api/debug" diff --git a/Server/Source/Routes/Drafting.ts b/Server/Source/Routes/Drafting.ts index 05d842b..b726826 100644 --- a/Server/Source/Routes/Drafting.ts +++ b/Server/Source/Routes/Drafting.ts @@ -1,6 +1,7 @@ import j from "joi"; import ffmpeg from "fluent-ffmpeg"; import sizeOf from "image-size"; +import cron from "node-cron"; import { Router } from "express"; import { RequireAuthentication, ValidateBody } from "../Modules/Middleware"; import { Song, SongStatus } from "../Schemas/Song"; @@ -11,6 +12,21 @@ import { rmSync, writeFileSync, renameSync, readFileSync } from "fs"; import { FULL_SERVER_ROOT, MAX_AMOUNT_OF_DRAFTS_AT_ONCE } from "../Modules/Constants"; import { UserPermissions } from "../Schemas/User"; +cron.schedule("*/2 * * * *", async () => { + Debug("Running cron schedule to check for broken drafts.") + const EligibleSongs = await Song.find({ where: { IsDraft: true, Status: SongStatus.PROCESSING } }); + for (const SongData of EligibleSongs) { + if (SongData.HasMidi && SongData.HasCover && SongData.HasAudio) + continue; + + if (SongData.CreationDate.getTime() + 60 * 1000 > Date.now()) + continue; + + SongData.Status = SongStatus.BROKEN; + await SongData.save(); + } +}); + const App = Router(); App.post("/create", @@ -64,11 +80,22 @@ App.post("/upload/midi", 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."); + if (SongData.HasMidi) { + 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/Songs/${req.body.TargetSong}/Data.mid`); + SongData.HasMidi = false; + SongData.IsDraft = true; + await SongData.save(); + } + writeFileSync(`./Saved/Songs/${req.body.TargetSong}/Data.mid`, Decoded); res.send(`${FULL_SERVER_ROOT}/song/download/${req.body.TargetSong}/midi.mid`); await SongData.reload(); SongData.HasMidi = true; + SongData.Status = SongData.HasMidi && SongData.HasCover && SongData.HasAudio ? SongStatus.DEFAULT : SongData.Status; await SongData.save(); }); @@ -92,6 +119,16 @@ App.post("/upload/cover", 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."); + if (SongData.HasCover) { + 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/Songs/${req.body.TargetSong}/Cover.png`); + SongData.HasCover = false; + SongData.IsDraft = true; + await SongData.save(); + } + try { const ImageSize = sizeOf(Decoded); if (!ImageSize.height || !ImageSize.width) @@ -118,6 +155,7 @@ App.post("/upload/cover", await SongData.reload(); SongData.HasCover = true; + SongData.Status = SongData.HasMidi && SongData.HasCover && SongData.HasAudio ? SongStatus.DEFAULT : SongData.Status; await SongData.save(); writeFileSync(`./Saved/Songs/${req.body.TargetSong}/Cover.png`, Decoded); @@ -150,6 +188,7 @@ App.post("/upload/audio", rmSync(`./Saved/Songs/${req.body.TargetSong}/Chunks`, { recursive: true }); SongData.HasAudio = false; + SongData.IsDraft = true; SongData.Status = SongStatus.PROCESSING; await SongData.save(); } @@ -158,7 +197,6 @@ App.post("/upload/audio", ffmpeg() .input(`./Saved/Songs/${req.body.TargetSong}/Audio.${ext}`) .outputOptions([ - "-map 0", "-use_timeline 1", "-f dash" ]) diff --git a/Server/Source/Routes/Library.ts b/Server/Source/Routes/Library.ts index a67732a..2d873c7 100644 --- a/Server/Source/Routes/Library.ts +++ b/Server/Source/Routes/Library.ts @@ -4,14 +4,26 @@ import { Song, SongStatus } from "../Schemas/Song"; import { OriginalSparks } from "../Modules/FNUtil"; import j from "joi"; import { UserPermissions } from "../Schemas/User"; +import { AsyncFilter } from "../Modules/Extensions"; const App = Router(); App.get("/me", RequireAuthentication({ BookmarkedSongs: true, CreatedTracks: true }), async (req, res) => { const ProcessingTracks = req.user!.CreatedTracks.filter(x => x.Status === SongStatus.PROCESSING); + // @ts-expect-error not gonna bother making type + const NonExistingActiveTracks = await AsyncFilter(req.user!.Library, async x => !(await Song.exists({ where: { ID: x.SongID } }))); + + if (NonExistingActiveTracks.length > 0) { + for (const Track of NonExistingActiveTracks) { + console.log(Track); + // @ts-expect-error again not gonna bother making type + req.user!.Library.splice(req.user!.Library.findIndex(x => x.SongID === Track.SongID), 1); + } + await req.user!.save(); + } + if (ProcessingTracks.length > 0) for (const Track of ProcessingTracks) { - console.log(Track.HasAudio, Track.HasMidi, Track.HasCover) if (!Track.HasAudio || !Track.HasMidi || !Track.HasCover) continue; diff --git a/Server/package-lock.json b/Server/package-lock.json index 174f0ec..106e791 100644 --- a/Server/package-lock.json +++ b/Server/package-lock.json @@ -9,6 +9,7 @@ "version": "1.0.0", "license": "ISC", "dependencies": { + "@types/node-cron": "^3.0.11", "axios": "^1.6.5", "better-sqlite3": "^9.3.0", "colorette": "^2.0.20", @@ -22,6 +23,7 @@ "image-size": "^1.1.1", "joi": "^17.12.0", "jsonwebtoken": "^9.0.2", + "node-cron": "^3.0.3", "typeorm": "^0.3.19", "underscore": "^1.13.6", "uuid": "^9.0.1" @@ -337,6 +339,11 @@ "resolved": "https://registry.npmjs.org/@types/node/-/node-20.8.2.tgz", "integrity": "sha512-Vvycsc9FQdwhxE3y3DzeIxuEJbWGDsnrxvMADzTDF/lcdR9/K+AQIeAghTQsHtotg/q0j3WEOYS/jQgSdWue3w==" }, + "node_modules/@types/node-cron": { + "version": "3.0.11", + "resolved": "https://registry.npmjs.org/@types/node-cron/-/node-cron-3.0.11.tgz", + "integrity": "sha512-0ikrnug3/IyneSHqCBeslAhlK2aBfYek1fGo4bP4QnZPmiqSGRK+Oy7ZMisLWkesffJvQ1cqAcBnJC+8+nxIAg==" + }, "node_modules/@types/qs": { "version": "6.9.8", "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.8.tgz", @@ -1756,6 +1763,25 @@ "node": ">=10" } }, + "node_modules/node-cron": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/node-cron/-/node-cron-3.0.3.tgz", + "integrity": "sha512-dOal67//nohNgYWb+nWmg5dkFdIwDm8EpeGYMekPMrngV3637lqnX0lbUcCtgibHTz6SEz7DAIjKvKDFYCnO1A==", + "dependencies": { + "uuid": "8.3.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/node-cron/node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "bin": { + "uuid": "dist/bin/uuid" + } + }, "node_modules/object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", diff --git a/Server/package.json b/Server/package.json index 9cb9e51..2f096f4 100644 --- a/Server/package.json +++ b/Server/package.json @@ -41,6 +41,7 @@ "typescript": "^5.2.2" }, "dependencies": { + "@types/node-cron": "^3.0.11", "axios": "^1.6.5", "better-sqlite3": "^9.3.0", "colorette": "^2.0.20", @@ -54,6 +55,7 @@ "image-size": "^1.1.1", "joi": "^17.12.0", "jsonwebtoken": "^9.0.2", + "node-cron": "^3.0.3", "typeorm": "^0.3.19", "underscore": "^1.13.6", "uuid": "^9.0.1" diff --git a/src/assets/NoCoverDetected.png b/src/assets/NoCoverDetected.png new file mode 100644 index 0000000..337e523 Binary files /dev/null and b/src/assets/NoCoverDetected.png differ diff --git a/src/components/Song.tsx b/src/components/Song.tsx index bcbc169..5b7c5f5 100644 --- a/src/components/Song.tsx +++ b/src/components/Song.tsx @@ -2,6 +2,7 @@ import { Box, Label, Text } from "@primer/react"; import { Divider } from "@primer/react/lib-esm/ActionList/Divider"; import { SongStatus } from "../utils/Extensions"; import { LabelColorOptions } from "@primer/react/lib-esm/Label/Label"; +import DefaultCover from "../assets/NoCoverDetected.png"; export function Song({ data, children }: { data: any, children?: JSX.Element[] | JSX.Element | string }) { function GetStatusLabel() { @@ -50,7 +51,7 @@ export function Song({ data, children }: { data: any, children?: JSX.Element[] | return ( - + (e.target as HTMLImageElement).src = DefaultCover} src={data.Cover} style={{ width: "100%", borderRadius: 10 }} />
{data.ArtistName} {data.Name} diff --git a/src/css/index.css b/src/css/index.css index 0156153..3b4c531 100644 --- a/src/css/index.css +++ b/src/css/index.css @@ -7,6 +7,9 @@ .songCategory { display: inline-flex; + flex: none; + overflow-x: auto; + overflow-y: hidden; gap: 10px; } diff --git a/src/routes/Home.tsx b/src/routes/Home.tsx index 0973d1e..0e35844 100644 --- a/src/routes/Home.tsx +++ b/src/routes/Home.tsx @@ -1,12 +1,11 @@ import { Box, Text } from "@primer/react"; -import { EULA } from "./EULA"; export function Home() { return ( <> - Welcome to the Online Test 1 - + online test 2 please kill me thanks + CLICK THE LOGIN ICON ON THE TOP RIGHT ) diff --git a/src/routes/Profile.tsx b/src/routes/Profile.tsx index b9886f3..5e67b25 100644 --- a/src/routes/Profile.tsx +++ b/src/routes/Profile.tsx @@ -1,3 +1,5 @@ +import axios from "axios"; +import { Buffer } from "buffer/"; import { ActionList, ActionMenu, Avatar, Box, Button, Dialog, FormControl, Heading, Text, TextInput } from "@primer/react" import { Divider } from "@primer/react/lib-esm/ActionList/Divider"; import { PageHeader } from "@primer/react/drafts"; @@ -5,7 +7,6 @@ import { useContext, useEffect, useRef, useState } from "react"; import { SiteContext } from "../utils/State"; import { useCookies } from "react-cookie"; import { Song } from "../components/Song"; -import axios from "axios"; import { toast } from "react-toastify"; import { SongStatus } from "../utils/Extensions"; @@ -82,27 +83,42 @@ export function Profile() { }
- + MIDI File (.mid) You can use the #tools-and-resources channel to find useful resources on how to create MIDIs. - + Audio File (.m4a, .mp3, .wav) This will play in the background of your song. Make sure it was exported from REAPER. - + Cover Image (.png) Must be a 1:1 ratio. Max: 2048x2048, min: 512x512 - diff --git a/src/routes/TrackSubmission.tsx b/src/routes/TrackSubmission.tsx index 8d7f361..80e4970 100644 --- a/src/routes/TrackSubmission.tsx +++ b/src/routes/TrackSubmission.tsx @@ -8,6 +8,7 @@ const formControlStyle = { paddingTop: 3 }; export function TrackSubmission() { const formRef = useRef(null); + const [waiting, setWaiting] = useState(false); const [Key, setKey] = useState("Select a key..."); const [Scale, setScale] = useState("Select a scale..."); const [GuitarStarterType, setGuitarStarterType] = useState("Select the starter type..."); @@ -119,7 +120,8 @@ export function TrackSubmission() { Ranges from 0-6 - + setWaiting(false); + toast("Finished processing song. You can now find it in your profile tab."); + }}>{waiting ? "Please wait..." : "Create"} )